~bigbes/sr-ht-compare

ref: 94142bd8510c87a4741b0b7f2b0f9b9471feb5ef sr-ht-compare/core/names.go -rw-r--r-- 3.8 KiB
94142bd8 — bigbes frontend: pierre diffs/trees bundle and sourcehut theme css a month ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package core

import "strings"

// maxRepoNameLen bounds repository names. Owners are bounded by meta.sr.ht at
// registration time, so ValidOwner enforces only structure, not length.
const maxRepoNameLen = 100

// isNameByte reports whether c is allowed in a sourcehut owner or repo name:
// lowercase alphanumerics plus '_', '-', and '.'. Note '/' is deliberately
// excluded, so a name can never span path components.
func isNameByte(c byte) bool {
	switch {
	case c >= 'a' && c <= 'z':
		return true
	case c >= '0' && c <= '9':
		return true
	case c == '_' || c == '-' || c == '.':
		return true
	default:
		return false
	}
}

// validName holds the rules shared by owners and repos: non-empty, drawn from
// the allowed byte set, not starting with '-' (which would look like a git or
// shell option), and containing no ".." (path traversal). '/' is rejected
// implicitly because it is not an allowed byte.
func validName(s string) bool {
	if s == "" {
		return false
	}
	if s[0] == '-' {
		return false
	}
	if strings.Contains(s, "..") {
		return false
	}
	for i := 0; i < len(s); i++ {
		if !isNameByte(s[i]) {
			return false
		}
	}
	return true
}

// ValidOwner reports whether s is a well-formed sourcehut owner name (the part
// after '~' in a URL). Callers must strip the leading '~' first.
func ValidOwner(s string) bool {
	return validName(s)
}

// ValidRepoName reports whether s is a well-formed repository name (same
// character family as an owner, capped at maxRepoNameLen).
func ValidRepoName(s string) bool {
	return len(s) <= maxRepoNameLen && validName(s)
}

// refForbiddenByte reports whether c is a byte git-check-ref-format forbids
// anywhere in a ref: ASCII control characters, space, DEL, and the special
// set ~ ^ : ? * [ \. High bytes (>= 0x80) are allowed so UTF-8 refs pass.
func refForbiddenByte(c byte) bool {
	if c <= 0x20 || c == 0x7f {
		return true
	}
	switch c {
	case '~', '^', ':', '?', '*', '[', '\\':
		return true
	default:
		return false
	}
}

// isHexSHA reports whether s is a bare 40-char (SHA-1) or 64-char (SHA-256)
// hexadecimal object id. These are accepted as refs directly.
func isHexSHA(s string) bool {
	if len(s) != 40 && len(s) != 64 {
		return false
	}
	for i := 0; i < len(s); i++ {
		c := s[i]
		if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
			return false
		}
	}
	return true
}

// ValidRef reports whether s is acceptable as a git ref or revision to hand to
// git. The rules follow git-check-ref-format(1) closely enough to keep hostile
// input (option injection, path traversal, revision-syntax tricks) away from
// the git command line, while still accepting ordinary multi-level branch and
// tag names such as "feature/foo" and bare object ids.
//
// git invocations additionally use "--end-of-options"/"--" as defense in
// depth; this function is the first line.
func ValidRef(s string) bool {
	if s == "" {
		return false
	}
	// Bare object ids are always fine and skip the component rules.
	if isHexSHA(s) {
		return true
	}
	// "@" alone is a git shorthand for HEAD and is not a valid ref name.
	if s == "@" {
		return false
	}
	if s[0] == '-' || s[0] == '.' || s[0] == '/' {
		return false
	}
	if strings.HasSuffix(s, "/") || strings.HasSuffix(s, ".") {
		return false
	}
	if strings.Contains(s, "..") || strings.Contains(s, "@{") || strings.Contains(s, "//") {
		return false
	}
	for i := 0; i < len(s); i++ {
		if refForbiddenByte(s[i]) {
			return false
		}
	}
	// Per-component rules: no component may start with '.' or end with
	// ".lock". Empty components are already excluded by the '//', leading
	// '/', and trailing '/' checks above.
	for _, comp := range strings.Split(s, "/") {
		if strings.HasPrefix(comp, ".") {
			return false
		}
		if strings.HasSuffix(comp, ".lock") {
			return false
		}
	}
	return true
}