package apt import ( "bytes" "fmt" "os" "path/filepath" "strconv" ) // DebPackage represents a single stanza from a Debian Packages file. type DebPackage struct { Package string Version string Architecture string Filename string // relative path, e.g. pool/main/c/curl/curl_7.88_amd64.deb SHA256 string Size int64 Raw []byte // original stanza bytes for faithful re-emission } // ParsePackages parses all stanzas from a Debian Packages control file. // Stanzas are separated by blank lines. func ParsePackages(data []byte) ([]DebPackage, error) { // Normalise line endings. data = bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n")) var pkgs []DebPackage stanzas := bytes.Split(data, []byte("\n\n")) for _, stanza := range stanzas { stanza = bytes.TrimSpace(stanza) if len(stanza) == 0 { continue } pkg, err := parseStanza(stanza) if err != nil { return nil, fmt.Errorf("parse stanza: %w", err) } pkgs = append(pkgs, pkg) } return pkgs, nil } func parseStanza(stanza []byte) (DebPackage, error) { var pkg DebPackage pkg.Raw = stanza lines := bytes.Split(stanza, []byte("\n")) for _, rawLine := range lines { line := bytes.TrimRight(rawLine, " \t") // Continuation lines (description body etc.) start with a space — skip. if len(line) == 0 || line[0] == ' ' || line[0] == '\t' { continue } colonIdx := bytes.IndexByte(line, ':') if colonIdx < 0 { continue } key := string(bytes.TrimSpace(line[:colonIdx])) value := string(bytes.TrimSpace(line[colonIdx+1:])) switch key { case "Package": pkg.Package = value case "Version": pkg.Version = value case "Architecture": pkg.Architecture = value case "Filename": pkg.Filename = value case "SHA256": pkg.SHA256 = value case "Size": n, err := strconv.ParseInt(value, 10, 64) if err != nil { return DebPackage{}, fmt.Errorf("bad Size value %q: %w", value, err) } pkg.Size = n } } if pkg.Package == "" { return DebPackage{}, fmt.Errorf("stanza missing Package field") } return pkg, nil } // FilterPackages returns only the packages for which presentOnDisk returns true. func FilterPackages(pkgs []DebPackage, presentOnDisk func(filename string) bool) []DebPackage { var out []DebPackage for _, p := range pkgs { if presentOnDisk(p.Filename) { out = append(out, p) } } return out } // emitPackages serialises a slice of DebPackage back to Packages-file format, // using the original Raw bytes for each stanza. func emitPackages(pkgs []DebPackage) []byte { var buf bytes.Buffer for i, pkg := range pkgs { // Strip trailing whitespace from each line within the stanza. lines := bytes.Split(pkg.Raw, []byte("\n")) for j, l := range lines { lines[j] = bytes.TrimRight(l, " \t") } buf.Write(bytes.Join(lines, []byte("\n"))) if i < len(pkgs)-1 { buf.WriteString("\n\n") } else { buf.WriteString("\n") } } return buf.Bytes() } // diskPresenceChecker returns a presentOnDisk func rooted at localDir. func diskPresenceChecker(localDir string) func(string) bool { return func(filename string) bool { p := filepath.Join(localDir, filepath.FromSlash(filename)) _, err := os.Stat(p) return err == nil } }