use js_int::int;
use ruma_common::{
CanonicalJsonObject, CanonicalJsonValue, ID_MAX_BYTES, RoomId,
room_version_rules::EventFormatRules,
};
use serde_json::to_string as to_json_string;
const MAX_PDU_BYTES: usize = 65_535;
const MAX_PREV_EVENTS_LENGTH: usize = 20;
const MAX_AUTH_EVENTS_LENGTH: usize = 10;
pub fn check_pdu_format(pdu: &CanonicalJsonObject, rules: &EventFormatRules) -> Result<(), String> {
let json =
to_json_string(&pdu).map_err(|e| format!("Failed to serialize canonical JSON: {e}"))?;
if json.len() > MAX_PDU_BYTES {
return Err("PDU is larger than maximum of {MAX_PDU_BYTES} bytes".to_owned());
}
let event_type = extract_required_string_field(pdu, "type")?;
extract_required_string_field(pdu, "sender")?;
let room_id = (event_type != "m.room.create" || rules.require_room_create_room_id)
.then(|| extract_required_string_field(pdu, "room_id"))
.transpose()?;
if rules.require_event_id {
extract_required_string_field(pdu, "event_id")?;
}
extract_optional_string_field(pdu, "state_key")?;
extract_required_array_field(pdu, "prev_events", MAX_PREV_EVENTS_LENGTH)?;
let auth_events = extract_required_array_field(pdu, "auth_events", MAX_AUTH_EVENTS_LENGTH)?;
if !rules.allow_room_create_in_auth_events {
if let Some(room_id) = room_id {
let room_create_event_reference_hash = <&RoomId>::try_from(room_id.as_str())
.map_err(|e| format!("invalid `room_id` field in PDU: {e}"))?
.strip_sigil();
for event_id in auth_events {
let CanonicalJsonValue::String(event_id) = event_id else {
return Err(format!(
"unexpected format of array item in `auth_events` field in PDU: \
expected string, got {event_id:?}"
));
};
let reference_hash = event_id.strip_prefix('$').ok_or(
"unexpected format of array item in `auth_events` field in PDU: \
string not beginning with the `$` sigil",
)?;
if reference_hash == room_create_event_reference_hash {
return Err("invalid `auth_events` field in PDU: \
cannot contain the `m.room.create` event ID"
.to_owned());
}
}
}
}
match pdu.get("depth") {
Some(CanonicalJsonValue::Integer(value)) => {
if *value < int!(0) {
return Err("invalid `depth` field in PDU: cannot be a negative integer".to_owned());
}
}
Some(value) => {
return Err(format!(
"unexpected format of `depth` field in PDU: \
expected integer, got {value:?}"
));
}
None => return Err("missing `depth` field in PDU".to_owned()),
}
Ok(())
}
fn extract_optional_string_field<'a>(
object: &'a CanonicalJsonObject,
field: &'a str,
) -> Result<Option<&'a String>, String> {
match object.get(field) {
Some(CanonicalJsonValue::String(value)) => {
if value.len() > ID_MAX_BYTES {
Err(format!(
"invalid `{field}` field in PDU: \
string length is larger than maximum of {ID_MAX_BYTES} bytes"
))
} else {
Ok(Some(value))
}
}
Some(value) => Err(format!(
"unexpected format of `{field}` field in PDU: \
expected string, got {value:?}"
)),
None => Ok(None),
}
}
fn extract_required_string_field<'a>(
object: &'a CanonicalJsonObject,
field: &'a str,
) -> Result<&'a String, String> {
extract_optional_string_field(object, field)?
.ok_or_else(|| format!("missing `{field}` field in PDU"))
}
fn extract_required_array_field<'a>(
object: &'a CanonicalJsonObject,
field: &'a str,
max_len: usize,
) -> Result<&'a [CanonicalJsonValue], String> {
match object.get(field) {
Some(CanonicalJsonValue::Array(value)) => {
if value.len() > max_len {
Err(format!(
"invalid `{field}` field in PDU: \
array length is larger than maximum of {max_len}"
))
} else {
Ok(value)
}
}
Some(value) => Err(format!(
"unexpected format of `{field}` field in PDU: \
expected array, got {value:?}"
)),
None => Err(format!("missing `{field}` field in PDU")),
}
}
#[cfg(test)]
mod tests {
use std::iter::repeat_n;
use js_int::int;
use ruma_common::{
CanonicalJsonObject, CanonicalJsonValue, room_version_rules::EventFormatRules,
};
use serde_json::{from_value as from_json_value, json};
use super::check_pdu_format;
fn pdu_v1() -> CanonicalJsonObject {
let pdu = json!({
"auth_events": [
[
"$af232176:example.org",
{ "sha256": "abase64encodedsha256hashshouldbe43byteslong" },
],
],
"content": {
"key": "value",
},
"depth": 12,
"event_id": "$a4ecee13e2accdadf56c1025:example.com",
"hashes": {
"sha256": "thishashcoversallfieldsincasethisisredacted"
},
"origin_server_ts": 1_838_188_000,
"prev_events": [
[
"$af232176:example.org",
{ "sha256": "abase64encodedsha256hashshouldbe43byteslong" }
],
],
"room_id": "!UcYsUzyxTGDxLBEvLy:example.org",
"sender": "@alice:example.com",
"signatures": {
"example.com": {
"ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus",
},
},
"type": "m.room.message",
"unsigned": {
"age": 4612,
},
});
from_json_value(pdu).unwrap()
}
fn pdu_v3() -> CanonicalJsonObject {
let pdu = json!({
"auth_events": [
"$base64encodedeventid",
"$adifferenteventid",
],
"content": {
"key": "value",
},
"depth": 12,
"hashes": {
"sha256": "thishashcoversallfieldsincasethisisredacted",
},
"origin_server_ts": 1_838_188_000,
"prev_events": [
"$base64encodedeventid",
"$adifferenteventid",
],
"redacts": "$some/old+event",
"room_id": "!UcYsUzyxTGDxLBEvLy:example.org",
"sender": "@alice:example.com",
"signatures": {
"example.com": {
"ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus",
},
},
"type": "m.room.message",
"unsigned": {
"age": 4612,
}
});
from_json_value(pdu).unwrap()
}
fn room_create_v12() -> CanonicalJsonObject {
let pdu = json!({
"auth_events": [],
"content": {
"room_version": "12",
},
"depth": 1,
"hashes": {
"sha256": "thishashcoversallfieldsincasethisisredacted",
},
"origin_server_ts": 1_838_188_000,
"prev_events": [],
"sender": "@alice:example.com",
"signatures": {
"example.com": {
"ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus",
},
},
"type": "m.room.create",
"unsigned": {
"age": 4612,
}
});
from_json_value(pdu).unwrap()
}
fn pdu_v12() -> CanonicalJsonObject {
let pdu = json!({
"auth_events": [
"$base64encodedeventid",
"$adifferenteventid",
],
"content": {
"key": "value",
},
"depth": 12,
"hashes": {
"sha256": "thishashcoversallfieldsincasethisisredacted",
},
"origin_server_ts": 1_838_188_000,
"prev_events": [
"$base64encodedeventid",
],
"room_id": "!roomcreatereferencehash",
"sender": "@alice:example.com",
"signatures": {
"example.com": {
"ed25519:key_version": "these86bytesofbase64signaturecoveressentialfieldsincludinghashessocancheckredactedpdus",
},
},
"type": "m.room.message",
"unsigned": {
"age": 4612,
}
});
from_json_value(pdu).unwrap()
}
#[test]
fn check_pdu_format_valid_v1() {
check_pdu_format(&pdu_v1(), &EventFormatRules::V1).unwrap();
}
#[test]
fn check_pdu_format_valid_v3() {
check_pdu_format(&pdu_v3(), &EventFormatRules::V3).unwrap();
}
#[test]
fn check_pdu_format_pdu_too_big() {
let mut pdu = pdu_v3();
let content = pdu.get_mut("content").unwrap().as_object_mut().unwrap();
let long_string = repeat_n('a', 66_000).collect::<String>();
content.insert("big_data".to_owned(), long_string.into());
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
}
#[test]
fn check_pdu_format_fields_missing() {
for field in
&["event_id", "sender", "room_id", "type", "prev_events", "auth_events", "depth"]
{
let mut pdu = pdu_v1();
pdu.remove(*field).unwrap();
check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err();
}
}
#[test]
fn check_pdu_format_strings_too_big() {
for field in &["event_id", "sender", "room_id", "type", "state_key"] {
let mut pdu = pdu_v1();
let value = repeat_n('a', 300).collect::<String>();
pdu.insert((*field).to_owned(), value.into());
check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err();
}
}
#[test]
fn check_pdu_format_strings_wrong_format() {
for field in &["event_id", "sender", "room_id", "type", "state_key"] {
let mut pdu = pdu_v1();
pdu.insert((*field).to_owned(), true.into());
check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err();
}
}
#[test]
fn check_pdu_format_arrays_too_big() {
for field in &["prev_events", "auth_events"] {
let mut pdu = pdu_v3();
let value =
repeat_n(CanonicalJsonValue::from("$eventid".to_owned()), 30).collect::<Vec<_>>();
pdu.insert((*field).to_owned(), value.into());
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
}
}
#[test]
fn check_pdu_format_arrays_wrong_format() {
for field in &["prev_events", "auth_events"] {
let mut pdu = pdu_v3();
pdu.insert((*field).to_owned(), true.into());
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
}
}
#[test]
fn check_pdu_format_negative_depth() {
let mut pdu = pdu_v3();
pdu.insert("depth".to_owned(), int!(-1).into()).unwrap();
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
}
#[test]
fn check_pdu_format_depth_wrong_format() {
let mut pdu = pdu_v3();
pdu.insert("depth".to_owned(), true.into());
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
}
#[test]
fn check_pdu_format_valid_room_create_v12() {
let pdu = room_create_v12();
check_pdu_format(&pdu, &EventFormatRules::V12).unwrap();
}
#[test]
fn check_pdu_format_valid_v12() {
let pdu = pdu_v12();
check_pdu_format(&pdu, &EventFormatRules::V12).unwrap();
}
#[test]
fn check_pdu_format_v12_with_room_create() {
let mut pdu = pdu_v12();
pdu.get_mut("auth_events")
.unwrap()
.as_array_mut()
.unwrap()
.push("$roomcreatereferencehash".to_owned().into());
check_pdu_format(&pdu, &EventFormatRules::V12).unwrap_err();
}
}