1use std::collections::BTreeMap;
6
7use js_int::UInt;
8use ruma_common::{
9 serde::{base64::UrlSafe, Base64},
10 OwnedMxcUri,
11};
12use serde::{de, Deserialize, Serialize};
13use zeroize::Zeroize;
14
15pub mod aliases;
16pub mod avatar;
17pub mod canonical_alias;
18pub mod create;
19pub mod encrypted;
20pub mod encryption;
21pub mod guest_access;
22pub mod history_visibility;
23pub mod join_rules;
24#[cfg(feature = "unstable-msc4334")]
25pub mod language;
26pub mod member;
27pub mod message;
28pub mod name;
29pub mod pinned_events;
30pub mod power_levels;
31pub mod redaction;
32pub mod server_acl;
33pub mod third_party_invite;
34mod thumbnail_source_serde;
35pub mod tombstone;
36pub mod topic;
37
38#[derive(Clone, Debug, Serialize)]
40#[allow(clippy::exhaustive_enums)]
41pub enum MediaSource {
42 #[serde(rename = "url")]
44 Plain(OwnedMxcUri),
45
46 #[serde(rename = "file")]
48 Encrypted(Box<EncryptedFile>),
49}
50
51impl<'de> Deserialize<'de> for MediaSource {
56 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
57 where
58 D: serde::Deserializer<'de>,
59 {
60 #[derive(Deserialize)]
61 struct MediaSourceJsonRepr {
62 url: Option<OwnedMxcUri>,
63 file: Option<Box<EncryptedFile>>,
64 }
65
66 match MediaSourceJsonRepr::deserialize(deserializer)? {
67 MediaSourceJsonRepr { url: None, file: None } => Err(de::Error::missing_field("url")),
68 MediaSourceJsonRepr { file: Some(file), .. } => Ok(MediaSource::Encrypted(file)),
70 MediaSourceJsonRepr { url: Some(url), .. } => Ok(MediaSource::Plain(url)),
71 }
72 }
73}
74
75#[derive(Clone, Debug, Default, Deserialize, Serialize)]
77#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
78pub struct ImageInfo {
79 #[serde(rename = "h", skip_serializing_if = "Option::is_none")]
81 pub height: Option<UInt>,
82
83 #[serde(rename = "w", skip_serializing_if = "Option::is_none")]
85 pub width: Option<UInt>,
86
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub mimetype: Option<String>,
90
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub size: Option<UInt>,
94
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub thumbnail_info: Option<Box<ThumbnailInfo>>,
98
99 #[serde(flatten, with = "thumbnail_source_serde", skip_serializing_if = "Option::is_none")]
101 pub thumbnail_source: Option<MediaSource>,
102
103 #[cfg(feature = "unstable-msc2448")]
108 #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")]
109 pub blurhash: Option<String>,
110
111 #[cfg(feature = "unstable-msc2448")]
116 #[serde(rename = "xyz.amorgan.thumbhash", skip_serializing_if = "Option::is_none")]
117 pub thumbhash: Option<Base64>,
118
119 #[cfg(feature = "unstable-msc4230")]
125 #[serde(rename = "org.matrix.msc4230.is_animated", skip_serializing_if = "Option::is_none")]
126 pub is_animated: Option<bool>,
127}
128
129impl ImageInfo {
130 pub fn new() -> Self {
132 Self::default()
133 }
134}
135
136#[derive(Clone, Debug, Default, Deserialize, Serialize)]
138#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
139pub struct ThumbnailInfo {
140 #[serde(rename = "h", skip_serializing_if = "Option::is_none")]
142 pub height: Option<UInt>,
143
144 #[serde(rename = "w", skip_serializing_if = "Option::is_none")]
146 pub width: Option<UInt>,
147
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub mimetype: Option<String>,
151
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub size: Option<UInt>,
155}
156
157impl ThumbnailInfo {
158 pub fn new() -> Self {
160 Self::default()
161 }
162}
163
164#[derive(Clone, Debug, Deserialize, Serialize)]
169#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
170pub struct EncryptedFile {
171 pub url: OwnedMxcUri,
173
174 pub key: JsonWebKey,
176
177 pub iv: Base64,
179
180 pub hashes: BTreeMap<String, Base64>,
184
185 pub v: String,
189}
190
191#[derive(Debug)]
196#[allow(clippy::exhaustive_structs)]
197pub struct EncryptedFileInit {
198 pub url: OwnedMxcUri,
200
201 pub key: JsonWebKey,
203
204 pub iv: Base64,
206
207 pub hashes: BTreeMap<String, Base64>,
211
212 pub v: String,
216}
217
218impl From<EncryptedFileInit> for EncryptedFile {
219 fn from(init: EncryptedFileInit) -> Self {
220 let EncryptedFileInit { url, key, iv, hashes, v } = init;
221 Self { url, key, iv, hashes, v }
222 }
223}
224
225#[derive(Clone, Deserialize, Serialize)]
230#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
231pub struct JsonWebKey {
232 pub kty: String,
236
237 pub key_ops: Vec<String>,
241
242 pub alg: String,
246
247 pub k: Base64<UrlSafe>,
249
250 pub ext: bool,
255}
256
257impl std::fmt::Debug for JsonWebKey {
258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259 f.debug_struct("JsonWebKey")
260 .field("kty", &self.kty)
261 .field("key_ops", &self.key_ops)
262 .field("alg", &self.alg)
263 .field("ext", &self.ext)
264 .finish_non_exhaustive()
265 }
266}
267
268impl Drop for JsonWebKey {
269 fn drop(&mut self) {
270 self.k.zeroize();
271 }
272}
273
274#[allow(clippy::exhaustive_structs)]
279pub struct JsonWebKeyInit {
280 pub kty: String,
284
285 pub key_ops: Vec<String>,
289
290 pub alg: String,
294
295 pub k: Base64<UrlSafe>,
297
298 pub ext: bool,
303}
304
305impl std::fmt::Debug for JsonWebKeyInit {
306 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307 f.debug_struct("JsonWebKeyInit")
308 .field("kty", &self.kty)
309 .field("key_ops", &self.key_ops)
310 .field("alg", &self.alg)
311 .field("ext", &self.ext)
312 .finish_non_exhaustive()
313 }
314}
315
316impl From<JsonWebKeyInit> for JsonWebKey {
317 fn from(init: JsonWebKeyInit) -> Self {
318 let JsonWebKeyInit { kty, key_ops, alg, k, ext } = init;
319 Self { kty, key_ops, alg, k, ext }
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use std::collections::BTreeMap;
326
327 use assert_matches2::assert_matches;
328 use ruma_common::{mxc_uri, serde::Base64};
329 use serde::Deserialize;
330 use serde_json::{from_value as from_json_value, json};
331
332 use super::{EncryptedFile, JsonWebKey, MediaSource};
333
334 #[derive(Deserialize)]
335 struct MsgWithAttachment {
336 #[allow(dead_code)]
337 body: String,
338 #[serde(flatten)]
339 source: MediaSource,
340 }
341
342 fn dummy_jwt() -> JsonWebKey {
343 JsonWebKey {
344 kty: "oct".to_owned(),
345 key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()],
346 alg: "A256CTR".to_owned(),
347 k: Base64::new(vec![0; 64]),
348 ext: true,
349 }
350 }
351
352 fn encrypted_file() -> EncryptedFile {
353 EncryptedFile {
354 url: mxc_uri!("mxc://localhost/encryptedfile").to_owned(),
355 key: dummy_jwt(),
356 iv: Base64::new(vec![0; 64]),
357 hashes: BTreeMap::new(),
358 v: "v2".to_owned(),
359 }
360 }
361
362 #[test]
363 fn prefer_encrypted_attachment_over_plain() {
364 let msg: MsgWithAttachment = from_json_value(json!({
365 "body": "",
366 "url": "mxc://localhost/file",
367 "file": encrypted_file(),
368 }))
369 .unwrap();
370
371 assert_matches!(msg.source, MediaSource::Encrypted(_));
372
373 let msg: MsgWithAttachment = from_json_value(json!({
375 "body": "",
376 "file": encrypted_file(),
377 "url": "mxc://localhost/file",
378 }))
379 .unwrap();
380
381 assert_matches!(msg.source, MediaSource::Encrypted(_));
382 }
383}