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::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}