1#![warn(clippy::all, clippy::pedantic, clippy::nursery, rust_2018_idioms)]
2#![allow(clippy::missing_errors_doc)]
3#![forbid(unsafe_code)]
4use std::fs::File;
5use std::path::{Path, PathBuf};
6
7pub mod builder;
8pub mod constraint;
9pub mod iter;
10pub mod scheme;
11
12#[derive(Debug, thiserror::Error)]
13pub enum Error {
14 #[error("I/O error")]
15 Io(#[from] std::io::Error),
16 #[error("Expected file")]
17 ExpectedFile(PathBuf),
18 #[error("Expected directory")]
19 ExpectedDirectory(PathBuf),
20 #[error("Invalid directory")]
21 InvalidDirectory(PathBuf),
22 #[error("Invalid name")]
23 InvalidName(String),
24}
25
26#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
27pub struct Entry<N> {
28 pub name: N,
29 pub path: PathBuf,
30}
31
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct Tree<S> {
34 base: PathBuf,
35 length_constraint: Option<constraint::Length>,
36 extension_constraint: Option<constraint::Extension>,
37 prefix_part_lengths: Vec<usize>,
38 scheme: S,
39}
40
41impl<S: scheme::Scheme> Tree<S> {
42 pub fn path(&self, name: &S::Name) -> Result<PathBuf, String> {
43 let name_string = self.scheme.name_to_string(name);
44
45 if name_string.len() >= self.prefix_part_lengths_total() {
46 let mut name_remaining = name_string.as_ref();
47 let mut path = self.base.clone();
48
49 for prefix_part_length in &self.prefix_part_lengths {
50 let next = &name_remaining[0..*prefix_part_length];
51 name_remaining = &name_remaining[*prefix_part_length..];
52
53 path.push(next);
54 }
55
56 path.push(name_string.as_ref());
57
58 Ok(path)
59 } else {
60 Err(name_string.to_string())
61 }
62 }
63
64 fn prefix_part_lengths_total(&self) -> usize {
65 self.prefix_part_lengths.iter().sum()
66 }
67
68 pub fn open_file(&self, name: &S::Name) -> Result<Option<File>, Error> {
69 let path = self.path(name).map_err(Error::InvalidName)?;
70
71 match File::open(&path) {
72 Ok(file) => {
73 if path.is_file() {
74 Ok(Some(file))
75 } else {
76 Err(Error::ExpectedFile(path))
77 }
78 }
79 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
80 Err(error) => Err(error.into()),
81 }
82 }
83
84 pub fn create_file(&self, name: &S::Name) -> Result<Option<File>, Error> {
85 let path = self.path(name).map_err(Error::InvalidName)?;
86
87 if let Some(parent) = path.parent() {
88 std::fs::create_dir_all(parent)?;
89 }
90
91 match File::create_new(path) {
92 Ok(file) => {
93 file.lock()?;
94
95 Ok(Some(file))
96 }
97 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => Ok(None),
98 Err(other) => Err(other.into()),
99 }
100 }
101
102 #[must_use]
103 pub fn entries(&self) -> iter::Entries<'_, S> {
104 iter::Entries::new(self)
105 }
106}
107
108impl Tree<scheme::Identity> {
109 pub fn builder<P: AsRef<Path>>(base: P) -> builder::TreeBuilder<scheme::Identity> {
110 builder::TreeBuilder::new(base.as_ref().to_path_buf())
111 }
112
113 pub fn infer_prefix_part_lengths<P: AsRef<Path>>(base: P) -> Result<Option<Vec<usize>>, Error> {
119 if base.as_ref().is_dir() {
120 let first = std::fs::read_dir(base)?
121 .next()
122 .map_or(Ok(None), |entry| entry.map(|entry| Some(entry.path())))?;
123
124 let mut acc = vec![];
125
126 let is_empty = first
127 .map(|first| Self::infer_prefix_part_lengths_rec(&first, &mut acc))
128 .map_or(Ok(true), |value| value)?;
129
130 Ok(if is_empty { None } else { Some(acc) })
131 } else {
132 Err(Error::ExpectedDirectory(base.as_ref().to_path_buf()))
133 }
134 }
135
136 fn infer_prefix_part_lengths_rec<P: AsRef<Path>>(
138 current: P,
139 acc: &mut Vec<usize>,
140 ) -> Result<bool, Error> {
141 if current.as_ref().is_file() {
142 Ok(false)
143 } else {
144 let directory_name = current
145 .as_ref()
146 .file_name()
147 .ok_or_else(|| Error::InvalidDirectory(current.as_ref().to_path_buf()))?;
148
149 acc.push(directory_name.len());
150
151 let next = std::fs::read_dir(current)?
152 .next()
153 .map_or(Ok(None), |entry| entry.map(|entry| Some(entry.path())))?;
154
155 next.map_or(Ok(true), |next| {
156 Self::infer_prefix_part_lengths_rec(next, acc)
157 })
158 }
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use std::io::Write;
166
167 #[test]
168 fn test_path_with_valid_prefix_lengths() -> Result<(), Box<dyn std::error::Error>> {
169 let temp_dir = tempfile::tempdir()?;
170 let tree = Tree::builder(temp_dir.path())
171 .with_scheme(scheme::Utf8)
172 .with_prefix_part_lengths([2, 2])
173 .build()?;
174
175 let path = tree.path(&"abcdef".to_string())?;
176 assert!(path.to_string_lossy().ends_with("/ab/cd/abcdef"));
177
178 Ok(())
179 }
180
181 #[test]
182 fn test_path_boundary_case_equal_length() -> Result<(), Box<dyn std::error::Error>> {
183 let temp_dir = tempfile::tempdir()?;
184 let tree = Tree::builder(temp_dir.path())
185 .with_scheme(scheme::Utf8)
186 .with_prefix_part_lengths([2, 1])
187 .build()?;
188
189 let path = tree.path(&"abc".to_string())?;
191 assert!(path.to_string_lossy().ends_with("/ab/c/abc"));
192
193 Ok(())
194 }
195
196 #[test]
197 fn test_path_too_short_name() {
198 let temp_dir = tempfile::tempdir().unwrap();
199 let tree = Tree::builder(temp_dir.path())
200 .with_scheme(scheme::Utf8)
201 .with_prefix_part_lengths([2, 2])
202 .build()
203 .unwrap();
204
205 let result = tree.path(&"abc".to_string());
207 assert!(result.is_err());
208 assert_eq!(result.unwrap_err(), "abc");
209 }
210
211 #[test]
212 fn test_open_file_nonexistent() -> Result<(), Box<dyn std::error::Error>> {
213 let temp_dir = tempfile::tempdir()?;
214 let tree = Tree::builder(temp_dir.path())
215 .with_scheme(scheme::Utf8)
216 .build()?;
217
218 let result = tree.open_file(&"nonexistent".to_string())?;
219 assert!(
220 result.is_none(),
221 "Should return `Ok(None)` for nonexistent file"
222 );
223
224 Ok(())
225 }
226
227 #[test]
228 fn test_open_file_exists() -> Result<(), Box<dyn std::error::Error>> {
229 let temp_dir = tempfile::tempdir()?;
230 let tree = Tree::builder(temp_dir.path())
231 .with_scheme(scheme::Utf8)
232 .build()?;
233
234 let test_name = "testfile".to_string();
236 let mut file = tree
237 .create_file(&test_name)?
238 .expect("Failed to create file");
239 file.write_all(b"test content")?;
240 drop(file);
241
242 let opened = tree.open_file(&test_name)?;
244 assert!(
245 opened.is_some(),
246 "Should return `Ok(Some(file))` for existing file"
247 );
248
249 Ok(())
250 }
251
252 #[test]
253 fn test_open_file_directory_instead_of_file() -> Result<(), Box<dyn std::error::Error>> {
254 let temp_dir = tempfile::tempdir()?;
255 let tree = Tree::builder(temp_dir.path())
256 .with_scheme(scheme::Utf8)
257 .with_prefix_part_lengths([2])
258 .build()?;
259
260 let mut file = tree
262 .create_file(&"abcd".to_string())?
263 .expect("Failed to create");
264 file.write_all(b"test")?;
265 drop(file);
266
267 let dir_name = "zz".to_string();
268 let dir_path = tree.path(&dir_name)?;
269 std::fs::create_dir_all(&dir_path)?;
270
271 let result = tree.open_file(&dir_name);
273
274 match result {
275 Err(Error::ExpectedFile(_)) | Ok(None) => {
276 }
278 Ok(Some(_)) => {
279 panic!("Should not return `Ok(Some)` for a directory")
280 }
281 other => {
282 panic!("Unexpected result: {other:?}")
283 }
284 }
285
286 Ok(())
287 }
288
289 #[test]
290 fn test_open_file_symlink_to_directory() -> Result<(), Box<dyn std::error::Error>> {
291 #[cfg(unix)]
292 {
293 use std::os::unix::fs::symlink;
294
295 let temp_dir = tempfile::tempdir()?;
296 let tree = Tree::builder(temp_dir.path())
297 .with_scheme(scheme::Utf8)
298 .build()?;
299
300 let dir_path = temp_dir.path().join("somedir");
302 std::fs::create_dir(&dir_path)?;
303
304 let link_name = "symlink".to_string();
305 let link_path = temp_dir.path().join(&link_name);
306 symlink(&dir_path, &link_path)?;
307
308 let result = tree.open_file(&link_name);
310
311 match result {
312 Err(Error::ExpectedFile(_)) | Ok(None) => {
313 }
315 other => panic!("Expected Err or Ok(None), got {other:?}"),
316 }
317 }
318
319 Ok(())
320 }
321
322 #[test]
323 fn test_create_file_idempotent() -> Result<(), Box<dyn std::error::Error>> {
324 let temp_dir = tempfile::tempdir()?;
325 let tree = Tree::builder(temp_dir.path())
326 .with_scheme(scheme::Utf8)
327 .build()?;
328
329 let name = "testfile".to_string();
330
331 let first = tree.create_file(&name)?;
333 assert!(first.is_some(), "First creation should return Some(file)");
334 drop(first);
335
336 let second = tree.create_file(&name)?;
338 assert!(second.is_none(), "Second creation should return None");
339
340 Ok(())
341 }
342
343 #[test]
344 fn test_create_file_with_nested_dirs() -> Result<(), Box<dyn std::error::Error>> {
345 let temp_dir = tempfile::tempdir()?;
346 let tree = Tree::builder(temp_dir.path())
347 .with_scheme(scheme::Utf8)
348 .with_prefix_part_lengths([2, 2, 2])
349 .build()?;
350
351 let name = "abcdefgh".to_string();
352 let mut file = tree.create_file(&name)?.expect("Failed to create file");
353 file.write_all(b"nested")?;
354 drop(file);
355
356 let path = tree.path(&name)?;
358 assert!(path.exists());
359 assert!(path.is_file());
360 assert!(path.to_string_lossy().contains("/ab/cd/ef/"));
361
362 Ok(())
363 }
364
365 #[test]
366 fn test_infer_empty_directory() -> Result<(), Box<dyn std::error::Error>> {
367 let temp_dir = tempfile::tempdir()?;
368
369 let result = Tree::infer_prefix_part_lengths(temp_dir.path())?;
370 assert_eq!(result, None, "Empty directory should return None");
371
372 Ok(())
373 }
374
375 #[test]
376 fn test_infer_with_files_no_subdirs() -> Result<(), Box<dyn std::error::Error>> {
377 let temp_dir = tempfile::tempdir()?;
378
379 std::fs::File::create(temp_dir.path().join("file.txt"))?;
381
382 let result = Tree::infer_prefix_part_lengths(temp_dir.path())?;
383 assert_eq!(
384 result,
385 Some(vec![]),
386 "File in root should give empty prefix list"
387 );
388
389 Ok(())
390 }
391
392 #[test]
393 fn test_infer_with_nested_structure() -> Result<(), Box<dyn std::error::Error>> {
394 let temp_dir = tempfile::tempdir()?;
395
396 let dir1 = temp_dir.path().join("ab");
398 let dir2 = dir1.join("cd");
399 std::fs::create_dir_all(&dir2)?;
400 std::fs::File::create(dir2.join("file.txt"))?;
401
402 let result = Tree::infer_prefix_part_lengths(temp_dir.path())?;
403 assert_eq!(
404 result,
405 Some(vec![2, 2]),
406 "Should infer `[2, 2]` from `ab/cd/`"
407 );
408
409 Ok(())
410 }
411
412 #[test]
413 fn test_infer_on_file_instead_of_directory() {
414 let temp_file = tempfile::NamedTempFile::new().unwrap();
415
416 let result = Tree::infer_prefix_part_lengths(temp_file.path());
417 assert!(
418 result.is_err(),
419 "Should return error when given a file path"
420 );
421 match result {
422 Err(Error::ExpectedDirectory(_)) => (),
423 other => panic!("Expected `Err(ExpectedDirectory)`, got {other:?}"),
424 }
425 }
426
427 #[test]
428 fn test_path_with_empty_prefix_parts() -> Result<(), Box<dyn std::error::Error>> {
429 let temp_dir = tempfile::tempdir()?;
430 let tree = Tree::builder(temp_dir.path())
431 .with_scheme(scheme::Utf8)
432 .with_prefix_part_lengths([])
433 .build()?;
434
435 let path = tree.path(&"filename".to_string())?;
436 assert!(path.to_string_lossy().ends_with("/filename"));
437 assert!(!path.to_string_lossy().contains("//"));
438
439 Ok(())
440 }
441
442 #[test]
443 fn test_entries_iteration() -> Result<(), Box<dyn std::error::Error>> {
444 let temp_dir = tempfile::tempdir()?;
445 let tree = Tree::builder(temp_dir.path())
446 .with_scheme(scheme::Utf8)
447 .with_prefix_part_lengths([1])
448 .build()?;
449
450 let names = vec!["aaa", "abc", "bcd", "bbb"];
452 for name in &names {
453 let mut file = tree
454 .create_file(&(*name).to_string())?
455 .expect("create failed");
456 file.write_all(name.as_bytes())?;
457 drop(file);
458 }
459
460 let entries: Vec<_> = tree.entries().collect::<Result<Vec<_>, _>>()?;
462 assert_eq!(entries.len(), 4, "Should find all four files");
463
464 let entry_names: Vec<String> = entries.iter().map(|e| e.name.clone()).collect();
466 for name in &names {
467 assert!(
468 entry_names.contains(&(*name).to_string()),
469 "Should contain {name}"
470 );
471 }
472
473 Ok(())
474 }
475}