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}