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  	"internal/safefilepath"
     9  	"os"
    10  	"strings"
    11  	"syscall"
    12  )
    13  
    14  func isSlash(c uint8) bool {
    15  	return c == '\\' || c == '/'
    16  }
    17  
    18  func toUpper(c byte) byte {
    19  	if 'a' <= c && c <= 'z' {
    20  		return c - ('a' - 'A')
    21  	}
    22  	return c
    23  }
    24  
    25  func isLocal(path string) bool {
    26  	if path == "" {
    27  		return false
    28  	}
    29  	if isSlash(path[0]) {
    30  		// Path rooted in the current drive.
    31  		return false
    32  	}
    33  	if strings.IndexByte(path, ':') >= 0 {
    34  		// Colons are only valid when marking a drive letter ("C:foo").
    35  		// Rejecting any path with a colon is conservative but safe.
    36  		return false
    37  	}
    38  	hasDots := false // contains . or .. path elements
    39  	for p := path; p != ""; {
    40  		var part string
    41  		part, p, _ = cutPath(p)
    42  		if part == "." || part == ".." {
    43  			hasDots = true
    44  		}
    45  		if safefilepath.IsReservedName(part) {
    46  			return false
    47  		}
    48  	}
    49  	if hasDots {
    50  		path = Clean(path)
    51  	}
    52  	if path == ".." || strings.HasPrefix(path, `..\`) {
    53  		return false
    54  	}
    55  	return true
    56  }
    57  
    58  // IsAbs reports whether the path is absolute.
    59  func IsAbs(path string) (b bool) {
    60  	l := volumeNameLen(path)
    61  	if l == 0 {
    62  		return false
    63  	}
    64  	// If the volume name starts with a double slash, this is an absolute path.
    65  	if isSlash(path[0]) && isSlash(path[1]) {
    66  		return true
    67  	}
    68  	path = path[l:]
    69  	if path == "" {
    70  		return false
    71  	}
    72  	return isSlash(path[0])
    73  }
    74  
    75  // volumeNameLen returns length of the leading volume name on Windows.
    76  // It returns 0 elsewhere.
    77  //
    78  // See:
    79  // https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
    80  // https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
    81  func volumeNameLen(path string) int {
    82  	switch {
    83  	case len(path) >= 2 && path[1] == ':':
    84  		// Path starts with a drive letter.
    85  		//
    86  		// Not all Windows functions necessarily enforce the requirement that
    87  		// drive letters be in the set A-Z, and we don't try to here.
    88  		//
    89  		// We don't handle the case of a path starting with a non-ASCII character,
    90  		// in which case the "drive letter" might be multiple bytes long.
    91  		return 2
    92  
    93  	case len(path) == 0 || !isSlash(path[0]):
    94  		// Path does not have a volume component.
    95  		return 0
    96  
    97  	case pathHasPrefixFold(path, `\\.\UNC`):
    98  		// We're going to treat the UNC host and share as part of the volume
    99  		// prefix for historical reasons, but this isn't really principled;
   100  		// Windows's own GetFullPathName will happily remove the first
   101  		// component of the path in this space, converting
   102  		// \\.\unc\a\b\..\c into \\.\unc\a\c.
   103  		return uncLen(path, len(`\\.\UNC\`))
   104  
   105  	case pathHasPrefixFold(path, `\\.`) ||
   106  		pathHasPrefixFold(path, `\\?`) || pathHasPrefixFold(path, `\??`):
   107  		// Path starts with \\.\, and is a Local Device path; or
   108  		// path starts with \\?\ or \??\ and is a Root Local Device path.
   109  		//
   110  		// We treat the next component after the \\.\ prefix as
   111  		// part of the volume name, which means Clean(`\\?\c:\`)
   112  		// won't remove the trailing \. (See #64028.)
   113  		if len(path) == 3 {
   114  			return 3 // exactly \\.
   115  		}
   116  		_, rest, ok := cutPath(path[4:])
   117  		if !ok {
   118  			return len(path)
   119  		}
   120  		return len(path) - len(rest) - 1
   121  
   122  	case len(path) >= 2 && isSlash(path[1]):
   123  		// Path starts with \\, and is a UNC path.
   124  		return uncLen(path, 2)
   125  	}
   126  	return 0
   127  }
   128  
   129  // pathHasPrefixFold tests whether the path s begins with prefix,
   130  // ignoring case and treating all path separators as equivalent.
   131  // If s is longer than prefix, then s[len(prefix)] must be a path separator.
   132  func pathHasPrefixFold(s, prefix string) bool {
   133  	if len(s) < len(prefix) {
   134  		return false
   135  	}
   136  	for i := 0; i < len(prefix); i++ {
   137  		if isSlash(prefix[i]) {
   138  			if !isSlash(s[i]) {
   139  				return false
   140  			}
   141  		} else if toUpper(prefix[i]) != toUpper(s[i]) {
   142  			return false
   143  		}
   144  	}
   145  	if len(s) > len(prefix) && !isSlash(s[len(prefix)]) {
   146  		return false
   147  	}
   148  	return true
   149  }
   150  
   151  // uncLen returns the length of the volume prefix of a UNC path.
   152  // prefixLen is the prefix prior to the start of the UNC host;
   153  // for example, for "//host/share", the prefixLen is len("//")==2.
   154  func uncLen(path string, prefixLen int) int {
   155  	count := 0
   156  	for i := prefixLen; i < len(path); i++ {
   157  		if isSlash(path[i]) {
   158  			count++
   159  			if count == 2 {
   160  				return i
   161  			}
   162  		}
   163  	}
   164  	return len(path)
   165  }
   166  
   167  // cutPath slices path around the first path separator.
   168  func cutPath(path string) (before, after string, found bool) {
   169  	for i := range path {
   170  		if isSlash(path[i]) {
   171  			return path[:i], path[i+1:], true
   172  		}
   173  	}
   174  	return path, "", false
   175  }
   176  
   177  // HasPrefix exists for historical compatibility and should not be used.
   178  //
   179  // Deprecated: HasPrefix does not respect path boundaries and
   180  // does not ignore case when required.
   181  func HasPrefix(p, prefix string) bool {
   182  	if strings.HasPrefix(p, prefix) {
   183  		return true
   184  	}
   185  	return strings.HasPrefix(strings.ToLower(p), strings.ToLower(prefix))
   186  }
   187  
   188  func splitList(path string) []string {
   189  	// The same implementation is used in LookPath in os/exec;
   190  	// consider changing os/exec when changing this.
   191  
   192  	if path == "" {
   193  		return []string{}
   194  	}
   195  
   196  	// Split path, respecting but preserving quotes.
   197  	list := []string{}
   198  	start := 0
   199  	quo := false
   200  	for i := 0; i < len(path); i++ {
   201  		switch c := path[i]; {
   202  		case c == '"':
   203  			quo = !quo
   204  		case c == ListSeparator && !quo:
   205  			list = append(list, path[start:i])
   206  			start = i + 1
   207  		}
   208  	}
   209  	list = append(list, path[start:])
   210  
   211  	// Remove quotes.
   212  	for i, s := range list {
   213  		list[i] = strings.ReplaceAll(s, `"`, ``)
   214  	}
   215  
   216  	return list
   217  }
   218  
   219  func abs(path string) (string, error) {
   220  	if path == "" {
   221  		// syscall.FullPath returns an error on empty path, because it's not a valid path.
   222  		// To implement Abs behavior of returning working directory on empty string input,
   223  		// special-case empty path by changing it to "." path. See golang.org/issue/24441.
   224  		path = "."
   225  	}
   226  	fullPath, err := syscall.FullPath(path)
   227  	if err != nil {
   228  		return "", err
   229  	}
   230  	return Clean(fullPath), nil
   231  }
   232  
   233  func join(elem []string) string {
   234  	var b strings.Builder
   235  	var lastChar byte
   236  	for _, e := range elem {
   237  		switch {
   238  		case b.Len() == 0:
   239  			// Add the first non-empty path element unchanged.
   240  		case isSlash(lastChar):
   241  			// If the path ends in a slash, strip any leading slashes from the next
   242  			// path element to avoid creating a UNC path (any path starting with "\\")
   243  			// from non-UNC elements.
   244  			//
   245  			// The correct behavior for Join when the first element is an incomplete UNC
   246  			// path (for example, "\\") is underspecified. We currently join subsequent
   247  			// elements so Join("\\", "host", "share") produces "\\host\share".
   248  			for len(e) > 0 && isSlash(e[0]) {
   249  				e = e[1:]
   250  			}
   251  			// If the path is \ and the next path element is ??,
   252  			// add an extra .\ to create \.\?? rather than \??\
   253  			// (a Root Local Device path).
   254  			if b.Len() == 1 && pathHasPrefixFold(e, "??") {
   255  				b.WriteString(`.\`)
   256  			}
   257  		case lastChar == ':':
   258  			// If the path ends in a colon, keep the path relative to the current directory
   259  			// on a drive and don't add a separator. Preserve leading slashes in the next
   260  			// path element, which may make the path absolute.
   261  			//
   262  			// 	Join(`C:`, `f`) = `C:f`
   263  			//	Join(`C:`, `\f`) = `C:\f`
   264  		default:
   265  			// In all other cases, add a separator between elements.
   266  			b.WriteByte('\\')
   267  			lastChar = '\\'
   268  		}
   269  		if len(e) > 0 {
   270  			b.WriteString(e)
   271  			lastChar = e[len(e)-1]
   272  		}
   273  	}
   274  	if b.Len() == 0 {
   275  		return ""
   276  	}
   277  	return Clean(b.String())
   278  }
   279  
   280  // joinNonEmpty is like join, but it assumes that the first element is non-empty.
   281  func joinNonEmpty(elem []string) string {
   282  	if len(elem[0]) == 2 && elem[0][1] == ':' {
   283  		// First element is drive letter without terminating slash.
   284  		// Keep path relative to current directory on that drive.
   285  		// Skip empty elements.
   286  		i := 1
   287  		for ; i < len(elem); i++ {
   288  			if elem[i] != "" {
   289  				break
   290  			}
   291  		}
   292  		return Clean(elem[0] + strings.Join(elem[i:], string(Separator)))
   293  	}
   294  	// The following logic prevents Join from inadvertently creating a
   295  	// UNC path on Windows. Unless the first element is a UNC path, Join
   296  	// shouldn't create a UNC path. See golang.org/issue/9167.
   297  	p := Clean(strings.Join(elem, string(Separator)))
   298  	if !isUNC(p) {
   299  		return p
   300  	}
   301  	// p == UNC only allowed when the first element is a UNC path.
   302  	head := Clean(elem[0])
   303  	if isUNC(head) {
   304  		return p
   305  	}
   306  	// head + tail == UNC, but joining two non-UNC paths should not result
   307  	// in a UNC path. Undo creation of UNC path.
   308  	tail := Clean(strings.Join(elem[1:], string(Separator)))
   309  	if head[len(head)-1] == Separator {
   310  		return head + tail
   311  	}
   312  	return head + string(Separator) + tail
   313  }
   314  
   315  // isUNC reports whether path is a UNC path.
   316  func isUNC(path string) bool {
   317  	return len(path) > 1 && isSlash(path[0]) && isSlash(path[1])
   318  }
   319  
   320  func sameWord(a, b string) bool {
   321  	return strings.EqualFold(a, b)
   322  }
   323  
   324  // postClean adjusts the results of Clean to avoid turning a relative path
   325  // into an absolute or rooted one.
   326  func postClean(out *lazybuf) {
   327  	if out.volLen != 0 || out.buf == nil {
   328  		return
   329  	}
   330  	// If a ':' appears in the path element at the start of a path,
   331  	// insert a .\ at the beginning to avoid converting relative paths
   332  	// like a/../c: into c:.
   333  	for _, c := range out.buf {
   334  		if os.IsPathSeparator(c) {
   335  			break
   336  		}
   337  		if c == ':' {
   338  			out.prepend('.', Separator)
   339  			return
   340  		}
   341  	}
   342  	// If a path begins with \??\, insert a \. at the beginning
   343  	// to avoid converting paths like \a\..\??\c:\x into \??\c:\x
   344  	// (equivalent to c:\x).
   345  	if len(out.buf) >= 3 && os.IsPathSeparator(out.buf[0]) && out.buf[1] == '?' && out.buf[2] == '?' {
   346  		out.prepend(Separator, '.')
   347  	}
   348  }
   349  

View as plain text