Skip to main content

tiny_dlna/
xml.rs

1//! SOAP / UPnP XML documents sent and served by tiny-dlna.
2
3pub const AVTRANSPORT_SCPD: &str = include_str!("../xml/AVTransport.xml");
4pub const RENDERING_CONTROL_SCPD: &str = include_str!("../xml/RenderingControl.xml");
5pub const CONNECTION_MANAGER_SCPD: &str = include_str!("../xml/ConnectionManager.xml");
6
7/// Escape text for use inside an XML element or attribute.
8pub fn escape(s: &str) -> String {
9    let mut out = String::with_capacity(s.len());
10    for c in s.chars() {
11        match c {
12            '&' => out.push_str("&"),
13            '<' => out.push_str("&lt;"),
14            '>' => out.push_str("&gt;"),
15            '"' => out.push_str("&quot;"),
16            _ => out.push(c),
17        }
18    }
19    out
20}
21
22/// Resolve entity references (`&amp;`, `&#38;`, `&#x26;`, ...).
23/// Unknown entities are kept as they are.
24pub fn unescape(s: &str) -> String {
25    let mut out = String::with_capacity(s.len());
26    let mut rest = s;
27    while let Some(pos) = rest.find('&') {
28        out.push_str(&rest[..pos]);
29        rest = &rest[pos..];
30        let decoded = rest[1..].find(';').filter(|&n| n <= 10).and_then(|n| {
31            let name = &rest[1..n + 1];
32            let c = match name {
33                "amp" => Some('&'),
34                "lt" => Some('<'),
35                "gt" => Some('>'),
36                "quot" => Some('"'),
37                "apos" => Some('\''),
38                "nbsp" => Some('\u{a0}'),
39                _ => {
40                    let code =
41                        if let Some(hex) = name.strip_prefix("#x").or(name.strip_prefix("#X")) {
42                            u32::from_str_radix(hex, 16).ok()
43                        } else if let Some(dec) = name.strip_prefix('#') {
44                            dec.parse().ok()
45                        } else {
46                            None
47                        };
48                    code.and_then(char::from_u32)
49                }
50            };
51            c.map(|c| (c, n + 2))
52        });
53        match decoded {
54            Some((c, len)) => {
55                out.push(c);
56                rest = &rest[len..];
57            }
58            None => {
59                out.push('&');
60                rest = &rest[1..];
61            }
62        }
63    }
64    out.push_str(rest);
65    out
66}
67
68/// DIDL-Lite metadata describing a video (and optional subtitle).
69pub fn didl_metadata(title: &str, url_video: &str, url_srt: Option<&str>) -> String {
70    let subtitle = match url_srt {
71        Some(srt) => {
72            let srt = escape(srt);
73            format!(
74                r#"
75    <res protocolInfo="http-get:*:text/srt:*">{srt}</res>
76    <sec:CaptionInfo sec:type="srt">{srt}</sec:CaptionInfo>
77    <sec:CaptionInfoEx sec:type="srt">{srt}</sec:CaptionInfoEx>
78    <pv:subtitleFileUri>{srt}</pv:subtitleFileUri>"#
79            )
80        }
81        None => String::new(),
82    };
83    format!(
84        r#"<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"
85  xmlns:dc="http://purl.org/dc/elements/1.1/"
86  xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
87  xmlns:sec="http://www.sec.co.kr/"
88  xmlns:pv="http://www.pv.com/pvns/">
89  <item id="0" parentID="-1" restricted="false">
90    <dc:title>{title}</dc:title>
91    <upnp:class>object.item.videoItem</upnp:class>
92    <res protocolInfo="http-get:*:video/mp4:DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01700000000000000000000000000000">{url_video}</res>{subtitle}
93  </item>
94</DIDL-Lite>"#,
95        title = escape(title),
96        url_video = escape(url_video),
97    )
98}
99
100pub fn set_av_transport_uri(url_video: &str, metadata: &str) -> String {
101    format!(
102        r#"<?xml version='1.0' encoding='utf-8'?>
103<s:Envelope
104    s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
105    xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
106  <s:Body>
107    <u:SetAVTransportURI xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
108      <InstanceID>0</InstanceID>
109      <CurrentURI>{}</CurrentURI>
110      <CurrentURIMetaData>{}</CurrentURIMetaData>
111    </u:SetAVTransportURI>
112  </s:Body>
113</s:Envelope>
114"#,
115        escape(url_video),
116        escape(metadata),
117    )
118}
119
120pub const PLAY: &str = r#"<?xml version='1.0' encoding='utf-8'?>
121<s:Envelope
122    xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
123    s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
124  <s:Body>
125    <u:Play xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
126      <InstanceID>0</InstanceID>
127      <Speed>1</Speed>
128    </u:Play>
129  </s:Body>
130</s:Envelope>
131"#;
132
133pub const STOP: &str = r#"<?xml version='1.0' encoding='utf-8'?>
134<s:Envelope
135    xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
136    s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
137  <s:Body>
138    <u:Stop xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
139      <InstanceID>0</InstanceID>
140    </u:Stop>
141  </s:Body>
142</s:Envelope>
143"#;
144
145pub fn seek(target: &str) -> String {
146    format!(
147        r#"<?xml version="1.0" encoding="utf-8"?>
148<s:Envelope
149    xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
150    s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
151  <s:Body>
152    <u:Seek xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
153      <InstanceID>0</InstanceID>
154      <Unit>REL_TIME</Unit>
155      <Target>{}</Target>
156    </u:Seek>
157  </s:Body>
158</s:Envelope>
159"#,
160        escape(target)
161    )
162}
163
164/// Device description served at `/description.xml`.
165pub fn description(friendly_name: &str, uuid: &str) -> String {
166    format!(
167        r#"<?xml version="1.0" encoding="UTF-8"?>
168<root xmlns:dlna="urn:schemas-dlna-org:device-1-0" xmlns="urn:schemas-upnp-org:device-1-0">
169  <specVersion>
170    <major>1</major>
171    <minor>0</minor>
172  </specVersion>
173  <device>
174    <deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
175    <friendlyName>{}</friendlyName>
176    <UDN>uuid:{}</UDN>
177    <manufacturer>mitnk</manufacturer>
178    <modelName>Tiny-Render</modelName>
179    <modelDescription>AVTransport Media Renderer</modelDescription>
180    <dlna:X_DLNADOC xmlns:dlna="urn:schemas-dlna-org:device-1-0">DMR-1.50</dlna:X_DLNADOC>
181    <serviceList>
182      <service>
183        <serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
184        <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
185        <controlURL>AVTransport/control</controlURL>
186        <SCPDURL>dlna/AVTransport.xml</SCPDURL>
187      </service>
188      <service>
189        <serviceType>urn:schemas-upnp-org:service:RenderingControl:1</serviceType>
190        <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
191        <controlURL>RenderingControl/action</controlURL>
192        <eventSubURL>RenderingControl/event</eventSubURL>
193        <SCPDURL>dlna/RenderingControl.xml</SCPDURL>
194      </service>
195      <service>
196        <serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>
197        <serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>
198        <controlURL>ConnectionManager/action</controlURL>
199        <eventSubURL>ConnectionManager/event</eventSubURL>
200        <SCPDURL>dlna/ConnectionManager.xml</SCPDURL>
201      </service>
202    </serviceList>
203  </device>
204</root>"#,
205        escape(friendly_name),
206        escape(uuid),
207    )
208}
209
210pub const SET_AV_TRANSPORT_URI_RESPONSE: &str = r#"
211<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
212  <s:Body>
213    <u:SetAVTransportURIResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
214  </s:Body>
215</s:Envelope>
216"#;
217
218pub const PLAY_RESPONSE: &str = r#"
219<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
220  <s:Body>
221    <u:PlayResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
222  </s:Body>
223</s:Envelope>
224"#;
225
226pub const SEEK_RESPONSE: &str = r#"
227<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
228  <s:Body>
229    <u:SeekResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
230  </s:Body>
231</s:Envelope>
232"#;
233
234pub const STOP_RESPONSE: &str = r#"
235<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
236  <s:Body>
237    <u:StopResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/>
238  </s:Body>
239</s:Envelope>
240"#;
241
242pub const TRANSPORT_INFO_RESPONSE: &str = r#"
243<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
244  <s:Body>
245    <u:GetTransportInfoResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
246      <CurrentTransportState>PLAYING</CurrentTransportState>
247      <CurrentTransportStatus>OK</CurrentTransportStatus>
248      <CurrentSpeed>1</CurrentSpeed>
249    </u:GetTransportInfoResponse>
250  </s:Body>
251</s:Envelope>
252"#;
253
254pub fn position_info_response(rel_time: &str) -> String {
255    format!(
256        r#"
257<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
258  <s:Body>
259    <u:GetPositionInfoResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
260      <Track>0</Track>
261      <TrackDuration>01:23:45</TrackDuration>
262      <RelTime>{rel_time}</RelTime>
263      <AbsTime>{rel_time}</AbsTime>
264      <RelCount>2147483647</RelCount>
265      <AbsCount>2147483647</AbsCount>
266    </u:GetPositionInfoResponse>
267  </s:Body>
268</s:Envelope>
269"#
270    )
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    #[test]
278    fn escape_roundtrip() {
279        let s = r#"a&b<c>"d"#;
280        assert_eq!(escape(s), "a&amp;b&lt;c&gt;&quot;d");
281        assert_eq!(unescape(&escape(s)), s);
282    }
283
284    #[test]
285    fn unescape_numeric_and_unknown() {
286        assert_eq!(unescape("&#38;&#x3C;&foo; & x"), "&<&foo; & x");
287    }
288
289    #[test]
290    fn static_documents_parse() {
291        for doc in [
292            AVTRANSPORT_SCPD,
293            RENDERING_CONTROL_SCPD,
294            CONNECTION_MANAGER_SCPD,
295        ] {
296            roxmltree::Document::parse(doc).unwrap();
297        }
298        roxmltree::Document::parse(&description("A & B <x>", "u-1")).unwrap();
299        let meta = didl_metadata("t&t", "http://h/v?a=1&b=2", Some("http://h/s.srt"));
300        roxmltree::Document::parse(&meta).unwrap();
301        roxmltree::Document::parse(&set_av_transport_uri("http://h/v?a=1&b=2", &meta)).unwrap();
302    }
303}