|
| 1 | +#![allow(clippy::expect_used, clippy::panic)] |
| 2 | + |
| 3 | +use std::path::{Path, PathBuf}; |
| 4 | +use std::sync::atomic::{AtomicU64, Ordering}; |
| 5 | + |
| 6 | +static NEXT_TEST_PATH: AtomicU64 = AtomicU64::new(0); |
| 7 | + |
| 8 | +pub fn unique_temp_path(label: &str) -> PathBuf { |
| 9 | + assert_safe_label(label); |
| 10 | + let root = test_root(); |
| 11 | + let nanos = std::time::SystemTime::now() |
| 12 | + .duration_since(std::time::UNIX_EPOCH) |
| 13 | + .expect("system clock before Unix epoch") |
| 14 | + .as_nanos(); |
| 15 | + safe_relative_path( |
| 16 | + &root, |
| 17 | + &format!( |
| 18 | + "fluxheim-test-{}-{}-{}", |
| 19 | + std::process::id(), |
| 20 | + nanos, |
| 21 | + NEXT_TEST_PATH.fetch_add(1, Ordering::Relaxed) |
| 22 | + ), |
| 23 | + ) |
| 24 | +} |
| 25 | + |
| 26 | +pub fn test_root() -> PathBuf { |
| 27 | + let root = PathBuf::from("target/fluxheim-test-tmp"); |
| 28 | + std::fs::create_dir_all(&root).expect("create repository-local test root"); |
| 29 | + root.canonicalize() |
| 30 | + .expect("canonicalize repository-local test root") |
| 31 | +} |
| 32 | + |
| 33 | +pub fn safe_child_path(base: &Path, name: &str) -> PathBuf { |
| 34 | + assert_safe_label(name); |
| 35 | + base.join(name) |
| 36 | +} |
| 37 | + |
| 38 | +pub fn safe_relative_path(base: &Path, relative: &str) -> PathBuf { |
| 39 | + assert!(!relative.is_empty(), "test relative path cannot be empty"); |
| 40 | + assert!( |
| 41 | + !relative.starts_with('/') && !relative.starts_with('\\'), |
| 42 | + "test relative path must stay relative: {relative:?}" |
| 43 | + ); |
| 44 | + let mut path = base.to_path_buf(); |
| 45 | + for component in relative.split('/') { |
| 46 | + assert_safe_label(component); |
| 47 | + path.push(component); |
| 48 | + } |
| 49 | + path |
| 50 | +} |
| 51 | + |
| 52 | +#[cfg(unix)] |
| 53 | +pub fn unique_world_writable_child(label: &str, child: &str) -> PathBuf { |
| 54 | + let parent = unique_temp_path(label); |
| 55 | + std::fs::create_dir_all(&parent).expect("create world-writable test parent"); |
| 56 | + use std::os::unix::fs::PermissionsExt; |
| 57 | + std::fs::set_permissions(&parent, std::fs::Permissions::from_mode(0o777)) |
| 58 | + .expect("mark test parent world-writable"); |
| 59 | + safe_relative_path(&parent, child) |
| 60 | +} |
| 61 | + |
| 62 | +#[cfg(unix)] |
| 63 | +pub fn unique_group_writable_child(label: &str, child: &str) -> PathBuf { |
| 64 | + let parent = unique_temp_path(label); |
| 65 | + std::fs::create_dir_all(&parent).expect("create group-writable test parent"); |
| 66 | + use std::os::unix::fs::PermissionsExt; |
| 67 | + std::fs::set_permissions(&parent, std::fs::Permissions::from_mode(0o770)) |
| 68 | + .expect("mark test parent group-writable"); |
| 69 | + safe_relative_path(&parent, child) |
| 70 | +} |
| 71 | + |
| 72 | +fn assert_safe_label(label: &str) { |
| 73 | + assert!(!label.is_empty(), "test path label cannot be empty"); |
| 74 | + assert!( |
| 75 | + label.len() <= 128, |
| 76 | + "test path label is too long: {} bytes", |
| 77 | + label.len() |
| 78 | + ); |
| 79 | + assert!( |
| 80 | + label |
| 81 | + .bytes() |
| 82 | + .all(|byte| { byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-') }), |
| 83 | + "test path label must be a single ASCII-safe path component: {label:?}" |
| 84 | + ); |
| 85 | + assert!( |
| 86 | + !label.contains(".."), |
| 87 | + "test path label must not contain parent-directory markers: {label:?}" |
| 88 | + ); |
| 89 | +} |
| 90 | + |
| 91 | +#[cfg(test)] |
| 92 | +mod tests { |
| 93 | + use super::{safe_child_path, safe_relative_path, unique_temp_path}; |
| 94 | + |
| 95 | + #[test] |
| 96 | + fn unique_temp_path_does_not_embed_label() { |
| 97 | + let path = unique_temp_path("example-label"); |
| 98 | + assert!(!path.display().to_string().contains("example-label")); |
| 99 | + } |
| 100 | + |
| 101 | + #[test] |
| 102 | + fn safe_relative_path_allows_nested_safe_components() { |
| 103 | + let path = safe_relative_path( |
| 104 | + std::path::Path::new("target/fluxheim-test-tmp"), |
| 105 | + "logs/fluxheim.log", |
| 106 | + ); |
| 107 | + assert_eq!( |
| 108 | + path, |
| 109 | + std::path::Path::new("target/fluxheim-test-tmp/logs/fluxheim.log") |
| 110 | + ); |
| 111 | + } |
| 112 | + |
| 113 | + #[test] |
| 114 | + #[should_panic(expected = "single ASCII-safe path component")] |
| 115 | + fn rejects_unsafe_labels() { |
| 116 | + let _ = safe_child_path( |
| 117 | + std::path::Path::new("target/fluxheim-test-tmp"), |
| 118 | + "../escape", |
| 119 | + ); |
| 120 | + } |
| 121 | +} |
0 commit comments