assorted_debian_utils/
package.rs

1// Copyright 2025 Sebastian Ramacher
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4//! # Helpers to handle Debian packages
5//!
6//! These helpers includes abstractions to check the validity of Debian packages names.
7
8use std::fmt::Display;
9
10use serde::Deserialize;
11use thiserror::Error;
12
13use crate::{utils::TryFromStrVisitor, version::PackageVersion};
14
15fn check_package_name(package: &str) -> Result<(), PackageError> {
16    // package names must be at least 2 characters long
17    if package.len() < 2 {
18        return Err(PackageError::InvalidNameLength);
19    }
20
21    if !package.chars().enumerate().all(|(i, c)| {
22        if c.is_ascii_lowercase() || c.is_ascii_digit() {
23            return true;
24        }
25        i > 0 && ".+-".contains(c)
26    }) {
27        return Err(PackageError::InvalidName);
28    }
29
30    Ok(())
31}
32
33/// Package errors
34#[derive(Clone, Copy, Debug, Error)]
35pub enum PackageError {
36    #[error("package name too short")]
37    /// Package name is too short
38    InvalidNameLength,
39    #[error("package name contains invalid character")]
40    /// Package name is invalid
41    InvalidName,
42}
43
44/// Package name
45#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
46pub struct PackageName(String);
47
48impl TryFrom<&str> for PackageName {
49    type Error = PackageError;
50
51    fn try_from(package: &str) -> Result<Self, Self::Error> {
52        check_package_name(package).map(|_| Self(package.to_owned()))
53    }
54}
55
56impl TryFrom<String> for PackageName {
57    type Error = PackageError;
58
59    fn try_from(package: String) -> Result<Self, Self::Error> {
60        check_package_name(&package).map(|_| Self(package))
61    }
62}
63
64impl AsRef<str> for PackageName {
65    fn as_ref(&self) -> &str {
66        self.0.as_str()
67    }
68}
69
70impl PartialEq<&str> for PackageName {
71    fn eq(&self, other: &&str) -> bool {
72        self.0.eq(other)
73    }
74}
75
76impl PartialEq<String> for PackageName {
77    fn eq(&self, other: &String) -> bool {
78        self.0.eq(other)
79    }
80}
81
82impl Display for PackageName {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        write!(f, "{}", self.0)
85    }
86}
87
88impl<'de> Deserialize<'de> for PackageName {
89    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
90    where
91        D: serde::Deserializer<'de>,
92    {
93        deserializer.deserialize_str(TryFromStrVisitor::new("a package name"))
94    }
95}
96
97/// A package together with its version
98#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
99pub struct VersionedPackage {
100    /// The package name
101    pub package: PackageName,
102    /// The package version
103    pub version: PackageVersion,
104}
105
106impl AsRef<PackageName> for VersionedPackage {
107    fn as_ref(&self) -> &PackageName {
108        &self.package
109    }
110}
111
112impl AsRef<PackageVersion> for VersionedPackage {
113    fn as_ref(&self) -> &PackageVersion {
114        &self.version
115    }
116}
117
118#[cfg(test)]
119mod test {
120    use super::*;
121
122    #[test]
123    fn valid_package_names() {
124        assert!(PackageName::try_from("zathura").is_ok());
125        assert!(PackageName::try_from("0ad").is_ok());
126        assert!(PackageName::try_from("zathura-pdf").is_ok());
127    }
128
129    #[test]
130    fn invalid_package_names() {
131        assert!(PackageName::try_from("z").is_err());
132        assert!(PackageName::try_from("-ad").is_err());
133    }
134}