aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorWill Dillon <william@housedillon.com>2025-11-13 06:16:41 +0000
committerWill Dillon <william@housedillon.com>2025-11-13 06:16:41 +0000
commitae789a540db3c7adbe3a85f890f865c01d18d9d4 (patch)
tree4695262fb12a75636b3899bb60ca101ac0f9a61c /src
parentAdded more packets (diff)
downloadmeshcore-rs-ae789a540db3c7adbe3a85f890f865c01d18d9d4.tar.gz
meshcore-rs-ae789a540db3c7adbe3a85f890f865c01d18d9d4.zip
Refactored packet_content and added way more tests
Diffstat (limited to 'src')
-rw-r--r--src/ack.rs59
-rw-r--r--src/advert.rs214
-rw-r--r--src/anon_req.rs128
-rw-r--r--src/bin/packet_analyzer.rs2
-rw-r--r--src/crypto.rs2
-rw-r--r--src/identity.rs46
-rw-r--r--src/lib.rs9
-rw-r--r--src/multipart.rs50
-rw-r--r--src/packet.rs56
-rw-r--r--src/packet_content.rs996
-rw-r--r--src/path.rs61
-rw-r--r--src/request.rs202
-rw-r--r--src/response.rs69
-rw-r--r--src/text.rs348
-rw-r--r--src/trace.rs87
15 files changed, 1320 insertions, 1009 deletions
diff --git a/src/ack.rs b/src/ack.rs
new file mode 100644
index 0000000..a5c4b63
--- /dev/null
+++ b/src/ack.rs
@@ -0,0 +1,59 @@
+use std::fmt::Display;
+
+use bytes::{Buf, Bytes};
+use structdiff::{Difference, StructDiff};
+
+#[derive(PartialEq, Debug, Clone, Difference)]
+#[difference(expose)]
+pub struct Ack {
+ checksum: u32
+}
+
+impl From<Bytes> for Ack {
+ fn from(value: Bytes) -> Self {
+ let mut bytes = value;
+ Ack { checksum: bytes.get_u32() }
+ }
+}
+
+impl Display for Ack {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_fmt(format_args!("Checksum: {:4x?}", self.checksum))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::str::FromStr;
+ use hex::decode;
+ use bytes::Bytes;
+ use crate::{ack::Ack, packet::*, packet_content::PacketContent};
+
+ #[test]
+ fn ack() {
+ let sample = "0D0E78B5561B03CD70DA326066B8A0CF24F3214D";
+
+ let lhs_packet = Packet {
+ route_type: RouteType::Flood,
+ version: PayloadVersion::VersionOne,
+ path: vec![0x78, 0xB5, 0x56, 0x1B, 0x03, 0xCD, 0x70, 0xDA, 0x32, 0x60, 0x66, 0xB8, 0xA0, 0xCF],
+ transport: [0, 0],
+ raw_content: Bytes::copy_from_slice(&decode("24F3214D").unwrap()),
+ content: PacketContent::Ack(Ack {
+ checksum: 0x24F3214D
+ }),
+ incomplete: false
+ };
+
+ let rhs_packet = Packet::from_str(sample).unwrap();
+ assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
+ }
+
+ #[test]
+ fn display() {
+ let ack = Ack { checksum: 0x24F3214D };
+ println!("{}", ack);
+ assert!(format!("{}", ack) == "Checksum: 24f3214d");
+ }
+
+} \ No newline at end of file
diff --git a/src/advert.rs b/src/advert.rs
new file mode 100644
index 0000000..dbbe84d
--- /dev/null
+++ b/src/advert.rs
@@ -0,0 +1,214 @@
+use structdiff::{Difference, StructDiff};
+use crate::{crypto::PublicKey, packet_content::NodeType};
+use bytes::{Buf, Bytes};
+use std::fmt::{Debug, Display};
+use chrono::{DateTime, Local, Utc};
+
+#[derive(PartialEq, Debug, Clone, Difference)]
+#[difference(expose)]
+pub struct Advert {
+ pub public_key: PublicKey,
+ pub timestamp: DateTime<Utc>,
+ pub signature: [u8; 64],
+
+ pub node_type: NodeType,
+ pub latitude: Option<f32>,
+ pub longitude: Option<f32>,
+ pub feature1: Option<u16>,
+ pub feature2: Option<u16>,
+ pub name: String
+}
+
+impl From<Bytes> for Advert {
+ fn from(value: Bytes) -> Self {
+ let mut bytes = value;
+
+ let mut advert = Advert {
+ public_key: PublicKey::default(),
+ timestamp: Local::now().into(),
+ signature: [0_u8; 64],
+
+ node_type: NodeType::Invalid,
+ latitude: None,
+ longitude: None,
+ feature1: None,
+ feature2: None,
+ name: "".to_string(),
+ };
+
+ if bytes.len() < 32 { return advert }
+ if let Ok(key) = PublicKey::try_from(bytes.split_to(32)) {
+ advert.public_key = key;
+ } else {
+ return advert
+ }
+
+ if bytes.len() < 4 { return advert }
+ if let Some(time) = DateTime::from_timestamp(bytes.get_u32_le() as i64, 0) {
+ advert.timestamp = time;
+ }
+
+ if bytes.len() < 64 { return advert }
+ _ = bytes.try_copy_to_slice(&mut advert.signature);
+
+ if bytes.is_empty() { return advert }
+ let flags = bytes.get_u8();
+ advert.node_type = NodeType::from(flags);
+
+ if (flags & 0x10) != 0 {
+ // The location is 8 bytes (4 each for lat and lon)
+ if bytes.len() < 8 { return advert }
+
+ advert.latitude = Some(bytes.get_i32_le() as f32 / 1_000_000.0);
+ advert.longitude = Some(bytes.get_i32_le() as f32 / 1_000_000.0);
+ }
+
+ if (flags & 0x20) != 0 {
+ // Feature 1 is 2 bytes when it's included
+ if bytes.len() < 2 { return advert }
+ advert.feature1 = Some(bytes.get_u16_le());
+ }
+
+ if (flags & 0x40) != 0 {
+ // Feature 2 is the same
+ if bytes.len() < 2 { return advert }
+ advert.feature2 = Some(bytes.get_u16_le());
+ }
+
+ // Lastly the name... The flag is a little
+ // irrelevant because the only different is
+ // whether the name is omitted or just empty.
+ // I'm not going to make the distinction
+
+ // The string is assumed to be utf8, but if that
+ // fails to parse correctly, try to make it lossy
+ // which will insert U+FFFD REPLACEMENT CHARACTER (�)
+ if let Ok(string) = String::from_utf8(bytes.to_vec()) {
+ advert.name = string
+ } else {
+ advert.name = String::from_utf8_lossy(&bytes).to_string();
+ }
+
+ advert
+ }
+}
+
+impl Display for Advert {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.node_type.fmt(f)?;
+ f.write_str(" \"")?;
+ f.write_str(&self.name)?;
+ f.write_fmt(format_args!("\" ({:2x?}) at: ", self.public_key.hash_prefix()))?;
+ std::fmt::Display::fmt(&self.timestamp, f)?;
+
+ match (self.latitude, self.longitude) {
+ (Some(lat), Some(lon)) => {
+ f.write_fmt(format_args!(" location: {}, {}", lat, lon))?;
+ },
+ _ => {},
+ }
+
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::str::FromStr;
+
+ use chrono::DateTime;
+ use hex::decode;
+
+ use crate::packet::*;
+ use crate::crypto::*;
+ use crate::packet_content::PacketContent;
+
+ use super::*;
+
+
+ #[test]
+ fn advert_with_lat_lon() {
+ // Real sample packet from the air
+ let sample = "110c015f7e9b60661da0f2671512460728508c17ef336412a223144d3a623215162682045c44fef7241af0161923d3af0769d2d976b687a506dd5325ef526bf3eb52ae687277fcbde9969a5b0087e0eb0f7c1760a50c6a88bec13cc30a2a9b681d713166515e3bbc2bc27f20c0e4d7b67e08910c29dc02c468b8f8484f574c";
+
+ let mut signature_slice = [0 as u8; 64];
+ hex::decode_to_slice("d2d976b687a506dd5325ef526bf3eb52ae687277fcbde9969a5b0087e0eb0f7c1760a50c6a88bec13cc30a2a9b681d713166515e3bbc2bc27f20c0e4d7b67e08", &mut signature_slice).unwrap();
+
+ let lhs_packet = Packet {
+ route_type: RouteType::Flood,
+ version: PayloadVersion::VersionOne,
+ path: vec![0x01, 0x5f, 0x7e, 0x9b, 0x60, 0x66, 0x1d, 0xa0, 0xf2, 0x67, 0x15, 0x12],
+ transport: [0x00, 0x00],
+ raw_content: Bytes::copy_from_slice(&decode("460728508c17ef336412a223144d3a623215162682045c44fef7241af0161923d3af0769d2d976b687a506dd5325ef526bf3eb52ae687277fcbde9969a5b0087e0eb0f7c1760a50c6a88bec13cc30a2a9b681d713166515e3bbc2bc27f20c0e4d7b67e08910c29dc02c468b8f8484f574c").unwrap()),
+ content: PacketContent::Advert(Advert {
+ public_key: PublicKey::from_str("460728508c17ef336412a223144d3a623215162682045c44fef7241af0161923").unwrap(),
+ timestamp: DateTime::from_timestamp(1762111443, 0).unwrap(),
+ signature: signature_slice,
+ node_type: NodeType::Chat,
+ latitude: Some(47.98286),
+ longitude: Some(-122.132286),
+ feature1: None,
+ feature2: None,
+ name: "HOWL".to_owned(),
+ }),
+ incomplete: false,
+ };
+ let rhs_packet = Packet::from_str(sample).unwrap();
+
+ assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
+ }
+
+ #[test]
+ fn advert() {
+ // Another on-air sample
+ let sample = "12007890b8573a6ba4a05b173d6ccfdfa73ac8ec4a12bf3c745ace636e1d191e132a4eb407695601f50907999735f3699a3c73ace2d8acc385ed209606abef6b914a346fbb88648c540ae794586b4d54eb08b473c8571a00c6b8d3dbcc77699afa169811fa098168707578373335";
+
+ let mut signature_slice = [0 as u8; 64];
+ hex::decode_to_slice("5601f50907999735f3699a3c73ace2d8acc385ed209606abef6b914a346fbb88648c540ae794586b4d54eb08b473c8571a00c6b8d3dbcc77699afa169811fa09", &mut signature_slice).unwrap();
+
+ let lhs_packet = Packet {
+ route_type: RouteType::Direct,
+ version: PayloadVersion::VersionOne,
+ path: vec![],
+ transport: [0x00, 0x00],
+ raw_content: Bytes::copy_from_slice(&decode("7890b8573a6ba4a05b173d6ccfdfa73ac8ec4a12bf3c745ace636e1d191e132a4eb407695601f50907999735f3699a3c73ace2d8acc385ed209606abef6b914a346fbb88648c540ae794586b4d54eb08b473c8571a00c6b8d3dbcc77699afa169811fa098168707578373335").unwrap()),
+ content: PacketContent::Advert(Advert {
+ public_key: PublicKey::from_str("7890b8573a6ba4a05b173d6ccfdfa73ac8ec4a12bf3c745ace636e1d191e132a").unwrap(),
+ timestamp: DateTime::from_timestamp(1762112590, 0).unwrap(),
+ signature: signature_slice,
+ node_type: NodeType::Chat,
+ latitude: None,
+ longitude: None,
+ feature1: None,
+ feature2: None,
+ name: "hpux735".to_owned(),
+ }),
+ incomplete: false,
+ };
+
+ let rhs_packet = Packet::from_str(sample).unwrap();
+ assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
+ }
+
+
+ #[test]
+ fn display() {
+ let mut signature_slice = [0 as u8; 64];
+ hex::decode_to_slice("d2d976b687a506dd5325ef526bf3eb52ae687277fcbde9969a5b0087e0eb0f7c1760a50c6a88bec13cc30a2a9b681d713166515e3bbc2bc27f20c0e4d7b67e08", &mut signature_slice).unwrap();
+
+ let advert = Advert {
+ public_key: PublicKey::from_str("460728508c17ef336412a223144d3a623215162682045c44fef7241af0161923").unwrap(),
+ timestamp: DateTime::from_timestamp(1762111443, 0).unwrap(),
+ signature: signature_slice,
+ node_type: NodeType::Chat,
+ latitude: Some(47.98286),
+ longitude: Some(-122.132286),
+ feature1: None,
+ feature2: None,
+ name: "HOWL".to_owned(),
+ };
+ println!("{}", advert);
+ assert!(format!("{}", advert) == "Chat \"HOWL\" (46) at: 2025-11-02 19:24:03 UTC location: 47.98286, -122.132286");
+ }
+
+} \ No newline at end of file
diff --git a/src/anon_req.rs b/src/anon_req.rs
new file mode 100644
index 0000000..2f449a7
--- /dev/null
+++ b/src/anon_req.rs
@@ -0,0 +1,128 @@
+use std::fmt::{Debug, Display};
+use chrono::{DateTime, Utc};
+use bytes::{Buf, Bytes};
+use structdiff::{Difference, StructDiff};
+use crate::crypto::PublicKey;
+
+#[derive(PartialEq, Debug, Clone, Difference)]
+#[difference(expose)]
+pub struct AnonReq {
+ pub dest: u8,
+ pub public_key: PublicKey,
+ pub mac: u16,
+ pub ciphertext: Bytes,
+ pub request: Option<ClearAnonRequest>,
+ incomplete: bool
+}
+
+impl From<Bytes> for AnonReq {
+ fn from(value: Bytes) -> Self {
+ let mut bytes = value;
+
+ let mut anon_req = AnonReq {
+ dest: 0x00,
+ public_key: PublicKey::default(),
+ mac: 0x0000,
+ ciphertext: Bytes::new(),
+ request: None,
+ incomplete: false,
+ };
+
+ if bytes.is_empty() { return anon_req; }
+ anon_req.dest = bytes.get_u8();
+
+ if bytes.len() < 32 { return anon_req; }
+ if let Ok(pub_key) = PublicKey::try_from(bytes.split_to(32)) {
+ anon_req.public_key = pub_key;
+ }
+
+ if bytes.len() < 2 { return anon_req; }
+ anon_req.mac = bytes.get_u16();
+
+ anon_req.ciphertext = bytes;
+ anon_req.incomplete = false;
+ anon_req
+ }
+}
+
+impl Display for AnonReq {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_fmt(format_args!("({:2x?}) -> ({:2x?}) MAC: {:4x?} ", self.public_key.hash_prefix(), self.dest, self.mac))?;
+
+ if let Some(cleartext) = &self.request {
+ f.write_fmt(format_args!("at: {} password: \"{}\"", cleartext.timestamp, cleartext.password))
+ } else {
+ f.write_str("ENCRYPTED")
+ }
+ }
+}
+
+#[derive(PartialEq, Debug, Clone, Difference)]
+#[difference(expose)]
+pub struct ClearAnonRequest {
+ timestamp: DateTime<Utc>,
+ sync_timestamp: Option<DateTime<Utc>>,
+ password: String
+}
+
+impl From<Bytes> for ClearAnonRequest {
+ fn from(value: Bytes) -> Self {
+ let mut bytes = value;
+
+ let mut anon_req = ClearAnonRequest {
+ timestamp: DateTime::from_timestamp(0, 0).unwrap(),
+ sync_timestamp: None,
+ password: "".to_string(),
+ };
+
+ // Just check for the whole fixed-size part at once
+ if bytes.len() < 4 { return anon_req }
+ if let Some(timestamp) = DateTime::from_timestamp(bytes.get_u32() as i64, 0) {
+ anon_req.timestamp = timestamp;
+ }
+
+ if bytes.len() < 4 { return anon_req; }
+ if let Some(timestamp) = DateTime::from_timestamp(bytes.get_u32() as i64, 0) {
+ anon_req.sync_timestamp = Some(timestamp);
+ }
+
+ anon_req.password = String::from_utf8_lossy(&bytes).to_string();
+ anon_req
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::str::FromStr;
+ use hex::decode;
+ use crate::packet::*;
+ use crate::crypto::*;
+ use crate::packet_content::PacketContent;
+ use super::*;
+
+ #[test]
+ fn anon_req() {
+ let sample = "1d001234569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8fd2db15a7138098557cc291928b4358fa7522ddd41d35c99fb78f0def2b3e673d73d2";
+
+ let lhs_packet = Packet {
+ route_type: RouteType::Flood,
+ version: PayloadVersion::VersionOne,
+ path: vec![],
+ transport: [0, 0],
+ raw_content: Bytes::copy_from_slice(&decode("1234569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8fd2db15a7138098557cc291928b4358fa7522ddd41d35c99fb78f0def2b3e673d73d2").unwrap()),
+ content: PacketContent::AnonReq(AnonReq {
+ dest: 0x12,
+ public_key: PublicKey::try_from(Bytes::copy_from_slice(&decode("34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8f").unwrap())).unwrap(),
+ mac: 0xd2db,
+ ciphertext: Bytes::copy_from_slice(&decode("15a7138098557cc291928b4358fa7522ddd41d35c99fb78f0def2b3e673d73d2").unwrap()),
+ incomplete: false,
+ request: None
+ }),
+ incomplete: false
+ };
+
+ let rhs_packet = Packet::from_str(sample).unwrap();
+ assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
+ }
+
+} \ No newline at end of file
diff --git a/src/bin/packet_analyzer.rs b/src/bin/packet_analyzer.rs
index 96fcfd2..98f4862 100644
--- a/src/bin/packet_analyzer.rs
+++ b/src/bin/packet_analyzer.rs
@@ -2,7 +2,7 @@ use std::{borrow::Cow, path::PathBuf};
use hex::encode;
use log::{error, trace};
use tokio::fs::File;
-use tokio_util::bytes::Bytes;
+use bytes::Bytes;
use clap::Parser;
use color_eyre::eyre::Result;
diff --git a/src/crypto.rs b/src/crypto.rs
index 3175501..f814f78 100644
--- a/src/crypto.rs
+++ b/src/crypto.rs
@@ -1,6 +1,6 @@
use std::{fmt::{Debug, Display}, str::FromStr};
use ed25519_dalek::{VerifyingKey, hazmat::ExpandedSecretKey};
-use tokio_util::bytes::{Buf, BufMut, Bytes, BytesMut};
+use bytes::{Buf, BufMut, Bytes, BytesMut};
use hex::{decode, decode_to_slice, encode};
// This seems to be an absolute nightmare. GenericArray sucks
diff --git a/src/identity.rs b/src/identity.rs
index 6e0e252..ccbc3de 100644
--- a/src/identity.rs
+++ b/src/identity.rs
@@ -1,10 +1,12 @@
-use std::{collections::HashMap, rc::Rc, str::FromStr};
-use hex::decode;
+use std::{collections::HashMap, rc::Rc};
+use bytes::Bytes;
+use crate::crypto::{PrivateKey, PublicKey, SharedSecret};
+
+#[cfg(feature = "std")]
use serde::{Deserialize, de};
-use tokio_util::bytes::Bytes;
-use crate::crypto::{MeshcoreCryptoError, PrivateKey, PublicKey, SharedSecret};
-#[derive(PartialEq, Debug, Clone, Deserialize)]
+#[derive(PartialEq, Debug, Clone)]
+#[cfg_attr(feature = "std", derive(Deserialize))]
/// The Identity structure contains the information to decrypt
/// incoming messages and sign and encrypt outgoing messages.
///
@@ -18,13 +20,13 @@ pub struct Identity {
/// no ways in this library to generate a new private key
/// from scratch (while ensuing it's sound) so it must be
/// provided by the user.
- #[serde(deserialize_with = "deserialize_private_key")]
+ #[cfg_attr(feature = "std", serde(deserialize_with = "deserialize_private_key"))]
pub private_key: PrivateKey,
/// The derived public key given this identity's
/// private key. The hash prefix of this (typically 1-byte)
/// is used as a first-pass to identity the recipient.
- #[serde(skip)]
+ #[cfg_attr(feature = "std", serde(skip))]
pub public_key: PublicKey,
/// The secrets has is a collection of shared secrets
@@ -32,11 +34,12 @@ pub struct Identity {
/// connection. The whole remote public key must be
/// used because there are many many hash collisions
/// when using just the 1-byte hash prefix.
- #[serde(skip)]
+ #[cfg_attr(feature = "std", serde(skip))]
pub secrets: Vec<(Rc<String>, SharedSecret)>
}
-#[derive(PartialEq, Debug, Clone, Deserialize)]
+#[derive(PartialEq, Debug, Clone)]
+#[cfg_attr(feature = "std", derive(Deserialize))]
/// The Contact structure contains the information needed
/// to decrypt messages from a remote user intended for
/// either an identity or a channel.
@@ -51,11 +54,12 @@ pub struct Contact {
pub name: Rc<String>,
/// The provided public key of the remote contact
- #[serde(deserialize_with = "deserialize_public_key")]
+ #[cfg_attr(feature = "std", serde(deserialize_with = "deserialize_public_key"))]
pub public_key: PublicKey
}
-#[derive(PartialEq, Debug, Clone, Deserialize)]
+#[derive(PartialEq, Debug, Clone)]
+#[cfg_attr(feature = "std", derive(Deserialize))]
/// A Group in MeshCore is a kind of contact, except that its
/// secret is fixed and shared directly. It's not derived via
/// a public and private key. Otherwise, it behaves more like a
@@ -66,7 +70,7 @@ pub struct Group {
pub name: Rc<String>,
/// The group's shared secret
- #[serde(deserialize_with = "deserialize_secret")]
+ #[cfg_attr(feature = "std", serde(deserialize_with = "deserialize_secret"))]
pub secret: SharedSecret
}
@@ -109,7 +113,8 @@ impl Keystore {
}
}
-#[derive(PartialEq, Debug, Clone, Deserialize)]
+#[derive(PartialEq, Debug, Clone)]
+#[cfg_attr(feature = "std", derive(Deserialize))]
pub struct KeystoreInput {
pub identities: Vec<Identity>,
pub contacts: Vec<Contact>,
@@ -157,8 +162,13 @@ impl KeystoreInput {
}
}
+#[cfg(feature = "std")]
fn deserialize_private_key<'de, D>(deserializer: D) -> Result<PrivateKey, D::Error>
where D: de::Deserializer<'de> {
+
+ use std::str::FromStr;
+ use crate::crypto::{PrivateKey};
+
let s: String = de::Deserialize::deserialize(deserializer)?;
match PrivateKey::from_str(&s) {
Ok(key) => Ok(key),
@@ -168,8 +178,13 @@ fn deserialize_private_key<'de, D>(deserializer: D) -> Result<PrivateKey, D::Err
}
}
+#[cfg(feature = "std")]
fn deserialize_public_key<'de, D>(deserializer: D) -> Result<PublicKey, D::Error>
where D: de::Deserializer<'de> {
+
+ use hex::decode;
+ use crate::crypto::{MeshcoreCryptoError, PublicKey};
+
let s: String = de::Deserialize::deserialize(deserializer)?;
match decode(s) {
@@ -197,8 +212,13 @@ fn deserialize_public_key<'de, D>(deserializer: D) -> Result<PublicKey, D::Error
}
}
+#[cfg(feature = "std")]
fn deserialize_secret<'de, D>(deserializer: D) -> Result<SharedSecret, D::Error>
where D: de::Deserializer<'de> {
+
+ use std::str::FromStr;
+ use crate::crypto::SharedSecret;
+
let s: String = de::Deserialize::deserialize(deserializer)?;
match SharedSecret::from_str(&s) {
diff --git a/src/lib.rs b/src/lib.rs
index 20b8e6b..dfd777f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,3 +3,12 @@ pub mod identity;
pub mod packet;
pub mod packet_content;
+pub mod request;
+pub mod response;
+pub mod text;
+pub mod ack;
+pub mod advert;
+pub mod anon_req;
+pub mod path;
+pub mod trace;
+pub mod multipart; \ No newline at end of file
diff --git a/src/multipart.rs b/src/multipart.rs
new file mode 100644
index 0000000..5b7147f
--- /dev/null
+++ b/src/multipart.rs
@@ -0,0 +1,50 @@
+use std::fmt::{Debug, Display};
+use bytes::Bytes;
+use structdiff::{Difference, StructDiff};
+
+#[derive(PartialEq, Debug, Clone, Difference)]
+#[difference(expose)]
+pub struct MultiPart {
+ payload: Bytes
+}
+
+impl From<Bytes> for MultiPart {
+ fn from(value: Bytes) -> Self {
+ MultiPart { payload: value }
+ }
+}
+
+impl Display for MultiPart {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_fmt(format_args!("Multipart isn't defined."))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::str::FromStr;
+ use hex::decode;
+ use crate::{packet::*, packet_content::PacketContent};
+
+ use super::*;
+
+ #[test]
+ fn multipart() {
+ let sample = "2A0013E8C59624";
+
+ let lhs_packet = Packet {
+ route_type: RouteType::Direct,
+ version: PayloadVersion::VersionOne,
+ path: vec![],
+ transport: [0, 0],
+ raw_content: Bytes::copy_from_slice(&decode("13E8C59624").unwrap()),
+ content: PacketContent::Multipart(MultiPart {
+ payload: Bytes::copy_from_slice(&decode("13E8C59624").unwrap())
+ }),
+ incomplete: false
+ };
+
+ let rhs_packet = Packet::from_str(sample).unwrap();
+ assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
+ }
+} \ No newline at end of file
diff --git a/src/packet.rs b/src/packet.rs
index 73de4b5..d28fe90 100644
--- a/src/packet.rs
+++ b/src/packet.rs
@@ -1,9 +1,9 @@
use std::{fmt::{Debug, Display}, str::FromStr};
use hex::decode;
-use tokio_util::bytes::{Buf, Bytes};
+use bytes::{Buf, Bytes};
use structdiff::{Difference, StructDiff};
-use crate::{identity::Keystore, packet_content::{ClearRequest, ClearText, PacketContent}};
+use crate::{identity::Keystore, packet_content::PacketContent, request::ClearRequest, text::ClearText};
#[derive(PartialEq, Debug, Clone, Difference)]
#[difference(expose)]
@@ -148,10 +148,10 @@ impl Display for Packet {
let len = self.path.len();
match self.path.len() {
0 => f.write_fmt(format_args!(" [] | ")),
- 1 => f.write_fmt(format_args!(" [{:2x?}] | ", self.path[0])),
- 2 => f.write_fmt(format_args!(" [{:2x?}, {:2x?}] | ", self.path[0], self.path[1])),
- 3 => f.write_fmt(format_args!(" [{:2x?}, {:2x?}, {:2x?}] | ", self.path[0], self.path[1], self.path[2])),
- _ => f.write_fmt(format_args!(" [{:2x?}, {:2x?}, ... {:2x?}, {:2x?}] | ", self.path[0], self.path[1], self.path[len - 2], self.path[len -1])),
+ 1 => f.write_fmt(format_args!(" [{:02x?}] | ", self.path[0])),
+ 2 => f.write_fmt(format_args!(" [{:02x?}, {:02x?}] | ", self.path[0], self.path[1])),
+ 3 => f.write_fmt(format_args!(" [{:02x?}, {:02x?}, {:02x?}] | ", self.path[0], self.path[1], self.path[2])),
+ _ => f.write_fmt(format_args!(" [{:02x?}, {:02x?}, ... {:02x?}, {:02x?}] | ", self.path[0], self.path[1], self.path[len - 2], self.path[len -1])),
}?;
if self.incomplete {
@@ -318,6 +318,12 @@ mod tests {
assert!(RouteType::Direct == RouteType::from(0xFE));
assert!(RouteType::TransportDirect == RouteType::from(0x03));
assert!(RouteType::TransportDirect == RouteType::from(0xFF));
+
+ assert!(format!("{}", RouteType::TransportFlood) == "T-Flood ");
+ assert!(format!("{}", RouteType::Flood) == " Flood ");
+ assert!(format!("{}", RouteType::Direct) == " Direct");
+ assert!(format!("{}", RouteType::TransportDirect) == "T-Direct");
+ assert!(format!("{}", RouteType::Invalid) == "INVALID ");
}
#[test]
@@ -330,6 +336,12 @@ mod tests {
assert!(PayloadVersion::VersionThree == PayloadVersion::from(0xBF));
assert!(PayloadVersion::VersionFour == PayloadVersion::from(0xC0));
assert!(PayloadVersion::VersionFour == PayloadVersion::from(0xFF));
+
+ assert!(format!("{}", PayloadVersion::VersionOne) == "v1");
+ assert!(format!("{}", PayloadVersion::VersionTwo) == "v2");
+ assert!(format!("{}", PayloadVersion::VersionThree) == "v3");
+ assert!(format!("{}", PayloadVersion::VersionFour) == "v4");
+ assert!(format!("{}", PayloadVersion::Invalid) == "xx");
}
#[test]
@@ -375,7 +387,39 @@ mod tests {
content: PacketContent::Raw(Raw { bytes: Bytes::copy_from_slice(&[0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19])}),
incomplete: false,
};
+
let rhs_packet = Packet::from_str("3F0201040305060708090A10111213141516171819").unwrap();
+
+ let compare_string = "T-Direct | v1 | 102, 304 | [06, 07, ... 09, 0a] | | RAW | Raw { bytes: b\"\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\" }";
+ assert!(format!("{}", rhs_packet) == compare_string);
+
assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
}
+
+ #[test]
+ fn packet_display() {
+ let mut packet = Packet {
+ route_type: RouteType::Direct,
+ version: PayloadVersion::VersionOne,
+ path: vec![],
+ transport: [0, 1],
+ raw_content: Bytes::new(),
+ content: PacketContent::Invalid,
+ incomplete: false
+ };
+
+ assert!(format!("{}", packet) == " Direct | v1 | | [] | | INVALID | INVALID");
+
+ packet.path = vec![0x01];
+ assert!(format!("{}", packet) == " Direct | v1 | | [01] | | INVALID | INVALID");
+
+ packet.path = vec![0x01, 0x02];
+ assert!(format!("{}", packet) == " Direct | v1 | | [01, 02] | | INVALID | INVALID");
+
+ packet.path = vec![0x01, 0x02, 0xff];
+ assert!(format!("{}", packet) == " Direct | v1 | | [01, 02, ff] | | INVALID | INVALID");
+
+ packet.incomplete = true;
+ assert!(format!("{}", packet) == " Direct | v1 | | [01, 02, ff] | x | INVALID | INVALID");
+ }
} \ No newline at end of file
diff --git a/src/packet_content.rs b/src/packet_content.rs
index cdad0ff..d13c663 100644
--- a/src/packet_content.rs
+++ b/src/packet_content.rs
@@ -1,11 +1,7 @@
-use std::{fmt::{Debug, Display}, rc::Rc};
-
-use aes::cipher;
-use chrono::{DateTime, Local, Utc};
-use hex::encode;
-use tokio_util::bytes::{Buf, Bytes};
+use std::fmt::{Debug, Display};
+use bytes::{Buf, Bytes};
use structdiff::{Difference, StructDiff};
-use crate::{crypto::PublicKey, identity::Keystore};
+use crate::{ack::Ack, advert::Advert, anon_req::AnonReq, identity::Keystore, multipart::MultiPart, path::Path, request::Request, response::Response, text::{GroupData, GroupText, Text}, trace::Trace};
#[derive(PartialEq, Debug, Clone, Difference)]
#[difference(expose)]
@@ -113,11 +109,11 @@ impl From<u8> for NodeType {
#[derive(PartialEq, Debug, Clone, Difference)]
#[difference(expose)]
pub struct PeerToPeerCipher {
- destination: u8,
- source: u8,
- mac: u16,
- pub(crate) ciphertext: Bytes,
- pub(crate) cleartext: Option<Bytes>
+ pub destination: u8,
+ pub source: u8,
+ pub mac: u16,
+ pub ciphertext: Bytes,
+ pub cleartext: Option<Bytes>
}
impl From<Bytes> for PeerToPeerCipher {
@@ -163,621 +159,6 @@ impl PeerToPeerCipher {
}
#[derive(PartialEq, Debug, Clone)]
-pub struct Request {
- pub cipher: PeerToPeerCipher,
- pub cleartext: Option<ClearRequest>,
-}
-
-#[derive(PartialEq, Debug, Clone)]
-pub struct ClearRequest {
- pub timestamp: DateTime<Utc>,
- pub request_type: RequestType,
- pub request_data: Bytes
-}
-
-impl From<Bytes> for ClearRequest {
- fn from(value: Bytes) -> Self {
- let mut bytes = value;
-
- let mut clear_request = ClearRequest {
- timestamp: DateTime::from_timestamp(0, 0).unwrap(),
- request_type: RequestType::Invalid,
- request_data: Bytes::new()
- };
-
- // Just check for the whole fixed-size part at once
- if bytes.len() < 5 { return clear_request }
- if let Some(timestamp) = DateTime::from_timestamp(bytes.get_u32_le() as i64, 0) {
- clear_request.timestamp = timestamp;
- }
-
- clear_request.request_type = RequestType::from(bytes.get_u8());
-
- clear_request.request_data = bytes;
-
- clear_request
- }
-}
-
-#[derive(PartialEq, Debug, Clone)]
-pub enum RequestType {
- Stats,
- Keepalive,
- Telemetry,
- MinMaxAvg,
- ACL,
- Invalid
-}
-
-impl From<u8> for RequestType {
- fn from(value: u8) -> Self {
- todo!()
- }
-}
-
-impl From<Bytes> for Request {
- fn from(value: Bytes) -> Self {
- Request {
- cipher: PeerToPeerCipher::from(value),
- cleartext: None
- }
- }
-}
-
-impl Display for Request {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_fmt(format_args!("({:2x?}) -> ({:2x?}) MAC: {:4x?} ",
- self.cipher.source,
- self.cipher.destination,
- self.cipher.mac
- ))?;
-
- if let Some(cleartext) = &self.cipher.cleartext {
- f.write_str("TODO")
- // f.write_fmt(format_args!("{}", cleartext))
- } else {
- f.write_str("ENCRYPTED")
- }
- }
-}
-
-#[derive(PartialEq, Debug, Clone, Difference)]
-#[difference(expose)]
-pub struct Response {
- pub(crate) cipher: PeerToPeerCipher,
-}
-
-impl From<Bytes> for Response {
- fn from(value: Bytes) -> Self {
- Response {
- cipher: PeerToPeerCipher::from(value)
- }
- }
-}
-
-impl Display for Response {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_fmt(format_args!("({:2x?}) -> ({:2x?}) MAC: {:4x?} ",
- self.cipher.source,
- self.cipher.destination,
- self.cipher.mac
- ))?;
-
- if let Some(_cleartext) = &self.cipher.cleartext {
- f.write_fmt(format_args!(""))
- } else {
- f.write_str("ENCRYPTED")
- }
- }
-}
-
-#[derive(PartialEq, Debug, Clone)]
-pub struct Text {
- pub cipher: PeerToPeerCipher,
- pub cleartext: Option<ClearText>,
-}
-
-impl From<Bytes> for Text {
- fn from(value: Bytes) -> Self {
- Text {
- cipher: PeerToPeerCipher::from(value),
- cleartext: None
- }
- }
-}
-
-impl Display for Text {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_fmt(format_args!("({:2x?}) -> ({:2x?}) MAC: {:4x?} ",
- self.cipher.source,
- self.cipher.destination,
- self.cipher.mac
- ))?;
-
- if let Some(cleartext) = &self.cleartext {
- if let Some(sender) = &cleartext.sender {
- f.write_fmt(format_args!("at: {}, {} attempts, from {} to {}: {}",
- cleartext.timestamp,
- cleartext.attempts,
- sender,
- cleartext.crypto_recipient,
- cleartext.message.replace("\n", "\\n"))
- )
- } else {
- f.write_fmt(format_args!("at: {}, {} attempts, from {} to {}: {}",
- cleartext.timestamp,
- cleartext.attempts,
- "Unknown",
- cleartext.crypto_recipient,
- cleartext.message.replace("\n", "\\n"))
- )
- }
-
- } else {
- f.write_str("ENCRYPTED")
- }
- }
-}
-
-#[derive(PartialEq, Debug, Clone)]
-pub struct Ack {
- checksum: u32
-}
-
-impl From<Bytes> for Ack {
- fn from(value: Bytes) -> Self {
- let mut bytes = value;
- Ack { checksum: bytes.get_u32() }
- }
-}
-
-impl Display for Ack {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_fmt(format_args!("Checksum: {:4x?}", self.checksum))
- }
-}
-
-#[derive(PartialEq, Debug, Clone, Difference)]
-#[difference(expose)]
-pub struct Advert {
- pub public_key: PublicKey,
- pub timestamp: DateTime<Utc>,
- pub signature: [u8; 64],
-
- pub node_type: NodeType,
- pub latitude: Option<f32>,
- pub longitude: Option<f32>,
- pub feature1: Option<u16>,
- pub feature2: Option<u16>,
- pub name: String
-}
-
-impl From<Bytes> for Advert {
- fn from(value: Bytes) -> Self {
- let mut bytes = value;
-
- let mut advert = Advert {
- public_key: PublicKey::default(),
- timestamp: Local::now().into(),
- signature: [0_u8; 64],
-
- node_type: NodeType::Invalid,
- latitude: None,
- longitude: None,
- feature1: None,
- feature2: None,
- name: "".to_string(),
- };
-
- if bytes.len() < 32 { return advert }
- if let Ok(key) = PublicKey::try_from(bytes.split_to(32)) {
- advert.public_key = key;
- } else {
- return advert
- }
-
- if bytes.len() < 4 { return advert }
- if let Some(time) = DateTime::from_timestamp(bytes.get_u32_le() as i64, 0) {
- advert.timestamp = time;
- }
-
- if bytes.len() < 64 { return advert }
- _ = bytes.try_copy_to_slice(&mut advert.signature);
-
- if bytes.is_empty() { return advert }
- let flags = bytes.get_u8();
- advert.node_type = NodeType::from(flags);
-
- if (flags & 0x10) != 0 {
- // The location is 8 bytes (4 each for lat and lon)
- if bytes.len() < 8 { return advert }
-
- advert.latitude = Some(bytes.get_i32_le() as f32 / 1_000_000.0);
- advert.longitude = Some(bytes.get_i32_le() as f32 / 1_000_000.0);
- }
-
- if (flags & 0x20) != 0 {
- // Feature 1 is 2 bytes when it's included
- if bytes.len() < 2 { return advert }
- advert.feature1 = Some(bytes.get_u16_le());
- }
-
- if (flags & 0x40) != 0 {
- // Feature 2 is the same
- if bytes.len() < 2 { return advert }
- advert.feature2 = Some(bytes.get_u16_le());
- }
-
- // Lastly the name... The flag is a little
- // irrelevant because the only different is
- // whether the name is omitted or just empty.
- // I'm not going to make the distinction
-
- // The string is assumed to be utf8, but if that
- // fails to parse correctly, try to make it lossy
- // which will insert U+FFFD REPLACEMENT CHARACTER (�)
- if let Ok(string) = String::from_utf8(bytes.to_vec()) {
- advert.name = string
- } else {
- advert.name = String::from_utf8_lossy(&bytes).to_string();
- }
-
- advert
- }
-}
-
-impl Display for Advert {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.node_type.fmt(f)?;
- f.write_str(" \"")?;
- f.write_str(&self.name)?;
- f.write_fmt(format_args!("\" ({:2x?}) at: ", self.public_key.hash_prefix()))?;
- std::fmt::Display::fmt(&self.timestamp, f)?;
-
- match (self.latitude, self.longitude) {
- (Some(lat), Some(lon)) => {
- f.write_fmt(format_args!(" location: {}, {}", lat, lon))?;
- },
- _ => {},
- }
-
- Ok(())
- }
-}
-
-#[derive(PartialEq, Debug, Clone, Difference)]
-#[difference(expose)]
-pub struct GroupText {
- hash: u8,
- mac: u16,
- ciphertext: Bytes,
-
- cleartext: Option<ClearText>,
-
- incomplete: bool
-}
-
-impl From<Bytes> for GroupText {
- fn from(value: Bytes) -> Self {
- let mut bytes = value;
-
- let mut group_text = GroupText {
- hash: 0x00,
- mac: 0x0000,
- ciphertext: Bytes::new(),
- cleartext: None,
- incomplete: true,
- };
-
- // Just check for the whole fixed-size part at once
- if bytes.len() < 3 { return group_text }
- group_text.hash = bytes.get_u8();
- group_text.mac = bytes.get_u16();
-
- group_text.ciphertext = bytes;
- group_text.incomplete = false;
- group_text
- }
-}
-
-impl GroupText {
- pub fn try_decrypt(&mut self, keysore: &Keystore) -> bool {
- let decrypt_result = keysore.decrypt_and_id_group(
- self.hash,
- self.mac,
- &self.ciphertext
- );
-
- if let Some((cleartext, group)) = decrypt_result {
- let mut cleartext = ClearText::from(cleartext);
- cleartext.crypto_recipient = group.name.clone();
- self.cleartext = Some(cleartext);
- true
- } else {
- false
- }
- }
-}
-
-impl Display for GroupText {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- if let Some(cleartext) = &self.cleartext {
- f.write_fmt(format_args!("({:2x?}) Mac: {:4x?} Group {}: {}",
- self.hash,
- self.mac,
- cleartext.crypto_recipient,
- cleartext.message.replace("\n", "\\n"))
- )
- } else {
- f.write_fmt(format_args!("({:2x?}) Mac: {:4x?} ENCRYPTED", self.hash, self.mac))
- }
- }
-}
-
-#[derive(PartialEq, Debug, Clone)]
-pub enum MessageType {
- Plain,
- CLI,
- SignedPlain,
- Invalid,
- Incomplete,
-}
-
-impl From<u8> for MessageType {
- fn from(value: u8) -> Self {
- match (value & 0xFC) >> 3 {
- 0x00 => MessageType::Plain,
- 0x01 => MessageType::CLI,
- 0x02 => MessageType::SignedPlain,
- _ => MessageType::Invalid
- }
- }
-}
-
-#[derive(PartialEq, Debug, Clone, Difference)]
-#[difference(expose)]
-pub struct ClearText {
- pub timestamp: DateTime<Utc>,
- pub message_type: MessageType,
- pub attempts: u8,
- pub sender_hash: u32,
- pub sender: Option<String>,
- pub message: String,
-
- pub crypto_recipient: Rc<String>
-}
-
-impl From<Bytes> for ClearText {
- fn from(value: Bytes) -> Self {
- let mut bytes = value;
-
- let mut clear_text = ClearText {
- timestamp: DateTime::from_timestamp(0, 0).unwrap(),
- message_type: MessageType::Incomplete,
- attempts: 0,
- sender_hash: 0,
- sender: None,
- message: "".to_string(),
- crypto_recipient: Rc::new("".to_string()),
- };
-
- // Just check for the whole fixed-size part at once
- if bytes.len() < 5 { return clear_text }
- if let Some(timestamp) = DateTime::from_timestamp(bytes.get_u32_le() as i64, 0) {
- clear_text.timestamp = timestamp;
- }
-
- let flags = bytes.get_u8();
- clear_text.message_type = MessageType::from(flags);
- clear_text.attempts = flags & 0x03;
-
- if clear_text.message_type == MessageType::SignedPlain &&
- bytes.len() > 4 {
- clear_text.sender_hash = bytes.get_u32();
- }
-
- let raw_message = String::from_utf8_lossy(&bytes).to_owned();
- let trimmed_message: Vec<&str> = raw_message.splitn(2, "\0").collect();
-
- // Apparently the sender is just whatever is before the first :
- let splits: Vec<&str> = trimmed_message[0].splitn(2, ": ").collect();
- if splits.len() > 1 {
- clear_text.sender = Some(splits[0].to_owned());
- clear_text.message = splits[1].to_owned();
- } else {
- clear_text.message = splits[0].to_owned();
- }
-
- clear_text
- }
-}
-
-#[derive(PartialEq, Debug, Clone)]
-pub struct GroupData {
- payload: Bytes
-}
-
-impl From<Bytes> for GroupData {
- fn from(value: Bytes) -> Self {
- GroupData { payload: value }
- }
-}
-
-impl Display for GroupData {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_fmt(format_args!("Payload: {}", encode(&self.payload)))
- }
-}
-
-#[derive(PartialEq, Debug, Clone, Difference)]
-#[difference(expose)]
-pub struct AnonReq {
- pub dest: u8,
- pub public_key: PublicKey,
- pub mac: u16,
- pub ciphertext: Bytes,
- pub request: Option<ClearAnonRequest>,
- incomplete: bool
-}
-
-impl From<Bytes> for AnonReq {
- fn from(value: Bytes) -> Self {
- let mut bytes = value;
-
- let mut anon_req = AnonReq {
- dest: 0x00,
- public_key: PublicKey::default(),
- mac: 0x0000,
- ciphertext: Bytes::new(),
- request: None,
- incomplete: false,
- };
-
- if bytes.is_empty() { return anon_req; }
- anon_req.dest = bytes.get_u8();
-
- if bytes.len() < 32 { return anon_req; }
- if let Ok(pub_key) = PublicKey::try_from(bytes.split_to(32)) {
- anon_req.public_key = pub_key;
- }
-
- if bytes.len() < 2 { return anon_req; }
- anon_req.mac = bytes.get_u16();
-
- anon_req.ciphertext = bytes;
- anon_req.incomplete = false;
- anon_req
- }
-}
-
-impl Display for AnonReq {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_fmt(format_args!("({:2x?}) -> ({:2x?}) MAC: {:4x?} ", self.public_key.hash_prefix(), self.dest, self.mac))?;
-
- if let Some(cleartext) = &self.request {
- f.write_fmt(format_args!("at: {} password: \"{}\"", cleartext.timestamp, cleartext.password))
- } else {
- f.write_str("ENCRYPTED")
- }
- }
-}
-
-#[derive(PartialEq, Debug, Clone, Difference)]
-#[difference(expose)]
-pub struct ClearAnonRequest {
- timestamp: DateTime<Utc>,
- sync_timestamp: Option<DateTime<Utc>>,
- password: String
-}
-
-impl From<Bytes> for ClearAnonRequest {
- fn from(value: Bytes) -> Self {
- let mut bytes = value;
-
- let mut anon_req = ClearAnonRequest {
- timestamp: DateTime::from_timestamp(0, 0).unwrap(),
- sync_timestamp: None,
- password: "".to_string(),
- };
-
- // Just check for the whole fixed-size part at once
- if bytes.len() < 4 { return anon_req }
- if let Some(timestamp) = DateTime::from_timestamp(bytes.get_u32() as i64, 0) {
- anon_req.timestamp = timestamp;
- }
-
- if bytes.len() < 4 { return anon_req; }
- if let Some(timestamp) = DateTime::from_timestamp(bytes.get_u32() as i64, 0) {
- anon_req.sync_timestamp = Some(timestamp);
- }
-
- anon_req.password = String::from_utf8_lossy(&bytes).to_string();
- anon_req
- }
-}
-
-#[derive(PartialEq, Debug, Clone)]
-pub struct Path {
- pub(crate) cipher: PeerToPeerCipher,
-}
-
-impl From<Bytes> for Path {
- fn from(value: Bytes) -> Self {
- Path {
- cipher: PeerToPeerCipher::from(value)
- }
- }
-}
-
-impl Display for Path {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_fmt(format_args!("({:2x?}) -> ({:2x?}) MAC: {:4x?} ", self.cipher.source, self.cipher.destination, self.cipher.mac))
- }
-}
-
-#[derive(PartialEq, Debug, Clone)]
-pub struct Trace {
- tag: u32,
- auth: u32,
- flags: u8,
- pub(crate) path_snr: Vec<f32>,
- invalid: bool,
-
- pub(crate) temp_path: Vec<u8>
-}
-
-impl From<Bytes> for Trace {
- fn from(value: Bytes) -> Self {
- let mut bytes = value;
-
- let mut trace = Trace {
- tag: 0,
- auth: 0,
- flags: 0,
- path_snr: vec![],
- invalid: true,
- temp_path: vec![]
- };
-
- if bytes.len() < 10 { return trace; }
-
- trace.tag = bytes.get_u32();
- trace.auth = bytes.get_u32();
- trace.flags = bytes.get_u8();
- trace.temp_path = bytes.iter().copied().collect();
- trace.invalid = false;
- trace
- }
-}
-
-impl Display for Trace {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- for (snr, hash) in self.path_snr.iter().zip(self.temp_path.clone()) {
- f.write_fmt(format_args!("({:2x?}): {}dB ", hash, snr))?;
- }
-
- Ok(())
- }
-}
-
-#[derive(PartialEq, Debug, Clone)]
-pub struct MultiPart {
- payload: Bytes
-}
-
-impl From<Bytes> for MultiPart {
- fn from(value: Bytes) -> Self {
- MultiPart { payload: value }
- }
-}
-
-impl Display for MultiPart {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_fmt(format_args!("Multipart isn't defined."))
- }
-}
-
-#[derive(PartialEq, Debug, Clone)]
pub struct Raw {
pub(crate) bytes: Bytes
}
@@ -790,17 +171,6 @@ impl Display for Raw {
#[cfg(test)]
mod tests {
- use std::rc::Rc;
- use std::str::FromStr;
-
- use chrono::DateTime;
- use hex::decode;
-
- use crate::identity::Group;
- use crate::identity::KeystoreInput;
- use crate::packet::*;
- use crate::crypto::*;
-
use super::*;
#[test]
@@ -817,354 +187,4 @@ mod tests {
assert!(NodeType::Sensor == NodeType::from(0xFC));
assert!(NodeType::Invalid == NodeType::from(0x05));
}
-
- #[test]
- fn advert_with_lat_lon() {
- // Real sample packet from the air
- let sample = "110c015f7e9b60661da0f2671512460728508c17ef336412a223144d3a623215162682045c44fef7241af0161923d3af0769d2d976b687a506dd5325ef526bf3eb52ae687277fcbde9969a5b0087e0eb0f7c1760a50c6a88bec13cc30a2a9b681d713166515e3bbc2bc27f20c0e4d7b67e08910c29dc02c468b8f8484f574c";
-
- let mut signature_slice = [0 as u8; 64];
- hex::decode_to_slice("d2d976b687a506dd5325ef526bf3eb52ae687277fcbde9969a5b0087e0eb0f7c1760a50c6a88bec13cc30a2a9b681d713166515e3bbc2bc27f20c0e4d7b67e08", &mut signature_slice).unwrap();
-
- let lhs_packet = Packet {
- route_type: RouteType::Flood,
- version: PayloadVersion::VersionOne,
- path: vec![0x01, 0x5f, 0x7e, 0x9b, 0x60, 0x66, 0x1d, 0xa0, 0xf2, 0x67, 0x15, 0x12],
- transport: [0x00, 0x00],
- raw_content: Bytes::copy_from_slice(&decode("460728508c17ef336412a223144d3a623215162682045c44fef7241af0161923d3af0769d2d976b687a506dd5325ef526bf3eb52ae687277fcbde9969a5b0087e0eb0f7c1760a50c6a88bec13cc30a2a9b681d713166515e3bbc2bc27f20c0e4d7b67e08910c29dc02c468b8f8484f574c").unwrap()),
- content: PacketContent::Advert(Advert {
- public_key: PublicKey::from_str("460728508c17ef336412a223144d3a623215162682045c44fef7241af0161923").unwrap(),
- timestamp: DateTime::from_timestamp(1762111443, 0).unwrap(),
- signature: signature_slice,
- node_type: NodeType::Chat,
- latitude: Some(47.98286),
- longitude: Some(-122.132286),
- feature1: None,
- feature2: None,
- name: "HOWL".to_owned(),
- }),
- incomplete: false,
- };
- let rhs_packet = Packet::from_str(sample).unwrap();
-
- assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
- }
-
- #[test]
- fn advert() {
- // Another on-air sample
- let sample = "12007890b8573a6ba4a05b173d6ccfdfa73ac8ec4a12bf3c745ace636e1d191e132a4eb407695601f50907999735f3699a3c73ace2d8acc385ed209606abef6b914a346fbb88648c540ae794586b4d54eb08b473c8571a00c6b8d3dbcc77699afa169811fa098168707578373335";
-
- let mut signature_slice = [0 as u8; 64];
- hex::decode_to_slice("5601f50907999735f3699a3c73ace2d8acc385ed209606abef6b914a346fbb88648c540ae794586b4d54eb08b473c8571a00c6b8d3dbcc77699afa169811fa09", &mut signature_slice).unwrap();
-
- let lhs_packet = Packet {
- route_type: RouteType::Direct,
- version: PayloadVersion::VersionOne,
- path: vec![],
- transport: [0x00, 0x00],
- raw_content: Bytes::copy_from_slice(&decode("7890b8573a6ba4a05b173d6ccfdfa73ac8ec4a12bf3c745ace636e1d191e132a4eb407695601f50907999735f3699a3c73ace2d8acc385ed209606abef6b914a346fbb88648c540ae794586b4d54eb08b473c8571a00c6b8d3dbcc77699afa169811fa098168707578373335").unwrap()),
- content: PacketContent::Advert(Advert {
- public_key: PublicKey::from_str("7890b8573a6ba4a05b173d6ccfdfa73ac8ec4a12bf3c745ace636e1d191e132a").unwrap(),
- timestamp: DateTime::from_timestamp(1762112590, 0).unwrap(),
- signature: signature_slice,
- node_type: NodeType::Chat,
- latitude: None,
- longitude: None,
- feature1: None,
- feature2: None,
- name: "hpux735".to_owned(),
- }),
- incomplete: false,
- };
-
- let rhs_packet = Packet::from_str(sample).unwrap();
- assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
- }
-
- #[test]
- fn response() {
- let sample = "06003412b9000cf1641739f7e4d49bff88bf5695b304111b15277de3a6031b9af3a2b6371cf75615ffefe7dcc0bbea2856c4e798a72d5b989d6de1aa646c1e2eef4cf13e6f92";
-
- let lhs_packet = Packet {
- route_type: RouteType::Direct,
- version: PayloadVersion::VersionOne,
- path: vec![],
- transport: [0, 0],
- raw_content: Bytes::copy_from_slice(&decode("3412b9000cf1641739f7e4d49bff88bf5695b304111b15277de3a6031b9af3a2b6371cf75615ffefe7dcc0bbea2856c4e798a72d5b989d6de1aa646c1e2eef4cf13e6f92").unwrap()),
- content: PacketContent::Response(Response {
- cipher: PeerToPeerCipher {
- destination: 0x34,
- source: 0x12,
- mac: 0xb900,
- ciphertext: Bytes::copy_from_slice(&decode("0cf1641739f7e4d49bff88bf5695b304111b15277de3a6031b9af3a2b6371cf75615ffefe7dcc0bbea2856c4e798a72d5b989d6de1aa646c1e2eef4cf13e6f92").unwrap()),
- cleartext: None
- }}),
- incomplete: false,
- };
-
- let rhs_packet = Packet::from_str(sample).unwrap();
- assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
- }
-
- #[test]
- fn request() {
- let sample = "020012341d87ccaac89563cbb39d2333b725e407a1a6";
-
- let lhs_packet = Packet {
- route_type: RouteType::Direct,
- version: PayloadVersion::VersionOne,
- path: vec![],
- transport: [0, 0],
- raw_content: Bytes::copy_from_slice(&decode("12341d87ccaac89563cbb39d2333b725e407a1a6").unwrap()),
- content: PacketContent::Request(Request {
- cipher: PeerToPeerCipher {
- destination: 0x12,
- source: 0x34,
- mac: 0x1d87,
- ciphertext: Bytes::copy_from_slice(&decode("ccaac89563cbb39d2333b725e407a1a6").unwrap()),
- cleartext: None
- },
- cleartext: None, }),
- incomplete: false,
- };
-
- let rhs_packet = Packet::from_str(sample).unwrap();
- assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
- }
-
- #[test]
- fn text() {
- let sample = "0a001234e91c8eb2b815e0eccf6781a3ff1820d0fb130fcfc87b914244fae227d4ad4c752fb9";
-
- let lhs_packet = Packet {
- route_type: RouteType::Direct,
- version: PayloadVersion::VersionOne,
- path: vec![],
- transport: [0, 0],
- raw_content: Bytes::copy_from_slice(&decode("1234e91c8eb2b815e0eccf6781a3ff1820d0fb130fcfc87b914244fae227d4ad4c752fb9").unwrap()),
- content: PacketContent::Text(Text {
- cipher: PeerToPeerCipher {
- destination: 0x12,
- source: 0x34,
- mac: 0xe91c,
- ciphertext: Bytes::copy_from_slice(&decode("8eb2b815e0eccf6781a3ff1820d0fb130fcfc87b914244fae227d4ad4c752fb9").unwrap()),
- cleartext: None
- },
- cleartext: None,
- }),
- incomplete: false,
- };
-
- let rhs_packet = Packet::from_str(sample).unwrap();
- assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
- }
-
- #[test]
- fn ack() {
- let sample = "0D0E78B5561B03CD70DA326066B8A0CF24F3214D";
-
- let lhs_packet = Packet {
- route_type: RouteType::Flood,
- version: PayloadVersion::VersionOne,
- path: vec![0x78, 0xB5, 0x56, 0x1B, 0x03, 0xCD, 0x70, 0xDA, 0x32, 0x60, 0x66, 0xB8, 0xA0, 0xCF],
- transport: [0, 0],
- raw_content: Bytes::copy_from_slice(&decode("24F3214D").unwrap()),
- content: PacketContent::Ack(Ack {
- checksum: 0x24F3214D
- }),
- incomplete: false
- };
-
- let rhs_packet = Packet::from_str(sample).unwrap();
- assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
- }
-
- #[test]
- fn trace() {
- let sample = "26032CE0F149EB5B240000000000ACA079";
-
- let lhs_packet = Packet {
- route_type: RouteType::Direct,
- version: PayloadVersion::VersionOne,
- path: vec![0xAC, 0xA0, 0x79],
- transport: [0, 0],
- raw_content: Bytes::copy_from_slice(&decode("49EB5B240000000000ACA079").unwrap()),
- content: PacketContent::Trace(Trace {
- tag: 0x49EB5B24,
- auth: 0x00000000,
- flags: 0x00,
- path_snr: vec![11.0, -8.0, -3.75],
- invalid: false,
- temp_path: vec![]
- }
- ),
- incomplete: false
- };
-
- let rhs_packet = Packet::from_str(sample).unwrap();
- assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
- }
-
- #[test]
- fn group_text() {
- let sample = "15107B2CF1A31C03E8AACD7E6066B8A0ACB71176B87DDF8FE67B33E7A63036E015311EE39232F094FAAD13C4442947947DD098886BC677DE2B6F456149D0A1B05C72F0EECC20DC07EDF163D9D63EBC9BC29A7C79AF";
-
- let lhs_packet = Packet {
- route_type: RouteType::Flood,
- version: PayloadVersion::VersionOne,
- path: vec![0x7B, 0x2C, 0xF1, 0xA3, 0x1C, 0x03, 0xE8, 0xAA, 0xCD, 0x7E, 0x60, 0x66, 0xB8, 0xA0, 0xAC, 0xB7],
- transport: [0, 0],
- raw_content: Bytes::copy_from_slice(&decode("1176B87DDF8FE67B33E7A63036E015311EE39232F094FAAD13C4442947947DD098886BC677DE2B6F456149D0A1B05C72F0EECC20DC07EDF163D9D63EBC9BC29A7C79AF").unwrap()),
- content: PacketContent::GroupText(GroupText {
- hash: 0x11,
- mac: 0x76B8,
- ciphertext: Bytes::copy_from_slice(&decode("7DDF8FE67B33E7A63036E015311EE39232F094FAAD13C4442947947DD098886BC677DE2B6F456149D0A1B05C72F0EECC20DC07EDF163D9D63EBC9BC29A7C79AF").unwrap()),
- cleartext: None, // "Just need some more high repeaters".to_owned()
- incomplete: false
- }),
- incomplete: false
- };
-
- let rhs_packet = Packet::from_str(sample).unwrap();
- assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
- }
-
- #[test]
- fn group_text_decrypted() {
- let sample = "150011C3C1354D619BAE9590E4D177DB7EEAF982F5BDCF78005D75157D9535FA90178F785D";
-
- let lhs_packet = Packet {
- route_type: RouteType::Flood,
- version: PayloadVersion::VersionOne,
- path: vec![],
- transport: [0, 0],
- raw_content: Bytes::copy_from_slice(&decode("11C3C1354D619BAE9590E4D177DB7EEAF982F5BDCF78005D75157D9535FA90178F785D").unwrap()),
- content: PacketContent::GroupText(GroupText {
- hash: 0x11,
- mac: 0xC3C1,
- ciphertext: Bytes::copy_from_slice(&decode("354D619BAE9590E4D177DB7EEAF982F5BDCF78005D75157D9535FA90178F785D").unwrap()),
- cleartext: Some(ClearText {
- sender: Some("🌲 Tree".to_owned()),
- message: "☁️".to_owned(),
- timestamp: DateTime::from_timestamp_secs(1758484279).unwrap(),
- message_type: MessageType::Plain,
- attempts: 0,
- sender_hash: 0,
- crypto_recipient: Rc::new("Public".to_owned()),
- }),
- incomplete: false
- }),
- incomplete: false
- };
-
- let mut rhs_packet = Packet::from_str(sample).unwrap();
-
- let keystore = KeystoreInput {
- identities: vec![],
- contacts: vec![],
- groups: vec![
- Group {
- name: Rc::new("Public".to_owned()),
- secret: SharedSecret::from_str("8b3387e9c5cdea6ac9e5edbaa115cd72").unwrap()
- }
- ]
- }.compile();
-
- _ = rhs_packet.try_decrypt(&keystore);
-
- assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
- }
-
- #[test]
- fn multipart() {
- let sample = "2A0013E8C59624";
-
- let lhs_packet = Packet {
- route_type: RouteType::Direct,
- version: PayloadVersion::VersionOne,
- path: vec![],
- transport: [0, 0],
- raw_content: Bytes::copy_from_slice(&decode("13E8C59624").unwrap()),
- content: PacketContent::Multipart(MultiPart {
- payload: Bytes::copy_from_slice(&decode("13E8C59624").unwrap())
- }),
- incomplete: false
- };
-
- let rhs_packet = Packet::from_str(sample).unwrap();
- assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
- }
-
- #[test]
- fn group_data() {
- let sample = "18079DBB163F3C88707DF4C430C8A06A587CF7E827641C6521A5DE85581C8800793AF4A5497196CB5F24B92A33A9C8AA3BAE4F8C94E8E464849BCBF6333509C3941C47B7ABF85ECFD2FF06DA1A39575D155941F152F63300D2C31B5FAFBDA79637";
-
- let lhs_packet = Packet {
- route_type: RouteType::TransportFlood,
- version: PayloadVersion::VersionOne,
- path: vec![0x3C, 0x88, 0x70, 0x7D, 0xF4, 0xC4, 0x30, 0xC8, 0xA0, 0x6A, 0x58, 0x7C, 0xF7, 0xE8, 0x27, 0x64, 0x1C, 0x65, 0x21, 0xA5, 0xDE, 0x85, 0x58, 0x1C, 0x88, 0x00, 0x79, 0x3A, 0xF4, 0xA5, 0x49, 0x71, 0x96, 0xCB, 0x5F, 0x24, 0xB9, 0x2A, 0x33, 0xA9, 0xC8, 0xAA, 0x3B, 0xAE, 0x4F, 0x8C, 0x94, 0xE8, 0xE4, 0x64, 0x84, 0x9B, 0xCB, 0xF6, 0x33, 0x35, 0x09, 0xC3, 0x94, 0x1C, 0x47, 0xB7, 0xAB],
- transport: [0x9d07, 0x16bb],
- raw_content: Bytes::copy_from_slice(&decode("F85ECFD2FF06DA1A39575D155941F152F63300D2C31B5FAFBDA79637").unwrap()),
- content: PacketContent::GroupData(GroupData {
- payload: Bytes::copy_from_slice(&decode("F85ECFD2FF06DA1A39575D155941F152F63300D2C31B5FAFBDA79637").unwrap())
- }),
- incomplete: false
- };
-
- let rhs_packet = Packet::from_str(sample).unwrap();
- assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
- }
-
- #[test]
- fn anon_req() {
- let sample = "1d001234569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8fd2db15a7138098557cc291928b4358fa7522ddd41d35c99fb78f0def2b3e673d73d2";
-
- let lhs_packet = Packet {
- route_type: RouteType::Flood,
- version: PayloadVersion::VersionOne,
- path: vec![],
- transport: [0, 0],
- raw_content: Bytes::copy_from_slice(&decode("1234569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8fd2db15a7138098557cc291928b4358fa7522ddd41d35c99fb78f0def2b3e673d73d2").unwrap()),
- content: PacketContent::AnonReq(AnonReq {
- dest: 0x12,
- public_key: PublicKey::try_from(Bytes::copy_from_slice(&decode("34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8f").unwrap())).unwrap(),
- mac: 0xd2db,
- ciphertext: Bytes::copy_from_slice(&decode("15a7138098557cc291928b4358fa7522ddd41d35c99fb78f0def2b3e673d73d2").unwrap()),
- incomplete: false,
- request: None
- }),
- incomplete: false
- };
-
- let rhs_packet = Packet::from_str(sample).unwrap();
- assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
- }
-
- #[test]
- fn path() {
- let sample = "2107BA03127F7EA9221351768DD2DF32E1D02F5851379F5AFCC667AB273442FAB2943673F26DDBEB9595027474";
-
- let lhs_packet = Packet {
- route_type: RouteType::Flood,
- version: PayloadVersion::VersionOne,
- path: vec![0xBA, 0x03, 0x12, 0x7F, 0x7E, 0xA9, 0x22],
- transport: [0, 0],
- raw_content: Bytes::copy_from_slice(&decode("1351768DD2DF32E1D02F5851379F5AFCC667AB273442FAB2943673F26DDBEB9595027474").unwrap()),
- content: PacketContent::Path(Path {
- cipher: PeerToPeerCipher {
- destination: 0x13,
- source: 0x51,
- mac: 0x768D,
- ciphertext: Bytes::copy_from_slice(&decode("D2DF32E1D02F5851379F5AFCC667AB273442FAB2943673F26DDBEB9595027474").unwrap()),
- cleartext: None
- }
- }),
- incomplete: false
- };
-
- let rhs_packet = Packet::from_str(sample).unwrap();
- assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
- }
-
} \ No newline at end of file
diff --git a/src/path.rs b/src/path.rs
new file mode 100644
index 0000000..11eaa5a
--- /dev/null
+++ b/src/path.rs
@@ -0,0 +1,61 @@
+use std::fmt::{Debug, Display};
+use bytes::Bytes;
+use structdiff::{Difference, StructDiff};
+use crate::packet_content::PeerToPeerCipher;
+
+
+#[derive(PartialEq, Debug, Clone, Difference)]
+#[difference(expose)]
+pub struct Path {
+ pub cipher: PeerToPeerCipher,
+}
+
+impl From<Bytes> for Path {
+ fn from(value: Bytes) -> Self {
+ Path {
+ cipher: PeerToPeerCipher::from(value)
+ }
+ }
+}
+
+impl Display for Path {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_fmt(format_args!("({:2x?}) -> ({:2x?}) MAC: {:4x?} ", self.cipher.source, self.cipher.destination, self.cipher.mac))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::str::FromStr;
+ use hex::decode;
+ use crate::{packet::*, packet_content::{PacketContent, PeerToPeerCipher}};
+
+ use super::*;
+
+
+ #[test]
+ fn path() {
+ let sample = "2107BA03127F7EA9221351768DD2DF32E1D02F5851379F5AFCC667AB273442FAB2943673F26DDBEB9595027474";
+
+ let lhs_packet = Packet {
+ route_type: RouteType::Flood,
+ version: PayloadVersion::VersionOne,
+ path: vec![0xBA, 0x03, 0x12, 0x7F, 0x7E, 0xA9, 0x22],
+ transport: [0, 0],
+ raw_content: Bytes::copy_from_slice(&decode("1351768DD2DF32E1D02F5851379F5AFCC667AB273442FAB2943673F26DDBEB9595027474").unwrap()),
+ content: PacketContent::Path(Path {
+ cipher: PeerToPeerCipher {
+ destination: 0x13,
+ source: 0x51,
+ mac: 0x768D,
+ ciphertext: Bytes::copy_from_slice(&decode("D2DF32E1D02F5851379F5AFCC667AB273442FAB2943673F26DDBEB9595027474").unwrap()),
+ cleartext: None
+ }
+ }),
+ incomplete: false
+ };
+
+ let rhs_packet = Packet::from_str(sample).unwrap();
+ assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
+ }
+} \ No newline at end of file
diff --git a/src/request.rs b/src/request.rs
new file mode 100644
index 0000000..7ce126c
--- /dev/null
+++ b/src/request.rs
@@ -0,0 +1,202 @@
+use std::fmt::Display;
+
+use chrono::{DateTime, Utc};
+use structdiff::{Difference, StructDiff};
+use crate::packet_content::PeerToPeerCipher;
+use bytes::{Buf, Bytes};
+
+#[derive(PartialEq, Debug, Clone, Difference)]
+pub struct Request {
+ pub cipher: PeerToPeerCipher,
+ pub cleartext: Option<ClearRequest>,
+}
+
+#[derive(PartialEq, Debug, Clone, Difference)]
+#[difference(expose)]
+pub struct ClearRequest {
+ pub timestamp: DateTime<Utc>,
+ pub request_type: RequestType,
+ pub request_data: Bytes
+}
+
+impl From<Bytes> for ClearRequest {
+ fn from(value: Bytes) -> Self {
+ let mut bytes = value;
+
+ let mut clear_request = ClearRequest {
+ timestamp: DateTime::from_timestamp(0, 0).unwrap(),
+ request_type: RequestType::Invalid,
+ request_data: Bytes::new()
+ };
+
+ // Just check for the whole fixed-size part at once
+ if bytes.len() < 5 { return clear_request }
+ if let Some(timestamp) = DateTime::from_timestamp(bytes.get_u32_le() as i64, 0) {
+ clear_request.timestamp = timestamp;
+ }
+
+ clear_request.request_type = RequestType::from(bytes.get_u8());
+
+ clear_request.request_data = bytes;
+
+ clear_request
+ }
+}
+
+#[derive(PartialEq, Debug, Clone)]
+pub enum RequestType {
+ Stats,
+ Keepalive,
+ Telemetry,
+ MinMaxAvg,
+ ACL,
+ Invalid
+}
+
+impl From<u8> for RequestType {
+ fn from(value: u8) -> Self {
+ match value {
+ 0x01 => RequestType::Stats,
+ 0x02 => RequestType::Keepalive,
+ 0x03 => RequestType::Telemetry,
+ 0x04 => RequestType::MinMaxAvg,
+ 0x05 => RequestType::ACL,
+ _ => RequestType::Invalid
+ }
+ }
+}
+
+impl Display for RequestType {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ RequestType::Stats => f.write_str("STATS"),
+ RequestType::Keepalive => f.write_str("KEEP ALIVE"),
+ RequestType::Telemetry => f.write_str("TELEMETRY"),
+ RequestType::MinMaxAvg => f.write_str("MIN/MAX/AVG"),
+ RequestType::ACL => f.write_str("ACL"),
+ RequestType::Invalid => f.write_str("INVALID"),
+ }
+ }
+}
+
+impl From<Bytes> for Request {
+ fn from(value: Bytes) -> Self {
+ Request {
+ cipher: PeerToPeerCipher::from(value),
+ cleartext: None
+ }
+ }
+}
+
+impl Display for Request {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_fmt(format_args!("({:2x?}) -> ({:2x?}) MAC: {:4x?} ",
+ self.cipher.source,
+ self.cipher.destination,
+ self.cipher.mac
+ ))?;
+
+ if let Some(cleartext) = &self.cleartext {
+ f.write_fmt(format_args!("at: {} ", cleartext.timestamp))?;
+ f.write_fmt(format_args!("{}", &cleartext.request_type))
+ } else {
+ f.write_str("ENCRYPTED")
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::str::FromStr;
+
+ use chrono::DateTime;
+ use hex::decode;
+ use bytes::Bytes;
+ use crate::{crypto::{PrivateKey, PublicKey}, identity::{Contact, Identity, KeystoreInput}, packet::*, packet_content::{PacketContent, PeerToPeerCipher}, request::{ClearRequest, Request, RequestType}};
+
+ #[test]
+ fn request_type() {
+ assert!(RequestType::from(0x01) == RequestType::Stats);
+ assert!(RequestType::from(0x02) == RequestType::Keepalive);
+ assert!(RequestType::from(0x03) == RequestType::Telemetry);
+ assert!(RequestType::from(0x04) == RequestType::MinMaxAvg);
+ assert!(RequestType::from(0x05) == RequestType::ACL);
+ assert!(RequestType::from(0x06) == RequestType::Invalid);
+ assert!(RequestType::from(0xFF) == RequestType::Invalid);
+
+
+ assert!(format!("{}", RequestType::Stats) == "STATS");
+ assert!(format!("{}", RequestType::Keepalive) == "KEEP ALIVE");
+ assert!(format!("{}", RequestType::Telemetry) == "TELEMETRY");
+ assert!(format!("{}", RequestType::MinMaxAvg) == "MIN/MAX/AVG");
+ assert!(format!("{}", RequestType::ACL) == "ACL");
+ assert!(format!("{}", RequestType::Invalid) == "INVALID");
+ }
+
+ #[test]
+ fn request() {
+ let sample = "020012341d87ccaac89563cbb39d2333b725e407a1a6";
+
+ let lhs_packet = Packet {
+ route_type: RouteType::Direct,
+ version: PayloadVersion::VersionOne,
+ path: vec![],
+ transport: [0, 0],
+ raw_content: Bytes::copy_from_slice(&decode("12341d87ccaac89563cbb39d2333b725e407a1a6").unwrap()),
+ content: PacketContent::Request(Request {
+ cipher: PeerToPeerCipher {
+ destination: 0x12,
+ source: 0x34,
+ mac: 0x1d87,
+ ciphertext: Bytes::copy_from_slice(&decode("ccaac89563cbb39d2333b725e407a1a6").unwrap()),
+ cleartext: Some(Bytes::copy_from_slice(b"\x9d\xd9\x16\xd2\x01\0\0\0\0\x9d0\x96\xbb\0\0\0")),
+ },
+ cleartext: Some(ClearRequest {
+ timestamp: DateTime::from_timestamp_secs(3524712861).unwrap(),
+ request_type: crate::request::RequestType::Stats,
+ request_data: Bytes::copy_from_slice(b"\0\0\0\0\x9d0\x96\xbb\0\0\0"),
+ })
+ }),
+ incomplete: false,
+ };
+
+ let mut rhs_packet = Packet::from_str(sample).unwrap();
+
+ let keystore = KeystoreInput {
+ identities: vec![
+ Identity {
+ name: "Sample 1 ID".to_owned().into(),
+ private_key: PrivateKey::from_str("4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E08").unwrap(),
+ public_key: PublicKey::default(),
+ secrets: vec![]
+ },
+ Identity {
+ name: "Sample 2 ID".to_owned().into(),
+ private_key: PrivateKey::from_str("38DAA98490B7284697C7ADA6175FD1F8DAD12032AD7ABAE625B7EAD8FEC6444CA281C3370B97155D9C8CECD89A929FDDE0FBF3A9D5C92A1B3C24D711934CD69D").unwrap(),
+ public_key: PublicKey::default(),
+ secrets: vec![]
+ },
+ ],
+ contacts: vec![
+ Contact {
+ name: "Sample 1 CT".to_owned().into(),
+ public_key: PublicKey::from_str("34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8f").unwrap()
+ },
+ Contact {
+ name: "Sample 2 CT".to_owned().into(),
+ public_key: PublicKey::from_str("12349bdc1f76a0c12149bb15f791dbe42fde02c209b04a85c6f512990c8cedec").unwrap()
+ },
+ ],
+ groups: vec![]
+ }.compile();
+
+ assert!(format!("{}", rhs_packet.content) == " REQUEST | (34) -> (12) MAC: 1d87 ENCRYPTED");
+
+ rhs_packet.try_decrypt(&keystore);
+
+ assert!(format!("{}", rhs_packet.content) == " REQUEST | (34) -> (12) MAC: 1d87 at: 2081-09-10 06:54:21 UTC STATS");
+
+ assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
+ }
+
+} \ No newline at end of file
diff --git a/src/response.rs b/src/response.rs
new file mode 100644
index 0000000..a3463e0
--- /dev/null
+++ b/src/response.rs
@@ -0,0 +1,69 @@
+use std::fmt::Display;
+
+use bytes::Bytes;
+use structdiff::{Difference, StructDiff};
+use crate::packet_content::PeerToPeerCipher;
+
+#[derive(PartialEq, Debug, Clone, Difference)]
+#[difference(expose)]
+pub struct Response {
+ pub(crate) cipher: PeerToPeerCipher,
+}
+
+impl From<Bytes> for Response {
+ fn from(value: Bytes) -> Self {
+ Response {
+ cipher: PeerToPeerCipher::from(value)
+ }
+ }
+}
+
+impl Display for Response {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_fmt(format_args!("({:2x?}) -> ({:2x?}) MAC: {:4x?} ",
+ self.cipher.source,
+ self.cipher.destination,
+ self.cipher.mac
+ ))?;
+
+ if let Some(_cleartext) = &self.cipher.cleartext {
+ f.write_fmt(format_args!(""))
+ } else {
+ f.write_str("ENCRYPTED")
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::str::FromStr;
+
+ use bytes::Bytes;
+ use hex::decode;
+ use crate::{packet::*, packet_content::{PacketContent, PeerToPeerCipher}, response::Response};
+
+ #[test]
+ fn response() {
+ let sample = "06003412b9000cf1641739f7e4d49bff88bf5695b304111b15277de3a6031b9af3a2b6371cf75615ffefe7dcc0bbea2856c4e798a72d5b989d6de1aa646c1e2eef4cf13e6f92";
+
+ let lhs_packet = Packet {
+ route_type: RouteType::Direct,
+ version: PayloadVersion::VersionOne,
+ path: vec![],
+ transport: [0, 0],
+ raw_content: Bytes::copy_from_slice(&decode("3412b9000cf1641739f7e4d49bff88bf5695b304111b15277de3a6031b9af3a2b6371cf75615ffefe7dcc0bbea2856c4e798a72d5b989d6de1aa646c1e2eef4cf13e6f92").unwrap()),
+ content: PacketContent::Response(Response {
+ cipher: PeerToPeerCipher {
+ destination: 0x34,
+ source: 0x12,
+ mac: 0xb900,
+ ciphertext: Bytes::copy_from_slice(&decode("0cf1641739f7e4d49bff88bf5695b304111b15277de3a6031b9af3a2b6371cf75615ffefe7dcc0bbea2856c4e798a72d5b989d6de1aa646c1e2eef4cf13e6f92").unwrap()),
+ cleartext: None
+ }}),
+ incomplete: false,
+ };
+
+ let rhs_packet = Packet::from_str(sample).unwrap();
+ assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
+ }
+} \ No newline at end of file
diff --git a/src/text.rs b/src/text.rs
new file mode 100644
index 0000000..ae2b0b9
--- /dev/null
+++ b/src/text.rs
@@ -0,0 +1,348 @@
+use std::{fmt::Display, rc::Rc};
+use bytes::{Buf, Bytes};
+use chrono::{DateTime, Utc};
+use hex::encode;
+use structdiff::{Difference, StructDiff};
+
+use crate::{identity::Keystore, packet_content::PeerToPeerCipher};
+
+#[derive(PartialEq, Debug, Clone, Difference)]
+#[difference(expose)]
+pub struct Text {
+ pub cipher: PeerToPeerCipher,
+ pub cleartext: Option<ClearText>,
+}
+
+impl From<Bytes> for Text {
+ fn from(value: Bytes) -> Self {
+ Text {
+ cipher: PeerToPeerCipher::from(value),
+ cleartext: None
+ }
+ }
+}
+
+impl Display for Text {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_fmt(format_args!("({:2x?}) -> ({:2x?}) MAC: {:4x?} ",
+ self.cipher.source,
+ self.cipher.destination,
+ self.cipher.mac
+ ))?;
+
+ if let Some(cleartext) = &self.cleartext {
+ if let Some(sender) = &cleartext.sender {
+ f.write_fmt(format_args!("at: {}, {} attempts, from {} to {}: {}",
+ cleartext.timestamp,
+ cleartext.attempts,
+ sender,
+ cleartext.crypto_recipient,
+ cleartext.message.replace("\n", "\\n"))
+ )
+ } else {
+ f.write_fmt(format_args!("at: {}, {} attempts, from {} to {}: {}",
+ cleartext.timestamp,
+ cleartext.attempts,
+ "Unknown",
+ cleartext.crypto_recipient,
+ cleartext.message.replace("\n", "\\n"))
+ )
+ }
+
+ } else {
+ f.write_str("ENCRYPTED")
+ }
+ }
+}
+
+
+#[derive(PartialEq, Debug, Clone, Difference)]
+#[difference(expose)]
+pub struct ClearText {
+ pub timestamp: DateTime<Utc>,
+ pub message_type: MessageType,
+ pub attempts: u8,
+ pub sender_hash: u32,
+ pub sender: Option<String>,
+ pub message: String,
+
+ pub crypto_recipient: Rc<String>
+}
+
+impl From<Bytes> for ClearText {
+ fn from(value: Bytes) -> Self {
+ let mut bytes = value;
+
+ let mut clear_text = ClearText {
+ timestamp: DateTime::from_timestamp(0, 0).unwrap(),
+ message_type: MessageType::Incomplete,
+ attempts: 0,
+ sender_hash: 0,
+ sender: None,
+ message: "".to_string(),
+ crypto_recipient: Rc::new("".to_string()),
+ };
+
+ // Just check for the whole fixed-size part at once
+ if bytes.len() < 5 { return clear_text }
+ if let Some(timestamp) = DateTime::from_timestamp(bytes.get_u32_le() as i64, 0) {
+ clear_text.timestamp = timestamp;
+ }
+
+ let flags = bytes.get_u8();
+ clear_text.message_type = MessageType::from(flags);
+ clear_text.attempts = flags & 0x03;
+
+ if clear_text.message_type == MessageType::SignedPlain &&
+ bytes.len() > 4 {
+ clear_text.sender_hash = bytes.get_u32();
+ }
+
+ let raw_message = String::from_utf8_lossy(&bytes).to_owned();
+ let trimmed_message: Vec<&str> = raw_message.splitn(2, "\0").collect();
+
+ // Apparently the sender is just whatever is before the first :
+ let splits: Vec<&str> = trimmed_message[0].splitn(2, ": ").collect();
+ if splits.len() > 1 {
+ clear_text.sender = Some(splits[0].to_owned());
+ clear_text.message = splits[1].to_owned();
+ } else {
+ clear_text.message = splits[0].to_owned();
+ }
+
+ clear_text
+ }
+}
+
+#[derive(PartialEq, Debug, Clone)]
+pub struct GroupData {
+ pub(crate) payload: Bytes
+}
+
+impl From<Bytes> for GroupData {
+ fn from(value: Bytes) -> Self {
+ GroupData { payload: value }
+ }
+}
+
+impl Display for GroupData {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_fmt(format_args!("Payload: {}", encode(&self.payload)))
+ }
+}
+
+#[derive(PartialEq, Debug, Clone, Difference)]
+#[difference(expose)]
+pub struct GroupText {
+ hash: u8,
+ mac: u16,
+ ciphertext: Bytes,
+
+ cleartext: Option<ClearText>,
+
+ incomplete: bool
+}
+
+impl From<Bytes> for GroupText {
+ fn from(value: Bytes) -> Self {
+ let mut bytes = value;
+
+ let mut group_text = GroupText {
+ hash: 0x00,
+ mac: 0x0000,
+ ciphertext: Bytes::new(),
+ cleartext: None,
+ incomplete: true,
+ };
+
+ // Just check for the whole fixed-size part at once
+ if bytes.len() < 3 { return group_text }
+ group_text.hash = bytes.get_u8();
+ group_text.mac = bytes.get_u16();
+
+ group_text.ciphertext = bytes;
+ group_text.incomplete = false;
+ group_text
+ }
+}
+
+impl GroupText {
+ pub fn try_decrypt(&mut self, keysore: &Keystore) -> bool {
+ let decrypt_result = keysore.decrypt_and_id_group(
+ self.hash,
+ self.mac,
+ &self.ciphertext
+ );
+
+ if let Some((cleartext, group)) = decrypt_result {
+ let mut cleartext = ClearText::from(cleartext);
+ cleartext.crypto_recipient = group.name.clone();
+ self.cleartext = Some(cleartext);
+ true
+ } else {
+ false
+ }
+ }
+}
+
+impl Display for GroupText {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ if let Some(cleartext) = &self.cleartext {
+ f.write_fmt(format_args!("({:2x?}) Mac: {:4x?} Group {}: {}",
+ self.hash,
+ self.mac,
+ cleartext.crypto_recipient,
+ cleartext.message.replace("\n", "\\n"))
+ )
+ } else {
+ f.write_fmt(format_args!("({:2x?}) Mac: {:4x?} ENCRYPTED", self.hash, self.mac))
+ }
+ }
+}
+
+#[derive(PartialEq, Debug, Clone)]
+pub enum MessageType {
+ Plain,
+ CLI,
+ SignedPlain,
+ Invalid,
+ Incomplete,
+}
+
+impl From<u8> for MessageType {
+ fn from(value: u8) -> Self {
+ match (value & 0xFC) >> 3 {
+ 0x00 => MessageType::Plain,
+ 0x01 => MessageType::CLI,
+ 0x02 => MessageType::SignedPlain,
+ _ => MessageType::Invalid
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::{rc::Rc, str::FromStr};
+
+ use chrono::DateTime;
+ use hex::decode;
+ use bytes::Bytes;
+ use crate::{crypto::SharedSecret, identity::{Group, KeystoreInput}, packet::*, packet_content::{PacketContent, PeerToPeerCipher}, text::{ClearText, GroupData, GroupText, MessageType, Text}};
+
+ #[test]
+ fn text() {
+ let sample = "0a001234e91c8eb2b815e0eccf6781a3ff1820d0fb130fcfc87b914244fae227d4ad4c752fb9";
+
+ let lhs_packet = Packet {
+ route_type: RouteType::Direct,
+ version: PayloadVersion::VersionOne,
+ path: vec![],
+ transport: [0, 0],
+ raw_content: Bytes::copy_from_slice(&decode("1234e91c8eb2b815e0eccf6781a3ff1820d0fb130fcfc87b914244fae227d4ad4c752fb9").unwrap()),
+ content: PacketContent::Text(Text {
+ cipher: PeerToPeerCipher {
+ destination: 0x12,
+ source: 0x34,
+ mac: 0xe91c,
+ ciphertext: Bytes::copy_from_slice(&decode("8eb2b815e0eccf6781a3ff1820d0fb130fcfc87b914244fae227d4ad4c752fb9").unwrap()),
+ cleartext: None
+ },
+ cleartext: None,
+ }),
+ incomplete: false,
+ };
+
+ let rhs_packet = Packet::from_str(sample).unwrap();
+ assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
+ }
+
+ #[test]
+ fn group_text() {
+ let sample = "15107B2CF1A31C03E8AACD7E6066B8A0ACB71176B87DDF8FE67B33E7A63036E015311EE39232F094FAAD13C4442947947DD098886BC677DE2B6F456149D0A1B05C72F0EECC20DC07EDF163D9D63EBC9BC29A7C79AF";
+
+ let lhs_packet = Packet {
+ route_type: RouteType::Flood,
+ version: PayloadVersion::VersionOne,
+ path: vec![0x7B, 0x2C, 0xF1, 0xA3, 0x1C, 0x03, 0xE8, 0xAA, 0xCD, 0x7E, 0x60, 0x66, 0xB8, 0xA0, 0xAC, 0xB7],
+ transport: [0, 0],
+ raw_content: Bytes::copy_from_slice(&decode("1176B87DDF8FE67B33E7A63036E015311EE39232F094FAAD13C4442947947DD098886BC677DE2B6F456149D0A1B05C72F0EECC20DC07EDF163D9D63EBC9BC29A7C79AF").unwrap()),
+ content: PacketContent::GroupText(GroupText {
+ hash: 0x11,
+ mac: 0x76B8,
+ ciphertext: Bytes::copy_from_slice(&decode("7DDF8FE67B33E7A63036E015311EE39232F094FAAD13C4442947947DD098886BC677DE2B6F456149D0A1B05C72F0EECC20DC07EDF163D9D63EBC9BC29A7C79AF").unwrap()),
+ cleartext: None, // "Just need some more high repeaters".to_owned()
+ incomplete: false
+ }),
+ incomplete: false
+ };
+
+ let rhs_packet = Packet::from_str(sample).unwrap();
+ assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
+ }
+
+ #[test]
+ fn group_text_decrypted() {
+ let sample = "150011C3C1354D619BAE9590E4D177DB7EEAF982F5BDCF78005D75157D9535FA90178F785D";
+
+ let lhs_packet = Packet {
+ route_type: RouteType::Flood,
+ version: PayloadVersion::VersionOne,
+ path: vec![],
+ transport: [0, 0],
+ raw_content: Bytes::copy_from_slice(&decode("11C3C1354D619BAE9590E4D177DB7EEAF982F5BDCF78005D75157D9535FA90178F785D").unwrap()),
+ content: PacketContent::GroupText(GroupText {
+ hash: 0x11,
+ mac: 0xC3C1,
+ ciphertext: Bytes::copy_from_slice(&decode("354D619BAE9590E4D177DB7EEAF982F5BDCF78005D75157D9535FA90178F785D").unwrap()),
+ cleartext: Some(ClearText {
+ sender: Some("🌲 Tree".to_owned()),
+ message: "☁️".to_owned(),
+ timestamp: DateTime::from_timestamp_secs(1758484279).unwrap(),
+ message_type: MessageType::Plain,
+ attempts: 0,
+ sender_hash: 0,
+ crypto_recipient: Rc::new("Public".to_owned()),
+ }),
+ incomplete: false
+ }),
+ incomplete: false
+ };
+
+ let mut rhs_packet = Packet::from_str(sample).unwrap();
+
+ let keystore = KeystoreInput {
+ identities: vec![],
+ contacts: vec![],
+ groups: vec![
+ Group {
+ name: Rc::new("Public".to_owned()),
+ secret: SharedSecret::from_str("8b3387e9c5cdea6ac9e5edbaa115cd72").unwrap()
+ }
+ ]
+ }.compile();
+
+ _ = rhs_packet.try_decrypt(&keystore);
+
+ assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
+ }
+
+ #[test]
+ fn group_data() {
+ let sample = "18079DBB163F3C88707DF4C430C8A06A587CF7E827641C6521A5DE85581C8800793AF4A5497196CB5F24B92A33A9C8AA3BAE4F8C94E8E464849BCBF6333509C3941C47B7ABF85ECFD2FF06DA1A39575D155941F152F63300D2C31B5FAFBDA79637";
+
+ let lhs_packet = Packet {
+ route_type: RouteType::TransportFlood,
+ version: PayloadVersion::VersionOne,
+ path: vec![0x3C, 0x88, 0x70, 0x7D, 0xF4, 0xC4, 0x30, 0xC8, 0xA0, 0x6A, 0x58, 0x7C, 0xF7, 0xE8, 0x27, 0x64, 0x1C, 0x65, 0x21, 0xA5, 0xDE, 0x85, 0x58, 0x1C, 0x88, 0x00, 0x79, 0x3A, 0xF4, 0xA5, 0x49, 0x71, 0x96, 0xCB, 0x5F, 0x24, 0xB9, 0x2A, 0x33, 0xA9, 0xC8, 0xAA, 0x3B, 0xAE, 0x4F, 0x8C, 0x94, 0xE8, 0xE4, 0x64, 0x84, 0x9B, 0xCB, 0xF6, 0x33, 0x35, 0x09, 0xC3, 0x94, 0x1C, 0x47, 0xB7, 0xAB],
+ transport: [0x9d07, 0x16bb],
+ raw_content: Bytes::copy_from_slice(&decode("F85ECFD2FF06DA1A39575D155941F152F63300D2C31B5FAFBDA79637").unwrap()),
+ content: PacketContent::GroupData(GroupData {
+ payload: Bytes::copy_from_slice(&decode("F85ECFD2FF06DA1A39575D155941F152F63300D2C31B5FAFBDA79637").unwrap())
+ }),
+ incomplete: false
+ };
+
+ let rhs_packet = Packet::from_str(sample).unwrap();
+ assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
+ }
+} \ No newline at end of file
diff --git a/src/trace.rs b/src/trace.rs
new file mode 100644
index 0000000..c64efc0
--- /dev/null
+++ b/src/trace.rs
@@ -0,0 +1,87 @@
+use std::fmt::{Debug, Display};
+use bytes::{Buf, Bytes};
+use structdiff::{Difference, StructDiff};
+
+#[derive(PartialEq, Debug, Clone, Difference)]
+#[difference(expose)]
+pub struct Trace {
+ tag: u32,
+ auth: u32,
+ flags: u8,
+ pub(crate) path_snr: Vec<f32>,
+ invalid: bool,
+
+ pub(crate) temp_path: Vec<u8>
+}
+
+impl From<Bytes> for Trace {
+ fn from(value: Bytes) -> Self {
+ let mut bytes = value;
+
+ let mut trace = Trace {
+ tag: 0,
+ auth: 0,
+ flags: 0,
+ path_snr: vec![],
+ invalid: true,
+ temp_path: vec![]
+ };
+
+ if bytes.len() < 10 { return trace; }
+
+ trace.tag = bytes.get_u32();
+ trace.auth = bytes.get_u32();
+ trace.flags = bytes.get_u8();
+ trace.temp_path = bytes.iter().copied().collect();
+ trace.invalid = false;
+ trace
+ }
+}
+
+impl Display for Trace {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ for (snr, hash) in self.path_snr.iter().zip(self.temp_path.clone()) {
+ f.write_fmt(format_args!("({:2x?}): {}dB ", hash, snr))?;
+ }
+
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::str::FromStr;
+ use hex::decode;
+ use crate::{packet::*, packet_content::PacketContent};
+
+ use super::*;
+
+
+ #[test]
+ fn trace() {
+ let sample = "26032CE0F149EB5B240000000000ACA079";
+
+ let lhs_packet = Packet {
+ route_type: RouteType::Direct,
+ version: PayloadVersion::VersionOne,
+ path: vec![0xAC, 0xA0, 0x79],
+ transport: [0, 0],
+ raw_content: Bytes::copy_from_slice(&decode("49EB5B240000000000ACA079").unwrap()),
+ content: PacketContent::Trace(Trace {
+ tag: 0x49EB5B24,
+ auth: 0x00000000,
+ flags: 0x00,
+ path_snr: vec![11.0, -8.0, -3.75],
+ invalid: false,
+ temp_path: vec![0xAC, 0xA0, 0x79]
+ }
+ ),
+ incomplete: false
+ };
+
+ let rhs_packet = Packet::from_str(sample).unwrap();
+ assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
+ }
+
+
+} \ No newline at end of file