diskplan_filesystem/
lib.rs1#![warn(missing_docs)]
4
5use std::fmt::Display;
6
7use anyhow::{bail, Result};
8use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
9
10mod attributes;
11mod memory;
12mod physical;
13mod root;
14
15pub use self::{
16 attributes::{Attrs, Mode, SetAttrs, DEFAULT_DIRECTORY_MODE, DEFAULT_FILE_MODE},
17 memory::MemoryFilesystem,
18 physical::DiskFilesystem,
19 root::Root,
20};
21
22impl SetAttrs<'_> {
23 pub fn matches(&self, attrs: &Attrs) -> bool {
25 let SetAttrs { owner, group, mode } = self;
26 owner.map(|owner| owner == attrs.owner).unwrap_or(true)
27 && group.map(|group| group == attrs.group).unwrap_or(true)
28 && mode.map(|mode| mode == attrs.mode).unwrap_or(true)
29 }
30}
31
32pub trait Filesystem {
34 fn create_directory(&mut self, path: impl AsRef<Utf8Path>, attrs: SetAttrs) -> Result<()>;
36
37 fn create_directory_all(&mut self, path: impl AsRef<Utf8Path>, attrs: SetAttrs) -> Result<()> {
39 let path = path.as_ref();
40 if let Some((parent, _)) = split(path) {
41 if parent != "/" {
42 self.create_directory_all(parent, attrs.clone())?;
43 }
44 }
45 if !self.is_directory(path) {
46 self.create_directory(path, attrs)?;
47 }
48 Ok(())
49 }
50
51 fn create_file(
53 &mut self,
54 path: impl AsRef<Utf8Path>,
55 attrs: SetAttrs,
56 content: String,
57 ) -> Result<()>;
58
59 fn create_symlink(
61 &mut self,
62 path: impl AsRef<Utf8Path>,
63 target: impl AsRef<Utf8Path>,
64 ) -> Result<()>;
65
66 fn exists(&self, path: impl AsRef<Utf8Path>) -> bool;
68
69 fn is_directory(&self, path: impl AsRef<Utf8Path>) -> bool;
71
72 fn is_file(&self, path: impl AsRef<Utf8Path>) -> bool;
74
75 fn is_link(&self, path: impl AsRef<Utf8Path>) -> bool;
77
78 fn list_directory(&self, path: impl AsRef<Utf8Path>) -> Result<Vec<String>>;
80
81 fn read_file(&self, path: impl AsRef<Utf8Path>) -> Result<String>;
83
84 fn read_link(&self, path: impl AsRef<Utf8Path>) -> Result<Utf8PathBuf>;
86
87 fn attributes(&self, path: impl AsRef<Utf8Path>) -> Result<Attrs>;
92
93 fn set_attributes(&mut self, path: impl AsRef<Utf8Path>, attrs: SetAttrs) -> Result<()>;
98
99 fn canonicalize(&self, path: impl AsRef<Utf8Path>) -> Result<Utf8PathBuf> {
101 let path = path.as_ref();
102 if !path.is_absolute() {
103 bail!("Only absolute paths supported");
105 }
106 let mut canon = Utf8PathBuf::with_capacity(path.as_str().len());
107 for part in path.components() {
108 if part == Utf8Component::ParentDir {
109 let pop = canon.pop();
110 assert!(pop);
111 continue;
112 }
113 canon.push(part);
114 if self.is_link(Utf8Path::new(&canon)) {
115 let link = self.read_link(&canon)?;
116 if link.is_absolute() {
117 canon.clear();
118 } else {
119 canon.pop();
120 }
121 canon.push(link);
122 canon = self.canonicalize(canon)?;
123 }
124 }
125 Ok(canon)
126 }
127}
128
129fn split(path: &Utf8Path) -> Option<(&Utf8Path, &str)> {
131 path.as_str().rsplit_once('/').map(|(parent, child)| {
133 if parent.is_empty() {
134 ("/".into(), child)
135 } else {
136 (parent.into(), child)
137 }
138 })
139}
140
141pub struct PlantedPath {
143 root_len: usize,
144 full: Utf8PathBuf,
145}
146
147impl PlantedPath {
148 pub fn new(root: &Root, path: Option<&Utf8Path>) -> Result<Self> {
153 let path = match path {
154 Some(path) => {
155 if !path.starts_with(root.path()) {
156 bail!("Path {} must start with root {}", path, root.path());
157 }
158 path
159 }
160 None => root.path(),
161 };
162 Ok(PlantedPath {
163 root_len: root.path().as_str().len(),
164 full: path.to_owned(),
165 })
166 }
167
168 pub fn root(&self) -> &Utf8Path {
170 self.full.as_str()[..self.root_len].into()
171 }
172
173 pub fn absolute(&self) -> &Utf8Path {
175 &self.full
176 }
177
178 pub fn relative(&self) -> &Utf8Path {
180 self.full.as_str()[self.root_len..]
181 .trim_start_matches('/')
182 .into()
183 }
184
185 pub fn join(&self, name: impl AsRef<str>) -> Result<Self> {
187 let name = name.as_ref();
188 if name.contains('/') {
189 bail!(
190 "Only single path components can be joined to a planted path: {}",
191 name
192 );
193 }
194 Ok(PlantedPath {
195 root_len: self.root_len,
196 full: self.full.join(name),
197 })
198 }
199}
200
201impl Display for PlantedPath {
202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 write!(f, "{}", self.full)
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use anyhow::Result;
210
211 use super::*;
212
213 #[test]
214 fn check_relative() {
215 let path = PlantedPath::new(
216 &Root::try_from("/example").unwrap(),
217 Some(Utf8Path::new("/example/path")),
218 )
219 .unwrap();
220 assert_eq!(path.relative(), "path");
221 }
222
223 #[test]
224 fn canonicalize() -> Result<()> {
225 let path = Utf8Path::new("/");
226 let mut fs = MemoryFilesystem::new();
227 assert_eq!(fs.canonicalize(path).unwrap(), "/");
228
229 fs.create_directory("/dir", Default::default())?;
230 fs.create_symlink("/dir/sym", "../dir2/deeper")?;
231
232 assert_eq!(fs.canonicalize("/dir/./sym//final")?, "/dir2/deeper/final");
237
238 fs.create_directory("/dir2", Default::default())?;
239 fs.create_directory("/dir2/deeper", Default::default())?;
240 fs.create_symlink("/dir2/deeper/final", "/end")?;
241
242 assert_eq!(fs.canonicalize("/dir/./sym//final")?, "/end");
250
251 assert_eq!(fs.canonicalize("/dir/sym")?, "/dir2/deeper");
252 assert_eq!(fs.canonicalize("/dir/sym/.")?, "/dir2/deeper");
253 assert_eq!(fs.canonicalize("/dir/sym/..")?, "/dir2");
254
255 Ok(())
256 }
257}