Skip to content

Commit adec637

Browse files
committed
fix(sync): prevent infinite reformat loop on Windows junction fallback
On Windows without Developer Mode, createLink falls back to an NTFS junction (always absolute). linkNeedsReformat would then return true on every sync, causing a remove→recreate loop. Add build-tagged canCreateRelativeLink(): returns true on Unix, and on Windows probes once with sync.Once by creating a test relative symlink in a temp dir. linkNeedsReformat skips reformat when the platform cannot create relative links. Also use filepath.Clean instead of filepath.Abs in resolveReadlink (input is already absolute, avoids unnecessary os.Getwd). Addresses PR #122 review feedback.
1 parent de0291c commit adec637

File tree

3 files changed

+35
-0
lines changed

3 files changed

+35
-0
lines changed

internal/sync/relative.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,11 @@ func linkNeedsReformat(dest string, wantRelative bool) bool {
4242
if dest == "" {
4343
return false
4444
}
45+
if wantRelative && !canCreateRelativeLink() {
46+
// Platform cannot create relative symlinks (e.g. Windows without
47+
// Developer Mode falls back to junctions, which are always absolute).
48+
// Skip reformat to avoid remove→recreate loop on every sync.
49+
return false
50+
}
4551
return wantRelative == filepath.IsAbs(dest)
4652
}

internal/sync/symlink_unix.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ func createLink(linkPath, sourcePath string, relative bool) error {
2121
return os.Symlink(target, linkPath)
2222
}
2323

24+
// canCreateRelativeLink returns true on Unix where os.Symlink always works.
25+
func canCreateRelativeLink() bool { return true }
26+
2427
// isJunctionOrSymlink checks if path is a symlink
2528
func isJunctionOrSymlink(path string) bool {
2629
info, err := os.Lstat(path)

internal/sync/symlink_windows.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"os/exec"
1111
"path/filepath"
1212
"strings"
13+
gosync "sync"
1314
)
1415

1516
// createLink creates a directory junction on Windows (no admin required).
@@ -73,6 +74,31 @@ func createLink(linkPath, sourcePath string, relative bool) error {
7374
return errors.New(errMsg)
7475
}
7576

77+
// canCreateRelativeLink probes whether the OS can create relative symlinks.
78+
// On Windows this requires Developer Mode; without it createLink falls back
79+
// to junctions which are always absolute.
80+
var relativeProbe struct {
81+
once gosync.Once
82+
ok bool
83+
}
84+
85+
func canCreateRelativeLink() bool {
86+
relativeProbe.once.Do(func() {
87+
dir, err := os.MkdirTemp("", "ss-relprobe-*")
88+
if err != nil {
89+
return
90+
}
91+
defer os.RemoveAll(dir)
92+
target := filepath.Join(dir, "t")
93+
if err := os.Mkdir(target, 0755); err != nil {
94+
return
95+
}
96+
link := filepath.Join(dir, "l")
97+
relativeProbe.ok = os.Symlink("t", link) == nil
98+
})
99+
return relativeProbe.ok
100+
}
101+
76102
// isJunctionOrSymlink checks if path is a junction or symlink
77103
func isJunctionOrSymlink(path string) bool {
78104
info, err := os.Lstat(path)

0 commit comments

Comments
 (0)