From bb34112d4df7b5dfd12fc83b8d1305631a7b8708 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Mon, 4 Dec 2023 15:32:01 -0500 Subject: [PATCH] os: document Readlink behavior for relative links Also provide a runnable example to illustrate that behavior. This should help users to avoid the common mistake of expecting os.Readlink to return an absolute path. Fixes #57766. Change-Id: I8f60aa111ebda0cae985758615019aaf26d5cb41 Reviewed-on: https://go-review.googlesource.com/c/go/+/546995 Auto-Submit: Bryan Mills LUCI-TryBot-Result: Go LUCI Reviewed-by: Carlos Amedee Reviewed-by: Rob Pike --- src/os/example_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++ src/os/file.go | 9 +++++++ src/os/file_plan9.go | 4 +--- src/os/file_unix.go | 4 +--- src/os/file_windows.go | 8 +++---- 5 files changed, 68 insertions(+), 11 deletions(-) diff --git a/src/os/example_test.go b/src/os/example_test.go index e9657ed1fc..656232c472 100644 --- a/src/os/example_test.go +++ b/src/os/example_test.go @@ -263,3 +263,57 @@ func ExampleMkdirAll() { log.Fatal(err) } } + +func ExampleReadlink() { + // First, we create a relative symlink to a file. + d, err := os.MkdirTemp("", "") + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(d) + targetPath := filepath.Join(d, "hello.txt") + if err := os.WriteFile(targetPath, []byte("Hello, Gophers!"), 0644); err != nil { + log.Fatal(err) + } + linkPath := filepath.Join(d, "hello.link") + if err := os.Symlink("hello.txt", filepath.Join(d, "hello.link")); err != nil { + if errors.Is(err, errors.ErrUnsupported) { + // Allow the example to run on platforms that do not support symbolic links. + fmt.Printf("%s links to %s\n", filepath.Base(linkPath), "hello.txt") + return + } + log.Fatal(err) + } + + // Readlink returns the relative path as passed to os.Symlink. + dst, err := os.Readlink(linkPath) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s links to %s\n", filepath.Base(linkPath), dst) + + var dstAbs string + if filepath.IsAbs(dst) { + dstAbs = dst + } else { + // Symlink targets are relative to the directory containing the link. + dstAbs = filepath.Join(filepath.Dir(linkPath), dst) + } + + // Check that the target is correct by comparing it with os.Stat + // on the original target path. + dstInfo, err := os.Stat(dstAbs) + if err != nil { + log.Fatal(err) + } + targetInfo, err := os.Stat(targetPath) + if err != nil { + log.Fatal(err) + } + if !os.SameFile(dstInfo, targetInfo) { + log.Fatalf("link destination (%s) is not the same file as %s", dstAbs, targetPath) + } + + // Output: + // hello.link links to hello.txt +} diff --git a/src/os/file.go b/src/os/file.go index 6fd0550eeb..090ffba4dc 100644 --- a/src/os/file.go +++ b/src/os/file.go @@ -392,6 +392,15 @@ func Rename(oldpath, newpath string) error { return rename(oldpath, newpath) } +// Readlink returns the destination of the named symbolic link. +// If there is an error, it will be of type *PathError. +// +// If the link destination is relative, Readlink returns the relative path +// without resolving it to an absolute one. +func Readlink(name string) (string, error) { + return readlink(name) +} + // Many functions in package syscall return a count of -1 instead of 0. // Using fixCount(call()) instead of call() corrects the count. func fixCount(n int, err error) (int, error) { diff --git a/src/os/file_plan9.go b/src/os/file_plan9.go index 03cdb5be4a..4cab2d4cdf 100644 --- a/src/os/file_plan9.go +++ b/src/os/file_plan9.go @@ -505,9 +505,7 @@ func Symlink(oldname, newname string) error { return &LinkError{"symlink", oldname, newname, syscall.EPLAN9} } -// Readlink returns the destination of the named symbolic link. -// If there is an error, it will be of type *PathError. -func Readlink(name string) (string, error) { +func readlink(name string) (string, error) { return "", &PathError{Op: "readlink", Path: name, Err: syscall.EPLAN9} } diff --git a/src/os/file_unix.go b/src/os/file_unix.go index 533a48404b..a527b23e4f 100644 --- a/src/os/file_unix.go +++ b/src/os/file_unix.go @@ -426,9 +426,7 @@ func Symlink(oldname, newname string) error { return nil } -// Readlink returns the destination of the named symbolic link. -// If there is an error, it will be of type *PathError. -func Readlink(name string) (string, error) { +func readlink(name string) (string, error) { for len := 128; ; len *= 2 { b := make([]byte, len) var ( diff --git a/src/os/file_windows.go b/src/os/file_windows.go index 63d53a1df8..8b04ed6e47 100644 --- a/src/os/file_windows.go +++ b/src/os/file_windows.go @@ -406,7 +406,7 @@ func normaliseLinkPath(path string) (string, error) { return "", errors.New("GetFinalPathNameByHandle returned unexpected path: " + s) } -func readlink(path string) (string, error) { +func readReparseLink(path string) (string, error) { h, err := openSymlink(path) if err != nil { return "", err @@ -438,10 +438,8 @@ func readlink(path string) (string, error) { } } -// Readlink returns the destination of the named symbolic link. -// If there is an error, it will be of type *PathError. -func Readlink(name string) (string, error) { - s, err := readlink(fixLongPath(name)) +func readlink(name string) (string, error) { + s, err := readReparseLink(fixLongPath(name)) if err != nil { return "", &PathError{Op: "readlink", Path: name, Err: err} } -- 2.48.1