Skip to content

Commit bc9f165

Browse files
authored
Merge pull request #26 from gms1/package-sqlite-zip
package sqlite as zip and extract at build time
2 parents 124acc6 + c0f7058 commit bc9f165

13 files changed

Lines changed: 204 additions & 321281 deletions

File tree

deps/extract.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#!/usr/bin/env node
2+
'use strict';
3+
4+
const fs = require('fs');
5+
const path = require('path');
6+
const zlib = require('zlib');
7+
8+
const LOCAL_FILE_HEADER_SIG = 0x04034b50;
9+
const COMPRESSION_STORE = 0;
10+
const COMPRESSION_DEFLATE = 8;
11+
12+
function extractZip(zipPath, destDir) {
13+
const data = fs.readFileSync(zipPath);
14+
let offset = 0;
15+
let entries = 0;
16+
17+
// Detect top-level directory prefix to strip (e.g., "sqlite-amalgamation-3530100/")
18+
let prefixToStrip = '';
19+
let firstEntry = true;
20+
21+
while (offset < data.length) {
22+
const sig = data.readUInt32LE(offset);
23+
if (sig !== LOCAL_FILE_HEADER_SIG) {
24+
break;
25+
}
26+
27+
const compressionMethod = data.readUInt16LE(offset + 8);
28+
const compressedSize = data.readUInt32LE(offset + 18);
29+
const fileNameLength = data.readUInt16LE(offset + 26);
30+
const extraFieldLength = data.readUInt16LE(offset + 28);
31+
32+
const fileName = data.toString('utf8', offset + 30, offset + 30 + fileNameLength);
33+
const dataOffset = offset + 30 + fileNameLength + extraFieldLength;
34+
const compressedData = data.subarray(dataOffset, dataOffset + compressedSize);
35+
36+
// Detect top-level directory prefix from first entry
37+
if (firstEntry && fileName.includes('/')) {
38+
prefixToStrip = fileName.substring(0, fileName.indexOf('/') + 1);
39+
firstEntry = false;
40+
}
41+
42+
// Strip prefix from filename
43+
let outFileName = fileName;
44+
if (prefixToStrip && outFileName.startsWith(prefixToStrip)) {
45+
outFileName = outFileName.substring(prefixToStrip.length);
46+
}
47+
48+
let fileData;
49+
if (compressionMethod === COMPRESSION_STORE) {
50+
fileData = compressedData;
51+
} else if (compressionMethod === COMPRESSION_DEFLATE) {
52+
fileData = zlib.inflateRawSync(compressedData);
53+
} else {
54+
throw new Error(`Unsupported compression method ${compressionMethod} for file ${fileName}`);
55+
}
56+
57+
if (outFileName && !outFileName.endsWith('/')) {
58+
const outPath = path.join(destDir, outFileName);
59+
const resolvedDest = path.resolve(destDir);
60+
const resolvedOut = path.resolve(outPath);
61+
if (!resolvedOut.startsWith(resolvedDest + path.sep)) {
62+
throw new Error(`Zip Slip detected: ${outFileName} resolves outside destination directory`);
63+
}
64+
const dir = path.dirname(outPath);
65+
if (!fs.existsSync(dir)) {
66+
fs.mkdirSync(dir, { recursive: true });
67+
}
68+
fs.writeFileSync(outPath, fileData);
69+
entries++;
70+
}
71+
72+
offset = dataOffset + compressedSize;
73+
}
74+
75+
if (entries === 0) {
76+
throw new Error('No files extracted — zip may be corrupted or empty');
77+
}
78+
79+
return entries;
80+
}
81+
82+
function main() {
83+
const archivePath = path.resolve(process.argv[2]);
84+
const destDir = path.resolve(process.argv[3]);
85+
86+
if (!archivePath || !destDir) {
87+
console.error('Usage: extract.js <archive_path> <dest_dir>');
88+
process.exit(1);
89+
}
90+
91+
// Resolve archive path relative to script directory if not absolute
92+
const zipFile = path.isAbsolute(archivePath)
93+
? archivePath
94+
: path.join(__dirname, archivePath);
95+
96+
// Compute extraction directory: destDir + archive basename without .zip
97+
const archiveName = path.basename(archivePath, '.zip');
98+
const extractDir = path.join(destDir, archiveName);
99+
const stampFile = path.join(extractDir, '.extract-stamp');
100+
101+
if (fs.existsSync(stampFile)) {
102+
console.log(`[extract] Already extracted: ${extractDir}`);
103+
process.exit(0);
104+
}
105+
106+
if (!fs.existsSync(zipFile)) {
107+
console.error(`[extract] ERROR: Zip file not found: ${zipFile}`);
108+
process.exit(1);
109+
}
110+
111+
if (fs.existsSync(extractDir)) {
112+
console.log(`[extract] Removing stale extraction directory: ${extractDir}`);
113+
fs.rmSync(extractDir, { recursive: true, force: true });
114+
}
115+
116+
console.log(`[extract] Extracting ${path.basename(zipFile)}...`);
117+
fs.mkdirSync(extractDir, { recursive: true });
118+
119+
try {
120+
const entries = extractZip(zipFile, extractDir);
121+
fs.writeFileSync(stampFile, `Extracted from ${path.basename(zipFile)}\n`);
122+
console.log(`[extract] Extraction complete: ${entries} file(s) extracted to ${extractDir}`);
123+
} catch (err) {
124+
console.error(`[extract] ERROR: ${err.message}`);
125+
fs.rmSync(extractDir, { recursive: true, force: true });
126+
process.exit(1);
127+
}
128+
}
129+
130+
main();
2.8 MB
Binary file not shown.

0 commit comments

Comments
 (0)