buninu-linux 0.7.3 → 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/build-uki.sh +21 -7
- package/check_build.js +217 -0
- package/fetch-alpine.sh +3 -0
- package/fetch-bun.sh +3 -0
- package/index.js +72 -8
- 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 +107 -0
- package/scripts/fetch.js +194 -0
- package/scripts/modules.js +158 -0
- package/scripts/pack-initramfs.sh +33 -0
- package/scripts/pack.js +184 -0
- package/scripts/uki.js +136 -0
- package/test/js-tools.test.js +213 -0
package/initramfs/bin/dd
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/bin/bun
|
|
2
|
+
// dd — copy blocks between files.
|
|
3
|
+
//
|
|
4
|
+
// dd [if=FILE] [of=FILE] [bs=N] [ibs=N] [obs=N] [count=N] [skip=N] [seek=N]
|
|
5
|
+
// [conv=notrunc,sparse,sync,fsync,fdatasync,noerror] [status=none|noxfer|progress]
|
|
6
|
+
//
|
|
7
|
+
// N takes c (1), w (2), b (512), K/KiB, M/MiB, G/GiB (powers of 1024) or
|
|
8
|
+
// kB, MB, GB (powers of 1000), and the AxB product form, as in coreutils.
|
|
9
|
+
// conv=sparse seeks over output blocks that are all zero instead of writing them.
|
|
10
|
+
|
|
11
|
+
import { closeSync, fstatSync, fsyncSync, ftruncateSync, openSync, readSync, writeSync } from "node:fs";
|
|
12
|
+
|
|
13
|
+
const fail = (message) => {
|
|
14
|
+
console.error(`dd: ${message}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const multipliers = { c: 1, w: 2, b: 512, k: 1024, K: 1024, KiB: 1024, kB: 1e3, KB: 1e3, M: 1024 ** 2, MiB: 1024 ** 2, MB: 1e6, G: 1024 ** 3, GiB: 1024 ** 3, GB: 1e9 };
|
|
19
|
+
const number = (name, text) => text.split("x").reduce((product, part) => {
|
|
20
|
+
const match = /^(\d+)([a-zA-Z]*)$/.exec(part);
|
|
21
|
+
if (!match || !(match[2] in { "": 1, ...multipliers })) fail(`invalid number: '${text}' for ${name}`);
|
|
22
|
+
return product * Number(match[1]) * (multipliers[match[2]] ?? 1);
|
|
23
|
+
}, 1);
|
|
24
|
+
|
|
25
|
+
const options = { if: null, of: null, ibs: 512, obs: 512, count: Infinity, skip: 0, seek: 0, conv: new Set(), status: "default" };
|
|
26
|
+
for (const argument of process.argv.slice(2)) {
|
|
27
|
+
if (argument === "-h" || argument === "--help") {
|
|
28
|
+
console.error("Usage: dd [if=FILE] [of=FILE] [bs=N] [count=N] [skip=N] [seek=N] [conv=notrunc,sparse,...] [status=none]");
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
const separator = argument.indexOf("=");
|
|
32
|
+
if (separator < 0) fail(`unrecognized operand '${argument}'`);
|
|
33
|
+
const key = argument.slice(0, separator);
|
|
34
|
+
const value = argument.slice(separator + 1);
|
|
35
|
+
if (key === "if" || key === "of") options[key] = value;
|
|
36
|
+
else if (key === "bs") options.ibs = options.obs = number(key, value);
|
|
37
|
+
else if (key === "ibs" || key === "obs" || key === "count" || key === "skip" || key === "seek") options[key] = number(key, value);
|
|
38
|
+
else if (key === "conv") for (const flag of value.split(",")) {
|
|
39
|
+
if (!["notrunc", "sparse", "sync", "fsync", "fdatasync", "noerror"].includes(flag)) fail(`invalid conversion: '${flag}'`);
|
|
40
|
+
options.conv.add(flag);
|
|
41
|
+
}
|
|
42
|
+
else if (key === "status") {
|
|
43
|
+
if (!["none", "noxfer", "progress"].includes(value)) fail(`invalid status level: '${value}'`);
|
|
44
|
+
options.status = value;
|
|
45
|
+
}
|
|
46
|
+
else if (key === "iflag" || key === "oflag") {}
|
|
47
|
+
else fail(`unrecognized operand '${argument}'`);
|
|
48
|
+
}
|
|
49
|
+
if (options.ibs < 1 || options.obs < 1) fail("block sizes must be positive");
|
|
50
|
+
|
|
51
|
+
const input = options.if === null ? 0 : (() => {
|
|
52
|
+
try { return openSync(options.if, "r"); } catch (error) { return fail(`failed to open '${options.if}': ${error.message}`); }
|
|
53
|
+
})();
|
|
54
|
+
const output = options.of === null ? 1 : (() => {
|
|
55
|
+
try { return openSync(options.of, options.conv.has("notrunc") ? (() => { try { openSync(options.of, "r"); return "r+"; } catch { return "w"; } })() : "a+"); }
|
|
56
|
+
catch (error) { return fail(`failed to open '${options.of}': ${error.message}`); }
|
|
57
|
+
})();
|
|
58
|
+
// "a+" would append; reopen read-write so writes land at explicit positions.
|
|
59
|
+
const outputFd = options.of !== null && !options.conv.has("notrunc") ? (closeSync(output), openSync(options.of, "r+")) : output;
|
|
60
|
+
const inputSeekable = fstatSync(input).isFile() || fstatSync(input).isBlockDevice();
|
|
61
|
+
const outputSeekable = fstatSync(outputFd).isFile() || fstatSync(outputFd).isBlockDevice();
|
|
62
|
+
|
|
63
|
+
let inputPosition = inputSeekable ? options.skip * options.ibs : null;
|
|
64
|
+
if (!inputSeekable && options.skip) {
|
|
65
|
+
// Pipes: read and drop skip blocks.
|
|
66
|
+
const discard = Buffer.alloc(options.ibs);
|
|
67
|
+
for (let block = 0; block < options.skip; block++) if (readSync(input, discard, 0, options.ibs, null) === 0) break;
|
|
68
|
+
}
|
|
69
|
+
let outputPosition = options.seek * options.obs;
|
|
70
|
+
// Without notrunc the output is cut at the seek point, as dd does.
|
|
71
|
+
if (options.of !== null && !options.conv.has("notrunc") && fstatSync(outputFd).isFile()) ftruncateSync(outputFd, outputPosition);
|
|
72
|
+
|
|
73
|
+
const stats = { fullIn: 0, partialIn: 0, fullOut: 0, partialOut: 0, bytes: 0 };
|
|
74
|
+
const started = performance.now();
|
|
75
|
+
const buffer = Buffer.alloc(options.ibs);
|
|
76
|
+
let pending = Buffer.alloc(0);
|
|
77
|
+
let skippedTail = false;
|
|
78
|
+
|
|
79
|
+
const writeBlock = (block) => {
|
|
80
|
+
if (options.conv.has("sparse") && outputSeekable && block.every((byte) => byte === 0)) {
|
|
81
|
+
outputPosition += block.length;
|
|
82
|
+
skippedTail = true;
|
|
83
|
+
} else {
|
|
84
|
+
for (let offset = 0; offset < block.length;) {
|
|
85
|
+
offset += writeSync(outputFd, block, offset, block.length - offset, outputSeekable ? outputPosition + offset : null);
|
|
86
|
+
}
|
|
87
|
+
outputPosition += block.length;
|
|
88
|
+
skippedTail = false;
|
|
89
|
+
}
|
|
90
|
+
if (block.length === options.obs) stats.fullOut++; else stats.partialOut++;
|
|
91
|
+
stats.bytes += block.length;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
for (let blocks = 0; blocks < options.count; blocks++) {
|
|
95
|
+
let count;
|
|
96
|
+
try {
|
|
97
|
+
count = readSync(input, buffer, 0, options.ibs, inputPosition);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (!options.conv.has("noerror")) fail(`error reading '${options.if ?? "standard input"}': ${error.message}`);
|
|
100
|
+
count = 0;
|
|
101
|
+
}
|
|
102
|
+
if (count === 0) break;
|
|
103
|
+
if (inputPosition !== null) inputPosition += count;
|
|
104
|
+
if (count === options.ibs) stats.fullIn++; else stats.partialIn++;
|
|
105
|
+
let block = buffer.subarray(0, count);
|
|
106
|
+
if (options.conv.has("sync") && count < options.ibs) block = Buffer.concat([block, Buffer.alloc(options.ibs - count)]);
|
|
107
|
+
pending = Buffer.concat([pending, block]);
|
|
108
|
+
while (pending.length >= options.obs) {
|
|
109
|
+
writeBlock(pending.subarray(0, options.obs));
|
|
110
|
+
pending = pending.subarray(options.obs);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (pending.length) writeBlock(Buffer.from(pending));
|
|
114
|
+
// A sparse run at the end still has to reach the file's length.
|
|
115
|
+
if (skippedTail && fstatSync(outputFd).size < outputPosition) ftruncateSync(outputFd, outputPosition);
|
|
116
|
+
if (options.conv.has("fsync") || options.conv.has("fdatasync")) fsyncSync(outputFd);
|
|
117
|
+
|
|
118
|
+
if (options.status !== "none") {
|
|
119
|
+
console.error(`${stats.fullIn}+${stats.partialIn} records in\n${stats.fullOut}+${stats.partialOut} records out`);
|
|
120
|
+
if (options.status !== "noxfer") {
|
|
121
|
+
const seconds = (performance.now() - started) / 1000;
|
|
122
|
+
console.error(`${stats.bytes} bytes copied, ${seconds.toFixed(3)} s, ${(stats.bytes / Math.max(seconds, 1e-9) / 1e6).toFixed(1)} MB/s`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (input !== 0) closeSync(input);
|
|
126
|
+
if (outputFd !== 1) closeSync(outputFd);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/bin/bun
|
|
2
|
+
// mkfs.fat — make a FAT32 filesystem in an image file, laid out as
|
|
3
|
+
// mkfs.fat 4.2 does (see /lib/fat.js).
|
|
4
|
+
//
|
|
5
|
+
// mkfs.fat [-F 32] [-n LABEL] [-h HIDDEN] [-i VOLID] [-s SECTORS_PER_CLUSTER]
|
|
6
|
+
// [--invariant] [-C] FILE [BLOCKS]
|
|
7
|
+
//
|
|
8
|
+
// BLOCKS (1 KiB each) sizes the filesystem; -C creates FILE with that size.
|
|
9
|
+
// SOURCE_DATE_EPOCH, when set, is the volume ID and the label's time, as
|
|
10
|
+
// with mkfs.fat; --invariant fixes both to mkfs.fat's constants.
|
|
11
|
+
|
|
12
|
+
import { closeSync, fstatSync, ftruncateSync, openSync } from "node:fs";
|
|
13
|
+
import { FatError, format } from "../lib/fat.js";
|
|
14
|
+
|
|
15
|
+
const fail = (message) => {
|
|
16
|
+
console.error(`mkfs.fat: ${message}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const usage = () => {
|
|
21
|
+
console.error(`Usage: mkfs.fat [-F 32] [-n LABEL] [-h HIDDEN] [-i VOLID] [-s SECTORS_PER_CLUSTER] [--invariant] [-C] FILE [BLOCKS]`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const options = { label: "NO NAME", hidden: 0, volumeId: null, invariant: false, sectorsPerCluster: undefined, create: false, quiet: false };
|
|
26
|
+
const operands = [];
|
|
27
|
+
const argv = process.argv.slice(2);
|
|
28
|
+
const value = (index, option) => {
|
|
29
|
+
if (index + 1 >= argv.length) fail(`option ${option} requires an argument`);
|
|
30
|
+
return argv[index + 1];
|
|
31
|
+
};
|
|
32
|
+
for (let index = 0; index < argv.length; index++) {
|
|
33
|
+
const argument = argv[index];
|
|
34
|
+
if (argument === "--help") usage();
|
|
35
|
+
else if (argument === "--invariant") options.invariant = true;
|
|
36
|
+
else if (argument === "-F") { if (value(index++, "-F") !== "32") fail("only -F 32 is supported"); }
|
|
37
|
+
else if (argument === "-n") options.label = value(index++, "-n");
|
|
38
|
+
else if (argument === "-h") options.hidden = Number(value(index++, "-h"));
|
|
39
|
+
else if (argument === "-i") options.volumeId = Number.parseInt(value(index++, "-i"), 16) >>> 0;
|
|
40
|
+
else if (argument === "-s") options.sectorsPerCluster = Number(value(index++, "-s"));
|
|
41
|
+
else if (argument === "-C") options.create = true;
|
|
42
|
+
else if (argument === "-v" || argument === "-I" || argument === "-a") {}
|
|
43
|
+
else if (argument === "-q") options.quiet = true;
|
|
44
|
+
else if (argument.startsWith("-") && argument.length > 1) fail(`unknown option ${argument}`);
|
|
45
|
+
else operands.push(argument);
|
|
46
|
+
}
|
|
47
|
+
if (operands.length < 1 || operands.length > 2) usage();
|
|
48
|
+
const [file, blocks] = operands;
|
|
49
|
+
if (options.label.length > 11) fail("label can be no longer than 11 characters");
|
|
50
|
+
if (!Number.isInteger(options.hidden) || options.hidden < 0) fail(`bad number of hidden sectors: ${options.hidden}`);
|
|
51
|
+
if (options.sectorsPerCluster !== undefined && ![1, 2, 4, 8, 16, 32, 64, 128].includes(options.sectorsPerCluster)) fail("bad number of sectors per cluster");
|
|
52
|
+
|
|
53
|
+
let fd;
|
|
54
|
+
try {
|
|
55
|
+
fd = openSync(file, options.create ? "wx" : "r+");
|
|
56
|
+
} catch (error) {
|
|
57
|
+
fail(`unable to open ${file}: ${error.message}`);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
if (options.create) {
|
|
61
|
+
if (!blocks) fail("-C needs BLOCKS");
|
|
62
|
+
ftruncateSync(fd, Number(blocks) * 1024);
|
|
63
|
+
}
|
|
64
|
+
const bytes = blocks ? Number(blocks) * 1024 : fstatSync(fd).size;
|
|
65
|
+
if (!Number.isFinite(bytes) || bytes <= 0) fail(`bad size for ${file}`);
|
|
66
|
+
console.log("mkfs.fat (buninu-linux /bin/mkfs.fat)");
|
|
67
|
+
format(fd, { sectors: Math.floor(bytes / 512), label: options.label, volumeId: options.volumeId, hidden: options.hidden, invariant: options.invariant, sectorsPerCluster: options.sectorsPerCluster });
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error instanceof FatError) fail(error.message);
|
|
70
|
+
throw error;
|
|
71
|
+
} finally {
|
|
72
|
+
closeSync(fd);
|
|
73
|
+
}
|
|
@@ -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);
|