bmweb-cli 0.1.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/LICENSE +674 -0
- package/README.md +351 -0
- package/dist/bmweb.js +2790 -0
- package/package.json +53 -0
- package/runtime/core/bestvm/codec.js +285 -0
- package/runtime/core/bestvm/environment.js +116 -0
- package/runtime/core/bestvm/executor.js +1483 -0
- package/runtime/core/bestvm/index.js +52 -0
- package/runtime/core/bestvm/machine.js +491 -0
- package/runtime/core/bestvm/operands.js +356 -0
- package/runtime/core/bestvm/registers.js +152 -0
- package/runtime/core/bestvm/write-guard.js +111 -0
- package/runtime/core/ipofile/compile.js +364 -0
- package/runtime/core/ipofile/decls.js +187 -0
- package/runtime/core/ipofile/emit.js +708 -0
- package/runtime/core/ipofile/exec.js +164 -0
- package/runtime/core/ipofile/lex.js +243 -0
- package/runtime/core/ipofile/parse.js +550 -0
- package/runtime/core/ipofile/pool.js +404 -0
- package/runtime/core/ipofile/walk.js +431 -0
- package/runtime/core/ipovm/builtin-helpers.js +182 -0
- package/runtime/core/ipovm/builtins-api.js +610 -0
- package/runtime/core/ipovm/builtins-screen.js +493 -0
- package/runtime/core/ipovm/builtins-table.js +166 -0
- package/runtime/core/ipovm/builtins-text.js +166 -0
- package/runtime/core/ipovm/emissions.js +138 -0
- package/runtime/core/ipovm/hosts.js +191 -0
- package/runtime/core/ipovm/operators.js +229 -0
- package/runtime/core/ipovm/structures.js +250 -0
- package/runtime/core/ipovm/suspensions.js +241 -0
- package/runtime/core/ipovm/tape.js +206 -0
- package/runtime/core/ipovm/values.js +241 -0
- package/runtime/core/ipovm/vm.js +1166 -0
- package/runtime/core/translate.js +526 -0
- package/runtime/core/webshim/api-router.js +592 -0
- package/runtime/core/webshim/bus.js +95 -0
- package/runtime/core/webshim/coding.js +82 -0
- package/runtime/core/webshim/data-fetch.js +66 -0
- package/runtime/core/webshim/exchange.js +288 -0
- package/runtime/core/webshim/framing.js +331 -0
- package/runtime/core/webshim/install.js +30 -0
- package/runtime/core/webshim/job-runner.js +319 -0
- package/runtime/core/webshim/native-bus.js +108 -0
- package/runtime/core/webshim/timers.js +82 -0
- package/runtime/core/webshim/trace.js +205 -0
- package/runtime/core/webshim/transport-base.js +128 -0
- package/runtime/core/webshim/variant-resolver.js +249 -0
- package/runtime/core/webshim/web-serial-bus.js +734 -0
- package/runtime/home/bmweb-home.ips +76 -0
- package/runtime/home/bmweb.h +26 -0
- package/runtime/screens/activations.js +258 -0
- package/runtime/screens/garage/diff.js +331 -0
- package/runtime/screens/garage/share.js +276 -0
- package/runtime/screens/garage/store.js +547 -0
- package/runtime/screens/ipo-runtime/cells.js +176 -0
- package/runtime/screens/ipo-runtime/dialogs.js +254 -0
- package/runtime/screens/ipo-runtime/home.js +358 -0
- package/runtime/screens/ipo-runtime/open.js +393 -0
- package/runtime/screens/ipo-runtime/paint-grid.js +106 -0
- package/runtime/screens/ipo-runtime/paint-modern.js +424 -0
- package/runtime/screens/ipo-runtime/print.js +281 -0
- package/runtime/screens/ipo-runtime/program.js +1337 -0
- package/runtime/screens/ipo-runtime/protocol.js +464 -0
- package/runtime/screens/ipo-runtime/script-scan.js +225 -0
- package/runtime/screens/ipo-runtime/translate-sets.js +130 -0
- package/runtime/screens/ipo-runtime/ui.js +249 -0
- package/runtime/screens/ipo-runtime/wire-policy.js +113 -0
- package/runtime/screens/ir.js +324 -0
- package/runtime/screens/search/data.js +153 -0
- package/runtime/screens/search/match.js +285 -0
- package/runtime/screens/search/open.js +66 -0
- package/runtime/vendor/fflate.min.js +1 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Compiling an INPA source script (.IPS / .SRC) to a runnable exec.
|
|
3
|
+
*
|
|
4
|
+
* The front door over lex / parse / emit: it resolves `#include`s from the
|
|
5
|
+
* files the user dropped alongside the script, builds the builtin-number table
|
|
6
|
+
* the emitter needs, and reports what it could not do with the line to look at.
|
|
7
|
+
*
|
|
8
|
+
* INCLUDES ARE THE USER'S TO SUPPLY. A header declares the builtins
|
|
9
|
+
* (`extern name(in: type a, out: type b)`) and the .SRC libraries carry
|
|
10
|
+
* function bodies the compiled file must contain, but those are BMW's files:
|
|
11
|
+
* they are not in this repo and never ship with it. A script that needs one it
|
|
12
|
+
* did not get is a clear error naming the missing include, not a silent
|
|
13
|
+
* half-compile.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Builtin name -> number, inverted from the walker's table. */
|
|
17
|
+
const IPOF_BUILTIN_NUMS = (() => {
|
|
18
|
+
const out = {};
|
|
19
|
+
for (const k of Object.keys(IPOF_BUILTINS)) out[IPOF_BUILTINS[k]] = Number(k);
|
|
20
|
+
return out;
|
|
21
|
+
})();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The aliases the decompiler prints for builtins the walker leaves numbered.
|
|
25
|
+
*
|
|
26
|
+
* A round trip has to accept its own output, so every name the decompiler can
|
|
27
|
+
* emit must compile back to the number it came from.
|
|
28
|
+
* @type {Object<string, number>}
|
|
29
|
+
*/
|
|
30
|
+
const IPOF_ALIAS_NUMS = {
|
|
31
|
+
settimer: 0x09,
|
|
32
|
+
testtimer: 0x0a,
|
|
33
|
+
control: 0x12,
|
|
34
|
+
stop: 0x14,
|
|
35
|
+
getapistring: 0x15,
|
|
36
|
+
togglelist: 0x16,
|
|
37
|
+
setcolor: 0x1a,
|
|
38
|
+
stringtoint: 0x21,
|
|
39
|
+
hexconvert: 0x22,
|
|
40
|
+
strcat: 0x23,
|
|
41
|
+
input2int: 0x47,
|
|
42
|
+
blankscreen: 0x51,
|
|
43
|
+
userboxclear: 0x57,
|
|
44
|
+
userboxsetcolor: 0x58,
|
|
45
|
+
INP1apiResultReal: 0x74,
|
|
46
|
+
callstatemachine: 0x07,
|
|
47
|
+
returnstatemachine: 0x08,
|
|
48
|
+
setjobstatus: 0x0b,
|
|
49
|
+
delay: 0x1b,
|
|
50
|
+
inputnum: 0x39,
|
|
51
|
+
inputtext: 0x3a,
|
|
52
|
+
ftextclear: 0x4f,
|
|
53
|
+
clearrect: 0x50,
|
|
54
|
+
SPSInit: 0x92,
|
|
55
|
+
SPSLeseVonSPS: 0x94,
|
|
56
|
+
SPSSendeAnSPS: 0x95,
|
|
57
|
+
ApiJobFsLesenFAB: 0x97,
|
|
58
|
+
ApiResultFsLesenFAB: 0x98,
|
|
59
|
+
ELDIOpenStartDialog: 0x99,
|
|
60
|
+
setitemrepeat: 0xa1,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Every builtin name the compiler can place, name -> number.
|
|
65
|
+
* @returns {Object<string, number>} The table.
|
|
66
|
+
*/
|
|
67
|
+
function ipofBuiltinTable() {
|
|
68
|
+
const out = Object.assign({}, IPOF_BUILTIN_NUMS, IPOF_ALIAS_NUMS);
|
|
69
|
+
// a `builtin_2a` spelling is what the decompiler prints for anything with no
|
|
70
|
+
// name at all, and it has to compile back to its own number
|
|
71
|
+
for (let n = 0; n < 256; n += 1) out[ipofBuiltinName(n)] = n;
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The parameter modes of every prototype an include declares.
|
|
77
|
+
*
|
|
78
|
+
* A builtin's `out:` / `inout:` parameters are passed BY REFERENCE, and the
|
|
79
|
+
* bytecode says so with a different opcode: an out argument compiles to a
|
|
80
|
+
* procref carrying the slot, not to an ordinary push. Without the prototypes
|
|
81
|
+
* there is no way to know which argument that is, so a script whose headers
|
|
82
|
+
* were not supplied cannot be compiled faithfully -- which is the other reason
|
|
83
|
+
* the includes are required rather than optional.
|
|
84
|
+
*
|
|
85
|
+
* @param {Object<string, string>} files The dropped files, name -> text.
|
|
86
|
+
* @returns {Object<string, string[]>} Function name -> parameter modes.
|
|
87
|
+
*/
|
|
88
|
+
function ipofScanPrototypes(files) {
|
|
89
|
+
const out = {};
|
|
90
|
+
for (const name of Object.keys(files || {})) {
|
|
91
|
+
const text = String(files[name])
|
|
92
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
93
|
+
.replace(/\/\/[^\n]*/g, '');
|
|
94
|
+
const re = /(?:extern|import32|import)\b[^(;]*?(\w+)\s*\(([^)]*)\)\s*;/g;
|
|
95
|
+
let m = re.exec(text);
|
|
96
|
+
while (m) {
|
|
97
|
+
const modes = [];
|
|
98
|
+
const pre = /\b(in|out|inout)\s*:/g;
|
|
99
|
+
let p = pre.exec(m[2]);
|
|
100
|
+
while (p) {
|
|
101
|
+
modes.push(p[1]);
|
|
102
|
+
p = pre.exec(m[2]);
|
|
103
|
+
}
|
|
104
|
+
if (modes.length && !out[m[1]]) out[m[1]] = modes;
|
|
105
|
+
m = re.exec(text);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The DLL functions the includes import, in declaration order.
|
|
113
|
+
*
|
|
114
|
+
* `import32 "C" lib "kernel32::GetPrivateProfileStringA" GetPrivateProfileString`
|
|
115
|
+
* binds a Windows entry point to a name the script may call. The compiled file
|
|
116
|
+
* numbers these per file and calls them by index, so the order they are
|
|
117
|
+
* declared in IS the numbering -- a name the script calls but no include
|
|
118
|
+
* imports has no number, and cannot be compiled.
|
|
119
|
+
*
|
|
120
|
+
* @param {Object<string, string>} files The dropped files, name -> text.
|
|
121
|
+
* @returns {string[]} The imported names by index, carrying an `alias` map of
|
|
122
|
+
* DLL entry-point names to the script-side name that holds each slot.
|
|
123
|
+
*/
|
|
124
|
+
function ipofScanImports(files) {
|
|
125
|
+
const out = [];
|
|
126
|
+
// entry-point name -> the script-side name that holds the slot
|
|
127
|
+
out.alias = {};
|
|
128
|
+
for (const name of Object.keys(files || {})) {
|
|
129
|
+
const text = String(files[name])
|
|
130
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
131
|
+
.replace(/\/\/[^\n]*/g, '');
|
|
132
|
+
// `import32 "C" lib "kernel32::Entry" Name (...)`: the name is what follows
|
|
133
|
+
// the LIB string, so the calling convention's own quoted "C" has to be
|
|
134
|
+
// stepped over rather than matched non-greedily past.
|
|
135
|
+
const re = /import32\b[^\n]*?\blib\s+"([^"]*)"\s+(\w+)/g;
|
|
136
|
+
let m = re.exec(text);
|
|
137
|
+
while (m) {
|
|
138
|
+
// Each import takes ONE slot -- the index is the number the call carries.
|
|
139
|
+
// The DLL's own entry point is recorded as an alias of that slot, not as
|
|
140
|
+
// a second one: a source the user wrote calls the script-side name, while
|
|
141
|
+
// a source recovered from a compiled file names the entry point, because
|
|
142
|
+
// that is all the import table holds. Both must reach the same number.
|
|
143
|
+
const entry = m[1].indexOf('::') >= 0 ? m[1].split('::')[1] : '';
|
|
144
|
+
if (out.indexOf(m[2]) < 0) {
|
|
145
|
+
if (entry && entry !== m[2]) out.alias[entry] = m[2];
|
|
146
|
+
out.push(m[2]);
|
|
147
|
+
}
|
|
148
|
+
m = re.exec(text);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The include names a source text asks for, in order.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} src The source text.
|
|
158
|
+
* @returns {string[]} The include names.
|
|
159
|
+
*/
|
|
160
|
+
function ipofScanIncludes(src) {
|
|
161
|
+
const out = [];
|
|
162
|
+
const re = /^[ \t]*#\s*include\s*"([^"]+)"/gm;
|
|
163
|
+
let m = re.exec(src);
|
|
164
|
+
while (m) {
|
|
165
|
+
if (out.indexOf(m[1]) < 0) out.push(m[1]);
|
|
166
|
+
m = re.exec(src);
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Whether an include only declares things (no bodies to compile in).
|
|
173
|
+
*
|
|
174
|
+
* A `.h` is prototypes and globals; a `.SRC` library carries function bodies
|
|
175
|
+
* the compiled file must contain, so it is concatenated instead.
|
|
176
|
+
*
|
|
177
|
+
* @param {string} name The include name.
|
|
178
|
+
* @returns {boolean} True for a declarations-only header.
|
|
179
|
+
*/
|
|
180
|
+
function ipofIsDeclOnly(name) {
|
|
181
|
+
return /\.h$/i.test(name);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Resolve a script and its includes into one source text.
|
|
186
|
+
*
|
|
187
|
+
* `.h` headers contribute their globals (so slot numbering matches) and their
|
|
188
|
+
* prototypes; `.SRC` libraries contribute everything, bodies included, ahead of
|
|
189
|
+
* the main file -- which is the order the compiled artifacts show.
|
|
190
|
+
*
|
|
191
|
+
* @param {string} src The main script's text.
|
|
192
|
+
* @param {Object<string, string>} files The dropped files, name -> text; names
|
|
193
|
+
* are matched case-insensitively, as INPA's own tooling matches them.
|
|
194
|
+
* @returns {{text: string, used: string[], missing: string[]}} The combined
|
|
195
|
+
* source, the includes that resolved, and the ones that did not.
|
|
196
|
+
*/
|
|
197
|
+
function ipofResolveIncludes(src, files) {
|
|
198
|
+
const lower = {};
|
|
199
|
+
for (const k of Object.keys(files || {})) lower[k.toLowerCase()] = files[k];
|
|
200
|
+
/**
|
|
201
|
+
* An include's text, matching on its base name.
|
|
202
|
+
*
|
|
203
|
+
* Scripts include their headers by relative path (`..\sgdat\inpa.h`), and a
|
|
204
|
+
* dropped file has no path at all -- only its name. Matching the base name
|
|
205
|
+
* is what lets the user drop the header next to the script, which is the
|
|
206
|
+
* only way they can supply it.
|
|
207
|
+
*
|
|
208
|
+
* @param {string} name The include name as written.
|
|
209
|
+
* @returns {string|undefined} The text, or undefined.
|
|
210
|
+
*/
|
|
211
|
+
const find = (name) => {
|
|
212
|
+
const key = name.toLowerCase();
|
|
213
|
+
if (lower[key] !== undefined) return lower[key];
|
|
214
|
+
const base = key.split(/[\\/]/).pop();
|
|
215
|
+
return lower[base];
|
|
216
|
+
};
|
|
217
|
+
const used = [];
|
|
218
|
+
const missing = [];
|
|
219
|
+
const seen = {};
|
|
220
|
+
const parts = [];
|
|
221
|
+
/**
|
|
222
|
+
* Pull one include in, depth first, so a nested include's globals land first.
|
|
223
|
+
* @param {string} name The include name.
|
|
224
|
+
* @returns {void}
|
|
225
|
+
*/
|
|
226
|
+
const pull = (name) => {
|
|
227
|
+
const key = name.toLowerCase().split(/[\\/]/).pop();
|
|
228
|
+
if (seen[key]) return;
|
|
229
|
+
seen[key] = true;
|
|
230
|
+
const text = find(name);
|
|
231
|
+
if (text === undefined) {
|
|
232
|
+
missing.push(name);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
used.push(name);
|
|
236
|
+
for (const nested of ipofScanIncludes(text)) pull(nested);
|
|
237
|
+
parts.push(ipofStripPrototypes(text, ipofIsDeclOnly(name)));
|
|
238
|
+
};
|
|
239
|
+
for (const name of ipofScanIncludes(src)) pull(name);
|
|
240
|
+
parts.push(src.replace(/^[ \t]*#\s*include\s*"[^"]+"[ \t]*$/gm, ''));
|
|
241
|
+
return { text: parts.join('\n'), used, missing };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Drop the `extern` prototype lines from an include.
|
|
246
|
+
*
|
|
247
|
+
* A prototype declares a builtin's signature for the real compiler; this one
|
|
248
|
+
* numbers builtins from its own tables, so the lines are noise the parser
|
|
249
|
+
* would reject. A declarations-only header keeps its globals, which do matter:
|
|
250
|
+
* they occupy the leading slots.
|
|
251
|
+
*
|
|
252
|
+
* @param {string} text The include's text.
|
|
253
|
+
* @param {boolean} declOnly Whether the include is a `.h`.
|
|
254
|
+
* @returns {string} The text the parser can take.
|
|
255
|
+
*/
|
|
256
|
+
function ipofStripPrototypes(text, declOnly) {
|
|
257
|
+
// Newlines are preserved so a later error still names the line the user has
|
|
258
|
+
// in front of them. A prototype runs to its semicolon and may span lines --
|
|
259
|
+
// the import32 declarations put one parameter per line.
|
|
260
|
+
let out = text.replace(
|
|
261
|
+
/^[ \t]*(?:extern|import32|import)\b[\s\S]*?;/gm,
|
|
262
|
+
(m) => m.replace(/[^\n]/g, ' ')
|
|
263
|
+
);
|
|
264
|
+
out = out.replace(/^[ \t]*#\s*include\s*"[^"]+"[ \t]*$/gm, '');
|
|
265
|
+
void declOnly;
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Compile an INPA source script into a runnable exec.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} src The script's text.
|
|
273
|
+
* @param {Object} [opts] Options.
|
|
274
|
+
* @param {string} [opts.name] The stem to report as the exec's ecu.
|
|
275
|
+
* @param {Object<string, string>} [opts.files] Includes the user supplied,
|
|
276
|
+
* name -> text.
|
|
277
|
+
* @param {string[]} [opts.imports] DLL import names by index, when known.
|
|
278
|
+
* @returns {{ok: boolean, exec: Object|null, errors: Object[], includes:
|
|
279
|
+
* string[], missing: string[]}} The result; `errors` carries a `line` and a
|
|
280
|
+
* ready-to-show `text` for each problem.
|
|
281
|
+
*/
|
|
282
|
+
function ipofCompileSource(src, opts) {
|
|
283
|
+
const o = opts || {};
|
|
284
|
+
const r = ipofResolveIncludes(src, o.files || {});
|
|
285
|
+
const errors = [];
|
|
286
|
+
for (const m of r.missing) {
|
|
287
|
+
errors.push({
|
|
288
|
+
line: 0,
|
|
289
|
+
message: `missing include "${m}" -- drop it alongside the script`,
|
|
290
|
+
text: `missing include "${m}" -- drop it alongside the script`,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (errors.length) {
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
exec: null,
|
|
297
|
+
errors,
|
|
298
|
+
includes: r.used,
|
|
299
|
+
missing: r.missing,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
let ast;
|
|
303
|
+
try {
|
|
304
|
+
ast = ipofParse(r.text);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
if (!(err instanceof IpofSyntaxError)) throw err;
|
|
307
|
+
errors.push({ line: err.line, message: err.message, text: err.message });
|
|
308
|
+
return {
|
|
309
|
+
ok: false,
|
|
310
|
+
exec: null,
|
|
311
|
+
errors,
|
|
312
|
+
includes: r.used,
|
|
313
|
+
missing: r.missing,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const c = new IpofCompiler(ast, {
|
|
317
|
+
builtins: ipofBuiltinTable(),
|
|
318
|
+
// the caller's table (a compiled file's own) wins; failing that, what the
|
|
319
|
+
// includes declare, which is the only source a bare .IPS has
|
|
320
|
+
imports:
|
|
321
|
+
o.imports && o.imports.length
|
|
322
|
+
? o.imports
|
|
323
|
+
: ipofScanImports(o.files || {}),
|
|
324
|
+
protos: ipofScanPrototypes(o.files || {}),
|
|
325
|
+
});
|
|
326
|
+
c.ecu = o.name || 'script';
|
|
327
|
+
const exec = c.compile();
|
|
328
|
+
if (exec.errors.length) {
|
|
329
|
+
return {
|
|
330
|
+
ok: false,
|
|
331
|
+
exec: null,
|
|
332
|
+
errors: exec.errors,
|
|
333
|
+
includes: r.used,
|
|
334
|
+
missing: r.missing,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
ok: true,
|
|
339
|
+
exec: {
|
|
340
|
+
ecu: exec.ecu,
|
|
341
|
+
procs: exec.procs,
|
|
342
|
+
byid: exec.byid,
|
|
343
|
+
coding: false,
|
|
344
|
+
includes: ast.includes,
|
|
345
|
+
imports: {},
|
|
346
|
+
},
|
|
347
|
+
errors: [],
|
|
348
|
+
includes: r.used,
|
|
349
|
+
missing: [],
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
354
|
+
module.exports = {
|
|
355
|
+
ipofCompileSource,
|
|
356
|
+
ipofResolveIncludes,
|
|
357
|
+
ipofScanIncludes,
|
|
358
|
+
ipofBuiltinTable,
|
|
359
|
+
ipofScanPrototypes,
|
|
360
|
+
ipofScanImports,
|
|
361
|
+
IPOF_BUILTIN_NUMS,
|
|
362
|
+
IPOF_ALIAS_NUMS,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Procedure declarations and the Constant Data metadata of an .IPO.
|
|
3
|
+
*
|
|
4
|
+
* Port of tools/decompile/ipo_disasm.py's find_decls / body_start and of
|
|
5
|
+
* tools/decompile/ipo_source.py's _constant_data. A declaration is
|
|
6
|
+
*
|
|
7
|
+
* <type u8> <name> \n <u32 id> \n [version] \n \x00 <u16 dwords>
|
|
8
|
+
*
|
|
9
|
+
* and it is the trailing block header that makes it one -- anchoring on that
|
|
10
|
+
* is what separates real declarations from the tens of thousands of
|
|
11
|
+
* name-shaped byte runs a file contains.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Declaration type byte -> the table its id indexes. */
|
|
15
|
+
const IPOF_DECL_TYPES = {
|
|
16
|
+
1: 'screen',
|
|
17
|
+
2: 'menu',
|
|
18
|
+
3: 'state',
|
|
19
|
+
4: 'statemachine',
|
|
20
|
+
5: 'func',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Whether a byte can start a declaration name.
|
|
25
|
+
*
|
|
26
|
+
* @param {number} c Byte value.
|
|
27
|
+
* @returns {boolean} True for A-Z, a-z or underscore.
|
|
28
|
+
*/
|
|
29
|
+
function ipofNameStart(c) {
|
|
30
|
+
return (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) || c === 0x5f;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether a byte can continue a declaration name.
|
|
35
|
+
*
|
|
36
|
+
* @param {number} c Byte value.
|
|
37
|
+
* @returns {boolean} True for A-Z, a-z, 0-9 or underscore.
|
|
38
|
+
*/
|
|
39
|
+
function ipofNameChar(c) {
|
|
40
|
+
return ipofNameStart(c) || (c >= 0x30 && c <= 0x39);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Where a declaration's CODE begins.
|
|
45
|
+
*
|
|
46
|
+
* The version string between the id and the block is normally EMPTY (the two
|
|
47
|
+
* newlines sit side by side). One shipped file fills it in, and those extra
|
|
48
|
+
* bytes would put each of its procs three bytes into its own body, so the
|
|
49
|
+
* field's real length is read rather than assumed.
|
|
50
|
+
*
|
|
51
|
+
* @param {Uint8Array} data The file bytes.
|
|
52
|
+
* @param {number} off Offset of the declaration's type byte.
|
|
53
|
+
* @param {string} name The declared name.
|
|
54
|
+
* @returns {number} The offset of the first code byte.
|
|
55
|
+
*/
|
|
56
|
+
function ipofBodyStart(data, off, name) {
|
|
57
|
+
const i = off + 1 + name.length + 1 + 4;
|
|
58
|
+
if (data[i] === 0x0a) {
|
|
59
|
+
// a non-empty version string: skip its content, leave its newline
|
|
60
|
+
for (let j = i + 1; j < Math.min(data.length, i + 64); j += 1) {
|
|
61
|
+
if (data[j] === 0x0a) return j > i + 1 ? j : i + 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return i + 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Match one declaration at `at`, or null.
|
|
69
|
+
*
|
|
70
|
+
* The name length is bounded only by INPA's own limit: a 30-character cap
|
|
71
|
+
* silently hides procs whose names are longer, and a minimum length hides the
|
|
72
|
+
* one- and two-character procs some ECU families declare, whose bodies then
|
|
73
|
+
* decode as garbage inside whichever proc preceded them.
|
|
74
|
+
*
|
|
75
|
+
* @param {Uint8Array} data The file bytes.
|
|
76
|
+
* @param {number} at Offset of the candidate type byte.
|
|
77
|
+
* @returns {{off: number, typ: string, name: string, id: number}|null} The
|
|
78
|
+
* declaration, or null.
|
|
79
|
+
*/
|
|
80
|
+
function ipofMatchDecl(data, at) {
|
|
81
|
+
const typ = IPOF_DECL_TYPES[data[at]];
|
|
82
|
+
if (!typ) return null;
|
|
83
|
+
let i = at + 1;
|
|
84
|
+
if (!ipofNameStart(data[i])) return null;
|
|
85
|
+
const nameLo = i;
|
|
86
|
+
i += 1;
|
|
87
|
+
while (i < data.length && i - nameLo <= 60 && ipofNameChar(data[i])) i += 1;
|
|
88
|
+
if (data[i] !== 0x0a) return null;
|
|
89
|
+
const name = ipofLatin1(data, nameLo, i);
|
|
90
|
+
i += 1;
|
|
91
|
+
if (i + 4 > data.length) return null;
|
|
92
|
+
const id = ipofUint(data, i, 4);
|
|
93
|
+
i += 4;
|
|
94
|
+
if (data[i] !== 0x0a) return null;
|
|
95
|
+
i += 1;
|
|
96
|
+
// the optional version string, then the mandatory \x00 block header
|
|
97
|
+
let j = i;
|
|
98
|
+
const cap = Math.min(data.length, i + 33);
|
|
99
|
+
while (j < cap && data[j] !== 0x0a) {
|
|
100
|
+
if (data[j] < 0x20 || data[j] > 0x7e) return null;
|
|
101
|
+
j += 1;
|
|
102
|
+
}
|
|
103
|
+
if (j >= cap || data[j] !== 0x0a) return null;
|
|
104
|
+
if (data[j + 1] !== 0x00) return null;
|
|
105
|
+
return { off: at, typ, name, id };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Every procedure declaration in the code region.
|
|
110
|
+
*
|
|
111
|
+
* @param {Uint8Array} data The file bytes.
|
|
112
|
+
* @param {number} end End of the code region (the pool start or code end).
|
|
113
|
+
* @returns {Array<{off: number, typ: string, name: string, id: number}>} The
|
|
114
|
+
* declarations, in file order.
|
|
115
|
+
*/
|
|
116
|
+
function ipofFindDecls(data, end) {
|
|
117
|
+
const out = [];
|
|
118
|
+
const cap = Math.min(end, data.length);
|
|
119
|
+
for (let i = 0; i < cap; i += 1) {
|
|
120
|
+
if (!IPOF_DECL_TYPES[data[i]]) continue;
|
|
121
|
+
const d = ipofMatchDecl(data, i);
|
|
122
|
+
// ids above this are not proc ids; the Python drops them the same way
|
|
123
|
+
if (d && d.id <= 2000) out.push(d);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The include list and the DLL import table from the Constant Data entries.
|
|
130
|
+
*
|
|
131
|
+
* The leading string entries are the `#include` names; any entry shaped like
|
|
132
|
+
* `lib::Function:signature%...` is an import32 signature, and `dllcall #n`
|
|
133
|
+
* names entry n.
|
|
134
|
+
*
|
|
135
|
+
* @param {Array<Array>} entries The pool entries.
|
|
136
|
+
* @returns {{includes: string[], imports: Object<number, string>}} The include
|
|
137
|
+
* names in order, and imports by pool index.
|
|
138
|
+
*/
|
|
139
|
+
function ipofConstantData(entries) {
|
|
140
|
+
const includes = [];
|
|
141
|
+
for (const e of entries) {
|
|
142
|
+
if (e[0] !== 's') break;
|
|
143
|
+
includes.push(e[1]);
|
|
144
|
+
}
|
|
145
|
+
const imports = {};
|
|
146
|
+
entries.forEach((e, i) => {
|
|
147
|
+
if (e[0] === 's' && e[1].indexOf('::') >= 0 && e[1].indexOf('%') >= 0) {
|
|
148
|
+
imports[i] = e[1].split('::')[1].split(':')[0];
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
return { includes, imports };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The include names as the Constant Data region lists them, read straight from
|
|
156
|
+
* the bytes for the dialects whose pool did not decode.
|
|
157
|
+
*
|
|
158
|
+
* @param {Uint8Array} data The file bytes.
|
|
159
|
+
* @returns {string[]} The include names.
|
|
160
|
+
*/
|
|
161
|
+
function ipofIncludesFromBytes(data) {
|
|
162
|
+
const marker = [0x12].concat(
|
|
163
|
+
Array.from('Constant Data').map((c) => c.charCodeAt(0)),
|
|
164
|
+
[0x0a]
|
|
165
|
+
);
|
|
166
|
+
const hits = ipofFindAll(data, marker, 0, data.length);
|
|
167
|
+
if (!hits.length) return [];
|
|
168
|
+
const out = [];
|
|
169
|
+
let i = hits[0] + marker.length;
|
|
170
|
+
while (i < data.length && data[i] === 6) {
|
|
171
|
+
const j = ipofFindNl(data, i + 1, data.length);
|
|
172
|
+
if (j < 0) break;
|
|
173
|
+
out.push(ipofLatin1(data, i + 1, j));
|
|
174
|
+
i = j + 1;
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
180
|
+
module.exports = {
|
|
181
|
+
ipofFindDecls,
|
|
182
|
+
ipofBodyStart,
|
|
183
|
+
ipofConstantData,
|
|
184
|
+
ipofIncludesFromBytes,
|
|
185
|
+
IPOF_DECL_TYPES,
|
|
186
|
+
};
|
|
187
|
+
}
|