deps: give legacy < and > relations their documented dpkg semantics

Debian Policy 7 defines the deprecated single-character spellings as
'earlier/later or equal' (i.e. <= and >=), and dpkg still accepts them
that way; the parser mapped them to the strict << and >> instead, so
'foo (< 1.0)' was wrongly reported unmet against installed 1.0.
This commit is contained in:
2026-09-16 01:30:25 +02:00
parent 60ee99adc8
commit 93176aa479
+58 -4
View File
@@ -187,12 +187,16 @@ pub fn parse_simple(dep: &str, build_dep: bool) -> Result<PkgRelation, String> {
let constraint = match (caps.get(5), caps.get(6)) { let constraint = match (caps.get(5), caps.get(6)) {
(Some(op), Some(version)) => { (Some(op), Some(version)) => {
// The deprecated single-character spellings `<` and `>` were
// "confusingly defined" (Debian Policy §7) to mean earlier-or-equal
// and later-or-equal, i.e. `<=` and `>=`; dpkg still accepts them
// with those non-strict semantics.
let relation = match op.as_str() { let relation = match op.as_str() {
"<<" | "<" => Relation::Lt, "<<" => Relation::Lt,
"<=" => Relation::Le, "<" | "<=" => Relation::Le,
"=" => Relation::Eq, "=" => Relation::Eq,
">=" => Relation::Ge, ">" | ">=" => Relation::Ge,
">>" | ">" => Relation::Gt, ">>" => Relation::Gt,
other => return Err(format!("invalid relation '{other}' in '{dep}'")), other => return Err(format!("invalid relation '{other}' in '{dep}'")),
}; };
let version = DebianVersion::parse(version.as_str()) let version = DebianVersion::parse(version.as_str())
@@ -1024,6 +1028,56 @@ mod tests {
assert!(Deps::parse("foo:native", &opts("amd64", &[])).is_ok()); assert!(Deps::parse("foo:native", &opts("amd64", &[])).is_ok());
} }
/// The deprecated single-character operators `<` and `>` mean `<=` and
/// `>=` (Debian Policy §7, dpkg behavior), and canonicalize on output.
#[test]
fn legacy_single_char_relations() {
let rel = |dep: &str| {
parse_simple(dep, true)
.unwrap()
.constraint
.unwrap()
.relation
};
assert_eq!(rel("foo (<< 1.0)"), Relation::Lt);
assert_eq!(rel("foo (< 1.0)"), Relation::Le);
assert_eq!(rel("foo (<= 1.0)"), Relation::Le);
assert_eq!(rel("foo (= 1.0)"), Relation::Eq);
assert_eq!(rel("foo (>= 1.0)"), Relation::Ge);
assert_eq!(rel("foo (> 1.0)"), Relation::Ge);
assert_eq!(rel("foo (>> 1.0)"), Relation::Gt);
// Rendering always uses the canonical modern spellings.
for (dep, rendered) in [
("foo (< 1.0)", "foo (<= 1.0)"),
("foo (> 1.0)", "foo (>= 1.0)"),
("foo (<< 1.0)", "foo (<< 1.0)"),
("foo (>> 1.0)", "foo (>> 1.0)"),
] {
assert_eq!(parse_simple(dep, true).unwrap().output(), rendered);
}
}
/// `foo (< 1.0)` is satisfied by an installed `foo 1.0` (the legacy
/// operator is non-strict); `foo (< 0.9)` is not.
#[test]
fn legacy_single_char_evaluation() {
let mut facts = Facts::new("amd64", "amd64");
facts.add_installed("foo", "1.0", "amd64", "no");
let o = |s: &str| parse_simple(s, true).unwrap();
assert_eq!(facts.evaluate_relation(&o("foo (< 1.0)")), Some(true));
assert_eq!(facts.evaluate_relation(&o("foo (<= 1.0)")), Some(true));
assert_eq!(facts.evaluate_relation(&o("foo (< 0.9)")), Some(false));
assert_eq!(facts.evaluate_relation(&o("foo (<< 1.0)")), Some(false));
assert_eq!(facts.evaluate_relation(&o("foo (> 1.0)")), Some(true));
assert_eq!(facts.evaluate_relation(&o("foo (>= 1.0)")), Some(true));
assert_eq!(facts.evaluate_relation(&o("foo (> 1.1)")), Some(false));
assert_eq!(facts.evaluate_relation(&o("foo (>> 1.0)")), Some(false));
}
/// Ported from dpkg `t/Dpkg_Deps.t`: architecture reduction. /// Ported from dpkg `t/Dpkg_Deps.t`: architecture reduction.
#[test] #[test]
fn arch_reduction() { fn arch_reduction() {