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;
8use std::str::FromStr;
9
10use std::collections::BTreeMap;
11
12use hashes::Hash;
13use jingle::{Creator, ContentId};
14use date::DateTime;
15
16use minidom::Element;
17
18use error::Error;
19use ns;
20
21generate_element!(
22 #[derive(PartialEq, Default)]
23 Range, "range", JINGLE_FT,
24 attributes: [
25 offset: u64 = "offset" => default,
26 length: Option<u64> = "length" => optional
27 ],
28 children: [
29 hashes: Vec<Hash> = ("hash", HASHES) => Hash
30 ]
31);
32
33impl Range {
34 pub fn new() -> Range {
35 Default::default()
36 }
37}
38
39type Lang = String;
40
41generate_id!(Desc);
42
43#[derive(Debug, Clone)]
44pub struct File {
45 pub date: Option<DateTime>,
46 pub media_type: Option<String>,
47 pub name: Option<String>,
48 pub descs: BTreeMap<Lang, Desc>,
49 pub size: Option<u64>,
50 pub range: Option<Range>,
51 pub hashes: Vec<Hash>,
52}
53
54impl File {
55 pub fn new() -> File {
56 File {
57 date: None,
58 media_type: None,
59 name: None,
60 descs: BTreeMap::new(),
61 size: None,
62 range: None,
63 hashes: Vec::new(),
64 }
65 }
66
67 pub fn with_date(mut self, date: DateTime) -> File {
68 self.date = Some(date);
69 self
70 }
71
72 pub fn with_date_str(mut self, date: &str) -> Result<File, Error> {
73 self.date = Some(DateTime::from_str(date)?);
74 Ok(self)
75 }
76
77 pub fn with_media_type(mut self, media_type: String) -> File {
78 self.media_type = Some(media_type);
79 self
80 }
81
82 pub fn with_name(mut self, name: String) -> File {
83 self.name = Some(name);
84 self
85 }
86
87 pub fn add_desc(mut self, lang: &str, desc: Desc) -> File {
88 self.descs.insert(Lang::from(lang), desc);
89 self
90 }
91
92 pub fn with_size(mut self, size: u64) -> File {
93 self.size = Some(size);
94 self
95 }
96
97 pub fn with_range(mut self, range: Range) -> File {
98 self.range = Some(range);
99 self
100 }
101
102 pub fn add_hash(mut self, hash: Hash) -> File {
103 self.hashes.push(hash);
104 self
105 }
106}
107
108impl TryFrom<Element> for File {
109 type Err = Error;
110
111 fn try_from(elem: Element) -> Result<File, Error> {
112 check_self!(elem, "file", JINGLE_FT);
113 check_no_attributes!(elem, "file");
114
115 let mut file = File {
116 date: None,
117 media_type: None,
118 name: None,
119 descs: BTreeMap::new(),
120 size: None,
121 range: None,
122 hashes: vec!(),
123 };
124
125 for child in elem.children() {
126 if child.is("date", ns::JINGLE_FT) {
127 if file.date.is_some() {
128 return Err(Error::ParseError("File must not have more than one date."));
129 }
130 file.date = Some(child.text().parse()?);
131 } else if child.is("media-type", ns::JINGLE_FT) {
132 if file.media_type.is_some() {
133 return Err(Error::ParseError("File must not have more than one media-type."));
134 }
135 file.media_type = Some(child.text());
136 } else if child.is("name", ns::JINGLE_FT) {
137 if file.name.is_some() {
138 return Err(Error::ParseError("File must not have more than one name."));
139 }
140 file.name = Some(child.text());
141 } else if child.is("desc", ns::JINGLE_FT) {
142 let lang = get_attr!(child, "xml:lang", default);
143 let desc = Desc(child.text());
144 if file.descs.insert(lang, desc).is_some() {
145 return Err(Error::ParseError("Desc element present twice for the same xml:lang."));
146 }
147 } else if child.is("size", ns::JINGLE_FT) {
148 if file.size.is_some() {
149 return Err(Error::ParseError("File must not have more than one size."));
150 }
151 file.size = Some(child.text().parse()?);
152 } else if child.is("range", ns::JINGLE_FT) {
153 if file.range.is_some() {
154 return Err(Error::ParseError("File must not have more than one range."));
155 }
156 file.range = Some(Range::try_from(child.clone())?);
157 } else if child.is("hash", ns::HASHES) {
158 file.hashes.push(Hash::try_from(child.clone())?);
159 } else {
160 return Err(Error::ParseError("Unknown element in JingleFT file."));
161 }
162 }
163
164 Ok(file)
165 }
166}
167
168impl From<File> for Element {
169 fn from(file: File) -> Element {
170 let mut root = Element::builder("file")
171 .ns(ns::JINGLE_FT);
172 if let Some(date) = file.date {
173 root = root.append(Element::builder("date")
174 .ns(ns::JINGLE_FT)
175 .append(date)
176 .build());
177 }
178 if let Some(media_type) = file.media_type {
179 root = root.append(Element::builder("media-type")
180 .ns(ns::JINGLE_FT)
181 .append(media_type)
182 .build());
183 }
184 if let Some(name) = file.name {
185 root = root.append(Element::builder("name")
186 .ns(ns::JINGLE_FT)
187 .append(name)
188 .build());
189 }
190 for (lang, desc) in file.descs.into_iter() {
191 root = root.append(Element::builder("desc")
192 .ns(ns::JINGLE_FT)
193 .attr("xml:lang", lang)
194 .append(desc.0)
195 .build());
196 }
197 if let Some(size) = file.size {
198 root = root.append(Element::builder("size")
199 .ns(ns::JINGLE_FT)
200 .append(format!("{}", size))
201 .build());
202 }
203 if let Some(range) = file.range {
204 root = root.append(range);
205 }
206 for hash in file.hashes {
207 root = root.append(hash);
208 }
209 root.build()
210 }
211}
212#[derive(Debug, Clone)]
213pub struct Description {
214 pub file: File,
215}
216
217impl TryFrom<Element> for Description {
218 type Err = Error;
219
220 fn try_from(elem: Element) -> Result<Description, Error> {
221 check_self!(elem, "description", JINGLE_FT, "JingleFT description");
222 check_no_attributes!(elem, "JingleFT description");
223 let mut file = None;
224 for child in elem.children() {
225 if file.is_some() {
226 return Err(Error::ParseError("JingleFT description element must have exactly one child."));
227 }
228 file = Some(File::try_from(child.clone())?);
229 }
230 if file.is_none() {
231 return Err(Error::ParseError("JingleFT description element must have exactly one child."));
232 }
233 Ok(Description {
234 file: file.unwrap(),
235 })
236 }
237}
238
239impl From<Description> for Element {
240 fn from(description: Description) -> Element {
241 Element::builder("description")
242 .ns(ns::JINGLE_FT)
243 .append(description.file)
244 .build()
245 }
246}
247
248#[derive(Debug, Clone)]
249pub struct Checksum {
250 pub name: ContentId,
251 pub creator: Creator,
252 pub file: File,
253}
254
255impl TryFrom<Element> for Checksum {
256 type Err = Error;
257
258 fn try_from(elem: Element) -> Result<Checksum, Error> {
259 check_self!(elem, "checksum", JINGLE_FT);
260 check_no_unknown_attributes!(elem, "checksum", ["name", "creator"]);
261 let mut file = None;
262 for child in elem.children() {
263 if file.is_some() {
264 return Err(Error::ParseError("JingleFT checksum element must have exactly one child."));
265 }
266 file = Some(File::try_from(child.clone())?);
267 }
268 if file.is_none() {
269 return Err(Error::ParseError("JingleFT checksum element must have exactly one child."));
270 }
271 Ok(Checksum {
272 name: get_attr!(elem, "name", required),
273 creator: get_attr!(elem, "creator", required),
274 file: file.unwrap(),
275 })
276 }
277}
278
279impl From<Checksum> for Element {
280 fn from(checksum: Checksum) -> Element {
281 Element::builder("checksum")
282 .ns(ns::JINGLE_FT)
283 .attr("name", checksum.name)
284 .attr("creator", checksum.creator)
285 .append(checksum.file)
286 .build()
287 }
288}
289
290generate_element!(Received, "received", JINGLE_FT,
291attributes: [
292 name: ContentId = "name" => required,
293 creator: Creator = "creator" => required,
294]);
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use hashes::Algo;
300 use base64;
301
302 #[test]
303 fn test_description() {
304 let elem: Element = r#"
305<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
306 <file>
307 <media-type>text/plain</media-type>
308 <name>test.txt</name>
309 <date>2015-07-26T21:46:00+01:00</date>
310 <size>6144</size>
311 <hash xmlns='urn:xmpp:hashes:2'
312 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
313 </file>
314</description>
315"#.parse().unwrap();
316 let desc = Description::try_from(elem).unwrap();
317 assert_eq!(desc.file.media_type, Some(String::from("text/plain")));
318 assert_eq!(desc.file.name, Some(String::from("test.txt")));
319 assert_eq!(desc.file.descs, BTreeMap::new());
320 assert_eq!(desc.file.date, DateTime::from_str("2015-07-26T21:46:00+01:00").ok());
321 assert_eq!(desc.file.size, Some(6144u64));
322 assert_eq!(desc.file.range, None);
323 assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);
324 assert_eq!(desc.file.hashes[0].hash, base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap());
325 }
326
327 #[test]
328 fn test_request() {
329 let elem: Element = r#"
330<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
331 <file>
332 <hash xmlns='urn:xmpp:hashes:2'
333 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
334 </file>
335</description>
336"#.parse().unwrap();
337 let desc = Description::try_from(elem).unwrap();
338 assert_eq!(desc.file.media_type, None);
339 assert_eq!(desc.file.name, None);
340 assert_eq!(desc.file.descs, BTreeMap::new());
341 assert_eq!(desc.file.date, None);
342 assert_eq!(desc.file.size, None);
343 assert_eq!(desc.file.range, None);
344 assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);
345 assert_eq!(desc.file.hashes[0].hash, base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap());
346 }
347
348 #[test]
349 fn test_descs() {
350 let elem: Element = r#"
351<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
352 <file>
353 <media-type>text/plain</media-type>
354 <desc xml:lang='fr'>Fichier secret !</desc>
355 <desc xml:lang='en'>Secret file!</desc>
356 <hash xmlns='urn:xmpp:hashes:2'
357 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
358 </file>
359</description>
360"#.parse().unwrap();
361 let desc = Description::try_from(elem).unwrap();
362 assert_eq!(desc.file.descs.keys().cloned().collect::<Vec<_>>(), ["en", "fr"]);
363 assert_eq!(desc.file.descs["en"], Desc(String::from("Secret file!")));
364 assert_eq!(desc.file.descs["fr"], Desc(String::from("Fichier secret !")));
365
366 let elem: Element = r#"
367<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
368 <file>
369 <media-type>text/plain</media-type>
370 <desc xml:lang='fr'>Fichier secret !</desc>
371 <desc xml:lang='fr'>Secret file!</desc>
372 <hash xmlns='urn:xmpp:hashes:2'
373 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
374 </file>
375</description>
376"#.parse().unwrap();
377 let error = Description::try_from(elem).unwrap_err();
378 let message = match error {
379 Error::ParseError(string) => string,
380 _ => panic!(),
381 };
382 assert_eq!(message, "Desc element present twice for the same xml:lang.");
383 }
384
385 #[test]
386 fn test_received() {
387 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'/>".parse().unwrap();
388 let received = Received::try_from(elem).unwrap();
389 assert_eq!(received.name, ContentId(String::from("coucou")));
390 assert_eq!(received.creator, Creator::Initiator);
391 let elem2 = Element::from(received.clone());
392 let received2 = Received::try_from(elem2).unwrap();
393 assert_eq!(received2.name, ContentId(String::from("coucou")));
394 assert_eq!(received2.creator, Creator::Initiator);
395
396 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><coucou/></received>".parse().unwrap();
397 let error = Received::try_from(elem).unwrap_err();
398 let message = match error {
399 Error::ParseError(string) => string,
400 _ => panic!(),
401 };
402 assert_eq!(message, "Unknown child in received element.");
403
404 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator' coucou=''/>".parse().unwrap();
405 let error = Received::try_from(elem).unwrap_err();
406 let message = match error {
407 Error::ParseError(string) => string,
408 _ => panic!(),
409 };
410 assert_eq!(message, "Unknown attribute in received element.");
411
412 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' creator='initiator'/>".parse().unwrap();
413 let error = Received::try_from(elem).unwrap_err();
414 let message = match error {
415 Error::ParseError(string) => string,
416 _ => panic!(),
417 };
418 assert_eq!(message, "Required attribute 'name' missing.");
419
420 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='coucou'/>".parse().unwrap();
421 let error = Received::try_from(elem).unwrap_err();
422 let message = match error {
423 Error::ParseError(string) => string,
424 _ => panic!(),
425 };
426 assert_eq!(message, "Unknown value for 'creator' attribute.");
427 }
428
429 #[test]
430 fn test_checksum() {
431 let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
432 let hash = vec!(195, 73, 156, 39, 41, 115, 10, 127, 128, 126, 251, 134, 118, 169, 45, 203, 111, 138, 63, 143);
433 let checksum = Checksum::try_from(elem).unwrap();
434 assert_eq!(checksum.name, ContentId(String::from("coucou")));
435 assert_eq!(checksum.creator, Creator::Initiator);
436 assert_eq!(checksum.file.hashes, vec!(Hash { algo: Algo::Sha_1, hash: hash.clone() }));
437 let elem2 = Element::from(checksum);
438 let checksum2 = Checksum::try_from(elem2).unwrap();
439 assert_eq!(checksum2.name, ContentId(String::from("coucou")));
440 assert_eq!(checksum2.creator, Creator::Initiator);
441 assert_eq!(checksum2.file.hashes, vec!(Hash { algo: Algo::Sha_1, hash: hash.clone() }));
442
443 let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><coucou/></checksum>".parse().unwrap();
444 let error = Checksum::try_from(elem).unwrap_err();
445 let message = match error {
446 Error::ParseError(string) => string,
447 _ => panic!(),
448 };
449 assert_eq!(message, "This is not a file element.");
450
451 let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator' coucou=''><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
452 let error = Checksum::try_from(elem).unwrap_err();
453 let message = match error {
454 Error::ParseError(string) => string,
455 _ => panic!(),
456 };
457 assert_eq!(message, "Unknown attribute in checksum element.");
458
459 let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' creator='initiator'><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
460 let error = Checksum::try_from(elem).unwrap_err();
461 let message = match error {
462 Error::ParseError(string) => string,
463 _ => panic!(),
464 };
465 assert_eq!(message, "Required attribute 'name' missing.");
466
467 let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='coucou'><file><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash></file></checksum>".parse().unwrap();
468 let error = Checksum::try_from(elem).unwrap_err();
469 let message = match error {
470 Error::ParseError(string) => string,
471 _ => panic!(),
472 };
473 assert_eq!(message, "Unknown value for 'creator' attribute.");
474 }
475
476 #[test]
477 fn test_range() {
478 let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5'/>".parse().unwrap();
479 let range = Range::try_from(elem).unwrap();
480 assert_eq!(range.offset, 0);
481 assert_eq!(range.length, None);
482 assert_eq!(range.hashes, vec!());
483
484 let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5' offset='2048' length='1024'><hash xmlns='urn:xmpp:hashes:2' algo='sha-1'>kHp5RSzW/h7Gm1etSf90Mr5PC/k=</hash></range>".parse().unwrap();
485 let hashes = vec!(Hash { algo: Algo::Sha_1, hash: vec!(144, 122, 121, 69, 44, 214, 254, 30, 198, 155, 87, 173, 73, 255, 116, 50, 190, 79, 11, 249) });
486 let range = Range::try_from(elem).unwrap();
487 assert_eq!(range.offset, 2048);
488 assert_eq!(range.length, Some(1024));
489 assert_eq!(range.hashes, hashes);
490 let elem2 = Element::from(range);
491 let range2 = Range::try_from(elem2).unwrap();
492 assert_eq!(range2.offset, 2048);
493 assert_eq!(range2.length, Some(1024));
494 assert_eq!(range2.hashes, hashes);
495
496 let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5' coucou=''/>".parse().unwrap();
497 let error = Range::try_from(elem).unwrap_err();
498 let message = match error {
499 Error::ParseError(string) => string,
500 _ => panic!(),
501 };
502 assert_eq!(message, "Unknown attribute in range element.");
503 }
504}