@henols/c64-re-tools 2.1.0-rc.1 → 2.1.0
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/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +24 -9
- package/skills/c64-assembler/scripts/assembler.ts +95 -0
- package/skills/c64-basic/SKILL.md +17 -9
- package/skills/c64-basic/scripts/basic.ts +26 -36
- package/skills/c64-disk/SKILL.md +17 -7
- package/skills/c64-disk/scripts/disk.ts +130 -104
- package/skills/c64-disk/scripts/lib/containers.ts +10 -12
- package/skills/c64-docs/SKILL.md +47 -28
- package/skills/c64-docs/references/style.md +6 -6
- package/skills/c64-docs/scripts/docs.ts +323 -0
- package/skills/c64-docs/scripts/lib/source-map.ts +1 -1
- package/skills/c64-emulator/SKILL.md +11 -7
- package/skills/c64-knowledge/SKILL.md +34 -21
- package/skills/c64-knowledge/scripts/knowledge.ts +597 -718
- package/skills/c64-knowledge/scripts/lib/listing.ts +5 -7
- package/skills/c64-memory-map/SKILL.md +18 -10
- package/skills/c64-memory-map/scripts/memory-map.ts +81 -0
- package/skills/c64-provenance/SKILL.md +33 -24
- package/skills/c64-provenance/scripts/lib/compare.ts +8 -12
- package/skills/c64-provenance/scripts/lib/leftovers.ts +19 -20
- package/skills/c64-provenance/scripts/lib/library.ts +15 -25
- package/skills/c64-provenance/scripts/lib/runtime.ts +12 -37
- package/skills/c64-provenance/scripts/lib/separate.ts +9 -10
- package/skills/c64-provenance/scripts/provenance.ts +177 -279
- package/skills/c64-reverse-engineering/SKILL.md +13 -12
- package/skills/c64-source/SKILL.md +28 -16
- package/skills/c64-source/scripts/lib/loader.ts +52 -77
- package/skills/c64-source/scripts/lib/source.ts +33 -42
- package/skills/c64-source/scripts/{generate.ts → source.ts} +68 -109
- package/skills/c64-static-analysis/SKILL.md +24 -13
- package/skills/c64-static-analysis/scripts/lib/coverage.ts +3 -4
- package/skills/c64-static-analysis/scripts/lib/findings.ts +2 -2
- package/skills/c64-static-analysis/scripts/lib/instructions.ts +1 -1
- package/skills/c64-static-analysis/scripts/lib/macros.ts +6 -8
- package/skills/c64-static-analysis/scripts/{analyze.ts → static-analysis.ts} +58 -118
- package/skills/c64-testing/SKILL.md +48 -32
- package/skills/c64-testing/references/recording-format.md +48 -19
- package/skills/c64-testing/references/scenario-format.md +24 -19
- package/skills/c64-testing/scripts/{diff.ts → lib/diff-files.ts} +18 -69
- package/skills/c64-testing/scripts/lib/diff.ts +5 -3
- package/skills/c64-testing/scripts/lib/play.ts +244 -54
- package/skills/c64-testing/scripts/lib/recording.ts +153 -120
- package/skills/c64-testing/scripts/{replay.ts → lib/replay.ts} +28 -86
- package/skills/c64-testing/scripts/{run.ts → lib/runs.ts} +36 -59
- package/skills/c64-testing/scripts/lib/scenario.ts +73 -81
- package/skills/c64-testing/scripts/testing.ts +189 -0
- package/skills/c64-unpacker/SKILL.md +42 -25
- package/skills/c64-unpacker/scripts/lib/evidence.ts +3 -4
- package/skills/c64-unpacker/scripts/lib/loads.ts +12 -10
- package/skills/c64-unpacker/scripts/lib/machine.ts +0 -2
- package/skills/c64-unpacker/scripts/lib/stages.ts +14 -19
- package/skills/c64-unpacker/scripts/{unpack.ts → unpacker.ts} +109 -211
- package/src/c64.ts +35 -20
- package/src/cli/main.ts +2 -2
- package/src/errors.ts +38 -0
- package/src/graphics.ts +3 -1
- package/src/host/server.ts +3 -2
- package/src/host/vice/session.ts +4 -4
- package/src/host-client/entry-state.ts +106 -0
- package/src/host-client/image.ts +21 -10
- package/src/host-client/steps.ts +285 -108
- package/src/host-client/transfer.ts +21 -15
- package/src/host-client/vice-session.ts +1 -1
- package/src/knowledge/database.ts +3 -23
- package/src/knowledge/expression.ts +8 -11
- package/src/knowledge/history.ts +2 -2
- package/src/knowledge/import.ts +13 -14
- package/src/knowledge/names.ts +50 -0
- package/src/knowledge/program.ts +11 -10
- package/src/knowledge/read.ts +16 -21
- package/src/knowledge/schema.ts +9 -3
- package/src/knowledge/text.ts +5 -5
- package/src/knowledge/write.ts +93 -137
- package/src/mcp/inputs.ts +105 -0
- package/src/mcp/output.ts +2 -13
- package/src/mcp/server.ts +12 -60
- package/src/mcp/tools/debug.ts +17 -12
- package/src/mcp/tools/execution.ts +5 -32
- package/src/mcp/tools/input.ts +7 -32
- package/src/mcp/tools/media.ts +6 -5
- package/src/mcp/tools/memory.ts +11 -38
- package/src/mcp/tools/video.ts +6 -5
- package/src/native/c64run.ts +1 -1
- package/src/native/dxa.ts +7 -6
- package/src/native/ghidra/analyze.ts +2 -1
- package/src/native/ghidra/index.ts +2 -1
- package/src/native/processes.ts +4 -3
- package/src/native/status.ts +2 -2
- package/src/native/types.ts +3 -7
- package/src/opcodes.ts +13 -13
- package/src/platform.ts +9 -9
- package/src/project.ts +16 -26
- package/src/protocol/messages.ts +3 -21
- package/src/protocol/vice.ts +7 -7
- package/src/script.ts +491 -49
- package/skills/c64-assembler/scripts/assemble.ts +0 -138
- package/skills/c64-docs/scripts/generate.ts +0 -109
- package/skills/c64-docs/scripts/link.ts +0 -239
- package/skills/c64-memory-map/scripts/memmap.ts +0 -92
- package/skills/c64-testing/scripts/checklist.ts +0 -51
- package/skills/c64-testing/scripts/record.ts +0 -96
- package/skills/c64-testing/scripts/test.ts +0 -65
|
@@ -6,69 +6,20 @@
|
|
|
6
6
|
// writes the memory into the project. "loads" captures each part of a
|
|
7
7
|
// multi-load program. No native unpack tool is used.
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
import { COVERAGE, EXECUTE, formatC64Address, parseC64Address, READ, WRITE } from "#src/c64.ts";
|
|
9
|
+
import { type AddressRange, COVERAGE, EXECUTE, formatC64Address, READ, WRITE } from "#src/c64.ts";
|
|
12
10
|
import { autostartLoaded, LOAD_LIMIT_FRAMES, RUN_LIMIT_FRAMES, typeRun } from "#src/host-client/autostart.ts";
|
|
13
11
|
import { readCoverage, writtenBlocks } from "#src/host-client/coverage.ts";
|
|
14
|
-
import {
|
|
12
|
+
import { resetToReady } from "#src/host-client/entry-state.ts";
|
|
13
|
+
import { readActionSteps, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
|
|
15
14
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
16
|
-
import { readProjectFile,
|
|
15
|
+
import { readProjectFile, writeReported, type Written } from "#src/host-client/transfer.ts";
|
|
17
16
|
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
18
|
-
import {
|
|
19
|
-
import { runScript, UsageError } from "#src/script.ts";
|
|
20
|
-
import { basicStart, inspect,
|
|
17
|
+
import { knowledgeAt, knowledgeNames } from "#src/knowledge/names.ts";
|
|
18
|
+
import { OPTIONS, runScript, UsageError, type Call, type CommandSpec, type OptionSpec } from "#src/script.ts";
|
|
19
|
+
import { basicStart, inspect, readMemoryMap, underRom, writtenThenExecuted } from "./lib/evidence.ts";
|
|
21
20
|
import { loads, readLoadSteps } from "./lib/loads.ts";
|
|
22
|
-
import { callReturn, captureSegments, executedIn, inRanges, mergeMaps, stageCandidates, stagePoints, withoutRanges
|
|
23
|
-
import { entryState, IO_AREAS, json, machineState, readRam,
|
|
24
|
-
|
|
25
|
-
const USAGE = `unpack.ts inspect <program.prg>
|
|
26
|
-
unpack.ts trace <program.prg> [--frames <n>] [--entry <address>] [--steps <file>]
|
|
27
|
-
unpack.ts capture <program.prg> --until <address> --out <dir> [--range <start> <end>] [--exclude <start>-<end>]... [--timeout-frames <n>] [--entry <address>] [--steps <file>] [--min-stage <bytes>]
|
|
28
|
-
unpack.ts stages <program.prg> [--frames <n>] [--entry <address>] [--steps <file>] [--min-stage <bytes>]
|
|
29
|
-
unpack.ts loads <program> --steps <file> --out <dir> [--drive] [--min-block <bytes>]
|
|
30
|
-
|
|
31
|
-
inspect packing evidence: the BASIC start and the entropy of the bytes
|
|
32
|
-
trace run the program for <n> frames (default 3000) and report the memory
|
|
33
|
-
that it wrote and then executed; --entry starts it there instead of RUN
|
|
34
|
-
capture run the program until the CPU gets to <address> after the steps, and write into the
|
|
35
|
-
directory --out: image.bin (64 KiB of RAM), image.json (the entry,
|
|
36
|
-
the segments that the stages wrote, the vectors, the stages), a PRG
|
|
37
|
-
for each segment, and entry_state.json (registers, $00/$01, pages
|
|
38
|
-
0-3, VIC, SID and CIA registers, vectors, and what differs from a
|
|
39
|
-
clean reset). --range gives one segment in place of the found ones.
|
|
40
|
-
--exclude (more than once) leaves a range out of the segments, for
|
|
41
|
-
example bytes that a crack left and the program never reads.
|
|
42
|
-
--entry starts it there instead of RUN.
|
|
43
|
-
|
|
44
|
-
stages run the program for <n> frames (default 3000) and report each stage:
|
|
45
|
-
where it starts, the memory it wrote, and where the next one starts.
|
|
46
|
-
A stage starts when the CPU executes a block of at least
|
|
47
|
-
--min-stage bytes (default 64) that the stage before wrote.
|
|
48
|
-
|
|
49
|
-
loads run a multi-load program (a PRG, or a disk, tape or cartridge image)
|
|
50
|
-
with the steps file; each capture step {"capture": "<name>"} writes
|
|
51
|
-
<dir>/<name>/ with image.bin, image.json, entry_state.json and a PRG
|
|
52
|
-
for each block of at least --min-block bytes (default 64) that the C64
|
|
53
|
-
side changed since the previous capture. --drive also writes the 2 KiB
|
|
54
|
-
drive RAM as drive8.prg, for knowledge space drive8.
|
|
55
|
-
|
|
56
|
-
--steps <file> a JSON list of input steps that run right after the program
|
|
57
|
-
starts: write, registers, joystick, type, frames and runUntil, as in
|
|
58
|
-
c64-testing scenarios, with $ addresses only. Use it to press a key
|
|
59
|
-
in an intro or to get through a menu. stages and capture track the
|
|
60
|
-
stages in frames steps and in runUntil steps with at alone.
|
|
61
|
-
|
|
62
|
-
Addresses are $ and four hex digits. The result is one JSON object.`;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
function address(text: string, option: string): number {
|
|
66
|
-
try {
|
|
67
|
-
return parseC64Address(text);
|
|
68
|
-
} catch {
|
|
69
|
-
throw new UsageError(`${option} must be $ followed by four hex digits, not ${text}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
21
|
+
import { callReturn, captureSegments, executedIn, inRanges, mergeMaps, stageCandidates, stagePoints, withoutRanges } from "./lib/stages.ts";
|
|
22
|
+
import { entryState, IO_AREAS, json, machineState, readRam, vectorsOf } from "./lib/machine.ts";
|
|
72
23
|
|
|
73
24
|
/** Written-then-executed bytes from this many on are unpacking, decrypting or relocating, not self-changing code. */
|
|
74
25
|
const UNPACKED_BYTES = 256;
|
|
@@ -76,26 +27,12 @@ const MAX_ADVANCE = 10_000;
|
|
|
76
27
|
/** The most ranges one memory map read gives. */
|
|
77
28
|
const MAX_RANGES = 1000;
|
|
78
29
|
|
|
79
|
-
/** Reads and checks a steps file: action steps with $ addresses only. */
|
|
80
|
-
function readSteps(path: string): ActionStep[] {
|
|
81
|
-
let value: unknown;
|
|
82
|
-
try {
|
|
83
|
-
value = JSON.parse(readProjectFile(path).bytes.toString("utf8"));
|
|
84
|
-
} catch (error) {
|
|
85
|
-
if (error instanceof WireFailure) throw error;
|
|
86
|
-
throw new UsageError(`--steps ${path} is not JSON: ${(error as Error).message}`);
|
|
87
|
-
}
|
|
88
|
-
return checkActionSteps(value, (message) => {
|
|
89
|
-
throw new UsageError(`--steps ${path}: ${message}`);
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
30
|
/** Runs the steps; the reason when a runUntil step did not reach its target. */
|
|
94
31
|
async function runSteps(client: ViceSessionClient, steps: ActionStep[]): Promise<string | undefined> {
|
|
95
32
|
for (const [index, step] of steps.entries()) {
|
|
96
|
-
const notReached = await runActionStep(client, step,
|
|
33
|
+
const notReached = await runActionStep(client, step, knowledgeAt());
|
|
97
34
|
if (notReached !== undefined && "runUntil" in step) {
|
|
98
|
-
return `Step ${index + 1} did not reach its target in ${step.timeoutFrames} frames. It stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)}.`;
|
|
35
|
+
return `Step ${index + 1} did not reach its target in ${step.runUntil.timeoutFrames} frames. It stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)}.`;
|
|
99
36
|
}
|
|
100
37
|
}
|
|
101
38
|
return undefined;
|
|
@@ -240,9 +177,11 @@ interface Followed {
|
|
|
240
177
|
failed?: string;
|
|
241
178
|
}
|
|
242
179
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
180
|
+
type RunUntilStep = Extract<ActionStep, { runUntil: unknown }>;
|
|
181
|
+
|
|
182
|
+
/** A runUntil step to an address with no condition: a breakpoint stops it, so the stages are tracked while it runs. */
|
|
183
|
+
function trackedRunUntil(step: ActionStep): step is RunUntilStep & { runUntil: { target: { kind: "address" } } } {
|
|
184
|
+
return "runUntil" in step && step.runUntil.target.kind === "address" && step.runUntil.target.condition === undefined;
|
|
246
185
|
}
|
|
247
186
|
|
|
248
187
|
/**
|
|
@@ -281,7 +220,7 @@ async function follow(
|
|
|
281
220
|
let stopAt: number | undefined;
|
|
282
221
|
const atTarget = (pc: number) => stopAt !== undefined && pc === stopAt;
|
|
283
222
|
// The current stage: its memory map since it started, chunk by chunk, its own code, and the written code it calls.
|
|
284
|
-
let stage = { map: new Uint8Array(0x10000), own: new Set<number>(), called: [] as
|
|
223
|
+
let stage = { map: new Uint8Array(0x10000), own: new Set<number>(), called: [] as AddressRange[] };
|
|
285
224
|
const startStage = async (next: number, last: Uint8Array) => {
|
|
286
225
|
mergeMaps(stage.map, last);
|
|
287
226
|
found.push({ entry, firstFrame, frames: total - firstFrame, wrote: writtenBlocks(stage.map, options.minStage), next });
|
|
@@ -296,9 +235,9 @@ async function follow(
|
|
|
296
235
|
* blocks, and stops at the first opcode that the stage wrote and the CPU
|
|
297
236
|
* then executes. Own code stops the run once; a write watch arms it again.
|
|
298
237
|
*/
|
|
299
|
-
const rerun = async (candidates:
|
|
238
|
+
const rerun = async (candidates: AddressRange[], frames: number): Promise<{ used: number; map: Uint8Array; next?: number; atTarget: boolean }> => {
|
|
300
239
|
const points = new Map<string, { kind: "exec" | "watch"; id: number }>();
|
|
301
|
-
const arm = async (wanted: { exec:
|
|
240
|
+
const arm = async (wanted: { exec: AddressRange[]; watch: AddressRange[] }) => {
|
|
302
241
|
const keys = new Set([...wanted.exec.map((range) => `exec:${range.start}:${range.end}`), ...wanted.watch.map((range) => `watch:${range.start}:${range.end}`)]);
|
|
303
242
|
for (const [key, point] of points) {
|
|
304
243
|
if (keys.has(key)) continue;
|
|
@@ -337,7 +276,7 @@ async function follow(
|
|
|
337
276
|
return undefined;
|
|
338
277
|
} finally {
|
|
339
278
|
await client.breakpoint({ action: "remove", id: point.id });
|
|
340
|
-
await client.snapshot({ action: "
|
|
279
|
+
await client.snapshot({ action: "remove", name: CALL_SNAPSHOT });
|
|
341
280
|
}
|
|
342
281
|
};
|
|
343
282
|
let left = frames;
|
|
@@ -448,17 +387,17 @@ async function follow(
|
|
|
448
387
|
for (const [index, step] of options.steps.entries()) {
|
|
449
388
|
if ("frames" in step) await track(step.frames);
|
|
450
389
|
else if (trackedRunUntil(step)) {
|
|
451
|
-
if (!(await trackTo(
|
|
390
|
+
if (!(await trackTo(knowledgeAt()(step.runUntil.target.address), step.runUntil.timeoutFrames))) {
|
|
452
391
|
const { pc } = await client.registersGet("c64");
|
|
453
|
-
return { stages: found, frames: total, reached: false, failed: `Step ${index + 1} did not reach its target in ${step.timeoutFrames} frames. It is at ${formatC64Address(pc)}.` };
|
|
392
|
+
return { stages: found, frames: total, reached: false, failed: `Step ${index + 1} did not reach its target in ${step.runUntil.timeoutFrames} frames. It is at ${formatC64Address(pc)}.` };
|
|
454
393
|
}
|
|
455
|
-
} else await runActionStep(client, step,
|
|
394
|
+
} else await runActionStep(client, step, knowledgeAt());
|
|
456
395
|
}
|
|
457
396
|
const left = Math.max(0, options.frames - total);
|
|
458
397
|
let reached = false;
|
|
459
398
|
if (options.until === undefined) await track(left);
|
|
460
399
|
else reached = await trackTo(options.until, left);
|
|
461
|
-
await client.snapshot({ action: "
|
|
400
|
+
await client.snapshot({ action: "remove", name: STAGE_SNAPSHOT }).catch(() => undefined);
|
|
462
401
|
mergeMaps(stage.map, await readCoverage(client));
|
|
463
402
|
found.push({ entry, firstFrame, frames: total - firstFrame, wrote: writtenBlocks(stage.map, options.minStage) });
|
|
464
403
|
return { stages: found, frames: total, reached };
|
|
@@ -494,10 +433,9 @@ async function stages(program: string, options: { frames: number; entry?: number
|
|
|
494
433
|
}
|
|
495
434
|
}
|
|
496
435
|
|
|
497
|
-
|
|
498
436
|
async function capture(
|
|
499
437
|
program: string,
|
|
500
|
-
options: { until: number; out: string; range?:
|
|
438
|
+
options: { until: number; out: string; range?: AddressRange; exclude: AddressRange[]; timeoutFrames: number; entry?: number; steps: ActionStep[]; minStage: number },
|
|
501
439
|
): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
502
440
|
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
503
441
|
try {
|
|
@@ -514,14 +452,15 @@ async function capture(
|
|
|
514
452
|
const memory = await readRam(client, 0x0000, 0x10000);
|
|
515
453
|
const state = await machineState(client);
|
|
516
454
|
// The segments: what the stages wrote, without loader leftovers, with no overlap.
|
|
517
|
-
const found = options.range !== undefined ? [
|
|
455
|
+
const found = options.range !== undefined ? [options.range] : captureSegments(followed.stages, registers.pc);
|
|
518
456
|
const inside = options.exclude.find((range) => registers.pc >= range.start && registers.pc <= range.end);
|
|
519
457
|
if (inside !== undefined) {
|
|
520
458
|
return { output: { captured: false, reason: `The entry ${formatC64Address(registers.pc)} is in --exclude ${formatC64Address(inside.start)}-${formatC64Address(inside.end)}. A segment must hold the entry.` }, failed: true };
|
|
521
459
|
}
|
|
522
460
|
const segments = withoutRanges(found, options.exclude);
|
|
523
461
|
const file = (path: string) => `${options.out}/${path}`;
|
|
524
|
-
const
|
|
462
|
+
const written: Written[] = [];
|
|
463
|
+
const write = (path: string, bytes: Uint8Array) => written.push(writeReported(file(path), bytes, { what: "--out" }));
|
|
525
464
|
write("image.bin", memory);
|
|
526
465
|
const files = segments.map((segment) => {
|
|
527
466
|
const name = `segment-${segment.start.toString(16).padStart(4, "0")}-${segment.end.toString(16).padStart(4, "0")}.prg`;
|
|
@@ -547,8 +486,7 @@ async function capture(
|
|
|
547
486
|
),
|
|
548
487
|
);
|
|
549
488
|
// A clean machine at the READY prompt shows what the loader and the stages changed.
|
|
550
|
-
await client
|
|
551
|
-
await client.execution({ action: "advance-frames", count: READY_FRAMES, space: "c64" });
|
|
489
|
+
await resetToReady(client);
|
|
552
490
|
const clean = await machineState(client);
|
|
553
491
|
const differs: Array<{ address: string; entry: number; reset: number }> = [];
|
|
554
492
|
const compare = (base: number, entry: Buffer, reset: Buffer) => entry.forEach((value, offset) => value !== reset[offset] && differs.push({ address: formatC64Address(base + offset), entry: value, reset: reset[offset]! }));
|
|
@@ -561,7 +499,7 @@ async function capture(
|
|
|
561
499
|
captured: true,
|
|
562
500
|
pc: formatC64Address(registers.pc),
|
|
563
501
|
out: options.out,
|
|
564
|
-
files:
|
|
502
|
+
files: written,
|
|
565
503
|
segments: segments.map(showRange),
|
|
566
504
|
...(options.exclude.length === 0 ? {} : { excluded: options.exclude.map(showRange) }),
|
|
567
505
|
stages: followed.stages.length,
|
|
@@ -574,127 +512,87 @@ async function capture(
|
|
|
574
512
|
}
|
|
575
513
|
}
|
|
576
514
|
|
|
577
|
-
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
515
|
+
const PROGRAM = { name: "<program>", help: "a PRG file, relative to the project folder" };
|
|
516
|
+
const FRAMES: OptionSpec = { ...OPTIONS.frames, max: 30_000, default: 3000, help: "run the program for n frames (default 3000)" };
|
|
517
|
+
const ENTRY: OptionSpec = { ...OPTIONS.entry, help: "start the program here in place of RUN" };
|
|
518
|
+
const STEPS: OptionSpec = { ...OPTIONS.steps, help: "action steps (JSON) that run right after the program starts, for example to press a key in an intro or to get through a menu" };
|
|
519
|
+
const MIN_STAGE: OptionSpec = { kind: "count", value: "<bytes>", min: 1, max: 0x10000, default: 64, help: "a stage starts when the CPU executes a block of at least this many bytes that the stage before wrote (default 64)" };
|
|
520
|
+
|
|
521
|
+
/** The result of a command: PASS, or FAIL with the reason. */
|
|
522
|
+
const outcome = ({ output, failed }: { output: unknown; failed: boolean }) => ({ result: failed ? "FAIL" : "PASS", ...(output as Record<string, unknown>) });
|
|
523
|
+
const entryOf = (call: Call) => {
|
|
524
|
+
const entry = call.address("entry");
|
|
525
|
+
return entry === undefined ? {} : { entry };
|
|
526
|
+
};
|
|
527
|
+
const stepsOf = (call: Call) => {
|
|
528
|
+
const path = call.string("steps");
|
|
529
|
+
return path === undefined ? [] : readActionSteps(path, "--steps");
|
|
584
530
|
};
|
|
585
531
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
532
|
+
const COMMANDS: CommandSpec[] = [
|
|
533
|
+
{
|
|
534
|
+
name: "inspect",
|
|
535
|
+
summary: "Packing evidence: the BASIC start and the entropy of the bytes.",
|
|
536
|
+
subjects: [PROGRAM],
|
|
537
|
+
run: (call) => inspect(readProjectFile(call.subject(0)).bytes),
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
name: "trace",
|
|
541
|
+
summary: "Runs the program and reports the memory that it wrote and then executed.",
|
|
542
|
+
subjects: [PROGRAM],
|
|
543
|
+
options: { frames: FRAMES, entry: ENTRY, steps: STEPS },
|
|
544
|
+
run: async (call) => outcome(await trace(call.subject(0), { frames: call.count("frames")!, steps: stepsOf(call), ...entryOf(call) })),
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
name: "stages",
|
|
548
|
+
summary: "Runs the program and reports each stage: where it starts, the memory it wrote, and where the next one starts.",
|
|
549
|
+
subjects: [PROGRAM],
|
|
550
|
+
options: { frames: FRAMES, entry: ENTRY, steps: STEPS, "min-stage": MIN_STAGE },
|
|
551
|
+
notes: "stages tracks the stages in frames steps and in runUntil steps to an address target without a condition.",
|
|
552
|
+
run: async (call) => {
|
|
553
|
+
const steps = stepsOf(call);
|
|
554
|
+
// A runUntil step to a raster line or a memory value cannot be tracked in chunks of frames; capture takes it.
|
|
555
|
+
if (steps.some((step) => "runUntil" in step && !trackedRunUntil(step))) throw new UsageError("stages takes a runUntil step only with an address target without a condition. Use frames steps, or capture");
|
|
556
|
+
return outcome(await stages(call.subject(0), { frames: call.count("frames")!, steps, minStage: call.count("min-stage")!, ...entryOf(call) }));
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
name: "capture",
|
|
561
|
+
summary: "Runs the program until the CPU gets to --until after the steps, and writes into --out: image.bin (64 KiB of RAM), image.json (the entry, the segments that the stages wrote, the vectors, the stages), a PRG for each segment, and entry_state.json.",
|
|
562
|
+
subjects: [PROGRAM],
|
|
591
563
|
options: {
|
|
592
|
-
until: {
|
|
593
|
-
out: {
|
|
594
|
-
range: {
|
|
595
|
-
exclude: {
|
|
596
|
-
"timeout-frames": {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
"min-stage": { type: "string" },
|
|
601
|
-
"min-block": { type: "string" },
|
|
602
|
-
drive: { type: "boolean" },
|
|
603
|
-
help: { type: "boolean" },
|
|
564
|
+
until: { kind: "address", value: "<address>", required: true, help: "the unpacked entry point" },
|
|
565
|
+
out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the folder for the capture, relative to the project folder" },
|
|
566
|
+
range: { ...OPTIONS.range, help: "one segment in place of the found ones" },
|
|
567
|
+
exclude: { kind: "range", value: "<start>-<end>", multiple: true, help: "a range to leave out of the segments, for example bytes that a crack left and the program never reads" },
|
|
568
|
+
"timeout-frames": { ...OPTIONS["timeout-frames"], max: 30_000, default: 3000, help: "stop the wait for --until after n frames (default 3000)" },
|
|
569
|
+
entry: ENTRY,
|
|
570
|
+
steps: STEPS,
|
|
571
|
+
"min-stage": MIN_STAGE,
|
|
604
572
|
},
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
if (command === "trace") {
|
|
625
|
-
if (rest.length > 0) throw new UsageError("trace takes one program");
|
|
626
|
-
const frames = values.frames === undefined ? 3000 : Number(values.frames);
|
|
627
|
-
if (!Number.isInteger(frames) || frames < 1 || frames > 30_000) throw new UsageError("--frames must be from 1 to 30000");
|
|
628
|
-
const steps = values.steps === undefined ? [] : readSteps(values.steps);
|
|
629
|
-
return trace(program, { frames, steps, ...(values.entry === undefined ? {} : { entry: address(values.entry, "--entry") }) });
|
|
630
|
-
}
|
|
631
|
-
if (command === "stages") {
|
|
632
|
-
if (rest.length > 0) throw new UsageError("stages takes one program");
|
|
633
|
-
const frames = values.frames === undefined ? 3000 : Number(values.frames);
|
|
634
|
-
if (!Number.isInteger(frames) || frames < 1 || frames > 30_000) throw new UsageError("--frames must be from 1 to 30000");
|
|
635
|
-
const minStage = values["min-stage"] === undefined ? 64 : Number(values["min-stage"]);
|
|
636
|
-
if (!Number.isInteger(minStage) || minStage < 1 || minStage > 0x10000) throw new UsageError("--min-stage must be from 1 to 65536");
|
|
637
|
-
const steps = values.steps === undefined ? [] : readSteps(values.steps);
|
|
638
|
-
// A runUntil step to a raster line or a memory value cannot be tracked in chunks of frames; capture takes it.
|
|
639
|
-
if (steps.some((step) => "runUntil" in step && !trackedRunUntil(step))) throw new UsageError("stages takes a runUntil step only with at alone; use frames steps or runUntil at");
|
|
640
|
-
return stages(program, { frames, steps, minStage, ...(values.entry === undefined ? {} : { entry: address(values.entry, "--entry") }) });
|
|
641
|
-
}
|
|
642
|
-
if (command === "loads") {
|
|
643
|
-
if (rest.length > 0) throw new UsageError("loads takes one program");
|
|
644
|
-
if (values.steps === undefined || values.out === undefined) throw new UsageError("loads needs --steps and --out");
|
|
645
|
-
try {
|
|
646
|
-
resolveProjectPath(values.out);
|
|
647
|
-
} catch {
|
|
648
|
-
throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
|
|
649
|
-
}
|
|
650
|
-
const minBlock = values["min-block"] === undefined ? 64 : Number(values["min-block"]);
|
|
651
|
-
if (!Number.isInteger(minBlock) || minBlock < 1 || minBlock > 0x10000) throw new UsageError("--min-block must be from 1 to 65536");
|
|
652
|
-
return loads(program, { out: values.out, steps: readLoadSteps(values.steps), drive: values.drive === true, minBlock });
|
|
653
|
-
}
|
|
654
|
-
if (values.until === undefined || values.out === undefined) throw new UsageError("capture needs --until and --out");
|
|
655
|
-
// --range takes two values: the end is the next positional argument.
|
|
656
|
-
let range: [number, number] | undefined;
|
|
657
|
-
if (values.range !== undefined) {
|
|
658
|
-
if (rest.length !== 1) throw new UsageError("--range takes a start and an end address");
|
|
659
|
-
range = [address(values.range, "--range"), address(rest[0]!, "--range")];
|
|
660
|
-
if (range[1] < range[0]) throw new UsageError("the --range end must not be before its start");
|
|
661
|
-
} else if (rest.length > 0) {
|
|
662
|
-
throw new UsageError("too many arguments");
|
|
663
|
-
}
|
|
664
|
-
const exclude = (values.exclude ?? []).map((value) => {
|
|
665
|
-
const parts = value.split("-");
|
|
666
|
-
if (parts.length !== 2) throw new UsageError(`--exclude takes <start>-<end>, not ${value}`);
|
|
667
|
-
const range = { start: address(parts[0]!, "--exclude"), end: address(parts[1]!, "--exclude") };
|
|
668
|
-
if (range.end < range.start) throw new UsageError(`the --exclude end must not be before its start: ${value}`);
|
|
669
|
-
return range;
|
|
670
|
-
});
|
|
671
|
-
try {
|
|
672
|
-
resolveProjectPath(values.out);
|
|
673
|
-
} catch {
|
|
674
|
-
throw new UsageError(`--out must be a path relative to the project folder that stays inside it, not ${values.out}`);
|
|
675
|
-
}
|
|
676
|
-
const timeoutFrames = values["timeout-frames"] === undefined ? 3000 : Number(values["timeout-frames"]);
|
|
677
|
-
if (!Number.isInteger(timeoutFrames) || timeoutFrames < 1 || timeoutFrames > 30_000) throw new UsageError("--timeout-frames must be from 1 to 30000");
|
|
678
|
-
const steps = values.steps === undefined ? [] : readSteps(values.steps);
|
|
679
|
-
const minStage = values["min-stage"] === undefined ? 64 : Number(values["min-stage"]);
|
|
680
|
-
if (!Number.isInteger(minStage) || minStage < 1 || minStage > 0x10000) throw new UsageError("--min-stage must be from 1 to 65536");
|
|
681
|
-
return capture(program, {
|
|
682
|
-
until: address(values.until, "--until"),
|
|
683
|
-
out: values.out,
|
|
684
|
-
...(range === undefined ? {} : { range }),
|
|
685
|
-
exclude,
|
|
686
|
-
timeoutFrames,
|
|
687
|
-
steps,
|
|
688
|
-
minStage,
|
|
689
|
-
...(values.entry === undefined ? {} : { entry: address(values.entry, "--entry") }),
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
await runScript(
|
|
694
|
-
async (argv) => {
|
|
695
|
-
const { output, failed } = await run(argv);
|
|
696
|
-
if (failed) process.exitCode = 1;
|
|
697
|
-
return output;
|
|
573
|
+
notes: "entry_state.json has the registers, $00/$01, pages 0-3, the VIC, SID and CIA registers, the vectors, and what differs from a clean reset.",
|
|
574
|
+
run: async (call) => {
|
|
575
|
+
const range = call.range("range");
|
|
576
|
+
return outcome(await capture(call.subject(0), { until: call.address("until")!, out: call.string("out")!, ...(range === undefined ? {} : { range }), exclude: call.ranges("exclude"), timeoutFrames: call.count("timeout-frames")!, steps: stepsOf(call), minStage: call.count("min-stage")!, ...entryOf(call) }));
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
name: "loads",
|
|
581
|
+
summary: "Runs a multi-load program with the steps file. Each capture step {\"capture\": \"<name>\"} writes <dir>/<name>/ with image.bin, image.json, entry_state.json and a PRG for each block that the C64 side changed since the previous capture.",
|
|
582
|
+
subjects: [{ name: "<program>", help: "a PRG, or a disk, tape or cartridge image, relative to the project folder" }],
|
|
583
|
+
options: {
|
|
584
|
+
steps: { ...STEPS, required: true, help: "action steps and capture steps (JSON)" },
|
|
585
|
+
out: { ...OPTIONS.out, value: "<dir>", required: true, help: "the folder for the captures, relative to the project folder" },
|
|
586
|
+
drive: { kind: "flag", help: "also write the 2 KiB drive RAM as drive8.prg, for knowledge space drive8" },
|
|
587
|
+
"min-block": { kind: "count", value: "<bytes>", min: 1, max: 0x10000, default: 64, help: "write a changed block of at least this many bytes (default 64)" },
|
|
588
|
+
},
|
|
589
|
+
run: async (call) => outcome(await loads(call.subject(0), { out: call.string("out")!, steps: readLoadSteps(call.string("steps")!), drive: call.flag("drive"), minBlock: call.count("min-block")! })),
|
|
698
590
|
},
|
|
699
|
-
|
|
700
|
-
|
|
591
|
+
];
|
|
592
|
+
|
|
593
|
+
await runScript({
|
|
594
|
+
name: "unpacker",
|
|
595
|
+
summary: "Packing evidence of a program, and captures of the program after it unpacked itself. It never names a packer and runs no native unpack tool.",
|
|
596
|
+
names: knowledgeNames,
|
|
597
|
+
commands: COMMANDS,
|
|
598
|
+
});
|
package/src/c64.ts
CHANGED
|
@@ -1,25 +1,40 @@
|
|
|
1
|
-
|
|
1
|
+
import { Refusal } from "./errors.ts";
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/** A C64 address as text: $ and 1 to 4 hex digits, either case. */
|
|
4
|
+
const ADDRESS_PATTERN = /^\$([0-9a-fA-F]{1,4})$/;
|
|
5
|
+
|
|
6
|
+
/** The address that the text gives ($ and 1 to 4 hex digits), or undefined. */
|
|
7
|
+
export function c64AddressOf(text: string): number | undefined {
|
|
8
|
+
const match = ADDRESS_PATTERN.exec(text);
|
|
9
|
+
return match === null ? undefined : Number.parseInt(match[1]!, 16);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Returns the value when it is a C64 address (0-$ffff). Refuses it otherwise. */
|
|
4
13
|
function checkAddress(value: number): number {
|
|
5
|
-
if (!Number.isInteger(value) || value < 0 || value > 0xffff) {
|
|
6
|
-
throw new RangeError(`C64 address out of range: ${value}`);
|
|
7
|
-
}
|
|
14
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xffff) throw new Refusal("invalid-input", `${value} is not a C64 address: an address is from $0000 to $ffff.`);
|
|
8
15
|
return value;
|
|
9
16
|
}
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return checkAddress(Number.parseInt(match[1]!, 16));
|
|
18
|
+
/** The address of $ and 1 to 4 hex digits. Refuses other text. */
|
|
19
|
+
export function parseC64Address(text: string): number {
|
|
20
|
+
const value = c64AddressOf(text);
|
|
21
|
+
if (value === undefined) throw new Refusal("invalid-input", `${JSON.stringify(text)} is not a C64 address: give $ and 1 to 4 hex digits, for example $c000 or $fb.`);
|
|
22
|
+
return value;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
export function formatC64Address(value: number): string {
|
|
20
26
|
return `$${checkAddress(value).toString(16).padStart(4, "0")}`;
|
|
21
27
|
}
|
|
22
28
|
|
|
29
|
+
/** An inclusive span of C64 addresses. */
|
|
30
|
+
export interface AddressRange {
|
|
31
|
+
start: number;
|
|
32
|
+
end: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The knowledge spaces: the C64, the 1541 drive in unit 8, and cartridge banks 0 to 255. */
|
|
36
|
+
export const SPACE_PATTERN = /^(c64|drive8|bank(0|[1-9][0-9]?|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$/;
|
|
37
|
+
|
|
23
38
|
/** VIC-II state with its addresses as $xxxx; the other values stay numbers. */
|
|
24
39
|
export function formatVicii<T extends { screenAddress: number; graphicsAddress: number }>(state: T): Omit<T, "screenAddress" | "graphicsAddress"> & { screenAddress: string; graphicsAddress: string } {
|
|
25
40
|
return { ...state, screenAddress: formatC64Address(state.screenAddress), graphicsAddress: formatC64Address(state.graphicsAddress) };
|
|
@@ -30,20 +45,20 @@ export function formatSprite<T extends { dataAddress: number }>(sprite: T): Omit
|
|
|
30
45
|
return { ...sprite, dataAddress: formatC64Address(sprite.dataAddress) };
|
|
31
46
|
}
|
|
32
47
|
|
|
33
|
-
/** A byte as $xx.
|
|
48
|
+
/** A byte as $xx. Refuses a value outside 0-255. */
|
|
34
49
|
export function formatC64Byte(value: number): string {
|
|
35
|
-
if (!Number.isInteger(value) || value < 0 || value > 0xff) throw new
|
|
50
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xff) throw new Refusal("invalid-input", `${value} is not a byte: a byte is from 0 to 255.`);
|
|
36
51
|
return `$${value.toString(16).padStart(2, "0")}`;
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
/**
|
|
40
55
|
* The memory a PRG loads into: the 2-byte load address, then the body.
|
|
41
|
-
*
|
|
56
|
+
* Refuses a file shorter than 3 bytes or one that runs past $ffff.
|
|
42
57
|
*/
|
|
43
58
|
export function prgRange(prg: Uint8Array): { start: number; end: number; body: Uint8Array } {
|
|
44
|
-
if (prg.length < 3) throw new
|
|
59
|
+
if (prg.length < 3) throw new Refusal("invalid-input", "A PRG has a 2-byte load address and at least one byte.");
|
|
45
60
|
const start = prg[0]! | (prg[1]! << 8);
|
|
46
|
-
if (start + prg.length - 2 > 0x10000) throw new
|
|
61
|
+
if (start + prg.length - 2 > 0x10000) throw new Refusal("invalid-input", "The PRG runs past $ffff.");
|
|
47
62
|
return { start, end: start + prg.length - 3, body: prg.subarray(2) };
|
|
48
63
|
}
|
|
49
64
|
|
|
@@ -86,7 +101,7 @@ export function d018Offsets(d018: number): { screen: number; characters: number;
|
|
|
86
101
|
* Converts text to the PETSCII bytes the C64 keyboard types after a reset
|
|
87
102
|
* (upper case/graphics mode). Letters of either case type the unshifted key,
|
|
88
103
|
* which shows as an upper-case letter. "\n" and "\r\n" type RETURN.
|
|
89
|
-
*
|
|
104
|
+
* Refuses the text with the first character that has no key.
|
|
90
105
|
*/
|
|
91
106
|
export function textToPetscii(text: string): Uint8Array {
|
|
92
107
|
const bytes: number[] = [];
|
|
@@ -100,7 +115,7 @@ export function textToPetscii(text: string): Uint8Array {
|
|
|
100
115
|
else if (character === "£") bytes.push(0x5c);
|
|
101
116
|
else if (character === "↑" || character === "^") bytes.push(0x5e);
|
|
102
117
|
else if (character === "←") bytes.push(0x5f);
|
|
103
|
-
else throw new
|
|
118
|
+
else throw new Refusal("invalid-input", `There is no C64 key for ${JSON.stringify(character)}`);
|
|
104
119
|
}
|
|
105
120
|
return Uint8Array.from(bytes);
|
|
106
121
|
}
|
|
@@ -125,7 +140,7 @@ export function petsciiNameToText(bytes: Uint8Array): string {
|
|
|
125
140
|
|
|
126
141
|
/**
|
|
127
142
|
* The reverse of petsciiNameToText. Letters of either case are the unshifted
|
|
128
|
-
* letters ($41-$5a).
|
|
143
|
+
* letters ($41-$5a). Refuses text that is no name.
|
|
129
144
|
*/
|
|
130
145
|
export function textToPetsciiName(text: string): Uint8Array {
|
|
131
146
|
const bytes: number[] = [];
|
|
@@ -144,7 +159,7 @@ export function textToPetsciiName(text: string): Uint8Array {
|
|
|
144
159
|
else if (character === "↑") bytes.push(0x5e);
|
|
145
160
|
else if (character === "←") bytes.push(0x5f);
|
|
146
161
|
else if (code >= 0x20 && code <= 0x5d) bytes.push(code);
|
|
147
|
-
else throw new
|
|
162
|
+
else throw new Refusal("invalid-input", `${JSON.stringify(character)} is not a name character. Write other bytes as {$xx}.`);
|
|
148
163
|
index++;
|
|
149
164
|
}
|
|
150
165
|
return Uint8Array.from(bytes);
|
package/src/cli/main.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { parseArgs } from "node:util";
|
|
|
10
10
|
import { oldNodeRefusal, PACKAGE } from "../package.ts";
|
|
11
11
|
import { localToolContext } from "../native/local.ts";
|
|
12
12
|
import { localToolStatus } from "../native/status.ts";
|
|
13
|
-
import {
|
|
13
|
+
import { isRefusal } from "../errors.ts";
|
|
14
14
|
import { startTrace } from "../trace.ts";
|
|
15
15
|
import { runApSdk } from "./ap-sdk.ts";
|
|
16
16
|
import { installedItems, parseTargets, tidyAfterUninstall, undoWindsurfProjectMcp, windsurfBefore, withoutWindsurfMcp, type Scope } from "./cleanup.ts";
|
|
@@ -124,7 +124,7 @@ try {
|
|
|
124
124
|
if ((error as NodeJS.ErrnoException).code?.startsWith("ERR_PARSE_ARGS")) {
|
|
125
125
|
console.error(`${(error as Error).message}\n\n${HELP}`);
|
|
126
126
|
process.exitCode = 2;
|
|
127
|
-
} else if (error
|
|
127
|
+
} else if (isRefusal(error)) {
|
|
128
128
|
// A refusal by name, for example AP SDK that did not finish in time.
|
|
129
129
|
console.error(error.message);
|
|
130
130
|
process.exitCode = 1;
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// The one list of error codes and the one refusal class. Every part of the
|
|
2
|
+
// repository may import this file: the scripts, the MCP server, the host and
|
|
3
|
+
// the knowledge all report a failure as a code from this list and a message.
|
|
4
|
+
|
|
5
|
+
export const ERROR_CODES = [
|
|
6
|
+
"invalid-input",
|
|
7
|
+
"machine-running",
|
|
8
|
+
"machine-unavailable",
|
|
9
|
+
"machine-state-lost",
|
|
10
|
+
"not-found",
|
|
11
|
+
"media-error",
|
|
12
|
+
"limit-exceeded",
|
|
13
|
+
"unsupported-in-space",
|
|
14
|
+
"installation-incomplete",
|
|
15
|
+
"operation-failed",
|
|
16
|
+
"conflict",
|
|
17
|
+
"stale-revision",
|
|
18
|
+
"invalid-database",
|
|
19
|
+
"unsupported-schema",
|
|
20
|
+
] as const;
|
|
21
|
+
export type ErrorCode = (typeof ERROR_CODES)[number];
|
|
22
|
+
|
|
23
|
+
/** A refused operation: a stable code and a message that tells what is wrong and what to do. */
|
|
24
|
+
export class Refusal extends Error {
|
|
25
|
+
override name = "Refusal";
|
|
26
|
+
readonly code: ErrorCode;
|
|
27
|
+
|
|
28
|
+
constructor(code: ErrorCode, message: string) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.code = code;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** True for an Error with a code from ERROR_CODES: a refusal, whatever its class. */
|
|
35
|
+
export function isRefusal(error: unknown): error is Error & { code: ErrorCode } {
|
|
36
|
+
const code = (error as { code?: unknown } | undefined)?.code;
|
|
37
|
+
return error instanceof Error && typeof code === "string" && (ERROR_CODES as readonly string[]).includes(code);
|
|
38
|
+
}
|
package/src/graphics.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import { crc32, deflateSync } from "node:zlib";
|
|
7
7
|
|
|
8
|
+
import { Refusal } from "./errors.ts";
|
|
9
|
+
|
|
8
10
|
export interface IndexedFrame {
|
|
9
11
|
width: number;
|
|
10
12
|
height: number;
|
|
@@ -26,7 +28,7 @@ function chunk(type: string, data: Uint8Array): Buffer {
|
|
|
26
28
|
/** Encodes an indexed frame as an 8-bit palette PNG. */
|
|
27
29
|
export function encodePng(frame: IndexedFrame, palette: ReadonlyArray<Rgb>): Buffer {
|
|
28
30
|
for (const index of frame.pixels) {
|
|
29
|
-
if (index >= palette.length) throw new
|
|
31
|
+
if (index >= palette.length) throw new Refusal("operation-failed", `pixel colour ${index} is not in the ${palette.length}-colour palette`);
|
|
30
32
|
}
|
|
31
33
|
const header = Buffer.alloc(13);
|
|
32
34
|
header.writeUInt32BE(frame.width, 0);
|