1
0
Fork 0
mirror of https://github.com/restic/restic.git synced 2025-03-09 13:53:03 +00:00

fs: implement and use filehandle based readlink

The implementations are 90% copy&paste from the go standard library as
the existing code does not offer any way to read the symlink target
based on a filehandle.

Fall back to a standard readlink on platforms other than Linux and
Windows as those either don't even provide the necessary syscall or in
case of macOS are not yet available in Go.
This commit is contained in:
Michael Eischer 2024-11-01 23:19:17 +01:00
parent 8072b999a6
commit 2a5bbf170d
5 changed files with 219 additions and 2 deletions

View file

@ -0,0 +1,9 @@
package fs
import "os"
// TODO macOS versions >= 13 support freadlink. Use that instead of the fallback codepath
func Freadlink(fd uintptr, name string) (string, error) {
return os.Readlink(name)
}

View file

@ -0,0 +1,47 @@
package fs
import (
"os"
"syscall"
"golang.org/x/sys/unix"
)
// based on readlink from go/src/os/file_unix.go Go 1.23.2
// modified to use Readlinkat syscall instead of readlink
// 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) {
if n < 0 {
n = 0
}
return n, err
}
func Freadlink(fd uintptr, name string) (string, error) {
for namelen := 128; ; namelen *= 2 {
b := make([]byte, namelen)
var (
n int
e error
)
for {
n, e = fixCount(freadlink(int(fd), b))
if e != syscall.EINTR {
break
}
}
if e != nil {
return "", &os.PathError{Op: "readlink", Path: name, Err: e}
}
if n < namelen {
return string(b[0:n]), nil
}
}
}
func freadlink(fd int, buf []byte) (n int, err error) {
// pass empty path to process the link itself
return unix.Readlinkat(fd, "", buf)
}

View file

@ -0,0 +1,24 @@
//go:build linux || windows || darwin
// +build linux windows darwin
package fs
import (
"os"
"path/filepath"
"testing"
rtest "github.com/restic/restic/internal/test"
)
func TestFreadlink(t *testing.T) {
tmpdir := t.TempDir()
link := filepath.Join(tmpdir, "link")
rtest.OK(t, os.Symlink("other", link))
f, err := openMetadataHandle(link, O_NOFOLLOW)
rtest.OK(t, err)
target, err := Freadlink(f.Fd(), link)
rtest.OK(t, err)
rtest.Equals(t, "other", target)
}

View file

@ -0,0 +1,138 @@
package fs
import (
"os"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
func Freadlink(fd uintptr, name string) (string, error) {
link, err := readReparseLink(windows.Handle(fd))
if err != nil {
return "", &os.PathError{Op: "readlink", Path: name, Err: err}
}
return link, nil
}
// based on src/os/file_windows.go from Go 1.23.2
// internally readReparseLink from the std library uses a filehandle, however,
// the external interface is based on a path. Thus, copy everything and minimally
// tweak it to allow passing in a file handle.
// normaliseLinkPath converts absolute paths returned by
// DeviceIoControl(h, FSCTL_GET_REPARSE_POINT, ...)
// into paths acceptable by all Windows APIs.
// For example, it converts
//
// \??\C:\foo\bar into C:\foo\bar
// \??\UNC\foo\bar into \\foo\bar
// \??\Volume{abc}\ into \\?\Volume{abc}\
func normaliseLinkPath(path string) (string, error) {
if len(path) < 4 || path[:4] != `\??\` {
// unexpected path, return it as is
return path, nil
}
// we have path that start with \??\
s := path[4:]
switch {
case len(s) >= 2 && s[1] == ':': // \??\C:\foo\bar
return s, nil
case len(s) >= 4 && s[:4] == `UNC\`: // \??\UNC\foo\bar
return `\\` + s[4:], nil
}
// \??\Volume{abc}\
return `\\?\` + path[4:], nil
// modified to remove the legacy codepath for winreadlinkvolume == 0
}
func readReparseLink(h windows.Handle) (string, error) {
rdbbuf := make([]byte, windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
var bytesReturned uint32
err := windows.DeviceIoControl(h, windows.FSCTL_GET_REPARSE_POINT, nil, 0, &rdbbuf[0], uint32(len(rdbbuf)), &bytesReturned, nil)
if err != nil {
return "", err
}
rdb := (*reparseDataBuffer)(unsafe.Pointer(&rdbbuf[0]))
switch rdb.ReparseTag {
case syscall.IO_REPARSE_TAG_SYMLINK:
rb := (*symbolicLinkReparseBuffer)(unsafe.Pointer(&rdb.DUMMYUNIONNAME))
s := rb.Path()
if rb.Flags&symlinkFlagRelative != 0 {
return s, nil
}
return normaliseLinkPath(s)
case windows.IO_REPARSE_TAG_MOUNT_POINT:
return normaliseLinkPath((*mountPointReparseBuffer)(unsafe.Pointer(&rdb.DUMMYUNIONNAME)).Path())
default:
// the path is not a symlink or junction but another type of reparse
// point
return "", syscall.ENOENT
}
}
// copied from src/internal/syscall/windows/reparse_windows.go from Go 1.23.0
// renamed to not export symbols
const symlinkFlagRelative = 1
type reparseDataBuffer struct {
ReparseTag uint32
ReparseDataLength uint16
Reserved uint16
DUMMYUNIONNAME byte
}
type symbolicLinkReparseBuffer struct {
// The integer that contains the offset, in bytes,
// of the substitute name string in the PathBuffer array,
// computed as an offset from byte 0 of PathBuffer. Note that
// this offset must be divided by 2 to get the array index.
SubstituteNameOffset uint16
// The integer that contains the length, in bytes, of the
// substitute name string. If this string is null-terminated,
// SubstituteNameLength does not include the Unicode null character.
SubstituteNameLength uint16
// PrintNameOffset is similar to SubstituteNameOffset.
PrintNameOffset uint16
// PrintNameLength is similar to SubstituteNameLength.
PrintNameLength uint16
// Flags specifies whether the substitute name is a full path name or
// a path name relative to the directory containing the symbolic link.
Flags uint32
PathBuffer [1]uint16
}
// Path returns path stored in rb.
func (rb *symbolicLinkReparseBuffer) Path() string {
n1 := rb.SubstituteNameOffset / 2
n2 := (rb.SubstituteNameOffset + rb.SubstituteNameLength) / 2
return syscall.UTF16ToString((*[0xffff]uint16)(unsafe.Pointer(&rb.PathBuffer[0]))[n1:n2:n2])
}
type mountPointReparseBuffer struct {
// The integer that contains the offset, in bytes,
// of the substitute name string in the PathBuffer array,
// computed as an offset from byte 0 of PathBuffer. Note that
// this offset must be divided by 2 to get the array index.
SubstituteNameOffset uint16
// The integer that contains the length, in bytes, of the
// substitute name string. If this string is null-terminated,
// SubstituteNameLength does not include the Unicode null character.
SubstituteNameLength uint16
// PrintNameOffset is similar to SubstituteNameOffset.
PrintNameOffset uint16
// PrintNameLength is similar to SubstituteNameLength.
PrintNameLength uint16
PathBuffer [1]uint16
}
// Path returns path stored in rb.
func (rb *mountPointReparseBuffer) Path() string {
n1 := rb.SubstituteNameOffset / 2
n2 := (rb.SubstituteNameOffset + rb.SubstituteNameLength) / 2
return syscall.UTF16ToString((*[0xffff]uint16)(unsafe.Pointer(&rb.PathBuffer[0]))[n1:n2:n2])
}

View file

@ -31,6 +31,5 @@ func (p *fdMetadataHandle) Stat() (*ExtendedFileInfo, error) {
} }
func (p *fdMetadataHandle) Readlink() (string, error) { func (p *fdMetadataHandle) Readlink() (string, error) {
// FIXME return Freadlink(p.f.Fd(), fixpath(p.name))
return os.Readlink(fixpath(p.name))
} }