Skip to main content

kellnr_common/
original_name.rs

1use std::convert::TryFrom;
2use std::fmt;
3use std::ops::Deref;
4use std::sync::LazyLock;
5
6use regex::Regex;
7use sea_orm::Value;
8use thiserror::Error;
9use utoipa::ToSchema;
10
11use crate::canonical_name::CanonicalName;
12
13static NAME_REGEX: LazyLock<Regex> =
14    LazyLock::new(|| Regex::new(r"^[a-zA-Z][a-zA-Z0-9-_]*$").unwrap());
15
16#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Clone, Hash, ToSchema)]
17#[schema(value_type = String)]
18// Validate the name on every deserialization (route params, JSON bodies, cached
19// index data) instead of only in the explicit `TryFrom` constructor. Otherwise a
20// name carrying `..`, `/`, etc. can slip through serde-based extractors and reach
21// filesystem paths. Mirrors the custom `Deserialize` on `Version`.
22#[serde(try_from = "String")]
23pub struct OriginalName(String);
24
25#[derive(Debug, PartialEq, Eq, Error)]
26pub enum NameError {
27    #[error("Invalid character in name")]
28    InvalidCharacter,
29    #[error("Invalid length for name")]
30    InvalidLength,
31}
32
33impl OriginalName {
34    pub fn to_canonical(&self) -> CanonicalName {
35        CanonicalName::from(self)
36    }
37    pub fn from_unchecked(name: String) -> Self {
38        Self(name)
39    }
40    pub fn into_inner(self) -> String {
41        self.0
42    }
43}
44
45impl TryFrom<String> for OriginalName {
46    type Error = NameError;
47
48    fn try_from(package_name: String) -> Result<Self, Self::Error> {
49        if !NAME_REGEX.is_match(&package_name) {
50            Err(NameError::InvalidCharacter)
51        } else if package_name.len() > 64 {
52            Err(NameError::InvalidLength)
53        } else {
54            Ok(OriginalName(package_name))
55        }
56    }
57}
58
59impl From<&OriginalName> for String {
60    fn from(name: &OriginalName) -> Self {
61        name.to_string()
62    }
63}
64
65impl From<OriginalName> for String {
66    fn from(name: OriginalName) -> Self {
67        name.to_string()
68    }
69}
70
71impl From<OriginalName> for Value {
72    fn from(value: OriginalName) -> Self {
73        Value::String(Some(value.0))
74    }
75}
76
77impl From<&OriginalName> for Value {
78    fn from(value: &OriginalName) -> Self {
79        Value::String(Some(value.0.clone()))
80    }
81}
82
83impl TryFrom<&String> for OriginalName {
84    type Error = NameError;
85
86    fn try_from(package_name: &String) -> Result<Self, Self::Error> {
87        OriginalName::try_from(package_name.clone())
88    }
89}
90
91impl TryFrom<&str> for OriginalName {
92    type Error = NameError;
93
94    fn try_from(package_name: &str) -> Result<Self, Self::Error> {
95        OriginalName::try_from(package_name.to_string())
96    }
97}
98
99impl Deref for OriginalName {
100    type Target = String;
101
102    fn deref(&self) -> &Self::Target {
103        &self.0
104    }
105}
106
107impl fmt::Display for OriginalName {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        write!(f, "{}", self.0)
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn valid_package_names() {
119        assert_eq!(
120            OriginalName::try_from("test-lib").unwrap(),
121            OriginalName("test-lib".to_string())
122        );
123        assert_eq!(
124            OriginalName::try_from("Test_lib").unwrap(),
125            OriginalName("Test_lib".to_string())
126        );
127        assert_eq!(
128            OriginalName::try_from("test_lib_foo").unwrap(),
129            OriginalName("test_lib_foo".to_string())
130        );
131        assert_eq!(
132            OriginalName::try_from("test-lIB-foo").unwrap(),
133            OriginalName("test-lIB-foo".to_string())
134        );
135        assert_eq!(
136            OriginalName::try_from("test12_14f").unwrap(),
137            OriginalName("test12_14f".to_string())
138        );
139        assert_eq!(
140            OriginalName::try_from("tEs2-23_1f").unwrap(),
141            OriginalName("tEs2-23_1f".to_string())
142        );
143        assert_eq!(
144            OriginalName::try_from("testlib").unwrap(),
145            OriginalName("testlib".to_string())
146        );
147        assert_eq!(
148            OriginalName::try_from("Testlib23").unwrap(),
149            OriginalName("Testlib23".to_string())
150        );
151    }
152
153    #[test]
154    fn invalid_characters_in_package_name() {
155        assert_eq!(
156            OriginalName::try_from("_test").unwrap_err(),
157            NameError::InvalidCharacter
158        );
159        assert_eq!(
160            OriginalName::try_from("44Test").unwrap_err(),
161            NameError::InvalidCharacter
162        );
163        assert_eq!(
164            OriginalName::try_from("-Av").unwrap_err(),
165            NameError::InvalidCharacter
166        );
167        assert_eq!(
168            OriginalName::try_from("testÄ").unwrap_err(),
169            NameError::InvalidCharacter
170        );
171        assert_eq!(
172            OriginalName::try_from("test?").unwrap_err(),
173            NameError::InvalidCharacter
174        );
175        assert_eq!(
176            OriginalName::try_from(".45A").unwrap_err(),
177            NameError::InvalidCharacter
178        );
179        assert_eq!(
180            OriginalName::try_from("").unwrap_err(),
181            NameError::InvalidCharacter
182        );
183    }
184
185    #[test]
186    fn deserialize_accepts_valid_name() {
187        let name: OriginalName = serde_json::from_str(r#""test-lib""#).unwrap();
188        assert_eq!(name, OriginalName("test-lib".to_string()));
189    }
190
191    #[test]
192    fn deserialize_rejects_invalid_name() {
193        // Traversal / separator characters must be rejected by serde itself,
194        // not silently wrapped into an OriginalName.
195        assert!(serde_json::from_str::<OriginalName>(r#""../etc""#).is_err());
196        assert!(serde_json::from_str::<OriginalName>(r#""a/b""#).is_err());
197        assert!(serde_json::from_str::<OriginalName>(r#""..""#).is_err());
198    }
199
200    #[test]
201    fn too_long_package_name() {
202        assert_eq!(
203            OriginalName::try_from(
204                "zbsfrofdgxekytxrporaocoieaviehlvjrroockxufdkzgtxudkmdkentyyhkmtpx"
205            )
206            .unwrap_err(),
207            NameError::InvalidLength
208        );
209    }
210}