678 lines
21 KiB
Rust
678 lines
21 KiB
Rust
//! 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
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
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
|
|
}
|
|
}
|