package rpm import ( "bytes" "compress/gzip" "crypto/sha256" "encoding/hex" "encoding/xml" "fmt" "os" "path/filepath" "strings" ) const PrimarySourceFile = ".primary-source" const localPrimaryHref = "repodata/primary.xml.gz" // filterablePackage captures location for filtering and inner XML for faithful re-emission. type filterablePackage struct { Type string `xml:"type,attr"` Location struct { Href string `xml:"href,attr"` } `xml:"http://linux.duke.edu/metadata/common location"` Inner string `xml:",innerxml"` } type filterableMetadata struct { XMLName xml.Name `xml:"http://linux.duke.edu/metadata/common metadata"` Packages []filterablePackage `xml:"http://linux.duke.edu/metadata/common package"` } // RegenerateMetadata filters the upstream primary.xml.gz to only include packages // present on disk, writes a new primary.xml.gz, and updates repomd.xml. func RegenerateMetadata(localDir string) error { sourcePath, err := upstreamPrimaryPath(localDir) if err != nil { return err } gzData, err := os.ReadFile(sourcePath) if err != nil { return fmt.Errorf("read upstream primary: %w", err) } gz, err := gzip.NewReader(bytes.NewReader(gzData)) if err != nil { return fmt.Errorf("open gzip: %w", err) } var meta filterableMetadata if err := xml.NewDecoder(gz).Decode(&meta); err != nil { gz.Close() return fmt.Errorf("parse primary.xml: %w", err) } gz.Close() var local []filterablePackage for _, pkg := range meta.Packages { dest := filepath.Join(localDir, filepath.FromSlash(pkg.Location.Href)) if _, statErr := os.Stat(dest); statErr == nil { local = append(local, pkg) } } var xmlBuf bytes.Buffer xmlBuf.WriteString("\n") fmt.Fprintf(&xmlBuf, ``, len(local)) for _, pkg := range local { fmt.Fprintf(&xmlBuf, `%s`, pkg.Type, pkg.Inner) } xmlBuf.WriteString(``) xmlBytes := xmlBuf.Bytes() var gzBuf bytes.Buffer gzw := gzip.NewWriter(&gzBuf) if _, err := gzw.Write(xmlBytes); err != nil { return fmt.Errorf("gzip write: %w", err) } if err := gzw.Close(); err != nil { return fmt.Errorf("gzip close: %w", err) } gzBytes := gzBuf.Bytes() openSum := sha256.Sum256(xmlBytes) gzSum := sha256.Sum256(gzBytes) outPath := filepath.Join(localDir, localPrimaryHref) if err := atomicWrite(outPath, gzBytes); err != nil { return fmt.Errorf("write primary.xml.gz: %w", err) } return updateRepoMD(localDir, gzSum[:], openSum[:], int64(len(gzBytes)), int64(len(xmlBytes))) } // upstreamPrimaryPath returns the full path to the original upstream primary.xml.gz, // persisting it in .primary-source so subsequent calls always filter from the full list. func upstreamPrimaryPath(localDir string) (string, error) { markerPath := filepath.Join(localDir, "repodata", PrimarySourceFile) if data, err := os.ReadFile(markerPath); err == nil { href := strings.TrimSpace(string(data)) full := filepath.Join(localDir, filepath.FromSlash(href)) if _, err := os.Stat(full); err == nil { return full, nil } } repomdData, err := os.ReadFile(filepath.Join(localDir, "repodata", "repomd.xml")) if err != nil { return "", fmt.Errorf("read repomd.xml: %w", err) } var repomd RepoMD if err := xml.Unmarshal(repomdData, &repomd); err != nil { return "", fmt.Errorf("parse repomd.xml: %w", err) } for _, entry := range repomd.Data { if entry.Type == "primary" && entry.Location.Href != localPrimaryHref { _ = os.WriteFile(markerPath, []byte(entry.Location.Href), 0o644) return filepath.Join(localDir, filepath.FromSlash(entry.Location.Href)), nil } } return "", fmt.Errorf("upstream primary source not found — repo must be cloned first") } func updateRepoMD(localDir string, gzSum, openSum []byte, gzSize, openSize int64) error { repomdPath := filepath.Join(localDir, "repodata", "repomd.xml") repomdData, err := os.ReadFile(repomdPath) if err != nil { return fmt.Errorf("read repomd.xml: %w", err) } var repomd RepoMD if err := xml.Unmarshal(repomdData, &repomd); err != nil { return fmt.Errorf("parse repomd.xml: %w", err) } for i, entry := range repomd.Data { if entry.Type == "primary" { repomd.Data[i].Location.Href = localPrimaryHref repomd.Data[i].Checksum = RepoMDChecksum{Type: "sha256", Value: hex.EncodeToString(gzSum)} repomd.Data[i].OpenChecksum = RepoMDChecksum{Type: "sha256", Value: hex.EncodeToString(openSum)} repomd.Data[i].Size = gzSize repomd.Data[i].OpenSize = openSize break } } newXML, err := xml.MarshalIndent(repomd, "", " ") if err != nil { return fmt.Errorf("marshal repomd.xml: %w", err) } return atomicWrite(repomdPath, append([]byte(xml.Header), newXML...)) } // InitEmptyRepo creates the directory structure and empty RPM metadata for a new repo. // It is idempotent: if repomd.xml already exists it is left untouched. func InitEmptyRepo(localDir string) error { for _, d := range []string{ filepath.Join(localDir, "repodata"), filepath.Join(localDir, "Packages"), } { if err := os.MkdirAll(d, 0o755); err != nil { return err } } repomdPath := filepath.Join(localDir, "repodata", "repomd.xml") if _, err := os.Stat(repomdPath); err == nil { return nil // already initialised } emptyXML := []byte("\n" + ``) var gzBuf bytes.Buffer gzw := gzip.NewWriter(&gzBuf) _, _ = gzw.Write(emptyXML) _ = gzw.Close() gzBytes := gzBuf.Bytes() openSum := sha256.Sum256(emptyXML) gzSum := sha256.Sum256(gzBytes) if err := atomicWrite(filepath.Join(localDir, localPrimaryHref), gzBytes); err != nil { return fmt.Errorf("write empty primary.xml.gz: %w", err) } repomd := RepoMD{ Data: []RepoMDEntry{{ Type: "primary", Location: RepoMDLocation{Href: localPrimaryHref}, Checksum: RepoMDChecksum{Type: "sha256", Value: hex.EncodeToString(gzSum[:])}, OpenChecksum: RepoMDChecksum{Type: "sha256", Value: hex.EncodeToString(openSum[:])}, Size: int64(len(gzBytes)), OpenSize: int64(len(emptyXML)), }}, } newXML, err := xml.MarshalIndent(repomd, "", " ") if err != nil { return err } return atomicWrite(repomdPath, append([]byte(xml.Header), newXML...)) } func atomicWrite(path string, data []byte) error { tmp := path + ".tmp" if err := os.WriteFile(tmp, data, 0o644); err != nil { return err } if err := os.Rename(tmp, path); err != nil { os.Remove(tmp) return err } return nil }