1#![deny(
48 clippy::unwrap_used,
49 clippy::expect_used,
50 clippy::indexing_slicing,
51 clippy::panic,
52 clippy::unreachable
53)]
54
55use std::collections::{HashMap, HashSet};
56use std::io::{self, Read, Seek, SeekFrom};
57
58use flate2::read::DeflateDecoder;
59
60pub const PREVIEW_SECTION: &str = "PreviewPNG";
62pub const COMPONENT_TREE_SECTION: &str = "swXmlContents/COMPINSTANCETREE";
64pub const CONTENT_MANAGER_SECTION: &str = "SwDocContentMgr/SwDocContentMgrInfo";
66
67const MARKER: [u8; 6] = [0x14, 0x00, 0x06, 0x00, 0x08, 0x00];
68const FIXED: usize = MARKER.len() + 20;
70const FIRST_WITHIN: usize = 64;
72const MAX_NAME: usize = 512;
73pub const MAX_SECTIONS: usize = 100_000;
75pub const MAX_SEARCH: u64 = 64 * 1024 * 1024;
78const SIGNATURE: usize = 4;
80const CHUNK: usize = 64 * 1024;
81pub const MAX_TRIES: usize = 4;
84
85#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct Section {
88 pub name: String,
89 pub offset: u64,
91 pub compressed: u64,
92 pub size: u64,
93 pub crc: u32,
94 pub confirmed: bool,
97}
98
99fn u32_at(bytes: &[u8], at: usize) -> Option<u32> {
100 let b = bytes.get(at..at.checked_add(4)?)?;
101 Some(u32::from_le_bytes(b.try_into().ok()?))
102}
103
104fn parse_header(buf: &[u8], at: u64, file_len: u64) -> Option<Section> {
108 if !buf.starts_with(&MARKER) {
109 return None;
110 }
111 let crc = u32_at(buf, 10)?;
112 let compressed = u32_at(buf, 14)?;
113 let size = u32_at(buf, 18)?;
114 let name_len = usize::try_from(u32_at(buf, 22)?).ok()?;
115 if !(1..=MAX_NAME).contains(&name_len) {
116 return None;
117 }
118 if compressed > 0
120 && u64::from(crc) == 4 * u64::from(compressed)
121 && u64::from(size) == 2 * u64::from(compressed)
122 {
123 return None;
124 }
125 let raw = buf.get(FIXED..FIXED + name_len)?;
126 let name: String = raw.iter().map(|b| char::from(b.rotate_left(4))).collect();
127 if !name.chars().all(|c| (' '..='~').contains(&c)) || name.trim().is_empty() {
128 return None;
129 }
130 let offset = at.checked_add((FIXED + name_len) as u64)?;
131 let end = offset.checked_add(u64::from(compressed))?;
132 if compressed == 0 || end > file_len {
133 return None;
134 }
135 Some(Section {
136 name,
137 offset,
138 compressed: u64::from(compressed),
139 size: u64::from(size),
140 crc,
141 confirmed: false,
142 })
143}
144
145fn find_marker(hay: &[u8]) -> Option<usize> {
146 let mut from = 0;
147 while let Some(i) = hay.get(from..)?.iter().position(|&b| b == MARKER[0]) {
148 let at = from + i;
149 if hay.get(at..at + MARKER.len())? == MARKER {
150 return Some(at);
151 }
152 from = at + 1;
153 }
154 None
155}
156
157pub fn is_container(head: &[u8]) -> bool {
161 first_section_at(head).is_some()
162}
163
164fn first_section_at(head: &[u8]) -> Option<usize> {
165 let window = head.get(..FIRST_WITHIN + MARKER.len()).unwrap_or(head);
166 let mut from = 0;
167 while let Some(i) = window.get(from..).and_then(find_marker) {
168 let at = from + i;
169 if head.get(at..).is_some_and(plausible_record) {
172 return Some(at);
173 }
174 from = at + 1;
175 }
176 None
177}
178
179fn plausible_record(buf: &[u8]) -> bool {
181 let Some(name_len) = u32_at(buf, 22).and_then(|n| usize::try_from(n).ok()) else {
182 return false;
183 };
184 (1..=MAX_NAME).contains(&name_len)
185 && buf
186 .get(FIXED..FIXED + name_len)
187 .is_some_and(|raw| raw.iter().all(|b| (0x20..0x7f).contains(&b.rotate_left(4))))
188}
189
190fn read_at<R: Read + Seek>(reader: &mut R, pos: u64, buf: &mut [u8]) -> io::Result<usize> {
192 reader.seek(SeekFrom::Start(pos))?;
193 let mut n = 0;
194 while let Some(rest) = buf.get_mut(n..).filter(|r| !r.is_empty()) {
195 match reader.read(rest) {
196 Ok(0) => break,
197 Ok(k) => n += k,
198 Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
199 Err(e) => return Err(e),
200 }
201 }
202 Ok(n)
203}
204
205pub fn inflate<R: Read>(compressed: R, size: u64, crc: u32) -> io::Result<Option<Vec<u8>>> {
209 let mut out = Vec::with_capacity(usize::try_from(size.min(1 << 20)).unwrap_or(0));
210 match DeflateDecoder::new(compressed)
211 .take(size.saturating_add(1))
212 .read_to_end(&mut out)
213 {
214 Ok(_) => {}
215 Err(e)
216 if matches!(
217 e.kind(),
218 io::ErrorKind::InvalidInput
219 | io::ErrorKind::InvalidData
220 | io::ErrorKind::UnexpectedEof
221 ) =>
222 {
223 return Ok(None)
224 }
225 Err(e) => return Err(e),
226 }
227 Ok((out.len() as u64 == size && crc32fast::hash(&out) == crc).then_some(out))
228}
229
230#[derive(Default)]
233struct Window {
234 start: u64,
235 data: Vec<u8>,
236 at_eof: bool,
238}
239
240impl Window {
241 fn get<R: Read + Seek>(&mut self, reader: &mut R, pos: u64, len: usize) -> io::Result<&[u8]> {
243 let rest = self.rest(reader, pos, len)?;
244 Ok(rest.get(..len).unwrap_or(rest))
245 }
246
247 fn rest<R: Read + Seek>(&mut self, reader: &mut R, pos: u64, min: usize) -> io::Result<&[u8]> {
252 let end = self.start + self.data.len() as u64;
253 let inside = pos >= self.start
254 && pos <= end
255 && (self.at_eof || pos.saturating_add(min as u64) <= end)
256 && !self.data.is_empty();
257 if !inside {
258 let want = CHUNK.max(min);
259 self.data.resize(want, 0);
260 let n = read_at(reader, pos, &mut self.data)?;
261 self.data.truncate(n);
262 self.at_eof = n < want;
263 self.start = pos;
264 }
265 let from = usize::try_from(pos - self.start).unwrap_or(usize::MAX);
266 Ok(self.data.get(from..).unwrap_or_default())
267 }
268}
269
270pub struct SwContainer<R> {
272 reader: R,
273 sections: Vec<Section>,
274}
275
276impl<R: Read + Seek> SwContainer<R> {
277 pub fn open(mut reader: R) -> io::Result<Option<Self>> {
279 let file_len = reader.seek(SeekFrom::End(0))?;
280 let mut head = vec![0u8; FIRST_WITHIN + FIXED + MAX_NAME];
281 let n = read_at(&mut reader, 0, &mut head)?;
282 head.truncate(n);
283 let Some(first) = first_section_at(&head) else {
284 return Ok(None);
285 };
286 let sections = Self::walk(&mut reader, first as u64, file_len)?;
287 Ok(Some(Self { reader, sections }))
288 }
289
290 fn walk(reader: &mut R, mut pos: u64, file_len: u64) -> io::Result<Vec<Section>> {
298 let mut sections = Vec::new();
299 let mut searched = 0u64;
300 let mut window = Window::default();
301 let mut signature: Option<[u8; 4]> = None;
302 while pos < file_len && searched < MAX_SEARCH && sections.len() < MAX_SECTIONS {
303 let chunk = window.rest(reader, pos, FIXED + MAX_NAME)?;
306 let n = chunk.len();
307 let Some(i) = find_marker(chunk) else {
308 if n < MARKER.len() {
309 break;
310 }
311 let step = (n - (MARKER.len() - 1)) as u64;
313 searched += step;
314 pos += step;
315 continue;
316 };
317 searched += i as u64 + 1;
318 let at = pos + i as u64;
319 let before: Option<[u8; 4]> = match at.checked_sub(4) {
320 Some(b) => window.get(reader, b, 4)?.try_into().ok(),
321 None => None,
322 };
323 let header = window.get(reader, at, FIXED + MAX_NAME)?;
324 let section = parse_header(header, at, file_len)
325 .filter(|_| signature.is_none() || before == signature);
326 let Some(mut section) = section else {
327 pos = at + 1;
328 continue;
329 };
330 let end = section.offset + section.compressed;
331 if signature.is_some() {
332 section.confirmed = true;
333 sections.push(section);
334 pos = end;
335 continue;
336 }
337 let after = window.get(reader, end, SIGNATURE + MARKER.len())?;
338 let (sig_after, marker_after) = after.split_at(SIGNATURE.min(after.len()));
339 if marker_after == MARKER && before.is_some_and(|b| sig_after == b) {
340 signature = before;
341 section.confirmed = true;
342 pos = end;
343 } else {
344 pos = at + 1;
345 }
346 sections.push(section);
347 }
348 Ok(sections)
349 }
350
351 pub fn sections(&self) -> &[Section] {
352 &self.sections
353 }
354
355 pub fn read(&mut self, name: &str, max: u64) -> io::Result<Option<Vec<u8>>> {
364 let named = |confirmed: bool| {
365 self.sections
366 .iter()
367 .filter(move |s| s.name == name && s.size <= max && s.confirmed == confirmed)
368 .take(MAX_TRIES)
369 .cloned()
370 };
371 let candidates: Vec<Section> = named(true).chain(named(false)).collect();
372 for s in candidates {
373 self.reader.seek(SeekFrom::Start(s.offset))?;
374 let input = (&mut self.reader).take(s.compressed);
375 if let Some(data) = inflate(input, s.size, s.crc)? {
376 return Ok(Some(data));
377 }
378 }
379 Ok(None)
380 }
381}
382
383#[derive(Clone, Debug, Default, PartialEq, Eq)]
385pub struct ReferenceData {
386 pub components: Option<Vec<String>>,
389 pub content_manager: Option<Vec<u8>>,
392}
393
394pub fn reference_data<R: Read + Seek>(reader: R, max: u64) -> io::Result<Option<ReferenceData>> {
397 let Some(mut file) = SwContainer::open(reader)? else {
398 return Ok(None);
399 };
400 let components = file
401 .read(COMPONENT_TREE_SECTION, max)?
402 .map(|xml| components(&xml));
403 let content_manager = match components {
404 Some(_) => None,
405 None => file.read(CONTENT_MANAGER_SECTION, max)?,
406 };
407 Ok(Some(ReferenceData {
408 components,
409 content_manager,
410 }))
411}
412
413pub fn file_component_counts<R: Read + Seek>(
416 reader: R,
417 max: u64,
418) -> io::Result<Option<Vec<(String, u32)>>> {
419 let Some(mut file) = SwContainer::open(reader)? else {
420 return Ok(None);
421 };
422 Ok(file
423 .read(COMPONENT_TREE_SECTION, max)?
424 .map(|xml| component_counts(&xml)))
425}
426
427fn xml_text(xml: &[u8]) -> String {
429 let utf16 = |bytes: &[u8], be: bool| {
430 let units: Vec<u16> = bytes
431 .chunks_exact(2)
432 .filter_map(|c| <[u8; 2]>::try_from(c).ok())
433 .map(|c| {
434 if be {
435 u16::from_be_bytes(c)
436 } else {
437 u16::from_le_bytes(c)
438 }
439 })
440 .collect();
441 String::from_utf16_lossy(&units)
442 };
443 match xml {
444 [0xFF, 0xFE, rest @ ..] => utf16(rest, false),
445 [0xFE, 0xFF, rest @ ..] => utf16(rest, true),
446 [b'<', 0, ..] => utf16(xml, false),
447 _ => String::from_utf8_lossy(xml.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(xml)).into_owned(),
448 }
449}
450
451fn attr(e: &quick_xml::events::BytesStart<'_>, name: &str) -> Option<String> {
452 e.attributes()
453 .flatten()
454 .find(|a| a.key.local_name().as_ref() == name)
455 .and_then(|a| {
456 a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
457 .ok()
458 .map(|v| v.into_owned())
459 })
460}
461
462pub fn components(xml: &[u8]) -> Vec<String> {
479 let mut seen = HashSet::new();
480 tree_instances(xml)
481 .into_iter()
482 .map(|(path, _)| path)
483 .filter(|path| seen.insert(path.to_lowercase()))
484 .collect()
485}
486
487pub fn component_counts(xml: &[u8]) -> Vec<(String, u32)> {
492 let mut out: Vec<(String, u32)> = Vec::new();
493 let mut index: HashMap<String, usize> = HashMap::new();
494 for (path, excluded) in tree_instances(xml) {
495 let at = *index.entry(path.to_lowercase()).or_insert_with(|| {
496 out.push((path, 0));
497 out.len() - 1
498 });
499 if let Some(entry) = out.get_mut(at) {
500 entry.1 += u32::from(!excluded);
501 }
502 }
503 out
504}
505
506fn tree_instances(xml: &[u8]) -> Vec<(String, bool)> {
508 use quick_xml::events::Event;
509 let text = xml_text(xml);
510 let mut reader = quick_xml::Reader::from_str(&text);
511 let mut files: HashMap<String, String> = HashMap::new();
512 let mut model_file: HashMap<String, String> = HashMap::new();
513 let mut instances: Vec<(String, String, bool)> = Vec::new();
515 let mut current_model: Option<String> = None;
516 loop {
517 let event = match reader.read_event() {
518 Ok(Event::Eof) | Err(_) => break,
519 Ok(event) => event,
520 };
521 match &event {
522 Event::Start(e) | Event::Empty(e) => match e.local_name().as_ref() {
523 "swFile" => {
524 if let (Some(id), Some(path)) = (attr(e, "id"), attr(e, "swPath")) {
525 files.insert(id, path);
526 }
527 }
528 "swModel" => {
529 let id = attr(e, "id").unwrap_or_default();
530 if let Some(file) = attr(e, "swFileRef") {
531 model_file.insert(id.clone(), file);
532 }
533 if matches!(event, Event::Start(_)) {
534 current_model = Some(id);
535 }
536 }
537 "swReference" => {
538 let is_virtual = attr(e, "swIsVirtualComponent")
539 .is_some_and(|v| v.eq_ignore_ascii_case("YES"));
540 let excluded = attr(e, "swExcludeFromBOM")
541 .is_some_and(|v| v.eq_ignore_ascii_case("YES") || v == "1");
542 if let (Some(parent), Some(target), false) =
543 (current_model.clone(), attr(e, "swModelRef"), is_virtual)
544 {
545 instances.push((parent, target, excluded));
546 }
547 }
548 _ => {}
549 },
550 Event::End(e) if e.local_name().as_ref() == "swModel" => current_model = None,
551 _ => {}
552 }
553 if instances.len() > 1_000_000 {
554 break;
555 }
556 }
557 let used: HashSet<&str> = instances.iter().map(|(_, t, _)| t.as_str()).collect();
558 instances
559 .iter()
560 .filter(|(parent, _, _)| !used.contains(parent.as_str()))
561 .filter_map(|(_, target, excluded)| {
562 Some((files.get(model_file.get(target)?)?.clone(), *excluded))
563 })
564 .collect()
565}
566
567const MAX_TREE_DEPTH: usize = 8;
569const MAX_PLACED: usize = 100_000;
571
572#[derive(Debug)]
574struct Instance {
575 model: String,
576 transform: [f64; 16],
577 left_out: bool,
578}
579
580pub fn placements(xml: &[u8]) -> Option<Vec<crate::nxasm::Component>> {
602 use quick_xml::events::Event;
603 let text = xml_text(xml);
604 let mut reader = quick_xml::Reader::from_str(&text);
605 let mut files: HashMap<String, (String, bool)> = HashMap::new();
607 let mut model_file: HashMap<String, String> = HashMap::new();
608 let mut instances: HashMap<String, Vec<Instance>> = HashMap::new();
609 let mut configurations: Vec<(String, bool)> = Vec::new();
610 let mut current_model: Option<String> = None;
611 let mut count = 0usize;
612 loop {
613 let event = match reader.read_event() {
614 Ok(Event::Eof) | Err(_) => break,
615 Ok(event) => event,
616 };
617 match &event {
618 Event::Start(e) | Event::Empty(e) => match e.local_name().as_ref() {
619 "swFile" => {
620 if let (Some(id), Some(path)) = (attr(e, "id"), attr(e, "swPath")) {
621 let assembly = attr(e, "swDocType")
622 .is_some_and(|t| t.eq_ignore_ascii_case("ASSEMBLY"));
623 files.insert(id, (path, assembly));
624 }
625 }
626 "swModel" => {
627 let id = attr(e, "id").unwrap_or_default();
628 if let Some(file) = attr(e, "swFileRef") {
629 model_file.insert(id.clone(), file);
630 }
631 if matches!(event, Event::Start(_)) {
632 current_model = Some(id);
633 }
634 }
635 "swReference" => {
636 let yes =
637 |name: &str| attr(e, name).is_some_and(|v| v.eq_ignore_ascii_case("YES"));
638 let transform = attr(e, "swTransform").and_then(|t| matrix(&t));
639 if let (Some(parent), Some(model), Some(transform)) =
640 (current_model.clone(), attr(e, "swModelRef"), transform)
641 {
642 count += 1;
643 instances.entry(parent).or_default().push(Instance {
644 model,
645 transform,
646 left_out: yes("swSuppressed")
647 || yes("swHidden")
648 || yes("swIsVirtualComponent"),
649 });
650 }
651 }
652 "swConfiguration" => {
653 if let Some(model) = attr(e, "swModelRef") {
654 let recent = attr(e, "swMostRecentConfiguration")
655 .is_some_and(|v| v.eq_ignore_ascii_case("YES"));
656 configurations.push((model, recent));
657 }
658 }
659 _ => {}
660 },
661 Event::End(e) if e.local_name().as_ref() == "swModel" => current_model = None,
662 _ => {}
663 }
664 if count > 1_000_000 {
665 break;
666 }
667 }
668 let top = configurations
669 .iter()
670 .find(|(_, recent)| *recent)
671 .or_else(|| configurations.first())
672 .map(|(model, _)| model.clone())
673 .filter(|m| instances.contains_key(m))
674 .or_else(|| {
675 let used: HashSet<&str> = instances
677 .values()
678 .flatten()
679 .map(|i| i.model.as_str())
680 .collect();
681 instances
682 .keys()
683 .find(|m| !used.contains(m.as_str()))
684 .cloned()
685 })?;
686 let mut out = Vec::new();
687 let mut stack = vec![(top, crate::nxasm::identity(), 0usize)];
688 while let Some((model, parent, depth)) = stack.pop() {
689 for instance in instances.get(&model).into_iter().flatten().rev() {
690 if instance.left_out || out.len() >= MAX_PLACED {
691 continue;
692 }
693 let Some((path, assembly)) = model_file.get(&instance.model).and_then(|f| files.get(f))
694 else {
695 continue;
696 };
697 let t = &instance.transform;
698 let placed = crate::nxasm::Component {
699 name: file_name(path).to_string(),
700 rotation: [[t[0], t[1], t[2]], [t[4], t[5], t[6]], [t[8], t[9], t[10]]],
701 translation: [t[12] * 1000.0, t[13] * 1000.0, t[14] * 1000.0],
702 layer: None,
703 occurrence: None,
704 document: None,
705 }
706 .within(&parent);
707 let children = instances
708 .get(&instance.model)
709 .is_some_and(|c| !c.is_empty());
710 if *assembly && children && depth < MAX_TREE_DEPTH {
711 stack.push((instance.model.clone(), placed, depth + 1));
712 } else if !*assembly || !children {
713 out.push(placed);
715 }
716 }
717 }
718 Some(out)
719}
720
721fn matrix(text: &str) -> Option<[f64; 16]> {
723 let mut m = [0.0; 16];
724 let mut values = text.split_ascii_whitespace();
725 for v in m.iter_mut() {
726 *v = values
727 .next()?
728 .parse::<f64>()
729 .ok()
730 .filter(|x| x.is_finite())?;
731 }
732 values.next().is_none().then_some(m)
733}
734
735fn file_name(path: &str) -> &str {
737 path.rsplit(['\\', '/']).next().unwrap_or(path)
738}
739
740#[cfg(test)]
741mod tests {
742 use super::*;
743 use std::io::{Cursor, Write};
744
745 fn fixture(name: &str) -> Vec<u8> {
746 let dir = concat!(
747 env!("CARGO_MANIFEST_DIR"),
748 "/tests/fixtures/cad/nist/mtc-assembly/solidworks"
749 );
750 std::fs::read(format!("{dir}/{name}")).unwrap()
751 }
752
753 fn deflate(data: &[u8]) -> Vec<u8> {
754 let mut e = flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::default());
755 e.write_all(data).unwrap();
756 e.finish().unwrap()
757 }
758
759 const SIG: [u8; 4] = [0xda, 0x1f, 0x8b, 0xe1];
760
761 fn section(name: &str, data: &[u8]) -> Vec<u8> {
763 let packed = deflate(data);
764 raw_section(name, &packed, data.len() as u32, crc32fast::hash(data))
765 }
766
767 fn raw_section(name: &str, packed: &[u8], size: u32, crc: u32) -> Vec<u8> {
768 let mut out = MARKER.to_vec();
769 out.extend_from_slice(&[0xc3, 0xf8, 0xf8, 0x33]);
770 for v in [crc, packed.len() as u32, size, name.len() as u32] {
771 out.extend_from_slice(&v.to_le_bytes());
772 }
773 out.extend(name.bytes().map(|b| b.rotate_left(4)));
774 out.extend_from_slice(packed);
775 out
776 }
777
778 fn container(sections: &[Vec<u8>]) -> Vec<u8> {
780 let mut out = vec![0x32, 0x34, 0x09, 0x2c, 0, 0, 0, 4, 0xc1];
781 for s in sections {
782 out.extend_from_slice(&SIG);
783 out.extend_from_slice(s);
784 }
785 out
786 }
787
788 #[test]
789 fn records_without_the_signature_are_ignored_once_it_is_known() {
790 let mut bytes = container(&[section("One", b"one"), section("Two", b"two")]);
791 bytes.extend_from_slice(&[9, 9, 9, 9]);
793 bytes.extend(section("Stray", b"stray"));
794 bytes.extend_from_slice(&SIG);
795 bytes.extend(section("Three", b"three"));
796 let p = std::env::temp_dir().join(format!("teamcraft-swpart-{}.bin", uuid::Uuid::new_v4()));
800 std::fs::write(&p, &bytes).unwrap();
801 let sw = SwContainer::open(std::fs::File::open(&p).unwrap())
802 .unwrap()
803 .unwrap();
804 let names: Vec<&str> = sw.sections().iter().map(|s| s.name.as_str()).collect();
805 let _ = std::fs::remove_file(&p);
806 assert_eq!(names, ["One", "Two", "Three"]);
807 }
808
809 fn brute_force(bytes: &[u8]) -> Vec<String> {
812 let mut out = Vec::new();
813 for at in 0..bytes.len() {
814 if let Some(s) = parse_header(&bytes[at..], at as u64, bytes.len() as u64) {
815 let data = &bytes[s.offset as usize..(s.offset + s.compressed) as usize];
816 if inflate(data, s.size, s.crc).unwrap().is_some() {
817 out.push(s.name);
818 }
819 }
820 }
821 out
822 }
823
824 fn all_fixtures() -> Vec<std::path::PathBuf> {
825 fn walk(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
826 for e in std::fs::read_dir(dir).unwrap() {
827 let p = e.unwrap().path();
828 let ext = p
829 .extension()
830 .map(|x| x.to_string_lossy().to_lowercase())
831 .unwrap_or_default();
832 if p.is_dir() {
833 walk(&p, out);
834 } else if matches!(ext.as_str(), "sldprt" | "sldasm" | "slddrw") {
835 out.push(p);
836 }
837 }
838 }
839 let mut out = Vec::new();
840 walk(
841 std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/cad")),
842 &mut out,
843 );
844 assert!(out.len() >= 10, "{out:?}");
845 out
846 }
847
848 #[test]
849 fn the_walk_finds_every_section_of_the_nist_files() {
850 for path in all_fixtures() {
851 let bytes = std::fs::read(&path).unwrap();
852 assert!(is_container(&bytes[..600]), "{path:?}");
853 let mut file = SwContainer::open(Cursor::new(&bytes)).unwrap().unwrap();
854 let mut good = Vec::new();
855 for s in file.sections().to_vec() {
856 if file.read(&s.name, u64::MAX).unwrap().is_some() && !good.contains(&s.name) {
857 good.push(s.name);
858 }
859 }
860 let mut expected = brute_force(&bytes);
861 expected.sort();
862 expected.dedup();
863 good.sort();
864 assert_eq!(good, expected, "{path:?}");
865 for name in [PREVIEW_SECTION, CONTENT_MANAGER_SECTION, "Header2"] {
866 assert!(good.iter().any(|n| n == name), "{name} in {path:?}");
867 }
868 }
869 }
870
871 #[test]
872 fn every_nist_file_has_a_png_preview() {
873 for path in all_fixtures() {
874 let file = std::fs::File::open(&path).unwrap();
875 let mut sw = SwContainer::open(io::BufReader::new(file))
876 .unwrap()
877 .unwrap();
878 let png = sw.read(PREVIEW_SECTION, 8 << 20).unwrap().unwrap();
879 assert!(png.starts_with(b"\x89PNG\r\n\x1a\n"), "{path:?}");
880 assert!(
881 sw.read(PREVIEW_SECTION, 1000).unwrap().is_none(),
882 "max size"
883 );
884 assert!(sw.read("Nope", 1 << 20).unwrap().is_none());
885 }
886 }
887
888 #[test]
889 fn the_assembly_names_exactly_its_eight_parts() {
890 let bytes = fixture("nist_mtc_crada_assembly_rev-D.SLDASM");
891 let data = reference_data(Cursor::new(&bytes), 32 << 20)
892 .unwrap()
893 .unwrap();
894 let mut names: Vec<String> = data
895 .components
896 .unwrap()
897 .iter()
898 .map(|p| p.rsplit('\\').next().unwrap().to_string())
899 .collect();
900 names.sort();
901 assert_eq!(
902 names,
903 [
904 "90591A141_ZINC PLATED STEEL HEX NUT.SLDPRT",
905 "91274A118_COATED ALLOY STEEL SOCKET HEAD CAP SCREW.SLDPRT",
906 "91274A141_COATED ALLOY STEEL SOCKET HEAD CAP SCREW.SLDPRT",
907 "91304A112_BLUE FLAT-HEAD SOCKET CAP SCREW.SLDPRT",
908 "nist_mtc_crada_box_rev-D.SLDPRT",
909 "nist_mtc_crada_cover_rev-B.SLDPRT",
910 "nist_mtc_crada_plate_rev-A.SLDPRT",
911 "nist_mtc_crada_polycarb-window.SLDPRT",
912 ]
913 );
914 assert!(data.content_manager.is_none());
915 let part = reference_data(
917 Cursor::new(fixture("nist_mtc_crada_box_rev-D.SLDPRT")),
918 32 << 20,
919 )
920 .unwrap()
921 .unwrap();
922 assert!(part.components.is_none());
923 assert!(part.content_manager.is_some());
924 }
925
926 #[test]
927 fn component_tree_counts_direct_components_only() {
928 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
929 <swSolidWorks><swHeader>
930 <swFile id="1" swPath="C:\p\Top.SLDASM"/>
931 <swFile id="2" swPath="C:\p\Sub.SLDASM"/>
932 <swFile id="3" swPath="C:\p\Bolt.SLDPRT"/>
933 <swFile id="4" swPath="C:\p\Nut.SLDPRT"/>
934 <swFile id="9" swPath="C:\p\Unused.SLDPRT"/>
935 </swHeader><swModelList>
936 <swModel id="10" swFileRef="3"/>
937 <swModel id="11" swFileRef="4"/>
938 <swModel id="12" swFileRef="2">
939 <swReference swModelRef="11" swIsVirtualComponent="NO"/>
940 </swModel>
941 <swModel id="13" swFileRef="1">
942 <swReference swModelRef="10" swIsVirtualComponent="NO"/>
943 <swReference swModelRef="10" swIsVirtualComponent="NO"/>
944 <swReference swModelRef="12" swIsVirtualComponent="NO"/>
945 <swReference swModelRef="11" swIsVirtualComponent="YES"/>
946 <swReference swModelRef="99"/>
947 </swModel>
948 </swModelList></swSolidWorks>"#;
949 assert_eq!(
950 components(xml.as_bytes()),
951 ["C:\\p\\Bolt.SLDPRT", "C:\\p\\Sub.SLDASM"]
952 );
953 let mut utf16 = vec![0xFF, 0xFE];
955 utf16.extend(xml.encode_utf16().flat_map(u16::to_le_bytes));
956 assert_eq!(components(&utf16), components(xml.as_bytes()));
957 assert!(components(b"\x00\x01\xff<<<").is_empty());
959 assert!(components(&xml.as_bytes()[..xml.len() / 3]).is_empty());
960 }
961
962 #[test]
963 fn component_tree_encodings_and_escapes() {
964 let xml = r#"<swSolidWorks><swFile id="1" swPath="C:\p\Gear & Shaft.SLDPRT"/><swFile id="2" swPath="c:\P\gear & shaft.sldprt"/><swModel id="3" swFileRef="1"/><swModel id="4" swFileRef="2"/><swModel id="5"><swReference swModelRef="3"/><swReference swModelRef="4"/></swModel></swSolidWorks>"#;
965 assert_eq!(components(xml.as_bytes()), ["C:\\p\\Gear & Shaft.SLDPRT"]);
967 let mut bom8 = b"\xEF\xBB\xBF".to_vec();
969 bom8.extend_from_slice(xml.as_bytes());
970 let mut be = vec![0xFE, 0xFF];
971 be.extend(xml.encode_utf16().flat_map(u16::to_be_bytes));
972 let le: Vec<u8> = xml.encode_utf16().flat_map(u16::to_le_bytes).collect();
973 for bytes in [bom8, be, le] {
974 assert_eq!(components(&bytes), components(xml.as_bytes()));
975 }
976 }
977
978 #[test]
979 fn component_counts_count_instances_without_bom_exclusion() {
980 let xml = br#"<swSolidWorks><swFile id="1" swPath="D:\a\Bolt.SLDPRT"/><swFile id="2" swPath="D:\a\Plate.SLDPRT"/><swFile id="3" swPath="D:\a\Glue.SLDPRT"/>
981<swModel id="4" swFileRef="1"/><swModel id="5" swFileRef="2"/><swModel id="6" swFileRef="3"/>
982<swModel id="7"><swReference swModelRef="4"/><swReference swModelRef="4"/><swReference swModelRef="5"/><swReference swModelRef="4" swExcludeFromBOM="YES"/><swReference swModelRef="6" swExcludeFromBOM="YES"/></swModel></swSolidWorks>"#;
983 assert_eq!(
984 component_counts(xml),
985 [
986 ("D:\\a\\Bolt.SLDPRT".to_string(), 2),
987 ("D:\\a\\Plate.SLDPRT".to_string(), 1),
988 ("D:\\a\\Glue.SLDPRT".to_string(), 0),
989 ]
990 );
991 assert_eq!(components(xml).len(), 3);
993 assert!(component_counts(b"garbage").is_empty());
994 }
995
996 #[test]
997 fn a_model_without_instances_has_no_components() {
998 let xml = br#"<swSolidWorks><swFile id="1" swPath="a.SLDPRT"/><swModel id="2" swFileRef="1"/></swSolidWorks>"#;
999 assert!(components(xml).is_empty());
1000 let xml = br#"<swSolidWorks><swModel id="2" swFileRef="9"/><swModel id="3"><swReference swModelRef="2"/><swReference swModelRef="8"/></swModel></swSolidWorks>"#;
1002 assert!(components(xml).is_empty());
1003 }
1004
1005 #[test]
1006 fn synthetic_container_round_trip() {
1007 let tree = br#"<swSolidWorks><swFile id="1" swPath="D:\a\Wheel.SLDPRT"/><swModel id="2" swFileRef="1"/><swModel id="3"><swReference swModelRef="2"/></swModel></swSolidWorks>"#;
1008 let bytes = container(&[
1009 section("Contents/Config-0", &[7u8; 5000]),
1010 section(COMPONENT_TREE_SECTION, tree),
1011 section(PREVIEW_SECTION, b"\x89PNG not really"),
1012 ]);
1013 let mut sw = SwContainer::open(Cursor::new(&bytes)).unwrap().unwrap();
1014 let names: Vec<&str> = sw.sections().iter().map(|s| s.name.as_str()).collect();
1015 assert_eq!(
1016 names,
1017 ["Contents/Config-0", COMPONENT_TREE_SECTION, PREVIEW_SECTION]
1018 );
1019 assert_eq!(
1020 sw.read(PREVIEW_SECTION, 100).unwrap().unwrap(),
1021 b"\x89PNG not really"
1022 );
1023 let refs = reference_data(Cursor::new(&bytes), 1 << 20)
1024 .unwrap()
1025 .unwrap();
1026 assert_eq!(refs.components.unwrap(), ["D:\\a\\Wheel.SLDPRT"]);
1027 }
1028
1029 #[test]
1030 fn other_files_and_garbage_are_not_containers() {
1031 for bytes in [
1032 b"".to_vec(),
1033 b"ISO-10303-21;".to_vec(),
1034 b"SPLMSSTR".to_vec(),
1035 vec![0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1, 0, 0],
1036 MARKER.to_vec(),
1037 [MARKER.to_vec(), vec![0; 16], vec![0xff; 4]].concat(),
1039 ] {
1040 assert!(!is_container(&bytes));
1041 assert!(SwContainer::open(Cursor::new(bytes)).unwrap().is_none());
1042 }
1043 let mut late = vec![0u8; 200];
1045 late.extend(section(PREVIEW_SECTION, b"x"));
1046 assert!(SwContainer::open(Cursor::new(late)).unwrap().is_none());
1047 }
1048
1049 #[test]
1050 fn hostile_sections_are_rejected() {
1051 let good = section("Good", b"hello hello hello");
1052 let mut beyond = raw_section("Beyond", &deflate(b"abc"), 3, crc32fast::hash(b"abc"));
1054 beyond[14..18].copy_from_slice(&u32::MAX.to_le_bytes());
1055 let bad_crc = raw_section("BadCrc", &deflate(b"abc"), 3, 12345);
1057 let bad_size = raw_section("BadSize", &deflate(b"abcdef"), 3, crc32fast::hash(b"abc"));
1058 let not_deflate = raw_section("Garbage", &[0xff; 64], 10, 0);
1059 let zeros = vec![0u8; 64 << 20];
1061 let bomb = raw_section("Bomb", &deflate(&zeros), 10, crc32fast::hash(&zeros[..10]));
1062 let huge = raw_section(
1064 "Huge",
1065 &deflate(&zeros),
1066 zeros.len() as u32,
1067 crc32fast::hash(&zeros),
1068 );
1069 let pseudo = raw_section("Pseudo", &[0u8; 20], 40, 80);
1071 let unprintable = raw_section("\u{1}\u{2}", &deflate(b"x"), 1, crc32fast::hash(b"x"));
1072 let bytes = container(&[
1073 good,
1074 bad_crc,
1075 bad_size,
1076 not_deflate,
1077 bomb,
1078 huge,
1079 pseudo,
1080 unprintable,
1081 beyond,
1082 ]);
1083 let mut sw = SwContainer::open(Cursor::new(&bytes)).unwrap().unwrap();
1084 let names: Vec<String> = sw.sections().iter().map(|s| s.name.clone()).collect();
1085 assert!(
1086 !names.iter().any(|n| n == "Pseudo" || n == "Beyond"),
1087 "{names:?}"
1088 );
1089 assert!(!names.iter().any(|n| n.contains('\u{1}')), "{names:?}");
1090 assert_eq!(sw.read("Good", 100).unwrap().unwrap(), b"hello hello hello");
1091 for name in ["BadCrc", "BadSize", "Garbage", "Bomb"] {
1092 assert!(names.iter().any(|n| n == name), "{name} in {names:?}");
1093 assert!(sw.read(name, u64::MAX).unwrap().is_none(), "{name}");
1094 }
1095 assert!(sw.read("Huge", 8 << 20).unwrap().is_none(), "limit");
1096 }
1097
1098 #[test]
1099 fn truncated_files_keep_the_sections_inside() {
1100 let bytes = fixture("nist_mtc_crada_assembly_rev-D.SLDASM");
1101 for cut in [700, bytes.len() / 3, bytes.len() / 2, bytes.len() - 1] {
1102 let part = &bytes[..cut];
1103 let Some(mut sw) = SwContainer::open(Cursor::new(part)).unwrap() else {
1104 continue;
1105 };
1106 assert!(sw
1107 .sections()
1108 .iter()
1109 .all(|s| s.offset + s.compressed <= cut as u64));
1110 for s in sw.sections().to_vec() {
1112 sw.read(&s.name, 1 << 20).unwrap();
1113 }
1114 let _ = reference_data(Cursor::new(part), 1 << 20).unwrap();
1115 }
1116 let s = section(PREVIEW_SECTION, &[9u8; 10_000]);
1118 let bytes = container(&[s]);
1119 let cut = &bytes[..bytes.len() - 50];
1120 let sw = SwContainer::open(Cursor::new(cut)).unwrap();
1121 assert!(sw.is_none_or(|sw| sw.sections().is_empty()));
1122 }
1123
1124 #[test]
1125 fn search_is_bounded_on_marker_floods() {
1126 let mut bytes = container(&[section("A", b"a")]);
1128 for _ in 0..20_000 {
1129 bytes.extend_from_slice(&MARKER);
1130 bytes.extend_from_slice(&[0xff; 26]);
1131 }
1132 let mut reader = CountingReader {
1133 inner: Cursor::new(&bytes),
1134 reads: 0,
1135 };
1136 let sw = SwContainer::open(&mut reader).unwrap().unwrap();
1137 assert_eq!(sw.sections().len(), 1);
1138 let chunks = bytes.len() / CHUNK + 1;
1141 assert!(reader.reads <= 4 * chunks + 8, "{} reads", reader.reads);
1142 }
1143
1144 struct CountingReader<R> {
1146 inner: R,
1147 reads: usize,
1148 }
1149
1150 impl<R: Read> Read for CountingReader<R> {
1151 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
1152 self.reads += 1;
1153 self.inner.read(buf)
1154 }
1155 }
1156
1157 impl<R: Seek> Seek for CountingReader<R> {
1158 fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
1159 self.inner.seek(pos)
1160 }
1161 }
1162
1163 #[test]
1164 fn bogus_copies_of_a_section_are_tried_only_a_few_times() {
1165 let bad = || raw_section("Dup", &deflate(b"abc"), 3, 1);
1167 let mut sections: Vec<Vec<u8>> = (0..MAX_TRIES).map(|_| bad()).collect();
1168 sections.push(section("Dup", b"abc"));
1169 let bytes = container(§ions);
1170 let mut sw = SwContainer::open(Cursor::new(&bytes)).unwrap().unwrap();
1171 assert_eq!(sw.sections().len(), MAX_TRIES + 1);
1172 assert!(sw.read("Dup", 100).unwrap().is_none(), "beyond the tries");
1173 let bytes = container(&[bad(), section("Dup", b"abc"), bad()]);
1175 let mut sw = SwContainer::open(Cursor::new(&bytes)).unwrap().unwrap();
1176 assert_eq!(sw.read("Dup", 100).unwrap().unwrap(), b"abc");
1177 }
1178
1179 #[test]
1180 fn damaged_nist_files_never_panic() {
1181 for name in [
1184 "nist_mtc_crada_assembly_rev-D.SLDASM",
1185 "nist_mtc_crada_box_rev-D.SLDPRT",
1186 ] {
1187 let bytes = fixture(name);
1188 let check = |data: &[u8]| {
1189 let Some(mut sw) = SwContainer::open(Cursor::new(data)).unwrap() else {
1190 return;
1191 };
1192 assert!(sw
1193 .sections()
1194 .iter()
1195 .all(|s| s.offset + s.compressed <= data.len() as u64));
1196 for name in [PREVIEW_SECTION, "Header2", "Contents/Config-0"] {
1197 sw.read(name, 1 << 20).unwrap();
1198 }
1199 let _ = reference_data(Cursor::new(data), 1 << 20).unwrap();
1200 };
1201 let step = bytes.len() / 60;
1202 for cut in (0..bytes.len()).step_by(step) {
1203 check(&bytes[..cut]);
1204 }
1205 let mut state = 0x2545_f491_4f6c_dd1du64;
1206 for _ in 0..60 {
1207 let mut damaged = bytes.clone();
1208 for _ in 0..8 {
1209 state ^= state << 13;
1210 state ^= state >> 7;
1211 state ^= state << 17;
1212 let at = (state % damaged.len() as u64) as usize;
1213 damaged[at] = (state >> 32) as u8;
1214 }
1215 check(&damaged);
1216 }
1217 }
1218 }
1219
1220 const TREE: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
1224<swSolidWorks xmlns="http://www.solidworks.com/sw2003/schema"><swHeader>
1225<swFile id="3" swDocType="ASSEMBLY" swPath="C:\work\Top.SLDASM"/>
1226<swFile id="6" swDocType="PART" swPath="C:\work\parts\Shaft.SLDPRT"/>
1227<swFile id="9" swDocType="ASSEMBLY" swPath="C:\work\Sub.SLDASM"/>
1228<swFile id="12" swDocType="PART" swPath="C:\Temp\Virtual.SLDPRT"/>
1229</swHeader><swModelList>
1230<swModel id="5" swFileRef="6"/>
1231<swModel id="11" swFileRef="12"/>
1232<swModel id="8" swFileRef="9">
1233<swReference swName="Shaft" swModelRef="5" swSuppressed="NO" swHidden="NO" swTransform="1 0 0 0 0 1 0 0 0 0 1 0 0.01 0 0 1"/>
1234</swModel>
1235<swModel id="2" swFileRef="3">
1236<swReference swName="Shaft" swModelRef="5" swSuppressed="NO" swHidden="NO" swTransform="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.005 1"/>
1237<swReference swName="Shaft" swModelRef="5" swSuppressed="NO" swHidden="YES" swTransform="1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 1"/>
1238<swReference swName="Shaft" swModelRef="5" swSuppressed="YES" swHidden="YES" swTransform="1 0 0 0 0 1 0 0 0 0 1 0 0 0 2 1"/>
1239<swReference swName="Virtual" swModelRef="11" swIsVirtualComponent="YES" swTransform="1 0 0 0 0 1 0 0 0 0 1 0 0 0 3 1"/>
1240<swReference swName="Sub" swModelRef="8" swSuppressed="NO" swHidden="NO" swTransform="0 1 0 0 -1 0 0 0 0 0 1 0 0 0 0.1 1"/>
1241</swModel>
1242</swModelList><swConfigurationList>
1243<swConfiguration swName="Default" swMostRecentConfiguration="YES" swModelRef="2"/>
1244</swConfigurationList></swSolidWorks>"#;
1245
1246 #[test]
1247 fn placements_resolve_sub_assemblies_and_leave_out_hidden_components() {
1248 let parts = placements(TREE.as_bytes()).unwrap();
1249 assert_eq!(parts.len(), 2, "{parts:?}");
1250 assert!(parts.iter().all(|p| p.name == "Shaft.SLDPRT"));
1251 let close = |a: [f64; 3], b: [f64; 3]| a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-9);
1252 let direct = parts.iter().find(|p| p.translation[2] < 50.0).unwrap();
1253 assert!(close(direct.place([0.0; 3]), [0.0, 0.0, 5.0]));
1254 let nested = parts.iter().find(|p| p.translation[2] > 50.0).unwrap();
1256 assert!(
1257 close(nested.place([0.0; 3]), [0.0, 10.0, 100.0]),
1258 "{nested:?}"
1259 );
1260 assert!(close(nested.place([1.0, 0.0, 0.0]), [0.0, 11.0, 100.0]));
1261 }
1262
1263 #[test]
1264 fn broken_component_trees_never_panic() {
1265 assert!(placements(b"").is_none());
1266 assert!(placements(b"<swSolidWorks><swModel id=\"1\"></swModel>").is_none());
1267 let bad = TREE.replace("0 0 0.1 1", "0 0 NaN 1");
1268 assert_eq!(placements(bad.as_bytes()).unwrap().len(), 1);
1269 for cut in (0..TREE.len()).step_by(37) {
1270 if TREE.is_char_boundary(cut) {
1271 let _ = placements(&TREE.as_bytes()[..cut]);
1272 }
1273 }
1274 }
1275
1276 #[test]
1277 fn the_nist_assembly_places_its_36_components() {
1278 let bytes = fixture("nist_mtc_crada_assembly_rev-D.SLDASM");
1279 let mut sw = SwContainer::open(Cursor::new(&bytes)).unwrap().unwrap();
1280 let xml = sw.read(COMPONENT_TREE_SECTION, 1 << 24).unwrap().unwrap();
1281 let parts = placements(&xml).unwrap();
1282 assert_eq!(parts.len(), 36);
1283 assert!(parts.iter().any(|p| p
1284 .name
1285 .eq_ignore_ascii_case("nist_mtc_crada_box_rev-D.SLDPRT")));
1286 for p in &parts {
1287 assert!(p.name.to_lowercase().ends_with(".sldprt"), "{}", p.name);
1288 for (i, row) in p.rotation.iter().enumerate() {
1289 for (j, other) in p.rotation.iter().enumerate() {
1290 let dot: f64 = row.iter().zip(other).map(|(a, b)| a * b).sum();
1291 let want = if i == j { 1.0 } else { 0.0 };
1292 assert!((dot - want).abs() < 1e-6, "{p:?}");
1293 }
1294 }
1295 }
1296 }
1297}