1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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, `'`, `'\''`) + `'`
}
|