1use ogeom_core::FastMap;
19use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
20use ogeom_math::{Point, Vector};
21use ogeom_topo::Triangulation;
22use std::fmt::Write as _;
23
24use crate::image::{Image, Wrap};
25use crate::mesh_formats::MeshColours;
26use crate::xml;
27
28#[derive(Debug, Clone)]
30pub struct Object<'a> {
31 pub mesh: &'a Triangulation,
33 pub name: Option<String>,
35}
36
37#[must_use]
42pub fn write_3mf(objects: &[Object<'_>]) -> Vec<u8> {
43 let content_types = r#"<?xml version="1.0" encoding="UTF-8"?>
44<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
45 <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
46 <Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/>
47</Types>
48"#;
49 let relationships = r#"<?xml version="1.0" encoding="UTF-8"?>
50<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
51 <Relationship Target="/3D/3dmodel.model" Id="rel0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/>
52</Relationships>
53"#;
54
55 let mut model = String::from(
56 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<model unit=\"millimeter\" \
57 xml:lang=\"en-US\" \
58 xmlns=\"http://schemas.microsoft.com/3dmanufacturing/core/2015/02\">\n \
59 <resources>\n",
60 );
61 let mut items = String::new();
62 for (i, object) in objects.iter().enumerate() {
63 if object.mesh.triangles.is_empty() {
64 continue;
65 }
66 let id = i + 1;
67 match &object.name {
68 Some(name) => {
69 let _ = writeln!(
70 model,
71 " <object id=\"{id}\" type=\"model\" name=\"{}\">",
72 escaped(name)
73 );
74 }
75 None => {
76 let _ = writeln!(model, " <object id=\"{id}\" type=\"model\">");
77 }
78 }
79 model.push_str(" <mesh>\n <vertices>\n");
80 for p in &object.mesh.positions {
81 let _ = writeln!(
82 model,
83 " <vertex x=\"{}\" y=\"{}\" z=\"{}\"/>",
84 p.x, p.y, p.z
85 );
86 }
87 model.push_str(" </vertices>\n <triangles>\n");
88 for [a, b, c] in &object.mesh.triangles {
89 let _ = writeln!(
90 model,
91 " <triangle v1=\"{a}\" v2=\"{b}\" v3=\"{c}\"/>"
92 );
93 }
94 model.push_str(" </triangles>\n </mesh>\n </object>\n");
95 let _ = writeln!(items, " <item objectid=\"{id}\"/>");
96 }
97 model.push_str(" </resources>\n <build>\n");
98 model.push_str(&items);
99 model.push_str(" </build>\n</model>\n");
100
101 archive(&[
102 ("[Content_Types].xml", content_types.as_bytes()),
103 ("_rels/.rels", relationships.as_bytes()),
104 ("3D/3dmodel.model", model.as_bytes()),
105 ])
106}
107
108fn escaped(text: &str) -> String {
110 text.replace('&', "&")
111 .replace('<', "<")
112 .replace('>', ">")
113 .replace('"', """)
114 .replace('\'', "'")
115}
116
117fn archive(entries: &[(&str, &[u8])]) -> Vec<u8> {
124 let mut out: Vec<u8> = Vec::new();
125 let mut directory: Vec<u8> = Vec::new();
126 let mut count = 0_u16;
127
128 for (name, data) in entries {
129 let offset = u32::try_from(out.len()).unwrap_or(u32::MAX);
130 let crc = crc32(data);
131 let size = u32::try_from(data.len()).unwrap_or(u32::MAX);
132 let name_bytes = name.as_bytes();
133 let name_len = u16::try_from(name_bytes.len()).unwrap_or(u16::MAX);
134
135 out.extend_from_slice(&0x0403_4b50_u32.to_le_bytes());
137 out.extend_from_slice(&20_u16.to_le_bytes()); out.extend_from_slice(&0_u16.to_le_bytes()); out.extend_from_slice(&0_u16.to_le_bytes()); out.extend_from_slice(&0_u16.to_le_bytes()); out.extend_from_slice(&0_u16.to_le_bytes()); out.extend_from_slice(&crc.to_le_bytes());
143 out.extend_from_slice(&size.to_le_bytes());
144 out.extend_from_slice(&size.to_le_bytes());
145 out.extend_from_slice(&name_len.to_le_bytes());
146 out.extend_from_slice(&0_u16.to_le_bytes()); out.extend_from_slice(name_bytes);
148 out.extend_from_slice(data);
149
150 directory.extend_from_slice(&0x0201_4b50_u32.to_le_bytes());
152 directory.extend_from_slice(&20_u16.to_le_bytes()); directory.extend_from_slice(&20_u16.to_le_bytes()); directory.extend_from_slice(&0_u16.to_le_bytes());
155 directory.extend_from_slice(&0_u16.to_le_bytes());
156 directory.extend_from_slice(&0_u16.to_le_bytes());
157 directory.extend_from_slice(&0_u16.to_le_bytes());
158 directory.extend_from_slice(&crc.to_le_bytes());
159 directory.extend_from_slice(&size.to_le_bytes());
160 directory.extend_from_slice(&size.to_le_bytes());
161 directory.extend_from_slice(&name_len.to_le_bytes());
162 directory.extend_from_slice(&0_u16.to_le_bytes()); directory.extend_from_slice(&0_u16.to_le_bytes()); directory.extend_from_slice(&0_u16.to_le_bytes()); directory.extend_from_slice(&0_u16.to_le_bytes()); directory.extend_from_slice(&0_u32.to_le_bytes()); directory.extend_from_slice(&offset.to_le_bytes());
168 directory.extend_from_slice(name_bytes);
169 count += 1;
170 }
171
172 let directory_offset = u32::try_from(out.len()).unwrap_or(u32::MAX);
173 let directory_size = u32::try_from(directory.len()).unwrap_or(u32::MAX);
174 out.extend_from_slice(&directory);
175 out.extend_from_slice(&0x0605_4b50_u32.to_le_bytes());
177 out.extend_from_slice(&0_u16.to_le_bytes()); out.extend_from_slice(&0_u16.to_le_bytes()); out.extend_from_slice(&count.to_le_bytes());
180 out.extend_from_slice(&count.to_le_bytes());
181 out.extend_from_slice(&directory_size.to_le_bytes());
182 out.extend_from_slice(&directory_offset.to_le_bytes());
183 out.extend_from_slice(&0_u16.to_le_bytes()); out
185}
186
187pub(crate) fn crc32(data: &[u8]) -> u32 {
190 const TABLE: [u32; 256] = {
191 let mut table = [0_u32; 256];
192 let mut i = 0_u32;
193 while i < 256 {
194 let mut crc = i;
195 let mut k = 0;
196 while k < 8 {
197 crc = if crc & 1 != 0 {
198 (crc >> 1) ^ 0xEDB8_8320
199 } else {
200 crc >> 1
201 };
202 k += 1;
203 }
204 table[i as usize] = crc;
205 i += 1;
206 }
207 table
208 };
209 let mut crc = 0xFFFF_FFFF_u32;
210 for byte in data {
211 crc = (crc >> 8) ^ TABLE[((crc ^ u32::from(*byte)) & 0xFF) as usize];
212 }
213 !crc
214}
215
216struct Entry {
220 name: String,
221 method: u16,
222 flags: u16,
223 crc: u32,
224 compressed: usize,
225 size: usize,
226 header: usize,
227}
228
229fn u16_at(bytes: &[u8], at: usize) -> OgeomResult<u16> {
230 match at.checked_add(2).and_then(|end| bytes.get(at..end)) {
231 Some(b) => Ok(u16::from_le_bytes([b[0], b[1]])),
232 None => ogeom_bail!(Construction, "the archive is cut short"),
233 }
234}
235
236fn u32_at(bytes: &[u8], at: usize) -> OgeomResult<u32> {
237 match at.checked_add(4).and_then(|end| bytes.get(at..end)) {
238 Some(b) => Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]])),
239 None => ogeom_bail!(Construction, "the archive is cut short"),
240 }
241}
242
243fn directory(bytes: &[u8]) -> OgeomResult<Vec<Entry>> {
246 const END: u32 = 0x0605_4b50;
247 const LOCATOR: u32 = 0x0706_4b50;
248 const END64: u32 = 0x0606_4b50;
249 let floor = bytes.len().saturating_sub(22 + 0xFFFF);
252 let mut end = None;
253 let mut at = bytes.len().saturating_sub(22);
254 while at >= floor && bytes.len() >= 22 {
255 if u32_at(bytes, at)? == END {
256 end = Some(at);
257 break;
258 }
259 if at == 0 {
260 break;
261 }
262 at -= 1;
263 }
264 let Some(end) = end else {
265 ogeom_bail!(Construction, "these bytes are not a ZIP archive");
266 };
267 let zip64 = end >= 20 && u32_at(bytes, end - 20)? == LOCATOR;
268 if !zip64 && (u16_at(bytes, end + 4)? != 0 || u16_at(bytes, end + 6)? != 0) {
271 ogeom_bail!(Construction, "the archive spans several disks");
272 }
273 let mut count = u64::from(u16_at(bytes, end + 10)?);
274 let mut offset = u64::from(u32_at(bytes, end + 16)?);
275 if zip64 {
280 let at = offset_in(bytes, u64_at(bytes, end - 20 + 8)?)?;
281 if u32_at(bytes, at)? != END64 {
282 ogeom_bail!(
283 Construction,
284 "the archive's ZIP64 locator points at no ZIP64 end record"
285 );
286 }
287 if u32_at(bytes, at + 16)? != 0 || u32_at(bytes, at + 20)? != 0 {
288 ogeom_bail!(Construction, "the archive spans several disks");
289 }
290 count = u64_at(bytes, at + 32)?;
291 offset = u64_at(bytes, at + 48)?;
292 } else if count == 0xFFFF || offset == 0xFFFF_FFFF {
293 ogeom_bail!(
294 Construction,
295 "the archive's end record is saturated and no ZIP64 record follows it"
296 );
297 }
298 let mut entries = Vec::new();
299 let mut at = offset_in(bytes, offset)?;
300 for _ in 0..count {
301 if u32_at(bytes, at)? != 0x0201_4b50 {
302 ogeom_bail!(Construction, "the archive's central directory is damaged");
303 }
304 let name_len = usize::from(u16_at(bytes, at + 28)?);
305 let extra_len = usize::from(u16_at(bytes, at + 30)?);
306 let comment_len = usize::from(u16_at(bytes, at + 32)?);
307 let Some(name) = bytes.get(at + 46..at + 46 + name_len) else {
308 ogeom_bail!(Construction, "the archive is cut short");
309 };
310 let name = String::from_utf8_lossy(name).into_owned();
311 let mut size = u64::from(u32_at(bytes, at + 24)?);
312 let mut compressed = u64::from(u32_at(bytes, at + 20)?);
313 let mut header = u64::from(u32_at(bytes, at + 42)?);
314 if [size, compressed, header].contains(&0xFFFF_FFFF) {
318 let Some(extra) = extra_field(bytes, at + 46 + name_len, extra_len, 0x0001) else {
319 ogeom_bail!(
320 Construction,
321 "the entry {name} has a saturated field and no ZIP64 extra"
322 );
323 };
324 let mut read = 0;
325 for field in [&mut size, &mut compressed, &mut header] {
326 if *field == 0xFFFF_FFFF {
327 *field = u64_at(extra, read)?;
328 read += 8;
329 }
330 }
331 }
332 entries.push(Entry {
333 name,
334 flags: u16_at(bytes, at + 8)?,
335 method: u16_at(bytes, at + 10)?,
336 crc: u32_at(bytes, at + 16)?,
337 compressed: offset_in(bytes, compressed)?,
338 size: to_usize(size)?,
339 header: offset_in(bytes, header)?,
340 });
341 at += 46 + name_len + extra_len + comment_len;
342 }
343 disjoint(&entries)?;
344 Ok(entries)
345}
346
347fn disjoint(entries: &[Entry]) -> OgeomResult<()> {
356 let mut spans: Vec<(usize, usize, &str)> = entries
357 .iter()
358 .map(|e| {
359 let end = e.header.saturating_add(30).saturating_add(e.compressed);
360 (e.header, end, e.name.as_str())
361 })
362 .collect();
363 spans.sort_unstable_by_key(|&(start, _, _)| start);
364 for pair in spans.windows(2) {
365 let ((_, end, first), (start, _, second)) = (pair[0], pair[1]);
366 if start < end {
367 ogeom_bail!(
368 Construction,
369 "the entries {first} and {second} overlap in the archive"
370 );
371 }
372 }
373 Ok(())
374}
375
376fn u64_at(bytes: &[u8], at: usize) -> OgeomResult<u64> {
377 match at.checked_add(8).and_then(|end| bytes.get(at..end)) {
378 Some(b) => Ok(u64::from_le_bytes([
379 b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
380 ])),
381 None => ogeom_bail!(Construction, "the archive is cut short"),
382 }
383}
384
385fn offset_in(bytes: &[u8], value: u64) -> OgeomResult<usize> {
388 match usize::try_from(value) {
389 Ok(v) if v <= bytes.len() => Ok(v),
390 _ => ogeom_bail!(Construction, "the archive names an offset past its own end"),
391 }
392}
393
394fn to_usize(value: u64) -> OgeomResult<usize> {
395 match usize::try_from(value) {
396 Ok(v) => Ok(v),
397 Err(_) => ogeom_bail!(
398 Construction,
399 "the archive names an offset past this machine's reach"
400 ),
401 }
402}
403
404fn extra_field(bytes: &[u8], at: usize, len: usize, id: u16) -> Option<&[u8]> {
407 let fields = bytes.get(at..at + len)?;
408 let mut i = 0;
409 while i + 4 <= fields.len() {
410 let tag = u16::from_le_bytes([fields[i], fields[i + 1]]);
411 let size = usize::from(u16::from_le_bytes([fields[i + 2], fields[i + 3]]));
412 let data = fields.get(i + 4..i + 4 + size)?;
413 if tag == id {
414 return Some(data);
415 }
416 i += 4 + size;
417 }
418 None
419}
420
421fn contents(bytes: &[u8], entry: &Entry) -> OgeomResult<Vec<u8>> {
424 let name = &entry.name;
425 if entry.flags & 1 != 0 {
426 ogeom_bail!(Construction, "the entry {name} is encrypted");
427 }
428 if u32_at(bytes, entry.header)? != 0x0403_4b50 {
429 ogeom_bail!(
430 Construction,
431 "the entry {name} has no local header where the directory puts it"
432 );
433 }
434 let name_len = usize::from(u16_at(bytes, entry.header + 26)?);
435 let extra_len = usize::from(u16_at(bytes, entry.header + 28)?);
436 let data_at = entry.header + 30 + name_len + extra_len;
437 let Some(data) = data_at
438 .checked_add(entry.compressed)
439 .and_then(|end| bytes.get(data_at..end))
440 else {
441 ogeom_bail!(
442 Construction,
443 "the entry {name} runs past the end of the file"
444 );
445 };
446 let out = match entry.method {
447 0 => data.to_vec(),
448 8 => crate::inflate::inflate(data, entry.size)?,
449 method => ogeom_bail!(
450 Construction,
451 "the entry {name} is compressed with method {method}; only stored and deflated entries are read"
452 ),
453 };
454 if out.len() != entry.size || crc32(&out) != entry.crc {
455 ogeom_bail!(Construction, "the entry {name} does not match its checksum");
456 }
457 Ok(out)
458}
459
460pub fn read_package(bytes: &[u8]) -> OgeomResult<Vec<(String, Vec<u8>)>> {
479 read_package_within(bytes, PackageLimits::default())
480}
481
482#[derive(Debug, Clone, Copy, PartialEq, Eq)]
489pub struct PackageLimits {
490 pub entries: usize,
492 pub bytes: u64,
494}
495
496impl PackageLimits {
497 pub const NONE: Self = Self {
501 entries: usize::MAX,
502 bytes: u64::MAX,
503 };
504
505 pub const DEFAULT: Self = Self {
510 entries: 4096,
511 bytes: 4 << 30,
512 };
513}
514
515impl Default for PackageLimits {
516 fn default() -> Self {
518 Self::DEFAULT
519 }
520}
521
522pub fn read_package_within(
531 bytes: &[u8],
532 limits: PackageLimits,
533) -> OgeomResult<Vec<(String, Vec<u8>)>> {
534 let entries = directory(bytes)?;
535 let files: Vec<&Entry> = entries.iter().filter(|e| !e.name.ends_with('/')).collect();
536 if files.len() > limits.entries {
537 ogeom_bail!(
538 Construction,
539 "the package has {} entries, past the {} allowed",
540 files.len(),
541 limits.entries
542 );
543 }
544 let total = files
545 .iter()
546 .map(|e| u64::try_from(e.size).unwrap_or(u64::MAX))
547 .fold(0_u64, u64::saturating_add);
548 if total > limits.bytes {
549 ogeom_bail!(
550 Construction,
551 "the package inflates to {total} bytes, past the {} allowed",
552 limits.bytes
553 );
554 }
555 files
556 .into_iter()
557 .map(|e| Ok((e.name.clone(), contents(bytes, e)?)))
558 .collect()
559}
560
561#[derive(Debug, Clone, Copy, PartialEq, Eq)]
563pub enum ObjectType {
564 Model,
566 SolidSupport,
568 Support,
570 Surface,
572 Other,
574}
575
576#[derive(Debug, Clone)]
578pub struct ThreeMfObject {
579 pub name: Option<String>,
581 pub object_type: ObjectType,
584 pub mesh: Triangulation,
587 pub colour: Option<[f64; 4]>,
590 pub role: ObjectRole,
593 pub colours: Option<MeshColours>,
597 pub volumes: Vec<ThreeMfVolume>,
602}
603
604#[derive(Debug, Clone, PartialEq, Eq)]
619pub enum ObjectRole {
620 Model,
622 Modifier,
625 SupportBlocker,
627 SupportEnforcer,
629 NegativeVolume,
631 Other(String),
633}
634
635impl ObjectRole {
636 fn describe(&self) -> String {
637 match self {
638 Self::Model => "a part".to_owned(),
639 Self::Modifier => "a modifier".to_owned(),
640 Self::SupportBlocker => "a support blocker".to_owned(),
641 Self::SupportEnforcer => "a support enforcer".to_owned(),
642 Self::NegativeVolume => "a negative volume".to_owned(),
643 Self::Other(role) => format!("of role {role}"),
644 }
645 }
646}
647
648#[derive(Debug, Clone, PartialEq, Eq)]
651pub struct ThreeMfVolume {
652 pub name: Option<String>,
654 pub role: ObjectRole,
656 pub triangles: std::ops::Range<usize>,
658}
659
660#[derive(Debug, Clone)]
662pub struct ThreeMfImport {
663 pub objects: Vec<ThreeMfObject>,
665 pub warnings: Vec<String>,
669}
670
671const CORE: &str = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02";
672const MATERIAL: &str = "http://schemas.microsoft.com/3dmanufacturing/material/2015/02";
673const PRODUCTION: &str = "http://schemas.microsoft.com/3dmanufacturing/production/2015/06";
674const MODEL_RELATIONSHIP: &str = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel";
675const UNDERSTOOD: [&str; 2] = [PRODUCTION, MATERIAL];
679
680pub fn read_3mf(bytes: &[u8], tol: Tolerances) -> OgeomResult<ThreeMfImport> {
715 let entries = directory(bytes)?;
716 let find = |path: &str| -> Option<&Entry> {
717 let wanted = part_key(path);
718 entries.iter().find(|e| part_key(&e.name) == wanted)
719 };
720 let mut warnings = Vec::new();
721 let start = match find("_rels/.rels") {
722 Some(rels) => {
723 let text = String::from_utf8_lossy(&contents(bytes, rels)?).into_owned();
724 start_part(&text)?
725 }
726 None => None,
727 }
728 .unwrap_or_else(|| "/3D/3dmodel.model".to_owned());
729
730 let mut parts: FastMap<String, Part> = FastMap::default();
731 let mut pending = vec![start.clone()];
732 while let Some(path) = pending.pop() {
733 let key = part_key(&path);
734 if parts.contains_key(&key) {
735 continue;
736 }
737 let Some(entry) = find(&path) else {
738 ogeom_bail!(Construction, "the package has no model part {path}");
739 };
740 let raw = contents(bytes, entry)?;
741 let Ok(text) = std::str::from_utf8(&raw) else {
742 ogeom_bail!(Construction, "the model part {path} is not UTF-8");
743 };
744 let part = parse_part(text, &path, &mut warnings)?;
745 pending.extend(part.referenced());
746 parts.insert(key, part);
747 }
748
749 let mut budget = crate::image::Budget::new();
752 for part in parts.values_mut() {
753 for def in part.textures.values_mut() {
754 let decoded = match find(&def.path) {
755 None => Err(format!("the package has no texture part {}", def.path)),
756 Some(entry) => contents(bytes, entry)
757 .and_then(|raw| budget.decode(&raw))
758 .map_err(|e| format!("the texture {} could not be decoded: {e}", def.path)),
759 };
760 match decoded {
761 Ok(image) => def.image = Some(image),
762 Err(warning) => warnings.push(warning),
763 }
764 }
765 }
766 let mut config: FastMap<u32, ConfigObject> = FastMap::default();
769 for entry in &entries {
770 let key = part_key(&entry.name);
771 if !(key.starts_with("metadata/") && key.ends_with(".config")) {
772 continue;
773 }
774 let read = contents(bytes, entry).and_then(|raw| match std::str::from_utf8(&raw) {
775 Ok(text) => parse_config(text),
776 Err(_) => ogeom_bail!(Construction, "it is not UTF-8"),
777 });
778 match read {
779 Ok(objects) => {
780 for (id, object) in objects {
781 let held = config.entry(id).or_default();
782 held.pairs.extend(object.pairs);
783 held.volumes.extend(object.volumes);
784 }
785 }
786 Err(e) => warnings.push(format!(
787 "the configuration part {} could not be read: {e}",
788 entry.name
789 )),
790 }
791 }
792
793 let context = Context {
794 parts: &parts,
795 start: part_key(&start),
796 config: &config,
797 };
798 let root = &parts[&part_key(&start)];
799 let scale = root.scale;
800 let mut objects = Vec::with_capacity(root.build.len());
801 for item in &root.build {
802 let path = item.path.clone().unwrap_or_else(|| start.clone());
803 let top = object(&parts, &path, item.object)?;
804 let outer = Tag {
805 name: None,
806 role: ObjectRole::Model,
807 };
808 let role = context.tag(&path, item.object, top, &outer).role;
809 let mut flat = Flat::default();
810 flatten(
811 &context,
812 &path,
813 item.object,
814 item.transform,
815 &mut flat,
816 0,
817 &outer,
818 )?;
819 let name = top.name.clone();
820 let label = name
821 .clone()
822 .unwrap_or_else(|| format!("object {}", item.object));
823 match top.object_type {
824 ObjectType::Model | ObjectType::SolidSupport => {}
825 ObjectType::Support => warnings.push(format!("{label} is a support structure")),
826 ObjectType::Surface => warnings.push(format!("{label} is a surface, not a solid")),
827 ObjectType::Other => warnings.push(format!("{label} is of type other, not a part")),
828 }
829 if role != ObjectRole::Model {
830 warnings.push(format!("{label} is {}, not a part", role.describe()));
831 }
832 if flat.skipped_properties {
833 warnings.push(format!(
834 "{label} carries composite materials or multiple properties; they are not read"
835 ));
836 }
837 if flat.undecoded {
838 warnings.push(format!(
839 "{label} is textured with an image that could not be decoded"
840 ));
841 }
842 let several = flat.colours.len() > 1;
843 let colour = match flat.colours.as_slice() {
844 [] => None,
845 [only] => *only,
846 _ => resolve_colour(parts.get(&part_key(&path)), top.pid, top.pindex),
847 };
848 if flat.degenerate > 0 {
849 warnings.push(format!(
850 "{label}: {} triangles repeat a vertex and were dropped",
851 flat.degenerate
852 ));
853 }
854 let Welded {
855 mesh,
856 corners,
857 volumes,
858 } = flat.into_mesh(scale, tol.confusion());
859 let coloured = corners.iter().filter(|c| c.is_some()).count();
860 let colours = match corners.into_iter().collect::<Option<Vec<_>>>() {
861 Some(corners) if !corners.is_empty() => Some(MeshColours::from_corners(
862 &mesh.triangles,
863 mesh.positions.len(),
864 corners,
865 )),
866 _ => {
867 if coloured > 0 || several {
868 warnings.push(format!(
869 "{label} carries several colours and not every triangle has one; the per-triangle colours are dropped"
870 ));
871 }
872 None
873 }
874 };
875 for volume in &volumes {
876 if volume.role != ObjectRole::Model && volume.role != role {
877 let named = volume
878 .name
879 .as_ref()
880 .map_or_else(String::new, |n| format!(" {n}"));
881 warnings.push(format!(
882 "{label} holds the volume{named}, {}; its triangles are in the mesh",
883 volume.role.describe()
884 ));
885 }
886 }
887 objects.push(ThreeMfObject {
888 name,
889 object_type: top.object_type,
890 mesh,
891 colour,
892 role,
893 colours,
894 volumes,
895 });
896 }
897 Ok(ThreeMfImport { objects, warnings })
898}
899
900fn part_key(path: &str) -> String {
904 let path = path.trim_start_matches('/');
905 let bytes = path.as_bytes();
906 let mut out = Vec::with_capacity(bytes.len());
907 let mut i = 0;
908 while i < bytes.len() {
909 if bytes[i] == b'%'
910 && let Some(hex) = path.get(i + 1..i + 3)
911 && let Ok(byte) = u8::from_str_radix(hex, 16)
912 {
913 out.push(byte);
914 i += 3;
915 continue;
916 }
917 out.push(bytes[i]);
918 i += 1;
919 }
920 String::from_utf8_lossy(&out).to_lowercase()
921}
922
923fn start_part(rels: &str) -> OgeomResult<Option<String>> {
925 let mut reader = xml::Reader::new(rels);
926 while let Some(event) = reader.next()? {
927 if let xml::Event::Start(start) = event
928 && start.local == "Relationship"
929 && start.get("Type") == Some(MODEL_RELATIONSHIP)
930 && let Some(target) = start.get("Target")
931 {
932 return Ok(Some(target.to_owned()));
933 }
934 }
935 Ok(None)
936}
937
938#[derive(Debug, Clone, Copy)]
941struct Affine {
942 axes: [Vector; 3],
943 translation: Vector,
944}
945
946impl Affine {
947 const IDENTITY: Self = Self {
948 axes: [Vector::X, Vector::Y, Vector::Z],
949 translation: Vector::ZERO,
950 };
951
952 fn parse(text: &str) -> OgeomResult<Self> {
953 let values: Vec<f64> = text
954 .split_ascii_whitespace()
955 .map(str::parse)
956 .collect::<Result<_, _>>()
957 .or_else(|_| ogeom_bail!(Construction, "the transform {text} is not twelve numbers"))?;
958 let [a, b, c, d, e, f, g, h, i, x, y, z] = values[..] else {
959 ogeom_bail!(Construction, "the transform {text} is not twelve numbers");
960 };
961 Ok(Self {
962 axes: [
963 Vector::new(a, b, c),
964 Vector::new(d, e, f),
965 Vector::new(g, h, i),
966 ],
967 translation: Vector::new(x, y, z),
968 })
969 }
970
971 fn vector(self, v: Vector) -> Vector {
972 self.axes[0] * v.x + self.axes[1] * v.y + self.axes[2] * v.z
973 }
974
975 fn point(self, p: Point) -> Point {
976 Point::ORIGIN + self.vector(p - Point::ORIGIN) + self.translation
977 }
978
979 fn after(self, inner: Self) -> Self {
981 Self {
982 axes: inner.axes.map(|a| self.vector(a)),
983 translation: self.vector(inner.translation) + self.translation,
984 }
985 }
986
987 fn mirrors(self) -> bool {
988 self.axes[0].dot(self.axes[1].cross(self.axes[2])) < 0.0
989 }
990}
991
992struct Reference {
995 path: Option<String>,
996 object: u32,
997 transform: Affine,
998}
999
1000enum Content {
1001 Mesh {
1002 positions: Vec<Point>,
1003 triangles: Vec<[u32; 3]>,
1004 properties: Vec<Option<(u32, [u32; 3])>>,
1007 },
1008 Components(Vec<Reference>),
1009}
1010
1011struct ObjectDef {
1012 name: Option<String>,
1013 object_type: ObjectType,
1014 pid: Option<u32>,
1015 pindex: Option<u32>,
1016 content: Content,
1017 metadata: Vec<(String, String)>,
1019}
1020
1021enum Group {
1023 Colours(Vec<[f64; 4]>),
1026 Texture {
1028 texture: u32,
1029 coords: Vec<(f64, f64)>,
1030 },
1031 Unread,
1033}
1034
1035struct TextureDef {
1038 path: String,
1039 wrap: [Wrap; 2],
1040 image: Option<Image>,
1041}
1042
1043struct Part {
1045 scale: f64,
1046 objects: FastMap<u32, ObjectDef>,
1047 groups: FastMap<u32, Group>,
1049 textures: FastMap<u32, TextureDef>,
1050 build: Vec<Reference>,
1051}
1052
1053impl Part {
1054 fn referenced(&self) -> Vec<String> {
1056 let components = self.objects.values().flat_map(|o| match &o.content {
1057 Content::Components(c) => c.iter().collect::<Vec<_>>(),
1058 Content::Mesh { .. } => Vec::new(),
1059 });
1060 components
1061 .chain(&self.build)
1062 .filter_map(|r| r.path.clone())
1063 .collect()
1064 }
1065}
1066
1067fn number<T: std::str::FromStr>(start: &xml::Start<'_>, name: &str) -> OgeomResult<Option<T>> {
1068 match start.get(name) {
1069 None => Ok(None),
1070 Some(text) => match text.trim().parse() {
1071 Ok(v) => Ok(Some(v)),
1072 Err(_) => ogeom_bail!(
1073 Construction,
1074 "the {} attribute {name}=\"{text}\" is not a number",
1075 start.local
1076 ),
1077 },
1078 }
1079}
1080
1081fn required<T: std::str::FromStr>(start: &xml::Start<'_>, name: &str) -> OgeomResult<T> {
1082 match number(start, name)? {
1083 Some(v) => Ok(v),
1084 None => ogeom_bail!(Construction, "a {} has no {name}", start.local),
1085 }
1086}
1087
1088fn parse_part(text: &str, path: &str, warnings: &mut Vec<String>) -> OgeomResult<Part> {
1089 let mut reader = xml::Reader::new(text);
1090 let mut part = Part {
1091 scale: 1.0,
1092 objects: FastMap::default(),
1093 groups: FastMap::default(),
1094 textures: FastMap::default(),
1095 build: Vec::new(),
1096 };
1097 let mut stack: Vec<(bool, String)> = Vec::new();
1099 let mut current: Option<(u32, ObjectDef)> = None;
1100 let mut group: Option<(u32, Group)> = None;
1101 while let Some(event) = reader.next()? {
1102 let start = match event {
1103 xml::Event::End => {
1104 if let Some((core, name)) = stack.pop() {
1105 match (core, name.as_str()) {
1106 (true, "object") => {
1107 if let Some((id, def)) = current.take() {
1108 part.objects.insert(id, def);
1109 }
1110 }
1111 (_, "basematerials" | "colorgroup" | "texture2dgroup") => {
1112 if let Some((id, g)) = group.take() {
1113 part.groups.insert(id, g);
1114 }
1115 }
1116 _ => {}
1117 }
1118 }
1119 continue;
1120 }
1121 xml::Event::Start(start) => start,
1122 };
1123 let ns = start.namespace.as_ref();
1124 let core = ns == CORE;
1125 match (ns, start.local, stack.len()) {
1126 (CORE, "model", 0) => {
1127 part.scale = unit_scale(start.get("unit").unwrap_or("millimeter"))?;
1128 for prefix in start
1129 .get("requiredextensions")
1130 .unwrap_or("")
1131 .split_whitespace()
1132 {
1133 let uri = namespace_of(text, prefix);
1134 if uri.contains("securecontent") {
1135 ogeom_bail!(
1136 Construction,
1137 "the model {path} requires the secure-content extension; its meshes are encrypted"
1138 );
1139 }
1140 if !UNDERSTOOD.contains(&uri.as_str()) {
1141 warnings.push(format!(
1142 "the model {path} requires the extension {uri}, which is not read"
1143 ));
1144 }
1145 }
1146 }
1147 (_, _, 0) => ogeom_bail!(Construction, "the part {path} is not a 3MF model"),
1148 (CORE, "object", _) => {
1149 let id = required(&start, "id")?;
1150 let object_type = match start.get("type").unwrap_or("model") {
1151 "model" => ObjectType::Model,
1152 "solidsupport" => ObjectType::SolidSupport,
1153 "support" => ObjectType::Support,
1154 "surface" => ObjectType::Surface,
1155 _ => ObjectType::Other,
1156 };
1157 current = Some((
1158 id,
1159 ObjectDef {
1160 name: start.get("name").map(str::to_owned),
1161 object_type,
1162 pid: number(&start, "pid")?,
1163 pindex: number(&start, "pindex")?,
1164 content: Content::Components(Vec::new()),
1165 metadata: Vec::new(),
1166 },
1167 ));
1168 }
1169 (CORE, "mesh", _) => {
1170 if let Some((_, def)) = current.as_mut() {
1171 def.content = Content::Mesh {
1172 positions: Vec::new(),
1173 triangles: Vec::new(),
1174 properties: Vec::new(),
1175 };
1176 }
1177 }
1178 (CORE, "vertex", _) => {
1179 if let Some((_, def)) = current.as_mut()
1180 && let Content::Mesh { positions, .. } = &mut def.content
1181 {
1182 positions.push(Point::new(
1183 required(&start, "x")?,
1184 required(&start, "y")?,
1185 required(&start, "z")?,
1186 ));
1187 }
1188 }
1189 (CORE, "triangle", _) => {
1190 if let Some((_, def)) = current.as_mut() {
1191 let pid = number(&start, "pid")?.or(def.pid);
1192 let first = number(&start, "p1")?;
1193 let corner = |name: &str, p1: u32| {
1196 first
1197 .and(start.get(name))
1198 .and_then(|t| t.trim().parse().ok())
1199 .unwrap_or(p1)
1200 };
1201 let index = first
1202 .or(def.pindex)
1203 .map(|p1| [p1, corner("p2", p1), corner("p3", p1)]);
1204 if let Content::Mesh {
1205 triangles,
1206 properties,
1207 ..
1208 } = &mut def.content
1209 {
1210 triangles.push([
1211 required(&start, "v1")?,
1212 required(&start, "v2")?,
1213 required(&start, "v3")?,
1214 ]);
1215 properties.push(pid.zip(index));
1216 }
1217 }
1218 }
1219 (CORE, "component", _) => {
1220 let reference = reference(&start)?;
1221 if let Some((_, def)) = current.as_mut()
1222 && let Content::Components(list) = &mut def.content
1223 {
1224 list.push(reference);
1225 }
1226 }
1227 (CORE, "item", _) => part.build.push(reference(&start)?),
1228 (CORE, "basematerials", _) | (MATERIAL, "colorgroup", _) => {
1229 group = Some((required(&start, "id")?, Group::Colours(Vec::new())));
1230 }
1231 (CORE, "base", _) => {
1232 if let Some((_, Group::Colours(colours))) = group.as_mut() {
1233 colours.push(parse_colour(
1234 start.get("displaycolor").unwrap_or("#FFFFFF"),
1235 )?);
1236 }
1237 }
1238 (MATERIAL, "color", _) => {
1239 if let Some((_, Group::Colours(colours))) = group.as_mut() {
1240 colours.push(parse_colour(start.get("color").unwrap_or("#FFFFFF"))?);
1241 }
1242 }
1243 (MATERIAL, "texture2d", _) => {
1244 let wrap = |name: &str| match start.get(name).unwrap_or("wrap") {
1245 "mirror" => Wrap::Mirror,
1246 "clamp" | "none" => Wrap::Clamp,
1247 _ => Wrap::Repeat,
1248 };
1249 if let Some(id) = number::<u32>(&start, "id")? {
1250 part.textures.insert(
1251 id,
1252 TextureDef {
1253 path: start.get("path").unwrap_or("").to_owned(),
1254 wrap: [wrap("tilestyleu"), wrap("tilestylev")],
1255 image: None,
1256 },
1257 );
1258 }
1259 }
1260 (MATERIAL, "texture2dgroup", _) => {
1263 if let Some(id) = number::<u32>(&start, "id")? {
1264 let texture = start.get("texid").and_then(|t| t.trim().parse().ok());
1265 group = Some((
1266 id,
1267 texture.map_or(Group::Unread, |texture| Group::Texture {
1268 texture,
1269 coords: Vec::new(),
1270 }),
1271 ));
1272 }
1273 }
1274 (MATERIAL, "tex2coord", _) => {
1275 if let Some((_, g @ Group::Texture { .. })) = group.as_mut() {
1276 let at = |name: &str| {
1277 start
1278 .get(name)
1279 .and_then(|t| t.trim().parse::<f64>().ok())
1280 .filter(|x| x.is_finite())
1281 };
1282 match (at("u"), at("v"), &mut *g) {
1283 (Some(u), Some(v), Group::Texture { coords, .. }) => coords.push((u, v)),
1284 _ => *g = Group::Unread,
1285 }
1286 }
1287 }
1288 (MATERIAL, _, _) => {
1289 if let Some(id) = number::<u32>(&start, "id")? {
1292 part.groups.insert(id, Group::Unread);
1293 }
1294 if !start.empty {
1295 reader.skip()?;
1296 continue;
1297 }
1298 }
1299 (CORE, "metadatagroup", _)
1300 if current.is_some() && stack.last().is_some_and(|(c, n)| *c && n == "object") => {}
1301 (CORE, "metadata", _)
1302 if current.is_some()
1303 && stack
1304 .last()
1305 .is_some_and(|(c, n)| *c && n == "metadatagroup") =>
1306 {
1307 let name = start.get("name").unwrap_or("").to_owned();
1308 let value = reader.text().into_owned();
1309 if let Some((_, def)) = current.as_mut() {
1310 def.metadata.push((name, value));
1311 }
1312 if !start.empty {
1313 reader.skip()?;
1314 continue;
1315 }
1316 }
1317 (CORE, "metadata" | "metadatagroup", _) if !start.empty => {
1318 reader.skip()?;
1319 continue;
1320 }
1321 _ => {}
1322 }
1323 stack.push((core, start.local.to_owned()));
1325 }
1326 Ok(part)
1327}
1328
1329fn reference(start: &xml::Start<'_>) -> OgeomResult<Reference> {
1330 Ok(Reference {
1331 path: start.get_in(PRODUCTION, "path").map(str::to_owned),
1332 object: required(start, "objectid")?,
1333 transform: match start.get("transform") {
1334 Some(text) => Affine::parse(text)?,
1335 None => Affine::IDENTITY,
1336 },
1337 })
1338}
1339
1340fn namespace_of(text: &str, prefix: &str) -> String {
1343 let declaration = format!("xmlns:{prefix}=");
1344 text.find(&declaration)
1345 .and_then(|i| {
1346 let rest = &text[i + declaration.len()..];
1347 let quote = rest.chars().next()?;
1348 let rest = &rest[1..];
1349 rest.find(quote).map(|end| rest[..end].to_owned())
1350 })
1351 .unwrap_or_else(|| prefix.to_owned())
1352}
1353
1354fn unit_scale(unit: &str) -> OgeomResult<f64> {
1355 Ok(match unit {
1356 "micron" => 1e-3,
1357 "millimeter" => 1.0,
1358 "centimeter" => 10.0,
1359 "inch" => 25.4,
1360 "foot" => 304.8,
1361 "meter" => 1000.0,
1362 _ => ogeom_bail!(Construction, "the model unit {unit} is not one 3MF defines"),
1363 })
1364}
1365
1366fn parse_colour(text: &str) -> OgeomResult<[f64; 4]> {
1368 let hex = text.trim().trim_start_matches('#');
1369 let channel = |i: usize| -> Option<f64> {
1370 let byte = u8::from_str_radix(hex.get(i..i + 2)?, 16).ok()?;
1371 Some(f64::from(byte) / 255.0)
1372 };
1373 match (hex.len(), channel(0), channel(2), channel(4)) {
1374 (6, Some(r), Some(g), Some(b)) => Ok([r, g, b, 1.0]),
1375 (8, Some(r), Some(g), Some(b)) => Ok([r, g, b, channel(6).unwrap_or(1.0)]),
1376 _ => ogeom_bail!(
1377 Construction,
1378 "the colour {text} is not #RRGGBB or #RRGGBBAA"
1379 ),
1380 }
1381}
1382
1383fn object<'p>(parts: &'p FastMap<String, Part>, path: &str, id: u32) -> OgeomResult<&'p ObjectDef> {
1384 match parts.get(&part_key(path)).and_then(|p| p.objects.get(&id)) {
1385 Some(def) => Ok(def),
1386 None => ogeom_bail!(Construction, "the model {path} has no object {id}"),
1387 }
1388}
1389
1390fn resolve_colour(part: Option<&Part>, pid: Option<u32>, index: Option<u32>) -> Option<[f64; 4]> {
1393 match part?.groups.get(&pid?)? {
1394 Group::Colours(colours) => colours.get(index? as usize).copied(),
1395 _ => None,
1396 }
1397}
1398
1399#[derive(Default)]
1401struct Flat {
1402 positions: Vec<Point>,
1403 triangles: Vec<[u32; 3]>,
1404 colours: Vec<Option<[f64; 4]>>,
1407 corners: Vec<Option<[[f64; 4]; 3]>>,
1409 volume_of: Vec<usize>,
1411 volumes: Vec<(Option<String>, ObjectRole)>,
1412 skipped_properties: bool,
1413 undecoded: bool,
1415 degenerate: usize,
1416}
1417
1418const DEPTH: usize = 64;
1420
1421#[derive(Default)]
1424struct ConfigObject {
1425 pairs: Vec<(String, String)>,
1426 volumes: Vec<ConfigVolume>,
1427}
1428
1429#[derive(Default)]
1432struct ConfigVolume {
1433 triangles: Option<(usize, usize)>,
1434 object: Option<u32>,
1435 pairs: Vec<(String, String)>,
1436}
1437
1438#[derive(Clone)]
1440struct Tag {
1441 name: Option<String>,
1442 role: ObjectRole,
1443}
1444
1445impl Tag {
1446 fn with(&self, pairs: &[(String, String)]) -> Self {
1448 Self {
1449 name: value_of(pairs, "name")
1450 .map(str::to_owned)
1451 .or_else(|| self.name.clone()),
1452 role: role_of(pairs).unwrap_or_else(|| self.role.clone()),
1453 }
1454 }
1455}
1456
1457fn bare_key(key: &str) -> String {
1459 key.rsplit(':').next().unwrap_or(key).to_ascii_lowercase()
1460}
1461
1462fn value_of<'a>(pairs: &'a [(String, String)], key: &str) -> Option<&'a str> {
1463 pairs
1464 .iter()
1465 .find(|(k, _)| bare_key(k) == key)
1466 .map(|(_, v)| v.as_str())
1467}
1468
1469fn role_of(pairs: &[(String, String)]) -> Option<ObjectRole> {
1472 let squashed = |v: &str| {
1473 v.chars()
1474 .filter(|c| !matches!(c, '_' | '-' | ' '))
1475 .collect::<String>()
1476 .to_ascii_lowercase()
1477 };
1478 for key in ["volume_type", "subtype"] {
1479 if let Some(value) = value_of(pairs, key) {
1480 return Some(match squashed(value).as_str() {
1481 "modelpart" | "normalpart" | "model" | "part" => ObjectRole::Model,
1482 "parametermodifier" | "modifierpart" | "modifier" => ObjectRole::Modifier,
1483 "negativevolume" | "negativepart" => ObjectRole::NegativeVolume,
1484 "supportblocker" => ObjectRole::SupportBlocker,
1485 "supportenforcer" => ObjectRole::SupportEnforcer,
1486 _ => ObjectRole::Other(value.to_owned()),
1487 });
1488 }
1489 }
1490 let set = |key: &str| {
1491 value_of(pairs, key)
1492 .is_some_and(|v| matches!(v.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
1493 };
1494 if set("modifier") || set("infill_mesh") || set("cutting_mesh") {
1495 return Some(ObjectRole::Modifier);
1496 }
1497 if set("anti_overhang_mesh") {
1498 return Some(ObjectRole::SupportBlocker);
1499 }
1500 if set("support_mesh") {
1501 return Some(ObjectRole::SupportEnforcer);
1502 }
1503 None
1504}
1505
1506fn parse_config(text: &str) -> OgeomResult<FastMap<u32, ConfigObject>> {
1510 let mut reader = xml::Reader::new(text);
1511 let mut objects: FastMap<u32, ConfigObject> = FastMap::default();
1512 let mut depth = 0_usize;
1513 let mut object: Option<(u32, usize, ConfigObject)> = None;
1515 let mut volume: Option<(usize, ConfigVolume)> = None;
1516 while let Some(event) = reader.next()? {
1517 let start = match event {
1518 xml::Event::End => {
1519 if volume.as_ref().is_some_and(|(d, _)| *d == depth)
1520 && let (Some((_, v)), Some((_, _, o))) = (volume.take(), object.as_mut())
1521 {
1522 o.volumes.push(v);
1523 }
1524 if object.as_ref().is_some_and(|(_, d, _)| *d == depth)
1525 && let Some((id, _, o)) = object.take()
1526 {
1527 let held = objects.entry(id).or_default();
1528 held.pairs.extend(o.pairs);
1529 held.volumes.extend(o.volumes);
1530 }
1531 depth = depth.saturating_sub(1);
1532 continue;
1533 }
1534 xml::Event::Start(start) => start,
1535 };
1536 depth += 1;
1537 let pairs = |start: &xml::Start<'_>| -> Vec<(String, String)> {
1538 start
1539 .attributes
1540 .iter()
1541 .map(|(_, k, v)| ((*k).to_owned(), v.clone().into_owned()))
1542 .collect()
1543 };
1544 match start.local {
1545 "object" if object.is_none() && depth == 2 => {
1546 if let Some(id) = number::<u32>(&start, "id")? {
1547 object = Some((id, depth, ConfigObject::default()));
1548 }
1549 }
1550 "volume" | "part" if object.is_some() && volume.is_none() => {
1551 let first = number::<usize>(&start, "firstid")?;
1552 let last = number::<usize>(&start, "lastid")?;
1553 volume = Some((
1554 depth,
1555 ConfigVolume {
1556 triangles: first.zip(last),
1557 object: number(&start, "id")?,
1558 pairs: pairs(&start),
1559 },
1560 ));
1561 }
1562 "metadata" => {
1563 let key = start.get("key").or_else(|| start.get("name"));
1564 let value = match start.get("value") {
1565 Some(v) => v.to_owned(),
1566 None => reader.text().into_owned(),
1567 };
1568 if let Some(key) = key {
1569 let pair = (key.to_owned(), value);
1570 match (volume.as_mut(), object.as_mut()) {
1571 (Some((_, v)), _) => v.pairs.push(pair),
1572 (None, Some((_, _, o))) => o.pairs.push(pair),
1573 (None, None) => {}
1574 }
1575 }
1576 }
1577 _ => {}
1578 }
1579 }
1580 Ok(objects)
1581}
1582
1583struct Context<'p> {
1586 parts: &'p FastMap<String, Part>,
1587 start: String,
1588 config: &'p FastMap<u32, ConfigObject>,
1589}
1590
1591impl Context<'_> {
1592 fn config(&self, path: &str, id: u32) -> Option<&ConfigObject> {
1595 (part_key(path) == self.start)
1596 .then(|| self.config.get(&id))
1597 .flatten()
1598 }
1599
1600 fn tag(&self, path: &str, id: u32, def: &ObjectDef, inherited: &Tag) -> Tag {
1602 let mut tag = inherited.clone();
1603 if let Some(config) = self.config(path, id) {
1604 tag.role = role_of(&config.pairs).unwrap_or(tag.role);
1605 }
1606 if let Some(role) = role_of(&def.metadata) {
1607 tag.role = role;
1608 }
1609 tag
1610 }
1611}
1612
1613fn corner_colours(
1617 part: Option<&Part>,
1618 pid: u32,
1619 index: [u32; 3],
1620) -> Result<Option<[[f64; 4]; 3]>, bool> {
1621 let Some(part) = part else {
1622 return Ok(None);
1623 };
1624 match part.groups.get(&pid) {
1625 None => Ok(None),
1626 Some(Group::Unread) => Err(false),
1627 Some(Group::Colours(colours)) => Ok(index
1628 .iter()
1629 .map(|&i| colours.get(i as usize).copied())
1630 .collect::<Option<Vec<_>>>()
1631 .map(|c| [c[0], c[1], c[2]])),
1632 Some(Group::Texture { texture, coords }) => {
1633 let Some(def) = part.textures.get(texture) else {
1634 return Ok(None);
1635 };
1636 let Some(image) = def.image.as_ref() else {
1637 return Err(true);
1638 };
1639 Ok(index
1640 .iter()
1641 .map(|&i| {
1642 coords
1643 .get(i as usize)
1644 .map(|&(u, v)| image.sample(u, v, def.wrap))
1645 })
1646 .collect::<Option<Vec<_>>>()
1647 .map(|c| [c[0], c[1], c[2]]))
1648 }
1649 }
1650}
1651
1652fn flatten(
1653 context: &Context<'_>,
1654 path: &str,
1655 id: u32,
1656 placed: Affine,
1657 flat: &mut Flat,
1658 depth: usize,
1659 inherited: &Tag,
1660) -> OgeomResult<()> {
1661 if depth > DEPTH {
1662 ogeom_bail!(Construction, "the object {id} in {path} contains itself");
1663 }
1664 let parts = context.parts;
1665 let def = object(parts, path, id)?;
1666 let part = parts.get(&part_key(path));
1667 let tag = context.tag(path, id, def, inherited);
1668 let config = context.config(path, id);
1669 match &def.content {
1670 Content::Components(components) => {
1671 for c in components {
1672 let inner = c.path.as_deref().unwrap_or(path);
1673 let child = config
1674 .and_then(|cfg| cfg.volumes.iter().find(|v| v.object == Some(c.object)))
1675 .map_or_else(|| tag.clone(), |v| tag.with(&v.pairs));
1676 flatten(
1677 context,
1678 inner,
1679 c.object,
1680 placed.after(c.transform),
1681 flat,
1682 depth + 1,
1683 &child,
1684 )?;
1685 }
1686 }
1687 Content::Mesh {
1688 positions,
1689 triangles,
1690 properties,
1691 } => {
1692 let base = u32::try_from(flat.positions.len()).unwrap_or(u32::MAX);
1693 let count = positions.len();
1694 flat.positions
1695 .extend(positions.iter().map(|p| placed.point(*p)));
1696 let mirrored = placed.mirrors();
1697 let ranges: Vec<(usize, usize, Tag)> = config
1698 .map(|cfg| {
1699 cfg.volumes
1700 .iter()
1701 .filter_map(|v| v.triangles.map(|(a, b)| (a, b, tag.with(&v.pairs))))
1702 .collect()
1703 })
1704 .unwrap_or_default();
1705 let mut open: Option<Option<usize>> = None;
1707 for (k, (triangle, property)) in triangles.iter().zip(properties).enumerate() {
1708 if triangle.iter().any(|&v| v as usize >= count) {
1709 ogeom_bail!(
1710 Construction,
1711 "a triangle of object {id} in {path} names a vertex past the {count} it has"
1712 );
1713 }
1714 let [a, b, c] = *triangle;
1715 if a == b || b == c || c == a {
1716 flat.degenerate += 1;
1717 continue;
1718 }
1719 let triangle = if mirrored { [a, c, b] } else { [a, b, c] };
1720 flat.triangles.push(triangle.map(|v| base + v));
1721 let range = ranges.iter().position(|(a, b, _)| (*a..=*b).contains(&k));
1722 if open != Some(range) {
1723 let volume = range.map_or(&tag, |r| &ranges[r].2);
1724 flat.volumes.push((
1725 volume.name.clone().or_else(|| def.name.clone()),
1726 volume.role.clone(),
1727 ));
1728 open = Some(range);
1729 }
1730 flat.volume_of.push(flat.volumes.len() - 1);
1731 let colour = match property {
1732 Some((pid, index)) => match part.and_then(|p| p.groups.get(pid)) {
1733 Some(Group::Colours(group)) => group.get(index[0] as usize).copied(),
1734 Some(Group::Unread) => {
1735 flat.skipped_properties = true;
1736 None
1737 }
1738 _ => None,
1739 },
1740 None => None,
1741 };
1742 if !flat.colours.contains(&colour) {
1743 flat.colours.push(colour);
1744 }
1745 let corners = match property {
1746 Some((pid, index)) => match corner_colours(part, *pid, *index) {
1747 Ok(c) => c,
1748 Err(undecoded) => {
1749 flat.undecoded |= undecoded;
1750 None
1751 }
1752 },
1753 None => None,
1754 };
1755 flat.corners
1757 .push(corners.map(|[p, q, r]| if mirrored { [p, r, q] } else { [p, q, r] }));
1758 }
1759 }
1760 }
1761 Ok(())
1762}
1763
1764struct Welded {
1767 mesh: Triangulation,
1768 corners: Vec<Option<[[f64; 4]; 3]>>,
1769 volumes: Vec<ThreeMfVolume>,
1770}
1771
1772impl Flat {
1773 fn into_mesh(self, scale: f64, weld: f64) -> Welded {
1776 let positions: Vec<Point> = self
1777 .positions
1778 .iter()
1779 .map(|p| Point::ORIGIN + (*p - Point::ORIGIN) * scale)
1780 .collect();
1781 #[allow(clippy::cast_possible_truncation, reason = "saturating")]
1786 let cell = |p: Point| {
1787 (
1788 (p.x / weld).floor() as i64,
1789 (p.y / weld).floor() as i64,
1790 (p.z / weld).floor() as i64,
1791 )
1792 };
1793 let mut grid: FastMap<(i64, i64, i64), Vec<u32>> = FastMap::default();
1794 let mut kept: Vec<Point> = Vec::with_capacity(positions.len());
1795 let mut remap = Vec::with_capacity(positions.len());
1796 for p in &positions {
1797 let (x, y, z) = cell(*p);
1798 let mut found = None;
1799 'search: for dx in -1..=1 {
1800 for dy in -1..=1 {
1801 for dz in -1..=1 {
1802 if let Some(list) = grid.get(&(
1803 x.saturating_add(dx),
1804 y.saturating_add(dy),
1805 z.saturating_add(dz),
1806 )) && let Some(&k) = list
1807 .iter()
1808 .find(|&&k| kept[k as usize].distance(*p) <= weld)
1809 {
1810 found = Some(k);
1811 break 'search;
1812 }
1813 }
1814 }
1815 }
1816 let index = found.unwrap_or_else(|| {
1817 let k = u32::try_from(kept.len()).unwrap_or(u32::MAX);
1818 kept.push(*p);
1819 grid.entry((x, y, z)).or_default().push(k);
1820 k
1821 });
1822 remap.push(index);
1823 }
1824 let mut triangles: Vec<[u32; 3]> = Vec::with_capacity(self.triangles.len());
1825 let mut corners = Vec::with_capacity(self.triangles.len());
1826 let mut volume_of = Vec::with_capacity(self.triangles.len());
1827 for (k, t) in self.triangles.iter().enumerate() {
1828 let [a, b, c] = t.map(|v| remap[v as usize]);
1829 if a != b && b != c && c != a {
1830 triangles.push([a, b, c]);
1831 corners.push(self.corners[k]);
1832 volume_of.push(self.volume_of[k]);
1833 }
1834 }
1835 let mut cursor = 0;
1836 let volumes = self
1837 .volumes
1838 .into_iter()
1839 .enumerate()
1840 .map(|(v, (name, role))| {
1841 let start = cursor;
1842 while volume_of.get(cursor) == Some(&v) {
1843 cursor += 1;
1844 }
1845 ThreeMfVolume {
1846 name,
1847 role,
1848 triangles: start..cursor,
1849 }
1850 })
1851 .collect();
1852 let mut normals = vec![Vector::ZERO; kept.len()];
1853 for triangle in &triangles {
1854 let [a, b, c] = triangle.map(|i| kept[i as usize]);
1855 let face = (b - a).cross(c - a);
1856 for &i in triangle {
1857 normals[i as usize] += face;
1858 }
1859 }
1860 let normals = normals
1861 .into_iter()
1862 .map(|n| {
1863 let m = n.magnitude();
1864 if m > 0.0 { n / m } else { Vector::Z }
1865 })
1866 .collect();
1867 Welded {
1868 mesh: Triangulation {
1869 parameters: vec![(0.0, 0.0); kept.len()],
1870 positions: kept,
1871 normals,
1872 triangles,
1873 deflection_met: true,
1874 },
1875 corners,
1876 volumes,
1877 }
1878 }
1879}
1880
1881#[cfg(test)]
1882mod tests {
1883 #![allow(
1884 clippy::unwrap_used,
1885 clippy::cast_possible_truncation,
1886 reason = "test code"
1887 )]
1888 use super::*;
1889
1890 const T: Tolerances = Tolerances::millimetres();
1891
1892 const TYPES: &str = r#"<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"/>"#;
1893 const RELS: &str = r#"<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Target="/3D/3dmodel.model" Id="r0" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/></Relationships>"#;
1894
1895 fn cube(x: f64, property: impl Fn(usize) -> String) -> String {
1898 let mut out = String::from("<mesh><vertices>");
1899 for k in 0..8 {
1900 let (dx, dy, dz) = (k & 1, (k >> 1) & 1, (k >> 2) & 1);
1901 out.push_str(&format!(
1902 r#"<vertex x="{}" y="{dy}" z="{dz}"/>"#,
1903 x + f64::from(dx)
1904 ));
1905 }
1906 out.push_str("</vertices><triangles>");
1907 let faces = [
1908 [0, 2, 3, 1],
1909 [4, 5, 7, 6],
1910 [0, 1, 5, 4],
1911 [2, 6, 7, 3],
1912 [0, 4, 6, 2],
1913 [1, 3, 7, 5],
1914 ];
1915 for (f, [a, b, c, d]) in faces.iter().enumerate() {
1916 for (k, [p, q, r]) in [[a, b, c], [a, c, d]].iter().enumerate() {
1917 out.push_str(&format!(
1918 r#"<triangle v1="{p}" v2="{q}" v3="{r}" {}/>"#,
1919 property(2 * f + k)
1920 ));
1921 }
1922 }
1923 out.push_str("</triangles></mesh>");
1924 out
1925 }
1926
1927 fn model(resources: &str, build: &str) -> String {
1928 format!(
1929 r#"<?xml version="1.0"?><model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02" xmlns:m="http://schemas.microsoft.com/3dmanufacturing/material/2015/02" xmlns:p="http://schemas.microsoft.com/3dmanufacturing/production/2015/06"><resources>{resources}</resources><build>{build}</build></model>"#
1930 )
1931 }
1932
1933 fn package(parts: &[(&str, &[u8])]) -> Vec<u8> {
1934 let mut all: Vec<(&str, &[u8])> = vec![
1935 ("[Content_Types].xml", TYPES.as_bytes()),
1936 ("_rels/.rels", RELS.as_bytes()),
1937 ];
1938 all.extend_from_slice(parts);
1939 archive(&all)
1940 }
1941
1942 #[test]
1945 fn a_modifier_volume_reports_its_role_by_triangle_range() {
1946 let mut mesh = cube(0.0, |_| String::new());
1947 let second = cube(5.0, |_| String::new());
1948 let (v2, t2) = second
1950 .trim_start_matches("<mesh><vertices>")
1951 .trim_end_matches("</triangles></mesh>")
1952 .split_once("</vertices><triangles>")
1953 .unwrap();
1954 let t2 = (0..8).rev().fold(t2.to_owned(), |t, k| {
1955 t.replace(&format!("\"{k}\""), &format!("\"#{}\"", k + 8))
1956 });
1957 let t2 = t2.replace("\"#", "\"");
1958 mesh = mesh.replace("</vertices>", &format!("{v2}</vertices>"));
1959 mesh = mesh.replace("</triangles>", &format!("{t2}</triangles>"));
1960 let model = model(
1961 &format!(r#"<object id="1" type="model" name="bracket">{mesh}</object>"#),
1962 r#"<item objectid="1"/>"#,
1963 );
1964 let config = r#"<?xml version="1.0"?><config><object id="1"><metadata type="object" key="name" value="bracket"/>
1965<volume firstid="0" lastid="11"><metadata type="volume" key="name" value="body"/><metadata type="volume" key="volume_type" value="ModelPart"/></volume>
1966<volume firstid="12" lastid="23"><metadata type="volume" key="name" value="denser"/><metadata type="volume" key="volume_type" value="ParameterModifier"/></volume>
1967</object></config>"#;
1968 let bytes = package(&[
1969 ("3D/3dmodel.model", model.as_bytes()),
1970 ("Metadata/model.config", config.as_bytes()),
1971 ]);
1972 let import = read_3mf(&bytes, T).unwrap();
1973 let object = &import.objects[0];
1974 assert_eq!(object.role, ObjectRole::Model);
1975 assert_eq!(
1976 object.volumes,
1977 vec![
1978 ThreeMfVolume {
1979 name: Some("body".into()),
1980 role: ObjectRole::Model,
1981 triangles: 0..12,
1982 },
1983 ThreeMfVolume {
1984 name: Some("denser".into()),
1985 role: ObjectRole::Modifier,
1986 triangles: 12..24,
1987 },
1988 ]
1989 );
1990 assert!(
1991 object.mesh.triangles[12..]
1992 .iter()
1993 .flatten()
1994 .all(|&v| object.mesh.positions[v as usize].x >= 5.0)
1995 );
1996 assert!(
1997 import.warnings.iter().any(|w| w.contains("a modifier")),
1998 "{:?}",
1999 import.warnings
2000 );
2001 let plain = package(&[("3D/3dmodel.model", model.as_bytes())]);
2003 let import = read_3mf(&plain, T).unwrap();
2004 assert_eq!(import.objects[0].volumes.len(), 1);
2005 assert_eq!(import.objects[0].volumes[0].triangles, 0..24);
2006 assert!(import.warnings.is_empty(), "{:?}", import.warnings);
2007 }
2008
2009 #[test]
2014 fn component_and_object_roles_read_by_name() {
2015 let parts = model(
2016 &format!(
2017 r#"<object id="2" type="model">{}</object><object id="3" type="model">{}</object>"#,
2018 cube(0.0, |_| String::new()),
2019 cube(0.25, |_| String::new()),
2020 ),
2021 "",
2022 );
2023 let root = model(
2024 &format!(
2025 r#"<object id="1" type="model" name="assembly"><components><component p:path="/3D/Objects/parts.model" objectid="2"/><component p:path="/3D/Objects/parts.model" objectid="3"/></components></object>
2026<object id="4" type="model" name="tower"><metadatagroup><metadata name="tool:support_mesh" preserve="1">True</metadata></metadatagroup>{}</object>"#,
2027 cube(9.0, |_| String::new())
2028 ),
2029 r#"<item objectid="1"/><item objectid="4"/>"#,
2030 );
2031 let config = r#"<?xml version="1.0"?><config><object id="1"><metadata key="name" value="assembly"/>
2032<part id="2" subtype="normal_part"><metadata key="name" value="housing"/></part>
2033<part id="3" subtype="negative_part"><metadata key="name" value="bore"/></part></object>
2034<plate><metadata key="plater_id" value="1"/></plate></config>"#;
2035 let bytes = package(&[
2036 ("3D/3dmodel.model", root.as_bytes()),
2037 ("3D/Objects/parts.model", parts.as_bytes()),
2038 ("Metadata/model_settings.config", config.as_bytes()),
2039 ]);
2040 let import = read_3mf(&bytes, T).unwrap();
2041 let (assembly, tower) = (&import.objects[0], &import.objects[1]);
2042 let roles: Vec<_> = assembly
2043 .volumes
2044 .iter()
2045 .map(|v| (v.name.as_deref(), v.role.clone(), v.triangles.clone()))
2046 .collect();
2047 assert_eq!(
2048 roles,
2049 vec![
2050 (Some("housing"), ObjectRole::Model, 0..12),
2051 (Some("bore"), ObjectRole::NegativeVolume, 12..24),
2052 ]
2053 );
2054 assert_eq!(tower.role, ObjectRole::SupportEnforcer);
2055 assert_eq!(tower.volumes[0].role, ObjectRole::SupportEnforcer);
2056 assert!(
2057 import
2058 .warnings
2059 .iter()
2060 .any(|w| w.contains("tower is a support enforcer"))
2061 );
2062 assert_eq!(
2064 role_of(&[("subtype".into(), "fuzzy_skin_part".into())]),
2065 Some(ObjectRole::Other("fuzzy_skin_part".into()))
2066 );
2067 assert_eq!(
2068 role_of(&[("modifier".into(), "1".into())]),
2069 Some(ObjectRole::Modifier)
2070 );
2071 assert_eq!(role_of(&[("name".into(), "x".into())]), None);
2072 }
2073
2074 #[test]
2077 fn colour_groups_and_textures_read_as_mesh_colours() {
2078 let texels = [[255, 0, 0, 255], [0, 0, 255, 255]];
2079 let png = crate::image::encode_png(2, 1, &texels);
2080 let resources = format!(
2081 r##"<m:colorgroup id="5"><m:color color="#FF0000"/><m:color color="#00FF0080"/></m:colorgroup>
2082<m:texture2d id="6" path="/3D/Textures/two.png" contenttype="image/png" tilestyleu="clamp"/>
2083<m:texture2dgroup id="7" texid="6"><m:tex2coord u="0.25" v="0.5"/><m:tex2coord u="0.75" v="0.5"/></m:texture2dgroup>
2084<object id="1" type="model" name="painted">{}</object><object id="2" type="model" name="textured">{}</object>"##,
2085 cube(0.0, |t| format!(r#"pid="5" p1="{}" p2="1" p3="0""#, t % 2)),
2086 cube(3.0, |t| format!(r#"pid="7" p1="{}""#, t / 6)),
2087 );
2088 let model = model(&resources, r#"<item objectid="1"/><item objectid="2"/>"#);
2089 let bytes = package(&[
2090 ("3D/3dmodel.model", model.as_bytes()),
2091 ("3D/Textures/two.png", &png),
2092 ]);
2093 let import = read_3mf(&bytes, T).unwrap();
2094 assert!(import.warnings.is_empty(), "{:?}", import.warnings);
2095 let red = [1.0, 0.0, 0.0, 1.0];
2096 let green = [0.0, 1.0, 0.0, 128.0 / 255.0];
2097 let painted = &import.objects[0];
2098 let Some(MeshColours::PerCorner(corners)) = &painted.colours else {
2099 panic!("{:?}", painted.colours);
2100 };
2101 assert_eq!(corners[0], [red, green, red]);
2102 assert_eq!(corners[1], [green, green, red]);
2103 let textured = &import.objects[1];
2104 let Some(MeshColours::PerTriangle(faces)) = &textured.colours else {
2105 panic!("{:?}", textured.colours);
2106 };
2107 assert!(faces[..6].iter().all(|c| *c == red));
2108 assert!(faces[6..].iter().all(|c| *c == [0.0, 0.0, 1.0, 1.0]));
2109 assert_eq!(textured.colour, None);
2110
2111 let bytes = package(&[("3D/3dmodel.model", model.as_bytes())]);
2114 let import = read_3mf(&bytes, T).unwrap();
2115 assert!(import.objects[1].colours.is_none());
2116 assert!(
2117 import
2118 .warnings
2119 .iter()
2120 .any(|w| w.contains("no texture part"))
2121 );
2122 }
2123
2124 fn overlapping(data: &[u8], copies: u16) -> Vec<u8> {
2127 let one = archive(&[("p0000", data)]);
2128 let end = one.len() - 22;
2129 let at = usize::try_from(u32_at(&one, end + 16).unwrap()).unwrap();
2130 let record = &one[at..end];
2131 let mut out = one[..at].to_vec();
2132 for k in 0..copies {
2133 let mut copy = record.to_vec();
2134 copy[46..51].copy_from_slice(format!("p{k:04}").as_bytes());
2135 out.extend_from_slice(©);
2136 }
2137 let size = u32::try_from(out.len() - at).unwrap();
2138 out.extend_from_slice(&one[end..end + 8]);
2139 out.extend_from_slice(&copies.to_le_bytes());
2140 out.extend_from_slice(&copies.to_le_bytes());
2141 out.extend_from_slice(&size.to_le_bytes());
2142 out.extend_from_slice(&u32::try_from(at).unwrap().to_le_bytes());
2143 out.extend_from_slice(&0_u16.to_le_bytes());
2144 out
2145 }
2146
2147 #[test]
2152 fn entries_overlapping_one_another_are_refused() {
2153 let data = vec![b'x'; 1 << 16];
2154 let bytes = overlapping(&data, 1000);
2155 assert!(bytes.len() < 120_000, "{}", bytes.len());
2156 let message = read_package(&bytes).unwrap_err().to_string();
2157 assert!(message.contains("overlap"), "{message}");
2158 assert!(read_3mf(&bytes, T).is_err());
2159 let parts = read_package(&overlapping(&data, 1)).unwrap();
2161 assert_eq!(parts.len(), 1);
2162 assert_eq!(parts[0].1, data);
2163 }
2164
2165 #[test]
2170 fn a_package_read_is_held_to_its_limits() {
2171 let bytes = archive(&[
2172 ("a", &[1; 10][..]),
2173 ("dir/", &[][..]),
2174 ("dir/b", &[2; 20][..]),
2175 ("c", &[3; 30][..]),
2176 ]);
2177 let exact = PackageLimits {
2178 entries: 3,
2179 bytes: 60,
2180 };
2181 assert_eq!(read_package_within(&bytes, exact).unwrap().len(), 3);
2182 assert_eq!(read_package(&bytes).unwrap().len(), 3);
2183 let fewer = PackageLimits {
2184 entries: 2,
2185 ..exact
2186 };
2187 let message = read_package_within(&bytes, fewer).unwrap_err().to_string();
2188 assert!(message.contains("3 entries"), "{message}");
2189 let smaller = PackageLimits { bytes: 59, ..exact };
2190 let message = read_package_within(&bytes, smaller)
2191 .unwrap_err()
2192 .to_string();
2193 assert!(message.contains("60 bytes"), "{message}");
2194 }
2195
2196 #[derive(Default)]
2198 struct BitWriter {
2199 out: Vec<u8>,
2200 held: u64,
2201 count: u32,
2202 }
2203
2204 impl BitWriter {
2205 fn put(&mut self, value: u64, bits: u32) {
2206 self.held |= value << self.count;
2207 self.count += bits;
2208 while self.count >= 8 {
2209 self.out.push((self.held & 0xFF) as u8);
2210 self.held >>= 8;
2211 self.count -= 8;
2212 }
2213 }
2214
2215 fn code(&mut self, code: u64, bits: u32) {
2217 self.put(code.reverse_bits() >> (64 - bits), bits);
2218 }
2219
2220 fn finish(mut self) -> Vec<u8> {
2221 if self.count > 0 {
2222 self.out.push((self.held & 0xFF) as u8);
2223 }
2224 self.out
2225 }
2226 }
2227
2228 fn zeros_bomb(repeats: usize) -> Vec<u8> {
2232 let mut w = BitWriter::default();
2233 w.put(1, 1); w.put(2, 2); w.put(29, 5); w.put(0, 5); w.put(14, 4); for slot in 0..18 {
2241 let length = match slot {
2242 2 => 1,
2243 15 | 17 => 2,
2244 _ => 0,
2245 };
2246 w.put(length, 3);
2247 }
2248 let zeros = |w: &mut BitWriter, mut run: u64| {
2249 while run > 0 {
2250 let take = run.min(138);
2251 w.code(0, 1);
2252 w.put(take - 11, 7);
2253 run -= take;
2254 }
2255 };
2256 w.code(0b11, 2); zeros(&mut w, 255);
2260 w.code(0b11, 2); zeros(&mut w, 28);
2262 w.code(0b10, 2); w.code(0b10, 2); w.code(0b10, 2);
2266 for _ in 0..repeats {
2267 w.code(0, 1);
2268 w.code(0, 1);
2269 }
2270 w.code(0b11, 2);
2271 w.finish()
2272 }
2273
2274 fn deflated_archive(entries: &[(&str, &[u8], u32)]) -> Vec<u8> {
2278 let mut out: Vec<u8> = Vec::new();
2279 let mut directory: Vec<u8> = Vec::new();
2280 let field = |v: &mut Vec<u8>, bytes: &[u8]| v.extend_from_slice(bytes);
2281 for (name, stream, size) in entries {
2282 let offset = u32::try_from(out.len()).unwrap();
2283 let compressed = u32::try_from(stream.len()).unwrap();
2284 let name_len = u16::try_from(name.len()).unwrap();
2285 field(&mut out, &0x0403_4b50_u32.to_le_bytes());
2286 field(&mut out, &[20, 0, 0, 0, 8, 0, 0, 0, 0, 0]);
2287 field(&mut out, &0_u32.to_le_bytes());
2288 field(&mut out, &compressed.to_le_bytes());
2289 field(&mut out, &size.to_le_bytes());
2290 field(&mut out, &name_len.to_le_bytes());
2291 field(&mut out, &0_u16.to_le_bytes());
2292 field(&mut out, name.as_bytes());
2293 field(&mut out, stream);
2294 field(&mut directory, &0x0201_4b50_u32.to_le_bytes());
2295 field(&mut directory, &[20, 0, 20, 0, 0, 0, 8, 0, 0, 0, 0, 0]);
2296 field(&mut directory, &0_u32.to_le_bytes());
2297 field(&mut directory, &compressed.to_le_bytes());
2298 field(&mut directory, &size.to_le_bytes());
2299 field(&mut directory, &name_len.to_le_bytes());
2300 field(&mut directory, &[0; 12]);
2301 field(&mut directory, &offset.to_le_bytes());
2302 field(&mut directory, name.as_bytes());
2303 }
2304 let count = u16::try_from(entries.len()).unwrap();
2305 let at = u32::try_from(out.len()).unwrap();
2306 let size = u32::try_from(directory.len()).unwrap();
2307 out.extend_from_slice(&directory);
2308 field(&mut out, &0x0605_4b50_u32.to_le_bytes());
2309 field(&mut out, &[0, 0, 0, 0]);
2310 field(&mut out, &count.to_le_bytes());
2311 field(&mut out, &count.to_le_bytes());
2312 field(&mut out, &size.to_le_bytes());
2313 field(&mut out, &at.to_le_bytes());
2314 field(&mut out, &0_u16.to_le_bytes());
2315 out
2316 }
2317
2318 #[test]
2323 fn the_default_limits_refuse_a_deflate_bomb() {
2324 let small = zeros_bomb(1000);
2326 let zeros = crate::inflate::inflate(&small, 1 + 258 * 1000).unwrap();
2327 assert!(zeros.iter().all(|&b| b == 0));
2328
2329 let repeats = (3 << 29) / 258;
2330 let size = u32::try_from(1 + 258 * repeats).unwrap();
2331 let stream = zeros_bomb(repeats);
2332 assert!(size as usize > 1000 * stream.len(), "{}", stream.len());
2333 let bytes = deflated_archive(&[
2334 ("a", &stream, size),
2335 ("b", &stream, size),
2336 ("c", &stream, size),
2337 ]);
2338 assert!(bytes.len() < 5 << 20, "{}", bytes.len());
2339 let message = read_package(&bytes).unwrap_err().to_string();
2340 assert!(message.contains("past the 4294967296 allowed"), "{message}");
2341
2342 let names: Vec<String> = (0..4097).map(|k| format!("e{k:04}")).collect();
2344 let many: Vec<(&str, &[u8])> = names.iter().map(|n| (n.as_str(), &b""[..])).collect();
2345 let message = read_package(&archive(&many)).unwrap_err().to_string();
2346 assert!(message.contains("4097 entries"), "{message}");
2347 assert_eq!(read_package(&archive(&many[..4096])).unwrap().len(), 4096);
2348 }
2349
2350 #[test]
2352 fn the_corpus_reads_within_the_default_limits() {
2353 let corpus = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/corpus");
2354 let mut read = 0;
2355 for entry in std::fs::read_dir(corpus).unwrap() {
2356 let path = entry.unwrap().path();
2357 if path.extension().is_some_and(|e| e == "3mf") {
2358 let bytes = std::fs::read(&path).unwrap();
2359 assert!(read_package(&bytes).is_ok(), "{}", path.display());
2360 assert!(read_3mf(&bytes, T).is_ok(), "{}", path.display());
2361 read += 1;
2362 }
2363 }
2364 assert!(read >= 4, "{read}");
2365 }
2366
2367 #[test]
2370 fn hostile_packages_never_panic() {
2371 let model = model(
2372 &format!(
2373 r#"<object id="1" type="model">{}</object>"#,
2374 cube(0.0, |_| String::new())
2375 ),
2376 r#"<item objectid="1"/>"#,
2377 );
2378 let bytes = package(&[
2379 ("3D/3dmodel.model", model.as_bytes()),
2380 (
2381 "Metadata/a.config",
2382 b"<config><object id=\"x\"><volume firstid=\"q\"/>",
2383 ),
2384 ]);
2385 let import = read_3mf(&bytes, T).unwrap();
2386 assert!(
2387 import
2388 .warnings
2389 .iter()
2390 .any(|w| w.contains("configuration part"))
2391 );
2392 assert_eq!(import.objects[0].mesh.triangles.len(), 12);
2393 for cut in 0..bytes.len() {
2394 assert!(read_3mf(&bytes[..cut], T).is_err(), "cut at {cut}");
2395 }
2396 let mut seed = 0x9e37_79b9_u32;
2397 for _ in 0..2000 {
2398 let mut noisy = bytes.clone();
2399 seed ^= seed << 13;
2400 seed ^= seed >> 17;
2401 seed ^= seed << 5;
2402 let at = seed as usize % noisy.len();
2403 noisy[at] ^= (seed >> 8) as u8 | 1;
2404 let _ = read_3mf(&noisy, T);
2405 }
2406 for garbage in [&b""[..], b"PK\x05\x06", b"\x89PNG\r\n\x1a\n"] {
2407 assert!(read_3mf(garbage, T).is_err());
2408 }
2409 }
2410}