diff --git a/CHANGELOG.md b/CHANGELOG.md index 1683709a..9310115c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### [13.14.1](https://kaos.sh/ek/13.14.1) - `[usage]` Fixed bug with rendering examples (_introduced in `13.14.0`_) +- `[path]` Added Windows support ### [13.14.0](https://kaos.sh/ek/13.14.0) diff --git a/path/example_test.go b/path/example_test.go index 4934f2c3..d30507fd 100644 --- a/path/example_test.go +++ b/path/example_test.go @@ -1,3 +1,6 @@ +//go:build !windows +// +build !windows + package path // ////////////////////////////////////////////////////////////////////////////////// // diff --git a/path/path.go b/path/path.go index 73aa42c5..633d4539 100644 --- a/path/path.go +++ b/path/path.go @@ -1,6 +1,3 @@ -//go:build !windows -// +build !windows - // Package path provides methods for working with paths (fully compatible with base path package) package path @@ -15,10 +12,8 @@ import ( "errors" "fmt" "os" - PATH "path" "path/filepath" "strings" - "syscall" ) // ////////////////////////////////////////////////////////////////////////////////// // @@ -26,75 +21,41 @@ import ( // ErrBadPattern indicates a globbing pattern was malformed var ErrBadPattern = errors.New("Syntax error in pattern") -// unsafePaths is slice with unsafe paths -var unsafePaths = []string{ - "/lost+found", - "/bin", - "/boot", - "/etc", - "/dev", - "/lib", - "/lib64", - "/proc", - "/root", - "/sbin", - "/selinux", - "/sys", - "/usr/bin", - "/usr/lib", - "/usr/lib64", - "/usr/libexec", - "/usr/sbin", - "/usr/include", - "/var/cache", - "/var/db", - "/var/lib", -} +// ////////////////////////////////////////////////////////////////////////////////// // + +var pathSeparator = string(filepath.Separator) // ////////////////////////////////////////////////////////////////////////////////// // // Base returns the last element of path func Base(path string) string { - return PATH.Base(path) + return filepath.Base(path) } // Clean returns the shortest path name equivalent to path by purely lexical processing func Clean(path string) string { path = evalHome(path) - return PATH.Clean(path) + return filepath.Clean(path) } // Dir returns all but the last element of path, typically the path's directory func Dir(path string) string { - return PATH.Dir(path) -} - -// DirN returns first N elements of path -func DirN(path string, n int) string { - if len(path) <= 1 || n == 0 { - return path - } - - if n > 0 { - return dirNRight(path, n) - } - - return dirNLeft(path, n*-1) + return filepath.Dir(path) } // Ext returns the file name extension used by path func Ext(path string) string { - return PATH.Ext(path) + return filepath.Ext(path) } // IsAbs reports whether the path is absolute func IsAbs(path string) bool { - return PATH.IsAbs(path) + return filepath.IsAbs(path) } // Join joins any number of path elements into a single path, adding a separating slash if necessary func Join(elem ...string) string { - return PATH.Join(elem...) + return filepath.Join(elem...) } // JoinSecure joins all elements of path, makes lexical processing, and evaluating all symlinks. @@ -109,15 +70,18 @@ func JoinSecure(root string, elem ...string) (string, error) { } for _, e := range elem { - result = Clean(result + "/" + e) - - if isLink(result) { - result, err = filepath.EvalSymlinks(result) + result = Clean(result + pathSeparator + e) + resultSym, err := filepath.EvalSymlinks(result) - if err != nil { + if err != nil { + if errors.Is(err, os.ErrNotExist) { + resultSym = result + } else { return "", fmt.Errorf("Can't eval symlinks: %w", err) } } + + result = resultSym } if !strings.HasPrefix(result, root) { @@ -129,29 +93,29 @@ func JoinSecure(root string, elem ...string) (string, error) { // Match reports whether name matches the shell file name pattern func Match(pattern, name string) (matched bool, err error) { - return PATH.Match(pattern, name) + return filepath.Match(pattern, name) } // Split splits path immediately following the final slash, separating it into a directory and file name component func Split(path string) (dir, file string) { - return PATH.Split(path) + return filepath.Split(path) } // Compact converts path to compact representation (e.g /some/random/directory/file.txt → /s/r/d/file.txt) func Compact(path string) string { - if !strings.Contains(path, "/") { + if !strings.ContainsRune(path, filepath.Separator) { return path } - pathSlice := strings.Split(path, "/") + pathSlice := strings.Split(path, pathSeparator) for i := 0; i < len(pathSlice)-1; i++ { - if len(pathSlice[i]) > 1 { + if len(pathSlice[i]) > 1 && !strings.HasSuffix(pathSlice[i], ":") { pathSlice[i] = pathSlice[i][0:1] } } - return strings.Join(pathSlice, "/") + return strings.Join(pathSlice, pathSeparator) } // IsSafe returns true is given path is safe to use (not points to system dirs) @@ -162,17 +126,11 @@ func IsSafe(path string) bool { absPath, err := filepath.Abs(Clean(path)) - if err != nil || absPath == "/" { + if err != nil || absPath == pathSeparator { return false } - for _, up := range unsafePaths { - if contains(absPath, up) { - return false - } - } - - return true + return isSafePath(absPath) } // IsDotfile returns true if file name begins with a full stop @@ -181,13 +139,13 @@ func IsDotfile(path string) bool { return false } - if !strings.Contains(path, "/") { - return path[0:1] == "." + if !strings.ContainsRune(path, filepath.Separator) { + return strings.HasPrefix(path, ".") } pathBase := Base(path) - return pathBase[0:1] == "." + return strings.HasPrefix(pathBase, ".") } // IsGlob returns true if given pattern is Unix-like glob @@ -217,14 +175,14 @@ func IsGlob(pattern string) bool { // ////////////////////////////////////////////////////////////////////////////////// // func dirNRight(path string, n int) string { - if path[0] == '/' { + if path[0] == filepath.Separator { n++ } var k int for i, r := range path { - if r == '/' { + if r == filepath.Separator { k++ } @@ -237,14 +195,14 @@ func dirNRight(path string, n int) string { } func dirNLeft(path string, n int) string { - if path[len(path)-1] == '/' { + if path[len(path)-1] == filepath.Separator { n++ } var k int for i := len(path) - 1; i > 0; i-- { - if path[i] == '/' { + if path[i] == filepath.Separator { k++ } @@ -256,27 +214,16 @@ func dirNLeft(path string, n int) string { return path } -func isLink(path string) bool { - var buf = make([]byte, 1) - _, err := syscall.Readlink(path, buf) - - return err == nil -} - func evalHome(path string) string { if path == "" || path[0:1] != "~" { return path } - return os.Getenv("HOME") + path[1:] -} - -func contains(path, subpath string) bool { - spl := len(subpath) + homeDir, err := os.UserHomeDir() - if len(path) < spl { - return false + if err != nil { + return path } - return path[:spl] == subpath + return homeDir + path[1:] } diff --git a/path/path_posix.go b/path/path_posix.go new file mode 100644 index 00000000..c66abb1e --- /dev/null +++ b/path/path_posix.go @@ -0,0 +1,69 @@ +//go:build !windows +// +build !windows + +package path + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2024 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import "strings" + +// ////////////////////////////////////////////////////////////////////////////////// // + +// unsafePaths is slice with unsafe paths +var unsafePaths = []string{ + "/lost+found", + "/bin", + "/boot", + "/etc", + "/dev", + "/lib", + "/lib64", + "/proc", + "/root", + "/sbin", + "/selinux", + "/sys", + "/usr/bin", + "/usr/lib", + "/usr/lib64", + "/usr/libexec", + "/usr/sbin", + "/usr/include", + "/var/cache", + "/var/db", + "/var/lib", +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// DirN returns first N elements of path +func DirN(path string, n int) string { + if strings.Count(path, pathSeparator) < 2 || n == 0 { + return path + } + + if n > 0 { + return dirNRight(path, n) + } + + return dirNLeft(path, n*-1) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +func isSafePath(path string) bool { + for _, p := range unsafePaths { + if strings.HasPrefix(path, p) { + return false + } + } + + return true +} + +// ////////////////////////////////////////////////////////////////////////////////// // diff --git a/path/path_posix_test.go b/path/path_posix_test.go new file mode 100644 index 00000000..8d235d23 --- /dev/null +++ b/path/path_posix_test.go @@ -0,0 +1,156 @@ +//go:build !windows +// +build !windows + +package path + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2024 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "os" + + . "github.com/essentialkaos/check" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +func (s *PathUtilSuite) TestBase(c *C) { + c.Assert(Base("/some/test/path"), Equals, "path") + c.Assert(Clean("//some/test//path"), Equals, "/some/test/path") + c.Assert(Dir("/some/test/path"), Equals, "/some/test") + c.Assert(Ext("/some/test/path/file.jpg"), Equals, ".jpg") + c.Assert(IsAbs("/some/test/path"), Equals, true) + c.Assert(Join("/", "some", "test", "path"), Equals, "/some/test/path") + + match, err := Match("/some/test/*", "/some/test/path") + + c.Assert(err, IsNil) + c.Assert(match, Equals, true) + + d, f := Split("/some/test/path/file.jpg") + + c.Assert(d, Equals, "/some/test/path/") + c.Assert(f, Equals, "file.jpg") +} + +func (s *PathUtilSuite) TestJoinSecure(c *C) { + p, err := JoinSecure("/test", "myapp") + c.Assert(err, IsNil) + c.Assert(p, Equals, "/test/myapp") + + p, err = JoinSecure("/test", "myapp/config/../global.cfg") + c.Assert(err, IsNil) + c.Assert(p, Equals, "/test/myapp/global.cfg") + + p, err = JoinSecure("/unknown", "myapp/config/../global.cfg") + c.Assert(err, IsNil) + c.Assert(p, Equals, "/unknown/myapp/global.cfg") + + tmpDir := c.MkDir() + os.Mkdir(tmpDir+"/test", 0755) + os.Symlink(tmpDir+"/test", tmpDir+"/testlink") + testDir := tmpDir + "/testlink" + + os.Symlink(testDir+"/test.log", testDir+"/test1.link") + os.WriteFile(testDir+"/test.log", []byte("\n"), 0644) + os.Symlink(testDir+"/test.log", testDir+"/test1.link") + os.Symlink("/etc", testDir+"/test2.link") + os.Symlink(testDir+"/test3.link", testDir+"/test3.link") + + p, err = JoinSecure(testDir, "mytest/../test1.link") + c.Assert(err, IsNil) + c.Assert(p, Matches, "*/test/test.log") + + p, err = JoinSecure(testDir, "mytest/../test2.link") + c.Assert(err, NotNil) + + p, err = JoinSecure(testDir, "mytest/../test3.link") + c.Assert(err, NotNil) +} + +func (s *PathUtilSuite) TestDirN(c *C) { + c.Assert(DirN("", 99), Equals, "") + c.Assert(DirN("1", 99), Equals, "1") + c.Assert(DirN("abcde", 1), Equals, "abcde") + c.Assert(DirN("abcde", -1), Equals, "abcde") + c.Assert(DirN("/a/b/c/d", 0), Equals, "/a/b/c/d") + c.Assert(DirN("/a/b/c/d", -1), Equals, "/a/b/c") + c.Assert(DirN("/a/b/c/d/", -1), Equals, "/a/b/c") + c.Assert(DirN("/a/b/c/d", 1), Equals, "/a") + c.Assert(DirN("a/b/c/d", 2), Equals, "a/b") + c.Assert(DirN("/a/b/c/d", 99), Equals, "/a/b/c/d") + c.Assert(DirN("/a/b/c/d", -4), Equals, "/a/b/c/d") + + c.Assert(DirN("/////////", 2), Equals, "//") + c.Assert(DirN("/////////", -2), Equals, "//////") +} + +func (s *PathUtilSuite) TestEvalHome(c *C) { + homeDir := os.Getenv("HOME") + + c.Assert(Clean("~/path"), Equals, homeDir+"/path") + c.Assert(Clean("/path"), Equals, "/path") +} + +func (s *PathUtilSuite) TestSafe(c *C) { + c.Assert(IsSafe("/home/user/test.jpg"), Equals, true) + c.Assert(IsSafe("/home/user"), Equals, true) + c.Assert(IsSafe("/opt/software-1234"), Equals, true) + c.Assert(IsSafe("/srv/my-supper-service"), Equals, true) + + c.Assert(IsSafe(""), Equals, false) + c.Assert(IsSafe("/"), Equals, false) + c.Assert(IsSafe("/dev/tty3"), Equals, false) + c.Assert(IsSafe("/etc/file.conf"), Equals, false) + c.Assert(IsSafe("/lib/some-lib"), Equals, false) + c.Assert(IsSafe("/lib64/some-lib"), Equals, false) + c.Assert(IsSafe("/lost+found"), Equals, false) + c.Assert(IsSafe("/proc/19313"), Equals, false) + c.Assert(IsSafe("/root"), Equals, false) + c.Assert(IsSafe("/sbin/useradd"), Equals, false) + c.Assert(IsSafe("/bin/useradd"), Equals, false) + c.Assert(IsSafe("/selinux"), Equals, false) + c.Assert(IsSafe("/sys/kernel"), Equals, false) + c.Assert(IsSafe("/usr/bin/du"), Equals, false) + c.Assert(IsSafe("/usr/sbin/chroot"), Equals, false) + c.Assert(IsSafe("/usr/lib/some-lib"), Equals, false) + c.Assert(IsSafe("/usr/lib64/some-lib"), Equals, false) + c.Assert(IsSafe("/usr/libexec/gcc"), Equals, false) + c.Assert(IsSafe("/usr/include/xlocale.h"), Equals, false) + c.Assert(IsSafe("/var/cache/yum"), Equals, false) + c.Assert(IsSafe("/var/db/yum"), Equals, false) + c.Assert(IsSafe("/var/lib/pgsql"), Equals, false) +} + +func (s *PathUtilSuite) TestDotfile(c *C) { + c.Assert(IsDotfile(""), Equals, false) + c.Assert(IsDotfile("/some/dir/abcd"), Equals, false) + c.Assert(IsDotfile("/some/dir/"), Equals, false) + c.Assert(IsDotfile("/"), Equals, false) + c.Assert(IsDotfile("/////"), Equals, false) + c.Assert(IsDotfile(" / "), Equals, false) + + c.Assert(IsDotfile(".dotfile"), Equals, true) + c.Assert(IsDotfile("/.dotfile"), Equals, true) + c.Assert(IsDotfile("/some/dir/.abcd"), Equals, true) +} + +func (s *PathUtilSuite) TestGlob(c *C) { + c.Assert(IsGlob(""), Equals, false) + c.Assert(IsGlob("ancd-1234"), Equals, false) + c.Assert(IsGlob("[1234"), Equals, false) + c.Assert(IsGlob("test*"), Equals, true) + c.Assert(IsGlob("t?st"), Equals, true) + c.Assert(IsGlob("t[a-z]st"), Equals, true) +} + +func (s *PathUtilSuite) TestCompact(c *C) { + c.Assert(Compact(""), Equals, "") + c.Assert(Compact("test"), Equals, "test") + c.Assert(Compact("/a/b/c/d"), Equals, "/a/b/c/d") + c.Assert(Compact("/my/random/directory/test.txt"), Equals, "/m/r/d/test.txt") +} diff --git a/path/path_stubs.go b/path/path_stubs.go deleted file mode 100644 index 2f297fec..00000000 --- a/path/path_stubs.go +++ /dev/null @@ -1,108 +0,0 @@ -//go:build !linux && !darwin && !freebsd -// +build !linux,!darwin,!freebsd - -// Package path provides methods for working with paths (fully compatible with base path package) -package path - -// ////////////////////////////////////////////////////////////////////////////////// // -// // -// Copyright (c) 2024 ESSENTIAL KAOS // -// Apache License, Version 2.0 // -// // -// ////////////////////////////////////////////////////////////////////////////////// // - -import ( - "errors" -) - -// ////////////////////////////////////////////////////////////////////////////////// // - -// ❗ ErrBadPattern indicates a globbing pattern was malformed -var ErrBadPattern = errors.New("syntax error in pattern") - -// ////////////////////////////////////////////////////////////////////////////////// // - -// ❗ Base returns the last element of path -func Base(path string) string { - panic("UNSUPPORTED") - return "" -} - -// ❗ Clean returns the shortest path name equivalent to path by purely lexical processing -func Clean(path string) string { - panic("UNSUPPORTED") - return "" -} - -// ❗ Dir returns all but the last element of path, typically the path's directory -func Dir(path string) string { - panic("UNSUPPORTED") - return "" -} - -// ❗ DirN returns first N elements of path -func DirN(path string, n int) string { - panic("UNSUPPORTED") - return "" -} - -// ❗ Ext returns the file name extension used by path -func Ext(path string) string { - panic("UNSUPPORTED") - return "" -} - -// ❗ IsAbs reports whether the path is absolute -func IsAbs(path string) bool { - panic("UNSUPPORTED") - return false -} - -// ❗ Join joins any number of path elements into a single path, adding a separating slash if necessary -func Join(elem ...string) string { - panic("UNSUPPORTED") - return "" -} - -// ❗ JoinSecure joins all elements of path, makes lexical processing, and evaluating all symlinks. -// Method returns error if final destination is not a child path of root. -func JoinSecure(root string, elem ...string) (string, error) { - panic("UNSUPPORTED") - return "", nil -} - -// ❗ Match reports whether name matches the shell file name pattern -func Match(pattern, name string) (matched bool, err error) { - panic("UNSUPPORTED") - return false, nil -} - -// ❗ Split splits path immediately following the final slash, separating it into a directory and file name component -func Split(path string) (dir, file string) { - panic("UNSUPPORTED") - return "", "" -} - -// ❗ Compact converts path to compact representation (e.g /some/random/directory/file.txt → /s/r/d/file.txt) -func Compact(path string) string { - panic("UNSUPPORTED") - return "" -} - -// ❗ IsSafe return true is given path is safe to use (not points to system dirs) -func IsSafe(path string) bool { - panic("UNSUPPORTED") - return false -} - -// ❗ IsDotfile return true if filename begins with a full stop -func IsDotfile(path string) bool { - panic("UNSUPPORTED") - return false -} - -// ❗ IsGlob returns true if given pattern is Unix-like glob -func IsGlob(pattern string) bool { - panic("UNSUPPORTED") - return false -} diff --git a/path/path_test.go b/path/path_test.go index 7f1c5cb4..ce1af23f 100644 --- a/path/path_test.go +++ b/path/path_test.go @@ -8,7 +8,6 @@ package path // ////////////////////////////////////////////////////////////////////////////////// // import ( - "os" "testing" . "github.com/essentialkaos/check" @@ -25,139 +24,3 @@ type PathUtilSuite struct{} var _ = Suite(&PathUtilSuite{}) // ////////////////////////////////////////////////////////////////////////////////// // - -func (s *PathUtilSuite) TestBase(c *C) { - c.Assert(Base("/some/test/path"), Equals, "path") - c.Assert(Clean("//some/test//path"), Equals, "/some/test/path") - c.Assert(Dir("/some/test/path"), Equals, "/some/test") - c.Assert(Ext("/some/test/path/file.jpg"), Equals, ".jpg") - c.Assert(IsAbs("/some/test/path"), Equals, true) - c.Assert(Join("/", "some", "test", "path"), Equals, "/some/test/path") - - match, err := Match("/some/test/*", "/some/test/path") - - c.Assert(err, IsNil) - c.Assert(match, Equals, true) - - d, f := Split("/some/test/path/file.jpg") - - c.Assert(d, Equals, "/some/test/path/") - c.Assert(f, Equals, "file.jpg") -} - -func (s *PathUtilSuite) TestJoinSecure(c *C) { - p, err := JoinSecure("/test", "myapp") - c.Assert(err, IsNil) - c.Assert(p, Equals, "/test/myapp") - - p, err = JoinSecure("/test", "myapp/config/../global.cfg") - c.Assert(err, IsNil) - c.Assert(p, Equals, "/test/myapp/global.cfg") - - p, err = JoinSecure("/unknown", "myapp/config/../global.cfg") - c.Assert(err, IsNil) - c.Assert(p, Equals, "/unknown/myapp/global.cfg") - - tmpDir := c.MkDir() - os.Mkdir(tmpDir+"/test", 0755) - os.Symlink(tmpDir+"/test", tmpDir+"/testlink") - testDir := tmpDir + "/testlink" - - os.Symlink(testDir+"/test.log", testDir+"/test1.link") - os.WriteFile(testDir+"/test.log", []byte("\n"), 0644) - os.Symlink(testDir+"/test.log", testDir+"/test1.link") - os.Symlink("/etc", testDir+"/test2.link") - os.Symlink(testDir+"/test3.link", testDir+"/test3.link") - - p, err = JoinSecure(testDir, "mytest/../test1.link") - c.Assert(err, IsNil) - c.Assert(p, Matches, "*/test/test.log") - - p, err = JoinSecure(testDir, "mytest/../test2.link") - c.Assert(err, NotNil) - - p, err = JoinSecure(testDir, "mytest/../test3.link") - c.Assert(err, NotNil) -} - -func (s *PathUtilSuite) TestDirN(c *C) { - c.Assert(DirN("", 99), Equals, "") - c.Assert(DirN("1", 99), Equals, "1") - c.Assert(DirN("abcde", 1), Equals, "abcde") - c.Assert(DirN("abcde", -1), Equals, "abcde") - c.Assert(DirN("/a/b/c/d", 0), Equals, "/a/b/c/d") - c.Assert(DirN("/a/b/c/d", -1), Equals, "/a/b/c") - c.Assert(DirN("/a/b/c/d/", -1), Equals, "/a/b/c") - c.Assert(DirN("/a/b/c/d", 1), Equals, "/a") - c.Assert(DirN("a/b/c/d", 2), Equals, "a/b") - c.Assert(DirN("/a/b/c/d", 99), Equals, "/a/b/c/d") - - c.Assert(DirN("/////////", 2), Equals, "//") - c.Assert(DirN("/////////", -2), Equals, "//////") -} - -func (s *PathUtilSuite) TestEvalHome(c *C) { - homeDir := os.Getenv("HOME") - - c.Assert(Clean("~/path"), Equals, homeDir+"/path") - c.Assert(Clean("/path"), Equals, "/path") -} - -func (s *PathUtilSuite) TestSafe(c *C) { - c.Assert(IsSafe("/home/user/test.jpg"), Equals, true) - c.Assert(IsSafe("/home/user"), Equals, true) - c.Assert(IsSafe("/opt/software-1234"), Equals, true) - c.Assert(IsSafe("/srv/my-supper-service"), Equals, true) - - c.Assert(IsSafe(""), Equals, false) - c.Assert(IsSafe("/"), Equals, false) - c.Assert(IsSafe("/dev/tty3"), Equals, false) - c.Assert(IsSafe("/etc/file.conf"), Equals, false) - c.Assert(IsSafe("/lib/some-lib"), Equals, false) - c.Assert(IsSafe("/lib64/some-lib"), Equals, false) - c.Assert(IsSafe("/lost+found"), Equals, false) - c.Assert(IsSafe("/proc/19313"), Equals, false) - c.Assert(IsSafe("/root"), Equals, false) - c.Assert(IsSafe("/sbin/useradd"), Equals, false) - c.Assert(IsSafe("/bin/useradd"), Equals, false) - c.Assert(IsSafe("/selinux"), Equals, false) - c.Assert(IsSafe("/sys/kernel"), Equals, false) - c.Assert(IsSafe("/usr/bin/du"), Equals, false) - c.Assert(IsSafe("/usr/sbin/chroot"), Equals, false) - c.Assert(IsSafe("/usr/lib/some-lib"), Equals, false) - c.Assert(IsSafe("/usr/lib64/some-lib"), Equals, false) - c.Assert(IsSafe("/usr/libexec/gcc"), Equals, false) - c.Assert(IsSafe("/usr/include/xlocale.h"), Equals, false) - c.Assert(IsSafe("/var/cache/yum"), Equals, false) - c.Assert(IsSafe("/var/db/yum"), Equals, false) - c.Assert(IsSafe("/var/lib/pgsql"), Equals, false) -} - -func (s *PathUtilSuite) TestDotfile(c *C) { - c.Assert(IsDotfile(""), Equals, false) - c.Assert(IsDotfile("/some/dir/abcd"), Equals, false) - c.Assert(IsDotfile("/some/dir/"), Equals, false) - c.Assert(IsDotfile("/"), Equals, false) - c.Assert(IsDotfile("/////"), Equals, false) - c.Assert(IsDotfile(" / "), Equals, false) - - c.Assert(IsDotfile(".dotfile"), Equals, true) - c.Assert(IsDotfile("/.dotfile"), Equals, true) - c.Assert(IsDotfile("/some/dir/.abcd"), Equals, true) -} - -func (s *PathUtilSuite) TestGlob(c *C) { - c.Assert(IsGlob(""), Equals, false) - c.Assert(IsGlob("ancd-1234"), Equals, false) - c.Assert(IsGlob("[1234"), Equals, false) - c.Assert(IsGlob("test*"), Equals, true) - c.Assert(IsGlob("t?st"), Equals, true) - c.Assert(IsGlob("t[a-z]st"), Equals, true) -} - -func (s *PathUtilSuite) TestCompact(c *C) { - c.Assert(Compact(""), Equals, "") - c.Assert(Compact("test"), Equals, "test") - c.Assert(Compact("/a/b/c/d"), Equals, "/a/b/c/d") - c.Assert(Compact("/my/random/directory/test.txt"), Equals, "/m/r/d/test.txt") -} diff --git a/path/path_windows.go b/path/path_windows.go new file mode 100644 index 00000000..156d8d3d --- /dev/null +++ b/path/path_windows.go @@ -0,0 +1,61 @@ +package path + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2024 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import "strings" + +// ////////////////////////////////////////////////////////////////////////////////// // + +// unsafePaths is slice with unsafe paths +var unsafePaths = []string{ + `\Recovery`, + `\Windows`, + `\ProgramData`, + `\Program Files (x86)`, + `\Program Files`, +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// DirN returns first N elements of path +func DirN(path string, n int) string { + if strings.Count(path, pathSeparator) < 2 || n == 0 { + return path + } + + disk, p, ok := strings.Cut(path, ":") + + if !ok { + p = path + disk = "" + } else { + disk += ":" + } + + if n > 0 { + return disk + dirNRight(p, n) + } + + return disk + dirNLeft(p, n*-1) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +func isSafePath(path string) bool { + for _, p := range unsafePaths { + if strings.ContainsRune(path, ':') { + _, path, _ = strings.Cut(path, ":") + } + + if strings.HasPrefix(path, p) { + return false + } + } + + return true +} diff --git a/path/path_windows_test.go b/path/path_windows_test.go new file mode 100644 index 00000000..0dc6565a --- /dev/null +++ b/path/path_windows_test.go @@ -0,0 +1,96 @@ +package path + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2024 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + . "github.com/essentialkaos/check" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +func (s *PathUtilSuite) TestBase(c *C) { + c.Assert(Base(`C:\some\test\path`), Equals, "path") + c.Assert(Clean(`C:\\some\test\\\path`), Equals, `C:\some\test\path`) + c.Assert(Dir(`C:\some\test\path`), Equals, `C:\some\test`) + c.Assert(Ext(`C:\some\test\path\file.jpg`), Equals, ".jpg") + c.Assert(IsAbs(`C:\some\test\path`), Equals, true) + c.Assert(Join("C:\\", "some", "test", "path"), Equals, `C:\some\test\path`) + + match, err := Match(`C:\some\test\*`, `C:\some\test\path`) + + c.Assert(err, IsNil) + c.Assert(match, Equals, true) + + d, f := Split(`C:\some\test\path\file.jpg`) + + c.Assert(d, Equals, `C:\some\test\path\`) + c.Assert(f, Equals, "file.jpg") +} + +func (s *PathUtilSuite) TestJoinSecure(c *C) { + p, err := JoinSecure(`C:\test`, "myapp") + c.Assert(err, IsNil) + c.Assert(p, Equals, `C:\test\myapp`) + + p, err = JoinSecure(`C:\test`, `myapp\config\..\global.cfg`) + c.Assert(err, IsNil) + c.Assert(p, Equals, `C:\test\myapp\global.cfg`) +} + +func (s *PathUtilSuite) TestDirN(c *C) { + c.Assert(DirN("", 99), Equals, "") + c.Assert(DirN("1", 99), Equals, "1") + c.Assert(DirN("abcde", 1), Equals, "abcde") + c.Assert(DirN("abcde", -1), Equals, "abcde") + c.Assert(DirN(`C:\a\b\c\d`, 0), Equals, `C:\a\b\c\d`) + c.Assert(DirN(`C:\a\b\c\d`, -1), Equals, `C:\a\b\c`) + c.Assert(DirN(`C:\a\b\c\d\`, -1), Equals, `C:\a\b\c`) + c.Assert(DirN(`C:\a\b\c\d`, 1), Equals, `C:\a`) + c.Assert(DirN(`a\b\c\d`, 2), Equals, `a\b`) + c.Assert(DirN("/a/b/c/d", 99), Equals, "/a/b/c/d") + + c.Assert(DirN(`\\\\\\\\\`, 2), Equals, `\\`) + c.Assert(DirN(`\\\\\\\\\`, -2), Equals, `\\\\\\`) +} + +func (s *PathUtilSuite) TestSafe(c *C) { + c.Assert(IsSafe(`C:\Users\john\Pictures\test.jpg`), Equals, true) + c.Assert(IsSafe(`C:\Users\john`), Equals, true) + c.Assert(IsSafe(`D:\`), Equals, true) + + c.Assert(IsSafe(`C:\Windows\System32`), Equals, false) + c.Assert(IsSafe(`C:\Windows\System32`), Equals, false) +} + +func (s *PathUtilSuite) TestDotfile(c *C) { + c.Assert(IsDotfile(""), Equals, false) + c.Assert(IsDotfile(`C:\some\dir\abcd`), Equals, false) + c.Assert(IsDotfile(`.\some\dir\`), Equals, false) + c.Assert(IsDotfile(`\\\\\\`), Equals, false) + + c.Assert(IsDotfile(`.dotfile`), Equals, true) + c.Assert(IsDotfile(`C:\.dotfile`), Equals, true) + c.Assert(IsDotfile(`.\some\dir\.abcd`), Equals, true) +} + +func (s *PathUtilSuite) TestGlob(c *C) { + c.Assert(IsGlob(""), Equals, false) + c.Assert(IsGlob("ancd-1234"), Equals, false) + c.Assert(IsGlob("[1234"), Equals, false) + c.Assert(IsGlob("test*"), Equals, true) + c.Assert(IsGlob("t?st"), Equals, true) + c.Assert(IsGlob("t[a-z]st"), Equals, true) +} + +func (s *PathUtilSuite) TestCompact(c *C) { + c.Assert(Compact(""), Equals, "") + c.Assert(Compact("test"), Equals, "test") + c.Assert(Compact(`C:\a\b\c\d`), Equals, `C:\a\b\c\d`) + c.Assert(Compact(`C:\my\random\directory\test.txt`), Equals, `C:\m\r\d\test.txt`) + c.Assert(Compact(`.\my\random\directory\test.txt`), Equals, `.\m\r\d\test.txt`) +}