#![allow(clippy::needless_borrow)]
use std::collections::BTreeMap;
use js_int::UInt;
use serde::{de, Deserialize, Serialize};
#[cfg(feature = "unstable-msc3551")]
use super::file::{EncryptedContent, EncryptedContentInit, FileContent};
#[cfg(feature = "unstable-msc3552")]
use super::{
file::FileContentInfo,
image::{ImageContent, ThumbnailContent, ThumbnailFileContent, ThumbnailFileContentInfo},
};
#[cfg(feature = "unstable-msc3551")]
use crate::MxcUri;
use crate::{
serde::{base64::UrlSafe, Base64},
OwnedMxcUri,
};
pub mod aliases;
pub mod avatar;
pub mod canonical_alias;
pub mod create;
pub mod encrypted;
pub mod encryption;
pub mod guest_access;
pub mod history_visibility;
pub mod join_rules;
pub mod member;
pub mod message;
pub mod name;
pub mod pinned_events;
pub mod power_levels;
pub mod redaction;
pub mod server_acl;
pub mod third_party_invite;
mod thumbnail_source_serde;
pub mod tombstone;
pub mod topic;
#[derive(Clone, Debug, Serialize)]
#[allow(clippy::exhaustive_enums)]
pub enum MediaSource {
#[serde(rename = "url")]
Plain(OwnedMxcUri),
#[serde(rename = "file")]
Encrypted(Box<EncryptedFile>),
}
#[cfg(feature = "unstable-msc3551")]
impl MediaSource {
pub(crate) fn into_extensible_content(self) -> (OwnedMxcUri, Option<EncryptedContent>) {
match self {
MediaSource::Plain(url) => (url, None),
MediaSource::Encrypted(encrypted_file) => {
let EncryptedFile { url, key, iv, hashes, v } = *encrypted_file;
(url, Some(EncryptedContentInit { key, iv, hashes, v }.into()))
}
}
}
}
impl<'de> Deserialize<'de> for MediaSource {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
pub struct MediaSourceJsonRepr {
url: Option<OwnedMxcUri>,
file: Option<Box<EncryptedFile>>,
}
match MediaSourceJsonRepr::deserialize(deserializer)? {
MediaSourceJsonRepr { url: None, file: None } => Err(de::Error::missing_field("url")),
MediaSourceJsonRepr { file: Some(file), .. } => Ok(MediaSource::Encrypted(file)),
MediaSourceJsonRepr { url: Some(url), .. } => Ok(MediaSource::Plain(url)),
}
}
}
#[cfg(feature = "unstable-msc3551")]
impl From<&FileContent> for MediaSource {
fn from(content: &FileContent) -> Self {
let FileContent { url, encryption_info, .. } = content;
if let Some(encryption_info) = encryption_info.as_deref() {
Self::Encrypted(Box::new(EncryptedFile::from_extensible_content(url, encryption_info)))
} else {
Self::Plain(url.to_owned())
}
}
}
#[cfg(feature = "unstable-msc3552")]
impl From<&ThumbnailFileContent> for MediaSource {
fn from(content: &ThumbnailFileContent) -> Self {
let ThumbnailFileContent { url, encryption_info, .. } = content;
if let Some(encryption_info) = encryption_info.as_deref() {
Self::Encrypted(Box::new(EncryptedFile::from_extensible_content(url, encryption_info)))
} else {
Self::Plain(url.to_owned())
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct ImageInfo {
#[serde(rename = "h", skip_serializing_if = "Option::is_none")]
pub height: Option<UInt>,
#[serde(rename = "w", skip_serializing_if = "Option::is_none")]
pub width: Option<UInt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mimetype: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_info: Option<Box<ThumbnailInfo>>,
#[serde(flatten, with = "thumbnail_source_serde", skip_serializing_if = "Option::is_none")]
pub thumbnail_source: Option<MediaSource>,
#[cfg(feature = "unstable-msc2448")]
#[serde(
rename = "xyz.amorgan.blurhash",
alias = "blurhash",
skip_serializing_if = "Option::is_none"
)]
pub blurhash: Option<String>,
}
impl ImageInfo {
pub fn new() -> Self {
Self::default()
}
#[cfg(feature = "unstable-msc3552")]
fn from_extensible_content(
file_info: Option<&FileContentInfo>,
image: &ImageContent,
thumbnail: &[ThumbnailContent],
) -> Option<Self> {
if file_info.is_none() && image.is_empty() && thumbnail.is_empty() {
None
} else {
let (mimetype, size) = file_info
.map(|info| (info.mimetype.to_owned(), info.size.to_owned()))
.unwrap_or_default();
let ImageContent { height, width } = image.to_owned();
let (thumbnail_source, thumbnail_info) = thumbnail
.get(0)
.map(|thumbnail| {
let source = (&thumbnail.file).into();
let info = ThumbnailInfo::from_extensible_content(
thumbnail.file.info.as_deref(),
thumbnail.image.as_deref(),
)
.map(Box::new);
(Some(source), info)
})
.unwrap_or_default();
Some(Self {
height,
width,
mimetype,
size,
thumbnail_source,
thumbnail_info,
#[cfg(feature = "unstable-msc2448")]
blurhash: None,
})
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct ThumbnailInfo {
#[serde(rename = "h", skip_serializing_if = "Option::is_none")]
pub height: Option<UInt>,
#[serde(rename = "w", skip_serializing_if = "Option::is_none")]
pub width: Option<UInt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mimetype: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
}
impl ThumbnailInfo {
pub fn new() -> Self {
Self::default()
}
#[cfg(feature = "unstable-msc3552")]
fn from_extensible_content(
file_info: Option<&ThumbnailFileContentInfo>,
image: Option<&ImageContent>,
) -> Option<Self> {
if file_info.is_none() && image.is_none() {
None
} else {
let ThumbnailFileContentInfo { mimetype, size } =
file_info.map(ToOwned::to_owned).unwrap_or_default();
let ImageContent { height, width } = image.map(ToOwned::to_owned).unwrap_or_default();
Some(Self { height, width, mimetype, size })
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct EncryptedFile {
pub url: OwnedMxcUri,
pub key: JsonWebKey,
pub iv: Base64,
pub hashes: BTreeMap<String, Base64>,
pub v: String,
}
#[cfg(feature = "unstable-msc3551")]
impl EncryptedFile {
fn from_extensible_content(url: &MxcUri, encryption_info: &EncryptedContent) -> Self {
let EncryptedContent { key, iv, hashes, v } = encryption_info.to_owned();
Self { url: url.to_owned(), key, iv, hashes, v }
}
}
#[derive(Debug)]
#[allow(clippy::exhaustive_structs)]
pub struct EncryptedFileInit {
pub url: OwnedMxcUri,
pub key: JsonWebKey,
pub iv: Base64,
pub hashes: BTreeMap<String, Base64>,
pub v: String,
}
impl From<EncryptedFileInit> for EncryptedFile {
fn from(init: EncryptedFileInit) -> Self {
let EncryptedFileInit { url, key, iv, hashes, v } = init;
Self { url, key, iv, hashes, v }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct JsonWebKey {
pub kty: String,
pub key_ops: Vec<String>,
pub alg: String,
pub k: Base64<UrlSafe>,
pub ext: bool,
}
#[derive(Debug)]
#[allow(clippy::exhaustive_structs)]
pub struct JsonWebKeyInit {
pub kty: String,
pub key_ops: Vec<String>,
pub alg: String,
pub k: Base64<UrlSafe>,
pub ext: bool,
}
impl From<JsonWebKeyInit> for JsonWebKey {
fn from(init: JsonWebKeyInit) -> Self {
let JsonWebKeyInit { kty, key_ops, alg, k, ext } = init;
Self { kty, key_ops, alg, k, ext }
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use assert_matches::assert_matches;
use serde::Deserialize;
use serde_json::{from_value as from_json_value, json};
use crate::{mxc_uri, serde::Base64};
use super::{EncryptedFile, JsonWebKey, MediaSource};
#[derive(Deserialize)]
struct MsgWithAttachment {
#[allow(dead_code)]
body: String,
#[serde(flatten)]
source: MediaSource,
}
fn dummy_jwt() -> JsonWebKey {
JsonWebKey {
kty: "oct".to_owned(),
key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()],
alg: "A256CTR".to_owned(),
k: Base64::new(vec![0; 64]),
ext: true,
}
}
fn encrypted_file() -> EncryptedFile {
EncryptedFile {
url: mxc_uri!("mxc://localhost/encryptedfile").to_owned(),
key: dummy_jwt(),
iv: Base64::new(vec![0; 64]),
hashes: BTreeMap::new(),
v: "v2".to_owned(),
}
}
#[test]
fn prefer_encrypted_attachment_over_plain() {
let msg: MsgWithAttachment = from_json_value(json!({
"body": "",
"url": "mxc://localhost/file",
"file": encrypted_file(),
}))
.unwrap();
assert_matches!(msg.source, MediaSource::Encrypted(_));
let msg: MsgWithAttachment = from_json_value(json!({
"body": "",
"file": encrypted_file(),
"url": "mxc://localhost/file",
}))
.unwrap();
assert_matches!(msg.source, MediaSource::Encrypted(_));
}
}