logix_type/types/
valid_path.rs

1use bstr::ByteSlice;
2use logix_vfs::LogixVfs;
3
4use crate::{
5    error::{ParseError, PathError, Result, SourceSpan, Wanted},
6    parser::LogixParser,
7    token::{Literal, StrLit, Token},
8    type_trait::{LogixTypeDescriptor, LogixValueDescriptor, Value},
9    LogixType,
10};
11use std::{
12    borrow::Cow,
13    ffi::OsStr,
14    fmt,
15    path::{Path, PathBuf},
16};
17
18macro_rules! impl_path_type_traits {
19    ($name:ident, $doc:literal) => {
20        impl $name {
21            pub fn from_lit(lit: StrLit, span: &SourceSpan) -> Result<Self> {
22                lit.decode_str(span)?
23                    .try_into()
24                    .map_err(|error| ParseError::PathError {
25                        span: span.clone(),
26                        error,
27                    })
28            }
29        }
30
31        impl<'a> TryFrom<&'a str> for $name {
32            type Error = PathError;
33
34            fn try_from(v: &'a str) -> Result<Self, PathError> {
35                PathBuf::from(v).try_into()
36            }
37        }
38
39        impl<'a> TryFrom<Cow<'a, str>> for $name {
40            type Error = PathError;
41
42            fn try_from(v: Cow<'a, str>) -> Result<Self, PathError> {
43                v.into_owned().try_into()
44            }
45        }
46
47        impl TryFrom<String> for $name {
48            type Error = PathError;
49
50            fn try_from(v: String) -> Result<Self, PathError> {
51                PathBuf::from(v).try_into()
52            }
53        }
54
55        impl<'a> TryFrom<&'a Path> for $name {
56            type Error = PathError;
57
58            fn try_from(v: &'a Path) -> Result<Self, PathError> {
59                v.to_path_buf().try_into()
60            }
61        }
62
63        impl<'a> TryFrom<Cow<'a, Path>> for $name {
64            type Error = PathError;
65
66            fn try_from(v: Cow<'a, Path>) -> Result<Self, PathError> {
67                v.into_owned().try_into()
68            }
69        }
70
71        impl std::ops::Deref for $name {
72            type Target = Path;
73
74            fn deref(&self) -> &Self::Target {
75                self.as_path()
76            }
77        }
78
79        impl AsRef<Path> for $name {
80            fn as_ref(&self) -> &Path {
81                self.as_path()
82            }
83        }
84
85        impl AsRef<::std::ffi::OsStr> for $name {
86            fn as_ref(&self) -> &std::ffi::OsStr {
87                self.as_path().as_ref()
88            }
89        }
90
91        impl PartialEq<PathBuf> for $name {
92            fn eq(&self, rhs: &PathBuf) -> bool {
93                self.as_path() == rhs
94            }
95        }
96
97        impl<'a> PartialEq<&'a Path> for $name {
98            fn eq(&self, rhs: &&'a Path) -> bool {
99                self.as_path() == *rhs
100            }
101        }
102
103        impl fmt::Debug for $name {
104            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105                fmt::Debug::fmt(&**self, f)
106            }
107        }
108
109        impl LogixType for $name {
110            fn descriptor() -> &'static LogixTypeDescriptor {
111                static DESC: LogixTypeDescriptor = LogixTypeDescriptor {
112                    name: stringify!($name),
113                    doc: $doc,
114                    value: LogixValueDescriptor::Native,
115                };
116                &DESC
117            }
118
119            fn default_value() -> Option<Self> {
120                None
121            }
122
123            fn logix_parse<FS: LogixVfs>(p: &mut LogixParser<FS>) -> Result<Value<Self>> {
124                match p.next_token()? {
125                    (span, Token::Literal(Literal::Str(value))) => Ok(Value {
126                        value: Self::from_lit(value, &span)?,
127                        span,
128                    }),
129                    (span, token) => Err(ParseError::UnexpectedToken {
130                        span,
131                        while_parsing: Self::descriptor().name,
132                        wanted: Wanted::$name,
133                        got_token: token.token_type_name(),
134                    }),
135                }
136            }
137        }
138    };
139
140    ($name:ident, $doc:literal, ($($variant:ident),+), $error:ident) => {
141        impl_path_type_traits!($name, $doc);
142
143        impl $name {
144            pub fn as_path(&self) -> &Path {
145                &self.path
146            }
147
148            pub fn join(&self, path: impl AsRef<Path>) -> Result<Self, PathError> {
149                let path = path.as_ref();
150                if path.is_absolute() {
151                    Err(PathError::JoinAbsolute)
152                } else {
153                    Ok(Self { path: self.path.join(path) })
154                }
155            }
156
157            pub fn with_file_name(&self, name: impl AsRef<OsStr>) -> Self {
158                Self { path: self.path.with_file_name(name) }
159            }
160
161            pub fn with_extension(&self, ext: impl AsRef<OsStr>) -> Self {
162                Self { path: self.path.with_extension(ext) }
163            }
164        }
165
166        impl From<$name> for PathBuf {
167            fn from(v: $name) -> Self {
168                v.path
169            }
170        }
171
172        impl TryFrom<PathBuf> for $name {
173            type Error = PathError;
174
175            fn try_from(path: PathBuf) -> Result<Self, PathError> {
176                match ValidPath::try_from(path)? {
177                    $(ValidPath::$variant(ret) => Ok(Self { path: PathBuf::from(ret) }),)+
178                    _ => Err(PathError::$error),
179                }
180            }
181        }
182    };
183}
184
185/// Represents a validated full path
186#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
187pub enum ValidPath {
188    Full(FullPath),
189    Rel(RelPath),
190    Name(NameOnlyPath),
191}
192
193impl ValidPath {
194    pub fn as_path(&self) -> &Path {
195        let (ValidPath::Full(FullPath { path })
196        | ValidPath::Rel(RelPath { path })
197        | ValidPath::Name(NameOnlyPath { path })) = self;
198        path
199    }
200
201    pub fn join(&self, path: impl AsRef<Path>) -> Result<Self, PathError> {
202        match self {
203            Self::Full(v) => v.join(path).map(Self::Full),
204            Self::Rel(v) => v.join(path).map(Self::Rel),
205            Self::Name(v) => v.join(path).map(Self::Name),
206        }
207    }
208
209    pub fn with_file_name(&self, name: impl AsRef<OsStr>) -> Self {
210        match self {
211            Self::Full(v) => Self::Full(v.with_file_name(name)),
212            Self::Rel(v) => Self::Rel(v.with_file_name(name)),
213            Self::Name(v) => Self::Name(v.with_file_name(name)),
214        }
215    }
216
217    pub fn with_extension(&self, ext: impl AsRef<OsStr>) -> Self {
218        match self {
219            Self::Full(v) => Self::Full(v.with_extension(ext)),
220            Self::Rel(v) => Self::Rel(v.with_extension(ext)),
221            Self::Name(v) => Self::Name(v.with_extension(ext)),
222        }
223    }
224}
225
226impl From<ValidPath> for PathBuf {
227    fn from(v: ValidPath) -> Self {
228        let (ValidPath::Full(FullPath { path })
229        | ValidPath::Rel(RelPath { path })
230        | ValidPath::Name(NameOnlyPath { path })) = v;
231        path
232    }
233}
234
235impl TryFrom<PathBuf> for ValidPath {
236    type Error = PathError;
237
238    fn try_from(path: PathBuf) -> Result<Self, PathError> {
239        let raw_bytes = path.as_os_str().as_encoded_bytes();
240        // NOTE: We are stricter than the actual operating system as we exclude inconvinient characters
241        if let Some(i) = raw_bytes.find_byteset(b"\n|\"'") {
242            Err(PathError::InvalidChar(char::from(raw_bytes[i])))
243        } else if path == PathBuf::new() {
244            Err(PathError::EmptyPath)
245        } else if path.is_absolute() {
246            Ok(Self::Full(FullPath { path }))
247        } else {
248            let mut it = path.components();
249            if matches!(it.next(), Some(std::path::Component::Normal(_))) && it.next().is_none() {
250                Ok(Self::Name(NameOnlyPath { path }))
251            } else {
252                Ok(Self::Rel(RelPath { path }))
253            }
254        }
255    }
256}
257
258impl_path_type_traits!(ValidPath, "A valid path");
259
260/// Represents a validated full path
261#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
262pub struct FullPath {
263    path: PathBuf,
264}
265
266impl_path_type_traits!(FullPath, "A full path", (Full), NotAbsolute);
267
268/// Represents a validated relative path
269#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
270pub struct RelPath {
271    path: PathBuf,
272}
273
274impl_path_type_traits!(RelPath, "A relative path", (Rel, Name), NotRelative);
275
276/// Represents a validated file or directory name without any path components
277#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
278pub struct NameOnlyPath {
279    path: PathBuf,
280}
281
282impl_path_type_traits!(NameOnlyPath, "A file or directory name", (Name), NotName);
283
284impl LogixType for PathBuf {
285    fn descriptor() -> &'static LogixTypeDescriptor {
286        static RET: LogixTypeDescriptor = LogixTypeDescriptor {
287            name: "path",
288            doc: "a valid path",
289            value: LogixValueDescriptor::Native,
290        };
291        &RET
292    }
293
294    fn default_value() -> Option<Self> {
295        None
296    }
297
298    fn logix_parse<FS: LogixVfs>(p: &mut LogixParser<FS>) -> Result<Value<Self>> {
299        Ok(ValidPath::logix_parse(p)?.map(|v| v.into()))
300    }
301}