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