Skip to content

Commit df8d285

Browse files
committed
filepath: Add a stand-alone package for explicit-OS path logic
Go's path/filepath has lots of useful path operations, but there is a compile-time decision to select only the logic that applies to your $GOOS. That makes it hard to validate a Windows config from a Linux host (or vice versa) because Go's builtin tools won't tell you whether a path is absolute on the target platform (just whether the path is absolute on *your* platform). This commit adds a new package to do the same sorts of things but with an explicit OS argument. In some cases, there's also an explicit workding directory argument. For example, Go's Abs has [1]: If the path is not absolute it will be joined with the current working directory to turn it into an absolute path. but that doesn't make sense for a cross-platform Abs call because the real current working directory will be for the wrong platform. Instead, cross-platform calls to Abs and similar should fake a working directory as if they were being called from the other platform. The Windows implementation is not very complete, with IsAbs definitely missing a lot of stuff; Abs, Clean, and IsAncestor probably missing stuff; and a lack of Windows-path tests. But the current tools are broken for validating Windows configs anyway, so I've left completing Windows support to future work. Besides adding the new package, I updated the config validation to use the new package where appropriate. For example checks for absolute hook paths (based on [2]) now appropriately account for the target platform (although Abs has limited Windows support at the moment, as mentioned above). There are still a number of config validation checks that use Go's stock filepath, because they're based around actual filesystem access (e.g. reading config.json off the disk, asserting that root.path exists on the disk, etc.). Some of those will need logic to convert between path platforms (which I'm leaving to future work). For example, if root.path is formed for another platform, then: * If root.path is absolute (on the target platform), there's no way to check whether root.path exists inside the bundle. * If root.path is relative, we should be converting it from the target platform to the host platform before joining it to our bundle path. For example, with a Windows bundle in "/foo" on a Linux host where root.path is "bar\baz", then runtime-tools should be checking for the root directory in /foo/bar/baz, not in /foo/bar\baz. The root.path example is somewhat guarded by the bundle requirement for siblinghood [3], but I'd rather enforce siblinghood independently from existence [4], since the spec has separate requirements for both. [1]: https://golang.org/pkg/path/filepath/#Abs [2]: https://github.com/opencontainers/runtime-spec/blame/v1.0.0/config.md#L375 [3]: https://github.com/opencontainers/runtime-spec/blame/v1.0.0/bundle.md#L17-L19 [4]: https://github.com/opencontainers/runtime-spec/blame/v1.0.0/config.md#L43 Signed-off-by: W. Trevor King <wking@tremily.us>
1 parent 9e0e42d commit df8d285

11 files changed

Lines changed: 415 additions & 54 deletions

File tree

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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package filepath
2+
3+
import (
4+
"errors"
5+
"strings"
6+
)
7+
8+
// Abs is a version of path/filepath's Abs with an explicit operating
9+
// system and current working directory.
10+
func Abs(os, path, cwd string) (_ string, err error) {
11+
if os == "windows" {
12+
return "", errors.New("Abs() does not support windows yet")
13+
}
14+
if IsAbs(os, path) {
15+
return Clean(os, path), nil
16+
}
17+
return Clean(os, Join(os, cwd, path)), nil
18+
}
19+
20+
// IsAbs is a version of path/filepath's IsAbs with an explicit
21+
// operating system.
22+
func IsAbs(os, path string) bool {
23+
if os == "windows" {
24+
// FIXME: copy hideous logic from Go's
25+
// src/path/filepath/path_windows.go into somewhere where we can
26+
// put 3-clause BSD licensed code.
27+
return false
28+
}
29+
sep := Separator(os)
30+
return strings.HasPrefix(path, string(sep))
31+
}

filepath/abs_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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: "../a",
54+
cwd: "/cwd",
55+
expected: "/a",
56+
},
57+
{
58+
os: "linux",
59+
path: "../../b",
60+
cwd: "/cwd",
61+
expected: "/b",
62+
},
63+
} {
64+
t.Run(
65+
fmt.Sprintf("Abs(%q,%q,%q)", test.os, test.path, test.cwd),
66+
func(t *testing.T) {
67+
abs, err := Abs(test.os, test.path, test.cwd)
68+
if err != nil {
69+
t.Error(err)
70+
} else if abs != test.expected {
71+
t.Errorf("unexpected result: %q (expected %q)\n", abs, test.expected)
72+
}
73+
},
74+
)
75+
}
76+
}

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+
}

filepath/clean.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package filepath
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// Clean is an explicit-OS version of path/filepath's Clean.
9+
func Clean(os, path string) string {
10+
abs := IsAbs(os, path)
11+
sep := Separator(os)
12+
elements := strings.Split(path, string(sep))
13+
14+
// Replace multiple Separator elements with a single one.
15+
for i := 0; i < len(elements); i++ {
16+
if len(elements[i]) == 0 {
17+
elements = append(elements[:i], elements[i+1:]...)
18+
i--
19+
}
20+
}
21+
22+
// Eliminate each . path name element (the current directory).
23+
for i := 0; i < len(elements); i++ {
24+
if elements[i] == "." && len(elements) > 1 {
25+
elements = append(elements[:i], elements[i+1:]...)
26+
i--
27+
}
28+
}
29+
30+
// Eliminate each inner .. path name element (the parent directory)
31+
// along with the non-.. element that precedes it.
32+
for i := 1; i < len(elements); i++ {
33+
if i > 0 && elements[i] == ".." {
34+
elements = append(elements[:i-1], elements[i+1:]...)
35+
i -= 2
36+
}
37+
}
38+
39+
// Eliminate .. elements that begin a rooted path:
40+
// that is, replace "/.." by "/" at the beginning of a path,
41+
// assuming Separator is '/'.
42+
if abs && len(elements) > 0 {
43+
for elements[0] == ".." {
44+
elements = elements[1:]
45+
}
46+
}
47+
48+
cleaned := strings.Join(elements, string(sep))
49+
if abs {
50+
cleaned = fmt.Sprintf("%c%s", sep, cleaned)
51+
}
52+
if cleaned == path {
53+
return path
54+
}
55+
return Clean(os, cleaned)
56+
}

filepath/clean_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package filepath
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
)
7+
8+
func TestClean(t *testing.T) {
9+
for _, test := range []struct {
10+
os string
11+
path string
12+
expected string
13+
}{
14+
{
15+
os: "linux",
16+
path: "/",
17+
expected: "/",
18+
},
19+
{
20+
os: "linux",
21+
path: "//",
22+
expected: "/",
23+
},
24+
{
25+
os: "linux",
26+
path: "/a",
27+
expected: "/a",
28+
},
29+
{
30+
os: "linux",
31+
path: "/a/",
32+
expected: "/a",
33+
},
34+
{
35+
os: "linux",
36+
path: "//a",
37+
expected: "/a",
38+
},
39+
{
40+
os: "linux",
41+
path: ".",
42+
expected: ".",
43+
},
44+
{
45+
os: "linux",
46+
path: "./c",
47+
expected: "c",
48+
},
49+
{
50+
os: "linux",
51+
path: ".././a",
52+
expected: "../a",
53+
},
54+
{
55+
os: "linux",
56+
path: "a/../b",
57+
expected: "b",
58+
},
59+
} {
60+
t.Run(
61+
fmt.Sprintf("Clean(%q,%q)", test.os, test.path),
62+
func(t *testing.T) {
63+
clean := Clean(test.os, test.path)
64+
if clean != test.expected {
65+
t.Errorf("unexpected result: %q (expected %q)\n", clean, test.expected)
66+
}
67+
},
68+
)
69+
}
70+
}

filepath/doc.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Package filepath implements Go's filepath package with explicit
2+
// operating systems (and for some functions and explicit working
3+
// directory). This allows tools built for one OS to operate on paths
4+
// targeting another OS. For example, a Linux build can determine
5+
// whether a path is absolute on Linux or on Windows.
6+
package filepath

0 commit comments

Comments
 (0)