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}