use crate::{CodeWalker, WalkConfig};
use std::fs;
#[cfg(unix)]
use std::os::unix::ffi::OsStringExt;
#[cfg(unix)]
use std::os::unix::fs::symlink as unix_symlink;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[cfg(windows)]
use std::os::windows::fs::symlink_dir as windows_symlink;
#[cfg(unix)]
#[test]
fn adversarial_symlink_loop_detection() {
let dir = tempfile::tempdir().unwrap();
let a = dir.path().join("a");
let b = dir.path().join("b");
let c = dir.path().join("c");
fs::create_dir(&a).unwrap();
fs::create_dir(&b).unwrap();
fs::create_dir(&c).unwrap();
unix_symlink(&b, a.join("link")).unwrap();
unix_symlink(&c, b.join("link")).unwrap();
unix_symlink(&a, c.join("link")).unwrap();
let config = WalkConfig {
follow_symlinks: true,
..WalkConfig::default()
};
let walker = CodeWalker::new(dir.path(), config);
let results: Vec<_> = walker.walk_iter().collect();
assert!(
results.iter().any(Result::is_err),
"symlink loop should surface an error instead of hanging"
);
let entries: Vec<_> = results.into_iter().filter_map(Result::ok).collect();
assert!(entries.len() <= 100); }
#[cfg(unix)]
#[test]
fn adversarial_permission_denied_directory() {
let dir = tempfile::tempdir().unwrap();
let accessible = dir.path().join("accessible.txt");
fs::write(&accessible, "accessible").unwrap();
let blocked = dir.path().join("blocked");
fs::create_dir(&blocked).unwrap();
let blocked_file = blocked.join("secret.txt");
fs::write(&blocked_file, "secret").unwrap();
let mut perms = fs::metadata(&blocked).unwrap().permissions();
perms.set_mode(0o000);
fs::set_permissions(&blocked, perms).unwrap();
let walker = CodeWalker::new(dir.path(), WalkConfig::default());
let results: Vec<_> = walker.walk_iter().collect();
let mut perms = fs::metadata(&blocked).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&blocked, perms).unwrap();
let entries: Vec<_> = results
.iter()
.filter_map(|result| result.as_ref().ok())
.collect();
let paths: Vec<_> = entries.iter().map(|e| e.path.clone()).collect();
assert!(paths.contains(&accessible));
assert!(!paths.contains(&blocked_file));
}
#[test]
fn adversarial_10k_files_in_directory() {
let dir = tempfile::tempdir().unwrap();
let files_dir = dir.path().join("files");
fs::create_dir(&files_dir).unwrap();
for i in 0..10_000 {
let path = files_dir.join(format!("file_{:05}.txt", i));
fs::write(&path, format!("content {}", i)).unwrap();
}
let walker = CodeWalker::new(&files_dir, WalkConfig::default());
let entries = walker.walk().unwrap();
assert_eq!(entries.len(), 10_000);
}
#[cfg(unix)]
#[test]
fn adversarial_non_utf8_paths() {
use std::ffi::OsString;
let dir = tempfile::tempdir().unwrap();
let bad_bytes = {
let mut raw = b"file-\xff\xfe".to_vec();
raw.extend_from_slice(b".txt");
OsString::from_vec(raw)
};
let bad_path = dir.path().join(&bad_bytes);
fs::write(&bad_path, "content").unwrap();
let bad_dir = dir.path().join(OsString::from_vec(b"dir-\xff".to_vec()));
fs::create_dir(&bad_dir).unwrap();
fs::write(bad_dir.join("inside.txt"), "inside").unwrap();
let walker = CodeWalker::new(dir.path(), WalkConfig::default());
let entries = walker.walk().unwrap();
assert_eq!(entries.len(), 2);
}
#[test]
fn adversarial_deeply_nested_empty_dirs() {
let dir = tempfile::tempdir().unwrap();
let mut current = dir.path().to_path_buf();
for i in 0..100 {
current = current.join(format!("level_{:03}", i));
fs::create_dir(¤t).unwrap();
}
fs::write(current.join("deep.txt"), "deep content").unwrap();
let walker = CodeWalker::new(dir.path(), WalkConfig::default());
let entries = walker.walk().unwrap();
assert_eq!(entries.len(), 1);
assert!(entries[0].path.ends_with("deep.txt"));
}
#[test]
fn adversarial_files_changing_during_walk() {
let dir = tempfile::tempdir().unwrap();
for i in 0..100 {
fs::write(dir.path().join(format!("file_{}.txt", i)), "initial").unwrap();
}
let walker = CodeWalker::new(dir.path(), WalkConfig::default());
let dir_path = dir.path().to_path_buf();
let modifier = std::thread::spawn(move || {
for i in 0..100 {
let _ = fs::write(dir_path.join(format!("file_{}.txt", i)), "modified");
std::thread::sleep(std::time::Duration::from_millis(1));
}
});
let entries = walker.walk().unwrap();
modifier.join().unwrap();
assert_eq!(entries.len(), 100);
}
#[test]
fn adversarial_directory_deleted_during_walk() {
let base = tempfile::tempdir().unwrap();
let victim = base.path().join("victim");
fs::create_dir(&victim).unwrap();
for i in 0..50 {
let sub = victim.join(format!("sub_{}", i));
fs::create_dir(&sub).unwrap();
fs::write(sub.join("file.txt"), "content").unwrap();
}
let walker = CodeWalker::new(&victim, WalkConfig::default());
let entries = walker.walk().unwrap_or_default();
assert!(
entries.len() <= 50,
"walk should not duplicate entries excessively"
);
}
#[cfg(unix)]
#[test]
fn adversarial_circular_symlink_to_parent() {
let dir = tempfile::tempdir().unwrap();
let parent = dir.path().join("parent");
fs::create_dir(&parent).unwrap();
unix_symlink(&parent, parent.join("loop")).unwrap();
fs::write(parent.join("real.txt"), "real").unwrap();
let config = WalkConfig {
follow_symlinks: true,
..WalkConfig::default()
};
let walker = CodeWalker::new(&parent, config);
let results: Vec<_> = walker.walk_iter().collect();
let entries: Vec<_> = results.into_iter().filter_map(Result::ok).collect();
assert!(entries.iter().any(|e| e.path.ends_with("real.txt")));
}
#[test]
fn adversarial_very_long_file_paths() {
let dir = tempfile::tempdir().unwrap();
let long_name = "a".repeat(200);
let long_path = dir.path().join(format!("{}.txt", long_name));
fs::write(&long_path, "content").unwrap();
let walker = CodeWalker::new(dir.path(), WalkConfig::default());
let entries = walker.walk().unwrap();
assert_eq!(entries.len(), 1);
}
#[cfg(unix)]
#[test]
fn adversarial_mixed_symlinks_and_files() {
let dir = tempfile::tempdir().unwrap();
let real_dir = dir.path().join("real");
fs::create_dir(&real_dir).unwrap();
for i in 0..50 {
let real_file = real_dir.join(format!("real_{}.txt", i));
fs::write(&real_file, format!("content {}", i)).unwrap();
let link = dir.path().join(format!("link_{}.txt", i));
unix_symlink(&real_file, &link).unwrap();
}
let config = WalkConfig {
follow_symlinks: true,
..WalkConfig::default()
};
let walker = CodeWalker::new(dir.path(), config);
let entries = walker.walk().unwrap();
assert!(entries.len() >= 50);
}
#[test]
fn adversarial_binary_files_with_nulls() {
let dir = tempfile::tempdir().unwrap();
for i in 0..10 {
let mut content = vec![b'A'; 100];
for j in 0..=5 {
content[j * 2] = 0;
}
fs::write(dir.path().join(format!("binary_{}.bin", i)), content).unwrap();
}
let config = WalkConfig::default();
let walker = CodeWalker::new(dir.path(), config);
let entries = walker.walk().unwrap();
assert_eq!(entries.len(), 0);
}
#[test]
fn adversarial_parallel_walk_contention() {
let dir = tempfile::tempdir().unwrap();
for i in 0..1000 {
fs::write(
dir.path().join(format!("file_{}.txt", i)),
format!("content {}", i),
)
.unwrap();
}
let walker = CodeWalker::new(dir.path(), WalkConfig::default());
let mut handles = vec![];
for _ in 0..5 {
let rx = walker.walk_parallel(4);
handles.push(std::thread::spawn(move || {
rx.iter().collect::<Result<Vec<_>, _>>().unwrap().len()
}));
}
let counts: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
for count in counts {
assert_eq!(count, 1000);
}
}
#[test]
fn adversarial_zero_max_file_size() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("small.txt"), "x").unwrap();
fs::write(dir.path().join("medium.txt"), "x".repeat(1000)).unwrap();
fs::write(dir.path().join("large.txt"), "x".repeat(1000000)).unwrap();
let config = WalkConfig {
max_file_size: 0, ..WalkConfig::default()
};
let walker = CodeWalker::new(dir.path(), config);
let entries = walker.walk().unwrap();
assert_eq!(entries.len(), 3);
}
#[test]
fn adversarial_conflicting_extension_filters() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("a.rs"), "rust").unwrap();
fs::write(dir.path().join("b.py"), "python").unwrap();
fs::write(dir.path().join("c.js"), "javascript").unwrap();
fs::write(dir.path().join("d.txt"), "text").unwrap();
let config = WalkConfig {
include_extensions: ["rs".to_string(), "py".to_string()].into_iter().collect(),
exclude_extensions: ["py".to_string()].into_iter().collect(), ..WalkConfig::default()
};
let walker = CodeWalker::new(dir.path(), config);
let entries = walker.walk().unwrap();
let paths: Vec<_> = entries.iter().map(|e| e.path.clone()).collect();
assert!(paths.iter().any(|p| p.to_string_lossy().ends_with(".rs")));
assert!(!paths.iter().any(|p| p.to_string_lossy().ends_with(".py")));
}
#[test]
fn adversarial_small_file_reads_without_mmap_config() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("test.txt"), "small content").unwrap();
let walker = CodeWalker::new(dir.path(), WalkConfig::default());
let entries = walker.walk().unwrap();
assert_eq!(entries.len(), 1);
let content = entries[0].content();
assert!(content.is_ok());
}
#[test]
fn adversarial_files_deleted_after_discovery() {
let dir = tempfile::tempdir().unwrap();
for i in 0..20 {
fs::write(
dir.path().join(format!("temp_{}.txt", i)),
format!("temp {}", i),
)
.unwrap();
}
let walker = CodeWalker::new(dir.path(), WalkConfig::default());
let entries = walker.walk().unwrap();
for i in 0..20 {
let _ = fs::remove_file(dir.path().join(format!("temp_{}.txt", i)));
}
for entry in entries {
let error = entry.content().unwrap_err();
match error {
crate::error::CodewalkError::Io(io_error) => {
assert_eq!(io_error.kind(), std::io::ErrorKind::NotFound);
}
other => panic!("expected io error, got {other:?}"),
}
}
}
#[test]
fn adversarial_hidden_and_special_files() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join(".hidden"), "hidden").unwrap();
fs::write(dir.path().join("..double_dot"), "double").unwrap();
fs::write(dir.path().join("-dash_start"), "dash").unwrap();
fs::write(dir.path().join(" space file "), "space").unwrap();
fs::write(dir.path().join("special!@#$%"), "special").unwrap();
let config = WalkConfig {
skip_hidden: false, ..WalkConfig::default()
};
let walker = CodeWalker::new(dir.path(), config);
let entries = walker.walk().unwrap();
assert!(entries.len() >= 4);
}
#[test]
fn adversarial_concurrent_directory_modification() {
let dir = tempfile::tempdir().unwrap();
let walker = CodeWalker::new(dir.path(), WalkConfig::default());
let dir_path = dir.path().to_path_buf();
let modifier = std::thread::spawn(move || {
for i in 0..50 {
let file = dir_path.join(format!("concurrent_{}.txt", i));
fs::write(&file, "content").unwrap();
std::thread::sleep(std::time::Duration::from_millis(5));
}
});
let entries = walker.walk().unwrap_or_default();
modifier.join().unwrap();
assert!(
entries.len() <= 50,
"concurrent creation should not produce duplicates"
);
}