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;
45pub mod redaction;
46pub mod server_acl;
47pub mod third_party_invite;
48mod thumbnail_source_serde;
49pub mod tombstone;
50pub mod topic;
51
52#[derive(Clone, Debug, Serialize)]
54#[allow(clippy::exhaustive_enums)]
55pub enum MediaSource {
56 #[serde(rename = "url")]
58 Plain(OwnedMxcUri),
59
60 #[serde(rename = "file")]
62 Encrypted(Box<EncryptedFile>),
63}
64
65impl<'de> Deserialize<'de> for MediaSource {
70 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
71 where
72 D: serde::Deserializer<'de>,
73 {
74 #[derive(Deserialize)]
75 struct MediaSourceJsonRepr {
76 url: Option<OwnedMxcUri>,
77 file: Option<Box<EncryptedFile>>,
78 }
79
80 match MediaSourceJsonRepr::deserialize(deserializer)? {
81 MediaSourceJsonRepr { url: None, file: None } => Err(de::Error::missing_field("url")),
82 MediaSourceJsonRepr { file: Some(file), .. } => Ok(MediaSource::Encrypted(file)),
84 MediaSourceJsonRepr { url: Some(url), .. } => Ok(MediaSource::Plain(url)),
85 }
86 }
87}
88
89#[derive(Clone, Debug, Default, Deserialize, Serialize)]
91#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
92pub struct ImageInfo {
93 #[serde(rename = "h", skip_serializing_if = "Option::is_none")]
95 pub height: Option<UInt>,
96
97 #[serde(rename = "w", skip_serializing_if = "Option::is_none")]
99 pub width: Option<UInt>,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub mimetype: Option<String>,
104
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub size: Option<UInt>,
108
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub thumbnail_info: Option<Box<ThumbnailInfo>>,
112
113 #[serde(flatten, with = "thumbnail_source_serde", skip_serializing_if = "Option::is_none")]
115 pub thumbnail_source: Option<MediaSource>,
116
117 #[cfg(feature = "unstable-msc2448")]
122 #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")]
123 pub blurhash: Option<String>,
124
125 #[cfg(feature = "unstable-msc2448")]
130 #[serde(rename = "xyz.amorgan.thumbhash", skip_serializing_if = "Option::is_none")]
131 pub thumbhash: Option<Base64>,
132
133 #[serde(skip_serializing_if = "Option::is_none")]
142 pub is_animated: Option<bool>,
143}
144
145impl ImageInfo {
146 pub fn new() -> Self {
148 Self::default()
149 }
150}
151
152#[derive(Clone, Debug, Default, Deserialize, Serialize)]
154#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
155pub struct ThumbnailInfo {
156 #[serde(rename = "h", skip_serializing_if = "Option::is_none")]
158 pub height: Option<UInt>,
159
160 #[serde(rename = "w", skip_serializing_if = "Option::is_none")]
162 pub width: Option<UInt>,
163
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub mimetype: Option<String>,
167
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub size: Option<UInt>,
171}
172
173impl ThumbnailInfo {
174 pub fn new() -> Self {
176 Self::default()
177 }
178}
179
180#[derive(Clone, Debug, Deserialize, Serialize)]
182#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
183pub struct EncryptedFile {
184 pub url: OwnedMxcUri,
186
187 #[serde(flatten)]
189 pub info: EncryptedFileInfo,
190
191 pub hashes: EncryptedFileHashes,
195}
196
197impl EncryptedFile {
198 pub fn new(url: OwnedMxcUri, info: EncryptedFileInfo, hashes: EncryptedFileHashes) -> Self {
200 Self { url, info, hashes }
201 }
202}
203
204#[derive(Debug, Clone, Serialize)]
206#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
207#[serde(tag = "v", rename_all = "lowercase")]
208pub enum EncryptedFileInfo {
209 V2(V2EncryptedFileInfo),
211
212 #[doc(hidden)]
213 #[serde(untagged)]
214 _Custom(CustomEncryptedFileInfo),
215}
216
217impl EncryptedFileInfo {
218 pub fn version(&self) -> &str {
222 match self {
223 Self::V2(_) => "v2",
224 Self::_Custom(info) => &info.v,
225 }
226 }
227
228 pub fn data(&self) -> Cow<'_, JsonObject> {
236 fn serialize<T: Serialize>(obj: &T) -> JsonObject {
237 match serde_json::to_value(obj).expect("encrypted file info serialization to succeed") {
238 JsonValue::Object(mut obj) => {
239 obj.remove("body");
240 obj
241 }
242 _ => panic!("all encrypted file info variants must serialize to objects"),
243 }
244 }
245
246 match self {
247 Self::V2(i) => Cow::Owned(serialize(i)),
248 Self::_Custom(i) => Cow::Borrowed(&i.data),
249 }
250 }
251}
252
253impl From<V2EncryptedFileInfo> for EncryptedFileInfo {
254 fn from(value: V2EncryptedFileInfo) -> Self {
255 Self::V2(value)
256 }
257}
258
259#[derive(Clone)]
261#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
262pub struct V2EncryptedFileInfo {
263 pub k: Base64<UrlSafe, [u8; 32]>,
265
266 pub iv: Base64<Standard, [u8; 16]>,
268}
269
270impl V2EncryptedFileInfo {
271 pub fn new(k: Base64<UrlSafe, [u8; 32]>, iv: Base64<Standard, [u8; 16]>) -> Self {
273 Self { k, iv }
274 }
275
276 pub fn encode(k: [u8; 32], iv: [u8; 16]) -> Self {
279 Self::new(Base64::new(k), Base64::new(iv))
280 }
281}
282
283impl fmt::Debug for V2EncryptedFileInfo {
284 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285 f.debug_struct("V2EncryptedFileInfo").finish_non_exhaustive()
286 }
287}
288
289impl Drop for V2EncryptedFileInfo {
290 fn drop(&mut self) {
291 self.k.zeroize();
292 }
293}
294
295#[doc(hidden)]
297#[derive(Debug, Clone, Serialize)]
298pub struct CustomEncryptedFileInfo {
299 v: String,
301
302 #[serde(flatten)]
304 data: JsonObject,
305}
306
307#[derive(Clone, Debug, Default)]
312#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
313pub struct EncryptedFileHashes(BTreeMap<EncryptedFileHashAlgorithm, EncryptedFileHash>);
314
315impl EncryptedFileHashes {
316 pub fn new() -> Self {
318 Self::default()
319 }
320
321 pub fn with_sha256(hash: [u8; 32]) -> Self {
323 std::iter::once(EncryptedFileHash::Sha256(Base64::new(hash))).collect()
324 }
325
326 pub fn insert(&mut self, hash: EncryptedFileHash) -> Option<EncryptedFileHash> {
330 self.0.insert(hash.algorithm(), hash)
331 }
332}
333
334impl Deref for EncryptedFileHashes {
335 type Target = BTreeMap<EncryptedFileHashAlgorithm, EncryptedFileHash>;
336
337 fn deref(&self) -> &Self::Target {
338 &self.0
339 }
340}
341
342impl FromIterator<EncryptedFileHash> for EncryptedFileHashes {
343 fn from_iter<T: IntoIterator<Item = EncryptedFileHash>>(iter: T) -> Self {
344 Self(iter.into_iter().map(|hash| (hash.algorithm(), hash)).collect())
345 }
346}
347
348impl Extend<EncryptedFileHash> for EncryptedFileHashes {
349 fn extend<T: IntoIterator<Item = EncryptedFileHash>>(&mut self, iter: T) {
350 self.0.extend(iter.into_iter().map(|hash| (hash.algorithm(), hash)));
351 }
352}
353
354impl IntoIterator for EncryptedFileHashes {
355 type Item = EncryptedFileHash;
356 type IntoIter = btree_map::IntoValues<EncryptedFileHashAlgorithm, EncryptedFileHash>;
357
358 fn into_iter(self) -> Self::IntoIter {
359 self.0.into_values()
360 }
361}
362
363#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
365#[derive(Clone, StringEnum)]
366#[ruma_enum(rename_all = "lowercase")]
367#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
368pub enum EncryptedFileHashAlgorithm {
369 Sha256,
371
372 #[doc(hidden)]
373 _Custom(PrivOwnedStr),
374}
375
376#[derive(Clone, Debug)]
378#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
379pub enum EncryptedFileHash {
380 Sha256(Base64<Standard, [u8; 32]>),
382
383 #[doc(hidden)]
384 _Custom(CustomEncryptedFileHash),
385}
386
387impl EncryptedFileHash {
388 pub fn algorithm(&self) -> EncryptedFileHashAlgorithm {
390 match self {
391 Self::Sha256(_) => EncryptedFileHashAlgorithm::Sha256,
392 Self::_Custom(custom) => custom.algorithm.as_str().into(),
393 }
394 }
395
396 pub fn as_bytes(&self) -> &[u8] {
398 match self {
399 Self::Sha256(hash) => hash.as_bytes(),
400 Self::_Custom(custom) => custom.hash.as_bytes(),
401 }
402 }
403
404 pub fn into_bytes(self) -> Vec<u8> {
406 match self {
407 Self::Sha256(hash) => hash.into_inner().into(),
408 Self::_Custom(custom) => custom.hash.into_inner(),
409 }
410 }
411}
412
413#[doc(hidden)]
415#[derive(Clone, Debug)]
416pub struct CustomEncryptedFileHash {
417 algorithm: String,
419
420 hash: Base64,
422}
423
424#[cfg(test)]
425mod tests {
426 use assert_matches2::assert_matches;
427 use ruma_common::owned_mxc_uri;
428 use serde::Deserialize;
429 use serde_json::{from_value as from_json_value, json};
430
431 use super::{EncryptedFile, MediaSource, V2EncryptedFileInfo};
432 use crate::room::EncryptedFileHashes;
433
434 #[derive(Deserialize)]
435 struct MsgWithAttachment {
436 #[allow(dead_code)]
437 body: String,
438 #[serde(flatten)]
439 source: MediaSource,
440 }
441
442 #[test]
443 fn prefer_encrypted_attachment_over_plain() {
444 let msg: MsgWithAttachment = from_json_value(json!({
445 "body": "",
446 "file": EncryptedFile::new(
447 owned_mxc_uri!("mxc://localhost/encryptedfile"),
448 V2EncryptedFileInfo::encode([0;32], [1;16]).into(),
449 EncryptedFileHashes::new(),
450 ),
451 "url": "mxc://localhost/file",
452 }))
453 .unwrap();
454
455 assert_matches!(msg.source, MediaSource::Encrypted(_));
456 }
457}