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}