use crate::error::{Error, Result};
use crate::model::{Model, ParserConfig};
use crate::opc::Package;
use quick_xml::Reader;
use quick_xml::events::Event;
use std::collections::{HashMap, HashSet};
use std::io::Read;
use super::secure_content::load_file_with_decryption;
use super::{get_local_name, parse_model_xml_with_config};
const MAX_DISPLAYED_OBJECT_IDS: usize = 20;
const XML_BUFFER_CAPACITY: usize = 4096;
pub(super) fn validate_boolean_external_paths<R: Read + std::io::Seek>(
package: &mut Package<R>,
model: &Model,
config: &ParserConfig,
) -> Result<()> {
let mut external_file_cache: HashMap<String, Vec<usize>> = HashMap::new();
for object in &model.resources.objects {
if let Some(ref boolean_shape) = object.boolean_shape {
if let Some(ref path) = boolean_shape.path {
let normalized_path = path.trim_start_matches('/');
let is_encrypted = model
.secure_content
.as_ref()
.map(|sc| {
sc.encrypted_files.iter().any(|encrypted_path| {
let enc_normalized = encrypted_path.trim_start_matches('/');
enc_normalized == normalized_path
})
})
.unwrap_or(false);
if is_encrypted {
continue;
}
if !package.has_file(normalized_path) {
return Err(Error::InvalidModel(format!(
"Object {}: Boolean shape references non-existent external file: {}\n\
The path attribute in <booleanshape> must reference a valid model file in the 3MF package.\n\
Check that:\n\
- The file exists in the package\n\
- The path is correct (case-sensitive)\n\
- The path format follows 3MF conventions (e.g., /3D/filename.model)",
object.id, path
)));
}
validate_external_object_id(
package,
normalized_path,
boolean_shape.objectid,
object.id,
"booleanshape base",
&mut external_file_cache,
model,
config,
)?;
}
for operand in &boolean_shape.operands {
if let Some(ref path) = operand.path {
let normalized_path = path.trim_start_matches('/');
let is_encrypted = model
.secure_content
.as_ref()
.map(|sc| {
sc.encrypted_files.iter().any(|encrypted_path| {
let enc_normalized = encrypted_path.trim_start_matches('/');
enc_normalized == normalized_path
})
})
.unwrap_or(false);
if is_encrypted {
continue;
}
if !package.has_file(normalized_path) {
return Err(Error::InvalidModel(format!(
"Object {}: Boolean operand references non-existent external file: {}\n\
The path attribute in <boolean> must reference a valid model file in the 3MF package.\n\
Check that:\n\
- The file exists in the package\n\
- The path is correct (case-sensitive)\n\
- The path format follows 3MF conventions (e.g., /3D/filename.model)",
object.id, path
)));
}
validate_external_object_id(
package,
normalized_path,
operand.objectid,
object.id,
"boolean operand",
&mut external_file_cache,
model,
config,
)?;
}
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn validate_external_object_id<R: Read + std::io::Seek>(
package: &mut Package<R>,
file_path: &str,
object_id: usize,
referring_object_id: usize,
reference_type: &str,
cache: &mut HashMap<String, Vec<usize>>,
model: &Model,
config: &ParserConfig,
) -> Result<()> {
if !cache.contains_key(file_path) {
let external_xml = load_file_with_decryption(package, file_path, file_path, model, config)?;
let mut reader = Reader::from_str(&external_xml);
reader.config_mut().trim_text(true);
let mut buf = Vec::with_capacity(XML_BUFFER_CAPACITY);
let mut ids = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
let name = e.name();
let name_str = std::str::from_utf8(name.as_ref())
.map_err(|e| Error::InvalidXml(e.to_string()))?;
let local_name = get_local_name(name_str);
if local_name == "object" {
for attr in e.attributes() {
let attr = attr.map_err(|e| Error::InvalidXml(e.to_string()))?;
let attr_name = std::str::from_utf8(attr.key.as_ref())
.map_err(|e| Error::InvalidXml(e.to_string()))?;
if attr_name == "id" {
let id_str = std::str::from_utf8(&attr.value)
.map_err(|e| Error::InvalidXml(e.to_string()))?;
if let Ok(id) = id_str.parse::<usize>() {
ids.push(id);
}
}
}
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(Error::Xml(e)),
_ => {}
}
buf.clear();
}
cache.insert(file_path.to_string(), ids);
}
let object_ids = cache.get(file_path).unwrap();
if !object_ids.contains(&object_id) {
let display_ids: Vec<usize> = object_ids
.iter()
.take(MAX_DISPLAYED_OBJECT_IDS)
.copied()
.collect();
let id_display = if object_ids.len() > MAX_DISPLAYED_OBJECT_IDS {
format!("{:?} ... ({} total)", display_ids, object_ids.len())
} else {
format!("{:?}", display_ids)
};
return Err(Error::InvalidModel(format!(
"Object {}: {} references object ID {} in external file '{}', but that object does not exist.\n\
Available object IDs in external file: {}\n\
Check that the referenced object ID is correct.",
referring_object_id, reference_type, object_id, file_path, id_display
)));
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(super) fn validate_external_object_reference<R: Read + std::io::Seek>(
package: &mut Package<R>,
file_path: &str,
object_id: usize,
_expected_uuid: &Option<String>,
reference_context: &str,
cache: &mut HashMap<String, Vec<(usize, Option<String>)>>,
model: &Model,
config: &ParserConfig,
) -> Result<()> {
if !cache.contains_key(file_path) {
let external_xml = load_file_with_decryption(package, file_path, file_path, model, config)?;
let mut reader = Reader::from_str(&external_xml);
reader.config_mut().trim_text(true);
let mut buf = Vec::with_capacity(XML_BUFFER_CAPACITY);
let mut info = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
let name = e.name();
let name_str = std::str::from_utf8(name.as_ref())
.map_err(|e| Error::InvalidXml(e.to_string()))?;
let local_name = get_local_name(name_str);
if local_name == "object" {
let mut obj_id = None;
let mut obj_uuid = None;
for attr in e.attributes() {
let attr = attr.map_err(|e| Error::InvalidXml(e.to_string()))?;
let attr_name = std::str::from_utf8(attr.key.as_ref())
.map_err(|e| Error::InvalidXml(e.to_string()))?;
match attr_name {
"id" => {
let id_str = std::str::from_utf8(&attr.value)
.map_err(|e| Error::InvalidXml(e.to_string()))?;
obj_id = id_str.parse::<usize>().ok();
}
"p:UUID" => {
let uuid_str = std::str::from_utf8(&attr.value)
.map_err(|e| Error::InvalidXml(e.to_string()))?;
obj_uuid = Some(uuid_str.to_string());
}
_ => {}
}
}
if let Some(id) = obj_id {
info.push((id, obj_uuid));
}
} else if local_name == "component" {
for attr in e.attributes() {
let attr = attr.map_err(|e| Error::InvalidXml(e.to_string()))?;
let attr_name = std::str::from_utf8(attr.key.as_ref())
.map_err(|e| Error::InvalidXml(e.to_string()))?;
if attr_name == "p:path" {
let path_value = std::str::from_utf8(&attr.value)
.map_err(|e| Error::InvalidXml(e.to_string()))?;
return Err(Error::InvalidModel(format!(
"External model file '{}' contains a component with p:path=\"{}\". \
Per 3MF Production Extension specification (Chapter 2), only components \
in the root model file may have p:path attributes. Non-root model files \
must only reference objects within the same file. This restriction \
prevents component reference chains across multiple files.",
file_path, path_value
)));
}
}
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(Error::Xml(e)),
_ => {}
}
buf.clear();
}
cache.insert(file_path.to_string(), info);
}
let object_info = cache.get(file_path).unwrap();
let found_obj = object_info.iter().find(|(id, _)| *id == object_id);
if found_obj.is_none() {
let available_ids: Vec<usize> = object_info
.iter()
.map(|(id, _)| *id)
.take(MAX_DISPLAYED_OBJECT_IDS)
.collect();
let id_display = if object_info.len() > MAX_DISPLAYED_OBJECT_IDS {
format!("{:?} ... ({} total)", available_ids, object_info.len())
} else {
format!("{:?}", available_ids)
};
return Err(Error::InvalidModel(format!(
"{}: References object ID {} in external file '{}', but that object does not exist.\n\
Available object IDs in external file: {}\n\
Check that the referenced object ID is correct.",
reference_context, object_id, file_path, id_display
)));
}
Ok(())
}
pub(super) fn validate_external_model_triangles<R: Read + std::io::Seek>(
package: &mut Package<R>,
file_path: &str,
model: &Model,
validated_files: &mut HashSet<String>,
config: &ParserConfig,
) -> Result<()> {
if validated_files.contains(file_path) {
return Ok(());
}
let is_encrypted = model
.secure_content
.as_ref()
.map(|sc| {
sc.encrypted_files.iter().any(|encrypted_path| {
let enc_normalized = encrypted_path.trim_start_matches('/');
enc_normalized == file_path
})
})
.unwrap_or(false);
if is_encrypted {
validated_files.insert(file_path.to_string());
return Ok(());
}
let external_xml = load_file_with_decryption(package, file_path, file_path, model, config)?;
let external_config = ParserConfig::with_all_extensions()
.with_custom_extension(
"http://schemas.3mf.io/3dmanufacturing/displacement/2023/10",
"Displacement 2023/10",
)
.with_custom_extension(
"http://schemas.microsoft.com/3dmanufacturing/trianglesets/2021/07",
"TriangleSets",
);
let external_model = match parse_model_xml_with_config(&external_xml, external_config) {
Ok(model) => model,
Err(e) => {
return Err(Error::InvalidModel(format!(
"External model file '{}' failed to parse: {}",
file_path, e
)));
}
};
for object in &external_model.resources.objects {
if let Some(ref mesh) = object.mesh {
crate::validator::validate_object_triangle_materials(
object.id,
object.pid,
mesh,
&format!("External model file '{}': Object {}", file_path, object.id),
)?;
}
}
validated_files.insert(file_path.to_string());
Ok(())
}