Implement credential-safe programs smoke gate (#97)
Some checks failed
Native code generation / deterministic (push) Failing after 2m12s
Imaging and meshing gate / native (push) Failing after 5m40s
JPEG 2000 feature / linux (push) Successful in 2m50s
Native Rust workspace compile / compile (push) Failing after 56s
Skia feature / linux (push) Successful in 31m24s

This commit is contained in:
2026-08-11 17:04:40 +00:00
parent 66f10baa1b
commit a9e2711447
18 changed files with 2216 additions and 25 deletions

View File

@@ -0,0 +1,3 @@
fn main() -> std::process::ExitCode {
libremetaverse_programs::live_grid_smoke::main_entry()
}

View File

@@ -0,0 +1,203 @@
//! Compile-time-backed audit of every pinned upstream program and `TestClient` command.
use crate::commands::TEST_CLIENT_COMMANDS;
use crate::test_client::{IMPLEMENTED_TEST_CLIENT_COMMANDS, registered_test_client_command_count};
use serde::Deserialize;
use std::collections::{BTreeMap, BTreeSet};
const CARGO_MANIFEST: &str = include_str!("../Cargo.toml");
const SOURCE_MANIFEST: &str = include_str!("../upstream-programs.json");
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ProgramTarget {
pub project: &'static str,
pub binary: &'static str,
binary_source: &'static str,
offline_test_source: &'static str,
}
pub const ORIGINAL_PROGRAMS: &[ProgramTarget] = &[
ProgramTarget {
project: "OSDInspector",
binary: "osd-inspector",
binary_source: include_str!("bin/osd_inspector.rs"),
offline_test_source: include_str!("../tests/osd_inspector_cli.rs"),
},
ProgramTarget {
project: "SimpleBot",
binary: "simple-bot",
binary_source: include_str!("bin/simple_bot.rs"),
offline_test_source: include_str!("../tests/simple_bot_cli.rs"),
},
ProgramTarget {
project: "PacketDump",
binary: "packet-dump",
binary_source: include_str!("bin/packet_dump.rs"),
offline_test_source: include_str!("../tests/packet_dump_cli.rs"),
},
ProgramTarget {
project: "PrimInspector",
binary: "prim-inspector",
binary_source: include_str!("bin/prim_inspector.rs"),
offline_test_source: include_str!("../tests/prim_inspector_cli.rs"),
},
ProgramTarget {
project: "InventoryExplorer",
binary: "inventory-explorer",
binary_source: include_str!("bin/inventory_explorer.rs"),
offline_test_source: include_str!("../tests/inventory_explorer_cli.rs"),
},
ProgramTarget {
project: "IRCGateway",
binary: "irc-gateway",
binary_source: include_str!("bin/irc_gateway.rs"),
offline_test_source: include_str!("../tests/irc_gateway_cli.rs"),
},
ProgramTarget {
project: "TestClient",
binary: "test-client",
binary_source: include_str!("bin/test_client.rs"),
offline_test_source: include_str!("../tests/test_client_services_cli.rs"),
},
ProgramTarget {
project: "VivoxTest",
binary: "vivox-test",
binary_source: include_str!("bin/vivox_test.rs"),
offline_test_source: include_str!("../tests/vivox_test_cli.rs"),
},
ProgramTarget {
project: "WebRtcTest",
binary: "webrtc-test",
binary_source: include_str!("bin/webrtc_test.rs"),
offline_test_source: include_str!("../tests/webrtc_test_cli.rs"),
},
];
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CompletenessReport {
pub programs: usize,
pub source_files: usize,
pub test_client_commands: usize,
pub pending_programs: usize,
pub pending_commands: usize,
}
#[derive(Deserialize)]
struct SourceManifest {
upstream_commit: String,
projects: Vec<SourceProject>,
}
#[derive(Deserialize)]
struct SourceProject {
project: String,
files: Vec<SourceFile>,
}
#[derive(Deserialize)]
struct SourceFile {
path: String,
sha256: String,
}
/// Audits the pinned source manifest, all original Rust program targets, their
/// offline test entry points, and every generated `TestClient` command.
///
/// # Errors
///
/// Returns an error when the pinned source manifest is malformed or differs
/// from the native target, test, implementation, or runtime registries.
pub fn audit_program_completeness() -> Result<CompletenessReport, String> {
let manifest: SourceManifest = serde_json::from_str(SOURCE_MANIFEST)
.map_err(|_| "program source manifest is not valid JSON".to_owned())?;
if manifest.upstream_commit.len() != 40
|| !manifest
.upstream_commit
.bytes()
.all(|byte| byte.is_ascii_hexdigit())
{
return Err("program source manifest has an invalid upstream commit".into());
}
let mut projects = BTreeMap::new();
let mut source_paths = BTreeSet::new();
let mut source_files = 0_usize;
for project in &manifest.projects {
if project.files.is_empty() || projects.insert(project.project.as_str(), project).is_some()
{
return Err("program source manifest has an empty or duplicate project".into());
}
for file in &project.files {
if !source_paths.insert(file.path.as_str())
|| file.sha256.len() != 64
|| !file.sha256.bytes().all(|byte| byte.is_ascii_hexdigit())
{
return Err("program source manifest has an invalid file record".into());
}
source_files += 1;
}
}
let expected_projects: BTreeSet<_> = ORIGINAL_PROGRAMS
.iter()
.map(|target| target.project)
.collect();
if projects.keys().copied().collect::<BTreeSet<_>>() != expected_projects {
return Err("Rust program targets do not exactly match the source manifest".into());
}
for target in ORIGINAL_PROGRAMS {
let cargo_marker = format!("name = \"{}\"", target.binary);
if !CARGO_MANIFEST.contains(&cargo_marker)
|| target.binary_source.contains("pending_program")
|| !target.binary_source.contains("main_entry")
|| !target.offline_test_source.contains("#[test]")
{
return Err(format!(
"program {} lacks a native target or offline test",
target.project
));
}
}
let source_commands: BTreeSet<_> = projects["TestClient"]
.files
.iter()
.filter_map(|file| {
file.path
.contains("/Commands/")
.then(|| file.path.rsplit('/').next())
.flatten()
.and_then(|name| name.strip_suffix(".cs"))
})
.collect();
let generated_commands: BTreeSet<_> = TEST_CLIENT_COMMANDS.iter().copied().collect();
let implemented_commands: BTreeSet<_> =
IMPLEMENTED_TEST_CLIENT_COMMANDS.iter().copied().collect();
if source_commands != generated_commands
|| implemented_commands != generated_commands
|| registered_test_client_command_count() != generated_commands.len()
{
return Err("TestClient source, implementation, and runtime registries differ".into());
}
Ok(CompletenessReport {
programs: ORIGINAL_PROGRAMS.len(),
source_files,
test_client_commands: generated_commands.len(),
pending_programs: 0,
pending_commands: 0,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_source_program_and_command_has_a_native_tested_target() {
let report = audit_program_completeness().unwrap();
assert_eq!(report.programs, 9);
assert!(report.source_files > report.programs);
assert_eq!(report.test_client_commands, TEST_CLIENT_COMMANDS.len());
assert_eq!(report.pending_programs, 0);
assert_eq!(report.pending_commands, 0);
}
}

View File

@@ -1,8 +1,10 @@
//! Rust targets corresponding to the upstream example and tool projects.
pub mod commands;
pub mod completeness;
pub mod inventory_explorer;
pub mod irc_gateway;
pub mod live_grid_smoke;
pub mod osd_inspector;
pub mod packet_dump;
pub mod prim_inspector;

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@
mod inventory;
mod services;
mod voice;
mod world;
use crate::commands::TEST_CLIENT_COMMANDS;
@@ -164,6 +165,8 @@ pub const IMPLEMENTED_TEST_CLIENT_COMMANDS: &[&str] = &[
"RegionInfoCommand",
"StatsCommand",
"UptimeCommand",
"ParcelVoiceInfo",
"VoiceAcountCommand",
];
#[must_use]
@@ -176,6 +179,11 @@ pub fn pending_test_client_commands() -> Vec<&'static str> {
.collect()
}
#[must_use]
pub fn registered_test_client_command_count() -> usize {
built_in_commands().len()
}
#[derive(Parser)]
#[command(
name = "test-client",
@@ -325,7 +333,9 @@ enum ClientEvent {
type BackendFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
type InventoryFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
trait ClientBackend: Send + Sync + inventory::Backend + services::Backend + world::Backend {
trait ClientBackend:
Send + Sync + inventory::Backend + services::Backend + voice::Backend + world::Backend
{
fn id(&self) -> UUID;
fn name(&self) -> String;
fn connected(&self) -> bool;
@@ -876,6 +886,7 @@ enum CommandHandler {
ImGroup,
Inventory(inventory::Command),
Services(services::Command),
Voice(voice::Command),
World(world::Command),
Load,
Login,
@@ -1038,6 +1049,7 @@ fn built_in_commands() -> HashMap<String, CommandDefinition> {
.chain(communication_commands())
.chain(inventory::commands())
.chain(services::commands())
.chain(voice::commands())
.chain(world::commands())
.map(|(name, description, category, handler)| {
(
@@ -1822,6 +1834,9 @@ async fn execute_client_command(
CommandHandler::Services(command) => {
services::execute(client.backend.as_ref(), *command, args, cancellation).await
}
CommandHandler::Voice(command) => {
voice::execute(client.backend.as_ref(), *command, args, cancellation).await
}
CommandHandler::World(command) => {
world::execute(client.backend.as_ref(), *command, args, cancellation).await
}
@@ -2716,7 +2731,7 @@ mod tests {
assert!(TEST_CLIENT_COMMANDS.contains(command));
assert!(!pending.contains(command));
}
assert_eq!(pending, ["ParcelVoiceInfo", "VoiceAcountCommand"]);
assert!(pending.is_empty());
}
#[test]

View File

@@ -0,0 +1,210 @@
//! Credential-safe native `TestClient` voice capability commands.
use super::{FakeBackend, InventoryFuture as BackendFuture, LiveBackend, lock};
use libremetaverse::types::compat::{CancellationToken, Uri};
use libremetaverse_structured_data::{OSD, OSDFormat, OSDParser};
use std::collections::HashMap;
use std::time::Duration;
const MAX_RESPONSE_BYTES: usize = 1024 * 1024;
const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Clone, Copy)]
pub(super) enum Command {
Parcel,
Account,
}
pub(super) fn commands() -> [super::CommandEntry; 2] {
use super::{CommandCategory as Cat, CommandHandler};
[
(
"voiceparcel",
"Obtain redacted parcel voice information. Usage: voiceparcel",
Cat::Other,
CommandHandler::Voice(Command::Parcel),
),
(
"voiceaccount",
"Provision voice account information with credentials redacted. Usage: voiceaccount",
Cat::Other,
CommandHandler::Voice(Command::Account),
),
]
}
pub(super) trait Backend {
fn voice_query(
&self,
command: Command,
cancellation: CancellationToken,
) -> BackendFuture<'_, Result<VoiceResult, String>>;
}
pub(super) enum VoiceResult {
Parcel {
region_name_present: bool,
parcel_local_id: i32,
channel_present: bool,
},
Account {
username_present: bool,
password_present: bool,
},
}
pub(super) async fn execute<B: Backend + ?Sized>(
backend: &B,
command: Command,
args: &[String],
cancellation: CancellationToken,
) -> String {
if !args.is_empty() {
return match command {
Command::Parcel => "Usage: voiceparcel",
Command::Account => "Usage: voiceaccount",
}
.into();
}
match backend.voice_query(command, cancellation).await {
Ok(VoiceResult::Parcel {
region_name_present,
parcel_local_id,
channel_present,
}) => format!(
"Parcel voice info: region-name-present={region_name_present}, parcel-local-id={parcel_local_id}, channel=<{}>",
if channel_present {
"redacted"
} else {
"missing"
}
),
Ok(VoiceResult::Account {
username_present,
password_present,
}) => format!(
"Voice account provisioned: credentials=<{}>",
if username_present && password_present {
"redacted"
} else {
"incomplete"
}
),
Err(error) => error,
}
}
impl Backend for FakeBackend {
fn voice_query(
&self,
command: Command,
cancellation: CancellationToken,
) -> BackendFuture<'_, Result<VoiceResult, String>> {
Box::pin(async move {
if cancellation.is_cancellation_requested() {
return Err("Voice capability request cancelled".into());
}
Ok(match command {
Command::Parcel => {
self.record("CALL voice-parcel-info".into());
VoiceResult::Parcel {
region_name_present: true,
parcel_local_id: 42,
channel_present: true,
}
}
Command::Account => {
self.record("CALL voice-provision-account credentials=<redacted>".into());
VoiceResult::Account {
username_present: true,
password_present: true,
}
}
})
})
}
}
impl Backend for LiveBackend {
fn voice_query(
&self,
command: Command,
cancellation: CancellationToken,
) -> BackendFuture<'_, Result<VoiceResult, String>> {
Box::pin(async move {
let capability = match command {
Command::Parcel => "ParcelVoiceInfoRequest",
Command::Account => "ProvisionVoiceAccountRequest",
};
let simulator = self
.network
.current_sim()
.ok_or_else(|| "No current simulator is available".to_owned())?;
let uri = simulator
.native_capability_uri(capability)
.map_err(|_| "Voice capability lookup failed".to_owned())?
.ok_or_else(|| "Required voice capability is unavailable".to_owned())?;
let http = lock(&self.client).http_caps_client();
let request = http.post_with_uri_osd_format_osd_cancellation_token_i_progress(
Uri(uri.0),
OSDFormat::Xml,
OSD::Map(HashMap::new()),
cancellation.clone(),
None,
);
let result = tokio::select! {
() = cancellation.cancelled() => return Err("Voice capability request cancelled".into()),
() = tokio::time::sleep(REQUEST_TIMEOUT) => return Err("Voice capability request timed out".into()),
result = request => result,
}
.map_err(|_| "Voice capability request failed".to_owned())?;
let (response, bytes) = result;
if !response.is_success_status_code() || bytes.len() > MAX_RESPONSE_BYTES {
return Err("Voice capability returned an error".into());
}
let OSD::Map(map) = OSDParser::deserialize_with_bytes(bytes)
.map_err(|_| "Voice capability returned malformed LLSD".to_owned())?
else {
return Err("Voice capability response was not an LLSD map".into());
};
Ok(match command {
Command::Parcel => {
let region_name_present = present_string(&map, "region_name");
let parcel_local_id = map
.get("parcel_local_id")
.and_then(|value| value.as_integer().ok())
.ok_or_else(|| "Parcel voice response omitted local ID".to_owned())?;
let channel_present = match map.get("voice_credentials") {
Some(OSD::Map(credentials)) => present_string(credentials, "channel_uri"),
_ => false,
};
if !region_name_present || !channel_present {
return Err("Parcel voice response omitted required fields".into());
}
VoiceResult::Parcel {
region_name_present,
parcel_local_id,
channel_present,
}
}
Command::Account => {
let username_present = present_string(&map, "username");
let password_present = present_string(&map, "password");
if !username_present || !password_present {
return Err("Voice account response omitted credentials".into());
}
VoiceResult::Account {
username_present,
password_present,
}
}
})
})
}
}
fn present_string(map: &HashMap<String, OSD>, key: &str) -> bool {
map.get(key)
.and_then(|value| value.as_string().ok())
.is_some_and(|value| !value.is_empty())
}