use crate::transit::{self, Abilities as TransitAbilities};
use serde_derive::{Deserialize, Serialize};
use std::{collections::HashMap, path::PathBuf};
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum PeerMessage {
Offer(Offer),
OfferV2(OfferV2),
Answer(Answer),
AnswerV2(AnswerV2),
Error(String),
Transit(TransitV1),
TransitV2(TransitV2),
#[serde(other)]
Unknown,
}
impl PeerMessage {
pub fn offer_message(msg: impl Into<String>) -> Self {
PeerMessage::Offer(Offer::Message(msg.into()))
}
pub fn offer_file(name: impl Into<PathBuf>, size: u64) -> Self {
PeerMessage::Offer(Offer::File {
filename: name.into(),
filesize: size,
})
}
#[allow(dead_code)]
pub fn offer_directory(
name: impl Into<PathBuf>,
mode: impl Into<String>,
compressed_size: u64,
numbytes: u64,
numfiles: u64,
) -> Self {
PeerMessage::Offer(Offer::Directory {
dirname: name.into(),
mode: mode.into(),
zipsize: compressed_size,
numbytes,
numfiles,
})
}
#[allow(dead_code)]
pub fn message_ack(msg: impl Into<String>) -> Self {
PeerMessage::Answer(Answer::MessageAck(msg.into()))
}
pub fn file_ack(msg: impl Into<String>) -> Self {
PeerMessage::Answer(Answer::FileAck(msg.into()))
}
pub fn error_message(msg: impl Into<String>) -> Self {
PeerMessage::Error(msg.into())
}
pub fn transit(abilities: TransitAbilities, hints: transit::Hints) -> Self {
PeerMessage::Transit(TransitV1 {
abilities_v1: abilities,
hints_v1: hints,
})
}
#[allow(dead_code)]
pub fn transit_v2(hints: transit::Hints) -> Self {
PeerMessage::TransitV2(TransitV2 { hints })
}
#[allow(dead_code)]
pub fn ser_json(&self) -> Vec<u8> {
serde_json::to_vec(self).unwrap()
}
#[allow(dead_code)]
pub fn ser_msgpack(&self) -> Vec<u8> {
let mut writer = Vec::with_capacity(128);
let mut ser = rmp_serde::encode::Serializer::new(&mut writer)
.with_struct_map()
.with_human_readable();
serde::Serialize::serialize(self, &mut ser).unwrap();
writer
}
#[allow(dead_code)]
pub fn de_msgpack(data: &[u8]) -> Result<Self, rmp_serde::decode::Error> {
rmp_serde::from_read(&mut &*data)
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum Offer {
Message(String),
File {
filename: PathBuf,
filesize: u64,
},
Directory {
dirname: PathBuf,
mode: String,
zipsize: u64,
numbytes: u64,
numfiles: u64,
},
#[serde(other)]
Unknown,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct OfferV2 {
transfer_name: Option<String>,
files: Vec<OfferV2Entry>,
format: String, }
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct OfferV2Entry {
path: String,
size: u64,
mtime: u64,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Answer {
MessageAck(String),
FileAck(String),
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub struct AnswerV2 {
files: HashMap<u64, u64>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct TransitV1 {
pub abilities_v1: TransitAbilities,
pub hints_v1: transit::Hints,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct TransitV2 {
pub hints: transit::Hints,
}
#[cfg(test)]
mod test {
use super::*;
use transit::{Abilities, DirectHint, RelayHint};
#[test]
fn test_transit() {
let abilities = Abilities::ALL_ABILITIES;
let hints = transit::Hints::new(
[DirectHint::new("192.168.1.8", 46295)],
[RelayHint::new(
None,
[DirectHint::new("magic-wormhole-transit.debian.net", 4001)],
[],
)],
);
assert_eq!(
serde_json::json!(crate::transfer::PeerMessage::transit(abilities, hints)),
serde_json::json!({
"transit": {
"abilities-v1": [{"type":"direct-tcp-v1"},{"type":"relay-v1"}],
"hints-v1": [
{"hostname":"192.168.1.8","port":46295,"type":"direct-tcp-v1"},
{
"type": "relay-v1",
"hints": [
{"type": "direct-tcp-v1", "hostname": "magic-wormhole-transit.debian.net", "port": 4001}
],
"name": null
}
],
}
})
);
}
#[test]
fn test_message() {
let m1 = PeerMessage::offer_message("hello from rust");
assert_eq!(
serde_json::json!(m1).to_string(),
"{\"offer\":{\"message\":\"hello from rust\"}}"
);
}
#[test]
fn test_offer_file() {
let f1 = PeerMessage::offer_file("somefile.txt", 34556);
assert_eq!(
serde_json::json!(f1).to_string(),
"{\"offer\":{\"file\":{\"filename\":\"somefile.txt\",\"filesize\":34556}}}"
);
}
#[test]
fn test_offer_directory() {
let d1 = PeerMessage::offer_directory("somedirectory", "zipped", 45, 1234, 10);
assert_eq!(
serde_json::json!(d1).to_string(),
"{\"offer\":{\"directory\":{\"dirname\":\"somedirectory\",\"mode\":\"zipped\",\"numbytes\":1234,\"numfiles\":10,\"zipsize\":45}}}"
);
}
#[test]
fn test_message_ack() {
let m1 = PeerMessage::message_ack("ok");
assert_eq!(
serde_json::json!(m1).to_string(),
"{\"answer\":{\"message_ack\":\"ok\"}}"
);
}
#[test]
fn test_file_ack() {
let f1 = PeerMessage::file_ack("ok");
assert_eq!(
serde_json::json!(f1).to_string(),
"{\"answer\":{\"file_ack\":\"ok\"}}"
);
}
}