use std::fs::File;
use std::path::Path;
use anyhow::{anyhow, Context, Result};
use cadmpeg_ir::codec::{Confidence, DecodeOptions};
use cadmpeg_ir::CadIr;
use crate::application::{
ArtifactStore, ForcedInput, InputCatalog, LoadOrigin, LoadedDocument, ResolvedSource,
};
pub const DETECTION_PREFIX_LEN: usize = 128 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LoadNotice {
LowConfidenceDetection {
format_id: &'static str,
confidence: Confidence,
},
}
#[derive(Debug)]
pub struct LoadOutcome {
pub document: LoadedDocument,
pub notices: Vec<LoadNotice>,
}
pub fn read_prefix(path: &Path, n: usize) -> Result<Vec<u8>> {
ArtifactStore::read_prefix(path, n)
}
pub fn load_artifact(
catalog: &InputCatalog,
path: &Path,
options: DecodeOptions,
forced: Option<ForcedInput>,
) -> Result<LoadOutcome> {
let prefix = ArtifactStore::read_prefix(path, DETECTION_PREFIX_LEN)?;
let resolved = catalog
.resolve_source(&prefix, forced)
.map_err(|error| anyhow!(error.to_string()))?;
let mut notices = Vec::new();
match resolved {
ResolvedSource::Native {
codec,
format_id,
confidence,
} => {
if let Some(confidence) = confidence.filter(|value| *value < Confidence::High) {
notices.push(LoadNotice::LowConfidenceDetection {
format_id,
confidence,
});
}
let mut f = File::open(path).with_context(|| format!("opening {}", path.display()))?;
let result = codec
.decode(&mut f, &options)
.with_context(|| format!("decoding {} as {}", path.display(), format_id))?;
return Ok(LoadOutcome {
document: LoadedDocument::decoded(result),
notices,
});
}
ResolvedSource::Cadir => {}
}
if forced.is_none() && prefix.iter().find(|byte| !byte.is_ascii_whitespace()) != Some(&b'{') {
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()
));
}
let max_bytes = options.policy.limits.max_input_bytes;
let text = ArtifactStore::read_bounded_text(path, max_bytes)
.with_context(|| format!("reading {} as a .cadir.json document", path.display()))?;
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) = ArtifactStore::load_matching_sidecar(path, text.as_bytes(), max_bytes)?
else {
return Ok(LoadOutcome {
document: LoadedDocument::neutral(ir),
notices,
});
};
Ok(LoadOutcome {
document: LoadedDocument {
ir,
origin: LoadOrigin::Decoded {
report: sidecar.report,
fidelity: sidecar.fidelity,
},
},
notices,
})
}
#[cfg(test)]
#[allow(clippy::default_trait_access, clippy::unwrap_used)]
mod tests {
use super::*;
use cadmpeg_ir::units::Units;
use cadmpeg_ir::{DecodeReport, DecodeSidecar, SourceFidelity};
#[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(Units::default()).to_canonical_json().unwrap();
std::fs::write(&path, &text).unwrap();
let report = DecodeReport {
format: "test".into(),
container_only: false,
geometry_transferred: false,
coverage: Default::default(),
transfer_ledger: cadmpeg_ir::report::TransferLedger::default(),
losses: Vec::new(),
notes: Vec::new(),
};
let sidecar = DecodeSidecar::bind(text.as_bytes(), report, SourceFidelity::default());
std::fs::write(
ArtifactStore::sidecar_path(&path),
sidecar.to_canonical_json().unwrap(),
)
.unwrap();
let outcome = load_artifact(
&InputCatalog::with_builtins(),
&path,
DecodeOptions::default(),
Some(ForcedInput::Cadir),
)
.unwrap();
assert!(matches!(
outcome.document.origin,
LoadOrigin::Decoded { .. }
));
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"));
}
}