//! Native implementations of the pinned static helper surface. use crate::{Error, Primitive, Simulator}; use flate2::Compression; use flate2::read::ZlibDecoder; use flate2::write::ZlibEncoder; use libremetaverse_structured_data::{OSD, OSDParser}; use libremetaverse_types::UUID; use libremetaverse_types::compat::{ExternalError, Object, ReadWrite}; use std::collections::HashMap; use std::io::{Read, Write}; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::path::PathBuf; const MAX_DECOMPRESSED_OSD: u64 = 128 * 1024 * 1024; pub struct Helpers; impl Helpers { pub const MSG_APPENDED_ACKS: u8 = 0x10; pub const MSG_RESENT: u8 = 0x20; pub const MSG_RELIABLE: u8 = 0x40; pub const MSG_ZEROCODED: u8 = 0x80; pub fn copy_stream( mut input: Box, mut output: Box, ) -> Result<(), Error> { std::io::copy(&mut input, &mut output).map_err(|_| Error::InvalidOperation)?; output.flush().map_err(|_| Error::InvalidOperation) } pub fn decompress_osd(mesh_bytes: Vec) -> Result { Self::decode_zlib(&mesh_bytes) } pub fn float_to_terse_string(val: f32) -> Result { if val == 0.0 { return Ok(".00".to_owned()); } if val.is_nan() { return Ok("NaN".to_owned()); } if val == f32::INFINITY { return Ok("Infinity".to_owned()); } if val == f32::NEG_INFINITY { return Ok("-Infinity".to_owned()); } let mut value = format!("{val:.2}"); while value.ends_with('0') { value.pop(); } if value.ends_with('.') { value.pop(); } else if value.starts_with("-0") { value.remove(1); } else if value.starts_with('0') { value.remove(0); } Ok(value) } pub fn get_resource_stream_with_string( resource_name: String, ) -> Result>, Error> { Self::get_resource_stream_with_string_string(resource_name, "openmetaverse_data".to_owned()) } pub fn get_resource_stream_with_string_string( resource_name: String, search_path: String, ) -> Result>, Error> { let base = std::env::current_exe() .ok() .and_then(|path| path.parent().map(std::path::Path::to_path_buf)) .unwrap_or_else(|| PathBuf::from(".")); let filename = base.join(search_path).join(resource_name); match std::fs::OpenOptions::new().read(true).open(filename) { Ok(file) => Ok(Some(Box::new(file))), Err(_) => Ok(None), } } pub fn global_pos_to_region_handle( global_x: f32, global_y: f32, local_x: &mut f32, local_y: &mut f32, ) -> Result { let x = (global_x as u32 / Simulator::DEFAULT_REGION_SIZE_X) * Simulator::DEFAULT_REGION_SIZE_X; let y = (global_y as u32 / Simulator::DEFAULT_REGION_SIZE_Y) * Simulator::DEFAULT_REGION_SIZE_Y; *local_x = global_x - x as f32; *local_y = global_y - y as f32; Ok((u64::from(x) << 32) | u64::from(y)) } #[allow(clippy::too_many_arguments)] pub fn inventory_crc( creation_date: i32, sale_type: u8, inv_type: i8, type_: i8, mut asset_id: UUID, mut group_id: UUID, sale_price: i32, mut owner_id: UUID, mut creator_id: UUID, mut item_id: UUID, mut folder_id: UUID, everyone_mask: u32, flags: u32, next_owner_mask: u32, group_mask: u32, owner_mask: u32, ) -> Result { let values = [ asset_id.crc()?, folder_id.crc()?, item_id.crc()?, creator_id.crc()?, owner_id.crc()?, group_id.crc()?, owner_mask, next_owner_mask, everyone_mask, group_mask, flags, inv_type as u32, type_ as u32, creation_date as u32, sale_price as u32, u32::from(sale_type).wrapping_mul(0x0707_3096), ]; Ok(values.into_iter().fold(0_u32, u32::wrapping_add)) } pub fn osd_to_prim_list(osd: OSD) -> Result, Error> { let OSD::Map(map) = osd else { return Err(Error::Argument); }; map.into_iter() .map(|(local_id, value)| { let mut primitive = Primitive::from_osd(value)?; primitive.local_id = local_id.parse().map_err(|_| Error::Argument)?; Ok(primitive) }) .collect() } pub fn prim_list_to_osd(prims: Vec) -> Result { let mut map = HashMap::with_capacity(prims.len()); for primitive in prims { map.insert(primitive.local_id.to_string(), primitive.get_osd()?); } Ok(OSD::Map(map)) } pub fn safe_action( action: Box, action_name: Option, logger: Option) + Send + Sync>>, ) -> Result<(), Error> { if let Err(payload) = catch_unwind(AssertUnwindSafe(action)) { if let Some(logger) = logger { let label = action_name.filter(|name| !name.is_empty()); let message = label.map_or_else( || "Error executing action".to_owned(), |name| format!("Error executing {name}"), ); let detail = payload.downcast_ref::<&str>().map_or_else( || { payload .downcast_ref::() .cloned() .unwrap_or_else(|| "panic".to_owned()) }, |value| (*value).to_owned(), ); logger(message, Some(ExternalError(detail))); } } Ok(()) } pub fn split_by( str: String, chunk_length: i32, ) -> Result>, Error> { if str.is_empty() || chunk_length < 1 { return Err(Error::Argument); } let chunk_length = usize::try_from(chunk_length).map_err(|_| Error::Argument)?; let units: Vec = str.encode_utf16().collect(); let chunks: Vec = units .chunks(chunk_length) .map(String::from_utf16_lossy) .collect(); Ok(Box::new(chunks.into_iter())) } pub fn struct_to_string(value: Object) -> Result { if let Object::Map(fields) = value { let mut fields: Vec<_> = fields.into_iter().collect(); fields.sort_unstable_by(|left, right| left.0.cmp(&right.0)); Ok(fields .into_iter() .map(|(name, value)| format!("{name}: {value:?}")) .collect::>() .join(" ")) } else { Ok(format!("{value:?}")) } } pub fn te_glow_byte(glow: f32) -> Result { Ok((glow * 255.0) as u8) } pub fn te_glow_float(bytes: Vec, pos: i32) -> Result { Ok(f32::from(*bytes.get(index(pos)?).ok_or(Error::IndexOutOfRange)?) / 255.0) } pub fn te_offset_float(bytes: Vec, pos: i32) -> Result { let pos = index(pos)?; let pair: [u8; 2] = bytes .get(pos..pos.checked_add(2).ok_or(Error::IndexOutOfRange)?) .ok_or(Error::IndexOutOfRange)? .try_into() .map_err(|_| Error::IndexOutOfRange)?; Ok(f32::from(i16::from_le_bytes(pair)) / 32767.0) } pub fn te_offset_short(offset: f32) -> Result { Ok((offset.clamp(-1.0, 1.0) * 32767.0).round_ties_even() as i16) } pub fn te_rotation_float(bytes: Vec, pos: i32) -> Result { let pos = index(pos)?; let pair: [u8; 2] = bytes .get(pos..pos.checked_add(2).ok_or(Error::IndexOutOfRange)?) .ok_or(Error::IndexOutOfRange)? .try_into() .map_err(|_| Error::IndexOutOfRange)?; Ok(f32::from(u16::from_le_bytes(pair)) / 32768.0 * std::f32::consts::TAU) } pub fn te_rotation_short(rotation: f32) -> Result { let remainder = rotation - (rotation / std::f32::consts::TAU).round_ties_even() * std::f32::consts::TAU; Ok((remainder / std::f32::consts::TAU * 32768.0 + 0.5).round_ties_even() as i16) } pub fn z_compress_osd(data: OSD) -> Result, Error> { let binary = OSDParser::serialize_llsd_binary_with_osd_boolean(data, false)?; let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best()); encoder .write_all(&binary) .map_err(|_| Error::InvalidOperation)?; encoder.finish().map_err(|_| Error::InvalidOperation) } pub fn z_decompress_osd(data: Vec) -> Result { Self::decode_zlib(&data) } pub fn zero_decode( src: Option<&[u8]>, srclen: i32, dest: Option<&mut [u8]>, ) -> Result { crate::packet_wire::zero_decode(src, srclen, dest) } pub fn zero_encode( src: Option<&[u8]>, srclen: i32, dest: Option<&mut [u8]>, ) -> Result { crate::packet_wire::zero_encode(src, srclen, dest) } fn decode_zlib(data: &[u8]) -> Result { let decoder = ZlibDecoder::new(data); let mut limited = decoder.take(MAX_DECOMPRESSED_OSD + 1); let mut decoded = Vec::new(); limited .read_to_end(&mut decoded) .map_err(|_| Error::Parse { position: 0, context: "invalid zlib-compressed LLSD", })?; if decoded.len() as u64 > MAX_DECOMPRESSED_OSD { return Err(Error::Argument); } OSDParser::deserialize_llsd_binary_with_bytes(decoded) } } fn index(value: i32) -> Result { usize::try_from(value).map_err(|_| Error::IndexOutOfRange) } #[cfg(test)] mod tests { use super::*; #[test] fn terse_float_and_texture_entry_conversions_match_pinned_examples() { assert_eq!(Helpers::float_to_terse_string(0.0).unwrap(), ".00"); assert_eq!(Helpers::float_to_terse_string(0.5).unwrap(), ".5"); assert_eq!(Helpers::float_to_terse_string(-0.25).unwrap(), "-.25"); for offset in [-1.0, -0.25, 0.0, 0.75, 1.0] { let encoded = Helpers::te_offset_short(offset).unwrap(); let decoded = Helpers::te_offset_float(encoded.to_le_bytes().to_vec(), 0).unwrap(); assert!((decoded - offset).abs() <= 1.0 / 32767.0); } } #[test] fn compressed_llsd_round_trips_and_rejects_non_zlib_input() { let value = OSD::Map(HashMap::from([ ("grid".to_owned(), OSD::String("OpenSim".to_owned())), ("count".to_owned(), OSD::Integer(3)), ])); let compressed = Helpers::z_compress_osd(value.clone()).unwrap(); assert_eq!(Helpers::z_decompress_osd(compressed).unwrap(), value); assert!(Helpers::z_decompress_osd(vec![1, 2, 3]).is_err()); } #[test] fn split_by_counts_utf16_units() { let chunks: Vec<_> = Helpers::split_by("ab🙂cd".to_owned(), 2).unwrap().collect(); assert_eq!(chunks, ["ab", "🙂", "cd"]); } #[test] fn region_handle_returns_local_coordinates() { let mut local_x = 0.0; let mut local_y = 0.0; let handle = Helpers::global_pos_to_region_handle(300.5, 600.25, &mut local_x, &mut local_y) .unwrap(); assert_eq!(handle, (256_u64 << 32) | 512); assert_eq!(local_x, 44.5); assert_eq!(local_y, 88.25); } }