Implement generated packet wire codecs (#47)
Some checks failed
Native code generation / deterministic (push) Successful in 4m29s
Imaging and meshing gate / native (push) Failing after 16s
JPEG 2000 feature / linux (push) Failing after 59s
Skia feature / linux (push) Failing after 1m37s

This commit is contained in:
2026-08-09 08:16:32 +00:00
parent 5bc22bdd83
commit 74801c3488
16 changed files with 87505 additions and 25470 deletions

View File

@@ -16,12 +16,19 @@ jobs:
run: cargo test -p libremetaverse-codegen
- name: Verify vendored hashes and checked-in output
run: cargo run -p libremetaverse-codegen -- check
- name: Test generated wire codecs and translated parity cases
run: |
cargo test -p libremetaverse --lib generated_codec_tests
cargo test -p libremetaverse --test packet_wire
cargo test -p libremetaverse-compat-tests --test wire_semantics
- name: Regenerate twice without reference checkout access
run: |
cargo run -p libremetaverse-codegen -- generate
cargo run -p libremetaverse-codegen -- generate
git diff --exit-code -- codegen/generated crates/libremetaverse/src/packet_catalog.rs
- name: Lint native generator
run: cargo clippy -p libremetaverse-codegen --all-targets -- -D warnings
run: |
cargo clippy -p libremetaverse-codegen --all-targets -- -D warnings
cargo clippy -p libremetaverse --all-targets --no-deps -- -D warnings
- name: Prove normal consumer build does not execute generation
run: cargo build -p libremetaverse --locked

View File

@@ -230,5 +230,9 @@ checked-in catalog covering all 483 packets, 905 blocks, exact field widths,
repetition rules, flags, IDs, and frequencies. It supplies the public
`PacketType`, deterministic lookup tables, and real default construction and
sizing behavior for every mapped packet/block while validating the complete
shape against the pinned compiled C# API catalog. Wire encoding and decoding
remain the separately gated next codegen issue.
shape against the pinned compiled C# API catalog. The generated native packet
codec implements the reference header frequencies and IDs, flags, sequences,
appended ACKs, little-endian fields, big-endian ports, fixed and length-prefixed
data, block repetition, zerocoding, bounded decoding, and MTU packet splitting.
Golden-byte tests and generated round trips cover every packet type without
requiring the C# toolchain at build or test time.

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand.
| Assembly | Types | Members | Status |
|---|---:|---:|---|
| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 4 types / 3,752 members; remaining surface is callable failure-only shims |
| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 5 types / 10,469 members; remaining surface is callable failure-only shims |
| `LibreMetaverse.Imaging.Abstractions` | 3 | 20 | native implementation: 3 types / 20 members; no generated shims remain |
| `LibreMetaverse.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain |
| `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim |

View File

@@ -51,6 +51,17 @@ receive the same zero defaults as the golden generator. The generated `Length`
metadata intentionally preserves the golden generator's variable-block count
semantics.
The same output contains the native wire implementation for every generated
packet and block. It preserves high-, medium-, and low-frequency headers,
network-order packet IDs, sequence numbers and appended ACKs; the reference
field endianness and widths; one- and two-byte variable prefixes; fixed-field
padding and truncation; fixed and variable block counts; normalized quaternion
encoding; zerocoding boundaries; and the reference `ToBytesMultiple` splitting
rules. Readers use checked bounds and allocation arithmetic and reject unknown
IDs or truncated payloads without panicking. Generated tests round-trip all 483
packet types and exercise every truncation boundary, while focused fixtures
assert exact reference bytes and malformed-input behavior.
Generation also compares all packet, block, field, field-type, and `PacketType`
entries against the pinned compiled public API catalog. This rejects a template
parser or naming change even if the emitted Rust would otherwise compile. The

View File

@@ -0,0 +1,305 @@
//! Bit-level packing compatible with `LibreMetaverse`'s `BitPack` wire helper.
#![allow(clippy::missing_errors_doc)] // Public signatures are fixed by the C# compatibility map.
#![allow(clippy::cast_precision_loss)] // Fixed-point conversion intentionally mirrors C# casts.
#![allow(clippy::cast_possible_truncation)] // Fixed-point packing truncates exactly like C#.
#![allow(clippy::cast_sign_loss)] // Signed fixed-point values are biased before conversion.
use crate::Error;
use libremetaverse_types::{Color4, UUID};
const BITS_PER_BYTE: i32 = 8;
/// A cursor over a caller-owned byte buffer that reads and writes from the
/// most-significant bit to the least-significant bit of each byte.
pub struct BitPack {
pub data: Vec<u8>,
byte_pos: usize,
bit_pos: i32,
}
impl BitPack {
pub fn new(data: Vec<u8>, pos: i32) -> Result<Self, Error> {
let byte_pos = usize::try_from(pos).map_err(|_| Error::IndexOutOfRange)?;
if byte_pos > data.len() {
return Err(Error::IndexOutOfRange);
}
Ok(Self {
data,
byte_pos,
bit_pos: 0,
})
}
pub fn pack_bit(&mut self, bit: bool) -> Result<(), Error> {
self.pack_bit_array(&[u8::from(bit)], 1)
}
pub fn pack_bits_with_int32_int32(&mut self, data: i32, total_count: i32) -> Result<(), Error> {
self.pack_bit_array(&data.to_le_bytes(), total_count)
}
pub fn pack_bits_with_u_int32_int32(
&mut self,
data: u32,
total_count: i32,
) -> Result<(), Error> {
self.pack_bit_array(&data.to_le_bytes(), total_count)
}
pub fn pack_color(&mut self, data: Color4) -> Result<(), Error> {
self.pack_bit_array(&data.get_bytes_with_method()?, 32)
}
pub fn pack_fixed(
&mut self,
data: f32,
is_signed: bool,
int_bits: i32,
frac_bits: i32,
) -> Result<(), Error> {
let unsigned_bits = int_bits.checked_add(frac_bits).ok_or(Error::Argument)?;
let total_bits = unsigned_bits
.checked_add(i32::from(is_signed))
.ok_or(Error::Argument)?;
let max = checked_power_of_two(int_bits)?;
let min = if is_signed { -max } else { 0 };
let scale = checked_power_of_two(frac_bits)?;
let mut fixed = data.clamp(min as f32, max as f32);
if is_signed {
fixed += max as f32;
}
fixed *= scale as f32;
let storage_bits = match total_bits {
..=8 => 8,
9..=16 => 16,
17..=31 => 32,
_ => return Err(Error::Argument),
};
self.pack_bits_with_u_int32_int32(fixed as u32, storage_bits)
}
pub fn pack_float(&mut self, data: f32) -> Result<(), Error> {
self.pack_bit_array(&data.to_le_bytes(), 32)
}
pub fn pack_uuid(&mut self, data: UUID) -> Result<(), Error> {
for byte in data.get_bytes()? {
self.pack_bit_array(&[byte], 8)?;
}
Ok(())
}
pub fn unpack_bits(&mut self, total_count: i32) -> Result<i32, Error> {
Ok(self.unpack_bits_to_u32(total_count)?.cast_signed())
}
pub fn unpack_byte(&mut self) -> Result<u8, Error> {
u8::try_from(self.unpack_bits_to_u32(8)?).map_err(|_| Error::Argument)
}
pub fn unpack_fixed(
&mut self,
signed: bool,
int_bits: i32,
frac_bits: i32,
) -> Result<f32, Error> {
let unsigned_bits = int_bits.checked_add(frac_bits).ok_or(Error::Argument)?;
let total_bits = unsigned_bits
.checked_add(i32::from(signed))
.ok_or(Error::Argument)?;
let max = checked_power_of_two(int_bits)?;
let scale = checked_power_of_two(frac_bits)?;
let packed = match total_bits {
..=8 => u32::from(self.unpack_byte()?),
9..=16 => self.unpack_bits_to_u32(16)?,
17..=31 => self.unpack_bits_to_u32(32)?,
_ => return Ok(0.0),
};
let mut fixed = packed as f32 / scale as f32;
if signed {
fixed -= max as f32;
}
Ok(fixed)
}
pub fn unpack_float(&mut self) -> Result<f32, Error> {
Ok(f32::from_bits(self.unpack_bits_to_u32(32)?))
}
pub fn unpack_int(&mut self) -> Result<i32, Error> {
self.unpack_bits(32)
}
pub fn unpack_short(&mut self) -> Result<i16, Error> {
Ok(u16::try_from(self.unpack_bits_to_u32(16)?)
.map_err(|_| Error::Argument)?
.cast_signed())
}
pub fn unpack_string(&mut self, size: i32) -> Result<String, Error> {
if self.bit_pos != 0 {
return Err(Error::IndexOutOfRange);
}
let size = usize::try_from(size).map_err(|_| Error::IndexOutOfRange)?;
let end = self
.byte_pos
.checked_add(size)
.ok_or(Error::IndexOutOfRange)?;
let bytes = self
.data
.get(self.byte_pos..end)
.ok_or(Error::IndexOutOfRange)?;
let value = String::from_utf8_lossy(bytes).into_owned();
self.byte_pos = end;
Ok(value)
}
pub fn unpack_u_bits(&mut self, total_count: i32) -> Result<u32, Error> {
self.unpack_bits_to_u32(total_count)
}
pub fn unpack_u_int(&mut self) -> Result<u32, Error> {
self.unpack_bits_to_u32(32)
}
pub fn unpack_u_short(&mut self) -> Result<u16, Error> {
u16::try_from(self.unpack_bits_to_u32(16)?).map_err(|_| Error::Argument)
}
pub fn unpack_uuid(&mut self) -> Result<UUID, Error> {
if self.bit_pos != 0 {
return Err(Error::IndexOutOfRange);
}
let end = self
.byte_pos
.checked_add(16)
.ok_or(Error::IndexOutOfRange)?;
let bytes = self
.data
.get(self.byte_pos..end)
.ok_or(Error::IndexOutOfRange)?;
let value = UUID::new_with_bytes_int32(bytes.to_vec(), 0)?;
self.byte_pos = end;
Ok(value)
}
#[must_use]
pub fn bit_pos(&self) -> i32 {
self.bit_pos
}
pub fn set_bit_pos(&mut self, value: i32) {
self.bit_pos = value;
}
#[must_use]
pub fn byte_pos(&self) -> i32 {
let position = if self.byte_pos != 0 && self.bit_pos == 0 {
self.byte_pos - 1
} else {
self.byte_pos
};
i32::try_from(position).unwrap_or(i32::MAX)
}
fn pack_bit_array(&mut self, source: &[u8], mut total_count: i32) -> Result<(), Error> {
if total_count <= 0 {
return Ok(());
}
let required = usize::try_from((total_count + 7) / 8).map_err(|_| Error::Argument)?;
if required > source.len() {
return Err(Error::Argument);
}
let mut source_byte = 0_usize;
while total_count > 0 {
let mut count = total_count.min(BITS_PER_BYTE);
total_count -= count;
while count > 0 {
let target = self
.data
.get_mut(self.byte_pos)
.ok_or(Error::IndexOutOfRange)?;
let target_bit = 0x80_u8 >> self.bit_pos;
let source_bit = 1_u8 << (count - 1);
if source[source_byte] & source_bit != 0 {
*target |= target_bit;
} else {
*target &= !target_bit;
}
count -= 1;
self.bit_pos += 1;
if self.bit_pos >= BITS_PER_BYTE {
self.bit_pos = 0;
self.byte_pos += 1;
}
}
source_byte += 1;
}
Ok(())
}
fn unpack_bits_to_u32(&mut self, mut total_count: i32) -> Result<u32, Error> {
if total_count <= 0 {
return Ok(0);
}
if total_count > 32 {
return Err(Error::Argument);
}
let mut output = [0_u8; 4];
let mut output_byte = 0_usize;
while total_count > 0 {
let mut count = total_count.min(BITS_PER_BYTE);
total_count -= count;
while count > 0 {
output[output_byte] <<= 1;
let source = *self.data.get(self.byte_pos).ok_or(Error::IndexOutOfRange)?;
if source & (0x80_u8 >> self.bit_pos) != 0 {
output[output_byte] += 1;
}
self.bit_pos += 1;
count -= 1;
if self.bit_pos >= BITS_PER_BYTE {
self.bit_pos = 0;
self.byte_pos += 1;
}
}
output_byte += 1;
}
Ok(u32::from_le_bytes(output))
}
}
fn checked_power_of_two(bits: i32) -> Result<i32, Error> {
let bits = u32::try_from(bits).map_err(|_| Error::Argument)?;
1_i32.checked_shl(bits).ok_or(Error::Argument)
}
#[cfg(test)]
mod tests {
use super::BitPack;
use crate::Error;
#[test]
fn reports_bounds_errors_without_panicking() {
let mut writer = BitPack::new(vec![], 0).unwrap();
assert_eq!(writer.pack_bit(true), Err(Error::IndexOutOfRange));
let mut reader = BitPack::new(vec![0], 0).unwrap();
assert_eq!(reader.unpack_u_bits(33), Err(Error::Argument));
assert_eq!(reader.unpack_u_bits(16), Err(Error::IndexOutOfRange));
}
#[test]
fn byte_position_matches_completed_byte_semantics() {
let mut bits = BitPack::new(vec![0; 2], 0).unwrap();
bits.pack_bit(true).unwrap();
assert_eq!(bits.byte_pos(), 0);
for _ in 1..8 {
bits.pack_bit(false).unwrap();
}
assert_eq!(bits.byte_pos(), 0);
assert_eq!(bits.bit_pos(), 0);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,10 +2,13 @@
extern crate self as libremetaverse;
mod bit_pack;
mod generated;
mod j2k;
mod message_codec;
#[rustfmt::skip]
pub mod packet_catalog;
mod packet_wire;
mod targa;
#[cfg(test)]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,740 @@
//! Bounded primitives shared by the generated UDP packet codecs.
use crate::packets::{Header, Packet};
use crate::{Error, Helpers, PacketFrequency};
use libremetaverse_types::{Quaternion, UUID, Vector3, Vector3d, Vector4};
/// A generous protocol safety ceiling. UDP packets normally remain below the
/// 1200-byte MTU, but callers may serialize an unsplit packet for diagnostics.
pub(crate) const MAX_PACKET_BYTES: usize = 16 * 1024 * 1024;
const HEADER_PREFIX_BYTES: usize = 6;
const fn parse(position: usize, context: &'static str) -> Error {
Error::Parse { position, context }
}
#[derive(Debug)]
pub(crate) struct WireReader<'a> {
bytes: &'a [u8],
position: usize,
end: usize,
}
impl<'a> WireReader<'a> {
pub(crate) fn new(bytes: &'a [u8], position: usize, end: usize) -> Result<Self, Error> {
if bytes.len() > MAX_PACKET_BYTES || position > end || end > bytes.len() {
return Err(parse(position.min(bytes.len()), "packet bounds"));
}
Ok(Self {
bytes,
position,
end,
})
}
fn take(&mut self, length: usize, context: &'static str) -> Result<&'a [u8], Error> {
let end = self
.position
.checked_add(length)
.ok_or_else(|| parse(self.position, context))?;
if end > self.end {
return Err(parse(self.position, context));
}
let output = &self.bytes[self.position..end];
self.position = end;
Ok(output)
}
pub(crate) fn finish(self, position: &mut i32) -> Result<(), Error> {
if self.position != self.end {
return Err(parse(self.position, "trailing packet payload"));
}
*position = i32::try_from(self.position).map_err(|_| Error::Argument)?;
Ok(())
}
pub(crate) fn commit_position(&self, position: &mut i32) -> Result<(), Error> {
*position = i32::try_from(self.position).map_err(|_| Error::Argument)?;
Ok(())
}
pub(crate) fn read_u8(&mut self) -> Result<u8, Error> {
Ok(self.take(1, "truncated u8")?[0])
}
pub(crate) fn read_i8(&mut self) -> Result<i8, Error> {
Ok(self.read_u8()?.cast_signed())
}
pub(crate) fn read_u16_le(&mut self) -> Result<u16, Error> {
Ok(u16::from_le_bytes(self.read_array("truncated u16")?))
}
pub(crate) fn read_u16_be(&mut self) -> Result<u16, Error> {
Ok(u16::from_be_bytes(self.read_array("truncated IP port")?))
}
pub(crate) fn read_i16_le(&mut self) -> Result<i16, Error> {
Ok(i16::from_le_bytes(self.read_array("truncated i16")?))
}
pub(crate) fn read_u32_le(&mut self) -> Result<u32, Error> {
Ok(u32::from_le_bytes(self.read_array("truncated u32")?))
}
pub(crate) fn read_i32_le(&mut self) -> Result<i32, Error> {
Ok(i32::from_le_bytes(self.read_array("truncated i32")?))
}
pub(crate) fn read_u64_le(&mut self) -> Result<u64, Error> {
Ok(u64::from_le_bytes(self.read_array("truncated u64")?))
}
pub(crate) fn read_f32_le(&mut self) -> Result<f32, Error> {
Ok(f32::from_le_bytes(self.read_array("truncated f32")?))
}
pub(crate) fn read_f64_le(&mut self) -> Result<f64, Error> {
Ok(f64::from_le_bytes(self.read_array("truncated f64")?))
}
fn read_array<const N: usize>(&mut self, context: &'static str) -> Result<[u8; N], Error> {
self.take(N, context)?
.try_into()
.map_err(|_| parse(self.position, context))
}
pub(crate) fn read_bytes(
&mut self,
length: usize,
context: &'static str,
) -> Result<Vec<u8>, Error> {
let source = self.take(length, context)?;
let mut output = Vec::new();
output
.try_reserve_exact(length)
.map_err(|_| Error::InvalidOperation)?;
output.extend_from_slice(source);
Ok(output)
}
pub(crate) fn read_variable(&mut self, prefix_bytes: usize) -> Result<Vec<u8>, Error> {
let length = match prefix_bytes {
1 => usize::from(self.read_u8()?),
2 => usize::from(self.read_u16_le()?),
_ => return Err(Error::InvalidOperation),
};
self.read_bytes(length, "truncated variable packet field")
}
pub(crate) fn read_uuid(&mut self) -> Result<UUID, Error> {
UUID::from_bytes(self.read_bytes(16, "truncated UUID")?, 0)
}
pub(crate) fn read_vector3(&mut self) -> Result<Vector3, Error> {
Vector3::from_bytes(self.read_bytes(12, "truncated Vector3")?, 0)
}
pub(crate) fn read_vector3d(&mut self) -> Result<Vector3d, Error> {
let mut value = Vector3d::zero();
value.from_bytes(self.read_bytes(24, "truncated Vector3d")?, 0)?;
Ok(value)
}
pub(crate) fn read_vector4(&mut self) -> Result<Vector4, Error> {
let mut value = Vector4::zero();
value.from_bytes(self.read_bytes(16, "truncated Vector4")?, 0)?;
Ok(value)
}
pub(crate) fn read_quaternion(&mut self) -> Result<Quaternion, Error> {
Quaternion::from_bytes(self.read_bytes(12, "truncated Quaternion")?, 0, true)
}
}
#[derive(Debug, Default)]
pub(crate) struct WireWriter {
bytes: Vec<u8>,
}
impl WireWriter {
pub(crate) fn with_capacity(capacity: usize) -> Result<Self, Error> {
if capacity > MAX_PACKET_BYTES {
return Err(Error::Argument);
}
let mut bytes = Vec::new();
bytes
.try_reserve_exact(capacity)
.map_err(|_| Error::InvalidOperation)?;
Ok(Self { bytes })
}
pub(crate) fn into_inner(self) -> Vec<u8> {
self.bytes
}
fn reserve(&mut self, additional: usize) -> Result<(), Error> {
let length = self
.bytes
.len()
.checked_add(additional)
.ok_or(Error::Argument)?;
if length > MAX_PACKET_BYTES {
return Err(Error::Argument);
}
self.bytes
.try_reserve(additional)
.map_err(|_| Error::InvalidOperation)
}
pub(crate) fn write_bytes(&mut self, value: &[u8]) -> Result<(), Error> {
self.reserve(value.len())?;
self.bytes.extend_from_slice(value);
Ok(())
}
pub(crate) fn write_u8(&mut self, value: u8) -> Result<(), Error> {
self.write_bytes(&[value])
}
pub(crate) fn write_i8(&mut self, value: i8) -> Result<(), Error> {
self.write_u8(value.cast_unsigned())
}
pub(crate) fn write_u16_le(&mut self, value: u16) -> Result<(), Error> {
self.write_bytes(&value.to_le_bytes())
}
pub(crate) fn write_u16_be(&mut self, value: u16) -> Result<(), Error> {
self.write_bytes(&value.to_be_bytes())
}
pub(crate) fn write_i16_le(&mut self, value: i16) -> Result<(), Error> {
self.write_bytes(&value.to_le_bytes())
}
pub(crate) fn write_u32_le(&mut self, value: u32) -> Result<(), Error> {
self.write_bytes(&value.to_le_bytes())
}
pub(crate) fn write_i32_le(&mut self, value: i32) -> Result<(), Error> {
self.write_bytes(&value.to_le_bytes())
}
pub(crate) fn write_u64_le(&mut self, value: u64) -> Result<(), Error> {
self.write_bytes(&value.to_le_bytes())
}
pub(crate) fn write_f32_le(&mut self, value: f32) -> Result<(), Error> {
self.write_bytes(&value.to_le_bytes())
}
pub(crate) fn write_f64_le(&mut self, value: f64) -> Result<(), Error> {
self.write_bytes(&value.to_le_bytes())
}
pub(crate) fn write_fixed(&mut self, value: &[u8], length: usize) -> Result<(), Error> {
self.reserve(length)?;
let copied = value.len().min(length);
self.bytes.extend_from_slice(&value[..copied]);
self.bytes.resize(self.bytes.len() + length - copied, 0);
Ok(())
}
pub(crate) fn write_variable(
&mut self,
value: &[u8],
prefix_bytes: usize,
) -> Result<(), Error> {
match prefix_bytes {
1 => self.write_u8(u8::try_from(value.len()).map_err(|_| Error::Argument)?)?,
2 => self.write_u16_le(u16::try_from(value.len()).map_err(|_| Error::Argument)?)?,
_ => return Err(Error::InvalidOperation),
}
self.write_bytes(value)
}
pub(crate) fn write_uuid(&mut self, value: &UUID) -> Result<(), Error> {
let mut bytes = [0_u8; 16];
value.to_bytes(&mut bytes, 0)?;
self.write_bytes(&bytes)
}
pub(crate) fn write_vector3(&mut self, value: &Vector3) -> Result<(), Error> {
let mut bytes = [0_u8; 12];
value.to_bytes(&mut bytes, 0)?;
self.write_bytes(&bytes)
}
pub(crate) fn write_vector3d(&mut self, value: &Vector3d) -> Result<(), Error> {
let mut bytes = [0_u8; 24];
value.to_bytes(&mut bytes, 0)?;
self.write_bytes(&bytes)
}
pub(crate) fn write_vector4(&mut self, value: &Vector4) -> Result<(), Error> {
let mut bytes = [0_u8; 16];
value.to_bytes(&mut bytes, 0)?;
self.write_bytes(&bytes)
}
pub(crate) fn write_quaternion(&mut self, value: &Quaternion) -> Result<(), Error> {
let mut bytes = [0_u8; 12];
value.to_bytes(&mut bytes, 0)?;
self.write_bytes(&bytes)
}
}
pub(crate) const fn header_length(frequency: PacketFrequency) -> usize {
match frequency {
PacketFrequency::High => 7,
PacketFrequency::Medium => 8,
PacketFrequency::Low => 10,
}
}
pub(crate) fn clone_header(header: &Header) -> Header {
Header {
ack_list: header.ack_list.clone(),
appended_acks: header.appended_acks,
frequency: header.frequency,
id: header.id,
reliable: header.reliable,
resent: header.resent,
sequence: header.sequence,
zerocoded: header.zerocoded,
}
}
pub(crate) fn encode_header(header: &Header, writer: &mut WireWriter) -> Result<(), Error> {
let mut flags = 0_u8;
if header.reliable {
flags |= Helpers::MSG_RELIABLE;
}
if header.resent {
flags |= Helpers::MSG_RESENT;
}
if header.zerocoded {
flags |= Helpers::MSG_ZEROCODED;
}
if header.appended_acks {
flags |= Helpers::MSG_APPENDED_ACKS;
}
writer.write_u8(flags)?;
writer.write_bytes(&header.sequence.to_be_bytes())?;
writer.write_u8(0)?;
match header.frequency {
PacketFrequency::High => {
writer.write_u8(u8::try_from(header.id).map_err(|_| Error::Argument)?)?;
}
PacketFrequency::Medium => {
writer.write_u8(0xff)?;
writer.write_u8(u8::try_from(header.id).map_err(|_| Error::Argument)?)?;
}
PacketFrequency::Low => {
writer.write_bytes(&[0xff, 0xff])?;
writer.write_bytes(&header.id.to_be_bytes())?;
}
}
Ok(())
}
pub(crate) fn encode_acks(header: &Header, writer: &mut WireWriter) -> Result<(), Error> {
let Some(acks) = &header.ack_list else {
return Ok(());
};
if acks.is_empty() {
return Ok(());
}
let count = u8::try_from(acks.len()).map_err(|_| Error::Argument)?;
for ack in acks {
writer.write_bytes(&ack.to_be_bytes())?;
}
writer.write_u8(count)
}
pub(crate) fn ack_length(header: &Header) -> Result<usize, Error> {
match &header.ack_list {
Some(acks) if !acks.is_empty() => {
let _ = u8::try_from(acks.len()).map_err(|_| Error::Argument)?;
acks.len()
.checked_mul(4)
.and_then(|length| length.checked_add(1))
.ok_or(Error::Argument)
}
_ => Ok(0),
}
}
pub(crate) fn decode_header(
bytes: &[u8],
position: &mut i32,
packet_end: &mut i32,
) -> Result<Header, Error> {
if bytes.len() > MAX_PACKET_BYTES || *position < 0 || *packet_end < 0 {
return Err(Error::Argument);
}
let start = usize::try_from(*position).map_err(|_| Error::Argument)?;
let mut end = usize::try_from(*packet_end).map_err(|_| Error::Argument)?;
if end >= bytes.len() || start > end || end - start + 1 < 7 {
return Err(parse(start.min(bytes.len()), "truncated packet header"));
}
let flags = bytes[start];
let appended_acks = flags & Helpers::MSG_APPENDED_ACKS != 0;
let reliable = flags & Helpers::MSG_RELIABLE != 0;
let resent = flags & Helpers::MSG_RESENT != 0;
let zerocoded = flags & Helpers::MSG_ZEROCODED != 0;
let sequence = u32::from_be_bytes(
bytes
.get(start + 1..start + 5)
.ok_or_else(|| parse(start, "truncated packet sequence"))?
.try_into()
.map_err(|_| parse(start, "truncated packet sequence"))?,
);
let marker = *bytes
.get(start + 6)
.ok_or_else(|| parse(start, "truncated packet ID"))?;
let (frequency, id, logical_header_length) = if marker == 0xff {
let second = *bytes
.get(start + 7)
.ok_or_else(|| parse(start + 7, "truncated packet ID"))?;
if second == 0xff {
let high = *bytes
.get(start + 8)
.ok_or_else(|| parse(start + 8, "truncated packet ID"))?;
let id = if zerocoded && high == 0 {
let count = *bytes
.get(start + 9)
.ok_or_else(|| parse(start + 9, "truncated zerocoded packet ID"))?;
if count == 0 {
return Err(parse(start + 9, "zero-length zerocode run"));
}
u16::from(
*bytes
.get(start + 10)
.ok_or_else(|| parse(start + 10, "truncated zerocoded packet ID"))?,
)
} else {
let low = *bytes
.get(start + 9)
.ok_or_else(|| parse(start + 9, "truncated packet ID"))?;
(u16::from(high) << 8) | u16::from(low)
};
(PacketFrequency::Low, id, 10)
} else {
(PacketFrequency::Medium, u16::from(second), 8)
}
} else {
(PacketFrequency::High, u16::from(marker), 7)
};
let mut ack_list = None;
if appended_acks {
let count = usize::from(bytes[end]);
let ack_bytes = count.checked_mul(4).ok_or(Error::Argument)?;
let total = ack_bytes.checked_add(1).ok_or(Error::Argument)?;
if total > end - start + 1 || end + 1 - total < start + logical_header_length {
return Err(parse(end, "truncated appended ACK list"));
}
let ack_start = end + 1 - total;
let mut acks = Vec::new();
acks.try_reserve_exact(count)
.map_err(|_| Error::InvalidOperation)?;
for chunk in bytes[ack_start..ack_start + ack_bytes].chunks_exact(4) {
acks.push(u32::from_be_bytes(
chunk
.try_into()
.map_err(|_| parse(ack_start, "truncated appended ACK"))?,
));
}
ack_list = Some(acks);
end = ack_start
.checked_sub(1)
.ok_or_else(|| parse(ack_start, "invalid appended ACK boundary"))?;
}
*position = i32::try_from(start + logical_header_length).map_err(|_| Error::Argument)?;
*packet_end = i32::try_from(end).map_err(|_| Error::Argument)?;
Ok(Header {
ack_list,
appended_acks,
frequency,
id,
reliable,
resent,
sequence,
zerocoded,
})
}
pub(crate) fn header_to_slice(
header: &Header,
bytes: &mut [u8],
position: &mut i32,
) -> Result<(), Error> {
if *position < 0 {
return Err(Error::Argument);
}
let start = usize::try_from(*position).map_err(|_| Error::Argument)?;
let mut writer = WireWriter::with_capacity(header_length(header.frequency))?;
encode_header(header, &mut writer)?;
let encoded = writer.into_inner();
let end = start.checked_add(encoded.len()).ok_or(Error::Argument)?;
if end > bytes.len() {
return Err(Error::IndexOutOfRange);
}
bytes[start..end].copy_from_slice(&encoded);
*position = i32::try_from(end).map_err(|_| Error::Argument)?;
Ok(())
}
pub(crate) fn acks_to_slice(
header: &Header,
bytes: &mut [u8],
position: &mut i32,
) -> Result<(), Error> {
if *position < 0 {
return Err(Error::Argument);
}
let start = usize::try_from(*position).map_err(|_| Error::Argument)?;
let mut writer = WireWriter::with_capacity(ack_length(header)?)?;
encode_acks(header, &mut writer)?;
let encoded = writer.into_inner();
let end = start.checked_add(encoded.len()).ok_or(Error::Argument)?;
if end > bytes.len() {
return Err(Error::IndexOutOfRange);
}
bytes[start..end].copy_from_slice(&encoded);
*position = i32::try_from(end).map_err(|_| Error::Argument)?;
Ok(())
}
pub(crate) fn zero_decode(
src: Option<&[u8]>,
source_length: i32,
dest: Option<&mut [u8]>,
) -> Result<i32, Error> {
let src = src.ok_or(Error::ArgumentNull)?;
let dest = dest.ok_or(Error::ArgumentNull)?;
if source_length < i32::try_from(HEADER_PREFIX_BYTES).unwrap_or(i32::MAX) {
return Err(Error::Argument);
}
let source_length = usize::try_from(source_length).map_err(|_| Error::Argument)?;
if source_length > src.len() {
return Err(Error::Argument);
}
if dest.len() < HEADER_PREFIX_BYTES {
return Err(Error::IndexOutOfRange);
}
dest[..HEADER_PREFIX_BYTES].copy_from_slice(&src[..HEADER_PREFIX_BYTES]);
let mut source = HEADER_PREFIX_BYTES;
let mut target = HEADER_PREFIX_BYTES;
while source < source_length {
if src[source] == 0 {
let trailing = source + 1 >= source_length;
let count = if trailing {
1
} else {
usize::from(src[source + 1])
};
let end = target.checked_add(count).ok_or(Error::IndexOutOfRange)?;
if end > dest.len() {
return Err(Error::IndexOutOfRange);
}
dest[target..end].fill(0);
target = end;
source += if trailing { 1 } else { 2 };
} else {
if target >= dest.len() {
return Err(Error::IndexOutOfRange);
}
dest[target] = src[source];
target += 1;
source += 1;
}
}
i32::try_from(target).map_err(|_| Error::Argument)
}
pub(crate) fn zero_encode(
src: Option<&[u8]>,
source_length: i32,
dest: Option<&mut [u8]>,
) -> Result<i32, Error> {
let src = src.ok_or(Error::ArgumentNull)?;
let dest = dest.ok_or(Error::ArgumentNull)?;
if source_length < i32::try_from(HEADER_PREFIX_BYTES).unwrap_or(i32::MAX) {
return Err(Error::Argument);
}
let source_length = usize::try_from(source_length).map_err(|_| Error::Argument)?;
if source_length > src.len() {
return Err(Error::Argument);
}
if dest.len() < HEADER_PREFIX_BYTES {
return Err(Error::IndexOutOfRange);
}
dest[..HEADER_PREFIX_BYTES].copy_from_slice(&src[..HEADER_PREFIX_BYTES]);
let body_end = if src[0] & Helpers::MSG_APPENDED_ACKS == 0 {
source_length
} else {
let count = usize::from(src[source_length - 1]);
let suffix = count
.checked_mul(4)
.and_then(|length| length.checked_add(1))
.ok_or(Error::Argument)?;
if suffix > source_length - HEADER_PREFIX_BYTES {
return Err(Error::Argument);
}
source_length - suffix
};
let mut source = HEADER_PREFIX_BYTES;
let mut target = HEADER_PREFIX_BYTES;
while source < body_end {
if src[source] != 0 {
if target >= dest.len() {
return Err(Error::IndexOutOfRange);
}
dest[target] = src[source];
target += 1;
source += 1;
continue;
}
let run_start = source;
while source < body_end && src[source] == 0 {
source += 1;
}
let mut remaining = source - run_start;
while remaining != 0 {
if target.checked_add(2).is_none_or(|end| end > dest.len()) {
return Err(Error::IndexOutOfRange);
}
let count = remaining.min(usize::from(u8::MAX));
dest[target] = 0;
dest[target + 1] = u8::try_from(count).map_err(|_| Error::Argument)?;
target += 2;
remaining -= count;
}
}
while source < source_length {
if target >= dest.len() {
return Err(Error::IndexOutOfRange);
}
dest[target] = src[source];
target += 1;
source += 1;
}
i32::try_from(target).map_err(|_| Error::Argument)
}
pub(crate) fn build_packet_from_bytes(
packet_buffer: &[u8],
packet_end: &mut i32,
zero_buffer: &mut [u8],
) -> Result<Packet, Error> {
let mut position = 0_i32;
let header = decode_header(packet_buffer, &mut position, packet_end)?;
let packet_type = crate::packet_catalog::packet_type(header.id, header.frequency);
let descriptor = crate::packet_catalog::descriptor_by_type(packet_type)
.ok_or_else(|| parse(usize::try_from(position).unwrap_or(0), "unknown packet ID"))?;
if header.zerocoded {
let decoded_length = zero_decode(
Some(packet_buffer),
packet_end.checked_add(1).ok_or(Error::Argument)?,
Some(zero_buffer),
)?;
*packet_end = decoded_length.checked_sub(1).ok_or(Error::Argument)?;
let decoded_length = usize::try_from(decoded_length).map_err(|_| Error::Argument)?;
crate::packet_catalog::validate_packet_payload(
packet_type,
clone_header(&header),
&zero_buffer[..decoded_length],
&mut position,
packet_end,
)?;
} else {
crate::packet_catalog::validate_packet_payload(
packet_type,
clone_header(&header),
packet_buffer,
&mut position,
packet_end,
)?;
}
Ok(Packet {
has_variable_blocks: descriptor.blocks.iter().any(|block| {
matches!(
block.repetition,
crate::packet_catalog::BlockRepetition::Variable
)
}),
header,
type_: packet_type,
})
}
#[cfg(test)]
mod tests {
use super::{WireReader, WireWriter};
#[test]
fn scalar_endianness_matches_the_packet_template_protocol() {
let mut writer = WireWriter::with_capacity(35).expect("writer");
writer.write_u8(0x81).unwrap();
writer.write_i8(-2).unwrap();
writer.write_u16_le(0x1234).unwrap();
writer.write_u16_be(0x5678).unwrap();
writer.write_i16_le(-0x1234).unwrap();
writer.write_u32_le(0x1122_3344).unwrap();
writer.write_i32_le(-0x1122_3344).unwrap();
writer.write_u64_le(0x0102_0304_0506_0708).unwrap();
writer.write_f32_le(1.5).unwrap();
writer.write_f64_le(-2.25).unwrap();
let bytes = writer.into_inner();
assert_eq!(
bytes,
[
0x81, 0xfe, 0x34, 0x12, 0x56, 0x78, 0xcc, 0xed, 0x44, 0x33, 0x22, 0x11, 0xbc, 0xcc,
0xdd, 0xee, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 0xc0, 0x3f, 0, 0, 0, 0, 0, 0, 2, 0xc0,
]
);
let mut reader = WireReader::new(&bytes, 0, bytes.len()).unwrap();
assert_eq!(reader.read_u8().unwrap(), 0x81);
assert_eq!(reader.read_i8().unwrap(), -2);
assert_eq!(reader.read_u16_le().unwrap(), 0x1234);
assert_eq!(reader.read_u16_be().unwrap(), 0x5678);
assert_eq!(reader.read_i16_le().unwrap(), -0x1234);
assert_eq!(reader.read_u32_le().unwrap(), 0x1122_3344);
assert_eq!(reader.read_i32_le().unwrap(), -0x1122_3344);
assert_eq!(reader.read_u64_le().unwrap(), 0x0102_0304_0506_0708);
assert_eq!(reader.read_f32_le().unwrap().to_bits(), 1.5_f32.to_bits());
assert_eq!(
reader.read_f64_le().unwrap().to_bits(),
(-2.25_f64).to_bits()
);
}
#[test]
fn fixed_and_variable_fields_enforce_reference_lengths() {
let mut writer = WireWriter::with_capacity(32).expect("writer");
writer.write_fixed(&[1, 2], 4).unwrap();
writer.write_fixed(&[3, 4, 5, 6], 2).unwrap();
writer.write_variable(&[7, 8, 9], 1).unwrap();
writer.write_variable(&[10, 11], 2).unwrap();
let bytes = writer.into_inner();
assert_eq!(bytes, [1, 2, 0, 0, 3, 4, 3, 7, 8, 9, 2, 0, 10, 11]);
let mut reader = WireReader::new(&bytes, 0, bytes.len()).unwrap();
assert_eq!(reader.read_bytes(4, "fixed").unwrap(), [1, 2, 0, 0]);
assert_eq!(reader.read_bytes(2, "fixed").unwrap(), [3, 4]);
assert_eq!(reader.read_variable(1).unwrap(), [7, 8, 9]);
assert_eq!(reader.read_variable(2).unwrap(), [10, 11]);
let mut oversized = WireWriter::with_capacity(300).unwrap();
assert!(oversized.write_variable(&[0; 256], 1).is_err());
}
}

View File

@@ -0,0 +1,178 @@
use libremetaverse::packets::{
AgentThrottlePacket, Header, Packet, PacketType, UseCircuitCodePacket,
};
use libremetaverse::{Error, Helpers, PacketFrequency};
use libremetaverse_types::UUID;
fn header(frequency: PacketFrequency, id: u16) -> Header {
Header {
ack_list: None,
appended_acks: false,
frequency,
id,
reliable: true,
resent: false,
sequence: 0x0102_0304,
zerocoded: false,
}
}
fn header_bytes(header: &Header) -> Vec<u8> {
let length = match header.frequency {
PacketFrequency::High => 7,
PacketFrequency::Medium => 8,
PacketFrequency::Low => 10,
};
let mut bytes = vec![0_u8; length];
let mut position = 0;
header
.to_bytes(&mut bytes, &mut position)
.expect("encode header");
assert_eq!(usize::try_from(position).unwrap(), bytes.len());
bytes
}
#[test]
fn header_frequencies_match_the_reference_wire_bytes() {
// Golden source: codegen/inputs/message_template.msg frequency rules and
// LibreMetaverse.Tests/PacketTests.cs::HeaderFlags (translated in
// tests/compat/tests/wire_semantics.rs::packet_header_flags).
assert_eq!(
header_bytes(&header(PacketFrequency::High, 0x2a)),
[0x40, 1, 2, 3, 4, 0, 0x2a]
);
assert_eq!(
header_bytes(&header(PacketFrequency::Medium, 0x2a)),
[0x40, 1, 2, 3, 4, 0, 0xff, 0x2a]
);
assert_eq!(
header_bytes(&header(PacketFrequency::Low, 0x1234)),
[0x40, 1, 2, 3, 4, 0, 0xff, 0xff, 0x12, 0x34]
);
}
#[test]
fn appended_acks_are_big_endian_and_removed_from_the_payload_boundary() {
let mut value = header(PacketFrequency::High, 0x2a);
value.appended_acks = true;
value.ack_list = Some(vec![0x0102_0304, 0xa0b0_c0d0]);
let mut bytes = header_bytes(&value);
let mut ack_bytes = [0_u8; 9];
let mut ack_position = 0;
value
.acks_to_bytes(&mut ack_bytes, &mut ack_position)
.expect("encode ACKs");
assert_eq!(ack_bytes, [1, 2, 3, 4, 0xa0, 0xb0, 0xc0, 0xd0, 2]);
bytes.extend_from_slice(&ack_bytes);
let mut position = 0;
let mut packet_end = i32::try_from(bytes.len()).unwrap() - 1;
let decoded = Header::build_header(bytes, &mut position, &mut packet_end).expect("decode");
assert!(decoded.appended_acks);
assert_eq!(decoded.ack_list, value.ack_list);
assert_eq!(position, 7);
assert_eq!(packet_end, 6);
}
#[test]
fn use_circuit_code_matches_the_reference_golden_packet() {
// Golden schema: codegen/inputs/message_template.msg::UseCircuitCode
// (Low 3, CircuitCode/U32 + SessionID/LLUUID + ID/LLUUID). Header behavior
// is linked to the translated HeaderFlags test above.
let session = (0_u8..16).collect::<Vec<_>>();
let agent = (16_u8..32).collect::<Vec<_>>();
let mut packet = UseCircuitCodePacket::new_with_constructor().expect("packet constructor");
packet.circuit_code.code = 0x1122_3344;
packet.circuit_code.session_id =
UUID::new_with_bytes_int32(session.clone(), 0).expect("session UUID");
packet.circuit_code.id = UUID::new_with_bytes_int32(agent.clone(), 0).expect("agent UUID");
let encoded = packet.to_bytes_with_method().expect("encode packet");
let mut expected = vec![
0x40, 0, 0, 0, 0, 0, 0xff, 0xff, 0, 3, 0x44, 0x33, 0x22, 0x11,
];
expected.extend_from_slice(&session);
expected.extend_from_slice(&agent);
assert_eq!(encoded, expected);
let mut position = 0;
let decoded =
UseCircuitCodePacket::new_with_bytes_int32(encoded.clone(), &mut position).expect("decode");
assert_eq!(usize::try_from(position).unwrap(), encoded.len());
assert_eq!(decoded.circuit_code.code, 0x1122_3344);
assert_eq!(
decoded.circuit_code.session_id.get_bytes().unwrap(),
session
);
assert_eq!(decoded.circuit_code.id.get_bytes().unwrap(), agent);
assert_eq!(decoded.to_bytes_with_method().unwrap(), encoded);
}
#[test]
fn malformed_and_unknown_packets_return_errors_without_panicking() {
let valid = [0x40, 0, 0, 0, 0, 0, 0xff, 0xff, 0, 3];
for length in 0..valid.len() {
let bytes = valid[..length].to_vec();
let outcome = std::panic::catch_unwind(|| {
let mut end = i32::try_from(bytes.len()).unwrap() - 1;
Packet::build_packet_with_bytes_int32_bytes(bytes.clone(), &mut end, vec![0; 64])
});
assert!(outcome.is_ok(), "truncation at {length} panicked");
assert!(
outcome.unwrap().is_err(),
"truncation at {length} was accepted"
);
}
let missing_payload = valid.to_vec();
let mut end = i32::try_from(missing_payload.len()).unwrap() - 1;
assert!(
Packet::build_packet_with_bytes_int32_bytes(missing_payload, &mut end, vec![0; 64])
.is_err()
);
let unknown = vec![0x40, 0, 0, 0, 0, 0, 0xfe];
let mut end = i32::try_from(unknown.len()).unwrap() - 1;
let Err(error) = Packet::build_packet_with_bytes_int32_bytes(unknown, &mut end, vec![0; 64])
else {
panic!("unknown packet ID was accepted");
};
assert!(matches!(error, Error::Parse { .. }));
}
#[test]
fn packet_factory_decodes_zerocoded_payloads() {
let mut packet = AgentThrottlePacket::new_with_constructor().expect("packet constructor");
packet.agent_data.circuit_code = 0x1122_3344;
packet.throttle.gen_counter = 0x5566_7788;
packet.throttle.throttles = vec![1, 0, 0, 2, 3];
let raw = packet.to_bytes_with_method().expect("raw packet");
let mut encoded = vec![0_u8; raw.len() * 2 + 2];
let encoded_length = Helpers::zero_encode(
Some(&raw),
i32::try_from(raw.len()).unwrap(),
Some(&mut encoded),
)
.expect("zero encode");
encoded.truncate(usize::try_from(encoded_length).unwrap());
let mut end = encoded_length - 1;
let built =
Packet::build_packet_with_bytes_int32_bytes(encoded, &mut end, vec![0_u8; raw.len()])
.expect("packet factory");
assert_eq!(built.type_, PacketType::AgentThrottle);
assert_eq!(end, i32::try_from(raw.len()).unwrap() - 1);
}
#[test]
fn packet_id_dispatch_covers_known_and_unknown_ids() {
assert_eq!(
Packet::get_type(3, PacketFrequency::Low).expect("packet type"),
PacketType::UseCircuitCode
);
assert_eq!(
Packet::get_type(u16::MAX, PacketFrequency::High).expect("unknown type"),
PacketType::Default
);
assert_eq!(Helpers::MSG_RELIABLE, 0x40);
}

View File

@@ -381,8 +381,8 @@ fn packet_to_bytes_multiple() {
)
.expect("Packet BuildPacket");
assert_eq!(built.type_, PacketType::DirPlacesReply);
end = packet_data.len() as i32 - 1;
let packet = DirPlacesReplyPacket::new_with_bytes_int32(packet_data, &mut end)
let mut start = 0;
let packet = DirPlacesReplyPacket::new_with_bytes_int32(packet_data, &mut start)
.expect("DirPlacesReplyPacket byte constructor");
assert_eq!(
packet

View File

@@ -843,6 +843,84 @@ fn field_default(kind: FieldKind) -> &'static str {
}
}
fn field_sample(field: &PacketField) -> String {
match field.kind {
FieldKind::Bool => "true".to_owned(),
FieldKind::U8 => "0xa5_u8".to_owned(),
FieldKind::U16 | FieldKind::IpPort => "0x1234_u16".to_owned(),
FieldKind::U32 | FieldKind::IpAddr => "0x1122_3344_u32".to_owned(),
FieldKind::U64 => "0x0102_0304_0506_0708_u64".to_owned(),
FieldKind::S8 => "-37_i8".to_owned(),
FieldKind::S16 => "-0x1234_i16".to_owned(),
FieldKind::S32 => "-0x1122_3344_i32".to_owned(),
FieldKind::F32 => "1.5_f32".to_owned(),
FieldKind::F64 => "-2.25_f64".to_owned(),
FieldKind::LlUuid => "libremetaverse_types::UUID::new_with_bytes_int32((0_u8..16).collect(), 0).expect(\"sample UUID\")".to_owned(),
FieldKind::LlVector3 => "libremetaverse_types::Vector3 { x: 1.25, y: -2.5, z: 3.75 }".to_owned(),
FieldKind::LlVector3d => "libremetaverse_types::Vector3d { x: 1.25, y: -2.5, z: 3.75 }".to_owned(),
FieldKind::LlVector4 => "libremetaverse_types::Vector4 { x: 1.25, y: -2.5, z: 3.75, w: -4.5 }".to_owned(),
FieldKind::LlQuaternion => "libremetaverse_types::Quaternion::identity()".to_owned(),
FieldKind::Variable => "vec![0_u8, 1, 2, 0, 0xff]".to_owned(),
FieldKind::Fixed => format!("vec![0xa5_u8; {}]", field.count),
}
}
fn field_decode_statement(field: &PacketField) -> String {
let name = rust_snake(&field.name);
let expression = match field.kind {
FieldKind::Bool => "reader.read_u8()? != 0".to_owned(),
FieldKind::U8 => "reader.read_u8()?".to_owned(),
FieldKind::U16 => "reader.read_u16_le()?".to_owned(),
FieldKind::U32 | FieldKind::IpAddr => "reader.read_u32_le()?".to_owned(),
FieldKind::U64 => "reader.read_u64_le()?".to_owned(),
FieldKind::S8 => "reader.read_i8()?".to_owned(),
FieldKind::S16 => "reader.read_i16_le()?".to_owned(),
FieldKind::S32 => "reader.read_i32_le()?".to_owned(),
FieldKind::F32 => "reader.read_f32_le()?".to_owned(),
FieldKind::F64 => "reader.read_f64_le()?".to_owned(),
FieldKind::LlUuid => "reader.read_uuid()?".to_owned(),
FieldKind::LlVector3 => "reader.read_vector3()?".to_owned(),
FieldKind::LlVector3d => "reader.read_vector3d()?".to_owned(),
FieldKind::LlVector4 => "reader.read_vector4()?".to_owned(),
FieldKind::LlQuaternion => "reader.read_quaternion()?".to_owned(),
FieldKind::IpPort => "reader.read_u16_be()?".to_owned(),
FieldKind::Variable => format!("reader.read_variable({})?", field.count),
FieldKind::Fixed => format!(
"reader.read_bytes({}, \"truncated fixed packet field\")?",
field.count
),
};
format!("self.{name} = {expression};")
}
fn field_encode_statement(field: &PacketField) -> String {
let name = rust_snake(&field.name);
match field.kind {
FieldKind::Bool => format!("writer.write_u8(u8::from(self.{name}))?;"),
FieldKind::U8 => format!("writer.write_u8(self.{name})?;"),
FieldKind::U16 => format!("writer.write_u16_le(self.{name})?;"),
FieldKind::U32 | FieldKind::IpAddr => {
format!("writer.write_u32_le(self.{name})?;")
}
FieldKind::U64 => format!("writer.write_u64_le(self.{name})?;"),
FieldKind::S8 => format!("writer.write_i8(self.{name})?;"),
FieldKind::S16 => format!("writer.write_i16_le(self.{name})?;"),
FieldKind::S32 => format!("writer.write_i32_le(self.{name})?;"),
FieldKind::F32 => format!("writer.write_f32_le(self.{name})?;"),
FieldKind::F64 => format!("writer.write_f64_le(self.{name})?;"),
FieldKind::LlUuid => format!("writer.write_uuid(&self.{name})?;"),
FieldKind::LlVector3 => format!("writer.write_vector3(&self.{name})?;"),
FieldKind::LlVector3d => format!("writer.write_vector3d(&self.{name})?;"),
FieldKind::LlVector4 => format!("writer.write_vector4(&self.{name})?;"),
FieldKind::LlQuaternion => format!("writer.write_quaternion(&self.{name})?;"),
FieldKind::IpPort => format!("writer.write_u16_be(self.{name})?;"),
FieldKind::Variable => {
format!("writer.write_variable(&self.{name}, {})?;", field.count)
}
FieldKind::Fixed => format!("writer.write_fixed(&self.{name}, {})?;", field.count),
}
}
fn ordered_packets(protocol: &PacketProtocol) -> Vec<&PacketDefinition> {
[
PacketFrequency::Low,
@@ -958,6 +1036,7 @@ fn append_descriptors(body: &mut String, packets: &[&PacketDefinition]) {
body.push_str("];\n\n");
}
#[allow(clippy::too_many_lines)]
fn append_dispatch(body: &mut String, packets: &[&PacketDefinition]) {
body.push_str("#[must_use]\n#[allow(clippy::too_many_lines)]\npub fn packet_type(id: u16, frequency: crate::PacketFrequency) -> PacketType {\n match (frequency, id) {\n");
for packet in packets {
@@ -1001,8 +1080,102 @@ fn append_dispatch(body: &mut String, packets: &[&PacketDefinition]) {
crate::packets::Header { ack_list: None, appended_acks: false, frequency, id, reliable: true, resent: false, sequence: 0, zerocoded }\n\
}\n\n\
fn usize_to_i32(value: usize) -> i32 { i32::try_from(value).unwrap_or(i32::MAX) }\n\n\
pub(crate) trait GeneratedBlock { fn new_generated() -> Self; fn generated_length(&self) -> i32; }\n\n\
pub(crate) trait GeneratedPacket { fn new_generated() -> Self; fn generated_length(&self) -> i32; const USES_BUFFER_POOLING: bool; }\n\n",
pub(crate) trait GeneratedBlock: Sized {\n\
fn new_generated() -> Self;\n\
fn generated_length(&self) -> i32;\n\
fn decode_payload(&mut self, reader: &mut crate::packet_wire::WireReader<'_>) -> Result<(), crate::Error>;\n\
fn encode_payload(&self, writer: &mut crate::packet_wire::WireWriter) -> Result<(), crate::Error>;\n\
fn new_from_bytes(bytes: &[u8], position: &mut i32) -> Result<Self, crate::Error> {\n\
let mut value = Self::new_generated();\n\
Self::decode_from_bytes(&mut value, bytes, position)?;\n\
Ok(value)\n\
}\n\
fn decode_from_bytes(&mut self, bytes: &[u8], position: &mut i32) -> Result<(), crate::Error> {\n\
if *position < 0 { return Err(crate::Error::Argument); }\n\
let start = usize::try_from(*position).map_err(|_| crate::Error::Argument)?;\n\
let mut reader = crate::packet_wire::WireReader::new(bytes, start, bytes.len())?;\n\
self.decode_payload(&mut reader)?;\n\
reader.commit_position(position)\n\
}\n\
fn encode_to_slice(&self, bytes: &mut [u8], position: &mut i32) -> Result<(), crate::Error> {\n\
if *position < 0 { return Err(crate::Error::Argument); }\n\
let start = usize::try_from(*position).map_err(|_| crate::Error::Argument)?;\n\
let capacity = usize::try_from(self.generated_length()).map_err(|_| crate::Error::Argument)?;\n\
let mut writer = crate::packet_wire::WireWriter::with_capacity(capacity)?;\n\
self.encode_payload(&mut writer)?;\n\
let encoded = writer.into_inner();\n\
let end = start.checked_add(encoded.len()).ok_or(crate::Error::Argument)?;\n\
if end > bytes.len() { return Err(crate::Error::IndexOutOfRange); }\n\
bytes[start..end].copy_from_slice(&encoded);\n\
*position = i32::try_from(end).map_err(|_| crate::Error::Argument)?;\n\
Ok(())\n\
}\n\
}\n\n\
pub(crate) trait GeneratedPacket: Sized {\n\
const USES_BUFFER_POOLING: bool;\n\
fn new_generated() -> Self;\n\
fn generated_length(&self) -> i32;\n\
fn generated_header(&self) -> crate::packets::Header;\n\
fn store_header(&mut self, header: crate::packets::Header);\n\
fn decode_payload(&mut self, reader: &mut crate::packet_wire::WireReader<'_>) -> Result<(), crate::Error>;\n\
fn encode_payload(&self, writer: &mut crate::packet_wire::WireWriter) -> Result<(), crate::Error>;\n\
fn encode_multiple(&self) -> Result<Vec<Vec<u8>>, crate::Error>;\n\
fn encode_packet(&self) -> Result<Vec<u8>, crate::Error> {\n\
let header = self.generated_header();\n\
let payload = usize::try_from(self.generated_length()).map_err(|_| crate::Error::Argument)?;\n\
let capacity = payload.checked_add(crate::packet_wire::ack_length(&header)?).ok_or(crate::Error::Argument)?;\n\
let mut writer = crate::packet_wire::WireWriter::with_capacity(capacity)?;\n\
crate::packet_wire::encode_header(&header, &mut writer)?;\n\
self.encode_payload(&mut writer)?;\n\
crate::packet_wire::encode_acks(&header, &mut writer)?;\n\
Ok(writer.into_inner())\n\
}\n\
fn new_from_bytes(bytes: &[u8], position: &mut i32) -> Result<Self, crate::Error> {\n\
let mut value = Self::new_generated();\n\
let mut packet_end = i32::try_from(bytes.len()).map_err(|_| crate::Error::Argument)?.checked_sub(1).ok_or(crate::Error::Argument)?;\n\
Self::decode_from_bytes(&mut value, bytes, position, &mut packet_end, None)?;\n\
Ok(value)\n\
}\n\
fn new_from_header(header: crate::packets::Header, bytes: &[u8], position: &mut i32) -> Result<Self, crate::Error> {\n\
let mut value = Self::new_generated();\n\
let mut packet_end = i32::try_from(bytes.len()).map_err(|_| crate::Error::Argument)?.checked_sub(1).ok_or(crate::Error::Argument)?;\n\
Self::decode_from_header(&mut value, header, bytes, position, &mut packet_end)?;\n\
Ok(value)\n\
}\n\
fn decode_from_header(&mut self, header: crate::packets::Header, bytes: &[u8], position: &mut i32, packet_end: &mut i32) -> Result<(), crate::Error> {\n\
if *position < 0 || *packet_end < 0 { return Err(crate::Error::Argument); }\n\
let start = usize::try_from(*position).map_err(|_| crate::Error::Argument)?;\n\
let end = usize::try_from(packet_end.checked_add(1).ok_or(crate::Error::Argument)?).map_err(|_| crate::Error::Argument)?;\n\
let mut reader = crate::packet_wire::WireReader::new(bytes, start, end)?;\n\
self.decode_payload(&mut reader)?;\n\
reader.finish(position)?;\n\
self.store_header(header);\n\
Ok(())\n\
}\n\
fn decode_from_bytes(&mut self, bytes: &[u8], position: &mut i32, packet_end: &mut i32, zero_buffer: Option<&mut [u8]>) -> Result<(), crate::Error> {\n\
let header = crate::packet_wire::decode_header(bytes, position, packet_end)?;\n\
if header.zerocoded && let Some(buffer) = zero_buffer {\n\
let decoded_length = crate::packet_wire::zero_decode(Some(bytes), packet_end.checked_add(1).ok_or(crate::Error::Argument)?, Some(buffer))?;\n\
*packet_end = decoded_length.checked_sub(1).ok_or(crate::Error::Argument)?;\n\
let end = usize::try_from(decoded_length).map_err(|_| crate::Error::Argument)?;\n\
return self.decode_from_header(header, &buffer[..end], position, packet_end);\n\
}\n\
self.decode_from_header(header, bytes, position, packet_end)\n\
}\n\
}\n\n",
);
body.push_str(
"#[allow(clippy::too_many_lines)]\npub(crate) fn validate_packet_payload(\n packet_type: PacketType,\n header: crate::packets::Header,\n bytes: &[u8],\n position: &mut i32,\n packet_end: &mut i32,\n) -> Result<(), crate::Error> {\n match packet_type {\n",
);
for packet in packets {
let _ = writeln!(
body,
" PacketType::{} => {{ let mut packet = <crate::packets::{}Packet as GeneratedPacket>::new_generated(); GeneratedPacket::decode_from_header(&mut packet, header, bytes, position, packet_end) }},",
packet.name, packet.name
);
}
body.push_str(
" PacketType::Default => Err(crate::Error::InvalidOperation),\n }\n}\n\n",
);
}
@@ -1043,7 +1216,17 @@ fn append_block_impls(body: &mut String, packets: &[&PacketDefinition]) {
}
body.push_str(" length\n");
}
body.push_str(" }\n}\n\n");
body.push_str(
" }\n\n fn decode_payload(&mut self, reader: &mut crate::packet_wire::WireReader<'_>) -> Result<(), crate::Error> {\n",
);
for field in &block.fields {
let _ = writeln!(body, " {}", field_decode_statement(field));
}
body.push_str(" Ok(())\n }\n\n fn encode_payload(&self, writer: &mut crate::packet_wire::WireWriter) -> Result<(), crate::Error> {\n");
for field in &block.fields {
let _ = writeln!(body, " {}", field_encode_statement(field));
}
body.push_str(" Ok(())\n }\n}\n\n");
}
}
}
@@ -1052,6 +1235,135 @@ fn packet_has_composed_base(name: &str) -> bool {
matches!(name, "DirPlacesReply" | "TestMessage")
}
fn packet_can_split(packet: &PacketDefinition) -> bool {
let mut saw_variable = false;
let mut has_variable = false;
for block in &packet.blocks {
if block.repetition == BlockRepetition::Variable {
saw_variable = true;
has_variable = true;
} else if saw_variable {
return false;
}
}
has_variable
}
#[allow(clippy::too_many_lines)]
fn append_packet_encode_multiple(body: &mut String, packet: &PacketDefinition) {
if !packet_can_split(packet) {
body.push_str(
" fn encode_multiple(&self) -> Result<Vec<Vec<u8>>, crate::Error> {\n Ok(vec![self.encode_packet()?])\n }\n",
);
return;
}
let variable_blocks = packet
.blocks
.iter()
.filter(|block| block.repetition == BlockRepetition::Variable)
.collect::<Vec<_>>();
body.push_str(
" #[allow(clippy::too_many_lines)]\n fn encode_multiple(&self) -> Result<Vec<Vec<u8>>, crate::Error> {\n let header = self.generated_header();\n let mut packets = Vec::new();\n",
);
for block in &variable_blocks {
let _ = writeln!(
body,
" let mut {}_start = 0_usize;",
rust_snake(&block.name)
);
}
body.push_str(" loop {\n let first_packet = packets.is_empty();\n let mut packet_header = crate::packet_wire::clone_header(&header);\n if !first_packet { packet_header.appended_acks = false; packet_header.ack_list = None; }\n let acks_length = crate::packet_wire::ack_length(&packet_header)?;\n let mut fixed_length = crate::packet_wire::header_length(packet_header.frequency);
");
for block in &packet.blocks {
let field_name = rust_snake(&block.name);
match block.repetition {
BlockRepetition::Single => {
let _ = writeln!(
body,
" fixed_length = fixed_length.checked_add(usize::try_from(GeneratedBlock::generated_length(&self.{field_name})).map_err(|_| crate::Error::Argument)?).ok_or(crate::Error::Argument)?;"
);
}
BlockRepetition::Multiple(_) => {
let _ = writeln!(
body,
" for block in &self.{field_name} {{ fixed_length = fixed_length.checked_add(usize::try_from(GeneratedBlock::generated_length(block)).map_err(|_| crate::Error::Argument)?).ok_or(crate::Error::Argument)?; }}"
);
}
BlockRepetition::Variable => {}
}
}
let _ = writeln!(
body,
" fixed_length = fixed_length.checked_add({}).ok_or(crate::Error::Argument)?;",
variable_blocks.len()
);
body.push_str(" let mut variable_length = 0_usize;\n");
for block in &variable_blocks {
let field_name = rust_snake(&block.name);
let _ = writeln!(body, " let mut {field_name}_count = 0_usize;");
let _ = writeln!(
body,
" while {field_name}_start + {field_name}_count < self.{field_name}.len() && {field_name}_count < usize::from(u8::MAX) {{"
);
let _ = writeln!(
body,
" let block_length = usize::try_from(GeneratedBlock::generated_length(&self.{field_name}[{field_name}_start + {field_name}_count])).map_err(|_| crate::Error::Argument)?;"
);
let _ = writeln!(
body,
" if fixed_length.saturating_add(variable_length).saturating_add(block_length).saturating_add(acks_length) <= usize::try_from(crate::packets::Packet::MTU).map_err(|_| crate::Error::Argument)? || {field_name}_count == 0 {{ variable_length = variable_length.checked_add(block_length).ok_or(crate::Error::Argument)?; {field_name}_count += 1; }} else {{ break; }}"
);
body.push_str(" }\n");
}
body.push_str(" let capacity = fixed_length.checked_add(variable_length).and_then(|length| length.checked_add(acks_length)).ok_or(crate::Error::Argument)?;\n let mut writer = crate::packet_wire::WireWriter::with_capacity(capacity)?;\n crate::packet_wire::encode_header(&packet_header, &mut writer)?;\n");
for block in &packet.blocks {
let field_name = rust_snake(&block.name);
match block.repetition {
BlockRepetition::Single => {
let _ = writeln!(
body,
" GeneratedBlock::encode_payload(&self.{field_name}, &mut writer)?;"
);
}
BlockRepetition::Multiple(count) => {
let _ = writeln!(
body,
" if self.{field_name}.len() != {count} {{ return Err(crate::Error::Argument); }}"
);
let _ = writeln!(
body,
" for block in &self.{field_name} {{ GeneratedBlock::encode_payload(block, &mut writer)?; }}"
);
}
BlockRepetition::Variable => {
let _ = writeln!(
body,
" writer.write_u8(u8::try_from({field_name}_count).map_err(|_| crate::Error::Argument)?)?;"
);
let _ = writeln!(
body,
" for block in &self.{field_name}[{field_name}_start..{field_name}_start + {field_name}_count] {{ GeneratedBlock::encode_payload(block, &mut writer)?; }}"
);
let _ = writeln!(
body,
" {field_name}_start += {field_name}_count;"
);
}
}
}
body.push_str(" crate::packet_wire::encode_acks(&packet_header, &mut writer)?;\n packets.push(writer.into_inner());\n if ");
for (index, block) in variable_blocks.iter().enumerate() {
if index != 0 {
body.push_str(" && ");
}
let field_name = rust_snake(&block.name);
let _ = write!(body, "{field_name}_start >= self.{field_name}.len()");
}
body.push_str(" { break; }\n }\n Ok(packets)\n }\n");
}
#[allow(clippy::too_many_lines)]
fn append_packet_impls(body: &mut String, packets: &[&PacketDefinition]) {
for packet in packets {
let type_name = format!("{}Packet", packet.name);
@@ -1105,14 +1417,8 @@ fn append_packet_impls(body: &mut String, packets: &[&PacketDefinition]) {
body.push_str(" }\n }\n\n fn generated_length(&self) -> i32 {\n");
if packet.blocks.is_empty() {
let _ = writeln!(body, " {base_length}_i32");
body.push_str(" }\n}\n\n");
continue;
}
let _ = writeln!(
body,
" let mut length = {}_i32;",
base_length + variable_count
);
} else {
let _ = writeln!(body, " let mut length = {base_length}_i32;");
for block in &packet.blocks {
let field_name = rust_snake(&block.name);
match block.repetition {
@@ -1137,8 +1443,170 @@ fn append_packet_impls(body: &mut String, packets: &[&PacketDefinition]) {
}
}
}
body.push_str(" length\n }\n}\n\n");
body.push_str(" length\n");
}
body.push_str(" }\n\n fn generated_header(&self) -> crate::packets::Header {\n");
if packet_has_composed_base(&packet.name) {
body.push_str(" crate::packet_wire::clone_header(&self.base.header)\n");
} else {
let _ = writeln!(
body,
" new_header(crate::PacketFrequency::{}, {}, {})",
frequency_name(packet.frequency),
packet.id,
packet.zerocoded
);
}
body.push_str(
" }\n\n fn store_header(&mut self, header: crate::packets::Header) {\n",
);
if packet_has_composed_base(&packet.name) {
body.push_str(" self.base.header = header;\n");
} else {
body.push_str(" let _ = header;\n");
}
body.push_str(" }\n\n fn decode_payload(&mut self, reader: &mut crate::packet_wire::WireReader<'_>) -> Result<(), crate::Error> {\n");
for block in &packet.blocks {
let field_name = rust_snake(&block.name);
let block_type = format!("crate::packets::{}Packet{}Block", packet.name, block.name);
match block.repetition {
BlockRepetition::Single => {
let _ = writeln!(
body,
" GeneratedBlock::decode_payload(&mut self.{field_name}, reader)?;"
);
}
BlockRepetition::Variable => {
let _ = writeln!(
body,
" let {field_name}_count = usize::from(reader.read_u8()?);"
);
let _ = writeln!(body, " let mut {field_name} = Vec::new();");
let _ = writeln!(
body,
" {field_name}.try_reserve_exact({field_name}_count).map_err(|_| crate::Error::InvalidOperation)?;"
);
let _ = writeln!(
body,
" for _ in 0..{field_name}_count {{ let mut block = <{block_type} as GeneratedBlock>::new_generated(); GeneratedBlock::decode_payload(&mut block, reader)?; {field_name}.push(block); }}"
);
let _ = writeln!(body, " self.{field_name} = {field_name};");
}
BlockRepetition::Multiple(count) => {
let _ = writeln!(body, " let mut {field_name} = Vec::new();");
let _ = writeln!(
body,
" {field_name}.try_reserve_exact({count}).map_err(|_| crate::Error::InvalidOperation)?;"
);
let _ = writeln!(
body,
" for _ in 0..{count} {{ let mut block = <{block_type} as GeneratedBlock>::new_generated(); GeneratedBlock::decode_payload(&mut block, reader)?; {field_name}.push(block); }}"
);
let _ = writeln!(body, " self.{field_name} = {field_name};");
}
}
}
if packet.blocks.is_empty() {
body.push_str(" let _ = reader;\n");
}
body.push_str(" Ok(())\n }\n\n fn encode_payload(&self, writer: &mut crate::packet_wire::WireWriter) -> Result<(), crate::Error> {\n");
for block in &packet.blocks {
let field_name = rust_snake(&block.name);
match block.repetition {
BlockRepetition::Single => {
let _ = writeln!(
body,
" GeneratedBlock::encode_payload(&self.{field_name}, writer)?;"
);
}
BlockRepetition::Variable => {
let _ = writeln!(
body,
" writer.write_u8(u8::try_from(self.{field_name}.len()).map_err(|_| crate::Error::Argument)?)?;"
);
let _ = writeln!(
body,
" for block in &self.{field_name} {{ GeneratedBlock::encode_payload(block, writer)?; }}"
);
}
BlockRepetition::Multiple(count) => {
let _ = writeln!(
body,
" if self.{field_name}.len() != {count} {{ return Err(crate::Error::Argument); }}"
);
let _ = writeln!(
body,
" for block in &self.{field_name} {{ GeneratedBlock::encode_payload(block, writer)?; }}"
);
}
}
}
if packet.blocks.is_empty() {
body.push_str(" let _ = writer;\n");
}
body.push_str(" Ok(())\n }\n\n");
append_packet_encode_multiple(body, packet);
body.push_str("}\n\n");
}
}
fn append_packet_codec_tests(body: &mut String, packets: &[&PacketDefinition]) {
body.push_str(
"#[cfg(test)]\nmod generated_codec_tests {\n use super::{GeneratedBlock, GeneratedPacket};\n\n fn assert_round_trip<T: GeneratedPacket>(packet: &T, name: &str) {\n let expected = packet.encode_packet().unwrap_or_else(|error| panic!(\"{name}: encode failed: {error}\"));\n let (wire, mut zero_buffer) = if expected[0] & crate::Helpers::MSG_ZEROCODED != 0 {\n let mut encoded = vec![0_u8; expected.len().saturating_mul(2).saturating_add(2)];\n let encoded_length = crate::packet_wire::zero_encode(\n Some(&expected),\n i32::try_from(expected.len()).expect(\"packet length fits i32\"),\n Some(&mut encoded),\n )\n .unwrap_or_else(|error| panic!(\"{name}: zero encode failed: {error}\"));\n encoded.truncate(usize::try_from(encoded_length).expect(\"encoded length fits usize\"));\n (encoded, vec![0_u8; expected.len()])\n } else {\n (expected.clone(), Vec::new())\n };\n let mut decoded = T::new_generated();\n let mut position = 0_i32;\n let mut packet_end = i32::try_from(wire.len()).expect(\"wire length fits i32\") - 1;\n let buffer = (!zero_buffer.is_empty()).then_some(zero_buffer.as_mut_slice());\n decoded\n .decode_from_bytes(&wire, &mut position, &mut packet_end, buffer)\n .unwrap_or_else(|error| panic!(\"{name}: decode failed: {error}\"));\n let actual = decoded.encode_packet().unwrap_or_else(|error| panic!(\"{name}: re-encode failed: {error}\"));\n assert_eq!(actual, expected, \"{name}: round-trip bytes differ\");\n assert_eq!(position, i32::try_from(expected.len()).expect(\"packet length fits i32\"), \"{name}: final position\");\n\n for truncated_length in 0..wire.len() {\n let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n let mut candidate = T::new_generated();\n let mut truncated_position = 0_i32;\n let mut truncated_end = i32::try_from(truncated_length).expect(\"truncated length fits i32\") - 1;\n let mut scratch = vec![0_u8; expected.len()];\n let truncated_buffer = (!zero_buffer.is_empty()).then_some(scratch.as_mut_slice());\n candidate.decode_from_bytes(\n &wire[..truncated_length],\n &mut truncated_position,\n &mut truncated_end,\n truncated_buffer,\n )\n }));\n assert!(outcome.is_ok(), \"{name}: truncation at {truncated_length} panicked\");\n }\n }\n\n #[test]\n #[allow(clippy::too_many_lines)]\n fn every_generated_packet_round_trips_all_fields() {\n",
);
for packet in packets {
let mutates_packet = packet
.blocks
.iter()
.any(|block| block.repetition == BlockRepetition::Variable || !block.fields.is_empty());
let _ = writeln!(
body,
" let {}packet = <crate::packets::{}Packet as GeneratedPacket>::new_generated();",
if mutates_packet { "mut " } else { "" },
packet.name,
);
for block in &packet.blocks {
let block_name = rust_snake(&block.name);
match block.repetition {
BlockRepetition::Single => {
for field in &block.fields {
let field_name = rust_snake(&field.name);
let sample = field_sample(field);
let _ =
writeln!(body, " packet.{block_name}.{field_name} = {sample};");
}
}
BlockRepetition::Multiple(_) => {
let _ = writeln!(body, " for block in &mut packet.{block_name} {{");
for field in &block.fields {
let field_name = rust_snake(&field.name);
let sample = field_sample(field);
let _ = writeln!(body, " block.{field_name} = {sample};");
}
body.push_str(" }\n");
}
BlockRepetition::Variable => {
let block_type = format!("{}Packet{}Block", packet.name, block.name);
let _ = writeln!(
body,
" let mut block = <crate::packets::{block_type} as GeneratedBlock>::new_generated();"
);
for field in &block.fields {
let field_name = rust_snake(&field.name);
let sample = field_sample(field);
let _ = writeln!(body, " block.{field_name} = {sample};");
}
let _ = writeln!(body, " packet.{block_name}.push(block);");
}
}
}
let _ = writeln!(
body,
" assert_round_trip(&packet, {:?});",
packet.name
);
}
body.push_str(" }\n}\n");
}
fn csharp_field_type(kind: FieldKind) -> &'static str {
@@ -1401,6 +1869,7 @@ pub fn packet_catalog_bytes(root: &Path) -> Result<Vec<u8>, String> {
append_dispatch(&mut body, &packets);
append_block_impls(&mut body, &packets);
append_packet_impls(&mut body, &packets);
append_packet_codec_tests(&mut body, &packets);
let syntax = syn::parse_file(&body)
.map_err(|error| format!("generated packet Rust is invalid: {error}"))?;
let formatted = prettyplease::unparse(&syntax);

View File

@@ -38,6 +38,7 @@ TARGETS = {
# implementations. The generated module keeps catalog markers and re-exports
# the hand-written type so coverage remains deterministic.
NATIVE_TYPES = {
"T:LibreMetaverse.BitPack": "crate::bit_pack::BitPack",
"T:LibreMetaverse.Packets.PacketType": "crate::packet_catalog::PacketType",
"T:LibreMetaverse.PrimMesher.Coord": "crate::prim_mesher::Coord",
"T:LibreMetaverse.PrimMesher.Face": "crate::prim_mesher::Face",
@@ -296,6 +297,52 @@ COMPOSED_BASE_TYPES = {
"T:LibreMetaverse.Packets.TestMessagePacket",
"T:LibreMetaverse.Rendering.Joint",
}
NATIVE_MESSAGE_CODEC_TYPES = {
"T:LibreMetaverse.Messages.Linden.AgentGroupDataUpdateMessage",
"T:LibreMetaverse.Messages.Linden.AgentGroupDataUpdateMessage.GroupData",
"T:LibreMetaverse.Messages.Linden.AgentGroupDataUpdateMessage.NewGroupData",
"T:LibreMetaverse.Messages.Linden.ChatSessionAcceptInvitation",
"T:LibreMetaverse.Messages.Linden.ChatSessionRequestBlock",
"T:LibreMetaverse.Messages.Linden.ChatSessionRequestMuteUpdate",
"T:LibreMetaverse.Messages.Linden.ChatSessionRequestStartConference",
"T:LibreMetaverse.Messages.Linden.ChatterBoxInvitationMessage",
"T:LibreMetaverse.Messages.Linden.ChatterBoxSessionAgentListUpdatesMessage",
"T:LibreMetaverse.Messages.Linden.ChatterBoxSessionAgentListUpdatesMessage.AgentUpdatesBlock",
"T:LibreMetaverse.Messages.Linden.ChatterBoxSessionStartReplyMessage",
"T:LibreMetaverse.Messages.Linden.ChatterboxSessionEventReplyMessage",
"T:LibreMetaverse.Messages.Linden.CopyInventoryFromNotecardMessage",
"T:LibreMetaverse.Messages.Linden.DirLandReplyMessage",
"T:LibreMetaverse.Messages.Linden.DirLandReplyMessage.QueryReply",
"T:LibreMetaverse.Messages.Linden.EnableSimulatorMessage",
"T:LibreMetaverse.Messages.Linden.EnableSimulatorMessage.SimulatorInfoBlock",
"T:LibreMetaverse.Messages.Linden.EstablishAgentCommunicationMessage",
"T:LibreMetaverse.Messages.Linden.LandStatReplyMessage",
"T:LibreMetaverse.Messages.Linden.LandStatReplyMessage.ReportDataBlock",
"T:LibreMetaverse.Messages.Linden.MapLayerMessageBase",
"T:LibreMetaverse.Messages.Linden.MapLayerReplyVariant",
"T:LibreMetaverse.Messages.Linden.MapLayerReplyVariant.LayerData",
"T:LibreMetaverse.Messages.Linden.ParcelObjectOwnersReplyMessage",
"T:LibreMetaverse.Messages.Linden.ParcelObjectOwnersReplyMessage.PrimOwner",
"T:LibreMetaverse.Messages.Linden.ParcelPropertiesMessage",
"T:LibreMetaverse.Messages.Linden.ParcelPropertiesUpdateMessage",
"T:LibreMetaverse.Messages.Linden.ParcelVoiceInfoRequestMessage",
"T:LibreMetaverse.Messages.Linden.PlacesReplyMessage",
"T:LibreMetaverse.Messages.Linden.PlacesReplyMessage.QueryData",
"T:LibreMetaverse.Messages.Linden.ProvisionVoiceAccountRequestMessage",
"T:LibreMetaverse.Messages.Linden.RemoteParcelRequestReply",
"T:LibreMetaverse.Messages.Linden.RequiredVoiceVersionMessage",
"T:LibreMetaverse.Messages.Linden.ScriptRunningReplyMessage",
"T:LibreMetaverse.Messages.Linden.SendPostcardMessage",
"T:LibreMetaverse.Messages.Linden.TeleportFailedMessage",
"T:LibreMetaverse.Messages.Linden.TeleportFinishMessage",
"T:LibreMetaverse.Messages.Linden.UpdateAgentInformationMessage",
"T:LibreMetaverse.Messages.Linden.UpdateAgentInventoryRequestMessage",
"T:LibreMetaverse.Messages.Linden.UpdateAgentLanguageMessage",
"T:LibreMetaverse.Messages.Linden.UpdateScriptAgentRequestMessage",
"T:LibreMetaverse.Messages.Linden.UpdateScriptTaskUpdateMessage",
"T:LibreMetaverse.Messages.Linden.ViewerStatsMessage",
}
OPEN_ENUM_IDS = {"T:LibreMetaverse.BakeType", "T:LibreMetaverse.HoleType"}
TRAIT_SUPERTRAITS = {
"T:LibreMetaverse.IBakingTextureProvider": "std::any::Any",
@@ -746,10 +793,68 @@ def native_member_body(member_id: str) -> str | None:
body = NATIVE_MEMBER_BODIES.get(member_id)
if body is not None:
return body
message_owner = "T:" + member_id[2:].split(".#ctor", 1)[0] if member_id.startswith("M:") and ".#ctor" in member_id else None
if message_owner in NATIVE_MESSAGE_CODEC_TYPES:
return "Ok(<Self as crate::message_codec::GeneratedMessage>::new_generated())"
message_owner = "T:" + member_id[2:].rsplit(".Deserialize", 1)[0] if member_id.startswith("M:") and ".Deserialize(" in member_id else None
if message_owner in NATIVE_MESSAGE_CODEC_TYPES:
return "<Self as crate::message_codec::GeneratedMessage>::deserialize_generated(self, &map)"
message_owner = "T:" + member_id[2:].rsplit(".Serialize", 1)[0] if member_id.startswith("M:") and member_id.endswith(".Serialize") else None
if message_owner in NATIVE_MESSAGE_CODEC_TYPES:
return "<Self as crate::message_codec::GeneratedMessage>::serialize_generated(self)"
if re.fullmatch(r"M:LibreMetaverse\.Packets\.[^.]+Packet\.#ctor", member_id):
return "Ok(<Self as crate::packet_catalog::GeneratedPacket>::new_generated())"
if re.fullmatch(
r"M:LibreMetaverse\.Packets\.[^.]+Packet\.#ctor\(LibreMetaverse\.Packets\.Header,System\.Byte\[\],System\.Int32@\)",
member_id,
):
return "<Self as crate::packet_catalog::GeneratedPacket>::new_from_header(head, &bytes, i)"
if re.fullmatch(
r"M:LibreMetaverse\.Packets\.[^.]+Packet\.#ctor\(System\.Byte\[\],System\.Int32@\)",
member_id,
):
return "<Self as crate::packet_catalog::GeneratedPacket>::new_from_bytes(&bytes, i)"
if re.fullmatch(r"M:LibreMetaverse\.Packets\.[^.]+Packet\.[^.]+Block\.#ctor", member_id):
return "Ok(<Self as crate::packet_catalog::GeneratedBlock>::new_generated())"
if re.fullmatch(
r"M:LibreMetaverse\.Packets\.[^.]+Packet\.[^.]+Block\.#ctor\(System\.Byte\[\],System\.Int32@\)",
member_id,
):
return "<Self as crate::packet_catalog::GeneratedBlock>::new_from_bytes(&bytes, i)"
if re.fullmatch(
r"M:LibreMetaverse\.Packets\.[^.]+Packet\.[^.]+Block\.FromBytes\(System\.Byte\[\],System\.Int32@\)",
member_id,
):
return "<Self as crate::packet_catalog::GeneratedBlock>::decode_from_bytes(self, &bytes, i)"
if re.fullmatch(
r"M:LibreMetaverse\.Packets\.[^.]+Packet\.[^.]+Block\.ToBytes\(System\.Byte\[\],System\.Int32@\)",
member_id,
):
return "<Self as crate::packet_catalog::GeneratedBlock>::encode_to_slice(self, bytes, i)"
if re.fullmatch(
r"M:LibreMetaverse\.Packets\.[^.]+Packet\.FromBytes\(LibreMetaverse\.Packets\.Header,System\.Byte\[\],System\.Int32@,System\.Int32@\)",
member_id,
):
return "<Self as crate::packet_catalog::GeneratedPacket>::decode_from_header(self, header, &bytes, i, packet_end)"
if re.fullmatch(
r"M:LibreMetaverse\.Packets\.[^.]+Packet\.FromBytes\(System\.Byte\[\],System\.Int32@,System\.Int32@,System\.Byte\[\]\)",
member_id,
):
return "<Self as crate::packet_catalog::GeneratedPacket>::decode_from_bytes(self, &bytes, i, packet_end, zero_buffer)"
if re.fullmatch(r"M:LibreMetaverse\.Packets\.[^.]+Packet\.ToBytes", member_id):
return "<Self as crate::packet_catalog::GeneratedPacket>::encode_packet(self)"
if re.fullmatch(
r"M:LibreMetaverse\.Packets\.[^.]+Packet\.ToBytes\(LibreMetaverse\.Interfaces\.IByteBufferPool,System\.Int32@\)",
member_id,
):
return "{ let _ = pool; let bytes = <Self as crate::packet_catalog::GeneratedPacket>::encode_packet(self)?; *size = i32::try_from(bytes.len()).map_err(|_| crate::Error::Argument)?; Ok(bytes) }"
if re.fullmatch(r"M:LibreMetaverse\.Packets\.[^.]+Packet\.ToBytesMultiple", member_id):
return "<Self as crate::packet_catalog::GeneratedPacket>::encode_multiple(self)"
if re.fullmatch(
r"M:LibreMetaverse\.Packets\.[^.]+Packet\.ToBytesMultiple\(LibreMetaverse\.Interfaces\.IByteBufferPool,System\.Int32\[\]@\)",
member_id,
):
return "{ let _ = pool; let packets = <Self as crate::packet_catalog::GeneratedPacket>::encode_multiple(self)?; *sizes = packets.iter().map(|packet| i32::try_from(packet.len()).unwrap_or(i32::MAX)).collect(); Ok(packets) }"
if re.fullmatch(r"P:LibreMetaverse\.Packets\.[^.]+Packet\.Length", member_id):
return "<Self as crate::packet_catalog::GeneratedPacket>::generated_length(self)"
if re.fullmatch(r"P:LibreMetaverse\.Packets\.[^.]+Packet\.UsesBufferPooling", member_id):
@@ -760,6 +865,20 @@ def native_member_body(member_id: str) -> str | None:
return "Ok(crate::packet_catalog::packet_type(id, frequency))"
if member_id == "M:LibreMetaverse.Packets.Packet.BuildPacket(LibreMetaverse.Packets.PacketType)":
return "crate::packet_catalog::build_packet(type_)"
if member_id == "M:LibreMetaverse.Packets.Packet.BuildPacket(System.Byte[],System.Int32@,System.Byte[])":
return "{ let mut zero_buffer = zero_buffer; crate::packet_wire::build_packet_from_bytes(&packet_buffer, packet_end, &mut zero_buffer) }"
if member_id == "M:LibreMetaverse.Packets.Header.BuildHeader(System.Byte[],System.Int32@,System.Int32@)":
return "crate::packet_wire::decode_header(&bytes, pos, packet_end)"
if member_id == "M:LibreMetaverse.Packets.Header.FromBytes(System.Byte[],System.Int32@,System.Int32@)":
return "{ *self = crate::packet_wire::decode_header(&bytes, pos, packet_end)?; Ok(()) }"
if member_id == "M:LibreMetaverse.Packets.Header.ToBytes(System.Byte[],System.Int32@)":
return "crate::packet_wire::header_to_slice(self, bytes, i)"
if member_id == "M:LibreMetaverse.Packets.Header.AcksToBytes(System.Byte[],System.Int32@)":
return "crate::packet_wire::acks_to_slice(self, bytes, i)"
if member_id == "M:LibreMetaverse.Helpers.ZeroDecode(System.Byte[],System.Int32,System.Byte[])":
return "crate::packet_wire::zero_decode(src, srclen, dest)"
if member_id == "M:LibreMetaverse.Helpers.ZeroEncode(System.Byte[],System.Int32,System.Byte[])":
return "crate::packet_wire::zero_encode(src, srclen, dest)"
return None

View File

@@ -985,6 +985,7 @@ def validate_generated_shims() -> None:
"crate::json_codec::",
"crate::notation::",
"crate::packet_catalog::",
"crate::packet_wire::",
"crate::protobuf::",
"crate::xml_codec::",
)
@@ -1156,9 +1157,8 @@ def method_receiver(owner: dict, item: dict) -> tuple[str, str]:
return "&self", "shared_self"
if owner["doc_id"] == "T:LibreMetaverse.BitPack" and item["name"].startswith(("Pack", "Unpack")):
return "&mut self", "mutable_self"
if (
owner["doc_id"] == "T:LibreMetaverse.Packets.InventoryDescendentsPacket"
and item["name"].startswith("FromBytes")
if owner["doc_id"].startswith("T:LibreMetaverse.Packets.") and item["name"].startswith(
"FromBytes"
):
return "&mut self", "mutable_self"
observers = ("Get", "TryGet", "Find", "Contains", "Has", "Is", "Can", "To", "Equals", "Compare")
@@ -1215,6 +1215,17 @@ def member_signature(mapper: Mapper, owner: dict, item: dict, rust_name: str) ->
return signature, ownership, "sync", "Result<_, crate::Error>", "method"
generics = generic_names(item, owner)
mapped_parameters = [map_parameter(mapper, parameter, generics) for parameter in item.get("parameters", [])]
packet_owner = owner["doc_id"].startswith("T:LibreMetaverse.Packets.")
if (
packet_owner
and item["name"] == "FromBytes"
and len(mapped_parameters) == 4
and item["doc_id"].endswith(",System.Byte[])")
):
mapped_parameters[3] = ("optional_mutable_borrow", "zero_buffer: Option<&mut [u8]>")
if packet_owner and item["name"] in {"AcksToBytes", "ToBytes"} and mapped_parameters:
if owner["doc_id"] == "T:LibreMetaverse.Packets.Header" or owner["doc_id"].endswith("Block"):
mapped_parameters[0] = ("mutable_borrow", "bytes: &mut [u8]")
own = [value[0] for value in mapped_parameters]
parameters = ", ".join(value[1] for value in mapped_parameters)
kind = item["kind"]