Skip to content

Commit 0b83d2e

Browse files
committed
Move test support into common crate
1 parent b6c1f61 commit 0b83d2e

7 files changed

Lines changed: 135 additions & 121 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ behavior when the change improves security or project direction.
1919
existing runtime code.
2020
- Move shared forward-path safety validation into `fluxheim-common` while
2121
keeping the root `crate::path_safety` adapter for existing proxy/cache code.
22+
- Move repository-local test path helpers behind the `fluxheim-common`
23+
`test-support` feature while keeping the root `crate::test_support` adapter
24+
for existing tests.
2225
- Keep all feature profiles, binaries, release scripts, RPM/container behavior,
2326
config syntax, and runtime behavior unchanged in this first workspace slice.
2427

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ zeroize = { version = "1.8.2", features = ["derive"], optional = true }
218218
zstd = { version = "0.13.3", default-features = false, optional = true }
219219

220220
[dev-dependencies]
221+
fluxheim-common = { path = "crates/fluxheim-common", features = ["test-support"] }
221222
proptest = "1.6.0"
222223

223224
[profile.release]

crates/fluxheim-common/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,8 @@ license = "EUPL-1.2"
77
publish = false
88
description = "Shared internal Fluxheim primitives."
99

10+
[features]
11+
test-support = []
12+
1013
[dependencies]
1114
thiserror = "2.0.18"

crates/fluxheim-common/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@
66

77
pub mod error;
88
pub mod path_safety;
9+
#[cfg(feature = "test-support")]
10+
pub mod test_support;
911

1012
pub use error::{FluxError, FluxResult};
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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+
}

release-notes/RELEASE_NOTES_1.5.17.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ PHP-FPM, Wasm, and runtime extractions smaller and safer.
1414
- Moved the Fluxheim-owned `FluxError` and `FluxResult` boundary into
1515
`fluxheim-common`.
1616
- Moved shared forward-path safety validation into `fluxheim-common`.
17+
- Moved repository-local test path helpers behind the `fluxheim-common`
18+
`test-support` feature.
1719
- Kept root compatibility adapters at `crate::flux_error` and
18-
`crate::path_safety` so existing modules continue to compile without broad
19-
churn.
20+
`crate::path_safety`, plus the test-only `crate::test_support` adapter, so
21+
existing modules and tests continue to compile without broad churn.
2022
- Kept Pingora-specific error conversion at the root adapter boundary instead
2123
of moving Pingora dependencies into the common crate.
2224

src/test_support.rs

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

0 commit comments

Comments
 (0)