Some checks failed
Native code generation / deterministic (push) Successful in 18m30s
Imaging and meshing gate / native (push) Successful in 5m41s
JPEG 2000 feature / linux (push) Successful in 2m53s
Skia feature / linux (push) Has been cancelled
Native Rust workspace compile / compile (push) Has been cancelled
530 lines
18 KiB
Rust
530 lines
18 KiB
Rust
//! Bounded, offline command implementation for the `OSDInspector` tool.
|
|
|
|
use clap::{Parser, Subcommand, ValueEnum};
|
|
use libremetaverse::structured_data::{OSD, OSDParser, OSDType};
|
|
use libremetaverse::types::{Material, PCode, PathCurve, ProfileCurve, Quaternion, UUID, Vector3};
|
|
use libremetaverse::{Primitive, PrimitiveConstructionData, PrimitiveObjectProperties};
|
|
use std::fmt;
|
|
use std::fs::File;
|
|
use std::io::{self, BufReader, BufWriter, Read, Write};
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::ExitCode;
|
|
|
|
/// Successful command completion.
|
|
pub const EXIT_SUCCESS: u8 = 0;
|
|
/// Command-line usage errors. Clap also uses this value for invalid arguments.
|
|
pub const EXIT_USAGE: u8 = 2;
|
|
/// File or standard-stream I/O failed.
|
|
pub const EXIT_IO: u8 = 3;
|
|
/// Input was too large or was not valid OSD.
|
|
pub const EXIT_INVALID_OSD: u8 = 4;
|
|
/// Valid OSD could not be interpreted as a primitive.
|
|
pub const EXIT_INVALID_PRIMITIVE: u8 = 5;
|
|
|
|
const DEFAULT_MAX_INPUT_BYTES: u64 = OSD::DEFAULT_MAX_BINARY_BYTES as u64;
|
|
|
|
#[derive(Debug, Parser)]
|
|
#[command(
|
|
name = "osd-inspector",
|
|
version,
|
|
about = "Inspect, convert, and validate LibreMetaverse structured data offline",
|
|
long_about = None,
|
|
arg_required_else_help = true
|
|
)]
|
|
pub struct Cli {
|
|
/// Maximum accepted input size. Parsing also enforces the library depth and node limits.
|
|
#[arg(long, global = true, default_value_t = DEFAULT_MAX_INPUT_BYTES, value_name = "BYTES")]
|
|
max_input_bytes: u64,
|
|
|
|
#[command(subcommand)]
|
|
command: Command,
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
enum Command {
|
|
/// Display an OSD value's type and recursively formatted structure.
|
|
#[command(alias = "i")]
|
|
Inspect {
|
|
/// Input file, or '-' for standard input.
|
|
input: PathBuf,
|
|
},
|
|
/// Convert OSD to JSON, XML, binary LLSD, or notation LLSD.
|
|
#[command(alias = "c")]
|
|
Convert {
|
|
/// Input file, or '-' for standard input.
|
|
input: PathBuf,
|
|
/// Output format (json/j, xml/x, binary/bin/b, notation/llsd/n).
|
|
format: OutputFormat,
|
|
/// Output file, or '-' for standard output.
|
|
output: PathBuf,
|
|
},
|
|
/// Validate an OSD value and report its root shape.
|
|
#[command(alias = "v")]
|
|
Validate {
|
|
/// Input file, or '-' for standard input.
|
|
input: PathBuf,
|
|
},
|
|
/// Emit a deterministic sample cube as pretty JSON.
|
|
PrimToOsd,
|
|
/// Parse an OSD value as a Primitive and summarize it.
|
|
OsdToPrim {
|
|
/// Input file, or '-' for standard input.
|
|
input: PathBuf,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, ValueEnum)]
|
|
enum OutputFormat {
|
|
#[value(alias = "j")]
|
|
Json,
|
|
#[value(alias = "x")]
|
|
Xml,
|
|
#[value(alias = "bin", alias = "b")]
|
|
Binary,
|
|
#[value(alias = "llsd", alias = "n")]
|
|
Notation,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
enum InputFormat {
|
|
Json,
|
|
Xml,
|
|
Binary,
|
|
Notation,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum InspectorError {
|
|
Io { action: String, source: io::Error },
|
|
InputTooLarge { limit: u64 },
|
|
InvalidOsd { input: String },
|
|
Serialization { format: OutputFormat },
|
|
InvalidPrimitive,
|
|
}
|
|
|
|
impl InspectorError {
|
|
const fn exit_code(&self) -> u8 {
|
|
match self {
|
|
Self::Io { .. } => EXIT_IO,
|
|
Self::InputTooLarge { .. } | Self::InvalidOsd { .. } => EXIT_INVALID_OSD,
|
|
Self::Serialization { .. } | Self::InvalidPrimitive => EXIT_INVALID_PRIMITIVE,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for InspectorError {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Self::Io { action, source } => write!(formatter, "{action}: {source}"),
|
|
Self::InputTooLarge { limit } => {
|
|
write!(formatter, "input exceeds the configured {limit}-byte limit")
|
|
}
|
|
Self::InvalidOsd { input } => write!(formatter, "invalid OSD input: {input}"),
|
|
Self::Serialization { format } => {
|
|
write!(formatter, "could not serialize OSD as {format:?}")
|
|
}
|
|
Self::InvalidPrimitive => write!(formatter, "OSD value is not a Primitive map"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parses command-line arguments with clap and runs the offline tool.
|
|
#[must_use]
|
|
pub fn main_entry() -> ExitCode {
|
|
let cli = Cli::parse();
|
|
let stdin = io::stdin();
|
|
let stdout = io::stdout();
|
|
let stderr = io::stderr();
|
|
let mut input = stdin.lock();
|
|
let mut output = stdout.lock();
|
|
let mut errors = stderr.lock();
|
|
match execute(cli, &mut input, &mut output) {
|
|
Ok(()) => ExitCode::from(EXIT_SUCCESS),
|
|
Err(error) => {
|
|
let _ = writeln!(errors, "osd-inspector: {error}");
|
|
ExitCode::from(error.exit_code())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn execute(cli: Cli, stdin: &mut dyn Read, stdout: &mut dyn Write) -> Result<(), InspectorError> {
|
|
if cli.max_input_bytes == 0 {
|
|
return Err(InspectorError::InputTooLarge { limit: 0 });
|
|
}
|
|
match cli.command {
|
|
Command::Inspect { input } => inspect(&input, cli.max_input_bytes, stdin, stdout),
|
|
Command::Convert {
|
|
input,
|
|
format,
|
|
output,
|
|
} => convert(&input, format, &output, cli.max_input_bytes, stdin, stdout),
|
|
Command::Validate { input } => validate(&input, cli.max_input_bytes, stdin, stdout),
|
|
Command::PrimToOsd => primitive_to_osd(stdout),
|
|
Command::OsdToPrim { input } => {
|
|
osd_to_primitive(&input, cli.max_input_bytes, stdin, stdout)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn inspect(
|
|
input: &Path,
|
|
limit: u64,
|
|
stdin: &mut dyn Read,
|
|
output: &mut dyn Write,
|
|
) -> Result<(), InspectorError> {
|
|
let bytes = read_input(input, limit, stdin)?;
|
|
let osd = parse_osd(&bytes, input)?;
|
|
writeln!(output, "File: {}", display_path(input)).map_err(stdout_error)?;
|
|
writeln!(output, "Type: {}", type_name(osd.type_())).map_err(stdout_error)?;
|
|
writeln!(output, "Size: {} bytes", bytes.len()).map_err(stdout_error)?;
|
|
writeln!(output, "\nStructure:\n{}", "-".repeat(60)).map_err(stdout_error)?;
|
|
display_osd(&osd, 0, output)?;
|
|
writeln!(output, "{}", "-".repeat(60)).map_err(stdout_error)
|
|
}
|
|
|
|
fn convert(
|
|
input: &Path,
|
|
format: OutputFormat,
|
|
destination: &Path,
|
|
limit: u64,
|
|
stdin: &mut dyn Read,
|
|
stdout: &mut dyn Write,
|
|
) -> Result<(), InspectorError> {
|
|
let bytes = read_input(input, limit, stdin)?;
|
|
let osd = parse_osd(&bytes, input)?;
|
|
let encoded = serialize(osd, format)?;
|
|
if is_standard_stream(destination) {
|
|
stdout.write_all(&encoded).map_err(stdout_error)?;
|
|
stdout.flush().map_err(stdout_error)?;
|
|
} else {
|
|
write_file(destination, &encoded)?;
|
|
writeln!(
|
|
stdout,
|
|
"Converted {} to {}: {} ({} bytes)",
|
|
display_path(input),
|
|
format_name(format),
|
|
destination.display(),
|
|
encoded.len()
|
|
)
|
|
.map_err(stdout_error)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate(
|
|
input: &Path,
|
|
limit: u64,
|
|
stdin: &mut dyn Read,
|
|
output: &mut dyn Write,
|
|
) -> Result<(), InspectorError> {
|
|
let bytes = read_input(input, limit, stdin)?;
|
|
let osd = parse_osd(&bytes, input)?;
|
|
writeln!(output, "Valid OSD file").map_err(stdout_error)?;
|
|
writeln!(output, " Type: {}", type_name(osd.type_())).map_err(stdout_error)?;
|
|
match osd {
|
|
OSD::Map(values) => writeln!(output, " Keys: {}", values.len()).map_err(stdout_error)?,
|
|
OSD::Array(values) => {
|
|
writeln!(output, " Elements: {}", values.len()).map_err(stdout_error)?;
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn primitive_to_osd(output: &mut dyn Write) -> Result<(), InspectorError> {
|
|
let osd = sample_primitive()
|
|
.and_then(|primitive| primitive.get_osd())
|
|
.map_err(|_| InspectorError::InvalidPrimitive)?;
|
|
let json = OSDParser::serialize_json_string(osd, Some(true)).map_err(|_| {
|
|
InspectorError::Serialization {
|
|
format: OutputFormat::Json,
|
|
}
|
|
})?;
|
|
writeln!(output, "{json}").map_err(stdout_error)
|
|
}
|
|
|
|
fn osd_to_primitive(
|
|
input: &Path,
|
|
limit: u64,
|
|
stdin: &mut dyn Read,
|
|
output: &mut dyn Write,
|
|
) -> Result<(), InspectorError> {
|
|
let bytes = read_input(input, limit, stdin)?;
|
|
let osd = parse_osd(&bytes, input)?;
|
|
if !matches!(osd, OSD::Map(_)) {
|
|
return Err(InspectorError::InvalidPrimitive);
|
|
}
|
|
let primitive = Primitive::from_osd(osd).map_err(|_| InspectorError::InvalidPrimitive)?;
|
|
let name = primitive
|
|
.properties
|
|
.as_ref()
|
|
.map_or("Unknown", |properties| properties.name.as_str());
|
|
writeln!(output, "Successfully parsed Primitive:").map_err(stdout_error)?;
|
|
writeln!(output, " ID: {}", primitive.id).map_err(stdout_error)?;
|
|
writeln!(output, " Name: {name}").map_err(stdout_error)?;
|
|
writeln!(output, " Type: {:?}", primitive.type_()).map_err(stdout_error)?;
|
|
writeln!(output, " Position: {}", primitive.position.to_string()).map_err(stdout_error)?;
|
|
writeln!(output, " Scale: {}", primitive.scale.to_string()).map_err(stdout_error)?;
|
|
writeln!(output, " Material: {:?}", primitive.prim_data.material).map_err(stdout_error)?;
|
|
writeln!(output, " PCode: {:?}", primitive.prim_data.p_code).map_err(stdout_error)
|
|
}
|
|
|
|
fn sample_primitive() -> Result<Primitive, libremetaverse::Error> {
|
|
let mut primitive = Primitive::new_with_constructor()?;
|
|
primitive.id = UUID::new_with_string("11111111-2222-3333-4444-555555555555".into())?;
|
|
primitive.local_id = 12_345;
|
|
primitive.position = Vector3 {
|
|
x: 128.0,
|
|
y: 128.0,
|
|
z: 25.0,
|
|
};
|
|
primitive.rotation = Quaternion::identity();
|
|
primitive.scale = Vector3 {
|
|
x: 1.0,
|
|
y: 1.0,
|
|
z: 1.0,
|
|
};
|
|
let mut construction = PrimitiveConstructionData::new_with_constructor()?;
|
|
construction.p_code = PCode::Prim;
|
|
construction.material = Material::Wood;
|
|
construction.path_curve = PathCurve::Line;
|
|
construction.path_scale_x = 1.0;
|
|
construction.path_scale_y = 1.0;
|
|
construction.set_profile_curve_with_property(ProfileCurve::Square);
|
|
primitive.prim_data = construction;
|
|
let mut properties = PrimitiveObjectProperties::new()?;
|
|
properties.name = "Example Cube".into();
|
|
properties.description = "A simple cube created with LibreMetaverse".into();
|
|
primitive.properties = Some(properties);
|
|
Ok(primitive)
|
|
}
|
|
|
|
fn read_input(path: &Path, limit: u64, stdin: &mut dyn Read) -> Result<Vec<u8>, InspectorError> {
|
|
if is_standard_stream(path) {
|
|
return read_bounded(stdin, limit, "reading standard input");
|
|
}
|
|
let file = File::open(path).map_err(|source| InspectorError::Io {
|
|
action: format!("opening {}", path.display()),
|
|
source,
|
|
})?;
|
|
if file.metadata().map_or(0, |value| value.len()) > limit {
|
|
return Err(InspectorError::InputTooLarge { limit });
|
|
}
|
|
read_bounded(
|
|
&mut BufReader::new(file),
|
|
limit,
|
|
&format!("reading {}", path.display()),
|
|
)
|
|
}
|
|
|
|
fn read_bounded(input: &mut dyn Read, limit: u64, action: &str) -> Result<Vec<u8>, InspectorError> {
|
|
let capacity = usize::try_from(limit.min(64 * 1024)).unwrap_or(64 * 1024);
|
|
let mut bytes = Vec::with_capacity(capacity);
|
|
input
|
|
.take(limit.saturating_add(1))
|
|
.read_to_end(&mut bytes)
|
|
.map_err(|source| InspectorError::Io {
|
|
action: action.into(),
|
|
source,
|
|
})?;
|
|
if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > limit {
|
|
return Err(InspectorError::InputTooLarge { limit });
|
|
}
|
|
Ok(bytes)
|
|
}
|
|
|
|
fn write_file(path: &Path, bytes: &[u8]) -> Result<(), InspectorError> {
|
|
let file = File::create(path).map_err(|source| InspectorError::Io {
|
|
action: format!("creating {}", path.display()),
|
|
source,
|
|
})?;
|
|
let mut writer = BufWriter::new(file);
|
|
writer
|
|
.write_all(bytes)
|
|
.and_then(|()| writer.flush())
|
|
.map_err(|source| InspectorError::Io {
|
|
action: format!("writing {}", path.display()),
|
|
source,
|
|
})
|
|
}
|
|
|
|
fn parse_osd(bytes: &[u8], path: &Path) -> Result<OSD, InspectorError> {
|
|
if bytes.is_empty() || bytes.iter().all(u8::is_ascii_whitespace) {
|
|
return Err(InspectorError::InvalidOsd {
|
|
input: display_path(path),
|
|
});
|
|
}
|
|
let hinted = format_hint(path, bytes);
|
|
let mut attempts = vec![hinted];
|
|
for format in [
|
|
InputFormat::Json,
|
|
InputFormat::Xml,
|
|
InputFormat::Binary,
|
|
InputFormat::Notation,
|
|
] {
|
|
if !attempts.contains(&format) {
|
|
attempts.push(format);
|
|
}
|
|
}
|
|
for format in attempts {
|
|
if let Ok(value) = parse_as(bytes, format) {
|
|
return Ok(value);
|
|
}
|
|
}
|
|
Err(InspectorError::InvalidOsd {
|
|
input: display_path(path),
|
|
})
|
|
}
|
|
|
|
fn parse_as(bytes: &[u8], format: InputFormat) -> Result<OSD, libremetaverse::Error> {
|
|
match format {
|
|
InputFormat::Json => OSDParser::deserialize_json_with_string(
|
|
String::from_utf8(bytes.to_vec()).map_err(|_| libremetaverse::Error::Argument)?,
|
|
),
|
|
InputFormat::Xml => OSDParser::deserialize_llsd_xml_with_bytes(bytes.to_vec()),
|
|
InputFormat::Binary => OSDParser::deserialize_llsd_binary_with_bytes(bytes.to_vec()),
|
|
InputFormat::Notation => OSDParser::deserialize_llsd_notation_with_string(
|
|
String::from_utf8(bytes.to_vec()).map_err(|_| libremetaverse::Error::Argument)?,
|
|
),
|
|
}
|
|
}
|
|
|
|
fn format_hint(path: &Path, bytes: &[u8]) -> InputFormat {
|
|
match path
|
|
.extension()
|
|
.and_then(|extension| extension.to_str())
|
|
.map(str::to_ascii_lowercase)
|
|
.as_deref()
|
|
{
|
|
Some("json") => InputFormat::Json,
|
|
Some("xml") => InputFormat::Xml,
|
|
Some("bin" | "binary") => InputFormat::Binary,
|
|
Some("notation" | "nt") => InputFormat::Notation,
|
|
_ => sniff_format(bytes),
|
|
}
|
|
}
|
|
|
|
fn sniff_format(bytes: &[u8]) -> InputFormat {
|
|
let text = String::from_utf8_lossy(&bytes[..bytes.len().min(64)]);
|
|
let trimmed = text.trim_start_matches('\u{feff}').trim_start();
|
|
if trimmed.starts_with("<? llsd/binary ?>") || trimmed.starts_with("<?llsd/binary?>") {
|
|
InputFormat::Binary
|
|
} else if trimmed.starts_with('<') {
|
|
InputFormat::Xml
|
|
} else if matches!(
|
|
trimmed.as_bytes().first(),
|
|
Some(b'{' | b'[' | b'"' | b'-' | b'0'..=b'9')
|
|
) || trimmed.starts_with("true")
|
|
|| trimmed.starts_with("false")
|
|
|| trimmed.starts_with("null")
|
|
{
|
|
InputFormat::Json
|
|
} else {
|
|
InputFormat::Notation
|
|
}
|
|
}
|
|
|
|
fn serialize(osd: OSD, format: OutputFormat) -> Result<Vec<u8>, InspectorError> {
|
|
let result = match format {
|
|
OutputFormat::Json => {
|
|
OSDParser::serialize_json_string(osd, Some(true)).map(String::into_bytes)
|
|
}
|
|
OutputFormat::Xml => OSDParser::serialize_llsd_xml_bytes(osd),
|
|
OutputFormat::Binary => OSDParser::serialize_llsd_binary_with_osd(osd),
|
|
OutputFormat::Notation => OSDParser::serialize_llsd_notation(osd).map(String::into_bytes),
|
|
};
|
|
result.map_err(|_| InspectorError::Serialization { format })
|
|
}
|
|
|
|
fn display_osd(osd: &OSD, indent: usize, output: &mut dyn Write) -> Result<(), InspectorError> {
|
|
let prefix = " ".repeat(indent);
|
|
match osd {
|
|
OSD::Map(values) => {
|
|
writeln!(output, "{prefix}Map ({} keys)", values.len()).map_err(stdout_error)?;
|
|
let mut entries = values.iter().collect::<Vec<_>>();
|
|
entries.sort_unstable_by_key(|(key, _)| *key);
|
|
for (key, value) in entries {
|
|
write!(output, "{prefix} {key}: ").map_err(stdout_error)?;
|
|
if matches!(value, OSD::Map(_) | OSD::Array(_)) {
|
|
writeln!(output).map_err(stdout_error)?;
|
|
display_osd(value, indent + 2, output)?;
|
|
} else {
|
|
display_osd(value, 0, output)?;
|
|
}
|
|
}
|
|
}
|
|
OSD::Array(values) => {
|
|
writeln!(output, "{prefix}Array ({} elements)", values.len()).map_err(stdout_error)?;
|
|
for (index, value) in values.iter().enumerate() {
|
|
write!(output, "{prefix} [{index}]: ").map_err(stdout_error)?;
|
|
if matches!(value, OSD::Map(_) | OSD::Array(_)) {
|
|
writeln!(output).map_err(stdout_error)?;
|
|
display_osd(value, indent + 2, output)?;
|
|
} else {
|
|
display_osd(value, 0, output)?;
|
|
}
|
|
}
|
|
}
|
|
OSD::String(value) => writeln!(output, "String: {value:?}").map_err(stdout_error)?,
|
|
OSD::Integer(value) => writeln!(output, "Integer: {value}").map_err(stdout_error)?,
|
|
OSD::Real(value) => writeln!(output, "Real: {value}").map_err(stdout_error)?,
|
|
OSD::Boolean(value) => writeln!(output, "Boolean: {value}").map_err(stdout_error)?,
|
|
OSD::UUID(value) => writeln!(output, "UUID: {value}").map_err(stdout_error)?,
|
|
OSD::Date(_) => writeln!(output, "Date: {}", osd.as_string().unwrap_or_default())
|
|
.map_err(stdout_error)?,
|
|
OSD::Uri(value) => writeln!(output, "URI: {}", value.0).map_err(stdout_error)?,
|
|
OSD::Binary(value) => {
|
|
writeln!(output, "Binary: {} bytes", value.len()).map_err(stdout_error)?;
|
|
}
|
|
OSD::LlsdXml(value) => writeln!(output, "LLSD XML: {value:?}").map_err(stdout_error)?,
|
|
OSD::Undefined => writeln!(output, "Unknown (Undefined)").map_err(stdout_error)?,
|
|
_ => writeln!(output, "Unknown ({:?})", osd.type_()).map_err(stdout_error)?,
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
const fn type_name(value: OSDType) -> &'static str {
|
|
match value {
|
|
OSDType::Unknown => "Unknown",
|
|
OSDType::Boolean => "Boolean",
|
|
OSDType::Integer => "Integer",
|
|
OSDType::Real => "Real",
|
|
OSDType::String => "String",
|
|
OSDType::UUID => "UUID",
|
|
OSDType::Date => "Date",
|
|
OSDType::URI => "URI",
|
|
OSDType::Binary => "Binary",
|
|
OSDType::Map => "Map",
|
|
OSDType::Array => "Array",
|
|
OSDType::LlsdXml => "LlsdXml",
|
|
}
|
|
}
|
|
|
|
const fn format_name(format: OutputFormat) -> &'static str {
|
|
match format {
|
|
OutputFormat::Json => "json",
|
|
OutputFormat::Xml => "xml",
|
|
OutputFormat::Binary => "binary",
|
|
OutputFormat::Notation => "notation",
|
|
}
|
|
}
|
|
|
|
fn is_standard_stream(path: &Path) -> bool {
|
|
path.as_os_str() == "-"
|
|
}
|
|
|
|
fn display_path(path: &Path) -> String {
|
|
if is_standard_stream(path) {
|
|
"<stdin>".into()
|
|
} else {
|
|
path.display().to_string()
|
|
}
|
|
}
|
|
|
|
fn stdout_error(source: io::Error) -> InspectorError {
|
|
InspectorError::Io {
|
|
action: "writing standard output".into(),
|
|
source,
|
|
}
|
|
}
|