Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

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 == '/'
}