From 74ca21a41bcb5a49d19e65c9ba88f1f864cb7095 Mon Sep 17 00:00:00 2001 From: "Rafael G. Martins" Date: Sat, 3 Sep 2016 19:57:54 +0200 Subject: *: big code reorganization. - source and tests are now splitted by target - utils lib is now called common still pending move error.c from blogc to common --- Makefile.am | 193 ++- src/blogc-git-receiver.c | 501 -------- src/blogc-git-receiver/main.c | 501 ++++++++ src/blogc-runserver.c | 386 ------ src/blogc-runserver/main.c | 386 ++++++ src/blogc.c | 297 ----- src/blogc/content-parser.c | 1264 ++++++++++++++++++++ src/blogc/content-parser.h | 23 + src/blogc/datetime-parser.c | 386 ++++++ src/blogc/datetime-parser.h | 17 + src/blogc/debug.c | 80 ++ src/blogc/debug.h | 16 + src/blogc/error.c | 139 +++ src/blogc/error.h | 34 + src/blogc/file.c | 81 ++ src/blogc/file.h | 21 + src/blogc/loader.c | 212 ++++ src/blogc/loader.h | 21 + src/blogc/main.c | 297 +++++ src/blogc/renderer.c | 457 ++++++++ src/blogc/renderer.h | 24 + src/blogc/source-parser.c | 218 ++++ src/blogc/source-parser.h | 19 + src/blogc/template-parser.c | 679 +++++++++++ src/blogc/template-parser.h | 53 + src/common/utf8.c | 102 ++ src/common/utf8.h | 21 + src/common/utils.c | 645 ++++++++++ src/common/utils.h | 102 ++ src/content-parser.c | 1264 -------------------- src/content-parser.h | 23 - src/datetime-parser.c | 386 ------ src/datetime-parser.h | 17 - src/debug.c | 80 -- src/debug.h | 16 - src/error.c | 139 --- src/error.h | 34 - src/file.c | 81 -- src/file.h | 21 - src/loader.c | 212 ---- src/loader.h | 21 - src/renderer.c | 457 -------- src/renderer.h | 24 - src/source-parser.c | 218 ---- src/source-parser.h | 19 - src/template-parser.c | 679 ----------- src/template-parser.h | 53 - src/utf8.c | 102 -- src/utf8.h | 21 - src/utils.c | 645 ---------- src/utils.h | 102 -- tests/blogc/check_content_parser.c | 2208 +++++++++++++++++++++++++++++++++++ tests/blogc/check_datetime_parser.c | 671 +++++++++++ tests/blogc/check_error.c | 109 ++ tests/blogc/check_loader.c | 771 ++++++++++++ tests/blogc/check_renderer.c | 1158 ++++++++++++++++++ tests/blogc/check_source_parser.c | 542 +++++++++ tests/blogc/check_template_parser.c | 1184 +++++++++++++++++++ tests/check_content_parser.c | 2208 ----------------------------------- tests/check_datetime_parser.c | 671 ----------- tests/check_error.c | 109 -- tests/check_loader.c | 771 ------------ tests/check_renderer.c | 1158 ------------------ tests/check_source_parser.c | 542 --------- tests/check_template_parser.c | 1184 ------------------- tests/check_utf8.c | 101 -- tests/check_utils.c | 992 ---------------- tests/common/check_utf8.c | 101 ++ tests/common/check_utils.c | 992 ++++++++++++++++ 69 files changed, 13628 insertions(+), 13633 deletions(-) delete mode 100644 src/blogc-git-receiver.c create mode 100644 src/blogc-git-receiver/main.c delete mode 100644 src/blogc-runserver.c create mode 100644 src/blogc-runserver/main.c delete mode 100644 src/blogc.c create mode 100644 src/blogc/content-parser.c create mode 100644 src/blogc/content-parser.h create mode 100644 src/blogc/datetime-parser.c create mode 100644 src/blogc/datetime-parser.h create mode 100644 src/blogc/debug.c create mode 100644 src/blogc/debug.h create mode 100644 src/blogc/error.c create mode 100644 src/blogc/error.h create mode 100644 src/blogc/file.c create mode 100644 src/blogc/file.h create mode 100644 src/blogc/loader.c create mode 100644 src/blogc/loader.h create mode 100644 src/blogc/main.c create mode 100644 src/blogc/renderer.c create mode 100644 src/blogc/renderer.h create mode 100644 src/blogc/source-parser.c create mode 100644 src/blogc/source-parser.h create mode 100644 src/blogc/template-parser.c create mode 100644 src/blogc/template-parser.h create mode 100644 src/common/utf8.c create mode 100644 src/common/utf8.h create mode 100644 src/common/utils.c create mode 100644 src/common/utils.h delete mode 100644 src/content-parser.c delete mode 100644 src/content-parser.h delete mode 100644 src/datetime-parser.c delete mode 100644 src/datetime-parser.h delete mode 100644 src/debug.c delete mode 100644 src/debug.h delete mode 100644 src/error.c delete mode 100644 src/error.h delete mode 100644 src/file.c delete mode 100644 src/file.h delete mode 100644 src/loader.c delete mode 100644 src/loader.h delete mode 100644 src/renderer.c delete mode 100644 src/renderer.h delete mode 100644 src/source-parser.c delete mode 100644 src/source-parser.h delete mode 100644 src/template-parser.c delete mode 100644 src/template-parser.h delete mode 100644 src/utf8.c delete mode 100644 src/utf8.h delete mode 100644 src/utils.c delete mode 100644 src/utils.h create mode 100644 tests/blogc/check_content_parser.c create mode 100644 tests/blogc/check_datetime_parser.c create mode 100644 tests/blogc/check_error.c create mode 100644 tests/blogc/check_loader.c create mode 100644 tests/blogc/check_renderer.c create mode 100644 tests/blogc/check_source_parser.c create mode 100644 tests/blogc/check_template_parser.c delete mode 100644 tests/check_content_parser.c delete mode 100644 tests/check_datetime_parser.c delete mode 100644 tests/check_error.c delete mode 100644 tests/check_loader.c delete mode 100644 tests/check_renderer.c delete mode 100644 tests/check_source_parser.c delete mode 100644 tests/check_template_parser.c delete mode 100644 tests/check_utf8.c delete mode 100644 tests/check_utils.c create mode 100644 tests/common/check_utf8.c create mode 100644 tests/common/check_utils.c diff --git a/Makefile.am b/Makefile.am index 2582881..179321c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -32,22 +32,22 @@ BUILT_SOURCES = \ $(NULL) noinst_HEADERS = \ - src/content-parser.h \ - src/datetime-parser.h \ - src/debug.h \ - src/error.h \ - src/file.h \ - src/loader.h \ - src/renderer.h \ - src/source-parser.h \ - src/template-parser.h \ - src/utf8.h \ - src/utils.h \ + src/blogc/content-parser.h \ + src/blogc/datetime-parser.h \ + src/blogc/debug.h \ + src/blogc/error.h \ + src/blogc/file.h \ + src/blogc/loader.h \ + src/blogc/renderer.h \ + src/blogc/source-parser.h \ + src/blogc/template-parser.h \ + src/common/utf8.h \ + src/common/utils.h \ $(NULL) noinst_LTLIBRARIES = \ libblogc.la \ - libblogc_utils.la \ + libblogc_common.la \ $(NULL) noinst_PROGRAMS = \ @@ -74,19 +74,18 @@ check_PROGRAMS = \ libblogc_la_SOURCES = \ - src/content-parser.c \ - src/datetime-parser.c \ - src/error.c \ - src/file.c \ - src/loader.c \ - src/renderer.c \ - src/source-parser.c \ - src/template-parser.c \ + src/blogc/content-parser.c \ + src/blogc/datetime-parser.c \ + src/blogc/error.c \ + src/blogc/file.c \ + src/blogc/loader.c \ + src/blogc/renderer.c \ + src/blogc/source-parser.c \ + src/blogc/template-parser.c \ $(NULL) libblogc_la_CFLAGS = \ $(AM_CFLAGS) \ - -I$(top_srcdir)/src \ $(NULL) libblogc_la_LIBADD = \ @@ -94,57 +93,53 @@ libblogc_la_LIBADD = \ $(NULL) -libblogc_utils_la_SOURCES = \ - src/utf8.c \ - src/utils.c \ +libblogc_common_la_SOURCES = \ + src/common/utf8.c \ + src/common/utils.c \ $(NULL) -libblogc_utils_la_CFLAGS = \ +libblogc_common_la_CFLAGS = \ $(AM_CFLAGS) \ - -I$(top_srcdir)/src \ $(NULL) blogc_SOURCES = \ - src/blogc.c \ - src/debug.c \ + src/blogc/main.c \ + src/blogc/debug.c \ $(NULL) blogc_CFLAGS = \ $(AM_CFLAGS) \ - -I$(top_srcdir)/src \ $(NULL) blogc_LDADD = \ libblogc.la \ - libblogc_utils.la \ + libblogc_common.la \ $(NULL) if BUILD_GIT_RECEIVER blogc_git_receiver_SOURCES = \ - src/blogc-git-receiver.c \ + src/blogc-git-receiver/main.c \ $(NULL) blogc_git_receiver_CFLAGS = \ $(AM_CFLAGS) \ - -I$(top_srcdir)/src \ $(NULL) blogc_git_receiver_LDADD = \ - libblogc_utils.la \ + libblogc_common.la \ $(NULL) endif if BUILD_RUNSERVER blogc_runserver_SOURCES = \ - src/blogc-runserver.c \ + src/blogc-runserver/main.c \ $(NULL) blogc_runserver_CFLAGS = \ $(AM_CFLAGS) \ - -I$(top_srcdir)/src \ $(LIBEVENT_CFLAGS) \ $(SQUAREBALL_CFLAGS) \ $(NULL) @@ -152,7 +147,7 @@ blogc_runserver_CFLAGS = \ blogc_runserver_LDADD = \ $(LIBEVENT_LIBS) \ $(MAGIC_LIBS) \ - libblogc_utils.la \ + libblogc_common.la \ $(NULL) endif @@ -272,177 +267,177 @@ endif if USE_CMOCKA check_PROGRAMS += \ - tests/check_content_parser \ - tests/check_datetime_parser \ - tests/check_error \ - tests/check_loader \ - tests/check_renderer \ - tests/check_source_parser \ - tests/check_template_parser \ - tests/check_utf8 \ - tests/check_utils \ + tests/blogc/check_content_parser \ + tests/blogc/check_datetime_parser \ + tests/blogc/check_error \ + tests/blogc/check_loader \ + tests/blogc/check_renderer \ + tests/blogc/check_source_parser \ + tests/blogc/check_template_parser \ + tests/common/check_utf8 \ + tests/common/check_utils \ $(NULL) -tests_check_error_SOURCES = \ - tests/check_error.c \ +tests_blogc_check_error_SOURCES = \ + tests/blogc/check_error.c \ $(NULL) -tests_check_error_CFLAGS = \ +tests_blogc_check_error_CFLAGS = \ $(CMOCKA_CFLAGS) \ $(NULL) -tests_check_error_LDFLAGS = \ +tests_blogc_check_error_LDFLAGS = \ -no-install \ $(NULL) -tests_check_error_LDADD = \ +tests_blogc_check_error_LDADD = \ $(CMOCKA_LIBS) \ libblogc.la \ - libblogc_utils.la \ + libblogc_common.la \ $(NULL) -tests_check_loader_SOURCES = \ - tests/check_loader.c \ +tests_blogc_check_loader_SOURCES = \ + tests/blogc/check_loader.c \ $(NULL) -tests_check_loader_CFLAGS = \ +tests_blogc_check_loader_CFLAGS = \ $(CMOCKA_CFLAGS) \ $(NULL) -tests_check_loader_LDFLAGS = \ +tests_blogc_check_loader_LDFLAGS = \ -no-install \ -Wl,--wrap=blogc_file_get_contents \ -Wl,--wrap=blogc_fprintf \ $(NULL) -tests_check_loader_LDADD = \ +tests_blogc_check_loader_LDADD = \ $(CMOCKA_LIBS) \ libblogc.la \ - libblogc_utils.la \ + libblogc_common.la \ $(NULL) -tests_check_content_parser_SOURCES = \ - tests/check_content_parser.c \ +tests_blogc_check_content_parser_SOURCES = \ + tests/blogc/check_content_parser.c \ $(NULL) -tests_check_content_parser_CFLAGS = \ +tests_blogc_check_content_parser_CFLAGS = \ $(CMOCKA_CFLAGS) \ $(NULL) -tests_check_content_parser_LDFLAGS = \ +tests_blogc_check_content_parser_LDFLAGS = \ -no-install \ $(NULL) -tests_check_content_parser_LDADD = \ +tests_blogc_check_content_parser_LDADD = \ $(CMOCKA_LIBS) \ libblogc.la \ - libblogc_utils.la \ + libblogc_common.la \ $(NULL) -tests_check_datetime_parser_SOURCES = \ - tests/check_datetime_parser.c \ +tests_blogc_check_datetime_parser_SOURCES = \ + tests/blogc/check_datetime_parser.c \ $(NULL) -tests_check_datetime_parser_CFLAGS = \ +tests_blogc_check_datetime_parser_CFLAGS = \ $(CMOCKA_CFLAGS) \ $(NULL) -tests_check_datetime_parser_LDFLAGS = \ +tests_blogc_check_datetime_parser_LDFLAGS = \ -no-install \ $(NULL) -tests_check_datetime_parser_LDADD = \ +tests_blogc_check_datetime_parser_LDADD = \ $(CMOCKA_LIBS) \ libblogc.la \ - libblogc_utils.la \ + libblogc_common.la \ $(NULL) -tests_check_renderer_SOURCES = \ - tests/check_renderer.c \ +tests_blogc_check_renderer_SOURCES = \ + tests/blogc/check_renderer.c \ $(NULL) -tests_check_renderer_CFLAGS = \ +tests_blogc_check_renderer_CFLAGS = \ $(CMOCKA_CFLAGS) \ $(NULL) -tests_check_renderer_LDFLAGS = \ +tests_blogc_check_renderer_LDFLAGS = \ -no-install \ $(NULL) -tests_check_renderer_LDADD = \ +tests_blogc_check_renderer_LDADD = \ $(CMOCKA_LIBS) \ libblogc.la \ - libblogc_utils.la \ + libblogc_common.la \ $(NULL) -tests_check_source_parser_SOURCES = \ - tests/check_source_parser.c \ +tests_blogc_check_source_parser_SOURCES = \ + tests/blogc/check_source_parser.c \ $(NULL) -tests_check_source_parser_CFLAGS = \ +tests_blogc_check_source_parser_CFLAGS = \ $(CMOCKA_CFLAGS) \ $(NULL) -tests_check_source_parser_LDFLAGS = \ +tests_blogc_check_source_parser_LDFLAGS = \ -no-install \ $(NULL) -tests_check_source_parser_LDADD = \ +tests_blogc_check_source_parser_LDADD = \ $(CMOCKA_LIBS) \ libblogc.la \ - libblogc_utils.la \ + libblogc_common.la \ $(NULL) -tests_check_template_parser_SOURCES = \ - tests/check_template_parser.c \ +tests_blogc_check_template_parser_SOURCES = \ + tests/blogc/check_template_parser.c \ $(NULL) -tests_check_template_parser_CFLAGS = \ +tests_blogc_check_template_parser_CFLAGS = \ $(CMOCKA_CFLAGS) \ $(NULL) -tests_check_template_parser_LDFLAGS = \ +tests_blogc_check_template_parser_LDFLAGS = \ -no-install \ $(NULL) -tests_check_template_parser_LDADD = \ +tests_blogc_check_template_parser_LDADD = \ $(CMOCKA_LIBS) \ libblogc.la \ - libblogc_utils.la \ + libblogc_common.la \ $(NULL) -tests_check_utf8_SOURCES = \ - tests/check_utf8.c \ +tests_common_check_utf8_SOURCES = \ + tests/common/check_utf8.c \ $(NULL) -tests_check_utf8_CFLAGS = \ +tests_common_check_utf8_CFLAGS = \ $(CMOCKA_CFLAGS) \ $(NULL) -tests_check_utf8_LDFLAGS = \ +tests_common_check_utf8_LDFLAGS = \ -no-install \ $(NULL) -tests_check_utf8_LDADD = \ +tests_common_check_utf8_LDADD = \ $(CMOCKA_LIBS) \ - libblogc_utils.la \ + libblogc_common.la \ $(NULL) -tests_check_utils_SOURCES = \ - tests/check_utils.c \ +tests_common_check_utils_SOURCES = \ + tests/common/check_utils.c \ $(NULL) -tests_check_utils_CFLAGS = \ +tests_common_check_utils_CFLAGS = \ $(CMOCKA_CFLAGS) \ $(NULL) -tests_check_utils_LDFLAGS = \ +tests_common_check_utils_LDFLAGS = \ -no-install \ $(NULL) -tests_check_utils_LDADD = \ +tests_common_check_utils_LDADD = \ $(CMOCKA_LIBS) \ - libblogc_utils.la \ + libblogc_common.la \ $(NULL) endif diff --git a/src/blogc-git-receiver.c b/src/blogc-git-receiver.c deleted file mode 100644 index 820fce5..0000000 --- a/src/blogc-git-receiver.c +++ /dev/null @@ -1,501 +0,0 @@ -/* - * blogc: A blog compiler. - * Copyright (C) 2015-2016 Rafael G. Martins - * - * This program can be distributed under the terms of the BSD License. - * See the file LICENSE. - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "utils.h" - -#ifndef BUFFER_SIZE -#define BUFFER_SIZE 4096 -#endif - - -static unsigned int -cpu_count(void) -{ -#ifdef _SC_NPROCESSORS_ONLN - long num = sysconf(_SC_NPROCESSORS_ONLN); - if (num >= 1) - return (unsigned int) num; -#endif - return 1; -} - - -static void -rmdir_recursive(const char *dir) -{ - struct stat buf; - if (0 != stat(dir, &buf)) { - fprintf(stderr, "warning: failed to remove directory (%s): %s\n", dir, - strerror(errno)); - return; - } - if (!S_ISDIR(buf.st_mode)) { - fprintf(stderr, "error: trying to remove invalid directory: %s\n", dir); - exit(2); - } - DIR *d = opendir(dir); - if (d == NULL) { - fprintf(stderr, "error: failed to open directory: %s\n", - strerror(errno)); - exit(2); - } - struct dirent *e; - while (NULL != (e = readdir(d))) { - if ((0 == strcmp(e->d_name, ".")) || (0 == strcmp(e->d_name, ".."))) - continue; - char *f = sb_strdup_printf("%s/%s", dir, e->d_name); - if (0 != stat(f, &buf)) { - fprintf(stderr, "error: failed to stat directory entry (%s): %s\n", - e->d_name, strerror(errno)); - free(f); - exit(2); - } - if (S_ISDIR(buf.st_mode)) { - rmdir_recursive(f); - } - else if (0 != unlink(f)) { - fprintf(stderr, "error: failed to remove file (%s): %s\n", f, - strerror(errno)); - free(f); - exit(2); - } - free(f); - } - if (0 != closedir(d)) { - fprintf(stderr, "error: failed to close directory: %s\n", - strerror(errno)); - exit(2); - } - if (0 != rmdir(dir)) { - fprintf(stderr, "error: failed to remove directory: %s\n", - strerror(errno)); - exit(2); - } -} - - -static int -git_shell(int argc, char *argv[]) -{ - int rv = 0; - - char *repo = NULL; - char *command_orig = NULL; - char *command_name = NULL; - char command_new[BUFFER_SIZE]; - - bool exec_git = false; - - // validate git command - size_t len = strlen(argv[2]); - if (!((len > 17 && (0 == strncmp(argv[2], "git-receive-pack ", 17))) || - (len > 16 && (0 == strncmp(argv[2], "git-upload-pack ", 16))) || - (len > 19 && (0 == strncmp(argv[2], "git-upload-archive ", 19))))) - { - fprintf(stderr, "error: unsupported git command: %s\n", argv[2]); - rv = 1; - goto cleanup; - } - - // get shell path - char *self = getenv("SHELL"); - if (self == NULL) { - fprintf(stderr, "error: failed to find blogc-git-receiver path\n"); - rv = 1; - goto cleanup; - } - - // get home path - char *home = getenv("HOME"); - if (home == NULL) { - fprintf(stderr, "error: failed to find user home path\n"); - rv = 1; - goto cleanup; - } - - // get git repository - command_orig = sb_strdup(argv[2]); - char *p, *r; - for (p = command_orig; *p != ' ' && *p != '\0'; p++); - if (*p == ' ') - p++; - if (*p == '\'' || *p == '"') - p++; - if (*p == '/') - p++; - for (r = p; *p != '\'' && *p != '"' && *p != '\0'; p++); - if (*p == '\'' || *p == '"') - *p = '\0'; - if (*--p == '/') - *p = '\0'; - - repo = sb_strdup_printf("repos/%s", r); - - // check if repository is sane - if (0 == strlen(repo)) { - fprintf(stderr, "error: invalid repository\n"); - rv = 1; - goto cleanup; - } - - if (0 == strncmp(argv[2], "git-upload-", 11)) // no need to check len here - goto git_exec; - - if (0 != chdir(home)) { - fprintf(stderr, "error: failed to chdir (%s): %s\n", home, - strerror(errno)); - rv = 1; - goto cleanup; - } - - if (0 != access(repo, F_OK)) { - char *git_init_cmd = sb_strdup_printf( - "git init --bare \"%s\" > /dev/null", repo); - if (0 != system(git_init_cmd)) { - fprintf(stderr, "error: failed to create git repository: %s\n", - repo); - rv = 1; - free(git_init_cmd); - goto cleanup; - } - free(git_init_cmd); - } - - if (0 != chdir(repo)) { - fprintf(stderr, "error: failed to chdir (%s/%s): %s\n", home, repo, - strerror(errno)); - rv = 1; - goto cleanup; - } - - if (0 != access("hooks", F_OK)) { - // openwrt git package won't install git templates, then the git - // repositories created with it won't have the hooks/ directory. - if (0 != mkdir("hooks", 0777)) { // mkdir honors umask for us. - fprintf(stderr, "error: failed to create directory (%s/%s/hooks): " - "%s\n", home, repo, strerror(errno)); - rv = 1; - goto cleanup; - } - } - - if (0 != chdir("hooks")) { - fprintf(stderr, "error: failed to chdir (%s/%s/hooks): %s\n", home, - repo, strerror(errno)); - rv = 1; - goto cleanup; - } - - if (0 == access("pre-receive", F_OK)) { - if (0 != unlink("pre-receive")) { - fprintf(stderr, "error: failed to remove old symlink " - "(%s/%s/hooks/pre-receive): %s\n", home, repo, strerror(errno)); - rv = 1; - goto cleanup; - } - } - - if (0 != symlink(self, "pre-receive")) { - fprintf(stderr, "error: failed to create symlink " - "(%s/%s/hooks/pre-receive): %s\n", home, repo, strerror(errno)); - rv = 1; - goto cleanup; - } - - if (0 == access("post-receive", F_OK)) { - if (0 != unlink("post-receive")) { - fprintf(stderr, "error: failed to remove old symlink " - "(%s/%s/hooks/post-receive): %s\n", home, repo, strerror(errno)); - rv = 1; - goto cleanup; - } - } - - if (0 != symlink(self, "post-receive")) { - fprintf(stderr, "error: failed to create symlink " - "(%s/%s/hooks/post-receive): %s\n", home, repo, strerror(errno)); - rv = 1; - goto cleanup; - } - - if (0 != chdir(home)) { - fprintf(stderr, "error: failed to chdir (%s): %s\n", home, - strerror(errno)); - rv = 1; - goto cleanup; - } - -git_exec: - command_name = sb_strdup(argv[2]); - for (p = command_name; *p != ' ' && *p != '\0'; p++); - if (*p == ' ') - *p = '\0'; - - if (BUFFER_SIZE < (strlen(command_name) + strlen(repo) + 4)) { - fprintf(stderr, "error: git-shell command is too big\n"); - rv = 1; - goto cleanup; - } - - if (snprintf(command_new, BUFFER_SIZE, "%s '%s'", command_name, repo)) - exec_git = true; - -cleanup: - free(repo); - free(command_orig); - free(command_name); - - if (exec_git) { - execlp("git-shell", "git-shell", "-c", command_new, NULL); - - // execlp only returns on error, then something bad happened - fprintf(stderr, "error: failed to execute git-shell\n"); - rv = 1; - } - - return rv; -} - - -static int -git_post_receive_hook(int argc, char *argv[]) -{ - if (0 != system("git config --local remote.mirror.pushurl &> /dev/null")) { - if (0 != system("git config --local remote.mirror.url &> /dev/null")) { - fprintf(stderr, "warning: repository mirroring disabled\n"); - return 0; - } - } - - // at this point we know that we have a remote called mirror, we can just - // push to it. - if (0 != system("git push --mirror mirror")) - fprintf(stderr, "warning: failed push to git mirror\n"); - - return 0; -} - - -typedef enum { - START_OLD = 1, - OLD, - START_NEW, - NEW, - START_REF, - REF -} input_state_t; - - -static int -git_pre_receive_hook(int argc, char *argv[]) -{ - int c; - char buffer[BUFFER_SIZE]; - - input_state_t state = START_OLD; - size_t i = 0; - size_t start = 0; - - int rv = 0; - char *new = NULL; - char *master = NULL; - - while (EOF != (c = getc(stdin))) { - - buffer[i] = (char) c; - - switch (state) { - case START_OLD: - start = i; - state = OLD; - break; - case OLD: - if (c != ' ') - break; - // no need to store old - state = START_NEW; - break; - case START_NEW: - start = i; - state = NEW; - break; - case NEW: - if (c != ' ') - break; - state = START_REF; - new = strndup(buffer + start, i - start); - break; - case START_REF: - start = i; - state = REF; - break; - case REF: - if (c != '\n') - break; - state = START_OLD; - // we just care about a ref (refs/heads/master), everything - // else is disposable :) - if (!((i - start == 17) && - (0 == strncmp("refs/heads/master", buffer + start, 17)))) - { - free(new); - new = NULL; - break; - } - master = new; - break; - } - - if (++i >= BUFFER_SIZE) { - fprintf(stderr, "error: pre-receive hook payload is too big.\n"); - rv = 1; - goto cleanup2; - } - } - - if (master == NULL) { - fprintf(stderr, "warning: no reference to master branch found. " - "nothing to deploy.\n"); - goto cleanup2; - } - - char *repo_dir = NULL; - char *output_dir = NULL; - - if (NULL == getcwd(buffer, BUFFER_SIZE)) { - fprintf(stderr, "error: failed to get repository remote path: %s\n", - strerror(errno)); - rv = 1; - goto cleanup; - } - - repo_dir = sb_strdup(buffer); - - char dir[] = "/tmp/blogc_XXXXXX"; - if (NULL == mkdtemp(dir)) { - rv = 1; - goto cleanup; - } - - char *git_archive_cmd = sb_strdup_printf( - "git archive \"%s\" | tar -x -C \"%s\" -f -", master, dir); - if (0 != system(git_archive_cmd)) { - fprintf(stderr, "error: failed to extract git content to temporary " - "directory: %s\n", dir); - rv = 1; - free(git_archive_cmd); - goto cleanup; - } - free(git_archive_cmd); - - if (0 != chdir(dir)) { - fprintf(stderr, "error: failed to chdir (%s): %s\n", dir, - strerror(errno)); - rv = 1; - goto cleanup; - } - - if ((0 != access("Makefile", F_OK)) && (0 != access("GNUMakefile", F_OK))) { - fprintf(stderr, "warning: no makefile found. skipping ...\n"); - goto cleanup; - } - - char *home = getenv("HOME"); - if (home == NULL) { - fprintf(stderr, "error: failed to find user home path\n"); - rv = 1; - goto cleanup; - } - - unsigned long epoch = time(NULL); - output_dir = sb_strdup_printf("%s/builds/%s-%lu", home, master, epoch); - char *gmake_cmd = sb_strdup_printf( - "gmake -j%d OUTPUT_DIR=\"%s\" BLOGC_GIT_RECEIVER=1", - cpu_count(), output_dir); - fprintf(stdout, "running command: %s\n\n", gmake_cmd); - fflush(stdout); - if (0 != system(gmake_cmd)) { - fprintf(stderr, "error: failed to build website ...\n"); - rmdir_recursive(output_dir); - free(gmake_cmd); - rv = 1; - goto cleanup; - } - free(gmake_cmd); - - if (0 != chdir(repo_dir)) { - fprintf(stderr, "error: failed to chdir (%s): %s\n", repo_dir, - strerror(errno)); - rmdir_recursive(output_dir); - rv = 1; - goto cleanup; - } - - char *htdocs_sym = NULL; - ssize_t htdocs_sym_len = readlink("htdocs", buffer, BUFFER_SIZE); - if (0 < htdocs_sym_len) { - if (0 != unlink("htdocs")) { - fprintf(stderr, "error: failed to remove symlink (%s/htdocs): %s\n", - repo_dir, strerror(errno)); - rmdir_recursive(output_dir); - rv = 1; - goto cleanup; - } - buffer[htdocs_sym_len] = '\0'; - htdocs_sym = buffer; - } - - if (0 != symlink(output_dir, "htdocs")) { - fprintf(stderr, "error: failed to create symlink (%s/htdocs): %s\n", - repo_dir, strerror(errno)); - rmdir_recursive(output_dir); - rv = 1; - goto cleanup; - } - - if (htdocs_sym != NULL) - rmdir_recursive(htdocs_sym); - -cleanup: - free(output_dir); - rmdir_recursive(dir); - free(repo_dir); -cleanup2: - free(new); - return rv; -} - - -int -main(int argc, char *argv[]) -{ - if (argc > 0) { - if (0 == strcmp(basename(argv[0]), "pre-receive")) - return git_pre_receive_hook(argc, argv); - if (0 == strcmp(basename(argv[0]), "post-receive")) - return git_post_receive_hook(argc, argv); - } - - if (argc == 3 && (0 == strcmp(argv[1], "-c"))) - return git_shell(argc, argv); - - fprintf(stderr, "error: this is a special shell, go away!\n"); - return 1; -} diff --git a/src/blogc-git-receiver/main.c b/src/blogc-git-receiver/main.c new file mode 100644 index 0000000..554ec0c --- /dev/null +++ b/src/blogc-git-receiver/main.c @@ -0,0 +1,501 @@ +/* + * blogc: A blog compiler. + * Copyright (C) 2015-2016 Rafael G. Martins + * + * This program can be distributed under the terms of the BSD License. + * See the file LICENSE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../common/utils.h" + +#ifndef BUFFER_SIZE +#define BUFFER_SIZE 4096 +#endif + + +static unsigned int +cpu_count(void) +{ +#ifdef _SC_NPROCESSORS_ONLN + long num = sysconf(_SC_NPROCESSORS_ONLN); + if (num >= 1) + return (unsigned int) num; +#endif + return 1; +} + + +static void +rmdir_recursive(const char *dir) +{ + struct stat buf; + if (0 != stat(dir, &buf)) { + fprintf(stderr, "warning: failed to remove directory (%s): %s\n", dir, + strerror(errno)); + return; + } + if (!S_ISDIR(buf.st_mode)) { + fprintf(stderr, "error: trying to remove invalid directory: %s\n", dir); + exit(2); + } + DIR *d = opendir(dir); + if (d == NULL) { + fprintf(stderr, "error: failed to open directory: %s\n", + strerror(errno)); + exit(2); + } + struct dirent *e; + while (NULL != (e = readdir(d))) { + if ((0 == strcmp(e->d_name, ".")) || (0 == strcmp(e->d_name, ".."))) + continue; + char *f = sb_strdup_printf("%s/%s", dir, e->d_name); + if (0 != stat(f, &buf)) { + fprintf(stderr, "error: failed to stat directory entry (%s): %s\n", + e->d_name, strerror(errno)); + free(f); + exit(2); + } + if (S_ISDIR(buf.st_mode)) { + rmdir_recursive(f); + } + else if (0 != unlink(f)) { + fprintf(stderr, "error: failed to remove file (%s): %s\n", f, + strerror(errno)); + free(f); + exit(2); + } + free(f); + } + if (0 != closedir(d)) { + fprintf(stderr, "error: failed to close directory: %s\n", + strerror(errno)); + exit(2); + } + if (0 != rmdir(dir)) { + fprintf(stderr, "error: failed to remove directory: %s\n", + strerror(errno)); + exit(2); + } +} + + +static int +git_shell(int argc, char *argv[]) +{ + int rv = 0; + + char *repo = NULL; + char *command_orig = NULL; + char *command_name = NULL; + char command_new[BUFFER_SIZE]; + + bool exec_git = false; + + // validate git command + size_t len = strlen(argv[2]); + if (!((len > 17 && (0 == strncmp(argv[2], "git-receive-pack ", 17))) || + (len > 16 && (0 == strncmp(argv[2], "git-upload-pack ", 16))) || + (len > 19 && (0 == strncmp(argv[2], "git-upload-archive ", 19))))) + { + fprintf(stderr, "error: unsupported git command: %s\n", argv[2]); + rv = 1; + goto cleanup; + } + + // get shell path + char *self = getenv("SHELL"); + if (self == NULL) { + fprintf(stderr, "error: failed to find blogc-git-receiver path\n"); + rv = 1; + goto cleanup; + } + + // get home path + char *home = getenv("HOME"); + if (home == NULL) { + fprintf(stderr, "error: failed to find user home path\n"); + rv = 1; + goto cleanup; + } + + // get git repository + command_orig = sb_strdup(argv[2]); + char *p, *r; + for (p = command_orig; *p != ' ' && *p != '\0'; p++); + if (*p == ' ') + p++; + if (*p == '\'' || *p == '"') + p++; + if (*p == '/') + p++; + for (r = p; *p != '\'' && *p != '"' && *p != '\0'; p++); + if (*p == '\'' || *p == '"') + *p = '\0'; + if (*--p == '/') + *p = '\0'; + + repo = sb_strdup_printf("repos/%s", r); + + // check if repository is sane + if (0 == strlen(repo)) { + fprintf(stderr, "error: invalid repository\n"); + rv = 1; + goto cleanup; + } + + if (0 == strncmp(argv[2], "git-upload-", 11)) // no need to check len here + goto git_exec; + + if (0 != chdir(home)) { + fprintf(stderr, "error: failed to chdir (%s): %s\n", home, + strerror(errno)); + rv = 1; + goto cleanup; + } + + if (0 != access(repo, F_OK)) { + char *git_init_cmd = sb_strdup_printf( + "git init --bare \"%s\" > /dev/null", repo); + if (0 != system(git_init_cmd)) { + fprintf(stderr, "error: failed to create git repository: %s\n", + repo); + rv = 1; + free(git_init_cmd); + goto cleanup; + } + free(git_init_cmd); + } + + if (0 != chdir(repo)) { + fprintf(stderr, "error: failed to chdir (%s/%s): %s\n", home, repo, + strerror(errno)); + rv = 1; + goto cleanup; + } + + if (0 != access("hooks", F_OK)) { + // openwrt git package won't install git templates, then the git + // repositories created with it won't have the hooks/ directory. + if (0 != mkdir("hooks", 0777)) { // mkdir honors umask for us. + fprintf(stderr, "error: failed to create directory (%s/%s/hooks): " + "%s\n", home, repo, strerror(errno)); + rv = 1; + goto cleanup; + } + } + + if (0 != chdir("hooks")) { + fprintf(stderr, "error: failed to chdir (%s/%s/hooks): %s\n", home, + repo, strerror(errno)); + rv = 1; + goto cleanup; + } + + if (0 == access("pre-receive", F_OK)) { + if (0 != unlink("pre-receive")) { + fprintf(stderr, "error: failed to remove old symlink " + "(%s/%s/hooks/pre-receive): %s\n", home, repo, strerror(errno)); + rv = 1; + goto cleanup; + } + } + + if (0 != symlink(self, "pre-receive")) { + fprintf(stderr, "error: failed to create symlink " + "(%s/%s/hooks/pre-receive): %s\n", home, repo, strerror(errno)); + rv = 1; + goto cleanup; + } + + if (0 == access("post-receive", F_OK)) { + if (0 != unlink("post-receive")) { + fprintf(stderr, "error: failed to remove old symlink " + "(%s/%s/hooks/post-receive): %s\n", home, repo, strerror(errno)); + rv = 1; + goto cleanup; + } + } + + if (0 != symlink(self, "post-receive")) { + fprintf(stderr, "error: failed to create symlink " + "(%s/%s/hooks/post-receive): %s\n", home, repo, strerror(errno)); + rv = 1; + goto cleanup; + } + + if (0 != chdir(home)) { + fprintf(stderr, "error: failed to chdir (%s): %s\n", home, + strerror(errno)); + rv = 1; + goto cleanup; + } + +git_exec: + command_name = sb_strdup(argv[2]); + for (p = command_name; *p != ' ' && *p != '\0'; p++); + if (*p == ' ') + *p = '\0'; + + if (BUFFER_SIZE < (strlen(command_name) + strlen(repo) + 4)) { + fprintf(stderr, "error: git-shell command is too big\n"); + rv = 1; + goto cleanup; + } + + if (snprintf(command_new, BUFFER_SIZE, "%s '%s'", command_name, repo)) + exec_git = true; + +cleanup: + free(repo); + free(command_orig); + free(command_name); + + if (exec_git) { + execlp("git-shell", "git-shell", "-c", command_new, NULL); + + // execlp only returns on error, then something bad happened + fprintf(stderr, "error: failed to execute git-shell\n"); + rv = 1; + } + + return rv; +} + + +static int +git_post_receive_hook(int argc, char *argv[]) +{ + if (0 != system("git config --local remote.mirror.pushurl &> /dev/null")) { + if (0 != system("git config --local remote.mirror.url &> /dev/null")) { + fprintf(stderr, "warning: repository mirroring disabled\n"); + return 0; + } + } + + // at this point we know that we have a remote called mirror, we can just + // push to it. + if (0 != system("git push --mirror mirror")) + fprintf(stderr, "warning: failed push to git mirror\n"); + + return 0; +} + + +typedef enum { + START_OLD = 1, + OLD, + START_NEW, + NEW, + START_REF, + REF +} input_state_t; + + +static int +git_pre_receive_hook(int argc, char *argv[]) +{ + int c; + char buffer[BUFFER_SIZE]; + + input_state_t state = START_OLD; + size_t i = 0; + size_t start = 0; + + int rv = 0; + char *new = NULL; + char *master = NULL; + + while (EOF != (c = getc(stdin))) { + + buffer[i] = (char) c; + + switch (state) { + case START_OLD: + start = i; + state = OLD; + break; + case OLD: + if (c != ' ') + break; + // no need to store old + state = START_NEW; + break; + case START_NEW: + start = i; + state = NEW; + break; + case NEW: + if (c != ' ') + break; + state = START_REF; + new = strndup(buffer + start, i - start); + break; + case START_REF: + start = i; + state = REF; + break; + case REF: + if (c != '\n') + break; + state = START_OLD; + // we just care about a ref (refs/heads/master), everything + // else is disposable :) + if (!((i - start == 17) && + (0 == strncmp("refs/heads/master", buffer + start, 17)))) + { + free(new); + new = NULL; + break; + } + master = new; + break; + } + + if (++i >= BUFFER_SIZE) { + fprintf(stderr, "error: pre-receive hook payload is too big.\n"); + rv = 1; + goto cleanup2; + } + } + + if (master == NULL) { + fprintf(stderr, "warning: no reference to master branch found. " + "nothing to deploy.\n"); + goto cleanup2; + } + + char *repo_dir = NULL; + char *output_dir = NULL; + + if (NULL == getcwd(buffer, BUFFER_SIZE)) { + fprintf(stderr, "error: failed to get repository remote path: %s\n", + strerror(errno)); + rv = 1; + goto cleanup; + } + + repo_dir = sb_strdup(buffer); + + char dir[] = "/tmp/blogc_XXXXXX"; + if (NULL == mkdtemp(dir)) { + rv = 1; + goto cleanup; + } + + char *git_archive_cmd = sb_strdup_printf( + "git archive \"%s\" | tar -x -C \"%s\" -f -", master, dir); + if (0 != system(git_archive_cmd)) { + fprintf(stderr, "error: failed to extract git content to temporary " + "directory: %s\n", dir); + rv = 1; + free(git_archive_cmd); + goto cleanup; + } + free(git_archive_cmd); + + if (0 != chdir(dir)) { + fprintf(stderr, "error: failed to chdir (%s): %s\n", dir, + strerror(errno)); + rv = 1; + goto cleanup; + } + + if ((0 != access("Makefile", F_OK)) && (0 != access("GNUMakefile", F_OK))) { + fprintf(stderr, "warning: no makefile found. skipping ...\n"); + goto cleanup; + } + + char *home = getenv("HOME"); + if (home == NULL) { + fprintf(stderr, "error: failed to find user home path\n"); + rv = 1; + goto cleanup; + } + + unsigned long epoch = time(NULL); + output_dir = sb_strdup_printf("%s/builds/%s-%lu", home, master, epoch); + char *gmake_cmd = sb_strdup_printf( + "gmake -j%d OUTPUT_DIR=\"%s\" BLOGC_GIT_RECEIVER=1", + cpu_count(), output_dir); + fprintf(stdout, "running command: %s\n\n", gmake_cmd); + fflush(stdout); + if (0 != system(gmake_cmd)) { + fprintf(stderr, "error: failed to build website ...\n"); + rmdir_recursive(output_dir); + free(gmake_cmd); + rv = 1; + goto cleanup; + } + free(gmake_cmd); + + if (0 != chdir(repo_dir)) { + fprintf(stderr, "error: failed to chdir (%s): %s\n", repo_dir, + strerror(errno)); + rmdir_recursive(output_dir); + rv = 1; + goto cleanup; + } + + char *htdocs_sym = NULL; + ssize_t htdocs_sym_len = readlink("htdocs", buffer, BUFFER_SIZE); + if (0 < htdocs_sym_len) { + if (0 != unlink("htdocs")) { + fprintf(stderr, "error: failed to remove symlink (%s/htdocs): %s\n", + repo_dir, strerror(errno)); + rmdir_recursive(output_dir); + rv = 1; + goto cleanup; + } + buffer[htdocs_sym_len] = '\0'; + htdocs_sym = buffer; + } + + if (0 != symlink(output_dir, "htdocs")) { + fprintf(stderr, "error: failed to create symlink (%s/htdocs): %s\n", + repo_dir, strerror(errno)); + rmdir_recursive(output_dir); + rv = 1; + goto cleanup; + } + + if (htdocs_sym != NULL) + rmdir_recursive(htdocs_sym); + +cleanup: + free(output_dir); + rmdir_recursive(dir); + free(repo_dir); +cleanup2: + free(new); + return rv; +} + + +int +main(int argc, char *argv[]) +{ + if (argc > 0) { + if (0 == strcmp(basename(argv[0]), "pre-receive")) + return git_pre_receive_hook(argc, argv); + if (0 == strcmp(basename(argv[0]), "post-receive")) + return git_post_receive_hook(argc, argv); + } + + if (argc == 3 && (0 == strcmp(argv[1], "-c"))) + return git_shell(argc, argv); + + fprintf(stderr, "error: this is a special shell, go away!\n"); + return 1; +} diff --git a/src/blogc-runserver.c b/src/blogc-runserver.c deleted file mode 100644 index 2fbbbc7..0000000 --- a/src/blogc-runserver.c +++ /dev/null @@ -1,386 +0,0 @@ -/* - * blogc: A blog compiler. - * Copyright (C) 2015-2016 Rafael G. Martins - * - * This program can be distributed under the terms of the BSD License. - * See the file LICENSE. - */ - -#ifdef HAVE_CONFIG_H -#include -#endif /* HAVE_CONFIG_H */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "utils.h" - - -/** - * this mapping is used to declare "supported" file types, that are forced over - * whatever detected by libmagic, but we will still use the charset provided by - * libmagic anyway. it also helps detecting index files when the client asks - * for a directory. - */ -static const struct content_type_map { - const char *mimetype; - const char *extension; - const char *index; -} content_types[] = { - {"text/html", "html", "index.html"}, - {"text/html", "htm", "index.htm"}, - {"text/xml", "xml", "index.xml"}, - {"text/plain", "txt", "index.txt"}, - {"text/css", "css", NULL}, - {"application/javascript", "js", NULL}, - {NULL, NULL, NULL} -}; - - -static magic_t magic_all = NULL; -static magic_t magic_charset = NULL; - - -static const char* -get_extension(const char *filename) -{ - const char *ext = NULL; - unsigned int i; - for (i = strlen(filename); i > 0; i--) { - if (filename[i] == '.') { - ext = filename + i + 1; - break; - } - } - if (i == 0) - return NULL; - return ext; -} - - -static char* -guess_content_type(const char *filename, int fd) -{ - int newfd; - - // try "supported" types first, and just use libmagic for charset - const char *extension = get_extension(filename); - if (extension == NULL) - goto libmagic; - const char *supported = NULL; - for (unsigned int i = 0; content_types[i].extension != NULL; i++) - if (0 == strcmp(content_types[i].extension, extension)) - supported = content_types[i].mimetype; - if (supported != NULL) { - newfd = dup(fd); - if (-1 != newfd) { - const char* charset = magic_descriptor(magic_charset, newfd); - close(newfd); - if (charset != NULL) - return sb_strdup_printf("%s; charset=%s", supported, charset); - } - return sb_strdup(supported); - } - -libmagic: - - // fallback to use libmagic for everything - newfd = dup(fd); - if (-1 != newfd) { - const char* content_type = magic_descriptor(magic_all, newfd); - close(newfd); - if (content_type != NULL) - return sb_strdup(content_type); - } - return sb_strdup("application/octet-stream"); -} - - -static void -handler(struct evhttp_request *request, void *ptr) -{ - const char *root = ptr; - const char *uri = evhttp_request_get_uri(request); - - struct evhttp_uri *decoded_uri = evhttp_uri_parse(uri); - if (decoded_uri == NULL) { - evhttp_send_error(request, 400, "Bad request"); - return; - } - - const char *path = evhttp_uri_get_path(decoded_uri); - if (path == NULL) - path = "/"; - - char *decoded_path = evhttp_uridecode(path, 0, NULL); - if (decoded_path == NULL) { - evhttp_send_error(request, 400, "Bad request"); - goto point1; - } - - char *abs_path = sb_strdup_printf("%s/%s", root, decoded_path); - char *real_path = realpath(abs_path, NULL); - free(abs_path); - - if (real_path == NULL) { - evhttp_send_error(request, 404, "Not found"); - goto point2; - } - - char *real_root = realpath(root, NULL); - if (real_root == NULL) { - evhttp_send_error(request, 500, "Internal server error"); - goto point3; - } - - if (0 != strncmp(real_root, real_path, strlen(real_root))) { - evhttp_send_error(request, 404, "Not found"); - goto point4; - } - - struct stat st; - if (0 > stat(real_path, &st)) { - evhttp_send_error(request, 404, "Not found"); - goto point4; - } - - bool add_slash = false; - - if (S_ISDIR(st.st_mode)) { - char *found = NULL; - - for (unsigned int i = 0; content_types[i].mimetype != NULL; i++) { - if (content_types[i].index == NULL) - continue; - char *f = sb_strdup_printf("%s/%s", real_path, - content_types[i].index); - if (0 == access(f, F_OK)) { - found = sb_strdup(f); - break; - } - free(f); - } - - if (found == NULL) { - evhttp_send_error(request, 403, "Forbidden"); - goto point4; - } - - size_t path_len = strlen(path); - if (path_len > 0 && path[path_len - 1] != '/') - add_slash = true; - - free(real_path); - real_path = found; - } - - int fd; - if ((fd = open(real_path, O_RDONLY)) < 0) { - evhttp_send_error(request, 500, "Internal server error"); - goto point4; - } - - char *type = guess_content_type(real_path, fd); - - if (fstat(fd, &st) < 0) { - evhttp_send_error(request, 500, "Internal server error"); - goto point5; - } - - struct evkeyvalq *headers = evhttp_request_get_output_headers(request); - - if (add_slash) { - char *tmp = sb_strdup_printf("%s/", path); - evhttp_add_header(headers, "Location", tmp); - free(tmp); - // production webservers usually returns 301 in such cases, but 302 is - // better for development/testing. - evhttp_send_reply(request, 302, "Found", NULL); - goto point5; - } - - evhttp_add_header(headers, "Content-Type", type); - char *content_length = sb_strdup_printf("%zu", st.st_size); - evhttp_add_header(headers, "Content-Length", content_length); - free(content_length); - - struct evbuffer *evb = evbuffer_new(); - evbuffer_add_file(evb, fd, 0, st.st_size); - evhttp_send_reply(request, 200, "OK", evb); - -point5: - free(type); -point4: - free(real_root); -point3: - free(real_path); -point2: - free(decoded_path); -point1: - evhttp_uri_free(decoded_uri); -} - - -static int -runserver(const char *address, unsigned short port, const char *root) -{ - struct event_base *base = event_base_new(); - if (base == NULL) { - fprintf(stderr, "error: failed to initialize event base\n"); - return 1; - } - - struct evhttp *http = evhttp_new(base); - if (http == NULL) { - fprintf(stderr, "error: failed to initialize HTTP server\n"); - return 1; - } - - evhttp_set_gencb(http, handler, (char*) root); - - evhttp_set_allowed_methods(http, EVHTTP_REQ_GET | EVHTTP_REQ_HEAD); - - if (0 != evhttp_bind_socket(http, address, port)) { - fprintf(stderr, "error: failed to bind socket to %s:%d\n", address, - port); - return 1; - } - - fprintf(stderr, " * Running on http://%s:%d/\n", address, port); - - event_base_dispatch(base); - - return 0; -} - - -static void -print_help(void) -{ - printf( - "usage:\n" - " blogc-runserver [-h] [-v] [-t HOST] [-p PORT] DOCROOT\n" - " - A simple HTTP server to test blogc websites.\n" - "\n" - "positional arguments:\n" - " DOCROOT document root directory\n" - "\n" - "optional arguments:\n" - " -h show this help message and exit\n" - " -v show version and exit\n" - " -t HOST set server listen address (default: 127.0.0.1)\n" - " -p PORT set server listen port (default: 8080)\n"); -} - - -static void -print_usage(void) -{ - printf("usage: blogc-runserver [-h] [-v] [-t HOST] [-p PORT] DOCROOT\n"); -} - - -int -main(int argc, char **argv) -{ - signal(SIGPIPE, SIG_IGN); - - int rv = 0; - char *host = NULL; - char *docroot = NULL; - unsigned short port = 8080; - - unsigned int args = 0; - - for (unsigned int i = 1; i < argc; i++) { - if (argv[i][0] == '-') { - switch (argv[i][1]) { - case 'h': - print_help(); - goto cleanup; - case 'v': - printf("%s\n", PACKAGE_STRING); - goto cleanup; - case 't': - if (argv[i][2] != '\0') - host = sb_strdup(argv[i] + 2); - else - host = sb_strdup(argv[++i]); - break; - case 'p': - if (argv[i][2] != '\0') - port = strtoul(argv[i] + 2, NULL, 10); - else - port = strtoul(argv[++i], NULL, 10); - break; - default: - print_usage(); - fprintf(stderr, "blogc-runserver: error: invalid " - "argument: -%c\n", argv[i][1]); - rv = 2; - goto cleanup; - } - } - else { - if (args > 0) { - print_usage(); - fprintf(stderr, "blogc-runserver: error: only one positional " - "argument allowed\n"); - rv = 2; - goto cleanup; - } - args++; - docroot = sb_strdup(argv[i]); - } - } - - if (docroot == NULL) { - print_usage(); - fprintf(stderr, "blogc-runserver: error: document root directory " - "required\n"); - rv = 2; - goto cleanup; - } - - if (host == NULL) - host = sb_strdup("127.0.0.1"); - - magic_all = magic_open(MAGIC_MIME); - magic_charset = magic_open(MAGIC_MIME_ENCODING); - if (magic_all == NULL || magic_charset == NULL) { - fprintf(stderr, "error: failed to initialize libmagic\n"); - rv = 1; - goto cleanup; - } - - if ((0 != magic_load(magic_all, NULL)) || - (0 != magic_load(magic_charset, NULL))) - { - fprintf(stderr, "error: failed to load libmagic data\n"); - magic_close(magic_all); - magic_close(magic_charset); - rv = 1; - goto cleanup; - } - - rv = runserver(host, port, docroot); - - magic_close(magic_all); - magic_close(magic_charset); - -cleanup: - free(host); - free(docroot); - - return rv; -} diff --git a/src/blogc-runserver/main.c b/src/blogc-runserver/main.c new file mode 100644 index 0000000..2a756f1 --- /dev/null +++ b/src/blogc-runserver/main.c @@ -0,0 +1,386 @@ +/* + * blogc: A blog compiler. + * Copyright (C) 2015-2016 Rafael G. Martins + * + * This program can be distributed under the terms of the BSD License. + * See the file LICENSE. + */ + +#ifdef HAVE_CONFIG_H +#include +#endif /* HAVE_CONFIG_H */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../common/utils.h" + + +/** + * this mapping is used to declare "supported" file types, that are forced over + * whatever detected by libmagic, but we will still use the charset provided by + * libmagic anyway. it also helps detecting index files when the client asks + * for a directory. + */ +static const struct content_type_map { + const char *mimetype; + const char *extension; + const char *index; +} content_types[] = { + {"text/html", "html", "index.html"}, + {"text/html", "htm", "index.htm"}, + {"text/xml", "xml", "index.xml"}, + {"text/plain", "txt", "index.txt"}, + {"text/css", "css", NULL}, + {"application/javascript", "js", NULL}, + {NULL, NULL, NULL} +}; + + +static magic_t magic_all = NULL; +static magic_t magic_charset = NULL; + + +static const char* +get_extension(const char *filename) +{ + const char *ext = NULL; + unsigned int i; + for (i = strlen(filename); i > 0; i--) { + if (filename[i] == '.') { + ext = filename + i + 1; + break; + } + } + if (i == 0) + return NULL; + return ext; +} + + +static char* +guess_content_type(const char *filename, int fd) +{ + int newfd; + + // try "supported" types first, and just use libmagic for charset + const char *extension = get_extension(filename); + if (extension == NULL) + goto libmagic; + const char *supported = NULL; + for (unsigned int i = 0; content_types[i].extension != NULL; i++) + if (0 == strcmp(content_types[i].extension, extension)) + supported = content_types[i].mimetype; + if (supported != NULL) { + newfd = dup(fd); + if (-1 != newfd) { + const char* charset = magic_descriptor(magic_charset, newfd); + close(newfd); + if (charset != NULL) + return sb_strdup_printf("%s; charset=%s", supported, charset); + } + return sb_strdup(supported); + } + +libmagic: + + // fallback to use libmagic for everything + newfd = dup(fd); + if (-1 != newfd) { + const char* content_type = magic_descriptor(magic_all, newfd); + close(newfd); + if (content_type != NULL) + return sb_strdup(content_type); + } + return sb_strdup("application/octet-stream"); +} + + +static void +handler(struct evhttp_request *request, void *ptr) +{ + const char *root = ptr; + const char *uri = evhttp_request_get_uri(request); + + struct evhttp_uri *decoded_uri = evhttp_uri_parse(uri); + if (decoded_uri == NULL) { + evhttp_send_error(request, 400, "Bad request"); + return; + } + + const char *path = evhttp_uri_get_path(decoded_uri); + if (path == NULL) + path = "/"; + + char *decoded_path = evhttp_uridecode(path, 0, NULL); + if (decoded_path == NULL) { + evhttp_send_error(request, 400, "Bad request"); + goto point1; + } + + char *abs_path = sb_strdup_printf("%s/%s", root, decoded_path); + char *real_path = realpath(abs_path, NULL); + free(abs_path); + + if (real_path == NULL) { + evhttp_send_error(request, 404, "Not found"); + goto point2; + } + + char *real_root = realpath(root, NULL); + if (real_root == NULL) { + evhttp_send_error(request, 500, "Internal server error"); + goto point3; + } + + if (0 != strncmp(real_root, real_path, strlen(real_root))) { + evhttp_send_error(request, 404, "Not found"); + goto point4; + } + + struct stat st; + if (0 > stat(real_path, &st)) { + evhttp_send_error(request, 404, "Not found"); + goto point4; + } + + bool add_slash = false; + + if (S_ISDIR(st.st_mode)) { + char *found = NULL; + + for (unsigned int i = 0; content_types[i].mimetype != NULL; i++) { + if (content_types[i].index == NULL) + continue; + char *f = sb_strdup_printf("%s/%s", real_path, + content_types[i].index); + if (0 == access(f, F_OK)) { + found = sb_strdup(f); + break; + } + free(f); + } + + if (found == NULL) { + evhttp_send_error(request, 403, "Forbidden"); + goto point4; + } + + size_t path_len = strlen(path); + if (path_len > 0 && path[path_len - 1] != '/') + add_slash = true; + + free(real_path); + real_path = found; + } + + int fd; + if ((fd = open(real_path, O_RDONLY)) < 0) { + evhttp_send_error(request, 500, "Internal server error"); + goto point4; + } + + char *type = guess_content_type(real_path, fd); + + if (fstat(fd, &st) < 0) { + evhttp_send_error(request, 500, "Internal server error"); + goto point5; + } + + struct evkeyvalq *headers = evhttp_request_get_output_headers(request); + + if (add_slash) { + char *tmp = sb_strdup_printf("%s/", path); + evhttp_add_header(headers, "Location", tmp); + free(tmp); + // production webservers usually returns 301 in such cases, but 302 is + // better for development/testing. + evhttp_send_reply(request, 302, "Found", NULL); + goto point5; + } + + evhttp_add_header(headers, "Content-Type", type); + char *content_length = sb_strdup_printf("%zu", st.st_size); + evhttp_add_header(headers, "Content-Length", content_length); + free(content_length); + + struct evbuffer *evb = evbuffer_new(); + evbuffer_add_file(evb, fd, 0, st.st_size); + evhttp_send_reply(request, 200, "OK", evb); + +point5: + free(type); +point4: + free(real_root); +point3: + free(real_path); +point2: + free(decoded_path); +point1: + evhttp_uri_free(decoded_uri); +} + + +static int +runserver(const char *address, unsigned short port, const char *root) +{ + struct event_base *base = event_base_new(); + if (base == NULL) { + fprintf(stderr, "error: failed to initialize event base\n"); + return 1; + } + + struct evhttp *http = evhttp_new(base); + if (http == NULL) { + fprintf(stderr, "error: failed to initialize HTTP server\n"); + return 1; + } + + evhttp_set_gencb(http, handler, (char*) root); + + evhttp_set_allowed_methods(http, EVHTTP_REQ_GET | EVHTTP_REQ_HEAD); + + if (0 != evhttp_bind_socket(http, address, port)) { + fprintf(stderr, "error: failed to bind socket to %s:%d\n", address, + port); + return 1; + } + + fprintf(stderr, " * Running on http://%s:%d/\n", address, port); + + event_base_dispatch(base); + + return 0; +} + + +static void +print_help(void) +{ + printf( + "usage:\n" + " blogc-runserver [-h] [-v] [-t HOST] [-p PORT] DOCROOT\n" + " - A simple HTTP server to test blogc websites.\n" + "\n" + "positional arguments:\n" + " DOCROOT document root directory\n" + "\n" + "optional arguments:\n" + " -h show this help message and exit\n" + " -v show version and exit\n" + " -t HOST set server listen address (default: 127.0.0.1)\n" + " -p PORT set server listen port (default: 8080)\n"); +} + + +static void +print_usage(void) +{ + printf("usage: blogc-runserver [-h] [-v] [-t HOST] [-p PORT] DOCROOT\n"); +} + + +int +main(int argc, char **argv) +{ + signal(SIGPIPE, SIG_IGN); + + int rv = 0; + char *host = NULL; + char *docroot = NULL; + unsigned short port = 8080; + + unsigned int args = 0; + + for (unsigned int i = 1; i < argc; i++) { + if (argv[i][0] == '-') { + switch (argv[i][1]) { + case 'h': + print_help(); + goto cleanup; + case 'v': + printf("%s\n", PACKAGE_STRING); + goto cleanup; + case 't': + if (argv[i][2] != '\0') + host = sb_strdup(argv[i] + 2); + else + host = sb_strdup(argv[++i]); + break; + case 'p': + if (argv[i][2] != '\0') + port = strtoul(argv[i] + 2, NULL, 10); + else + port = strtoul(argv[++i], NULL, 10); + break; + default: + print_usage(); + fprintf(stderr, "blogc-runserver: error: invalid " + "argument: -%c\n", argv[i][1]); + rv = 2; + goto cleanup; + } + } + else { + if (args > 0) { + print_usage(); + fprintf(stderr, "blogc-runserver: error: only one positional " + "argument allowed\n"); + rv = 2; + goto cleanup; + } + args++; + docroot = sb_strdup(argv[i]); + } + } + + if (docroot == NULL) { + print_usage(); + fprintf(stderr, "blogc-runserver: error: document root directory " + "required\n"); + rv = 2; + goto cleanup; + } + + if (host == NULL) + host = sb_strdup("127.0.0.1"); + + magic_all = magic_open(MAGIC_MIME); + magic_charset = magic_open(MAGIC_MIME_ENCODING); + if (magic_all == NULL || magic_charset == NULL) { + fprintf(stderr, "error: failed to initialize libmagic\n"); + rv = 1; + goto cleanup; + } + + if ((0 != magic_load(magic_all, NULL)) || + (0 != magic_load(magic_charset, NULL))) + { + fprintf(stderr, "error: failed to load libmagic data\n"); + magic_close(magic_all); + magic_close(magic_charset); + rv = 1; + goto cleanup; + } + + rv = runserver(host, port, docroot); + + magic_close(magic_all); + magic_close(magic_charset); + +cleanup: + free(host); + free(docroot); + + return rv; +} diff --git a/src/blogc.c b/src/blogc.c deleted file mode 100644 index c90070d..0000000 --- a/src/blogc.c +++ /dev/null @@ -1,297 +0,0 @@ -/* - * blogc: A blog compiler. - * Copyright (C) 2015-2016 Rafael G. Martins - * - * This program can be distributed under the terms of the BSD License. - * See the file LICENSE. - */ - -#ifdef HAVE_CONFIG_H -#include -#endif /* HAVE_CONFIG_H */ - -#ifdef HAVE_SYS_STAT_H -#include -#endif /* HAVE_SYS_STAT_H */ - -#include -#include -#include -#include -#include -#include -#include - -#include "debug.h" -#include "template-parser.h" -#include "loader.h" -#include "renderer.h" -#include "error.h" -#include "utf8.h" -#include "utils.h" - -#ifndef PACKAGE_VERSION -#define PACKAGE_VERSION "Unknown" -#endif - - -static void -blogc_print_help(void) -{ - printf( - "usage:\n" - " blogc [-h] [-v] [-d] [-l] [-D KEY=VALUE ...] [-p KEY] [-t TEMPLATE]\n" - " [-o OUTPUT] [SOURCE ...] - A blog compiler.\n" - "\n" - "positional arguments:\n" - " SOURCE source file(s)\n" - "\n" - "optional arguments:\n" - " -h show this help message and exit\n" - " -v show version and exit\n" - " -d enable debug\n" - " -l build listing page, from multiple source files\n" - " -D KEY=VALUE set global configuration parameter\n" - " -p KEY show the value of a global configuration parameter\n" - " after source parsing and exit\n" - " -t TEMPLATE template file\n" - " -o OUTPUT output file\n"); -} - - -static void -blogc_print_usage(void) -{ - printf( - "usage: blogc [-h] [-v] [-d] [-l] [-D KEY=VALUE ...] [-p KEY] [-t TEMPLATE]\n" - " [-o OUTPUT] [SOURCE ...]\n"); -} - - -static void -blogc_mkdir_recursive(const char *filename) -{ - char *fname = sb_strdup(filename); - for (char *tmp = fname; *tmp != '\0'; tmp++) { - if (*tmp != '/' && *tmp != '\\') - continue; -#ifdef HAVE_SYS_STAT_H - char bkp = *tmp; - *tmp = '\0'; - if ((strlen(fname) > 0) && -#if defined(WIN32) || defined(_WIN32) - (-1 == mkdir(fname)) && -#else - (-1 == mkdir(fname, 0777)) && -#endif - (errno != EEXIST)) - { - fprintf(stderr, "blogc: error: failed to create output " - "directory (%s): %s\n", fname, strerror(errno)); - free(fname); - exit(2); - } - *tmp = bkp; -#else - // FIXME: show this warning only if actually trying to create a directory. - fprintf(stderr, "blogc: warning: can't create output directories " - "for your platform. please create the directories yourself.\n"); - break; -#endif - } - free(fname); -} - - -int -main(int argc, char **argv) -{ - setlocale(LC_ALL, ""); - - int rv = 0; - - bool debug = false; - bool listing = false; - char *template = NULL; - char *output = NULL; - char *print = NULL; - char *tmp = NULL; - char **pieces = NULL; - - sb_slist_t *sources = NULL; - sb_trie_t *config = sb_trie_new(free); - sb_trie_insert(config, "BLOGC_VERSION", sb_strdup(PACKAGE_VERSION)); - - for (unsigned int i = 1; i < argc; i++) { - tmp = NULL; - if (argv[i][0] == '-') { - switch (argv[i][1]) { - case 'h': - blogc_print_help(); - goto cleanup; - case 'v': - printf("%s\n", PACKAGE_STRING); - goto cleanup; - case 'd': - debug = true; - break; - case 'l': - listing = true; - break; - case 't': - if (argv[i][2] != '\0') - template = sb_strdup(argv[i] + 2); - else if (i + 1 < argc) - template = sb_strdup(argv[++i]); - break; - case 'o': - if (argv[i][2] != '\0') - output = sb_strdup(argv[i] + 2); - else if (i + 1 < argc) - output = sb_strdup(argv[++i]); - break; - case 'p': - if (argv[i][2] != '\0') - print = sb_strdup(argv[i] + 2); - else if (i + 1 < argc) - print = sb_strdup(argv[++i]); - break; - case 'D': - if (argv[i][2] != '\0') - tmp = argv[i] + 2; - else if (i + 1 < argc) - tmp = argv[++i]; - if (tmp != NULL) { - if (!blogc_utf8_validate((uint8_t*) tmp, strlen(tmp))) { - fprintf(stderr, "blogc: error: invalid value for " - "-D (must be valid UTF-8 string): %s\n", tmp); - goto cleanup; - } - pieces = sb_str_split(tmp, '=', 2); - if (sb_strv_length(pieces) != 2) { - fprintf(stderr, "blogc: error: invalid value for " - "-D (must have an '='): %s\n", tmp); - sb_strv_free(pieces); - rv = 2; - goto cleanup; - } - for (unsigned int j = 0; pieces[0][j] != '\0'; j++) { - if (!((pieces[0][j] >= 'A' && pieces[0][j] <= 'Z') || - pieces[0][j] == '_')) - { - fprintf(stderr, "blogc: error: invalid value " - "for -D (configuration key must be uppercase " - "with '_'): %s\n", pieces[0]); - sb_strv_free(pieces); - rv = 2; - goto cleanup; - } - } - sb_trie_insert(config, pieces[0], sb_strdup(pieces[1])); - sb_strv_free(pieces); - pieces = NULL; - } - break; - default: - blogc_print_usage(); - fprintf(stderr, "blogc: error: invalid argument: -%c\n", - argv[i][1]); - rv = 2; - goto cleanup; - } - } - else - sources = sb_slist_append(sources, sb_strdup(argv[i])); - } - - if (!listing && sb_slist_length(sources) == 0) { - blogc_print_usage(); - fprintf(stderr, "blogc: error: one source file is required\n"); - rv = 2; - goto cleanup; - } - - if (!listing && sb_slist_length(sources) > 1) { - blogc_print_usage(); - fprintf(stderr, "blogc: error: only one source file should be provided, " - "if running without '-l'\n"); - rv = 2; - goto cleanup; - } - - blogc_error_t *err = NULL; - - sb_slist_t *s = blogc_source_parse_from_files(config, sources, &err); - if (err != NULL) { - blogc_error_print(err); - rv = 2; - goto cleanup2; - } - - if (print != NULL) { - const char *val = sb_trie_lookup(config, print); - if (val == NULL) { - fprintf(stderr, "blogc: error: configuration variable not found: %s\n", - print); - rv = 2; - } - else { - printf("%s\n", val); - } - goto cleanup2; - } - - if (template == NULL) { - blogc_print_usage(); - fprintf(stderr, "blogc: error: argument -t is required when rendering content\n"); - rv = 2; - goto cleanup2; - } - - sb_slist_t* l = blogc_template_parse_from_file(template, &err); - if (err != NULL) { - blogc_error_print(err); - rv = 2; - goto cleanup3; - } - - if (debug) - blogc_debug_template(l); - - char *out = blogc_render(l, s, config, listing); - - bool write_to_stdout = (output == NULL || (0 == strcmp(output, "-"))); - - FILE *fp = stdout; - if (!write_to_stdout) { - blogc_mkdir_recursive(output); - fp = fopen(output, "w"); - if (fp == NULL) { - fprintf(stderr, "blogc: error: failed to open output file (%s): %s\n", - output, strerror(errno)); - rv = 2; - goto cleanup4; - } - } - - if (out != NULL) - fprintf(fp, "%s", out); - - if (!write_to_stdout) - fclose(fp); - -cleanup4: - free(out); -cleanup3: - blogc_template_free_stmts(l); -cleanup2: - sb_slist_free_full(s, (sb_free_func_t) sb_trie_free); - blogc_error_free(err); -cleanup: - sb_trie_free(config); - free(template); - free(output); - free(print); - sb_slist_free_full(sources, free); - return rv; -} diff --git a/src/blogc/content-parser.c b/src/blogc/content-parser.c new file mode 100644 index 0000000..e751548 --- /dev/null +++ b/src/blogc/content-parser.c @@ -0,0 +1,1264 @@ +/* + * blogc: A blog compiler. + * Copyright (C) 2015-2016 Rafael G. Martins + * + * This program can be distributed under the terms of the BSD License. + * See the file LICENSE. + */ + +#include +#include +#include + +#include "content-parser.h" +#include "../common/utils.h" + +// this is a half ass implementation of a markdown-like syntax. bugs are +// expected. feel free to improve the parser and add new features. + + +char* +blogc_slugify(const char *str) +{ + if (str == NULL) + return NULL; + char *new_str = sb_strdup(str); + int diff = 'a' - 'A'; // just to avoid magic numbers + for (size_t i = 0; new_str[i] != '\0'; i++) { + if (new_str[i] >= 'a' && new_str[i] <= 'z') + continue; + if (new_str[i] >= '0' && new_str[i] <= '9') + continue; + if (new_str[i] >= 'A' && new_str[i] <= 'Z') + new_str[i] += diff; + else + new_str[i] = '-'; + } + return new_str; +} + + +static const char* +htmlentities(char c) +{ + switch (c) { + case '&': + return "&"; + case '<': + return "<"; + case '>': + return ">"; + case '"': + return """; + case '\'': + return "'"; + case '/': + return "/"; + } + return NULL; +} + + +static void +htmlentities_append(sb_string_t *str, char c) +{ + const char *e = htmlentities(c); + if (e == NULL) + sb_string_append_c(str, c); + else + sb_string_append(str, e); +} + + +char* +blogc_htmlentities(const char *str) +{ + if (str == NULL) + return NULL; + sb_string_t *rv = sb_string_new(); + for (size_t i = 0; str[i] != '\0'; i++) + htmlentities_append(rv, str[i]); + return sb_string_free(rv, false); +} + + +char* +blogc_fix_description(const char *paragraph) +{ + if (paragraph == NULL) + return NULL; + sb_string_t *rv = sb_string_new(); + bool last = false; + bool newline = false; + char *tmp = NULL; + size_t start = 0; + size_t current = 0; + while (true) { + switch (paragraph[current]) { + case '\0': + last = true; + case '\r': + case '\n': + if (newline) + break; + tmp = sb_strndup(paragraph + start, current - start); + sb_string_append(rv, sb_str_strip(tmp)); + free(tmp); + tmp = NULL; + if (!last) + sb_string_append_c(rv, ' '); + start = current + 1; + newline = true; + break; + default: + newline = false; + } + if (last) + break; + current++; + } + tmp = blogc_htmlentities(sb_str_strip(rv->str)); + sb_string_free(rv, true); + return tmp; +} + + +typedef enum { + CONTENT_START_LINE = 1, + CONTENT_EXCERPT, + CONTENT_EXCERPT_END, + CONTENT_HEADER, + CONTENT_HEADER_TITLE_START, + CONTENT_HEADER_TITLE, + CONTENT_HTML, + CONTENT_HTML_END, + CONTENT_BLOCKQUOTE, + CONTENT_BLOCKQUOTE_START, + CONTENT_BLOCKQUOTE_END, + CONTENT_CODE, + CONTENT_CODE_START, + CONTENT_CODE_END, + CONTENT_UNORDERED_LIST_OR_HORIZONTAL_RULE, + CONTENT_HORIZONTAL_RULE, + CONTENT_UNORDERED_LIST_START, + CONTENT_UNORDERED_LIST_END, + CONTENT_ORDERED_LIST, + CONTENT_ORDERED_LIST_SPACE, + CONTENT_ORDERED_LIST_START, + CONTENT_ORDERED_LIST_END, + CONTENT_PARAGRAPH, + CONTENT_PARAGRAPH_END, +} blogc_content_parser_state_t; + + +typedef enum { + CONTENT_INLINE_START = 1, + CONTENT_INLINE_ASTERISK, + CONTENT_INLINE_ASTERISK_DOUBLE, + CONTENT_INLINE_UNDERSCORE, + CONTENT_INLINE_UNDERSCORE_DOUBLE, + CONTENT_INLINE_BACKTICKS, + CONTENT_INLINE_BACKTICKS_DOUBLE, + CONTENT_INLINE_LINK_START, + CONTENT_INLINE_LINK_AUTO, + CONTENT_INLINE_LINK_CONTENT, + CONTENT_INLINE_LINK_URL_START, + CONTENT_INLINE_LINK_URL, + CONTENT_INLINE_IMAGE_START, + CONTENT_INLINE_IMAGE_ALT, + CONTENT_INLINE_IMAGE_URL_START, + CONTENT_INLINE_IMAGE_URL, + CONTENT_INLINE_ENDASH, + CONTENT_INLINE_EMDASH, + CONTENT_INLINE_LINE_BREAK_START, + CONTENT_INLINE_LINE_BREAK, +} blogc_content_parser_inline_state_t; + + +static char* +blogc_content_parse_inline_internal(const char *src, size_t src_len) +{ + size_t current = 0; + size_t start = 0; + size_t count = 0; + + const char *tmp = NULL; + char *tmp2 = NULL; + char *tmp3 = NULL; + + size_t start_link = 0; + char *link1 = NULL; + + sb_string_t *rv = sb_string_new(); + + blogc_content_parser_inline_state_t state = CONTENT_INLINE_START; + + while (current < src_len) { + char c = src[current]; + bool is_last = current == src_len - 1; + + switch (state) { + case CONTENT_INLINE_START: + if (is_last) { + htmlentities_append(rv, c); + break; + } + if (c == '\\') { + htmlentities_append(rv, src[++current]); + break; + } + if (c == '*') { + state = CONTENT_INLINE_ASTERISK; + break; + } + if (c == '_') { + state = CONTENT_INLINE_UNDERSCORE; + break; + } + if (c == '`') { + state = CONTENT_INLINE_BACKTICKS; + break; + } + if (c == '[') { + state = CONTENT_INLINE_LINK_START; + break; + } + if (c == '!') { + state = CONTENT_INLINE_IMAGE_START; + break; + } + if (c == '-') { + state = CONTENT_INLINE_ENDASH; + break; + } + if (c == ' ') { + state = CONTENT_INLINE_LINE_BREAK_START; + break; + } + htmlentities_append(rv, c); + break; + + case CONTENT_INLINE_ASTERISK: + if (c == '*') { + state = CONTENT_INLINE_ASTERISK_DOUBLE; + break; + } + tmp = sb_str_find(src + current, '*'); + if (tmp == NULL || ((tmp - src) >= src_len)) { + sb_string_append_c(rv, '*'); + state = CONTENT_INLINE_START; + continue; + } + tmp2 = blogc_content_parse_inline_internal( + src + current, (tmp - src) - current); + sb_string_append_printf(rv, "%s", tmp2); + current = tmp - src; + tmp = NULL; + free(tmp2); + tmp2 = NULL; + state = CONTENT_INLINE_START; + break; + + case CONTENT_INLINE_ASTERISK_DOUBLE: + tmp = src + current; + do { + tmp = sb_str_find(tmp, '*'); + if (((tmp - src) < src_len) && *(tmp + 1) == '*') { + break; + } + tmp++; + } while (tmp != NULL && (tmp - src) < src_len); + if (tmp == NULL || ((tmp - src) >= src_len)) { + sb_string_append_c(rv, '*'); + sb_string_append_c(rv, '*'); + state = CONTENT_INLINE_START; + continue; + } + tmp2 = blogc_content_parse_inline_internal( + src + current, (tmp - src) - current); + sb_string_append_printf(rv, "%s", tmp2); + current = tmp - src + 1; + tmp = NULL; + free(tmp2); + tmp2 = NULL; + state = CONTENT_INLINE_START; + break; + + case CONTENT_INLINE_UNDERSCORE: + if (c == '_') { + state = CONTENT_INLINE_UNDERSCORE_DOUBLE; + break; + } + tmp = sb_str_find(src + current, '_'); + if (tmp == NULL || ((tmp - src) >= src_len)) { + sb_string_append_c(rv, '_'); + state = CONTENT_INLINE_START; + continue; + } + tmp2 = blogc_content_parse_inline_internal( + src + current, (tmp - src) - current); + sb_string_append_printf(rv, "%s", tmp2); + current = tmp - src; + tmp = NULL; + free(tmp2); + tmp2 = NULL; + state = CONTENT_INLINE_START; + break; + + case CONTENT_INLINE_UNDERSCORE_DOUBLE: + tmp = src + current; + do { + tmp = sb_str_find(tmp, '_'); + if (((tmp - src) < src_len) && *(tmp + 1) == '_') { + break; + } + tmp++; + } while (tmp != NULL && (tmp - src) < src_len); + if (tmp == NULL || ((tmp - src) >= src_len)) { + sb_string_append_c(rv, '_'); + sb_string_append_c(rv, '_'); + state = CONTENT_INLINE_START; + continue; + } + tmp2 = blogc_content_parse_inline_internal( + src + current, (tmp - src) - current); + sb_string_append_printf(rv, "%s", tmp2); + current = tmp - src + 1; + tmp = NULL; + free(tmp2); + tmp2 = NULL; + state = CONTENT_INLINE_START; + break; + + case CONTENT_INLINE_BACKTICKS: + if (c == '`') { + state = CONTENT_INLINE_BACKTICKS_DOUBLE; + break; + } + tmp = sb_str_find(src + current, '`'); + if (tmp == NULL || ((tmp - src) >= src_len)) { + sb_string_append_c(rv, '`'); + state = CONTENT_INLINE_START; + continue; + } + tmp3 = sb_strndup(src + current, (tmp - src) - current); + tmp2 = blogc_htmlentities(tmp3); + free(tmp3); + tmp3 = NULL; + sb_string_append(rv, ""); + sb_string_append_escaped(rv, tmp2); + sb_string_append(rv, ""); + current = tmp - src; + tmp = NULL; + free(tmp2); + tmp2 = NULL; + state = CONTENT_INLINE_START; + break; + + case CONTENT_INLINE_BACKTICKS_DOUBLE: + tmp = src + current; + do { + tmp = sb_str_find(tmp, '`'); + if (((tmp - src) < src_len) && *(tmp + 1) == '`') { + break; + } + tmp++; + } while (tmp != NULL && (tmp - src) < src_len); + if (tmp == NULL || ((tmp - src) >= src_len)) { + sb_string_append_c(rv, '`'); + sb_string_append_c(rv, '`'); + state = CONTENT_INLINE_START; + continue; + } + tmp3 = sb_strndup(src + current, (tmp - src) - current); + tmp2 = blogc_htmlentities(tmp3); + free(tmp3); + tmp3 = NULL; + sb_string_append(rv, ""); + sb_string_append_escaped(rv, tmp2); + sb_string_append(rv, ""); + current = tmp - src + 1; + tmp = NULL; + free(tmp2); + tmp2 = NULL; + state = CONTENT_INLINE_START; + break; + + case CONTENT_INLINE_LINK_START: + if (c == '[') { + state = CONTENT_INLINE_LINK_AUTO; + break; + } + start_link = current; + count = 1; + state = CONTENT_INLINE_LINK_CONTENT; + break; + + case CONTENT_INLINE_LINK_AUTO: + tmp = src + current; + do { + tmp = sb_str_find(tmp, ']'); + if (((tmp - src) < src_len) && *(tmp + 1) == ']') { + break; + } + tmp++; + } while (tmp != NULL && (tmp - src) < src_len); + if (tmp == NULL || ((tmp - src) >= src_len)) { + sb_string_append_c(rv, '['); + sb_string_append_c(rv, '['); + state = CONTENT_INLINE_START; + continue; + } + tmp2 = sb_strndup(src + current, (tmp - src) - current); + sb_string_append(rv, ""); + sb_string_append_escaped(rv, tmp2); + sb_string_append(rv, ""); + current = tmp - src + 1; + tmp = NULL; + free(tmp2); + tmp2 = NULL; + state = CONTENT_INLINE_START; + break; + + case CONTENT_INLINE_LINK_CONTENT: + if (c == '\\') { + current++; + break; + } + if (c == '[') { // links can be nested :/ + count++; + break; + } + if (c == ']') { + if (--count == 0) { + link1 = sb_strndup(src + start_link, current - start_link); + state = CONTENT_INLINE_LINK_URL_START; + } + } + break; + + case CONTENT_INLINE_LINK_URL_START: + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') + break; + if (c == '(') { + state = CONTENT_INLINE_LINK_URL; + start = current + 1; + break; + } + sb_string_append_c(rv, '['); + state = CONTENT_INLINE_START; + current = start_link; + start_link = 0; + continue; + + case CONTENT_INLINE_LINK_URL: + if (c == '\\') { + current++; + break; + } + if (c == ')') { + tmp2 = sb_strndup(src + start, current - start); + tmp3 = blogc_content_parse_inline(link1); + free(link1); + link1 = NULL; + sb_string_append(rv, "%s", tmp3); + free(tmp2); + tmp2 = NULL; + free(tmp3); + tmp3 = NULL; + state = CONTENT_INLINE_START; + break; + } + break; + + case CONTENT_INLINE_IMAGE_START: + // we use the same variables used for links, because why not? + if (c == '[') { + state = CONTENT_INLINE_IMAGE_ALT; + start_link = current + 1; + break; + } + sb_string_append_c(rv, '!'); + state = CONTENT_INLINE_START; + continue; + + case CONTENT_INLINE_IMAGE_ALT: + if (c == '\\') { + current++; + break; + } + if (c == ']') { + link1 = sb_strndup(src + start_link, current - start_link); + state = CONTENT_INLINE_IMAGE_URL_START; + } + break; + + case CONTENT_INLINE_IMAGE_URL_START: + if (c == ' ' || c == '\t' || c == '\n' || c == '\r') + break; + if (c == '(') { + state = CONTENT_INLINE_IMAGE_URL; + start = current + 1; + break; + } + sb_string_append_c(rv, '!'); + sb_string_append_c(rv, '['); + state = CONTENT_INLINE_START; + current = start_link; + start_link = 0; + continue; + + case CONTENT_INLINE_IMAGE_URL: + if (c == '\\') { + current++; + break; + } + if (c == ')') { + tmp2 = sb_strndup(src + start, current - start); + sb_string_append(rv, "\"");"); + free(tmp2); + tmp2 = NULL; + free(link1); + link1 = NULL; + state = CONTENT_INLINE_START; + break; + } + break; + + case CONTENT_INLINE_ENDASH: + if (c == '-') { + if (is_last) { + sb_string_append(rv, "–"); + state = CONTENT_INLINE_START; // wat + break; + } + state = CONTENT_INLINE_EMDASH; + break; + } + sb_string_append_c(rv, '-'); + state = CONTENT_INLINE_START; + continue; + + case CONTENT_INLINE_EMDASH: + if (c == '-') { + sb_string_append(rv, "—"); + state = CONTENT_INLINE_START; + break; + } + sb_string_append(rv, "–"); + state = CONTENT_INLINE_START; + continue; + + case CONTENT_INLINE_LINE_BREAK_START: + if (c == ' ') { + if (is_last) { + sb_string_append(rv, "
"); + state = CONTENT_INLINE_START; // wat + break; + } + count = 2; + state = CONTENT_INLINE_LINE_BREAK; + break; + } + sb_string_append_c(rv, ' '); + state = CONTENT_INLINE_START; + continue; + + case CONTENT_INLINE_LINE_BREAK: + if (c == ' ') { + if (is_last) { + sb_string_append(rv, "
"); + state = CONTENT_INLINE_START; // wat + break; + } + count++; + break; + } + if (c == '\n' || c == '\r') { + sb_string_append_printf(rv, "
%c", c); + state = CONTENT_INLINE_START; + break; + } + for (size_t i = 0; i < count; i++) + sb_string_append_c(rv, ' '); + state = CONTENT_INLINE_START; + continue; + } + current++; + } + + switch (state) { + + // if after the end of the loop we are on any of the following states, + // we must call the parser again, from start_link + case CONTENT_INLINE_IMAGE_START: + case CONTENT_INLINE_IMAGE_ALT: + case CONTENT_INLINE_IMAGE_URL_START: + case CONTENT_INLINE_IMAGE_URL: + sb_string_append_c(rv, '!'); + + case CONTENT_INLINE_LINK_CONTENT: + case CONTENT_INLINE_LINK_URL_START: + case CONTENT_INLINE_LINK_URL: + tmp2 = blogc_content_parse_inline(src + start_link); + sb_string_append_c(rv, '['); + sb_string_append_escaped(rv, tmp2); // no need to free, as it wil be done below. + break; + + // add all the other states here explicitly, so the compiler helps us + // not missing any new state that should be handled. + case CONTENT_INLINE_START: + case CONTENT_INLINE_ASTERISK: + case CONTENT_INLINE_ASTERISK_DOUBLE: + case CONTENT_INLINE_UNDERSCORE: + case CONTENT_INLINE_UNDERSCORE_DOUBLE: + case CONTENT_INLINE_BACKTICKS: + case CONTENT_INLINE_BACKTICKS_DOUBLE: + case CONTENT_INLINE_LINK_START: + case CONTENT_INLINE_LINK_AUTO: + case CONTENT_INLINE_ENDASH: + case CONTENT_INLINE_EMDASH: + case CONTENT_INLINE_LINE_BREAK_START: + case CONTENT_INLINE_LINE_BREAK: + break; + } + + free(tmp2); + free(tmp3); + free(link1); + + return sb_string_free(rv, false); +} + + +char* +blogc_content_parse_inline(const char *src) +{ + return blogc_content_parse_inline_internal(src, strlen(src)); +} + + +bool +blogc_is_ordered_list_item(const char *str, size_t prefix_len) +{ + if (str == NULL) + return false; + + if (strlen(str) < 2) + return false; + + size_t i; + + for (i = 0; str[i] >= '0' && str[i] <= '9'; i++); + + if (i == 0) + return false; + if (str[i] != '.') + return false; + + for (i++; i < prefix_len && (str[i] == ' ' || str[i] == '\t'); i++); + + if (str[i] == '\0') + return false; + + return i == prefix_len; +} + + +char* +blogc_content_parse(const char *src, size_t *end_excerpt, char **description) +{ + // src is always nul-terminated. + size_t src_len = strlen(src); + + size_t current = 0; + size_t start = 0; + size_t start2 = 0; + size_t end = 0; + size_t eend = 0; + size_t real_end = 0; + + unsigned int header_level = 0; + char *prefix = NULL; + size_t prefix_len = 0; + char *tmp = NULL; + char *tmp2 = NULL; + char *parsed = NULL; + char *slug = NULL; + + // this isn't empty because we need some reasonable default value in the + // unlikely case that we need to print some line ending before evaluating + // the "real" value. + char line_ending[3] = "\n"; + bool line_ending_found = false; + + char d = '\0'; + + sb_slist_t *lines = NULL; + sb_slist_t *lines2 = NULL; + + sb_string_t *rv = sb_string_new(); + sb_string_t *tmp_str = NULL; + + blogc_content_parser_state_t state = CONTENT_START_LINE; + + while (current < src_len) { + char c = src[current]; + bool is_last = current == src_len - 1; + + if (c == '\n' || c == '\r') { + if ((current + 1) < src_len) { + if ((c == '\n' && src[current + 1] == '\r') || + (c == '\r' && src[current + 1] == '\n')) + { + if (!line_ending_found) { + line_ending[0] = c; + line_ending[1] = src[current + 1]; + line_ending[2] = '\0'; + line_ending_found = true; + } + real_end = current; + c = src[++current]; + is_last = current == src_len - 1; + } + } + if (!line_ending_found) { + line_ending[0] = c; + line_ending[1] = '\0'; + line_ending_found = true; + } + } + + switch (state) { + + case CONTENT_START_LINE: + if (c == '\n' || c == '\r' || is_last) + break; + start = current; + if (c == '.') { + if (end_excerpt != NULL) { + eend = rv->len; // fuck it + state = CONTENT_EXCERPT; + break; + } + } + if (c == '#') { + header_level = 1; + state = CONTENT_HEADER; + break; + } + if (c == '*' || c == '+' || c == '-') { + start2 = current; + state = CONTENT_UNORDERED_LIST_OR_HORIZONTAL_RULE; + d = c; + break; + } + if (c >= '0' && c <= '9') { + start2 = current; + state = CONTENT_ORDERED_LIST; + break; + } + if (c == ' ' || c == '\t') { + start2 = current; + state = CONTENT_CODE; + break; + } + if (c == '<') { + state = CONTENT_HTML; + break; + } + if (c == '>') { + state = CONTENT_BLOCKQUOTE; + start2 = current; + break; + } + state = CONTENT_PARAGRAPH; + break; + + case CONTENT_EXCERPT: + if (end_excerpt != NULL) { + if (c == '.') + break; + if (c == '\n' || c == '\r') { + state = CONTENT_EXCERPT_END; + break; + } + } + eend = 0; + state = CONTENT_PARAGRAPH; + break; + + case CONTENT_EXCERPT_END: + if (end_excerpt != NULL) { + if (c == '\n' || c == '\r') { + *end_excerpt = eend; + state = CONTENT_START_LINE; + break; + } + } + eend = 0; + state = CONTENT_PARAGRAPH_END; + break; + + case CONTENT_HEADER: + if (c == '#') { + header_level += 1; + break; + } + if (c == ' ' || c == '\t') { + state = CONTENT_HEADER_TITLE_START; + break; + } + state = CONTENT_PARAGRAPH; + break; + + case CONTENT_HEADER_TITLE_START: + if (c == ' ' || c == '\t') + break; + start = current; + if (c != '\n' && c != '\r') { + state = CONTENT_HEADER_TITLE; + break; + } + + case CONTENT_HEADER_TITLE: + if (c == '\n' || c == '\r' || is_last) { + end = is_last && c != '\n' && c != '\r' ? src_len : + (real_end != 0 ? real_end : current); + tmp = sb_strndup(src + start, end - start); + parsed = blogc_content_parse_inline(tmp); + slug = blogc_slugify(tmp); + if (slug == NULL) + sb_string_append_printf(rv, "%s%s", + header_level, parsed, header_level, line_ending); + else + sb_string_append_printf(rv, "%s%s", + header_level, slug, parsed, header_level, + line_ending); + free(slug); + free(parsed); + parsed = NULL; + free(tmp); + tmp = NULL; + state = CONTENT_START_LINE; + start = current; + } + break; + + case CONTENT_HTML: + if (c == '\n' || c == '\r' || is_last) { + state = CONTENT_HTML_END; + end = is_last && c != '\n' && c != '\r' ? src_len : + (real_end != 0 ? real_end : current); + } + if (!is_last) + break; + + case CONTENT_HTML_END: + if (c == '\n' || c == '\r' || is_last) { + tmp = sb_strndup(src + start, end - start); + sb_string_append_printf(rv, "%s%s", tmp, line_ending); + free(tmp); + tmp = NULL; + state = CONTENT_START_LINE; + start = current; + } + else + state = CONTENT_HTML; + break; + + case CONTENT_BLOCKQUOTE: + if (c == ' ' || c == '\t') + break; + prefix = sb_strndup(src + start, current - start); + state = CONTENT_BLOCKQUOTE_START; + break; + + case CONTENT_BLOCKQUOTE_START: + if (c == '\n' || c == '\r' || is_last) { + end = is_last && c != '\n' && c != '\r' ? src_len : + (real_end != 0 ? real_end : current); + tmp = sb_strndup(src + start2, end - start2); + if (sb_str_starts_with(tmp, prefix)) { + lines = sb_slist_append(lines, sb_strdup(tmp + strlen(prefix))); + state = CONTENT_BLOCKQUOTE_END; + } + else { + state = CONTENT_PARAGRAPH; + free(prefix); + prefix = NULL; + sb_slist_free_full(lines, free); + lines = NULL; + if (is_last) { + free(tmp); + tmp = NULL; + continue; + } + } + free(tmp); + tmp = NULL; + } + if (!is_last) + break; + + case CONTENT_BLOCKQUOTE_END: + if (c == '\n' || c == '\r' || is_last) { + tmp_str = sb_string_new(); + for (sb_slist_t *l = lines; l != NULL; l = l->next) + sb_string_append_printf(tmp_str, "%s%s", l->data, + line_ending); + // do not propagate description to blockquote parsing, + // because we just want paragraphs from first level of + // content. + tmp = blogc_content_parse(tmp_str->str, NULL, NULL); + sb_string_append_printf(rv, "
%s
%s", + tmp, line_ending); + free(tmp); + tmp = NULL; + sb_string_free(tmp_str, true); + tmp_str = NULL; + sb_slist_free_full(lines, free); + lines = NULL; + free(prefix); + prefix = NULL; + state = CONTENT_START_LINE; + start2 = current; + } + else { + start2 = current; + state = CONTENT_BLOCKQUOTE_START; + } + break; + + case CONTENT_CODE: + if (c == ' ' || c == '\t') + break; + prefix = sb_strndup(src + start, current - start); + state = CONTENT_CODE_START; + break; + + case CONTENT_CODE_START: + if (c == '\n' || c == '\r' || is_last) { + end = is_last && c != '\n' && c != '\r' ? src_len : + (real_end != 0 ? real_end : current); + tmp = sb_strndup(src + start2, end - start2); + if (sb_str_starts_with(tmp, prefix)) { + lines = sb_slist_append(lines, sb_strdup(tmp + strlen(prefix))); + state = CONTENT_CODE_END; + } + else { + state = CONTENT_PARAGRAPH; + free(prefix); + prefix = NULL; + sb_slist_free_full(lines, free); + lines = NULL; + free(tmp); + tmp = NULL; + if (is_last) + continue; + break; + } + free(tmp); + tmp = NULL; + } + if (!is_last) + break; + + case CONTENT_CODE_END: + if (c == '\n' || c == '\r' || is_last) { + sb_string_append(rv, "
");
+                    for (sb_slist_t *l = lines; l != NULL; l = l->next) {
+                        char *tmp_line = blogc_htmlentities(l->data);
+                        if (l->next == NULL)
+                            sb_string_append_printf(rv, "%s", tmp_line);
+                        else
+                            sb_string_append_printf(rv, "%s%s", tmp_line,
+                                line_ending);
+                        free(tmp_line);
+                    }
+                    sb_string_append_printf(rv, "
%s", line_ending); + sb_slist_free_full(lines, free); + lines = NULL; + free(prefix); + prefix = NULL; + state = CONTENT_START_LINE; + start2 = current; + } + else { + start2 = current; + state = CONTENT_CODE_START; + } + break; + + case CONTENT_UNORDERED_LIST_OR_HORIZONTAL_RULE: + if (c == d) { + state = CONTENT_HORIZONTAL_RULE; + if (is_last) + continue; + break; + } + if (c == ' ' || c == '\t') + break; + prefix = sb_strndup(src + start, current - start); + state = CONTENT_UNORDERED_LIST_START; + break; + + case CONTENT_HORIZONTAL_RULE: + if (c == d && !is_last) { + break; + } + if (c == '\n' || c == '\r' || is_last) { + sb_string_append_printf(rv, "
%s", line_ending); + state = CONTENT_START_LINE; + start = current; + d = '\0'; + break; + } + state = CONTENT_PARAGRAPH; + break; + + case CONTENT_UNORDERED_LIST_START: + if (c == '\n' || c == '\r' || is_last) { + end = is_last && c != '\n' && c != '\r' ? src_len : + (real_end != 0 ? real_end : current); + tmp = sb_strndup(src + start2, end - start2); + tmp2 = sb_strdup_printf("%-*s", strlen(prefix), ""); + if (sb_str_starts_with(tmp, prefix)) { + if (lines2 != NULL) { + tmp_str = sb_string_new(); + for (sb_slist_t *l = lines2; l != NULL; l = l->next) { + if (l->next == NULL) + sb_string_append_printf(tmp_str, "%s", l->data); + else + sb_string_append_printf(tmp_str, "%s%s", l->data, + line_ending); + } + sb_slist_free_full(lines2, free); + lines2 = NULL; + parsed = blogc_content_parse_inline(tmp_str->str); + sb_string_free(tmp_str, true); + lines = sb_slist_append(lines, sb_strdup(parsed)); + free(parsed); + parsed = NULL; + } + lines2 = sb_slist_append(lines2, sb_strdup(tmp + strlen(prefix))); + } + else if (sb_str_starts_with(tmp, tmp2)) { + lines2 = sb_slist_append(lines2, sb_strdup(tmp + strlen(prefix))); + } + else { + state = CONTENT_PARAGRAPH_END; + free(tmp); + tmp = NULL; + free(tmp2); + tmp2 = NULL; + free(prefix); + prefix = NULL; + sb_slist_free_full(lines, free); + sb_slist_free_full(lines2, free); + lines = NULL; + if (is_last) + continue; + break; + } + free(tmp); + tmp = NULL; + free(tmp2); + tmp2 = NULL; + state = CONTENT_UNORDERED_LIST_END; + } + if (!is_last) + break; + + case CONTENT_UNORDERED_LIST_END: + if (c == '\n' || c == '\r' || is_last) { + if (lines2 != NULL) { + // FIXME: avoid repeting the code below + tmp_str = sb_string_new(); + for (sb_slist_t *l = lines2; l != NULL; l = l->next) { + if (l->next == NULL) + sb_string_append_printf(tmp_str, "%s", l->data); + else + sb_string_append_printf(tmp_str, "%s%s", l->data, + line_ending); + } + sb_slist_free_full(lines2, free); + lines2 = NULL; + parsed = blogc_content_parse_inline(tmp_str->str); + sb_string_free(tmp_str, true); + lines = sb_slist_append(lines, sb_strdup(parsed)); + free(parsed); + parsed = NULL; + } + sb_string_append_printf(rv, "
    %s", line_ending); + for (sb_slist_t *l = lines; l != NULL; l = l->next) + sb_string_append_printf(rv, "
  • %s
  • %s", l->data, + line_ending); + sb_string_append_printf(rv, "
%s", line_ending); + sb_slist_free_full(lines, free); + lines = NULL; + free(prefix); + prefix = NULL; + state = CONTENT_START_LINE; + start2 = current; + } + else { + start2 = current; + state = CONTENT_UNORDERED_LIST_START; + } + break; + + case CONTENT_ORDERED_LIST: + if (c >= '0' && c <= '9') + break; + if (c == '.') { + state = CONTENT_ORDERED_LIST_SPACE; + break; + } + state = CONTENT_PARAGRAPH; + if (is_last) + continue; + break; + + case CONTENT_ORDERED_LIST_SPACE: + if (c == ' ' || c == '\t') + break; + prefix_len = current - start; + state = CONTENT_ORDERED_LIST_START; + if (c != '\n' && c != '\r' && !is_last) + break; + + case CONTENT_ORDERED_LIST_START: + if (c == '\n' || c == '\r' || is_last) { + end = is_last && c != '\n' && c != '\r' ? src_len : + (real_end != 0 ? real_end : current); + tmp = sb_strndup(src + start2, end - start2); + tmp2 = sb_strdup_printf("%-*s", prefix_len, ""); + if (blogc_is_ordered_list_item(tmp, prefix_len)) { + if (lines2 != NULL) { + tmp_str = sb_string_new(); + for (sb_slist_t *l = lines2; l != NULL; l = l->next) { + if (l->next == NULL) + sb_string_append_printf(tmp_str, "%s", l->data); + else + sb_string_append_printf(tmp_str, "%s%s", l->data, + line_ending); + } + sb_slist_free_full(lines2, free); + lines2 = NULL; + parsed = blogc_content_parse_inline(tmp_str->str); + sb_string_free(tmp_str, true); + lines = sb_slist_append(lines, sb_strdup(parsed)); + free(parsed); + parsed = NULL; + } + lines2 = sb_slist_append(lines2, sb_strdup(tmp + prefix_len)); + } + else if (sb_str_starts_with(tmp, tmp2)) { + lines2 = sb_slist_append(lines2, sb_strdup(tmp + prefix_len)); + } + else { + state = CONTENT_PARAGRAPH_END; + free(tmp); + tmp = NULL; + free(tmp2); + tmp2 = NULL; + free(parsed); + parsed = NULL; + sb_slist_free_full(lines, free); + sb_slist_free_full(lines2, free); + lines = NULL; + if (is_last) + continue; + break; + } + free(tmp); + tmp = NULL; + free(tmp2); + tmp2 = NULL; + state = CONTENT_ORDERED_LIST_END; + } + if (!is_last) + break; + + case CONTENT_ORDERED_LIST_END: + if (c == '\n' || c == '\r' || is_last) { + if (lines2 != NULL) { + // FIXME: avoid repeting the code below + tmp_str = sb_string_new(); + for (sb_slist_t *l = lines2; l != NULL; l = l->next) { + if (l->next == NULL) + sb_string_append_printf(tmp_str, "%s", l->data); + else + sb_string_append_printf(tmp_str, "%s%s", l->data, + line_ending); + } + sb_slist_free_full(lines2, free); + lines2 = NULL; + parsed = blogc_content_parse_inline(tmp_str->str); + sb_string_free(tmp_str, true); + lines = sb_slist_append(lines, sb_strdup(parsed)); + free(parsed); + parsed = NULL; + } + sb_string_append_printf(rv, "
    %s", line_ending); + for (sb_slist_t *l = lines; l != NULL; l = l->next) + sb_string_append_printf(rv, "
  1. %s
  2. %s", l->data, + line_ending); + sb_string_append_printf(rv, "
%s", line_ending); + sb_slist_free_full(lines, free); + lines = NULL; + free(prefix); + prefix = NULL; + state = CONTENT_START_LINE; + start2 = current; + } + else { + start2 = current; + state = CONTENT_ORDERED_LIST_START; + } + break; + + case CONTENT_PARAGRAPH: + if (c == '\n' || c == '\r' || is_last) { + state = CONTENT_PARAGRAPH_END; + end = is_last && c != '\n' && c != '\r' ? src_len : + (real_end != 0 ? real_end : current); + } + if (!is_last) + break; + + case CONTENT_PARAGRAPH_END: + if (c == '\n' || c == '\r' || is_last) { + tmp = sb_strndup(src + start, end - start); + if (description != NULL && *description == NULL) + *description = blogc_fix_description(tmp); + parsed = blogc_content_parse_inline(tmp); + sb_string_append_printf(rv, "

%s

%s", parsed, + line_ending); + free(parsed); + parsed = NULL; + free(tmp); + tmp = NULL; + state = CONTENT_START_LINE; + start = current; + } + else + state = CONTENT_PARAGRAPH; + break; + + } + + current++; + } + + return sb_string_free(rv, false); +} diff --git a/src/blogc/content-parser.h b/src/blogc/content-parser.h new file mode 100644 index 0000000..37e38d7 --- /dev/null +++ b/src/blogc/content-parser.h @@ -0,0 +1,23 @@ +/* + * blogc: A blog compiler. + * Copyright (C) 2015-2016 Rafael G. Martins + * + * This program can be distributed under the terms of the BSD License. + * See the file LICENSE. + */ + +#ifndef _CONTENT_PARSER_H +#define _CONTENT_PARSER_H + +#include +#include + +char* blogc_slugify(const char *str); +char* blogc_htmlentities(const char *str); +char* blogc_fix_description(const char *paragraph); +char* blogc_content_parse_inline(const char *src); +bool blogc_is_ordered_list_item(const char *str, size_t prefix_len); +char* blogc_content_parse(const char *src, size_t *end_excerpt, + char **description); + +#endif /* _CONTENT_PARSER_H */ diff --git a/src/blogc/datetime-parser.c b/src/blogc/datetime-parser.c new file mode 100644 index 0000000..28efb74 --- /dev/null +++ b/src/blogc/datetime-parser.c @@ -0,0 +1,386 @@ +/* + * blogc: A blog compiler. + * Copyright (C) 2015-2016 Rafael G. Martins + * + * This program can be distributed under the terms of the BSD License. + * See the file LICENSE. + */ + +#ifdef HAVE_CONFIG_H +#include +#endif /* HAVE_CONFIG_H */ + +#ifdef HAVE_TIME_H +#include +#endif /* HAVE_TIME_H */ + +#include + +#include "error.h" +#include "datetime-parser.h" +#include "../common/utils.h" + + +typedef enum { + DATETIME_FIRST_YEAR = 1, + DATETIME_SECOND_YEAR, + DATETIME_THIRD_YEAR, + DATETIME_FOURTH_YEAR, + DATETIME_FIRST_HYPHEN, + DATETIME_FIRST_MONTH, + DATETIME_SECOND_MONTH, + DATETIME_SECOND_HYPHEN, + DATETIME_FIRST_DAY, + DATETIME_SECOND_DAY, + DATETIME_SPACE, + DATETIME_FIRST_HOUR, + DATETIME_SECOND_HOUR, + DATETIME_FIRST_COLON, + DATETIME_FIRST_MINUTE, + DATETIME_SECOND_MINUTE, + DATETIME_SECOND_COLON, + DATETIME_FIRST_SECOND, + DATETIME_SECOND_SECOND, + DATETIME_DONE, +} blogc_datetime_state_t; + + +char* +blogc_convert_datetime(const char *orig, const char *format, + blogc_error_t **err) +{ + if (err == NULL || *err != NULL) + return NULL; + +#ifndef HAVE_TIME_H + + *err = blogc_error_new(BLOGC_WARNING_DATETIME_PARSER, + "Your operating system does not supports the datetime functionalities " + "used by blogc. Sorry."); + return NULL; + +#else + + struct tm t; + memset(&t, 0, sizeof(struct tm)); + t.tm_isdst = -1; + + blogc_datetime_state_t state = DATETIME_FIRST_YEAR; + int tmp = 0; + int diff = '0'; + + for (unsigned int i = 0; orig[i] != '\0'; i++) { + char c = orig[i]; + + switch (state) { + + case DATETIME_FIRST_YEAR: + if (c >= '0' && c <= '9') { + tmp += (c - diff) * 1000; + state = DATETIME_SECOND_YEAR; + break; + } + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid first digit of year. " + "Found '%c', must be integer >= 0 and <= 9.", c); + break; + + case DATETIME_SECOND_YEAR: + if (c >= '0' && c <= '9') { + tmp += (c - diff) * 100; + state = DATETIME_THIRD_YEAR; + break; + } + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid second digit of year. " + "Found '%c', must be integer >= 0 and <= 9.", c); + break; + + case DATETIME_THIRD_YEAR: + if (c >= '0' && c <= '9') { + tmp += (c - diff) * 10; + state = DATETIME_FOURTH_YEAR; + break; + } + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid third digit of year. " + "Found '%c', must be integer >= 0 and <= 9.", c); + break; + + case DATETIME_FOURTH_YEAR: + if (c >= '0' && c <= '9') { + tmp += c - diff - 1900; + if (tmp < 0) { + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid year. Found %d, must be >= 1900.", + tmp + 1900); + break; + } + t.tm_year = tmp; + state = DATETIME_FIRST_HYPHEN; + break; + } + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid fourth digit of year. " + "Found '%c', must be integer >= 0 and <= 9.", c); + break; + + case DATETIME_FIRST_HYPHEN: + if (c == '-') { + tmp = 0; + state = DATETIME_FIRST_MONTH; + break; + } + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid separator between year and month. " + "Found '%c', must be '-'.", c); + break; + + case DATETIME_FIRST_MONTH: + if (c >= '0' && c <= '1') { + tmp += (c - diff) * 10; + state = DATETIME_SECOND_MONTH; + break; + } + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid first digit of month. " + "Found '%c', must be integer >= 0 and <= 1.", c); + break; + + case DATETIME_SECOND_MONTH: + if (c >= '0' && c <= '9') { + tmp += c - diff - 1; + if (tmp < 0 || tmp > 11) { + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid month. Found %d, must be >= 1 and <= 12.", + tmp + 1); + break; + } + t.tm_mon = tmp; + state = DATETIME_SECOND_HYPHEN; + break; + } + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid second digit of month. " + "Found '%c', must be integer >= 0 and <= 9.", c); + break; + + case DATETIME_SECOND_HYPHEN: + if (c == '-') { + tmp = 0; + state = DATETIME_FIRST_DAY; + break; + } + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid separator between month and day. " + "Found '%c', must be '-'.", c); + break; + + case DATETIME_FIRST_DAY: + if (c >= '0' && c <= '3') { + tmp += (c - diff) * 10; + state = DATETIME_SECOND_DAY; + break; + } + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid first digit of day. " + "Found '%c', must be integer >= 0 and <= 3.", c); + break; + + case DATETIME_SECOND_DAY: + if (c >= '0' && c <= '9') { + tmp += c - diff; + if (tmp < 1 || tmp > 31) { + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid day. Found %d, must be >= 1 and <= 31.", + tmp); + break; + } + t.tm_mday = tmp; + state = DATETIME_SPACE; + break; + } + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid second digit of day. " + "Found '%c', must be integer >= 0 and <= 9.", c); + break; + + case DATETIME_SPACE: + if (c == ' ') { + tmp = 0; + state = DATETIME_FIRST_HOUR; + break; + } + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid separator between date and time. " + "Found '%c', must be ' ' (empty space).", c); + break; + + case DATETIME_FIRST_HOUR: + if (c >= '0' && c <= '2') { + tmp += (c - diff) * 10; + state = DATETIME_SECOND_HOUR; + break; + } + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid first digit of hours. " + "Found '%c', must be integer >= 0 and <= 2.", c); + break; + + case DATETIME_SECOND_HOUR: + if (c >= '0' && c <= '9') { + tmp += c - diff; + if (tmp < 0 || tmp > 23) { + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid hours. Found %d, must be >= 0 and <= 23.", + tmp); + break; + } + t.tm_hour = tmp; + state = DATETIME_FIRST_COLON; + break; + } + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid second digit of hours. " + "Found '%c', must be integer >= 0 and <= 9.", c); + break; + + case DATETIME_FIRST_COLON: + if (c == ':') { + tmp = 0; + state = DATETIME_FIRST_MINUTE; + break; + } + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid separator between hours and minutes. " + "Found '%c', must be ':'.", c); + break; + + case DATETIME_FIRST_MINUTE: + if (c >= '0' && c <= '5') { + tmp += (c - diff) * 10; + state = DATETIME_SECOND_MINUTE; + break; + } + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid first digit of minutes. " + "Found '%c', must be integer >= 0 and <= 5.", c); + break; + + case DATETIME_SECOND_MINUTE: + if (c >= '0' && c <= '9') { + tmp += c - diff; + if (tmp < 0 || tmp > 59) { + // this won't happen because we are restricting the digits + // to 00-59 already, but lets keep the code here for + // reference. + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid minutes. Found %d, must be >= 0 and <= 59.", + tmp); + break; + } + t.tm_min = tmp; + state = DATETIME_SECOND_COLON; + break; + } + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid second digit of minutes. " + "Found '%c', must be integer >= 0 and <= 9.", c); + break; + + case DATETIME_SECOND_COLON: + if (c == ':') { + tmp = 0; + state = DATETIME_FIRST_SECOND; + break; + } + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid separator between minutes and seconds. " + "Found '%c', must be ':'.", c); + break; + + case DATETIME_FIRST_SECOND: + if (c >= '0' && c <= '6') { + tmp += (c - diff) * 10; + state = DATETIME_SECOND_SECOND; + break; + } + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid first digit of seconds. " + "Found '%c', must be integer >= 0 and <= 6.", c); + break; + + case DATETIME_SECOND_SECOND: + if (c >= '0' && c <= '9') { + tmp += c - diff; + if (tmp < 0 || tmp > 60) { + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid seconds. Found %d, must be >= 0 and <= 60.", + tmp); + break; + } + t.tm_sec = tmp; + state = DATETIME_DONE; + break; + } + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid second digit of seconds. " + "Found '%c', must be integer >= 0 and <= 9.", c); + break; + + case DATETIME_DONE: + // well, its done ;) + break; + } + + if (*err != NULL) + return NULL; + } + + if (*err == NULL) { + switch (state) { + case DATETIME_FIRST_YEAR: + case DATETIME_SECOND_YEAR: + case DATETIME_THIRD_YEAR: + case DATETIME_FOURTH_YEAR: + case DATETIME_FIRST_HYPHEN: + case DATETIME_FIRST_MONTH: + case DATETIME_SECOND_MONTH: + case DATETIME_SECOND_HYPHEN: + case DATETIME_FIRST_DAY: + case DATETIME_SECOND_DAY: + case DATETIME_FIRST_HOUR: + case DATETIME_SECOND_HOUR: + case DATETIME_FIRST_MINUTE: + case DATETIME_SECOND_MINUTE: + case DATETIME_FIRST_SECOND: + case DATETIME_SECOND_SECOND: + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Invalid datetime string. " + "Found '%s', formats allowed are: 'yyyy-mm-dd hh:mm:ss', " + "'yyyy-mm-dd hh:ss', 'yyyy-mm-dd hh' and 'yyyy-mm-dd'.", + orig); + return NULL; + + case DATETIME_SPACE: + case DATETIME_FIRST_COLON: + case DATETIME_SECOND_COLON: + case DATETIME_DONE: + break; // these states are ok + } + } + + mktime(&t); + + char buf[1024]; + if (0 == strftime(buf, sizeof(buf), format, &t)) { + *err = blogc_error_new_printf(BLOGC_WARNING_DATETIME_PARSER, + "Failed to format DATE variable, FORMAT is too long: %s", + format); + return NULL; + } + + return sb_strdup(buf); + +#endif +} diff --git a/src/blogc/datetime-parser.h b/src/blogc/datetime-parser.h new file mode 100644 index 0000000..a5087b3 --- /dev/null +++ b/src/blogc/datetime-parser.h @@ -0,0 +1,17 @@ +/* + * blogc: A blog compiler. + * Copyright (C) 2015-2016 Rafael G. Martins + * + * This program can be distributed under the terms of the BSD License. + * See the file LICENSE. + */ + +#ifndef _DATETIME_H +#define _DATETIME_H + +#include "error.h" + +char* blogc_convert_datetime(const char *orig, const char *format, + blogc_error_t **err); + +#endif /* _DATETIME_H */ diff --git a/src/blogc/debug.c b/src/blogc/debug.c new file mode 100644 index 0000000..2840f60 --- /dev/null +++ b/src/blogc/debug.c @@ -0,0 +1,80 @@ +/* + * blogc: A blog compiler. + * Copyright (C) 2015-2016 Rafael G. Martins + * + * This program can be distributed under the terms of the BSD License. + * See the file LICENSE. + */ + +#include + +#include "template-parser.h" +#include "../common/utils.h" +#include "debug.h" + + +static const char* +get_operator(blogc_template_stmt_operator_t op) +{ + if (op & BLOGC_TEMPLATE_OP_NEQ) + return "!="; + if (op & BLOGC_TEMPLATE_OP_EQ) { + if (op & BLOGC_TEMPLATE_OP_LT) + return "<="; + else if (op & BLOGC_TEMPLATE_OP_GT) + return ">="; + return "=="; + } + if (op & BLOGC_TEMPLATE_OP_LT) + return "<"; + else if (op & BLOGC_TEMPLATE_OP_GT) + return ">"; + return ""; +} + + +void +blogc_debug_template(sb_slist_t *stmts) +{ + for (sb_slist_t *tmp = stmts; tmp != NULL; tmp = tmp->next) { + blogc_template_stmt_t *data = tmp->data; + fprintf(stderr, "DEBUG: