Skip to main content

teamcraft/
invpart.rs

1//! Autodesk Inventor files (`.ipt`, `.iam`): the segments below the OLE
2//! storage `RSeStorage` (see `docs/src/research/inventor-display-preview.md`).
3//!
4//! Every segment has a meta stream `M<id>` and a data stream `B<id>`:
5//!
6//! ```text
7//! M<id>: u32 25 "RSe Meta Stream Version 8" | u16 | 16 bytes
8//!        | u32 n, n × UTF-16 segment name | class UUID | 3 × u32
9//!        | u32 n, ASCII date | u32 n, ASCII date | u8 | zlib/zstd body:
10//!          7 × u16 | u32 sections, sections × u32 (bit 31 + length) | u32
11//!          | u32 n, n × 10 bytes | u32 | u32 n, n × 28 bytes | u32
12//!          | u32 classes, classes × (UUID, u16, u32, u16, u32)
13//! B<id>: 18 bytes | zlib/zstd body: per section
14//!          u32 class word (low byte: class table index) | payload
15//!          | u32 payload length again | u8 0
16//! ```
17//!
18//! The bodies are zlib (Inventor 2019–2024 as tested) or Zstandard (newer
19//! files, issue #271), told apart by their magic bytes. Where the header of a
20//! Zstandard stream ends is not known from real files: the frame is taken at
21//! the zlib offset, else at the first Zstandard magic close to it.
22//!
23//! Classes are named by the first 32 bits of their UUID (`time_low`). Every
24//! length is checked against the data before it is used, unpacked streams
25//! are capped, and a damaged file is an error, never a panic.
26#![deny(
27    clippy::unwrap_used,
28    clippy::expect_used,
29    clippy::indexing_slicing,
30    clippy::panic,
31    clippy::unreachable
32)]
33
34use std::io::{self, Read, Seek};
35
36use flate2::read::ZlibDecoder;
37
38/// Largest meta or data stream read (compressed).
39const MAX_RAW: u64 = 256 * 1024 * 1024;
40/// Bytes before the zlib body of a data stream (Zstandard: see `unpack`).
41const DATA_HEADER: usize = 18;
42/// Magic number of a Zstandard frame (RFC 8878).
43const ZSTD_MAGIC: [u8; 4] = [0x28, 0xB5, 0x2F, 0xFD];
44/// How far before or after the zlib offset a Zstandard frame is looked for.
45const ZSTD_SLACK: usize = 64;
46/// Largest Zstandard window a frame may ask for: the decoder allocates it up
47/// front, so it is also capped by the output limit (at least 1 MiB).
48const MAX_ZSTD_WINDOW: u64 = 32 * 1024 * 1024;
49/// Most Zstandard frames read one after the other from one stream.
50const MAX_ZSTD_FRAMES: usize = 64;
51/// Marker of a typed array.
52pub const ARRAY: u32 = 0x3000_0002;
53/// Element type of `u32` arrays.
54pub const ELEM_U32: u32 = 0;
55/// Element type of key arrays (`u32` too).
56pub const ELEM_KEYS: u32 = 0x1000_0000;
57
58fn bad(what: &str) -> io::Error {
59    io::Error::new(io::ErrorKind::InvalidData, format!("Inventor file: {what}"))
60}
61
62pub(crate) fn u16_at(data: &[u8], at: usize) -> Option<u16> {
63    Some(u16::from_le_bytes(
64        data.get(at..at.checked_add(2)?)?.try_into().ok()?,
65    ))
66}
67
68pub(crate) fn u32_at(data: &[u8], at: usize) -> Option<u32> {
69    Some(u32::from_le_bytes(
70        data.get(at..at.checked_add(4)?)?.try_into().ok()?,
71    ))
72}
73
74pub(crate) fn f64_at(data: &[u8], at: usize) -> Option<f64> {
75    Some(f64::from_le_bytes(
76        data.get(at..at.checked_add(8)?)?.try_into().ok()?,
77    ))
78}
79
80fn usize_at(data: &[u8], at: usize) -> Option<usize> {
81    usize::try_from(u32_at(data, at)?).ok()
82}
83
84/// One segment: its section lengths, its class table and the inflated data.
85#[derive(Debug)]
86pub struct Segment {
87    /// Payload length of every section, in order.
88    lengths: Vec<usize>,
89    /// Class (`time_low` of the UUID) per class-table index.
90    pub classes: Vec<u32>,
91    data: Vec<u8>,
92}
93
94/// One section of a segment.
95#[derive(Clone, Copy, Debug)]
96pub struct Section<'a> {
97    /// Position in the segment.
98    pub index: usize,
99    /// Class (`time_low` of the UUID); 0 when the index is outside the table.
100    pub class: u32,
101    pub payload: &'a [u8],
102}
103
104impl Segment {
105    /// The sections in order; `Err` at the first section whose framing does
106    /// not check out.
107    pub fn sections(&self) -> Sections<'_> {
108        Sections {
109            segment: self,
110            index: 0,
111            at: 0,
112        }
113    }
114
115    /// Element size of an array element type (`None`: unknown class).
116    pub fn element_size(&self, elem: u32) -> Option<usize> {
117        match elem {
118            ELEM_U32 | ELEM_KEYS => Some(4),
119            e if e & 0xFFFF_FF00 == 0x100 => {
120                let class = *self.classes.get(usize::try_from(e & 0xFF).ok()?)?;
121                match class {
122                    POINT3F | VECTOR3F => Some(12),
123                    POINT2F => Some(8),
124                    _ => None,
125                }
126            }
127            _ => None,
128        }
129    }
130
131    /// The class of an array element type (`ELEM_U32`/`ELEM_KEYS` as is).
132    pub fn element_class(&self, elem: u32) -> Option<u32> {
133        if elem & 0xFFFF_FF00 == 0x100 {
134            self.classes
135                .get(usize::try_from(elem & 0xFF).ok()?)
136                .copied()
137        } else {
138            Some(elem)
139        }
140    }
141}
142
143/// Array element class: `f32` point (x, y, z).
144pub const POINT3F: u32 = 0xF272_10C1;
145/// Array element class: `f32` unit vector.
146pub const VECTOR3F: u32 = 0x8D9D_0B25;
147/// Array element class: `f32` 2D point.
148pub const POINT2F: u32 = 0x68FC_ABB1;
149
150/// Iterator over the sections of a [`Segment`].
151pub struct Sections<'a> {
152    segment: &'a Segment,
153    index: usize,
154    at: usize,
155}
156
157impl<'a> Iterator for Sections<'a> {
158    type Item = io::Result<Section<'a>>;
159
160    fn next(&mut self) -> Option<Self::Item> {
161        let len = *self.segment.lengths.get(self.index)?;
162        let data = &self.segment.data;
163        let result = (|| {
164            let word = u32_at(data, self.at).ok_or_else(|| bad("section cut short"))?;
165            let start = self.at + 4;
166            let end = start
167                .checked_add(len)
168                .filter(|&e| e <= data.len())
169                .ok_or_else(|| bad("section cut short"))?;
170            if usize_at(data, end) != Some(len) {
171                return Err(bad("section length check failed"));
172            }
173            // a flag byte follows every section; 0 in every file seen
174            match data.get(end + 4) {
175                None | Some(0) => {}
176                Some(_) => return Err(bad("unsupported section trailer")),
177            }
178            let class = usize::try_from(word & 0xFF)
179                .ok()
180                .and_then(|k| self.segment.classes.get(k).copied())
181                .unwrap_or(0);
182            let section = Section {
183                index: self.index,
184                class,
185                payload: data.get(start..end).unwrap_or_default(),
186            };
187            self.at = end + 5;
188            Ok(section)
189        })();
190        self.index += 1;
191        if result.is_err() {
192            // stop after the first damaged section
193            self.index = usize::MAX;
194        }
195        Some(result)
196    }
197}
198
199/// Reads typed values from a section payload.
200pub struct Cursor<'a> {
201    pub data: &'a [u8],
202    pub at: usize,
203}
204
205/// A typed array: its element type and raw elements.
206#[derive(Clone, Copy, Debug)]
207pub struct Array<'a> {
208    pub elem: u32,
209    pub count: usize,
210    pub bytes: &'a [u8],
211}
212
213impl<'a> Cursor<'a> {
214    pub fn new(data: &'a [u8], at: usize) -> Cursor<'a> {
215        Cursor { data, at }
216    }
217
218    pub fn u32(&mut self) -> Option<u32> {
219        let v = u32_at(self.data, self.at)?;
220        self.at += 4;
221        Some(v)
222    }
223
224    /// The array at the cursor; element sizes come from the segment's
225    /// class table, so unknown element classes end the read.
226    pub fn array(&mut self, segment: &Segment) -> Option<Array<'a>> {
227        if u32_at(self.data, self.at)? != ARRAY {
228            return None;
229        }
230        let count = usize_at(self.data, self.at + 4)?;
231        if count == 0 {
232            self.at += 8;
233            return Some(Array {
234                elem: ELEM_U32,
235                count: 0,
236                bytes: &[],
237            });
238        }
239        let elem = u32_at(self.data, self.at + 12)?;
240        let size = segment.element_size(elem)?;
241        let start = self.at + 16;
242        let end = start.checked_add(count.checked_mul(size)?)?;
243        let bytes = self.data.get(start..end)?;
244        self.at = end;
245        Some(Array { elem, count, bytes })
246    }
247}
248
249/// The segment named `name` (e.g. `PmGraphicsSegment`), inflating at most
250/// `limit` bytes; `Ok(None)` when the file has no such segment.
251pub fn segment<F: Read + Seek>(
252    ole: &mut cfb::CompoundFile<F>,
253    name: &str,
254    limit: u64,
255) -> io::Result<Option<Segment>> {
256    let metas: Vec<(String, std::path::PathBuf)> = match ole.read_storage("/RSeStorage") {
257        Ok(entries) => entries
258            .filter(|e| e.is_stream() && e.name().starts_with('M') && e.len() <= MAX_RAW)
259            .map(|e| {
260                let id = e.name().get(1..).unwrap_or_default().to_string();
261                (id, e.path().to_path_buf())
262            })
263            .collect(),
264        Err(_) => return Ok(None),
265    };
266    for (id, path) in metas {
267        let raw = read_stream(ole, &path, MAX_RAW)?;
268        let Some((seg_name, body_at)) = meta_head(&raw) else {
269            continue;
270        };
271        if seg_name != name {
272            continue;
273        }
274        let body = unpack(&raw, body_at, limit)?;
275        let (lengths, classes) = meta_body(&body).ok_or_else(|| bad("damaged meta stream"))?;
276        let data_path = std::path::PathBuf::from(format!("/RSeStorage/B{id}"));
277        let raw = read_stream(ole, &data_path, MAX_RAW)?;
278        let data = unpack(&raw, DATA_HEADER, limit)?;
279        return Ok(Some(Segment {
280            lengths,
281            classes,
282            data,
283        }));
284    }
285    Ok(None)
286}
287
288/// A whole stream of the compound file, at most `limit` bytes.
289pub fn read_stream<F: Read + Seek>(
290    ole: &mut cfb::CompoundFile<F>,
291    path: &std::path::Path,
292    limit: u64,
293) -> io::Result<Vec<u8>> {
294    let stream = ole.open_stream(path)?;
295    if stream.len() > limit {
296        return Err(bad("stream too large"));
297    }
298    let mut out = Vec::new();
299    stream.take(limit).read_to_end(&mut out)?;
300    Ok(out)
301}
302
303/// The compressed body of a meta or data stream whose zlib body starts at
304/// `at`: Zstandard when a frame starts there, zlib when a zlib header does,
305/// else the first Zstandard frame within [`ZSTD_SLACK`] bytes of `at`.
306fn unpack(raw: &[u8], at: usize, limit: u64) -> io::Result<Vec<u8>> {
307    let body = raw.get(at..).unwrap_or_default();
308    if body.starts_with(&ZSTD_MAGIC) {
309        return unzstd(body, limit);
310    }
311    if !zlib_header(body) {
312        let from = at.saturating_sub(ZSTD_SLACK);
313        let to = at
314            .saturating_add(ZSTD_SLACK + ZSTD_MAGIC.len())
315            .min(raw.len());
316        if let Some(near) = raw.get(from..to).and_then(|w| {
317            w.windows(ZSTD_MAGIC.len())
318                .position(|m| m == ZSTD_MAGIC)
319                .and_then(|k| raw.get(from + k..))
320        }) {
321            return unzstd(near, limit);
322        }
323    }
324    inflate(body, limit)
325}
326
327/// A zlib header: Deflate with a window of at most 32 KiB and a check sum
328/// that divides by 31 (RFC 1950).
329fn zlib_header(data: &[u8]) -> bool {
330    match data {
331        [cmf, flg, ..] => {
332            cmf & 0x0F == 8 && cmf >> 4 <= 7 && (u16::from(*cmf) << 8 | u16::from(*flg)) % 31 == 0
333        }
334        _ => false,
335    }
336}
337
338fn inflate(data: &[u8], limit: u64) -> io::Result<Vec<u8>> {
339    let mut out = Vec::new();
340    ZlibDecoder::new(data)
341        .take(limit.saturating_add(1))
342        .read_to_end(&mut out)
343        .map_err(|_| bad("damaged compressed data"))?;
344    if out.len() as u64 > limit {
345        return Err(bad("segment larger than the preview reads"));
346    }
347    Ok(out)
348}
349
350/// Zstandard frames, one after the other while the next one starts right
351/// behind; the output is capped like the zlib output.
352fn unzstd(data: &[u8], limit: u64) -> io::Result<Vec<u8>> {
353    let mut out = Vec::new();
354    let mut rest = data;
355    for _ in 0..MAX_ZSTD_FRAMES {
356        if !rest.starts_with(&ZSTD_MAGIC) {
357            break;
358        }
359        let left = limit.saturating_add(1).saturating_sub(out.len() as u64);
360        let mut decoder = ruzstd::decoding::StreamingDecoder::new_with_max_window_size(
361            &mut rest,
362            MAX_ZSTD_WINDOW.min(limit.max(1 << 20)),
363        )
364        .map_err(|_| bad("damaged compressed data"))?;
365        (&mut decoder)
366            .take(left)
367            .read_to_end(&mut out)
368            .map_err(|_| bad("damaged compressed data"))?;
369        if out.len() as u64 > limit {
370            return Err(bad("segment larger than the preview reads"));
371        }
372        if !decoder.decoder.is_finished() {
373            // the frame stopped before its end: no further frame can follow
374            break;
375        }
376    }
377    Ok(out)
378}
379
380/// The segment name and where the zlib body starts.
381fn meta_head(raw: &[u8]) -> Option<(String, usize)> {
382    const VERSION: &[u8] = b"RSe Meta Stream Version ";
383    let n = usize_at(raw, 0)?;
384    if !raw.get(4..4 + n)?.starts_with(VERSION) {
385        return None;
386    }
387    // u16 version, 16 bytes
388    let mut at = 4 + n + 2 + 16;
389    let chars = usize_at(raw, at)?;
390    if chars > 256 {
391        return None;
392    }
393    at += 4;
394    let units: Vec<u16> = raw
395        .get(at..at + 2 * chars)?
396        .chunks_exact(2)
397        .filter_map(|c| c.try_into().ok().map(u16::from_le_bytes))
398        .collect();
399    let name = String::from_utf16(&units).ok()?;
400    at += 2 * chars + 16 + 12;
401    for _ in 0..2 {
402        let len = usize_at(raw, at)?;
403        if len > 256 {
404            return None;
405        }
406        at += 4 + len;
407    }
408    Some((name, at + 1))
409}
410
411/// Section lengths and class table of an inflated meta body.
412fn meta_body(body: &[u8]) -> Option<(Vec<usize>, Vec<u32>)> {
413    let mut at = 14;
414    let n = usize_at(body, at)?;
415    at += 4;
416    if n.checked_mul(4)? > body.len() {
417        return None;
418    }
419    let mut lengths = Vec::with_capacity(n);
420    for k in 0..n {
421        let word = u32_at(body, at + 4 * k)?;
422        lengths.push(usize::try_from(word & 0x7FFF_FFFF).ok()?);
423    }
424    at += 4 * n + 4;
425    for size in [10usize, 28] {
426        let count = usize_at(body, at)?;
427        at = at.checked_add(4)?.checked_add(count.checked_mul(size)?)?;
428        at = at.checked_add(4)?;
429        if at > body.len() {
430            return None;
431        }
432    }
433    let count = usize_at(body, at)?;
434    at += 4;
435    if count > 256 || at.checked_add(count.checked_mul(28)?)? > body.len() {
436        return None;
437    }
438    let classes = (0..count)
439        .map(|k| u32_at(body, at + 28 * k))
440        .collect::<Option<Vec<u32>>>()?;
441    Some((lengths, classes))
442}
443
444#[cfg(test)]
445pub(crate) mod tests {
446    use super::*;
447
448    /// A segment to write: (name, classes, sections as (class index, payload)).
449    pub(crate) type SegmentSpec<'a> = (&'a str, &'a [u32], Vec<(u8, Vec<u8>)>);
450
451    /// Writes a minimal Inventor-like compound file with the given segments:
452    /// (name, classes, sections as (class index, payload)).
453    pub(crate) fn file(segments: &[SegmentSpec], extra: &[(&str, Vec<u8>)]) -> Vec<u8> {
454        use std::io::Write;
455        let mut ole = cfb::CompoundFile::create(std::io::Cursor::new(Vec::new())).unwrap();
456        ole.create_storage("/RSeStorage").unwrap();
457        for (k, (name, classes, sections)) in segments.iter().enumerate() {
458            let mut meta = Vec::new();
459            let version = b"RSe Meta Stream Version 8";
460            meta.extend((version.len() as u32).to_le_bytes());
461            meta.extend(version);
462            meta.extend([8, 0]);
463            meta.extend([0; 16]);
464            let units: Vec<u16> = name.encode_utf16().collect();
465            meta.extend((units.len() as u32).to_le_bytes());
466            for u in units {
467                meta.extend(u.to_le_bytes());
468            }
469            meta.extend([0; 16 + 12]);
470            for _ in 0..2 {
471                meta.extend(19u32.to_le_bytes());
472                meta.extend(b"01/01/2026 00:00:00");
473            }
474            meta.push(1);
475            let mut body = vec![0u8; 14];
476            body.extend((sections.len() as u32).to_le_bytes());
477            for (_, p) in sections {
478                body.extend((0x8000_0000 | p.len() as u32).to_le_bytes());
479            }
480            body.extend((4 * sections.len() as u32 + 4).to_le_bytes());
481            for _ in 0..2 {
482                body.extend(0u32.to_le_bytes());
483                body.extend(4u32.to_le_bytes());
484            }
485            body.extend((classes.len() as u32).to_le_bytes());
486            for c in classes.iter() {
487                body.extend(c.to_le_bytes());
488                body.extend([0; 24]);
489            }
490            meta.extend(zlib(&body));
491            let mut data = Vec::new();
492            for (class, p) in sections {
493                data.extend([*class, 1, 0, 0]);
494                data.extend(p);
495                data.extend((p.len() as u32).to_le_bytes());
496                data.push(0);
497            }
498            let mut stream = vec![0u8; DATA_HEADER];
499            stream.extend(zlib(&data));
500            ole.create_stream(format!("/RSeStorage/Mseg{k}"))
501                .unwrap()
502                .write_all(&meta)
503                .unwrap();
504            ole.create_stream(format!("/RSeStorage/Bseg{k}"))
505                .unwrap()
506                .write_all(&stream)
507                .unwrap();
508        }
509        for (name, bytes) in extra {
510            ole.create_stream(name).unwrap().write_all(bytes).unwrap();
511        }
512        ole.flush().unwrap();
513        ole.into_inner().into_inner()
514    }
515
516    pub(crate) fn zlib(data: &[u8]) -> Vec<u8> {
517        use std::io::Write;
518        let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast());
519        z.write_all(data).unwrap();
520        z.finish().unwrap()
521    }
522
523    pub(crate) fn zstd(data: &[u8]) -> Vec<u8> {
524        ruzstd::encoding::compress_to_vec(data, ruzstd::encoding::CompressionLevel::Fastest)
525    }
526
527    /// The same Inventor file with every `RSeStorage` segment repacked as
528    /// Zstandard (as issue #271 reports for newer files), the frame `shift`
529    /// bytes behind (or before) where the zlib body started.
530    pub(crate) fn as_zstd(bytes: &[u8], shift: isize) -> Vec<u8> {
531        use std::io::Write;
532        let mut ole = cfb::CompoundFile::open(std::io::Cursor::new(bytes.to_vec())).unwrap();
533        let streams: Vec<(String, std::path::PathBuf)> = ole
534            .read_storage("/RSeStorage")
535            .unwrap()
536            .filter(|e| e.is_stream())
537            .map(|e| (e.name().to_string(), e.path().to_path_buf()))
538            .collect();
539        for (name, path) in streams {
540            let at = match name.chars().next() {
541                Some('M') => {
542                    meta_head(&read_stream(&mut ole, &path, MAX_RAW).unwrap())
543                        .unwrap()
544                        .1
545                }
546                Some('B') => DATA_HEADER,
547                _ => continue,
548            };
549            let raw = read_stream(&mut ole, &path, MAX_RAW).unwrap();
550            let body = unpack(&raw, at, MAX_RAW).unwrap();
551            let mut out = raw[..at.saturating_add_signed(shift.min(0))].to_vec();
552            out.extend(vec![0u8; shift.max(0) as usize]);
553            out.extend(zstd(&body));
554            ole.create_stream(&path).unwrap().write_all(&out).unwrap();
555        }
556        ole.flush().unwrap();
557        ole.into_inner().into_inner()
558    }
559
560    /// A typed array as Inventor writes it.
561    pub(crate) fn array(elem: u32, count: usize, bytes: &[u8]) -> Vec<u8> {
562        let mut out = ARRAY.to_le_bytes().to_vec();
563        out.extend((count as u32).to_le_bytes());
564        if count > 0 {
565            out.extend((count as u32).to_le_bytes());
566            out.extend(elem.to_le_bytes());
567            out.extend(bytes);
568        }
569        out
570    }
571
572    fn open(bytes: Vec<u8>) -> cfb::CompoundFile<std::io::Cursor<Vec<u8>>> {
573        cfb::CompoundFile::open(std::io::Cursor::new(bytes)).unwrap()
574    }
575
576    #[test]
577    fn segments_are_found_by_name_and_walked() {
578        let bytes = file(
579            &[
580                ("PmAppSegment", &[1], vec![(0, vec![9; 3])]),
581                (
582                    "PmGraphicsSegment",
583                    &[0xAAAA_0001, POINT3F],
584                    vec![(1, vec![1, 2]), (0, array(0x101, 1, &[0; 12]))],
585                ),
586            ],
587            &[],
588        );
589        let mut ole = open(bytes);
590        let seg = segment(&mut ole, "PmGraphicsSegment", 1 << 20)
591            .unwrap()
592            .unwrap();
593        let sections: Vec<Section> = seg.sections().collect::<io::Result<_>>().unwrap();
594        assert_eq!(sections.len(), 2);
595        assert_eq!(sections[0].class, POINT3F);
596        assert_eq!(sections[0].payload, [1, 2]);
597        assert_eq!(sections[1].class, 0xAAAA_0001);
598        let a = Cursor::new(sections[1].payload, 0).array(&seg).unwrap();
599        assert_eq!((a.count, a.bytes.len()), (1, 12));
600        assert_eq!(seg.element_class(a.elem), Some(POINT3F));
601        assert!(segment(&mut ole, "AmRxSegment", 1 << 20).unwrap().is_none());
602    }
603
604    #[test]
605    fn damaged_segments_are_errors_not_panics() {
606        let bytes = file(
607            &[(
608                "PmGraphicsSegment",
609                &[1],
610                vec![(0, vec![7; 40]), (0, vec![8; 4])],
611            )],
612            &[],
613        );
614        // every truncation of the whole file
615        for cut in (0..bytes.len()).step_by(97) {
616            if let Ok(mut ole) =
617                cfb::CompoundFile::open(std::io::Cursor::new(bytes[..cut].to_vec()))
618            {
619                if let Ok(Some(seg)) = segment(&mut ole, "PmGraphicsSegment", 1 << 20) {
620                    for s in seg.sections() {
621                        let _ = s;
622                    }
623                }
624            }
625        }
626        // a wrong length check stops the walk with an error
627        let seg = Segment {
628            lengths: vec![2, 2],
629            classes: vec![1],
630            data: vec![0, 1, 0, 0, 5, 5, 3, 0, 0, 0, 0],
631        };
632        let all: Vec<_> = seg.sections().collect();
633        assert_eq!(all.len(), 1);
634        assert!(all[0].is_err());
635        // an inflate limit
636        let mut ole = open(file(
637            &[("PmGraphicsSegment", &[1], vec![(0, vec![0; 5000])])],
638            &[],
639        ));
640        assert!(segment(&mut ole, "PmGraphicsSegment", 100).is_err());
641    }
642
643    /// The segments of the walk test above, written with zlib.
644    fn two_segments() -> Vec<u8> {
645        file(
646            &[
647                ("PmAppSegment", &[1], vec![(0, vec![9; 3])]),
648                (
649                    "PmGraphicsSegment",
650                    &[0xAAAA_0001, POINT3F],
651                    vec![(1, vec![1, 2]), (0, array(0x101, 1, &[0; 12]))],
652                ),
653            ],
654            &[],
655        )
656    }
657
658    fn sections_of(bytes: Vec<u8>) -> io::Result<Vec<(u32, Vec<u8>)>> {
659        let mut ole = open(bytes);
660        let seg =
661            segment(&mut ole, "PmGraphicsSegment", 1 << 20)?.ok_or_else(|| bad("no segment"))?;
662        seg.sections()
663            .map(|s| s.map(|s| (s.class, s.payload.to_vec())))
664            .collect()
665    }
666
667    #[test]
668    fn zstandard_segments_read_like_zlib_ones() {
669        let zlib = sections_of(two_segments()).unwrap();
670        assert_eq!(zlib.len(), 2);
671        // the frame where the zlib body was, and a few bytes off either way
672        for shift in [0, 1, 4, 16, -2, -1] {
673            let packed = as_zstd(&two_segments(), shift);
674            assert_eq!(sections_of(packed).unwrap(), zlib, "shift {shift}");
675        }
676    }
677
678    #[test]
679    fn zstandard_bodies_are_told_apart_from_zlib_by_their_magic() {
680        let data = b"PmGraphicsSegment ".repeat(50);
681        assert_eq!(unpack(&zlib(&data), 0, 1 << 20).unwrap(), data);
682        assert_eq!(unpack(&zstd(&data), 0, 1 << 20).unwrap(), data);
683        // frames one after the other
684        let mut two = zstd(&data[..300]);
685        two.extend(zstd(&data[300..]));
686        assert_eq!(unpack(&two, 0, 1 << 20).unwrap(), data);
687        // a header byte before the frame (the u8 of the meta stream)
688        let mut late = vec![2u8];
689        late.extend(zstd(&data));
690        assert_eq!(unpack(&late, 0, 1 << 20).unwrap(), data);
691        // a frame too far behind the expected start is not searched for
692        let mut far = vec![0u8; ZSTD_SLACK + 1];
693        far.extend(zstd(&data));
694        assert!(unpack(&far, 0, 1 << 20).is_err());
695        assert!(zlib_header(&zlib(&data)) && !zlib_header(&zstd(&data)));
696    }
697
698    /// The NIST parts with every segment repacked as Zstandard: each
699    /// segment unpacks to the same bytes as from the zlib original (the
700    /// preview is built from these bytes alone).
701    #[test]
702    fn nist_parts_repacked_as_zstandard_unpack_to_the_same_segments() {
703        let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/cad/nist");
704        let zlib = std::fs::read(format!(
705            "{dir}/ftc-11/inventor-2021/INV_nist_ftc_11_asme1_2021.ipt"
706        ))
707        .unwrap();
708        let zstd = as_zstd(&zlib, 0);
709        let (mut a, mut b) = (open(zlib), open(zstd));
710        for name in ["PmGraphicsSegment", "PmAppSegment", "PmBRepSegment"] {
711            let x = segment(&mut a, name, MAX_RAW).unwrap();
712            let y = segment(&mut b, name, MAX_RAW).unwrap();
713            let (x, y) = (x.expect(name), y.expect(name));
714            assert_eq!(x.lengths, y.lengths, "{name}");
715            assert_eq!(x.classes, y.classes, "{name}");
716            assert!(x.data == y.data, "{name}: data differs");
717        }
718    }
719
720    #[test]
721    fn damaged_zstandard_segments_are_errors_not_panics() {
722        let data: Vec<u8> = (0..2_000u32)
723            .flat_map(|k| (k % 251).to_le_bytes())
724            .collect();
725        let packed = zstd(&data);
726        // the output limit holds for Zstandard too
727        assert!(unpack(&packed, 0, 1000).is_err());
728        assert_eq!(unpack(&packed, 0, data.len() as u64).unwrap(), data);
729        // every truncation and bytes flipped all over the frame
730        for cut in 0..packed.len() {
731            let _ = unpack(&packed[..cut], 0, 1 << 20);
732        }
733        for at in 0..packed.len() {
734            let mut bad = packed.clone();
735            bad[at] ^= 0x5A;
736            let _ = unpack(&bad, 0, 1 << 20);
737        }
738        // a frame header asking for a huge window
739        let mut huge = ZSTD_MAGIC.to_vec();
740        huge.extend([0x00, 0xF8, 0, 0, 0, 0]);
741        assert!(unpack(&huge, 0, 1 << 30).is_err());
742        // the whole file, cut and corrupted
743        let bytes = as_zstd(&two_segments(), 0);
744        for cut in (0..bytes.len()).step_by(61) {
745            if let Ok(mut ole) =
746                cfb::CompoundFile::open(std::io::Cursor::new(bytes[..cut].to_vec()))
747            {
748                if let Ok(Some(seg)) = segment(&mut ole, "PmGraphicsSegment", 1 << 20) {
749                    for s in seg.sections() {
750                        let _ = s;
751                    }
752                }
753            }
754        }
755    }
756
757    #[test]
758    fn arrays_with_unknown_elements_or_short_data_end_the_read() {
759        let seg = Segment {
760            lengths: vec![],
761            classes: vec![POINT3F, 0x1234_5678],
762            data: vec![],
763        };
764        let ok = array(0x100, 2, &[0; 24]);
765        assert_eq!(Cursor::new(&ok, 0).array(&seg).unwrap().count, 2);
766        assert!(Cursor::new(&ok[..30], 0).array(&seg).is_none());
767        assert!(Cursor::new(&array(0x101, 1, &[0; 12]), 0)
768            .array(&seg)
769            .is_none());
770        let mut huge = array(0x100, 1, &[0; 12]);
771        huge[4..8].copy_from_slice(&u32::MAX.to_le_bytes());
772        assert!(Cursor::new(&huge, 0).array(&seg).is_none());
773        let empty = array(0, 0, &[]);
774        let mut c = Cursor::new(&empty, 0);
775        assert_eq!(c.array(&seg).unwrap().count, 0);
776        assert_eq!(c.at, 8);
777    }
778}