Skip to main content

clankerdiff_core/models/
repo_path.rs

1use crate::RepoPathError;
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3use std::{fmt, sync::Arc};
4
5/// A validated UTF-8 path relative to a repository root.
6#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
7pub struct RepoPath(Arc<str>);
8
9impl RepoPath {
10    /// Validates and stores a repository-relative UTF-8 path.
11    ///
12    /// # Errors
13    ///
14    /// Returns an error for empty, absolute, traversing, or NUL-containing paths.
15    pub fn new(path: impl AsRef<str>) -> Result<Self, RepoPathError> {
16        let raw = path.as_ref();
17        if raw.is_empty() {
18            return Err(RepoPathError::Empty);
19        }
20        if raw.bytes().any(|byte| byte == 0) {
21            return Err(RepoPathError::Nul);
22        }
23        if raw.starts_with('/') || raw.starts_with("\\\\") || raw.as_bytes().get(1) == Some(&b':') {
24            return Err(RepoPathError::Absolute);
25        }
26        for component in raw.split('/') {
27            match component {
28                "" => return Err(RepoPathError::Empty),
29                "." | ".." => return Err(RepoPathError::Traversal),
30                _ => {}
31            }
32        }
33        Ok(Self(Arc::from(raw)))
34    }
35
36    /// Returns the validated Git path.
37    #[must_use]
38    pub fn as_str(&self) -> &str {
39        self.0.as_ref()
40    }
41}
42
43impl AsRef<str> for RepoPath {
44    fn as_ref(&self) -> &str {
45        self.as_str()
46    }
47}
48
49impl fmt::Display for RepoPath {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        f.write_str(self.as_str())
52    }
53}
54
55impl TryFrom<String> for RepoPath {
56    type Error = RepoPathError;
57    fn try_from(value: String) -> Result<Self, Self::Error> {
58        Self::new(value)
59    }
60}
61
62impl TryFrom<&str> for RepoPath {
63    type Error = RepoPathError;
64    fn try_from(value: &str) -> Result<Self, Self::Error> {
65        Self::new(value)
66    }
67}
68
69impl Serialize for RepoPath {
70    fn serialize<T: Serializer>(&self, serializer: T) -> Result<T::Ok, T::Error> {
71        serializer.serialize_str(self.as_str())
72    }
73}
74
75impl<'de> Deserialize<'de> for RepoPath {
76    fn deserialize<T: Deserializer<'de>>(deserializer: T) -> Result<Self, T::Error> {
77        Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
78    }
79}