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:
parent
8072b999a6
commit
2a5bbf170d
5 changed files with 219 additions and 2 deletions
9
internal/fs/freadlink_darwin.go
Normal file
9
internal/fs/freadlink_darwin.go
Normal 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)
|
||||||
|
}
|
47
internal/fs/freadlink_linux.go
Normal file
47
internal/fs/freadlink_linux.go
Normal 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)
|
||||||
|
}
|
24
internal/fs/freadlink_test.go
Normal file
24
internal/fs/freadlink_test.go
Normal 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)
|
||||||
|
}
|
138
internal/fs/freadlink_windows.go
Normal file
138
internal/fs/freadlink_windows.go
Normal 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])
|
||||||
|
}
|
|
@ -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))
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue