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, `\\.`) ||
   223  		pathHasPrefixFold(path, `\\?`) || pathHasPrefixFold(path, `\??`):
   224  		// Path starts with a device prefix: \\.\ for Local Device paths,
   225  		// or \\?\ or \??\ for Root Local Device paths.
   226  		switch {
   227  		case len(path) == 3:
   228  			return 3 // exactly \\., \\?, or \??
   229  		case pathHasPrefixFold(path[4:], `UNC`):
   230  			// We're going to treat the UNC host and share as part of the volume
   231  			// prefix for historical reasons, but this isn't really principled;
   232  			// Windows's own GetFullPathName will happily remove the first
   233  			// component of the path in this space, converting
   234  			// \\.\unc\a\b\..\c into \\.\unc\a\c.
   235  			return validVolumeNameLen(path, uncLen(path, len(`\\.\UNC\`)))
   236  		case pathHasPrefixFold(path[4:], `GLOBALROOT`):
   237  			// GLOBALROOT links to the root of the NT object namespace. A volume
   238  			// device path such as \\?\GLOBALROOT\Device\HarddiskVolume1\ has two
   239  			// components after GLOBALROOT. Treat them as the volume name so Clean
   240  			// preserves the separator denoting the volume root. This is a lexical
   241  			// approximation; other object namespace paths can have different layouts.
   242  			// (See #67880.)
   243  			return validVolumeNameLen(path, uncLen(path, len(`\\?\GLOBALROOT\`)))
   244  		}
   245  		//
   246  		// We treat the next component after the device prefix as
   247  		// part of the volume name, which means Clean(`\\?\c:\`)
   248  		// won't remove the trailing \. (See #64028.)
   249  		_, rest, ok := cutPath(path[4:])
   250  		if !ok {
   251  			return validVolumeNameLen(path, len(path))
   252  		}
   253  		return validVolumeNameLen(path, len(path)-len(rest)-1)
   254  
   255  	case len(path) >= 2 && IsPathSeparator(path[1]):
   256  		// Path starts with \\, and is a UNC path.
   257  		return validVolumeNameLen(path, uncLen(path, 2))
   258  	}
   259  	return 0
   260  }
   261  
   262  // validVolumeNameLen returns n if path[:n] is a valid Windows volume name.
   263  // If the volume name contains a ".." path component, it returns 0.
   264  func validVolumeNameLen(path string, n int) int {
   265  	for p := path[:n]; p != ""; {
   266  		var part string
   267  		part, p, _ = cutPath(p)
   268  		if part == ".." {
   269  			return 0
   270  		}
   271  	}
   272  	return n
   273  }
   274  
   275  // pathHasPrefixFold tests whether the path s begins with prefix,
   276  // ignoring case and treating all path separators as equivalent.
   277  // If s is longer than prefix, then s[len(prefix)] must be a path separator.
   278  func pathHasPrefixFold(s, prefix string) bool {
   279  	if len(s) < len(prefix) {
   280  		return false
   281  	}
   282  	for i := 0; i < len(prefix); i++ {
   283  		if IsPathSeparator(prefix[i]) {
   284  			if !IsPathSeparator(s[i]) {
   285  				return false
   286  			}
   287  		} else if toUpper(prefix[i]) != toUpper(s[i]) {
   288  			return false
   289  		}
   290  	}
   291  	if len(s) > len(prefix) && !IsPathSeparator(s[len(prefix)]) {
   292  		return false
   293  	}
   294  	return true
   295  }
   296  
   297  // uncLen returns the length of the volume prefix of a UNC path.
   298  // prefixLen is the prefix prior to the start of the UNC host;
   299  // for example, for "//host/share", the prefixLen is len("//")==2.
   300  func uncLen(path string, prefixLen int) int {
   301  	count := 0
   302  	for i := prefixLen; i < len(path); i++ {
   303  		if IsPathSeparator(path[i]) {
   304  			count++
   305  			if count == 2 {
   306  				return i
   307  			}
   308  		}
   309  	}
   310  	return len(path)
   311  }
   312  
   313  // cutPath slices path around the first path separator.
   314  func cutPath(path string) (before, after string, found bool) {
   315  	for i := range path {
   316  		if IsPathSeparator(path[i]) {
   317  			return path[:i], path[i+1:], true
   318  		}
   319  	}
   320  	return path, "", false
   321  }
   322  
   323  // postClean adjusts the results of Clean to avoid turning a relative path
   324  // into an absolute or rooted one.
   325  func postClean(out *lazybuf) {
   326  	if out.volLen != 0 || out.buf == nil {
   327  		return
   328  	}
   329  	// If a ':' appears in the path element at the start of a path,
   330  	// insert a .\ at the beginning to avoid converting relative paths
   331  	// like a/../c: into c:.
   332  	for _, c := range out.buf {
   333  		if IsPathSeparator(c) {
   334  			break
   335  		}
   336  		if c == ':' {
   337  			out.prepend('.', Separator)
   338  			return
   339  		}
   340  	}
   341  	// If a path begins with \??\, insert a \. at the beginning
   342  	// to avoid converting paths like \a\..\??\c:\x into \??\c:\x
   343  	// (equivalent to c:\x).
   344  	if len(out.buf) >= 3 && IsPathSeparator(out.buf[0]) && out.buf[1] == '?' && out.buf[2] == '?' {
   345  		out.prepend(Separator, '.')
   346  	}
   347  }
   348  

View as plain text