mod beam_lattice;
mod boolean_ops;
mod core;
mod displacement;
mod material;
mod production;
mod secure_content;
mod slice;
mod volumetric;
use crate::error::{Error, Result};
use crate::model::*;
use crate::opc::Package;
use crate::validator;
use boolean_ops::validate_boolean_external_paths;
use production::validate_production_external_paths;
use quick_xml::Reader;
use quick_xml::events::Event;
use secure_content::{load_file_with_decryption, load_keystore};
use std::collections::{HashMap, HashSet};
use std::io::Read;
use beam_lattice::{parse_ball, parse_beam, parse_beamlattice_start};
use core::parse_component;
use displacement::{
parse_disp2dcoord, parse_disp2dgroup_start, parse_displacement2d, parse_normvector,
parse_normvectorgroup_start, validate_displacement_namespace_prefix,
};
use material::{
parse_base_element, parse_base_material, parse_basematerials_start, parse_color_element,
parse_colorgroup_start, parse_composite, parse_compositematerials_start, parse_multi,
parse_multiproperties_start, parse_tex2coord, parse_texture2d, parse_texture2dgroup_start,
validate_texture_file_paths,
};
use slice::{
load_slice_references, parse_slice_polygon_start, parse_slice_segment, parse_slice_start,
parse_slice_vertex, parse_sliceref, parse_slicestack_start,
};
use volumetric::{
parse_boundary, parse_implicit_start, parse_volumetric_property, parse_volumetricdata_start,
parse_volumetricpropertygroup_start, parse_voxel, parse_voxels_start,
};
pub use core::{parse_build_item, parse_object, parse_triangle, parse_vertex};
pub use displacement::parse_displacement_triangle;
const TRANSFORM_MATRIX_SIZE: usize = 12;
const XML_BUFFER_CAPACITY: usize = 4096;
pub fn parse_3mf<R: Read + std::io::Seek>(reader: R) -> Result<Model> {
parse_3mf_with_config(reader, ParserConfig::with_all_extensions())
}
pub fn parse_3mf_with_config<R: Read + std::io::Seek>(
reader: R,
config: ParserConfig,
) -> Result<Model> {
let mut package = Package::open_lenient(reader, config.is_lenient())?;
let thumbnail = package.get_thumbnail_metadata()?;
package.validate_no_model_level_thumbnails()?;
let config_clone = config.clone();
let model_reader = package.get_model_reader()?;
let mut model = parse_model_from_reader(model_reader, config)?;
model.thumbnail = thumbnail;
load_keystore(&mut package, &mut model, &config_clone)?;
load_slice_references(&mut package, &mut model, &config_clone)?;
validate_boolean_external_paths(&mut package, &model, &config_clone)?;
validate_production_external_paths(&mut package, &model, &config_clone)?;
validate_texture_file_paths(&mut package, &model)?;
validator::validate_model_with_config(&model, &config_clone)?;
config_clone.registry().post_parse_all(&mut model)?;
Ok(model)
}
pub fn read_thumbnail<R: Read + std::io::Seek>(reader: R) -> Result<Option<Vec<u8>>> {
let mut package = Package::open(reader)?;
let thumbnail = package.get_thumbnail_metadata()?;
match thumbnail {
Some(thumb) => {
let data = package.get_file_binary(&thumb.path)?;
Ok(Some(data))
}
None => Ok(None),
}
}
pub(crate) fn get_local_name(name_str: &str) -> &str {
if let Some(pos) = name_str.rfind(':') {
&name_str[pos + 1..]
} else {
name_str
}
}
fn get_attr_by_local_name(attrs: &HashMap<String, String>, local_name: &str) -> Option<String> {
attrs.iter().find_map(|(key, value)| {
if get_local_name(key) == local_name {
Some(value.clone())
} else {
None
}
})
}
#[doc(hidden)]
pub fn parse_model_xml(xml: &str) -> Result<Model> {
parse_model_xml_with_config(xml, ParserConfig::with_all_extensions())
}
#[doc(hidden)]
pub fn parse_model_xml_with_config(xml: &str, config: ParserConfig) -> Result<Model> {
let check_len = xml.len().min(2000);
let xml_start = &xml[..check_len];
let xml_start_lower = xml_start.to_lowercase();
if xml_start_lower.contains("<!doctype") {
return Err(Error::InvalidXml(
"DTD declarations are not allowed in 3MF files for security reasons".to_string(),
));
}
let mut reader = Reader::from_str(xml);
reader.config_mut().trim_text(true);
parse_model_from_xml_reader(&mut reader, config)
}
pub fn parse_model_from_reader(reader: impl Read, config: ParserConfig) -> Result<Model> {
let buf_reader = std::io::BufReader::new(reader);
let mut xml_reader = Reader::from_reader(buf_reader);
xml_reader.config_mut().trim_text(true);
parse_model_from_xml_reader(&mut xml_reader, config)
}
fn parse_model_from_xml_reader<R: std::io::BufRead>(
reader: &mut Reader<R>,
config: ParserConfig,
) -> Result<Model> {
let mut model = Model::new();
let mut buf = Vec::with_capacity(XML_BUFFER_CAPACITY);
let mut in_resources = false;
let mut in_build = false;
let mut current_object: Option<Object> = None;
let mut current_mesh: Option<Mesh> = None;
let mut in_basematerials = false;
let mut material_index: usize = 0;
let mut current_basematerialgroup: Option<BaseMaterialGroup> = None;
let mut current_colorgroup: Option<ColorGroup> = None;
let mut in_colorgroup = false;
let mut current_beamset: Option<BeamSet> = None;
let mut in_beamset = false;
let mut in_ballsets = false;
let mut current_slicestack: Option<SliceStack> = None;
let mut in_slicestack = false;
let mut current_slice: Option<Slice> = None;
let mut in_slice = false;
let mut current_slice_polygon: Option<SlicePolygon> = None;
let mut in_slice_polygon = false;
let mut in_slice_vertices = false;
let mut current_boolean_shape: Option<BooleanShape> = None;
let mut in_boolean_shape = false;
let mut current_normvectorgroup: Option<NormVectorGroup> = None;
let mut in_normvectorgroup = false;
let mut current_disp2dgroup: Option<Disp2DGroup> = None;
let mut in_disp2dgroup = false;
let mut current_displacement_mesh: Option<DisplacementMesh> = None;
let mut in_displacement_mesh = false;
let mut current_displacement_triangles_did: Option<usize> = None; let mut in_displacement_triangles = false;
let mut has_displacement_triangles = false;
let mut declared_displacement2d_ids = std::collections::HashSet::<usize>::new();
let mut declared_normvectorgroup_ids = std::collections::HashSet::<usize>::new();
let mut declared_disp2dgroup_ids = std::collections::HashSet::<usize>::new();
let mut current_volumetricdata: Option<VolumetricData> = None;
let mut in_volumetricdata = false;
let mut current_voxelgrid: Option<VoxelGrid> = None;
let mut in_voxels = false;
let mut current_volumetric_propgroup: Option<VolumetricPropertyGroup> = None;
let mut in_volumetric_propgroup = false;
let mut current_texture2dgroup: Option<Texture2DGroup> = None;
let mut in_texture2dgroup = false;
let mut current_compositematerials: Option<CompositeMaterials> = None;
let mut in_compositematerials = false;
let mut current_multiproperties: Option<MultiProperties> = None;
let mut in_multiproperties = false;
let mut in_components = false;
let mut in_trianglesets = false;
let mut resources_count = 0;
let mut build_count = 0;
let mut declared_namespaces: HashMap<String, String> = HashMap::new();
let mut resource_parse_order: usize = 0;
loop {
let event_result = reader.read_event_into(&mut buf);
let is_empty_element = matches!(&event_result, Ok(Event::Empty(_)));
match event_result {
Ok(Event::Decl(_)) => {
}
Ok(Event::DocType(_)) => {
return Err(Error::InvalidXml(
"DTD declarations are not allowed in 3MF files for security reasons"
.to_string(),
));
}
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);
match local_name {
"model" => {
let mut namespaces = HashMap::new();
let mut required_ext_value = None;
let mut recommended_ext_value = None;
let mut all_attrs = HashMap::new();
for attr in e.attributes() {
let attr = attr?;
let key = std::str::from_utf8(attr.key.as_ref())
.map_err(|e| Error::InvalidXml(e.to_string()))?;
let value = std::str::from_utf8(&attr.value)
.map_err(|e| Error::InvalidXml(e.to_string()))?;
all_attrs.insert(key.to_string(), value.to_string());
match key {
"unit" => {
match value {
"micron" | "millimeter" | "centimeter" | "inch"
| "foot" | "meter" => model.unit = value.to_string(),
_ => {
return Err(Error::InvalidXml(format!(
"Invalid unit '{}'. Must be one of: micron, millimeter, centimeter, inch, foot, meter",
value
)));
}
}
}
"xmlns" => model.xmlns = value.to_string(),
"requiredextensions" => {
required_ext_value = Some(value.to_string());
}
"recommendedextensions" => {
if value.trim().is_empty() {
return Err(Error::InvalidXml(
"recommendedextensions attribute cannot be empty"
.to_string(),
));
}
recommended_ext_value = Some(value.to_string());
}
_ => {
if let Some(prefix) = key.strip_prefix("xmlns:") {
namespaces.insert(prefix.to_string(), value.to_string());
}
}
}
}
declared_namespaces = namespaces.clone();
validate_attributes(
&all_attrs,
&[
"unit",
"xml:lang",
"requiredextensions",
"recommendedextensions",
"xmlns",
"thumbnail",
],
"model",
)?;
if let Some(ref ext_value) = required_ext_value {
let (extensions, custom_extensions) =
parse_required_extensions_with_namespaces(ext_value, &namespaces)?;
model.required_extensions = extensions;
model.required_custom_extensions = custom_extensions;
validate_extensions(
&model.required_extensions,
&model.required_custom_extensions,
&config,
)?;
}
if let (Some(req_value), Some(rec_value)) =
(&required_ext_value, &recommended_ext_value)
{
validate_no_duplicate_extensions(req_value, rec_value, &namespaces)?;
}
}
"metadata" => {
let attrs = parse_attributes(reader, e)?;
let name = attrs.get("name").ok_or_else(|| {
Error::InvalidXml(
"Metadata element must have a 'name' attribute".to_string(),
)
})?;
if name.contains(':') {
if let Some(namespace_prefix) = name.split(':').next() {
if namespace_prefix != "xml"
&& namespace_prefix != "xmlns"
&& !declared_namespaces.contains_key(namespace_prefix)
{
return Err(Error::InvalidXml(format!(
"Metadata name '{}' uses undeclared namespace prefix '{}'",
name, namespace_prefix
)));
}
}
}
let preserve = if let Some(preserve_str) = attrs.get("preserve") {
match preserve_str.as_str() {
"0" | "false" => Some(false),
"1" | "true" => Some(true),
_ => {
return Err(Error::InvalidXml(format!(
"Invalid preserve attribute value '{}'. Must be '0', '1', 'false', or 'true'",
preserve_str
)));
}
}
} else {
None
};
if let Ok(Event::Text(t)) = reader.read_event_into(&mut buf) {
let value = t.decode().map_err(|e| Error::InvalidXml(e.to_string()))?;
if model.has_metadata(name) {
return Err(Error::InvalidXml(format!(
"Duplicate metadata name '{}'. Each metadata element must have a unique name attribute",
name
)));
}
let entry = if let Some(preserve_val) = preserve {
crate::model::MetadataEntry::new_with_preserve(
name.clone(),
value.to_string(),
preserve_val,
)
} else {
crate::model::MetadataEntry::new(name.clone(), value.to_string())
};
model.metadata.push(entry);
}
}
"resources" => {
resources_count += 1;
if resources_count > 1 {
return Err(Error::InvalidXml(
"Model must contain exactly one <resources> element".to_string(),
));
}
in_resources = true;
}
"build" => {
build_count += 1;
if build_count > 1 {
return Err(Error::InvalidXml(
"Model must contain exactly one <build> element".to_string(),
));
}
in_build = true;
let attrs = parse_attributes(reader, e)?;
validate_attributes(&attrs, &[], "build")?;
if let Some(p_uuid) = get_attr_by_local_name(&attrs, "UUID") {
model.build.production_uuid = Some(p_uuid);
}
}
"object" if in_resources => {
current_object = Some(parse_object(reader, e)?);
}
"mesh" if in_resources && current_object.is_some() => {
current_mesh = Some(Mesh::with_capacity(1024, 2048));
}
"displacementmesh" if in_resources && current_object.is_some() => {
current_displacement_mesh = Some(DisplacementMesh::new());
in_displacement_mesh = true;
has_displacement_triangles = false; if let Some(ref mut obj) = current_object {
obj.has_extension_shapes = true;
}
}
"components" if in_resources && current_object.is_some() => {
in_components = true;
}
"component" if in_components => {
if let Some(ref mut obj) = current_object {
let component = parse_component(reader, e)?;
obj.components.push(component);
}
}
"vertices" if current_mesh.is_some() => {
}
"vertices" if in_displacement_mesh => {
}
"vertex" if in_displacement_mesh => {
validate_displacement_namespace_prefix(name_str, "vertex")?;
if let Some(ref mut disp_mesh) = current_displacement_mesh {
let vertex = parse_vertex(reader, e)?;
disp_mesh.vertices.push(vertex);
}
}
"vertex" if current_mesh.is_some() => {
if let Some(ref mut mesh) = current_mesh {
let vertex = parse_vertex(reader, e)?;
mesh.vertices.push(vertex);
}
}
"triangles" if in_displacement_mesh => {
validate_displacement_namespace_prefix(name_str, "triangles")?;
if has_displacement_triangles {
return Err(Error::InvalidXml(
"Displacement mesh can only have one <triangles> element"
.to_string(),
));
}
has_displacement_triangles = true;
in_displacement_triangles = true;
let attrs = parse_attributes(reader, e)?;
if let Some(did_str) = attrs.get("did") {
let did = did_str.parse::<usize>()?;
if !declared_disp2dgroup_ids.contains(&did) {
return Err(Error::InvalidXml(format!(
"Triangles element references Disp2DGroup with ID {} which has not been declared yet. \
Resources must be declared before they are referenced.",
did
)));
}
current_displacement_triangles_did = Some(did);
} else {
current_displacement_triangles_did = None;
}
}
"triangles" if current_mesh.is_some() => {
}
"triangle" if in_displacement_triangles => {
validate_displacement_namespace_prefix(name_str, "triangle")?;
if let Some(ref mut disp_mesh) = current_displacement_mesh {
let mut triangle = parse_displacement_triangle(reader, e)?;
if (triangle.d2.is_some() || triangle.d3.is_some())
&& triangle.d1.is_none()
{
return Err(Error::InvalidXml(
"Displacement triangle: d2 or d3 displacement coordinate index specified without d1. \
If d1 is unspecified, no displacement coordinate indices can be used."
.to_string()
));
}
if let Some(did) = triangle.did
&& !declared_disp2dgroup_ids.contains(&did)
{
return Err(Error::InvalidXml(format!(
"Triangle element references Disp2DGroup with ID {} which has not been declared yet. \
Resources must be declared before they are referenced.",
did
)));
}
if triangle.did.is_none() {
triangle.did = current_displacement_triangles_did;
}
if triangle.d1.is_some() && triangle.did.is_none() {
return Err(Error::InvalidXml(
"Displacement triangle: d1 displacement coordinate index is specified but 'did' attribute is not. \
The 'did' must be specified either on the <triangle> or <triangles> element."
.to_string()
));
}
disp_mesh.triangles.push(triangle);
}
}
"triangle" if current_mesh.is_some() => {
if let Some(ref mut mesh) = current_mesh {
let triangle = parse_triangle(reader, e)?;
mesh.triangles.push(triangle);
}
}
"item" if in_build => {
let item = parse_build_item(reader, e)?;
model.build.items.push(item);
}
"basematerials" if in_resources => {
in_basematerials = true;
material_index = 0;
let group = parse_basematerials_start(reader, e, resource_parse_order)?;
resource_parse_order += 1;
current_basematerialgroup = Some(group);
}
"base" if in_basematerials => {
if let Some(ref mut group) = current_basematerialgroup {
let base = parse_base_element(reader, e)?;
group.materials.push(base);
}
let material = parse_base_material(reader, e, material_index)?;
model.resources.materials.push(material);
material_index += 1;
}
"colorgroup" if in_resources => {
in_colorgroup = true;
let group = parse_colorgroup_start(reader, e, resource_parse_order)?;
resource_parse_order += 1;
current_colorgroup = Some(group);
}
"color" if in_colorgroup => {
if let Some(ref mut colorgroup) = current_colorgroup {
let color = parse_color_element(reader, e, colorgroup.id)?;
colorgroup.colors.push(color);
}
}
"texture2d" if in_resources => {
let texture = parse_texture2d(reader, e, resource_parse_order)?;
resource_parse_order += 1;
model.resources.texture2d_resources.push(texture);
}
"texture2dgroup" if in_resources => {
in_texture2dgroup = true;
let group = parse_texture2dgroup_start(reader, e, resource_parse_order)?;
resource_parse_order += 1;
current_texture2dgroup = Some(group);
}
"tex2coord" if in_texture2dgroup => {
if let Some(ref mut group) = current_texture2dgroup {
let coord = parse_tex2coord(reader, e)?;
group.tex2coords.push(coord);
}
}
"compositematerials" if in_resources => {
in_compositematerials = true;
let group =
parse_compositematerials_start(reader, e, resource_parse_order)?;
resource_parse_order += 1;
current_compositematerials = Some(group);
}
"composite" if in_compositematerials => {
if let Some(ref mut group) = current_compositematerials {
let composite = parse_composite(reader, e)?;
group.composites.push(composite);
}
}
"multiproperties" if in_resources => {
in_multiproperties = true;
let multi = parse_multiproperties_start(reader, e, resource_parse_order)?;
resource_parse_order += 1;
current_multiproperties = Some(multi);
}
"multi" if in_multiproperties => {
if let Some(ref mut group) = current_multiproperties {
let multi = parse_multi(reader, e)?;
group.multis.push(multi);
}
}
"displacement2d" if in_resources => {
let disp = parse_displacement2d(reader, e)?;
let id = disp.id;
model.resources.displacement_maps.push(disp);
declared_displacement2d_ids.insert(id); }
"beamlattice" if current_mesh.is_some() => {
if in_beamset {
return Err(Error::InvalidXml(
"Multiple or nested beamlattice elements are not allowed. \
Each mesh can have only one beamlattice element."
.to_string(),
));
}
in_beamset = true;
if let Some(ref mut obj) = current_object {
obj.has_extension_shapes = true;
}
let beamset = parse_beamlattice_start(reader, e)?;
current_beamset = Some(beamset);
}
"beams" if in_beamset => {
}
"beam" if in_beamset => {
if let Some(ref mut beamset) = current_beamset {
let beam = parse_beam(reader, e)?;
beamset.beams.push(beam);
}
}
"beamsets" if in_beamset => {
}
"beamset" if in_beamset => {
}
"ref" if in_beamset => {
let attrs = parse_attributes(reader, e)?;
if let Some(index_str) = attrs.get("index") {
let index = index_str.parse::<usize>()?;
if let Some(ref mut beamset) = current_beamset {
if in_ballsets {
beamset.ball_set_refs.push(index);
} else {
beamset.beam_set_refs.push(index);
}
}
}
}
"balls" if in_beamset => {
}
"ball" if in_beamset => {
if let Some(ref mut beamset) = current_beamset {
let ball = parse_ball(reader, e)?;
beamset.balls.push(ball);
}
}
"ballsets" if in_beamset => {
in_ballsets = true;
}
"ballset" if in_beamset => {
}
"ballref" if in_beamset => {
let attrs = parse_attributes(reader, e)?;
if let Some(index_str) = attrs.get("index") {
let index = index_str.parse::<usize>()?;
if let Some(ref mut beamset) = current_beamset {
beamset.ball_set_refs.push(index);
}
}
}
"normvectorgroup" if in_resources => {
in_normvectorgroup = true;
let group = parse_normvectorgroup_start(reader, e)?;
current_normvectorgroup = Some(group);
}
"normvector" if in_normvectorgroup => {
if let Some(ref mut nvgroup) = current_normvectorgroup {
let vector = parse_normvector(reader, e)?;
nvgroup.vectors.push(vector);
}
}
"disp2dgroup" if in_resources => {
in_disp2dgroup = true;
let group = parse_disp2dgroup_start(
reader,
e,
&declared_displacement2d_ids,
&declared_normvectorgroup_ids,
)?;
current_disp2dgroup = Some(group);
}
"disp2dcoord" if in_disp2dgroup => {
if let Some(ref mut d2dgroup) = current_disp2dgroup {
let coord = parse_disp2dcoord(reader, e)?;
d2dgroup.coords.push(coord);
}
}
"slicestack" if in_resources => {
in_slicestack = true;
let stack = parse_slicestack_start(reader, e)?;
current_slicestack = Some(stack);
}
"slice" if in_slicestack => {
in_slice = true;
let slice = parse_slice_start(reader, e)?;
if is_empty_element {
if let Some(ref mut slicestack) = current_slicestack {
slicestack.slices.push(slice);
}
in_slice = false;
} else {
current_slice = Some(slice);
}
}
"sliceref" if in_slicestack => {
let slice_ref = parse_sliceref(reader, e)?;
if let Some(ref mut slicestack) = current_slicestack {
slicestack.slice_refs.push(slice_ref);
}
}
"vertices" if in_slice => {
in_slice_vertices = true;
}
"vertex" if in_slice_vertices => {
let vertex = parse_slice_vertex(reader, e)?;
if let Some(ref mut slice) = current_slice {
slice.vertices.push(vertex);
}
}
"polygon" if in_slice => {
in_slice_polygon = true;
let polygon = parse_slice_polygon_start(reader, e)?;
current_slice_polygon = Some(polygon);
}
"segment" if in_slice_polygon => {
let segment = parse_slice_segment(reader, e)?;
if let Some(ref mut polygon) = current_slice_polygon {
polygon.segments.push(segment);
}
}
"booleanshape" if in_resources && current_object.is_some() => {
if in_boolean_shape
|| current_object
.as_ref()
.map(|obj| obj.boolean_shape.is_some())
.unwrap_or(false)
{
return Err(Error::InvalidXml(
"Object can only have one booleanshape element".to_string(),
));
}
let attrs = parse_attributes(reader, e)?;
let objectid = attrs
.get("objectid")
.ok_or_else(|| {
Error::InvalidXml(
"Boolean shape missing objectid attribute".to_string(),
)
})?
.parse::<usize>()?;
let operation = attrs
.get("operation")
.and_then(|s| BooleanOpType::parse(s))
.unwrap_or(BooleanOpType::Union);
let mut shape = BooleanShape::new(objectid, operation);
shape.path = attrs.get("path").cloned();
current_boolean_shape = Some(shape);
in_boolean_shape = true;
}
"boolean" if in_boolean_shape => {
let attrs = parse_attributes(reader, e)?;
let objectid = attrs
.get("objectid")
.ok_or_else(|| {
Error::InvalidXml(
"Boolean operand missing objectid attribute".to_string(),
)
})?
.parse::<usize>()?;
if let Some(ref mut shape) = current_boolean_shape {
let mut operand = BooleanRef::new(objectid);
operand.path = attrs.get("path").cloned();
shape.operands.push(operand);
}
}
"trianglesets" if current_mesh.is_some() => {
in_trianglesets = true;
}
"triangleset" if in_trianglesets => {
let attrs = parse_attributes(reader, e)?;
if let Some(name) = attrs.get("name")
&& name.trim().is_empty()
{
return Err(Error::InvalidXml(
"triangleset name attribute cannot be empty".to_string(),
));
}
}
"ref" if in_trianglesets => {
let attrs = parse_attributes(reader, e)?;
if let Some(index_str) = attrs.get("index") {
let index = index_str.parse::<usize>().map_err(|_| {
Error::InvalidXml(format!("Invalid triangle index: {}", index_str))
})?;
if let Some(ref mesh) = current_mesh {
validate_triangle_index(mesh, index, "ref")?;
}
}
}
"refrange" if in_trianglesets => {
let attrs = parse_attributes(reader, e)?;
if let (Some(start_str), Some(end_str)) =
(attrs.get("startindex"), attrs.get("endindex"))
{
let start_index = start_str.parse::<usize>().map_err(|_| {
Error::InvalidXml(format!(
"Invalid triangle start index: {}",
start_str
))
})?;
let end_index = end_str.parse::<usize>().map_err(|_| {
Error::InvalidXml(format!(
"Invalid triangle end index: {}",
end_str
))
})?;
if start_index > end_index {
return Err(Error::InvalidXml(format!(
"refrange start index {} is greater than end index {}",
start_index, end_index
)));
}
if let Some(ref mesh) = current_mesh {
validate_triangle_index(mesh, start_index, "refrange start")?;
validate_triangle_index(mesh, end_index, "refrange end")?;
}
}
}
"volumetricdata" if in_resources => {
in_volumetricdata = true;
let vol_data = parse_volumetricdata_start(reader, e)?;
current_volumetricdata = Some(vol_data);
}
"boundary" if in_volumetricdata => {
let boundary = parse_boundary(reader, e)?;
if let Some(ref mut vol_data) = current_volumetricdata {
vol_data.boundary = Some(boundary);
}
}
"voxels" if in_volumetricdata => {
in_voxels = true;
let grid = parse_voxels_start(reader, e)?;
current_voxelgrid = Some(grid);
}
"voxel" if in_voxels => {
if let Some(ref mut grid) = current_voxelgrid {
let voxel = parse_voxel(reader, e)?;
grid.voxels.push(voxel);
}
}
"implicit" if in_volumetricdata => {
let implicit = parse_implicit_start(reader, e)?;
if let Some(ref mut vol_data) = current_volumetricdata {
vol_data.implicit = Some(implicit);
}
}
"volumetricpropertygroup" if in_resources => {
in_volumetric_propgroup = true;
let group = parse_volumetricpropertygroup_start(reader, e)?;
current_volumetric_propgroup = Some(group);
}
"property" if in_volumetric_propgroup => {
if let Some(ref mut group) = current_volumetric_propgroup {
let prop = parse_volumetric_property(reader, e)?;
group.properties.push(prop);
}
}
_ => {
let attrs = parse_attributes(reader, e)?;
validate_attribute_values(&attrs, &declared_namespaces)?;
}
}
}
Ok(Event::End(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);
match local_name {
"resources" => {
in_resources = false;
}
"build" => {
in_build = false;
}
"object" => {
if let Some(mut obj) = current_object.take() {
if let Some(mesh) = current_mesh.take() {
obj.mesh = Some(mesh);
}
if let Some(disp_mesh) = current_displacement_mesh.take() {
obj.displacement_mesh = Some(disp_mesh);
}
obj.parse_order = resource_parse_order;
resource_parse_order += 1;
model.resources.objects.push(obj);
}
}
"mesh" => {
}
"displacementmesh" => {
in_displacement_mesh = false;
if let Some(ref obj) = current_object
&& obj.object_type != ObjectType::Model
{
return Err(Error::InvalidXml(format!(
"Object {} with displacementmesh must have type=\"model\", found type=\"{}\"",
obj.id,
match obj.object_type {
ObjectType::Model => "model",
ObjectType::Support => "support",
ObjectType::SolidSupport => "solidsupport",
ObjectType::Surface => "surface",
ObjectType::Other => "other",
}
)));
}
}
"triangles" if in_displacement_triangles => {
in_displacement_triangles = false;
current_displacement_triangles_did = None;
}
"components" => {
in_components = false;
}
"basematerials" => {
if let Some(group) = current_basematerialgroup.take() {
model.resources.base_material_groups.push(group);
}
in_basematerials = false;
}
"colorgroup" => {
if let Some(colorgroup) = current_colorgroup.take() {
model.resources.color_groups.push(colorgroup);
}
in_colorgroup = false;
}
"texture2dgroup" => {
if let Some(group) = current_texture2dgroup.take() {
model.resources.texture2d_groups.push(group);
}
in_texture2dgroup = false;
}
"compositematerials" => {
if let Some(group) = current_compositematerials.take() {
model.resources.composite_materials.push(group);
}
in_compositematerials = false;
}
"multiproperties" => {
if let Some(group) = current_multiproperties.take() {
model.resources.multi_properties.push(group);
}
in_multiproperties = false;
}
"normvectorgroup" => {
if let Some(nvgroup) = current_normvectorgroup.take() {
declared_normvectorgroup_ids.insert(nvgroup.id); model.resources.norm_vector_groups.push(nvgroup);
}
in_normvectorgroup = false;
}
"disp2dgroup" => {
if let Some(d2dgroup) = current_disp2dgroup.take() {
declared_disp2dgroup_ids.insert(d2dgroup.id); model.resources.disp2d_groups.push(d2dgroup);
}
in_disp2dgroup = false;
}
"beamlattice" => {
if let Some(beamset) = current_beamset.take()
&& let Some(ref mut mesh) = current_mesh
{
mesh.beamset = Some(beamset);
}
in_beamset = false;
in_ballsets = false;
}
"slicestack" => {
if let Some(slicestack) = current_slicestack.take() {
model.resources.slice_stacks.push(slicestack);
}
in_slicestack = false;
}
"slice" => {
if let Some(slice) = current_slice.take()
&& let Some(ref mut slicestack) = current_slicestack
{
slicestack.slices.push(slice);
}
in_slice = false;
}
"vertices" => {
in_slice_vertices = false;
}
"polygon" => {
if let Some(polygon) = current_slice_polygon.take()
&& let Some(ref mut slice) = current_slice
{
slice.polygons.push(polygon);
}
in_slice_polygon = false;
}
"booleanshape" => {
if let Some(shape) = current_boolean_shape.take()
&& let Some(ref mut obj) = current_object
{
obj.boolean_shape = Some(shape);
}
in_boolean_shape = false;
}
"trianglesets" => {
in_trianglesets = false;
}
"ballsets" => {
in_ballsets = false;
}
"volumetricdata" => {
if let Some(mut vol_data) = current_volumetricdata.take() {
if let Some(grid) = current_voxelgrid.take() {
vol_data.voxels = Some(grid);
}
model.resources.volumetric_data.push(vol_data);
}
in_volumetricdata = false;
in_voxels = false;
}
"voxels" => {
in_voxels = false;
}
"volumetricpropertygroup" => {
if let Some(group) = current_volumetric_propgroup.take() {
model.resources.volumetric_property_groups.push(group);
}
in_volumetric_propgroup = false;
}
_ => {}
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(Error::Xml(e)),
_ => {}
}
buf.clear();
}
if resources_count == 0 {
return Err(Error::InvalidXml(
"Model must contain a <resources> element".to_string(),
));
}
if build_count == 0 {
return Err(Error::InvalidXml(
"Model must contain a <build> element".to_string(),
));
}
Ok(model)
}
#[allow(dead_code)] fn parse_required_extensions(extensions_str: &str) -> Result<(Vec<Extension>, Vec<String>)> {
parse_required_extensions_with_namespaces(extensions_str, &HashMap::new())
}
fn parse_required_extensions_with_namespaces(
extensions_str: &str,
namespaces: &HashMap<String, String>,
) -> Result<(Vec<Extension>, Vec<String>)> {
let mut extensions = Vec::new();
let mut custom_namespaces = Vec::new();
for item in extensions_str.split_whitespace() {
if let Some(ext) = Extension::from_namespace(item) {
extensions.push(ext);
} else if let Some(namespace_uri) = namespaces.get(item) {
if let Some(ext) = Extension::from_namespace(namespace_uri) {
extensions.push(ext);
} else {
custom_namespaces.push(namespace_uri.clone());
}
} else {
if item.contains("://") {
custom_namespaces.push(item.to_string());
}
}
}
Ok((extensions, custom_namespaces))
}
fn validate_extensions(
required: &[Extension],
required_custom: &[String],
config: &ParserConfig,
) -> Result<()> {
for ext in required {
if !config.supports(ext) {
return Err(Error::UnsupportedExtension(format!(
"Extension '{}' (namespace: {}) is required but not supported",
ext.name(),
ext.namespace()
)));
}
}
for namespace in required_custom {
if !config.has_custom_extension(namespace) {
return Err(Error::UnsupportedExtension(format!(
"Custom extension with namespace '{}' is required but not registered",
namespace
)));
}
}
Ok(())
}
fn validate_no_duplicate_extensions(
required_str: &str,
recommended_str: &str,
namespaces: &HashMap<String, String>,
) -> Result<()> {
let required_items: Vec<&str> = required_str.split_whitespace().collect();
let recommended_items: Vec<&str> = recommended_str.split_whitespace().collect();
let mut required_namespaces = HashSet::new();
for item in required_items {
if let Some(uri) = namespaces.get(item) {
required_namespaces.insert(uri.as_str());
} else {
required_namespaces.insert(item);
}
}
for item in recommended_items {
let resolved = if let Some(uri) = namespaces.get(item) {
uri.as_str()
} else {
item
};
if required_namespaces.contains(resolved) {
return Err(Error::InvalidXml(format!(
"Extension '{}' cannot appear in both requiredextensions and recommendedextensions",
item
)));
}
}
Ok(())
}
fn validate_triangle_index(mesh: &Mesh, index: usize, context: &str) -> Result<()> {
if index >= mesh.triangles.len() {
return Err(Error::InvalidXml(format!(
"{} triangle index {} is out of bounds (mesh has {} triangles)",
context,
index,
mesh.triangles.len()
)));
}
Ok(())
}
#[allow(dead_code)] fn try_handle_custom_element<R: std::io::BufRead>(
reader: &Reader<R>,
e: &quick_xml::events::BytesStart,
config: &ParserConfig,
) -> Result<bool> {
use crate::model::{CustomElementResult, CustomExtensionContext};
let name = e.name();
let name_str =
std::str::from_utf8(name.as_ref()).map_err(|e| Error::InvalidXml(e.to_string()))?;
if let Some((_prefix, local_name)) = name_str.split_once(':') {
let attrs = parse_attributes(reader, e)?;
for (namespace, ext_info) in config.custom_extensions() {
if let Some(handler) = &ext_info.element_handler {
let context = CustomExtensionContext {
element_name: local_name.to_string(),
namespace: namespace.clone(),
attributes: attrs.clone(),
};
match handler(&context) {
Ok(CustomElementResult::Handled) => {
return Ok(true);
}
Ok(CustomElementResult::NotHandled) => {
continue;
}
Err(err) => {
return Err(Error::InvalidXml(format!(
"Custom extension handler error: {}",
err
)));
}
}
}
}
}
Ok(false)
}
pub(crate) fn parse_attributes<R: std::io::BufRead>(
_reader: &Reader<R>,
e: &quick_xml::events::BytesStart,
) -> Result<HashMap<String, String>> {
let mut attrs = HashMap::with_capacity(8);
for attr in e.attributes() {
let attr = attr?;
let key =
std::str::from_utf8(attr.key.as_ref()).map_err(|e| Error::InvalidXml(e.to_string()))?;
let value =
std::str::from_utf8(&attr.value).map_err(|e| Error::InvalidXml(e.to_string()))?;
attrs.insert(key.to_string(), value.to_string());
}
Ok(attrs)
}
pub(crate) fn validate_attribute_values(
attrs: &HashMap<String, String>,
declared_namespaces: &HashMap<String, String>,
) -> Result<()> {
for (key, value) in attrs {
if key.starts_with("xmlns") || key.starts_with("xml:") {
continue;
}
if key == "identifier" || key.ends_with(":identifier") {
continue;
}
if value.contains("://") {
continue;
}
if let Some(colon_pos) = value.find(':') {
let potential_prefix = &value[..colon_pos];
if declared_namespaces.contains_key(potential_prefix) {
return Err(Error::InvalidXml(format!(
"Attribute '{}' has value '{}' which uses namespace prefix '{}:'. \
Namespace prefixes are not allowed in attribute values per 3MF specification",
key, value, potential_prefix
)));
}
}
}
Ok(())
}
pub(crate) fn validate_attributes(
_attrs: &HashMap<String, String>,
_allowed: &[&str],
_element_name: &str,
) -> Result<()> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dtd_rejected_in_xml_string() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [<!ENTITY xxe "evil">]>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources></resources>
<build></build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("DTD"));
}
#[test]
fn test_invalid_unit_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="parsec" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources></resources>
<build></build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("parsec"));
}
#[test]
fn test_all_valid_units() {
for unit in &[
"micron",
"millimeter",
"centimeter",
"inch",
"foot",
"meter",
] {
let xml = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="{unit}" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources></resources>
<build></build>
</model>"#,
unit = unit
);
let model = parse_model_xml(&xml).unwrap();
assert_eq!(model.unit, *unit);
}
}
#[test]
fn test_metadata_with_preserve_true() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<metadata name="Title" preserve="true">My Model</metadata>
<resources></resources>
<build></build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
let entry = model.metadata.iter().find(|m| m.name == "Title").unwrap();
assert_eq!(entry.preserve, Some(true));
}
#[test]
fn test_metadata_with_preserve_false() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<metadata name="Title" preserve="0">My Model</metadata>
<resources></resources>
<build></build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
let entry = model.metadata.iter().find(|m| m.name == "Title").unwrap();
assert_eq!(entry.preserve, Some(false));
}
#[test]
fn test_metadata_with_invalid_preserve() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<metadata name="Title" preserve="maybe">My Model</metadata>
<resources></resources>
<build></build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("preserve"));
}
#[test]
fn test_metadata_with_namespaced_name_valid() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02" xmlns:foo="http://example.com/ns">
<metadata name="foo:Author">Jane</metadata>
<resources></resources>
<build></build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
let entry = model
.metadata
.iter()
.find(|m| m.name == "foo:Author")
.unwrap();
assert_eq!(entry.value, "Jane");
}
#[test]
fn test_metadata_with_undeclared_namespace_prefix_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<metadata name="bar:Author">Jane</metadata>
<resources></resources>
<build></build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("bar"));
}
#[test]
fn test_metadata_with_xml_prefix_allowed() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<metadata name="xml:Author">Jane</metadata>
<resources></resources>
<build></build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
assert!(model.metadata.iter().any(|m| m.name == "xml:Author"));
}
#[test]
fn test_metadata_duplicate_name_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<metadata name="Title">First</metadata>
<metadata name="Title">Second</metadata>
<resources></resources>
<build></build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Duplicate"));
}
#[test]
fn test_recommendedextensions_valid() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
xmlns:m="http://schemas.microsoft.com/3dmanufacturing/material/2015/02"
recommendedextensions="m">
<resources></resources>
<build></build>
</model>"#;
assert!(parse_model_xml(xml).is_ok());
}
#[test]
fn test_empty_recommendedextensions_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
recommendedextensions="">
<resources></resources>
<build></build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("recommendedextensions")
);
}
#[test]
fn test_requiredextensions_via_namespace_prefix() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
xmlns:m="http://schemas.microsoft.com/3dmanufacturing/material/2015/02"
requiredextensions="m">
<resources></resources>
<build></build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
assert!(!model.required_extensions.is_empty());
}
#[test]
fn test_requiredextensions_unknown_rejected() {
let config = ParserConfig::default(); let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
xmlns:m="http://schemas.microsoft.com/3dmanufacturing/material/2015/02"
requiredextensions="m">
<resources></resources>
<build></build>
</model>"#;
let result = parse_model_xml_with_config(xml, config);
assert!(result.is_err());
}
#[test]
fn test_duplicate_required_recommended_extension_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
xmlns:m="http://schemas.microsoft.com/3dmanufacturing/material/2015/02"
requiredextensions="m"
recommendedextensions="m">
<resources></resources>
<build></build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("both"));
}
#[test]
fn test_requiredextensions_direct_uri() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
requiredextensions="http://schemas.microsoft.com/3dmanufacturing/material/2015/02">
<resources></resources>
<build></build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
assert!(!model.required_extensions.is_empty());
}
#[test]
fn test_requiredextensions_unknown_uri_tracked_as_custom() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
xmlns:x="http://example.com/custom/extension"
requiredextensions="x">
<resources></resources>
<build></build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
}
#[test]
fn test_multiple_resources_elements_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources></resources>
<resources></resources>
<build></build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("exactly one <resources>")
);
}
#[test]
fn test_multiple_build_elements_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources></resources>
<build></build>
<build></build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("exactly one <build>")
);
}
#[test]
fn test_missing_resources_element_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<build></build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("<resources>"));
}
#[test]
fn test_missing_build_element_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources></resources>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("<build>"));
}
#[test]
fn test_build_with_production_uuid() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
xmlns:p="http://schemas.microsoft.com/3dmanufacturing/production/2015/06">
<resources></resources>
<build p:UUID="12345678-1234-1234-1234-123456789012"></build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
assert!(model.build.production_uuid.is_some());
}
#[test]
fn test_parse_colorgroup() {
let xml = r##"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
xmlns:m="http://schemas.microsoft.com/3dmanufacturing/material/2015/02">
<resources>
<colorgroup id="1">
<color color="#FF0000"/>
<color color="#00FF00"/>
</colorgroup>
<object id="2" pid="1" pindex="0">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="2"/>
</build>
</model>"##;
let model = parse_model_xml(xml).unwrap();
assert_eq!(model.resources.color_groups.len(), 1);
assert_eq!(model.resources.color_groups[0].colors.len(), 2);
assert_eq!(model.resources.color_groups[0].colors[0], (255, 0, 0, 255));
}
#[test]
fn test_parse_beamlattice() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
xmlns:b="http://schemas.microsoft.com/3dmanufacturing/beamlattice/2017/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="10" y="0" z="0"/>
<vertex x="0" y="10" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
<beamlattice radius="1.0" minlength="0.01" cap="sphere">
<beams>
<beam v1="0" v2="1" r1="1.5" r2="1.5"/>
<beam v1="1" v2="2"/>
</beams>
</beamlattice>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
let mesh = model.resources.objects[0].mesh.as_ref().unwrap();
let beamset = mesh.beamset.as_ref().unwrap();
assert_eq!(beamset.beams.len(), 2);
assert_eq!(beamset.radius, 1.0);
}
#[test]
fn test_beamlattice_invalid_radius_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
<beamlattice radius="-1.0" minlength="0.01">
<beams/>
</beamlattice>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("radius"));
}
#[test]
fn test_beamlattice_invalid_minlength_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
<beamlattice radius="1.0" minlength="-1.0">
<beams/>
</beamlattice>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("minlength"));
}
#[test]
fn test_beam_r2_without_r1_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
<beamlattice radius="1.0" minlength="0.01">
<beams>
<beam v1="0" v2="1" r2="2.0"/>
</beams>
</beamlattice>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("r1"));
}
#[test]
fn test_beam_negative_r1_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
<beamlattice radius="1.0" minlength="0.01">
<beams>
<beam v1="0" v2="1" r1="-1.0"/>
</beams>
</beamlattice>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("r1"));
}
#[test]
fn test_beam_p2_without_p1_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
<beamlattice radius="1.0" minlength="0.01">
<beams>
<beam v1="0" v2="1" p2="5"/>
</beams>
</beamlattice>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("p1"));
}
#[test]
fn test_beamlattice_with_beamsets_and_ballsets() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="10" y="0" z="0"/>
<vertex x="0" y="10" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
<beamlattice radius="1.0" minlength="0.01" ballmode="all" ballradius="2.0">
<beams>
<beam v1="0" v2="1"/>
<beam v1="1" v2="2"/>
</beams>
<balls>
<ball vindex="0"/>
<ball vindex="1" r="1.5"/>
</balls>
<beamsets>
<beamset>
<ref index="0"/>
</beamset>
</beamsets>
<ballsets>
<ballset>
<ref index="0"/>
<ballref index="1"/>
</ballset>
</ballsets>
</beamlattice>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
let mesh = model.resources.objects[0].mesh.as_ref().unwrap();
let beamset = mesh.beamset.as_ref().unwrap();
assert_eq!(beamset.beams.len(), 2);
assert_eq!(beamset.balls.len(), 2);
assert!(!beamset.beam_set_refs.is_empty());
assert!(!beamset.ball_set_refs.is_empty());
}
#[test]
fn test_multiple_beamlattice_elements_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
<beamlattice radius="1.0" minlength="0.01">
<beams/>
<beamlattice radius="2.0" minlength="0.01">
<beams/>
</beamlattice>
</beamlattice>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
}
#[test]
fn test_parse_slicestack_with_slices() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
xmlns:s="http://schemas.microsoft.com/3dmanufacturing/slice/2015/07">
<resources>
<slicestack id="1" zbottom="0.0">
<slice ztop="1.0">
<vertices>
<vertex x="0" y="0"/>
<vertex x="1" y="0"/>
<vertex x="1" y="1"/>
<vertex x="0" y="1"/>
</vertices>
<polygon startv="0">
<segment v2="1"/>
<segment v2="2"/>
<segment v2="3"/>
<segment v2="0"/>
</polygon>
</slice>
<slice ztop="2.0"/>
</slicestack>
<object id="2" s:slicestackid="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="2"/>
</build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
assert_eq!(model.resources.slice_stacks.len(), 1);
let stack = &model.resources.slice_stacks[0];
assert_eq!(stack.id, 1);
assert_eq!(stack.zbottom, 0.0);
assert_eq!(stack.slices.len(), 2);
let first_slice = &stack.slices[0];
assert_eq!(first_slice.ztop, 1.0);
assert_eq!(first_slice.vertices.len(), 4);
assert_eq!(first_slice.polygons.len(), 1);
assert_eq!(first_slice.polygons[0].segments.len(), 4);
}
#[test]
fn test_slicestack_missing_id_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<slicestack zbottom="0.0">
</slicestack>
</resources>
<build></build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
}
#[test]
fn test_slicestack_missing_zbottom_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<slicestack id="1">
</slicestack>
</resources>
<build></build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
}
#[test]
fn test_slice_missing_ztop_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<slicestack id="1" zbottom="0.0">
<slice>
</slice>
</slicestack>
</resources>
<build></build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
}
#[test]
fn test_parse_boolean_shape() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
<object id="2">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
<object id="3">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
<booleanshape objectid="1" operation="union">
<boolean objectid="2"/>
</booleanshape>
</mesh>
</object>
</resources>
<build>
<item objectid="3"/>
</build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
let obj3 = &model.resources.objects[2];
assert!(obj3.boolean_shape.is_some());
let shape = obj3.boolean_shape.as_ref().unwrap();
assert_eq!(shape.objectid, 1);
assert_eq!(shape.operands.len(), 1);
assert_eq!(shape.operands[0].objectid, 2);
}
#[test]
fn test_multiple_boolean_shapes_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
<booleanshape objectid="1" operation="union">
</booleanshape>
<booleanshape objectid="1" operation="difference">
</booleanshape>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("one booleanshape"));
}
#[test]
fn test_boolean_shape_missing_objectid_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
<booleanshape operation="union">
</booleanshape>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("objectid"));
}
#[test]
fn test_boolean_operand_missing_objectid_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
<booleanshape objectid="1">
<boolean/>
</booleanshape>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("objectid"));
}
#[test]
fn test_parse_trianglesets() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
<vertex x="1" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
<triangle v1="1" v2="3" v3="2"/>
</triangles>
<trianglesets>
<triangleset name="group1">
<ref index="0"/>
<refrange startindex="0" endindex="1"/>
</triangleset>
</trianglesets>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
assert!(parse_model_xml(xml).is_ok());
}
#[test]
fn test_triangleset_empty_name_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
<trianglesets>
<triangleset name=" ">
</triangleset>
</trianglesets>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("name"));
}
#[test]
fn test_triangleset_ref_out_of_bounds_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
<trianglesets>
<triangleset name="g1">
<ref index="99"/>
</triangleset>
</trianglesets>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("out of bounds"));
}
#[test]
fn test_triangleset_refrange_reversed_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
<vertex x="1" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
<triangle v1="1" v2="3" v3="2"/>
</triangles>
<trianglesets>
<triangleset name="g1">
<refrange startindex="1" endindex="0"/>
</triangleset>
</trianglesets>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("start index"));
}
#[test]
fn test_triangleset_refrange_out_of_bounds_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
<trianglesets>
<triangleset name="g1">
<refrange startindex="0" endindex="99"/>
</triangleset>
</trianglesets>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("out of bounds"));
}
#[test]
fn test_parse_all_object_types() {
for obj_type in &["model", "support", "solidsupport", "surface", "other"] {
let xml = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1" type="{t}">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#,
t = obj_type
);
assert!(
parse_model_xml(&xml).is_ok(),
"Failed for type: {}",
obj_type
);
}
}
#[test]
fn test_invalid_object_type_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1" type="invalid_type">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Invalid object type")
);
}
#[test]
fn test_vertex_missing_x_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex y="0" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("x"));
}
#[test]
fn test_vertex_invalid_attribute_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0" w="1"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("w"));
}
#[test]
fn test_vertex_non_finite_x_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="inf" y="0" z="0"/>
</vertices>
<triangles/>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
}
#[test]
fn test_triangle_missing_v1_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("v1"));
}
#[test]
fn test_triangle_invalid_attribute_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2" color="red"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("color"));
}
#[test]
fn test_build_item_wrong_transform_size_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="1" transform="1 0 0 0 1 0 0 0 1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("12"));
}
#[test]
fn test_build_item_non_finite_transform_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="1" transform="1 0 0 0 1 0 0 0 1 inf 0 0"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("finite"));
}
#[test]
fn test_get_local_name_with_prefix() {
assert_eq!(get_local_name("m:colorgroup"), "colorgroup");
assert_eq!(get_local_name("p:UUID"), "UUID");
assert_eq!(get_local_name("s:slicestack"), "slicestack");
}
#[test]
fn test_get_local_name_without_prefix() {
assert_eq!(get_local_name("object"), "object");
assert_eq!(get_local_name("mesh"), "mesh");
assert_eq!(get_local_name("vertex"), "vertex");
}
#[test]
fn test_get_local_name_multiple_colons() {
assert_eq!(get_local_name("a:b:c"), "c");
}
#[test]
fn test_validate_attribute_values_allows_uris() {
let mut attrs = HashMap::new();
attrs.insert("xmlns:foo".to_string(), "http://example.com".to_string());
attrs.insert("type".to_string(), "http://example.com/type".to_string());
let namespaces = HashMap::new();
assert!(validate_attribute_values(&attrs, &namespaces).is_ok());
}
#[test]
fn test_validate_attribute_values_rejects_namespace_prefix_in_value() {
let mut attrs = HashMap::new();
attrs.insert("type".to_string(), "foo:bar".to_string());
let mut namespaces = HashMap::new();
namespaces.insert("foo".to_string(), "http://foo.com".to_string());
let result = validate_attribute_values(&attrs, &namespaces);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("foo"));
}
#[test]
fn test_validate_attribute_values_allows_identifier_with_prefix() {
let mut attrs = HashMap::new();
attrs.insert("identifier".to_string(), "foo:bar".to_string());
let mut namespaces = HashMap::new();
namespaces.insert("foo".to_string(), "http://foo.com".to_string());
assert!(validate_attribute_values(&attrs, &namespaces).is_ok());
}
#[test]
fn test_validate_attribute_values_skips_xmlns() {
let mut attrs = HashMap::new();
attrs.insert("xmlns:foo".to_string(), "foo:namespace".to_string());
let mut namespaces = HashMap::new();
namespaces.insert("foo".to_string(), "http://foo.com".to_string());
assert!(validate_attribute_values(&attrs, &namespaces).is_ok());
}
#[test]
fn test_validate_attributes_allows_unknown_attributes() {
let mut attrs = HashMap::new();
attrs.insert("foo".to_string(), "bar".to_string());
let result = validate_attributes(&attrs, &["id", "name"], "object");
assert!(result.is_ok());
}
#[test]
fn test_validate_attributes_allows_known_attributes() {
let mut attrs = HashMap::new();
attrs.insert("id".to_string(), "1".to_string());
attrs.insert("name".to_string(), "test".to_string());
assert!(validate_attributes(&attrs, &["id", "name"], "object").is_ok());
}
#[test]
fn test_parse_required_extensions_with_namespaces_empty() {
let result = parse_required_extensions_with_namespaces("", &HashMap::new());
assert!(result.is_ok());
let (extensions, custom) = result.unwrap();
assert!(extensions.is_empty());
assert!(custom.is_empty());
}
#[test]
fn test_parse_required_extensions_with_namespaces_known_uri() {
let result = parse_required_extensions_with_namespaces(
"http://schemas.microsoft.com/3dmanufacturing/material/2015/02",
&HashMap::new(),
);
assert!(result.is_ok());
let (extensions, _) = result.unwrap();
assert!(!extensions.is_empty());
}
#[test]
fn test_parse_required_extensions_with_namespaces_prefix() {
let mut namespaces = HashMap::new();
namespaces.insert(
"m".to_string(),
"http://schemas.microsoft.com/3dmanufacturing/material/2015/02".to_string(),
);
let result = parse_required_extensions_with_namespaces("m", &namespaces);
assert!(result.is_ok());
let (extensions, _) = result.unwrap();
assert!(!extensions.is_empty());
}
#[test]
fn test_parse_required_extensions_unknown_uri_tracked() {
let result =
parse_required_extensions_with_namespaces("http://example.com/custom", &HashMap::new());
assert!(result.is_ok());
let (extensions, custom) = result.unwrap();
assert!(extensions.is_empty());
assert!(!custom.is_empty());
}
#[test]
fn test_validate_extensions_all_supported() {
let config = ParserConfig::with_all_extensions();
assert!(validate_extensions(&[], &[], &config).is_ok());
}
#[test]
fn test_validate_extensions_unsupported_extension() {
let config = ParserConfig::default();
let extensions = vec![crate::model::Extension::Material];
let result = validate_extensions(&extensions, &[], &config);
assert!(result.is_err());
}
#[test]
fn test_validate_no_duplicate_extensions_ok() {
let namespaces = HashMap::new();
let result = validate_no_duplicate_extensions("m", "b", &namespaces);
assert!(result.is_ok());
}
#[test]
fn test_validate_no_duplicate_extensions_duplicate_rejected() {
let namespaces = HashMap::new();
let result = validate_no_duplicate_extensions(
"http://example.com/ext",
"http://example.com/ext",
&namespaces,
);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("both"));
}
#[test]
fn test_validate_triangle_index_ok() {
let mut mesh = Mesh::new();
mesh.triangles.push(crate::model::Triangle::new(0, 1, 2));
mesh.triangles.push(crate::model::Triangle::new(1, 2, 3));
assert!(validate_triangle_index(&mesh, 0, "test").is_ok());
assert!(validate_triangle_index(&mesh, 1, "test").is_ok());
}
#[test]
fn test_validate_triangle_index_out_of_bounds() {
let mut mesh = Mesh::new();
mesh.triangles.push(crate::model::Triangle::new(0, 1, 2));
let result = validate_triangle_index(&mesh, 5, "test");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("out of bounds"));
}
#[test]
fn test_unknown_model_attribute_accepted() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
unknown_attr="foo">
<resources></resources>
<build></build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_ok());
}
#[test]
fn test_parse_minimal_model() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
assert_eq!(model.unit, "millimeter");
assert_eq!(
model.xmlns,
"http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
);
assert_eq!(model.resources.objects.len(), 1);
assert_eq!(model.build.items.len(), 1);
}
#[test]
fn test_parse_component_simple() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
<object id="2">
<components>
<component objectid="1"/>
</components>
</object>
</resources>
<build>
<item objectid="2"/>
</build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
assert_eq!(model.resources.objects.len(), 2);
let obj2 = &model.resources.objects[1];
assert_eq!(obj2.id, 2);
assert_eq!(obj2.components.len(), 1);
assert_eq!(obj2.components[0].objectid, 1);
assert!(obj2.components[0].transform.is_none());
}
#[test]
fn test_parse_component_with_transform() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
<object id="2">
<components>
<component objectid="1" transform="1 0 0 0 1 0 0 0 1 10 20 30"/>
</components>
</object>
</resources>
<build>
<item objectid="2"/>
</build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
let obj2 = &model.resources.objects[1];
assert_eq!(obj2.components.len(), 1);
assert_eq!(obj2.components[0].objectid, 1);
let transform = obj2.components[0]
.transform
.expect("Transform should be present");
assert_eq!(
transform,
[
1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 10.0, 20.0, 30.0
]
);
}
#[test]
fn test_parse_multiple_components() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
<object id="2">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="2" y="0" z="0"/>
<vertex x="0" y="2" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
<object id="3">
<components>
<component objectid="1"/>
<component objectid="2" transform="1 0 0 0 1 0 0 0 1 5 5 5"/>
</components>
</object>
</resources>
<build>
<item objectid="3"/>
</build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
assert_eq!(model.resources.objects.len(), 3);
let obj3 = &model.resources.objects[2];
assert_eq!(obj3.id, 3);
assert_eq!(obj3.components.len(), 2);
assert_eq!(obj3.components[0].objectid, 1);
assert!(obj3.components[0].transform.is_none());
assert_eq!(obj3.components[1].objectid, 2);
assert!(obj3.components[1].transform.is_some());
}
#[test]
fn test_parse_model_from_reader() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let model_str = parse_model_xml(xml).unwrap();
let cursor = std::io::Cursor::new(xml.as_bytes());
let config = ParserConfig::with_all_extensions();
let model_reader = parse_model_from_reader(cursor, config).unwrap();
assert_eq!(model_str.unit, model_reader.unit);
assert_eq!(model_str.xmlns, model_reader.xmlns);
assert_eq!(
model_str.resources.objects.len(),
model_reader.resources.objects.len()
);
assert_eq!(model_str.build.items.len(), model_reader.build.items.len());
}
}