deps: treat corrupt Provides as undecidable, not unmet
A versioned Provides whose version failed to parse was silently skipped, so a corrupt dpkg status entry could yield a wrong 'unmet' verdict where the truth is 'cannot decide': unparseable provided versions now set lackinfos like unparseable installed versions do. Provides alternatives with a non-= constraint are likewise rejected as a whole field (dpkg rejects the entry), replacing the skip-per- alternative behavior that contradicted the code's own comment.
This commit is contained in:
+144
-17
@@ -10,7 +10,7 @@
|
||||
//! (<https://manpages.debian.org/libdpkg-perl>) and were validated
|
||||
//! differentially against the real tool.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
@@ -583,6 +583,24 @@ pub struct Facts {
|
||||
build_arch: String,
|
||||
installed: HashMap<String, Vec<InstalledPkg>>,
|
||||
provided: HashMap<String, Vec<ProvidedPkg>>,
|
||||
/// Virtual packages whose `Provides` entries had to be rejected as a
|
||||
/// whole (unparseable field, malformed version, or a relation other
|
||||
/// than `=`), mirroring a dpkg rejection of the entry: versioned
|
||||
/// relations on them stay undecidable instead of turning unmet.
|
||||
unreadable_provides: HashSet<String>,
|
||||
}
|
||||
|
||||
/// Best-effort virtual package names mentioned in a raw `Provides` value,
|
||||
/// used to remember rejected entries whose content cannot even be parsed
|
||||
/// (the full dependency grammar is deliberately not re-applied here).
|
||||
fn raw_provides_names(field: &str) -> impl Iterator<Item = &str> {
|
||||
field
|
||||
.split([',', '|'])
|
||||
.filter_map(|alt| alt.split_whitespace().next())
|
||||
.map(|tok| match tok.split_once(':') {
|
||||
Some((name, _qual)) => name,
|
||||
None => tok,
|
||||
})
|
||||
}
|
||||
|
||||
impl Facts {
|
||||
@@ -593,6 +611,7 @@ impl Facts {
|
||||
build_arch: build_arch.to_string(),
|
||||
installed: HashMap::new(),
|
||||
provided: HashMap::new(),
|
||||
unreadable_provides: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,6 +628,11 @@ impl Facts {
|
||||
}
|
||||
|
||||
/// Record that `provider` provides the virtual package `virtual_name`.
|
||||
///
|
||||
/// A `relation` other than [`Relation::Eq`], or a `version` that fails
|
||||
/// to parse, makes the provide unusable: versioned relations on
|
||||
/// `virtual_name` then evaluate to `None` (undecidable) instead of a
|
||||
/// possibly-wrong `Some(false)`.
|
||||
pub fn add_provided(
|
||||
&mut self,
|
||||
virtual_name: &str,
|
||||
@@ -639,7 +663,10 @@ impl Facts {
|
||||
/// Only stanzas whose `Status` ends with `ok installed` participate;
|
||||
/// their `Provides` field registers versioned/unversioned virtual
|
||||
/// packages (architecture-restricted provides are reduced against
|
||||
/// `host_arch`).
|
||||
/// `host_arch`). A `Provides` field that fails to parse, or that
|
||||
/// carries a relation other than `=`, is rejected as a whole (like
|
||||
/// dpkg rejects the entry) and its virtual names are remembered as
|
||||
/// unreadable.
|
||||
pub fn from_status(content: &str, host_arch: &str, build_arch: &str) -> Facts {
|
||||
let mut facts = Facts::new(host_arch, build_arch);
|
||||
for para in crate::debian::control::parse_paragraphs(content) {
|
||||
@@ -664,20 +691,38 @@ impl Facts {
|
||||
union: true,
|
||||
build_dep: false,
|
||||
};
|
||||
// Virtual (Provides) fields only accept '=' relations; a
|
||||
// parse failure skips the whole field, like dpkg does.
|
||||
let Ok(parsed) = Deps::parse_inner(provides, &opts, true) else {
|
||||
// Virtual (Provides) fields only accept versionless or
|
||||
// exactly '='-versioned alternatives; a field that fails
|
||||
// to parse, or that carries any other relation, is
|
||||
// rejected as a whole, like dpkg rejects the entry. The
|
||||
// mentioned virtual names are remembered so versioned
|
||||
// relations on them stay undecidable instead of turning
|
||||
// into a possibly-wrong "unmet" verdict.
|
||||
let mut parsed = None;
|
||||
let mut rejected: Vec<String> = Vec::new();
|
||||
match Deps::parse_inner(provides, &opts, true) {
|
||||
Ok(deps) => {
|
||||
if deps.clauses().flatten().any(|alt| {
|
||||
alt.constraint
|
||||
.as_ref()
|
||||
.is_some_and(|c| c.relation != Relation::Eq)
|
||||
}) {
|
||||
rejected
|
||||
.extend(deps.clauses().flatten().map(|alt| alt.package.clone()));
|
||||
} else {
|
||||
parsed = Some(deps);
|
||||
}
|
||||
}
|
||||
// The field could not even be parsed: recover the raw
|
||||
// names so the rejection is still remembered.
|
||||
Err(_) => rejected.extend(raw_provides_names(provides).map(str::to_string)),
|
||||
}
|
||||
facts.unreadable_provides.extend(rejected);
|
||||
let Some(parsed) = parsed else {
|
||||
continue;
|
||||
};
|
||||
for clause in parsed.clauses() {
|
||||
for alt in clause {
|
||||
if alt
|
||||
.constraint
|
||||
.as_ref()
|
||||
.is_some_and(|c| c.relation != Relation::Eq)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let provided_version = alt
|
||||
.constraint
|
||||
.as_ref()
|
||||
@@ -755,11 +800,21 @@ impl Facts {
|
||||
}
|
||||
}
|
||||
|
||||
// A rejected Provides entry for this virtual package carries
|
||||
// information that could not be read: a versioned relation can
|
||||
// then not be decided (an unversioned one only needs the name,
|
||||
// which the rejection removed).
|
||||
if rel.constraint.is_some() && self.unreadable_provides.contains(&rel.package) {
|
||||
lackinfos = true;
|
||||
}
|
||||
|
||||
if let Some(providers) = self.provided.get(&rel.package) {
|
||||
for vp in providers {
|
||||
// Only unversioned provides and strictly-versioned provides
|
||||
// can satisfy a dependency.
|
||||
// Only unversioned provides and exactly-versioned provides
|
||||
// can satisfy a dependency; anything else is an invalid
|
||||
// provide and leaves the relation undecidable.
|
||||
if vp.relation.is_some_and(|r| r != Relation::Eq) {
|
||||
lackinfos = true;
|
||||
continue;
|
||||
}
|
||||
match &rel.constraint {
|
||||
@@ -767,11 +822,16 @@ impl Facts {
|
||||
let Some(vp_version) = &vp.version else {
|
||||
continue;
|
||||
};
|
||||
if let Ok(vp_v) = DebianVersion::parse(vp_version)
|
||||
&& constraint.relation.eval(&vp_v, &constraint.version)
|
||||
{
|
||||
match DebianVersion::parse(vp_version) {
|
||||
Ok(vp_v) if constraint.relation.eval(&vp_v, &constraint.version) => {
|
||||
return Some(true);
|
||||
}
|
||||
// An unreadable provided version, like an
|
||||
// unreadable installed version, makes the
|
||||
// relation undecidable instead of unmet.
|
||||
Err(_) => lackinfos = true,
|
||||
Ok(_) => {}
|
||||
}
|
||||
}
|
||||
None => return Some(true),
|
||||
}
|
||||
@@ -1251,6 +1311,73 @@ Provides: old-virtual (= 0.5)
|
||||
assert_eq!(facts.evaluate_relation(&o("old-virtual")), Some(true));
|
||||
}
|
||||
|
||||
/// A `Provides` entry that dpkg would reject as a whole (a malformed
|
||||
/// provided version, or a relation other than `=`) must not degrade to
|
||||
/// a bogus "unmet" verdict: versioned relations on the affected virtual
|
||||
/// names stay undecidable.
|
||||
#[test]
|
||||
fn corrupt_provides_are_undecidable_not_unmet() {
|
||||
// `not-a-version!` is rejected by DebianVersion::parse ('!' is not
|
||||
// a legal version character), unlike dpkg-invalid but here-valid
|
||||
// letter-only spellings.
|
||||
let status = "\
|
||||
Package: bad-version-provider
|
||||
Status: install ok installed
|
||||
Version: 1.0
|
||||
Architecture: amd64
|
||||
Provides: virt (= not-a-version!)
|
||||
|
||||
Package: bad-relation-provider
|
||||
Status: install ok installed
|
||||
Version: 1.0
|
||||
Architecture: amd64
|
||||
Provides: virt2 (>= 1.0), plain
|
||||
";
|
||||
let facts = Facts::from_status(status, "amd64", "amd64");
|
||||
let o = |s: &str| parse_simple(s, true).unwrap();
|
||||
|
||||
// The provided version fails to parse: undecidable, not unmet.
|
||||
assert_eq!(facts.evaluate_relation(&o("virt (>= 0.5)")), None);
|
||||
// A non-'=' relation invalidates the whole Provides field, so
|
||||
// neither of its alternatives may produce a verdict.
|
||||
assert_eq!(facts.evaluate_relation(&o("virt2 (>= 0.5)")), None);
|
||||
assert_eq!(facts.evaluate_relation(&o("plain (>= 0.5)")), None);
|
||||
// Unversioned relations only need the name, which the rejected
|
||||
// entries no longer provide: genuinely unmet.
|
||||
assert_eq!(facts.evaluate_relation(&o("virt")), Some(false));
|
||||
assert_eq!(facts.evaluate_relation(&o("virt2")), Some(false));
|
||||
assert_eq!(facts.evaluate_relation(&o("plain")), Some(false));
|
||||
}
|
||||
|
||||
/// The same undecidable verdicts through the direct facts API: an
|
||||
/// unreadable provided version and an invalid (non-`=`) provide each
|
||||
/// leave a versioned relation undecided, while a readable provider
|
||||
/// elsewhere still satisfies it.
|
||||
#[test]
|
||||
fn unreadable_provided_version_and_relation_are_undecidable() {
|
||||
let o = |s: &str| parse_simple(s, true).unwrap();
|
||||
|
||||
let mut facts = Facts::new("amd64", "amd64");
|
||||
facts.add_installed("provider", "1.0", "amd64", "no");
|
||||
// `virt (= not-a-version!)`: the version string fails to parse.
|
||||
facts.add_provided(
|
||||
"virt",
|
||||
Some(Relation::Eq),
|
||||
Some("not-a-version!"),
|
||||
"provider",
|
||||
);
|
||||
assert_eq!(facts.evaluate_relation(&o("virt (>= 0.5)")), None);
|
||||
// `virt2 (>= 1.0)`: a non-'=' provide is invalid.
|
||||
facts.add_provided("virt2", Some(Relation::Ge), Some("1.0"), "provider");
|
||||
assert_eq!(facts.evaluate_relation(&o("virt2 (>= 0.5)")), None);
|
||||
|
||||
// A readable provider decides the relation when it matches;
|
||||
// otherwise the unreadable entry keeps it undecidable.
|
||||
facts.add_provided("virt", Some(Relation::Eq), Some("2.0"), "better");
|
||||
assert_eq!(facts.evaluate_relation(&o("virt (>= 0.5)")), Some(true));
|
||||
assert_eq!(facts.evaluate_relation(&o("virt (>> 2.0)")), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simplify_reports_unmet() {
|
||||
let facts = Facts::from_status(STATUS, "amd64", "amd64");
|
||||
|
||||
Reference in New Issue
Block a user