1use std::{
6 collections::{BTreeMap, btree_map},
7 fmt,
8 ops::Deref,
9};
10
11use as_variant::as_variant;
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 zeroize::Zeroize;
23
24use crate::PrivOwnedStr;
25
26pub mod avatar;
27pub mod canonical_alias;
28pub mod create;
29pub mod encrypted;
30mod encrypted_file_serde;
31pub mod encryption;
32pub mod guest_access;
33pub mod history_visibility;
34pub mod join_rules;
35#[cfg(feature = "unstable-msc4334")]
36pub mod language;
37pub mod member;
38pub mod message;
39pub mod name;
40pub mod pinned_events;
41pub mod policy;
42pub mod power_levels;
43pub mod redaction;
44pub mod server_acl;
45pub mod third_party_invite;
46mod thumbnail_source_serde;
47pub mod tombstone;
48pub mod topic;
49
50#[derive(Clone, Debug, Serialize)]
52#[allow(clippy::exhaustive_enums)]
53pub enum MediaSource {
54 #[serde(rename = "url")]
56 Plain(OwnedMxcUri),
57
58 #[serde(rename = "file")]
60 Encrypted(Box<EncryptedFile>),
61}
62
63impl<'de> Deserialize<'de> for MediaSource {
68 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
69 where
70 D: serde::Deserializer<'de>,
71 {
72 #[derive(Deserialize)]
73 struct MediaSourceJsonRepr {
74 url: Option<OwnedMxcUri>,
75 file: Option<Box<EncryptedFile>>,
76 }
77
78 match MediaSourceJsonRepr::deserialize(deserializer)? {
79 MediaSourceJsonRepr { url: None, file: None } => Err(de::Error::missing_field("url")),
80 MediaSourceJsonRepr { file: Some(file), .. } => Ok(MediaSource::Encrypted(file)),
82 MediaSourceJsonRepr { url: Some(url), .. } => Ok(MediaSource::Plain(url)),
83 }
84 }
85}
86
87#[derive(Clone, Debug, Default, Deserialize, Serialize)]
89#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
90pub struct ImageInfo {
91 #[serde(rename = "h", skip_serializing_if = "Option::is_none")]
93 pub height: Option<UInt>,
94
95 #[serde(rename = "w", skip_serializing_if = "Option::is_none")]
97 pub width: Option<UInt>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub mimetype: Option<String>,
102
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub size: Option<UInt>,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub thumbnail_info: Option<Box<ThumbnailInfo>>,
110
111 #[serde(flatten, with = "thumbnail_source_serde", skip_serializing_if = "Option::is_none")]
113 pub thumbnail_source: Option<MediaSource>,
114
115 #[cfg(feature = "unstable-msc2448")]
120 #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")]
121 pub blurhash: Option<String>,
122
123 #[cfg(feature = "unstable-msc2448")]
128 #[serde(rename = "xyz.amorgan.thumbhash", skip_serializing_if = "Option::is_none")]
129 pub thumbhash: Option<Base64>,
130
131 #[serde(skip_serializing_if = "Option::is_none")]
140 pub is_animated: Option<bool>,
141}
142
143impl ImageInfo {
144 pub fn new() -> Self {
146 Self::default()
147 }
148}
149
150#[derive(Clone, Debug, Default, Deserialize, Serialize)]
152#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
153pub struct ThumbnailInfo {
154 #[serde(rename = "h", skip_serializing_if = "Option::is_none")]
156 pub height: Option<UInt>,
157
158 #[serde(rename = "w", skip_serializing_if = "Option::is_none")]
160 pub width: Option<UInt>,
161
162 #[serde(skip_serializing_if = "Option::is_none")]
164 pub mimetype: Option<String>,
165
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub size: Option<UInt>,
169}
170
171impl ThumbnailInfo {
172 pub fn new() -> Self {
174 Self::default()
175 }
176}
177
178#[derive(Clone, Debug, Deserialize, Serialize)]
180#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
181pub struct EncryptedFile {
182 pub url: OwnedMxcUri,
184
185 #[serde(flatten)]
187 pub info: EncryptedFileInfo,
188
189 pub hashes: EncryptedFileHashes,
193}
194
195impl EncryptedFile {
196 pub fn new(url: OwnedMxcUri, info: EncryptedFileInfo, hashes: EncryptedFileHashes) -> Self {
198 Self { url, info, hashes }
199 }
200}
201
202#[derive(Debug, Clone, Deserialize, Serialize)]
204#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
205#[serde(tag = "v", rename_all = "lowercase")]
206pub enum EncryptedFileInfo {
207 V2(V2EncryptedFileInfo),
209
210 #[doc(hidden)]
211 #[serde(untagged)]
212 _Custom(CustomEncryptedFileInfo),
213}
214
215impl EncryptedFileInfo {
216 pub fn version(&self) -> &str {
220 match self {
221 Self::V2(_) => "v2",
222 Self::_Custom(info) => &info.v,
223 }
224 }
225
226 pub fn custom_data(&self) -> Option<&JsonObject> {
229 as_variant!(self, Self::_Custom(info) => &info.data)
230 }
231}
232
233impl From<V2EncryptedFileInfo> for EncryptedFileInfo {
234 fn from(value: V2EncryptedFileInfo) -> Self {
235 Self::V2(value)
236 }
237}
238
239#[derive(Clone)]
241#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
242pub struct V2EncryptedFileInfo {
243 pub k: Base64<UrlSafe, [u8; 32]>,
245
246 pub iv: Base64<Standard, [u8; 16]>,
248}
249
250impl V2EncryptedFileInfo {
251 pub fn new(k: Base64<UrlSafe, [u8; 32]>, iv: Base64<Standard, [u8; 16]>) -> Self {
253 Self { k, iv }
254 }
255
256 pub fn encode(k: [u8; 32], iv: [u8; 16]) -> Self {
259 Self::new(Base64::new(k), Base64::new(iv))
260 }
261}
262
263impl fmt::Debug for V2EncryptedFileInfo {
264 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265 f.debug_struct("V2EncryptedFileInfo").finish_non_exhaustive()
266 }
267}
268
269impl Drop for V2EncryptedFileInfo {
270 fn drop(&mut self) {
271 self.k.zeroize();
272 }
273}
274
275#[doc(hidden)]
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct CustomEncryptedFileInfo {
279 v: String,
281
282 #[serde(flatten)]
284 data: JsonObject,
285}
286
287#[derive(Clone, Debug, Default)]
292#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
293pub struct EncryptedFileHashes(BTreeMap<EncryptedFileHashAlgorithm, EncryptedFileHash>);
294
295impl EncryptedFileHashes {
296 pub fn new() -> Self {
298 Self::default()
299 }
300
301 pub fn with_sha256(hash: [u8; 32]) -> Self {
303 std::iter::once(EncryptedFileHash::Sha256(Base64::new(hash))).collect()
304 }
305
306 pub fn insert(&mut self, hash: EncryptedFileHash) -> Option<EncryptedFileHash> {
310 self.0.insert(hash.algorithm(), hash)
311 }
312}
313
314impl Deref for EncryptedFileHashes {
315 type Target = BTreeMap<EncryptedFileHashAlgorithm, EncryptedFileHash>;
316
317 fn deref(&self) -> &Self::Target {
318 &self.0
319 }
320}
321
322impl FromIterator<EncryptedFileHash> for EncryptedFileHashes {
323 fn from_iter<T: IntoIterator<Item = EncryptedFileHash>>(iter: T) -> Self {
324 Self(iter.into_iter().map(|hash| (hash.algorithm(), hash)).collect())
325 }
326}
327
328impl Extend<EncryptedFileHash> for EncryptedFileHashes {
329 fn extend<T: IntoIterator<Item = EncryptedFileHash>>(&mut self, iter: T) {
330 self.0.extend(iter.into_iter().map(|hash| (hash.algorithm(), hash)));
331 }
332}
333
334impl IntoIterator for EncryptedFileHashes {
335 type Item = EncryptedFileHash;
336 type IntoIter = btree_map::IntoValues<EncryptedFileHashAlgorithm, EncryptedFileHash>;
337
338 fn into_iter(self) -> Self::IntoIter {
339 self.0.into_values()
340 }
341}
342
343#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
345#[derive(Clone, StringEnum)]
346#[ruma_enum(rename_all = "lowercase")]
347#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
348pub enum EncryptedFileHashAlgorithm {
349 Sha256,
351
352 #[doc(hidden)]
353 _Custom(PrivOwnedStr),
354}
355
356#[derive(Clone, Debug)]
358#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
359pub enum EncryptedFileHash {
360 Sha256(Base64<Standard, [u8; 32]>),
362
363 #[doc(hidden)]
364 _Custom(CustomEncryptedFileHash),
365}
366
367impl EncryptedFileHash {
368 pub fn algorithm(&self) -> EncryptedFileHashAlgorithm {
370 match self {
371 Self::Sha256(_) => EncryptedFileHashAlgorithm::Sha256,
372 Self::_Custom(custom) => custom.algorithm.as_str().into(),
373 }
374 }
375
376 pub fn as_bytes(&self) -> &[u8] {
378 match self {
379 Self::Sha256(hash) => hash.as_bytes(),
380 Self::_Custom(custom) => custom.hash.as_bytes(),
381 }
382 }
383
384 pub fn into_bytes(self) -> Vec<u8> {
386 match self {
387 Self::Sha256(hash) => hash.into_inner().into(),
388 Self::_Custom(custom) => custom.hash.into_inner(),
389 }
390 }
391}
392
393#[doc(hidden)]
395#[derive(Clone, Debug)]
396pub struct CustomEncryptedFileHash {
397 algorithm: String,
399
400 hash: Base64,
402}
403
404#[cfg(test)]
405mod tests {
406 use assert_matches2::assert_matches;
407 use ruma_common::owned_mxc_uri;
408 use serde::Deserialize;
409 use serde_json::{from_value as from_json_value, json};
410
411 use super::{EncryptedFile, MediaSource, V2EncryptedFileInfo};
412 use crate::room::EncryptedFileHashes;
413
414 #[derive(Deserialize)]
415 struct MsgWithAttachment {
416 #[allow(dead_code)]
417 body: String,
418 #[serde(flatten)]
419 source: MediaSource,
420 }
421
422 #[test]
423 fn prefer_encrypted_attachment_over_plain() {
424 let msg: MsgWithAttachment = from_json_value(json!({
425 "body": "",
426 "file": EncryptedFile::new(
427 owned_mxc_uri!("mxc://localhost/encryptedfile"),
428 V2EncryptedFileInfo::encode([0;32], [1;16]).into(),
429 EncryptedFileHashes::new(),
430 ),
431 "url": "mxc://localhost/file",
432 }))
433 .unwrap();
434
435 assert_matches!(msg.source, MediaSource::Encrypted(_));
436 }
437}