//! Deterministic, reflection-free packet and compatibility-object diagnostics. use std::fmt::Write as _; use libremetaverse_structured_data::OSD; use libremetaverse_types::Error; use libremetaverse_types::compat::Object; use crate::packets::{Header, Packet}; #[must_use] pub(crate) fn interpret_options(header: &Header) -> String { format!( "[{} {} {} {}]", if header.appended_acks { "Ack" } else { " " }, if header.resent { "Res" } else { " " }, if header.reliable { "Rel" } else { " " }, if header.zerocoded { "Zer" } else { " " }, ) } pub(crate) fn packet_to_string(packet: &Packet) -> Result { let descriptor = crate::packet_catalog::descriptor_by_type(packet.type_).ok_or(Error::InvalidOperation)?; let mut result = String::new(); writeln!( result, "Packet Type: {0} http://lib.openmetaverse.co/wiki/{0} http://wiki.secondlife.com/wiki/{0}", descriptor.name, ) .map_err(|_| Error::InvalidOperation)?; writeln!(result, "[Packet Header]").map_err(|_| Error::InvalidOperation)?; writeln!(result, "Sequence: {}", packet.header.sequence) .map_err(|_| Error::InvalidOperation)?; writeln!(result, " Options: {}", interpret_options(&packet.header)) .map_err(|_| Error::InvalidOperation)?; writeln!(result, "\n[Packet Payload]").map_err(|_| Error::InvalidOperation)?; let body = packet.packet_to_osd()?; format_osd_map(&body.snapshot(), 0, &mut result)?; Ok(result) } pub(crate) fn message_to_string(message: &Object, recurse_level: i32) -> Result { if recurse_level < 0 || recurse_level > 64 { return Err(Error::Argument); } let mut result = String::new(); format_object( message, usize::try_from(recurse_level).map_err(|_| Error::Argument)?, &mut result, )?; Ok(result) } fn format_object(value: &Object, depth: usize, output: &mut String) -> Result<(), Error> { let indent = " ".repeat(depth); match value { Object::Undefined => output.push_str(&format!("{indent}undefined\n")), Object::Boolean(value) => output.push_str(&format!("{indent}{value} [Boolean]\n")), Object::Integer(value) => output.push_str(&format!("{indent}{value} [Int32]\n")), Object::UInteger(value) => output.push_str(&format!("{indent}{value} [UInt32]\n")), Object::Long(value) => output.push_str(&format!("{indent}{value} [Int64]\n")), Object::ULong(value) => output.push_str(&format!("{indent}{value} [UInt64]\n")), Object::Real(value) => output.push_str(&format!("{indent}{value} [Double]\n")), Object::String(value) => output.push_str(&format!("{indent}{value} [String]\n")), Object::Bytes(value) => { output.push_str(&format!("{indent}[Byte[{0}]]\n", value.len())); format_hex(value, depth + 1, output)?; } Object::Array(values) => { output.push_str(&format!("{indent}[Array {0}]\n", values.len())); for (index, value) in values.iter().enumerate() { output.push_str(&format!("{indent} [{index}]\n")); format_object(value, depth + 2, output)?; } } Object::Map(values) => { output.push_str(&format!("{indent}[Map {0}]\n", values.len())); let mut entries = values.iter().collect::>(); entries.sort_by(|left, right| left.0.cmp(right.0)); for (key, value) in entries { output.push_str(&format!("{indent} {key}:\n")); format_object(value, depth + 2, output)?; } } other => output.push_str(&format!("{indent}{other:?}\n")), } Ok(()) } fn format_osd_map( values: &std::collections::HashMap, depth: usize, output: &mut String, ) -> Result<(), Error> { let mut entries = values.iter().collect::>(); entries.sort_by(|left, right| left.0.cmp(right.0)); for (key, value) in entries { writeln!(output, "{}{key}:", " ".repeat(depth)).map_err(|_| Error::InvalidOperation)?; format_osd(value, depth + 1, output)?; } Ok(()) } fn format_osd(value: &OSD, depth: usize, output: &mut String) -> Result<(), Error> { let indent = " ".repeat(depth); match value { OSD::Map(values) => format_osd_map(values, depth, output), OSD::Array(values) => { for (index, value) in values.iter().enumerate() { writeln!(output, "{indent}[{index}]").map_err(|_| Error::InvalidOperation)?; format_osd(value, depth + 1, output)?; } Ok(()) } OSD::Binary(bytes) => format_hex(bytes, depth, output), other => writeln!(output, "{indent}{other:?}").map_err(|_| Error::InvalidOperation), } } fn format_hex(bytes: &[u8], depth: usize, output: &mut String) -> Result<(), Error> { let indent = " ".repeat(depth); for chunk in bytes.chunks(16) { write!(output, "{indent}").map_err(|_| Error::InvalidOperation)?; for byte in chunk { write!(output, "{byte:02X} ").map_err(|_| Error::InvalidOperation)?; } output.push('\n'); } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn options_match_reference_layout() { let mut header = crate::packet_catalog::new_header(crate::PacketFrequency::High, 1, false); header.reliable = true; header.resent = true; assert_eq!(interpret_options(&header), "[ Res Rel ]"); } #[test] fn packet_dump_contains_header_and_sorted_payload() { let packet = crate::packet_catalog::build_packet(crate::packet_catalog::PacketType::CloseCircuit) .unwrap(); let dump = packet_to_string(&packet).unwrap(); assert!(dump.contains("Packet Type: CloseCircuit")); assert!(dump.contains("[Packet Header]")); assert!(dump.contains("[Packet Payload]")); } #[test] fn message_dump_formats_nested_values_and_bytes() { let value = Object::Map(std::collections::HashMap::from([ ("z".to_owned(), Object::Bytes(vec![0xab, 0xcd])), ("a".to_owned(), Object::Integer(7)), ])); let text = message_to_string(&value, 0).unwrap(); assert!(text.find("a:").unwrap() < text.find("z:").unwrap()); assert!(text.contains("AB CD")); } }