/* * Serves a commit as a downloadable archive. Writing the archive is git's * work, so each format here is a thin wrapper that either calls write_archive * directly or pipes its tar output through an external compressor. The table * of formats also lives here, and since a repository's snapshots mask is one * bit per table position, that order is part of what cgitrc means. When the * request carries no revision the file name is worked backwards to find one, * so that a link to cgit-1.2.tar.gz can serve the tag it was named after. A * name ending in .asc asks for the detached signature kept under refs/notes * instead of the archive itself. */ #define USE_THE_REPOSITORY_VARIABLE #include "cgit.h" #include "filter.h" #include "html.h" #include "ui-shared.h" #include "ui-snapshot.h" #define SIG_SUFFIX ".asc" static int write_archive_format(const char *format_arg, const char *hex, const char *prefix) { struct strvec argv = STRVEC_INIT; const char **args; int result; strvec_push(&argv, "snapshot"); strvec_push(&argv, format_arg); if (prefix) { struct strbuf buf = STRBUF_INIT; strbuf_addstr(&buf, prefix); strbuf_addch(&buf, '/'); strvec_push(&argv, "--prefix"); strvec_push(&argv, buf.buf); strbuf_release(&buf); } strvec_push(&argv, hex); // write_archive rearranges the argv it is handed, which would leave // strvec_clear leaking or double freeing, so it gets a copy of the // pointers, one entry longer than argv.nr for the trailing NULL. args = xmalloc(sizeof(char *) * (argv.nr + 1)); memcpy(args, argv.v, sizeof(char *) * (argv.nr + 1)); result = write_archive(argv.nr, args, NULL, the_repository, NULL, 0); strvec_clear(&argv); free(args); return result; } static int write_tar_archive(const char *hex, const char *prefix) { return write_archive_format("--format=tar", hex, prefix); } static int write_zip_archive(const char *hex, const char *prefix) { return write_archive_format("--format=zip", hex, prefix); } static int write_compressed_tar_archive(const char *hex, const char *prefix, char *argv[]) { struct cgit_exec_filter filter; int result; cgit_exec_filter_init(&filter, argv[0], argv); cgit_open_filter(&filter.base); result = write_tar_archive(hex, prefix); cgit_close_filter(&filter.base); return result; } static int write_tar_gzip_archive(const char *hex, const char *prefix) { char *argv[] = { "gzip", "-n", NULL }; return write_compressed_tar_archive(hex, prefix, argv); } static int write_tar_bzip2_archive(const char *hex, const char *prefix) { char *argv[] = { "bzip2", NULL }; return write_compressed_tar_archive(hex, prefix, argv); } static int write_tar_lzip_archive(const char *hex, const char *prefix) { char *argv[] = { "lzip", NULL }; return write_compressed_tar_archive(hex, prefix, argv); } static int write_tar_xz_archive(const char *hex, const char *prefix) { char *argv[] = { "xz", NULL }; return write_compressed_tar_archive(hex, prefix, argv); } static int write_tar_zstd_archive(const char *hex, const char *prefix) { // Single-threaded like the other compressors, since -T0 would let one // request pin every core. char *argv[] = { "zstd", NULL }; return write_compressed_tar_archive(hex, prefix, argv); } // ui-shared.c reads entry zero as the tar whose signature stands in for every // tar variant, so this order cannot change. const struct cgit_snapshot_format cgit_snapshot_formats[] = { { ".tar", "application/x-tar", write_tar_archive }, { ".tar.gz", "application/gzip", write_tar_gzip_archive }, { ".tar.bz2", "application/x-bzip2", write_tar_bzip2_archive }, { ".tar.lz", "application/x-lzip", write_tar_lzip_archive }, { ".tar.xz", "application/x-xz", write_tar_xz_archive }, { ".tar.zst", "application/zstd", write_tar_zstd_archive }, { ".zip", "application/zip", write_zip_archive }, { NULL } }; // Each tree is read the first time a signature for that format is asked for, // then kept for the life of the process. static struct notes_tree sig_notes[ARRAY_SIZE(cgit_snapshot_formats)]; static size_t format_index(const struct cgit_snapshot_format *f) { return f - cgit_snapshot_formats; } static const struct cgit_snapshot_format *find_format(const char *filename) { const struct cgit_snapshot_format *f; for (f = cgit_snapshot_formats; f->suffix; f++) { if (ends_with(filename, f->suffix)) return f; } return NULL; } static int resolves(const char *rev) { struct object_id oid; return repo_get_oid(the_repository, rev, &oid) == 0; } static const char *ref_from_filename(const struct cgit_repo *repo, const char *filename, const struct cgit_snapshot_format *format) { struct strbuf rev = STRBUF_INIT; const char *repo_prefix; int found = 1; strbuf_addstr(&rev, filename); strbuf_setlen(&rev, rev.len - strlen(format->suffix)); if (resolves(rev.buf)) goto out; repo_prefix = cgit_snapshot_prefix(repo); if (starts_with(rev.buf, repo_prefix)) { const char *rest = rev.buf + strlen(repo_prefix); while (rest && (*rest == '-' || *rest == '_')) rest++; strbuf_splice(&rev, 0, rest - rev.buf, "", 0); } if (resolves(rev.buf)) goto out; // A tag is often written with a leading v while the file named after it // is not, so that is tried last. strbuf_insert(&rev, 0, "v", 1); if (resolves(rev.buf)) goto out; strbuf_splice(&rev, 0, 1, "V", 1); if (resolves(rev.buf)) goto out; found = 0; strbuf_release(&rev); out: return found ? strbuf_detach(&rev, NULL) : NULL; } static int send_snapshot(const struct cgit_snapshot_format *format, const char *hex, const char *prefix, const char *filename) { struct object_id oid; if (repo_get_oid(the_repository, hex, &oid)) { cgit_print_error_page(404, "Not Found", "Bad object id: %s", hex); return 1; } if (!lookup_commit_reference(the_repository, &oid)) { cgit_print_error_page(400, "Bad Request", "Not a commit reference: %s", hex); return 1; } ctx.page.mimetype = xstrdup(format->mimetype); ctx.page.filename = xstrdup(filename); cgit_print_http_headers(); init_archivers(); format->write_func(hex, prefix); return 0; } static int send_sig(const struct cgit_snapshot_format *format, const char *hex, const char *archive_name, const char *sig_filename) { const struct object_id *note = cgit_snapshot_get_sig(hex, format); enum object_type type; unsigned long size; char *buf; if (!note) { cgit_print_error_page(404, "Not Found", "No signature for %s", archive_name); return 0; } buf = odb_read_object(the_repository->objects, note, &type, &size); if (!buf) { cgit_print_error_page(404, "Not Found", "Not found"); return 0; } ctx.page.untrusted = 1; ctx.page.mimetype = xstrdup("application/pgp-signature"); ctx.page.filename = xstrdup(sig_filename); cgit_print_http_headers(); html_raw(buf, size); free(buf); return 0; } const struct object_id *cgit_snapshot_get_sig(const char *ref, const struct cgit_snapshot_format *f) { struct notes_tree *tree; struct object_id oid; if (repo_get_oid(the_repository, ref, &oid)) return NULL; tree = &sig_notes[format_index(f)]; if (!tree->initialized) { struct strbuf notes_ref = STRBUF_INIT; // Signatures live under the format suffix with the leading dot // dropped, so plain tar is refs/notes/signatures/tar. strbuf_addf(¬es_ref, "refs/notes/signatures/%s", f->suffix + 1); init_notes(tree, notes_ref.buf, combine_notes_ignore, 0); strbuf_release(¬es_ref); } return get_note(tree, &oid); } unsigned cgit_snapshot_format_bit(const struct cgit_snapshot_format *f) { return 1U << format_index(f); } int cgit_parse_snapshots_mask(const char *str) { struct string_list tokens = STRING_LIST_INIT_DUP; struct string_list_item *item; const struct cgit_snapshot_format *f; int mask = 0; // A number is the legacy form of this setting and is still taken // ahead of the named forms below. if (atoi(str)) return 1; if (strcmp(str, "all") == 0) return INT_MAX; string_list_split(&tokens, str, " ", -1); string_list_remove_empty_items(&tokens, 0); for_each_string_list_item(item, &tokens) { for (f = cgit_snapshot_formats; f->suffix; f++) { if (!strcmp(item->string, f->suffix) || !strcmp(item->string, f->suffix + 1)) { mask |= cgit_snapshot_format_bit(f); break; } } } string_list_clear(&tokens, 0); return mask; } void cgit_print_snapshot(const char *head, const char *hex, const char *filename, int dwim) { const struct cgit_snapshot_format *f; const char *sig_filename = NULL; char *archive_name = NULL; char *prefix = NULL; if (!filename) { cgit_print_error_page(400, "Bad Request", "No snapshot name specified"); return; } if (ends_with(filename, SIG_SUFFIX)) { sig_filename = filename; archive_name = xstrdup(filename); archive_name[strlen(archive_name) - strlen(SIG_SUFFIX)] = '\0'; filename = archive_name; } f = find_format(filename); if (!f || (!sig_filename && !(ctx.repo->snapshots & cgit_snapshot_format_bit(f)))) { cgit_print_error_page(400, "Bad Request", "Unsupported snapshot format: %s", filename); return; } if (!hex && dwim) { hex = ref_from_filename(ctx.repo, filename, f); if (!hex) { cgit_print_error_page(404, "Not Found", "Not found"); return; } prefix = xstrdup(filename); prefix[strlen(filename) - strlen(f->suffix)] = '\0'; } if (!hex) hex = head; if (!prefix) prefix = xstrdup(cgit_snapshot_prefix(ctx.repo)); if (sig_filename) send_sig(f, hex, filename, sig_filename); else send_snapshot(f, hex, prefix, filename); free(prefix); free(archive_name); }