diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 42e3474d..1a2af8b5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,6 +37,8 @@ jobs: run: cargo clippy -- -D warnings - name: Run clippy for tests and fail if any warnings run: cargo clippy --tests -- -D warnings + - name: Run clippy for tests with features enabled + run: cargo clippy --tests --all-features -- -D warnings - name: Run tests with debugs if: matrix.os == 'ubuntu-20.04' || matrix.os == 'macos-14' timeout-minutes: 2 @@ -46,3 +48,12 @@ jobs: RUST_LOG: debug if: matrix.os == 'windows-latest' run: cargo test -- --nocapture + - name: Run tests with features + if: matrix.os == 'ubuntu-20.04' || matrix.os == 'macos-14' + timeout-minutes: 2 + run: RUST_LOG=debug cargo test --all-features -- --nocapture + - name: Run tests with features on Windows + env: + RUST_LOG: debug + if: matrix.os == 'windows-latest' + run: cargo test --all-features -- --nocapture diff --git a/Cargo.toml b/Cargo.toml index 95c80bdd..bda9bb2a 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 d785f191..19513c8e 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, @@ -62,6 +66,7 @@ impl From<&Interface> for InterfaceId { /// An IPv4 address with an interface identifier. #[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] pub struct ScopedIpV4 { addr: Ipv4Addr, /// The `interface_id` indicates which interface this address is associated with. @@ -82,6 +87,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, @@ -101,6 +107,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 72137fea..c2ec9957 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." @@ -1697,4 +1705,52 @@ mod tests { assert!(!service_info.is_address_supported(&intf_loopback_v4)); assert!(service_info.is_address_supported(&intf_loopback_v6)); } + + #[cfg(test)] + #[cfg(feature = "serde")] + mod serde { + use super::{Ipv4Addr, Ipv6Addr}; + use crate::{ResolvedService, ScopedIp, TxtProperties}; + + use std::collections::HashSet; + use std::net::IpAddr; + + #[test] + fn test_deserialize_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, + txt_properties: TxtProperties::new(), + }; + + let json = serde_json::to_value(&service)?; + + let parsed: ResolvedService = serde_json::from_value(json)?; + + assert!(compare(&service, &parsed)); + + Ok(()) + } + + fn compare(service: &ResolvedService, other: &ResolvedService) -> bool { + service.ty_domain == other.ty_domain + && service.sub_ty_domain == other.sub_ty_domain + && service.fullname == other.fullname + && service.host == other.host + && service.port == other.port + && service.addresses == other.addresses + && service.txt_properties == other.txt_properties + } + } }