streaming 7-Zip archive extractor
1import { assert, assertEquals, assertRejects } from '@std/assert';
2
3import { ChecksumMismatchError, InvalidSignatureError, TruncatedArchiveError } from '../lib/errors.ts';
4import { readContainer, readNextHeader, readSignatureHeader } from '../lib/container.ts';
5import { fromUint8Array } from '../lib/reader/common.ts';
6
7const FIXTURES = new URL('./fixtures/commons-compress/', import.meta.url);
8const LIBARCHIVE_FIXTURES = new URL('./fixtures/libarchive/', import.meta.url);
9
10const loadFixture = (name: string): Uint8Array<ArrayBuffer> => {
11 return Deno.readFileSync(new URL(name, FIXTURES));
12};
13
14const loadLibarchive = (name: string): Uint8Array<ArrayBuffer> => {
15 return Deno.readFileSync(new URL(name, LIBARCHIVE_FIXTURES));
16};
17
18Deno.test('readSignatureHeader: valid mhc-off-copy fixture', async () => {
19 const reader = fromUint8Array(loadFixture('7z-hello-mhc-off-copy.7z'));
20 const start = await readSignatureHeader(reader);
21 assertEquals(start.versionMajor, 0);
22 // nextHeader is at offset 32 + 14 = 46 (after 14 bytes of "Hello, world!\n" payload)
23 assertEquals(start.nextHeaderOffset, 14);
24 assertEquals(start.nextHeaderSize, 86);
25});
26
27Deno.test('readSignatureHeader: rejects non-7z input', async () => {
28 const garbage = new Uint8Array(64);
29 garbage.set([0x50, 0x4b, 0x03, 0x04]); // PK\x03\x04 (zip)
30 const reader = fromUint8Array(garbage);
31 await assertRejects(() => readSignatureHeader(reader), InvalidSignatureError);
32});
33
34Deno.test('readSignatureHeader: rejects truncated input', async () => {
35 const reader = fromUint8Array(new Uint8Array(10));
36 await assertRejects(() => readSignatureHeader(reader), TruncatedArchiveError);
37});
38
39Deno.test('readSignatureHeader: rejects bad StartHeader CRC', async () => {
40 const data = loadFixture('7z-hello-mhc-off-copy.7z');
41 const clone = new Uint8Array(data);
42 clone[12] ^= 0xff; // mangle first byte of the StartHeader payload
43 const reader = fromUint8Array(clone);
44 await assertRejects(() => readSignatureHeader(reader), ChecksumMismatchError);
45});
46
47Deno.test('readNextHeader: returns a plain Header for mhc-off-copy', async () => {
48 const reader = fromUint8Array(loadFixture('7z-hello-mhc-off-copy.7z'));
49 const start = await readSignatureHeader(reader);
50 const raw = await readNextHeader(reader, start);
51 assertEquals(raw.kind, 'header');
52});
53
54Deno.test('readContainer: mhc-off-copy parses to one file with one folder', async () => {
55 const reader = fromUint8Array(loadFixture('7z-hello-mhc-off-copy.7z'));
56 const container = await readContainer(reader);
57 assertEquals(container.header.files.length, 1);
58 assertEquals(container.header.files[0].name, 'Hello world.txt');
59 assertEquals(container.header.files[0].size, 14);
60 assertEquals(container.header.files[0].hasStream, true);
61 assertEquals(container.header.files[0].isDirectory, false);
62 assert(container.header.files[0].crc !== undefined);
63 assertEquals(container.header.folders.length, 1);
64 assertEquals(container.header.folders[0].coders.length, 1);
65 // Copy method has id 0x00, single zero byte
66 assertEquals(container.header.folders[0].coders[0].methodId.length, 1);
67 assertEquals(container.header.folders[0].coders[0].methodId[0], 0x00);
68 assertEquals(container.header.pack.packSizes, [14]);
69 assertEquals(container.packBase, 32);
70});
71
72Deno.test('readContainer: mhc-off-lzma2 parses to one file with one lzma2 coder', async () => {
73 const reader = fromUint8Array(loadFixture('7z-hello-mhc-off-lzma2.7z'));
74 const container = await readContainer(reader);
75 assertEquals(container.header.files.length, 1);
76 assertEquals(container.header.files[0].name, 'Hello world.txt');
77 assertEquals(container.header.files[0].size, 14);
78 assertEquals(container.header.folders.length, 1);
79 const coder = container.header.folders[0].coders[0];
80 assertEquals(coder.methodId.length, 1);
81 assertEquals(coder.methodId[0], 0x21); // LZMA2
82 // LZMA2 stores a 1-byte dictionary descriptor as its property
83 assertEquals(coder.properties.length, 1);
84});
85
86Deno.test('readContainer: libarchive copy.7z (plain header) parses', async () => {
87 const reader = fromUint8Array(loadLibarchive('test_read_format_7zip_copy.7z'));
88 const container = await readContainer(reader);
89 assertEquals(container.header.files.length, 1);
90 assertEquals(container.header.files[0].name, 'file1');
91 assertEquals(container.header.folders.length, 1);
92 // the copy fixture stores a single file with the Copy coder
93 assertEquals(container.header.folders[0].coders[0].methodId[0], 0x00);
94 assert(container.header.files[0].size > 0);
95});
96
97Deno.test('readNextHeader: bla.7z carries an EncodedHeader', async () => {
98 const reader = fromUint8Array(loadFixture('bla.7z'));
99 const start = await readSignatureHeader(reader);
100 const raw = await readNextHeader(reader, start);
101 assertEquals(raw.kind, 'encoded');
102});
103
104Deno.test('readContainer: bla.7z decodes its encoded header', async () => {
105 const reader = fromUint8Array(loadFixture('bla.7z'));
106 const container = await readContainer(reader);
107 assert(container.header.files.length > 0);
108 assertEquals(container.header.files[0].name, 'test1.xml');
109});
110
111Deno.test('readContainer: mhc-off-empty has one zero-byte file and no folders', async () => {
112 // commons-compress's "empty" mhc-off fixture records a file named "empty"
113 // with no data stream (and consequently no folders or pack data).
114 const reader = fromUint8Array(loadFixture('7z-empty-mhc-off.7z'));
115 const container = await readContainer(reader);
116 assertEquals(container.header.files.length, 1);
117 assertEquals(container.header.files[0].name, 'empty');
118 assertEquals(container.header.files[0].size, 0);
119 assertEquals(container.header.files[0].hasStream, false);
120 assertEquals(container.header.files[0].isDirectory, false);
121 assertEquals(container.header.folders.length, 0);
122});