Skip to content

Adding memory card support to psyqo#2042

Open
nicolasnoble wants to merge 7 commits into
grumpycoders:mainfrom
nicolasnoble:psyqo-memorycard
Open

Adding memory card support to psyqo#2042
nicolasnoble wants to merge 7 commits into
grumpycoders:mainfrom
nicolasnoble:psyqo-memorycard

Conversation

@nicolasnoble

Copy link
Copy Markdown
Member

No description provided.

nicolasnoble and others added 6 commits June 22, 2026 13:13
Inverts the existing sjis-table.h decode table into a Unicode -> Shift-JIS
mapping, so code that needs to write Shift-JIS (memory card save titles and
the like) can encode from UTF-8 or ASCII instead of hand-rolling a partial
table. Ships the generator, the generated table (sorted for binary search),
and a small header exposing unicodeToSjis, a UTF-8 string encoder, and a
fullwidth-title helper matching the BIOS save-title convention. Verified by
round-tripping encoded output back through the existing Shift-JIS decoder.

Signed-off-by: Nicolas 'Pixel' Noble <nicolas@nobis-crew.org>
Adds a PSYQo memory card driver in two layers: a low level MemoryCard
transport (the raw 128-byte sector protocol over SIO0, interrupt driven,
exposed as callback, blocking, awaiter and TaskQueue forms) and a
Sony-compatible MemoryCardFileSystem on top of it (format, list, read, write
and delete files that the retail BIOS manager shows with their title and
icon, and can copy or delete itself).

The controllers and the card share the single SIO0 bus and cannot drive it at
the same time, and a card transaction can span several frames, so an SIO0Bus
ownership lock is held for the whole transaction rather than per sector;
AdvancedPad consults it and stands down its frame polling while a card
operation is in flight. Because of this the driver only works alongside
AdvancedPad, which drives SIO0 directly; using SimplePad (the BIOS-driven
pad) at the same time touches the bus outside the lock and is undefined.

Save titles are supplied as UTF-8 and encoded to the Shift-JIS field the BIOS
displays, with printable ASCII promoted to its fullwidth form. Includes a
worked example exercising format, write, read-back and delete.

Co-authored-by: Matt Hadden <forceh2010@gmail.com>
Co-authored-by: Bandwidth <jracek@honzuvkod.dev>
Signed-off-by: Nicolas 'Pixel' Noble <nicolas@nobis-crew.org>
The SIO0 card protocol is timing sensitive within a single sector transfer:
the bytes must flow without interruption, and the card has no dependable
acknowledge interrupt to lean on. So a sector is now a busy-polled exchange
run with all interrupts disabled (fastEnterCriticalSection), polling the
latched acknowledge bit, and the controller-IRQ state machine is removed
entirely. The per-sector transfer is intentionally synchronous: it is the
atomic unit that the timer-driven filesystem layer chains across frames.

Signed-off-by: Nicolas 'Pixel' Noble <nicolas@nobis-crew.org>
…hine

A memory card is itself a small state machine whose timing varies by card and
by phase, and reading the data register before the chip has advanced is a
no-op rather than a stale byte. So a sector transfer now clocks each byte and
then polls the card's output until it reaches the expected reply: this
self-synchronizes to the card's per-state timing without tracking a fixed
full-duplex offset. The identifier and command-acknowledge bytes are the slow,
variable states that are polled; the data phase is regular and
acknowledge-paced. Every protocol byte is named rather than a hanging hex
literal, and the inner transfer runs with interrupts disabled.

The read path follows an inner loop verified on hardware; the write path
mirrors its structure over the standard protocol and still needs validation on
a real card.

Signed-off-by: Nicolas 'Pixel' Noble <nicolas@nobis-crew.org>
The memory card filesystem now follows the same asynchronous contract as
CDRomDevice. A whole operation is a transaction spanning many sector transfers
across several frames, so each is driven by a main-loop timer (never an
interrupt): one sector is transferred per tick and the game keeps running in
between, and only one operation runs at a time. Every operation has a callback
form (the basis: it takes a GPU to arm the timer and reports its result to an
eastl::function during pumping) and a *Blocking(GPU&) form that pumps until it
completes. The SIO0 bus is owned for the whole transaction, so AdvancedPad
stands down for its full duration.

Internally each operation is a small state machine over shared phases (read the
directory, write the directory, read or write data) advanced one sector per
tick. The example exercises format, write, read-back, list and delete.

Signed-off-by: Nicolas 'Pixel' Noble <nicolas@nobis-crew.org>
Testing the memory card driver on real hardware surfaced a set of fixes plus
assorted cleanups across the transport and filesystem layers. The main
correctness fix is an inter-transaction recovery delay before each port
(re)select: the retail BIOS gets that recovery for free by driving one sector
per vblank, but back-to-back transactions can reselect a slow third-party card
too soon and read stale data that still carries a valid checksum. The
port-select sequence is also corrected to enable the transmitter and the
acknowledge IRQ before the bus-settle delay, so a present card is not misread
as absent on the first byte.

Signed-off-by: Nicolas 'Pixel' Noble <nicolas@nobis-crew.org>
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a complete memory card subsystem to the psyqo MIPS library: a SIO0Bus ownership arbiter, a low-level MemoryCard SIO0 sector driver (IRQ and polling modes, async/blocking/coroutine APIs), SJIS encoding utilities for file titles, a high-level MemoryCardFileSystem with async and blocking filesystem operations, and an interactive integration-test example.

Changes

psyqo Memory Card Stack

Layer / File(s) Summary
SIO0 bus arbiter and advancedpad guard
src/mips/psyqo/sio0-bus.hh, src/mips/psyqo/src/advancedpad.cpp
Introduces psyqo::SIO0Bus with a re-entrant depth counter and an RAII Lock. AdvancedPad::readPad() early-returns when the bus is owned by an in-flight memory card transfer.
MemoryCard low-level driver API
src/mips/psyqo/memory-card.hh
Declares psyqo::MemoryCard with Error/Port enums, sector geometry constants, blocking sector read/write/probe, callback-based and TaskQueue-based sector ops, and coroutine awaiter types.
MemoryCard SIO0 driver implementation
src/mips/psyqo/src/memory-card.cpp
Implements error messages, SIO0 init, IRQ installation, byte exchange, IRQ transfer start/finish, IRQ protocol state machine (read/write steps), blocking IRQ watchdog wrapper, polling sector primitives, retry logic, and non-blocking async sector APIs with TaskQueue adapters.
SJIS encoding table generator and inline encoder
src/mips/common/util/gen-sjis-encode-table.py, src/mips/common/util/sjis-encode.h
Python script generates sjis-encode-table.h as the inverse Unicode→Shift-JIS mapping. C++ header adds unicodeToSjis, utf8Decode, utf8ToSjis, and utf8ToSjisTitle (fullwidth ASCII promotion for save titles).
MemoryCardFileSystem public API and internal engine declarations
src/mips/psyqo/memory-card-filesystem.hh
Declares psyqo::MemoryCardFileSystem with Icon/FileEntry structs, all async and blocking filesystem operations, op/phase enums, frame-helper declarations, and transaction engine member storage.
MemoryCardFileSystem state machine implementation
src/mips/psyqo/src/memory-card-filesystem.cpp
Implements little-endian frame helpers, directory checksum/name matching, begin()/runBlocking(), issueOrFinish() sector-dispatch, onSectorDone() phase transitions with payload copying, afterReadDir() directory logic (chain walking, allocation, delete), and all async/blocking public API wrappers.
Memory card interactive test example
src/mips/psyqo/examples/memorycard/Makefile, src/mips/psyqo/examples/memorycard/memorycard.cpp
Adds a C++20 example with a 13-step test harness exercising probe, format, state checks, icon write, existence/read/list/delete verification, and free-block accounting, with a PASS/FAIL log rendered on screen.

Sequence Diagram(s)

sequenceDiagram
    participant App
    participant MemoryCardFileSystem
    participant MemoryCard
    participant SIO0Bus
    participant SIO0HW as SIO0 Hardware

    App->>MemoryCardFileSystem: writeFile(port, name, title, icon, data, len, cb)
    MemoryCardFileSystem->>MemoryCardFileSystem: begin() — preflight block count
    MemoryCardFileSystem->>SIO0Bus: acquire() via Lock
    MemoryCardFileSystem->>MemoryCard: readSector(header sector)
    MemoryCard->>SIO0HW: startTransfer / IRQ state machine
    SIO0HW-->>MemoryCard: IRQ per byte → finishTransfer
    MemoryCard-->>MemoryCardFileSystem: onSectorDone(OK)
    MemoryCardFileSystem->>MemoryCard: readSector(dir frames ×15)
    MemoryCard-->>MemoryCardFileSystem: onSectorDone(OK) ×15
    MemoryCardFileSystem->>MemoryCardFileSystem: afterReadDir() — allocate blocks
    MemoryCardFileSystem->>MemoryCard: writeSector(title/icon/data frames)
    MemoryCard-->>MemoryCardFileSystem: onSectorDone(OK) per frame
    MemoryCardFileSystem->>MemoryCard: writeSector(dirty directory entries)
    MemoryCard-->>MemoryCardFileSystem: onSectorDone(OK)
    MemoryCardFileSystem->>SIO0Bus: release()
    MemoryCardFileSystem-->>App: callback(Error::OK)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 Hoppy little rabbit taps the SIO line,
Writes icons and titles in Shift-JIS just fine,
Sectors and blocks in a 15-frame dance,
IRQs and checksums — no leaving to chance!
PASS glows in green on the screen every time,
The memory card saves every nibble and rhyme. 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No description was provided by the author, making it impossible to assess whether the description relates to the changeset or is off-topic. Add a pull request description explaining the memory card implementation, its components, and any relevant context for reviewers.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Adding memory card support to psyqo' directly and clearly describes the main objective of the changeset, which introduces comprehensive memory card functionality to the psyqo library across multiple new files and components.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
src/mips/psyqo/src/memory-card.cpp (1)

718-758: 🩺 Stability & Availability | 🔵 Trivial

Async sector transfers should acquire SIO0Bus for their in-flight duration to prevent AdvancedPad from corrupting the transfer.

The callback variants of readSector and writeSector return immediately and let the interrupt state machine run across frames without acquiring SIO0Bus, relying entirely on the caller to hold the bus. While the header comment documents this caller responsibility and the current codebase respects it (the filesystem transaction holds the outer lock), this is a footgun waiting for a direct caller to bypass the safeguard. Acquire SIO0Bus in startAsyncAttempt and release in onAsyncAttemptDone; the re-entrant counter tolerates nested acquisition from the filesystem's outer lock.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mips/psyqo/src/memory-card.cpp` around lines 718 - 758, The async sector
transfer methods `readSector` and `writeSector` initiate async operations via
`startAsyncAttempt` without acquiring the SIO0Bus lock, relying entirely on the
caller to hold it and creating a footgun for direct callers. To fix this,
acquire SIO0Bus at the beginning of `startAsyncAttempt` method and release it in
the `onAsyncAttemptDone` method that completes the async operation. Since
SIO0Bus uses a re-entrant counter, nested acquisition from the filesystem's
outer lock will be safely tolerated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/mips/common/util/gen-sjis-encode-table.py`:
- Around line 17-19: The if, elif, and else statements on lines 17-19 contain
multiple statements on a single line (condition and assignment together),
violating Ruff E701/E702 rules. Split each statement by moving the assignment of
base and lead_hi to a new line below the if/elif/else condition, so each line
contains only one statement. Apply the same fix to line 46 which has a similar
multi-statement violation.

In `@src/mips/psyqo/sio0-bus.hh`:
- Around line 46-47: The documentation comment at lines 46-47 is misleading
about how the re-entrant count works. The current wording implies that the
low-level driver function irqTransferBlocking() also acquires and releases the
lock, but it never actually calls acquire() or release(). Only the filesystem
layer (MemoryCardFileSystem) holds the lock across multiple sector operations.
Update the documentation to clarify that the re-entrant count exists because
filesystem operations hold the lock across all their sector transfers, not
because the blocking transfer function itself participates in arbitration. Make
it clear that only the filesystem operation caller (not the driver) acquires and
holds the bus.

In `@src/mips/psyqo/src/memory-card-filesystem.cpp`:
- Around line 229-238: The Phase::WriteDir case commits dirty directory entries
by index order, but this creates a safety issue with the multi-entry chain logic
built elsewhere (around lines 347-377) which requires the first entry of a chain
to be written last. Currently, if power loss occurs after writing the first
entry in a chain but before all linked entries are written, an incomplete file
chain can be exposed on the card. Modify the WriteDir phase to commit directory
entries in a specific order where the first entry of any chain is written LAST,
after all its linked entries are already committed. This ensures the "first
entry last" rule is applied consistently throughout the write and overwrite
paths, preventing partially committed files from being visible on interruption.
- Around line 444-455: The block chain traversal loop exits when the block value
goes out of range but still proceeds to the ReadTitle phase as if it completed
successfully. Add validation after the while loop to check if the chain
terminated cleanly by verifying that either the 0xffff terminator was
encountered or the chain length reached its maximum. If the loop exited due to
block going out of bounds without a clean termination, set m_result to
Error::BadData and return StepResult::Done instead of proceeding to the
ReadTitle phase.
- Around line 128-133: The totalBytes calculation at line 128 uses uint32_t
which can overflow when m_dataLen is large, allowing the FileTooLarge check to
be bypassed silently. Before computing totalBytes, check if adding
m_headerFrames multiplied by MemoryCard::c_sectorSize and m_dataLen would
overflow a uint32_t, or alternatively compute totalBytes using a larger type
like uint64_t and verify the result does not exceed the uint32_t maximum before
proceeding with the block-count calculation and the existing bounds check.

---

Nitpick comments:
In `@src/mips/psyqo/src/memory-card.cpp`:
- Around line 718-758: The async sector transfer methods `readSector` and
`writeSector` initiate async operations via `startAsyncAttempt` without
acquiring the SIO0Bus lock, relying entirely on the caller to hold it and
creating a footgun for direct callers. To fix this, acquire SIO0Bus at the
beginning of `startAsyncAttempt` method and release it in the
`onAsyncAttemptDone` method that completes the async operation. Since SIO0Bus
uses a re-entrant counter, nested acquisition from the filesystem's outer lock
will be safely tolerated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f213614b-0fe8-4a0f-b059-576161ffe694

📥 Commits

Reviewing files that changed from the base of the PR and between 221e96b and 79a12bc.

📒 Files selected for processing (11)
  • src/mips/common/util/gen-sjis-encode-table.py
  • src/mips/common/util/sjis-encode-table.h
  • src/mips/common/util/sjis-encode.h
  • src/mips/psyqo/examples/memorycard/Makefile
  • src/mips/psyqo/examples/memorycard/memorycard.cpp
  • src/mips/psyqo/memory-card-filesystem.hh
  • src/mips/psyqo/memory-card.hh
  • src/mips/psyqo/sio0-bus.hh
  • src/mips/psyqo/src/advancedpad.cpp
  • src/mips/psyqo/src/memory-card-filesystem.cpp
  • src/mips/psyqo/src/memory-card.cpp

Comment on lines +17 to +19
if index < 0x1100: base, lead_hi = 0x100, 0x80
elif index < 0x2100: base, lead_hi = 0x1100, 0x90
else: base, lead_hi = 0x2100, 0xe0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Resolve Ruff E701/E702 inline-statement violations.

Line 17, Line 18, Line 19, and Line 46 use multi-statements on one line and currently fail the configured lint rules.

💡 Suggested cleanup
 def index_to_sjis(index):
     if index < 0x100:
         return index
-    if index < 0x1100:   base, lead_hi = 0x100,  0x80
-    elif index < 0x2100: base, lead_hi = 0x1100, 0x90
-    else:                base, lead_hi = 0x2100, 0xe0
+    if index < 0x1100:
+        base, lead_hi = 0x100, 0x80
+    elif index < 0x2100:
+        base, lead_hi = 0x1100, 0x90
+    else:
+        base, lead_hi = 0x2100, 0xe0
@@
         tok = "{0x%04x, 0x%04x}, " % (uni, sjis)
         if len(line) + len(tok) > 116:
-            f.write(line.rstrip() + "\n"); line = "    "
+            f.write(line.rstrip() + "\n")
+            line = "    "

Also applies to: 46-46

🧰 Tools
🪛 Ruff (0.15.18)

[error] 17-17: Multiple statements on one line (colon)

(E701)


[error] 18-18: Multiple statements on one line (colon)

(E701)


[error] 19-19: Multiple statements on one line (colon)

(E701)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mips/common/util/gen-sjis-encode-table.py` around lines 17 - 19, The if,
elif, and else statements on lines 17-19 contain multiple statements on a single
line (condition and assignment together), violating Ruff E701/E702 rules. Split
each statement by moving the assignment of base and lead_hi to a new line below
the if/elif/else condition, so each line contains only one statement. Apply the
same fix to line 46 which has a similar multi-statement violation.

Source: Linters/SAST tools

Comment on lines +46 to +47
* The count is re-entrant: a filesystem operation holds it across all of its
* sector transfers even though each blocking sector transfer also takes it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether the low-level MemoryCard driver ever acquires SIO0Bus.
echo "=== SIO0Bus references across psyqo ==="
rg -nH -C2 'SIO0Bus' src/mips/psyqo
echo "=== sio0-bus.hh include in memory-card.cpp? ==="
rg -nH 'sio0-bus' src/mips/psyqo/src/memory-card.cpp || echo "no include"

Repository: grumpycoders/pcsx-redux

Length of output: 2140


🏁 Script executed:

sed -n '40,60p' src/mips/psyqo/sio0-bus.hh

Repository: grumpycoders/pcsx-redux

Length of output: 1114


🏁 Script executed:

ast-grep outline src/mips/psyqo/src/memory-card.cpp | head -50

Repository: grumpycoders/pcsx-redux

Length of output: 210


🏁 Script executed:

wc -l src/mips/psyqo/src/memory-card.cpp && head -100 src/mips/psyqo/src/memory-card.cpp

Repository: grumpycoders/pcsx-redux

Length of output: 3633


🏁 Script executed:

rg -nH 'blocking|transfer|readBlock|writeBlock' src/mips/psyqo/src/memory-card.cpp | head -30

Repository: grumpycoders/pcsx-redux

Length of output: 1536


🏁 Script executed:

sed -n '400,430p' src/mips/psyqo/src/memory-card.cpp

Repository: grumpycoders/pcsx-redux

Length of output: 1282


🏁 Script executed:

sed -n '750,800p' src/mips/psyqo/src/memory-card.cpp && echo "---" && rg 'acquire|release' src/mips/psyqo/src/memory-card.cpp

Repository: grumpycoders/pcsx-redux

Length of output: 2615


Correct documentation: the low-level driver does not acquire the SIO0Bus.

The blocking sector transfer function (irqTransferBlocking()) in src/mips/psyqo/src/memory-card.cpp never calls acquire() or release(). The re-entrant count exists because the filesystem layer (MemoryCardFileSystem) holds the lock across multiple sector operations, not because blocking transfers self-arbitrate.

The current wording at lines 46–47 is misleading: "a filesystem operation holds it across all of its sector transfers even though each blocking sector transfer also takes it" falsely implies the low-level driver participates in arbitration. Update the documentation to clarify that only the filesystem layer (the operation caller) acquires and holds the bus.

The current code is functionally safe (blocking transfers run synchronously on the main thread, and AdvancedPad respects the owned() check), but the contract must be explicit to prevent a future maintainer from incorrectly assuming the driver self-protects.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mips/psyqo/sio0-bus.hh` around lines 46 - 47, The documentation comment
at lines 46-47 is misleading about how the re-entrant count works. The current
wording implies that the low-level driver function irqTransferBlocking() also
acquires and releases the lock, but it never actually calls acquire() or
release(). Only the filesystem layer (MemoryCardFileSystem) holds the lock
across multiple sector operations. Update the documentation to clarify that the
re-entrant count exists because filesystem operations hold the lock across all
their sector transfers, not because the blocking transfer function itself
participates in arbitration. Make it clear that only the filesystem operation
caller (not the driver) acquires and holds the bus.

Comment on lines +128 to +133
uint32_t totalBytes = m_headerFrames * MemoryCard::c_sectorSize + m_dataLen;
m_blocksNeeded = (totalBytes + (MemoryCard::c_blockSize - 1)) >> 13; // ceil over 8192
if (m_blocksNeeded < 1) m_blocksNeeded = 1;
if (m_blocksNeeded > 15) {
m_result = Error::FileTooLarge;
m_phase = Phase::Fail;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Guard totalBytes against overflow before block-count calculation.

Line 128 computes totalBytes in uint32_t; large dataLen can wrap and bypass the FileTooLarge check, causing silent truncation instead of a deterministic error.

💡 Suggested fix
-            uint32_t totalBytes = m_headerFrames * MemoryCard::c_sectorSize + m_dataLen;
-            m_blocksNeeded = (totalBytes + (MemoryCard::c_blockSize - 1)) >> 13;  // ceil over 8192
+            const uint32_t headerBytes = m_headerFrames * MemoryCard::c_sectorSize;
+            const uint32_t maxCardBytes = 15u * MemoryCard::c_blockSize;
+            if (headerBytes > maxCardBytes || m_dataLen > (maxCardBytes - headerBytes)) {
+                m_result = Error::FileTooLarge;
+                m_phase = Phase::Fail;
+                return;
+            }
+            const uint32_t totalBytes = headerBytes + m_dataLen;
+            m_blocksNeeded = (totalBytes + (MemoryCard::c_blockSize - 1)) >> 13;  // ceil over 8192
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uint32_t totalBytes = m_headerFrames * MemoryCard::c_sectorSize + m_dataLen;
m_blocksNeeded = (totalBytes + (MemoryCard::c_blockSize - 1)) >> 13; // ceil over 8192
if (m_blocksNeeded < 1) m_blocksNeeded = 1;
if (m_blocksNeeded > 15) {
m_result = Error::FileTooLarge;
m_phase = Phase::Fail;
const uint32_t headerBytes = m_headerFrames * MemoryCard::c_sectorSize;
const uint32_t maxCardBytes = 15u * MemoryCard::c_blockSize;
if (headerBytes > maxCardBytes || m_dataLen > (maxCardBytes - headerBytes)) {
m_result = Error::FileTooLarge;
m_phase = Phase::Fail;
return;
}
const uint32_t totalBytes = headerBytes + m_dataLen;
m_blocksNeeded = (totalBytes + (MemoryCard::c_blockSize - 1)) >> 13; // ceil over 8192
if (m_blocksNeeded < 1) m_blocksNeeded = 1;
if (m_blocksNeeded > 15) {
m_result = Error::FileTooLarge;
m_phase = Phase::Fail;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mips/psyqo/src/memory-card-filesystem.cpp` around lines 128 - 133, The
totalBytes calculation at line 128 uses uint32_t which can overflow when
m_dataLen is large, allowing the FileTooLarge check to be bypassed silently.
Before computing totalBytes, check if adding m_headerFrames multiplied by
MemoryCard::c_sectorSize and m_dataLen would overflow a uint32_t, or
alternatively compute totalBytes using a larger type like uint64_t and verify
the result does not exceed the uint32_t maximum before proceeding with the
block-count calculation and the existing bounds check.

Comment thread src/mips/psyqo/src/memory-card-filesystem.cpp
Comment on lines +444 to +455
while (block >= 1 && block <= 15 && m_chainLen < 16) {
if (visited & (1 << block)) {
m_result = Error::BadData;
return StepResult::Done;
}
visited |= (1 << block);
m_chain[m_chainLen++] = static_cast<uint8_t>(block);
uint16_t next = get16(m_dir[block - 1].bytes + c_offNext);
if (next == 0xffff) break;
block = next + 1;
}
m_phase = Phase::ReadTitle;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject malformed block chains that do not terminate cleanly.

Line 444 exits the traversal when the next block goes out-of-range, but still proceeds as success. That can return partial/truncated data as OK instead of Error::BadData.

💡 Suggested fix
             m_chainLen = 0;
             uint16_t visited = 0;
             int block = first;
+            bool terminated = false;
             while (block >= 1 && block <= 15 && m_chainLen < 16) {
                 if (visited & (1 << block)) {
                     m_result = Error::BadData;
                     return StepResult::Done;
                 }
                 visited |= (1 << block);
                 m_chain[m_chainLen++] = static_cast<uint8_t>(block);
                 uint16_t next = get16(m_dir[block - 1].bytes + c_offNext);
-                if (next == 0xffff) break;
+                if (next == 0xffff) {
+                    terminated = true;
+                    break;
+                }
                 block = next + 1;
             }
+            if (!terminated) {
+                m_result = Error::BadData;
+                return StepResult::Done;
+            }
             m_phase = Phase::ReadTitle;
             return StepResult::Continue;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while (block >= 1 && block <= 15 && m_chainLen < 16) {
if (visited & (1 << block)) {
m_result = Error::BadData;
return StepResult::Done;
}
visited |= (1 << block);
m_chain[m_chainLen++] = static_cast<uint8_t>(block);
uint16_t next = get16(m_dir[block - 1].bytes + c_offNext);
if (next == 0xffff) break;
block = next + 1;
}
m_phase = Phase::ReadTitle;
m_chainLen = 0;
uint16_t visited = 0;
int block = first;
bool terminated = false;
while (block >= 1 && block <= 15 && m_chainLen < 16) {
if (visited & (1 << block)) {
m_result = Error::BadData;
return StepResult::Done;
}
visited |= (1 << block);
m_chain[m_chainLen++] = static_cast<uint8_t>(block);
uint16_t next = get16(m_dir[block - 1].bytes + c_offNext);
if (next == 0xffff) {
terminated = true;
break;
}
block = next + 1;
}
if (!terminated) {
m_result = Error::BadData;
return StepResult::Done;
}
m_phase = Phase::ReadTitle;
return StepResult::Continue;
🧰 Tools
🪛 GitHub Check: CodeScene Code Health Review (main)

[warning] 444-444: ❌ New issue: Complex Conditional
psyqo::MemoryCardFileSystem::afterReadDir has 1 complex conditionals with 2 branches, threshold = 2

A complex conditional is an expression inside a branch (e.g. if, for, while) which consists of multiple,
logical operators such as AND/OR. The more logical operators in an expression, the more severe the code
smell.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mips/psyqo/src/memory-card-filesystem.cpp` around lines 444 - 455, The
block chain traversal loop exits when the block value goes out of range but
still proceeds to the ReadTitle phase as if it completed successfully. Add
validation after the while loop to check if the chain terminated cleanly by
verifying that either the 0xffff terminator was encountered or the chain length
reached its maximum. If the loop exited due to block going out of bounds without
a clean termination, set m_result to Error::BadData and return StepResult::Done
instead of proceeding to the ReadTitle phase.

The directory parser only knew about in-use (0x51/0x52/0x53) and
never-allocated (0xa0) blocks, which is incomplete: deleting a file marks its
blocks 0xa1/0xa2/0xa3 (a deleted first/middle/last), keeping the chain so it
stays recoverable.

- Deletion (and the replace path of writeFile) now flips block states to their
  deleted forms in place instead of wiping them, and allocation prefers
  never-allocated (0xa0) blocks over deleted ones so deletions survive as long
  as possible.
- A block counts as in use only when it is reachable from a valid first-block
  head, so an orphaned middle/last block (such as one left by an interrupted
  write) is reclaimed as free rather than leaked.
- The directory is committed in an order that survives power loss: a create
  writes the file's 0x51 head last and a delete cuts the head first, so a loss
  mid-commit can only orphan tail blocks (which read as free) and never leaves a
  referenced file pointing at a half-written chain.

Signed-off-by: Nicolas 'Pixel' Noble <nicolas@nobis-crew.org>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/mips/psyqo/src/memory-card-filesystem.cpp (1)

373-399: 🗄️ Data Integrity & Integration | 🟠 Major

Unbounded name length in directory entry construction can corrupt metadata fields.

The code at lines 373-399 computes nameLen by scanning m_name to its NUL terminator without any bounds check. Line 391 then copies nameLen bytes into the name field at entry + c_offName. Since ListFiles (line 452) reads exactly 20 bytes from this position and expects the field width to be 20 bytes (matching FileEntry.name[21]), any nameLen > 20 will overflow into adjacent metadata fields. Additionally, if the caller passes an unterminated string via writeFile() or deleteFileBlocking(), the scan at line 374 will read out of bounds.

The writeFile() and deleteFile() API entry points do not validate the name parameter before storing it in m_name. Other code paths in this same file (line 137-139) demonstrate proper validation using c_maxNameLength.

Clamp nameLen to c_maxNameLength before the memcpy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mips/psyqo/src/memory-card-filesystem.cpp` around lines 373 - 399, The
nameLen variable is computed by scanning m_name without bounds checking, and
when copied to entry + c_offName using __builtin_memcpy, any nameLen exceeding
the field width of 20 bytes will overflow into adjacent metadata fields.
Additionally, if m_name is unterminated, the while loop will read out of bounds.
After computing nameLen (and before the memcpy at line 391), clamp nameLen to
c_maxNameLength to ensure the copied name does not overflow the field width,
matching the validation pattern used elsewhere in this file.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/mips/psyqo/src/memory-card-filesystem.cpp`:
- Around line 373-399: The nameLen variable is computed by scanning m_name
without bounds checking, and when copied to entry + c_offName using
__builtin_memcpy, any nameLen exceeding the field width of 20 bytes will
overflow into adjacent metadata fields. Additionally, if m_name is unterminated,
the while loop will read out of bounds. After computing nameLen (and before the
memcpy at line 391), clamp nameLen to c_maxNameLength to ensure the copied name
does not overflow the field width, matching the validation pattern used
elsewhere in this file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 77d2af96-3aad-4062-937a-f42214e4ccfc

📥 Commits

Reviewing files that changed from the base of the PR and between 79a12bc and c162675.

📒 Files selected for processing (2)
  • src/mips/psyqo/memory-card-filesystem.hh
  • src/mips/psyqo/src/memory-card-filesystem.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/mips/psyqo/memory-card-filesystem.hh


#include "common/util/sjis-encode-table.h"

// Unicode -> Shift-JIS encoding, the inverse of support/sjis_conv.cc's decoder.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be good to add an EASTL version of sjis_conv.h/.cc to psyqo to make it possible to display/printf obtained filenames?

Right now you can't use it as-is, because sjis_conv.h/.cc use which is not available in freestanding mode.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants