first with full handle over rpm

Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
GnomeZworc 2026-04-25 14:49:14 +02:00
commit 274ea454dd
Signed by: nicolas.boufideline
GPG key ID: 4406BBBF8845D632
50 changed files with 4309 additions and 0 deletions

View file

@ -0,0 +1,212 @@
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("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
fmt.Fprintf(&xmlBuf,
`<metadata xmlns="http://linux.duke.edu/metadata/common" xmlns:rpm="http://linux.duke.edu/metadata/rpm" packages="%d">`,
len(local))
for _, pkg := range local {
fmt.Fprintf(&xmlBuf, `<package type="%s">%s</package>`, pkg.Type, pkg.Inner)
}
xmlBuf.WriteString(`</metadata>`)
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("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
`<metadata xmlns="http://linux.duke.edu/metadata/common" xmlns:rpm="http://linux.duke.edu/metadata/rpm" packages="0"></metadata>`)
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
}