@henols/c64-re-tools 0.2.1 → 0.2.3
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/README.md +1 -1
- package/THIRD-PARTY-NOTICES.md +26 -0
- package/bin/cli.mjs +18 -7
- package/package.json +6 -4
- package/skills/acme-build/SKILL.md +83 -33
- package/skills/acme-build/scripts/acme.mjs +159 -64
- package/skills/acme-build/template.a +1 -1
- package/skills/c64-disk-access/SKILL.md +156 -0
- package/skills/c64-disk-access/scripts/c1541.mjs +569 -0
- package/skills/c64-memory-mapping/SKILL.md +419 -23
- package/skills/c64-memory-mapping/scripts/driver.mjs +1 -1
- package/skills/c64-petcat/SKILL.md +87 -0
- package/skills/c64-petcat/scripts/petcat.mjs +221 -0
- package/skills/c64-program-recon/SKILL.md +497 -92
- package/skills/c64-program-recon/references/control-flow.md +12 -15
- package/skills/c64-program-recon/references/graphics.md +1 -1
- package/skills/c64-program-recon/references/observation-hazards.md +18 -16
- package/skills/c64-program-recon/references/reconstruction.md +11 -6
- package/skills/c64-program-recon/references/sound-and-input.md +6 -8
- package/skills/c64-program-recon/references/tool-selection.md +37 -18
- package/skills/c64-program-recon/scripts/packer-finding.mjs +709 -0
- package/skills/c64-program-recon/templates/memory-map.template.md +27 -13
- package/skills/c64-provenance-diff/SKILL.md +43 -8
- package/skills/c64-provenance-diff/scripts/diff-images.mjs +9 -6
- package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +20 -8
- package/skills/c64-ram-capture/RELEASES.json.example +17 -0
- package/skills/c64-ram-capture/SKILL.md +147 -46
- package/skills/c64-ram-capture/scripts/compare.mjs +2 -2
- package/skills/c64-ram-capture/scripts/derive-transients.mjs +575 -0
- package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +3 -3
- package/skills/c64-ram-capture/scripts/mcp-module.mjs +174 -0
- package/skills/c64-ram-capture/scripts/project-paths.mjs +1 -1
- package/skills/c64-ram-capture/scripts/releases.mjs +1 -1
- package/skills/c64-ram-capture/scripts/vsf-slice.mjs +147 -0
- package/skills/c64-ram-capture/scripts/watch-loads.mjs +19 -13
- package/skills/c64-ram-capture/templates/capture-record.template.md +44 -4
- package/skills/c64-ram-capture/transients/README.md +136 -0
- package/skills/routine-queue-walker/SKILL.md +365 -0
- package/skills/routine-queue-walker/scripts/completeness-report.mjs +463 -0
- package/skills/vice-wedge-triage/SKILL.md +104 -97
- package/skills/c64-provenance-diff/scripts/diff-images.test.mjs +0 -665
- package/skills/c64-ram-capture/scripts/d64-parse.mjs +0 -243
- package/skills/c64-ram-capture/scripts/d64-parse.test.mjs +0 -243
- package/skills/c64-ram-capture/scripts/dump-artifacts.test.mjs +0 -133
- package/skills/c64-ram-capture/scripts/test-corpus.mjs +0 -75
- package/skills/c64-ram-capture/scripts/watch-loads.test.mjs +0 -339
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// packer-finding.mjs -- the ONE place this project answers "which packer was
|
|
3
|
+
// used on this binary", as a project-owned recon finding with an ordered
|
|
4
|
+
// oracle chain and a hard, reasoned unknown.
|
|
5
|
+
//
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// WHY THIS FILE EXISTS
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// DATED PROVENANCE (2026-08-29): the retired static analyser this project once
|
|
10
|
+
// rented -- the external analyser, pinned at 0.9.20 -- computed packer identity on
|
|
11
|
+
// EVERY load and then threw it away before it reached any machine-readable
|
|
12
|
+
// surface. That analyser is GONE from this repository; nothing below calls it,
|
|
13
|
+
// and this paragraph is history in the past tense, not a route. It is kept
|
|
14
|
+
// because it is the whole reason this file exists: without it, the obvious
|
|
15
|
+
// next step is to go looking for the packer-name call that was already proven
|
|
16
|
+
// not to be there. At the pin, that absence was established four independent
|
|
17
|
+
// ways, each read at the pin:
|
|
18
|
+
//
|
|
19
|
+
// (a) the curated binary-info tool emits a fixed seven-field object
|
|
20
|
+
// (origin, size, system, filename, description, an illegal-opcode hint
|
|
21
|
+
// and entropy) built from a static literal -- a packed input cannot
|
|
22
|
+
// change the field set;
|
|
23
|
+
// (b) the unpack result carries no name field at all: it returns data,
|
|
24
|
+
// start/end addresses, an entry point, a dependency address and an
|
|
25
|
+
// instruction count, and the one place the detected packer is consulted
|
|
26
|
+
// applies a memory patch and feeds a progress callback the MCP handler
|
|
27
|
+
// passes nothing to;
|
|
28
|
+
// (c) the loaded-project value that DOES hold a detected packer name is an
|
|
29
|
+
// in-memory return value only -- it is absent from the serialised
|
|
30
|
+
// project shape, so saving and re-reading the project file yields
|
|
31
|
+
// nothing;
|
|
32
|
+
// (d) every consumer of the name lives in the terminal-UI crate. There are
|
|
33
|
+
// zero non-UI, non-internal consumers, and the command line has no flag
|
|
34
|
+
// that reports file info.
|
|
35
|
+
//
|
|
36
|
+
// So there was no read-only route to a packer name in that analyser, and there
|
|
37
|
+
// is none on the surface that replaced it either: the annotation store holds
|
|
38
|
+
// annotations, the derived reads decode instructions, and neither answers
|
|
39
|
+
// "which packer". The two routes that would produce a name -- copying that
|
|
40
|
+
// upstream project's signature table (or transcribing its bytes into search
|
|
41
|
+
// patterns), and inventing a tool name on this project's own surface that no
|
|
42
|
+
// verb actually implements -- are both refused: the first is the copy this
|
|
43
|
+
// milestone exists to avoid, and the second would be a fabricated tool that
|
|
44
|
+
// this repository's own honesty gates would then treat as legitimate. What is
|
|
45
|
+
// left, and what this file is, is a project-owned finding with an EXTERNAL
|
|
46
|
+
// oracle and an explicit unknown.
|
|
47
|
+
//
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// WHAT THIS IS THE ONE AUTHORITATIVE PLACE FOR
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// - the ordered oracle chain (external identifier, then a packedness-only
|
|
52
|
+
// entropy gate, then an explicit unknown) and its first-hit-wins rule;
|
|
53
|
+
// - the finding's response shape and its four-verdict vocabulary
|
|
54
|
+
// (`PACKER_VERDICTS`);
|
|
55
|
+
// - the external identifier's probe (`probeUnp64`) and the defensive parser
|
|
56
|
+
// for its standard output (`parseUnp64Stdout`).
|
|
57
|
+
//
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// WHAT NOT TO DO -- the four rules, each enforced structurally below
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// 1. NEVER write the `packer` field from anything but an oracle's own
|
|
62
|
+
// verbatim output. Not from entropy, not from a decompression or
|
|
63
|
+
// dependency address, not from a byte pattern. There is exactly ONE
|
|
64
|
+
// assignment of that field in this module, inside `identifiedByOracle()`
|
|
65
|
+
// below, and it refuses a value the oracle did not supply.
|
|
66
|
+
// 2. NEVER return the high confidence level off the oracle route. The
|
|
67
|
+
// entropy route is MEDIUM and answers packedness only; the no-route case
|
|
68
|
+
// is LOW and answers `unknown`. `CONFIDENCE_HIGH` is ASSIGNED at exactly
|
|
69
|
+
// one site in this file, in the oracle branch, and the colocated test
|
|
70
|
+
// counts that site at source level.
|
|
71
|
+
// 3. NEVER return the `unknown` verdict with an empty reason. A silent null
|
|
72
|
+
// with nothing said about it is the failure mode this whole file exists
|
|
73
|
+
// to prevent, so `unknownFinding()` throws rather than build one. More
|
|
74
|
+
// generally: every finding whose `packer` is null states, in
|
|
75
|
+
// `unavailableReason`, why no name is being reported.
|
|
76
|
+
// 4. NEVER add vocabulary beyond the four verdicts. No fractional rating, no
|
|
77
|
+
// ratio, no hedged phrasing, no named-but-unconfirmed packer. A reader
|
|
78
|
+
// must get either a name an oracle stated or an explicit unknown, and
|
|
79
|
+
// nothing in between.
|
|
80
|
+
//
|
|
81
|
+
// Also: NEVER invoke the oracle through a command interpreter. Every child
|
|
82
|
+
// process below is launched with an argument ARRAY and an explicitly disabled
|
|
83
|
+
// interpreter, and a configured oracle path that does not exist on disk is
|
|
84
|
+
// treated as oracle-absent rather than being placed into any command anywhere
|
|
85
|
+
// (T-19-18). The oracle's standard output reaches exactly one field, through
|
|
86
|
+
// one bounded parser that evaluates nothing (T-19-19).
|
|
87
|
+
//
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// STATUS OF THE ORACLE BRANCH (recorded, deliberate)
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// The env-var convention below (`UNP64`, then `UNP64_PATH`) is the same one
|
|
92
|
+
// upstream's own comparison harness uses, and is verified. The SHAPE of the
|
|
93
|
+
// identifier's standard output is NOT verified here -- the tool was not
|
|
94
|
+
// installed on the machine where this was written, so `parseUnp64Stdout()` is
|
|
95
|
+
// written defensively and its accepted marker set is a stated assumption, not
|
|
96
|
+
// a measurement. Installing the identifier and running it against a genuinely
|
|
97
|
+
// packed fixture is the experiment that would settle it; until then the
|
|
98
|
+
// oracle-route test SKIPS with a visible reason and never reads as a pass.
|
|
99
|
+
import { execFileSync } from "node:child_process";
|
|
100
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
101
|
+
import { basename, dirname, resolve } from "node:path";
|
|
102
|
+
|
|
103
|
+
import { resolveMcpModule, refusalMessage } from "../../c64-ram-capture/scripts/mcp-module.mjs";
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// The oracle's own child-process spawn.
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Both `probeUnp64()` and `runUnp64()` used to spawn `unp64` directly
|
|
109
|
+
// (`spawnSync`, and a `mkdtempSync()`-created scratch directory for the
|
|
110
|
+
// unpacked output). Both spawn sites are now behind the host-tool execution
|
|
111
|
+
// seam (`src/mcp/vice/host-tool.mts`'s `oracle.probe`/`oracle.run` allowlist
|
|
112
|
+
// entries) -- the project owner's rule of 2026-08-28 is that this script
|
|
113
|
+
// runs container-side, the oracle binary lives host-side, and there is no
|
|
114
|
+
// container PATH to find it on. Everything about the BINARY (locating it
|
|
115
|
+
// from `ORACLE_ENV_VARS`, the version-banner probe, the scratch output
|
|
116
|
+
// location, the argument array, the runtime bound, the standard-output cap)
|
|
117
|
+
// now lives in host-tool.mts; this file keeps everything about the FINDING
|
|
118
|
+
// (the name parser, the accepted character set, the caps below, the
|
|
119
|
+
// packedness threshold, and both functions' never-throw contract).
|
|
120
|
+
|
|
121
|
+
// SYNCHRONOUS ON PURPOSE: `execFileSync`, not the async `spawn` acme.mjs's
|
|
122
|
+
// own migration uses. `probeUnp64()`/`runUnp64()` are called synchronously,
|
|
123
|
+
// with no `await`, throughout this module's own colocated test file
|
|
124
|
+
// (`packer-finding.test.mjs`, unmodified by this migration) -- including at
|
|
125
|
+
// module scope (`const PROBED = probeUnp64();`). Converting them to
|
|
126
|
+
// async/Promise-returning functions would silently break every one of those
|
|
127
|
+
// call sites (a Promise is not the finding object the assertions expect),
|
|
128
|
+
// so the OUTER call into the seam's CLI wrapper must itself be synchronous.
|
|
129
|
+
// The asynchronous work (the actual child-process spawn of the oracle
|
|
130
|
+
// binary) still happens -- inside the SPAWNED subprocess, in
|
|
131
|
+
// host-tool.mts's own async `runHostTool()` -- `execFileSync` merely blocks
|
|
132
|
+
// this function until that subprocess exits, exactly as `spawnSync` used to
|
|
133
|
+
// block until `unp64` itself exited.
|
|
134
|
+
const HOST_TOOL_CLIENT_FILE = "host-tool-client.ts";
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Synchronously invokes the host-tool execution seam for `tool`/`args`,
|
|
138
|
+
* optionally rooted at `repoRoot` for workspace-relative path resolution.
|
|
139
|
+
* NEVER throws: an unresolvable ladder, a spawn failure, a timeout, or
|
|
140
|
+
* unparseable output all return `{ ok: false, message }` -- the SAME shape
|
|
141
|
+
* a tool's own transport-level refusal uses, so callers translate a failure
|
|
142
|
+
* here identically to a `{ ok: false }` response from the seam itself.
|
|
143
|
+
*/
|
|
144
|
+
function invokeSeamSync(tool, args, repoRoot) {
|
|
145
|
+
const resolved = resolveMcpModule(HOST_TOOL_CLIENT_FILE);
|
|
146
|
+
if (!resolved.ok) {
|
|
147
|
+
return { ok: false, message: refusalMessage(HOST_TOOL_CLIENT_FILE, resolved.rungs) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const cliArgs = [resolved.path, "run", "--tool", tool, "--args", JSON.stringify(args)];
|
|
151
|
+
if (repoRoot) cliArgs.push("--repo-root", repoRoot);
|
|
152
|
+
|
|
153
|
+
let stdout;
|
|
154
|
+
try {
|
|
155
|
+
stdout = execFileSync(process.execPath, cliArgs, {
|
|
156
|
+
encoding: "utf8",
|
|
157
|
+
timeout: ORACLE_TIMEOUT_MS + 5_000,
|
|
158
|
+
shell: false,
|
|
159
|
+
windowsHide: true,
|
|
160
|
+
});
|
|
161
|
+
} catch (err) {
|
|
162
|
+
// execFileSync throws on a non-zero exit, a timeout, or a genuine spawn
|
|
163
|
+
// failure -- but a non-zero exit is the NORMAL signal for a tool-level
|
|
164
|
+
// `{ ok: false }` result (host-tool-client.ts's own CLI wrapper always
|
|
165
|
+
// prints its one JSON line before exiting non-zero), so recover it from
|
|
166
|
+
// the error object rather than treating every non-zero exit as a
|
|
167
|
+
// transport failure.
|
|
168
|
+
const recovered = typeof err.stdout === "string" ? err.stdout : err.stdout ? err.stdout.toString("utf8") : "";
|
|
169
|
+
if (recovered.trim() === "") {
|
|
170
|
+
return { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
171
|
+
}
|
|
172
|
+
stdout = recovered;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const lines = stdout.split("\n").filter((line) => line.trim() !== "");
|
|
176
|
+
const last = lines[lines.length - 1];
|
|
177
|
+
if (last === undefined) return { ok: false, message: "host-tool-client.ts produced no output" };
|
|
178
|
+
try {
|
|
179
|
+
return JSON.parse(last);
|
|
180
|
+
} catch {
|
|
181
|
+
return { ok: false, message: `host-tool-client.ts produced non-JSON output: ${last}` };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Splits an arbitrary (absolute or cwd-relative) file path into a workspace
|
|
186
|
+
* root + a plain relative name, so a single-file oracle.run request can
|
|
187
|
+
* satisfy the seam's workspace-relative path requirement (`resolveWorkspacePath()`
|
|
188
|
+
* in host-tool.mts refuses an absolute path outright) without needing the
|
|
189
|
+
* caller's actual project root at all -- the smallest possible root for a
|
|
190
|
+
* single file is its own containing directory. */
|
|
191
|
+
function toWorkspaceRelative(anyPath) {
|
|
192
|
+
const abs = resolve(anyPath);
|
|
193
|
+
return { repoRoot: dirname(abs), source: basename(abs) };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Vocabulary. Exactly four verdicts, frozen. Rule 4.
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The complete verdict vocabulary. Nothing else may ever appear in a
|
|
202
|
+
* finding's `verdict` field.
|
|
203
|
+
*
|
|
204
|
+
* identified -- an oracle stated a name, verbatim.
|
|
205
|
+
* packed-unidentified -- the entropy gate places the bytes at or above its
|
|
206
|
+
* packedness threshold, and NO oracle named the
|
|
207
|
+
* packer. Packedness, never identity.
|
|
208
|
+
* unpacked -- the entropy gate puts the bytes below its
|
|
209
|
+
* threshold. Still not an identity claim.
|
|
210
|
+
* unknown -- no route produced an answer. Always carries a
|
|
211
|
+
* reason.
|
|
212
|
+
*/
|
|
213
|
+
export const PACKER_VERDICTS = Object.freeze(["identified", "packed-unidentified", "unpacked", "unknown"]);
|
|
214
|
+
|
|
215
|
+
const VERDICT_IDENTIFIED = "identified";
|
|
216
|
+
const VERDICT_PACKED_UNIDENTIFIED = "packed-unidentified";
|
|
217
|
+
const VERDICT_UNPACKED = "unpacked";
|
|
218
|
+
const VERDICT_UNKNOWN = "unknown";
|
|
219
|
+
|
|
220
|
+
const CONFIDENCE_HIGH = "HIGH";
|
|
221
|
+
const CONFIDENCE_MEDIUM = "MEDIUM";
|
|
222
|
+
const CONFIDENCE_LOW = "LOW";
|
|
223
|
+
|
|
224
|
+
const ROUTE_ORACLE = "unp64";
|
|
225
|
+
const ROUTE_ENTROPY = "entropy-only";
|
|
226
|
+
const ROUTE_NONE = "none";
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The packedness threshold, taken from the curated binary-info tool's own
|
|
230
|
+
* description ("values higher than 7.5 suggest the binary might be
|
|
231
|
+
* compressed") rather than restated from a second source. Shannon entropy
|
|
232
|
+
* over bytes runs 0.0 to 8.0.
|
|
233
|
+
*
|
|
234
|
+
* This number gates PACKEDNESS ONLY. It can never reach the `packer` field --
|
|
235
|
+
* see rule 1.
|
|
236
|
+
*/
|
|
237
|
+
export const PACKED_ENTROPY_THRESHOLD = 7.5;
|
|
238
|
+
|
|
239
|
+
/** Default command name when neither environment variable is set. */
|
|
240
|
+
const DEFAULT_ORACLE_COMMAND = "unp64";
|
|
241
|
+
|
|
242
|
+
/** The two environment variables the external identifier is located from, in
|
|
243
|
+
* this order -- the same convention upstream's own comparison harness uses. */
|
|
244
|
+
const ORACLE_ENV_VARS = Object.freeze(["UNP64", "UNP64_PATH"]);
|
|
245
|
+
|
|
246
|
+
/** The opt-in variable that turns an absent oracle from an expected skip into
|
|
247
|
+
* a hard failure, by the established `VICE_REQUIRE_*` precedent. */
|
|
248
|
+
export const REQUIRE_ORACLE_ENV_VAR = "VICE_REQUIRE_UNP64";
|
|
249
|
+
|
|
250
|
+
/** Every child process here is bounded. A hung identifier is treated exactly
|
|
251
|
+
* like an absent one (T-19-23). */
|
|
252
|
+
const ORACLE_TIMEOUT_MS = 20_000;
|
|
253
|
+
|
|
254
|
+
/** Hard cap on how much of the oracle's standard output the parser will even
|
|
255
|
+
* look at. Longer than this is rejected outright rather than scanned
|
|
256
|
+
* (T-19-19). */
|
|
257
|
+
export const MAX_ORACLE_STDOUT_BYTES = 64 * 1024;
|
|
258
|
+
|
|
259
|
+
/** Hard cap on the length of a parsed packer name. */
|
|
260
|
+
export const MAX_PACKER_NAME_LENGTH = 64;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* The accepted shape of a parsed name: begins alphanumeric, then a small,
|
|
264
|
+
* explicitly listed printable set. Deliberately narrow -- the parsed value's
|
|
265
|
+
* only destination is a report field, and narrowing it here means a hostile
|
|
266
|
+
* standard output cannot smuggle control characters or markup into a document
|
|
267
|
+
* a human later reads.
|
|
268
|
+
*/
|
|
269
|
+
const PACKER_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9 ._/+-]*$/;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Line markers the parser accepts, each anchored at the start of a trimmed
|
|
273
|
+
* line and each followed by the name. ASSUMED, not measured -- see the module
|
|
274
|
+
* header's status note. An unrecognised line yields null, never a guess drawn
|
|
275
|
+
* from the surrounding text.
|
|
276
|
+
*/
|
|
277
|
+
const ORACLE_NAME_MARKERS = Object.freeze([/^packer\s*:\s*(.+)$/i, /^detected\s+packer\s*:\s*(.+)$/i, /^detected\s*:\s*(.+)$/i]);
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Entropy -- packedness only
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Shannon entropy over a byte buffer, 0.0 to 8.0 -- the same quantity the
|
|
285
|
+
* curated binary-info tool reports, computed here so the command-line entry
|
|
286
|
+
* can answer from a bare file with no live session.
|
|
287
|
+
*
|
|
288
|
+
* Returns null for an empty buffer: zero bytes is an absent measurement, not
|
|
289
|
+
* a measurement of zero (a zero would read as "definitely not compressed",
|
|
290
|
+
* which is a claim nobody made).
|
|
291
|
+
*/
|
|
292
|
+
export function shannonEntropy(bytes) {
|
|
293
|
+
if (!bytes || bytes.length === 0) return null;
|
|
294
|
+
const counts = new Array(256).fill(0);
|
|
295
|
+
for (const b of bytes) counts[b & 0xff]++;
|
|
296
|
+
let total = 0;
|
|
297
|
+
for (const count of counts) {
|
|
298
|
+
if (count === 0) continue;
|
|
299
|
+
const p = count / bytes.length;
|
|
300
|
+
total -= p * Math.log2(p);
|
|
301
|
+
}
|
|
302
|
+
return total;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// The oracle: probe, run, parse. All three read-only.
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Turns a container-side environment record
|
|
311
|
+
* into a DIAGNOSTIC HINT, never a configuration value. This script's own
|
|
312
|
+
* filesystem is not the filesystem the oracle runs on -- host-tool.mts's
|
|
313
|
+
* `resolveOracleCommand()` decides the oracle's location from the HOST
|
|
314
|
+
* BROKER PROCESS'S OWN environment now, so a variable set in THIS
|
|
315
|
+
* (container-side) environment can only ever explain a possibly-surprising
|
|
316
|
+
* absent result, never select what actually runs.
|
|
317
|
+
*
|
|
318
|
+
* Answers `null` when no oracle variable is set in `env`. Otherwise answers
|
|
319
|
+
* a non-empty string naming WHICH variable was set and stating that the
|
|
320
|
+
* seam consults the host broker process's own environment instead --
|
|
321
|
+
* NEVER interpolating the variable's value (T-19-18, unchanged by this
|
|
322
|
+
* migration).
|
|
323
|
+
*/
|
|
324
|
+
export function oracleConfigurationHint(env = process.env) {
|
|
325
|
+
const source = env ?? {};
|
|
326
|
+
for (const name of ORACLE_ENV_VARS) {
|
|
327
|
+
const value = source[name];
|
|
328
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
329
|
+
return (
|
|
330
|
+
`${name} is set in this container-side environment, but it is not consulted: the host-tool ` +
|
|
331
|
+
"execution seam reads the oracle's location from the HOST BROKER PROCESS'S OWN environment, " +
|
|
332
|
+
`not this script's -- point ${name} at the oracle in the environment the host broker process sees`
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Appends `hint` to `reason` when both are present, returns whichever of the
|
|
340
|
+
* two is non-null when only one is, and returns `null` when neither is. Kept
|
|
341
|
+
* as its own function so the "when absent, append the hint" rule in
|
|
342
|
+
* `probeUnp64()` below is one small, testable operation rather than inlined
|
|
343
|
+
* string-concatenation logic repeated at every call site. */
|
|
344
|
+
function appendHint(reason, hint) {
|
|
345
|
+
if (hint === null) return reason ?? null;
|
|
346
|
+
if (reason === null || reason === undefined) return hint;
|
|
347
|
+
return `${reason} ${hint}`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Locates and probes the external packer identifier, WITHOUT touching any
|
|
352
|
+
* input file.
|
|
353
|
+
*
|
|
354
|
+
* This function decides NOTHING about the
|
|
355
|
+
* binary any more -- it sends the seam call UNCONDITIONALLY, with an empty
|
|
356
|
+
* argument object, whether or not a container-side oracle variable is set.
|
|
357
|
+
* There is no filesystem existence check on an oracle path here (the removed
|
|
358
|
+
* check answered the wrong question: this script's own filesystem is not the
|
|
359
|
+
* filesystem the oracle runs on). A container-side variable can only ever
|
|
360
|
+
* add a diagnostic hint to an ABSENT result -- see `oracleConfigurationHint()`
|
|
361
|
+
* -- never select what the host executes.
|
|
362
|
+
*
|
|
363
|
+
* Never throws. A launch error, a non-zero status or a timeout are all
|
|
364
|
+
* "absent", never a failure -- absence of the oracle is an expected state.
|
|
365
|
+
*/
|
|
366
|
+
export function probeUnp64(env = process.env) {
|
|
367
|
+
const hint = oracleConfigurationHint(env);
|
|
368
|
+
const response = invokeSeamSync("oracle.probe", {});
|
|
369
|
+
|
|
370
|
+
if (!response || response.ok !== true) {
|
|
371
|
+
const reason = (response && response.message) || "the packer identifier oracle.probe seam call failed";
|
|
372
|
+
return { available: false, command: null, version: null, reason: appendHint(reason, hint) };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const available = response.available === true;
|
|
376
|
+
return {
|
|
377
|
+
available,
|
|
378
|
+
command: available ? response.command : null,
|
|
379
|
+
version: available ? response.version : null,
|
|
380
|
+
reason: available ? null : appendHint(response.reason, hint),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Runs the located identifier against `filePath`, read-only.
|
|
386
|
+
*
|
|
387
|
+
* The input file is never modified: any unpacked output the identifier writes
|
|
388
|
+
* goes to a scratch path under the system temporary directory, which is
|
|
389
|
+
* removed before this function returns (T-19-24). The child is launched with
|
|
390
|
+
* an argument ARRAY and an explicitly disabled command interpreter, so neither
|
|
391
|
+
* the configured command nor the caller's filename is ever parsed as a
|
|
392
|
+
* command (T-19-18).
|
|
393
|
+
*
|
|
394
|
+
* Never throws: every failure is reported as `{ ok: false, reason }`.
|
|
395
|
+
*/
|
|
396
|
+
export function runUnp64(probe, filePath) {
|
|
397
|
+
if (!probe || probe.available !== true || typeof probe.command !== "string") {
|
|
398
|
+
return { ok: false, stdout: "", reason: "the oracle was not available" };
|
|
399
|
+
}
|
|
400
|
+
if (typeof filePath !== "string" || filePath === "") {
|
|
401
|
+
return { ok: false, stdout: "", reason: "no input file was given to the oracle" };
|
|
402
|
+
}
|
|
403
|
+
if (!existsSync(filePath)) {
|
|
404
|
+
return { ok: false, stdout: "", reason: "the input file does not exist" };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// The scratch output location, the argument array, the runtime bound and
|
|
408
|
+
// the input file's absolute/relative form are all resolved host-side now;
|
|
409
|
+
// this file only ever hands the seam a workspace-relative `source`, rooted
|
|
410
|
+
// at the smallest root that can express it -- the file's own directory.
|
|
411
|
+
const { repoRoot, source } = toWorkspaceRelative(filePath);
|
|
412
|
+
const response = invokeSeamSync("oracle.run", { source }, repoRoot);
|
|
413
|
+
if (!response || typeof response.ok !== "boolean") {
|
|
414
|
+
return { ok: false, stdout: "", reason: (response && response.message) || "the oracle.run seam call failed" };
|
|
415
|
+
}
|
|
416
|
+
return { ok: response.ok, stdout: typeof response.stdout === "string" ? response.stdout : "", reason: response.reason ?? null };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Parses a packer name out of the oracle's standard output.
|
|
421
|
+
*
|
|
422
|
+
* Defensive by construction (T-19-19): an explicit byte cap before anything
|
|
423
|
+
* is scanned, no evaluation of any kind, a narrow accepted character set, and
|
|
424
|
+
* a length cap on the result. Empty, truncated, over-long and unrecognised
|
|
425
|
+
* input all return null WITHOUT throwing -- a parser that throws inside a
|
|
426
|
+
* recon pass would turn an unhelpful oracle into a stopped session.
|
|
427
|
+
*
|
|
428
|
+
* The returned value's only destination is the finding's `packer` field. It
|
|
429
|
+
* never reaches a command, a path, or any executable position.
|
|
430
|
+
*/
|
|
431
|
+
export function parseUnp64Stdout(text) {
|
|
432
|
+
if (typeof text !== "string") return null;
|
|
433
|
+
if (text.length === 0) return null;
|
|
434
|
+
if (text.length > MAX_ORACLE_STDOUT_BYTES) return null;
|
|
435
|
+
|
|
436
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
437
|
+
const line = rawLine.trim();
|
|
438
|
+
if (line === "") continue;
|
|
439
|
+
for (const marker of ORACLE_NAME_MARKERS) {
|
|
440
|
+
const matched = line.match(marker);
|
|
441
|
+
if (!matched) continue;
|
|
442
|
+
const candidate = (matched[1] ?? "").trim();
|
|
443
|
+
if (candidate === "") return null;
|
|
444
|
+
if (candidate.length > MAX_PACKER_NAME_LENGTH) return null;
|
|
445
|
+
if (!PACKER_NAME_RE.test(candidate)) return null;
|
|
446
|
+
return candidate;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
// Finding constructors. Each route has exactly one, and only the oracle's
|
|
454
|
+
// constructor may set `packer`.
|
|
455
|
+
// ---------------------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* THE ONLY PLACE IN THIS MODULE THAT ASSIGNS A PACKER NAME (rule 1), and the
|
|
459
|
+
* only place the high confidence level is written (rule 2). It refuses to
|
|
460
|
+
* build a finding from anything but a non-empty string the oracle's own
|
|
461
|
+
* output supplied.
|
|
462
|
+
*/
|
|
463
|
+
function identifiedByOracle(name, evidence, checkedAt) {
|
|
464
|
+
if (typeof name !== "string" || name.trim() === "") {
|
|
465
|
+
throw new Error("identifiedByOracle: refusing to report a packer name the oracle did not state");
|
|
466
|
+
}
|
|
467
|
+
return Object.freeze({
|
|
468
|
+
packer: name,
|
|
469
|
+
verdict: VERDICT_IDENTIFIED,
|
|
470
|
+
confidence: CONFIDENCE_HIGH,
|
|
471
|
+
route: ROUTE_ORACLE,
|
|
472
|
+
evidence: Object.freeze(evidence.slice()),
|
|
473
|
+
checkedAt,
|
|
474
|
+
unavailableReason: null,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** The entropy route. Answers PACKEDNESS and nothing else -- `packer` is
|
|
479
|
+
* written as the literal null here, and the entropy value has no path to it
|
|
480
|
+
* (rule 1). Never the high confidence level (rule 2). */
|
|
481
|
+
function fromEntropy(entropy, threshold, evidence, checkedAt) {
|
|
482
|
+
const packed = entropy >= threshold;
|
|
483
|
+
return Object.freeze({
|
|
484
|
+
packer: null,
|
|
485
|
+
verdict: packed ? VERDICT_PACKED_UNIDENTIFIED : VERDICT_UNPACKED,
|
|
486
|
+
confidence: CONFIDENCE_MEDIUM,
|
|
487
|
+
route: ROUTE_ENTROPY,
|
|
488
|
+
evidence: Object.freeze(evidence.slice()),
|
|
489
|
+
checkedAt,
|
|
490
|
+
unavailableReason: packed
|
|
491
|
+
? "the entropy gate answers packedness only -- no oracle named the packer, and this project never " +
|
|
492
|
+
"infers a name from entropy, from a decompression address, or from a byte pattern"
|
|
493
|
+
: "no name is claimed: the entropy gate places these bytes below its packedness threshold, which is " +
|
|
494
|
+
"a statement about compression and not about identity",
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** The no-route case. Rule 3 is enforced here: an `unknown` verdict with an
|
|
499
|
+
* empty reason cannot be constructed at all. */
|
|
500
|
+
function unknownFinding(reason, evidence, checkedAt) {
|
|
501
|
+
if (typeof reason !== "string" || reason.trim() === "") {
|
|
502
|
+
throw new Error("unknownFinding: refusing to return an unknown verdict with no reason (rule 3)");
|
|
503
|
+
}
|
|
504
|
+
return Object.freeze({
|
|
505
|
+
packer: null,
|
|
506
|
+
verdict: VERDICT_UNKNOWN,
|
|
507
|
+
confidence: CONFIDENCE_LOW,
|
|
508
|
+
route: ROUTE_NONE,
|
|
509
|
+
evidence: Object.freeze(evidence.slice()),
|
|
510
|
+
checkedAt,
|
|
511
|
+
unavailableReason: reason,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ---------------------------------------------------------------------------
|
|
516
|
+
// The finding
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Produces the packer recon finding for one binary, walking the ordered oracle
|
|
521
|
+
* chain: the external identifier first (first hit wins for the name), then the
|
|
522
|
+
* packedness-only entropy gate, then an explicit unknown.
|
|
523
|
+
*
|
|
524
|
+
* Options:
|
|
525
|
+
* filePath -- the binary to ask the oracle about. Required for the oracle
|
|
526
|
+
* route; the entropy route does not need it when `entropy` is
|
|
527
|
+
* supplied directly.
|
|
528
|
+
* entropy -- a measured entropy value (for example the one the curated
|
|
529
|
+
* binary-info tool reports). When absent and `bytes` is given,
|
|
530
|
+
* it is computed locally; the evidence entry records WHICH.
|
|
531
|
+
* bytes -- the binary's bytes, for the local entropy computation.
|
|
532
|
+
* threshold -- overrides the packedness threshold. The default is the one
|
|
533
|
+
* the curated tool's own description states.
|
|
534
|
+
* probe -- injectable oracle probe, so a test can force the oracle
|
|
535
|
+
* absent without uninstalling anything.
|
|
536
|
+
* run -- injectable oracle runner, same reason.
|
|
537
|
+
* now -- injectable clock, so a test can pin `checkedAt`.
|
|
538
|
+
*/
|
|
539
|
+
export function packerFinding(options = {}) {
|
|
540
|
+
const {
|
|
541
|
+
filePath = null,
|
|
542
|
+
entropy = null,
|
|
543
|
+
bytes = null,
|
|
544
|
+
threshold = PACKED_ENTROPY_THRESHOLD,
|
|
545
|
+
env = process.env,
|
|
546
|
+
probe = probeUnp64,
|
|
547
|
+
run = runUnp64,
|
|
548
|
+
now = () => new Date().toISOString(),
|
|
549
|
+
} = options;
|
|
550
|
+
|
|
551
|
+
const checkedAt = now();
|
|
552
|
+
const evidence = [];
|
|
553
|
+
|
|
554
|
+
// --- Route 1: the external oracle. The ONLY route that can name a packer.
|
|
555
|
+
const probed = probe(env);
|
|
556
|
+
evidence.push(
|
|
557
|
+
Object.freeze({
|
|
558
|
+
source: ROUTE_ORACLE,
|
|
559
|
+
available: probed.available === true,
|
|
560
|
+
version: probed.available === true ? probed.version : null,
|
|
561
|
+
reason: probed.available === true ? null : (probed.reason ?? "the oracle was not available"),
|
|
562
|
+
}),
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
if (probed.available === true && typeof filePath === "string" && filePath !== "") {
|
|
566
|
+
const result = run(probed, filePath);
|
|
567
|
+
const raw = typeof result.stdout === "string" ? result.stdout.slice(0, MAX_PACKER_NAME_LENGTH * 8) : "";
|
|
568
|
+
evidence.push(Object.freeze({ source: ROUTE_ORACLE, ok: result.ok === true, raw, reason: result.reason ?? null }));
|
|
569
|
+
if (result.ok === true) {
|
|
570
|
+
const name = parseUnp64Stdout(result.stdout);
|
|
571
|
+
if (name !== null) {
|
|
572
|
+
return identifiedByOracle(name, evidence, checkedAt);
|
|
573
|
+
}
|
|
574
|
+
// The oracle ran and named nothing. That is a real answer about the
|
|
575
|
+
// oracle, not about the packer -- fall through to the entropy gate
|
|
576
|
+
// rather than inventing a name from what it did print.
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// --- Route 2: the entropy gate. Packedness only, never a name.
|
|
581
|
+
let measured = null;
|
|
582
|
+
let entropySource = null;
|
|
583
|
+
if (typeof entropy === "number" && Number.isFinite(entropy)) {
|
|
584
|
+
measured = entropy;
|
|
585
|
+
// THE RECORDED PROVENANCE VALUE, DECIDED 2026-08-29 -- not string-replaced.
|
|
586
|
+
// Until this date this field carried the retired static analyser's
|
|
587
|
+
// binary-info verb, in that verb's own tool-name shape. Carrying that
|
|
588
|
+
// shape forward under ANY spelling is wrong twice over. It is a FACT
|
|
589
|
+
// ABOUT A PAST RUN: renaming it to a verb on the current surface would
|
|
590
|
+
// claim a run that never happened. And this branch cannot know who
|
|
591
|
+
// produced the number in the first place -- `--entropy` is
|
|
592
|
+
// caller-supplied, and the caller may equally have measured it, read it
|
|
593
|
+
// from a derived binary-info read, or copied it out of a report. So the
|
|
594
|
+
// value now names the CHANNEL, which this code can actually observe,
|
|
595
|
+
// rather than guessing a producer; the historical producer is recorded
|
|
596
|
+
// above in prose, with no token an extractor could mistake for a live
|
|
597
|
+
// route.
|
|
598
|
+
entropySource = "caller-supplied";
|
|
599
|
+
} else if (bytes && bytes.length > 0) {
|
|
600
|
+
measured = shannonEntropy(bytes);
|
|
601
|
+
entropySource = "local-shannon-entropy";
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (measured !== null) {
|
|
605
|
+
evidence.push(Object.freeze({ source: entropySource, entropy: measured, threshold }));
|
|
606
|
+
return fromEntropy(measured, threshold, evidence, checkedAt);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// --- Route 3: the explicit unknown, always with a reason.
|
|
610
|
+
return unknownFinding(
|
|
611
|
+
"no packer-identity route was available: this project's own tool and command-line surfaces expose no " +
|
|
612
|
+
"packer name, the external oracle was absent, and no entropy measurement was supplied, so packedness " +
|
|
613
|
+
"could not be established either",
|
|
614
|
+
evidence,
|
|
615
|
+
checkedAt,
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ---------------------------------------------------------------------------
|
|
620
|
+
// The live gate. Absence of the oracle is an EXPECTED SKIP by default and a
|
|
621
|
+
// hard FAIL under the opt-in variable -- never a pass either way.
|
|
622
|
+
// ---------------------------------------------------------------------------
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Returns a non-empty skip reason naming the absent oracle, or `false` when a
|
|
626
|
+
* real one is available -- meant to be handed straight to a test runner's
|
|
627
|
+
* `{ skip }` option so the skip is VISIBLE in the report rather than a test
|
|
628
|
+
* that quietly returns early and reads as a pass.
|
|
629
|
+
*/
|
|
630
|
+
export function skipReasonForUnp64(probed) {
|
|
631
|
+
const result = probed ?? probeUnp64();
|
|
632
|
+
if (result.available === true) return false;
|
|
633
|
+
return (
|
|
634
|
+
`the oracle-route tests are skipped -- no external packer identifier was found (${result.reason ?? "reason not recorded"}). ` +
|
|
635
|
+
`Point ${ORACLE_ENV_VARS.join(" or ")} at one, or install "${DEFAULT_ORACLE_COMMAND}". ` +
|
|
636
|
+
`An absent oracle is an EXPECTED SKIP here, never a pass: set ${REQUIRE_ORACLE_ENV_VAR} to turn it into a hard failure.`
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
641
|
+
// Command-line entry: one file in, one JSON object out.
|
|
642
|
+
// ---------------------------------------------------------------------------
|
|
643
|
+
|
|
644
|
+
const USAGE = `usage: node src/skills/c64-program-recon/scripts/packer-finding.mjs <file> [--entropy N]
|
|
645
|
+
|
|
646
|
+
Prints ONE JSON object: the packer recon finding for <file>.
|
|
647
|
+
|
|
648
|
+
packer the packer name, or null. Non-null ONLY when an external
|
|
649
|
+
oracle stated it verbatim. This project never guesses one.
|
|
650
|
+
verdict one of: ${PACKER_VERDICTS.join(", ")}
|
|
651
|
+
confidence HIGH (oracle route only), MEDIUM (entropy route), LOW
|
|
652
|
+
route ${ROUTE_ORACLE}, ${ROUTE_ENTROPY}, or ${ROUTE_NONE}
|
|
653
|
+
evidence what each step actually saw
|
|
654
|
+
checkedAt when this was answered
|
|
655
|
+
unavailableReason why no name is being reported, whenever there is none
|
|
656
|
+
|
|
657
|
+
--entropy overrides the locally computed value with one you already have (the
|
|
658
|
+
curated binary-info tool reports it). The entropy gate answers PACKEDNESS and
|
|
659
|
+
never identity.`;
|
|
660
|
+
|
|
661
|
+
function readFlag(argv, name) {
|
|
662
|
+
const index = argv.indexOf(`--${name}`);
|
|
663
|
+
if (index === -1) return undefined;
|
|
664
|
+
const value = argv[index + 1];
|
|
665
|
+
if (value === undefined || value.startsWith("--")) return undefined;
|
|
666
|
+
return value;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function main(argv) {
|
|
670
|
+
const positional = [];
|
|
671
|
+
for (let i = 0; i < argv.length; i++) {
|
|
672
|
+
const token = argv[i];
|
|
673
|
+
if (token === "--entropy") {
|
|
674
|
+
i++;
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
if (token.startsWith("--")) continue;
|
|
678
|
+
positional.push(token);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const filePath = positional[0];
|
|
682
|
+
if (filePath === undefined || argv.includes("--help") || argv.includes("-h")) {
|
|
683
|
+
console.log(USAGE);
|
|
684
|
+
process.exit(filePath === undefined && !argv.includes("--help") && !argv.includes("-h") ? 2 : 0);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
let bytes = null;
|
|
688
|
+
try {
|
|
689
|
+
bytes = readFileSync(filePath);
|
|
690
|
+
} catch (err) {
|
|
691
|
+
console.error(`packer-finding: could not read ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
692
|
+
process.exit(1);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const entropyRaw = readFlag(argv, "entropy");
|
|
696
|
+
const entropy = entropyRaw === undefined ? null : Number.parseFloat(entropyRaw);
|
|
697
|
+
if (entropyRaw !== undefined && !Number.isFinite(entropy)) {
|
|
698
|
+
console.error(`packer-finding: --entropy must be a number, got "${entropyRaw}"`);
|
|
699
|
+
process.exit(1);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const finding = packerFinding({ filePath, bytes, ...(entropy === null ? {} : { entropy }) });
|
|
703
|
+
console.log(JSON.stringify(finding, null, 2));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Run only when invoked directly, never when imported by the colocated test.
|
|
707
|
+
if (process.argv[1] && process.argv[1].endsWith("packer-finding.mjs")) {
|
|
708
|
+
main(process.argv.slice(2));
|
|
709
|
+
}
|