Skip to main content

teamcraft/
catprod.rs

1//! CATIA V5 products (`.CATProduct`): which documents are placed where (see
2//! `docs/src/research/catia-product-structure.md`).
3//!
4//! * Every CATIA document lists its containers near the start of the file:
5//!
6//!   ```text
7//!   02 00 81 20 | u32 n | name \0 | type \0 | up to 8 × 00
8//!   | 03 00 f7 00 03 00 00 00 | uuid (4 × u32 big-endian)
9//!   ```
10//!
11//!   Products refer to a component by the UUID of its container *named*
12//!   `CATProdCont` – no file name is stored ([`document`]).
13//! * The product's own `CATProdCont` is an object stream (`OSMX`) of
14//!   sections `7c <kind> <u32 length from the 7c>`: header `0c`, objects
15//!   `04`, one side block per object `08`, extensions `0b`, strings `02`.
16//!   Small numbers are `81..cf` (− 0x81), `d1..e3 xx` or `80 <u32>`; a
17//!   string reference is `34..ff` (− 0x34) or `32 <u32 index + 1>`; doubles
18//!   are `e6 <f64>`, `e7` / `e8` / `e9` (1, 0, −1).
19//! * An object (`7c 05`) has a header `7c 06` (two flag bytes, class, name,
20//!   `ea <u32 id>`) and attributes `7c 07`:
21//!   `<name> <key> e8|e9 <flags> <type> [byte after e9] 37 <value> fe`
22//!   (type `12`: a string, `07`: `<n> <0> n doubles`).
23//! * An instance is an `ASMPRODUCT` with `_InstanceName`; its name is the
24//!   part number. Its side block holds the link to the referenced document
25//!   (`e5 <u32 length> 11 03 <uuid> …`) and the list `_BagRepsList`, whose
26//!   `PRDBAGREP` carries `_Position`: X, Y and Z axis and origin (mm) of
27//!   the instance in the product. Without it the instance sits at the
28//!   origin.
29//!
30//! Everything is read defensively: every length is checked against the
31//! bytes that are left, and an instance that does not add up is left out,
32//! never a panic. No CATIA code or documentation was used.
33#![deny(
34    clippy::unwrap_used,
35    clippy::expect_used,
36    clippy::indexing_slicing,
37    clippy::panic,
38    clippy::unreachable
39)]
40
41use std::{
42    io::{self, Read},
43    path::Path,
44};
45
46use crate::nxasm::{Component, Kind};
47
48/// Magic of a CATIA V5 document.
49const MAGIC: &[u8; 8] = b"V5_CFV2\0";
50/// The container table lies in the first bytes of the file.
51pub const HEAD: usize = 64 * 1024;
52/// Largest product read.
53pub const MAX_PRODUCT: u64 = 64 * 1024 * 1024;
54/// Object streams tried at most.
55const MAX_STREAMS: usize = 64;
56/// Instances read at most.
57const MAX_INSTANCES: usize = 100_000;
58
59const CONTAINER_ENTRY: [u8; 4] = [0x02, 0x00, 0x81, 0x20];
60const UUID_TAG: [u8; 8] = [0x03, 0x00, 0xf7, 0x00, 0x03, 0x00, 0x00, 0x00];
61
62/// The text form of a container UUID, as CATIA names its containers
63/// (`Cont_<uuid>`): `hhhhhhhh_h_hhhhhhhh_h`.
64pub fn uuid_text(raw: &[u8]) -> Option<String> {
65    let w =
66        |k: usize| -> Option<u32> { Some(u32::from_be_bytes(raw.get(k..k + 4)?.try_into().ok()?)) };
67    Some(format!(
68        "{:08x}_{:x}_{:08x}_{:x}",
69        w(0)?,
70        w(4)?,
71        w(8)?,
72        w(12)?
73    ))
74}
75
76/// The containers of a CATIA document: (name, type, UUID).
77pub fn containers(data: &[u8]) -> Vec<(String, String, String)> {
78    let head = data.get(..data.len().min(HEAD)).unwrap_or_default();
79    let mut out = Vec::new();
80    let mut at = 0;
81    while let Some(found) = find(head, &CONTAINER_ENTRY, at) {
82        at = found + 1;
83        if let Some(entry) = container_at(head, found + CONTAINER_ENTRY.len() + 4) {
84            out.push(entry);
85        }
86    }
87    out
88}
89
90fn container_at(d: &[u8], at: usize) -> Option<(String, String, String)> {
91    let word = |from: usize| -> Option<(String, usize)> {
92        let rest = d.get(from..)?;
93        let len = rest.iter().take(64).position(|&b| b == 0)?;
94        let text = rest.get(..len)?;
95        if len == 0 || !text.iter().all(|b| b.is_ascii_alphanumeric() || *b == b'_') {
96            return None;
97        }
98        Some((String::from_utf8_lossy(text).into_owned(), from + len + 1))
99    };
100    let (name, at) = word(at)?;
101    let (kind, mut at) = word(at)?;
102    for _ in 0..8 {
103        if d.get(at) != Some(&0) {
104            break;
105        }
106        at += 1;
107    }
108    if d.get(at..at + UUID_TAG.len())? != UUID_TAG {
109        return None;
110    }
111    let uuid = uuid_text(d.get(at + UUID_TAG.len()..at + UUID_TAG.len() + 16)?)?;
112    Some((name, kind, uuid))
113}
114
115/// The UUID products refer to this document by (its container named
116/// `CATProdCont`), from the first [`HEAD`] bytes of a CATIA file.
117pub fn document(data: &[u8]) -> Option<String> {
118    if !data.starts_with(MAGIC) {
119        return None;
120    }
121    containers(data)
122        .into_iter()
123        .find(|(name, _, _)| name == "CATProdCont")
124        .map(|(_, _, uuid)| uuid)
125}
126
127/// [`document`] of a file (reads only its first bytes).
128pub fn document_of(path: &Path) -> io::Result<Option<String>> {
129    let mut head = Vec::with_capacity(HEAD);
130    std::fs::File::open(path)?
131        .take(HEAD as u64)
132        .read_to_end(&mut head)?;
133    Ok(document(&head))
134}
135
136/// What a product file holds: its instances, or [`Kind::Other`].
137pub fn kind_of(path: &Path) -> io::Result<Kind> {
138    let mut data = Vec::new();
139    std::fs::File::open(path)?
140        .take(MAX_PRODUCT)
141        .read_to_end(&mut data)?;
142    Ok(match components(&data) {
143        Some(c) if !c.is_empty() => Kind::Assembly(c),
144        _ => Kind::Other,
145    })
146}
147
148fn find(hay: &[u8], needle: &[u8], from: usize) -> Option<usize> {
149    hay.get(from..)?
150        .windows(needle.len())
151        .position(|w| w == needle)
152        .map(|p| p + from)
153}
154
155fn u32_at(d: &[u8], at: usize) -> Option<u32> {
156    Some(u32::from_le_bytes(d.get(at..at + 4)?.try_into().ok()?))
157}
158
159/// A small number: (value, next position).
160fn varint(d: &[u8], at: usize) -> Option<(u64, usize)> {
161    let b = *d.get(at)?;
162    match b {
163        0x80 => Some((u64::from(u32_at(d, at + 1)?), at + 5)),
164        0x81..=0xcf => Some((u64::from(b - 0x81), at + 1)),
165        0xd1..=0xe3 => Some((
166            (u64::from(b - 0xd1) << 8) + u64::from(*d.get(at + 1)?),
167            at + 2,
168        )),
169        _ => None,
170    }
171}
172
173/// A string-table reference: (index, next position).
174fn strref(d: &[u8], at: usize) -> Option<(usize, usize)> {
175    match *d.get(at)? {
176        0x32 => Some(((u32_at(d, at + 1)? as usize).checked_sub(1)?, at + 5)),
177        b @ 0x34..=0xff => Some((usize::from(b - 0x34), at + 1)),
178        _ => None,
179    }
180}
181
182/// The bytes of a string-table reference.
183fn strref_bytes(index: usize) -> Vec<u8> {
184    match u8::try_from(index + 0x34) {
185        Ok(b) => vec![b],
186        Err(_) => {
187            let mut v = vec![0x32];
188            v.extend((index as u32 + 1).to_le_bytes());
189            v
190        }
191    }
192}
193
194/// A number token: (value, next position).
195fn number(d: &[u8], at: usize) -> Option<(f64, usize)> {
196    match *d.get(at)? {
197        0xe6 => Some((
198            f64::from_le_bytes(d.get(at + 1..at + 9)?.try_into().ok()?),
199            at + 9,
200        )),
201        0xe7 => Some((1.0, at + 1)),
202        0xe8 => Some((0.0, at + 1)),
203        0xe9 => Some((-1.0, at + 1)),
204        _ => None,
205    }
206}
207
208/// The section `7c <kind>` at `at`: (body start, end).
209fn section(d: &[u8], at: usize, kind: u8) -> Option<(usize, usize)> {
210    if d.get(at..at + 2)? != [0x7c, kind] {
211        return None;
212    }
213    let end = at.checked_add(u32_at(d, at + 2)? as usize)?;
214    (end >= at + 6 && end <= d.len()).then_some((at + 6, end))
215}
216
217/// The section `7c <kind>` at `at` or a few bytes later.
218fn section_near(d: &[u8], at: usize, kind: u8) -> Option<(usize, usize, usize)> {
219    (at..at + 8).find_map(|p| section(d, p, kind).map(|(s, e)| (p, s, e)))
220}
221
222/// One object of the stream.
223struct Object {
224    id: Option<u32>,
225    class: Option<usize>,
226    name: Option<usize>,
227    attributes: (usize, usize),
228}
229
230/// The lists of an object's side block that matter.
231#[derive(Default)]
232struct Side {
233    link: Option<String>,
234    bags: Vec<u64>,
235}
236
237/// One object stream with its string table.
238struct Stream<'a> {
239    d: &'a [u8],
240    strings: Vec<&'a [u8]>,
241    objects: (usize, usize),
242    sides: (usize, usize),
243}
244
245impl<'a> Stream<'a> {
246    fn at(d: &'a [u8], osmx: usize) -> Option<Stream<'a>> {
247        let head = find(d.get(..d.len().min(osmx + 0x200))?, &[0x7c, 0x0c], osmx)?;
248        let (_, end) = section(d, head, 0x0c)?;
249        let (_, obj_start, obj_end) = section_near(d, end, 0x04)?;
250        let (_, side_start, side_end) = section_near(d, obj_end, 0x08)?;
251        let (_, _, ext_end) = section_near(d, side_end, 0x0b)?;
252        let (_, str_start, str_end) = section_near(d, ext_end, 0x02)?;
253        let strings = strings(d, str_start, str_end)?;
254        Some(Stream {
255            d,
256            strings,
257            objects: (obj_start, obj_end),
258            sides: (side_start, side_end),
259        })
260    }
261
262    fn index(&self, text: &str) -> Option<usize> {
263        self.strings.iter().position(|s| *s == text.as_bytes())
264    }
265
266    fn text(&self, index: Option<usize>) -> Option<String> {
267        let s = self.strings.get(index?)?;
268        Some(s.iter().map(|&b| char::from(b)).collect())
269    }
270
271    fn objects(&self) -> Vec<Object> {
272        let d = self.d;
273        let (mut p, end) = self.objects;
274        let mut out = Vec::new();
275        while p + 7 < end && out.len() <= MAX_INSTANCES * 4 {
276            let Some((_, next)) = section(d, p, 0x05) else {
277                break;
278            };
279            if next <= p {
280                break;
281            }
282            if let Some(object) = self.object(p + 7) {
283                out.push(object);
284            }
285            p = next;
286        }
287        out
288    }
289
290    fn object(&self, at: usize) -> Option<Object> {
291        let d = self.d;
292        let (h, hend) = section(d, at, 0x06)?;
293        let (class, q) = strref(d, h + 2)?;
294        let (name, q) = strref(d, q)?;
295        let id = (d.get(q) == Some(&0xea))
296            .then(|| u32_at(d, q + 1))
297            .flatten();
298        let attributes = section(d, hend, 0x07)?;
299        Some(Object {
300            id,
301            class: Some(class),
302            name: Some(name),
303            attributes,
304        })
305    }
306
307    /// One side block per object, in object order (`None` if the counts
308    /// differ).
309    fn sides(&self, count: usize, bags: Option<usize>) -> Option<Vec<Side>> {
310        let d = self.d;
311        let (mut p, end) = self.sides;
312        let mut out = Vec::new();
313        while p < end && out.len() <= count {
314            let (s, e) = section(d, p, 0x09)?;
315            out.push(side(d, s, e, bags));
316            p = e;
317        }
318        (out.len() == count).then_some(out)
319    }
320
321    /// Right after `<name> <key> e8|e9 <flags> <type> [x] 37` of the
322    /// attribute `name`, with its type.
323    fn attribute(&self, object: &Object, name: Option<usize>) -> Option<(usize, u8)> {
324        let d = self.d;
325        let pattern = strref_bytes(name?);
326        let (a, end) = object.attributes;
327        let mut p = a;
328        while p < end {
329            let starts = p == a || d.get(p.wrapping_sub(1)) == Some(&0xfe);
330            if starts && d.get(p..p + pattern.len()) == Some(pattern.as_slice()) {
331                if let Some((_, q)) = varint(d, p + pattern.len()) {
332                    if matches!(d.get(q), Some(0xe8 | 0xe9)) {
333                        let kind = *d.get(q + 2)?;
334                        let mut r = q + 3;
335                        if d.get(r) != Some(&0x37) {
336                            r += 1;
337                        }
338                        if d.get(r) == Some(&0x37) && r < end {
339                            return Some((r + 1, kind));
340                        }
341                    }
342                }
343            }
344            p += 1;
345        }
346        None
347    }
348
349    fn string_attribute(&self, object: &Object, name: Option<usize>) -> Option<String> {
350        let (at, kind) = self.attribute(object, name)?;
351        if kind != 0x12 {
352            return None;
353        }
354        self.text(Some(strref(self.d, at)?.0))
355    }
356
357    /// The doubles of an array attribute: `None` when it is absent,
358    /// `Some(None)` when it is there but cannot be read.
359    fn doubles(&self, object: &Object, name: Option<usize>) -> Option<Option<Vec<f64>>> {
360        let (at, kind) = self.attribute(object, name)?;
361        let read = || -> Option<Vec<f64>> {
362            if kind != 0x07 {
363                return None;
364            }
365            let (n, mut p) = varint(self.d, at)?;
366            (_, p) = varint(self.d, p)?;
367            let mut values = Vec::new();
368            for _ in 0..n.min(64) {
369                let (v, next) = number(self.d, p)?;
370                values.push(v);
371                p = next;
372            }
373            (self.d.get(p) == Some(&0xfe)).then_some(values)
374        };
375        Some(read())
376    }
377}
378
379/// The string table `<count> { <len + 1> bytes | 00 <u32 len> bytes }`.
380fn strings(d: &[u8], start: usize, end: usize) -> Option<Vec<&[u8]>> {
381    let (count, mut p) = varint(d, start)?;
382    let mut out = Vec::new();
383    while p < end {
384        let len = *d.get(p)?;
385        let (from, to) = if len == 0 {
386            let n = u32_at(d, p + 1)? as usize;
387            (p + 5, p.checked_add(5)?.checked_add(n)?)
388        } else {
389            (p + 1, p + usize::from(len))
390        };
391        out.push(d.get(from..to.min(end))?);
392        p = to;
393    }
394    (out.len() as u64 == count).then_some(out)
395}
396
397/// A side block `<b0> <b1> [e5 <u32 L> link] <small numbers> 7c 0a { <name>
398/// <count> <0> { <id> (80 <u32> | 81) } × count } × fe`.
399fn side(d: &[u8], s: usize, e: usize, bags: Option<usize>) -> Side {
400    let mut out = Side::default();
401    let mut q = s + 2;
402    if d.get(q) == Some(&0xe5) {
403        let Some(len) = u32_at(d, q + 1) else {
404            return out;
405        };
406        let start = q + 5;
407        let end = start.saturating_add(len as usize).min(e);
408        out.link = d.get(start..end).and_then(link);
409        q = end;
410    }
411    while q < e && d.get(q..q + 2) != Some(&[0x7c, 0x0a]) {
412        match varint(d, q) {
413            Some((_, next)) => q = next,
414            None => return out,
415        }
416    }
417    let Some((mut r, end)) = section(d, q, 0x0a) else {
418        return out;
419    };
420    while r < end && d.get(r) != Some(&0xfe) {
421        let Some((name, next)) = strref(d, r) else {
422            break;
423        };
424        let Some((count, next)) = varint(d, next) else {
425            break;
426        };
427        let Some((_, mut next)) = varint(d, next) else {
428            break;
429        };
430        let mut ids = Vec::new();
431        for _ in 0..count.min(1_000_000) {
432            let Some((id, after)) = varint(d, next) else {
433                return out;
434            };
435            ids.push(id);
436            next = if d.get(after) == Some(&0x80) {
437                after + 5
438            } else {
439                after + 1
440            };
441            if next > end {
442                return out;
443            }
444        }
445        if Some(name) == bags {
446            out.bags = ids;
447        }
448        r = next;
449    }
450    out
451}
452
453/// The link of a side block: the UUID of the target container.
454fn link(blob: &[u8]) -> Option<String> {
455    if blob.get(..2)? != [0x11, 0x03] {
456        return None;
457    }
458    uuid_text(blob.get(2..18)?)
459}
460
461/// The placement of an instance from its 12 doubles (X, Y, Z axis and
462/// origin), or `None` when they are no rotation and translation.
463fn placement(values: &[f64]) -> Option<([[f64; 3]; 3], [f64; 3])> {
464    let v: &[f64; 12] = values.try_into().ok()?;
465    if !v.iter().all(|x| x.is_finite() && x.abs() < 1e9) {
466        return None;
467    }
468    let rotation = [[v[0], v[1], v[2]], [v[3], v[4], v[5]], [v[6], v[7], v[8]]];
469    // the axes are unit vectors (a mirrored component is a document of its own)
470    if rotation
471        .iter()
472        .any(|a| ((a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt() - 1.0).abs() > 1e-3)
473    {
474        return None;
475    }
476    Some((rotation, [v[9], v[10], v[11]]))
477}
478
479/// The instances of a product (the whole file): name = part number,
480/// `document` = UUID of the referenced document. `None` when it is no
481/// CATIA product.
482pub fn components(data: &[u8]) -> Option<Vec<Component>> {
483    if !data.starts_with(MAGIC) {
484        return None;
485    }
486    let mut at = 0;
487    let mut tried = 0;
488    let stream = loop {
489        let osmx = find(data, b"OSMX", at)?;
490        at = osmx + 4;
491        tried += 1;
492        if tried > MAX_STREAMS {
493            return None;
494        }
495        if let Some(s) = Stream::at(data, osmx) {
496            if s.index("ASMPRODUCT").is_some() && s.index("_InstanceName").is_some() {
497                break s;
498            }
499        }
500    };
501    let asm = stream.index("ASMPRODUCT");
502    let bag_class = stream.index("PRDBAGREP");
503    let instance_name = stream.index("_InstanceName");
504    let position = stream.index("_Position");
505    let objects = stream.objects();
506    let by_id: std::collections::HashMap<u64, usize> = objects
507        .iter()
508        .enumerate()
509        .filter_map(|(k, o)| Some((u64::from(o.id?), k)))
510        .collect();
511    let sides = stream.sides(objects.len(), stream.index("_BagRepsList"));
512    let mut out = Vec::new();
513    for (k, object) in objects.iter().enumerate() {
514        if object.class != asm || out.len() >= MAX_INSTANCES {
515            continue;
516        }
517        let Some(instance) = stream.string_attribute(object, instance_name) else {
518            continue; // the product itself
519        };
520        let side = sides.as_ref().and_then(|s| s.get(k));
521        let Some(document) = side.and_then(|s| s.link.clone()) else {
522            continue; // a component without a document of its own
523        };
524        // the placement holder: from the side table, else the next object
525        let listed = side
526            .and_then(|s| s.bags.first())
527            .and_then(|id| by_id.get(id))
528            .and_then(|&k| objects.get(k));
529        let bag = listed
530            .or_else(|| objects.get(k + 1))
531            .filter(|o| o.class == bag_class);
532        let (rotation, translation) = match bag.and_then(|b| stream.doubles(b, position)) {
533            None => (
534                [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
535                [0.0; 3],
536            ),
537            Some(values) => match values.as_deref().and_then(placement) {
538                Some(p) => p,
539                None => continue,
540            },
541        };
542        let part_number = stream.text(object.name).filter(|n| !n.trim().is_empty());
543        out.push(Component {
544            name: part_number.unwrap_or(instance),
545            rotation,
546            translation,
547            layer: None,
548            occurrence: None,
549            document: Some(document),
550        });
551    }
552    Some(out)
553}
554
555/// An instance of [`sample`]: part number, UUID of the referenced
556/// document, placement (X, Y, Z axis and origin) or `None`.
557pub type SampleInstance<'a> = (&'a str, [u8; 16], Option<[f64; 12]>);
558
559/// A product as CATIA stores it, reduced to what [`components`] reads:
560/// `own` is the product's container UUID, each instance (part number,
561/// UUID of the referenced document, placement as X, Y, Z axis and origin
562/// or `None` for none stored). For tests and the fuzz corpus.
563#[doc(hidden)]
564pub fn sample(own: [u8; 16], instances: &[SampleInstance]) -> Vec<u8> {
565    let mut strings: Vec<String> = [
566        "ASMPRODUCT",
567        "PRDBAGREP",
568        "Product1",
569        "_Component",
570        "_BagRepsList",
571        "_InstanceName",
572        "_Position",
573        "",
574    ]
575    .iter()
576    .map(|s| s.to_string())
577    .collect();
578    let mut s = |text: String| -> Vec<u8> {
579        let k = match strings.iter().position(|x| *x == text) {
580            Some(k) => k,
581            None => {
582                strings.push(text);
583                strings.len() - 1
584            }
585        };
586        strref_bytes(k)
587    };
588    let small = |n: usize| -> Vec<u8> {
589        if n < 0x4f {
590            vec![0x81 + n as u8]
591        } else {
592            let mut v = vec![0x80];
593            v.extend((n as u32).to_le_bytes());
594            v
595        }
596    };
597    let sect = |kind: u8, body: &[u8]| -> Vec<u8> {
598        let mut v = vec![0x7c, kind];
599        v.extend((body.len() as u32 + 6).to_le_bytes());
600        v.extend(body);
601        v
602    };
603    let object = |class: &[u8], name: &[u8], id: u32, attributes: &[u8]| -> Vec<u8> {
604        let mut header = vec![0x00, 0x08];
605        header.extend(class);
606        header.extend(name);
607        header.push(0xea);
608        header.extend(id.to_le_bytes());
609        let mut body = vec![0x00];
610        body.extend(sect(0x06, &header));
611        body.extend(sect(0x07, attributes));
612        sect(0x05, &body)
613    };
614    let list = |name: &[u8], ids: &[u32]| -> Vec<u8> {
615        let mut v = name.to_vec();
616        v.extend(small(ids.len()));
617        v.push(0x81);
618        for id in ids {
619            v.extend(small(*id as usize));
620            v.push(0x81);
621        }
622        v
623    };
624    let (asm, bag, product, component, bags, instance_name, position, empty) = (
625        s("ASMPRODUCT".into()),
626        s("PRDBAGREP".into()),
627        s("Product1".into()),
628        s("_Component".into()),
629        s("_BagRepsList".into()),
630        s("_InstanceName".into()),
631        s("_Position".into()),
632        s(String::new()),
633    );
634    // root (id 2), then per instance k: the instance (id 10 + 2k) and its bag
635    let mut objects = object(&asm, &product, 2, &[0xfe]);
636    let ids: Vec<u32> = (0..instances.len() as u32).map(|k| 10 + 2 * k).collect();
637    let mut root_lists = list(&component, &ids);
638    root_lists.push(0xfe);
639    let mut sides = sect(
640        0x09,
641        &[[0x14, 0x01, 0x82].as_slice(), &sect(0x0a, &root_lists)].concat(),
642    );
643    for (k, (part_number, document, at)) in instances.iter().enumerate() {
644        let id = ids.get(k).copied().unwrap_or_default();
645        let mut attributes = instance_name.clone();
646        attributes.extend([0x82, 0xe8, 0xc0, 0x12, 0x37]);
647        attributes.extend(s(format!("{part_number}.{}", k + 1)));
648        attributes.extend([0xfe, 0xfe]);
649        objects.extend(object(&asm, &s(part_number.to_string()), id, &attributes));
650        let mut placed = Vec::new();
651        if let Some(values) = at {
652            placed.extend(&position);
653            placed.extend([0x82, 0xe8, 0xf4, 0x07, 0x37, 0x8d, 0x81]);
654            for v in values {
655                placed.push(0xe6);
656                placed.extend(v.to_le_bytes());
657            }
658            placed.push(0xfe);
659        }
660        placed.push(0xfe);
661        objects.extend(object(&bag, &empty, id + 1, &placed));
662        let mut target = vec![0x11, 0x03];
663        target.extend(document);
664        target.extend([7, 0, 0, 0, 5, 0, 9]);
665        target.extend(2u32.to_le_bytes());
666        target.push(0);
667        target.extend([0x0c, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0]);
668        let mut block = vec![0x14, 0x00, 0xe5];
669        block.extend((target.len() as u32).to_le_bytes());
670        block.extend(target);
671        block.push(0x86);
672        let mut lists = list(&bags, &[id + 1]);
673        lists.push(0xfe);
674        block.extend(sect(0x0a, &lists));
675        sides.extend(sect(0x09, &block));
676        sides.extend(sect(
677            0x09,
678            &[0x14, 0x01, 0x82, 0x7c, 0x0a, 7, 0, 0, 0, 0xfe],
679        ));
680    }
681    let mut table = small(strings.len());
682    for text in &strings {
683        table.push(text.len() as u8 + 1);
684        table.extend(text.as_bytes());
685    }
686    let mut out = MAGIC.to_vec();
687    out.resize(0x800, 0);
688    out.extend(CONTAINER_ENTRY);
689    out.extend(4u32.to_le_bytes());
690    out.extend(b"CATProdCont\0CATFeatCont\0");
691    out.extend(UUID_TAG);
692    out.extend(own);
693    out.resize(0x1000, 0);
694    out.extend(b"OSMX");
695    out.extend([0xa4, 0x82, 0x00]);
696    out.extend(sect(0x0c, &[]));
697    objects.push(0x00);
698    out.extend(sect(0x04, &objects));
699    out.extend(sect(0x08, &sides));
700    out.extend(sect(0x0b, &[]));
701    out.extend(sect(0x02, &table));
702    out
703}
704
705#[cfg(test)]
706mod tests {
707    use super::*;
708
709    const OWN: [u8; 16] = [
710        0xbd, 0x77, 0xb6, 0xa4, 0, 0, 0x1b, 0xa4, 0x64, 0xce, 0x5d, 0x6a, 0, 0, 6, 0x24,
711    ];
712    const PART: [u8; 16] = [
713        0xbd, 0x77, 0xb6, 0xa4, 0, 0, 0x1b, 0xa4, 0x64, 0xce, 0x51, 0xd8, 0, 0, 0, 0x89,
714    ];
715
716    #[test]
717    fn uuids_read_like_catia_names_its_containers() {
718        assert_eq!(uuid_text(&OWN).unwrap(), "bd77b6a4_1ba4_64ce5d6a_624");
719        let product = sample(OWN, &[]);
720        assert_eq!(document(&product).unwrap(), "bd77b6a4_1ba4_64ce5d6a_624");
721        assert_eq!(document(b"not a CATIA file"), None);
722    }
723
724    #[test]
725    fn instances_with_their_documents_and_placements() {
726        // beam: X axis (0, -1, 0), Y axis (1, 0, 0), Z axis (0, 0, 1)
727        let beam = [
728            0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, -0.3, -480.0, 718.0,
729        ];
730        let product = sample(OWN, &[("frame", PART, None), ("beam", PART, Some(beam))]);
731        let c = components(&product).unwrap();
732        assert_eq!(c.len(), 2);
733        assert_eq!(c[0].name, "frame");
734        assert_eq!(c[0].document.as_deref(), Some("bd77b6a4_1ba4_64ce51d8_89"));
735        assert_eq!(c[0].place([1.0, 2.0, 3.0]), [1.0, 2.0, 3.0]);
736        assert_eq!(c[1].name, "beam");
737        // local x runs along the stored X axis: world = x X + y Y + z Z + origin
738        assert_eq!(c[1].place([1.0, 0.0, 0.0]), [-0.3, -481.0, 718.0]);
739        assert_eq!(c[1].place([0.0, 1.0, 0.0]), [0.7, -480.0, 718.0]);
740    }
741
742    #[test]
743    fn a_broken_placement_leaves_the_instance_out() {
744        let mut skew = [0.0; 12];
745        skew[0] = 2.0;
746        let product = sample(OWN, &[("a", PART, Some(skew)), ("b", PART, None)]);
747        let c = components(&product).unwrap();
748        assert_eq!(c.iter().map(|c| c.name.as_str()).collect::<Vec<_>>(), ["b"]);
749    }
750
751    #[test]
752    fn long_string_references_are_read() {
753        assert_eq!(strref(&strref_bytes(5), 0), Some((5, 1)));
754        assert_eq!(strref(&strref_bytes(500), 0), Some((500, 5)));
755        assert_eq!(varint(&[0xd2, 0x3a], 0), Some((314, 2)));
756        assert_eq!(varint(&[0x8d], 0), Some((12, 1)));
757    }
758
759    #[test]
760    fn damaged_products_never_panic() {
761        let beam = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 5.0, 6.0, 7.0];
762        let product = sample(OWN, &[("frame", PART, None), ("beam", PART, Some(beam))]);
763        // every byte that is not padding: the container entry (0x800) and
764        // the object stream (0x1000 on); every third byte stays fast
765        let entry = 0x800..0x800 + 64;
766        let stream = 0x1000..product.len();
767        for at in entry.chain(stream.step_by(3)) {
768            let mut copy = product.clone();
769            copy[at] ^= 0x5a;
770            let _ = components(&copy);
771            let _ = document(&copy);
772        }
773        for cut in (0x1000..product.len()).step_by(7) {
774            let _ = components(&product[..cut]);
775        }
776    }
777}