package opensuse import ( "bytes" "context" "encoding/xml" "fmt" "io" "regexp" "strings" "time" "github.com/klauspost/compress/zstd" "tangled.org/odd.computer/isomorph/internal/distro" "tangled.org/odd.computer/isomorph/internal/fetch" "tangled.org/odd.computer/isomorph/internal/model" ) // Resolution starts from Tumbleweed's repository metadata rather than OBS one package at a // time: the src-oss repository's primary.xml lists every source package with its version and spec // Url:, and the oss repository's lists every binary package with the source RPM it was built // from. Both are single downloads from download.opensuse.org (redirected to a mirror). // DefaultRepoBase is the Tumbleweed repository tree. const DefaultRepoBase = "https://download.opensuse.org/tumbleweed/repo" // RPM is one package entry of a primary.xml. type RPM struct { Name string Arch string Version string // [epoch:]ver-rel URL string // SourceRPM is the source RPM file a binary was built from (empty for source packages). SourceRPM string } // Primary downloads and parses the primary metadata of repo ("src-oss", "oss"). func Primary(ctx context.Context, c *fetch.Client, base, repo string) ([]RPM, error) { if base == "" { base = DefaultRepoBase } md, err := c.Get(ctx, base+"/"+repo+"/repodata/repomd.xml", fetch.WithTTL(6*time.Hour)) if err != nil { return nil, fmt.Errorf("opensuse: %s repomd: %w", repo, err) } m := primaryHrefRe.FindSubmatch(md) if m == nil { return nil, fmt.Errorf("opensuse: %s repomd lists no primary metadata", repo) } // The file name carries its checksum, so a cached copy is valid for good. b, err := c.Get(ctx, base+"/"+repo+"/"+string(m[1]), fetch.WithTTL(30*24*time.Hour)) if err != nil { return nil, fmt.Errorf("opensuse: %s primary: %w", repo, err) } var r io.Reader = bytes.NewReader(b) if bytes.HasSuffix(m[1], []byte(".zst")) { zr, err := zstd.NewReader(r) if err != nil { return nil, err } defer zr.Close() r = zr } return ParsePrimary(r) } var primaryHrefRe = regexp.MustCompile(`[\s\S]*? // "foo-bar"): the name is everything before the last two dash-separated fields. func SourceName(srpm string) string { s := strings.TrimSuffix(srpm, ".src.rpm") s = strings.TrimSuffix(s, ".nosrc.rpm") for i := 0; i < 2; i++ { j := strings.LastIndexByte(s, '-') if j < 0 { return "" } s = s[:j] } return s } // Candidates scores what the repository metadata, giturls and, where they were fetched, the // package's OBS files say about a package: the spec Url: as a homepage, giturls entries as // corroborating git URLs, the _service's source-control URL and the spec's Git-Clone and // SourceN lines as the packaging's own statements. (Source URLs taken from the giturls debug // lines instead of the spec were tried and measured: +1% coverage, lower agreement with // Debian, mostly through archive.apache.org directories that are not repositories.) func Candidates(homepage string, giturls []string, files *OBSFiles) []distro.Candidate { var r Resolver var cands []distro.Candidate for _, u := range giturls { cands = distro.AddCandidate(cands, u, model.Evidence{Source: "zq1:giturls", Value: u, Weight: 0.7}) } if files != nil { if len(files.Service) > 0 { cands, _, _ = r.serviceCandidates(cands, files.Service) } if len(files.Spec) > 0 { spec := ParseSpec(string(files.Spec)) cands, _ = r.specCandidates(cands, spec, []*Spec{spec}) } } if homepage != "" { cands = r.addHomepage(cands, homepage, "spec:URL") } return distro.RankCandidates(cands) } // GitURLs returns giturls.good (package -> git URLs) from the default results directory. func (r *Resolver) GitURLs(ctx context.Context) (map[string][]string, error) { return r.loadGitURLs(ctx) }