mirror of
https://github.com/YGGverse/aquatic.git
synced 2026-03-31 17:55:36 +00:00
Move all crates to new crates dir
This commit is contained in:
parent
3835da22ac
commit
9b032f7e24
128 changed files with 27 additions and 26 deletions
33
crates/ws_protocol/Cargo.toml
Normal file
33
crates/ws_protocol/Cargo.toml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
[package]
|
||||
name = "aquatic_ws_protocol"
|
||||
description = "WebTorrent tracker protocol"
|
||||
exclude = ["target"]
|
||||
keywords = ["webtorrent", "protocol", "peer-to-peer", "torrent", "bittorrent"]
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
readme.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "aquatic_ws_protocol"
|
||||
|
||||
[[bench]]
|
||||
name = "bench_deserialize_announce_request"
|
||||
path = "benches/bench_deserialize_announce_request.rs"
|
||||
harness = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
hashbrown = { version = "0.14", features = ["serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
simd-json = "0.12"
|
||||
tungstenite = "0.20"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.5"
|
||||
quickcheck = "1"
|
||||
quickcheck_macros = "1"
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use std::time::Duration;
|
||||
|
||||
use aquatic_ws_protocol::*;
|
||||
|
||||
pub fn bench(c: &mut Criterion) {
|
||||
let info_hash = InfoHash([
|
||||
b'a', b'b', b'c', b'd', b'e', b'?', b'\n', b'1', b'2', b'3', 0, 1, 2, 3, 4, 0, 1, 2, 3, 4,
|
||||
]);
|
||||
let peer_id = PeerId(info_hash.0);
|
||||
let offers: Vec<AnnounceRequestOffer> = (0..10)
|
||||
.map(|i| {
|
||||
let mut offer_id = OfferId(info_hash.0);
|
||||
|
||||
offer_id.0[i] = i as u8;
|
||||
|
||||
AnnounceRequestOffer {
|
||||
offer: JsonValue(::serde_json::json!({ "sdp": "abcdef" })),
|
||||
offer_id,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let offers_len = offers.len();
|
||||
|
||||
let request = InMessage::AnnounceRequest(AnnounceRequest {
|
||||
action: AnnounceAction,
|
||||
info_hash,
|
||||
peer_id,
|
||||
bytes_left: Some(2),
|
||||
event: Some(AnnounceEvent::Started),
|
||||
offers: Some(offers),
|
||||
numwant: Some(offers_len),
|
||||
answer: Some(JsonValue(::serde_json::json!({ "sdp": "abcdef" }))),
|
||||
to_peer_id: Some(peer_id),
|
||||
offer_id: Some(OfferId(info_hash.0)),
|
||||
});
|
||||
|
||||
let ws_message = request.to_ws_message();
|
||||
|
||||
c.bench_function("deserialize-announce-request", |b| {
|
||||
b.iter(|| InMessage::from_ws_message(black_box(ws_message.clone())))
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group! {
|
||||
name = benches;
|
||||
config = Criterion::default()
|
||||
.sample_size(1000)
|
||||
.measurement_time(Duration::from_secs(180))
|
||||
.significance_level(0.01);
|
||||
targets = bench
|
||||
}
|
||||
criterion_main!(benches);
|
||||
251
crates/ws_protocol/src/common.rs
Normal file
251
crates/ws_protocol/src/common.rs
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct PeerId(
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_20_bytes",
|
||||
serialize_with = "serialize_20_bytes"
|
||||
)]
|
||||
pub [u8; 20],
|
||||
);
|
||||
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct InfoHash(
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_20_bytes",
|
||||
serialize_with = "serialize_20_bytes"
|
||||
)]
|
||||
pub [u8; 20],
|
||||
);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct OfferId(
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_20_bytes",
|
||||
serialize_with = "serialize_20_bytes"
|
||||
)]
|
||||
pub [u8; 20],
|
||||
);
|
||||
|
||||
/// Some kind of nested structure from https://www.npmjs.com/package/simple-peer
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct JsonValue(pub ::serde_json::Value);
|
||||
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
|
||||
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'"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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| char::from(*byte)).collect();
|
||||
|
||||
serializer.serialize_str(&text)
|
||||
}
|
||||
|
||||
struct TwentyByteVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for TwentyByteVisitor {
|
||||
type Value = [u8; 20];
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("string consisting of 20 bytes")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: ::serde::de::Error,
|
||||
{
|
||||
// Value is encoded in nodejs reference client something as follows:
|
||||
// ```
|
||||
// var infoHash = 'abcd..'; // 40 hexadecimals
|
||||
// Buffer.from(infoHash, 'hex').toString('binary');
|
||||
// ```
|
||||
// As I understand it:
|
||||
// - the code above produces a UTF16 string of 20 chars, each having
|
||||
// only the "low byte" set (e.g., numeric value ranges from 0-255)
|
||||
// - serde_json decodes this to string of 20 chars (tested), each in
|
||||
// the aforementioned range (tested), so the bytes can be extracted
|
||||
// by casting each char to u8.
|
||||
|
||||
let mut arr = [0u8; 20];
|
||||
let mut char_iter = value.chars();
|
||||
|
||||
for a in arr.iter_mut() {
|
||||
if let Some(c) = char_iter.next() {
|
||||
if c as u32 > 255 {
|
||||
return Err(E::custom(format!(
|
||||
"character not in single byte range: {:#?}",
|
||||
c
|
||||
)));
|
||||
}
|
||||
|
||||
*a = c as u8;
|
||||
} else {
|
||||
return Err(E::custom(format!("not 20 bytes: {:#?}", value)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(arr)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn deserialize_20_bytes<'de, D>(deserializer: D) -> Result<[u8; 20], D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(TwentyByteVisitor)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use quickcheck_macros::quickcheck;
|
||||
|
||||
use crate::common::InfoHash;
|
||||
|
||||
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_20_bytes() {
|
||||
unsafe {
|
||||
let mut input = r#""aaaabbbbccccddddeeee""#.to_string();
|
||||
|
||||
let expected = info_hash_from_bytes(b"aaaabbbbccccddddeeee");
|
||||
let observed: InfoHash = ::simd_json::serde::from_str(&mut input).unwrap();
|
||||
|
||||
assert_eq!(observed, expected);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let mut input = r#""aaaabbbbccccddddeee""#.to_string();
|
||||
let res_info_hash: Result<InfoHash, _> = ::simd_json::serde::from_str(&mut input);
|
||||
|
||||
assert!(res_info_hash.is_err());
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let mut input = r#""aaaabbbbccccddddeee𝕊""#.to_string();
|
||||
let res_info_hash: Result<InfoHash, _> = ::simd_json::serde::from_str(&mut input);
|
||||
|
||||
assert!(res_info_hash.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serde_20_bytes() {
|
||||
let info_hash = info_hash_from_bytes(b"aaaabbbbccccddddeeee");
|
||||
|
||||
let info_hash_2 = unsafe {
|
||||
let mut out = ::simd_json::serde::to_string(&info_hash).unwrap();
|
||||
|
||||
::simd_json::serde::from_str(&mut out).unwrap()
|
||||
};
|
||||
|
||||
assert_eq!(info_hash, info_hash_2);
|
||||
}
|
||||
|
||||
#[quickcheck]
|
||||
fn quickcheck_serde_20_bytes(info_hash: InfoHash) -> bool {
|
||||
unsafe {
|
||||
let mut out = ::simd_json::serde::to_string(&info_hash).unwrap();
|
||||
let info_hash_2 = ::simd_json::serde::from_str(&mut out).unwrap();
|
||||
|
||||
info_hash == info_hash_2
|
||||
}
|
||||
}
|
||||
}
|
||||
364
crates/ws_protocol/src/lib.rs
Normal file
364
crates/ws_protocol/src/lib.rs
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
//! WebTorrent protocol implementation
|
||||
//!
|
||||
//! Typical announce workflow:
|
||||
//! - Peer A sends announce request with info hash and offers
|
||||
//! - Tracker sends on offers to other peers announcing with that info hash and
|
||||
//! sends back announce response to peer A
|
||||
//! - Tracker receives answers to those offers from other peers and send them
|
||||
//! on to peer A
|
||||
//!
|
||||
//! Typical scrape workflow
|
||||
//! - Peer sends scrape request and receives scrape response
|
||||
|
||||
pub mod common;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
|
||||
pub use common::*;
|
||||
pub use request::*;
|
||||
pub use response::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use quickcheck::Arbitrary;
|
||||
use quickcheck_macros::quickcheck;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn arbitrary_20_bytes(g: &mut quickcheck::Gen) -> [u8; 20] {
|
||||
let mut bytes = [0u8; 20];
|
||||
|
||||
for byte in bytes.iter_mut() {
|
||||
*byte = u8::arbitrary(g);
|
||||
}
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
fn sdp_json_value() -> JsonValue {
|
||||
JsonValue(::serde_json::json!({ "sdp": "test" }))
|
||||
}
|
||||
|
||||
impl Arbitrary for InfoHash {
|
||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||
Self(arbitrary_20_bytes(g))
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for PeerId {
|
||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||
Self(arbitrary_20_bytes(g))
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for OfferId {
|
||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||
Self(arbitrary_20_bytes(g))
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for AnnounceEvent {
|
||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||
match (bool::arbitrary(g), bool::arbitrary(g)) {
|
||||
(false, false) => Self::Started,
|
||||
(true, false) => Self::Started,
|
||||
(false, true) => Self::Completed,
|
||||
(true, true) => Self::Update,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for MiddlemanOfferToPeer {
|
||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||
Self {
|
||||
action: AnnounceAction,
|
||||
peer_id: Arbitrary::arbitrary(g),
|
||||
info_hash: Arbitrary::arbitrary(g),
|
||||
offer_id: Arbitrary::arbitrary(g),
|
||||
offer: sdp_json_value(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for MiddlemanAnswerToPeer {
|
||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||
Self {
|
||||
action: AnnounceAction,
|
||||
peer_id: Arbitrary::arbitrary(g),
|
||||
info_hash: Arbitrary::arbitrary(g),
|
||||
offer_id: Arbitrary::arbitrary(g),
|
||||
answer: sdp_json_value(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for AnnounceRequestOffer {
|
||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||
Self {
|
||||
offer_id: Arbitrary::arbitrary(g),
|
||||
offer: sdp_json_value(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for AnnounceRequest {
|
||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||
let has_offers_or_answer_or_neither: Option<bool> = Arbitrary::arbitrary(g);
|
||||
|
||||
let mut offers: Option<Vec<AnnounceRequestOffer>> = None;
|
||||
let mut answer: Option<JsonValue> = None;
|
||||
let mut to_peer_id: Option<PeerId> = None;
|
||||
let mut offer_id: Option<OfferId> = None;
|
||||
|
||||
match has_offers_or_answer_or_neither {
|
||||
Some(true) => {
|
||||
offers = Some(Arbitrary::arbitrary(g));
|
||||
}
|
||||
Some(false) => {
|
||||
answer = Some(sdp_json_value());
|
||||
to_peer_id = Some(Arbitrary::arbitrary(g));
|
||||
offer_id = Some(Arbitrary::arbitrary(g));
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
|
||||
let numwant = offers.as_ref().map(|offers| offers.len());
|
||||
|
||||
Self {
|
||||
action: AnnounceAction,
|
||||
info_hash: Arbitrary::arbitrary(g),
|
||||
peer_id: Arbitrary::arbitrary(g),
|
||||
bytes_left: Arbitrary::arbitrary(g),
|
||||
event: Arbitrary::arbitrary(g),
|
||||
offers,
|
||||
numwant,
|
||||
answer,
|
||||
to_peer_id,
|
||||
offer_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for AnnounceResponse {
|
||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||
Self {
|
||||
action: AnnounceAction,
|
||||
info_hash: Arbitrary::arbitrary(g),
|
||||
complete: Arbitrary::arbitrary(g),
|
||||
incomplete: Arbitrary::arbitrary(g),
|
||||
announce_interval: Arbitrary::arbitrary(g),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for ScrapeRequest {
|
||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||
Self {
|
||||
action: ScrapeAction,
|
||||
info_hashes: Arbitrary::arbitrary(g),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for ScrapeRequestInfoHashes {
|
||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||
if Arbitrary::arbitrary(g) {
|
||||
ScrapeRequestInfoHashes::Multiple(Arbitrary::arbitrary(g))
|
||||
} else {
|
||||
ScrapeRequestInfoHashes::Single(Arbitrary::arbitrary(g))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for ScrapeStatistics {
|
||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||
Self {
|
||||
complete: Arbitrary::arbitrary(g),
|
||||
incomplete: Arbitrary::arbitrary(g),
|
||||
downloaded: Arbitrary::arbitrary(g),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for ScrapeResponse {
|
||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||
let files: Vec<(InfoHash, ScrapeStatistics)> = Arbitrary::arbitrary(g);
|
||||
|
||||
Self {
|
||||
action: ScrapeAction,
|
||||
files: files.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for InMessage {
|
||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||
if Arbitrary::arbitrary(g) {
|
||||
Self::AnnounceRequest(Arbitrary::arbitrary(g))
|
||||
} else {
|
||||
Self::ScrapeRequest(Arbitrary::arbitrary(g))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arbitrary for OutMessage {
|
||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||
match (Arbitrary::arbitrary(g), Arbitrary::arbitrary(g)) {
|
||||
(false, false) => Self::AnnounceResponse(Arbitrary::arbitrary(g)),
|
||||
(true, false) => Self::ScrapeResponse(Arbitrary::arbitrary(g)),
|
||||
(false, true) => Self::Offer(Arbitrary::arbitrary(g)),
|
||||
(true, true) => Self::Answer(Arbitrary::arbitrary(g)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[quickcheck]
|
||||
fn quickcheck_serde_identity_in_message(in_message_1: InMessage) -> bool {
|
||||
let ws_message = in_message_1.to_ws_message();
|
||||
|
||||
let in_message_2 = InMessage::from_ws_message(ws_message.clone()).unwrap();
|
||||
|
||||
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 quickcheck_serde_identity_out_message(out_message_1: OutMessage) -> bool {
|
||||
let ws_message = out_message_1.to_ws_message();
|
||||
|
||||
let out_message_2 = OutMessage::from_ws_message(ws_message.clone()).unwrap();
|
||||
|
||||
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 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 = unsafe {
|
||||
let mut input: String = r#"{
|
||||
"action": "scrape",
|
||||
"info_hash": ["aaaabbbbccccddddeeee", "aaaabbbbccccddddeeee"]
|
||||
}"#
|
||||
.into();
|
||||
|
||||
::simd_json::serde::from_str(&mut input).unwrap()
|
||||
};
|
||||
|
||||
assert_eq!(expected, observed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_info_hashes_str() {
|
||||
let info_hashes =
|
||||
ScrapeRequestInfoHashes::Single(info_hash_from_bytes(b"aaaabbbbccccddddeeee"));
|
||||
|
||||
let expected = ScrapeRequest {
|
||||
action: ScrapeAction,
|
||||
info_hashes: Some(info_hashes),
|
||||
};
|
||||
|
||||
let observed: ScrapeRequest = unsafe {
|
||||
let mut input: String = r#"{
|
||||
"action": "scrape",
|
||||
"info_hash": "aaaabbbbccccddddeeee"
|
||||
}"#
|
||||
.into();
|
||||
|
||||
::simd_json::serde::from_str(&mut input).unwrap()
|
||||
};
|
||||
|
||||
assert_eq!(expected, observed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_info_hashes_null() {
|
||||
let observed: ScrapeRequest = unsafe {
|
||||
let mut input: String = r#"{
|
||||
"action": "scrape",
|
||||
"info_hash": null
|
||||
}"#
|
||||
.into();
|
||||
|
||||
::simd_json::serde::from_str(&mut input).unwrap()
|
||||
};
|
||||
|
||||
let expected = ScrapeRequest {
|
||||
action: ScrapeAction,
|
||||
info_hashes: None,
|
||||
};
|
||||
|
||||
assert_eq!(expected, observed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_info_hashes_missing() {
|
||||
let observed: ScrapeRequest = unsafe {
|
||||
let mut input: String = r#"{
|
||||
"action": "scrape"
|
||||
}"#
|
||||
.into();
|
||||
|
||||
::simd_json::serde::from_str(&mut input).unwrap()
|
||||
};
|
||||
|
||||
let expected = ScrapeRequest {
|
||||
action: ScrapeAction,
|
||||
info_hashes: None,
|
||||
};
|
||||
|
||||
assert_eq!(expected, observed);
|
||||
}
|
||||
|
||||
#[quickcheck]
|
||||
fn quickcheck_serde_identity_info_hashes(info_hashes: ScrapeRequestInfoHashes) -> bool {
|
||||
let deserialized: ScrapeRequestInfoHashes = unsafe {
|
||||
let mut json = ::simd_json::serde::to_string(&info_hashes).unwrap();
|
||||
|
||||
::simd_json::serde::from_str(&mut json).unwrap()
|
||||
};
|
||||
|
||||
let success = info_hashes == deserialized;
|
||||
|
||||
if !success {}
|
||||
|
||||
success
|
||||
}
|
||||
}
|
||||
60
crates/ws_protocol/src/request/announce.rs
Normal file
60
crates/ws_protocol/src/request/announce.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::common::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AnnounceEvent {
|
||||
Started,
|
||||
Stopped,
|
||||
Completed,
|
||||
Update,
|
||||
}
|
||||
|
||||
impl Default for AnnounceEvent {
|
||||
fn default() -> Self {
|
||||
Self::Update
|
||||
}
|
||||
}
|
||||
|
||||
/// Element of AnnounceRequest.offers
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AnnounceRequestOffer {
|
||||
pub offer: JsonValue,
|
||||
pub offer_id: OfferId,
|
||||
}
|
||||
|
||||
#[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
|
||||
/// when opening a magnet link
|
||||
#[serde(rename = "left")]
|
||||
pub bytes_left: Option<usize>,
|
||||
/// Can be empty. Then, default is "update"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub event: Option<AnnounceEvent>,
|
||||
|
||||
/// Only when this is an array offers are sent to random peers
|
||||
/// Length of this is number of peers wanted?
|
||||
/// Max length of this is 10 in reference client code
|
||||
/// Not sent when announce event is stopped or completed
|
||||
pub offers: Option<Vec<AnnounceRequestOffer>>,
|
||||
/// Seems to only get sent by client when sending offers, and is also same
|
||||
/// as length of offers vector (or at least never less)
|
||||
/// Max length of this is 10 in reference client code
|
||||
/// Could probably be ignored, `offers.len()` should provide needed info
|
||||
pub numwant: Option<usize>,
|
||||
|
||||
/// If empty, send response before sending offers (or possibly "skip sending update back"?)
|
||||
/// Else, send MiddlemanAnswerToPeer to peer with "to_peer_id" as peer_id.
|
||||
/// I think using Option is good, it seems like this isn't always set
|
||||
/// (same as `offers`)
|
||||
pub answer: Option<JsonValue>,
|
||||
/// Likely undefined if !(answer == true)
|
||||
pub to_peer_id: Option<PeerId>,
|
||||
/// Sent if answer is set
|
||||
pub offer_id: Option<OfferId>,
|
||||
}
|
||||
40
crates/ws_protocol/src/request/mod.rs
Normal file
40
crates/ws_protocol/src/request/mod.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
use anyhow::Context;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod announce;
|
||||
pub mod scrape;
|
||||
|
||||
pub use announce::*;
|
||||
pub use scrape::*;
|
||||
|
||||
/// Message received by tracker
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum InMessage {
|
||||
AnnounceRequest(AnnounceRequest),
|
||||
ScrapeRequest(ScrapeRequest),
|
||||
}
|
||||
|
||||
impl InMessage {
|
||||
#[inline]
|
||||
pub fn to_ws_message(&self) -> ::tungstenite::Message {
|
||||
::tungstenite::Message::from(::serde_json::to_string(&self).unwrap())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn from_ws_message(ws_message: tungstenite::Message) -> ::anyhow::Result<Self> {
|
||||
use tungstenite::Message;
|
||||
|
||||
match ws_message {
|
||||
Message::Text(text) => {
|
||||
let mut text: Vec<u8> = text.into();
|
||||
|
||||
::simd_json::serde::from_slice(&mut text).context("deserialize with serde")
|
||||
}
|
||||
Message::Binary(mut bytes) => {
|
||||
::simd_json::serde::from_slice(&mut bytes[..]).context("deserialize with serde")
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("Message is neither text nor binary")),
|
||||
}
|
||||
}
|
||||
}
|
||||
29
crates/ws_protocol/src/request/scrape.rs
Normal file
29
crates/ws_protocol/src/request/scrape.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::common::*;
|
||||
|
||||
#[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")]
|
||||
pub info_hashes: Option<ScrapeRequestInfoHashes>,
|
||||
}
|
||||
14
crates/ws_protocol/src/response/announce.rs
Normal file
14
crates/ws_protocol/src/response/announce.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::common::*;
|
||||
|
||||
#[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,
|
||||
pub incomplete: usize,
|
||||
#[serde(rename = "interval")]
|
||||
pub announce_interval: usize, // Default 2 min probably
|
||||
}
|
||||
16
crates/ws_protocol/src/response/answer.rs
Normal file
16
crates/ws_protocol/src/response/answer.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::common::*;
|
||||
|
||||
/// If announce request has answer = true, send this to peer with
|
||||
/// peer id == "to_peer_id" field
|
||||
/// 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,
|
||||
pub answer: JsonValue,
|
||||
pub offer_id: OfferId,
|
||||
}
|
||||
24
crates/ws_protocol/src/response/error.rs
Normal file
24
crates/ws_protocol/src/response/error.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::common::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ErrorResponseAction {
|
||||
Announce,
|
||||
Scrape,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ErrorResponse {
|
||||
#[serde(rename = "failure reason")]
|
||||
pub failure_reason: Cow<'static, str>,
|
||||
/// Action of original request
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub action: Option<ErrorResponseAction>,
|
||||
// Should not be renamed
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub info_hash: Option<InfoHash>,
|
||||
}
|
||||
44
crates/ws_protocol/src/response/mod.rs
Normal file
44
crates/ws_protocol/src/response/mod.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod announce;
|
||||
pub mod answer;
|
||||
pub mod error;
|
||||
pub mod offer;
|
||||
pub mod scrape;
|
||||
|
||||
pub use announce::*;
|
||||
pub use answer::*;
|
||||
pub use error::*;
|
||||
pub use offer::*;
|
||||
pub use scrape::*;
|
||||
|
||||
/// Message sent by tracker
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum OutMessage {
|
||||
Offer(MiddlemanOfferToPeer),
|
||||
Answer(MiddlemanAnswerToPeer),
|
||||
AnnounceResponse(AnnounceResponse),
|
||||
ScrapeResponse(ScrapeResponse),
|
||||
ErrorResponse(ErrorResponse),
|
||||
}
|
||||
|
||||
impl OutMessage {
|
||||
#[inline]
|
||||
pub fn to_ws_message(&self) -> tungstenite::Message {
|
||||
::tungstenite::Message::from(::serde_json::to_string(&self).unwrap())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn from_ws_message(message: ::tungstenite::Message) -> ::anyhow::Result<Self> {
|
||||
use tungstenite::Message::{Binary, Text};
|
||||
|
||||
let mut text: Vec<u8> = match message {
|
||||
Text(text) => text.into(),
|
||||
Binary(bytes) => String::from_utf8(bytes)?.into(),
|
||||
_ => return Err(anyhow::anyhow!("Message is neither text nor bytes")),
|
||||
};
|
||||
|
||||
Ok(::simd_json::serde::from_slice(&mut text)?)
|
||||
}
|
||||
}
|
||||
19
crates/ws_protocol/src/response/offer.rs
Normal file
19
crates/ws_protocol/src/response/offer.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::common::*;
|
||||
|
||||
/// Apparently, these are sent to a number of peers when they are set
|
||||
/// in an AnnounceRequest
|
||||
/// 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,
|
||||
pub info_hash: InfoHash,
|
||||
/// Gets copied from AnnounceRequestOffer
|
||||
pub offer: JsonValue,
|
||||
/// Gets copied from AnnounceRequestOffer
|
||||
pub offer_id: OfferId,
|
||||
}
|
||||
19
crates/ws_protocol/src/response/scrape.rs
Normal file
19
crates/ws_protocol/src/response/scrape.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
use hashbrown::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::common::*;
|
||||
|
||||
#[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, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ScrapeStatistics {
|
||||
pub complete: usize,
|
||||
pub incomplete: usize,
|
||||
pub downloaded: usize,
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"group_id":"deserialize-announce-request","function_id":null,"value_str":null,"throughput":null,"full_id":"deserialize-announce-request","directory_name":"deserialize-announce-request","title":"deserialize-announce-request"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"mean":{"confidence_interval":{"confidence_level":0.95,"lower_bound":17747.517549723954,"upper_bound":17827.267537642005},"point_estimate":17784.6041414677,"standard_error":20.37407375834334},"median":{"confidence_interval":{"confidence_level":0.95,"lower_bound":17577.94007936508,"upper_bound":17610.776785618924},"point_estimate":17593.548306902798,"standard_error":9.518459372549549},"median_abs_dev":{"confidence_interval":{"confidence_level":0.95,"lower_bound":178.47341704422882,"upper_bound":242.9566257467244},"point_estimate":214.59916963759514,"standard_error":16.547608837291573},"slope":{"confidence_interval":{"confidence_level":0.95,"lower_bound":17756.912325578603,"upper_bound":17852.279555854253},"point_estimate":17798.93543491748,"standard_error":24.423183313014615},"std_dev":{"confidence_interval":{"confidence_level":0.95,"lower_bound":426.35101181405963,"upper_bound":857.9956431905033},"point_estimate":643.9714734076442,"standard_error":111.389827472484}}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
[16365.852585890654,16945.45216926808,18491.05105827454,19070.650641651962]
|
||||
Loading…
Add table
Add a link
Reference in a new issue