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 }