@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.4
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 +19 -59
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +5 -2
- package/skills/c64-assembler/scripts/assemble.ts +8 -1
- package/skills/c64-assembler/scripts/compare.ts +84 -0
- package/skills/c64-basic/SKILL.md +1 -2
- package/skills/c64-disk/SKILL.md +20 -6
- package/skills/c64-disk/scripts/containers.ts +124 -0
- package/skills/c64-disk/scripts/disk.ts +59 -5
- package/skills/c64-emulator/SKILL.md +11 -4
- package/skills/c64-knowledge/SKILL.md +53 -3
- package/skills/c64-knowledge/scripts/knowledge.ts +258 -29
- package/skills/c64-memory-map/SKILL.md +4 -1
- package/skills/c64-memory-map/scripts/memmap.ts +14 -3
- package/skills/c64-provenance/SKILL.md +37 -9
- package/skills/c64-provenance/scripts/compare.ts +60 -29
- package/skills/c64-provenance/scripts/provenance.ts +248 -19
- package/skills/c64-provenance/scripts/runtime.ts +179 -0
- package/skills/c64-provenance/scripts/separate.ts +132 -0
- package/skills/c64-reverse-engineering/SKILL.md +65 -11
- package/skills/c64-source/SKILL.md +119 -0
- package/skills/c64-source/scripts/generate.ts +402 -0
- package/skills/c64-source/scripts/loader.ts +320 -0
- package/skills/c64-source/scripts/opcodes.ts +61 -0
- package/skills/c64-source/scripts/source.ts +558 -0
- package/skills/c64-static-analysis/SKILL.md +27 -4
- package/skills/c64-static-analysis/scripts/analyze.ts +102 -13
- package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
- package/skills/c64-static-analysis/scripts/findings.ts +6 -4
- package/skills/c64-static-analysis/scripts/idioms.ts +144 -0
- package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
- package/skills/c64-testing/SKILL.md +27 -3
- package/skills/c64-testing/references/scenario-format.md +43 -3
- package/skills/c64-testing/scripts/run.ts +189 -0
- package/skills/c64-testing/scripts/scenario.ts +153 -135
- package/skills/c64-testing/scripts/test.ts +16 -6
- package/skills/c64-unpacker/SKILL.md +65 -14
- package/skills/c64-unpacker/scripts/evidence.ts +86 -21
- package/skills/c64-unpacker/scripts/loads.ts +174 -0
- package/skills/c64-unpacker/scripts/machine.ts +68 -0
- package/skills/c64-unpacker/scripts/unpack.ts +302 -48
- package/src/c64.ts +91 -0
- package/src/cli/status.ts +3 -1
- package/src/host/main.ts +10 -25
- package/src/host/server.ts +8 -40
- package/src/host/vice/adapter.ts +25 -10
- package/src/host/vice/session.ts +64 -20
- package/src/host-client/autostart.ts +45 -0
- package/src/host-client/connect.ts +4 -5
- package/src/host-client/coverage.ts +59 -0
- package/src/host-client/steps.ts +170 -0
- package/src/host-client/transfer.ts +29 -3
- package/src/host-client/vice-session.ts +42 -13
- package/src/knowledge/history.ts +40 -13
- package/src/knowledge/import.ts +72 -56
- package/src/knowledge/read.ts +203 -52
- package/src/knowledge/schema.ts +94 -12
- package/src/knowledge/write.ts +432 -123
- package/src/mcp/main.ts +1 -4
- package/src/mcp/output.ts +32 -0
- package/src/mcp/server.ts +4 -2
- package/src/mcp/tools/debug.ts +110 -24
- package/src/mcp/tools/input.ts +1 -0
- package/src/mcp/tools/media.ts +22 -6
- package/src/mcp/tools/memory.ts +57 -10
- package/src/mcp/tools/video.ts +9 -3
- package/src/native/discover.ts +5 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +30 -1
- package/{skills/c64-memory-map/scripts → src}/platform.ts +18 -2
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/vice.ts +72 -18
- package/src/script.ts +3 -2
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Writes the structural result for the loaded image: functions with the
|
|
4
4
|
// source of their names, code and defined-data regions, memory references
|
|
5
|
-
// from instructions,
|
|
5
|
+
// from instructions, every instruction with its bytes, mnemonic, operand
|
|
6
|
+
// text and flow targets (D44), and the requested decompilations within their bounds.
|
|
6
7
|
//@category c64-re-tools
|
|
7
8
|
|
|
8
9
|
import java.io.FileReader;
|
|
@@ -53,6 +54,7 @@ public class C64Export extends GhidraScript {
|
|
|
53
54
|
out.add("functions", functions(coverage));
|
|
54
55
|
out.add("regions", regions(coverage));
|
|
55
56
|
out.add("references", references(coverage));
|
|
57
|
+
out.add("instructions", instructions(coverage));
|
|
56
58
|
JsonArray notDecompiled = new JsonArray();
|
|
57
59
|
out.add("decompilations", decompile(request, notDecompiled));
|
|
58
60
|
out.add("notDecompiled", notDecompiled);
|
|
@@ -156,6 +158,36 @@ public class C64Export extends GhidraScript {
|
|
|
156
158
|
return references;
|
|
157
159
|
}
|
|
158
160
|
|
|
161
|
+
/** Every instruction in the coverage, in address order, as Ghidra decoded it with the 6510 language. */
|
|
162
|
+
private JsonArray instructions(AddressSet coverage) throws Exception {
|
|
163
|
+
JsonArray instructions = new JsonArray();
|
|
164
|
+
for (Instruction instruction : currentProgram.getListing().getInstructions(coverage, true)) {
|
|
165
|
+
JsonObject item = new JsonObject();
|
|
166
|
+
item.addProperty("address", instruction.getMinAddress().getOffset());
|
|
167
|
+
StringBuilder bytes = new StringBuilder();
|
|
168
|
+
for (byte value : instruction.getBytes()) {
|
|
169
|
+
bytes.append(String.format("%02x", value & 0xff));
|
|
170
|
+
}
|
|
171
|
+
item.addProperty("bytes", bytes.toString());
|
|
172
|
+
item.addProperty("mnemonic", instruction.getMnemonicString().toLowerCase());
|
|
173
|
+
StringBuilder operands = new StringBuilder();
|
|
174
|
+
for (int index = 0; index < instruction.getNumOperands(); index++) {
|
|
175
|
+
if (index > 0) {
|
|
176
|
+
operands.append(",");
|
|
177
|
+
}
|
|
178
|
+
operands.append(instruction.getDefaultOperandRepresentation(index));
|
|
179
|
+
}
|
|
180
|
+
item.addProperty("operands", operands.toString());
|
|
181
|
+
JsonArray flows = new JsonArray();
|
|
182
|
+
for (Address flow : instruction.getFlows()) {
|
|
183
|
+
flows.add(flow.getOffset());
|
|
184
|
+
}
|
|
185
|
+
item.add("flows", flows);
|
|
186
|
+
instructions.add(item);
|
|
187
|
+
}
|
|
188
|
+
return instructions;
|
|
189
|
+
}
|
|
190
|
+
|
|
159
191
|
private static void add(JsonArray references, Set<String> seen, long from, long to, String type) {
|
|
160
192
|
if (!seen.add(from + ":" + to + ":" + type)) {
|
|
161
193
|
return;
|
package/src/native/status.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { stripVTControlCharacters } from "node:util";
|
|
|
9
9
|
import { WireFailure } from "../protocol/messages.ts";
|
|
10
10
|
import { type ToolStatus } from "../protocol/tools.ts";
|
|
11
11
|
import { ACME, DXA, findTool } from "./discover.ts";
|
|
12
|
-
import { findGhidra, ghidraVersion } from "./ghidra/index.ts";
|
|
12
|
+
import { findGhidra, ghidraVersion, NO_USABLE_JDK, noJdk } from "./ghidra/index.ts";
|
|
13
13
|
import { runTool, type ToolContext } from "./run.ts";
|
|
14
14
|
import { Workspace } from "./staging.ts";
|
|
15
15
|
|
|
@@ -35,10 +35,21 @@ export function versionLine(output: string, pattern: RegExp): string | undefined
|
|
|
35
35
|
.find((line) => pattern.test(line));
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
async function run(
|
|
38
|
+
async function run(
|
|
39
|
+
name: string,
|
|
40
|
+
path: string,
|
|
41
|
+
args: string[],
|
|
42
|
+
version: (output: string) => string | undefined,
|
|
43
|
+
context: ToolContext,
|
|
44
|
+
/** A known cause in the output, said in place of the exit status. */
|
|
45
|
+
cause?: (output: string) => string | undefined,
|
|
46
|
+
): Promise<ToolStatus> {
|
|
39
47
|
const result = await runTool({ argv: [path, ...args], cwd: tmpdir(), supervisor: context.supervisor, signal: context.signal, timeoutMs: PROBE_TIMEOUT_MS, env: context.env });
|
|
40
|
-
const
|
|
48
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
49
|
+
const found = version(output);
|
|
41
50
|
if (found !== undefined) return { name, found: true, path, version: found, runs: true };
|
|
51
|
+
const known = cause?.(output);
|
|
52
|
+
if (known !== undefined) return { name, found: true, path, runs: false, problem: known };
|
|
42
53
|
const problem = result.timedOut ? "It did not answer in time." : result.spawnError !== undefined ? `It did not start (${result.spawnError}).` : `It did not run correctly (exit ${result.code ?? result.signal}).`;
|
|
43
54
|
return { name, found: true, path, runs: false, problem };
|
|
44
55
|
}
|
|
@@ -79,6 +90,7 @@ export async function probeGhidra(context: ToolContext): Promise<ToolStatus> {
|
|
|
79
90
|
[],
|
|
80
91
|
(output) => (/Headless Analyzer Usage/.test(output) ? ghidraVersion(ghidra) : undefined),
|
|
81
92
|
{ ...context, env: { ...env, XDG_CONFIG_HOME: settings, XDG_CACHE_HOME: settings } },
|
|
93
|
+
(output) => (NO_USABLE_JDK.test(output) ? noJdk(ghidra).message : undefined),
|
|
82
94
|
);
|
|
83
95
|
return { ...status, path: ghidra.root };
|
|
84
96
|
} finally {
|
package/src/native/types.ts
CHANGED
|
@@ -52,6 +52,17 @@ export interface GhidraParams {
|
|
|
52
52
|
decompile: number[];
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export interface GhidraInstruction {
|
|
56
|
+
address: number;
|
|
57
|
+
/** Lowercase hex, 1 to 3 bytes. */
|
|
58
|
+
bytes: string;
|
|
59
|
+
mnemonic: string;
|
|
60
|
+
/** Ghidra's text of the operands, empty for an implied operand. */
|
|
61
|
+
operands: string;
|
|
62
|
+
/** Where the flow goes besides the next instruction: a branch, jump or call target. */
|
|
63
|
+
flows: number[];
|
|
64
|
+
}
|
|
65
|
+
|
|
55
66
|
export interface GhidraResult {
|
|
56
67
|
coverage: Array<{ start: number; end: number }>;
|
|
57
68
|
functions: Array<{ entry: number; name: string; nameSource: "seed" | "generated" | "native" }>;
|
|
@@ -60,6 +71,8 @@ export interface GhidraResult {
|
|
|
60
71
|
decompilations: Array<{ entry: number; text: string; truncated: boolean }>;
|
|
61
72
|
/** Requested routines without decompiled code, with the decompiler's reason. */
|
|
62
73
|
notDecompiled?: Array<{ entry: number; reason: string }>;
|
|
74
|
+
/** Every instruction in the coverage, as Ghidra decoded it (D44). */
|
|
75
|
+
instructions: GhidraInstruction[];
|
|
63
76
|
/** Which categories are complete inside the coverage; private to the importer. */
|
|
64
77
|
completeness: { functions: boolean; regions: boolean; references: boolean };
|
|
65
78
|
/** The version of the Ghidra that made the result, for example "Ghidra 12.1.3". */
|
|
@@ -115,7 +128,7 @@ const isList = (value: unknown): value is unknown[] => Array.isArray(value);
|
|
|
115
128
|
*/
|
|
116
129
|
export function checkGhidraResult(value: unknown): GhidraResult {
|
|
117
130
|
if (!isObject(value)) fail("not an object");
|
|
118
|
-
const { coverage, functions, regions, references, decompilations, notDecompiled, completeness, toolVersion } = value;
|
|
131
|
+
const { coverage, functions, regions, references, decompilations, notDecompiled, completeness, toolVersion, instructions } = value;
|
|
119
132
|
if (!isList(coverage) || coverage.length === 0) fail("no coverage");
|
|
120
133
|
const ranges: Array<{ start: number; end: number }> = [];
|
|
121
134
|
for (const range of coverage) {
|
|
@@ -138,6 +151,22 @@ export function checkGhidraResult(value: unknown): GhidraResult {
|
|
|
138
151
|
if (!ranges.some((range) => range.start <= start && end <= range.end)) fail("a region lies outside the coverage");
|
|
139
152
|
previousEnd = end;
|
|
140
153
|
}
|
|
154
|
+
if (!isList(instructions)) fail("no instructions");
|
|
155
|
+
for (const instruction of instructions) {
|
|
156
|
+
if (
|
|
157
|
+
!isObject(instruction) ||
|
|
158
|
+
!isInteger(instruction.address, 0, 0xffff) ||
|
|
159
|
+
typeof instruction.bytes !== "string" ||
|
|
160
|
+
!/^(?:[0-9a-f]{2}){1,3}$/.test(instruction.bytes) ||
|
|
161
|
+
typeof instruction.mnemonic !== "string" ||
|
|
162
|
+
typeof instruction.operands !== "string" ||
|
|
163
|
+
!isList(instruction.flows) ||
|
|
164
|
+
!instruction.flows.every((flow) => isInteger(flow, 0, 0xffff))
|
|
165
|
+
) {
|
|
166
|
+
fail("bad instruction");
|
|
167
|
+
}
|
|
168
|
+
if (!covered(instruction.address)) fail("an instruction lies outside the coverage");
|
|
169
|
+
}
|
|
141
170
|
if (!isList(references)) fail("no references");
|
|
142
171
|
for (const reference of references) {
|
|
143
172
|
if (!isObject(reference) || !isInteger(reference.from, 0, 0xffff) || !isInteger(reference.to, 0, 0xffff) || !isOneOf(REFERENCE_KINDS, reference.type)) fail("bad reference");
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
// C64 platform reference data and decoders
|
|
1
|
+
// C64 platform reference data and decoders: the c64-memory-map script, and the
|
|
2
|
+
// platform names that c64-source uses (D52).
|
|
2
3
|
// Names follow "Mapping the Commodore 64". Only platform facts live here:
|
|
3
4
|
// an address without one belongs to the application.
|
|
4
5
|
|
|
5
|
-
import { d018Offsets, vicBankBase } from "
|
|
6
|
+
import { d018Offsets, vicBankBase } from "./c64.ts";
|
|
6
7
|
|
|
7
8
|
export interface Entry {
|
|
8
9
|
start: number;
|
|
@@ -340,6 +341,21 @@ export function lookup(address: number): Lookup {
|
|
|
340
341
|
return { address, kind: area.kind, area: area.area, meaning: area.meaning };
|
|
341
342
|
}
|
|
342
343
|
|
|
344
|
+
/**
|
|
345
|
+
* The platform name of each named address (D52): system variables, ROM
|
|
346
|
+
* entries and I/O registers, not mirrors, in address order. Only names that
|
|
347
|
+
* an assembler can use as a symbol, and each name once, at its lowest address.
|
|
348
|
+
*/
|
|
349
|
+
export function platformNames(): Array<{ address: number; name: string; meaning: string }> {
|
|
350
|
+
const io = IO.flatMap((block) => block.registers);
|
|
351
|
+
const seen = new Set<string>();
|
|
352
|
+
return [...SYSTEM, ...ROM, ...io]
|
|
353
|
+
.map((entry) => ({ address: entry.start, name: entry.name, meaning: entry.meaning }))
|
|
354
|
+
.filter((entry) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(entry.name))
|
|
355
|
+
.sort((a, b) => a.address - b.address)
|
|
356
|
+
.filter((entry) => !seen.has(entry.name) && seen.add(entry.name) !== undefined);
|
|
357
|
+
}
|
|
358
|
+
|
|
343
359
|
export type Decoded = Record<string, unknown>;
|
|
344
360
|
|
|
345
361
|
const bits = (value: number) => [0, 1, 2, 3, 4, 5, 6, 7].filter((bit) => (value & (1 << bit)) !== 0);
|
package/src/protocol/messages.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { ViceOperation, ViceOperations } from "./vice.ts";
|
|
|
7
7
|
/** Private Host Runtime protocol identifier. */
|
|
8
8
|
export const HOST_PROTOCOL_ID = "c64-re-tools-host" as const;
|
|
9
9
|
/** Bumped on any incompatible change; a mismatch fails the handshake. */
|
|
10
|
-
export const HOST_PROTOCOL_VERSION =
|
|
10
|
+
export const HOST_PROTOCOL_VERSION = 3;
|
|
11
11
|
/** After ready, the client pings this often, on every connection. */
|
|
12
12
|
export const HEARTBEAT_INTERVAL_MS = 10_000;
|
|
13
13
|
/** Either side closes a connection that has sent nothing for this long. */
|
|
@@ -54,13 +54,8 @@ export interface Hello {
|
|
|
54
54
|
role: Role;
|
|
55
55
|
/** vice-session only; fixed for the session. */
|
|
56
56
|
videoStandard?: VideoStandard;
|
|
57
|
-
/** The shared secret (C64RT_HOST_TOKEN) for a host that listens beyond loopback. */
|
|
58
|
-
token?: string;
|
|
59
57
|
}
|
|
60
58
|
|
|
61
|
-
/** Longest accepted shared token. */
|
|
62
|
-
const MAX_TOKEN_LENGTH = 256;
|
|
63
|
-
|
|
64
59
|
export interface Ready {
|
|
65
60
|
type: "ready";
|
|
66
61
|
}
|
|
@@ -111,6 +106,22 @@ export class ProtocolError extends Error {
|
|
|
111
106
|
}
|
|
112
107
|
|
|
113
108
|
/** Thrown by parameter/result validators; carries the wire code to answer with. */
|
|
109
|
+
/**
|
|
110
|
+
* The instruction at the end of a refusal that only the user can fix: a
|
|
111
|
+
* missing or broken tool, or a Host Runtime that does not run (D37).
|
|
112
|
+
*/
|
|
113
|
+
export const STOP_AND_ASK = "Stop the task. Give this message to the user and wait for the user. Do not make a replacement for the tool or the host runtime.";
|
|
114
|
+
|
|
115
|
+
/** The message with STOP_AND_ASK at its end, once. */
|
|
116
|
+
export function withStopAndAsk(message: string): string {
|
|
117
|
+
return message.endsWith(STOP_AND_ASK) ? message : `${message} ${STOP_AND_ASK}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The message that the agent sees for a refusal: an installation-incomplete one ends with STOP_AND_ASK. */
|
|
121
|
+
export function shownMessage(error: { code: string; message: string }): string {
|
|
122
|
+
return error.code === "installation-incomplete" ? withStopAndAsk(error.message) : error.message;
|
|
123
|
+
}
|
|
124
|
+
|
|
114
125
|
export class WireFailure extends Error implements WireError {
|
|
115
126
|
override name = "WireFailure";
|
|
116
127
|
readonly code: ErrorCode;
|
|
@@ -166,12 +177,8 @@ export function parseClientMessage(value: unknown): ReceivedClientMessage {
|
|
|
166
177
|
if (value.videoStandard !== undefined && !isOneOf(VIDEO_STANDARDS, value.videoStandard)) {
|
|
167
178
|
throw new ProtocolError("hello has an unknown video standard");
|
|
168
179
|
}
|
|
169
|
-
if (value.token !== undefined && (typeof value.token !== "string" || value.token.length > MAX_TOKEN_LENGTH)) {
|
|
170
|
-
throw new ProtocolError("hello has a malformed token");
|
|
171
|
-
}
|
|
172
180
|
const hello: Hello = { type: "hello", protocol: value.protocol, version: value.version, role: value.role };
|
|
173
181
|
if (value.videoStandard !== undefined) hello.videoStandard = value.videoStandard;
|
|
174
|
-
if (value.token !== undefined) hello.token = value.token;
|
|
175
182
|
return hello;
|
|
176
183
|
}
|
|
177
184
|
if (value.type === "request") {
|
package/src/protocol/vice.ts
CHANGED
|
@@ -94,6 +94,8 @@ export const MAX_WATCH_SIZE = 256;
|
|
|
94
94
|
export interface Breakpoint {
|
|
95
95
|
id: number;
|
|
96
96
|
address: number;
|
|
97
|
+
/** A range from address: the CPU stops at the first instruction it executes in it (D43). One address when not given. */
|
|
98
|
+
size?: number;
|
|
97
99
|
space: Space;
|
|
98
100
|
enabled: boolean;
|
|
99
101
|
/** The condition given at add, when there was one. */
|
|
@@ -112,15 +114,23 @@ export interface Watchpoint {
|
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
export type BreakpointParams =
|
|
115
|
-
| { action: "add"; address: number; space: Space; condition?: Condition }
|
|
116
|
-
| { action: "remove"
|
|
117
|
+
| { action: "add"; address: number; size?: number; space: Space; condition?: Condition }
|
|
118
|
+
| { action: "remove"; id: number }
|
|
119
|
+
| { action: "enable" | "disable"; id: number }
|
|
117
120
|
| { action: "list" };
|
|
118
121
|
|
|
119
122
|
export type WatchpointParams =
|
|
120
123
|
| { action: "add"; address: number; size: number; access: WatchAccess; space: Space; condition?: Condition }
|
|
121
|
-
| { action: "remove"
|
|
124
|
+
| { action: "remove"; id: number }
|
|
125
|
+
| { action: "enable" | "disable"; id: number }
|
|
122
126
|
| { action: "list" };
|
|
123
127
|
|
|
128
|
+
/** The result of remove: the point is gone, so only its id comes back. */
|
|
129
|
+
export interface PointRemoved {
|
|
130
|
+
removed: true;
|
|
131
|
+
id: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
124
134
|
// Chip state
|
|
125
135
|
|
|
126
136
|
export const VICII_MODES = ["text", "multicolor-text", "extended-color-text", "bitmap", "multicolor-bitmap", "invalid"] as const;
|
|
@@ -223,12 +233,11 @@ export interface ProfileEntry {
|
|
|
223
233
|
percent: number;
|
|
224
234
|
}
|
|
225
235
|
|
|
236
|
+
/** Addresses with the same access flags: the coverage byte of src/c64.ts (D40), RAM, ROM and I/O apart. */
|
|
226
237
|
export interface MemmapRange {
|
|
227
238
|
start: number;
|
|
228
239
|
end: number;
|
|
229
|
-
|
|
230
|
-
read: boolean;
|
|
231
|
-
write: boolean;
|
|
240
|
+
flags: number;
|
|
232
241
|
}
|
|
233
242
|
|
|
234
243
|
// Screen baselines and snapshots
|
|
@@ -386,6 +395,8 @@ export interface RunUntilResult {
|
|
|
386
395
|
export interface ViceOperations {
|
|
387
396
|
status: { params: Record<string, never>; result: MachineStatus };
|
|
388
397
|
memoryRead: { params: MemoryReadParams; result: MemoryReadResult };
|
|
398
|
+
/** The I/O registers at $d000-$dfff as the chips hold them, whatever the CPU banks in, without side effects (D43). */
|
|
399
|
+
ioRead: { params: { address: number; size: number }; result: MemoryReadResult };
|
|
389
400
|
registersGet: { params: { space: Space }; result: Registers };
|
|
390
401
|
memoryWrite: { params: MemoryWriteParams; result: { address: number; bytesWritten: number } };
|
|
391
402
|
/** `pattern`: bytes, with null as a wildcard. */
|
|
@@ -412,8 +423,8 @@ export interface ViceOperations {
|
|
|
412
423
|
/** `data`: PETSCII bytes as lowercase hex. */
|
|
413
424
|
keyboard: { params: { data: string }; result: { queuedBytes: number } };
|
|
414
425
|
joystick: { params: JoystickState; result: JoystickState };
|
|
415
|
-
breakpoint: { params: BreakpointParams; result: Breakpoint | { breakpoints: Breakpoint[] } };
|
|
416
|
-
watchpoint: { params: WatchpointParams; result: Watchpoint | { watchpoints: Watchpoint[] } };
|
|
426
|
+
breakpoint: { params: BreakpointParams; result: Breakpoint | PointRemoved | { breakpoints: Breakpoint[] } };
|
|
427
|
+
watchpoint: { params: WatchpointParams; result: Watchpoint | PointRemoved | { watchpoints: Watchpoint[] } };
|
|
417
428
|
cpuHistory: { params: { limit: number; space: Space }; result: { entries: HistoryEntry[] } };
|
|
418
429
|
backtrace: { params: { depth: number; space: Space }; result: { frames: BacktraceFrame[] } };
|
|
419
430
|
/** `cycles` is a decimal string. */
|
|
@@ -421,7 +432,8 @@ export interface ViceOperations {
|
|
|
421
432
|
profile: { params: { limit: number }; result: { entries: ProfileEntry[] } };
|
|
422
433
|
memmap: {
|
|
423
434
|
params: { action: "read"; start: number; end: number; maxRanges: number } | { action: "clear" };
|
|
424
|
-
|
|
435
|
+
/** `next`: more ranges follow from this address; read again from there. */
|
|
436
|
+
result: { ranges: MemmapRange[]; next?: number } | { cleared: boolean };
|
|
425
437
|
};
|
|
426
438
|
vicii: { params: Record<string, never>; result: ViciiState };
|
|
427
439
|
/** `indexes`: unique sprite numbers 0-7, in the order to report them. */
|
|
@@ -440,6 +452,14 @@ export interface ViceOperations {
|
|
|
440
452
|
params: { action: "save" | "restore" | "discard"; name: string } | { action: "list" };
|
|
441
453
|
result: { saved: boolean; name: string } | { restored: boolean; state: RunState } | { snapshots: string[] } | { discarded: boolean };
|
|
442
454
|
};
|
|
455
|
+
/**
|
|
456
|
+
* Snapshot files (D41): export sends the bytes of a named snapshot back as
|
|
457
|
+
* one attachment; restore takes the bytes of a .vsf as one attachment.
|
|
458
|
+
*/
|
|
459
|
+
snapshotFile: {
|
|
460
|
+
params: { action: "export"; name: string } | { action: "restore" };
|
|
461
|
+
result: { exported: boolean; bytes: number } | { restored: boolean; state: RunState };
|
|
462
|
+
};
|
|
443
463
|
screenDiscard: { params: { baseline: string }; result: { discarded: boolean } };
|
|
444
464
|
warp: { params: { enabled: boolean }; result: { enabled: boolean } };
|
|
445
465
|
window: { params: { action: (typeof WINDOW_ACTIONS)[number] }; result: WindowResult };
|
|
@@ -469,6 +489,8 @@ export const VICE_OPERATIONS = [
|
|
|
469
489
|
"screenBaselines",
|
|
470
490
|
"screenDiscard",
|
|
471
491
|
"snapshot",
|
|
492
|
+
"snapshotFile",
|
|
493
|
+
"ioRead",
|
|
472
494
|
"cpuHistory",
|
|
473
495
|
"backtrace",
|
|
474
496
|
"timing",
|
|
@@ -483,8 +505,9 @@ export const VICE_OPERATIONS = [
|
|
|
483
505
|
"window",
|
|
484
506
|
] as const satisfies readonly ViceOperation[];
|
|
485
507
|
|
|
486
|
-
/** How many attachments
|
|
487
|
-
export function attachmentCount(op: ViceOperation): number {
|
|
508
|
+
/** How many attachments an operation takes; a snapshotFile restore takes the .vsf. */
|
|
509
|
+
export function attachmentCount(op: ViceOperation, params?: unknown): number {
|
|
510
|
+
if (op === "snapshotFile") return isObject(params) && params.action === "restore" ? 1 : 0;
|
|
488
511
|
return op === "programLoad" || op === "autostart" || op === "diskAttach" ? 1 : 0;
|
|
489
512
|
}
|
|
490
513
|
|
|
@@ -514,6 +537,15 @@ export function validateViceParams<O extends ViceOperation>(op: O, params: unkno
|
|
|
514
537
|
}
|
|
515
538
|
return { action: params.action, name: validName(params.name, "name") } as ViceOperations[O]["params"];
|
|
516
539
|
}
|
|
540
|
+
case "snapshotFile": {
|
|
541
|
+
if (params.action === "restore") {
|
|
542
|
+
onlyFields(params, ["action"]);
|
|
543
|
+
return { action: "restore" } as ViceOperations[O]["params"];
|
|
544
|
+
}
|
|
545
|
+
if (params.action !== "export") invalid("action must be export or restore");
|
|
546
|
+
onlyFields(params, ["action", "name"]);
|
|
547
|
+
return { action: "export", name: validName(params.name, "name") } as ViceOperations[O]["params"];
|
|
548
|
+
}
|
|
517
549
|
case "screenCompare": {
|
|
518
550
|
onlyFields(params, ["baseline", "maxMismatchRatio", "mask", "includeDiff"]);
|
|
519
551
|
const ratio = params.maxMismatchRatio;
|
|
@@ -609,6 +641,12 @@ export function validateViceParams<O extends ViceOperation>(op: O, params: unkno
|
|
|
609
641
|
const result: MemoryReadParams = { address: params.address, size: params.size, space: params.space, view: params.view };
|
|
610
642
|
return result as ViceOperations[O]["params"];
|
|
611
643
|
}
|
|
644
|
+
case "ioRead": {
|
|
645
|
+
onlyFields(params, ["address", "size"]);
|
|
646
|
+
if (!isInteger(params.address, 0xd000, 0xdfff)) invalid("address must be in the I/O area $d000-$dfff");
|
|
647
|
+
if (!isInteger(params.size, 1, 0x1000) || params.address + params.size > 0xe000) invalid("the range must stay inside $d000-$dfff");
|
|
648
|
+
return { address: params.address, size: params.size } as ViceOperations[O]["params"];
|
|
649
|
+
}
|
|
612
650
|
case "registersGet": {
|
|
613
651
|
onlyFields(params, ["space"]);
|
|
614
652
|
if (!isOneOf(SPACES, params.space)) invalid("space must be c64 or drive8");
|
|
@@ -765,7 +803,7 @@ export function validateViceParams<O extends ViceOperation>(op: O, params: unkno
|
|
|
765
803
|
if (!isInteger(params.id, 1, 0xffff_ffff)) invalid("id must be a positive integer");
|
|
766
804
|
return { action: params.action, id: params.id } as ViceOperations[O]["params"];
|
|
767
805
|
}
|
|
768
|
-
onlyFields(params, op === "watchpoint" ? ["action", "address", "space", "size", "access", "condition"] : ["action", "address", "space", "condition"]);
|
|
806
|
+
onlyFields(params, op === "watchpoint" ? ["action", "address", "space", "size", "access", "condition"] : ["action", "address", "space", "size", "condition"]);
|
|
769
807
|
if (!isInteger(params.address, 0, 0xffff)) invalid("address must be an integer from 0 to 65535");
|
|
770
808
|
if (!isOneOf(SPACES, params.space)) invalid("space must be c64 or drive8");
|
|
771
809
|
const added: Record<string, unknown> = { action: "add", address: params.address, space: params.space };
|
|
@@ -775,6 +813,9 @@ export function validateViceParams<O extends ViceOperation>(op: O, params: unkno
|
|
|
775
813
|
if (!isOneOf(WATCH_ACCESS, params.access)) invalid("access must be read, write or read-write");
|
|
776
814
|
added.size = params.size;
|
|
777
815
|
added.access = params.access;
|
|
816
|
+
} else if (params.size !== undefined) {
|
|
817
|
+
if (!isInteger(params.size, 1, 0x10000) || params.address + params.size > 0x10000) invalid("size must keep the range inside $0000-$ffff");
|
|
818
|
+
added.size = params.size;
|
|
778
819
|
}
|
|
779
820
|
if (params.condition !== undefined) added.condition = validateCondition(params.condition);
|
|
780
821
|
return added as ViceOperations[O]["params"];
|
|
@@ -868,11 +909,16 @@ function validateRunTarget(value: unknown): RunTarget {
|
|
|
868
909
|
return invalid("target kind must be address, memory or raster");
|
|
869
910
|
}
|
|
870
911
|
|
|
912
|
+
function isRemoved(value: unknown): value is PointRemoved {
|
|
913
|
+
return isObject(value) && value.removed === true && isInteger(value.id, 1, 0xffff_ffff) && Object.keys(value).length === 2;
|
|
914
|
+
}
|
|
915
|
+
|
|
871
916
|
function isBreakpoint(value: unknown): value is Breakpoint {
|
|
872
917
|
return (
|
|
873
918
|
isObject(value) &&
|
|
874
919
|
isInteger(value.id, 1, 0xffff_ffff) &&
|
|
875
920
|
isInteger(value.address, 0, 0xffff) &&
|
|
921
|
+
(value.size === undefined || isInteger(value.size, 1, 0x10000)) &&
|
|
876
922
|
isOneOf(SPACES, value.space) &&
|
|
877
923
|
typeof value.enabled === "boolean" &&
|
|
878
924
|
(value.condition === undefined || isCondition(value.condition))
|
|
@@ -921,9 +967,10 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
|
|
|
921
967
|
if (!ok) throw new ProtocolError("status result is malformed");
|
|
922
968
|
break;
|
|
923
969
|
}
|
|
924
|
-
case "memoryRead":
|
|
970
|
+
case "memoryRead":
|
|
971
|
+
case "ioRead": {
|
|
925
972
|
if (!isInteger(value.address, 0, 0xffff) || !isHexData(value.data)) {
|
|
926
|
-
throw new ProtocolError(
|
|
973
|
+
throw new ProtocolError(`${op} result is malformed`);
|
|
927
974
|
}
|
|
928
975
|
break;
|
|
929
976
|
}
|
|
@@ -1017,12 +1064,12 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
|
|
|
1017
1064
|
break;
|
|
1018
1065
|
}
|
|
1019
1066
|
case "breakpoint": {
|
|
1020
|
-
const ok = Array.isArray(value.breakpoints) ? value.breakpoints.every(isBreakpoint) : isBreakpoint(value);
|
|
1067
|
+
const ok = Array.isArray(value.breakpoints) ? value.breakpoints.every(isBreakpoint) : isRemoved(value) || isBreakpoint(value);
|
|
1021
1068
|
if (!ok) throw new ProtocolError("breakpoint result is malformed");
|
|
1022
1069
|
break;
|
|
1023
1070
|
}
|
|
1024
1071
|
case "watchpoint": {
|
|
1025
|
-
const ok = Array.isArray(value.watchpoints) ? value.watchpoints.every(isWatchpoint) : isWatchpoint(value);
|
|
1072
|
+
const ok = Array.isArray(value.watchpoints) ? value.watchpoints.every(isWatchpoint) : isRemoved(value) || isWatchpoint(value);
|
|
1026
1073
|
if (!ok) throw new ProtocolError("watchpoint result is malformed");
|
|
1027
1074
|
break;
|
|
1028
1075
|
}
|
|
@@ -1059,6 +1106,11 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
|
|
|
1059
1106
|
if (!ok) throw new ProtocolError("snapshot result is malformed");
|
|
1060
1107
|
break;
|
|
1061
1108
|
}
|
|
1109
|
+
case "snapshotFile": {
|
|
1110
|
+
const ok = (value.exported === true && isInteger(value.bytes, 1, 0xffff_ffff)) || (value.restored === true && isOneOf(RUN_STATES, value.state));
|
|
1111
|
+
if (!ok) throw new ProtocolError("snapshotFile result is malformed");
|
|
1112
|
+
break;
|
|
1113
|
+
}
|
|
1062
1114
|
case "cpuHistory": {
|
|
1063
1115
|
const ok =
|
|
1064
1116
|
Array.isArray(value.entries) &&
|
|
@@ -1111,8 +1163,10 @@ export function validateViceResult<O extends ViceOperation>(op: O, value: unknow
|
|
|
1111
1163
|
isObject(range) &&
|
|
1112
1164
|
isInteger(range.start, 0, 0xffff) &&
|
|
1113
1165
|
isInteger(range.end, 0, 0xffff) &&
|
|
1114
|
-
(
|
|
1115
|
-
const ok = Array.isArray(value.ranges)
|
|
1166
|
+
isInteger(range.flags, 1, 0xff);
|
|
1167
|
+
const ok = Array.isArray(value.ranges)
|
|
1168
|
+
? value.ranges.every(isRange) && (value.next === undefined || isInteger(value.next, 0, 0xffff))
|
|
1169
|
+
: value.cleared === true;
|
|
1116
1170
|
if (!ok) throw new ProtocolError("memmap result is malformed");
|
|
1117
1171
|
break;
|
|
1118
1172
|
}
|
package/src/script.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// a refusal prints {"error":{code,message}} with exit status 1, and a usage error prints the
|
|
3
3
|
// usage text as an invalid-input error with exit status 2.
|
|
4
4
|
|
|
5
|
-
import { WireFailure } from "./protocol/messages.ts";
|
|
5
|
+
import { shownMessage, WireFailure } from "./protocol/messages.ts";
|
|
6
6
|
import { elapsedMs, startTrace, trace, type TraceFields } from "./trace.ts";
|
|
7
7
|
|
|
8
8
|
/** A wrong command line. The message names the problem; an empty message asks for the usage text alone. */
|
|
@@ -38,7 +38,8 @@ export async function runScript(body: () => unknown, options: { usage: string; r
|
|
|
38
38
|
result = await body();
|
|
39
39
|
} catch (error) {
|
|
40
40
|
if ([WireFailure, ...(options.refusals ?? [])].some((refusal) => error instanceof refusal)) {
|
|
41
|
-
const { code
|
|
41
|
+
const { code } = error as Error & { code: string };
|
|
42
|
+
const message = shownMessage(error as Error & { code: string });
|
|
42
43
|
printLine({ error: { code, message } });
|
|
43
44
|
process.exitCode = 1;
|
|
44
45
|
end({ error: { code, message } });
|