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::{borrow::Borrow, 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 Borrow<str> for PackageName {
70    fn borrow(&self) -> &str {
71        self.0.as_str()
72    }
73}
74
75impl PartialEq<&str> for PackageName {
76    fn eq(&self, other: &&str) -> bool {
77        self.0.eq(other)
78    }
79}
80
81impl PartialEq<String> for PackageName {
82    fn eq(&self, other: &String) -> bool {
83        self.0.eq(other)
84    }
85}
86
87impl Display for PackageName {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        write!(f, "{}", self.0)
90    }
91}
92
93impl<'de> Deserialize<'de> for PackageName {
94    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
95    where
96        D: serde::Deserializer<'de>,
97    {
98        deserializer.deserialize_str(TryFromStrVisitor::new("a package name"))
99    }
100}
101
102/// A package together with its version
103#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
104pub struct VersionedPackage {
105    /// The package name
106    pub package: PackageName,
107    /// The package version
108    pub version: PackageVersion,
109}
110
111impl AsRef<PackageName> for VersionedPackage {
112    fn as_ref(&self) -> &PackageName {
113        &self.package
114    }
115}
116
117impl AsRef<PackageVersion> for VersionedPackage {
118    fn as_ref(&self) -> &PackageVersion {
119        &self.version
120    }
121}
122
123#[cfg(test)]
124mod test {
125    use super::*;
126
127    #[test]
128    fn valid_package_names() {
129        assert!(PackageName::try_from("zathura").is_ok());
130        assert!(PackageName::try_from("0ad").is_ok());
131        assert!(PackageName::try_from("zathura-pdf").is_ok());
132    }
133
134    #[test]
135    fn invalid_package_names() {
136        assert!(PackageName::try_from("z").is_err());
137        assert!(PackageName::try_from("-ad").is_err());
138    }
139}