From 078bc91cf885b2514cd9750a353138aa6eda6c98 Mon Sep 17 00:00:00 2001 From: RGBCube Date: Thu, 8 May 2025 22:40:25 +0300 Subject: [PATCH] feat: move parse_name_and_version to StorePath impl, rename util to version.rs --- src/lib.rs | 71 +++++++++++++-- src/util.rs | 234 ------------------------------------------------- src/version.rs | 125 ++++++++++++++++++++++++++ 3 files changed, 190 insertions(+), 240 deletions(-) delete mode 100644 src/util.rs create mode 100644 src/version.rs diff --git a/src/lib.rs b/src/lib.rs index 4c8e1ac..92f49b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,17 @@ -use std::path::{ - Path, - PathBuf, +use std::{ + path::{ + Path, + PathBuf, + }, + sync, }; +use anyhow::{ + Context as _, + Result, + anyhow, + bail, +}; use derive_more::Deref; use ref_cast::RefCast; @@ -11,14 +20,64 @@ pub mod print; pub mod store; -pub mod util; +mod version; +pub use version::Version; #[derive(Deref, Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct DerivationId(i64); +#[derive(Deref, Debug, Clone, PartialEq, Eq)] +pub struct StorePathBuf(PathBuf); + #[derive(RefCast, Deref, Debug, PartialEq, Eq)] #[repr(transparent)] pub struct StorePath(Path); -#[derive(Deref, Debug, Clone, PartialEq, Eq)] -pub struct StorePathBuf(PathBuf); +impl StorePath { + /// Parses a Nix store path to extract the packages name and possibly its + /// version. + /// + /// This function first drops the inputs first 44 chars, since that is exactly + /// the length of the `/nix/store/0004yybkm5hnwjyxv129js3mjp7kbrax-` prefix. + /// Then it matches that against our store path regex. + pub fn parse_name_and_version(&self) -> Result<(&str, Option<&Version>)> { + static STORE_PATH_REGEX: sync::LazyLock = + sync::LazyLock::new(|| { + regex::Regex::new("(.+?)(-([0-9].*?))?$") + .expect("failed to compile regex pattern for nix store paths") + }); + + let path = self.to_str().with_context(|| { + format!( + "failed to convert path '{path}' to valid unicode", + path = self.display(), + ) + })?; + + // We can strip the path since it _always_ follows the format: + // + // /nix/store/0004yybkm5hnwjyxv129js3mjp7kbrax-... + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + // This part is exactly 44 chars long, so we just remove it. + assert_eq!(&path[..11], "/nix/store/"); + assert_eq!(&path[43..44], "-"); + let path = &path[44..]; + + log::debug!("stripped path: {path}"); + + let captures = STORE_PATH_REGEX.captures(path).ok_or_else(|| { + anyhow!("path '{path}' does not match expected Nix store format") + })?; + + let name = captures.get(1).map_or("", |capture| capture.as_str()); + if name.is_empty() { + bail!("failed to extract name from path '{path}'"); + } + + let version: Option<&Version> = captures.get(2).map(|capture| { + Version::ref_cast(capture.as_str().trim_start_matches('-')) + }); + + Ok((name, version)) + } +} diff --git a/src/util.rs b/src/util.rs deleted file mode 100644 index 2ed37fe..0000000 --- a/src/util.rs +++ /dev/null @@ -1,234 +0,0 @@ -use std::{ - cmp::Ordering, - collections::{ - HashMap, - HashSet, - }, - sync::OnceLock, -}; - -use log::debug; -use regex::Regex; - -use crate::error::AppError; - -// Use type alias for Result with our custom error type -type Result = std::result::Result; - -use std::string::ToString; - -#[derive(Eq, PartialEq, Debug)] -#[derive(Debug, Clone, Eq, PartialEq)] -enum VersionComponent<'a> { - Number(u64), - Text(&'a str), -} - -impl PartialOrd for VersionComponent<'_> { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl cmp::Ord for VersionComponent<'_> { - fn cmp(&self, other: &Self) -> cmp::Ordering { - use VersionComponent::{ - Number, - Text, - }; - - match (self, other) { - (Number(x), Number(y)) => x.cmp(y), - (Text(x), Text(y)) => { - match (*x, *y) { - ("pre", _) => cmp::Ordering::Less, - (_, "pre") => cmp::Ordering::Greater, - _ => x.cmp(y), - } - }, - (Text(_), Number(_)) => cmp::Ordering::Less, - (Number(_), Text(_)) => cmp::Ordering::Greater, - } - } -} - -/// Yields [`VertionComponent`] from a version string. -#[derive(Deref, DerefMut, From)] -struct VersionComponentIter<'a>(&'a str); - -impl<'a> Iterator for VersionComponentIter<'a> { - type Item = VersionComponent<'a>; - - fn next(&mut self) -> Option { - // Skip all '-' and '.'. - while self.starts_with(['.', '-']) { - **self = &self[1..]; - } - - // Get the next character and decide if it is a digit. - let is_digit = self.chars().next()?.is_ascii_digit(); - - // Based on this collect characters after this into the component. - let component_len = self - .chars() - .take_while(|&c| { - c.is_ascii_digit() == is_digit && !matches!(c, '.' | '-') - }) - .map(char::len_utf8) - .sum(); - - let component = &self[..component_len]; - **self = &self[component_len..]; - - assert!(!component.is_empty()); - - if is_digit { - component.parse::().ok().map(VersionComponent::Number) - } else { - Some(VersionComponent::Text(component)) - } - } -} - -/// Compares two strings of package versions, and figures out the greater one. -pub fn compare_versions(this: &str, that: &str) -> cmp::Ordering { - let this = VersionComponentIter::from(this); - let that = VersionComponentIter::from(that); - - this.cmp(that) -} - -/// Parses a Nix store path to extract the packages name and possibly its -/// version. -/// -/// This function first drops the inputs first 44 chars, since that is exactly -/// the length of the `/nix/store/0004yybkm5hnwjyxv129js3mjp7kbrax-` prefix. -/// Then it matches that against our store path regex. -pub fn parse_name_and_version( - path: &StorePath, -) -> Result<(&str, Option<&str>)> { - static STORE_PATH_REGEX: LazyLock = LazyLock::new(|| { - Regex::new("(.+?)(-([0-9].*?))?$") - .expect("failed to compile regex pattern for nix store paths") - }); - - let path = path.to_str().with_context(|| { - format!( - "failed to convert path '{path}' to valid unicode", - path = path.display(), - ) - })?; - - // We can strip the path since it _always_ follows the format: - // - // /nix/store/0004yybkm5hnwjyxv129js3mjp7kbrax-... - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - // This part is exactly 44 chars long, so we just remove it. - assert_eq!(&path[..11], "/nix/store/"); - assert_eq!(&path[43..44], "-"); - let path = &path[44..]; - - log::debug!("stripped path: {path}"); - - let captures = STORE_PATH_REGEX.captures(path).ok_or_else(|| { - anyhow!("path '{path}' does not match expected Nix store format") - })?; - - let name = captures.get(1).map_or("", |m| m.as_str()); - if name.is_empty() { - bail!("failed to extract name from path '{path}'"); - } - - let version = captures.get(2).map(|m| m.as_str().trim_start_matches('-')); - - Ok((name, version)) -} - -// TODO: move this somewhere else, this does not really -// belong into this file -pub struct PackageDiff<'a> { - pub pkg_to_versions_pre: HashMap<&'a str, HashSet<&'a str>>, - pub pkg_to_versions_post: HashMap<&'a str, HashSet<&'a str>>, - pub pre_keys: HashSet<&'a str>, - pub post_keys: HashSet<&'a str>, - pub added: HashSet<&'a str>, - pub removed: HashSet<&'a str>, - pub changed: HashSet<&'a str>, -} - -impl<'a> PackageDiff<'a> { - pub fn new + 'a>( - pkgs_pre: &'a [S], - pkgs_post: &'a [S], - ) -> Self { - // Map from packages of the first closure to their version - let mut pre = HashMap::<&str, HashSet<&str>>::new(); - let mut post = HashMap::<&str, HashSet<&str>>::new(); - - for p in pkgs_pre { - match get_version(p.as_ref()) { - Ok((name, version)) => { - pre.entry(name).or_default().insert(version); - }, - Err(e) => { - debug!("Error parsing package version: {e}"); - }, - } - } - - for p in pkgs_post { - match get_version(p.as_ref()) { - Ok((name, version)) => { - post.entry(name).or_default().insert(version); - }, - Err(e) => { - debug!("Error parsing package version: {e}"); - }, - } - } - - // Compare the package names of both versions - let pre_keys: HashSet<&str> = pre.keys().copied().collect(); - let post_keys: HashSet<&str> = post.keys().copied().collect(); - - // Difference gives us added and removed packages - let added: HashSet<&str> = &post_keys - &pre_keys; - - let removed: HashSet<&str> = &pre_keys - &post_keys; - // Get the intersection of the package names for version changes - let changed: HashSet<&str> = &pre_keys & &post_keys; - Self { - pkg_to_versions_pre: pre, - pkg_to_versions_post: post, - pre_keys, - post_keys, - added, - removed, - changed, - } - } -} - -mod test { - - #[test] - fn test_version_component_iter() { - use super::VersionComponent::{ - Number, - Text, - }; - use crate::util::VersionComponentIter; - let v = "132.1.2test234-1-man----.--.......---------..---"; - - let comp: Vec<_> = VersionComponentIter::new(v).collect(); - assert_eq!(comp, [ - Number(132), - Number(1), - Number(2), - Text("test".into()), - Number(234), - Number(1), - Text("man".into()) - ]); - } -} diff --git a/src/version.rs b/src/version.rs new file mode 100644 index 0000000..f69535d --- /dev/null +++ b/src/version.rs @@ -0,0 +1,125 @@ +use std::cmp; + +use derive_more::{ + Deref, + DerefMut, + From, +}; +use ref_cast::RefCast; + +#[derive(RefCast, Deref, Debug, PartialEq, Eq)] +#[repr(transparent)] +pub struct Version(str); + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl cmp::Ord for Version { + fn cmp(&self, that: &Self) -> cmp::Ordering { + let this = VersionComponentIter::from(&**self); + let that = VersionComponentIter::from(&**that); + + this.cmp(that) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +enum VersionComponent<'a> { + Number(u64), + Text(&'a str), +} + +impl PartialOrd for VersionComponent<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl cmp::Ord for VersionComponent<'_> { + fn cmp(&self, other: &Self) -> cmp::Ordering { + use VersionComponent::{ + Number, + Text, + }; + + match (self, other) { + (Number(x), Number(y)) => x.cmp(y), + (Text(x), Text(y)) => { + match (*x, *y) { + ("pre", _) => cmp::Ordering::Less, + (_, "pre") => cmp::Ordering::Greater, + _ => x.cmp(y), + } + }, + (Text(_), Number(_)) => cmp::Ordering::Less, + (Number(_), Text(_)) => cmp::Ordering::Greater, + } + } +} + +/// Yields [`VertionComponent`] from a version string. +#[derive(Deref, DerefMut, From)] +struct VersionComponentIter<'a>(&'a str); + +impl<'a> Iterator for VersionComponentIter<'a> { + type Item = VersionComponent<'a>; + + fn next(&mut self) -> Option { + // Skip all '-' and '.'. + while self.starts_with(['.', '-']) { + **self = &self[1..]; + } + + // Get the next character and decide if it is a digit. + let is_digit = self.chars().next()?.is_ascii_digit(); + + // Based on this collect characters after this into the component. + let component_len = self + .chars() + .take_while(|&c| { + c.is_ascii_digit() == is_digit && !matches!(c, '.' | '-') + }) + .map(char::len_utf8) + .sum(); + + let component = &self[..component_len]; + **self = &self[component_len..]; + + assert!(!component.is_empty()); + + if is_digit { + component.parse::().ok().map(VersionComponent::Number) + } else { + Some(VersionComponent::Text(component)) + } + } +} + +#[cfg(test)] +mod tests { + use crate::version::{ + VersionComponent::{ + Number, + Text, + }, + VersionComponentIter, + }; + + #[test] + fn version_component_iter() { + let version = "132.1.2test234-1-man----.--.......---------..---"; + + assert_eq!(VersionComponentIter::from(version).collect::>(), [ + Number(132), + Number(1), + Number(2), + Text("test".into()), + Number(234), + Number(1), + Text("man".into()) + ]); + } +}