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;
48pub mod server_acl;
49pub mod third_party_invite;
50mod thumbnail_source_serde;
51pub mod tombstone;
52pub mod topic;
53
54#[derive(Clone, Debug, Serialize)]
56#[allow(clippy::exhaustive_enums)]
57pub enum MediaSource {
58 #[serde(rename = "url")]
60 Plain(OwnedMxcUri),
61
62 #[serde(rename = "file")]
64 Encrypted(Box<EncryptedFile>),
65}
66
67impl<'de> Deserialize<'de> for MediaSource {
72 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
73 where
74 D: serde::Deserializer<'de>,
75 {
76 #[derive(Deserialize)]
77 struct MediaSourceJsonRepr {
78 url: Option<OwnedMxcUri>,
79 file: Option<Box<EncryptedFile>>,
80 }
81
82 match MediaSourceJsonRepr::deserialize(deserializer)? {
83 MediaSourceJsonRepr { url: None, file: None } => Err(de::Error::missing_field("url")),
84 MediaSourceJsonRepr { file: Some(file), .. } => Ok(MediaSource::Encrypted(file)),
86 MediaSourceJsonRepr { url: Some(url), .. } => Ok(MediaSource::Plain(url)),
87 }
88 }
89}
90
91#[derive(Clone, Debug, Default, Deserialize, Serialize)]
93#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
94pub struct ImageInfo {
95 #[serde(rename = "h", skip_serializing_if = "Option::is_none")]
97 pub height: Option<UInt>,
98
99 #[serde(rename = "w", skip_serializing_if = "Option::is_none")]
101 pub width: Option<UInt>,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub mimetype: Option<String>,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub size: Option<UInt>,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub thumbnail_info: Option<Box<ThumbnailInfo>>,
114
115 #[serde(flatten, with = "thumbnail_source_serde", skip_serializing_if = "Option::is_none")]
117 pub thumbnail_source: Option<MediaSource>,
118
119 #[cfg(feature = "unstable-msc2448")]
124 #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")]
125 pub blurhash: Option<String>,
126
127 #[cfg(feature = "unstable-msc2448")]
132 #[serde(rename = "xyz.amorgan.thumbhash", skip_serializing_if = "Option::is_none")]
133 pub thumbhash: Option<Base64>,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
144 pub is_animated: Option<bool>,
145}
146
147impl ImageInfo {
148 pub fn new() -> Self {
150 Self::default()
151 }
152}
153
154#[derive(Clone, Debug, Default, Deserialize, Serialize)]
156#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
157pub struct ThumbnailInfo {
158 #[serde(rename = "h", skip_serializing_if = "Option::is_none")]
160 pub height: Option<UInt>,
161
162 #[serde(rename = "w", skip_serializing_if = "Option::is_none")]
164 pub width: Option<UInt>,
165
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub mimetype: Option<String>,
169
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub size: Option<UInt>,
173}
174
175impl ThumbnailInfo {
176 pub fn new() -> Self {
178 Self::default()
179 }
180}
181
182#[derive(Clone, Debug, Deserialize, Serialize)]
184#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
185pub struct EncryptedFile {
186 pub url: OwnedMxcUri,
188
189 #[serde(flatten)]
191 pub info: EncryptedFileInfo,
192
193 pub hashes: EncryptedFileHashes,
197}
198
199impl EncryptedFile {
200 pub fn new(url: OwnedMxcUri, info: EncryptedFileInfo, hashes: EncryptedFileHashes) -> Self {
202 Self { url, info, hashes }
203 }
204}
205
206#[derive(Debug, Clone, Serialize)]
208#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
209#[serde(tag = "v", rename_all = "lowercase")]
210pub enum EncryptedFileInfo {
211 V2(V2EncryptedFileInfo),
213
214 #[doc(hidden)]
215 #[serde(untagged)]
216 _Custom(CustomEncryptedFileInfo),
217}
218
219impl EncryptedFileInfo {
220 pub fn version(&self) -> &str {
224 match self {
225 Self::V2(_) => "v2",
226 Self::_Custom(info) => &info.v,
227 }
228 }
229
230 pub fn data(&self) -> Cow<'_, JsonObject> {
238 fn serialize<T: Serialize>(obj: &T) -> JsonObject {
239 match serde_json::to_value(obj).expect("encrypted file info serialization to succeed") {
240 JsonValue::Object(mut obj) => {
241 obj.remove("body");
242 obj
243 }
244 _ => panic!("all encrypted file info variants must serialize to objects"),
245 }
246 }
247
248 match self {
249 Self::V2(i) => Cow::Owned(serialize(i)),
250 Self::_Custom(i) => Cow::Borrowed(&i.data),
251 }
252 }
253}
254
255impl From<V2EncryptedFileInfo> for EncryptedFileInfo {
256 fn from(value: V2EncryptedFileInfo) -> Self {
257 Self::V2(value)
258 }
259}
260
261#[derive(Clone)]
263#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
264pub struct V2EncryptedFileInfo {
265 pub k: Base64<UrlSafe, [u8; 32]>,
267
268 pub iv: Base64<Standard, [u8; 16]>,
270}
271
272impl V2EncryptedFileInfo {
273 pub fn new(k: Base64<UrlSafe, [u8; 32]>, iv: Base64<Standard, [u8; 16]>) -> Self {
275 Self { k, iv }
276 }
277
278 pub fn encode(k: [u8; 32], iv: [u8; 16]) -> Self {
281 Self::new(Base64::new(k), Base64::new(iv))
282 }
283}
284
285impl fmt::Debug for V2EncryptedFileInfo {
286 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287 f.debug_struct("V2EncryptedFileInfo").finish_non_exhaustive()
288 }
289}
290
291impl Drop for V2EncryptedFileInfo {
292 fn drop(&mut self) {
293 self.k.zeroize();
294 }
295}
296
297#[doc(hidden)]
299#[derive(Debug, Clone, Serialize)]
300pub struct CustomEncryptedFileInfo {
301 v: String,
303
304 #[serde(flatten)]
306 data: JsonObject,
307}
308
309#[derive(Clone, Debug, Default)]
314#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
315pub struct EncryptedFileHashes(BTreeMap<EncryptedFileHashAlgorithm, EncryptedFileHash>);
316
317impl EncryptedFileHashes {
318 pub fn new() -> Self {
320 Self::default()
321 }
322
323 pub fn with_sha256(hash: [u8; 32]) -> Self {
325 std::iter::once(EncryptedFileHash::Sha256(Base64::new(hash))).collect()
326 }
327
328 pub fn insert(&mut self, hash: EncryptedFileHash) -> Option<EncryptedFileHash> {
332 self.0.insert(hash.algorithm(), hash)
333 }
334}
335
336impl Deref for EncryptedFileHashes {
337 type Target = BTreeMap<EncryptedFileHashAlgorithm, EncryptedFileHash>;
338
339 fn deref(&self) -> &Self::Target {
340 &self.0
341 }
342}
343
344impl FromIterator<EncryptedFileHash> for EncryptedFileHashes {
345 fn from_iter<T: IntoIterator<Item = EncryptedFileHash>>(iter: T) -> Self {
346 Self(iter.into_iter().map(|hash| (hash.algorithm(), hash)).collect())
347 }
348}
349
350impl Extend<EncryptedFileHash> for EncryptedFileHashes {
351 fn extend<T: IntoIterator<Item = EncryptedFileHash>>(&mut self, iter: T) {
352 self.0.extend(iter.into_iter().map(|hash| (hash.algorithm(), hash)));
353 }
354}
355
356impl IntoIterator for EncryptedFileHashes {
357 type Item = EncryptedFileHash;
358 type IntoIter = btree_map::IntoValues<EncryptedFileHashAlgorithm, EncryptedFileHash>;
359
360 fn into_iter(self) -> Self::IntoIter {
361 self.0.into_values()
362 }
363}
364
365#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
367#[derive(Clone, StringEnum)]
368#[ruma_enum(rename_all = "lowercase")]
369#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
370pub enum EncryptedFileHashAlgorithm {
371 Sha256,
373
374 #[doc(hidden)]
375 _Custom(PrivOwnedStr),
376}
377
378#[derive(Clone, Debug)]
380#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
381pub enum EncryptedFileHash {
382 Sha256(Base64<Standard, [u8; 32]>),
384
385 #[doc(hidden)]
386 _Custom(CustomEncryptedFileHash),
387}
388
389impl EncryptedFileHash {
390 pub fn algorithm(&self) -> EncryptedFileHashAlgorithm {
392 match self {
393 Self::Sha256(_) => EncryptedFileHashAlgorithm::Sha256,
394 Self::_Custom(custom) => custom.algorithm.as_str().into(),
395 }
396 }
397
398 pub fn as_bytes(&self) -> &[u8] {
400 match self {
401 Self::Sha256(hash) => hash.as_bytes(),
402 Self::_Custom(custom) => custom.hash.as_bytes(),
403 }
404 }
405
406 pub fn into_bytes(self) -> Vec<u8> {
408 match self {
409 Self::Sha256(hash) => hash.into_inner().into(),
410 Self::_Custom(custom) => custom.hash.into_inner(),
411 }
412 }
413}
414
415#[doc(hidden)]
417#[derive(Clone, Debug)]
418pub struct CustomEncryptedFileHash {
419 algorithm: String,
421
422 hash: Base64,
424}
425
426#[cfg(test)]
427mod tests {
428 use assert_matches2::assert_matches;
429 use ruma_common::owned_mxc_uri;
430 use serde::Deserialize;
431 use serde_json::{from_value as from_json_value, json};
432
433 use super::{EncryptedFile, MediaSource, V2EncryptedFileInfo};
434 use crate::room::EncryptedFileHashes;
435
436 #[derive(Deserialize)]
437 struct MsgWithAttachment {
438 #[allow(dead_code)]
439 body: String,
440 #[serde(flatten)]
441 source: MediaSource,
442 }
443
444 #[test]
445 fn prefer_encrypted_attachment_over_plain() {
446 let msg: MsgWithAttachment = from_json_value(json!({
447 "body": "",
448 "file": EncryptedFile::new(
449 owned_mxc_uri!("mxc://localhost/encryptedfile"),
450 V2EncryptedFileInfo::encode([0;32], [1;16]).into(),
451 EncryptedFileHashes::new(),
452 ),
453 "url": "mxc://localhost/file",
454 }))
455 .unwrap();
456
457 assert_matches!(msg.source, MediaSource::Encrypted(_));
458 }
459}