1use std::{
6 borrow::Cow,
7 collections::{BTreeMap, btree_map},
8 fmt,
9 ops::Deref,
10};
11
12use js_int::UInt;
13use ruma_common::{
14 OwnedMxcUri,
15 serde::{
16 Base64, JsonObject,
17 base64::{Standard, UrlSafe},
18 },
19};
20use ruma_macros::StringEnum;
21use serde::{Deserialize, Serialize, de};
22use serde_json::Value as JsonValue;
23use zeroize::Zeroize;
24
25use crate::PrivOwnedStr;
26
27pub mod avatar;
28pub mod canonical_alias;
29pub mod create;
30pub mod encrypted;
31mod encrypted_file_serde;
32pub mod encryption;
33pub mod guest_access;
34pub mod history_visibility;
35pub mod image_pack;
36pub mod join_rules;
37#[cfg(feature = "unstable-msc4334")]
38pub mod language;
39pub mod member;
40pub mod message;
41pub mod name;
42pub mod pinned_events;
43pub mod policy;
44pub mod power_levels;
45#[cfg(feature = "unstable-msc4495")]
46pub mod presence_sharing;
47pub mod redaction;
48#[cfg(feature = "unstable-msc1763")]
49pub mod retention;
50pub mod server_acl;
51pub mod third_party_invite;
52mod thumbnail_source_serde;
53pub mod tombstone;
54pub mod topic;
55
56#[derive(Clone, Debug, Serialize)]
58#[allow(clippy::exhaustive_enums)]
59pub enum MediaSource {
60 #[serde(rename = "url")]
62 Plain(OwnedMxcUri),
63
64 #[serde(rename = "file")]
66 Encrypted(Box<EncryptedFile>),
67}
68
69impl<'de> Deserialize<'de> for MediaSource {
74 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
75 where
76 D: serde::Deserializer<'de>,
77 {
78 #[derive(Deserialize)]
79 struct MediaSourceJsonRepr {
80 url: Option<OwnedMxcUri>,
81 file: Option<Box<EncryptedFile>>,
82 }
83
84 match MediaSourceJsonRepr::deserialize(deserializer)? {
85 MediaSourceJsonRepr { url: None, file: None } => Err(de::Error::missing_field("url")),
86 MediaSourceJsonRepr { file: Some(file), .. } => Ok(MediaSource::Encrypted(file)),
88 MediaSourceJsonRepr { url: Some(url), .. } => Ok(MediaSource::Plain(url)),
89 }
90 }
91}
92
93#[derive(Clone, Debug, Default, Deserialize, Serialize)]
95#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
96pub struct ImageInfo {
97 #[serde(rename = "h", skip_serializing_if = "Option::is_none")]
99 pub height: Option<UInt>,
100
101 #[serde(rename = "w", skip_serializing_if = "Option::is_none")]
103 pub width: Option<UInt>,
104
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub mimetype: Option<String>,
108
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub size: Option<UInt>,
112
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub thumbnail_info: Option<Box<ThumbnailInfo>>,
116
117 #[serde(flatten, with = "thumbnail_source_serde", skip_serializing_if = "Option::is_none")]
119 pub thumbnail_source: Option<MediaSource>,
120
121 #[cfg(feature = "unstable-msc2448")]
126 #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")]
127 pub blurhash: Option<String>,
128
129 #[cfg(feature = "unstable-msc2448")]
134 #[serde(rename = "xyz.amorgan.thumbhash", skip_serializing_if = "Option::is_none")]
135 pub thumbhash: Option<Base64>,
136
137 #[serde(skip_serializing_if = "Option::is_none")]
146 pub is_animated: Option<bool>,
147}
148
149impl ImageInfo {
150 pub fn new() -> Self {
152 Self::default()
153 }
154}
155
156#[derive(Clone, Debug, Default, Deserialize, Serialize)]
158#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
159pub struct ThumbnailInfo {
160 #[serde(rename = "h", skip_serializing_if = "Option::is_none")]
162 pub height: Option<UInt>,
163
164 #[serde(rename = "w", skip_serializing_if = "Option::is_none")]
166 pub width: Option<UInt>,
167
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub mimetype: Option<String>,
171
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub size: Option<UInt>,
175}
176
177impl ThumbnailInfo {
178 pub fn new() -> Self {
180 Self::default()
181 }
182}
183
184#[derive(Clone, Debug, Deserialize, Serialize)]
186#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
187pub struct EncryptedFile {
188 pub url: OwnedMxcUri,
190
191 #[serde(flatten)]
193 pub info: EncryptedFileInfo,
194
195 pub hashes: EncryptedFileHashes,
199}
200
201impl EncryptedFile {
202 pub fn new(url: OwnedMxcUri, info: EncryptedFileInfo, hashes: EncryptedFileHashes) -> Self {
204 Self { url, info, hashes }
205 }
206}
207
208#[derive(Debug, Clone, Serialize)]
210#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
211#[serde(tag = "v", rename_all = "lowercase")]
212pub enum EncryptedFileInfo {
213 V2(V2EncryptedFileInfo),
215
216 #[doc(hidden)]
217 #[serde(untagged)]
218 _Custom(CustomEncryptedFileInfo),
219}
220
221impl EncryptedFileInfo {
222 pub fn version(&self) -> &str {
226 match self {
227 Self::V2(_) => "v2",
228 Self::_Custom(info) => &info.v,
229 }
230 }
231
232 pub fn data(&self) -> Cow<'_, JsonObject> {
240 fn serialize<T: Serialize>(obj: &T) -> JsonObject {
241 match serde_json::to_value(obj).expect("encrypted file info serialization to succeed") {
242 JsonValue::Object(mut obj) => {
243 obj.remove("body");
244 obj
245 }
246 _ => panic!("all encrypted file info variants must serialize to objects"),
247 }
248 }
249
250 match self {
251 Self::V2(i) => Cow::Owned(serialize(i)),
252 Self::_Custom(i) => Cow::Borrowed(&i.data),
253 }
254 }
255}
256
257impl From<V2EncryptedFileInfo> for EncryptedFileInfo {
258 fn from(value: V2EncryptedFileInfo) -> Self {
259 Self::V2(value)
260 }
261}
262
263#[derive(Clone)]
265#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
266pub struct V2EncryptedFileInfo {
267 pub k: Base64<UrlSafe, [u8; 32]>,
269
270 pub iv: Base64<Standard, [u8; 16]>,
272}
273
274impl V2EncryptedFileInfo {
275 pub fn new(k: Base64<UrlSafe, [u8; 32]>, iv: Base64<Standard, [u8; 16]>) -> Self {
277 Self { k, iv }
278 }
279
280 pub fn encode(k: [u8; 32], iv: [u8; 16]) -> Self {
283 Self::new(Base64::new(k), Base64::new(iv))
284 }
285}
286
287impl fmt::Debug for V2EncryptedFileInfo {
288 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289 f.debug_struct("V2EncryptedFileInfo").finish_non_exhaustive()
290 }
291}
292
293impl Drop for V2EncryptedFileInfo {
294 fn drop(&mut self) {
295 self.k.zeroize();
296 }
297}
298
299#[doc(hidden)]
301#[derive(Debug, Clone, Serialize)]
302pub struct CustomEncryptedFileInfo {
303 v: String,
305
306 #[serde(flatten)]
308 data: JsonObject,
309}
310
311#[derive(Clone, Debug, Default)]
316#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
317pub struct EncryptedFileHashes(BTreeMap<EncryptedFileHashAlgorithm, EncryptedFileHash>);
318
319impl EncryptedFileHashes {
320 pub fn new() -> Self {
322 Self::default()
323 }
324
325 pub fn with_sha256(hash: [u8; 32]) -> Self {
327 std::iter::once(EncryptedFileHash::Sha256(Base64::new(hash))).collect()
328 }
329
330 pub fn insert(&mut self, hash: EncryptedFileHash) -> Option<EncryptedFileHash> {
334 self.0.insert(hash.algorithm(), hash)
335 }
336}
337
338impl Deref for EncryptedFileHashes {
339 type Target = BTreeMap<EncryptedFileHashAlgorithm, EncryptedFileHash>;
340
341 fn deref(&self) -> &Self::Target {
342 &self.0
343 }
344}
345
346impl FromIterator<EncryptedFileHash> for EncryptedFileHashes {
347 fn from_iter<T: IntoIterator<Item = EncryptedFileHash>>(iter: T) -> Self {
348 Self(iter.into_iter().map(|hash| (hash.algorithm(), hash)).collect())
349 }
350}
351
352impl Extend<EncryptedFileHash> for EncryptedFileHashes {
353 fn extend<T: IntoIterator<Item = EncryptedFileHash>>(&mut self, iter: T) {
354 self.0.extend(iter.into_iter().map(|hash| (hash.algorithm(), hash)));
355 }
356}
357
358impl IntoIterator for EncryptedFileHashes {
359 type Item = EncryptedFileHash;
360 type IntoIter = btree_map::IntoValues<EncryptedFileHashAlgorithm, EncryptedFileHash>;
361
362 fn into_iter(self) -> Self::IntoIter {
363 self.0.into_values()
364 }
365}
366
367#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
369#[derive(Clone, StringEnum)]
370#[ruma_enum(rename_all = "lowercase")]
371#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
372pub enum EncryptedFileHashAlgorithm {
373 Sha256,
375
376 #[doc(hidden)]
377 _Custom(PrivOwnedStr),
378}
379
380#[derive(Clone, Debug)]
382#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
383pub enum EncryptedFileHash {
384 Sha256(Base64<Standard, [u8; 32]>),
386
387 #[doc(hidden)]
388 _Custom(CustomEncryptedFileHash),
389}
390
391impl EncryptedFileHash {
392 pub fn algorithm(&self) -> EncryptedFileHashAlgorithm {
394 match self {
395 Self::Sha256(_) => EncryptedFileHashAlgorithm::Sha256,
396 Self::_Custom(custom) => custom.algorithm.as_str().into(),
397 }
398 }
399
400 pub fn as_bytes(&self) -> &[u8] {
402 match self {
403 Self::Sha256(hash) => hash.as_bytes(),
404 Self::_Custom(custom) => custom.hash.as_bytes(),
405 }
406 }
407
408 pub fn into_bytes(self) -> Vec<u8> {
410 match self {
411 Self::Sha256(hash) => hash.into_inner().into(),
412 Self::_Custom(custom) => custom.hash.into_inner(),
413 }
414 }
415}
416
417#[doc(hidden)]
419#[derive(Clone, Debug)]
420pub struct CustomEncryptedFileHash {
421 algorithm: String,
423
424 hash: Base64,
426}
427
428#[cfg(test)]
429mod tests {
430 use assert_matches2::assert_matches;
431 use ruma_common::owned_mxc_uri;
432 use serde::Deserialize;
433 use serde_json::{from_value as from_json_value, json};
434
435 use super::{EncryptedFile, MediaSource, V2EncryptedFileInfo};
436 use crate::room::EncryptedFileHashes;
437
438 #[derive(Deserialize)]
439 struct MsgWithAttachment {
440 #[allow(dead_code)]
441 body: String,
442 #[serde(flatten)]
443 source: MediaSource,
444 }
445
446 #[test]
447 fn prefer_encrypted_attachment_over_plain() {
448 let msg: MsgWithAttachment = from_json_value(json!({
449 "body": "",
450 "file": EncryptedFile::new(
451 owned_mxc_uri!("mxc://localhost/encryptedfile"),
452 V2EncryptedFileInfo::encode([0;32], [1;16]).into(),
453 EncryptedFileHashes::new(),
454 ),
455 "url": "mxc://localhost/file",
456 }))
457 .unwrap();
458
459 assert_matches!(msg.source, MediaSource::Encrypted(_));
460 }
461}