Monorepo for Tangled tangled.org
2

Configure Feed

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

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}