diff --git a/.gitignore b/.gitignore index b60de5b..4c7b3d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ **/target +.vscode diff --git a/examples/Cargo.lock b/examples/Cargo.lock index 2902598..7476f86 100644 --- a/examples/Cargo.lock +++ b/examples/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "aho-corasick" version = "0.7.13" @@ -500,7 +502,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "zeroconf" -version = "0.7.0" +version = "0.7.2" dependencies = [ "avahi-sys", "bonjour-sys", diff --git a/examples/service/src/main.rs b/examples/service/src/main.rs index 452dd84..a3a2d39 100644 --- a/examples/service/src/main.rs +++ b/examples/service/src/main.rs @@ -1,8 +1,8 @@ use std::any::Any; use std::sync::{Arc, Mutex}; use std::time::Duration; -use zeroconf::{MdnsService, ServiceRegistration, TxtRecord}; use zeroconf::prelude::*; +use zeroconf::{MdnsService, ServiceRegistration, ServiceType, TxtRecord}; #[derive(Default, Debug)] pub struct Context { @@ -10,7 +10,7 @@ pub struct Context { } fn main() { - let mut service = MdnsService::new("_http._tcp", 8080); + let mut service = MdnsService::new(ServiceType::new("http", "tcp").unwrap(), 8080); let mut txt_record = TxtRecord::new(); let context: Arc> = Arc::default(); diff --git a/zeroconf/src/lib.rs b/zeroconf/src/lib.rs index 0213344..9b079a2 100644 --- a/zeroconf/src/lib.rs +++ b/zeroconf/src/lib.rs @@ -20,8 +20,8 @@ //! use std::any::Any; //! use std::sync::{Arc, Mutex}; //! use std::time::Duration; -//! use zeroconf::{MdnsService, ServiceRegistration, TxtRecord}; //! use zeroconf::prelude::*; +//! use zeroconf::{MdnsService, ServiceRegistration, ServiceType, TxtRecord}; //! //! #[derive(Default, Debug)] //! pub struct Context { @@ -29,7 +29,7 @@ //! } //! //! fn main() { -//! let mut service = MdnsService::new("_http._tcp", 8080); +//! let mut service = MdnsService::new(ServiceType::new("http", "tcp").unwrap(), 8080); //! let mut txt_record = TxtRecord::new(); //! let context: Arc> = Arc::default(); //! @@ -143,6 +143,7 @@ pub mod event_loop; pub mod ffi; pub mod prelude; pub mod service; +pub mod service_type; pub mod txt_record; #[cfg(target_os = "linux")] @@ -153,6 +154,7 @@ pub mod macos; pub use browser::{ServiceDiscoveredCallback, ServiceDiscovery}; pub use interface::*; pub use service::{ServiceRegisteredCallback, ServiceRegistration}; +pub use service_type::*; /// Type alias for the platform-specific mDNS browser implementation #[cfg(target_os = "linux")] diff --git a/zeroconf/src/linux/service.rs b/zeroconf/src/linux/service.rs index 1c438ff..7ae2361 100644 --- a/zeroconf/src/linux/service.rs +++ b/zeroconf/src/linux/service.rs @@ -8,7 +8,8 @@ use super::poll::ManagedAvahiSimplePoll; use crate::ffi::{c_str, AsRaw, FromRaw, UnwrapOrNull}; use crate::prelude::*; use crate::{ - EventLoop, NetworkInterface, Result, ServiceRegisteredCallback, ServiceRegistration, TxtRecord, + EventLoop, NetworkInterface, Result, ServiceRegisteredCallback, ServiceRegistration, + ServiceType, TxtRecord, }; use avahi_sys::{ AvahiClient, AvahiClientFlags, AvahiClientState, AvahiEntryGroup, AvahiEntryGroupState, @@ -18,6 +19,7 @@ use libc::c_void; use std::any::Any; use std::ffi::CString; use std::fmt::{self, Formatter}; +use std::str::FromStr; use std::sync::Arc; #[derive(Debug)] @@ -28,11 +30,14 @@ pub struct AvahiMdnsService { } impl TMdnsService for AvahiMdnsService { - fn new(kind: &str, port: u16) -> Self { + fn new(service_type: ServiceType, port: u16) -> Self { Self { client: None, poll: None, - context: Box::into_raw(Box::new(AvahiServiceContext::new(kind, port))), + context: Box::into_raw(Box::new(AvahiServiceContext::new( + &service_type.to_string(), + port, + ))), } } @@ -231,7 +236,9 @@ unsafe fn handle_group_established(context: &AvahiServiceContext) -> Result<()> let result = ServiceRegistration::builder() .name(c_str::copy_raw(context.name.as_ref().unwrap().as_ptr())) - .kind(c_str::copy_raw(context.kind.as_ptr())) + .service_type(ServiceType::from_str(&c_str::copy_raw( + context.kind.as_ptr(), + ))?) .domain("local".to_string()) .build()?; diff --git a/zeroconf/src/service.rs b/zeroconf/src/service.rs index 3cb5312..b1939f0 100644 --- a/zeroconf/src/service.rs +++ b/zeroconf/src/service.rs @@ -1,14 +1,14 @@ //! Trait definition for cross-platform service. -use crate::{EventLoop, NetworkInterface, Result, TxtRecord}; +use crate::{EventLoop, NetworkInterface, Result, ServiceType, TxtRecord}; use std::any::Any; use std::sync::Arc; /// Interface for interacting with underlying mDNS service implementation registration /// capabilities. pub trait TMdnsService { - /// Creates a new `MdnsService` with the specified `kind` (e.g. `_http._tcp`) and `port`. - fn new(kind: &str, port: u16) -> Self; + /// Creates a new `MdnsService` with the specified `ServiceType` (e.g. `_http._tcp`) and `port`. + fn new(service_type: ServiceType, port: u16) -> Self; /// Sets the name to register this service under. fn set_name(&mut self, name: &str); @@ -64,6 +64,6 @@ pub type ServiceRegisteredCallback = dyn Fn(Result, Option< #[derive(Builder, BuilderDelegate, Debug, Getters, Clone, Default, PartialEq, Eq)] pub struct ServiceRegistration { name: String, - kind: String, + service_type: ServiceType, domain: String, } diff --git a/zeroconf/src/service_type.rs b/zeroconf/src/service_type.rs new file mode 100644 index 0000000..dcd1caa --- /dev/null +++ b/zeroconf/src/service_type.rs @@ -0,0 +1,139 @@ +//! Data type for constructing a service type + +use crate::Result; +use std::str::FromStr; + +/// Data type for constructing a service type to register as an mDNS service. +#[derive(Default, Debug, Getters, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct ServiceType { + name: String, + protocol: String, + sub_types: Vec, +} + +impl ServiceType { + /// Creates a new `ServiceType` with the specified name (e.g. `http`) and protocol (e.g. `tcp`) + pub fn new(name: &str, protocol: &str) -> Result { + Ok(Self { + name: Self::check_part(name)?.to_string(), + protocol: Self::check_part(protocol)?.to_string(), + sub_types: vec![], + }) + } + + /// Creates a new `ServiceType` with the specified name (e.g. `http`) and protocol (e.g. `tcp`) + /// and sub-types. + pub fn with_sub_types(name: &str, protocol: &str, sub_types: Vec<&str>) -> Result { + for sub_type in &sub_types { + Self::check_part(sub_type)?; + } + + Ok(Self { + name: name.to_string(), + protocol: protocol.to_string(), + sub_types: sub_types.iter().map(|s| s.to_string()).collect(), + }) + } + + fn check_part(part: &str) -> Result<&str> { + if part.contains(".") { + Err("invalid character: .".into()) + } else if part.contains(",") { + Err("invalid character: ,".into()) + } else { + Ok(part) + } + } +} + +impl ToString for ServiceType { + fn to_string(&self) -> String { + format!("_{}._{}{}", self.name, self.protocol, { + if !self.sub_types.is_empty() { + format!(",_{}", self.sub_types.join(",_")) + } else { + "".to_string() + } + }) + } +} + +impl FromStr for ServiceType { + type Err = crate::error::Error; + fn from_str(s: &str) -> Result { + let parts: Vec<&str> = s.split(",").collect(); + if parts.is_empty() { + return Err("could not parse ServiceType from string".into()); + } + + let head: Vec<&str> = parts[0].split(".").collect(); + let mut name = head[0]; + if name.starts_with("_") { + name = &name[1..]; + } + let mut protocol = head[1]; + if protocol.starts_with("_") { + protocol = &protocol[1..]; + } + + let mut sub_types: Vec<&str> = vec![]; + if parts.len() > 1 { + for i in 1..parts.len() { + let mut sub_type = parts[i]; + if sub_type.starts_with("_") { + sub_type = &sub_type[1..]; + } + sub_types.push(sub_type); + } + } + + Ok(ServiceType::with_sub_types(name, protocol, sub_types)?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_invalid() { + ServiceType::new(".http", "tcp").expect_err("invalid character: .".into()); + ServiceType::new("http", ".tcp").expect_err("invalid character: .".into()); + ServiceType::new(",http", "tcp").expect_err("invalid character: ,".into()); + ServiceType::new("http", ",tcp").expect_err("invalid character: ,".into()); + } + + #[test] + fn to_string_success() { + assert_eq!( + ServiceType::new("http", "tcp").unwrap().to_string(), + "_http._tcp" + ); + } + + #[test] + fn to_string_with_sub_types_success() { + assert_eq!( + ServiceType::with_sub_types("http", "tcp", vec!["api-v1", "api-v2"]) + .unwrap() + .to_string(), + "_http._tcp,_api-v1,_api-v2" + ); + } + + #[test] + fn from_str_success() { + assert_eq!( + ServiceType::from_str("_http._tcp").unwrap(), + ServiceType::new("http", "tcp").unwrap() + ); + } + + #[test] + fn from_str_with_sub_types_success() { + assert_eq!( + ServiceType::from_str("_http._tcp,api-v1,api-v2").unwrap(), + ServiceType::with_sub_types("http", "tcp", vec!["api-v1", "api-v2"]).unwrap() + ); + } +} diff --git a/zeroconf/src/tests/service_test.rs b/zeroconf/src/tests/service_test.rs index c4acd11..4b98230 100644 --- a/zeroconf/src/tests/service_test.rs +++ b/zeroconf/src/tests/service_test.rs @@ -1,5 +1,5 @@ use crate::prelude::*; -use crate::{MdnsBrowser, MdnsService, TxtRecord}; +use crate::{MdnsBrowser, MdnsService, ServiceType, TxtRecord}; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -14,7 +14,7 @@ fn service_register_is_browsable() { } static SERVICE_NAME: &str = "service_register_is_browsable"; - let mut service = MdnsService::new("_http._tcp", 8080); + let mut service = MdnsService::new(ServiceType::new("http", "tcp").unwrap(), 8080); let context: Arc> = Arc::default(); let mut txt = TxtRecord::new();