@henols/c64-re-tools 1.0.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,441 @@
1
+ // C64 platform reference data and decoders: the c64-memory-map script, and the
2
+ // platform names that c64-source uses (D52).
3
+ // Names follow "Mapping the Commodore 64". Only platform facts live here:
4
+ // an address without one belongs to the application.
5
+
6
+ import { d018Offsets, vicBankBase } from "./c64.ts";
7
+
8
+ export interface Entry {
9
+ start: number;
10
+ end: number;
11
+ name: string;
12
+ meaning: string;
13
+ }
14
+
15
+ export interface Lookup {
16
+ address: number;
17
+ kind: "platform" | "application";
18
+ area: string;
19
+ name?: string;
20
+ meaning: string;
21
+ /** For an I/O register mirror: the register it repeats. */
22
+ mirrorOf?: number;
23
+ /** The first address of a multi-byte entry. */
24
+ entryStart?: number;
25
+ }
26
+
27
+ const e = (start: number, end: number, name: string, meaning: string): Entry => ({ start, end, name, meaning });
28
+
29
+ /** Zero page and the system area $0100-$03FF. */
30
+ export const SYSTEM: Entry[] = [
31
+ e(0x00, 0x00, "D6510", "Processor port data direction register. Default $2F."),
32
+ e(0x01, 0x01, "R6510", "Processor port. Bits 0-2 (LORAM, HIRAM, CHAREN) select the memory configuration; bits 3-5 control the cassette."),
33
+ e(0x03, 0x04, "ADRAY1", "Vector to the routine that converts a floating-point number to an integer."),
34
+ e(0x05, 0x06, "ADRAY2", "Vector to the routine that converts an integer to a floating-point number."),
35
+ e(0x0d, 0x0d, "VALTYP", "BASIC data type flag: $FF string, $00 number."),
36
+ e(0x0e, 0x0e, "INTFLG", "BASIC number type flag: $80 integer, $00 floating point."),
37
+ e(0x13, 0x13, "CHANNL", "Current BASIC I/O channel (logical file number)."),
38
+ e(0x14, 0x15, "LINNUM", "Integer value: a line number, or the address for PEEK, POKE, SYS and WAIT."),
39
+ e(0x22, 0x25, "INDEX", "BASIC temporary pointers."),
40
+ e(0x2b, 0x2c, "TXTTAB", "Start of the BASIC program text. Default $0801."),
41
+ e(0x2d, 0x2e, "VARTAB", "Start of the BASIC variables (end of the program)."),
42
+ e(0x2f, 0x30, "ARYTAB", "Start of the BASIC arrays."),
43
+ e(0x31, 0x32, "STREND", "End of the BASIC arrays."),
44
+ e(0x33, 0x34, "FRETOP", "Bottom of the BASIC string storage."),
45
+ e(0x37, 0x38, "MEMSIZ", "Top of the memory that BASIC uses."),
46
+ e(0x39, 0x3a, "CURLIN", "Current BASIC line number."),
47
+ e(0x41, 0x42, "DATPTR", "Address of the next DATA item."),
48
+ e(0x61, 0x66, "FAC1", "Floating-point accumulator 1."),
49
+ e(0x69, 0x6e, "FAC2", "Floating-point accumulator 2."),
50
+ e(0x73, 0x8a, "CHRGET", "The CHRGET routine that BASIC copies from ROM; reads the next character of BASIC text."),
51
+ e(0x7a, 0x7b, "TXTPTR", "Pointer to the current character of BASIC text (inside CHRGET)."),
52
+ e(0x8b, 0x8f, "RNDX", "Seed of the BASIC RND function."),
53
+ e(0x90, 0x90, "STATUS", "KERNAL I/O status byte (BASIC ST)."),
54
+ e(0x91, 0x91, "STKEY", "STOP key flag: $7F when STOP is pressed."),
55
+ e(0x93, 0x93, "VERCK", "KERNAL flag: 0 for LOAD, 1 for VERIFY."),
56
+ e(0x98, 0x98, "LDTND", "Number of open files."),
57
+ e(0x99, 0x99, "DFLTN", "Default input device (0 = keyboard)."),
58
+ e(0x9a, 0x9a, "DFLTO", "Default output device (3 = screen)."),
59
+ e(0x9d, 0x9d, "MSGFLG", "KERNAL message control: bit 7 for direct-mode messages, bit 6 for error messages."),
60
+ e(0xa0, 0xa2, "TIME", "Jiffy clock, 1/60 s, most significant byte first (BASIC TI)."),
61
+ e(0xae, 0xaf, "EAL", "End address of the last LOAD or SAVE."),
62
+ e(0xb2, 0xb3, "TAPE1", "Pointer to the cassette buffer."),
63
+ e(0xb7, 0xb7, "FNLEN", "Length of the current file name."),
64
+ e(0xb8, 0xb8, "LA", "Current logical file number."),
65
+ e(0xb9, 0xb9, "SA", "Current secondary address."),
66
+ e(0xba, 0xba, "FA", "Current device number (8 = first disk drive)."),
67
+ e(0xbb, 0xbc, "FNADR", "Pointer to the current file name."),
68
+ e(0xc1, 0xc2, "STAL", "Start address for LOAD and SAVE."),
69
+ e(0xc3, 0xc4, "MEMUSS", "Load address of a KERNAL LOAD."),
70
+ e(0xc5, 0xc5, "LSTX", "Matrix code of the last key pressed."),
71
+ e(0xc6, 0xc6, "NDX", "Number of characters in the keyboard buffer."),
72
+ e(0xc7, 0xc7, "RVS", "Reverse character flag."),
73
+ e(0xcb, 0xcb, "SFDX", "Matrix code of the current key ($40 = no key)."),
74
+ e(0xcc, 0xcc, "BLNSW", "Cursor blink enable: 0 blinks the cursor."),
75
+ e(0xcd, 0xcd, "BLNCT", "Countdown to the next cursor blink."),
76
+ e(0xce, 0xce, "GDBLN", "Character under the cursor."),
77
+ e(0xcf, 0xcf, "BLNON", "Cursor blink phase."),
78
+ e(0xd1, 0xd2, "PNT", "Pointer to the screen line of the cursor."),
79
+ e(0xd3, 0xd3, "PNTR", "Cursor column on the logical line."),
80
+ e(0xd4, 0xd4, "QTSW", "Quote mode flag."),
81
+ e(0xd5, 0xd5, "LNMX", "Length of the logical screen line (39 or 79)."),
82
+ e(0xd6, 0xd6, "TBLX", "Cursor row."),
83
+ e(0xd8, 0xd8, "INSRT", "Number of pending inserts."),
84
+ e(0xd9, 0xf2, "LDTB1", "Screen line link table."),
85
+ e(0xf3, 0xf4, "USER", "Pointer to the color RAM line of the cursor."),
86
+ e(0xf5, 0xf6, "KEYTAB", "Pointer to the keyboard decode table."),
87
+ e(0xf7, 0xf8, "RIBUF", "Pointer to the RS-232 input buffer."),
88
+ e(0xf9, 0xfa, "ROBUF", "Pointer to the RS-232 output buffer."),
89
+ e(0xfb, 0xfe, "FREEZP", "Free zero page: the system does not use these bytes."),
90
+ e(0xff, 0xff, "BASZPT", "Temporary byte for floating-point to string conversion."),
91
+ e(0x0100, 0x01ff, "STACK", "6510 stack. BASIC also uses $0100-$010A to convert numbers to text."),
92
+ e(0x0200, 0x0258, "BUF", "BASIC input buffer."),
93
+ e(0x0259, 0x0262, "LAT", "Table of logical file numbers of the open files."),
94
+ e(0x0263, 0x026c, "FAT", "Table of device numbers of the open files."),
95
+ e(0x026d, 0x0276, "SAT", "Table of secondary addresses of the open files."),
96
+ e(0x0277, 0x0280, "KEYD", "Keyboard buffer."),
97
+ e(0x0281, 0x0282, "MEMSTR", "Start of the memory for the operating system."),
98
+ e(0x0283, 0x0284, "MEMSIZ", "Top of the memory for the operating system."),
99
+ e(0x0286, 0x0286, "COLOR", "Current text color."),
100
+ e(0x0287, 0x0287, "GDCOL", "Color under the cursor."),
101
+ e(0x0288, 0x0288, "HIBASE", "High byte of the screen memory address for the screen editor. Default 4 ($0400)."),
102
+ e(0x0289, 0x0289, "XMAX", "Largest number of characters in the keyboard buffer (10)."),
103
+ e(0x028a, 0x028a, "RPTFLG", "Key repeat control: $80 repeats all keys."),
104
+ e(0x028d, 0x028d, "SHFLAG", "SHIFT, C= and CTRL key flags."),
105
+ e(0x028f, 0x0290, "KEYLOG", "Vector to the keyboard table setup routine."),
106
+ e(0x0291, 0x0291, "MODE", "Lock of the SHIFT and C= character set switch."),
107
+ e(0x02a6, 0x02a6, "PALNTS", "Video standard flag: 0 NTSC, 1 PAL."),
108
+ e(0x0300, 0x0301, "IERROR", "Vector to the BASIC error message routine."),
109
+ e(0x0302, 0x0303, "IMAIN", "Vector to the BASIC main loop (warm start)."),
110
+ e(0x0304, 0x0305, "ICRNCH", "Vector to the BASIC tokenizer."),
111
+ e(0x0306, 0x0307, "IQPLOP", "Vector to the BASIC LIST token printer."),
112
+ e(0x0308, 0x0309, "IGONE", "Vector to the BASIC statement executor."),
113
+ e(0x030a, 0x030b, "IEVAL", "Vector to the BASIC expression element evaluator."),
114
+ e(0x030c, 0x030c, "SAREG", "A register for SYS, and A after SYS returns."),
115
+ e(0x030d, 0x030d, "SXREG", "X register for SYS."),
116
+ e(0x030e, 0x030e, "SYREG", "Y register for SYS."),
117
+ e(0x030f, 0x030f, "SPREG", "Status register for SYS."),
118
+ e(0x0310, 0x0310, "USRPOK", "JMP instruction of the BASIC USR function."),
119
+ e(0x0311, 0x0312, "USRADD", "Address that the BASIC USR function calls."),
120
+ e(0x0314, 0x0315, "CINV", "IRQ vector. Default $EA31 (the KERNAL IRQ handler)."),
121
+ e(0x0316, 0x0317, "CBINV", "BRK vector. Default $FE66."),
122
+ e(0x0318, 0x0319, "NMINV", "NMI vector. Default $FE47."),
123
+ e(0x031a, 0x031b, "IOPEN", "Vector of the KERNAL OPEN routine."),
124
+ e(0x031c, 0x031d, "ICLOSE", "Vector of the KERNAL CLOSE routine."),
125
+ e(0x031e, 0x031f, "ICHKIN", "Vector of the KERNAL CHKIN routine."),
126
+ e(0x0320, 0x0321, "ICKOUT", "Vector of the KERNAL CHKOUT routine."),
127
+ e(0x0322, 0x0323, "ICLRCH", "Vector of the KERNAL CLRCHN routine."),
128
+ e(0x0324, 0x0325, "IBASIN", "Vector of the KERNAL CHRIN routine."),
129
+ e(0x0326, 0x0327, "IBSOUT", "Vector of the KERNAL CHROUT routine."),
130
+ e(0x0328, 0x0329, "ISTOP", "Vector of the KERNAL STOP routine."),
131
+ e(0x032a, 0x032b, "IGETIN", "Vector of the KERNAL GETIN routine."),
132
+ e(0x032c, 0x032d, "ICLALL", "Vector of the KERNAL CLALL routine."),
133
+ e(0x032e, 0x032f, "USRCMD", "User-defined vector."),
134
+ e(0x0330, 0x0331, "ILOAD", "Vector of the KERNAL LOAD routine."),
135
+ e(0x0332, 0x0333, "ISAVE", "Vector of the KERNAL SAVE routine."),
136
+ e(0x033c, 0x03fb, "TBUFFR", "Cassette buffer. Programs often use it when no tape is in use."),
137
+ e(0x0400, 0x07e7, "VICSCN", "Default screen memory (40 x 25 characters)."),
138
+ e(0x07f8, 0x07ff, "SPRPTR", "Default sprite pointers (for screen memory at $0400)."),
139
+ ];
140
+
141
+ /** KERNAL jump table, ROM routines and hardware vectors. */
142
+ export const ROM: Entry[] = [
143
+ ...[
144
+ ["CINT", "Initializes the screen editor and the VIC-II."],
145
+ ["IOINIT", "Initializes the CIAs and the I/O devices."],
146
+ ["RAMTAS", "Tests RAM and sets the memory pointers."],
147
+ ["RESTOR", "Sets the standard KERNAL vectors at $0314-$0333."],
148
+ ["VECTOR", "Reads or sets the KERNAL vectors."],
149
+ ["SETMSG", "Sets the KERNAL message control."],
150
+ ["SECOND", "Sends a secondary address after LISTEN."],
151
+ ["TKSA", "Sends a secondary address after TALK."],
152
+ ["MEMTOP", "Reads or sets the top of memory."],
153
+ ["MEMBOT", "Reads or sets the bottom of memory."],
154
+ ["SCNKEY", "Scans the keyboard."],
155
+ ["SETTMO", "Sets the IEEE timeout flag."],
156
+ ["ACPTR", "Reads a byte from the serial bus."],
157
+ ["CIOUT", "Writes a byte to the serial bus."],
158
+ ["UNTLK", "Sends UNTALK on the serial bus."],
159
+ ["UNLSN", "Sends UNLISTEN on the serial bus."],
160
+ ["LISTEN", "Sends LISTEN on the serial bus."],
161
+ ["TALK", "Sends TALK on the serial bus."],
162
+ ["READST", "Reads the I/O status byte."],
163
+ ["SETLFS", "Sets the logical file, device and secondary address."],
164
+ ["SETNAM", "Sets the file name."],
165
+ ["OPEN", "Opens a logical file."],
166
+ ["CLOSE", "Closes a logical file."],
167
+ ["CHKIN", "Selects a file as the input channel."],
168
+ ["CHKOUT", "Selects a file as the output channel."],
169
+ ["CLRCHN", "Restores the default I/O channels."],
170
+ ["CHRIN", "Reads a character from the input channel."],
171
+ ["CHROUT", "Writes the character in A to the output channel."],
172
+ ["LOAD", "Loads or verifies a file into memory."],
173
+ ["SAVE", "Saves memory to a file."],
174
+ ["SETTIM", "Sets the jiffy clock."],
175
+ ["RDTIM", "Reads the jiffy clock."],
176
+ ["STOP", "Tests the STOP key."],
177
+ ["GETIN", "Reads a character from the keyboard buffer or the input channel."],
178
+ ["CLALL", "Closes all files."],
179
+ ["UDTIM", "Increments the jiffy clock."],
180
+ ["SCREEN", "Returns the screen size in columns and rows."],
181
+ ["PLOT", "Reads or sets the cursor position."],
182
+ ["IOBASE", "Returns the I/O base address ($DC00)."],
183
+ ].map(([name, meaning], index) => e(0xff81 + index * 3, 0xff83 + index * 3, name!, `KERNAL jump table: ${meaning}`)),
184
+ e(0xa000, 0xa001, "BASIC cold start vector", "Points to the BASIC cold start ($E394)."),
185
+ e(0xa002, 0xa003, "BASIC warm start vector", "Points to the BASIC warm start ($E37B)."),
186
+ e(0xa474, 0xa474, "READY", "BASIC: prints READY. and waits for a line."),
187
+ e(0xa7ae, 0xa7ae, "NEWSTT", "BASIC: executes the next statement."),
188
+ e(0xab1e, 0xab1e, "STROUT", "BASIC: prints the zero-terminated string at A (low) and Y (high)."),
189
+ e(0xbdcd, 0xbdcd, "LINPRT", "BASIC: prints the 16-bit number in A (high) and X (low)."),
190
+ e(0xe37b, 0xe37b, "BASIC warm start", "BASIC warm start (RUN/STOP-RESTORE)."),
191
+ e(0xe394, 0xe394, "BASIC cold start", "BASIC cold start: initializes BASIC and prints the start message."),
192
+ e(0xe544, 0xe544, "CLRSCR", "KERNAL: clears the screen."),
193
+ e(0xe566, 0xe566, "HOME", "KERNAL: moves the cursor to the top left corner."),
194
+ e(0xe716, 0xe716, "SCRNOUT", "KERNAL: writes the character in A to the screen."),
195
+ e(0xea31, 0xea31, "IRQ handler", "Standard KERNAL IRQ handler (jiffy clock, cursor, keyboard), at the default IRQ vector."),
196
+ e(0xea7e, 0xea7e, "IRQ end with CIA ack", "Reads $DC0D to acknowledge the CIA 1 interrupt, then continues at $EA81."),
197
+ e(0xea81, 0xea81, "IRQ end", "Restores Y, X and A from the stack and returns from the interrupt."),
198
+ e(0xfce2, 0xfce2, "RESET", "KERNAL reset routine (the reset vector)."),
199
+ e(0xfd15, 0xfd15, "RESTOR body", "Sets the standard KERNAL vectors."),
200
+ e(0xfd50, 0xfd50, "RAMTAS body", "Clears the system areas and tests RAM."),
201
+ e(0xfda3, 0xfda3, "IOINIT body", "Initializes the CIAs and the SID volume."),
202
+ e(0xfe43, 0xfe43, "NMI entry", "KERNAL NMI entry (the NMI hardware vector): continues through $0318."),
203
+ e(0xfe47, 0xfe47, "NMI handler", "Standard KERNAL NMI handler (RESTORE key, RS-232)."),
204
+ e(0xfe66, 0xfe66, "BRK handler", "Standard KERNAL BRK handler: warm start."),
205
+ e(0xff48, 0xff48, "IRQ/BRK entry", "Saves A, X and Y, then continues through $0314 (IRQ) or $0316 (BRK)."),
206
+ e(0xff5b, 0xff5b, "CINT body", "Initializes the screen editor and the VIC-II."),
207
+ e(0xfffa, 0xfffb, "NMI vector", "Hardware NMI vector ($FE43 in the KERNAL ROM)."),
208
+ e(0xfffc, 0xfffd, "RESET vector", "Hardware reset vector ($FCE2 in the KERNAL ROM)."),
209
+ e(0xfffe, 0xffff, "IRQ vector", "Hardware IRQ and BRK vector ($FF48 in the KERNAL ROM)."),
210
+ ];
211
+
212
+ const VIC: Entry[] = [
213
+ ...Array.from({ length: 8 }, (_, sprite) => [
214
+ e(0xd000 + sprite * 2, 0xd000 + sprite * 2, `SP${sprite}X`, `Sprite ${sprite} X position, bits 0-7 (bit 8 is in $D010).`),
215
+ e(0xd001 + sprite * 2, 0xd001 + sprite * 2, `SP${sprite}Y`, `Sprite ${sprite} Y position.`),
216
+ ]).flat(),
217
+ e(0xd010, 0xd010, "MSIGX", "Bit 8 of the X position of each sprite (bit n = sprite n)."),
218
+ e(0xd011, 0xd011, "SCROLY", "Control 1: vertical scroll (bits 0-2), 25/24 rows (bit 3), display enable (bit 4), bitmap mode (bit 5), extended color mode (bit 6), raster line bit 8 (bit 7)."),
219
+ e(0xd012, 0xd012, "RASTER", "Read: raster line bits 0-7. Write: raster interrupt line bits 0-7."),
220
+ e(0xd013, 0xd013, "LPENX", "Light pen X position."),
221
+ e(0xd014, 0xd014, "LPENY", "Light pen Y position."),
222
+ e(0xd015, 0xd015, "SPENA", "Sprite enable (bit n = sprite n)."),
223
+ e(0xd016, 0xd016, "SCROLX", "Control 2: horizontal scroll (bits 0-2), 40/38 columns (bit 3), multicolor mode (bit 4)."),
224
+ e(0xd017, 0xd017, "YXPAND", "Sprite double height (bit n = sprite n)."),
225
+ e(0xd018, 0xd018, "VMCSB", "Memory setup: character or bitmap base (bits 1-3), screen memory (bits 4-7), inside the VIC-II bank."),
226
+ e(0xd019, 0xd019, "VICIRQ", "Interrupt flags: raster (bit 0), sprite-background (bit 1), sprite-sprite (bit 2), light pen (bit 3), any (bit 7). Write 1 to clear a flag."),
227
+ e(0xd01a, 0xd01a, "IRQMSK", "Interrupt enable: raster (bit 0), sprite-background (bit 1), sprite-sprite (bit 2), light pen (bit 3)."),
228
+ e(0xd01b, 0xd01b, "SPBGPR", "Sprite behind the background (bit n = sprite n)."),
229
+ e(0xd01c, 0xd01c, "SPMC", "Sprite multicolor mode (bit n = sprite n)."),
230
+ e(0xd01d, 0xd01d, "XXPAND", "Sprite double width (bit n = sprite n)."),
231
+ e(0xd01e, 0xd01e, "SPSPCL", "Sprite-sprite collision (bit n = sprite n). Reading clears it."),
232
+ e(0xd01f, 0xd01f, "SPBGCL", "Sprite-background collision (bit n = sprite n). Reading clears it."),
233
+ e(0xd020, 0xd020, "EXTCOL", "Border color (bits 0-3)."),
234
+ e(0xd021, 0xd021, "BGCOL0", "Background color 0 (bits 0-3)."),
235
+ e(0xd022, 0xd022, "BGCOL1", "Background color 1 (multicolor and extended color modes)."),
236
+ e(0xd023, 0xd023, "BGCOL2", "Background color 2 (multicolor and extended color modes)."),
237
+ e(0xd024, 0xd024, "BGCOL3", "Background color 3 (extended color mode)."),
238
+ e(0xd025, 0xd025, "SPMC0", "Sprite multicolor 0."),
239
+ e(0xd026, 0xd026, "SPMC1", "Sprite multicolor 1."),
240
+ ...Array.from({ length: 8 }, (_, sprite) => e(0xd027 + sprite, 0xd027 + sprite, `SP${sprite}COL`, `Sprite ${sprite} color.`)),
241
+ ];
242
+
243
+ const SID_VOICE = ["FRELO", "FREHI", "PWLO", "PWHI", "VCREG", "ATDCY", "SUREL"] as const;
244
+ const SID_VOICE_MEANING: Record<(typeof SID_VOICE)[number], string> = {
245
+ FRELO: "frequency, low byte",
246
+ FREHI: "frequency, high byte",
247
+ PWLO: "pulse width, low byte",
248
+ PWHI: "pulse width, high nybble",
249
+ VCREG: "control: gate (bit 0), sync (bit 1), ring modulation (bit 2), test (bit 3), waveform triangle/saw/pulse/noise (bits 4-7)",
250
+ ATDCY: "attack (bits 4-7) and decay (bits 0-3)",
251
+ SUREL: "sustain (bits 4-7) and release (bits 0-3)",
252
+ };
253
+ const SID: Entry[] = [
254
+ ...[0, 1, 2].flatMap((voice) =>
255
+ SID_VOICE.map((name, index) => e(0xd400 + voice * 7 + index, 0xd400 + voice * 7 + index, `${name}${voice + 1}`, `SID voice ${voice + 1} ${SID_VOICE_MEANING[name]}. Write only.`)),
256
+ ),
257
+ e(0xd415, 0xd415, "CUTLO", "SID filter cutoff, bits 0-2. Write only."),
258
+ e(0xd416, 0xd416, "CUTHI", "SID filter cutoff, bits 3-10. Write only."),
259
+ e(0xd417, 0xd417, "RESON", "SID filter resonance (bits 4-7) and the voices through the filter (bits 0-3). Write only."),
260
+ e(0xd418, 0xd418, "SIGVOL", "SID volume (bits 0-3) and filter mode (bits 4-7). Write only."),
261
+ e(0xd419, 0xd419, "POTX", "Paddle X value. Read only."),
262
+ e(0xd41a, 0xd41a, "POTY", "Paddle Y value. Read only."),
263
+ e(0xd41b, 0xd41b, "RANDOM", "Voice 3 oscillator output, often a random number source. Read only."),
264
+ e(0xd41c, 0xd41c, "ENV3", "Voice 3 envelope output. Read only."),
265
+ ];
266
+
267
+ function cia(base: number, number: 1 | 2): Entry[] {
268
+ const prefix = number === 1 ? "CIA" : "CI2";
269
+ const port =
270
+ number === 1
271
+ ? ["Port A: keyboard columns (write) and joystick 2 (read).", "Port B: keyboard rows (read) and joystick 1 (read)."]
272
+ : ["Port A: VIC-II bank (bits 0-1, inverted), serial bus and RS-232.", "Port B: user port."];
273
+ const names = [`${prefix}PRA`, `${prefix}PRB`, `${prefix}DDRA`, `${prefix}DDRB`, `${prefix}TALO`, `${prefix}TAHI`, `${prefix}TBLO`, `${prefix}TBHI`, `${prefix}TOD10`, `${prefix}TODS`, `${prefix}TODM`, `${prefix}TODH`, `${prefix}SDR`, `${prefix}ICR`, `${prefix}CRA`, `${prefix}CRB`];
274
+ const meanings = [
275
+ port[0]!,
276
+ port[1]!,
277
+ "Data direction of port A (1 = output).",
278
+ "Data direction of port B (1 = output).",
279
+ "Timer A, low byte.",
280
+ "Timer A, high byte.",
281
+ "Timer B, low byte.",
282
+ "Timer B, high byte.",
283
+ "Time of day, tenths of seconds (BCD).",
284
+ "Time of day, seconds (BCD).",
285
+ "Time of day, minutes (BCD).",
286
+ "Time of day, hours (BCD, bit 7 = PM).",
287
+ "Serial shift register.",
288
+ `Interrupt control: read gives the sources and clears them; write sets (bit 7 = 1) or clears (bit 7 = 0) the enable bits. Its interrupt goes to the ${number === 1 ? "IRQ" : "NMI"} line.`,
289
+ "Control A: start (bit 0), PB6 output (bit 1), toggle (bit 2), one-shot (bit 3), force load (bit 4), count CNT (bit 5), serial output (bit 6), TOD 50 Hz (bit 7).",
290
+ "Control B: start (bit 0), PB7 output (bit 1), toggle (bit 2), one-shot (bit 3), force load (bit 4), input (bits 5-6), TOD alarm write (bit 7).",
291
+ ];
292
+ return names.map((name, index) => e(base + index, base + index, name, `CIA ${number}: ${meanings[index]}`));
293
+ }
294
+
295
+ interface IoBlock {
296
+ start: number;
297
+ end: number;
298
+ area: string;
299
+ /** Registers repeat every `period` bytes. */
300
+ period: number;
301
+ registers: Entry[];
302
+ }
303
+
304
+ export const IO: IoBlock[] = [
305
+ { start: 0xd000, end: 0xd3ff, area: "VIC-II registers", period: 0x40, registers: VIC },
306
+ { start: 0xd400, end: 0xd7ff, area: "SID registers", period: 0x20, registers: SID },
307
+ { start: 0xdc00, end: 0xdcff, area: "CIA 1 registers", period: 0x10, registers: cia(0xdc00, 1) },
308
+ { start: 0xdd00, end: 0xddff, area: "CIA 2 registers", period: 0x10, registers: cia(0xdd00, 2) },
309
+ ];
310
+
311
+ const AREAS: Array<{ start: number; end: number; area: string; kind: Lookup["kind"]; meaning: string }> = [
312
+ { start: 0x0000, end: 0x00ff, area: "zero page", kind: "platform", meaning: "Zero page. The system uses most of it; $FB-$FE are free." },
313
+ { start: 0x0100, end: 0x03ff, area: "system area", kind: "platform", meaning: "Stack and system variables." },
314
+ { start: 0x0400, end: 0x07ff, area: "screen memory", kind: "platform", meaning: "Default screen memory and sprite pointers." },
315
+ { start: 0x0800, end: 0x9fff, area: "BASIC program RAM", kind: "application", meaning: "RAM for programs; a BASIC program starts at $0801. The application decides what is here." },
316
+ { start: 0xa000, end: 0xbfff, area: "BASIC ROM or RAM", kind: "application", meaning: "BASIC ROM when $01 bits 0 and 1 are set, else RAM. RAM under the ROM is application memory." },
317
+ { start: 0xc000, end: 0xcfff, area: "free RAM", kind: "application", meaning: "Free RAM. The application decides what is here." },
318
+ { start: 0xd800, end: 0xdbff, area: "color RAM", kind: "platform", meaning: "Color RAM: one 4-bit color for each screen character (I/O visible)." },
319
+ { start: 0xde00, end: 0xdeff, area: "I/O 1", kind: "platform", meaning: "I/O area 1 for cartridges and expansions." },
320
+ { start: 0xdf00, end: 0xdfff, area: "I/O 2", kind: "platform", meaning: "I/O area 2 for cartridges and expansions." },
321
+ { start: 0xe000, end: 0xffff, area: "KERNAL ROM or RAM", kind: "platform", meaning: "KERNAL ROM when $01 bit 1 is set, else RAM." },
322
+ ];
323
+
324
+ const hex = (value: number) => `$${value.toString(16).padStart(4, "0")}`;
325
+
326
+ /** The platform meaning of one address, or its application area. */
327
+ export function lookup(address: number): Lookup {
328
+ for (const block of IO) {
329
+ if (address < block.start || address > block.end) continue;
330
+ const base = block.start + ((address - block.start) % block.period);
331
+ const register = block.registers.find((entry) => entry.start === base);
332
+ const mirror = base !== address ? { mirrorOf: base } : {};
333
+ if (register === undefined) return { address, kind: "platform", area: block.area, meaning: `Unused register (${hex(base)}): reads give no useful value.`, ...mirror };
334
+ return { address, kind: "platform", area: block.area, name: register.name, meaning: register.meaning, ...mirror };
335
+ }
336
+ const entry = [...SYSTEM, ...ROM].find((candidate) => address >= candidate.start && address <= candidate.end);
337
+ const area = AREAS.find((candidate) => address >= candidate.start && address <= candidate.end)!;
338
+ if (entry !== undefined) {
339
+ return { address, kind: "platform", area: area.area, name: entry.name, meaning: entry.meaning, ...(entry.start !== address ? { entryStart: entry.start } : {}) };
340
+ }
341
+ return { address, kind: area.kind, area: area.area, meaning: area.meaning };
342
+ }
343
+
344
+ /**
345
+ * The platform name of each named address (D52): system variables, ROM
346
+ * entries and I/O registers, not mirrors, in address order. Only names that
347
+ * an assembler can use as a symbol, and each name once, at its lowest address.
348
+ * `end` is the last address of the entry: a vector has two bytes.
349
+ */
350
+ export function platformNames(): Array<{ address: number; end: number; name: string; meaning: string }> {
351
+ const io = IO.flatMap((block) => block.registers);
352
+ const seen = new Set<string>();
353
+ return [...SYSTEM, ...ROM, ...io]
354
+ .map((entry) => ({ address: entry.start, end: entry.end, name: entry.name, meaning: entry.meaning }))
355
+ .filter((entry) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(entry.name))
356
+ .sort((a, b) => a.address - b.address)
357
+ .filter((entry) => !seen.has(entry.name) && seen.add(entry.name) !== undefined);
358
+ }
359
+
360
+ export type Decoded = Record<string, unknown>;
361
+
362
+ const bits = (value: number) => [0, 1, 2, 3, 4, 5, 6, 7].filter((bit) => (value & (1 << bit)) !== 0);
363
+
364
+ /** The VIC-II bank from a $DD00 value, with bits 0-1 of port A as outputs, as the KERNAL sets them. */
365
+ function vicBank(dd00: number): { bank: number; start: number; end: number } {
366
+ const start = vicBankBase(dd00);
367
+ return { bank: start / 0x4000, start, end: start + 0x3fff };
368
+ }
369
+
370
+ /** What a value in a platform register means. `dd00` gives the VIC-II bank for $D018. */
371
+ export function decode(register: number, value: number, dd00?: number): Decoded {
372
+ switch (register) {
373
+ case 0x01: {
374
+ const config = value & 0x07;
375
+ const ram = "RAM";
376
+ const a000 = (config & 0x03) === 0x03 ? "BASIC ROM" : ram;
377
+ const d000 = (config & 0x03) === 0 ? ram : config & 0x04 ? "I/O" : "character ROM";
378
+ const e000 = config & 0x02 ? "KERNAL ROM" : ram;
379
+ return { loram: (value & 1) === 1, hiram: (value & 2) === 2, charen: (value & 4) === 4, $a000: a000, $d000: d000, $e000: e000, note: "Valid when $00 sets bits 0-2 as outputs (default $2F)." };
380
+ }
381
+ case 0xd011: {
382
+ return {
383
+ yScroll: value & 0x07,
384
+ rows: value & 0x08 ? 25 : 24,
385
+ displayEnabled: (value & 0x10) !== 0,
386
+ bitmapMode: (value & 0x20) !== 0,
387
+ extendedColorMode: (value & 0x40) !== 0,
388
+ rasterBit8: (value & 0x80) >> 7,
389
+ };
390
+ }
391
+ case 0xd016:
392
+ return { xScroll: value & 0x07, columns: value & 0x08 ? 40 : 38, multicolorMode: (value & 0x10) !== 0 };
393
+ case 0xd018: {
394
+ const { screen, characters, bitmap } = d018Offsets(value);
395
+ const result: Decoded = { screenOffset: hex(screen), characterOffset: hex(characters), bitmapOffset: hex(bitmap) };
396
+ if (dd00 !== undefined) {
397
+ const bank = vicBank(dd00);
398
+ result.bank = bank.bank;
399
+ result.screen = hex(bank.start + screen);
400
+ result.characters = hex(bank.start + characters);
401
+ result.bitmap = hex(bank.start + bitmap);
402
+ if (bank.bank === 0 || bank.bank === 2) result.note = "In banks 0 and 2 the VIC-II sees the character ROM at offsets $1000-$1FFF.";
403
+ } else {
404
+ result.note = "Offsets are inside the VIC-II bank; give --bank with the $DD00 value for addresses.";
405
+ }
406
+ return result;
407
+ }
408
+ case 0xdd00: {
409
+ const bank = vicBank(value);
410
+ return { vicBank: bank.bank, vicAddresses: `${hex(bank.start)}-${hex(bank.end)}` };
411
+ }
412
+ case 0xd015:
413
+ case 0xd017:
414
+ case 0xd01b:
415
+ case 0xd01c:
416
+ case 0xd01d:
417
+ case 0xd01e:
418
+ case 0xd01f:
419
+ case 0xd010:
420
+ return { sprites: bits(value) };
421
+ case 0xd019:
422
+ case 0xd01a: {
423
+ const names = ["raster", "sprite-background collision", "sprite-sprite collision", "light pen"];
424
+ const result: Decoded = { sources: bits(value & 0x0f).map((bit) => names[bit]) };
425
+ if (register === 0xd019) result.anyInterrupt = (value & 0x80) !== 0;
426
+ return result;
427
+ }
428
+ case 0xdc0d:
429
+ case 0xdd0d: {
430
+ const names = ["timer A", "timer B", "TOD alarm", "serial port", "FLAG line"];
431
+ const sources = bits(value & 0x1f).map((bit) => names[bit]);
432
+ return {
433
+ read: { sources, interruptOccurred: (value & 0x80) !== 0 },
434
+ write: { [(value & 0x80) !== 0 ? "enables" : "disables"]: sources },
435
+ line: register === 0xdc0d ? "IRQ" : "NMI",
436
+ };
437
+ }
438
+ default:
439
+ throw new RangeError(`no decoder for ${hex(register)}; decoders exist for $0001, $d010, $d011, $d015-$d01f (sprite and interrupt bits), $d016, $d018, $dc0d, $dd00 and $dd0d`);
440
+ }
441
+ }
package/src/project.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { isAbsolute, relative, resolve, sep } from "node:path";
3
+
4
+ export function projectRoot(): string {
5
+ return process.cwd();
6
+ }
7
+
8
+ export function resolveProjectPath(path: string, root = projectRoot()): string {
9
+ if (path.length === 0) {
10
+ throw new TypeError("Project path must not be empty");
11
+ }
12
+ if (isAbsolute(path)) {
13
+ throw new TypeError("Project path must be relative");
14
+ }
15
+
16
+ const candidate = resolve(root, path);
17
+ if (isInside(root, candidate)) return candidate;
18
+ throw new TypeError("Project path escapes the project root");
19
+ }
20
+
21
+ /** True when `candidate` is `root` or below it. Both are absolute paths. */
22
+ export function isInside(root: string, candidate: string): boolean {
23
+ const fromRoot = relative(root, candidate);
24
+ return fromRoot === "" || (fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot));
25
+ }
26
+
27
+ /**
28
+ * Resolves a project-relative path to an existing file system entry whose real
29
+ * location is inside the project too, so a symbolic link cannot reach outside
30
+ * it. Throws TypeError for a refused path; lets ENOENT through.
31
+ */
32
+ export function resolveProjectFile(path: string, root = projectRoot()): string {
33
+ const candidate = resolveProjectPath(path, root);
34
+ const real = realpathSync(candidate);
35
+ if (!isInside(realpathSync(root), real)) {
36
+ throw new TypeError("Project path escapes the project root through a symbolic link");
37
+ }
38
+ return real;
39
+ }
@@ -0,0 +1,132 @@
1
+ // The frame codec of the Host Runtime wire: the frame limits, encodeFrame and MessageReader.
2
+
3
+ import { type ClientMessage, type HostMessage, isInteger, isObject, ProtocolError } from "./messages.ts";
4
+
5
+ /** Largest frame body either side accepts. */
6
+ export const MAX_FRAME_BYTES = 1024 * 1024;
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Framing: 4-byte big-endian body length, then the body. A message body
10
+ // is UTF-8 JSON. A message may announce binary attachments with an
11
+ // `attachments` array of byte sizes; each attachment then follows as raw
12
+ // frames of at most MAX_FRAME_BYTES each, in order.
13
+
14
+ const HEADER_BYTES = 4;
15
+ /** Total attachment bytes one message may carry. */
16
+ export const MAX_ATTACHMENT_BYTES = 16 * 1024 * 1024;
17
+
18
+ function frame(body: Uint8Array): Buffer {
19
+ const header = Buffer.allocUnsafe(HEADER_BYTES);
20
+ header.writeUInt32BE(body.length, 0);
21
+ return Buffer.concat([header, body]);
22
+ }
23
+
24
+ /** Encodes one message and its attachments as one buffer, so writes never interleave. */
25
+ export function encodeFrame(message: ClientMessage | HostMessage, attachments: readonly Uint8Array[] = []): Buffer {
26
+ const total = attachments.reduce((sum, attachment) => sum + attachment.length, 0);
27
+ if (total > MAX_ATTACHMENT_BYTES) throw new ProtocolError(`attachments of ${total} bytes exceed ${MAX_ATTACHMENT_BYTES}`);
28
+ const announced = attachments.length === 0 ? message : { ...message, attachments: attachments.map((attachment) => attachment.length) };
29
+ const body = Buffer.from(JSON.stringify(announced), "utf8");
30
+ if (body.length > MAX_FRAME_BYTES) throw new ProtocolError(`frame of ${body.length} bytes exceeds ${MAX_FRAME_BYTES}`);
31
+ const frames = [frame(body)];
32
+ for (const attachment of attachments) {
33
+ for (let offset = 0; offset < attachment.length; offset += MAX_FRAME_BYTES) {
34
+ frames.push(frame(attachment.subarray(offset, offset + MAX_FRAME_BYTES)));
35
+ }
36
+ }
37
+ return Buffer.concat(frames);
38
+ }
39
+
40
+ /** Splits a stream into frame bodies. After a ProtocolError the decoder is unusable. */
41
+ class RawFrameDecoder {
42
+ #pending: Buffer = Buffer.alloc(0);
43
+
44
+ push(chunk: Buffer): Buffer[] {
45
+ this.#pending = this.#pending.length === 0 ? chunk : Buffer.concat([this.#pending, chunk]);
46
+ const bodies: Buffer[] = [];
47
+ while (this.#pending.length >= HEADER_BYTES) {
48
+ const length = this.#pending.readUInt32BE(0);
49
+ if (length > MAX_FRAME_BYTES) throw new ProtocolError(`frame of ${length} bytes exceeds ${MAX_FRAME_BYTES}`);
50
+ if (this.#pending.length < HEADER_BYTES + length) break;
51
+ bodies.push(Buffer.from(this.#pending.subarray(HEADER_BYTES, HEADER_BYTES + length)));
52
+ this.#pending = this.#pending.subarray(HEADER_BYTES + length);
53
+ }
54
+ return bodies;
55
+ }
56
+ }
57
+
58
+ export interface ReceivedMessage {
59
+ /** The parsed JSON body, with any `attachments` announcement removed. */
60
+ message: unknown;
61
+ attachments: Buffer[];
62
+ }
63
+
64
+ /** Reassembles messages and their attachments from stream chunks. Throws ProtocolError. */
65
+ export class MessageReader {
66
+ readonly #frames = new RawFrameDecoder();
67
+ readonly #textDecoder = new TextDecoder("utf-8", { fatal: true });
68
+ #current: { message: unknown; sizes: number[]; done: Buffer[]; parts: Buffer[]; received: number } | undefined;
69
+
70
+ push(chunk: Buffer): ReceivedMessage[] {
71
+ const complete: ReceivedMessage[] = [];
72
+ for (const body of this.#frames.push(chunk)) {
73
+ if (this.#current === undefined) this.#start(body);
74
+ else this.#collect(body);
75
+ const finished = this.#finish();
76
+ if (finished !== undefined) complete.push(finished);
77
+ }
78
+ return complete;
79
+ }
80
+
81
+ #start(body: Buffer): void {
82
+ let text: string;
83
+ try {
84
+ text = this.#textDecoder.decode(body);
85
+ } catch {
86
+ throw new ProtocolError("frame body is not valid UTF-8");
87
+ }
88
+ let message: unknown;
89
+ try {
90
+ message = JSON.parse(text);
91
+ } catch {
92
+ throw new ProtocolError("frame body is not valid JSON");
93
+ }
94
+ let sizes: number[] = [];
95
+ if (isObject(message) && "attachments" in message) {
96
+ const announced = message.attachments;
97
+ if (!Array.isArray(announced) || !announced.every((size) => isInteger(size, 0, MAX_ATTACHMENT_BYTES))) {
98
+ throw new ProtocolError("attachments must be a list of byte sizes");
99
+ }
100
+ if (announced.reduce((sum: number, size: number) => sum + size, 0) > MAX_ATTACHMENT_BYTES) {
101
+ throw new ProtocolError(`attachments exceed ${MAX_ATTACHMENT_BYTES} bytes`);
102
+ }
103
+ sizes = announced as number[];
104
+ const { attachments: _announced, ...rest } = message;
105
+ message = rest;
106
+ }
107
+ this.#current = { message, sizes, done: [], parts: [], received: 0 };
108
+ }
109
+
110
+ #collect(body: Buffer): void {
111
+ const current = this.#current!;
112
+ const expected = current.sizes[current.done.length]!;
113
+ // An attachment of size 0 is complete without a frame, so the pending one needs bytes; an empty frame would only add an empty part.
114
+ if (body.length === 0) throw new ProtocolError("attachment frame is empty");
115
+ if (current.received + body.length > expected) throw new ProtocolError("attachment frame runs past its announced size");
116
+ current.parts.push(body);
117
+ current.received += body.length;
118
+ }
119
+
120
+ /** Completes attachments whose bytes are all here; returns the message once nothing is missing. */
121
+ #finish(): ReceivedMessage | undefined {
122
+ const current = this.#current!;
123
+ while (current.done.length < current.sizes.length && current.received === current.sizes[current.done.length]) {
124
+ current.done.push(Buffer.concat(current.parts));
125
+ current.parts = [];
126
+ current.received = 0;
127
+ }
128
+ if (current.done.length < current.sizes.length) return undefined;
129
+ this.#current = undefined;
130
+ return { message: current.message, attachments: current.done };
131
+ }
132
+ }