pathmut/
component.rs

1use crate::Action;
2use typed_path::{
3    PathType, TypedPath, TypedPathBuf, WindowsComponent, WindowsEncoding, WindowsPath,
4    WindowsPrefix,
5};
6
7// use clap::{builder::PossibleValue, ValueEnum};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum Component {
11    Extension,
12    Stem,
13    Prefix,
14    Name,
15    Parent,
16    Disk,
17    Nth(isize),
18    // more windows prefixes exist
19    // https://docs.rs/typed-path/0.10.0/typed_path/enum.WindowsPrefix.html#variant.Disk
20    // URIs later
21    // scheme, route, anchor, query params, domain, server
22    // Example lib: https://docs.rs/http/latest/http/uri/struct.Uri.html
23    // authority doesn't segment between user:pass
24    // Also url crate: https://docs.rs/url/latest/url/
25    // this crate has all the set methods I'd want
26}
27
28// may not need this because of ValueEnum
29impl TryFrom<&str> for Component {
30    type Error = ();
31
32    fn try_from(s: &str) -> Result<Self, Self::Error> {
33        use Component::*;
34        let comp = match s {
35            "ext" => Extension,
36            "stem" => Stem,
37            "prefix" => Prefix,
38            "name" => Name,
39            "parent" => Parent,
40            "disk" => Disk,
41            _ => Nth(s.parse::<isize>().map_err(|_| ())?),
42        };
43        Ok(comp)
44    }
45}
46
47pub fn arg_into_component(s: &str) -> Result<Component, String> {
48    use Component::*;
49    if let Ok(n) = s.parse::<isize>() {
50        Ok(Nth(n))
51    } else {
52        let component = match s {
53            "ext" => Extension,
54            "stem" => Stem,
55            "prefix" => Prefix,
56            "name" => Name,
57            "parent" => Parent,
58            "disk" => Disk,
59            _ => Err("invalid component")?,
60        };
61        Ok(component)
62    }
63}
64
65// todo: make my own typed value parser
66//struct ComponentParser;
67//impl clap::builder::TypedValueParser for ComponentParser {
68//    type Value = Component;
69//
70//    fn parse_ref(
71//        &self,
72//        cmd: &clap::Command,
73//        arg: Option<&clap::Arg>,
74//        value: &std::ffi::OsStr,
75//    ) -> Result<Self::Value, clap::Error> {
76//    }
77//}
78
79trait FilePrefix {
80    // TODO: consider if this is the right name,
81    // since it conflicts with WindowsPrefix
82    // is this even a useful component?
83    fn file_prefix(&self) -> Option<&[u8]>;
84}
85
86impl FilePrefix for TypedPath<'_> {
87    // Referencing std::path::Path::file_prefix
88    // https://doc.rust-lang.org/stable/src/std/path.rs.html#2648-2650
89    fn file_prefix(&self) -> Option<&[u8]> {
90        self.file_name()
91            .map(split_file_at_dot)
92            .map(|(before, _after)| before)
93    }
94}
95
96fn split_file_at_dot(file: &[u8]) -> (&[u8], Option<&[u8]>) {
97    // Referencing std::path::split_file_at_dot
98    // https://doc.rust-lang.org/stable/src/std/path.rs.html#340
99    let slice = file;
100    if slice == b".." {
101        return (file, None);
102    }
103
104    let i = match slice[1..].iter().position(|b| *b == b'.') {
105        Some(i) => i + 1,
106        None => return (file, None),
107    };
108    let before = &slice[..i];
109    let after = &slice[i + 1..];
110    (before, Some(after))
111}
112
113impl Component {
114    pub fn action(self, action: &Action, path: &TypedPath) -> Vec<u8> {
115        match action {
116            Action::Get => self.get(path),
117            Action::Set(s) => self.set(path, s),
118            Action::Replace(s) => self.replace(path, s),
119            Action::Delete => self.delete(path),
120        }
121    }
122
123    pub fn get(self, path: &TypedPath) -> Vec<u8> {
124        use Component::*;
125        match self {
126            Extension => path.extension().unwrap_or_default().into(),
127            Stem => path.file_stem().unwrap_or_default().into(),
128            Prefix => path.file_prefix().unwrap_or_default().into(),
129            Name => path.file_name().unwrap_or_default().into(),
130            Parent => path
131                .parent()
132                .map(|p| p.as_bytes().to_vec())
133                .unwrap_or_default(),
134            Disk => match path {
135                TypedPath::Unix(_) => "".into(),
136                TypedPath::Windows(w) => match w.components().next() {
137                    Some(WindowsComponent::Prefix(prefix)) => match prefix.kind() {
138                        WindowsPrefix::Disk(disk) => [disk].into(),
139                        _ => "".into(),
140                    },
141                    _ => "".into(),
142                },
143            },
144            Nth(n) => {
145                let num_components: usize = path.components().count();
146                let index: usize = if n >= 0 {
147                    let positive: usize = n.try_into().unwrap();
148                    positive
149                } else {
150                    let positive: usize = (-n).try_into().unwrap();
151                    if positive > num_components {
152                        // index is behind first component
153                        return Vec::new();
154                    }
155                    num_components - positive
156                };
157                path.components()
158                    .nth(index)
159                    .map(|c| c.as_bytes().to_vec())
160                    .unwrap_or_default()
161            }
162        }
163    }
164
165    pub fn has(self, path: &TypedPath) -> bool {
166        !self.get(path).is_empty()
167    }
168
169    pub fn set(self, path: &TypedPath, value: &[u8]) -> Vec<u8> {
170        use Component::*;
171        match self {
172            Extension => path.with_extension(value).into_vec(),
173            Stem => {
174                if let Some(ext) = path.extension() {
175                    let name = [value, b".", ext].concat();
176                    path.with_file_name(name).into_vec()
177                } else {
178                    path.with_file_name(value).into_vec()
179                }
180            }
181            Prefix => {
182                let after: &[u8] = path
183                    .file_name()
184                    .map(split_file_at_dot)
185                    .and_then(|(_, after)| after)
186                    .unwrap_or_default();
187
188                if let Some(parent) = path.parent() {
189                    let name = if !after.is_empty() {
190                        [value, b".", after].concat()
191                    } else {
192                        value.to_vec()
193                    };
194                    parent.join(name).into_vec()
195                } else {
196                    let new_path = if path.is_unix() {
197                        TypedPath::new(value, PathType::Unix)
198                    } else {
199                        TypedPath::new(value, PathType::Windows)
200                    };
201                    new_path.join(after).into_vec()
202                }
203            }
204            Name => path.with_file_name(value).into_vec(),
205            Parent => {
206                let new_parent = match path {
207                    TypedPath::Unix(_) => TypedPath::new(value, PathType::Unix),
208                    TypedPath::Windows(_) => TypedPath::new(value, PathType::Windows),
209                };
210                new_parent
211                    .join(path.file_name().unwrap_or_default())
212                    .into_vec()
213            }
214            Disk => match path {
215                TypedPath::Unix(_) => path.to_path_buf().into_vec(),
216                TypedPath::Windows(w) => {
217                    let mut original = w.components();
218                    let mut new = original.clone();
219                    let has_prefix = match new.next() {
220                        Some(WindowsComponent::Prefix(prefix)) => match prefix.kind() {
221                            WindowsPrefix::Disk(_) => true,
222                            _ => false,
223                        },
224                        _ => false,
225                    };
226
227                    let no_disk: &typed_path::Path<WindowsEncoding> = if has_prefix {
228                        original.next(); // remove prefix
229                        original.as_path()
230                    } else {
231                        original.as_path()
232                    };
233
234                    if value.len() == 0 {
235                        return original
236                            .as_path::<WindowsEncoding>()
237                            .to_path_buf()
238                            .into_vec();
239                    }
240
241                    // TEST: what happens if disk is more one char?
242                    // what if 0 chars
243
244                    // this is so garbage
245                    let disk_str = format!(r"{}:", String::from_utf8(vec![value[0]]).unwrap());
246                    let disk_path = WindowsPath::new(&disk_str);
247                    let mut new_path = disk_path.to_path_buf();
248                    new_path.push(no_disk);
249
250                    new_path.into()
251                }
252            },
253            Nth(n) => {
254                // what if path is root?
255                // todo
256
257                let num_components: usize = path.components().count();
258                let index: usize = if n >= 0 {
259                    let positive: usize = n.try_into().unwrap();
260                    positive
261                } else {
262                    let positive: usize = (-n).try_into().unwrap();
263                    if positive > num_components {
264                        // index is behind first component
265                        return Vec::new();
266                    }
267                    num_components - positive
268                };
269
270                // what if n == number of components?
271                let num_components = path.components().count();
272                if num_components == index {
273                    return path.join(value).into_vec();
274                }
275
276                // what if n > number of components?
277                // todo
278
279                path.components()
280                    .enumerate()
281                    .map(|(i, c)| {
282                        if i == index {
283                            TypedPathBuf::from(value)
284                        } else {
285                            TypedPathBuf::from(c.as_bytes())
286                        }
287                    })
288                    .reduce(|a, b| a.join(b))
289                    .map(|p| p.into_vec())
290                    .unwrap_or_default()
291            }
292        }
293    }
294
295    pub fn replace(self, path: &TypedPath, value: &[u8]) -> Vec<u8> {
296        //println!("{:?} {:?}", path, value);
297        if self.has(path) {
298            self.set(path, value)
299        } else {
300            path.to_path_buf().into_vec()
301        }
302    }
303
304    pub fn delete(&self, path: &TypedPath) -> Vec<u8> {
305        use Component::*;
306        match self {
307            Stem => {
308                if let Some(ext) = path.extension() {
309                    path.with_file_name(ext).into_vec()
310                } else {
311                    path.with_file_name("").into_vec()
312                }
313            }
314            Prefix => {
315                // revisit, this feels like hard coded, not edge case
316                if path == &TypedPath::derive("/") {
317                    return path.to_path_buf().into_vec();
318                }
319
320                let after: &[u8] = path
321                    .file_name()
322                    .map(split_file_at_dot)
323                    .and_then(|(_, after)| after)
324                    .unwrap_or_default();
325
326                if let Some(parent) = path.parent() {
327                    parent.join(after).into_vec()
328                } else {
329                    let new_path = if path.is_unix() {
330                        TypedPath::new(after, PathType::Unix)
331                    } else {
332                        TypedPath::new(after, PathType::Windows)
333                    };
334                    new_path.to_path_buf().into_vec()
335                }
336            }
337            Name => path.with_file_name("").into_vec(),
338            _ => self.replace(path, b""),
339        }
340    }
341}