buninu-linux 0.7.4 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +46 -46
- package/README.md +23 -5
- package/fetch-alpine.sh +3 -0
- package/fetch-bun.sh +3 -0
- package/index.js +48 -5
- package/initramfs/bin/dd +126 -0
- package/initramfs/bin/gunzip +6 -0
- package/initramfs/bin/gzip +6 -0
- package/initramfs/bin/mcopy +6 -0
- package/initramfs/bin/mdir +6 -0
- package/initramfs/bin/mkfs.fat +73 -0
- package/initramfs/bin/mmd +6 -0
- package/initramfs/bin/parted +178 -0
- package/initramfs/bin/truncate +78 -0
- package/initramfs/bin/unzip +248 -0
- package/initramfs/bin/zcat +6 -0
- package/initramfs/lib/crc32.js +16 -0
- package/initramfs/lib/fat.js +534 -0
- package/initramfs/lib/gpt.js +144 -0
- package/initramfs/lib/gzip.js +115 -0
- package/initramfs/lib/mtools.js +158 -0
- package/package.json +1 -1
- package/scripts/arch.js +156 -0
- package/scripts/arch.sh +5 -5
- package/scripts/build.js +145 -0
- package/scripts/cpio.js +0 -0
- package/scripts/fetch.js +194 -0
- package/scripts/modules.js +158 -0
- package/scripts/pack-initramfs.sh +17 -0
- package/scripts/pack.js +184 -0
- package/scripts/uki.js +0 -0
- package/test/js-tools.test.js +213 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/bin/bun
|
|
2
|
+
// parted — GPT partitioning of a disk image, script mode only (see /lib/gpt.js).
|
|
3
|
+
//
|
|
4
|
+
// parted [-s] IMAGE COMMAND...
|
|
5
|
+
//
|
|
6
|
+
// Commands, any number in a row as with parted -s:
|
|
7
|
+
// mklabel gpt
|
|
8
|
+
// mkpart NAME [FS-TYPE] START END START/END with s, B, kB/MB/GB/TB,
|
|
9
|
+
// KiB/MiB/GiB/TiB, % or the current unit;
|
|
10
|
+
// a negative value counts from the end
|
|
11
|
+
// set NUMBER FLAG on|off esp, boot, msftdata, bios_grub,
|
|
12
|
+
// legacy_boot, hidden, no_automount
|
|
13
|
+
// name NUMBER NAME
|
|
14
|
+
// rm NUMBER
|
|
15
|
+
// unit UNIT s, B, kB, MB, GB, TB, KiB, MiB, GiB, TiB, %
|
|
16
|
+
// print
|
|
17
|
+
// START and END are used as given, without alignment; END is inclusive.
|
|
18
|
+
|
|
19
|
+
import { closeSync, fstatSync, openSync } from "node:fs";
|
|
20
|
+
import { GptError, SECTOR, TYPES, randomGuid, readGpt, usable, writeGpt } from "../lib/gpt.js";
|
|
21
|
+
|
|
22
|
+
const fail = (message) => {
|
|
23
|
+
console.error(`Error: ${message}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const usage = () => {
|
|
28
|
+
console.error(`Usage: parted [-s] IMAGE COMMAND...
|
|
29
|
+
mklabel gpt | mkpart NAME [FS-TYPE] START END | set NUMBER FLAG on|off
|
|
30
|
+
name NUMBER NAME | rm NUMBER | unit UNIT | print`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// -s is implied; -a takes a value and alignment is not applied anyway.
|
|
35
|
+
const raw = process.argv.slice(2);
|
|
36
|
+
const words = [];
|
|
37
|
+
for (let index = 0; index < raw.length; index++) {
|
|
38
|
+
const argument = raw[index];
|
|
39
|
+
if (argument === "-s" || argument === "--script") continue;
|
|
40
|
+
if (argument === "-a" || argument === "--align") { index++; continue; }
|
|
41
|
+
if (argument.startsWith("--align=")) continue;
|
|
42
|
+
if (argument === "-h" || argument === "--help") usage();
|
|
43
|
+
if (argument.startsWith("-") && words.length === 0) fail(`unknown option ${argument}`);
|
|
44
|
+
words.push(argument);
|
|
45
|
+
}
|
|
46
|
+
if (words.length < 2) usage();
|
|
47
|
+
const [image, ...commands] = words;
|
|
48
|
+
|
|
49
|
+
let fd;
|
|
50
|
+
try { fd = openSync(image, "r+"); } catch (error) { fail(`Could not stat device ${image} - ${error.message}.`); }
|
|
51
|
+
const stat = fstatSync(fd);
|
|
52
|
+
if (!stat.isFile()) fail(`${image}: only disk image files are supported`);
|
|
53
|
+
const sectors = Math.floor(stat.size / SECTOR);
|
|
54
|
+
if (sectors < 2 + 2 * 33 + 1) fail(`${image} is too small for a GPT`);
|
|
55
|
+
|
|
56
|
+
let table = readGpt(fd, sectors);
|
|
57
|
+
let dirty = false;
|
|
58
|
+
let unit = "MB";
|
|
59
|
+
|
|
60
|
+
const units = { s: SECTOR, B: 1, kB: 1e3, MB: 1e6, GB: 1e9, TB: 1e12, KiB: 1024, MiB: 1024 ** 2, GiB: 1024 ** 3, TiB: 1024 ** 4 };
|
|
61
|
+
const unitName = (text) => Object.keys(units).find((name) => name.toLowerCase() === text.toLowerCase()) ?? (text === "%" ? "%" : null);
|
|
62
|
+
|
|
63
|
+
/** A START or END position in sectors (END inclusive). */
|
|
64
|
+
const position = (text, isEnd) => {
|
|
65
|
+
const match = /^(-?)(\d+(?:\.\d+)?)([a-zA-Z%]*)$/.exec(text);
|
|
66
|
+
if (!match) fail(`invalid position ${text}`);
|
|
67
|
+
const name = match[3] ? unitName(match[3]) : unit;
|
|
68
|
+
if (!name) fail(`invalid unit in ${text}`);
|
|
69
|
+
const amount = Number(match[2]);
|
|
70
|
+
if (name === "%") return Math.round((sectors - 1) * amount / 100);
|
|
71
|
+
const count = name === "s" ? amount : Math.round(amount * units[name] / SECTOR);
|
|
72
|
+
// A negative position counts back from the end of the disk: -1s is the last sector.
|
|
73
|
+
if (match[1]) return Math.floor(sectors - count);
|
|
74
|
+
return Math.floor(count - (isEnd && name !== "s" ? 1 : 0));
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const requireTable = () => {
|
|
78
|
+
if (!table) fail(`${image}: unrecognised disk label`);
|
|
79
|
+
return table;
|
|
80
|
+
};
|
|
81
|
+
const partition = (text) => {
|
|
82
|
+
const number = Number(text);
|
|
83
|
+
const entry = requireTable().entries.find((candidate) => candidate.number === number);
|
|
84
|
+
if (!entry) fail(`Partition doesn't exist.`);
|
|
85
|
+
return entry;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const typeForFs = (fs) => {
|
|
89
|
+
if (!fs) return TYPES.linux;
|
|
90
|
+
if (/^(fat16|fat32|fat12|ntfs|exfat)$/i.test(fs)) return TYPES.msdata;
|
|
91
|
+
if (/^linux-swap/i.test(fs)) return TYPES.swap;
|
|
92
|
+
return TYPES.linux;
|
|
93
|
+
};
|
|
94
|
+
const knownFs = /^(fat12|fat16|fat32|ext2|ext3|ext4|btrfs|xfs|ntfs|exfat|linux-swap(\(v[01]\))?|hfs\+?|udf|zfs)$/i;
|
|
95
|
+
|
|
96
|
+
const print = () => {
|
|
97
|
+
const current = requireTable();
|
|
98
|
+
const show = (sector) => unit === "s" ? `${sector}s` : unit === "%" ? `${((sector / (sectors - 1)) * 100).toFixed(1)}%` : `${+(sector * SECTOR / units[unit]).toFixed(1)}${unit}`;
|
|
99
|
+
console.log(`Model: (file)\nDisk ${image}: ${show(sectors - 1 + (unit === "s" ? 1 : 0))}\nSector size (logical/physical): 512B/512B\nPartition Table: gpt\nDisk Flags: \n`);
|
|
100
|
+
console.log("Number Start End Size Name Flags");
|
|
101
|
+
for (const entry of current.entries) {
|
|
102
|
+
const flags = [];
|
|
103
|
+
if (entry.type === TYPES.esp) flags.push("boot", "esp");
|
|
104
|
+
if (entry.type === TYPES.bios_grub) flags.push("bios_grub");
|
|
105
|
+
if (entry.type === TYPES.msdata) flags.push("msftdata");
|
|
106
|
+
if (entry.attributes & 4n) flags.push("legacy_boot");
|
|
107
|
+
console.log(` ${entry.number} ${show(entry.first)} ${show(entry.last)} ${show(entry.last - entry.first + 1)} ${entry.name} ${flags.join(", ")}`);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
for (let index = 0; index < commands.length;) {
|
|
113
|
+
const command = commands[index++];
|
|
114
|
+
const take = (what) => {
|
|
115
|
+
if (index >= commands.length) fail(`${command}: expecting ${what}`);
|
|
116
|
+
return commands[index++];
|
|
117
|
+
};
|
|
118
|
+
if (command === "mklabel") {
|
|
119
|
+
const label = take("a disk label type");
|
|
120
|
+
if (label !== "gpt") fail(`only gpt labels are supported, not ${label}`);
|
|
121
|
+
table = { diskGuid: randomGuid(), entries: [] };
|
|
122
|
+
dirty = true;
|
|
123
|
+
} else if (command === "unit") {
|
|
124
|
+
const name = unitName(take("a unit"));
|
|
125
|
+
if (!name) fail(`invalid unit ${commands[index - 1]}`);
|
|
126
|
+
unit = name;
|
|
127
|
+
} else if (command === "mkpart") {
|
|
128
|
+
const current = requireTable();
|
|
129
|
+
const name = take("a partition name");
|
|
130
|
+
let fs = null;
|
|
131
|
+
if (knownFs.test(commands[index] ?? "")) fs = commands[index++];
|
|
132
|
+
const first = position(take("a start"), false);
|
|
133
|
+
const last = position(take("an end"), true);
|
|
134
|
+
const range = usable(sectors);
|
|
135
|
+
if (first < range.first || last > range.last || first > last) {
|
|
136
|
+
fail(`The location ${first}s-${last}s is outside of the usable range ${range.first}s-${range.last}s of the device.`);
|
|
137
|
+
}
|
|
138
|
+
if (current.entries.some((entry) => first <= entry.last && entry.first <= last)) fail("the partition overlaps another one");
|
|
139
|
+
let number = 1;
|
|
140
|
+
while (current.entries.some((entry) => entry.number === number)) number++;
|
|
141
|
+
current.entries.push({ number, type: typeForFs(fs), guid: randomGuid(), first, last, attributes: 0n, name, fs });
|
|
142
|
+
current.entries.sort((a, b) => a.number - b.number);
|
|
143
|
+
dirty = true;
|
|
144
|
+
} else if (command === "set") {
|
|
145
|
+
const entry = partition(take("a partition number"));
|
|
146
|
+
const flag = take("a flag").toLowerCase();
|
|
147
|
+
const state = take("on or off").toLowerCase();
|
|
148
|
+
if (state !== "on" && state !== "off") fail(`expecting on or off, not ${state}`);
|
|
149
|
+
const on = state === "on";
|
|
150
|
+
if (flag === "esp" || flag === "boot") entry.type = on ? TYPES.esp : typeForFs(entry.fs ?? "fat32");
|
|
151
|
+
else if (flag === "bios_grub") entry.type = on ? TYPES.bios_grub : typeForFs(entry.fs);
|
|
152
|
+
else if (flag === "msftdata") entry.type = on ? TYPES.msdata : TYPES.linux;
|
|
153
|
+
else {
|
|
154
|
+
const bits = { legacy_boot: 2n, hidden: 1n << 62n, no_automount: 1n << 63n }[flag];
|
|
155
|
+
if (bits === undefined) fail(`unsupported flag ${flag}`);
|
|
156
|
+
entry.attributes = on ? entry.attributes | bits : entry.attributes & ~bits;
|
|
157
|
+
}
|
|
158
|
+
dirty = true;
|
|
159
|
+
} else if (command === "name") {
|
|
160
|
+
const entry = partition(take("a partition number"));
|
|
161
|
+
entry.name = take("a name");
|
|
162
|
+
dirty = true;
|
|
163
|
+
} else if (command === "rm") {
|
|
164
|
+
const entry = partition(take("a partition number"));
|
|
165
|
+
table.entries = table.entries.filter((candidate) => candidate !== entry);
|
|
166
|
+
dirty = true;
|
|
167
|
+
} else if (command === "print") {
|
|
168
|
+
if (dirty) { writeGpt(fd, sectors, table); dirty = false; }
|
|
169
|
+
print();
|
|
170
|
+
} else fail(`unknown command ${command}`);
|
|
171
|
+
}
|
|
172
|
+
if (dirty) writeGpt(fd, sectors, table);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error instanceof GptError) fail(error.message);
|
|
175
|
+
throw error;
|
|
176
|
+
} finally {
|
|
177
|
+
closeSync(fd);
|
|
178
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/bin/bun
|
|
2
|
+
// truncate — set the size of files, creating them (sparse) when missing.
|
|
3
|
+
//
|
|
4
|
+
// truncate -s [+|-|<|>|/|%]SIZE [-c] FILE...
|
|
5
|
+
//
|
|
6
|
+
// SIZE takes K, M, G, T (powers of 1024; KiB ... also), KB, MB, GB, TB
|
|
7
|
+
// (powers of 1000) or nothing (bytes), as in coreutils.
|
|
8
|
+
|
|
9
|
+
import { closeSync, fstatSync, ftruncateSync, openSync } from "node:fs";
|
|
10
|
+
|
|
11
|
+
const fail = (message) => {
|
|
12
|
+
console.error(`truncate: ${message}`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const usage = () => {
|
|
17
|
+
console.error(`Usage: truncate -s [+|-|<|>|/|%]SIZE [-c] FILE...`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const units = { "": 1, K: 1024, M: 1024 ** 2, G: 1024 ** 3, T: 1024 ** 4, KB: 1e3, MB: 1e6, GB: 1e9, TB: 1e12 };
|
|
22
|
+
const parseSize = (text) => {
|
|
23
|
+
const match = /^([+\-<>/%]?)(\d+)([KMGT]?)(B|iB)?$/i.exec(text);
|
|
24
|
+
if (!match) fail(`invalid size: '${text}'`);
|
|
25
|
+
const unit = match[3].toUpperCase() + (match[4] === "B" ? "B" : "");
|
|
26
|
+
if (!(unit in units)) fail(`invalid size: '${text}'`);
|
|
27
|
+
return { relation: match[1], bytes: Number(match[2]) * units[unit] };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
let size = null;
|
|
31
|
+
let create = true;
|
|
32
|
+
const files = [];
|
|
33
|
+
const argv = process.argv.slice(2);
|
|
34
|
+
for (let index = 0; index < argv.length; index++) {
|
|
35
|
+
const argument = argv[index];
|
|
36
|
+
if (argument === "-h" || argument === "--help") usage();
|
|
37
|
+
else if (argument === "-s" || argument === "--size") {
|
|
38
|
+
if (index + 1 >= argv.length) fail("option requires an argument -- 's'");
|
|
39
|
+
size = parseSize(argv[++index]);
|
|
40
|
+
} else if (argument.startsWith("--size=")) size = parseSize(argument.slice(7));
|
|
41
|
+
else if (argument.startsWith("-s") && argument.length > 2) size = parseSize(argument.slice(2));
|
|
42
|
+
else if (argument === "-c" || argument === "--no-create") create = false;
|
|
43
|
+
else if (argument === "--") files.push(...argv.slice(index + 1)), index = argv.length;
|
|
44
|
+
else if (argument.startsWith("-") && argument !== "-") fail(`unknown option ${argument}`);
|
|
45
|
+
else files.push(argument);
|
|
46
|
+
}
|
|
47
|
+
if (size === null || files.length === 0) usage();
|
|
48
|
+
|
|
49
|
+
let status = 0;
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
let fd;
|
|
52
|
+
try {
|
|
53
|
+
fd = openSync(file, create ? "a+" : "r+");
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error.code === "ENOENT" && !create) continue;
|
|
56
|
+
console.error(`truncate: cannot open '${file}' for writing: ${error.message}`);
|
|
57
|
+
status = 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const current = fstatSync(fd).size;
|
|
62
|
+
const { relation, bytes } = size;
|
|
63
|
+
const target = relation === "+" ? current + bytes
|
|
64
|
+
: relation === "-" ? Math.max(0, current - bytes)
|
|
65
|
+
: relation === "<" ? Math.min(current, bytes)
|
|
66
|
+
: relation === ">" ? Math.max(current, bytes)
|
|
67
|
+
: relation === "/" ? (bytes ? Math.floor(current / bytes) * bytes : fail("division by zero"))
|
|
68
|
+
: relation === "%" ? (bytes ? Math.ceil(current / bytes) * bytes : fail("division by zero"))
|
|
69
|
+
: bytes;
|
|
70
|
+
ftruncateSync(fd, target);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(`truncate: failed to truncate '${file}': ${error.message}`);
|
|
73
|
+
status = 1;
|
|
74
|
+
} finally {
|
|
75
|
+
closeSync(fd);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
process.exit(status);
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/bin/bun
|
|
2
|
+
// unzip — list, test and extract zip archives (stored and deflated members,
|
|
3
|
+
// zip64 included) over node:zlib.
|
|
4
|
+
//
|
|
5
|
+
// Supported: -p (members to standard output), -l (list), -t (test),
|
|
6
|
+
// -d DIR, -o (overwrite), -n (never overwrite), -j (junk paths), -q/-qq;
|
|
7
|
+
// MEMBER operands may be wildcards. Unix modes and symlinks are restored.
|
|
8
|
+
// Runs under node as well as bun.
|
|
9
|
+
|
|
10
|
+
import { chmodSync, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readSync, rmSync, symlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
11
|
+
import { basename, dirname, join, normalize, resolve } from "node:path";
|
|
12
|
+
import { inflateRawSync } from "node:zlib";
|
|
13
|
+
import { crc32 } from "../lib/crc32.js";
|
|
14
|
+
|
|
15
|
+
const fail = (message, status = 1) => {
|
|
16
|
+
console.error(`unzip: ${message}`);
|
|
17
|
+
process.exit(status);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const usage = () => {
|
|
21
|
+
console.error(`Usage:
|
|
22
|
+
unzip [-o|-n] [-j] [-q] ZIP [MEMBER...] [-d DIR]
|
|
23
|
+
unzip -p ZIP [MEMBER...]
|
|
24
|
+
unzip -l ZIP [MEMBER...]
|
|
25
|
+
unzip -t ZIP`);
|
|
26
|
+
process.exit(10);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let mode = "extract";
|
|
30
|
+
let overwrite = null; // null: refuse, true: -o, false: -n
|
|
31
|
+
let junkPaths = false;
|
|
32
|
+
let quiet = 0;
|
|
33
|
+
let directory = ".";
|
|
34
|
+
const operands = [];
|
|
35
|
+
const argv = process.argv.slice(2);
|
|
36
|
+
for (let index = 0; index < argv.length; index++) {
|
|
37
|
+
const argument = argv[index];
|
|
38
|
+
if (argument === "-h" || argument === "--help") usage();
|
|
39
|
+
if (argument === "-d") {
|
|
40
|
+
if (index + 1 >= argv.length) fail("-d requires a directory", 10);
|
|
41
|
+
directory = argv[++index];
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (argument.startsWith("-d") && argument.length > 2) { directory = argument.slice(2); continue; }
|
|
45
|
+
if (argument.startsWith("-") && argument.length > 1) {
|
|
46
|
+
for (const letter of argument.slice(1)) {
|
|
47
|
+
if (letter === "p") mode = "pipe";
|
|
48
|
+
else if (letter === "l") mode = "list";
|
|
49
|
+
else if (letter === "t") mode = "test";
|
|
50
|
+
else if (letter === "o") overwrite = true;
|
|
51
|
+
else if (letter === "n") overwrite = false;
|
|
52
|
+
else if (letter === "j") junkPaths = true;
|
|
53
|
+
else if (letter === "q") quiet++;
|
|
54
|
+
else fail(`unknown option -${letter}`, 10);
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
operands.push(argument);
|
|
59
|
+
}
|
|
60
|
+
if (operands.length === 0) usage();
|
|
61
|
+
const [archive, ...patterns] = operands;
|
|
62
|
+
|
|
63
|
+
const fd = (() => {
|
|
64
|
+
try { return openSync(archive, "r"); } catch (error) { return fail(`cannot open ${archive}: ${error.message}`, 9); }
|
|
65
|
+
})();
|
|
66
|
+
const size = fstatSync(fd).size;
|
|
67
|
+
const read = (position, length) => {
|
|
68
|
+
const buffer = Buffer.alloc(length);
|
|
69
|
+
let done = 0;
|
|
70
|
+
while (done < length) {
|
|
71
|
+
const count = readSync(fd, buffer, done, length - done, position + done);
|
|
72
|
+
if (count === 0) fail(`${archive}: unexpected end of file`, 3);
|
|
73
|
+
done += count;
|
|
74
|
+
}
|
|
75
|
+
return buffer;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// End of central directory: in the last 64 KiB + 22 bytes, searched backwards.
|
|
79
|
+
const tailLength = Math.min(size, 65557);
|
|
80
|
+
const tail = read(size - tailLength, tailLength);
|
|
81
|
+
let eocd = -1;
|
|
82
|
+
for (let offset = tailLength - 22; offset >= 0; offset--) {
|
|
83
|
+
if (tail.readUInt32LE(offset) === 0x06054b50) { eocd = offset; break; }
|
|
84
|
+
}
|
|
85
|
+
if (eocd < 0) fail(`${archive}: not a zip archive (no end of central directory)`, 9);
|
|
86
|
+
let entryCount = tail.readUInt16LE(eocd + 10);
|
|
87
|
+
let directorySize = tail.readUInt32LE(eocd + 12);
|
|
88
|
+
let directoryOffset = tail.readUInt32LE(eocd + 16);
|
|
89
|
+
// zip64: the locator sits just before the end of central directory record.
|
|
90
|
+
if (eocd >= 20 && tail.readUInt32LE(eocd - 20) === 0x07064b50) {
|
|
91
|
+
const record = read(Number(tail.readBigUInt64LE(eocd - 20 + 8)), 56);
|
|
92
|
+
if (record.readUInt32LE(0) !== 0x06064b50) fail(`${archive}: bad zip64 end of central directory`, 3);
|
|
93
|
+
entryCount = Number(record.readBigUInt64LE(32));
|
|
94
|
+
directorySize = Number(record.readBigUInt64LE(40));
|
|
95
|
+
directoryOffset = Number(record.readBigUInt64LE(48));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const entries = [];
|
|
99
|
+
const central = read(directoryOffset, directorySize);
|
|
100
|
+
for (let offset = 0, index = 0; index < entryCount; index++) {
|
|
101
|
+
if (central.readUInt32LE(offset) !== 0x02014b50) fail(`${archive}: bad central directory entry ${index}`, 3);
|
|
102
|
+
const nameLength = central.readUInt16LE(offset + 28);
|
|
103
|
+
const extraLength = central.readUInt16LE(offset + 30);
|
|
104
|
+
const commentLength = central.readUInt16LE(offset + 32);
|
|
105
|
+
const flags = central.readUInt16LE(offset + 8);
|
|
106
|
+
const entry = {
|
|
107
|
+
madeBy: central.readUInt16LE(offset + 4) >> 8,
|
|
108
|
+
flags,
|
|
109
|
+
method: central.readUInt16LE(offset + 10),
|
|
110
|
+
time: central.readUInt16LE(offset + 12),
|
|
111
|
+
date: central.readUInt16LE(offset + 14),
|
|
112
|
+
crc: central.readUInt32LE(offset + 16),
|
|
113
|
+
compressedSize: central.readUInt32LE(offset + 20),
|
|
114
|
+
size: central.readUInt32LE(offset + 24),
|
|
115
|
+
external: central.readUInt32LE(offset + 38),
|
|
116
|
+
localOffset: central.readUInt32LE(offset + 42),
|
|
117
|
+
name: central.toString(flags & 0x800 ? "utf8" : "latin1", offset + 46, offset + 46 + nameLength),
|
|
118
|
+
};
|
|
119
|
+
// zip64 extra field: the 0xFFFFFFFF fields follow, in this order.
|
|
120
|
+
const extra = central.subarray(offset + 46 + nameLength, offset + 46 + nameLength + extraLength);
|
|
121
|
+
for (let position = 0; position + 4 <= extra.length;) {
|
|
122
|
+
const id = extra.readUInt16LE(position);
|
|
123
|
+
const length = extra.readUInt16LE(position + 2);
|
|
124
|
+
if (id === 0x0001) {
|
|
125
|
+
let field = position + 4;
|
|
126
|
+
for (const key of ["size", "compressedSize", "localOffset"]) {
|
|
127
|
+
if (entry[key] === 0xffffffff) { entry[key] = Number(extra.readBigUInt64LE(field)); field += 8; }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
position += 4 + length;
|
|
131
|
+
}
|
|
132
|
+
entries.push(entry);
|
|
133
|
+
offset += 46 + nameLength + extraLength + commentLength;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Bun.Glob when running under Bun; elsewhere the wildcards become a RegExp:
|
|
137
|
+
// * and ? match across "/" as in Info-ZIP, [...] is a set.
|
|
138
|
+
const globToRegExp = (pattern) => new RegExp(`^${pattern.replace(/\[[^\]]*\]|[*?]|[.+^${}()|\\]/g, (token) =>
|
|
139
|
+
token === "*" ? ".*" : token === "?" ? "." : token.startsWith("[") ? token.replace(/^\[!/, "[^") : `\\${token}`)}$`, "s");
|
|
140
|
+
const toMatcher = (pattern) => {
|
|
141
|
+
if (typeof Bun !== "undefined") {
|
|
142
|
+
const glob = new Bun.Glob(pattern);
|
|
143
|
+
return { test: (name) => glob.match(name) };
|
|
144
|
+
}
|
|
145
|
+
return globToRegExp(pattern);
|
|
146
|
+
};
|
|
147
|
+
const globs = patterns.map(toMatcher);
|
|
148
|
+
const selected = globs.length ? entries.filter((entry) => globs.some((glob) => glob.test(entry.name))) : entries;
|
|
149
|
+
for (const [index, pattern] of patterns.entries()) {
|
|
150
|
+
if (!entries.some((entry) => globs[index].test(entry.name))) {
|
|
151
|
+
console.error(`caution: filename not matched: ${pattern}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const dosDate = (entry) => {
|
|
156
|
+
const year = (entry.date >> 9) + 1980;
|
|
157
|
+
const month = (entry.date >> 5) & 15;
|
|
158
|
+
const day = entry.date & 31;
|
|
159
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
160
|
+
return `${year}-${pad(month)}-${pad(day)} ${pad(entry.time >> 11)}:${pad((entry.time >> 5) & 63)}`;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const data = (entry) => {
|
|
164
|
+
if (entry.flags & 1) throw new Error("encrypted members are not supported");
|
|
165
|
+
const local = read(entry.localOffset, 30);
|
|
166
|
+
if (local.readUInt32LE(0) !== 0x04034b50) throw new Error("bad local header");
|
|
167
|
+
const start = entry.localOffset + 30 + local.readUInt16LE(26) + local.readUInt16LE(28);
|
|
168
|
+
const raw = read(start, entry.compressedSize);
|
|
169
|
+
let bytes;
|
|
170
|
+
if (entry.method === 0) bytes = raw;
|
|
171
|
+
else if (entry.method === 8) bytes = inflateRawSync(raw);
|
|
172
|
+
else throw new Error(`compression method ${entry.method} is not supported`);
|
|
173
|
+
if (bytes.length !== entry.size) throw new Error(`size mismatch (${bytes.length} != ${entry.size})`);
|
|
174
|
+
if (crc32(bytes) !== entry.crc) throw new Error("CRC error");
|
|
175
|
+
return bytes;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const writeAll = (target, bytes) => {
|
|
179
|
+
for (let offset = 0; offset < bytes.length;) offset += writeSync(target, bytes, offset, bytes.length - offset);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
let status = 0;
|
|
183
|
+
if (mode === "list") {
|
|
184
|
+
let total = 0;
|
|
185
|
+
console.log(" Length Date Time Name\n--------- ---------- ----- ----");
|
|
186
|
+
for (const entry of selected) {
|
|
187
|
+
total += entry.size;
|
|
188
|
+
console.log(`${String(entry.size).padStart(9)} ${dosDate(entry)} ${entry.name}`);
|
|
189
|
+
}
|
|
190
|
+
console.log(`--------- -------\n${String(total).padStart(9)} ${selected.length} file${selected.length === 1 ? "" : "s"}`);
|
|
191
|
+
} else if (mode === "pipe") {
|
|
192
|
+
for (const entry of selected) {
|
|
193
|
+
if (entry.name.endsWith("/")) continue;
|
|
194
|
+
try { writeAll(1, data(entry)); } catch (error) { console.error(`unzip: ${entry.name}: ${error.message}`); status = 2; }
|
|
195
|
+
}
|
|
196
|
+
} else if (mode === "test") {
|
|
197
|
+
for (const entry of selected) {
|
|
198
|
+
if (entry.name.endsWith("/")) continue;
|
|
199
|
+
try { data(entry); if (!quiet) console.log(` testing: ${entry.name.padEnd(24)} OK`); }
|
|
200
|
+
catch (error) { console.log(` testing: ${entry.name.padEnd(24)} ${error.message}`); status = 2; }
|
|
201
|
+
}
|
|
202
|
+
if (status === 0 && quiet < 2) console.log(`No errors detected in compressed data of ${archive}.`);
|
|
203
|
+
} else {
|
|
204
|
+
if (!quiet) console.log(`Archive: ${archive}`);
|
|
205
|
+
const root = resolve(directory);
|
|
206
|
+
mkdirSync(root, { recursive: true });
|
|
207
|
+
for (const entry of selected) {
|
|
208
|
+
const name = junkPaths ? basename(entry.name) : entry.name;
|
|
209
|
+
if (!name || (junkPaths && entry.name.endsWith("/"))) continue;
|
|
210
|
+
// No absolute paths or "..": a member never lands outside DIR.
|
|
211
|
+
const relative = normalize(name.replace(/^\/+/, ""));
|
|
212
|
+
if (relative.startsWith("..") || relative.split("/").includes("..")) {
|
|
213
|
+
console.error(`unzip: skipping ${entry.name}: path outside the destination`);
|
|
214
|
+
status = 1;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const target = join(root, relative);
|
|
218
|
+
const unixMode = entry.madeBy === 3 ? entry.external >>> 16 : 0;
|
|
219
|
+
try {
|
|
220
|
+
if (entry.name.endsWith("/")) {
|
|
221
|
+
mkdirSync(target, { recursive: true });
|
|
222
|
+
if (!quiet) console.log(` creating: ${join(directory, relative)}/`);
|
|
223
|
+
if (unixMode & 0o7777) chmodSync(target, unixMode & 0o7777);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const exists = (() => { try { lstatSync(target); return true; } catch { return false; } })();
|
|
227
|
+
if (exists) {
|
|
228
|
+
if (overwrite === false) continue;
|
|
229
|
+
if (overwrite === null) { console.error(`unzip: ${join(directory, relative)} exists; use -o to overwrite or -n to skip`); status = 1; continue; }
|
|
230
|
+
rmSync(target, { force: true });
|
|
231
|
+
}
|
|
232
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
233
|
+
const bytes = data(entry);
|
|
234
|
+
if ((unixMode & 0o170000) === 0o120000) {
|
|
235
|
+
symlinkSync(bytes.toString(), target);
|
|
236
|
+
} else {
|
|
237
|
+
writeFileSync(target, bytes);
|
|
238
|
+
if (unixMode & 0o7777) chmodSync(target, unixMode & 0o7777);
|
|
239
|
+
}
|
|
240
|
+
if (!quiet) console.log(` inflating: ${join(directory, relative)}`);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.error(`unzip: ${entry.name}: ${error.message}`);
|
|
243
|
+
status = 2;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
closeSync(fd);
|
|
248
|
+
process.exit(status);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// /lib/crc32.js — the CRC-32 of zip, gzip and GPT (IEEE 802.3, reflected,
|
|
2
|
+
// polynomial 0xEDB88320).
|
|
3
|
+
|
|
4
|
+
const table = new Uint32Array(256);
|
|
5
|
+
for (let index = 0; index < 256; index++) {
|
|
6
|
+
let value = index;
|
|
7
|
+
for (let bit = 0; bit < 8; bit++) value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
8
|
+
table[index] = value >>> 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** CRC-32 of `bytes`, continuing from `previous` (the CRC of what came before). */
|
|
12
|
+
export const crc32 = (bytes, previous = 0) => {
|
|
13
|
+
let crc = ~previous >>> 0;
|
|
14
|
+
for (let index = 0; index < bytes.length; index++) crc = table[(crc ^ bytes[index]) & 0xff] ^ (crc >>> 8);
|
|
15
|
+
return ~crc >>> 0;
|
|
16
|
+
};
|