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;
24pub mod member;
25pub mod message;
26pub mod name;
27pub mod pinned_events;
28pub mod power_levels;
29pub mod redaction;
30pub mod server_acl;
31pub mod third_party_invite;
32mod thumbnail_source_serde;
33pub mod tombstone;
34pub mod topic;
35
36#[derive(Clone, Debug, Serialize)]
38#[allow(clippy::exhaustive_enums)]
39pub enum MediaSource {
40 #[serde(rename = "url")]
42 Plain(OwnedMxcUri),
43
44 #[serde(rename = "file")]
46 Encrypted(Box<EncryptedFile>),
47}
48
49impl<'de> Deserialize<'de> for MediaSource {
54 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
55 where
56 D: serde::Deserializer<'de>,
57 {
58 #[derive(Deserialize)]
59 struct MediaSourceJsonRepr {
60 url: Option<OwnedMxcUri>,
61 file: Option<Box<EncryptedFile>>,
62 }
63
64 match MediaSourceJsonRepr::deserialize(deserializer)? {
65 MediaSourceJsonRepr { url: None, file: None } => Err(de::Error::missing_field("url")),
66 MediaSourceJsonRepr { file: Some(file), .. } => Ok(MediaSource::Encrypted(file)),
68 MediaSourceJsonRepr { url: Some(url), .. } => Ok(MediaSource::Plain(url)),
69 }
70 }
71}
72
73#[derive(Clone, Debug, Default, Deserialize, Serialize)]
75#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
76pub struct ImageInfo {
77 #[serde(rename = "h", skip_serializing_if = "Option::is_none")]
79 pub height: Option<UInt>,
80
81 #[serde(rename = "w", skip_serializing_if = "Option::is_none")]
83 pub width: Option<UInt>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub mimetype: Option<String>,
88
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub size: Option<UInt>,
92
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub thumbnail_info: Option<Box<ThumbnailInfo>>,
96
97 #[serde(flatten, with = "thumbnail_source_serde", skip_serializing_if = "Option::is_none")]
99 pub thumbnail_source: Option<MediaSource>,
100
101 #[cfg(feature = "unstable-msc2448")]
106 #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")]
107 pub blurhash: Option<String>,
108
109 #[cfg(feature = "unstable-msc2448")]
114 #[serde(rename = "xyz.amorgan.thumbhash", skip_serializing_if = "Option::is_none")]
115 pub thumbhash: Option<Base64>,
116
117 #[cfg(feature = "unstable-msc4230")]
123 #[serde(rename = "org.matrix.msc4230.is_animated", skip_serializing_if = "Option::is_none")]
124 pub is_animated: Option<bool>,
125}
126
127impl ImageInfo {
128 pub fn new() -> Self {
130 Self::default()
131 }
132}
133
134#[derive(Clone, Debug, Default, Deserialize, Serialize)]
136#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
137pub struct ThumbnailInfo {
138 #[serde(rename = "h", skip_serializing_if = "Option::is_none")]
140 pub height: Option<UInt>,
141
142 #[serde(rename = "w", skip_serializing_if = "Option::is_none")]
144 pub width: Option<UInt>,
145
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub mimetype: Option<String>,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub size: Option<UInt>,
153}
154
155impl ThumbnailInfo {
156 pub fn new() -> Self {
158 Self::default()
159 }
160}
161
162#[derive(Clone, Debug, Deserialize, Serialize)]
167#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
168pub struct EncryptedFile {
169 pub url: OwnedMxcUri,
171
172 pub key: JsonWebKey,
174
175 pub iv: Base64,
177
178 pub hashes: BTreeMap<String, Base64>,
182
183 pub v: String,
187}
188
189#[derive(Debug)]
194#[allow(clippy::exhaustive_structs)]
195pub struct EncryptedFileInit {
196 pub url: OwnedMxcUri,
198
199 pub key: JsonWebKey,
201
202 pub iv: Base64,
204
205 pub hashes: BTreeMap<String, Base64>,
209
210 pub v: String,
214}
215
216impl From<EncryptedFileInit> for EncryptedFile {
217 fn from(init: EncryptedFileInit) -> Self {
218 let EncryptedFileInit { url, key, iv, hashes, v } = init;
219 Self { url, key, iv, hashes, v }
220 }
221}
222
223#[derive(Clone, Deserialize, Serialize)]
228#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
229pub struct JsonWebKey {
230 pub kty: String,
234
235 pub key_ops: Vec<String>,
239
240 pub alg: String,
244
245 pub k: Base64<UrlSafe>,
247
248 pub ext: bool,
253}
254
255impl std::fmt::Debug for JsonWebKey {
256 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257 f.debug_struct("JsonWebKey")
258 .field("kty", &self.kty)
259 .field("key_ops", &self.key_ops)
260 .field("alg", &self.alg)
261 .field("ext", &self.ext)
262 .finish_non_exhaustive()
263 }
264}
265
266impl Drop for JsonWebKey {
267 fn drop(&mut self) {
268 self.k.zeroize();
269 }
270}
271
272#[allow(clippy::exhaustive_structs)]
277pub struct JsonWebKeyInit {
278 pub kty: String,
282
283 pub key_ops: Vec<String>,
287
288 pub alg: String,
292
293 pub k: Base64<UrlSafe>,
295
296 pub ext: bool,
301}
302
303impl std::fmt::Debug for JsonWebKeyInit {
304 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305 f.debug_struct("JsonWebKeyInit")
306 .field("kty", &self.kty)
307 .field("key_ops", &self.key_ops)
308 .field("alg", &self.alg)
309 .field("ext", &self.ext)
310 .finish_non_exhaustive()
311 }
312}
313
314impl From<JsonWebKeyInit> for JsonWebKey {
315 fn from(init: JsonWebKeyInit) -> Self {
316 let JsonWebKeyInit { kty, key_ops, alg, k, ext } = init;
317 Self { kty, key_ops, alg, k, ext }
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use std::collections::BTreeMap;
324
325 use assert_matches2::assert_matches;
326 use ruma_common::{mxc_uri, serde::Base64};
327 use serde::Deserialize;
328 use serde_json::{from_value as from_json_value, json};
329
330 use super::{EncryptedFile, JsonWebKey, MediaSource};
331
332 #[derive(Deserialize)]
333 struct MsgWithAttachment {
334 #[allow(dead_code)]
335 body: String,
336 #[serde(flatten)]
337 source: MediaSource,
338 }
339
340 fn dummy_jwt() -> JsonWebKey {
341 JsonWebKey {
342 kty: "oct".to_owned(),
343 key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()],
344 alg: "A256CTR".to_owned(),
345 k: Base64::new(vec![0; 64]),
346 ext: true,
347 }
348 }
349
350 fn encrypted_file() -> EncryptedFile {
351 EncryptedFile {
352 url: mxc_uri!("mxc://localhost/encryptedfile").to_owned(),
353 key: dummy_jwt(),
354 iv: Base64::new(vec![0; 64]),
355 hashes: BTreeMap::new(),
356 v: "v2".to_owned(),
357 }
358 }
359
360 #[test]
361 fn prefer_encrypted_attachment_over_plain() {
362 let msg: MsgWithAttachment = from_json_value(json!({
363 "body": "",
364 "url": "mxc://localhost/file",
365 "file": encrypted_file(),
366 }))
367 .unwrap();
368
369 assert_matches!(msg.source, MediaSource::Encrypted(_));
370
371 let msg: MsgWithAttachment = from_json_value(json!({
373 "body": "",
374 "file": encrypted_file(),
375 "url": "mxc://localhost/file",
376 }))
377 .unwrap();
378
379 assert_matches!(msg.source, MediaSource::Encrypted(_));
380 }
381}