use serde_json::{json, Value};
pub fn offer_msg(
id: &str,
sid: u32,
name: &str,
size: u64,
head: Option<&str>,
full: Option<&str>,
resume: bool,
) -> Value {
let mut o = json!({
"type": "file-offer", "id": id, "sid": sid,
"name": name, "size": size, "mime": "application/octet-stream",
});
if let Some(h) = head {
o["head"] = json!(h);
}
if let Some(f) = full {
o["full"] = json!(f);
}
if resume {
o["resume"] = json!(true);
}
o
}
pub fn accept_msg(id: &str, offset: u64) -> Value {
json!({ "type": "file-accept", "id": id, "offset": offset })
}
pub fn decline_msg(id: &str) -> Value {
json!({ "type": "file-decline", "id": id })
}
pub fn end_msg(id: &str, sid: u32) -> Value {
json!({ "type": "file-end", "id": id, "sid": sid })
}
pub fn delivery_ack_msg(id: &str, sid: u32) -> Value {
json!({ "type": "delivery-ack", "id": id, "sid": sid, "v": 1 })
}
pub fn recv_transfer_done(completed: usize, keep_open: bool, by_sid_empty: bool) -> bool {
completed > 0 && !keep_open && by_sid_empty
}
#[derive(Debug, PartialEq, Eq)]
pub enum AckFallback {
Reprobe,
FailUnconfirmed,
}
pub fn decide_ack_fallback(link_alive: bool, reprobed: bool) -> AckFallback {
if !link_alive {
return AckFallback::FailUnconfirmed;
}
if !reprobed {
return AckFallback::Reprobe;
}
AckFallback::FailUnconfirmed
}
#[derive(Debug, PartialEq, Eq)]
pub enum VerifyResult {
Match,
Mismatch { restart_from_zero: bool },
}
pub fn decide_verify(received: u64, size: u64, hash_matches: Option<bool>) -> VerifyResult {
if received < size {
return VerifyResult::Mismatch { restart_from_zero: false };
}
match hash_matches {
Some(true) => VerifyResult::Match,
_ => VerifyResult::Mismatch { restart_from_zero: true },
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn offer_msg_shapes() {
assert_eq!(
offer_msg("id1", 7, "a.bin", 100, None, None, false),
json!({ "type": "file-offer", "id": "id1", "sid": 7, "name": "a.bin", "size": 100, "mime": "application/octet-stream" })
);
assert_eq!(
offer_msg("id1", 7, "a.bin", 100, Some("hh"), Some("ff"), true),
json!({ "type": "file-offer", "id": "id1", "sid": 7, "name": "a.bin", "size": 100, "mime": "application/octet-stream", "head": "hh", "full": "ff", "resume": true })
);
}
#[test]
fn accept_decline_end_ack_shapes() {
assert_eq!(accept_msg("id1", 0), json!({ "type": "file-accept", "id": "id1", "offset": 0 }));
assert_eq!(accept_msg("id1", 4096), json!({ "type": "file-accept", "id": "id1", "offset": 4096 }));
assert_eq!(decline_msg("id1"), json!({ "type": "file-decline", "id": "id1" }));
assert_eq!(end_msg("id1", 7), json!({ "type": "file-end", "id": "id1", "sid": 7 }));
assert_eq!(delivery_ack_msg("id1", 7), json!({ "type": "delivery-ack", "id": "id1", "sid": 7, "v": 1 }));
}
#[test]
fn recv_done_drops_only_when_complete() {
assert!(recv_transfer_done(1, false, true));
assert!(recv_transfer_done(3, false, true));
}
#[test]
fn recv_done_false_mid_transfer_protects_resume() {
assert!(!recv_transfer_done(0, false, false)); assert!(!recv_transfer_done(1, false, false)); assert!(!recv_transfer_done(1, true, true));
assert!(!recv_transfer_done(5, true, true));
assert!(!recv_transfer_done(0, false, true));
}
#[test]
fn ack_fallback_never_completes_silently() {
assert_eq!(decide_ack_fallback(false, false), AckFallback::FailUnconfirmed);
assert_eq!(decide_ack_fallback(false, true), AckFallback::FailUnconfirmed);
assert_eq!(decide_ack_fallback(true, false), AckFallback::Reprobe);
assert_eq!(decide_ack_fallback(true, true), AckFallback::FailUnconfirmed);
}
#[test]
fn verify_match_only_on_full_size_correct_hash() {
assert_eq!(decide_verify(100, 100, Some(true)), VerifyResult::Match);
}
#[test]
fn verify_truncated_resumes_tail() {
assert_eq!(decide_verify(60, 100, None), VerifyResult::Mismatch { restart_from_zero: false });
assert_eq!(decide_verify(60, 100, Some(false)), VerifyResult::Mismatch { restart_from_zero: false });
}
#[test]
fn verify_full_size_wrong_hash_is_corrupt_restart() {
assert_eq!(decide_verify(100, 100, Some(false)), VerifyResult::Mismatch { restart_from_zero: true });
assert_eq!(decide_verify(100, 100, None), VerifyResult::Mismatch { restart_from_zero: true });
}
}