1// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7use try_from::TryFrom;
8
9use std::collections::BTreeMap;
10use std::str::FromStr;
11
12use hashes::Hash;
13use jingle::Creator;
14
15use minidom::{Element, IntoElements, IntoAttributeValue, ElementEmitter};
16use chrono::{DateTime, FixedOffset};
17
18use error::Error;
19use ns;
20
21#[derive(Debug, Clone, PartialEq)]
22pub struct Range {
23 pub offset: u64,
24 pub length: Option<u64>,
25 pub hashes: Vec<Hash>,
26}
27
28impl IntoElements for Range {
29 fn into_elements(self, emitter: &mut ElementEmitter) {
30 let mut elem = Element::builder("range")
31 .ns(ns::JINGLE_FT)
32 .attr("offset", if self.offset == 0 { None } else { Some(self.offset) })
33 .attr("length", self.length)
34 .build();
35 for hash in self.hashes {
36 elem.append_child(hash.into());
37 }
38 emitter.append_child(elem);
39 }
40}
41
42type Lang = String;
43
44generate_id!(Desc);
45
46#[derive(Debug, Clone)]
47pub struct File {
48 pub date: Option<DateTime<FixedOffset>>,
49 pub media_type: Option<String>,
50 pub name: Option<String>,
51 pub descs: BTreeMap<Lang, Desc>,
52 pub size: Option<u64>,
53 pub range: Option<Range>,
54 pub hashes: Vec<Hash>,
55}
56
57#[derive(Debug, Clone)]
58pub struct Description {
59 pub file: File,
60}
61
62#[derive(Debug, Clone)]
63pub struct Checksum {
64 pub name: String,
65 pub creator: Creator,
66 pub file: File,
67}
68
69#[derive(Debug, Clone)]
70pub struct Received {
71 pub name: String,
72 pub creator: Creator,
73}
74
75impl TryFrom<Element> for Received {
76 type Err = Error;
77
78 fn try_from(elem: Element) -> Result<Received, Error> {
79 check_self!(elem, "received", ns::JINGLE_FT);
80 check_no_children!(elem, "received");
81 check_no_unknown_attributes!(elem, "received", ["name", "creator"]);
82 Ok(Received {
83 name: get_attr!(elem, "name", required),
84 creator: get_attr!(elem, "creator", required),
85 })
86 }
87}
88
89impl From<Received> for Element {
90 fn from(received: Received) -> Element {
91 Element::builder("received")
92 .ns(ns::JINGLE_FT)
93 .attr("name", received.name)
94 .attr("creator", received.creator)
95 .build()
96 }
97}
98
99impl TryFrom<Element> for Description {
100 type Err = Error;
101
102 fn try_from(elem: Element) -> Result<Description, Error> {
103 if !elem.is("description", ns::JINGLE_FT) {
104 return Err(Error::ParseError("This is not a JingleFT description element."));
105 }
106 if elem.children().count() != 1 {
107 return Err(Error::ParseError("JingleFT description element must have exactly one child."));
108 }
109
110 let mut date = None;
111 let mut media_type = None;
112 let mut name = None;
113 let mut descs = BTreeMap::new();
114 let mut size = None;
115 let mut range = None;
116 let mut hashes = vec!();
117 for description_payload in elem.children() {
118 if !description_payload.is("file", ns::JINGLE_FT) {
119 return Err(Error::ParseError("Unknown element in JingleFT description."));
120 }
121 for file_payload in description_payload.children() {
122 if file_payload.is("date", ns::JINGLE_FT) {
123 if date.is_some() {
124 return Err(Error::ParseError("File must not have more than one date."));
125 }
126 date = Some(file_payload.text().parse()?);
127 } else if file_payload.is("media-type", ns::JINGLE_FT) {
128 if media_type.is_some() {
129 return Err(Error::ParseError("File must not have more than one media-type."));
130 }
131 media_type = Some(file_payload.text());
132 } else if file_payload.is("name", ns::JINGLE_FT) {
133 if name.is_some() {
134 return Err(Error::ParseError("File must not have more than one name."));
135 }
136 name = Some(file_payload.text());
137 } else if file_payload.is("desc", ns::JINGLE_FT) {
138 let lang = get_attr!(file_payload, "xml:lang", default);
139 let desc = Desc(file_payload.text());
140 if descs.insert(lang, desc).is_some() {
141 return Err(Error::ParseError("Desc element present twice for the same xml:lang."));
142 }
143 } else if file_payload.is("size", ns::JINGLE_FT) {
144 if size.is_some() {
145 return Err(Error::ParseError("File must not have more than one size."));
146 }
147 size = Some(file_payload.text().parse()?);
148 } else if file_payload.is("range", ns::JINGLE_FT) {
149 if range.is_some() {
150 return Err(Error::ParseError("File must not have more than one range."));
151 }
152 let offset = get_attr!(file_payload, "offset", default);
153 let length = get_attr!(file_payload, "length", optional);
154 let mut range_hashes = vec!();
155 for hash_element in file_payload.children() {
156 if !hash_element.is("hash", ns::HASHES) {
157 return Err(Error::ParseError("Unknown element in JingleFT range."));
158 }
159 range_hashes.push(Hash::try_from(hash_element.clone())?);
160 }
161 range = Some(Range {
162 offset: offset,
163 length: length,
164 hashes: range_hashes,
165 });
166 } else if file_payload.is("hash", ns::HASHES) {
167 hashes.push(Hash::try_from(file_payload.clone())?);
168 } else {
169 return Err(Error::ParseError("Unknown element in JingleFT file."));
170 }
171 }
172 }
173
174 Ok(Description {
175 file: File {
176 date: date,
177 media_type: media_type,
178 name: name,
179 descs: descs,
180 size: size,
181 range: range,
182 hashes: hashes,
183 },
184 })
185 }
186}
187
188impl From<File> for Element {
189 fn from(file: File) -> Element {
190 let mut root = Element::builder("file")
191 .ns(ns::JINGLE_FT)
192 .build();
193 if let Some(date) = file.date {
194 root.append_child(Element::builder("date")
195 .ns(ns::JINGLE_FT)
196 .append(date.to_rfc3339())
197 .build());
198 }
199 if let Some(media_type) = file.media_type {
200 root.append_child(Element::builder("media-type")
201 .ns(ns::JINGLE_FT)
202 .append(media_type)
203 .build());
204 }
205 if let Some(name) = file.name {
206 root.append_child(Element::builder("name")
207 .ns(ns::JINGLE_FT)
208 .append(name)
209 .build());
210 }
211 for (lang, desc) in file.descs.into_iter() {
212 root.append_child(Element::builder("desc")
213 .ns(ns::JINGLE_FT)
214 .attr("xml:lang", lang)
215 .append(desc.0)
216 .build());
217 }
218 if let Some(size) = file.size {
219 root.append_child(Element::builder("size")
220 .ns(ns::JINGLE_FT)
221 .append(format!("{}", size))
222 .build());
223 }
224 if let Some(range) = file.range {
225 root.append_child(Element::builder("range")
226 .ns(ns::JINGLE_FT)
227 .append(range)
228 .build());
229 }
230 for hash in file.hashes {
231 root.append_child(hash.into());
232 }
233 root
234 }
235}
236
237impl From<Description> for Element {
238 fn from(description: Description) -> Element {
239 let file: Element = description.file.into();
240 Element::builder("description")
241 .ns(ns::JINGLE_FT)
242 .append(file)
243 .build()
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use hashes::Algo;
251 use base64;
252
253 #[test]
254 fn test_description() {
255 let elem: Element = r#"
256<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
257 <file>
258 <media-type>text/plain</media-type>
259 <name>test.txt</name>
260 <date>2015-07-26T21:46:00+01:00</date>
261 <size>6144</size>
262 <hash xmlns='urn:xmpp:hashes:2'
263 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
264 </file>
265</description>
266"#.parse().unwrap();
267 let desc = Description::try_from(elem).unwrap();
268 assert_eq!(desc.file.media_type, Some(String::from("text/plain")));
269 assert_eq!(desc.file.name, Some(String::from("test.txt")));
270 assert_eq!(desc.file.descs, BTreeMap::new());
271 assert_eq!(desc.file.date, Some(DateTime::parse_from_rfc3339("2015-07-26T21:46:00+01:00").unwrap()));
272 assert_eq!(desc.file.size, Some(6144u64));
273 assert_eq!(desc.file.range, None);
274 assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);
275 assert_eq!(desc.file.hashes[0].hash, base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap());
276 }
277
278 #[test]
279 fn test_request() {
280 let elem: Element = r#"
281<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
282 <file>
283 <hash xmlns='urn:xmpp:hashes:2'
284 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
285 </file>
286</description>
287"#.parse().unwrap();
288 let desc = Description::try_from(elem).unwrap();
289 assert_eq!(desc.file.media_type, None);
290 assert_eq!(desc.file.name, None);
291 assert_eq!(desc.file.descs, BTreeMap::new());
292 assert_eq!(desc.file.date, None);
293 assert_eq!(desc.file.size, None);
294 assert_eq!(desc.file.range, None);
295 assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);
296 assert_eq!(desc.file.hashes[0].hash, base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap());
297 }
298
299 #[test]
300 fn test_descs() {
301 let elem: Element = r#"
302<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
303 <file>
304 <media-type>text/plain</media-type>
305 <desc xml:lang='fr'>Fichier secret !</desc>
306 <desc xml:lang='en'>Secret file!</desc>
307 <hash xmlns='urn:xmpp:hashes:2'
308 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
309 </file>
310</description>
311"#.parse().unwrap();
312 let desc = Description::try_from(elem).unwrap();
313 assert_eq!(desc.file.descs.keys().cloned().collect::<Vec<_>>(), ["en", "fr"]);
314 assert_eq!(desc.file.descs["en"], Desc(String::from("Secret file!")));
315 assert_eq!(desc.file.descs["fr"], Desc(String::from("Fichier secret !")));
316
317 let elem: Element = r#"
318<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
319 <file>
320 <media-type>text/plain</media-type>
321 <desc xml:lang='fr'>Fichier secret !</desc>
322 <desc xml:lang='fr'>Secret file!</desc>
323 <hash xmlns='urn:xmpp:hashes:2'
324 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
325 </file>
326</description>
327"#.parse().unwrap();
328 let error = Description::try_from(elem).unwrap_err();
329 let message = match error {
330 Error::ParseError(string) => string,
331 _ => panic!(),
332 };
333 assert_eq!(message, "Desc element present twice for the same xml:lang.");
334 }
335
336 #[test]
337 fn test_received() {
338 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'/>".parse().unwrap();
339 let received = Received::try_from(elem).unwrap();
340 assert_eq!(received.name, String::from("coucou"));
341 assert_eq!(received.creator, Creator::Initiator);
342 let elem2 = Element::from(received.clone());
343 let received2 = Received::try_from(elem2).unwrap();
344 assert_eq!(received2.name, String::from("coucou"));
345 assert_eq!(received2.creator, Creator::Initiator);
346
347 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><coucou/></received>".parse().unwrap();
348 let error = Received::try_from(elem).unwrap_err();
349 let message = match error {
350 Error::ParseError(string) => string,
351 _ => panic!(),
352 };
353 assert_eq!(message, "Unknown child in received element.");
354
355 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator' coucou=''/>".parse().unwrap();
356 let error = Received::try_from(elem).unwrap_err();
357 let message = match error {
358 Error::ParseError(string) => string,
359 _ => panic!(),
360 };
361 assert_eq!(message, "Unknown attribute in received element.");
362
363 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' creator='initiator'/>".parse().unwrap();
364 let error = Received::try_from(elem).unwrap_err();
365 let message = match error {
366 Error::ParseError(string) => string,
367 _ => panic!(),
368 };
369 assert_eq!(message, "Required attribute 'name' missing.");
370
371 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='coucou'/>".parse().unwrap();
372 let error = Received::try_from(elem).unwrap_err();
373 let message = match error {
374 Error::ParseError(string) => string,
375 _ => panic!(),
376 };
377 assert_eq!(message, "Unknown value for 'creator' attribute.");
378 }
379}