Monorepo for Tangled tangled.org
1

Configure Feed

Select the types of activity you want to include in your feed.

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