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 std::convert::TryFrom;
8use std::str::FromStr;
9
10use minidom::{Element, IntoAttributeValue};
11use base64;
12
13use error::Error;
14
15use ns;
16
17#[derive(Debug, Clone, PartialEq)]
18pub enum Stanza {
19 Iq,
20 Message,
21}
22
23impl Default for Stanza {
24 fn default() -> Stanza {
25 Stanza::Iq
26 }
27}
28
29impl FromStr for Stanza {
30 type Err = Error;
31
32 fn from_str(s: &str) -> Result<Stanza, Error> {
33 Ok(match s {
34 "iq" => Stanza::Iq,
35 "message" => Stanza::Message,
36
37 _ => return Err(Error::ParseError("Invalid 'stanza' attribute.")),
38 })
39 }
40}
41
42impl IntoAttributeValue for Stanza {
43 fn into_attribute_value(self) -> Option<String> {
44 match self {
45 Stanza::Iq => None,
46 Stanza::Message => Some(String::from("message")),
47 }
48 }
49}
50
51#[derive(Debug, Clone)]
52pub enum IBB {
53 Open {
54 block_size: u16,
55 sid: String,
56 stanza: Stanza,
57 },
58 Data {
59 seq: u16,
60 sid: String,
61 data: Vec<u8>,
62 },
63 Close {
64 sid: String,
65 },
66}
67
68fn required_attr<T: FromStr>(elem: &Element, attr: &str, err: Error) -> Result<T, Error> {
69 elem.attr(attr)
70 .and_then(|value| value.parse().ok())
71 .ok_or(err)
72}
73
74impl<'a> TryFrom<&'a Element> for IBB {
75 type Error = Error;
76
77 fn try_from(elem: &'a Element) -> Result<IBB, Error> {
78 if elem.is("open", ns::IBB) {
79 for _ in elem.children() {
80 return Err(Error::ParseError("Unknown child in open element."));
81 }
82 let block_size = required_attr(elem, "block-size", Error::ParseError("Required attribute 'block-size' missing in open element."))?;
83 let sid = required_attr(elem, "sid", Error::ParseError("Required attribute 'sid' missing in open element."))?;
84 let stanza = match elem.attr("stanza") {
85 Some(stanza) => stanza.parse()?,
86 None => Default::default(),
87 };
88 Ok(IBB::Open {
89 block_size: block_size,
90 sid: sid,
91 stanza: stanza
92 })
93 } else if elem.is("data", ns::IBB) {
94 for _ in elem.children() {
95 return Err(Error::ParseError("Unknown child in data element."));
96 }
97 let seq = required_attr(elem, "seq", Error::ParseError("Required attribute 'seq' missing in data element."))?;
98 let sid = required_attr(elem, "sid", Error::ParseError("Required attribute 'sid' missing in data element."))?;
99 let data = base64::decode(&elem.text())?;
100 Ok(IBB::Data {
101 seq: seq,
102 sid: sid,
103 data: data
104 })
105 } else if elem.is("close", ns::IBB) {
106 let sid = required_attr(elem, "sid", Error::ParseError("Required attribute 'sid' missing in data element."))?;
107 for _ in elem.children() {
108 return Err(Error::ParseError("Unknown child in close element."));
109 }
110 Ok(IBB::Close {
111 sid: sid,
112 })
113 } else {
114 Err(Error::ParseError("This is not an ibb element."))
115 }
116 }
117}
118
119impl<'a> Into<Element> for &'a IBB {
120 fn into(self) -> Element {
121 match *self {
122 IBB::Open { ref block_size, ref sid, ref stanza } => {
123 Element::builder("open")
124 .ns(ns::IBB)
125 .attr("block-size", format!("{}", block_size))
126 .attr("sid", sid.to_owned())
127 .attr("stanza", stanza.to_owned())
128 .build()
129 },
130 IBB::Data { ref seq, ref sid, ref data } => {
131 Element::builder("data")
132 .ns(ns::IBB)
133 .attr("seq", format!("{}", seq))
134 .attr("sid", sid.to_owned())
135 .append(base64::encode(&data))
136 .build()
137 },
138 IBB::Close { ref sid } => {
139 Element::builder("close")
140 .ns(ns::IBB)
141 .attr("sid", sid.to_owned())
142 .build()
143 },
144 }
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn test_simple() {
154 let elem: Element = "<open xmlns='http://jabber.org/protocol/ibb' block-size='3' sid='coucou'/>".parse().unwrap();
155 let open = IBB::try_from(&elem).unwrap();
156 match open {
157 IBB::Open { block_size, sid, stanza } => {
158 assert_eq!(block_size, 3);
159 assert_eq!(sid, "coucou");
160 assert_eq!(stanza, Stanza::Iq);
161 },
162 _ => panic!(),
163 }
164
165 let elem: Element = "<data xmlns='http://jabber.org/protocol/ibb' seq='0' sid='coucou'>AAAA</data>".parse().unwrap();
166 let data = IBB::try_from(&elem).unwrap();
167 match data {
168 IBB::Data { seq, sid, data } => {
169 assert_eq!(seq, 0);
170 assert_eq!(sid, "coucou");
171 assert_eq!(data, vec!(0, 0, 0));
172 },
173 _ => panic!(),
174 }
175
176 let elem: Element = "<close xmlns='http://jabber.org/protocol/ibb' sid='coucou'/>".parse().unwrap();
177 let close = IBB::try_from(&elem).unwrap();
178 match close {
179 IBB::Close { sid } => {
180 assert_eq!(sid, "coucou");
181 },
182 _ => panic!(),
183 }
184 }
185
186 #[test]
187 fn test_invalid() {
188 let elem: Element = "<open xmlns='http://jabber.org/protocol/ibb'/>".parse().unwrap();
189 let error = IBB::try_from(&elem).unwrap_err();
190 let message = match error {
191 Error::ParseError(string) => string,
192 _ => panic!(),
193 };
194 assert_eq!(message, "Required attribute 'block-size' missing in open element.");
195
196 // TODO: maybe make a better error message here.
197 let elem: Element = "<open xmlns='http://jabber.org/protocol/ibb' block-size='-5'/>".parse().unwrap();
198 let error = IBB::try_from(&elem).unwrap_err();
199 let message = match error {
200 Error::ParseError(string) => string,
201 _ => panic!(),
202 };
203 assert_eq!(message, "Required attribute 'block-size' missing in open element.");
204
205 let elem: Element = "<open xmlns='http://jabber.org/protocol/ibb' block-size='128'/>".parse().unwrap();
206 let error = IBB::try_from(&elem).unwrap_err();
207 let message = match error {
208 Error::ParseError(string) => string,
209 _ => panic!(),
210 };
211 assert_eq!(message, "Required attribute 'sid' missing in open element.");
212 }
213
214 #[test]
215 fn test_invalid_stanza() {
216 let elem: Element = "<open xmlns='http://jabber.org/protocol/ibb' block-size='128' sid='coucou' stanza='fdsq'/>".parse().unwrap();
217 let error = IBB::try_from(&elem).unwrap_err();
218 let message = match error {
219 Error::ParseError(string) => string,
220 _ => panic!(),
221 };
222 assert_eq!(message, "Invalid 'stanza' attribute.");
223 }
224}