mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-09-21 05:21:43 +00:00
feat(worker): RAR ranged header-walk + single-part listing
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readVint, detectRarSignature, parseRar5BlockExtent, parseRar4BlockExtent } from "./rar-ranged.js";
|
||||
import { readVint, detectRarSignature, parseRar5BlockExtent, parseRar4BlockExtent, walkRarVolume, readRarListingRanged } from "./rar-ranged.js";
|
||||
import type { RangeReader } from "./range-reader.js";
|
||||
|
||||
describe("readVint", () => {
|
||||
it("reads single-byte and multi-byte values (base-128 LE)", () => {
|
||||
@@ -54,3 +55,46 @@ describe("parseRar4BlockExtent", () => {
|
||||
expect(ext.isEnd).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// Build a synthetic RAR5 volume: signature + main header + 2 file blocks (each
|
||||
// with data) + end block. We only need extents to be walkable.
|
||||
function buildRar5Volume(): Buffer {
|
||||
const sig = Buffer.from([0x52,0x61,0x72,0x21,0x1a,0x07,0x01,0x00]);
|
||||
const block = (type: number, flags: number, dataSize: number, pad = 0) => {
|
||||
const body = [Buffer.from([type]), Buffer.from([flags])];
|
||||
if (flags & 0x0002) body.push(Buffer.from([dataSize])); // DataSize (<=127 for test)
|
||||
if (pad) body.push(Buffer.alloc(pad));
|
||||
const bodyBuf = Buffer.concat(body);
|
||||
const hs = Buffer.from([bodyBuf.length]); // HeaderSize vint (<=127)
|
||||
const header = Buffer.concat([Buffer.alloc(4), hs, bodyBuf]); // CRC(4)+HeaderSize+body
|
||||
const data = Buffer.alloc(flags & 0x0002 ? dataSize : 0, 0xEE);
|
||||
return Buffer.concat([header, data]);
|
||||
};
|
||||
const main = block(1, 0, 0); // main archive header, no data
|
||||
const f1 = block(2, 0x02, 20); // file header + 20 bytes data
|
||||
const f2 = block(2, 0x02, 30); // file header + 30 bytes data
|
||||
const end = block(5, 0, 0); // end of archive
|
||||
return Buffer.concat([sig, main, f1, f2, end]);
|
||||
}
|
||||
|
||||
describe("walkRarVolume", () => {
|
||||
it("harvests every block header and stops at end-of-archive", async () => {
|
||||
const vol = buildRar5Volume();
|
||||
const read: RangeReader = async (_id, offset, length) => vol.subarray(offset, offset + length);
|
||||
const regions = await walkRarVolume(read, { fileId: "1", fileSize: BigInt(vol.length), fileName: "a.rar" }, 5, 8);
|
||||
expect(regions).not.toBeNull();
|
||||
// main + 2 files + end = 4 header regions
|
||||
expect(regions!).toHaveLength(4);
|
||||
// First region starts right after the 8-byte signature
|
||||
expect(regions![0].offset).toBe(8);
|
||||
});
|
||||
});
|
||||
|
||||
describe("readRarListingRanged (single part)", () => {
|
||||
it("returns null cleanly when the reconstructed file isn't a real RAR", async () => {
|
||||
const vol = buildRar5Volume();
|
||||
const read: RangeReader = async (_id, offset, length) => vol.subarray(offset, offset + length);
|
||||
const res = await readRarListingRanged([{ fileId: "1", fileSize: BigInt(vol.length), fileName: "a.rar" }], read);
|
||||
expect(res === null || Array.isArray(res)).toBe(true); // real unrar parse covered live
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,3 +44,65 @@ export function parseRar4BlockExtent(buf: Buffer, pos: number): BlockExtent {
|
||||
const dataSize = (flags & 0x8000) ? buf.readUInt32LE(pos + 7) : 0;
|
||||
return { headerBytes: headSize, dataSize, isEnd: type === 0x7b };
|
||||
}
|
||||
|
||||
import type { FileEntry } from "../zip-reader.js";
|
||||
import type { RangeReader } from "./range-reader.js";
|
||||
import type { RangedPart } from "./sevenz-ranged.js";
|
||||
import { listFromSparse, type SparsePart } from "./sparse-list.js";
|
||||
import { readRarContents } from "../rar-reader.js";
|
||||
import { childLogger } from "../../util/logger.js";
|
||||
|
||||
const rlog = childLogger("rar-ranged");
|
||||
const MAX_RAR_BLOCKS = 50000;
|
||||
const HEADER_CHUNK = 8192;
|
||||
|
||||
export async function walkRarVolume(
|
||||
read: RangeReader,
|
||||
part: RangedPart,
|
||||
version: 4 | 5,
|
||||
sigLen: number,
|
||||
): Promise<{ offset: number; bytes: Buffer }[] | null> {
|
||||
const size = Number(part.fileSize);
|
||||
const regions: { offset: number; bytes: Buffer }[] = [];
|
||||
let pos = sigLen;
|
||||
let blocks = 0;
|
||||
try {
|
||||
while (pos < size) {
|
||||
if (++blocks > MAX_RAR_BLOCKS) return null;
|
||||
const chunkLen = Math.min(HEADER_CHUNK, size - pos);
|
||||
let chunk = await read(part.fileId, pos, chunkLen, part.fileSize);
|
||||
const ext = version === 5 ? parseRar5BlockExtent(chunk, 0) : parseRar4BlockExtent(chunk, 0);
|
||||
// Ensure we have the full header bytes to harvest (long filenames).
|
||||
let headerBuf = chunk;
|
||||
if (ext.headerBytes > chunk.length) {
|
||||
headerBuf = await read(part.fileId, pos, Math.min(ext.headerBytes, size - pos), part.fileSize);
|
||||
}
|
||||
regions.push({ offset: pos, bytes: headerBuf.subarray(0, Math.min(ext.headerBytes, size - pos)) });
|
||||
if (ext.isEnd) break;
|
||||
const advance = ext.headerBytes + ext.dataSize;
|
||||
if (advance <= 0) return null;
|
||||
if (pos + advance > size) break; // data clamped at the volume boundary (multipart continuation)
|
||||
pos += advance;
|
||||
}
|
||||
return regions;
|
||||
} catch (err) {
|
||||
rlog.warn({ err, fileId: part.fileId }, "RAR volume walk failed");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readRarListingRanged(
|
||||
parts: RangedPart[],
|
||||
read: RangeReader,
|
||||
): Promise<FileEntry[] | null> {
|
||||
const sparseParts: SparsePart[] = [];
|
||||
for (const part of parts) {
|
||||
const head = await read(part.fileId, 0, 16, part.fileSize);
|
||||
const sig = detectRarSignature(head);
|
||||
if (!sig) return null;
|
||||
const regions = await walkRarVolume(read, part, sig.version, sig.sigLen);
|
||||
if (!regions) return null;
|
||||
sparseParts.push({ fileName: part.fileName, size: Number(part.fileSize), regions });
|
||||
}
|
||||
return listFromSparse(sparseParts, readRarContents);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user