fix(saves): the Linux saves' move leaves a linked folder and its saves alone #2282
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!2282
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/linux-saves-move-links"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
On Linux, the one-time move of the saves to
~/.local/share/hookshot(7eeb91a) can empty a folder outside the game folder and leave the game showing no saves, whenuser/holds a symbolic link (saves kept in a synced folder or on another drive).What happens
saves_dirmoves each entry ofuser/with a rename, or a copy and delete when the rename fails. A link is treated as the folder it names:delete_treedeletes everything in the folder the link names. That folder is the player's, not Hookshot's. This start still finds the saves, in the copy. The link itself is left inuser/, so the next start seessavein both places, keeps the old place for good, and reads the folder it emptied: no saves.../../sync) names nothing from~/.local/share/hookshot: no saves, and none can be written.Also in the same loop: when the new place already has one of the entries, the old place is kept, but the entries looked at before that one have already gone, and the game runs in the old place without them.
The change
delete_treeremoves a link without entering it, asnn_fs.c's does since the DeleteDirectoryRecursively fix.user/that is itself a link is the player's choice of place: used as it is, nothing moved.Windows is not touched (the code is under
#ifndef _WIN32).Tested
tests/test_linux_saves_move.py(new) buildssaves_dirand what it calls fromhos_main.cas it is, overhost/posix/win32_posix.c, and starts it twice on folders of its own: a link written in full, one written relative, one deeper in (user/save/<user id>), one to an entry that moves too, one that names nothing, a linkeduser/, a new place that has an entry already, and a plainuser/; with the game on another drive too (/dev/shmagainst the temp folder) where the PC has one.main: 12 of its checks fail in 6 of the cases, among them "the linked folder lost its save" and "start 2 did not use the new place".python -I -B tests/run_all.py: nothing new fails.Not tested in a built game: I checked the function as the test builds it (gcc,
-D_GNU_SOURCEasnx2/build.pypasses), not a full Linux build of the game.This fixes a real data-loss path, and the fix is careful.
delete_treeno longer follows a link, so the cross-drive copy can't empty the player's synced folder any more. Links are recreated with their resolved target, and the look-then-move pass stops the half-moved state when the new place already has an entry. I readcopy_link,has_linkand the reworkedsaves_dirand found nothing blocking:has_linkandcopy_treetestis_linkbeforeis_dir, so neither enters a linked folder (no loops, no walking into the player's data). Dangling links are handled too, sinceis_diris false for them anddelete_treeunlinks them.delete_tree(to)) is now safe as well. Without theis_linkcheck there, it would have walked into the linkscopy_linkhad just made.realpathintochar[MAX_PATH]is fine on Linux becausehost/posix/windows.hdefinesMAX_PATHas 4096.test_linux_saves_move,test_linux_update_skipandtest_oot_savesall pass.minor
host/hos_main.c:272-273,373:move_oldis left pointing atsaves_dir's stack bufferrealafter the function returns. Today onlysaves_dirreachescopy_tree/copy_link, so nothing reads it, but the next caller ofcopy_treethat meets a link would read a dead stack pointer. Resettingmove_old = move_new = NULLbeforesaves_dirreturns (or passing them down as parameters) closes that off.minor (it predates this PR, but this PR's comment now promises otherwise)
host/hos_main.c:386-388: in the moving pass, a copy that fails doeskept++but thedo … whilekeeps moving the remaining entries, and the function then returns the old place without them. That's the same split the new first pass was added to prevent, just reached through a failure (a full disk, a read-only file) instead of a collision. Stopping the inner loop oncekeptis set would at least stop the damage spreading. Fully undoing the earlier moves can wait for a later PR.coordination: #2317 (the per-user data folder) moves
saves_dirand its helpers intohost/hos_data.c, and the two branches conflict. Whichever merges second has to carry the link handling over, together withtest_linux_saves_move.py, which cuts the functions out ofhos_main.cby text and would need to point at the new file. Worth agreeing on the order with @LNDF. Theappimagefailure on this run is unrelated: the mirror bootstrap timed out.This fixes real data loss:
delete_treecould empty a linked sync folder.is_linknow comes beforeis_dirincopy_tree,has_linkanddelete_tree, and the test passes, including the other-drive case. The appimage failure is infrastructure.Two minor points, both optional:
host/hos_main.c:373leavesmove_oldpointing atsaves_dir's stack buffer after return. It's harmless today, and a one-line reset fixes it.:388, which predates this PR: a failed copy doeskept++but keeps moving the other entries, so on a full disk saves can still end up split between the two places.This should merge before #2317, which moves
saves_dirintohos_data.cand should carry the link handling with it.github-sync referenced this pull request2026-11-12 06:22:54 +00:00