diff --git a/.github/workflows/host-tests.yml b/.github/workflows/host-tests.yml new file mode 100644 index 00000000..5f9c3094 --- /dev/null +++ b/.github/workflows/host-tests.yml @@ -0,0 +1,43 @@ +name: Host regression tests + +on: + push: + paths: + - "addon/usbcdgadget/**" + - "addon/cueparser/**" + - "addon/discimage/**" + - "addon/libchdr-src/**" + - "integration-tests/**" + - ".github/workflows/host-tests.yml" + pull_request: + paths: + - "addon/usbcdgadget/**" + - "addon/cueparser/**" + - "addon/discimage/**" + - "addon/libchdr-src/**" + - "integration-tests/**" + - ".github/workflows/host-tests.yml" + workflow_dispatch: + +jobs: + host-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # The CHD build compiles the vendored libchdr, which is a git submodule + # (addon/libchdr-src). Fetch just that submodule so libchdr/chd.h is + # present; scoping it to the path avoids cloning the large circle-stdlib + # submodule, which these host tests do not need. + - name: Fetch libchdr submodule (for the CHD build) + run: git submodule update --init addon/libchdr-src + + # Command-layer + real-image tests (ISO and synthetic CUE/BIN). Fully + # self-contained: no external libraries needed. + - name: Build and run SCSI/BOT + real-image regression tests + run: make -C integration-tests + + # Same suite plus the real CHD image, compiling libchdr with its + # vendored zlib/zstd/lzma decoders (still no system dependencies). + - name: Build and run including CHD image tests + run: make -C integration-tests clean && make -C integration-tests WITH_CHD=1 diff --git a/addon/usbcdgadget/tcdstate_update.cpp b/addon/usbcdgadget/tcdstate_update.cpp index a774a405..c4c6e376 100644 --- a/addon/usbcdgadget/tcdstate_update.cpp +++ b/addon/usbcdgadget/tcdstate_update.cpp @@ -356,8 +356,10 @@ void CUSBCDGadget::Update() { #if AARCH == 64 asm volatile("dc cvac, %0" : : "r"(addr) : "memory"); -#else +#elif AARCH == 32 asm volatile("mcr p15, 0, %0, c7, c10, 1" : : "r"(addr) : "memory"); +#else + (void)addr; // host test build: no cache maintenance needed #endif } diff --git a/addon/usbcdgadget/usbcdgadget.h b/addon/usbcdgadget/usbcdgadget.h index 2de3697e..571270cb 100644 --- a/addon/usbcdgadget/usbcdgadget.h +++ b/addon/usbcdgadget/usbcdgadget.h @@ -150,6 +150,7 @@ class CUSBCDGadget : public CDWUSBGadget friend class SCSIToolbox; friend class SCSIMisc; friend class CDUtils; + friend class CGadgetTestBench; // host regression tests (test/host) boolean m_bPendingDiscSwap = false; unsigned m_nDiscSwapStartTick = 0; diff --git a/integration-tests/.gitignore b/integration-tests/.gitignore new file mode 100644 index 00000000..89f9ac04 --- /dev/null +++ b/integration-tests/.gitignore @@ -0,0 +1 @@ +out/ diff --git a/integration-tests/Makefile b/integration-tests/Makefile new file mode 100644 index 00000000..2ccc81d8 --- /dev/null +++ b/integration-tests/Makefile @@ -0,0 +1,155 @@ +# Host-compiled regression tests for the USBODE SCSI/BOT layer. +# +# make -C integration-tests build + run +# make -C integration-tests build build only +# make -C integration-tests WITH_CHD=1 build + run including CHD tests +# +# Compiles the real gadget sources (addon/usbcdgadget + cueparser) and the +# real CUE/BIN/ISO reader (addon/discimage/cuebinfile.cpp) for the build +# machine against the stub Circle headers in harness/stubs/, links them with +# the test harness, and runs the resulting binary. The real-image tests drive +# those readers with actual image files. No cross-compiler or circle-stdlib +# checkout is needed. +# +# CHD support (WITH_CHD=1) additionally compiles addon/discimage/chdfile.cpp +# and libchdr with its vendored zlib/zstd/lzma decoders, so .chd images load +# through the real decompressor. It is opt-in only because those decoders add +# a heavier first-build cost. + +CXX ?= c++ +CC ?= cc +CXXFLAGS ?= -O1 -g +CXXFLAGS += -std=c++17 -Wall -Wno-format -Wno-unused-variable -Wno-unused-but-set-variable +CFLAGS ?= -O1 -g -w + +ADDON := ../addon +BUILD := out +IMAGES := $(BUILD)/images + +INCLUDES := -Iharness/stubs -I$(ADDON) -Iharness +DEFINES := -DUSBODE_TESTDATA=\"$(IMAGES)\" -DUSBODE_SDCARD=\"../sdcard\" + +# --------------------------------------------------------------------------- +# Real firmware sources under test (compiled from the repo, unchanged) +# --------------------------------------------------------------------------- +GADGET_SRCS := \ + $(ADDON)/usbcdgadget/usbcdgadget.cpp \ + $(ADDON)/usbcdgadget/usbcdgadgetendpoint.cpp \ + $(ADDON)/usbcdgadget/tcdstate_update.cpp \ + $(ADDON)/usbcdgadget/scsi_inquiry.cpp \ + $(ADDON)/usbcdgadget/scsi_read.cpp \ + $(ADDON)/usbcdgadget/scsi_toc.cpp \ + $(ADDON)/usbcdgadget/scsi_misc.cpp \ + $(ADDON)/usbcdgadget/scsi_toolbox.cpp \ + $(ADDON)/usbcdgadget/cd_utils.cpp \ + $(ADDON)/cueparser/cueparser.cpp \ + $(ADDON)/cueparser/cueutil.cpp + +# Real CUE/BIN/ISO reader. Its only host-side dependency is the FatFs seam +# (harness/fatfs_host.cpp) plus the two FatFsOptimizer entry points provided +# in harness/discimage_host.cpp. No reader logic is reimplemented. +DISCIMAGE_SRCS := $(ADDON)/discimage/cuebinfile.cpp + +CHDR_OBJS := +ifeq ($(WITH_CHD),1) +DISCIMAGE_SRCS += $(ADDON)/discimage/chdfile.cpp +DEFINES += -DWITH_CHD=1 +LIBCHDR_DIR := $(ADDON)/libchdr-src +LIBCHDR_SRCS := \ + $(LIBCHDR_DIR)/src/libchdr_chd.c \ + $(LIBCHDR_DIR)/src/libchdr_cdrom.c \ + $(LIBCHDR_DIR)/src/libchdr_flac.c \ + $(LIBCHDR_DIR)/src/libchdr_huffman.c \ + $(LIBCHDR_DIR)/src/libchdr_bitstream.c +ZLIB_DIR := $(LIBCHDR_DIR)/deps/zlib-1.3.1 +ZLIB_SRCS := $(ZLIB_DIR)/adler32.c $(ZLIB_DIR)/crc32.c $(ZLIB_DIR)/inflate.c \ + $(ZLIB_DIR)/inftrees.c $(ZLIB_DIR)/inffast.c $(ZLIB_DIR)/zutil.c +ZSTD_DIR := $(LIBCHDR_DIR)/deps/zstd-1.5.6/lib +ZSTD_SRCS := $(wildcard $(ZSTD_DIR)/common/*.c) $(wildcard $(ZSTD_DIR)/decompress/*.c) +LZMA_DIR := $(LIBCHDR_DIR)/deps/lzma-24.05 +LZMA_SRCS := $(LZMA_DIR)/src/LzmaDec.c $(LZMA_DIR)/src/LzmaEnc.c $(LZMA_DIR)/src/LzFind.c \ + $(LZMA_DIR)/src/Sort.c $(LZMA_DIR)/src/Alloc.c $(LZMA_DIR)/src/CpuArch.c \ + $(LZMA_DIR)/src/Delta.c +C_SRCS := $(LIBCHDR_SRCS) $(ZLIB_SRCS) $(ZSTD_SRCS) $(LZMA_SRCS) +LIBCHDR_INC := -I$(LIBCHDR_DIR)/include -I$(ZLIB_DIR) -I$(ZSTD_DIR) -I$(ZSTD_DIR)/common \ + -I$(LZMA_DIR)/include +CHDR_OBJS := $(addprefix $(BUILD)/,$(notdir $(C_SRCS:.c=.o))) +endif + +HARNESS_SRCS := $(wildcard harness/*.cpp) +TEST_SRCS := $(wildcard test-suite/*.cpp) + +CXX_SRCS := $(GADGET_SRCS) $(DISCIMAGE_SRCS) $(HARNESS_SRCS) $(TEST_SRCS) +CXX_OBJS := $(addprefix $(BUILD)/,$(notdir $(CXX_SRCS:.cpp=.o))) +OBJS := $(CXX_OBJS) $(CHDR_OBJS) + +VPATH := $(sort $(dir $(CXX_SRCS) $(C_SRCS))) + +BINARY := $(BUILD)/usbode-host-tests + +# Prepared test images (real files the real readers open at run time). +# The .gz sources live in testdata/ and are decompressed here; the .cue and +# .chd are copied as-is so the readers find them next to their data. +TEST_IMAGES := $(IMAGES)/image.iso \ + $(IMAGES)/freedos-test.iso \ + $(IMAGES)/shareware.iso \ + $(IMAGES)/audiocd.bin $(IMAGES)/audiocd.cue \ + $(IMAGES)/mixed.bin $(IMAGES)/mixed.cue \ + $(IMAGES)/mixed.chd + +all: run + +build: $(BINARY) + +run: $(BINARY) $(TEST_IMAGES) + ./$(BINARY) + +$(BINARY): $(OBJS) + $(CXX) $(CXXFLAGS) -o $@ $(OBJS) + +# -MMD/-MP emit .d dependency files so edits to a header (a stub, a firmware +# header) trigger the right recompiles; without this an incremental build can +# silently keep a stale object when only a header changed. +$(BUILD)/%.o: %.cpp + @mkdir -p $(BUILD) + $(CXX) $(CXXFLAGS) $(INCLUDES) $(LIBCHDR_INC) $(DEFINES) -MMD -MP -c $< -o $@ + +# Z7_ST builds the LZMA SDK single-threaded, so LzFindMt (the multithreaded +# matcher) is not pulled in; libchdr only needs the decoder anyway. +# ZSTD_DISABLE_ASM keeps zstd on its portable C decode path: on x86-64 zstd +# otherwise enables an assembly Huffman fast-loop (huf_decompress_amd64.S) +# that we do not build, which fails at link time. The C path is equivalent. +$(BUILD)/%.o: %.c + @mkdir -p $(BUILD) + $(CC) $(CFLAGS) -DZ7_ST -DZSTD_DISABLE_ASM=1 $(LIBCHDR_INC) -MMD -MP -c $< -o $@ + +-include $(OBJS:.o=.d) + +# Real ISO image: decompress the tracked sdcard/image.iso.gz so the reader +# opens an actual ISO9660 disc. +$(IMAGES)/image.iso: ../sdcard/image.iso.gz + @mkdir -p $(IMAGES) + gzip -dc $< > $@ + +# Real disc-image fixtures under testdata/ (see testdata/README-testdata.md). +# .gz images are decompressed; .cue/.chd are copied as-is next to their data. +$(IMAGES)/%.iso: testdata/%.iso.gz + @mkdir -p $(IMAGES) + gzip -dc $< > $@ + +$(IMAGES)/%.bin: testdata/%.bin.gz + @mkdir -p $(IMAGES) + gzip -dc $< > $@ + +$(IMAGES)/%.cue: testdata/%.cue + @mkdir -p $(IMAGES) + cp $< $@ + +$(IMAGES)/%.chd: testdata/%.chd + @mkdir -p $(IMAGES) + cp $< $@ + +clean: + rm -rf $(BUILD) + +.PHONY: all build run clean diff --git a/integration-tests/README.md b/integration-tests/README.md new file mode 100644 index 00000000..3dfad65c --- /dev/null +++ b/integration-tests/README.md @@ -0,0 +1,176 @@ +# USBODE integration tests + +Automated tests for the SCSI/Bulk-Only-Transport layer and the disc-image +readers that run on an ordinary PC — no Raspberry Pi, no cross-compiler, no +circle-stdlib checkout. CI runs them on every push and pull request that +touches the gadget or reader code (`.github/workflows/host-tests.yml`). + +``` +make -C integration-tests # build + run (command-layer + ISO/CUE/BIN images) +make -C integration-tests WITH_CHD=1 # also run the real .chd image through libchdr +USBODE_TEST_VERBOSE=1 integration-tests/out/usbode-host-tests # with firmware logs +``` + +## What this is + +The **real firmware sources** — all of `addon/usbcdgadget` (command +handlers, BOT state machine, `Update()` chunked-read path), the real +`cueparser`, and the real disc-image readers (`addon/discimage/cuebinfile.cpp` +and, under `WITH_CHD`, `chdfile.cpp` + libchdr) — are compiled for the build +machine against a set of thin stub headers (`harness/stubs/`) that stand in for +Circle. Nothing under test is reimplemented or mocked: the same `scsi_toc.cpp` +and `cuebinfile.cpp` that answer a Windows 98 machine answer the tests. + +The harness then behaves exactly like a USB host: + +1. `CGadgetTestBench` builds a CBW and copies it into the buffer the + gadget queued on its OUT endpoint, then calls `OnTransferComplete()` — + the same entry point the DWC interrupt handler uses on hardware. +2. The gadget runs its production dispatch (`HandleSCSICommand`), unit + attention gating, and response builders. Its `BeginTransfer()` calls + land in a test sink instead of DWC registers. +3. The bench completes data phases (calling `Update()` when the gadget + enters `DataInRead`, so multi-chunk READs work) until the CSW arrives. +4. Tests assert on the exact wire bytes: data phase, CSW status, and + **data residue** — the field Win9x storage stacks are strict about. + +Two flavours of disc back the bench, both driven through the same +`IImageDevice` interface the firmware uses: + +- **Command-layer tests** use a fake in-memory `IImageDevice` + (`harness/fakedisc.h`) with deterministic sector contents, to exercise the + SCSI/BOT layer in isolation. +- **Real-image tests** (`test-suite/test_realimages.cpp`) load actual files + through the real readers: + - the tracked `sdcard/image.iso.gz` and a **real FreeDOS ISO9660 + Joliet + disc** (`testdata/freedos-test.iso.gz`, built from genuine FreeDOS 1.3 GPL + files — see `testdata/README-testdata.md`), checked against real + on-disc structure (primary + Joliet volume descriptors, volume id); + - a **real game disc** (`testdata/shareware.iso.gz`: the Descent shareware + episode and the SkyRoads freeware game, both cleared for redistribution) + read back in full and compared byte-for-byte against the file — a real + ~3.5 MB filesystem with multi-megabyte files spanning many sectors and + read-ahead-cache refills; + - synthetic CUE/BIN pairs written at run time, plus committed **real cue + sheets loaded off the filesystem through the FatFs shim** + (`testdata/audiocd.*`, `testdata/mixed.*`), with known bytes so reads, + per-track offsets across the 2048->2352 boundary, and TOC/medium-type are + checked exactly; + - with `WITH_CHD=1`, the tracked `sdcard/usbode-audio-test.chd` and a + **mixed-mode CHD built with `chdman`** (`testdata/mixed.chd`), whose data + track is decoded through real libchdr and checked byte-exact. + + All committed test images are free-to-redistribute (FreeDOS GPL files or + generated content); see `testdata/README-testdata.md`. + +The only host-side seams the readers get are the raw file-access boundary +(`harness/fatfs_host.cpp`, a FatFs shim over stdio) and the two +`FatFsOptimizer` fast-seek entry points (`harness/discimage_host.cpp`, a no-op +since there is no FAT under the host filesystem). All reader logic — cue +parsing, per-track sector-size math, the read-ahead cache, CHD hunk +decompression — is the real firmware code. + +## Why these tests exist + +Every case encodes behavior that a real host depends on, and most encode a +bug USBODE actually shipped: + +| Test | Shipped bug it would have caught | +| --- | --- | +| `mode_sense10_medium_type_*` | #164: hardcoded medium type 0x13 broke Win98 CD audio ("data or no disc loaded") | +| `mode_sense10_page0e_win98_golden` | Byte-exact MODE SENSE page 0x0E response retail Win98 SE accepted before PLAY AUDIO (from a Trace Lab golden capture); also the pad-to-allocation-length fix | +| `read_toc_legacy_cdb9_session_info` | Win9x session-info request encoded in CDB[9] answered with a full TOC | +| `read_toc_format0_lba` residue check | CSW residue reported as 0 on short responses made Win98 usbstor.sys discard and retry forever | +| `read_blocked_by_unit_attention` | Missing STALL before CSW reset the device on Windows 11 xHCI | +| `read10_*` | Boundary clamping, multi-chunk `Update()` batching (32 blocks HS / 16 blocks FS), residue on truncated reads | +| `play_audio_*`, `read_subchannel_*` | The full MCICDA analog-audio sequence (the one Win98 QuickInstall's replaced USB stack never sends — oerg866/win98-quickinstall#151) | +| `real_iso_*`, `real_cuebin_*`, `real_chd_*` | The reader path: cue parsing, per-track offsets across the 2048->2352 boundary, the read-ahead cache, and real CHD hunk decompression, driven from real files rather than a fake | + +The bench itself found one latent firmware bug on day one: passing a +device to the `CUSBCDGadget` constructor makes `SetDevice()` delete the +device it was just handed and continue using the freed pointer +(production code always passes `nullptr` and calls `SetDevice()` later, +so the path is dormant — see `harness/bench.cpp`). + +## Layout + +``` +integration-tests/ + Makefile host build; `make` = build + run, WITH_CHD=1 adds CHD + harness/ + stubs/ minimal Circle/service headers (circle/, cdplayer/, fatfs/, ...) + stubs.cpp logger/scheduler/timer/endpoint implementations + testbus.h records BeginTransfer()/Stall() from the gadget + fakedisc.* in-memory disc images + cue sheets + fatfs_host.cpp FatFs f_open/f_read/... over host stdio (real-image reads) + discimage_host.cpp FatFsOptimizer no-op backing (fast seek n/a on host) + bench.* the virtual USB host + framework.* tiny TEST()/CHECK() runner + test-suite/ one file per command family, plus test_realimages.cpp +``` + +Two production accommodations (both inert on the device): + +- `tcdstate_update.cpp`: the ARM cache-maintenance asm is guarded by + `#if AARCH == 64 / #elif AARCH == 32 / #else (host: no-op)`. +- `usbcdgadget.h`: one `friend class CGadgetTestBench;` declaration. + +## Adding a test + +```cpp +#include "bench.h" +#include "framework.h" + +TEST(my_new_case) +{ + CFakeImageDevice *disc = MakeDataISO(1200); // 1200-sector data CD + CGadgetTestBench bench(disc); // optional: player fake + bench.Activate(); // enumerate + bench.RequestSense(); // clear unit attention + + const u8 cdb[10] = {0x43, /* ... */}; + auto r = bench.SendCommand(cdb, sizeof(cdb), /*transferLen=*/100); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(r.csw.dCSWDataResidue, 80u); + CHECK_BYTES(r.data.data(), r.data.size(), expected, sizeof(expected)); +} +``` + +Drop the file in `test-suite/` — the Makefile globs it, the `TEST` macro +registers it. To drive a real file instead of the fake, construct the reader +directly (see `test_realimages.cpp`) and hand it to the same bench. + +When a Trace Lab capture from a real machine shows a request/response +pair worth preserving (like the Win98 page 0x0E case), turn it into a +byte-exact test: that is the cheapest way to convert one afternoon of +hardware debugging into permanent coverage. + +## What this does not cover (Tier 2 ideas) + +Host-side tests cannot see real USB timing, the DWC controller, IRQ +interleaving, or actual host OS quirks. (The real FatFs is now partially +stood in for by a stdio shim; genuine on-SD-card behavior like fragmentation +and fast-seek still only runs on hardware.) A few production paths are also +deliberately out of scope here and only run on hardware: + +- **The image-loader factory** (`util.cpp` `loadCueBinIsoFileDevice`): format + dispatch by file extension and `.cue`->`.bin` filename resolution. The + real-image tests now read a `.cue` off the filesystem through the FatFs shim + and parse it, but they open the matching `.bin` by known name rather than + going through the production factory (it pulls in the MDS chain, which host + tests don't build). +- **`FatFsOptimizer` fast-seek**: CLMT allocation, `CREATE_LINKMAP`, and + fragmented-file seeking. The host shim makes fast-seek a no-op, so the + reader falls back to plain seeks. +- **The property-tag serial-number success path**: the host property-tag stub + always reports failure, so the gadget always takes its fallback-serial + branch; the hardware success branch and serial-descriptor formatting are + uncovered. + +The natural next tier is +hardware-in-the-loop: a PC with `sg3_utils` issuing the same command set +to a real USBODE over USB, comparing Trace Lab captures against golden +traces (`usbode_trace.py compare`). QEMU is not a shortcut here — its +dwc2 model is host-mode only, so the gadget side cannot run under +emulation. diff --git a/integration-tests/harness/bench.cpp b/integration-tests/harness/bench.cpp new file mode 100644 index 00000000..ac4a9016 --- /dev/null +++ b/integration-tests/harness/bench.cpp @@ -0,0 +1,141 @@ +// +// bench.cpp +// +#include "bench.h" + +#include +#include + +#include +#include + +CGadgetTestBench::CGadgetTestBench(IImageDevice *pDisc, bool bFullSpeed, + CCDPlayer *pPlayer, ConfigService *pConfig, + SCSITBService *pTBService) +{ + TestBus::Get().Reset(); + CTimer::Get()->TestReset(); + CScheduler::Get()->TestClearTasks(); + + if (pPlayer != nullptr) + { + CScheduler::Get()->TestRegisterTask("cdplayer", pPlayer); + } + if (pConfig != nullptr) + { + CScheduler::Get()->TestRegisterTask("configservice", pConfig); + } + if (pTBService != nullptr) + { + CScheduler::Get()->TestRegisterTask("scsitbservice", pTBService); + } + + // The gadget's destructor intentionally asserts (it must never be + // destroyed on the device), so bench instances leak it. Tests are + // short-lived processes; that is fine. + // + // Construct with nullptr and attach the disc via SetDevice(), + // mirroring CDROMService::Initialize(). Passing the device to the + // constructor takes SetDevice()'s eject path with m_pDevice == dev + // and deletes the device it was just given (latent use-after-free; + // production never uses that path). + gadget = new CUSBCDGadget(&m_Interrupt, bFullSpeed, nullptr); + gadget->SetDevice(pDisc); +} + +void CGadgetTestBench::Activate() +{ + gadget->AddEndpoints(); + + // Real hardware: the OUT endpoint's OnActivate() forwards to the + // gadget and arms the audio-init flag. + gadget->m_pEP[CUSBCDGadget::EPOut]->OnActivate(); +} + +CGadgetTestBench::Result CGadgetTestBench::SendCommand(const u8 *pCDB, size_t nCDBLength, + u32 nTransferLength, bool bDirIn, + const u8 *pOutData, size_t nOutLength) +{ + Result result; + TestBus &bus = TestBus::Get(); + + bus.inStalled = false; + bus.outStalled = false; + + // The gadget must be waiting for a CBW (armed by OnActivate or by the + // completion of the previous CSW). + assert(bus.outTransfer.valid && "gadget is not waiting for a CBW"); + + TUSBCDCBW cbw; + memset(&cbw, 0, sizeof(cbw)); + cbw.dCBWSignature = VALID_CBW_SIG; + cbw.dCBWTag = m_nNextTag++; + cbw.dCBWDataTransferLength = nTransferLength; + cbw.bmCBWFlags = bDirIn ? 0x80 : 0x00; + cbw.bCBWLUN = 0; + cbw.bCBWCBLength = (u8)nCDBLength; + memcpy(cbw.CBWCB, pCDB, nCDBLength); + + memcpy(bus.outTransfer.buffer, &cbw, SIZE_CBW); + bus.outTransfer.valid = false; + gadget->OnTransferComplete(FALSE, SIZE_CBW); + + // Pump the state machine until the CSW has been transferred. + for (int guard = 0; guard < 100000; guard++) + { + if (bus.inTransfer.valid) + { + TestBus::Pending t = bus.inTransfer; + bus.inTransfer.valid = false; + + if (gadget->m_nState == CUSBCDGadget::TCDState::SentCSW) + { + assert(t.length == SIZE_CSW); + memcpy(&result.csw, t.buffer, SIZE_CSW); + result.gotCSW = true; + gadget->OnTransferComplete(TRUE, t.length); // re-arms CBW + break; + } + + const u8 *p = (const u8 *)t.buffer; + result.data.insert(result.data.end(), p, p + t.length); + result.dataChunks++; + gadget->OnTransferComplete(TRUE, t.length); + continue; + } + + if (bus.outTransfer.valid && gadget->m_nState == CUSBCDGadget::TCDState::DataOut) + { + size_t n = nOutLength; + if (n > bus.outTransfer.length) + { + n = bus.outTransfer.length; + } + if (pOutData != nullptr && n > 0) + { + memcpy(bus.outTransfer.buffer, pOutData, n); + } + bus.outTransfer.valid = false; + gadget->OnTransferComplete(FALSE, n); + continue; + } + + if (gadget->m_nState == CUSBCDGadget::TCDState::DataInRead) + { + gadget->Update(); + continue; + } + + break; // no progress possible + } + + result.stalledIn = bus.inStalled; + result.stalledOut = bus.outStalled; + return result; +} + +CGadgetTestBench::Result CGadgetTestBench::RequestSense() +{ + const u8 cdb[6] = {0x03, 0x00, 0x00, 0x00, 18, 0x00}; + return SendCommand(cdb, sizeof(cdb), 18); +} diff --git a/integration-tests/harness/bench.h b/integration-tests/harness/bench.h new file mode 100644 index 00000000..68cc741a --- /dev/null +++ b/integration-tests/harness/bench.h @@ -0,0 +1,76 @@ +// +// bench.h +// +// CGadgetTestBench: constructs a real CUSBCDGadget around a fake disc and +// drives it exactly like a USB host does over Bulk-Only Transport: +// +// SendCommand() writes a CBW into the buffer the gadget queued for the +// OUT endpoint and fires OnTransferComplete(), then pumps the state +// machine — completing IN data phases, feeding OUT data phases, and +// calling Update() for the DataInRead chunked-read path — until the CSW +// arrives. +// +// Because the bench runs the production dispatch (HandleSCSICommand), unit +// attention gating, residue bookkeeping, and Update() chunking, tests see +// exactly the bytes a host would see on the wire. +// +#ifndef _test_host_bench_h +#define _test_host_bench_h + +#include "fakedisc.h" +#include "testbus.h" + +#include + +#include +#include +#include +#include + +#include + +class CGadgetTestBench +{ +public: + struct Result + { + std::vector data; // concatenated data-phase bytes + TUSBCDCSW csw{}; + bool gotCSW = false; + bool stalledIn = false; + bool stalledOut = false; + int dataChunks = 0; // number of IN data transfers (not counting CSW) + }; + + // Takes ownership of nothing; disc must outlive the bench. Accepts any + // IImageDevice: the in-memory CFakeImageDevice for command-layer tests, + // or a real reader (CCueBinFileDevice/CCHDFileDevice/...) for the + // real-image tests. Optional fakes are registered with the scheduler + // stub under their production task names before the gadget is constructed. + CGadgetTestBench(IImageDevice *pDisc, + bool bFullSpeed = false, + CCDPlayer *pPlayer = nullptr, + ConfigService *pConfig = nullptr, + SCSITBService *pTBService = nullptr); + + // AddEndpoints + endpoint activation: after this the drive is in + // UNIT ATTENTION state with a CBW transfer armed, same as right after + // enumeration on real hardware. + void Activate(); + + Result SendCommand(const u8 *pCDB, size_t nCDBLength, u32 nTransferLength, + bool bDirIn = true, + const u8 *pOutData = nullptr, size_t nOutLength = 0); + + // Convenience: REQUEST SENSE (also clears unit attention, like a host + // would after the first CHECK CONDITION). + Result RequestSense(); + + CUSBCDGadget *gadget = nullptr; + +private: + CInterruptSystem m_Interrupt; + u32 m_nNextTag = 1; +}; + +#endif diff --git a/integration-tests/harness/discimage_host.cpp b/integration-tests/harness/discimage_host.cpp new file mode 100644 index 00000000..5b4ddd3d --- /dev/null +++ b/integration-tests/harness/discimage_host.cpp @@ -0,0 +1,32 @@ +// +// discimage_host.cpp +// +// Host backing for the one piece of addon/discimage/util.cpp that the real +// CUE/BIN reader links against: FatFsOptimizer. The optimizer builds a FatFs +// cluster link map so seeks skip FAT-chain walks on the Pi's SD card. There +// is no FAT under the host filesystem, so fast seek simply does not apply +// here: EnableFastSeek reports "disabled" and the reader falls back to +// ordinary f_lseek() (functionally identical, just without the on-Pi +// fragmentation optimization). This lets the tests exercise cuebinfile.cpp +// without pulling in util.cpp's image-format factory (and its MDS chain). +// +#include + +boolean FatFsOptimizer::EnableFastSeek(FIL* pFile, DWORD** ppCLMT, size_t clmtSize, const char* logPrefix) +{ + (void)pFile; + (void)clmtSize; + (void)logPrefix; + if (ppCLMT) { + *ppCLMT = nullptr; + } + return false; +} + +void FatFsOptimizer::DisableFastSeek(DWORD** ppCLMT) +{ + if (ppCLMT && *ppCLMT) { + delete[] *ppCLMT; + *ppCLMT = nullptr; + } +} diff --git a/integration-tests/harness/fakedisc.cpp b/integration-tests/harness/fakedisc.cpp new file mode 100644 index 00000000..79fcd77b --- /dev/null +++ b/integration-tests/harness/fakedisc.cpp @@ -0,0 +1,82 @@ +// +// fakedisc.cpp +// +#include "fakedisc.h" + +#include + +void FillPatternSector(u8 *dest, u32 lba, u32 sectorSize) +{ + for (u32 j = 0; j < sectorSize; j++) + { + dest[j] = (u8)((lba * 7 + j) & 0xFF); + } + dest[0] = (u8)(lba >> 24); + dest[1] = (u8)(lba >> 16); + dest[2] = (u8)(lba >> 8); + dest[3] = (u8)(lba >> 0); +} + +static std::string MSFString(u32 lba) +{ + char buf[16]; + snprintf(buf, sizeof(buf), "%02u:%02u:%02u", lba / (75 * 60), (lba / 75) % 60, lba % 75); + return buf; +} + +static std::vector MakePatternImage(u32 numSectors, u32 sectorSize) +{ + std::vector image((size_t)numSectors * sectorSize); + for (u32 lba = 0; lba < numSectors; lba++) + { + FillPatternSector(image.data() + (size_t)lba * sectorSize, lba, sectorSize); + } + return image; +} + +CFakeImageDevice *MakeDataISO(u32 numSectors) +{ + std::string cue = "FILE \"image.bin\" BINARY\n" + " TRACK 01 MODE1/2048\n" + " INDEX 01 00:00:00\n"; + + CFakeImageDevice *dev = new CFakeImageDevice(cue, MakePatternImage(numSectors, 2048), 2048); + dev->m_numTracks = 1; + return dev; +} + +CFakeImageDevice *MakeAudioCD(int nTracks, u32 sectorsPerTrack) +{ + std::string cue = "FILE \"image.bin\" BINARY\n"; + for (int t = 0; t < nTracks; t++) + { + char line[64]; + snprintf(line, sizeof(line), " TRACK %02d AUDIO\n", t + 1); + cue += line; + cue += " INDEX 01 " + MSFString((u32)t * sectorsPerTrack) + "\n"; + } + + CFakeImageDevice *dev = new CFakeImageDevice( + cue, MakePatternImage((u32)nTracks * sectorsPerTrack, 2352), 2352); + dev->m_numTracks = nTracks; + return dev; +} + +CFakeImageDevice *MakeMixedModeCD(u32 dataSectors, int nAudioTracks, u32 audioSectorsPerTrack) +{ + std::string cue = "FILE \"image.bin\" BINARY\n" + " TRACK 01 MODE1/2352\n" + " INDEX 01 00:00:00\n"; + for (int t = 0; t < nAudioTracks; t++) + { + char line[64]; + snprintf(line, sizeof(line), " TRACK %02d AUDIO\n", t + 2); + cue += line; + cue += " INDEX 01 " + MSFString(dataSectors + (u32)t * audioSectorsPerTrack) + "\n"; + } + + u32 total = dataSectors + (u32)nAudioTracks * audioSectorsPerTrack; + CFakeImageDevice *dev = new CFakeImageDevice(cue, MakePatternImage(total, 2352), 2352); + dev->m_numTracks = 1 + nAudioTracks; + return dev; +} diff --git a/integration-tests/harness/fakedisc.h b/integration-tests/harness/fakedisc.h new file mode 100644 index 00000000..48457416 --- /dev/null +++ b/integration-tests/harness/fakedisc.h @@ -0,0 +1,96 @@ +// +// fakedisc.h +// +// In-memory IImageDevice with a synthetic cue sheet, standing in for +// CueBinFileDevice. Physical sector size is uniform across the disc so +// GetByteOffsetForLBA() stays a simple multiply (same as a pure-ISO or +// pure-raw BIN image). +// +// Data sectors carry a deterministic pattern: the first 4 bytes are the +// LBA big-endian, the rest is (lba * 7 + offset) & 0xFF, so tests can +// verify READ payloads end to end. +// +#ifndef _test_host_fakedisc_h +#define _test_host_fakedisc_h + +#include + +#include + +#include +#include + +class CFakeImageDevice : public IImageDevice +{ +public: + CFakeImageDevice(std::string cueSheet, std::vector image, u32 physSectorSize) + : m_cue(std::move(cueSheet)), m_image(std::move(image)), m_sectorSize(physSectorSize) + { + } + + // CDevice + int Read(void *pBuffer, size_t nCount) override + { + if (m_pos >= m_image.size()) + { + return 0; + } + size_t avail = m_image.size() - (size_t)m_pos; + if (nCount > avail) + { + nCount = avail; + } + memcpy(pBuffer, m_image.data() + m_pos, nCount); + m_pos += nCount; + return (int)nCount; + } + + // IImageDevice + u64 Seek(u64 ullOffset) override + { + if (ullOffset > m_image.size()) + { + return (u64)-1; + } + m_pos = ullOffset; + return ullOffset; + } + + u64 GetSize(void) const override { return m_image.size(); } + u64 Tell(void) const override { return m_pos; } + + u64 GetByteOffsetForLBA(u32 lba) const override { return (u64)lba * m_sectorSize; } + + FileType GetFileType(void) const override { return FileType::CUEBIN; } + + int GetNumTracks(void) const override { return m_numTracks; } + u32 GetTrackStart(int track) const override { return 0; } + u32 GetTrackLength(int track) const override { return 0; } + bool IsAudioTrack(int track) const override { return false; } + + const char *GetCueSheet(void) const override { return m_cue.c_str(); } + + int m_numTracks = 1; + +private: + std::string m_cue; + std::vector m_image; + u32 m_sectorSize; + u64 m_pos = 0; +}; + +// Fills one sector's worth of bytes with the deterministic test pattern. +void FillPatternSector(u8 *dest, u32 lba, u32 sectorSize); + +// Single MODE1/2048 data track ("ISO style"), physical sector size 2048. +CFakeImageDevice *MakeDataISO(u32 numSectors); + +// Pure audio CD: nTracks AUDIO tracks of sectorsPerTrack each, 2352-byte +// sectors. +CFakeImageDevice *MakeAudioCD(int nTracks, u32 sectorsPerTrack); + +// Mixed mode: one MODE1/2352 data track followed by audio tracks, uniform +// 2352-byte physical sectors. +CFakeImageDevice *MakeMixedModeCD(u32 dataSectors, int nAudioTracks, u32 audioSectorsPerTrack); + +#endif diff --git a/integration-tests/harness/fatfs_host.cpp b/integration-tests/harness/fatfs_host.cpp new file mode 100644 index 00000000..01bf1c34 --- /dev/null +++ b/integration-tests/harness/fatfs_host.cpp @@ -0,0 +1,135 @@ +// +// fatfs_host.cpp +// +// Host backend for the FatFs seam declared in stubs/fatfs/ff.h. Maps the +// handful of f_* calls the real disc-image readers make onto plain host +// stdio, so cuebinfile/mdsfile/util.cpp can open real image files off the +// build machine's filesystem. No FatFs or reader logic is reimplemented; +// this is purely the raw-file access boundary. +// +#include + +#include + +extern "C" { + +FRESULT f_open(FIL* fp, const TCHAR* path, BYTE mode) +{ + if (!fp || !path) { + return FR_INVALID_PARAMETER; + } + // The readers only ever open images read-only. + FILE* f = fopen(path, "rb"); + if (!f) { + return FR_NO_FILE; + } + if (fseeko(f, 0, SEEK_END) != 0) { + fclose(f); + return FR_DISK_ERR; + } + off_t size = ftello(f); + if (size < 0) { + fclose(f); + return FR_DISK_ERR; + } + rewind(f); + + fp->obj.objsize = (FSIZE_t)size; + fp->fptr = 0; + fp->cltbl = nullptr; + fp->host_fp = f; + return FR_OK; +} + +FRESULT f_close(FIL* fp) +{ + if (!fp || !fp->host_fp) { + return FR_INVALID_OBJECT; + } + fclose((FILE*)fp->host_fp); + fp->host_fp = nullptr; + return FR_OK; +} + +FRESULT f_read(FIL* fp, void* buff, UINT btr, UINT* br) +{ + if (br) { + *br = 0; + } + if (!fp || !fp->host_fp || !buff) { + return FR_INVALID_OBJECT; + } + size_t n = fread(buff, 1, btr, (FILE*)fp->host_fp); + if (n != btr && ferror((FILE*)fp->host_fp)) { + return FR_DISK_ERR; + } + fp->fptr += n; + if (br) { + *br = (UINT)n; + } + return FR_OK; +} + +FRESULT f_write(FIL* fp, const void* buff, UINT btw, UINT* bw) +{ + if (bw) { + *bw = 0; + } + if (!fp || !fp->host_fp || !buff) { + return FR_INVALID_OBJECT; + } + size_t n = fwrite(buff, 1, btw, (FILE*)fp->host_fp); + fp->fptr += n; + if (bw) { + *bw = (UINT)n; + } + return (n == btw) ? FR_OK : FR_DISK_ERR; +} + +FRESULT f_lseek(FIL* fp, FSIZE_t ofs) +{ + if (!fp || !fp->host_fp) { + return FR_INVALID_OBJECT; + } + // Fast-seek link-map creation: no FAT here, so report success and let the + // readers fall through to ordinary seeks (functionally identical, just + // without the on-Pi fragmentation optimization). + if (ofs == CREATE_LINKMAP) { + return FR_OK; + } + if (fseeko((FILE*)fp->host_fp, (off_t)ofs, SEEK_SET) != 0) { + return FR_DISK_ERR; + } + fp->fptr = ofs; + return FR_OK; +} + +// Directory walk: intentionally unbacked. Only mdsfile.cpp calls these, and +// MDS images are not exercised by the tests; these exist so the loader links. +// f_opendir reports "no path" so any accidental MDS load fails cleanly rather +// than silently pretending a directory is empty. +FRESULT f_opendir(DIR* dp, const TCHAR* path) +{ + (void)path; + if (dp) { + dp->host_dir = nullptr; + } + return FR_NO_PATH; +} + +FRESULT f_readdir(DIR* dp, FILINFO* fno) +{ + (void)dp; + if (fno) { + fno->fname[0] = '\0'; + } + return FR_NO_PATH; +} + +FRESULT f_closedir(DIR* dp) +{ + (void)dp; + return FR_OK; +} + +} // extern "C" diff --git a/integration-tests/harness/framework.cpp b/integration-tests/harness/framework.cpp new file mode 100644 index 00000000..9babb0c7 --- /dev/null +++ b/integration-tests/harness/framework.cpp @@ -0,0 +1,133 @@ +// +// framework.cpp +// +#include "framework.h" + +#include + +#include + +namespace +{ + struct TestCase + { + const char *name; + void (*fn)(); + }; + + std::vector &Tests() + { + static std::vector tests; + return tests; + } + + int g_nFailures = 0; + int g_nFailuresInCurrent = 0; + const char *g_pCurrentTest = ""; +} + +int RegisterTest(const char *pName, void (*pfnTest)()) +{ + Tests().push_back({pName, pfnTest}); + return (int)Tests().size(); +} + +void ReportFailure(const char *pFile, int nLine, const std::string &message) +{ + printf(" FAIL [%s] %s:%d: %s\n", g_pCurrentTest, pFile, nLine, message.c_str()); + g_nFailures++; + g_nFailuresInCurrent++; +} + +static void HexDump(const char *pLabel, const uint8_t *pData, size_t nLen) +{ + printf(" %s (%zu bytes):\n", pLabel, nLen); + for (size_t i = 0; i < nLen; i += 16) + { + printf(" [%04zx]", i); + for (size_t j = i; j < i + 16 && j < nLen; j++) + { + printf(" %02x", pData[j]); + } + printf("\n"); + } +} + +void CheckBytesImpl(const char *pFile, int nLine, const char *pWhat, + const uint8_t *pActual, size_t nActualLen, + const uint8_t *pExpected, size_t nExpectedLen) +{ + bool lengthOk = nActualLen == nExpectedLen; + bool bytesOk = lengthOk; + size_t firstDiff = 0; + + if (lengthOk) + { + for (size_t i = 0; i < nActualLen; i++) + { + if (pActual[i] != pExpected[i]) + { + bytesOk = false; + firstDiff = i; + break; + } + } + } + + if (bytesOk) + { + return; + } + + std::ostringstream os; + os << "CHECK_BYTES failed for " << pWhat; + if (!lengthOk) + { + os << ": length " << nActualLen << ", expected " << nExpectedLen; + } + else + { + os << ": first difference at offset " << firstDiff; + } + ReportFailure(pFile, nLine, os.str()); + HexDump("actual", pActual, nActualLen); + HexDump("expected", pExpected, nExpectedLen); +} + +int RunAllTests() +{ + int nRun = 0; + int nFailedTests = 0; + + for (const auto &test : Tests()) + { + g_pCurrentTest = test.name; + g_nFailuresInCurrent = 0; + if (getenv("USBODE_TEST_TRACE") != nullptr) + { + printf(" run %s\n", test.name); + fflush(stdout); + } + test.fn(); + nRun++; + if (g_nFailuresInCurrent > 0) + { + nFailedTests++; + } + else + { + printf(" ok %s\n", test.name); + } + } + + printf("\n%d tests, %d failed (%d individual check failures)\n", + nRun, nFailedTests, g_nFailures); + return g_nFailures == 0 ? 0 : 1; +} + +int main() +{ + setvbuf(stdout, nullptr, _IONBF, 0); // keep output on crashes + printf("USBODE host regression tests\n"); + return RunAllTests(); +} diff --git a/integration-tests/harness/framework.h b/integration-tests/harness/framework.h new file mode 100644 index 00000000..e37e7953 --- /dev/null +++ b/integration-tests/harness/framework.h @@ -0,0 +1,59 @@ +// +// framework.h +// +// Micro test framework: TEST() registers a case, CHECK*() records +// failures with context, main() runs everything and returns nonzero if +// anything failed. +// +#ifndef _test_host_framework_h +#define _test_host_framework_h + +#include +#include + +#include +#include + +int RegisterTest(const char *pName, void (*pfnTest)()); +void ReportFailure(const char *pFile, int nLine, const std::string &message); +int RunAllTests(); + +void CheckBytesImpl(const char *pFile, int nLine, const char *pWhat, + const uint8_t *pActual, size_t nActualLen, + const uint8_t *pExpected, size_t nExpectedLen); + +#define TEST(name) \ + static void test_##name(); \ + static const int reg_##name = RegisterTest(#name, test_##name); \ + static void test_##name() + +#define CHECK(cond) \ + do \ + { \ + if (!(cond)) \ + { \ + ReportFailure(__FILE__, __LINE__, "CHECK failed: " #cond); \ + } \ + } while (0) + +#define CHECK_EQ(actual, expected) \ + do \ + { \ + auto _a = (actual); \ + auto _e = (expected); \ + if (!(_a == _e)) \ + { \ + std::ostringstream _os; \ + _os << "CHECK_EQ failed: " #actual " == " #expected \ + << " (actual " << +_a << " / 0x" << std::hex << +_a \ + << ", expected " << std::dec << +_e << " / 0x" << std::hex \ + << +_e << ")"; \ + ReportFailure(__FILE__, __LINE__, _os.str()); \ + } \ + } while (0) + +#define CHECK_BYTES(actual, actualLen, expected, expectedLen) \ + CheckBytesImpl(__FILE__, __LINE__, #actual, (const uint8_t *)(actual), (actualLen), \ + (const uint8_t *)(expected), (expectedLen)) + +#endif diff --git a/integration-tests/harness/stubs.cpp b/integration-tests/harness/stubs.cpp new file mode 100644 index 00000000..37dddad1 --- /dev/null +++ b/integration-tests/harness/stubs.cpp @@ -0,0 +1,207 @@ +// +// stubs.cpp +// +// Host implementations for the Circle classes the USBODE gadget links +// against: logger (env-gated stdout), scheduler task registry, virtual +// timer, and the USB gadget base classes whose BeginTransfer()/Stall() +// land in the TestBus sink instead of DWC hardware. +// +#include "testbus.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +// --------------------------------------------------------------------------- +// TestBus +// --------------------------------------------------------------------------- + +TestBus &TestBus::Get() +{ + static TestBus instance; + return instance; +} + +// --------------------------------------------------------------------------- +// CLogger +// --------------------------------------------------------------------------- + +CLogger *CLogger::Get(void) +{ + static CLogger instance; + return &instance; +} + +void CLogger::Write(const char *pSource, TLogSeverity Severity, const char *pMessage, ...) +{ + static const bool verbose = getenv("USBODE_TEST_VERBOSE") != nullptr; + if (!verbose) + { + return; + } + + static const char *severityNames[] = {"panic", "error", "warn", "note", "debug"}; + fprintf(stdout, "[%s] %s: ", severityNames[Severity], pSource); + + va_list var; + va_start(var, pMessage); + vfprintf(stdout, pMessage, var); + va_end(var); + + fprintf(stdout, "\n"); +} + +// --------------------------------------------------------------------------- +// CScheduler +// --------------------------------------------------------------------------- + +namespace +{ + std::vector> &TaskRegistry() + { + static std::vector> registry; + return registry; + } +} + +CScheduler *CScheduler::Get(void) +{ + static CScheduler instance; + return &instance; +} + +CTask *CScheduler::GetTask(const char *pTaskName) +{ + for (auto &entry : TaskRegistry()) + { + if (entry.first == pTaskName) + { + return entry.second; + } + } + return nullptr; +} + +void CScheduler::TestRegisterTask(const char *pName, CTask *pTask) +{ + TaskRegistry().push_back({pName, pTask}); +} + +void CScheduler::TestClearTasks(void) +{ + TaskRegistry().clear(); +} + +// --------------------------------------------------------------------------- +// CTimer +// --------------------------------------------------------------------------- + +namespace +{ + unsigned g_nTicks = 1000; // arbitrary nonzero start +} + +CTimer *CTimer::Get(void) +{ + static CTimer instance; + return &instance; +} + +unsigned CTimer::GetTicks(void) +{ + return g_nTicks; +} + +unsigned CTimer::GetClockTicks(void) +{ + return g_nTicks * (CLOCKHZ / 100); +} + +void CTimer::TestAdvanceTicks(unsigned nTicks) +{ + g_nTicks += nTicks; +} + +void CTimer::TestReset(void) +{ + g_nTicks = 1000; +} + +// --------------------------------------------------------------------------- +// ConfigService static +// --------------------------------------------------------------------------- + +ConfigService *ConfigService::s_pThis = nullptr; + +// --------------------------------------------------------------------------- +// CDWUSBGadget +// --------------------------------------------------------------------------- + +CDWUSBGadget::CDWUSBGadget(CInterruptSystem *pInterruptSystem, TDeviceSpeed DeviceSpeed) + : m_DeviceSpeed(DeviceSpeed) +{ +} + +CDWUSBGadget::~CDWUSBGadget(void) +{ +} + +// --------------------------------------------------------------------------- +// CDWUSBGadgetEndpoint +// --------------------------------------------------------------------------- + +CDWUSBGadgetEndpoint::CDWUSBGadgetEndpoint(const TUSBEndpointDescriptor *pDesc, CDWUSBGadget *pGadget) + : m_Direction((pDesc->bEndpointAddress & 0x80) ? DirectionIn : DirectionOut), + m_nMaxPacketSize(pDesc->wMaxPacketSize) +{ +} + +CDWUSBGadgetEndpoint::~CDWUSBGadgetEndpoint(void) +{ +} + +void CDWUSBGadgetEndpoint::BeginTransfer(TTransferMode Mode, void *pBuffer, size_t nLength) +{ + TestBus &bus = TestBus::Get(); + if (Mode == TransferDataIn) + { + bus.inTransfer.valid = true; + bus.inTransfer.buffer = pBuffer; + bus.inTransfer.length = nLength; + } + else + { + bus.outTransfer.valid = true; + bus.outTransfer.buffer = pBuffer; + bus.outTransfer.length = nLength; + } +} + +void CDWUSBGadgetEndpoint::Stall(boolean bIn) +{ + TestBus &bus = TestBus::Get(); + if (bIn) + { + bus.inStalled = true; + } + else + { + bus.outStalled = true; + } +} + +void CDWUSBGadgetEndpoint::SetMaxPacketSize(size_t nSize) +{ + m_nMaxPacketSize = nSize; +} diff --git a/integration-tests/harness/stubs/cdplayer/cdplayer.h b/integration-tests/harness/stubs/cdplayer/cdplayer.h new file mode 100644 index 00000000..95b62a8d --- /dev/null +++ b/integration-tests/harness/stubs/cdplayer/cdplayer.h @@ -0,0 +1,104 @@ +// +// Host-build stub for . +// An instrumented fake: the PlayState enum and the method signatures match +// the real CCDPlayer, but every call is recorded so tests can assert what +// the SCSI layer asked the player to do (e.g. that PLAY AUDIO MSF reaches +// Play() with the right LBA range), and tests can preset the state the +// player reports back (for READ SUB-CHANNEL). +// +#ifndef _cdplayer_cdplayer_h +#define _cdplayer_cdplayer_h + +#include +#include +#include + +class CCDPlayer : public CTask +{ +public: + enum PlayState + { + PLAYING, + SEEKING, + SEEKING_PLAYING, + STOPPED_OK, + STOPPED_ERROR, + PAUSED, + NONE + }; + + CCDPlayer(void) {} + + void EnsureAudioInitialized(void) { ensureAudioInitializedCalls++; } + + boolean SetDevice(IImageDevice *pDevice) + { + device = pDevice; + setDeviceCalls++; + return TRUE; + } + + boolean Pause(void) + { + pauseCalls++; + state = PAUSED; + return TRUE; + } + + boolean Resume(void) + { + resumeCalls++; + state = PLAYING; + return TRUE; + } + + boolean SetVolume(u8 vol) + { + volume = vol; + setVolumeCalls++; + return TRUE; + } + + u8 GetVolume(void) { return volume; } + + unsigned int GetState(void) { return state; } + + u32 GetCurrentAddress(void) { return currentAddress; } + + boolean Seek(u32 lba) + { + seekCalls++; + lastSeekLBA = lba; + currentAddress = lba; + return TRUE; + } + + boolean Play(u32 lba, u32 num_blocks) + { + playCalls++; + lastPlayLBA = lba; + lastPlayBlocks = num_blocks; + currentAddress = lba; + state = PLAYING; + return TRUE; + } + + // Test-visible call log and presettable state + IImageDevice *device = nullptr; + PlayState state = NONE; + u32 currentAddress = 0; + u8 volume = 255; + + int playCalls = 0; + u32 lastPlayLBA = 0; + u32 lastPlayBlocks = 0; + int pauseCalls = 0; + int resumeCalls = 0; + int seekCalls = 0; + u32 lastSeekLBA = 0; + int setVolumeCalls = 0; + int setDeviceCalls = 0; + int ensureAudioInitializedCalls = 0; +}; + +#endif diff --git a/integration-tests/harness/stubs/circle/bcmpropertytags.h b/integration-tests/harness/stubs/circle/bcmpropertytags.h new file mode 100644 index 00000000..da22db81 --- /dev/null +++ b/integration-tests/harness/stubs/circle/bcmpropertytags.h @@ -0,0 +1,27 @@ +// +// Host-build stub for . +// GetTag() always fails, so the gadget falls back to its default serial +// number ("USBODE-00000001") — deterministic for tests. +// +#ifndef _circle_bcmpropertytags_h +#define _circle_bcmpropertytags_h + +#include + +#define PROPTAG_GET_BOARD_SERIAL 0x00010004 + +struct TPropertyTagSerial +{ + u32 Serial[2]; +}; + +class CBcmPropertyTags +{ +public: + boolean GetTag(u32 nTagId, void *pTag, unsigned nTagSize, unsigned nRequestParmSize = 0) + { + return FALSE; + } +}; + +#endif diff --git a/integration-tests/harness/stubs/circle/device.h b/integration-tests/harness/stubs/circle/device.h new file mode 100644 index 00000000..fbbba21d --- /dev/null +++ b/integration-tests/harness/stubs/circle/device.h @@ -0,0 +1,18 @@ +// +// Host-build stub for . +// +#ifndef _circle_device_h +#define _circle_device_h + +#include + +class CDevice +{ +public: + virtual ~CDevice(void) {} + + virtual int Read(void *pBuffer, size_t nCount) { return -1; } + virtual int Write(const void *pBuffer, size_t nCount) { return -1; } +}; + +#endif diff --git a/integration-tests/harness/stubs/circle/fs/partitionmanager.h b/integration-tests/harness/stubs/circle/fs/partitionmanager.h new file mode 100644 index 00000000..53fe97c1 --- /dev/null +++ b/integration-tests/harness/stubs/circle/fs/partitionmanager.h @@ -0,0 +1,9 @@ +// +// Host-build stub for . +// Pulled in transitively by the disc-image readers; no symbol from it is +// referenced on the paths the tests exercise, so an empty shim suffices. +// +#ifndef _circle_fs_partitionmanager_h +#define _circle_fs_partitionmanager_h + +#endif diff --git a/integration-tests/harness/stubs/circle/interrupt.h b/integration-tests/harness/stubs/circle/interrupt.h new file mode 100644 index 00000000..fed8f506 --- /dev/null +++ b/integration-tests/harness/stubs/circle/interrupt.h @@ -0,0 +1,13 @@ +// +// Host-build stub for . +// +#ifndef _circle_interrupt_h +#define _circle_interrupt_h + +class CInterruptSystem +{ +public: + CInterruptSystem(void) {} +}; + +#endif diff --git a/integration-tests/harness/stubs/circle/koptions.h b/integration-tests/harness/stubs/circle/koptions.h new file mode 100644 index 00000000..801c94c5 --- /dev/null +++ b/integration-tests/harness/stubs/circle/koptions.h @@ -0,0 +1,13 @@ +// +// Host-build stub for . +// +#ifndef _circle_koptions_h +#define _circle_koptions_h + +class CKernelOptions +{ +public: + static CKernelOptions *Get(void) { return nullptr; } +}; + +#endif diff --git a/integration-tests/harness/stubs/circle/logger.h b/integration-tests/harness/stubs/circle/logger.h new file mode 100644 index 00000000..be9b05a5 --- /dev/null +++ b/integration-tests/harness/stubs/circle/logger.h @@ -0,0 +1,38 @@ +// +// Host-build stub for . +// Messages are printed to stdout only when USBODE_TEST_VERBOSE is set in +// the environment, so test output stays readable by default. +// +#ifndef _circle_logger_h +#define _circle_logger_h + +#include + +enum TLogSeverity +{ + LogPanic, + LogError, + LogWarning, + LogNotice, + LogDebug +}; + +class CLogger +{ +public: + static CLogger *Get(void); + + void Write(const char *pSource, TLogSeverity Severity, const char *pMessage, ...); +}; + +// Match the real Circle logging macros so firmware sources that use +// LOGMODULE()/LOGNOTE()/LOGERR()/... (e.g. the disc-image readers) compile +// unchanged. Output is still gated by USBODE_TEST_VERBOSE in Write(). +#define LOGMODULE(name) static const char From[] = name +#define LOGPANIC(...) CLogger::Get()->Write(From, LogPanic, __VA_ARGS__) +#define LOGERR(...) CLogger::Get()->Write(From, LogError, __VA_ARGS__) +#define LOGWARN(...) CLogger::Get()->Write(From, LogWarning, __VA_ARGS__) +#define LOGNOTE(...) CLogger::Get()->Write(From, LogNotice, __VA_ARGS__) +#define LOGDBG(...) CLogger::Get()->Write(From, LogDebug, __VA_ARGS__) + +#endif diff --git a/integration-tests/harness/stubs/circle/macros.h b/integration-tests/harness/stubs/circle/macros.h new file mode 100644 index 00000000..859fb55c --- /dev/null +++ b/integration-tests/harness/stubs/circle/macros.h @@ -0,0 +1,11 @@ +// +// Host-build stub for . +// +#ifndef _circle_macros_h +#define _circle_macros_h + +#define PACKED __attribute__((packed)) +#define ALIGN(n) __attribute__((aligned(n))) +#define NORETURN __attribute__((noreturn)) + +#endif diff --git a/integration-tests/harness/stubs/circle/new.h b/integration-tests/harness/stubs/circle/new.h new file mode 100644 index 00000000..06d804c3 --- /dev/null +++ b/integration-tests/harness/stubs/circle/new.h @@ -0,0 +1,9 @@ +// +// Host-build stub for . +// +#ifndef _circle_new_h +#define _circle_new_h + +#include + +#endif diff --git a/integration-tests/harness/stubs/circle/sched/scheduler.h b/integration-tests/harness/stubs/circle/sched/scheduler.h new file mode 100644 index 00000000..9dd0a7a4 --- /dev/null +++ b/integration-tests/harness/stubs/circle/sched/scheduler.h @@ -0,0 +1,30 @@ +// +// Host-build stub for . +// GetTask() resolves names from a test-populated registry, so tests decide +// which services (cdplayer, configservice, scsitbservice) "exist". +// +#ifndef _circle_sched_scheduler_h +#define _circle_sched_scheduler_h + +#include +#include // Circle's scheduler.h pulls this in transitively +#include + +class CScheduler +{ +public: + static CScheduler *Get(void); + + CTask *GetTask(const char *pTaskName); + + void Sleep(unsigned nSeconds) {} + void MsSleep(unsigned nMilliSeconds) {} + void usSleep(unsigned nMicroSeconds) {} + void Yield(void) {} + + // Test control + void TestRegisterTask(const char *pName, CTask *pTask); + void TestClearTasks(void); +}; + +#endif diff --git a/integration-tests/harness/stubs/circle/sched/synchronizationevent.h b/integration-tests/harness/stubs/circle/sched/synchronizationevent.h new file mode 100644 index 00000000..7d89e45f --- /dev/null +++ b/integration-tests/harness/stubs/circle/sched/synchronizationevent.h @@ -0,0 +1,16 @@ +// +// Host-build stub for . +// +#ifndef _circle_sched_synchronizationevent_h +#define _circle_sched_synchronizationevent_h + +class CSynchronizationEvent +{ +public: + CSynchronizationEvent(void) {} + void Set(void) {} + void Clear(void) {} + void Wait(void) {} +}; + +#endif diff --git a/integration-tests/harness/stubs/circle/sched/task.h b/integration-tests/harness/stubs/circle/sched/task.h new file mode 100644 index 00000000..f9e69476 --- /dev/null +++ b/integration-tests/harness/stubs/circle/sched/task.h @@ -0,0 +1,18 @@ +// +// Host-build stub for . +// +#ifndef _circle_sched_task_h +#define _circle_sched_task_h + +#include + +class CTask +{ +public: + CTask(void) {} + virtual ~CTask(void) {} + + virtual void Run(void) {} +}; + +#endif diff --git a/integration-tests/harness/stubs/circle/stdarg.h b/integration-tests/harness/stubs/circle/stdarg.h new file mode 100644 index 00000000..9b135b2a --- /dev/null +++ b/integration-tests/harness/stubs/circle/stdarg.h @@ -0,0 +1,9 @@ +// +// Host-build stub for : just the host's variadic macros. +// +#ifndef _circle_stdarg_h +#define _circle_stdarg_h + +#include + +#endif diff --git a/integration-tests/harness/stubs/circle/synchronize.h b/integration-tests/harness/stubs/circle/synchronize.h new file mode 100644 index 00000000..fc5a6f2d --- /dev/null +++ b/integration-tests/harness/stubs/circle/synchronize.h @@ -0,0 +1,20 @@ +// +// Host-build stub for . +// DMA buffers are ordinary aligned arrays on the host; barriers and +// peripheral fences are no-ops. +// +#ifndef _circle_synchronize_h +#define _circle_synchronize_h + +#define CACHE_ALIGN alignas(64) +#define DMA_BUFFER(type, name, num) alignas(64) type name[num] + +static inline void DataSyncBarrier(void) {} +static inline void DataMemBarrier(void) {} +static inline void InstructionSyncBarrier(void) {} +static inline void PeripheralEntry(void) {} +static inline void PeripheralExit(void) {} +static inline void EnableIRQs(void) {} +static inline void DisableIRQs(void) {} + +#endif diff --git a/integration-tests/harness/stubs/circle/sysconfig.h b/integration-tests/harness/stubs/circle/sysconfig.h new file mode 100644 index 00000000..d60337b8 --- /dev/null +++ b/integration-tests/harness/stubs/circle/sysconfig.h @@ -0,0 +1,8 @@ +// +// Host-build stub for . Intentionally empty; AARCH is +// left undefined so architecture-specific code paths compile out. +// +#ifndef _circle_sysconfig_h +#define _circle_sysconfig_h + +#endif diff --git a/integration-tests/harness/stubs/circle/timer.h b/integration-tests/harness/stubs/circle/timer.h new file mode 100644 index 00000000..7e886752 --- /dev/null +++ b/integration-tests/harness/stubs/circle/timer.h @@ -0,0 +1,32 @@ +// +// Host-build stub for . +// Tick time is virtual and test-controlled: tests advance it explicitly +// with TestAdvanceTicks() to exercise time-dependent paths (disc-swap +// settle window) deterministically. +// +#ifndef _circle_timer_h +#define _circle_timer_h + +#include + +#define CLOCKHZ 1000000 +#ifndef HZ +#define HZ 100 +#endif + +class CTimer +{ +public: + static CTimer *Get(void); + + unsigned GetTicks(void); + unsigned GetClockTicks(void); + void MsDelay(unsigned nMilliSeconds) {} + void usDelay(unsigned nMicroSeconds) {} + + // Test control + void TestAdvanceTicks(unsigned nTicks); + void TestReset(void); +}; + +#endif diff --git a/integration-tests/harness/stubs/circle/types.h b/integration-tests/harness/stubs/circle/types.h new file mode 100644 index 00000000..0f8f250b --- /dev/null +++ b/integration-tests/harness/stubs/circle/types.h @@ -0,0 +1,48 @@ +// +// Host-build stub for . +// Minimal type aliases so USBODE sources compile unmodified on a PC. +// +#ifndef _circle_types_h +#define _circle_types_h + +#include +#include + +// On the device the mem/str/printf functions arrive transitively through +// Circle headers; provide them the same way here so firmware sources +// compile with both libc++ (macOS) and libstdc++ (Linux). +#include +#include + +// Use the host libc's htons/htonl and tell usbcdgadget.h not to define its +// own fallback inlines. (An audit suggested dropping this so the firmware's +// own byte-swap inlines run instead, but on macOS the system htonl is a macro +// that leaks into this translation unit and collides with the firmware's +// function definition, breaking the local build. The firmware inlines are a +// trivial, obviously-correct byte swap, so using the host's here costs no +// meaningful coverage while keeping the build portable across macOS and the +// Linux CI runner.) +#include +#ifndef HAVE_ARPA_INET_H +#define HAVE_ARPA_INET_H 1 +#endif + +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; + +typedef int8_t s8; +typedef int16_t s16; +typedef int32_t s32; +typedef int64_t s64; + +typedef bool boolean; +#ifndef TRUE +#define TRUE true +#endif +#ifndef FALSE +#define FALSE false +#endif + +#endif diff --git a/integration-tests/harness/stubs/circle/usb/gadget/dwusbgadget.h b/integration-tests/harness/stubs/circle/usb/gadget/dwusbgadget.h new file mode 100644 index 00000000..3e291e16 --- /dev/null +++ b/integration-tests/harness/stubs/circle/usb/gadget/dwusbgadget.h @@ -0,0 +1,43 @@ +// +// Host-build stub for . +// Declares just enough of CDWUSBGadget for CUSBCDGadget to derive from it. +// None of the DWC controller machinery exists here; the test bench drives +// the gadget's callbacks directly. +// +#ifndef _circle_usb_gadget_dwusbgadget_h +#define _circle_usb_gadget_dwusbgadget_h + +#include +#include +#include + +#ifndef USB_GADGET_VENDOR_ID +#define USB_GADGET_VENDOR_ID 0x1d6b +#endif + +enum TDeviceSpeed +{ + FullSpeed, + HighSpeed, + DeviceSpeedUnknown +}; + +class CDWUSBGadget +{ +public: + CDWUSBGadget(CInterruptSystem *pInterruptSystem, TDeviceSpeed DeviceSpeed); + virtual ~CDWUSBGadget(void); + +protected: + virtual const void *GetDescriptor(u16 wValue, u16 wIndex, size_t *pLength) = 0; + virtual void AddEndpoints(void) = 0; + virtual void CreateDevice(void) = 0; + virtual void OnSuspend(void) {} + virtual int OnClassOrVendorRequest(const TSetupData *pSetupData, u8 *pData) { return -1; } + virtual void OnNegotiatedSpeed(TDeviceSpeed Speed) {} + +private: + TDeviceSpeed m_DeviceSpeed; +}; + +#endif diff --git a/integration-tests/harness/stubs/circle/usb/gadget/dwusbgadgetendpoint.h b/integration-tests/harness/stubs/circle/usb/gadget/dwusbgadgetendpoint.h new file mode 100644 index 00000000..12ba78bb --- /dev/null +++ b/integration-tests/harness/stubs/circle/usb/gadget/dwusbgadgetendpoint.h @@ -0,0 +1,54 @@ +// +// Host-build stub for . +// BeginTransfer()/Stall() record their arguments in the test bus sink +// (see harness/stubs.cpp) instead of touching hardware. The test bench +// inspects the sink and calls OnTransferComplete() to emulate the host. +// +#ifndef _circle_usb_gadget_dwusbgadgetendpoint_h +#define _circle_usb_gadget_dwusbgadgetendpoint_h + +#include +#include + +class CDWUSBGadget; + +class CDWUSBGadgetEndpoint +{ +public: + // Enumerator order/values mirror Circle's real header so the numeric + // values match the device build (the firmware only names these + // symbolically, but keeping them identical avoids any drift). + enum TTransferMode + { + TransferSetupOut, + TransferDataOut, + TransferDataIn, + TransferUnknown + }; + + enum TDirection + { + DirectionOut, + DirectionIn, + DirectionInOut + }; + + CDWUSBGadgetEndpoint(const TUSBEndpointDescriptor *pDesc, CDWUSBGadget *pGadget); + virtual ~CDWUSBGadgetEndpoint(void); + + virtual void OnActivate(void) = 0; + virtual void OnDeactivate(void) = 0; + virtual void OnTransferComplete(boolean bIn, size_t nLength) = 0; + + void BeginTransfer(TTransferMode Mode, void *pBuffer, size_t nLength); + void Stall(boolean bIn); + void SetMaxPacketSize(size_t nSize); + + TDirection GetDirection(void) const { return m_Direction; } + +private: + TDirection m_Direction; + size_t m_nMaxPacketSize; +}; + +#endif diff --git a/integration-tests/harness/stubs/circle/usb/usb.h b/integration-tests/harness/stubs/circle/usb/usb.h new file mode 100644 index 00000000..4a87d762 --- /dev/null +++ b/integration-tests/harness/stubs/circle/usb/usb.h @@ -0,0 +1,80 @@ +// +// Host-build stub for . +// Only the descriptor structures and constants USBODE's gadget code +// references. Layouts match Circle's originals byte for byte. +// +#ifndef _circle_usb_usb_h +#define _circle_usb_usb_h + +#include +#include + +#define DESCRIPTOR_DEVICE 1 +#define DESCRIPTOR_CONFIGURATION 2 +#define DESCRIPTOR_STRING 3 +#define DESCRIPTOR_INTERFACE 4 +#define DESCRIPTOR_ENDPOINT 5 + +struct TSetupData +{ + u8 bmRequestType; + u8 bRequest; + u16 wValue; + u16 wIndex; + u16 wLength; +} PACKED; + +struct TUSBDeviceDescriptor +{ + u8 bLength; + u8 bDescriptorType; + u16 bcdUSB; + u8 bDeviceClass; + u8 bDeviceSubClass; + u8 bDeviceProtocol; + u8 bMaxPacketSize0; + u16 idVendor; + u16 idProduct; + u16 bcdDevice; + u8 iManufacturer; + u8 iProduct; + u8 iSerialNumber; + u8 bNumConfigurations; +} PACKED; + +struct TUSBConfigurationDescriptor +{ + u8 bLength; + u8 bDescriptorType; + u16 wTotalLength; + u8 bNumInterfaces; + u8 bConfigurationValue; + u8 iConfiguration; + u8 bmAttributes; + u8 bMaxPower; +} PACKED; + +struct TUSBInterfaceDescriptor +{ + u8 bLength; + u8 bDescriptorType; + u8 bInterfaceNumber; + u8 bAlternateSetting; + u8 bNumEndpoints; + u8 bInterfaceClass; + u8 bInterfaceSubClass; + u8 bInterfaceProtocol; + u8 iInterface; +} PACKED; + +struct TUSBEndpointDescriptor +{ + u8 bLength; + u8 bDescriptorType; + u8 bEndpointAddress; + u8 bmAttributes; + u16 wMaxPacketSize; + u8 bInterval; +} PACKED; + +#endif diff --git a/integration-tests/harness/stubs/circle/util.h b/integration-tests/harness/stubs/circle/util.h new file mode 100644 index 00000000..77b2807d --- /dev/null +++ b/integration-tests/harness/stubs/circle/util.h @@ -0,0 +1,13 @@ +// +// Host-build stub for . +// On the device Circle provides the mem/str functions itself; on the host +// they come from the C library. +// +#ifndef _circle_util_h +#define _circle_util_h + +#include +#include +#include + +#endif diff --git a/integration-tests/harness/stubs/configservice/configservice.h b/integration-tests/harness/stubs/configservice/configservice.h new file mode 100644 index 00000000..ee2d11a9 --- /dev/null +++ b/integration-tests/harness/stubs/configservice/configservice.h @@ -0,0 +1,49 @@ +// +// Host-build stub for . +// USBTargetOS matches the real enum. The fake service returns +// test-presettable values; if no "configservice" task is registered with +// the scheduler stub, the gadget falls back to its built-in defaults +// (debug logging off, target OS DosWin), same as on the device. +// +#ifndef _configservice_configservice_h +#define _configservice_configservice_h + +#include +#include + +enum class USBTargetOS : unsigned +{ + DosWin = 0, + Apple = 1, + Unknown = 255 +}; + +class ConfigService : public CTask +{ +public: + ConfigService(void) {} + + static ConfigService *Get(void) { return s_pThis; } + + unsigned GetProperty(const char *pName, unsigned defaultValue) + { + if (debugCdrom && pName != nullptr) + { + return 1U; + } + return defaultValue; + } + + USBTargetOS GetUSBTargetOS(USBTargetOS defaultValue = USBTargetOS::DosWin) + { + return targetOS; + } + + // Test-presettable values + USBTargetOS targetOS = USBTargetOS::DosWin; + bool debugCdrom = false; + + static ConfigService *s_pThis; +}; + +#endif diff --git a/integration-tests/harness/stubs/fatfs/ff.h b/integration-tests/harness/stubs/fatfs/ff.h new file mode 100644 index 00000000..61bda71f --- /dev/null +++ b/integration-tests/harness/stubs/fatfs/ff.h @@ -0,0 +1,129 @@ +// +// Host-build stub for . +// +// This is NOT a reimplementation of FatFs. It is a thin seam that lets the +// REAL disc-image readers (addon/discimage/cuebinfile.cpp, mdsfile.cpp, +// util.cpp) open image files on the build machine. Every f_* entry point +// here is backed by host stdio (fopen/fread/fseek) in fatfs_host.cpp. The +// readers' own logic (cache windows, per-track sector math, LBA translation) +// is compiled and exercised unchanged; only the raw file access is retargeted +// off the Pi's SD card onto the host filesystem. +// +#ifndef _fatfs_ff_h +#define _fatfs_ff_h + +#include +#include + +typedef unsigned char BYTE; +typedef unsigned short WORD; +typedef unsigned int UINT; +// DWORD is also typedef'd (to u32) by the scsitbservice stub that gets pulled +// in transitively; keep this identical (unsigned int == uint32_t on the LP64 +// hosts we build on) so the redefinition is allowed rather than a conflict. +typedef unsigned int DWORD; +typedef uint64_t QWORD; +typedef uint64_t FSIZE_t; +typedef uint64_t LBA_t; +typedef char TCHAR; + +// Subset of the real FRESULT codes actually referenced by the readers. +typedef enum { + FR_OK = 0, + FR_DISK_ERR, + FR_INT_ERR, + FR_NOT_READY, + FR_NO_FILE, + FR_NO_PATH, + FR_INVALID_NAME, + FR_DENIED, + FR_EXIST, + FR_INVALID_OBJECT, + FR_WRITE_PROTECTED, + FR_INVALID_DRIVE, + FR_NOT_ENABLED, + FR_NO_FILESYSTEM, + FR_MKFS_ABORTED, + FR_TIMEOUT, + FR_LOCKED, + FR_NOT_ENOUGH_CORE, + FR_TOO_MANY_OPEN_FILES, + FR_INVALID_PARAMETER +} FRESULT; + +// Object identifier: the readers only ever read obj.objsize (via f_size()). +typedef struct { + FSIZE_t objsize; +} FFOBJID; + +// Directory attribute bits (only AM_DIR is referenced). +#define AM_RDO 0x01 +#define AM_HID 0x02 +#define AM_SYS 0x04 +#define AM_DIR 0x10 +#define AM_ARC 0x20 + +// Directory-entry info. Present so mdsfile.cpp's companion-file scan compiles +// and links; MDS is not exercised by the tests, so f_opendir/f_readdir below +// are not backed by a real host directory walk. +typedef struct { + FSIZE_t fsize; + WORD fdate; + WORD ftime; + BYTE fattrib; + TCHAR fname[256]; +} FILINFO; + +typedef struct { + void* host_dir; +} DIR; + +// File object. Layout is deliberately minimal: it carries just the fields the +// real readers touch (obj.objsize via f_size, fptr via f_tell, and cltbl for +// the fast-seek optimizer) plus a private host FILE* handle. +typedef struct { + FFOBJID obj; // object.objsize -> file size (f_size) + FSIZE_t fptr; // current file pointer (f_tell) + DWORD* cltbl; // fast-seek cluster link map (set by FatFsOptimizer) + void* host_fp; // backing host FILE* (opaque to firmware code) +} FIL; + +// Access-mode flags (values match real FatFs; only FA_READ is used here). +#define FA_READ 0x01 +#define FA_WRITE 0x02 +#define FA_OPEN_EXISTING 0x00 +#define FA_CREATE_NEW 0x04 +#define FA_CREATE_ALWAYS 0x08 +#define FA_OPEN_ALWAYS 0x10 +#define FA_OPEN_APPEND 0x30 + +// Special f_lseek() offset that asks FatFs to build the fast-seek link map. +// On the host there is no FAT, so fatfs_host.cpp treats this as a successful +// no-op and the readers proceed with ordinary seeks. +#define CREATE_LINKMAP ((FSIZE_t)0 - 1) + +#ifdef __cplusplus +extern "C" { +#endif + +FRESULT f_open (FIL* fp, const TCHAR* path, BYTE mode); +FRESULT f_close (FIL* fp); +FRESULT f_read (FIL* fp, void* buff, UINT btr, UINT* br); +FRESULT f_write (FIL* fp, const void* buff, UINT btw, UINT* bw); +FRESULT f_lseek (FIL* fp, FSIZE_t ofs); + +// Directory walk: link-only stubs for mdsfile.cpp (MDS is not under test). +FRESULT f_opendir (DIR* dp, const TCHAR* path); +FRESULT f_readdir (DIR* dp, FILINFO* fno); +FRESULT f_closedir (DIR* dp); + +#ifdef __cplusplus +} +#endif + +#define f_tell(fp) ((fp)->fptr) +#define f_size(fp) ((fp)->obj.objsize) +#define f_eof(fp) ((int)((fp)->fptr == (fp)->obj.objsize)) +#define f_rewind(fp) f_lseek((fp), 0) + +#endif diff --git a/integration-tests/harness/stubs/linux/kernel.h b/integration-tests/harness/stubs/linux/kernel.h new file mode 100644 index 00000000..ebe999ec --- /dev/null +++ b/integration-tests/harness/stubs/linux/kernel.h @@ -0,0 +1,22 @@ +// +// Host-build stub for . +// The disc-image readers include this transitively but use only offsetof/ +// container_of and the printf family, all provided by the host libc. +// +#ifndef _linux_kernel_h +#define _linux_kernel_h + +#include +#include +#include + +#ifndef offsetof +#define offsetof(type, member) __builtin_offsetof(type, member) +#endif + +#ifndef container_of +#define container_of(ptr, type, member) \ + ((type*)((char*)(ptr) - offsetof(type, member))) +#endif + +#endif diff --git a/integration-tests/harness/stubs/scsitbservice/scsitbservice.h b/integration-tests/harness/stubs/scsitbservice/scsitbservice.h new file mode 100644 index 00000000..09732477 --- /dev/null +++ b/integration-tests/harness/stubs/scsitbservice/scsitbservice.h @@ -0,0 +1,52 @@ +// +// Host-build stub for . +// Fake file catalog for the vendor toolbox commands (0xD0/0xD2/0xD8/0xD9). +// +#ifndef _scsitbservice_scsitbservice_h +#define _scsitbservice_scsitbservice_h + +#include +#include + +#include +#include + +typedef u32 DWORD; + +class SCSITBService : public CTask +{ +public: + struct Entry + { + std::string name; + DWORD size; + }; + + SCSITBService(void) {} + + size_t GetCount() const { return entries.size(); } + + const char *GetName(size_t index) const + { + return index < entries.size() ? entries[index].name.c_str() : ""; + } + + DWORD GetSize(size_t index) const + { + return index < entries.size() ? entries[index].size : 0; + } + + bool SetNextCD(size_t index) + { + lastSetNextCD = (int)index; + setNextCDCalls++; + return true; + } + + // Test-visible state + std::vector entries; + int lastSetNextCD = -1; + int setNextCDCalls = 0; +}; + +#endif diff --git a/integration-tests/harness/stubs/tracelab/tracelab.h b/integration-tests/harness/stubs/tracelab/tracelab.h new file mode 100644 index 00000000..bfa268e6 --- /dev/null +++ b/integration-tests/harness/stubs/tracelab/tracelab.h @@ -0,0 +1,38 @@ +// +// Host-build stub for . +// All trace calls are no-ops; Get() always returns a valid singleton, like +// on the device after CUSBCDGadget's constructor runs. +// +#ifndef _tracelab_tracelab_h +#define _tracelab_tracelab_h + +#include + +class CTraceLab +{ +public: + CTraceLab(void) {} + + static CTraceLab *Get(void) + { + static CTraceLab instance; + return &instance; + } + + boolean Initialize(void) { return TRUE; } + + void TraceCDBReceived(u8 lun, const u8 *pCDB, u8 nCDBLength) {} + void TraceCommandComplete(u8 opcode, u8 status, u32 residue) {} + void TraceSenseSet(u8 senseKey, u8 asc, u8 ascq) {} + void TraceMediaState(u8 fromState, u8 toState) {} + void TraceUSBSuspend(void) {} + void TraceUSBActivate(void) {} + void TraceUSBSpeed(boolean bFullSpeed) {} + void TraceImageReadStart(u32 lba, u32 bytes) {} + void TraceImageReadComplete(u32 lba, u32 bytesRead) {} + void TraceImageReadError(u32 lba, u32 bytes) {} + void TraceTransferStart(u32 bytes) {} + void TraceTransferComplete(u32 bytes) {} +}; + +#endif diff --git a/integration-tests/harness/testbus.h b/integration-tests/harness/testbus.h new file mode 100644 index 00000000..28862ac6 --- /dev/null +++ b/integration-tests/harness/testbus.h @@ -0,0 +1,40 @@ +// +// testbus.h +// +// Records what the gadget asks the (nonexistent) USB controller to do. +// CDWUSBGadgetEndpoint::BeginTransfer()/Stall() in the stub layer write +// here; CGadgetTestBench reads it and completes transfers the way a real +// host would. +// +#ifndef _test_host_testbus_h +#define _test_host_testbus_h + +#include + +struct TestBus +{ + struct Pending + { + bool valid = false; + void *buffer = nullptr; + size_t length = 0; + }; + + Pending inTransfer; // device -> host (data phase or CSW) + Pending outTransfer; // host -> device (CBW or data phase) + + bool inStalled = false; + bool outStalled = false; + + static TestBus &Get(); + + void Reset() + { + inTransfer = Pending(); + outTransfer = Pending(); + inStalled = false; + outStalled = false; + } +}; + +#endif diff --git a/integration-tests/test-suite/test_audio.cpp b/integration-tests/test-suite/test_audio.cpp new file mode 100644 index 00000000..2706c14a --- /dev/null +++ b/integration-tests/test-suite/test_audio.cpp @@ -0,0 +1,162 @@ +// +// test_audio.cpp +// +// The analog CD audio command set: PLAY AUDIO (MSF/10), READ SUB-CHANNEL +// position polling, PAUSE/RESUME, SEEK. This is the exact sequence retail +// Win98 SE's MCICDA driver issues (Trace Lab golden capture) and the +// sequence Win98 QuickInstall's replacement USB stack never sends +// (oerg866/win98-quickinstall#151). +// +#include "bench.h" +#include "framework.h" + +TEST(play_audio_msf_reaches_player) +{ + CFakeImageDevice *disc = MakeAudioCD(3, 3000); + CCDPlayer player; + CGadgetTestBench bench(disc, false, &player); + bench.Activate(); + bench.RequestSense(); + + // Play track 2: LBA 3000..6000 -> MSF 00:42:00 .. 01:22:00. + const u8 cdb[10] = {0x47, 0x00, 0x00, 0x00, 42, 0x00, 0x01, 22, 0x00, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 0); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(player.playCalls, 1); + CHECK_EQ(player.lastPlayLBA, 3000u); + CHECK_EQ(player.lastPlayBlocks, 3000u); +} + +TEST(play_audio_10_reaches_player) +{ + CFakeImageDevice *disc = MakeAudioCD(3, 3000); + CCDPlayer player; + CGadgetTestBench bench(disc, false, &player); + bench.Activate(); + bench.RequestSense(); + + // PLAY AUDIO(10): LBA 3000, 500 blocks. + const u8 cdb[10] = {0x45, 0x00, 0x00, 0x00, 0x0B, 0xB8, 0x00, 0x01, 0xF4, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 0); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(player.playCalls, 1); + CHECK_EQ(player.lastPlayLBA, 3000u); + CHECK_EQ(player.lastPlayBlocks, 500u); +} + +TEST(play_audio_on_data_track_fails) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + CCDPlayer player; + CGadgetTestBench bench(disc, false, &player); + bench.Activate(); + bench.RequestSense(); + + const u8 cdb[10] = {0x45, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x40, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 0); + + CHECK_EQ(r.csw.bmCSWStatus, 1); + CHECK_EQ(player.playCalls, 0); + + auto sense = bench.RequestSense(); + CHECK_EQ(sense.data[2], 0x05); + CHECK_EQ(sense.data[12], 0x64); // ILLEGAL MODE FOR THIS TRACK +} + +TEST(read_subchannel_position_playing) +{ + CFakeImageDevice *disc = MakeAudioCD(3, 3000); + CCDPlayer player; + CGadgetTestBench bench(disc, false, &player); + bench.Activate(); + bench.RequestSense(); + + // Pretend the player is 1500 sectors into track 2. + player.state = CCDPlayer::PLAYING; + player.currentAddress = 4500; + + // READ SUB-CHANNEL, MSF, current position, alloc 16 — Win98 polls + // this every ~200 ms while the CD Player window is open. + const u8 cdb[10] = {0x42, 0x02, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00, 16, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 16); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + const u8 expected[16] = { + 0x00, 0x11, // audio status: playing + 0x00, 0x0C, // 12 bytes of position data follow + 0x01, // format: current position + 0x10, // ADR 1, control: audio + 0x02, // track 2 + 0x01, // index 1 + 0x00, 0x01, 0x02, 0x00, // absolute: MSF 01:02:00 (LBA 4500 + pregap) + 0x00, 0x00, 0x14, 0x00, // relative: MSF 00:20:00 (1500 into track) + }; + CHECK_BYTES(r.data.data(), r.data.size(), expected, sizeof(expected)); +} + +TEST(read_subchannel_status_paused) +{ + CFakeImageDevice *disc = MakeAudioCD(3, 3000); + CCDPlayer player; + CGadgetTestBench bench(disc, false, &player); + bench.Activate(); + bench.RequestSense(); + + player.state = CCDPlayer::PAUSED; + player.currentAddress = 0; + + const u8 cdb[10] = {0x42, 0x02, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00, 16, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 16); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(r.data[1], 0x12); // paused +} + +TEST(pause_resume_and_seek) +{ + CFakeImageDevice *disc = MakeAudioCD(3, 3000); + CCDPlayer player; + CGadgetTestBench bench(disc, false, &player); + bench.Activate(); + bench.RequestSense(); + + // PAUSE (resume bit clear) + const u8 pause[10] = {0x4B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + auto r = bench.SendCommand(pause, sizeof(pause), 0); + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(player.pauseCalls, 1); + + // RESUME + const u8 resume[10] = {0x4B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00}; + r = bench.SendCommand(resume, sizeof(resume), 0); + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(player.resumeCalls, 1); + + // SEEK to LBA 6000 (track 3) + const u8 seek[10] = {0x2B, 0x00, 0x00, 0x00, 0x17, 0x70, 0x00, 0x00, 0x00, 0x00}; + r = bench.SendCommand(seek, sizeof(seek), 0); + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(player.seekCalls, 1); + CHECK_EQ(player.lastSeekLBA, 6000u); +} + +TEST(read_disc_information_audio) +{ + CFakeImageDevice *disc = MakeAudioCD(3, 3000); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + const u8 cdb[10] = {0x51, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 34, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 34); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(r.data.size(), (size_t)34); + CHECK_EQ(r.data[2], 0x0E); // complete, finalized + CHECK_EQ(r.data[3], 0x01); // first track + CHECK_EQ(r.data[4], 0x01); // one session + CHECK_EQ(r.data[6], 0x03); // last track in last session + CHECK_EQ(r.data[8], 0x00); // disc type: CD-DA +} diff --git a/integration-tests/test-suite/test_basics.cpp b/integration-tests/test-suite/test_basics.cpp new file mode 100644 index 00000000..7764c601 --- /dev/null +++ b/integration-tests/test-suite/test_basics.cpp @@ -0,0 +1,148 @@ +// +// test_basics.cpp +// +// INQUIRY, TEST UNIT READY / unit attention flow, REQUEST SENSE, +// READ CAPACITY, unknown opcodes, and BOT protocol basics (stall before +// CSW on CHECK CONDITION with a data phase, residue accounting). +// +#include "bench.h" +#include "framework.h" + +// Struct layouts must match the device exactly for byte-level tests to +// mean anything. +static_assert(sizeof(TUSBCDCBW) == 31, "CBW must be 31 bytes"); +static_assert(sizeof(TUSBCDCSW) == 13, "CSW must be 13 bytes"); +static_assert(sizeof(TUSBCDInquiryReply) == 96, "INQUIRY reply must be 96 bytes"); +static_assert(sizeof(ModePage0x2AData) == 68, "mode page 0x2A must be 68 bytes"); +static_assert(sizeof(ModeSense10Header) == 8, "MODE SENSE(10) header must be 8 bytes"); + +TEST(inquiry_standard) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc); + bench.Activate(); + + // INQUIRY must work even while unit attention is pending. + const u8 cdb[6] = {0x12, 0x00, 0x00, 0x00, 36, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 36); + + CHECK(r.gotCSW); + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(r.csw.dCSWDataResidue, 0u); + CHECK_EQ(r.data.size(), (size_t)36); + CHECK_EQ(r.data[0], 0x05); // CD/DVD device + CHECK_EQ(r.data[1], 0x80); // removable + CHECK_BYTES(r.data.data() + 8, 8, "USBODE ", 8); + CHECK_BYTES(r.data.data() + 16, 16, "CDROM EMULATOR ", 16); +} + +TEST(unit_attention_flow) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc); + bench.Activate(); + + // 1. TEST UNIT READY under unit attention: CHECK CONDITION 06/28/00. + const u8 tur[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + auto r = bench.SendCommand(tur, sizeof(tur), 0); + CHECK(r.gotCSW); + CHECK_EQ(r.csw.bmCSWStatus, 1); + CHECK(!r.stalledIn); // no data phase expected -> no stall + + // 2. REQUEST SENSE reports 06/28/00 (medium changed) and clears it. + auto sense = bench.RequestSense(); + CHECK_EQ(sense.csw.bmCSWStatus, 0); + CHECK_EQ(sense.data.size(), (size_t)18); + CHECK_EQ(sense.data[0], 0x70); // current error, fixed format + CHECK_EQ(sense.data[2], 0x06); // UNIT ATTENTION + CHECK_EQ(sense.data[12], 0x28); // ASC: medium may have changed + CHECK_EQ(sense.data[13], 0x00); + + // 3. TEST UNIT READY now succeeds. + r = bench.SendCommand(tur, sizeof(tur), 0); + CHECK_EQ(r.csw.bmCSWStatus, 0); +} + +TEST(read_blocked_by_unit_attention) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc); + bench.Activate(); + + // READ(10) is on the blocked list while unit attention is pending; + // the data phase must be stalled before the failing CSW (BOT 6.7.2). + const u8 read10[10] = {0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00}; + auto r = bench.SendCommand(read10, sizeof(read10), 2048); + CHECK(r.gotCSW); + CHECK_EQ(r.csw.bmCSWStatus, 1); + CHECK(r.stalledIn); + CHECK_EQ(r.csw.dCSWDataResidue, 2048u); // nothing was transferred + CHECK_EQ(r.data.size(), (size_t)0); +} + +TEST(read_capacity) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); // clear unit attention + + const u8 cdb[10] = {0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 8); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + // Last LBA = leadout - 1 = 1199 = 0x04AF, block size 2048, big-endian. + const u8 expected[8] = {0x00, 0x00, 0x04, 0xAF, 0x00, 0x00, 0x08, 0x00}; + CHECK_BYTES(r.data.data(), r.data.size(), expected, sizeof(expected)); +} + +TEST(unknown_opcode) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + const u8 cdb[6] = {0xEE, 0x00, 0x00, 0x00, 0x00, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 0); + CHECK_EQ(r.csw.bmCSWStatus, 1); + + auto sense = bench.RequestSense(); + CHECK_EQ(sense.data[2], 0x05); // ILLEGAL REQUEST + CHECK_EQ(sense.data[12], 0x20); // INVALID COMMAND OPERATION CODE +} + +TEST(get_configuration_cd_profile) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + const u8 cdb[10] = {0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 256); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + // Full feature set for CD media (profile list + core + morphing + + // removable medium + random readable + multi-read + CD read + power + // management + analog audio play + real-time streaming) is 88 bytes; + // the header reports 84 (total minus its own length field). + CHECK_EQ(r.data.size(), (size_t)88); + const u8 header[8] = {0x00, 0x00, 0x00, 84, 0x00, 0x00, 0x00, 0x08}; + CHECK_BYTES(r.data.data(), 8, header, 8); +} + +TEST(toolbox_set_next_cd) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + SCSITBService tbservice; + CGadgetTestBench bench(disc, false, nullptr, nullptr, &tbservice); + bench.Activate(); + bench.RequestSense(); + + const u8 cdb[10] = {0xD8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 0); + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(tbservice.setNextCDCalls, 1); + CHECK_EQ(tbservice.lastSetNextCD, 3); +} diff --git a/integration-tests/test-suite/test_modesense.cpp b/integration-tests/test-suite/test_modesense.cpp new file mode 100644 index 00000000..22e69f6c --- /dev/null +++ b/integration-tests/test-suite/test_modesense.cpp @@ -0,0 +1,181 @@ +// +// test_modesense.cpp +// +// MODE SENSE(6)/(10) and MODE SELECT(10). Locks in the two Win9x CD-audio +// regressions fixed after 3.2.0: +// - medium type byte reflects the actual disc (issue #164: hardcoded +// 0x13 made Win98 MCICDA treat every disc as data-only) +// - MODE SENSE(10) responses are padded to the allocation length +// (Win9x usbstor.sys rejects short data phases and retries forever) +// +#include "bench.h" +#include "framework.h" + +TEST(mode_sense10_medium_type_data_cd) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + const u8 cdb[10] = {0x5A, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 128, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 128); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(r.data[2], 0x01); // medium type: data CD + + // Header + page 0x2A = 8 + 68 = 76 real bytes; mode data length + // reports the true length... + CHECK_EQ(r.data[0], 0x00); + CHECK_EQ(r.data[1], 74); // 76 - 2 + // ...but the data phase is padded to the allocation length. + CHECK_EQ(r.data.size(), (size_t)128); + CHECK_EQ(r.csw.dCSWDataResidue, 0u); + for (size_t i = 76; i < 128; i++) + { + CHECK_EQ(r.data[i], 0x00); + } + + // Page 0x2A starts after the 8-byte header. + CHECK_EQ(r.data[8], 0x2A); + CHECK_EQ(r.data[9], 0x42); + CHECK_EQ(r.data[10], 0x07); // CD media capability byte +} + +TEST(mode_sense10_medium_type_audio_cd) +{ + CFakeImageDevice *disc = MakeAudioCD(3, 3000); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + const u8 cdb[10] = {0x5A, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 128, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 128); + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(r.data[2], 0x02); // medium type: audio CD +} + +TEST(mode_sense10_medium_type_mixed_cd) +{ + CFakeImageDevice *disc = MakeMixedModeCD(1000, 2, 2000); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + const u8 cdb[10] = {0x5A, 0x00, 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 128, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 128); + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(r.data[2], 0x03); // medium type: mixed data+audio +} + +TEST(mode_sense10_page0e_win98_golden) +{ + // Exact request retail Win98 SE sends before playing audio (from the + // Trace Lab golden capture): MODE SENSE(10) page 0x0E, alloc 0x18. + // The full 24-byte response below is what a working Win98 SE host + // accepted right before issuing PLAY AUDIO MSF. + CFakeImageDevice *disc = MakeAudioCD(3, 3000); + CCDPlayer player; + CGadgetTestBench bench(disc, false, &player); + bench.Activate(); + bench.RequestSense(); + + const u8 cdb[10] = {0x5A, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 0x18); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + const u8 expected[24] = { + 0x00, 0x16, // mode data length 22 + 0x02, // medium type: audio CD + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0E, 0x0E, // page 0x0E, length 14 + 0x04, // IMMED + 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xFF, // output 0 -> channel 0, max volume + 0x02, 0xFF, // output 1 -> channel 1, max volume + 0x00, 0x00, 0x00, 0x00, + }; + CHECK_BYTES(r.data.data(), r.data.size(), expected, sizeof(expected)); + CHECK_EQ(r.csw.dCSWDataResidue, 0u); +} + +TEST(mode_sense10_unsupported_page) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + const u8 cdb[10] = {0x5A, 0x00, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 128, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 128); + + CHECK_EQ(r.csw.bmCSWStatus, 1); + CHECK(r.stalledIn); + auto sense = bench.RequestSense(); + CHECK_EQ(sense.data[2], 0x05); + CHECK_EQ(sense.data[12], 0x24); // INVALID FIELD IN CDB +} + +TEST(mode_sense10_saved_values_unsupported) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + // Page control 0b11 = saved values. + const u8 cdb[10] = {0x5A, 0x00, (u8)(0xC0 | 0x2A), 0x00, 0x00, 0x00, 0x00, 0x00, 128, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 128); + + CHECK_EQ(r.csw.bmCSWStatus, 1); + auto sense = bench.RequestSense(); + CHECK_EQ(sense.data[2], 0x05); + CHECK_EQ(sense.data[12], 0x39); // SAVING PARAMETERS NOT SUPPORTED +} + +TEST(mode_sense6_no_padding) +{ + // The padding fix applies to MODE SENSE(10) only; MODE SENSE(6) + // keeps the classic short response with a nonzero residue. + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + const u8 cdb[6] = {0x1A, 0x00, 0x2A, 0x00, 128, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 128); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(r.data.size(), (size_t)72); // 4-byte header + 68-byte page + CHECK_EQ(r.data[0], 71); // mode data length + CHECK_EQ(r.data[1], 0x01); // medium type + CHECK_EQ(r.csw.dCSWDataResidue, 56u); +} + +TEST(mode_select10_sets_player_volume) +{ + // Retail Win98 writes the CD volume via MODE SELECT(10) page 0x0E + // before playing. The gadget picks the lower of the two channel + // volumes (Descent 2 quirk). + CFakeImageDevice *disc = MakeAudioCD(3, 3000); + CCDPlayer player; + CGadgetTestBench bench(disc, false, &player); + bench.Activate(); + bench.RequestSense(); + + u8 payload[24]; + memset(payload, 0, sizeof(payload)); + payload[8] = 0x0E; // page code + payload[9] = 0x0E; // page length + payload[16] = 0x01; // output 0 channel + payload[17] = 100; // output 0 volume + payload[18] = 0x02; // output 1 channel + payload[19] = 255; // output 1 volume + + const u8 cdb[10] = {0x55, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 24, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 24, false, payload, sizeof(payload)); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(player.setVolumeCalls, 1); + CHECK_EQ(player.volume, 100); +} diff --git a/integration-tests/test-suite/test_read10.cpp b/integration-tests/test-suite/test_read10.cpp new file mode 100644 index 00000000..7aa227c8 --- /dev/null +++ b/integration-tests/test-suite/test_read10.cpp @@ -0,0 +1,115 @@ +// +// test_read10.cpp +// +// READ(10) through the real DataInRead/Update() chunked-transfer path, +// including multi-chunk reads, USB 1.1 batch sizing, boundary clamping, +// and residue accounting. +// +#include "bench.h" +#include "framework.h" + +#include + +static std::vector ExpectedSectors(u32 firstLBA, u32 count) +{ + std::vector expected((size_t)count * 2048); + for (u32 i = 0; i < count; i++) + { + FillPatternSector(expected.data() + (size_t)i * 2048, firstLBA + i, 2048); + } + return expected; +} + +static CGadgetTestBench::Result Read10(CGadgetTestBench &bench, u32 lba, u16 blocks) +{ + const u8 cdb[10] = {0x28, 0x00, + (u8)(lba >> 24), (u8)(lba >> 16), (u8)(lba >> 8), (u8)lba, + 0x00, (u8)(blocks >> 8), (u8)blocks, 0x00}; + return bench.SendCommand(cdb, sizeof(cdb), (u32)blocks * 2048); +} + +TEST(read10_single_chunk) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + auto r = Read10(bench, 2, 4); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(r.csw.dCSWDataResidue, 0u); + CHECK_EQ(r.dataChunks, 1); + auto expected = ExpectedSectors(2, 4); + CHECK_BYTES(r.data.data(), r.data.size(), expected.data(), expected.size()); +} + +TEST(read10_multi_chunk_high_speed) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + // 64 blocks > the 32-block high-speed batch: two Update() rounds. + auto r = Read10(bench, 0, 64); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(r.csw.dCSWDataResidue, 0u); + CHECK_EQ(r.dataChunks, 2); + auto expected = ExpectedSectors(0, 64); + CHECK_BYTES(r.data.data(), r.data.size(), expected.data(), expected.size()); +} + +TEST(read10_multi_chunk_full_speed) +{ + // USB 1.1 (UHCI/OHCI hosts, and the Win98 target) batches 16 blocks. + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc, true /* full speed */); + bench.Activate(); + bench.RequestSense(); + + auto r = Read10(bench, 0, 64); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(r.csw.dCSWDataResidue, 0u); + CHECK_EQ(r.dataChunks, 4); + auto expected = ExpectedSectors(0, 64); + CHECK_BYTES(r.data.data(), r.data.size(), expected.data(), expected.size()); +} + +TEST(read10_beyond_end_rejected) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + auto r = Read10(bench, 1300, 1); + + CHECK_EQ(r.csw.bmCSWStatus, 1); + CHECK(r.stalledIn); + CHECK_EQ(r.csw.dCSWDataResidue, 2048u); + + auto sense = bench.RequestSense(); + CHECK_EQ(sense.data[2], 0x05); + CHECK_EQ(sense.data[12], 0x21); // LBA OUT OF RANGE +} + +TEST(read10_truncated_at_disc_end) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + // 4 blocks requested, only 2 exist: transfer 2, report the shortfall + // in the residue. + auto r = Read10(bench, 1198, 4); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(r.data.size(), (size_t)2 * 2048); + CHECK_EQ(r.csw.dCSWDataResidue, 2u * 2048); + auto expected = ExpectedSectors(1198, 2); + CHECK_BYTES(r.data.data(), r.data.size(), expected.data(), expected.size()); +} diff --git a/integration-tests/test-suite/test_readtoc.cpp b/integration-tests/test-suite/test_readtoc.cpp new file mode 100644 index 00000000..bde1adbd --- /dev/null +++ b/integration-tests/test-suite/test_readtoc.cpp @@ -0,0 +1,168 @@ +// +// test_readtoc.cpp +// +// READ TOC in every format Win9x and modern hosts use, including the two +// vendor/legacy CDB[9] encodings that shipped as Win98 fixes in 3.2.x. +// +#include "bench.h" +#include "framework.h" + +TEST(read_toc_format0_lba) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + const u8 cdb[10] = {0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 100, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 100); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + // 1 track + leadout: header(4) + 2 descriptors(8) = 20 bytes. + const u8 expected[20] = { + 0x00, 0x12, // TOC length = 18 + 0x01, 0x01, // first/last track + 0x00, 0x14, 0x01, 0x00, // track 1: data track, ADR 1 + 0x00, 0x00, 0x00, 0x00, // LBA 0 + 0x00, 0x14, 0xAA, 0x00, // leadout + 0x00, 0x00, 0x04, 0xB0, // LBA 1200 + }; + CHECK_BYTES(r.data.data(), r.data.size(), expected, sizeof(expected)); + + // Host asked for 100, got 20 -> residue 80. Win98's usbstor.sys + // discards short responses whose CSW claims residue 0. + CHECK_EQ(r.csw.dCSWDataResidue, 80u); +} + +TEST(read_toc_format0_msf) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + const u8 cdb[10] = {0x43, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 100, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 100); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + const u8 expected[20] = { + 0x00, 0x12, + 0x01, 0x01, + 0x00, 0x14, 0x01, 0x00, + 0x00, 0x00, 0x02, 0x00, // LBA 0 -> MSF 00:02:00 + 0x00, 0x14, 0xAA, 0x00, + 0x00, 0x00, 0x12, 0x00, // LBA 1200 -> MSF 00:18:00 + }; + CHECK_BYTES(r.data.data(), r.data.size(), expected, sizeof(expected)); +} + +TEST(read_toc_leadout_only) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + // Starting track 0xAA requests only the leadout descriptor. + const u8 cdb[10] = {0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0x00, 100, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 100); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + const u8 expected[12] = { + 0x00, 0x0A, + 0x01, 0x01, + 0x00, 0x14, 0xAA, 0x00, + 0x00, 0x00, 0x04, 0xB0, + }; + CHECK_BYTES(r.data.data(), r.data.size(), expected, sizeof(expected)); +} + +TEST(read_toc_legacy_cdb9_session_info) +{ + // Win9x's CD-ROM class driver encodes "session info" in CDB[9] bits + // 7-6 (old SFF-8020i/ATAPI style). Answering with a full TOC instead + // broke CD audio ("data or no disc loaded"). + CFakeImageDevice *disc = MakeAudioCD(3, 3000); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + const u8 cdb[10] = {0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 12, 0x40}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 12); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + const u8 expected[12] = { + 0x00, 0x0A, // length 10 + 0x01, 0x01, // first/last session + 0x00, 0x14, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, // first track of session at LBA 0 + }; + CHECK_BYTES(r.data.data(), r.data.size(), expected, sizeof(expected)); + CHECK_EQ(r.csw.dCSWDataResidue, 0u); +} + +TEST(read_toc_matshita_bcd_full_toc) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + // CDB[9] = 0x80: vendor extension, full TOC with BCD addresses. + const u8 cdb[10] = {0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x80}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 256); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + // A0/A1/A2 descriptors + 1 track descriptor = 37 + 11 = 48 bytes. + CHECK_EQ(r.data.size(), (size_t)48); + CHECK_EQ(r.data[12], 0x01); // A0: first track + CHECK_EQ(r.data[23], 0x01); // A1: last track + // A2: leadout LBA 1200 -> MSF 00:18:00 -> BCD 00 18 00 + CHECK_EQ(r.data[34], 0x00); + CHECK_EQ(r.data[35], 0x18); + CHECK_EQ(r.data[36], 0x00); + // Track 1 descriptor: session 1, data control, POINT 01, start MSF + // 00:02:00 in BCD. + const u8 track1[11] = {0x01, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x02, 0x00}; + CHECK_BYTES(r.data.data() + 37, 11, track1, 11); +} + +TEST(read_toc_allocation_truncation) +{ + CFakeImageDevice *disc = MakeDataISO(1200); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + const u8 cdb[10] = {0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 4, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 4); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + CHECK_EQ(r.data.size(), (size_t)4); + CHECK_EQ(r.csw.dCSWDataResidue, 0u); + const u8 expected[4] = {0x00, 0x12, 0x01, 0x01}; + CHECK_BYTES(r.data.data(), r.data.size(), expected, sizeof(expected)); +} + +TEST(read_toc_audio_control_bits) +{ + CFakeImageDevice *disc = MakeAudioCD(3, 3000); + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + const u8 cdb[10] = {0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 100, 0x00}; + auto r = bench.SendCommand(cdb, sizeof(cdb), 100); + + CHECK_EQ(r.csw.bmCSWStatus, 0); + // 3 tracks + leadout = 4 descriptors + header = 36 bytes. + CHECK_EQ(r.data.size(), (size_t)36); + CHECK_EQ(r.data[2], 0x01); + CHECK_EQ(r.data[3], 0x03); + CHECK_EQ(r.data[5], 0x10); // track 1 control: audio + CHECK_EQ(r.data[13], 0x10); // track 2 control: audio + CHECK_EQ(r.data[21], 0x10); // track 3 control: audio + CHECK_EQ(r.data[29], 0x10); // leadout inherits audio control + CHECK_EQ(r.data[30], 0xAA); +} diff --git a/integration-tests/test-suite/test_realimages.cpp b/integration-tests/test-suite/test_realimages.cpp new file mode 100644 index 00000000..7d8b47c5 --- /dev/null +++ b/integration-tests/test-suite/test_realimages.cpp @@ -0,0 +1,725 @@ +// +// test_realimages.cpp +// +// Drives the gadget with the REAL disc-image reader (addon/discimage/ +// cuebinfile.cpp) loading actual image files off disk, rather than the +// in-memory fake. This closes the gap the command-layer tests leave open: +// the cue parsing, per-track sector-size math, read-ahead cache, and file +// I/O are all real firmware code here, exercised end to end from a host +// command down to bytes read out of a file. +// +// * A real ISO9660 disc: the tracked sdcard/image.iso.gz, decompressed by +// the Makefile into USBODE_TESTDATA/image.iso. +// * Synthetic CUE/BIN pairs written to disk at run time (a pure audio CD +// and a mixed data+audio CD) whose byte content is known, so reads and +// TOC/medium-type can be checked exactly. They are real cue sheets and +// real BIN files parsed by the real reader; only the authoring is local. +// +#include "bench.h" +#include "framework.h" + +#include +#include +#ifdef WITH_CHD +#include +#endif + +#include +#include +#include + +#include +#include + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +static std::string TestDataDir() +{ +#ifdef USBODE_TESTDATA + return USBODE_TESTDATA; +#else + return "out/images"; +#endif +} + +// Deterministic byte at a given file offset, so BIN content is reproducible +// and READ(10) payloads can be checked exactly. +static u8 PatternByte(u64 fileOffset) +{ + return (u8)(fileOffset * 31u + 7u); +} + +static void WriteFileWithPattern(const std::string &path, u64 size) +{ + FILE *f = fopen(path.c_str(), "wb"); + if (!f) { + return; + } + std::vector buf(64 * 1024); + u64 written = 0; + while (written < size) { + u64 chunk = buf.size(); + if (chunk > size - written) { + chunk = size - written; + } + for (u64 i = 0; i < chunk; i++) { + buf[i] = PatternByte(written + i); + } + fwrite(buf.data(), 1, (size_t)chunk, f); + written += chunk; + } + fclose(f); +} + +static u64 FileSize(const std::string &path) +{ + struct stat st; + if (stat(path.c_str(), &st) != 0) { + return 0; + } + return (u64)st.st_size; +} + +static std::string FramesToMSF(u32 frames) +{ + u32 mm = frames / (60 * 75); + u32 rem = frames % (60 * 75); + char buf[16]; + snprintf(buf, sizeof(buf), "%02u:%02u:%02u", mm, rem / 75, rem % 75); + return buf; +} + +// Construct a real CCueBinFileDevice from a BIN path plus its cue text (pass +// an empty cue to load the file as a plain ISO). Mirrors the ~10 lines of +// util.cpp's loadCueBinIsoFileDevice, minus the format dispatch. +static CCueBinFileDevice *OpenReader(const std::string &binPath, const std::string &cueText) +{ + FIL *fp = new FIL(); + if (f_open(fp, binPath.c_str(), FA_READ) != FR_OK) { + delete fp; + return nullptr; + } + char *cue = nullptr; + if (!cueText.empty()) { + cue = new char[cueText.size() + 1]; + memcpy(cue, cueText.c_str(), cueText.size() + 1); + } + CCueBinFileDevice *dev = new CCueBinFileDevice(fp, cue, MEDIA_TYPE::CD); + delete[] cue; // the device copies it + return dev; +} + +// Load a reader from a real .cue file on disk: read the cue text back THROUGH +// the FatFs shim (f_open/f_read) rather than handing it an in-memory string, +// then open the .bin. This exercises the "cue sheet came off the filesystem" +// path the other tests skip. +static CCueBinFileDevice *OpenReaderFromCueFile(const std::string &cuePath, + const std::string &binPath) +{ + FIL cf; + if (f_open(&cf, cuePath.c_str(), FA_READ) != FR_OK) { + return nullptr; + } + std::string cueText; + char tmp[512]; + UINT br = 0; + do { + if (f_read(&cf, tmp, sizeof(tmp), &br) != FR_OK) { + f_close(&cf); + return nullptr; + } + cueText.append(tmp, br); + } while (br == sizeof(tmp)); + f_close(&cf); + return OpenReader(binPath, cueText); +} + +// --------------------------------------------------------------------------- +// Real ISO9660 disc (tracked sdcard/image.iso.gz) +// --------------------------------------------------------------------------- + +TEST(real_iso_reads_through_cuebin_reader) +{ + const std::string iso = TestDataDir() + "/image.iso"; + u64 size = FileSize(iso); + CHECK(size > 0); // Makefile should have decompressed it + if (size == 0) { + return; + } + u32 totalBlocks = (u32)(size / 2048); + + CCueBinFileDevice *disc = OpenReader(iso, ""); + CHECK(disc != nullptr); + if (!disc) { + return; + } + + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + // READ CAPACITY: last addressable LBA is one below the block count. + const u8 capCdb[10] = {0x25, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + auto cap = bench.SendCommand(capCdb, sizeof(capCdb), 8); + CHECK_EQ(cap.csw.bmCSWStatus, 0); + CHECK_EQ(cap.data.size(), (size_t)8); + u32 lastLBA = (cap.data[0] << 24) | (cap.data[1] << 16) | (cap.data[2] << 8) | cap.data[3]; + u32 blockSize = (cap.data[4] << 24) | (cap.data[5] << 16) | (cap.data[6] << 8) | cap.data[7]; + CHECK_EQ(blockSize, 2048u); + CHECK_EQ(lastLBA, totalBlocks - 1); + + // READ(10) one sector, twice: the real file read path returns a full + // sector with GOOD status, zero residue, and is stable across reads. + const u8 rdCdb[10] = {0x28, 0, 0, 0, 0, 10, 0, 0, 1, 0}; // LBA 10, 1 block + auto r1 = bench.SendCommand(rdCdb, sizeof(rdCdb), 2048); + CHECK_EQ(r1.csw.bmCSWStatus, 0); + CHECK_EQ(r1.csw.dCSWDataResidue, 0u); + CHECK_EQ(r1.data.size(), (size_t)2048); + auto r2 = bench.SendCommand(rdCdb, sizeof(rdCdb), 2048); + CHECK_BYTES(r2.data.data(), r2.data.size(), r1.data.data(), r1.data.size()); + + // Independent content oracle: LBA 16 of any ISO9660 disc is the Primary + // Volume Descriptor, whose byte 0 is 0x01 and bytes 1..5 are the "CD001" + // standard identifier. This pins the LBA->byte-offset math to real disc + // content - a stable but wrong offset would pass the read-twice check + // above but fail here. + const u8 pvdCdb[10] = {0x28, 0, 0, 0, 0, 16, 0, 0, 1, 0}; // LBA 16, 1 block + auto pvd = bench.SendCommand(pvdCdb, sizeof(pvdCdb), 2048); + CHECK_EQ(pvd.csw.bmCSWStatus, 0); + CHECK_EQ(pvd.data.size(), (size_t)2048); + CHECK_EQ(pvd.data[0], 0x01); // primary volume descriptor + CHECK(memcmp(pvd.data.data() + 1, "CD001", 5) == 0); + + // A pure data disc reports medium type 0x01 and a data track in the TOC. + const u8 msCdb[10] = {0x5A, 0x00, 0x2A, 0, 0, 0, 0, 0, 128, 0}; + auto ms = bench.SendCommand(msCdb, sizeof(msCdb), 128); + CHECK_EQ(ms.csw.bmCSWStatus, 0); + CHECK_EQ(ms.data[2], 0x01); // data CD + + const u8 tocCdb[10] = {0x43, 0x00, 0, 0, 0, 0, 0, 0, 100, 0}; + auto toc = bench.SendCommand(tocCdb, sizeof(tocCdb), 100); + CHECK_EQ(toc.csw.bmCSWStatus, 0); + CHECK_EQ(toc.data[2], 0x01); // first track + CHECK_EQ(toc.data[3], 0x01); // last track + CHECK_EQ(toc.data[5] & 0x04, 0x04); // track 1 control: data +} + +// --------------------------------------------------------------------------- +// Synthetic pure-audio CD through the real reader +// --------------------------------------------------------------------------- + +TEST(real_cuebin_audio_cd) +{ + const u32 nTracks = 3; + const u32 sectorsPerTrack = 150; // 2 seconds + const std::string bin = TestDataDir() + "/audio.bin"; + WriteFileWithPattern(bin, (u64)nTracks * sectorsPerTrack * 2352); + + std::string cue = "FILE \"audio.bin\" BINARY\n"; + for (u32 t = 0; t < nTracks; t++) { + char hdr[64]; + snprintf(hdr, sizeof(hdr), " TRACK %02u AUDIO\n", t + 1); + cue += hdr; + cue += " INDEX 01 " + FramesToMSF(t * sectorsPerTrack) + "\n"; + } + + CCueBinFileDevice *disc = OpenReader(bin, cue); + CHECK(disc != nullptr); + if (!disc) { + return; + } + + CCDPlayer player; + CGadgetTestBench bench(disc, false, &player); + bench.Activate(); + bench.RequestSense(); + + // All-audio disc -> medium type 0x02. + const u8 msCdb[10] = {0x5A, 0x00, 0x2A, 0, 0, 0, 0, 0, 128, 0}; + auto ms = bench.SendCommand(msCdb, sizeof(msCdb), 128); + CHECK_EQ(ms.csw.bmCSWStatus, 0); + CHECK_EQ(ms.data[2], 0x02); + + // TOC: first/last track = 1/3. + const u8 tocCdb[10] = {0x43, 0x00, 0, 0, 0, 0, 0, 0, (u8)200, 0}; + auto toc = bench.SendCommand(tocCdb, sizeof(tocCdb), 200); + CHECK_EQ(toc.csw.bmCSWStatus, 0); + CHECK_EQ(toc.data[2], 0x01); + CHECK_EQ(toc.data[3], (u8)nTracks); + CHECK_EQ(toc.data[5] & 0x04, 0x00); // track 1 control: audio (data bit clear) + + // PLAY AUDIO MSF starting at track 2 (LBA 150). CDB MSF is absolute, so + // it carries LBA + 150 (the 2-second lead-in); the drive subtracts it + // back off to recover the LBA. + auto lbaToMsf = [](u32 lba, u8 &m, u8 &s, u8 &f) { + u32 fr = lba + 150; + m = (u8)(fr / (60 * 75)); + s = (u8)((fr / 75) % 60); + f = (u8)(fr % 75); + }; + u8 sm, ss, sf, em, es, ef; + lbaToMsf(sectorsPerTrack, sm, ss, sf); // start = LBA 150 + lbaToMsf(nTracks * sectorsPerTrack, em, es, ef); // end = leadout + const u8 playCdb[10] = {0x47, 0x00, 0x00, sm, ss, sf, em, es, ef, 0x00}; + auto play = bench.SendCommand(playCdb, sizeof(playCdb), 0); + CHECK_EQ(play.csw.bmCSWStatus, 0); + CHECK_EQ(player.playCalls, 1); + CHECK_EQ(player.lastPlayLBA, (u32)sectorsPerTrack); // LBA 150 +} + +// --------------------------------------------------------------------------- +// Synthetic mixed data+audio CD through the real reader +// --------------------------------------------------------------------------- + +TEST(real_cuebin_mixed_mode) +{ + const u32 dataSectors = 100; // MODE1/2048 + const u32 audioSectors = 150; // per audio track, 2352 + const u64 dataBytes = (u64)dataSectors * 2048; + const std::string bin = TestDataDir() + "/mixed.bin"; + // BIN layout: data track at 2048/sector, then two audio tracks at 2352. + WriteFileWithPattern(bin, dataBytes + (u64)2 * audioSectors * 2352); + + std::string cue = "FILE \"mixed.bin\" BINARY\n"; + cue += " TRACK 01 MODE1/2048\n INDEX 01 00:00:00\n"; + cue += " TRACK 02 AUDIO\n INDEX 01 " + FramesToMSF(dataSectors) + "\n"; + cue += " TRACK 03 AUDIO\n INDEX 01 " + FramesToMSF(dataSectors + audioSectors) + "\n"; + + CCueBinFileDevice *disc = OpenReader(bin, cue); + CHECK(disc != nullptr); + if (!disc) { + return; + } + + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + // Data + audio -> medium type 0x03 (the #164-relevant path, now through + // the real cue parser reading a real cue sheet). + const u8 msCdb[10] = {0x5A, 0x00, 0x2A, 0, 0, 0, 0, 0, 128, 0}; + auto ms = bench.SendCommand(msCdb, sizeof(msCdb), 128); + CHECK_EQ(ms.csw.bmCSWStatus, 0); + CHECK_EQ(ms.data[2], 0x03); + + // TOC: 3 tracks, track 1 data, track 2 audio. + const u8 tocCdb[10] = {0x43, 0x00, 0, 0, 0, 0, 0, 0, (u8)200, 0}; + auto toc = bench.SendCommand(tocCdb, sizeof(tocCdb), 200); + CHECK_EQ(toc.csw.bmCSWStatus, 0); + CHECK_EQ(toc.data[2], 0x01); + CHECK_EQ(toc.data[3], 0x03); + CHECK_EQ(toc.data[5] & 0x04, 0x04); // track 1: data + CHECK_EQ(toc.data[13] & 0x04, 0x00); // track 2: audio + + // READ(10) a sector out of the data track and confirm it is the exact + // bytes we wrote (offset = LBA * 2048 for the data track). + const u32 lba = 5; + const u8 rdCdb[10] = {0x28, 0, 0, 0, 0, (u8)lba, 0, 0, 1, 0}; + auto rd = bench.SendCommand(rdCdb, sizeof(rdCdb), 2048); + CHECK_EQ(rd.csw.bmCSWStatus, 0); + CHECK_EQ(rd.data.size(), (size_t)2048); + u8 expected[2048]; + for (u32 i = 0; i < 2048; i++) { + expected[i] = PatternByte((u64)lba * 2048 + i); + } + CHECK_BYTES(rd.data.data(), rd.data.size(), expected, sizeof(expected)); + + // Cross-track offset math: an audio-track sector lives past the + // 2048->2352 sector-size change at the track-1/track-2 boundary, so its + // byte offset is (data-track bytes) + relative_lba * 2352. Read 5 sectors + // into audio track 2 straight from the reader and confirm both the + // computed offset and the returned 2352-byte sector are exact. The + // data-track READ(10) above never crosses the transition, so without this + // a bug in the per-track offset accumulation could survive. + const u32 audioLBA = dataSectors + 5; // 5 frames into audio track 2 + const u64 audioOff = disc->GetByteOffsetForLBA(audioLBA); + CHECK_EQ(audioOff, dataBytes + (u64)5 * 2352); + CHECK_EQ(disc->Seek(audioOff), audioOff); + u8 aData[2352]; + int an = disc->Read(aData, sizeof(aData)); + CHECK_EQ(an, (int)sizeof(aData)); + u8 aExpected[2352]; + for (u32 i = 0; i < 2352; i++) { + aExpected[i] = PatternByte(audioOff + i); + } + CHECK_BYTES(aData, (size_t)an, aExpected, sizeof(aExpected)); +} + +// --------------------------------------------------------------------------- +// Real FreeDOS ISO9660 + Joliet filesystem (testdata/freedos-test.iso.gz) +// --------------------------------------------------------------------------- + +TEST(real_freedos_iso9660_filesystem) +{ + const std::string iso = TestDataDir() + "/freedos-test.iso"; + u64 size = FileSize(iso); + CHECK(size > 0); // Makefile decompresses testdata/freedos-test.iso.gz + if (size == 0) { + return; + } + u32 totalBlocks = (u32)(size / 2048); + + CCueBinFileDevice *disc = OpenReader(iso, ""); + CHECK(disc != nullptr); + if (!disc) { + return; + } + + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + // Capacity matches the real on-disk file. + const u8 capCdb[10] = {0x25, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + auto cap = bench.SendCommand(capCdb, sizeof(capCdb), 8); + CHECK_EQ(cap.csw.bmCSWStatus, 0); + u32 lastLBA = (cap.data[0] << 24) | (cap.data[1] << 16) | (cap.data[2] << 8) | cap.data[3]; + CHECK_EQ(lastLBA, totalBlocks - 1); + + // LBA 16 = Primary Volume Descriptor: type 0x01, "CD001", and the volume + // identifier we authored (offset 40) - an oracle pinned to real content. + const u8 pvdCdb[10] = {0x28, 0, 0, 0, 0, 16, 0, 0, 1, 0}; + auto pvd = bench.SendCommand(pvdCdb, sizeof(pvdCdb), 2048); + CHECK_EQ(pvd.csw.bmCSWStatus, 0); + CHECK_EQ(pvd.data.size(), (size_t)2048); + CHECK_EQ(pvd.data[0], 0x01); + CHECK(memcmp(pvd.data.data() + 1, "CD001", 5) == 0); + CHECK(memcmp(pvd.data.data() + 40, "FREEDOS_TEST", 12) == 0); + + // LBA 17 = Joliet supplementary volume descriptor: type 0x02, "CD001". + // Confirms the reader returns later sectors correctly, not just LBA 16. + const u8 svdCdb[10] = {0x28, 0, 0, 0, 0, 17, 0, 0, 1, 0}; + auto svd = bench.SendCommand(svdCdb, sizeof(svdCdb), 2048); + CHECK_EQ(svd.csw.bmCSWStatus, 0); + CHECK_EQ(svd.data[0], 0x02); + CHECK(memcmp(svd.data.data() + 1, "CD001", 5) == 0); + + // Data disc -> medium type 0x01, single data track in the TOC. + const u8 msCdb[10] = {0x5A, 0x00, 0x2A, 0, 0, 0, 0, 0, 128, 0}; + auto ms = bench.SendCommand(msCdb, sizeof(msCdb), 128); + CHECK_EQ(ms.csw.bmCSWStatus, 0); + CHECK_EQ(ms.data[2], 0x01); +} + +// --------------------------------------------------------------------------- +// Real shareware/freeware game disc (testdata/shareware.iso.gz): the Descent +// shareware episode and the SkyRoads freeware game. Reads the ENTIRE disc +// through the reader and checks it byte-for-byte against the raw file - an +// extent-agnostic oracle that stresses a real ~3.5 MB filesystem with +// multi-megabyte files spanning many sectors and read-ahead-cache refills. +// See testdata/README-testdata.md. +// --------------------------------------------------------------------------- + +TEST(real_shareware_game_disc_full_readback) +{ + const std::string iso = TestDataDir() + "/shareware.iso"; + u64 size = FileSize(iso); + CHECK(size > 0); + if (size == 0) { + return; + } + + CCueBinFileDevice *disc = OpenReader(iso, ""); + CHECK(disc != nullptr); + if (!disc) { + return; + } + + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + // PVD sanity: "CD001" and the volume id we authored. + const u8 pvdCdb[10] = {0x28, 0, 0, 0, 0, 16, 0, 0, 1, 0}; + auto pvd = bench.SendCommand(pvdCdb, sizeof(pvdCdb), 2048); + CHECK_EQ(pvd.csw.bmCSWStatus, 0); + CHECK(memcmp(pvd.data.data() + 1, "CD001", 5) == 0); + CHECK(memcmp(pvd.data.data() + 40, "SHAREWARE", 9) == 0); + + // Whole-disc readback: pull every byte through the real reader in an + // odd-sized chunk (deliberately not aligned to the 128 KiB cache window, + // so refills and cross-window reads are exercised) and compare against the + // raw file read with plain stdio. Every sector of the real disc must match. + FILE *raw = fopen(iso.c_str(), "rb"); + CHECK(raw != nullptr); + if (!raw) { + return; + } + disc->Seek(0); + const size_t chunk = 7000; + std::vector got(chunk), want(chunk); + u64 pos = 0; + bool mismatch = false; + while (pos < size) { + size_t n = (size_t)((size - pos < chunk) ? (size - pos) : chunk); + int rn = disc->Read(got.data(), n); + size_t wn = fread(want.data(), 1, n, raw); + if (rn != (int)n || wn != n || memcmp(got.data(), want.data(), n) != 0) { + mismatch = true; + break; + } + pos += n; + } + fclose(raw); + CHECK(!mismatch); + CHECK_EQ(pos, size); // read the whole disc +} + +// --------------------------------------------------------------------------- +// Real audio CD, cue sheet loaded off disk through the FatFs shim +// --------------------------------------------------------------------------- + +TEST(real_audiocd_cue_loaded_via_fatfs) +{ + const std::string cue = TestDataDir() + "/audiocd.cue"; + const std::string bin = TestDataDir() + "/audiocd.bin"; + CHECK(FileSize(cue) > 0); + CHECK(FileSize(bin) > 0); + + CCueBinFileDevice *disc = OpenReaderFromCueFile(cue, bin); + CHECK(disc != nullptr); + if (!disc) { + return; + } + + CCDPlayer player; + CGadgetTestBench bench(disc, false, &player); + bench.Activate(); + bench.RequestSense(); + + // All-audio disc parsed from the on-disk cue -> medium type 0x02. + const u8 msCdb[10] = {0x5A, 0x00, 0x2A, 0, 0, 0, 0, 0, 128, 0}; + auto ms = bench.SendCommand(msCdb, sizeof(msCdb), 128); + CHECK_EQ(ms.csw.bmCSWStatus, 0); + CHECK_EQ(ms.data[2], 0x02); + + // TOC: three tracks. + const u8 tocCdb[10] = {0x43, 0x00, 0, 0, 0, 0, 0, 0, (u8)200, 0}; + auto toc = bench.SendCommand(tocCdb, sizeof(tocCdb), 200); + CHECK_EQ(toc.csw.bmCSWStatus, 0); + CHECK_EQ(toc.data[2], 0x01); + CHECK_EQ(toc.data[3], 0x03); + + // Exact bytes 3 sectors into track 2 (LBA 100). All-audio is contiguous + // 2352-byte sectors, so the byte offset is LBA * 2352. + const u32 lba = 100 + 3; + const u64 off = disc->GetByteOffsetForLBA(lba); + CHECK_EQ(off, (u64)lba * 2352); + CHECK_EQ(disc->Seek(off), off); + u8 buf[2352]; + int n = disc->Read(buf, sizeof(buf)); + CHECK_EQ(n, (int)sizeof(buf)); + u8 exp[2352]; + for (u32 i = 0; i < 2352; i++) { + exp[i] = PatternByte(off + i); + } + CHECK_BYTES(buf, (size_t)n, exp, sizeof(exp)); +} + +// --------------------------------------------------------------------------- +// Real mixed-mode CD, cue sheet loaded off disk through the FatFs shim +// --------------------------------------------------------------------------- + +TEST(real_mixed_cue_loaded_via_fatfs) +{ + const std::string cue = TestDataDir() + "/mixed.cue"; + const std::string bin = TestDataDir() + "/mixed.bin"; + CHECK(FileSize(cue) > 0); + CHECK(FileSize(bin) > 0); + + CCueBinFileDevice *disc = OpenReaderFromCueFile(cue, bin); + CHECK(disc != nullptr); + if (!disc) { + return; + } + + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + // Data + audio -> medium type 0x03. + const u8 msCdb[10] = {0x5A, 0x00, 0x2A, 0, 0, 0, 0, 0, 128, 0}; + auto ms = bench.SendCommand(msCdb, sizeof(msCdb), 128); + CHECK_EQ(ms.csw.bmCSWStatus, 0); + CHECK_EQ(ms.data[2], 0x03); + + // Data track (2048/sector): READ(10) at LBA 5 returns exact bytes. + const u32 dataSectors = 100; + const u64 dataBytes = (u64)dataSectors * 2048; + const u8 rdCdb[10] = {0x28, 0, 0, 0, 0, 5, 0, 0, 1, 0}; + auto rd = bench.SendCommand(rdCdb, sizeof(rdCdb), 2048); + CHECK_EQ(rd.csw.bmCSWStatus, 0); + u8 dexp[2048]; + for (u32 i = 0; i < 2048; i++) { + dexp[i] = PatternByte((u64)5 * 2048 + i); + } + CHECK_BYTES(rd.data.data(), rd.data.size(), dexp, sizeof(dexp)); + + // Audio track past the 2048->2352 boundary: 5 sectors into track 2 + // (LBA 100). Byte offset must switch sector size at the boundary. + const u32 audioLBA = dataSectors + 5; + const u64 off = disc->GetByteOffsetForLBA(audioLBA); + CHECK_EQ(off, dataBytes + (u64)5 * 2352); + CHECK_EQ(disc->Seek(off), off); + u8 abuf[2352]; + int an = disc->Read(abuf, sizeof(abuf)); + CHECK_EQ(an, (int)sizeof(abuf)); + u8 aexp[2352]; + for (u32 i = 0; i < 2352; i++) { + aexp[i] = PatternByte(off + i); + } + CHECK_BYTES(abuf, (size_t)an, aexp, sizeof(aexp)); +} + +// --------------------------------------------------------------------------- +// Real CHD image (tracked sdcard/usbode-audio-test.chd) through libchdr +// --------------------------------------------------------------------------- + +#ifdef WITH_CHD +static std::string SdcardDir() +{ +#ifdef USBODE_SDCARD + return USBODE_SDCARD; +#else + return "../sdcard"; +#endif +} + +TEST(real_chd_loads_through_libchdr) +{ + const std::string chd = SdcardDir() + "/usbode-audio-test.chd"; + CHECK(FileSize(chd) > 0); + if (FileSize(chd) == 0) { + return; + } + + CCHDFileDevice *disc = new CCHDFileDevice(chd.c_str(), MEDIA_TYPE::CD); + bool ok = disc->Init(); + CHECK(ok); // real libchdr open + metadata parse + if (!ok) { + return; + } + + CCDPlayer player; + CGadgetTestBench bench(disc, false, &player); + bench.Activate(); + bench.RequestSense(); + + // TOC parses from the CHD metadata: a sensible track range. + const u8 tocCdb[10] = {0x43, 0x00, 0, 0, 0, 0, 0, 0, (u8)200, 0}; + auto toc = bench.SendCommand(tocCdb, sizeof(tocCdb), 200); + CHECK_EQ(toc.csw.bmCSWStatus, 0); + CHECK_EQ(toc.data[2], 0x01); // first track number + u8 lastTrack = toc.data[3]; + CHECK(lastTrack >= 1); + + // Medium type is one of the valid CD codes. + const u8 msCdb[10] = {0x5A, 0x00, 0x2A, 0, 0, 0, 0, 0, 128, 0}; + auto ms = bench.SendCommand(msCdb, sizeof(msCdb), 128); + CHECK_EQ(ms.csw.bmCSWStatus, 0); + CHECK(ms.data[2] == 0x01 || ms.data[2] == 0x02 || ms.data[2] == 0x03); + + // READ CAPACITY reports a non-empty disc. + const u8 capCdb[10] = {0x25, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + auto cap = bench.SendCommand(capCdb, sizeof(capCdb), 8); + CHECK_EQ(cap.csw.bmCSWStatus, 0); + u32 lastLBA = (cap.data[0] << 24) | (cap.data[1] << 16) | (cap.data[2] << 8) | cap.data[3]; + CHECK(lastLBA > 0); + + // Force a real hunk decompression. The checks above only parse CHD + // metadata; this reads sector 0 straight through CCHDFileDevice::Read, + // which calls chd_read and runs the real zlib/zstd/lzma decoder. A broken + // decode path returns a short/negative read here instead of silently + // passing a metadata-only test. Read it twice to confirm the decoded + // bytes are stable (cold decode vs cached hunk agree). + disc->Seek(0); + std::vector sec0(2048); + int n0 = disc->Read(sec0.data(), sec0.size()); + CHECK_EQ(n0, (int)sec0.size()); + disc->Seek(0); + std::vector sec0b(2048); + int n0b = disc->Read(sec0b.data(), sec0b.size()); + CHECK_EQ(n0b, (int)sec0b.size()); + CHECK_BYTES(sec0b.data(), sec0b.size(), sec0.data(), sec0.size()); +} + +// A mixed-mode CHD we build ourselves with chdman (testdata/mixed.chd: +// one MODE1/2048 data track + two audio tracks). Exercises libchdr on a +// real, tool-produced multi-track CHD rather than only the tracked audio one. +TEST(real_mixed_chd_through_libchdr) +{ + const std::string chd = TestDataDir() + "/mixed.chd"; + CHECK(FileSize(chd) > 0); + if (FileSize(chd) == 0) { + return; + } + + CCHDFileDevice *disc = new CCHDFileDevice(chd.c_str(), MEDIA_TYPE::CD); + bool ok = disc->Init(); + CHECK(ok); + if (!ok) { + return; + } + + CGadgetTestBench bench(disc); + bench.Activate(); + bench.RequestSense(); + + // TOC: three tracks (data + 2 audio), track 1 is data. + const u8 tocCdb[10] = {0x43, 0x00, 0, 0, 0, 0, 0, 0, (u8)200, 0}; + auto toc = bench.SendCommand(tocCdb, sizeof(tocCdb), 200); + CHECK_EQ(toc.csw.bmCSWStatus, 0); + CHECK_EQ(toc.data[2], 0x01); // first track + CHECK_EQ(toc.data[3], 0x03); // last track + CHECK_EQ(toc.data[5] & 0x04, 0x04); // track 1: data + + // Data + audio -> medium type 0x03. + const u8 msCdb[10] = {0x5A, 0x00, 0x2A, 0, 0, 0, 0, 0, 128, 0}; + auto ms = bench.SendCommand(msCdb, sizeof(msCdb), 128); + CHECK_EQ(ms.csw.bmCSWStatus, 0); + CHECK_EQ(ms.data[2], 0x03); + + // Decode a data-track hunk and check the bytes are EXACT, not just stable. + // chdman stored the MODE1/2048 track's user data (our PatternByte fill); + // reading sector 0 back runs chd_read -> the real LZMA/deflate decoder and + // must reproduce those exact bytes. A broken decode (or a zeroed buffer) + // fails here rather than passing a metadata-only or read-twice check. + disc->Seek(0); + std::vector sec(2048); + CHECK_EQ(disc->Read(sec.data(), sec.size()), (int)sec.size()); + u8 exp[2048]; + for (u32 i = 0; i < 2048; i++) { + exp[i] = PatternByte(i); // data-track sector 0, file offset 0 + } + CHECK_BYTES(sec.data(), sec.size(), exp, sizeof(exp)); + + // Same through the full host path: READ(10) a data sector further in and + // confirm exact bytes (exercises the gadget's LBA->offset->chd_read chain). + const u8 rdCdb[10] = {0x28, 0, 0, 0, 0, 5, 0, 0, 1, 0}; // LBA 5, 1 block + auto rd = bench.SendCommand(rdCdb, sizeof(rdCdb), 2048); + CHECK_EQ(rd.csw.bmCSWStatus, 0); + u8 exp5[2048]; + for (u32 i = 0; i < 2048; i++) { + exp5[i] = PatternByte((u64)5 * 2048 + i); + } + CHECK_BYTES(rd.data.data(), rd.data.size(), exp5, sizeof(exp5)); + + // Audio track 2 (starts at LBA 100), read straight from the reader. This + // exercises CCHDFileDevice's audio path: track-type detection plus the + // pair byte-swap. chdman stores CD audio big-endian, so the swap converts + // it back to the host's little-endian - round-tripping to the original + // mixed.bin bytes (PatternByte from the audio track's file offset). A + // broken or missing swap would NOT reproduce these exact bytes. + const u64 audioFileOff = (u64)100 * 2048; // audio track 2 starts at LBA 100 + disc->Seek((u64)100 * 2352); // LBA 100 * 2352/sector + u8 aud[2352]; + CHECK_EQ(disc->Read(aud, sizeof(aud)), (int)sizeof(aud)); + u8 aexp[2352]; + for (u32 i = 0; i < 2352; i++) { + aexp[i] = PatternByte(audioFileOff + i); + } + CHECK_BYTES(aud, sizeof(aud), aexp, sizeof(aexp)); +} +#endif // WITH_CHD diff --git a/integration-tests/testdata/README-testdata.md b/integration-tests/testdata/README-testdata.md new file mode 100644 index 00000000..cf80244e --- /dev/null +++ b/integration-tests/testdata/README-testdata.md @@ -0,0 +1,32 @@ +# Integration-test disc images + +Real disc images used by `test-suite/test_realimages.cpp`. Everything here is +either genuinely free-to-redistribute software or content generated by the +build script below. **No copyrighted commercial material.** The Makefile +decompresses/copies these into `out/images/` before the tests run. + +| File | What it is | Provenance / license | +| --- | --- | --- | +| `freedos-test.iso.gz` | Real ISO9660 + Joliet disc, volume id `FREEDOS_TEST`, with a nested directory tree | Built from genuine **FreeDOS 1.3** system files (`KERNEL.SYS`, `command.com`, `kernl386.sys`, and GPL utilities from the official FreeDOS 1.3 Floppy Edition, freedos.org) plus generated data. FreeDOS is free software, predominantly GPL. See the on-disc `PROVENANCE.TXT`. | +| `shareware.iso.gz` | Real ISO9660 + Joliet disc, volume id `SHAREWARE`, holding the **Descent** shareware episode and the **SkyRoads** freeware game | **Descent shareware v1.2** (c) Parallax Software / Interplay — the freely-distributable shareware release, as its original installer package (`INSTALL.EXE` + `.SOW`); and **SkyRoads** (c) Bluemoon Interactive — **developer-released freeware** (its `readme.txt` grants free redistribution as an intact single unit, which is honored here), a non-violent space driving game. Sourced from the Internet Archive items `descent-v1-2` and `SkyRoads`. See the on-disc `PROVENANCE.TXT`. Descent remains copyrighted by its owners and is included only as the freely-redistributable shareware; drop this file if a fully third-party-copyright-free set is preferred. | +| `audiocd.cue` + `audiocd.bin.gz` | Real CD-DA layout: 3 contiguous audio tracks (2352 B/sector) | Generated (deterministic byte pattern in a real Red Book sector layout). Public domain. | +| `mixed.cue` + `mixed.bin.gz` | Real mixed-mode CD: one `MODE1/2048` data track + 2 audio tracks | Generated. Public domain. | +| `mixed.chd` | The mixed CD compressed with **chdman** (real MAME CHD, cdlz/cdzl/cdfl codecs) | Generated from `mixed.cue`/`mixed.bin` by `chdman createcd`. Public domain. | + +The generated images use the same `PatternByte(off) = (off*31 + 7) & 0xFF` +fill the harness expects, so reads can be checked byte-exact. + +## Rebuilding + +The audio/mixed images and the CHD are reproducible with a short script +(Python for the pattern fill, `chdman createcd` for the CHD). The FreeDOS ISO +is built on macOS with: + +``` +hdiutil makehybrid -iso -joliet -default-volume-name FREEDOS_TEST -o freedos-test.iso +``` + +where `` holds the FreeDOS GPL files (extracted from the FreeDOS +1.3 Floppy Edition boot floppy) plus the generated data and text files. See +the git history / the maintainer notes for the exact staging layout. Any +ISO9660 authoring tool (`genisoimage`, `xorriso`) produces an equivalent disc. diff --git a/integration-tests/testdata/audiocd.bin.gz b/integration-tests/testdata/audiocd.bin.gz new file mode 100644 index 00000000..12deddeb Binary files /dev/null and b/integration-tests/testdata/audiocd.bin.gz differ diff --git a/integration-tests/testdata/audiocd.cue b/integration-tests/testdata/audiocd.cue new file mode 100644 index 00000000..1a8150a4 --- /dev/null +++ b/integration-tests/testdata/audiocd.cue @@ -0,0 +1,7 @@ +FILE "audiocd.bin" BINARY + TRACK 01 AUDIO + INDEX 01 00:00:00 + TRACK 02 AUDIO + INDEX 01 00:01:25 + TRACK 03 AUDIO + INDEX 01 00:02:30 diff --git a/integration-tests/testdata/freedos-test.iso.gz b/integration-tests/testdata/freedos-test.iso.gz new file mode 100644 index 00000000..9bf759c9 Binary files /dev/null and b/integration-tests/testdata/freedos-test.iso.gz differ diff --git a/integration-tests/testdata/mixed.bin.gz b/integration-tests/testdata/mixed.bin.gz new file mode 100644 index 00000000..eead8d39 Binary files /dev/null and b/integration-tests/testdata/mixed.bin.gz differ diff --git a/integration-tests/testdata/mixed.chd b/integration-tests/testdata/mixed.chd new file mode 100644 index 00000000..de8e84a8 Binary files /dev/null and b/integration-tests/testdata/mixed.chd differ diff --git a/integration-tests/testdata/mixed.cue b/integration-tests/testdata/mixed.cue new file mode 100644 index 00000000..6dcdb680 --- /dev/null +++ b/integration-tests/testdata/mixed.cue @@ -0,0 +1,7 @@ +FILE "mixed.bin" BINARY + TRACK 01 MODE1/2048 + INDEX 01 00:00:00 + TRACK 02 AUDIO + INDEX 01 00:01:25 + TRACK 03 AUDIO + INDEX 01 00:02:30 diff --git a/integration-tests/testdata/shareware.iso.gz b/integration-tests/testdata/shareware.iso.gz new file mode 100644 index 00000000..d89c2314 Binary files /dev/null and b/integration-tests/testdata/shareware.iso.gz differ