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