-
Notifications
You must be signed in to change notification settings - Fork 152
/
Copy pathstandalone.js
129 lines (102 loc) · 3.68 KB
/
standalone.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import * as std from "qjs:std";
import * as os from "qjs:os";
import * as bjson from "qjs:bjson";
// See quickjs.h
const JS_READ_OBJ_BYTECODE = 1 << 0;
const JS_READ_OBJ_REFERENCE = 1 << 3;
const JS_WRITE_OBJ_BYTECODE = 1 << 0;
const JS_WRITE_OBJ_REFERENCE = 1 << 3;
const JS_WRITE_OBJ_STRIP_SOURCE = 1 << 4;
/**
* Trailer for standalone binaries. When some code gets bundled with the qjs
* executable we add a 12 byte trailer. The first 8 bytes are the magic
* string that helps us understand this is a standalone binary, and the
* remaining 4 are the offset (from the beginning of the binary) where the
* bundled data is located.
*
* The offset is stored as a 32bit little-endian number.
*/
const Trailer = {
Magic: 'quickjs2',
MagicSize: 8,
DataSize: 4,
Size: 12
};
function encodeAscii(txt) {
return new Uint8Array(txt.split('').map(c => c.charCodeAt(0)));
}
function decodeAscii(buf) {
return Array.from(buf).map(c => String.fromCharCode(c)).join('')
}
export function compileStandalone(inFile, outFile, targetExe) {
// Step 1: compile the source file to bytecode
const js = std.loadFile(inFile);
if (!js) {
throw new Error(`failed to open ${inFile}`);
}
const code = std.evalScript(js, {
compile_only: true,
compile_module: true
});
const bytecode = new Uint8Array(bjson.write(code, JS_WRITE_OBJ_BYTECODE | JS_WRITE_OBJ_REFERENCE | JS_WRITE_OBJ_STRIP_SOURCE));
// Step 2: copy the bytecode to the end of the executable and add a marker.
const exeFileName = targetExe ?? globalThis.argv0;
const exe = std.loadFile(exeFileName, { binary: true });
if (!exe) {
throw new Error(`failed to open executable: ${exeFileName}`);
}
const exeSize = exe.length;
const newBuffer = exe.buffer.transfer(exeSize + bytecode.length + Trailer.Size);
const newExe = new Uint8Array(newBuffer);
newExe.set(bytecode, exeSize);
newExe.set(encodeAscii(Trailer.Magic), exeSize + bytecode.length);
const dw = new DataView(newBuffer, exeSize + bytecode.length + Trailer.MagicSize, Trailer.DataSize);
dw.setUint32(0, exeSize, true /* little-endian */);
// We use os.open() so we can set the permissions mask.
const newFd = os.open(outFile, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o755);
if (newFd < 0) {
throw new Error(`failed to create ${outFile}`);
}
if (os.write(newFd, newBuffer, 0, newBuffer.byteLength) < 0) {
os.close(newFd);
throw new Error(`failed to write to output file`);
}
os.close(newFd);
}
export function runStandalone() {
const file = globalThis.argv0;
const exe = std.open(file, 'rb');
if (!exe) {
throw new Error(`failed to open executable: ${file}`);
}
let r = exe.seek(-Trailer.Size, std.SEEK_END);
if (r < 0) {
throw new Error(`seek error: ${-r}`);
}
const trailer = new Uint8Array(Trailer.Size);
exe.read(trailer.buffer, 0, Trailer.Size);
const magic = new Uint8Array(trailer.buffer, 0, Trailer.MagicSize);
// Shouldn't happen since qjs.c checks for it.
if (decodeAscii(magic) !== Trailer.Magic) {
exe.close();
throw new Error('corrupted binary, magic mismatch');
}
const dw = new DataView(trailer.buffer, Trailer.MagicSize, Trailer.DataSize);
const offset = dw.getUint32(0, true /* little-endian */);
const bytecode = new Uint8Array(offset - Trailer.Size);
r = exe.seek(offset, std.SEEK_SET);
if (r < 0) {
exe.close();
throw new Error(`seek error: ${-r}`);
}
exe.read(bytecode.buffer, 0, bytecode.length);
if (exe.error()) {
exe.close();
throw new Error('read error');
}
exe.close();
const code = bjson.read(bytecode.buffer, 0, bytecode.length, JS_READ_OBJ_BYTECODE | JS_READ_OBJ_REFERENCE);
return std.evalScript(code, {
eval_module: true
});
}