@henols/c64-re-tools 1.1.0 → 2.0.0-rc.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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +138 -37
  3. package/distribution/package.ts +36 -0
  4. package/distribution/plugin.ts +120 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +73 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +103 -0
  8. package/skills/c64-basic/SKILL.md +67 -0
  9. package/skills/c64-basic/scripts/basic.ts +46 -0
  10. package/skills/c64-disk/SKILL.md +79 -0
  11. package/skills/c64-disk/scripts/disk.ts +65 -0
  12. package/skills/c64-emulator/SKILL.md +124 -0
  13. package/skills/c64-knowledge/SKILL.md +102 -0
  14. package/skills/c64-knowledge/scripts/knowledge.ts +257 -0
  15. package/skills/c64-memory-map/SKILL.md +76 -0
  16. package/skills/c64-memory-map/scripts/memmap.ts +81 -0
  17. package/skills/c64-memory-map/scripts/platform.ts +424 -0
  18. package/skills/c64-provenance/SKILL.md +76 -0
  19. package/skills/c64-provenance/scripts/compare.ts +92 -0
  20. package/skills/c64-provenance/scripts/provenance.ts +83 -0
  21. package/skills/c64-reverse-engineering/SKILL.md +90 -0
  22. package/skills/c64-static-analysis/SKILL.md +96 -0
  23. package/skills/c64-static-analysis/scripts/analyze.ts +175 -0
  24. package/skills/c64-static-analysis/scripts/findings.ts +73 -0
  25. package/skills/c64-testing/SKILL.md +79 -0
  26. package/skills/c64-testing/references/scenario-format.md +76 -0
  27. package/skills/c64-testing/scripts/checklist.ts +51 -0
  28. package/skills/c64-testing/scripts/scenario.ts +473 -0
  29. package/skills/c64-testing/scripts/test.ts +55 -0
  30. package/skills/c64-unpacker/SKILL.md +82 -0
  31. package/skills/c64-unpacker/scripts/evidence.ts +135 -0
  32. package/skills/c64-unpacker/scripts/unpack.ts +216 -0
  33. package/src/c64.ts +281 -0
  34. package/src/cli/ap-sdk.ts +44 -0
  35. package/src/cli/cleanup.ts +194 -0
  36. package/src/cli/main.ts +122 -0
  37. package/src/cli/status.ts +34 -0
  38. package/src/host/main.ts +106 -0
  39. package/src/host/server.ts +376 -0
  40. package/src/host/tools/c1541.ts +255 -0
  41. package/src/host/tools/index.ts +31 -0
  42. package/src/host/tools/petcat.ts +70 -0
  43. package/src/host/tools/status.ts +30 -0
  44. package/src/host/tools/version.ts +41 -0
  45. package/src/host/vice/adapter.ts +761 -0
  46. package/src/host/vice/binary-monitor.ts +389 -0
  47. package/src/host/vice/chips.ts +108 -0
  48. package/src/host/vice/connect.ts +34 -0
  49. package/src/host/vice/process.ts +402 -0
  50. package/src/host/vice/screen.ts +183 -0
  51. package/src/host/vice/session.ts +1316 -0
  52. package/src/host/vice/text-monitor.ts +223 -0
  53. package/src/host-client/connect.ts +238 -0
  54. package/src/host-client/tools.ts +89 -0
  55. package/src/host-client/transfer.ts +100 -0
  56. package/src/host-client/vice-session.ts +254 -0
  57. package/src/knowledge/database.ts +132 -0
  58. package/src/knowledge/history.ts +128 -0
  59. package/src/knowledge/import.ts +344 -0
  60. package/src/knowledge/read.ts +191 -0
  61. package/src/knowledge/schema.ts +140 -0
  62. package/src/knowledge/write.ts +348 -0
  63. package/src/mcp/main.ts +106 -0
  64. package/src/mcp/server.ts +307 -0
  65. package/src/mcp/tools/debug.ts +237 -0
  66. package/src/mcp/tools/execution.ts +116 -0
  67. package/src/mcp/tools/input.ts +69 -0
  68. package/src/mcp/tools/machine.ts +97 -0
  69. package/src/mcp/tools/media.ts +106 -0
  70. package/src/mcp/tools/memory.ts +203 -0
  71. package/src/mcp/tools/video.ts +260 -0
  72. package/src/native/acme.ts +116 -0
  73. package/src/native/discover.ts +141 -0
  74. package/src/native/dxa.ts +166 -0
  75. package/src/native/ghidra/analyze.ts +86 -0
  76. package/src/native/ghidra/index.ts +157 -0
  77. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  78. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  79. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  80. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  81. package/src/native/ghidra/scripts/C64Export.java +242 -0
  82. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  83. package/src/native/local.ts +34 -0
  84. package/src/native/processes.ts +350 -0
  85. package/src/native/run.ts +172 -0
  86. package/src/native/staging.ts +83 -0
  87. package/src/native/status.ts +101 -0
  88. package/src/native/types.ts +228 -0
  89. package/src/native/watchdog.ts +78 -0
  90. package/src/project.ts +39 -0
  91. package/src/protocol/framing.ts +132 -0
  92. package/src/protocol/messages.ts +243 -0
  93. package/src/protocol/tools.ts +247 -0
  94. package/src/protocol/vice.ts +1235 -0
  95. package/src/script.ts +59 -0
  96. package/src/time.ts +15 -0
  97. package/src/trace.ts +162 -0
  98. package/THIRD-PARTY-NOTICES.md +0 -26
  99. package/bin/cli.mjs +0 -283
  100. package/skills/acme-build/SKILL.md +0 -260
  101. package/skills/acme-build/scripts/acme.mjs +0 -341
  102. package/skills/acme-build/template.a +0 -53
  103. package/skills/c64-disk-access/SKILL.md +0 -159
  104. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  105. package/skills/c64-memory-mapping/SKILL.md +0 -631
  106. package/skills/c64-memory-mapping/memmap.json +0 -8800
  107. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  108. package/skills/c64-petcat/SKILL.md +0 -87
  109. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  110. package/skills/c64-program-recon/SKILL.md +0 -706
  111. package/skills/c64-program-recon/references/control-flow.md +0 -180
  112. package/skills/c64-program-recon/references/graphics.md +0 -79
  113. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  114. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  115. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  116. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  117. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  118. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  119. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  120. package/skills/c64-provenance-diff/SKILL.md +0 -292
  121. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  122. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  123. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  124. package/skills/c64-ram-capture/SKILL.md +0 -474
  125. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  126. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  127. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  128. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  129. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  130. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  131. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  132. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  133. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  134. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  135. package/skills/c64-ram-capture/transients/README.md +0 -137
  136. package/skills/routine-queue-walker/SKILL.md +0 -394
  137. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  138. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: c64-unpacker
3
+ description: Use this skill to find out if a packer or a cruncher compressed a C64 program, and to get the unpacked program for analysis. It shows evidence of packing without a guess of the packer name. It runs the program in an emulator until the unpacked code starts, then writes the memory into the project. Do not use it to analyze the unpacked code.
4
+ ---
5
+
6
+ # c64-unpacker
7
+
8
+ ## Purpose
9
+
10
+ Find out if a packer compressed a C64 program. Get the unpacked program for analysis.
11
+
12
+ This skill does not change project knowledge.
13
+
14
+ ## Inputs
15
+
16
+ - The program: a PRG file, relative to the project directory.
17
+ - For a capture: the address where the unpacked program starts, and where to write the result.
18
+
19
+ ## Tools / execution path
20
+
21
+ ```
22
+ node <skill>/scripts/unpack.ts inspect original/game.prg
23
+ node <skill>/scripts/unpack.ts trace original/game.prg
24
+ node <skill>/scripts/unpack.ts capture original/game.prg --until '$c000' --out unpacked/game.prg --range '$c000' '$cfff'
25
+ ```
26
+
27
+ `inspect` examines the file locally. `trace` and `capture` start their own emulator through the host runtime and start the program. `trace` runs the program and finds the memory that the program wrote and then executed. `capture` stops the program when the CPU gets to the `--until` address.
28
+
29
+ | Option | Meaning |
30
+ |---|---|
31
+ | `--until '$c000'` | The address where the unpacked program starts. |
32
+ | `--out unpacked/game.prg` | Where to write the result, relative to the project directory. |
33
+ | `--range '$c000' '$cfff'` | Write this range as a PRG. Without it, the script writes all 64 KiB of RAM as a flat file. |
34
+ | `--timeout-frames 3000` | How many frames the program can run before the capture stops (default 3000). |
35
+ | `--frames 3000` | `trace` only: how many frames the program runs (default 3000). |
36
+ | `--entry '$c000'` | `trace` only: start the program at this address. Without it, `trace` types RUN. A program without a BASIC start line needs it. |
37
+
38
+ The capture reads RAM, also the RAM under the ROMs and the I/O area. The script prints one JSON object.
39
+
40
+ ## Workflow
41
+
42
+ 1. Run `inspect`. Read `basicStart` and `packing`:
43
+ - `likely`: most of the bytes look compressed. Packed data has this property.
44
+ - `unlikely`: the bytes look like code and data. A simple cruncher can also give this result.
45
+ - `unclear`: the file is short, or the blocks do not give clear evidence.
46
+ 2. Run `trace`. It gives the evidence from the run. Read `packing`:
47
+ - `likely`: the program wrote code and then executed it. It unpacked, decrypted or moved code. `runningRange` is the code that runs now. Its `start` is usually the entry point of the unpacked program.
48
+ - `unclear`: the program executed a small quantity of code that it wrote. This can be code that changes itself.
49
+ - `unlikely`: the program executed no code that it wrote. If the program waits for a key or a joystick, it can move code after that input. Then use the c64-emulator skill.
50
+ 3. Find the address where the unpacked program starts:
51
+ - Use `runningRange.start` from `trace`, or `suggestedUntil`. The CPU gets to `suggestedUntil` after the unpacking.
52
+ - Or use the c64-emulator skill: set a breakpoint after the unpack loop, or look at the last JMP of the unpacker.
53
+ 4. Run `capture` with that address. Use `--range` for the area that holds the unpacked program, if you know it.
54
+ 5. Make sure that the result is the application: the code at the start address must not be the unpacker again.
55
+ 6. Give the result to the c64-static-analysis skill. For a flat 64 KiB file, use its `--flat64k` option.
56
+
57
+ ## Knowledge
58
+
59
+ Automatic write:
60
+ - none.
61
+
62
+ Do not record the unpacker in the project knowledge, unless the user asks to examine the unpacker.
63
+
64
+ ## Result
65
+
66
+ The packing evidence, or the unpacked program in the project with the address where it starts.
67
+
68
+ The exit status is 0 for a result. The exit status is 1 for `traced: false`, for `captured: false` and for an error. The exit status is 2 for a wrong argument.
69
+
70
+ ## Failure and conflicts
71
+
72
+ - `traced: false`: the emulator did not load the program into memory in the time limit. The reason gives the load address. Make sure that the file is a PRG that the C64 can load.
73
+ - `captured: false`: the program did not get to the address in the time limit. The reason tells where it stopped. Find a better address with the c64-emulator skill, or give more frames.
74
+ - Do not name a packer from weak evidence. The script never names one.
75
+ - Some programs unpack in more than one stage. Make sure that the captured code is the application and not a second unpacker.
76
+ - `not-found` or `invalid-input`: a path or an argument is wrong. Read the message.
77
+ - `operation-failed` with a message about the host runtime: the host runtime does not run. Tell the user to start the host runtime with `npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host`.
78
+
79
+ ## Handoffs
80
+
81
+ - Unpacked machine code → c64-static-analysis
82
+ - Find the start address at run time → c64-emulator
@@ -0,0 +1,135 @@
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
+ }
@@ -0,0 +1,216 @@
1
+ // The c64-unpacker script. "inspect" gives local evidence of packing; it never
2
+ // names a packer. "trace" runs the program and reports the memory that it
3
+ // wrote and then executed: the run-time evidence that also finds a simple
4
+ // cruncher whose bytes look like code. "capture" runs the program in an
5
+ // emulator until it reaches a given address (the unpacked entry point) and
6
+ // writes the memory into the project. No native unpack tool is used.
7
+
8
+ import { mkdirSync, writeFileSync } from "node:fs";
9
+ import { dirname } from "node:path";
10
+ import { parseArgs } from "node:util";
11
+
12
+ import { formatC64Address, parseC64Address, textToPetscii } from "#src/c64.ts";
13
+ import { WireFailure } from "#src/host-client/tools.ts";
14
+ import { readProjectFile } from "#src/host-client/transfer.ts";
15
+ import { ViceSessionClient } from "#src/host-client/vice-session.ts";
16
+ import { resolveProjectPath } from "#src/project.ts";
17
+ import { runScript, UsageError } from "#src/script.ts";
18
+ import { inspect, PackingError, readMemoryMap, underRom, writtenThenExecuted } from "./evidence.ts";
19
+
20
+ const USAGE = `unpack.ts inspect <program.prg>
21
+ unpack.ts trace <program.prg> [--frames <n>] [--entry <address>]
22
+ unpack.ts capture <program.prg> --until <address> --out <file> [--range <start> <end>] [--timeout-frames <n>]
23
+
24
+ inspect packing evidence: the BASIC start and the entropy of the bytes
25
+ trace run the program for <n> frames (default 3000) and report the memory
26
+ that it wrote and then executed; --entry starts it there instead of RUN
27
+ capture run the program until the CPU gets to <address>, then write memory:
28
+ with --range, a PRG of that range; without it, all 64 KiB of RAM
29
+
30
+ Addresses are $ and four hex digits. The result is one JSON object.`;
31
+
32
+ const MAX_READ = 4096;
33
+
34
+ function address(text: string, option: string): number {
35
+ try {
36
+ return parseC64Address(text);
37
+ } catch {
38
+ throw new UsageError(`${option} must be $ followed by four hex digits, not ${text}`);
39
+ }
40
+ }
41
+
42
+ /** Written-then-executed bytes from this many on are unpacking, decrypting or relocating, not self-changing code. */
43
+ const UNPACKED_BYTES = 256;
44
+ const LOAD_POLL_FRAMES = 25;
45
+ const LOAD_LIMIT_FRAMES = 1500;
46
+ const MAX_ADVANCE = 10_000;
47
+ /** The most ranges one memory map read gives. */
48
+ const MAX_RANGES = 1000;
49
+
50
+ async function trace(program: string, options: { frames: number; entry?: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
51
+ const { bytes } = readProjectFile(program);
52
+ if (bytes.length < 3) throw new UsageError("the file is too short for a PRG");
53
+ const load = bytes[0]! | (bytes[1]! << 8);
54
+ const basic = inspect(bytes).basicStart;
55
+ if (options.entry === undefined && basic === null) throw new UsageError("the program has no BASIC start line; give --entry <address>");
56
+ const client = await ViceSessionClient.open({ videoStandard: "pal" });
57
+ try {
58
+ // Loaded but not started, so the memory map can start empty: the KERNAL start-up writes all of RAM.
59
+ await client.autostart({ path: program, index: 0, run: false });
60
+ const head = bytes.subarray(2, Math.min(bytes.length, 2 + 64)).toString("hex");
61
+ let loaded = false;
62
+ for (let frames = 0; frames < LOAD_LIMIT_FRAMES && !loaded; frames += LOAD_POLL_FRAMES) {
63
+ await client.execution({ action: "advance-frames", count: LOAD_POLL_FRAMES, space: "c64" });
64
+ loaded = (await client.memoryRead({ address: load, size: head.length / 2, space: "c64", view: "ram" })).data === head;
65
+ }
66
+ if (!loaded) return { output: { traced: false, reason: `The program was not in memory at ${formatC64Address(load)} after ${LOAD_LIMIT_FRAMES} frames.` }, failed: true };
67
+ await client.memmap({ action: "clear" });
68
+ if (options.entry !== undefined) await client.registersSet("c64", { pc: options.entry });
69
+ else await client.keyboard(textToPetscii("RUN\n"));
70
+ let ran = 0;
71
+ while (ran < options.frames) {
72
+ const step = await client.execution({ action: "advance-frames", count: Math.min(MAX_ADVANCE, options.frames - ran), space: "c64" });
73
+ const advanced = step.advancedFrames ?? 0;
74
+ ran += advanced;
75
+ if (advanced === 0) break;
76
+ }
77
+ const { pc } = await client.registersGet("c64");
78
+ const ranges = await readMemoryMap(async (start, end) => {
79
+ const map = await client.memmap({ action: "read", start, end, maxRanges: MAX_RANGES });
80
+ if (!("ranges" in map)) throw new WireFailure("operation-failed", "The emulator did not give the memory map.");
81
+ return map.ranges;
82
+ }, MAX_RANGES);
83
+ const { written, bytes: count, running } = writtenThenExecuted(ranges, pc);
84
+ const packing = count >= UNPACKED_BYTES || running !== undefined ? "likely" : count > 0 ? "unclear" : "unlikely";
85
+ return {
86
+ output: {
87
+ traced: true,
88
+ frames: ran,
89
+ pc: formatC64Address(pc),
90
+ writtenThenExecuted: written.map((range) => ({
91
+ start: formatC64Address(range.start),
92
+ end: formatC64Address(range.end),
93
+ ...(underRom(range.start) ? { underRom: true } : {}),
94
+ })),
95
+ bytes: count,
96
+ packing,
97
+ evidence:
98
+ packing === "likely"
99
+ ? running !== undefined
100
+ ? `The CPU now runs code at ${formatC64Address(pc)} that the program wrote: it unpacked, decrypted or relocated it.`
101
+ : `The program wrote ${count} bytes and then executed them: it unpacked, decrypted or relocated code.`
102
+ : packing === "unclear"
103
+ ? `The program executed ${count} bytes that it wrote. This can be code that changes itself or a small loader.`
104
+ : "The program executed no code that it wrote while it ran.",
105
+ // A point after the unpacking where the unpacked code runs; the entry point can be at the start of its range.
106
+ ...(running === undefined ? {} : { suggestedUntil: formatC64Address(pc), runningRange: { start: formatC64Address(running.start), end: formatC64Address(running.end) } }),
107
+ },
108
+ failed: false,
109
+ };
110
+ } finally {
111
+ await client.close();
112
+ }
113
+ }
114
+
115
+ async function capture(program: string, options: { until: number; out: string; range?: [number, number]; timeoutFrames: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
116
+ const client = await ViceSessionClient.open({ videoStandard: "pal" });
117
+ try {
118
+ await client.autostart({ path: program, index: 0, run: true });
119
+ const reached = await client.runUntil({ target: { kind: "address", address: options.until, space: "c64" }, timeoutFrames: options.timeoutFrames });
120
+ if (!reached.reached) {
121
+ return {
122
+ output: { captured: false, reason: `The program did not get to ${formatC64Address(options.until)} in ${options.timeoutFrames} frames. It stopped by ${reached.stopReason} at ${formatC64Address(reached.pc)}.` },
123
+ failed: true,
124
+ };
125
+ }
126
+ const [start, end] = options.range ?? [0x0000, 0xffff];
127
+ const memory = Buffer.alloc(end - start + 1);
128
+ for (let at = start; at <= end; at += MAX_READ) {
129
+ const size = Math.min(MAX_READ, end - at + 1);
130
+ // The RAM view: unpacked code is often under the ROMs or the I/O area.
131
+ const read = await client.memoryRead({ address: at, size, space: "c64", view: "ram" });
132
+ Buffer.from(read.data, "hex").copy(memory, at - start);
133
+ }
134
+ const bytes = options.range === undefined ? memory : Buffer.concat([Buffer.from([start & 0xff, start >> 8]), memory]);
135
+ const path = resolveProjectPath(options.out);
136
+ mkdirSync(dirname(path), { recursive: true });
137
+ writeFileSync(path, bytes);
138
+ const registers = await client.registersGet("c64");
139
+ return {
140
+ output: {
141
+ captured: true,
142
+ pc: formatC64Address(registers.pc),
143
+ output: options.out,
144
+ ...(options.range === undefined ? { flat64k: true } : { loadRange: { start: formatC64Address(start), end: formatC64Address(end) } }),
145
+ registers: { a: registers.a, x: registers.x, y: registers.y, sp: registers.sp },
146
+ },
147
+ failed: false,
148
+ };
149
+ } finally {
150
+ await client.close();
151
+ }
152
+ }
153
+
154
+ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }> {
155
+ const { values, positionals } = parseArgs({
156
+ args: argv,
157
+ strict: true,
158
+ allowPositionals: true,
159
+ options: {
160
+ until: { type: "string" },
161
+ out: { type: "string" },
162
+ range: { type: "string" },
163
+ "timeout-frames": { type: "string" },
164
+ frames: { type: "string" },
165
+ entry: { type: "string" },
166
+ help: { type: "boolean" },
167
+ },
168
+ });
169
+ if (values.help) throw new UsageError("");
170
+ const [command, program, ...rest] = positionals;
171
+ if (program === undefined) throw new UsageError("give a command and a program");
172
+ if (command === "inspect") {
173
+ if (rest.length > 0) throw new UsageError("inspect takes one program");
174
+ const { bytes } = readProjectFile(program);
175
+ try {
176
+ return { output: inspect(bytes), failed: false };
177
+ } catch (error) {
178
+ if (error instanceof PackingError) throw new UsageError(error.message);
179
+ throw error;
180
+ }
181
+ }
182
+ if (command === "trace") {
183
+ if (rest.length > 0) throw new UsageError("trace takes one program");
184
+ const frames = values.frames === undefined ? 3000 : Number(values.frames);
185
+ if (!Number.isInteger(frames) || frames < 1 || frames > 30_000) throw new UsageError("--frames must be from 1 to 30000");
186
+ return trace(program, { frames, ...(values.entry === undefined ? {} : { entry: address(values.entry, "--entry") }) });
187
+ }
188
+ if (command !== "capture") throw new UsageError(`unknown command ${command}`);
189
+ if (values.until === undefined || values.out === undefined) throw new UsageError("capture needs --until and --out");
190
+ // --range takes two values: the end is the next positional argument.
191
+ let range: [number, number] | undefined;
192
+ if (values.range !== undefined) {
193
+ if (rest.length !== 1) throw new UsageError("--range takes a start and an end address");
194
+ range = [address(values.range, "--range"), address(rest[0]!, "--range")];
195
+ if (range[1] < range[0]) throw new UsageError("the --range end must not be before its start");
196
+ } else if (rest.length > 0) {
197
+ throw new UsageError("too many arguments");
198
+ }
199
+ try {
200
+ resolveProjectPath(values.out);
201
+ } catch {
202
+ throw new UsageError(`--out must be a path relative to the project directory that stays inside it, not ${values.out}`);
203
+ }
204
+ const timeoutFrames = values["timeout-frames"] === undefined ? 3000 : Number(values["timeout-frames"]);
205
+ if (!Number.isInteger(timeoutFrames) || timeoutFrames < 1 || timeoutFrames > 30_000) throw new UsageError("--timeout-frames must be from 1 to 30000");
206
+ return capture(program, { until: address(values.until, "--until"), out: values.out, ...(range === undefined ? {} : { range }), timeoutFrames });
207
+ }
208
+
209
+ await runScript(
210
+ async () => {
211
+ const { output, failed } = await run(process.argv.slice(2));
212
+ if (failed) process.exitCode = 1;
213
+ return output;
214
+ },
215
+ { usage: USAGE },
216
+ );