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
This commit is contained in:
AJ ONeal 2026-03-28 18:47:04 -06:00
parent d779cd44c1
commit da4cd3aa48
No known key found for this signature in database
7 changed files with 1255 additions and 0 deletions

5
path/winpath/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# Ignore downloaded original files
orig/
*.orig
*.tmp
*.tmp.d

84
path/winpath/README.md Normal file
View File

@ -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.

View File

@ -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)
}

View File

@ -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, '.')
}

View File

@ -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)
-}

View File

@ -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)
-}

174
path/winpath/sync.sh Executable file
View File

@ -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 "$@"