@gjsify/cli 0.44.0 → 0.46.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/dist/cli.gjs.mjs +202 -167
- package/lib/commands/gettext.d.ts +2 -1
- package/lib/commands/gettext.js +105 -47
- package/lib/commands/onboard.d.ts +7 -0
- package/lib/commands/onboard.js +349 -96
- package/lib/commands/ship.d.ts +3 -0
- package/lib/commands/ship.js +850 -58
- package/lib/commands/trust.d.ts +6 -0
- package/lib/commands/trust.js +1 -1
- package/lib/commands/upgrade.d.ts +23 -0
- package/lib/commands/upgrade.js +95 -12
- package/lib/templates/install.mjs.tmpl +2 -2
- package/lib/types/config-data.d.ts +127 -1
- package/lib/utils/gi-search-path.js +11 -3
- package/lib/utils/msgfmt-merge.d.ts +35 -0
- package/lib/utils/msgfmt-merge.js +102 -0
- package/lib/utils/npm-otp.d.ts +27 -2
- package/lib/utils/npm-otp.js +82 -7
- package/lib/utils/onboard-discovery.d.ts +68 -0
- package/lib/utils/onboard-discovery.js +178 -0
- package/lib/utils/onboard-probe.d.ts +118 -1
- package/lib/utils/onboard-probe.js +153 -4
- package/lib/utils/prompt-output.d.ts +12 -0
- package/lib/utils/prompt-output.js +54 -0
- package/lib/utils/prompt.js +22 -2
- package/lib/utils/publishable-packages.d.ts +1 -1
- package/lib/utils/publishable-packages.js +1 -0
- package/lib/utils/resolve-npm-package.d.ts +56 -0
- package/lib/utils/resolve-npm-package.js +69 -0
- package/lib/utils/ship/app-runtime.d.ts +214 -0
- package/lib/utils/ship/app-runtime.js +429 -0
- package/lib/utils/ship/depends.d.ts +50 -0
- package/lib/utils/ship/depends.js +117 -3
- package/lib/utils/ship/discover.js +103 -1
- package/lib/utils/ship/dmg.d.ts +71 -0
- package/lib/utils/ship/dmg.js +181 -0
- package/lib/utils/ship/flatpak.js +1 -1
- package/lib/utils/ship/formats.d.ts +108 -7
- package/lib/utils/ship/formats.js +778 -15
- package/lib/utils/ship/launcher.d.ts +13 -3
- package/lib/utils/ship/launcher.js +419 -17
- package/lib/utils/ship/layout.d.ts +330 -0
- package/lib/utils/ship/layout.js +504 -0
- package/lib/utils/ship/localize-metadata.d.ts +22 -0
- package/lib/utils/ship/localize-metadata.js +126 -0
- package/lib/utils/ship/mime.js +1 -9
- package/lib/utils/ship/msi.d.ts +126 -0
- package/lib/utils/ship/msi.js +434 -0
- package/lib/utils/ship/node-runtime.d.ts +71 -0
- package/lib/utils/ship/node-runtime.js +122 -0
- package/lib/utils/ship/payload.d.ts +156 -5
- package/lib/utils/ship/payload.js +599 -14
- package/lib/utils/ship/pe-launcher.d.ts +29 -0
- package/lib/utils/ship/pe-launcher.js +663 -0
- package/lib/utils/ship/plan.d.ts +21 -0
- package/lib/utils/ship/plan.js +55 -7
- package/lib/utils/ship/plist.d.ts +98 -0
- package/lib/utils/ship/plist.js +256 -0
- package/lib/utils/ship/rpm.js +16 -3
- package/lib/utils/ship/schemas.d.ts +61 -0
- package/lib/utils/ship/schemas.js +118 -0
- package/lib/utils/ship/scripts.js +7 -3
- package/lib/utils/ship/settings.d.ts +91 -2
- package/lib/utils/ship/settings.js +93 -1
- package/lib/utils/ship/share-dirs.d.ts +53 -0
- package/lib/utils/ship/share-dirs.js +76 -0
- package/lib/utils/ship/signing.d.ts +311 -0
- package/lib/utils/ship/signing.js +506 -0
- package/lib/utils/ship/stage-manifest.d.ts +40 -12
- package/lib/utils/ship/stage-manifest.js +99 -13
- package/lib/utils/ship/stage-writer.d.ts +48 -0
- package/lib/utils/ship/stage-writer.js +91 -2
- package/lib/utils/ship/types.d.ts +191 -14
- package/lib/utils/ship/xml.d.ts +7 -0
- package/lib/utils/ship/xml.js +25 -0
- package/lib/utils/ship/zip.d.ts +69 -0
- package/lib/utils/ship/zip.js +245 -0
- package/lib/utils/system-gi.d.ts +20 -5
- package/lib/utils/system-gi.js +36 -13
- package/lib/utils/trust-registry.js +5 -0
- package/package.json +19 -19
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
// The GUI-subsystem launcher `gjsify ship windows` writes for itself.
|
|
2
|
+
//
|
|
3
|
+
// THE DEFECT IT CLOSES. `node.exe` is an `IMAGE_SUBSYSTEM_WINDOWS_CUI` image —
|
|
4
|
+
// `Subsystem` = 3, measured at offset 0xd4 of `node-v24.20.0-win-x64.zip`'s
|
|
5
|
+
// `node.exe` — and the Node release carries no `nodew.exe` to put in its place.
|
|
6
|
+
// The staged launcher is a `.cmd`, which `cmd.exe` runs, and `cmd.exe` is a
|
|
7
|
+
// console image too. So Windows allocates a console for every double-click and
|
|
8
|
+
// every installer shortcut, and a black window sits behind the app for its whole
|
|
9
|
+
// life. ADR 0024 § M3 records that NO CI leg can see this: every Windows job
|
|
10
|
+
// starts the app from a shell and therefore already has a console to inherit.
|
|
11
|
+
//
|
|
12
|
+
// WHY THIS FILE EMITS THE IMAGE INSTEAD OF SHIPPING ONE. The same argument
|
|
13
|
+
// `deb.ts`, `rpm.ts`, `cpio.ts`, `ar.ts`, `zip.ts` and `msi.ts` already make one
|
|
14
|
+
// directory over, with one addition that is specific to a PROGRAM:
|
|
15
|
+
//
|
|
16
|
+
// * a prebuilt stub is a binary in git, and a binary in git needs a toolchain
|
|
17
|
+
// to reproduce it (there is no MSVC and no mingw on the machine that
|
|
18
|
+
// assembles Windows artifacts — ADR 0024 § A1: assembly happens on Linux),
|
|
19
|
+
// a licence row of its own, and a gate proving the committed bytes are the
|
|
20
|
+
// ones that source produces. Emitting the image needs none of the three: the
|
|
21
|
+
// bytes are a pure function of this file, so `pe-launcher.spec.ts` reads them
|
|
22
|
+
// back on any host.
|
|
23
|
+
// * the alternative that needs no new image at all — patching a copy of
|
|
24
|
+
// `node.exe`'s `Subsystem` from 3 to 2 — does not solve the problem it looks
|
|
25
|
+
// like it solves. The console is allocated for `cmd.exe`, before `node.exe`
|
|
26
|
+
// is started at all, so the field that decides it is `cmd.exe`'s and not
|
|
27
|
+
// ours. It also silently invalidates node.exe's Authenticode signature and
|
|
28
|
+
// makes a modified redistributable of somebody else's binary.
|
|
29
|
+
//
|
|
30
|
+
// WHAT IT DOES NOT THROW AWAY. A GUI-subsystem image is not a detached one:
|
|
31
|
+
// Windows allocates a console only when there is none to INHERIT, so this stub
|
|
32
|
+
// started from PowerShell or `cmd.exe` runs inside the console it was started
|
|
33
|
+
// from and every byte the app writes to stdout/stderr lands in that terminal,
|
|
34
|
+
// exactly as the `.cmd` does today. The Explorer path is the one with nowhere to
|
|
35
|
+
// write, and it is the one this stub redirects: with no console, the child is
|
|
36
|
+
// created `CREATE_NO_WINDOW` with its stdout and stderr pointing at
|
|
37
|
+
// `%TEMP%\<logLeaf>`, so an app that dies in its first second leaves a stack
|
|
38
|
+
// trace behind instead of a window that never appeared. Losing the output was the
|
|
39
|
+
// obvious design and it is the one that makes an unlaunchable app undiagnosable.
|
|
40
|
+
//
|
|
41
|
+
// IT RUNS THE `.cmd`, IT DOES NOT REPLACE IT. The stub derives its own path,
|
|
42
|
+
// rewrites the trailing `exe` to `cmd` and runs THAT — so every decision the
|
|
43
|
+
// launcher makes (the prefix, `XDG_DATA_DIRS`, `GJSIFY_GTK_RUNTIME`,
|
|
44
|
+
// `NODE_GI_NATIVE`, `GI_TYPELIB_PATH`, `PATH`, the interpreter) stays in
|
|
45
|
+
// `launcher.ts` where `assertLauncherMatchesInterpreter` and
|
|
46
|
+
// `.github/ship-oracle/verify-program-dir.py` already read it. A second copy of
|
|
47
|
+
// that logic in machine code is a second truth, and it is the copy nobody can
|
|
48
|
+
// read.
|
|
49
|
+
//
|
|
50
|
+
// THE ENCODING IS THE RISKY PART AND IT IS HELD BY A READER THAT IS NOT OURS:
|
|
51
|
+
// `objdump -x` (binutils' `pei-x86-64`) and `.github/ship-oracle/verify-program-dir.py`
|
|
52
|
+
// both parse the emitted file, and `tests/e2e/ship-windows` drives the emitted
|
|
53
|
+
// image through the same `readBinaryArch` that reads a real `node.exe`.
|
|
54
|
+
/**
|
|
55
|
+
* `IMAGE_SUBSYSTEM_WINDOWS_GUI` — the one field this whole file exists to set.
|
|
56
|
+
*
|
|
57
|
+
* The counterpart is 3, `_CUI`, which is what `node.exe` and `cmd.exe` are and
|
|
58
|
+
* what makes Windows allocate a console when there is none to inherit.
|
|
59
|
+
*/
|
|
60
|
+
export const PE_SUBSYSTEM_GUI = 2;
|
|
61
|
+
/** Where `Subsystem` lands in the emitted image: `e_lfanew` + 4 + 20 + 68. */
|
|
62
|
+
export const PE_SUBSYSTEM_OFFSET = 0x80 + 4 + 20 + 68;
|
|
63
|
+
/** `IMAGE_FILE_HEADER.Machine` for x86-64. The only architecture the windows layout has. */
|
|
64
|
+
const MACHINE_AMD64 = 0x8664;
|
|
65
|
+
/** Where the PE signature starts. MSVC emits 0x78; nothing reads the value except through `e_lfanew`. */
|
|
66
|
+
const PE_OFFSET = 0x80;
|
|
67
|
+
const FILE_ALIGNMENT = 0x200;
|
|
68
|
+
const SECTION_ALIGNMENT = 0x1000;
|
|
69
|
+
const IMAGE_BASE = 0x140000000n;
|
|
70
|
+
/**
|
|
71
|
+
* The kernel32 entry points the stub calls, in the order its IAT lists them.
|
|
72
|
+
*
|
|
73
|
+
* ONE dll, and every name is a documented kernel32 export — including
|
|
74
|
+
* `GetConsoleCP` (which is what decides whether the child may be given a console
|
|
75
|
+
* of its own) and the two `lstr*` string helpers, which is why the stub needs no
|
|
76
|
+
* CRT and therefore carries nobody's licence but this repository's.
|
|
77
|
+
*/
|
|
78
|
+
const IMPORTS = [
|
|
79
|
+
'GetModuleFileNameW',
|
|
80
|
+
'GetSystemDirectoryW',
|
|
81
|
+
'GetCommandLineW',
|
|
82
|
+
'GetConsoleCP',
|
|
83
|
+
'GetStdHandle',
|
|
84
|
+
'GetTempPathW',
|
|
85
|
+
'CreateFileW',
|
|
86
|
+
'CreateProcessW',
|
|
87
|
+
'WaitForSingleObject',
|
|
88
|
+
'GetExitCodeProcess',
|
|
89
|
+
'ExitProcess',
|
|
90
|
+
'lstrcpyW',
|
|
91
|
+
'lstrcatW',
|
|
92
|
+
];
|
|
93
|
+
/**
|
|
94
|
+
* A byte buffer with named positions and deferred 32-bit fixups.
|
|
95
|
+
*
|
|
96
|
+
* Everything this file emits is either an absolute value known at layout time or
|
|
97
|
+
* a RIP-relative displacement that is not — so the assembler records the second
|
|
98
|
+
* kind and patches it once the data section's addresses exist. Fixed instruction
|
|
99
|
+
* lengths (every displacement is a `disp32`, every branch a `rel32`) are what
|
|
100
|
+
* make one patching pass enough.
|
|
101
|
+
*/
|
|
102
|
+
class Emitter {
|
|
103
|
+
bytes = [];
|
|
104
|
+
labels = new Map();
|
|
105
|
+
ripFixups = [];
|
|
106
|
+
branchFixups = [];
|
|
107
|
+
push(...values) {
|
|
108
|
+
for (const value of values)
|
|
109
|
+
this.bytes.push(value & 0xff);
|
|
110
|
+
}
|
|
111
|
+
label(name) {
|
|
112
|
+
this.labels.set(name, this.bytes.length);
|
|
113
|
+
}
|
|
114
|
+
/** A `disp32` that will be `symbolRva - (ripAfterThisInstruction)`. */
|
|
115
|
+
ripSlot(symbol) {
|
|
116
|
+
this.ripFixups.push({ at: this.bytes.length, symbol });
|
|
117
|
+
this.push(0, 0, 0, 0);
|
|
118
|
+
}
|
|
119
|
+
branchSlot(label) {
|
|
120
|
+
this.branchFixups.push({ at: this.bytes.length, label });
|
|
121
|
+
this.push(0, 0, 0, 0);
|
|
122
|
+
}
|
|
123
|
+
/** `lea <reg>, [rip + symbol]` — the ModRM byte differs, the REX prefix carries the high bit. */
|
|
124
|
+
leaRip(reg, symbol) {
|
|
125
|
+
const modrm = { rax: 0x05, rcx: 0x0d, rdx: 0x15, rdi: 0x3d, r8: 0x05, r9: 0x0d }[reg];
|
|
126
|
+
this.push(reg === 'r8' || reg === 'r9' ? 0x4c : 0x48, 0x8d, modrm);
|
|
127
|
+
this.ripSlot(symbol);
|
|
128
|
+
}
|
|
129
|
+
/** `call qword ptr [rip + symbol]` — an indirect call through the IAT slot. */
|
|
130
|
+
callImport(name) {
|
|
131
|
+
this.push(0xff, 0x15);
|
|
132
|
+
this.ripSlot(`iat.${name}`);
|
|
133
|
+
}
|
|
134
|
+
jmp(label) {
|
|
135
|
+
this.push(0xe9);
|
|
136
|
+
this.branchSlot(label);
|
|
137
|
+
}
|
|
138
|
+
/** `jcc rel32`, spelled by the two-byte form so no branch is ever out of range. */
|
|
139
|
+
jcc(condition, label) {
|
|
140
|
+
this.push(0x0f, { e: 0x84, ne: 0x85, b: 0x82 }[condition]);
|
|
141
|
+
this.branchSlot(label);
|
|
142
|
+
}
|
|
143
|
+
/** Resolve every recorded fixup. `symbolRva` answers for data, the label map for code. */
|
|
144
|
+
resolve(codeRva, symbolRva) {
|
|
145
|
+
const out = Buffer.from(this.bytes);
|
|
146
|
+
for (const { at, symbol } of this.ripFixups) {
|
|
147
|
+
// The displacement is relative to the END of the instruction, which is
|
|
148
|
+
// four bytes past the slot itself — the one rule about RIP-relative
|
|
149
|
+
// addressing that is not visible in the encoding.
|
|
150
|
+
out.writeInt32LE(symbolRva(symbol) - (codeRva + at + 4), at);
|
|
151
|
+
}
|
|
152
|
+
for (const { at, label } of this.branchFixups) {
|
|
153
|
+
const target = this.labels.get(label);
|
|
154
|
+
if (target === undefined)
|
|
155
|
+
throw new Error(`gjsify ship: the launcher stub jumps to no label "${label}".`);
|
|
156
|
+
out.writeInt32LE(target - (at + 4), at);
|
|
157
|
+
}
|
|
158
|
+
return new Uint8Array(out);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** `mov <reg32>, imm32`. */
|
|
162
|
+
function movImm32(emit, reg, value) {
|
|
163
|
+
const opcode = { eax: 0xb8, ecx: 0xb9, edx: 0xba, r8d: 0xb8, r9d: 0xb9, r14d: 0xbe }[reg];
|
|
164
|
+
if (reg === 'r8d' || reg === 'r9d' || reg === 'r14d')
|
|
165
|
+
emit.push(0x41);
|
|
166
|
+
emit.push(opcode);
|
|
167
|
+
const buffer = Buffer.alloc(4);
|
|
168
|
+
buffer.writeInt32LE(value | 0);
|
|
169
|
+
emit.push(...buffer);
|
|
170
|
+
}
|
|
171
|
+
/** `mov dword ptr [rsp + disp32], imm32`. */
|
|
172
|
+
function movStackDword(emit, offset, value) {
|
|
173
|
+
emit.push(0xc7, 0x84, 0x24);
|
|
174
|
+
const displacement = Buffer.alloc(4);
|
|
175
|
+
displacement.writeInt32LE(offset);
|
|
176
|
+
emit.push(...displacement);
|
|
177
|
+
const immediate = Buffer.alloc(4);
|
|
178
|
+
immediate.writeInt32LE(value | 0);
|
|
179
|
+
emit.push(...immediate);
|
|
180
|
+
}
|
|
181
|
+
/** `mov qword ptr [rsp + disp32], <reg>`. */
|
|
182
|
+
function movStackFromReg(emit, reg, offset) {
|
|
183
|
+
const modrm = { rax: 0x84, rbx: 0x9c, rsi: 0xb4, rdi: 0xbc }[reg];
|
|
184
|
+
emit.push(0x48, 0x89, modrm, 0x24);
|
|
185
|
+
const displacement = Buffer.alloc(4);
|
|
186
|
+
displacement.writeInt32LE(offset);
|
|
187
|
+
emit.push(...displacement);
|
|
188
|
+
}
|
|
189
|
+
/** `mov rax, qword ptr [rsp + disp32]`. */
|
|
190
|
+
function movRaxFromStack(emit, offset) {
|
|
191
|
+
emit.push(0x48, 0x8b, 0x84, 0x24);
|
|
192
|
+
const displacement = Buffer.alloc(4);
|
|
193
|
+
displacement.writeInt32LE(offset);
|
|
194
|
+
emit.push(...displacement);
|
|
195
|
+
}
|
|
196
|
+
/** `lea <reg>, [rsp + disp32]`. */
|
|
197
|
+
function leaStack(emit, reg, offset) {
|
|
198
|
+
const modrm = { rax: 0x84, rcx: 0x8c, rdx: 0x94, rdi: 0xbc, r9: 0x8c }[reg];
|
|
199
|
+
emit.push(reg === 'r9' ? 0x4c : 0x48, 0x8d, modrm, 0x24);
|
|
200
|
+
const displacement = Buffer.alloc(4);
|
|
201
|
+
displacement.writeInt32LE(offset);
|
|
202
|
+
emit.push(...displacement);
|
|
203
|
+
}
|
|
204
|
+
// The stack frame, as offsets from `rsp` after the prologue. Named because the
|
|
205
|
+
// three Win32 structures below are addressed by hand and a wrong offset is a
|
|
206
|
+
// silent argument, not a compile error.
|
|
207
|
+
const FRAME = {
|
|
208
|
+
/** Shadow space plus the six stack arguments `CreateProcessW` takes. */
|
|
209
|
+
args: 0x20,
|
|
210
|
+
/** `SECURITY_ATTRIBUTES` — 24 bytes, `bInheritHandle` at +0x10. */
|
|
211
|
+
securityAttributes: 0x50,
|
|
212
|
+
/** `STARTUPINFOW` — 104 bytes; `cb` at +0, `dwFlags` at +0x3c, the three handles at +0x50. */
|
|
213
|
+
startupInfo: 0x70,
|
|
214
|
+
/** `PROCESS_INFORMATION` — 24 bytes; `hProcess` first. */
|
|
215
|
+
processInformation: 0xd8,
|
|
216
|
+
/** Where `GetExitCodeProcess` writes. */
|
|
217
|
+
exitCode: 0xf0,
|
|
218
|
+
/** Total frame size, 16-byte aligned. */
|
|
219
|
+
size: 0x110,
|
|
220
|
+
};
|
|
221
|
+
const STARTUPINFO_CB = 104;
|
|
222
|
+
const SECURITY_ATTRIBUTES_CB = 24;
|
|
223
|
+
const STARTF_USESTDHANDLES = 0x100;
|
|
224
|
+
const CREATE_NO_WINDOW = 0x08000000;
|
|
225
|
+
const GENERIC_WRITE = 0x40000000;
|
|
226
|
+
const FILE_SHARE_READ_WRITE = 0x3;
|
|
227
|
+
const CREATE_ALWAYS = 2;
|
|
228
|
+
const FILE_ATTRIBUTE_NORMAL = 0x80;
|
|
229
|
+
/** Capacities in WCHARs. `cmdBuf` holds the whole assembled command line. */
|
|
230
|
+
const BUFFERS = { sysBuf: 264, selfBuf: 520, tmpBuf: 264, logBuf: 600, cmdBuf: 4096 };
|
|
231
|
+
/**
|
|
232
|
+
* The stub's instruction stream.
|
|
233
|
+
*
|
|
234
|
+
* Reads top to bottom as the program it is:
|
|
235
|
+
*
|
|
236
|
+
* 1. `GetModuleFileNameW` → our own path; rewrite the trailing `exe` to `cmd`.
|
|
237
|
+
* A three-character overwrite and not a path search, which is what makes the
|
|
238
|
+
* whole stub free of string parsing: `<App>.exe` and `<App>.cmd` differ in
|
|
239
|
+
* exactly those bytes.
|
|
240
|
+
* 2. `GetSystemDirectoryW` → the real `cmd.exe`. `CreateProcessW` with a bare
|
|
241
|
+
* `cmd.exe` searches the CURRENT directory too, which under Explorer is the
|
|
242
|
+
* program directory — an absolute path is the only spelling that cannot be
|
|
243
|
+
* answered by a file sitting beside the app.
|
|
244
|
+
* 3. assemble `"<sys>\cmd.exe" /s /c ""<self>.cmd" <our own arguments>"`. `/s`
|
|
245
|
+
* is the documented flag that makes `cmd.exe` strip exactly the outer pair
|
|
246
|
+
* of quotes and take the rest literally, which is the only form that
|
|
247
|
+
* survives a program directory with a space in its path.
|
|
248
|
+
* 4. `GetConsoleCP` and `GetStdHandle` — two questions, three handles — decide
|
|
249
|
+
* the two remaining arguments: a console to inherit means no flags and no
|
|
250
|
+
* redirect (the terminal sees everything an unhidden `.cmd` shows today); no
|
|
251
|
+
* console but a usable stdout AND stderr means `CREATE_NO_WINDOW` and the
|
|
252
|
+
* caller's own handles; a dead handle on either means `CREATE_NO_WINDOW` plus
|
|
253
|
+
* `%TEMP%\<logLeaf>`, substituted for the dead one only so a caller's
|
|
254
|
+
* surviving redirect is never overwritten. `GetConsoleWindow` is deliberately
|
|
255
|
+
* NOT the probe — it answers NULL for a windowless console, and the comment at
|
|
256
|
+
* step 4 below carries the measurement that cost.
|
|
257
|
+
* 5. wait, and exit with the child's status — so `<App>.exe` in a script is a
|
|
258
|
+
* truthful `%ERRORLEVEL%` and not a fire-and-forget.
|
|
259
|
+
*/
|
|
260
|
+
function emitStub() {
|
|
261
|
+
const emit = new Emitter();
|
|
262
|
+
// `and rsp, -16` then `sub rsp, FRAME.size`. The entry point is called, so
|
|
263
|
+
// `rsp % 16 == 8` here; aligning explicitly is cheaper than reasoning about it
|
|
264
|
+
// and is what every call below needs.
|
|
265
|
+
emit.push(0x48, 0x83, 0xe4, 0xf0);
|
|
266
|
+
emit.push(0x48, 0x81, 0xec, FRAME.size & 0xff, (FRAME.size >> 8) & 0xff, 0, 0);
|
|
267
|
+
// Zero everything from the outgoing arguments to the end of the frame. The
|
|
268
|
+
// stack argument slots are eight bytes each while `bInheritHandles` and
|
|
269
|
+
// `dwCreationFlags` are DWORDs, so their upper halves are read from whatever
|
|
270
|
+
// was there — `rep stosq` is what makes them defined.
|
|
271
|
+
leaStack(emit, 'rdi', FRAME.args);
|
|
272
|
+
emit.push(0x31, 0xc0); // xor eax, eax
|
|
273
|
+
movImm32(emit, 'ecx', (FRAME.size - FRAME.args) / 8);
|
|
274
|
+
emit.push(0xf3, 0x48, 0xab); // rep stosq
|
|
275
|
+
// ── 1. our own path, rewritten to the `.cmd` beside us ───────────────────
|
|
276
|
+
emit.push(0x31, 0xc9); // xor ecx, ecx — hModule = NULL
|
|
277
|
+
emit.leaRip('rdx', 'selfBuf');
|
|
278
|
+
movImm32(emit, 'r8d', BUFFERS.selfBuf);
|
|
279
|
+
emit.callImport('GetModuleFileNameW');
|
|
280
|
+
emit.push(0x83, 0xf8, 0x04); // cmp eax, 4
|
|
281
|
+
emit.jcc('b', 'fail');
|
|
282
|
+
emit.leaRip('rdx', 'selfBuf');
|
|
283
|
+
emit.push(0x89, 0xc1); // mov ecx, eax (zero-extends: the length in WCHARs)
|
|
284
|
+
emit.push(0x48, 0x8d, 0x04, 0x4a); // lea rax, [rdx + rcx*2] — one past the last WCHAR
|
|
285
|
+
emit.push(0x66, 0xc7, 0x40, 0xfa, 0x63, 0x00); // mov word [rax-6], 'c'
|
|
286
|
+
emit.push(0x66, 0xc7, 0x40, 0xfc, 0x6d, 0x00); // mov word [rax-4], 'm'
|
|
287
|
+
emit.push(0x66, 0xc7, 0x40, 0xfe, 0x64, 0x00); // mov word [rax-2], 'd'
|
|
288
|
+
// ── 2. the real cmd.exe ──────────────────────────────────────────────────
|
|
289
|
+
emit.leaRip('rcx', 'sysBuf');
|
|
290
|
+
movImm32(emit, 'edx', BUFFERS.sysBuf);
|
|
291
|
+
emit.callImport('GetSystemDirectoryW');
|
|
292
|
+
// ── 3. the command line ──────────────────────────────────────────────────
|
|
293
|
+
emit.leaRip('rcx', 'cmdBuf');
|
|
294
|
+
emit.leaRip('rdx', 'strQuote');
|
|
295
|
+
emit.callImport('lstrcpyW');
|
|
296
|
+
for (const part of ['sysBuf', 'strCmdExe', 'selfBuf', 'strQuoteSpace']) {
|
|
297
|
+
emit.leaRip('rcx', 'cmdBuf');
|
|
298
|
+
emit.leaRip('rdx', part);
|
|
299
|
+
emit.callImport('lstrcatW');
|
|
300
|
+
}
|
|
301
|
+
// Our own arguments, which are everything after argv[0]. Windows hands the
|
|
302
|
+
// raw line back, so argv[0] is skipped by the same rule the C runtime uses:
|
|
303
|
+
// a quoted first token ends at its closing quote, an unquoted one at the
|
|
304
|
+
// first space or tab.
|
|
305
|
+
emit.callImport('GetCommandLineW');
|
|
306
|
+
emit.push(0x48, 0x89, 0xc2); // mov rdx, rax
|
|
307
|
+
emit.push(0x66, 0x83, 0x3a, 0x22); // cmp word [rdx], '"'
|
|
308
|
+
emit.jcc('ne', 'skipPlain');
|
|
309
|
+
emit.push(0x48, 0x83, 0xc2, 0x02); // add rdx, 2
|
|
310
|
+
emit.label('skipQuoted');
|
|
311
|
+
emit.push(0x66, 0x8b, 0x02); // mov ax, [rdx]
|
|
312
|
+
emit.push(0x66, 0x85, 0xc0); // test ax, ax
|
|
313
|
+
emit.jcc('e', 'argsReady');
|
|
314
|
+
emit.push(0x48, 0x83, 0xc2, 0x02); // add rdx, 2
|
|
315
|
+
emit.push(0x66, 0x83, 0xf8, 0x22); // cmp ax, '"'
|
|
316
|
+
emit.jcc('ne', 'skipQuoted');
|
|
317
|
+
emit.jmp('skipSpace');
|
|
318
|
+
emit.label('skipPlain');
|
|
319
|
+
emit.push(0x66, 0x8b, 0x02); // mov ax, [rdx]
|
|
320
|
+
emit.push(0x66, 0x85, 0xc0); // test ax, ax
|
|
321
|
+
emit.jcc('e', 'argsReady');
|
|
322
|
+
emit.push(0x66, 0x83, 0xf8, 0x20); // cmp ax, ' '
|
|
323
|
+
emit.jcc('e', 'skipSpace');
|
|
324
|
+
emit.push(0x66, 0x83, 0xf8, 0x09); // cmp ax, '\t'
|
|
325
|
+
emit.jcc('e', 'skipSpace');
|
|
326
|
+
emit.push(0x48, 0x83, 0xc2, 0x02); // add rdx, 2
|
|
327
|
+
emit.jmp('skipPlain');
|
|
328
|
+
emit.label('skipSpace');
|
|
329
|
+
emit.push(0x66, 0x8b, 0x02); // mov ax, [rdx]
|
|
330
|
+
emit.push(0x66, 0x83, 0xf8, 0x20); // cmp ax, ' '
|
|
331
|
+
emit.jcc('e', 'skipSpaceStep');
|
|
332
|
+
emit.push(0x66, 0x83, 0xf8, 0x09); // cmp ax, '\t'
|
|
333
|
+
emit.jcc('ne', 'argsReady');
|
|
334
|
+
emit.label('skipSpaceStep');
|
|
335
|
+
emit.push(0x48, 0x83, 0xc2, 0x02); // add rdx, 2
|
|
336
|
+
emit.jmp('skipSpace');
|
|
337
|
+
emit.label('argsReady');
|
|
338
|
+
emit.leaRip('rcx', 'cmdBuf');
|
|
339
|
+
emit.callImport('lstrcatW');
|
|
340
|
+
emit.leaRip('rcx', 'cmdBuf');
|
|
341
|
+
emit.leaRip('rdx', 'strQuote');
|
|
342
|
+
emit.callImport('lstrcatW');
|
|
343
|
+
// ── 4. a console to inherit, standard handles to pass on, or a log file ──
|
|
344
|
+
//
|
|
345
|
+
// TWO INDEPENDENT QUESTIONS, and collapsing them into one is the mistake this
|
|
346
|
+
// sequence is the second version of. "Does this process own a console" decides
|
|
347
|
+
// whether `cmd.exe` may be given one of its own (`CREATE_NO_WINDOW`, which is a
|
|
348
|
+
// console with no window rather than no console); "can this process write
|
|
349
|
+
// anywhere" decides whether the child's output needs a file. They come apart in
|
|
350
|
+
// both directions — a scheduled task has a console with NO WINDOW, and a GUI
|
|
351
|
+
// process started with a redirected stdout has a writable handle and no console
|
|
352
|
+
// at all.
|
|
353
|
+
//
|
|
354
|
+
// `GetConsoleWindow` was the first probe and it answers neither: it returns NULL
|
|
355
|
+
// for a windowless console, so a task-launched run took the log branch while its
|
|
356
|
+
// caller's own `> file` redirect captured nothing. `GetConsoleCP` fails with no
|
|
357
|
+
// console and succeeds with a windowless one, which is the question actually
|
|
358
|
+
// being asked.
|
|
359
|
+
//
|
|
360
|
+
// `rbx`, `rsi`, `rdi` and `r14` are all non-volatile, so the handles and the
|
|
361
|
+
// creation flags survive every call below.
|
|
362
|
+
movImm32(emit, 'ecx', -10); // STD_INPUT_HANDLE
|
|
363
|
+
emit.callImport('GetStdHandle');
|
|
364
|
+
emit.push(0x48, 0x89, 0xc7); // mov rdi, rax
|
|
365
|
+
movImm32(emit, 'ecx', -11); // STD_OUTPUT_HANDLE
|
|
366
|
+
emit.callImport('GetStdHandle');
|
|
367
|
+
emit.push(0x48, 0x89, 0xc3); // mov rbx, rax
|
|
368
|
+
movImm32(emit, 'ecx', -12); // STD_ERROR_HANDLE
|
|
369
|
+
emit.callImport('GetStdHandle');
|
|
370
|
+
emit.push(0x48, 0x89, 0xc6); // mov rsi, rax
|
|
371
|
+
emit.callImport('GetConsoleCP');
|
|
372
|
+
emit.push(0x85, 0xc0); // test eax, eax
|
|
373
|
+
emit.jcc('ne', 'haveConsole');
|
|
374
|
+
// No console: the child may not be given a window, and the handles have to be
|
|
375
|
+
// named explicitly — a process that allocates a console gets that console's
|
|
376
|
+
// buffers as its standard handles unless `STARTF_USESTDHANDLES` says otherwise.
|
|
377
|
+
movImm32(emit, 'r14d', CREATE_NO_WINDOW);
|
|
378
|
+
// BOTH HANDLES ARE PROBED, not just stdout, and the reason is the whole point
|
|
379
|
+
// of this file: `fd 2` is where an uncaught exception's trace comes out, and
|
|
380
|
+
// Node prints it from C++ (`src/debug_utils.cc`), so a dead stderr loses the
|
|
381
|
+
// one output that says why the app did not start.
|
|
382
|
+
//
|
|
383
|
+
// MEASURED, red before green, on `win11-gjsify` (2026-09-02). The state is
|
|
384
|
+
// constructible with `CreateProcessW` + `DETACHED_PROCESS` (no console at all)
|
|
385
|
+
// and `STARTF_USESTDHANDLES` where `hStdOutput` is a real file and `hStdError`
|
|
386
|
+
// is NULL: with `rbx` probed alone, the child's `console.log` reached the
|
|
387
|
+
// caller's file and the stderr line plus the entire uncaught-exception trace
|
|
388
|
+
// reached NOTHING — no `%TEMP%` log was even opened, because stdout looked
|
|
389
|
+
// fine. With both probed, the trace lands in `%TEMP%\<logLeaf>` and the
|
|
390
|
+
// caller's stdout redirect is untouched. `INVALID_HANDLE_VALUE` in place of
|
|
391
|
+
// NULL takes the same path, measured separately.
|
|
392
|
+
//
|
|
393
|
+
// The predicate is the SAME for both, deliberately: NULL or
|
|
394
|
+
// `INVALID_HANDLE_VALUE`. A handle that is neither is the caller's, and the
|
|
395
|
+
// caller's redirect always wins over our log file.
|
|
396
|
+
emit.push(0x48, 0x85, 0xdb); // test rbx, rbx
|
|
397
|
+
emit.jcc('e', 'needLog');
|
|
398
|
+
emit.push(0x48, 0x83, 0xfb, 0xff); // cmp rbx, -1 (INVALID_HANDLE_VALUE)
|
|
399
|
+
emit.jcc('e', 'needLog');
|
|
400
|
+
emit.push(0x48, 0x85, 0xf6); // test rsi, rsi
|
|
401
|
+
emit.jcc('e', 'needLog');
|
|
402
|
+
emit.push(0x48, 0x83, 0xfe, 0xff); // cmp rsi, -1
|
|
403
|
+
emit.jcc('ne', 'useStdHandles');
|
|
404
|
+
emit.label('needLog');
|
|
405
|
+
movImm32(emit, 'ecx', BUFFERS.tmpBuf);
|
|
406
|
+
emit.leaRip('rdx', 'tmpBuf');
|
|
407
|
+
emit.callImport('GetTempPathW');
|
|
408
|
+
emit.leaRip('rcx', 'logBuf');
|
|
409
|
+
emit.leaRip('rdx', 'tmpBuf');
|
|
410
|
+
emit.callImport('lstrcpyW');
|
|
411
|
+
emit.leaRip('rcx', 'logBuf');
|
|
412
|
+
emit.leaRip('rdx', 'strLogLeaf');
|
|
413
|
+
emit.callImport('lstrcatW');
|
|
414
|
+
emit.leaRip('rcx', 'logBuf');
|
|
415
|
+
movImm32(emit, 'edx', GENERIC_WRITE);
|
|
416
|
+
movImm32(emit, 'r8d', FILE_SHARE_READ_WRITE);
|
|
417
|
+
leaStack(emit, 'r9', FRAME.securityAttributes);
|
|
418
|
+
movStackDword(emit, FRAME.securityAttributes, SECURITY_ATTRIBUTES_CB);
|
|
419
|
+
// Inheritable, or the child cannot be handed it — the one field that makes this
|
|
420
|
+
// a redirect rather than a file this process opened and nobody wrote to.
|
|
421
|
+
movStackDword(emit, FRAME.securityAttributes + 0x10, 1);
|
|
422
|
+
movStackDword(emit, FRAME.args, CREATE_ALWAYS);
|
|
423
|
+
movStackDword(emit, FRAME.args + 0x8, FILE_ATTRIBUTE_NORMAL);
|
|
424
|
+
movStackDword(emit, FRAME.args + 0x10, 0); // hTemplateFile
|
|
425
|
+
emit.callImport('CreateFileW');
|
|
426
|
+
emit.push(0x48, 0x83, 0xf8, 0xff); // cmp rax, -1
|
|
427
|
+
// A `%TEMP%` that cannot be written to is not a reason to refuse to start the
|
|
428
|
+
// app: run it with no window and no output rather than not at all.
|
|
429
|
+
emit.jcc('e', 'create');
|
|
430
|
+
// THE LOG REPLACES ONLY WHAT IS UNUSABLE. Both handles reach here when either
|
|
431
|
+
// one is dead, so substituting blindly would overwrite a redirect the caller
|
|
432
|
+
// asked for — `cmd /c "app.exe > file"` with a broken stderr must still put
|
|
433
|
+
// stdout in `file`. Each is tested again against the same predicate as above
|
|
434
|
+
// and only the dead one becomes the log.
|
|
435
|
+
emit.push(0x48, 0x85, 0xdb); // test rbx, rbx
|
|
436
|
+
emit.jcc('e', 'logIsStdout');
|
|
437
|
+
emit.push(0x48, 0x83, 0xfb, 0xff); // cmp rbx, -1
|
|
438
|
+
emit.jcc('ne', 'stdoutKept');
|
|
439
|
+
emit.label('logIsStdout');
|
|
440
|
+
emit.push(0x48, 0x89, 0xc3); // mov rbx, rax
|
|
441
|
+
emit.label('stdoutKept');
|
|
442
|
+
emit.push(0x48, 0x85, 0xf6); // test rsi, rsi
|
|
443
|
+
emit.jcc('e', 'logIsStderr');
|
|
444
|
+
emit.push(0x48, 0x83, 0xfe, 0xff); // cmp rsi, -1
|
|
445
|
+
emit.jcc('ne', 'useStdHandles');
|
|
446
|
+
emit.label('logIsStderr');
|
|
447
|
+
emit.push(0x48, 0x89, 0xc6); // mov rsi, rax
|
|
448
|
+
emit.label('useStdHandles');
|
|
449
|
+
movStackFromReg(emit, 'rdi', FRAME.startupInfo + 0x50); // hStdInput
|
|
450
|
+
movStackFromReg(emit, 'rbx', FRAME.startupInfo + 0x58); // hStdOutput
|
|
451
|
+
movStackFromReg(emit, 'rsi', FRAME.startupInfo + 0x60); // hStdError
|
|
452
|
+
movStackDword(emit, FRAME.startupInfo + 0x3c, STARTF_USESTDHANDLES);
|
|
453
|
+
emit.jmp('create');
|
|
454
|
+
// A console of our own: hand the child nothing and let it share everything —
|
|
455
|
+
// the console, and any redirection already applied to it. This is the branch
|
|
456
|
+
// that makes `<App>.exe` in a terminal behave exactly as the `.cmd` does.
|
|
457
|
+
emit.label('haveConsole');
|
|
458
|
+
emit.push(0x45, 0x31, 0xf6); // xor r14d, r14d
|
|
459
|
+
// ── 5. run it, wait, and be its exit code ────────────────────────────────
|
|
460
|
+
emit.label('create');
|
|
461
|
+
movStackDword(emit, FRAME.startupInfo, STARTUPINFO_CB); // si.cb
|
|
462
|
+
emit.push(0x31, 0xc9); // xor ecx, ecx — lpApplicationName = NULL
|
|
463
|
+
emit.leaRip('rdx', 'cmdBuf');
|
|
464
|
+
emit.push(0x45, 0x31, 0xc0); // xor r8d, r8d
|
|
465
|
+
emit.push(0x45, 0x31, 0xc9); // xor r9d, r9d
|
|
466
|
+
movStackDword(emit, FRAME.args, 1); // bInheritHandles = TRUE
|
|
467
|
+
emit.push(0x44, 0x89, 0xb4, 0x24); // mov [rsp + disp32], r14d — dwCreationFlags
|
|
468
|
+
{
|
|
469
|
+
const displacement = Buffer.alloc(4);
|
|
470
|
+
displacement.writeInt32LE(FRAME.args + 0x8);
|
|
471
|
+
emit.push(...displacement);
|
|
472
|
+
}
|
|
473
|
+
movStackDword(emit, FRAME.args + 0x10, 0); // lpEnvironment
|
|
474
|
+
movStackDword(emit, FRAME.args + 0x18, 0); // lpCurrentDirectory
|
|
475
|
+
leaStack(emit, 'rax', FRAME.startupInfo);
|
|
476
|
+
movStackFromReg(emit, 'rax', FRAME.args + 0x20);
|
|
477
|
+
leaStack(emit, 'rax', FRAME.processInformation);
|
|
478
|
+
movStackFromReg(emit, 'rax', FRAME.args + 0x28);
|
|
479
|
+
emit.callImport('CreateProcessW');
|
|
480
|
+
emit.push(0x85, 0xc0); // test eax, eax
|
|
481
|
+
emit.jcc('e', 'fail');
|
|
482
|
+
movRaxFromStack(emit, FRAME.processInformation); // hProcess
|
|
483
|
+
emit.push(0x48, 0x89, 0xc1); // mov rcx, rax
|
|
484
|
+
movImm32(emit, 'edx', -1); // INFINITE
|
|
485
|
+
emit.callImport('WaitForSingleObject');
|
|
486
|
+
movRaxFromStack(emit, FRAME.processInformation);
|
|
487
|
+
emit.push(0x48, 0x89, 0xc1); // mov rcx, rax
|
|
488
|
+
leaStack(emit, 'rdx', FRAME.exitCode);
|
|
489
|
+
emit.callImport('GetExitCodeProcess');
|
|
490
|
+
emit.push(0x8b, 0x8c, 0x24); // mov ecx, [rsp + disp32]
|
|
491
|
+
{
|
|
492
|
+
const displacement = Buffer.alloc(4);
|
|
493
|
+
displacement.writeInt32LE(FRAME.exitCode);
|
|
494
|
+
emit.push(...displacement);
|
|
495
|
+
}
|
|
496
|
+
emit.callImport('ExitProcess');
|
|
497
|
+
// Unreachable except through the two guards above. `ExitProcess` does not
|
|
498
|
+
// return, so there is nothing after it to fall into.
|
|
499
|
+
emit.label('fail');
|
|
500
|
+
movImm32(emit, 'ecx', 1);
|
|
501
|
+
emit.callImport('ExitProcess');
|
|
502
|
+
emit.push(0xcc); // int3 — a trap, not a fallthrough into the data section
|
|
503
|
+
return emit;
|
|
504
|
+
}
|
|
505
|
+
/** UTF-16LE with the terminator, which is what every `lstr*W` reads to. */
|
|
506
|
+
function wide(text) {
|
|
507
|
+
return new Uint8Array(Buffer.from(`${text}\0`, 'utf16le'));
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Everything the stub reads or writes, laid out in one read-write section.
|
|
511
|
+
*
|
|
512
|
+
* The IAT comes FIRST because two data directories name it and the loader writes
|
|
513
|
+
* it; the strings and the buffers follow. Buffers are emitted as zeros rather
|
|
514
|
+
* than declared as uninitialised data — a `VirtualSize` larger than
|
|
515
|
+
* `SizeOfRawData` is the same thing with one more field to get wrong, and eleven
|
|
516
|
+
* kilobytes of zeros compresses to nothing in every container this image ships
|
|
517
|
+
* inside.
|
|
518
|
+
*/
|
|
519
|
+
function dataItems(input) {
|
|
520
|
+
const items = [];
|
|
521
|
+
for (const name of IMPORTS)
|
|
522
|
+
items.push({ symbol: `iat.${name}`, bytes: new Uint8Array(8) });
|
|
523
|
+
items.push({ symbol: 'iat.null', bytes: new Uint8Array(8) });
|
|
524
|
+
items.push({ symbol: 'importDescriptors', bytes: new Uint8Array(40) });
|
|
525
|
+
for (const name of IMPORTS)
|
|
526
|
+
items.push({ symbol: `ilt.${name}`, bytes: new Uint8Array(8) });
|
|
527
|
+
items.push({ symbol: 'ilt.null', bytes: new Uint8Array(8) });
|
|
528
|
+
for (const name of IMPORTS) {
|
|
529
|
+
// IMAGE_IMPORT_BY_NAME: a two-byte hint the loader may ignore, the name,
|
|
530
|
+
// its terminator, and a pad byte when that lands odd — the structure has
|
|
531
|
+
// to start on an even address.
|
|
532
|
+
const raw = Buffer.from(`\0\0${name}\0`, 'latin1');
|
|
533
|
+
items.push({
|
|
534
|
+
symbol: `hint.${name}`,
|
|
535
|
+
bytes: new Uint8Array(raw.length % 2 === 0 ? raw : Buffer.concat([raw, Buffer.alloc(1)])),
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
items.push({ symbol: 'dllName', bytes: new Uint8Array(Buffer.from('KERNEL32.dll\0\0', 'latin1')) });
|
|
539
|
+
items.push({ symbol: 'strQuote', bytes: wide('"') });
|
|
540
|
+
items.push({ symbol: 'strCmdExe', bytes: wide('\\cmd.exe" /s /c ""') });
|
|
541
|
+
items.push({ symbol: 'strQuoteSpace', bytes: wide('" ') });
|
|
542
|
+
items.push({ symbol: 'strLogLeaf', bytes: wide(input.logLeaf) });
|
|
543
|
+
for (const [name, capacity] of Object.entries(BUFFERS)) {
|
|
544
|
+
items.push({ symbol: name, bytes: new Uint8Array(capacity * 2) });
|
|
545
|
+
}
|
|
546
|
+
return items;
|
|
547
|
+
}
|
|
548
|
+
function align(value, to) {
|
|
549
|
+
return Math.ceil(value / to) * to;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* The GUI-subsystem launcher, as a complete PE32+ image.
|
|
553
|
+
*
|
|
554
|
+
* Deterministic: the same input always produces the same bytes, with no
|
|
555
|
+
* timestamp, no checksum and no build-host string anywhere in the file — which
|
|
556
|
+
* is what lets `tests/e2e/ship-windows` compare two runs and what keeps the
|
|
557
|
+
* artifact reproducible for anyone who repacks a stage (ADR 0024 § A2).
|
|
558
|
+
*/
|
|
559
|
+
export function buildGuiLauncher(input) {
|
|
560
|
+
if (input.logLeaf === '' || /[\\/:*?"<>|]/.test(input.logLeaf)) {
|
|
561
|
+
throw new Error(`gjsify ship: "${input.logLeaf}" cannot be the launcher's log file name — it is empty or holds a ` +
|
|
562
|
+
'character Windows refuses in one. The stub writes it into `%TEMP%`, so the value is a leaf ' +
|
|
563
|
+
'and never a path.');
|
|
564
|
+
}
|
|
565
|
+
const code = emitStub();
|
|
566
|
+
const codeSize = code.bytes.length;
|
|
567
|
+
const textRva = SECTION_ALIGNMENT;
|
|
568
|
+
const dataRva = textRva + align(codeSize, SECTION_ALIGNMENT);
|
|
569
|
+
const items = dataItems(input);
|
|
570
|
+
const offsets = new Map();
|
|
571
|
+
let cursor = 0;
|
|
572
|
+
for (const item of items) {
|
|
573
|
+
offsets.set(item.symbol, cursor);
|
|
574
|
+
cursor += item.bytes.length;
|
|
575
|
+
}
|
|
576
|
+
const dataSize = cursor;
|
|
577
|
+
const rvaOf = (symbol) => {
|
|
578
|
+
const offset = offsets.get(symbol);
|
|
579
|
+
if (offset === undefined)
|
|
580
|
+
throw new Error(`gjsify ship: the launcher stub names no datum "${symbol}".`);
|
|
581
|
+
return dataRva + offset;
|
|
582
|
+
};
|
|
583
|
+
const data = Buffer.alloc(dataSize);
|
|
584
|
+
for (const item of items)
|
|
585
|
+
data.set(item.bytes, offsets.get(item.symbol) ?? 0);
|
|
586
|
+
// The import descriptor and both thunk arrays, filled now that every RVA is
|
|
587
|
+
// known. The ILT and the IAT hold the SAME values before the loader runs; the
|
|
588
|
+
// loader overwrites the IAT with the resolved addresses and leaves the ILT,
|
|
589
|
+
// which is what lets a tool read the names back out of a bound image.
|
|
590
|
+
data.writeUInt32LE(rvaOf('ilt.GetModuleFileNameW'), offsets.get('importDescriptors') ?? 0);
|
|
591
|
+
data.writeUInt32LE(rvaOf('dllName'), (offsets.get('importDescriptors') ?? 0) + 12);
|
|
592
|
+
data.writeUInt32LE(rvaOf('iat.GetModuleFileNameW'), (offsets.get('importDescriptors') ?? 0) + 16);
|
|
593
|
+
for (const name of IMPORTS) {
|
|
594
|
+
const hint = BigInt(rvaOf(`hint.${name}`));
|
|
595
|
+
data.writeBigUInt64LE(hint, offsets.get(`ilt.${name}`) ?? 0);
|
|
596
|
+
data.writeBigUInt64LE(hint, offsets.get(`iat.${name}`) ?? 0);
|
|
597
|
+
}
|
|
598
|
+
const text = code.resolve(textRva, rvaOf);
|
|
599
|
+
const textRaw = align(codeSize, FILE_ALIGNMENT);
|
|
600
|
+
const dataRaw = align(dataSize, FILE_ALIGNMENT);
|
|
601
|
+
const headerSize = FILE_ALIGNMENT;
|
|
602
|
+
const textOffset = headerSize;
|
|
603
|
+
const dataOffset = textOffset + textRaw;
|
|
604
|
+
const image = Buffer.alloc(dataOffset + dataRaw);
|
|
605
|
+
// ── DOS header ───────────────────────────────────────────────────────────
|
|
606
|
+
image.write('MZ', 0, 'ascii');
|
|
607
|
+
image.write('This program cannot be run in DOS mode.\r\r\n$', 0x40, 'ascii');
|
|
608
|
+
image.writeUInt32LE(PE_OFFSET, 0x3c);
|
|
609
|
+
// ── COFF header ──────────────────────────────────────────────────────────
|
|
610
|
+
image.write('PE\0\0', PE_OFFSET, 'ascii');
|
|
611
|
+
const coff = PE_OFFSET + 4;
|
|
612
|
+
image.writeUInt16LE(MACHINE_AMD64, coff);
|
|
613
|
+
image.writeUInt16LE(2, coff + 2); // NumberOfSections
|
|
614
|
+
image.writeUInt32LE(0, coff + 4); // TimeDateStamp — zero, so the image is reproducible
|
|
615
|
+
image.writeUInt16LE(240, coff + 16); // SizeOfOptionalHeader (112 + 16 * 8)
|
|
616
|
+
// EXECUTABLE_IMAGE | LARGE_ADDRESS_AWARE | RELOCS_STRIPPED. The stub is
|
|
617
|
+
// position-independent (every reference is RIP-relative) and carries no
|
|
618
|
+
// `.reloc`, so it says so rather than letting the loader look for one.
|
|
619
|
+
image.writeUInt16LE(0x0002 | 0x0020 | 0x0001, coff + 18);
|
|
620
|
+
// ── optional header ──────────────────────────────────────────────────────
|
|
621
|
+
const opt = coff + 20;
|
|
622
|
+
image.writeUInt16LE(0x20b, opt); // PE32+
|
|
623
|
+
image.writeUInt8(14, opt + 2); // MajorLinkerVersion — cosmetic
|
|
624
|
+
image.writeUInt32LE(textRaw, opt + 4); // SizeOfCode
|
|
625
|
+
image.writeUInt32LE(dataRaw, opt + 8); // SizeOfInitializedData
|
|
626
|
+
image.writeUInt32LE(textRva, opt + 16); // AddressOfEntryPoint
|
|
627
|
+
image.writeUInt32LE(textRva, opt + 20); // BaseOfCode
|
|
628
|
+
image.writeBigUInt64LE(IMAGE_BASE, opt + 24);
|
|
629
|
+
image.writeUInt32LE(SECTION_ALIGNMENT, opt + 32);
|
|
630
|
+
image.writeUInt32LE(FILE_ALIGNMENT, opt + 36);
|
|
631
|
+
image.writeUInt16LE(6, opt + 40); // MajorOperatingSystemVersion
|
|
632
|
+
image.writeUInt16LE(6, opt + 48); // MajorSubsystemVersion
|
|
633
|
+
image.writeUInt32LE(dataRva + align(dataSize, SECTION_ALIGNMENT), opt + 56); // SizeOfImage
|
|
634
|
+
image.writeUInt32LE(headerSize, opt + 60); // SizeOfHeaders
|
|
635
|
+
image.writeUInt16LE(PE_SUBSYSTEM_GUI, opt + 68);
|
|
636
|
+
image.writeUInt16LE(0x0100 | 0x8000, opt + 70); // NX_COMPAT | TERMINAL_SERVER_AWARE
|
|
637
|
+
image.writeBigUInt64LE(0x100000n, opt + 72); // SizeOfStackReserve
|
|
638
|
+
image.writeBigUInt64LE(0x1000n, opt + 80); // SizeOfStackCommit
|
|
639
|
+
image.writeBigUInt64LE(0x100000n, opt + 88); // SizeOfHeapReserve
|
|
640
|
+
image.writeBigUInt64LE(0x1000n, opt + 96); // SizeOfHeapCommit
|
|
641
|
+
image.writeUInt32LE(16, opt + 108); // NumberOfRvaAndSizes
|
|
642
|
+
const directories = opt + 112;
|
|
643
|
+
image.writeUInt32LE(rvaOf('importDescriptors'), directories + 1 * 8); // IMPORT
|
|
644
|
+
image.writeUInt32LE(40, directories + 1 * 8 + 4);
|
|
645
|
+
image.writeUInt32LE(rvaOf('iat.GetModuleFileNameW'), directories + 12 * 8); // IAT
|
|
646
|
+
image.writeUInt32LE((IMPORTS.length + 1) * 8, directories + 12 * 8 + 4);
|
|
647
|
+
// ── section table ────────────────────────────────────────────────────────
|
|
648
|
+
const sections = directories + 16 * 8;
|
|
649
|
+
const writeSection = (index, name, rva, virtualSize, raw, rawSize, characteristics) => {
|
|
650
|
+
const at = sections + index * 40;
|
|
651
|
+
image.write(name.padEnd(8, '\0'), at, 'latin1');
|
|
652
|
+
image.writeUInt32LE(virtualSize, at + 8);
|
|
653
|
+
image.writeUInt32LE(rva, at + 12);
|
|
654
|
+
image.writeUInt32LE(rawSize, at + 16);
|
|
655
|
+
image.writeUInt32LE(raw, at + 20);
|
|
656
|
+
image.writeUInt32LE(characteristics, at + 36);
|
|
657
|
+
};
|
|
658
|
+
writeSection(0, '.text', textRva, codeSize, textOffset, textRaw, 0x60000020);
|
|
659
|
+
writeSection(1, '.data', dataRva, dataSize, dataOffset, dataRaw, 0xc0000040);
|
|
660
|
+
image.set(text, textOffset);
|
|
661
|
+
image.set(data, dataOffset);
|
|
662
|
+
return new Uint8Array(image);
|
|
663
|
+
}
|