src/internal/syscall/windows/at_windows.go | 2 +- src/os/root_openat.go | 10 +++++----- src/os/root_test.go | 135 ++++++++++++++++++++++++++++++++++++----------------- src/os/root_unix.go | 4 ++++ src/os/root_windows.go | 10 ++++++++-- src/os/root_windows_test.go | 18 ++++++++++++++++++ diff --git a/src/internal/syscall/windows/at_windows.go b/src/internal/syscall/windows/at_windows.go index ecae18874799730f8951c4f3470c207140e2acee..1939e985764660304373883c77f8c53945af7557 100644 --- a/src/internal/syscall/windows/at_windows.go +++ b/src/internal/syscall/windows/at_windows.go @@ -298,7 +298,7 @@ nil, syscall.FILE_ATTRIBUTE_NORMAL, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, FILE_CREATE, - FILE_DIRECTORY_FILE, + FILE_DIRECTORY_FILE|FILE_OPEN_REPARSE_POINT, nil, 0, ) diff --git a/src/os/root_openat.go b/src/os/root_openat.go index 62e6be665a9233ebf679145d0e0dfe277222491b..ee786176ce1fb66935f78a7ae87db365d8085619 100644 --- a/src/os/root_openat.go +++ b/src/os/root_openat.go @@ -153,13 +153,13 @@ // openLastComponentFunc opens the last path component. openLastComponentFunc := func(parent sysfdType, name string, endsInSlash bool) (struct{}, error) { err := mkdirat(parent, name, perm) if err == syscall.EEXIST { - mode, e := modeAt(parent, name) + fi, e := lstatat(parent, name) if e == nil { - if mode.IsDir() { + if fi.Mode().IsDir() { // The target of MkdirAll is an existing directory. err = nil - } else if mode&ModeSymlink != 0 { - // The target of MkdirAll is a symlink. + } else if isLink(fi) { + // The target of MkdirAll is a symlink or junction. // For consistency with os.MkdirAll, // succeed if the link resolves to a directory. // We don't return errSymlink here, because we don't @@ -420,7 +420,7 @@ err = nil case err != nil: return case fi.Mode().Type() == fs.ModeDir: - case fi.Mode().Type() == fs.ModeSymlink: + case isLink(fi): if runtime.GOOS != "windows" || flags&doInRootAlwaysResolveTerminalSlash != 0 { err = checkSymlink(dirfd, parts[i], syscall.ENOTDIR) } else { diff --git a/src/os/root_test.go b/src/os/root_test.go index 92cbe8aa144d70ec3c245c561043671467108063..936c6d2f307d00b868a30849a1cd89b3fa3e7f62 100644 --- a/src/os/root_test.go +++ b/src/os/root_test.go @@ -2166,7 +2166,7 @@ isSimple := func(desc testFileDesc) bool { if desc.ref.template != "BASE" { return false } - if desc.kind == testFileSymlink && desc.target.ref.template != "BASE" { + if desc.kind.isLink() && desc.target.ref.template != "BASE" { return false } return true @@ -2370,11 +2370,12 @@ // testFileKind is a kind of file. type testFileKind int const ( - testFileUnused = testFileKind(iota) - testFileAbsent // file does not exist - testFileFile // regular file - testFileDir // directory - testFileSymlink // symlink + testFileUnused = testFileKind(iota) + testFileAbsent // file does not exist + testFileFile // regular file + testFileDir // directory + testFileSymlink // symlink + testFileJunction // Windows directory junction testFileMax // testFileError represents a path which fails during resolution, @@ -2394,11 +2395,17 @@ case testFileDir: return "dir" case testFileSymlink: return "symlink" + case testFileJunction: + return "junction" case testFileError: return "error" default: return fmt.Sprintf("testFileKind(%d)", kind) } +} + +func (kind testFileKind) isLink() bool { + return kind == testFileSymlink || kind == testFileJunction } // testFileRef is a kind of reference to a file. @@ -2467,11 +2474,13 @@ // - "name/", where name is a symlink to "DIR/../target/", where target is a plain file. type testFileDesc struct { kind testFileKind ref testFileRef - target *testFileDesc // symlink target, nil when kind is not testFileSymlink + target *testFileDesc // symlink or junction target, nil when !kind.isLink() } var rootComprehensive = flag.Bool("root_comprehensive", false, "run many more os.Root test variations (slow, uncertain value)") + +var createJunction func(t *testing.T, link, target string) // allTestFileDescs returns an iterator over all the testFileDescs we use in tests. func allTestFileDescs() iter.Seq[testFileDesc] { @@ -2481,7 +2490,7 @@ // // When the kind is symlink, the desc contains a reference type and file kind for // the link target as well. We only exercise one level of symlink (although we // could do more), so this means a testFileDesc effectively contains four axes of - // variation: ref, kind, symlink ref, symlink kind. + // variation: ref, kind, link ref, link kind. // // For example: // @@ -2495,9 +2504,10 @@ // It is feasible to test every possible variation of these four axes, // but this is quite a few tests and gets quite slow. So by default we exclude // some variations. We test: // - // - every reference to every kind, except symlink - // - direct and direct/ references to a symlink to every reference to a file - // - a direct reference to a symlink to a direct reference to every kind (except file) + // - direct and direct/ references to a symlink or junction + // to every reference to a file + // - a direct reference to a symlink or junction + // to a direct reference to every kind (except file) // // The full set of variations may be enabled with the -comprehensive_root_tests flag. @@ -2505,7 +2515,7 @@ return func(yield func(testFileDesc) bool) { // Every type of reference to every type of file, except symlink. for _, ref := range testFileRefs { for kind := range testFileMax { - if kind == testFileUnused || kind == testFileSymlink { + if kind == testFileUnused || kind.isLink() { continue } desc := testFileDesc{ @@ -2523,27 +2533,32 @@ refs := testFileRefs if !*rootComprehensive { refs = testFileLimitedRefs } - for _, ref := range refs { - for linkKind := range testFileMax { - if linkKind == testFileUnused || linkKind == testFileSymlink { - continue - } + for _, kind := range []testFileKind{testFileSymlink, testFileJunction} { + if kind == testFileJunction && runtime.GOOS != "windows" { + continue + } + for _, ref := range refs { + for linkKind := range testFileMax { + if linkKind == testFileUnused || linkKind.isLink() { + continue + } - linkRefs := testFileRefs - if !*rootComprehensive && linkKind != testFileFile && linkKind != testFileDir { - linkRefs = testFileLimitedRefs - } - for _, linkRef := range linkRefs { - desc := testFileDesc{ - kind: testFileSymlink, - ref: ref, - target: &testFileDesc{ - kind: linkKind, - ref: linkRef, - }, + linkRefs := testFileRefs + if !*rootComprehensive && linkKind != testFileFile && linkKind != testFileDir { + linkRefs = testFileLimitedRefs } - if !yield(desc) { - return + for _, linkRef := range linkRefs { + desc := testFileDesc{ + kind: kind, + ref: ref, + target: &testFileDesc{ + kind: linkKind, + ref: linkRef, + }, + } + if !yield(desc) { + return + } } } } @@ -2564,7 +2579,7 @@ // // So, open "file1/", where file1 is a symlink to "DIR/../file2", where file2 is a directory. func (desc testFileDesc) String() string { s := desc.ref.name + strings.ToUpper(desc.kind.String()[:1]) - if desc.kind == testFileSymlink { + if desc.kind.isLink() { s += desc.target.String() } return s @@ -2579,6 +2594,9 @@ } if desc.kind == testFileSymlink { return desc.target.escapes() } + if desc.kind == testFileJunction { + return true + } return false } @@ -2592,7 +2610,7 @@ // symlinks in the last path component to be resolved. // On Windows, a trailing slash does not cause symlink resolution. return false } - if desc.ref.hasSlashSuffix() && desc.kind == testFileSymlink { + if desc.ref.hasSlashSuffix() && desc.kind.isLink() { return desc.target.escapes() } return false @@ -2600,7 +2618,7 @@ } // finalKind reports the kind of the file after following all symlinks. func (desc testFileDesc) finalKind() testFileKind { - if desc.kind == testFileSymlink { + if desc.kind.isLink() { return desc.target.finalKind() } return desc.kind @@ -2609,11 +2627,11 @@ func (desc testFileDesc) lfinalKind() testFileKind { switch runtime.GOOS { case "windows": - if desc.ref.hasSlashSuffix() && desc.kind == testFileSymlink && desc.target.kind != testFileDir { + if desc.ref.hasSlashSuffix() && desc.kind.isLink() && desc.target.kind != testFileDir { return testFileError } default: - if desc.ref.hasSlashSuffix() && desc.kind == testFileSymlink { + if desc.ref.hasSlashSuffix() && desc.kind.isLink() { return desc.target.finalKind() } } @@ -2640,6 +2658,11 @@ if runtime.GOOS == "windows" && hasSuffix && desc.target.kind != testFileDir { return true } return isError(*desc.target, hasSuffix) + case testFileJunction: + if desc.target.kind == testFileFile || (hasSuffix && desc.target.kind != testFileDir) { + return true + } + return isError(*desc.target, hasSuffix) default: return hasSuffix } @@ -2648,11 +2671,16 @@ return isError(desc, false) } func (desc testFileDesc) isSymlinkToDir() bool { - if desc.kind != testFileSymlink { + if !desc.kind.isLink() { return false } if desc.ref.escapes { return false + } + if desc.kind == testFileJunction { + // Windows junctions are always directory links, regardless of what the + // target of the junction might be. + return true } if desc.finalKind() == testFileDir { return true @@ -2668,7 +2696,7 @@ name := desc.ref.template if len(name) > 0 && os.IsPathSeparator(name[len(name)-1]) { return true } - if desc.kind == testFileSymlink { + if desc.kind.isLink() { return desc.target.anySlashSuffix() } return false @@ -2723,6 +2751,14 @@ fi = desc.target.create(t, dir, "s_"+base, token) if err := os.Symlink(linktarget, path); err != nil { t.Fatal(err) } + case testFileJunction: + // Directory junction. We create a target named "s_"+base. + if runtime.GOOS != "windows" { + t.Skip("junctions not supported on " + runtime.GOOS) + } + linktarget := desc.target.ref.path(dir, "s_"+base) + fi = desc.target.create(t, dir, "s_"+base, token) + createJunction(t, path, linktarget) default: t.Fatalf("can't create file of kind: %v", desc.kind) } @@ -2783,7 +2819,7 @@ ent := info.Mode().String() + " " + path switch d.Type() { case fs.ModeDir: ent += "/" - case fs.ModeSymlink: + case fs.ModeSymlink, fs.ModeIrregular: target, err := root.Readlink(path) if err != nil { t.Fatal(err) @@ -2850,6 +2886,8 @@ // (Note that most tests use only a target file, no source.) got := test.describeFile(t, f) switch { + case test.target.isError(): + test.wantError(t, gotErr, errAny) case test.root != nil && test.target.escapes(): // The operation escapes the root. test.wantError(t, gotErr, os.ErrPathEscapes) @@ -3017,7 +3055,7 @@ case test.root != nil && test.source.lescapes(): test.wantError(t, gotErr, os.ErrPathEscapes) case test.source.lfinalKind() == testFileAbsent: test.wantError(t, gotErr, errAny) - case test.source.kind == testFileSymlink: + case test.source.kind.isLink(): // os.Link(old, new) may or may not deference old when it is a symlink. // POSIX says that link(2) should deference the source, but implementations // are inconsistent. @@ -3066,6 +3104,11 @@ test.wantError(t, gotErr, nil) if got, want := gotStat.Mode().Type(), fs.ModeSymlink; got != want { test.errorf(t, "got mode %v, want %v", got, want) } + case finalKind == testFileJunction: + test.wantError(t, gotErr, nil) + if got, want := gotStat.Mode().Type(), fs.ModeIrregular; got != want { + test.errorf(t, "got mode %v, want %v", got, want) + } case gotErr != nil: default: if !os.SameFile(gotStat, test.targetInfo) { @@ -3095,7 +3138,7 @@ switch { case test.root != nil && test.target.ref.escapes: // "mkdir ../target", or equivalent escaping path. test.wantError(t, gotErr, os.ErrPathEscapes) - case test.target.slashSuffix() && test.target.kind == testFileSymlink: + case test.target.slashSuffix() && test.target.kind.isLink(): // "mkdir symlink/", inconsistent behavior across platforms // as to whether this follows the symlink or not. // @@ -3160,7 +3203,7 @@ case test.root != nil && test.target.escapes(): // "mkdir ../target", or equivalent escaping path. test.wantError(t, gotErr, errAny) return "", errSkipRootConsistencyCheck - case test.root != nil && test.target.kind == testFileSymlink && test.target.target.kind == testFileAbsent && targetPath != test.targetPath: + case test.root != nil && test.target.kind.isLink() && test.target.target.kind == testFileAbsent && targetPath != test.targetPath: // A minor inconsistency between Root.MkdirAll and os.MkdirAll: // When an intermediate component of the tree being constructed is a // dangling symlink, Root.MkdirAll will follow the symlink and create @@ -3194,7 +3237,7 @@ test.setOp("Rename(%q, %q)", test.sourcePath, test.targetPath) gotErr := rename(test.sourcePath, test.targetPath) if runtime.GOOS == "windows" && - (test.source.finalKind() != test.target.finalKind() || test.source.kind == testFileSymlink || test.target.kind == testFileSymlink) { + (test.source.finalKind() != test.target.finalKind() || test.source.kind.isLink() || test.target.kind.isLink()) { // os.Rename on Windows is implemented using MoveFileEx, // while Root.Rename is implemented using NtSetInformationFileEx // with an explicit request for POSIX semantics. @@ -3259,6 +3302,8 @@ got = string(data) } switch { + case test.target.isError(): + test.wantError(t, gotErr, errAny) case test.root != nil && test.target.escapes(): test.wantError(t, gotErr, os.ErrPathEscapes) case test.target.finalKind() == testFileAbsent: @@ -3417,7 +3462,7 @@ switch { case test.root != nil && test.target.lescapes(): test.wantError(t, gotErr, os.ErrPathEscapes) - case test.target.kind != testFileSymlink: + case !test.target.kind.isLink(): test.wantError(t, gotErr, errAny) case test.target.anySlashSuffix(): default: @@ -3470,6 +3515,8 @@ got := test.describeFile(t, f) switch { + case test.target.isError(): + test.wantError(t, gotErr, errAny) case test.root != nil && test.target.escapes(): test.wantError(t, gotErr, os.ErrPathEscapes) case test.target.finalKind() == testFileAbsent: diff --git a/src/os/root_unix.go b/src/os/root_unix.go index e88058715387b3984c9024f90a81ac7380b02e32..36db082e4f36d83b47cdb8d35d7ef8895f788ba5 100644 --- a/src/os/root_unix.go +++ b/src/os/root_unix.go @@ -306,6 +306,10 @@ } } } +func isLink(fi FileInfo) bool { + return fi.Mode()&ModeSymlink != 0 +} + // isDirectoryLink always returns false, because Unix systems don't have separate // symlink types for files and directories. // (See the Windows version of this function for more details.) diff --git a/src/os/root_windows.go b/src/os/root_windows.go index 6bd37c50a1fc5be31070bce2a1343474a7d07442..2c156bc63ac4eb5e892e43bdb839f9e066b91dd7 100644 --- a/src/os/root_windows.go +++ b/src/os/root_windows.go @@ -421,8 +421,14 @@ } return fi, nil } -// isDirectoryLink reports whether fi (assumed to be a symlink) is a directory link. -// Windows symlinks come in two flavors: file and directory. This function distinguishes +// isLink reports whether fi is a symlink or other surrogate reparse point (such as a junction). +func isLink(fi FileInfo) bool { + fs, ok := fi.(*fileStat) + return ok && fs.isReparseTagNameSurrogate() +} + +// isDirectoryLink reports whether fi (assumed to be a link) is a directory link. +// Windows links come in two flavors: file and directory. This function distinguishes // between the two. func isDirectoryLink(fi FileInfo) bool { fs, ok := fi.(*fileStat) diff --git a/src/os/root_windows_test.go b/src/os/root_windows_test.go index 30eda63c7cf64efb8a22dd9449a7689830b0b4f0..9397ad9880663d3954e1e707244d6e30715c2b87 100644 --- a/src/os/root_windows_test.go +++ b/src/os/root_windows_test.go @@ -18,6 +18,24 @@ "testing" "unsafe" ) +func init() { + createJunction = func(t *testing.T, link, target string) { + if !filepath.IsAbs(target) { + target = filepath.Dir(link) + `\` + target + } + target, err := syscall.FullPath(target) + if err != nil { + t.Fatal(err) + } + var rd reparseData + rd.addSubstituteName(`\??\` + target) + rd.addPrintName(target) + if err := createMountPoint(link, &rd); err != nil { + t.Fatal(err) + } + } +} + func TestRootOpenatFallback(t *testing.T) { windows.TestOpenatFallback = true t.Cleanup(func() { windows.TestOpenatFallback = false })