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