diff --git a/util/util.go b/util/util.go index a5ab5c93..fa1d7ce8 100644 --- a/util/util.go +++ b/util/util.go @@ -293,6 +293,84 @@ loop: return result } +// SafePathClean does path.Clean, but it preserves any trailing slash if it was +// present in the initial path. +func SafePathClean(s string) string { + hasSlash := strings.HasSuffix(s, "/") + clean := path.Clean(s) // removes trailing slashes + if hasSlash { // add it back if it was taken off + clean = clean + "/" + } + return clean +} + +// SegmentedPathSplit splits an absolute path into chunks including their slash. +// This is similar to the PathSplit function, but the slashes are not lost here! +// TODO: There is likely a more efficient implementation of this function. +func SegmentedPathSplit(p string) []string { + out := []string{} + sponge := "" + for i := 0; i < len(p); i++ { + sponge = sponge + string(p[i]) + if string(p[i]) == "/" { + out = append(out, sponge) // found one! + sponge = "" // reset + } + } + if sponge != "" { + out = append(out, sponge) // the last piece + } + + return out +} + +// CommonPathPrefix returns the longest common prefix directory out of all the +// input paths! This is always a directory and thus ends with a slash, unless +// all of the paths are identical and are files, or if only one path is given in +// which case that is returned, or unless no paths are given in which case the +// empty string is returned. If any of your input paths are not absolute, and as +// such do not begin with a slash, then the behaviour is undefined. +func CommonPathPrefix(paths ...string) string { + // XXX: I am not a good algorithmist, there is probably a more efficient + // way to write this algorithm. Patches are welcome! + if len(paths) == 0 { + return "" + } + if len(paths) == 1 { + return paths[0] + } + + sps := make([][]string, 0) + for _, x := range paths { + if !strings.HasPrefix(x, "/") { + // TODO: panic? error? + return "" // undefined behaviour! + } + //x = SafePathClean(x) // TODO: Should we "safe clean" each path? + sp := SegmentedPathSplit(x) // compute once in advance + sps = append(sps, sp) + } + + z := sps[0] // SegmentedPathSplit(paths[0]) // arbitrarily choose the first one + ix := 0 + ret := "" + for { + for i := range paths { + sp := sps[i] // SegmentedPathSplit(paths[i]) + if len(sp) <= ix { // one path is longer + return ret + } + + if sp[ix] != z[ix] { // the chunk differs! + return ret + } + } + + ret = ret + z[ix] // append what we've got so far! + ix++ + } +} + // PathPrefixDelta returns the delta of the path prefix, which tells you how // many path tokens different the prefix is. func PathPrefixDelta(p, prefix string) int { diff --git a/util/util_test.go b/util/util_test.go index e88c2ed1..a06006ec 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -795,6 +795,117 @@ func TestUtilT11(t *testing.T) { } } +func TestSegmentedPathSplit(t *testing.T) { + if ex, out := []string{}, SegmentedPathSplit( + "", + ); !reflect.DeepEqual(out, ex) { + t.Errorf("expected: %v got: %v", ex, out) + } + + if ex, out := []string{"/"}, SegmentedPathSplit( + "/", + ); !reflect.DeepEqual(out, ex) { + t.Errorf("expected: %v got: %v", ex, out) + } + + if ex, out := []string{"/", "foo/", "bar/"}, SegmentedPathSplit( + "/foo/bar/", + ); !reflect.DeepEqual(out, ex) { + t.Errorf("expected: %v got: %v", ex, out) + } + + if ex, out := []string{"/", "foo/", "bar"}, SegmentedPathSplit( + "/foo/bar", + ); !reflect.DeepEqual(out, ex) { + t.Errorf("expected: %v got: %v", ex, out) + } +} + +func TestCommonPathPrefix1(t *testing.T) { + if ex, out := "/foo/whatever2/", CommonPathPrefix( + "/foo/whatever2/", + "/foo/whatever2/", + "/foo/whatever2/", + ); out != ex { + t.Errorf("expected: %v got: %v", ex, out) + } +} + +func TestCommonPathPrefix2(t *testing.T) { + if ex, out := "/whatever1", CommonPathPrefix( + "/whatever1", + ); out != ex { + t.Errorf("expected: %v got: %v", ex, out) + } + if ex, out := "/whatever2", CommonPathPrefix( + "/whatever2", + "/whatever2", + ); out != ex { + t.Errorf("expected: %v got: %v", ex, out) + } + if ex, out := "/foo/whatever1", CommonPathPrefix( + "/foo/whatever1", + "/foo/whatever1", + ); out != ex { + t.Errorf("expected: %v got: %v", ex, out) + } + if ex, out := "/foo/whatever2", CommonPathPrefix( + "/foo/whatever2", + "/foo/whatever2", + "/foo/whatever2", + ); out != ex { + t.Errorf("expected: %v got: %v", ex, out) + } + + if ex, out := "/whatever3/", CommonPathPrefix( + "/whatever3/", + "/whatever3/", + ); out != ex { + t.Errorf("expected: %v got: %v", ex, out) + } + if ex, out := "/foo/whatever3/", CommonPathPrefix( + "/foo/whatever3/", + "/foo/whatever3/", + ); out != ex { + t.Errorf("expected: %v got: %v", ex, out) + } + if ex, out := "/foo/whatever4/", CommonPathPrefix( + "/foo/whatever4/", + "/foo/whatever4/", + "/foo/whatever4/", + ); out != ex { + t.Errorf("expected: %v got: %v", ex, out) + } + + if ex, out := "/", CommonPathPrefix( + "/foo/bar", + "/bar/baz/", + "/baz/bing/wow", + ); out != ex { + t.Errorf("expected: %v got: %v", ex, out) + } + + if ex, out := "/foo/", CommonPathPrefix( + "/foo/bar/", + "/foo/bar/dude", + "/foo/bar", // this is not the same as /foo/bar/ ! + ); out != ex { + t.Errorf("expected: %v got: %v", ex, out) + } + + // If we want to "safe clean" each path, then this test should be added. + //if ex, out := "/home/james/tmp/", CommonPathPrefix( + // "/home/james/tmp/coverage/test", + // "/home/james/tmp/covert/operator", + // "/home/james/tmp/coven/members", + // "/home//james/tmp/coventry", + // "/home/james/././tmp/covertly/foo", + // "/home/luser/../james/tmp/coved/bar", + //); out != ex { + // t.Errorf("expected: %v got: %v", ex, out) + //} +} + func TestUtilFlattenListWithSplit1(t *testing.T) { { in := []string{} // input