From 93176aa4799f71a9cc891a218ff335f09fb581ca Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Wed, 16 Sep 2026 01:30:25 +0200 Subject: [PATCH] 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. --- src/debian/deps.rs | 62 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/src/debian/deps.rs b/src/debian/deps.rs index 481c3a4..7dd90e1 100644 --- a/src/debian/deps.rs +++ b/src/debian/deps.rs @@ -187,12 +187,16 @@ pub fn parse_simple(dep: &str, build_dep: bool) -> Result { let constraint = match (caps.get(5), caps.get(6)) { (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() { - "<<" | "<" => Relation::Lt, - "<=" => Relation::Le, + "<<" => Relation::Lt, + "<" | "<=" => Relation::Le, "=" => Relation::Eq, - ">=" => Relation::Ge, - ">>" | ">" => Relation::Gt, + ">" | ">=" => Relation::Ge, + ">>" => Relation::Gt, other => return Err(format!("invalid relation '{other}' in '{dep}'")), }; let version = DebianVersion::parse(version.as_str()) @@ -1024,6 +1028,56 @@ mod tests { 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. #[test] fn arch_reduction() {