Skip to content
Open
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
78 changes: 48 additions & 30 deletions path.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,29 @@ package gin
//
// If the result of this process is an empty string, "/" is returned.
func cleanPath(p string) string {
const stackBufSize = 128
// Turn empty string into "/"
if p == "" {
return "/"
}

n := len(p)

// If the path length is 1, handle special cases separately:
// - If it is "/" or ".", return "/".
// - Otherwise, prepend "/" to the path and return it.
if n == 1 {
if p[0] == '/' || p[0] == '.' {
return "/"
}
return "/" + p
}

const stackBufSize = 128

// Reasonably sized buffer on stack to avoid allocations in the common case.
// If a larger buffer is required, it gets allocated dynamically.
buf := make([]byte, 0, stackBufSize)

n := len(p)

// Invariants:
// reading from path; r is index of next byte to process.
// writing to buf; w is index of next byte to write.
Expand All @@ -50,46 +61,52 @@ func cleanPath(p string) string {
buf[0] = '/'
}

trailing := n > 1 && p[n-1] == '/'
var trailing bool

// A bit more clunky without a 'lazybuf' like the path package, but the loop
// gets completely inlined (bufApp calls).
// loop has no expensive function calls (except 1x make) // So in contrast to the path package this loop has no expensive function
// calls (except make, if needed).

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] == '/':
// . 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 len(buf) == 0 {
for w > 1 && p[w] != '/' {
w--
}
} else {
for w > 1 && buf[w] != '/' {
case '.':
if r+1 == n {
trailing = true
r++
// Reduce one comparison between r and n
goto endOfLoop
}
switch p[r+1] {
case '/':
// . element
r += 2

case '.':
if r+2 == n || p[r+2] == '/' {
// .. element: remove to last /
r += 3

if w > 1 {
// can backtrack
w--

if len(buf) == 0 {
for w > 1 && p[w] != '/' {
w--
}
} else {
for w > 1 && buf[w] != '/' {
w--
}
}
}
}
}

default:
// Real path element.
// Add slash if needed
Expand All @@ -107,8 +124,9 @@ func cleanPath(p string) string {
}
}

endOfLoop:
// Re-append trailing slash
if trailing && w > 1 {
if (trailing || p[n-1] == '/') && w > 1 {
bufApp(&buf, p, w, '/')
w++
}
Expand Down
115 changes: 115 additions & 0 deletions path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var cleanTests = []cleanPathTest{

// missing root
{"", "/"},
{"a", "/a"},
{"a/", "/a/"},
{"abc", "/abc"},
{"abc/def", "/abc/def"},
Expand Down Expand Up @@ -143,3 +144,117 @@ func BenchmarkPathCleanLong(b *testing.B) {
}
}
}

func cleanPathOld(p string) string {
const stackBufSize = 128
// Turn empty string into "/"
if p == "" {
return "/"
}

// Reasonably sized buffer on stack to avoid allocations in the common case.
// If a larger buffer is required, it gets allocated dynamically.
buf := make([]byte, 0, stackBufSize)

n := len(p)

// 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

if n+1 > stackBufSize {
buf = make([]byte, n+1)
} else {
buf = buf[: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 calls).
// loop has no expensive function calls (except 1x make) // So in contrast to the path package this loop has no expensive function
// calls (except make, if needed).

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 len(buf) == 0 {
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 the original string was not modified (or only shortened at the end),
// return the respective substring of the original string.
// Otherwise return a new string from the buffer.
if len(buf) == 0 {
return p[:w]
}
return string(buf[:w])
}

func BenchmarkPathCleanOld(b *testing.B) {
b.ReportAllocs()

for i := 0; i < b.N; i++ {
for _, test := range cleanTests {
cleanPathOld(test.path)
}
}
}