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

This commit is contained in:
2026-08-12 14:44:28 +00:00
parent dceb394378
commit c9a1170a27
140 changed files with 82175 additions and 27179 deletions

View File

@@ -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());