/* * pack.c — on-disk pack format (Section 9), load-time integrity * validation (Section 7), and compaction's writer (Section 4.1, 4.3, * 5.3, 9.1, 9.2). * * On-disk layout (a concrete realization of the illustrative sketch in * Section 9, extended with the checksum field Section 7 requires): * * PackHeader (fixed size, 8-byte aligned) * blobs (index_offset - sizeof(PackHeader) bytes) * PackIndexEntry[index_count] at index_offset, sorted by name (9.1) * strings (each name NUL-terminated) at strings_offset */ #include #include #include #include #include #include #include #include "internal.h" #define PACK_VERSION 1 typedef struct PackHeader { char magic[4]; uint32_t version; uint64_t index_offset; uint64_t index_count; uint64_t strings_offset; uint64_t strings_len; uint64_t checksum; /* FNV-1a64 over [index_offset, strings_offset+strings_len) */ } PackHeader; static uint64_t align8(uint64_t n) { return (n + 7u) & ~(uint64_t)7u; } /* ---- loading + validation (Section 7) ---- */ void pack_close(Pack *p) { if (!p) return; if (p->map && p->map != MAP_FAILED) munmap(p->map, p->map_len); if (p->fd >= 0) close(p->fd); free(p->path); free(p); } int pack_load(const char *path, Pack **out, int *err) { int fd = open(path, O_RDONLY | O_CLOEXEC); if (fd < 0) { if (err) *err = VFS_ERR_NOENT; return -1; } struct stat st; if (fstat(fd, &st) < 0) { close(fd); if (err) *err = VFS_ERR_IO; return -1; } size_t len = (size_t)st.st_size; if (len < sizeof(PackHeader)) { close(fd); if (err) *err = VFS_ERR_CORRUPT; return -1; } void *map = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0); if (map == MAP_FAILED) { close(fd); if (err) *err = VFS_ERR_IO; return -1; } const PackHeader *hdr = (const PackHeader *)map; if (memcmp(hdr->magic, "PKFS", 4) != 0 || hdr->version != PACK_VERSION) { munmap(map, len); close(fd); if (err) *err = VFS_ERR_CORRUPT; return -1; } /* Bounds-check every offset before trusting it (Section 7): the * index and strings regions must lie within the file and must not * overlap the header. */ if (hdr->index_offset < sizeof(PackHeader) || hdr->index_offset > len || hdr->index_count > (len - hdr->index_offset) / sizeof(PackIndexEntry)) { munmap(map, len); close(fd); if (err) *err = VFS_ERR_CORRUPT; return -1; } uint64_t index_bytes = hdr->index_count * (uint64_t)sizeof(PackIndexEntry); uint64_t index_end = hdr->index_offset + index_bytes; if (hdr->strings_offset < index_end || hdr->strings_offset > len || hdr->strings_len > len - hdr->strings_offset) { munmap(map, len); close(fd); if (err) *err = VFS_ERR_CORRUPT; return -1; } uint64_t strings_end = hdr->strings_offset + hdr->strings_len; if (strings_end > len) { munmap(map, len); close(fd); if (err) *err = VFS_ERR_CORRUPT; return -1; } uint64_t checksum = pfs_fnv1a64((const char *)map + hdr->index_offset, (size_t)(strings_end - hdr->index_offset)); if (checksum != hdr->checksum) { munmap(map, len); close(fd); if (err) *err = VFS_ERR_CORRUPT; return -1; } const PackIndexEntry *entries = (const PackIndexEntry *)((const char *)map + hdr->index_offset); const char *strings_base = (const char *)map + hdr->strings_offset; for (uint64_t i = 0; i < hdr->index_count; i++) { const PackIndexEntry *e = &entries[i]; if (e->data_off < sizeof(PackHeader) || e->data_off > hdr->index_offset || e->size > hdr->index_offset - e->data_off) { munmap(map, len); close(fd); if (err) *err = VFS_ERR_CORRUPT; return -1; } if (e->name_off > hdr->strings_len || e->name_len > hdr->strings_len - e->name_off) { munmap(map, len); close(fd); if (err) *err = VFS_ERR_CORRUPT; return -1; } /* Section 9's names are NUL-terminated in the strings region; a * missing terminator is treated as corruption, not tolerated. */ if (strings_base[e->name_off + e->name_len] != '\0') { munmap(map, len); close(fd); if (err) *err = VFS_ERR_CORRUPT; return -1; } if (i > 0) { const PackIndexEntry *prev = &entries[i - 1]; if (strcmp(strings_base + prev->name_off, strings_base + e->name_off) >= 0) { munmap(map, len); close(fd); if (err) *err = VFS_ERR_CORRUPT; /* Section 9.1 relies on sortedness */ return -1; } } } Pack *p = (Pack *)calloc(1, sizeof(Pack)); if (!p) { munmap(map, len); close(fd); if (err) *err = VFS_ERR_NOSPC; return -1; } p->fd = fd; p->map = map; p->map_len = len; p->entries = entries; p->entry_count = (size_t)hdr->index_count; p->strings_base = strings_base; p->strings_len = (size_t)hdr->strings_len; p->path = strdup(path); *out = p; return 0; } const char *pack_entry_name(const Pack *p, const PackIndexEntry *e) { return p->strings_base + e->name_off; } const void *pack_entry_data(const Pack *p, const PackIndexEntry *e) { return (const char *)p->map + e->data_off; } int pack_find(const Pack *p, const char *name, const PackIndexEntry **out) { size_t lo = 0, hi = p->entry_count; while (lo < hi) { size_t mid = lo + (hi - lo) / 2; int c = strcmp(pack_entry_name(p, &p->entries[mid]), name); if (c == 0) { *out = &p->entries[mid]; return 1; } if (c < 0) lo = mid + 1; else hi = mid; } return 0; } static size_t lower_bound_str(const Pack *p, const char *key) { size_t lo = 0, hi = p->entry_count; while (lo < hi) { size_t mid = lo + (hi - lo) / 2; if (strcmp(pack_entry_name(p, &p->entries[mid]), key) < 0) lo = mid + 1; else hi = mid; } return lo; } void pack_range(const Pack *p, const char *prefix, size_t *lo_out, size_t *hi_out) { size_t lo = lower_bound_str(p, prefix); size_t plen = strlen(prefix); char *upper = (char *)malloc(plen + 2); memcpy(upper, prefix, plen); upper[plen] = (char)0x7F; /* > any valid path byte, per Section 9.1's argument */ upper[plen + 1] = '\0'; size_t hi = lower_bound_str(p, upper); free(upper); *lo_out = lo; *hi_out = hi; } /* ---- writing (compaction target, Section 4.1 / 4.3 / 9.2) ---- */ static int pack_build_entry_cmp(const void *a, const void *b) { return strcmp(((const PackBuildEntry *)a)->name, ((const PackBuildEntry *)b)->name); } typedef struct DedupSlot { uint64_t hash; uint64_t size; uint64_t data_off; } DedupSlot; int pack_write(const char *path, const PackBuildEntry *in, size_t count, int *err) { PackBuildEntry *entries = NULL; if (count > 0) { entries = (PackBuildEntry *)malloc(count * sizeof(PackBuildEntry)); if (!entries) { if (err) *err = VFS_ERR_NOSPC; return -1; } memcpy(entries, in, count * sizeof(PackBuildEntry)); } qsort(entries, count, sizeof(PackBuildEntry), pack_build_entry_cmp); /* Pass 1: compute blob region with exact-duplicate elimination * (Section 9.2) and the strings region size. */ DedupSlot *slots = count ? (DedupSlot *)malloc(count * sizeof(DedupSlot)) : NULL; size_t slot_count = 0; uint64_t *data_off_for = count ? (uint64_t *)malloc(count * sizeof(uint64_t)) : NULL; uint64_t blob_cursor = align8(sizeof(PackHeader)); uint64_t strings_total = 0; for (size_t i = 0; i < count; i++) { uint64_t h = pfs_fnv1a64(entries[i].data, (size_t)entries[i].size); uint64_t reuse = UINT64_MAX; for (size_t j = 0; j < slot_count; j++) { if (slots[j].hash == h && slots[j].size == entries[i].size) { reuse = slots[j].data_off; break; } } if (reuse != UINT64_MAX) { data_off_for[i] = reuse; } else { data_off_for[i] = blob_cursor; slots[slot_count].hash = h; slots[slot_count].size = entries[i].size; slots[slot_count].data_off = blob_cursor; slot_count++; blob_cursor += entries[i].size; } strings_total += strlen(entries[i].name) + 1; } free(slots); uint64_t index_offset = align8(blob_cursor); uint64_t index_bytes = (uint64_t)count * sizeof(PackIndexEntry); uint64_t strings_offset = index_offset + index_bytes; uint64_t total_size = strings_offset + strings_total; unsigned char *buf = (unsigned char *)calloc(1, (size_t)total_size); if (!buf) { free(entries); free(data_off_for); if (err) *err = VFS_ERR_NOSPC; return -1; } for (size_t i = 0; i < count; i++) { /* Deduplicated entries (Section 9.2) share a data_off; writing * the same bytes to it more than once is redundant but harmless. */ memcpy(buf + data_off_for[i], entries[i].data, entries[i].size); } PackIndexEntry *out_entries = (PackIndexEntry *)(buf + index_offset); uint64_t str_cursor = 0; for (size_t i = 0; i < count; i++) { size_t nlen = strlen(entries[i].name); out_entries[i].name_off = str_cursor; out_entries[i].name_len = (uint32_t)nlen; out_entries[i].data_off = data_off_for[i]; out_entries[i].size = entries[i].size; out_entries[i].mode = entries[i].mode; out_entries[i].mtime = entries[i].mtime; memcpy(buf + strings_offset + str_cursor, entries[i].name, nlen + 1); str_cursor += nlen + 1; } PackHeader hdr; memset(&hdr, 0, sizeof(hdr)); memcpy(hdr.magic, "PKFS", 4); hdr.version = PACK_VERSION; hdr.index_offset = index_offset; hdr.index_count = count; hdr.strings_offset = strings_offset; hdr.strings_len = strings_total; hdr.checksum = pfs_fnv1a64(buf + index_offset, (size_t)(strings_offset + strings_total - index_offset)); memcpy(buf, &hdr, sizeof(hdr)); free(entries); free(data_off_for); /* Atomic compaction (Section 4.3): write to a temp file, fsync, then * rename over the target. A failure here aborts compaction and * leaves the existing pack untouched. */ char tmp_path[PFS_PATH_MAX]; snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path); int fd = open(tmp_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd < 0) { free(buf); if (err) *err = VFS_ERR_IO; return -1; } size_t written = 0; while (written < (size_t)total_size) { ssize_t w = write(fd, buf + written, (size_t)total_size - written); if (w < 0) { close(fd); free(buf); unlink(tmp_path); if (err) *err = VFS_ERR_IO; return -1; } written += (size_t)w; } free(buf); if (fsync(fd) < 0) { close(fd); unlink(tmp_path); if (err) *err = VFS_ERR_IO; return -1; } close(fd); if (rename(tmp_path, path) < 0) { unlink(tmp_path); if (err) *err = VFS_ERR_IO; return -1; } return 0; } /* * ---- standalone read-only pack Backend (Section 2.1, 3.1) ---- * * `backend_overlay_new` is how a pack is normally made writable (Sections * 4-5), but Section 2.1's architecture diagram lists `pack` as its own * backend kind too — "one file, indexed, read-only" — for the case where * no writable upper layer is wanted at all, e.g. mounting a shipped asset * pack somewhere a program never intends to write. This wraps the same * `Pack` load/lookup/range machinery used by the overlay as a plain, * read-only Backend: every mutating operation returns VFS_ERR_PERM rather * than being silently absorbed. */ typedef struct PackFile { const Pack *pack; const PackIndexEntry *entry; pfs_usize pos; } PackFile; static int packbe_open(Backend *b, const char *path, int flags, VfsFile **out) { Pack *p = (Pack *)b->state; if (flags & (VFS_O_WRONLY | VFS_O_RDWR | VFS_O_CREAT | VFS_O_TRUNC)) return VFS_ERR_PERM; const PackIndexEntry *e; if (!pack_find(p, path, &e)) return VFS_ERR_NOENT; if (e->mode & PFS_MODE_DIR) return VFS_ERR_ISDIR; PackFile *pf = (PackFile *)calloc(1, sizeof(PackFile)); pf->pack = p; pf->entry = e; VfsFile *f = (VfsFile *)calloc(1, sizeof(VfsFile)); f->backend = b; f->state = pf; *out = f; return VFS_OK; } static pfs_isize packbe_read(VfsFile *f, void *buf, pfs_usize n) { PackFile *pf = (PackFile *)f->state; pfs_usize size = pf->entry->size; pfs_usize avail = pf->pos < size ? size - pf->pos : 0; pfs_usize to_copy = n < avail ? n : avail; if (to_copy) memcpy(buf, (const char *)pack_entry_data(pf->pack, pf->entry) + pf->pos, to_copy); pf->pos += to_copy; return (pfs_isize)to_copy; } static pfs_isize packbe_write(VfsFile *f, const void *buf, pfs_usize n) { (void)f; (void)buf; (void)n; return VFS_ERR_PERM; } static int packbe_close(VfsFile *f) { free(f->state); free(f); return VFS_OK; } static int packbe_stat(Backend *b, const char *path, VfsStat *out) { Pack *p = (Pack *)b->state; const PackIndexEntry *e; if (pack_find(p, path, &e)) { out->size = e->size; out->mtime = e->mtime; out->kind = (e->mode & PFS_MODE_DIR) ? VFS_KIND_DIR : VFS_KIND_FILE; return VFS_OK; } char prefix[PFS_PATH_MAX]; if (strcmp(path, "/") == 0) strcpy(prefix, "/"); else snprintf(prefix, sizeof(prefix), "%s/", path); size_t lo, hi; pack_range(p, prefix, &lo, &hi); if (hi > lo || strcmp(path, "/") == 0) { out->size = 0; out->mtime = 0; out->kind = VFS_KIND_DIR; return VFS_OK; } return VFS_ERR_NOENT; } static int packbe_readdir(Backend *b, const char *path, VfsDir *out) { Pack *p = (Pack *)b->state; char prefix[PFS_PATH_MAX]; if (strcmp(path, "/") == 0) strcpy(prefix, "/"); else snprintf(prefix, sizeof(prefix), "%s/", path); size_t plen = strlen(prefix); size_t lo, hi; pack_range(p, prefix, &lo, &hi); if (lo == hi && strcmp(path, "/") != 0) { const PackIndexEntry *e; if (!pack_find(p, path, &e)) return VFS_ERR_NOENT; if (!(e->mode & PFS_MODE_DIR)) return VFS_ERR_NOTDIR; } VfsDirEntry *entries = NULL; pfs_usize count = 0, cap = 0; for (size_t i = lo; i < hi; i++) { const char *name = pack_entry_name(p, &p->entries[i]); const char *restp = name + plen; const char *slash = strchr(restp, '/'); size_t clen = slash ? (size_t)(slash - restp) : strlen(restp); if (clen == 0 || clen >= sizeof(entries[0].name)) continue; if (count > 0 && strncmp(entries[count - 1].name, restp, clen) == 0 && entries[count - 1].name[clen] == '\0') continue; /* sorted -> dup is adjacent */ if (count == cap) { cap = cap ? cap * 2 : 8; entries = (VfsDirEntry *)realloc(entries, cap * sizeof(VfsDirEntry)); } memcpy(entries[count].name, restp, clen); entries[count].name[clen] = '\0'; entries[count].kind = slash ? VFS_KIND_DIR : ((p->entries[i].mode & PFS_MODE_DIR) ? VFS_KIND_DIR : VFS_KIND_FILE); count++; } out->entries = entries; out->count = count; return VFS_OK; } static int packbe_mkdir(Backend *b, const char *path) { (void)b; (void)path; return VFS_ERR_PERM; } static int packbe_unlink(Backend *b, const char *path) { (void)b; (void)path; return VFS_ERR_PERM; } static int packbe_rename(Backend *b, const char *from, const char *to) { (void)b; (void)from; (void)to; return VFS_ERR_PERM; } static int packbe_sync(Backend *b) { (void)b; return VFS_ERR_PERM; } static void packbe_free(Backend *b) { pack_close((Pack *)b->state); free(b); } static const BackendOps PACK_OPS = { packbe_open, packbe_read, packbe_write, packbe_close, packbe_stat, packbe_readdir, packbe_mkdir, packbe_unlink, packbe_rename, packbe_sync, packbe_free }; Backend *backend_pack_new(const char *pack_path, int *err) { Pack *p; if (pack_load(pack_path, &p, err) < 0) return NULL; Backend *b = (Backend *)calloc(1, sizeof(Backend)); b->ops = &PACK_OPS; b->state = p; return b; }