|
2 | 2 |
|
3 | 3 | use std::fs::{self, File}; |
4 | 4 | use std::io::{self, Write as _}; |
5 | | -use std::path::Path; |
| 5 | +use std::path::{Component, Path, PathBuf}; |
| 6 | + |
| 7 | +/// Resolves an output path through its nearest existing ancestor. |
| 8 | +/// |
| 9 | +/// Missing descendants are normalized lexically so a report path can be checked |
| 10 | +/// before its parent directory is created. This does not lock directory entries. |
| 11 | +/// |
| 12 | +/// # Errors |
| 13 | +/// |
| 14 | +/// Returns an error when the current directory or an existing ancestor cannot |
| 15 | +/// be resolved. |
| 16 | +pub fn output_path_identity(path: &Path) -> io::Result<PathBuf> { |
| 17 | + let absolute = std::path::absolute(path)?; |
| 18 | + let mut ancestor = absolute.as_path(); |
| 19 | + let mut suffix = Vec::new(); |
| 20 | + loop { |
| 21 | + match fs::canonicalize(ancestor) { |
| 22 | + Ok(mut resolved) => { |
| 23 | + for component in suffix.into_iter().rev() { |
| 24 | + match component { |
| 25 | + Component::ParentDir => { |
| 26 | + resolved.pop(); |
| 27 | + } |
| 28 | + Component::CurDir => {} |
| 29 | + other => resolved.push(other.as_os_str()), |
| 30 | + } |
| 31 | + } |
| 32 | + return Ok(resolved); |
| 33 | + } |
| 34 | + Err(error) if error.kind() == io::ErrorKind::NotFound => { |
| 35 | + let component = ancestor |
| 36 | + .components() |
| 37 | + .next_back() |
| 38 | + .ok_or_else(|| io::Error::other("output path has no existing ancestor"))?; |
| 39 | + suffix.push(component); |
| 40 | + ancestor = ancestor |
| 41 | + .parent() |
| 42 | + .ok_or_else(|| io::Error::other("output path has no existing ancestor"))?; |
| 43 | + } |
| 44 | + Err(error) => return Err(error), |
| 45 | + } |
| 46 | + } |
| 47 | +} |
6 | 48 |
|
7 | 49 | /// Writes bytes to a sibling temporary file, then replaces the destination. |
8 | 50 | /// |
@@ -64,6 +106,27 @@ fn write_atomic_with( |
64 | 106 | mod tests { |
65 | 107 | use super::*; |
66 | 108 |
|
| 109 | + #[test] |
| 110 | + fn output_identity_resolves_missing_parent_aliases_without_creating_them() { |
| 111 | + let directory = tempfile::tempdir().unwrap(); |
| 112 | + let direct = directory.path().join("document.nuif"); |
| 113 | + let indirect = directory |
| 114 | + .path() |
| 115 | + .join("missing") |
| 116 | + .join("..") |
| 117 | + .join("document.nuif"); |
| 118 | + assert_eq!( |
| 119 | + output_path_identity(&direct).unwrap(), |
| 120 | + output_path_identity(&indirect).unwrap() |
| 121 | + ); |
| 122 | + assert_eq!(fs::read_dir(directory.path()).unwrap().count(), 0); |
| 123 | + fs::write(&direct, b"existing").unwrap(); |
| 124 | + assert_eq!( |
| 125 | + output_path_identity(&direct).unwrap(), |
| 126 | + output_path_identity(&indirect).unwrap() |
| 127 | + ); |
| 128 | + } |
| 129 | + |
67 | 130 | #[test] |
68 | 131 | fn failed_staged_write_preserves_existing_file_and_cleans_temporary_file() { |
69 | 132 | let directory = tempfile::tempdir().unwrap(); |
|
0 commit comments