summaryrefslogtreecommitdiff
path: root/internal/envfile/envfile.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/envfile/envfile.go')
-rw-r--r--internal/envfile/envfile.go90
1 files changed, 90 insertions, 0 deletions
diff --git a/internal/envfile/envfile.go b/internal/envfile/envfile.go
new file mode 100644
index 0000000..e729ab2
--- /dev/null
+++ b/internal/envfile/envfile.go
@@ -0,0 +1,90 @@
+// Package envfile reads and writes the KEY='value' shell-sourceable env
+// file skiff stores at each app's /app/env - the on-disk form of `skiff
+// config`. It only ever parses the format Save itself writes, not general
+// POSIX shell syntax.
+package envfile
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strings"
+)
+
+var keyPattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
+
+// ValidKey reports whether key is a legal env var name. Save is the
+// authoritative guard and re-checks every key itself; callers can use
+// ValidKey for an earlier, pre-snapshot failure on a typo'd key.
+func ValidKey(key string) bool {
+ return keyPattern.MatchString(key)
+}
+
+// Load reads path into a KEY->value map. A missing file is not an error -
+// it's treated as an empty config, covering a fresh app and list/unset
+// against one.
+func Load(path string) (map[string]string, error) {
+ data, err := os.ReadFile(path)
+ if os.IsNotExist(err) {
+ return map[string]string{}, nil
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ kv := map[string]string{}
+ for _, line := range strings.Split(string(data), "\n") {
+ if line == "" {
+ continue
+ }
+ eq := strings.IndexByte(line, '=')
+ if eq < 0 {
+ continue
+ }
+ key := line[:eq]
+ quoted := line[eq+1:]
+ if !strings.HasPrefix(quoted, "'") || !strings.HasSuffix(quoted, "'") {
+ continue
+ }
+ value := strings.ReplaceAll(quoted[1:len(quoted)-1], `'\''`, `'`)
+ kv[key] = value
+ }
+ return kv, nil
+}
+
+// Save validates every key and writes kv to path atomically (write to a
+// temp file, then rename over path) at mode 0600 - a deliberate departure
+// from the codebase's universal 0644, since this file holds secrets.
+func Save(path string, kv map[string]string) error {
+ keys := make([]string, 0, len(kv))
+ for key := range kv {
+ if !keyPattern.MatchString(key) {
+ return fmt.Errorf("invalid env var key %q", key)
+ }
+ keys = append(keys, key)
+ }
+ sort.Strings(keys)
+
+ var b strings.Builder
+ for _, key := range keys {
+ b.WriteString(key)
+ b.WriteByte('=')
+ b.WriteString(escape(kv[key]))
+ b.WriteByte('\n')
+ }
+
+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+ return err
+ }
+ tmp := path + ".tmp"
+ if err := os.WriteFile(tmp, []byte(b.String()), 0600); err != nil {
+ return err
+ }
+ return os.Rename(tmp, path)
+}
+
+func escape(value string) string {
+ return `'` + strings.ReplaceAll(value, `'`, `'\''`) + `'`
+}