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