buninu-linux 0.2.0 → 0.3.0
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 +22 -2
- package/README.md +399 -196
- package/build-uki.sh +4 -2
- package/fetch-alpine.sh +73 -43
- package/index.js +4 -1
- package/initramfs/bin/ip +629 -0
- package/initramfs/bin/mount +400 -0
- package/initramfs/bin/poweroff +31 -0
- package/initramfs/bin/umount +70 -0
- package/initramfs/init.js +164 -11
- package/initramfs/lib/dlopen.js +130 -0
- package/initramfs/lib/modprobe.js +235 -0
- package/initramfs/usr/share/doc/buninu-linux/ip.md +155 -0
- package/initramfs/usr/share/doc/buninu-linux/mount.md +155 -0
- package/initramfs/usr/share/doc/buninu-linux/poweroff.md +15 -0
- package/initramfs/usr/share/doc/buninu-linux/umount.md +48 -0
- package/package.json +2 -2
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
#!/bin/bun
|
|
2
|
+
// mount — mount a filesystem, or list what is mounted. `mount --help` renders
|
|
3
|
+
// /usr/share/doc/buninu-linux/mount.md.
|
|
4
|
+
//
|
|
5
|
+
// A Bun script over the mount(2) binding in /lib/dlopen.js. Filesystem
|
|
6
|
+
// drivers and block-device drivers are kernel modules on the Alpine kernels
|
|
7
|
+
// this distro boots, so the command loads them through /lib/modprobe.js:
|
|
8
|
+
// ext4 (with jbd2/mbcache/crc16), vfat/exfat/ntfs3 with their NLS tables,
|
|
9
|
+
// and common virtio / SATA / NVMe / USB storage drivers when the device node
|
|
10
|
+
// is not there yet.
|
|
11
|
+
|
|
12
|
+
import { existsSync, mkdirSync, openSync, readSync, closeSync, readdirSync, readFileSync } from "node:fs";
|
|
13
|
+
import { libc, cString, ptr, errno, strerror, Errno, SysError, showDocument } from "../lib/dlopen.js";
|
|
14
|
+
import { modprobe, tryModprobe, modulesDirectory, ModuleNotFound } from "../lib/modprobe.js";
|
|
15
|
+
|
|
16
|
+
const fail = (message, status = 32) => {
|
|
17
|
+
console.error(`mount: ${message}`);
|
|
18
|
+
process.exit(status);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------- options --
|
|
22
|
+
|
|
23
|
+
// <linux/mount.h> flags, keyed by the -o word that sets or clears them.
|
|
24
|
+
const MS = {
|
|
25
|
+
RDONLY: 1n, NOSUID: 2n, NODEV: 4n, NOEXEC: 8n, SYNCHRONOUS: 16n, REMOUNT: 32n,
|
|
26
|
+
MANDLOCK: 64n, DIRSYNC: 128n, NOSYMFOLLOW: 256n, NOATIME: 1024n, NODIRATIME: 2048n,
|
|
27
|
+
BIND: 4096n, MOVE: 8192n, REC: 16384n, SILENT: 32768n, UNBINDABLE: 1n << 17n,
|
|
28
|
+
PRIVATE: 1n << 18n, SLAVE: 1n << 19n, SHARED: 1n << 20n, RELATIME: 1n << 21n,
|
|
29
|
+
STRICTATIME: 1n << 24n, LAZYTIME: 1n << 25n,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const flagWords = {
|
|
33
|
+
ro: [MS.RDONLY, true], rw: [MS.RDONLY, false],
|
|
34
|
+
nosuid: [MS.NOSUID, true], suid: [MS.NOSUID, false],
|
|
35
|
+
nodev: [MS.NODEV, true], dev: [MS.NODEV, false],
|
|
36
|
+
noexec: [MS.NOEXEC, true], exec: [MS.NOEXEC, false],
|
|
37
|
+
sync: [MS.SYNCHRONOUS, true], async: [MS.SYNCHRONOUS, false],
|
|
38
|
+
dirsync: [MS.DIRSYNC, true],
|
|
39
|
+
mand: [MS.MANDLOCK, true], nomand: [MS.MANDLOCK, false],
|
|
40
|
+
noatime: [MS.NOATIME, true], atime: [MS.NOATIME, false],
|
|
41
|
+
nodiratime: [MS.NODIRATIME, true], diratime: [MS.NODIRATIME, false],
|
|
42
|
+
relatime: [MS.RELATIME, true], norelatime: [MS.RELATIME, false],
|
|
43
|
+
strictatime: [MS.STRICTATIME, true], nostrictatime: [MS.STRICTATIME, false],
|
|
44
|
+
lazytime: [MS.LAZYTIME, true], nolazytime: [MS.LAZYTIME, false],
|
|
45
|
+
nosymfollow: [MS.NOSYMFOLLOW, true], symfollow: [MS.NOSYMFOLLOW, false],
|
|
46
|
+
silent: [MS.SILENT, true], loud: [MS.SILENT, false],
|
|
47
|
+
remount: [MS.REMOUNT, true],
|
|
48
|
+
bind: [MS.BIND, true], rbind: [MS.BIND | MS.REC, true],
|
|
49
|
+
move: [MS.MOVE, true],
|
|
50
|
+
shared: [MS.SHARED, true], rshared: [MS.SHARED | MS.REC, true],
|
|
51
|
+
private: [MS.PRIVATE, true], rprivate: [MS.PRIVATE | MS.REC, true],
|
|
52
|
+
slave: [MS.SLAVE, true], rslave: [MS.SLAVE | MS.REC, true],
|
|
53
|
+
unbindable: [MS.UNBINDABLE, true], runbindable: [MS.UNBINDABLE | MS.REC, true],
|
|
54
|
+
defaults: [0n, true], auto: [0n, true], noauto: [0n, true], user: [0n, true],
|
|
55
|
+
nouser: [0n, true], nofail: [0n, true], _netdev: [0n, true],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Splits "-o ro,noatime,size=64M" into mount(2) flags plus the data string the
|
|
59
|
+
// filesystem driver parses itself.
|
|
60
|
+
const parseOptions = (list, state) => {
|
|
61
|
+
for (const word of list.split(",")) {
|
|
62
|
+
if (!word) continue;
|
|
63
|
+
const key = word.split("=")[0];
|
|
64
|
+
if (word in flagWords) {
|
|
65
|
+
const [bit, set] = flagWords[word];
|
|
66
|
+
state.flags = set ? state.flags | bit : state.flags & ~bit;
|
|
67
|
+
} else if (key === "loop") {
|
|
68
|
+
fail("loop devices are not supported yet");
|
|
69
|
+
} else {
|
|
70
|
+
state.data.push(word);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const usage = () => {
|
|
76
|
+
console.error(`Usage:
|
|
77
|
+
mount [-l]
|
|
78
|
+
mount [-fv] [-t TYPE] [-o OPTIONS] [-r|-w] [--mkdir] SOURCE TARGET
|
|
79
|
+
mount -L LABEL|-U UUID [-t TYPE] [-o OPTIONS] TARGET
|
|
80
|
+
mount --bind|--rbind|--move OLD NEW
|
|
81
|
+
mount -o remount[,ro|rw] TARGET
|
|
82
|
+
mount -h | --help full manual (mount.md)`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const parseArguments = (argv) => {
|
|
87
|
+
const state = { flags: 0n, data: [], positional: [], type: null, mkdir: false, verbose: false, fake: false };
|
|
88
|
+
const takeValue = (i, flag) => {
|
|
89
|
+
if (i + 1 >= argv.length) fail(`option ${flag} needs an argument`, 1);
|
|
90
|
+
return argv[i + 1];
|
|
91
|
+
};
|
|
92
|
+
for (let i = 0; i < argv.length; i++) {
|
|
93
|
+
const argument = argv[i];
|
|
94
|
+
if (argument === "-h" || argument === "--help") state.help = true;
|
|
95
|
+
else if (argument === "-t" || argument === "--types") state.type = argv[++i] ?? usage();
|
|
96
|
+
else if (argument.startsWith("--types=")) state.type = argument.slice(8);
|
|
97
|
+
else if (argument.startsWith("-t") && argument.length > 2) state.type = argument.slice(2);
|
|
98
|
+
else if (argument === "-o" || argument === "--options") parseOptions(takeValue(i++, "-o"), state);
|
|
99
|
+
else if (argument.startsWith("--options=")) parseOptions(argument.slice(10), state);
|
|
100
|
+
else if (argument.startsWith("-o") && argument.length > 2) parseOptions(argument.slice(2), state);
|
|
101
|
+
else if (argument === "-L" || argument === "--label") state.source = `LABEL=${takeValue(i++, "-L")}`;
|
|
102
|
+
else if (argument === "-U" || argument === "--uuid") state.source = `UUID=${takeValue(i++, "-U")}`;
|
|
103
|
+
else if (argument === "-r" || argument === "--read-only") state.flags |= MS.RDONLY;
|
|
104
|
+
else if (argument === "-w" || argument === "--rw" || argument === "--read-write") state.flags &= ~MS.RDONLY;
|
|
105
|
+
else if (argument === "-B" || argument === "--bind") state.flags |= MS.BIND;
|
|
106
|
+
else if (argument === "-R" || argument === "--rbind") state.flags |= MS.BIND | MS.REC;
|
|
107
|
+
else if (argument === "-M" || argument === "--move") state.flags |= MS.MOVE;
|
|
108
|
+
else if (argument === "--make-shared") state.flags |= MS.SHARED;
|
|
109
|
+
else if (argument === "--make-private") state.flags |= MS.PRIVATE;
|
|
110
|
+
else if (argument === "--make-rshared") state.flags |= MS.SHARED | MS.REC;
|
|
111
|
+
else if (argument === "--make-rprivate") state.flags |= MS.PRIVATE | MS.REC;
|
|
112
|
+
else if (argument === "--mkdir") state.mkdir = true;
|
|
113
|
+
else if (/^-[fv]{2,}$/.test(argument)) {
|
|
114
|
+
if (argument.includes("f")) state.fake = true;
|
|
115
|
+
if (argument.includes("v")) state.verbose = true;
|
|
116
|
+
}
|
|
117
|
+
else if (argument === "-v" || argument === "--verbose") state.verbose = true;
|
|
118
|
+
else if (argument === "-f" || argument === "--fake") state.fake = true;
|
|
119
|
+
else if (argument === "-l" || argument === "-n" || argument === "--no-mtab" || argument === "-i" || argument === "--no-canonicalize") {}
|
|
120
|
+
else if (argument === "-a" || argument === "--all") fail("there is no /etc/fstab on this system, so -a does nothing", 1);
|
|
121
|
+
else if (argument === "--") state.positional.push(...argv.slice(i + 1)), i = argv.length;
|
|
122
|
+
else if (argument.startsWith("-") && argument.length > 1) console.error(`mount: unknown option ${argument}`), usage();
|
|
123
|
+
else state.positional.push(argument);
|
|
124
|
+
}
|
|
125
|
+
return state;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// -------------------------------------------------------------- listing ----
|
|
129
|
+
|
|
130
|
+
const listMounts = () => {
|
|
131
|
+
const table = readFileSync("/proc/self/mounts", "utf8");
|
|
132
|
+
const unescape = (field) => field.replace(/\\([0-7]{3})/g, (_, code) => String.fromCharCode(parseInt(code, 8)));
|
|
133
|
+
for (const line of table.split("\n")) {
|
|
134
|
+
if (!line) continue;
|
|
135
|
+
const [source, target, type, options] = line.split(" ").map(unescape);
|
|
136
|
+
console.log(`${source} on ${target} type ${type} (${options})`);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------- probing --
|
|
141
|
+
|
|
142
|
+
// Reads the first 128 KiB of a block device: enough for every superblock
|
|
143
|
+
// magic checked below (btrfs sits at 64 KiB, ISO 9660 at 32 KiB).
|
|
144
|
+
const readHeader = (device) => {
|
|
145
|
+
const fd = openSync(device, "r");
|
|
146
|
+
try {
|
|
147
|
+
const buffer = Buffer.alloc(128 * 1024);
|
|
148
|
+
const length = readSync(fd, buffer, 0, buffer.length, 0);
|
|
149
|
+
return buffer.subarray(0, length);
|
|
150
|
+
} finally {
|
|
151
|
+
closeSync(fd);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const ascii = (buffer, offset, length) =>
|
|
156
|
+
buffer.subarray(offset, offset + length).toString("latin1").replace(/\0.*$/s, "").trim();
|
|
157
|
+
|
|
158
|
+
const hex = (buffer, offset, length, reverse = false) => {
|
|
159
|
+
const bytes = [...buffer.subarray(offset, offset + length)];
|
|
160
|
+
if (reverse) bytes.reverse();
|
|
161
|
+
return bytes.map((byte) => byte.toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// { type, label, uuid } for the volume at `device`, or null when unknown.
|
|
165
|
+
// Identifies what the shipped drivers can mount, plus a few more so the
|
|
166
|
+
// error for an unsupported volume can at least name it.
|
|
167
|
+
const probe = (device) => {
|
|
168
|
+
let header;
|
|
169
|
+
try {
|
|
170
|
+
header = readHeader(device);
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const at = (offset, text) =>
|
|
175
|
+
header.length >= offset + text.length && header.subarray(offset, offset + text.length).toString("latin1") === text;
|
|
176
|
+
const u16 = (offset) => header.readUInt16LE(offset);
|
|
177
|
+
const u32 = (offset) => header.readUInt32LE(offset);
|
|
178
|
+
|
|
179
|
+
if (header.length >= 0x800 && u16(0x438) === 0xef53) {
|
|
180
|
+
// ext2/3/4 share the superblock; a journal means ext3+, extents or
|
|
181
|
+
// 64-bit or metadata_csum mean ext4. The ext4 driver handles ext3 too.
|
|
182
|
+
const compat = u32(0x45c), incompat = u32(0x460), roCompat = u32(0x464);
|
|
183
|
+
const type = incompat & 0x2c0 || roCompat & 0x478 ? "ext4" : compat & 0x4 ? "ext3" : "ext2";
|
|
184
|
+
const raw = hex(header, 0x468, 16);
|
|
185
|
+
const uuid = `${raw.slice(0, 8)}-${raw.slice(8, 12)}-${raw.slice(12, 16)}-${raw.slice(16, 20)}-${raw.slice(20)}`.toLowerCase();
|
|
186
|
+
return { type, label: ascii(header, 0x478, 16), uuid };
|
|
187
|
+
}
|
|
188
|
+
if (at(3, "NTFS ")) return { type: "ntfs3", label: "", uuid: hex(header, 0x48, 8, true) };
|
|
189
|
+
if (at(3, "EXFAT ")) return { type: "exfat", label: "", uuid: `${hex(header, 0x66, 2, true)}-${hex(header, 0x64, 2, true)}` };
|
|
190
|
+
if (header.length >= 512 && u16(510) === 0xaa55 && (at(0x52, "FAT") || at(0x36, "FAT"))) {
|
|
191
|
+
const fat32 = at(0x52, "FAT32");
|
|
192
|
+
const serialOffset = fat32 ? 0x43 : 0x27;
|
|
193
|
+
const labelOffset = fat32 ? 0x47 : 0x2b;
|
|
194
|
+
let label = ascii(header, labelOffset, 11);
|
|
195
|
+
if (label === "NO NAME") label = "";
|
|
196
|
+
return { type: "vfat", label, uuid: `${hex(header, serialOffset + 2, 2, true)}-${hex(header, serialOffset, 2, true)}` };
|
|
197
|
+
}
|
|
198
|
+
if (at(0, "XFSB")) return { type: "xfs", label: ascii(header, 0x6c, 12), uuid: "" };
|
|
199
|
+
if (at(0x10040, "_BHRfS_M")) return { type: "btrfs", label: ascii(header, 0x1012b, 256), uuid: "" };
|
|
200
|
+
if (at(0, "hsqs")) return { type: "squashfs", label: "", uuid: "" };
|
|
201
|
+
if (at(0x8001, "CD001")) return { type: "iso9660", label: ascii(header, 0x8028, 32), uuid: "" };
|
|
202
|
+
if (header.length >= 0x404 && u32(0x400) === 0xf2f52010) return { type: "f2fs", label: "", uuid: "" };
|
|
203
|
+
if (header.length >= 4096 && at(4096 - 10, "SWAPSPACE2")) return { type: "swap", label: "", uuid: "" };
|
|
204
|
+
return null;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const blockDevices = () => {
|
|
208
|
+
try {
|
|
209
|
+
return readdirSync("/sys/class/block")
|
|
210
|
+
.filter((name) => !/^(loop|ram|zram|sr)\d/.test(name))
|
|
211
|
+
.sort()
|
|
212
|
+
.map((name) => `/dev/${name}`)
|
|
213
|
+
.filter((path) => existsSync(path));
|
|
214
|
+
} catch {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const findVolume = (key, value) => {
|
|
220
|
+
for (const device of blockDevices()) {
|
|
221
|
+
const info = probe(device);
|
|
222
|
+
if (!info) continue;
|
|
223
|
+
const candidate = key === "LABEL" ? info.label : info.uuid;
|
|
224
|
+
if (candidate.toUpperCase() === value.toUpperCase()) return device;
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// LABEL=/UUID= sources need udev's /dev/disk/by-*; this scans instead.
|
|
230
|
+
const resolveSource = (source) => {
|
|
231
|
+
const match = /^(LABEL|UUID)=(.+)$/.exec(source);
|
|
232
|
+
if (!match) return source;
|
|
233
|
+
ensureBlockDrivers(null);
|
|
234
|
+
const device = findVolume(match[1], match[2]);
|
|
235
|
+
if (!device) fail(`can't find ${source}`);
|
|
236
|
+
return device;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------- drivers --
|
|
240
|
+
|
|
241
|
+
const sleep = (milliseconds) => Bun.sleepSync(milliseconds);
|
|
242
|
+
|
|
243
|
+
// The device name says which driver creates it. Loading is skipped when the
|
|
244
|
+
// node already exists, so this costs nothing after the first mount.
|
|
245
|
+
const ensureBlockDrivers = (device) => {
|
|
246
|
+
if (device && existsSync(device)) return;
|
|
247
|
+
const drivers = device === null
|
|
248
|
+
? ["virtio_blk", "vmd", "nvme", "ahci", "ata_generic", "pata_acpi", "usb_storage", "uas", "sd_mod"]
|
|
249
|
+
: /^\/dev\/vd/.test(device) ? ["virtio_blk"]
|
|
250
|
+
: /^\/dev\/sd/.test(device) ? ["ahci", "ata_generic", "pata_acpi", "usb_storage", "uas", "sd_mod"]
|
|
251
|
+
: /^\/dev\/nvme/.test(device) ? ["vmd", "nvme"]
|
|
252
|
+
: [];
|
|
253
|
+
let loaded = 0;
|
|
254
|
+
for (const driver of drivers) loaded += tryModprobe(driver)?.length ?? 0;
|
|
255
|
+
if (device === null) {
|
|
256
|
+
if (loaded) sleep(500);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// devtmpfs creates the node once the driver has probed the disk.
|
|
260
|
+
for (let waited = 0; !existsSync(device) && waited < 3000; waited += 100) sleep(100);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const kernelFilesystems = () => {
|
|
264
|
+
try {
|
|
265
|
+
return new Set(
|
|
266
|
+
readFileSync("/proc/filesystems", "utf8").split("\n").map((line) => line.split("\t").pop()).filter(Boolean),
|
|
267
|
+
);
|
|
268
|
+
} catch {
|
|
269
|
+
return new Set();
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// The kernel's own request_module() has no /sbin/modprobe to run here, so the
|
|
274
|
+
// filesystem driver and the NLS tables FAT/exFAT/NTFS ask for are loaded
|
|
275
|
+
// before mount(2). Names come from the fs-* aliases (ntfs -> ntfs3, ...).
|
|
276
|
+
// A type with no module is still handed to mount(2): the kernel knows about
|
|
277
|
+
// built-in filesystems that neither /proc/filesystems (unreadable) nor
|
|
278
|
+
// modules.builtin (tmpfs, proc, ...) may list, and answers ENODEV otherwise.
|
|
279
|
+
let driverHint = "";
|
|
280
|
+
const ensureFilesystemDriver = (type, data) => {
|
|
281
|
+
const registered = kernelFilesystems();
|
|
282
|
+
const loaded = [];
|
|
283
|
+
if (!registered.has(type)) {
|
|
284
|
+
try {
|
|
285
|
+
loaded.push(...modprobe(`fs-${type}`));
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (!(error instanceof ModuleNotFound)) throw error;
|
|
288
|
+
driverHint = ` (no ${type} module in ${modulesDirectory()})`;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (["vfat", "msdos", "exfat", "ntfs3", "iso9660", "udf"].includes(type)) {
|
|
292
|
+
const charsets = new Set(["utf8", "cp437", "iso8859-1"]);
|
|
293
|
+
for (const option of data) {
|
|
294
|
+
const [key, value] = option.split("=");
|
|
295
|
+
if (key === "iocharset" || key === "nls") charsets.add(value);
|
|
296
|
+
if (key === "codepage") charsets.add(`cp${value}`);
|
|
297
|
+
}
|
|
298
|
+
for (const charset of charsets) loaded.push(...(tryModprobe(`nls_${charset}`) ?? []));
|
|
299
|
+
}
|
|
300
|
+
return loaded;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// ---------------------------------------------------------------- mounting -
|
|
304
|
+
|
|
305
|
+
const explain = (code, source, target, type) => {
|
|
306
|
+
switch (code) {
|
|
307
|
+
case Errno.EPERM: return "must be superuser to use mount";
|
|
308
|
+
case Errno.ENOENT: return existsSync(target) ? `special device ${source} does not exist` : `mount point ${target} does not exist`;
|
|
309
|
+
case Errno.ENOTDIR: return `mount point ${target} is not a directory`;
|
|
310
|
+
case Errno.EBUSY: return `${source} already mounted or ${target} busy`;
|
|
311
|
+
case Errno.ENODEV: return `unknown filesystem type '${type}'${driverHint}`;
|
|
312
|
+
case Errno.EROFS: return `${source} is write-protected; try -o ro`;
|
|
313
|
+
case Errno.EACCES: return `${source} is not readable`;
|
|
314
|
+
case Errno.ENXIO: return `${source}: no such device or the partition has not been detected`;
|
|
315
|
+
case Errno.EINVAL: return `wrong fs type, bad option, bad superblock on ${source}, missing codepage or helper program, or other error (dmesg may say more)`;
|
|
316
|
+
default: return `${strerror(code)}`;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const doMount = (source, target, type, flags, data, { verbose, fake }) => {
|
|
321
|
+
if (verbose || fake) {
|
|
322
|
+
const words = [];
|
|
323
|
+
for (const [name, bit] of Object.entries(MS)) if (flags & bit) words.push(name.toLowerCase());
|
|
324
|
+
console.log(`mount(${source}, ${target}, ${type ?? "(none)"}, [${words.join(",")}], "${data}")`);
|
|
325
|
+
}
|
|
326
|
+
if (fake) return;
|
|
327
|
+
const dataBytes = data ? cString(data) : null;
|
|
328
|
+
const typeBytes = type ? cString(type) : null;
|
|
329
|
+
const result = libc.symbols.mount(
|
|
330
|
+
ptr(cString(source)),
|
|
331
|
+
ptr(cString(target)),
|
|
332
|
+
typeBytes ? ptr(typeBytes) : null,
|
|
333
|
+
flags,
|
|
334
|
+
dataBytes ? ptr(dataBytes) : null,
|
|
335
|
+
);
|
|
336
|
+
if (result === 0) return;
|
|
337
|
+
const code = errno();
|
|
338
|
+
// The kernel demotes a dirty NTFS or a FAT with errors to read-only rather
|
|
339
|
+
// than fail; when it does refuse outright, tell the user the escape hatch.
|
|
340
|
+
const hint = type === "ntfs3" && code === Errno.EINVAL
|
|
341
|
+
? " — a Windows volume left dirty by hibernation or Fast Startup needs -o force, or -o ro" : "";
|
|
342
|
+
fail(explain(code, source, target, type) + hint);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const main = async () => {
|
|
346
|
+
const state = parseArguments(process.argv.slice(2));
|
|
347
|
+
if (state.help) return showDocument("mount");
|
|
348
|
+
|
|
349
|
+
if (state.source) state.positional.unshift(state.source);
|
|
350
|
+
if (state.positional.length === 0) return listMounts();
|
|
351
|
+
|
|
352
|
+
const pure = state.flags & (MS.BIND | MS.MOVE | MS.REMOUNT | MS.SHARED | MS.PRIVATE | MS.SLAVE | MS.UNBINDABLE);
|
|
353
|
+
let [source, target] = state.positional;
|
|
354
|
+
if (state.positional.length === 1) {
|
|
355
|
+
// `mount -o remount,rw /` and `mount --make-private /` name the target only.
|
|
356
|
+
if (!(state.flags & MS.REMOUNT) && !(state.flags & (MS.SHARED | MS.PRIVATE | MS.SLAVE | MS.UNBINDABLE))) usage();
|
|
357
|
+
target = source;
|
|
358
|
+
source = "none";
|
|
359
|
+
} else if (state.positional.length > 2) usage();
|
|
360
|
+
|
|
361
|
+
if (state.mkdir) mkdirSync(target, { recursive: true });
|
|
362
|
+
|
|
363
|
+
let type = state.type;
|
|
364
|
+
const loaded = [];
|
|
365
|
+
if (!pure) {
|
|
366
|
+
source = resolveSource(source);
|
|
367
|
+
if (source.startsWith("/dev/")) {
|
|
368
|
+
ensureBlockDrivers(source);
|
|
369
|
+
if (!existsSync(source)) fail(`special device ${source} does not exist`);
|
|
370
|
+
}
|
|
371
|
+
if (!type || type === "auto") {
|
|
372
|
+
const info = source.startsWith("/dev/") ? probe(source) : null;
|
|
373
|
+
if (!info) fail(`can't identify the filesystem on ${source}; use -t TYPE`);
|
|
374
|
+
if (info.type === "swap") fail(`${source} is swap space, not a filesystem`);
|
|
375
|
+
type = info.type;
|
|
376
|
+
if (state.verbose) console.log(`${source}: ${info.type}${info.label ? ` label "${info.label}"` : ""}${info.uuid ? ` uuid ${info.uuid}` : ""}`);
|
|
377
|
+
}
|
|
378
|
+
if (type === "ntfs") type = "ntfs3";
|
|
379
|
+
if (type === "ext3") type = "ext4";
|
|
380
|
+
loaded.push(...ensureFilesystemDriver(type, state.data));
|
|
381
|
+
}
|
|
382
|
+
if (state.verbose && loaded.length) console.log(`loaded modules: ${loaded.join(" ")}`);
|
|
383
|
+
|
|
384
|
+
doMount(source, target, pure ? null : type, state.flags, state.data.join(","), state);
|
|
385
|
+
// mount(2) ignores every other flag on a bind, so `--bind -o ro` takes a
|
|
386
|
+
// second, remount pass the way util-linux does it.
|
|
387
|
+
if (state.flags & MS.BIND && state.flags & MS.RDONLY && !(state.flags & MS.REMOUNT)) {
|
|
388
|
+
doMount("none", target, null, MS.REMOUNT | MS.BIND | MS.RDONLY | (state.flags & MS.REC), "", state);
|
|
389
|
+
}
|
|
390
|
+
if (state.fake) console.log("(DRY RUN)");
|
|
391
|
+
else if (state.verbose) console.log(`${source} mounted on ${target}.`);
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
await main();
|
|
396
|
+
} catch (error) {
|
|
397
|
+
if (error instanceof SysError) fail(error.message);
|
|
398
|
+
if (error?.code === "ENOENT" || error?.code === "EACCES") fail(`${error.path ?? ""}: ${error.message}`);
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/bin/bun
|
|
2
|
+
// poweroff — sync filesystems and power off through Linux reboot(2).
|
|
3
|
+
|
|
4
|
+
import { libc, check, showDocument } from "../lib/dlopen.js";
|
|
5
|
+
|
|
6
|
+
const LINUX_REBOOT_CMD_POWER_OFF = 0x4321fedc;
|
|
7
|
+
const arguments_ = process.argv.slice(2);
|
|
8
|
+
|
|
9
|
+
if (arguments_.length === 1 && (arguments_[0] === "-h" || arguments_[0] === "--help")) {
|
|
10
|
+
await showDocument("poweroff");
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (arguments_.length !== 0) {
|
|
15
|
+
console.error("Usage: poweroff [-h | --help]");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log("poweroff: syncing filesystems");
|
|
20
|
+
libc.symbols.sync();
|
|
21
|
+
console.log("poweroff: powering off");
|
|
22
|
+
try {
|
|
23
|
+
check("reboot(LINUX_REBOOT_CMD_POWER_OFF)", libc.symbols.reboot(LINUX_REBOOT_CMD_POWER_OFF));
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(`poweroff: ${error.message}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// A successful reboot(2) never returns.
|
|
30
|
+
console.error("poweroff: reboot returned without powering off");
|
|
31
|
+
process.exit(1);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/bin/bun
|
|
2
|
+
// umount — unmount a filesystem. `umount --help` renders
|
|
3
|
+
// /usr/share/doc/buninu-linux/umount.md.
|
|
4
|
+
|
|
5
|
+
import { libc, cString, ptr, errno, strerror, Errno, showDocument } from "../lib/dlopen.js";
|
|
6
|
+
|
|
7
|
+
const MNT = { FORCE: 1, DETACH: 2, EXPIRE: 4, NOFOLLOW: 8 };
|
|
8
|
+
|
|
9
|
+
const usage = () => {
|
|
10
|
+
console.error(`Usage:
|
|
11
|
+
umount [-f] [-l] [-R] [-v] TARGET...
|
|
12
|
+
umount -h | --help`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let flags = 0;
|
|
17
|
+
let recursive = false;
|
|
18
|
+
let verbose = false;
|
|
19
|
+
let help = false;
|
|
20
|
+
const targets = [];
|
|
21
|
+
for (const argument of process.argv.slice(2)) {
|
|
22
|
+
if (argument === "-h" || argument === "--help") help = true;
|
|
23
|
+
else if (argument === "-f" || argument === "--force") flags |= MNT.FORCE;
|
|
24
|
+
else if (argument === "-l" || argument === "--lazy") flags |= MNT.DETACH;
|
|
25
|
+
else if (argument === "-R" || argument === "--recursive") recursive = true;
|
|
26
|
+
else if (argument === "-v" || argument === "--verbose") verbose = true;
|
|
27
|
+
else if (argument === "-n" || argument === "-d" || argument === "--no-mtab" || argument === "--detach-loop") {}
|
|
28
|
+
else if (argument === "-a" || argument === "--all") { console.error("umount: -a is not supported"); process.exit(1); }
|
|
29
|
+
else if (argument.startsWith("-") && argument.length > 1) { console.error(`umount: unknown option ${argument}`); usage(); }
|
|
30
|
+
else targets.push(argument);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (help) await showDocument("umount");
|
|
34
|
+
else if (targets.length === 0) usage();
|
|
35
|
+
|
|
36
|
+
const mountPoints = () => {
|
|
37
|
+
const table = Bun.file("/proc/self/mounts");
|
|
38
|
+
return table.text().then((text) =>
|
|
39
|
+
text.split("\n").filter(Boolean).map((line) =>
|
|
40
|
+
line.split(" ")[1].replace(/\\([0-7]{3})/g, (_, code) => String.fromCharCode(parseInt(code, 8))),
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const unmount = (target) => {
|
|
46
|
+
const result = libc.symbols.umount2(ptr(cString(target)), flags);
|
|
47
|
+
if (result === 0) {
|
|
48
|
+
if (verbose) console.log(`${target} unmounted`);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
const code = errno();
|
|
52
|
+
const reason = code === Errno.EBUSY ? "target is busy (try -l for a lazy unmount)"
|
|
53
|
+
: code === Errno.EINVAL ? "not mounted"
|
|
54
|
+
: code === Errno.EPERM ? "must be superuser to unmount"
|
|
55
|
+
: strerror(code);
|
|
56
|
+
console.error(`umount: ${target}: ${reason}.`);
|
|
57
|
+
return false;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
let status = 0;
|
|
61
|
+
for (const target of targets) {
|
|
62
|
+
const path = target.length > 1 ? target.replace(/\/+$/, "") : target;
|
|
63
|
+
// -R takes every mount at or below the target, deepest first, so each
|
|
64
|
+
// umount2 sees an empty subtree.
|
|
65
|
+
const list = recursive
|
|
66
|
+
? (await mountPoints()).filter((point) => point === path || point.startsWith(`${path}/`)).sort((a, b) => b.length - a.length)
|
|
67
|
+
: [path];
|
|
68
|
+
for (const point of list) if (!unmount(point)) status = 32;
|
|
69
|
+
}
|
|
70
|
+
process.exit(status);
|