From 1b1f5b7972412c695fd4061760c30529fc639ef5 Mon Sep 17 00:00:00 2001 From: xCyanGrizzly Date: Mon, 27 Jul 2026 11:14:17 +0200 Subject: [PATCH] feat(worker): sparse-file reconstruction helper for ranged archive listing --- worker/src/archive/ranged/sparse-list.test.ts | 41 +++++++++++++++ worker/src/archive/ranged/sparse-list.ts | 52 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 worker/src/archive/ranged/sparse-list.test.ts create mode 100644 worker/src/archive/ranged/sparse-list.ts diff --git a/worker/src/archive/ranged/sparse-list.test.ts b/worker/src/archive/ranged/sparse-list.test.ts new file mode 100644 index 0000000..08545c0 --- /dev/null +++ b/worker/src/archive/ranged/sparse-list.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from "vitest"; +import { open } from "fs/promises"; +import { listFromSparse } from "./sparse-list.js"; + +describe("listFromSparse", () => { + it("writes each region at its offset into a sparse file and passes the path to the lister", async () => { + const size = 1_000_000; + const regions = [ + { offset: 0, bytes: Buffer.from("HEAD") }, + { offset: size - 4, bytes: Buffer.from("TAIL") }, + ]; + let seenPath = ""; + const entries = await listFromSparse( + [{ fileName: "sample.7z", size, regions }], + async (firstPartPath) => { + seenPath = firstPartPath; + const fh = await open(firstPartPath, "r"); + try { + const head = Buffer.alloc(4); await fh.read(head, 0, 4, 0); + const tail = Buffer.alloc(4); await fh.read(tail, 0, 4, size - 4); + const hole = Buffer.alloc(4); await fh.read(hole, 0, 4, 500_000); + expect(head.toString()).toBe("HEAD"); + expect(tail.toString()).toBe("TAIL"); + expect(hole.equals(Buffer.alloc(4))).toBe(true); // gap is zero + } finally { await fh.close(); } + return [{ path: "a/b.stl", fileName: "b.stl", extension: "stl", compressedSize: 1n, uncompressedSize: 1n, crc32: null }]; + }, + ); + expect(seenPath.endsWith("sample.7z")).toBe(true); + expect(entries).not.toBeNull(); + expect(entries!).toHaveLength(1); + }); + + it("returns null when the lister yields no entries", async () => { + const res = await listFromSparse( + [{ fileName: "x.7z", size: 100, regions: [{ offset: 0, bytes: Buffer.from("A") }] }], + async () => [], + ); + expect(res).toBeNull(); + }); +}); diff --git a/worker/src/archive/ranged/sparse-list.ts b/worker/src/archive/ranged/sparse-list.ts new file mode 100644 index 0000000..f96f0e4 --- /dev/null +++ b/worker/src/archive/ranged/sparse-list.ts @@ -0,0 +1,52 @@ +import { mkdtemp, open, rm } from "fs/promises"; +import path from "path"; +import { config } from "../../util/config.js"; +import { childLogger } from "../../util/logger.js"; +import type { FileEntry } from "../zip-reader.js"; + +const log = childLogger("sparse-list"); + +export interface SparsePart { + fileName: string; + size: number; + regions: { offset: number; bytes: Buffer }[]; +} + +export type SparseLister = (firstPartPath: string) => Promise; + +/** + * Reconstruct archive header bytes into sparse temp files (data areas left as + * zero holes), run `lister` on the first part, return its entries. + * Returns null on any error or when the lister finds nothing. + */ +export async function listFromSparse( + parts: SparsePart[], + lister: SparseLister, +): Promise { + if (parts.length === 0) return null; + const dir = await mkdtemp(path.join(config.tempDir, "ranged-")); + try { + let firstPath = ""; + for (let i = 0; i < parts.length; i++) { + const p = parts[i]; + const filePath = path.join(dir, p.fileName); + if (i === 0) firstPath = filePath; + const fh = await open(filePath, "w"); + try { + await fh.truncate(p.size); // create the sparse hole + for (const r of p.regions) { + await fh.write(r.bytes, 0, r.bytes.length, r.offset); + } + } finally { + await fh.close(); + } + } + const entries = await lister(firstPath); + return entries.length > 0 ? entries : null; + } catch (err) { + log.warn({ err }, "sparse listing failed"); + return null; + } finally { + await rm(dir, { recursive: true, force: true }); + } +}