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
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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
484
crates/libremetaverse/src/asset_cache.rs
Normal file
484
crates/libremetaverse/src/asset_cache.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
1319
crates/libremetaverse/src/asset_manager.rs
Normal file
1319
crates/libremetaverse/src/asset_manager.rs
Normal file
File diff suppressed because it is too large
Load Diff
719
crates/libremetaverse/src/asset_material.rs
Normal file
719
crates/libremetaverse/src/asset_material.rs
Normal 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)),
|
||||
])
|
||||
}
|
||||
676
crates/libremetaverse/src/asset_models.rs
Normal file
676
crates/libremetaverse/src/asset_models.rs
Normal 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
|
||||
}
|
||||
}
|
||||
133
crates/libremetaverse/src/asset_pipeline_semantics.rs
Normal file
133
crates/libremetaverse/src/asset_pipeline_semantics.rs
Normal 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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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::*;
|
||||
|
||||
@@ -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;
|
||||
|
||||
192
crates/libremetaverse/src/object_material.rs
Normal file
192
crates/libremetaverse/src/object_material.rs
Normal 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
|
||||
}
|
||||
}
|
||||
226
crates/libremetaverse/src/transfers.rs
Normal file
226
crates/libremetaverse/src/transfers.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user