Skip to content

Saves named before backslash paths are copied to their new names at mount - #904

Closed
JoseZamoraCifras wants to merge 1 commit into
seedds:mainfrom
JoseZamoraCifras:fs-backslash-saves
Closed

JoseZamoraCifras wants to merge 1 commit into
seedds:mainfrom
JoseZamoraCifras:fs-backslash-saves

Conversation

@JoseZamoraCifras

Copy link
Copy Markdown
Contributor

What and why

Builds before 7c23cc0 (#513: backslashes separate path components) took a backslash as part of a name. A game that opens save:\savedata0.bin therefore stored a file literally named \savedata0.bin in its save folder. Since #513 the host looks for savedata0.bin, finds nothing, and the save looks lost: The Legend of Zelda: Link's Awakening (01006BB00C6F0000) shows every file of its file select as "New game", while \savedata0.bin, \savedata3.bin, \screenshot0.bin and \screenshot3.bin are still in the folder. Any game that writes save:\... paths is affected the same way.

At each transactional save mount, right after the work folder is cloned, save_legacy_names (hle_fs.c):

  • finds each entry whose guest name has a backslash, at any depth, without entering a folder with such a name and without following symlinks
  • clones it (a file, or a folder with its tree, CLONE_NOFOLLOW) to the path that name names now, making parent folders by exact guest name (a case collision gets a .dm-case-N alias as a create does)
  • never overwrites an entry, and keeps the old one, which a build before fs: backslash path separators and 8-byte aligned file handles #513 still reads
  • marks the old entry with the attribute org.d2rmobile.backslash-copied, so this happens once: a file the game erases later stays erased
  • logs one line per copy (save: \savedata0.bin copied to savedata0.bin (...)). A name that leads above the save's root, or a copy the disk refuses, logs a red line, stays unmarked and is tried again at the next mount

It works in the work folder only, so it rides the save transaction. The copies reach the committed save with the game's own Commit. A kill or a remount before that leaves the committed save as it was and does the same again. The other way, a lookup that falls back to the old name, would have had to teach every write, create, rename, delete and listing about it, and an erased file would have come back.

Known limit: a save packed for cloud saves (save_sync.c) carries only the guest-name attribute, not the mark. A device that receives a save in which the game erased a copied file copies the old entry again.

The reference entry is "Saves named by a build before backslash paths" in docs/reference/services.md, and the game note dm/games/01006BB00C6F0000/README.md links to it. The same change is carried by NX2Apple as a patch: LNPANDY/NX2Apple#461.

Checked

  • Tests: make -C dm/host/tests GAME=01006BB00C6F0000 run on this branch (every host test, including the new t_fs_backslash, 26 checks: copies at the root, in folders and below an ordinary folder, no overwrite, case collision, a name above the root, committed save untouched before Commit, remount, an erased copy stays erased, a refused copy is retried); python3 -m unittest dm.tools.tests.test_docs. On NX2Apple's tree (the same change on top of its patches) also make ... asan (ASan and UBSan, every host test).
  • Games run, with title ID and version: Link's Awakening (01006BB00C6F0000, v131072), built with --platform mac from this change on top of NX2Apple's patches (pin cd859c3, not this branch's base). A save folder holding only \savedata0.bin, \savedata3.bin, \screenshot0.bin and \screenshot3.bin from a build before fs: backslash path separators and 8-byte aligned file handles #513 logs the four copies at mount and no red line, and the file select shows File 1 and the autosave with their old progress instead of "New game". After the game's Commit the committed save holds both names, the copies are byte for byte the old files, and the old entries carry the mark.
  • Devices and OS: M1 Pro, macOS 26.6.2. The iPhone was not run: it mounts saves through the same hle_fs.c (DM_SAVE).
  • regress.py check --games: not run. The change runs only at a save mount, and does nothing in a save that has no backslash in a name, which covers every game that never wrote save:\....
  • Not run: clang-tidy (no LLVM clang-tidy on this Mac).

Docs

  • The finding is in its game note or its docs/reference/<subsystem>.md entry, and python3 dm/tools/docs_symptoms.py was run (the symptom index)
  • A chain tool changed: dmsh_chain.py --header was run and dm/gfx/nvn/dmsh_chain.h is part of the change (no chain tool changed)
  • No game files, keys or data extracted from a game in the change or the attachments

🤖 Generated with Claude Code

…at mount

Builds before 7c23cc0 (backslashes separate path components, PR seedds#513) took a
backslash as part of a name, so a game that opens save:\savedata0.bin stored a
file named "\savedata0.bin". Since that change the host looks for
savedata0.bin, finds nothing, and the save looks lost: The Legend of Zelda:
Link's Awakening (01006BB00C6F0000) shows every file of its file select as
"New game" while the old files are still in the save folder.

At each transactional save mount, right after the work directory is cloned,
save_legacy_names clones each entry whose guest name has a backslash (a file,
or a directory with its tree) to the path that name names now, makes its
parent directories by exact guest name (a case collision gets an alias, as a
create does), and marks the old entry with an attribute so that this happens
once: a copy the game erases later stays erased. It never overwrites an entry
and keeps the old one, which a build before the change still reads. One log
line per copy; a name that leads above the save's root, or a copy the disk
refuses, logs a red line and is tried again at the next mount.

It works in the work directory only, so it rides the save transaction: the
copies reach the committed save with the game's own Commit, and a kill or a
remount before that leaves the committed save as it was and does the same
again. A lookup that falls back to the old name would have had to teach every
write, create, rename, delete and listing about it, and an erased file would
have come back.

Test: dm/host/tests/t_fs_backslash.c (make -C dm/host/tests fs-backslash).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JoseZamoraCifras added a commit to JoseZamoraCifras/NX2Apple that referenced this pull request Nov 1, 2026
The same change is open upstream as seedds/nxnative#904, and the patch's
Issue line names this pull request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant