resolve.go 5.9 KB

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