diff --git a/cleanpath_benchmark_test.go b/cleanpath_benchmark_test.go new file mode 100644 index 00000000..6741e9f7 --- /dev/null +++ b/cleanpath_benchmark_test.go @@ -0,0 +1,135 @@ +package gin + +import "testing" + +func oldCleanPath(p string) string { + // Turn empty string into "/" + if p == "" { + return "/" + } + + n := len(p) + var buf []byte + + // Invariants: + // reading from path; r is index of next byte to process. + // writing to buf; w is index of next byte to write. + + // path must start with '/' + r := 1 + w := 1 + + if p[0] != '/' { + r = 0 + buf = make([]byte, n+1) + buf[0] = '/' + } + + trailing := n > 1 && p[n-1] == '/' + + // A bit more clunky without a 'lazybuf' like the path package, but the loop + // gets completely inlined (bufApp). So in contrast to the path package this + // loop has no expensive function calls (except 1x make) + + for r < n { + switch { + case p[r] == '/': + // empty path element, trailing slash is added after the end + r++ + + case p[r] == '.' && r+1 == n: + trailing = true + r++ + + case p[r] == '.' && p[r+1] == '/': + // . element + r += 2 + + case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): + // .. element: remove to last / + r += 3 + + if w > 1 { + // can backtrack + w-- + + if buf == nil { + for w > 1 && p[w] != '/' { + w-- + } + } else { + for w > 1 && buf[w] != '/' { + w-- + } + } + } + + default: + // real path element. + // add slash if needed + if w > 1 { + bufApp(&buf, p, w, '/') + w++ + } + + // copy element + for r < n && p[r] != '/' { + bufApp(&buf, p, w, p[r]) + w++ + r++ + } + } + } + + // re-append trailing slash + if trailing && w > 1 { + bufApp(&buf, p, w, '/') + w++ + } + + if buf == nil { + return p[:w] + } + return string(buf[:w]) +} + +var result string + +func BenchmarkCleanPath(b *testing.B) { + functions := []struct { + test string + fun func(string) string + }{ + {"OldCleanPath", oldCleanPath}, + {"NewCleanPath", cleanPath}, + } + + paths := []string{ + "/", "/abc", "/a/b/c", "/abc/", "/a/b/c/", + "", "a/", "abc", "abc/def", "a/b/c", + "//", "/abc//", "/abc/def//", "/a/b/c//", + "/abc//def//ghi", "//abc", "///abc", + "//abc//", ".", "./", "/abc/./def", + "/./abc/def", "/abc/.", "..", "../", + "../../", "../..", "../../abc", + "/abc/def/ghi/../jkl", "/abc/def/../ghi/../jkl", + "/abc/def/..", "/abc/def/../..", + "/abc/def/../../..", "/abc/def/../../..", + "/abc/def/../../../ghi/jkl/../../../mno", + "abc/..def", "abc/./...", "abc/a../...", "abc/a..z", + "abc/./../def", "abc//./../def", "abc/../../././../def", + "abc/./../..def", "abc/../.../..def", "abc/.//../..def", + } + + for _, function := range functions { + b.Run(function.test, func(b *testing.B) { + for n := 0; n < b.N; n++ { + var r string + for _, path := range paths { + r = function.fun(path) + } + result = r + } + }) + } +} diff --git a/path.go b/path.go index d1f59622..36aa883a 100644 --- a/path.go +++ b/path.go @@ -48,37 +48,48 @@ func cleanPath(p string) string { // loop has no expensive function calls (except 1x make) for r < n { - switch { - case p[r] == '/': + switch p[r] { + case '/': // empty path element, trailing slash is added after the end r++ - case p[r] == '.' && r+1 == n: - trailing = true - r++ - - case p[r] == '.' && p[r+1] == '/': + case '.': // . element - r += 2 + if r+1 == n { + trailing = true + r++ + continue - case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): - // .. element: remove to last / - r += 3 + } else { + switch p[r+1] { + case '/': + r += 2 + continue - if w > 1 { - // can backtrack - w-- + case '.': + if r+2 == n || p[r+2] == '/' { + // .. element: remove to last / + r += 3 - if buf == nil { - for w > 1 && p[w] != '/' { - w-- - } - } else { - for w > 1 && buf[w] != '/' { - w-- + if w > 1 { + // can backtrack + w-- + + if buf == nil { + for w > 1 && p[w] != '/' { + w-- + } + } else { + for w > 1 && buf[w] != '/' { + w-- + } + } + } + continue } } } + fallthrough default: // real path element. diff --git a/path_test.go b/path_test.go index c1e6ed4f..f20150ae 100644 --- a/path_test.go +++ b/path_test.go @@ -60,10 +60,20 @@ var cleanTests = []struct { {"/abc/def/../../..", "/"}, {"/abc/def/../../../ghi/jkl/../../../mno", "/mno"}, + // Keep .. elements + {"abc/..def", "/abc/..def"}, + {"abc/./...", "/abc/..."}, + {"abc/a../...", "/abc/a../..."}, + {"abc/a..z", "/abc/a..z"}, + // Combinations {"abc/./../def", "/def"}, {"abc//./../def", "/def"}, {"abc/../../././../def", "/def"}, + {"abc/../../././../...", "/..."}, + {"abc/./../..def", "/..def"}, + {"abc/../.../..def", "/.../..def"}, + {"abc/.//../..def", "/..def"}, } func TestPathClean(t *testing.T) {