Raw
  1package config
  2
  3import (
  4	"sort"
  5	"strconv"
  6	"strings"
  7)
  8
  9const (
 10	reasonUnreadable           = "file cannot be read"
 11	reasonSyntax               = "invalid TOML syntax"
 12	reasonDuplicate            = "duplicate key or table"
 13	reasonWrongType            = "value has the wrong type"
 14	reasonUnknownKey           = "unknown key"
 15	reasonUnknownKeyCase       = "unknown key (keys are case-sensitive)"
 16	reasonRequired             = "required"
 17	reasonEmpty                = "must not be empty"
 18	reasonSocketXorListen      = "set exactly one of server.socket and server.listen"
 19	reasonAbsolutePath         = "must be a clean absolute path"
 20	reasonHostPort             = "must be an IP address and port (host:port)"
 21	reasonPort                 = "port must be a decimal number from 1 to 65535"
 22	reasonLoopbackOnly         = "must be a loopback address"
 23	reasonUnspecifiedAddress   = "must not be an unspecified address"
 24	reasonMulticastAddress     = "must not be a multicast address"
 25	reasonLoopbackAddress      = "must not be a loopback address"
 26	reasonListenerReused       = "address is already used by another listener"
 27	reasonHostname             = "must be a lowercase DNS hostname with at least two labels, without scheme, port or trailing dot"
 28	reasonDomainReused         = "domain is already used by another service domain or app"
 29	reasonAppsOnly             = "a file in apps.d holds [[app]] tables only"
 30	reasonAppsTooLarge         = "file is larger than 1 MiB"
 31	reasonAppsUnderGit         = "apps.d is read only with [config] source = \"file\": under \"git\" the commit holds every app"
 32	reasonGroupName            = "must match the group grammar ^[a-z0-9][a-z0-9_-]{0,31}$"
 33	reasonGroupDuplicate       = "group is listed more than once"
 34	reasonAdminIsPrimary       = "admin_group must differ from the primary group"
 35	reasonAdminIsDefaultGroup  = "admin_group must not be an invite default group; every invitation would grant it"
 36	reasonBaseDN               = "must be comma-separated dc=, o= or ou= components, each value matching ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"
 37	reasonTLSPair              = "set both ldap.tls_cert and ldap.tls_key, or neither"
 38	reasonDuration             = "must be a Go duration such as \"12h\" or \"15m\" (no \"d\" unit)"
 39	reasonDurationPositive     = "duration must be positive"
 40	reasonSourceValue          = "must be \"file\" or \"git\""
 41	reasonReposDistinct        = "repository must differ from the other configured repositories"
 42	reasonClientID             = "must match ^[a-z][a-z0-9_]{0,31}$ (ids starting with _ are reserved for first-party clients)"
 43	reasonClientIDDuplicate    = "client id is used more than once"
 44	reasonRedirectRequired     = "a confidential client needs at least one redirect URI"
 45	reasonRedirectURI          = "must be an absolute URI without a fragment"
 46	reasonRedirectDuplicate    = "redirect URI is listed more than once"
 47	reasonSecretsMode          = "secrets file must not be readable or writable by group or others (use mode 0600)"
 48	reasonSecretsTooLarge      = "secrets file is larger than 1 MiB"
 49	reasonSecretsBOM           = "secrets file starts with a byte order mark"
 50	reasonSecretsCRNUL         = "line contains a CR or NUL byte"
 51	reasonSecretsLineTooLong   = "line is 64 KiB or longer"
 52	reasonSecretsNotAssignment = "line is not NAME=VALUE with NAME matching ^[A-Z][A-Z0-9_]*$"
 53	reasonSecretsEmptyValue    = "value is empty"
 54	reasonSecretsQuote         = "value starts with a quote"
 55	reasonSecretsBackslash     = "value contains a backslash"
 56	reasonSecretsWhitespace    = "value starts or ends with a space or tab"
 57	reasonSecretsUTF8          = "value is not valid UTF-8"
 58	reasonSecretsControl       = "value contains a control character other than tab"
 59	reasonSecretsNoncharacter  = "value contains a Unicode noncharacter"
 60	reasonSecretsDuplicate     = "variable is set more than once"
 61	reasonSecretsMissing       = "required variable is missing"
 62	reasonSecretsUnexpected    = "variable is not expected by this configuration"
 63	reasonSecretsBase64        = "value is not valid standard base64"
 64	reasonSecretsKeyLength     = "value must decode to exactly 32 bytes"
 65	reasonSecretsAgeIdentity   = "value is not an age X25519 identity (the secret key line age-keygen writes)"
 66	reasonSecretsKeyFile       = "value is not base64 of an OpenSSH private key file"
 67	reasonSecretsKeyEncrypted  = "private key file is passphrase-protected (generate it with an empty passphrase)"
 68	reasonSecretsKeyType       = "private key must be ed25519"
 69	reasonSecretsBreakGlass    = "the recipient of this identity must differ from userbase.break_glass_recipient"
 70	reasonAgeRecipient         = "must be an age X25519 recipient (age1...)"
 71	reasonRemote               = "must be an ssh:// URL with a user, a host and a path and without a password, query or fragment, or a clean absolute path"
 72	reasonKnownHostsUnused     = "set only when repo or mirror is an ssh:// URL"
 73)
 74
 75type Problem struct {
 76	File   string
 77	Line   int
 78	Key    string
 79	Reason string
 80}
 81
 82func (p Problem) String() string {
 83	var b strings.Builder
 84	b.WriteString(p.File)
 85	if p.Line > 0 {
 86		b.WriteString(":")
 87		b.WriteString(strconv.Itoa(p.Line))
 88	}
 89	if p.Key != "" {
 90		b.WriteString(": ")
 91		b.WriteString(p.Key)
 92	}
 93	b.WriteString(": ")
 94	b.WriteString(p.Reason)
 95	return b.String()
 96}
 97
 98type Error struct {
 99	Problems []Problem
100}
101
102func (e *Error) Error() string {
103	lines := make([]string, len(e.Problems))
104	for i, p := range e.Problems {
105		lines[i] = p.String()
106	}
107	return strings.Join(lines, "\n")
108}
109
110func sortProblems(ps []Problem) {
111	sort.SliceStable(ps, func(i, j int) bool {
112		if ps[i].File != ps[j].File {
113			return ps[i].File < ps[j].File
114		}
115		if ps[i].Line != ps[j].Line {
116			return ps[i].Line < ps[j].Line
117		}
118		if ps[i].Key != ps[j].Key {
119			return ps[i].Key < ps[j].Key
120		}
121		return ps[i].Reason < ps[j].Reason
122	})
123}