Something went wrong. Try again.
Maps Linux distribution source packages to their upstream repositories, and through them to each other
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209package bulk
import ( "context" "database/sql" "errors" "fmt" "strings"
"tangled.org/odd.computer/isomorph/internal/model" "tangled.org/odd.computer/isomorph/internal/repourl")
// Match is one package a query found, and how.type Match struct { Package // Via is "package" (the source package's name), "binary" (the name of one of its binary // packages) or "upstream" (its upstream repository). Via string `json:"via"`}
// Answer is what Lookup returns for one query.type Answer struct { Query string `json:"query"` Matches []Match `json:"matches"` // Counterparts are the other packages, in any distribution, that share an upstream with a // match. Counterparts []Package `json:"counterparts"`}
// Lookup answers one query:// - distro[/suite]:name, a source package of one distribution;// - name, a source package of any distribution;// - an upstream repository, as a URL or key (github.com/curl/curl).//// A name that is no source package is looked up among the binary packages (libcurl4 finds// Debian's curl). A repository is mapped through alias (LoadAliases) onto the table's keys.// Rows carry their evidence only when evidence is set.func Lookup(ctx context.Context, db *sql.DB, alias map[string]string, query string, evidence bool) (Answer, error) { a := Answer{Query: query} q := strings.TrimSpace(query) if q == "" { return a, errors.New("empty query") } var matches []Package via := "package" if isRepoQuery(q) { key, err := queryKey(q) if err != nil { return a, err } via = "upstream" if matches, err = queryPackages(ctx, db, "upstream_key = ?", Resolve(alias, key)); err != nil { return a, err } } else { name, where, args := q, "", []any{} if d, n, ok := strings.Cut(q, ":"); ok { distro, suite, _ := strings.Cut(d, "/") if !knownDistro(distro) { return a, fmt.Errorf("unknown distribution %q (want one of %v)", distro, model.AllDistros) } name, where, args = n, " and distro = ?", []any{distro} if suite != "" { where += " and suite = ?" args = append(args, suite) } } var err error if matches, err = queryPackages(ctx, db, "package = ?"+where, append([]any{name}, args...)...); err != nil { return a, err } if len(matches) == 0 { via = "binary" bin := "(distro, suite, package) in (select distro, suite, package from binaries where binary = ?" + where + ")" if matches, err = queryPackages(ctx, db, bin, append([]any{name}, args...)...); err != nil { return a, err } } } seen := map[[3]string]bool{} for _, p := range matches { seen[[3]string{string(p.Distro), p.Suite, p.Name}] = true a.Matches = append(a.Matches, Match{Package: p, Via: via}) } keys := map[string]bool{} for _, p := range matches { if p.UpstreamKey == "" || keys[p.UpstreamKey] { continue } keys[p.UpstreamKey] = true others, err := queryPackages(ctx, db, "upstream_key = ?", p.UpstreamKey) if err != nil { return a, err } for _, o := range others { if k := [3]string{string(o.Distro), o.Suite, o.Name}; !seen[k] { seen[k] = true a.Counterparts = append(a.Counterparts, o) } } } for _, ps := range [][]*Package{matchPtrs(a.Matches), pkgPtrs(a.Counterparts)} { for _, p := range ps { if err := loadBinaries(ctx, db, p); err != nil { return a, err } if evidence { if err := loadEvidence(ctx, db, p); err != nil { return a, err } } } } return a, nil}
// isRepoQuery reports whether q names a repository rather than a package: a URL, or a key// whose first segment is a host name.func isRepoQuery(q string) bool { if strings.Contains(q, "://") { return true } host, _, ok := strings.Cut(q, "/") return ok && strings.Contains(host, ".")}
// queryKey canonicalizes a repository URL or key; a release or project URL counts through the// repository it hints at.func queryKey(q string) (string, error) { u := q if !strings.Contains(u, "://") { u = "https://" + u } c, err := repourl.Parse(u) if err != nil { return "", fmt.Errorf("%q: %w", q, err) } switch { case c.Kind == repourl.KindRepo: return c.Key, nil case c.Hint != "": return c.Hint, nil } return "", fmt.Errorf("%q names no repository", q)}
func knownDistro(d string) bool { for _, k := range model.AllDistros { if string(k) == d { return true } } return false}
func queryPackages(ctx context.Context, db *sql.DB, where string, args ...any) ([]Package, error) { rows, err := db.QueryContext(ctx, `select distro, suite, package, version, coalesce(osv_ecosystem,''), coalesce(purl,''), coalesce(homepage,''), coalesce(upstream_key,''), coalesce(repo,''), status, coalesce(basis,'') from packages where `+where+` order by distro, suite, package`, args...) if err != nil { return nil, err } defer rows.Close() var out []Package for rows.Next() { var p Package if err := rows.Scan(&p.Distro, &p.Suite, &p.Name, &p.Version, &p.OSVEcosystem, &p.PURL, &p.Homepage, &p.UpstreamKey, &p.Repo, &p.Status, &p.Basis); err != nil { return nil, err } out = append(out, p) } return out, rows.Err()}
func loadBinaries(ctx context.Context, db *sql.DB, p *Package) error { rows, err := db.QueryContext(ctx, `select binary from binaries where distro = ? and suite = ? and package = ? order by rowid`, p.Distro, p.Suite, p.Name) if err != nil { return err } defer rows.Close() for rows.Next() { var b string if err := rows.Scan(&b); err != nil { return err } p.Binaries = append(p.Binaries, b) } return rows.Err()}
func matchPtrs(ms []Match) []*Package { out := make([]*Package, len(ms)) for i := range ms { out[i] = &ms[i].Package } return out}
func pkgPtrs(ps []Package) []*Package { out := make([]*Package, len(ps)) for i := range ps { out[i] = &ps[i] } return out}