From da4cd3aa48ca56b34bc9636d269344e94c5718b2 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Sat, 28 Mar 2026 18:47:04 -0600 Subject: [PATCH] feat(path/winpath): add sync script and patches from Go stdlib - Add sync.sh to download and apply patches from Go standard library - Add patches/*.diff for transforming internal/filepathlite to winpath - Backup Nextron's original diff for reference - Add .gitignore for orig/ directory - Add README with usage examples and attribution --- path/winpath/.gitignore | 5 + path/winpath/README.md | 84 +++++ path/winpath/patches/path_lite.diff | 234 ++++++++++++ .../patches/path_lite_windowsspecific.diff | 301 ++++++++++++++++ .../winpath/patches/path_windowsspecific.diff | 116 ++++++ path/winpath/patches/windows.diff.nextron.bak | 341 ++++++++++++++++++ path/winpath/sync.sh | 174 +++++++++ 7 files changed, 1255 insertions(+) create mode 100644 path/winpath/.gitignore create mode 100644 path/winpath/README.md create mode 100644 path/winpath/patches/path_lite.diff create mode 100644 path/winpath/patches/path_lite_windowsspecific.diff create mode 100644 path/winpath/patches/path_windowsspecific.diff create mode 100644 path/winpath/patches/windows.diff.nextron.bak create mode 100755 path/winpath/sync.sh diff --git a/path/winpath/.gitignore b/path/winpath/.gitignore new file mode 100644 index 0000000..898f662 --- /dev/null +++ b/path/winpath/.gitignore @@ -0,0 +1,5 @@ +# Ignore downloaded original files +orig/ +*.orig +*.tmp +*.tmp.d \ No newline at end of file diff --git a/path/winpath/README.md b/path/winpath/README.md new file mode 100644 index 0000000..f4b3c0d --- /dev/null +++ b/path/winpath/README.md @@ -0,0 +1,84 @@ +# path/winpath + +[![Go Reference](https://pkg.go.dev/badge/github.com/therootcompany/golib/path/winpath.svg)](https://pkg.go.dev/github.com/therootcompany/golib/path/winpath) + +Windows-style path manipulation that works on any platform. + +## Installation + +```bash +go get github.com/therootcompany/golib/path/winpath +``` + +## Usage + +```go +import winpath "github.com/therootcompany/golib/path/winpath" + +// Clean a Windows path (from any platform) +clean := winpath.Clean(`C:\foo\..\bar`) // C:\bar + +// Join path elements +joined := winpath.Join(`C:\`, "foo", "bar.txt") // C:\foo\bar.txt + +// Split path into directory and file +dir, file := winpath.Split(`C:\foo\bar.txt`) // C:\foo\, bar.txt + +// Get file extension +ext := winpath.Ext(`C:\foo\bar.txt`) // .txt + +// Get base name +base := winpath.Base(`C:\foo\bar.txt`) // bar.txt + +// Get directory +d := winpath.Dir(`C:\foo\bar.txt`) // C:\foo + +// Check if path is absolute +winpath.IsAbs(`C:\foo`) // true +winpath.IsAbs(`foo\bar`) // false +winpath.IsAbs(`\foo`) // false (rooted but no drive) +winpath.IsAbs(`\\server\share`) // true (UNC) + +// Get volume name +vol := winpath.VolumeName(`C:\foo\bar`) // C: +len := winpath.VolumeNameLen(`C:\foo`) // 2 + +// Convert separators +fwd := winpath.ToSlash(`C:\foo\bar`) // C:/foo/bar +bck := winpath.FromSlash(`C:/foo/bar`) // C:\foo\bar + +// Constants +winpath.Separator // '\' +winpath.ListSeparator // ';' +winpath.IsPathSeparator('\\') // true +winpath.IsPathSeparator('/') // true +``` + +## Comparison with stdlib + +| Feature | `path/filepath` | `path/winpath` | +|---------|-----------------|----------------| +| Platform aware | Yes (uses OS) | No (always Windows) | +| Use alongside `filepath` | N/A | ✅ Works together | + +Use `path/filepath` for native OS paths on any platform. +Use `path/winpath` when you need to handle Windows paths on non-Windows platforms (e.g., parsing config files, cross-platform tools). + +## Attribution + +This package is derived from the Go standard library's `internal/filepathlite` and `path/filepath` packages, adapted from [NextronSystems/universalpath](https://github.com/NextronSystems/universalpath). + +### License + +The Go Authors. See the [Go license](https://golang.org/LICENSE). + +## Syncing from Go stdlib + +To update from newer Go versions: + +```bash +cd path/winpath +GO_VERSION=go1.22.0 ./sync.sh +``` + +Run `./sync.sh diff` to regenerate patches after manual edits. \ No newline at end of file diff --git a/path/winpath/patches/path_lite.diff b/path/winpath/patches/path_lite.diff new file mode 100644 index 0000000..c0b2132 --- /dev/null +++ b/path/winpath/patches/path_lite.diff @@ -0,0 +1,234 @@ +--- orig/path_lite.go 2026-03-28 16:17:06 ++++ path_lite.go 2026-03-28 16:17:03 +@@ -2,25 +2,18 @@ + // Use of this source code is governed by a BSD-style + // license that can be found in the LICENSE file. + +-// Package filepathlite implements a subset of path/filepath, +-// only using packages which may be imported by "os". +-// +-// Tests for these functions are in path/filepath. +-package filepathlite ++// Package windows provides Windows-style path manipulation ++// that works on any platform. ++package winpath + + import ( + "errors" +- "internal/stringslite" +- "io/fs" + "slices" ++ "strings" + ) + + var errInvalidPath = errors.New("invalid path") + +-// A lazybuf is a lazily constructed path buffer. +-// It supports append, reading previously appended bytes, +-// and retrieving the final string. It does not allocate a buffer +-// to hold the output until that output diverges from s. + type lazybuf struct { + path string + buf []byte +@@ -61,25 +54,18 @@ + return b.volAndPath[:b.volLen] + string(b.buf[:b.w]) + } + +-// Clean is filepath.Clean. + func Clean(path string) string { + originalPath := path + volLen := volumeNameLen(path) + path = path[volLen:] + if path == "" { + if volLen > 1 && IsPathSeparator(originalPath[0]) && IsPathSeparator(originalPath[1]) { +- // should be UNC + return FromSlash(originalPath) + } + return originalPath + "." + } + rooted := IsPathSeparator(path[0]) + +- // Invariants: +- // reading from path; r is index of next byte to process. +- // writing to buf; w is index of next byte to write. +- // dotdot is index in buf where .. must stop, either because +- // it is the leading slash or it is a leading ../../.. prefix. + n := len(path) + out := lazybuf{path: path, volAndPath: originalPath, volLen: volLen} + r, dotdot := 0, 0 +@@ -91,23 +77,18 @@ + for r < n { + switch { + case IsPathSeparator(path[r]): +- // empty path element + r++ + case path[r] == '.' && (r+1 == n || IsPathSeparator(path[r+1])): +- // . element + r++ + case path[r] == '.' && path[r+1] == '.' && (r+2 == n || IsPathSeparator(path[r+2])): +- // .. element: remove to last separator + r += 2 + switch { + case out.w > dotdot: +- // can backtrack + out.w-- + for out.w > dotdot && !IsPathSeparator(out.index(out.w)) { + out.w-- + } + case !rooted: +- // cannot backtrack, but not rooted, so append .. element. + if out.w > 0 { + out.append(Separator) + } +@@ -116,63 +97,23 @@ + dotdot = out.w + } + default: +- // real path element. +- // add slash if needed + if rooted && out.w != 1 || !rooted && out.w != 0 { + out.append(Separator) + } +- // copy element + for ; r < n && !IsPathSeparator(path[r]); r++ { + out.append(path[r]) + } + } + } + +- // Turn empty string into "." + if out.w == 0 { + out.append('.') + } + +- postClean(&out) // avoid creating absolute paths on Windows ++ postClean(&out) + return FromSlash(out.string()) + } + +-// IsLocal is filepath.IsLocal. +-func IsLocal(path string) bool { +- return isLocal(path) +-} +- +-func unixIsLocal(path string) bool { +- if IsAbs(path) || path == "" { +- return false +- } +- hasDots := false +- for p := path; p != ""; { +- var part string +- part, p, _ = stringslite.Cut(p, "/") +- if part == "." || part == ".." { +- hasDots = true +- break +- } +- } +- if hasDots { +- path = Clean(path) +- } +- if path == ".." || stringslite.HasPrefix(path, "../") { +- return false +- } +- return true +-} +- +-// Localize is filepath.Localize. +-func Localize(path string) (string, error) { +- if !fs.ValidPath(path) { +- return "", errInvalidPath +- } +- return localize(path) +-} +- +-// ToSlash is filepath.ToSlash. + func ToSlash(path string) string { + if Separator == '/' { + return path +@@ -180,7 +121,6 @@ + return replaceStringByte(path, Separator, '/') + } + +-// FromSlash is filepath.FromSlash. + func FromSlash(path string) string { + if Separator == '/' { + return path +@@ -189,7 +129,7 @@ + } + + func replaceStringByte(s string, old, new byte) string { +- if stringslite.IndexByte(s, old) == -1 { ++ if strings.IndexByte(s, old) == -1 { + return s + } + n := []byte(s) +@@ -201,7 +141,6 @@ + return string(n) + } + +-// Split is filepath.Split. + func Split(path string) (dir, file string) { + vol := VolumeName(path) + i := len(path) - 1 +@@ -211,7 +150,6 @@ + return path[:i+1], path[i+1:] + } + +-// Ext is filepath.Ext. + func Ext(path string) string { + for i := len(path) - 1; i >= 0 && !IsPathSeparator(path[i]); i-- { + if path[i] == '.' { +@@ -221,18 +159,14 @@ + return "" + } + +-// Base is filepath.Base. + func Base(path string) string { + if path == "" { + return "." + } +- // Strip trailing slashes. + for len(path) > 0 && IsPathSeparator(path[len(path)-1]) { + path = path[0 : len(path)-1] + } +- // Throw away volume name + path = path[len(VolumeName(path)):] +- // Find the last element + i := len(path) - 1 + for i >= 0 && !IsPathSeparator(path[i]) { + i-- +@@ -240,14 +174,12 @@ + if i >= 0 { + path = path[i+1:] + } +- // If empty now, it had only slashes. + if path == "" { + return string(Separator) + } + return path + } + +-// Dir is filepath.Dir. + func Dir(path string) string { + vol := VolumeName(path) + i := len(path) - 1 +@@ -256,19 +188,15 @@ + } + dir := Clean(path[len(vol) : i+1]) + if dir == "." && len(vol) > 2 { +- // must be UNC + return vol + } + return vol + dir + } + +-// VolumeName is filepath.VolumeName. + func VolumeName(path string) string { + return FromSlash(path[:volumeNameLen(path)]) + } + +-// VolumeNameLen returns the length of the leading volume name on Windows. +-// It returns 0 elsewhere. + func VolumeNameLen(path string) int { + return volumeNameLen(path) + } diff --git a/path/winpath/patches/path_lite_windowsspecific.diff b/path/winpath/patches/path_lite_windowsspecific.diff new file mode 100644 index 0000000..2741b95 --- /dev/null +++ b/path/winpath/patches/path_lite_windowsspecific.diff @@ -0,0 +1,301 @@ +--- orig/path_lite_windowsspecific.go 2026-03-28 16:15:19 ++++ path_lite_windowsspecific.go 2026-03-28 16:14:52 +@@ -2,15 +2,8 @@ + // Use of this source code is governed by a BSD-style + // license that can be found in the LICENSE file. + +-package filepathlite ++package winpath + +-import ( +- "internal/bytealg" +- "internal/stringslite" +- "internal/syscall/windows" +- "syscall" +-) +- + const ( + Separator = '\\' // OS-specific path separator + ListSeparator = ';' // OS-specific path list separator +@@ -20,159 +13,6 @@ + return c == '\\' || c == '/' + } + +-func isLocal(path string) bool { +- if path == "" { +- return false +- } +- if IsPathSeparator(path[0]) { +- // Path rooted in the current drive. +- return false +- } +- if stringslite.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 isReservedName(part) { +- return false +- } +- } +- if hasDots { +- path = Clean(path) +- } +- if path == ".." || stringslite.HasPrefix(path, `..\`) { +- return false +- } +- return true +-} +- +-func localize(path string) (string, error) { +- for i := 0; i < len(path); i++ { +- switch path[i] { +- case ':', '\\', 0: +- return "", errInvalidPath +- } +- } +- containsSlash := false +- for p := path; p != ""; { +- // Find the next path element. +- var element string +- i := bytealg.IndexByteString(p, '/') +- if i < 0 { +- element = p +- p = "" +- } else { +- containsSlash = true +- element = p[:i] +- p = p[i+1:] +- } +- if isReservedName(element) { +- return "", errInvalidPath +- } +- } +- if containsSlash { +- // We can't depend on strings, so substitute \ for / manually. +- buf := []byte(path) +- for i, b := range buf { +- if b == '/' { +- buf[i] = '\\' +- } +- } +- path = string(buf) +- } +- return path, nil +-} +- +-// isReservedName reports if name is a Windows reserved device name. +-// It does not detect names with an extension, which are also reserved on some Windows versions. +-// +-// For details, search for PRN in +-// https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file. +-func isReservedName(name string) bool { +- // Device names can have arbitrary trailing characters following a dot or colon. +- base := name +- for i := 0; i < len(base); i++ { +- switch base[i] { +- case ':', '.': +- base = base[:i] +- } +- } +- // Trailing spaces in the last path element are ignored. +- for len(base) > 0 && base[len(base)-1] == ' ' { +- base = base[:len(base)-1] +- } +- if !isReservedBaseName(base) { +- return false +- } +- if len(base) == len(name) { +- return true +- } +- // The path element is a reserved name with an extension. +- // Since Windows 11, reserved names with extensions are no +- // longer reserved. For example, "CON.txt" is a valid file +- // name. Use RtlIsDosDeviceName_U to see if the name is reserved. +- p, err := syscall.UTF16PtrFromString(name) +- if err != nil { +- return false +- } +- return windows.RtlIsDosDeviceName_U(p) > 0 +-} +- +-func isReservedBaseName(name string) bool { +- if len(name) == 3 { +- switch string([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) { +- case "CON", "PRN", "AUX", "NUL": +- return true +- } +- } +- if len(name) >= 4 { +- switch string([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) { +- case "COM", "LPT": +- if len(name) == 4 && '1' <= name[3] && name[3] <= '9' { +- return true +- } +- // Superscript ¹, ², and ³ are considered numbers as well. +- switch name[3:] { +- case "\u00b2", "\u00b3", "\u00b9": +- return true +- } +- return false +- } +- } +- +- // Passing CONIN$ or CONOUT$ to CreateFile opens a console handle. +- // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles +- // +- // While CONIN$ and CONOUT$ aren't documented as being files, +- // they behave the same as CON. For example, ./CONIN$ also opens the console input. +- if len(name) == 6 && name[5] == '$' && equalFold(name, "CONIN$") { +- return true +- } +- if len(name) == 7 && name[6] == '$' && equalFold(name, "CONOUT$") { +- return true +- } +- return false +-} +- +-func equalFold(a, b string) bool { +- if len(a) != len(b) { +- return false +- } +- for i := 0; i < len(a); i++ { +- if toUpper(a[i]) != toUpper(b[i]) { +- return false +- } +- } +- return true +-} +- + func toUpper(c byte) byte { + if 'a' <= c && c <= 'z' { + return c - ('a' - 'A') +@@ -180,13 +20,11 @@ + return c + } + +-// 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 IsPathSeparator(path[0]) && IsPathSeparator(path[1]) { + return true + } +@@ -197,46 +35,21 @@ + return IsPathSeparator(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 || !IsPathSeparator(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, `\\.`) || + pathHasPrefixFold(path, `\\?`) || pathHasPrefixFold(path, `\??`): +- // Path starts with \\.\, and is a Local Device path; or +- // path starts with \\?\ or \??\ and is a Root Local Device path. +- // +- // We treat the next component after the \\.\ prefix as +- // part of the volume name, which means Clean(`\\?\c:\`) +- // won't remove the trailing \. (See #64028.) + if len(path) == 3 { +- return 3 // exactly \\. ++ return 3 + } + _, rest, ok := cutPath(path[4:]) + if !ok { +@@ -245,15 +58,11 @@ + return len(path) - len(rest) - 1 + + case len(path) >= 2 && IsPathSeparator(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 +@@ -273,9 +82,6 @@ + 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++ { +@@ -289,7 +95,6 @@ + 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 IsPathSeparator(path[i]) { +@@ -299,15 +104,10 @@ + return path, "", false + } + +-// 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 IsPathSeparator(c) { + break +@@ -317,9 +117,6 @@ + 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 && IsPathSeparator(out.buf[0]) && out.buf[1] == '?' && out.buf[2] == '?' { + out.prepend(Separator, '.') + } diff --git a/path/winpath/patches/path_windowsspecific.diff b/path/winpath/patches/path_windowsspecific.diff new file mode 100644 index 0000000..a2ff8b4 --- /dev/null +++ b/path/winpath/patches/path_windowsspecific.diff @@ -0,0 +1,116 @@ +--- orig/path_windowsspecific.go 2026-03-28 16:15:19 ++++ path_windowsspecific.go 2026-03-28 16:14:52 +@@ -2,103 +2,28 @@ + // Use of this source code is governed by a BSD-style + // license that can be found in the LICENSE file. + +-package filepath ++package winpath + + import ( + "os" + "strings" +- "syscall" + ) + +-// 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 { ++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 os.IsPathSeparator(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 && os.IsPathSeparator(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 && strings.HasPrefix(e, "??") && (len(e) == len("??") || os.IsPathSeparator(e[2])) { + 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 = '\\' + } +@@ -112,7 +37,3 @@ + } + return Clean(b.String()) + } +- +-func sameWord(a, b string) bool { +- return strings.EqualFold(a, b) +-} diff --git a/path/winpath/patches/windows.diff.nextron.bak b/path/winpath/patches/windows.diff.nextron.bak new file mode 100644 index 0000000..36335a7 --- /dev/null +++ b/path/winpath/patches/windows.diff.nextron.bak @@ -0,0 +1,341 @@ +diff --git a/windows/path_lite.go b/windows/path_lite.go +index 4a37298..2ebb0d1 100644 +--- a/windows/path_lite.go ++++ b/windows/path_lite.go +@@ -2,17 +2,12 @@ + // Use of this source code is governed by a BSD-style + // license that can be found in the LICENSE file. + +-// Package filepathlite implements a subset of path/filepath, +-// only using packages which may be imported by "os". +-// +-// Tests for these functions are in path/filepath. +-package filepathlite ++package windows + + import ( + "errors" +- "internal/stringslite" +- "io/fs" + "slices" ++ "strings" + ) + + var errInvalidPath = errors.New("invalid path") +@@ -137,41 +132,6 @@ func Clean(path string) string { + return FromSlash(out.string()) + } + +-// IsLocal is filepath.IsLocal. +-func IsLocal(path string) bool { +- return isLocal(path) +-} +- +-func unixIsLocal(path string) bool { +- if IsAbs(path) || path == "" { +- return false +- } +- hasDots := false +- for p := path; p != ""; { +- var part string +- part, p, _ = stringslite.Cut(p, "/") +- if part == "." || part == ".." { +- hasDots = true +- break +- } +- } +- if hasDots { +- path = Clean(path) +- } +- if path == ".." || stringslite.HasPrefix(path, "../") { +- return false +- } +- return true +-} +- +-// Localize is filepath.Localize. +-func Localize(path string) (string, error) { +- if !fs.ValidPath(path) { +- return "", errInvalidPath +- } +- return localize(path) +-} +- + // ToSlash is filepath.ToSlash. + func ToSlash(path string) string { + if Separator == '/' { +@@ -189,7 +149,7 @@ func FromSlash(path string) string { + } + + func replaceStringByte(s string, old, new byte) string { +- if stringslite.IndexByte(s, old) == -1 { ++ if strings.IndexByte(s, old) == -1 { + return s + } + n := []byte(s) +diff --git a/windows/path_lite_windowsspecific.go b/windows/path_lite_windowsspecific.go +index 011baa9..23ba2b6 100644 +--- a/windows/path_lite_windowsspecific.go ++++ b/windows/path_lite_windowsspecific.go +@@ -2,14 +2,7 @@ + // Use of this source code is governed by a BSD-style + // license that can be found in the LICENSE file. + +-package filepathlite +- +-import ( +- "internal/bytealg" +- "internal/stringslite" +- "internal/syscall/windows" +- "syscall" +-) ++package windows + + const ( + Separator = '\\' // OS-specific path separator +@@ -20,159 +13,6 @@ func IsPathSeparator(c uint8) bool { + return c == '\\' || c == '/' + } + +-func isLocal(path string) bool { +- if path == "" { +- return false +- } +- if IsPathSeparator(path[0]) { +- // Path rooted in the current drive. +- return false +- } +- if stringslite.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 isReservedName(part) { +- return false +- } +- } +- if hasDots { +- path = Clean(path) +- } +- if path == ".." || stringslite.HasPrefix(path, `..\`) { +- return false +- } +- return true +-} +- +-func localize(path string) (string, error) { +- for i := 0; i < len(path); i++ { +- switch path[i] { +- case ':', '\\', 0: +- return "", errInvalidPath +- } +- } +- containsSlash := false +- for p := path; p != ""; { +- // Find the next path element. +- var element string +- i := bytealg.IndexByteString(p, '/') +- if i < 0 { +- element = p +- p = "" +- } else { +- containsSlash = true +- element = p[:i] +- p = p[i+1:] +- } +- if isReservedName(element) { +- return "", errInvalidPath +- } +- } +- if containsSlash { +- // We can't depend on strings, so substitute \ for / manually. +- buf := []byte(path) +- for i, b := range buf { +- if b == '/' { +- buf[i] = '\\' +- } +- } +- path = string(buf) +- } +- return path, nil +-} +- +-// isReservedName reports if name is a Windows reserved device name. +-// It does not detect names with an extension, which are also reserved on some Windows versions. +-// +-// For details, search for PRN in +-// https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file. +-func isReservedName(name string) bool { +- // Device names can have arbitrary trailing characters following a dot or colon. +- base := name +- for i := 0; i < len(base); i++ { +- switch base[i] { +- case ':', '.': +- base = base[:i] +- } +- } +- // Trailing spaces in the last path element are ignored. +- for len(base) > 0 && base[len(base)-1] == ' ' { +- base = base[:len(base)-1] +- } +- if !isReservedBaseName(base) { +- return false +- } +- if len(base) == len(name) { +- return true +- } +- // The path element is a reserved name with an extension. +- // Since Windows 11, reserved names with extensions are no +- // longer reserved. For example, "CON.txt" is a valid file +- // name. Use RtlIsDosDeviceName_U to see if the name is reserved. +- p, err := syscall.UTF16PtrFromString(name) +- if err != nil { +- return false +- } +- return windows.RtlIsDosDeviceName_U(p) > 0 +-} +- +-func isReservedBaseName(name string) bool { +- if len(name) == 3 { +- switch string([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) { +- case "CON", "PRN", "AUX", "NUL": +- return true +- } +- } +- if len(name) >= 4 { +- switch string([]byte{toUpper(name[0]), toUpper(name[1]), toUpper(name[2])}) { +- case "COM", "LPT": +- if len(name) == 4 && '1' <= name[3] && name[3] <= '9' { +- return true +- } +- // Superscript ¹, ², and ³ are considered numbers as well. +- switch name[3:] { +- case "\u00b2", "\u00b3", "\u00b9": +- return true +- } +- return false +- } +- } +- +- // Passing CONIN$ or CONOUT$ to CreateFile opens a console handle. +- // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles +- // +- // While CONIN$ and CONOUT$ aren't documented as being files, +- // they behave the same as CON. For example, ./CONIN$ also opens the console input. +- if len(name) == 6 && name[5] == '$' && equalFold(name, "CONIN$") { +- return true +- } +- if len(name) == 7 && name[6] == '$' && equalFold(name, "CONOUT$") { +- return true +- } +- return false +-} +- +-func equalFold(a, b string) bool { +- if len(a) != len(b) { +- return false +- } +- for i := 0; i < len(a); i++ { +- if toUpper(a[i]) != toUpper(b[i]) { +- return false +- } +- } +- return true +-} +- + func toUpper(c byte) byte { + if 'a' <= c && c <= 'z' { + return c - ('a' - 'A') +diff --git a/windows/path_windowsspecific.go b/windows/path_windowsspecific.go +index d0eb42c..40b9315 100644 +--- a/windows/path_windowsspecific.go ++++ b/windows/path_windowsspecific.go +@@ -2,71 +2,14 @@ + // Use of this source code is governed by a BSD-style + // license that can be found in the LICENSE file. + +-package filepath ++package windows + + import ( + "os" + "strings" +- "syscall" + ) + +-// 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 { ++func Join(elem ...string) string { + var b strings.Builder + var lastChar byte + for _, e := range elem { +@@ -112,7 +55,3 @@ func join(elem []string) string { + } + return Clean(b.String()) + } +- +-func sameWord(a, b string) bool { +- return strings.EqualFold(a, b) +-} diff --git a/path/winpath/sync.sh b/path/winpath/sync.sh new file mode 100755 index 0000000..1745f4d --- /dev/null +++ b/path/winpath/sync.sh @@ -0,0 +1,174 @@ +#!/bin/sh +# Sync winpath package from Go standard library +# Downloads source files to orig/ and applies patches from patches/ + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ORIG_DIR="$SCRIPT_DIR/orig" +PATCHES_DIR="$SCRIPT_DIR/patches" + +# Go version to sync from (override with GO_VERSION env) +GO_VERSION="${GO_VERSION:-go1.26.1}" +GO_BASE_URL="https://raw.githubusercontent.com/golang/go/${GO_VERSION}/src" + +# Source files to download +# Format: "local_file::source_path" +SOURCES=" +path_lite.go::internal/filepathlite/path.go +path_lite_windowsspecific.go::internal/filepathlite/path_windows.go +path_windowsspecific.go::path/filepath/path_windows.go +" + +download() { + local dest="$1" + local src="$2" + local url="$GO_BASE_URL/$src" + + printf " Downloading %s... " "$dest" + if curl -fsSL "$url" -o "$dest.tmp"; then + mv "$dest.tmp" "$dest" + echo "OK" + else + rm -f "$dest.tmp" + echo "FAILED" + return 1 + fi +} + +show_agent_instructions() { + cat <<'EOF' + +AGENT: PATCH APPLICATION FAILED +AGENT: +AGENT: The Go source files have diverged from the patches. +AGENT: +AGENT: To manually update: +AGENT: 1. Run: ./sync.sh download +AGENT: This downloads original files to orig/ +AGENT: 2. Compare orig/*.go with current *.go +AGENT: 3. Update patches/*.diff files +AGENT: 4. Run: ./sync.sh +AGENT: +AGENT: Key transformations for winpath: +AGENT: - Package name: 'package winpath' +AGENT: - Replace internal/stringslite with strings +AGENT: - Remove internal/* imports +AGENT: - Remove IsLocal, Localize, isLocal, localize, isReservedName functions +AGENT: - Change join(elem []string) to Join(elem ...string) +AGENT: - Export: Clean, Join, Split, Ext, Base, Dir, IsAbs, VolumeName, ToSlash, FromSlash +EOF +} + +cmd_download() { + echo "Downloading source files to orig/..." + mkdir -p "$ORIG_DIR" + for entry in $SOURCES; do + local_file="${entry%%::*}" + source_path="${entry#*::}" + if ! download "$ORIG_DIR/$local_file" "$source_path"; then + echo "" + echo "ERROR: Failed to download $source_path" + echo "AGENT: Check if Go version $GO_VERSION exists in golang/go repository" + exit 1 + fi + done + echo "" + echo "Download complete. Original files in: $ORIG_DIR/" +} + +cmd_patch() { + if ! test -d "$PATCHES_DIR"; then + echo "ERROR: No patches directory found" + echo "AGENT: Create patches/ directory with .diff files" + exit 1 + fi + + # Copy original files to working directory first + for entry in $SOURCES; do + local_file="${entry%%::*}" + if test -f "$ORIG_DIR/$local_file"; then + cp "$ORIG_DIR/$local_file" "$SCRIPT_DIR/$local_file" + fi + done + + echo "Applying patches..." + patch_failed="" + for patch in "$PATCHES_DIR"/*.diff; do + if test -f "$patch"; then + patch_name=$(basename "$patch") + printf " Applying %s... " "$patch_name" + if patch -d "$SCRIPT_DIR" -p1 <"$patch" 2>/dev/null; then + echo "OK" + else + echo "FAILED" + patch_failed=1 + fi + fi + done + echo "" + + if test -n "$patch_failed"; then + show_agent_instructions + exit 1 + fi +} + +cmd_verify() { + printf "Verifying build... " + if ! go build . 2>&1; then + echo "FAILED" + show_agent_instructions + exit 1 + fi + + if ! go vet . 2>&1; then + echo "FAILED (vet)" + show_agent_instructions + exit 1 + fi + + printf "Running tests... " + if ! go test -v . 2>&1; then + echo "FAILED (tests)" + exit 1 + fi +} + +cmd_diff() { + echo "Generating diffs..." + mkdir -p "$PATCHES_DIR" + for entry in $SOURCES; do + local_file="${entry%%::*}" + if test -f "$ORIG_DIR/$local_file" && test -f "$SCRIPT_DIR/$local_file"; then + diff -u "$ORIG_DIR/$local_file" "$SCRIPT_DIR/$local_file" >"$PATCHES_DIR/${local_file%.go}.diff" 2>/dev/null || true + echo " Created patches/${local_file%.go}.diff" + fi + done + echo "" + echo "Diffs created in: $PATCHES_DIR/" +} + +main() { + case "${1:-}" in + download) + cmd_download + ;; + diff) + cmd_download + cmd_diff + ;; + *) + # Default: download, patch, and verify + cmd_download + if test -d "$PATCHES_DIR"; then + cmd_patch + fi + cmd_verify + echo "" + echo "Sync complete: path/winpath/" + ;; + esac +} + +main "$@"