Complete first release candidate audit (#107)
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
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
This commit is contained in:
350
crates/libremetaverse/src/helpers.rs
Normal file
350
crates/libremetaverse/src/helpers.rs
Normal file
@@ -0,0 +1,350 @@
|
||||
//! 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<dyn ReadWrite + Send>,
|
||||
mut output: Box<dyn ReadWrite + Send>,
|
||||
) -> 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<u8>) -> Result<OSD, Error> {
|
||||
Self::decode_zlib(&mesh_bytes)
|
||||
}
|
||||
|
||||
pub fn float_to_terse_string(val: f32) -> Result<String, Error> {
|
||||
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<Option<Box<dyn ReadWrite + Send>>, 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<Option<Box<dyn ReadWrite + Send>>, 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<u64, Error> {
|
||||
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<u32, Error> {
|
||||
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<Vec<Primitive>, 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<Primitive>) -> Result<OSD, Error> {
|
||||
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<dyn Fn() + Send + Sync>,
|
||||
action_name: Option<String>,
|
||||
logger: Option<Box<dyn Fn(String, Option<ExternalError>) + 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::<String>()
|
||||
.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<Box<dyn Iterator<Item = String>>, 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<u16> = str.encode_utf16().collect();
|
||||
let chunks: Vec<String> = units
|
||||
.chunks(chunk_length)
|
||||
.map(String::from_utf16_lossy)
|
||||
.collect();
|
||||
Ok(Box::new(chunks.into_iter()))
|
||||
}
|
||||
|
||||
pub fn struct_to_string(value: Object) -> Result<String, Error> {
|
||||
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::<Vec<_>>()
|
||||
.join(" "))
|
||||
} else {
|
||||
Ok(format!("{value:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn te_glow_byte(glow: f32) -> Result<u8, Error> {
|
||||
Ok((glow * 255.0) as u8)
|
||||
}
|
||||
|
||||
pub fn te_glow_float(bytes: Vec<u8>, pos: i32) -> Result<f32, Error> {
|
||||
Ok(f32::from(*bytes.get(index(pos)?).ok_or(Error::IndexOutOfRange)?) / 255.0)
|
||||
}
|
||||
|
||||
pub fn te_offset_float(bytes: Vec<u8>, pos: i32) -> Result<f32, Error> {
|
||||
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<i16, Error> {
|
||||
Ok((offset.clamp(-1.0, 1.0) * 32767.0).round_ties_even() as i16)
|
||||
}
|
||||
|
||||
pub fn te_rotation_float(bytes: Vec<u8>, pos: i32) -> Result<f32, Error> {
|
||||
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<i16, Error> {
|
||||
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<Vec<u8>, 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<u8>) -> Result<OSD, Error> {
|
||||
Self::decode_zlib(&data)
|
||||
}
|
||||
|
||||
pub fn zero_decode(
|
||||
src: Option<&[u8]>,
|
||||
srclen: i32,
|
||||
dest: Option<&mut [u8]>,
|
||||
) -> Result<i32, Error> {
|
||||
crate::packet_wire::zero_decode(src, srclen, dest)
|
||||
}
|
||||
|
||||
pub fn zero_encode(
|
||||
src: Option<&[u8]>,
|
||||
srclen: i32,
|
||||
dest: Option<&mut [u8]>,
|
||||
) -> Result<i32, Error> {
|
||||
crate::packet_wire::zero_encode(src, srclen, dest)
|
||||
}
|
||||
|
||||
fn decode_zlib(data: &[u8]) -> Result<OSD, Error> {
|
||||
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, Error> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user