diff --git a/rbx_util/CHANGELOG.md b/rbx_util/CHANGELOG.md new file mode 100644 index 000000000..9bcc59b68 --- /dev/null +++ b/rbx_util/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +## Version 0.2.0 + +- Refactor commands into seperate files +- Add `verbosity` and `color` global flags to control logging and terminal color + +# Version 0.1.0 + +- Initial implementation \ No newline at end of file diff --git a/rbx_util/Cargo.toml b/rbx_util/Cargo.toml index 51a1d97f4..ad96e8cc0 100644 --- a/rbx_util/Cargo.toml +++ b/rbx_util/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rbx_util" -version = "0.1.0" +version = "0.2.0" description = "Utilities for working with Roblox model and place files" license = "MIT" documentation = "https://docs.rs/rbx_util" @@ -20,9 +20,13 @@ path = "src/main.rs" name = "rbx-util" [dependencies] -anyhow = "1.0.57" -fs-err = "2.7.0" rbx_binary = { path = "../rbx_binary", features = ["unstable_text_format"] } rbx_xml = { path = "../rbx_xml" } + serde_yaml = "0.8.24" -structopt = "0.3.26" +clap = { version = "4.5.4", features = ["derive"] } + +fs-err = "2.7.0" +anyhow = "1.0.57" +env_logger = "0.11.3" +log = "0.4.21" diff --git a/rbx_util/src/convert.rs b/rbx_util/src/convert.rs new file mode 100644 index 000000000..bbdbb43db --- /dev/null +++ b/rbx_util/src/convert.rs @@ -0,0 +1,64 @@ +use std::{ + io::{BufReader, BufWriter}, + path::PathBuf, +}; + +use anyhow::Context; +use clap::Parser; +use fs_err::File; + +use crate::ModelKind; + +#[derive(Debug, Parser)] +pub struct ConvertCommand { + /// A path to the file to convert. + input_path: PathBuf, + /// A path to the desired output for the conversion. The output format is + /// deteremined by the file extension of this path. + output_path: PathBuf, +} + +impl ConvertCommand { + pub fn run(&self) -> anyhow::Result<()> { + let input_kind = ModelKind::from_path(&self.input_path)?; + let output_kind = ModelKind::from_path(&self.output_path)?; + + let input_file = BufReader::new(File::open(&self.input_path)?); + + log::debug!("Reading file into WeakDom"); + let dom = match input_kind { + ModelKind::Xml => { + let options = rbx_xml::DecodeOptions::new() + .property_behavior(rbx_xml::DecodePropertyBehavior::ReadUnknown); + + rbx_xml::from_reader(input_file, options) + .with_context(|| format!("Failed to read {}", self.input_path.display()))? + } + + ModelKind::Binary => rbx_binary::from_reader(input_file) + .with_context(|| format!("Failed to read {}", self.input_path.display()))?, + }; + + let root_ids = dom.root().children(); + + let output_file = BufWriter::new(File::create(&self.output_path)?); + + log::debug!("Writing into new file at {}", self.output_path.display()); + match output_kind { + ModelKind::Xml => { + let options = rbx_xml::EncodeOptions::new() + .property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown); + + rbx_xml::to_writer(output_file, &dom, root_ids, options) + .with_context(|| format!("Failed to write {}", self.output_path.display()))?; + } + + ModelKind::Binary => { + rbx_binary::to_writer(output_file, &dom, root_ids) + .with_context(|| format!("Failed to write {}", self.output_path.display()))?; + } + } + + Ok(()) + } +} diff --git a/rbx_util/src/main.rs b/rbx_util/src/main.rs index 1cc6405e1..0410527e0 100644 --- a/rbx_util/src/main.rs +++ b/rbx_util/src/main.rs @@ -1,119 +1,130 @@ -use std::io::{self, BufReader, BufWriter}; -use std::path::{Path, PathBuf}; +mod convert; +mod view_binary; + use std::process; +use std::{path::Path, str::FromStr}; + +use clap::Parser; +use convert::ConvertCommand; -use anyhow::{anyhow, bail, Context}; -use fs_err::File; -use structopt::StructOpt; +use view_binary::ViewBinaryCommand; -#[derive(Debug, StructOpt)] +#[derive(Debug, Parser)] +#[clap(name = "rbx_util", about)] struct Options { - #[structopt(subcommand)] + #[clap(flatten)] + global: GlobalOptions, + #[clap(subcommand)] subcommand: Subcommand, } -#[derive(Debug, StructOpt)] +impl Options { + fn run(self) -> anyhow::Result<()> { + match self.subcommand { + Subcommand::ViewBinary(command) => command.run(), + Subcommand::Convert(command) => command.run(), + } + } +} + +#[derive(Debug, Parser)] enum Subcommand { - /// Convert a model or place file in one format to another. - Convert { input: PathBuf, output: PathBuf }, + /// Displays a binary file in a text format. + ViewBinary(ViewBinaryCommand), + /// Convert between the XML and binary formats for places and models. + Convert(ConvertCommand), +} - /// View a binary file as an undefined text representation. - ViewBinary { input: PathBuf }, +#[derive(Debug, Parser, Clone, Copy)] +struct GlobalOptions { + /// Sets verbosity level. Can be specified multiple times. + #[clap(long, short, global(true), action = clap::ArgAction::Count)] + verbosity: u8, + /// Set color behavior. Valid values are auto, always, and never. + #[clap(long, global(true), default_value = "auto")] + color: ColorChoice, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ModelKind { +pub enum ModelKind { Binary, Xml, } impl ModelKind { - fn from_path(path: &Path) -> anyhow::Result { + pub fn from_path(path: &Path) -> anyhow::Result { + log::trace!("Resolving type of file for path {}", path.display()); match path.extension().and_then(|ext| ext.to_str()) { Some("rbxm") | Some("rbxl") => Ok(ModelKind::Binary), Some("rbxmx") | Some("rbxlx") => Ok(ModelKind::Xml), - _ => Err(anyhow!( - "not a Roblox model or place file: {}", - path.display() - )), + _ => anyhow::bail!("not a Roblox model or place file: {}", path.display()), } } } -fn run(options: Options) -> anyhow::Result<()> { - match options.subcommand { - Subcommand::Convert { input, output } => convert(&input, &output)?, - Subcommand::ViewBinary { input } => view_binary(&input)?, - } - - Ok(()) -} - -fn convert(input_path: &Path, output_path: &Path) -> anyhow::Result<()> { - let input_kind = ModelKind::from_path(input_path)?; - let output_kind = ModelKind::from_path(output_path)?; - - let input_file = BufReader::new(File::open(input_path)?); - - let dom = match input_kind { - ModelKind::Xml => { - let options = rbx_xml::DecodeOptions::new() - .property_behavior(rbx_xml::DecodePropertyBehavior::ReadUnknown); - - rbx_xml::from_reader(input_file, options) - .with_context(|| format!("Failed to read {}", input_path.display()))? - } +fn main() { + let options = Options::parse(); - ModelKind::Binary => rbx_binary::from_reader(input_file) - .with_context(|| format!("Failed to read {}", input_path.display()))?, + let log_filter = match options.global.verbosity { + 0 => "info", + 1 => "info,rbx_binary=debug,rbx_xml=debug,rbx_util=debug", + 2 => "debug,rbx_binary=trace,rbx_xml=trace,rbx_util=trace", + _ => "trace", }; - let root_ids = dom.root().children(); + let log_env = env_logger::Env::default().default_filter_or(log_filter); + env_logger::Builder::from_env(log_env) + .format_module_path(false) + .format_timestamp(None) + .format_indent(Some(8)) + .write_style(options.global.color.into()) + .init(); - let output_file = BufWriter::new(File::create(output_path)?); - - match output_kind { - ModelKind::Xml => { - let options = rbx_xml::EncodeOptions::new() - .property_behavior(rbx_xml::EncodePropertyBehavior::WriteUnknown); + if let Err(err) = options.run() { + eprintln!("{:?}", err); + process::exit(1); + } +} - rbx_xml::to_writer(output_file, &dom, root_ids, options) - .with_context(|| format!("Failed to write {}", output_path.display()))?; - } +#[derive(Debug, Clone, Copy)] +pub enum ColorChoice { + Auto, + Always, + Never, +} - ModelKind::Binary => { - rbx_binary::to_writer(output_file, &dom, root_ids) - .with_context(|| format!("Failed to write {}", output_path.display()))?; +impl FromStr for ColorChoice { + type Err = anyhow::Error; + + fn from_str(source: &str) -> Result { + match source { + "auto" => Ok(ColorChoice::Auto), + "always" => Ok(ColorChoice::Always), + "never" => Ok(ColorChoice::Never), + _ => anyhow::bail!( + "Invalid color choice '{source}'. Valid values are: auto, always, never" + ), } } - - Ok(()) } -fn view_binary(input_path: &Path) -> anyhow::Result<()> { - let input_kind = ModelKind::from_path(input_path)?; - - if input_kind != ModelKind::Binary { - bail!("not a binary model or place file: {}", input_path.display()); +impl From for clap::ColorChoice { + fn from(value: ColorChoice) -> Self { + match value { + ColorChoice::Auto => clap::ColorChoice::Auto, + ColorChoice::Always => clap::ColorChoice::Always, + ColorChoice::Never => clap::ColorChoice::Never, + } } - - let input_file = BufReader::new(File::open(input_path)?); - - let model = rbx_binary::text_format::DecodedModel::from_reader(input_file); - - let stdout = io::stdout(); - let output = BufWriter::new(stdout.lock()); - serde_yaml::to_writer(output, &model)?; - - Ok(()) } -fn main() { - let options = Options::from_args(); - - if let Err(err) = run(options) { - eprintln!("{:?}", err); - process::exit(1); +impl From for env_logger::WriteStyle { + fn from(value: ColorChoice) -> Self { + match value { + ColorChoice::Auto => env_logger::WriteStyle::Auto, + ColorChoice::Always => env_logger::WriteStyle::Always, + ColorChoice::Never => env_logger::WriteStyle::Never, + } } } diff --git a/rbx_util/src/view_binary.rs b/rbx_util/src/view_binary.rs new file mode 100644 index 000000000..0e96c0eaa --- /dev/null +++ b/rbx_util/src/view_binary.rs @@ -0,0 +1,37 @@ +use std::{ + io::{self, BufReader, BufWriter}, + path::PathBuf, +}; + +use clap::Parser; +use fs_err::File; + +use crate::ModelKind; + +#[derive(Debug, Parser)] +pub struct ViewBinaryCommand { + /// The file to emit the contents of. + input: PathBuf, +} + +impl ViewBinaryCommand { + pub fn run(&self) -> anyhow::Result<()> { + let input_kind = ModelKind::from_path(&self.input)?; + + if input_kind != ModelKind::Binary { + anyhow::bail!("not a binary model or place file: {}", self.input.display()); + } + + let input_file = BufReader::new(File::open(&self.input)?); + + log::debug!("Decoding file into text format"); + let model = rbx_binary::text_format::DecodedModel::from_reader(input_file); + + log::debug!("Writing to stdout"); + let stdout = io::stdout(); + let output = BufWriter::new(stdout.lock()); + serde_yaml::to_writer(output, &model)?; + + Ok(()) + } +}