@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.21

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 (126) hide show
  1. package/README.md +28 -62
  2. package/distribution/package.ts +1 -16
  3. package/distribution/plugin.ts +5 -3
  4. package/package.json +1 -1
  5. package/skills/c64-assembler/SKILL.md +25 -6
  6. package/skills/c64-assembler/scripts/assemble.ts +45 -10
  7. package/skills/c64-assembler/scripts/lib/compare.ts +48 -0
  8. package/skills/c64-basic/SKILL.md +15 -7
  9. package/skills/c64-basic/scripts/basic.ts +4 -4
  10. package/skills/c64-disk/SKILL.md +34 -11
  11. package/skills/c64-disk/scripts/disk.ts +65 -10
  12. package/skills/c64-disk/scripts/lib/containers.ts +124 -0
  13. package/skills/c64-docs/SKILL.md +138 -0
  14. package/skills/c64-docs/references/page-types.md +141 -0
  15. package/skills/c64-docs/references/style.md +43 -0
  16. package/skills/c64-docs/scripts/generate.ts +102 -0
  17. package/skills/c64-docs/scripts/lib/links.ts +155 -0
  18. package/skills/c64-docs/scripts/lib/markdown.ts +79 -0
  19. package/skills/c64-docs/scripts/lib/reference.ts +406 -0
  20. package/skills/c64-docs/scripts/lib/source-map.ts +81 -0
  21. package/skills/c64-docs/scripts/link.ts +207 -0
  22. package/skills/c64-emulator/SKILL.md +29 -8
  23. package/skills/c64-knowledge/SKILL.md +201 -15
  24. package/skills/c64-knowledge/scripts/knowledge.ts +927 -78
  25. package/skills/c64-knowledge/scripts/lib/listing.ts +162 -0
  26. package/skills/c64-memory-map/SKILL.md +14 -2
  27. package/skills/c64-memory-map/scripts/memmap.ts +15 -4
  28. package/skills/c64-provenance/SKILL.md +100 -11
  29. package/skills/c64-provenance/scripts/lib/compare.ts +152 -0
  30. package/skills/c64-provenance/scripts/lib/library.ts +584 -0
  31. package/skills/c64-provenance/scripts/lib/runtime.ts +191 -0
  32. package/skills/c64-provenance/scripts/lib/separate.ts +140 -0
  33. package/skills/c64-provenance/scripts/provenance.ts +477 -24
  34. package/skills/c64-reverse-engineering/SKILL.md +90 -11
  35. package/skills/c64-source/SKILL.md +174 -0
  36. package/skills/c64-source/scripts/generate.ts +788 -0
  37. package/skills/c64-source/scripts/lib/loader.ts +630 -0
  38. package/skills/c64-source/scripts/lib/source.ts +1121 -0
  39. package/skills/c64-static-analysis/SKILL.md +53 -13
  40. package/skills/c64-static-analysis/scripts/analyze.ts +260 -31
  41. package/skills/c64-static-analysis/scripts/lib/coverage.ts +72 -0
  42. package/skills/c64-static-analysis/scripts/{findings.ts → lib/findings.ts} +11 -5
  43. package/skills/c64-static-analysis/scripts/lib/idioms.ts +158 -0
  44. package/skills/c64-static-analysis/scripts/lib/instructions.ts +105 -0
  45. package/skills/c64-static-analysis/scripts/lib/macros.ts +233 -0
  46. package/skills/c64-static-analysis/scripts/lib/pitfalls.ts +68 -0
  47. package/skills/c64-static-analysis/scripts/lib/pointers.ts +108 -0
  48. package/skills/c64-testing/SKILL.md +57 -4
  49. package/skills/c64-testing/references/scenario-format.md +50 -6
  50. package/skills/c64-testing/scripts/checklist.ts +3 -3
  51. package/skills/c64-testing/scripts/diff.ts +167 -0
  52. package/skills/c64-testing/scripts/lib/diff.ts +70 -0
  53. package/skills/c64-testing/scripts/{scenario.ts → lib/scenario.ts} +183 -152
  54. package/skills/c64-testing/scripts/run.ts +241 -0
  55. package/skills/c64-testing/scripts/test.ts +20 -10
  56. package/skills/c64-unpacker/SKILL.md +88 -21
  57. package/skills/c64-unpacker/scripts/lib/evidence.ts +207 -0
  58. package/skills/c64-unpacker/scripts/lib/loads.ts +209 -0
  59. package/skills/c64-unpacker/scripts/lib/machine.ts +80 -0
  60. package/skills/c64-unpacker/scripts/lib/stages.ts +142 -0
  61. package/skills/c64-unpacker/scripts/unpack.ts +545 -61
  62. package/src/c64.ts +240 -12
  63. package/src/cli/cleanup.ts +1 -1
  64. package/src/cli/main.ts +14 -8
  65. package/src/cli/status.ts +105 -11
  66. package/src/graphics.ts +158 -0
  67. package/src/host/main.ts +57 -31
  68. package/src/host/server.ts +30 -47
  69. package/src/host/tools/c1541.ts +34 -5
  70. package/src/host/tools/index.ts +6 -2
  71. package/src/host/tools/petcat.ts +10 -3
  72. package/src/host/tools/status.ts +23 -6
  73. package/src/host/vice/adapter.ts +64 -20
  74. package/src/host/vice/chips.ts +11 -1
  75. package/src/host/vice/process.ts +19 -8
  76. package/src/host/vice/screen.ts +4 -44
  77. package/src/host/vice/session.ts +343 -60
  78. package/src/host-client/autostart.ts +55 -0
  79. package/src/host-client/connect.ts +14 -14
  80. package/src/host-client/coverage.ts +36 -0
  81. package/src/host-client/image.ts +63 -0
  82. package/src/host-client/steps.ts +175 -0
  83. package/src/host-client/tools.ts +7 -6
  84. package/src/host-client/transfer.ts +60 -10
  85. package/src/host-client/vice-session.ts +107 -28
  86. package/src/knowledge/conflicts.ts +161 -0
  87. package/src/knowledge/database.ts +150 -38
  88. package/src/knowledge/expression.ts +209 -0
  89. package/src/knowledge/graph.ts +104 -0
  90. package/src/knowledge/history.ts +105 -27
  91. package/src/knowledge/import.ts +141 -85
  92. package/src/knowledge/join.ts +211 -0
  93. package/src/knowledge/program.ts +258 -0
  94. package/src/knowledge/read.ts +445 -63
  95. package/src/knowledge/schema.ts +277 -65
  96. package/src/knowledge/text.ts +415 -0
  97. package/src/knowledge/write.ts +1145 -150
  98. package/src/mcp/main.ts +44 -19
  99. package/src/mcp/output.ts +39 -0
  100. package/src/mcp/server.ts +15 -4
  101. package/src/mcp/tools/debug.ts +202 -31
  102. package/src/mcp/tools/execution.ts +39 -5
  103. package/src/mcp/tools/input.ts +7 -4
  104. package/src/mcp/tools/machine.ts +14 -2
  105. package/src/mcp/tools/media.ts +44 -16
  106. package/src/mcp/tools/memory.ts +68 -12
  107. package/src/mcp/tools/video.ts +20 -9
  108. package/src/native/discover.ts +44 -6
  109. package/src/native/ghidra/analyze.ts +2 -2
  110. package/src/native/ghidra/index.ts +19 -3
  111. package/src/native/ghidra/language/c64rt_6510.slaspec +4 -1
  112. package/src/native/ghidra/scripts/C64Export.java +33 -1
  113. package/src/native/ghidra/scripts/C64Prepare.java +7 -1
  114. package/src/native/status.ts +15 -3
  115. package/src/native/types.ts +43 -8
  116. package/src/opcodes.ts +133 -0
  117. package/src/package.ts +21 -0
  118. package/{skills/c64-memory-map/scripts → src}/platform.ts +87 -7
  119. package/src/project.ts +166 -3
  120. package/src/protocol/messages.ts +17 -10
  121. package/src/protocol/tools.ts +27 -22
  122. package/src/protocol/vice.ts +255 -33
  123. package/src/script.ts +49 -9
  124. package/src/trace.ts +55 -23
  125. package/skills/c64-provenance/scripts/compare.ts +0 -92
  126. package/skills/c64-unpacker/scripts/evidence.ts +0 -135
package/src/script.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  // The one error envelope for the skill scripts and the CLI: a script prints one JSON line,
2
2
  // a refusal prints {"error":{code,message}} with exit status 1, and a usage error prints the
3
- // usage text as an invalid-input error with exit status 2.
3
+ // usage text as an invalid-input error with exit status 2. Every script takes --project <folder> (D67).
4
4
 
5
- import { WireFailure } from "./protocol/messages.ts";
5
+ import { ProjectError, useProject } from "./project.ts";
6
+ import { shownMessage, WireFailure } from "./protocol/messages.ts";
6
7
  import { elapsedMs, startTrace, trace, type TraceFields } from "./trace.ts";
7
8
 
8
9
  /** A wrong command line. The message names the problem; an empty message asks for the usage text alone. */
@@ -17,28 +18,67 @@ function isParseArgsError(error: unknown): error is Error {
17
18
  return error instanceof Error && typeof code === "string" && code.startsWith("ERR_PARSE_ARGS_");
18
19
  }
19
20
 
21
+ /** The line that every usage text ends with. */
22
+ export const PROJECT_USAGE = "Every script takes --project <folder>: the project folder, relative to the working directory. Without it, the script uses the nearest folder at or above the working directory that holds .c64-re-tools/.";
23
+
24
+ /**
25
+ * Takes --project <folder> and --project=<folder> out of a command line, up to "--".
26
+ * Returns the folder and the other arguments. Throws UsageError for a --project without a folder or a second --project.
27
+ */
28
+ export function takeProject(argv: readonly string[]): { project?: string; argv: string[] } {
29
+ const rest: string[] = [];
30
+ let project: string | undefined;
31
+ for (let index = 0; index < argv.length; index++) {
32
+ const argument = argv[index]!;
33
+ if (argument === "--") {
34
+ rest.push(...argv.slice(index));
35
+ break;
36
+ }
37
+ let value: string | undefined;
38
+ if (argument === "--project") {
39
+ value = argv[index + 1];
40
+ if (value === undefined || value.startsWith("-")) throw new UsageError("--project needs a folder");
41
+ index++;
42
+ } else if (argument.startsWith("--project=")) {
43
+ value = argument.slice("--project=".length);
44
+ if (value === "") throw new UsageError("--project needs a folder");
45
+ } else {
46
+ rest.push(argument);
47
+ continue;
48
+ }
49
+ if (project !== undefined) throw new UsageError("give --project only once");
50
+ project = value;
51
+ }
52
+ return project === undefined ? { argv: rest } : { project, argv: rest };
53
+ }
54
+
20
55
  function printLine(value: unknown): void {
21
56
  process.stdout.write(`${JSON.stringify(value)}\n`);
22
57
  }
23
58
 
24
59
  /**
25
- * Runs a script body. Prints its result as one JSON line when it returns a value (undefined prints nothing).
26
- * A refusal (WireFailure always, plus `refusals`) prints {"error":{code,message}} with exit status 1.
60
+ * Runs a script body with the command line of the process, without --project: that sets the project first (D67).
61
+ * Prints its result as one JSON line when it returns a value (undefined prints nothing).
62
+ * A refusal (WireFailure and ProjectError always, plus `refusals`) prints {"error":{code,message}} with exit status 1.
27
63
  * A UsageError or a node:util parseArgs error (code ERR_PARSE_ARGS_*) prints {"error":{"code":"invalid-input",
28
64
  * "message": message === "" ? usage : message + "\n\n" + usage}} with exit status 2. Anything else is rethrown.
29
65
  * The body may set process.exitCode itself for an extra exit status; a value it returns is printed as usual.
30
66
  */
31
- export async function runScript(body: () => unknown, options: { usage: string; refusals?: readonly RefusalClass[] }): Promise<void> {
67
+ export async function runScript(body: (argv: string[]) => unknown, options: { usage: string; refusals?: readonly RefusalClass[] }): Promise<void> {
68
+ const usage = `${options.usage}\n\n${PROJECT_USAGE}`;
32
69
  // The trace of the script, when C64RT_TRACE is set; its start event carries the script and its arguments.
33
70
  startTrace("script", { warn: (line) => process.stderr.write(`${line}\n`) });
34
71
  const started = performance.now();
35
72
  const end = (fields: TraceFields) => trace().event("script.end", { ms: elapsedMs(started), status: process.exitCode ?? 0, ...fields });
36
73
  let result: unknown;
37
74
  try {
38
- result = await body();
75
+ const { project, argv } = takeProject(process.argv.slice(2));
76
+ if (project !== undefined) useProject(project);
77
+ result = await body(argv);
39
78
  } catch (error) {
40
- if ([WireFailure, ...(options.refusals ?? [])].some((refusal) => error instanceof refusal)) {
41
- const { code, message } = error as Error & { code: string };
79
+ if ([WireFailure, ProjectError, ...(options.refusals ?? [])].some((refusal) => error instanceof refusal)) {
80
+ const { code } = error as Error & { code: string };
81
+ const message = shownMessage(error as Error & { code: string });
42
82
  printLine({ error: { code, message } });
43
83
  process.exitCode = 1;
44
84
  end({ error: { code, message } });
@@ -46,7 +86,7 @@ export async function runScript(body: () => unknown, options: { usage: string; r
46
86
  }
47
87
  if (error instanceof UsageError || isParseArgsError(error)) {
48
88
  const { message } = error;
49
- printLine({ error: { code: "invalid-input", message: message === "" ? options.usage : `${message}\n\n${options.usage}` } });
89
+ printLine({ error: { code: "invalid-input", message: message === "" ? usage : `${message}\n\n${usage}` } });
50
90
  process.exitCode = 2;
51
91
  end({ error: { code: "invalid-input", message } });
52
92
  return;
package/src/trace.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  // An opt-in trace for the person who debugs a run. With C64RT_TRACE=<directory>
2
- // in the environment, each process appends its events as JSON lines to a file
3
- // of its own in that directory; unset, every call does nothing. The trace is
4
- // for people: nothing of it reaches the agent, and no result depends on it.
2
+ // in the environment (the host: --log-dir), each process appends its events as
3
+ // JSON lines to a file of its own in that directory. The host can also echo
4
+ // each event to the console (--log-console). Off, every call does nothing. The
5
+ // trace is for people: nothing of it reaches the agent, and no result depends on it.
5
6
 
6
7
  import { appendFileSync, copyFileSync, mkdirSync, readFileSync } from "node:fs";
7
8
  import { join } from "node:path";
@@ -66,14 +67,16 @@ export class Trace {
66
67
  readonly kind: TraceKind;
67
68
  /** The directory of the trace, or undefined when the trace is off. */
68
69
  readonly dir: string | undefined;
69
- /** The file this trace appends to, or undefined when the trace is off. */
70
+ /** The file this trace appends to, or undefined when the trace writes no file. */
70
71
  readonly file: string | undefined;
72
+ readonly #echo: ((line: string) => void) | undefined;
71
73
  #broken = false;
72
74
 
73
- private constructor(kind: TraceKind, dir: string | undefined, file: string | undefined) {
75
+ private constructor(kind: TraceKind, dir: string | undefined, file: string | undefined, echo?: (line: string) => void) {
74
76
  this.kind = kind;
75
77
  this.dir = dir;
76
78
  this.file = file;
79
+ this.#echo = echo;
77
80
  }
78
81
 
79
82
  /** A trace that records nothing. */
@@ -81,31 +84,46 @@ export class Trace {
81
84
  return new Trace(kind, undefined, undefined);
82
85
  }
83
86
 
87
+ /** A trace that writes no file and gives each event to `echo` as one line. */
88
+ static echoing(kind: TraceKind, echo: (line: string) => void): Trace {
89
+ return new Trace(kind, undefined, undefined, echo);
90
+ }
91
+
84
92
  /**
85
93
  * A trace into `dir`, created when it is missing. The file is
86
94
  * `<kind>-<CET time>-<pid>.jsonl`, and its first event is `trace.start`.
87
95
  * Throws when the directory cannot be used.
88
96
  */
89
- static open(kind: TraceKind, dir: string, now = new Date()): Trace {
97
+ static open(kind: TraceKind, dir: string, now = new Date(), echo?: (line: string) => void): Trace {
90
98
  mkdirSync(dir, { recursive: true });
91
- const trace = new Trace(kind, dir, join(dir, `${kind}-${fileStampCet(now)}-${process.pid}.jsonl`));
99
+ const trace = new Trace(kind, dir, join(dir, `${kind}-${fileStampCet(now)}-${process.pid}.jsonl`), echo);
92
100
  trace.event("trace.start", { version: runningVersion(), pid: process.pid, argv: process.argv.slice(1), cwd: process.cwd(), node: process.version, platform: process.platform });
93
101
  return trace;
94
102
  }
95
103
 
96
104
  get on(): boolean {
105
+ return this.#writes || this.#echo !== undefined;
106
+ }
107
+
108
+ get #writes(): boolean {
97
109
  return this.file !== undefined && !this.#broken;
98
110
  }
99
111
 
100
- /** Appends one event. A write that fails turns the trace off; nothing throws. */
112
+ /**
113
+ * Records one event: appends it to the file, and gives it to the echo as
114
+ * "<event> <fields as JSON>". The echo skips `log` events, which the program
115
+ * prints itself. A write that fails stops the file; nothing throws.
116
+ */
101
117
  event(name: string, fields: TraceFields = {}): void {
102
118
  if (!this.on) return;
103
- const line: Record<string, unknown> = { t: isoCet(), kind: this.kind, event: name };
119
+ const summary: Record<string, unknown> = {};
104
120
  for (const [key, value] of Object.entries(fields)) {
105
- if (!RESERVED.has(key) && value !== undefined) line[key] = summarize(value);
121
+ if (!RESERVED.has(key) && value !== undefined) summary[key] = summarize(value);
106
122
  }
123
+ if (this.#echo !== undefined && name !== "log") this.#echo(`${name} ${JSON.stringify(summary)}`);
124
+ if (!this.#writes) return;
107
125
  try {
108
- appendFileSync(this.file!, `${JSON.stringify(line)}\n`);
126
+ appendFileSync(this.file!, `${JSON.stringify({ t: isoCet(), kind: this.kind, event: name, ...summary })}\n`);
109
127
  } catch {
110
128
  this.#broken = true;
111
129
  }
@@ -121,7 +139,7 @@ export class Trace {
121
139
  * trace, as `<kind>-<pid>-<name>`. A copy that fails is recorded, not thrown.
122
140
  */
123
141
  keep(source: string, name: string): void {
124
- if (!this.on) return;
142
+ if (!this.#writes) return;
125
143
  const target = join(this.dir!, `${this.kind}-${process.pid}-${name}`);
126
144
  try {
127
145
  copyFileSync(source, target);
@@ -135,23 +153,37 @@ export class Trace {
135
153
  // Off until a program calls startTrace; the kind is a placeholder until then.
136
154
  let current = Trace.off("script");
137
155
 
156
+ /** Where the trace of a process goes: a directory for its file, and an echo for each event. */
157
+ export interface TraceTarget {
158
+ /** The directory, and how the user gave it (`--log-dir <path>`, for example) for a warning. */
159
+ dir?: { path: string; given: string };
160
+ echo?: (line: string) => void;
161
+ }
162
+
163
+ /** The target that C64RT_TRACE in `env` gives: its directory, or nothing. */
164
+ export function traceTargetFromEnv(env: NodeJS.ProcessEnv = process.env): TraceTarget {
165
+ const path = env[TRACE_VARIABLE];
166
+ return path === undefined || path === "" ? {} : { dir: { path, given: `${TRACE_VARIABLE}=${path}` } };
167
+ }
168
+
138
169
  /**
139
- * Switches the trace of this process on when `env` (the environment by
140
- * default) has C64RT_TRACE, and off otherwise. A directory that cannot be used
141
- * leaves the trace off and reports that through `warn`. Called once, at the
142
- * start of a program.
170
+ * Switches the trace of this process on for `target` (by default what
171
+ * C64RT_TRACE in `env` gives), and off when the target is empty. A directory
172
+ * that cannot be used writes no file and reports that through `warn`; an echo
173
+ * stays on. Called once, at the start of a program.
143
174
  */
144
- export function startTrace(kind: TraceKind, options: { env?: NodeJS.ProcessEnv; warn?: (line: string) => void } = {}): Trace {
145
- const dir = (options.env ?? process.env)[TRACE_VARIABLE];
146
- if (dir === undefined || dir === "") {
147
- current = Trace.off(kind);
175
+ export function startTrace(kind: TraceKind, options: { env?: NodeJS.ProcessEnv; target?: TraceTarget; warn?: (line: string) => void } = {}): Trace {
176
+ const { dir, echo } = options.target ?? traceTargetFromEnv(options.env);
177
+ const noFile = () => (echo === undefined ? Trace.off(kind) : Trace.echoing(kind, echo));
178
+ if (dir === undefined) {
179
+ current = noFile();
148
180
  return current;
149
181
  }
150
182
  try {
151
- current = Trace.open(kind, dir);
183
+ current = Trace.open(kind, dir.path, new Date(), echo);
152
184
  } catch (error) {
153
- current = Trace.off(kind);
154
- options.warn?.(`${TRACE_VARIABLE}=${dir} cannot be used (${error instanceof Error ? error.message : String(error)}); the trace is off.`);
185
+ current = noFile();
186
+ options.warn?.(`${dir.given} cannot be used (${error instanceof Error ? error.message : String(error)}); no trace file is written.`);
155
187
  }
156
188
  return current;
157
189
  }
@@ -1,92 +0,0 @@
1
- // Byte comparison of releases for the c64-provenance script. Each release is
2
- // placed at its load address plus an optional shift; the comparison reports
3
- // where the releases differ and which releases agree there. It never decides
4
- // which bytes are original: that needs evidence beyond one comparison.
5
-
6
- export interface Release {
7
- name: string;
8
- /** Address of the first byte after the shift. */
9
- start: number;
10
- bytes: Uint8Array;
11
- }
12
-
13
- export interface DifferingRange {
14
- start: number;
15
- end: number;
16
- /** The bytes of each release in this range, by release name. */
17
- values: Record<string, number[]>;
18
- /** Releases with the same bytes here, largest group first. */
19
- groups: string[][];
20
- }
21
-
22
- export interface Comparison {
23
- /** The addresses that every release covers. */
24
- common: { start: number; end: number } | null;
25
- identicalBytes: number;
26
- differingBytes: number;
27
- differences: DifferingRange[];
28
- /** True when more differing ranges exist than the limit allowed. */
29
- moreDifferences: boolean;
30
- }
31
-
32
- /** Joins differing addresses that are at most this far apart into one range. */
33
- const JOIN_GAP = 4;
34
-
35
- export function compareReleases(releases: Release[], options: { from?: number; to?: number; maxRanges?: number } = {}): Comparison {
36
- const start = Math.max(options.from ?? 0, ...releases.map((release) => release.start));
37
- const end = Math.min(options.to ?? 0xffff, ...releases.map((release) => release.start + release.bytes.length - 1));
38
- if (start > end) return { common: null, identicalBytes: 0, differingBytes: 0, differences: [], moreDifferences: false };
39
- const at = (release: Release, address: number) => release.bytes[address - release.start]!;
40
- let identicalBytes = 0;
41
- const differing: number[] = [];
42
- for (let address = start; address <= end; address++) {
43
- const first = at(releases[0]!, address);
44
- if (releases.every((release) => at(release, address) === first)) identicalBytes++;
45
- else differing.push(address);
46
- }
47
- const ranges: Array<{ start: number; end: number }> = [];
48
- for (const address of differing) {
49
- const last = ranges.at(-1);
50
- if (last !== undefined && address - last.end <= JOIN_GAP) last.end = address;
51
- else ranges.push({ start: address, end: address });
52
- }
53
- const limit = options.maxRanges ?? 64;
54
- const differences = ranges.slice(0, limit).map((range) => {
55
- const values: Record<string, number[]> = {};
56
- const groups = new Map<string, string[]>();
57
- for (const release of releases) {
58
- const bytes = Array.from(release.bytes.subarray(range.start - release.start, range.end - release.start + 1));
59
- values[release.name] = bytes;
60
- const key = bytes.join(",");
61
- groups.set(key, [...(groups.get(key) ?? []), release.name]);
62
- }
63
- return { ...range, values, groups: [...groups.values()].sort((a, b) => b.length - a.length) };
64
- });
65
- return { common: { start, end }, identicalBytes, differingBytes: differing.length, differences, moreDifferences: ranges.length > limit };
66
- }
67
-
68
- /**
69
- * The shift that puts `other` over `base` best: the most common address
70
- * difference between equal 8-byte sequences. Undefined when nothing matches.
71
- */
72
- export function suggestShift(base: Release, other: Release): { shift: number; matches: number } | undefined {
73
- const WINDOW = 8;
74
- const key = (bytes: Uint8Array, at: number) => Buffer.from(bytes.subarray(at, at + WINDOW)).toString("latin1");
75
- const positions = new Map<string, number[]>();
76
- for (let at = 0; at + WINDOW <= base.bytes.length; at++) {
77
- const positionsOfKey = positions.get(key(base.bytes, at));
78
- // Very common sequences (fill bytes) say nothing about the alignment.
79
- if (positionsOfKey === undefined) positions.set(key(base.bytes, at), [at]);
80
- else if (positionsOfKey.length < 8) positionsOfKey.push(at);
81
- }
82
- const votes = new Map<number, number>();
83
- for (let at = 0; at + WINDOW <= other.bytes.length; at++) {
84
- for (const position of positions.get(key(other.bytes, at)) ?? []) {
85
- const shift = base.start + position - (other.start + at);
86
- votes.set(shift, (votes.get(shift) ?? 0) + 1);
87
- }
88
- }
89
- let best: { shift: number; matches: number } | undefined;
90
- for (const [shift, matches] of votes) if (best === undefined || matches > best.matches) best = { shift, matches };
91
- return best;
92
- }
@@ -1,135 +0,0 @@
1
- // Local evidence of packing for the c64-unpacker script: where BASIC hands
2
- // over and how compressed the bytes look. It never names a packer.
3
-
4
- import { formatC64Address } from "#src/c64.ts";
5
-
6
- export class PackingError extends Error {}
7
-
8
- /** A 256-byte block above this many bits per byte looks compressed. */
9
- const HIGH_ENTROPY = 6.9;
10
- const BLOCK = 256;
11
-
12
- function entropy(bytes: Uint8Array): number {
13
- const counts = new Array<number>(256).fill(0);
14
- for (const byte of bytes) counts[byte]!++;
15
- let bits = 0;
16
- for (const count of counts) {
17
- if (count === 0) continue;
18
- const p = count / bytes.length;
19
- bits -= p * Math.log2(p);
20
- }
21
- return bits;
22
- }
23
-
24
- /** The constant SYS address of the first BASIC line, when the program starts with one. */
25
- function basicSys(program: Uint8Array): { line: number; address: number; end: number } | undefined {
26
- if (program.length < 8 || program[0] !== 0x01 || program[1] !== 0x08) return undefined;
27
- const number = program[4]! | (program[5]! << 8);
28
- const end = program.indexOf(0, 6);
29
- if (end < 0) return undefined;
30
- const sys = program.subarray(6, end).indexOf(0x9e);
31
- if (sys < 0) return undefined;
32
- const digits = Buffer.from(program.subarray(6 + sys + 1, end)).toString("latin1").replace(/[ (]/g, "");
33
- const match = /^(\d{1,5})/.exec(digits);
34
- if (match === null || Number(match[1]) > 0xffff) return undefined;
35
- return { line: number, address: Number(match[1]), end: 0x0801 + end - 2 };
36
- }
37
-
38
- /** The local packing evidence of one PRG. */
39
- export interface PackingEvidence {
40
- loadRange: { start: string; end: string };
41
- bytes: number;
42
- /** The first BASIC line and its constant SYS address, or null when the program has none. */
43
- basicStart: { line: number; sys: string } | null;
44
- entropy: { blocks: number; highBlocks: number; median: number | null };
45
- packing: "likely" | "unlikely" | "unclear";
46
- evidence: string;
47
- }
48
-
49
- export function inspect(program: Uint8Array): PackingEvidence {
50
- if (program.length < 3) throw new PackingError("the file is too short for a PRG");
51
- const load = program[0]! | (program[1]! << 8);
52
- const end = load + program.length - 3;
53
- const stub = basicSys(program);
54
- // Without a BASIC start, all bytes count; with one, only the bytes after the BASIC line.
55
- const from = stub === undefined ? 2 : Math.min(program.length, stub.end - load + 2 + 3);
56
- const body = program.subarray(from);
57
- const blocks: number[] = [];
58
- for (let at = 0; at + BLOCK <= body.length; at += BLOCK) blocks.push(entropy(body.subarray(at, at + BLOCK)));
59
- const high = blocks.filter((value) => value > HIGH_ENTROPY).length;
60
- const share = blocks.length === 0 ? 0 : high / blocks.length;
61
- const packing = blocks.length < 2 ? "unclear" : share >= 0.75 ? "likely" : share <= 0.1 ? "unlikely" : "unclear";
62
- const sorted = [...blocks].sort((a, b) => a - b);
63
- return {
64
- loadRange: { start: formatC64Address(load), end: formatC64Address(Math.min(end, 0xffff)) },
65
- bytes: program.length - 2,
66
- basicStart: stub === undefined ? null : { line: stub.line, sys: formatC64Address(stub.address) },
67
- entropy: {
68
- blocks: blocks.length,
69
- highBlocks: high,
70
- median: sorted.length === 0 ? null : Number(sorted[sorted.length >> 1]!.toFixed(2)),
71
- },
72
- packing,
73
- evidence:
74
- packing === "likely"
75
- ? `${high} of ${blocks.length} blocks of 256 bytes look compressed (above ${HIGH_ENTROPY} bits per byte); code and data are usually below.`
76
- : packing === "unlikely"
77
- ? `Only ${high} of ${blocks.length} blocks look compressed; the bytes look like code and data. A simple cruncher can look the same: run trace to see if the program writes code and then executes it.`
78
- : "The entropy does not show clearly if the program is packed. Run it and look at what it writes to memory.",
79
- };
80
- }
81
-
82
- /** One range of the emulator's memory map: what the program did with these addresses. */
83
- export interface AccessRange {
84
- start: number;
85
- end: number;
86
- read: boolean;
87
- write: boolean;
88
- execute: boolean;
89
- }
90
-
91
- /** The addresses that one memory map read covers before it is split. */
92
- const MEMMAP_WINDOW = 0x1000;
93
-
94
- /**
95
- * Reads the memory map of $0000-$ffff in windows, in address order. `read`
96
- * gives at most `maxRanges` ranges for one window; a window that gives that
97
- * many can have more, so it is read again as two halves.
98
- */
99
- export async function readMemoryMap(read: (start: number, end: number) => Promise<AccessRange[]>, maxRanges: number): Promise<AccessRange[]> {
100
- const ranges: AccessRange[] = [];
101
- const window = async (start: number, end: number): Promise<void> => {
102
- const found = await read(start, end);
103
- if (found.length < maxRanges || start === end) {
104
- ranges.push(...found);
105
- return;
106
- }
107
- const middle = Math.floor((start + end) / 2);
108
- await window(start, middle);
109
- await window(middle + 1, end);
110
- };
111
- for (let start = 0; start <= 0xffff; start += MEMMAP_WINDOW) await window(start, start + MEMMAP_WINDOW - 1);
112
- return ranges;
113
- }
114
-
115
- /** ROM areas: an address here can be written (RAM below) and executed (ROM) without one being the other. */
116
- export const underRom = (address: number) => (address >= 0xa000 && address <= 0xbfff) || address >= 0xe000;
117
-
118
- /**
119
- * The memory that the program wrote and then executed, in pieces of code.
120
- * `bytes` and `running` count only RAM outside the ROM areas: a write under
121
- * a ROM and code that runs in that ROM are not one piece of code.
122
- */
123
- export function writtenThenExecuted(ranges: AccessRange[], pc: number): { written: Array<{ start: number; end: number }>; bytes: number; running?: { start: number; end: number } } {
124
- // VICE marks only opcode bytes as executed; a gap of at most two operand bytes stays inside one piece of code.
125
- const written: Array<{ start: number; end: number }> = [];
126
- for (const range of ranges.filter((candidate) => candidate.write && candidate.execute)) {
127
- const last = written.at(-1);
128
- if (last !== undefined && range.start - last.end <= 3 && underRom(range.start) === underRom(last.start)) last.end = range.end;
129
- else written.push({ start: range.start, end: range.end });
130
- }
131
- const counted = written.filter((range) => !underRom(range.start));
132
- const bytes = counted.reduce((sum, range) => sum + range.end - range.start + 1, 0);
133
- const running = counted.find((range) => pc >= range.start && pc <= range.end);
134
- return { written, bytes, ...(running === undefined ? {} : { running }) };
135
- }