140 lines
6.1 KiB
Go
140 lines
6.1 KiB
Go
//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
|
|
}
|