use std::{
borrow::Cow,
collections::{BTreeMap, btree_map},
fmt,
ops::Deref,
};
use js_int::UInt;
use ruma_common::{
OwnedMxcUri,
serde::{
Base64, JsonObject,
base64::{Standard, UrlSafe},
},
};
use ruma_macros::StringEnum;
use serde::{Deserialize, Serialize, de};
use serde_json::Value as JsonValue;
use zeroize::Zeroize;
use crate::PrivOwnedStr;
pub mod avatar;
pub mod canonical_alias;
pub mod create;
pub mod encrypted;
mod encrypted_file_serde;
pub mod encryption;
pub mod guest_access;
pub mod history_visibility;
pub mod join_rules;
#[cfg(feature = "unstable-msc4334")]
pub mod language;
pub mod member;
pub mod message;
pub mod name;
pub mod pinned_events;
pub mod policy;
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>),
}
impl<'de> Deserialize<'de> for MediaSource {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
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)),
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(ruma_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", skip_serializing_if = "Option::is_none")]
pub blurhash: Option<String>,
#[cfg(feature = "unstable-msc2448")]
#[serde(rename = "xyz.amorgan.thumbhash", skip_serializing_if = "Option::is_none")]
pub thumbhash: Option<Base64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_animated: Option<bool>,
}
impl ImageInfo {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(ruma_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()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct EncryptedFile {
pub url: OwnedMxcUri,
#[serde(flatten)]
pub info: EncryptedFileInfo,
pub hashes: EncryptedFileHashes,
}
impl EncryptedFile {
pub fn new(url: OwnedMxcUri, info: EncryptedFileInfo, hashes: EncryptedFileHashes) -> Self {
Self { url, info, hashes }
}
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
#[serde(tag = "v", rename_all = "lowercase")]
pub enum EncryptedFileInfo {
V2(V2EncryptedFileInfo),
#[doc(hidden)]
#[serde(untagged)]
_Custom(CustomEncryptedFileInfo),
}
impl EncryptedFileInfo {
pub fn version(&self) -> &str {
match self {
Self::V2(_) => "v2",
Self::_Custom(info) => &info.v,
}
}
pub fn data(&self) -> Cow<'_, JsonObject> {
fn serialize<T: Serialize>(obj: &T) -> JsonObject {
match serde_json::to_value(obj).expect("encrypted file info serialization to succeed") {
JsonValue::Object(mut obj) => {
obj.remove("body");
obj
}
_ => panic!("all encrypted file info variants must serialize to objects"),
}
}
match self {
Self::V2(i) => Cow::Owned(serialize(i)),
Self::_Custom(i) => Cow::Borrowed(&i.data),
}
}
}
impl From<V2EncryptedFileInfo> for EncryptedFileInfo {
fn from(value: V2EncryptedFileInfo) -> Self {
Self::V2(value)
}
}
#[derive(Clone)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct V2EncryptedFileInfo {
pub k: Base64<UrlSafe, [u8; 32]>,
pub iv: Base64<Standard, [u8; 16]>,
}
impl V2EncryptedFileInfo {
pub fn new(k: Base64<UrlSafe, [u8; 32]>, iv: Base64<Standard, [u8; 16]>) -> Self {
Self { k, iv }
}
pub fn encode(k: [u8; 32], iv: [u8; 16]) -> Self {
Self::new(Base64::new(k), Base64::new(iv))
}
}
impl fmt::Debug for V2EncryptedFileInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("V2EncryptedFileInfo").finish_non_exhaustive()
}
}
impl Drop for V2EncryptedFileInfo {
fn drop(&mut self) {
self.k.zeroize();
}
}
#[doc(hidden)]
#[derive(Debug, Clone, Serialize)]
pub struct CustomEncryptedFileInfo {
v: String,
#[serde(flatten)]
data: JsonObject,
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct EncryptedFileHashes(BTreeMap<EncryptedFileHashAlgorithm, EncryptedFileHash>);
impl EncryptedFileHashes {
pub fn new() -> Self {
Self::default()
}
pub fn with_sha256(hash: [u8; 32]) -> Self {
std::iter::once(EncryptedFileHash::Sha256(Base64::new(hash))).collect()
}
pub fn insert(&mut self, hash: EncryptedFileHash) -> Option<EncryptedFileHash> {
self.0.insert(hash.algorithm(), hash)
}
}
impl Deref for EncryptedFileHashes {
type Target = BTreeMap<EncryptedFileHashAlgorithm, EncryptedFileHash>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl FromIterator<EncryptedFileHash> for EncryptedFileHashes {
fn from_iter<T: IntoIterator<Item = EncryptedFileHash>>(iter: T) -> Self {
Self(iter.into_iter().map(|hash| (hash.algorithm(), hash)).collect())
}
}
impl Extend<EncryptedFileHash> for EncryptedFileHashes {
fn extend<T: IntoIterator<Item = EncryptedFileHash>>(&mut self, iter: T) {
self.0.extend(iter.into_iter().map(|hash| (hash.algorithm(), hash)));
}
}
impl IntoIterator for EncryptedFileHashes {
type Item = EncryptedFileHash;
type IntoIter = btree_map::IntoValues<EncryptedFileHashAlgorithm, EncryptedFileHash>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_values()
}
}
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
#[derive(Clone, StringEnum)]
#[ruma_enum(rename_all = "lowercase")]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub enum EncryptedFileHashAlgorithm {
Sha256,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
#[derive(Clone, Debug)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub enum EncryptedFileHash {
Sha256(Base64<Standard, [u8; 32]>),
#[doc(hidden)]
_Custom(CustomEncryptedFileHash),
}
impl EncryptedFileHash {
pub fn algorithm(&self) -> EncryptedFileHashAlgorithm {
match self {
Self::Sha256(_) => EncryptedFileHashAlgorithm::Sha256,
Self::_Custom(custom) => custom.algorithm.as_str().into(),
}
}
pub fn as_bytes(&self) -> &[u8] {
match self {
Self::Sha256(hash) => hash.as_bytes(),
Self::_Custom(custom) => custom.hash.as_bytes(),
}
}
pub fn into_bytes(self) -> Vec<u8> {
match self {
Self::Sha256(hash) => hash.into_inner().into(),
Self::_Custom(custom) => custom.hash.into_inner(),
}
}
}
#[doc(hidden)]
#[derive(Clone, Debug)]
pub struct CustomEncryptedFileHash {
algorithm: String,
hash: Base64,
}
#[cfg(test)]
mod tests {
use assert_matches2::assert_matches;
use ruma_common::owned_mxc_uri;
use serde::Deserialize;
use serde_json::{from_value as from_json_value, json};
use super::{EncryptedFile, MediaSource, V2EncryptedFileInfo};
use crate::room::EncryptedFileHashes;
#[derive(Deserialize)]
struct MsgWithAttachment {
#[allow(dead_code)]
body: String,
#[serde(flatten)]
source: MediaSource,
}
#[test]
fn prefer_encrypted_attachment_over_plain() {
let msg: MsgWithAttachment = from_json_value(json!({
"body": "",
"file": EncryptedFile::new(
owned_mxc_uri!("mxc://localhost/encryptedfile"),
V2EncryptedFileInfo::encode([0;32], [1;16]).into(),
EncryptedFileHashes::new(),
),
"url": "mxc://localhost/file",
}))
.unwrap();
assert_matches!(msg.source, MediaSource::Encrypted(_));
}
}