use std::fs::File;
use std::path::Path;
use anyhow::{anyhow, Context};
use cadmpeg_core::decode::{DecodeArena, DecodeContext, View};
use cadmpeg_ir::codec::DecodeOptions;
use cadmpeg_ir::CadIr;
use cadmpeg_registry::{
ForcedInput, InputCatalog, ResolveSourceError, ResolvedSource, DETECTION_PREFIX_LEN,
};
use crate::application::artifact_store;
use crate::application::document::LoadedDocument;
use crate::application::refusal::ApplicationError;
pub(crate) fn detection_failure(error: &ResolveSourceError) -> anyhow::Error {
anyhow!("{error}; pass --input-format")
}
pub(crate) fn load_artifact(
catalog: &InputCatalog,
path: &Path,
options: DecodeOptions,
forced: Option<ForcedInput>,
) -> Result<LoadedDocument, ApplicationError> {
let arena = DecodeArena::new();
let ctx = DecodeContext::new(&arena, &options.policy, options.container_only);
let mut file = File::open(path).with_context(|| format!("opening {}", path.display()))?;
let mut prefix =
cadmpeg_container::compound::read_detection_prefix(&ctx, &mut file, DETECTION_PREFIX_LEN)?;
let resolved = catalog
.resolve_seekable_source(&ctx, View::over_retained(&prefix), &mut file, forced)
.map_err(|error| match error {
ResolveSourceError::Codec(error) => ApplicationError::from(error),
error => ApplicationError::from(detection_failure(&error)),
})?;
match resolved {
ResolvedSource::Native { codec, selection } => {
let format_id = codec.id();
ctx.complete_input(&mut file, &mut prefix)?;
let result = codec.decode_with_context(&ctx, View::over_retained(&prefix), &options);
ctx.finish_session()?;
let result = result.map_err(|failure| {
ApplicationError::from_decode_failure(path, format_id, failure)
})?;
return Ok(LoadedDocument::decoded(
result,
selection,
cadmpeg_core::decode::u64_from_index(prefix.len()),
));
}
ResolvedSource::Cadir => {}
ResolvedSource::Unrecognized => {
return Err(anyhow!(
"unrecognized format for {}; supported: FCStd, f3d, Inventor IPT/IAM, sldprt, CATPart, NX/Creo prt, Rhino 3DM, IGES, STEP, ASM sat/smt/smb/sab, .cadir.json; use --input-format to override detection",
path.display()
)
.into());
}
}
let max_bytes = options.policy.limits.max_input_bytes;
ctx.complete_input(&mut file, &mut prefix)?;
ctx.charge_work(
cadmpeg_core::decode::u64_from_index(prefix.len()),
"validate CADIR input UTF-8",
)?;
let text =
String::from_utf8(prefix).map_err(|error| anyhow!("invalid CADIR UTF-8: {error}"))?;
ctx.finish_session()?;
let ir = CadIr::from_json(&text).map_err(|e| {
anyhow!(
"{} is neither a recognized CAD file nor a valid .cadir.json document: {e}",
path.display()
)
})?;
let Some(sidecar) = artifact_store::load_matching_sidecar(path, text.as_bytes(), max_bytes)?
else {
return Ok(LoadedDocument::neutral(
ir,
cadmpeg_core::decode::u64_from_index(text.len()),
));
};
Ok(LoadedDocument::restored(
ir,
sidecar.report,
sidecar.fidelity,
cadmpeg_core::decode::u64_from_index(text.len()),
))
}
#[cfg(test)]
#[allow(clippy::default_trait_access, clippy::unwrap_used)]
mod tests {
use super::load_artifact;
use crate::application::document::LoadOrigin;
use cadmpeg_ir::codec::DecodeOptions;
use cadmpeg_ir::CadIr;
use cadmpeg_ir::{report::decode::DecodeReport, DecodeSidecar, SourceFidelity};
use cadmpeg_registry::{ForcedInput, InputCatalog};
#[test]
fn input_acquisition_resource_refusal_stays_typed() {
let directory = tempfile::tempdir().expect("directory");
let path = directory.path().join("input.json");
std::fs::write(&path, b"12345").expect("input");
let mut options = DecodeOptions::default();
options.policy.limits.max_input_bytes = 4;
let error = load_artifact(
&InputCatalog::with_builtins(),
&path,
options,
Some(ForcedInput::Cadir),
)
.expect_err("input limit");
assert!(
matches!(error.refusal(), Some(crate::application::refusal::ConversionRefusal::ResourceLimit { limit, .. }) if limit.dimension == cadmpeg_core::decode::ResourceDimension::InputBytes && limit.limit == 4)
);
}
#[test]
fn matching_sidecar_restores_decoded_origin_and_mismatch_is_hard_error() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("part.cadir.json");
let text = CadIr::empty().to_canonical_json().unwrap();
std::fs::write(&path, &text).unwrap();
let report: DecodeReport = serde_json::from_value(serde_json::json!({
"identity": {"classification": "unclassified", "format": "test"},
"transfer": {"transfer": "full", "geometry_transferred": false},
"losses": [],
"notes": [],
}))
.unwrap();
let sidecar = DecodeSidecar::bind_sha256(
cadmpeg_ir::hash::digest::Sha256Digest::digest(text.as_bytes()),
report,
SourceFidelity::default(),
);
std::fs::write(
cadmpeg_ir::decode_sidecar_path(&path).unwrap(),
sidecar.to_canonical_json().unwrap(),
)
.unwrap();
let outcome = load_artifact(
&InputCatalog::with_builtins(),
&path,
DecodeOptions::default(),
Some(ForcedInput::Cadir),
)
.unwrap();
assert!(matches!(outcome.origin, LoadOrigin::Restored { .. }));
assert_eq!(outcome.input_bytes, u64::try_from(text.len()).unwrap());
std::fs::write(&path, format!("{text}\n")).unwrap();
let error = load_artifact(
&InputCatalog::with_builtins(),
&path,
DecodeOptions::default(),
Some(ForcedInput::Cadir),
)
.unwrap_err();
assert!(error.to_string().contains("does not match"));
}
}