package 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 }