xmpp_parsers/
http_upload.rs

1// Copyright (c) 2021 Maxime “pep” Buquet <pep@bouah.net>
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 xso::{error::Error, AsXml, AsXmlText, FromXml, FromXmlText};
8
9use crate::iq::{IqGetPayload, IqResultPayload};
10use crate::ns;
11use alloc::borrow::Cow;
12
13/// Requesting a slot
14#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
15#[xml(namespace = ns::HTTP_UPLOAD, name = "request")]
16pub struct SlotRequest {
17    /// The filename to be uploaded.
18    #[xml(attribute)]
19    pub filename: String,
20
21    /// Size of the file to be uploaded.
22    #[xml(attribute)]
23    pub size: u64,
24
25    /// Content-Type of the file.
26    #[xml(attribute(name = "content-type"))]
27    pub content_type: Option<String>,
28}
29
30impl IqGetPayload for SlotRequest {}
31
32/// All three possible header names.
33#[derive(Debug, Clone, PartialEq)]
34pub enum HeaderName {
35    /// Authorization header
36    Authorization,
37
38    /// Cookie header
39    Cookie,
40
41    /// Expires header
42    Expires,
43}
44
45impl HeaderName {
46    /// Returns the string version of this enum value.
47    pub fn as_str(&self) -> &'static str {
48        match self {
49            HeaderName::Authorization => "Authorization",
50            HeaderName::Cookie => "Cookie",
51            HeaderName::Expires => "Expires",
52        }
53    }
54}
55
56impl FromXmlText for HeaderName {
57    fn from_xml_text(mut s: String) -> Result<Self, Error> {
58        s.make_ascii_lowercase();
59        Ok(match s.as_str() {
60            "authorization" => HeaderName::Authorization,
61            "cookie" => HeaderName::Cookie,
62            "expires" => HeaderName::Expires,
63            _ => {
64                return Err(Error::Other(
65                    "Header name must be either 'Authorization', 'Cookie', or 'Expires'.",
66                ))
67            }
68        })
69    }
70}
71
72impl AsXmlText for HeaderName {
73    fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
74        Ok(Cow::Borrowed(self.as_str()))
75    }
76}
77
78/// Slot header
79#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
80#[xml(namespace = ns::HTTP_UPLOAD, name = "header")]
81pub struct Header {
82    /// Name of the header
83    #[xml(attribute)]
84    pub name: HeaderName,
85
86    /// Value of the header
87    #[xml(text)]
88    pub value: String,
89}
90
91/// Put URL
92#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
93#[xml(namespace = ns::HTTP_UPLOAD, name = "put")]
94pub struct Put {
95    /// URL
96    #[xml(attribute)]
97    pub url: String,
98
99    /// Header list
100    #[xml(child(n = ..))]
101    pub headers: Vec<Header>,
102}
103
104/// Get URL
105#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
106#[xml(namespace = ns::HTTP_UPLOAD, name = "get")]
107pub struct Get {
108    /// URL
109    #[xml(attribute)]
110    pub url: String,
111}
112
113/// Requesting a slot
114#[derive(FromXml, AsXml, Debug, Clone, PartialEq)]
115#[xml(namespace = ns::HTTP_UPLOAD, name = "slot")]
116pub struct SlotResult {
117    /// Put URL and headers
118    #[xml(child)]
119    pub put: Put,
120
121    /// Get URL
122    #[xml(child)]
123    pub get: Get,
124}
125
126impl IqResultPayload for SlotResult {}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use minidom::Element;
132
133    #[cfg(target_pointer_width = "32")]
134    #[test]
135    fn test_size() {
136        assert_size!(SlotRequest, 32);
137        assert_size!(HeaderName, 1);
138        assert_size!(Header, 16);
139        assert_size!(Put, 24);
140        assert_size!(Get, 12);
141        assert_size!(SlotResult, 36);
142    }
143
144    #[cfg(target_pointer_width = "64")]
145    #[test]
146    fn test_size() {
147        assert_size!(SlotRequest, 56);
148        assert_size!(HeaderName, 1);
149        assert_size!(Header, 32);
150        assert_size!(Put, 48);
151        assert_size!(Get, 24);
152        assert_size!(SlotResult, 72);
153    }
154
155    #[test]
156    fn test_slot_request() {
157        let elem: Element = "<request xmlns='urn:xmpp:http:upload:0'
158            filename='très cool.jpg'
159            size='23456'
160            content-type='image/jpeg' />"
161            .parse()
162            .unwrap();
163        let slot = SlotRequest::try_from(elem).unwrap();
164        assert_eq!(slot.filename, String::from("très cool.jpg"));
165        assert_eq!(slot.size, 23456);
166        assert_eq!(slot.content_type, Some(String::from("image/jpeg")));
167    }
168
169    #[test]
170    fn test_slot_result() {
171        let elem: Element = "<slot xmlns='urn:xmpp:http:upload:0'>
172            <put url='https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/tr%C3%A8s%20cool.jpg'>
173              <header name='Authorization'>Basic Base64String==</header>
174              <header name='Cookie'>foo=bar; user=romeo</header>
175            </put>
176            <get url='https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/tr%C3%A8s%20cool.jpg' />
177          </slot>"
178            .parse()
179            .unwrap();
180        let slot = SlotResult::try_from(elem).unwrap();
181        assert_eq!(slot.put.url, String::from("https://upload.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/tr%C3%A8s%20cool.jpg"));
182        assert_eq!(
183            slot.put.headers[0],
184            Header {
185                name: HeaderName::Authorization,
186                value: String::from("Basic Base64String==")
187            }
188        );
189        assert_eq!(
190            slot.put.headers[1],
191            Header {
192                name: HeaderName::Cookie,
193                value: String::from("foo=bar; user=romeo")
194            }
195        );
196        assert_eq!(slot.get.url, String::from("https://download.montague.tld/4a771ac1-f0b2-4a4a-9700-f2a26fa2bb67/tr%C3%A8s%20cool.jpg"));
197    }
198
199    #[test]
200    fn test_result_no_header() {
201        let elem: Element = "<slot xmlns='urn:xmpp:http:upload:0'>
202            <put url='https://URL' />
203            <get url='https://URL' />
204          </slot>"
205            .parse()
206            .unwrap();
207        let slot = SlotResult::try_from(elem).unwrap();
208        assert_eq!(slot.put.url, String::from("https://URL"));
209        assert_eq!(slot.put.headers.len(), 0);
210        assert_eq!(slot.get.url, String::from("https://URL"));
211    }
212
213    #[test]
214    fn test_result_bad_header() {
215        let elem: Element = "<slot xmlns='urn:xmpp:http:upload:0'>
216            <put url='https://URL'>
217              <header name='EvilHeader'>EvilValue</header>
218            </put>
219            <get url='https://URL' />
220          </slot>"
221            .parse()
222            .unwrap();
223        SlotResult::try_from(elem).unwrap_err();
224    }
225}