package packages import ( "encoding/json" "fmt" "io" "strings" ) // ParsePackages reads and parses a nix-env JSON output file. func ParsePackages(r io.Reader) (map[string]*ParsedPackage, error) { var raw PackagesFile if err := json.NewDecoder(r).Decode(&raw); err != nil { return nil, fmt.Errorf("failed to decode packages JSON: %w", err) } packages := make(map[string]*ParsedPackage, len(raw)) for attrPath, pkg := range raw { parsed := &ParsedPackage{ AttrPath: attrPath, Pname: pkg.Pname, Version: pkg.Version, Description: pkg.Meta.Description, LongDescription: pkg.Meta.LongDescription, Homepage: normalizeHomepage(pkg.Meta.Homepage), License: normalizeLicense(pkg.Meta.License), Platforms: normalizePlatforms(pkg.Meta.Platforms), Maintainers: normalizeMaintainers(pkg.Meta.Maintainers), Broken: pkg.Meta.Broken, Unfree: pkg.Meta.Unfree, Insecure: pkg.Meta.Insecure, } packages[attrPath] = parsed } return packages, nil } // normalizeHomepage converts homepage to a string. func normalizeHomepage(v interface{}) string { if v == nil { return "" } switch hp := v.(type) { case string: return hp case []interface{}: if len(hp) > 0 { if s, ok := hp[0].(string); ok { return s } } } return "" } // normalizeLicense converts license to a JSON array string. func normalizeLicense(v interface{}) string { if v == nil { return "[]" } licenses := make([]string, 0) switch l := v.(type) { case string: licenses = append(licenses, l) case map[string]interface{}: // Single license object if spdxID, ok := l["spdxId"].(string); ok { licenses = append(licenses, spdxID) } else if fullName, ok := l["fullName"].(string); ok { licenses = append(licenses, fullName) } else if shortName, ok := l["shortName"].(string); ok { licenses = append(licenses, shortName) } case []interface{}: for _, item := range l { switch li := item.(type) { case string: licenses = append(licenses, li) case map[string]interface{}: if spdxID, ok := li["spdxId"].(string); ok { licenses = append(licenses, spdxID) } else if fullName, ok := li["fullName"].(string); ok { licenses = append(licenses, fullName) } else if shortName, ok := li["shortName"].(string); ok { licenses = append(licenses, shortName) } } } } data, _ := json.Marshal(licenses) return string(data) } // normalizePlatforms converts platforms to a JSON array string. func normalizePlatforms(v []interface{}) string { if v == nil { return "[]" } platforms := make([]string, 0, len(v)) for _, p := range v { switch pv := p.(type) { case string: platforms = append(platforms, pv) // Skip complex platform specs (objects) } } data, _ := json.Marshal(platforms) return string(data) } // normalizeMaintainers converts maintainers to a JSON array string. func normalizeMaintainers(maintainers []Maintainer) string { if len(maintainers) == 0 { return "[]" } names := make([]string, 0, len(maintainers)) for _, m := range maintainers { name := m.Name if name == "" && m.Github != "" { name = "@" + m.Github } if name != "" { names = append(names, name) } } data, _ := json.Marshal(names) return string(data) } // ParsePackagesStream parses packages from a reader using streaming to reduce memory usage. // It yields parsed packages through a callback function. func ParsePackagesStream(r io.Reader, callback func(*ParsedPackage) error) (int, error) { dec := json.NewDecoder(r) // Read the opening brace t, err := dec.Token() if err != nil { return 0, fmt.Errorf("failed to read opening token: %w", err) } if delim, ok := t.(json.Delim); !ok || delim != '{' { return 0, fmt.Errorf("expected opening brace, got %v", t) } count := 0 for dec.More() { // Read the key (attr path) t, err := dec.Token() if err != nil { return count, fmt.Errorf("failed to read attr path: %w", err) } attrPath, ok := t.(string) if !ok { return count, fmt.Errorf("expected string key, got %T", t) } // Read the value (package) var pkg RawPackage if err := dec.Decode(&pkg); err != nil { // Skip malformed packages continue } parsed := &ParsedPackage{ AttrPath: attrPath, Pname: pkg.Pname, Version: pkg.Version, Description: pkg.Meta.Description, LongDescription: pkg.Meta.LongDescription, Homepage: normalizeHomepage(pkg.Meta.Homepage), License: normalizeLicense(pkg.Meta.License), Platforms: normalizePlatforms(pkg.Meta.Platforms), Maintainers: normalizeMaintainers(pkg.Meta.Maintainers), Broken: pkg.Meta.Broken, Unfree: pkg.Meta.Unfree, Insecure: pkg.Meta.Insecure, } if err := callback(parsed); err != nil { return count, fmt.Errorf("callback error for %s: %w", attrPath, err) } count++ } return count, nil } // SplitAttrPath splits an attribute path into its components. // For example, "python312Packages.requests" returns ["python312Packages", "requests"]. func SplitAttrPath(attrPath string) []string { return strings.Split(attrPath, ".") }