~liz/yooper

dcfdadfb1420e10b4e6491acd5e6134061abe580 — Ellie Frost 2 years ago 1dd755b
Add module for describing services
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;