use crate::{crypto::PublicKey, packet_content::NodeType, string_helper::NameString}; use bytes::{Buf, Bytes}; use chrono::{DateTime, Local, Utc}; #[derive(PartialEq, Clone, core::fmt::Debug)] pub struct Advert { pub public_key: PublicKey, pub timestamp: DateTime, pub signature: [u8; 64], pub node_type: NodeType, pub latitude: Option, pub longitude: Option, pub feature1: Option, pub feature2: Option, pub name: Option, } impl Advert { #[cfg(feature = "std")] fn name_from_bytes(bytes: Bytes) -> Option { let mut name_split = bytes.chunk().split(|c| *c == 0); if let Some(name) = name_split.next() { if let Ok(name) = NameString::from_utf8(name.to_vec()) { Some(name) } else { None } } else { None } } #[cfg(not(feature = "std"))] fn name_from_bytes(bytes: Bytes) -> Option { let mut name_split = bytes.chunk().split(|c| *c == 0); if let Some(name) = name_split.next() { if let Ok(name) = NameString::from_utf8(name) { Some(name) } else { None } } else { None } } } impl Default for Advert { fn default() -> Self { Self { public_key: PublicKey::default(), timestamp: Local::now().into(), signature: [0_u8; 64], node_type: NodeType::Invalid, latitude: None, longitude: None, feature1: None, feature2: None, #[cfg(feature = "std")] name: None, #[cfg(not(feature = "std"))] name: None, } } } impl From for Advert { fn from(value: Bytes) -> Self { let mut bytes = value; let mut advert = Advert::default(); 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 difference is // whether the name is omitted or just empty. // I'm not going to make the distinction // Find only the bytes (characters) before the first null. advert.name = Self::name_from_bytes(bytes); advert } } impl core::fmt::Display for Advert { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { core::fmt::Debug::fmt(&self.node_type, f)?; f.write_str(" \"")?; if let Some(name) = &self.name { f.write_str(&name)?; } else { f.write_str("")?; } f.write_fmt(format_args!( "\" ({:2x?}) at: ", self.public_key.hash_prefix() >> 24 ))?; core::fmt::Display::fmt(&self.timestamp, f)?; match (self.latitude, self.longitude) { (Some(lat), Some(lon)) => { f.write_fmt(format_args!(" location: {}, {}", lat, lon))?; } _ => {} } Ok(()) } } // Tests for std operations #[cfg(test)] mod tests { use core::str::FromStr; use chrono::DateTime; use hex::decode; use tinyvec::ArrayVec; use crate::crypto::*; use crate::packet::*; use crate::packet_content::PacketContent; use crate::string_helper::name_string_from_slice; use super::*; #[test] fn advert_with_lat_lon() { // Real sample packet from the air use tinyvec::array_vec; let sample = "110c015f7e9b60661da0f2671512460728508c17ef336412a223144d3a623215162682045c44fef7241af0161923d3af0769d2d976b687a506dd5325ef526bf3eb52ae687277fcbde9969a5b0087e0eb0f7c1760a50c6a88bec13cc30a2a9b681d713166515e3bbc2bc27f20c0e4d7b67e08910c29dc02c468b8f8484f574c"; let mut signature_slice = [0 as u8; 64]; hex::decode_to_slice("d2d976b687a506dd5325ef526bf3eb52ae687277fcbde9969a5b0087e0eb0f7c1760a50c6a88bec13cc30a2a9b681d713166515e3bbc2bc27f20c0e4d7b67e08", &mut signature_slice).unwrap(); let mut name_slice = [0u8; 32]; let string_slice = "HOWL".as_bytes(); name_slice[0..string_slice.len()].copy_from_slice(string_slice); let lhs_packet = Packet { route_type: RouteType::Flood, version: PayloadVersion::VersionOne, path: array_vec!([u16; 64] => 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: Some(name_string_from_slice(b"HOWL")), }), incomplete: false, }; let rhs_packet = Packet::from_str(sample).unwrap(); assert_eq!(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: ArrayVec::new(), 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: Some(name_string_from_slice(b"hpux735")), }), incomplete: false, }; let rhs_packet = Packet::from_str(sample).unwrap(); assert_eq!(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: Some(name_string_from_slice(b"HOWL")), }; #[cfg(feature = "std")] assert_eq!( format!("{}", advert), "Chat \"HOWL\" (46) at: 2025-11-02 19:24:03 UTC location: 47.98286, -122.132286" ); } }