package nativepath import ( "os" "os/exec" "path/filepath" "strings" "testing" "gotest.tools/v3/assert" ) func TestIsSymlinkOrReparsePoint(t *testing.T) { t.Parallel() tmp := t.TempDir() t.Run("regular file", func(t *testing.T) { t.Parallel() file := filepath.Join(tmp, "regular.txt") assert.NilError(t, os.WriteFile(file, []byte("hello"), 0o666)) assert.Equal(t, IsSymlinkOrReparsePoint(file), false) }) t.Run("regular directory", func(t *testing.T) { t.Parallel() dir := filepath.Join(tmp, "regular-dir") assert.NilError(t, os.MkdirAll(dir, 0o777)) assert.Equal(t, IsSymlinkOrReparsePoint(dir), false) }) t.Run("junction point", func(t *testing.T) { t.Parallel() target := filepath.Join(tmp, "junction-target") link := filepath.Join(tmp, "junction-link") assert.NilError(t, os.MkdirAll(target, 0o777)) mklink(t, target, link, true) assert.Equal(t, IsSymlinkOrReparsePoint(link), true) }) t.Run("file symlink", func(t *testing.T) { t.Parallel() target := filepath.Join(tmp, "symlink-target.txt") link := filepath.Join(tmp, "symlink-link.txt") assert.NilError(t, os.WriteFile(target, []byte("hello"), 0o666)) mklink(t, target, link, false) assert.Equal(t, IsSymlinkOrReparsePoint(link), true) }) t.Run("directory symlink", func(t *testing.T) { t.Parallel() target := filepath.Join(tmp, "dir-symlink-target") link := filepath.Join(tmp, "dir-symlink-link") assert.NilError(t, os.MkdirAll(target, 0o777)) mklink(t, target, link, false) assert.Equal(t, IsSymlinkOrReparsePoint(link), true) }) t.Run("nonexistent path", func(t *testing.T) { t.Parallel() nonexistent := filepath.Join(tmp, "does-not-exist") assert.Equal(t, IsSymlinkOrReparsePoint(nonexistent), false) }) t.Run("empty path", func(t *testing.T) { t.Parallel() assert.Equal(t, IsSymlinkOrReparsePoint(""), false) }) t.Run("invalid path with null byte", func(t *testing.T) { t.Parallel() assert.Equal(t, IsSymlinkOrReparsePoint("invalid\x00path"), false) }) } func TestIsSymlinkOrReparsePointLongPath(t *testing.T) { t.Parallel() tmp := t.TempDir() // Create a deeply nested path that exceeds 248 characters longPathBase := tmp pathComponent := "very_long_directory_name_to_exceed_max_path_limit_abcdefghijklmnopqrstuvwxyz" for len(longPathBase) < 250 { longPathBase = filepath.Join(longPathBase, pathComponent) } target := filepath.Join(longPathBase, "target") link := filepath.Join(longPathBase, "link") // Use \\?\ prefix to enable long path support for mklink longTarget := `\\?\` + target longLink := `\\?\` + link assert.NilError(t, os.MkdirAll(longTarget, 0o777)) assert.NilError(t, exec.Command("cmd", "/c", "mklink", "/J", longLink, longTarget).Run()) // With long path support enabled, this should work even for paths >= 248 chars assert.Equal(t, IsSymlinkOrReparsePoint(link), true) } func TestIsSymlinkOrReparsePointNestedInSymlink(t *testing.T) { t.Parallel() tmp := t.TempDir() // Create a structure: target/inner-target, link -> target, then check link/inner-link target := filepath.Join(tmp, "target") innerTarget := filepath.Join(target, "inner-target") assert.NilError(t, os.MkdirAll(innerTarget, 0o777)) link := filepath.Join(tmp, "link") mklink(t, target, link, true) // Create a junction inside the target innerLink := filepath.Join(target, "inner-link") mklink(t, innerTarget, innerLink, true) // Check the junction through the symlink path nestedPath := filepath.Join(link, "inner-link") assert.Equal(t, IsSymlinkOrReparsePoint(nestedPath), true) } func TestIsSymlinkOrReparsePointRelativePath(t *testing.T) { //nolint:paralleltest // Cannot use t.Parallel() with t.Chdir() tmp := t.TempDir() t.Chdir(tmp) target := "target-rel" link := "link-rel" assert.NilError(t, os.MkdirAll(target, 0o777)) mklink(t, target, link, true) assert.Equal(t, IsSymlinkOrReparsePoint(link), true) assert.Equal(t, IsSymlinkOrReparsePoint(target), false) } func BenchmarkIsSymlinkOrJunction(b *testing.B) { tmp := b.TempDir() regularFile := filepath.Join(tmp, "regular.txt") assert.NilError(b, os.WriteFile(regularFile, []byte("hello"), 0o666)) target := filepath.Join(tmp, "target") link := filepath.Join(tmp, "link") assert.NilError(b, os.MkdirAll(target, 0o777)) assert.NilError(b, exec.Command("cmd", "/c", "mklink", "/J", link, target).Run()) b.Run("regular file", func(b *testing.B) { b.ReportAllocs() for b.Loop() { IsSymlinkOrReparsePoint(regularFile) } }) b.Run("junction", func(b *testing.B) { b.ReportAllocs() for b.Loop() { IsSymlinkOrReparsePoint(link) } }) b.Run("nonexistent", func(b *testing.B) { b.ReportAllocs() nonexistent := filepath.Join(tmp, "does-not-exist") for b.Loop() { IsSymlinkOrReparsePoint(nonexistent) } }) } func mklink(tb testing.TB, target, link string, isDir bool) { tb.Helper() if isDir { assert.NilError(tb, exec.Command("cmd", "/c", "mklink", "/J", link, target).Run()) } else { err := os.Symlink(target, link) if err != nil && strings.Contains(err.Error(), "A required privilege is not held by the client") { tb.Log(err) tb.Skip("file symlink support is not enabled without elevation or developer mode") } assert.NilError(tb, err) } }