coverage~bigbes/sr-ht-compare9720ccc2core/names.go

Coverage
98.0% 49/50 statements
Δ
Blob
4a804ac
Uncovered L83-L84
1 package core
2
3 import "strings"
4
5 // maxRepoNameLen bounds repository names. Owners are bounded by meta.sr.ht at
6 // registration time, so ValidOwner enforces only structure, not length.
7 const maxRepoNameLen = 100
8
9 // isNameByte reports whether c is allowed in a sourcehut owner or repo name:
10 // lowercase alphanumerics plus '_', '-', and '.'. Note '/' is deliberately
11 // excluded, so a name can never span path components.
12 182 func isNameByte(c byte) bool {
13 182 switch {
14 165 case c >= 'a' && c <= 'z':
15 165 return true
16 4 case c >= '0' && c <= '9':
17 4 return true
18 6 case c == '_' || c == '-' || c == '.':
19 6 return true
20 7 default:
21 7 return false
22 }
23 }
24
25 // validName holds the rules shared by owners and repos: non-empty, drawn from
26 // the allowed byte set, not starting with '-' (which would look like a git or
27 // shell option), and containing no ".." (path traversal). '/' is rejected
28 // implicitly because it is not an allowed byte.
29 23 func validName(s string) bool {
30 23 if s == "" {
31 2 return false
32 2 }
33 21 if s[0] == '-' {
34 2 return false
35 2 }
36 19 if strings.Contains(s, "..") {
37 2 return false
38 2 }
39 182 for i := 0; i < len(s); i++ {
40 182 if !isNameByte(s[i]) {
41 7 return false
42 7 }
43 }
44 10 return true
45 }
46
47 // ValidOwner reports whether s is a well-formed sourcehut owner name (the part
48 // after '~' in a URL). Callers must strip the leading '~' first.
49 14 func ValidOwner(s string) bool {
50 14 return validName(s)
51 14 }
52
53 // ValidRepoName reports whether s is a well-formed repository name (same
54 // character family as an owner, capped at maxRepoNameLen).
55 10 func ValidRepoName(s string) bool {
56 10 return len(s) <= maxRepoNameLen && validName(s)
57 10 }
58
59 // refForbiddenByte reports whether c is a byte git-check-ref-format forbids
60 // anywhere in a ref: ASCII control characters, space, DEL, and the special
61 // set ~ ^ : ? * [ \. High bytes (>= 0x80) are allowed so UTF-8 refs pass.
62 372 func refForbiddenByte(c byte) bool {
63 372 if c <= 0x20 || c == 0x7f {
64 5 return true
65 5 }
66 367 switch c {
67 10 case '~', '^', ':', '?', '*', '[', '\\':
68 10 return true
69 357 default:
70 357 return false
71 }
72 }
73
74 // isHexSHA reports whether s is a bare 40-char (SHA-1) or 64-char (SHA-256)
75 // hexadecimal object id. These are accepted as refs directly.
76 81 func isHexSHA(s string) bool {
77 81 if len(s) != 40 && len(s) != 64 {
78 76 return false
79 76 }
80 248 for i := 0; i < len(s); i++ {
81 248 c := s[i]
82 248 if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
83 0 return false
84 0 }
85 }
86 5 return true
87 }
88
89 // ValidRef reports whether s is acceptable as a git ref or revision to hand to
90 // git. The rules follow git-check-ref-format(1) closely enough to keep hostile
91 // input (option injection, path traversal, revision-syntax tricks) away from
92 // the git command line, while still accepting ordinary multi-level branch and
93 // tag names such as "feature/foo" and bare object ids.
94 //
95 // git invocations additionally use "--end-of-options"/"--" as defense in
96 // depth; this function is the first line.
97 85 func ValidRef(s string) bool {
98 85 if s == "" {
99 4 return false
100 4 }
101 // Bare object ids are always fine and skip the component rules.
102 81 if isHexSHA(s) {
103 5 return true
104 5 }
105 // "@" alone is a git shorthand for HEAD and is not a valid ref name.
106 76 if s == "@" {
107 2 return false
108 2 }
109 74 if s[0] == '-' || s[0] == '.' || s[0] == '/' {
110 9 return false
111 9 }
112 65 if strings.HasSuffix(s, "/") || strings.HasSuffix(s, ".") {
113 2 return false
114 2 }
115 63 if strings.Contains(s, "..") || strings.Contains(s, "@{") || strings.Contains(s, "//") {
116 5 return false
117 5 }
118 372 for i := 0; i < len(s); i++ {
119 372 if refForbiddenByte(s[i]) {
120 15 return false
121 15 }
122 }
123 // Per-component rules: no component may start with '.' or end with
124 // ".lock". Empty components are already excluded by the '//', leading
125 // '/', and trailing '/' checks above.
126 57 for _, comp := range strings.Split(s, "/") {
127 57 if strings.HasPrefix(comp, ".") {
128 1 return false
129 1 }
130 56 if strings.HasSuffix(comp, ".lock") {
131 3 return false
132 3 }
133 }
134 39 return true
135 }