diff --git a/worker/src/archive/ranged/rar-ranged.test.ts b/worker/src/archive/ranged/rar-ranged.test.ts index 4394817..7d2e547 100644 --- a/worker/src/archive/ranged/rar-ranged.test.ts +++ b/worker/src/archive/ranged/rar-ranged.test.ts @@ -1,7 +1,19 @@ -import { describe, it, expect } from "vitest"; -import { readVint, detectRarSignature, parseRar5BlockExtent, parseRar4BlockExtent, walkRarVolume, readRarListingRanged } from "./rar-ranged.js"; +import { describe, it, expect, vi } from "vitest"; import type { RangeReader } from "./range-reader.js"; +let capturedFirstBytes: Buffer | null = null; +vi.mock("../rar-reader.js", () => ({ + readRarContents: async (firstPartPath: string) => { + const { readFile } = await import("fs/promises"); + const reconstructed = await readFile(firstPartPath); + capturedFirstBytes = reconstructed.subarray(0, 8); + return [{ name: "dummy", size: 0 }]; // non-empty so listFromSparse returns it + }, +})); + +const { readVint, detectRarSignature, parseRar5BlockExtent, parseRar4BlockExtent, walkRarVolume, readRarListingRanged } = + await import("./rar-ranged.js"); + describe("readVint", () => { it("reads single-byte and multi-byte values (base-128 LE)", () => { expect(readVint(Buffer.from([0x08]), 0)).toEqual({ value: 8, bytes: 1 }); @@ -123,6 +135,23 @@ describe("readRarListingRanged (single part)", () => { 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 }); + + it("preserves the RAR signature bytes in the reconstructed sparse file", async () => { + // Regression test: walkRarVolume starts at pos = sigLen and never + // harvests the signature itself. If readRarListingRanged forgets to add + // it as its own region, the reconstructed file starts with zero bytes + // instead of "Rar!\x1a\x07\x01\x00", and every real unrar invocation + // rejects it as "not RAR archive" — silently forcing every RAR archive + // through the expensive download+reupload fallback regardless of the + // channel's forwarding permission. + const vol = buildRar5Volume(); + const sig = vol.subarray(0, 8); + const read: RangeReader = async (_id, offset, length) => vol.subarray(offset, offset + length); + capturedFirstBytes = null; + await readRarListingRanged([{ fileId: "1", fileSize: BigInt(vol.length), fileName: "a.rar" }], read); + expect(capturedFirstBytes).not.toBeNull(); + expect(capturedFirstBytes).toEqual(sig); + }); }); describe("readRarListingRanged (multipart)", () => { diff --git a/worker/src/archive/ranged/rar-ranged.ts b/worker/src/archive/ranged/rar-ranged.ts index 1e87194..4e02322 100644 --- a/worker/src/archive/ranged/rar-ranged.ts +++ b/worker/src/archive/ranged/rar-ranged.ts @@ -104,7 +104,12 @@ export async function readRarListingRanged( 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 }); + // walkRarVolume starts at pos = sigLen and never harvests the signature + // itself, so it must be added as its own region — otherwise the + // reconstructed sparse file starts with zero bytes instead of the "Rar!" + // magic, and unrar rejects it outright as "not RAR archive". + const sigRegion = { offset: 0, bytes: head.subarray(0, sig.sigLen) }; + sparseParts.push({ fileName: part.fileName, size: Number(part.fileSize), regions: [sigRegion, ...regions] }); } return listFromSparse(sparseParts, readRarContents); }