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;
  8
  9use hashes::Hash;
 10
 11use minidom::{Element, IntoElements, ElementEmitter};
 12use chrono::{DateTime, FixedOffset};
 13
 14use error::Error;
 15use ns;
 16
 17#[derive(Debug, Clone, PartialEq)]
 18pub struct Range {
 19    pub offset: u64,
 20    pub length: Option<u64>,
 21    pub hashes: Vec<Hash>,
 22}
 23
 24impl IntoElements for Range {
 25    fn into_elements(self, emitter: &mut ElementEmitter) {
 26        let mut elem = Element::builder("range")
 27                               .ns(ns::JINGLE_FT)
 28                               .attr("offset", self.offset)
 29                               .attr("length", self.length)
 30                               .build();
 31        for hash in self.hashes {
 32            elem.append_child(hash.into());
 33        }
 34        emitter.append_child(elem);
 35    }
 36}
 37
 38#[derive(Debug, Clone)]
 39pub struct File {
 40    pub date: Option<DateTime<FixedOffset>>,
 41    pub media_type: Option<String>,
 42    pub name: Option<String>,
 43    pub desc: Option<String>,
 44    pub size: Option<u64>,
 45    pub range: Option<Range>,
 46    pub hashes: Vec<Hash>,
 47}
 48
 49#[derive(Debug, Clone)]
 50pub struct Description {
 51    pub file: File,
 52}
 53
 54#[derive(Debug, Clone)]
 55pub enum Creator {
 56    Initiator,
 57    Responder,
 58}
 59
 60#[derive(Debug, Clone)]
 61pub struct Checksum {
 62    pub name: String,
 63    pub creator: Creator,
 64    pub file: File,
 65}
 66
 67#[derive(Debug, Clone)]
 68pub struct Received {
 69    pub name: String,
 70    pub creator: Creator,
 71}
 72
 73impl IntoElements for Received {
 74    fn into_elements(self, emitter: &mut ElementEmitter) {
 75        let elem = Element::builder("received")
 76                           .ns(ns::JINGLE_FT)
 77                           .attr("name", self.name)
 78                           .attr("creator", match self.creator {
 79                                Creator::Initiator => "initiator",
 80                                Creator::Responder => "responder",
 81                            })
 82                           .build();
 83        emitter.append_child(elem);
 84    }
 85}
 86
 87impl TryFrom<Element> for Description {
 88    type Err = Error;
 89
 90    fn try_from(elem: Element) -> Result<Description, Error> {
 91        if !elem.is("description", ns::JINGLE_FT) {
 92            return Err(Error::ParseError("This is not a JingleFT description element."));
 93        }
 94        if elem.children().count() != 1 {
 95            return Err(Error::ParseError("JingleFT description element must have exactly one child."));
 96        }
 97
 98        let mut date = None;
 99        let mut media_type = None;
100        let mut name = None;
101        let mut desc = None;
102        let mut size = None;
103        let mut range = None;
104        let mut hashes = vec!();
105        for description_payload in elem.children() {
106            if !description_payload.is("file", ns::JINGLE_FT) {
107                return Err(Error::ParseError("Unknown element in JingleFT description."));
108            }
109            for file_payload in description_payload.children() {
110                if file_payload.is("date", ns::JINGLE_FT) {
111                    if date.is_some() {
112                        return Err(Error::ParseError("File must not have more than one date."));
113                    }
114                    date = Some(file_payload.text().parse()?);
115                } else if file_payload.is("media-type", ns::JINGLE_FT) {
116                    if media_type.is_some() {
117                        return Err(Error::ParseError("File must not have more than one media-type."));
118                    }
119                    media_type = Some(file_payload.text());
120                } else if file_payload.is("name", ns::JINGLE_FT) {
121                    if name.is_some() {
122                        return Err(Error::ParseError("File must not have more than one name."));
123                    }
124                    name = Some(file_payload.text());
125                } else if file_payload.is("desc", ns::JINGLE_FT) {
126                    if desc.is_some() {
127                        return Err(Error::ParseError("File must not have more than one desc."));
128                    }
129                    desc = Some(file_payload.text());
130                } else if file_payload.is("size", ns::JINGLE_FT) {
131                    if size.is_some() {
132                        return Err(Error::ParseError("File must not have more than one size."));
133                    }
134                    size = Some(file_payload.text().parse()?);
135                } else if file_payload.is("range", ns::JINGLE_FT) {
136                    if range.is_some() {
137                        return Err(Error::ParseError("File must not have more than one range."));
138                    }
139                    let offset = get_attr!(file_payload, "offset", default);
140                    let length = get_attr!(file_payload, "length", optional);
141                    let mut range_hashes = vec!();
142                    for hash_element in file_payload.children() {
143                        if !hash_element.is("hash", ns::HASHES) {
144                            return Err(Error::ParseError("Unknown element in JingleFT range."));
145                        }
146                        range_hashes.push(Hash::try_from(hash_element.clone())?);
147                    }
148                    range = Some(Range {
149                        offset: offset,
150                        length: length,
151                        hashes: range_hashes,
152                    });
153                } else if file_payload.is("hash", ns::HASHES) {
154                    hashes.push(Hash::try_from(file_payload.clone())?);
155                } else {
156                    return Err(Error::ParseError("Unknown element in JingleFT file."));
157                }
158            }
159        }
160
161        Ok(Description {
162            file: File {
163                date: date,
164                media_type: media_type,
165                name: name,
166                desc: desc,
167                size: size,
168                range: range,
169                hashes: hashes,
170            },
171        })
172    }
173}
174
175impl From<File> for Element {
176    fn from(file: File) -> Element {
177        let mut root = Element::builder("file")
178                               .ns(ns::JINGLE_FT)
179                               .build();
180        if let Some(date) = file.date {
181            root.append_child(Element::builder("date")
182                                      .ns(ns::JINGLE_FT)
183                                      .append(date.to_rfc3339())
184                                      .build());
185        }
186        if let Some(media_type) = file.media_type {
187            root.append_child(Element::builder("media-type")
188                                      .ns(ns::JINGLE_FT)
189                                      .append(media_type)
190                                      .build());
191        }
192        if let Some(name) = file.name {
193            root.append_child(Element::builder("name")
194                                      .ns(ns::JINGLE_FT)
195                                      .append(name)
196                                      .build());
197        }
198        if let Some(desc) = file.desc {
199            root.append_child(Element::builder("desc")
200                                      .ns(ns::JINGLE_FT)
201                                      .append(desc)
202                                      .build());
203        }
204        if let Some(size) = file.size {
205            root.append_child(Element::builder("size")
206                                      .ns(ns::JINGLE_FT)
207                                      .append(format!("{}", size))
208                                      .build());
209        }
210        if let Some(range) = file.range {
211            root.append_child(Element::builder("range")
212                                      .ns(ns::JINGLE_FT)
213                                      .append(range)
214                                      .build());
215        }
216        for hash in file.hashes {
217            root.append_child(hash.into());
218        }
219        root
220    }
221}
222
223impl From<Description> for Element {
224    fn from(description: Description) -> Element {
225        let file: Element = description.file.into();
226        Element::builder("description")
227                .ns(ns::JINGLE_FT)
228                .append(file)
229                .build()
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use hashes::Algo;
237    use base64;
238
239    #[test]
240    fn test_description() {
241        let elem: Element = r#"
242<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
243  <file>
244    <media-type>text/plain</media-type>
245    <name>test.txt</name>
246    <date>2015-07-26T21:46:00+01:00</date>
247    <size>6144</size>
248    <hash xmlns='urn:xmpp:hashes:2'
249          algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
250  </file>
251</description>
252"#.parse().unwrap();
253
254        let desc = Description::try_from(elem).unwrap();
255        assert_eq!(desc.file.media_type, Some(String::from("text/plain")));
256        assert_eq!(desc.file.name, Some(String::from("test.txt")));
257        assert_eq!(desc.file.desc, None);
258        assert_eq!(desc.file.date, Some(DateTime::parse_from_rfc3339("2015-07-26T21:46:00+01:00").unwrap()));
259        assert_eq!(desc.file.size, Some(6144u64));
260        assert_eq!(desc.file.range, None);
261        assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1);
262        assert_eq!(desc.file.hashes[0].hash, base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap());
263    }
264
265    #[test]
266    fn test_request() {
267        let elem: Element = r#"
268<description xmlns='urn:xmpp:jingle:apps:file-transfer:5'>
269  <file>
270    <hash xmlns='urn:xmpp:hashes:2'
271          algo='sha-1'>w0mcJylzCn+AfvuGdqkty2+KP48=</hash>
272  </file>
273</description>
274"#.parse().unwrap();
275
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.desc, None);
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}