mirror of
https://github.com/YGGverse/aquatic.git
synced 2026-03-31 17:55:36 +00:00
aquatic_ws: rewrite failing serialization, add tests
Use different approach to action fields and info_hash vec, fixing failing tests
This commit is contained in:
parent
c8512e1cdf
commit
0637f83daa
5 changed files with 311 additions and 250 deletions
|
|
@ -184,6 +184,7 @@ pub fn handle_announce_requests(
|
|||
}
|
||||
|
||||
let middleman_offer = MiddlemanOfferToPeer {
|
||||
action: AnnounceAction,
|
||||
info_hash: request.info_hash,
|
||||
peer_id: request.peer_id,
|
||||
offer: offer.offer,
|
||||
|
|
@ -205,6 +206,7 @@ pub fn handle_announce_requests(
|
|||
.get(&answer_receiver_id)
|
||||
{
|
||||
let middleman_answer = MiddlemanAnswerToPeer {
|
||||
action: AnnounceAction,
|
||||
peer_id: request.peer_id,
|
||||
info_hash: request.info_hash,
|
||||
answer,
|
||||
|
|
@ -219,6 +221,7 @@ pub fn handle_announce_requests(
|
|||
}
|
||||
|
||||
let response = OutMessage::AnnounceResponse(AnnounceResponse {
|
||||
action: AnnounceAction,
|
||||
info_hash: request.info_hash,
|
||||
complete: torrent_data.num_seeders,
|
||||
incomplete: torrent_data.num_leechers,
|
||||
|
|
@ -236,12 +239,19 @@ pub fn handle_scrape_requests(
|
|||
messages_out: &mut Vec<(ConnectionMeta, OutMessage)>,
|
||||
requests: Drain<(ConnectionMeta, ScrapeRequest)>,
|
||||
){
|
||||
messages_out.extend(requests.map(|(meta, request)| {
|
||||
let num_to_take = request.info_hashes.len().min(
|
||||
for (meta, request) in requests {
|
||||
let info_hashes = if let Some(info_hashes) = request.info_hashes {
|
||||
info_hashes.as_vec()
|
||||
} else {
|
||||
continue
|
||||
};
|
||||
|
||||
let num_to_take = info_hashes.len().min(
|
||||
config.protocol.max_scrape_torrents
|
||||
);
|
||||
|
||||
let mut response = ScrapeResponse {
|
||||
action: ScrapeAction,
|
||||
files: HashMap::with_capacity(num_to_take),
|
||||
};
|
||||
|
||||
|
|
@ -253,7 +263,7 @@ pub fn handle_scrape_requests(
|
|||
|
||||
// If request.info_hashes is empty, don't return scrape for all
|
||||
// torrents, even though reference server does it. It is too expensive.
|
||||
for info_hash in request.info_hashes.into_iter().take(num_to_take){
|
||||
for info_hash in info_hashes.into_iter().take(num_to_take){
|
||||
if let Some(torrent_data) = torrent_map.get(&info_hash){
|
||||
let stats = ScrapeStatistics {
|
||||
complete: torrent_data.num_seeders,
|
||||
|
|
@ -265,6 +275,6 @@ pub fn handle_scrape_requests(
|
|||
}
|
||||
}
|
||||
|
||||
(meta, OutMessage::ScrapeResponse(response))
|
||||
}));
|
||||
messages_out.push((meta, OutMessage::ScrapeResponse(response)));
|
||||
}
|
||||
}
|
||||
|
|
@ -193,7 +193,7 @@ pub fn run_handshakes_and_read_messages(
|
|||
|
||||
match established_ws.ws.read_message(){
|
||||
Ok(ws_message) => {
|
||||
if let Some(in_message) = InMessage::from_ws_message(ws_message){
|
||||
if let Ok(in_message) = InMessage::from_ws_message(ws_message){
|
||||
let naive_peer_addr = established_ws.peer_addr;
|
||||
let converted_peer_ip = convert_ipv4_mapped_ipv6(
|
||||
naive_peer_addr.ip()
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ fn create_announce_request(
|
|||
.copy_from_slice(&connection_id.to_ne_bytes());
|
||||
|
||||
InMessage::AnnounceRequest(AnnounceRequest {
|
||||
action: AnnounceAction,
|
||||
info_hash: state.info_hashes[info_hash_index],
|
||||
peer_id,
|
||||
bytes_left: Some(bytes_left),
|
||||
|
|
@ -108,7 +109,8 @@ fn create_scrape_request(
|
|||
}
|
||||
|
||||
InMessage::ScrapeRequest(ScrapeRequest {
|
||||
info_hashes: scrape_hashes,
|
||||
action: ScrapeAction,
|
||||
info_hashes: Some(ScrapeRequestInfoHashes::Multiple(scrape_hashes)),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,64 @@
|
|||
use anyhow::Context;
|
||||
use hashbrown::HashMap;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde::{Serialize, Deserialize, Serializer, Deserializer};
|
||||
|
||||
mod serde_helpers;
|
||||
|
||||
use serde_helpers::*;
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct AnnounceAction;
|
||||
|
||||
|
||||
impl Serialize for AnnounceAction {
|
||||
fn serialize<S>(
|
||||
&self,
|
||||
serializer: S
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer
|
||||
{
|
||||
serializer.serialize_str("announce")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl<'de> Deserialize<'de> for AnnounceAction {
|
||||
fn deserialize<D>(deserializer: D) -> Result<AnnounceAction, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(AnnounceActionVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ScrapeAction;
|
||||
|
||||
|
||||
impl Serialize for ScrapeAction {
|
||||
fn serialize<S>(
|
||||
&self,
|
||||
serializer: S
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where S: Serializer
|
||||
{
|
||||
serializer.serialize_str("scrape")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl<'de> Deserialize<'de> for ScrapeAction {
|
||||
fn deserialize<D>(deserializer: D) -> Result<ScrapeAction, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(ScrapeActionVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct PeerId(
|
||||
|
|
@ -67,6 +120,7 @@ impl Default for AnnounceEvent {
|
|||
/// action = "announce"
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MiddlemanOfferToPeer {
|
||||
pub action: AnnounceAction,
|
||||
/// Peer id of peer sending offer
|
||||
/// Note: if equal to client peer_id, client ignores offer
|
||||
pub peer_id: PeerId,
|
||||
|
|
@ -83,6 +137,7 @@ pub struct MiddlemanOfferToPeer {
|
|||
/// Action field should be 'announce'
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MiddlemanAnswerToPeer {
|
||||
pub action: AnnounceAction,
|
||||
/// Note: if equal to client peer_id, client ignores answer
|
||||
pub peer_id: PeerId,
|
||||
pub info_hash: InfoHash,
|
||||
|
|
@ -101,6 +156,7 @@ pub struct AnnounceRequestOffer {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AnnounceRequest {
|
||||
pub action: AnnounceAction,
|
||||
pub info_hash: InfoHash,
|
||||
pub peer_id: PeerId,
|
||||
/// Just called "left" in protocol. Is set to None in some cases, such as
|
||||
|
|
@ -136,6 +192,7 @@ pub struct AnnounceRequest {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AnnounceResponse {
|
||||
pub action: AnnounceAction,
|
||||
pub info_hash: InfoHash,
|
||||
/// Client checks if this is null, not clear why
|
||||
pub complete: usize,
|
||||
|
|
@ -145,17 +202,32 @@ pub struct AnnounceResponse {
|
|||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum ScrapeRequestInfoHashes {
|
||||
Single(InfoHash),
|
||||
Multiple(Vec<InfoHash>),
|
||||
}
|
||||
|
||||
|
||||
impl ScrapeRequestInfoHashes {
|
||||
pub fn as_vec(self) -> Vec<InfoHash> {
|
||||
match self {
|
||||
Self::Single(info_hash) => vec![info_hash],
|
||||
Self::Multiple(info_hashes) => info_hashes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ScrapeRequest {
|
||||
pub action: ScrapeAction,
|
||||
// If omitted, scrape for all torrents, apparently
|
||||
// There is some kind of parsing here too which accepts a single info hash
|
||||
// and puts it into a vector
|
||||
#[serde(
|
||||
rename = "info_hash",
|
||||
deserialize_with = "deserialize_info_hashes",
|
||||
default
|
||||
)]
|
||||
pub info_hashes: Vec<InfoHash>,
|
||||
#[serde(rename = "info_hash")]
|
||||
pub info_hashes: Option<ScrapeRequestInfoHashes>,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -169,48 +241,15 @@ pub struct ScrapeStatistics {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ScrapeResponse {
|
||||
pub action: ScrapeAction,
|
||||
pub files: HashMap<InfoHash, ScrapeStatistics>,
|
||||
// Looks like `flags` field is ignored in reference client
|
||||
// pub flags: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Action {
|
||||
Announce,
|
||||
Scrape
|
||||
}
|
||||
|
||||
|
||||
/// Helper for serializing and deserializing messages
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ActionWrapper<T> {
|
||||
pub action: Action,
|
||||
#[serde(flatten)]
|
||||
pub inner: T,
|
||||
}
|
||||
|
||||
|
||||
impl <T>ActionWrapper<T> {
|
||||
#[inline]
|
||||
pub fn announce(t: T) -> Self {
|
||||
Self {
|
||||
action: Action::Announce,
|
||||
inner: t
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub fn scrape(t: T) -> Self {
|
||||
Self {
|
||||
action: Action::Scrape,
|
||||
inner: t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum InMessage {
|
||||
AnnounceRequest(AnnounceRequest),
|
||||
ScrapeRequest(ScrapeRequest),
|
||||
|
|
@ -218,49 +257,26 @@ pub enum InMessage {
|
|||
|
||||
|
||||
impl InMessage {
|
||||
/// Try parsing as announce request first. If that fails, try parsing as
|
||||
/// scrape request, or return None
|
||||
#[inline]
|
||||
pub fn from_ws_message(ws_message: tungstenite::Message) -> Option<Self> {
|
||||
pub fn from_ws_message(ws_message: tungstenite::Message) -> ::anyhow::Result<Self> {
|
||||
use tungstenite::Message::{Text, Binary};
|
||||
|
||||
let text = match ws_message {
|
||||
Text(text) => Some(text),
|
||||
Binary(bytes) => String::from_utf8(bytes).ok(),
|
||||
_ => None
|
||||
}?;
|
||||
Text(text) => text,
|
||||
Binary(bytes) => String::from_utf8(bytes)?,
|
||||
_ => return Err(anyhow::anyhow!("Message is neither text nor bytes")),
|
||||
};
|
||||
|
||||
let res: Result<ActionWrapper<AnnounceRequest>, _> = serde_json::from_str(&text);
|
||||
|
||||
if let Ok(ActionWrapper { action: Action::Announce, inner }) = res {
|
||||
return Some(InMessage::AnnounceRequest(inner));
|
||||
}
|
||||
|
||||
let res: Result<ActionWrapper<ScrapeRequest>, _> = serde_json::from_str(&text);
|
||||
|
||||
if let Ok(ActionWrapper { action: Action::Scrape, inner }) = res {
|
||||
return Some(InMessage::ScrapeRequest(inner));
|
||||
}
|
||||
|
||||
None
|
||||
::serde_json::from_str(&text).context("serialize with serde")
|
||||
}
|
||||
|
||||
pub fn to_ws_message(&self) -> ::tungstenite::Message {
|
||||
let text = match self {
|
||||
InMessage::AnnounceRequest(r) => {
|
||||
serde_json::to_string(&ActionWrapper::announce(r)).unwrap()
|
||||
},
|
||||
InMessage::ScrapeRequest(r) => {
|
||||
serde_json::to_string(&ActionWrapper::scrape(r)).unwrap()
|
||||
},
|
||||
};
|
||||
|
||||
::tungstenite::Message::from(text)
|
||||
::tungstenite::Message::from(::serde_json::to_string(&self).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum OutMessage {
|
||||
AnnounceResponse(AnnounceResponse),
|
||||
|
|
@ -273,30 +289,7 @@ pub enum OutMessage {
|
|||
impl OutMessage {
|
||||
#[inline]
|
||||
pub fn into_ws_message(self) -> tungstenite::Message {
|
||||
let json = match self {
|
||||
Self::AnnounceResponse(message) => {
|
||||
serde_json::to_string(
|
||||
&ActionWrapper::announce(message)
|
||||
).unwrap()
|
||||
},
|
||||
Self::Offer(message) => {
|
||||
serde_json::to_string(
|
||||
&ActionWrapper::announce(message)
|
||||
).unwrap()
|
||||
},
|
||||
Self::Answer(message) => {
|
||||
serde_json::to_string(
|
||||
&ActionWrapper::announce(message)
|
||||
).unwrap()
|
||||
},
|
||||
Self::ScrapeResponse(message) => {
|
||||
serde_json::to_string(
|
||||
&ActionWrapper::scrape(message)
|
||||
).unwrap()
|
||||
},
|
||||
};
|
||||
|
||||
tungstenite::Message::from(json)
|
||||
::tungstenite::Message::from(::serde_json::to_string(&self).unwrap())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
@ -308,7 +301,7 @@ impl OutMessage {
|
|||
let text = match message {
|
||||
Text(text) => text,
|
||||
Binary(bytes) => String::from_utf8(bytes)?,
|
||||
_ => return Err(anyhow::anyhow!("message type not supported")),
|
||||
_ => return Err(anyhow::anyhow!("Message is neither text nor bytes")),
|
||||
};
|
||||
|
||||
if text.contains("answer"){
|
||||
|
|
@ -381,6 +374,7 @@ mod tests {
|
|||
impl Arbitrary for MiddlemanOfferToPeer {
|
||||
fn arbitrary<G: quickcheck::Gen>(g: &mut G) -> Self {
|
||||
Self {
|
||||
action: AnnounceAction,
|
||||
peer_id: Arbitrary::arbitrary(g),
|
||||
info_hash: Arbitrary::arbitrary(g),
|
||||
offer_id: Arbitrary::arbitrary(g),
|
||||
|
|
@ -392,6 +386,7 @@ mod tests {
|
|||
impl Arbitrary for MiddlemanAnswerToPeer {
|
||||
fn arbitrary<G: quickcheck::Gen>(g: &mut G) -> Self {
|
||||
Self {
|
||||
action: AnnounceAction,
|
||||
peer_id: Arbitrary::arbitrary(g),
|
||||
info_hash: Arbitrary::arbitrary(g),
|
||||
offer_id: Arbitrary::arbitrary(g),
|
||||
|
|
@ -434,6 +429,7 @@ mod tests {
|
|||
.map(|offers| offers.len());
|
||||
|
||||
Self {
|
||||
action: AnnounceAction,
|
||||
info_hash: Arbitrary::arbitrary(g),
|
||||
peer_id: Arbitrary::arbitrary(g),
|
||||
bytes_left: Arbitrary::arbitrary(g),
|
||||
|
|
@ -450,6 +446,7 @@ mod tests {
|
|||
impl Arbitrary for AnnounceResponse {
|
||||
fn arbitrary<G: quickcheck::Gen>(g: &mut G) -> Self {
|
||||
Self {
|
||||
action: AnnounceAction,
|
||||
info_hash: Arbitrary::arbitrary(g),
|
||||
complete: Arbitrary::arbitrary(g),
|
||||
incomplete: Arbitrary::arbitrary(g),
|
||||
|
|
@ -461,11 +458,23 @@ mod tests {
|
|||
impl Arbitrary for ScrapeRequest {
|
||||
fn arbitrary<G: quickcheck::Gen>(g: &mut G) -> Self {
|
||||
Self {
|
||||
action: ScrapeAction,
|
||||
info_hashes: Arbitrary::arbitrary(g),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Arbitrary for ScrapeRequestInfoHashes {
|
||||
fn arbitrary<G: quickcheck::Gen>(g: &mut G) -> Self {
|
||||
if Arbitrary::arbitrary(g) {
|
||||
ScrapeRequestInfoHashes::Multiple(Arbitrary::arbitrary(g))
|
||||
} else {
|
||||
ScrapeRequestInfoHashes::Single(Arbitrary::arbitrary(g))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for ScrapeStatistics {
|
||||
fn arbitrary<G: quickcheck::Gen>(g: &mut G) -> Self {
|
||||
Self {
|
||||
|
|
@ -481,6 +490,7 @@ mod tests {
|
|||
let files: Vec<(InfoHash, ScrapeStatistics)> = Arbitrary::arbitrary(g);
|
||||
|
||||
Self {
|
||||
action: ScrapeAction,
|
||||
files: files.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
|
@ -508,28 +518,144 @@ mod tests {
|
|||
}
|
||||
|
||||
#[quickcheck]
|
||||
fn test_serialize_deserialize_in_message(in_message_1: InMessage){
|
||||
dbg!(in_message_1.clone());
|
||||
|
||||
fn quickcheck_serde_identity_in_message(in_message_1: InMessage) -> bool {
|
||||
let ws_message = in_message_1.to_ws_message();
|
||||
dbg!(ws_message.clone());
|
||||
|
||||
let in_message_2 = InMessage::from_ws_message(ws_message).unwrap();
|
||||
dbg!(in_message_2.clone());
|
||||
let in_message_2 = InMessage::from_ws_message(ws_message.clone()).unwrap();
|
||||
|
||||
assert_eq!(in_message_1, in_message_2);
|
||||
let success = in_message_1 == in_message_2;
|
||||
|
||||
if !success {
|
||||
dbg!(in_message_1);
|
||||
dbg!(in_message_2);
|
||||
if let ::tungstenite::Message::Text(text) = ws_message {
|
||||
println!("{}", text);
|
||||
}
|
||||
}
|
||||
|
||||
success
|
||||
}
|
||||
|
||||
#[quickcheck]
|
||||
fn test_serialize_deserialize_out_message(out_message_1: OutMessage){
|
||||
dbg!(out_message_1.clone());
|
||||
|
||||
fn quickcheck_serde_identity_out_message(out_message_1: OutMessage) -> bool {
|
||||
let ws_message = out_message_1.clone().into_ws_message();
|
||||
dbg!(ws_message.clone());
|
||||
|
||||
let out_message_2 = OutMessage::from_ws_message(ws_message).unwrap();
|
||||
dbg!(out_message_2.clone());
|
||||
let out_message_2 = OutMessage::from_ws_message(ws_message.clone()).unwrap();
|
||||
|
||||
assert_eq!(out_message_1, out_message_2);
|
||||
let success = out_message_1 == out_message_2;
|
||||
|
||||
if !success {
|
||||
dbg!(out_message_1);
|
||||
dbg!(out_message_2);
|
||||
if let ::tungstenite::Message::Text(text) = ws_message {
|
||||
println!("{}", text);
|
||||
}
|
||||
}
|
||||
|
||||
success
|
||||
}
|
||||
|
||||
fn info_hash_from_bytes(bytes: &[u8]) -> InfoHash {
|
||||
let mut arr = [0u8; 20];
|
||||
|
||||
assert!(bytes.len() == 20);
|
||||
|
||||
arr.copy_from_slice(&bytes[..]);
|
||||
|
||||
InfoHash(arr)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_info_hashes_vec(){
|
||||
let input = r#"{
|
||||
"action": "scrape",
|
||||
"info_hash": ["aaaabbbbccccddddeeee", "aaaabbbbccccddddeeee"]
|
||||
}"#;
|
||||
|
||||
let info_hashes = ScrapeRequestInfoHashes::Multiple(
|
||||
vec![
|
||||
info_hash_from_bytes(b"aaaabbbbccccddddeeee"),
|
||||
info_hash_from_bytes(b"aaaabbbbccccddddeeee"),
|
||||
]
|
||||
);
|
||||
|
||||
let expected = ScrapeRequest {
|
||||
action: ScrapeAction,
|
||||
info_hashes: Some(info_hashes)
|
||||
};
|
||||
|
||||
let observed: ScrapeRequest = serde_json::from_str(input).unwrap();
|
||||
|
||||
assert_eq!(expected, observed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_info_hashes_str(){
|
||||
let input = r#"{
|
||||
"action": "scrape",
|
||||
"info_hash": "aaaabbbbccccddddeeee"
|
||||
}"#;
|
||||
|
||||
let info_hashes = ScrapeRequestInfoHashes::Single(
|
||||
info_hash_from_bytes(b"aaaabbbbccccddddeeee")
|
||||
);
|
||||
|
||||
let expected = ScrapeRequest {
|
||||
action: ScrapeAction,
|
||||
info_hashes: Some(info_hashes)
|
||||
};
|
||||
|
||||
let observed: ScrapeRequest = serde_json::from_str(input).unwrap();
|
||||
|
||||
assert_eq!(expected, observed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_info_hashes_null(){
|
||||
let input = r#"{
|
||||
"action": "scrape",
|
||||
"info_hash": null
|
||||
}"#;
|
||||
|
||||
let expected = ScrapeRequest {
|
||||
action: ScrapeAction,
|
||||
info_hashes: None
|
||||
};
|
||||
|
||||
let observed: ScrapeRequest = serde_json::from_str(input).unwrap();
|
||||
|
||||
assert_eq!(expected, observed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_info_hashes_missing(){
|
||||
let input = r#"{
|
||||
"action": "scrape"
|
||||
}"#;
|
||||
|
||||
let expected = ScrapeRequest {
|
||||
action: ScrapeAction,
|
||||
info_hashes: None
|
||||
};
|
||||
|
||||
let observed: ScrapeRequest = serde_json::from_str(input).unwrap();
|
||||
|
||||
assert_eq!(expected, observed);
|
||||
}
|
||||
|
||||
#[quickcheck]
|
||||
fn quickcheck_serde_identity_info_hashes(info_hashes: ScrapeRequestInfoHashes) -> bool {
|
||||
let json = ::serde_json::to_string(&info_hashes).unwrap();
|
||||
|
||||
println!("{}", json);
|
||||
|
||||
let deserialized: ScrapeRequestInfoHashes = ::serde_json::from_str(&json).unwrap();
|
||||
|
||||
let success = info_hashes == deserialized;
|
||||
|
||||
if !success {
|
||||
}
|
||||
|
||||
success
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,55 @@
|
|||
use serde::{Serializer, Deserializer, de::{Visitor, SeqAccess}};
|
||||
use serde::{Serializer, Deserializer, de::Visitor};
|
||||
|
||||
use super::InfoHash;
|
||||
use super::{AnnounceAction, ScrapeAction};
|
||||
|
||||
|
||||
pub struct AnnounceActionVisitor;
|
||||
|
||||
|
||||
impl<'de> Visitor<'de> for AnnounceActionVisitor {
|
||||
type Value = AnnounceAction;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("string with value 'announce'")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where E: ::serde::de::Error, {
|
||||
if v == "announce" {
|
||||
Ok(AnnounceAction)
|
||||
} else {
|
||||
Err(E::custom("value is not 'announce'"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct ScrapeActionVisitor;
|
||||
|
||||
|
||||
impl<'de> Visitor<'de> for ScrapeActionVisitor {
|
||||
type Value = ScrapeAction;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("string with value 'scrape'")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where E: ::serde::de::Error, {
|
||||
if v == "scrape" {
|
||||
Ok(ScrapeAction)
|
||||
} else {
|
||||
Err(E::custom("value is not 'scrape'"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn serialize_20_bytes<S>(
|
||||
data: &[u8; 20],
|
||||
serializer: S
|
||||
) -> Result<S::Ok, S::Error> where S: Serializer {
|
||||
let text: String = data.iter().map(|byte| *byte as char).collect();
|
||||
let text: String = data.iter().map(|byte| char::from(*byte)).collect();
|
||||
|
||||
serializer.serialize_str(&text)
|
||||
}
|
||||
|
|
@ -71,69 +113,11 @@ pub fn deserialize_20_bytes<'de, D>(
|
|||
}
|
||||
|
||||
|
||||
pub struct InfoHashVecVisitor;
|
||||
|
||||
|
||||
impl<'de> Visitor<'de> for InfoHashVecVisitor {
|
||||
type Value = Vec<InfoHash>;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("string or array of strings consisting of 20 bytes")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where E: ::serde::de::Error,
|
||||
{
|
||||
match TwentyByteVisitor::visit_str::<E>(TwentyByteVisitor, value){
|
||||
Ok(arr) => Ok(vec![InfoHash(arr)]),
|
||||
Err(err) => Err(E::custom(format!("got string, but {}", err)))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||
where A: SeqAccess<'de>
|
||||
{
|
||||
let mut info_hashes: Self::Value = Vec::new();
|
||||
|
||||
while let Ok(Some(value)) = seq.next_element::<&str>(){
|
||||
let arr = TwentyByteVisitor::visit_str(
|
||||
TwentyByteVisitor, value
|
||||
)?;
|
||||
|
||||
info_hashes.push(InfoHash(arr));
|
||||
}
|
||||
|
||||
Ok(info_hashes)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn visit_none<E>(self) -> Result<Self::Value, E>
|
||||
where E: ::serde::de::Error
|
||||
{
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Empty vector is returned if value is null or any invalid info hash
|
||||
/// is present
|
||||
#[inline]
|
||||
pub fn deserialize_info_hashes<'de, D>(
|
||||
deserializer: D
|
||||
) -> Result<Vec<InfoHash>, D::Error>
|
||||
where D: Deserializer<'de>,
|
||||
{
|
||||
Ok(deserializer.deserialize_any(InfoHashVecVisitor).unwrap_or_default())
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde::Deserialize;
|
||||
use quickcheck_macros::quickcheck;
|
||||
|
||||
use super::*;
|
||||
use crate::InfoHash;
|
||||
|
||||
fn info_hash_from_bytes(bytes: &[u8]) -> InfoHash {
|
||||
let mut arr = [0u8; 20];
|
||||
|
|
@ -175,73 +159,12 @@ mod tests {
|
|||
assert_eq!(info_hash, info_hash_2);
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Deserialize)]
|
||||
struct Test {
|
||||
#[serde(deserialize_with = "deserialize_info_hashes", default)]
|
||||
info_hashes: Vec<InfoHash>,
|
||||
#[quickcheck]
|
||||
fn quickcheck_serde_20_bytes(info_hash: InfoHash) -> bool {
|
||||
let out = serde_json::to_string(&info_hash).unwrap();
|
||||
let info_hash_2 = serde_json::from_str(&out).unwrap();
|
||||
|
||||
info_hash == info_hash_2
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_info_hashes_vec(){
|
||||
let input = r#"{
|
||||
"info_hashes": ["aaaabbbbccccddddeeee", "aaaabbbbccccddddeeee"]
|
||||
}"#;
|
||||
|
||||
let expected = Test {
|
||||
info_hashes: vec![
|
||||
info_hash_from_bytes(b"aaaabbbbccccddddeeee"),
|
||||
info_hash_from_bytes(b"aaaabbbbccccddddeeee"),
|
||||
]
|
||||
};
|
||||
|
||||
let observed: Test = serde_json::from_str(input).unwrap();
|
||||
|
||||
assert_eq!(observed, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_info_hashes_str(){
|
||||
let input = r#"{
|
||||
"info_hashes": "aaaabbbbccccddddeeee"
|
||||
}"#;
|
||||
|
||||
let expected = Test {
|
||||
info_hashes: vec![
|
||||
info_hash_from_bytes(b"aaaabbbbccccddddeeee"),
|
||||
]
|
||||
};
|
||||
|
||||
let observed: Test = serde_json::from_str(input).unwrap();
|
||||
|
||||
assert_eq!(observed, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_info_hashes_null(){
|
||||
let input = r#"{
|
||||
"info_hashes": null
|
||||
}"#;
|
||||
|
||||
let expected = Test {
|
||||
info_hashes: vec![]
|
||||
};
|
||||
|
||||
let observed: Test = serde_json::from_str(input).unwrap();
|
||||
|
||||
assert_eq!(observed, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_info_hashes_missing(){
|
||||
let input = r#"{}"#;
|
||||
|
||||
let expected = Test {
|
||||
info_hashes: vec![]
|
||||
};
|
||||
|
||||
let observed: Test = serde_json::from_str(input).unwrap();
|
||||
|
||||
assert_eq!(observed, expected);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue