Some checks failed
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
168 lines
6.3 KiB
Rust
168 lines
6.3 KiB
Rust
//! 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<String, Error> {
|
|
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<String, Error> {
|
|
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::<Vec<_>>();
|
|
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<String, OSD>,
|
|
depth: usize,
|
|
output: &mut String,
|
|
) -> Result<(), Error> {
|
|
let mut entries = values.iter().collect::<Vec<_>>();
|
|
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"));
|
|
}
|
|
}
|