Monorepo for Tangled tangled.org
8

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} 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}