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 crate::date::DateTime;
8use crate::util::error::Error;
9use crate::hashes::Hash;
10use crate::jingle::{ContentId, Creator};
11use crate::ns;
12use minidom::Element;
13use std::collections::BTreeMap;
14use std::str::FromStr;
15use try_from::TryFrom;
16
17generate_element!(
18 /// Represents a range in a file.
19 #[derive(PartialEq, Default)]
20 Range, "range", JINGLE_FT,
21 attributes: [
22 /// The offset in bytes from the beginning of the file.
23 offset: u64 = "offset" => default,
24
25 /// The length in bytes of the range, or None to be the entire
26 /// remaining of the file.
27 length: Option<u64> = "length" => optional
28 ],
29 children: [
30 /// List of hashes for this range.
31 hashes: Vec<Hash> = ("hash", HASHES) => Hash
32 ]
33);
34
35impl Range {
36 /// Creates a new range.
37 pub fn new() -> Range {
38 Default::default()
39 }
40}
41
42type Lang = String;
43
44generate_id!(
45 /// Wrapper for a file description.
46 Desc
47);
48
49/// Represents a file to be transferred.
50#[derive(Debug, Clone)]
51pub struct File {
52 /// The date of last modification of this file.
53 pub date: Option<DateTime>,
54
55 /// The MIME type of this file.
56 pub media_type: Option<String>,
57
58 /// The name of this file.
59 pub name: Option<String>,
60
61 /// The description of this file, possibly localised.
62 pub descs: BTreeMap<Lang, Desc>,
63
64 /// The size of this file, in bytes.
65 pub size: Option<u64>,
66
67 /// Used to request only a part of this file.
68 pub range: Option<Range>,
69
70 /// A list of hashes matching this entire file.
71 pub hashes: Vec<Hash>,
72}
73
74impl File {
75 /// Creates a new file descriptor.
76 pub fn new() -> File {
77 File {
78 date: None,
79 media_type: None,
80 name: None,
81 descs: BTreeMap::new(),
82 size: None,
83 range: None,
84 hashes: Vec::new(),
85 }
86 }
87
88 /// Sets the date of last modification on this file.
89 pub fn with_date(mut self, date: DateTime) -> File {
90 self.date = Some(date);
91 self
92 }
93
94 /// Sets the date of last modification on this file from an ISO-8601
95 /// string.
96 pub fn with_date_str(mut self, date: &str) -> Result<File, Error> {
97 self.date = Some(DateTime::from_str(date)?);
98 Ok(self)
99 }
100
101 /// Sets the MIME type of this file.
102 pub fn with_media_type(mut self, media_type: String) -> File {
103 self.media_type = Some(media_type);
104 self
105 }
106
107 /// Sets the name of this file.
108 pub fn with_name(mut self, name: String) -> File {
109 self.name = Some(name);
110 self
111 }
112
113 /// Sets a description for this file.
114 pub fn add_desc(mut self, lang: &str, desc: Desc) -> File {
115 self.descs.insert(Lang::from(lang), desc);
116 self
117 }
118
119 /// Sets the file size of this file, in bytes.
120 pub fn with_size(mut self, size: u64) -> File {
121 self.size = Some(size);
122 self
123 }
124
125 /// Request only a range of this file.
126 pub fn with_range(mut self, range: Range) -> File {
127 self.range = Some(range);
128 self
129 }
130
131 /// Add a hash on this file.
132 pub fn add_hash(mut self, hash: Hash) -> File {
133 self.hashes.push(hash);
134 self
135 }
136}
137
138impl TryFrom<Element> for File {
139 type Err = Error;
140
141 fn try_from(elem: Element) -> Result<File, Error> {
142 check_self!(elem, "file", JINGLE_FT);
143 check_no_attributes!(elem, "file");
144
145 let mut file = File {
146 date: None,
147 media_type: None,
148 name: None,
149 descs: BTreeMap::new(),
150 size: None,
151 range: None,
152 hashes: vec![],
153 };
154
155 for child in elem.children() {
156 if child.is("date", ns::JINGLE_FT) {
157 if file.date.is_some() {
158 return Err(Error::ParseError("File must not have more than one date."));
159 }
160 file.date = Some(child.text().parse()?);
161 } else if child.is("media-type", ns::JINGLE_FT) {
162 if file.media_type.is_some() {
163 return Err(Error::ParseError(
164 "File must not have more than one media-type.",
165 ));
166 }
167 file.media_type = Some(child.text());
168 } else if child.is("name", ns::JINGLE_FT) {
169 if file.name.is_some() {
170 return Err(Error::ParseError("File must not have more than one name."));
171 }
172 file.name = Some(child.text());
173 } else if child.is("desc", ns::JINGLE_FT) {
174 let lang = get_attr!(child, "xml:lang", default);
175 let desc = Desc(child.text());
176 if file.descs.insert(lang, desc).is_some() {
177 return Err(Error::ParseError(
178 "Desc element present twice for the same xml:lang.",
179 ));
180 }
181 } else if child.is("size", ns::JINGLE_FT) {
182 if file.size.is_some() {
183 return Err(Error::ParseError("File must not have more than one size."));
184 }
185 file.size = Some(child.text().parse()?);
186 } else if child.is("range", ns::JINGLE_FT) {
187 if file.range.is_some() {
188 return Err(Error::ParseError("File must not have more than one range."));
189 }
190 file.range = Some(Range::try_from(child.clone())?);
191 } else if child.is("hash", ns::HASHES) {
192 file.hashes.push(Hash::try_from(child.clone())?);
193 } else {
194 return Err(Error::ParseError("Unknown element in JingleFT file."));
195 }
196 }
197
198 Ok(file)
199 }
200}
201
202impl From<File> for Element {
203 fn from(file: File) -> Element {
204 let mut root = Element::builder("file").ns(ns::JINGLE_FT);
205 if let Some(date) = file.date {
206 root = root.append(
207 Element::builder("date")
208 .ns(ns::JINGLE_FT)
209 .append(date)
210 .build(),
211 );
212 }
213 if let Some(media_type) = file.media_type {
214 root = root.append(
215 Element::builder("media-type")
216 .ns(ns::JINGLE_FT)
217 .append(media_type)
218 .build(),
219 );
220 }
221 if let Some(name) = file.name {
222 root = root.append(
223 Element::builder("name")
224 .ns(ns::JINGLE_FT)
225 .append(name)
226 .build(),
227 );
228 }
229 for (lang, desc) in file.descs.into_iter() {
230 root = root.append(
231 Element::builder("desc")
232 .ns(ns::JINGLE_FT)
233 .attr("xml:lang", lang)
234 .append(desc.0)
235 .build(),
236 );
237 }
238 if let Some(size) = file.size {
239 root = root.append(
240 Element::builder("size")
241 .ns(ns::JINGLE_FT)
242 .append(format!("{}", size))
243 .build(),
244 );
245 }
246 if let Some(range) = file.range {
247 root = root.append(range);
248 }
249 for hash in file.hashes {
250 root = root.append(hash);
251 }
252 root.build()
253 }
254}
255
256/// A wrapper element for a file.
257#[derive(Debug, Clone)]
258pub struct Description {
259 /// The actual file descriptor.
260 pub file: File,
261}
262
263impl TryFrom<Element> for Description {
264 type Err = Error;
265
266 fn try_from(elem: Element) -> Result<Description, Error> {
267 check_self!(elem, "description", JINGLE_FT, "JingleFT description");
268 check_no_attributes!(elem, "JingleFT description");
269 let mut file = None;
270 for child in elem.children() {
271 if file.is_some() {
272 return Err(Error::ParseError(
273 "JingleFT description element must have exactly one child.",
274 ));
275 }
276 file = Some(File::try_from(child.clone())?);
277 }
278 if file.is_none() {
279 return Err(Error::ParseError(
280 "JingleFT description element must have exactly one child.",
281 ));
282 }
283 Ok(Description {
284 file: file.unwrap(),
285 })
286 }
287}
288
289impl From<Description> for Element {
290 fn from(description: Description) -> Element {
291 Element::builder("description")
292 .ns(ns::JINGLE_FT)
293 .append(description.file)
294 .build()
295 }
296}
297
298/// A checksum for checking that the file has been transferred correctly.
299#[derive(Debug, Clone)]
300pub struct Checksum {
301 /// The identifier of the file transfer content.
302 pub name: ContentId,
303
304 /// The creator of this file transfer.
305 pub creator: Creator,
306
307 /// The file being checksummed.
308 pub file: File,
309}
310
311impl TryFrom<Element> for Checksum {
312 type Err = Error;
313
314 fn try_from(elem: Element) -> Result<Checksum, Error> {
315 check_self!(elem, "checksum", JINGLE_FT);
316 check_no_unknown_attributes!(elem, "checksum", ["name", "creator"]);
317 let mut file = None;
318 for child in elem.children() {
319 if file.is_some() {
320 return Err(Error::ParseError(
321 "JingleFT checksum element must have exactly one child.",
322 ));
323 }
324 file = Some(File::try_from(child.clone())?);
325 }
326 if file.is_none() {
327 return Err(Error::ParseError(
328 "JingleFT checksum element must have exactly one child.",
329 ));
330 }
331 Ok(Checksum {
332 name: get_attr!(elem, "name", required),
333 creator: get_attr!(elem, "creator", required),
334 file: file.unwrap(),
335 })
336 }
337}
338
339impl From<Checksum> for Element {
340 fn from(checksum: Checksum) -> Element {
341 Element::builder("checksum")
342 .ns(ns::JINGLE_FT)
343 .attr("name", checksum.name)
344 .attr("creator", checksum.creator)
345 .append(checksum.file)
346 .build()
347 }
348}
349
350generate_element!(
351 /// A notice that the file transfer has been completed.
352 Received, "received", JINGLE_FT,
353 attributes: [
354 /// The content identifier of this Jingle session.
355 name: ContentId = "name" => required,
356
357 /// The creator of this file transfer.
358 creator: Creator = "creator" => required,
359 ]
360);
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365 use crate::hashes::Algo;
366 use base64;
367
368 #[cfg(target_pointer_width = "32")]
369 #[test]
370 fn test_size() {
371 assert_size!(Range, 40);
372 assert_size!(File, 128);
373 assert_size!(Description, 128);
374 assert_size!(Checksum, 144);
375 assert_size!(Received, 16);
376 }
377
378 #[cfg(target_pointer_width = "64")]
379 #[test]
380 fn test_size() {
381 assert_size!(Range, 48);
382 assert_size!(File, 184);
383 assert_size!(Description, 184);
384 assert_size!(Checksum, 216);
385 assert_size!(Received, 32);
386 }
387
388 #[test]
389 fn test_description() {
390 let elem: Element = r#"
391<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
392 <file>
393 <media-type>text/plain</media-type>
394 <name>test.txt</name>
395 <date>2015-07-26T21:46:00+01:00</date>
396 <size>6144</size>
397 <hash xmlns='urn:xmpp:hashes:2'
398 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
399 </file>
400</description>
401"#
402 .parse()
403 .unwrap();
404 let desc = Description::try_from(elem).unwrap();
405 assert_eq!(desc.file.media_type, Some(String::from("text/plain")));
406 assert_eq!(desc.file.name, Some(String::from("test.txt")));
407 assert_eq!(desc.file.descs, BTreeMap::new());
408 assert_eq!(
409 desc.file.date,
410 DateTime::from_str("2015-07-26T21:46:00+01:00").ok()
411 );
412 assert_eq!(desc.file.size, Some(6144u64));
413 assert_eq!(desc.file.range, None);
414 assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);
415 assert_eq!(
416 desc.file.hashes[0].hash,
417 base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap()
418 );
419 }
420
421 #[test]
422 fn test_request() {
423 let elem: Element = r#"
424<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
425 <file>
426 <hash xmlns='urn:xmpp:hashes:2'
427 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
428 </file>
429</description>
430"#
431 .parse()
432 .unwrap();
433 let desc = Description::try_from(elem).unwrap();
434 assert_eq!(desc.file.media_type, None);
435 assert_eq!(desc.file.name, None);
436 assert_eq!(desc.file.descs, BTreeMap::new());
437 assert_eq!(desc.file.date, None);
438 assert_eq!(desc.file.size, None);
439 assert_eq!(desc.file.range, None);
440 assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);
441 assert_eq!(
442 desc.file.hashes[0].hash,
443 base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap()
444 );
445 }
446
447 #[test]
448 fn test_descs() {
449 let elem: Element = r#"
450<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
451 <file>
452 <media-type>text/plain</media-type>
453 <desc xml:lang='fr'>Fichier secret !</desc>
454 <desc xml:lang='en'>Secret file!</desc>
455 <hash xmlns='urn:xmpp:hashes:2'
456 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
457 </file>
458</description>
459"#
460 .parse()
461 .unwrap();
462 let desc = Description::try_from(elem).unwrap();
463 assert_eq!(
464 desc.file.descs.keys().cloned().collect::<Vec<_>>(),
465 ["en", "fr"]
466 );
467 assert_eq!(desc.file.descs["en"], Desc(String::from("Secret file!")));
468 assert_eq!(
469 desc.file.descs["fr"],
470 Desc(String::from("Fichier secret !"))
471 );
472
473 let elem: Element = r#"
474<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
475 <file>
476 <media-type>text/plain</media-type>
477 <desc xml:lang='fr'>Fichier secret !</desc>
478 <desc xml:lang='fr'>Secret file!</desc>
479 <hash xmlns='urn:xmpp:hashes:2'
480 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
481 </file>
482</description>
483"#
484 .parse()
485 .unwrap();
486 let error = Description::try_from(elem).unwrap_err();
487 let message = match error {
488 Error::ParseError(string) => string,
489 _ => panic!(),
490 };
491 assert_eq!(message, "Desc element present twice for the same xml:lang.");
492 }
493
494 #[test]
495 fn test_received() {
496 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'/>".parse().unwrap();
497 let received = Received::try_from(elem).unwrap();
498 assert_eq!(received.name, ContentId(String::from("coucou")));
499 assert_eq!(received.creator, Creator::Initiator);
500 let elem2 = Element::from(received.clone());
501 let received2 = Received::try_from(elem2).unwrap();
502 assert_eq!(received2.name, ContentId(String::from("coucou")));
503 assert_eq!(received2.creator, Creator::Initiator);
504
505 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><coucou/></received>".parse().unwrap();
506 let error = Received::try_from(elem).unwrap_err();
507 let message = match error {
508 Error::ParseError(string) => string,
509 _ => panic!(),
510 };
511 assert_eq!(message, "Unknown child in received element.");
512
513 let elem: Element =
514 "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' creator='initiator'/>"
515 .parse()
516 .unwrap();
517 let error = Received::try_from(elem).unwrap_err();
518 let message = match error {
519 Error::ParseError(string) => string,
520 _ => panic!(),
521 };
522 assert_eq!(message, "Required attribute 'name' missing.");
523
524 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='coucou'/>".parse().unwrap();
525 let error = Received::try_from(elem).unwrap_err();
526 let message = match error {
527 Error::ParseError(string) => string,
528 _ => panic!(),
529 };
530 assert_eq!(message, "Unknown value for 'creator' attribute.");
531 }
532
533 #[cfg(not(feature = "disable-validation"))]
534 #[test]
535 fn test_invalid_received() {
536 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator' coucou=''/>".parse().unwrap();
537 let error = Received::try_from(elem).unwrap_err();
538 let message = match error {
539 Error::ParseError(string) => string,
540 _ => panic!(),
541 };
542 assert_eq!(message, "Unknown attribute in received element.");
543 }
544
545 #[test]
546 fn test_checksum() {
547 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();
548 let hash = vec![
549 195, 73, 156, 39, 41, 115, 10, 127, 128, 126, 251, 134, 118, 169, 45, 203, 111, 138,
550 63, 143,
551 ];
552 let checksum = Checksum::try_from(elem).unwrap();
553 assert_eq!(checksum.name, ContentId(String::from("coucou")));
554 assert_eq!(checksum.creator, Creator::Initiator);
555 assert_eq!(
556 checksum.file.hashes,
557 vec!(Hash {
558 algo: Algo::Sha_1,
559 hash: hash.clone()
560 })
561 );
562 let elem2 = Element::from(checksum);
563 let checksum2 = Checksum::try_from(elem2).unwrap();
564 assert_eq!(checksum2.name, ContentId(String::from("coucou")));
565 assert_eq!(checksum2.creator, Creator::Initiator);
566 assert_eq!(
567 checksum2.file.hashes,
568 vec!(Hash {
569 algo: Algo::Sha_1,
570 hash: hash.clone()
571 })
572 );
573
574 let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><coucou/></checksum>".parse().unwrap();
575 let error = Checksum::try_from(elem).unwrap_err();
576 let message = match error {
577 Error::ParseError(string) => string,
578 _ => panic!(),
579 };
580 assert_eq!(message, "This is not a file element.");
581
582 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();
583 let error = Checksum::try_from(elem).unwrap_err();
584 let message = match error {
585 Error::ParseError(string) => string,
586 _ => panic!(),
587 };
588 assert_eq!(message, "Required attribute 'name' missing.");
589
590 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();
591 let error = Checksum::try_from(elem).unwrap_err();
592 let message = match error {
593 Error::ParseError(string) => string,
594 _ => panic!(),
595 };
596 assert_eq!(message, "Unknown value for 'creator' attribute.");
597 }
598
599 #[cfg(not(feature = "disable-validation"))]
600 #[test]
601 fn test_invalid_checksum() {
602 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();
603 let error = Checksum::try_from(elem).unwrap_err();
604 let message = match error {
605 Error::ParseError(string) => string,
606 _ => panic!(),
607 };
608 assert_eq!(message, "Unknown attribute in checksum element.");
609 }
610
611 #[test]
612 fn test_range() {
613 let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5'/>"
614 .parse()
615 .unwrap();
616 let range = Range::try_from(elem).unwrap();
617 assert_eq!(range.offset, 0);
618 assert_eq!(range.length, None);
619 assert_eq!(range.hashes, vec!());
620
621 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();
622 let hashes = vec![Hash {
623 algo: Algo::Sha_1,
624 hash: vec![
625 144, 122, 121, 69, 44, 214, 254, 30, 198, 155, 87, 173, 73, 255, 116, 50, 190, 79,
626 11, 249,
627 ],
628 }];
629 let range = Range::try_from(elem).unwrap();
630 assert_eq!(range.offset, 2048);
631 assert_eq!(range.length, Some(1024));
632 assert_eq!(range.hashes, hashes);
633 let elem2 = Element::from(range);
634 let range2 = Range::try_from(elem2).unwrap();
635 assert_eq!(range2.offset, 2048);
636 assert_eq!(range2.length, Some(1024));
637 assert_eq!(range2.hashes, hashes);
638 }
639
640 #[cfg(not(feature = "disable-validation"))]
641 #[test]
642 fn test_invalid_range() {
643 let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5' coucou=''/>"
644 .parse()
645 .unwrap();
646 let error = Range::try_from(elem).unwrap_err();
647 let message = match error {
648 Error::ParseError(string) => string,
649 _ => panic!(),
650 };
651 assert_eq!(message, "Unknown attribute in range element.");
652 }
653}