buninu-linux 0.7.2 → 0.7.4

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.
@@ -115,6 +115,26 @@ function assetFilePath(key) {
115
115
  return assets ? assets.assetDiskPath(key) : path.join(__dirname, key);
116
116
  }
117
117
 
118
+ function windowsConptyDllPath() {
119
+ const dll = assetFilePath("conpty/conpty.dll");
120
+ if (fs.existsSync(dll)) return dll;
121
+ if (!assets) return null;
122
+ try {
123
+ const dllBytes = assets.readAssetBytesSync("conpty/conpty.dll");
124
+ const hostBytes = assets.readAssetBytesSync("conpty/OpenConsole.exe");
125
+ const version = crypto.createHash("sha256").update(dllBytes).digest("hex").slice(0, 16);
126
+ const dir = path.join(os.tmpdir(), `jsgotty-conpty-${version}`);
127
+ fs.mkdirSync(dir, { recursive: true });
128
+ for (const [name, bytes] of [["conpty.dll", dllBytes], ["OpenConsole.exe", hostBytes]]) {
129
+ const dest = path.join(dir, name);
130
+ if (!fs.existsSync(dest)) fs.writeFileSync(dest, bytes);
131
+ }
132
+ return path.join(dir, "conpty.dll");
133
+ } catch (error) {
134
+ throw new Error(`Could not prepare bundled ConPTY: ${error.message}`);
135
+ }
136
+ }
137
+
118
138
  // URL-relative path -> asset key, or null when it escapes `static/`.
119
139
  // Normalizing against "/" is what makes "../../etc/passwd" unreachable.
120
140
  function staticAssetKey(relativePath) {
@@ -1624,6 +1644,10 @@ function createPtyBackend(options) {
1624
1644
  if (globalThis.Bun?.semver?.satisfies?.(
1625
1645
  globalThis.Bun?.version, ">=1.3.14"
1626
1646
  )) {
1647
+ const conptyDll = windowsConptyDllPath();
1648
+ if (conptyDll) {
1649
+ return new (require("./windows-conpty.js"))(options, conptyDll);
1650
+ }
1627
1651
  return new BunPtyBackend(options);
1628
1652
  }
1629
1653
  return new NodePtyBackend(options);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "jsgotty",
3
- "version": "1.1.11",
3
+ "version": "1.1.14",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "jsgotty",
9
- "version": "1.1.11",
9
+ "version": "1.1.14",
10
10
  "dependencies": {
11
11
  "node-addon-api": "^7.1.1",
12
12
  "ws": "^8.18.3"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsgotty",
3
- "version": "1.1.12",
3
+ "version": "1.1.14",
4
4
  "description": "Share your terminal as a web application: A JavaScript rewrite of GoTTY",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -8,9 +8,13 @@
8
8
  },
9
9
  "type": "commonjs",
10
10
  "assets": [
11
+ "conpty",
11
12
  "static",
12
13
  "README.md",
13
- "README.en.md"
14
+ "README.en.md",
15
+ "LICENSE",
16
+ "NOTICE",
17
+ "LICENSES"
14
18
  ],
15
19
  "dependencies": {
16
20
  "node-addon-api": "^7.1.1",
@@ -0,0 +1,135 @@
1
+ // Windows PTY adapter for the out-of-band Microsoft Console package.
2
+ // Keep the same interface as BunPtyBackend in gotty.js.
3
+ const { dlopen, ptr } = require("bun:ffi");
4
+ const path = require("path");
5
+
6
+ const kernel = dlopen("kernel32.dll", {
7
+ CreatePipe: { args: ["ptr", "ptr", "ptr", "u32"], returns: "i32" },
8
+ InitializeProcThreadAttributeList: { args: ["ptr", "u32", "u32", "ptr"], returns: "i32" },
9
+ UpdateProcThreadAttribute: { args: ["ptr", "u32", "usize", "ptr", "usize", "ptr", "ptr"], returns: "i32" },
10
+ DeleteProcThreadAttributeList: { args: ["ptr"], returns: "void" },
11
+ CreateProcessW: { args: ["ptr", "ptr", "ptr", "ptr", "i32", "u32", "ptr", "ptr", "ptr", "ptr"], returns: "i32" },
12
+ PeekNamedPipe: { args: ["ptr", "ptr", "u32", "ptr", "ptr", "ptr"], returns: "i32" },
13
+ ReadFile: { args: ["ptr", "ptr", "u32", "ptr", "ptr"], returns: "i32" },
14
+ WriteFile: { args: ["ptr", "ptr", "u32", "ptr", "ptr"], returns: "i32" },
15
+ GetExitCodeProcess: { args: ["ptr", "ptr"], returns: "i32" },
16
+ TerminateProcess: { args: ["ptr", "u32"], returns: "i32" },
17
+ CloseHandle: { args: ["ptr"], returns: "i32" },
18
+ GetLastError: { args: [], returns: "u32" },
19
+ }).symbols;
20
+
21
+ const conptySymbols = {
22
+ ConptyCreatePseudoConsole: { args: ["u32", "ptr", "ptr", "u32", "ptr"], returns: "i32" },
23
+ ConptyResizePseudoConsole: { args: ["ptr", "u32"], returns: "i32" },
24
+ ConptyClosePseudoConsole: { args: ["ptr"], returns: "void" },
25
+ };
26
+ const wide = (value) => Buffer.from(`${value}\0`, "utf16le");
27
+ const handle = (buffer) => Number(buffer.readBigUInt64LE(0));
28
+ const size = (cols, rows) => ((Math.max(1, rows) & 0xffff) << 16) | (Math.max(1, cols) & 0xffff);
29
+ const winError = (operation) => new Error(`${operation} failed: ${kernel.GetLastError()}`);
30
+ // Windows CRT argv escaping: double backslashes before quotes and at the end.
31
+ function quoteArg(value) {
32
+ let result = '"', slashes = 0;
33
+ for (const char of String(value)) {
34
+ if (char === "\\") { slashes++; continue; }
35
+ if (char === '"') result += "\\".repeat(slashes * 2 + 1) + '"';
36
+ else result += "\\".repeat(slashes) + char;
37
+ slashes = 0;
38
+ }
39
+ return result + "\\".repeat(slashes * 2) + '"';
40
+ }
41
+
42
+ class WindowsConptyBackend {
43
+ constructor(options, dllPath) {
44
+ this.options = options;
45
+ this.dataHandlers = [];
46
+ this.exitHandlers = [];
47
+ this.closed = false;
48
+ this.conpty = dlopen(path.resolve(dllPath), conptySymbols).symbols;
49
+ const inputRead = Buffer.alloc(8), inputWrite = Buffer.alloc(8);
50
+ const outputRead = Buffer.alloc(8), outputWrite = Buffer.alloc(8);
51
+ if (!kernel.CreatePipe(inputRead, inputWrite, 0, 0)) throw winError("CreatePipe input");
52
+ if (!kernel.CreatePipe(outputRead, outputWrite, 0, 0)) throw winError("CreatePipe output");
53
+ this.input = handle(inputWrite);
54
+ this.output = handle(outputRead);
55
+ const pseudo = Buffer.alloc(8);
56
+ const hr = this.conpty.ConptyCreatePseudoConsole(
57
+ size(options.width || 80, options.height || 24), handle(inputRead), handle(outputWrite), 0, pseudo);
58
+ kernel.CloseHandle(handle(inputRead));
59
+ kernel.CloseHandle(handle(outputWrite));
60
+ if (hr !== 0) throw new Error(`ConptyCreatePseudoConsole failed: ${hr}`);
61
+ this.pseudo = handle(pseudo);
62
+
63
+ const listSize = Buffer.alloc(8);
64
+ kernel.InitializeProcThreadAttributeList(0, 1, 0, listSize);
65
+ const list = Buffer.alloc(Number(listSize.readBigUInt64LE(0)));
66
+ if (!kernel.InitializeProcThreadAttributeList(list, 1, 0, listSize)) throw winError("InitializeProcThreadAttributeList");
67
+ if (!kernel.UpdateProcThreadAttribute(list, 0, 0x00020016, this.pseudo, 8, 0, 0)) throw winError("UpdateProcThreadAttribute");
68
+ const startup = Buffer.alloc(112);
69
+ startup.writeUInt32LE(112, 0);
70
+ startup.writeUInt32LE(0x100, 60); // STARTF_USESTDHANDLES
71
+ startup.writeBigUInt64LE(BigInt(ptr(list)), 104);
72
+ const processInfo = Buffer.alloc(24);
73
+ const env = { ...process.env, TERM: "xterm-256color", ...(options.headerEnv || {}) };
74
+ const envBlock = wide(Object.entries(env).filter(([key]) => !key.startsWith("="))
75
+ .map(([key, value]) => `${key}=${value}`).join("\0") + "\0");
76
+ const executable = Bun.which(options.command) || options.command;
77
+ const command = wide([executable, ...options.argv].map(quoteArg).join(" "));
78
+ const ok = kernel.CreateProcessW(wide(executable), command, 0, 0, 0,
79
+ 0x00080000 | 0x00000400, envBlock, wide(process.cwd()), startup, processInfo);
80
+ kernel.DeleteProcThreadAttributeList(list);
81
+ if (!ok) throw winError("CreateProcessW");
82
+ this.process = handle(processInfo);
83
+ this.pid = processInfo.readUInt32LE(16);
84
+ kernel.CloseHandle(Number(processInfo.readBigUInt64LE(8)));
85
+ this.pollTimer = setInterval(() => this.poll(), 5);
86
+ }
87
+
88
+ onData(handler) { this.dataHandlers.push(handler); }
89
+ onExit(handler) { this.exitHandlers.push(handler); }
90
+ write(buffer) { this.writeBinary(buffer); }
91
+ writeBinary(buffer) {
92
+ const data = Buffer.from(buffer);
93
+ const written = Buffer.alloc(4);
94
+ for (let offset = 0; offset < data.length;) {
95
+ const chunk = data.subarray(offset);
96
+ if (!kernel.WriteFile(this.input, chunk, chunk.length, written, 0)) throw winError("WriteFile");
97
+ const count = written.readUInt32LE(0);
98
+ if (!count) throw new Error("WriteFile wrote no data");
99
+ offset += count;
100
+ }
101
+ }
102
+ resize(cols, rows) {
103
+ const hr = this.conpty.ConptyResizePseudoConsole(this.pseudo, size(cols, rows));
104
+ if (hr !== 0) throw new Error(`ConptyResizePseudoConsole failed: ${hr}`);
105
+ }
106
+ kill() { if (!this.closed) kernel.TerminateProcess(this.process, 1); }
107
+ close() { this.kill(); }
108
+
109
+ poll() {
110
+ if (this.closed) return;
111
+ const available = Buffer.alloc(4), readCount = Buffer.alloc(4);
112
+ if (kernel.PeekNamedPipe(this.output, 0, 0, 0, available, 0)) {
113
+ for (let bytes = available.readUInt32LE(0); bytes > 0; bytes = available.readUInt32LE(0)) {
114
+ const data = Buffer.alloc(Math.min(bytes, 65536));
115
+ if (!kernel.ReadFile(this.output, data, data.length, readCount, 0)) break;
116
+ const count = readCount.readUInt32LE(0);
117
+ if (!count) break;
118
+ for (const handler of this.dataHandlers) handler(data.subarray(0, count));
119
+ if (!kernel.PeekNamedPipe(this.output, 0, 0, 0, available, 0)) break;
120
+ }
121
+ }
122
+ const exitCode = Buffer.alloc(4);
123
+ if (kernel.GetExitCodeProcess(this.process, exitCode) && exitCode.readUInt32LE(0) !== 259) {
124
+ this.closed = true;
125
+ clearInterval(this.pollTimer);
126
+ this.conpty.ConptyClosePseudoConsole(this.pseudo);
127
+ kernel.CloseHandle(this.input);
128
+ kernel.CloseHandle(this.output);
129
+ kernel.CloseHandle(this.process);
130
+ for (const handler of this.exitHandlers) handler();
131
+ }
132
+ }
133
+ }
134
+
135
+ module.exports = WindowsConptyBackend;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu",
3
- "version": "0.4.17",
3
+ "version": "0.4.18",
4
4
  "description": "Buninu is a portable, self-bootstrapping Unix-like userspace built on Bun that runs across operating systems",
5
5
  "type": "module",
6
6
  "main": "./bin/init.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buninu-linux",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "description": "A Linux distribution running Bun as PID 1 with the Buninu userspace.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bun
2
+ // Writes the initramfs without fakeroot, cpio or gzip: a gzipped newc
3
+ // archive of a directory tree, plus device nodes that exist only in the
4
+ // archive. pack-initramfs.sh runs it instead of `fakeroot-tcp ... cpio |
5
+ // gzip` when BUNINU_JS_CPIO=1. Written against node: APIs only, so it runs
6
+ // under node as well as bun.
7
+ //
8
+ // scripts/cpio.js DIRECTORY OUTPUT.cpio.gz [PATH:c:MAJOR:MINOR:MODE]...
9
+ //
10
+ // e.g. dev/console:c:5:1:0600. Like `find . | sort | cpio --create
11
+ // --format=newc --owner=0:0 --reproducible` with the nodes made by mknod:
12
+ // every entry is owned by 0:0, inode numbers count up from 1 and device
13
+ // numbers of the containing filesystem are 0. Entries are sorted by name
14
+ // byte by byte, so the archive does not depend on the locale. mtimes are the
15
+ // files' own, or SOURCE_DATE_EPOCH for every entry when it is set; the
16
+ // device nodes get SOURCE_DATE_EPOCH or the current time.
17
+
18
+ import { lstatSync, readdirSync, readFileSync, readlinkSync, writeFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { constants, gzipSync } from "node:zlib";
21
+
22
+ const fail = (message) => {
23
+ console.error(`cpio.js: ${message}`);
24
+ process.exit(1);
25
+ };
26
+
27
+ const [directory, output, ...deviceSpecs] = process.argv.slice(2);
28
+ if (!directory || !output) fail("usage: cpio.js DIRECTORY OUTPUT.cpio.gz [PATH:c:MAJOR:MINOR:MODE]...");
29
+ const epoch = process.env.SOURCE_DATE_EPOCH === undefined ? null : Number(process.env.SOURCE_DATE_EPOCH);
30
+ if (epoch !== null && !Number.isInteger(epoch)) fail(`SOURCE_DATE_EPOCH is not a number: ${process.env.SOURCE_DATE_EPOCH}`);
31
+
32
+ const S_IFMT = 0o170000;
33
+ const S_IFDIR = 0o040000;
34
+ const S_IFREG = 0o100000;
35
+ const S_IFLNK = 0o120000;
36
+ const S_IFCHR = 0o020000;
37
+ const S_IFBLK = 0o060000;
38
+
39
+ // Every path under the directory, relative to it.
40
+ const entries = [];
41
+ const walk = (relative) => {
42
+ for (const name of readdirSync(join(directory, relative))) {
43
+ const path = relative ? `${relative}/${name}` : name;
44
+ const stat = lstatSync(join(directory, path));
45
+ entries.push({ path, stat });
46
+ if (stat.isDirectory()) walk(path);
47
+ }
48
+ };
49
+ walk("");
50
+
51
+ const now = Math.floor(Date.now() / 1000);
52
+ const existing = new Set(entries.map(({ path }) => path));
53
+ for (const spec of deviceSpecs) {
54
+ const [path, kind, major, minor, mode] = spec.split(":");
55
+ if (!path || !["c", "b"].includes(kind) || !/^\d+$/.test(major) || !/^\d+$/.test(minor) || !/^[0-7]{3,4}$/.test(mode)) {
56
+ fail(`bad device ${spec}; expected PATH:c:MAJOR:MINOR:MODE`);
57
+ }
58
+ if (existing.has(path)) fail(`${path} already exists in ${directory}`);
59
+ const parent = path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "";
60
+ if (parent && !existing.has(parent)) fail(`${path}: no directory ${parent} in ${directory}`);
61
+ entries.push({
62
+ path,
63
+ device: { mode: (kind === "c" ? S_IFCHR : S_IFBLK) | parseInt(mode, 8), major: Number(major), minor: Number(minor) },
64
+ });
65
+ existing.add(path);
66
+ }
67
+ entries.sort((a, b) => Buffer.compare(Buffer.from(a.path), Buffer.from(b.path)));
68
+
69
+ const chunks = [];
70
+ const hex = (value) => (value >>> 0).toString(16).padStart(8, "0");
71
+ const pad = (length) => { if (length % 4) chunks.push(Buffer.alloc(4 - (length % 4))); };
72
+ const add = (name, { ino, mode, nlink, mtime, data, rdevMajor = 0, rdevMinor = 0 }) => {
73
+ const nameBytes = Buffer.from(`${name}\0`);
74
+ // magic, ino, mode, uid, gid, nlink, mtime, filesize, devmajor, devminor,
75
+ // rdevmajor, rdevminor, namesize, check
76
+ const header = Buffer.from("070701" + [ino, mode, 0, 0, nlink, mtime, data.length, 0, 0, rdevMajor, rdevMinor, nameBytes.length, 0]
77
+ .map(hex).join(""), "latin1");
78
+ chunks.push(header, nameBytes);
79
+ pad(header.length + nameBytes.length);
80
+ chunks.push(data);
81
+ pad(data.length);
82
+ };
83
+
84
+ entries.forEach(({ path, stat, device }, index) => {
85
+ const ino = index + 1;
86
+ if (device) {
87
+ add(path, { ino, mode: device.mode, nlink: 1, mtime: epoch ?? now, data: Buffer.alloc(0), rdevMajor: device.major, rdevMinor: device.minor });
88
+ return;
89
+ }
90
+ const type = stat.mode & S_IFMT;
91
+ const mtime = epoch ?? Math.floor(stat.mtimeMs / 1000);
92
+ let data = Buffer.alloc(0);
93
+ let nlink = 1;
94
+ if (type === S_IFREG) data = readFileSync(join(directory, path));
95
+ else if (type === S_IFLNK) data = Buffer.from(readlinkSync(join(directory, path)));
96
+ else if (type === S_IFDIR) nlink = stat.nlink;
97
+ else fail(`${path}: unsupported file type ${type.toString(8)}; pass device nodes as PATH:c:MAJOR:MINOR:MODE`);
98
+ add(path, { ino, mode: stat.mode, nlink, mtime, data });
99
+ });
100
+ add("TRAILER!!!", { ino: 0, mode: 0, nlink: 1, mtime: 0, data: Buffer.alloc(0) });
101
+ // GNU cpio pads the archive to a 512-byte block.
102
+ const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
103
+ if (total % 512) chunks.push(Buffer.alloc(512 - (total % 512)));
104
+
105
+ // Like `gzip -9n`: zlib leaves the name out and writes 0 as the mtime.
106
+ writeFileSync(output, gzipSync(Buffer.concat(chunks), { level: constants.Z_BEST_COMPRESSION }));
107
+ console.log(`cpio.js: ${output}: ${entries.length} entries`);
@@ -50,6 +50,22 @@ mkdir -p "$staging/usr/share/icons/hicolor/512x512/apps" "$staging/usr/share/pix
50
50
  ln -sfn ../../../../../../buninu/icon.png "$staging/usr/share/icons/hicolor/512x512/apps/buninu-linux.png"
51
51
  ln -sfn ../../../buninu/icon.png "$staging/usr/share/pixmaps/buninu-linux.png"
52
52
 
53
+ # BUNINU_JS_CPIO=1 writes the archive with scripts/cpio.js (bun or node)
54
+ # instead of fakeroot, cpio and gzip: the device nodes below exist only in
55
+ # the archive, so nothing needs root.
56
+ if [ "${BUNINU_JS_CPIO:-}" = 1 ]; then
57
+ js=$(command -v bun || command -v node) || {
58
+ echo "error: BUNINU_JS_CPIO=1 needs bun or node on PATH" >&2
59
+ exit 1
60
+ }
61
+ "$js" scripts/cpio.js "$staging" build/initramfs.cpio.gz \
62
+ dev/console:c:5:1:0600 "dev/$serial_console:c:$serial_major:64:0620" \
63
+ dev/null:c:1:3:0666 dev/zero:c:1:5:0666 dev/random:c:1:8:0666 \
64
+ dev/urandom:c:1:9:0666 dev/tty:c:5:0:0666
65
+ echo "Built build/initramfs.cpio.gz for $arch (scripts/cpio.js)"
66
+ exit 0
67
+ fi
68
+
53
69
  # fakeroot lets cpio record the character devices without real root. Nothing
54
70
  # has mounted devtmpfs when the kernel execs Bun as PID 1, so every device
55
71
  # Bun's startup needs has to be in the archive itself:
package/scripts/uki.js ADDED
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env bun
2
+ // Assembles a UKI without binutils: the systemd EFI stub (a PE32+ image,
3
+ // x86_64 or aarch64) with .osrel, .cmdline, .linux and .initrd added as
4
+ // read-only data sections. build-uki.sh runs it instead of objdump/objcopy
5
+ // when BUNINU_JS_UKI=1. Written from the PE/COFF specification against
6
+ // node: APIs only, so it runs under node as well as bun.
7
+ //
8
+ // scripts/uki.js STUB OUTPUT .osrel=FILE .cmdline=FILE .linux=FILE .initrd=FILE
9
+ //
10
+ // The section addresses follow build-uki.sh: the conventional offsets from
11
+ // the image base (.osrel +0x20000, .cmdline +0x30000, .linux +0x2000000,
12
+ // .initrd +0x3000000), moved up when the stub or the kernel is larger. The
13
+ // stub's own headers and sections are kept where they are; the new sections
14
+ // are appended to the file. TimeDateStamp is SOURCE_DATE_EPOCH when set,
15
+ // otherwise the stub's own, so the output depends only on the inputs.
16
+
17
+ import { readFileSync, writeFileSync } from "node:fs";
18
+
19
+ const fail = (message) => {
20
+ console.error(`uki.js: ${message}`);
21
+ process.exit(1);
22
+ };
23
+
24
+ const [stubPath, outputPath, ...pairs] = process.argv.slice(2);
25
+ if (!stubPath || !outputPath) fail("usage: uki.js STUB OUTPUT .osrel=FILE .cmdline=FILE .linux=FILE .initrd=FILE");
26
+ const files = new Map();
27
+ for (const pair of pairs) {
28
+ const separator = pair.indexOf("=");
29
+ if (separator < 1) fail(`expected .section=FILE, got ${pair}`);
30
+ files.set(pair.slice(0, separator), pair.slice(separator + 1));
31
+ }
32
+ const order = [".osrel", ".cmdline", ".linux", ".initrd"];
33
+ for (const name of order) if (!files.has(name)) fail(`missing ${name}=FILE`);
34
+ if (files.size !== order.length) fail(`only ${order.join(", ")} are supported`);
35
+
36
+ const stub = readFileSync(stubPath);
37
+ if (stub.toString("latin1", 0, 2) !== "MZ") fail(`${stubPath} is not a PE image`);
38
+ const pe = stub.readUInt32LE(0x3c);
39
+ if (stub.toString("latin1", pe, pe + 4) !== "PE\0\0") fail(`${stubPath} is not a PE image`);
40
+ const coff = pe + 4;
41
+ const sectionCount = stub.readUInt16LE(coff + 2);
42
+ const optionalSize = stub.readUInt16LE(coff + 16);
43
+ const optional = coff + 20;
44
+ if (stub.readUInt16LE(optional) !== 0x20b) fail(`${stubPath} is not PE32+`);
45
+ if (stub.readUInt32LE(coff + 8) !== 0) fail(`${stubPath} has a COFF symbol table, which is not supported`);
46
+ const imageBase = stub.readBigUInt64LE(optional + 24);
47
+ const sectionAlignment = stub.readUInt32LE(optional + 32);
48
+ const fileAlignment = stub.readUInt32LE(optional + 36);
49
+ const stubImageSize = stub.readUInt32LE(optional + 56);
50
+ const headersSize = stub.readUInt32LE(optional + 60);
51
+ const directoryCount = stub.readUInt32LE(optional + 108);
52
+ // A signature (the certificate table) covers the file up to its end;
53
+ // appending after it would invalidate it.
54
+ if (directoryCount > 4 && stub.readUInt32LE(optional + 112 + 4 * 8 + 4) !== 0) {
55
+ fail(`${stubPath} is signed; adding sections would break the signature`);
56
+ }
57
+
58
+ const table = optional + optionalSize;
59
+ let firstData = stub.length;
60
+ for (let index = 0; index < sectionCount; index++) {
61
+ const header = table + index * 40;
62
+ const raw = stub.readUInt32LE(header + 20);
63
+ if (stub.readUInt32LE(header + 16) && raw) firstData = Math.min(firstData, raw);
64
+ }
65
+ const tableEnd = table + (sectionCount + order.length) * 40;
66
+ if (tableEnd > Math.min(headersSize, firstData)) {
67
+ fail(`no room in ${stubPath}'s headers for ${order.length} more section headers`);
68
+ }
69
+
70
+ const alignUp = (value, alignment) => Math.ceil(value / alignment) * alignment;
71
+ const data = order.map((name) => readFileSync(files.get(name)));
72
+ const [, , kernel] = data;
73
+ const osrel = Math.max(0x20000, alignUp(stubImageSize, 0x10000));
74
+ const cmdline = osrel + 0x10000;
75
+ const linux = Math.max(0x2000000, alignUp(cmdline + 0x10000, 0x1000000));
76
+ const initrd = Math.max(0x3000000, alignUp(linux + kernel.length, 0x1000000));
77
+ const addresses = [osrel, cmdline, linux, initrd];
78
+ if (data[0].length > 0x10000 || data[1].length > 0x10000) fail(".osrel and .cmdline must each fit in 64 KiB");
79
+
80
+ // Section contents go after everything already in the file, each at a
81
+ // FileAlignment boundary and padded to a multiple of it.
82
+ let offset = alignUp(stub.length, fileAlignment);
83
+ const pointers = data.map((bytes) => {
84
+ const pointer = offset;
85
+ offset += alignUp(bytes.length, fileAlignment);
86
+ return pointer;
87
+ });
88
+ const output = Buffer.alloc(offset);
89
+ stub.copy(output);
90
+ order.forEach((name, index) => {
91
+ const header = table + (sectionCount + index) * 40;
92
+ const bytes = data[index];
93
+ output.fill(0, header, header + 40);
94
+ output.write(name, header, 8, "latin1");
95
+ output.writeUInt32LE(bytes.length, header + 8); // VirtualSize
96
+ output.writeUInt32LE(addresses[index], header + 12); // VirtualAddress
97
+ output.writeUInt32LE(alignUp(bytes.length, fileAlignment), header + 16); // SizeOfRawData
98
+ output.writeUInt32LE(pointers[index], header + 20); // PointerToRawData
99
+ // IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ: objcopy's
100
+ // contents,alloc,load,readonly,data.
101
+ output.writeUInt32LE(0x40000040, header + 36);
102
+ bytes.copy(output, pointers[index]);
103
+ });
104
+ output.writeUInt16LE(sectionCount + order.length, coff + 2);
105
+ // SizeOfCode, SizeOfInitializedData and SizeOfUninitializedData: the raw
106
+ // sizes of the sections of each kind, recomputed as objcopy does.
107
+ const sizes = [0, 0, 0];
108
+ for (let index = 0; index < sectionCount + order.length; index++) {
109
+ const header = table + index * 40;
110
+ const characteristics = output.readUInt32LE(header + 36);
111
+ const rawSize = output.readUInt32LE(header + 16);
112
+ [0x20, 0x40, 0x80].forEach((flag, kind) => { if (characteristics & flag) sizes[kind] += rawSize; });
113
+ }
114
+ sizes.forEach((size, kind) => output.writeUInt32LE(size >>> 0, optional + 4 + kind * 4));
115
+ output.writeUInt32LE(alignUp(initrd + data[3].length, sectionAlignment), optional + 56); // SizeOfImage
116
+ if (process.env.SOURCE_DATE_EPOCH !== undefined) {
117
+ output.writeUInt32LE(Number(process.env.SOURCE_DATE_EPOCH) >>> 0, coff + 4);
118
+ }
119
+
120
+ // The PE checksum: 16-bit one's-complement-style sum of the file with the
121
+ // CheckSum field itself taken as zero, plus the file length.
122
+ output.writeUInt32LE(0, optional + 64);
123
+ let sum = 0;
124
+ for (let index = 0; index + 1 < output.length; index += 2) {
125
+ sum += output.readUInt16LE(index);
126
+ sum = (sum & 0xffff) + (sum >>> 16);
127
+ }
128
+ if (output.length & 1) {
129
+ sum += output[output.length - 1];
130
+ sum = (sum & 0xffff) + (sum >>> 16);
131
+ }
132
+ output.writeUInt32LE(((sum & 0xffff) + output.length) >>> 0, optional + 64);
133
+
134
+ writeFileSync(outputPath, output);
135
+ const hex = (value) => `0x${value.toString(16)}`;
136
+ console.log(`uki.js: ${outputPath}: image base ${hex(imageBase)}, ${order.map((name, index) => `${name} +${hex(addresses[index])}`).join(", ")}`);