|
| 1 | +/* |
| 2 | + * Copyright (c) 2023 The GoPlus Authors (goplus.org). All rights reserved. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +package url |
| 18 | + |
| 19 | +// CleanPath is the URL version of path.Clean, it returns a canonical URL path |
| 20 | +// for p, eliminating . and .. elements. |
| 21 | +// |
| 22 | +// The following rules are applied iteratively until no further processing can |
| 23 | +// be done: |
| 24 | +// 1. Replace multiple slashes with a single slash. |
| 25 | +// 2. Eliminate each . path name element (the current directory). |
| 26 | +// 3. Eliminate each inner .. path name element (the parent directory) |
| 27 | +// along with the non-.. element that precedes it. |
| 28 | +// 4. Eliminate .. elements that begin a rooted path: |
| 29 | +// that is, replace "/.." by "/" at the beginning of a path. |
| 30 | +// |
| 31 | +// If the result of this process is an empty string, "/" is returned |
| 32 | +func CleanPath(p string) string { |
| 33 | + const stackBufSize = 128 |
| 34 | + |
| 35 | + // Turn empty string into "/" |
| 36 | + if p == "" { |
| 37 | + return "/" |
| 38 | + } |
| 39 | + |
| 40 | + // Reasonably sized buffer on stack to avoid allocations in the common case. |
| 41 | + // If a larger buffer is required, it gets allocated dynamically. |
| 42 | + buf := make([]byte, 0, stackBufSize) |
| 43 | + |
| 44 | + n := len(p) |
| 45 | + |
| 46 | + // Invariants: |
| 47 | + // reading from path; r is index of next byte to process. |
| 48 | + // writing to buf; w is index of next byte to write. |
| 49 | + |
| 50 | + // path must start with '/' |
| 51 | + r := 1 |
| 52 | + w := 1 |
| 53 | + |
| 54 | + if p[0] != '/' { |
| 55 | + r = 0 |
| 56 | + |
| 57 | + if n+1 > stackBufSize { |
| 58 | + buf = make([]byte, n+1) |
| 59 | + } else { |
| 60 | + buf = buf[:n+1] |
| 61 | + } |
| 62 | + buf[0] = '/' |
| 63 | + } |
| 64 | + |
| 65 | + trailing := n > 1 && p[n-1] == '/' |
| 66 | + |
| 67 | + // A bit more clunky without a 'lazybuf' like the path package, but the loop |
| 68 | + // gets completely inlined (bufApp calls). |
| 69 | + // So in contrast to the path package this loop has no expensive function |
| 70 | + // calls (except make, if needed). |
| 71 | + |
| 72 | + for r < n { |
| 73 | + switch { |
| 74 | + case p[r] == '/': |
| 75 | + // empty path element, trailing slash is added after the end |
| 76 | + r++ |
| 77 | + |
| 78 | + case p[r] == '.' && r+1 == n: |
| 79 | + trailing = true |
| 80 | + r++ |
| 81 | + |
| 82 | + case p[r] == '.' && p[r+1] == '/': |
| 83 | + // . element |
| 84 | + r += 2 |
| 85 | + |
| 86 | + case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): |
| 87 | + // .. element: remove to last / |
| 88 | + r += 3 |
| 89 | + |
| 90 | + if w > 1 { |
| 91 | + // can backtrack |
| 92 | + w-- |
| 93 | + |
| 94 | + if len(buf) == 0 { |
| 95 | + for w > 1 && p[w] != '/' { |
| 96 | + w-- |
| 97 | + } |
| 98 | + } else { |
| 99 | + for w > 1 && buf[w] != '/' { |
| 100 | + w-- |
| 101 | + } |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + default: |
| 106 | + // Real path element. |
| 107 | + // Add slash if needed |
| 108 | + if w > 1 { |
| 109 | + bufApp(&buf, p, w, '/') |
| 110 | + w++ |
| 111 | + } |
| 112 | + |
| 113 | + // Copy element |
| 114 | + for r < n && p[r] != '/' { |
| 115 | + bufApp(&buf, p, w, p[r]) |
| 116 | + w++ |
| 117 | + r++ |
| 118 | + } |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + // Re-append trailing slash |
| 123 | + if trailing && w > 1 { |
| 124 | + bufApp(&buf, p, w, '/') |
| 125 | + w++ |
| 126 | + } |
| 127 | + |
| 128 | + // If the original string was not modified (or only shortened at the end), |
| 129 | + // return the respective substring of the original string. |
| 130 | + // Otherwise return a new string from the buffer. |
| 131 | + if len(buf) == 0 { |
| 132 | + return p[:w] |
| 133 | + } |
| 134 | + return string(buf[:w]) |
| 135 | +} |
| 136 | + |
| 137 | +// Internal helper to lazily create a buffer if necessary. |
| 138 | +// Calls to this function get inlined. |
| 139 | +func bufApp(buf *[]byte, s string, w int, c byte) { |
| 140 | + b := *buf |
| 141 | + if len(b) == 0 { |
| 142 | + // No modification of the original string so far. |
| 143 | + // If the next character is the same as in the original string, we do |
| 144 | + // not yet have to allocate a buffer. |
| 145 | + if s[w] == c { |
| 146 | + return |
| 147 | + } |
| 148 | + |
| 149 | + // Otherwise use either the stack buffer, if it is large enough, or |
| 150 | + // allocate a new buffer on the heap, and copy all previous characters. |
| 151 | + if l := len(s); l > cap(b) { |
| 152 | + *buf = make([]byte, len(s)) |
| 153 | + } else { |
| 154 | + *buf = (*buf)[:l] |
| 155 | + } |
| 156 | + b = *buf |
| 157 | + |
| 158 | + copy(b, s[:w]) |
| 159 | + } |
| 160 | + b[w] = c |
| 161 | +} |
0 commit comments