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::hashes::Hash;
9use crate::jingle::{ContentId, Creator};
10use crate::ns;
11use crate::util::error::Error;
12use minidom::{Element, Node};
13use std::collections::BTreeMap;
14use std::convert::TryFrom;
15use std::str::FromStr;
16
17generate_element!(
18 /// Represents a range in a file.
19 #[derive(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 Element::builder("file", ns::JINGLE_FT)
197 .append_all(
198 file.date
199 .map(|date| Element::builder("date", ns::JINGLE_FT).append(date)),
200 )
201 .append_all(
202 file.media_type.map(|media_type| {
203 Element::builder("media-type", ns::JINGLE_FT).append(media_type)
204 }),
205 )
206 .append_all(
207 file.name
208 .map(|name| Element::builder("name", ns::JINGLE_FT).append(name)),
209 )
210 .append_all(file.descs.into_iter().map(|(lang, desc)| {
211 Element::builder("desc", ns::JINGLE_FT)
212 .attr("xml:lang", lang)
213 .append(desc.0)
214 }))
215 .append_all(
216 file.size.map(|size| {
217 Element::builder("size", ns::JINGLE_FT).append(format!("{}", size))
218 }),
219 )
220 .append_all(file.range)
221 .append_all(file.hashes)
222 .build()
223 }
224}
225
226/// A wrapper element for a file.
227#[derive(Debug, Clone)]
228pub struct Description {
229 /// The actual file descriptor.
230 pub file: File,
231}
232
233impl TryFrom<Element> for Description {
234 type Error = Error;
235
236 fn try_from(elem: Element) -> Result<Description, Error> {
237 check_self!(elem, "description", JINGLE_FT, "JingleFT description");
238 check_no_attributes!(elem, "JingleFT description");
239 let mut file = None;
240 for child in elem.children() {
241 if file.is_some() {
242 return Err(Error::ParseError(
243 "JingleFT description element must have exactly one child.",
244 ));
245 }
246 file = Some(File::try_from(child.clone())?);
247 }
248 if file.is_none() {
249 return Err(Error::ParseError(
250 "JingleFT description element must have exactly one child.",
251 ));
252 }
253 Ok(Description {
254 file: file.unwrap(),
255 })
256 }
257}
258
259impl From<Description> for Element {
260 fn from(description: Description) -> Element {
261 Element::builder("description", ns::JINGLE_FT)
262 .append(Node::Element(description.file.into()))
263 .build()
264 }
265}
266
267/// A checksum for checking that the file has been transferred correctly.
268#[derive(Debug, Clone)]
269pub struct Checksum {
270 /// The identifier of the file transfer content.
271 pub name: ContentId,
272
273 /// The creator of this file transfer.
274 pub creator: Creator,
275
276 /// The file being checksummed.
277 pub file: File,
278}
279
280impl TryFrom<Element> for Checksum {
281 type Error = Error;
282
283 fn try_from(elem: Element) -> Result<Checksum, Error> {
284 check_self!(elem, "checksum", JINGLE_FT);
285 check_no_unknown_attributes!(elem, "checksum", ["name", "creator"]);
286 let mut file = None;
287 for child in elem.children() {
288 if file.is_some() {
289 return Err(Error::ParseError(
290 "JingleFT checksum element must have exactly one child.",
291 ));
292 }
293 file = Some(File::try_from(child.clone())?);
294 }
295 if file.is_none() {
296 return Err(Error::ParseError(
297 "JingleFT checksum element must have exactly one child.",
298 ));
299 }
300 Ok(Checksum {
301 name: get_attr!(elem, "name", Required),
302 creator: get_attr!(elem, "creator", Required),
303 file: file.unwrap(),
304 })
305 }
306}
307
308impl From<Checksum> for Element {
309 fn from(checksum: Checksum) -> Element {
310 Element::builder("checksum", ns::JINGLE_FT)
311 .attr("name", checksum.name)
312 .attr("creator", checksum.creator)
313 .append(Node::Element(checksum.file.into()))
314 .build()
315 }
316}
317
318generate_element!(
319 /// A notice that the file transfer has been completed.
320 Received, "received", JINGLE_FT,
321 attributes: [
322 /// The content identifier of this Jingle session.
323 name: Required<ContentId> = "name",
324
325 /// The creator of this file transfer.
326 creator: Required<Creator> = "creator",
327 ]
328);
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use crate::hashes::Algo;
334 use base64::{engine::general_purpose::STANDARD as Base64, Engine};
335
336 // Apparently, i686 and AArch32/PowerPC seem to disagree here. So instead
337 // of trying to figure this out now, we just ignore the test.
338 #[cfg(target_pointer_width = "32")]
339 #[test]
340 #[ignore]
341 fn test_size() {
342 assert_size!(Range, 32);
343 assert_size!(File, 112);
344 assert_size!(Description, 112);
345 assert_size!(Checksum, 128);
346 assert_size!(Received, 16);
347 }
348
349 #[cfg(target_pointer_width = "64")]
350 #[test]
351 fn test_size() {
352 assert_size!(Range, 48);
353 assert_size!(File, 184);
354 assert_size!(Description, 184);
355 assert_size!(Checksum, 216);
356 assert_size!(Received, 32);
357 }
358
359 #[test]
360 fn test_description() {
361 let elem: Element = r#"<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
362 <file>
363 <media-type>text/plain</media-type>
364 <name>test.txt</name>
365 <date>2015-07-26T21:46:00+01:00</date>
366 <size>6144</size>
367 <hash xmlns='urn:xmpp:hashes:2'
368 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
369 </file>
370</description>
371"#
372 .parse()
373 .unwrap();
374 let desc = Description::try_from(elem).unwrap();
375 assert_eq!(desc.file.media_type, Some(String::from("text/plain")));
376 assert_eq!(desc.file.name, Some(String::from("test.txt")));
377 assert_eq!(desc.file.descs, BTreeMap::new());
378 assert_eq!(
379 desc.file.date,
380 DateTime::from_str("2015-07-26T21:46:00+01:00").ok()
381 );
382 assert_eq!(desc.file.size, Some(6144u64));
383 assert_eq!(desc.file.range, None);
384 assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);
385 assert_eq!(
386 desc.file.hashes[0].hash,
387 Base64.decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap()
388 );
389 }
390
391 #[test]
392 fn test_request() {
393 let elem: Element = r#"<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
394 <file>
395 <hash xmlns='urn:xmpp:hashes:2'
396 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
397 </file>
398</description>
399"#
400 .parse()
401 .unwrap();
402 let desc = Description::try_from(elem).unwrap();
403 assert_eq!(desc.file.media_type, None);
404 assert_eq!(desc.file.name, None);
405 assert_eq!(desc.file.descs, BTreeMap::new());
406 assert_eq!(desc.file.date, None);
407 assert_eq!(desc.file.size, None);
408 assert_eq!(desc.file.range, None);
409 assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);
410 assert_eq!(
411 desc.file.hashes[0].hash,
412 Base64.decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap()
413 );
414 }
415
416 #[test]
417 fn test_descs() {
418 let elem: Element = r#"<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
419 <file>
420 <media-type>text/plain</media-type>
421 <desc xml:lang='fr'>Fichier secret !</desc>
422 <desc xml:lang='en'>Secret file!</desc>
423 <hash xmlns='urn:xmpp:hashes:2'
424 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
425 </file>
426</description>
427"#
428 .parse()
429 .unwrap();
430 let desc = Description::try_from(elem).unwrap();
431 assert_eq!(
432 desc.file.descs.keys().cloned().collect::<Vec<_>>(),
433 ["en", "fr"]
434 );
435 assert_eq!(desc.file.descs["en"], Desc(String::from("Secret file!")));
436 assert_eq!(
437 desc.file.descs["fr"],
438 Desc(String::from("Fichier secret !"))
439 );
440
441 let elem: Element = r#"<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
442 <file>
443 <media-type>text/plain</media-type>
444 <desc xml:lang='fr'>Fichier secret !</desc>
445 <desc xml:lang='fr'>Secret file!</desc>
446 <hash xmlns='urn:xmpp:hashes:2'
447 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
448 </file>
449</description>
450"#
451 .parse()
452 .unwrap();
453 let error = Description::try_from(elem).unwrap_err();
454 let message = match error {
455 Error::ParseError(string) => string,
456 _ => panic!(),
457 };
458 assert_eq!(message, "Desc element present twice for the same xml:lang.");
459 }
460
461 #[test]
462 fn test_received() {
463 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'/>".parse().unwrap();
464 let received = Received::try_from(elem).unwrap();
465 assert_eq!(received.name, ContentId(String::from("coucou")));
466 assert_eq!(received.creator, Creator::Initiator);
467 let elem2 = Element::from(received.clone());
468 let received2 = Received::try_from(elem2).unwrap();
469 assert_eq!(received2.name, ContentId(String::from("coucou")));
470 assert_eq!(received2.creator, Creator::Initiator);
471
472 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><coucou/></received>".parse().unwrap();
473 let error = Received::try_from(elem).unwrap_err();
474 let message = match error {
475 Error::ParseError(string) => string,
476 _ => panic!(),
477 };
478 assert_eq!(message, "Unknown child in received element.");
479
480 let elem: Element =
481 "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' creator='initiator'/>"
482 .parse()
483 .unwrap();
484 let error = Received::try_from(elem).unwrap_err();
485 let message = match error {
486 Error::ParseError(string) => string,
487 _ => panic!(),
488 };
489 assert_eq!(message, "Required attribute 'name' missing.");
490
491 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='coucou'/>".parse().unwrap();
492 let error = Received::try_from(elem).unwrap_err();
493 let message = match error {
494 Error::ParseError(string) => string,
495 _ => panic!(),
496 };
497 assert_eq!(message, "Unknown value for 'creator' attribute.");
498 }
499
500 #[cfg(not(feature = "disable-validation"))]
501 #[test]
502 fn test_invalid_received() {
503 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator' coucou=''/>".parse().unwrap();
504 let error = Received::try_from(elem).unwrap_err();
505 let message = match error {
506 Error::ParseError(string) => string,
507 _ => panic!(),
508 };
509 assert_eq!(message, "Unknown attribute in received element.");
510 }
511
512 #[test]
513 fn test_checksum() {
514 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();
515 let hash = vec![
516 195, 73, 156, 39, 41, 115, 10, 127, 128, 126, 251, 134, 118, 169, 45, 203, 111, 138,
517 63, 143,
518 ];
519 let checksum = Checksum::try_from(elem).unwrap();
520 assert_eq!(checksum.name, ContentId(String::from("coucou")));
521 assert_eq!(checksum.creator, Creator::Initiator);
522 assert_eq!(
523 checksum.file.hashes,
524 vec!(Hash {
525 algo: Algo::Sha_1,
526 hash: hash.clone()
527 })
528 );
529 let elem2 = Element::from(checksum);
530 let checksum2 = Checksum::try_from(elem2).unwrap();
531 assert_eq!(checksum2.name, ContentId(String::from("coucou")));
532 assert_eq!(checksum2.creator, Creator::Initiator);
533 assert_eq!(
534 checksum2.file.hashes,
535 vec!(Hash {
536 algo: Algo::Sha_1,
537 hash: hash.clone()
538 })
539 );
540
541 let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><coucou/></checksum>".parse().unwrap();
542 let error = Checksum::try_from(elem).unwrap_err();
543 let message = match error {
544 Error::ParseError(string) => string,
545 _ => panic!(),
546 };
547 assert_eq!(message, "This is not a file element.");
548
549 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();
550 let error = Checksum::try_from(elem).unwrap_err();
551 let message = match error {
552 Error::ParseError(string) => string,
553 _ => panic!(),
554 };
555 assert_eq!(message, "Required attribute 'name' missing.");
556
557 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();
558 let error = Checksum::try_from(elem).unwrap_err();
559 let message = match error {
560 Error::ParseError(string) => string,
561 _ => panic!(),
562 };
563 assert_eq!(message, "Unknown value for 'creator' attribute.");
564 }
565
566 #[cfg(not(feature = "disable-validation"))]
567 #[test]
568 fn test_invalid_checksum() {
569 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();
570 let error = Checksum::try_from(elem).unwrap_err();
571 let message = match error {
572 Error::ParseError(string) => string,
573 _ => panic!(),
574 };
575 assert_eq!(message, "Unknown attribute in checksum element.");
576 }
577
578 #[test]
579 fn test_range() {
580 let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5'/>"
581 .parse()
582 .unwrap();
583 let range = Range::try_from(elem).unwrap();
584 assert_eq!(range.offset, 0);
585 assert_eq!(range.length, None);
586 assert_eq!(range.hashes, vec!());
587
588 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();
589 let hashes = vec![Hash {
590 algo: Algo::Sha_1,
591 hash: vec![
592 144, 122, 121, 69, 44, 214, 254, 30, 198, 155, 87, 173, 73, 255, 116, 50, 190, 79,
593 11, 249,
594 ],
595 }];
596 let range = Range::try_from(elem).unwrap();
597 assert_eq!(range.offset, 2048);
598 assert_eq!(range.length, Some(1024));
599 assert_eq!(range.hashes, hashes);
600 let elem2 = Element::from(range);
601 let range2 = Range::try_from(elem2).unwrap();
602 assert_eq!(range2.offset, 2048);
603 assert_eq!(range2.length, Some(1024));
604 assert_eq!(range2.hashes, hashes);
605 }
606
607 #[cfg(not(feature = "disable-validation"))]
608 #[test]
609 fn test_invalid_range() {
610 let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5' coucou=''/>"
611 .parse()
612 .unwrap();
613 let error = Range::try_from(elem).unwrap_err();
614 let message = match error {
615 Error::ParseError(string) => string,
616 _ => panic!(),
617 };
618 assert_eq!(message, "Unknown attribute in range element.");
619 }
620}