1use std::{
2 env, fs, io, iter,
3 path::{Path, PathBuf},
4};
5
6pub struct FsFixtureBuilderOptions {
7 pub temp_dir: PathBuf,
10}
11impl Default for FsFixtureBuilderOptions {
12 fn default() -> Self {
13 FsFixtureBuilderOptions {
14 temp_dir: env::temp_dir().canonicalize().unwrap(),
15 }
16 }
17}
18
19enum FileValue {
20 File(String),
21 Dir,
22 SymlinkFile(String),
23 SymlinkDir(String),
24}
25
26trait FileTreeBuilder {
27 fn get_files_vec(&mut self) -> &mut Vec<(String, FileValue)>;
28 fn get_prefix(&self) -> String;
29
30 fn get_path(&self, path: &str) -> String {
31 format!("{}{}", self.get_prefix(), &clean_path(path))
32 }
33
34 fn add_file(&mut self, path: &str, content: &str) {
35 let path = self.get_path(path);
36 self.get_files_vec()
37 .push((path, FileValue::File(content.to_string())));
38 }
39
40 fn add_dir(&mut self, path: &str, cb: impl FnOnce(FsFixtureDirBuilder) -> FsFixtureDirBuilder) {
41 let path = self.get_path(path);
42 let files = self.get_files_vec();
43 let start_files_len = files.len();
44
45 let builder = FsFixtureDirBuilder::new(files, &path);
46 let _ = cb(builder);
47
48 if files.len() == start_files_len {
49 files.push((path, FileValue::Dir));
51 }
52 }
53
54 fn add_symlink_file(&mut self, path: &str, target: &str) {
55 let path = self.get_path(path);
56 self.get_files_vec()
57 .push((path, FileValue::SymlinkFile(target.to_string())));
58 }
59
60 fn add_symlink_dir(&mut self, path: &str, target: &str) {
61 let dir_path = self.get_path(path);
62 self.get_files_vec()
63 .push((dir_path, FileValue::SymlinkDir(target.to_string())));
64 }
65}
66
67pub struct FsFixtureBuilder {
68 files: Vec<(String, FileValue)>,
69 options: FsFixtureBuilderOptions,
70}
71impl FileTreeBuilder for FsFixtureBuilder {
72 fn get_files_vec(&mut self) -> &mut Vec<(String, FileValue)> {
73 &mut self.files
74 }
75
76 fn get_prefix(&self) -> String {
77 "".to_string()
78 }
79}
80impl FsFixtureBuilder {
81 pub fn new() -> Self {
82 FsFixtureBuilder {
83 files: vec![],
84 options: FsFixtureBuilderOptions::default(),
85 }
86 }
87
88 pub fn options(mut self, options: FsFixtureBuilderOptions) -> Self {
89 self.options = options;
90 self
91 }
92
93 pub fn file(mut self, path: &str, content: &str) -> Self {
95 self.add_file(path, content);
96 self
97 }
98
99 pub fn dir(
101 mut self,
102 path: &str,
103 cb: impl FnOnce(FsFixtureDirBuilder) -> FsFixtureDirBuilder,
104 ) -> Self {
105 self.add_dir(path, cb);
106 self
107 }
108
109 pub fn symlink_file(mut self, path: &str, target: &str) -> Self {
111 self.add_symlink_file(path, target);
112 self
113 }
114
115 pub fn symlink_dir(mut self, path: &str, target: &str) -> Self {
117 self.add_symlink_dir(path, target);
118 self
119 }
120
121 pub fn build(self) -> io::Result<FsFixture> {
122 let temp_dir = get_temp_dir_name();
123 let resolved_temp_dir = self.options.temp_dir.join(temp_dir);
124
125 if self.files.is_empty() {
126 fs::create_dir_all(&resolved_temp_dir)?;
128 } else {
129 for (path, value) in self.files {
130 let full_path = resolved_temp_dir.join(&path);
131 match value {
132 FileValue::File(content) => {
133 fs::create_dir_all(full_path.parent().unwrap())?;
134 fs::write(full_path, content)?;
135 }
136 FileValue::Dir => {
137 fs::create_dir_all(full_path)?;
138 }
139 FileValue::SymlinkFile(target) => {
140 let target = resolved_temp_dir.join(&target);
141 fs::create_dir_all(full_path.parent().unwrap())?;
142 symlink_file(&target, &full_path)?;
143 }
144 FileValue::SymlinkDir(target) => {
145 let target = resolved_temp_dir.join(&target);
146 fs::create_dir_all(full_path.parent().unwrap())?;
147 symlink_dir(&target, &full_path)?;
148 }
149 }
150 }
151 }
152
153 Ok(FsFixture::new(resolved_temp_dir))
154 }
155}
156
157pub struct FsFixtureDirBuilder<'a> {
158 files: &'a mut Vec<(String, FileValue)>,
159 dir: &'a str,
160}
161impl<'a> FileTreeBuilder for FsFixtureDirBuilder<'a> {
162 fn get_files_vec(&mut self) -> &mut Vec<(String, FileValue)> {
163 self.files
164 }
165
166 fn get_prefix(&self) -> String {
167 format!("{}/", self.dir)
168 }
169}
170impl<'a> FsFixtureDirBuilder<'a> {
171 fn new(files: &'a mut Vec<(String, FileValue)>, dir: &'a str) -> Self {
172 FsFixtureDirBuilder { files, dir }
173 }
174
175 pub fn file(mut self, path: &str, content: &str) -> Self {
177 self.add_file(path, content);
178 self
179 }
180
181 pub fn dir(
183 mut self,
184 path: &str,
185 cb: impl FnOnce(FsFixtureDirBuilder) -> FsFixtureDirBuilder,
186 ) -> Self {
187 self.add_dir(path, cb);
188 self
189 }
190
191 pub fn symlink_file(mut self, path: &str, target: &str) -> Self {
193 self.add_symlink_file(path, target);
194 self
195 }
196
197 pub fn symlink_dir(mut self, path: &str, target: &str) -> Self {
199 self.add_symlink_dir(path, target);
200 self
201 }
202}
203
204pub struct FsFixture {
205 resolved_temp_dir: PathBuf,
206}
207impl FsFixture {
208 fn new(resolved_temp_dir: PathBuf) -> Self {
209 FsFixture { resolved_temp_dir }
210 }
211
212 pub fn path(&self) -> &Path {
214 &self.resolved_temp_dir
215 }
216
217 pub fn path_join(&self, path: &str) -> PathBuf {
219 self.resolved_temp_dir.join(clean_path(path))
220 }
221
222 pub fn exists(&self, path: &str) -> bool {
224 self.resolved_temp_dir.join(path).exists()
225 }
226
227 pub fn write_file(&self, path: &str, content: &str) -> io::Result<()> {
229 let full_path = self.resolved_temp_dir.join(&path);
230 fs::create_dir_all(full_path.parent().unwrap())?;
231 fs::write(full_path, content)?;
232 Ok(())
233 }
234
235 pub fn read_file(&self, path: &str) -> io::Result<String> {
237 fs::read_to_string(self.resolved_temp_dir.join(path))
238 }
239
240 pub fn remove_file(&self, path: &str) -> io::Result<()> {
242 fs::remove_file(self.resolved_temp_dir.join(path))
243 }
244
245 pub fn remove(&self) -> Result<(), io::Error> {
247 fs::remove_dir_all(&self.resolved_temp_dir)
248 }
249}
250impl Drop for FsFixture {
251 fn drop(&mut self) {
252 let _ = fs::remove_dir_all(&self.resolved_temp_dir);
253 }
254}
255
256fn clean_path(path: &str) -> String {
264 let mut path = path.replace("/../", "/").replace("/./", "/");
265
266 if path.starts_with("./") {
267 path = path[2..].to_string();
268 } else if path.starts_with("../") {
269 path = path[3..].to_string();
270 }
271
272 while path.contains("//") {
273 path = path.replace("//", "/");
274 }
275
276 while path.starts_with('/') {
277 path = path[1..].to_string();
278 }
279
280 while path.ends_with('/') {
281 path = path[0..path.len() - 1].to_string();
282 }
283
284 path
285}
286
287fn get_temp_dir_name() -> String {
288 let random_id: String = iter::repeat_with(fastrand::alphanumeric).take(8).collect();
289 format!("fs-fixture-{}", random_id)
290}
291
292fn symlink_file(original: &Path, link: &Path) -> io::Result<()> {
293 #[cfg(unix)]
294 {
295 use std::os::unix::fs::symlink;
296 symlink(original, link)
297 }
298 #[cfg(windows)]
299 {
300 use std::os::windows::fs::symlink_file;
301 symlink_file(original, link)
302 }
303}
304
305fn symlink_dir(original: &Path, link: &Path) -> io::Result<()> {
306 #[cfg(unix)]
307 {
308 use std::os::unix::fs::symlink;
309 symlink(original, link)
310 }
311 #[cfg(windows)]
312 {
313 use std::os::windows::fs::symlink_dir;
314 symlink_dir(original, link)
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn test_clean_path() {
324 assert_eq!(clean_path("./foo.txt"), "foo.txt");
325 assert_eq!(clean_path("../foo.txt"), "foo.txt");
326 assert_eq!(clean_path("foo.txt"), "foo.txt");
327 assert_eq!(clean_path("dir/foo.txt"), "dir/foo.txt");
328 assert_eq!(clean_path("dir/../foo.txt"), "dir/foo.txt");
329 assert_eq!(clean_path("dir/./foo.txt"), "dir/foo.txt");
330 assert_eq!(clean_path("/foo.txt"), "foo.txt");
331 assert_eq!(clean_path("/dir/foo.txt"), "dir/foo.txt");
332 assert_eq!(clean_path("/dir/../foo.txt"), "dir/foo.txt");
333 assert_eq!(clean_path("/dir/./foo.txt"), "dir/foo.txt");
334 assert_eq!(clean_path("dir/"), "dir");
335 assert_eq!(clean_path("/dir/"), "dir");
336 assert_eq!(clean_path("/dir/../"), "dir");
337 assert_eq!(clean_path("dir/./"), "dir");
338 assert_eq!(clean_path("./dir/"), "dir");
339 assert_eq!(clean_path("../dir/"), "dir");
340 assert_eq!(clean_path("dir///////foo//////bar"), "dir/foo/bar");
341 assert_eq!(clean_path("dir/.././foo/.../bar"), "dir/foo/.../bar");
342 }
343}