1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
|
//go:build windows
package fs
import (
"errors"
"os"
"strings"
"golang.org/x/sys/windows"
"github.com/Microsoft/go-winio/internal/fs"
)
// ResolvePath returns the final path to a file or directory represented, resolving symlinks,
// handling mount points, etc.
// The resolution works by using the Windows API GetFinalPathNameByHandle, which takes a
// handle and returns the final path to that file.
//
// It is intended to address short-comings of [filepath.EvalSymlinks], which does not work
// well on Windows.
func ResolvePath(path string) (string, error) {
h, err := openMetadata(path)
if err != nil {
return "", err
}
defer windows.CloseHandle(h) //nolint:errcheck
// We use the Windows API GetFinalPathNameByHandle to handle path resolution. GetFinalPathNameByHandle
// returns a resolved path name for a file or directory. The returned path can be in several different
// formats, based on the flags passed. There are several goals behind the design here:
// - Do as little manual path manipulation as possible. Since Windows path formatting can be quite
// complex, we try to just let the Windows APIs handle that for us.
// - Retain as much compatibility with existing Go path functions as we can. In particular, we try to
// ensure paths returned from resolvePath can be passed to EvalSymlinks.
//
// First, we query for the VOLUME_NAME_GUID path of the file. This will return a path in the form
// "\\?\Volume{8a25748f-cf34-4ac6-9ee2-c89400e886db}\dir\file.txt". If the path is a UNC share
// (e.g. "\\server\share\dir\file.txt"), then the VOLUME_NAME_GUID query will fail with ERROR_PATH_NOT_FOUND.
// In this case, we will next try a VOLUME_NAME_DOS query. This query will return a path for a UNC share
// in the form "\\?\UNC\server\share\dir\file.txt". This path will work with most functions, but EvalSymlinks
// fails on it. Therefore, we rewrite the path to the form "\\server\share\dir\file.txt" before returning it.
// This path rewrite may not be valid in all cases (see the notes in the next paragraph), but those should
// be very rare edge cases, and this case wouldn't have worked with EvalSymlinks anyways.
//
// The "\\?\" prefix indicates that no path parsing or normalization should be performed by Windows.
// Instead the path is passed directly to the object manager. The lack of parsing means that "." and ".." are
// interpreted literally and "\"" must be used as a path separator. Additionally, because normalization is
// not done, certain paths can only be represented in this format. For instance, "\\?\C:\foo." (with a trailing .)
// cannot be written as "C:\foo.", because path normalization will remove the trailing ".".
//
// FILE_NAME_NORMALIZED can fail on some UNC paths based on access restrictions.
// Attempt to query with FILE_NAME_NORMALIZED, and then fall back on FILE_NAME_OPENED if access is denied.
//
// Querying for VOLUME_NAME_DOS first instead of VOLUME_NAME_GUID would yield a "nicer looking" path in some cases.
// For instance, it could return "\\?\C:\dir\file.txt" instead of "\\?\Volume{8a25748f-cf34-4ac6-9ee2-c89400e886db}\dir\file.txt".
// However, we query for VOLUME_NAME_GUID first for two reasons:
// - The volume GUID path is more stable. A volume's mount point can change when it is remounted, but its
// volume GUID should not change.
// - If the volume is mounted at a non-drive letter path (e.g. mounted to "C:\mnt"), then VOLUME_NAME_DOS
// will return the mount path. EvalSymlinks fails on a path like this due to a bug.
//
// References:
// - GetFinalPathNameByHandle: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlea
// - Naming Files, Paths, and Namespaces: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
// - Naming a Volume: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-volume
normalize := true
guid := true
rPath := ""
for i := 1; i <= 4; i++ { // maximum of 4 different cases to try
var flags fs.GetFinalPathFlag
if normalize {
flags |= fs.FILE_NAME_NORMALIZED // nop; for clarity
} else {
flags |= fs.FILE_NAME_OPENED
}
if guid {
flags |= fs.VOLUME_NAME_GUID
} else {
flags |= fs.VOLUME_NAME_DOS // nop; for clarity
}
rPath, err = fs.GetFinalPathNameByHandle(h, flags)
switch {
case guid && errors.Is(err, windows.ERROR_PATH_NOT_FOUND):
// ERROR_PATH_NOT_FOUND is returned from the VOLUME_NAME_GUID query if the path is a
// network share (UNC path). In this case, query for the DOS name instead.
guid = false
continue
case normalize && errors.Is(err, windows.ERROR_ACCESS_DENIED):
// normalization failed when accessing individual components along path for SMB share
normalize = false
continue
default:
}
break
}
if err == nil && strings.HasPrefix(rPath, `\\?\UNC\`) {
// Convert \\?\UNC\server\share -> \\server\share. The \\?\UNC syntax does not work with
// some Go filepath functions such as EvalSymlinks. In the future if other components
// move away from EvalSymlinks and use GetFinalPathNameByHandle instead, we could remove
// this path munging.
rPath = `\\` + rPath[len(`\\?\UNC\`):]
}
return rPath, err
}
// openMetadata takes a path, opens it with only meta-data access, and returns the resulting handle.
// It works for both file and directory paths.
func openMetadata(path string) (windows.Handle, error) {
// We are not able to use builtin Go functionality for opening a directory path:
// - os.Open on a directory returns a os.File where Fd() is a search handle from FindFirstFile.
// - syscall.Open does not provide a way to specify FILE_FLAG_BACKUP_SEMANTICS, which is needed to
// open a directory.
//
// We could use os.Open if the path is a file, but it's easier to just use the same code for both.
// Therefore, we call windows.CreateFile directly.
h, err := fs.CreateFile(
path,
fs.FILE_ANY_ACCESS,
fs.FILE_SHARE_READ|fs.FILE_SHARE_WRITE|fs.FILE_SHARE_DELETE,
nil, // security attributes
fs.OPEN_EXISTING,
fs.FILE_FLAG_BACKUP_SEMANTICS, // Needed to open a directory handle.
fs.NullHandle,
)
if err != nil {
return 0, &os.PathError{
Op: "CreateFile",
Path: path,
Err: err,
}
}
return h, nil
}
|