From d7109e2bc42f8ca65a4f07b3acf9560b0d4d36b5 Mon Sep 17 00:00:00 2001 From: Rascal <30204467+rabbit-time@users.noreply.github.com> Date: Sun, 8 Mar 2026 21:35:11 -0500 Subject: [PATCH 1/3] Derive Serialize/Deserialize for ResolvedService --- Cargo.toml | 4 +++ src/dns_parser.rs | 8 ++++++ src/service_info.rs | 62 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4f54a6a..b7891a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ description = "mDNS Service Discovery library with no async runtime dependency" [features] async = ["flume/async"] logging = ["log"] +serde = ["dep:serde"] default = ["async", "logging"] [dependencies] @@ -24,10 +25,13 @@ log = { version = "0.4", optional = true } # logging mio = { version = "1.1", features = ["os-poll", "net"] } # select/poll sockets socket2 = { version = "0.6", features = ["all"] } # socket APIs socket-pktinfo = "0.3.2" +# support for serde's deserialize/serialize traits +serde = { version = "1.0.228", features = ["derive"], optional = true } [dev-dependencies] env_logger = { version = "= 0.10.2", default-features = false, features= ["humantime"] } fastrand = "2.3" humantime = "2.1" +serde_json = "1.0.149" test-log = "= 0.2.14" test-log-macros = "= 0.2.14" diff --git a/src/dns_parser.rs b/src/dns_parser.rs index 7bf56b5..ba16d75 100644 --- a/src/dns_parser.rs +++ b/src/dns_parser.rs @@ -12,6 +12,9 @@ use crate::service_info::is_unicast_link_local; use if_addrs::Interface; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + use std::{ any::Any, cmp, @@ -25,6 +28,7 @@ use std::{ /// Represents a network interface identifier defined by the OS. #[derive(Clone, Debug, Eq, Hash, PartialEq, Default)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub struct InterfaceId { /// Interface name, e.g. "en0", "wlan0", etc. pub name: String, @@ -53,6 +57,7 @@ impl From<&Interface> for InterfaceId { /// Note: IPv4 addresses don't have scope IDs, but this type is named for consistency /// with the rest of the addressing system. #[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub struct ScopedIpV4 { addr: Ipv4Addr, } @@ -66,6 +71,7 @@ impl ScopedIpV4 { /// An IPv6 address with scope_id (interface identifier). #[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub struct ScopedIpV6 { addr: Ipv6Addr, scope_id: InterfaceId, @@ -85,6 +91,8 @@ impl ScopedIpV6 { /// An IP address, either IPv4 or IPv6, that supports scope_id for IPv6. #[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[cfg_attr(feature = "serde", serde(untagged))] #[non_exhaustive] pub enum ScopedIp { V4(ScopedIpV4), diff --git a/src/service_info.rs b/src/service_info.rs index 72137fe..6659355 100644 --- a/src/service_info.rs +++ b/src/service_info.rs @@ -17,6 +17,9 @@ use std::{ str::FromStr, }; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + /// Default TTL values in seconds const DNS_HOST_TTL: u32 = 120; // 2 minutes for host records (A, SRV etc) per RFC6762 const DNS_OTHER_TTL: u32 = 4500; // 75 minutes for non-host records (PTR, TXT etc) per RFC6762 @@ -605,6 +608,8 @@ impl AsIpAddrs for Box { /// [RFC 6763](https://www.rfc-editor.org/rfc/rfc6763#section-6.4): /// "A given key SHOULD NOT appear more than once in a TXT record." #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[cfg_attr(feature = "serde", serde(transparent))] pub struct TxtProperties { // Use `Vec` instead of `HashMap` to keep the order of insertions. properties: Vec, @@ -703,6 +708,7 @@ impl From<&[u8]> for TxtProperties { /// Represents a property in a TXT record. #[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub struct TxtProperty { /// The name of the property. The original cases are kept. key: String, @@ -710,6 +716,7 @@ pub struct TxtProperty { /// RFC 6763 says values are bytes, not necessarily UTF-8. /// It is also possible that there is no value, in which case /// the key is a boolean key. + #[cfg_attr(feature = "serde", serde(rename = "value"))] val: Option>, } @@ -1276,6 +1283,7 @@ pub(crate) fn is_unicast_link_local(addr: &Ipv6Addr) -> bool { /// Represents a resolved service as a plain data struct. /// This is from a client (i.e. querier) point of view. #[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] #[non_exhaustive] pub struct ResolvedService { /// Service type and domain. For example, "_http._tcp.local." @@ -1371,9 +1379,12 @@ impl ResolvedService { #[cfg(test)] mod tests { use super::{decode_txt, encode_txt, u8_slice_to_hex, ServiceInfo, TxtProperty}; - use crate::IfKind; + use crate::{IfKind, ResolvedService, ScopedIp, TxtProperties}; use if_addrs::{IfAddr, IfOperStatus, Ifv4Addr, Ifv6Addr, Interface}; - use std::net::{Ipv4Addr, Ipv6Addr}; + use std::{ + collections::HashSet, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + }; #[test] fn test_txt_encode_decode() { @@ -1697,4 +1708,51 @@ mod tests { assert!(!service_info.is_address_supported(&intf_loopback_v4)); assert!(service_info.is_address_supported(&intf_loopback_v6)); } + + #[test] + fn test_serialize() -> Result<(), Box> { + let addresses = HashSet::from([ + ScopedIp::from(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))), + ScopedIp::from(IpAddr::V6(Ipv6Addr::new( + 0xfe80, 0x2001, 0x0db8, 0x85a3, 0x0000, 0x8a2e, 0x0370, 0x7334, + ))), + ]); + + let service = ResolvedService { + ty_domain: "_http._tcp.local.".to_owned(), + sub_ty_domain: None, + fullname: "example._http._tcp.local.".to_owned(), + host: "example.local.".to_owned(), + port: 1234, + addresses: addresses, + txt_properties: TxtProperties::new(), + }; + + let json = serde_json::to_string(&service)?; + + let correct = r#"{ + "ty_domain": "_http._tcp.local.", + "sub_ty_domain": null, + "fullname": "example._http._tcp.local.", + "host": "example.local.", + "port": 1234, + "addresses": [ + { + "addr": "fe80:2001:db8:85a3:0:8a2e:370:7334", + "scope_id": { + "name": "", + "index": 0 + } + }, + { + "addr": "127.0.0.1" + } + ], + "txt_properties": [] + }"#.replace(" ", "").replace("\n", ""); + + assert_eq!(json, correct); + + Ok(()) + } } From 547bf60d372f1ce70cc569d733e455bd218a92c9 Mon Sep 17 00:00:00 2001 From: Rascal <30204467+rabbit-time@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:02:20 -0500 Subject: [PATCH 2/3] Run rustfmt --- src/service_info.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/service_info.rs b/src/service_info.rs index 6659355..e0431a1 100644 --- a/src/service_info.rs +++ b/src/service_info.rs @@ -1749,7 +1749,9 @@ mod tests { } ], "txt_properties": [] - }"#.replace(" ", "").replace("\n", ""); + }"# + .replace(" ", "") + .replace("\n", ""); assert_eq!(json, correct); From 95be57b1a3668ff68bd6555390e5d7c1d28b5286 Mon Sep 17 00:00:00 2001 From: Rascal <30204467+rabbit-time@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:14:33 -0500 Subject: [PATCH 3/3] Fix redundant field name --- src/service_info.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service_info.rs b/src/service_info.rs index e0431a1..fef16b3 100644 --- a/src/service_info.rs +++ b/src/service_info.rs @@ -1724,7 +1724,7 @@ mod tests { fullname: "example._http._tcp.local.".to_owned(), host: "example.local.".to_owned(), port: 1234, - addresses: addresses, + addresses, txt_properties: TxtProperties::new(), };