path_windows.mx raw

   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 []byte) 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 []byte
  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 []byte) ([]byte, 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 []byte
  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 = []byte(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 []byte) 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 []byte) bool {
 129  	if len(name) == 3 {
 130  		switch []byte([]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 []byte([]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 []byte) 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 []byte) (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 []byte) 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 []byte) 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 []byte, 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 []byte) (before, after []byte, 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