onionwire/tests/file.rs

134 lines
4.3 KiB
Rust
Raw Permalink Normal View History

//! M10: fail-closed file transfer — frames, names, assemble, slash parse.
use onionwire::dispatch::{Kind, classify};
use onionwire::file::{self, MAX_BYTES};
use onionwire::tui::{SlashCmd, parse_cmd};
#[test]
fn encode_decode_roundtrip_one_chunk() {
let body = b"hello file";
let chunks = file::chunks("note.txt", body).unwrap();
assert_eq!(chunks.len(), 1);
let encoded = file::encode(&chunks[0]);
let decoded = file::decode(&encoded).expect("decode");
assert_eq!(decoded.filename, "note.txt");
assert_eq!(decoded.idx, 0);
assert_eq!(decoded.total, 1);
assert_eq!(decoded.data, body);
assert_eq!(decoded.sha256, chunks[0].sha256);
assert_eq!(decoded.xfer_id, chunks[0].xfer_id);
}
#[test]
fn chat_is_not_file() {
assert_eq!(file::decode(b"hello wire"), None);
}
#[test]
fn name_with_dotdot_or_slash_rejected() {
assert!(file::chunks("../secret", b"x").is_err());
assert!(file::chunks("a/b", b"x").is_err());
assert!(file::safe_name("..").is_err());
assert!(file::safe_name("foo/bar").is_err());
assert!(file::safe_name("a\0b").is_err());
assert!(file::safe_name("ok.txt").is_ok());
}
#[test]
fn oversize_rejected_before_send() {
let too_big = vec![0u8; MAX_BYTES + 1];
assert!(file::chunks("big.bin", &too_big).is_err());
assert!(file::chunks("ok.bin", &vec![0u8; MAX_BYTES]).is_ok());
}
#[test]
fn assemble_two_chunks_writes_file_and_matches_hash() {
let dir = tempfile::tempdir().unwrap();
let body = vec![7u8; 80_000];
let chunks = file::chunks("pic.bin", &body).unwrap();
assert!(chunks.len() >= 2, "expected split, got {}", chunks.len());
let mut inbox = file::Inbox::new(dir.path());
let fp = "aabbccddeeff";
let mut done = None;
for c in &chunks {
done = inbox.ingest(fp, c).unwrap();
}
let path = done.expect("assembled path");
assert_eq!(std::fs::read(&path).unwrap(), body);
assert!(path.ends_with("pic.bin"));
assert!(path.to_string_lossy().contains(fp));
}
#[test]
fn bad_hash_leaves_no_inbox_file() {
let dir = tempfile::tempdir().unwrap();
let chunks = file::chunks("evil.bin", b"payload").unwrap();
let mut bad = chunks[0].clone();
bad.sha256 = [0u8; 32];
let mut inbox = file::Inbox::new(dir.path());
let fp = "deadbeef";
assert!(inbox.ingest(fp, &bad).is_err());
let dest = dir.path().join(fp).join("evil.bin");
assert!(!dest.exists(), "hash mismatch must not write inbox file");
let partials: Vec<_> = std::fs::read_dir(dir.path())
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().starts_with(".partial-"))
.collect();
assert!(
partials.is_empty(),
"partial must be deleted on hash mismatch"
);
}
#[test]
fn ingest_rejects_oversize_and_deletes_partial() {
let dir = tempfile::tempdir().unwrap();
let mut inbox = file::Inbox::new(dir.path());
let mut chunk = file::chunks("fat.bin", b"x").unwrap().remove(0);
chunk.total = 2;
chunk.idx = 0;
chunk.data = vec![1u8; MAX_BYTES + 1];
assert!(inbox.ingest("aa", &chunk).is_err());
assert!(!dir.path().join("aa").join("fat.bin").exists());
let leftover: Vec<_> = std::fs::read_dir(dir.path())
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().starts_with(".partial-"))
.collect();
assert!(leftover.is_empty());
}
#[test]
fn ingest_rejects_cumulative_oversize() {
let dir = tempfile::tempdir().unwrap();
let mut inbox = file::Inbox::new(dir.path());
let mut a = file::chunks("fat.bin", b"x").unwrap().remove(0);
a.total = 2;
a.idx = 0;
a.data = vec![1u8; MAX_BYTES - 10];
assert_eq!(inbox.ingest("aa", &a).unwrap(), None);
let mut b = a.clone();
b.idx = 1;
b.data = vec![1u8; 11];
assert!(inbox.ingest("aa", &b).is_err());
assert!(!dir.path().join("aa").join("fat.bin").exists());
}
#[test]
fn slash_file_parses_path_and_empty_is_none() {
assert_eq!(
parse_cmd("/file /tmp/a"),
Some(SlashCmd::File {
path: "/tmp/a".into()
})
);
assert_eq!(parse_cmd("/file"), None);
}
#[test]
fn dispatch_fil_is_file_not_chat() {
assert_eq!(classify(b"fil abc"), Kind::File);
assert_eq!(classify(b"hello wire"), Kind::Chat);
}