/* * footprint.c - package footprint create, check, and update * https://rawnix.org * * Copyright (c) 2025 zorz@gmx.com * SPDX-License-Identifier: BSD-2-Clause * * Footprint format (one line per entry, tab-separated): * \t\t[ -> target][ (EMPTY)][ (major, minor)] * * Generated either from a package archive (tar -tvf) or from the pkg/ * directory directly after build (fast path, avoids double-archive). */ #include "mkpkg.h" #include "archive.h" #include #include #include #include #include #include #include #include #include #include #include #include #include /* ── Permission string ───────────────────────────────────────────────── */ static void format_mode(mode_t mode, char *buf) { /* buf must be at least 11 bytes */ buf[0] = S_ISDIR(mode) ? 'd' : S_ISLNK(mode) ? 'l' : S_ISCHR(mode) ? 'c' : S_ISBLK(mode) ? 'b' : S_ISFIFO(mode) ? 'p' : S_ISSOCK(mode) ? 's' : '-'; buf[1] = (mode & S_IRUSR) ? 'r' : '-'; buf[2] = (mode & S_IWUSR) ? 'w' : '-'; buf[3] = (mode & S_ISUID) ? ((mode & S_IXUSR) ? 's' : 'S') : ((mode & S_IXUSR) ? 'x' : '-'); buf[4] = (mode & S_IRGRP) ? 'r' : '-'; buf[5] = (mode & S_IWGRP) ? 'w' : '-'; buf[6] = (mode & S_ISGID) ? ((mode & S_IXGRP) ? 's' : 'S') : ((mode & S_IXGRP) ? 'x' : '-'); buf[7] = (mode & S_IROTH) ? 'r' : '-'; buf[8] = (mode & S_IWOTH) ? 'w' : '-'; buf[9] = (mode & S_ISVTX) ? ((mode & S_IXOTH) ? 't' : 'T') : ((mode & S_IXOTH) ? 'x' : '-'); buf[10] = '\0'; } /* ── make_footprint_fast ─────────────────────────────────────────────── */ typedef struct { char *line; ino_t ino; dev_t dev; int is_reg; /* 1 if regular file (may be hardlink) */ } fp_entry_t; typedef struct { fp_entry_t *ents; size_t count; size_t cap; const char *base; } fp_ctx_t; static fp_ctx_t g_fpctx; static void fpctx_add(const char *line, ino_t ino, dev_t dev, int is_reg) { if (g_fpctx.count >= g_fpctx.cap) { g_fpctx.cap = g_fpctx.cap ? g_fpctx.cap * 2 : 256; g_fpctx.ents = xrealloc(g_fpctx.ents, g_fpctx.cap * sizeof(fp_entry_t)); } g_fpctx.ents[g_fpctx.count].line = xstrdup(line); g_fpctx.ents[g_fpctx.count].ino = ino; g_fpctx.ents[g_fpctx.count].dev = dev; g_fpctx.ents[g_fpctx.count].is_reg = is_reg; g_fpctx.count++; } static void fpctx_free(void) { size_t i; for (i = 0; i < g_fpctx.count; i++) free(g_fpctx.ents[i].line); free(g_fpctx.ents); g_fpctx.ents = NULL; g_fpctx.count = 0; g_fpctx.cap = 0; g_fpctx.base = NULL; } /* extract path column (third tab-separated field) */ static const char *fp_path_col(const char *line) { const char *p = strchr(line, '\t'); if (!p) return line; p = strchr(p + 1, '\t'); if (!p) return line; return p + 1; } /* * Copy the path column into `out`, stripping any trailing annotation: * " -> target" (symlink target) * " (EMPTY)" (empty regular file marker) * " (N, N)" (device major/minor) * * Used as the sort/merge key so that, for example, a symlink whose * target changed between builds compares as the same path on both * sides — yielding a single "mismatch" entry rather than a misleading * missing+new pair. */ static const char *fp_path_only(const char *line, char *out, size_t outsz) { const char *p = fp_path_col(line); size_t len = strlen(p); size_t i, end = len; for (i = 0; i < len; i++) { if (p[i] != ' ') continue; if (i + 3 < len && p[i+1] == '-' && p[i+2] == '>' && p[i+3] == ' ') { end = i; break; } if (i + 1 < len && p[i+1] == '(') { end = i; break; } } if (end >= outsz) end = outsz - 1; memcpy(out, p, end); out[end] = '\0'; return out; } /* * Sort by path column only. * "bin/" sorts before "bin/foo" because '\0' < 'f', * so directories naturally appear before their contents. */ static int fp_cmp(const void *a, const void *b) { char ka[PATH_MAX_LEN * 2], kb[PATH_MAX_LEN * 2]; fp_path_only(((const fp_entry_t *)a)->line, ka, sizeof(ka)); fp_path_only(((const fp_entry_t *)b)->line, kb, sizeof(kb)); return strcmp(ka, kb); } /* comparator for strarray_t (char * items) used in read_footprint */ static int fp_str_cmp(const void *a, const void *b) { char ka[PATH_MAX_LEN * 2], kb[PATH_MAX_LEN * 2]; fp_path_only(*(const char **)a, ka, sizeof(ka)); fp_path_only(*(const char **)b, kb, sizeof(kb)); return strcmp(ka, kb); } static void walk_for_footprint(const char *rel) { char fullpath[PATH_MAX_LEN]; char relpath[PATH_MAX_LEN]; DIR *dir; struct dirent *ent; struct stat st; snprintf(fullpath, sizeof(fullpath), rel[0] ? "%s/%s" : "%s", g_fpctx.base, rel); dir = opendir(fullpath); if (!dir) return; while ((ent = readdir(dir)) != NULL) { char perms[11]; char owner[64]; char line[PATH_MAX_LEN * 2]; struct passwd *pw; struct group *gr; char *uname, *gname; 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_fpctx.base, relpath); if (lstat(fullpath, &st) != 0) continue; format_mode(st.st_mode, perms); pw = getpwuid(st.st_uid); gr = getgrgid(st.st_gid); uname = pw ? pw->pw_name : "root"; gname = gr ? gr->gr_name : "root"; snprintf(owner, sizeof(owner), "%s/%s", uname, gname); if (S_ISDIR(st.st_mode)) { snprintf(line, sizeof(line), "%s\t%s\t%s/", perms, owner, relpath); fpctx_add(line, st.st_ino, st.st_dev, 0); walk_for_footprint(relpath); } else if (S_ISLNK(st.st_mode)) { char target[PATH_MAX_LEN]; ssize_t n = readlink(fullpath, target, sizeof(target) - 1); if (n >= 0) target[n] = '\0'; else target[0] = '\0'; snprintf(line, sizeof(line), "%s\t%s\t%s -> %s", perms, owner, relpath, target); fpctx_add(line, st.st_ino, st.st_dev, 0); } else if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode)) { snprintf(line, sizeof(line), "%s\t%s\t%s (%lu, %lu)", perms, owner, relpath, (unsigned long)major(st.st_rdev), (unsigned long)minor(st.st_rdev)); fpctx_add(line, st.st_ino, st.st_dev, 0); } else { /* regular file — track inode for hardlink detection */ if (st.st_size == 0) snprintf(line, sizeof(line), "%s\t%s\t%s (EMPTY)", perms, owner, relpath); else snprintf(line, sizeof(line), "%s\t%s\t%s", perms, owner, relpath); fpctx_add(line, st.st_ino, st.st_dev, 1); } } closedir(dir); } /* simple inode table for hardlink detection */ typedef struct { ino_t ino; dev_t dev; const char *path; } inode_rec_t; int make_footprint_fast(const char *pkgdir, const char *outfile) { FILE *fp; size_t i; inode_rec_t *inodes = NULL; size_t ino_count = 0, ino_cap = 0; g_fpctx.ents = NULL; g_fpctx.count = 0; g_fpctx.cap = 0; g_fpctx.base = pkgdir; walk_for_footprint(""); if (g_fpctx.count == 0) { fpctx_free(); return -1; } qsort(g_fpctx.ents, g_fpctx.count, sizeof(fp_entry_t), fp_cmp); fp = fopen(outfile, "w"); if (!fp) { fpctx_free(); return -1; } for (i = 0; i < g_fpctx.count; i++) { fp_entry_t *e = &g_fpctx.ents[i]; if (e->is_reg && e->ino != 0) { /* check if this inode was seen before */ size_t j; const char *target = NULL; for (j = 0; j < ino_count; j++) { if (inodes[j].ino == e->ino && inodes[j].dev == e->dev) { target = inodes[j].path; break; } } if (target) { /* hardlink — rewrite line with h prefix and -> target */ const char *owner_start = strchr(e->line, '\t'); const char *path_start = NULL; char perms[11], owner[64]; if (owner_start) { size_t pl = (size_t)(owner_start - e->line); if (pl > 10) pl = 10; memcpy(perms, e->line, pl); perms[pl] = '\0'; path_start = strchr(owner_start + 1, '\t'); if (path_start) { size_t ol = (size_t)(path_start - owner_start - 1); if (ol >= sizeof(owner)) ol = sizeof(owner) - 1; memcpy(owner, owner_start + 1, ol); owner[ol] = '\0'; path_start++; /* skip tab */ } } /* replace leading '-' with 'h' */ perms[0] = 'h'; if (path_start) fprintf(fp, "%s\t%s\t%s -> %s\n", perms, owner, path_start, target); else fprintf(fp, "%s\n", e->line); continue; } else { /* first occurrence — record inode */ if (ino_count >= ino_cap) { ino_cap = ino_cap ? ino_cap * 2 : 64; inodes = xrealloc(inodes, ino_cap * sizeof(inode_rec_t)); } inodes[ino_count].ino = e->ino; inodes[ino_count].dev = e->dev; inodes[ino_count].path = fp_path_col(e->line); /* note: path points into e->line — valid until fpctx_free */ ino_count++; } } fprintf(fp, "%s\n", e->line); } fclose(fp); /* strip hardlink (EMPTY) annotations — hardlinks are never empty */ free(inodes); fpctx_free(); return 0; } /* ── make_footprint ──────────────────────────────────────────────────── */ int make_footprint(const char *pkgfile, const char *outfile) { int outfd; pid_t pid; int status; outfd = open(outfile, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (outfd < 0) { warn("cannot create: %s", outfile); return -1; } pid = fork(); if (pid == 0) { if (dup2(outfd, STDOUT_FILENO) < 0) _exit(1); close(outfd); arc_footprint(pkgfile); fflush(stdout); _exit(0); } close(outfd); if (pid < 0) { warn("fork"); return -1; } waitpid(pid, &status, 0); return (WIFEXITED(status) && WEXITSTATUS(status) == 0) ? 0 : -1; } /* ── read footprint file into a sorted string array ──────────────────── */ static strarray_t *read_footprint(const char *path) { FILE *fp; strarray_t *a; char line[PATH_MAX_LEN * 2]; fp = fopen(path, "r"); if (!fp) return NULL; a = strarray_new(); while (fgets(line, sizeof(line), fp)) { line[strcspn(line, "\n")] = '\0'; if (line[0]) strarray_add(a, line); } fclose(fp); if (a->count > 1) qsort(a->items, a->count, sizeof(char *), fp_str_cmp); a->sorted = 1; return a; } /* fp_path_col() defined above, used here too */ /* ── check_footprint ─────────────────────────────────────────────────── */ int check_footprint(const char *pkgfile, const char *fpfile, config_t *config) { char tmp[PATH_MAX_LEN]; strarray_t *stored, *current; size_t si, ci; int errors = 0, new_files = 0; int fd; (void)config; /* first build — no .footprint yet, create it */ if (access(fpfile, F_OK) != 0) { if (make_footprint(pkgfile, fpfile) == 0) info("footprint created: %s", fpfile); return 0; } /* generate current footprint into temp file */ { const char *tmpdir = getenv("TMPDIR"); if (!tmpdir || !tmpdir[0]) tmpdir = "/dev/shm"; snprintf(tmp, sizeof(tmp), "%s/mkpkg_fp.XXXXXX", tmpdir); } fd = mkstemp(tmp); if (fd < 0) { warn("mkstemp"); return E_FOOTPRINT; } close(fd); if (make_footprint(pkgfile, tmp) != 0) { unlink(tmp); return E_FOOTPRINT; } stored = read_footprint(fpfile); current = read_footprint(tmp); unlink(tmp); if (!stored || !current) { strarray_free(stored); strarray_free(current); return E_FOOTPRINT; } /* * Merge-compare on path-only keys (annotations stripped). Two * symlinks with the same path but different targets, or a regular * file that becomes empty across builds, will hit the cmp == 0 * branch and report as a mismatch — not as missing+new. * * Missing and new entries are buffered and printed in two clean * sections after the merge, so the output isn't interleaved. */ { strarray_t *chg_buf = strarray_new(); strarray_t *miss_buf = strarray_new(); strarray_t *new_buf = strarray_new(); size_t i; si = 0; ci = 0; while (si < stored->count || ci < current->count) { char s_key[PATH_MAX_LEN * 2], c_key[PATH_MAX_LEN * 2]; const char *s_full = si < stored->count ? stored->items[si] : NULL; const char *c_full = ci < current->count ? current->items[ci] : NULL; int cmp; if (s_full) fp_path_only(s_full, s_key, sizeof(s_key)); if (c_full) fp_path_only(c_full, c_key, sizeof(c_key)); if (!s_full) cmp = 1; else if (!c_full) cmp = -1; else cmp = strcmp(s_key, c_key); if (cmp == 0) { if (strcmp(s_full, c_full) != 0) strarray_add(chg_buf, c_full); si++; ci++; } else if (cmp < 0) { strarray_add(miss_buf, s_full); si++; } else { strarray_add(new_buf, c_full); ci++; } } if (chg_buf->count > 0) { fprintf(stderr, "mkpkg: footprint: changed\n"); for (i = 0; i < chg_buf->count; i++) fprintf(stderr, "%s\n", chg_buf->items[i]); } if (miss_buf->count > 0) { fprintf(stderr, "mkpkg: footprint: missing\n"); for (i = 0; i < miss_buf->count; i++) fprintf(stderr, "%s\n", miss_buf->items[i]); } if (new_buf->count > 0) { fprintf(stderr, "mkpkg: footprint: new\n"); for (i = 0; i < new_buf->count; i++) fprintf(stderr, "%s\n", new_buf->items[i]); } errors = (int)chg_buf->count + (int)miss_buf->count; new_files = (int)new_buf->count; strarray_free(chg_buf); strarray_free(miss_buf); strarray_free(new_buf); } strarray_free(stored); strarray_free(current); if (errors > 0 || new_files > 0) { fprintf(stderr, "mkpkg: footprint: warning: mismatch\n"); return E_FOOTPRINT; } return 0; } /* ── update_footprint ────────────────────────────────────────────────── */ int update_footprint(const char *pkgfile, const char *fpfile, config_t *config) { int ret; (void)config; ret = make_footprint(pkgfile, fpfile); if (ret == 0) info("footprint updated: %s", fpfile); return ret; }