jotta_osd/
path.rs

1//! Path utilities.
2
3use derive_more::{AsRef, Deref, DerefMut};
4use once_cell::sync::Lazy;
5use regex::Regex;
6
7use serde_with::{DeserializeFromStr, SerializeDisplay};
8use std::{fmt::Display, str::FromStr, string::FromUtf8Error};
9
10/// A human-readable object name.
11///
12/// ```
13/// use jotta_osd::path::ObjectName;
14/// use std::str::FromStr;
15///
16/// assert!(ObjectName::from_str("").is_err());
17/// assert!(ObjectName::from_str("hello\nworld").is_err());
18/// assert!(ObjectName::from_str("bye\r\nlword").is_err());
19/// ```
20#[derive(
21    Debug,
22    SerializeDisplay,
23    DeserializeFromStr,
24    Clone,
25    PartialEq,
26    Eq,
27    PartialOrd,
28    Ord,
29    Deref,
30    DerefMut,
31    AsRef,
32)]
33pub struct ObjectName(String);
34
35impl ObjectName {
36    /// Convert the object name to hexadecimal.
37    ///
38    /// ```
39    /// use jotta_osd::path::ObjectName;
40    /// use std::str::FromStr;
41    ///
42    /// # fn main() -> Result<(), jotta_osd::path::ParseObjectNameError> {
43    /// let name = ObjectName::from_str("cat.jpeg")?;
44    ///
45    /// assert_eq!(name.to_hex(), "6361742e6a706567");
46    /// # Ok(())
47    /// # }
48    /// ```
49    #[must_use]
50    pub fn to_hex(&self) -> String {
51        hex::encode(&self.0)
52    }
53
54    /// Convert a hexadecimal string to an [`ObjectName`].
55    ///
56    /// # Errors
57    ///
58    /// Errors only if the hexadecimal value cannot be parsed;
59    /// It is not  as restrictive as the [`FromStr`] implementation.
60    pub fn try_from_hex(hex: &str) -> Result<Self, ParseObjectNameError> {
61        let bytes = hex::decode(hex)?;
62        let text = String::from_utf8(bytes)?;
63        Ok(Self(text))
64    }
65
66    pub(crate) fn chunk_path(&self, index: u32) -> String {
67        format!("{}/{}", self.to_hex(), index)
68    }
69}
70
71impl Display for ObjectName {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        write!(f, "{}", self.0)
74    }
75}
76
77impl FromStr for ObjectName {
78    type Err = ParseObjectNameError;
79
80    fn from_str(s: &str) -> Result<Self, Self::Err> {
81        if !(1..=1024).contains(&s.len()) {
82            return Err(Self::Err::InvalidLength);
83        }
84
85        for c in s.chars() {
86            if c.is_ascii_control() {
87                return Err(Self::Err::IllegalChar(c));
88            }
89        }
90
91        Ok(Self(s.into()))
92    }
93}
94
95/// Object name parse errors.
96#[derive(Debug, thiserror::Error)]
97pub enum ParseObjectNameError {
98    /// Hexadecimal parse error.
99    #[error("invalid hex: {0}")]
100    InvalidHex(#[from] hex::FromHexError),
101
102    /// Invalid unicode.
103    #[error("invalid utf-8: {0}")]
104    InvalidUtf8(#[from] FromUtf8Error),
105
106    /// Some characters, such as the newline (`\n`), are banned in
107    /// object names.
108    #[error("invalid character: `{0}`")]
109    IllegalChar(char),
110
111    /// The object name must be between 1 and 1024 characters long.
112    #[error("invalid name length")]
113    InvalidLength,
114}
115
116/// A bucket name
117///
118/// ```
119/// use jotta_osd::path::BucketName;
120/// use std::str::FromStr;
121///
122/// assert!(BucketName::from_str("hello").is_ok());
123/// assert!(BucketName::from_str("...").is_err()); // dots are not allowed
124/// assert!(BucketName::from_str(&"a".repeat(100)).is_err()); // maximum 63 characters long
125/// assert!(BucketName::from_str("AAAAAAAAAAAAAAAAAAA").is_err()); // uppercase letters are banned
126/// assert!(BucketName::from_str("e").is_err()); // bucket names must be at least 3 characters long
127/// assert!(BucketName::from_str("-a-").is_err()); // bucket names must start and end with alphanumerics
128/// ```
129#[derive(
130    Debug,
131    SerializeDisplay,
132    DeserializeFromStr,
133    Clone,
134    PartialEq,
135    Eq,
136    PartialOrd,
137    Ord,
138    Deref,
139    DerefMut,
140    AsRef,
141)]
142pub struct BucketName(pub(crate) String);
143
144static BUCKET_RE: Lazy<Regex> =
145    Lazy::new(|| Regex::new(r"^[a-z0-9][a-z0-9\-]{1,61}[a-z0-9]$").unwrap());
146
147impl Display for BucketName {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        write!(f, "{}", self.0)
150    }
151}
152
153impl FromStr for BucketName {
154    type Err = ParseBucketNameError;
155
156    fn from_str(s: &str) -> Result<Self, Self::Err> {
157        if BUCKET_RE.is_match(s) {
158            Ok(Self(s.into()))
159        } else {
160            Err(ParseBucketNameError::InvalidName)
161        }
162    }
163}
164
165/// Bucket name parsing error.
166#[derive(Debug, thiserror::Error)]
167pub enum ParseBucketNameError {
168    /// Invalid bucket name.
169    #[error(
170        "bucket names must be between 3 and 63 characters long, \
171  only contain alphanumerics and dashes, and must not begin or end \
172  with a dash (-)"
173    )]
174    InvalidName,
175}