web-a2e 1.0.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.
Files changed (295) hide show
  1. package/.clangd +5 -0
  2. package/.mcp.json +12 -0
  3. package/CLAUDE.md +362 -0
  4. package/CMakeLists.txt +774 -0
  5. package/LICENSE +21 -0
  6. package/README.md +392 -0
  7. package/build-wasm/generated/roms.cpp +2447 -0
  8. package/docker-compose.staging.yml +9 -0
  9. package/docs/basic-rom-disassembly.md +6663 -0
  10. package/docs/softswitch-comparison.md +273 -0
  11. package/docs/thunderclock-debug.md +89 -0
  12. package/examples/cube.bas +72 -0
  13. package/examples/hello.s +55 -0
  14. package/examples/scroll.s +140 -0
  15. package/package.json +18 -0
  16. package/public/assets/apple-logo-old.png +0 -0
  17. package/public/assets/apple-logo.png +0 -0
  18. package/public/assets/drive-closed-light-on.png +0 -0
  19. package/public/assets/drive-closed.png +0 -0
  20. package/public/assets/drive-open-light-on.png +0 -0
  21. package/public/assets/drive-open.png +0 -0
  22. package/public/audio-worklet.js +82 -0
  23. package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
  24. package/public/disks/ProDOS 2.4.3.po +0 -0
  25. package/public/disks/h32mb.2mg +0 -0
  26. package/public/disks/library.json +26 -0
  27. package/public/docs/llms/llm-assembler.txt +90 -0
  28. package/public/docs/llms/llm-basic-program.txt +256 -0
  29. package/public/docs/llms/llm-disk-drives.txt +72 -0
  30. package/public/docs/llms/llm-file-explorer.txt +50 -0
  31. package/public/docs/llms/llm-hard-drives.txt +80 -0
  32. package/public/docs/llms/llm-main.txt +51 -0
  33. package/public/docs/llms/llm-slot-configuration.txt +66 -0
  34. package/public/icons/icon-192.svg +4 -0
  35. package/public/icons/icon-512.svg +4 -0
  36. package/public/index.html +661 -0
  37. package/public/llms.txt +49 -0
  38. package/public/manifest.json +29 -0
  39. package/public/shaders/burnin.glsl +22 -0
  40. package/public/shaders/crt.glsl +706 -0
  41. package/public/shaders/edge.glsl +109 -0
  42. package/public/shaders/vertex.glsl +8 -0
  43. package/public/sw.js +186 -0
  44. package/roms/341-0027.bin +0 -0
  45. package/roms/341-0160-A-US-UK.bin +0 -0
  46. package/roms/341-0160-A.bin +0 -0
  47. package/roms/342-0273-A-US-UK.bin +0 -0
  48. package/roms/342-0349-B-C0-FF.bin +0 -0
  49. package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
  50. package/roms/Thunderclock Plus ROM.bin +0 -0
  51. package/scripts/generate_roms.sh +69 -0
  52. package/src/bindings/wasm_interface.cpp +1940 -0
  53. package/src/core/assembler/assembler.cpp +1239 -0
  54. package/src/core/assembler/assembler.hpp +115 -0
  55. package/src/core/audio/audio.cpp +160 -0
  56. package/src/core/audio/audio.hpp +81 -0
  57. package/src/core/basic/basic_detokenizer.cpp +436 -0
  58. package/src/core/basic/basic_detokenizer.hpp +41 -0
  59. package/src/core/basic/basic_tokenizer.cpp +286 -0
  60. package/src/core/basic/basic_tokenizer.hpp +26 -0
  61. package/src/core/basic/basic_tokens.hpp +295 -0
  62. package/src/core/cards/disk2_card.cpp +568 -0
  63. package/src/core/cards/disk2_card.hpp +316 -0
  64. package/src/core/cards/expansion_card.hpp +185 -0
  65. package/src/core/cards/mockingboard/ay8910.cpp +616 -0
  66. package/src/core/cards/mockingboard/ay8910.hpp +159 -0
  67. package/src/core/cards/mockingboard/via6522.cpp +530 -0
  68. package/src/core/cards/mockingboard/via6522.hpp +163 -0
  69. package/src/core/cards/mockingboard_card.cpp +312 -0
  70. package/src/core/cards/mockingboard_card.hpp +159 -0
  71. package/src/core/cards/mouse_card.cpp +654 -0
  72. package/src/core/cards/mouse_card.hpp +190 -0
  73. package/src/core/cards/smartport/block_device.cpp +202 -0
  74. package/src/core/cards/smartport/block_device.hpp +60 -0
  75. package/src/core/cards/smartport/smartport_card.cpp +603 -0
  76. package/src/core/cards/smartport/smartport_card.hpp +120 -0
  77. package/src/core/cards/thunderclock_card.cpp +237 -0
  78. package/src/core/cards/thunderclock_card.hpp +122 -0
  79. package/src/core/cpu/cpu6502.cpp +1609 -0
  80. package/src/core/cpu/cpu6502.hpp +203 -0
  81. package/src/core/debug/condition_evaluator.cpp +470 -0
  82. package/src/core/debug/condition_evaluator.hpp +87 -0
  83. package/src/core/disassembler/disassembler.cpp +552 -0
  84. package/src/core/disassembler/disassembler.hpp +171 -0
  85. package/src/core/disk-image/disk_image.hpp +267 -0
  86. package/src/core/disk-image/dsk_disk_image.cpp +827 -0
  87. package/src/core/disk-image/dsk_disk_image.hpp +204 -0
  88. package/src/core/disk-image/gcr_encoding.cpp +147 -0
  89. package/src/core/disk-image/gcr_encoding.hpp +78 -0
  90. package/src/core/disk-image/woz_disk_image.cpp +1049 -0
  91. package/src/core/disk-image/woz_disk_image.hpp +343 -0
  92. package/src/core/emulator.cpp +2126 -0
  93. package/src/core/emulator.hpp +434 -0
  94. package/src/core/filesystem/dos33.cpp +178 -0
  95. package/src/core/filesystem/dos33.hpp +66 -0
  96. package/src/core/filesystem/pascal.cpp +262 -0
  97. package/src/core/filesystem/pascal.hpp +87 -0
  98. package/src/core/filesystem/prodos.cpp +369 -0
  99. package/src/core/filesystem/prodos.hpp +119 -0
  100. package/src/core/input/keyboard.cpp +227 -0
  101. package/src/core/input/keyboard.hpp +111 -0
  102. package/src/core/mmu/mmu.cpp +1387 -0
  103. package/src/core/mmu/mmu.hpp +236 -0
  104. package/src/core/types.hpp +196 -0
  105. package/src/core/video/video.cpp +680 -0
  106. package/src/core/video/video.hpp +156 -0
  107. package/src/css/assembler-editor.css +1617 -0
  108. package/src/css/base.css +470 -0
  109. package/src/css/basic-debugger.css +791 -0
  110. package/src/css/basic-editor.css +792 -0
  111. package/src/css/controls.css +783 -0
  112. package/src/css/cpu-debugger.css +1413 -0
  113. package/src/css/debug-base.css +160 -0
  114. package/src/css/debug-windows.css +6455 -0
  115. package/src/css/disk-drives.css +406 -0
  116. package/src/css/documentation.css +392 -0
  117. package/src/css/file-explorer.css +867 -0
  118. package/src/css/hard-drive.css +180 -0
  119. package/src/css/layout.css +217 -0
  120. package/src/css/memory-windows.css +798 -0
  121. package/src/css/modals.css +510 -0
  122. package/src/css/monitor.css +425 -0
  123. package/src/css/release-notes.css +101 -0
  124. package/src/css/responsive.css +400 -0
  125. package/src/css/rule-builder.css +340 -0
  126. package/src/css/save-states.css +201 -0
  127. package/src/css/settings-windows.css +1231 -0
  128. package/src/css/window-switcher.css +150 -0
  129. package/src/js/agent/agent-manager.js +643 -0
  130. package/src/js/agent/agent-tools.js +293 -0
  131. package/src/js/agent/agent-version-tools.js +131 -0
  132. package/src/js/agent/assembler-tools.js +357 -0
  133. package/src/js/agent/basic-program-tools.js +894 -0
  134. package/src/js/agent/disk-tools.js +417 -0
  135. package/src/js/agent/file-explorer-tools.js +269 -0
  136. package/src/js/agent/index.js +13 -0
  137. package/src/js/agent/main-tools.js +222 -0
  138. package/src/js/agent/slot-tools.js +303 -0
  139. package/src/js/agent/smartport-tools.js +257 -0
  140. package/src/js/agent/window-tools.js +80 -0
  141. package/src/js/audio/audio-driver.js +417 -0
  142. package/src/js/audio/audio-worklet.js +85 -0
  143. package/src/js/audio/index.js +8 -0
  144. package/src/js/config/default-layout.js +34 -0
  145. package/src/js/config/version.js +8 -0
  146. package/src/js/data/apple2-rom-routines.js +577 -0
  147. package/src/js/debug/assembler-editor-window.js +2993 -0
  148. package/src/js/debug/basic-breakpoint-manager.js +529 -0
  149. package/src/js/debug/basic-program-parser.js +436 -0
  150. package/src/js/debug/basic-program-window.js +2594 -0
  151. package/src/js/debug/basic-variable-inspector.js +447 -0
  152. package/src/js/debug/breakpoint-manager.js +472 -0
  153. package/src/js/debug/cpu-debugger-window.js +2396 -0
  154. package/src/js/debug/index.js +22 -0
  155. package/src/js/debug/label-manager.js +238 -0
  156. package/src/js/debug/memory-browser-window.js +416 -0
  157. package/src/js/debug/memory-heat-map-window.js +481 -0
  158. package/src/js/debug/memory-map-window.js +206 -0
  159. package/src/js/debug/mockingboard-window.js +882 -0
  160. package/src/js/debug/mouse-card-window.js +355 -0
  161. package/src/js/debug/rule-builder-window.js +648 -0
  162. package/src/js/debug/soft-switch-window.js +458 -0
  163. package/src/js/debug/stack-viewer-window.js +221 -0
  164. package/src/js/debug/symbols.js +416 -0
  165. package/src/js/debug/trace-panel.js +291 -0
  166. package/src/js/debug/zero-page-watch-window.js +297 -0
  167. package/src/js/disk-manager/disk-drives-window.js +212 -0
  168. package/src/js/disk-manager/disk-operations.js +284 -0
  169. package/src/js/disk-manager/disk-persistence.js +301 -0
  170. package/src/js/disk-manager/disk-surface-renderer.js +388 -0
  171. package/src/js/disk-manager/drive-sounds.js +139 -0
  172. package/src/js/disk-manager/hard-drive-manager.js +481 -0
  173. package/src/js/disk-manager/hard-drive-persistence.js +187 -0
  174. package/src/js/disk-manager/hard-drive-window.js +57 -0
  175. package/src/js/disk-manager/index.js +890 -0
  176. package/src/js/display/display-settings-window.js +383 -0
  177. package/src/js/display/index.js +10 -0
  178. package/src/js/display/screen-window.js +342 -0
  179. package/src/js/display/webgl-renderer.js +705 -0
  180. package/src/js/file-explorer/disassembler.js +574 -0
  181. package/src/js/file-explorer/dos33.js +266 -0
  182. package/src/js/file-explorer/file-viewer.js +359 -0
  183. package/src/js/file-explorer/index.js +1261 -0
  184. package/src/js/file-explorer/prodos.js +549 -0
  185. package/src/js/file-explorer/utils.js +67 -0
  186. package/src/js/help/documentation-window.js +1096 -0
  187. package/src/js/help/index.js +10 -0
  188. package/src/js/help/release-notes-window.js +85 -0
  189. package/src/js/help/release-notes.js +612 -0
  190. package/src/js/input/gamepad-handler.js +176 -0
  191. package/src/js/input/index.js +12 -0
  192. package/src/js/input/input-handler.js +396 -0
  193. package/src/js/input/joystick-window.js +404 -0
  194. package/src/js/input/mouse-handler.js +99 -0
  195. package/src/js/input/text-selection.js +462 -0
  196. package/src/js/main.js +653 -0
  197. package/src/js/state/index.js +15 -0
  198. package/src/js/state/save-states-window.js +393 -0
  199. package/src/js/state/state-manager.js +409 -0
  200. package/src/js/state/state-persistence.js +218 -0
  201. package/src/js/ui/confirm.js +43 -0
  202. package/src/js/ui/disk-drive-positioner.js +347 -0
  203. package/src/js/ui/reminder-controller.js +129 -0
  204. package/src/js/ui/slot-configuration-window.js +560 -0
  205. package/src/js/ui/theme-manager.js +61 -0
  206. package/src/js/ui/toast.js +44 -0
  207. package/src/js/ui/ui-controller.js +897 -0
  208. package/src/js/ui/window-switcher.js +275 -0
  209. package/src/js/utils/basic-autocomplete.js +832 -0
  210. package/src/js/utils/basic-highlighting.js +473 -0
  211. package/src/js/utils/basic-tokenizer.js +153 -0
  212. package/src/js/utils/basic-tokens.js +117 -0
  213. package/src/js/utils/constants.js +28 -0
  214. package/src/js/utils/indexeddb-helper.js +225 -0
  215. package/src/js/utils/merlin-editor-support.js +905 -0
  216. package/src/js/utils/merlin-highlighting.js +551 -0
  217. package/src/js/utils/storage.js +125 -0
  218. package/src/js/utils/string-utils.js +19 -0
  219. package/src/js/utils/wasm-memory.js +54 -0
  220. package/src/js/windows/base-window.js +690 -0
  221. package/src/js/windows/index.js +9 -0
  222. package/src/js/windows/window-manager.js +375 -0
  223. package/tests/catch2/catch.hpp +17976 -0
  224. package/tests/common/basic_program_builder.cpp +119 -0
  225. package/tests/common/basic_program_builder.hpp +209 -0
  226. package/tests/common/disk_image_builder.cpp +444 -0
  227. package/tests/common/disk_image_builder.hpp +141 -0
  228. package/tests/common/test_helpers.hpp +118 -0
  229. package/tests/gcr/gcr-test.cpp +142 -0
  230. package/tests/integration/check-rom.js +70 -0
  231. package/tests/integration/compare-boot.js +239 -0
  232. package/tests/integration/crash-trace.js +102 -0
  233. package/tests/integration/disk-boot-test.js +264 -0
  234. package/tests/integration/memory-crash.js +108 -0
  235. package/tests/integration/nibble-read-test.js +249 -0
  236. package/tests/integration/phase-test.js +159 -0
  237. package/tests/integration/test_emulator.cpp +291 -0
  238. package/tests/integration/test_emulator_basic.cpp +91 -0
  239. package/tests/integration/test_emulator_debug.cpp +344 -0
  240. package/tests/integration/test_emulator_disk.cpp +153 -0
  241. package/tests/integration/test_emulator_state.cpp +163 -0
  242. package/tests/klaus/6502_functional_test.bin +0 -0
  243. package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
  244. package/tests/klaus/klaus_6502_test.cpp +184 -0
  245. package/tests/klaus/klaus_65c02_test.cpp +197 -0
  246. package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
  247. package/tests/thunderclock/thunderclock_test.cpp +550 -0
  248. package/tests/unit/test_assembler.cpp +521 -0
  249. package/tests/unit/test_audio.cpp +196 -0
  250. package/tests/unit/test_ay8910.cpp +311 -0
  251. package/tests/unit/test_basic_detokenizer.cpp +265 -0
  252. package/tests/unit/test_basic_tokenizer.cpp +382 -0
  253. package/tests/unit/test_block_device.cpp +259 -0
  254. package/tests/unit/test_condition_evaluator.cpp +219 -0
  255. package/tests/unit/test_cpu6502.cpp +1301 -0
  256. package/tests/unit/test_cpu_addressing.cpp +361 -0
  257. package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
  258. package/tests/unit/test_cpu_decimal.cpp +166 -0
  259. package/tests/unit/test_cpu_interrupts.cpp +285 -0
  260. package/tests/unit/test_disassembler.cpp +323 -0
  261. package/tests/unit/test_disk2_card.cpp +330 -0
  262. package/tests/unit/test_dos33.cpp +273 -0
  263. package/tests/unit/test_dsk_disk_image.cpp +315 -0
  264. package/tests/unit/test_expansion_card.cpp +178 -0
  265. package/tests/unit/test_gcr_encoding.cpp +232 -0
  266. package/tests/unit/test_keyboard.cpp +262 -0
  267. package/tests/unit/test_mmu.cpp +555 -0
  268. package/tests/unit/test_mmu_slots.cpp +323 -0
  269. package/tests/unit/test_mockingboard.cpp +352 -0
  270. package/tests/unit/test_mouse_card.cpp +386 -0
  271. package/tests/unit/test_pascal.cpp +248 -0
  272. package/tests/unit/test_prodos.cpp +259 -0
  273. package/tests/unit/test_smartport_card.cpp +321 -0
  274. package/tests/unit/test_thunderclock.cpp +354 -0
  275. package/tests/unit/test_via6522.cpp +323 -0
  276. package/tests/unit/test_video.cpp +319 -0
  277. package/tests/unit/test_woz_disk_image.cpp +257 -0
  278. package/vite.config.js +96 -0
  279. package/wiki/AI-Agent.md +372 -0
  280. package/wiki/Architecture-Overview.md +303 -0
  281. package/wiki/Audio-System.md +449 -0
  282. package/wiki/CPU-Emulation.md +477 -0
  283. package/wiki/Debugger.md +516 -0
  284. package/wiki/Disk-Drives.md +161 -0
  285. package/wiki/Disk-System-Internals.md +547 -0
  286. package/wiki/Display-Settings.md +88 -0
  287. package/wiki/Expansion-Slots.md +187 -0
  288. package/wiki/File-Explorer.md +259 -0
  289. package/wiki/Getting-Started.md +156 -0
  290. package/wiki/Home.md +69 -0
  291. package/wiki/Input-Devices.md +183 -0
  292. package/wiki/Keyboard-Shortcuts.md +158 -0
  293. package/wiki/Memory-System.md +364 -0
  294. package/wiki/Save-States.md +172 -0
  295. package/wiki/Video-Rendering.md +658 -0
@@ -0,0 +1,330 @@
1
+ /*
2
+ * test_disk2_card.cpp - Unit tests for Disk2Card
3
+ *
4
+ * Tests the Disk II controller card implementation including:
5
+ * - Construction with ROM data
6
+ * - ROM read access
7
+ * - Motor control (on/off)
8
+ * - Drive selection
9
+ * - Q6/Q7 latch states
10
+ * - Phase control for head stepping
11
+ * - Disk insert/eject operations
12
+ * - Card metadata (name, preferred slot)
13
+ * - State serialization round-trip
14
+ */
15
+
16
+ #define CATCH_CONFIG_MAIN
17
+ #include "catch.hpp"
18
+
19
+ #include "disk2_card.hpp"
20
+ #include "roms.cpp"
21
+
22
+ #include <cstring>
23
+ #include <vector>
24
+
25
+ using namespace a2e;
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Construction
29
+ // ---------------------------------------------------------------------------
30
+
31
+ TEST_CASE("Disk2Card constructor with ROM loads ROM data", "[disk2]") {
32
+ Disk2Card card(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
33
+
34
+ // The ROM should be 256 bytes; every byte should match the source data
35
+ for (size_t i = 0; i < roms::ROM_DISK2_SIZE; ++i) {
36
+ REQUIRE(card.readROM(static_cast<uint8_t>(i)) == roms::ROM_DISK2[i]);
37
+ }
38
+ }
39
+
40
+ TEST_CASE("Disk2Card default constructor creates valid card", "[disk2]") {
41
+ Disk2Card card;
42
+ // Should still be constructable; ROM content may be zeroed
43
+ REQUIRE(card.getName() != nullptr);
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // readROM returns bytes from loaded ROM
48
+ // ---------------------------------------------------------------------------
49
+
50
+ TEST_CASE("Disk2Card readROM returns ROM bytes after loadROM", "[disk2]") {
51
+ Disk2Card card;
52
+ card.loadROM(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
53
+
54
+ REQUIRE(card.readROM(0x00) == roms::ROM_DISK2[0]);
55
+ REQUIRE(card.readROM(0x7F) == roms::ROM_DISK2[0x7F]);
56
+ REQUIRE(card.readROM(0xFF) == roms::ROM_DISK2[0xFF]);
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // reset() clears controller state
61
+ // ---------------------------------------------------------------------------
62
+
63
+ TEST_CASE("Disk2Card reset clears state", "[disk2]") {
64
+ Disk2Card card(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
65
+
66
+ // Turn motor on and select drive 2
67
+ card.readIO(0x09); // Motor on
68
+ card.readIO(0x0B); // Drive 2
69
+
70
+ REQUIRE(card.isMotorOn());
71
+ REQUIRE(card.getSelectedDrive() == 1);
72
+
73
+ card.reset();
74
+
75
+ REQUIRE_FALSE(card.isMotorOn());
76
+ REQUIRE(card.getSelectedDrive() == 0);
77
+ REQUIRE(card.getQ6() == false);
78
+ REQUIRE(card.getQ7() == false);
79
+ REQUIRE(card.getPhaseStates() == 0);
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Motor on/off via readIO
84
+ // ---------------------------------------------------------------------------
85
+
86
+ TEST_CASE("Disk2Card motor control via I/O offsets", "[disk2]") {
87
+ Disk2Card card(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
88
+
89
+ SECTION("Motor starts off") {
90
+ REQUIRE_FALSE(card.isMotorOn());
91
+ }
92
+
93
+ SECTION("Offset 0x09 turns motor on") {
94
+ card.readIO(0x09);
95
+ REQUIRE(card.isMotorOn());
96
+ }
97
+
98
+ SECTION("Offset 0x08 turns motor off") {
99
+ card.readIO(0x09); // on
100
+ REQUIRE(card.isMotorOn());
101
+ card.readIO(0x08); // off
102
+ // Motor may not turn off immediately (delay), but the off request is latched
103
+ // After enough update cycles, motor will be off. For the immediate test,
104
+ // just verify the toggle was accepted without error.
105
+ }
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Drive select
110
+ // ---------------------------------------------------------------------------
111
+
112
+ TEST_CASE("Disk2Card drive selection via I/O offsets", "[disk2]") {
113
+ Disk2Card card(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
114
+
115
+ SECTION("Default drive is 0 (drive 1)") {
116
+ REQUIRE(card.getSelectedDrive() == 0);
117
+ }
118
+
119
+ SECTION("Offset 0x0B selects drive 2 (index 1)") {
120
+ card.readIO(0x0B);
121
+ REQUIRE(card.getSelectedDrive() == 1);
122
+ }
123
+
124
+ SECTION("Offset 0x0A selects drive 1 (index 0)") {
125
+ card.readIO(0x0B); // select drive 2 first
126
+ card.readIO(0x0A); // back to drive 1
127
+ REQUIRE(card.getSelectedDrive() == 0);
128
+ }
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Q6/Q7 latches
133
+ // ---------------------------------------------------------------------------
134
+
135
+ TEST_CASE("Disk2Card Q6 and Q7 latch control", "[disk2]") {
136
+ Disk2Card card(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
137
+
138
+ SECTION("Q6 and Q7 start low") {
139
+ REQUIRE(card.getQ6() == false);
140
+ REQUIRE(card.getQ7() == false);
141
+ }
142
+
143
+ SECTION("Offset 0x0C sets Q6 low") {
144
+ card.readIO(0x0D); // Q6H first
145
+ REQUIRE(card.getQ6() == true);
146
+ card.readIO(0x0C); // Q6L
147
+ REQUIRE(card.getQ6() == false);
148
+ }
149
+
150
+ SECTION("Offset 0x0D sets Q6 high") {
151
+ card.readIO(0x0D);
152
+ REQUIRE(card.getQ6() == true);
153
+ }
154
+
155
+ SECTION("Offset 0x0E sets Q7 low") {
156
+ card.readIO(0x0F); // Q7H first
157
+ REQUIRE(card.getQ7() == true);
158
+ card.readIO(0x0E); // Q7L
159
+ REQUIRE(card.getQ7() == false);
160
+ }
161
+
162
+ SECTION("Offset 0x0F sets Q7 high") {
163
+ card.readIO(0x0F);
164
+ REQUIRE(card.getQ7() == true);
165
+ }
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // Phase control
170
+ // ---------------------------------------------------------------------------
171
+
172
+ TEST_CASE("Disk2Card phase control via I/O offsets", "[disk2]") {
173
+ Disk2Card card(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
174
+
175
+ SECTION("All phases off initially") {
176
+ REQUIRE(card.getPhaseStates() == 0x00);
177
+ }
178
+
179
+ SECTION("Phase 0 on (offset 0x01)") {
180
+ card.readIO(0x01);
181
+ REQUIRE((card.getPhaseStates() & 0x01) != 0);
182
+ }
183
+
184
+ SECTION("Phase 0 off (offset 0x00)") {
185
+ card.readIO(0x01); // on
186
+ card.readIO(0x00); // off
187
+ REQUIRE((card.getPhaseStates() & 0x01) == 0);
188
+ }
189
+
190
+ SECTION("Phase 1 on (offset 0x03)") {
191
+ card.readIO(0x03);
192
+ REQUIRE((card.getPhaseStates() & 0x02) != 0);
193
+ }
194
+
195
+ SECTION("Phase 1 off (offset 0x02)") {
196
+ card.readIO(0x03);
197
+ card.readIO(0x02);
198
+ REQUIRE((card.getPhaseStates() & 0x02) == 0);
199
+ }
200
+
201
+ SECTION("Phase 2 on (offset 0x05)") {
202
+ card.readIO(0x05);
203
+ REQUIRE((card.getPhaseStates() & 0x04) != 0);
204
+ }
205
+
206
+ SECTION("Phase 2 off (offset 0x04)") {
207
+ card.readIO(0x05);
208
+ card.readIO(0x04);
209
+ REQUIRE((card.getPhaseStates() & 0x04) == 0);
210
+ }
211
+
212
+ SECTION("Phase 3 on (offset 0x07)") {
213
+ card.readIO(0x07);
214
+ REQUIRE((card.getPhaseStates() & 0x08) != 0);
215
+ }
216
+
217
+ SECTION("Phase 3 off (offset 0x06)") {
218
+ card.readIO(0x07);
219
+ card.readIO(0x06);
220
+ REQUIRE((card.getPhaseStates() & 0x08) == 0);
221
+ }
222
+
223
+ SECTION("Multiple phases can be on simultaneously") {
224
+ card.readIO(0x01); // phase 0 on
225
+ card.readIO(0x03); // phase 1 on
226
+ REQUIRE((card.getPhaseStates() & 0x03) == 0x03);
227
+ }
228
+ }
229
+
230
+ // ---------------------------------------------------------------------------
231
+ // Disk insert / eject
232
+ // ---------------------------------------------------------------------------
233
+
234
+ TEST_CASE("Disk2Card hasDisk is false by default", "[disk2]") {
235
+ Disk2Card card(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
236
+ REQUIRE_FALSE(card.hasDisk(0));
237
+ REQUIRE_FALSE(card.hasDisk(1));
238
+ }
239
+
240
+ TEST_CASE("Disk2Card insertDisk with DSK data makes hasDisk true", "[disk2]") {
241
+ Disk2Card card(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
242
+
243
+ // Create a minimal valid DSK image (143360 bytes, all zeros)
244
+ std::vector<uint8_t> dskData(143360, 0x00);
245
+
246
+ bool result = card.insertDisk(0, dskData.data(), dskData.size(), "test.dsk");
247
+ REQUIRE(result);
248
+ REQUIRE(card.hasDisk(0));
249
+ }
250
+
251
+ TEST_CASE("Disk2Card insertDisk into drive 2", "[disk2]") {
252
+ Disk2Card card(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
253
+
254
+ std::vector<uint8_t> dskData(143360, 0x00);
255
+ bool result = card.insertDisk(1, dskData.data(), dskData.size(), "test.dsk");
256
+ REQUIRE(result);
257
+ REQUIRE(card.hasDisk(1));
258
+ REQUIRE_FALSE(card.hasDisk(0)); // drive 1 still empty
259
+ }
260
+
261
+ TEST_CASE("Disk2Card ejectDisk clears the disk", "[disk2]") {
262
+ Disk2Card card(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
263
+
264
+ std::vector<uint8_t> dskData(143360, 0x00);
265
+ card.insertDisk(0, dskData.data(), dskData.size(), "test.dsk");
266
+ REQUIRE(card.hasDisk(0));
267
+
268
+ card.ejectDisk(0);
269
+ REQUIRE_FALSE(card.hasDisk(0));
270
+ }
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // Card metadata
274
+ // ---------------------------------------------------------------------------
275
+
276
+ TEST_CASE("Disk2Card getName returns Disk II", "[disk2]") {
277
+ Disk2Card card(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
278
+ REQUIRE(std::string(card.getName()) == "Disk II");
279
+ }
280
+
281
+ TEST_CASE("Disk2Card getPreferredSlot returns 6", "[disk2]") {
282
+ Disk2Card card(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
283
+ REQUIRE(card.getPreferredSlot() == 6);
284
+ }
285
+
286
+ TEST_CASE("Disk2Card hasROM returns true", "[disk2]") {
287
+ Disk2Card card(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
288
+ REQUIRE(card.hasROM());
289
+ }
290
+
291
+ TEST_CASE("Disk2Card hasExpansionROM returns false", "[disk2]") {
292
+ Disk2Card card(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
293
+ REQUIRE_FALSE(card.hasExpansionROM());
294
+ }
295
+
296
+ // ---------------------------------------------------------------------------
297
+ // Serialization round-trip
298
+ // ---------------------------------------------------------------------------
299
+
300
+ TEST_CASE("Disk2Card getStateSize is greater than zero", "[disk2]") {
301
+ Disk2Card card(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
302
+ REQUIRE(card.getStateSize() > 0);
303
+ }
304
+
305
+ TEST_CASE("Disk2Card serialize/deserialize round-trip preserves state", "[disk2]") {
306
+ Disk2Card card1(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
307
+
308
+ // Set up some state
309
+ card1.readIO(0x09); // motor on
310
+ card1.readIO(0x0B); // drive 2
311
+ card1.readIO(0x0D); // Q6H
312
+ card1.readIO(0x01); // phase 0 on
313
+
314
+ size_t stateSize = card1.getStateSize();
315
+ std::vector<uint8_t> buffer(stateSize);
316
+
317
+ size_t written = card1.serialize(buffer.data(), buffer.size());
318
+ REQUIRE(written > 0);
319
+ REQUIRE(written <= stateSize);
320
+
321
+ Disk2Card card2(roms::ROM_DISK2, roms::ROM_DISK2_SIZE);
322
+ size_t consumed = card2.deserialize(buffer.data(), written);
323
+ REQUIRE(consumed > 0);
324
+
325
+ // Verify key state was preserved
326
+ REQUIRE(card2.getSelectedDrive() == card1.getSelectedDrive());
327
+ REQUIRE(card2.getQ6() == card1.getQ6());
328
+ REQUIRE(card2.getQ7() == card1.getQ7());
329
+ REQUIRE(card2.getPhaseStates() == card1.getPhaseStates());
330
+ }
@@ -0,0 +1,273 @@
1
+ /*
2
+ * test_dos33.cpp - Unit tests for DOS 3.3 filesystem parser
3
+ *
4
+ * Tests the DOS 3.3 filesystem reader including:
5
+ * - Format detection (isDOS33)
6
+ * - Catalog reading
7
+ * - File data retrieval
8
+ * - Binary file header extraction
9
+ * - Multiple files in catalog
10
+ */
11
+
12
+ #define CATCH_CONFIG_MAIN
13
+ #include "catch.hpp"
14
+
15
+ #include "dos33.hpp"
16
+ #include "disk_image_builder.hpp"
17
+
18
+ #include <array>
19
+ #include <cstring>
20
+ #include <vector>
21
+
22
+ using namespace a2e;
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // isDOS33 - Format detection
26
+ // ---------------------------------------------------------------------------
27
+
28
+ TEST_CASE("isDOS33 returns true for valid DOS 3.3 image", "[dos33][detection]") {
29
+ test::DOS33DiskBuilder builder;
30
+ REQUIRE(DOS33::isDOS33(builder.data(), builder.size()));
31
+ }
32
+
33
+ TEST_CASE("isDOS33 returns false for zeroed data", "[dos33][detection]") {
34
+ std::vector<uint8_t> zeroed(143360, 0x00);
35
+ REQUIRE_FALSE(DOS33::isDOS33(zeroed.data(), zeroed.size()));
36
+ }
37
+
38
+ TEST_CASE("isDOS33 returns false for too-small data", "[dos33][detection]") {
39
+ std::vector<uint8_t> small(1024, 0x00);
40
+ REQUIRE_FALSE(DOS33::isDOS33(small.data(), small.size()));
41
+ }
42
+
43
+ TEST_CASE("isDOS33 returns false for null pointer", "[dos33][detection]") {
44
+ REQUIRE_FALSE(DOS33::isDOS33(nullptr, 0));
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // readCatalog - Catalog reading
49
+ // ---------------------------------------------------------------------------
50
+
51
+ TEST_CASE("readCatalog returns correct file count after addFile", "[dos33][catalog]") {
52
+ test::DOS33DiskBuilder builder;
53
+
54
+ const uint8_t fileData[] = "HELLO WORLD";
55
+ builder.addFile("TESTFILE", 0x00, fileData, sizeof(fileData));
56
+
57
+ DOS33CatalogEntry entries[32];
58
+ int count = DOS33::readCatalog(builder.data(), builder.size(), entries, 32);
59
+ REQUIRE(count == 1);
60
+ }
61
+
62
+ TEST_CASE("Catalog entry has correct filename", "[dos33][catalog]") {
63
+ test::DOS33DiskBuilder builder;
64
+
65
+ const uint8_t fileData[] = "CONTENT";
66
+ builder.addFile("MYFILE", 0x00, fileData, sizeof(fileData));
67
+
68
+ DOS33CatalogEntry entries[32];
69
+ int count = DOS33::readCatalog(builder.data(), builder.size(), entries, 32);
70
+ REQUIRE(count == 1);
71
+ REQUIRE(std::string(entries[0].filename) == "MYFILE");
72
+ }
73
+
74
+ TEST_CASE("Catalog entry has correct file type for text file", "[dos33][catalog]") {
75
+ test::DOS33DiskBuilder builder;
76
+
77
+ const uint8_t fileData[] = "TEXT DATA";
78
+ builder.addFile("README", 0x00, fileData, sizeof(fileData)); // 0x00 = Text
79
+
80
+ DOS33CatalogEntry entries[32];
81
+ int count = DOS33::readCatalog(builder.data(), builder.size(), entries, 32);
82
+ REQUIRE(count == 1);
83
+ CHECK(entries[0].fileType == 0x00);
84
+ CHECK(std::string(entries[0].fileTypeName) == "T");
85
+ }
86
+
87
+ TEST_CASE("Catalog entry has correct file type for binary file", "[dos33][catalog]") {
88
+ test::DOS33DiskBuilder builder;
89
+
90
+ const uint8_t fileData[] = {0x00, 0x20, 0x05, 0x00, 0xEA, 0xEA, 0xEA, 0x60, 0x00};
91
+ builder.addFile("BINFILE", 0x04, fileData, sizeof(fileData)); // 0x04 = Binary
92
+
93
+ DOS33CatalogEntry entries[32];
94
+ int count = DOS33::readCatalog(builder.data(), builder.size(), entries, 32);
95
+ REQUIRE(count == 1);
96
+ CHECK(entries[0].fileType == 0x04);
97
+ CHECK(std::string(entries[0].fileTypeName) == "B");
98
+ }
99
+
100
+ TEST_CASE("Catalog entry locked status is correctly set", "[dos33][catalog]") {
101
+ test::DOS33DiskBuilder builder;
102
+
103
+ const uint8_t data1[] = "UNLOCKED";
104
+ const uint8_t data2[] = "LOCKED";
105
+ builder.addFile("FREE", 0x00, data1, sizeof(data1), false);
106
+ builder.addFile("PROT", 0x00, data2, sizeof(data2), true);
107
+
108
+ DOS33CatalogEntry entries[32];
109
+ int count = DOS33::readCatalog(builder.data(), builder.size(), entries, 32);
110
+ REQUIRE(count == 2);
111
+
112
+ // Find each entry by name
113
+ bool foundFree = false, foundProt = false;
114
+ for (int i = 0; i < count; i++) {
115
+ if (std::string(entries[i].filename) == "FREE") {
116
+ CHECK_FALSE(entries[i].isLocked);
117
+ foundFree = true;
118
+ }
119
+ if (std::string(entries[i].filename) == "PROT") {
120
+ CHECK(entries[i].isLocked);
121
+ foundProt = true;
122
+ }
123
+ }
124
+ REQUIRE(foundFree);
125
+ REQUIRE(foundProt);
126
+ }
127
+
128
+ TEST_CASE("readCatalog with empty disk returns zero", "[dos33][catalog]") {
129
+ test::DOS33DiskBuilder builder;
130
+
131
+ DOS33CatalogEntry entries[32];
132
+ int count = DOS33::readCatalog(builder.data(), builder.size(), entries, 32);
133
+ REQUIRE(count == 0);
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Multiple files in catalog
138
+ // ---------------------------------------------------------------------------
139
+
140
+ TEST_CASE("Multiple files in catalog are all enumerated", "[dos33][catalog]") {
141
+ test::DOS33DiskBuilder builder;
142
+
143
+ const uint8_t data1[] = "FILE ONE DATA";
144
+ const uint8_t data2[] = "FILE TWO DATA";
145
+ const uint8_t data3[] = "FILE THREE DATA";
146
+
147
+ builder.addFile("FILE1", 0x00, data1, sizeof(data1));
148
+ builder.addFile("FILE2", 0x02, data2, sizeof(data2)); // 0x02 = Applesoft
149
+ builder.addFile("FILE3", 0x04, data3, sizeof(data3)); // 0x04 = Binary
150
+
151
+ DOS33CatalogEntry entries[32];
152
+ int count = DOS33::readCatalog(builder.data(), builder.size(), entries, 32);
153
+ REQUIRE(count == 3);
154
+
155
+ // Verify filenames are present (order may vary)
156
+ bool found1 = false, found2 = false, found3 = false;
157
+ for (int i = 0; i < count; i++) {
158
+ std::string name(entries[i].filename);
159
+ if (name == "FILE1") found1 = true;
160
+ if (name == "FILE2") found2 = true;
161
+ if (name == "FILE3") found3 = true;
162
+ }
163
+ CHECK(found1);
164
+ CHECK(found2);
165
+ CHECK(found3);
166
+ }
167
+
168
+ TEST_CASE("Catalog entries have non-zero sector count", "[dos33][catalog]") {
169
+ test::DOS33DiskBuilder builder;
170
+
171
+ const uint8_t fileData[] = "SOME DATA CONTENT";
172
+ builder.addFile("HASDATA", 0x00, fileData, sizeof(fileData));
173
+
174
+ DOS33CatalogEntry entries[32];
175
+ int count = DOS33::readCatalog(builder.data(), builder.size(), entries, 32);
176
+ REQUIRE(count == 1);
177
+ CHECK(entries[0].sectorCount > 0);
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // readFile - File data retrieval
182
+ // ---------------------------------------------------------------------------
183
+
184
+ TEST_CASE("readFile retrieves file data correctly", "[dos33][readFile]") {
185
+ test::DOS33DiskBuilder builder;
186
+
187
+ // Create a file with known content
188
+ std::vector<uint8_t> fileData(64);
189
+ for (int i = 0; i < 64; i++) {
190
+ fileData[i] = static_cast<uint8_t>(i);
191
+ }
192
+ builder.addFile("SEQDATA", 0x00, fileData.data(), static_cast<int>(fileData.size()));
193
+
194
+ // Read catalog to get the file's first track/sector
195
+ DOS33CatalogEntry entries[32];
196
+ int count = DOS33::readCatalog(builder.data(), builder.size(), entries, 32);
197
+ REQUIRE(count == 1);
198
+
199
+ // Read the file data
200
+ std::vector<uint8_t> outBuf(4096, 0);
201
+ int bytesRead = DOS33::readFile(builder.data(), builder.size(),
202
+ entries[0].firstTrack, entries[0].firstSector,
203
+ outBuf.data(), static_cast<int>(outBuf.size()));
204
+ REQUIRE(bytesRead > 0);
205
+
206
+ // Verify the data is present in the output
207
+ // The raw file data starts from the first data sector
208
+ bool found = false;
209
+ for (int offset = 0; offset <= bytesRead - 64; offset++) {
210
+ if (memcmp(&outBuf[offset], fileData.data(), 64) == 0) {
211
+ found = true;
212
+ break;
213
+ }
214
+ }
215
+ CHECK(found);
216
+ }
217
+
218
+ TEST_CASE("readFile returns zero for invalid track/sector", "[dos33][readFile]") {
219
+ test::DOS33DiskBuilder builder;
220
+
221
+ std::vector<uint8_t> outBuf(4096, 0);
222
+ int bytesRead = DOS33::readFile(builder.data(), builder.size(),
223
+ 0, 0, outBuf.data(),
224
+ static_cast<int>(outBuf.size()));
225
+ CHECK(bytesRead == 0);
226
+ }
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // getBinaryFileInfo - Binary file header extraction
230
+ // ---------------------------------------------------------------------------
231
+
232
+ TEST_CASE("getBinaryFileInfo extracts address and length from binary header", "[dos33][binaryInfo]") {
233
+ // Binary file format: first 4 bytes are [addrLo, addrHi, lenLo, lenHi]
234
+ uint8_t binaryFile[] = {
235
+ 0x00, 0x20, // Load address = $2000
236
+ 0x05, 0x00, // Length = 5 bytes
237
+ 0xEA, 0xEA, 0xEA, 0xEA, 0x60 // NOP NOP NOP NOP RTS
238
+ };
239
+
240
+ uint16_t address = 0, length = 0;
241
+ bool result = DOS33::getBinaryFileInfo(binaryFile, sizeof(binaryFile),
242
+ &address, &length);
243
+ REQUIRE(result);
244
+ CHECK(address == 0x2000);
245
+ CHECK(length == 5);
246
+ }
247
+
248
+ TEST_CASE("getBinaryFileInfo returns false for too-small data", "[dos33][binaryInfo]") {
249
+ uint8_t tinyData[] = {0x00, 0x20};
250
+
251
+ uint16_t address = 0, length = 0;
252
+ bool result = DOS33::getBinaryFileInfo(tinyData, sizeof(tinyData),
253
+ &address, &length);
254
+ CHECK_FALSE(result);
255
+ }
256
+
257
+ TEST_CASE("getBinaryFileInfo with various load addresses", "[dos33][binaryInfo]") {
258
+ SECTION("Zero page address") {
259
+ uint8_t data[] = {0x00, 0x00, 0x10, 0x00, 0x00};
260
+ uint16_t addr, len;
261
+ REQUIRE(DOS33::getBinaryFileInfo(data, sizeof(data), &addr, &len));
262
+ CHECK(addr == 0x0000);
263
+ CHECK(len == 0x0010);
264
+ }
265
+
266
+ SECTION("High memory address") {
267
+ uint8_t data[] = {0x00, 0xBF, 0x00, 0x01, 0x00};
268
+ uint16_t addr, len;
269
+ REQUIRE(DOS33::getBinaryFileInfo(data, sizeof(data), &addr, &len));
270
+ CHECK(addr == 0xBF00);
271
+ CHECK(len == 0x0100);
272
+ }
273
+ }