diff --git a/worker/src/archive/ranged/sevenz-ranged.test.ts b/worker/src/archive/ranged/sevenz-ranged.test.ts index 1a3cc5a..692c440 100644 --- a/worker/src/archive/ranged/sevenz-ranged.test.ts +++ b/worker/src/archive/ranged/sevenz-ranged.test.ts @@ -63,3 +63,67 @@ describe("readSevenZListingRanged", () => { expect(entries === null || Array.isArray(entries)).toBe(true); }); }); + +import { read7zNumber, locate7zEncodedHeaderPack } from "./sevenz-ranged.js"; + +describe("read7zNumber", () => { + it("reads a single-byte number", () => { + expect(read7zNumber(Buffer.from([0x2a]), 0)).toEqual({ value: 42, next: 1 }); + }); + it("reads a two-byte number (first-byte length mask + LE trailing byte)", () => { + // 1000 = 0x03E8: low byte 0xE8, high nibble 0x03 -> first = 0x80|0x03 = 0x83, trailing 0xE8 + expect(read7zNumber(Buffer.from([0x83, 0xe8]), 0)).toEqual({ value: 1000, next: 2 }); + // 500 = 0x01F4 -> first 0x81, trailing 0xF4 + expect(read7zNumber(Buffer.from([0x81, 0xf4]), 0)).toEqual({ value: 500, next: 2 }); + }); +}); + +describe("locate7zEncodedHeaderPack", () => { + it("parses PackPos and summed PackSize from an encoded header", () => { + // kEncodedHeader, kPackInfo, PackPos=1000([0x83,0xe8]), NumStreams=1([0x01]), + // kSize, PackSize=500([0x81,0xf4]) + const enc = Buffer.from([0x17, 0x06, 0x83, 0xe8, 0x01, 0x09, 0x81, 0xf4]); + expect(locate7zEncodedHeaderPack(enc)).toEqual({ packPos: 1000, packSize: 500 }); + }); + it("returns null for a plain (kHeader 0x01) header", () => { + expect(locate7zEncodedHeaderPack(Buffer.from([0x01, 0x04]))).toBeNull(); + }); + it("sums multiple pack streams", () => { + // PackPos=0([0x00]), NumStreams=2([0x02]), kSize, sizes 10([0x0a]) + 20([0x14]) + const enc = Buffer.from([0x17, 0x06, 0x00, 0x02, 0x09, 0x0a, 0x14]); + expect(locate7zEncodedHeaderPack(enc)).toEqual({ packPos: 0, packSize: 30 }); + }); +}); + +describe("readSevenZListingRanged (encoded header)", () => { + it("fetches the mid-file packed-header region as a third read", async () => { + const size = 5_000_000; + const nextHeaderOffset = 4_000_000; // relative to end of 32-byte sig header + const endStart = 32 + nextHeaderOffset; // absolute + const packPos = 1000; // relative to end of sig header + const packStart = 32 + packPos; // absolute + const packSize = 500; + const sig = Buffer.alloc(32); + Buffer.from([0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c]).copy(sig, 0); + sig.writeBigUInt64LE(BigInt(nextHeaderOffset), 12); + sig.writeBigUInt64LE(8n, 20); // NextHeaderSize = 8 (the encoded-header descriptor below) + const encHeader = Buffer.from([0x17, 0x06, 0x83, 0xe8, 0x01, 0x09, 0x81, 0xf4]); + + const reads: { offset: number; length: number }[] = []; + const read = async (_id: string, offset: number, length: number) => { + reads.push({ offset, length }); + if (offset === 0) return sig.subarray(0, length); + if (offset === endStart) return encHeader.subarray(0, length); + return Buffer.alloc(length, 0xcd); // stand-in packed-header bytes + }; + + const entries = await readSevenZListingRanged( + [{ fileId: "1", fileSize: BigInt(size), fileName: "a.7z" }], + read, + ); + expect(reads[0]).toEqual({ offset: 0, length: 32 }); + expect(reads[1]).toEqual({ offset: endStart, length: 8 }); + expect(reads[2]).toEqual({ offset: packStart, length: packSize }); + expect(entries === null || Array.isArray(entries)).toBe(true); + }); +}); diff --git a/worker/src/archive/ranged/sevenz-ranged.ts b/worker/src/archive/ranged/sevenz-ranged.ts index 0892f00..f27de73 100644 --- a/worker/src/archive/ranged/sevenz-ranged.ts +++ b/worker/src/archive/ranged/sevenz-ranged.ts @@ -8,6 +8,55 @@ const log = childLogger("sevenz-ranged"); const SEVENZ_MAGIC = Buffer.from([0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c]); +const K_HEADER = 0x01; +const K_ENCODED_HEADER = 0x17; +const K_PACK_INFO = 0x06; +const K_SIZE = 0x09; + +/** Read a 7z variable-length number: first byte is a length mask, followed by + * little-endian bytes. Math.pow keeps values exact above 2^31. */ +export function read7zNumber(buf: Buffer, pos: number): { value: number; next: number } { + const first = buf[pos]; + let mask = 0x80; + let value = 0; + let p = pos + 1; + for (let i = 0; i < 8; i++) { + if ((first & mask) === 0) { + value += (first & (mask - 1)) * Math.pow(2, 8 * i); + return { value, next: p }; + } + if (p >= buf.length) throw new RangeError("7z number overruns buffer"); + value += buf[p] * Math.pow(2, 8 * i); + p++; + mask >>= 1; + } + return { value, next: p }; +} + +/** For an encoded (kEncodedHeader) 7z next-header, return the absolute-ish + * location of the packed header stream(s): PackPos (relative to end of the + * 32-byte signature header) and the summed PackSize. Null if not encoded or + * the StreamsInfo doesn't start with PackInfo as expected. */ +export function locate7zEncodedHeaderPack( + nextHeader: Buffer, +): { packPos: number; packSize: number } | null { + let p = 0; + if (nextHeader[p] !== K_ENCODED_HEADER) return null; + p++; + if (nextHeader[p] !== K_PACK_INFO) return null; + p++; + const packPos = read7zNumber(nextHeader, p); p = packPos.next; + const numStreams = read7zNumber(nextHeader, p); p = numStreams.next; + if (nextHeader[p] !== K_SIZE) return null; + p++; + let total = 0; + for (let i = 0; i < numStreams.value; i++) { + const s = read7zNumber(nextHeader, p); p = s.next; + total += s.value; + } + return { packPos: packPos.value, packSize: total }; +} + export function parseSevenZSignatureHeader( buf: Buffer, ): { nextHeaderOffset: number; nextHeaderSize: number } | null { @@ -35,17 +84,26 @@ export async function readSevenZListingRanged( const endStart = 32 + parsed.nextHeaderOffset; if (endStart < 0 || endStart + parsed.nextHeaderSize > size) return null; const endHeader = await read(part.fileId, endStart, parsed.nextHeaderSize, part.fileSize); - return listFromSparse( - [{ - fileName: part.fileName, - size, - regions: [ - { offset: 0, bytes: sig }, - { offset: endStart, bytes: endHeader }, - ], - }], - read7zContents, - ); + + const regions = [ + { offset: 0, bytes: sig }, + { offset: endStart, bytes: endHeader }, + ]; + + const headerType = endHeader[0]; + if (headerType === K_ENCODED_HEADER) { + // Compressed header: its packed bytes live mid-file, not at EOF. Fetch them. + const pack = locate7zEncodedHeaderPack(endHeader); + if (!pack) return null; + const packStart = 32 + pack.packPos; + if (packStart < 0 || packStart + pack.packSize > size) return null; + const packBytes = await read(part.fileId, packStart, pack.packSize, part.fileSize); + regions.push({ offset: packStart, bytes: packBytes }); + } else if (headerType !== K_HEADER) { + return null; // unknown next-header type + } + + return listFromSparse([{ fileName: part.fileName, size, regions }], read7zContents); } catch (err) { log.warn({ err, fileId: part.fileId }, "ranged 7z listing failed"); return null;