puressh 0.0.2

A pure-Rust SSH (Secure Shell) protocol library, in the spirit of libssh, built on purecrypto.
Documentation
//! Sender↔Receiver round-trip tests over `UnixStream::pair()` — the
//! transport is full duplex and ordering-preserving, mirroring how an
//! SSH channel behaves once buffering is squared away. Each test
//! spawns the receiver on its own thread so the sender can drive
//! reads/writes from the test thread without deadlocking.

use std::io::Write as _;
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::thread;

use super::{Receiver, ScpRecvOptions, ScpSendOptions, Sender};

/// Allocate a fresh temp directory rooted at $TMPDIR, with an unlikely
/// suffix so concurrent test runs don't collide. Cleaned by the test
/// `Drop` guard at the end of each test (or left in /tmp if a panic
/// short-circuits cleanup).
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");

    // Receiver thread runs `Receiver::run` until EOF.
    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); // closes stream — receiver thread sees EOF
    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(), // recursive: false
        )
        .expect("recv new");
        r.run() // expected to fail
    });

    let mut sender = Sender::new(a).expect("send new");
    let opts = ScpSendOptions {
        recursive: true,
        ..Default::default()
    };
    // Sender will receive a 0x02 frame from the receiver. The error
    // surfaces somewhere in the send path; we don't care exactly when.
    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() {
    // Direct protocol-level check — Sender::send_file would refuse a
    // path whose basename starts with '-' via write_header → validate_name.
    use super::protocol::{validate_name, ScpError};
    assert!(matches!(validate_name("-rf"), Err(ScpError::BadName(_))));
}

#[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());
}