#![cfg_attr(test, allow(clippy::unwrap_used))]
mod application;
mod commands;
mod inspect;
mod loader;
mod query;
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Args, Parser, Subcommand, ValueEnum};
use crate::application::{ForcedInput, InputCatalog, NativeValidatorCatalog};
use crate::commands::AppCatalogs;
#[cfg(feature = "step")]
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
enum StepTarget {
Ap203e1,
Ap203e2,
#[default]
Ap214,
Ap242e1,
Ap242e2,
Ap242e3,
}
#[cfg(feature = "step")]
impl StepTarget {
fn schema(self) -> cadmpeg_codec_step::StepSchema {
match self {
Self::Ap203e1 => cadmpeg_codec_step::StepSchema::Ap203Edition1,
Self::Ap203e2 => cadmpeg_codec_step::StepSchema::Ap203Edition2,
Self::Ap214 => cadmpeg_codec_step::StepSchema::Ap214,
Self::Ap242e1 => cadmpeg_codec_step::StepSchema::Ap242Edition1,
Self::Ap242e2 => cadmpeg_codec_step::StepSchema::Ap242Edition2,
Self::Ap242e3 => cadmpeg_codec_step::StepSchema::Ap242Edition3,
}
}
}
#[cfg(feature = "iges")]
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
enum IgesTarget {
#[default]
#[value(name = "5.3", alias = "v5.3", alias = "v5_3")]
V5_3,
#[value(name = "5.2", alias = "v5.2", alias = "v5_2")]
V5_2,
#[value(name = "5.1", alias = "v5.1", alias = "v5_1")]
V5_1,
#[value(name = "5.0", alias = "v5.0", alias = "v5_0")]
V5_0,
#[value(name = "4.0", alias = "v4.0", alias = "v4_0")]
V4_0,
}
#[cfg(feature = "iges")]
impl IgesTarget {
const fn options(self) -> cadmpeg_codec_iges::IgesWriteOptions {
cadmpeg_codec_iges::IgesWriteOptions {
version: match self {
Self::V4_0 => cadmpeg_codec_iges::IgesVersion::V4_0,
Self::V5_0 => cadmpeg_codec_iges::IgesVersion::V5_0,
Self::V5_1 => cadmpeg_codec_iges::IgesVersion::V5_1,
Self::V5_3 => cadmpeg_codec_iges::IgesVersion::V5_3,
Self::V5_2 => cadmpeg_codec_iges::IgesVersion::V5_2,
},
}
}
}
#[cfg(feature = "step")]
#[derive(Debug, Clone, Args)]
struct StepOutputArgs {
#[arg(long, value_enum)]
step_target: Option<StepTarget>,
#[arg(long)]
reject_step_losses: bool,
}
#[cfg(feature = "step")]
impl StepOutputArgs {
fn flag_present(&self) -> bool {
self.step_target.is_some() || self.reject_step_losses
}
fn options(&self) -> cadmpeg_codec_step::StepWriteOptions {
cadmpeg_codec_step::StepWriteOptions {
schema: self.step_target.unwrap_or_default().schema(),
unsupported: if self.reject_step_losses {
cadmpeg_codec_step::StepUnsupportedPolicy::Reject
} else {
cadmpeg_codec_step::StepUnsupportedPolicy::Report
},
..Default::default()
}
}
}
#[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, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum Format {
#[value(alias = "json")]
Cadir,
#[cfg(feature = "step")]
Step,
#[cfg(feature = "fcstd")]
Fcstd,
#[cfg(feature = "f3d")]
F3d,
#[cfg(feature = "sldprt")]
Sldprt,
#[cfg(feature = "rhino")]
#[value(alias = "3dm")]
Rhino,
#[cfg(feature = "iges")]
#[value(alias = "igs")]
Iges,
}
#[cfg(feature = "rhino")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum RhinoVersion {
#[value(name = "50", alias = "5")]
V5,
#[value(name = "60", alias = "6")]
V6,
#[value(name = "70", alias = "7")]
V7,
#[value(name = "80", alias = "8")]
V8,
}
#[cfg(feature = "rhino")]
impl RhinoVersion {
const fn codec(self) -> cadmpeg_codec_rhino::RhinoArchiveVersion {
match self {
Self::V5 => cadmpeg_codec_rhino::RhinoArchiveVersion::V5,
Self::V6 => cadmpeg_codec_rhino::RhinoArchiveVersion::V6,
Self::V7 => cadmpeg_codec_rhino::RhinoArchiveVersion::V7,
Self::V8 => cadmpeg_codec_rhino::RhinoArchiveVersion::V8,
}
}
}
impl Format {
fn from_extension(extension: &str) -> Option<Self> {
match extension.to_ascii_lowercase().as_str() {
"cadir" | "json" => Some(Self::Cadir),
#[cfg(feature = "step")]
"step" | "stp" => Some(Self::Step),
#[cfg(feature = "fcstd")]
"fcstd" => Some(Self::Fcstd),
#[cfg(feature = "f3d")]
"f3d" => Some(Self::F3d),
#[cfg(feature = "sldprt")]
"sldprt" => Some(Self::Sldprt),
#[cfg(feature = "rhino")]
"3dm" => Some(Self::Rhino),
#[cfg(feature = "iges")]
"iges" | "igs" => Some(Self::Iges),
_ => None,
}
}
fn is_geometry_export(self) -> bool {
match self {
Self::Cadir => false,
#[cfg(feature = "step")]
Self::Step => true,
#[cfg(feature = "fcstd")]
Self::Fcstd => true,
#[cfg(feature = "f3d")]
Self::F3d => true,
#[cfg(feature = "sldprt")]
Self::Sldprt => true,
#[cfg(feature = "rhino")]
Self::Rhino => true,
#[cfg(feature = "iges")]
Self::Iges => true,
}
}
fn is_binary_container(self) -> bool {
match self {
#[cfg(feature = "fcstd")]
Self::Fcstd => true,
#[cfg(feature = "f3d")]
Self::F3d => true,
#[cfg(feature = "sldprt")]
Self::Sldprt => true,
#[cfg(feature = "rhino")]
Self::Rhino => true,
_ => false,
}
}
fn from_path(path: Option<&std::path::Path>) -> Option<Self> {
path.and_then(std::path::Path::extension)
.and_then(|extension| extension.to_str())
.and_then(Self::from_extension)
}
fn name(self) -> &'static str {
match self {
Self::Cadir => "cadir",
#[cfg(feature = "step")]
Self::Step => "step",
#[cfg(feature = "fcstd")]
Self::Fcstd => "fcstd",
#[cfg(feature = "f3d")]
Self::F3d => "f3d",
#[cfg(feature = "sldprt")]
Self::Sldprt => "sldprt",
#[cfg(feature = "rhino")]
Self::Rhino => "rhino",
#[cfg(feature = "iges")]
Self::Iges => "iges",
}
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum InputFormat {
#[cfg(feature = "fcstd")]
Fcstd,
#[cfg(feature = "f3d")]
F3d,
#[cfg(feature = "inventor")]
#[value(alias = "ipt", alias = "iam")]
Inventor,
#[cfg(feature = "sldprt")]
Sldprt,
#[cfg(feature = "catia")]
#[value(alias = "catia")]
Catpart,
#[cfg(feature = "nx")]
Nx,
#[cfg(feature = "creo")]
Creo,
#[cfg(feature = "rhino")]
#[value(alias = "3dm")]
Rhino,
#[cfg(feature = "iges")]
#[value(alias = "igs")]
Iges,
#[cfg(feature = "step")]
Step,
#[cfg(feature = "sat")]
#[value(alias = "smt", alias = "smb", alias = "sab")]
Sat,
Cadir,
}
impl InputFormat {
fn resolution(self) -> ForcedInput {
match self {
#[cfg(feature = "fcstd")]
Self::Fcstd => ForcedInput::Codec("fcstd"),
#[cfg(feature = "f3d")]
Self::F3d => ForcedInput::Codec("f3d"),
#[cfg(feature = "inventor")]
Self::Inventor => ForcedInput::Codec("inventor"),
#[cfg(feature = "sldprt")]
Self::Sldprt => ForcedInput::Codec("sldprt"),
#[cfg(feature = "catia")]
Self::Catpart => ForcedInput::Codec("catia"),
#[cfg(feature = "nx")]
Self::Nx => ForcedInput::Codec("nx"),
#[cfg(feature = "creo")]
Self::Creo => ForcedInput::Codec("creo"),
#[cfg(feature = "rhino")]
Self::Rhino => ForcedInput::Codec("rhino"),
#[cfg(feature = "iges")]
Self::Iges => ForcedInput::Codec("iges"),
#[cfg(feature = "step")]
Self::Step => ForcedInput::Codec("step"),
#[cfg(feature = "sat")]
Self::Sat => ForcedInput::Codec("sat"),
Self::Cadir => ForcedInput::Cadir,
}
}
}
#[derive(Debug, Clone, Args)]
struct InputArgs {
#[arg(long, visible_alias = "from", value_enum)]
input_format: Option<InputFormat>,
}
impl InputArgs {
fn forced(&self) -> Option<ForcedInput> {
self.input_format.map(InputFormat::resolution)
}
}
#[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,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum LimitProfile {
Desktop,
Service,
}
impl DecodeArgs {
fn options(&self) -> cadmpeg_ir::DecodeOptions {
let limits = self.limits.limits();
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) -> cadmpeg_core::decode::ResourceLimits {
match self {
LimitProfile::Desktop => cadmpeg_core::decode::ResourceLimits::desktop(),
LimitProfile::Service => cadmpeg_core::decode::ResourceLimits::service(),
}
}
}
#[derive(Debug, Subcommand)]
enum Command {
#[command(
display_order = 1,
after_help = "Examples:\n cadmpeg convert part.sldprt -o part.step\n cadmpeg convert part.f3d -f step"
)]
Convert {
#[arg(required_unless_present = "input_flag")]
input: Option<PathBuf>,
#[arg(
long = "input",
value_name = "FILE",
hide = true,
conflicts_with = "input"
)]
input_flag: Option<PathBuf>,
#[arg(long, hide = true)]
json: bool,
#[arg(long, hide = true)]
binary_stdout: bool,
#[arg(short, long, visible_alias = "to", value_enum)]
format: Option<Format>,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
force: bool,
#[arg(long)]
report: Option<PathBuf>,
#[arg(long)]
allow_errors: bool,
#[arg(long)]
allow_empty: bool,
#[arg(long)]
reject_lossy: bool,
#[cfg(feature = "rhino")]
#[arg(long, value_enum)]
rhino_target: Option<RhinoVersion>,
#[cfg(feature = "iges")]
#[arg(long, value_enum)]
iges_target: Option<IgesTarget>,
#[command(flatten)]
input_args: InputArgs,
#[command(flatten)]
decode: DecodeArgs,
#[cfg(feature = "step")]
#[command(flatten)]
step: StepOutputArgs,
},
#[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 {
#[arg(required_unless_present = "input_flag")]
input: Option<PathBuf>,
#[arg(
long = "input",
value_name = "FILE",
hide = true,
conflicts_with = "input"
)]
input_flag: Option<PathBuf>,
#[arg(long)]
json: bool,
#[arg(short = 'o', long, visible_alias = "output")]
report: Option<PathBuf>,
#[arg(long)]
force: bool,
#[arg(long, value_enum, default_value_t = LimitProfile::Desktop)]
limits: LimitProfile,
#[command(flatten)]
input_args: InputArgs,
#[command(subcommand)]
bytes: Option<inspect::ByteCommand>,
},
#[command(
display_order = 5,
after_help = "Examples:\n cadmpeg dump part.sldprt -o part.cadir.json"
)]
Dump {
#[arg(required_unless_present = "input_flag")]
input: Option<PathBuf>,
#[arg(
long = "input",
value_name = "FILE",
hide = true,
conflicts_with = "input"
)]
input_flag: Option<PathBuf>,
#[arg(long, hide = true)]
json: bool,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
force: bool,
#[arg(long)]
report: Option<PathBuf>,
#[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(
display_order = 4,
after_help = "Examples:\n cadmpeg check part.sldprt"
)]
Check {
#[arg(required_unless_present = "input_flag")]
input: Option<PathBuf>,
#[arg(
long = "input",
value_name = "FILE",
hide = true,
conflicts_with = "input"
)]
input_flag: Option<PathBuf>,
#[arg(long)]
json: bool,
#[arg(short = 'o', long, visible_alias = "output")]
report: Option<PathBuf>,
#[arg(long)]
force: bool,
#[command(flatten)]
input_args: InputArgs,
#[command(flatten)]
decode: DecodeArgs,
},
#[command(
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_enum)]
input_format_a: Option<InputFormat>,
#[arg(long, value_enum)]
input_format_b: Option<InputFormat>,
#[arg(long)]
json: bool,
#[arg(short = 'o', long, visible_alias = "output")]
report: Option<PathBuf>,
#[arg(long)]
force: bool,
#[command(flatten)]
decode: DecodeArgs,
},
}
fn resolve_input(positional: Option<PathBuf>, flag: Option<PathBuf>) -> PathBuf {
positional
.or(flag)
.expect("clap requires one input spelling")
}
fn misdirected_json(command: &str) -> anyhow::Error {
anyhow::anyhow!(
"--json is not an output selector on {command}; the artifact format comes from \
--format/-o, and the machine-readable report from --report FILE, projected \
with `cadmpeg query`"
)
}
#[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 catalogs = AppCatalogs {
inputs: InputCatalog::with_builtins(),
validators: NativeValidatorCatalog::with_builtins(),
};
let result = match command {
Command::Inspect {
input,
input_flag,
json,
report,
force,
limits,
input_args,
bytes,
} => match bytes {
Some(byte_command) => inspect::run(byte_command),
None => {
let input = resolve_input(input, input_flag);
commands::inspect(
&catalogs,
&input,
input_args.forced(),
json,
report.as_deref(),
force,
limits.limits(),
)
.map(|()| ExitCode::SUCCESS)
}
},
Command::Dump {
input,
input_flag,
json,
output,
force,
report,
input_args,
decode,
} => {
if json {
Err(anyhow::anyhow!(
"dump writes the CADIR JSON artifact itself; its standard output is \
already JSON when -o is omitted; the dump report goes to --report FILE, \
projected with `cadmpeg query`"
))
} else {
commands::dump(
&catalogs,
&resolve_input(input, input_flag),
output.as_deref(),
force,
report.as_deref(),
input_args.forced(),
&decode,
)
}
}
.map(|()| ExitCode::SUCCESS),
Command::Query { view } => query::run(&view).map(|()| ExitCode::SUCCESS),
Command::Check {
input,
input_flag,
json,
report,
force,
input_args,
decode,
} => commands::check_cmd(
&catalogs,
&resolve_input(input, input_flag),
input_args.forced(),
&decode,
json,
report.as_deref(),
force,
)
.map(|()| ExitCode::SUCCESS),
Command::Diff {
a,
b,
input_format_a,
input_format_b,
json,
report,
force,
decode,
} => commands::diff(
&catalogs,
commands::DiffInput {
path: &a,
forced: input_format_a.map(InputFormat::resolution),
},
commands::DiffInput {
path: &b,
forced: input_format_b.map(InputFormat::resolution),
},
&decode,
json,
report.as_deref(),
force,
),
Command::Convert {
input,
input_flag,
json,
binary_stdout,
format,
output,
force,
report,
allow_errors,
allow_empty,
reject_lossy,
#[cfg(feature = "rhino")]
rhino_target,
#[cfg(feature = "iges")]
iges_target,
input_args,
decode,
#[cfg(feature = "step")]
step,
} => {
if json {
Err(misdirected_json("convert"))
} else {
let plan = commands::ConversionPlan {
force,
report,
binary_stdout,
allow_errors,
allow_empty,
reject_lossy,
#[cfg(feature = "rhino")]
rhino_target: rhino_target.map(RhinoVersion::codec),
#[cfg(feature = "step")]
step_options: step.flag_present().then(|| step.options()),
#[cfg(feature = "step")]
step_flag_present: step.flag_present(),
#[cfg(feature = "iges")]
iges_options: iges_target.map(IgesTarget::options),
forced_input: input_args.forced(),
};
commands::convert(
&catalogs,
&resolve_input(input, input_flag),
format,
output.as_deref(),
&plan,
&decode,
)
}
}
.map(|()| ExitCode::SUCCESS),
};
result.unwrap_or_else(|err| {
eprintln!("error: {err:#}");
if let Some(refusal) = err.downcast_ref::<application::ConversionRefusal>() {
ExitCode::from(refusal.exit_code())
} else {
ExitCode::from(2)
}
})
}