@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.20
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 +28 -62
- package/distribution/package.ts +1 -16
- package/distribution/plugin.ts +5 -3
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +25 -6
- package/skills/c64-assembler/scripts/assemble.ts +45 -10
- package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
- package/skills/c64-basic/SKILL.md +15 -7
- package/skills/c64-basic/scripts/basic.ts +4 -4
- package/skills/c64-disk/SKILL.md +34 -11
- package/skills/c64-disk/scripts/disk.ts +65 -10
- package/skills/c64-disk/scripts/lib/containers.ts +124 -0
- package/skills/c64-docs/SKILL.md +137 -0
- package/skills/c64-docs/references/page-types.md +113 -0
- package/skills/c64-docs/references/style.md +43 -0
- package/skills/c64-docs/scripts/generate.ts +102 -0
- package/skills/c64-docs/scripts/lib/links.ts +155 -0
- package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
- package/skills/c64-docs/scripts/lib/reference.ts +406 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
- package/skills/c64-docs/scripts/link.ts +193 -0
- package/skills/c64-emulator/SKILL.md +29 -8
- package/skills/c64-knowledge/SKILL.md +201 -15
- package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
- package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
- package/skills/c64-memory-map/SKILL.md +14 -2
- package/skills/c64-memory-map/scripts/memmap.ts +15 -4
- package/skills/c64-provenance/SKILL.md +100 -11
- package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
- package/skills/c64-provenance/scripts/lib/library.ts +584 -0
- package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
- package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
- package/skills/c64-provenance/scripts/provenance.ts +477 -24
- package/skills/c64-reverse-engineering/SKILL.md +89 -11
- package/skills/c64-source/SKILL.md +174 -0
- package/skills/c64-source/scripts/generate.ts +788 -0
- package/skills/c64-source/scripts/lib/loader.ts +630 -0
- package/skills/c64-source/scripts/lib/source.ts +1121 -0
- package/skills/c64-static-analysis/SKILL.md +53 -13
- package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
- package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
- package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
- package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
- package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
- package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
- package/skills/c64-testing/SKILL.md +57 -4
- package/skills/c64-testing/references/scenario-format.md +50 -6
- package/skills/c64-testing/scripts/checklist.ts +3 -3
- package/skills/c64-testing/scripts/diff.ts +167 -0
- package/skills/c64-testing/scripts/lib/diff.ts +70 -0
- package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
- package/skills/c64-testing/scripts/run.ts +241 -0
- package/skills/c64-testing/scripts/test.ts +20 -10
- package/skills/c64-unpacker/SKILL.md +88 -21
- package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
- package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
- package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
- package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
- package/skills/c64-unpacker/scripts/unpack.ts +545 -60
- package/src/c64.ts +240 -12
- package/src/cli/cleanup.ts +1 -1
- package/src/cli/main.ts +14 -8
- package/src/cli/status.ts +105 -11
- package/src/graphics.ts +158 -0
- package/src/host/main.ts +57 -31
- package/src/host/server.ts +30 -47
- package/src/host/tools/c1541.ts +34 -5
- package/src/host/tools/index.ts +6 -2
- package/src/host/tools/petcat.ts +10 -3
- package/src/host/tools/status.ts +23 -6
- package/src/host/vice/adapter.ts +64 -20
- package/src/host/vice/chips.ts +11 -1
- package/src/host/vice/process.ts +19 -8
- package/src/host/vice/screen.ts +4 -44
- package/src/host/vice/session.ts +343 -60
- package/src/host-client/autostart.ts +55 -0
- package/src/host-client/connect.ts +14 -14
- package/src/host-client/coverage.ts +63 -0
- package/src/host-client/image.ts +66 -0
- package/src/host-client/steps.ts +175 -0
- package/src/host-client/tools.ts +7 -6
- package/src/host-client/transfer.ts +60 -10
- package/src/host-client/vice-session.ts +107 -28
- package/src/knowledge/conflicts.ts +161 -0
- package/src/knowledge/database.ts +150 -38
- package/src/knowledge/expression.ts +209 -0
- package/src/knowledge/graph.ts +104 -0
- package/src/knowledge/history.ts +105 -27
- package/src/knowledge/import.ts +141 -85
- package/src/knowledge/join.ts +211 -0
- package/src/knowledge/program.ts +258 -0
- package/src/knowledge/read.ts +445 -63
- package/src/knowledge/schema.ts +277 -65
- package/src/knowledge/text.ts +415 -0
- package/src/knowledge/write.ts +1145 -150
- package/src/mcp/main.ts +44 -19
- package/src/mcp/output.ts +39 -0
- package/src/mcp/server.ts +15 -4
- package/src/mcp/tools/debug.ts +203 -32
- package/src/mcp/tools/execution.ts +39 -5
- package/src/mcp/tools/input.ts +7 -4
- package/src/mcp/tools/machine.ts +14 -2
- package/src/mcp/tools/media.ts +44 -16
- package/src/mcp/tools/memory.ts +68 -12
- package/src/mcp/tools/video.ts +20 -9
- package/src/native/discover.ts +44 -6
- package/src/native/ghidra/analyze.ts +2 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/ghidra/scripts/C64Prepare.java +7 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +43 -8
- package/src/opcodes.ts +137 -0
- package/src/package.ts +21 -0
- package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
- package/src/project.ts +166 -3
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/tools.ts +27 -8
- package/src/protocol/vice.ts +255 -33
- package/src/script.ts +49 -9
- package/src/trace.ts +55 -23
- package/skills/c64-provenance/scripts/compare.ts +0 -92
- package/skills/c64-unpacker/scripts/evidence.ts +0 -135
package/src/script.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// The one error envelope for the skill scripts and the CLI: a script prints one JSON line,
|
|
2
2
|
// a refusal prints {"error":{code,message}} with exit status 1, and a usage error prints the
|
|
3
|
-
// usage text as an invalid-input error with exit status 2.
|
|
3
|
+
// usage text as an invalid-input error with exit status 2. Every script takes --project <folder> (D67).
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { ProjectError, useProject } from "./project.ts";
|
|
6
|
+
import { shownMessage, WireFailure } from "./protocol/messages.ts";
|
|
6
7
|
import { elapsedMs, startTrace, trace, type TraceFields } from "./trace.ts";
|
|
7
8
|
|
|
8
9
|
/** A wrong command line. The message names the problem; an empty message asks for the usage text alone. */
|
|
@@ -17,28 +18,67 @@ function isParseArgsError(error: unknown): error is Error {
|
|
|
17
18
|
return error instanceof Error && typeof code === "string" && code.startsWith("ERR_PARSE_ARGS_");
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/** The line that every usage text ends with. */
|
|
22
|
+
export const PROJECT_USAGE = "Every script takes --project <folder>: the project folder, relative to the working directory. Without it, the script uses the nearest folder at or above the working directory that holds .c64-re-tools/.";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Takes --project <folder> and --project=<folder> out of a command line, up to "--".
|
|
26
|
+
* Returns the folder and the other arguments. Throws UsageError for a --project without a folder or a second --project.
|
|
27
|
+
*/
|
|
28
|
+
export function takeProject(argv: readonly string[]): { project?: string; argv: string[] } {
|
|
29
|
+
const rest: string[] = [];
|
|
30
|
+
let project: string | undefined;
|
|
31
|
+
for (let index = 0; index < argv.length; index++) {
|
|
32
|
+
const argument = argv[index]!;
|
|
33
|
+
if (argument === "--") {
|
|
34
|
+
rest.push(...argv.slice(index));
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
let value: string | undefined;
|
|
38
|
+
if (argument === "--project") {
|
|
39
|
+
value = argv[index + 1];
|
|
40
|
+
if (value === undefined || value.startsWith("-")) throw new UsageError("--project needs a folder");
|
|
41
|
+
index++;
|
|
42
|
+
} else if (argument.startsWith("--project=")) {
|
|
43
|
+
value = argument.slice("--project=".length);
|
|
44
|
+
if (value === "") throw new UsageError("--project needs a folder");
|
|
45
|
+
} else {
|
|
46
|
+
rest.push(argument);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (project !== undefined) throw new UsageError("give --project only once");
|
|
50
|
+
project = value;
|
|
51
|
+
}
|
|
52
|
+
return project === undefined ? { argv: rest } : { project, argv: rest };
|
|
53
|
+
}
|
|
54
|
+
|
|
20
55
|
function printLine(value: unknown): void {
|
|
21
56
|
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
22
57
|
}
|
|
23
58
|
|
|
24
59
|
/**
|
|
25
|
-
* Runs a script body
|
|
26
|
-
*
|
|
60
|
+
* Runs a script body with the command line of the process, without --project: that sets the project first (D67).
|
|
61
|
+
* Prints its result as one JSON line when it returns a value (undefined prints nothing).
|
|
62
|
+
* A refusal (WireFailure and ProjectError always, plus `refusals`) prints {"error":{code,message}} with exit status 1.
|
|
27
63
|
* A UsageError or a node:util parseArgs error (code ERR_PARSE_ARGS_*) prints {"error":{"code":"invalid-input",
|
|
28
64
|
* "message": message === "" ? usage : message + "\n\n" + usage}} with exit status 2. Anything else is rethrown.
|
|
29
65
|
* The body may set process.exitCode itself for an extra exit status; a value it returns is printed as usual.
|
|
30
66
|
*/
|
|
31
|
-
export async function runScript(body: () => unknown, options: { usage: string; refusals?: readonly RefusalClass[] }): Promise<void> {
|
|
67
|
+
export async function runScript(body: (argv: string[]) => unknown, options: { usage: string; refusals?: readonly RefusalClass[] }): Promise<void> {
|
|
68
|
+
const usage = `${options.usage}\n\n${PROJECT_USAGE}`;
|
|
32
69
|
// The trace of the script, when C64RT_TRACE is set; its start event carries the script and its arguments.
|
|
33
70
|
startTrace("script", { warn: (line) => process.stderr.write(`${line}\n`) });
|
|
34
71
|
const started = performance.now();
|
|
35
72
|
const end = (fields: TraceFields) => trace().event("script.end", { ms: elapsedMs(started), status: process.exitCode ?? 0, ...fields });
|
|
36
73
|
let result: unknown;
|
|
37
74
|
try {
|
|
38
|
-
|
|
75
|
+
const { project, argv } = takeProject(process.argv.slice(2));
|
|
76
|
+
if (project !== undefined) useProject(project);
|
|
77
|
+
result = await body(argv);
|
|
39
78
|
} catch (error) {
|
|
40
|
-
if ([WireFailure, ...(options.refusals ?? [])].some((refusal) => error instanceof refusal)) {
|
|
41
|
-
const { code
|
|
79
|
+
if ([WireFailure, ProjectError, ...(options.refusals ?? [])].some((refusal) => error instanceof refusal)) {
|
|
80
|
+
const { code } = error as Error & { code: string };
|
|
81
|
+
const message = shownMessage(error as Error & { code: string });
|
|
42
82
|
printLine({ error: { code, message } });
|
|
43
83
|
process.exitCode = 1;
|
|
44
84
|
end({ error: { code, message } });
|
|
@@ -46,7 +86,7 @@ export async function runScript(body: () => unknown, options: { usage: string; r
|
|
|
46
86
|
}
|
|
47
87
|
if (error instanceof UsageError || isParseArgsError(error)) {
|
|
48
88
|
const { message } = error;
|
|
49
|
-
printLine({ error: { code: "invalid-input", message: message === "" ?
|
|
89
|
+
printLine({ error: { code: "invalid-input", message: message === "" ? usage : `${message}\n\n${usage}` } });
|
|
50
90
|
process.exitCode = 2;
|
|
51
91
|
end({ error: { code: "invalid-input", message } });
|
|
52
92
|
return;
|
package/src/trace.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// An opt-in trace for the person who debugs a run. With C64RT_TRACE=<directory>
|
|
2
|
-
// in the environment, each process appends its events as
|
|
3
|
-
// of its own in that directory
|
|
4
|
-
//
|
|
2
|
+
// in the environment (the host: --log-dir), each process appends its events as
|
|
3
|
+
// JSON lines to a file of its own in that directory. The host can also echo
|
|
4
|
+
// each event to the console (--log-console). Off, every call does nothing. The
|
|
5
|
+
// trace is for people: nothing of it reaches the agent, and no result depends on it.
|
|
5
6
|
|
|
6
7
|
import { appendFileSync, copyFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
7
8
|
import { join } from "node:path";
|
|
@@ -66,14 +67,16 @@ export class Trace {
|
|
|
66
67
|
readonly kind: TraceKind;
|
|
67
68
|
/** The directory of the trace, or undefined when the trace is off. */
|
|
68
69
|
readonly dir: string | undefined;
|
|
69
|
-
/** The file this trace appends to, or undefined when the trace
|
|
70
|
+
/** The file this trace appends to, or undefined when the trace writes no file. */
|
|
70
71
|
readonly file: string | undefined;
|
|
72
|
+
readonly #echo: ((line: string) => void) | undefined;
|
|
71
73
|
#broken = false;
|
|
72
74
|
|
|
73
|
-
private constructor(kind: TraceKind, dir: string | undefined, file: string | undefined) {
|
|
75
|
+
private constructor(kind: TraceKind, dir: string | undefined, file: string | undefined, echo?: (line: string) => void) {
|
|
74
76
|
this.kind = kind;
|
|
75
77
|
this.dir = dir;
|
|
76
78
|
this.file = file;
|
|
79
|
+
this.#echo = echo;
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
/** A trace that records nothing. */
|
|
@@ -81,31 +84,46 @@ export class Trace {
|
|
|
81
84
|
return new Trace(kind, undefined, undefined);
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
/** A trace that writes no file and gives each event to `echo` as one line. */
|
|
88
|
+
static echoing(kind: TraceKind, echo: (line: string) => void): Trace {
|
|
89
|
+
return new Trace(kind, undefined, undefined, echo);
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
/**
|
|
85
93
|
* A trace into `dir`, created when it is missing. The file is
|
|
86
94
|
* `<kind>-<CET time>-<pid>.jsonl`, and its first event is `trace.start`.
|
|
87
95
|
* Throws when the directory cannot be used.
|
|
88
96
|
*/
|
|
89
|
-
static open(kind: TraceKind, dir: string, now = new Date()): Trace {
|
|
97
|
+
static open(kind: TraceKind, dir: string, now = new Date(), echo?: (line: string) => void): Trace {
|
|
90
98
|
mkdirSync(dir, { recursive: true });
|
|
91
|
-
const trace = new Trace(kind, dir, join(dir, `${kind}-${fileStampCet(now)}-${process.pid}.jsonl`));
|
|
99
|
+
const trace = new Trace(kind, dir, join(dir, `${kind}-${fileStampCet(now)}-${process.pid}.jsonl`), echo);
|
|
92
100
|
trace.event("trace.start", { version: runningVersion(), pid: process.pid, argv: process.argv.slice(1), cwd: process.cwd(), node: process.version, platform: process.platform });
|
|
93
101
|
return trace;
|
|
94
102
|
}
|
|
95
103
|
|
|
96
104
|
get on(): boolean {
|
|
105
|
+
return this.#writes || this.#echo !== undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get #writes(): boolean {
|
|
97
109
|
return this.file !== undefined && !this.#broken;
|
|
98
110
|
}
|
|
99
111
|
|
|
100
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Records one event: appends it to the file, and gives it to the echo as
|
|
114
|
+
* "<event> <fields as JSON>". The echo skips `log` events, which the program
|
|
115
|
+
* prints itself. A write that fails stops the file; nothing throws.
|
|
116
|
+
*/
|
|
101
117
|
event(name: string, fields: TraceFields = {}): void {
|
|
102
118
|
if (!this.on) return;
|
|
103
|
-
const
|
|
119
|
+
const summary: Record<string, unknown> = {};
|
|
104
120
|
for (const [key, value] of Object.entries(fields)) {
|
|
105
|
-
if (!RESERVED.has(key) && value !== undefined)
|
|
121
|
+
if (!RESERVED.has(key) && value !== undefined) summary[key] = summarize(value);
|
|
106
122
|
}
|
|
123
|
+
if (this.#echo !== undefined && name !== "log") this.#echo(`${name} ${JSON.stringify(summary)}`);
|
|
124
|
+
if (!this.#writes) return;
|
|
107
125
|
try {
|
|
108
|
-
appendFileSync(this.file!, `${JSON.stringify(
|
|
126
|
+
appendFileSync(this.file!, `${JSON.stringify({ t: isoCet(), kind: this.kind, event: name, ...summary })}\n`);
|
|
109
127
|
} catch {
|
|
110
128
|
this.#broken = true;
|
|
111
129
|
}
|
|
@@ -121,7 +139,7 @@ export class Trace {
|
|
|
121
139
|
* trace, as `<kind>-<pid>-<name>`. A copy that fails is recorded, not thrown.
|
|
122
140
|
*/
|
|
123
141
|
keep(source: string, name: string): void {
|
|
124
|
-
if (!this
|
|
142
|
+
if (!this.#writes) return;
|
|
125
143
|
const target = join(this.dir!, `${this.kind}-${process.pid}-${name}`);
|
|
126
144
|
try {
|
|
127
145
|
copyFileSync(source, target);
|
|
@@ -135,23 +153,37 @@ export class Trace {
|
|
|
135
153
|
// Off until a program calls startTrace; the kind is a placeholder until then.
|
|
136
154
|
let current = Trace.off("script");
|
|
137
155
|
|
|
156
|
+
/** Where the trace of a process goes: a directory for its file, and an echo for each event. */
|
|
157
|
+
export interface TraceTarget {
|
|
158
|
+
/** The directory, and how the user gave it (`--log-dir <path>`, for example) for a warning. */
|
|
159
|
+
dir?: { path: string; given: string };
|
|
160
|
+
echo?: (line: string) => void;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The target that C64RT_TRACE in `env` gives: its directory, or nothing. */
|
|
164
|
+
export function traceTargetFromEnv(env: NodeJS.ProcessEnv = process.env): TraceTarget {
|
|
165
|
+
const path = env[TRACE_VARIABLE];
|
|
166
|
+
return path === undefined || path === "" ? {} : { dir: { path, given: `${TRACE_VARIABLE}=${path}` } };
|
|
167
|
+
}
|
|
168
|
+
|
|
138
169
|
/**
|
|
139
|
-
* Switches the trace of this process on
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* start of a program.
|
|
170
|
+
* Switches the trace of this process on for `target` (by default what
|
|
171
|
+
* C64RT_TRACE in `env` gives), and off when the target is empty. A directory
|
|
172
|
+
* that cannot be used writes no file and reports that through `warn`; an echo
|
|
173
|
+
* stays on. Called once, at the start of a program.
|
|
143
174
|
*/
|
|
144
|
-
export function startTrace(kind: TraceKind, options: { env?: NodeJS.ProcessEnv; warn?: (line: string) => void } = {}): Trace {
|
|
145
|
-
const dir =
|
|
146
|
-
|
|
147
|
-
|
|
175
|
+
export function startTrace(kind: TraceKind, options: { env?: NodeJS.ProcessEnv; target?: TraceTarget; warn?: (line: string) => void } = {}): Trace {
|
|
176
|
+
const { dir, echo } = options.target ?? traceTargetFromEnv(options.env);
|
|
177
|
+
const noFile = () => (echo === undefined ? Trace.off(kind) : Trace.echoing(kind, echo));
|
|
178
|
+
if (dir === undefined) {
|
|
179
|
+
current = noFile();
|
|
148
180
|
return current;
|
|
149
181
|
}
|
|
150
182
|
try {
|
|
151
|
-
current = Trace.open(kind, dir);
|
|
183
|
+
current = Trace.open(kind, dir.path, new Date(), echo);
|
|
152
184
|
} catch (error) {
|
|
153
|
-
current =
|
|
154
|
-
options.warn?.(`${
|
|
185
|
+
current = noFile();
|
|
186
|
+
options.warn?.(`${dir.given} cannot be used (${error instanceof Error ? error.message : String(error)}); no trace file is written.`);
|
|
155
187
|
}
|
|
156
188
|
return current;
|
|
157
189
|
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
// Byte comparison of releases for the c64-provenance script. Each release is
|
|
2
|
-
// placed at its load address plus an optional shift; the comparison reports
|
|
3
|
-
// where the releases differ and which releases agree there. It never decides
|
|
4
|
-
// which bytes are original: that needs evidence beyond one comparison.
|
|
5
|
-
|
|
6
|
-
export interface Release {
|
|
7
|
-
name: string;
|
|
8
|
-
/** Address of the first byte after the shift. */
|
|
9
|
-
start: number;
|
|
10
|
-
bytes: Uint8Array;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface DifferingRange {
|
|
14
|
-
start: number;
|
|
15
|
-
end: number;
|
|
16
|
-
/** The bytes of each release in this range, by release name. */
|
|
17
|
-
values: Record<string, number[]>;
|
|
18
|
-
/** Releases with the same bytes here, largest group first. */
|
|
19
|
-
groups: string[][];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface Comparison {
|
|
23
|
-
/** The addresses that every release covers. */
|
|
24
|
-
common: { start: number; end: number } | null;
|
|
25
|
-
identicalBytes: number;
|
|
26
|
-
differingBytes: number;
|
|
27
|
-
differences: DifferingRange[];
|
|
28
|
-
/** True when more differing ranges exist than the limit allowed. */
|
|
29
|
-
moreDifferences: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Joins differing addresses that are at most this far apart into one range. */
|
|
33
|
-
const JOIN_GAP = 4;
|
|
34
|
-
|
|
35
|
-
export function compareReleases(releases: Release[], options: { from?: number; to?: number; maxRanges?: number } = {}): Comparison {
|
|
36
|
-
const start = Math.max(options.from ?? 0, ...releases.map((release) => release.start));
|
|
37
|
-
const end = Math.min(options.to ?? 0xffff, ...releases.map((release) => release.start + release.bytes.length - 1));
|
|
38
|
-
if (start > end) return { common: null, identicalBytes: 0, differingBytes: 0, differences: [], moreDifferences: false };
|
|
39
|
-
const at = (release: Release, address: number) => release.bytes[address - release.start]!;
|
|
40
|
-
let identicalBytes = 0;
|
|
41
|
-
const differing: number[] = [];
|
|
42
|
-
for (let address = start; address <= end; address++) {
|
|
43
|
-
const first = at(releases[0]!, address);
|
|
44
|
-
if (releases.every((release) => at(release, address) === first)) identicalBytes++;
|
|
45
|
-
else differing.push(address);
|
|
46
|
-
}
|
|
47
|
-
const ranges: Array<{ start: number; end: number }> = [];
|
|
48
|
-
for (const address of differing) {
|
|
49
|
-
const last = ranges.at(-1);
|
|
50
|
-
if (last !== undefined && address - last.end <= JOIN_GAP) last.end = address;
|
|
51
|
-
else ranges.push({ start: address, end: address });
|
|
52
|
-
}
|
|
53
|
-
const limit = options.maxRanges ?? 64;
|
|
54
|
-
const differences = ranges.slice(0, limit).map((range) => {
|
|
55
|
-
const values: Record<string, number[]> = {};
|
|
56
|
-
const groups = new Map<string, string[]>();
|
|
57
|
-
for (const release of releases) {
|
|
58
|
-
const bytes = Array.from(release.bytes.subarray(range.start - release.start, range.end - release.start + 1));
|
|
59
|
-
values[release.name] = bytes;
|
|
60
|
-
const key = bytes.join(",");
|
|
61
|
-
groups.set(key, [...(groups.get(key) ?? []), release.name]);
|
|
62
|
-
}
|
|
63
|
-
return { ...range, values, groups: [...groups.values()].sort((a, b) => b.length - a.length) };
|
|
64
|
-
});
|
|
65
|
-
return { common: { start, end }, identicalBytes, differingBytes: differing.length, differences, moreDifferences: ranges.length > limit };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* The shift that puts `other` over `base` best: the most common address
|
|
70
|
-
* difference between equal 8-byte sequences. Undefined when nothing matches.
|
|
71
|
-
*/
|
|
72
|
-
export function suggestShift(base: Release, other: Release): { shift: number; matches: number } | undefined {
|
|
73
|
-
const WINDOW = 8;
|
|
74
|
-
const key = (bytes: Uint8Array, at: number) => Buffer.from(bytes.subarray(at, at + WINDOW)).toString("latin1");
|
|
75
|
-
const positions = new Map<string, number[]>();
|
|
76
|
-
for (let at = 0; at + WINDOW <= base.bytes.length; at++) {
|
|
77
|
-
const positionsOfKey = positions.get(key(base.bytes, at));
|
|
78
|
-
// Very common sequences (fill bytes) say nothing about the alignment.
|
|
79
|
-
if (positionsOfKey === undefined) positions.set(key(base.bytes, at), [at]);
|
|
80
|
-
else if (positionsOfKey.length < 8) positionsOfKey.push(at);
|
|
81
|
-
}
|
|
82
|
-
const votes = new Map<number, number>();
|
|
83
|
-
for (let at = 0; at + WINDOW <= other.bytes.length; at++) {
|
|
84
|
-
for (const position of positions.get(key(other.bytes, at)) ?? []) {
|
|
85
|
-
const shift = base.start + position - (other.start + at);
|
|
86
|
-
votes.set(shift, (votes.get(shift) ?? 0) + 1);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
let best: { shift: number; matches: number } | undefined;
|
|
90
|
-
for (const [shift, matches] of votes) if (best === undefined || matches > best.matches) best = { shift, matches };
|
|
91
|
-
return best;
|
|
92
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
// Local evidence of packing for the c64-unpacker script: where BASIC hands
|
|
2
|
-
// over and how compressed the bytes look. It never names a packer.
|
|
3
|
-
|
|
4
|
-
import { formatC64Address } from "#src/c64.ts";
|
|
5
|
-
|
|
6
|
-
export class PackingError extends Error {}
|
|
7
|
-
|
|
8
|
-
/** A 256-byte block above this many bits per byte looks compressed. */
|
|
9
|
-
const HIGH_ENTROPY = 6.9;
|
|
10
|
-
const BLOCK = 256;
|
|
11
|
-
|
|
12
|
-
function entropy(bytes: Uint8Array): number {
|
|
13
|
-
const counts = new Array<number>(256).fill(0);
|
|
14
|
-
for (const byte of bytes) counts[byte]!++;
|
|
15
|
-
let bits = 0;
|
|
16
|
-
for (const count of counts) {
|
|
17
|
-
if (count === 0) continue;
|
|
18
|
-
const p = count / bytes.length;
|
|
19
|
-
bits -= p * Math.log2(p);
|
|
20
|
-
}
|
|
21
|
-
return bits;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** The constant SYS address of the first BASIC line, when the program starts with one. */
|
|
25
|
-
function basicSys(program: Uint8Array): { line: number; address: number; end: number } | undefined {
|
|
26
|
-
if (program.length < 8 || program[0] !== 0x01 || program[1] !== 0x08) return undefined;
|
|
27
|
-
const number = program[4]! | (program[5]! << 8);
|
|
28
|
-
const end = program.indexOf(0, 6);
|
|
29
|
-
if (end < 0) return undefined;
|
|
30
|
-
const sys = program.subarray(6, end).indexOf(0x9e);
|
|
31
|
-
if (sys < 0) return undefined;
|
|
32
|
-
const digits = Buffer.from(program.subarray(6 + sys + 1, end)).toString("latin1").replace(/[ (]/g, "");
|
|
33
|
-
const match = /^(\d{1,5})/.exec(digits);
|
|
34
|
-
if (match === null || Number(match[1]) > 0xffff) return undefined;
|
|
35
|
-
return { line: number, address: Number(match[1]), end: 0x0801 + end - 2 };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** The local packing evidence of one PRG. */
|
|
39
|
-
export interface PackingEvidence {
|
|
40
|
-
loadRange: { start: string; end: string };
|
|
41
|
-
bytes: number;
|
|
42
|
-
/** The first BASIC line and its constant SYS address, or null when the program has none. */
|
|
43
|
-
basicStart: { line: number; sys: string } | null;
|
|
44
|
-
entropy: { blocks: number; highBlocks: number; median: number | null };
|
|
45
|
-
packing: "likely" | "unlikely" | "unclear";
|
|
46
|
-
evidence: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function inspect(program: Uint8Array): PackingEvidence {
|
|
50
|
-
if (program.length < 3) throw new PackingError("the file is too short for a PRG");
|
|
51
|
-
const load = program[0]! | (program[1]! << 8);
|
|
52
|
-
const end = load + program.length - 3;
|
|
53
|
-
const stub = basicSys(program);
|
|
54
|
-
// Without a BASIC start, all bytes count; with one, only the bytes after the BASIC line.
|
|
55
|
-
const from = stub === undefined ? 2 : Math.min(program.length, stub.end - load + 2 + 3);
|
|
56
|
-
const body = program.subarray(from);
|
|
57
|
-
const blocks: number[] = [];
|
|
58
|
-
for (let at = 0; at + BLOCK <= body.length; at += BLOCK) blocks.push(entropy(body.subarray(at, at + BLOCK)));
|
|
59
|
-
const high = blocks.filter((value) => value > HIGH_ENTROPY).length;
|
|
60
|
-
const share = blocks.length === 0 ? 0 : high / blocks.length;
|
|
61
|
-
const packing = blocks.length < 2 ? "unclear" : share >= 0.75 ? "likely" : share <= 0.1 ? "unlikely" : "unclear";
|
|
62
|
-
const sorted = [...blocks].sort((a, b) => a - b);
|
|
63
|
-
return {
|
|
64
|
-
loadRange: { start: formatC64Address(load), end: formatC64Address(Math.min(end, 0xffff)) },
|
|
65
|
-
bytes: program.length - 2,
|
|
66
|
-
basicStart: stub === undefined ? null : { line: stub.line, sys: formatC64Address(stub.address) },
|
|
67
|
-
entropy: {
|
|
68
|
-
blocks: blocks.length,
|
|
69
|
-
highBlocks: high,
|
|
70
|
-
median: sorted.length === 0 ? null : Number(sorted[sorted.length >> 1]!.toFixed(2)),
|
|
71
|
-
},
|
|
72
|
-
packing,
|
|
73
|
-
evidence:
|
|
74
|
-
packing === "likely"
|
|
75
|
-
? `${high} of ${blocks.length} blocks of 256 bytes look compressed (above ${HIGH_ENTROPY} bits per byte); code and data are usually below.`
|
|
76
|
-
: packing === "unlikely"
|
|
77
|
-
? `Only ${high} of ${blocks.length} blocks look compressed; the bytes look like code and data. A simple cruncher can look the same: run trace to see if the program writes code and then executes it.`
|
|
78
|
-
: "The entropy does not show clearly if the program is packed. Run it and look at what it writes to memory.",
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** One range of the emulator's memory map: what the program did with these addresses. */
|
|
83
|
-
export interface AccessRange {
|
|
84
|
-
start: number;
|
|
85
|
-
end: number;
|
|
86
|
-
read: boolean;
|
|
87
|
-
write: boolean;
|
|
88
|
-
execute: boolean;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** The addresses that one memory map read covers before it is split. */
|
|
92
|
-
const MEMMAP_WINDOW = 0x1000;
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Reads the memory map of $0000-$ffff in windows, in address order. `read`
|
|
96
|
-
* gives at most `maxRanges` ranges for one window; a window that gives that
|
|
97
|
-
* many can have more, so it is read again as two halves.
|
|
98
|
-
*/
|
|
99
|
-
export async function readMemoryMap(read: (start: number, end: number) => Promise<AccessRange[]>, maxRanges: number): Promise<AccessRange[]> {
|
|
100
|
-
const ranges: AccessRange[] = [];
|
|
101
|
-
const window = async (start: number, end: number): Promise<void> => {
|
|
102
|
-
const found = await read(start, end);
|
|
103
|
-
if (found.length < maxRanges || start === end) {
|
|
104
|
-
ranges.push(...found);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
const middle = Math.floor((start + end) / 2);
|
|
108
|
-
await window(start, middle);
|
|
109
|
-
await window(middle + 1, end);
|
|
110
|
-
};
|
|
111
|
-
for (let start = 0; start <= 0xffff; start += MEMMAP_WINDOW) await window(start, start + MEMMAP_WINDOW - 1);
|
|
112
|
-
return ranges;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** ROM areas: an address here can be written (RAM below) and executed (ROM) without one being the other. */
|
|
116
|
-
export const underRom = (address: number) => (address >= 0xa000 && address <= 0xbfff) || address >= 0xe000;
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* The memory that the program wrote and then executed, in pieces of code.
|
|
120
|
-
* `bytes` and `running` count only RAM outside the ROM areas: a write under
|
|
121
|
-
* a ROM and code that runs in that ROM are not one piece of code.
|
|
122
|
-
*/
|
|
123
|
-
export function writtenThenExecuted(ranges: AccessRange[], pc: number): { written: Array<{ start: number; end: number }>; bytes: number; running?: { start: number; end: number } } {
|
|
124
|
-
// VICE marks only opcode bytes as executed; a gap of at most two operand bytes stays inside one piece of code.
|
|
125
|
-
const written: Array<{ start: number; end: number }> = [];
|
|
126
|
-
for (const range of ranges.filter((candidate) => candidate.write && candidate.execute)) {
|
|
127
|
-
const last = written.at(-1);
|
|
128
|
-
if (last !== undefined && range.start - last.end <= 3 && underRom(range.start) === underRom(last.start)) last.end = range.end;
|
|
129
|
-
else written.push({ start: range.start, end: range.end });
|
|
130
|
-
}
|
|
131
|
-
const counted = written.filter((range) => !underRom(range.start));
|
|
132
|
-
const bytes = counted.reduce((sum, range) => sum + range.end - range.start + 1, 0);
|
|
133
|
-
const running = counted.find((range) => pc >= range.start && pc <= range.end);
|
|
134
|
-
return { written, bytes, ...(running === undefined ? {} : { running }) };
|
|
135
|
-
}
|