1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
//! Description is the second step of UPnP, after [Discovery](../Discovery).
//! Using the location retrieved from discovery, retrieve an XML document over HTTP.
//! This document enumerates the capabilities of the given device.
#[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 {
    /// Will be None for standard devices specified by the UPnP Forum.
    pub vendor_domain: Option<String>,
    pub device_type: String,
    pub 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_owned(),
                version: (*version).to_owned(),
            }),

            ["urn", vendor_domain, "device", device_id, version] => Ok(Self {
                vendor_domain: Some((*vendor_domain).to_owned()),
                device_type: (*device_id).to_owned(),
                version: (*version).to_owned(),
            }),
            _ => Err(Error::MalformedField("service_id", s.to_owned())),
        }
    }
}

#[derive(Debug, PartialEq)]
pub struct ServiceType {
    /// Will be None for standard services specified by the UPnP Forum.
    pub vendor_domain: Option<String>,
    pub service_type: String,
    pub 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 {
    /// Will be None for standard services specified by the UPnP Forum
    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()))
        }
    }
}

/// A Logical device.
/// One physical "Device" may contain multiple logical Devices.
#[derive(Debug, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Device {
    /// UPnP Device Type
    #[serde(with = "display_fromstr", rename = "deviceType")]
    pub device_type: DeviceType,
    /// Short description for end user. Should be < 64 characters.
    #[serde(rename = "friendlyName")]
    pub friendly_name: String,
    /// Manufacturer's name. Should be <64 characters.
    pub manufacturer: String,
    ///Web site for Manufacturer
    #[serde(rename = "manufacturerURL")]
    pub manufacturer_url: Option<String>,

    /// Long description for the end user. Should be < 128 characters
    pub model_description: Option<String>,
    /// Should be < 128 characters
    pub model_name: Option<String>,
    /// Should be < 32 characters
    pub model_number: Option<String>,
    #[serde(rename = "modelURL")]
    pub model_url: Option<String>,

    /// Shourd be < 64 characters
    pub serial_number: Option<String>,
    /// universally-unique identifier for the device.
    /// Shall be the same over time for a specific device.
    /// Shall max the field value of the NT header in discovery messages
    #[serde(with = "display_fromstr", rename = "UDN")]
    pub unique_device_name: UniqueDeviceName,
    #[serde(rename = "UPC")]
    /// Universal product code.
    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")]
    /// A page to display to the end user
    pub presentation_url: Option<String>,
}

/// Logical functional unit, Smallest  units of control.
/// Exposes actions and models the state of a physical device with state variables.
#[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,

    /// URL for service description. Relative to device description URL
    #[serde(rename = "SCPDURL")]
    pub scpd_url: String,

    /// Relative to device description URL
    #[serde(rename = "controlURL")]
    pub control_url: String,
    /// Relative to device description URL
    #[serde(rename = "eventSubURL")]
    pub event_sub_url: String,
}

/// This document contains the root device description and metadata
#[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)
}

/// Retrieve and parse a device description.
/// See the location field from [discovery::Device](../discovery/struct.Device.html#structfield.location).
pub async fn describe(location: &str) -> Result<Device, Error> {
    let body = reqwest::get(location).await?.text().await?;

    let document: Description = serde_xml_rs::from_str(&body)?;
    Ok(document.device)
}