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,444 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* disk_image_builder.cpp - Programmatic disk image builders
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
#include "disk_image_builder.hpp"
|
|
6
|
+
#include <algorithm>
|
|
7
|
+
#include <cstring>
|
|
8
|
+
|
|
9
|
+
namespace test {
|
|
10
|
+
|
|
11
|
+
// ==================== DOS33DiskBuilder ====================
|
|
12
|
+
|
|
13
|
+
DOS33DiskBuilder::DOS33DiskBuilder() : data_(DISK_SIZE, 0) {
|
|
14
|
+
initVTOC();
|
|
15
|
+
initCatalog();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
int DOS33DiskBuilder::getSectorOffset(int track, int sector) const {
|
|
19
|
+
return (track * SECTORS + sector) * SECTOR_SIZE;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
void DOS33DiskBuilder::initVTOC() {
|
|
23
|
+
// VTOC is at track 17, sector 0
|
|
24
|
+
int offset = getSectorOffset(17, 0);
|
|
25
|
+
|
|
26
|
+
data_[offset + 0x01] = 17; // Catalog track
|
|
27
|
+
data_[offset + 0x02] = 15; // Catalog sector
|
|
28
|
+
data_[offset + 0x03] = 3; // DOS version (3.3)
|
|
29
|
+
data_[offset + 0x06] = 254; // Volume number
|
|
30
|
+
data_[offset + 0x27] = 122; // Max track/sector pairs per TS list
|
|
31
|
+
data_[offset + 0x30] = 17; // Last track allocated
|
|
32
|
+
data_[offset + 0x31] = 1; // Direction of allocation (+1)
|
|
33
|
+
data_[offset + 0x34] = 35; // Number of tracks
|
|
34
|
+
data_[offset + 0x35] = 16; // Sectors per track
|
|
35
|
+
data_[offset + 0x36] = 0; // Bytes per sector low
|
|
36
|
+
data_[offset + 0x37] = 1; // Bytes per sector high (256)
|
|
37
|
+
|
|
38
|
+
// Free sector bitmap - mark all sectors as free
|
|
39
|
+
// Each track gets 4 bytes starting at offset 0x38
|
|
40
|
+
for (int t = 0; t < 35; t++) {
|
|
41
|
+
int bitmapOffset = offset + 0x38 + t * 4;
|
|
42
|
+
data_[bitmapOffset + 0] = 0xFF; // Sectors 0-7 free
|
|
43
|
+
data_[bitmapOffset + 1] = 0xFF; // Sectors 8-15 free
|
|
44
|
+
data_[bitmapOffset + 2] = 0x00;
|
|
45
|
+
data_[bitmapOffset + 3] = 0x00;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Mark VTOC track (17) sectors as used
|
|
49
|
+
int vtocBitmap = offset + 0x38 + 17 * 4;
|
|
50
|
+
data_[vtocBitmap + 0] = 0x00;
|
|
51
|
+
data_[vtocBitmap + 1] = 0x00;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
void DOS33DiskBuilder::initCatalog() {
|
|
55
|
+
// First catalog sector: track 17, sector 15
|
|
56
|
+
// Just leave it zeroed - that marks it as having no entries
|
|
57
|
+
// The next catalog link is already 0,0 which means end of catalog
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
bool DOS33DiskBuilder::allocateSector(int& track, int& sector) {
|
|
61
|
+
if (nextAllocTrack_ >= TRACKS) return false;
|
|
62
|
+
track = nextAllocTrack_;
|
|
63
|
+
sector = nextAllocSector_;
|
|
64
|
+
nextAllocSector_++;
|
|
65
|
+
if (nextAllocSector_ >= SECTORS) {
|
|
66
|
+
nextAllocSector_ = 0;
|
|
67
|
+
nextAllocTrack_++;
|
|
68
|
+
if (nextAllocTrack_ == 17) nextAllocTrack_ = 18; // Skip catalog track
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
void DOS33DiskBuilder::writeSector(int track, int sector, const uint8_t* sdata, int len) {
|
|
74
|
+
int offset = getSectorOffset(track, sector);
|
|
75
|
+
int copyLen = std::min(len, SECTOR_SIZE);
|
|
76
|
+
std::memcpy(&data_[offset], sdata, copyLen);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
bool DOS33DiskBuilder::addFile(const std::string& name, uint8_t fileType,
|
|
80
|
+
const uint8_t* fileData, int dataLen, bool locked) {
|
|
81
|
+
// Allocate a track/sector list sector
|
|
82
|
+
int tsTrack, tsSector;
|
|
83
|
+
if (!allocateSector(tsTrack, tsSector)) return false;
|
|
84
|
+
|
|
85
|
+
int tsOffset = getSectorOffset(tsTrack, tsSector);
|
|
86
|
+
// TS list: bytes 0x01-0x02 = next TS list (0,0 = none)
|
|
87
|
+
// Pairs start at byte 0x0C
|
|
88
|
+
|
|
89
|
+
// Write data sectors
|
|
90
|
+
int remaining = dataLen;
|
|
91
|
+
const uint8_t* ptr = fileData;
|
|
92
|
+
int pairIdx = 0;
|
|
93
|
+
int sectorCount = 0;
|
|
94
|
+
|
|
95
|
+
while (remaining > 0 && pairIdx < 122) {
|
|
96
|
+
int dataTrack, dataSector;
|
|
97
|
+
if (!allocateSector(dataTrack, dataSector)) return false;
|
|
98
|
+
|
|
99
|
+
int writeLen = std::min(remaining, SECTOR_SIZE);
|
|
100
|
+
writeSector(dataTrack, dataSector, ptr, writeLen);
|
|
101
|
+
|
|
102
|
+
// Record in TS list
|
|
103
|
+
data_[tsOffset + 0x0C + pairIdx * 2] = dataTrack;
|
|
104
|
+
data_[tsOffset + 0x0C + pairIdx * 2 + 1] = dataSector;
|
|
105
|
+
|
|
106
|
+
ptr += writeLen;
|
|
107
|
+
remaining -= writeLen;
|
|
108
|
+
pairIdx++;
|
|
109
|
+
sectorCount++;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Add catalog entry
|
|
113
|
+
int catOffset = getSectorOffset(catalogTrack_, catalogSector_);
|
|
114
|
+
int entryOffset = catOffset + 0x0B + catalogEntryIndex_ * 0x23;
|
|
115
|
+
|
|
116
|
+
// First TS list track/sector
|
|
117
|
+
data_[entryOffset + 0x00] = tsTrack;
|
|
118
|
+
data_[entryOffset + 0x01] = tsSector;
|
|
119
|
+
|
|
120
|
+
// File type with locked flag
|
|
121
|
+
data_[entryOffset + 0x02] = (locked ? 0x80 : 0x00) | fileType;
|
|
122
|
+
|
|
123
|
+
// Filename (30 bytes, space-padded, high bit set)
|
|
124
|
+
for (int i = 0; i < 30; i++) {
|
|
125
|
+
if (i < static_cast<int>(name.size())) {
|
|
126
|
+
data_[entryOffset + 0x03 + i] = name[i] | 0x80;
|
|
127
|
+
} else {
|
|
128
|
+
data_[entryOffset + 0x03 + i] = ' ' | 0x80;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Sector count
|
|
133
|
+
data_[entryOffset + 0x21] = sectorCount & 0xFF;
|
|
134
|
+
data_[entryOffset + 0x22] = (sectorCount >> 8) & 0xFF;
|
|
135
|
+
|
|
136
|
+
catalogEntryIndex_++;
|
|
137
|
+
if (catalogEntryIndex_ >= 7) {
|
|
138
|
+
// Would need another catalog sector - for testing, 7 entries is enough
|
|
139
|
+
catalogEntryIndex_ = 0;
|
|
140
|
+
catalogSector_--;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ==================== ProDOSDiskBuilder ====================
|
|
147
|
+
|
|
148
|
+
ProDOSDiskBuilder::ProDOSDiskBuilder(const std::string& volumeName)
|
|
149
|
+
: data_(DISK_SIZE, 0), volumeName_(volumeName) {
|
|
150
|
+
initVolumeDirectory();
|
|
151
|
+
initBitmap();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
void ProDOSDiskBuilder::readBlock(int blockNum, uint8_t* out) const {
|
|
155
|
+
// ProDOS block to physical sector mapping for 140K disks
|
|
156
|
+
// Block N maps to track N/8, sectors interleaved
|
|
157
|
+
static const uint8_t INTERLEAVE[16] = {
|
|
158
|
+
0, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 15
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
int track = blockNum / 8;
|
|
162
|
+
int blockInTrack = blockNum % 8;
|
|
163
|
+
int sector1 = INTERLEAVE[blockInTrack * 2];
|
|
164
|
+
int sector2 = INTERLEAVE[blockInTrack * 2 + 1];
|
|
165
|
+
|
|
166
|
+
int off1 = (track * 16 + sector1) * 256;
|
|
167
|
+
int off2 = (track * 16 + sector2) * 256;
|
|
168
|
+
|
|
169
|
+
std::memcpy(out, &data_[off1], 256);
|
|
170
|
+
std::memcpy(out + 256, &data_[off2], 256);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
void ProDOSDiskBuilder::writeBlock(int blockNum, const uint8_t* blockData, int len) {
|
|
174
|
+
// For simplicity, store blocks in ProDOS order (linear)
|
|
175
|
+
// Since we're building images for testing the ProDOS parser,
|
|
176
|
+
// we write in ProDOS-order format (blockNum * 512)
|
|
177
|
+
int offset = blockNum * BLOCK_SIZE;
|
|
178
|
+
if (offset + len > DISK_SIZE) return;
|
|
179
|
+
int copyLen = std::min(len, BLOCK_SIZE);
|
|
180
|
+
std::memcpy(&data_[offset], blockData, copyLen);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
void ProDOSDiskBuilder::initVolumeDirectory() {
|
|
184
|
+
// Block 2 = volume directory header
|
|
185
|
+
uint8_t block[BLOCK_SIZE] = {};
|
|
186
|
+
|
|
187
|
+
// Prev/next directory block pointers
|
|
188
|
+
block[0] = 0; // Previous block (none for volume dir)
|
|
189
|
+
block[1] = 0;
|
|
190
|
+
block[2] = 3; // Next block
|
|
191
|
+
block[3] = 0;
|
|
192
|
+
|
|
193
|
+
// Volume directory header entry at offset 4
|
|
194
|
+
int nameLen = std::min(static_cast<int>(volumeName_.size()), 15);
|
|
195
|
+
block[4] = 0xF0 | nameLen; // Storage type F = volume header, name length
|
|
196
|
+
|
|
197
|
+
for (int i = 0; i < nameLen; i++) {
|
|
198
|
+
block[5 + i] = volumeName_[i] & 0x7F;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Creation date/time (leave zeroed)
|
|
202
|
+
// Access byte
|
|
203
|
+
block[0x22] = 0xC3; // Full access
|
|
204
|
+
|
|
205
|
+
// Entry length
|
|
206
|
+
block[0x23] = 0x27; // 39 bytes per entry
|
|
207
|
+
|
|
208
|
+
// Entries per block
|
|
209
|
+
block[0x24] = 0x0D; // 13 entries per block
|
|
210
|
+
|
|
211
|
+
// File count
|
|
212
|
+
block[0x25] = 0;
|
|
213
|
+
block[0x26] = 0;
|
|
214
|
+
|
|
215
|
+
// Bitmap pointer
|
|
216
|
+
block[0x27] = 6; // Block 6
|
|
217
|
+
block[0x28] = 0;
|
|
218
|
+
|
|
219
|
+
// Total blocks
|
|
220
|
+
block[0x29] = 0x18; // 280 blocks (0x118)
|
|
221
|
+
block[0x2A] = 0x01;
|
|
222
|
+
|
|
223
|
+
writeBlock(2, block, BLOCK_SIZE);
|
|
224
|
+
|
|
225
|
+
// Block 3 - continuation of volume directory (empty)
|
|
226
|
+
uint8_t block3[BLOCK_SIZE] = {};
|
|
227
|
+
block3[0] = 2; block3[1] = 0; // Previous = block 2
|
|
228
|
+
block3[2] = 4; block3[3] = 0; // Next = block 4
|
|
229
|
+
writeBlock(3, block3, BLOCK_SIZE);
|
|
230
|
+
|
|
231
|
+
// Block 4 - continuation (empty)
|
|
232
|
+
uint8_t block4[BLOCK_SIZE] = {};
|
|
233
|
+
block4[0] = 3; block4[1] = 0; // Previous = block 3
|
|
234
|
+
block4[2] = 5; block4[3] = 0; // Next = block 5
|
|
235
|
+
writeBlock(4, block4, BLOCK_SIZE);
|
|
236
|
+
|
|
237
|
+
// Block 5 - last dir block
|
|
238
|
+
uint8_t block5[BLOCK_SIZE] = {};
|
|
239
|
+
block5[0] = 4; block5[1] = 0; // Previous = block 4
|
|
240
|
+
block5[2] = 0; block5[3] = 0; // Next = none
|
|
241
|
+
writeBlock(5, block5, BLOCK_SIZE);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
void ProDOSDiskBuilder::initBitmap() {
|
|
245
|
+
// Volume bitmap at block 6
|
|
246
|
+
// 280 blocks = 35 bytes, each bit = 1 block (1=free, 0=used)
|
|
247
|
+
uint8_t bitmap[BLOCK_SIZE] = {};
|
|
248
|
+
|
|
249
|
+
// Mark all blocks as free initially
|
|
250
|
+
for (int i = 0; i < 35; i++) {
|
|
251
|
+
bitmap[i] = 0xFF;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Mark blocks 0-9 as used (boot, volume dir, bitmap)
|
|
255
|
+
bitmap[0] = 0x00; // Blocks 0-7 used
|
|
256
|
+
bitmap[1] = 0x3F; // Blocks 8-9 used, 10-15 free
|
|
257
|
+
|
|
258
|
+
writeBlock(6, bitmap, BLOCK_SIZE);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
bool ProDOSDiskBuilder::allocateBlock(int& blockNum) {
|
|
262
|
+
if (nextAllocBlock_ >= 280) return false;
|
|
263
|
+
blockNum = nextAllocBlock_++;
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
bool ProDOSDiskBuilder::addFile(const std::string& name, uint8_t fileType,
|
|
268
|
+
uint16_t auxType, const uint8_t* fileData, int dataLen) {
|
|
269
|
+
// Allocate block for file data (seedling - single block for small files)
|
|
270
|
+
int dataBlock;
|
|
271
|
+
if (!allocateBlock(dataBlock)) return false;
|
|
272
|
+
|
|
273
|
+
// Write file data
|
|
274
|
+
uint8_t blockBuf[BLOCK_SIZE] = {};
|
|
275
|
+
int writeLen = std::min(dataLen, BLOCK_SIZE);
|
|
276
|
+
if (fileData && writeLen > 0) {
|
|
277
|
+
std::memcpy(blockBuf, fileData, writeLen);
|
|
278
|
+
}
|
|
279
|
+
writeBlock(dataBlock, blockBuf, BLOCK_SIZE);
|
|
280
|
+
|
|
281
|
+
// Add directory entry
|
|
282
|
+
// Directory entries start at offset 4 + entry * 0x27 within a block
|
|
283
|
+
// First entry in block 2 is the volume header, so file entries start at index 1
|
|
284
|
+
int entryBlock = 2 + (dirEntryCount_ + 1) / 13;
|
|
285
|
+
int entryInBlock = (dirEntryCount_ + 1) % 13;
|
|
286
|
+
if (entryInBlock == 0 && entryBlock > 2) {
|
|
287
|
+
entryInBlock = 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Read the directory block
|
|
291
|
+
int blockOffset = entryBlock * BLOCK_SIZE;
|
|
292
|
+
int entryOffset = blockOffset + 4 + entryInBlock * 0x27;
|
|
293
|
+
|
|
294
|
+
int nameLen = std::min(static_cast<int>(name.size()), 15);
|
|
295
|
+
data_[entryOffset] = 0x10 | nameLen; // Storage type 1 = seedling
|
|
296
|
+
|
|
297
|
+
for (int i = 0; i < nameLen; i++) {
|
|
298
|
+
data_[entryOffset + 1 + i] = name[i] & 0x7F;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// File type
|
|
302
|
+
data_[entryOffset + 0x10] = fileType;
|
|
303
|
+
|
|
304
|
+
// Key pointer (data block)
|
|
305
|
+
data_[entryOffset + 0x11] = dataBlock & 0xFF;
|
|
306
|
+
data_[entryOffset + 0x12] = (dataBlock >> 8) & 0xFF;
|
|
307
|
+
|
|
308
|
+
// Blocks used
|
|
309
|
+
data_[entryOffset + 0x13] = 1;
|
|
310
|
+
data_[entryOffset + 0x14] = 0;
|
|
311
|
+
|
|
312
|
+
// EOF
|
|
313
|
+
data_[entryOffset + 0x15] = dataLen & 0xFF;
|
|
314
|
+
data_[entryOffset + 0x16] = (dataLen >> 8) & 0xFF;
|
|
315
|
+
data_[entryOffset + 0x17] = (dataLen >> 16) & 0xFF;
|
|
316
|
+
|
|
317
|
+
// Aux type
|
|
318
|
+
data_[entryOffset + 0x1F] = auxType & 0xFF;
|
|
319
|
+
data_[entryOffset + 0x20] = (auxType >> 8) & 0xFF;
|
|
320
|
+
|
|
321
|
+
// Access
|
|
322
|
+
data_[entryOffset + 0x1E] = 0xC3;
|
|
323
|
+
|
|
324
|
+
// Update file count in volume header
|
|
325
|
+
int volOffset = 2 * BLOCK_SIZE + 4;
|
|
326
|
+
dirEntryCount_++;
|
|
327
|
+
data_[volOffset + 0x21] = dirEntryCount_ & 0xFF;
|
|
328
|
+
data_[volOffset + 0x22] = (dirEntryCount_ >> 8) & 0xFF;
|
|
329
|
+
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ==================== PascalDiskBuilder ====================
|
|
334
|
+
|
|
335
|
+
PascalDiskBuilder::PascalDiskBuilder(const std::string& volumeName)
|
|
336
|
+
: data_(DISK_SIZE, 0), volumeName_(volumeName) {
|
|
337
|
+
initVolumeHeader();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
void PascalDiskBuilder::initVolumeHeader() {
|
|
341
|
+
// Pascal directory is blocks 2-5 (2048 bytes)
|
|
342
|
+
// Volume header is the first 26-byte entry in block 2
|
|
343
|
+
// For testing, we write in ProDOS block order
|
|
344
|
+
|
|
345
|
+
int offset = 2 * BLOCK_SIZE; // Block 2
|
|
346
|
+
|
|
347
|
+
// Entry 0: Volume header
|
|
348
|
+
data_[offset + 0] = 0; // firstBlock = 0
|
|
349
|
+
data_[offset + 1] = 0;
|
|
350
|
+
data_[offset + 2] = 6; // nextBlock (first free block)
|
|
351
|
+
data_[offset + 3] = 0;
|
|
352
|
+
data_[offset + 4] = 0; // fileType = 0 (volume)
|
|
353
|
+
data_[offset + 5] = 0;
|
|
354
|
+
|
|
355
|
+
// Volume name (Pascal format: length byte + chars)
|
|
356
|
+
int nameLen = std::min(static_cast<int>(volumeName_.size()), 7);
|
|
357
|
+
data_[offset + 6] = nameLen;
|
|
358
|
+
for (int i = 0; i < nameLen; i++) {
|
|
359
|
+
data_[offset + 7 + i] = volumeName_[i];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Total blocks
|
|
363
|
+
data_[offset + 14] = 0x18; // 280 blocks (low)
|
|
364
|
+
data_[offset + 15] = 0x01; // (high)
|
|
365
|
+
|
|
366
|
+
// File count (will be updated as files are added)
|
|
367
|
+
data_[offset + 16] = 0;
|
|
368
|
+
data_[offset + 17] = 0;
|
|
369
|
+
|
|
370
|
+
// Last access date (leave 0)
|
|
371
|
+
// Most recent date set
|
|
372
|
+
data_[offset + 20] = 0;
|
|
373
|
+
data_[offset + 21] = 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
bool PascalDiskBuilder::addFile(const std::string& name, uint8_t fileType,
|
|
377
|
+
const uint8_t* fileData, int dataLen) {
|
|
378
|
+
int blocksNeeded = (dataLen + BLOCK_SIZE - 1) / BLOCK_SIZE;
|
|
379
|
+
if (blocksNeeded == 0) blocksNeeded = 1;
|
|
380
|
+
if (nextBlock_ + blocksNeeded > 280) return false;
|
|
381
|
+
|
|
382
|
+
int startBlock = nextBlock_;
|
|
383
|
+
|
|
384
|
+
// Write file data
|
|
385
|
+
for (int i = 0; i < blocksNeeded; i++) {
|
|
386
|
+
int blockOffset = (nextBlock_ + i) * BLOCK_SIZE;
|
|
387
|
+
int remaining = dataLen - i * BLOCK_SIZE;
|
|
388
|
+
int writeLen = std::min(remaining, BLOCK_SIZE);
|
|
389
|
+
if (writeLen > 0 && fileData) {
|
|
390
|
+
std::memcpy(&data_[blockOffset], fileData + i * BLOCK_SIZE, writeLen);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
nextBlock_ += blocksNeeded;
|
|
395
|
+
|
|
396
|
+
// Add directory entry (26 bytes each, starting at entry 1 in block 2)
|
|
397
|
+
dirEntryCount_++;
|
|
398
|
+
int entryOffset = 2 * BLOCK_SIZE + dirEntryCount_ * 26;
|
|
399
|
+
|
|
400
|
+
data_[entryOffset + 0] = startBlock & 0xFF;
|
|
401
|
+
data_[entryOffset + 1] = (startBlock >> 8) & 0xFF;
|
|
402
|
+
data_[entryOffset + 2] = nextBlock_ & 0xFF;
|
|
403
|
+
data_[entryOffset + 3] = (nextBlock_ >> 8) & 0xFF;
|
|
404
|
+
data_[entryOffset + 4] = fileType;
|
|
405
|
+
data_[entryOffset + 5] = 0;
|
|
406
|
+
|
|
407
|
+
int nameLen = std::min(static_cast<int>(name.size()), 15);
|
|
408
|
+
data_[entryOffset + 6] = nameLen;
|
|
409
|
+
for (int i = 0; i < nameLen; i++) {
|
|
410
|
+
data_[entryOffset + 7 + i] = name[i];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Bytes in last block
|
|
414
|
+
int bytesInLast = dataLen % BLOCK_SIZE;
|
|
415
|
+
if (bytesInLast == 0 && dataLen > 0) bytesInLast = BLOCK_SIZE;
|
|
416
|
+
data_[entryOffset + 22] = bytesInLast & 0xFF;
|
|
417
|
+
data_[entryOffset + 23] = (bytesInLast >> 8) & 0xFF;
|
|
418
|
+
|
|
419
|
+
// Update volume header file count
|
|
420
|
+
int volOffset = 2 * BLOCK_SIZE;
|
|
421
|
+
data_[volOffset + 16] = dirEntryCount_ & 0xFF;
|
|
422
|
+
data_[volOffset + 17] = (dirEntryCount_ >> 8) & 0xFF;
|
|
423
|
+
|
|
424
|
+
// Note: Do NOT update the nextBlock field in the volume header.
|
|
425
|
+
// That field must remain 6 (first block after the 4-block directory area)
|
|
426
|
+
// as required by the Pascal filesystem validator.
|
|
427
|
+
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ==================== BlockImageBuilder ====================
|
|
432
|
+
|
|
433
|
+
BlockImageBuilder::BlockImageBuilder(int totalBlocks)
|
|
434
|
+
: data_(totalBlocks * BLOCK_SIZE, 0) {
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
void BlockImageBuilder::writeBlock(int blockNum, const uint8_t* blockData, int len) {
|
|
438
|
+
int offset = blockNum * BLOCK_SIZE;
|
|
439
|
+
if (offset + len > static_cast<int>(data_.size())) return;
|
|
440
|
+
int copyLen = std::min(len, BLOCK_SIZE);
|
|
441
|
+
std::memcpy(&data_[offset], blockData, copyLen);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
} // namespace test
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* disk_image_builder.hpp - Programmatic disk image builders for testing
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
#pragma once
|
|
6
|
+
|
|
7
|
+
#include <array>
|
|
8
|
+
#include <cstdint>
|
|
9
|
+
#include <cstring>
|
|
10
|
+
#include <string>
|
|
11
|
+
#include <vector>
|
|
12
|
+
|
|
13
|
+
namespace test {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* DOS33DiskBuilder - Build minimal DOS 3.3 disk images for testing
|
|
17
|
+
*
|
|
18
|
+
* Creates 143360-byte images with a valid VTOC and catalog.
|
|
19
|
+
*/
|
|
20
|
+
class DOS33DiskBuilder {
|
|
21
|
+
public:
|
|
22
|
+
static constexpr int TRACKS = 35;
|
|
23
|
+
static constexpr int SECTORS = 16;
|
|
24
|
+
static constexpr int SECTOR_SIZE = 256;
|
|
25
|
+
static constexpr int DISK_SIZE = TRACKS * SECTORS * SECTOR_SIZE; // 143360
|
|
26
|
+
|
|
27
|
+
DOS33DiskBuilder();
|
|
28
|
+
|
|
29
|
+
// Write raw data to a specific sector
|
|
30
|
+
void writeSector(int track, int sector, const uint8_t* data, int len);
|
|
31
|
+
|
|
32
|
+
// Add a file to the catalog. fileType: 0x00=T, 0x01=I, 0x02=A, 0x04=B
|
|
33
|
+
// Data is written sequentially to available sectors.
|
|
34
|
+
// Returns true on success.
|
|
35
|
+
bool addFile(const std::string& name, uint8_t fileType,
|
|
36
|
+
const uint8_t* data, int dataLen, bool locked = false);
|
|
37
|
+
|
|
38
|
+
// Get the built disk image
|
|
39
|
+
const std::vector<uint8_t>& build() const { return data_; }
|
|
40
|
+
const uint8_t* data() const { return data_.data(); }
|
|
41
|
+
size_t size() const { return data_.size(); }
|
|
42
|
+
|
|
43
|
+
private:
|
|
44
|
+
std::vector<uint8_t> data_;
|
|
45
|
+
|
|
46
|
+
// Track/sector allocation
|
|
47
|
+
int nextAllocTrack_ = 20; // Start allocating from track 20
|
|
48
|
+
int nextAllocSector_ = 0;
|
|
49
|
+
|
|
50
|
+
// Catalog state
|
|
51
|
+
int catalogTrack_ = 17;
|
|
52
|
+
int catalogSector_ = 15;
|
|
53
|
+
int catalogEntryIndex_ = 0; // 0-6 entries per sector
|
|
54
|
+
|
|
55
|
+
int getSectorOffset(int track, int sector) const;
|
|
56
|
+
bool allocateSector(int& track, int& sector);
|
|
57
|
+
void initVTOC();
|
|
58
|
+
void initCatalog();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* ProDOSDiskBuilder - Build minimal ProDOS disk images for testing
|
|
63
|
+
*/
|
|
64
|
+
class ProDOSDiskBuilder {
|
|
65
|
+
public:
|
|
66
|
+
static constexpr int BLOCK_SIZE = 512;
|
|
67
|
+
static constexpr int DISK_SIZE = 143360; // 280 blocks
|
|
68
|
+
|
|
69
|
+
ProDOSDiskBuilder(const std::string& volumeName = "TEST");
|
|
70
|
+
|
|
71
|
+
// Write raw data to a block
|
|
72
|
+
void writeBlock(int blockNum, const uint8_t* data, int len);
|
|
73
|
+
|
|
74
|
+
// Add a file to the volume directory
|
|
75
|
+
// Returns true on success.
|
|
76
|
+
bool addFile(const std::string& name, uint8_t fileType, uint16_t auxType,
|
|
77
|
+
const uint8_t* data, int dataLen);
|
|
78
|
+
|
|
79
|
+
const std::vector<uint8_t>& build() const { return data_; }
|
|
80
|
+
const uint8_t* data() const { return data_.data(); }
|
|
81
|
+
size_t size() const { return data_.size(); }
|
|
82
|
+
|
|
83
|
+
private:
|
|
84
|
+
std::vector<uint8_t> data_;
|
|
85
|
+
std::string volumeName_;
|
|
86
|
+
|
|
87
|
+
int nextAllocBlock_ = 10;
|
|
88
|
+
int dirEntryCount_ = 0;
|
|
89
|
+
|
|
90
|
+
void readBlock(int blockNum, uint8_t* out) const;
|
|
91
|
+
void initVolumeDirectory();
|
|
92
|
+
void initBitmap();
|
|
93
|
+
bool allocateBlock(int& blockNum);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* PascalDiskBuilder - Build minimal Apple Pascal disk images for testing
|
|
98
|
+
*/
|
|
99
|
+
class PascalDiskBuilder {
|
|
100
|
+
public:
|
|
101
|
+
static constexpr int BLOCK_SIZE = 512;
|
|
102
|
+
static constexpr int DISK_SIZE = 143360;
|
|
103
|
+
|
|
104
|
+
PascalDiskBuilder(const std::string& volumeName = "TEST");
|
|
105
|
+
|
|
106
|
+
bool addFile(const std::string& name, uint8_t fileType,
|
|
107
|
+
const uint8_t* data, int dataLen);
|
|
108
|
+
|
|
109
|
+
const std::vector<uint8_t>& build() const { return data_; }
|
|
110
|
+
const uint8_t* data() const { return data_.data(); }
|
|
111
|
+
size_t size() const { return data_.size(); }
|
|
112
|
+
|
|
113
|
+
private:
|
|
114
|
+
std::vector<uint8_t> data_;
|
|
115
|
+
std::string volumeName_;
|
|
116
|
+
int nextBlock_ = 6; // First usable block after directory
|
|
117
|
+
int dirEntryCount_ = 0;
|
|
118
|
+
|
|
119
|
+
void initVolumeHeader();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* BlockImageBuilder - Build variable-size HDV block device images
|
|
124
|
+
*/
|
|
125
|
+
class BlockImageBuilder {
|
|
126
|
+
public:
|
|
127
|
+
static constexpr int BLOCK_SIZE = 512;
|
|
128
|
+
|
|
129
|
+
explicit BlockImageBuilder(int totalBlocks = 65535);
|
|
130
|
+
|
|
131
|
+
void writeBlock(int blockNum, const uint8_t* data, int len);
|
|
132
|
+
|
|
133
|
+
const std::vector<uint8_t>& build() const { return data_; }
|
|
134
|
+
const uint8_t* data() const { return data_.data(); }
|
|
135
|
+
size_t size() const { return data_.size(); }
|
|
136
|
+
|
|
137
|
+
private:
|
|
138
|
+
std::vector<uint8_t> data_;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
} // namespace test
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* test_helpers.hpp - Shared test utilities for native C++ tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
#pragma once
|
|
6
|
+
|
|
7
|
+
#include <array>
|
|
8
|
+
#include <cstdint>
|
|
9
|
+
#include <cstring>
|
|
10
|
+
#include <vector>
|
|
11
|
+
#include <stdexcept>
|
|
12
|
+
#include "cpu6502.hpp"
|
|
13
|
+
|
|
14
|
+
namespace test {
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* FlatMemory - Simple 64KB memory for CPU testing
|
|
18
|
+
*
|
|
19
|
+
* Provides read/write callbacks compatible with CPU6502 constructor.
|
|
20
|
+
* Includes helpers for loading programs, setting vectors, etc.
|
|
21
|
+
*/
|
|
22
|
+
class FlatMemory {
|
|
23
|
+
public:
|
|
24
|
+
FlatMemory() { mem_.fill(0); }
|
|
25
|
+
|
|
26
|
+
uint8_t read(uint16_t addr) const { return mem_[addr]; }
|
|
27
|
+
void write(uint16_t addr, uint8_t val) { mem_[addr] = val; }
|
|
28
|
+
|
|
29
|
+
// Set RESET vector ($FFFC-$FFFD)
|
|
30
|
+
void setResetVector(uint16_t addr) {
|
|
31
|
+
mem_[0xFFFC] = addr & 0xFF;
|
|
32
|
+
mem_[0xFFFD] = (addr >> 8) & 0xFF;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Set IRQ/BRK vector ($FFFE-$FFFF)
|
|
36
|
+
void setIRQVector(uint16_t addr) {
|
|
37
|
+
mem_[0xFFFE] = addr & 0xFF;
|
|
38
|
+
mem_[0xFFFF] = (addr >> 8) & 0xFF;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Set NMI vector ($FFFA-$FFFB)
|
|
42
|
+
void setNMIVector(uint16_t addr) {
|
|
43
|
+
mem_[0xFFFA] = addr & 0xFF;
|
|
44
|
+
mem_[0xFFFB] = (addr >> 8) & 0xFF;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Load a byte sequence at a given address
|
|
48
|
+
void loadProgram(uint16_t addr, const std::vector<uint8_t>& bytes) {
|
|
49
|
+
for (size_t i = 0; i < bytes.size(); i++) {
|
|
50
|
+
mem_[static_cast<uint16_t>(addr + i)] = bytes[i];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Load raw byte array
|
|
55
|
+
void loadProgram(uint16_t addr, const uint8_t* data, size_t len) {
|
|
56
|
+
for (size_t i = 0; i < len; i++) {
|
|
57
|
+
mem_[static_cast<uint16_t>(addr + i)] = data[i];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Direct array access
|
|
62
|
+
uint8_t& operator[](uint16_t addr) { return mem_[addr]; }
|
|
63
|
+
const uint8_t& operator[](uint16_t addr) const { return mem_[addr]; }
|
|
64
|
+
|
|
65
|
+
// Clear all memory
|
|
66
|
+
void clear() { mem_.fill(0); }
|
|
67
|
+
|
|
68
|
+
private:
|
|
69
|
+
std::array<uint8_t, 65536> mem_;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Execute exactly N instructions on the CPU
|
|
74
|
+
*/
|
|
75
|
+
inline void runInstructions(a2e::CPU6502& cpu, int count) {
|
|
76
|
+
for (int i = 0; i < count; i++) {
|
|
77
|
+
cpu.executeInstruction();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Run until PC reaches target address, with a maximum instruction limit.
|
|
83
|
+
* Returns true if target was reached, false if limit was hit.
|
|
84
|
+
*/
|
|
85
|
+
inline bool runUntilPC(a2e::CPU6502& cpu, uint16_t target, int maxInstructions = 100000) {
|
|
86
|
+
for (int i = 0; i < maxInstructions; i++) {
|
|
87
|
+
if (cpu.getPC() == target) return true;
|
|
88
|
+
cpu.executeInstruction();
|
|
89
|
+
}
|
|
90
|
+
return cpu.getPC() == target;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Helper: create a CPU with FlatMemory and reset it to a given PC
|
|
95
|
+
*/
|
|
96
|
+
struct CPUTestFixture {
|
|
97
|
+
FlatMemory mem;
|
|
98
|
+
std::unique_ptr<a2e::CPU6502> cpu;
|
|
99
|
+
|
|
100
|
+
CPUTestFixture(uint16_t startPC = 0x0400,
|
|
101
|
+
a2e::CPUVariant variant = a2e::CPUVariant::CMOS_65C02) {
|
|
102
|
+
mem.setResetVector(startPC);
|
|
103
|
+
cpu = std::make_unique<a2e::CPU6502>(
|
|
104
|
+
[this](uint16_t addr) -> uint8_t { return mem.read(addr); },
|
|
105
|
+
[this](uint16_t addr, uint8_t val) { mem.write(addr, val); },
|
|
106
|
+
variant
|
|
107
|
+
);
|
|
108
|
+
cpu->reset();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
void loadAndReset(uint16_t addr, const std::vector<uint8_t>& program) {
|
|
112
|
+
mem.setResetVector(addr);
|
|
113
|
+
mem.loadProgram(addr, program);
|
|
114
|
+
cpu->reset();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
} // namespace test
|