Introduce the single Backend interface every credential source implements.
v0 ships two working backends: file (0600 KEY=VALUE env files, parsed in
pure Go, never sourced; looser permission masks refused before read) and
env (process-environment indirection). Bitwarden Secrets Manager and
HashiCorp Vault ship as explicit not-implemented stubs behind the same
interface so the phase-3 connectors are drop-ins. All failures are typed
ResolveErrors carrying only the ref, backend, and a fixed reason enum —
never material.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
103 lines
2.8 KiB
Go
103 lines
2.8 KiB
Go
package backend
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// parseEnvFile parses KEY=VALUE lines in pure Go. Env files are NEVER
|
|
// sourced or eval'd (house rule: no exec of env files); there is no
|
|
// shell expansion, no command substitution, no interpolation of any
|
|
// kind. Accepted syntax per line:
|
|
//
|
|
// # comment skipped (also inline comments after whitespace)
|
|
// (blank line) skipped
|
|
// export KEY=VALUE optional "export " prefix, skipped
|
|
// KEY=VALUE KEY is [A-Za-z_][A-Za-z0-9_]*; VALUE is the rest
|
|
// of the line, trimmed; one matched pair of
|
|
// surrounding single or double quotes is stripped
|
|
// (no escape processing)
|
|
//
|
|
// A later duplicate KEY overrides an earlier one (same order semantics
|
|
// as sourcing the file). Malformed lines fail loudly: the error carries
|
|
// the line NUMBER only, never the line contents (contents may be
|
|
// material).
|
|
func parseEnvFile(data []byte) (map[string]string, error) {
|
|
out := map[string]string{}
|
|
for i, line := range strings.Split(string(data), "\n") {
|
|
line = strings.TrimRight(line, "\r")
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
|
continue
|
|
}
|
|
line = trimmed
|
|
if strings.HasPrefix(line, "export ") || strings.HasPrefix(line, "export\t") {
|
|
line = strings.TrimSpace(line[len("export"):])
|
|
}
|
|
eq := strings.IndexByte(line, '=')
|
|
if eq <= 0 {
|
|
return nil, fmt.Errorf("line %d: malformed KEY=VALUE line", i+1)
|
|
}
|
|
key := strings.TrimSpace(line[:eq])
|
|
if !validEnvKey(key) {
|
|
return nil, fmt.Errorf("line %d: malformed KEY=VALUE line", i+1)
|
|
}
|
|
value := strings.TrimSpace(line[eq+1:])
|
|
if idx := commentIndex(value); idx >= 0 {
|
|
value = strings.TrimSpace(value[:idx])
|
|
}
|
|
out[key] = unquote(value)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// validEnvKey reports whether key is a legal env-file identifier.
|
|
func validEnvKey(key string) bool {
|
|
if key == "" {
|
|
return false
|
|
}
|
|
for i := 0; i < len(key); i++ {
|
|
c := key[i]
|
|
switch {
|
|
case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c == '_':
|
|
case c >= '0' && c <= '9':
|
|
if i == 0 {
|
|
return false
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// commentIndex finds an inline comment start (a # preceded by whitespace)
|
|
// OUTSIDE a quoted value; -1 if none.
|
|
func commentIndex(value string) int {
|
|
var quote byte
|
|
for i := 0; i < len(value); i++ {
|
|
c := value[i]
|
|
switch {
|
|
case quote != 0:
|
|
if c == quote {
|
|
quote = 0
|
|
}
|
|
case c == '"' || c == '\'':
|
|
quote = c
|
|
case c == '#' && (i == 0 || value[i-1] == ' ' || value[i-1] == '\t'):
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// unquote strips one matched pair of surrounding quotes.
|
|
func unquote(v string) string {
|
|
if len(v) >= 2 {
|
|
if (v[0] == '"' && v[len(v)-1] == '"') || (v[0] == '\'' && v[len(v)-1] == '\'') {
|
|
return v[1 : len(v)-1]
|
|
}
|
|
}
|
|
return v
|
|
}
|