mini_fs/
caseless.rs

1//! This module contains a caseless filesystem.
2//!
3//! A caseless filesystem wraps an inner filesystem and treats paths as
4//! case-insensitive, regardless of the case of the inner filesystem.
5//!
6//! Case-insensitive paths are refered to as caseless paths.
7//! A caseless path that matches the real path of a file always opens that file.
8//! Otherwise a caseless path will open the first path of the inner filesystem
9//! that matches the caseless path.
10//!
11//! Internally paths are strings of type OsString, which can contain invalid
12//! utf8. There is no safe way to make case-insensitive comparisons when invalid
13//! utf8 is present. To minimize the effect of this restriction, the path
14//! components are compared individually. Path components with valid utf8 are
15//! compared in a case-insensitive way. Path components with invalid utf8 are
16//! compared raw (case-sensitive).
17
18use std::ffi::OsString;
19use std::io;
20use std::path::{Component, Path, PathBuf};
21
22use crate::index::normalize_path;
23use crate::prelude::*;
24use crate::store::Entries;
25
26/// Caseless filesystem wrapping an inner filesystem.
27#[derive(Clone, Debug)]
28pub struct CaselessFs<S> {
29    /// Inner filesystem store.
30    inner: S,
31}
32
33impl<S: Store> CaselessFs<S> {
34    /// Creates a new caseless filesystem with the provided inner filesystem.
35    /// It treats paths as case-insensitive, regardless of the case of the inner
36    /// filesystem.
37    pub fn new(inner: S) -> Self {
38        Self { inner }
39    }
40
41    /// Moves the inner filesystem out of the caseless filesystem.
42    /// Inspired by std::io::Cursor.
43    pub fn into_inner(self) -> S {
44        self.inner
45    }
46
47    /// Gets a reference to the inner filesystem.
48    /// Inspired by std::io::Cursor.
49    pub fn get_ref(&self) -> &S {
50        &self.inner
51    }
52
53    /// Gets a mutable reference to the inner filesystem.
54    /// Inspired by std::io::Cursor.
55    pub fn get_mut(&mut self) -> &mut S {
56        &mut self.inner
57    }
58
59    /// Finds paths that match the caseless path.
60    /// Path components with valid utf8 are compared in a case-insensitive way.
61    /// Path components with invalid utf8 are compared raw (case-sensitive).
62    pub fn find<P: AsRef<Path>>(&self, path: P) -> Vec<PathBuf> {
63        let path = normalize_path(path.as_ref());
64        let mut paths = vec![PathBuf::new()];
65        for component in path.components() {
66            paths = find_next_ascii_lowercase(&self.inner, &component, paths);
67            if paths.len() == 0 {
68                return paths;
69            }
70        }
71        paths
72    }
73}
74
75impl<S: Store> Store for CaselessFs<S> {
76    type File = S::File;
77
78    /// Opens the file identified by the caseless path.
79    /// A caseless path that matches the real path of a file always opens that
80    /// file. Otherwise a caseless path will open the first path of the
81    /// inner filesystem that matches the caseless path.
82    fn open_path(&self, path: &Path) -> io::Result<Self::File> {
83        // real path
84        if let Ok(file) = self.inner.open_path(path) {
85            return Ok(file);
86        }
87        // caseless path
88        for path in self.find(path) {
89            return self.inner.open_path(&path);
90        }
91        Err(io::ErrorKind::NotFound.into())
92    }
93
94    /// Iterates over the entries of the inner filesystem.
95    fn entries_path(&self, path: &Path) -> io::Result<Entries> {
96        self.inner.entries_path(path)
97    }
98}
99
100/// Finds the next path candidates.
101fn find_next_ascii_lowercase<S: Store>(
102    fs: &S,
103    component: &Component,
104    paths: Vec<PathBuf>,
105) -> Vec<PathBuf> {
106    let mut next = Vec::new();
107    let target: OsString = match component {
108        Component::Normal(os_s) => (*os_s).to_owned(),
109        Component::RootDir => {
110            // nothing can go before the root
111            next.push(Path::new("/").to_owned());
112            return next;
113        }
114        _ => {
115            panic!(format!("unexpected path component {:?}", component));
116        }
117    };
118    if let Some(t_s) = target.to_str() {
119        // compare utf8
120        for path in paths {
121            if let Ok(entries) = fs.entries(&path) {
122                for e in entries {
123                    if let Ok(entry) = e {
124                        if let Some(e_s) = entry.name.to_str() {
125                            if t_s.to_ascii_lowercase() == e_s.to_ascii_lowercase() {
126                                let mut path = path.to_owned();
127                                path.push(&entry.name);
128                                next.push(path);
129                            }
130                        }
131                    }
132                }
133            }
134        }
135    } else {
136        // compare raw
137        for path in paths {
138            if let Ok(entries) = fs.entries(&path) {
139                for e in entries {
140                    if let Ok(entry) = e {
141                        if &entry.name == &target {
142                            let mut path = path.to_owned();
143                            path.push(&entry.name);
144                            next.push(path);
145                        }
146                    }
147                }
148            }
149        }
150    }
151    next
152}