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}