feat(worker): RAR vint/signature/block-extent parsers

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-27 11:39:19 +02:00
co-authored by Claude Opus 4.8
parent 49f14bcb0d
commit abdfa437d9
2 changed files with 102 additions and 0 deletions
@@ -0,0 +1,56 @@
import { describe, it, expect } from "vitest";
import { readVint, detectRarSignature, parseRar5BlockExtent, parseRar4BlockExtent } from "./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 });
// 0x80,0x01 => 0 | (1<<7) = 128
expect(readVint(Buffer.from([0x80, 0x01]), 0)).toEqual({ value: 128, bytes: 2 });
});
});
describe("detectRarSignature", () => {
it("detects RAR5 and RAR4", () => {
expect(detectRarSignature(Buffer.from([0x52,0x61,0x72,0x21,0x1a,0x07,0x01,0x00]))).toEqual({ version: 5, sigLen: 8 });
expect(detectRarSignature(Buffer.from([0x52,0x61,0x72,0x21,0x1a,0x07,0x00]))).toEqual({ version: 4, sigLen: 7 });
expect(detectRarSignature(Buffer.alloc(8))).toBeNull();
});
});
describe("parseRar5BlockExtent", () => {
it("computes header+data extent and flags end-of-archive", () => {
// CRC32(4) | HeaderSize vint=5 | Type vint=2 (file) | Flags vint=2 (data present) | DataSize vint=100 | (pad to headerSize)
const b = Buffer.concat([
Buffer.from([0,0,0,0]), // CRC
Buffer.from([0x05]), // HeaderSize = 5 (bytes after this vint)
Buffer.from([0x02]), // Type = 2 (file)
Buffer.from([0x02]), // Flags = 0x02 -> data present
Buffer.from([0x64]), // DataSize = 100
Buffer.from([0x00, 0x00]), // padding to fill HeaderSize(5): Type+Flags+DataSize=3, +2 pad =5
]);
const ext = parseRar5BlockExtent(b, 0);
// headerBytes = 4 (CRC) + 1 (HeaderSize vint) + 5 (HeaderSize) = 10
expect(ext.headerBytes).toBe(10);
expect(ext.dataSize).toBe(100);
expect(ext.isEnd).toBe(false);
const endBlk = Buffer.from([0,0,0,0, 0x02, 0x05, 0x00]); // HeaderSize=2, Type=5(end), Flags=0
const e2 = parseRar5BlockExtent(endBlk, 0);
expect(e2.isEnd).toBe(true);
});
});
describe("parseRar4BlockExtent", () => {
it("computes extent with ADD_SIZE when flag 0x8000 is set", () => {
// CRC(2) TYPE(1)=0x74 FLAGS(2)=0x8000 HEAD_SIZE(2)=11 ADD_SIZE(4)=200
const b = Buffer.alloc(11);
b.writeUInt8(0x74, 2);
b.writeUInt16LE(0x8000, 3);
b.writeUInt16LE(11, 5);
b.writeUInt32LE(200, 7);
const ext = parseRar4BlockExtent(b, 0);
expect(ext.headerBytes).toBe(11);
expect(ext.dataSize).toBe(200);
expect(ext.isEnd).toBe(false);
});
});
+46
View File
@@ -0,0 +1,46 @@
const RAR4_SIG = Buffer.from([0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x00]);
const RAR5_SIG = Buffer.from([0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x01, 0x00]);
export function readVint(buf: Buffer, pos: number): { value: number; bytes: number } {
let value = 0, shift = 0, bytes = 0;
while (pos + bytes < buf.length) {
const b = buf[pos + bytes];
value += (b & 0x7f) * Math.pow(2, shift); // Math.pow keeps >32-bit sizes exact up to 2^53
bytes++;
if ((b & 0x80) === 0) return { value, bytes };
shift += 7;
if (shift > 63) break;
}
throw new RangeError("incomplete RAR vint");
}
export function detectRarSignature(buf: Buffer): { version: 4 | 5; sigLen: number } | null {
if (buf.length >= 8 && buf.subarray(0, 8).equals(RAR5_SIG)) return { version: 5, sigLen: 8 };
if (buf.length >= 7 && buf.subarray(0, 7).equals(RAR4_SIG)) return { version: 4, sigLen: 7 };
return null;
}
export interface BlockExtent { headerBytes: number; dataSize: number; isEnd: boolean }
// RAR5: CRC32(4) | HeaderSize(vint) | HeaderType(vint) | HeaderFlags(vint)
// [ExtraAreaSize(vint) if flags&0x0001] [DataSize(vint) if flags&0x0002] ...
export function parseRar5BlockExtent(buf: Buffer, pos: number): BlockExtent {
let p = pos + 4; // skip CRC32
const hs = readVint(buf, p); p += hs.bytes;
const headerBytes = 4 + hs.bytes + hs.value; // CRC + HeaderSize-vint + HeaderSize
const type = readVint(buf, p); p += type.bytes;
const flags = readVint(buf, p); p += flags.bytes;
if (flags.value & 0x0001) { const ea = readVint(buf, p); p += ea.bytes; } // extra area size (skip)
let dataSize = 0;
if (flags.value & 0x0002) { const ds = readVint(buf, p); p += ds.bytes; dataSize = ds.value; }
return { headerBytes, dataSize, isEnd: type.value === 5 };
}
// RAR4: HEAD_CRC(2) | HEAD_TYPE(1) | HEAD_FLAGS(2) | HEAD_SIZE(2) [ADD_SIZE(4) if flags&0x8000]
export function parseRar4BlockExtent(buf: Buffer, pos: number): BlockExtent {
const type = buf.readUInt8(pos + 2);
const flags = buf.readUInt16LE(pos + 3);
const headSize = buf.readUInt16LE(pos + 5);
const dataSize = (flags & 0x8000) ? buf.readUInt32LE(pos + 7) : 0;
return { headerBytes: headSize, dataSize, isEnd: type === 0x7b };
}