#![cfg_attr(test, allow(clippy::unwrap_used))]
mod application;
mod commands;
mod input_format;
mod inspect;
mod loader;
mod query;
mod registry_view;
mod reject_json;
use std::path::PathBuf;
use std::process::ExitCode;
use cadmpeg_registry::{ForcedInput, InputCatalog};
use clap::{Args, Parser, Subcommand, ValueEnum};
use registry_view::{print_dialects, print_formats};
use crate::application::artifact_store::{OptionalFileDestination, OutputDestinations};
use crate::application::transcoder::{ConversionDestinations, DestinationPolicy, LossPolicy};
#[derive(Debug, Parser)]
#[command(
name = "cadmpeg",
version,
about = "Convert and inspect native CAD files.",
long_about = "Convert and inspect native CAD files.\n\n\
Reads vendor CAD files and writes them in another format.",
after_help = "Examples:\n \
cadmpeg convert part.sldprt -o part.step\n \
cadmpeg inspect part.sldprt\n \
cadmpeg diff a.sldprt b.step\n\n\
Exit codes: 0 success, 1 negative verdict (failed check, refused write, files differ), 2 operational error."
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Clone, Args)]
struct InputArgs {
#[arg(
long,
visible_alias = "from",
value_parser = crate::input_format::input_format_parser()
)]
input_format: Option<ForcedInput>,
}
#[derive(Debug, Clone, Args)]
struct DecodeArgs {
#[arg(long)]
container_only: bool,
#[arg(long)]
no_salvage: bool,
#[arg(long, value_enum, default_value_t = LimitProfile::Desktop)]
limits: LimitProfile,
#[arg(long)]
max_input_bytes: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum LimitProfile {
Desktop,
Service,
}
impl DecodeArgs {
fn options(&self) -> cadmpeg_ir::DecodeOptions {
let limits = self.limits.limits(self.max_input_bytes);
let mode = if self.no_salvage {
cadmpeg_core::decode::DecodeMode::Strict
} else {
cadmpeg_core::decode::DecodeMode::Salvage
};
cadmpeg_ir::DecodeOptions {
container_only: self.container_only,
policy: cadmpeg_core::decode::DecodePolicy { mode, limits },
}
}
}
impl LimitProfile {
const fn limits(self, max_input_bytes: Option<u64>) -> cadmpeg_core::decode::ResourceLimits {
let mut limits = match self {
LimitProfile::Desktop => cadmpeg_core::decode::ResourceLimits::desktop(),
LimitProfile::Service => cadmpeg_core::decode::ResourceLimits::service(),
};
if let Some(max_input_bytes) = max_input_bytes {
limits.max_input_bytes = max_input_bytes;
}
limits
}
}
#[derive(Debug, Subcommand)]
enum Command {
#[command(
display_order = 1,
after_help = "Examples:\n cadmpeg convert part.sldprt -o part.step\n cadmpeg convert part.sldprt -o out.3dm --to rhino:archive-80\n cadmpeg convert part.f3d -o out.igs --to 5.1\n cadmpeg convert part.f3d --to step"
)]
Convert {
#[command(flatten)]
file: inspect::FileArg,
#[command(flatten)]
_reject_json: crate::reject_json::RejectJson,
#[arg(short, long, visible_alias = "to", value_name = "FORMAT[:DIALECT]")]
format: Option<String>,
#[command(flatten)]
destinations: ConversionDestinations,
#[arg(long)]
allow_errors: bool,
#[arg(long)]
allow_empty: bool,
#[arg(
long,
value_enum,
num_args = 0..=1,
require_equals = true,
default_missing_value = "any",
value_name = "SCOPE"
)]
reject_lossy: Option<LossPolicy>,
#[command(flatten)]
input_args: InputArgs,
#[command(flatten)]
decode: DecodeArgs,
},
#[command(display_order = 7)]
Formats,
#[command(
display_order = 8,
after_help = "Examples:\n cadmpeg dialects\n cadmpeg dialects rhino"
)]
Dialects {
format: Option<String>,
},
#[command(
display_order = 2,
args_conflicts_with_subcommands = true,
subcommand_negates_reqs = true,
subcommand_help_heading = "Byte tools",
after_help = "Examples:\n cadmpeg inspect part.sldprt"
)]
Inspect(inspect::InspectArgs),
#[command(
display_order = 5,
after_help = "Examples:\n cadmpeg dump part.sldprt -o part.cadir.json"
)]
Dump {
#[command(flatten)]
file: inspect::FileArg,
#[command(flatten)]
_reject_json: crate::reject_json::RejectJson,
#[command(flatten)]
destinations: OutputDestinations,
#[command(flatten)]
input_args: InputArgs,
#[command(flatten)]
decode: DecodeArgs,
},
#[command(
display_order = 6,
subcommand_help_heading = "Views",
after_help = "Examples:\n cadmpeg query losses part.convert.json\n cadmpeg query counts part.cadir.json\n cadmpeg query graph part.cadir.json model.features ID\n cadmpeg query join part.cadir.json model.features native.rhino.unknowns --left-key native_ref --right-key id"
)]
Query {
#[command(subcommand)]
view: query::QueryView,
},
#[command(
mut_arg("output", |arg| arg.long("report").visible_alias("output").help("Write a JSON report to this file")),
display_order = 4,
after_help = "Examples:\n cadmpeg check part.sldprt"
)]
Check {
#[command(flatten)]
file: inspect::FileArg,
#[arg(long)]
json: bool,
#[command(flatten)]
report: OptionalFileDestination,
#[command(flatten)]
input_args: InputArgs,
#[command(flatten)]
decode: DecodeArgs,
},
#[command(
mut_arg("output", |arg| arg.long("report").visible_alias("output").help("Write a JSON report to this file")),
display_order = 3,
after_help = "Examples:\n cadmpeg diff a.sldprt b.step\n cadmpeg diff before.f3d after.f3d\n\n\
Exit status 1 means the models differ.\n\
`inspect cmp` compares raw bytes, not decoded models."
)]
Diff {
a: PathBuf,
b: PathBuf,
#[arg(
long,
value_parser = crate::input_format::input_format_parser()
)]
input_format_a: Option<ForcedInput>,
#[arg(
long,
value_parser = crate::input_format::input_format_parser()
)]
input_format_b: Option<ForcedInput>,
#[arg(long)]
json: bool,
#[command(flatten)]
report: OptionalFileDestination,
#[command(flatten)]
decode: DecodeArgs,
},
}
#[cfg(unix)]
fn reset_sigpipe() {
const SIGPIPE: i32 = 13;
const SIG_DFL: usize = 0;
unsafe extern "C" {
fn signal(signum: i32, handler: usize) -> usize;
}
unsafe {
signal(SIGPIPE, SIG_DFL);
}
}
fn main() -> ExitCode {
#[cfg(unix)]
reset_sigpipe();
let command = Cli::parse().command;
let inputs = InputCatalog::with_builtins();
let result: Result<ExitCode, application::refusal::ApplicationError> = match command {
Command::Inspect(inspect::InspectArgs::Bytes(byte_command)) => {
inspect::run(byte_command).map_err(application::refusal::ApplicationError::from)
}
Command::Inspect(inspect::InspectArgs::Summary(args)) => commands::inspect(
&inputs,
args.file.path(),
args.input_format,
args.json,
args.report.0.as_ref(),
args.limits.limits(args.max_input_bytes),
)
.map(|()| ExitCode::SUCCESS),
Command::Dump {
file,
_reject_json: _,
destinations,
input_args,
decode,
} => commands::dump(
&inputs,
file.path(),
&match destinations.output.0 {
Some(file) => DestinationPolicy::File(file),
None => DestinationPolicy::Stdout {
allow_binary: false,
},
},
destinations.report.as_ref(),
input_args.input_format,
&decode,
)
.map(|()| ExitCode::SUCCESS),
Command::Query { view } => query::run(&view)
.map(|()| ExitCode::SUCCESS)
.map_err(application::refusal::ApplicationError::from),
Command::Check {
file,
json,
report,
input_args,
decode,
} => commands::check_cmd(
&inputs,
file.path(),
input_args.input_format,
&decode,
json,
report.0.as_ref(),
)
.map(|()| ExitCode::SUCCESS),
Command::Diff {
a,
b,
input_format_a,
input_format_b,
json,
report,
decode,
} => commands::diff(
&inputs,
commands::DiffInput {
path: &a,
forced: input_format_a,
},
commands::DiffInput {
path: &b,
forced: input_format_b,
},
&decode,
json,
report.0.as_ref(),
),
Command::Convert {
file,
_reject_json: _,
format,
destinations,
allow_errors,
allow_empty,
reject_lossy,
input_args,
decode,
} => {
let conversion_args = commands::ConversionArgs {
losses: reject_lossy.unwrap_or_default(),
allow_errors,
allow_empty,
destination: destinations.destination,
report: destinations.report,
forced_input: input_args.input_format,
};
commands::convert(
&inputs,
file.path(),
format.as_deref(),
&conversion_args,
&decode,
)
}
.map(|()| ExitCode::SUCCESS),
Command::Formats => print_formats(&inputs)
.map(|()| ExitCode::SUCCESS)
.map_err(application::refusal::ApplicationError::from),
Command::Dialects { format } => print_dialects(format.as_deref())
.map(|()| ExitCode::SUCCESS)
.map_err(anyhow::Error::new)
.map_err(application::refusal::ApplicationError::from),
};
result.unwrap_or_else(|err| {
eprintln!("error: {err:#}");
ExitCode::from(err.exit_code())
})
}