// Copyright (c) 2019 Emmanuel Gil Peyrot // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. use std::borrow::Cow; use std::str::FromStr; use xso::{error::Error, text::Base64, AsXml, AsXmlText, FromXml, FromXmlText}; use crate::hashes::{Algo, Hash}; use crate::ns; use minidom::IntoAttributeValue; /// A Content-ID, as defined in RFC2111. /// /// The text value SHOULD be of the form algo+hash@bob.xmpp.org, this struct /// enforces that format. #[derive(Clone, Debug, PartialEq)] pub struct ContentId { hash: Hash, } impl FromStr for ContentId { type Err = Error; fn from_str(s: &str) -> Result { let temp: Vec<_> = s.splitn(2, '@').collect(); let temp: Vec<_> = match temp[..] { [lhs, rhs] => { if rhs != "bob.xmpp.org" { return Err(Error::Other("Wrong domain for cid URI.")); } lhs.splitn(2, '+').collect() } _ => return Err(Error::Other("Missing @ in cid URI.")), }; let (algo, hex) = match temp[..] { [lhs, rhs] => { let algo = match lhs { "sha1" => Algo::Sha_1, "sha256" => Algo::Sha_256, _ => unimplemented!(), }; (algo, rhs) } _ => return Err(Error::Other("Missing + in cid URI.")), }; let hash = Hash::from_hex(algo, hex).map_err(Error::text_parse_error)?; Ok(ContentId { hash }) } } impl FromXmlText for ContentId { fn from_xml_text(value: String) -> Result { value.parse().map_err(Error::text_parse_error) } } impl AsXmlText for ContentId { fn as_xml_text(&self) -> Result, Error> { let algo = match self.hash.algo { Algo::Sha_1 => "sha1", Algo::Sha_256 => "sha256", _ => unimplemented!(), }; Ok(Cow::Owned(format!( "{}+{}@bob.xmpp.org", algo, self.hash.to_hex() ))) } } impl IntoAttributeValue for ContentId { fn into_attribute_value(self) -> Option { let algo = match self.hash.algo { Algo::Sha_1 => "sha1", Algo::Sha_256 => "sha256", _ => unimplemented!(), }; Some(format!("{}+{}@bob.xmpp.org", algo, self.hash.to_hex())) } } /// Request for an uncached cid file. #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = ns::BOB, name = "data")] pub struct Data { /// The cid in question. #[xml(attribute)] pub cid: ContentId, /// How long to cache it (in seconds). #[xml(attribute(default, name = "max-age"))] pub max_age: Option, /// The MIME type of the data being transmitted. /// /// See the [IANA MIME Media Types Registry][1] for a list of /// registered types, but unregistered or yet-to-be-registered are /// accepted too. /// /// [1]: #[xml(attribute(default, name = "type"))] pub type_: Option, /// The actual data. #[xml(text = Base64)] pub data: Vec, } #[cfg(test)] mod tests { use super::*; use minidom::Element; use xso::error::FromElementError; #[cfg(target_pointer_width = "32")] #[test] fn test_size() { assert_size!(ContentId, 24); assert_size!(Data, 56); } #[cfg(target_pointer_width = "64")] #[test] fn test_size() { assert_size!(ContentId, 48); assert_size!(Data, 112); } #[test] fn test_simple() { let cid: ContentId = "sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org" .parse() .unwrap(); assert_eq!(cid.hash.algo, Algo::Sha_1); assert_eq!( cid.hash.hash, b"\x8f\x35\xfe\xf1\x10\xff\xc5\xdf\x08\xd5\x79\xa5\x00\x83\xff\x93\x08\xfb\x62\x42" ); assert_eq!( cid.into_attribute_value().unwrap(), "sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org" ); let elem: Element = "".parse().unwrap(); let data = Data::try_from(elem).unwrap(); assert_eq!(data.cid.hash.algo, Algo::Sha_1); assert_eq!( data.cid.hash.hash, b"\x8f\x35\xfe\xf1\x10\xff\xc5\xdf\x08\xd5\x79\xa5\x00\x83\xff\x93\x08\xfb\x62\x42" ); assert!(data.max_age.is_none()); assert!(data.type_.is_none()); assert!(data.data.is_empty()); } #[test] fn invalid_cid() { let error = "Hello world!".parse::().unwrap_err(); let message = match error { Error::Other(string) => string, _ => panic!(), }; assert_eq!(message, "Missing @ in cid URI."); let error = "Hello world@bob.xmpp.org".parse::().unwrap_err(); let message = match error { Error::Other(string) => string, _ => panic!(), }; assert_eq!(message, "Missing + in cid URI."); let error = "sha1+1234@coucou.linkmauve.fr" .parse::() .unwrap_err(); let message = match error { Error::Other(string) => string, _ => panic!(), }; assert_eq!(message, "Wrong domain for cid URI."); let error = "sha1+invalid@bob.xmpp.org" .parse::() .unwrap_err(); let message = match error { Error::TextParseError(error) if error.is::() => error, _ => panic!(), }; assert_eq!(message.to_string(), "invalid digit found in string"); } #[test] fn unknown_child() { let elem: Element = "" .parse() .unwrap(); let error = Data::try_from(elem).unwrap_err(); let message = match error { FromElementError::Invalid(Error::Other(string)) => string, _ => panic!(), }; assert_eq!(message, "Unknown child in Data element."); } }