a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
186 lines
6.1 KiB
Go
186 lines
6.1 KiB
Go
package pathkey
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
// CaseInsensitivePaths reports whether path identity comparisons should
|
|
// ignore letter case. The default is chosen for the host filesystem:
|
|
// true on Windows and macOS (whose default NTFS / APFS volumes are
|
|
// case-insensitive), false on Linux.
|
|
//
|
|
// The environment override GORTEX_CASE_SENSITIVE_PATHS lets an operator
|
|
// correct the default for an unusual mount:
|
|
//
|
|
// - =1 (or true/yes) forces case-SENSITIVE comparisons — for a
|
|
// case-sensitive APFS or NTFS volume on macOS / Windows.
|
|
// - =0 (or false/no) forces case-INSENSITIVE comparisons — for a
|
|
// case-insensitive mount (e.g. an exFAT / SMB share) on Linux.
|
|
//
|
|
// It is a settable package variable rather than a constant so that
|
|
// higher-layer tests can flip it deterministically on any CI platform.
|
|
// A test that flips it MUST restore it via t.Cleanup and MUST NOT call
|
|
// t.Parallel — the variable is process-global.
|
|
var CaseInsensitivePaths bool
|
|
|
|
func init() {
|
|
CaseInsensitivePaths = defaultCaseInsensitive()
|
|
}
|
|
|
|
// defaultCaseInsensitive resolves the initial value of CaseInsensitivePaths
|
|
// from the environment override, falling back to the host GOOS default.
|
|
func defaultCaseInsensitive() bool {
|
|
switch strings.ToLower(strings.TrimSpace(os.Getenv("GORTEX_CASE_SENSITIVE_PATHS"))) {
|
|
case "1", "true", "yes", "on":
|
|
return false
|
|
case "0", "false", "no", "off":
|
|
return true
|
|
}
|
|
return runtime.GOOS == "windows" || runtime.GOOS == "darwin"
|
|
}
|
|
|
|
// foldPath reduces p to a canonical identity key: filepath.Clean removes
|
|
// redundant separators and dot segments, Normalize folds Unicode to NFC,
|
|
// the Windows-style volume component (drive letter or UNC root) is
|
|
// upper-cased so "c:" and "C:" agree, and — when ci is true — the
|
|
// remainder is lower-cased so a case-insensitive filesystem treats
|
|
// "Documents" and "documents" as one path.
|
|
//
|
|
// It is a pure function of (string, bool): the volume is detected with
|
|
// host-independent semantics (see volumeNameLen) so Windows and macOS
|
|
// folding can be table-tested on a Linux CI runner. It never touches the
|
|
// filesystem and never resolves symlinks — identity is lexical.
|
|
func foldPath(p string, ci bool) string {
|
|
p = filepath.Clean(p)
|
|
p = Normalize(p)
|
|
vl := volumeNameLen(p)
|
|
vol := strings.ToUpper(p[:vl])
|
|
rest := p[vl:]
|
|
if ci {
|
|
rest = strings.ToLower(rest)
|
|
}
|
|
return vol + rest
|
|
}
|
|
|
|
// EqualPaths reports whether two absolute paths identify the same
|
|
// location once folded. A byte-equal fast path mirrors Equal so the
|
|
// common case allocates nothing.
|
|
func EqualPaths(a, b string) bool {
|
|
if a == b {
|
|
return true
|
|
}
|
|
return foldPath(a, CaseInsensitivePaths) == foldPath(b, CaseInsensitivePaths)
|
|
}
|
|
|
|
// HasPathPrefix reports whether path is root itself or lies beneath it,
|
|
// comparing on folded identity and respecting path-component boundaries:
|
|
// "/Users/me/Doc" is not under "/Users/me/Documents". The volume root
|
|
// ("/" on POSIX, "C:\" on Windows) already ends in a separator after
|
|
// filepath.Clean, which the trailing-separator branch handles.
|
|
func HasPathPrefix(path, root string) bool {
|
|
fp := foldPath(path, CaseInsensitivePaths)
|
|
fr := foldPath(root, CaseInsensitivePaths)
|
|
if fp == fr {
|
|
return true
|
|
}
|
|
sep := string(filepath.Separator)
|
|
if strings.HasSuffix(fr, sep) {
|
|
return strings.HasPrefix(fp, fr)
|
|
}
|
|
return strings.HasPrefix(fp, fr+sep)
|
|
}
|
|
|
|
// NormalizeVolume returns p with its Windows-style volume component
|
|
// (drive letter or UNC root) upper-cased, e.g. "c:\x" -> "C:\x". It is a
|
|
// no-op when there is no volume component (every POSIX path) and when the
|
|
// volume is already upper-cased, so it never allocates on the common path.
|
|
//
|
|
// It is applied at store time to NEW tracked-repo entries only, to
|
|
// converge cosmetically with os.Getwd's convention (which upper-cases the
|
|
// drive letter on Windows). The volume is never part of a repo basename,
|
|
// so re-casing it cannot rotate a repo prefix or node IDs. It must never
|
|
// be applied to an already-stored path.
|
|
func NormalizeVolume(p string) string {
|
|
vl := volumeNameLen(p)
|
|
if vl == 0 {
|
|
return p
|
|
}
|
|
up := strings.ToUpper(p[:vl])
|
|
if up == p[:vl] {
|
|
return p
|
|
}
|
|
return up + p[vl:]
|
|
}
|
|
|
|
// SamePathIdentity is the store-time dedup predicate. It treats a and b
|
|
// as the same repo when they fold equal AND, when they differ byte-wise
|
|
// but both stat cleanly, os.SameFile confirms they resolve to the same
|
|
// directory — so two genuinely distinct directories on a case-sensitive
|
|
// volume are never merged. If either path fails to stat, the fold is
|
|
// trusted: a path that cannot be stat'd cannot be a distinct live repo.
|
|
//
|
|
// It stats the filesystem, so it must never be called on a per-request
|
|
// hot path — only when adding, removing, or deduping a tracked entry.
|
|
func SamePathIdentity(a, b string) bool {
|
|
if a == b {
|
|
return true
|
|
}
|
|
if !EqualPaths(a, b) {
|
|
return false
|
|
}
|
|
ai, aerr := os.Stat(a)
|
|
bi, berr := os.Stat(b)
|
|
if aerr != nil || berr != nil {
|
|
return true
|
|
}
|
|
return os.SameFile(ai, bi)
|
|
}
|
|
|
|
// volumeNameLen returns the length of the leading Windows-style volume
|
|
// component of p — a drive letter ("c:") or a UNC root ("\\host\share").
|
|
// It uses Windows semantics regardless of the host OS so that path
|
|
// folding is deterministic and testable on every platform; a POSIX path
|
|
// (which begins with "/") has no volume and yields 0. Both '\\' and '/'
|
|
// are accepted as separators because a Windows path may arrive with
|
|
// either.
|
|
func volumeNameLen(p string) int {
|
|
if len(p) < 2 {
|
|
return 0
|
|
}
|
|
// Drive letter, e.g. "c:".
|
|
if p[1] == ':' && isDriveLetter(p[0]) {
|
|
return 2
|
|
}
|
|
// UNC root, e.g. "\\host\share" or "//host/share".
|
|
if isAnySeparator(p[0]) && isAnySeparator(p[1]) {
|
|
n := len(p)
|
|
i := 2
|
|
host := i
|
|
for i < n && !isAnySeparator(p[i]) {
|
|
i++
|
|
}
|
|
if i == host {
|
|
return 0 // "\\" with no host is not a UNC volume
|
|
}
|
|
if i < n {
|
|
i++ // consume the separator before the share name
|
|
}
|
|
for i < n && !isAnySeparator(p[i]) {
|
|
i++
|
|
}
|
|
return i
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func isDriveLetter(c byte) bool {
|
|
return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')
|
|
}
|
|
|
|
func isAnySeparator(c byte) bool {
|
|
return c == '\\' || c == '/'
|
|
}
|