@henols/vice-mcp 0.1.11 → 0.2.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/backend-detect.mts +58 -8
- package/capability-registry.ts +388 -0
- package/disasm-decoder.ts +28 -4
- package/package.json +12 -1
- package/resources/backend-detect.mjs +30 -2
- package/resources/broker-launch.mjs +166 -34
- package/resources/vice-broker.mjs +25 -4
- package/stock-cia.ts +598 -0
- package/stock-connect.ts +137 -20
- package/stock-derived.ts +67 -14
- package/stock-diagnose.ts +994 -0
- package/stock-dispatch.ts +105 -4
- package/stock-handler.ts +29 -0
- package/stock-memory-search.ts +441 -0
- package/stock-memory.ts +92 -13
- package/stock-protocol.ts +328 -0
- package/stock-recycle.ts +500 -0
- package/stock-run-until.ts +400 -0
- package/stock-runstate.ts +46 -2
- package/stock-sprites.ts +712 -0
- package/stock-symbols.ts +431 -0
- package/stock-timing.ts +562 -0
- package/stock-vicii.ts +318 -0
- package/tools-manifest.stock.json +3031 -223
- package/version.ts +279 -0
- package/vice-proxy.ts +49 -6
package/stock-memory.ts
CHANGED
|
@@ -25,6 +25,20 @@
|
|
|
25
25
|
// exactly how an answer ships without `runState`.
|
|
26
26
|
// - Never send an EXIT to "restore" the machine after a read (D-05) -- a
|
|
27
27
|
// read leaves the machine halted, and the answer says so via runState.
|
|
28
|
+
// - CR-01 (2026-08-17): a chip-state or VIC-fetch read must NEVER pass a
|
|
29
|
+
// literal bank id and must NEVER default to `0x0000` -- bank 0 is the
|
|
30
|
+
// CPU view and follows `$00`/`$01` banking, so with I/O banked out it
|
|
31
|
+
// silently returns the RAM underneath $D000-$DFFF as if it were chip
|
|
32
|
+
// registers. Such a caller MUST use resolveRequiredBank() below and
|
|
33
|
+
// refuse when the emulator's own catalog has no `io` bank -- never
|
|
34
|
+
// guess, never fall back to bank 0.
|
|
35
|
+
// - WR-01 (2026-08-17): never REPORT or LIST banks out of the catalog's
|
|
36
|
+
// `byId` map. Stock VICE reports several names for one wire id (3.9:
|
|
37
|
+
// both `default` and `cpu` are id 0), so an id-keyed map is lossy by
|
|
38
|
+
// construction -- enumerating it made vice_memory_banks answer 5 banks
|
|
39
|
+
// where the emulator enumerated 6, and made resolveRequiredBank()'s
|
|
40
|
+
// refusal tell an agent a working bank name did not exist. Anything
|
|
41
|
+
// agent-facing reads `entries`, the verbatim wire list.
|
|
28
42
|
import { CommandType, memGetBody, memSetBody } from "./stock-protocol.ts";
|
|
29
43
|
import { parseAddress, parseByteCount } from "./stock-address.ts";
|
|
30
44
|
import { convertWireError, isErrorText, stockAnswer, type StockSessionHandler, type StockToolResult } from "./stock-handler.ts";
|
|
@@ -51,7 +65,18 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
51
65
|
|
|
52
66
|
export interface BankCatalog {
|
|
53
67
|
byName: Map<string, number>;
|
|
68
|
+
/** ONE name per id, for the reverse lookup only. Real stock VICE reports
|
|
69
|
+
* MORE THAN ONE name for the same wire id (3.9 reports both `default` and
|
|
70
|
+
* `cpu` for id 0), so this map is LOSSY BY CONSTRUCTION -- never enumerate
|
|
71
|
+
* it to report "the banks the emulator has" (WR-01, 2026-08-17: doing
|
|
72
|
+
* exactly that made vice_memory_banks answer 5 banks where the emulator
|
|
73
|
+
* enumerated 6, and made a refusal claim a working bank name did not
|
|
74
|
+
* exist). Use `entries` for anything that reports or lists. */
|
|
54
75
|
byId: Map<number, string>;
|
|
76
|
+
/** Every (id, name) pair the emulator reported, in wire order -- aliases
|
|
77
|
+
* included. This is the faithful record of the enumeration and the only
|
|
78
|
+
* thing that may be reported to a caller. */
|
|
79
|
+
entries: { id: number; name: string }[];
|
|
55
80
|
}
|
|
56
81
|
|
|
57
82
|
/** The one place this file's per-session cache storage is defined -- an
|
|
@@ -93,22 +118,34 @@ export async function bankCatalogFor(session: StockConnectSession): Promise<Bank
|
|
|
93
118
|
|
|
94
119
|
const byName = new Map<string, number>();
|
|
95
120
|
const byId = new Map<number, string>();
|
|
121
|
+
const entries: { id: number; name: string }[] = [];
|
|
96
122
|
for (const bank of response.banks) {
|
|
97
123
|
byName.set(bank.name.toLowerCase(), bank.id);
|
|
98
|
-
|
|
124
|
+
// WR-01: FIRST name per id wins here, so the reverse lookup is stable
|
|
125
|
+
// rather than "whichever alias the emulator listed last". Aliases are
|
|
126
|
+
// never lost -- they all live in `entries`.
|
|
127
|
+
if (!byId.has(bank.id)) {
|
|
128
|
+
byId.set(bank.id, bank.name);
|
|
129
|
+
}
|
|
130
|
+
entries.push({ id: bank.id, name: bank.name });
|
|
99
131
|
}
|
|
100
132
|
|
|
101
|
-
const catalog: BankCatalog = { byName, byId };
|
|
133
|
+
const catalog: BankCatalog = { byName, byId, entries };
|
|
102
134
|
bankCatalogs.set(session, catalog);
|
|
103
135
|
return catalog;
|
|
104
136
|
}
|
|
105
137
|
|
|
106
|
-
/** Shared bank-argument resolution for
|
|
107
|
-
* `bank` resolves to wire id 0x0000, a non-string `bank`
|
|
108
|
-
* unknown name refuses listing the names the catalog actually
|
|
109
|
-
* never a hardcoded table. Factored once so
|
|
110
|
-
*
|
|
111
|
-
|
|
138
|
+
/** Shared bank-argument resolution for every handler whose `bank` argument is
|
|
139
|
+
* OPTIONAL: omitted `bank` resolves to wire id 0x0000, a non-string `bank`
|
|
140
|
+
* refuses, and an unknown name refuses listing the names the catalog actually
|
|
141
|
+
* returned -- never a hardcoded table. Factored once so
|
|
142
|
+
* handleMemoryRead/Write, and stock-memory-search.ts's
|
|
143
|
+
* vice_memory_search/vice_memory_compare (WR-06), do not each re-derive the
|
|
144
|
+
* same three branches. An omitted bank is the CPU view, which is a defensible
|
|
145
|
+
* DEFAULT for a caller who asked to read memory as the CPU sees it -- but it
|
|
146
|
+
* is never correct for a chip-state or VIC-fetch read: those call
|
|
147
|
+
* resolveRequiredBank() below instead, where an absent catalog entry refuses. */
|
|
148
|
+
export async function resolveBank(
|
|
112
149
|
toolName: string,
|
|
113
150
|
bankArg: unknown,
|
|
114
151
|
session: StockConnectSession,
|
|
@@ -119,7 +156,30 @@ async function resolveBank(
|
|
|
119
156
|
if (typeof bankArg !== "string") {
|
|
120
157
|
return { ok: false, result: isErrorText(`${toolName}: bank must be a string, got ${typeof bankArg}`) };
|
|
121
158
|
}
|
|
159
|
+
return await resolveRequiredBank(toolName, bankArg, session);
|
|
160
|
+
}
|
|
122
161
|
|
|
162
|
+
/**
|
|
163
|
+
* The one exported seam that turns a REQUIRED bank NAME into the emulator's
|
|
164
|
+
* own wire bank id, or refuses (CR-01, 2026-08-17, plan_decision_D-05-14).
|
|
165
|
+
* Unlike resolveBank() above -- whose contract is "an omitted bank means
|
|
166
|
+
* wire id 0x0000", correct for vice_memory_read/write where the caller
|
|
167
|
+
* asked for the CPU view -- this function's `bankName` is MANDATORY, and an
|
|
168
|
+
* absent catalog entry is a REFUSAL, never a fallback. Consumers that need
|
|
169
|
+
* a specific chip's register view (vice_vicii_get_state,
|
|
170
|
+
* vice_cia_get_state) call this directly; they must never default to bank
|
|
171
|
+
* 0x0000, which follows $00/$01 banking and returns the RAM underneath
|
|
172
|
+
* $D000-$DFFF whenever the running program has banked I/O out.
|
|
173
|
+
*
|
|
174
|
+
* Bank ids are never hardcoded here -- they come only from the emulator's
|
|
175
|
+
* own BANKS_AVAILABLE catalog (bankCatalogFor()'s per-session cache), since
|
|
176
|
+
* ids are build- and machine-specific.
|
|
177
|
+
*/
|
|
178
|
+
export async function resolveRequiredBank(
|
|
179
|
+
toolName: string,
|
|
180
|
+
bankName: string,
|
|
181
|
+
session: StockConnectSession,
|
|
182
|
+
): Promise<{ ok: true; id: number; name: string } | { ok: false; result: StockToolResult }> {
|
|
123
183
|
let catalog: BankCatalog;
|
|
124
184
|
try {
|
|
125
185
|
catalog = await bankCatalogFor(session);
|
|
@@ -127,12 +187,28 @@ async function resolveBank(
|
|
|
127
187
|
return { ok: false, result: convertWireError(toolName, err) };
|
|
128
188
|
}
|
|
129
189
|
|
|
130
|
-
const
|
|
190
|
+
const requested = bankName.toLowerCase();
|
|
191
|
+
const resolved = catalog.byName.get(requested);
|
|
131
192
|
if (resolved === undefined) {
|
|
132
|
-
|
|
133
|
-
|
|
193
|
+
// WR-01: listed from `entries`, NOT from byId -- byId collapses aliases
|
|
194
|
+
// sharing a wire id, so listing it told an agent that a bank name which
|
|
195
|
+
// resolves perfectly well (VICE 3.9's `default`, id 0) does not exist.
|
|
196
|
+
const names = catalog.entries.map((bank) => bank.name).join(", ") || "(none reported)";
|
|
197
|
+
return {
|
|
198
|
+
ok: false,
|
|
199
|
+
result: isErrorText(
|
|
200
|
+
`${toolName}: unknown bank "${bankName}" -- refusing rather than reading the banking-dependent CPU view. ` +
|
|
201
|
+
`The CPU view (bank 0) returns the RAM underneath $D000-$DFFF whenever the running program has banked ` +
|
|
202
|
+
`I/O out via $01 -- available banks: ${names}`,
|
|
203
|
+
),
|
|
204
|
+
};
|
|
134
205
|
}
|
|
135
|
-
|
|
206
|
+
// WR-01: echo the wire spelling of the name the CALLER asked for, not
|
|
207
|
+
// "whatever name byId happens to hold for this id" -- asking for `default`
|
|
208
|
+
// and being answered `cpu` looks like the resolver silently substituted a
|
|
209
|
+
// different bank.
|
|
210
|
+
const match = catalog.entries.find((bank) => bank.id === resolved && bank.name.toLowerCase() === requested);
|
|
211
|
+
return { ok: true, id: resolved, name: match?.name ?? bankName };
|
|
136
212
|
}
|
|
137
213
|
|
|
138
214
|
// ---------------------------------------------------------------------------
|
|
@@ -155,7 +231,10 @@ export const handleMemoryBanks: StockSessionHandler = async (args, session, _dep
|
|
|
155
231
|
return convertWireError("vice_memory_banks", err);
|
|
156
232
|
}
|
|
157
233
|
|
|
158
|
-
|
|
234
|
+
// WR-01: report the emulator's OWN enumeration verbatim, in wire order,
|
|
235
|
+
// aliases included -- never `byId`, which keeps one name per id and so
|
|
236
|
+
// answered 5 banks on a machine that enumerated 6.
|
|
237
|
+
const banks = catalog.entries.map(({ id, name }) => ({ id, name }));
|
|
159
238
|
return stockAnswer(session.client, { banks, count: banks.length });
|
|
160
239
|
};
|
|
161
240
|
|
package/stock-protocol.ts
CHANGED
|
@@ -914,6 +914,60 @@ function requireAsciiFilename(callerName: string, filename: string): Buffer {
|
|
|
914
914
|
return filenameBuf;
|
|
915
915
|
}
|
|
916
916
|
|
|
917
|
+
/** Shared name guard for resourceGetBody(): refuses a non-string, a length
|
|
918
|
+
* of 0 or over 255 (name_length is a uint8), and any byte outside printable
|
|
919
|
+
* ASCII (0x20-0x7e), naming the offending value/index and the valid bound --
|
|
920
|
+
* the same discipline as requireAsciiFilename() above. Returns the encoded
|
|
921
|
+
* ASCII Buffer so the caller never re-encodes. */
|
|
922
|
+
function requireResourceName(name: unknown): Buffer {
|
|
923
|
+
if (typeof name !== "string") {
|
|
924
|
+
throw new StockEncodingError(`resourceGetBody: name must be a string, got ${typeof name}`, { field: "name" });
|
|
925
|
+
}
|
|
926
|
+
if (name.length === 0) {
|
|
927
|
+
throw new StockEncodingError("resourceGetBody: name must not be empty", { field: "name" });
|
|
928
|
+
}
|
|
929
|
+
for (let index = 0; index < name.length; index += 1) {
|
|
930
|
+
const codePoint = name.charCodeAt(index);
|
|
931
|
+
if (codePoint < 0x20 || codePoint > 0x7e) {
|
|
932
|
+
throw new StockEncodingError(
|
|
933
|
+
`resourceGetBody: name contains a non-printable-ASCII character at index ${index} (code point ${codePoint}) -- printable ASCII is 0x20-0x7e`,
|
|
934
|
+
{ field: "name" },
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
const nameBuf = Buffer.from(name, "ascii");
|
|
939
|
+
if (nameBuf.length > 255) {
|
|
940
|
+
throw new StockEncodingError(`resourceGetBody: name exceeds 255 bytes (${nameBuf.length}) -- name_length is a uint8`, { field: "name" });
|
|
941
|
+
}
|
|
942
|
+
return nameBuf;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
export interface ResourceGetBodyOptions {
|
|
946
|
+
name: string;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* RESOURCE_GET (0x51) request body -- `name_length(1) name(ASCII, NOT
|
|
951
|
+
* NUL-terminated)`. [CITED monitor_binary.c:918-935]
|
|
952
|
+
*
|
|
953
|
+
* READ-SIDE ONLY: this encoder exists so this phase's sole production
|
|
954
|
+
* caller can read `MachineVideoStandard` to pick the right cycles-per-line
|
|
955
|
+
* and lines-per-frame constants. There is no `RESOURCE_SET` (0x52) encoder
|
|
956
|
+
* in this tree and this plan does not add one -- the SET side of
|
|
957
|
+
* `MachineVideoStandard`, `VICIIModel` and `MachinePowerFrequency` reaches
|
|
958
|
+
* `machine_trigger_reset(POWER_CYCLE)` one call deep (`c64/c64.c:1367`) and
|
|
959
|
+
* destroys all emulation state (CLAUDE.md's Safety constraint). Do not add
|
|
960
|
+
* a `resourceSetBody()` or a `case ResponseType.ResourceSet` beside this one
|
|
961
|
+
* without re-deriving that deny-list boundary first.
|
|
962
|
+
*/
|
|
963
|
+
export function resourceGetBody({ name }: ResourceGetBodyOptions): Buffer {
|
|
964
|
+
const nameBuf = requireResourceName(name);
|
|
965
|
+
const body = Buffer.alloc(1 + nameBuf.length);
|
|
966
|
+
body[0] = nameBuf.length;
|
|
967
|
+
nameBuf.copy(body, 1);
|
|
968
|
+
return body;
|
|
969
|
+
}
|
|
970
|
+
|
|
917
971
|
// CHECKPOINT_LIST (0x14), PING (0x81), BANKS_AVAILABLE (0x82),
|
|
918
972
|
// EXECUTE_UNTIL_RETURN (0x73) and EXIT (0xaa) take EMPTY bodies --
|
|
919
973
|
// deliberately no encoder for any of the five: ViceMonitorClient.send()
|
|
@@ -1046,6 +1100,73 @@ export interface ParsedUndumpResponse extends ParsedBaseResponse {
|
|
|
1046
1100
|
programCounter: number;
|
|
1047
1101
|
}
|
|
1048
1102
|
|
|
1103
|
+
/**
|
|
1104
|
+
* One CPUHISTORY_GET (0x86) entry. `cycle` is the monotonic absolute clock
|
|
1105
|
+
* value Phase 7's Route A stopwatch reads -- a `bigint` via
|
|
1106
|
+
* `readBigUInt64LE`, never narrowed to `Number`, since a uint64 clock does
|
|
1107
|
+
* not fit a JS number safely and the stopwatch's whole value is exactness.
|
|
1108
|
+
* The per-entry register block (the `regCount`-prefixed items between the
|
|
1109
|
+
* `item_size` byte and `cycle`, written by `write_registers()`) is
|
|
1110
|
+
* deliberately NOT decoded here: VICE hard-fills `LIN`/`CYC` inside
|
|
1111
|
+
* CPU-history entries with the sentinel `0xffff`
|
|
1112
|
+
* (`monitor_binary.c:1585-1590`), so those per-entry raster positions carry
|
|
1113
|
+
* no real value and this phase never interprets them.
|
|
1114
|
+
*
|
|
1115
|
+
* `instructionLength` is NOT the real length of the decoded 6502
|
|
1116
|
+
* instruction -- VICE hardcodes `uint8_t instruction_length = 4;`
|
|
1117
|
+
* (`monitor_binary.c:1468`) regardless of what `opcode` actually is. Nothing
|
|
1118
|
+
* may treat this field as an instruction size.
|
|
1119
|
+
*
|
|
1120
|
+
* Layout re-derived 2026-08-18 from `monitor_binary_process_cpuhistory()`
|
|
1121
|
+
* (`monitor_binary.c:1452-1620`) and verified against the real committed
|
|
1122
|
+
* captures `fixtures/binmon/cpuhistory-get.bin` and `cpuhistory-get-multi.bin`
|
|
1123
|
+
* (plan 07-12). The PREVIOUS layout this comment cited was disproven live
|
|
1124
|
+
* against a genuine VICE 3.10 build -- see
|
|
1125
|
+
* `.planning/phases/07-cycle-timing-and-wedge-triage/deferred-items.md`,
|
|
1126
|
+
* "Route A (CPUHISTORY_GET) live decode mismatch" -- do not re-trust it.
|
|
1127
|
+
*/
|
|
1128
|
+
export interface ParsedCpuHistoryEntry {
|
|
1129
|
+
cycle: bigint;
|
|
1130
|
+
opcode: number;
|
|
1131
|
+
instructionLength: number;
|
|
1132
|
+
p1: number;
|
|
1133
|
+
p2: number;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* CPUHISTORY_GET (0x86) parsed shape. CORRECTED 2026-08-18 (plan 07-12): the
|
|
1138
|
+
* real `cpuhistory-get-multi.bin` capture (`count:4`) decodes to
|
|
1139
|
+
* STRICTLY ASCENDING `cycle` values across `entries[0..3]` -- i.e.
|
|
1140
|
+
* `entries[0]` is the OLDEST of the returned window and `entries[count-1]`
|
|
1141
|
+
* is the NEWEST, the opposite of this comment's previous unverified
|
|
1142
|
+
* "entries[0] is the newest" claim. Route A's live stopwatch
|
|
1143
|
+
* (`stock-timing.ts`'s `readCycleBaseline()`) is UNAFFECTED by this
|
|
1144
|
+
* correction: it always requests `CPUHISTORY_GET(count:1)`, never a larger
|
|
1145
|
+
* count, so `entries[0]` is the only entry either way. Layout re-derived
|
|
1146
|
+
* from `monitor_binary_process_cpuhistory()` (`monitor_binary.c:1452-1620`)
|
|
1147
|
+
* and verified against the real captures `fixtures/binmon/cpuhistory-get.bin`
|
|
1148
|
+
* (single entry) and `cpuhistory-get-multi.bin` (multi-entry, the stride AND
|
|
1149
|
+
* order proof) -- see plan 07-12 and
|
|
1150
|
+
* `.planning/phases/07-cycle-timing-and-wedge-triage/deferred-items.md` for
|
|
1151
|
+
* the disproven earlier layout this replaces.
|
|
1152
|
+
*/
|
|
1153
|
+
export interface ParsedCpuHistoryResponse extends ParsedBaseResponse {
|
|
1154
|
+
type: "cpu_history";
|
|
1155
|
+
count: number;
|
|
1156
|
+
entries: ParsedCpuHistoryEntry[];
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* RESOURCE_GET (0x51) parsed shape. A discriminated union on `valueType` --
|
|
1161
|
+
* `e_MON_RESOURCE_TYPE_STRING` (wire byte 0) decodes to `value: string`,
|
|
1162
|
+
* `e_MON_RESOURCE_TYPE_INT` (wire byte 1) decodes to `value: number`.
|
|
1163
|
+
* [CITED monitor_binary.c:938-965]
|
|
1164
|
+
*/
|
|
1165
|
+
export type ParsedResourceGetResponse = ParsedBaseResponse & { type: "resource_get" } & (
|
|
1166
|
+
| { valueType: "integer"; value: number }
|
|
1167
|
+
| { valueType: "string"; value: string }
|
|
1168
|
+
);
|
|
1169
|
+
|
|
1049
1170
|
/** Fallback shape for any responseType this module has no specific case for
|
|
1050
1171
|
* (including VERIF-02 case 6's response type byte 0x00, which is never a
|
|
1051
1172
|
* real response/event type on the wire) -- the parser must produce this
|
|
@@ -1069,6 +1190,8 @@ export type ParsedResponse =
|
|
|
1069
1190
|
| ParsedResumedEvent
|
|
1070
1191
|
| ParsedJamEvent
|
|
1071
1192
|
| ParsedUndumpResponse
|
|
1193
|
+
| ParsedCpuHistoryResponse
|
|
1194
|
+
| ParsedResourceGetResponse
|
|
1072
1195
|
| ParsedUnknownResponse;
|
|
1073
1196
|
|
|
1074
1197
|
// ---------------------------------------------------------------------------
|
|
@@ -1319,6 +1442,209 @@ export function parseResponse({ apiVersion, responseType, errorCode, requestId,
|
|
|
1319
1442
|
case ResponseType.Undump:
|
|
1320
1443
|
need(body, 2, responseType, requestId);
|
|
1321
1444
|
return { type: "undump", requestId, errorCode, programCounter: body.readUInt16LE(0) };
|
|
1445
|
+
case ResponseType.CpuHistoryGet: {
|
|
1446
|
+
// CPUHISTORY_GET (0x86) body layout, re-derived 2026-08-18 from
|
|
1447
|
+
// monitor_binary_process_cpuhistory() (monitor_binary.c:1452-1620) and
|
|
1448
|
+
// verified against the real captures fixtures/binmon/cpuhistory-get.bin
|
|
1449
|
+
// (single entry) and cpuhistory-get-multi.bin (multi-entry, the stride
|
|
1450
|
+
// proof) -- see plan 07-12 and
|
|
1451
|
+
// .planning/phases/07-cycle-timing-and-wedge-triage/deferred-items.md's
|
|
1452
|
+
// "Route A (CPUHISTORY_GET) live decode mismatch" for the disproven
|
|
1453
|
+
// earlier layout this replaces (WR-13): that layout was NEVER
|
|
1454
|
+
// confirmed against a real reply and does not match what a genuine
|
|
1455
|
+
// VICE >= 3.10 build actually sends.
|
|
1456
|
+
//
|
|
1457
|
+
// count(u32LE) at offset 0. Then per entry: item_size(1) -- the byte
|
|
1458
|
+
// count of EVERYTHING AFTER this size byte
|
|
1459
|
+
// (response_size = 4 + count * (item_size + 1), so the entry stride is
|
|
1460
|
+
// item_size + 1) -- NOT the register-block length alone, the single
|
|
1461
|
+
// misreading that produced the previous layout. Inside one entry: a
|
|
1462
|
+
// register block written by write_registers() -- regCount(u16LE),
|
|
1463
|
+
// then regCount items of size(1)+id(1)+value(u16LE), each item's own
|
|
1464
|
+
// stride being its declared size + 1 (same item_size-is-the-wire's-own
|
|
1465
|
+
// -stride discipline as the RegisterInfo case above); then
|
|
1466
|
+
// cycle(u64LE); then instruction_length(1); then exactly
|
|
1467
|
+
// instruction_length bytes of instruction data (opcode, p1, p2, and a
|
|
1468
|
+
// trailing placeholder byte for a third parameter that exists on some
|
|
1469
|
+
// machines). instruction_length is a hardcoded constant 4 in VICE
|
|
1470
|
+
// (monitor_binary.c:1468) regardless of the decoded instruction's real
|
|
1471
|
+
// size -- see ParsedCpuHistoryEntry's doc comment.
|
|
1472
|
+
//
|
|
1473
|
+
// Every field is bounds-checked against BOTH the declared entry end
|
|
1474
|
+
// (entryEnd = offset + 1 + itemSize) and the body end via need() --
|
|
1475
|
+
// an item_size that does not leave room for its own
|
|
1476
|
+
// regCount/register-items/cycle/instruction_length is a
|
|
1477
|
+
// StockFramingError naming the observed item_size and what did not
|
|
1478
|
+
// fit, never a truncated silent success and never a RangeError
|
|
1479
|
+
// (T-07-12-03). 07-REVIEW.md WR-05 closed two holes in that claim: the
|
|
1480
|
+
// regCount read was bounds-checked against the BODY only, so a
|
|
1481
|
+
// multi-entry frame whose first entry declared item_size < 2 read its
|
|
1482
|
+
// regCount out of the NEXT entry and reported a fabricated number; and
|
|
1483
|
+
// the instruction-bytes guard used a hardcoded 3 rather than the
|
|
1484
|
+
// entry's own declared instruction_length, so an entry declaring 200
|
|
1485
|
+
// with 3 bytes present parsed successfully. Both are now
|
|
1486
|
+
// entry-relative. Do not reintroduce a literal in either guard. count is rejected above 65535 up front: VICE's own
|
|
1487
|
+
// count is a uint16_t (monitor_binary.c:1469, 1491's
|
|
1488
|
+
// little_endian_to_uint32() read), so a larger declared count is a
|
|
1489
|
+
// desynced or hostile frame and iterating it would be a
|
|
1490
|
+
// denial-of-service loop (T-07-12-01). The cursor always advances by
|
|
1491
|
+
// exactly 1 + itemSize per entry -- the stride from the source, never
|
|
1492
|
+
// a recomputed sum of the fields just read.
|
|
1493
|
+
need(body, 4, responseType, requestId);
|
|
1494
|
+
const count = body.readUInt32LE(0);
|
|
1495
|
+
if (count > 0xffff) {
|
|
1496
|
+
throw new StockFramingError(
|
|
1497
|
+
`CPUHISTORY_GET declared count ${count}, exceeding VICE's own uint16_t ceiling of 65535 (monitor_binary.c:1469) -- desynced or hostile frame`,
|
|
1498
|
+
{ observed: count, expected: 0xffff, responseType, requestId },
|
|
1499
|
+
);
|
|
1500
|
+
}
|
|
1501
|
+
let offset = 4;
|
|
1502
|
+
const entries: ParsedCpuHistoryEntry[] = [];
|
|
1503
|
+
for (let index = 0; index < count; index += 1) {
|
|
1504
|
+
need(body, offset + 1, responseType, requestId);
|
|
1505
|
+
const itemSize = body[offset]!;
|
|
1506
|
+
const entryEnd = offset + 1 + itemSize;
|
|
1507
|
+
need(body, entryEnd, responseType, requestId);
|
|
1508
|
+
|
|
1509
|
+
let cursor = offset + 1;
|
|
1510
|
+
// WR-05 (1): bound regCount by the DECLARED ENTRY, not just the body.
|
|
1511
|
+
// The body-relative need() alone let an entry with item_size < 2 in a
|
|
1512
|
+
// multi-entry frame read its regCount out of the NEXT entry's bytes;
|
|
1513
|
+
// the resulting diagnostic then named a fabricated register count
|
|
1514
|
+
// (observed live: "does not leave room for register item 0 of 4608",
|
|
1515
|
+
// where 4608 was assembled from bytes outside entry 0). Check the
|
|
1516
|
+
// entry first, so the message can only ever name real numbers.
|
|
1517
|
+
if (cursor + 2 > entryEnd) {
|
|
1518
|
+
throw new StockFramingError(
|
|
1519
|
+
`CPUHISTORY_GET entry ${index}'s item_size ${itemSize} does not leave room for its own 2-byte regCount field`,
|
|
1520
|
+
{ observed: itemSize, expected: 2, responseType, requestId },
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
need(body, cursor + 2, responseType, requestId);
|
|
1524
|
+
const regCount = body.readUInt16LE(cursor);
|
|
1525
|
+
cursor += 2;
|
|
1526
|
+
for (let regIndex = 0; regIndex < regCount; regIndex += 1) {
|
|
1527
|
+
if (cursor + 2 > entryEnd) {
|
|
1528
|
+
throw new StockFramingError(
|
|
1529
|
+
`CPUHISTORY_GET entry ${index}'s item_size ${itemSize} does not leave room for register item ${regIndex} of ${regCount}`,
|
|
1530
|
+
{ observed: itemSize, responseType, requestId },
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
const regItemSize = body[cursor]!;
|
|
1534
|
+
cursor += 1 + regItemSize;
|
|
1535
|
+
}
|
|
1536
|
+
if (cursor + 8 > entryEnd) {
|
|
1537
|
+
throw new StockFramingError(
|
|
1538
|
+
`CPUHISTORY_GET entry ${index}'s item_size ${itemSize} does not leave room for the 8-byte cycle field after its ${regCount}-item register block`,
|
|
1539
|
+
{ observed: itemSize, responseType, requestId },
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
need(body, cursor + 8, responseType, requestId);
|
|
1543
|
+
const cycle = body.readBigUInt64LE(cursor);
|
|
1544
|
+
cursor += 8;
|
|
1545
|
+
|
|
1546
|
+
if (cursor + 1 > entryEnd) {
|
|
1547
|
+
throw new StockFramingError(
|
|
1548
|
+
`CPUHISTORY_GET entry ${index}'s item_size ${itemSize} does not leave room for the instruction_length byte`,
|
|
1549
|
+
{ observed: itemSize, responseType, requestId },
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
need(body, cursor + 1, responseType, requestId);
|
|
1553
|
+
const instructionLength = body[cursor]!;
|
|
1554
|
+
cursor += 1;
|
|
1555
|
+
if (instructionLength < 3) {
|
|
1556
|
+
throw new StockFramingError(
|
|
1557
|
+
`CPUHISTORY_GET entry ${index} declares instruction_length ${instructionLength}, too short to hold opcode/p1/p2`,
|
|
1558
|
+
{ observed: instructionLength, expected: 3, responseType, requestId },
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
// WR-05 (2): validate against what the entry DECLARES, not a
|
|
1562
|
+
// hardcoded 3. The error message here has always claimed to check
|
|
1563
|
+
// "room for its declared instruction_length", but the guard used a
|
|
1564
|
+
// literal 3 -- so an entry declaring instruction_length 200 with only
|
|
1565
|
+
// 3 instruction bytes present PARSED SUCCESSFULLY and handed
|
|
1566
|
+
// `instructionLength: 200` to consumers (observed against the real
|
|
1567
|
+
// parser). Bound it by instructionLength, then read the 3 fields this
|
|
1568
|
+
// struct defines (opcode/p1/p2); any bytes beyond those 3 within the
|
|
1569
|
+
// declared length are the trailing third-parameter placeholder VICE
|
|
1570
|
+
// reserves and are deliberately not surfaced.
|
|
1571
|
+
if (cursor + instructionLength > entryEnd) {
|
|
1572
|
+
throw new StockFramingError(
|
|
1573
|
+
`CPUHISTORY_GET entry ${index}'s item_size ${itemSize} does not leave room for its declared instruction_length ${instructionLength} ` +
|
|
1574
|
+
`(${entryEnd - cursor} byte(s) remain in the entry)`,
|
|
1575
|
+
{ observed: itemSize, expected: instructionLength, responseType, requestId },
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
need(body, cursor + instructionLength, responseType, requestId);
|
|
1579
|
+
const opcode = body[cursor]!;
|
|
1580
|
+
const p1 = body[cursor + 1]!;
|
|
1581
|
+
const p2 = body[cursor + 2]!;
|
|
1582
|
+
|
|
1583
|
+
entries.push({ cycle, instructionLength, opcode, p1, p2 });
|
|
1584
|
+
offset = entryEnd; // advance by 1 + itemSize -- the stride from the source
|
|
1585
|
+
}
|
|
1586
|
+
return { type: "cpu_history", requestId, errorCode, count, entries };
|
|
1587
|
+
}
|
|
1588
|
+
case ResponseType.ResourceGet: {
|
|
1589
|
+
// RESOURCE_GET (0x51) response layout: type(1) size(1) payload(size
|
|
1590
|
+
// bytes). type: 0 = e_MON_RESOURCE_TYPE_STRING, 1 =
|
|
1591
|
+
// e_MON_RESOURCE_TYPE_INT. For the integer case size is always 4 and
|
|
1592
|
+
// the payload is a uint32LE; for the string case size is the payload's
|
|
1593
|
+
// own byte length. [CITED monitor_binary.c:938-965]
|
|
1594
|
+
//
|
|
1595
|
+
// CR-02 (code review, plan 07-12 Task 3): the integer branch used to
|
|
1596
|
+
// read a FIXED 4 bytes at offset 2 behind only `need(body, 2 + size,
|
|
1597
|
+
// ...)` -- a body of `[0x01, 0x00]` (size=0) satisfies that guard and
|
|
1598
|
+
// then `readUInt32LE(2)` throws a bare RangeError straight out of
|
|
1599
|
+
// parseResponse(), violating this function's own normative rule
|
|
1600
|
+
// (never add a case that reads at a fixed or wire-derived offset
|
|
1601
|
+
// without a preceding need() for the bytes it ACTUALLY reads). A
|
|
1602
|
+
// 4-byte payload is part of e_MON_RESOURCE_TYPE_INT's own contract,
|
|
1603
|
+
// not a value to read past -- so the size mismatch is checked FIRST,
|
|
1604
|
+
// independent of how many bytes the (possibly also-lying) body
|
|
1605
|
+
// physically has, naming the observed size rather than surfacing as
|
|
1606
|
+
// a generic "body too short" message. Only once size is confirmed to
|
|
1607
|
+
// be exactly 4 does need(body, 2 + 4, ...) bounds-check the actual
|
|
1608
|
+
// payload bytes before reading them.
|
|
1609
|
+
need(body, 2, responseType, requestId);
|
|
1610
|
+
const valueTypeByte = body[0]!;
|
|
1611
|
+
const size = body[1]!;
|
|
1612
|
+
if (valueTypeByte === 1) {
|
|
1613
|
+
if (size !== 4) {
|
|
1614
|
+
throw new StockFramingError(
|
|
1615
|
+
`RESOURCE_GET integer response declares size ${size}, expected 4 (a uint32LE payload)`,
|
|
1616
|
+
{ observed: size, expected: 4, responseType, requestId },
|
|
1617
|
+
);
|
|
1618
|
+
}
|
|
1619
|
+
need(body, 2 + 4, responseType, requestId);
|
|
1620
|
+
return { type: "resource_get", requestId, errorCode, valueType: "integer", value: body.readUInt32LE(2) };
|
|
1621
|
+
}
|
|
1622
|
+
// WR-06 (07-REVIEW.md): the documented contract is EXACTLY two value
|
|
1623
|
+
// types -- 0 = string, 1 = int ([CITED monitor_binary.c:938-965]
|
|
1624
|
+
// above). The CR-02 rewrite hardened `=== 1` and let everything else
|
|
1625
|
+
// fall through to the string branch, so a wire byte of 2, 7 or 0xff was
|
|
1626
|
+
// reported as `valueType: "string"` with `size` bytes of arbitrary
|
|
1627
|
+
// memory rendered as ASCII -- a MISLABELLED value where every other
|
|
1628
|
+
// out-of-contract input in this function produces a framing error, and
|
|
1629
|
+
// a direct sibling of the CR-02 rule stated at the top of this file. It
|
|
1630
|
+
// degraded safely only because the single consumer
|
|
1631
|
+
// (stock-timing.ts's resolveVideoStandard) rejects any non-integer
|
|
1632
|
+
// reply; that is a property of today's consumer, not of this parser.
|
|
1633
|
+
if (valueTypeByte !== 0) {
|
|
1634
|
+
throw new StockFramingError(
|
|
1635
|
+
`RESOURCE_GET reply declared value type ${valueTypeByte}, expected 0 (string) or 1 (integer)`,
|
|
1636
|
+
{ observed: valueTypeByte, expected: 0, responseType, requestId },
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
need(body, 2 + size, responseType, requestId);
|
|
1640
|
+
return {
|
|
1641
|
+
type: "resource_get",
|
|
1642
|
+
requestId,
|
|
1643
|
+
errorCode,
|
|
1644
|
+
valueType: "string",
|
|
1645
|
+
value: body.subarray(2, 2 + size).toString("ascii"),
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1322
1648
|
default:
|
|
1323
1649
|
return { type: "unknown", requestId, errorCode, responseType };
|
|
1324
1650
|
}
|
|
@@ -1540,6 +1866,8 @@ const RESPONSE_TYPE_OF_PARSED_KIND: Partial<Record<ParsedResponse["type"], Respo
|
|
|
1540
1866
|
resumed: ResponseType.Resumed,
|
|
1541
1867
|
jam: ResponseType.Jam,
|
|
1542
1868
|
undump: ResponseType.Undump,
|
|
1869
|
+
cpu_history: ResponseType.CpuHistoryGet,
|
|
1870
|
+
resource_get: ResponseType.ResourceGet,
|
|
1543
1871
|
};
|
|
1544
1872
|
|
|
1545
1873
|
/** The wire ResponseType byte a parsed response was decoded from, for
|