| 1 |
|
package core |
| 2 |
|
|
| 3 |
|
import ( |
| 4 |
|
"fmt" |
| 5 |
|
"regexp" |
| 6 |
|
"strings" |
| 7 |
|
) |
| 8 |
|
|
| 9 |
|
// MaxNameLen is the maximum length of a database name. Names double as SQL |
| 10 |
|
// database identifiers, so they are kept short and conservative. |
| 11 |
|
const MaxNameLen = 64 |
| 12 |
|
|
| 13 |
|
// nameRe matches a valid database (or owner) name: an alphanumeric first and |
| 14 |
|
// last character, with alphanumerics, hyphens and underscores in between. It |
| 15 |
|
// structurally forbids "." and ".." (no dots at all) and leading/trailing |
| 16 |
|
// separators. |
| 17 |
|
var nameRe = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9_-]*[a-zA-Z0-9])?$`) |
| 18 |
|
|
| 19 |
|
// ValidateName reports whether name is an acceptable database name. It rejects |
| 20 |
|
// the empty string, names longer than MaxNameLen, the traversal names "." and |
| 21 |
|
// ".." explicitly, and anything not matching nameRe. |
| 22 |
21 |
func ValidateName(name string) error { |
| 23 |
21 |
if name == "" { |
| 24 |
1 |
return fmt.Errorf("name must not be empty") |
| 25 |
1 |
} |
| 26 |
20 |
if name == "." || name == ".." { |
| 27 |
2 |
return fmt.Errorf("name %q is not allowed", name) |
| 28 |
2 |
} |
| 29 |
18 |
if len(name) > MaxNameLen { |
| 30 |
1 |
return fmt.Errorf("name is too long (%d > %d)", len(name), MaxNameLen) |
| 31 |
1 |
} |
| 32 |
17 |
if !nameRe.MatchString(name) { |
| 33 |
9 |
return fmt.Errorf("name %q must match %s", name, nameRe.String()) |
| 34 |
9 |
} |
| 35 |
8 |
return nil |
| 36 |
|
} |
| 37 |
|
|
| 38 |
|
// ParseRepoPath splits a repository path into its owner and database segments. |
| 39 |
|
// It accepts "~user/db", "user/db", and leading/trailing slashes. It rejects |
| 40 |
|
// empty segments, ".."/"." traversal segments, and any path that does not have |
| 41 |
|
// exactly two segments. The returned owner never carries a leading "~". |
| 42 |
|
// |
| 43 |
|
// ParseRepoPath validates structure only; owner and db are not name-validated |
| 44 |
|
// here (callers that touch disk or SQL must additionally run ValidateName). |
| 45 |
15 |
func ParseRepoPath(path string) (owner, db string, err error) { |
| 46 |
15 |
trimmed := strings.Trim(path, "/") |
| 47 |
15 |
if trimmed == "" { |
| 48 |
2 |
return "", "", fmt.Errorf("empty repo path") |
| 49 |
2 |
} |
| 50 |
13 |
segs := strings.Split(trimmed, "/") |
| 51 |
13 |
if len(segs) != 2 { |
| 52 |
3 |
return "", "", fmt.Errorf("repo path %q must have exactly 2 segments, got %d", path, len(segs)) |
| 53 |
3 |
} |
| 54 |
19 |
for _, s := range segs { |
| 55 |
19 |
if s == "" { |
| 56 |
0 |
return "", "", fmt.Errorf("repo path %q has an empty segment", path) |
| 57 |
0 |
} |
| 58 |
19 |
if s == "." || s == ".." { |
| 59 |
3 |
return "", "", fmt.Errorf("repo path %q contains a traversal segment %q", path, s) |
| 60 |
3 |
} |
| 61 |
|
} |
| 62 |
7 |
owner = strings.TrimPrefix(segs[0], "~") |
| 63 |
7 |
db = segs[1] |
| 64 |
7 |
if owner == "" { |
| 65 |
1 |
return "", "", fmt.Errorf("repo path %q has an empty owner", path) |
| 66 |
1 |
} |
| 67 |
6 |
return owner, db, nil |
| 68 |
|
} |