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

View as plain text