#include "config.h" #include "logerr.h" #include "tar.h" #include "gistatic.h" #include #include #include #include #include #include #include #include #include #include #include #include #ifdef TEST #include "tests-lib.h" #endif static const int EXIT_ERROR = 1; static const int EXIT_USAGE = 2; #define PROGNAME "gistatic" static const char *const CATALOG_NAME = PROGNAME; static nl_catd catalog_descriptor = NULL; #define MSG_DEFAULT_TITLE 1 #define MSG_LOGO_ALT_INDEX 2 #define MSG_LOGO_ALT_REPOSITORY 3 #define MSG_FOOTER_TEMPLATE 4 #define MSG_LANG 5 #define MSG_NAME 6 #define MSG_DESCRIPTION 7 #define MSG_LAST_COMMIT 8 #define MSG_USAGE 9 #define MSG_HELP 10 #define MSG_COMMIT_FEED 11 #define MSG_TAGS_FEED 12 #define MSG_NAV_FILES 13 #define MSG_NAV_LOG 14 #define MSG_NAV_REFS 15 #define MSG_THEAD_BRANCH 16 #define MSG_THEAD_COMMITMSG 17 #define MSG_THEAD_AUTHOR 18 #define MSG_THEAD_DATE 19 #define MSG_THEAD_TAG 20 #define MSG_MISSING_URL 21 #define MSG_MISSING_ARGS 22 #define MSG_INCOMPATIBLE_OPTIONS 23 #define MSG_INDEX_DESCRIPTION 24 static const char *const MSGS[] = { "", [MSG_DEFAULT_TITLE]="Repositories", [MSG_LOGO_ALT_INDEX]="Logo image of the repository list", [MSG_LOGO_ALT_REPOSITORY]="Logo image of the repository", [MSG_FOOTER_TEMPLATE]="Generated with %s", [MSG_LANG]="en", [MSG_NAME]="Name", [MSG_DESCRIPTION]="Description", [MSG_LAST_COMMIT]="Last commit", [MSG_USAGE]="" "Usage:\n" " " PROGNAME " -i -o DIRECTORY REPOSITORY...\n" " " PROGNAME " -o DIRECTORY -u CLONE_URL REPOSITORY\n" " " PROGNAME " [-hV]\n" "", [MSG_HELP]="" "\n" "Options:\n" " -i build the index page of the repositories\n" " -u CLONE_URL address to be shown alongside \"git clone\"\n" " -o DIRECTORY output where to place the generated files\n" " -h, --help show this help\n" " -V, --version print the version number\n" "\n" "See \"man gistatic\" for more information.\n" "", [MSG_COMMIT_FEED]="commit feed", [MSG_TAGS_FEED]="tags feed", [MSG_NAV_FILES]="files", [MSG_NAV_LOG]="log", [MSG_NAV_REFS]="refs", [MSG_THEAD_BRANCH]="Branch", [MSG_THEAD_COMMITMSG]="Commit message", [MSG_THEAD_AUTHOR]="Author", [MSG_THEAD_DATE]="Date", [MSG_THEAD_TAG]="Tag", [MSG_MISSING_URL]="Missing '-u CLONE_URL'", [MSG_MISSING_ARGS]="Missing [PATH | [PATHS]]", [MSG_INCOMPATIBLE_OPTIONS]="Incompatible options -u and -i", [MSG_INDEX_DESCRIPTION]="Index of repositories", NULL }; #ifdef TEST static void dump_translatable_strings(void) { const size_t size = strlen(__FILE__) - strlen(".c") + strlen(".msg") + sizeof('\0'); char *const catalog_path = malloc(size); assert(catalog_path); strcpy(catalog_path, __FILE__); catalog_path[strlen(__FILE__) - strlen(".c")] = '\0'; strcat(catalog_path, ".msg"); FILE *const f = fopen(catalog_path, "w"); assert(f); for (size_t i = 1; MSGS[i] != NULL; i++) { assert(fprintf(f, "%ld %s\n\n", i, MSGS[i]) > 0); } assert(fclose(f) == 0); free(catalog_path); } #endif static const char *const LOGO_STR = "" "\n" "\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n"; static const char *const STYLE_STR = "" ":root {\n" " --color: black;\n" " --background-color: white;\n" " --hover-color: hsl(0, 0%, 93%);\n" " --nav-color: hsl(0, 0%, 87%);\n" "}\n" "\n" "@media(prefers-color-scheme: dark) {\n" " :root {\n" " --color: white;\n" " --background-color: black;\n" " --hover-color: hsl(0, 0%, 7%);\n" " --nav-color: hsl(0, 0%, 13%);\n" " }\n" "\n" " body {\n" " color: var(--color);\n" " background-color: var(--background-color);\n" " }\n" "\n" " a {\n" " color: hsl(211, 100%, 60%);\n" " }\n" "\n" " a:visited {\n" " color: hsl(242, 100%, 80%);\n" " }\n" "}\n" "\n" "body {\n" " font-family: monospace;\n" " max-width: 1100px;\n" " margin: 0 auto 0 auto;\n" "}\n" "\n" ".logo {\n" " height: 6em;\n" " width: 6em;\n" "}\n" "\n" ".header-horizontal-grouping {\n" " display: flex;\n" " align-items: center;\n" " margin-top: 1em;\n" " margin-bottom: 1em;\n" "}\n" "\n" ".header-description {\n" " margin-left: 2em;\n" "}\n" "\n" "nav {\n" " margin-top: 2em;\n" "}\n" "\n" "nav ul {\n" " display: flex;\n" " list-style-type: none;\n" " margin-bottom: 0;\n" "}\n" "\n" "nav li {\n" " margin-left: 10px;\n" "}\n" "\n" "nav a, nav a:visited {\n" " padding: 2px 8px 0px 8px;\n" " color: var(--color);\n" "}\n" "\n" ".selected-nav-item {\n" " background-color: var(--nav-color);\n" "}\n" "\n" "hr {\n" " margin-top: 0;\n" " border: 0;\n" " border-top: 3px solid var(--nav-color);\n" "}\n" "\n" "table {\n" " margin: 2em auto;\n" "}\n" "\n" "th {\n" " padding-bottom: 1em;\n" "}\n" "\n" "tbody tr:hover {\n" " background-color: var(--hover-color);\n" "}\n" "\n" "td {\n" " padding-left: 1em;\n" " padding-right: 1em;\n" "}\n" "\n" "footer {\n" " text-align: center;\n" "}\n" ""; #define DESCRIPTION_MAXLENGTH 81 static bool verbose = false; static const char *const GIT_SUFFIX = ".git"; static const char *const PROJECT_HOMEPAGE_LINK = "" PROGNAME "."; static void logerr( const char *const s, const char *const msg, const int lineno ) { logerr_file(s, msg, __FILE__, lineno); } static void logerrs( const char *const pre, const char *const mid, const char *const post, const char *const msg, const int lineno ) { logerrs_file(pre, mid, post, msg, __FILE__, lineno); } static void logerrl( const char *const pre, const size_t mid, const char *const post, const char *const msg, const int lineno ) { logerrl_file(pre, mid, post, msg, __FILE__, lineno); } static const char * _(int msg_id) { if (!catalog_descriptor || catalog_descriptor == (nl_catd)-1) { return MSGS[msg_id]; } errno = 0; const char *const ret = catgets(catalog_descriptor, NL_SETD, msg_id, MSGS[msg_id]); if (errno && verbose) { logerrl("catgets(", msg_id, ")", strerror(errno), __LINE__); } return ret; } #ifdef TEST static void test_underscore(void) { test_start("test_underscore"); const char *const original_locale = setlocale(LC_ALL, NULL); setlocale(LC_ALL, ""); setenv("NLSPATH", "", 1); { testing("a NULL value for catalog_descriptor"); test_ok(); } setlocale(LC_ALL, original_locale); } #endif static int print_msg(FILE *const fd, const char *const msg) { if (fprintf(fd, "%s", msg) < 0) { logerr("fprintf()", strerror(errno), __LINE__); return -1; } return 0; } static int print_help(FILE *const fd) { return print_msg(fd, _(MSG_HELP)); } static int print_version(FILE *const fd) { return print_msg(fd, PROGNAME "-" VERSION " " DATE "\n"); } static int print_usage(FILE *const fd) { return print_msg(fd, _(MSG_USAGE)); } static char * remove_suffix(char *const s, const char *const suffix) { if (!s || !suffix) { return NULL; } char *const name = strdup(s); if (!name) { logerrs("strdup(\"", s, "\")", strerror(errno), __LINE__); return NULL; } const char *const match_suffix = strrchr(name, suffix[0]); if (match_suffix && (strcmp(match_suffix, suffix) == 0)) { const size_t suffix_idx = strlen(name) - strlen(suffix); name[suffix_idx] = '\0'; } return name; } #ifdef TEST static void test_remove_suffix(void) { test_start("test_remove_suffix"); { testing("empty string"); char *const s = remove_suffix("", "suffix"); assert(s); assert(strcmp(s, "") == 0); free(s); test_ok(); } { testing("empty suffix"); char *const s = remove_suffix("a string", ""); assert(s); assert(strcmp(s, "a string") == 0); free(s); test_ok(); } { testing("with no suffix match"); char *const s = remove_suffix("another string", "NO MATCH!"); assert(s); assert(strcmp(s, "another string") == 0); free(s); test_ok(); } { testing("with suffix bigger than string"); char *const s = remove_suffix("str", "a string"); assert(s); assert(strcmp(s, "str") == 0); free(s); test_ok(); } { testing("with a NULL string"); char *const s = remove_suffix(NULL, "a non-null suffix"); assert(s == NULL); test_ok(); } { testing("with a NULL suffix"); char *const s = remove_suffix("not null", NULL); assert(s == NULL);; test_ok(); } { testing("both are NULL"); char *const s = remove_suffix(NULL, NULL); assert(s == NULL); test_ok(); } { testing("matching prefix"); char *const s = remove_suffix("aaa bbb ccc", "aaa"); assert(s); assert(strcmp(s, "aaa bbb ccc") == 0); free(s); test_ok(); } { testing("matching infix"); char *const s = remove_suffix("aaa bbb ccc", "bbb"); assert(s); assert(strcmp(s, "aaa bbb ccc") == 0); free(s); test_ok(); } { testing("with suffix equal to string"); char *const s = remove_suffix("string", "string"); assert(s); assert(strcmp(s, "") == 0); free(s); test_ok(); } { testing("with matching suffix"); char *const s = remove_suffix("string", "ing"); assert(s); assert(strcmp(s, "str") == 0); free(s); test_ok(); } { testing("example usage: with .git suffix"); char *const s = remove_suffix("reponame.git", GIT_SUFFIX); assert(s); assert(strcmp(s, "reponame") == 0); free(s); test_ok(); } { testing("example usage: with full path"); char *const s = remove_suffix("an/example/path", "ath"); assert(s); assert(strcmp(s, "an/example/p") == 0); free(s); test_ok(); } } #endif static char * strjoin(const char *const s1, const char *const s2) { if (!s1 || !s2) { return NULL; } const size_t size1 = strnlen(s1, SIZE_MAX); const size_t size2 = strnlen(s2, SIZE_MAX - sizeof('\0')) + sizeof('\0'); if (SIZE_MAX - size1 < size2) { errno = EOVERFLOW; logerr("strjoin()", strerror(errno), __LINE__); return NULL; } const size_t size = size1 + size2; char *const s = malloc(size); if (!s) { logerrl("malloc(", size, ")", strerror(errno), __LINE__); return NULL; } strcpy(s, s1); strcat(s, s2); return s; } #ifdef TEST static void test_strjoin(void) { test_start("test_strjoin"); { testing("joining empty strings"); char *const s = strjoin("", ""); assert(s); assert(strcmp(s, "") == 0); free(s); test_ok(); } { testing("joining NULL strings"); assert(strjoin(NULL, NULL) == NULL); assert(strjoin("", NULL) == NULL); assert(strjoin(NULL, "") == NULL); test_ok(); } { testing("first string is empty"); char *const s = strjoin("", "second not empty"); assert(s); assert(strcmp(s, "second not empty") == 0); free(s); test_ok(); } { testing("second string is empty"); char *const s = strjoin("first not empty", ""); assert(s); assert(strcmp(s, "first not empty") == 0); free(s); test_ok(); } { testing("two non-empty strings"); char *const s = strjoin("abc", "def"); assert(s); assert(strcmp(s, "abcdef") == 0); free(s); test_ok(); } { testing("example usage: with file names"); char *const s = strjoin("../repository.git", "/description"); assert(s); assert(strcmp(s, "../repository.git/description") == 0); free(s); test_ok(); } } #endif static char * strsjoin(const char *const strs[]) { size_t size = sizeof('\0'); for (size_t i = 0; strs[i]; i++) { const size_t curr_len = strnlen(strs[i], SIZE_MAX); if (SIZE_MAX - size < curr_len) { errno = EOVERFLOW; logerr("strsjoin()", strerror(errno), __LINE__); return NULL; } if (SIZE_MAX == i) { errno = EOVERFLOW; logerr("strsjoin()", strerror(errno), __LINE__); return NULL; } size += curr_len; } char *const s = malloc(size); if (!s) { logerrl("malloc(", size, ")", strerror(errno), __LINE__); return NULL; } *s = '\0'; // "i" can't overflow now, as it was already checked above for (size_t i = 0; strs[i]; i++) { strcat(s, strs[i]); } return s; } #ifdef TEST static void test_strsjoin(void) { test_start("test_strsjoin"); { testing("joining empty strings"); char *const s = strsjoin((const char *const []) { "", "", NULL }); assert(s); assert(strcmp(s, "") == 0); free(s); test_ok(); } { testing("joining empty strs array"); char *const s = strsjoin((const char *const []) { NULL }); assert(s); assert(strcmp(s, "") == 0); free(s); test_ok(); } { testing("first string is empty"); char *const s = strsjoin((const char *const []) { "", "second not empty", NULL }); assert(s); assert(strcmp(s, "second not empty") == 0); free(s); test_ok(); } { testing("third string is empty"); char *const s = strsjoin((const char *const []) { "first not empty", "second not empty", "", NULL }); assert(s); assert(strcmp(s, "first not emptysecond not empty") == 0); free(s); test_ok(); } { testing("four non-empty strings"); char *const s = strsjoin((const char *const []) { "abc", "def", "ghi", "jkl", NULL }); assert(s); assert(strcmp(s, "abcdefghijkl") == 0); free(s); test_ok(); } { testing("example usage: with file names"); char *const s = strsjoin((const char *const []) { "../repository.git", "/description", NULL }); assert(s); assert(strcmp(s, "../repository.git/description") == 0); free(s); test_ok(); } { testing("example usage: with file name parts"); char *const s = strsjoin((const char *const []){ "/tmp/gistatic.ABCDEF", "/", "project", "-", "main", NULL }); assert(s); assert(strcmp(s, "/tmp/gistatic.ABCDEF/project-main") == 0); free(s); test_ok(); } } #endif static char * formatted_date(const time_t time_sec) { const struct tm *const time_utc = gmtime(&time_sec); if (!time_utc) { logerrl("gmtime(", time_sec, ")", strerror(errno), __LINE__); return NULL; } /* Expected size, plus breathing room for date format variations */ const size_t size = (strlen("XXX-XX-XX XX:XX") + sizeof('\0')) * 2; char *const formatted = malloc(size); if (!formatted) { logerrl("malloc(", size, ")", strerror(errno), __LINE__); return NULL; } strftime(formatted, size, "%Y-%m-%d %H:%M", time_utc); return formatted; } #ifdef TEST static void test_formatted_date(void) { test_start("test_formatted_date"); { testing("when given 0"); char *const s = formatted_date(0); assert(s); assert(strcmp(s, "1970-01-01 00:00") == 0); free(s); test_ok(); } { testing("when given negative values"); char *s; s = formatted_date(-1); assert(s); assert(strcmp(s, "1969-12-31 23:59") == 0); free(s); s = formatted_date(-123456789); assert(s); assert(strcmp(s, "1966-02-02 02:26") == 0); free(s); s = formatted_date(-999999999); assert(s); assert(strcmp(s, "1938-04-24 22:13") == 0); free(s); test_ok(); } { testing("when given recent date values"); char *s; s = formatted_date(1627645522); assert(s); assert(strcmp(s, "2021-07-30 11:45") == 0); free(s); s = formatted_date(1500000000); assert(s); assert(strcmp(s, "2017-07-14 02:40") == 0); free(s); s = formatted_date(1000000000); assert(s); assert(strcmp(s, "2001-09-09 01:46") == 0); free(s); test_ok(); } } #endif static size_t max(const size_t size1, const size_t size2) { return size1 > size2 ? size1 : size2; } #ifdef TEST static void test_max(void) { test_start("max"); { testing("equal values"); assert(max(1, 1) == 1); assert(max(3, 3) == 3); assert(max(0, 0) == 0); assert(max(999, 999) == 999); assert(max(SIZE_MAX, SIZE_MAX) == SIZE_MAX); test_ok(); } { testing("first is bigger"); assert(max(SIZE_MAX, SIZE_MAX - 1) == SIZE_MAX); assert(max(SIZE_MAX - 1, SIZE_MAX - 2) == SIZE_MAX - 1); assert(max(1, 0) == 1); assert(max(999, 3) == 999); test_ok(); } { testing("second is bigger"); assert(max(123, 321) == 321); assert(max(1, 999) == 999); test_ok(); } } #endif static char * escape_html(const char *s) { if (!s) { return NULL; } #define AMPCHAR '&' #define LTCHAR '<' #define GTCHAR '>' #define DQUOTCHAR '"' #define SQUOTCHAR '\'' static const char *const AMP = "&"; static const char *const LT = "<"; static const char *const GT = ">"; static const char *const DQUOT = """; static const char *const SQUOT = "'"; const size_t AMP_SIZE = strlen(AMP); const size_t LT_SIZE = strlen(LT); const size_t GT_SIZE = strlen(GT); const size_t DQUOT_SIZE = strlen(DQUOT); const size_t SQUOT_SIZE = strlen(SQUOT); const size_t BIGGEST = max(AMP_SIZE, max(LT_SIZE, max(GT_SIZE, max(DQUOT_SIZE, SQUOT_SIZE)))); const size_t input_size = strnlen(s, SIZE_MAX); size_t escaped_size = 0; for (size_t i = 0; i < input_size; i++) { if (SIZE_MAX - escaped_size < BIGGEST) { errno = EOVERFLOW; logerr("escape_html()", strerror(errno), __LINE__); return NULL; } switch (s[i]) { case AMPCHAR: escaped_size += AMP_SIZE; break; case LTCHAR: escaped_size += LT_SIZE; break; case GTCHAR: escaped_size += GT_SIZE; break; case DQUOTCHAR: escaped_size += DQUOT_SIZE; break; case SQUOTCHAR: escaped_size += SQUOT_SIZE; break; default: escaped_size += sizeof(s[i]); break; } } const size_t size = escaped_size + sizeof('\0'); char *const escaped = malloc(size); if (!escaped) { logerrl("malloc(", size, ")", strerror(errno), __LINE__); return NULL; } escaped[0] = '\0'; char c; size_t escaped_idx = 0; while ((c = *s++)) { switch (c) { case AMPCHAR: strcat(escaped, AMP); escaped_idx += AMP_SIZE; break; case LTCHAR: strcat(escaped, LT); escaped_idx += LT_SIZE; break; case GTCHAR: strcat(escaped, GT); escaped_idx += GT_SIZE; break; case DQUOTCHAR: strcat(escaped, DQUOT); escaped_idx += DQUOT_SIZE; break; case SQUOTCHAR: strcat(escaped, SQUOT); escaped_idx += SQUOT_SIZE; break; default: escaped[escaped_idx] = c; escaped[escaped_idx + 1] = '\0'; escaped_idx += sizeof(c); break; } } return escaped; } #ifdef TEST static void test_escape_html(void) { test_start("test_escape_html"); { testing("string with no escapable chars"); char *const s = escape_html("a plain string"); assert(s); assert(strcmp(s, "a plain string") == 0); free(s); test_ok(); } { testing("an empty string"); char *const s = escape_html(""); assert(s); assert(strcmp(s, "") == 0); free(s); test_ok(); } { testing("a NULL value"); char *const s = escape_html(NULL); assert(s == NULL); test_ok(); } { testing("string with a single & character"); char *const s = escape_html("&"); assert(s); assert(strcmp(s, "&") == 0); free(s); test_ok(); } { testing("a string with many escapable characters in sequence"); char *const s = escape_html("&& >> text <<"); assert(s); assert(strcmp(s, "&& >> text <<") == 0); free(s); test_ok(); } { testing("all escapable characters"); char *const s = escape_html("&<>\"'"); assert(s); assert(strcmp(s, "&<>"'") == 0); free(s); test_ok(); } { testing("example usage: /path/edn->json.git/"); char *const s = escape_html("/path/edn->json.git/"); assert(s); assert(strcmp(s, "/path/edn->json.git/") == 0); free(s); test_ok(); } { testing("example usage: Description" " with \"quotes\" && 'quotes'"); char *const s = escape_html( "Description with \"quotes\" && 'quotes'" ); assert(s); const char *const expected = "Description with" " "quotes" && 'quotes'"; assert(strcmp(s, expected) == 0); free(s); test_ok(); } { testing("example usage: plain HTML"); char *const s = escape_html("link"); assert(s); const char *const expected = "<a" " href="#">link</a>"; assert(strcmp(s, expected) == 0); free(s); test_ok(); } } #endif static bool should_trim(const char c) { return c == '\n' || c == '\t' || c == '\r' || c == ' '; } #ifdef TEST static void test_should_trim(void) { test_start("test_should_trim"); { testing("the \\0 null character"); assert(should_trim('\0') == false); test_ok(); } { testing("the \\b character"); assert(should_trim('\b') == false); test_ok(); } { testing("the space character"); assert(should_trim(' ') == true); test_ok(); } } #endif static void strtrim(char *const s) { if (s == NULL) { return; } if (s[0] == '\0') { return; } size_t len = strnlen(s, SIZE_MAX); while (len != 0 && should_trim(s[len - 1])) { s[len - 1] = '\0'; len--; } size_t offset_count = 0; while (should_trim(s[offset_count])) { offset_count++; }; if (offset_count == 0) { return; } for (size_t i = 0; s[i]; i++) { s[i] = s[i + offset_count]; } } #ifdef TEST static void test_strtrim(void) { test_start("test_strtrim"); { testing("empty string"); char *const s = ""; strtrim(s); assert(strcmp(s, "") == 0); test_ok(); } { testing("NULL string"); char *const s = NULL; strtrim(s); assert(s == NULL); test_ok(); } { testing("string without a newline"); char *const s = "a string without a newline"; strtrim(s); assert(strcmp(s, "a string without a newline") == 0); test_ok(); } { testing("string with a newline"); char *const s = strdup("a string with an ending newline\n"); assert(s); strtrim(s); assert(strcmp(s, "a string with an ending newline") == 0); free(s); test_ok(); } { testing("a single newline"); char *const s = strdup("\n"); assert(s); strtrim(s); assert(strcmp(s, "") == 0); free(s); test_ok(); } { testing("multiple newlines at the end"); char *const s = strdup("a string\n\n\n\n\n"); assert(s); strtrim(s); assert(strcmp(s, "a string") == 0); free(s); test_ok(); } { testing("a single space at the end"); char *const s = strdup("a string "); assert(s); strtrim(s); assert(strcmp(s, "a string") == 0); free(s); test_ok(); } { testing("a single space at the beginning"); char *const s = strdup(" a string"); assert(s); strtrim(s); assert(strcmp(s, "a string") == 0); free(s); test_ok(); } { testing("multiple newlines"); char *const s = strdup("\n\na string\n\n"); assert(s); strtrim(s); assert(strcmp(s, "a string") == 0); free(s); test_ok(); } { testing("newline on the middle of the string"); char *const s = "a\nstring"; strtrim(s); assert(strcmp(s, "a\nstring") == 0); test_ok(); } { testing("multiple trimmable characters"); char *const s = strdup(" \t \n \r a string \n \t \r "); assert(s); strtrim(s); assert(strcmp(s, "a string") == 0); free(s); test_ok(); } } #endif static int index_header_write(FILE *const fd, const char *const idx_title) { const int e = fprintf( fd, "" "\n" "\n" " \n" " \n" " \n" " \n" " \n" " \n" " %s\n" " \n" " \n" "
\n" "
\n" " \"%s\"\n" "

\n" " %s\n" "

\n" "
\n" "
\n" "
\n" "
\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n", _(MSG_LANG), _(MSG_INDEX_DESCRIPTION), idx_title, _(MSG_LOGO_ALT_INDEX), idx_title, _(MSG_NAME), _(MSG_DESCRIPTION), _(MSG_LAST_COMMIT) ); if (e < 0) { logerr("fprintf()", strerror(errno), __LINE__); return -1; } return 0; } static char * footer_signature_string(void) { const size_t template_size = strnlen( _(MSG_FOOTER_TEMPLATE), SIZE_MAX ) - strlen("%s"); const size_t link_size = strnlen( PROJECT_HOMEPAGE_LINK, SIZE_MAX - sizeof('\0') ) + sizeof('\0'); if (SIZE_MAX - template_size < link_size) { errno = EOVERFLOW; logerr("footer_signature_string()", strerror(errno), __LINE__); return NULL; } const size_t signature_size = template_size + link_size; char *const signature_text = malloc(signature_size); if (!signature_text) { logerrl("malloc(", signature_size, ")", strerror(errno), __LINE__); return NULL; } snprintf( signature_text, signature_size, _(MSG_FOOTER_TEMPLATE), PROJECT_HOMEPAGE_LINK ); return signature_text; } static int index_footer_write(FILE *const fd) { char *const signature_text = footer_signature_string(); if (!signature_text) { return -1; } const int e = fprintf( fd, "" " \n" "
\n" " %s\n" " \n" " %s\n" " \n" " %s\n" "
\n" "
\n" "
\n" "
\n" "

\n" " %s\n" "

\n" "
\n" " \n" "\n", signature_text ); free(signature_text); if (e < 0) { logerr("fprintf()", strerror(errno), __LINE__); return -1; } return 0; } static char * last_commit_date(struct git_repository *const repo) { struct git_commit *commit = NULL; struct git_revwalk *walker = NULL; struct git_oid oid; char *ret; if ( git_revwalk_new(&walker, repo) || git_revwalk_push_head(walker) || git_revwalk_next(&oid, walker) || git_commit_lookup(&commit, repo, &oid) ) { char *const default_return = strdup(""); if (!default_return) { logerr("strdup(\"\")", strerror(errno), __LINE__); } ret = default_return; goto cleanup; } const struct git_signature *const author = git_commit_author(commit); assert(author); ret = formatted_date(author->when.time); cleanup: git_commit_free(commit); git_revwalk_free(walker); return ret; } #ifdef TEST static void test_last_commit_date(void) { test_start("test_last_commit_date"); { testing("embedded Git repository" " tests/resources/repositories/repo-1"); struct git_repository *repo; const int e = git_repository_open_ext( &repo, "tests/resources/repositories/repo-1", GIT_REPOSITORY_OPEN_NO_SEARCH, NULL ); assert(e == 0); char *const date = last_commit_date(repo); assert(date); assert(strcmp(date, "2021-07-31 19:29") == 0); free(date); git_repository_free(repo); test_ok(); } { testing("embedded Git repository" " tests/resources/repositories/repo-2"); struct git_repository *repo; const int e = git_repository_open_ext( &repo, "tests/resources/repositories/repo-2", GIT_REPOSITORY_OPEN_NO_SEARCH, NULL ); assert(e == 0); char *const date = last_commit_date(repo); assert(date); assert(strcmp(date, "2021-07-31 19:27") == 0); free(date); git_repository_free(repo); test_ok(); } } #endif static int logo_write(FILE *const fd) { if (fprintf(fd, "%s", LOGO_STR) < 0) { logerr("fprintf()", strerror(errno), __LINE__); return -1; } return 0; } static int style_write(FILE *const fd) { if (fprintf(fd, "%s", STYLE_STR) < 0) { logerr("fprintf()", strerror(errno), __LINE__); return -1; } return 0; } static int index_row_write(FILE *const fd, char *const repopath) { int ret = 0; int e; struct git_repository *repo = NULL; char *name = NULL; char *description_path = NULL; char *date = NULL; char *encoded_name = NULL; char *encoded_description = NULL; char *encoded_date = NULL; e = git_repository_open_ext( &repo, repopath, GIT_REPOSITORY_OPEN_NO_SEARCH, NULL ); if (e) { fprintf(stderr, "%s: cannot open repository\n", repopath); ret = 1; goto cleanup; } if ( !(name = remove_suffix(basename(repopath), GIT_SUFFIX)) || !(description_path = strjoin(repopath, "/description")) || !(date = last_commit_date(repo)) ) { ret = -1; goto cleanup; } char description[DESCRIPTION_MAXLENGTH] = ""; FILE *const f = fopen(description_path, "r"); if (f) { if (!fgets(description, sizeof(description), f)) { logerrs("fgets(\"", description_path, "\")", strerror(errno), __LINE__); } else { strtrim(description); } if (fclose(f)) { logerrs("fclose(\"", description_path, "\")", strerror(errno), __LINE__); ret = -1; goto cleanup; } } if ( !(encoded_name = escape_html(name)) || !(encoded_description = escape_html(description)) || !(encoded_date = escape_html(date)) ) { ret = -1; goto cleanup; } e = fprintf( fd, "" " \n" " \n" " \n" " %s\n" " \n" " \n" " \n" " %s\n" " \n" " \n" " %s\n" " \n" " \n", encoded_name, encoded_name, encoded_description, encoded_date ); if (e < 0) { logerr("fprintf()", strerror(errno), __LINE__); ret = -1; goto cleanup; } cleanup: free(encoded_date); free(encoded_description); free(encoded_name); free(date); free(description_path); free(name); git_repository_free(repo); return ret; } static int index_write( const char *const outdir, const char *const title, const int repoc, char *const repov[] ) { int ret = 0; char *index_path = NULL; char *logo_path = NULL; char *style_path = NULL; FILE *index_fd = NULL; FILE *logo_fd = NULL; FILE *style_fd = NULL; if ( !(index_path = strjoin(outdir, "/index.html")) || !(logo_path = strjoin(outdir, "/logo.svg")) || !(style_path = strjoin(outdir, "/style.css")) ) { ret = -1; goto cleanup; } if (!(index_fd = fopen(index_path, "w"))) { logerrs("fopen(\"", index_path, "\")", strerror(errno), __LINE__); ret = -1; goto cleanup; } if (!(logo_fd = fopen(logo_path, "w"))) { logerrs("fopen(\"", logo_path, "\")", strerror(errno), __LINE__); ret = -1; goto cleanup; } if (!(style_fd = fopen(style_path, "w"))) { logerrs("fopen(\"", style_path, "\")", strerror(errno), __LINE__); ret = -1; goto cleanup; } if (index_header_write(index_fd, title)) { ret = -1; goto cleanup; } for (int i = 0; i < repoc; i++) { const int e = index_row_write(index_fd, repov[i]); if (e == -1) { ret = -1; goto cleanup; } if (e) { ret = 1; } } if ( index_footer_write(index_fd) || logo_write(logo_fd) || style_write(style_fd) ) { ret = -1; goto cleanup; } cleanup: if (style_fd && fclose(style_fd)) { logerrs("fclose(\"", style_path, "\")", strerror(errno), __LINE__); ret = -1; } if (logo_fd && fclose(logo_fd)) { logerrs("fclose(\"", logo_path, "\")", strerror(errno), __LINE__); ret = -1; } if (index_fd && fclose(index_fd)) { logerrs("fclose(\"", index_path, "\")", strerror(errno), __LINE__); ret = -1; } free(style_path); free(logo_path); free(index_path); return ret; } static int repo_refs_branches_each( FILE *const refs_fd, struct git_repository *const repo, struct git_reference *const ref ) { int ret = 0; struct git_commit *commit = NULL; char *encoded_name = NULL; char *encoded_summary = NULL; char *encoded_author = NULL; char *date = NULL; int e; const char *const name = git_reference_shorthand(ref); assert(name); const struct git_oid *const oid = git_reference_target(ref); assert(oid); if (git_commit_lookup(&commit, repo, oid)) { const git_error *const error = git_error_last(); assert(error); logerrs("git_commit_lookup(", name, ")", error->message, __LINE__); ret = -1; goto cleanup; } const char *const sha = git_oid_tostr_s(oid); assert(sha); const char *const summary = git_commit_summary(commit); assert(summary); const struct git_signature *const author = git_commit_author(commit); assert(author); if ( !(encoded_name = escape_html(name)) || !(encoded_summary = escape_html(summary)) || !(encoded_author = escape_html(author->name)) || !(date = formatted_date(author->when.time)) ) { ret = -1; goto cleanup; } e = fprintf( refs_fd, "" " \n" " \n" " \n" " %s\n" " \n" " \n" " \n" " \n" " %s\n" " \n" " \n" " \n" " %s\n" " \n" " \n" " %s\n" " \n" " \n" "", encoded_name, encoded_name, sha, encoded_summary, encoded_author, date ); if (e < 0) { logerr("fprintf()", strerror(errno), __LINE__); ret = -1; goto cleanup; } cleanup: free(date); free(encoded_author); free(encoded_summary); free(encoded_name); git_commit_free(commit); return ret; } static int repo_refs_tags_each( FILE *const refs_fd, struct git_repository *const repo, struct git_reference *const ref, const char *const project_name ) { int ret = 0; struct git_commit *commit = NULL; char *encoded_name = NULL; char *encoded_author = NULL; char *date = NULL; int e; if (!git_reference_is_tag(ref)) { goto cleanup; } const char *const name = git_reference_shorthand(ref); assert(name); const struct git_oid *const oid = git_reference_target(ref); assert(oid); if (git_commit_lookup(&commit, repo, oid)) { const git_error *const error = git_error_last(); assert(error); logerrs("git_commit_lookup(", name, ")", error->message, __LINE__); ret = -1; goto cleanup; } const char *const summary = git_commit_summary(commit); assert(summary); const struct git_signature *const author = git_commit_author(commit); assert(author); if ( !(encoded_name = escape_html(name)) || !(encoded_author = escape_html(author->name)) || !(date = formatted_date(author->when.time)) ) { ret = -1; goto cleanup; } e = fprintf( refs_fd, "" " \n" " \n" " \n" " %s\n" " \n" " (tarball, sig)\n" " \n" " \n" " %s\n" " \n" " \n" " %s\n" " \n" " \n" "", encoded_name, encoded_name, project_name, encoded_name, project_name, encoded_name, encoded_author, date ); if (e < 0) { logerr("fprintf()", strerror(errno), __LINE__); ret = -1; goto cleanup; } cleanup: free(date); free(encoded_author); free(encoded_name); git_commit_free(commit); return ret; } static int repo_refs_write( const char *const outdir, struct git_repository *const repo, const char *const project_name, const char *const description, const char *const clone_url ) { int ret = 0; char *signature_text = NULL; char *refs_path = NULL; FILE *refs_fd = NULL; struct git_branch_iterator *branch_iter = NULL; struct git_reference_iterator *ref_iter = NULL; int e; if ( !(signature_text = footer_signature_string()) || !(refs_path = strjoin(outdir, "/refs.html")) ) { ret = -1; goto cleanup; } if (!(refs_fd = fopen(refs_path, "w"))) { logerrs("fopen(\"", refs_path, "\")", strerror(errno), __LINE__); ret = -1; goto cleanup; } if ( git_branch_iterator_new(&branch_iter, repo, GIT_BRANCH_LOCAL) || git_reference_iterator_new(&ref_iter, repo) ) { const git_error *const error = git_error_last(); assert(error); const char *const fn = branch_iter == NULL ? "git_branch_iterator_new()" : "git_reference_iterator_new()"; logerr(fn, error->message, __LINE__); ret = -1; goto cleanup; } e = fprintf( refs_fd, "" "\n" "\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " %s\n" " \n" " \n" "
\n" "
\n" " \n" " \"%s\"\n" " \n" "
\n" "

\n" " %s\n" "

\n" "

\n" " %s\n" "

\n" " \n" " git clone %s\n" " \n" "
\n" "
\n" " \n" "
\n" "
\n" "
\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n", _(MSG_LANG), description, project_name, _(MSG_COMMIT_FEED), _(MSG_LANG), project_name, _(MSG_TAGS_FEED), _(MSG_LANG), project_name, _(MSG_LOGO_ALT_REPOSITORY), project_name, description, clone_url, _(MSG_NAV_FILES), _(MSG_NAV_LOG), _(MSG_NAV_REFS), _(MSG_THEAD_BRANCH), _(MSG_THEAD_COMMITMSG), _(MSG_THEAD_AUTHOR), _(MSG_THEAD_DATE) ); if (e < 0) { logerr("fprintf()", strerror(errno), __LINE__); ret = -1; goto cleanup; } struct git_reference *ref; git_branch_t _btype; while (!(e = git_branch_next(&ref, &_btype, branch_iter))) { e = repo_refs_branches_each(refs_fd, repo, ref); git_reference_free(ref); if (e == -1) { ret = -1; goto cleanup; } if (e) { ret = 1; } } if (e != GIT_ITEROVER) { const git_error *const error = git_error_last(); assert(error); logerr("git_branch_next()", error->message, __LINE__); ret = -1; goto cleanup; } e = fprintf( refs_fd, "" " \n" "
\n" " %s\n" " \n" " %s\n" " \n" " %s\n" " \n" " %s\n" "
\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n", _(MSG_THEAD_TAG), _(MSG_THEAD_AUTHOR), _(MSG_THEAD_DATE) ); if (e < 0) { logerr("fprintf()", strerror(errno), __LINE__); ret = -1; goto cleanup; } while (!(e = git_reference_next(&ref, ref_iter))) { e = repo_refs_tags_each(refs_fd, repo, ref, project_name); git_reference_free(ref); if (e == -1) { ret = -1; goto cleanup; } if (e) { ret = 1; } } if (e != GIT_ITEROVER) { const git_error *const error = git_error_last(); assert(error); logerr("git_reference_next()", error->message, __LINE__); ret = -1; goto cleanup; } e = fprintf( refs_fd, "" " \n" "
\n" " %s\n" " \n" " %s\n" " \n" " %s\n" "
\n" "
\n" " \n" " \n" "\n", signature_text ); if (e < 0) { logerr("fprintf()", strerror(errno), __LINE__); ret = -1; goto cleanup; } cleanup: git_branch_iterator_free(branch_iter); git_reference_iterator_free(ref_iter); if (refs_fd && fclose(refs_fd)) { logerrs("fclose(\"", refs_path, "\")", strerror(errno), __LINE__); ret = -1; } free(refs_path); free(signature_text); return ret; } static int repo_tarballs_refs_each( struct git_repository *const repo, struct git_reference *const ref, const char *const tarballs_dir, const char *const project_name ) { int ret = 0; char *out = NULL; struct git_commit *commit = NULL; struct git_tree *tree = NULL; char *tar_path = NULL; FILE *tar_fd = NULL; const bool is_tag = git_reference_is_tag(ref); const bool is_branch = git_reference_is_branch(ref); if (!is_branch && !is_tag) { goto cleanup; } const char *const name = git_reference_shorthand(ref); assert(name); const struct git_oid *const oid = git_reference_target(ref); assert(oid != NULL); char template[] = "/tmp/gistatic.XXXXXX"; const char *const tmpdir = mkdtemp(template); if (!tmpdir) { logerrs("mkdtemp(", tmpdir, ")", strerror(errno), __LINE__); ret = -1; goto cleanup; } out = strsjoin((const char *const []) { tmpdir, "/", project_name, "-", name, NULL }); if (!out) { ret = -1; goto cleanup; } git_checkout_options options; if (git_checkout_options_init(&options, GIT_CHECKOUT_OPTIONS_VERSION)) { const git_error *const error = git_error_last(); assert(error); logerr("git_checkout_options_init()", error->message, __LINE__); ret = -1; goto cleanup; } options.target_directory = out; options.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_RECREATE_MISSING; if (git_commit_lookup(&commit, repo, oid)) { const git_error *const error = git_error_last(); assert(error); logerrs("git_commit_lookup(", name, ")", error->message, __LINE__); ret = -1; goto cleanup; } const struct git_object *const treeish = (const git_object *const)commit; if (git_checkout_tree(repo, treeish, &options)) { const git_error *const error = git_error_last(); assert(error); logerr("git_reference_iterator_new()", error->message, __LINE__); ret = -1; goto cleanup; } tar_path = strsjoin((const char *const []) { tarballs_dir, "/", project_name, "-", name, ".tar", NULL }); if (!tar_path) { ret = -1; goto cleanup; } if (!(tar_fd = fopen(tar_path, "w"))) { logerrs("fopen(\"", tar_path, "\")", strerror(errno), __LINE__); ret = -1; goto cleanup; } if (tarball_write_from_directory(tar_fd, out)) { ret = -1; goto cleanup; } cleanup: if (tar_fd && fclose(tar_fd)) { logerrs("fclose(\"", tar_path, "\")", strerror(errno), __LINE__); ret = -1; } free(tar_path); git_tree_free(tree); git_commit_free(commit); free(out); return ret; } static int repo_tarballs_write( const char *const outdir, struct git_repository *const repo, const char *const project_name ) { int ret = 0; int e; char *tarballs_dir = NULL; struct git_reference_iterator *ref_iter = NULL; if (!(tarballs_dir = strjoin(outdir, "/tarballs"))) { ret = -1; goto cleanup; } errno = 0; if (mkdir(tarballs_dir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) && errno != EEXIST) { logerrs("mkdir(\"", tarballs_dir, "\")", strerror(errno), __LINE__); ret = EXIT_ERROR; goto cleanup; } if (git_reference_iterator_new(&ref_iter, repo)) { const git_error *const error = git_error_last(); assert(error); logerr("git_reference_iterator_new()", error->message, __LINE__); ret = -1; goto cleanup; } struct git_reference *ref; while (!(e = git_reference_next(&ref, ref_iter))) { e = repo_tarballs_refs_each(repo, ref, tarballs_dir, project_name); git_reference_free(ref); if (e == -1) { ret = -1; goto cleanup; } if (e) { ret = 1; } } if (e != GIT_ITEROVER) { const git_error *const error = git_error_last(); assert(error); logerr("git_reference_next()", error->message, __LINE__); ret = -1; goto cleanup; } cleanup: git_reference_iterator_free(ref_iter); free(tarballs_dir); return ret; } static int repo_write( const char *const outdir, char *const reporelpath, const char *const clone_url ) { int ret = 0; int e; char *repopath = NULL; char *description_path = NULL; char *logo_path = NULL; char *style_path = NULL; char *name = NULL; char *encoded_name = NULL; char *encoded_description = NULL; FILE *logo_fd = NULL; FILE *style_fd = NULL; struct git_repository *repo = NULL; if (!(repopath = realpath(reporelpath, NULL))) { logerrs("realpath(\"", reporelpath, "\")", strerror(errno), __LINE__); ret = -1; goto cleanup; } if ( !(description_path = strjoin(repopath, "/description")) || !(logo_path = strjoin(outdir, "/logo.svg")) || !(style_path = strjoin(outdir, "/style.css")) || !(name = remove_suffix(basename(repopath), GIT_SUFFIX)) ) { ret = -1; goto cleanup; } if (!(logo_fd = fopen(logo_path, "w"))) { logerrs("fopen(\"", logo_path, "\")", strerror(errno), __LINE__); ret = -1; goto cleanup; } if (!(style_fd = fopen(style_path, "w"))) { logerrs("fopen(\"", style_path, "\")", strerror(errno), __LINE__); ret = -1; goto cleanup; } if (logo_write(logo_fd) || style_write(style_fd)) { ret = -1; goto cleanup; } e = git_repository_open_ext( &repo, repopath, GIT_REPOSITORY_OPEN_NO_SEARCH, NULL ); if (e) { const git_error *const error = git_error_last(); assert(error); logerr("git_repository_open_ext()", error->message, __LINE__); ret = -1; goto cleanup; } char description[DESCRIPTION_MAXLENGTH] = ""; FILE *const f = fopen(description_path, "r"); if (f) { if (!fgets(description, sizeof(description), f)) { logerrs("fget(\"", description_path, "\")", strerror(errno), __LINE__); } else { strtrim(description); } if (fclose(f)) { logerrs("fclose(\"", description_path, "\")", strerror(errno), __LINE__); ret = -1; goto cleanup; } } if ( !(encoded_name = escape_html(name)) || !(encoded_description = escape_html(description)) ) { ret = -1; goto cleanup; } if ( repo_refs_write(outdir, repo, encoded_name, encoded_description, clone_url) || repo_tarballs_write(outdir, repo, encoded_name) ) { ret = -1; goto cleanup; } cleanup: free(encoded_description); free(encoded_name); if (repo) { git_repository_free(repo); } if (style_fd && fclose(style_fd)) { logerrs("fclose(\"", style_path, "\")", strerror(errno), __LINE__); ret = -1; } if (logo_fd && fclose(logo_fd)) { logerrs("fclose(\"", style_path, "\")", strerror(errno), __LINE__); ret = -1; } free(name); free(style_path); free(logo_path); free(description_path); free(repopath); return ret; } int gistatic_main(const int argc, char *const argv[]) { int ret = EXIT_SUCCESS; bool cleanup_libgit = false; catalog_descriptor = catopen(CATALOG_NAME, NL_CAT_LOCALE); if (argc < 2) { if (print_usage(stderr)) { ret = EXIT_ERROR; goto cleanup; } ret = EXIT_USAGE; goto cleanup; } int flag; bool index = false; const char *idx_title = _(MSG_DEFAULT_TITLE); const char *outdir = "."; const char *clone_url = NULL; while ((flag = getopt(argc, argv, "o:t:u:ivhV")) != -1) { switch (flag) { case 'i': index = true; break; case 't': idx_title = optarg; break; case 'o': outdir = optarg; break; case 'v': verbose = true; break; case 'u': clone_url = optarg; break; case 'h': if (print_usage(stdout) || print_help(stdout)) { ret = EXIT_ERROR; } goto cleanup; case 'V': if (print_version(stdout)) { ret = EXIT_ERROR; } goto cleanup; default: if (print_usage(stderr)) { ret = EXIT_ERROR; } goto cleanup; } } if (!index && !clone_url) { if (fprintf(stderr, "%s\n", _(MSG_MISSING_URL)) < 0 || print_usage(stderr)) { ret = EXIT_ERROR; goto cleanup; } ret = EXIT_USAGE; goto cleanup; } if (index && clone_url) { if (fprintf(stderr, "%s\n", _(MSG_INCOMPATIBLE_OPTIONS)) < 0 || print_usage(stderr)) { ret = EXIT_ERROR; goto cleanup; } ret = EXIT_USAGE; goto cleanup; } if (optind == argc) { if (fprintf(stderr, "%s\n", _(MSG_MISSING_ARGS)) < 0 || print_usage(stderr)) { ret = EXIT_ERROR; goto cleanup; } ret = EXIT_USAGE; goto cleanup; } errno = 0; if (mkdir(outdir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) && errno != EEXIST) { logerrs("mkdir(\"", outdir, "\")", strerror(errno), __LINE__); ret = EXIT_ERROR; goto cleanup; } git_libgit2_init(); cleanup_libgit = true; if (index) { if (index_write(outdir, idx_title, argc - optind, argv + optind)) { ret = EXIT_ERROR; goto cleanup; } } else { if (repo_write(outdir, argv[optind], clone_url)) { ret = EXIT_ERROR; goto cleanup; } } cleanup: if (cleanup_libgit) { git_libgit2_shutdown(); } if (catalog_descriptor && catalog_descriptor != (nl_catd)-1) { if (catclose(catalog_descriptor)) { logerr("catclose()", strerror(errno), __LINE__); ret = EXIT_ERROR; } } return ret; } #ifdef TEST void unit_tests_gistatic(void) { dump_translatable_strings(); git_libgit2_init(); test_underscore(); test_remove_suffix(); test_strjoin(); test_strsjoin(); test_formatted_date(); test_max(); test_escape_html(); test_should_trim(); test_strtrim(); test_last_commit_date(); git_libgit2_shutdown(); } #endif