|
| 1 | +/* |
| 2 | + Copyright The containerd Authors. |
| 3 | +
|
| 4 | + Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + you may not use this file except in compliance with the License. |
| 6 | + You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | + Unless required by applicable law or agreed to in writing, software |
| 11 | + distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + See the License for the specific language governing permissions and |
| 14 | + limitations under the License. |
| 15 | +*/ |
| 16 | + |
| 17 | +package os |
| 18 | + |
| 19 | +import ( |
| 20 | + "os" |
| 21 | + "strings" |
| 22 | + "sync" |
| 23 | + "unicode/utf16" |
| 24 | + |
| 25 | + "golang.org/x/sys/windows" |
| 26 | +) |
| 27 | + |
| 28 | +// openPath takes a path, opens it, and returns the resulting handle. |
| 29 | +// It works for both file and directory paths. |
| 30 | +// |
| 31 | +// We are not able to use builtin Go functionality for opening a directory path: |
| 32 | +// - os.Open on a directory returns a os.File where Fd() is a search handle from FindFirstFile. |
| 33 | +// - syscall.Open does not provide a way to specify FILE_FLAG_BACKUP_SEMANTICS, which is needed to |
| 34 | +// open a directory. |
| 35 | +// We could use os.Open if the path is a file, but it's easier to just use the same code for both. |
| 36 | +// Therefore, we call windows.CreateFile directly. |
| 37 | +func openPath(path string) (windows.Handle, error) { |
| 38 | + u16, err := windows.UTF16PtrFromString(path) |
| 39 | + if err != nil { |
| 40 | + return 0, err |
| 41 | + } |
| 42 | + h, err := windows.CreateFile( |
| 43 | + u16, |
| 44 | + 0, |
| 45 | + windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, |
| 46 | + nil, |
| 47 | + windows.OPEN_EXISTING, |
| 48 | + windows.FILE_FLAG_BACKUP_SEMANTICS, // Needed to open a directory handle. |
| 49 | + 0) |
| 50 | + if err != nil { |
| 51 | + return 0, &os.PathError{ |
| 52 | + Op: "CreateFile", |
| 53 | + Path: path, |
| 54 | + Err: err, |
| 55 | + } |
| 56 | + } |
| 57 | + return h, nil |
| 58 | +} |
| 59 | + |
| 60 | +// GetFinalPathNameByHandle flags. |
| 61 | +//nolint:golint |
| 62 | +const ( |
| 63 | + cFILE_NAME_OPENED = 0x8 |
| 64 | + |
| 65 | + cVOLUME_NAME_DOS = 0x0 |
| 66 | + cVOLUME_NAME_GUID = 0x1 |
| 67 | +) |
| 68 | + |
| 69 | +var pool = sync.Pool{ |
| 70 | + New: func() interface{} { |
| 71 | + // Size of buffer chosen somewhat arbitrarily to accommodate a large number of path strings. |
| 72 | + // MAX_PATH (260) + size of volume GUID prefix (49) + null terminator = 310. |
| 73 | + b := make([]uint16, 310) |
| 74 | + return &b |
| 75 | + }, |
| 76 | +} |
| 77 | + |
| 78 | +// getFinalPathNameByHandle facilitates calling the Windows API GetFinalPathNameByHandle |
| 79 | +// with the given handle and flags. It transparently takes care of creating a buffer of the |
| 80 | +// correct size for the call. |
| 81 | +func getFinalPathNameByHandle(h windows.Handle, flags uint32) (string, error) { |
| 82 | + b := *(pool.Get().(*[]uint16)) |
| 83 | + defer func() { pool.Put(&b) }() |
| 84 | + for { |
| 85 | + n, err := windows.GetFinalPathNameByHandle(h, &b[0], uint32(len(b)), flags) |
| 86 | + if err != nil { |
| 87 | + return "", err |
| 88 | + } |
| 89 | + // If the buffer wasn't large enough, n will be the total size needed (including null terminator). |
| 90 | + // Resize and try again. |
| 91 | + if n > uint32(len(b)) { |
| 92 | + b = make([]uint16, n) |
| 93 | + continue |
| 94 | + } |
| 95 | + // If the buffer is large enough, n will be the size not including the null terminator. |
| 96 | + // Convert to a Go string and return. |
| 97 | + return string(utf16.Decode(b[:n])), nil |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +// resolvePath implements path resolution for Windows. It attempts to return the "real" path to the |
| 102 | +// file or directory represented by the given path. |
| 103 | +// The resolution works by using the Windows API GetFinalPathNameByHandle, which takes a handle and |
| 104 | +// returns the final path to that file. |
| 105 | +func resolvePath(path string) (string, error) { |
| 106 | + h, err := openPath(path) |
| 107 | + if err != nil { |
| 108 | + return "", err |
| 109 | + } |
| 110 | + defer windows.CloseHandle(h) |
| 111 | + |
| 112 | + // We use the Windows API GetFinalPathNameByHandle to handle path resolution. GetFinalPathNameByHandle |
| 113 | + // returns a resolved path name for a file or directory. The returned path can be in several different |
| 114 | + // formats, based on the flags passed. There are several goals behind the design here: |
| 115 | + // - Do as little manual path manipulation as possible. Since Windows path formatting can be quite |
| 116 | + // complex, we try to just let the Windows APIs handle that for us. |
| 117 | + // - Retain as much compatibility with existing Go path functions as we can. In particular, we try to |
| 118 | + // ensure paths returned from resolvePath can be passed to EvalSymlinks. |
| 119 | + // |
| 120 | + // First, we query for the VOLUME_NAME_GUID path of the file. This will return a path in the form |
| 121 | + // "\\?\Volume{8a25748f-cf34-4ac6-9ee2-c89400e886db}\dir\file.txt". If the path is a UNC share |
| 122 | + // (e.g. "\\server\share\dir\file.txt"), then the VOLUME_NAME_GUID query will fail with ERROR_PATH_NOT_FOUND. |
| 123 | + // In this case, we will next try a VOLUME_NAME_DOS query. This query will return a path for a UNC share |
| 124 | + // in the form "\\?\UNC\server\share\dir\file.txt". This path will work with most functions, but EvalSymlinks |
| 125 | + // fails on it. Therefore, we rewrite the path to the form "\\server\share\dir\file.txt" before returning it. |
| 126 | + // This path rewrite may not be valid in all cases (see the notes in the next paragraph), but those should |
| 127 | + // be very rare edge cases, and this case wouldn't have worked with EvalSymlinks anyways. |
| 128 | + // |
| 129 | + // The "\\?\" prefix indicates that no path parsing or normalization should be performed by Windows. |
| 130 | + // Instead the path is passed directly to the object manager. The lack of parsing means that "." and ".." are |
| 131 | + // interpreted literally and "\"" must be used as a path separator. Additionally, because normalization is |
| 132 | + // not done, certain paths can only be represented in this format. For instance, "\\?\C:\foo." (with a trailing .) |
| 133 | + // cannot be written as "C:\foo.", because path normalization will remove the trailing ".". |
| 134 | + // |
| 135 | + // We use FILE_NAME_OPENED instead of FILE_NAME_NORMALIZED, as FILE_NAME_NORMALIZED can fail on some |
| 136 | + // UNC paths based on access restrictions. The additional normalization done is also quite minimal in |
| 137 | + // most cases. |
| 138 | + // |
| 139 | + // Querying for VOLUME_NAME_DOS first instead of VOLUME_NAME_GUID would yield a "nicer looking" path in some cases. |
| 140 | + // For instance, it could return "\\?\C:\dir\file.txt" instead of "\\?\Volume{8a25748f-cf34-4ac6-9ee2-c89400e886db}\dir\file.txt". |
| 141 | + // However, we query for VOLUME_NAME_GUID first for two reasons: |
| 142 | + // - The volume GUID path is more stable. A volume's mount point can change when it is remounted, but its |
| 143 | + // volume GUID should not change. |
| 144 | + // - If the volume is mounted at a non-drive letter path (e.g. mounted to "C:\mnt"), then VOLUME_NAME_DOS |
| 145 | + // will return the mount path. EvalSymlinks fails on a path like this due to a bug. |
| 146 | + // |
| 147 | + // References: |
| 148 | + // - GetFinalPathNameByHandle: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlea |
| 149 | + // - Naming Files, Paths, and Namespaces: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file |
| 150 | + // - Naming a Volume: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-volume |
| 151 | + |
| 152 | + rPath, err := getFinalPathNameByHandle(h, cFILE_NAME_OPENED|cVOLUME_NAME_GUID) |
| 153 | + if err == windows.ERROR_PATH_NOT_FOUND { |
| 154 | + // ERROR_PATH_NOT_FOUND is returned from the VOLUME_NAME_GUID query if the path is a |
| 155 | + // network share (UNC path). In this case, query for the DOS name instead, then translate |
| 156 | + // the returned path to make it more palatable to other path functions. |
| 157 | + rPath, err = getFinalPathNameByHandle(h, cFILE_NAME_OPENED|cVOLUME_NAME_DOS) |
| 158 | + if err != nil { |
| 159 | + return "", err |
| 160 | + } |
| 161 | + if strings.HasPrefix(rPath, `\\?\UNC\`) { |
| 162 | + // Convert \\?\UNC\server\share -> \\server\share. The \\?\UNC syntax does not work with |
| 163 | + // some Go filepath functions such as EvalSymlinks. In the future if other components |
| 164 | + // move away from EvalSymlinks and use GetFinalPathNameByHandle instead, we could remove |
| 165 | + // this path munging. |
| 166 | + rPath = `\\` + rPath[len(`\\?\UNC\`):] |
| 167 | + } |
| 168 | + } else if err != nil { |
| 169 | + return "", err |
| 170 | + } |
| 171 | + return rPath, nil |
| 172 | +} |
| 173 | + |
| 174 | +// ResolveSymbolicLink will follow any symbolic links |
| 175 | +func (RealOS) ResolveSymbolicLink(path string) (string, error) { |
| 176 | + // filepath.EvalSymlinks does not work very well on Windows, so instead we resolve the path |
| 177 | + // via resolvePath which uses GetFinalPathNameByHandle. This returns either a path prefixed with `\\?\`, |
| 178 | + // or a remote share path in the form \\server\share. These should work with most Go and Windows APIs. |
| 179 | + return resolvePath(path) |
| 180 | +} |
0 commit comments