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,549 @@
1
+ /*
2
+ * prodos.js - ProDOS filesystem parser
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ /**
9
+ * ProDOS Filesystem Parser
10
+ * Parses ProDOS disk images to read catalog and file contents
11
+ */
12
+
13
+ import { parseProDOSFilename } from './utils.js';
14
+
15
+ // ProDOS Constants
16
+ const BLOCK_SIZE = 512;
17
+ const VOLUME_DIRECTORY_BLOCK = 2;
18
+ const ENTRIES_PER_BLOCK = 13;
19
+ const ENTRY_SIZE = 39;
20
+
21
+ // Storage types
22
+ const STORAGE_TYPE_DELETED = 0x0;
23
+ const STORAGE_TYPE_SEEDLING = 0x1; // Single block file (<=512 bytes)
24
+ const STORAGE_TYPE_SAPLING = 0x2; // 2-256 blocks (index block + data)
25
+ const STORAGE_TYPE_TREE = 0x3; // 257+ blocks (master index + indexes + data)
26
+ const STORAGE_TYPE_SUBDIR = 0xD; // Subdirectory
27
+ const STORAGE_TYPE_SUBDIR_HEADER = 0xE; // Subdirectory header
28
+ const STORAGE_TYPE_VOLUME_HEADER = 0xF; // Volume directory header
29
+
30
+ // ProDOS file types
31
+ const FILE_TYPES = {
32
+ 0x00: { name: 'UNK', description: 'Unknown' },
33
+ 0x01: { name: 'BAD', description: 'Bad Block' },
34
+ 0x04: { name: 'TXT', description: 'Text' },
35
+ 0x06: { name: 'BIN', description: 'Binary' },
36
+ 0x0F: { name: 'DIR', description: 'Directory' },
37
+ 0x19: { name: 'ADB', description: 'AppleWorks DB' },
38
+ 0x1A: { name: 'AWP', description: 'AppleWorks WP' },
39
+ 0x1B: { name: 'ASP', description: 'AppleWorks SS' },
40
+ 0xB0: { name: 'SRC', description: 'Source Code' },
41
+ 0xB3: { name: 'S16', description: 'GS/OS App' },
42
+ 0xBF: { name: 'DOC', description: 'Document' },
43
+ 0xC0: { name: 'PNT', description: 'Packed HiRes' },
44
+ 0xC1: { name: 'PIC', description: 'HiRes Picture' },
45
+ 0xE0: { name: 'SHK', description: 'ShrinkIt Archive' },
46
+ 0xEF: { name: 'PAS', description: 'Pascal' },
47
+ 0xF0: { name: 'CMD', description: 'Command' },
48
+ 0xFA: { name: 'INT', description: 'Integer BASIC' },
49
+ 0xFB: { name: 'IVR', description: 'Integer Vars' },
50
+ 0xFC: { name: 'BAS', description: 'Applesoft BASIC' },
51
+ 0xFD: { name: 'VAR', description: 'Applesoft Vars' },
52
+ 0xFE: { name: 'REL', description: 'Relocatable' },
53
+ 0xFF: { name: 'SYS', description: 'System' },
54
+ };
55
+
56
+ // Standard 140K disk size
57
+ const DISK_140K_SIZE = 143360;
58
+
59
+ // ProDOS logical sector to DOS logical sector conversion
60
+ // When reading a DOS-order disk image (.DO/.DSK), we need to convert
61
+ // ProDOS sector numbers to DOS sector numbers to find the right file offset
62
+ const PRODOS_TO_DOS_SECTOR = [0, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 15];
63
+
64
+ /**
65
+ * Read a 512-byte block from disk data (internal)
66
+ * @param {Uint8Array} diskData - Raw disk image data
67
+ * @param {number} blockNum - Block number
68
+ * @param {boolean} dosOrder - If true, convert ProDOS sectors to DOS sector offsets
69
+ * @returns {Uint8Array} 512-byte block data
70
+ */
71
+ function readBlockInternal(diskData, blockNum, dosOrder) {
72
+ const result = new Uint8Array(BLOCK_SIZE);
73
+
74
+ if (diskData.length <= DISK_140K_SIZE) {
75
+ // 140K disk - block N is at track N/8, ProDOS sectors (N%8)*2 and (N%8)*2+1
76
+ const track = Math.floor(blockNum / 8);
77
+ const blockInTrack = blockNum % 8;
78
+
79
+ // ProDOS logical sectors for this block
80
+ const prodosSector1 = blockInTrack * 2;
81
+ const prodosSector2 = blockInTrack * 2 + 1;
82
+
83
+ let sector1, sector2;
84
+ if (dosOrder) {
85
+ // Convert ProDOS sector numbers to DOS sector numbers
86
+ sector1 = PRODOS_TO_DOS_SECTOR[prodosSector1];
87
+ sector2 = PRODOS_TO_DOS_SECTOR[prodosSector2];
88
+ } else {
89
+ // File is already in ProDOS order
90
+ sector1 = prodosSector1;
91
+ sector2 = prodosSector2;
92
+ }
93
+
94
+ // Calculate offsets (256 bytes per sector, 16 sectors per track)
95
+ const offset1 = (track * 16 + sector1) * 256;
96
+ const offset2 = (track * 16 + sector2) * 256;
97
+
98
+ if (offset1 + 256 <= diskData.length) {
99
+ result.set(diskData.slice(offset1, offset1 + 256), 0);
100
+ }
101
+ if (offset2 + 256 <= diskData.length) {
102
+ result.set(diskData.slice(offset2, offset2 + 256), 256);
103
+ }
104
+ } else {
105
+ // Larger disk (800K, etc.) - blocks are sequential
106
+ const offset = blockNum * BLOCK_SIZE;
107
+ if (offset + BLOCK_SIZE <= diskData.length) {
108
+ result.set(diskData.slice(offset, offset + BLOCK_SIZE));
109
+ }
110
+ }
111
+
112
+ return result;
113
+ }
114
+
115
+ /**
116
+ * Read a 512-byte block from disk data using specified sector ordering
117
+ * @param {Uint8Array} diskData - Raw disk image data
118
+ * @param {number} blockNum - Block number
119
+ * @param {boolean} dosOrder - If true, use DOS sector ordering
120
+ * @returns {Uint8Array} 512-byte block data
121
+ */
122
+ function readBlock(diskData, blockNum, dosOrder) {
123
+ return readBlockInternal(diskData, blockNum, dosOrder);
124
+ }
125
+
126
+ /**
127
+ * Check if a block contains a valid ProDOS volume header
128
+ * @param {Uint8Array} block - 512-byte block data
129
+ * @returns {boolean} True if valid volume header
130
+ */
131
+ function isValidVolumeHeader(block) {
132
+ const storageTypeAndNameLength = block[0x04];
133
+ const storageType = (storageTypeAndNameLength >> 4) & 0x0F;
134
+ const nameLength = storageTypeAndNameLength & 0x0F;
135
+
136
+ // Must be volume header type
137
+ if (storageType !== STORAGE_TYPE_VOLUME_HEADER) {
138
+ return false;
139
+ }
140
+
141
+ // Name length must be 1-15
142
+ if (nameLength === 0 || nameLength > 15) {
143
+ return false;
144
+ }
145
+
146
+ // Check standard ProDOS values
147
+ const entryLength = block[0x23];
148
+ const entriesPerBlock = block[0x24];
149
+
150
+ if (entryLength !== 0x27 || entriesPerBlock !== 0x0D) {
151
+ return false;
152
+ }
153
+
154
+ return true;
155
+ }
156
+
157
+ /**
158
+ * Parse the volume header from block 2
159
+ * Tries both sector orderings (ProDOS-order and DOS-order) for 140K disks
160
+ * @param {Uint8Array} diskData - Raw disk image data
161
+ * @returns {Object|null} Volume info or null if not ProDOS
162
+ */
163
+ export function parseVolumeInfo(diskData) {
164
+ if (diskData.length < DISK_140K_SIZE) {
165
+ return null;
166
+ }
167
+
168
+ // Try ProDOS sector order first (for .PO files)
169
+ let block2 = readBlockInternal(diskData, 2, false);
170
+ let useDOSSectorOrder = false;
171
+
172
+ if (!isValidVolumeHeader(block2)) {
173
+ // Try DOS sector order (for .DO/.DSK files)
174
+ block2 = readBlockInternal(diskData, 2, true);
175
+ if (!isValidVolumeHeader(block2)) {
176
+ return null;
177
+ }
178
+ useDOSSectorOrder = true;
179
+ }
180
+
181
+ const storageTypeAndNameLength = block2[0x04];
182
+ const nameLength = storageTypeAndNameLength & 0x0F;
183
+
184
+ // Volume name is at offset 0x05
185
+ const volumeName = parseProDOSFilename(block2.slice(0x05, 0x05 + nameLength));
186
+
187
+ // Validate volume name contains only valid ProDOS characters (A-Z, 0-9, .)
188
+ if (!/^[A-Z][A-Z0-9.]*$/i.test(volumeName)) {
189
+ return null;
190
+ }
191
+
192
+ // Get additional volume info
193
+ const creationDate = parseProDOSDate(block2[0x1C] | (block2[0x1D] << 8),
194
+ block2[0x1E] | (block2[0x1F] << 8));
195
+ const version = block2[0x20];
196
+ const minVersion = block2[0x21];
197
+ const access = block2[0x22];
198
+ const entryLength = block2[0x23];
199
+ const entriesPerBlock = block2[0x24];
200
+ const fileCount = block2[0x25] | (block2[0x26] << 8);
201
+ const bitmapPointer = block2[0x27] | (block2[0x28] << 8);
202
+ const totalBlocks = block2[0x29] | (block2[0x2A] << 8);
203
+
204
+ // Validate total blocks is reasonable for disk size
205
+ const expectedBlocks = diskData.length <= DISK_140K_SIZE ? 280 : Math.floor(diskData.length / BLOCK_SIZE);
206
+ if (totalBlocks === 0 || totalBlocks > expectedBlocks + 10) {
207
+ return null;
208
+ }
209
+
210
+ return {
211
+ volumeName,
212
+ creationDate,
213
+ version,
214
+ minVersion,
215
+ access,
216
+ entryLength,
217
+ entriesPerBlock,
218
+ fileCount,
219
+ bitmapPointer,
220
+ totalBlocks,
221
+ useDOSSectorOrder, // Include sector ordering for subsequent reads
222
+ };
223
+ }
224
+
225
+ /**
226
+ * Parse ProDOS date/time
227
+ * @param {number} dateWord - Date word
228
+ * @param {number} timeWord - Time word
229
+ * @returns {Date|null} Parsed date or null
230
+ */
231
+ function parseProDOSDate(dateWord, timeWord) {
232
+ if (dateWord === 0) return null;
233
+
234
+ const year = ((dateWord >> 9) & 0x7F) + 1900;
235
+ const month = (dateWord >> 5) & 0x0F;
236
+ const day = dateWord & 0x1F;
237
+ const hour = (timeWord >> 8) & 0x1F;
238
+ const minute = timeWord & 0x3F;
239
+
240
+ // Adjust year - ProDOS uses years 0-99 for 1900-1999 and 2000-2039
241
+ const adjustedYear = year < 1940 ? year + 100 : year;
242
+
243
+ return new Date(adjustedYear, month - 1, day, hour, minute);
244
+ }
245
+
246
+
247
+ /**
248
+ * Parse a directory entry
249
+ * @param {Uint8Array} entry - 39-byte directory entry
250
+ * @returns {Object|null} Parsed entry or null if empty/deleted
251
+ */
252
+ function parseDirectoryEntry(entry) {
253
+ const storageTypeAndNameLength = entry[0x00];
254
+ const storageType = (storageTypeAndNameLength >> 4) & 0x0F;
255
+ const nameLength = storageTypeAndNameLength & 0x0F;
256
+
257
+ // Check for deleted or empty entry
258
+ if (storageType === STORAGE_TYPE_DELETED || nameLength === 0) {
259
+ return null;
260
+ }
261
+
262
+ // Skip volume and subdir headers
263
+ if (storageType === STORAGE_TYPE_VOLUME_HEADER ||
264
+ storageType === STORAGE_TYPE_SUBDIR_HEADER) {
265
+ return null;
266
+ }
267
+
268
+ const filename = parseProDOSFilename(entry.slice(0x01, 0x01 + nameLength));
269
+ const fileType = entry[0x10];
270
+ const keyPointer = entry[0x11] | (entry[0x12] << 8);
271
+ const blocksUsed = entry[0x13] | (entry[0x14] << 8);
272
+ const eof = entry[0x15] | (entry[0x16] << 8) | (entry[0x17] << 16);
273
+ const creationDate = parseProDOSDate(entry[0x18] | (entry[0x19] << 8),
274
+ entry[0x1A] | (entry[0x1B] << 8));
275
+ const version = entry[0x1C];
276
+ const minVersion = entry[0x1D];
277
+ const access = entry[0x1E];
278
+ const auxType = entry[0x1F] | (entry[0x20] << 8);
279
+ const modDate = parseProDOSDate(entry[0x21] | (entry[0x22] << 8),
280
+ entry[0x23] | (entry[0x24] << 8));
281
+ const headerPointer = entry[0x25] | (entry[0x26] << 8);
282
+
283
+ const typeInfo = FILE_TYPES[fileType] || { name: `$${fileType.toString(16).toUpperCase().padStart(2, '0')}`, description: 'Unknown' };
284
+
285
+ return {
286
+ storageType,
287
+ filename,
288
+ fileType,
289
+ fileTypeName: typeInfo.name,
290
+ fileTypeDescription: typeInfo.description,
291
+ keyPointer,
292
+ blocksUsed,
293
+ eof,
294
+ creationDate,
295
+ version,
296
+ minVersion,
297
+ access,
298
+ auxType,
299
+ modDate,
300
+ headerPointer,
301
+ isLocked: (access & 0x02) === 0, // Write-disabled = locked
302
+ isDirectory: storageType === STORAGE_TYPE_SUBDIR,
303
+ };
304
+ }
305
+
306
+ /**
307
+ * Read a directory (volume or subdirectory)
308
+ * @param {Uint8Array} diskData - Raw disk image data
309
+ * @param {number} startBlock - Starting block of directory
310
+ * @param {string} pathPrefix - Path prefix for entries
311
+ * @param {boolean} dosOrder - If true, use DOS sector ordering
312
+ * @returns {Array} Array of directory entries
313
+ */
314
+ function readDirectory(diskData, startBlock, pathPrefix = '', dosOrder = false) {
315
+ const entries = [];
316
+ let blockNum = startBlock;
317
+ const visited = new Set();
318
+
319
+ while (blockNum !== 0) {
320
+ if (visited.has(blockNum)) break;
321
+ visited.add(blockNum);
322
+
323
+ const block = readBlock(diskData, blockNum, dosOrder);
324
+
325
+ // Get prev/next block pointers (first 4 bytes of block)
326
+ const nextBlock = block[0x02] | (block[0x03] << 8);
327
+
328
+ // Parse entries in this block
329
+ // First block has header at entry 0, subsequent blocks start at entry 0
330
+ const firstEntry = blockNum === startBlock ? 1 : 0;
331
+ const entryOffset = blockNum === startBlock ? 0x04 : 0x04;
332
+
333
+ for (let i = firstEntry; i < ENTRIES_PER_BLOCK; i++) {
334
+ const offset = 0x04 + (i * ENTRY_SIZE);
335
+ if (offset + ENTRY_SIZE > BLOCK_SIZE) break;
336
+
337
+ const entryData = block.slice(offset, offset + ENTRY_SIZE);
338
+ const entry = parseDirectoryEntry(entryData);
339
+
340
+ if (entry) {
341
+ entry.path = pathPrefix ? `${pathPrefix}/${entry.filename}` : entry.filename;
342
+ entries.push(entry);
343
+ }
344
+ }
345
+
346
+ blockNum = nextBlock;
347
+ }
348
+
349
+ return entries;
350
+ }
351
+
352
+ /**
353
+ * Read the disk catalog (volume directory and all subdirectories)
354
+ * @param {Uint8Array} diskData - Raw disk image data
355
+ * @returns {Array} Array of all file entries with full paths (includes _dosOrder property for readFile)
356
+ */
357
+ export function readCatalog(diskData) {
358
+ const volumeInfo = parseVolumeInfo(diskData);
359
+ if (!volumeInfo) {
360
+ return [];
361
+ }
362
+
363
+ const dosOrder = volumeInfo.useDOSSectorOrder;
364
+ const allEntries = [];
365
+ const dirsToProcess = [{ block: VOLUME_DIRECTORY_BLOCK, path: '' }];
366
+
367
+ while (dirsToProcess.length > 0) {
368
+ const { block, path } = dirsToProcess.shift();
369
+ const entries = readDirectory(diskData, block, path, dosOrder);
370
+
371
+ for (const entry of entries) {
372
+ // Attach sector ordering to each entry for use by readFile
373
+ entry._dosOrder = dosOrder;
374
+ if (entry.isDirectory) {
375
+ // Queue subdirectory for processing
376
+ dirsToProcess.push({
377
+ block: entry.keyPointer,
378
+ path: entry.path,
379
+ });
380
+ }
381
+ allEntries.push(entry);
382
+ }
383
+ }
384
+
385
+ return allEntries;
386
+ }
387
+
388
+ /**
389
+ * Read file contents using appropriate storage type
390
+ * @param {Uint8Array} diskData - Raw disk image data
391
+ * @param {Object} entry - Catalog entry for the file (includes _dosOrder from readCatalog)
392
+ * @returns {Uint8Array} File contents
393
+ */
394
+ export function readFile(diskData, entry) {
395
+ if (entry.isDirectory) {
396
+ return new Uint8Array(0);
397
+ }
398
+
399
+ const eof = entry.eof;
400
+ const dosOrder = entry._dosOrder || false;
401
+ let data;
402
+
403
+ switch (entry.storageType) {
404
+ case STORAGE_TYPE_SEEDLING:
405
+ data = readSeedlingFile(diskData, entry.keyPointer, eof, dosOrder);
406
+ break;
407
+ case STORAGE_TYPE_SAPLING:
408
+ data = readSaplingFile(diskData, entry.keyPointer, eof, dosOrder);
409
+ break;
410
+ case STORAGE_TYPE_TREE:
411
+ data = readTreeFile(diskData, entry.keyPointer, eof, dosOrder);
412
+ break;
413
+ default:
414
+ data = new Uint8Array(0);
415
+ }
416
+
417
+ return data;
418
+ }
419
+
420
+ /**
421
+ * Read a seedling file (single block, <=512 bytes)
422
+ * @param {Uint8Array} diskData - Raw disk image data
423
+ * @param {number} blockNum - Block number containing file data
424
+ * @param {number} eof - File size
425
+ * @param {boolean} dosOrder - If true, use DOS sector ordering
426
+ * @returns {Uint8Array} File contents
427
+ */
428
+ function readSeedlingFile(diskData, blockNum, eof, dosOrder) {
429
+ const block = readBlock(diskData, blockNum, dosOrder);
430
+ return block.slice(0, eof);
431
+ }
432
+
433
+ /**
434
+ * Read a sapling file (index block + data blocks)
435
+ * @param {Uint8Array} diskData - Raw disk image data
436
+ * @param {number} indexBlock - Block number of index block
437
+ * @param {number} eof - File size
438
+ * @param {boolean} dosOrder - If true, use DOS sector ordering
439
+ * @returns {Uint8Array} File contents
440
+ */
441
+ function readSaplingFile(diskData, indexBlock, eof, dosOrder) {
442
+ const index = readBlock(diskData, indexBlock, dosOrder);
443
+ const result = new Uint8Array(eof);
444
+ let bytesRead = 0;
445
+
446
+ for (let i = 0; i < 256 && bytesRead < eof; i++) {
447
+ const dataBlockNum = index[i] | (index[i + 256] << 8);
448
+ if (dataBlockNum === 0) {
449
+ // Sparse file - fill with zeros
450
+ const bytesToFill = Math.min(BLOCK_SIZE, eof - bytesRead);
451
+ bytesRead += bytesToFill;
452
+ continue;
453
+ }
454
+
455
+ const dataBlock = readBlock(diskData, dataBlockNum, dosOrder);
456
+ const bytesToCopy = Math.min(BLOCK_SIZE, eof - bytesRead);
457
+ result.set(dataBlock.slice(0, bytesToCopy), bytesRead);
458
+ bytesRead += bytesToCopy;
459
+ }
460
+
461
+ return result;
462
+ }
463
+
464
+ /**
465
+ * Read a tree file (master index + index blocks + data blocks)
466
+ * @param {Uint8Array} diskData - Raw disk image data
467
+ * @param {number} masterBlock - Block number of master index block
468
+ * @param {number} eof - File size
469
+ * @param {boolean} dosOrder - If true, use DOS sector ordering
470
+ * @returns {Uint8Array} File contents
471
+ */
472
+ function readTreeFile(diskData, masterBlock, eof, dosOrder) {
473
+ const master = readBlock(diskData, masterBlock, dosOrder);
474
+ const result = new Uint8Array(eof);
475
+ let bytesRead = 0;
476
+
477
+ for (let i = 0; i < 128 && bytesRead < eof; i++) {
478
+ const indexBlockNum = master[i] | (master[i + 256] << 8);
479
+ if (indexBlockNum === 0) {
480
+ // Sparse - skip 256 blocks worth of data
481
+ const bytesToSkip = Math.min(256 * BLOCK_SIZE, eof - bytesRead);
482
+ bytesRead += bytesToSkip;
483
+ continue;
484
+ }
485
+
486
+ const index = readBlock(diskData, indexBlockNum, dosOrder);
487
+
488
+ for (let j = 0; j < 256 && bytesRead < eof; j++) {
489
+ const dataBlockNum = index[j] | (index[j + 256] << 8);
490
+ if (dataBlockNum === 0) {
491
+ const bytesToFill = Math.min(BLOCK_SIZE, eof - bytesRead);
492
+ bytesRead += bytesToFill;
493
+ continue;
494
+ }
495
+
496
+ const dataBlock = readBlock(diskData, dataBlockNum, dosOrder);
497
+ const bytesToCopy = Math.min(BLOCK_SIZE, eof - bytesRead);
498
+ result.set(dataBlock.slice(0, bytesToCopy), bytesRead);
499
+ bytesRead += bytesToCopy;
500
+ }
501
+ }
502
+
503
+ return result;
504
+ }
505
+
506
+ /**
507
+ * Map ProDOS file type to DOS 3.3 viewer type
508
+ * @param {number} prodosType - ProDOS file type
509
+ * @returns {number} DOS 3.3 equivalent type for viewer
510
+ */
511
+ export function mapFileTypeForViewer(prodosType) {
512
+ switch (prodosType) {
513
+ case 0x04: // TXT
514
+ return 0x00; // DOS 3.3 Text
515
+ case 0xFA: // INT
516
+ return 0x01; // DOS 3.3 Integer BASIC
517
+ case 0xFC: // BAS
518
+ return 0x02; // DOS 3.3 Applesoft BASIC
519
+ case 0x06: // BIN
520
+ case 0xFF: // SYS
521
+ return 0x04; // DOS 3.3 Binary
522
+ default:
523
+ return -1; // Use hex dump
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Get binary file info from ProDOS entry
529
+ * ProDOS stores load address in auxType field, not in file data
530
+ * @param {Object} entry - ProDOS catalog entry
531
+ * @returns {Object} {address, length}
532
+ */
533
+ export function getBinaryFileInfo(entry) {
534
+ return {
535
+ address: entry.auxType,
536
+ length: entry.eof,
537
+ };
538
+ }
539
+
540
+ /**
541
+ * Check if disk is ProDOS format
542
+ * @param {Uint8Array} diskData - Raw disk image data
543
+ * @returns {boolean} True if ProDOS format
544
+ */
545
+ export function isProDOS(diskData) {
546
+ return parseVolumeInfo(diskData) !== null;
547
+ }
548
+
549
+ export { FILE_TYPES, BLOCK_SIZE };
@@ -0,0 +1,67 @@
1
+ /*
2
+ * utils.js - Filename parsing utilities for Apple II disk formats
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ /**
9
+ * Parse Apple II filename from bytes (strips high bit ASCII)
10
+ * @param {Uint8Array} bytes - Filename bytes (may have high-bit set)
11
+ * @param {Object} options - Parsing options
12
+ * @param {boolean} [options.trimSpaces=true] - Whether to trim trailing spaces
13
+ * @param {boolean} [options.skipLeadingSpaces=false] - Whether to skip leading spaces
14
+ * @param {boolean} [options.stopAtNull=true] - Whether to stop at null byte
15
+ * @returns {string} Clean filename
16
+ */
17
+ export function parseFilename(bytes, options = {}) {
18
+ const {
19
+ trimSpaces = true,
20
+ skipLeadingSpaces = false,
21
+ stopAtNull = true,
22
+ } = options;
23
+
24
+ let name = '';
25
+ for (let i = 0; i < bytes.length; i++) {
26
+ const ch = bytes[i] & 0x7F; // Strip high bit
27
+
28
+ // Stop at null if enabled
29
+ if (stopAtNull && ch === 0) break;
30
+
31
+ // Skip null bytes (always)
32
+ if (ch === 0x00) continue;
33
+
34
+ // Skip leading spaces if enabled
35
+ if (skipLeadingSpaces && ch === 0x20 && name.length === 0) continue;
36
+
37
+ name += String.fromCharCode(ch);
38
+ }
39
+
40
+ return trimSpaces ? name.trimEnd() : name;
41
+ }
42
+
43
+ /**
44
+ * Parse DOS 3.3 filename (high-bit ASCII, trim spaces, skip leading spaces)
45
+ * @param {Uint8Array} bytes - Filename bytes
46
+ * @returns {string} Clean filename
47
+ */
48
+ export function parseDOS33Filename(bytes) {
49
+ return parseFilename(bytes, {
50
+ trimSpaces: true,
51
+ skipLeadingSpaces: true,
52
+ stopAtNull: false,
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Parse ProDOS filename (high-bit ASCII, stop at null)
58
+ * @param {Uint8Array} bytes - Filename bytes
59
+ * @returns {string} Clean filename
60
+ */
61
+ export function parseProDOSFilename(bytes) {
62
+ return parseFilename(bytes, {
63
+ trimSpaces: false,
64
+ skipLeadingSpaces: false,
65
+ stopAtNull: true,
66
+ });
67
+ }