1use std::fmt::Display;
12use std::fmt::Formatter;
13use std::fmt::Result as FmtResult;
14
15use super::PathComponent;
16use super::PathComponents;
17use super::PathSemantics;
18use super::RelativePath;
19use crate::error::FsError;
20use crate::error::FsOperation;
21use crate::error::FsResult;
22
23#[derive(Clone, Debug, Eq, Hash, PartialEq)]
36pub struct Path {
37 absolute: bool,
39 text: String,
41 literal: bool,
43 semantics: PathSemantics,
45}
46
47impl Path {
48 #[inline]
50 #[must_use]
51 pub fn root() -> Self {
52 Self {
53 absolute: true,
54 text: "/".to_owned(),
55 literal: false,
56 semantics: PathSemantics::Hierarchical,
57 }
58 }
59
60 #[inline]
74 pub fn from_components<I, S>(absolute: bool, components: I) -> FsResult<Self>
75 where
76 I: IntoIterator<Item = S>,
77 S: AsRef<str>,
78 {
79 let components = components
80 .into_iter()
81 .map(|value| PathComponent::parse(value.as_ref()))
82 .collect::<FsResult<Vec<_>>>()?;
83 if !absolute && components.is_empty() {
84 return Err(invalid_path());
85 }
86 let joined = components
87 .iter()
88 .map(PathComponent::as_str)
89 .collect::<Vec<_>>()
90 .join("/");
91 Ok(Self {
92 absolute,
93 text: if absolute {
94 if joined.is_empty() {
95 "/".to_owned()
96 } else {
97 format!("/{joined}")
98 }
99 } else {
100 joined
101 },
102 literal: false,
103 semantics: PathSemantics::Hierarchical,
104 })
105 }
106
107 #[inline]
115 pub fn parse(text: &str) -> FsResult<Self> {
116 Self::parse_with_semantics(text, PathSemantics::Hierarchical)
117 }
118
119 #[inline]
127 pub fn parse_literal(text: &str) -> FsResult<Self> {
128 Self::parse_with_semantics(text, PathSemantics::ObjectKey)
129 }
130
131 pub fn parse_with_semantics(text: &str, semantics: PathSemantics) -> FsResult<Self> {
144 if text.is_empty() || text.contains('\0') {
145 return Err(invalid_path());
146 }
147 if semantics != PathSemantics::Hierarchical {
148 return Ok(Self {
149 absolute: text.starts_with('/'),
150 text: text.to_owned(),
151 literal: true,
152 semantics,
153 });
154 }
155 let absolute = text.starts_with('/');
156 let mut components = Vec::new();
157 for component in text.split('/') {
158 match component {
159 "" | "." => {}
160 ".." => {
161 if components.pop().is_none() {
162 return Err(invalid_path());
163 }
164 }
165 value => components.push(value),
166 }
167 }
168 let text = if absolute {
169 if components.is_empty() {
170 "/".to_owned()
171 } else {
172 format!("/{}", components.join("/"))
173 }
174 } else {
175 components.join("/")
176 };
177 if text.is_empty() {
178 return Err(invalid_path());
179 }
180 Ok(Self {
181 absolute,
182 text,
183 literal: false,
184 semantics,
185 })
186 }
187
188 #[inline]
190 #[must_use]
191 pub fn as_str(&self) -> &str {
192 &self.text
193 }
194
195 #[inline]
205 #[must_use]
206 pub fn file_name(&self) -> Option<&str> {
207 if self.text == "/" || (self.literal && self.text.ends_with('/')) {
208 return None;
209 }
210 self.text.rsplit('/').find(|component| !component.is_empty())
211 }
212
213 #[inline]
215 #[must_use]
216 pub const fn is_absolute(&self) -> bool {
217 self.absolute
218 }
219
220 #[inline]
222 #[must_use]
223 pub const fn semantics(&self) -> PathSemantics {
224 self.semantics
225 }
226
227 #[inline]
229 #[must_use]
230 pub fn components(&self) -> PathComponents<'_> {
231 PathComponents::new(&self.text, self.absolute, self.literal)
232 }
233
234 #[inline]
236 #[must_use]
237 pub fn child(&self, component: &PathComponent) -> Self {
238 self.append(component.as_str())
239 }
240
241 #[inline]
244 #[must_use]
245 pub fn join(&self, relative: &RelativePath) -> Self {
246 self.append(relative.as_str())
247 }
248
249 fn append(&self, suffix: &str) -> Self {
251 let text = if self.text == "/" {
252 format!("/{suffix}")
253 } else {
254 format!("{}/{}", self.text, suffix)
255 };
256 Self {
257 absolute: self.absolute,
258 text,
259 literal: self.literal,
260 semantics: self.semantics,
261 }
262 }
263}
264
265impl Display for Path {
266 #[inline]
268 fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult {
269 formatter.write_str(self.as_str())
270 }
271}
272
273impl AsRef<str> for Path {
274 #[inline]
276 fn as_ref(&self) -> &str {
277 self.as_str()
278 }
279}
280
281fn invalid_path() -> FsError {
283 FsError::invalid_path(
284 FsOperation::ParsePath,
285 "path must be non-empty, NUL-free, and remain within its root",
286 )
287}
288
289#[cfg(test)]
290mod tests {
291 use std::hint::black_box;
292
293 use super::Path;
294 use crate::path::PathComponent;
295 use crate::path::PathSemantics;
296 use crate::path::RelativePath;
297
298 #[test]
299 fn path_accessors_and_constructors_are_executed_at_runtime() {
300 let root: fn() -> Path = black_box(Path::root);
301 let parse_literal: fn(&str) -> crate::error::FsResult<Path> = black_box(Path::parse_literal);
302 let parse_with_semantics: fn(&str, PathSemantics) -> crate::error::FsResult<Path> =
303 black_box(Path::parse_with_semantics);
304 let as_str: for<'a> fn(&'a Path) -> &'a str = black_box(Path::as_str);
305 let file_name: for<'a> fn(&'a Path) -> Option<&'a str> = black_box(Path::file_name);
306 let is_absolute: fn(&Path) -> bool = black_box(Path::is_absolute);
307 let semantics: fn(&Path) -> PathSemantics = black_box(Path::semantics);
308 let components = black_box(Path::components);
309 let child: fn(&Path, &PathComponent) -> Path = black_box(Path::child);
310 let join: fn(&Path, &RelativePath) -> Path = black_box(Path::join);
311 let as_ref: for<'a> fn(&'a Path) -> &'a str = black_box(<Path as AsRef<str>>::as_ref);
312
313 let built = Path::from_components(true, vec!["reports", "daily.csv"]).expect("components should form a path");
314 assert!(Path::from_components(false, Vec::<&str>::new()).is_err());
315 let literal = parse_literal("bucket/key").expect("literal path should parse");
316 let provider = parse_with_semantics("bucket/key", PathSemantics::ProviderSpecific)
317 .expect("provider-specific path should parse");
318 let component = PathComponent::parse("archive").expect("component should parse");
319 let relative = RelativePath::parse("daily.csv").expect("relative path should parse");
320
321 assert_eq!("/", as_str(&root()));
322 assert_eq!(Some("daily.csv"), file_name(&built));
323 assert!(is_absolute(&built));
324 assert_eq!(PathSemantics::ObjectKey, semantics(&literal));
325 assert_eq!(PathSemantics::ProviderSpecific, semantics(&provider));
326 let parent = Path::parse("/reports").expect("parent path should parse");
327 assert_eq!("/reports/daily.csv", as_str(&join(&parent, &relative)));
328 assert_eq!(
329 "/reports/archive",
330 as_str(&child(&Path::parse("/reports").unwrap(), &component))
331 );
332 assert_eq!("reports/daily.csv", components(&built).collect::<Vec<_>>().join("/"));
333 assert_eq!(as_str(&built), as_ref(&built));
334 }
335}