Skip to content

Commit 2302d4c

Browse files
authored
Merge pull request #445 from wking/explicit-os-filepath
filepath: Add a stand-alone package for explicit-OS path logic
2 parents 02b49ac + cf64923 commit 2302d4c

File tree

11 files changed

+534
-84
lines changed

11 files changed

+534
-84
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,6 @@ test: .gofmt .govet .golint .gotest
5353
.golint:
5454
golint -set_exit_status $(PACKAGES)
5555

56-
UTDIRS = ./validate/...
56+
UTDIRS = ./filepath/... ./validate/...
5757
.gotest:
5858
go test $(UTDIRS)

filepath/abs.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package filepath
2+
3+
import (
4+
"errors"
5+
"regexp"
6+
"strings"
7+
)
8+
9+
var windowsAbs = regexp.MustCompile(`^[a-zA-Z]:\\.*$`)
10+
11+
// Abs is a version of path/filepath's Abs with an explicit operating
12+
// system and current working directory.
13+
func Abs(os, path, cwd string) (_ string, err error) {
14+
if os == "windows" {
15+
return "", errors.New("Abs() does not support windows yet")
16+
}
17+
if IsAbs(os, path) {
18+
return Clean(os, path), nil
19+
}
20+
return Clean(os, Join(os, cwd, path)), nil
21+
}
22+
23+
// IsAbs is a version of path/filepath's IsAbs with an explicit
24+
// operating system.
25+
func IsAbs(os, path string) bool {
26+
if os == "windows" {
27+
// FIXME: copy hideous logic from Go's
28+
// src/path/filepath/path_windows.go into somewhere where we can
29+
// put 3-clause BSD licensed code.
30+
return windowsAbs.MatchString(path)
31+
}
32+
sep := Separator(os)
33+
34+
// POSIX has [1]:
35+
//
36+
// > If a pathname begins with two successive <slash> characters,
37+
// > the first component following the leading <slash> characters
38+
// > may be interpreted in an implementation-defined manner,
39+
// > although more than two leading <slash> characters shall be
40+
// > treated as a single <slash> character.
41+
//
42+
// And Boost treats // as non-absolute [2], but Linux [3,4], Python
43+
// [5] and Go [6] all treat // as absolute.
44+
//
45+
// [1]: http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13
46+
// [2]: https://github.com/boostorg/filesystem/blob/boost-1.64.0/test/path_test.cpp#L861
47+
// [3]: http://man7.org/linux/man-pages/man7/path_resolution.7.html
48+
// [4]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/filesystems/path-lookup.md?h=v4.12#n41
49+
// [5]: https://github.com/python/cpython/blob/v3.6.1/Lib/posixpath.py#L64-L66
50+
// [6]: https://go.googlesource.com/go/+/go1.8.3/src/path/path.go#199
51+
return strings.HasPrefix(path, string(sep))
52+
}

filepath/abs_test.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package filepath
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
)
7+
8+
func TestAbs(t *testing.T) {
9+
for _, test := range []struct {
10+
os string
11+
path string
12+
cwd string
13+
expected string
14+
}{
15+
{
16+
os: "linux",
17+
path: "/",
18+
cwd: "/cwd",
19+
expected: "/",
20+
},
21+
{
22+
os: "linux",
23+
path: "/a",
24+
cwd: "/cwd",
25+
expected: "/a",
26+
},
27+
{
28+
os: "linux",
29+
path: "/a/",
30+
cwd: "/cwd",
31+
expected: "/a",
32+
},
33+
{
34+
os: "linux",
35+
path: "//a",
36+
cwd: "/cwd",
37+
expected: "/a",
38+
},
39+
{
40+
os: "linux",
41+
path: ".",
42+
cwd: "/cwd",
43+
expected: "/cwd",
44+
},
45+
{
46+
os: "linux",
47+
path: "./c",
48+
cwd: "/a/b",
49+
expected: "/a/b/c",
50+
},
51+
{
52+
os: "linux",
53+
path: ".//c",
54+
cwd: "/a/b",
55+
expected: "/a/b/c",
56+
},
57+
{
58+
os: "linux",
59+
path: "../a",
60+
cwd: "/cwd",
61+
expected: "/a",
62+
},
63+
{
64+
os: "linux",
65+
path: "../../b",
66+
cwd: "/cwd",
67+
expected: "/b",
68+
},
69+
} {
70+
t.Run(
71+
fmt.Sprintf("Abs(%q,%q,%q)", test.os, test.path, test.cwd),
72+
func(t *testing.T) {
73+
abs, err := Abs(test.os, test.path, test.cwd)
74+
if err != nil {
75+
t.Error(err)
76+
} else if abs != test.expected {
77+
t.Errorf("unexpected result: %q (expected %q)\n", abs, test.expected)
78+
}
79+
},
80+
)
81+
}
82+
}
83+
84+
func TestIsAbs(t *testing.T) {
85+
for _, test := range []struct {
86+
os string
87+
path string
88+
expected bool
89+
}{
90+
{
91+
os: "linux",
92+
path: "/",
93+
expected: true,
94+
},
95+
{
96+
os: "linux",
97+
path: "/a",
98+
expected: true,
99+
},
100+
{
101+
os: "linux",
102+
path: "//",
103+
expected: true,
104+
},
105+
{
106+
os: "linux",
107+
path: "//a",
108+
expected: true,
109+
},
110+
{
111+
os: "linux",
112+
path: ".",
113+
expected: false,
114+
},
115+
{
116+
os: "linux",
117+
path: "./a",
118+
expected: false,
119+
},
120+
{
121+
os: "linux",
122+
path: ".//a",
123+
expected: false,
124+
},
125+
{
126+
os: "linux",
127+
path: "../a",
128+
expected: false,
129+
},
130+
{
131+
os: "linux",
132+
path: "../../a",
133+
expected: false,
134+
},
135+
{
136+
os: "windows",
137+
path: "c:\\",
138+
expected: true,
139+
},
140+
{
141+
os: "windows",
142+
path: "c:\\a",
143+
expected: true,
144+
},
145+
{
146+
os: "windows",
147+
path: ".",
148+
expected: false,
149+
},
150+
{
151+
os: "windows",
152+
path: ".\\a",
153+
expected: false,
154+
},
155+
{
156+
os: "windows",
157+
path: "..\\a",
158+
expected: false,
159+
},
160+
} {
161+
t.Run(
162+
fmt.Sprintf("IsAbs(%q,%q)", test.os, test.path),
163+
func(t *testing.T) {
164+
abs := IsAbs(test.os, test.path)
165+
if abs != test.expected {
166+
t.Errorf("unexpected result: %t (expected %t)\n", abs, test.expected)
167+
}
168+
},
169+
)
170+
}
171+
}

filepath/ancestor.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package filepath
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// IsAncestor returns true when pathB is an strict ancestor of pathA,
9+
// and false where the paths are equal or pathB is outside of pathA.
10+
// Paths that are not absolute will be made absolute with Abs.
11+
func IsAncestor(os, pathA, pathB, cwd string) (_ bool, err error) {
12+
if pathA == pathB {
13+
return false, nil
14+
}
15+
16+
pathA, err = Abs(os, pathA, cwd)
17+
if err != nil {
18+
return false, err
19+
}
20+
pathB, err = Abs(os, pathB, cwd)
21+
if err != nil {
22+
return false, err
23+
}
24+
sep := Separator(os)
25+
if !strings.HasSuffix(pathA, string(sep)) {
26+
pathA = fmt.Sprintf("%s%c", pathA, sep)
27+
}
28+
if pathA == pathB {
29+
return false, nil
30+
}
31+
return strings.HasPrefix(pathB, pathA), nil
32+
}

filepath/ancestor_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package filepath
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
)
7+
8+
func TestIsAncestor(t *testing.T) {
9+
for _, test := range []struct {
10+
os string
11+
pathA string
12+
pathB string
13+
cwd string
14+
expected bool
15+
}{
16+
{
17+
os: "linux",
18+
pathA: "/",
19+
pathB: "/a",
20+
cwd: "/cwd",
21+
expected: true,
22+
},
23+
{
24+
os: "linux",
25+
pathA: "/a",
26+
pathB: "/a",
27+
cwd: "/cwd",
28+
expected: false,
29+
},
30+
{
31+
os: "linux",
32+
pathA: "/a",
33+
pathB: "/",
34+
cwd: "/cwd",
35+
expected: false,
36+
},
37+
{
38+
os: "linux",
39+
pathA: "/a",
40+
pathB: "/ab",
41+
cwd: "/cwd",
42+
expected: false,
43+
},
44+
{
45+
os: "linux",
46+
pathA: "/a/",
47+
pathB: "/a",
48+
cwd: "/cwd",
49+
expected: false,
50+
},
51+
{
52+
os: "linux",
53+
pathA: "//a",
54+
pathB: "/a",
55+
cwd: "/cwd",
56+
expected: false,
57+
},
58+
{
59+
os: "linux",
60+
pathA: "//a",
61+
pathB: "/a/b",
62+
cwd: "/cwd",
63+
expected: true,
64+
},
65+
{
66+
os: "linux",
67+
pathA: "/a",
68+
pathB: "/a/",
69+
cwd: "/cwd",
70+
expected: false,
71+
},
72+
{
73+
os: "linux",
74+
pathA: "/a",
75+
pathB: ".",
76+
cwd: "/cwd",
77+
expected: false,
78+
},
79+
{
80+
os: "linux",
81+
pathA: "/a",
82+
pathB: "b",
83+
cwd: "/a",
84+
expected: true,
85+
},
86+
{
87+
os: "linux",
88+
pathA: "/a",
89+
pathB: "../a",
90+
cwd: "/cwd",
91+
expected: false,
92+
},
93+
{
94+
os: "linux",
95+
pathA: "/a",
96+
pathB: "../a/b",
97+
cwd: "/cwd",
98+
expected: true,
99+
},
100+
} {
101+
t.Run(
102+
fmt.Sprintf("IsAncestor(%q,%q,%q,%q)", test.os, test.pathA, test.pathB, test.cwd),
103+
func(t *testing.T) {
104+
ancestor, err := IsAncestor(test.os, test.pathA, test.pathB, test.cwd)
105+
if err != nil {
106+
t.Error(err)
107+
} else if ancestor != test.expected {
108+
t.Errorf("unexpected result: %t", ancestor)
109+
}
110+
},
111+
)
112+
}
113+
}

0 commit comments

Comments
 (0)