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;
8
9use std::collections::BTreeMap;
10use std::str::FromStr;
11
12use hashes::Hash;
13use jingle::{Creator, ContentId};
14use date::DateTime;
15
16use minidom::{Element, IntoAttributeValue};
17
18use error::Error;
19use ns;
20
21generate_element_with_children!(
22 #[derive(PartialEq)]
23 Range, "range", ns::JINGLE_FT,
24 attributes: [
25 offset: u64 = "offset" => default,
26 length: Option<u64> = "length" => optional
27 ],
28 children: [
29 hashes: Vec<Hash> = ("hash", ns::HASHES) => Hash
30 ]
31);
32
33type Lang = String;
34
35generate_id!(Desc);
36
37#[derive(Debug, Clone)]
38pub struct File {
39 pub date: Option<DateTime>,
40 pub media_type: Option<String>,
41 pub name: Option<String>,
42 pub descs: BTreeMap<Lang, Desc>,
43 pub size: Option<u64>,
44 pub range: Option<Range>,
45 pub hashes: Vec<Hash>,
46}
47
48impl TryFrom<Element> for File {
49 type Err = Error;
50
51 fn try_from(elem: Element) -> Result<File, Error> {
52 check_self!(elem, "file", ns::JINGLE_FT);
53 check_no_attributes!(elem, "file");
54
55 let mut file = File {
56 date: None,
57 media_type: None,
58 name: None,
59 descs: BTreeMap::new(),
60 size: None,
61 range: None,
62 hashes: vec!(),
63 };
64
65 for child in elem.children() {
66 if child.is("date", ns::JINGLE_FT) {
67 if file.date.is_some() {
68 return Err(Error::ParseError("File must not have more than one date."));
69 }
70 file.date = Some(child.text().parse()?);
71 } else if child.is("media-type", ns::JINGLE_FT) {
72 if file.media_type.is_some() {
73 return Err(Error::ParseError("File must not have more than one media-type."));
74 }
75 file.media_type = Some(child.text());
76 } else if child.is("name", ns::JINGLE_FT) {
77 if file.name.is_some() {
78 return Err(Error::ParseError("File must not have more than one name."));
79 }
80 file.name = Some(child.text());
81 } else if child.is("desc", ns::JINGLE_FT) {
82 let lang = get_attr!(child, "xml:lang", default);
83 let desc = Desc(child.text());
84 if file.descs.insert(lang, desc).is_some() {
85 return Err(Error::ParseError("Desc element present twice for the same xml:lang."));
86 }
87 } else if child.is("size", ns::JINGLE_FT) {
88 if file.size.is_some() {
89 return Err(Error::ParseError("File must not have more than one size."));
90 }
91 file.size = Some(child.text().parse()?);
92 } else if child.is("range", ns::JINGLE_FT) {
93 if file.range.is_some() {
94 return Err(Error::ParseError("File must not have more than one range."));
95 }
96 file.range = Some(Range::try_from(child.clone())?);
97 } else if child.is("hash", ns::HASHES) {
98 file.hashes.push(Hash::try_from(child.clone())?);
99 } else {
100 return Err(Error::ParseError("Unknown element in JingleFT file."));
101 }
102 }
103
104 Ok(file)
105 }
106}
107
108impl From<File> for Element {
109 fn from(file: File) -> Element {
110 let mut root = Element::builder("file")
111 .ns(ns::JINGLE_FT);
112 if let Some(date) = file.date {
113 root = root.append(Element::builder("date")
114 .ns(ns::JINGLE_FT)
115 .append(date)
116 .build());
117 }
118 if let Some(media_type) = file.media_type {
119 root = root.append(Element::builder("media-type")
120 .ns(ns::JINGLE_FT)
121 .append(media_type)
122 .build());
123 }
124 if let Some(name) = file.name {
125 root = root.append(Element::builder("name")
126 .ns(ns::JINGLE_FT)
127 .append(name)
128 .build());
129 }
130 for (lang, desc) in file.descs.into_iter() {
131 root = root.append(Element::builder("desc")
132 .ns(ns::JINGLE_FT)
133 .attr("xml:lang", lang)
134 .append(desc.0)
135 .build());
136 }
137 if let Some(size) = file.size {
138 root = root.append(Element::builder("size")
139 .ns(ns::JINGLE_FT)
140 .append(format!("{}", size))
141 .build());
142 }
143 if let Some(range) = file.range {
144 root = root.append(range);
145 }
146 for hash in file.hashes {
147 root = root.append(hash);
148 }
149 root.build()
150 }
151}
152#[derive(Debug, Clone)]
153pub struct Description {
154 pub file: File,
155}
156
157impl TryFrom<Element> for Description {
158 type Err = Error;
159
160 fn try_from(elem: Element) -> Result<Description, Error> {
161 check_self!(elem, "description", ns::JINGLE_FT, "JingleFT description");
162 check_no_attributes!(elem, "JingleFT description");
163 let mut file = None;
164 for child in elem.children() {
165 if file.is_some() {
166 return Err(Error::ParseError("JingleFT description element must have exactly one child."));
167 }
168 file = Some(File::try_from(child.clone())?);
169 }
170 if file.is_none() {
171 return Err(Error::ParseError("JingleFT description element must have exactly one child."));
172 }
173 Ok(Description {
174 file: file.unwrap(),
175 })
176 }
177}
178
179impl From<Description> for Element {
180 fn from(description: Description) -> Element {
181 Element::builder("description")
182 .ns(ns::JINGLE_FT)
183 .append(description.file)
184 .build()
185 }
186}
187
188#[derive(Debug, Clone)]
189pub struct Checksum {
190 pub name: ContentId,
191 pub creator: Creator,
192 pub file: File,
193}
194
195impl TryFrom<Element> for Checksum {
196 type Err = Error;
197
198 fn try_from(elem: Element) -> Result<Checksum, Error> {
199 check_self!(elem, "checksum", ns::JINGLE_FT);
200 check_no_unknown_attributes!(elem, "checksum", ["name", "creator"]);
201 let mut file = None;
202 for child in elem.children() {
203 if file.is_some() {
204 return Err(Error::ParseError("JingleFT checksum element must have exactly one child."));
205 }
206 file = Some(File::try_from(child.clone())?);
207 }
208 if file.is_none() {
209 return Err(Error::ParseError("JingleFT checksum element must have exactly one child."));
210 }
211 Ok(Checksum {
212 name: get_attr!(elem, "name", required),
213 creator: get_attr!(elem, "creator", required),
214 file: file.unwrap(),
215 })
216 }
217}
218
219impl From<Checksum> for Element {
220 fn from(checksum: Checksum) -> Element {
221 Element::builder("checksum")
222 .ns(ns::JINGLE_FT)
223 .attr("name", checksum.name)
224 .attr("creator", checksum.creator)
225 .append(checksum.file)
226 .build()
227 }
228}
229
230generate_element_with_only_attributes!(Received, "received", ns::JINGLE_FT, [
231 name: ContentId = "name" => required,
232 creator: Creator = "creator" => required,
233]);
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use hashes::Algo;
239 use base64;
240
241 #[test]
242 fn test_description() {
243 let elem: Element = r#"
244<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
245 <file>
246 <media-type>text/plain</media-type>
247 <name>test.txt</name>
248 <date>2015-07-26T21:46:00+01:00</date>
249 <size>6144</size>
250 <hash xmlns='urn:xmpp:hashes:2'
251 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
252 </file>
253</description>
254"#.parse().unwrap();
255 let desc = Description::try_from(elem).unwrap();
256 assert_eq!(desc.file.media_type, Some(String::from("text/plain")));
257 assert_eq!(desc.file.name, Some(String::from("test.txt")));
258 assert_eq!(desc.file.descs, BTreeMap::new());
259 assert_eq!(desc.file.date, DateTime::from_str("2015-07-26T21:46:00+01:00").ok());
260 assert_eq!(desc.file.size, Some(6144u64));
261 assert_eq!(desc.file.range, None);
262 assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);
263 assert_eq!(desc.file.hashes[0].hash, base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap());
264 }
265
266 #[test]
267 fn test_request() {
268 let elem: Element = r#"
269<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
270 <file>
271 <hash xmlns='urn:xmpp:hashes:2'
272 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
273 </file>
274</description>
275"#.parse().unwrap();
276 let desc = Description::try_from(elem).unwrap();
277 assert_eq!(desc.file.media_type, None);
278 assert_eq!(desc.file.name, None);
279 assert_eq!(desc.file.descs, BTreeMap::new());
280 assert_eq!(desc.file.date, None);
281 assert_eq!(desc.file.size, None);
282 assert_eq!(desc.file.range, None);
283 assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);
284 assert_eq!(desc.file.hashes[0].hash, base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap());
285 }
286
287 #[test]
288 fn test_descs() {
289 let elem: Element = r#"
290<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
291 <file>
292 <media-type>text/plain</media-type>
293 <desc xml:lang='fr'>Fichier secret !</desc>
294 <desc xml:lang='en'>Secret file!</desc>
295 <hash xmlns='urn:xmpp:hashes:2'
296 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
297 </file>
298</description>
299"#.parse().unwrap();
300 let desc = Description::try_from(elem).unwrap();
301 assert_eq!(desc.file.descs.keys().cloned().collect::<Vec<_>>(), ["en", "fr"]);
302 assert_eq!(desc.file.descs["en"], Desc(String::from("Secret file!")));
303 assert_eq!(desc.file.descs["fr"], Desc(String::from("Fichier secret !")));
304
305 let elem: Element = r#"
306<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
307 <file>
308 <media-type>text/plain</media-type>
309 <desc xml:lang='fr'>Fichier secret !</desc>
310 <desc xml:lang='fr'>Secret file!</desc>
311 <hash xmlns='urn:xmpp:hashes:2'
312 algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
313 </file>
314</description>
315"#.parse().unwrap();
316 let error = Description::try_from(elem).unwrap_err();
317 let message = match error {
318 Error::ParseError(string) => string,
319 _ => panic!(),
320 };
321 assert_eq!(message, "Desc element present twice for the same xml:lang.");
322 }
323
324 #[test]
325 fn test_received() {
326 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'/>".parse().unwrap();
327 let received = Received::try_from(elem).unwrap();
328 assert_eq!(received.name, ContentId(String::from("coucou")));
329 assert_eq!(received.creator, Creator::Initiator);
330 let elem2 = Element::from(received.clone());
331 let received2 = Received::try_from(elem2).unwrap();
332 assert_eq!(received2.name, ContentId(String::from("coucou")));
333 assert_eq!(received2.creator, Creator::Initiator);
334
335 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><coucou/></received>".parse().unwrap();
336 let error = Received::try_from(elem).unwrap_err();
337 let message = match error {
338 Error::ParseError(string) => string,
339 _ => panic!(),
340 };
341 assert_eq!(message, "Unknown child in received element.");
342
343 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator' coucou=''/>".parse().unwrap();
344 let error = Received::try_from(elem).unwrap_err();
345 let message = match error {
346 Error::ParseError(string) => string,
347 _ => panic!(),
348 };
349 assert_eq!(message, "Unknown attribute in received element.");
350
351 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' creator='initiator'/>".parse().unwrap();
352 let error = Received::try_from(elem).unwrap_err();
353 let message = match error {
354 Error::ParseError(string) => string,
355 _ => panic!(),
356 };
357 assert_eq!(message, "Required attribute 'name' missing.");
358
359 let elem: Element = "<received xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='coucou'/>".parse().unwrap();
360 let error = Received::try_from(elem).unwrap_err();
361 let message = match error {
362 Error::ParseError(string) => string,
363 _ => panic!(),
364 };
365 assert_eq!(message, "Unknown value for 'creator' attribute.");
366 }
367
368 #[test]
369 fn test_checksum() {
370 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();
371 let hash = vec!(195, 73, 156, 39, 41, 115, 10, 127, 128, 126, 251, 134, 118, 169, 45, 203, 111, 138, 63, 143);
372 let checksum = Checksum::try_from(elem).unwrap();
373 assert_eq!(checksum.name, ContentId(String::from("coucou")));
374 assert_eq!(checksum.creator, Creator::Initiator);
375 assert_eq!(checksum.file.hashes, vec!(Hash { algo: Algo::Sha_1, hash: hash.clone() }));
376 let elem2 = Element::from(checksum);
377 let checksum2 = Checksum::try_from(elem2).unwrap();
378 assert_eq!(checksum2.name, ContentId(String::from("coucou")));
379 assert_eq!(checksum2.creator, Creator::Initiator);
380 assert_eq!(checksum2.file.hashes, vec!(Hash { algo: Algo::Sha_1, hash: hash.clone() }));
381
382 let elem: Element = "<checksum xmlns='urn:xmpp:jingle:apps:file-transfer:5' name='coucou' creator='initiator'><coucou/></checksum>".parse().unwrap();
383 let error = Checksum::try_from(elem).unwrap_err();
384 let message = match error {
385 Error::ParseError(string) => string,
386 _ => panic!(),
387 };
388 assert_eq!(message, "This is not a file element.");
389
390 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();
391 let error = Checksum::try_from(elem).unwrap_err();
392 let message = match error {
393 Error::ParseError(string) => string,
394 _ => panic!(),
395 };
396 assert_eq!(message, "Unknown attribute in checksum element.");
397
398 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();
399 let error = Checksum::try_from(elem).unwrap_err();
400 let message = match error {
401 Error::ParseError(string) => string,
402 _ => panic!(),
403 };
404 assert_eq!(message, "Required attribute 'name' missing.");
405
406 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();
407 let error = Checksum::try_from(elem).unwrap_err();
408 let message = match error {
409 Error::ParseError(string) => string,
410 _ => panic!(),
411 };
412 assert_eq!(message, "Unknown value for 'creator' attribute.");
413 }
414
415 #[test]
416 fn test_range() {
417 let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5'/>".parse().unwrap();
418 let range = Range::try_from(elem).unwrap();
419 assert_eq!(range.offset, 0);
420 assert_eq!(range.length, None);
421 assert_eq!(range.hashes, vec!());
422
423 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();
424 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) });
425 let range = Range::try_from(elem).unwrap();
426 assert_eq!(range.offset, 2048);
427 assert_eq!(range.length, Some(1024));
428 assert_eq!(range.hashes, hashes);
429 let elem2 = Element::from(range);
430 let range2 = Range::try_from(elem2).unwrap();
431 assert_eq!(range2.offset, 2048);
432 assert_eq!(range2.length, Some(1024));
433 assert_eq!(range2.hashes, hashes);
434
435 let elem: Element = "<range xmlns='urn:xmpp:jingle:apps:file-transfer:5' coucou=''/>".parse().unwrap();
436 let error = Range::try_from(elem).unwrap_err();
437 let message = match error {
438 Error::ParseError(string) => string,
439 _ => panic!(),
440 };
441 assert_eq!(message, "Unknown attribute in range element.");
442 }
443}