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