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.
- package/.clangd +5 -0
- package/.mcp.json +12 -0
- package/CLAUDE.md +362 -0
- package/CMakeLists.txt +774 -0
- package/LICENSE +21 -0
- package/README.md +392 -0
- package/build-wasm/generated/roms.cpp +2447 -0
- package/docker-compose.staging.yml +9 -0
- package/docs/basic-rom-disassembly.md +6663 -0
- package/docs/softswitch-comparison.md +273 -0
- package/docs/thunderclock-debug.md +89 -0
- package/examples/cube.bas +72 -0
- package/examples/hello.s +55 -0
- package/examples/scroll.s +140 -0
- package/package.json +18 -0
- package/public/assets/apple-logo-old.png +0 -0
- package/public/assets/apple-logo.png +0 -0
- package/public/assets/drive-closed-light-on.png +0 -0
- package/public/assets/drive-closed.png +0 -0
- package/public/assets/drive-open-light-on.png +0 -0
- package/public/assets/drive-open.png +0 -0
- package/public/audio-worklet.js +82 -0
- package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
- package/public/disks/ProDOS 2.4.3.po +0 -0
- package/public/disks/h32mb.2mg +0 -0
- package/public/disks/library.json +26 -0
- package/public/docs/llms/llm-assembler.txt +90 -0
- package/public/docs/llms/llm-basic-program.txt +256 -0
- package/public/docs/llms/llm-disk-drives.txt +72 -0
- package/public/docs/llms/llm-file-explorer.txt +50 -0
- package/public/docs/llms/llm-hard-drives.txt +80 -0
- package/public/docs/llms/llm-main.txt +51 -0
- package/public/docs/llms/llm-slot-configuration.txt +66 -0
- package/public/icons/icon-192.svg +4 -0
- package/public/icons/icon-512.svg +4 -0
- package/public/index.html +661 -0
- package/public/llms.txt +49 -0
- package/public/manifest.json +29 -0
- package/public/shaders/burnin.glsl +22 -0
- package/public/shaders/crt.glsl +706 -0
- package/public/shaders/edge.glsl +109 -0
- package/public/shaders/vertex.glsl +8 -0
- package/public/sw.js +186 -0
- package/roms/341-0027.bin +0 -0
- package/roms/341-0160-A-US-UK.bin +0 -0
- package/roms/341-0160-A.bin +0 -0
- package/roms/342-0273-A-US-UK.bin +0 -0
- package/roms/342-0349-B-C0-FF.bin +0 -0
- package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
- package/roms/Thunderclock Plus ROM.bin +0 -0
- package/scripts/generate_roms.sh +69 -0
- package/src/bindings/wasm_interface.cpp +1940 -0
- package/src/core/assembler/assembler.cpp +1239 -0
- package/src/core/assembler/assembler.hpp +115 -0
- package/src/core/audio/audio.cpp +160 -0
- package/src/core/audio/audio.hpp +81 -0
- package/src/core/basic/basic_detokenizer.cpp +436 -0
- package/src/core/basic/basic_detokenizer.hpp +41 -0
- package/src/core/basic/basic_tokenizer.cpp +286 -0
- package/src/core/basic/basic_tokenizer.hpp +26 -0
- package/src/core/basic/basic_tokens.hpp +295 -0
- package/src/core/cards/disk2_card.cpp +568 -0
- package/src/core/cards/disk2_card.hpp +316 -0
- package/src/core/cards/expansion_card.hpp +185 -0
- package/src/core/cards/mockingboard/ay8910.cpp +616 -0
- package/src/core/cards/mockingboard/ay8910.hpp +159 -0
- package/src/core/cards/mockingboard/via6522.cpp +530 -0
- package/src/core/cards/mockingboard/via6522.hpp +163 -0
- package/src/core/cards/mockingboard_card.cpp +312 -0
- package/src/core/cards/mockingboard_card.hpp +159 -0
- package/src/core/cards/mouse_card.cpp +654 -0
- package/src/core/cards/mouse_card.hpp +190 -0
- package/src/core/cards/smartport/block_device.cpp +202 -0
- package/src/core/cards/smartport/block_device.hpp +60 -0
- package/src/core/cards/smartport/smartport_card.cpp +603 -0
- package/src/core/cards/smartport/smartport_card.hpp +120 -0
- package/src/core/cards/thunderclock_card.cpp +237 -0
- package/src/core/cards/thunderclock_card.hpp +122 -0
- package/src/core/cpu/cpu6502.cpp +1609 -0
- package/src/core/cpu/cpu6502.hpp +203 -0
- package/src/core/debug/condition_evaluator.cpp +470 -0
- package/src/core/debug/condition_evaluator.hpp +87 -0
- package/src/core/disassembler/disassembler.cpp +552 -0
- package/src/core/disassembler/disassembler.hpp +171 -0
- package/src/core/disk-image/disk_image.hpp +267 -0
- package/src/core/disk-image/dsk_disk_image.cpp +827 -0
- package/src/core/disk-image/dsk_disk_image.hpp +204 -0
- package/src/core/disk-image/gcr_encoding.cpp +147 -0
- package/src/core/disk-image/gcr_encoding.hpp +78 -0
- package/src/core/disk-image/woz_disk_image.cpp +1049 -0
- package/src/core/disk-image/woz_disk_image.hpp +343 -0
- package/src/core/emulator.cpp +2126 -0
- package/src/core/emulator.hpp +434 -0
- package/src/core/filesystem/dos33.cpp +178 -0
- package/src/core/filesystem/dos33.hpp +66 -0
- package/src/core/filesystem/pascal.cpp +262 -0
- package/src/core/filesystem/pascal.hpp +87 -0
- package/src/core/filesystem/prodos.cpp +369 -0
- package/src/core/filesystem/prodos.hpp +119 -0
- package/src/core/input/keyboard.cpp +227 -0
- package/src/core/input/keyboard.hpp +111 -0
- package/src/core/mmu/mmu.cpp +1387 -0
- package/src/core/mmu/mmu.hpp +236 -0
- package/src/core/types.hpp +196 -0
- package/src/core/video/video.cpp +680 -0
- package/src/core/video/video.hpp +156 -0
- package/src/css/assembler-editor.css +1617 -0
- package/src/css/base.css +470 -0
- package/src/css/basic-debugger.css +791 -0
- package/src/css/basic-editor.css +792 -0
- package/src/css/controls.css +783 -0
- package/src/css/cpu-debugger.css +1413 -0
- package/src/css/debug-base.css +160 -0
- package/src/css/debug-windows.css +6455 -0
- package/src/css/disk-drives.css +406 -0
- package/src/css/documentation.css +392 -0
- package/src/css/file-explorer.css +867 -0
- package/src/css/hard-drive.css +180 -0
- package/src/css/layout.css +217 -0
- package/src/css/memory-windows.css +798 -0
- package/src/css/modals.css +510 -0
- package/src/css/monitor.css +425 -0
- package/src/css/release-notes.css +101 -0
- package/src/css/responsive.css +400 -0
- package/src/css/rule-builder.css +340 -0
- package/src/css/save-states.css +201 -0
- package/src/css/settings-windows.css +1231 -0
- package/src/css/window-switcher.css +150 -0
- package/src/js/agent/agent-manager.js +643 -0
- package/src/js/agent/agent-tools.js +293 -0
- package/src/js/agent/agent-version-tools.js +131 -0
- package/src/js/agent/assembler-tools.js +357 -0
- package/src/js/agent/basic-program-tools.js +894 -0
- package/src/js/agent/disk-tools.js +417 -0
- package/src/js/agent/file-explorer-tools.js +269 -0
- package/src/js/agent/index.js +13 -0
- package/src/js/agent/main-tools.js +222 -0
- package/src/js/agent/slot-tools.js +303 -0
- package/src/js/agent/smartport-tools.js +257 -0
- package/src/js/agent/window-tools.js +80 -0
- package/src/js/audio/audio-driver.js +417 -0
- package/src/js/audio/audio-worklet.js +85 -0
- package/src/js/audio/index.js +8 -0
- package/src/js/config/default-layout.js +34 -0
- package/src/js/config/version.js +8 -0
- package/src/js/data/apple2-rom-routines.js +577 -0
- package/src/js/debug/assembler-editor-window.js +2993 -0
- package/src/js/debug/basic-breakpoint-manager.js +529 -0
- package/src/js/debug/basic-program-parser.js +436 -0
- package/src/js/debug/basic-program-window.js +2594 -0
- package/src/js/debug/basic-variable-inspector.js +447 -0
- package/src/js/debug/breakpoint-manager.js +472 -0
- package/src/js/debug/cpu-debugger-window.js +2396 -0
- package/src/js/debug/index.js +22 -0
- package/src/js/debug/label-manager.js +238 -0
- package/src/js/debug/memory-browser-window.js +416 -0
- package/src/js/debug/memory-heat-map-window.js +481 -0
- package/src/js/debug/memory-map-window.js +206 -0
- package/src/js/debug/mockingboard-window.js +882 -0
- package/src/js/debug/mouse-card-window.js +355 -0
- package/src/js/debug/rule-builder-window.js +648 -0
- package/src/js/debug/soft-switch-window.js +458 -0
- package/src/js/debug/stack-viewer-window.js +221 -0
- package/src/js/debug/symbols.js +416 -0
- package/src/js/debug/trace-panel.js +291 -0
- package/src/js/debug/zero-page-watch-window.js +297 -0
- package/src/js/disk-manager/disk-drives-window.js +212 -0
- package/src/js/disk-manager/disk-operations.js +284 -0
- package/src/js/disk-manager/disk-persistence.js +301 -0
- package/src/js/disk-manager/disk-surface-renderer.js +388 -0
- package/src/js/disk-manager/drive-sounds.js +139 -0
- package/src/js/disk-manager/hard-drive-manager.js +481 -0
- package/src/js/disk-manager/hard-drive-persistence.js +187 -0
- package/src/js/disk-manager/hard-drive-window.js +57 -0
- package/src/js/disk-manager/index.js +890 -0
- package/src/js/display/display-settings-window.js +383 -0
- package/src/js/display/index.js +10 -0
- package/src/js/display/screen-window.js +342 -0
- package/src/js/display/webgl-renderer.js +705 -0
- package/src/js/file-explorer/disassembler.js +574 -0
- package/src/js/file-explorer/dos33.js +266 -0
- package/src/js/file-explorer/file-viewer.js +359 -0
- package/src/js/file-explorer/index.js +1261 -0
- package/src/js/file-explorer/prodos.js +549 -0
- package/src/js/file-explorer/utils.js +67 -0
- package/src/js/help/documentation-window.js +1096 -0
- package/src/js/help/index.js +10 -0
- package/src/js/help/release-notes-window.js +85 -0
- package/src/js/help/release-notes.js +612 -0
- package/src/js/input/gamepad-handler.js +176 -0
- package/src/js/input/index.js +12 -0
- package/src/js/input/input-handler.js +396 -0
- package/src/js/input/joystick-window.js +404 -0
- package/src/js/input/mouse-handler.js +99 -0
- package/src/js/input/text-selection.js +462 -0
- package/src/js/main.js +653 -0
- package/src/js/state/index.js +15 -0
- package/src/js/state/save-states-window.js +393 -0
- package/src/js/state/state-manager.js +409 -0
- package/src/js/state/state-persistence.js +218 -0
- package/src/js/ui/confirm.js +43 -0
- package/src/js/ui/disk-drive-positioner.js +347 -0
- package/src/js/ui/reminder-controller.js +129 -0
- package/src/js/ui/slot-configuration-window.js +560 -0
- package/src/js/ui/theme-manager.js +61 -0
- package/src/js/ui/toast.js +44 -0
- package/src/js/ui/ui-controller.js +897 -0
- package/src/js/ui/window-switcher.js +275 -0
- package/src/js/utils/basic-autocomplete.js +832 -0
- package/src/js/utils/basic-highlighting.js +473 -0
- package/src/js/utils/basic-tokenizer.js +153 -0
- package/src/js/utils/basic-tokens.js +117 -0
- package/src/js/utils/constants.js +28 -0
- package/src/js/utils/indexeddb-helper.js +225 -0
- package/src/js/utils/merlin-editor-support.js +905 -0
- package/src/js/utils/merlin-highlighting.js +551 -0
- package/src/js/utils/storage.js +125 -0
- package/src/js/utils/string-utils.js +19 -0
- package/src/js/utils/wasm-memory.js +54 -0
- package/src/js/windows/base-window.js +690 -0
- package/src/js/windows/index.js +9 -0
- package/src/js/windows/window-manager.js +375 -0
- package/tests/catch2/catch.hpp +17976 -0
- package/tests/common/basic_program_builder.cpp +119 -0
- package/tests/common/basic_program_builder.hpp +209 -0
- package/tests/common/disk_image_builder.cpp +444 -0
- package/tests/common/disk_image_builder.hpp +141 -0
- package/tests/common/test_helpers.hpp +118 -0
- package/tests/gcr/gcr-test.cpp +142 -0
- package/tests/integration/check-rom.js +70 -0
- package/tests/integration/compare-boot.js +239 -0
- package/tests/integration/crash-trace.js +102 -0
- package/tests/integration/disk-boot-test.js +264 -0
- package/tests/integration/memory-crash.js +108 -0
- package/tests/integration/nibble-read-test.js +249 -0
- package/tests/integration/phase-test.js +159 -0
- package/tests/integration/test_emulator.cpp +291 -0
- package/tests/integration/test_emulator_basic.cpp +91 -0
- package/tests/integration/test_emulator_debug.cpp +344 -0
- package/tests/integration/test_emulator_disk.cpp +153 -0
- package/tests/integration/test_emulator_state.cpp +163 -0
- package/tests/klaus/6502_functional_test.bin +0 -0
- package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
- package/tests/klaus/klaus_6502_test.cpp +184 -0
- package/tests/klaus/klaus_65c02_test.cpp +197 -0
- package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
- package/tests/thunderclock/thunderclock_test.cpp +550 -0
- package/tests/unit/test_assembler.cpp +521 -0
- package/tests/unit/test_audio.cpp +196 -0
- package/tests/unit/test_ay8910.cpp +311 -0
- package/tests/unit/test_basic_detokenizer.cpp +265 -0
- package/tests/unit/test_basic_tokenizer.cpp +382 -0
- package/tests/unit/test_block_device.cpp +259 -0
- package/tests/unit/test_condition_evaluator.cpp +219 -0
- package/tests/unit/test_cpu6502.cpp +1301 -0
- package/tests/unit/test_cpu_addressing.cpp +361 -0
- package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
- package/tests/unit/test_cpu_decimal.cpp +166 -0
- package/tests/unit/test_cpu_interrupts.cpp +285 -0
- package/tests/unit/test_disassembler.cpp +323 -0
- package/tests/unit/test_disk2_card.cpp +330 -0
- package/tests/unit/test_dos33.cpp +273 -0
- package/tests/unit/test_dsk_disk_image.cpp +315 -0
- package/tests/unit/test_expansion_card.cpp +178 -0
- package/tests/unit/test_gcr_encoding.cpp +232 -0
- package/tests/unit/test_keyboard.cpp +262 -0
- package/tests/unit/test_mmu.cpp +555 -0
- package/tests/unit/test_mmu_slots.cpp +323 -0
- package/tests/unit/test_mockingboard.cpp +352 -0
- package/tests/unit/test_mouse_card.cpp +386 -0
- package/tests/unit/test_pascal.cpp +248 -0
- package/tests/unit/test_prodos.cpp +259 -0
- package/tests/unit/test_smartport_card.cpp +321 -0
- package/tests/unit/test_thunderclock.cpp +354 -0
- package/tests/unit/test_via6522.cpp +323 -0
- package/tests/unit/test_video.cpp +319 -0
- package/tests/unit/test_woz_disk_image.cpp +257 -0
- package/vite.config.js +96 -0
- package/wiki/AI-Agent.md +372 -0
- package/wiki/Architecture-Overview.md +303 -0
- package/wiki/Audio-System.md +449 -0
- package/wiki/CPU-Emulation.md +477 -0
- package/wiki/Debugger.md +516 -0
- package/wiki/Disk-Drives.md +161 -0
- package/wiki/Disk-System-Internals.md +547 -0
- package/wiki/Display-Settings.md +88 -0
- package/wiki/Expansion-Slots.md +187 -0
- package/wiki/File-Explorer.md +259 -0
- package/wiki/Getting-Started.md +156 -0
- package/wiki/Home.md +69 -0
- package/wiki/Input-Devices.md +183 -0
- package/wiki/Keyboard-Shortcuts.md +158 -0
- package/wiki/Memory-System.md +364 -0
- package/wiki/Save-States.md +172 -0
- 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
|
+
}
|