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
601 lines
20 KiB
Rust
601 lines
20 KiB
Rust
use std::collections::HashMap;
|
|
use std::fs::{File, OpenOptions};
|
|
use std::io::{BufRead, BufReader, BufWriter, Read, Write};
|
|
|
|
use libremetaverse_types::compat::Object;
|
|
|
|
use crate::{Error, FieldType, GridClient, PacketFrequency};
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct MapField {
|
|
pub count: i32,
|
|
pub keyword_position: i32,
|
|
pub name: String,
|
|
pub type_: FieldType,
|
|
}
|
|
|
|
impl Default for MapField {
|
|
fn default() -> Self {
|
|
Self {
|
|
count: 0,
|
|
keyword_position: 0,
|
|
name: String::new(),
|
|
type_: FieldType::U8,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl MapField {
|
|
pub fn new() -> Result<Self, Error> {
|
|
Ok(Self::default())
|
|
}
|
|
|
|
pub fn compare_to(&self, obj: Option<Object>) -> Result<i32, Error> {
|
|
let Some(obj) = obj else {
|
|
return Ok(1);
|
|
};
|
|
let other = obj.downcast_ref::<Self>().ok_or(Error::Argument)?;
|
|
Ok(self.keyword_position.cmp(&other.keyword_position) as i32)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct MapBlock {
|
|
pub count: i32,
|
|
pub fields: Vec<MapField>,
|
|
pub keyword_position: i32,
|
|
pub name: String,
|
|
}
|
|
|
|
impl MapBlock {
|
|
pub fn new() -> Result<Self, Error> {
|
|
Ok(Self::default())
|
|
}
|
|
|
|
pub fn compare_to(&self, obj: Option<Object>) -> Result<i32, Error> {
|
|
let Some(obj) = obj else {
|
|
return Ok(1);
|
|
};
|
|
let other = obj.downcast_ref::<Self>().ok_or(Error::Argument)?;
|
|
Ok(self.keyword_position.cmp(&other.keyword_position) as i32)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct MapPacket {
|
|
pub blocks: Vec<MapBlock>,
|
|
pub encoded: bool,
|
|
pub frequency: PacketFrequency,
|
|
pub id: u16,
|
|
pub name: String,
|
|
pub trusted: bool,
|
|
}
|
|
|
|
impl Default for MapPacket {
|
|
fn default() -> Self {
|
|
Self {
|
|
blocks: Vec::new(),
|
|
encoded: false,
|
|
frequency: PacketFrequency::Low,
|
|
id: 0,
|
|
name: String::new(),
|
|
trusted: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl MapPacket {
|
|
pub fn new() -> Result<Self, Error> {
|
|
Ok(Self::default())
|
|
}
|
|
}
|
|
|
|
pub struct ProtocolManager {
|
|
pub high_maps: Vec<MapPacket>,
|
|
pub keyword_positions: HashMap<String, i32>,
|
|
pub low_maps: Vec<MapPacket>,
|
|
pub medium_maps: Vec<MapPacket>,
|
|
pub type_sizes: HashMap<FieldType, i32>,
|
|
}
|
|
|
|
impl ProtocolManager {
|
|
pub fn new(map_file: String, client: GridClient) -> Result<Self, Error> {
|
|
// The C# instance retains this reference only to enrich parse-error logging.
|
|
// Rust reports parsing failures to the caller, so no client ownership is needed.
|
|
drop(client);
|
|
let mut manager = Self::empty();
|
|
manager.load_map_file(&map_file)?;
|
|
Ok(manager)
|
|
}
|
|
|
|
fn empty() -> Self {
|
|
let type_sizes = HashMap::from([
|
|
(FieldType::U8, 1),
|
|
(FieldType::U16, 2),
|
|
(FieldType::U32, 4),
|
|
(FieldType::U64, 8),
|
|
(FieldType::S8, 1),
|
|
(FieldType::S16, 2),
|
|
(FieldType::S32, 4),
|
|
(FieldType::F32, 4),
|
|
(FieldType::F64, 8),
|
|
(FieldType::UUID, 16),
|
|
(FieldType::BOOL, 1),
|
|
(FieldType::Vector3, 12),
|
|
(FieldType::Vector3d, 24),
|
|
(FieldType::Vector4, 16),
|
|
(FieldType::Quaternion, 16),
|
|
(FieldType::IPADDR, 4),
|
|
(FieldType::IPPORT, 2),
|
|
(FieldType::Variable, -1),
|
|
(FieldType::Fixed, -2),
|
|
]);
|
|
Self {
|
|
high_maps: vec![MapPacket::default(); 256],
|
|
keyword_positions: HashMap::new(),
|
|
low_maps: vec![MapPacket::default(); 65_536],
|
|
medium_maps: vec![MapPacket::default(); 256],
|
|
type_sizes,
|
|
}
|
|
}
|
|
|
|
pub fn command_with_bytes(&self, data: Vec<u8>) -> Result<Option<MapPacket>, Error> {
|
|
let Some(&frequency_byte) = data.get(4) else {
|
|
return Ok(None);
|
|
};
|
|
let result = if frequency_byte != 0xff {
|
|
self.command_with_u_int16_packet_frequency(
|
|
u16::from(frequency_byte),
|
|
PacketFrequency::High,
|
|
)
|
|
} else if data.get(5) != Some(&0xff) {
|
|
let Some(&command) = data.get(5) else {
|
|
return Ok(None);
|
|
};
|
|
self.command_with_u_int16_packet_frequency(u16::from(command), PacketFrequency::Medium)
|
|
} else {
|
|
let (Some(&high), Some(&low)) = (data.get(6), data.get(7)) else {
|
|
return Ok(None);
|
|
};
|
|
self.command_with_u_int16_packet_frequency(
|
|
u16::from_be_bytes([high, low]),
|
|
PacketFrequency::Low,
|
|
)
|
|
};
|
|
match result {
|
|
Ok(packet) => Ok(Some(packet)),
|
|
Err(Error::IndexOutOfRange | Error::InvalidOperation) => Ok(None),
|
|
Err(error) => Err(error),
|
|
}
|
|
}
|
|
|
|
pub fn command_with_string(&self, command: String) -> Result<MapPacket, Error> {
|
|
self.high_maps
|
|
.iter()
|
|
.chain(&self.medium_maps)
|
|
.chain(&self.low_maps)
|
|
.find(|packet| !packet.name.is_empty() && packet.name == command)
|
|
.cloned()
|
|
.ok_or(Error::InvalidOperation)
|
|
}
|
|
|
|
pub fn command_with_u_int16_packet_frequency(
|
|
&self,
|
|
command: u16,
|
|
frequency: PacketFrequency,
|
|
) -> Result<MapPacket, Error> {
|
|
let maps = match frequency {
|
|
PacketFrequency::High => &self.high_maps,
|
|
PacketFrequency::Medium => &self.medium_maps,
|
|
PacketFrequency::Low => &self.low_maps,
|
|
};
|
|
let packet = maps
|
|
.get(usize::from(command))
|
|
.ok_or(Error::IndexOutOfRange)?;
|
|
if packet.name.is_empty() {
|
|
return Err(Error::InvalidOperation);
|
|
}
|
|
Ok(packet.clone())
|
|
}
|
|
|
|
pub fn decode_map_file(map_file: String, output_file: String) -> Result<(), Error> {
|
|
let input = File::open(map_file).map_err(|_| Error::InvalidOperation)?;
|
|
let output = OpenOptions::new()
|
|
.write(true)
|
|
.create_new(true)
|
|
.open(output_file)
|
|
.map_err(|_| Error::InvalidOperation)?;
|
|
Self::decode(BufReader::new(input), BufWriter::new(output))
|
|
}
|
|
|
|
fn decode(mut input: impl Read, mut output: impl Write) -> Result<(), Error> {
|
|
let mut key = 0_u8;
|
|
let mut buffer = [0_u8; 2048];
|
|
loop {
|
|
let count = input
|
|
.read(&mut buffer)
|
|
.map_err(|_| Error::InvalidOperation)?;
|
|
if count == 0 {
|
|
break;
|
|
}
|
|
for byte in &mut buffer[..count] {
|
|
*byte ^= key;
|
|
key = key.wrapping_add(43);
|
|
}
|
|
output
|
|
.write_all(&buffer[..count])
|
|
.map_err(|_| Error::InvalidOperation)?;
|
|
}
|
|
output.flush().map_err(|_| Error::InvalidOperation)
|
|
}
|
|
|
|
pub fn print_map(&self) -> Result<(), Error> {
|
|
self.print_one_map(&self.low_maps, "Low ");
|
|
self.print_one_map(&self.medium_maps, "Medium");
|
|
self.print_one_map(&self.high_maps, "High ");
|
|
Ok(())
|
|
}
|
|
|
|
fn print_one_map(&self, maps: &[MapPacket], frequency: &str) {
|
|
for (index, packet) in maps
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, packet)| !packet.name.is_empty())
|
|
{
|
|
println!(
|
|
"{frequency} {index:5} - {} - {} - {}",
|
|
packet.name,
|
|
if packet.trusted {
|
|
"Trusted"
|
|
} else {
|
|
"Untrusted"
|
|
},
|
|
if packet.encoded {
|
|
"Unencoded"
|
|
} else {
|
|
"Zerocoded"
|
|
}
|
|
);
|
|
for block in &packet.blocks {
|
|
if block.count == -1 {
|
|
println!("\t{:4} {} (Variable)", block.keyword_position, block.name);
|
|
} else {
|
|
println!(
|
|
"\t{:4} {} ({})",
|
|
block.keyword_position, block.name, block.count
|
|
);
|
|
}
|
|
for field in &block.fields {
|
|
println!(
|
|
"\t\t{:4} {} ({:?} / {})",
|
|
field.keyword_position, field.name, field.type_, field.count
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn load_map_file(&mut self, map_file: &str) -> Result<(), Error> {
|
|
let input = File::open(map_file).map_err(|_| Error::InvalidOperation)?;
|
|
self.parse(BufReader::new(input))
|
|
}
|
|
|
|
fn parse(&mut self, input: impl BufRead) -> Result<(), Error> {
|
|
let mut low = 1_u16;
|
|
let mut medium = 1_u16;
|
|
let mut high = 1_u16;
|
|
let mut in_packet = false;
|
|
let mut in_block = false;
|
|
let mut current_packet: Option<(PacketFrequency, usize)> = None;
|
|
let mut current_block: Option<MapBlock> = None;
|
|
|
|
for (line_number, line) in input.lines().enumerate() {
|
|
let line = line.map_err(|_| Error::Parse {
|
|
position: line_number,
|
|
context: "could not read protocol map line",
|
|
})?;
|
|
let trimmed = line.split_whitespace().collect::<Vec<_>>().join(" ");
|
|
if !in_packet {
|
|
if trimmed == "{" {
|
|
in_packet = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if !in_block {
|
|
if trimmed == "{" {
|
|
if current_packet.is_none() {
|
|
return Err(Self::parse_error(line_number, "block has no packet header"));
|
|
}
|
|
in_block = true;
|
|
} else if trimmed == "}" {
|
|
if let Some((frequency, index)) = current_packet.take() {
|
|
self.packet_mut(frequency, index)?
|
|
.blocks
|
|
.sort_by_key(|block| block.keyword_position);
|
|
}
|
|
in_packet = false;
|
|
} else if !trimmed.is_empty() && !trimmed.starts_with("//") {
|
|
current_packet = Some(self.parse_packet_header(
|
|
&trimmed,
|
|
line_number,
|
|
&mut low,
|
|
&mut medium,
|
|
&mut high,
|
|
)?);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if trimmed.starts_with('{') {
|
|
let tokens: Vec<_> = trimmed.split_whitespace().collect();
|
|
if tokens.len() < 4 {
|
|
return Err(Self::parse_error(
|
|
line_number,
|
|
"malformed field declaration",
|
|
));
|
|
}
|
|
let type_ = Self::parse_field_type(tokens[2])
|
|
.ok_or_else(|| Self::parse_error(line_number, "unknown field type"))?;
|
|
let count = if tokens[3] == "}" {
|
|
1
|
|
} else {
|
|
tokens[3]
|
|
.parse()
|
|
.map_err(|_| Self::parse_error(line_number, "invalid field count"))?
|
|
};
|
|
let keyword_position = self.keyword_position(tokens[1])?;
|
|
let block = current_block
|
|
.as_mut()
|
|
.ok_or_else(|| Self::parse_error(line_number, "field has no block header"))?;
|
|
block.fields.push(MapField {
|
|
count,
|
|
keyword_position,
|
|
name: tokens[1].to_owned(),
|
|
type_,
|
|
});
|
|
} else if trimmed == "}" {
|
|
let mut block = current_block.take().ok_or_else(|| {
|
|
Self::parse_error(line_number, "block terminator has no header")
|
|
})?;
|
|
block.fields.sort_by_key(|field| field.keyword_position);
|
|
let (frequency, index) = current_packet
|
|
.ok_or_else(|| Self::parse_error(line_number, "block has no packet"))?;
|
|
self.packet_mut(frequency, index)?.blocks.push(block);
|
|
in_block = false;
|
|
} else if !trimmed.is_empty() && !trimmed.starts_with("//") {
|
|
if current_block.is_some() {
|
|
return Err(Self::parse_error(line_number, "block header is incomplete"));
|
|
}
|
|
let tokens: Vec<_> = trimmed.split_whitespace().collect();
|
|
if tokens.len() < 2 {
|
|
return Err(Self::parse_error(line_number, "malformed block header"));
|
|
}
|
|
let count = match tokens[1] {
|
|
"Single" => 1,
|
|
"Variable" => -1,
|
|
"Multiple" => tokens
|
|
.get(2)
|
|
.ok_or_else(|| Self::parse_error(line_number, "missing block count"))?
|
|
.parse()
|
|
.map_err(|_| Self::parse_error(line_number, "invalid block count"))?,
|
|
_ => return Err(Self::parse_error(line_number, "unknown block frequency")),
|
|
};
|
|
current_block = Some(MapBlock {
|
|
count,
|
|
fields: Vec::new(),
|
|
keyword_position: self.keyword_position(tokens[0])?,
|
|
name: tokens[0].to_owned(),
|
|
});
|
|
}
|
|
}
|
|
|
|
if in_packet || in_block || current_block.is_some() {
|
|
return Err(Self::parse_error(
|
|
0,
|
|
"unterminated protocol map declaration",
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn parse_packet_header(
|
|
&mut self,
|
|
line: &str,
|
|
position: usize,
|
|
low: &mut u16,
|
|
medium: &mut u16,
|
|
high: &mut u16,
|
|
) -> Result<(PacketFrequency, usize), Error> {
|
|
let tokens: Vec<_> = line.split_whitespace().collect();
|
|
if tokens.len() < 4 {
|
|
return Err(Self::parse_error(position, "malformed packet header"));
|
|
}
|
|
self.keyword_position(tokens[0])?;
|
|
let (frequency, id, trusted_index, encoded_index) = match tokens[1] {
|
|
"Fixed" => {
|
|
if tokens.len() < 5 {
|
|
return Err(Self::parse_error(position, "malformed fixed packet header"));
|
|
}
|
|
let raw = tokens[2].strip_prefix("0x").unwrap_or(tokens[2]);
|
|
let raw = u32::from_str_radix(raw, 16)
|
|
.map_err(|_| Self::parse_error(position, "invalid fixed packet id"))?;
|
|
(PacketFrequency::Low, (raw ^ 0xffff_0000) as u16, 3, 4)
|
|
}
|
|
"Low" => {
|
|
let id = *low;
|
|
*low = low.checked_add(1).ok_or(Error::IndexOutOfRange)?;
|
|
(PacketFrequency::Low, id, 2, 3)
|
|
}
|
|
"Medium" => {
|
|
let id = *medium;
|
|
*medium = medium.checked_add(1).ok_or(Error::IndexOutOfRange)?;
|
|
(PacketFrequency::Medium, id, 2, 3)
|
|
}
|
|
"High" => {
|
|
let id = *high;
|
|
*high = high.checked_add(1).ok_or(Error::IndexOutOfRange)?;
|
|
(PacketFrequency::High, id, 2, 3)
|
|
}
|
|
_ => return Err(Self::parse_error(position, "unknown packet frequency")),
|
|
};
|
|
let packet = MapPacket {
|
|
blocks: Vec::new(),
|
|
encoded: tokens.get(encoded_index) == Some(&"Zerocoded"),
|
|
frequency,
|
|
id,
|
|
name: tokens[0].to_owned(),
|
|
trusted: tokens.get(trusted_index) == Some(&"Trusted"),
|
|
};
|
|
let index = usize::from(id);
|
|
*self.packet_mut(frequency, index)? = packet;
|
|
Ok((frequency, index))
|
|
}
|
|
|
|
fn packet_mut(
|
|
&mut self,
|
|
frequency: PacketFrequency,
|
|
index: usize,
|
|
) -> Result<&mut MapPacket, Error> {
|
|
match frequency {
|
|
PacketFrequency::High => self.high_maps.get_mut(index),
|
|
PacketFrequency::Medium => self.medium_maps.get_mut(index),
|
|
PacketFrequency::Low => self.low_maps.get_mut(index),
|
|
}
|
|
.ok_or(Error::IndexOutOfRange)
|
|
}
|
|
|
|
fn keyword_position(&mut self, keyword: &str) -> Result<i32, Error> {
|
|
if let Some(position) = self.keyword_positions.get(keyword) {
|
|
return Ok(*position);
|
|
}
|
|
let mut hash = keyword
|
|
.chars()
|
|
.skip(1)
|
|
.fold(0_i32, |hash, character| {
|
|
hash.wrapping_add(character as i32).wrapping_mul(2)
|
|
})
|
|
.wrapping_mul(2)
|
|
& 0x1fff;
|
|
let start = hash;
|
|
while self
|
|
.keyword_positions
|
|
.values()
|
|
.any(|position| *position == hash)
|
|
{
|
|
hash = (hash + 1) & 0x1fff;
|
|
if hash == start {
|
|
return Err(Error::InvalidOperation);
|
|
}
|
|
}
|
|
self.keyword_positions.insert(keyword.to_owned(), hash);
|
|
Ok(hash)
|
|
}
|
|
|
|
fn parse_field_type(value: &str) -> Option<FieldType> {
|
|
Some(match value.to_ascii_lowercase().as_str() {
|
|
"u8" => FieldType::U8,
|
|
"u16" => FieldType::U16,
|
|
"u32" => FieldType::U32,
|
|
"u64" => FieldType::U64,
|
|
"s8" => FieldType::S8,
|
|
"s16" => FieldType::S16,
|
|
"s32" => FieldType::S32,
|
|
"f32" => FieldType::F32,
|
|
"f64" => FieldType::F64,
|
|
"uuid" => FieldType::UUID,
|
|
"bool" => FieldType::BOOL,
|
|
"vector3" => FieldType::Vector3,
|
|
"vector3d" => FieldType::Vector3d,
|
|
"vector4" => FieldType::Vector4,
|
|
"quaternion" => FieldType::Quaternion,
|
|
"ipaddr" => FieldType::IPADDR,
|
|
"ipport" => FieldType::IPPORT,
|
|
"variable" => FieldType::Variable,
|
|
"fixed" => FieldType::Fixed,
|
|
"single" => FieldType::Single,
|
|
"multiple" => FieldType::Multiple,
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
const fn parse_error(position: usize, context: &'static str) -> Error {
|
|
Error::Parse { position, context }
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::io::Cursor;
|
|
|
|
use super::*;
|
|
|
|
const MAP: &str = r#"
|
|
// version header
|
|
{
|
|
TestHigh High Trusted Zerocoded
|
|
{
|
|
AgentData Single
|
|
{ AgentID UUID }
|
|
{ SessionID UUID }
|
|
}
|
|
}
|
|
{
|
|
TestLow Fixed 0xFFFF00F2 Untrusted Unencoded
|
|
{
|
|
Data Variable
|
|
{ Value U32 }
|
|
}
|
|
}
|
|
"#;
|
|
|
|
#[test]
|
|
fn parses_and_resolves_protocol_commands() {
|
|
let mut manager = ProtocolManager::empty();
|
|
manager.parse(Cursor::new(MAP)).unwrap();
|
|
|
|
let high = manager.command_with_string("TestHigh".to_owned()).unwrap();
|
|
assert_eq!(high.frequency, PacketFrequency::High);
|
|
assert_eq!(high.id, 1);
|
|
assert!(high.trusted);
|
|
assert!(high.encoded);
|
|
assert_eq!(high.blocks[0].fields.len(), 2);
|
|
|
|
let low = manager
|
|
.command_with_bytes(vec![0, 0, 0, 0, 0xff, 0xff, 0, 0xf2])
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(low.name, "TestLow");
|
|
assert_eq!(low.blocks[0].count, -1);
|
|
assert_eq!(manager.type_sizes[&FieldType::Vector3d], 24);
|
|
}
|
|
|
|
#[test]
|
|
fn xor_decoder_tracks_key_across_chunks() {
|
|
let input: Vec<_> = (0_u16..4097).map(|value| value as u8).collect();
|
|
let mut encoded = Vec::new();
|
|
ProtocolManager::decode(Cursor::new(&input), &mut encoded).unwrap();
|
|
let mut decoded = Vec::new();
|
|
ProtocolManager::decode(Cursor::new(encoded), &mut decoded).unwrap();
|
|
assert_eq!(decoded, input);
|
|
}
|
|
|
|
#[test]
|
|
fn malformed_short_packet_headers_do_not_panic() {
|
|
let manager = ProtocolManager::empty();
|
|
assert!(manager.command_with_bytes(vec![0; 5]).unwrap().is_none());
|
|
assert!(
|
|
manager
|
|
.command_with_bytes(vec![0, 0, 0, 0, 0xff])
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
assert!(
|
|
manager
|
|
.command_with_bytes(vec![0, 0, 0, 0, 0xff, 0xff, 0])
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
}
|
|
}
|