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
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bmweb-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "BMWeb's tools as a command line: read INPA .IPO scripts, compile .IPS sources, search the corpus job index, decode and diff shared Garage reports, and, over a K+DCAN cable, run jobs, whole-car scans and INPA screens in the terminal.",
|
|
5
|
+
"license": "GPL-3.0-only",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"bmweb": "dist/bmweb.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/",
|
|
12
|
+
"runtime/",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/dader34/BMWeb.git",
|
|
22
|
+
"directory": "cli"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://bmweb.danner.ink/",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/dader34/BMWeb/issues"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"bmw",
|
|
30
|
+
"inpa",
|
|
31
|
+
"diagnostics",
|
|
32
|
+
"ediabas",
|
|
33
|
+
"obd",
|
|
34
|
+
"cli"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"sync": "node scripts/sync-runtime.mjs",
|
|
38
|
+
"build": "npm run sync && node scripts/build.mjs && tsc --noEmit -p tsconfig.json",
|
|
39
|
+
"test": "node --test dist-test/*.test.js",
|
|
40
|
+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"scripts/**/*.mjs\"",
|
|
41
|
+
"format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\" \"scripts/**/*.mjs\"",
|
|
42
|
+
"prepack": "npm run build"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^20.19.0",
|
|
46
|
+
"esbuild": "^0.28.2",
|
|
47
|
+
"prettier": "^3.9.6",
|
|
48
|
+
"typescript": "^7.0.2"
|
|
49
|
+
},
|
|
50
|
+
"optionalDependencies": {
|
|
51
|
+
"serialport": "^13.0.0"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Encodings the VM shares with its callers: CP1252 text <-> bytes,
|
|
3
|
+
* the engine's number parsers (StringToValue, float constants), the float
|
|
4
|
+
* formatter behind `flt2a`, little-endian byte folding, and the
|
|
5
|
+
* set_communication_pars decoder. Pure functions; no VM state.
|
|
6
|
+
*
|
|
7
|
+
* Best2Vm re-exposes the text and number codecs as static methods
|
|
8
|
+
* (machine.js) because callers outside the VM address them that way.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The upper half of Windows-1252, the engine's ambient Encoding -- NOT
|
|
13
|
+
* latin-1. Bytes 0x80..0x9F are printable there (0x96 is an en dash), and
|
|
14
|
+
* decoding them as latin-1 control characters turned "LLR - Solldrehzahl"
|
|
15
|
+
* into a C1 escape. Index = byte - 0x80, value = the Unicode code point.
|
|
16
|
+
* @type {number[]}
|
|
17
|
+
*/
|
|
18
|
+
const CP1252_HIGH = [
|
|
19
|
+
0x20ac, 0x81, 0x201a, 0x0192, 0x201e, 0x2026, 0x2020, 0x2021, 0x02c6, 0x2030,
|
|
20
|
+
0x0160, 0x2039, 0x0152, 0x8d, 0x017d, 0x8f, 0x90, 0x2018, 0x2019, 0x201c,
|
|
21
|
+
0x201d, 0x2022, 0x2013, 0x2014, 0x02dc, 0x2122, 0x0161, 0x203a, 0x0153, 0x9d,
|
|
22
|
+
0x017e, 0x0178,
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/** The byte a character outside CP1252 becomes when written back: '?'. */
|
|
26
|
+
const CP1252_REPLACEMENT = 0x3f;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Significant digits `flt2a` keeps: the engine's _floatPrecision default.
|
|
30
|
+
*/
|
|
31
|
+
const FLOAT_PRECISION = 4;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Wire parameters decoded from a set_communication_pars blob (`xsetpar`).
|
|
35
|
+
* The transport reads the concept to frame, checksum and pace an exchange;
|
|
36
|
+
* the VM never touches the port itself.
|
|
37
|
+
* @typedef {Object} CommParams
|
|
38
|
+
* @property {number} concept - The protocol concept (1..6 K-line variants,
|
|
39
|
+
* 0x10B..0x10D KWP2000, 0x10F BMW-FAST, 0x110 D-CAN).
|
|
40
|
+
* @property {number} baud - Line speed in baud.
|
|
41
|
+
* @property {?number} timeout - Answer timeout (time to first byte), ms.
|
|
42
|
+
* @property {?number} regen - Regeneration gap between telegrams, ms.
|
|
43
|
+
* @property {?number} telEnd - Telegram-end / response-pending timeout, ms.
|
|
44
|
+
* @property {?number} timeoutNr78 - The 0x78 busy-wait timeout, ms.
|
|
45
|
+
* @property {number[]} params - The raw CommParameter words.
|
|
46
|
+
* @property {number[]} [answerLen] - set_answer_length pairs (`xawlen`).
|
|
47
|
+
* @property {number} [waitMs] - A `wait`/`waitex` pause owed before the
|
|
48
|
+
* next exchange.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Text and number codecs, namespaced so the pieces share one implementation.
|
|
53
|
+
*/
|
|
54
|
+
const Best2Codec = {
|
|
55
|
+
CP1252_HIGH,
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Bytes -> text, CP1252.
|
|
59
|
+
* @param {Uint8Array|number[]} b - Raw bytes.
|
|
60
|
+
* @returns {string} The decoded text, one character per byte.
|
|
61
|
+
*/
|
|
62
|
+
bytesStr(b) {
|
|
63
|
+
let s = '';
|
|
64
|
+
for (const x of b) {
|
|
65
|
+
s += String.fromCharCode(
|
|
66
|
+
x >= 0x80 && x <= 0x9f ? CP1252_HIGH[x - 0x80] : x
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return s;
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Text -> bytes, CP1252: the inverse of bytesStr, for text written back
|
|
74
|
+
* into a byte buffer. A character with no CP1252 byte becomes '?'.
|
|
75
|
+
* @param {string} str - The text.
|
|
76
|
+
* @returns {Uint8Array} One byte per character.
|
|
77
|
+
*/
|
|
78
|
+
strBytesCp1252(str) {
|
|
79
|
+
const out = new Uint8Array(str.length);
|
|
80
|
+
for (let i = 0; i < str.length; i++) {
|
|
81
|
+
const c = str.charCodeAt(i);
|
|
82
|
+
if (c <= 0xff) {
|
|
83
|
+
out[i] = c;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const k = CP1252_HIGH.indexOf(c);
|
|
87
|
+
out[i] = k >= 0 ? 0x80 + k : CP1252_REPLACEMENT;
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Any value -> CP1252 bytes of its string form (null/undefined -> empty).
|
|
94
|
+
* @param {*} s - The value to encode.
|
|
95
|
+
* @returns {Uint8Array} The encoded bytes.
|
|
96
|
+
*/
|
|
97
|
+
strBytes(s) {
|
|
98
|
+
return Best2Codec.strBytesCp1252(String(s ?? ''));
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* NUL-terminated text, the way result strings are published: decode up to
|
|
103
|
+
* the first NUL byte.
|
|
104
|
+
* @param {Uint8Array|number[]} b - Raw bytes.
|
|
105
|
+
* @returns {string} The text before the first NUL.
|
|
106
|
+
*/
|
|
107
|
+
cstr(b) {
|
|
108
|
+
const z = b.indexOf(0);
|
|
109
|
+
return Best2Codec.bytesStr(z < 0 ? b : b.slice(0, z));
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* EDIABAS's StringToValue: 0x hex, 0y binary, else decimal truncated at
|
|
114
|
+
* the first '.' or ','. Unparseable yields 0. This is what table cells
|
|
115
|
+
* are run through by tabseeku, so "0x10" seeks as 16.
|
|
116
|
+
*
|
|
117
|
+
* StringToValue uses Convert.ToInt64, which is ALL-OR-NOTHING: it
|
|
118
|
+
* throws on trailing junk and the caller catches it as 0. parseInt
|
|
119
|
+
* happily takes a valid prefix, and that difference is visible --
|
|
120
|
+
* y2bcd renders a non-BCD nibble as '*', so LSZ's ID_HW_NR is the text
|
|
121
|
+
* "2*", which the engine reports as 0 and parseInt would call 2.
|
|
122
|
+
* @param {*} s - The text to parse.
|
|
123
|
+
* @returns {number} The integer value, or 0.
|
|
124
|
+
*/
|
|
125
|
+
strToValue(s) {
|
|
126
|
+
const t = String(s ?? '').replace(/\s+$/, '');
|
|
127
|
+
if (!t) return 0;
|
|
128
|
+
const low = t.toLowerCase();
|
|
129
|
+
if (low.startsWith('0x')) {
|
|
130
|
+
const body = t.slice(2);
|
|
131
|
+
if (!/^[0-9a-fA-F]+$/.test(body)) return 0;
|
|
132
|
+
const v = parseInt(body, 16);
|
|
133
|
+
return Number.isFinite(v) ? v : 0;
|
|
134
|
+
}
|
|
135
|
+
if (low.startsWith('0y')) {
|
|
136
|
+
const body = t.slice(2);
|
|
137
|
+
if (!/^[01]+$/.test(body)) return 0;
|
|
138
|
+
const v = parseInt(body, 2);
|
|
139
|
+
return Number.isFinite(v) ? v : 0;
|
|
140
|
+
}
|
|
141
|
+
if (low === '-' || low === '--') return 0;
|
|
142
|
+
if (/^[a-z]/i.test(low)) return 0; // a leading letter rejects it
|
|
143
|
+
// decimal, truncated at the first '.' or ',' -- then it must be a
|
|
144
|
+
// COMPLETE integer or the conversion fails
|
|
145
|
+
const cut = t.trimStart().split(/[.,]/)[0];
|
|
146
|
+
if (!/^[+-]?[0-9]+$/.test(cut)) return 0;
|
|
147
|
+
const v = parseInt(cut, 10);
|
|
148
|
+
return Number.isFinite(v) ? v : 0;
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A float constant. EDIABAS writes them with either decimal separator.
|
|
153
|
+
* @param {*} s - The text to parse.
|
|
154
|
+
* @returns {number} The value, or 0 when it does not parse.
|
|
155
|
+
*/
|
|
156
|
+
parseNum(s) {
|
|
157
|
+
if (s === undefined || s === null) return 0;
|
|
158
|
+
const v = parseFloat(String(s).replace(',', '.'));
|
|
159
|
+
return Number.isFinite(v) ? v : 0;
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* OpFlt2A, ported faithfully: round to floatPrecision (default 4)
|
|
164
|
+
* SIGNIFICANT digits (RoundToSignificantDigits, Math.Round = half-even),
|
|
165
|
+
* format invariant, then CUT THE STRING after the Nth digit character --
|
|
166
|
+
* including the engine's own quirk of truncating an exponent's digits.
|
|
167
|
+
* JS shortest-roundtrip formatting ("0.30000000000000004") never appears:
|
|
168
|
+
* the rounding and the cut both bound it.
|
|
169
|
+
* @param {number} value - The float to format.
|
|
170
|
+
* @returns {string} The engine's text for it.
|
|
171
|
+
*/
|
|
172
|
+
fltText(value) {
|
|
173
|
+
const digits = FLOAT_PRECISION;
|
|
174
|
+
let v = Number(value);
|
|
175
|
+
if (Number.isFinite(v) && v !== 0) {
|
|
176
|
+
const scale = 10 ** (Math.floor(Math.log10(Math.abs(v))) + 1);
|
|
177
|
+
const x = (v / scale) * 10 ** digits;
|
|
178
|
+
let r = Math.round(x); // JS rounds half toward +inf...
|
|
179
|
+
if (Math.abs(x % 1) === 0.5 && r % 2 !== 0) r -= 1;
|
|
180
|
+
v = scale * (r / 10 ** digits); // ...Math.Round is half to even
|
|
181
|
+
}
|
|
182
|
+
let s = String(v);
|
|
183
|
+
const em = /^(-?[\d.]+)e([+-])(\d+)$/.exec(s);
|
|
184
|
+
if (em) s = `${em[1]}E${em[2]}${em[3].padStart(2, '0')}`;
|
|
185
|
+
let count = 0;
|
|
186
|
+
for (let i = 0; i < s.length; i++) {
|
|
187
|
+
if (s[i] >= '0' && s[i] <= '9') {
|
|
188
|
+
count++;
|
|
189
|
+
if (count >= digits) {
|
|
190
|
+
s = s.slice(0, i + 1);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return s;
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Fold `n` bytes at `offset` into an unsigned number, LITTLE-endian (byte
|
|
200
|
+
* 0 is the low byte), zero-padding when the slice is short -- the way
|
|
201
|
+
* Register.GetValueData and Operand.GetValueData assemble a value.
|
|
202
|
+
* @param {Uint8Array|number[]} buf - The bytes.
|
|
203
|
+
* @param {number} offset - Index of the low byte.
|
|
204
|
+
* @param {number} n - How many bytes to fold.
|
|
205
|
+
* @returns {number} The unsigned value.
|
|
206
|
+
*/
|
|
207
|
+
leValue(buf, offset, n) {
|
|
208
|
+
let v = 0;
|
|
209
|
+
for (let k = n - 1; k >= 0; k--) v = v * 256 + (buf[offset + k] || 0);
|
|
210
|
+
return v;
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Serialize an unsigned number as `n` bytes, LITTLE-endian (low byte
|
|
215
|
+
* first); higher bytes are dropped.
|
|
216
|
+
* @param {number} value - The value (already non-negative).
|
|
217
|
+
* @param {number} n - How many bytes to write.
|
|
218
|
+
* @returns {Uint8Array} The bytes.
|
|
219
|
+
*/
|
|
220
|
+
leBytes(value, n) {
|
|
221
|
+
const out = new Uint8Array(n);
|
|
222
|
+
let v = value;
|
|
223
|
+
for (let k = 0; k < n; k++) {
|
|
224
|
+
out[k] = v & 0xff;
|
|
225
|
+
v = Math.floor(v / 256);
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* set_communication_pars, decoded the way EdInterfaceObd.cs reads it: the
|
|
232
|
+
* answer timeout, the regeneration gap and the response-pending timeout
|
|
233
|
+
* sit at a concept-specific index in the CommParameter words. The old
|
|
234
|
+
* "largest plausible timing word" guess picked ParTimeoutNr78 (5000 ms,
|
|
235
|
+
* the 0x78 busy-wait) as the answer timeout on every 0x1xx concept, so a
|
|
236
|
+
* silent KWP2000* probe cost 5 s where EDIABAS waits 500 ms -- the whole
|
|
237
|
+
* "MS45 takes ages to identify the first time".
|
|
238
|
+
* concepts 1,2,3,5,6 (ISO 9141, KWP1281, DS1/DS2): [5] [6] [7]
|
|
239
|
+
* 0x10B/0x10C/0x10D (KWP2000 std/BMW/*): [2] [3] [4], Nr78 [7]
|
|
240
|
+
* 0x10F (BMW-FAST): [2] [3] [4], Nr78 [6]
|
|
241
|
+
* 0x110 (D-CAN): [7] [8], Nr78 [9]
|
|
242
|
+
* @param {number[]} words - The CommParameter words, concept first.
|
|
243
|
+
* @returns {CommParams} The decoded wire parameters.
|
|
244
|
+
*/
|
|
245
|
+
decodeCommParams(words) {
|
|
246
|
+
const c = words[0];
|
|
247
|
+
const at = (i) => (i < words.length && words[i] > 0 ? words[i] : null);
|
|
248
|
+
let timeout = null,
|
|
249
|
+
regen = null,
|
|
250
|
+
telEnd = null,
|
|
251
|
+
timeoutNr78 = null;
|
|
252
|
+
if (c >= 0x1 && c <= 0x6) {
|
|
253
|
+
timeout = at(5);
|
|
254
|
+
regen = at(6);
|
|
255
|
+
telEnd = at(7);
|
|
256
|
+
} else if (c === 0x10b || c === 0x10c || c === 0x10d) {
|
|
257
|
+
timeout = at(2);
|
|
258
|
+
regen = at(3);
|
|
259
|
+
telEnd = at(4);
|
|
260
|
+
timeoutNr78 = at(7);
|
|
261
|
+
} else if (c === 0x10f) {
|
|
262
|
+
timeout = at(2);
|
|
263
|
+
regen = at(3);
|
|
264
|
+
telEnd = at(4);
|
|
265
|
+
timeoutNr78 = at(6);
|
|
266
|
+
} else if (c === 0x110) {
|
|
267
|
+
timeout = at(7);
|
|
268
|
+
regen = at(8);
|
|
269
|
+
timeoutNr78 = at(9);
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
concept: c,
|
|
273
|
+
baud: words[1],
|
|
274
|
+
timeout,
|
|
275
|
+
regen,
|
|
276
|
+
telEnd,
|
|
277
|
+
timeoutNr78,
|
|
278
|
+
params: words,
|
|
279
|
+
};
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
284
|
+
module.exports = { Best2Codec, CP1252_HIGH, FLOAT_PRECISION };
|
|
285
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The job's environment: the constant pool, the argument list,
|
|
3
|
+
* shared-memory keys and SGBD table lookup. Extends Best2Vm (machine.js).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
if (typeof require === 'function' && typeof module !== 'undefined') {
|
|
7
|
+
Object.assign(globalThis, require('./machine.js'), require('./operands.js'));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A cell by column name, the exact name preferred and a case-insensitive
|
|
12
|
+
* match otherwise (TableNameDict is keyed on ToUpper; so are the column
|
|
13
|
+
* lookups of tabseek and tabget).
|
|
14
|
+
* @param {import('./machine.js').TableRow} row - The row.
|
|
15
|
+
* @param {string} col - The column name.
|
|
16
|
+
* @returns {string|undefined} The cell, or undefined when no column matches.
|
|
17
|
+
*/
|
|
18
|
+
function sgbdTableCell(row, col) {
|
|
19
|
+
if (row[col] !== undefined) return row[col];
|
|
20
|
+
const k = Object.keys(row).find(
|
|
21
|
+
(x) => x.toUpperCase() === String(col).toUpperCase()
|
|
22
|
+
);
|
|
23
|
+
return k === undefined ? undefined : row[k];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Object.assign(Best2Vm.prototype, {
|
|
27
|
+
/**
|
|
28
|
+
* A pool entry as TEXT: a byte-array literal is NUL-terminated text.
|
|
29
|
+
* @param {number} i - The constant-pool index.
|
|
30
|
+
* @returns {string} The text ('' for a missing entry).
|
|
31
|
+
*/
|
|
32
|
+
lit(i) {
|
|
33
|
+
const v = this.code.strings[i];
|
|
34
|
+
if (Array.isArray(v)) return Best2Codec.cstr(Uint8Array.from(v));
|
|
35
|
+
return v ?? '';
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A shared-data key. GetStringData stops at the first NUL, so a key that
|
|
40
|
+
* IS a NUL byte reads as the empty string -- which is a perfectly valid
|
|
41
|
+
* key, and the one MS450's AIF block uses. Uppercased, as the engine does.
|
|
42
|
+
* @param {import('./machine.js').Operand} op - The key operand.
|
|
43
|
+
* @returns {string} The key.
|
|
44
|
+
*/
|
|
45
|
+
shmKey(op) {
|
|
46
|
+
const raw =
|
|
47
|
+
op[0] === OpMode.IMM_STR ? this.code.strings[op[1]] : this.bytes(op);
|
|
48
|
+
const bytes = Array.isArray(raw) ? Uint8Array.from(raw) : raw;
|
|
49
|
+
return Best2Codec.cstr(bytes).toUpperCase();
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Job arguments split on ';'. An EMPTY argument string is ZERO
|
|
54
|
+
* parameters (GetActiveArgStrings only splits when length > 0), not one
|
|
55
|
+
* empty string -- otherwise `parn` reports 1 and every arg guard inverts.
|
|
56
|
+
* @returns {string[]} The arguments.
|
|
57
|
+
*/
|
|
58
|
+
args() {
|
|
59
|
+
if (this._args === undefined) {
|
|
60
|
+
this._args = this.argText.length > 0 ? this.argText.split(';') : [];
|
|
61
|
+
}
|
|
62
|
+
return this._args;
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Case-insensitive table lookup in the SGBD's own tables, with the exact
|
|
67
|
+
* name preferred.
|
|
68
|
+
* @param {string} name - The table name.
|
|
69
|
+
* @returns {?import('./machine.js').TableRow[]} The rows, or null.
|
|
70
|
+
*/
|
|
71
|
+
findTable(name) {
|
|
72
|
+
if (!name) return null;
|
|
73
|
+
if (this.tables[name]) return this.tables[name];
|
|
74
|
+
const want = String(name).toUpperCase();
|
|
75
|
+
if (!this._tabIndex) {
|
|
76
|
+
this._tabIndex = new Map();
|
|
77
|
+
for (const k of Object.keys(this.tables)) {
|
|
78
|
+
this._tabIndex.set(k.toUpperCase(), this.tables[k]);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return this._tabIndex.get(want) || null;
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A table in ANOTHER best file (`tabsetex "Name", "file"`). Both levels
|
|
86
|
+
* case-insensitive, like TableNameDict and the engine's file lookup.
|
|
87
|
+
* Per OpTabsetex (EdOperations.cs): a NON-empty file name switches the
|
|
88
|
+
* table stream to that file and looks the table up THERE -- there is no
|
|
89
|
+
* fallback to the current SGBD's own tables.
|
|
90
|
+
* @param {string} file - The bare best-file name.
|
|
91
|
+
* @param {string} name - The table name.
|
|
92
|
+
* @returns {?import('./machine.js').TableRow[]} The rows, or null.
|
|
93
|
+
*/
|
|
94
|
+
findExtTable(file, name) {
|
|
95
|
+
if (!file || !name) return null;
|
|
96
|
+
const wantFile = String(file).toUpperCase();
|
|
97
|
+
let group = null;
|
|
98
|
+
for (const k of Object.keys(this.extTables)) {
|
|
99
|
+
if (k.toUpperCase() === wantFile) {
|
|
100
|
+
group = this.extTables[k];
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!group) return null;
|
|
105
|
+
if (group[name]) return group[name];
|
|
106
|
+
const want = String(name).toUpperCase();
|
|
107
|
+
for (const k of Object.keys(group)) {
|
|
108
|
+
if (k.toUpperCase() === want) return group[k];
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
115
|
+
module.exports = { sgbdTableCell };
|
|
116
|
+
}
|