Skip to content

Commit 877282c

Browse files
committed
Do not rely on external zip tool in tests
1 parent 4dca810 commit 877282c

3 files changed

Lines changed: 99 additions & 22 deletions

File tree

src/extras/filesystem/readZip.mts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ export async function readZip(source: string): Promise<ZipDirectory> {
151151
continue;
152152
}
153153
const localHeader = await getFilePart(localHeaderOffset, 30);
154+
if (read32BE(localHeader, 0) !== 0x504b0304) {
155+
throw new ZipError(`invalid local header for ${fileName}`);
156+
}
154157
const localNameLength = read16LE(localHeader, 26);
155158
const localExraLength = read16LE(localHeader, 28);
156159
const localHeaderSize = 30 + localNameLength + localExraLength;

src/extras/filesystem/zipFileFinder.test.mts

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import { spawn } from 'node:child_process';
2-
import { rm } from 'node:fs/promises';
31
import { join, sep } from 'node:path';
42
import {
5-
makeFileStructure,
63
makeTestTempDir,
4+
writeTestZip,
75
type FilesDefinition,
86
} from '../../test-helpers/makeFileStructure.mts';
97
import { Negotiator } from '../request/Negotiator.mts';
@@ -23,24 +21,9 @@ describe('zipFileFinder', () => {
2321
options: FileFinderOptions = {},
2422
relativePath: string[] = [],
2523
) => {
26-
const dir = ctx.getTyped(TEST_DIR);
27-
await makeFileStructure(dir, { content: structure });
28-
await new Promise<void>((resolve, reject) => {
29-
const p = spawn('zip', ['-X', '-r', '-n', '.gz:.deflate', join('..', 'all.zip'), '.'], {
30-
cwd: join(dir, 'content'),
31-
stdio: ['ignore', 'ignore', 'inherit'],
32-
});
33-
p.once('error', reject);
34-
p.once('exit', (code, signal) => {
35-
if (code === 0) {
36-
resolve();
37-
} else {
38-
reject(code ?? signal);
39-
}
40-
});
41-
});
42-
await rm(join(dir, 'content'), { recursive: true });
43-
const root = await readZip(join(dir, 'all.zip'));
24+
const zipPath = join(ctx.getTyped(TEST_DIR), 'all.zip');
25+
await writeTestZip(zipPath, structure);
26+
const root = await readZip(zipPath);
4427
return zipFileFinder(root.find(relativePath) as ZipDirectory, options);
4528
};
4629

src/test-helpers/makeFileStructure.mts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { tmpdir } from 'node:os';
2-
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
2+
import { deflateRawSync } from 'node:zlib';
3+
import { constants, mkdir, mkdtemp, open, rm, writeFile } from 'node:fs/promises';
34
import { join } from 'node:path';
5+
import { Queue } from '../util/Queue.mts';
46
import 'lean-test';
57

68
export type FilesDefinition = { [k in string]: string | FilesDefinition };
@@ -37,3 +39,92 @@ export async function makeFileStructure(dir: string, structure: FilesDefinition)
3739
}
3840
}
3941
}
42+
43+
export async function writeTestZip(path: string, structure: FilesDefinition) {
44+
// this writes enough data for readZip to understand the contents
45+
// in particular, it does not write the modification time, permission flags, or crc32
46+
47+
const cdItems: {
48+
filename: Buffer;
49+
pos: number;
50+
compression: number;
51+
compressedSize: number;
52+
uncompressedSize: number;
53+
}[] = [];
54+
const handle = await open(path, constants.O_CREAT | constants.O_WRONLY);
55+
try {
56+
const stream = handle.createWriteStream();
57+
let pos = 0;
58+
59+
const queue = new Queue({ path: [] as string[], structure });
60+
for (const { path, structure } of queue) {
61+
for (const [name, content] of Object.entries(structure)) {
62+
const itemPath = [...path, name];
63+
let compressed = VOID;
64+
let uncompressedSize = 0;
65+
let compression = 0;
66+
let filename: Buffer;
67+
if (typeof content === 'string') {
68+
filename = Buffer.from(itemPath.join('/'), 'utf-8');
69+
const uncompressed = Buffer.from(content, 'utf-8');
70+
uncompressedSize = uncompressed.byteLength;
71+
if (uncompressedSize < 20) {
72+
compressed = uncompressed;
73+
} else {
74+
compressed = deflateRawSync(uncompressed);
75+
compression = 8;
76+
}
77+
} else {
78+
queue.push({ path: itemPath, structure: content });
79+
filename = Buffer.from(itemPath.join('/') + '/', 'utf-8');
80+
}
81+
cdItems.push({
82+
filename,
83+
pos,
84+
compression,
85+
compressedSize: compressed.byteLength,
86+
uncompressedSize,
87+
});
88+
const localFileHeader = Buffer.alloc(30);
89+
localFileHeader.writeUint32BE(0x504b0304, 0);
90+
localFileHeader.writeUint16LE(20, 4);
91+
localFileHeader[6] = 0x40;
92+
localFileHeader.writeUint16LE(compression, 8);
93+
localFileHeader.writeUint32LE(compressed.byteLength, 18);
94+
localFileHeader.writeUint32LE(uncompressedSize, 22);
95+
localFileHeader.writeUint16LE(filename.byteLength, 26);
96+
stream.write(localFileHeader);
97+
stream.write(filename);
98+
stream.write(compressed);
99+
pos += localFileHeader.byteLength + filename.byteLength + compressed.byteLength;
100+
}
101+
}
102+
103+
const cdPos = pos;
104+
for (const item of cdItems) {
105+
const cdFileHeader = Buffer.alloc(46);
106+
cdFileHeader.writeUint32BE(0x504b0102, 0);
107+
cdFileHeader.writeUint16LE(20, 6);
108+
cdFileHeader[8] = 0x40;
109+
cdFileHeader.writeUint16LE(item.compression, 10);
110+
cdFileHeader.writeUint32LE(item.compressedSize, 20);
111+
cdFileHeader.writeUint32LE(item.uncompressedSize, 24);
112+
cdFileHeader.writeUint16LE(item.filename.byteLength, 28);
113+
cdFileHeader.writeUint32LE(item.pos, 42);
114+
stream.write(cdFileHeader);
115+
stream.write(item.filename);
116+
pos += cdFileHeader.byteLength + item.filename.byteLength;
117+
}
118+
const eocd = Buffer.alloc(22);
119+
eocd.writeUint32BE(0x504b0506, 0);
120+
eocd.writeUint16LE(cdItems.length, 8);
121+
eocd.writeUint16LE(cdItems.length, 10);
122+
eocd.writeUint32LE(pos - cdPos, 12);
123+
eocd.writeUint32LE(cdPos, 16);
124+
stream.write(eocd);
125+
} finally {
126+
await handle.close();
127+
}
128+
}
129+
130+
const VOID = Buffer.alloc(0);

0 commit comments

Comments
 (0)