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

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 (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -37
  3. package/distribution/package.ts +21 -0
  4. package/distribution/plugin.ts +122 -0
  5. package/package.json +34 -40
  6. package/skills/c64-assembler/SKILL.md +76 -0
  7. package/skills/c64-assembler/scripts/assemble.ts +110 -0
  8. package/skills/c64-assembler/scripts/compare.ts +84 -0
  9. package/skills/c64-basic/SKILL.md +66 -0
  10. package/skills/c64-basic/scripts/basic.ts +46 -0
  11. package/skills/c64-disk/SKILL.md +93 -0
  12. package/skills/c64-disk/scripts/containers.ts +124 -0
  13. package/skills/c64-disk/scripts/disk.ts +120 -0
  14. package/skills/c64-emulator/SKILL.md +134 -0
  15. package/skills/c64-knowledge/SKILL.md +166 -0
  16. package/skills/c64-knowledge/scripts/knowledge.ts +492 -0
  17. package/skills/c64-memory-map/SKILL.md +79 -0
  18. package/skills/c64-memory-map/scripts/memmap.ts +92 -0
  19. package/skills/c64-provenance/SKILL.md +110 -0
  20. package/skills/c64-provenance/scripts/compare.ts +152 -0
  21. package/skills/c64-provenance/scripts/provenance.ts +334 -0
  22. package/skills/c64-provenance/scripts/runtime.ts +191 -0
  23. package/skills/c64-provenance/scripts/separate.ts +140 -0
  24. package/skills/c64-reverse-engineering/SKILL.md +144 -0
  25. package/skills/c64-source/SKILL.md +126 -0
  26. package/skills/c64-source/scripts/generate.ts +490 -0
  27. package/skills/c64-source/scripts/loader.ts +571 -0
  28. package/skills/c64-source/scripts/opcodes.ts +61 -0
  29. package/skills/c64-source/scripts/source.ts +595 -0
  30. package/skills/c64-static-analysis/SKILL.md +119 -0
  31. package/skills/c64-static-analysis/scripts/analyze.ts +275 -0
  32. package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
  33. package/skills/c64-static-analysis/scripts/findings.ts +75 -0
  34. package/skills/c64-static-analysis/scripts/idioms.ts +149 -0
  35. package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
  36. package/skills/c64-testing/SKILL.md +103 -0
  37. package/skills/c64-testing/references/scenario-format.md +116 -0
  38. package/skills/c64-testing/scripts/checklist.ts +51 -0
  39. package/skills/c64-testing/scripts/run.ts +189 -0
  40. package/skills/c64-testing/scripts/scenario.ts +493 -0
  41. package/skills/c64-testing/scripts/test.ts +65 -0
  42. package/skills/c64-unpacker/SKILL.md +135 -0
  43. package/skills/c64-unpacker/scripts/evidence.ts +207 -0
  44. package/skills/c64-unpacker/scripts/loads.ts +209 -0
  45. package/skills/c64-unpacker/scripts/machine.ts +80 -0
  46. package/skills/c64-unpacker/scripts/stages.ts +130 -0
  47. package/skills/c64-unpacker/scripts/unpack.ts +683 -0
  48. package/src/c64.ts +411 -0
  49. package/src/cli/ap-sdk.ts +44 -0
  50. package/src/cli/cleanup.ts +194 -0
  51. package/src/cli/main.ts +125 -0
  52. package/src/cli/status.ts +61 -0
  53. package/src/host/main.ts +134 -0
  54. package/src/host/server.ts +392 -0
  55. package/src/host/tools/c1541.ts +284 -0
  56. package/src/host/tools/index.ts +35 -0
  57. package/src/host/tools/petcat.ts +77 -0
  58. package/src/host/tools/status.ts +47 -0
  59. package/src/host/tools/version.ts +41 -0
  60. package/src/host/vice/adapter.ts +804 -0
  61. package/src/host/vice/binary-monitor.ts +389 -0
  62. package/src/host/vice/chips.ts +118 -0
  63. package/src/host/vice/connect.ts +34 -0
  64. package/src/host/vice/process.ts +413 -0
  65. package/src/host/vice/screen.ts +184 -0
  66. package/src/host/vice/session.ts +1426 -0
  67. package/src/host/vice/text-monitor.ts +223 -0
  68. package/src/host-client/autostart.ts +55 -0
  69. package/src/host-client/connect.ts +241 -0
  70. package/src/host-client/coverage.ts +63 -0
  71. package/src/host-client/steps.ts +170 -0
  72. package/src/host-client/tools.ts +89 -0
  73. package/src/host-client/transfer.ts +126 -0
  74. package/src/host-client/vice-session.ts +316 -0
  75. package/src/knowledge/database.ts +132 -0
  76. package/src/knowledge/history.ts +163 -0
  77. package/src/knowledge/import.ts +391 -0
  78. package/src/knowledge/read.ts +342 -0
  79. package/src/knowledge/schema.ts +229 -0
  80. package/src/knowledge/write.ts +663 -0
  81. package/src/mcp/main.ts +134 -0
  82. package/src/mcp/output.ts +32 -0
  83. package/src/mcp/server.ts +316 -0
  84. package/src/mcp/tools/debug.ts +327 -0
  85. package/src/mcp/tools/execution.ts +117 -0
  86. package/src/mcp/tools/input.ts +72 -0
  87. package/src/mcp/tools/machine.ts +100 -0
  88. package/src/mcp/tools/media.ts +129 -0
  89. package/src/mcp/tools/memory.ts +250 -0
  90. package/src/mcp/tools/video.ts +271 -0
  91. package/src/native/acme.ts +116 -0
  92. package/src/native/discover.ts +179 -0
  93. package/src/native/dxa.ts +166 -0
  94. package/src/native/ghidra/analyze.ts +86 -0
  95. package/src/native/ghidra/index.ts +173 -0
  96. package/src/native/ghidra/language/c64rt_6510.cspec +34 -0
  97. package/src/native/ghidra/language/c64rt_6510.ldefs +15 -0
  98. package/src/native/ghidra/language/c64rt_6510.pspec +15 -0
  99. package/src/native/ghidra/language/c64rt_6510.slaspec +596 -0
  100. package/src/native/ghidra/scripts/C64Export.java +274 -0
  101. package/src/native/ghidra/scripts/C64Prepare.java +60 -0
  102. package/src/native/local.ts +34 -0
  103. package/src/native/processes.ts +350 -0
  104. package/src/native/run.ts +172 -0
  105. package/src/native/staging.ts +83 -0
  106. package/src/native/status.ts +113 -0
  107. package/src/native/types.ts +257 -0
  108. package/src/native/watchdog.ts +78 -0
  109. package/src/package.ts +21 -0
  110. package/src/platform.ts +441 -0
  111. package/src/project.ts +39 -0
  112. package/src/protocol/framing.ts +132 -0
  113. package/src/protocol/messages.ts +259 -0
  114. package/src/protocol/tools.ts +266 -0
  115. package/src/protocol/vice.ts +1348 -0
  116. package/src/script.ts +60 -0
  117. package/src/time.ts +15 -0
  118. package/src/trace.ts +194 -0
  119. package/THIRD-PARTY-NOTICES.md +0 -26
  120. package/bin/cli.mjs +0 -283
  121. package/skills/acme-build/SKILL.md +0 -260
  122. package/skills/acme-build/scripts/acme.mjs +0 -341
  123. package/skills/acme-build/template.a +0 -53
  124. package/skills/c64-disk-access/SKILL.md +0 -159
  125. package/skills/c64-disk-access/scripts/c1541.mjs +0 -569
  126. package/skills/c64-memory-mapping/SKILL.md +0 -631
  127. package/skills/c64-memory-mapping/memmap.json +0 -8800
  128. package/skills/c64-memory-mapping/scripts/driver.mjs +0 -553
  129. package/skills/c64-petcat/SKILL.md +0 -87
  130. package/skills/c64-petcat/scripts/petcat.mjs +0 -221
  131. package/skills/c64-program-recon/SKILL.md +0 -706
  132. package/skills/c64-program-recon/references/control-flow.md +0 -180
  133. package/skills/c64-program-recon/references/graphics.md +0 -79
  134. package/skills/c64-program-recon/references/observation-hazards.md +0 -143
  135. package/skills/c64-program-recon/references/reconstruction.md +0 -133
  136. package/skills/c64-program-recon/references/sound-and-input.md +0 -74
  137. package/skills/c64-program-recon/references/tool-selection.md +0 -78
  138. package/skills/c64-program-recon/scripts/derive.mjs +0 -364
  139. package/skills/c64-program-recon/scripts/packer-finding.mjs +0 -709
  140. package/skills/c64-program-recon/templates/memory-map.template.md +0 -101
  141. package/skills/c64-provenance-diff/SKILL.md +0 -292
  142. package/skills/c64-provenance-diff/scripts/diff-images.mjs +0 -984
  143. package/skills/c64-provenance-diff/scripts/recovery-schema.mjs +0 -395
  144. package/skills/c64-ram-capture/RELEASES.json.example +0 -17
  145. package/skills/c64-ram-capture/SKILL.md +0 -474
  146. package/skills/c64-ram-capture/scripts/compare-cross-binary.mjs +0 -525
  147. package/skills/c64-ram-capture/scripts/compare.mjs +0 -258
  148. package/skills/c64-ram-capture/scripts/derive-transients.mjs +0 -575
  149. package/skills/c64-ram-capture/scripts/dump-artifacts.mjs +0 -317
  150. package/skills/c64-ram-capture/scripts/mcp-module.mjs +0 -174
  151. package/skills/c64-ram-capture/scripts/project-paths.mjs +0 -81
  152. package/skills/c64-ram-capture/scripts/releases.mjs +0 -109
  153. package/skills/c64-ram-capture/scripts/vsf-slice.mjs +0 -147
  154. package/skills/c64-ram-capture/scripts/watch-loads.mjs +0 -581
  155. package/skills/c64-ram-capture/templates/capture-record.template.md +0 -99
  156. package/skills/c64-ram-capture/transients/README.md +0 -137
  157. package/skills/routine-queue-walker/SKILL.md +0 -394
  158. package/skills/routine-queue-walker/scripts/completeness-report.mjs +0 -465
  159. package/skills/vice-wedge-triage/SKILL.md +0 -269
@@ -0,0 +1,113 @@
1
+ // Tool status: finds each native tool the way its operations do and runs it
2
+ // once with a version option, so a user can see in advance which tools work.
3
+ // The Host Runtime reports VICE and its tools (host.status); the CLI reports
4
+ // the tools that skill scripts run themselves (ACME, DXA, Ghidra).
5
+
6
+ import { tmpdir } from "node:os";
7
+ import { stripVTControlCharacters } from "node:util";
8
+
9
+ import { WireFailure } from "../protocol/messages.ts";
10
+ import { type ToolStatus } from "../protocol/tools.ts";
11
+ import { ACME, DXA, findTool } from "./discover.ts";
12
+ import { findGhidra, ghidraVersion, NO_USABLE_JDK, noJdk } from "./ghidra/index.ts";
13
+ import { runTool, type ToolContext } from "./run.ts";
14
+ import { Workspace } from "./staging.ts";
15
+
16
+ const PROBE_TIMEOUT_MS = 20_000;
17
+
18
+ export interface Probe {
19
+ name: string;
20
+ /** The executable; throws WireFailure(installation-incomplete) when missing. */
21
+ find: (env: NodeJS.ProcessEnv) => string;
22
+ args: string[];
23
+ /** The version line; no such line means the tool did not run correctly. */
24
+ pattern: RegExp;
25
+ }
26
+
27
+ /** dxa's version line, also as "D:\\tools\\dxa.exe v0.1.5 -- symbolic 65xx disassembler" on Windows. */
28
+ export const DXA_VERSION = /^(?:.*[\\/])?dxa(?:\.exe)? v\d/i;
29
+
30
+ /** The first output line that matches, without terminal color codes (c1541 colors an OpenCBM notice). */
31
+ export function versionLine(output: string, pattern: RegExp): string | undefined {
32
+ return stripVTControlCharacters(output)
33
+ .split(/\r?\n/)
34
+ .map((line) => line.trim())
35
+ .find((line) => pattern.test(line));
36
+ }
37
+
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> {
47
+ const result = await runTool({ argv: [path, ...args], cwd: tmpdir(), supervisor: context.supervisor, signal: context.signal, timeoutMs: PROBE_TIMEOUT_MS, env: context.env });
48
+ const output = `${result.stdout}\n${result.stderr}`;
49
+ const found = version(output);
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 };
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}).`;
54
+ return { name, found: true, path, runs: false, problem };
55
+ }
56
+
57
+ function missing(name: string, error: unknown): ToolStatus {
58
+ if (!(error instanceof WireFailure)) throw error;
59
+ return { name, found: false, runs: false, problem: error.message };
60
+ }
61
+
62
+ /** Finds and runs each probed tool once. */
63
+ export function probeTools(probes: Probe[], context: ToolContext): Promise<ToolStatus[]> {
64
+ const env = context.env ?? process.env;
65
+ return Promise.all(
66
+ probes.map(async (probe) => {
67
+ let path: string;
68
+ try {
69
+ path = probe.find(env);
70
+ } catch (error) {
71
+ return missing(probe.name, error);
72
+ }
73
+ return run(probe.name, path, probe.args, (output) => versionLine(output, probe.pattern), context);
74
+ }),
75
+ );
76
+ }
77
+
78
+ /** Ghidra: without arguments analyzeHeadless starts Java and prints its usage, which proves that it runs. */
79
+ export async function probeGhidra(context: ToolContext): Promise<ToolStatus> {
80
+ const env = context.env ?? process.env;
81
+ try {
82
+ const ghidra = findGhidra(env);
83
+ // Its own settings directory keeps the user's Ghidra settings and logs untouched.
84
+ const workspace = Workspace.create(context.supervisor, "c64-re-tools-ghidra-status-");
85
+ const settings = workspace.root;
86
+ try {
87
+ const status = await run(
88
+ "Ghidra",
89
+ ghidra.analyzeHeadless,
90
+ [],
91
+ (output) => (/Headless Analyzer Usage/.test(output) ? ghidraVersion(ghidra) : undefined),
92
+ { ...context, env: { ...env, XDG_CONFIG_HOME: settings, XDG_CACHE_HOME: settings } },
93
+ (output) => (NO_USABLE_JDK.test(output) ? noJdk(ghidra).message : undefined),
94
+ );
95
+ return { ...status, path: ghidra.root };
96
+ } finally {
97
+ workspace.remove();
98
+ }
99
+ } catch (error) {
100
+ return missing("Ghidra", error);
101
+ }
102
+ }
103
+
104
+ /** The tools that skill scripts run themselves, on this machine. */
105
+ export async function localToolStatus(context: ToolContext): Promise<ToolStatus[]> {
106
+ const probes: Probe[] = [
107
+ { name: "ACME", find: (env) => findTool(ACME, env), args: ["--version"], pattern: /^This is ACME, release / },
108
+ // dxa -V prints its version and exits with status 1. It names itself by argv[0], without the
109
+ // directory only where that uses "/": on Windows the line starts with the full path.
110
+ { name: "dxa", find: (env) => findTool(DXA, env), args: ["-V"], pattern: DXA_VERSION },
111
+ ];
112
+ return [...(await probeTools(probes, context)), await probeGhidra(context)];
113
+ }
@@ -0,0 +1,257 @@
1
+ // Types of the native tools that skill scripts run directly (ACME, DXA,
2
+ // Ghidra), and the checks of their complete results. The skill script runs
3
+ // these tools itself: nothing here crosses the Host Runtime wire. Bytes
4
+ // (a program, an image, a listing) go next to these values as attachments.
5
+
6
+ import { prgRange, REFERENCE_KINDS, SYMBOL_NAME, type ReferenceKind } from "../c64.ts";
7
+ import { isInteger, isObject, isOneOf, WireFailure } from "../protocol/messages.ts";
8
+ import { type SourceTree } from "../protocol/tools.ts";
9
+
10
+ export interface Diagnostic {
11
+ severity: "error" | "warning" | "note";
12
+ /** Relative to the source root; absent when the message has no source location. */
13
+ file?: string;
14
+ line?: number;
15
+ message: string;
16
+ }
17
+
18
+ export interface AcmeParams {
19
+ files: SourceTree;
20
+ /** Relative to the source root. */
21
+ entrySource: string;
22
+ /** Directories relative to the source root. */
23
+ includeDirs: string[];
24
+ defines: Record<string, number | boolean>;
25
+ setPc?: number;
26
+ }
27
+
28
+ /** A symbol from the assembly: an address inside the program, or a constant value. */
29
+ export type AssembledSymbol = { name: string; kind: "address"; address: number; used: boolean } | { name: string; kind: "constant"; value: number; used: boolean };
30
+
31
+ export interface AcmeResult {
32
+ assembled: boolean;
33
+ /** Present when assembled: where the program loads. The program bytes are the one attachment that assemble returns. */
34
+ loadRange?: { start: number; end: number; bytes: number };
35
+ symbols?: AssembledSymbol[];
36
+ diagnostics: Diagnostic[];
37
+ }
38
+
39
+ export const IMAGE_KINDS = ["prg", "flat64k"] as const;
40
+ export type ImageKind = (typeof IMAGE_KINDS)[number];
41
+
42
+ export const GHIDRA_LIMITS = { entryPoints: 1024, dataRanges: 1024, labels: 4096, decompile: 32, decompiledChars: 16_000, decompiledTotalChars: 128_000 } as const;
43
+
44
+ /** The Ghidra analysis. Its image is given next to these fields: a PRG, or 64 KiB from $0000. */
45
+ export interface GhidraParams {
46
+ imageKind: ImageKind;
47
+ /** Seeds from current knowledge. */
48
+ entryPoints: number[];
49
+ dataRanges: Array<{ start: number; end: number }>;
50
+ labels: Array<{ address: number; name: string }>;
51
+ /** Routine entries to decompile for immediate reasoning; the text is never stored. */
52
+ decompile: number[];
53
+ }
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
+
66
+ export interface GhidraResult {
67
+ coverage: Array<{ start: number; end: number }>;
68
+ functions: Array<{ entry: number; name: string; nameSource: "seed" | "generated" | "native" }>;
69
+ regions: Array<{ start: number; end: number; classification: "code" | "data" }>;
70
+ references: Array<{ from: number; to: number; type: ReferenceKind }>;
71
+ decompilations: Array<{ entry: number; text: string; truncated: boolean }>;
72
+ /** Requested routines without decompiled code, with the decompiler's reason. */
73
+ notDecompiled?: Array<{ entry: number; reason: string }>;
74
+ /** Every instruction in the coverage, as Ghidra decoded it (D44). */
75
+ instructions: GhidraInstruction[];
76
+ /** Which categories are complete inside the coverage; private to the importer. */
77
+ completeness: { functions: boolean; regions: boolean; references: boolean };
78
+ /** The version of the Ghidra that made the result, for example "Ghidra 12.1.3". */
79
+ toolVersion?: string;
80
+ }
81
+
82
+ /** Refuses a DXA or Ghidra request with more seeds or decompilations than the request bounds allow, by name. */
83
+ export function checkRequestBounds(params: DxaParams & { decompile?: number[] }): void {
84
+ const bounds = [
85
+ ["entry points", params.entryPoints.length, GHIDRA_LIMITS.entryPoints],
86
+ ["data ranges", params.dataRanges.length, GHIDRA_LIMITS.dataRanges],
87
+ ["labels", params.labels.length, GHIDRA_LIMITS.labels],
88
+ ["routines to decompile", params.decompile?.length ?? 0, GHIDRA_LIMITS.decompile],
89
+ ] as const;
90
+ for (const [name, count, limit] of bounds) {
91
+ if (count > limit) throw new WireFailure("invalid-input", `The request has ${count} ${name}. Give at most ${limit} ${name}.`);
92
+ }
93
+ }
94
+
95
+ /** The DXA analysis: the same image and seeds as Ghidra, without decompilation. */
96
+ export type DxaParams = Omit<GhidraParams, "decompile">;
97
+
98
+ export interface DxaResult {
99
+ coverage: Array<{ start: number; end: number }>;
100
+ regions: Array<{ start: number; end: number; classification: "code" | "data" }>;
101
+ labels: Array<{ address: number; name: string }>;
102
+ /** The size of the listing, which is the one attachment that analyze returns (UTF-8 text). */
103
+ listingBytes: number;
104
+ /** Which categories are complete inside the coverage; private to the importer. */
105
+ completeness: { regions: boolean; labels: boolean };
106
+ /** The version line of the dxa that made the result; absent when dxa prints none. */
107
+ toolVersion?: string;
108
+ }
109
+
110
+ /** The longest tool version that a result can carry. */
111
+ const MAX_TOOL_VERSION = 200;
112
+
113
+ /** True for a tool version that a result can carry: absent, or one line of text without control characters, at most MAX_TOOL_VERSION characters. */
114
+ const isToolVersion = (value: unknown) => value === undefined || (typeof value === "string" && value.length <= MAX_TOOL_VERSION && /^[^\p{Cc}]+$/u.test(value));
115
+
116
+ /** Ends a result check with the Error that names its first problem. */
117
+ function fail(message: string): never {
118
+ throw new Error(message);
119
+ }
120
+
121
+ /** Narrows to a list whose items are still unchecked. */
122
+ const isList = (value: unknown): value is unknown[] => Array.isArray(value);
123
+
124
+ /**
125
+ * Checks a complete Ghidra result: every field typed and in range, every
126
+ * function, region and reference source inside the coverage, regions sorted
127
+ * and without overlap. Throws an Error naming the first problem.
128
+ */
129
+ export function checkGhidraResult(value: unknown): GhidraResult {
130
+ if (!isObject(value)) fail("not an object");
131
+ const { coverage, functions, regions, references, decompilations, notDecompiled, completeness, toolVersion, instructions } = value;
132
+ if (!isList(coverage) || coverage.length === 0) fail("no coverage");
133
+ const ranges: Array<{ start: number; end: number }> = [];
134
+ for (const range of coverage) {
135
+ if (!isObject(range) || !isInteger(range.start, 0, 0xffff) || !isInteger(range.end, range.start, 0xffff)) fail("bad coverage range");
136
+ ranges.push({ start: range.start, end: range.end });
137
+ }
138
+ const covered = (address: number) => ranges.some((range) => range.start <= address && address <= range.end);
139
+ if (!isList(functions)) fail("no functions");
140
+ for (const fn of functions) {
141
+ if (!isObject(fn) || !isInteger(fn.entry, 0, 0xffff) || typeof fn.name !== "string" || !SYMBOL_NAME.test(fn.name) || !isOneOf(["seed", "generated", "native"] as const, fn.nameSource)) fail("bad function");
142
+ if (!covered(fn.entry)) fail("a function lies outside the coverage");
143
+ }
144
+ if (!isList(regions)) fail("no regions");
145
+ let previousEnd = -1;
146
+ for (const region of regions) {
147
+ if (!isObject(region) || !isInteger(region.start, 0, 0xffff) || !isInteger(region.end, region.start, 0xffff) || (region.classification !== "code" && region.classification !== "data")) fail("bad region");
148
+ const start = region.start;
149
+ const end = region.end;
150
+ if (start <= previousEnd) fail("regions overlap or are out of order");
151
+ if (!ranges.some((range) => range.start <= start && end <= range.end)) fail("a region lies outside the coverage");
152
+ previousEnd = end;
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
+ }
170
+ if (!isList(references)) fail("no references");
171
+ for (const reference of references) {
172
+ if (!isObject(reference) || !isInteger(reference.from, 0, 0xffff) || !isInteger(reference.to, 0, 0xffff) || !isOneOf(REFERENCE_KINDS, reference.type)) fail("bad reference");
173
+ if (!covered(reference.from)) fail("a reference source lies outside the coverage");
174
+ }
175
+ if (!isList(decompilations) || decompilations.length > GHIDRA_LIMITS.decompile) fail("bad decompilations");
176
+ let total = 0;
177
+ for (const item of decompilations) {
178
+ if (!isObject(item) || !isInteger(item.entry, 0, 0xffff) || typeof item.text !== "string" || typeof item.truncated !== "boolean") fail("bad decompilation");
179
+ if (item.text.length > GHIDRA_LIMITS.decompiledChars) fail("a decompilation is too long");
180
+ total += item.text.length;
181
+ }
182
+ if (total > GHIDRA_LIMITS.decompiledTotalChars) fail("the decompilations are too long together");
183
+ if (notDecompiled !== undefined) {
184
+ if (!isList(notDecompiled) || notDecompiled.length > GHIDRA_LIMITS.decompile) fail("bad list of routines that were not decompiled");
185
+ for (const item of notDecompiled) {
186
+ if (!isObject(item) || !isInteger(item.entry, 0, 0xffff) || typeof item.reason !== "string" || item.reason.length > 1000) fail("bad routine that was not decompiled");
187
+ }
188
+ }
189
+ if (!isObject(completeness) || typeof completeness.functions !== "boolean" || typeof completeness.regions !== "boolean" || typeof completeness.references !== "boolean") fail("no completeness");
190
+ if (!isToolVersion(toolVersion)) fail("bad tool version");
191
+ // Every field is checked: the object is the result as it is.
192
+ return value as unknown as GhidraResult;
193
+ }
194
+
195
+ /**
196
+ * Checks a complete DXA result: typed fields, regions in order, without
197
+ * overlap and covering the coverage exactly, labels inside the coverage, and
198
+ * the listing as the one attachment. Throws an Error naming the first problem.
199
+ */
200
+ export function checkDxaResult(value: unknown, attachments: readonly Uint8Array[]): DxaResult {
201
+ if (!isObject(value)) fail("not an object");
202
+ const { coverage, regions, labels, listingBytes, completeness, toolVersion } = value;
203
+ if (!isList(coverage) || coverage.length !== 1) fail("the coverage is not one range");
204
+ const range = coverage[0];
205
+ if (!isObject(range) || !isInteger(range.start, 0, 0xffff) || !isInteger(range.end, range.start, 0xffff)) fail("bad coverage range");
206
+ const start = range.start;
207
+ const end = range.end;
208
+ if (!isList(regions)) fail("no regions");
209
+ let next = start;
210
+ for (const region of regions) {
211
+ if (!isObject(region) || !isInteger(region.start, 0, 0xffff) || !isInteger(region.end, region.start, 0xffff) || (region.classification !== "code" && region.classification !== "data")) fail("bad region");
212
+ if (region.start !== next) fail("the regions leave a gap or overlap");
213
+ next = region.end + 1;
214
+ }
215
+ if (next !== end + 1) fail("the regions do not reach the end of the coverage");
216
+ if (!isList(labels)) fail("no labels");
217
+ for (const label of labels) {
218
+ if (!isObject(label) || !isInteger(label.address, start, end) || typeof label.name !== "string" || !SYMBOL_NAME.test(label.name)) fail("bad label");
219
+ }
220
+ const [listing] = attachments;
221
+ if (attachments.length !== 1 || listing === undefined || listingBytes !== listing.length) fail("the listing is missing");
222
+ if (!isObject(completeness) || typeof completeness.regions !== "boolean" || typeof completeness.labels !== "boolean") fail("no completeness");
223
+ if (!isToolVersion(toolVersion)) fail("bad tool version");
224
+ // Every field is checked: the object is the result as it is.
225
+ return value as unknown as DxaResult;
226
+ }
227
+
228
+ /**
229
+ * The address range of an analyzer image: a PRG at its load address, or 64 KiB
230
+ * of memory from $0000. Throws WireFailure(invalid-input) for anything else.
231
+ */
232
+ export function imageRange(kind: ImageKind, image: Uint8Array): { start: number; end: number; body: Uint8Array } {
233
+ if (kind === "flat64k") {
234
+ if (image.length !== 0x10000) throw new WireFailure("invalid-input", "A flat 64 KiB image has exactly 65536 bytes.");
235
+ return { start: 0, end: 0xffff, body: image };
236
+ }
237
+ try {
238
+ return prgRange(image);
239
+ } catch (error) {
240
+ if (!(error instanceof RangeError)) throw error;
241
+ throw new WireFailure("invalid-input", error.message);
242
+ }
243
+ }
244
+
245
+ /**
246
+ * The seeds that an analysis of [start, end] can use: the entry points inside
247
+ * it, and the data ranges cut to it. A data range that holds an entry point
248
+ * is left out, so that the entry point is disassembled.
249
+ */
250
+ export function seedsInside(params: Pick<DxaParams, "entryPoints" | "dataRanges">, start: number, end: number): Pick<DxaParams, "entryPoints" | "dataRanges"> {
251
+ const entryPoints = params.entryPoints.filter((address) => address >= start && address <= end);
252
+ const dataRanges = params.dataRanges
253
+ .filter((range) => range.end >= start && range.start <= end)
254
+ .map((range) => ({ start: Math.max(range.start, start), end: Math.min(range.end, end) }))
255
+ .filter((range) => !entryPoints.some((entry) => entry >= range.start && entry <= range.end));
256
+ return { entryPoints, dataRanges };
257
+ }
@@ -0,0 +1,78 @@
1
+ // The watchdog of a supervisor's process (the Host Runtime or a skill
2
+ // script), started by ProcessSupervisor.startWatchdog.
3
+ // Arguments: <owner pid> <registry file>. Standard input: a pipe whose
4
+ // writing end only the owner holds.
5
+ //
6
+ // It runs in its own process group, so a signal to the owner's group does
7
+ // not reach it. When the owner dies without its exit guard (SIGKILL, a
8
+ // crash), it stops every process group in the registry and removes every
9
+ // path. When the owner exits normally, the exit guard removes the registry
10
+ // and the watchdog ends.
11
+ //
12
+ // The end of the pipe tells it at once that the owner is gone: the system
13
+ // closes the owner's end when the owner ends, however it ends, and a pid that
14
+ // another process now uses cannot keep the pipe open. A poll of the owner's
15
+ // pid is the fallback for a pipe that does not end.
16
+
17
+ import { existsSync, readFileSync, rmSync } from "node:fs";
18
+ import { dirname } from "node:path";
19
+
20
+ import { isAlive, killTree, signalGroup, type Registry } from "./processes.ts";
21
+
22
+ const POLL_MS = 500;
23
+ const GRACE_MS = 2_000;
24
+
25
+ const [ownerPidText, registryArgument] = process.argv.slice(2);
26
+ const ownerPid = Number(ownerPidText);
27
+ if (!Number.isInteger(ownerPid) || ownerPid <= 0 || registryArgument === undefined) process.exit(2);
28
+ const registryFile: string = registryArgument;
29
+
30
+ function signal(group: number, name: NodeJS.Signals): void {
31
+ try {
32
+ if (process.platform !== "win32") signalGroup(group, name);
33
+ else if (name === "SIGKILL") killTree(group);
34
+ } catch {
35
+ // The group is gone already.
36
+ }
37
+ }
38
+
39
+ /** Removes a path. A path that cannot be removed does not stop the removal of the others; nothing reads the watchdog's output. */
40
+ function remove(path: string): void {
41
+ try {
42
+ rmSync(path, { recursive: true, force: true });
43
+ } catch {
44
+ // The path stays.
45
+ }
46
+ }
47
+
48
+ let cleaning = false;
49
+
50
+ function cleanUp(): void {
51
+ if (cleaning) return;
52
+ cleaning = true;
53
+ clearInterval(timer);
54
+ let registry: Registry;
55
+ try {
56
+ registry = JSON.parse(readFileSync(registryFile, "utf8")) as Registry;
57
+ } catch {
58
+ process.exit(0);
59
+ }
60
+ for (const group of registry.groups) signal(group, "SIGTERM");
61
+ setTimeout(() => {
62
+ for (const group of registry.groups) signal(group, "SIGKILL");
63
+ for (const path of registry.paths) remove(path);
64
+ remove(dirname(registryFile));
65
+ process.exit(0);
66
+ }, GRACE_MS);
67
+ }
68
+
69
+ // The owner writes nothing: only the end of the pipe (or an error, which also ends it) counts.
70
+ process.stdin.on("data", () => {});
71
+ process.stdin.once("end", cleanUp);
72
+ process.stdin.once("error", cleanUp);
73
+
74
+ const timer = setInterval(() => {
75
+ // A normal exit removed the registry: nothing is left to do.
76
+ if (!existsSync(registryFile)) process.exit(0);
77
+ if (!isAlive(ownerPid)) cleanUp();
78
+ }, POLL_MS);
package/src/package.ts ADDED
@@ -0,0 +1,21 @@
1
+ // The name, version and npm channel of this copy of c64-re-tools, for the npx
2
+ // commands that refusals and help texts give. The package.json above src/ is
3
+ // the package's own one; in an installed skill it is the skill's one, which
4
+ // has the same name and version (distribution/plugin.ts).
5
+
6
+ import { readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+
9
+ /**
10
+ * The npm dist-tag that a version is published under: a prerelease
11
+ * (2.0.0-rc.1) goes to "next", a release to "latest".
12
+ */
13
+ export function channelOf(version: string): "latest" | "next" {
14
+ return version.includes("-") ? "next" : "latest";
15
+ }
16
+
17
+ /** The name, version and dist-tag of this npm package, from its package.json. */
18
+ export const PACKAGE = (() => {
19
+ const metadata = JSON.parse(readFileSync(join(import.meta.dirname, "..", "package.json"), "utf8")) as { name: string; version: string };
20
+ return { name: metadata.name, version: metadata.version, tag: channelOf(metadata.version) };
21
+ })();