Implement native asset pipeline and cache (#64)
Some checks failed
Native code generation / deterministic (push) Failing after 2m18s
Imaging and meshing gate / native (push) Failing after 1m30s
JPEG 2000 feature / linux (push) Successful in 2m40s
Native Rust workspace compile / compile (push) Failing after 57s
Skia feature / linux (push) Successful in 31m8s

This commit is contained in:
2026-08-10 07:51:17 +00:00
parent d5c318d280
commit 52f62d8c39
23 changed files with 4511 additions and 1620 deletions

View File

@@ -223,10 +223,10 @@ impl J2kCodec {
if options.max_encoded_bytes == 0 {
return Err(Error::Argument);
}
if let J2kCompression::Lossy { compression_ratio } = options.compression {
if !compression_ratio.is_finite() || compression_ratio < 1.0 {
return Err(Error::Argument);
}
if let J2kCompression::Lossy { compression_ratio } = options.compression
&& (!compression_ratio.is_finite() || compression_ratio < 1.0)
{
return Err(Error::Argument);
}
encode_with_openjpeg(image, options)
}

View File

@@ -8,7 +8,7 @@ repository.workspace = true
description = "Rust rewrite shell for the LibreMetaverse client library"
[features]
default = ["dds-bc67"]
default = ["dds-bc67", "jpeg2000"]
dds-bc67 = ["dep:bcdec_rs"]
jpeg2000 = ["libremetaverse-imaging/jpeg2000"]
@@ -24,7 +24,9 @@ mac_address2 = "2.0.2"
os_info = { version = "3.15.0", default-features = false }
reqwest = { version = "0.13.4", default-features = false, features = ["rustls", "stream"] }
roxmltree = "0.21.1"
serde_json = "1.0.143"
tokio = { version = "1.47.1", features = ["macros", "net", "rt", "sync", "time"] }
vorbis_rs = "0.5.5"
[dev-dependencies]
tokio = { version = "1.47.1", features = ["macros", "net", "rt-multi-thread", "sync", "test-util", "time"] }

View File

@@ -0,0 +1,484 @@
//! Cross-platform, bounded on-disk asset cache.
#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::collapsible_if,
clippy::missing_errors_doc,
clippy::must_use_candidate,
clippy::needless_pass_by_value,
clippy::single_match_else,
clippy::type_complexity,
clippy::uninlined_format_args,
clippy::unused_async,
clippy::unused_self
)]
use crate::{Error, GridClient, ImageDownload};
use libremetaverse_types::UUID;
use libremetaverse_types::compat::{CancellationToken, Object};
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
const CACHE_TARGET_PERCENT: i64 = 90;
const ABSOLUTE_CACHE_FILE_LIMIT: u64 = crate::asset_models::MAX_ASSET_BYTES as u64;
type CacheFilenameHandler = dyn Fn(String, UUID) -> Result<String, Error> + Send + Sync;
#[derive(Clone)]
pub struct AssetCacheComputeAssetCacheFilenameDelegate {
handler: Arc<CacheFilenameHandler>,
}
impl std::fmt::Debug for AssetCacheComputeAssetCacheFilenameDelegate {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("AssetCacheComputeAssetCacheFilenameDelegate")
.finish_non_exhaustive()
}
}
impl AssetCacheComputeAssetCacheFilenameDelegate {
pub fn from_handler(
handler: impl Fn(String, UUID) -> Result<String, Error> + Send + Sync + 'static,
) -> Self {
Self {
handler: Arc::new(handler),
}
}
pub fn new(object: Object, method: isize) -> Result<Self, Error> {
if method == 0 {
return Err(Error::Argument);
}
object
.downcast_arc::<Self>()
.map(|value| (*value).clone())
.ok_or(Error::Argument)
}
pub fn invoke(&self, cache_dir: String, asset_id: UUID) -> Result<String, Error> {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
(self.handler)(cache_dir, asset_id)
}))
.map_err(|_| Error::InvalidOperation)?
}
pub fn begin_invoke(
&self,
cache_dir: String,
asset_id: UUID,
callback: Box<dyn Fn(&dyn std::any::Any) + Send + Sync>,
object: Object,
) -> Result<Box<dyn std::any::Any + Send + Sync>, Error> {
let result = self.invoke(cache_dir, asset_id)?;
callback(&object);
Ok(Box::new(result))
}
pub fn end_invoke(
&self,
result: Box<dyn std::any::Any + Send + Sync>,
) -> Result<String, Error> {
result
.downcast::<String>()
.map(|value| *value)
.map_err(|_| Error::Argument)
}
}
struct AssetCacheInner {
client: crate::client_core::ClientWeakHandle,
pruning: AtomicBool,
auto_prune: AtomicBool,
interval_bits: AtomicU64,
last_prune_ms: AtomicU64,
}
pub struct AssetCache {
inner: Arc<AssetCacheInner>,
pub compute_asset_cache_filename: Option<AssetCacheComputeAssetCacheFilenameDelegate>,
}
impl Clone for AssetCache {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
compute_asset_cache_filename: self.compute_asset_cache_filename.clone(),
}
}
}
impl std::fmt::Debug for AssetCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AssetCache")
.field("auto_prune_enabled", &self.auto_prune_enabled())
.finish_non_exhaustive()
}
}
impl AssetCache {
fn now_ms() -> u64 {
u64::try_from(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis(),
)
.unwrap_or(u64::MAX)
}
pub fn new(client: GridClient) -> Result<Self, Error> {
Ok(Self {
inner: Arc::new(AssetCacheInner {
client: client.native_weak_handle(),
pruning: AtomicBool::new(false),
auto_prune: AtomicBool::new(true),
interval_bits: AtomicU64::new((300_000_f64).to_bits()),
last_prune_ms: AtomicU64::new(Self::now_ms()),
}),
compute_asset_cache_filename: None,
})
}
fn config(&self) -> Result<crate::AssetCacheSettings, Error> {
let client = self.inner.client.upgrade().ok_or(Error::InvalidOperation)?;
Ok(client.settings_ref().asset_cache())
}
fn cache_dir(&self) -> Result<PathBuf, Error> {
let config = self.config()?;
let dir = PathBuf::from(config.dir);
if dir.as_os_str().is_empty() {
return Err(Error::InvalidOperation);
}
Ok(dir)
}
fn path(&self, id: UUID) -> Result<PathBuf, Error> {
let dir = self.cache_dir()?;
if let Some(compute) = &self.compute_asset_cache_filename {
let path = PathBuf::from(compute.invoke(dir.to_string_lossy().into_owned(), id)?);
if path.parent() == Some(dir.as_path()) {
return Ok(path);
}
return Err(Error::Argument);
}
Ok(dir.join(id.to_string()))
}
fn operational(&self) -> Result<bool, Error> {
Ok(self.config()?.enabled)
}
fn checked_read(&self, path: &Path) -> Result<Option<Vec<u8>>, Error> {
let metadata = match fs::metadata(path) {
Ok(value) => value,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(_) => return Err(Error::InvalidOperation),
};
if !metadata.is_file() || metadata.len() == 0 || metadata.len() > ABSOLUTE_CACHE_FILE_LIMIT
{
return Err(Error::Argument);
}
let bytes = fs::read(path).map_err(|_| Error::InvalidOperation)?;
if bytes.len() as u64 != metadata.len() {
return Err(Error::InvalidOperation);
}
Ok(Some(bytes))
}
pub fn get_cached_asset_bytes_with_uuid(
&self,
asset_id: UUID,
) -> Result<Option<Vec<u8>>, Error> {
if !self.operational()? {
return Ok(None);
}
self.checked_read(&self.path(asset_id)?)
}
pub async fn get_cached_asset_bytes_with_uuid_cancellation_token(
&self,
asset_id: UUID,
cancellation_token: Option<CancellationToken>,
) -> Result<Option<Vec<u8>>, Error> {
if let Some(token) = &cancellation_token {
token.throw_if_cancellation_requested()?;
}
let result = self.get_cached_asset_bytes_with_uuid(asset_id);
if let Some(token) = &cancellation_token {
token.throw_if_cancellation_requested()?;
}
result
}
pub fn try_get_cached_asset_bytes_with_uuid_bytes(
&self,
asset_id: UUID,
data: &mut Option<Vec<u8>>,
) -> bool {
match self.get_cached_asset_bytes_with_uuid(asset_id) {
Ok(Some(bytes)) => {
*data = Some(bytes);
true
}
_ => {
*data = None;
false
}
}
}
pub async fn try_get_cached_asset_bytes_with_uuid_cancellation_token(
&self,
asset_id: UUID,
cancellation_token: Option<CancellationToken>,
) -> Result<(bool, Option<Vec<u8>>), Error> {
let data = self
.get_cached_asset_bytes_with_uuid_cancellation_token(asset_id, cancellation_token)
.await?;
Ok((data.is_some(), data))
}
pub fn save_asset_to_cache_with_uuid_bytes(
&self,
asset_id: UUID,
asset_data: Vec<u8>,
) -> Result<bool, Error> {
if !self.operational()? {
return Ok(false);
}
if asset_data.is_empty() || asset_data.len() > crate::asset_models::MAX_ASSET_BYTES {
return Err(Error::Argument);
}
let path = self.path(asset_id)?;
if path.is_file() {
return Ok(true);
}
let dir = path.parent().ok_or(Error::Argument)?;
fs::create_dir_all(dir).map_err(|_| Error::InvalidOperation)?;
let nonce = UUID::random()?.to_string();
let temp = dir.join(format!(".{}.{}.tmp", asset_id, nonce));
let write_result = (|| -> Result<(), Error> {
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&temp)
.map_err(|_| Error::InvalidOperation)?;
file.write_all(&asset_data)
.map_err(|_| Error::InvalidOperation)?;
file.sync_all().map_err(|_| Error::InvalidOperation)?;
match fs::rename(&temp, &path) {
Ok(()) => Ok(()),
Err(_) if path.is_file() => Ok(()),
Err(_) => Err(Error::InvalidOperation),
}
})();
if write_result.is_err() || temp.exists() {
let _ = fs::remove_file(&temp);
}
write_result.map(|()| {
self.maybe_begin_prune();
true
})
}
fn maybe_begin_prune(&self) {
if !self.auto_prune_enabled() {
return;
}
let interval = self.auto_prune_interval();
if !interval.is_finite() || interval <= 0.0 {
return;
}
let now = Self::now_ms();
let last = self.inner.last_prune_ms.load(Ordering::Acquire);
if now.saturating_sub(last) < interval as u64 {
return;
}
if self
.inner
.last_prune_ms
.compare_exchange(last, now, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
let _ = self.begin_prune();
}
}
pub async fn save_asset_to_cache_with_uuid_bytes_cancellation_token(
&self,
asset_id: UUID,
asset_data: Vec<u8>,
cancellation_token: Option<CancellationToken>,
) -> Result<bool, Error> {
if let Some(token) = &cancellation_token {
token.throw_if_cancellation_requested()?;
}
let result = self.save_asset_to_cache_with_uuid_bytes(asset_id, asset_data);
if let Some(token) = &cancellation_token {
token.throw_if_cancellation_requested()?;
}
result
}
pub fn asset_file_name(&self, asset_id: UUID) -> Result<Option<String>, Error> {
if !self.operational()? {
return Ok(None);
}
let path = self.path(asset_id)?;
Ok(path.is_file().then(|| path.to_string_lossy().into_owned()))
}
pub fn has_asset(&self, asset_id: UUID) -> Result<bool, Error> {
Ok(self.operational()? && self.path(asset_id)?.is_file())
}
fn cache_entries(&self) -> Result<Vec<(PathBuf, u64, SystemTime)>, Error> {
let dir = self.cache_dir()?;
let mut entries = Vec::new();
let read = match fs::read_dir(dir) {
Ok(value) => value,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(entries),
Err(_) => return Err(Error::InvalidOperation),
};
for entry in read.flatten() {
if let Ok(metadata) = entry.metadata() {
if metadata.is_file() {
entries.push((
entry.path(),
metadata.len(),
metadata
.accessed()
.or_else(|_| metadata.modified())
.unwrap_or(UNIX_EPOCH),
));
}
}
}
Ok(entries)
}
pub fn clear(&self) -> Result<(), Error> {
for (path, _, _) in self.cache_entries()? {
fs::remove_file(path).map_err(|_| Error::InvalidOperation)?;
}
Ok(())
}
pub async fn prune(&self, cancellation_token: Option<CancellationToken>) -> Result<(), Error> {
if let Some(token) = &cancellation_token {
token.throw_if_cancellation_requested()?;
}
let max = self.config()?.max_size;
if max <= 0 {
return Err(Error::InvalidOperation);
}
let mut entries = self.cache_entries()?;
let mut size: u64 = entries.iter().map(|(_, size, _)| *size).sum();
let max = u64::try_from(max).map_err(|_| Error::InvalidOperation)?;
if size <= max {
return Ok(());
}
entries.sort_unstable_by_key(|(_, _, accessed)| *accessed);
let target = max.saturating_mul(CACHE_TARGET_PERCENT as u64) / 100;
for (path, length, _) in entries {
if let Some(token) = &cancellation_token {
token.throw_if_cancellation_requested()?;
}
fs::remove_file(path).map_err(|_| Error::InvalidOperation)?;
size = size.saturating_sub(length);
if size <= target {
break;
}
}
Ok(())
}
pub fn begin_prune(&self) -> Result<(), Error> {
if self.inner.pruning.swap(true, Ordering::AcqRel) {
return Ok(());
}
let cache = self.clone();
std::thread::Builder::new()
.name("asset-cache-prune".into())
.spawn(move || {
let _ = cache.prune_sync(None);
cache.inner.pruning.store(false, Ordering::Release);
})
.map_err(|_| {
self.inner.pruning.store(false, Ordering::Release);
Error::InvalidOperation
})?;
Ok(())
}
fn prune_sync(&self, cancellation_token: Option<CancellationToken>) -> Result<(), Error> {
if let Some(token) = &cancellation_token {
token.throw_if_cancellation_requested()?;
}
let max = self.config()?.max_size;
if max <= 0 {
return Err(Error::InvalidOperation);
}
let mut entries = self.cache_entries()?;
let mut size: u64 = entries.iter().map(|(_, size, _)| *size).sum();
let max = u64::try_from(max).map_err(|_| Error::InvalidOperation)?;
if size <= max {
return Ok(());
}
entries.sort_unstable_by_key(|(_, _, accessed)| *accessed);
let target = max.saturating_mul(CACHE_TARGET_PERCENT as u64) / 100;
for (path, length, _) in entries {
if let Some(token) = &cancellation_token {
token.throw_if_cancellation_requested()?;
}
fs::remove_file(path).map_err(|_| Error::InvalidOperation)?;
size = size.saturating_sub(length);
if size <= target {
break;
}
}
Ok(())
}
pub fn dispose(&self) -> Result<(), Error> {
self.inner.auto_prune.store(false, Ordering::Release);
Ok(())
}
pub fn get_cached_image(&self, image_id: UUID) -> Result<Option<ImageDownload>, Error> {
let Some(bytes) = self.get_cached_asset_bytes_with_uuid(image_id)? else {
return Ok(None);
};
let mut image = ImageDownload::new()?;
image.base.id = image_id;
image.base.asset_type = libremetaverse_types::AssetType::Texture;
image.base.size = i32::try_from(bytes.len()).map_err(|_| Error::Argument)?;
image.base.transferred = image.base.size;
image.base.success = true;
image.base.asset_data = bytes;
image.codec = crate::ImageCodec::J2C;
Ok(Some(image))
}
pub fn auto_prune_enabled(&self) -> bool {
self.inner.auto_prune.load(Ordering::Acquire)
}
pub fn set_auto_prune_enabled(&mut self, value: bool) {
self.inner.auto_prune.store(value, Ordering::Release);
}
pub fn auto_prune_interval(&self) -> f64 {
f64::from_bits(self.inner.interval_bits.load(Ordering::Acquire))
}
pub fn set_auto_prune_interval(&mut self, value: f64) {
if value.is_finite() && value > 0.0 {
self.inner
.interval_bits
.store(value.to_bits(), Ordering::Release);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,719 @@
//! GLTF material asset and simulator material-override encoding.
#![allow(
clippy::assigning_clones,
clippy::cast_possible_truncation,
clippy::collapsible_if,
clippy::float_cmp,
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::must_use_candidate,
clippy::needless_pass_by_value,
clippy::should_implement_trait
)]
use crate::{Error, assets::GltfAlphaMode};
use libremetaverse_structured_data::{OSD, OSDMap};
use libremetaverse_types::{AssetType, Color4, UUID, Vector2, Vector3};
use serde_json::{Value, json};
use std::collections::HashMap;
const FLOAT_EPSILON: f32 = 1.192_092_9e-7;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GltfTextureTransform {
pub offset: Vector2,
pub rotation: f32,
pub scale: Vector2,
}
impl GltfTextureTransform {
pub fn default() -> Self {
Self {
offset: Vector2::zero(),
rotation: 0.0,
scale: Vector2 { x: 1.0, y: 1.0 },
}
}
pub fn equals_with_gltf_texture_transform(&self, other: Self) -> bool {
*self == other
}
pub fn equals_with_object(&self, _obj: Option<libremetaverse_types::compat::Object>) -> bool {
false
}
pub fn get_hash_code(&self) -> i32 {
(self.offset.x.to_bits()
^ self.offset.y.to_bits()
^ self.rotation.to_bits()
^ self.scale.x.to_bits()
^ self.scale.y.to_bits())
.cast_signed()
}
pub fn is_default(&self) -> bool {
self.offset.x.abs() <= FLOAT_EPSILON
&& self.offset.y.abs() <= FLOAT_EPSILON
&& self.rotation.abs() <= FLOAT_EPSILON
&& (self.scale.x - 1.0).abs() <= FLOAT_EPSILON
&& (self.scale.y - 1.0).abs() <= FLOAT_EPSILON
}
}
#[derive(Clone, Debug)]
pub struct AssetMaterial {
asset_id: UUID,
asset_data: Vec<u8>,
name: String,
texture_ids: Vec<UUID>,
texture_transforms: Vec<GltfTextureTransform>,
base_color_factor: Color4,
emissive_factor: Vector3,
metallic_factor: f32,
roughness_factor: f32,
alpha_cutoff: f32,
alpha_mode: GltfAlphaMode,
double_sided: bool,
override_alpha_mode: bool,
override_double_sided: bool,
}
impl AssetMaterial {
pub const TEXTURE_BASE_COLOR: i32 = 0;
pub const TEXTURE_NORMAL: i32 = 1;
pub const TEXTURE_METALLIC_ROUGHNESS: i32 = 2;
pub const TEXTURE_EMISSIVE: i32 = 3;
pub const TEXTURE_COUNT: i32 = 4;
pub fn default() -> Self {
Self {
asset_id: UUID::zero(),
asset_data: Vec::new(),
name: String::new(),
texture_ids: vec![UUID::zero(); Self::TEXTURE_COUNT as usize],
texture_transforms: vec![GltfTextureTransform::default(); Self::TEXTURE_COUNT as usize],
base_color_factor: Color4 {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
},
emissive_factor: Vector3::zero(),
metallic_factor: 1.0,
roughness_factor: 1.0,
alpha_cutoff: 0.5,
alpha_mode: GltfAlphaMode::Opaque,
double_sided: false,
override_alpha_mode: false,
override_double_sided: false,
}
}
pub fn gltf_override_null_uuid() -> UUID {
UUID::new_with_string("ffffffff-ffff-ffff-ffff-ffffffffffff".into())
.expect("constant UUID is valid")
}
pub fn new_with_constructor() -> Result<Self, Error> {
Ok(Self::default())
}
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
if asset_data.len() > crate::asset_models::MAX_ASSET_BYTES {
return Err(Error::Argument);
}
let mut material = Self {
asset_id,
asset_data,
..Self::default()
};
if !material.decode()? {
return Err(Error::Argument);
}
Ok(material)
}
fn slot(slot: i32) -> Result<usize, Error> {
usize::try_from(slot)
.ok()
.filter(|slot| *slot < Self::TEXTURE_COUNT as usize)
.ok_or(Error::Argument)
}
pub fn set_texture_id(
&mut self,
slot: i32,
id: UUID,
for_override: Option<bool>,
) -> Result<(), Error> {
let slot = Self::slot(slot)?;
self.texture_ids[slot] = if for_override.unwrap_or(false) && id == UUID::zero() {
Self::gltf_override_null_uuid()
} else {
id
};
Ok(())
}
pub fn set_texture_offset(&mut self, slot: i32, offset: Vector2) -> Result<(), Error> {
self.texture_transforms[Self::slot(slot)?].offset = offset;
Ok(())
}
pub fn set_texture_scale(&mut self, slot: i32, scale: Vector2) -> Result<(), Error> {
self.texture_transforms[Self::slot(slot)?].scale = scale;
Ok(())
}
pub fn set_texture_rotation(&mut self, slot: i32, rotation: f32) -> Result<(), Error> {
self.texture_transforms[Self::slot(slot)?].rotation = rotation;
Ok(())
}
pub fn set_base_color_factor_with_color4_boolean(
&mut self,
mut color: Color4,
for_override: Option<bool>,
) -> Result<(), Error> {
if for_override.unwrap_or(false) && color == Self::default().base_color_factor {
color.a -= FLOAT_EPSILON;
}
self.base_color_factor = color;
Ok(())
}
pub fn set_emissive_factor_with_vector3_boolean(
&mut self,
mut color: Vector3,
for_override: Option<bool>,
) -> Result<(), Error> {
if for_override.unwrap_or(false) && color == Vector3::zero() {
color.x += FLOAT_EPSILON;
}
self.emissive_factor = color;
Ok(())
}
pub fn set_metallic_factor_with_single_boolean(
&mut self,
metallic: f32,
for_override: Option<bool>,
) -> Result<(), Error> {
if !metallic.is_finite() {
return Err(Error::Argument);
}
self.metallic_factor = if for_override.unwrap_or(false) {
metallic.min(1.0 - FLOAT_EPSILON)
} else {
metallic
};
Ok(())
}
pub fn set_roughness_factor_with_single_boolean(
&mut self,
roughness: f32,
for_override: Option<bool>,
) -> Result<(), Error> {
if !roughness.is_finite() {
return Err(Error::Argument);
}
self.roughness_factor = if for_override.unwrap_or(false) {
roughness.min(1.0 - FLOAT_EPSILON)
} else {
roughness
};
Ok(())
}
pub fn set_alpha_cutoff_with_single_boolean(
&mut self,
cutoff: f32,
for_override: Option<bool>,
) -> Result<(), Error> {
if !cutoff.is_finite() {
return Err(Error::Argument);
}
self.alpha_cutoff = if for_override.unwrap_or(false) && cutoff == 0.5 {
cutoff - FLOAT_EPSILON
} else {
cutoff
};
Ok(())
}
pub fn set_alpha_mode_with_gltf_alpha_mode_boolean(
&mut self,
mode: GltfAlphaMode,
for_override: Option<bool>,
) -> Result<(), Error> {
self.alpha_mode = mode;
self.override_alpha_mode = for_override.unwrap_or(false);
Ok(())
}
pub fn set_double_sided_with_boolean_boolean(
&mut self,
double_sided: bool,
for_override: Option<bool>,
) -> Result<(), Error> {
self.double_sided = double_sided;
self.override_double_sided = for_override.unwrap_or(false);
Ok(())
}
pub fn set_base_material(&mut self) -> Result<(), Error> {
let transforms = self.texture_transforms.clone();
let asset_id = self.asset_id;
*self = Self::default();
self.asset_id = asset_id;
self.texture_transforms = transforms;
Ok(())
}
pub fn apply_override(&mut self, value: Self) -> Result<(), Error> {
let sentinel = Self::gltf_override_null_uuid();
for slot in 0..Self::TEXTURE_COUNT as usize {
if value.texture_ids[slot] == sentinel {
self.texture_ids[slot] = UUID::zero();
} else if value.texture_ids[slot] != UUID::zero() {
self.texture_ids[slot] = value.texture_ids[slot];
}
if !value.texture_transforms[slot].is_default() {
self.texture_transforms[slot] = value.texture_transforms[slot];
}
}
let defaults = Self::default();
if value.base_color_factor != defaults.base_color_factor {
self.base_color_factor = value.base_color_factor;
}
if value.emissive_factor != defaults.emissive_factor {
self.emissive_factor = value.emissive_factor;
}
if value.metallic_factor != defaults.metallic_factor {
self.metallic_factor = value.metallic_factor;
}
if value.roughness_factor != defaults.roughness_factor {
self.roughness_factor = value.roughness_factor;
}
if value.alpha_cutoff != defaults.alpha_cutoff {
self.alpha_cutoff = value.alpha_cutoff;
}
if value.override_alpha_mode {
self.alpha_mode = value.alpha_mode;
}
if value.override_double_sided {
self.double_sided = value.double_sided;
}
Ok(())
}
fn alpha_name(mode: GltfAlphaMode) -> &'static str {
match mode {
GltfAlphaMode::Opaque => "OPAQUE",
GltfAlphaMode::Blend => "BLEND",
GltfAlphaMode::Mask => "MASK",
}
}
fn parse_alpha(value: Option<&str>) -> Result<GltfAlphaMode, Error> {
match value.unwrap_or("OPAQUE") {
"OPAQUE" => Ok(GltfAlphaMode::Opaque),
"BLEND" => Ok(GltfAlphaMode::Blend),
"MASK" => Ok(GltfAlphaMode::Mask),
_ => Err(Error::Argument),
}
}
fn texture_json(
&self,
slot: usize,
images: &mut Vec<Value>,
textures: &mut Vec<Value>,
) -> Option<Value> {
let id = self.texture_ids[slot];
if id == UUID::zero() || id == Self::gltf_override_null_uuid() {
return None;
}
let index = images.len();
images.push(json!({"uri": id.to_string()}));
textures.push(json!({"source": index}));
let mut info = json!({"index": index});
let transform = self.texture_transforms[slot];
if !transform.is_default() {
info["extensions"] = json!({"KHR_texture_transform": {"offset": [transform.offset.x, transform.offset.y], "scale": [transform.scale.x, transform.scale.y], "rotation": transform.rotation}});
}
Some(info)
}
pub fn to_json(&self) -> Result<String, Error> {
let mut images = Vec::new();
let mut textures = Vec::new();
let base = self.texture_json(
Self::TEXTURE_BASE_COLOR as usize,
&mut images,
&mut textures,
);
let normal = self.texture_json(Self::TEXTURE_NORMAL as usize, &mut images, &mut textures);
let metallic = self.texture_json(
Self::TEXTURE_METALLIC_ROUGHNESS as usize,
&mut images,
&mut textures,
);
let emissive =
self.texture_json(Self::TEXTURE_EMISSIVE as usize, &mut images, &mut textures);
let mut pbr = json!({"baseColorFactor": [self.base_color_factor.r,self.base_color_factor.g,self.base_color_factor.b,self.base_color_factor.a], "metallicFactor": self.metallic_factor, "roughnessFactor": self.roughness_factor});
if let Some(value) = base {
pbr["baseColorTexture"] = value;
}
if let Some(value) = metallic {
pbr["metallicRoughnessTexture"] = value;
}
let mut material = json!({"name": self.name, "pbrMetallicRoughness": pbr, "emissiveFactor": [self.emissive_factor.x,self.emissive_factor.y,self.emissive_factor.z], "alphaMode": Self::alpha_name(self.alpha_mode), "alphaCutoff": self.alpha_cutoff, "doubleSided": self.double_sided});
if let Some(value) = normal {
material["normalTexture"] = value;
}
if let Some(value) = emissive {
material["emissiveTexture"] = value;
}
let mut root = json!({"asset":{"version":"2.0"},"materials":[material]});
if !images.is_empty() {
root["images"] = Value::Array(images);
root["textures"] = Value::Array(textures);
root["extensionsUsed"] = json!(["KHR_texture_transform"]);
}
serde_json::to_string(&root).map_err(|_| Error::InvalidOperation)
}
pub fn encode(&mut self) -> Result<(), Error> {
self.asset_data = self.to_json()?.into_bytes();
Ok(())
}
pub fn decode(&mut self) -> Result<bool, Error> {
let root: Value = serde_json::from_slice(&self.asset_data).map_err(|_| Error::Argument)?;
let mat = root
.get("materials")
.and_then(Value::as_array)
.and_then(|v| v.first())
.and_then(Value::as_object)
.ok_or(Error::Argument)?;
self.name = mat
.get("name")
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned();
let pbr = mat.get("pbrMetallicRoughness").and_then(Value::as_object);
if let Some(values) = pbr
.and_then(|v| v.get("baseColorFactor"))
.and_then(Value::as_array)
{
if values.len() == 4 {
self.base_color_factor = Color4 {
r: number(&values[0])?,
g: number(&values[1])?,
b: number(&values[2])?,
a: number(&values[3])?,
};
}
}
self.metallic_factor = pbr
.and_then(|v| v.get("metallicFactor"))
.map(number)
.transpose()?
.unwrap_or(1.0);
self.roughness_factor = pbr
.and_then(|v| v.get("roughnessFactor"))
.map(number)
.transpose()?
.unwrap_or(1.0);
if let Some(values) = mat.get("emissiveFactor").and_then(Value::as_array) {
if values.len() == 3 {
self.emissive_factor = Vector3 {
x: number(&values[0])?,
y: number(&values[1])?,
z: number(&values[2])?,
};
}
}
self.alpha_mode = Self::parse_alpha(mat.get("alphaMode").and_then(Value::as_str))?;
self.alpha_cutoff = mat
.get("alphaCutoff")
.map(number)
.transpose()?
.unwrap_or(0.5);
self.double_sided = mat
.get("doubleSided")
.and_then(Value::as_bool)
.unwrap_or(false);
let textures = root.get("textures").and_then(Value::as_array);
let images = root.get("images").and_then(Value::as_array);
for (slot, info) in [
(0, pbr.and_then(|v| v.get("baseColorTexture"))),
(1, mat.get("normalTexture")),
(2, pbr.and_then(|v| v.get("metallicRoughnessTexture"))),
(3, mat.get("emissiveTexture")),
] {
if let Some(info) = info {
self.decode_texture(slot, info, textures, images)?;
}
}
Ok(true)
}
fn decode_texture(
&mut self,
slot: usize,
info: &Value,
textures: Option<&Vec<Value>>,
images: Option<&Vec<Value>>,
) -> Result<(), Error> {
let index = info
.get("index")
.and_then(Value::as_u64)
.and_then(|v| usize::try_from(v).ok())
.ok_or(Error::Argument)?;
let source = textures
.and_then(|v| v.get(index))
.and_then(|v| v.get("source"))
.and_then(Value::as_u64)
.and_then(|v| usize::try_from(v).ok())
.ok_or(Error::Argument)?;
let uri = images
.and_then(|v| v.get(source))
.and_then(|v| v.get("uri"))
.and_then(Value::as_str)
.ok_or(Error::Argument)?;
self.texture_ids[slot] = UUID::new_with_string(uri.to_owned())?;
if let Some(transform) = info.pointer("/extensions/KHR_texture_transform") {
if let Some(v) = transform.get("offset").and_then(Value::as_array) {
if v.len() == 2 {
self.texture_transforms[slot].offset = Vector2 {
x: number(&v[0])?,
y: number(&v[1])?,
};
}
}
if let Some(v) = transform.get("scale").and_then(Value::as_array) {
if v.len() == 2 {
self.texture_transforms[slot].scale = Vector2 {
x: number(&v[0])?,
y: number(&v[1])?,
};
}
}
if let Some(v) = transform.get("rotation") {
self.texture_transforms[slot].rotation = number(v)?;
}
}
Ok(())
}
pub fn to_override_osd(&self) -> Result<OSDMap, Error> {
let defaults = Self::default();
let mut map = HashMap::new();
let mut tex = Vec::new();
let mut last = None;
for (index, id) in self.texture_ids.iter().enumerate() {
if *id != UUID::zero() {
last = Some(index);
}
}
if let Some(last) = last {
for id in &self.texture_ids[..=last] {
tex.push(OSD::UUID(*id));
}
map.insert("tex".into(), OSD::Array(tex));
}
if self.base_color_factor != defaults.base_color_factor {
map.insert("bc".into(), color_osd(self.base_color_factor));
}
if self.emissive_factor != defaults.emissive_factor {
map.insert("ec".into(), vector3_osd(self.emissive_factor));
}
if self.metallic_factor != defaults.metallic_factor {
map.insert("mf".into(), OSD::Real(f64::from(self.metallic_factor)));
}
if self.roughness_factor != defaults.roughness_factor {
map.insert("rf".into(), OSD::Real(f64::from(self.roughness_factor)));
}
if self.override_alpha_mode {
map.insert("am".into(), OSD::Integer(self.alpha_mode as i32));
}
if self.alpha_cutoff != defaults.alpha_cutoff {
map.insert("ac".into(), OSD::Real(f64::from(self.alpha_cutoff)));
}
if self.override_double_sided {
map.insert("ds".into(), OSD::Boolean(self.double_sided));
}
let transforms: Vec<_> = self
.texture_transforms
.iter()
.map(|t| {
OSD::Map(HashMap::from([
("o".into(), vector2_osd(t.offset)),
("s".into(), vector2_osd(t.scale)),
("r".into(), OSD::Real(f64::from(t.rotation))),
]))
})
.collect();
if self.texture_transforms.iter().any(|v| !v.is_default()) {
map.insert("ti".into(), OSD::Array(transforms));
}
OSDMap::new_with_dictionary(map)
}
pub fn from_override_osd(data: OSDMap) -> Result<Self, Error> {
let map = data.snapshot();
let mut value = Self::default();
if let Some(OSD::Array(ids)) = map.get("tex") {
for (slot, id) in ids.iter().take(Self::TEXTURE_COUNT as usize).enumerate() {
value.texture_ids[slot] = id.as_uuid()?;
}
}
if let Some(v) = map.get("bc") {
value.base_color_factor = v.as_color4()?;
if value.base_color_factor == Self::default().base_color_factor {
value.base_color_factor.a -= FLOAT_EPSILON;
}
}
if let Some(v) = map.get("ec") {
value.emissive_factor = v.as_vector3()?;
if value.emissive_factor == Vector3::zero() {
value.emissive_factor.x += FLOAT_EPSILON;
}
}
if let Some(v) = map.get("mf") {
value.metallic_factor = (v.as_real()? as f32).min(1.0 - FLOAT_EPSILON);
}
if let Some(v) = map.get("rf") {
value.roughness_factor = (v.as_real()? as f32).min(1.0 - FLOAT_EPSILON);
}
if let Some(v) = map.get("am") {
value.alpha_mode = match v.as_integer()? {
0 => GltfAlphaMode::Opaque,
1 => GltfAlphaMode::Blend,
2 => GltfAlphaMode::Mask,
_ => return Err(Error::Argument),
};
value.override_alpha_mode = true;
}
if let Some(v) = map.get("ac") {
value.alpha_cutoff = v.as_real()? as f32;
if value.alpha_cutoff == 0.5 {
value.alpha_cutoff -= FLOAT_EPSILON;
}
}
if let Some(v) = map.get("ds") {
value.double_sided = v.as_boolean()?;
value.override_double_sided = true;
}
if let Some(OSD::Array(values)) = map.get("ti") {
for (slot, transform) in values.iter().take(Self::TEXTURE_COUNT as usize).enumerate() {
if let OSD::Map(t) = transform {
if let Some(v) = t.get("o") {
value.texture_transforms[slot].offset = v.as_vector2()?;
}
if let Some(v) = t.get("s") {
value.texture_transforms[slot].scale = v.as_vector2()?;
}
if let Some(v) = t.get("r") {
value.texture_transforms[slot].rotation = v.as_real()? as f32;
}
}
}
}
Ok(value)
}
pub const fn asset_type(&self) -> AssetType {
AssetType::Material
}
pub fn alpha_cutoff(&self) -> f32 {
self.alpha_cutoff
}
pub fn set_alpha_cutoff(&mut self, value: f32) {
self.alpha_cutoff = value;
}
pub fn alpha_mode(&self) -> GltfAlphaMode {
self.alpha_mode
}
pub fn set_alpha_mode(&mut self, value: GltfAlphaMode) {
self.alpha_mode = value;
}
pub fn base_color_factor(&self) -> Color4 {
self.base_color_factor
}
pub fn set_base_color_factor(&mut self, value: Color4) {
self.base_color_factor = value;
}
pub fn double_sided(&self) -> bool {
self.double_sided
}
pub fn set_double_sided(&mut self, value: bool) {
self.double_sided = value;
}
pub fn emissive_factor(&self) -> Vector3 {
self.emissive_factor
}
pub fn set_emissive_factor(&mut self, value: Vector3) {
self.emissive_factor = value;
}
pub fn metallic_factor(&self) -> f32 {
self.metallic_factor
}
pub fn set_metallic_factor(&mut self, value: f32) {
self.metallic_factor = value;
}
pub fn roughness_factor(&self) -> f32 {
self.roughness_factor
}
pub fn set_roughness_factor(&mut self, value: f32) {
self.roughness_factor = value;
}
pub fn name(&self) -> String {
self.name.clone()
}
pub fn set_name(&mut self, value: String) {
self.name = value;
}
pub fn override_alpha_mode(&self) -> bool {
self.override_alpha_mode
}
pub fn set_override_alpha_mode(&mut self, value: bool) {
self.override_alpha_mode = value;
}
pub fn override_double_sided(&self) -> bool {
self.override_double_sided
}
pub fn set_override_double_sided(&mut self, value: bool) {
self.override_double_sided = value;
}
pub fn texture_ids(&self) -> Vec<UUID> {
self.texture_ids.clone()
}
pub fn set_texture_ids(&mut self, value: Vec<UUID>) {
self.texture_ids = value;
}
pub fn texture_transforms(&self) -> Vec<GltfTextureTransform> {
self.texture_transforms.clone()
}
pub fn set_texture_transforms(&mut self, value: Vec<GltfTextureTransform>) {
self.texture_transforms = value;
}
}
fn number(value: &Value) -> Result<f32, Error> {
value
.as_f64()
.map(|v| v as f32)
.filter(|v| v.is_finite())
.ok_or(Error::Argument)
}
fn vector2_osd(value: Vector2) -> OSD {
OSD::Array(vec![
OSD::Real(f64::from(value.x)),
OSD::Real(f64::from(value.y)),
])
}
fn vector3_osd(value: Vector3) -> OSD {
OSD::Array(vec![
OSD::Real(f64::from(value.x)),
OSD::Real(f64::from(value.y)),
OSD::Real(f64::from(value.z)),
])
}
fn color_osd(value: Color4) -> OSD {
OSD::Array(vec![
OSD::Real(f64::from(value.r)),
OSD::Real(f64::from(value.g)),
OSD::Real(f64::from(value.b)),
OSD::Real(f64::from(value.a)),
])
}

View File

@@ -0,0 +1,676 @@
//! Native asset value models and their format boundaries.
#![allow(
clippy::format_push_string,
clippy::missing_errors_doc,
clippy::must_use_candidate,
clippy::needless_pass_by_value,
clippy::struct_field_names
)]
use crate::{Error, Permissions};
use libremetaverse_imaging::ManagedImage;
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
use libremetaverse_types::{AssetType, SaleType, UUID, Vector3, WearableType};
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
/// Maximum accepted in-memory asset payload. Network limits are enforced before
/// this boundary too, but constructors are public and must defend themselves.
pub const MAX_ASSET_BYTES: usize = 64 * 1024 * 1024;
fn validate_bytes(bytes: &[u8]) -> Result<(), Error> {
if bytes.len() > MAX_ASSET_BYTES {
Err(Error::Argument)
} else {
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct Asset {
pub asset_data: Vec<u8>,
pub temporary: bool,
asset_id: UUID,
asset_type: AssetType,
}
impl Asset {
pub(crate) fn native_new(
asset_type: AssetType,
asset_id: UUID,
data: Vec<u8>,
) -> Result<Self, Error> {
validate_bytes(&data)?;
Ok(Self {
asset_data: data,
temporary: false,
asset_id,
asset_type,
})
}
pub fn decode(&self) -> Result<bool, Error> {
validate_bytes(&self.asset_data)?;
Ok(true)
}
pub fn encode(&self) -> Result<(), Error> {
validate_bytes(&self.asset_data)
}
pub const fn asset_id(&self) -> UUID {
self.asset_id
}
pub fn set_asset_id(&mut self, value: UUID) {
self.asset_id = value;
}
pub const fn asset_type(&self) -> AssetType {
self.asset_type
}
}
#[derive(Clone, Debug)]
struct RawAsset(Arc<RwLock<Asset>>);
impl RawAsset {
fn empty(asset_type: AssetType) -> Result<Self, Error> {
Ok(Self(Arc::new(RwLock::new(Asset::native_new(
asset_type,
UUID::zero(),
Vec::new(),
)?))))
}
fn with_data(asset_type: AssetType, id: UUID, data: Vec<u8>) -> Result<Self, Error> {
Ok(Self(Arc::new(RwLock::new(Asset::native_new(
asset_type, id, data,
)?))))
}
fn decode(&self) -> Result<bool, Error> {
self.0
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.decode()
}
fn encode(&self) -> Result<(), Error> {
self.0
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.encode()
}
fn bytes(&self) -> Vec<u8> {
self.0
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.asset_data
.clone()
}
fn replace(&self, bytes: Vec<u8>) -> Result<(), Error> {
validate_bytes(&bytes)?;
self.0
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.asset_data = bytes;
Ok(())
}
}
macro_rules! raw_asset {
($name:ident, $kind:expr) => {
#[derive(Clone, Debug)]
pub struct $name {
raw: RawAsset,
}
impl $name {
pub fn new_with_constructor() -> Result<Self, Error> {
Ok(Self {
raw: RawAsset::empty($kind)?,
})
}
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
Ok(Self {
raw: RawAsset::with_data($kind, asset_id, asset_data)?,
})
}
pub fn decode(&self) -> Result<bool, Error> {
self.raw.decode()
}
pub fn encode(&self) -> Result<(), Error> {
self.raw.encode()
}
pub const fn asset_type(&self) -> AssetType {
$kind
}
}
};
}
raw_asset!(AssetAnimation, AssetType::Animation);
raw_asset!(AssetScriptBinary, AssetType::LSLBytecode);
raw_asset!(AssetSound, AssetType::Sound);
impl AssetSound {
pub fn pcm_to_ogg(
pcm_data: Vec<u8>,
sample_rate: i32,
channels: i32,
bits_per_sample: Option<i32>,
) -> Result<Vec<u8>, Error> {
validate_bytes(&pcm_data)?;
if pcm_data.is_empty()
|| sample_rate <= 0
|| !matches!(channels, 1 | 2)
|| !matches!(bits_per_sample.unwrap_or(16), 8 | 16)
{
return Err(Error::Argument);
}
let bits = bits_per_sample.unwrap_or(16);
let bytes_per_sample = usize::try_from(bits / 8).map_err(|_| Error::Argument)?;
let channels = usize::try_from(channels).map_err(|_| Error::Argument)?;
let frame_size = bytes_per_sample
.checked_mul(channels)
.ok_or(Error::Argument)?;
if !pcm_data.len().is_multiple_of(frame_size) {
return Err(Error::Argument);
}
let frequency =
std::num::NonZeroU32::new(u32::try_from(sample_rate).map_err(|_| Error::Argument)?)
.ok_or(Error::Argument)?;
let channel_count =
std::num::NonZeroU8::new(u8::try_from(channels).map_err(|_| Error::Argument)?)
.ok_or(Error::Argument)?;
let mut builder = vorbis_rs::VorbisEncoderBuilder::new_with_serial(
frequency,
channel_count,
Vec::new(),
0x4d43_4154,
);
let mut encoder = builder.build().map_err(|_| Error::InvalidOperation)?;
let frame_count = pcm_data.len() / frame_size;
for first_frame in (0..frame_count).step_by(1024) {
let end_frame = first_frame.saturating_add(1024).min(frame_count);
let mut planar = vec![Vec::with_capacity(end_frame - first_frame); channels];
for frame in first_frame..end_frame {
for (channel, samples) in planar.iter_mut().enumerate() {
let offset = frame * frame_size + channel * bytes_per_sample;
let sample = if bits == 8 {
(f32::from(pcm_data[offset]) - 128.0) / 128.0
} else {
f32::from(i16::from_le_bytes([pcm_data[offset], pcm_data[offset + 1]]))
/ 32768.0
};
samples.push(sample);
}
}
encoder
.encode_audio_block(&planar)
.map_err(|_| Error::InvalidOperation)?;
}
let ogg_data = encoder.finish().map_err(|_| Error::InvalidOperation)?;
validate_bytes(&ogg_data)?;
Ok(ogg_data)
}
}
#[derive(Clone, Debug)]
pub struct AssetMutable {
raw: RawAsset,
pub current_type: AssetType,
}
impl AssetMutable {
pub fn new_with_asset_type(type_: AssetType) -> Result<Self, Error> {
Ok(Self {
raw: RawAsset::empty(type_)?,
current_type: type_,
})
}
pub fn new_with_asset_type_uuid_bytes(
type_: AssetType,
asset_id: UUID,
asset_data: Vec<u8>,
) -> Result<Self, Error> {
Ok(Self {
raw: RawAsset::with_data(type_, asset_id, asset_data)?,
current_type: type_,
})
}
pub fn decode(&self) -> Result<bool, Error> {
self.raw.decode()
}
pub fn encode(&self) -> Result<(), Error> {
self.raw.encode()
}
pub const fn asset_type(&self) -> AssetType {
self.current_type
}
}
#[derive(Clone, Debug)]
pub struct AssetCallingCard {
raw: RawAsset,
pub avatar_id: UUID,
}
impl AssetCallingCard {
pub fn new_with_constructor() -> Result<Self, Error> {
Self::new_with_uuid(UUID::zero())
}
pub fn new_with_uuid(avatar_id: UUID) -> Result<Self, Error> {
let value = Self {
raw: RawAsset::empty(AssetType::CallingCard)?,
avatar_id,
};
value.encode()?;
Ok(value)
}
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
let raw = RawAsset::with_data(AssetType::CallingCard, asset_id, asset_data)?;
let text = String::from_utf8(raw.bytes()).map_err(|_| Error::Argument)?;
let id = text
.lines()
.find_map(|line| line.strip_prefix("avatar_id "))
.ok_or(Error::Argument)
.and_then(|id| UUID::new_with_string(id.trim().to_owned()))?;
Ok(Self { raw, avatar_id: id })
}
pub fn decode(&self) -> Result<bool, Error> {
let text = String::from_utf8(self.raw.bytes()).map_err(|_| Error::Argument)?;
Ok(text.lines().any(|line| {
line.strip_prefix("avatar_id ")
.and_then(|id| UUID::new_with_string(id.trim().to_owned()).ok())
== Some(self.avatar_id)
}))
}
pub fn encode(&self) -> Result<(), Error> {
self.raw.replace(
format!(
"Linden text version 2\n{{\navatar_id {}\n}}\n",
self.avatar_id
)
.into_bytes(),
)
}
pub const fn asset_type(&self) -> AssetType {
AssetType::CallingCard
}
}
#[derive(Clone, Debug)]
pub struct AssetLandmark {
raw: RawAsset,
pub position: Vector3,
pub region_id: UUID,
}
impl AssetLandmark {
pub fn new_with_constructor() -> Result<Self, Error> {
Ok(Self {
raw: RawAsset::empty(AssetType::Landmark)?,
position: Vector3::zero(),
region_id: UUID::zero(),
})
}
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
let raw = RawAsset::with_data(AssetType::Landmark, asset_id, asset_data)?;
let text = String::from_utf8(raw.bytes()).map_err(|_| Error::Argument)?;
let region = text
.lines()
.find_map(|line| line.strip_prefix("region_id "))
.ok_or(Error::Argument)
.and_then(|id| UUID::new_with_string(id.trim().to_owned()))?;
let values: Vec<f32> = text
.lines()
.find_map(|line| line.strip_prefix("local_pos "))
.ok_or(Error::Argument)?
.split_whitespace()
.map(str::parse)
.collect::<Result<_, _>>()
.map_err(|_| Error::Argument)?;
if values.len() != 3 {
return Err(Error::Argument);
}
Ok(Self {
raw,
region_id: region,
position: Vector3 {
x: values[0],
y: values[1],
z: values[2],
},
})
}
pub fn decode(&self) -> Result<bool, Error> {
Ok(Self::new_with_uuid_bytes(UUID::zero(), self.raw.bytes()).is_ok())
}
pub fn encode(&self) -> Result<(), Error> {
self.raw.replace(
format!(
"Landmark version 2\nregion_id {}\nlocal_pos {} {} {}\n",
self.region_id, self.position.x, self.position.y, self.position.z
)
.into_bytes(),
)
}
pub const fn asset_type(&self) -> AssetType {
AssetType::Landmark
}
}
#[derive(Clone, Debug)]
pub struct AssetScriptText {
raw: RawAsset,
pub source: Option<String>,
}
impl AssetScriptText {
pub fn new_with_constructor() -> Result<Self, Error> {
Ok(Self {
raw: RawAsset::empty(AssetType::LSLText)?,
source: None,
})
}
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
let source = String::from_utf8(asset_data.clone()).map_err(|_| Error::Argument)?;
Ok(Self {
raw: RawAsset::with_data(AssetType::LSLText, asset_id, asset_data)?,
source: Some(source),
})
}
pub fn decode(&self) -> Result<bool, Error> {
Ok(String::from_utf8(self.raw.bytes()).is_ok())
}
pub fn encode(&self) -> Result<(), Error> {
self.raw
.replace(self.source.clone().unwrap_or_default().into_bytes())
}
pub const fn asset_type(&self) -> AssetType {
AssetType::LSLText
}
}
#[derive(Clone, Debug)]
pub struct AssetSettings {
raw: RawAsset,
pub settings: Option<OSD>,
}
impl AssetSettings {
pub fn new_with_constructor() -> Result<Self, Error> {
Ok(Self {
raw: RawAsset::empty(AssetType::Settings)?,
settings: None,
})
}
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
let text = String::from_utf8(asset_data.clone()).map_err(|_| Error::Argument)?;
let settings = OSDParser::deserialize_with_string(text)?;
Ok(Self {
raw: RawAsset::with_data(AssetType::Settings, asset_id, asset_data)?,
settings: Some(settings),
})
}
pub fn decode(&self) -> Result<bool, Error> {
Ok(OSDParser::deserialize_with_bytes(self.raw.bytes()).is_ok())
}
pub fn encode(&self) -> Result<(), Error> {
let value = self.settings.clone().unwrap_or_default();
self.raw.replace(value.as_string()?.into_bytes())
}
pub const fn asset_type(&self) -> AssetType {
AssetType::Settings
}
}
pub struct AssetMesh {
raw: RawAsset,
pub mesh_data: OSDMap,
}
impl AssetMesh {
pub fn new_with_constructor() -> Result<Self, Error> {
Ok(Self {
raw: RawAsset::empty(AssetType::Mesh)?,
mesh_data: OSDMap::new_with_constructor()?,
})
}
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
let raw = RawAsset::with_data(AssetType::Mesh, asset_id, asset_data.clone())?;
let mesh_data = match OSDParser::deserialize_llsd_binary_with_bytes(asset_data)? {
OSD::Map(map) => OSDMap::new_with_dictionary(map)?,
_ => return Err(Error::Argument),
};
Ok(Self { raw, mesh_data })
}
pub fn decode(&self) -> Result<bool, Error> {
Ok(matches!(
OSDParser::deserialize_llsd_binary_with_bytes(self.raw.bytes()),
Ok(OSD::Map(_))
))
}
pub fn encode(&self) -> Result<(), Error> {
self.raw
.replace(OSDParser::serialize_llsd_binary_with_osd(OSD::Map(
self.mesh_data.snapshot(),
))?)
}
pub const fn asset_type(&self) -> AssetType {
AssetType::Mesh
}
}
pub struct AssetTexture {
raw: RawAsset,
pub components: i32,
pub image: Option<ManagedImage>,
}
impl AssetTexture {
pub fn new_with_constructor() -> Result<Self, Error> {
Ok(Self {
raw: RawAsset::empty(AssetType::Texture)?,
components: 0,
image: None,
})
}
pub fn new_with_managed_image(image: ManagedImage) -> Result<Self, Error> {
Ok(Self {
raw: RawAsset::empty(AssetType::Texture)?,
components: 0,
image: Some(image),
})
}
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
Ok(Self {
raw: RawAsset::with_data(AssetType::Texture, asset_id, asset_data)?,
components: 0,
image: None,
})
}
pub fn decode(&self) -> Result<bool, Error> {
#[cfg(feature = "jpeg2000")]
{
Ok(libremetaverse_imaging::J2kCodec::decode_bytes(
&self.raw.bytes(),
libremetaverse_imaging::J2kDecodeOptions::default(),
)
.is_ok())
}
#[cfg(not(feature = "jpeg2000"))]
{
validate_bytes(&self.raw.bytes())?;
Err(Error::InvalidOperation)
}
}
pub fn encode(&self) -> Result<(), Error> {
let image = self.image.as_ref().ok_or(Error::InvalidOperation)?;
#[cfg(feature = "jpeg2000")]
{
let encoded = libremetaverse_imaging::J2kCodec::encode(
image,
libremetaverse_imaging::J2kEncodeOptions::default(),
)?;
self.raw.replace(encoded)
}
#[cfg(not(feature = "jpeg2000"))]
{
let _ = image;
Err(Error::InvalidOperation)
}
}
pub const fn asset_type(&self) -> AssetType {
AssetType::Texture
}
}
pub struct AssetWearable {
raw: RawAsset,
pub creator: UUID,
pub description: String,
pub for_sale: SaleType,
pub group: UUID,
pub group_owned: bool,
pub last_owner: UUID,
pub name: String,
pub owner: UUID,
pub params: HashMap<i32, f32>,
pub permissions: Permissions,
pub sale_price: i32,
pub textures: HashMap<crate::AvatarTextureIndex, UUID>,
pub wearable_type: WearableType,
}
impl AssetWearable {
fn empty(kind: AssetType) -> Result<Self, Error> {
Ok(Self {
raw: RawAsset::empty(kind)?,
creator: UUID::zero(),
description: String::new(),
for_sale: SaleType::Not,
group: UUID::zero(),
group_owned: false,
last_owner: UUID::zero(),
name: String::new(),
owner: UUID::zero(),
params: HashMap::new(),
permissions: Permissions::default(),
sale_price: 0,
textures: HashMap::new(),
wearable_type: WearableType::Invalid,
})
}
pub fn decode(&self) -> Result<bool, Error> {
Ok(std::str::from_utf8(&self.raw.bytes()).is_ok())
}
pub fn encode(&self) -> Result<(), Error> {
let mut text = format!(
"LLWearable version 22\n{}\n{}\ntype {}\nparameters {}\n",
self.name,
self.description,
self.wearable_type as i32,
self.params.len()
);
let mut params: Vec<_> = self.params.iter().collect();
params.sort_unstable_by_key(|(id, _)| **id);
for (id, value) in params {
text.push_str(&format!("{id} {value}\n"));
}
text.push_str(&format!("textures {}\n", self.textures.len()));
self.raw.replace(text.into_bytes())
}
}
pub struct AssetBodypart {
wearable: AssetWearable,
}
impl AssetBodypart {
pub fn new_with_constructor() -> Result<Self, Error> {
Ok(Self {
wearable: AssetWearable::empty(AssetType::Bodypart)?,
})
}
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
let mut wearable = AssetWearable::empty(AssetType::Bodypart)?;
wearable.raw = RawAsset::with_data(AssetType::Bodypart, asset_id, asset_data)?;
Ok(Self { wearable })
}
pub const fn asset_type(&self) -> AssetType {
AssetType::Bodypart
}
pub fn decode(&self) -> Result<bool, Error> {
self.wearable.decode()
}
pub fn encode(&self) -> Result<(), Error> {
self.wearable.encode()
}
}
pub struct AssetClothing {
wearable: AssetWearable,
}
impl AssetClothing {
pub fn new_with_constructor() -> Result<Self, Error> {
Ok(Self {
wearable: AssetWearable::empty(AssetType::Clothing)?,
})
}
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
let mut wearable = AssetWearable::empty(AssetType::Clothing)?;
wearable.raw = RawAsset::with_data(AssetType::Clothing, asset_id, asset_data)?;
Ok(Self { wearable })
}
pub const fn asset_type(&self) -> AssetType {
AssetType::Clothing
}
pub fn decode(&self) -> Result<bool, Error> {
self.wearable.decode()
}
pub fn encode(&self) -> Result<(), Error> {
self.wearable.encode()
}
}
pub struct AssetNotecard {
raw: RawAsset,
pub body_text: String,
pub embedded_items: Vec<crate::InventoryItem>,
}
impl AssetNotecard {
pub fn new_with_constructor() -> Result<Self, Error> {
Ok(Self {
raw: RawAsset::empty(AssetType::Notecard)?,
body_text: String::new(),
embedded_items: Vec::new(),
})
}
pub fn new_with_uuid_bytes(asset_id: UUID, asset_data: Vec<u8>) -> Result<Self, Error> {
let text = String::from_utf8(asset_data.clone()).map_err(|_| Error::Argument)?;
let marker = "Text length ";
let start = text.find(marker).ok_or(Error::Argument)? + marker.len();
let line_end = text[start..].find('\n').ok_or(Error::Argument)? + start;
let length: usize = text[start..line_end]
.trim()
.parse()
.map_err(|_| Error::Argument)?;
let body_start = line_end + 1;
let body_end = body_start.checked_add(length).ok_or(Error::Argument)?;
let body_text = text
.get(body_start..body_end)
.ok_or(Error::Argument)?
.to_owned();
Ok(Self {
raw: RawAsset::with_data(AssetType::Notecard, asset_id, asset_data)?,
body_text,
embedded_items: Vec::new(),
})
}
pub fn decode(&self) -> Result<bool, Error> {
Ok(Self::new_with_uuid_bytes(UUID::zero(), self.raw.bytes()).is_ok())
}
pub fn encode(&self) -> Result<(), Error> {
if !self.embedded_items.is_empty() {
return Err(Error::InvalidOperation);
}
let bytes = self.body_text.as_bytes();
self.raw.replace(format!("Linden text version 2\n{{\nLLEmbeddedItems version 1\n{{\ncount 0\n}}\nText length {}\n{}\n}}\n", bytes.len(), self.body_text).into_bytes())
}
pub const fn asset_type(&self) -> AssetType {
AssetType::Notecard
}
}

View File

@@ -0,0 +1,133 @@
use crate::assets::{AssetMesh, AssetMutable, AssetSound, AssetTexture};
use crate::{
AssetCache, AssetCacheComputeAssetCacheFilenameDelegate, Error, GridClient, ImageCodec,
};
use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels};
use libremetaverse_types::{AssetType, UUID};
use std::fs;
use std::path::PathBuf;
fn fixture(max_size: i64) -> (GridClient, AssetCache, PathBuf) {
let path =
std::env::temp_dir().join(format!("metacrate-asset-cache-{}", UUID::random().unwrap()));
let mut client = GridClient::new().unwrap();
let settings = client.settings();
settings.asset_cache_mut().dir = path.to_string_lossy().into_owned();
settings.asset_cache_mut().enabled = true;
settings.asset_cache_mut().max_size = max_size;
let cache = AssetCache::new(client.clone()).unwrap();
(client, cache, path)
}
#[test]
fn asset_models_retain_type_and_reject_malformed_meshes() {
let asset = AssetMutable::new_with_asset_type_uuid_bytes(
AssetType::Sound,
UUID::random().unwrap(),
vec![1, 2, 3],
)
.unwrap();
assert_eq!(asset.asset_type(), AssetType::Sound);
assert!(asset.decode().unwrap());
assert!(AssetMesh::new_with_uuid_bytes(UUID::random().unwrap(), b"not-llsd".to_vec()).is_err());
}
#[test]
fn sound_and_texture_codecs_produce_real_valid_payloads() {
let ogg = AssetSound::pcm_to_ogg(vec![0_u8; 256 * 2], 44_100, 1, Some(16)).unwrap();
assert!(ogg.starts_with(b"OggS"));
let mut image = ManagedImage::new(2, 2, ManagedImageImageChannels::COLOR).unwrap();
image.red.fill(255);
let texture = AssetTexture::new_with_managed_image(image).unwrap();
texture.encode().unwrap();
assert!(texture.decode().unwrap());
}
#[test]
fn cache_writes_are_atomic_and_corruption_is_typed() {
let (_client, cache, path) = fixture(1024);
let id = UUID::random().unwrap();
cache
.save_asset_to_cache_with_uuid_bytes(id, vec![1, 2, 3, 4])
.unwrap();
cache
.save_asset_to_cache_with_uuid_bytes(id, vec![9, 9, 9, 9])
.unwrap();
assert_eq!(
cache.get_cached_asset_bytes_with_uuid(id).unwrap(),
Some(vec![1, 2, 3, 4])
);
let image = cache.get_cached_image(id).unwrap().unwrap();
assert_eq!(image.codec, ImageCodec::J2C);
assert_eq!(image.base.asset_data, vec![1, 2, 3, 4]);
let corrupt = UUID::random().unwrap();
fs::write(path.join(corrupt.to_string()), []).unwrap();
assert_eq!(
cache.get_cached_asset_bytes_with_uuid(corrupt),
Err(Error::Argument)
);
fs::remove_dir_all(path).unwrap();
}
#[test]
fn custom_cache_naming_is_used_but_cannot_escape_the_cache_directory() {
let (_client, mut cache, path) = fixture(1024);
cache.compute_asset_cache_filename = Some(
AssetCacheComputeAssetCacheFilenameDelegate::from_handler(|directory, id| {
Ok(PathBuf::from(directory)
.join(format!("{id}.asset"))
.to_string_lossy()
.into_owned())
}),
);
let id = UUID::random().unwrap();
cache
.save_asset_to_cache_with_uuid_bytes(id, vec![7, 8, 9])
.unwrap();
assert_eq!(
cache.get_cached_asset_bytes_with_uuid(id).unwrap(),
Some(vec![7, 8, 9])
);
assert!(path.join(format!("{id}.asset")).is_file());
cache.clear().unwrap();
assert!(!path.join(format!("{id}.asset")).exists());
cache.compute_asset_cache_filename = Some(
AssetCacheComputeAssetCacheFilenameDelegate::from_handler(|directory, _| {
Ok(PathBuf::from(directory)
.join("..")
.join("escape")
.to_string_lossy()
.into_owned())
}),
);
assert_eq!(
cache.has_asset(UUID::random().unwrap()),
Err(Error::Argument)
);
fs::remove_dir_all(path).unwrap();
}
#[tokio::test]
async fn cache_pruning_reclaims_old_entries_to_below_the_limit() {
let (_client, cache, path) = fixture(12);
cache
.save_asset_to_cache_with_uuid_bytes(UUID::random().unwrap(), vec![1; 8])
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(5));
cache
.save_asset_to_cache_with_uuid_bytes(UUID::random().unwrap(), vec![2; 8])
.unwrap();
cache.prune(None).await.unwrap();
let bytes: u64 = fs::read_dir(&path)
.unwrap()
.filter_map(Result::ok)
.filter_map(|entry| entry.metadata().ok())
.map(|metadata| metadata.len())
.sum();
assert!(bytes <= 10);
fs::remove_dir_all(path).unwrap();
}

View File

@@ -120,6 +120,7 @@ struct ClientRuntime {
agent_manager: Mutex<std::sync::Weak<crate::agent_manager::AgentManagerInner>>,
inventory_manager: Mutex<Option<Arc<crate::inventory_manager::InventoryManagerInner>>>,
inventory_ais_client: Mutex<Option<crate::inventory_ais::InventoryAISClient>>,
asset_manager: Mutex<Option<Arc<crate::asset_manager::AssetManagerInner>>>,
shutdown_complete: Condvar,
shutdown_wait: Mutex<()>,
}
@@ -150,6 +151,7 @@ impl ClientRuntime {
agent_manager: Mutex::new(std::sync::Weak::new()),
inventory_manager: Mutex::new(None),
inventory_ais_client: Mutex::new(None),
asset_manager: Mutex::new(None),
shutdown_complete: Condvar::new(),
shutdown_wait: Mutex::new(()),
}
@@ -447,6 +449,29 @@ impl GridClient {
Ok(manager)
}
pub(crate) fn native_assets(&self) -> Result<crate::AssetManager, crate::Error> {
let mut cached = self
.runtime
.asset_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(inner) = cached.as_ref() {
return crate::asset_manager::AssetManager::native_from_inner(Arc::clone(inner));
}
let manager = crate::asset_manager::AssetManager::new(Some(self.clone()))?;
*cached = Some(manager.native_inner());
Ok(manager)
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn native_set_assets(&mut self, value: crate::AssetManager) {
*self
.runtime
.asset_manager
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value.native_inner());
}
pub(crate) fn native_ais_client(&self) -> Result<crate::InventoryAISClient, crate::Error> {
let mut cached = self
.runtime

View File

@@ -5,8 +5,7 @@
use crate::{Error, GridClient, HttpCapsClientProgressReport};
use libremetaverse_types::compat::{
CancellationToken, CancellationTokenSource, HttpResponse, IProgress, Subscription,
TaskCompletionSource, Uri,
CancellationToken, CancellationTokenSource, HttpResponse, IProgress, TaskCompletionSource, Uri,
};
use std::collections::HashMap;
use std::fmt;
@@ -82,7 +81,6 @@ impl fmt::Debug for DownloadRequest {
struct ActiveDownload {
cancellation: CancellationTokenSource,
cancellation_guards: Mutex<Vec<Subscription>>,
progress: Mutex<Vec<Arc<dyn IProgress<HttpCapsClientProgressReport>>>>,
completion_sources: Mutex<Vec<DownloadCompletion>>,
waiters: Mutex<Vec<oneshot::Sender<DownloadResult>>>,
@@ -93,7 +91,6 @@ impl ActiveDownload {
fn new() -> Self {
Self {
cancellation: CancellationTokenSource::new(),
cancellation_guards: Mutex::new(Vec::new()),
progress: Mutex::new(Vec::new()),
completion_sources: Mutex::new(Vec::new()),
waiters: Mutex::new(Vec::new()),
@@ -101,12 +98,6 @@ impl ActiveDownload {
}
}
fn attach_cancellation(&self, token: &CancellationToken) {
let cancellation = self.cancellation.clone();
let guard = token.register_callback(Arc::new(move || cancellation.cancel()));
mutex(&self.cancellation_guards).push(guard);
}
fn attach_progress(&self, progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>) {
if let Some(progress) = progress {
mutex(&self.progress).push(Arc::from(progress));
@@ -146,7 +137,6 @@ impl ActiveDownload {
}
}
mutex(&self.progress).clear();
mutex(&self.cancellation_guards).clear();
}
fn report(&self, report: HttpCapsClientProgressReport) {
@@ -371,13 +361,8 @@ impl DownloadManager {
request: DownloadRequest,
) -> DownloadResult {
let cancellation = request.cancellation_token.clone();
let (active, receiver) = self.enqueue(request, true)?;
await_download(
active,
receiver.ok_or(Error::InvalidOperation)?,
cancellation,
)
.await
let (_, receiver) = self.enqueue(request, true)?;
await_download(receiver.ok_or(Error::InvalidOperation)?, cancellation).await
}
pub async fn queue_download_with_uri_string_i_progress_cancellation_token_int32(
@@ -392,13 +377,8 @@ impl DownloadManager {
let mut request = DownloadRequest::new(address, content_type, progress_callback)?;
request.cancellation_token = cancellation.clone();
request.retries = retries.unwrap_or(5).max(0);
let (active, receiver) = self.enqueue(request, true)?;
await_download(
active,
receiver.ok_or(Error::InvalidOperation)?,
cancellation,
)
.await
let (_, receiver) = self.enqueue(request, true)?;
await_download(receiver.ok_or(Error::InvalidOperation)?, cancellation).await
}
pub async fn download_with_uri_string_i_progress_cancellation_token(
@@ -466,7 +446,6 @@ impl DownloadManager {
}
return Err(Error::Cancelled);
};
active.attach_cancellation(&request.cancellation_token);
active.attach_progress(request.download_progress_callback.take());
active.attach_completion_source(request.completion_tcs.take());
let receiver = with_waiter.then(|| active.add_waiter());
@@ -507,14 +486,12 @@ impl fmt::Debug for DownloadManager {
}
async fn await_download(
active: Arc<ActiveDownload>,
receiver: oneshot::Receiver<DownloadResult>,
cancellation: CancellationToken,
) -> DownloadResult {
tokio::select! {
result = receiver => result.map_err(|_| Error::Cancelled)?,
() = cancellation.cancelled() => {
active.cancellation.cancel();
Err(Error::Cancelled)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -4396,6 +4396,117 @@ fn parse_task_inventory_values(
Ok(result)
}
impl InventoryManager {
pub(crate) async fn native_update_material_inventory(
&self,
material: crate::assets::AssetMaterial,
material_item_id: UUID,
task_id: Option<UUID>,
cancellation_token: Option<CancellationToken>,
progress: Option<
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
>,
) -> Result<(bool, String, UUID, UUID), Error> {
let client = self.client()?;
let simulator = client
.native_network()?
.current_sim()
.ok_or(Error::InvalidOperation)?;
let caps = simulator.native_caps().ok_or(Error::InvalidOperation)?;
let name = if task_id.is_some() {
"UpdateMaterialTaskInventory"
} else {
"UpdateMaterialAgentInventory"
};
let deadline = Instant::now() + Duration::from_secs(2);
let uri = loop {
if let Some(uri) = caps.capability_uri(name.to_owned())? {
break uri;
}
if caps.seed_request_finished() || Instant::now() >= deadline {
return Err(Error::InvalidOperation);
}
std::thread::sleep(Duration::from_millis(1));
};
let mut query = HashMap::from([("item_id".to_owned(), OSD::UUID(material_item_id))]);
if let Some(task_id) = task_id {
query.insert("task_id".to_owned(), OSD::UUID(task_id));
}
let payload = OSDParser::serialize_llsd_xml_bytes(OSD::Map(query))?;
let token = cancellation_token.unwrap_or_default();
let (response, metadata) = client
.native_http_caps_client()
.post_with_uri_string_bytes_cancellation_token_i_progress(
uri,
"application/llsd+xml".into(),
payload,
token.clone(),
None,
)
.await?;
if !(200..300).contains(&response.status_code) {
return Ok((
false,
format!("HTTP {}", response.status_code),
UUID::zero(),
UUID::zero(),
));
}
let meta: serde_json::Value =
serde_json::from_slice(&metadata).map_err(|_| Error::Argument)?;
let status = meta
.get("state")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.to_owned();
let uploader = meta
.get("uploader")
.and_then(serde_json::Value::as_str)
.ok_or(Error::Argument)?
.to_owned();
let data = material.to_json()?.into_bytes();
let (upload_response, response_body) = client
.native_http_caps_client()
.post_with_uri_string_bytes_cancellation_token_i_progress(
Uri(uploader),
"application/octet-stream".into(),
data,
token,
progress,
)
.await?;
if !(200..300).contains(&upload_response.status_code) {
return Ok((
false,
format!("HTTP {}", upload_response.status_code),
material_item_id,
UUID::zero(),
));
}
let result: serde_json::Value =
serde_json::from_slice(&response_body).map_err(|_| Error::Argument)?;
let final_status = result
.get("state")
.and_then(serde_json::Value::as_str)
.unwrap_or(&status)
.to_owned();
let asset_id = result
.get("new_asset")
.or_else(|| result.get("new_asset_id"))
.and_then(serde_json::Value::as_str)
.map(|id| UUID::new_with_string(id.to_owned()))
.transpose()?
.unwrap_or_else(UUID::zero);
let item_id = result
.get("item_id")
.and_then(serde_json::Value::as_str)
.map(|id| UUID::new_with_string(id.to_owned()))
.transpose()?
.unwrap_or(material_item_id);
Ok((final_status == "complete", final_status, asset_id, item_id))
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -7,6 +7,10 @@ mod attention_catalog;
mod agent_manager;
mod agent_messages;
mod agent_movement;
mod asset_cache;
mod asset_manager;
mod asset_material;
mod asset_models;
mod bit_pack;
mod caps;
mod caps_http;
@@ -27,6 +31,7 @@ mod login;
mod message_codec;
mod message_decoder;
mod network_manager;
mod object_material;
#[rustfmt::skip]
pub mod packet_catalog;
mod packet_wire;
@@ -35,6 +40,7 @@ mod skeleton;
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
mod skeleton_catalog;
mod targa;
mod transfers;
mod udp_transport;
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
mod visual_catalog;
@@ -44,6 +50,8 @@ mod appearance_baker_semantics;
#[cfg(test)]
mod asset_archive_semantics;
#[cfg(test)]
mod asset_pipeline_semantics;
#[cfg(test)]
mod avatar_animesh_semantics;
#[cfg(test)]
mod batch_link_internal_semantics;

View File

@@ -0,0 +1,192 @@
//! `ModifyMaterialParams` capability integration.
#![allow(
clippy::missing_errors_doc,
clippy::must_use_candidate,
clippy::option_option
)]
use crate::assets::AssetMaterial;
use crate::{Error, GridClient, Simulator};
use libremetaverse_structured_data::{OSD, OSDParser};
use libremetaverse_types::{UUID, compat::CancellationToken};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Clone, Debug)]
pub struct GLTFMaterialUpdate {
asset_id: Option<Option<UUID>>,
object_id: UUID,
override_: Option<AssetMaterial>,
side: i32,
}
impl GLTFMaterialUpdate {
pub fn new() -> Result<Self, Error> {
Ok(Self {
asset_id: None,
object_id: UUID::zero(),
override_: None,
side: 0,
})
}
pub fn asset_id(&self) -> Option<Option<UUID>> {
self.asset_id
}
pub fn set_asset_id(&mut self, value: Option<Option<UUID>>) {
self.asset_id = value;
}
pub const fn object_id(&self) -> UUID {
self.object_id
}
pub fn set_object_id(&mut self, value: UUID) {
self.object_id = value;
}
pub fn override_(&self) -> Option<AssetMaterial> {
self.override_.clone()
}
pub fn set_override_(&mut self, value: Option<AssetMaterial>) {
self.override_ = value;
}
pub const fn side(&self) -> i32 {
self.side
}
pub fn set_side(&mut self, value: i32) {
self.side = value;
}
}
pub struct ObjectManager {
client: crate::client_core::ClientWeakHandle,
}
impl ObjectManager {
pub(crate) fn native_new(client: Option<Arc<GridClient>>) -> Result<Self, Error> {
Ok(Self {
client: client.ok_or(Error::ArgumentNull)?.native_weak_handle(),
})
}
fn client(&self) -> Result<GridClient, Error> {
self.client.upgrade().ok_or(Error::InvalidOperation)
}
pub(crate) async fn native_send_material_updates(
&self,
sim: Simulator,
updates: Box<dyn Iterator<Item = GLTFMaterialUpdate>>,
cancellation_token: Option<CancellationToken>,
) -> Result<bool, Error> {
let Some(caps) = sim.native_caps() else {
return Ok(false);
};
let deadline = Instant::now() + Duration::from_secs(2);
let uri = loop {
if let Some(uri) = caps.capability_uri("ModifyMaterialParams".into())? {
break uri;
}
if caps.seed_request_finished() || Instant::now() >= deadline {
return Ok(false);
}
std::thread::sleep(Duration::from_millis(1));
};
let mut body = Vec::new();
for update in updates {
if update.side < 0 {
return Err(Error::Argument);
}
let mut entry = HashMap::from([
("object_id".into(), OSD::UUID(update.object_id)),
("side".into(), OSD::Integer(update.side)),
]);
if let Some(Some(asset_id)) = update.asset_id {
entry.insert("asset_id".into(), OSD::UUID(asset_id));
}
if let Some(material) = update.override_ {
entry.insert("gltf_json".into(), OSD::String(material.to_json()?));
} else if update.asset_id.flatten().is_none() {
entry.insert("gltf_json".into(), OSD::String(String::new()));
}
body.push(OSD::Map(entry));
}
if body.is_empty() {
return Err(Error::Argument);
}
let payload = OSDParser::serialize_llsd_xml_bytes(OSD::Array(body))?;
let client = self.client()?;
let (response, bytes) = client
.native_http_caps_client()
.post_with_uri_string_bytes_cancellation_token_i_progress(
uri,
"application/llsd+xml".into(),
payload,
cancellation_token.unwrap_or_default(),
None,
)
.await?;
if !(200..300).contains(&response.status_code) {
return Ok(false);
}
let response: serde_json::Value =
serde_json::from_slice(&bytes).map_err(|_| Error::Argument)?;
Ok(response
.get("success")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false))
}
pub(crate) async fn native_set_material_override(
&self,
sim: Simulator,
object_id: UUID,
side: i32,
override_material: AssetMaterial,
cancellation_token: Option<CancellationToken>,
) -> Result<bool, Error> {
let mut update = GLTFMaterialUpdate::new()?;
update.object_id = object_id;
update.side = side;
update.override_ = Some(override_material);
self.native_send_material_updates(
sim,
Box::new(std::iter::once(update)),
cancellation_token,
)
.await
}
pub(crate) async fn native_clear_material_override(
&self,
sim: Simulator,
object_id: UUID,
side: i32,
cancellation_token: Option<CancellationToken>,
) -> Result<bool, Error> {
let mut update = GLTFMaterialUpdate::new()?;
update.object_id = object_id;
update.side = side;
self.native_send_material_updates(
sim,
Box::new(std::iter::once(update)),
cancellation_token,
)
.await
}
pub(crate) async fn native_apply_material(
&self,
sim: Simulator,
object_id: UUID,
side: i32,
material_asset_id: UUID,
override_material: Option<AssetMaterial>,
cancellation_token: Option<CancellationToken>,
) -> Result<bool, Error> {
let mut update = GLTFMaterialUpdate::new()?;
update.object_id = object_id;
update.side = side;
update.asset_id = Some(Some(material_asset_id));
update.override_ = override_material;
self.native_send_material_updates(
sim,
Box::new(std::iter::once(update)),
cancellation_token,
)
.await
}
}

View File

@@ -0,0 +1,226 @@
//! Asset transfer state shared by HTTP and LLUDP pipelines.
#![allow(clippy::missing_errors_doc, clippy::must_use_candidate)]
use crate::{
ChannelType, Error, ImageCodec, ImageType, Simulator, SourceType, StatusCode, TargetType,
TransferError,
};
use libremetaverse_types::compat::SortedList;
use libremetaverse_types::{AssetType, UUID};
use std::collections::HashMap;
use std::sync::RwLock;
use std::time::Instant;
#[derive(Clone, Debug)]
pub struct Transfer {
pub asset_data: Vec<u8>,
pub asset_type: AssetType,
pub id: UUID,
pub size: i32,
pub success: bool,
pub transferred: i32,
pub(crate) last_packet: Instant,
}
impl Transfer {
pub fn new() -> Result<Self, Error> {
Ok(Self {
asset_data: Vec::new(),
asset_type: AssetType::Unknown,
id: UUID::zero(),
size: 0,
success: false,
transferred: 0,
last_packet: Instant::now(),
})
}
pub fn time_since_last_packet(&self) -> i32 {
i32::try_from(self.last_packet.elapsed().as_millis()).unwrap_or(i32::MAX)
}
pub fn set_time_since_last_packet(&mut self, value: i32) {
self.last_packet = Instant::now()
.checked_sub(std::time::Duration::from_millis(
u64::try_from(value.max(0)).unwrap_or(0),
))
.unwrap_or_else(Instant::now);
}
}
#[derive(Debug)]
pub struct AssetDownload {
pub asset_id: UUID,
pub channel: ChannelType,
pub priority: f32,
pub simulator: Option<Simulator>,
pub source: SourceType,
pub status: StatusCode,
pub target: TargetType,
pub next_packet: i32,
pub out_of_order_packets: RwLock<HashMap<i32, Vec<u8>>>,
pub base: Transfer,
}
impl AssetDownload {
pub fn new() -> Result<Self, Error> {
Ok(Self {
asset_id: UUID::zero(),
channel: ChannelType::Asset,
priority: 0.0,
simulator: None,
source: SourceType::Asset,
status: StatusCode::Unknown,
target: TargetType::Unknown,
next_packet: 0,
out_of_order_packets: RwLock::new(HashMap::new()),
base: Transfer::new()?,
})
}
}
#[derive(Debug)]
pub struct ImageDownload {
pub codec: ImageCodec,
pub discard_level: i32,
pub image_type: ImageType,
pub packet_count: u16,
pub packets_seen: SortedList<u16, u16>,
pub priority: f32,
pub simulator: Option<Simulator>,
pub base: Transfer,
}
impl ImageDownload {
pub fn new() -> Result<Self, Error> {
Ok(Self {
codec: ImageCodec::Invalid,
discard_level: 0,
image_type: ImageType::Normal,
packet_count: 0,
packets_seen: SortedList(std::collections::BTreeMap::new()),
priority: 0.0,
simulator: None,
base: Transfer::new()?,
})
}
}
#[derive(Clone, Debug)]
pub struct AssetUpload {
pub asset_id: UUID,
pub packet_num: u32,
pub type_: AssetType,
pub xfer_id: u64,
pub base: Transfer,
}
impl AssetUpload {
pub fn new() -> Result<Self, Error> {
Ok(Self {
asset_id: UUID::zero(),
packet_num: 0,
type_: AssetType::Unknown,
xfer_id: 0,
base: Transfer::new()?,
})
}
}
#[derive(Clone, Debug)]
pub struct AssetUploadEventArgs {
upload: AssetUpload,
}
impl AssetUploadEventArgs {
pub fn new(upload: AssetUpload) -> Result<Self, Error> {
Ok(Self { upload })
}
pub fn upload(&self) -> AssetUpload {
self.upload.clone()
}
}
#[derive(Clone, Debug)]
pub struct ImageReceiveProgressEventArgs {
image_id: UUID,
received: i32,
total: i32,
}
impl ImageReceiveProgressEventArgs {
pub fn new(image_id: UUID, received: i32, total: i32) -> Result<Self, Error> {
if received < 0 || total < 0 || received > total {
return Err(Error::Argument);
}
Ok(Self {
image_id,
received,
total,
})
}
pub const fn image_id(&self) -> UUID {
self.image_id
}
pub const fn received(&self) -> i32 {
self.received
}
pub const fn total(&self) -> i32 {
self.total
}
}
#[derive(Clone, Debug)]
pub struct InitiateDownloadEventArgs {
sim_filename: String,
viewer_filename: String,
}
impl InitiateDownloadEventArgs {
pub fn new(sim_filename: String, viewer_filename: String) -> Result<Self, Error> {
if sim_filename.as_bytes().contains(&0) || viewer_filename.as_bytes().contains(&0) {
return Err(Error::Argument);
}
Ok(Self {
sim_filename,
viewer_filename,
})
}
pub fn sim_file_name(&self) -> String {
self.sim_filename.clone()
}
pub fn viewer_file_name(&self) -> String {
self.viewer_filename.clone()
}
}
#[derive(Clone, Debug)]
pub struct XferDownload {
pub error: TransferError,
pub filename: String,
pub packet_num: u32,
pub v_file_id: UUID,
pub xfer_id: u64,
pub base: Transfer,
}
impl XferDownload {
pub fn new() -> Result<Self, Error> {
Ok(Self {
error: TransferError::None,
filename: String::new(),
packet_num: 0,
v_file_id: UUID::zero(),
xfer_id: 0,
base: Transfer::new()?,
})
}
}
#[derive(Clone, Debug)]
pub struct XferReceivedEventArgs {
xfer: XferDownload,
}
impl XferReceivedEventArgs {
pub fn new(xfer: XferDownload) -> Result<Self, Error> {
Ok(Self { xfer })
}
pub fn xfer(&self) -> XferDownload {
self.xfer.clone()
}
}

View File

@@ -600,14 +600,17 @@ async fn download_manager_retries_transient_but_not_permanent_statuses() {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn deduplicated_subscriber_cancellation_cancels_the_shared_download() {
async fn deduplicated_subscriber_cancellation_is_independent() {
let attempts = Arc::new(AtomicUsize::new(0));
let handler_attempts = Arc::clone(&attempts);
let handler = HttpMessageHandler::new(move |_request, cancellation| {
let release = Arc::new(tokio::sync::Notify::new());
let handler_release = Arc::clone(&release);
let handler = HttpMessageHandler::new(move |_request, _cancellation| {
handler_attempts.fetch_add(1, Ordering::AcqRel);
let release = Arc::clone(&handler_release);
async move {
cancellation.cancelled().await;
response(200, b"too late".to_vec())
release.notified().await;
response(200, b"shared".to_vec())
}
});
let mut client = GridClient::new().expect("client");
@@ -645,8 +648,12 @@ async fn deduplicated_subscriber_cancellation_cancels_the_shared_download() {
tokio::task::yield_now().await;
}
cancellation.cancel();
assert_eq!(first.await.expect("first task"), Err(Error::Cancelled));
assert_eq!(second.await.expect("second task"), Err(Error::Cancelled));
release.notify_waiters();
assert_eq!(
first.await.expect("first task").expect("first download").1,
b"shared"
);
assert_eq!(attempts.load(Ordering::Acquire), 1);
downloads.dispose().expect("dispose");
}