1#![deny(
52 clippy::unwrap_used,
53 clippy::expect_used,
54 clippy::indexing_slicing,
55 clippy::panic,
56 clippy::unreachable
57)]
58
59use std::io::{self, Read, Seek};
60
61pub const DESCRIPTOR_STREAM: &str = "/UFRxDoc";
63const TABLE_GAP: usize = 10;
65const MAX_PATH: usize = 1024;
67const MAX_NAME: usize = 512;
68
69fn u32_at(data: &[u8], i: usize) -> Option<usize> {
70 let bytes: [u8; 4] = data.get(i..i + 4)?.try_into().ok()?;
71 usize::try_from(u32::from_le_bytes(bytes)).ok()
72}
73
74fn u16_at(data: &[u8], i: usize) -> Option<u16> {
75 let bytes: [u8; 2] = data.get(i..i + 2)?.try_into().ok()?;
76 Some(u16::from_le_bytes(bytes))
77}
78
79fn text_at(data: &[u8], i: usize, min: usize, max: usize) -> Option<(String, usize)> {
82 let n = u32_at(data, i)?;
83 if !(min..=max).contains(&n) {
84 return None;
85 }
86 let end = i.checked_add(4)?.checked_add(n.checked_mul(2)?)?;
87 let units: Vec<u16> = data
88 .get(i + 4..end)?
89 .chunks_exact(2)
90 .map(|p| {
91 u16::from_le_bytes([
92 p.first().copied().unwrap_or(0),
93 p.get(1).copied().unwrap_or(0),
94 ])
95 })
96 .collect();
97 let text: String = char::decode_utf16(units).collect::<Result<_, _>>().ok()?;
98 if text.chars().any(char::is_control) {
99 return None;
100 }
101 Some((text, end))
102}
103
104struct Descriptor {
106 name: String,
108 display: String,
110 count: u32,
112 end: usize,
114}
115
116impl Descriptor {
117 fn is_named(&self, occurrence: &str) -> bool {
120 let occurrence = occurrence.trim().to_lowercase();
121 let stem = self
122 .name
123 .rsplit_once('.')
124 .map_or(self.name.as_str(), |(stem, _)| stem);
125 (!self.display.trim().is_empty() && self.display.trim().to_lowercase() == occurrence)
126 || stem.trim().to_lowercase() == occurrence
127 }
128}
129
130fn descriptor_at(data: &[u8], i: usize) -> Option<Descriptor> {
132 let (path, after_path) = text_at(data, i, 5, MAX_PATH)?;
133 let name = path.rsplit(['\\', '/']).next()?.trim();
134 if !path.contains(['\\', '/']) || !name.contains('.') || name.starts_with('.') {
135 return None;
136 }
137 u32_at(data, after_path)?; let (_, after_workspace) = text_at(data, after_path + 4, 0, MAX_NAME)?;
139 let (display, after_display) = text_at(data, after_workspace + 2, 0, MAX_NAME)?;
140 if data.get(after_display..after_display + 6)? != [0u8; 6] {
141 return None;
142 }
143 let tail = after_display + 6 + 32;
144 u32_at(data, tail)?;
145 if u16_at(data, tail + 4)? != 0 {
146 return None;
147 }
148 let count = u16_at(data, tail + 6)?;
149 if count == 0 {
150 return None;
151 }
152 Some(Descriptor {
153 name: name.to_string(),
154 display,
155 count: u32::from(count),
156 end: tail + 8,
157 })
158}
159
160fn descriptor_table(data: &[u8]) -> Vec<Descriptor> {
163 let mut out = Vec::new();
164 let mut i = 0;
165 while i + 4 <= data.len() {
166 let Some(first) = descriptor_at(data, i) else {
167 i += 1;
168 continue;
169 };
170 let mut next = first.end + TABLE_GAP;
171 out.push(first);
172 while let Some(d) = descriptor_at(data, next) {
173 next = d.end + TABLE_GAP;
174 out.push(d);
175 }
176 break;
177 }
178 out
179}
180
181fn tag(t: u8) -> [u8; 7] {
183 [1, t, 1, 0, 0, 0, t]
184}
185
186fn occurrence_records(data: &[u8]) -> Vec<(String, bool)> {
188 let (name_tag, id_tag, flag_tag) = (tag(0x05), tag(0x02), tag(0x07));
189 let mut out = Vec::new();
190 let mut i = 0;
191 while let Some(found) = data
192 .get(i..)
193 .and_then(|rest| rest.windows(7).position(|w| w == name_tag))
194 {
195 let at = i + found;
196 i = at + 1;
197 let Some((name, after_name)) = text_at(data, at + 7, 1, MAX_NAME) else {
198 continue;
199 };
200 let id_at = after_name + 4;
201 let flag_at = id_at + 7 + 20;
202 if data.get(id_at..id_at + 7) != Some(&id_tag[..])
203 || data.get(flag_at..flag_at + 7) != Some(&flag_tag[..])
204 {
205 continue;
206 }
207 match data.get(flag_at + 7) {
208 Some(0) => out.push((name, false)),
209 Some(1) => out.push((name, true)),
210 _ => {}
211 }
212 }
213 out
214}
215
216pub fn occurrence_counts(data: &[u8]) -> Vec<(String, u32)> {
222 let table = descriptor_table(data);
223 let all: Vec<(String, u32)> = table.iter().map(|d| (d.name.clone(), d.count)).collect();
224 let total: u32 = table.iter().map(|d| d.count).sum();
225 let records = occurrence_records(data);
226 let Some(current) = records.get(..usize::try_from(total).unwrap_or(usize::MAX)) else {
227 return all; };
229 let mut suppressed = vec![0u32; table.len()];
230 for (name, _) in current.iter().filter(|(_, s)| *s) {
231 let mut matches = table.iter().enumerate().filter(|(_, d)| d.is_named(name));
232 match (matches.next(), matches.next()) {
233 (Some((index, _)), None) => {
234 if let Some(n) = suppressed.get_mut(index) {
235 *n += 1;
236 }
237 }
238 _ => return all, }
240 }
241 let mut out = Vec::with_capacity(table.len());
242 for (d, n) in table.iter().zip(&suppressed) {
243 let Some(active) = d.count.checked_sub(*n) else {
244 return all;
245 };
246 out.push((d.name.clone(), active));
247 }
248 out
249}
250
251pub fn file_occurrence_counts<R: Read + Seek>(
255 reader: R,
256 max: u64,
257) -> io::Result<Option<Vec<(String, u32)>>> {
258 let Ok(mut cfb) = cfb::CompoundFile::open(reader) else {
259 return Ok(None);
260 };
261 let Ok(stream) = cfb.open_stream(DESCRIPTOR_STREAM) else {
262 return Ok(None);
263 };
264 let mut data = Vec::new();
265 stream.take(max).read_to_end(&mut data)?;
266 Ok(Some(occurrence_counts(&data)))
267}
268
269#[cfg(test)]
272pub(crate) fn test_assembly(entries: &[(&str, bool, u16)]) -> Vec<u8> {
273 test_assembly_with_records(entries, &[])
274}
275
276#[cfg(test)]
278pub(crate) fn test_assembly_with_records(
279 entries: &[(&str, bool, u16)],
280 records: &[(&str, bool)],
281) -> Vec<u8> {
282 use std::io::{Cursor, Write};
283 let descriptors: Vec<Vec<u8>> = entries
284 .iter()
285 .map(|(path, library, count)| tests::descriptor(path, *library, "", *count))
286 .collect();
287 let mut data = tests::table(&descriptors);
288 for (name, suppressed) in records {
289 data.extend(tests::record(name, *suppressed));
290 }
291 let mut cfb = cfb::CompoundFile::create(Cursor::new(Vec::new())).unwrap();
292 cfb.create_stream(DESCRIPTOR_STREAM)
293 .unwrap()
294 .write_all(&data)
295 .unwrap();
296 cfb.flush().unwrap();
297 cfb.into_inner().into_inner()
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use std::io::{Cursor, Write};
304
305 fn text(s: &str) -> Vec<u8> {
306 let units: Vec<u16> = s.encode_utf16().collect();
307 let mut out = u32::try_from(units.len()).unwrap().to_le_bytes().to_vec();
308 out.extend(units.iter().flat_map(|u| u.to_le_bytes()));
309 out
310 }
311
312 pub(super) fn descriptor(path: &str, library: bool, display: &str, count: u16) -> Vec<u8> {
314 let mut d = text(path);
315 d.extend(if library { 0x4du32 } else { 0x43 }.to_le_bytes());
316 d.extend(text(if library {
317 "Content Center Files"
318 } else {
319 "Workspace"
320 }));
321 d.extend(if library { 3u16 } else { 1 }.to_le_bytes());
322 d.extend(text(display));
323 d.extend([0u8; 6]);
324 d.extend((0..32u8).map(|b| b.wrapping_mul(37)));
325 d.extend(0x0001_3652u32.to_le_bytes());
326 d.extend(0u16.to_le_bytes());
327 d.extend(count.to_le_bytes());
328 d
329 }
330
331 pub(super) fn table(entries: &[Vec<u8>]) -> Vec<u8> {
332 let mut out = Vec::new();
333 for (i, e) in entries.iter().enumerate() {
334 if i > 0 {
335 out.extend([0x2f, 0, 0, 0, 1, 0, 4, 0, 0, 0]); }
337 out.extend_from_slice(e);
338 }
339 out
340 }
341
342 pub(super) fn record(name: &str, suppressed: bool) -> Vec<u8> {
345 let mut r = Vec::new();
346 r.extend(tag(0x12));
347 r.extend([0x5au8; 16]);
348 r.extend([0u8; 4]);
349 r.extend(tag(0x19));
350 r.extend(if suppressed { 5u64 } else { 1 }.to_le_bytes());
351 r.extend(tag(0x05));
352 r.extend(text(name));
353 r.extend([0u8; 4]);
354 r.extend(tag(0x02));
355 r.extend([0xa7u8; 16]);
356 r.extend([0u8; 4]);
357 r.extend(tag(0x07));
358 r.push(u8::from(suppressed));
359 r.extend([0u8; 4]);
360 r.extend(tag(0x1e));
361 r.extend(text(name));
362 r.extend([0u8; 30]);
363 r
364 }
365
366 fn with_records(table_entries: &[Vec<u8>], records: &[(&str, bool)]) -> Vec<u8> {
367 let mut data = table(table_entries);
368 data.extend([0x33u8; 64]);
369 for (name, suppressed) in records {
370 data.extend(record(name, *suppressed));
371 }
372 data
373 }
374
375 fn suppression_table() -> Vec<Vec<u8>> {
376 vec![
377 descriptor("C:\\Rover\\Wheel\\Wheel.iam", false, "", 4),
378 descriptor("C:\\Rover\\Frame.ipt", false, "", 1),
379 descriptor(
380 "C:\\CC\\ISO 4762\\ISO 4762 - M6 x 20.ipt",
381 true,
382 "ISO 4762 M6 x 20",
383 3,
384 ),
385 ]
386 }
387
388 #[test]
389 fn suppressed_occurrences_are_not_counted() {
390 let data = with_records(
391 &suppression_table(),
392 &[
393 ("Wheel", false),
394 ("Wheel", true),
395 ("Frame", true),
396 ("ISO 4762 M6 x 20", false),
397 ("Wheel", false),
398 ("ISO 4762 M6 x 20", false),
399 ("WHEEL", false), ("ISO 4762 M6 x 20", false),
401 ("Wheel", true),
403 ("ISO 4762 M6 x 20", true),
404 ],
405 );
406 assert_eq!(
407 occurrence_counts(&data),
408 vec![
409 ("Wheel.iam".to_string(), 3),
410 ("Frame.ipt".to_string(), 0),
412 ("ISO 4762 - M6 x 20.ipt".to_string(), 3),
413 ]
414 );
415 }
416
417 #[test]
418 fn suppression_that_does_not_add_up_is_not_guessed() {
419 let all = vec![
420 ("Wheel.iam".to_string(), 4),
421 ("Frame.ipt".to_string(), 1),
422 ("ISO 4762 - M6 x 20.ipt".to_string(), 3),
423 ];
424 let few = with_records(&suppression_table(), &[("Wheel", true), ("Frame", false)]);
426 assert_eq!(occurrence_counts(&few), all);
427 let mut unknown: Vec<(&str, bool)> = vec![("Wheel", false); 4];
429 unknown.extend([
430 ("Bracket", true),
431 ("ISO 4762 M6 x 20", false),
432 ("ISO 4762 M6 x 20", false),
433 ]);
434 assert_eq!(
435 occurrence_counts(&with_records(&suppression_table(), &unknown)),
436 all
437 );
438 let mut too_many: Vec<(&str, bool)> = vec![("Frame", true); 2];
440 too_many.extend(vec![("Wheel", false); 3]);
441 too_many.extend(vec![("ISO 4762 M6 x 20", false); 3]);
442 assert_eq!(
443 occurrence_counts(&with_records(&suppression_table(), &too_many)),
444 all
445 );
446 let twins = vec![
448 descriptor("C:\\Rover\\Washer A.ipt", false, "DIN 988 S7", 1),
449 descriptor("C:\\Rover\\Washer B.ipt", false, "DIN 988 S7", 1),
450 ];
451 assert_eq!(
452 occurrence_counts(&with_records(
453 &twins,
454 &[("DIN 988 S7", true), ("DIN 988 S7", false)]
455 )),
456 vec![
457 ("Washer A.ipt".to_string(), 1),
458 ("Washer B.ipt".to_string(), 1)
459 ]
460 );
461 let mut odd = record("Frame", true);
464 let flag_at = odd.len() - 30 - (4 + 2 * "Frame".len()) - 7 - 4 - 1;
466 assert_eq!(odd.get(flag_at), Some(&1));
467 if let Some(b) = odd.get_mut(flag_at) {
468 *b = 2;
469 }
470 let mut data = table(&suppression_table());
471 for _ in 0..4 {
472 data.extend(record("Wheel", false));
473 }
474 data.extend(odd);
475 for _ in 0..3 {
476 data.extend(record("ISO 4762 M6 x 20", false));
477 }
478 assert_eq!(occurrence_counts(&data), all);
479 }
480
481 fn stream() -> Vec<u8> {
482 let mut data = vec![0x07, 0x00, 0x00, 0x00, 0x01, 0x00];
485 data.extend(text("C:\\Rover\\Rover.iam"));
486 data.extend([0x11u8; 40]);
487 data.extend(table(&[
488 descriptor("C:\\Rover\\Wheel\\Wheel.iam", false, "Rad", 4),
489 descriptor("C:\\Rover\\Frame.ipt", false, "", 1),
490 descriptor(
491 "C:\\CC\\de-DE\\DIN 912\\DIN 912 - M6 x 20.ipt",
492 true,
493 "",
494 16,
495 ),
496 descriptor("C:\\Rover\\Old\\Frame.ipt", false, "", 2),
497 ]));
498 data.extend([0x22u8; 300]);
499 data.extend(table(&[
500 descriptor("D:\\Vault\\Rover\\Wheel\\Wheel.iam", false, "Rad", 3),
501 descriptor("D:\\Vault\\Rover\\Frame.ipt", false, "", 1),
502 ]));
503 data
504 }
505
506 #[test]
507 fn counts_from_the_first_descriptor_table() {
508 assert_eq!(
509 occurrence_counts(&stream()),
510 vec![
511 ("Wheel.iam".to_string(), 4),
512 ("Frame.ipt".to_string(), 1),
513 ("DIN 912 - M6 x 20.ipt".to_string(), 16),
514 ("Frame.ipt".to_string(), 2),
516 ]
517 );
518 }
519
520 #[test]
521 fn broken_descriptors_are_not_counted() {
522 let mut bad = descriptor("C:\\Rover\\Frame.ipt", false, "", 1);
524 let pad = bad.len() - 8 - 32 - 6; if let Some(b) = bad.get_mut(pad) {
526 *b = 1;
527 }
528 assert!(occurrence_counts(&bad).is_empty());
529 assert!(occurrence_counts(&descriptor("C:\\Rover\\Frame.ipt", false, "", 0)).is_empty());
531 assert!(occurrence_counts(&descriptor("C:\\Rover\\Frame", false, "", 1)).is_empty());
532 assert!(occurrence_counts(&descriptor("Frame.ipt", false, "", 1)).is_empty());
533 let full = descriptor("C:\\Rover\\Frame.ipt", false, "", 1);
535 assert!(occurrence_counts(full.get(..full.len() - 3).unwrap()).is_empty());
536 assert!(occurrence_counts(&[]).is_empty());
537 }
538
539 #[test]
540 fn a_gap_ends_the_table() {
541 let mut data = descriptor("C:\\Rover\\Frame.ipt", false, "", 2);
542 data.extend([0u8; 11]); data.extend(descriptor("C:\\Rover\\Wheel.iam", false, "", 5));
544 assert_eq!(occurrence_counts(&data), vec![("Frame.ipt".to_string(), 2)]);
545 }
546
547 #[test]
548 fn reads_the_stream_of_a_compound_file() {
549 let mut cfb = cfb::CompoundFile::create(Cursor::new(Vec::new())).unwrap();
550 cfb.create_storage("/RSeStorage").unwrap();
551 cfb.create_stream("/RSeStorage/RSeSegInfo")
552 .unwrap()
553 .write_all(b"x")
554 .unwrap();
555 cfb.create_stream(DESCRIPTOR_STREAM)
556 .unwrap()
557 .write_all(&stream())
558 .unwrap();
559 cfb.flush().unwrap();
560 let bytes = cfb.into_inner().into_inner();
561 let counts = file_occurrence_counts(Cursor::new(bytes), 1 << 20)
562 .unwrap()
563 .unwrap();
564 assert_eq!(counts.len(), 4);
565 assert_eq!(counts.first(), Some(&("Wheel.iam".to_string(), 4)));
566 assert_eq!(
568 file_occurrence_counts(Cursor::new(b"plain text".to_vec()), 1 << 20).unwrap(),
569 None
570 );
571 let mut other = cfb::CompoundFile::create(Cursor::new(Vec::new())).unwrap();
572 other
573 .create_stream("/Other")
574 .unwrap()
575 .write_all(b"x")
576 .unwrap();
577 other.flush().unwrap();
578 assert_eq!(
579 file_occurrence_counts(Cursor::new(other.into_inner().into_inner()), 1 << 20).unwrap(),
580 None
581 );
582 }
583}