1use 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#[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#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
82#[xml(namespace = ns::BOB, name = "data")]
83pub struct Data {
84 #[xml(attribute)]
86 pub cid: ContentId,
87
88 #[xml(attribute(default, name = "max-age"))]
90 pub max_age: Option<usize>,
91
92 #[xml(attribute(default, name = "type"))]
100 pub type_: Option<String>,
101
102 #[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}