Skip to main content

fslite_core/
path.rs

1use std::fmt;
2use std::sync::Arc;
3
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5
6use crate::{FsError, FsResult};
7
8/// An absolute, normalized path within a filesystem workspace.
9#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10pub struct VirtualPath {
11    canonical: Arc<str>,
12}
13
14impl VirtualPath {
15    /// Parses and normalizes an absolute virtual path.
16    pub fn parse(input: &str) -> FsResult<Self> {
17        if !input.starts_with('/') || input.contains('\0') {
18            return Err(FsError::invalid_path_or_name(input));
19        }
20
21        Ok(Self {
22            canonical: normalize(input.split('/')),
23        })
24    }
25
26    /// Returns the workspace root path.
27    pub fn root() -> Self {
28        Self {
29            canonical: Arc::from("/"),
30        }
31    }
32
33    /// Returns the canonical absolute path.
34    pub fn as_str(&self) -> &str {
35        &self.canonical
36    }
37
38    /// Returns the final path segment, or `None` for the root.
39    pub fn name(&self) -> Option<&str> {
40        if self.canonical.len() == 1 {
41            None
42        } else {
43            self.canonical.rsplit('/').next()
44        }
45    }
46
47    /// Returns the normalized parent, or `None` for the root.
48    pub fn parent(&self) -> Option<Self> {
49        let separator = self.canonical.rfind('/')?;
50        if separator == 0 {
51            if self.canonical.len() == 1 {
52                None
53            } else {
54                Some(Self::root())
55            }
56        } else {
57            Some(Self {
58                canonical: Arc::from(&self.canonical[..separator]),
59            })
60        }
61    }
62
63    /// Resolves a relative path against this path without escaping the root.
64    pub fn join(&self, relative: &str) -> FsResult<Self> {
65        if relative.starts_with('/') || relative.contains('\0') {
66            return Err(FsError::invalid_path_or_name(relative));
67        }
68
69        Ok(Self {
70            canonical: normalize(self.segments().chain(relative.split('/'))),
71        })
72    }
73
74    /// Iterates over canonical path segments from root to leaf.
75    pub fn segments(&self) -> impl Iterator<Item = &str> {
76        self.canonical[1..]
77            .split('/')
78            .filter(|segment| !segment.is_empty())
79    }
80}
81
82impl AsRef<str> for VirtualPath {
83    fn as_ref(&self) -> &str {
84        self.as_str()
85    }
86}
87
88impl fmt::Display for VirtualPath {
89    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90        formatter.write_str(self.as_str())
91    }
92}
93
94impl Serialize for VirtualPath {
95    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
96    where
97        S: Serializer,
98    {
99        serializer.serialize_str(self.as_str())
100    }
101}
102
103impl<'de> Deserialize<'de> for VirtualPath {
104    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
105    where
106        D: Deserializer<'de>,
107    {
108        let input = String::deserialize(deserializer)?;
109        Self::parse(&input).map_err(serde::de::Error::custom)
110    }
111}
112
113/// A normalized absolute or relative target stored by a symbolic link.
114///
115/// Relative targets retain leading `..` segments so a filesystem backend can
116/// resolve them against the link's containing directory without allowing the
117/// resolved [`VirtualPath`] to escape its workspace root.
118#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
119pub struct LinkTarget {
120    canonical: Arc<str>,
121}
122
123impl LinkTarget {
124    /// Parses and normalizes a symbolic-link target.
125    ///
126    /// # Errors
127    ///
128    /// Returns an error when the target is empty or contains a NUL byte.
129    pub fn parse(input: &str) -> FsResult<Self> {
130        if input.is_empty() || input.contains('\0') {
131            return Err(FsError::invalid_path_or_name(input));
132        }
133
134        let canonical = if input.starts_with('/') {
135            normalize(input.split('/'))
136        } else {
137            normalize_relative(input.split('/'))
138        };
139
140        Ok(Self { canonical })
141    }
142
143    /// Returns the normalized target string.
144    pub fn as_str(&self) -> &str {
145        &self.canonical
146    }
147
148    /// Returns whether this target is absolute.
149    pub fn is_absolute(&self) -> bool {
150        self.canonical.starts_with('/')
151    }
152}
153
154impl AsRef<str> for LinkTarget {
155    fn as_ref(&self) -> &str {
156        self.as_str()
157    }
158}
159
160impl fmt::Display for LinkTarget {
161    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
162        formatter.write_str(self.as_str())
163    }
164}
165
166impl Serialize for LinkTarget {
167    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
168    where
169        S: Serializer,
170    {
171        serializer.serialize_str(self.as_str())
172    }
173}
174
175impl<'de> Deserialize<'de> for LinkTarget {
176    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
177    where
178        D: Deserializer<'de>,
179    {
180        let input = String::deserialize(deserializer)?;
181        Self::parse(&input).map_err(serde::de::Error::custom)
182    }
183}
184
185fn normalize<'a>(segments: impl IntoIterator<Item = &'a str>) -> Arc<str> {
186    let mut normalized = Vec::new();
187
188    for segment in segments {
189        match segment {
190            "" | "." => {}
191            ".." => {
192                normalized.pop();
193            }
194            _ => normalized.push(segment),
195        }
196    }
197
198    if normalized.is_empty() {
199        return Arc::from("/");
200    }
201
202    let capacity = normalized.iter().map(|segment| segment.len() + 1).sum();
203    let mut canonical = String::with_capacity(capacity);
204    for segment in normalized {
205        canonical.push('/');
206        canonical.push_str(segment);
207    }
208    Arc::from(canonical)
209}
210
211fn normalize_relative<'a>(segments: impl IntoIterator<Item = &'a str>) -> Arc<str> {
212    let mut normalized = Vec::new();
213
214    for segment in segments {
215        match segment {
216            "" | "." => {}
217            ".." if normalized.last().is_some_and(|last| *last != "..") => {
218                normalized.pop();
219            }
220            ".." => normalized.push(segment),
221            _ => normalized.push(segment),
222        }
223    }
224
225    if normalized.is_empty() {
226        return Arc::from(".");
227    }
228
229    Arc::from(normalized.join("/"))
230}