mirror of
https://github.com/RGBCube/dix
synced 2025-07-28 04:07:46 +00:00
feat: move parse_name_and_version to StorePath impl, rename util to version.rs
This commit is contained in:
parent
066652cee3
commit
078bc91cf8
3 changed files with 190 additions and 240 deletions
67
src/lib.rs
67
src/lib.rs
|
@ -1,8 +1,17 @@
|
|||
use std::path::{
|
||||
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<regex::Regex> =
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
234
src/util.rs
234
src/util.rs
|
@ -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<T> = std::result::Result<T, AppError>;
|
||||
|
||||
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<cmp::Ordering> {
|
||||
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<Self::Item> {
|
||||
// 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::<u64>().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<Regex> = 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<S: AsRef<str> + '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())
|
||||
]);
|
||||
}
|
||||
}
|
125
src/version.rs
Normal file
125
src/version.rs
Normal file
|
@ -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<cmp::Ordering> {
|
||||
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<cmp::Ordering> {
|
||||
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<Self::Item> {
|
||||
// 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::<u64>().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::<Vec<_>>(), [
|
||||
Number(132),
|
||||
Number(1),
|
||||
Number(2),
|
||||
Text("test".into()),
|
||||
Number(234),
|
||||
Number(1),
|
||||
Text("man".into())
|
||||
]);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue