Source file src/path/filepath/path_windows.go

     1  // Copyright 2010 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  	"strings"
     9  	"syscall"
    10  )
    11  
    12  func isSlash(c uint8) bool {
    13  	return c == '\\' || c == '/'
    14  }
    15  
    16  func toUpper(c byte) byte {
    17  	if 'a' <= c && c <= 'z' {
    18  		return c - ('a' - 'A')
    19  	}
    20  	return c
    21  }
    22  
    23  // isReservedName reports if name is a Windows reserved device name or a console handle.
    24  // It does not detect names with an extension, which are also reserved on some Windows versions.
    25  //
    26  // For details, search for PRN in
    27  // https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file.
    28  func isReservedName(name string) bool {
    29  	if 3 <= len(name) && len(name) <= 4 {
    30  		switch string([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) {
    31  		case "CON", "PRN", "AUX", "NUL":
    32  			return len(name) == 3
    33  		case "COM", "LPT":
    34  			return len(name) == 4 && '1' <= name[3] && name[3] <= '9'
    35  		}
    36  	}
    37  	// Passing CONIN$ or CONOUT$ to CreateFile opens a console handle.
    38  	// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles
    39  	//
    40  	// While CONIN$ and CONOUT$ aren't documented as being files,
    41  	// they behave the same as CON. For example, ./CONIN$ also opens the console input.
    42  	if len(name) == 6 && name[5] == '$' && strings.EqualFold(name, "CONIN$") {
    43  		return true
    44  	}
    45  	if len(name) == 7 && name[6] == '$' && strings.EqualFold(name, "CONOUT$") {
    46  		return true
    47  	}
    48  	return false
    49  }
    50  
    51  func isLocal(path string) bool {
    52  	if path == "" {
    53  		return false
    54  	}
    55  	if isSlash(path[0]) {
    56  		// Path rooted in the current drive.
    57  		return false
    58  	}
    59  	if strings.IndexByte(path, ':') >= 0 {
    60  		// Colons are only valid when marking a drive letter ("C:foo").
    61  		// Rejecting any path with a colon is conservative but safe.
    62  		return false
    63  	}
    64  	hasDots := false // contains . or .. path elements
    65  	for p := path; p != ""; {
    66  		var part string
    67  		part, p, _ = cutPath(p)
    68  		if part == "." || part == ".." {
    69  			hasDots = true
    70  		}
    71  		// Trim the extension and look for a reserved name.
    72  		base, _, hasExt := strings.Cut(part, ".")
    73  		if isReservedName(base) {
    74  			if !hasExt {
    75  				return false
    76  			}
    77  			// The path element is a reserved name with an extension. Some Windows
    78  			// versions consider this a reserved name, while others do not. Use
    79  			// FullPath to see if the name is reserved.
    80  			//
    81  			// FullPath will convert references to reserved device names to their
    82  			// canonical form: \\.\${DEVICE_NAME}
    83  			//
    84  			// FullPath does not perform this conversion for paths which contain
    85  			// a reserved device name anywhere other than in the last element,
    86  			// so check the part rather than the full path.
    87  			if p, _ := syscall.FullPath(part); len(p) >= 4 && p[:4] == `\\.\` {
    88  				return false
    89  			}
    90  		}
    91  	}
    92  	if hasDots {
    93  		path = Clean(path)
    94  	}
    95  	if path == ".." || strings.HasPrefix(path, `..\`) {
    96  		return false
    97  	}
    98  	return true
    99  }
   100  
   101  // IsAbs reports whether the path is absolute.
   102  func IsAbs(path string) (b bool) {
   103  	l := volumeNameLen(path)
   104  	if l == 0 {
   105  		return false
   106  	}
   107  	// If the volume name starts with a double slash, this is an absolute path.
   108  	if isSlash(path[0]) && isSlash(path[1]) {
   109  		return true
   110  	}
   111  	path = path[l:]
   112  	if path == "" {
   113  		return false
   114  	}
   115  	return isSlash(path[0])
   116  }
   117  
   118  // volumeNameLen returns length of the leading volume name on Windows.
   119  // It returns 0 elsewhere.
   120  //
   121  // See: https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
   122  func volumeNameLen(path string) int {
   123  	if len(path) < 2 {
   124  		return 0
   125  	}
   126  	// with drive letter
   127  	c := path[0]
   128  	if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
   129  		return 2
   130  	}
   131  	// UNC and DOS device paths start with two slashes.
   132  	if !isSlash(path[0]) || !isSlash(path[1]) {
   133  		return 0
   134  	}
   135  	rest := path[2:]
   136  	p1, rest, _ := cutPath(rest)
   137  	p2, rest, ok := cutPath(rest)
   138  	if !ok {
   139  		return len(path)
   140  	}
   141  	if p1 != "." && p1 != "?" {
   142  		// This is a UNC path: \\${HOST}\${SHARE}\
   143  		return len(path) - len(rest) - 1
   144  	}
   145  	// This is a DOS device path.
   146  	if len(p2) == 3 && toUpper(p2[0]) == 'U' && toUpper(p2[1]) == 'N' && toUpper(p2[2]) == 'C' {
   147  		// This is a DOS device path that links to a UNC: \\.\UNC\${HOST}\${SHARE}\
   148  		_, rest, _ = cutPath(rest)  // host
   149  		_, rest, ok = cutPath(rest) // share
   150  		if !ok {
   151  			return len(path)
   152  		}
   153  	}
   154  	return len(path) - len(rest) - 1
   155  }
   156  
   157  // cutPath slices path around the first path separator.
   158  func cutPath(path string) (before, after string, found bool) {
   159  	for i := range path {
   160  		if isSlash(path[i]) {
   161  			return path[:i], path[i+1:], true
   162  		}
   163  	}
   164  	return path, "", false
   165  }
   166  
   167  // HasPrefix exists for historical compatibility and should not be used.
   168  //
   169  // Deprecated: HasPrefix does not respect path boundaries and
   170  // does not ignore case when required.
   171  func HasPrefix(p, prefix string) bool {
   172  	if strings.HasPrefix(p, prefix) {
   173  		return true
   174  	}
   175  	return strings.HasPrefix(strings.ToLower(p), strings.ToLower(prefix))
   176  }
   177  
   178  func splitList(path string) []string {
   179  	// The same implementation is used in LookPath in os/exec;
   180  	// consider changing os/exec when changing this.
   181  
   182  	if path == "" {
   183  		return []string{}
   184  	}
   185  
   186  	// Split path, respecting but preserving quotes.
   187  	list := []string{}
   188  	start := 0
   189  	quo := false
   190  	for i := 0; i < len(path); i++ {
   191  		switch c := path[i]; {
   192  		case c == '"':
   193  			quo = !quo
   194  		case c == ListSeparator && !quo:
   195  			list = append(list, path[start:i])
   196  			start = i + 1
   197  		}
   198  	}
   199  	list = append(list, path[start:])
   200  
   201  	// Remove quotes.
   202  	for i, s := range list {
   203  		list[i] = strings.ReplaceAll(s, `"`, ``)
   204  	}
   205  
   206  	return list
   207  }
   208  
   209  func abs(path string) (string, error) {
   210  	if path == "" {
   211  		// syscall.FullPath returns an error on empty path, because it's not a valid path.
   212  		// To implement Abs behavior of returning working directory on empty string input,
   213  		// special-case empty path by changing it to "." path. See golang.org/issue/24441.
   214  		path = "."
   215  	}
   216  	fullPath, err := syscall.FullPath(path)
   217  	if err != nil {
   218  		return "", err
   219  	}
   220  	return Clean(fullPath), nil
   221  }
   222  
   223  func join(elem []string) string {
   224  	var b strings.Builder
   225  	var lastChar byte
   226  	for _, e := range elem {
   227  		switch {
   228  		case b.Len() == 0:
   229  			// Add the first non-empty path element unchanged.
   230  		case isSlash(lastChar):
   231  			// If the path ends in a slash, strip any leading slashes from the next
   232  			// path element to avoid creating a UNC path (any path starting with "\\")
   233  			// from non-UNC elements.
   234  			//
   235  			// The correct behavior for Join when the first element is an incomplete UNC
   236  			// path (for example, "\\") is underspecified. We currently join subsequent
   237  			// elements so Join("\\", "host", "share") produces "\\host\share".
   238  			for len(e) > 0 && isSlash(e[0]) {
   239  				e = e[1:]
   240  			}
   241  		case lastChar == ':':
   242  			// If the path ends in a colon, keep the path relative to the current directory
   243  			// on a drive and don't add a separator. Preserve leading slashes in the next
   244  			// path element, which may make the path absolute.
   245  			//
   246  			// 	Join(`C:`, `f`) = `C:f`
   247  			//	Join(`C:`, `\f`) = `C:\f`
   248  		default:
   249  			// In all other cases, add a separator between elements.
   250  			b.WriteByte('\\')
   251  			lastChar = '\\'
   252  		}
   253  		if len(e) > 0 {
   254  			b.WriteString(e)
   255  			lastChar = e[len(e)-1]
   256  		}
   257  	}
   258  	if b.Len() == 0 {
   259  		return ""
   260  	}
   261  	return Clean(b.String())
   262  }
   263  
   264  // joinNonEmpty is like join, but it assumes that the first element is non-empty.
   265  func joinNonEmpty(elem []string) string {
   266  	if len(elem[0]) == 2 && elem[0][1] == ':' {
   267  		// First element is drive letter without terminating slash.
   268  		// Keep path relative to current directory on that drive.
   269  		// Skip empty elements.
   270  		i := 1
   271  		for ; i < len(elem); i++ {
   272  			if elem[i] != "" {
   273  				break
   274  			}
   275  		}
   276  		return Clean(elem[0] + strings.Join(elem[i:], string(Separator)))
   277  	}
   278  	// The following logic prevents Join from inadvertently creating a
   279  	// UNC path on Windows. Unless the first element is a UNC path, Join
   280  	// shouldn't create a UNC path. See golang.org/issue/9167.
   281  	p := Clean(strings.Join(elem, string(Separator)))
   282  	if !isUNC(p) {
   283  		return p
   284  	}
   285  	// p == UNC only allowed when the first element is a UNC path.
   286  	head := Clean(elem[0])
   287  	if isUNC(head) {
   288  		return p
   289  	}
   290  	// head + tail == UNC, but joining two non-UNC paths should not result
   291  	// in a UNC path. Undo creation of UNC path.
   292  	tail := Clean(strings.Join(elem[1:], string(Separator)))
   293  	if head[len(head)-1] == Separator {
   294  		return head + tail
   295  	}
   296  	return head + string(Separator) + tail
   297  }
   298  
   299  // isUNC reports whether path is a UNC path.
   300  func isUNC(path string) bool {
   301  	return len(path) > 1 && isSlash(path[0]) && isSlash(path[1])
   302  }
   303  
   304  func sameWord(a, b string) bool {
   305  	return strings.EqualFold(a, b)
   306  }
   307  

View as plain text