Files
rust-xmpp/src/pep_diagnostics.rs
T
2026-10-20 15:59:55 +07:00

118 lines
4.3 KiB
Rust

//! Payload-shape diagnostics. Values are fixed classifications and counts;
//! account names, node names, item identifiers and key material stay private.
use std::collections::BTreeSet;
const EVENT: &str = "http://jabber.org/protocol/pubsub#event";
const LEGACY: &str = "eu.siacs.conversations.axolotl";
const OMEMO2: &str = "urn:xmpp:omemo:2";
/// Inspect the first direct item in an IQ or event `items` wrapper.
pub fn item_fields(items: roxmltree::Node<'_, '_>) -> Vec<(&'static str, String)> {
let payload = items
.children()
.find(|n| {
n.is_element()
&& n.tag_name().name() == "item"
&& n.tag_name().namespace() == items.tag_name().namespace()
})
.and_then(|n| n.first_element_child());
let protocol = match payload.map(|n| (n.tag_name().namespace(), n.tag_name().name())) {
Some((Some(LEGACY), "list")) => "legacy",
Some((Some(OMEMO2), "devices")) => "omemo2",
_ => "other",
};
let mut valid = BTreeSet::new();
let mut invalid = 0;
if protocol != "other" {
for device in payload
.unwrap()
.children()
.filter(|n| n.is_element() && n.tag_name().name() == "device")
{
// Conversations' parser checks the local name, then uses Java
// Integer.valueOf; it does not trim or restrict IDs to positive values.
if let Some(id) = device.attribute("id").and_then(java_integer) {
valid.insert(id);
} else {
invalid += 1;
}
}
}
vec![
("device_protocol", protocol.into()),
("valid_device_count", valid.len().to_string()),
("invalid_device_count", invalid.to_string()),
]
}
/// Inspect outgoing PEP item notifications without disclosing stanza values.
pub fn event_fields(message: roxmltree::Node<'_, '_>) -> Option<Vec<(&'static str, String)>> {
if message.tag_name().name() != "message" {
return None;
}
let event = message
.children()
.find(|n| n.has_tag_name((EVENT, "event")))?;
let items = event
.children()
.find(|n| n.has_tag_name((EVENT, "items")))?;
let node_kind = match items.attribute("node") {
Some("eu.siacs.conversations.axolotl.devicelist" | "urn:xmpp:omemo:2:devices") => {
"omemo-devices"
}
Some("urn:xmpp:avatar:metadata") => "avatar-metadata",
Some("urn:xmpp:avatar:data") => "avatar-data",
_ => "other",
};
let mut direct_items = items.children().filter(|n| n.has_tag_name((EVENT, "item")));
let content_present = direct_items
.next()
.is_some_and(|n| n.first_element_child().is_some());
let items_count = items
.children()
.filter(|n| n.has_tag_name((EVENT, "item")))
.count();
let mut fields = vec![
("node_kind", node_kind.into()),
("items_count", items_count.to_string()),
("content_present", content_present.to_string()),
];
fields.extend(item_fields(items));
Some(fields)
}
// Java Integer.parseInt accepts BMP decimal digits through Character.digit(char,
// 10), but rejects supplementary digits because it processes UTF-16 code units.
fn java_integer(value: &str) -> Option<i32> {
let (negative, digits) = if let Some(v) = value.strip_prefix('-') {
(true, v)
} else {
(false, value.strip_prefix('+').unwrap_or(value))
};
if digits.is_empty() {
return None;
}
let limit = if negative {
2_147_483_648_i64
} else {
2_147_483_647
};
let mut number = 0_i64;
for digit in digits.chars() {
let code = digit as u32;
let zero = [
0x0030, 0x0660, 0x06f0, 0x07c0, 0x0966, 0x09e6, 0x0a66, 0x0ae6, 0x0b66, 0x0be6, 0x0c66,
0x0ce6, 0x0d66, 0x0de6, 0x0e50, 0x0ed0, 0x0f20, 0x1040, 0x1090, 0x17e0, 0x1810, 0x1946,
0x19d0, 0x1a80, 0x1a90, 0x1b50, 0x1bb0, 0x1c40, 0x1c50, 0xa620, 0xa8d0, 0xa900, 0xa9d0,
0xa9f0, 0xaa50, 0xabf0, 0xff10,
]
.into_iter()
.find(|zero| code >= *zero && code < *zero + 10)?;
number = number * 10 + i64::from(code - zero);
if number > limit {
return None;
}
}
Some(if negative { -number } else { number } as i32)
}