Monorepo for Tangled
tangled.org
1package microvm
2
3import (
4 "bufio"
5 "fmt"
6 "io"
7 "path/filepath"
8 "regexp"
9 "strconv"
10 "strings"
11)
12
13type narinfo struct {
14 StorePath string
15 URL string
16 NarHash string
17 NarSize int64
18 // paths this path depends on
19 References []string
20}
21
22const (
23 maxNarinfoSize = 1 << 20 // 1 MiB
24 storePrefix = "/nix/store/"
25 maxNarinfoLineLen = maxNarinfoSize
26)
27
28var nixStorePathBaseRe = regexp.MustCompile(`^[0-9abcdfghijklmnpqrsvwxyz]{32}-[^/]+$`)
29
30// parseNarinfo parses and validates a narinfo body.
31// - required fields must be present
32// - StorePath must be under /nix/store/
33// - URL must be a relative, traversal-safe path referencing a NAR in the
34// same staging cache
35// - NarSize must be a non-negative integer
36func parseNarinfo(r io.Reader) (*narinfo, error) {
37 lr := io.LimitReader(r, maxNarinfoSize+1)
38 scanner := bufio.NewScanner(lr)
39 scanner.Buffer(make([]byte, 4096), maxNarinfoLineLen)
40
41 var info narinfo
42 for scanner.Scan() {
43 line := scanner.Text()
44 if line == "" {
45 continue
46 }
47 key, value, ok := strings.Cut(line, ":")
48 if !ok {
49 return nil, fmt.Errorf("invalid narinfo line %q", line)
50 }
51 key = strings.TrimSpace(key)
52 value = strings.TrimSpace(value)
53
54 switch key {
55 case "StorePath":
56 info.StorePath = value
57 case "URL":
58 info.URL = value
59 case "NarHash":
60 info.NarHash = value
61 case "NarSize":
62 n, err := strconv.ParseInt(value, 10, 64)
63 if err != nil {
64 return nil, fmt.Errorf("invalid NarSize %q: %w", value, err)
65 }
66 info.NarSize = n
67 case "References":
68 info.References = strings.Fields(value)
69 }
70 }
71 if err := scanner.Err(); err != nil {
72 return nil, fmt.Errorf("read narinfo: %w", err)
73 }
74
75 if err := validateNarinfo(&info); err != nil {
76 return nil, err
77 }
78 return &info, nil
79}
80
81func validateNarinfo(info *narinfo) error {
82 if info.StorePath == "" {
83 return fmt.Errorf("narinfo missing StorePath")
84 }
85 if _, _, err := parseStorePath(info.StorePath); err != nil {
86 return fmt.Errorf("invalid StorePath: %w", err)
87 }
88 if info.URL == "" {
89 return fmt.Errorf("narinfo missing URL")
90 }
91 if strings.HasPrefix(info.URL, "/") || strings.Contains(info.URL, "..") {
92 return fmt.Errorf("narinfo URL %q is not a safe relative path", info.URL)
93 }
94 if !strings.HasPrefix(info.URL, "nar/") {
95 return fmt.Errorf("narinfo URL %q must reference a staged nar/ object", info.URL)
96 }
97 name := strings.TrimPrefix(info.URL, "nar/")
98 if name == "" || name == "." || name != filepath.Base(name) || strings.Contains(name, "/") {
99 return fmt.Errorf("narinfo URL %q is not a safe nar object path", info.URL)
100 }
101 if info.NarHash == "" {
102 return fmt.Errorf("narinfo missing NarHash")
103 }
104 if info.NarSize < 0 {
105 return fmt.Errorf("narinfo NarSize must be non-negative")
106 }
107 return nil
108}
109
110func parseStorePath(path string) (hash string, name string, err error) {
111 if !strings.HasPrefix(path, storePrefix) {
112 return "", "", fmt.Errorf("store path %q does not start with %q", path, storePrefix)
113 }
114
115 base := strings.TrimPrefix(path, storePrefix)
116 if base == "" || strings.Contains(base, "/") {
117 return "", "", fmt.Errorf("store path %q has invalid base name", path)
118 }
119 if !nixStorePathBaseRe.MatchString(base) {
120 return "", "", fmt.Errorf("store path %q is not a valid nix store path", path)
121 }
122
123 hash, name, ok := strings.Cut(base, "-")
124 if !ok || hash == "" || name == "" {
125 return "", "", fmt.Errorf("store path %q is missing hash or name", path)
126 }
127 return hash, name, nil
128}