blob: c490424f20c968b33486eb32f826f2548a9ce032 [file] [log] [blame]
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package filepath
import (
"internal/safefilepath"
"os"
"strings"
"syscall"
)
func isSlash(c uint8) bool {
return c == '\\' || c == '/'
}
func toUpper(c byte) byte {
if 'a' <= c && c <= 'z' {
return c - ('a' - 'A')
}
return c
}
func isLocal(path string) bool {
if path == "" {
return false
}
if isSlash(path[0]) {
// Path rooted in the current drive.
return false
}
if strings.IndexByte(path, ':') >= 0 {
// Colons are only valid when marking a drive letter ("C:foo").
// Rejecting any path with a colon is conservative but safe.
return false
}
hasDots := false // contains . or .. path elements
for p := path; p != ""; {
var part string
part, p, _ = cutPath(p)
if part == "." || part == ".." {
hasDots = true
}
if safefilepath.IsReservedName(part) {
return false
}
}
if hasDots {
path = Clean(path)
}
if path == ".." || strings.HasPrefix(path, `..\`) {
return false
}
return true
}
// IsAbs reports whether the path is absolute.
func IsAbs(path string) (b bool) {
l := volumeNameLen(path)
if l == 0 {
return false
}
// If the volume name starts with a double slash, this is an absolute path.
if isSlash(path[0]) && isSlash(path[1]) {
return true
}
path = path[l:]
if path == "" {
return false
}
return isSlash(path[0])
}
// volumeNameLen returns length of the leading volume name on Windows.
// It returns 0 elsewhere.
//
// See:
// https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
// https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
func volumeNameLen(path string) int {
switch {
case len(path) >= 2 && path[1] == ':':
// Path starts with a drive letter.
//
// Not all Windows functions necessarily enforce the requirement that
// drive letters be in the set A-Z, and we don't try to here.
//
// We don't handle the case of a path starting with a non-ASCII character,
// in which case the "drive letter" might be multiple bytes long.
return 2
case len(path) == 0 || !isSlash(path[0]):
// Path does not have a volume component.
return 0
case pathHasPrefixFold(path, `\\.\UNC`):
// We're going to treat the UNC host and share as part of the volume
// prefix for historical reasons, but this isn't really principled;
// Windows's own GetFullPathName will happily remove the first
// component of the path in this space, converting
// \\.\unc\a\b\..\c into \\.\unc\a\c.
return uncLen(path, len(`\\.\UNC\`))
case pathHasPrefixFold(path, `\\.`):
// Path starts with \\., and is a Local Device path.
//
// We currently treat the next component after the \\.\ prefix
// as part of the volume name, although there doesn't seem to be
// a principled reason to do this.
if len(path) == 3 {
return 3 // exactly \\.
}
_, rest, ok := cutPath(path[4:])
if !ok {
return len(path)
}
return len(path) - len(rest) - 1
case pathHasPrefixFold(path, `\\?`) || pathHasPrefixFold(path, `\??`):
// Path starts with \\?\ or \??\, and is a Root Local Device path.
//
// While Windows usually treats / and \ as equivalent,
// /??/ does not seem to be recognized as a Root Local Device path.
// We treat it as one anyway here to be safe.
return 3
case len(path) >= 2 && isSlash(path[1]):
// Path starts with \\, and is a UNC path.
return uncLen(path, 2)
}
return 0
}
// pathHasPrefixFold tests whether the path s begins with prefix,
// ignoring case and treating all path separators as equivalent.
// If s is longer than prefix, then s[len(prefix)] must be a path separator.
func pathHasPrefixFold(s, prefix string) bool {
if len(s) < len(prefix) {
return false
}
for i := 0; i < len(prefix); i++ {
if isSlash(prefix[i]) {
if !isSlash(s[i]) {
return false
}
} else if toUpper(prefix[i]) != toUpper(s[i]) {
return false
}
}
if len(s) > len(prefix) && !isSlash(s[len(prefix)]) {
return false
}
return true
}
// uncLen returns the length of the volume prefix of a UNC path.
// prefixLen is the prefix prior to the start of the UNC host;
// for example, for "//host/share", the prefixLen is len("//")==2.
func uncLen(path string, prefixLen int) int {
count := 0
for i := prefixLen; i < len(path); i++ {
if isSlash(path[i]) {
count++
if count == 2 {
return i
}
}
}
return len(path)
}
// cutPath slices path around the first path separator.
func cutPath(path string) (before, after string, found bool) {
for i := range path {
if isSlash(path[i]) {
return path[:i], path[i+1:], true
}
}
return path, "", false
}
// HasPrefix exists for historical compatibility and should not be used.
//
// Deprecated: HasPrefix does not respect path boundaries and
// does not ignore case when required.
func HasPrefix(p, prefix string) bool {
if strings.HasPrefix(p, prefix) {
return true
}
return strings.HasPrefix(strings.ToLower(p), strings.ToLower(prefix))
}
func splitList(path string) []string {
// The same implementation is used in LookPath in os/exec;
// consider changing os/exec when changing this.
if path == "" {
return []string{}
}
// Split path, respecting but preserving quotes.
list := []string{}
start := 0
quo := false
for i := 0; i < len(path); i++ {
switch c := path[i]; {
case c == '"':
quo = !quo
case c == ListSeparator && !quo:
list = append(list, path[start:i])
start = i + 1
}
}
list = append(list, path[start:])
// Remove quotes.
for i, s := range list {
list[i] = strings.ReplaceAll(s, `"`, ``)
}
return list
}
func abs(path string) (string, error) {
if path == "" {
// syscall.FullPath returns an error on empty path, because it's not a valid path.
// To implement Abs behavior of returning working directory on empty string input,
// special-case empty path by changing it to "." path. See golang.org/issue/24441.
path = "."
}
fullPath, err := syscall.FullPath(path)
if err != nil {
return "", err
}
return Clean(fullPath), nil
}
func join(elem []string) string {
var b strings.Builder
var lastChar byte
for _, e := range elem {
switch {
case b.Len() == 0:
// Add the first non-empty path element unchanged.
case isSlash(lastChar):
// If the path ends in a slash, strip any leading slashes from the next
// path element to avoid creating a UNC path (any path starting with "\\")
// from non-UNC elements.
//
// The correct behavior for Join when the first element is an incomplete UNC
// path (for example, "\\") is underspecified. We currently join subsequent
// elements so Join("\\", "host", "share") produces "\\host\share".
for len(e) > 0 && isSlash(e[0]) {
e = e[1:]
}
// If the path is \ and the next path element is ??,
// add an extra .\ to create \.\?? rather than \??\
// (a Root Local Device path).
if b.Len() == 1 && pathHasPrefixFold(e, "??") {
b.WriteString(`.\`)
}
case lastChar == ':':
// If the path ends in a colon, keep the path relative to the current directory
// on a drive and don't add a separator. Preserve leading slashes in the next
// path element, which may make the path absolute.
//
// Join(`C:`, `f`) = `C:f`
// Join(`C:`, `\f`) = `C:\f`
default:
// In all other cases, add a separator between elements.
b.WriteByte('\\')
lastChar = '\\'
}
if len(e) > 0 {
b.WriteString(e)
lastChar = e[len(e)-1]
}
}
if b.Len() == 0 {
return ""
}
return Clean(b.String())
}
// joinNonEmpty is like join, but it assumes that the first element is non-empty.
func joinNonEmpty(elem []string) string {
if len(elem[0]) == 2 && elem[0][1] == ':' {
// First element is drive letter without terminating slash.
// Keep path relative to current directory on that drive.
// Skip empty elements.
i := 1
for ; i < len(elem); i++ {
if elem[i] != "" {
break
}
}
return Clean(elem[0] + strings.Join(elem[i:], string(Separator)))
}
// The following logic prevents Join from inadvertently creating a
// UNC path on Windows. Unless the first element is a UNC path, Join
// shouldn't create a UNC path. See golang.org/issue/9167.
p := Clean(strings.Join(elem, string(Separator)))
if !isUNC(p) {
return p
}
// p == UNC only allowed when the first element is a UNC path.
head := Clean(elem[0])
if isUNC(head) {
return p
}
// head + tail == UNC, but joining two non-UNC paths should not result
// in a UNC path. Undo creation of UNC path.
tail := Clean(strings.Join(elem[1:], string(Separator)))
if head[len(head)-1] == Separator {
return head + tail
}
return head + string(Separator) + tail
}
// isUNC reports whether path is a UNC path.
func isUNC(path string) bool {
return len(path) > 1 && isSlash(path[0]) && isSlash(path[1])
}
func sameWord(a, b string) bool {
return strings.EqualFold(a, b)
}
// postClean adjusts the results of Clean to avoid turning a relative path
// into an absolute or rooted one.
func postClean(out *lazybuf) {
if out.volLen != 0 || out.buf == nil {
return
}
// If a ':' appears in the path element at the start of a path,
// insert a .\ at the beginning to avoid converting relative paths
// like a/../c: into c:.
for _, c := range out.buf {
if os.IsPathSeparator(c) {
break
}
if c == ':' {
out.prepend('.', Separator)
return
}
}
// If a path begins with \??\, insert a \. at the beginning
// to avoid converting paths like \a\..\??\c:\x into \??\c:\x
// (equivalent to c:\x).
if len(out.buf) >= 3 && os.IsPathSeparator(out.buf[0]) && out.buf[1] == '?' && out.buf[2] == '?' {
out.prepend(Separator, '.')
}
}