#[cfg(windows)]
use cageforge_path::NativePathKey;
use cageforge_path::{
PathDialect, PlatformPathKey, contains_component_path, contains_parent_traversal,
contains_parent_traversal_text, is_absolute_text, is_within, normalize_lexical_path,
paths_equal, resolve_lexical_path,
};
use proptest::prelude::*;
use std::path::Path;
use std::path::PathBuf;
#[test]
fn containment_is_component_aware() {
assert!(is_within(
Path::new("/workspace/src"),
Path::new("/workspace")
));
assert!(is_within(Path::new("/workspace"), Path::new("/workspace")));
assert!(!is_within(
Path::new("/workspace-other/src"),
Path::new("/workspace")
));
assert!(is_within(Path::new(".git/config"), Path::new(".")));
assert!(paths_equal(Path::new("./src"), Path::new("src")));
}
#[test]
fn containment_rejects_parent_escape_and_mismatched_anchors() {
assert!(!is_within(
Path::new("/workspace/../outside"),
Path::new("/workspace")
));
assert!(!is_within(Path::new("/workspace"), Path::new(".")));
assert!(is_within(Path::new("src/lib.rs"), Path::new(".")));
}
#[test]
fn complete_paths_compare_components() {
assert!(paths_equal(
Path::new("/workspace/./src"),
Path::new("/workspace/src")
));
assert!(!paths_equal(
Path::new("/workspace/src"),
Path::new("/workspace/src2")
));
}
#[test]
fn lexical_normalization_removes_current_directory_without_resolving_parent_traversal() {
assert_eq!(
normalize_lexical_path(Path::new("/workspace/./src//lib.rs")).as_ref(),
Path::new("/workspace/src/lib.rs")
);
assert_eq!(
normalize_lexical_path(Path::new("/workspace/src/../secret")).as_ref(),
Path::new("/workspace/src/../secret")
);
}
#[test]
fn component_path_matching_does_not_match_partial_components() {
assert!(contains_component_path(
Path::new("/workspace/project/.git/config"),
Path::new(".git"),
));
assert!(contains_component_path(
Path::new("/workspace/project/.cache/tool/state"),
Path::new(".cache/tool"),
));
assert!(!contains_component_path(
Path::new("/workspace/project/.github/config"),
Path::new(".git"),
));
}
#[cfg(windows)]
#[test]
fn protected_component_matching_is_case_insensitive_on_windows() {
assert!(contains_component_path(
Path::new(r"C:\workspace\.GIT\config"),
Path::new(".git"),
));
}
#[cfg(not(windows))]
#[test]
fn protected_component_matching_is_case_sensitive_on_posix() {
assert!(!contains_component_path(
Path::new("/workspace/.GIT/config"),
Path::new(".git"),
));
}
#[test]
fn parent_traversal_is_detected_without_filesystem_access() {
assert!(contains_parent_traversal(Path::new("workspace/../outside")));
assert!(!contains_parent_traversal(Path::new("workspace/src")));
}
#[test]
fn target_dialect_validates_foreign_path_text_without_host_filesystem_semantics() {
assert!(is_absolute_text("C:/runtime", PathDialect::Windows));
assert!(is_absolute_text(
r"\\server\share\runtime",
PathDialect::Windows
));
assert!(!is_absolute_text("runtime", PathDialect::Windows));
assert!(is_absolute_text("/runtime", PathDialect::Posix));
assert!(!is_absolute_text(r"C:\runtime", PathDialect::Posix));
assert!(contains_parent_traversal_text(
r"C:\runtime\..\other",
PathDialect::Windows
));
assert!(!contains_parent_traversal_text(
r"C:\runtime\parent.txt",
PathDialect::Windows
));
assert!(contains_parent_traversal_text(
"/runtime/../other",
PathDialect::Posix
));
}
#[test]
fn target_dialect_path_keys_use_the_selected_platform_identity() {
assert_eq!(
PlatformPathKey::new(r"C:\Runtime\bin", PathDialect::Windows),
PlatformPathKey::new("c:/runtime/bin", PathDialect::Windows)
);
assert_eq!(
PlatformPathKey::new(r"\\?\C:\Runtime", PathDialect::Windows),
PlatformPathKey::new("c:/runtime", PathDialect::Windows)
);
assert_ne!(
PlatformPathKey::new("/Runtime", PathDialect::Posix),
PlatformPathKey::new("/runtime", PathDialect::Posix)
);
}
#[test]
fn lexical_resolution_is_shared_and_rejects_unsafe_declarations() {
let base = Path::new("/workspace");
assert_eq!(
resolve_lexical_path(base, Path::new("src/./lib"))
.expect("relative declaration should resolve"),
PathBuf::from("/workspace/src/lib")
);
assert_eq!(
resolve_lexical_path(base, Path::new("/opt/tool"))
.expect("absolute declaration should remain absolute"),
PathBuf::from("/opt/tool")
);
assert_eq!(
resolve_lexical_path(base, Path::new("src/../secret")),
Err(cageforge_path::PathResolutionError::ParentTraversal)
);
assert_eq!(
resolve_lexical_path(base, Path::new("")),
Err(cageforge_path::PathResolutionError::Empty)
);
}
fn absolute_root() -> PathBuf {
#[cfg(windows)]
{
PathBuf::from(r"C:\workspace")
}
#[cfg(not(windows))]
{
PathBuf::from("/workspace")
}
}
fn append_segments(mut path: PathBuf, segments: &[String]) -> PathBuf {
for segment in segments {
path.push(segment);
}
path
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(32))]
#[test]
fn generated_descendants_use_component_aware_native_containment(
root_segments in prop::collection::vec(prop::string::string_regex("[a-z]{1,6}").expect("segment regex"), 0..=4),
child_segments in prop::collection::vec(prop::string::string_regex("[a-z]{1,6}").expect("segment regex"), 0..=4),
) {
let root = append_segments(absolute_root(), &root_segments);
let descendant = append_segments(root.clone(), &child_segments);
let dotted = append_segments(root.join("."), &child_segments);
prop_assert!(is_within(&descendant, &root));
prop_assert!(paths_equal(&descendant, &dotted));
prop_assert!(!is_within(&descendant, &PathBuf::from("/workspace-other")));
prop_assert!(!contains_parent_traversal(&descendant));
}
#[test]
fn generated_metadata_paths_match_only_complete_components(
prefix in prop::collection::vec(prop::string::string_regex("[a-z]{1,6}").expect("segment regex"), 0..=4),
) {
let path = append_segments(absolute_root(), &prefix).join(".git").join("config");
let partial = append_segments(absolute_root(), &prefix).join(".github").join("config");
prop_assert!(contains_component_path(&path, Path::new(".git")));
prop_assert!(!contains_component_path(&partial, Path::new(".git")));
}
#[test]
fn generated_parent_components_are_always_rejected(
segments in prop::collection::vec(prop::string::string_regex("[a-z]{1,6}").expect("segment regex"), 0..=4),
) {
let escaped = append_segments(absolute_root(), &segments).join("..").join("outside");
prop_assert!(contains_parent_traversal(&escaped));
}
}
#[cfg(windows)]
#[test]
fn windows_path_comparison_is_case_insensitive() {
assert!(is_within(
Path::new(r"C:\Workspace\src"),
Path::new(r"c:\workspace")
));
assert!(paths_equal(
Path::new(r"C:\Workspace"),
Path::new(r"c:\workspace")
));
}
#[cfg(windows)]
#[test]
fn windows_device_and_verbatim_aliases_share_one_identity() {
for alias in [r"\\?\C:\Workspace\src", r"\\.\C:\Workspace\src"] {
assert!(paths_equal(
Path::new(alias),
Path::new(r"c:\workspace\src")
));
assert_eq!(
NativePathKey::new(Path::new(alias)),
NativePathKey::new(Path::new(r"C:\WORKSPACE\SRC"))
);
}
for alias in [r"\\?\UNC\server\share\src", r"\\.\UNC\server\share\src"] {
assert!(paths_equal(
Path::new(alias),
Path::new(r"\\SERVER\SHARE\SRC")
));
}
}
#[cfg(windows)]
#[test]
fn malformed_utf16_paths_remain_distinct() {
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
let left = PathBuf::from(OsString::from_wide(&[0xD800]));
let right = PathBuf::from(OsString::from_wide(&[0xD801]));
assert!(!paths_equal(&left, &right));
assert_ne!(NativePathKey::new(&left), NativePathKey::new(&right));
}
#[cfg(not(windows))]
#[test]
fn posix_path_comparison_is_case_sensitive() {
assert!(!is_within(
Path::new("/Workspace/src"),
Path::new("/workspace")
));
assert!(!paths_equal(
Path::new("/Workspace"),
Path::new("/workspace")
));
}