use lazy_static::lazy_static;
use regex::Regex;
use std::fmt;
use std::path::Path;
use std::error::Error;
use crate::models::PackageFormat;
use crate::rpm_requires::{parse_rpm_requires, parse_package};
use crate::conda_requires::parse_conda_requires;
* Design and Rules for Parsing Package Requirements
*
* This implementation normalizes package requirements from various package managers
* (RPM, DEB, Arch Linux, Python, Conda) into a consistent hierarchical structure.
* The goal is to represent dependencies in a unified way, making it easier to
* compare, analyze, or process them programmatically.
*
* ### Normalized Data Structure
*
* The output is structured as follows:
*
* 1. `and_depends`: A list of `or_depends` (logical AND groups).
* - Each `or_depends` represents a group of dependencies that must be satisfied together.
* - and_depends := [or_depends1, or_depends2, or_depends3, ...]
*
* 2. `or_depends`: A list of `pkg_depend` (logical OR groups).
* - Each `pkg_depend` represents an alternative dependency that can satisfy the requirement.
* - or_depends := [pkg_depend1, pkg_depend2, ...]
*
* 3. `pkg_depend`: A tuple of `capability` and `constraints`.
* - `capability`: A string representing the package name, filename, or module name.
* - capability := String of pkgname | filename | modulename | ...
* - `constraints`: A list of `version_constraint` (version or conditional requirements).
* - pkg_depend := (capability, [version_constraint1, version_constraint2, ...])
*
* 4. `version_constraint`: A tuple of `operator` and `operand`.
* - `operator`: An enum representing the relationship (e.g., `VersionGreaterThan`, `VersionGreaterThanEqual`, `IfInstall`).
* - operator := Enum {
* IfInstall, // Dependency is required if the operand capability is installed.
* VersionGreaterThan, // Version must be greater than the operand.
* VersionGreaterThanEqual, // Version must be greater than or equal to the operand.
* VersionEqual, // Version must be equal to the operand.
* VersionLessThan, // Version must be less than the operand.
* VersionLessThanEqual, // Version must be less than or equal to the operand.
* VersionNotEqual, // Version must not be equal to the operand.
* ...
* }
* - `operand`: A string representing the version or condition.
* - operand := String of version | capability (when operator=IfInstall)
*
* ### Rules for Parsing
*
* 1. **Logical Operators**:
* - `and`: Represents a conjunction of dependencies (all must be satisfied).
* - `or`: Represents a disjunction of dependencies (any one can satisfy the requirement).
* - `if`: Represents a conditional dependency (e.g., "feh if Xserver").
*
* 2. **Version Constraints**:
* - Operators: `>=`, `<=`, `>`, `<`, `=`, `==`, `!=`.
* - Operands: Version strings or capabilities (for `if` conditions).
*
* 3. **Parentheses Handling**:
* - Nested parentheses are supported (e.g., `(feh and xrandr) if Xserver`).
* - Parentheses are used to group logical expressions.
*
* 4. **Package-Specific Rules**:
* - **RPM**:
* - Supports conditional dependencies (`if`).
* - Handles nested logical expressions.
* - **DEB**:
* - Uses `|` for alternatives (logical OR).
* - Version constraints are enclosed in parentheses (e.g., `libc6 (>= 2.34)`).
* - **Arch Linux**:
* - Simple space-separated dependencies.
* - Version constraints are directly appended to package names.
* - **Python**:
* - Supports environment markers (e.g., `; sys.platform == "win32"`).
* - Multiple constraints per package (e.g., `pbr!=2.1.0,>=2.0.0`).
* - **Conda**:
* - Simple space-separated dependencies.
* - Version constraints use `>=`, `<=`, `=`, etc.
*
* ### Error Handling
*
* 1. **Unbalanced Parentheses**:
* - Detects and reports mismatched parentheses.
* 2. **Invalid Format**:
* - Reports malformed package requirements (e.g., missing version after operator).
* 3. **Unsupported Operator**:
* - Reports unrecognized operators.
* 4. **Unsupported Package Type**:
* - Reports if the package type is not supported.
*
* ### Example Inputs and Outputs
*
* 1. **RPM Input**: `"((feh and xrandr) if Xserver)"`
* - Output:
* [
* [
* PkgDepend { capability: "feh", constraints: [VersionConstraint { operator: IfInstall, operand: "Xserver" }] },
* ],
* [
* PkgDepend { capability: "xrandr", constraints: [VersionConstraint { operator: IfInstall, operand: "Xserver" }] },
* ],
* ]
*
* 2. **DEB Input**: `"libc6 (>= 2.34), libgcc-s1 (>= 3.0) | gcc"`
* - Output:
* [
* [
* PkgDepend { capability: "libc6", constraints: [VersionConstraint { operator: VersionGreaterThanEqual, operand: "2.34" }] },
* ],
* [
* PkgDepend { capability: "libgcc-s1", constraints: [VersionConstraint { operator: VersionGreaterThanEqual, operand: "3.0" }] },
* PkgDepend { capability: "gcc", constraints: [] },
* ],
* ]
*
* 3. **Python Input**: `"networkx>=2.3.0\npbr!=2.1.0,>=2.0.0"`
* - Output:
* [
* [
* PkgDepend { capability: "networkx", constraints: [VersionConstraint { operator: VersionGreaterThanEqual, operand: "2.3.0" }] },
* ],
* [
* PkgDepend { capability: "pbr", constraints: [
* VersionConstraint { operator: VersionNotEqual, operand: "2.1.0" },
* VersionConstraint { operator: VersionGreaterThanEqual, operand: "2.0.0" },
* ]},
* ],
* ]
*
* 4. **Conda Input**: `"python 3.6*, cudatoolkit 9.0.*"`
* - Output:
* [
* [
* PkgDepend { capability: "python", constraints: [VersionConstraint { operator: VersionEqual, operand: "3.6*" }] },
* ],
* [
* PkgDepend { capability: "cudatoolkit", constraints: [VersionConstraint { operator: VersionEqual, operand: "9.0.*" }] },
* ],
* ]
*
* ### Extensibility
*
* - New package types can be added by implementing their respective parsers.
* - The normalized structure (`AndDepends`) ensures consistency across package types.
*/
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
#[allow(dead_code)]
pub enum Operator {
IfInstall,
VersionGreaterThan,
VersionGreaterThanEqual,
VersionLessThan,
VersionLessThanEqual,
VersionEqual,
VersionNotEqual,
VersionCompatible,
}
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub struct VersionConstraint {
pub operator: Operator,
pub operand: String,
}
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub struct PkgDepend {
pub capability: String,
pub constraints: Vec<VersionConstraint>,
}
pub type OrDepends = Vec<PkgDepend>;
pub type AndDepends = Vec<OrDepends>;
#[derive(Debug, PartialEq)]
pub enum ParseError {
UnbalancedParentheses,
InvalidFormat(String),
UnsupportedOperator,
UnsupportedPackageType,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ParseError::UnbalancedParentheses => write!(f, "Unbalanced parentheses"),
ParseError::InvalidFormat(msg) => write!(f, "Invalid format: {}", msg),
ParseError::UnsupportedOperator => write!(f, "Unsupported operator"),
ParseError::UnsupportedPackageType => write!(f, "Unsupported package type"),
}
}
}
impl Error for ParseError {}
pub fn parse_requires(package_format: PackageFormat, requires: &str) -> Result<AndDepends, ParseError> {
match package_format {
PackageFormat::Rpm => parse_rpm_requires(requires),
PackageFormat::Deb => parse_deb_requires(requires),
PackageFormat::Pacman => parse_archlinux_requires(requires),
PackageFormat::Python => parse_python_requires(requires),
PackageFormat::Conda => parse_conda_requires(requires),
PackageFormat::Apk => parse_archlinux_requires(requires),
PackageFormat::Epkg => Err(ParseError::UnsupportedPackageType),
PackageFormat::Brew => parse_brew_requires(requires),
}
}
lazy_static! {
static ref RPM_OPERATOR_REGEX: Regex = Regex::new(r"(>=|<=|==|!=|>|<|~=|=)").unwrap();
static ref ARCHLINUX_OPERATOR_REGEX: Regex = Regex::new(r"(>=|<=|==|!=|>|<|=~|=|~=|~)").unwrap();
static ref ARCHLINUX_COMMENT_REGEX: Regex = Regex::new(r"\s*: .*").unwrap();
static ref PYTHON_COMMENT_REGEX: Regex = Regex::new(r"\s*# .*").unwrap();
}
fn parse_deb_requires(requires: &str) -> Result<AndDepends, ParseError> {
let mut and_depends = Vec::new();
for dependency in requires.split(',') {
let dependency = dependency.trim();
if dependency.is_empty() {
continue;
}
let mut or_depends = Vec::new();
for alternative in dependency.split('|') {
let alternative = alternative.trim();
if alternative.is_empty() {
continue;
}
let (name, constraints) = parse_debian_package(alternative)?;
or_depends.push(PkgDepend {
capability: name,
constraints,
});
}
and_depends.push(or_depends);
}
Ok(and_depends)
}
fn parse_debian_package(clause: &str) -> Result<(String, Vec<VersionConstraint>), ParseError> {
let clause = clause.trim();
if let Some((name, version_part)) = clause.split_once('(') {
let name = name.trim().to_string();
let version_part = version_part.trim();
if !version_part.ends_with(')') {
return Err(ParseError::InvalidFormat("Missing closing parenthesis".to_string()));
}
let version_part = &version_part[..version_part.len() - 1];
let constraints = parse_version_constraints(version_part)?;
Ok((name, constraints))
} else {
Ok((clause.to_string(), Vec::new()))
}
}
pub fn format_version_constraint_for_world(constraints: &[VersionConstraint]) -> String {
if constraints.is_empty() {
return String::new();
}
if let Some(constraint) = constraints.first() {
let op_str = match constraint.operator {
Operator::VersionEqual => "=",
Operator::VersionGreaterThanEqual => ">=",
Operator::VersionGreaterThan => ">",
Operator::VersionLessThanEqual => "<=",
Operator::VersionLessThan => "<",
Operator::VersionCompatible => "~",
_ => "=",
};
format!("{}{}", op_str, constraint.operand)
} else {
String::new()
}
}
pub fn parse_world_constraint(constraint_str: &str) -> Option<Vec<VersionConstraint>> {
if constraint_str.is_empty() {
return None;
}
match parse_version_constraints(constraint_str) {
Ok(constraints) if !constraints.is_empty() => Some(constraints),
_ => None,
}
}
pub fn parse_package_spec_with_version(spec: &str, format: PackageFormat) -> (String, Option<Vec<VersionConstraint>>) {
let parts: Vec<&str> = spec.split("__").collect();
if parts.len() == 3 && !spec.contains('(') && !spec.contains(')') {
let pkgname = parts[0].to_string();
let version = parts[1].to_string();
let constraint = VersionConstraint {
operator: Operator::VersionEqual,
operand: version,
};
return (pkgname, Some(vec![constraint]));
}
let has_parens = spec.contains('(') && spec.contains(')');
let has_whitespace = spec.chars().any(|c| c.is_ascii_whitespace());
let mut paren_depth = 0;
let has_version_op = spec.chars().any(|c| {
match c {
'(' => {
paren_depth += 1;
false
}
')' => {
paren_depth -= 1;
false
}
'<' | '>' | '=' | '!' if paren_depth == 0 => true,
_ => false,
}
});
let looks_like_atomic_capability = has_parens && !has_whitespace && !has_version_op;
if looks_like_atomic_capability {
return (spec.to_string(), None);
}
let mut paren_depth = 0;
let mut found_idx = None;
let chars: Vec<char> = spec.chars().collect();
for (idx, ch) in spec.char_indices() {
match ch {
'(' => paren_depth += 1,
')' => paren_depth -= 1,
'~' if paren_depth == 0 => {
if idx + 1 < chars.len() && chars[idx + 1] == '=' {
found_idx = Some(idx);
break;
} else if format == PackageFormat::Apk && idx + 1 < chars.len() {
let next_char = chars[idx + 1];
if next_char.is_ascii_digit() {
found_idx = Some(idx);
break;
}
}
}
'>' | '<' | '=' | '!' if paren_depth == 0 => {
found_idx = Some(idx);
break;
}
_ => {}
}
}
if let Some(idx) = found_idx {
let name = spec[..idx].trim().to_string();
let version_part = spec[idx..].trim();
match parse_version_constraints(version_part) {
Ok(constraints) if !constraints.is_empty() => {
return (name, Some(constraints));
}
_ => {
return (spec.to_string(), None);
}
}
}
(spec.to_string(), None)
}
pub fn parse_version_constraints(version_part: &str) -> Result<Vec<VersionConstraint>, ParseError> {
parse_version_constraints_and(version_part)
}
pub fn parse_version_constraints_and(version_part: &str) -> Result<Vec<VersionConstraint>, ParseError> {
let mut constraints = Vec::new();
for constraint in version_part.split(',') {
let constraint = constraint.trim();
if constraint.is_empty() {
continue;
}
let (operator, op_len) = match parse_operator_from_start(constraint) {
Some((op, len)) => (op, len),
None => (Operator::VersionEqual, 0),
};
let operand = if op_len > 0 {
constraint[op_len..].trim().to_string()
} else {
constraint.to_string()
};
if operand.is_empty() {
return Err(ParseError::InvalidFormat(format!(
"Invalid version constraint: {}",
constraint
)));
}
constraints.push(VersionConstraint { operator, operand });
}
Ok(constraints)
}
fn parse_operator_from_start(s: &str) -> Option<(Operator, usize)> {
if s.starts_with(">=") {
Some((Operator::VersionGreaterThanEqual, 2))
} else if s.starts_with(">>") {
Some((Operator::VersionGreaterThan, 2))
} else if s.starts_with(">") {
Some((Operator::VersionGreaterThan, 1))
} else if s.starts_with("<=") {
Some((Operator::VersionLessThanEqual, 2))
} else if s.starts_with("<<") {
Some((Operator::VersionLessThan, 2))
} else if s.starts_with("<") {
Some((Operator::VersionLessThan, 1))
} else if s.starts_with("==") {
Some((Operator::VersionEqual, 2))
} else if s.starts_with("=") {
Some((Operator::VersionEqual, 1))
} else if s.starts_with("!=") {
Some((Operator::VersionNotEqual, 2))
} else if s.starts_with("~=") {
Some((Operator::VersionCompatible, 2))
} else if s.starts_with("~") {
Some((Operator::VersionCompatible, 1))
} else {
None
}
}
pub fn parse_operator(op: &str) -> Option<Operator> {
match op {
">=" => Some(Operator::VersionGreaterThanEqual),
"<=" => Some(Operator::VersionLessThanEqual),
">" | ">>" => Some(Operator::VersionGreaterThan),
"<" | "<<" => Some(Operator::VersionLessThan),
"=" | "==" => Some(Operator::VersionEqual),
"!=" => Some(Operator::VersionNotEqual),
"~=" => Some(Operator::VersionCompatible),
"=~" => Some(Operator::VersionCompatible),
"~" => Some(Operator::VersionCompatible),
"if" => Some(Operator::IfInstall),
_ => None,
}
}
pub fn parse_archlinux_requires(requires: &str) -> Result<AndDepends, ParseError> {
let requires = ARCHLINUX_COMMENT_REGEX.replace(requires, "").to_string();
let mut and_depends = Vec::new();
for clause in requires.split_whitespace() {
if let Some(equals_pos) = clause.find('=') {
let after_equals = &clause[equals_pos + 1..];
if after_equals.contains(".so") && !after_equals.chars().next().map_or(false, |c| c.is_ascii_digit()) {
let capability = clause[..equals_pos].to_string();
and_depends.push(vec![PkgDepend {
capability,
constraints: Vec::new(),
}]);
continue;
}
}
let normalized_clause = ARCHLINUX_OPERATOR_REGEX.replace_all(clause, " $1 ").to_string();
let (name, constraints) = parse_package(&normalized_clause)?;
and_depends.push(vec![PkgDepend {
capability: name,
constraints,
}]);
}
Ok(and_depends)
}
pub fn parse_brew_requires(requires: &str) -> Result<AndDepends, ParseError> {
let mut and_depends = Vec::new();
for name in requires.split(',') {
let name = name.trim();
if name.is_empty() {
continue;
}
and_depends.push(vec![PkgDepend {
capability: name.to_string(),
constraints: Vec::new(),
}]);
}
Ok(and_depends)
}
pub fn parse_python_requires(requires: &str) -> Result<AndDepends, ParseError> {
let mut and_depends = Vec::new();
let requires = PYTHON_COMMENT_REGEX.replace(requires, "").to_string();
for line in requires.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let (spec_part, marker) = match line.split_once(';') {
Some((s, m)) => (s.trim(), Some(m.trim())),
None => (line, None),
};
let (name, constraints) = parse_python_package(spec_part)?;
let mut or_depends = Vec::new();
if let Some(marker) = marker {
or_depends.push(PkgDepend {
capability: name,
constraints: vec![VersionConstraint {
operator: Operator::IfInstall,
operand: marker.to_string(),
}],
});
} else {
or_depends.push(PkgDepend {
capability: name,
constraints,
});
}
and_depends.push(or_depends);
}
Ok(and_depends)
}
fn parse_python_package(clause: &str) -> Result<(String, Vec<VersionConstraint>), ParseError> {
let clause = clause.trim();
let (name, version_part) = if let Some(idx) = clause.find(|c: char| c == '>' || c == '<' || c == '=' || c == '!' || c == '~') {
let name = clause[..idx].trim().to_string();
let version_part = clause[idx..].trim();
(name, version_part)
} else {
(clause.to_string(), "")
};
let constraints = if !version_part.is_empty() {
parse_version_constraints(version_part)?
} else {
Vec::new()
};
Ok((name, constraints))
}
#[allow(dead_code)]
pub fn get_package_format(origin_url: &str) -> Option<PackageFormat> {
let path = Path::new(origin_url);
let file_name = path.file_name()
.and_then(|n| n.to_str())?;
PackageFormat::from_suffix(file_name).ok().or(Some(PackageFormat::Epkg))
}
#[cfg(test)]
pub fn pkg(name: &str, constraints: &[(&str, &str)]) -> PkgDepend {
PkgDepend {
capability: name.to_string(),
constraints: constraints
.iter()
.map(|(op, ver)| VersionConstraint {
operator: parse_operator(op).unwrap(),
operand: ver.to_string(),
})
.collect(),
}
}
#[cfg(test)]
pub fn pkg_if(name: &str, condition: &str) -> PkgDepend {
PkgDepend {
capability: name.to_string(),
constraints: vec![VersionConstraint {
operator: Operator::IfInstall,
operand: condition.to_string(),
}],
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rpm_requires::has_outer_parentheses;
#[test]
fn test_parse_package_spec_with_version_apk_tilde_operator() {
let spec = "package~2.2";
let (name, constraints) = parse_package_spec_with_version(spec, PackageFormat::Apk);
assert_eq!(name, "package");
assert!(constraints.is_some());
let constraints = constraints.unwrap();
assert_eq!(constraints.len(), 1);
assert_eq!(constraints[0].operator, Operator::VersionCompatible);
assert_eq!(constraints[0].operand, "2.2");
}
#[test]
fn test_parse_package_spec_with_version_apk_tilde_not_operator_when_followed_by_letter() {
let spec = "package~rc1";
let (name, constraints) = parse_package_spec_with_version(spec, PackageFormat::Apk);
assert_eq!(name, spec);
assert!(constraints.is_none());
}
#[test]
fn test_deb() {
assert_eq!(
parse_requires(PackageFormat::Deb, "libc6 (>= 2.34)").unwrap(),
vec![vec![pkg("libc6", &[(">=", "2.34")])]]
);
assert_eq!(
parse_requires(PackageFormat::Deb, "libgcc-s1 (>= 3.0) | gcc").unwrap(),
vec![vec![
pkg("libgcc-s1", &[(">=", "3.0")]),
pkg("gcc", &[]),
]]
);
assert_eq!(
parse_requires(PackageFormat::Deb, "emacs | emacs-gtk | emacs-lucid").unwrap(),
vec![vec![
pkg("emacs", &[]),
pkg("emacs-gtk", &[]),
pkg("emacs-lucid", &[]),
]]
);
let input = "libao4 (>= 1.1.0), libc6 (>= 2.34), debconf (>= 0.5) | debconf-2.0";
let result = parse_requires(PackageFormat::Deb, input).unwrap();
assert_eq!(result.len(), 3);
assert!(result.contains(&vec![pkg("libao4", &[(">=", "1.1.0")])]));
assert!(result.contains(&vec![pkg("libc6", &[(">=", "2.34")])]));
assert!(result.contains(&vec![
pkg("debconf", &[(">=", "0.5")]),
pkg("debconf-2.0", &[]),
]));
}
#[test]
fn test_archlinux() {
assert_eq!(
parse_requires(PackageFormat::Pacman, "bash: GNU Bourne Again SHell").unwrap(),
vec![vec![pkg("bash", &[])]]
);
assert_eq!(
parse_requires(PackageFormat::Pacman, "zsh>=4.3.9").unwrap(),
vec![vec![pkg("zsh", &[(">=", "4.3.9")])]]
);
assert_eq!(
parse_requires(PackageFormat::Pacman, "git python").unwrap(),
vec![
vec![pkg("git", &[])],
vec![pkg("python", &[])]
]
);
let input = "python python-gobject ttf-font gtk3 python-xdg";
let result = parse_requires(PackageFormat::Pacman, input).unwrap();
assert_eq!(result.len(), 5);
assert!(result.contains(&vec![pkg("python", &[])]));
assert!(result.contains(&vec![pkg("python-gobject", &[])]));
assert!(result.contains(&vec![pkg("ttf-font", &[])]));
assert!(result.contains(&vec![pkg("gtk3", &[])]));
assert!(result.contains(&vec![pkg("python-xdg", &[])]));
}
#[test]
fn test_python() {
assert_eq!(
parse_requires(PackageFormat::Python, "networkx>=2.3.0").unwrap(),
vec![vec![pkg("networkx", &[(">=", "2.3.0")])]]
);
assert_eq!(
parse_requires(PackageFormat::Python, "pbr!=2.1.0,>=2.0.0").unwrap(),
vec![vec![pkg("pbr", &[("!=", "2.1.0"), (">=", "2.0.0")])]]
);
assert_eq!(
parse_requires(PackageFormat::Python, "pkg # comment").unwrap(),
vec![vec![pkg("pkg", &[])]]
);
assert_eq!(
parse_requires(PackageFormat::Python, "./granulate-utils/").unwrap(),
vec![vec![pkg("./granulate-utils/", &[])]]
);
assert_eq!(
parse_requires(PackageFormat::Python, "package~=1.0").unwrap(),
vec![vec![pkg("package", &[("~=", "1.0")])]]
);
}
#[test]
fn test_package_naming_conventions() {
assert_eq!(
parse_requires(PackageFormat::Rpm, "perl(Net::LibIDN)").unwrap(),
vec![vec![pkg("perl(Net::LibIDN)", &[])]]
);
assert_eq!(
parse_requires(PackageFormat::Deb, "lib:package").unwrap(),
vec![vec![pkg("lib:package", &[])]]
);
assert_eq!(
parse_requires(PackageFormat::Python, "package-name").unwrap(),
vec![vec![pkg("package-name", &[])]]
);
}
#[test]
fn test_parentheses() {
let input = "(A and B and C)";
assert_eq!(has_outer_parentheses(input), Ok(true));
let input = "(A and B and C) if (X or Y)";
assert_eq!(has_outer_parentheses(input), Ok(false));
let input = "(A and B and C";
assert_eq!(
has_outer_parentheses(input),
Err(ParseError::UnbalancedParentheses)
);
let input = "(A and B and C))";
assert_eq!(
has_outer_parentheses(input),
Ok(false)
);
let input = "(A and (B and C)";
assert_eq!(
has_outer_parentheses(input),
Err(ParseError::UnbalancedParentheses)
);
let input = "((A and B) if (X or Y))";
assert_eq!(has_outer_parentheses(input), Ok(true));
let input = "";
assert_eq!(has_outer_parentheses(input), Ok(false));
let input = "A and B and C";
assert_eq!(has_outer_parentheses(input), Ok(false));
let input = "(";
assert_eq!(
has_outer_parentheses(input),
Err(ParseError::UnbalancedParentheses)
);
let input = ")";
assert_eq!(
has_outer_parentheses(input),
Ok(false)
);
let input = "((A and B) and (C or D))";
assert_eq!(has_outer_parentheses(input), Ok(true));
let input = "((A and B) and (C or D)";
assert_eq!(
has_outer_parentheses(input),
Err(ParseError::UnbalancedParentheses)
);
}
#[test]
fn test_get_package_format() {
let test_url = "https://mirrors.huaweicloud.com/ubuntu//pool/main/u/ubuntu-themes/ubuntu-mono_24.04-0ubuntu1_all.deb";
match get_package_format(test_url) {
Some(format) => println!("包格式: {:?}", format),
None => println!("无法确定包格式"),
}
assert_eq!(get_package_format(test_url), Some(PackageFormat::Deb));
let rpm_url = "https://repo.openeuler.org/openEuler-24.09/everything/aarch64/Packages/http_load-09Mar2016-1.oe2409.aarch64.rpm";
assert_eq!(get_package_format(rpm_url), Some(PackageFormat::Rpm));
}
}