events: Generate PossiblyRedacted type for original state events

Fix deserialization of redacted prev_content
Can be overriden with the `custom_possibly_redacted` attribute

Co-authored-by: Jonas Platte <jplatte@element.io>
This commit is contained in:
Kévin Commaille 2023-01-03 10:28:40 +01:00 committed by GitHub
parent 0578e7af50
commit cd74cdcc0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 352 additions and 24 deletions

View File

@ -8,6 +8,7 @@ Bug fixes:
and `events::secret` modules
* Fix deserialization of `RoomMessageEventContent` and `RoomEncryptedEventContent` when there
is no relation
* Fix deserialization of `StateUnsigned` when the `prev_content` is redacted
Breaking changes:

View File

@ -72,6 +72,7 @@ impl StateEventContent for CustomStateEventContent {
}
impl OriginalStateEventContent for CustomStateEventContent {
type Unsigned = StateUnsigned<Self>;
type PossiblyRedacted = Self;
}
impl RedactedStateEventContent for CustomStateEventContent {}

View File

@ -130,6 +130,9 @@ pub trait StateEventContent: EventContent<EventType = StateEventType> {
pub trait OriginalStateEventContent: StateEventContent + RedactContent {
/// The type of the event's `unsigned` field.
type Unsigned: Clone + fmt::Debug + Default + CanBeEmpty + StateUnsignedFromParts;
/// The possibly redacted form of the event's content.
type PossiblyRedacted: StateEventContent;
}
/// Content of a redacted state event.

View File

@ -471,7 +471,7 @@ pub struct DecryptedMegolmV1Event<C: MessageLikeEventContent> {
/// A non-redacted content also contains the `prev_content` from the unsigned event data.
#[allow(clippy::exhaustive_enums)]
#[derive(Clone, Debug)]
pub enum FullStateEventContent<C: StateEventContent + RedactContent>
pub enum FullStateEventContent<C: OriginalStateEventContent>
where
C::Redacted: RedactedStateEventContent,
{
@ -481,14 +481,14 @@ where
content: C,
/// Previous content of the room state.
prev_content: Option<C>,
prev_content: Option<C::PossiblyRedacted>,
},
/// Redacted content of the event.
Redacted(C::Redacted),
}
impl<C: StateEventContent + RedactContent> FullStateEventContent<C>
impl<C: OriginalStateEventContent> FullStateEventContent<C>
where
C::Redacted: RedactedStateEventContent,
{

View File

@ -32,6 +32,28 @@ impl PolicyRuleEventContent {
}
}
/// The possibly redacted form of [`PolicyRuleEventContent`].
///
/// This type is used when it's not obvious whether the content is redacted or not.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct PossiblyRedactedPolicyRuleEventContent {
/// The entity affected by this rule.
///
/// Glob characters `*` and `?` can be used to match zero or more characters or exactly one
/// character respectively.
#[serde(skip_serializing_if = "Option::is_none")]
pub entity: Option<String>,
/// The suggested action to take.
#[serde(skip_serializing_if = "Option::is_none")]
pub recommendation: Option<Recommendation>,
/// The human-readable description for the recommendation.
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
/// The possible actions that can be taken.
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
#[derive(Clone, Debug, PartialEq, Eq, StringEnum)]

View File

@ -4,17 +4,48 @@
use ruma_macros::EventContent;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue as RawJsonValue;
use super::PolicyRuleEventContent;
use super::{PolicyRuleEventContent, PossiblyRedactedPolicyRuleEventContent};
use crate::events::{EventContent, StateEventContent, StateEventType};
/// The content of an `m.policy.rule.room` event.
///
/// This event type is used to apply rules to room entities.
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
#[allow(clippy::exhaustive_structs)]
#[ruma_event(type = "m.policy.rule.room", kind = State, state_key_type = String)]
#[ruma_event(type = "m.policy.rule.room", kind = State, state_key_type = String, custom_possibly_redacted)]
pub struct PolicyRuleRoomEventContent(pub PolicyRuleEventContent);
/// The possibly redacted form of [`PolicyRuleRoomEventContent`].
///
/// This type is used when it's not obvious whether the content is redacted or not.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[allow(clippy::exhaustive_structs)]
pub struct PossiblyRedactedPolicyRuleRoomEventContent(pub PossiblyRedactedPolicyRuleEventContent);
impl EventContent for PossiblyRedactedPolicyRuleRoomEventContent {
type EventType = StateEventType;
fn event_type(&self) -> Self::EventType {
StateEventType::PolicyRuleRoom
}
fn from_parts(event_type: &str, content: &RawJsonValue) -> serde_json::Result<Self> {
if event_type != "m.policy.rule.room" {
return Err(::serde::de::Error::custom(format!(
"expected event type `m.policy.rule.room`, found `{event_type}`",
)));
}
serde_json::from_str(content.get())
}
}
impl StateEventContent for PossiblyRedactedPolicyRuleRoomEventContent {
type StateKey = String;
}
#[cfg(test)]
mod tests {
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};

View File

@ -4,13 +4,44 @@
use ruma_macros::EventContent;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue as RawJsonValue;
use super::PolicyRuleEventContent;
use super::{PolicyRuleEventContent, PossiblyRedactedPolicyRuleEventContent};
use crate::events::{EventContent, StateEventContent, StateEventType};
/// The content of an `m.policy.rule.server` event.
///
/// This event type is used to apply rules to server entities.
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
#[allow(clippy::exhaustive_structs)]
#[ruma_event(type = "m.policy.rule.server", kind = State, state_key_type = String)]
#[ruma_event(type = "m.policy.rule.server", kind = State, state_key_type = String, custom_possibly_redacted)]
pub struct PolicyRuleServerEventContent(pub PolicyRuleEventContent);
/// The possibly redacted form of [`PolicyRuleServerEventContent`].
///
/// This type is used when it's not obvious whether the content is redacted or not.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[allow(clippy::exhaustive_structs)]
pub struct PossiblyRedactedPolicyRuleServerEventContent(pub PossiblyRedactedPolicyRuleEventContent);
impl EventContent for PossiblyRedactedPolicyRuleServerEventContent {
type EventType = StateEventType;
fn event_type(&self) -> Self::EventType {
StateEventType::PolicyRuleServer
}
fn from_parts(event_type: &str, content: &RawJsonValue) -> serde_json::Result<Self> {
if event_type != "m.policy.rule.server" {
return Err(::serde::de::Error::custom(format!(
"expected event type `m.policy.rule.server`, found `{event_type}`",
)));
}
serde_json::from_str(content.get())
}
}
impl StateEventContent for PossiblyRedactedPolicyRuleServerEventContent {
type StateKey = String;
}

View File

@ -4,13 +4,44 @@
use ruma_macros::EventContent;
use serde::{Deserialize, Serialize};
use serde_json::value::RawValue as RawJsonValue;
use super::PolicyRuleEventContent;
use super::{PolicyRuleEventContent, PossiblyRedactedPolicyRuleEventContent};
use crate::events::{EventContent, StateEventContent, StateEventType};
/// The content of an `m.policy.rule.user` event.
///
/// This event type is used to apply rules to user entities.
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
#[allow(clippy::exhaustive_structs)]
#[ruma_event(type = "m.policy.rule.user", kind = State, state_key_type = String)]
#[ruma_event(type = "m.policy.rule.user", kind = State, state_key_type = String, custom_possibly_redacted)]
pub struct PolicyRuleUserEventContent(pub PolicyRuleEventContent);
/// The possibly redacted form of [`PolicyRuleUserEventContent`].
///
/// This type is used when it's not obvious whether the content is redacted or not.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[allow(clippy::exhaustive_structs)]
pub struct PossiblyRedactedPolicyRuleUserEventContent(pub PossiblyRedactedPolicyRuleEventContent);
impl EventContent for PossiblyRedactedPolicyRuleUserEventContent {
type EventType = StateEventType;
fn event_type(&self) -> Self::EventType {
StateEventType::PolicyRuleUser
}
fn from_parts(event_type: &str, content: &RawJsonValue) -> serde_json::Result<Self> {
if event_type != "m.policy.rule.user" {
return Err(::serde::de::Error::custom(format!(
"expected event type `m.policy.rule.user`, found `{event_type}`",
)));
}
serde_json::from_str(content.get())
}
}
impl StateEventContent for PossiblyRedactedPolicyRuleUserEventContent {
type StateKey = String;
}

View File

@ -53,6 +53,7 @@ pub use self::change::{Change, MembershipChange, MembershipDetails};
state_key_type = OwnedUserId,
unsigned_type = RoomMemberUnsigned,
custom_redacted,
custom_possibly_redacted,
)]
pub struct RoomMemberEventContent {
/// The avatar URL for this user, if any.
@ -179,6 +180,11 @@ impl RedactContent for RoomMemberEventContent {
}
}
/// The possibly redacted form of [`RoomMemberEventContent`].
///
/// This type is used when it's not obvious whether the content is redacted or not.
pub type PossiblyRedactedRoomMemberEventContent = RoomMemberEventContent;
/// A member event that has been redacted.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
@ -509,7 +515,7 @@ pub struct RoomMemberUnsigned {
pub transaction_id: Option<OwnedTransactionId>,
/// Optional previous content of the event.
pub prev_content: Option<RoomMemberEventContent>,
pub prev_content: Option<PossiblyRedactedRoomMemberEventContent>,
/// State events to assist the receiver in identifying the room.
#[serde(default)]

View File

@ -60,7 +60,7 @@ fn deserialize_aliases_with_prev_content() {
assert_eq!(ev.sender, "@carl:example.com");
let prev_content = ev.unsigned.prev_content.unwrap();
assert_eq!(prev_content.aliases, vec![room_alias_id!("#inner:localhost")]);
assert_eq!(prev_content.aliases.unwrap(), vec![room_alias_id!("#inner:localhost")]);
}
#[test]
@ -78,7 +78,7 @@ fn deserialize_aliases_sync_with_room_id() {
assert_eq!(ev.sender, "@carl:example.com");
let prev_content = ev.unsigned.prev_content.unwrap();
assert_eq!(prev_content.aliases, vec![room_alias_id!("#inner:localhost")]);
assert_eq!(prev_content.aliases.unwrap(), vec![room_alias_id!("#inner:localhost")]);
}
#[test]
@ -177,7 +177,7 @@ fn deserialize_full_event_convert_to_sync() {
assert_eq!(sync_ev.event_id, "$h29iv0s8:example.com");
assert_eq!(sync_ev.origin_server_ts, MilliSecondsSinceUnixEpoch(uint!(1)));
assert_eq!(
sync_ev.unsigned.prev_content.unwrap().aliases,
sync_ev.unsigned.prev_content.unwrap().aliases.unwrap(),
vec![room_alias_id!("#inner:localhost")]
);
assert_eq!(sync_ev.sender, "@carl:example.com");

View File

@ -6,7 +6,7 @@ error: no event type attribute found, add `#[ruma_event(type = "any.room.event",
|
= note: this error originates in the derive macro `EventContent` (in Nightly builds, run with -Z macro-backtrace for more info)
error: expected one of: `type`, `kind`, `custom_redacted`, `state_key_type`, `unsigned_type`, `alias`, `without_relation`
error: expected one of: `type`, `kind`, `custom_redacted`, `custom_possibly_redacted`, `state_key_type`, `unsigned_type`, `alias`, `without_relation`
--> tests/events/ui/03-invalid-event-type.rs:11:14
|
11 | #[ruma_event(event = "m.macro.test", kind = State)]

View File

@ -7,8 +7,9 @@ use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote, ToTokens};
use syn::{
parse::{Parse, ParseStream},
parse_quote,
punctuated::Punctuated,
DeriveInput, Field, Ident, LitStr, Token, Type,
DeriveInput, Field, Ident, LitStr, Meta, NestedMeta, Token, Type,
};
use crate::util::m_prefix_name_to_type_name;
@ -20,6 +21,8 @@ mod kw {
syn::custom_keyword!(skip_redaction);
// Do not emit any redacted event code.
syn::custom_keyword!(custom_redacted);
// Do not emit any possibly redacted event code.
syn::custom_keyword!(custom_possibly_redacted);
// The kind of event content this is.
syn::custom_keyword!(kind);
syn::custom_keyword!(type_fragment);
@ -66,6 +69,7 @@ struct ContentMeta {
event_type: Option<LitStr>,
event_kind: Option<EventKind>,
custom_redacted: Option<kw::custom_redacted>,
custom_possibly_redacted: Option<kw::custom_possibly_redacted>,
state_key_type: Option<Box<Type>>,
unsigned_type: Option<Box<Type>>,
aliases: Vec<LitStr>,
@ -101,6 +105,10 @@ impl ContentMeta {
event_type: either_spanned(self.event_type, other.event_type)?,
event_kind: either_named("event_kind", self.event_kind, other.event_kind)?,
custom_redacted: either_spanned(self.custom_redacted, other.custom_redacted)?,
custom_possibly_redacted: either_spanned(
self.custom_possibly_redacted,
other.custom_possibly_redacted,
)?,
state_key_type: either_spanned(self.state_key_type, other.state_key_type)?,
unsigned_type: either_spanned(self.unsigned_type, other.unsigned_type)?,
aliases: [self.aliases, other.aliases].concat(),
@ -128,6 +136,13 @@ impl Parse for ContentMeta {
let custom_redacted: kw::custom_redacted = input.parse()?;
Ok(Self { custom_redacted: Some(custom_redacted), ..Default::default() })
} else if lookahead.peek(kw::custom_possibly_redacted) {
let custom_possibly_redacted: kw::custom_possibly_redacted = input.parse()?;
Ok(Self {
custom_possibly_redacted: Some(custom_possibly_redacted),
..Default::default()
})
} else if lookahead.peek(kw::state_key_type) {
let _: kw::state_key_type = input.parse()?;
let _: Token![=] = input.parse()?;
@ -162,7 +177,8 @@ struct ContentAttrs {
state_key_type: Option<TokenStream>,
unsigned_type: Option<TokenStream>,
aliases: Vec<LitStr>,
is_custom: bool,
is_custom_redacted: bool,
is_custom_possibly_redacted: bool,
has_without_relation: bool,
}
@ -174,6 +190,7 @@ impl TryFrom<ContentMeta> for ContentAttrs {
event_type,
event_kind,
custom_redacted,
custom_possibly_redacted,
state_key_type,
unsigned_type,
aliases,
@ -206,7 +223,8 @@ impl TryFrom<ContentMeta> for ContentAttrs {
}
};
let is_custom = custom_redacted.is_some();
let is_custom_redacted = custom_redacted.is_some();
let is_custom_possibly_redacted = custom_possibly_redacted.is_some();
let unsigned_type = unsigned_type.map(|ty| quote! { #ty });
@ -244,7 +262,8 @@ impl TryFrom<ContentMeta> for ContentAttrs {
state_key_type,
unsigned_type,
aliases,
is_custom,
is_custom_redacted,
is_custom_possibly_redacted,
has_without_relation,
})
}
@ -272,7 +291,8 @@ pub fn expand_event_content(
state_key_type,
unsigned_type,
aliases,
is_custom,
is_custom_redacted,
is_custom_possibly_redacted,
has_without_relation,
} = content_meta.try_into()?;
@ -288,7 +308,7 @@ pub fn expand_event_content(
};
// We only generate redacted content structs for state and message-like events
let redacted_event_content = needs_redacted(is_custom, event_kind).then(|| {
let redacted_event_content = needs_redacted(is_custom_redacted, event_kind).then(|| {
generate_redacted_event_content(
ident,
fields.clone(),
@ -302,6 +322,22 @@ pub fn expand_event_content(
.unwrap_or_else(syn::Error::into_compile_error)
});
// We only generate possibly redacted content structs for state events.
let possibly_redacted_event_content =
needs_possibly_redacted(is_custom_possibly_redacted, event_kind).then(|| {
generate_possibly_redacted_event_content(
ident,
fields.clone(),
&event_type,
event_kind,
state_key_type.as_ref(),
unsigned_type.clone(),
&aliases,
ruma_common,
)
.unwrap_or_else(syn::Error::into_compile_error)
});
let event_content_without_relation = has_without_relation.then(|| {
generate_event_content_without_relation(ident, fields.clone(), ruma_common)
.unwrap_or_else(syn::Error::into_compile_error)
@ -328,6 +364,7 @@ pub fn expand_event_content(
Ok(quote! {
#redacted_event_content
#possibly_redacted_event_content
#event_content_without_relation
#event_content_impl
#static_event_content_impl
@ -452,6 +489,157 @@ fn generate_redacted_event_content<'a>(
})
}
fn generate_possibly_redacted_event_content<'a>(
ident: &Ident,
fields: impl Iterator<Item = &'a Field>,
event_type: &LitStr,
event_kind: Option<EventKind>,
state_key_type: Option<&TokenStream>,
unsigned_type: Option<TokenStream>,
aliases: &[LitStr],
ruma_common: &TokenStream,
) -> syn::Result<TokenStream> {
assert!(
!event_type.value().contains('*'),
"Event type shouldn't contain a `*`, this should have been checked previously"
);
let serde = quote! { #ruma_common::exports::serde };
let doc = format!(
"The possibly redacted form of [`{ident}`].\n\n\
This type is used when it's not obvious whether the content is redacted or not."
);
let possibly_redacted_ident = format_ident!("PossiblyRedacted{ident}");
let mut field_changed = false;
let possibly_redacted_fields: Vec<_> = fields
.map(|f| {
let mut keep_field = false;
let mut unsupported_serde_attribute = None;
if let Type::Path(type_path) = &f.ty {
if type_path.path.segments.first().filter(|s| s.ident == "Option").is_some() {
// Keep the field if it's an `Option`.
keep_field = true;
}
}
let mut attrs = f
.attrs
.iter()
.map(|a| -> syn::Result<_> {
if a.path.is_ident("ruma_event") {
// Keep the field if it is not redacted.
if let EventFieldMeta::SkipRedaction = a.parse_args()? {
keep_field = true;
}
// Don't re-emit our `ruma_event` attributes.
Ok(None)
} else {
if a.path.is_ident("serde") {
let serde_meta = a.parse_meta()?;
if let Meta::List(list) = serde_meta {
for meta in list.nested.iter().filter_map(|nested_meta| match nested_meta {
NestedMeta::Meta(meta) => Some(meta),
NestedMeta::Lit(_) => None,
}) {
if meta.path().is_ident("default") {
// Keep the field if it deserializes to its default value.
keep_field = true;
} else if !meta.path().is_ident("rename") && !meta.path().is_ident("alias") && unsupported_serde_attribute.is_none() {
// Error if the field is not kept and uses an unsupported serde attribute.
unsupported_serde_attribute = Some(
syn::Error::new_spanned(
meta,
"Can't generate PossiblyRedacted struct with unsupported serde attribute\n\
Expected one of `default`, `rename` or `alias`\n\
Use the `custom_possibly_redacted` attribute and create the struct manually"
)
);
}
}
}
}
Ok(Some(a.clone()))
}
})
.filter_map(Result::transpose)
.collect::<syn::Result<_>>()?;
if keep_field {
Ok(Field { attrs, ..f.clone() })
} else if let Some(err) = unsupported_serde_attribute {
Err(err)
} else if f.ident.is_none() {
// If the field has no `ident`, it's a tuple struct. Since `content` is an object,
// it will need a custom struct to deserialize from an empty object.
Err(syn::Error::new(
Span::call_site(),
"Can't generate PossiblyRedacted struct for tuple structs\n\
Use the `custom_possibly_redacted` attribute and create the struct manually",
))
} else {
// Change the field to an `Option`.
field_changed = true;
let old_type = &f.ty;
let ty = parse_quote!{ Option<#old_type> };
attrs.push(parse_quote! { #[serde(skip_serializing_if = "Option::is_none")] });
Ok(Field { attrs, ty, ..f.clone() })
}
})
.collect::<syn::Result<_>>()?;
// If at least one field needs to change, generate a new struct, else use a type alias.
if field_changed {
let possibly_redacted_event_content = generate_event_content_impl(
&possibly_redacted_ident,
possibly_redacted_fields.iter(),
event_type,
event_kind,
state_key_type,
unsigned_type,
aliases,
ruma_common,
false,
)
.unwrap_or_else(syn::Error::into_compile_error);
let static_event_content_impl = event_kind.map(|kind| {
generate_static_event_content_impl(
&possibly_redacted_ident,
kind,
true,
event_type,
ruma_common,
)
});
Ok(quote! {
#[doc = #doc]
#[derive(Clone, Debug, #serde::Deserialize, #serde::Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct #possibly_redacted_ident {
#( #possibly_redacted_fields, )*
}
#possibly_redacted_event_content
#static_event_content_impl
})
} else {
Ok(quote! {
#[doc = #doc]
pub type #possibly_redacted_ident = #ident;
})
}
}
fn generate_event_content_without_relation<'a>(
ident: &Ident,
fields: impl Iterator<Item = &'a Field>,
@ -679,14 +867,17 @@ fn generate_event_content_impl<'a>(
let original_state_event_content_impl =
(event_kind == Some(EventKind::State) && is_original).then(|| {
let trait_name = format_ident!("Original{kind}Content");
let possibly_redacted_ident = format_ident!("PossiblyRedacted{ident}");
let unsigned_type = unsigned_type
.unwrap_or_else(|| quote! { #ruma_common::events::StateUnsigned<Self> });
let unsigned_type = unsigned_type.unwrap_or_else(
|| quote! { #ruma_common::events::StateUnsigned<Self::PossiblyRedacted> },
);
quote! {
#[automatically_derived]
impl #ruma_common::events::#trait_name for #ident {
type Unsigned = #unsigned_type;
type PossiblyRedacted = #possibly_redacted_ident;
}
}
});
@ -797,9 +988,20 @@ fn generate_static_event_content_impl(
}
}
fn needs_redacted(is_custom: bool, event_kind: Option<EventKind>) -> bool {
fn needs_redacted(is_custom_redacted: bool, event_kind: Option<EventKind>) -> bool {
// `is_custom` means that the content struct does not need a generated
// redacted struct also. If no `custom_redacted` attrs are found the content
// needs a redacted struct generated.
!is_custom && matches!(event_kind, Some(EventKind::MessageLike) | Some(EventKind::State))
!is_custom_redacted
&& matches!(event_kind, Some(EventKind::MessageLike) | Some(EventKind::State))
}
fn needs_possibly_redacted(
is_custom_possibly_redacted: bool,
event_kind: Option<EventKind>,
) -> bool {
// `is_custom_possibly_redacted` means that the content struct does not need
// a generated possibly redacted struct also. If no `custom_possibly_redacted`
// attrs are found the content needs a possibly redacted struct generated.
!is_custom_possibly_redacted && event_kind == Some(EventKind::State)
}