From 335d13ca335817277f379b3e77de0cd06ecaa17b Mon Sep 17 00:00:00 2001 From: David Runge <dvzrv@archlinux.org> Date: Wed, 22 Mar 2023 16:45:02 +0100 Subject: [PATCH] feat: Implement Name type Add a Name type to describe package names. Use proptest to define generic test scenarios for valid and invalid package name strings. --- .ci/check.sh | 2 +- Cargo.toml | 3 ++ README.md | 11 +++++++ src/error.rs | 4 +++ src/lib.rs | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/macros.rs | 23 ++++++++++++++ 6 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 src/macros.rs diff --git a/.ci/check.sh b/.ci/check.sh index 32df9f8..32df5ca 100755 --- a/.ci/check.sh +++ b/.ci/check.sh @@ -8,5 +8,5 @@ set -euxo pipefail cargo fmt -- --check cargo clippy --all -- -D warnings cargo test --all -codespell --skip '.cargo,.git,target' +codespell --skip '.cargo,.git,target' --ignore-words-list 'crate' reuse lint diff --git a/Cargo.toml b/Cargo.toml index 8458bf7..206e377 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,12 @@ version = "0.0.0" [dependencies] chrono = "0.4.23" +once_cell = "1.17.1" +regex = "1.7.2" strum = "0.24.1" strum_macros = "0.24.3" thiserror = "1.0.40" [dev-dependencies] +proptest = "1.1.0" rstest = "0.17.0" diff --git a/README.md b/README.md index 5f17c5a..d90c96c 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,17 @@ use std::str::FromStr; assert_eq!(InstalledSize::from_str("1"), Ok(InstalledSize::new(1))); ``` +The name for a package is restricted to a specific set of characters. +You can create `Name` directly or from str, which yields a Result: + +```rust +use std::str::FromStr; +use alpm_types::{Error, Name}; + +assert_eq!(Name::from_str("test-123@.foo_+"), Ok(Name::new("test-123@.foo_+"))); +assert_eq!(Name::from_str(".foo"), Err(Error::InvalidName(".foo".to_string()))); +``` + Package types are distinguished using the `PkgType` enum. Its variants can be constructed from str: ```rust diff --git a/src/error.rs b/src/error.rs index f66ca42..bdefa21 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,6 +15,9 @@ pub enum Error { /// An invalid installed package size (in bytes) #[error("Invalid installed size: {0}")] InvalidInstalledSize(String), + /// An invalid package name + #[error("Invalid package name: {0}")] + InvalidName(String), } #[cfg(test)] @@ -32,6 +35,7 @@ mod tests { "Invalid installed size: -1", Error::InvalidInstalledSize(String::from("-1")) )] + #[case("Invalid package name: -1", Error::InvalidName(String::from("-1")))] fn error_format_string(#[case] error_str: &str, #[case] error: Error) { assert_eq!(error_str, format!("{}", error)); } diff --git a/src/lib.rs b/src/lib.rs index cde989d..f352409 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,9 @@ use strum_macros::EnumString; mod error; pub use error::Error; +mod macros; +use macros::regex_once; + /// CPU architecture /// /// Members of the Architecture enum can be created from `&str`. @@ -229,6 +232,69 @@ impl Display for InstalledSize { } } +/// A package name +/// +/// Package names may contain the characters `[a-z\d\-._@+]`, but must not +/// start with `[-.]`. +/// +/// ## Examples +/// ``` +/// use alpm_types::{Name, Error}; +/// use std::str::FromStr; +/// +/// // create Name from &str +/// assert_eq!( +/// Name::from_str("test-123@.foo_+"), +/// Ok(Name::new("test-123@.foo_+").unwrap()) +/// ); +/// assert_eq!( +/// Name::from_str(".test"), +/// Err(Error::InvalidName(".test".to_string())) +/// ); +/// +/// // format as String +/// assert_eq!("foo", format!("{}", Name::new("foo").unwrap())); +/// ``` +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Name { + name: String, +} + +impl Name { + /// Create a new Name in a Result + pub fn new(name: &str) -> Result<Name, Error> { + Name::validate(name) + } + + /// Validate a string and return a Name in a Result + /// + /// The validation happens on the basis of the allowed characters as + /// defined by the Name type. + pub fn validate(name: &str) -> Result<Name, Error> { + if regex_once!(r"^[a-z\d_@+]+[a-z\d\-._@+]*$").is_match(name) { + Ok(Name { + name: name.to_string(), + }) + } else { + Err(Error::InvalidName(name.to_string())) + } + } +} + +impl FromStr for Name { + type Err = Error; + /// Create a Name from a string + fn from_str(input: &str) -> Result<Name, Self::Err> { + Name::validate(input) + } +} + +impl Display for Name { + fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result { + write!(fmt, "{}", self.name) + } +} + /// The type of a package /// /// ## Examples @@ -266,6 +332,7 @@ pub enum PkgType { mod tests { use super::*; use chrono::NaiveDateTime; + use proptest::prelude::*; use rstest::rstest; use strum::ParseError; @@ -361,6 +428,22 @@ mod tests { assert_eq!("1", format!("{}", InstalledSize::new(1))); } + proptest! { + #![proptest_config(ProptestConfig::with_cases(1000))] + + #[test] + fn valid_name_from_string(name_str in r"[a-z\d_@+]+[a-z\d\-._@+]*") { + let name = Name::from_str(&name_str).unwrap(); + prop_assert_eq!(name_str, format!("{}", name)); + } + + #[test] + fn invalid_name_from_string_start(name_str in r"[\-.]+[a-z\d\-._@+]*") { + let error = Name::from_str(&name_str).unwrap_err(); + assert!(format!("{}", error).ends_with(&name_str)); + } + } + #[rstest] #[case("debug", Ok(PkgType::Debug))] #[case("pkg", Ok(PkgType::Package))] diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..472daa2 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2023 David Runge <dvzrv@archlinux.org> +// SPDX-License-Identifier: LGPL-3.0-or-later + +/// A convenient way to create a regular expression only once +/// +/// A string literal as input is used to define the regular expression. +/// With the help of OnceCell the regular expression is created only once. +/// +/// ## Examples +/// ``` +/// #[macro_use] extern crate alpm_types; +/// +/// let re = regex_once!("^(foo)$"); +/// assert!(re.is_match("foo")); +/// ``` +macro_rules! regex_once { + ($re:literal $(,)?) => {{ + static RE: once_cell::sync::OnceCell<regex::Regex> = once_cell::sync::OnceCell::new(); + RE.get_or_init(|| regex::Regex::new($re).unwrap()) + }}; +} + +pub(crate) use regex_once; -- GitLab