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.
@@ -0,0 +1,534 @@
1
+ // /lib/fat.js — FAT32 for /bin/mkfs.fat, /bin/mmd, /bin/mcopy and /bin/mdir.
2
+ //
3
+ // Layout and directory entries follow what mkfs.fat 4.2 and mtools 4.0 write,
4
+ // so the same operations give the same bytes (the boot code of mkfs.fat
5
+ // aside: the boot sector here only halts): geometry and cluster size by
6
+ // image size, 32 reserved sectors with the FSInfo sector at 1 and the backup
7
+ // boot sector at 6, the smallest FAT that holds every cluster, data aligned
8
+ // to the cluster size, clusters allocated in order from the start, 8.3 names
9
+ // with the NT lower-case flags where they suffice and VFAT long names (and a
10
+ // NAME~N alias) where not. Only FAT32 and 512-byte sectors are supported.
11
+
12
+ import { closeSync, fstatSync, openSync, readSync, writeSync } from "node:fs";
13
+
14
+ export class FatError extends Error {}
15
+
16
+ const SECTOR = 512;
17
+ const MiB = 1024 * 1024;
18
+ const FREE = 0;
19
+ const END = 0x0fffffff;
20
+ const ATTR = { READ_ONLY: 0x01, HIDDEN: 0x02, SYSTEM: 0x04, VOLUME: 0x08, DIRECTORY: 0x10, ARCHIVE: 0x20, LONG_NAME: 0x0f };
21
+
22
+ const readAt = (fd, position, length) => {
23
+ const buffer = Buffer.alloc(length);
24
+ for (let done = 0; done < length;) {
25
+ const count = readSync(fd, buffer, done, length - done, position + done);
26
+ if (count === 0) throw new FatError(`unexpected end of image at ${position + done}`);
27
+ done += count;
28
+ }
29
+ return buffer;
30
+ };
31
+
32
+ const writeAt = (fd, position, buffer) => {
33
+ for (let done = 0; done < buffer.length;) done += writeSync(fd, buffer, done, buffer.length - done, position + done);
34
+ };
35
+
36
+ /** "image" or mtools' "image@@offset" (offset in bytes, or with K, M, G). */
37
+ export const parseImageSpec = (spec) => {
38
+ const at = spec.lastIndexOf("@@");
39
+ if (at < 0) return { path: spec, offset: 0 };
40
+ const match = /^(\d+)([KMGkmg]?)$/.exec(spec.slice(at + 2));
41
+ if (!match) throw new FatError(`bad offset in ${spec}`);
42
+ return { path: spec.slice(0, at), offset: Number(match[1]) * ({ "": 1, k: 1024, m: MiB, g: 1024 * MiB }[match[2].toLowerCase()]) };
43
+ };
44
+
45
+ // --- time ------------------------------------------------------------------
46
+
47
+ /** "Now" for new entries: SOURCE_DATE_EPOCH when set, as mtools and mkfs.fat do. */
48
+ export const now = () => {
49
+ const epoch = process.env.SOURCE_DATE_EPOCH;
50
+ return epoch !== undefined && /^\d+$/.test(epoch) ? { date: new Date(Number(epoch) * 1000), fromEpoch: true } : { date: new Date(), fromEpoch: false };
51
+ };
52
+
53
+ /** DOS date and time words, in local time or UTC. */
54
+ export const dosTime = (date, utc = false) => {
55
+ const get = (name) => date[`get${utc ? "UTC" : ""}${name}`]();
56
+ const year = Math.min(Math.max(get("FullYear"), 1980), 2107);
57
+ return {
58
+ date: ((year - 1980) << 9) | ((get("Month") + 1) << 5) | get("Date"),
59
+ time: (get("Hours") << 11) | (get("Minutes") << 5) | Math.floor(get("Seconds") / 2),
60
+ };
61
+ };
62
+
63
+ // --- names -----------------------------------------------------------------
64
+
65
+ const SHORT_CHARS = /^[A-Z0-9!#$%&'()\-@^_`{}~\x80-\xff]+$/;
66
+
67
+ /** The 11-byte short name of a valid 8.3 name, or null. Case handled by the caller. */
68
+ const shortField = (base, extension) => Buffer.from(base.padEnd(8) + extension.padEnd(3), "latin1");
69
+
70
+ /**
71
+ * How `name` is stored: a plain 8.3 entry (with the NT flags 0x08/0x10 for an
72
+ * all-lower-case base/extension), or a long name needing an alias.
73
+ */
74
+ const classify = (name) => {
75
+ if (name === "." || name === "..") return { short: shortField(name, ""), ntFlags: 0, long: false };
76
+ const dot = name.lastIndexOf(".");
77
+ const base = dot > 0 ? name.slice(0, dot) : name;
78
+ const extension = dot > 0 ? name.slice(dot + 1) : "";
79
+ const fits = (part, length) => part.length > 0 && part.length <= length && SHORT_CHARS.test(part.toUpperCase()) && !/[^\x20-\x7e]/.test(part);
80
+ const caseFlag = (part, flag) => part === part.toUpperCase() ? 0 : part === part.toLowerCase() ? flag : -1;
81
+ if (fits(base, 8) && (extension === "" || fits(extension, 3)) && (dot <= 0 || dot === name.indexOf("."))) {
82
+ const flags = [caseFlag(base, 0x08), extension ? caseFlag(extension, 0x10) : 0];
83
+ if (!flags.includes(-1)) return { short: shortField(base.toUpperCase(), extension.toUpperCase()), ntFlags: flags[0] | flags[1], long: false };
84
+ }
85
+ return { long: true };
86
+ };
87
+
88
+ /** A NAME~N.EXT alias for a long name, unique among `taken` (11-byte keys). */
89
+ const aliasFor = (name, taken) => {
90
+ const clean = (text) => text.toUpperCase().replace(/[ .]/g, "").replace(/[^A-Z0-9!#$%&'()\-@^_`{}~]/g, "_");
91
+ const dot = name.lastIndexOf(".");
92
+ const base = clean(dot > 0 ? name.slice(0, dot) : name) || "_";
93
+ const extension = clean(dot > 0 ? name.slice(dot + 1) : "").slice(0, 3);
94
+ for (let number = 1; number < 1000000; number++) {
95
+ const tail = `~${number}`;
96
+ const field = shortField(base.slice(0, 8 - tail.length) + tail, extension);
97
+ if (!taken.has(field.toString("latin1"))) return field;
98
+ }
99
+ throw new FatError(`no short name left for ${name}`);
100
+ };
101
+
102
+ const checksum = (short) => {
103
+ let sum = 0;
104
+ for (const byte of short) sum = (((sum & 1) << 7) + (sum >> 1) + byte) & 0xff;
105
+ return sum;
106
+ };
107
+
108
+ /** The VFAT long-name entries for `name`, last part first, as they are stored. */
109
+ const longEntries = (name, short) => {
110
+ const units = [...Buffer.from(name, "utf16le")].length / 2;
111
+ if (units > 255) throw new FatError(`name too long: ${name}`);
112
+ const characters = Buffer.from(name, "utf16le");
113
+ const count = Math.ceil(units / 13);
114
+ const sum = checksum(short);
115
+ const entries = [];
116
+ for (let part = count; part >= 1; part--) {
117
+ const entry = Buffer.alloc(32);
118
+ entry[0] = part | (part === count ? 0x40 : 0);
119
+ entry[11] = ATTR.LONG_NAME;
120
+ entry[13] = sum;
121
+ const slots = [1, 3, 5, 7, 9, 14, 16, 18, 20, 22, 24, 28, 30];
122
+ slots.forEach((offset, index) => {
123
+ const unit = (part - 1) * 13 + index;
124
+ const value = unit < units ? characters.readUInt16LE(unit * 2) : unit === units ? 0x0000 : 0xffff;
125
+ entry.writeUInt16LE(value, offset);
126
+ });
127
+ entries.push(entry);
128
+ }
129
+ return entries;
130
+ };
131
+
132
+ const shortName = (entry) => {
133
+ const base = entry.toString("latin1", 0, 8).trimEnd();
134
+ const extension = entry.toString("latin1", 8, 11).trimEnd();
135
+ const lower = (text, flag) => entry[12] & flag ? text.toLowerCase() : text;
136
+ return lower(base, 0x08) + (extension ? `.${lower(extension, 0x10)}` : "");
137
+ };
138
+
139
+ // --- mkfs ------------------------------------------------------------------
140
+
141
+ /**
142
+ * mkfs.fat's parameters for an image of `sectors` sectors: the geometry it
143
+ * picks for a file, the cluster size of the Microsoft table, and the total
144
+ * cut to whole tracks.
145
+ */
146
+ export const layout = (sectors, { sectorsPerCluster } = {}) => {
147
+ const [perTrack, heads] = sectors <= 262144 ? [32, 8] : sectors <= 524288 ? [32, 16]
148
+ : [63, sectors < 1048576 ? 16 : sectors < 2097152 ? 32 : sectors < 4194304 ? 64 : sectors < 8388608 ? 128 : 255];
149
+ const total = Math.floor(sectors / perTrack) * perTrack;
150
+ const perCluster = sectorsPerCluster ?? (sectors <= 532480 ? 1 : sectors <= 16777216 ? 8 : sectors <= 33554432 ? 16 : sectors <= 67108864 ? 32 : 64);
151
+ let reserved = 32;
152
+ let fatSize;
153
+ for (;;) {
154
+ // The smallest FAT that has an entry for every cluster it leaves.
155
+ const clustersWith = (size) => Math.floor((total - reserved - 2 * size) / perCluster);
156
+ const needed = (size) => Math.ceil((clustersWith(size) + 2) * 4 / SECTOR);
157
+ let low = 1;
158
+ let high = Math.ceil((total / perCluster + 2) * 4 / SECTOR);
159
+ while (low < high) {
160
+ const middle = Math.floor((low + high) / 2);
161
+ if (needed(middle) <= middle) high = middle; else low = middle + 1;
162
+ }
163
+ fatSize = low;
164
+ // Data starts on a cluster boundary.
165
+ const misalignment = (reserved + 2 * fatSize) % perCluster;
166
+ if (misalignment === 0) break;
167
+ reserved += perCluster - misalignment;
168
+ }
169
+ const clusters = Math.floor((total - reserved - 2 * fatSize) / perCluster);
170
+ if (clusters < 65525) throw new FatError(`${clusters} clusters is too few for FAT32 (needs 65525; the image must be at least about 33 MiB)`);
171
+ if (clusters > 0x0ffffff4) throw new FatError("too many clusters for FAT32");
172
+ return { perTrack, heads, total, perCluster, reserved, fatSize, clusters };
173
+ };
174
+
175
+ /**
176
+ * Writes an empty FAT32 filesystem of `sectors` sectors at `offset` in `fd`.
177
+ * `invariant` fixes the volume ID and times as mkfs.fat --invariant does.
178
+ */
179
+ export const format = (fd, { offset = 0, sectors, label = "NO NAME", volumeId = null, hidden = 0, invariant = false, sectorsPerCluster } = {}) => {
180
+ const geometry = layout(sectors, { sectorsPerCluster });
181
+ const { date, fromEpoch } = invariant ? { date: new Date(1426325213 * 1000), fromEpoch: true } : now();
182
+ const id = volumeId ?? (invariant ? 0x1234abcd : fromEpoch ? Math.floor(date.getTime() / 1000) >>> 0 : (Math.random() * 2 ** 32) >>> 0);
183
+ const labelField = Buffer.from(label.toUpperCase().slice(0, 11).padEnd(11), "latin1");
184
+
185
+ const boot = Buffer.alloc(SECTOR);
186
+ boot.set([0xeb, 0x58, 0x90], 0);
187
+ boot.write("mkfs.fat", 3, "latin1");
188
+ boot.writeUInt16LE(SECTOR, 11);
189
+ boot[13] = geometry.perCluster;
190
+ boot.writeUInt16LE(geometry.reserved, 14);
191
+ boot[16] = 2; // FATs
192
+ boot[21] = 0xf8; // media: fixed disk
193
+ boot.writeUInt16LE(geometry.perTrack, 24);
194
+ boot.writeUInt16LE(geometry.heads, 26);
195
+ boot.writeUInt32LE(hidden, 28);
196
+ boot.writeUInt32LE(geometry.total, 32);
197
+ boot.writeUInt32LE(geometry.fatSize, 36);
198
+ boot.writeUInt32LE(2, 44); // root directory cluster
199
+ boot.writeUInt16LE(1, 48); // FSInfo sector
200
+ boot.writeUInt16LE(6, 50); // backup boot sector
201
+ boot[64] = 0x80; // drive number
202
+ boot[66] = 0x29; // extended boot signature
203
+ boot.writeUInt32LE(id, 67);
204
+ labelField.copy(boot, 71);
205
+ boot.write("FAT32 ", 82, "latin1");
206
+ boot.set([0xf4, 0xeb, 0xfd], 90); // not bootable: hlt; jmp $-1
207
+ boot.writeUInt16LE(0xaa55, 510);
208
+
209
+ const info = Buffer.alloc(SECTOR);
210
+ info.writeUInt32LE(0x41615252, 0);
211
+ info.writeUInt32LE(0x61417272, 484);
212
+ info.writeUInt32LE(geometry.clusters - 1, 488); // the root directory takes one
213
+ info.writeUInt32LE(2, 492);
214
+ info.writeUInt32LE(0xaa550000, 508);
215
+
216
+ // Reserved sectors, both FATs and the root directory cluster start zeroed.
217
+ const zeroLength = (geometry.reserved + 2 * geometry.fatSize + geometry.perCluster) * SECTOR;
218
+ const chunk = Buffer.alloc(Math.min(zeroLength, 4 * MiB));
219
+ for (let done = 0; done < zeroLength; done += chunk.length) writeAt(fd, offset + done, chunk.subarray(0, Math.min(chunk.length, zeroLength - done)));
220
+ writeAt(fd, offset, boot);
221
+ writeAt(fd, offset + SECTOR, info);
222
+ writeAt(fd, offset + 6 * SECTOR, boot);
223
+ writeAt(fd, offset + 7 * SECTOR, info);
224
+ const fatStart = Buffer.alloc(12);
225
+ fatStart.writeUInt32LE(0x0ffffff8, 0); // media in entry 0
226
+ fatStart.writeUInt32LE(0x0fffffff, 4);
227
+ fatStart.writeUInt32LE(0x0ffffff8, 8); // root directory: one cluster
228
+ for (let copy = 0; copy < 2; copy++) writeAt(fd, offset + (geometry.reserved + copy * geometry.fatSize) * SECTOR, fatStart);
229
+ if (label.toUpperCase() !== "NO NAME") {
230
+ const entry = Buffer.alloc(32);
231
+ labelField.copy(entry, 0);
232
+ entry[11] = ATTR.VOLUME;
233
+ // mkfs.fat stamps the label in UTC for SOURCE_DATE_EPOCH and --invariant.
234
+ const stamp = dosTime(date, fromEpoch);
235
+ entry.writeUInt16LE(stamp.time, 14);
236
+ entry.writeUInt16LE(stamp.date, 16);
237
+ entry.writeUInt16LE(stamp.date, 18);
238
+ entry.writeUInt16LE(stamp.time, 22);
239
+ entry.writeUInt16LE(stamp.date, 24);
240
+ writeAt(fd, offset + (geometry.reserved + 2 * geometry.fatSize) * SECTOR, entry);
241
+ }
242
+ return { ...geometry, volumeId: id };
243
+ };
244
+
245
+ // --- an existing filesystem --------------------------------------------------
246
+
247
+ export class Volume {
248
+ constructor(fd, offset = 0) {
249
+ this.fd = fd;
250
+ this.offset = offset;
251
+ const boot = readAt(fd, offset, SECTOR);
252
+ if (boot.readUInt16LE(510) !== 0xaa55) throw new FatError("no FAT boot sector (missing 0x55AA)");
253
+ if (boot.readUInt16LE(11) !== SECTOR) throw new FatError(`${boot.readUInt16LE(11)}-byte sectors are not supported`);
254
+ if (boot.readUInt16LE(22) !== 0 || boot.readUInt16LE(17) !== 0) throw new FatError("only FAT32 is supported");
255
+ this.perCluster = boot[13];
256
+ this.reserved = boot.readUInt16LE(14);
257
+ this.fats = boot[16];
258
+ this.total = boot.readUInt32LE(32);
259
+ this.fatSize = boot.readUInt32LE(36);
260
+ this.rootCluster = boot.readUInt32LE(44);
261
+ this.infoSector = boot.readUInt16LE(48);
262
+ this.clusterBytes = this.perCluster * SECTOR;
263
+ this.dataStart = this.reserved + this.fats * this.fatSize;
264
+ this.clusters = Math.floor((this.total - this.dataStart) / this.perCluster);
265
+ this.fat = readAt(fd, offset + this.reserved * SECTOR, this.fatSize * SECTOR);
266
+ this.lastAllocated = null;
267
+ this.dirty = false;
268
+ }
269
+
270
+ clusterPosition(cluster) {
271
+ return this.offset + (this.dataStart + (cluster - 2) * this.perCluster) * SECTOR;
272
+ }
273
+
274
+ next(cluster) {
275
+ return this.fat.readUInt32LE(cluster * 4) & 0x0fffffff;
276
+ }
277
+
278
+ setNext(cluster, value) {
279
+ const high = this.fat.readUInt32LE(cluster * 4) & 0xf0000000;
280
+ this.fat.writeUInt32LE((high | value) >>> 0, cluster * 4);
281
+ this.dirty = true;
282
+ }
283
+
284
+ chain(start) {
285
+ const clusters = [];
286
+ for (let cluster = start; cluster >= 2 && cluster < 0x0ffffff8; cluster = this.next(cluster)) {
287
+ if (clusters.length > this.clusters) throw new FatError("cluster chain loops");
288
+ clusters.push(cluster);
289
+ }
290
+ return clusters;
291
+ }
292
+
293
+ readChain(start, length = Infinity) {
294
+ const clusters = this.chain(start);
295
+ const buffer = Buffer.alloc(Math.min(length, clusters.length * this.clusterBytes));
296
+ // Consecutive clusters are read in one go.
297
+ for (let index = 0; index < clusters.length && index * this.clusterBytes < buffer.length;) {
298
+ let run = 1;
299
+ while (index + run < clusters.length && clusters[index + run] === clusters[index] + run) run++;
300
+ const start = index * this.clusterBytes;
301
+ const size = Math.min(run * this.clusterBytes, buffer.length - start);
302
+ readAt(this.fd, this.clusterPosition(clusters[index]), size).copy(buffer, start);
303
+ index += run;
304
+ }
305
+ return buffer;
306
+ }
307
+
308
+ /** Allocates `count` clusters, the lowest free ones first, chained in order. */
309
+ allocate(count) {
310
+ const found = [];
311
+ for (let cluster = 2; cluster < this.clusters + 2 && found.length < count; cluster++) {
312
+ if (this.next(cluster) === FREE) found.push(cluster);
313
+ }
314
+ if (found.length < count) throw new FatError("no space left in the filesystem");
315
+ found.forEach((cluster, index) => this.setNext(cluster, index + 1 < found.length ? found[index + 1] : END));
316
+ if (found.length) this.lastAllocated = found.at(-1);
317
+ return found;
318
+ }
319
+
320
+ free(start) {
321
+ for (const cluster of this.chain(start)) this.setNext(cluster, FREE);
322
+ }
323
+
324
+ /** Writes `bytes` over the clusters of a chain, consecutive runs at once. */
325
+ writeClusters(clusters, bytes) {
326
+ for (let index = 0; index < clusters.length;) {
327
+ let run = 1;
328
+ while (index + run < clusters.length && clusters[index + run] === clusters[index] + run) run++;
329
+ const slice = bytes.subarray(index * this.clusterBytes, (index + run) * this.clusterBytes);
330
+ if (slice.length) writeAt(this.fd, this.clusterPosition(clusters[index]), slice);
331
+ index += run;
332
+ }
333
+ }
334
+
335
+ /** The entries of a directory: long name, short name, attributes, cluster, size and their slots. */
336
+ entries(cluster) {
337
+ const buffer = this.readChain(cluster);
338
+ const entries = [];
339
+ let pending = [];
340
+ for (let slot = 0; slot * 32 < buffer.length; slot++) {
341
+ const entry = buffer.subarray(slot * 32, slot * 32 + 32);
342
+ if (entry[0] === 0x00) break;
343
+ if (entry[0] === 0xe5) { pending = []; continue; }
344
+ if (entry[11] === ATTR.LONG_NAME) { pending.push({ entry, slot }); continue; }
345
+ let longName = null;
346
+ if (pending.length && pending.every(({ entry: part }) => part[13] === checksum(entry.subarray(0, 11)))) {
347
+ const units = [];
348
+ for (const { entry: part } of [...pending].reverse()) {
349
+ for (const offset of [1, 3, 5, 7, 9, 14, 16, 18, 20, 22, 24, 28, 30]) units.push(part.readUInt16LE(offset));
350
+ }
351
+ const end = units.indexOf(0);
352
+ longName = Buffer.from(Uint16Array.from(end < 0 ? units : units.slice(0, end)).buffer).toString("utf16le");
353
+ }
354
+ entries.push({
355
+ name: longName ?? shortName(entry),
356
+ short: Buffer.from(entry.subarray(0, 11)),
357
+ attributes: entry[11],
358
+ cluster: (entry.readUInt16LE(20) << 16) | entry.readUInt16LE(26),
359
+ size: entry.readUInt32LE(28),
360
+ time: entry.readUInt16LE(22),
361
+ date: entry.readUInt16LE(24),
362
+ slot,
363
+ firstSlot: pending.length ? pending[0].slot : slot,
364
+ });
365
+ pending = [];
366
+ }
367
+ return entries;
368
+ }
369
+
370
+ /** The entry at `path` ("/" is the root, with its cluster), or null. */
371
+ lookup(path) {
372
+ let current = { attributes: ATTR.DIRECTORY, cluster: this.rootCluster, name: "/", root: true };
373
+ for (const part of path.split("/").filter(Boolean)) {
374
+ if (!(current.attributes & ATTR.DIRECTORY)) return null;
375
+ const lower = part.toLowerCase();
376
+ const found = this.entries(current.cluster || this.rootCluster)
377
+ .find((entry) => !(entry.attributes & ATTR.VOLUME) && (entry.name.toLowerCase() === lower || shortName(entry.short).toLowerCase() === lower));
378
+ if (!found) return null;
379
+ current = found.cluster === 0 && found.attributes & ATTR.DIRECTORY ? { ...found, cluster: this.rootCluster } : found;
380
+ }
381
+ return current;
382
+ }
383
+
384
+ /** Adds raw 32-byte entries to a directory, growing it by a cluster if it is full. */
385
+ addEntries(directoryCluster, raw) {
386
+ for (;;) {
387
+ const buffer = this.readChain(directoryCluster);
388
+ let run = 0;
389
+ for (let slot = 0; slot * 32 < buffer.length; slot++) {
390
+ const first = buffer[slot * 32];
391
+ run = first === 0x00 || first === 0xe5 ? run + 1 : 0;
392
+ if (run === raw.length) {
393
+ const start = slot - raw.length + 1;
394
+ raw.forEach((entry, index) => entry.copy(buffer, (start + index) * 32));
395
+ this.writeClusters(this.chain(directoryCluster), buffer);
396
+ return;
397
+ }
398
+ }
399
+ const clusters = this.chain(directoryCluster);
400
+ const [added] = this.allocate(1);
401
+ this.setNext(clusters.at(-1), added);
402
+ writeAt(this.fd, this.clusterPosition(added), Buffer.alloc(this.clusterBytes));
403
+ }
404
+ }
405
+
406
+ /** A short entry (plus long-name entries when needed) for `name` in a directory. */
407
+ makeEntries(directoryCluster, name, { attributes, cluster, size, date }) {
408
+ if (!name || /[\x00-\x1f"*/:<>?\\|]/.test(name) || name === "." || name === "..") throw new FatError(`invalid name: ${name}`);
409
+ const kind = classify(name);
410
+ let short = kind.short;
411
+ let parts = [];
412
+ if (kind.long) {
413
+ const taken = new Set(this.entries(directoryCluster).map((entry) => entry.short.toString("latin1")));
414
+ short = aliasFor(name, taken);
415
+ parts = longEntries(name, short);
416
+ }
417
+ const entry = Buffer.alloc(32);
418
+ short.copy(entry, 0);
419
+ entry[11] = attributes;
420
+ entry[12] = kind.long ? 0 : kind.ntFlags;
421
+ const stamp = dosTime(date);
422
+ entry.writeUInt16LE(stamp.time, 14);
423
+ entry.writeUInt16LE(stamp.date, 16);
424
+ entry.writeUInt16LE(stamp.date, 18);
425
+ entry.writeUInt16LE(cluster >>> 16, 20);
426
+ entry.writeUInt16LE(stamp.time, 22);
427
+ entry.writeUInt16LE(stamp.date, 24);
428
+ entry.writeUInt16LE(cluster & 0xffff, 26);
429
+ entry.writeUInt32LE(size, 28);
430
+ return [...parts, entry];
431
+ }
432
+
433
+ parentOf(path) {
434
+ const parts = path.split("/").filter(Boolean);
435
+ const name = parts.pop();
436
+ if (!name) throw new FatError("the root directory already exists");
437
+ const parent = this.lookup(parts.join("/"));
438
+ if (!parent) throw new FatError(`${"/" + parts.join("/")}: no such directory`);
439
+ if (!(parent.attributes & ATTR.DIRECTORY)) throw new FatError(`${"/" + parts.join("/")}: not a directory`);
440
+ return { parent, name };
441
+ }
442
+
443
+ mkdir(path, date = now().date) {
444
+ const { parent, name } = this.parentOf(path);
445
+ if (this.lookup(path)) throw new FatError(`${path}: file exists`);
446
+ const [cluster] = this.allocate(1);
447
+ const parentCluster = parent.root ? 0 : parent.cluster;
448
+ const block = Buffer.alloc(this.clusterBytes);
449
+ const dot = (field, target) => {
450
+ const entry = Buffer.alloc(32);
451
+ Buffer.from(field.padEnd(11), "latin1").copy(entry, 0);
452
+ entry[11] = ATTR.DIRECTORY;
453
+ const stamp = dosTime(date);
454
+ entry.writeUInt16LE(stamp.time, 14);
455
+ entry.writeUInt16LE(stamp.date, 16);
456
+ entry.writeUInt16LE(stamp.date, 18);
457
+ entry.writeUInt16LE(target >>> 16, 20);
458
+ entry.writeUInt16LE(stamp.time, 22);
459
+ entry.writeUInt16LE(stamp.date, 24);
460
+ entry.writeUInt16LE(target & 0xffff, 26);
461
+ return entry;
462
+ };
463
+ dot(".", cluster).copy(block, 0);
464
+ dot("..", parentCluster).copy(block, 32);
465
+ writeAt(this.fd, this.clusterPosition(cluster), block);
466
+ this.addEntries(parent.cluster, this.makeEntries(parent.cluster, name, { attributes: ATTR.DIRECTORY, cluster, size: 0, date }));
467
+ }
468
+
469
+ /** Writes a file at `path`; an existing file is replaced only with `overwrite`. */
470
+ writeFile(path, bytes, { date = now().date, overwrite = false } = {}) {
471
+ const { parent, name } = this.parentOf(path);
472
+ const existing = this.lookup(path);
473
+ if (existing) {
474
+ if (existing.attributes & ATTR.DIRECTORY) throw new FatError(`${path}: is a directory`);
475
+ if (!overwrite) throw new FatError(`${path}: file exists`);
476
+ this.remove(path);
477
+ }
478
+ const clusters = this.allocate(Math.ceil(bytes.length / this.clusterBytes));
479
+ this.writeClusters(clusters, bytes);
480
+ this.addEntries(parent.cluster, this.makeEntries(parent.cluster, name, { attributes: ATTR.ARCHIVE, cluster: clusters[0] ?? 0, size: bytes.length, date }));
481
+ }
482
+
483
+ readFile(path) {
484
+ const entry = this.lookup(path);
485
+ if (!entry) throw new FatError(`${path}: no such file`);
486
+ if (entry.attributes & ATTR.DIRECTORY) throw new FatError(`${path}: is a directory`);
487
+ return this.readChain(entry.cluster, entry.size);
488
+ }
489
+
490
+ remove(path) {
491
+ const { parent } = this.parentOf(path);
492
+ const entry = this.lookup(path);
493
+ if (!entry || entry.root) throw new FatError(`${path}: no such file`);
494
+ if (entry.cluster) this.free(entry.cluster);
495
+ const buffer = this.readChain(parent.cluster);
496
+ for (let slot = entry.firstSlot; slot <= entry.slot; slot++) buffer[slot * 32] = 0xe5;
497
+ this.writeClusters(this.chain(parent.cluster), buffer);
498
+ }
499
+
500
+ list(path) {
501
+ const entry = this.lookup(path);
502
+ if (!entry) throw new FatError(`${path}: no such file or directory`);
503
+ if (!(entry.attributes & ATTR.DIRECTORY)) return [entry];
504
+ return this.entries(entry.cluster);
505
+ }
506
+
507
+ /** Writes the FAT copies and the FSInfo sector (the backup copy is left alone, as mtools does). */
508
+ flush() {
509
+ if (!this.dirty) return;
510
+ for (let copy = 0; copy < this.fats; copy++) writeAt(this.fd, this.offset + (this.reserved + copy * this.fatSize) * SECTOR, this.fat);
511
+ let freeCount = 0;
512
+ for (let cluster = 2; cluster < this.clusters + 2; cluster++) if (this.next(cluster) === FREE) freeCount++;
513
+ const info = readAt(this.fd, this.offset + this.infoSector * SECTOR, SECTOR);
514
+ if (info.readUInt32LE(0) === 0x41615252) {
515
+ info.writeUInt32LE(freeCount, 488);
516
+ if (this.lastAllocated !== null) info.writeUInt32LE(this.lastAllocated, 492);
517
+ writeAt(this.fd, this.offset + this.infoSector * SECTOR, info);
518
+ }
519
+ this.dirty = false;
520
+ }
521
+ }
522
+
523
+ export { ATTR };
524
+
525
+ /** Opens an mtools image spec read-write (or read-only) as a Volume. */
526
+ export const openVolume = (spec, writable = true) => {
527
+ const { path, offset } = parseImageSpec(spec);
528
+ let fd;
529
+ try { fd = openSync(path, writable ? "r+" : "r"); } catch (error) { throw new FatError(`cannot open ${path}: ${error.message}`); }
530
+ if (fstatSync(fd).size < offset + SECTOR) { closeSync(fd); throw new FatError(`${path}: no filesystem at offset ${offset}`); }
531
+ const volume = new Volume(fd, offset);
532
+ volume.close = () => { volume.flush(); closeSync(fd); };
533
+ return volume;
534
+ };
@@ -0,0 +1,144 @@
1
+ // /lib/gpt.js — GUID partition tables for /bin/parted.
2
+ //
3
+ // Writes what parted writes for a disk image: a protective MBR with one 0xEE
4
+ // partition over the disk, the primary header at LBA 1 with 128 entries of
5
+ // 128 bytes from LBA 2, and the backup entries and header at the end of the
6
+ // disk; first usable LBA 34. Only 512-byte sectors.
7
+
8
+ import { readSync, writeSync } from "node:fs";
9
+ import { randomUUID } from "node:crypto";
10
+ import { crc32 } from "./crc32.js";
11
+
12
+ export class GptError extends Error {}
13
+
14
+ export const SECTOR = 512;
15
+ const ENTRIES = 128;
16
+ const ENTRY_SIZE = 128;
17
+ const ENTRY_SECTORS = (ENTRIES * ENTRY_SIZE) / SECTOR;
18
+
19
+ export const TYPES = {
20
+ esp: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
21
+ msdata: "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7",
22
+ linux: "0FC63DAF-8483-4772-8E79-3D69D8477DE4",
23
+ swap: "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F",
24
+ bios_grub: "21686148-6449-6E6F-744E-656564454649",
25
+ };
26
+
27
+ /** GUID text to its on-disk bytes: the first three fields little-endian. */
28
+ export const guidBytes = (text) => {
29
+ const hex = text.replace(/-/g, "");
30
+ if (!/^[0-9a-fA-F]{32}$/.test(hex)) throw new GptError(`bad GUID ${text}`);
31
+ const bytes = Buffer.from(hex, "hex");
32
+ bytes.subarray(0, 4).reverse();
33
+ bytes.subarray(4, 6).reverse();
34
+ bytes.subarray(6, 8).reverse();
35
+ return bytes;
36
+ };
37
+
38
+ export const guidText = (buffer, offset = 0) => {
39
+ const bytes = Buffer.from(buffer.subarray(offset, offset + 16));
40
+ bytes.subarray(0, 4).reverse();
41
+ bytes.subarray(4, 6).reverse();
42
+ bytes.subarray(6, 8).reverse();
43
+ const hex = bytes.toString("hex").toUpperCase();
44
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
45
+ };
46
+
47
+ export const randomGuid = () => randomUUID().toUpperCase();
48
+
49
+ const readAt = (fd, position, length) => {
50
+ const buffer = Buffer.alloc(length);
51
+ for (let done = 0; done < length;) {
52
+ const count = readSync(fd, buffer, done, length - done, position + done);
53
+ if (count === 0) throw new GptError("unexpected end of disk");
54
+ done += count;
55
+ }
56
+ return buffer;
57
+ };
58
+
59
+ const writeAt = (fd, position, buffer) => {
60
+ for (let done = 0; done < buffer.length;) done += writeSync(fd, buffer, done, buffer.length - done, position + done);
61
+ };
62
+
63
+ /** The usable range of a disk of `sectors` sectors. */
64
+ export const usable = (sectors) => ({ first: 2 + ENTRY_SECTORS, last: sectors - 2 - ENTRY_SECTORS });
65
+
66
+ /** The table on the disk (primary, else backup), or null when neither is valid. */
67
+ export const readGpt = (fd, sectors) => {
68
+ for (const lba of [1, sectors - 1]) {
69
+ const header = readAt(fd, lba * SECTOR, SECTOR);
70
+ if (header.toString("latin1", 0, 8) !== "EFI PART") continue;
71
+ const size = header.readUInt32LE(12);
72
+ const check = Buffer.from(header.subarray(0, size));
73
+ check.writeUInt32LE(0, 16);
74
+ if (crc32(check) !== header.readUInt32LE(16)) continue;
75
+ const count = header.readUInt32LE(80);
76
+ const entrySize = header.readUInt32LE(84);
77
+ const table = readAt(fd, Number(header.readBigUInt64LE(72)) * SECTOR, count * entrySize);
78
+ if (crc32(table) !== header.readUInt32LE(88)) continue;
79
+ const entries = [];
80
+ for (let index = 0; index < count; index++) {
81
+ const entry = table.subarray(index * entrySize, (index + 1) * entrySize);
82
+ if (entry.subarray(0, 16).every((byte) => byte === 0)) continue;
83
+ const name = entry.subarray(56, 128).toString("utf16le").replace(/\0.*$/s, "");
84
+ entries.push({
85
+ number: index + 1,
86
+ type: guidText(entry, 0),
87
+ guid: guidText(entry, 16),
88
+ first: Number(entry.readBigUInt64LE(32)),
89
+ last: Number(entry.readBigUInt64LE(40)),
90
+ attributes: entry.readBigUInt64LE(48),
91
+ name,
92
+ });
93
+ }
94
+ return { diskGuid: guidText(header, 56), entries };
95
+ }
96
+ return null;
97
+ };
98
+
99
+ /** Writes the protective MBR and both copies of the table. */
100
+ export const writeGpt = (fd, sectors, { diskGuid, entries }) => {
101
+ const { first, last } = usable(sectors);
102
+ const table = Buffer.alloc(ENTRIES * ENTRY_SIZE);
103
+ for (const entry of entries) {
104
+ if (entry.number < 1 || entry.number > ENTRIES) throw new GptError(`partition number ${entry.number} out of range`);
105
+ const slot = table.subarray((entry.number - 1) * ENTRY_SIZE, entry.number * ENTRY_SIZE);
106
+ guidBytes(entry.type).copy(slot, 0);
107
+ guidBytes(entry.guid).copy(slot, 16);
108
+ slot.writeBigUInt64LE(BigInt(entry.first), 32);
109
+ slot.writeBigUInt64LE(BigInt(entry.last), 40);
110
+ slot.writeBigUInt64LE(BigInt(entry.attributes ?? 0n), 48);
111
+ const name = Buffer.from(entry.name ?? "", "utf16le");
112
+ if (name.length > 72) throw new GptError(`partition name too long: ${entry.name}`);
113
+ name.copy(slot, 56);
114
+ }
115
+ const tableCrc = crc32(table);
116
+ const header = (current, backup, tableLba) => {
117
+ const buffer = Buffer.alloc(SECTOR);
118
+ buffer.write("EFI PART", 0, "latin1");
119
+ buffer.writeUInt32LE(0x00010000, 8);
120
+ buffer.writeUInt32LE(92, 12);
121
+ buffer.writeBigUInt64LE(BigInt(current), 24);
122
+ buffer.writeBigUInt64LE(BigInt(backup), 32);
123
+ buffer.writeBigUInt64LE(BigInt(first), 40);
124
+ buffer.writeBigUInt64LE(BigInt(last), 48);
125
+ guidBytes(diskGuid).copy(buffer, 56);
126
+ buffer.writeBigUInt64LE(BigInt(tableLba), 72);
127
+ buffer.writeUInt32LE(ENTRIES, 80);
128
+ buffer.writeUInt32LE(ENTRY_SIZE, 84);
129
+ buffer.writeUInt32LE(tableCrc, 88);
130
+ buffer.writeUInt32LE(crc32(buffer.subarray(0, 92)), 16);
131
+ return buffer;
132
+ };
133
+ const mbr = readAt(fd, 0, SECTOR);
134
+ mbr.fill(0, 446, 510);
135
+ mbr.set([0x00, 0x00, 0x02, 0x00, 0xee, 0xff, 0xff, 0xff], 446);
136
+ mbr.writeUInt32LE(1, 454);
137
+ mbr.writeUInt32LE(Math.min(sectors - 1, 0xffffffff), 458);
138
+ mbr.writeUInt16LE(0xaa55, 510);
139
+ writeAt(fd, 0, mbr);
140
+ writeAt(fd, SECTOR, header(1, sectors - 1, 2));
141
+ writeAt(fd, 2 * SECTOR, table);
142
+ writeAt(fd, (sectors - 1 - ENTRY_SECTORS) * SECTOR, table);
143
+ writeAt(fd, (sectors - 1) * SECTOR, header(sectors - 1, 1, sectors - 1 - ENTRY_SECTORS));
144
+ };