From 73b2c333051c1ee7549e36ec2e467f3317637d17 Mon Sep 17 00:00:00 2001 From: xCyanGrizzly Date: Tue, 11 Aug 2026 23:17:22 +0200 Subject: [PATCH] fix(worker): include the RAR signature in the ranged-listing sparse reconstruction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- worker/src/archive/ranged/rar-ranged.test.ts | 33 ++++++++++++++++++-- worker/src/archive/ranged/rar-ranged.ts | 7 ++++- 2 files changed, 37 insertions(+), 3 deletions(-) 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); }