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