jingle_ft.rs

  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}