teamcraft/mesh/creo/
container.rs1use anyhow::{anyhow, bail, Result};
21
22const MAX_SECTION: usize = 64 * 1024 * 1024;
24const MAX_ENTRIES: usize = 100_000;
26
27#[derive(Debug, Clone)]
29pub struct Entry {
30 pub name: String,
31 pub offset: usize,
33 pub size: usize,
35 pub unpacked: usize,
37 pub kind: String,
39}
40
41pub fn is_creo(data: &[u8]) -> bool {
43 data.starts_with(b"#UGC:")
44}
45
46pub fn toc(data: &[u8]) -> Result<Vec<Entry>> {
48 if !is_creo(data) {
49 bail!("not a Creo file (no #UGC header)");
50 }
51 let first_end = data
52 .iter()
53 .position(|&b| b == b'\n')
54 .ok_or_else(|| anyhow!("Creo header without line end"))?;
55 let first = std::str::from_utf8(data.get(..first_end).unwrap_or_default())
56 .map_err(|_| anyhow!("Creo header not text"))?;
57 let base = first
58 .split([' ', '\\'])
59 .rfind(|s| !s.is_empty())
60 .and_then(|s| usize::from_str_radix(s, 16).ok())
61 .ok_or_else(|| anyhow!("Creo header without base offset"))?;
62 let mut out = Vec::new();
63 let mut at =
64 find(data, b"#UGC_TOC", 0).ok_or_else(|| anyhow!("Creo file without table of contents"))?;
65 let mut seen = std::collections::HashSet::new();
66 while seen.insert(at) {
67 let line_end = find(data, b"\n", at).ok_or_else(|| anyhow!("table of contents cut off"))?;
68 let head = std::str::from_utf8(data.get(at..line_end).unwrap_or_default())
69 .map_err(|_| anyhow!("table head not text"))?;
70 let mut words = head.trim_end_matches('#').split_whitespace();
71 let _tag = words.next();
72 let _two = words.next();
73 let n: usize = words
74 .next()
75 .and_then(|s| s.parse().ok())
76 .ok_or_else(|| anyhow!("table head"))?;
77 let entry_size: usize = words
78 .next()
79 .and_then(|s| s.parse().ok())
80 .ok_or_else(|| anyhow!("table head"))?;
81 if entry_size == 0 || n > MAX_ENTRIES {
82 bail!("implausible table of contents");
83 }
84 let mut next = None;
85 let mut pos = line_end + 1;
86 for _ in 0..n {
87 let end = pos
88 .checked_add(entry_size)
89 .filter(|&e| e <= data.len())
90 .ok_or_else(|| anyhow!("table cut off"))?;
91 let rec = std::str::from_utf8(data.get(pos..end).unwrap_or_default()).unwrap_or("");
92 pos = end;
93 let mut w = rec.trim_end_matches(['#', '\n']).split_whitespace();
94 let Some(name) = w.next() else { break };
95 let hex = |s: Option<&str>| s.and_then(|s| usize::from_str_radix(s, 16).ok());
96 let (Some(off), Some(size), Some(unpacked)) =
97 (hex(w.next()), hex(w.next()), hex(w.next()))
98 else {
99 bail!("table entry '{name}' unreadable");
100 };
101 let _ver = w.next();
102 let kind = w.next().unwrap_or("").to_string();
103 let offset = base
104 .checked_add(off)
105 .ok_or_else(|| anyhow!("offset overflow"))?;
106 if name == "NEXT_TOC_ENTRY" {
107 next = Some(offset);
108 continue;
109 }
110 if out.len() >= MAX_ENTRIES {
111 bail!("too many sections");
112 }
113 out.push(Entry {
114 name: name.to_string(),
115 offset,
116 size,
117 unpacked,
118 kind,
119 });
120 }
121 let Some(next_at) = next else { break };
122 let Some(found) = find(data, b"#UGC_TOC", next_at.saturating_sub(8)) else {
124 break;
125 };
126 if found > next_at + 64 {
127 break;
128 }
129 at = found;
130 }
131 Ok(out)
132}
133
134pub fn section(data: &[u8], entries: &[Entry], name: &str) -> Result<Option<Vec<u8>>> {
137 let Some(e) = entries.iter().find(|e| e.name == name) else {
138 return Ok(None);
139 };
140 let end = e
141 .offset
142 .checked_add(e.size)
143 .filter(|&x| x <= data.len())
144 .ok_or_else(|| anyhow!("section '{name}' outside the file"))?;
145 let raw = data.get(e.offset..end).unwrap_or_default();
146 let body_start = raw
147 .iter()
148 .position(|&b| b == b'\n')
149 .map(|i| i + 1)
150 .ok_or_else(|| anyhow!("section '{name}' without name line"))?;
151 let body = raw.get(body_start..).unwrap_or_default();
152 if body.starts_with(&[0x1f, 0x9d]) {
153 if e.unpacked > MAX_SECTION {
154 bail!("section '{name}' claims {} bytes", e.unpacked);
155 }
156 return Ok(Some(unlzw(body, e.unpacked.max(1))?));
157 }
158 Ok(Some(body.to_vec()))
159}
160
161fn find(data: &[u8], needle: &[u8], from: usize) -> Option<usize> {
162 if from >= data.len() || needle.is_empty() {
163 return None;
164 }
165 data.get(from..)?
166 .windows(needle.len())
167 .position(|w| w == needle)
168 .map(|i| i + from)
169}
170
171pub fn unlzw(data: &[u8], expected: usize) -> Result<Vec<u8>> {
175 let [0x1f, 0x9d, flags, ..] = *data else {
176 bail!("not LZW data");
177 };
178 let maxbits = usize::from(flags & 0x1f);
179 if !(9..=16).contains(&maxbits) {
180 bail!("LZW with {maxbits} bits");
181 }
182 let block = flags & 0x80 != 0;
183 let limit = expected.min(MAX_SECTION);
184 let mut out: Vec<u8> = Vec::with_capacity(limit.min(1 << 20));
185 let mut prefix: Vec<u32> = (0..256).collect();
187 let mut last: Vec<u8> = (0..=255).collect();
188 let clear = if block { 1 } else { 0 };
189 for _ in 0..clear {
190 prefix.push(0);
191 last.push(0);
192 }
193 let mut nbits = 9;
194 let mut pos = 3usize;
195 let mut bitbuf: u64 = 0;
196 let mut bitcnt = 0u32;
197 let mut codes_in_group = 0usize;
198 let mut prev: Option<u32> = None;
199 let mut stack: Vec<u8> = Vec::new();
200 let skip_group = |pos: &mut usize,
201 bitbuf: &mut u64,
202 bitcnt: &mut u32,
203 codes_in_group: usize,
204 nbits: usize| {
205 let rem = (8 - codes_in_group % 8) % 8;
206 let mut skip = rem * nbits;
207 while skip > 0 {
208 if *bitcnt == 0 {
209 if *pos >= data.len() {
210 return;
211 }
212 *bitbuf = u64::from(data.get(*pos).copied().unwrap_or(0));
213 *pos += 1;
214 *bitcnt = 8;
215 }
216 let t = skip.min(*bitcnt as usize);
217 *bitbuf >>= t;
218 *bitcnt -= t as u32;
219 skip -= t;
220 }
221 };
222 loop {
223 while (bitcnt as usize) < nbits {
224 if pos >= data.len() {
225 return Ok(out);
226 }
227 bitbuf |= u64::from(data.get(pos).copied().unwrap_or(0)) << bitcnt;
228 pos += 1;
229 bitcnt += 8;
230 }
231 let code = (bitbuf & ((1u64 << nbits) - 1)) as u32;
232 bitbuf >>= nbits;
233 bitcnt -= nbits as u32;
234 codes_in_group += 1;
235 if block && code == 256 {
236 prefix.truncate(257);
237 last.truncate(257);
238 skip_group(&mut pos, &mut bitbuf, &mut bitcnt, codes_in_group, nbits);
239 nbits = 9;
240 codes_in_group = 0;
241 prev = None;
242 continue;
243 }
244 let table_len = prefix.len() as u32;
245 let (entry_code, first_byte) = match prev {
246 None => {
247 if code >= table_len {
248 bail!("LZW: bad first code");
249 }
250 (code, None)
251 }
252 Some(p) => {
253 if code < table_len {
254 (code, None)
255 } else if code == table_len {
256 (p, Some(()))
257 } else {
258 bail!("LZW: code {code} outside the table");
259 }
260 }
261 };
262 stack.clear();
264 let mut c = entry_code;
265 let mut guard = 0u32;
266 loop {
267 let (Some(&l), Some(&pre)) = (last.get(c as usize), prefix.get(c as usize)) else {
268 bail!("LZW: code {c} outside the table");
269 };
270 stack.push(l);
271 if c < 256 {
272 break;
273 }
274 c = pre;
275 guard += 1;
276 if guard > 1 << 17 {
277 bail!("LZW: dictionary loop");
278 }
279 }
280 let first = *stack.last().unwrap_or(&0);
281 if out.len() + stack.len() + usize::from(first_byte.is_some()) > limit {
282 bail!("LZW: more data than the table says");
283 }
284 out.extend(stack.iter().rev());
285 if first_byte.is_some() {
286 out.push(first);
287 }
288 if let Some(p) = prev {
289 if prefix.len() < (1 << maxbits) {
290 prefix.push(p);
291 last.push(first);
292 }
293 }
294 prev = Some(code.min(prefix.len() as u32 - 1));
295 if prefix.len() >= (1 << nbits) && nbits < maxbits {
296 skip_group(&mut pos, &mut bitbuf, &mut bitcnt, codes_in_group, nbits);
297 nbits += 1;
298 codes_in_group = 0;
299 }
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 fn fixture() -> Vec<u8> {
308 std::fs::read(concat!(
309 env!("CARGO_MANIFEST_DIR"),
310 "/tests/fixtures/cad/nist/ctc-01/creo-10/nist_ctc_01_asme1_rd_cr10030.prt.5"
311 ))
312 .unwrap()
313 }
314
315 #[test]
316 fn the_table_of_contents_lists_the_geometry_sections() {
317 let data = fixture();
318 let toc = toc(&data).unwrap();
319 assert!(toc.len() > 100, "{}", toc.len());
320 let vis = toc.iter().find(|e| e.name == "VisibGeom").unwrap();
321 assert_eq!(vis.kind, "9");
322 assert_eq!(vis.unpacked, 70593);
323 assert!(toc.iter().any(|e| e.name == "CGFPersistTable"));
325 }
326
327 #[test]
328 fn compressed_sections_inflate_to_their_stated_size() {
329 let data = fixture();
330 let toc = toc(&data).unwrap();
331 for name in ["SolidPersistTable", "SolidPrimdata", "IdsGenSimpRepDb"] {
332 let e = toc.iter().find(|e| e.name == name).unwrap();
333 let body = section(&data, &toc, name).unwrap().unwrap();
334 assert_eq!(body.len(), e.unpacked, "{name}");
335 assert_eq!(body[0], 0xe0, "{name} starts with a field definition");
336 }
337 let vis = section(&data, &toc, "VisibGeom").unwrap().unwrap();
338 assert!(vis.starts_with(b"\xe0\x00Sld_VisGeom\x00"));
339 }
340
341 #[test]
342 fn damaged_files_are_errors_not_panics() {
343 let data = fixture();
344 for cut in (0..data.len()).step_by(data.len() / 50) {
345 if let Ok(t) = toc(&data[..cut]) {
346 for e in &t {
347 let _ = section(&data[..cut], &t, &e.name);
348 }
349 }
350 }
351 assert!(toc(b"#UGC:2 PART 1 2 3 zz \\\n#UGC_TOC 2 99999999 81 1#\n").is_err());
352 assert!(unlzw(&[0x1f, 0x9d, 0x10, 0xff, 0xff, 0xff, 0xff], 100).is_err() || true);
353 }
354}