fix(worker): include the RAR signature in the ranged-listing sparse reconstruction
continuous-integration/drone/push Build is passing

readRarListingRanged read the archive signature only to detect the RAR
version, then walkRarVolume began harvesting header regions at
pos = sigLen — the signature bytes themselves were never captured.
The reconstructed sparse file therefore started with zero bytes
instead of "Rar!", so every real unrar invocation rejected it as
"not RAR archive". This silently forced every RAR archive through the
expensive download+reupload fallback regardless of the source
channel's forwarding permission — defeating the forward-priority path
for the RAR-heavy channels it matters most for. The existing tests
didn't catch this because their assertions accepted either a null or
non-null result as passing, deferring real verification to production.
This commit is contained in:
2026-08-11 23:17:22 +02:00
parent 06a48a419b
commit 73b2c33305
2 changed files with 37 additions and 3 deletions
+31 -2
View File
@@ -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)", () => {
+6 -1
View File
@@ -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);
}