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