matrix_sdk_common/edit_validation.rs
1// Copyright 2026 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use ruma::{
16 events::{AnySyncTimelineEvent, relation::RelationType},
17 serde::Raw,
18};
19use serde::Deserialize;
20
21use crate::deserialized_responses::EncryptionInfo;
22
23/// Represents all possible validation errors that can occur when processing an
24/// event edit.
25///
26/// These errors ensure that a replacement event complies with the rules
27/// required to safely and correctly modify an existing event.
28///
29/// [spec]: https://spec.matrix.org/v1.17/client-server-api/#validity-of-replacement-events
30#[derive(Debug, thiserror::Error)]
31pub enum EditValidityError {
32 /// Occurs when the sender of the replacement event does not match the
33 /// sender of the original event.
34 ///
35 /// Only the original sender is allowed to edit their own event.
36 #[error(
37 "the sender of the original event isn't the same as the sender of the replacement event"
38 )]
39 InvalidSender,
40
41 /// Occurs when either the original event or the replacement event contains
42 /// a state key.
43 ///
44 /// State events are not allowed to be edited.
45 #[error("the original event or the replacement event contains a state key")]
46 StateKeyPresent,
47
48 /// Occurs when the content type of the original event differs from that of
49 /// the replacement event.
50 ///
51 /// Edits must not change the event’s content type, as this would introduce
52 /// semantic inconsistencies.
53 #[error(
54 "the content type of the original event is `{content_type}` while the replacement is a `{replacement_type}`"
55 )]
56 MismatchContentType {
57 /// The content type of the original event.
58 content_type: String,
59
60 /// The content type of the replacement event.
61 replacement_type: String,
62 },
63
64 /// Occurs when the original event is itself already a replacement (edit).
65 #[error("the original event is an edit as well")]
66 OriginalEventIsReplacement,
67
68 /// Occurs when the replacement event is not a replacement for the original
69 /// event.
70 #[error("the replacement event is not a replacement for the original event")]
71 NotReplacement,
72
73 /// Occurs when a required field is missing from either the original or the
74 /// replacement event.
75 ///
76 /// The event is considered malformed and cannot be validated.
77 #[error("the event was encrypted, as such it should have an `m.new_content` field")]
78 MissingNewContent,
79
80 #[error(transparent)]
81 InvalidJson(#[from] serde_json::Error),
82
83 /// Occurs when the original event is encrypted but the replacement event is
84 /// not.
85 #[error("the original event was encrypted while the replacement is not")]
86 ReplacementNotEncrypted,
87}
88
89/// This implements the Matrix spec rule set for validity of replacement events
90/// (edits). Invalid replacements must be ignored.
91///
92/// This function implements the steps documented in the [spec] with one
93/// exception, the step to check if the room IDs match isn't done. The JSON of
94/// the event might not contain the room ID if it wasn received over a `/sync`
95/// request.
96///
97/// _Warning_: Callers must ensure that the original event and replacement event
98/// belong to the same room, that is, they have the same room ID.
99///
100/// [spec]: https://spec.matrix.org/v1.17/client-server-api/#validity-of-replacement-events
101pub fn check_validity_of_replacement_events(
102 original_json: &Raw<AnySyncTimelineEvent>,
103 original_encryption_info: Option<&EncryptionInfo>,
104 replacement_json: &Raw<AnySyncTimelineEvent>,
105 replacement_encryption_info: Option<&EncryptionInfo>,
106) -> Result<(), EditValidityError> {
107 #[derive(Debug, Deserialize)]
108 struct MinimalEvent<'a> {
109 sender: &'a str,
110 event_id: &'a str,
111 #[serde(rename = "type")]
112 event_type: &'a str,
113 state_key: Option<&'a str>,
114 content: MinimalContent<'a>,
115 }
116
117 #[derive(Debug, Deserialize)]
118 struct MinimalContent<'a> {
119 #[serde(borrow, rename = "m.relates_to")]
120 relates_to: Option<MinimalRelatesTo<'a>>,
121 #[serde(rename = "m.new_content")]
122 new_content: Option<MinimalNewContent>,
123 }
124
125 #[derive(Debug, Deserialize)]
126 struct MinimalNewContent {}
127
128 #[derive(Debug, Deserialize)]
129 struct MinimalRelatesTo<'a> {
130 rel_type: Option<RelationType>,
131 event_id: Option<&'a str>,
132 }
133
134 let original_event = original_json.deserialize_as_unchecked::<MinimalEvent<'_>>()?;
135 let replacement_event = replacement_json.deserialize_as_unchecked::<MinimalEvent<'_>>()?;
136
137 // We don't check the room ID here since this event might have been received
138 // over /sync, in this case the JSON likely won't contain the room ID field.
139
140 // The original event and replacement event must have the same sender (i.e.
141 // you cannot edit someone else’s messages).
142 if original_event.sender != replacement_event.sender {
143 return Err(EditValidityError::InvalidSender);
144 }
145
146 // This check isn't part of the list in the spec, but it makes sense to
147 // check if the replacement event is has the correct rel_type and if it's an
148 // edit for the original event.
149 if let Some(relates_to) = replacement_event.content.relates_to {
150 if relates_to.rel_type != Some(RelationType::Replacement)
151 || relates_to.event_id != Some(original_event.event_id)
152 {
153 return Err(EditValidityError::NotReplacement);
154 }
155 } else {
156 return Err(EditValidityError::NotReplacement);
157 }
158
159 // The replacement and original events must have the same type (i.e. you
160 // cannot change the original event’s type).
161 if original_event.event_type != replacement_event.event_type {
162 return Err(EditValidityError::MismatchContentType {
163 content_type: original_event.event_type.to_owned(),
164 replacement_type: replacement_event.event_type.to_owned(),
165 });
166 }
167
168 // The replacement and original events must not have a state_key property
169 // (i.e. you cannot edit state events at all).
170 if original_event.state_key.is_some() || replacement_event.state_key.is_some() {
171 return Err(EditValidityError::StateKeyPresent);
172 }
173
174 // The original event must not, itself, have a rel_type of m.replace (i.e.
175 // you cannot edit an edit — though you can send multiple edits for a single
176 // original event).
177 if let Some(relates_to) = original_event.content.relates_to
178 && relates_to.rel_type == Some(RelationType::Replacement)
179 {
180 return Err(EditValidityError::OriginalEventIsReplacement);
181 }
182
183 // The replacement event (once decrypted, if appropriate) must have an
184 // m.new_content property.
185 if replacement_encryption_info.is_some() && replacement_event.content.new_content.is_none() {
186 return Err(EditValidityError::MissingNewContent);
187 }
188
189 // If the original event was encrypted, the replacement should be too.
190 if original_encryption_info.is_some() && replacement_encryption_info.is_none() {
191 return Err(EditValidityError::ReplacementNotEncrypted);
192 }
193
194 Ok(())
195}