@henols/c64-re-tools 1.1.0 → 2.0.0-rc.11

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.
Files changed (162) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -37
  3. package/distribution/package.ts +21 -0
  4. package/distribution/plugin.ts +122 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +76 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +110 -0
  8. package/skills/c64-assembler/scripts/compare.ts +84 -0
  9. package/skills/c64-basic/SKILL.md +66 -0
  10. package/skills/c64-basic/scripts/basic.ts +46 -0
  11. package/skills/c64-disk/SKILL.md +93 -0
  12. package/skills/c64-disk/scripts/containers.ts +124 -0
  13. package/skills/c64-disk/scripts/disk.ts +120 -0
  14. package/skills/c64-emulator/SKILL.md +134 -0
  15. package/skills/c64-knowledge/SKILL.md +192 -0
  16. package/skills/c64-knowledge/scripts/knowledge.ts +613 -0
  17. package/skills/c64-memory-map/SKILL.md +79 -0
  18. package/skills/c64-memory-map/scripts/memmap.ts +92 -0
  19. package/skills/c64-provenance/SKILL.md +110 -0
  20. package/skills/c64-provenance/scripts/compare.ts +152 -0
  21. package/skills/c64-provenance/scripts/provenance.ts +334 -0
  22. package/skills/c64-provenance/scripts/runtime.ts +191 -0
  23. package/skills/c64-provenance/scripts/separate.ts +140 -0
  24. package/skills/c64-reverse-engineering/SKILL.md +144 -0
  25. package/skills/c64-source/SKILL.md +133 -0
  26. package/skills/c64-source/scripts/generate.ts +503 -0
  27. package/skills/c64-source/scripts/loader.ts +589 -0
  28. package/skills/c64-source/scripts/opcodes.ts +61 -0
  29. package/skills/c64-source/scripts/source.ts +673 -0
  30. package/skills/c64-static-analysis/SKILL.md +120 -0
  31. package/skills/c64-static-analysis/scripts/analyze.ts +293 -0
  32. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  33. package/skills/c64-static-analysis/scripts/findings.ts +75 -0
  34. package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
  35. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  36. package/skills/c64-static-analysis/scripts/pointers.ts +108 -0
  37. package/skills/c64-testing/SKILL.md +103 -0
  38. package/skills/c64-testing/references/scenario-format.md +116 -0
  39. package/skills/c64-testing/scripts/checklist.ts +51 -0
  40. package/skills/c64-testing/scripts/run.ts +189 -0
  41. package/skills/c64-testing/scripts/scenario.ts +493 -0
  42. package/skills/c64-testing/scripts/test.ts +65 -0
  43. package/skills/c64-unpacker/SKILL.md +135 -0
  44. package/skills/c64-unpacker/scripts/evidence.ts +207 -0
  45. package/skills/c64-unpacker/scripts/loads.ts +209 -0
  46. package/skills/c64-unpacker/scripts/machine.ts +80 -0
  47. package/skills/c64-unpacker/scripts/stages.ts +130 -0
  48. package/skills/c64-unpacker/scripts/unpack.ts +683 -0
  49. package/src/c64.ts +411 -0
  50. package/src/cli/ap-sdk.ts +44 -0
  51. package/src/cli/cleanup.ts +194 -0
  52. package/src/cli/main.ts +125 -0
  53. package/src/cli/status.ts +61 -0
  54. package/src/host/main.ts +134 -0
  55. package/src/host/server.ts +392 -0
  56. package/src/host/tools/c1541.ts +284 -0
  57. package/src/host/tools/index.ts +35 -0
  58. package/src/host/tools/petcat.ts +77 -0
  59. package/src/host/tools/status.ts +47 -0
  60. package/src/host/tools/version.ts +41 -0
  61. package/src/host/vice/adapter.ts +804 -0
  62. package/src/host/vice/binary-monitor.ts +389 -0
  63. package/src/host/vice/chips.ts +118 -0
  64. package/src/host/vice/connect.ts +34 -0
  65. package/src/host/vice/process.ts +413 -0
  66. package/src/host/vice/screen.ts +184 -0
  67. package/src/host/vice/session.ts +1426 -0
  68. package/src/host/vice/text-monitor.ts +223 -0
  69. package/src/host-client/autostart.ts +55 -0
  70. package/src/host-client/connect.ts +241 -0
  71. package/src/host-client/coverage.ts +63 -0
  72. package/src/host-client/image.ts +63 -0
  73. package/src/host-client/steps.ts +170 -0
  74. package/src/host-client/tools.ts +89 -0
  75. package/src/host-client/transfer.ts +126 -0
  76. package/src/host-client/vice-session.ts +316 -0
  77. package/src/knowledge/database.ts +132 -0
  78. package/src/knowledge/expression.ts +209 -0
  79. package/src/knowledge/history.ts +169 -0
  80. package/src/knowledge/import.ts +392 -0
  81. package/src/knowledge/read.ts +392 -0
  82. package/src/knowledge/schema.ts +250 -0
  83. package/src/knowledge/write.ts +837 -0
  84. package/src/mcp/main.ts +134 -0
  85. package/src/mcp/output.ts +32 -0
  86. package/src/mcp/server.ts +316 -0
  87. package/src/mcp/tools/debug.ts +327 -0
  88. package/src/mcp/tools/execution.ts +117 -0
  89. package/src/mcp/tools/input.ts +72 -0
  90. package/src/mcp/tools/machine.ts +100 -0
  91. package/src/mcp/tools/media.ts +129 -0
  92. package/src/mcp/tools/memory.ts +250 -0
  93. package/src/mcp/tools/video.ts +271 -0
  94. package/src/native/acme.ts +116 -0
  95. package/src/native/discover.ts +179 -0
  96. package/src/native/dxa.ts +166 -0
  97. package/src/native/ghidra/analyze.ts +86 -0
  98. package/src/native/ghidra/index.ts +173 -0
  99. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  100. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  101. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  102. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  103. package/src/native/ghidra/scripts/C64Export.java +274 -0
  104. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  105. package/src/native/local.ts +34 -0
  106. package/src/native/processes.ts +350 -0
  107. package/src/native/run.ts +172 -0
  108. package/src/native/staging.ts +83 -0
  109. package/src/native/status.ts +113 -0
  110. package/src/native/types.ts +257 -0
  111. package/src/native/watchdog.ts +78 -0
  112. package/src/package.ts +21 -0
  113. package/src/platform.ts +441 -0
  114. package/src/project.ts +39 -0
  115. package/src/protocol/framing.ts +132 -0
  116. package/src/protocol/messages.ts +259 -0
  117. package/src/protocol/tools.ts +266 -0
  118. package/src/protocol/vice.ts +1348 -0
  119. package/src/script.ts +60 -0
  120. package/src/time.ts +15 -0
  121. package/src/trace.ts +194 -0
  122. package/THIRD-PARTY-NOTICES.md +0 -26
  123. package/bin/cli.mjs +0 -283
  124. package/skills/acme-build/SKILL.md +0 -260
  125. package/skills/acme-build/scripts/acme.mjs +0 -341
  126. package/skills/acme-build/template.a +0 -53
  127. package/skills/c64-disk-access/SKILL.md +0 -159
  128. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  129. package/skills/c64-memory-mapping/SKILL.md +0 -631
  130. package/skills/c64-memory-mapping/memmap.json +0 -8800
  131. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  132. package/skills/c64-petcat/SKILL.md +0 -87
  133. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  134. package/skills/c64-program-recon/SKILL.md +0 -706
  135. package/skills/c64-program-recon/references/control-flow.md +0 -180
  136. package/skills/c64-program-recon/references/graphics.md +0 -79
  137. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  138. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  139. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  140. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  141. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  142. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  143. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  144. package/skills/c64-provenance-diff/SKILL.md +0 -292
  145. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  146. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  147. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  148. package/skills/c64-ram-capture/SKILL.md +0 -474
  149. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  150. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  151. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  152. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  153. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  154. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  155. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  156. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  157. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  158. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  159. package/skills/c64-ram-capture/transients/README.md +0 -137
  160. package/skills/routine-queue-walker/SKILL.md +0 -394
  161. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  162. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -0,0 +1,334 @@
1
+ // The c64-provenance script: compares two or more releases byte by byte at
2
+ // their addresses and reports where they differ and which releases agree. A
3
+ // release is a PRG or an unpacker image.json (D50). With --run, each release
4
+ // runs in its own emulator, and each listed difference gets its first access
5
+ // and whether the run changed it. The separate command gives evidence from
6
+ // one release (D51).
7
+
8
+ import { posix } from "node:path";
9
+ import { parseArgs } from "node:util";
10
+
11
+ import { formatC64Address, parseC64Address } from "#src/c64.ts";
12
+ import { readCoverageFiles } from "#src/host-client/coverage.ts";
13
+ import { checkActionSteps, type ActionStep } from "#src/host-client/steps.ts";
14
+ import { MAX_EXECUTION_COUNT, WireFailure } from "#src/host-client/tools.ts";
15
+ import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
16
+ import { ViceSessionClient } from "#src/host-client/vice-session.ts";
17
+ import { KnowledgeError, openForRead } from "#src/knowledge/database.ts";
18
+ import { listReferences, listRegions } from "#src/knowledge/read.ts";
19
+ import { CODE_REGION_TYPES } from "#src/knowledge/schema.ts";
20
+ import { runScript, UsageError } from "#src/script.ts";
21
+ import { compareReleases, shiftSuggestion, suggestShifts, type Range, type Release } from "./compare.ts";
22
+ import { watchRelease, type ImageState, type Start } from "./runtime.ts";
23
+ import { separate } from "./separate.ts";
24
+
25
+ const USAGE = `provenance.ts <release> <release> [<release> ...] [options]
26
+ provenance.ts separate <release> [--coverage <file> ...] [--out <file>] [--max-listed <n>]
27
+
28
+ --shift <n>:<bytes> move release n (1 = the first) by a number of bytes, for example 2:-256
29
+ --from <address> compare from this address
30
+ --to <address> compare up to this address
31
+ --run <frames> run each release in its own emulator for this many frames, and
32
+ give each listed difference its first access and whether it changed
33
+ --steps <file> with --run: action steps (JSON) to run before the frames
34
+
35
+ separate gives evidence from one release: the unpack stage that wrote each range,
36
+ its access in the --coverage files (64 KiB each, repeatable), whether call and
37
+ jump references in knowledge reach each code region from the entry, and what the
38
+ program reads or executes outside the image. --out writes all ranges to a file;
39
+ the result lists at most --max-listed of each list (default 50) and its count.
40
+
41
+ A release is a PRG, placed at its load address, or an image.json of the
42
+ c64-unpacker skill, compared inside its segments. Paths are relative to the
43
+ project directory. The result is one JSON object.`;
44
+
45
+ function address(text: string, option: string): number {
46
+ try {
47
+ return parseC64Address(text);
48
+ } catch {
49
+ throw new UsageError(`${option} must be $ followed by four hex digits, not ${text}`);
50
+ }
51
+ }
52
+
53
+ const hexBytes = (bytes: number[]) => Buffer.from(bytes).toString("hex").replace(/(..)(?!$)/g, "$1 ");
54
+
55
+ interface LoadedRelease {
56
+ release: Release;
57
+ shift: number;
58
+ start: Start | { kind: "none"; reason: string };
59
+ /** 64 KiB with the release bytes at their own addresses. */
60
+ memory: Uint8Array;
61
+ entry?: number;
62
+ stages?: Array<{ entry: number; wrote: Range[] }>;
63
+ }
64
+
65
+ /** Reads entry_state.json of the c64-unpacker skill for a start by writes. */
66
+ function entryState(path: string): ImageState | string {
67
+ let value: Record<string, any>;
68
+ try {
69
+ value = JSON.parse(readProjectFile(path).bytes.toString("utf8")) as Record<string, any>;
70
+ } catch (error) {
71
+ if (error instanceof WireFailure && error.code === "not-found") return `It has no ${path}; capture it with c64-unpacker.`;
72
+ if (error instanceof WireFailure) throw error;
73
+ return `${path} is not JSON: ${(error as Error).message}`;
74
+ }
75
+ const registers = value.registers ?? {};
76
+ const byte = (item: unknown) => Number.isInteger(item) && (item as number) >= 0 && (item as number) <= 0xff;
77
+ const pages = ["$0000", "$0100", "$0200", "$0300"].map((page) => value.pages?.[page]);
78
+ const ok =
79
+ typeof registers.pc === "string" &&
80
+ /^\$[0-9a-f]{4}$/i.test(registers.pc) &&
81
+ ["a", "x", "y", "sp"].every((name) => byte(registers[name])) &&
82
+ typeof registers.flags === "object" &&
83
+ byte(value.cpuPort?.$00) &&
84
+ byte(value.cpuPort?.$01) &&
85
+ pages.every((page) => typeof page === "string" && /^[0-9a-f]{512}$/i.test(page));
86
+ if (!ok) return `${path} is not an entry_state.json of the c64-unpacker skill.`;
87
+ const flag = (name: string) => registers.flags[name] === true;
88
+ return {
89
+ pc: Number.parseInt(registers.pc.slice(1), 16),
90
+ a: registers.a,
91
+ x: registers.x,
92
+ y: registers.y,
93
+ sp: registers.sp,
94
+ flags: { n: flag("n"), v: flag("v"), d: flag("d"), i: flag("i"), z: flag("z"), c: flag("c") },
95
+ port: [value.cpuPort.$00, value.cpuPort.$01],
96
+ pages: Buffer.from(pages.join(""), "hex"),
97
+ };
98
+ }
99
+
100
+ function readRelease(path: string, shift: number): LoadedRelease {
101
+ const { bytes } = readProjectFile(path);
102
+ const moved = (range: Range) => ({ start: range.start + shift, end: range.end + shift });
103
+ const inside = (range: Range) => range.start >= 0 && range.end <= 0xffff;
104
+ if (!path.endsWith(".json")) {
105
+ if (bytes.length < 3) throw new UsageError(`${path} is too short for a PRG`);
106
+ const load = bytes.readUInt16LE(0);
107
+ const segment = moved({ start: load, end: load + bytes.length - 3 });
108
+ if (!inside(segment)) throw new UsageError(`the shift moves ${path} outside $0000-$ffff`);
109
+ const memory = new Uint8Array(0x10000);
110
+ memory.set(bytes.subarray(2), load);
111
+ return { release: { name: path, segments: [segment], byteAt: (at) => bytes[2 + at - segment.start]! }, shift, start: { kind: "program", path }, memory };
112
+ }
113
+ const fail = (what: string): never => {
114
+ throw new UsageError(`${path} is not an image.json of the c64-unpacker skill: ${what}`);
115
+ };
116
+ let manifest: Record<string, unknown>;
117
+ try {
118
+ manifest = JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
119
+ } catch (error) {
120
+ return fail((error as Error).message);
121
+ }
122
+ const parse = (value: unknown) => (typeof value === "string" && /^\$[0-9a-f]{4}$/i.test(value) ? Number.parseInt(value.slice(1), 16) : fail("a segment needs start and end as $ addresses"));
123
+ if (typeof manifest.image !== "string" || !Array.isArray(manifest.segments) || manifest.segments.length === 0) fail("it needs image and segments");
124
+ const own = (manifest.segments as Array<Record<string, unknown>>).map((segment) => ({ start: parse(segment.start), end: parse(segment.end) })).sort((a, b) => a.start - b.start);
125
+ const directory = posix.dirname(path);
126
+ const memory = readProjectFile(posix.join(directory, manifest.image as string)).bytes;
127
+ if (memory.length !== 0x10000) fail(`its image has ${memory.length} bytes, not 65536`);
128
+ const segments = own.map(moved);
129
+ if (!segments.every(inside)) throw new UsageError(`the shift moves ${path} outside $0000-$ffff`);
130
+ const state = entryState(posix.join(directory, "entry_state.json"));
131
+ const stages = Array.isArray(manifest.stages)
132
+ ? (manifest.stages as Array<Record<string, unknown>>).map((stage) => ({
133
+ entry: parse(stage.entry),
134
+ wrote: (Array.isArray(stage.wrote) ? (stage.wrote as Array<Record<string, unknown>>) : []).map((range) => ({ start: parse(range.start), end: parse(range.end) })),
135
+ }))
136
+ : undefined;
137
+ return {
138
+ release: { name: path, segments, byteAt: (at) => memory[at - shift]! },
139
+ shift,
140
+ start: typeof state === "string" ? { kind: "none", reason: state } : { kind: "image", segments: own, byteAt: (at) => memory[at]!, state },
141
+ memory,
142
+ ...(manifest.entry === undefined ? {} : { entry: parse(manifest.entry) }),
143
+ ...(stages === undefined ? {} : { stages }),
144
+ };
145
+ }
146
+
147
+ async function run(argv: string[]): Promise<unknown> {
148
+ const { values, positionals } = parseArgs({
149
+ args: argv,
150
+ strict: true,
151
+ allowPositionals: true,
152
+ options: {
153
+ shift: { type: "string", multiple: true },
154
+ from: { type: "string" },
155
+ to: { type: "string" },
156
+ run: { type: "string" },
157
+ steps: { type: "string" },
158
+ coverage: { type: "string", multiple: true },
159
+ out: { type: "string" },
160
+ "max-listed": { type: "string" },
161
+ help: { type: "boolean" },
162
+ },
163
+ });
164
+ if (values.help) throw new UsageError("");
165
+ if (positionals[0] === "separate") {
166
+ const used = ["shift", "from", "to", "run", "steps"].filter((option) => values[option as keyof typeof values] !== undefined);
167
+ if (used.length > 0) throw new UsageError(`separate does not take --${used.join(", --")}`);
168
+ if (positionals.length !== 2) throw new UsageError("separate needs one release");
169
+ return separateCommand(positionals[1]!, values.coverage ?? [], values.out, values["max-listed"]);
170
+ }
171
+ if (values.coverage !== undefined || values.out !== undefined || values["max-listed"] !== undefined) throw new UsageError("--coverage, --out and --max-listed work only with separate");
172
+ if (positionals.length < 2) throw new UsageError("give two or more releases");
173
+ const shifts = new Map<number, number>();
174
+ for (const text of values.shift ?? []) {
175
+ const match = /^(\d+):([+-]?\d+)$/.exec(text);
176
+ if (match === null || Number(match[1]) < 1 || Number(match[1]) > positionals.length) throw new UsageError(`--shift must be <release number>:<bytes>, not ${text}`);
177
+ shifts.set(Number(match[1]), Number(match[2]));
178
+ }
179
+ if (values.steps !== undefined && values.run === undefined) throw new UsageError("--steps works only with --run");
180
+ let frames: number | undefined;
181
+ if (values.run !== undefined) {
182
+ if (!/^\d{1,6}$/.test(values.run) || Number(values.run) < 1 || Number(values.run) > MAX_EXECUTION_COUNT) throw new UsageError(`--run needs a frame count from 1 to ${MAX_EXECUTION_COUNT}, not ${values.run}`);
183
+ frames = Number(values.run);
184
+ }
185
+ let steps: ActionStep[] = [];
186
+ if (values.steps !== undefined) {
187
+ let parsed: unknown;
188
+ try {
189
+ parsed = JSON.parse(readProjectFile(values.steps).bytes.toString("utf8"));
190
+ } catch (error) {
191
+ if (error instanceof WireFailure) throw error;
192
+ throw new UsageError(`the steps file ${values.steps} is not JSON: ${(error as Error).message}`);
193
+ }
194
+ steps = checkActionSteps(parsed, (message) => {
195
+ throw new UsageError(`steps file ${values.steps}: ${message}`);
196
+ });
197
+ }
198
+ const loaded = positionals.map((path, index) => readRelease(path, shifts.get(index + 1) ?? 0));
199
+ const releases = loaded.map((item) => item.release);
200
+ const comparison = compareReleases(releases, {
201
+ ...(values.from === undefined ? {} : { from: address(values.from, "--from") }),
202
+ ...(values.to === undefined ? {} : { to: address(values.to, "--to") }),
203
+ });
204
+ const compared = comparison.identicalBytes + comparison.differingBytes;
205
+ const identicalShare = compared === 0 ? 0 : comparison.identicalBytes / compared;
206
+ // A poor match at the load addresses can mean that the releases are placed differently.
207
+ // Each release gets its best shifts: data that stayed can outvote code that moved, so one is not enough (b2/04).
208
+ const applied = loaded.map((item) => item.shift);
209
+ const suggestions =
210
+ identicalShare < 0.5
211
+ ? releases.slice(1).flatMap((release, index) =>
212
+ suggestShifts(releases[0]!, release).map((found) => {
213
+ const suggestion = shiftSuggestion(releases, applied, index + 1, found);
214
+ // The shift that the run already has: its votes tell how good the present alignment is.
215
+ return "shift" in suggestion && suggestion.shift === applied[suggestion.release - 1] ? { ...suggestion, current: true } : suggestion;
216
+ }),
217
+ )
218
+ : [];
219
+ const suggested = suggestions.filter((suggestion) => "shift" in suggestion);
220
+ const notSuggested = suggestions.filter((suggestion) => "reason" in suggestion);
221
+
222
+ // The runtime evidence: one emulator for each release, the differences at the release's own addresses.
223
+ const runtime = new Map<string, Array<Range & { first: string; changed: boolean }>>();
224
+ const runs: Record<string, unknown> = {};
225
+ if (frames !== undefined && comparison.differences.length > 0) {
226
+ for (const item of loaded) {
227
+ const name = item.release.name;
228
+ if (item.start.kind === "none") {
229
+ runs[name] = { started: false, reason: item.start.reason };
230
+ continue;
231
+ }
232
+ const ranges = comparison.differences.map((difference) => ({ start: difference.start - item.shift, end: difference.end - item.shift }));
233
+ const client = await ViceSessionClient.open({ videoStandard: "pal" });
234
+ try {
235
+ const watched = await watchRelease(client, item.start, ranges, (at) => item.release.byteAt(at + item.shift), { frames, steps });
236
+ if (!watched.started) runs[name] = { started: false, reason: watched.reason };
237
+ else {
238
+ runs[name] = { started: true };
239
+ runtime.set(name, watched.accesses.map((access, index) => ({ ...ranges[index]!, ...access })));
240
+ }
241
+ } finally {
242
+ await client.close();
243
+ }
244
+ }
245
+ }
246
+
247
+ return {
248
+ releases: loaded.map((item) => ({ name: item.release.name, shift: item.shift, segments: item.release.segments.map((segment) => ({ start: formatC64Address(segment.start), end: formatC64Address(segment.end) })) })),
249
+ common: comparison.common.map((range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) })),
250
+ identicalBytes: comparison.identicalBytes,
251
+ differingBytes: comparison.differingBytes,
252
+ identicalShare: Number(identicalShare.toFixed(3)),
253
+ differences: comparison.differences.map((difference, index) => ({
254
+ start: formatC64Address(difference.start),
255
+ end: formatC64Address(difference.end),
256
+ values: Object.fromEntries(Object.entries(difference.values).map(([name, bytes]) => [name, hexBytes(bytes.slice(0, 32)) + (bytes.length > 32 ? " ..." : "")])),
257
+ groups: difference.groups,
258
+ // runtime gives the release's own addresses: with a shift, they are not the addresses of the difference (F-24).
259
+ ...(runtime.size === 0
260
+ ? {}
261
+ : {
262
+ runtime: Object.fromEntries(
263
+ [...runtime].map(([name, accesses]) => {
264
+ const { start, end, first, changed } = accesses[index]!;
265
+ return [name, { start: formatC64Address(start), end: formatC64Address(end), first, changed }];
266
+ }),
267
+ ),
268
+ }),
269
+ })),
270
+ ...(comparison.moreDifferences ? { moreDifferences: true } : {}),
271
+ ...(suggested.length > 0 ? { suggestedShifts: suggested } : {}),
272
+ ...(notSuggested.length > 0 ? { shiftsNotSuggested: notSuggested } : {}),
273
+ ...(frames === undefined
274
+ ? {}
275
+ : { run: { frames, steps: steps.length, releases: runs, ...(comparison.differences.length === 0 ? { skipped: "The releases have no differences, so no release ran." } : {}) } }),
276
+ };
277
+ }
278
+
279
+ /** separate: evidence from one release (D51), with all ranges in --out and the first ones in the result. */
280
+ function separateCommand(path: string, coveragePaths: string[], out: string | undefined, maxListed: string | undefined): unknown {
281
+ const max = maxListed ?? "50";
282
+ if (!/^\d{1,5}$/.test(max)) throw new UsageError(`--max-listed must be a whole number, not ${max}`);
283
+ const loaded = readRelease(path, 0);
284
+ const coverage = readCoverageFiles(coveragePaths, (message) => {
285
+ throw new UsageError(message);
286
+ });
287
+ const db = openForRead();
288
+ let regions: ReturnType<typeof listRegions>;
289
+ let references: ReturnType<typeof listReferences>;
290
+ try {
291
+ regions = listRegions(db).filter((region) => CODE_REGION_TYPES.includes(region.type));
292
+ references = listReferences(db);
293
+ } finally {
294
+ db?.close();
295
+ }
296
+ const found = separate({
297
+ segments: loaded.release.segments,
298
+ memory: loaded.memory,
299
+ ...(loaded.entry === undefined ? {} : { entry: loaded.entry }),
300
+ ...(loaded.stages === undefined ? {} : { stages: loaded.stages }),
301
+ ...(coverage === undefined ? {} : { coverage }),
302
+ codeRegions: regions,
303
+ references,
304
+ });
305
+ const show = (range: Range) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
306
+ const ranges = found.ranges.map((range) => ({ ...show(range), ...(range.stage === undefined ? {} : { stage: range.stage }), ...(range.access === undefined ? {} : { access: range.access }) }));
307
+ const codeRegions = found.codeRegions.map((region) => ({ ...show(region), type: region.type, ...(region.reachable === undefined ? {} : { reachable: region.reachable }), ...(region.executed === undefined ? {} : { executed: region.executed }) }));
308
+ const outside = { read: found.outside.read.map(show), executed: found.outside.executed.map(show), generated: found.outside.generated.map(show) };
309
+ const listed = <T>(items: T[]) => ({ count: items.length, listed: items.slice(0, Number(max)) });
310
+ if (out !== undefined) writeProjectFile(out, Buffer.from(`${JSON.stringify({ release: path, ranges, codeRegions, outside }, null, 1)}\n`), { what: "--out" });
311
+ const bytes = (filter: (range: (typeof found.ranges)[number]) => boolean) => found.ranges.filter(filter).reduce((sum, range) => sum + range.end - range.start + 1, 0);
312
+ const lastStage = loaded.stages === undefined ? undefined : loaded.stages.length - 1;
313
+ return {
314
+ release: path,
315
+ ...(loaded.entry === undefined ? {} : { entry: formatC64Address(loaded.entry) }),
316
+ ...(lastStage === undefined
317
+ ? { noStages: "image.json has no stage log. Only a capture of c64-unpacker writes one. A capture of loads holds one part of a program and has none." }
318
+ : {
319
+ stages: lastStage + 1,
320
+ bytesByStage: Object.fromEntries([...[...Array(lastStage + 1).keys()].map((stage): [string, number] => [String(stage), bytes((range) => range.stage === stage)]), ["none", bytes((range) => range.stage === undefined)]]),
321
+ }),
322
+ ...(coverage === undefined ? {} : { bytesByAccess: Object.fromEntries((["executed", "read", "written", "read-written", "untouched"] as const).map((access) => [access, bytes((range) => range.access === access)])) }),
323
+ ranges: listed(ranges),
324
+ // total counts every code region; unreachable is a list like the others (b2/08).
325
+ codeRegions: {
326
+ total: codeRegions.length,
327
+ ...(loaded.entry === undefined ? {} : { unreachable: listed(codeRegions.filter((region) => region.reachable === false)) }),
328
+ },
329
+ ...(coverage === undefined ? {} : { outside: { read: listed(outside.read), executed: listed(outside.executed), generated: listed(outside.generated) } }),
330
+ ...(out === undefined ? {} : { out }),
331
+ };
332
+ }
333
+
334
+ await runScript(() => run(process.argv.slice(2)), { usage: USAGE, refusals: [KnowledgeError] });
@@ -0,0 +1,191 @@
1
+ // Runtime evidence for the differences between releases (D50): each release
2
+ // runs in its own emulator, and each differing range gets its first access
3
+ // (read, write, execute or none) and whether its bytes changed by the end.
4
+ // Watchpoints and range breakpoints stop the run at an access; the coverage
5
+ // map then tells which range and which access it was.
6
+
7
+ import { COVERAGE, formatC64Address, instructionLength } from "#src/c64.ts";
8
+ import { runProgram } from "#src/host-client/autostart.ts";
9
+ import { readCoverage } from "#src/host-client/coverage.ts";
10
+ import { runActionStep, type ActionStep } from "#src/host-client/steps.ts";
11
+ import { MAX_MEMORY_READ, MAX_MEMORY_WRITE } from "#src/host-client/tools.ts";
12
+ import { readProjectFile } from "#src/host-client/transfer.ts";
13
+ import type { ViceSessionClient } from "#src/host-client/vice-session.ts";
14
+ import type { Range } from "./compare.ts";
15
+
16
+ export type FirstAccess = "read" | "write" | "execute" | "none";
17
+
18
+ export interface RangeAccess {
19
+ first: FirstAccess;
20
+ /** True when the bytes at the end differ from the bytes of the release. */
21
+ changed: boolean;
22
+ }
23
+
24
+ /** How a release starts in the emulator. */
25
+ export type Start =
26
+ | { kind: "program"; path: string }
27
+ | { kind: "image"; segments: Range[]; byteAt: (address: number) => number; state: ImageState };
28
+
29
+ export interface ImageState {
30
+ pc: number;
31
+ a: number;
32
+ x: number;
33
+ y: number;
34
+ sp: number;
35
+ flags: { n: boolean; v: boolean; d: boolean; i: boolean; z: boolean; c: boolean };
36
+ port: [number, number];
37
+ /** $0000-$03ff at the entry. */
38
+ pages: Uint8Array;
39
+ }
40
+
41
+ const MAX_WATCH = 256;
42
+ /** Frames until the KERNAL is at the READY prompt, before an image is written. */
43
+ const READY_FRAMES = 150;
44
+
45
+ const READ = COVERAGE.ramRead | COVERAGE.romRead | COVERAGE.ioRead;
46
+ const WRITE = COVERAGE.ramWrite | COVERAGE.ioWrite;
47
+ const EXECUTE = COVERAGE.ramExecute | COVERAGE.romExecute | COVERAGE.ioExecute;
48
+
49
+ /** The first access that coverage flags show; a read-modify-write reads first. */
50
+ export function accessOf(flags: number): FirstAccess {
51
+ if ((flags & EXECUTE) !== 0) return "execute";
52
+ if ((flags & READ) !== 0) return "read";
53
+ if ((flags & WRITE) !== 0) return "write";
54
+ return "none";
55
+ }
56
+
57
+ /** Starts the release; returns why it did not start, or undefined. */
58
+ async function start(client: ViceSessionClient, how: Start): Promise<string | undefined> {
59
+ if (how.kind === "program") {
60
+ const failed = await runProgram(client, how.path, readProjectFile(how.path).bytes);
61
+ if (failed === "not-loaded") return "The program was not in memory after autostart.";
62
+ if (failed === "not-started") return "The program did not start after RUN; it needs a BASIC start line.";
63
+ return undefined;
64
+ }
65
+ await client.reset({ mode: "hard", run: true });
66
+ await client.execution({ action: "advance-frames", count: READY_FRAMES, space: "c64" });
67
+ for (const segment of how.segments) {
68
+ for (let at = segment.start; at <= segment.end; at += MAX_MEMORY_WRITE) {
69
+ const last = Math.min(segment.end, at + MAX_MEMORY_WRITE - 1);
70
+ const bytes = Buffer.from(Array.from({ length: last - at + 1 }, (_, offset) => how.byteAt(at + offset)));
71
+ await client.memoryWrite({ address: at, data: bytes.toString("hex"), space: "c64", view: "ram" });
72
+ }
73
+ }
74
+ const { state } = how;
75
+ // Pages 0 to 3 from $0002, then the CPU port: the port bits switch what the CPU sees.
76
+ await client.memoryWrite({ address: 0x0002, data: Buffer.from(state.pages.subarray(2)).toString("hex"), space: "c64", view: "ram" });
77
+ await client.memoryWrite({ address: 0x0000, data: Buffer.from(state.port).toString("hex"), space: "c64", view: "cpu" });
78
+ await client.registersSet("c64", { pc: state.pc, a: state.a, x: state.x, y: state.y, sp: state.sp, flags: state.flags });
79
+ return undefined;
80
+ }
81
+
82
+ /**
83
+ * Runs one release and watches the ranges (addresses of the release, after
84
+ * any shift is taken away). `release` gives the release's own bytes.
85
+ */
86
+ export async function watchRelease(
87
+ client: ViceSessionClient,
88
+ how: Start,
89
+ ranges: Range[],
90
+ release: (address: number) => number,
91
+ run: { frames: number; steps: ActionStep[] },
92
+ ): Promise<{ started: true; accesses: RangeAccess[] } | { started: false; reason: string }> {
93
+ await client.reset({ mode: "hard", run: false });
94
+ const failed = await start(client, how);
95
+ if (failed !== undefined) return { started: false, reason: failed };
96
+ if ((await client.status()).state === "running") await client.execution({ action: "pause", space: "c64" });
97
+
98
+ const first: FirstAccess[] = ranges.map(() => "none");
99
+ const points = new Map<number, Array<{ kind: "watch" | "break"; id: number }>>();
100
+ for (const [index, range] of ranges.entries()) {
101
+ const ids: Array<{ kind: "watch" | "break"; id: number }> = [];
102
+ for (let at = range.start; at <= range.end; at += MAX_WATCH) {
103
+ const size = Math.min(MAX_WATCH, range.end - at + 1);
104
+ ids.push({ kind: "watch", id: (await client.watchpoint({ action: "add", address: at, size, access: "read-write", space: "c64" })).id });
105
+ ids.push({ kind: "break", id: (await client.breakpoint({ action: "add", address: at, size, space: "c64" })).id });
106
+ }
107
+ points.set(index, ids);
108
+ }
109
+ await client.memmap({ action: "clear" });
110
+
111
+ /**
112
+ * After a stop: the ranges that the coverage shows accessed get their first
113
+ * access, and lose their points. VICE marks only the opcode byte of an
114
+ * executed instruction, so an instruction that starts up to two bytes before
115
+ * a range, or in it, executes its operand bytes too (F-27).
116
+ */
117
+ const settle = async (): Promise<boolean> => {
118
+ const map = await readCoverage(client);
119
+ let found = false;
120
+ for (const [index, ids] of points) {
121
+ const range = ranges[index]!;
122
+ let flags = 0;
123
+ for (let address = range.start; address <= range.end; address++) flags |= map[address]!;
124
+ const from = Math.max(0, range.start - 2);
125
+ if ((flags & EXECUTE) === 0 && map.subarray(from, range.start).some((byte) => (byte & COVERAGE.ramExecute) !== 0)) {
126
+ const code = Buffer.from((await client.memoryRead({ address: from, size: range.start - from, space: "c64", view: "ram" })).data, "hex");
127
+ for (let address = from; address < range.start; address++) {
128
+ if ((map[address]! & COVERAGE.ramExecute) !== 0 && address + instructionLength(code[address - from]!) - 1 >= range.start) flags |= COVERAGE.ramExecute;
129
+ }
130
+ }
131
+ if (accessOf(flags) === "none") continue;
132
+ first[index] = accessOf(flags);
133
+ for (const point of ids) {
134
+ if (point.kind === "watch") await client.watchpoint({ action: "remove", id: point.id });
135
+ else await client.breakpoint({ action: "remove", id: point.id });
136
+ }
137
+ points.delete(index);
138
+ found = true;
139
+ }
140
+ return found;
141
+ };
142
+
143
+ // Each stop at a point settles at least one range, so the stops are bounded; the guard ends a run that stops without an access.
144
+ let guard = 4 * ranges.length + 16;
145
+ /** Runs frames; a stop at a point settles and goes on with the frames that are left. */
146
+ const runFrames = async (count: number) => {
147
+ let left = count;
148
+ while (left > 0 && guard-- > 0) {
149
+ const ran = await client.execution({ action: "advance-frames", count: left, space: "c64" });
150
+ left -= ran.advancedFrames ?? left;
151
+ if (left > 0 && !(await settle())) left--;
152
+ }
153
+ };
154
+ for (const step of run.steps) {
155
+ if ("frames" in step) {
156
+ await runFrames(step.frames);
157
+ continue;
158
+ }
159
+ if ("runUntil" in step) {
160
+ // A stop at a point is not the target: settle and run to the target again.
161
+ while (guard-- > 0) {
162
+ const reached = await runActionStep(client, step, (location) => Number.parseInt(location.slice(1), 16));
163
+ if (reached === undefined || reached.stopReason !== "watchpoint" && reached.stopReason !== "breakpoint") break;
164
+ await settle();
165
+ }
166
+ continue;
167
+ }
168
+ await runActionStep(client, step, (location) => Number.parseInt(location.slice(1), 16));
169
+ }
170
+ await runFrames(run.frames);
171
+ if (points.size > 0) await settle();
172
+
173
+ const accesses: RangeAccess[] = [];
174
+ for (const [index, range] of ranges.entries()) {
175
+ const parts: Buffer[] = [];
176
+ for (let at = range.start; at <= range.end; at += MAX_MEMORY_READ) {
177
+ parts.push(Buffer.from((await client.memoryRead({ address: at, size: Math.min(MAX_MEMORY_READ, range.end - at + 1), space: "c64", view: "ram" })).data, "hex"));
178
+ }
179
+ const now = Buffer.concat(parts);
180
+ accesses.push({ first: first[index]!, changed: now.some((byte, offset) => byte !== release(range.start + offset)) });
181
+ }
182
+ for (const ids of points.values()) {
183
+ for (const point of ids) {
184
+ if (point.kind === "watch") await client.watchpoint({ action: "remove", id: point.id });
185
+ else await client.breakpoint({ action: "remove", id: point.id });
186
+ }
187
+ }
188
+ return { started: true, accesses };
189
+ }
190
+
191
+ export const showRange = (range: Range) => `${formatC64Address(range.start)}-${formatC64Address(range.end)}`;
@@ -0,0 +1,140 @@
1
+ // Evidence from one release (D51): for each range of the image, the unpack
2
+ // stage that wrote it and the access class from coverage; for each code
3
+ // region of knowledge, whether call and jump references reach it from the
4
+ // entry; and what the program reads or executes outside the image. Evidence
5
+ // only: nothing here decides what is original.
6
+
7
+ import { COVERAGE, executedRamCode } from "#src/c64.ts";
8
+ import type { Range } from "./compare.ts";
9
+
10
+ export type Access = "executed" | "read" | "written" | "read-written" | "untouched";
11
+
12
+ export interface SeparationInput {
13
+ segments: Range[];
14
+ /** 64 KiB with the image bytes at their addresses. */
15
+ memory: Uint8Array;
16
+ entry?: number;
17
+ /** The stages of an unpacker capture, in order; the last one is the program. */
18
+ stages?: Array<{ entry: number; wrote: Range[] }>;
19
+ /** Joined coverage files (D40). */
20
+ coverage?: Uint8Array;
21
+ codeRegions: Array<Range & { type: string }>;
22
+ references: Array<{ from: number; to: number; kind: string }>;
23
+ }
24
+
25
+ export interface EvidenceRange extends Range {
26
+ /** The last stage that wrote the range; absent when no stage wrote it. */
27
+ stage?: number;
28
+ access?: Access;
29
+ }
30
+
31
+ export interface CodeRegionEvidence extends Range {
32
+ type: string;
33
+ /** Absent when the image has no entry. */
34
+ reachable?: boolean;
35
+ /** Absent without coverage. */
36
+ executed?: boolean;
37
+ }
38
+
39
+ export interface Separation {
40
+ ranges: EvidenceRange[];
41
+ codeRegions: CodeRegionEvidence[];
42
+ /**
43
+ * RAM outside the image and outside pages 0-3, from the coverage: `read`
44
+ * and `executed` the program never wrote (a loader or an earlier stage put
45
+ * those bytes there), and `generated` it wrote and then executed, in
46
+ * pieces of code with gaps of up to two operand bytes.
47
+ */
48
+ outside: { read: Range[]; executed: Range[]; generated: Range[] };
49
+ }
50
+
51
+ const SYSTEM_PAGES_END = 0x03ff;
52
+
53
+ /** Joins addresses in order into ranges; `gap` is the most bytes between two addresses in one range. */
54
+ function merge(addresses: number[], gap = 0, joins: (from: number, to: number) => boolean = () => true): Range[] {
55
+ const ranges: Range[] = [];
56
+ for (const address of addresses) {
57
+ const last = ranges.at(-1);
58
+ if (last !== undefined && address - last.end - 1 <= gap && joins(last.end, address)) last.end = address;
59
+ else ranges.push({ start: address, end: address });
60
+ }
61
+ return ranges;
62
+ }
63
+
64
+ export function separate(input: SeparationInput): Separation {
65
+ const inImage = (address: number) => input.segments.some((segment) => address >= segment.start && address <= segment.end);
66
+ const executed = new Uint8Array(0x10000);
67
+ if (input.coverage !== undefined) for (const range of executedRamCode(input.coverage, 0, input.memory)) executed.fill(1, range.start, range.end + 1);
68
+ const accessAt = (address: number): Access | undefined => {
69
+ if (input.coverage === undefined) return undefined;
70
+ if (executed[address] === 1) return "executed";
71
+ const flags = input.coverage[address]!;
72
+ const read = (flags & COVERAGE.ramRead) !== 0;
73
+ const written = (flags & COVERAGE.ramWrite) !== 0;
74
+ return read && written ? "read-written" : read ? "read" : written ? "written" : "untouched";
75
+ };
76
+ const stageAt = (address: number): number | undefined => {
77
+ const stages = input.stages ?? [];
78
+ for (let index = stages.length - 1; index >= 0; index--) if (stages[index]!.wrote.some((range) => address >= range.start && address <= range.end)) return index;
79
+ return undefined;
80
+ };
81
+
82
+ const ranges: EvidenceRange[] = [];
83
+ for (const segment of input.segments) {
84
+ for (let address = segment.start; address <= segment.end; address++) {
85
+ const stage = stageAt(address);
86
+ const access = accessAt(address);
87
+ const last = ranges.at(-1);
88
+ if (last !== undefined && last.end + 1 === address && last.stage === stage && last.access === access) {
89
+ last.end = address;
90
+ continue;
91
+ }
92
+ ranges.push({ start: address, end: address, ...(stage === undefined ? {} : { stage }), ...(access === undefined ? {} : { access }) });
93
+ }
94
+ }
95
+
96
+ // Reachability: from the region that holds the entry, through call and jump references, to a fixed point.
97
+ const regions = input.codeRegions.filter((region) => input.segments.some((segment) => region.start <= segment.end && segment.start <= region.end));
98
+ const holding = (address: number) => regions.findIndex((region) => address >= region.start && address <= region.end);
99
+ const reached = new Set<number>();
100
+ if (input.entry !== undefined && holding(input.entry) >= 0) reached.add(holding(input.entry));
101
+ const flows = input.references.filter((reference) => reference.kind === "call" || reference.kind === "jump");
102
+ for (let grown = true; grown; ) {
103
+ grown = false;
104
+ for (const flow of flows) {
105
+ const from = holding(flow.from);
106
+ const to = holding(flow.to);
107
+ if (from >= 0 && to >= 0 && reached.has(from) && !reached.has(to)) {
108
+ reached.add(to);
109
+ grown = true;
110
+ }
111
+ }
112
+ }
113
+ const codeRegions = regions.map((region, index): CodeRegionEvidence => {
114
+ let ran: boolean | undefined;
115
+ if (input.coverage !== undefined) {
116
+ ran = false;
117
+ for (let address = region.start; address <= region.end && !ran; address++) ran = executed[address] === 1;
118
+ }
119
+ return { start: region.start, end: region.end, type: region.type, ...(input.entry === undefined ? {} : { reachable: reached.has(index) }), ...(ran === undefined ? {} : { executed: ran }) };
120
+ });
121
+
122
+ const read: number[] = [];
123
+ const ranOutside: number[] = [];
124
+ const generated: number[] = [];
125
+ if (input.coverage !== undefined) {
126
+ for (let address = SYSTEM_PAGES_END + 1; address < 0x10000; address++) {
127
+ if (inImage(address)) continue;
128
+ const written = (input.coverage[address]! & COVERAGE.ramWrite) !== 0;
129
+ if (executed[address] === 1) (written ? generated : ranOutside).push(address);
130
+ else if (!written && (input.coverage[address]! & COVERAGE.ramRead) !== 0) read.push(address);
131
+ }
132
+ }
133
+ // The image has no bytes of generated code, so only its opcode bytes count as executed. VICE marks only
134
+ // opcode bytes: a gap of at most two operand bytes outside the image stays inside one piece of code (F-28).
135
+ const outsideImage = (from: number, to: number) => {
136
+ for (let address = from + 1; address < to; address++) if (inImage(address)) return false;
137
+ return true;
138
+ };
139
+ return { ranges, codeRegions, outside: { read: merge(read), executed: merge(ranOutside), generated: merge(generated, 2, outsideImage) } };
140
+ }