1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
|
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package filepath
import (
"errors"
"internal/syscall/windows"
"os"
"strings"
"syscall"
)
// normVolumeName is like VolumeName, but makes drive letter upper case.
// result of EvalSymlinks must be unique, so we have
// EvalSymlinks(`c:\a`) == EvalSymlinks(`C:\a`).
func normVolumeName(path string) string {
volume := VolumeName(path)
if len(volume) > 2 { // isUNC
return volume
}
return strings.ToUpper(volume)
}
// normBase returns the last element of path with correct case.
func normBase(path string) (string, error) {
p, err := syscall.UTF16PtrFromString(path)
if err != nil {
return "", err
}
var data syscall.Win32finddata
h, err := syscall.FindFirstFile(p, &data)
if err != nil {
return "", err
}
syscall.FindClose(h)
return syscall.UTF16ToString(data.FileName[:]), nil
}
// baseIsDotDot returns whether the last element of path is "..".
// The given path should be 'Clean'-ed in advance.
func baseIsDotDot(path string) bool {
i := strings.LastIndexByte(path, Separator)
return path[i+1:] == ".."
}
// toNorm returns the normalized path that is guaranteed to be unique.
// It should accept the following formats:
// * UNC paths (e.g \\server\share\foo\bar)
// * absolute paths (e.g C:\foo\bar)
// * relative paths begin with drive letter (e.g C:foo\bar, C:..\foo\bar, C:.., C:.)
// * relative paths begin with '\' (e.g \foo\bar)
// * relative paths begin without '\' (e.g foo\bar, ..\foo\bar, .., .)
// The returned normalized path will be in the same form (of 5 listed above) as the input path.
// If two paths A and B are indicating the same file with the same format, toNorm(A) should be equal to toNorm(B).
// The normBase parameter should be equal to the normBase func, except for in tests. See docs on the normBase func.
func toNorm(path string, normBase func(string) (string, error)) (string, error) {
if path == "" {
return path, nil
}
path = Clean(path)
volume := normVolumeName(path)
path = path[len(volume):]
// skip special cases
if path == "." || path == `\` {
return volume + path, nil
}
var normPath string
for {
if baseIsDotDot(path) {
normPath = path + `\` + normPath
break
}
name, err := normBase(volume + path)
if err != nil {
return "", err
}
normPath = name + `\` + normPath
i := strings.LastIndexByte(path, Separator)
if i == -1 {
break
}
if i == 0 { // `\Go` or `C:\Go`
normPath = `\` + normPath
break
}
path = path[:i]
}
normPath = normPath[:len(normPath)-1] // remove trailing '\'
return volume + normPath, nil
}
// evalSymlinksUsingGetFinalPathNameByHandle uses Windows
// GetFinalPathNameByHandle API to retrieve the final
// path for the specified file.
func evalSymlinksUsingGetFinalPathNameByHandle(path string) (string, error) {
err := windows.LoadGetFinalPathNameByHandle()
if err != nil {
// we must be using old version of Windows
return "", err
}
if path == "" {
return path, nil
}
// Use Windows I/O manager to dereference the symbolic link, as per
// https://blogs.msdn.microsoft.com/oldnewthing/20100212-00/?p=14963/
p, err := syscall.UTF16PtrFromString(path)
if err != nil {
return "", err
}
h, err := syscall.CreateFile(p, 0, 0, nil,
syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
if err != nil {
return "", err
}
defer syscall.CloseHandle(h)
buf := make([]uint16, 100)
for {
n, err := windows.GetFinalPathNameByHandle(h, &buf[0], uint32(len(buf)), windows.VOLUME_NAME_DOS)
if err != nil {
return "", err
}
if n < uint32(len(buf)) {
break
}
buf = make([]uint16, n)
}
s := syscall.UTF16ToString(buf)
if len(s) > 4 && s[:4] == `\\?\` {
s = s[4:]
if len(s) > 3 && s[:3] == `UNC` {
// return path like \\server\share\...
return `\` + s[3:], nil
}
return s, nil
}
return "", errors.New("GetFinalPathNameByHandle returned unexpected path=" + s)
}
func samefile(path1, path2 string) bool {
fi1, err := os.Lstat(path1)
if err != nil {
return false
}
fi2, err := os.Lstat(path2)
if err != nil {
return false
}
return os.SameFile(fi1, fi2)
}
func evalSymlinks(path string) (string, error) {
newpath, err := walkSymlinks(path)
if err != nil {
newpath2, err2 := evalSymlinksUsingGetFinalPathNameByHandle(path)
if err2 == nil {
return toNorm(newpath2, normBase)
}
return "", err
}
newpath, err = toNorm(newpath, normBase)
if err != nil {
newpath2, err2 := evalSymlinksUsingGetFinalPathNameByHandle(path)
if err2 == nil {
return toNorm(newpath2, normBase)
}
return "", err
}
if strings.ToUpper(newpath) == strings.ToUpper(path) {
// walkSymlinks did not actually walk any symlinks,
// so we don't need to try GetFinalPathNameByHandle.
return newpath, nil
}
newpath2, err2 := evalSymlinksUsingGetFinalPathNameByHandle(path)
if err2 != nil {
return newpath, nil
}
newpath2, err2 = toNorm(newpath2, normBase)
if err2 != nil {
return newpath, nil
}
if samefile(newpath, newpath2) {
return newpath, nil
}
return newpath2, nil
}
|