Skip to content

Commit

Permalink
Perform revamp of rbx_util to make it easier to add thing to (#412)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dekkonot authored May 13, 2024
1 parent 65cac04 commit c5bb118
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 83 deletions.
10 changes: 10 additions & 0 deletions rbx_util/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
12 changes: 8 additions & 4 deletions rbx_util/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
64 changes: 64 additions & 0 deletions rbx_util/src/convert.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
}
169 changes: 90 additions & 79 deletions rbx_util/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<ModelKind> {
pub fn from_path(path: &Path) -> anyhow::Result<ModelKind> {
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<Self, Self::Err> {
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<ColorChoice> 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<ColorChoice> 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,
}
}
}
37 changes: 37 additions & 0 deletions rbx_util/src/view_binary.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
}

0 comments on commit c5bb118

Please sign in to comment.