package config import ( "sort" "strconv" "strings" ) const ( reasonUnreadable = "file cannot be read" reasonSyntax = "invalid TOML syntax" reasonDuplicate = "duplicate key or table" reasonWrongType = "value has the wrong type" reasonUnknownKey = "unknown key" reasonUnknownKeyCase = "unknown key (keys are case-sensitive)" reasonRequired = "required" reasonEmpty = "must not be empty" reasonSocketXorListen = "set exactly one of server.socket and server.listen" reasonAbsolutePath = "must be a clean absolute path" reasonHostPort = "must be an IP address and port (host:port)" reasonPort = "port must be a decimal number from 1 to 65535" reasonLoopbackOnly = "must be a loopback address" reasonUnspecifiedAddress = "must not be an unspecified address" reasonMulticastAddress = "must not be a multicast address" reasonLoopbackAddress = "must not be a loopback address" reasonListenerReused = "address is already used by another listener" reasonHostname = "must be a lowercase DNS hostname with at least two labels, without scheme, port or trailing dot" reasonDomainReused = "domain is already used by another service domain or app" reasonAppsOnly = "a file in apps.d holds [[app]] tables only" reasonAppsTooLarge = "file is larger than 1 MiB" reasonAppsUnderGit = "apps.d is read only with [config] source = \"file\": under \"git\" the commit holds every app" reasonGroupName = "must match the group grammar ^[a-z0-9][a-z0-9_-]{0,31}$" reasonGroupDuplicate = "group is listed more than once" reasonAdminIsPrimary = "admin_group must differ from the primary group" reasonAdminIsDefaultGroup = "admin_group must not be an invite default group; every invitation would grant it" reasonBaseDN = "must be comma-separated dc=, o= or ou= components, each value matching ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$" reasonTLSPair = "set both ldap.tls_cert and ldap.tls_key, or neither" reasonDuration = "must be a Go duration such as \"12h\" or \"15m\" (no \"d\" unit)" reasonDurationPositive = "duration must be positive" reasonSourceValue = "must be \"file\" or \"git\"" reasonReposDistinct = "repository must differ from the other configured repositories" reasonClientID = "must match ^[a-z][a-z0-9_]{0,31}$ (ids starting with _ are reserved for first-party clients)" reasonClientIDDuplicate = "client id is used more than once" reasonRedirectRequired = "a confidential client needs at least one redirect URI" reasonRedirectURI = "must be an absolute URI without a fragment" reasonRedirectDuplicate = "redirect URI is listed more than once" reasonSecretsMode = "secrets file must not be readable or writable by group or others (use mode 0600)" reasonSecretsTooLarge = "secrets file is larger than 1 MiB" reasonSecretsBOM = "secrets file starts with a byte order mark" reasonSecretsCRNUL = "line contains a CR or NUL byte" reasonSecretsLineTooLong = "line is 64 KiB or longer" reasonSecretsNotAssignment = "line is not NAME=VALUE with NAME matching ^[A-Z][A-Z0-9_]*$" reasonSecretsEmptyValue = "value is empty" reasonSecretsQuote = "value starts with a quote" reasonSecretsBackslash = "value contains a backslash" reasonSecretsWhitespace = "value starts or ends with a space or tab" reasonSecretsUTF8 = "value is not valid UTF-8" reasonSecretsControl = "value contains a control character other than tab" reasonSecretsNoncharacter = "value contains a Unicode noncharacter" reasonSecretsDuplicate = "variable is set more than once" reasonSecretsMissing = "required variable is missing" reasonSecretsUnexpected = "variable is not expected by this configuration" reasonSecretsBase64 = "value is not valid standard base64" reasonSecretsKeyLength = "value must decode to exactly 32 bytes" reasonSecretsAgeIdentity = "value is not an age X25519 identity (the secret key line age-keygen writes)" reasonSecretsKeyFile = "value is not base64 of an OpenSSH private key file" reasonSecretsKeyEncrypted = "private key file is passphrase-protected (generate it with an empty passphrase)" reasonSecretsKeyType = "private key must be ed25519" reasonSecretsBreakGlass = "the recipient of this identity must differ from userbase.break_glass_recipient" reasonAgeRecipient = "must be an age X25519 recipient (age1...)" 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" reasonKnownHostsUnused = "set only when repo or mirror is an ssh:// URL" ) type Problem struct { File string Line int Key string Reason string } func (p Problem) String() string { var b strings.Builder b.WriteString(p.File) if p.Line > 0 { b.WriteString(":") b.WriteString(strconv.Itoa(p.Line)) } if p.Key != "" { b.WriteString(": ") b.WriteString(p.Key) } b.WriteString(": ") b.WriteString(p.Reason) return b.String() } type Error struct { Problems []Problem } func (e *Error) Error() string { lines := make([]string, len(e.Problems)) for i, p := range e.Problems { lines[i] = p.String() } return strings.Join(lines, "\n") } func sortProblems(ps []Problem) { sort.SliceStable(ps, func(i, j int) bool { if ps[i].File != ps[j].File { return ps[i].File < ps[j].File } if ps[i].Line != ps[j].Line { return ps[i].Line < ps[j].Line } if ps[i].Key != ps[j].Key { return ps[i].Key < ps[j].Key } return ps[i].Reason < ps[j].Reason }) }