bob.rs

  1// Copyright (c) 2019 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 alloc::borrow::Cow;
  8use core::str::FromStr;
  9
 10use xso::{error::Error, text::Base64, AsXml, AsXmlText, FromXml, FromXmlText};
 11
 12use crate::hashes::{Algo, Hash};
 13use crate::ns;
 14use minidom::IntoAttributeValue;
 15
 16/// A Content-ID, as defined in RFC2111.
 17///
 18/// The text value SHOULD be of the form algo+hash@bob.xmpp.org, this struct
 19/// enforces that format.
 20#[derive(Clone, Debug, PartialEq)]
 21pub struct ContentId {
 22    hash: Hash,
 23}
 24
 25impl FromStr for ContentId {
 26    type Err = Error;
 27
 28    fn from_str(s: &str) -> Result<Self, Error> {
 29        let (lhs, rhs) = s
 30            .split_once('@')
 31            .ok_or(Error::Other("Missing @ in cid URI."))?;
 32        if rhs != "bob.xmpp.org" {
 33            return Err(Error::Other("Wrong domain for cid URI."));
 34        }
 35        let (algo, hex) = lhs
 36            .split_once('+')
 37            .ok_or(Error::Other("Missing + in cid URI."))?;
 38        let algo = match algo {
 39            "sha1" => Algo::Sha_1,
 40            "sha256" => Algo::Sha_256,
 41            _ => unimplemented!(),
 42        };
 43        let hash = Hash::from_hex(algo, hex).map_err(Error::text_parse_error)?;
 44        Ok(ContentId { hash })
 45    }
 46}
 47
 48impl FromXmlText for ContentId {
 49    fn from_xml_text(value: String) -> Result<Self, Error> {
 50        value.parse().map_err(Error::text_parse_error)
 51    }
 52}
 53
 54impl AsXmlText for ContentId {
 55    fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
 56        let algo = match self.hash.algo {
 57            Algo::Sha_1 => "sha1",
 58            Algo::Sha_256 => "sha256",
 59            _ => unimplemented!(),
 60        };
 61        Ok(Cow::Owned(format!(
 62            "{}+{}@bob.xmpp.org",
 63            algo,
 64            self.hash.to_hex()
 65        )))
 66    }
 67}
 68
 69impl IntoAttributeValue for ContentId {
 70    fn into_attribute_value(self) -> Option<String> {
 71        let algo = match self.hash.algo {
 72            Algo::Sha_1 => "sha1",
 73            Algo::Sha_256 => "sha256",
 74            _ => unimplemented!(),
 75        };
 76        Some(format!("{}+{}@bob.xmpp.org", algo, self.hash.to_hex()))
 77    }
 78}
 79
 80/// Request for an uncached cid file.
 81#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
 82#[xml(namespace = ns::BOB, name = "data")]
 83pub struct Data {
 84    /// The cid in question.
 85    #[xml(attribute)]
 86    pub cid: ContentId,
 87
 88    /// How long to cache it (in seconds).
 89    #[xml(attribute(default, name = "max-age"))]
 90    pub max_age: Option<usize>,
 91
 92    /// The MIME type of the data being transmitted.
 93    ///
 94    /// See the [IANA MIME Media Types Registry][1] for a list of
 95    /// registered types, but unregistered or yet-to-be-registered are
 96    /// accepted too.
 97    ///
 98    /// [1]: <https://www.iana.org/assignments/media-types/media-types.xhtml>
 99    #[xml(attribute(default, name = "type"))]
100    pub type_: Option<String>,
101
102    /// The actual data.
103    #[xml(text = Base64)]
104    pub data: Vec<u8>,
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use minidom::Element;
111    use xso::error::FromElementError;
112
113    #[cfg(target_pointer_width = "32")]
114    #[test]
115    fn test_size() {
116        assert_size!(ContentId, 24);
117        assert_size!(Data, 56);
118    }
119
120    #[cfg(target_pointer_width = "64")]
121    #[test]
122    fn test_size() {
123        assert_size!(ContentId, 48);
124        assert_size!(Data, 112);
125    }
126
127    #[test]
128    fn test_simple() {
129        let cid: ContentId = "sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org"
130            .parse()
131            .unwrap();
132        assert_eq!(cid.hash.algo, Algo::Sha_1);
133        assert_eq!(
134            cid.hash.hash,
135            b"\x8f\x35\xfe\xf1\x10\xff\xc5\xdf\x08\xd5\x79\xa5\x00\x83\xff\x93\x08\xfb\x62\x42"
136        );
137        assert_eq!(
138            cid.into_attribute_value().unwrap(),
139            "sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org"
140        );
141
142        let elem: Element = "<data xmlns='urn:xmpp:bob' cid='sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org'/>".parse().unwrap();
143        let data = Data::try_from(elem).unwrap();
144        assert_eq!(data.cid.hash.algo, Algo::Sha_1);
145        assert_eq!(
146            data.cid.hash.hash,
147            b"\x8f\x35\xfe\xf1\x10\xff\xc5\xdf\x08\xd5\x79\xa5\x00\x83\xff\x93\x08\xfb\x62\x42"
148        );
149        assert!(data.max_age.is_none());
150        assert!(data.type_.is_none());
151        assert!(data.data.is_empty());
152    }
153
154    #[test]
155    fn invalid_cid() {
156        let error = "Hello world!".parse::<ContentId>().unwrap_err();
157        let message = match error {
158            Error::Other(string) => string,
159            _ => panic!(),
160        };
161        assert_eq!(message, "Missing @ in cid URI.");
162
163        let error = "Hello world@bob.xmpp.org".parse::<ContentId>().unwrap_err();
164        let message = match error {
165            Error::Other(string) => string,
166            _ => panic!(),
167        };
168        assert_eq!(message, "Missing + in cid URI.");
169
170        let error = "sha1+1234@coucou.linkmauve.fr"
171            .parse::<ContentId>()
172            .unwrap_err();
173        let message = match error {
174            Error::Other(string) => string,
175            _ => panic!(),
176        };
177        assert_eq!(message, "Wrong domain for cid URI.");
178
179        let error = "sha1+invalid@bob.xmpp.org"
180            .parse::<ContentId>()
181            .unwrap_err();
182        let message = match error {
183            Error::TextParseError(error) if error.is::<core::num::ParseIntError>() => error,
184            _ => panic!(),
185        };
186        assert_eq!(message.to_string(), "invalid digit found in string");
187    }
188
189    #[test]
190    #[cfg_attr(feature = "disable-validation", should_panic = "Result::unwrap_err")]
191    fn unknown_child() {
192        let elem: Element = "<data xmlns='urn:xmpp:bob' cid='sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org'><coucou/></data>"
193            .parse()
194            .unwrap();
195        let error = Data::try_from(elem).unwrap_err();
196        let message = match error {
197            FromElementError::Invalid(Error::Other(string)) => string,
198            _ => panic!(),
199        };
200        assert_eq!(message, "Unknown child in Data element.");
201    }
202}