1
0
Fork 0
mirror of https://github.com/restic/restic.git synced 2025-01-21 06:48:35 +00:00
restic/internal/fs/freadlink_windows.go
Michael Eischer 2a5bbf170d 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.
2024-11-30 19:17:25 +01:00

138 lines
4.6 KiB
Go

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])
}