M Cargo.lock => Cargo.lock +42 -0
@@ 328,6 328,17 @@ dependencies = [
]
[[package]]
+name = "serde-xml-rs"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)",
+ "thiserror 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)",
+ "xml-rs 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "serde_derive"
version = "1.0.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 338,6 349,25 @@ dependencies = [
]
[[package]]
+name = "serde_with"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_with_macros 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "proc-macro2 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
name = "slab"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ 462,6 492,11 @@ dependencies = [
]
[[package]]
+name = "xml-rs"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
name = "yooper"
version = "0.1.0"
dependencies = [
@@ 470,6 505,9 @@ dependencies = [
"indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"mac_address 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"os_info 2.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde-xml-rs 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_with 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"thiserror 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ 528,7 566,10 @@ dependencies = [
"checksum proc-macro2 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "1502d12e458c49a4c9cbff560d0fe0060c252bc29799ed94ca2ed4bb665a0101"
"checksum quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "54a21852a652ad6f610c9510194f398ff6f8692e334fd1145fed931f7fbe44ea"
"checksum serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)" = "99e7b308464d16b56eba9964e4972a3eee817760ab60d88c3f86e1fecb08204c"
+"checksum serde-xml-rs 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "efe415925cf3d0bbb2fc47d09b56ce03eef51c5d56846468a39bcc293c7a846c"
"checksum serde_derive 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)" = "818fbf6bfa9a42d3bfcaca148547aa00c7b915bec71d1757aa2d44ca68771984"
+"checksum serde_with 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "89d3d595d64120bbbc70b7f6d5ae63298b62a3d9f373ec2f56acf5365ca8a444"
+"checksum serde_with_macros 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4070d2c9b9d258465ad1d82aabb985b84cd9a3afa94da25ece5a9938ba5f1606"
"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
"checksum syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)" = "95b5f192649e48a5302a13f2feb224df883b98933222369e4b3b0fe2a5447269"
"checksum thiserror 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)" = "b13f926965ad00595dd129fa12823b04bbf866e9085ab0a5f2b05b850fbfc344"
@@ 545,3 586,4 @@ dependencies = [
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
+"checksum xml-rs 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a"
M yooper/Cargo.toml => yooper/Cargo.toml +19 -1
@@ 5,6 5,11 @@ authors = ["Ellie Frost <web@stillinbeta.com>"]
edition = "2018"
license = "BSD-3-Clause"
+[features]
+default = ["description"]
+description = ["serde", "serde-xml-rs", "serde_with"]
+
+
[dependencies]
bytes = "0.5.4"
yooper_derive = { path = "../yooper_derive" }
@@ 25,4 30,17 @@ features = ["udp", "codec"]
[dependencies.uuid]
version = "0.8"
-features = ["v1"]>
\ No newline at end of file
+features = ["v1"]
+
+[dependencies.serde]
+version = "1.0"
+optional = true
+features = ["derive"]
+
+[dependencies.serde-xml-rs]
+version = "0.4"
+optional = true
+
+[dependencies.serde_with]
+version = "1.4"
+optional = true<
\ No newline at end of file
A yooper/src/description.rs => yooper/src/description.rs +223 -0
@@ 0,0 1,223 @@
+#[cfg(test)]
+mod tests;
+
+use crate::Error;
+use serde::{Deserialize, Deserializer};
+use serde_with::rust::display_fromstr;
+use std::str::FromStr;
+
+#[derive(PartialEq, Debug)]
+pub struct DeviceType {
+ vendor_domain: Option<String>,
+ device_type: String,
+ version: String,
+}
+
+impl ToString for DeviceType {
+ fn to_string(&self) -> String {
+ format!(
+ "urn:{}:device:{}:{}",
+ self.vendor_domain
+ .as_ref()
+ .map_or("schemas-upnp-org", String::as_ref),
+ self.device_type,
+ self.version,
+ )
+ }
+}
+
+impl FromStr for DeviceType {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s.split(':').collect::<Vec<&str>>().as_slice() {
+ ["urn", "schemas-upnp-org", "device", device_type, version] => Ok(Self {
+ vendor_domain: None,
+ device_type: device_type.to_string(),
+ version: version.to_string(),
+ }),
+
+ ["urn", vendor_domain, "device", device_id, version] => Ok(Self {
+ vendor_domain: Some(vendor_domain.to_string()),
+ device_type: device_id.to_string(),
+ version: version.to_string(),
+ }),
+ _ => Err(Error::MalformedField("service_id", s.to_owned())),
+ }
+ }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct ServiceType {
+ vendor_domain: Option<String>,
+ service_type: String,
+ version: String,
+}
+
+impl FromStr for ServiceType {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s.split(':').collect::<Vec<&str>>().as_slice() {
+ ["urn", "schemas-upnp-org", "service", device_type, version] => Ok(Self {
+ vendor_domain: None,
+ service_type: device_type.to_string(),
+ version: version.to_string(),
+ }),
+
+ ["urn", vendor_domain, "service", service_id, version] => Ok(Self {
+ vendor_domain: Some(vendor_domain.to_string()),
+ service_type: service_id.to_string(),
+ version: version.to_string(),
+ }),
+ _ => Err(Error::MalformedField("service_id", s.to_owned())),
+ }
+ }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct ServiceId {
+ pub vendor_domain: Option<String>,
+ pub service_id: String,
+}
+
+impl FromStr for ServiceId {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s.split(':').collect::<Vec<&str>>().as_slice() {
+ ["urn", "upnp-org", "serviceId", service_id] => Ok(Self {
+ vendor_domain: None,
+ service_id: service_id.to_string(),
+ }),
+
+ ["urn", vendor_domain, "serviceId", service_id] => Ok(Self {
+ vendor_domain: Some(vendor_domain.to_string()),
+ service_id: service_id.to_string(),
+ }),
+ _ => Err(Error::MalformedField("service_id", s.to_owned())),
+ }
+ }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct UniqueDeviceName {
+ pub uuid: String,
+}
+
+impl FromStr for UniqueDeviceName {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ if s.starts_with("uuid:") {
+ Ok(Self {
+ uuid: s[5..].to_owned(),
+ })
+ } else {
+ Err(Error::MalformedField("udn", s.to_owned()))
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Device {
+ #[serde(with = "display_fromstr", rename = "deviceType")]
+ pub device_type: DeviceType,
+ #[serde(rename = "friendlyName")]
+ pub friendly_name: String,
+ pub manufacturer: String,
+ #[serde(rename = "manufacturerURL")]
+ pub manufacturer_url: Option<String>,
+
+ pub model_description: Option<String>,
+ pub model_name: Option<String>,
+ pub model_number: Option<String>,
+ #[serde(rename = "modelURL")]
+ pub model_url: Option<String>,
+
+ pub serial_number: Option<String>,
+ #[serde(with = "display_fromstr", rename = "UDN")]
+ pub unique_device_name: UniqueDeviceName,
+ #[serde(rename = "UPC")]
+ pub upc: Option<String>,
+
+ // TODO(EKF): IconList
+ #[serde(
+ rename = "serviceList",
+ deserialize_with = "deserialize_services",
+ default
+ )]
+ pub services: Vec<Service>,
+ #[serde(
+ rename = "deviceList",
+ deserialize_with = "deserialize_devices",
+ default
+ )]
+ pub devices: Vec<Device>,
+
+ #[serde(rename = "presentationURL")]
+ pub presentation_url: Option<String>,
+}
+
+#[derive(Debug, PartialEq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Service {
+ #[serde(with = "display_fromstr")]
+ pub service_type: ServiceType,
+ #[serde(with = "display_fromstr")]
+ pub service_id: ServiceId,
+
+ #[serde(rename = "SCPDURL")]
+ pub scpd_url: String,
+
+ #[serde(rename = "controlURL")]
+ pub control_url: String,
+ #[serde(rename = "eventSubURL")]
+ pub event_sub_url: String,
+}
+
+#[derive(Debug, PartialEq, Deserialize)]
+pub struct DescriptionDocument {
+ pub root: Description,
+}
+
+#[derive(Debug, PartialEq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Description {
+ pub config_id: Option<String>,
+ pub spec_version: SpecVersion,
+ pub device: Device,
+}
+
+#[derive(Debug, PartialEq, Deserialize)]
+pub struct SpecVersion {
+ pub major: u32,
+ pub minor: u32,
+}
+
+#[derive(Debug, PartialEq, Deserialize)]
+struct ServiceOuter {
+ service: Vec<Service>,
+}
+
+/// Flatten the `service` list down
+fn deserialize_services<'de, D>(d: D) -> Result<Vec<Service>, D::Error>
+where
+ D: Deserializer<'de>,
+{
+ ServiceOuter::deserialize(d).map(|s| s.service)
+}
+
+#[derive(Debug, PartialEq, Deserialize)]
+struct DeviceOuter {
+ device: Vec<Device>,
+}
+
+/// Flatten the `device` list down
+fn deserialize_devices<'de, D>(d: D) -> Result<Vec<Device>, D::Error>
+where
+ D: Deserializer<'de>,
+{
+ DeviceOuter::deserialize(d).map(|d| d.device)
+}
A yooper/src/description/testdata/igd.xml => yooper/src/description/testdata/igd.xml +70 -0
@@ 0,0 1,70 @@
+<?xml version="1.0"?>
+<root xmlns="urn:schemas-upnp-org:device-1-0">
+ <specVersion>
+ <major>1</major>
+ <minor>0</minor>
+ </specVersion>
+ <URLBase>http://192.168.7.1:1900/</URLBase>
+ <device>
+ <deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
+ <friendlyName></friendlyName>
+ <manufacturer></manufacturer>
+ <manufacturerURL></manufacturerURL>
+ <modelDescription></modelDescription>
+ <modelNumber></modelNumber>
+ <modelName></modelName>
+ <UDN>uuid:</UDN>
+ <serviceList>
+ <service>
+ <serviceType>urn:schemas-upnp-org:service:Layer3Forwarding:1</serviceType>
+ <serviceId>urn:upnp-org:serviceId:L3Forwarding1</serviceId>
+ <controlURL>/l3f</controlURL>
+ <eventSubURL>/l3f/events</eventSubURL>
+ <SCPDURL>/l3f.xml</SCPDURL>
+ </service>
+ </serviceList>
+ <deviceList>
+ <device>
+ <deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
+ <friendlyName></friendlyName>
+ <manufacturer></manufacturer>
+ <manufacturerURL></manufacturerURL>
+ <modelDescription></modelDescription>
+ <modelNumber></modelNumber>
+ <modelName></modelName>
+ <UDN>uuid:</UDN>
+ <serviceList>
+ <service>
+ <serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
+ <serviceId>urn:upnp-org:serviceId:WANCommonInterfaceConfig</serviceId>
+ <controlURL>/ifc</controlURL>
+ <eventSubURL>/ifc/events</eventSubURL>
+ <SCPDURL>/ifc.xml</SCPDURL>
+ </service>
+ </serviceList>
+ <deviceList>
+ <device>
+ <deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
+ <friendlyName></friendlyName>
+ <manufacturer></manufacturer>
+ <manufacturerURL></manufacturerURL>
+ <modelDescription></modelDescription>
+ <modelNumber></modelNumber>
+ <modelName></modelName>
+ <UDN>uuid:</UDN>
+ <serviceList>
+ <service>
+ <serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
+ <serviceId>urn:upnp-org:serviceId:WANIPConnection</serviceId>
+ <controlURL>/ipc</controlURL>
+ <eventSubURL>/ipc/events</eventSubURL>
+ <SCPDURL>/ipc.xml</SCPDURL>
+ </service>
+ </serviceList>
+ </device>
+ </deviceList>
+ </device>
+ </deviceList>
+ <presentationURL></presentationURL>
+ </device>
+</root><
\ No newline at end of file
A yooper/src/description/tests.rs => yooper/src/description/tests.rs +147 -0
@@ 0,0 1,147 @@
+use super::*;
+
+const IGD_EXAMPLE: &str = include_str!("testdata/igd.xml");
+
+#[test]
+fn test_device_type_from_str() {
+ let s = "urn:schemas-upnp-org:device:deviceType:ver";
+ assert_eq!(
+ DeviceType {
+ vendor_domain: None,
+ device_type: "deviceType".into(),
+ version: "ver".into()
+ },
+ s.parse().unwrap()
+ );
+
+ let s2 = "urn:domain-name:device:deviceType:ver";
+ assert_eq!(
+ DeviceType {
+ vendor_domain: Some("domain-name".into()),
+ device_type: "deviceType".into(),
+ version: "ver".into()
+ },
+ s2.parse().unwrap()
+ );
+
+ let s3 = "urn:non-matching:service:value";
+ match s3.parse::<DeviceType>().unwrap_err() {
+ Error::MalformedField("service_id", v) if v == s3 => (),
+ e => panic!("Didn't get the error we assumed! {:?}", e),
+ };
+}
+
+#[test]
+fn test_deserialize_with_example() {
+ let expected = Description {
+ config_id: None,
+ spec_version: SpecVersion { major: 1, minor: 0 },
+ device: Device {
+ device_type: DeviceType {
+ vendor_domain: None,
+ device_type: "InternetGatewayDevice".into(),
+ version: "1".into(),
+ },
+ friendly_name: "".into(),
+ manufacturer: "".into(),
+ manufacturer_url: Some("".into()),
+
+ model_description: Some("".into()),
+ model_name: Some("".into()),
+ model_number: Some("".into()),
+ model_url: None,
+
+ serial_number: None,
+ unique_device_name: UniqueDeviceName { uuid: "".into() },
+ upc: None,
+ services: vec![Service {
+ service_type: ServiceType {
+ vendor_domain: None,
+ service_type: "Layer3Forwarding".into(),
+ version: "1".into(),
+ },
+ service_id: ServiceId {
+ vendor_domain: None,
+ service_id: "L3Forwarding1".into(),
+ },
+ scpd_url: "/l3f.xml".into(),
+ control_url: "/l3f".into(),
+ event_sub_url: "/l3f/events".into(),
+ }],
+
+ devices: vec![Device {
+ device_type: DeviceType {
+ vendor_domain: None,
+ device_type: "WANDevice".into(),
+ version: "1".into(),
+ },
+ friendly_name: "".into(),
+ manufacturer: "".into(),
+ manufacturer_url: Some("".into()),
+
+ model_description: Some("".into()),
+ model_name: Some("".into()),
+ model_number: Some("".into()),
+ model_url: None,
+
+ serial_number: None,
+ unique_device_name: UniqueDeviceName { uuid: "".into() },
+ upc: None,
+ services: vec![Service {
+ service_type: ServiceType {
+ vendor_domain: None,
+ service_type: "WANCommonInterfaceConfig".into(),
+ version: "1".into(),
+ },
+ service_id: ServiceId {
+ vendor_domain: None,
+ service_id: "WANCommonInterfaceConfig".into(),
+ },
+ scpd_url: "/ifc.xml".into(),
+ control_url: "/ifc".into(),
+ event_sub_url: "/ifc/events".into(),
+ }],
+
+ devices: vec![Device {
+ device_type: DeviceType {
+ vendor_domain: None,
+ device_type: "WANConnectionDevice".into(),
+ version: "1".into(),
+ },
+ friendly_name: "".into(),
+ manufacturer: "".into(),
+ manufacturer_url: Some("".into()),
+
+ model_description: Some("".into()),
+ model_name: Some("".into()),
+ model_number: Some("".into()),
+ model_url: None,
+
+ serial_number: None,
+ unique_device_name: UniqueDeviceName { uuid: "".into() },
+ upc: None,
+ services: vec![Service {
+ service_type: ServiceType {
+ vendor_domain: None,
+ service_type: "WANIPConnection".into(),
+ version: "1".into(),
+ },
+ service_id: ServiceId {
+ vendor_domain: None,
+ service_id: "WANIPConnection".into(),
+ },
+ scpd_url: "/ipc.xml".into(),
+ control_url: "/ipc".into(),
+ event_sub_url: "/ipc/events".into(),
+ }],
+ devices: vec![],
+ presentation_url: None,
+ }],
+ presentation_url: None,
+ }],
+ presentation_url: Some("".into()),
+ },
+ };
+
+ assert_eq!(expected, serde_xml_rs::from_str(IGD_EXAMPLE).unwrap());
+}
M yooper/src/errors.rs => yooper/src/errors.rs +3 -0
@@ 21,6 21,9 @@ pub enum Error {
#[error("Header {0} had a value we couldn't parse ({1})")]
MalformedHeader(&'static str, String),
+ #[error("Field {0} had a value we couldn't parse ({1})")]
+ MalformedField(&'static str, String),
+
#[error("IO Error {0}")]
IO(#[from] std::io::Error),
M yooper/src/lib.rs => yooper/src/lib.rs +3 -1
@@ 1,5 1,7 @@
+#[cfg(feature = "description")]
+pub mod description;
+pub mod discovery;
mod errors;
pub mod ssdp;
-pub mod discovery;
pub use errors::Error;