streaming 7-Zip archive extractor
1import { assertRejects } from '@std/assert';
2
3import {
4 ChecksumMismatchError,
5 InvalidPasswordError,
6 InvalidSignatureError,
7 MissingPasswordError,
8 TruncatedArchiveError,
9 UnsupportedFeatureError,
10} from '../lib/errors.ts';
11import { unsevenzip } from '../lib/extract.ts';
12import { fromUint8Array } from '../lib/reader/common.ts';
13
14const COMMONS_COMPRESS = new URL('./fixtures/commons-compress/', import.meta.url);
15const LIBARCHIVE = new URL('./fixtures/libarchive/', import.meta.url);
16const PY7ZR = new URL('./fixtures/py7zr/', import.meta.url);
17
18const load = (base: URL, name: string): Uint8Array<ArrayBuffer> => Deno.readFileSync(new URL(name, base));
19
20const drain = async (reader: ReturnType<typeof fromUint8Array>, password?: string): Promise<void> => {
21 for await (const entry of unsevenzip(reader, password !== undefined ? { password } : {})) {
22 await entry.bytes();
23 }
24};
25
26// #region unsupported codecs
27
28Deno.test('unsevenzip: bzip2 throws UnsupportedFeatureError at decode time', async () => {
29 const reader = fromUint8Array(load(LIBARCHIVE, 'test_read_format_7zip_bzip2.7z'));
30 await assertRejects(() => drain(reader), UnsupportedFeatureError);
31});
32
33Deno.test('unsevenzip: bcj2+bzip2 throws UnsupportedFeatureError at decode time', async () => {
34 const reader = fromUint8Array(load(LIBARCHIVE, 'test_read_format_7zip_bcj2_bzip2.7z'));
35 await assertRejects(() => drain(reader), UnsupportedFeatureError);
36});
37
38Deno.test('unsevenzip: lz4 throws UnsupportedFeatureError at decode time', async () => {
39 const reader = fromUint8Array(load(PY7ZR, 'lz4.7z'));
40 await assertRejects(() => drain(reader), UnsupportedFeatureError);
41});
42
43Deno.test('unsevenzip: zstd+brotli throws UnsupportedFeatureError at decode time', async () => {
44 const reader = fromUint8Array(load(PY7ZR, 'zstdmt-brotli.7z'));
45 await assertRejects(() => drain(reader), UnsupportedFeatureError);
46});
47
48// #endregion
49
50// #region missing password
51
52Deno.test('unsevenzip: encrypted entry without password throws MissingPasswordError', async () => {
53 const reader = fromUint8Array(load(LIBARCHIVE, 'test_read_format_7zip_encryption.7z'));
54 await assertRejects(() => drain(reader), MissingPasswordError);
55});
56
57Deno.test('unsevenzip: encrypted header without password throws MissingPasswordError', async () => {
58 // bla.encrypted has an encrypted EncodedHeader — failure surfaces during
59 // container parsing, before any entry is yielded.
60 const reader = fromUint8Array(load(COMMONS_COMPRESS, 'bla.encrypted.7z'));
61 await assertRejects(() => drain(reader), MissingPasswordError);
62});
63
64Deno.test('unsevenzip: filename_encryption without password throws MissingPasswordError', async () => {
65 const reader = fromUint8Array(load(PY7ZR, 'filename_encryption.7z'));
66 await assertRejects(() => drain(reader), MissingPasswordError);
67});
68
69// #endregion
70
71// #region wrong password
72
73Deno.test('unsevenzip: wrong password on encrypted entry throws InvalidPasswordError', async () => {
74 const reader = fromUint8Array(load(LIBARCHIVE, 'test_read_format_7zip_encryption.7z'));
75 await assertRejects(() => drain(reader, 'wrong'), InvalidPasswordError);
76});
77
78Deno.test('unsevenzip: wrong password on encrypted header throws InvalidPasswordError', async () => {
79 const reader = fromUint8Array(load(LIBARCHIVE, 'test_read_format_7zip_encryption_header.7z'));
80 await assertRejects(() => drain(reader, 'wrong'), InvalidPasswordError);
81});
82
83Deno.test('unsevenzip: wrong password on partially-encrypted archive throws InvalidPasswordError', async () => {
84 const reader = fromUint8Array(load(LIBARCHIVE, 'test_read_format_7zip_encryption_partially.7z'));
85 await assertRejects(() => drain(reader, 'wrong'), InvalidPasswordError);
86});
87
88Deno.test('unsevenzip: wrong password on bla.encrypted throws InvalidPasswordError', async () => {
89 const reader = fromUint8Array(load(COMMONS_COMPRESS, 'bla.encrypted.7z'));
90 await assertRejects(() => drain(reader, 'wrong'), InvalidPasswordError);
91});
92
93Deno.test('unsevenzip: wrong password on filename_encryption throws InvalidPasswordError', async () => {
94 const reader = fromUint8Array(load(PY7ZR, 'filename_encryption.7z'));
95 await assertRejects(() => drain(reader, 'wrong'), InvalidPasswordError);
96});
97
98// #endregion
99
100// #region malformed archives
101
102Deno.test('unsevenzip: libarchive malformed.7z truncated NextHeader', async () => {
103 const reader = fromUint8Array(load(LIBARCHIVE, 'test_read_format_7zip_malformed.7z'));
104 await assertRejects(() => drain(reader), TruncatedArchiveError);
105});
106
107Deno.test('unsevenzip: libarchive malformed2.7z truncated NextHeader', async () => {
108 const reader = fromUint8Array(load(LIBARCHIVE, 'test_read_format_7zip_malformed2.7z'));
109 await assertRejects(() => drain(reader), TruncatedArchiveError);
110});
111
112Deno.test('unsevenzip: libarchive malformed3.7z rejected as non-7z input', async () => {
113 const reader = fromUint8Array(load(LIBARCHIVE, 'test_read_format_7zip_malformed3.7z'));
114 await assertRejects(() => drain(reader), InvalidSignatureError);
115});
116
117Deno.test('unsevenzip: COMPRESS-542-endheadercorrupted fails StartHeader CRC', async () => {
118 const reader = fromUint8Array(load(COMMONS_COMPRESS, 'COMPRESS-542-endheadercorrupted.7z'));
119 await assertRejects(() => drain(reader), ChecksumMismatchError);
120});
121
122Deno.test('unsevenzip: COMPRESS-542-endheadercorrupted2 fails StartHeader CRC', async () => {
123 const reader = fromUint8Array(load(COMMONS_COMPRESS, 'COMPRESS-542-endheadercorrupted2.7z'));
124 await assertRejects(() => drain(reader), ChecksumMismatchError);
125});
126
127Deno.test('unsevenzip: bla.noendheaderoffset fails StartHeader CRC', async () => {
128 const reader = fromUint8Array(load(COMMONS_COMPRESS, 'bla.noendheaderoffset.7z'));
129 await assertRejects(() => drain(reader), ChecksumMismatchError);
130});
131
132// #endregion
133
134// #region CRC mismatch on stored content
135
136Deno.test('unsevenzip: mangled pack byte triggers ChecksumMismatchError on extraction', async () => {
137 // 7z-hello-mhc-off-copy.7z stores raw "Hello, world!\n" at offset 32 with
138 // an entry CRC. flipping one byte changes the computed CRC over the
139 // reconstructed plaintext, so one of the CRC checks (folder- or entry-
140 // level) must fire.
141 const data = load(COMMONS_COMPRESS, '7z-hello-mhc-off-copy.7z');
142 const mangled = new Uint8Array(data);
143 mangled[32] ^= 0xff;
144 const reader = fromUint8Array(mangled);
145 await assertRejects(() => drain(reader), ChecksumMismatchError);
146});
147
148// #endregion