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