diff --git a/path/winpath/filepath_test.go b/path/winpath/filepath_test.go new file mode 100644 index 0000000..ba21074 --- /dev/null +++ b/path/winpath/filepath_test.go @@ -0,0 +1,177 @@ +package winpath_test + +import ( + "testing" + + winpath "github.com/therootcompany/golib/path/winpath" +) + +type stringTest struct { + input string + output string +} + +func TestBase(t *testing.T) { + for _, tc := range []stringTest{ + {input: `C:\foo\bar\baz.txt`, output: "baz.txt"}, + {input: `foo\bar\baz.txt`, output: "baz.txt"}, + {input: `baz.txt`, output: "baz.txt"}, + {input: `\\.\pipe\baz.txt`, output: "baz.txt"}, + {input: ".", output: "."}, + {input: "..", output: ".."}, + {input: "/", output: "\\"}, + {input: "", output: "."}, + } { + result := winpath.Base(tc.input) + if result != tc.output { + t.Errorf("winpath.Base(%q) = %q; want %q", tc.input, result, tc.output) + } + } +} + +func TestDir(t *testing.T) { + for _, tc := range []stringTest{ + {input: `C:\foo\bar\baz.txt`, output: `C:\foo\bar`}, + {input: `foo\bar\baz.txt`, output: `foo\bar`}, + {input: `baz.txt`, output: `.`}, + {input: ".", output: "."}, + {input: "..", output: "."}, + {input: "C:\\", output: "C:\\"}, + {input: "", output: "."}, + } { + result := winpath.Dir(tc.input) + if result != tc.output { + t.Errorf("winpath.Dir(%q) = %q; want %q", tc.input, result, tc.output) + } + } +} + +func TestJoin(t *testing.T) { + for _, tc := range []struct { + parts []string + output string + }{ + {parts: []string{`C:\foo`, "bar", "baz.txt"}, output: `C:\foo\bar\baz.txt`}, + {parts: []string{`foo`, "bar", "baz.txt"}, output: `foo\bar\baz.txt`}, + {parts: []string{`baz.txt`}, output: `baz.txt`}, + {parts: []string{`C:\`, "foo", "..", "baz.txt"}, output: `C:\baz.txt`}, + {parts: []string{`C:\`, "..", "baz.txt"}, output: `C:\baz.txt`}, + {parts: []string{"..", "baz.txt"}, output: `..\baz.txt`}, + } { + result := winpath.Join(tc.parts...) + if result != tc.output { + t.Errorf("winpath.Join(%q) = %q; want %q", tc.parts, result, tc.output) + } + } +} + +func TestExt(t *testing.T) { + for _, tc := range []stringTest{ + {input: `C:\foo\bar\baz.txt`, output: ".txt"}, + {input: `foo\bar\baz.tar.gz`, output: ".gz"}, + {input: `baz`, output: ""}, + {input: `\baz.`, output: "."}, + } { + result := winpath.Ext(tc.input) + if result != tc.output { + t.Errorf("winpath.Ext(%q) = %q; want %q", tc.input, result, tc.output) + } + } +} + +func TestSplit(t *testing.T) { + for _, tc := range []struct { + input string + dir string + base string + }{ + {input: `C:\foo\bar\baz.txt`, dir: `C:\foo\bar\`, base: "baz.txt"}, + {input: `foo\bar\baz.txt`, dir: `foo\bar\`, base: "baz.txt"}, + {input: `baz.txt`, dir: ``, base: "baz.txt"}, + {input: `\\.\pipe\baz.txt`, dir: `\\.\pipe\`, base: "baz.txt"}, + {input: `\\network\path\baz.txt`, dir: `\\network\path\`, base: "baz.txt"}, + } { + dir, base := winpath.Split(tc.input) + if dir != tc.dir || base != tc.base { + t.Errorf("winpath.Split(%q) = (%q, %q); want (%q, %q)", tc.input, dir, base, tc.dir, tc.base) + } + } +} + +func TestClean(t *testing.T) { + for _, tc := range []stringTest{ + {input: `C:\foo\..\bar\baz.txt`, output: `C:\bar\baz.txt`}, + {input: `foo\..\bar\baz.txt`, output: `bar\baz.txt`}, + {input: `.\baz.txt`, output: `baz.txt`}, + {input: `C:\foo\.\bar\baz.txt`, output: `C:\foo\bar\baz.txt`}, + {input: `C:\foo\\bar\\baz.txt`, output: `C:\foo\bar\baz.txt`}, + {input: `C:\foo\bar\..\..\baz.txt`, output: `C:\baz.txt`}, + {input: `..\baz.txt`, output: `..\baz.txt`}, + {input: `\\network\path\..\baz.txt`, output: `\\network\path\baz.txt`}, + } { + result := winpath.Clean(tc.input) + if result != tc.output { + t.Errorf("winpath.Clean(%q) = %q; want %q", tc.input, result, tc.output) + } + } +} + +func TestIsAbs(t *testing.T) { + for _, tc := range []struct { + input string + isAbs bool + }{ + {input: `C:\foo\bar\baz.txt`, isAbs: true}, + {input: `\foo\bar\baz.txt`, isAbs: false}, + {input: `\\network\path`, isAbs: true}, + {input: `foo\bar\baz.txt`, isAbs: false}, + {input: `.\baz.txt`, isAbs: false}, + {input: `..\baz.txt`, isAbs: false}, + } { + result := winpath.IsAbs(tc.input) + if result != tc.isAbs { + t.Errorf("winpath.IsAbs(%q) = %v; want %v", tc.input, result, tc.isAbs) + } + } +} + +func TestVolumeName(t *testing.T) { + for _, tc := range []stringTest{ + {input: `C:\foo\bar\b.txt`, output: "C:"}, + {input: `\\network\path\b.txt`, output: `\\network\path`}, + {input: `\\.\C:\b.txt`, output: `\\.\C:`}, + {input: `foo\bar\b.txt`, output: ""}, + {input: `\foo\bar\b.txt`, output: ""}, + } { + result := winpath.VolumeName(tc.input) + if result != tc.output { + t.Errorf("winpath.VolumeName(%q) = %q; want %q", tc.input, result, tc.output) + } + } +} + +func TestToSlash(t *testing.T) { + for _, tc := range []stringTest{ + {input: `C:\foo\bar\b.txt`, output: "C:/foo/bar/b.txt"}, + {input: `foo\bar\b.txt`, output: "foo/bar/b.txt"}, + {input: `C:/foo/bar/b.txt`, output: "C:/foo/bar/b.txt"}, + } { + result := winpath.ToSlash(tc.input) + if result != tc.output { + t.Errorf("winpath.ToSlash(%q) = %q; want %q", tc.input, result, tc.output) + } + } +} + +func TestFromSlash(t *testing.T) { + for _, tc := range []stringTest{ + {input: `C:/foo/bar/b.txt`, output: `C:\foo\bar\b.txt`}, + {input: `foo/bar/b.txt`, output: `foo\bar\b.txt`}, + {input: `C:\foo\bar\b.txt`, output: `C:\foo\bar\b.txt`}, + } { + result := winpath.FromSlash(tc.input) + if result != tc.output { + t.Errorf("winpath.FromSlash(%q) = %q; want %q", tc.input, result, tc.output) + } + } +} diff --git a/path/winpath/origins.go b/path/winpath/origins.go new file mode 100644 index 0000000..1a3d754 --- /dev/null +++ b/path/winpath/origins.go @@ -0,0 +1,14 @@ +package winpath + +// The following files in this directory are derived from the Go standard library +// and are licensed under a BSD-style license (see LICENSE file in Go source). +// +// They have been modified to create a Windows-only path handling package +// that works on any platform. +// +// Source files: +// - path_lite.go: derived from internal/filepathlite/path.go +// - path_lite_windowsspecific.go: derived from internal/filepathlite/path_windows.go +// - path_windowsspecific.go: derived from path/filepath/path_windows.go +// +// Use './sync.sh' to re-sync from upstream Go sources. diff --git a/path/winpath/path_lite.go b/path/winpath/path_lite.go new file mode 100644 index 0000000..c8ce816 --- /dev/null +++ b/path/winpath/path_lite.go @@ -0,0 +1,202 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package windows provides Windows-style path manipulation +// that works on any platform. +package winpath + +import ( + "errors" + "slices" + "strings" +) + +var errInvalidPath = errors.New("invalid path") + +type lazybuf struct { + path string + buf []byte + w int + volAndPath string + volLen int +} + +func (b *lazybuf) index(i int) byte { + if b.buf != nil { + return b.buf[i] + } + return b.path[i] +} + +func (b *lazybuf) append(c byte) { + if b.buf == nil { + if b.w < len(b.path) && b.path[b.w] == c { + b.w++ + return + } + b.buf = make([]byte, len(b.path)) + copy(b.buf, b.path[:b.w]) + } + b.buf[b.w] = c + b.w++ +} + +func (b *lazybuf) prepend(prefix ...byte) { + b.buf = slices.Insert(b.buf, 0, prefix...) + b.w += len(prefix) +} + +func (b *lazybuf) string() string { + if b.buf == nil { + return b.volAndPath[:b.volLen+b.w] + } + return b.volAndPath[:b.volLen] + string(b.buf[:b.w]) +} + +func Clean(path string) string { + originalPath := path + volLen := volumeNameLen(path) + path = path[volLen:] + if path == "" { + if volLen > 1 && IsPathSeparator(originalPath[0]) && IsPathSeparator(originalPath[1]) { + return FromSlash(originalPath) + } + return originalPath + "." + } + rooted := IsPathSeparator(path[0]) + + n := len(path) + out := lazybuf{path: path, volAndPath: originalPath, volLen: volLen} + r, dotdot := 0, 0 + if rooted { + out.append(Separator) + r, dotdot = 1, 1 + } + + for r < n { + switch { + case IsPathSeparator(path[r]): + r++ + case path[r] == '.' && (r+1 == n || IsPathSeparator(path[r+1])): + r++ + case path[r] == '.' && path[r+1] == '.' && (r+2 == n || IsPathSeparator(path[r+2])): + r += 2 + switch { + case out.w > dotdot: + out.w-- + for out.w > dotdot && !IsPathSeparator(out.index(out.w)) { + out.w-- + } + case !rooted: + if out.w > 0 { + out.append(Separator) + } + out.append('.') + out.append('.') + dotdot = out.w + } + default: + if rooted && out.w != 1 || !rooted && out.w != 0 { + out.append(Separator) + } + for ; r < n && !IsPathSeparator(path[r]); r++ { + out.append(path[r]) + } + } + } + + if out.w == 0 { + out.append('.') + } + + postClean(&out) + return FromSlash(out.string()) +} + +func ToSlash(path string) string { + if Separator == '/' { + return path + } + return replaceStringByte(path, Separator, '/') +} + +func FromSlash(path string) string { + if Separator == '/' { + return path + } + return replaceStringByte(path, '/', Separator) +} + +func replaceStringByte(s string, old, new byte) string { + if strings.IndexByte(s, old) == -1 { + return s + } + n := []byte(s) + for i := range n { + if n[i] == old { + n[i] = new + } + } + return string(n) +} + +func Split(path string) (dir, file string) { + vol := VolumeName(path) + i := len(path) - 1 + for i >= len(vol) && !IsPathSeparator(path[i]) { + i-- + } + return path[:i+1], path[i+1:] +} + +func Ext(path string) string { + for i := len(path) - 1; i >= 0 && !IsPathSeparator(path[i]); i-- { + if path[i] == '.' { + return path[i:] + } + } + return "" +} + +func Base(path string) string { + if path == "" { + return "." + } + for len(path) > 0 && IsPathSeparator(path[len(path)-1]) { + path = path[0 : len(path)-1] + } + path = path[len(VolumeName(path)):] + i := len(path) - 1 + for i >= 0 && !IsPathSeparator(path[i]) { + i-- + } + if i >= 0 { + path = path[i+1:] + } + if path == "" { + return string(Separator) + } + return path +} + +func Dir(path string) string { + vol := VolumeName(path) + i := len(path) - 1 + for i >= len(vol) && !IsPathSeparator(path[i]) { + i-- + } + dir := Clean(path[len(vol) : i+1]) + if dir == "." && len(vol) > 2 { + return vol + } + return vol + dir +} + +func VolumeName(path string) string { + return FromSlash(path[:volumeNameLen(path)]) +} + +func VolumeNameLen(path string) int { + return volumeNameLen(path) +} diff --git a/path/winpath/path_lite_windowsspecific.go b/path/winpath/path_lite_windowsspecific.go new file mode 100644 index 0000000..0f02186 --- /dev/null +++ b/path/winpath/path_lite_windowsspecific.go @@ -0,0 +1,123 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package winpath + +const ( + Separator = '\\' // OS-specific path separator + ListSeparator = ';' // OS-specific path list separator +) + +func IsPathSeparator(c uint8) bool { + return c == '\\' || c == '/' +} + +func toUpper(c byte) byte { + if 'a' <= c && c <= 'z' { + return c - ('a' - 'A') + } + return c +} + +func IsAbs(path string) (b bool) { + l := volumeNameLen(path) + if l == 0 { + return false + } + if IsPathSeparator(path[0]) && IsPathSeparator(path[1]) { + return true + } + path = path[l:] + if path == "" { + return false + } + return IsPathSeparator(path[0]) +} + +func volumeNameLen(path string) int { + switch { + case len(path) >= 2 && path[1] == ':': + return 2 + + case len(path) == 0 || !IsPathSeparator(path[0]): + return 0 + + case pathHasPrefixFold(path, `\\.\UNC`): + return uncLen(path, len(`\\.\UNC\`)) + + case pathHasPrefixFold(path, `\\.`) || + pathHasPrefixFold(path, `\\?`) || pathHasPrefixFold(path, `\??`): + if len(path) == 3 { + return 3 + } + _, rest, ok := cutPath(path[4:]) + if !ok { + return len(path) + } + return len(path) - len(rest) - 1 + + case len(path) >= 2 && IsPathSeparator(path[1]): + return uncLen(path, 2) + } + return 0 +} + +func pathHasPrefixFold(s, prefix string) bool { + if len(s) < len(prefix) { + return false + } + for i := 0; i < len(prefix); i++ { + if IsPathSeparator(prefix[i]) { + if !IsPathSeparator(s[i]) { + return false + } + } else if toUpper(prefix[i]) != toUpper(s[i]) { + return false + } + } + if len(s) > len(prefix) && !IsPathSeparator(s[len(prefix)]) { + return false + } + return true +} + +func uncLen(path string, prefixLen int) int { + count := 0 + for i := prefixLen; i < len(path); i++ { + if IsPathSeparator(path[i]) { + count++ + if count == 2 { + return i + } + } + } + return len(path) +} + +func cutPath(path string) (before, after string, found bool) { + for i := range path { + if IsPathSeparator(path[i]) { + return path[:i], path[i+1:], true + } + } + return path, "", false +} + +func postClean(out *lazybuf) { + if out.volLen != 0 || out.buf == nil { + return + } + for _, c := range out.buf { + if IsPathSeparator(c) { + break + } + if c == ':' { + out.prepend('.', Separator) + return + } + } + if len(out.buf) >= 3 && IsPathSeparator(out.buf[0]) && out.buf[1] == '?' && out.buf[2] == '?' { + out.prepend(Separator, '.') + } +} diff --git a/path/winpath/path_windowsspecific.go b/path/winpath/path_windowsspecific.go new file mode 100644 index 0000000..0c6db52 --- /dev/null +++ b/path/winpath/path_windowsspecific.go @@ -0,0 +1,39 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package winpath + +import ( + "os" + "strings" +) + +func Join(elem ...string) string { + var b strings.Builder + var lastChar byte + for _, e := range elem { + switch { + case b.Len() == 0: + case os.IsPathSeparator(lastChar): + for len(e) > 0 && os.IsPathSeparator(e[0]) { + e = e[1:] + } + if b.Len() == 1 && strings.HasPrefix(e, "??") && (len(e) == len("??") || os.IsPathSeparator(e[2])) { + b.WriteString(`.\`) + } + case lastChar == ':': + default: + b.WriteByte('\\') + lastChar = '\\' + } + if len(e) > 0 { + b.WriteString(e) + lastChar = e[len(e)-1] + } + } + if b.Len() == 0 { + return "" + } + return Clean(b.String()) +}