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