use std::io::Write as _;
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::thread;
use super::{Receiver, ScpRecvOptions, ScpSendOptions, Sender};
fn fresh_tmp(label: &str) -> PathBuf {
use std::sync::atomic::{AtomicU32, Ordering};
static COUNTER: AtomicU32 = AtomicU32::new(0);
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let pid = std::process::id();
let dir = std::env::temp_dir().join(format!("puressh-scp-test-{}-{}-{}", label, pid, n));
std::fs::create_dir_all(&dir).expect("tmp dir");
dir
}
struct DirGuard(PathBuf);
impl Drop for DirGuard {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[test]
fn round_trip_single_file() {
let src_dir = fresh_tmp("send");
let _g1 = DirGuard(src_dir.clone());
let dst_dir = fresh_tmp("recv");
let _g2 = DirGuard(dst_dir.clone());
let src_path = src_dir.join("hello.txt");
let payload = b"hello, scp world!\n";
std::fs::write(&src_path, payload).unwrap();
let (a, b) = UnixStream::pair().expect("socketpair");
let dst_dir_thread = dst_dir.clone();
let recv = thread::spawn(move || {
let mut r = Receiver::new(
b,
&dst_dir_thread,
ScpRecvOptions {
recursive: false,
preserve_times: false,
target_is_file: false,
},
)
.expect("recv new");
r.run().expect("recv run");
});
let mut sender = Sender::new(a).expect("send new");
sender
.send_path(&src_path, &ScpSendOptions::default())
.expect("send file");
drop(sender); recv.join().expect("recv join");
let got = std::fs::read(dst_dir.join("hello.txt")).expect("read dst");
assert_eq!(got, payload);
}
#[test]
fn round_trip_directory_tree() {
let src_dir = fresh_tmp("send-tree");
let _g1 = DirGuard(src_dir.clone());
let dst_dir = fresh_tmp("recv-tree");
let _g2 = DirGuard(dst_dir.clone());
let tree = src_dir.join("tree");
std::fs::create_dir_all(tree.join("a/b")).unwrap();
std::fs::write(tree.join("top.txt"), b"top").unwrap();
std::fs::write(tree.join("a/middle.txt"), b"middle").unwrap();
std::fs::write(tree.join("a/b/deep.txt"), b"deep").unwrap();
let (a, b) = UnixStream::pair().expect("socketpair");
let dst_dir_thread = dst_dir.clone();
let recv = thread::spawn(move || {
let mut r = Receiver::new(
b,
&dst_dir_thread,
ScpRecvOptions {
recursive: true,
preserve_times: false,
target_is_file: false,
},
)
.expect("recv new");
r.run().expect("recv run");
});
let mut sender = Sender::new(a).expect("send new");
let opts = ScpSendOptions {
recursive: true,
preserve_times: false,
};
sender.send_path(&tree, &opts).expect("send tree");
drop(sender);
recv.join().expect("recv join");
assert_eq!(std::fs::read(dst_dir.join("tree/top.txt")).unwrap(), b"top");
assert_eq!(
std::fs::read(dst_dir.join("tree/a/middle.txt")).unwrap(),
b"middle"
);
assert_eq!(
std::fs::read(dst_dir.join("tree/a/b/deep.txt")).unwrap(),
b"deep"
);
}
#[test]
fn round_trip_preserve_times() {
let src_dir = fresh_tmp("send-t");
let _g1 = DirGuard(src_dir.clone());
let dst_dir = fresh_tmp("recv-t");
let _g2 = DirGuard(dst_dir.clone());
let src_path = src_dir.join("t.txt");
std::fs::write(&src_path, b"hello").unwrap();
let (a, b) = UnixStream::pair().expect("socketpair");
let dst_dir_thread = dst_dir.clone();
let recv = thread::spawn(move || {
let mut r = Receiver::new(
b,
&dst_dir_thread,
ScpRecvOptions {
recursive: false,
preserve_times: true,
target_is_file: false,
},
)
.expect("recv new");
r.run().expect("recv run");
});
let mut sender = Sender::new(a).expect("send new");
let opts = ScpSendOptions {
recursive: false,
preserve_times: true,
};
sender.send_path(&src_path, &opts).expect("send");
drop(sender);
recv.join().expect("recv join");
assert_eq!(std::fs::read(dst_dir.join("t.txt")).unwrap(), b"hello");
}
#[test]
fn receiver_refuses_directory_without_recursive() {
let src_dir = fresh_tmp("send-nor");
let _g1 = DirGuard(src_dir.clone());
let dst_dir = fresh_tmp("recv-nor");
let _g2 = DirGuard(dst_dir.clone());
let tree = src_dir.join("noR");
std::fs::create_dir(&tree).unwrap();
std::fs::write(tree.join("f"), b"x").unwrap();
let (a, b) = UnixStream::pair().expect("socketpair");
let dst_dir_thread = dst_dir.clone();
let recv = thread::spawn(move || {
let mut r = Receiver::new(
b,
&dst_dir_thread,
ScpRecvOptions::default(), )
.expect("recv new");
r.run() });
let mut sender = Sender::new(a).expect("send new");
let opts = ScpSendOptions {
recursive: true,
..Default::default()
};
let _ = sender.send_path(&tree, &opts);
drop(sender);
let r = recv.join().expect("recv join");
assert!(r.is_err());
}
#[test]
fn validate_rejects_leading_dash() {
use super::protocol::{validate_name, ScpError};
assert!(matches!(validate_name("-rf"), Err(ScpError::BadName(_))));
}
#[test]
fn validate_rejects_dot_name() {
use super::protocol::{validate_name, ScpError};
assert!(matches!(validate_name("."), Err(ScpError::BadName(_))));
}
#[test]
fn validate_rejects_dotdot_name() {
use super::protocol::{validate_name, ScpError};
assert!(matches!(validate_name(".."), Err(ScpError::BadName(_))));
}
#[test]
fn receiver_new_rejects_missing_base() {
let bogus = std::env::temp_dir().join(format!(
"puressh-scp-test-DOES-NOT-EXIST-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let (a, _b) = UnixStream::pair().expect("socketpair");
let r = Receiver::new(a, &bogus, ScpRecvOptions::default());
assert!(r.is_err(), "expected Receiver::new to reject missing base");
}
#[cfg(unix)]
#[test]
fn recv_file_refuses_to_overwrite_symlink() {
let src_dir = fresh_tmp("send-sym-f");
let _g1 = DirGuard(src_dir.clone());
let dst_dir = fresh_tmp("recv-sym-f");
let _g2 = DirGuard(dst_dir.clone());
let outside_dir = fresh_tmp("recv-sym-outside");
let _g3 = DirGuard(outside_dir.clone());
let outside_file = outside_dir.join("target.txt");
std::fs::write(&outside_file, b"PRECIOUS").unwrap();
let dst_link = dst_dir.join("hello.txt");
std::os::unix::fs::symlink(&outside_file, &dst_link).unwrap();
let src_file = src_dir.join("hello.txt");
std::fs::write(&src_file, b"OVERWRITE").unwrap();
let (a, b) = UnixStream::pair().expect("socketpair");
let dst_dir_thread = dst_dir.clone();
let recv = thread::spawn(move || {
let mut r = Receiver::new(b, &dst_dir_thread, ScpRecvOptions::default()).expect("recv new");
r.run()
});
let mut sender = Sender::new(a).expect("send new");
let _ = sender.send_path(&src_file, &ScpSendOptions::default());
drop(sender);
let r = recv.join().expect("recv join");
assert!(r.is_err(), "receiver should refuse symlink overwrite");
let still = std::fs::read(&outside_file).expect("read outside");
assert_eq!(still, b"PRECIOUS");
}
#[cfg(unix)]
#[test]
fn recv_dir_refuses_to_descend_into_symlinked_directory() {
let src_dir = fresh_tmp("send-sym-d");
let _g1 = DirGuard(src_dir.clone());
let dst_dir = fresh_tmp("recv-sym-d");
let _g2 = DirGuard(dst_dir.clone());
let outside_dir = fresh_tmp("recv-sym-d-outside");
let _g3 = DirGuard(outside_dir.clone());
let tree_name = "tree";
let tree = src_dir.join(tree_name);
std::fs::create_dir(&tree).unwrap();
std::fs::write(tree.join("f.txt"), b"x").unwrap();
std::os::unix::fs::symlink(&outside_dir, dst_dir.join(tree_name)).unwrap();
let (a, b) = UnixStream::pair().expect("socketpair");
let dst_dir_thread = dst_dir.clone();
let recv = thread::spawn(move || {
let mut r = Receiver::new(
b,
&dst_dir_thread,
ScpRecvOptions {
recursive: true,
preserve_times: false,
target_is_file: false,
},
)
.expect("recv new");
r.run()
});
let opts = ScpSendOptions {
recursive: true,
preserve_times: false,
};
let mut sender = Sender::new(a).expect("send new");
let _ = sender.send_path(&tree, &opts);
drop(sender);
let r = recv.join().expect("recv join");
assert!(r.is_err(), "receiver should refuse symlinked dir");
assert!(!outside_dir.join("f.txt").exists());
}
#[test]
fn round_trip_empty_file() {
let src_dir = fresh_tmp("send-empty");
let _g1 = DirGuard(src_dir.clone());
let dst_dir = fresh_tmp("recv-empty");
let _g2 = DirGuard(dst_dir.clone());
let src_path = src_dir.join("empty.txt");
std::fs::File::create(&src_path).unwrap().flush().unwrap();
let (a, b) = UnixStream::pair().expect("socketpair");
let dst_dir_thread = dst_dir.clone();
let recv = thread::spawn(move || {
let mut r = Receiver::new(b, &dst_dir_thread, ScpRecvOptions::default()).expect("recv new");
r.run().expect("recv run");
});
let mut sender = Sender::new(a).expect("send new");
sender
.send_path(&src_path, &ScpSendOptions::default())
.expect("send empty");
drop(sender);
recv.join().expect("recv join");
let got = std::fs::read(dst_dir.join("empty.txt")).expect("read dst");
assert_eq!(got, Vec::<u8>::new());
}