#[cfg(any(feature = "jpeg2000", feature = "rust-j2k"))] use crate::assets::AssetTexture; use crate::assets::{AssetMesh, AssetMutable, AssetSound}; use crate::{ AssetCache, AssetCacheComputeAssetCacheFilenameDelegate, Error, GridClient, ImageCodec, }; #[cfg(any(feature = "jpeg2000", feature = "rust-j2k"))] 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()); let malformed = AssetMesh::new_with_uuid_bytes(UUID::random().unwrap(), b"not-llsd".to_vec()).unwrap(); assert!(!malformed.decode().unwrap()); } #[test] #[cfg(feature = "vorbis")] fn sound_codec_produces_real_valid_payload() { let ogg = AssetSound::pcm_to_ogg(vec![0_u8; 256 * 2], 44_100, 1, Some(16)).unwrap(); assert!(ogg.starts_with(b"OggS")); } #[test] #[cfg(not(feature = "vorbis"))] fn sound_codec_is_explicitly_feature_gated() { assert_eq!( AssetSound::pcm_to_ogg(vec![0_u8; 256 * 2], 44_100, 1, Some(16)), Err(Error::InvalidOperation) ); assert_eq!( AssetSound::pcm_to_ogg(Vec::new(), 44_100, 1, Some(16)), Err(Error::Argument) ); } #[test] #[cfg(any(feature = "jpeg2000", feature = "rust-j2k"))] fn texture_codec_produces_real_valid_payload() { 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(); }