1220 lines
37 KiB
Go
1220 lines
37 KiB
Go
package tspath
|
||
|
||
import (
|
||
"cmp"
|
||
"slices"
|
||
"strings"
|
||
"unicode"
|
||
"unsafe"
|
||
|
||
"github.com/microsoft/typescript-go/internal/stringutil"
|
||
)
|
||
|
||
type Path string
|
||
|
||
// Internally, we represent paths as strings with '/' as the directory separator.
|
||
// When we make system calls (eg: LanguageServiceHost.getDirectory()),
|
||
// we expect the host to correctly handle paths in our specified format.
|
||
const (
|
||
DirectorySeparator = '/'
|
||
urlSchemeSeparator = "://"
|
||
)
|
||
|
||
//// Path Tests
|
||
|
||
// Determines whether a byte corresponds to `/` or `\`.
|
||
func isAnyDirectorySeparator(char byte) bool {
|
||
return char == '/' || char == '\\'
|
||
}
|
||
|
||
// Determines whether a path starts with a URL scheme (e.g. starts with `http://`, `ftp://`, `file://`, etc.).
|
||
func IsUrl(path string) bool {
|
||
return GetEncodedRootLength(path) < 0
|
||
}
|
||
|
||
// Determines whether a path is an absolute disk path (e.g. starts with `/`, or a dos path
|
||
// like `c:`, `c:\` or `c:/`).
|
||
func IsRootedDiskPath(path string) bool {
|
||
return GetEncodedRootLength(path) > 0
|
||
}
|
||
|
||
// Determines whether a path consists only of a path root.
|
||
func IsDiskPathRoot(path string) bool {
|
||
rootLength := GetEncodedRootLength(path)
|
||
return rootLength > 0 && rootLength == len(path)
|
||
}
|
||
|
||
// IsDynamicFileName returns true if the file name represents a dynamic/virtual file
|
||
// that doesn't exist on disk (e.g., untitled files with paths like "^/untitled/...").
|
||
func IsDynamicFileName(fileName string) bool {
|
||
return strings.HasPrefix(fileName, "^/")
|
||
}
|
||
|
||
// Determines whether a path starts with an absolute path component (i.e. `/`, `c:/`, `file://`, etc.).
|
||
//
|
||
// ```
|
||
// // POSIX
|
||
// PathIsAbsolute("/path/to/file.ext") === true
|
||
// // DOS
|
||
// PathIsAbsolute("c:/path/to/file.ext") === true
|
||
// // URL
|
||
// PathIsAbsolute("file:///path/to/file.ext") === true
|
||
// // Non-absolute
|
||
// PathIsAbsolute("path/to/file.ext") === false
|
||
// PathIsAbsolute("./path/to/file.ext") === false
|
||
// ```
|
||
func PathIsAbsolute(path string) bool {
|
||
return GetEncodedRootLength(path) != 0
|
||
}
|
||
|
||
func HasTrailingDirectorySeparator(path string) bool {
|
||
return len(path) > 0 && isAnyDirectorySeparator(path[len(path)-1])
|
||
}
|
||
|
||
// Combines paths. If a path is absolute, it replaces any previous path. Relative paths are not simplified.
|
||
//
|
||
// ```
|
||
// // Non-rooted
|
||
// CombinePaths("path", "to", "file.ext") === "path/to/file.ext"
|
||
// CombinePaths("path", "dir", "..", "to", "file.ext") === "path/dir/../to/file.ext"
|
||
// // POSIX
|
||
// CombinePaths("/path", "to", "file.ext") === "/path/to/file.ext"
|
||
// CombinePaths("/path", "/to", "file.ext") === "/to/file.ext"
|
||
// // DOS
|
||
// CombinePaths("c:/path", "to", "file.ext") === "c:/path/to/file.ext"
|
||
// CombinePaths("c:/path", "c:/to", "file.ext") === "c:/to/file.ext"
|
||
// // URL
|
||
// CombinePaths("file:///path", "to", "file.ext") === "file:///path/to/file.ext"
|
||
// CombinePaths("file:///path", "file:///to", "file.ext") === "file:///to/file.ext"
|
||
// ```
|
||
func CombinePaths(firstPath string, paths ...string) string {
|
||
// TODO (drosen): There is potential for a fast path here.
|
||
// In the case where we find the last absolute path and just path.Join from there.
|
||
firstPath = NormalizeSlashes(firstPath)
|
||
|
||
var b strings.Builder
|
||
size := len(firstPath) + len(paths)
|
||
for _, p := range paths {
|
||
size += len(p)
|
||
}
|
||
b.Grow(size)
|
||
|
||
b.WriteString(firstPath)
|
||
|
||
// To provide a way to "set" the path, keep track of the start and then slice.
|
||
// This will waste some memory each time we do it, but saving memory is more common.
|
||
start := 0
|
||
result := func() string {
|
||
return b.String()[start:]
|
||
}
|
||
setResult := func(value string) {
|
||
start = b.Len()
|
||
b.WriteString(value)
|
||
}
|
||
|
||
for _, trailingPath := range paths {
|
||
if trailingPath == "" {
|
||
continue
|
||
}
|
||
trailingPath = NormalizeSlashes(trailingPath)
|
||
if result() == "" || GetRootLength(trailingPath) != 0 {
|
||
// `trailingPath` is absolute.
|
||
setResult(trailingPath)
|
||
} else {
|
||
if !HasTrailingDirectorySeparator(result()) {
|
||
b.WriteByte(DirectorySeparator)
|
||
}
|
||
b.WriteString(trailingPath)
|
||
}
|
||
}
|
||
return result()
|
||
}
|
||
|
||
func GetPathComponents(path string, currentDirectory string) []string {
|
||
path = CombinePaths(currentDirectory, path)
|
||
return pathComponents(path, GetRootLength(path))
|
||
}
|
||
|
||
func pathComponents(path string, rootLength int) []string {
|
||
root := path[:rootLength]
|
||
rest := strings.Split(path[rootLength:], "/")
|
||
if len(rest) > 0 && rest[len(rest)-1] == "" {
|
||
rest = rest[:len(rest)-1]
|
||
}
|
||
return append([]string{root}, rest...)
|
||
}
|
||
|
||
func IsVolumeCharacter(char byte) bool {
|
||
return char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z'
|
||
}
|
||
|
||
func getFileUrlVolumeSeparatorEnd(url string, start int) int {
|
||
if len(url) <= start {
|
||
return -1
|
||
}
|
||
ch0 := url[start]
|
||
if ch0 == ':' {
|
||
return start + 1
|
||
}
|
||
if ch0 == '%' && len(url) > start+2 && url[start+1] == '3' {
|
||
ch2 := url[start+2]
|
||
if ch2 == 'a' || ch2 == 'A' {
|
||
return start + 3
|
||
}
|
||
}
|
||
return -1
|
||
}
|
||
|
||
func GetEncodedRootLength(path string) int {
|
||
ln := len(path)
|
||
if ln == 0 {
|
||
return 0
|
||
}
|
||
ch0 := path[0]
|
||
|
||
// POSIX or UNC
|
||
if ch0 == '/' || ch0 == '\\' {
|
||
if ln == 1 || path[1] != ch0 {
|
||
return 1 // POSIX: "/" (or non-normalized "\")
|
||
}
|
||
|
||
offset := 2
|
||
p1 := strings.IndexByte(path[offset:], ch0)
|
||
if p1 < 0 {
|
||
return ln // UNC: "//server" or "\\server"
|
||
}
|
||
|
||
return p1 + offset + 1 // UNC: "//server/" or "\\server\"
|
||
}
|
||
|
||
// DOS
|
||
if IsVolumeCharacter(ch0) && ln > 1 && path[1] == ':' {
|
||
if ln == 2 {
|
||
return 2 // DOS: "c:" (but not "c:d")
|
||
}
|
||
ch2 := path[2]
|
||
if ch2 == '/' || ch2 == '\\' {
|
||
return 3 // DOS: "c:/" or "c:\"
|
||
}
|
||
}
|
||
|
||
// Untitled paths (e.g., "^/untitled/ts-nul-authority/Untitled-1")
|
||
if ch0 == '^' && ln > 1 && path[1] == '/' {
|
||
return 2 // Untitled: "^/"
|
||
}
|
||
|
||
// URL
|
||
schemeEnd := strings.Index(path, urlSchemeSeparator)
|
||
if schemeEnd != -1 {
|
||
authorityStart := schemeEnd + len(urlSchemeSeparator)
|
||
authorityLength := strings.Index(path[authorityStart:], "/")
|
||
if authorityLength != -1 { // URL: "file:///", "file://server/", "file://server/path"
|
||
authorityEnd := authorityStart + authorityLength
|
||
|
||
// For local "file" URLs, include the leading DOS volume (if present).
|
||
// Per https://www.ietf.org/rfc/rfc1738.txt, a host of "" or "localhost" is a
|
||
// special case interpreted as "the machine from which the URL is being interpreted".
|
||
scheme := path[:schemeEnd]
|
||
authority := path[authorityStart:authorityEnd]
|
||
if scheme == "file" && (authority == "" || authority == "localhost") && (len(path) > authorityEnd+2) && IsVolumeCharacter(path[authorityEnd+1]) {
|
||
volumeSeparatorEnd := getFileUrlVolumeSeparatorEnd(path, authorityEnd+2)
|
||
if volumeSeparatorEnd != -1 {
|
||
if volumeSeparatorEnd == len(path) {
|
||
// URL: "file:///c:", "file://localhost/c:", "file:///c$3a", "file://localhost/c%3a"
|
||
// but not "file:///c:d" or "file:///c%3ad"
|
||
return ^volumeSeparatorEnd
|
||
}
|
||
if path[volumeSeparatorEnd] == '/' {
|
||
// URL: "file:///c:/", "file://localhost/c:/", "file:///c%3a/", "file://localhost/c%3a/"
|
||
return ^(volumeSeparatorEnd + 1)
|
||
}
|
||
}
|
||
}
|
||
return ^(authorityEnd + 1) // URL: "file://server/", "http://server/"
|
||
}
|
||
return ^ln // URL: "file://server", "http://server"
|
||
}
|
||
|
||
// relative
|
||
return 0
|
||
}
|
||
|
||
func GetRootLength(path string) int {
|
||
rootLength := GetEncodedRootLength(path)
|
||
if rootLength < 0 {
|
||
return ^rootLength
|
||
}
|
||
return rootLength
|
||
}
|
||
|
||
func GetDirectoryPath(path string) string {
|
||
path = NormalizeSlashes(path)
|
||
|
||
// If the path provided is itself a root, then return it.
|
||
rootLength := GetRootLength(path)
|
||
if rootLength == len(path) {
|
||
return path
|
||
}
|
||
|
||
// return the leading portion of the path up to the last (non-terminal) directory separator
|
||
// but not including any trailing directory separator.
|
||
path = RemoveTrailingDirectorySeparator(path)
|
||
return path[:max(rootLength, strings.LastIndex(path, "/"))]
|
||
}
|
||
|
||
func (p Path) GetDirectoryPath() Path {
|
||
return Path(GetDirectoryPath(string(p)))
|
||
}
|
||
|
||
func GetPathFromPathComponents(pathComponents []string) string {
|
||
if len(pathComponents) == 0 {
|
||
return ""
|
||
}
|
||
|
||
root := pathComponents[0]
|
||
if root != "" {
|
||
root = EnsureTrailingDirectorySeparator(root)
|
||
}
|
||
|
||
return root + strings.Join(pathComponents[1:], "/")
|
||
}
|
||
|
||
func NormalizeSlashes(path string) string {
|
||
return strings.ReplaceAll(path, "\\", "/")
|
||
}
|
||
|
||
func reducePathComponents(components []string) []string {
|
||
if len(components) == 0 {
|
||
return []string{}
|
||
}
|
||
reduced := []string{components[0]}
|
||
for i := 1; i < len(components); i++ {
|
||
component := components[i]
|
||
if component == "" {
|
||
continue
|
||
}
|
||
if component == "." {
|
||
continue
|
||
}
|
||
if component == ".." {
|
||
if len(reduced) > 1 {
|
||
if reduced[len(reduced)-1] != ".." {
|
||
reduced = reduced[:len(reduced)-1]
|
||
continue
|
||
}
|
||
} else if reduced[0] != "" {
|
||
continue
|
||
}
|
||
}
|
||
reduced = append(reduced, component)
|
||
}
|
||
return reduced
|
||
}
|
||
|
||
// Combines and resolves paths. If a path is absolute, it replaces any previous path. Any
|
||
// `.` and `..` path components are resolved. Trailing directory separators are preserved.
|
||
//
|
||
// ```go
|
||
// resolvePath("/path", "to", "file.ext") == "path/to/file.ext"
|
||
// resolvePath("/path", "to", "file.ext/") == "path/to/file.ext/"
|
||
// resolvePath("/path", "dir", "..", "to", "file.ext") == "path/to/file.ext"
|
||
// ```
|
||
func ResolvePath(path string, paths ...string) string {
|
||
var combinedPath string
|
||
if len(paths) > 0 {
|
||
combinedPath = CombinePaths(path, paths...)
|
||
} else {
|
||
combinedPath = NormalizeSlashes(path)
|
||
}
|
||
return NormalizePath(combinedPath)
|
||
}
|
||
|
||
func ResolveTripleslashReference(moduleName string, containingFile string) string {
|
||
basePath := GetDirectoryPath(containingFile)
|
||
if IsRootedDiskPath(moduleName) {
|
||
return NormalizePath(moduleName)
|
||
}
|
||
return NormalizePath(CombinePaths(basePath, moduleName))
|
||
}
|
||
|
||
func GetNormalizedPathComponents(path string, currentDirectory string) []string {
|
||
combined := CombinePaths(currentDirectory, path)
|
||
return getNormalizedPathComponentsFromCombined(combined)
|
||
}
|
||
|
||
func getNormalizedPathComponentsFromCombined(path string) []string {
|
||
rootLength := GetRootLength(path)
|
||
// Always include the root component (empty string for relative paths).
|
||
components := make([]string, 1, 8)
|
||
components[0] = path[:rootLength]
|
||
|
||
for i := rootLength; i < len(path); {
|
||
// Skip directory separators (handles consecutive separators and trailing '/').
|
||
for i < len(path) && path[i] == '/' {
|
||
i++
|
||
}
|
||
if i >= len(path) {
|
||
break
|
||
}
|
||
|
||
start := i
|
||
for i < len(path) && path[i] != '/' {
|
||
i++
|
||
}
|
||
component := path[start:i]
|
||
|
||
if component == "" || component == "." {
|
||
continue
|
||
}
|
||
if component == ".." {
|
||
if len(components) > 1 {
|
||
if components[len(components)-1] != ".." {
|
||
components = components[:len(components)-1]
|
||
continue
|
||
}
|
||
} else if components[0] != "" {
|
||
// If this is an absolute path, we can't go above the root.
|
||
continue
|
||
}
|
||
}
|
||
|
||
components = append(components, component)
|
||
}
|
||
|
||
return components
|
||
}
|
||
|
||
func GetNormalizedAbsolutePathWithoutRoot(fileName string, currentDirectory string) string {
|
||
absolutePath := GetNormalizedAbsolutePath(fileName, currentDirectory)
|
||
rootLength := GetRootLength(absolutePath)
|
||
return absolutePath[rootLength:]
|
||
}
|
||
|
||
func GetNormalizedAbsolutePath(fileName string, currentDirectory string) string {
|
||
rootLength := GetRootLength(fileName)
|
||
if rootLength == 0 && currentDirectory != "" {
|
||
fileName = CombinePaths(currentDirectory, fileName)
|
||
} else {
|
||
// CombinePaths normalizes slashes, so not necessary in other branch
|
||
fileName = NormalizeSlashes(fileName)
|
||
}
|
||
rootLength = GetRootLength(fileName)
|
||
|
||
if simpleNormalized, ok := simpleNormalizePath(fileName); ok {
|
||
length := len(simpleNormalized)
|
||
if length > rootLength {
|
||
return RemoveTrailingDirectorySeparator(simpleNormalized)
|
||
}
|
||
if length == rootLength && rootLength != 0 {
|
||
return EnsureTrailingDirectorySeparator(simpleNormalized)
|
||
}
|
||
return simpleNormalized
|
||
}
|
||
|
||
length := len(fileName)
|
||
root := fileName[:rootLength]
|
||
// `normalized` is only initialized once `fileName` is determined to be non-normalized.
|
||
// `changed` is set at the same time.
|
||
var changed bool
|
||
var normalized string
|
||
var segmentStart int
|
||
index := rootLength
|
||
normalizedUpTo := index
|
||
seenNonDotDotSegment := rootLength != 0
|
||
for index < length {
|
||
// At beginning of segment
|
||
segmentStart = index
|
||
ch := fileName[index]
|
||
for ch == '/' {
|
||
index++
|
||
if index < length {
|
||
ch = fileName[index]
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
if index > segmentStart {
|
||
// Seen superfluous separator
|
||
if !changed {
|
||
normalized = fileName[:max(rootLength, segmentStart-1)]
|
||
changed = true
|
||
}
|
||
if index == length {
|
||
break
|
||
}
|
||
segmentStart = index
|
||
}
|
||
// Past any superfluous separators
|
||
segmentEnd := strings.IndexByte(fileName[index+1:], '/')
|
||
if segmentEnd == -1 {
|
||
segmentEnd = length
|
||
} else {
|
||
segmentEnd += index + 1
|
||
}
|
||
segmentLength := segmentEnd - segmentStart
|
||
if segmentLength == 1 && fileName[index] == '.' {
|
||
// "." segment (skip)
|
||
if !changed {
|
||
normalized = fileName[:normalizedUpTo]
|
||
changed = true
|
||
}
|
||
} else if segmentLength == 2 && fileName[index] == '.' && fileName[index+1] == '.' {
|
||
// ".." segment
|
||
if !seenNonDotDotSegment {
|
||
if changed {
|
||
if len(normalized) == rootLength {
|
||
normalized += ".."
|
||
} else {
|
||
normalized += "/.."
|
||
}
|
||
} else {
|
||
normalizedUpTo = index + 2
|
||
}
|
||
} else if !changed {
|
||
if normalizedUpTo-1 >= 0 {
|
||
normalized = fileName[:max(rootLength, strings.LastIndexByte(fileName[:normalizedUpTo-1], '/'))]
|
||
} else {
|
||
normalized = fileName[:normalizedUpTo]
|
||
}
|
||
changed = true
|
||
seenNonDotDotSegment = (len(normalized) != rootLength || rootLength != 0) && normalized != ".." && !strings.HasSuffix(normalized, "/..")
|
||
} else {
|
||
lastSlash := strings.LastIndexByte(normalized, '/')
|
||
if lastSlash != -1 {
|
||
normalized = normalized[:max(rootLength, lastSlash)]
|
||
} else {
|
||
normalized = root
|
||
}
|
||
seenNonDotDotSegment = (len(normalized) != rootLength || rootLength != 0) && normalized != ".." && !strings.HasSuffix(normalized, "/..")
|
||
}
|
||
} else if changed {
|
||
if len(normalized) != rootLength {
|
||
normalized += "/"
|
||
}
|
||
seenNonDotDotSegment = true
|
||
normalized += fileName[segmentStart:segmentEnd]
|
||
} else {
|
||
seenNonDotDotSegment = true
|
||
normalizedUpTo = segmentEnd
|
||
}
|
||
index = segmentEnd + 1
|
||
}
|
||
if changed {
|
||
return normalized
|
||
}
|
||
if length > rootLength {
|
||
return RemoveTrailingDirectorySeparators(fileName)
|
||
}
|
||
if length == rootLength {
|
||
return EnsureTrailingDirectorySeparator(fileName)
|
||
}
|
||
return fileName
|
||
}
|
||
|
||
func simpleNormalizePath(path string) (string, bool) {
|
||
// Most paths don't require normalization
|
||
if !hasRelativePathSegment(path) {
|
||
return path, true
|
||
}
|
||
// Some paths only require cleanup of `/./` or leading `./`
|
||
simplified := strings.ReplaceAll(path, "/./", "/")
|
||
trimmed := strings.TrimPrefix(simplified, "./")
|
||
if trimmed != path && !hasRelativePathSegment(trimmed) && !(trimmed != simplified && strings.HasPrefix(trimmed, "/")) {
|
||
// If we trimmed a leading "./" and the path now starts with "/", we changed the meaning
|
||
path = trimmed
|
||
return path, true
|
||
}
|
||
return "", false
|
||
}
|
||
|
||
// hasRelativePathSegment reports whether p contains ".", "..", "./", "../", "/.", "/..", "//", "/./", or "/../".
|
||
func hasRelativePathSegment(p string) bool {
|
||
n := len(p)
|
||
if n == 0 {
|
||
return false
|
||
}
|
||
|
||
if p == "." || p == ".." {
|
||
return true
|
||
}
|
||
|
||
// Leading "./" OR "../"
|
||
if p[0] == '.' {
|
||
if n >= 2 && p[1] == '/' {
|
||
return true
|
||
}
|
||
// Leading "../"
|
||
if n >= 3 && p[1] == '.' && p[2] == '/' {
|
||
return true
|
||
}
|
||
}
|
||
// Trailing "/." OR "/.."
|
||
if p[n-1] == '.' {
|
||
if n >= 2 && p[n-2] == '/' {
|
||
return true
|
||
}
|
||
if n >= 3 && p[n-2] == '.' && p[n-3] == '/' {
|
||
return true
|
||
}
|
||
}
|
||
|
||
// Now look for any `//` or `/./` or `/../`
|
||
|
||
prevSlash := false
|
||
segLen := 0 // length of current segment since last slash
|
||
dotCount := 0 // consecutive dots at start of the current segment; -1 => not only dots
|
||
|
||
for i := range n {
|
||
c := p[i]
|
||
if c == '/' {
|
||
// "//"
|
||
if prevSlash {
|
||
return true
|
||
}
|
||
// "/./" or "/../"
|
||
if (segLen == 1 && dotCount == 1) || (segLen == 2 && dotCount == 2) {
|
||
return true
|
||
}
|
||
prevSlash = true
|
||
segLen = 0
|
||
dotCount = 0
|
||
continue
|
||
}
|
||
|
||
if c == '.' {
|
||
if dotCount >= 0 {
|
||
dotCount++
|
||
}
|
||
} else {
|
||
dotCount = -1
|
||
}
|
||
segLen++
|
||
prevSlash = false
|
||
}
|
||
|
||
// Trailing "/." or "/.."
|
||
return (segLen == 1 && dotCount == 1) || (segLen == 2 && dotCount == 2)
|
||
}
|
||
|
||
func NormalizePath(path string) string {
|
||
path = NormalizeSlashes(path)
|
||
if normalized, ok := simpleNormalizePath(path); ok {
|
||
return normalized
|
||
}
|
||
normalized := GetNormalizedAbsolutePath(path, "")
|
||
if normalized != "" && HasTrailingDirectorySeparator(path) {
|
||
normalized = EnsureTrailingDirectorySeparator(normalized)
|
||
}
|
||
return normalized
|
||
}
|
||
|
||
func GetCanonicalFileName(fileName string, useCaseSensitiveFileNames bool) string {
|
||
if useCaseSensitiveFileNames {
|
||
return fileName
|
||
}
|
||
return ToFileNameLowerCase(fileName)
|
||
}
|
||
|
||
// We convert the file names to lower case as key for file name on case insensitive file system
|
||
// While doing so we need to handle special characters (eg \u0130) to ensure that we dont convert
|
||
// it to lower case, fileName with its lowercase form can exist along side it.
|
||
// Handle special characters and make those case sensitive instead
|
||
//
|
||
// |-#--|-Unicode--|-Char code-|-Desc-------------------------------------------------------------------|
|
||
// | 1. | i | 105 | Ascii i |
|
||
// | 2. | I | 73 | Ascii I |
|
||
// |-------- Special characters ------------------------------------------------------------------------|
|
||
// | 3. | \u0130 | 304 | Upper case I with dot above |
|
||
// | 4. | i,\u0307 | 105,775 | i, followed by 775: Lower case of (3rd item) |
|
||
// | 5. | I,\u0307 | 73,775 | I, followed by 775: Upper case of (4th item), lower case is (4th item) |
|
||
// | 6. | \u0131 | 305 | Lower case i without dot, upper case is I (2nd item) |
|
||
// | 7. | \u00DF | 223 | Lower case sharp s |
|
||
//
|
||
// Because item 3 is special where in its lowercase character has its own
|
||
// upper case form we cant convert its case.
|
||
// Rest special characters are either already in lower case format or
|
||
// they have corresponding upper case character so they dont need special handling
|
||
|
||
func ToFileNameLowerCase(fileName string) string {
|
||
const IWithDot = '\u0130'
|
||
|
||
ascii := true
|
||
needsLower := false
|
||
fileNameLen := len(fileName)
|
||
for i := range fileNameLen {
|
||
c := fileName[i]
|
||
if c >= 0x80 {
|
||
ascii = false
|
||
break
|
||
}
|
||
if 'A' <= c && c <= 'Z' {
|
||
needsLower = true
|
||
}
|
||
}
|
||
if ascii {
|
||
if !needsLower {
|
||
return fileName
|
||
}
|
||
b := make([]byte, fileNameLen)
|
||
for i := range fileNameLen {
|
||
c := fileName[i]
|
||
if 'A' <= c && c <= 'Z' {
|
||
c += 'a' - 'A' // +32
|
||
}
|
||
b[i] = c
|
||
}
|
||
// SAFETY: We construct a string that aliases b’s backing array without copying.
|
||
// (1) Lifetime: The address of b’s elements escapes via the returned string,
|
||
// so escape analysis allocates b’s backing array on the heap. The string
|
||
// header points to that heap allocation, ensuring it remains live for the
|
||
// string’s lifetime.
|
||
// (2) Initialization: We assign to every b[i] before creating the string.
|
||
// (Note: Go zeroes all allocated memory, so “uninitialized” bytes cannot occur.)
|
||
// (3) Immutability: We do not modify b after this point, so the string view
|
||
// observes immutable data.
|
||
// (4) Non-empty: On this path len(b) > 0, so &b[0] is a valid, non-nil pointer.
|
||
return unsafe.String(&b[0], len(b))
|
||
}
|
||
|
||
return strings.Map(func(r rune) rune {
|
||
if r == IWithDot {
|
||
return r
|
||
}
|
||
return unicode.ToLower(r)
|
||
}, fileName)
|
||
}
|
||
|
||
func ToPath(fileName string, basePath string, useCaseSensitiveFileNames bool) Path {
|
||
var nonCanonicalizedPath string
|
||
if IsRootedDiskPath(fileName) {
|
||
nonCanonicalizedPath = NormalizePath(fileName)
|
||
} else {
|
||
nonCanonicalizedPath = GetNormalizedAbsolutePath(fileName, basePath)
|
||
}
|
||
return Path(GetCanonicalFileName(nonCanonicalizedPath, useCaseSensitiveFileNames))
|
||
}
|
||
|
||
func RemoveTrailingDirectorySeparator(path string) string {
|
||
if HasTrailingDirectorySeparator(path) {
|
||
return path[:len(path)-1]
|
||
}
|
||
return path
|
||
}
|
||
|
||
func (p Path) RemoveTrailingDirectorySeparator() Path {
|
||
return Path(RemoveTrailingDirectorySeparator(string(p)))
|
||
}
|
||
|
||
func RemoveTrailingDirectorySeparators(path string) string {
|
||
for HasTrailingDirectorySeparator(path) {
|
||
path = RemoveTrailingDirectorySeparator(path)
|
||
}
|
||
return path
|
||
}
|
||
|
||
func EnsureTrailingDirectorySeparator(path string) string {
|
||
if !HasTrailingDirectorySeparator(path) {
|
||
return path + "/"
|
||
}
|
||
|
||
return path
|
||
}
|
||
|
||
func (p Path) EnsureTrailingDirectorySeparator() Path {
|
||
return Path(EnsureTrailingDirectorySeparator(string(p)))
|
||
}
|
||
|
||
//// Relative Paths
|
||
|
||
func GetPathComponentsRelativeTo(from string, to string, options ComparePathsOptions) []string {
|
||
fromComponents := reducePathComponents(GetPathComponents(from, options.CurrentDirectory))
|
||
toComponents := reducePathComponents(GetPathComponents(to, options.CurrentDirectory))
|
||
|
||
start := 0
|
||
maxCommonComponents := min(len(fromComponents), len(toComponents))
|
||
stringEqualer := options.getEqualityComparer()
|
||
for ; start < maxCommonComponents; start++ {
|
||
fromComponent := fromComponents[start]
|
||
toComponent := toComponents[start]
|
||
if start == 0 {
|
||
if !stringutil.EquateStringCaseInsensitive(fromComponent, toComponent) {
|
||
break
|
||
}
|
||
} else {
|
||
if !stringEqualer(fromComponent, toComponent) {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
if start == 0 {
|
||
return toComponents
|
||
}
|
||
|
||
numDotDotSlashes := len(fromComponents) - start
|
||
result := make([]string, 1+numDotDotSlashes+len(toComponents)-start)
|
||
|
||
result[0] = ""
|
||
i := 1
|
||
// Add all the relative components until we hit a common directory.
|
||
for range numDotDotSlashes {
|
||
result[i] = ".."
|
||
i++
|
||
}
|
||
// Now add all the remaining components of the "to" path.
|
||
for _, component := range toComponents[start:] {
|
||
result[i] = component
|
||
i++
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
func GetRelativePathFromDirectory(fromDirectory string, to string, options ComparePathsOptions) string {
|
||
if (GetRootLength(fromDirectory) > 0) != (GetRootLength(to) > 0) {
|
||
panic("paths must either both be absolute or both be relative")
|
||
}
|
||
pathComponents := GetPathComponentsRelativeTo(fromDirectory, to, options)
|
||
return GetPathFromPathComponents(pathComponents)
|
||
}
|
||
|
||
func GetRelativePathFromFile(from string, to string, options ComparePathsOptions) string {
|
||
return EnsurePathIsNonModuleName(GetRelativePathFromDirectory(GetDirectoryPath(from), to, options))
|
||
}
|
||
|
||
func ConvertToRelativePath(absoluteOrRelativePath string, options ComparePathsOptions) string {
|
||
if !IsRootedDiskPath(absoluteOrRelativePath) {
|
||
return absoluteOrRelativePath
|
||
}
|
||
|
||
return GetRelativePathToDirectoryOrUrl(options.CurrentDirectory, absoluteOrRelativePath, false /*isAbsolutePathAnUrl*/, options)
|
||
}
|
||
|
||
func GetRelativePathToDirectoryOrUrl(directoryPathOrUrl string, relativeOrAbsolutePath string, isAbsolutePathAnUrl bool, options ComparePathsOptions) string {
|
||
pathComponents := GetPathComponentsRelativeTo(
|
||
directoryPathOrUrl,
|
||
relativeOrAbsolutePath,
|
||
options,
|
||
)
|
||
|
||
firstComponent := pathComponents[0]
|
||
if isAbsolutePathAnUrl && IsRootedDiskPath(firstComponent) {
|
||
var prefix string
|
||
if firstComponent[0] == DirectorySeparator {
|
||
prefix = "file://"
|
||
} else {
|
||
prefix = "file:///"
|
||
}
|
||
pathComponents[0] = prefix + firstComponent
|
||
}
|
||
|
||
return GetPathFromPathComponents(pathComponents)
|
||
}
|
||
|
||
// Gets the portion of a path following the last (non-terminal) separator (`/`).
|
||
// Semantics align with NodeJS's `path.basename` except that we support URL's as well.
|
||
// If the base name has any one of the provided extensions, it is removed.
|
||
//
|
||
// // POSIX
|
||
// GetBaseFileName("/path/to/file.ext") == "file.ext"
|
||
// GetBaseFileName("/path/to/") == "to"
|
||
// GetBaseFileName("/") == ""
|
||
// // DOS
|
||
// GetBaseFileName("c:/path/to/file.ext") == "file.ext"
|
||
// GetBaseFileName("c:/path/to/") == "to"
|
||
// GetBaseFileName("c:/") == ""
|
||
// GetBaseFileName("c:") == ""
|
||
// // URL
|
||
// GetBaseFileName("http://typescriptlang.org/path/to/file.ext") == "file.ext"
|
||
// GetBaseFileName("http://typescriptlang.org/path/to/") == "to"
|
||
// GetBaseFileName("http://typescriptlang.org/") == ""
|
||
// GetBaseFileName("http://typescriptlang.org") == ""
|
||
// GetBaseFileName("file://server/path/to/file.ext") == "file.ext"
|
||
// GetBaseFileName("file://server/path/to/") == "to"
|
||
// GetBaseFileName("file://server/") == ""
|
||
// GetBaseFileName("file://server") == ""
|
||
// GetBaseFileName("file:///path/to/file.ext") == "file.ext"
|
||
// GetBaseFileName("file:///path/to/") == "to"
|
||
// GetBaseFileName("file:///") == ""
|
||
// GetBaseFileName("file://") == ""
|
||
func GetBaseFileName(path string) string {
|
||
path = NormalizeSlashes(path)
|
||
|
||
// if the path provided is itself the root, then it has no file name.
|
||
rootLength := GetRootLength(path)
|
||
if rootLength == len(path) {
|
||
return ""
|
||
}
|
||
|
||
// return the trailing portion of the path starting after the last (non-terminal) directory
|
||
// separator but not including any trailing directory separator.
|
||
path = RemoveTrailingDirectorySeparator(path)
|
||
return path[max(GetRootLength(path), strings.LastIndex(path, string(DirectorySeparator))+1):]
|
||
}
|
||
|
||
// Gets the file extension for a path.
|
||
// If extensions are provided, gets the file extension for a path, provided it is one of the provided extensions.
|
||
//
|
||
// GetAnyExtensionFromPath("/path/to/file.ext", nil, false) == ".ext"
|
||
// GetAnyExtensionFromPath("/path/to/file.ext/", nil, false) == ".ext"
|
||
// GetAnyExtensionFromPath("/path/to/file", nil, false) == ""
|
||
// GetAnyExtensionFromPath("/path/to.ext/file", nil, false) == ""
|
||
// GetAnyExtensionFromPath("/path/to/file.ext", ".ext", true) === ".ext"
|
||
// GetAnyExtensionFromPath("/path/to/file.js", ".ext", true) === ""
|
||
// GetAnyExtensionFromPath("/path/to/file.js", [".ext", ".js"], true) === ".js"
|
||
// GetAnyExtensionFromPath("/path/to/file.ext", ".EXT", false) === ""
|
||
func GetAnyExtensionFromPath(path string, extensions []string, ignoreCase bool) string {
|
||
// Retrieves any string from the final "." onwards from a base file name.
|
||
// Unlike extensionFromPath, which throws an exception on unrecognized extensions.
|
||
if len(extensions) > 0 {
|
||
return getAnyExtensionFromPathWorker(RemoveTrailingDirectorySeparator(path), extensions, stringutil.GetStringEqualityComparer(ignoreCase))
|
||
}
|
||
|
||
baseFileName := GetBaseFileName(path)
|
||
extensionIndex := strings.LastIndex(baseFileName, ".")
|
||
if extensionIndex >= 0 {
|
||
return baseFileName[extensionIndex:]
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func getAnyExtensionFromPathWorker(path string, extensions []string, stringEqualityComparer func(a, b string) bool) string {
|
||
for _, extension := range extensions {
|
||
result := tryGetExtensionFromPath(path, extension, stringEqualityComparer)
|
||
if result != "" {
|
||
return result
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func tryGetExtensionFromPath(path string, extension string, stringEqualityComparer func(a, b string) bool) string {
|
||
if !strings.HasPrefix(extension, ".") {
|
||
extension = "." + extension
|
||
}
|
||
if len(path) >= len(extension) && path[len(path)-len(extension)] == '.' {
|
||
pathExtension := path[len(path)-len(extension):]
|
||
if stringEqualityComparer(pathExtension, extension) {
|
||
return pathExtension
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func PathIsRelative(path string) bool {
|
||
// True if path is ".", "..", or starts with "./", "../", ".\\", or "..\\".
|
||
|
||
if path == "." || path == ".." {
|
||
return true
|
||
}
|
||
|
||
if len(path) >= 2 && path[0] == '.' && (path[1] == '/' || path[1] == '\\') {
|
||
return true
|
||
}
|
||
|
||
if len(path) >= 3 && path[0] == '.' && path[1] == '.' && (path[2] == '/' || path[2] == '\\') {
|
||
return true
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
// EnsurePathIsNonModuleName ensures a path is either absolute (prefixed with `/` or `c:`) or dot-relative (prefixed
|
||
// with `./` or `../`) so as not to be confused with an unprefixed module name.
|
||
func EnsurePathIsNonModuleName(path string) string {
|
||
if !PathIsAbsolute(path) && !PathIsRelative(path) {
|
||
return "./" + path
|
||
}
|
||
return path
|
||
}
|
||
|
||
func IsExternalModuleNameRelative(moduleName string) bool {
|
||
// TypeScript 1.0 spec (April 2014): 11.2.1
|
||
// An external module name is "relative" if the first term is "." or "..".
|
||
// Update: We also consider a path like `C:\foo.ts` "relative" because we do not search for it in `node_modules` or treat it as an ambient module.
|
||
return PathIsRelative(moduleName) || IsRootedDiskPath(moduleName)
|
||
}
|
||
|
||
type ComparePathsOptions struct {
|
||
UseCaseSensitiveFileNames bool
|
||
CurrentDirectory string
|
||
}
|
||
|
||
func (o ComparePathsOptions) GetComparer() func(a, b string) int {
|
||
return stringutil.GetStringComparer(!o.UseCaseSensitiveFileNames)
|
||
}
|
||
|
||
func (o ComparePathsOptions) getEqualityComparer() func(a, b string) bool {
|
||
return stringutil.GetStringEqualityComparer(!o.UseCaseSensitiveFileNames)
|
||
}
|
||
|
||
func ComparePaths(a string, b string, options ComparePathsOptions) int {
|
||
a = CombinePaths(options.CurrentDirectory, a)
|
||
b = CombinePaths(options.CurrentDirectory, b)
|
||
|
||
if a == b {
|
||
return 0
|
||
}
|
||
if a == "" {
|
||
return -1
|
||
}
|
||
if b == "" {
|
||
return 1
|
||
}
|
||
|
||
// NOTE: Performance optimization - shortcut if the root segments differ as there would be no
|
||
// need to perform path reduction.
|
||
aRoot := a[:GetRootLength(a)]
|
||
bRoot := b[:GetRootLength(b)]
|
||
result := stringutil.CompareStringsCaseInsensitive(aRoot, bRoot)
|
||
if result != 0 {
|
||
return result
|
||
}
|
||
|
||
// NOTE: Performance optimization - shortcut if there are no relative path segments in
|
||
// the non-root portion of the path
|
||
aRest := a[len(aRoot):]
|
||
bRest := b[len(bRoot):]
|
||
if !hasRelativePathSegment(aRest) && !hasRelativePathSegment(bRest) {
|
||
return options.GetComparer()(aRest, bRest)
|
||
}
|
||
|
||
// The path contains a relative path segment. Normalize the paths and perform a slower component
|
||
// by component comparison.
|
||
aComponents := reducePathComponents(GetPathComponents(a, ""))
|
||
bComponents := reducePathComponents(GetPathComponents(b, ""))
|
||
sharedLength := min(len(aComponents), len(bComponents))
|
||
for i := 1; i < sharedLength; i++ {
|
||
result := options.GetComparer()(aComponents[i], bComponents[i])
|
||
if result != 0 {
|
||
return result
|
||
}
|
||
}
|
||
return cmp.Compare(len(aComponents), len(bComponents))
|
||
}
|
||
|
||
func ComparePathsCaseSensitive(a string, b string, currentDirectory string) int {
|
||
return ComparePaths(a, b, ComparePathsOptions{UseCaseSensitiveFileNames: true, CurrentDirectory: currentDirectory})
|
||
}
|
||
|
||
func ComparePathsCaseInsensitive(a string, b string, currentDirectory string) int {
|
||
return ComparePaths(a, b, ComparePathsOptions{UseCaseSensitiveFileNames: false, CurrentDirectory: currentDirectory})
|
||
}
|
||
|
||
func ContainsPath(parent string, child string, options ComparePathsOptions) bool {
|
||
parent = CombinePaths(options.CurrentDirectory, parent)
|
||
child = CombinePaths(options.CurrentDirectory, child)
|
||
if parent == "" || child == "" {
|
||
return false
|
||
}
|
||
if parent == child {
|
||
return true
|
||
}
|
||
parentComponents := reducePathComponents(GetPathComponents(parent, ""))
|
||
childComponents := reducePathComponents(GetPathComponents(child, ""))
|
||
if len(childComponents) < len(parentComponents) {
|
||
return false
|
||
}
|
||
|
||
componentComparer := options.getEqualityComparer()
|
||
for i, parentComponent := range parentComponents {
|
||
var comparer func(a, b string) bool
|
||
if i == 0 {
|
||
comparer = stringutil.EquateStringCaseInsensitive
|
||
} else {
|
||
comparer = componentComparer
|
||
}
|
||
if !comparer(parentComponent, childComponents[i]) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
// ContainsPath checks whether child is contained within or equal to p.
|
||
// Since Path values are already rooted, reduced, and case-canonicalized,
|
||
// this is a simple string prefix check.
|
||
func (p Path) ContainsPath(child Path) bool {
|
||
if len(p) == 0 {
|
||
return false
|
||
}
|
||
return p == child || len(child) > len(p) && strings.HasPrefix(string(child), string(p)) && (p[len(p)-1] == '/' || child[len(p)] == '/')
|
||
}
|
||
|
||
func FileExtensionIs(path string, extension string) bool {
|
||
return len(path) > len(extension) && strings.HasSuffix(path, extension)
|
||
}
|
||
|
||
// Calls `callback` on `directory` and every ancestor directory it has, returning the first defined result.
|
||
// Stops at global cache location
|
||
func ForEachAncestorDirectoryStoppingAtGlobalCache[T any](
|
||
globalCacheLocation string,
|
||
directory string,
|
||
callback func(directory string) (result T, stop bool),
|
||
) T {
|
||
result, _ := ForEachAncestorDirectory(directory, func(ancestorDirectory string) (T, bool) {
|
||
result, stop := callback(ancestorDirectory)
|
||
if stop || ancestorDirectory == globalCacheLocation {
|
||
return result, true
|
||
}
|
||
return result, false
|
||
})
|
||
return result
|
||
}
|
||
|
||
func ForEachAncestorDirectory[T any](directory string, callback func(directory string) (result T, stop bool)) (result T, ok bool) {
|
||
for {
|
||
result, stop := callback(directory)
|
||
if stop {
|
||
return result, true
|
||
}
|
||
|
||
parentPath := GetDirectoryPath(directory)
|
||
if parentPath == directory {
|
||
var zero T
|
||
return zero, false
|
||
}
|
||
|
||
directory = parentPath
|
||
}
|
||
}
|
||
|
||
func ForEachAncestorDirectoryPath[T any](directory Path, callback func(directory Path) (result T, stop bool)) (result T, ok bool) {
|
||
return ForEachAncestorDirectory(string(directory), func(directory string) (T, bool) {
|
||
return callback(Path(directory))
|
||
})
|
||
}
|
||
|
||
func HasExtension(fileName string) bool {
|
||
return strings.Contains(GetBaseFileName(fileName), ".")
|
||
}
|
||
|
||
func SplitVolumePath(path string) (volume string, rest string, ok bool) {
|
||
if len(path) >= 2 && IsVolumeCharacter(path[0]) && path[1] == ':' {
|
||
return strings.ToLower(path[0:2]), path[2:], true
|
||
}
|
||
return "", path, false
|
||
}
|
||
|
||
// GetCommonParents returns the smallest set of directories that are parents of all paths with
|
||
// at least `minComponents` directory components. Any path that has fewer than `minComponents` directory components
|
||
// will be returned in the second return value. Examples:
|
||
//
|
||
// /a/b/c/d, /a/b/c/e, /a/b/f/g => /a/b
|
||
// /a/b/c/d, /a/b/c/e, /a/b/f/g, /x/y => /
|
||
// /a/b/c/d, /a/b/c/e, /a/b/f/g, /x/y (minComponents: 2) => /a/b, /x/y
|
||
// c:/a/b/c/d, d:/a/b/c/d => c:/a/b/c/d, d:/a/b/c/d
|
||
func GetCommonParents(
|
||
paths []string,
|
||
minComponents int,
|
||
getPathComponents func(path string, currentDirectory string) []string,
|
||
options ComparePathsOptions,
|
||
) (parents []string, ignored map[string]struct{}) {
|
||
if minComponents < 1 {
|
||
panic("minComponents must be at least 1")
|
||
}
|
||
if len(paths) == 0 {
|
||
return nil, nil
|
||
}
|
||
if len(paths) == 1 {
|
||
if len(reducePathComponents(getPathComponents(paths[0], options.CurrentDirectory))) < minComponents {
|
||
return nil, map[string]struct{}{paths[0]: {}}
|
||
}
|
||
return paths, nil
|
||
}
|
||
|
||
ignored = make(map[string]struct{})
|
||
pathComponents := make([][]string, 0, len(paths))
|
||
for _, path := range paths {
|
||
components := reducePathComponents(getPathComponents(path, options.CurrentDirectory))
|
||
if len(components) < minComponents {
|
||
ignored[path] = struct{}{}
|
||
} else {
|
||
pathComponents = append(pathComponents, components)
|
||
}
|
||
}
|
||
|
||
results := getCommonParentsWorker(pathComponents, minComponents, options)
|
||
resultPaths := make([]string, len(results))
|
||
for i, comps := range results {
|
||
resultPaths[i] = GetPathFromPathComponents(comps)
|
||
}
|
||
|
||
return resultPaths, ignored
|
||
}
|
||
|
||
func getCommonParentsWorker(componentGroups [][]string, minComponents int, options ComparePathsOptions) [][]string {
|
||
if len(componentGroups) == 0 {
|
||
return nil
|
||
}
|
||
// Determine the maximum depth we can consider
|
||
maxDepth := len(componentGroups[0])
|
||
for _, comps := range componentGroups[1:] {
|
||
if l := len(comps); l < maxDepth {
|
||
maxDepth = l
|
||
}
|
||
}
|
||
|
||
equality := options.getEqualityComparer()
|
||
for lastCommonIndex := range maxDepth {
|
||
candidate := componentGroups[0][lastCommonIndex]
|
||
for j, comps := range componentGroups[1:] {
|
||
if !equality(candidate, comps[lastCommonIndex]) { // divergence
|
||
if lastCommonIndex < minComponents {
|
||
// Not enough components, we need to fan out
|
||
orderedGroups := make([]Path, 0, len(componentGroups)-j)
|
||
newGroups := make(map[Path]struct {
|
||
head []string
|
||
tails [][]string
|
||
})
|
||
for _, g := range componentGroups {
|
||
key := ToPath(g[lastCommonIndex], options.CurrentDirectory, options.UseCaseSensitiveFileNames)
|
||
if _, ok := newGroups[key]; !ok {
|
||
orderedGroups = append(orderedGroups, key)
|
||
}
|
||
newGroups[key] = struct {
|
||
head []string
|
||
tails [][]string
|
||
}{
|
||
head: g[:lastCommonIndex+1],
|
||
tails: append(newGroups[key].tails, g[lastCommonIndex+1:]),
|
||
}
|
||
}
|
||
slices.Sort(orderedGroups)
|
||
result := make([][]string, 0, len(newGroups))
|
||
for _, key := range orderedGroups {
|
||
group := newGroups[key]
|
||
subResults := getCommonParentsWorker(group.tails, minComponents-(lastCommonIndex+1), options)
|
||
for _, sr := range subResults {
|
||
result = append(result, append(group.head, sr...))
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
return [][]string{componentGroups[0][:lastCommonIndex]}
|
||
}
|
||
}
|
||
}
|
||
|
||
return [][]string{componentGroups[0][:maxDepth]}
|
||
}
|
||
|
||
func StartsWithDirectory(fileName string, directoryName string, useCaseSensitiveFileNames bool) bool {
|
||
if directoryName == "" {
|
||
return false
|
||
}
|
||
|
||
canonicalFileName := GetCanonicalFileName(fileName, useCaseSensitiveFileNames)
|
||
canonicalDirectoryName := GetCanonicalFileName(directoryName, useCaseSensitiveFileNames)
|
||
canonicalDirectoryName = strings.TrimSuffix(canonicalDirectoryName, "/")
|
||
canonicalDirectoryName = strings.TrimSuffix(canonicalDirectoryName, "\\")
|
||
|
||
return strings.HasPrefix(canonicalFileName, canonicalDirectoryName+"/") ||
|
||
strings.HasPrefix(canonicalFileName, canonicalDirectoryName+"\\")
|
||
}
|
||
|
||
func CompareNumberOfDirectorySeparators(path1, path2 string) int {
|
||
return cmp.Compare(strings.Count(path1, "/"), strings.Count(path2, "/"))
|
||
}
|