Source file src/internal/filepathlite/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 filepathlite
     6  
     7  import (
     8  	"internal/bytealg"
     9  	"internal/stringslite"
    10  	"syscall"
    11  )
    12  
    13  const (
    14  	Separator     = '\\' // OS-specific path separator
    15  	ListSeparator = ';'  // OS-specific path list separator
    16  )
    17  
    18  func IsPathSeparator(c uint8) bool {
    19  	return c == '\\' || c == '/'
    20  }
    21  
    22  func isLocal(path string) bool {
    23  	if path == "" {
    24  		return false
    25  	}
    26  	if IsPathSeparator(path[0]) {
    27  		// Path rooted in the current drive.
    28  		return false
    29  	}
    30  	if stringslite.IndexByte(path, ':') >= 0 {
    31  		// Colons are only valid when marking a drive letter ("C:foo").
    32  		// Rejecting any path with a colon is conservative but safe.
    33  		return false
    34  	}
    35  	hasDots := false // contains . or .. path elements
    36  	for p := path; p != ""; {
    37  		var part string
    38  		part, p, _ = cutPath(p)
    39  		if part == "." || part == ".." {
    40  			hasDots = true
    41  		}
    42  		if isReservedName(part) {
    43  			return false
    44  		}
    45  	}
    46  	if hasDots {
    47  		path = Clean(path)
    48  	}
    49  	if path == ".." || stringslite.HasPrefix(path, `..\`) {
    50  		return false
    51  	}
    52  	return true
    53  }
    54  
    55  func localize(path string) (string, error) {
    56  	for i := 0; i < len(path); i++ {
    57  		switch path[i] {
    58  		case ':', '\\', 0:
    59  			return "", errInvalidPath
    60  		}
    61  	}
    62  	containsSlash := false
    63  	for p := path; p != ""; {
    64  		// Find the next path element.
    65  		var element string
    66  		i := bytealg.IndexByteString(p, '/')
    67  		if i < 0 {
    68  			element = p
    69  			p = ""
    70  		} else {
    71  			containsSlash = true
    72  			element = p[:i]
    73  			p = p[i+1:]
    74  		}
    75  		if isReservedName(element) {
    76  			return "", errInvalidPath
    77  		}
    78  	}
    79  	if containsSlash {
    80  		// We can't depend on strings, so substitute \ for / manually.
    81  		buf := []byte(path)
    82  		for i, b := range buf {
    83  			if b == '/' {
    84  				buf[i] = '\\'
    85  			}
    86  		}
    87  		path = string(buf)
    88  	}
    89  	return path, nil
    90  }
    91  
    92  // isReservedName reports if name is a Windows reserved device name.
    93  // It does not detect names with an extension, which are also reserved on some Windows versions.
    94  //
    95  // For details, search for PRN in
    96  // https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file.
    97  func isReservedName(name string) bool {
    98  	// Device names can have arbitrary trailing characters following a dot or colon.
    99  	base := name
   100  	for i := 0; i < len(base); i++ {
   101  		switch base[i] {
   102  		case ':', '.':
   103  			base = base[:i]
   104  		}
   105  	}
   106  	// Trailing spaces in the last path element are ignored.
   107  	for len(base) > 0 && base[len(base)-1] == ' ' {
   108  		base = base[:len(base)-1]
   109  	}
   110  	if !isReservedBaseName(base) {
   111  		return false
   112  	}
   113  	if len(base) == len(name) {
   114  		return true
   115  	}
   116  	// The path element is a reserved name with an extension.
   117  	// Some Windows versions consider this a reserved name,
   118  	// while others do not. Use FullPath to see if the name is
   119  	// reserved.
   120  	if p, _ := syscall.FullPath(name); len(p) >= 4 && p[:4] == `\\.\` {
   121  		return true
   122  	}
   123  	return false
   124  }
   125  
   126  func isReservedBaseName(name string) bool {
   127  	if len(name) == 3 {
   128  		switch string([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) {
   129  		case "CON", "PRN", "AUX", "NUL":
   130  			return true
   131  		}
   132  	}
   133  	if len(name) >= 4 {
   134  		switch string([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) {
   135  		case "COM", "LPT":
   136  			if len(name) == 4 && '1' <= name[3] && name[3] <= '9' {
   137  				return true
   138  			}
   139  			// Superscript ¹, ², and ³ are considered numbers as well.
   140  			switch name[3:] {
   141  			case "\u00b2", "\u00b3", "\u00b9":
   142  				return true
   143  			}
   144  			return false
   145  		}
   146  	}
   147  
   148  	// Passing CONIN$ or CONOUT$ to CreateFile opens a console handle.
   149  	// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles
   150  	//
   151  	// While CONIN$ and CONOUT$ aren't documented as being files,
   152  	// they behave the same as CON. For example, ./CONIN$ also opens the console input.
   153  	if len(name) == 6 && name[5] == '$' && equalFold(name, "CONIN$") {
   154  		return true
   155  	}
   156  	if len(name) == 7 && name[6] == '$' && equalFold(name, "CONOUT$") {
   157  		return true
   158  	}
   159  	return false
   160  }
   161  
   162  func equalFold(a, b string) bool {
   163  	if len(a) != len(b) {
   164  		return false
   165  	}
   166  	for i := 0; i < len(a); i++ {
   167  		if toUpper(a[i]) != toUpper(b[i]) {
   168  			return false
   169  		}
   170  	}
   171  	return true
   172  }
   173  
   174  func toUpper(c byte) byte {
   175  	if 'a' <= c && c <= 'z' {
   176  		return c - ('a' - 'A')
   177  	}
   178  	return c
   179  }
   180  
   181  // IsAbs reports whether the path is absolute.
   182  func IsAbs(path string) (b bool) {
   183  	l := volumeNameLen(path)
   184  	if l == 0 {
   185  		return false
   186  	}
   187  	// If the volume name starts with a double slash, this is an absolute path.
   188  	if IsPathSeparator(path[0]) && IsPathSeparator(path[1]) {
   189  		return true
   190  	}
   191  	path = path[l:]
   192  	if path == "" {
   193  		return false
   194  	}
   195  	return IsPathSeparator(path[0])
   196  }
   197  
   198  // volumeNameLen returns length of the leading volume name on Windows.
   199  // It returns 0 elsewhere.
   200  //
   201  // See:
   202  // https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
   203  // https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
   204  func volumeNameLen(path string) int {
   205  	switch {
   206  	case len(path) >= 2 && path[1] == ':':
   207  		// Path starts with a drive letter.
   208  		//
   209  		// Not all Windows functions necessarily enforce the requirement that
   210  		// drive letters be in the set A-Z, and we don't try to here.
   211  		//
   212  		// We don't handle the case of a path starting with a non-ASCII character,
   213  		// in which case the "drive letter" might be multiple bytes long.
   214  		return 2
   215  
   216  	case len(path) == 0 || !IsPathSeparator(path[0]):
   217  		// Path does not have a volume component.
   218  		return 0
   219  
   220  	case pathHasPrefixFold(path, `\\.\UNC`):
   221  		// We're going to treat the UNC host and share as part of the volume
   222  		// prefix for historical reasons, but this isn't really principled;
   223  		// Windows's own GetFullPathName will happily remove the first
   224  		// component of the path in this space, converting
   225  		// \\.\unc\a\b\..\c into \\.\unc\a\c.
   226  		return uncLen(path, len(`\\.\UNC\`))
   227  
   228  	case pathHasPrefixFold(path, `\\.`) ||
   229  		pathHasPrefixFold(path, `\\?`) || pathHasPrefixFold(path, `\??`):
   230  		// Path starts with \\.\, and is a Local Device path; or
   231  		// path starts with \\?\ or \??\ and is a Root Local Device path.
   232  		//
   233  		// We treat the next component after the \\.\ prefix as
   234  		// part of the volume name, which means Clean(`\\?\c:\`)
   235  		// won't remove the trailing \. (See #64028.)
   236  		if len(path) == 3 {
   237  			return 3 // exactly \\.
   238  		}
   239  		_, rest, ok := cutPath(path[4:])
   240  		if !ok {
   241  			return len(path)
   242  		}
   243  		return len(path) - len(rest) - 1
   244  
   245  	case len(path) >= 2 && IsPathSeparator(path[1]):
   246  		// Path starts with \\, and is a UNC path.
   247  		return uncLen(path, 2)
   248  	}
   249  	return 0
   250  }
   251  
   252  // pathHasPrefixFold tests whether the path s begins with prefix,
   253  // ignoring case and treating all path separators as equivalent.
   254  // If s is longer than prefix, then s[len(prefix)] must be a path separator.
   255  func pathHasPrefixFold(s, prefix string) bool {
   256  	if len(s) < len(prefix) {
   257  		return false
   258  	}
   259  	for i := 0; i < len(prefix); i++ {
   260  		if IsPathSeparator(prefix[i]) {
   261  			if !IsPathSeparator(s[i]) {
   262  				return false
   263  			}
   264  		} else if toUpper(prefix[i]) != toUpper(s[i]) {
   265  			return false
   266  		}
   267  	}
   268  	if len(s) > len(prefix) && !IsPathSeparator(s[len(prefix)]) {
   269  		return false
   270  	}
   271  	return true
   272  }
   273  
   274  // uncLen returns the length of the volume prefix of a UNC path.
   275  // prefixLen is the prefix prior to the start of the UNC host;
   276  // for example, for "//host/share", the prefixLen is len("//")==2.
   277  func uncLen(path string, prefixLen int) int {
   278  	count := 0
   279  	for i := prefixLen; i < len(path); i++ {
   280  		if IsPathSeparator(path[i]) {
   281  			count++
   282  			if count == 2 {
   283  				return i
   284  			}
   285  		}
   286  	}
   287  	return len(path)
   288  }
   289  
   290  // cutPath slices path around the first path separator.
   291  func cutPath(path string) (before, after string, found bool) {
   292  	for i := range path {
   293  		if IsPathSeparator(path[i]) {
   294  			return path[:i], path[i+1:], true
   295  		}
   296  	}
   297  	return path, "", false
   298  }
   299  
   300  // isUNC reports whether path is a UNC path.
   301  func isUNC(path string) bool {
   302  	return len(path) > 1 && IsPathSeparator(path[0]) && IsPathSeparator(path[1])
   303  }
   304  
   305  // postClean adjusts the results of Clean to avoid turning a relative path
   306  // into an absolute or rooted one.
   307  func postClean(out *lazybuf) {
   308  	if out.volLen != 0 || out.buf == nil {
   309  		return
   310  	}
   311  	// If a ':' appears in the path element at the start of a path,
   312  	// insert a .\ at the beginning to avoid converting relative paths
   313  	// like a/../c: into c:.
   314  	for _, c := range out.buf {
   315  		if IsPathSeparator(c) {
   316  			break
   317  		}
   318  		if c == ':' {
   319  			out.prepend('.', Separator)
   320  			return
   321  		}
   322  	}
   323  	// If a path begins with \??\, insert a \. at the beginning
   324  	// to avoid converting paths like \a\..\??\c:\x into \??\c:\x
   325  	// (equivalent to c:\x).
   326  	if len(out.buf) >= 3 && IsPathSeparator(out.buf[0]) && out.buf[1] == '?' && out.buf[2] == '?' {
   327  		out.prepend(Separator, '.')
   328  	}
   329  }
   330  

View as plain text