use bashkit::{Bash, FileSystem, InMemoryFs, MountableFs, OverlayFs};
use std::path::Path;
use std::sync::Arc;
#[tokio::test]
async fn overlay_rename_preserves_symlink() {
let lower = Arc::new(InMemoryFs::new()) as Arc<dyn FileSystem>;
let overlay = Arc::new(OverlayFs::new(lower));
overlay
.write_file(Path::new("/target.txt"), b"hello")
.await
.unwrap();
overlay
.symlink(Path::new("/target.txt"), Path::new("/link1"))
.await
.unwrap();
overlay
.rename(Path::new("/link1"), Path::new("/link2"))
.await
.unwrap();
let target = overlay.read_link(Path::new("/link2")).await.unwrap();
assert_eq!(target, Path::new("/target.txt"));
assert!(!overlay.exists(Path::new("/link1")).await.unwrap());
}
#[tokio::test]
async fn overlay_rename_symlink_from_lower_layer() {
let lower = Arc::new(InMemoryFs::new());
lower
.write_file(Path::new("/data.txt"), b"secret")
.await
.unwrap();
lower
.symlink(Path::new("/data.txt"), Path::new("/link_lower"))
.await
.unwrap();
let overlay = Arc::new(OverlayFs::new(lower.clone() as Arc<dyn FileSystem>));
overlay
.rename(Path::new("/link_lower"), Path::new("/link_upper"))
.await
.unwrap();
let target = overlay.read_link(Path::new("/link_upper")).await.unwrap();
assert_eq!(target, Path::new("/data.txt"));
}
#[tokio::test]
async fn overlay_copy_preserves_symlink() {
let lower = Arc::new(InMemoryFs::new()) as Arc<dyn FileSystem>;
let overlay = Arc::new(OverlayFs::new(lower));
overlay
.write_file(Path::new("/target.txt"), b"data")
.await
.unwrap();
overlay
.symlink(Path::new("/target.txt"), Path::new("/link"))
.await
.unwrap();
overlay
.copy(Path::new("/link"), Path::new("/link_copy"))
.await
.unwrap();
let target = overlay.read_link(Path::new("/link_copy")).await.unwrap();
assert_eq!(target, Path::new("/target.txt"));
let orig_target = overlay.read_link(Path::new("/link")).await.unwrap();
assert_eq!(orig_target, Path::new("/target.txt"));
}
#[tokio::test]
async fn symlink_rename_cannot_escape_mount_via_read() {
let lower = Arc::new(InMemoryFs::new());
lower.mkdir(Path::new("/etc"), false).await.unwrap();
lower
.write_file(Path::new("/etc/passwd"), b"root:x:0:0")
.await
.unwrap();
lower.mkdir(Path::new("/sandbox"), false).await.unwrap();
let overlay = Arc::new(OverlayFs::new(lower as Arc<dyn FileSystem>));
overlay
.symlink(Path::new("/etc/passwd"), Path::new("/sandbox/evil"))
.await
.unwrap();
overlay
.rename(Path::new("/sandbox/evil"), Path::new("/sandbox/moved"))
.await
.unwrap();
let target = overlay
.read_link(Path::new("/sandbox/moved"))
.await
.unwrap();
assert_eq!(target, Path::new("/etc/passwd"));
let result = overlay.read_file(Path::new("/sandbox/moved")).await;
assert!(result.is_err(), "read_file on symlink must not follow it");
}
#[tokio::test]
async fn mountable_cross_mount_rename_preserves_symlink() {
let root = Arc::new(InMemoryFs::new());
let mount_a = Arc::new(InMemoryFs::new());
let mount_b = Arc::new(InMemoryFs::new());
mount_a
.symlink(Path::new("/target.txt"), Path::new("/link"))
.await
.unwrap();
let mountable = MountableFs::new(root as Arc<dyn FileSystem>);
mountable
.mount("/mnt/a", mount_a as Arc<dyn FileSystem>)
.unwrap();
mountable
.mount("/mnt/b", mount_b as Arc<dyn FileSystem>)
.unwrap();
mountable
.rename(Path::new("/mnt/a/link"), Path::new("/mnt/b/link"))
.await
.unwrap();
let target = mountable.read_link(Path::new("/mnt/b/link")).await.unwrap();
assert_eq!(target, Path::new("/target.txt"));
assert!(!mountable.exists(Path::new("/mnt/a/link")).await.unwrap());
}
#[tokio::test]
async fn mountable_cross_mount_copy_preserves_symlink() {
let root = Arc::new(InMemoryFs::new());
let mount_a = Arc::new(InMemoryFs::new());
let mount_b = Arc::new(InMemoryFs::new());
mount_a
.symlink(Path::new("/target.txt"), Path::new("/link"))
.await
.unwrap();
let mountable = MountableFs::new(root as Arc<dyn FileSystem>);
mountable
.mount("/mnt/a", mount_a as Arc<dyn FileSystem>)
.unwrap();
mountable
.mount("/mnt/b", mount_b as Arc<dyn FileSystem>)
.unwrap();
mountable
.copy(Path::new("/mnt/a/link"), Path::new("/mnt/b/link"))
.await
.unwrap();
let target_a = mountable.read_link(Path::new("/mnt/a/link")).await.unwrap();
let target_b = mountable.read_link(Path::new("/mnt/b/link")).await.unwrap();
assert_eq!(target_a, Path::new("/target.txt"));
assert_eq!(target_b, Path::new("/target.txt"));
}
#[tokio::test]
async fn bash_mv_symlink_in_overlay() {
let lower = Arc::new(InMemoryFs::new()) as Arc<dyn FileSystem>;
let overlay = Arc::new(OverlayFs::new(lower));
overlay
.write_file(Path::new("/tmp/target.txt"), b"content")
.await
.unwrap();
overlay.mkdir(Path::new("/tmp"), true).await.unwrap_or(());
let mut bash = Bash::builder().fs(overlay.clone()).build();
bash.exec("ln -s /tmp/target.txt /tmp/mylink")
.await
.unwrap();
let result = bash.exec("mv /tmp/mylink /tmp/renamed_link").await.unwrap();
assert_eq!(result.exit_code, 0, "mv should succeed: {}", result.stderr);
let result = bash.exec("readlink /tmp/renamed_link").await.unwrap();
assert_eq!(result.stdout.trim(), "/tmp/target.txt");
}