Skip to main content

kellnr_common/
original_name.rs

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