fix(saves): a link to an entry that moved first follows it to the new place #2832
Closed
github-sync
wants to merge 2 commits from
github/pr/2832 into main
pull from: github/pr/2832
merge into: MagicBeanDev:main
MagicBeanDev:main
MagicBeanDev:github/pr/2893
MagicBeanDev:github/pr/2890
MagicBeanDev:github/pr/2887
MagicBeanDev:github/pr/2878
MagicBeanDev:github/pr/2875
MagicBeanDev:github/pr/2874
MagicBeanDev:github/pr/2873
MagicBeanDev:github/pr/2871
MagicBeanDev:github/pr/2756
MagicBeanDev:github/pr/2897
MagicBeanDev:github/pr/2896
MagicBeanDev:github/pr/2718
MagicBeanDev:github/pr/2842
MagicBeanDev:github/pr/2691
MagicBeanDev:github/pr/2895
MagicBeanDev:github/pr/2743
MagicBeanDev:github/pr/2765
MagicBeanDev:github/pr/2708
MagicBeanDev:github/pr/2732
MagicBeanDev:github/pr/2755
MagicBeanDev:github/pr/2817
MagicBeanDev:github/pr/2876
MagicBeanDev:release-please--branches--main
MagicBeanDev:release-please--branches--main--release-notes
MagicBeanDev:github/pr/2778
MagicBeanDev:github/pr/2888
MagicBeanDev:github/pr/2866
MagicBeanDev:github/pr/2877
MagicBeanDev:github/pr/2800
MagicBeanDev:github/pr/2829
MagicBeanDev:github/pr/2850
MagicBeanDev:github/pr/2889
MagicBeanDev:github/pr/2882
MagicBeanDev:github/pr/2860
MagicBeanDev:github/pr/2849
MagicBeanDev:github/pr/2886
MagicBeanDev:github/pr/2794
MagicBeanDev:github/pr/2840
MagicBeanDev:github/pr/2830
MagicBeanDev:github/pr/2834
MagicBeanDev:github/pr/2719
MagicBeanDev:github/pr/2742
MagicBeanDev:github/pr/2741
MagicBeanDev:github/pr/2820
MagicBeanDev:github/pr/2771
MagicBeanDev:github/pr/2859
MagicBeanDev:github/pr/2858
MagicBeanDev:github/pr/2823
MagicBeanDev:github/pr/2757
MagicBeanDev:github/pr/2769
MagicBeanDev:github/pr/2844
MagicBeanDev:github/pr/2843
MagicBeanDev:github/pr/2673
MagicBeanDev:github/pr/2855
MagicBeanDev:github/pr/2739
MagicBeanDev:github/pr/2807
MagicBeanDev:github/pr/2851
MagicBeanDev:github/pr/2789
MagicBeanDev:github/pr/2814
MagicBeanDev:github/pr/2818
MagicBeanDev:github/pr/2774
MagicBeanDev:github/pr/2772
MagicBeanDev:github/pr/2750
MagicBeanDev:github/pr/2786
MagicBeanDev:github/pr/2785
MagicBeanDev:github/pr/2782
MagicBeanDev:github/pr/2779
MagicBeanDev:github/pr/2775
MagicBeanDev:github/pr/2706
MagicBeanDev:github/pr/2766
MagicBeanDev:github/pr/2747
MagicBeanDev:github/pr/2759
MagicBeanDev:github/pr/2745
MagicBeanDev:github/pr/2740
MagicBeanDev:github/pr/2733
MagicBeanDev:github/pr/2725
MagicBeanDev:github/pr/2727
MagicBeanDev:github/pr/2728
MagicBeanDev:github/pr/2729
MagicBeanDev:github/pr/2721
MagicBeanDev:github/pr/2722
MagicBeanDev:github/pr/2715
MagicBeanDev:github/pr/2714
MagicBeanDev:github/pr/2707
MagicBeanDev:github/pr/2703
MagicBeanDev:github/pr/2705
MagicBeanDev:github/pr/2697
MagicBeanDev:github/pr/2679
MagicBeanDev:github/pr/2676
MagicBeanDev:github/pr/2669
MagicBeanDev:docs/what-hookshot-connects-to
MagicBeanDev:fix/hdr-encode-test
MagicBeanDev:fix/main-tests
MagicBeanDev:fix/play-cache-files
MagicBeanDev:perf/game-barrier-order
MagicBeanDev:feat/heap-lazy
MagicBeanDev:feat/texture-detail
MagicBeanDev:perf/astc-opaque-bc1
MagicBeanDev:fix/fs-dir-lock
MagicBeanDev:fix/framegen-native-kept
MagicBeanDev:fix/update-no-downgrade
MagicBeanDev:port/k10-gcn-f16-mirrors
MagicBeanDev:fix/framegen-mult-cap
MagicBeanDev:chore/release-as-2.1.1
MagicBeanDev:perf/pcache-load-switch
MagicBeanDev:fix/prepare-progress-log
MagicBeanDev:fix/update-closes-stale-game
MagicBeanDev:fix/hdr-scaled-sampled
MagicBeanDev:perf/first-start-preset
MagicBeanDev:perf/zero-upload
MagicBeanDev:fix/vcore-escape-2s
MagicBeanDev:test/fake-gh-atomic
MagicBeanDev:perf/batch-idle-flush
MagicBeanDev:test/finish-stub-a
MagicBeanDev:chore/record-2.1.0
MagicBeanDev:fix/main-red-tests
MagicBeanDev:fix/test-display-startup-driver-cache
MagicBeanDev:feat/mesh-shader-mode
MagicBeanDev:fix/menu-probes-at-startup
MagicBeanDev:fix/fps-counter-classic
MagicBeanDev:fix/test-log-batch-flake
MagicBeanDev:fix/narrow-screen-hud
MagicBeanDev:ci/prereleases
MagicBeanDev:release/v2.0.15
MagicBeanDev:hotfix/v2.0.15
MagicBeanDev:fix/present-ahead-replay
MagicBeanDev:release/v2.0.13
MagicBeanDev:fix/bracket-paths-2013
MagicBeanDev:fix/hdr-overlays-2013
MagicBeanDev:fix/fps-counter-scale-2013
MagicBeanDev:fix/stick-seen-at-rest
MagicBeanDev:hotfix/menu-fits-screen
MagicBeanDev:fix/f1-always-opens-menu
MagicBeanDev:fix/ini-keep-lines
MagicBeanDev:claude/cranky-pike-7cf247
MagicBeanDev:perf/texture-answer-flat
MagicBeanDev:fix/stamp-test-modules
MagicBeanDev:fix/appimage-fonts
MagicBeanDev:fix/display-awake
MagicBeanDev:release/v2.0.12
MagicBeanDev:fix/discord-windows
MagicBeanDev:rc/v2.0.12-tests
MagicBeanDev:fix/dlss-keep-feature
MagicBeanDev:rc/v2.0.12
MagicBeanDev:button-icons
No reviewers
Labels
Clear labels
accessibility
Barrier affecting people with disabilities
autorelease: pending
autorelease: tagged
bug
Something isn't working
dependencies
Pull requests that update a dependency file
documentation
Improvements or additions to documentation
duplicate
This issue or pull request already exists
enhancement
New feature or request
github_actions
Pull requests that update GitHub Actions code
good first issue
Good for newcomers
help wanted
Extra attention is needed
invalid
This doesn't seem right
question
Further information is requested
testing wanted
wontfix
This will not be worked on
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
Milestone
Clear milestone
No items
No milestone
Assignees
Clear assignees
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!2832
Loading…
Reference in a new issue
No description provided.
Delete branch "github/pr/2832"
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?
test_linux_saves_movefails now and then on CI's ubuntu part 1/2 ("a link to an entry that moves too: last names another folder", e.g. on #2822, which only touches release scripts), and the same bug can point a player's saves link at a folder that no longer exists after the first start on Linux.Cause. When
user/moves to$XDG_DATA_HOME/hookshot,copy_link(host/hos_main.c) makes each link again. A link to an entry that moves too is pointed at that entry's new place, but only whenrealpathresolves it. An absolute link (user/last -> <game>/user/save/<id>) listed aftersave/names nothing oncesave/has gone.realpaththen fails, the link's text is copied unchanged, and the new link names the old place, which is removed at the end of the move. Which entry comes first depends on the file system's directory order, so the check passes on most runners and fails on some.Fix. The link's text is checked against the folder on the move both as
realpathnames it and as given, afterreadlinktoo. Links written from where they stand and links that name nothing are unchanged.Test. The case "a link to an entry that moves too" now makes 36 more absolute links to
save/<id>(last-a...last-9), so on any directory order some are listed aftersave/. Without the fix, those name the old place. The check is Linux only (it skips on Windows), so the run on this PR's ubuntu job is the proof.python -I -B tests/run_all.pyon Windows: 419 passed; the 5 that fail (fpx_sticky, bake_portability, input_background, lift_cache, spawn_emit) fail onmainon this PC too.Overlap. #2317 moves this code (
copy_link,move_old/move_new) into the per-user data folder work and has the samerealpath-only check. Whichever merges second should carry the change across.Not tested in the game. The Linux move runs once, at a first start with a
user/beside the game, which can't be run on this PC (Windows). Wanted: someone on Linux with auser/folder holding an absolute symlink to a file inuser/save/to start the game once and check that the link in~/.local/share/hookshotpoints inside that folder (readlink).🤖 Generated with Claude Code
Thanks, this is a real bug and the fix is the right shape. I read it with "can this lose or duplicate a save" in mind, and I think it can't. This is a code read only: I did not build or run anything. Since it's a draft, this is a COMMENT.
The scenario, as I read it.
saves_dirmovesuser/entry by entry. An entry holding a link is copied and then deleted (copy_tree,delete_tree), andcopy_linkre-makes each link. Before: an absolute link listed after the entry it names gotrealpathfailing (the entry had already gone), so the link text was copied as written and the new link named the olduser/that is removed at the end. The saves themselves were safe in the new place; the player's link just dangled. After: the text is also checked againstmove_oldandmove_from(both need to start with/, and the next character must be a separator, souser2/isn't matched), and rewritten undermove_new. That covers a link to a link too, sincereadlinkgives the text of the first hop. I see no way for the old or new code to lose or overwrite a save from this: the saves are never touched bycopy_link, only the link text.Edge cases, checked against the code:
realpathfails,readlinkgives the text, it is copied as written; nothing follows it, so no endless loop.user/is now pointed under the new place. That is better than before, but the description says "links that name nothing are unchanged", which isn't quite true for that case. The test only has the relative../goneone.kept, nothing moves, the old folder stays. Not touched by this PR.keptand keeps the old folder (nothing lost). Not touched by this PR either.readlinkfails,copy_linkreturns 0, the copy is undone and the old place is kept. Safe.user/itself (target equal to the folder, no trailing separator) isn't rewritten, because oftarget[n] != SEP. I doubt anyone has one; mention only.The test. The extra links are the right idea, and CI on this head is green (ubuntu 1/2 and 2/2). But it still depends on the file system's order, so a green run is not proof the fix works: on a file system that lists newest-first (tmpfs, for instance), all 36 links come before
save/and the old code would pass too. It would be sturdier to not depend on the order: for example a case that removes or movessave/first on purpose, or a small check of the link-text path on its own. Not a blocker, but the reason for this PR is that the old test flaked, so it is worth a try. The edge cases above (a link outside, a loop, an absolute dangling link) are not covered either; one each would be cheap.Evidence (rule 3c). There is no real run, and the PR says so: the move runs once, on Linux, and the author is on Windows. For a fix that touches players' saves I would like a before/after listing from a Linux machine: a
user/with an absolute link intouser/save/<id>(and some names that sort both sides ofsave), thereadlinkof each link in~/.local/share/hookshotbefore the fix (names the old place) and after (names the new one), plus the saves still there. Anyone on Linux or SteamOS with a copy of thatuser/can do it in a few minutes.Overlap. #2317 (not a draft) deletes
move_old/move_new/copy_linkfrom this part ofhost/hos_main.cand does the move elsewhere, so the two will conflict inhos_main.c. If #2317 goes first, this fix needs porting to its version; if this goes first, #2317 needs rebasing and the new check carried across. The PR already says this. #2453, #1958 and #2842 don't touch these functions. The worklist row is fine.To take it out of draft: a Linux before/after listing as above (or a Linux tester's result), and ideally the order-independent test. The CI run is green and no conflicts with main today. With that I'd be happy to approve.
The Linux run asked for in T-BHEP, finished just after the merge: the fix holds on both file systems I could reach, and the old code fails on one of them every time. Tested at the head
aea86182(the merge commit itself was not built).Arch Linux, GCC 16.2 and Clang 23.1;
tests/test_linux_saves_move.py, 30 runs a cell, passes/failures:51da0464with only the new testWhy the base passes on tmpfs, which matters for the test: the directory order decides it, as the description says. Listed unsorted, btrfs gives
savebefore all 36 new links (creation order), so every link is read after its entry has gone and the old code fails each time. tmpfs gives the newest first, so all 36 links come beforesaveand the old code passes. So on a runner whose temporary folder lists newest-first, the 36 links do not exercise the bug. Making some of the links beforesave/is created and some after would put a late link on both orders; I can send that as a small test-only follow-up if you want it.Measured on the edge cases the review read from the code (same behaviour on the base unless said):
user/is now rewritten under the new folder (still dangling): the one behaviour change, as the review noted.savethrough another spelling of the parent (a symlinked parent folder, orgame//user/save) still names the removed folder whensavewent first.userfolder itself ends up naming the removed folder.Complete game-free suite on the head: 474 passed, 1 failed, 20 skipped; the one failure (the updater digest check, a missing .NET reference pack on this PC) fails on the base too.
Not tested: a real first start of the game that moves
user/, ext4, xfs, Windows.Thanks @mooki000 for the Linux run (T-BHEP) and @hckrman101 for the review. Both of you found the same weak spot: the test leans on the directory order, so a newest-first listing (tmpfs) passes on the old code. mooki000, yes please, send the test-only follow-up with some links made before
save/and some after; I won't start one, so we don't do it twice. A correction to the merged description, as the review says: an absolute link that already named nothing under the olduser/is now pointed under the new place (still dangling), so "links that name nothing are unchanged" holds only for the others. The link touser/itself (no trailing separator) is left as it was before; noted, not changed.Opened as #2888: half the links are made before
save/and half after, so the old code now fails on tmpfs as well as btrfs (30 of 30 in every cell); main passes everywhere.