diff --git a/rust-solv/Cargo.toml b/rust-solv/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..b89ee234b6196d59206f9df7432d078d3818345f --- /dev/null +++ b/rust-solv/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "rust-solv" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0" +toml = "0.5.9" +serde = { version = "1.0", features = ["derive"] } +quick-xml = { version = "0.23.0", features = ["serialize"] } +reqwest = { version = "0.11.11", features = ["blocking"] } +flate2 = "1.0.24" +configparser = { version = "3.0.0", features = ["indexmap"] } +varisat = "0.2.2" +indexmap = "1.9.1" \ No newline at end of file diff --git a/rust-solv/README.md b/rust-solv/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b4d56ced1060979d3af2f4f49d6901b55d16d33a --- /dev/null +++ b/rust-solv/README.md @@ -0,0 +1,21 @@ +# rust-solv + +## 简介 + +rust-solv 是一个使用 Rust 实现的基于 SAT 算法的软件包依赖分析库。 + +## 使用 + +在使用之前需要先在 `~/.config/rust-solv/config.toml` 编写配置文件,格式形如: + +```toml +[repoinfo] +name = "OS" +baseurl = "http://repo.openeuler.org/openEuler-22.03-LTS/OS/$basearch/" +``` + +之后便可以执行程序,查询在配置文件指定仓库中能否满足指定软件的依赖。 + +``` +$ cargo run package1 package2 ... +``` \ No newline at end of file diff --git a/rust-solv/src/config.rs b/rust-solv/src/config.rs new file mode 100644 index 0000000000000000000000000000000000000000..3b70357e316e1aef76337230d6b47f934245d029 --- /dev/null +++ b/rust-solv/src/config.rs @@ -0,0 +1,36 @@ +use anyhow::{self, Context, Result}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::Path; +use toml; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Config { + repoinfo: Repoinfo, +} + +#[derive(Debug, Serialize, Deserialize)] +struct Repoinfo { + name: Option, + baseurl: Option, +} + +impl Config { + fn from_str(s: &str) -> Result { + toml::from_str(s).with_context(|| "failed to parse the config file.") + } + + pub fn from_file(path: &Path) -> Result { + let s = fs::read_to_string(path) + .with_context(|| format!("failed to open the config file {:?}.", path))?; + Config::from_str(&s) + } + + pub fn get_repo_name(&self) -> &Option { + &self.repoinfo.name + } + + pub fn get_repo_baseurl(&self) -> &Option { + &self.repoinfo.baseurl + } +} diff --git a/rust-solv/src/lib.rs b/rust-solv/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..b6a81458e5b0a396fc5544889a41198a3302f941 --- /dev/null +++ b/rust-solv/src/lib.rs @@ -0,0 +1,6 @@ +pub mod config; +pub mod repo; +mod repomd; +pub mod solve; +mod yum; +mod version; \ No newline at end of file diff --git a/rust-solv/src/main.rs b/rust-solv/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..eaa6a3c713c2733652eb20b34341f9ff3d768e15 --- /dev/null +++ b/rust-solv/src/main.rs @@ -0,0 +1,32 @@ +use anyhow::Result; +use rust_solv::{config, repo, solve}; +use std::{env, path::Path}; + +fn main() -> Result<()> { + let packages: Vec = env::args() + .enumerate() + .filter(|&(i, _)| i > 0) + .map(|(_, v)| v) + .collect(); + if packages.is_empty() { + panic!("Package name not found!"); + } else { + let config_path_str = std::env::var("HOME")? + "/.config/rust-solv/config.toml"; + let cfg = config::Config::from_file(Path::new(&config_path_str))?; + if let Some(repo_baseurl) = cfg.get_repo_baseurl() { + let repo = repo::Repo::from_baseurl(repo_baseurl)?; + for package_name in packages { + match solve::check_package_satisfiability_in_repo(&repo, &package_name) { + Ok(solve::ReturnValue::Satisfied) => println!("Congratulations! Package {}'s dependencies can be satisfied in the repo. :)", package_name), + Ok(solve::ReturnValue::Unsatisfied) => println!("Sorry, package {}'s dependencies can not be satisfied in the repo. :(", package_name), + Ok(solve::ReturnValue::VersionConflict) => println!("Sorry, package {}'s dependencies can not be satisfied in the repo. (version conflict) :(", package_name), + Ok(solve::ReturnValue::PackageNotFound) => println!("Error: package {} not found in the repo. :(", package_name), + Err(_) => println!("Error: something wrong happened while solving. :("), + } + } + Ok(()) + } else { + panic!("Repo baseurl not found! Please check the config file!"); + } + } +} diff --git a/rust-solv/src/repo.rs b/rust-solv/src/repo.rs new file mode 100644 index 0000000000000000000000000000000000000000..fdd65ddd7bfde5fabc91924de37c46d0d88fa802 --- /dev/null +++ b/rust-solv/src/repo.rs @@ -0,0 +1,290 @@ +use crate::repomd::Repomd; +use crate::version::{version_compare, Flag}; +use crate::yum::YumVariables; +use anyhow::{anyhow, Context, Result}; +use quick_xml; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Serialize, Deserialize)] +struct Version { + epoch: i32, + ver: String, + rel: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RpmEntry { + pub name: String, + pub flags: Option, + pub epoch: Option, + pub ver: Option, + pub rel: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Entries { + #[serde(rename = "entry")] + entries: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Format { + provides: Option, + requires: Option, + conflicts: Option, + obsoletes: Option, +} + +pub type IdT = usize; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Package { + name: String, + version: Version, + format: Format, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Repo { + #[serde(rename = "package")] + packages: Vec, + #[serde(skip)] + providers: HashMap>, +} + +impl Repo { + pub fn from_str(primary_xml: &str) -> Result { + let mut repo: Repo = + quick_xml::de::from_str(&primary_xml).with_context(|| "Failed to parse primary.xml")?; + for (index, package) in repo.packages.iter().enumerate() { + if let Some(ref provides) = package.format.provides { + for entry in &provides.entries { + if let Some(ids) = repo.providers.get_mut(&entry.name) { + ids.push(index); + } else { + repo.providers.insert(entry.name.clone(), vec![index]); + } + } + } + } + Ok(repo) + } + + pub fn from_baseurl(repo_baseurl: &str) -> Result { + let repo_baseurl = if repo_baseurl.ends_with('/') { + repo_baseurl.to_string() + } else { + repo_baseurl.to_string() + "/" + }; + let yum_variables = YumVariables::new()?; + let repo_baseurl = yum_variables.replace_yum_variables(repo_baseurl)?; + let primary_xml = Repomd::get_primary_xml(repo_baseurl)?; + Repo::from_str(&primary_xml) + } + + pub fn get_package_id_by_name(&self, name: &str) -> Option { + for (id, package) in self.packages.iter().enumerate() { + if package.name == name { + return Some(id); + } + } + None + } + + pub fn get_package_requires_by_id<'a>(&'a self, package_id: IdT) -> Option<&'a Vec> { + if let Some(package) = self.packages.get(package_id) { + if let Some(ref e) = package.format.requires { + return Some(&e.entries); + } + } + None + } + + pub fn get_package_conflicts_by_id<'a>(&'a self, package_id: IdT) -> Option<&'a Vec> { + if let Some(package) = self.packages.get(package_id) { + if let Some(ref e) = package.format.conflicts { + return Some(&e.entries); + } + } + None + } + + pub fn get_package_obsoletes_by_id<'a>(&'a self, package_id: IdT) -> Option<&'a Vec> { + if let Some(package) = self.packages.get(package_id) { + if let Some(ref e) = package.format.obsoletes { + return Some(&e.entries); + } + } + None + } + + pub fn get_entry_provider_id(&self, entry: &RpmEntry) -> Option<&Vec> { + self.providers.get(&entry.name) + } + + fn get_entry_by_provider_id(&self, provider_id: IdT, entry_name: &str) -> Option<&RpmEntry> { + for entry in &self.packages[provider_id] + .format + .provides + .as_ref() + .unwrap() + .entries + { + if entry.name == entry_name { + return Some(entry); + } + } + None + } + + // Package requires entry x, and it is provided by another package as entry y. + pub fn check_version_constraint( + &self, + entry_required: &RpmEntry, + provider_id: &IdT, + ) -> Result { + if let Some(flags) = &entry_required.flags { + let entry_provided = self + .get_entry_by_provider_id(*provider_id, &entry_required.name) + .unwrap(); + match flags.as_str() { + "LT" => match &entry_provided.flags { + Some(flags) => match flags.as_str() { + "GE" | "EQ" | "GT" => { + version_compare(entry_required, entry_provided, Flag::GT) + } + _ => Ok(true), + }, + _ => Ok(true), + }, + "LE" => match &entry_provided.flags { + Some(flags) => match flags.as_str() { + "GE" | "EQ" => { + version_compare(entry_required, entry_provided, Flag::GE) + }, + "GT" => { + version_compare(entry_required, entry_provided, Flag::GT) + }, + _ => Ok(true), + }, + _ => Ok(true), + }, + "EQ" => match &entry_provided.flags { + Some(flags) => match flags.as_str() { + "LE" => { + version_compare(entry_required, entry_provided, Flag::GE) + }, + "LT" => { + version_compare(entry_required, entry_provided, Flag::GT) + }, + "EQ" => { + version_compare(entry_required, entry_provided, Flag::EQ) + }, + "GT" => { + version_compare(entry_required, entry_provided, Flag::LT) + }, + "GE" => { + version_compare(entry_required, entry_provided, Flag::LE) + } + _ => Ok(true), + }, + _ => Ok(true), + }, + "GE" => match &entry_provided.flags { + Some(flags) => match flags.as_str() { + "LE" | "EQ" => { + version_compare(entry_required, entry_provided, Flag::LE) + }, + "LT" => { + version_compare(entry_required, entry_provided, Flag::LT) + }, + _ => Ok(true), + }, + _ => Ok(true), + }, + "GT" => match &entry_provided.flags { + Some(flags) => match flags.as_str() { + "LE" | "EQ" | "LT" => { + version_compare(entry_required, entry_provided, Flag::LT) + } + _ => Ok(true), + }, + _ => Ok(true), + }, + _ => Err(anyhow!("invalid flags of entry")), + } + } else { + Ok(true) + } + } +} + +impl Package { + pub fn requires(self) -> Option> { + if let Some(e) = self.format.requires { + Some(e.entries) + } else { + None + } + } + + pub fn conflicts(self) -> Option> { + if let Some(e) = self.format.conflicts { + Some(e.entries) + } else { + None + } + } + + pub fn obsoletes(self) -> Option> { + if let Some(e) = self.format.obsoletes { + Some(e.entries) + } else { + None + } + } + + pub fn provides(self) -> Option> { + if let Some(e) = self.format.provides { + Some(e.entries) + } else { + None + } + } +} + +impl RpmEntry { + pub fn get_name(&self) -> &String { + &self.name + } + + pub fn get_epoch(&self) -> Option { + self.epoch + } + + pub fn get_ver(&self) -> Option<&String> { + self.ver.as_ref() + } + + pub fn get_rel(&self) -> Option<&String> { + self.rel.as_ref() + } + + pub fn get_flags(&self) -> Option<&String> { + self.flags.as_ref() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_primary_xml() -> Result<()> { + let repo_url = String::from("https://repo.openeuler.org/openEuler-22.03-LTS/OS/x86_64/"); + let repo: Repo = Repo::from_baseurl(&repo_url)?; + println!("{:?}", repo.packages); + Ok(()) + } +} diff --git a/rust-solv/src/repomd.rs b/rust-solv/src/repomd.rs new file mode 100644 index 0000000000000000000000000000000000000000..05c067ffb1b7095c0fd5fbd8bffda5d7278b4c36 --- /dev/null +++ b/rust-solv/src/repomd.rs @@ -0,0 +1,52 @@ +use std::io::Read; + +use anyhow::{Context, Result}; +use flate2::read::GzDecoder; +use serde::{self, Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Repomd { + #[serde(rename = "data")] + datas: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +struct Data { + r#type: String, + location: Location, +} + +#[derive(Debug, Serialize, Deserialize)] +struct Location { + href: String, +} + +impl Repomd { + pub fn get_primary_xml(repo_url: String) -> Result { + // Get repomd.xml from the repo. + let repomd_url = repo_url.clone() + "repodata/repomd.xml"; + let repomd_xml = reqwest::blocking::get(&repomd_url) + .with_context(|| format!("Failed to connect to {:?}", &repomd_url))? + .text()?; + // Deserialize repomd.xml into a structure using serde. + let repomd: Repomd = + quick_xml::de::from_str(&repomd_xml).with_context(|| "Failed to parse repomd.xml")?; + // Get the url of primary.xml.gz, download and decompress it. + let primary_data: Vec = repomd + .datas + .into_iter() + .filter(|data| data.r#type == "primary") + .collect(); + let primary_gz_url = repo_url.clone() + &primary_data[0].location.href; + let primary_gz_bytes: Result, _> = reqwest::blocking::get(&primary_gz_url) + .with_context(|| format!("Failed to connect to {:?}", &primary_gz_url))? + .bytes()? + .bytes() + .collect(); + let primary_gz_bytes = primary_gz_bytes.unwrap(); + let mut primary_gz = GzDecoder::new(&primary_gz_bytes[..]); + let mut primary_xml = String::new(); + primary_gz.read_to_string(&mut primary_xml)?; + Ok(primary_xml) + } +} diff --git a/rust-solv/src/solve.rs b/rust-solv/src/solve.rs new file mode 100644 index 0000000000000000000000000000000000000000..965096f1c8338c861e5ef253cd0b336c48a4b584 --- /dev/null +++ b/rust-solv/src/solve.rs @@ -0,0 +1,109 @@ +use crate::repo::{IdT, Repo}; +use anyhow::{anyhow, Result}; +use std::collections::{HashSet, VecDeque}; +use varisat::{solver::Solver, CnfFormula, ExtendFormula, Lit}; + +pub enum ReturnValue { + Satisfied, + Unsatisfied, + VersionConflict, + PackageNotFound, +} + +fn get_formula_by_package_id(repo: &Repo, package_id: IdT) -> Result { + let mut q = VecDeque::new(); + let mut formula = CnfFormula::new(); + let mut appeared = HashSet::new(); + q.push_back(package_id); + appeared.insert(package_id); + while let Some(package_id) = q.pop_front() { + if let Some(requires) = repo.get_package_requires_by_id(package_id) { + for entry in requires { + if let Some(providers) = repo.get_entry_provider_id(entry) { + let mut clause: Vec = providers + .iter() + .filter(|&id| { + if let Ok(constraint) = repo.check_version_constraint(entry, id) { + constraint + } else { + false + } + }) + .map(|&id| Lit::from_index(id, true)) + .collect(); + if clause.len() > 0 { + for lit in &clause { + if appeared.contains(&lit.index()) == false { + appeared.insert(lit.index()); + q.push_back(lit.index()); + } + } + clause.push(Lit::from_index(package_id, false)); + formula.add_clause(&clause); + } else { + return Err(anyhow!("version constraint not satisfied")); + } + } + } + } + if let Some(conflicts) = repo.get_package_conflicts_by_id(package_id) { + for entry in conflicts { + if let Some(providers) = repo.get_entry_provider_id(entry) { + for provider_id in providers { + if *provider_id == package_id { + continue; + } + match repo.check_version_constraint(entry, provider_id) { + Ok(true) => formula.add_clause(&[ + Lit::from_index(*provider_id, false), + Lit::from_index(package_id, false), + ]), + _ => continue, + } + } + } + } + } + if let Some(obsoletes) = repo.get_package_obsoletes_by_id(package_id) { + for entry in obsoletes { + if let Some(providers) = repo.get_entry_provider_id(entry) { + for provider_id in providers { + if *provider_id == package_id { + continue; + } + match repo.check_version_constraint(entry, provider_id) { + Ok(true) => formula.add_clause(&[ + Lit::from_index(*provider_id, false), + Lit::from_index(package_id, false), + ]), + _ => continue, + } + } + } + } + } + } + Ok(formula) +} + +pub fn check_package_satisfiability_in_repo(repo: &Repo, package_name: &String) -> Result { + if let Some(package_id) = repo.get_package_id_by_name(&package_name) { + if let Ok(formula) = get_formula_by_package_id(repo, package_id) { + let mut solver = Solver::new(); + solver.add_formula(&formula); + solver.assume(&[Lit::from_index(package_id, true)]); + match solver.solve() { + Ok(true) => Ok(ReturnValue::Satisfied), + _ => Ok(ReturnValue::Unsatisfied), + } + } else { + Ok(ReturnValue::VersionConflict) + } + } else { + println!( + "Error: the package {} is not found in the repository!", + package_name + ); + Ok(ReturnValue::PackageNotFound) + } +} diff --git a/rust-solv/src/version.rs b/rust-solv/src/version.rs new file mode 100644 index 0000000000000000000000000000000000000000..e35e0b44511835229c71ab186cf0d4b3a1406ab1 --- /dev/null +++ b/rust-solv/src/version.rs @@ -0,0 +1,301 @@ +use crate::repo::RpmEntry; +use anyhow::Result; +use std::cmp::min; + +pub enum Flag { + LE, + LT, + EQ, + GT, + GE, +} + +fn split_string_to_alpha_and_numeric_sections(str: &String) -> Result> { + let mut tmp = String::new(); + // flag: 0 - empty + // 1 - alpha + // 2 - numeric + let mut flag = 0; + let mut result: Vec = Vec::new(); + for ch in str.chars() { + if ch.is_alphabetic() { + match flag { + 1 => tmp = tmp + &ch.to_string(), + 2 => { + result.push(tmp.clone()); + tmp.clear(); + flag = 1; + tmp = tmp + &ch.to_string() + } + _ => { + flag = 1; + tmp = tmp + &ch.to_string() + } + } + } else if ch.is_numeric() { + match flag { + 1 => { + result.push(tmp.clone()); + tmp.clear(); + flag = 2; + tmp = tmp + &ch.to_string() + } + 2 => tmp = tmp + &ch.to_string(), + _ => { + flag = 2; + tmp = tmp + &ch.to_string() + } + } + } else if flag > 0 { + result.push(tmp.clone()); + tmp.clear(); + flag = 0; + } + } + if flag > 0 { + result.push(tmp.clone()); + } + Ok(result) +} + +fn is_alphabetic_string(str: &String) -> Result { + for ch in str.chars() { + if !ch.is_alphabetic() { + return Ok(false); + } + } + Ok(true) +} + +fn is_equal_section(x: &String, y: &String) -> Result { + match (is_alphabetic_string(&x)?, is_alphabetic_string(&y)?) { + (true, true) => Ok(x == y), + (false, false) => match (x.parse::(), y.parse::()) { + (Ok(x_num), Ok(y_num)) => Ok(x_num == y_num), + (_, _) => Ok(false), + }, + (_, _) => Ok(false), + } +} + +fn section_compare(x: &String, y: &String, op: Flag) -> Result { + match (is_alphabetic_string(&x)?, is_alphabetic_string(&y)?) { + // Both of the sections are alphabetic. + // Compare like strcmp function. + (true, true) => match op { + Flag::LT => Ok(x.cmp(&y).is_lt()), + Flag::LE => Ok(x.cmp(&y).is_le()), + Flag::EQ => Ok(x.cmp(&y).is_eq()), + Flag::GE => Ok(x.cmp(&y).is_ge()), + Flag::GT => Ok(x.cmp(&y).is_gt()), + }, + // If one of the sections is a number, while the other is alphabetic, + // the numeric elements is considered newer. + // (alphabetic, numeric) + (true, false) => match op { + Flag::LT | Flag::LE => Ok(true), + _ => Ok(false), + }, + // (numeric, alphabetic) + (false, true) => match op { + Flag::GE | Flag::GT => Ok(true), + _ => Ok(false), + }, + // Both of the sections are numbers. + (false, false) => match (x.parse::(), y.parse::()) { + (Ok(x_num), Ok(y_num)) => match op { + Flag::LT => Ok(x_num < y_num), + Flag::LE => Ok(x_num <= y_num), + Flag::EQ => Ok(x_num == y_num), + Flag::GE => Ok(x_num >= y_num), + Flag::GT => Ok(x_num > y_num), + }, + (_, _) => Ok(true), + }, + } +} + +fn label_compare(x: &String, y: &String, op: Flag) -> Result { + let x_vec = split_string_to_alpha_and_numeric_sections(x)?; + let y_vec = split_string_to_alpha_and_numeric_sections(y)?; + let limit = min(x_vec.len(), y_vec.len()); + for i in 0..limit { + if is_equal_section(&x_vec[i], &y_vec[i])? { + if i == limit - 1 { + match op { + Flag::LT | Flag::GT => { + if x_vec.len() == y_vec.len() { + return Ok(false); + } + } + _ => continue, + } + } + } else { + return section_compare(&x_vec[i], &y_vec[i], op); + } + } + if x_vec.len() != y_vec.len() { + match op { + Flag::LT | Flag::LE => return Ok(x_vec.len() < y_vec.len()), + Flag::EQ => return Ok(false), + Flag::GE | Flag::GT => return Ok(x_vec.len() > y_vec.len()), + } + } + Ok(true) +} + +pub fn version_compare(x: &RpmEntry, y: &RpmEntry, op: Flag) -> Result { + match (x.get_epoch(), y.get_epoch()) { + (Some(e1), Some(e2)) => { + if e1 == e2 { + match (x.get_ver(), y.get_ver()) { + (Some(v1), Some(v2)) => { + if label_compare(v1, v2, Flag::EQ)? { + match (x.get_rel(), y.get_rel()) { + (Some(r1), Some(r2)) => label_compare(r1, r2, op), + (Some(_), None) => match op { + Flag::LT | Flag::LE | Flag::EQ => Ok(false), + Flag::GE | Flag::GT => Ok(true), + }, + (None, Some(_)) => match op { + Flag::GE | Flag::GT | Flag::EQ => Ok(false), + Flag::LT | Flag::LE => Ok(true), + }, + (None, None) => match op { + Flag::GE | Flag::LE | Flag::EQ => Ok(true), + Flag::LT | Flag::GT => Ok(false), + }, + } + } else { + label_compare(v1, v2, op) + } + } + (_, _) => Ok(true), + } + } else { + match op { + Flag::LE | Flag::LT => Ok(e1 < e2), + Flag::GT | Flag::GE => Ok(e1 > e2), + Flag::EQ => Ok(false), + } + } + } + (Some(e1), None) => match op { + Flag::LE => Ok(e1 <= 0), + Flag::LT => Ok(e1 < 0), + Flag::EQ => Ok(e1 == 0), + Flag::GT => Ok(e1 > 0), + Flag::GE => Ok(e1 >= 0), + }, + (None, Some(e2)) => match op { + Flag::LE => Ok(e2 >= 0), + Flag::LT => Ok(e2 > 0), + Flag::EQ => Ok(e2 == 0), + Flag::GT => Ok(e2 < 0), + Flag::GE => Ok(e2 <= 0), + }, + (None, None) => match (x.get_ver(), y.get_ver()) { + (Some(v1), Some(v2)) => { + if label_compare(v1, v2, Flag::EQ)? { + match (x.get_rel(), y.get_rel()) { + (Some(r1), Some(r2)) => label_compare(r1, r2, op), + (Some(_), None) => match op { + Flag::LT | Flag::LE | Flag::EQ => Ok(false), + Flag::GE | Flag::GT => Ok(true), + }, + (None, Some(_)) => match op { + Flag::GE | Flag::GT | Flag::EQ => Ok(false), + Flag::LT | Flag::LE => Ok(true), + }, + (None, None) => match op { + Flag::GE | Flag::LE | Flag::EQ => Ok(true), + Flag::LT | Flag::GT => Ok(false), + }, + } + } else { + label_compare(v1, v2, op) + } + } + (_, _) => Ok(true), + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_version_compare() -> Result<()> { + let e1 = RpmEntry { + name: "QAQ".to_string(), + flags: Some("EQ".to_string()), + epoch: None, + ver: Some("1.2-1".to_string()), + rel: None, + }; + let e2 = RpmEntry { + name: "TAT".to_string(), + flags: Some("EQ".to_string()), + epoch: None, + ver: Some("1.2-1".to_string()), + rel: None, + }; + assert_eq!(version_compare(&e1, &e2, Flag::LE)?, true); + Ok(()) + } + + #[test] + fn test_label_compare() -> Result<()> { + assert_eq!( + label_compare(&"1.0010".to_string(), &"1.9".to_string(), Flag::LT)?, + false + ); + assert_eq!( + label_compare(&"1.0010".to_string(), &"1.9".to_string(), Flag::EQ)?, + false + ); + assert_eq!( + label_compare(&"1.0010".to_string(), &"1.9".to_string(), Flag::GE)?, + true + ); + assert_eq!( + label_compare(&"1.05".to_string(), &"1.5".to_string(), Flag::EQ)?, + true + ); + assert_eq!( + label_compare(&"1.05".to_string(), &"1.5".to_string(), Flag::GT)?, + false + ); + assert_eq!( + label_compare(&"1.05".to_string(), &"1.5".to_string(), Flag::LE)?, + true + ); + assert_eq!( + label_compare(&"1.0".to_string(), &"1".to_string(), Flag::GT)?, + true + ); + assert_eq!( + label_compare(&"2.50".to_string(), &"2.5".to_string(), Flag::GE)?, + true + ); + assert_eq!( + label_compare(&"fc4".to_string(), &"fc.4".to_string(), Flag::EQ)?, + true + ); + assert_eq!( + label_compare(&"FC5".to_string(), &"fc4".to_string(), Flag::LT)?, + true + ); + assert_eq!( + label_compare(&"2a".to_string(), &"2.5".to_string(), Flag::LT)?, + true + ); + assert_eq!( + label_compare(&"2.5.0".to_string(), &"2.5".to_string(), Flag::GT)?, + true + ); + Ok(()) + } +} diff --git a/rust-solv/src/yum.rs b/rust-solv/src/yum.rs new file mode 100644 index 0000000000000000000000000000000000000000..48560038a4b737cf3700e0c1de2b8df63894f334 --- /dev/null +++ b/rust-solv/src/yum.rs @@ -0,0 +1,100 @@ +use anyhow::{anyhow, Context, Result}; +use configparser; +use indexmap::IndexMap; +use std::process::Command; + +#[derive(Debug)] +pub struct YumVariables { + arch: String, + basearch: String, + releasever: String, +} + +impl YumVariables { + // $arch refers to the system's CPU architecture. + fn get_arch() -> Result { + let arch = String::from_utf8(Command::new("arch").output()?.stdout) + .with_context(|| "Error: failed to get $arch")?; + Ok(arch.trim().to_string()) + } + + // $basearch refers to the base architecture of the system. + // For example, i686 machines have a base architecture of i386, + // and AMD64 and Intel 64 machines have a base architecture of x86_64. + fn get_basearch() -> Result { + let arch = YumVariables::get_arch()?; + match arch.as_str() { + "i386" | "i586" | "i686" => Ok("i386".to_string()), + "x86_64" => Ok("x86_64".to_string()), + "aarch64" => Ok("aarch64".to_string()), + _ => Err(anyhow!("Error: unknown basearch.")), + } + } + + // $releasever refers to the release version of the system. + // Yum obtains the value of $releasever from the distroverpkg=value line in the /etc/yum.conf configuration file. + // If there is no such line in /etc/yum.conf, + // then yum infers the correct value by deriving the version number from the system-release package. + fn get_releasever() -> Result { + // First find distroverpkg=value line in /etc/yum.conf. + let mut config_loader = configparser::ini::Ini::new_cs(); + // Create a vector which contains maps with key "distroverpkg". + let maps_with_distroverpkg: Vec>> = config_loader + .load("/etc/yum.conf") + .unwrap() + .into_iter() + .map(|(_, kvs)| kvs) + .filter(|kvs| kvs.contains_key("distroverpkg")) + .collect(); + match maps_with_distroverpkg.get(0) { + Some(kvs) => Ok(kvs["distroverpkg"].to_owned().unwrap()), + None => { + let release = String::from_utf8( + Command::new("rpm") + .args(["-q", "openEuler-release"]) + .output()? + .stdout, + ) + .with_context(|| "Error: system-release package not found.")?; + // The variable "release" is a string like "system-release-version-...". + // So we split the string by "-", then get the element with index 2. + let release: Vec<&str> = release.split("-").collect(); + Ok(release[2].to_string()) + } + } + } + + pub fn new() -> Result { + Ok(YumVariables { + arch: YumVariables::get_arch()?, + basearch: YumVariables::get_basearch()?, + releasever: YumVariables::get_releasever()?, + }) + } + + pub fn replace_yum_variables(&self, s: String) -> Result { + let mut ret = s; + if ret.contains("$arch") { + ret = ret.replace("$arch", &self.arch); + } + if ret.contains("$basearch") { + ret = ret.replace("$basearch", &self.basearch); + } + if ret.contains("$releasever") { + ret = ret.replace("$releasever", &self.releasever); + } + Ok(ret) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_yum_variables() -> Result<()> { + let yum_var = YumVariables::new()?; + println!("{:?}", yum_var); + Ok(()) + } +} diff --git a/rust-solv/tests/dependency-unsatisfied.xml b/rust-solv/tests/dependency-unsatisfied.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1bf0c54365c2ba3b2b0ad109b417930be364f6e --- /dev/null +++ b/rust-solv/tests/dependency-unsatisfied.xml @@ -0,0 +1,54 @@ + + + + A + x86_64 + + + + + + + + + + + + + + B + x86_64 + + + + + + + + + C + x86_64 + + + + + + + + + + + + + + + D + x86_64 + + + + + + + + \ No newline at end of file diff --git a/rust-solv/tests/integration_test.rs b/rust-solv/tests/integration_test.rs new file mode 100644 index 0000000000000000000000000000000000000000..84c9eb494c72497924b240aecc747eb8b2a900a1 --- /dev/null +++ b/rust-solv/tests/integration_test.rs @@ -0,0 +1,78 @@ +use anyhow::Result; +use rust_solv::{repo, solve}; +use std::fs; + +#[test] +fn test_dependency_unsatisfied() -> Result<()> { + let xml = fs::read_to_string("/root/rust-solv/tests/dependency-unsatisfied.xml")?; + let repo = repo::Repo::from_str(&xml)?; + match solve::check_package_satisfiability_in_repo(&repo, &"A".to_string()) { + Ok(solve::ReturnValue::Satisfied) => println!( + "Congratulations! Package {}'s dependencies can be satisfied in the repo. :)", + "A" + ), + Ok(solve::ReturnValue::Unsatisfied) => println!( + "Sorry, package {}'s dependencies can not be satisfied in the repo. :(", + "A" + ), + Ok(solve::ReturnValue::VersionConflict) => println!( + "Sorry, package {}'s dependencies can not be satisfied in the repo. (version conflict) :(", + "A" + ), + Ok(solve::ReturnValue::PackageNotFound) => { + println!("Error: package {} not found in the repo. :(", "A") + } + Err(_) => println!("Error: something wrong happened while solving. :("), + } + Ok(()) +} + +#[test] +fn test_version_unsatisfied() -> Result<()> { + let xml = fs::read_to_string("/root/rust-solv/tests/version-unsatisfied.xml")?; + let repo = repo::Repo::from_str(&xml)?; + match solve::check_package_satisfiability_in_repo(&repo, &"A".to_string()) { + Ok(solve::ReturnValue::Satisfied) => println!( + "Congratulations! Package {}'s dependencies can be satisfied in the repo. :)", + "A" + ), + Ok(solve::ReturnValue::Unsatisfied) => println!( + "Sorry, package {}'s dependencies can not be satisfied in the repo. :(", + "A" + ), + Ok(solve::ReturnValue::VersionConflict) => println!( + "Sorry, package {}'s dependencies can not be satisfied in the repo. (version conflict) :(", + "A" + ), + Ok(solve::ReturnValue::PackageNotFound) => { + println!("Error: package {} not found in the repo. :(", "A") + } + Err(_) => println!("Error: something wrong happened while solving. :("), + } + Ok(()) +} + +#[test] +fn test_satisfied() -> Result<()> { + let xml = fs::read_to_string("/root/rust-solv/tests/satisfied.xml")?; + let repo = repo::Repo::from_str(&xml)?; + match solve::check_package_satisfiability_in_repo(&repo, &"A".to_string()) { + Ok(solve::ReturnValue::Satisfied) => println!( + "Congratulations! Package {}'s dependencies can be satisfied in the repo. :)", + "A" + ), + Ok(solve::ReturnValue::Unsatisfied) => println!( + "Sorry, package {}'s dependencies can not be satisfied in the repo. :(", + "A" + ), + Ok(solve::ReturnValue::VersionConflict) => println!( + "Sorry, package {}'s dependencies can not be satisfied in the repo. (version conflict) :(", + "A" + ), + Ok(solve::ReturnValue::PackageNotFound) => { + println!("Error: package {} not found in the repo. :(", "A") + } + Err(_) => println!("Error: something wrong happened while solving. :("), + } + Ok(()) +} diff --git a/rust-solv/tests/satisfied.xml b/rust-solv/tests/satisfied.xml new file mode 100644 index 0000000000000000000000000000000000000000..8f87c9728409a4115063ea693d49d9d632943960 --- /dev/null +++ b/rust-solv/tests/satisfied.xml @@ -0,0 +1,51 @@ + + + + A + x86_64 + + + + + + + + + + + + + + B + x86_64 + + + + + + + + + C + x86_64 + + + + + + + + + + + + D + x86_64 + + + + + + + + \ No newline at end of file diff --git a/rust-solv/tests/version-unsatisfied.xml b/rust-solv/tests/version-unsatisfied.xml new file mode 100644 index 0000000000000000000000000000000000000000..1f4ce7c9deed949c8886913ce7d476bf38fbc78e --- /dev/null +++ b/rust-solv/tests/version-unsatisfied.xml @@ -0,0 +1,51 @@ + + + + A + x86_64 + + + + + + + + + + + + + + B + x86_64 + + + + + + + + + C + x86_64 + + + + + + + + + + + + D + x86_64 + + + + + + + + \ No newline at end of file