/* * archive.c - tar archive operations for mkpkg * https://rawnix.org * * Copyright (c) 2025 zorz@gmx.com * SPDX-License-Identifier: BSD-2-Clause */ #include "archive.h" #include #include #include #include #include #include #include #include #include #include #include #include static int run_tar(char *const argv[], strarray_t *output); /* ── Extension detection ─────────────────────────────────────────────── */ int arc_is_archive(const char *f) { if (!f) return 0; return str_ends_with(f, ".tar") || str_ends_with(f, ".tar.gz") || str_ends_with(f, ".tgz") || str_ends_with(f, ".tar.bz2") || str_ends_with(f, ".tbz2") || str_ends_with(f, ".tar.xz") || str_ends_with(f, ".txz") || str_ends_with(f, ".tar.lz") || str_ends_with(f, ".tar.lzma") || str_ends_with(f, ".tar.zst") || str_ends_with(f, ".tzst") || str_ends_with(f, ".tar.Z"); } /* ── Package filename parser ─────────────────────────────────────────── */ /* * Parse "foo#1.0-1.pkg.tar.gz" into name and version. * Returns 0 on success, -1 if the filename does not match the format. */ static int parse_pkgname(const char *path, char **name, char **version) { const char *base, *delim, *ext; size_t len; base = strrchr(path, '/'); base = base ? base + 1 : path; delim = strchr(base, PKG_DELIM); if (!delim) return -1; ext = strstr(base, PKG_EXT); if (!ext || ext <= delim) return -1; len = (size_t)(delim - base); *name = xmalloc(len + 1); memcpy(*name, base, len); (*name)[len] = '\0'; len = (size_t)(ext - delim - 1); *version = xmalloc(len + 1); memcpy(*version, delim + 1, len); (*version)[len] = '\0'; return 0; } /* ── fork/exec tar helper ────────────────────────────────────────────── */ /* * Sniff the compression format of a file from magic bytes. * Returns "gz", "xz", "zst", "bz2", "lz", or NULL if unrecognised. * * Detection is from file CONTENT, not MKPKG_COMPRESSION_MODE or the * extension: the configured mode only governs what mkpkg produces; * what addpkg & friends consume is whatever is on disk. Switching * the default (e.g. gz → zst) must never require rebuilding or * recompressing existing packages, and a misnamed file must still * extract correctly. */ static const char *sniff_compression(const char *file) { unsigned char m[6] = {0}; FILE *fp; size_t n; fp = fopen(file, "rb"); if (!fp) return NULL; n = fread(m, 1, sizeof(m), fp); fclose(fp); if (n >= 2 && m[0] == 0x1f && m[1] == 0x8b) return "gz"; if (n >= 4 && m[0] == 0x28 && m[1] == 0xb5 && m[2] == 0x2f && m[3] == 0xfd) return "zst"; if (n >= 6 && m[0] == 0xfd && m[1] == '7' && m[2] == 'z' && m[3] == 'X' && m[4] == 'Z' && m[5] == 0x00) return "xz"; if (n >= 3 && m[0] == 'B' && m[1] == 'Z' && m[2] == 'h') return "bz2"; if (n >= 4 && m[0] == 'L' && m[1] == 'Z' && m[2] == 'I' && m[3] == 'P') return "lz"; return NULL; } /* * Exec the best DEcompressor for `mode` (stdin → stdout). * Mirrors exec_compressor()'s fallback chains. * * Note the honest expectations here: gzip's format is a sequential * stream, so pigz cannot parallelise the actual inflate — it threads * reading, writing and CRC checking, worth a real but modest * ~10-25%. bzip2 is block-based: lbzip2 decompresses any .bz2 * in parallel, pbzip2 only multi-stream files (in practice, ones it * compressed itself). zstd is simply fast. Output bytes are * identical to the single-threaded tools in all cases. * * Never returns on success; caller must _exit() after. */ static void exec_decompressor(const char *mode) { if (strcmp(mode, "gz") == 0) { execlp("pigz", "pigz", "-dc", (char *)NULL); execlp("gzip", "gzip", "-dc", (char *)NULL); } else if (strcmp(mode, "xz") == 0) { execlp("xz", "xz", "-dc", "-T0", (char *)NULL); } else if (strcmp(mode, "zst") == 0) { execlp("zstd", "zstd", "-dcq", (char *)NULL); } else if (strcmp(mode, "bz2") == 0) { execlp("lbzip2", "lbzip2", "-dc", (char *)NULL); execlp("pbzip2", "pbzip2", "-dc", (char *)NULL); execlp("bzip2", "bzip2", "-dc", (char *)NULL); } else if (strcmp(mode, "lz") == 0) { execlp("plzip", "plzip", "-dc", (char *)NULL); execlp("lzip", "lzip", "-dc", (char *)NULL); } else { /* unknown — pass through, tar auto-detects on stdin */ execlp("cat", "cat", (char *)NULL); } } /* * Run tar over an archive FILE, feeding it through the best * decompressor: file → → pipe → tar (argv) * * argv must read the archive from stdin ("-f", "-"). Output capture * works as in run_tar(). When the format is unrecognised the file * is passed through unmodified and tar's own auto-detection applies, * which keeps today's behaviour as the fallback. No shell involved. */ static int run_tar_on_archive(const char *file, char *const argv[], strarray_t *output) { const char *mode = sniff_compression(file); int pipefd[2]; int infd; pid_t pid_dc; int status, ret; infd = open(file, O_RDONLY); if (infd < 0) { warn("cannot open %s", file); return -1; } if (pipe(pipefd) < 0) { warn("pipe"); close(infd); return -1; } pid_dc = fork(); if (pid_dc < 0) { warn("fork"); close(infd); close(pipefd[0]); close(pipefd[1]); return -1; } if (pid_dc == 0) { /* decompressor: file → stdout(pipe) */ close(pipefd[0]); if (dup2(infd, STDIN_FILENO) < 0) _exit(127); if (dup2(pipefd[1], STDOUT_FILENO) < 0) _exit(127); close(infd); close(pipefd[1]); exec_decompressor(mode ? mode : ""); _exit(127); } close(infd); close(pipefd[1]); /* tar reads the decompressed stream from our pipe via stdin */ { int saved_stdin = dup(STDIN_FILENO); if (saved_stdin < 0 || dup2(pipefd[0], STDIN_FILENO) < 0) { warn("dup2"); close(pipefd[0]); if (saved_stdin >= 0) close(saved_stdin); waitpid(pid_dc, &status, 0); return -1; } close(pipefd[0]); ret = run_tar(argv, output); dup2(saved_stdin, STDIN_FILENO); close(saved_stdin); } waitpid(pid_dc, &status, 0); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { warnx("decompressor failed on %s", file); return -1; } return ret; } /* * Run tar with argv, optionally capturing stdout into a strarray. * Returns 0 on success, -1 on failure. */ static int run_tar(char *const argv[], strarray_t *output) { int pipefd[2] = {-1, -1}; pid_t pid; int status; if (output) { if (pipe(pipefd) < 0) { warn("pipe"); return -1; } } pid = fork(); if (pid < 0) { warn("fork"); if (output) { close(pipefd[0]); close(pipefd[1]); } return -1; } if (pid == 0) { if (output) { close(pipefd[0]); if (dup2(pipefd[1], STDOUT_FILENO) < 0) _exit(127); close(pipefd[1]); } execvp("tar", argv); _exit(127); } if (output) { FILE *fp; char line[PATH_MAX_LEN]; close(pipefd[1]); fp = fdopen(pipefd[0], "r"); if (fp) { while (fgets(line, sizeof(line), fp)) { line[strcspn(line, "\n")] = '\0'; if (line[0]) strarray_add(output, line); } fclose(fp); } else { close(pipefd[0]); } } waitpid(pid, &status, 0); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) return -1; return 0; } /* ── arc_extract_src ─────────────────────────────────────────────────── */ int arc_extract_src(const char *file, const char *destdir) { char *const argv[] = { "tar", "--no-same-owner", "-C", (char *)destdir, "-xf", "-", NULL }; info("extracting %s", file); if (run_tar_on_archive(file, argv, NULL) != 0) { warnx("failed to extract: %s", file); return -1; } return 0; } /* ── arc_create_pkg ──────────────────────────────────────────────────── */ /* directory entry for stable sort */ typedef struct { char *path; mode_t mode; } arc_entry_t; /* file-static walk context — single-threaded, reset before each use */ static arc_entry_t *g_ent = NULL; static size_t g_cnt = 0; static size_t g_cap = 0; static const char *g_base = NULL; /* absolute pkgdir path */ static void ent_free(void) { size_t i; for (i = 0; i < g_cnt; i++) free(g_ent[i].path); free(g_ent); g_ent = NULL; g_cnt = 0; g_cap = 0; g_base = NULL; } static int ent_add(const char *path, mode_t mode) { arc_entry_t *tmp; if (g_cnt >= g_cap) { g_cap = g_cap ? g_cap * 2 : 256; tmp = realloc(g_ent, g_cap * sizeof(arc_entry_t)); if (!tmp) return -1; g_ent = tmp; } g_ent[g_cnt].path = xstrdup(path); g_ent[g_cnt].mode = mode; g_cnt++; return 0; } /* * Stable tar sort: parent directory first, then dir → file → symlink, * then alphabetical. Guarantees targets appear before symlinks. */ static int tar_cmp(const void *A, const void *B) { const arc_entry_t *a = A; const arc_entry_t *b = B; const char *sa, *sb; size_t da, db, dmin; int ta, tb, r; sa = strrchr(a->path, '/'); sb = strrchr(b->path, '/'); da = sa ? (size_t)(sa - a->path + 1) : 0; db = sb ? (size_t)(sb - b->path + 1) : 0; dmin = da < db ? da : db; r = strncmp(a->path, b->path, dmin); if (r != 0) return r; if (da != db) return da < db ? -1 : 1; ta = S_ISDIR(a->mode) ? 0 : S_ISLNK(a->mode) ? 2 : 1; tb = S_ISDIR(b->mode) ? 0 : S_ISLNK(b->mode) ? 2 : 1; if (ta != tb) return ta - tb; return strcmp(a->path, b->path); } static int walk_pkgdir(const char *rel) { char fullpath[PATH_MAX_LEN]; char relpath[PATH_MAX_LEN]; DIR *dir; struct dirent *ent; struct stat st; if (rel[0]) snprintf(fullpath, sizeof(fullpath), "%s/%s", g_base, rel); else snprintf(fullpath, sizeof(fullpath), "%s", g_base); dir = opendir(fullpath); if (!dir) return -1; while ((ent = readdir(dir)) != NULL) { if (ent->d_name[0] == '.' && (!ent->d_name[1] || (ent->d_name[1] == '.' && !ent->d_name[2]))) continue; if (rel[0]) snprintf(relpath, sizeof(relpath), "%s/%s", rel, ent->d_name); else snprintf(relpath, sizeof(relpath), "%s", ent->d_name); snprintf(fullpath, sizeof(fullpath), "%s/%s", g_base, relpath); /* always lstat — never follow symlinks */ if (lstat(fullpath, &st) != 0) { closedir(dir); return -1; } if (S_ISDIR(st.st_mode)) { char dirpath[PATH_MAX_LEN]; snprintf(dirpath, sizeof(dirpath), "%s/", relpath); ent_add(dirpath, st.st_mode); if (walk_pkgdir(relpath) != 0) { closedir(dir); return -1; } } else { ent_add(relpath, st.st_mode); } } closedir(dir); return 0; } /* * Map a compression token to (program, default-mode-fallback). * Returns the program name; never returns NULL. * Used for messages only — exec_compressor() picks the actual binary. */ static const char *compressor_program(const char *mode) { if (!mode || !*mode || strcmp(mode, "gz") == 0) return "gzip"; if (strcmp(mode, "xz") == 0) return "xz"; if (strcmp(mode, "zst") == 0) return "zstd"; if (strcmp(mode, "bz2") == 0) return "bzip2"; if (strcmp(mode, "lz") == 0) return "lzip"; /* unknown mode — fall back to gzip rather than fail the build */ return "gzip"; } /* * Exec the best compressor for `mode` in the child process. * * Single-threaded gzip/bzip2/lzip are the bottleneck on large packages * (llama.cpp, rocm-llvm: hundreds of MB of static libs), so prefer the * parallel drop-in variants when installed. execvp only returns on * failure, so chaining calls gives clean fallback with no PATH probing. * xz and zstd thread natively via -T0. Output formats are identical * to the single-threaded tools. * * Never returns on success; caller must _exit() after. */ static void exec_compressor(const char *mode) { if (!mode || !*mode || strcmp(mode, "gz") == 0) { execlp("pigz", "pigz", (char *)NULL); execlp("gzip", "gzip", (char *)NULL); } else if (strcmp(mode, "xz") == 0) { execlp("xz", "xz", "-T0", (char *)NULL); } else if (strcmp(mode, "zst") == 0) { execlp("zstd", "zstd", "-q", "-T0", (char *)NULL); } else if (strcmp(mode, "bz2") == 0) { execlp("lbzip2", "lbzip2", (char *)NULL); execlp("pbzip2", "pbzip2", (char *)NULL); execlp("bzip2", "bzip2", (char *)NULL); } else if (strcmp(mode, "lz") == 0) { execlp("plzip", "plzip", (char *)NULL); execlp("lzip", "lzip", (char *)NULL); } else { /* unknown mode — same fallback as compressor_program() */ execlp("pigz", "pigz", (char *)NULL); execlp("gzip", "gzip", (char *)NULL); } } /* Write exactly len bytes to fd, retrying on partial writes/EINTR */ static int write_all(int fd, const void *buf, size_t len) { const char *p = buf; while (len > 0) { ssize_t w = write(fd, p, len); if (w < 0) { if (errno == EINTR) continue; return -1; } p += w; len -= (size_t)w; } return 0; } /* * Pipeline: parent writes filenames → pipe1 → tar → pipe2 → → pkgfile * No shell involved. * * When `sink` is non-NULL the compressor's output is routed through * the parent instead of straight to the file: * * tar → pipe2 → → pipe3 → parent (sink(buf); write) → pkgfile * * letting the caller observe the exact bytes as they are written — * used by mkpkg to compute the package SHA256 during the build, so * signing doesn't have to re-read a multi-GB tarball afterwards. * The parent then has TWO pipes to service at once (names out, * compressed data in); they are multiplexed with poll(), because * writing the full name list first can deadlock: tar stops reading * names as soon as the downstream pipes fill up. */ int arc_create_pkg(const char *pkgfile, const char *pkgdir, const char *compression, arc_sink_fn sink, void *sink_ctx) { int pipe1[2], pipe2[2]; int pipe3[2] = {-1, -1}; pid_t pid_tar = -1, pid_gz = -1; int status, outfd; size_t i; const char *compressor = compressor_program(compression); /* walk and sort */ g_base = pkgdir; g_cnt = 0; info("compressing package"); if (walk_pkgdir("") != 0) { ent_free(); warnx("cannot read package directory: %s", pkgdir); return -1; } if (g_cnt == 0) { ent_free(); warnx("empty package directory: %s", pkgdir); return -1; } qsort(g_ent, g_cnt, sizeof(arc_entry_t), tar_cmp); outfd = open(pkgfile, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (outfd < 0) { warn("cannot create %s", pkgfile); ent_free(); return -1; } if (pipe(pipe1) < 0 || pipe(pipe2) < 0 || (sink && pipe(pipe3) < 0)) { warn("pipe"); close(outfd); ent_free(); return -1; } /* fork tar: reads file list from pipe1, writes raw tar to pipe2 */ pid_tar = fork(); if (pid_tar < 0) { warn("fork"); goto fail; } if (pid_tar == 0) { char *const argv[] = { "tar", "--no-recursion", "-cf", "-", "-C", (char *)pkgdir, "-T", "-", NULL }; close(pipe1[1]); close(pipe2[0]); if (dup2(pipe1[0], STDIN_FILENO) < 0) _exit(127); if (dup2(pipe2[1], STDOUT_FILENO) < 0) _exit(127); close(pipe1[0]); close(pipe2[1]); execvp("tar", argv); _exit(127); } /* fork compressor: reads from pipe2, writes to pkgfile */ pid_gz = fork(); if (pid_gz < 0) { warn("fork"); goto fail; } if (pid_gz == 0) { close(pipe1[0]); close(pipe1[1]); close(pipe2[1]); if (sink) close(pipe3[0]); if (dup2(pipe2[0], STDIN_FILENO) < 0) _exit(127); if (dup2(sink ? pipe3[1] : outfd, STDOUT_FILENO) < 0) _exit(127); close(pipe2[0]); if (sink) close(pipe3[1]); close(outfd); exec_compressor(compression); _exit(127); } /* parent: write sorted file list to tar's stdin */ close(pipe1[0]); close(pipe2[0]); close(pipe2[1]); if (sink) close(pipe3[1]); if (!sink) { /* simple path: compressor writes the file itself */ close(outfd); for (i = 0; i < g_cnt; i++) { if (write(pipe1[1], g_ent[i].path, strlen(g_ent[i].path)) < 0 || write(pipe1[1], "\n", 1) < 0) break; } close(pipe1[1]); } else { /* * Sink path: interleave writing names into pipe1 with * draining compressed data from pipe3 into the file. */ size_t ent_idx = 0; /* next entry to send */ size_t ent_off = 0; /* offset inside that entry */ char namebuf[PATH_MAX_LEN + 1]; size_t namelen = 0; int names_done = 0; int data_done = 0; char iobuf[1 << 16]; int failed = 0; if (g_cnt == 0) names_done = 1; fcntl(pipe1[1], F_SETFL, O_NONBLOCK); while (!names_done || !data_done) { struct pollfd pfd[2]; int n = 0, wi = -1, ri = -1; if (!names_done) { pfd[n].fd = pipe1[1]; pfd[n].events = POLLOUT; wi = n++; } if (!data_done) { pfd[n].fd = pipe3[0]; pfd[n].events = POLLIN; ri = n++; } if (poll(pfd, n, -1) < 0) { if (errno == EINTR) continue; warn("poll"); failed = 1; break; } /* feed file names to tar */ if (wi >= 0 && (pfd[wi].revents & (POLLOUT | POLLERR))) { if (pfd[wi].revents & POLLERR) { /* tar died — let waitpid report it */ names_done = 1; close(pipe1[1]); } else { if (namelen == 0) { namelen = (size_t)snprintf(namebuf, sizeof(namebuf), "%s\n", g_ent[ent_idx].path); ent_off = 0; } ssize_t w = write(pipe1[1], namebuf + ent_off, namelen - ent_off); if (w > 0) { ent_off += (size_t)w; if (ent_off == namelen) { namelen = 0; if (++ent_idx == g_cnt) { names_done = 1; close(pipe1[1]); } } } else if (w < 0 && errno != EAGAIN && errno != EINTR) { names_done = 1; close(pipe1[1]); } } } /* drain compressed bytes: observe, then write to file */ if (ri >= 0 && (pfd[ri].revents & (POLLIN | POLLHUP))) { ssize_t r = read(pipe3[0], iobuf, sizeof(iobuf)); if (r > 0) { sink(iobuf, (size_t)r, sink_ctx); if (write_all(outfd, iobuf, (size_t)r) != 0) { warn("write %s", pkgfile); failed = 1; break; } } else if (r == 0) { data_done = 1; close(pipe3[0]); } else if (errno != EINTR && errno != EAGAIN) { warn("read from compressor"); failed = 1; break; } } } if (!names_done) close(pipe1[1]); if (!data_done) close(pipe3[0]); close(outfd); if (failed) { ent_free(); waitpid(pid_tar, &status, 0); waitpid(pid_gz, &status, 0); return -1; } } ent_free(); waitpid(pid_tar, &status, 0); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { warnx("tar failed creating package"); waitpid(pid_gz, &status, 0); return -1; } waitpid(pid_gz, &status, 0); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { warnx("%s failed creating package", compressor); return -1; } return 0; fail: close(pipe1[0]); close(pipe1[1]); close(pipe2[0]); close(pipe2[1]); if (pipe3[0] >= 0) { close(pipe3[0]); close(pipe3[1]); } close(outfd); if (pid_tar > 0) waitpid(pid_tar, &status, 0); ent_free(); return -1; } /* ── arc_list ────────────────────────────────────────────────────────── */ int arc_list(const char *pkgfile, char **name, char **version, strarray_t **files) { char *const argv[] = { "tar", "-tf", "-", NULL }; if (parse_pkgname(pkgfile, name, version) < 0) { warnx("invalid package filename: %s", pkgfile); return -1; } *files = strarray_new(); if (run_tar_on_archive(pkgfile, argv, *files) < 0) { warnx("cannot list %s", pkgfile); strarray_free(*files); free(*name); free(*version); return -1; } return 0; } /* ── arc_extract_pkg ─────────────────────────────────────────────────── */ int arc_extract_pkg(const char *pkgfile, const char *root, strarray_t *keep, strarray_t *skip) { char tmpfile[PATH_MAX_LEN]; int tmpfd = -1; int need_excl = 0; int ret; size_t i; mkdirp(root, 0755); /* build exclude file for files that should be kept or skipped */ if ((keep && keep->count > 0) || (skip && skip->count > 0)) { FILE *tfp; const char *tmpdir = getenv("TMPDIR"); if (!tmpdir || !tmpdir[0]) tmpdir = "/tmp"; snprintf(tmpfile, sizeof(tmpfile), "%s/mkpkg-excl.XXXXXX", tmpdir); tmpfd = mkstemp(tmpfile); if (tmpfd < 0) { warn("mkstemp"); return -1; } tfp = fdopen(tmpfd, "w"); if (!tfp) { warn("fdopen"); close(tmpfd); unlink(tmpfile); return -1; } if (skip) for (i = 0; i < skip->count; i++) fprintf(tfp, "%s\n", skip->items[i]); if (keep) for (i = 0; i < keep->count; i++) { char *fp = path_join(root, keep->items[i]); if (file_exists(fp)) fprintf(tfp, "%s\n", keep->items[i]); free(fp); } fclose(tfp); need_excl = 1; } { char *argv[12]; int ac = 0; argv[ac++] = "tar"; argv[ac++] = "-xpf"; argv[ac++] = "-"; argv[ac++] = "-C"; argv[ac++] = (char *)root; if (need_excl) { /* * --exclude-from entries are literal package paths, not * patterns. Without --no-wildcards tar treats *, ? and [] * as globs, so a config file such as etc/foo[1].conf would * fail to match itself while a stray pattern could exclude * unrelated members. --anchored keeps matching rooted at * the start of the member name. */ argv[ac++] = "--no-wildcards"; argv[ac++] = "--anchored"; argv[ac++] = "-X"; argv[ac++] = tmpfile; } argv[ac] = NULL; ret = run_tar_on_archive(pkgfile, argv, NULL); } if (need_excl) unlink(tmpfile); if (ret < 0) { warnx("extraction failed for %s", pkgfile); return -1; } return 0; } /* ── arc_extract_single ──────────────────────────────────────────────── */ /* * Extract one member into destdir, stderr silenced (a missing member * is normal for optional files like package meta). The archive is * decompressed through the sniffed pipeline: tar stops once the * member is written, but reaching a member near the end of a large * archive still decompresses most of it — so the parallel * decompressors matter here too. */ int arc_extract_single(const char *pkgfile, const char *member, const char *destdir) { int devnull, saved_stderr, ret; char *const argv[] = { "tar", "-xf", "-", "-C", (char *)destdir, (char *)member, NULL }; /* silence tar's "not found in archive" on stderr */ saved_stderr = dup(STDERR_FILENO); devnull = open("/dev/null", O_WRONLY); if (devnull >= 0) { dup2(devnull, STDERR_FILENO); close(devnull); } ret = run_tar_on_archive(pkgfile, argv, NULL); if (saved_stderr >= 0) { dup2(saved_stderr, STDERR_FILENO); close(saved_stderr); } return ret; } /* ── arc_extract_members ─────────────────────────────────────────────── */ /* * Extract only the named members. tar accepts member names as * positional arguments, so there is no need to unpack the whole * archive and pick through the result — which mattered: the caller * used to unpack every package in full into TMPDIR (usually tmpfs) * just to reach a handful of config files. */ int arc_extract_members(const char *pkgfile, strarray_t *members, const char *destdir) { char **argv; size_t i; int ac = 0, ret; if (!members || members->count == 0) return 0; argv = xmalloc((members->count + 6) * sizeof(char *)); argv[ac++] = "tar"; argv[ac++] = "-xpf"; argv[ac++] = "-"; argv[ac++] = "-C"; argv[ac++] = (char *)destdir; for (i = 0; i < members->count; i++) argv[ac++] = members->items[i]; argv[ac] = NULL; ret = run_tar_on_archive(pkgfile, argv, NULL); free(argv); return ret; } /* ── arc_footprint ───────────────────────────────────────────────────── */ /* * Parse one line of "tar -tvf" output into footprint format. * * Input: -rw-r--r-- root/root 123 2025-01-01 12:00 usr/lib/foo.a * Output: \t\t[ -> link][ (EMPTY)] */ static void print_footprint_line(const char *line) { char perms[12]; char owner[256]; char sizestr[64]; char pathname[PATH_MAX_LEN]; const char *p = line; const char *end; long long filesize = 0; int is_hardlink = 0; int n; if (!*p) return; /* hardlink prefix 'h' used by GNU tar */ if (*p == 'h') { is_hardlink = 1; if (strlen(p) < 10) return; memcpy(perms, p, 10); /* keep 'h' as-is */ perms[10] = '\0'; p += 10; } else { if (strlen(p) < 10) return; memcpy(perms, p, 10); perms[10] = '\0'; p += 10; } /* owner/group field */ while (*p == ' ') p++; end = p; while (*end && *end != ' ') end++; n = (int)(end - p); if (n >= (int)sizeof(owner)) n = (int)sizeof(owner) - 1; memcpy(owner, p, n); owner[n] = '\0'; p = end; /* size field (or major,minor for devices) */ while (*p == ' ') p++; end = p; while (*end && *end != ' ') end++; n = (int)(end - p); if (n >= (int)sizeof(sizestr)) n = (int)sizeof(sizestr) - 1; memcpy(sizestr, p, n); sizestr[n] = '\0'; filesize = strtoll(sizestr, NULL, 10); p = end; /* skip date and time fields */ for (int skip = 0; skip < 2; skip++) { while (*p == ' ') p++; while (*p && *p != ' ') p++; } while (*p == ' ') p++; /* remainder is pathname */ snprintf(pathname, sizeof(pathname), "%s", p); n = (int)strlen(pathname); while (n > 0 && (pathname[n-1] == '\n' || pathname[n-1] == '\r')) pathname[--n] = '\0'; /* convert "link to" annotation to " -> " format */ { char *lt = strstr(pathname, " link to "); if (lt) { /* replace " link to " with " -> " in-place */ char after[PATH_MAX_LEN]; snprintf(after, sizeof(after), "%s", lt + 9); /* skip " link to " */ snprintf(lt, sizeof(pathname) - (size_t)(lt - pathname), " -> %s", after); if (!is_hardlink) is_hardlink = 1; } } printf("%s\t%s\t%s", perms, owner, pathname); if ((perms[0] == 'c' || perms[0] == 'b') && strchr(sizestr, ',')) { unsigned long maj = 0, min = 0; sscanf(sizestr, "%lu,%lu", &maj, &min); printf(" (%lu, %lu)", maj, min); } else if (perms[0] == '-' && filesize == 0 && !is_hardlink) { printf(" (EMPTY)"); } putchar('\n'); } int arc_footprint(const char *pkgfile) { strarray_t *lines; size_t i; char *const argv[] = { "tar", "-tvf", "-", NULL }; lines = strarray_new(); if (run_tar_on_archive(pkgfile, argv, lines) < 0) { warnx("cannot read %s", pkgfile); strarray_free(lines); return -1; } for (i = 0; i < lines->count; i++) print_footprint_line(lines->items[i]); strarray_free(lines); return 0; }