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
22        .chars()
23        .enumerate()
24        .all(|(i, c)| c.is_ascii_lowercase() || c.is_ascii_digit() || (i > 0 && ".+-".contains(c)))
25    {
26        Err(PackageError::InvalidName)
27    } else {
28        Ok(())
29    }
30}
31
32/// Package errors
33#[derive(Clone, Copy, Debug, Error)]
34pub enum PackageError {
35    #[error("package name too short")]
36    /// Package name is too short
37    InvalidNameLength,
38    #[error("package name contains invalid character")]
39    /// Package name is invalid
40    InvalidName,
41}
42
43/// Package name
44#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
45pub struct PackageName(String);
46
47impl TryFrom<&str> for PackageName {
48    type Error = PackageError;
49
50    fn try_from(package: &str) -> Result<Self, Self::Error> {
51        check_package_name(package).map(|_| Self(package.to_owned()))
52    }
53}
54
55impl TryFrom<String> for PackageName {
56    type Error = PackageError;
57
58    fn try_from(package: String) -> Result<Self, Self::Error> {
59        check_package_name(&package).map(|_| Self(package))
60    }
61}
62
63impl AsRef<str> for PackageName {
64    fn as_ref(&self) -> &str {
65        self.0.as_str()
66    }
67}
68
69impl PartialEq<&str> for PackageName {
70    fn eq(&self, other: &&str) -> bool {
71        self.0.eq(other)
72    }
73}
74
75impl PartialEq<String> for PackageName {
76    fn eq(&self, other: &String) -> bool {
77        self.0.eq(other)
78    }
79}
80
81impl Display for PackageName {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        write!(f, "{}", self.0)
84    }
85}
86
87impl<'de> Deserialize<'de> for PackageName {
88    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
89    where
90        D: serde::Deserializer<'de>,
91    {
92        deserializer.deserialize_str(TryFromStrVisitor::new("a package name"))
93    }
94}
95
96/// A package together with its version
97#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
98pub struct VersionedPackage {
99    /// The package name
100    pub package: PackageName,
101    /// The package version
102    pub version: PackageVersion,
103}
104
105impl AsRef<PackageName> for VersionedPackage {
106    fn as_ref(&self) -> &PackageName {
107        &self.package
108    }
109}
110
111impl AsRef<PackageVersion> for VersionedPackage {
112    fn as_ref(&self) -> &PackageVersion {
113        &self.version
114    }
115}
116
117#[cfg(test)]
118mod test {
119    use super::*;
120
121    #[test]
122    fn valid_package_names() {
123        assert!(PackageName::try_from("zathura").is_ok());
124        assert!(PackageName::try_from("0ad").is_ok());
125        assert!(PackageName::try_from("zathura-pdf").is_ok());
126    }
127
128    #[test]
129    fn invalid_package_names() {
130        assert!(PackageName::try_from("z").is_err());
131        assert!(PackageName::try_from("-ad").is_err());
132    }
133}