Complete first release candidate audit (#107)
Some checks failed
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
Some checks failed
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
#![allow(clippy::missing_errors_doc)] // Public signatures mirror the C# API.
|
||||
#![allow(clippy::must_use_candidate)]
|
||||
#![allow(clippy::needless_pass_by_value)]
|
||||
#![allow(clippy::too_many_arguments)] // Capability calls preserve mapped request fields.
|
||||
#![allow(clippy::too_many_lines)] // Protocol parsers intentionally keep each wire state machine together.
|
||||
#![allow(clippy::unnecessary_wraps)] // Mapped C# methods require Result-shaped Rust APIs.
|
||||
|
||||
@@ -1657,7 +1658,7 @@ impl InventoryManager {
|
||||
}
|
||||
|
||||
pub(crate) fn native_no_results() -> Task<Vec<InventoryBase>> {
|
||||
Task(std::marker::PhantomData)
|
||||
Task::ready(Vec::new())
|
||||
}
|
||||
|
||||
pub(crate) fn native_create_or_retrieve_inventory_item(
|
||||
@@ -4426,6 +4427,507 @@ fn parse_task_inventory_values(
|
||||
}
|
||||
|
||||
impl InventoryManager {
|
||||
async fn post_capability_osd(
|
||||
&self,
|
||||
uri: Uri,
|
||||
body: OSD,
|
||||
token: CancellationToken,
|
||||
) -> Result<OSD, Error> {
|
||||
token.throw_if_cancellation_requested()?;
|
||||
let payload = OSDParser::serialize_llsd_xml_bytes(body)?;
|
||||
let (response, bytes) = self
|
||||
.client()?
|
||||
.native_http_caps_client()
|
||||
.post_with_uri_string_bytes_cancellation_token_i_progress(
|
||||
uri,
|
||||
"application/llsd+xml".into(),
|
||||
payload,
|
||||
token,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
if !response.is_success_status_code() {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
OSDParser::deserialize_with_bytes(bytes)
|
||||
}
|
||||
|
||||
async fn post_asset_bytes_osd(
|
||||
&self,
|
||||
uri: Uri,
|
||||
data: Vec<u8>,
|
||||
token: CancellationToken,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<OSD, Error> {
|
||||
token.throw_if_cancellation_requested()?;
|
||||
if data.is_empty() || data.len() > crate::asset_models::MAX_ASSET_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let (response, bytes) = self
|
||||
.client()?
|
||||
.native_http_caps_client()
|
||||
.post_with_uri_string_bytes_cancellation_token_i_progress(
|
||||
uri,
|
||||
"application/octet-stream".into(),
|
||||
data,
|
||||
token,
|
||||
progress,
|
||||
)
|
||||
.await?;
|
||||
if !response.is_success_status_code() {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
OSDParser::deserialize_with_bytes(bytes)
|
||||
}
|
||||
|
||||
async fn begin_inventory_capability(
|
||||
&self,
|
||||
capability: &str,
|
||||
query: HashMap<String, OSD>,
|
||||
token: &CancellationToken,
|
||||
) -> Result<OSD, Error> {
|
||||
let simulator = self
|
||||
.client()?
|
||||
.native_network()?
|
||||
.current_sim()
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
let uri = self
|
||||
.wait_for_capability(&simulator, capability, token)
|
||||
.await?
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
self.post_capability_osd(uri, OSD::Map(query), token.clone())
|
||||
.await
|
||||
}
|
||||
|
||||
async fn finish_inventory_upload(
|
||||
&self,
|
||||
data: Vec<u8>,
|
||||
item_id: Option<UUID>,
|
||||
mut result: OSD,
|
||||
token: CancellationToken,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<((bool, String, UUID, UUID), OSD), Error> {
|
||||
let mut progress = progress;
|
||||
for _ in 0..3 {
|
||||
let OSD::Map(contents) = &result else {
|
||||
return Ok((
|
||||
(false, "invalid_response".into(), UUID::zero(), UUID::zero()),
|
||||
result,
|
||||
));
|
||||
};
|
||||
let state = contents
|
||||
.get("state")
|
||||
.map(OSD::as_string)
|
||||
.transpose()?
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
match state.as_str() {
|
||||
"upload" => {
|
||||
let uploader = contents
|
||||
.get("uploader")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uri()?
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
result = self
|
||||
.post_asset_bytes_osd(
|
||||
uploader,
|
||||
data.clone(),
|
||||
token.clone(),
|
||||
progress.take(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
"complete" => {
|
||||
let resolved_item = match item_id {
|
||||
Some(id) => id,
|
||||
None => contents
|
||||
.get("new_inventory_item")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uuid()?,
|
||||
};
|
||||
let asset_id = contents
|
||||
.get("new_asset")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uuid()?;
|
||||
let owner = self.client()?.native_agent_manager()?.native_agent_id();
|
||||
let _ = self.native_request_fetch_inventory(
|
||||
HashMap::from([(resolved_item, owner)]),
|
||||
Some(token),
|
||||
);
|
||||
return Ok(((true, String::new(), resolved_item, asset_id), result));
|
||||
}
|
||||
_ => {
|
||||
return Ok(((false, state, UUID::zero(), UUID::zero()), result));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((
|
||||
(
|
||||
false,
|
||||
"Upload state machine exceeded maximum iterations".into(),
|
||||
UUID::zero(),
|
||||
UUID::zero(),
|
||||
),
|
||||
result,
|
||||
))
|
||||
}
|
||||
|
||||
fn new_asset_query(
|
||||
&self,
|
||||
name: String,
|
||||
description: String,
|
||||
asset_type: AssetType,
|
||||
inventory_type: InventoryType,
|
||||
folder_id: UUID,
|
||||
permissions: &Permissions,
|
||||
include_expected_cost: bool,
|
||||
) -> Result<HashMap<String, OSD>, Error> {
|
||||
let mut query = HashMap::from([
|
||||
("folder_id".into(), OSD::UUID(folder_id)),
|
||||
(
|
||||
"asset_type".into(),
|
||||
OSD::String(Utils::asset_type_to_string(asset_type)?),
|
||||
),
|
||||
(
|
||||
"inventory_type".into(),
|
||||
OSD::String(Utils::inventory_type_to_string(inventory_type)?),
|
||||
),
|
||||
("name".into(), OSD::String(name)),
|
||||
("description".into(), OSD::String(description)),
|
||||
(
|
||||
"everyone_mask".into(),
|
||||
OSD::Integer(permissions.everyone_mask.0.cast_signed()),
|
||||
),
|
||||
(
|
||||
"group_mask".into(),
|
||||
OSD::Integer(permissions.group_mask.0.cast_signed()),
|
||||
),
|
||||
(
|
||||
"next_owner_mask".into(),
|
||||
OSD::Integer(permissions.next_owner_mask.0.cast_signed()),
|
||||
),
|
||||
]);
|
||||
if include_expected_cost {
|
||||
query.insert(
|
||||
"expected_upload_cost".into(),
|
||||
OSD::Integer(self.client()?.settings_ref().upload_cost()),
|
||||
);
|
||||
}
|
||||
Ok(query)
|
||||
}
|
||||
|
||||
pub(crate) async fn native_request_create_item_from_asset(
|
||||
&self,
|
||||
data: Vec<u8>,
|
||||
name: String,
|
||||
description: String,
|
||||
asset_type: AssetType,
|
||||
inventory_type: InventoryType,
|
||||
folder_id: UUID,
|
||||
permissions: Permissions,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<(bool, String, UUID, UUID), Error> {
|
||||
let token = cancellation_token.unwrap_or_default();
|
||||
let query = self.new_asset_query(
|
||||
name,
|
||||
description,
|
||||
asset_type,
|
||||
inventory_type,
|
||||
folder_id,
|
||||
&permissions,
|
||||
true,
|
||||
)?;
|
||||
let initial = self
|
||||
.begin_inventory_capability("NewFileAgentInventory", query, &token)
|
||||
.await?;
|
||||
self.finish_inventory_upload(data, None, initial, token, progress)
|
||||
.await
|
||||
.map(|(result, _)| result)
|
||||
}
|
||||
|
||||
pub(crate) async fn native_create_item_from_asset(
|
||||
&self,
|
||||
data: Vec<u8>,
|
||||
name: String,
|
||||
description: String,
|
||||
asset_type: AssetType,
|
||||
inventory_type: InventoryType,
|
||||
folder_id: UUID,
|
||||
permissions: Permissions,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<crate::InventoryManagerCreateItemFromAssetResult, Error> {
|
||||
let mut output = crate::InventoryManagerCreateItemFromAssetResult::default();
|
||||
match self
|
||||
.native_request_create_item_from_asset(
|
||||
data,
|
||||
name,
|
||||
description,
|
||||
asset_type,
|
||||
inventory_type,
|
||||
folder_id,
|
||||
permissions,
|
||||
cancellation_token,
|
||||
progress,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((success, status, item_id, asset_id)) => {
|
||||
output.set_success(success);
|
||||
output.set_status(Some(status));
|
||||
output.set_item_id(item_id);
|
||||
output.set_asset_id(asset_id);
|
||||
}
|
||||
Err(error) => {
|
||||
let status = error.to_string();
|
||||
output.set_status(Some(status.clone()));
|
||||
output.set_error(Some(ExternalError(status)));
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub(crate) fn native_can_upload_large_textures(&self) -> bool {
|
||||
let Ok(client) = self.client() else {
|
||||
return false;
|
||||
};
|
||||
let premium = client
|
||||
.native_agent_manager()
|
||||
.is_ok_and(|manager| manager.native_benefits().premium_access() > 0);
|
||||
premium
|
||||
&& client
|
||||
.native_network()
|
||||
.ok()
|
||||
.and_then(|network| network.current_sim())
|
||||
.and_then(|simulator| simulator.native_caps())
|
||||
.and_then(|caps| {
|
||||
caps.capability_uri("NewFileAgentInventoryVariablePrice".into())
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub(crate) async fn native_create_item_from_asset_variable_price(
|
||||
&self,
|
||||
data: Vec<u8>,
|
||||
name: String,
|
||||
description: String,
|
||||
asset_type: AssetType,
|
||||
inventory_type: InventoryType,
|
||||
folder_id: UUID,
|
||||
permissions: Permissions,
|
||||
confirm_cost: Option<Box<dyn Fn(i32) -> bool + Send + Sync>>,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<crate::InventoryManagerCreateItemFromAssetResult, Error> {
|
||||
let mut output = crate::InventoryManagerCreateItemFromAssetResult::default();
|
||||
if !self.native_can_upload_large_textures() {
|
||||
let status = "membership_or_capability_required".to_owned();
|
||||
output.set_status(Some(status.clone()));
|
||||
output.set_error(Some(ExternalError(status)));
|
||||
return Ok(output);
|
||||
}
|
||||
let token = cancellation_token.unwrap_or_default();
|
||||
let query = self.new_asset_query(
|
||||
name,
|
||||
description,
|
||||
asset_type,
|
||||
inventory_type,
|
||||
folder_id,
|
||||
&permissions,
|
||||
false,
|
||||
)?;
|
||||
let initial = self
|
||||
.begin_inventory_capability("NewFileAgentInventoryVariablePrice", query, &token)
|
||||
.await?;
|
||||
output.set_raw_result(Some(initial.clone()));
|
||||
let OSD::Map(contents) = initial else {
|
||||
output.set_status(Some("invalid_response".into()));
|
||||
return Ok(output);
|
||||
};
|
||||
let state = contents
|
||||
.get("state")
|
||||
.map(OSD::as_string)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
if state != "confirm_upload" {
|
||||
output.set_status(Some(format!("unexpected_state:{state}")));
|
||||
return Ok(output);
|
||||
}
|
||||
let price = contents
|
||||
.get("upload_price")
|
||||
.map(OSD::as_integer)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
if confirm_cost.as_ref().is_some_and(|confirm| !confirm(price)) {
|
||||
let status = "cost_rejected".to_owned();
|
||||
output.set_status(Some(status.clone()));
|
||||
output.set_error(Some(ExternalError(status)));
|
||||
return Ok(output);
|
||||
}
|
||||
let rsvp = contents
|
||||
.get("rsvp")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uri()?
|
||||
.filter(|uri| uri.0 != "about:blank")
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
let uploaded = self
|
||||
.post_asset_bytes_osd(rsvp, data, token.clone(), progress)
|
||||
.await?;
|
||||
output.set_raw_result(Some(uploaded.clone()));
|
||||
let OSD::Map(uploaded) = uploaded else {
|
||||
output.set_status(Some("invalid_upload_response".into()));
|
||||
return Ok(output);
|
||||
};
|
||||
let state = uploaded
|
||||
.get("state")
|
||||
.map(OSD::as_string)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
output.set_status(Some(state.clone()));
|
||||
if state == "complete" {
|
||||
let item_id = uploaded
|
||||
.get("new_inventory_item")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uuid()?;
|
||||
let asset_id = uploaded
|
||||
.get("new_asset")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uuid()?;
|
||||
output.set_item_id(item_id);
|
||||
output.set_asset_id(asset_id);
|
||||
output.set_success(true);
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub(crate) async fn native_upload_inventory_asset(
|
||||
&self,
|
||||
capability: &str,
|
||||
data: Vec<u8>,
|
||||
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 token = cancellation_token.unwrap_or_default();
|
||||
let mut query = HashMap::from([("item_id".into(), OSD::UUID(item_id))]);
|
||||
if let Some(task_id) = task_id {
|
||||
query.insert("task_id".into(), OSD::UUID(task_id));
|
||||
}
|
||||
let initial = self
|
||||
.begin_inventory_capability(capability, query, &token)
|
||||
.await?;
|
||||
self.finish_inventory_upload(data, Some(item_id), initial, token, progress)
|
||||
.await
|
||||
.map(|(result, _)| result)
|
||||
}
|
||||
|
||||
pub(crate) async fn native_upload_script(
|
||||
&self,
|
||||
capability: &str,
|
||||
data: Vec<u8>,
|
||||
item_id: UUID,
|
||||
task_id: Option<UUID>,
|
||||
mono: bool,
|
||||
running: Option<bool>,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
progress: Option<
|
||||
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
|
||||
>,
|
||||
) -> Result<(bool, String, bool, Option<Vec<String>>, UUID, UUID), Error> {
|
||||
let token = cancellation_token.unwrap_or_default();
|
||||
let mut progress = progress;
|
||||
let mut query = HashMap::from([
|
||||
("item_id".into(), OSD::UUID(item_id)),
|
||||
(
|
||||
"target".into(),
|
||||
OSD::String(if mono { "mono" } else { "lsl2" }.into()),
|
||||
),
|
||||
]);
|
||||
if let Some(task_id) = task_id {
|
||||
query.insert("task_id".into(), OSD::UUID(task_id));
|
||||
}
|
||||
if let Some(running) = running {
|
||||
query.insert("is_script_running".into(), OSD::Boolean(running));
|
||||
}
|
||||
let mut result = self
|
||||
.begin_inventory_capability(capability, query, &token)
|
||||
.await?;
|
||||
for _ in 0..3 {
|
||||
let OSD::Map(contents) = &result else {
|
||||
return Ok((
|
||||
false,
|
||||
"invalid_response".into(),
|
||||
false,
|
||||
None,
|
||||
UUID::zero(),
|
||||
UUID::zero(),
|
||||
));
|
||||
};
|
||||
let state = contents
|
||||
.get("state")
|
||||
.map(OSD::as_string)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
if state == "upload" {
|
||||
let uploader = contents
|
||||
.get("uploader")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uri()?
|
||||
.ok_or(Error::InvalidOperation)?;
|
||||
result = self
|
||||
.post_asset_bytes_osd(uploader, data.clone(), token.clone(), progress.take())
|
||||
.await?;
|
||||
continue;
|
||||
}
|
||||
if state == "complete" {
|
||||
let asset_id = contents
|
||||
.get("new_asset")
|
||||
.ok_or(Error::InvalidOperation)?
|
||||
.as_uuid()?;
|
||||
let compiled = contents
|
||||
.get("compiled")
|
||||
.map(OSD::as_boolean)
|
||||
.transpose()?
|
||||
.unwrap_or(false);
|
||||
let errors = match contents.get("errors") {
|
||||
Some(OSD::Array(values)) => Some(
|
||||
values
|
||||
.iter()
|
||||
.map(OSD::as_string)
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
return Ok((true, state, compiled, errors, item_id, asset_id));
|
||||
}
|
||||
return Ok((false, state, false, None, UUID::zero(), UUID::zero()));
|
||||
}
|
||||
Ok((
|
||||
false,
|
||||
"Upload state machine exceeded maximum iterations".into(),
|
||||
false,
|
||||
None,
|
||||
UUID::zero(),
|
||||
UUID::zero(),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn native_upload_thumbnail(
|
||||
&self,
|
||||
inventory_id: UUID,
|
||||
@@ -5030,7 +5532,7 @@ mod tests {
|
||||
.expect("network")
|
||||
.dispatch_caps_event("ScriptRunningReply", &reply, simulator);
|
||||
|
||||
let event = observed.lock().expect("observed").clone().expect("event");
|
||||
let event = (*observed.lock().expect("observed")).expect("event");
|
||||
assert_eq!(event.object_id(), object_id);
|
||||
assert_eq!(event.script_id(), script_id);
|
||||
assert!(event.is_mono());
|
||||
|
||||
Reference in New Issue
Block a user