Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

filepath: Add a stand-alone package for explicit-OS path logic #445

Merged
merged 1 commit into from
Sep 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,6 @@ test: .gofmt .govet .golint .gotest
.golint:
golint -set_exit_status $(PACKAGES)

UTDIRS = ./validate/...
UTDIRS = ./filepath/... ./validate/...
.gotest:
go test $(UTDIRS)
52 changes: 52 additions & 0 deletions filepath/abs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package filepath

import (
"errors"
"regexp"
"strings"
)

var windowsAbs = regexp.MustCompile(`^[a-zA-Z]:\\.*$`)

// Abs is a version of path/filepath's Abs with an explicit operating
// system and current working directory.
func Abs(os, path, cwd string) (_ string, err error) {
if os == "windows" {
return "", errors.New("Abs() does not support windows yet")
}
if IsAbs(os, path) {
return Clean(os, path), nil
}
return Clean(os, Join(os, cwd, path)), nil
}

// IsAbs is a version of path/filepath's IsAbs with an explicit
// operating system.
func IsAbs(os, path string) bool {
if os == "windows" {
// FIXME: copy hideous logic from Go's
// src/path/filepath/path_windows.go into somewhere where we can
// put 3-clause BSD licensed code.
return windowsAbs.MatchString(path)
}
sep := Separator(os)

// POSIX has [1]:
//
// > If a pathname begins with two successive <slash> characters,
// > the first component following the leading <slash> characters
// > may be interpreted in an implementation-defined manner,
// > although more than two leading <slash> characters shall be
// > treated as a single <slash> character.
//
// And Boost treats // as non-absolute [2], but Linux [3,4], Python
// [5] and Go [6] all treat // as absolute.
//
// [1]: http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13
// [2]: https://github.com/boostorg/filesystem/blob/boost-1.64.0/test/path_test.cpp#L861
// [3]: http://man7.org/linux/man-pages/man7/path_resolution.7.html
// [4]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/filesystems/path-lookup.md?h=v4.12#n41
// [5]: https://github.com/python/cpython/blob/v3.6.1/Lib/posixpath.py#L64-L66
// [6]: https://go.googlesource.com/go/+/go1.8.3/src/path/path.go#199
return strings.HasPrefix(path, string(sep))
}
171 changes: 171 additions & 0 deletions filepath/abs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package filepath

import (
"fmt"
"testing"
)

func TestAbs(t *testing.T) {
for _, test := range []struct {
os string
path string
cwd string
expected string
}{
{
os: "linux",
path: "/",
cwd: "/cwd",
expected: "/",
},
{
os: "linux",
path: "/a",
cwd: "/cwd",
expected: "/a",
},
{
os: "linux",
path: "/a/",
cwd: "/cwd",
expected: "/a",
},
{
os: "linux",
path: "//a",
cwd: "/cwd",
expected: "/a",
},
{
os: "linux",
path: ".",
cwd: "/cwd",
expected: "/cwd",
},
{
os: "linux",
path: "./c",
cwd: "/a/b",
expected: "/a/b/c",
},
{
os: "linux",
path: ".//c",
cwd: "/a/b",
expected: "/a/b/c",
},
{
os: "linux",
path: "../a",
cwd: "/cwd",
expected: "/a",
},
{
os: "linux",
path: "../../b",
cwd: "/cwd",
expected: "/b",
},
} {
t.Run(
fmt.Sprintf("Abs(%q,%q,%q)", test.os, test.path, test.cwd),
func(t *testing.T) {
abs, err := Abs(test.os, test.path, test.cwd)
if err != nil {
t.Error(err)
} else if abs != test.expected {
t.Errorf("unexpected result: %q (expected %q)\n", abs, test.expected)
}
},
)
}
}

func TestIsAbs(t *testing.T) {
for _, test := range []struct {
os string
path string
expected bool
}{
{
os: "linux",
path: "/",
expected: true,
},
{
os: "linux",
path: "/a",
expected: true,
},
{
os: "linux",
path: "//",
expected: true,
},
{
os: "linux",
path: "//a",
expected: true,
},
{
os: "linux",
path: ".",
expected: false,
},
{
os: "linux",
path: "./a",
expected: false,
},
{
os: "linux",
path: ".//a",
expected: false,
},
{
os: "linux",
path: "../a",
expected: false,
},
{
os: "linux",
path: "../../a",
expected: false,
},
{
os: "windows",
path: "c:\\",
expected: true,
},
{
os: "windows",
path: "c:\\a",
expected: true,
},
{
os: "windows",
path: ".",
expected: false,
},
{
os: "windows",
path: ".\\a",
expected: false,
},
{
os: "windows",
path: "..\\a",
expected: false,
},
} {
t.Run(
fmt.Sprintf("IsAbs(%q,%q)", test.os, test.path),
func(t *testing.T) {
abs := IsAbs(test.os, test.path)
if abs != test.expected {
t.Errorf("unexpected result: %t (expected %t)\n", abs, test.expected)
}
},
)
}
}
32 changes: 32 additions & 0 deletions filepath/ancestor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package filepath

import (
"fmt"
"strings"
)

// IsAncestor returns true when pathB is an strict ancestor of pathA,
// and false where the paths are equal or pathB is outside of pathA.
// Paths that are not absolute will be made absolute with Abs.
func IsAncestor(os, pathA, pathB, cwd string) (_ bool, err error) {
if pathA == pathB {
return false, nil
}

pathA, err = Abs(os, pathA, cwd)
if err != nil {
return false, err
}
pathB, err = Abs(os, pathB, cwd)
if err != nil {
return false, err
}
sep := Separator(os)
if !strings.HasSuffix(pathA, string(sep)) {
pathA = fmt.Sprintf("%s%c", pathA, sep)
}
if pathA == pathB {
return false, nil
}
return strings.HasPrefix(pathB, pathA), nil
}
113 changes: 113 additions & 0 deletions filepath/ancestor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package filepath

import (
"fmt"
"testing"
)

func TestIsAncestor(t *testing.T) {
for _, test := range []struct {
os string
pathA string
pathB string
cwd string
expected bool
}{
{
os: "linux",
pathA: "/",
pathB: "/a",
cwd: "/cwd",
expected: true,
},
{
os: "linux",
pathA: "/a",
pathB: "/a",
cwd: "/cwd",
expected: false,
},
{
os: "linux",
pathA: "/a",
pathB: "/",
cwd: "/cwd",
expected: false,
},
{
os: "linux",
pathA: "/a",
pathB: "/ab",
cwd: "/cwd",
expected: false,
},
{
os: "linux",
pathA: "/a/",
pathB: "/a",
cwd: "/cwd",
expected: false,
},
{
os: "linux",
pathA: "//a",
pathB: "/a",
cwd: "/cwd",
expected: false,
},
{
os: "linux",
pathA: "//a",
pathB: "/a/b",
cwd: "/cwd",
expected: true,
},
{
os: "linux",
pathA: "/a",
pathB: "/a/",
cwd: "/cwd",
expected: false,
},
{
os: "linux",
pathA: "/a",
pathB: ".",
cwd: "/cwd",
expected: false,
},
{
os: "linux",
pathA: "/a",
pathB: "b",
cwd: "/a",
expected: true,
},
{
os: "linux",
pathA: "/a",
pathB: "../a",
cwd: "/cwd",
expected: false,
},
{
os: "linux",
pathA: "/a",
pathB: "../a/b",
cwd: "/cwd",
expected: true,
},
} {
t.Run(
fmt.Sprintf("IsAncestor(%q,%q,%q,%q)", test.os, test.pathA, test.pathB, test.cwd),
func(t *testing.T) {
ancestor, err := IsAncestor(test.os, test.pathA, test.pathB, test.cwd)
if err != nil {
t.Error(err)
} else if ancestor != test.expected {
t.Errorf("unexpected result: %t", ancestor)
}
},
)
}
}
Loading