1use std::sync::Arc;
9
10use crate::error::{Error, Result};
11use crate::zip::Source;
12
13pub const SIGNATURE: [u8; 8] = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
14const MAXREGSECT: u32 = 0xffff_fffa;
15const FATSECT: u32 = 0xffff_fffd;
16const ENDOFCHAIN: u32 = 0xffff_fffe;
17const FREESECT: u32 = 0xffff_ffff;
18const NOSTREAM: u32 = 0xffff_ffff;
19const HEADER_LEN: usize = 512;
20const HEADER_DIFAT_ENTRIES: usize = 109;
21const DIRECTORY_ENTRY_LEN: usize = 128;
22const MINI_SECTOR_LEN: usize = 64;
23const DEFAULT_MINI_CUTOFF: u64 = 4096;
24const MAX_CHAIN: usize = 1 << 24;
26
27pub fn is_compound(bytes: &[u8]) -> bool {
29 bytes.starts_with(&SIGNATURE)
30}
31
32#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct Entry {
35 pub name: String,
36 pub kind: EntryKind,
37 left: u32,
38 right: u32,
39 child: u32,
40 start: u32,
41 size: u64,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum EntryKind {
46 Storage,
47 Stream,
48 Root,
49 Unallocated,
50}
51
52pub struct Compound {
54 data: Vec<u8>,
55 sector_len: usize,
56 fat: Vec<u32>,
57 mini_fat: Vec<u32>,
58 mini_stream: Vec<u8>,
59 mini_cutoff: u64,
60 entries: Vec<Entry>,
61}
62
63impl Compound {
64 pub fn open(source: &dyn Source) -> Result<Self> {
66 let len =
67 usize::try_from(source.len()).map_err(|_| Error::Other("file too large".into()))?;
68 let mut data = vec![0u8; len];
69 source.read_at(0, &mut data)?;
70 Self::from_bytes(data)
71 }
72
73 pub fn open_source(source: Arc<dyn Source>) -> Result<Self> {
74 Self::open(source.as_ref())
75 }
76
77 pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
78 if data.len() < HEADER_LEN || !is_compound(&data) {
79 return Err(cfb("missing compound file signature"));
80 }
81 let major = u16_at(&data, 0x1a);
82 let sector_shift = u16_at(&data, 0x1e);
83 if !matches!(major, 3 | 4) {
84 return Err(cfb(&format!("unsupported major version {major}")));
85 }
86 if !matches!(sector_shift, 9 | 12) {
87 return Err(cfb(&format!("unsupported sector shift {sector_shift}")));
88 }
89 let sector_len = 1usize << sector_shift;
90 if data.len() < sector_len * 3 {
91 return Err(cfb("file shorter than three sectors"));
92 }
93 let fat_sector_count = u32_at(&data, 0x2c) as usize;
94 let first_directory = u32_at(&data, 0x30);
95 let mut mini_cutoff = u64::from(u32_at(&data, 0x38));
96 if mini_cutoff == 0 {
97 mini_cutoff = DEFAULT_MINI_CUTOFF;
98 }
99 let first_mini_fat = u32_at(&data, 0x3c);
100 let mini_fat_count = u32_at(&data, 0x40) as usize;
101 let first_difat = u32_at(&data, 0x44);
102 let difat_count = u32_at(&data, 0x48) as usize;
103
104 let mut difat: Vec<u32> = (0..HEADER_DIFAT_ENTRIES)
105 .map(|i| u32_at(&data, 0x4c + i * 4))
106 .take_while(|§or| sector <= MAXREGSECT)
107 .collect();
108 let mut next = first_difat;
109 let mut visited = 0usize;
110 while next <= MAXREGSECT && visited < difat_count.max(1) && visited < MAX_CHAIN {
111 let Some(sector) = sector_bytes(&data, sector_len, next) else {
112 break;
113 };
114 let entries = sector_len / 4 - 1;
115 difat.extend(
116 (0..entries)
117 .map(|i| u32_at(sector, i * 4))
118 .take_while(|&s| s <= MAXREGSECT),
119 );
120 next = u32_at(sector, entries * 4);
121 visited += 1;
122 }
123 if fat_sector_count > 0 {
124 difat.truncate(fat_sector_count);
125 }
126
127 let mut fat = Vec::with_capacity(difat.len() * (sector_len / 4));
128 for §or in &difat {
129 let Some(bytes) = sector_bytes(&data, sector_len, sector) else {
130 break;
131 };
132 fat.extend((0..sector_len / 4).map(|i| u32_at(bytes, i * 4)));
133 }
134
135 let directory = read_chain(&data, sector_len, &fat, first_directory, u64::MAX);
136 let mut entries = Vec::with_capacity(directory.len() / DIRECTORY_ENTRY_LEN);
137 for raw in directory.as_chunks::<DIRECTORY_ENTRY_LEN>().0 {
138 entries.push(parse_entry(raw, major));
139 }
140 if entries.is_empty() {
141 return Err(cfb("empty directory"));
142 }
143
144 let root = &entries[0];
145 let mini_stream = match root.size {
146 0 => Vec::new(),
147 size => read_chain(&data, sector_len, &fat, root.start, size),
148 };
149 let mini_fat_bytes = match mini_fat_count {
150 0 => Vec::new(),
151 _ => read_chain(&data, sector_len, &fat, first_mini_fat, u64::MAX),
152 };
153 let mini_fat = mini_fat_bytes
154 .as_chunks::<4>()
155 .0
156 .iter()
157 .map(|c| u32::from_le_bytes(*c))
158 .collect();
159
160 Ok(Self {
161 data,
162 sector_len,
163 fat,
164 mini_fat,
165 mini_stream,
166 mini_cutoff,
167 entries,
168 })
169 }
170
171 pub fn entries(&self) -> &[Entry] {
172 &self.entries
173 }
174
175 pub fn stream_paths(&self) -> Vec<String> {
177 let mut paths = Vec::new();
178 self.collect_paths(0, "", &mut paths, &mut vec![false; self.entries.len()]);
179 paths
180 }
181
182 fn collect_paths(&self, id: u32, prefix: &str, out: &mut Vec<String>, seen: &mut Vec<bool>) {
183 let Some(entry) = self.entries.get(id as usize) else {
184 return;
185 };
186 let child = entry.child;
187 let mut stack = vec![child];
188 while let Some(current) = stack.pop() {
189 if current == NOSTREAM
190 || current as usize >= self.entries.len()
191 || seen[current as usize]
192 {
193 continue;
194 }
195 seen[current as usize] = true;
196 let node = &self.entries[current as usize];
197 let path = match prefix.is_empty() {
198 true => node.name.clone(),
199 false => format!("{prefix}/{}", node.name),
200 };
201 match node.kind {
202 EntryKind::Stream => out.push(path),
203 EntryKind::Storage => self.collect_paths(current, &path, out, seen),
204 _ => {}
205 }
206 stack.push(node.left);
207 stack.push(node.right);
208 }
209 }
210
211 pub fn entry(&self, path: &str) -> Option<&Entry> {
214 let mut current = self.entries.first()?;
215 for component in path.split('/').filter(|part| !part.is_empty()) {
216 current = self.find_child(current.child, component)?;
217 }
218 Some(current)
219 }
220
221 fn find_child(&self, root: u32, name: &str) -> Option<&Entry> {
222 let mut stack = vec![root];
223 let mut seen = vec![false; self.entries.len()];
224 while let Some(id) = stack.pop() {
225 if id == NOSTREAM || id as usize >= self.entries.len() || seen[id as usize] {
226 continue;
227 }
228 seen[id as usize] = true;
229 let entry = &self.entries[id as usize];
230 if entry.kind != EntryKind::Unallocated && entry.name.eq_ignore_ascii_case(name) {
231 return Some(entry);
232 }
233 stack.push(entry.left);
234 stack.push(entry.right);
235 }
236 None
237 }
238
239 pub fn has_stream(&self, path: &str) -> bool {
240 self.entry(path)
241 .is_some_and(|entry| entry.kind == EntryKind::Stream)
242 }
243
244 pub fn stream(&self, path: &str) -> Option<Vec<u8>> {
246 let entry = self.entry(path)?;
247 if entry.kind != EntryKind::Stream {
248 return None;
249 }
250 Some(self.read_entry(entry))
251 }
252
253 fn read_entry(&self, entry: &Entry) -> Vec<u8> {
254 if entry.size == 0 {
255 return Vec::new();
256 }
257 if entry.size < self.mini_cutoff {
258 let mut out = Vec::with_capacity(entry.size as usize);
259 let mut sector = entry.start;
260 let mut steps = 0usize;
261 while sector <= MAXREGSECT && (out.len() as u64) < entry.size && steps < MAX_CHAIN {
262 let start = sector as usize * MINI_SECTOR_LEN;
263 let Some(bytes) = self.mini_stream.get(start..) else {
264 break;
265 };
266 let take = bytes.len().min(MINI_SECTOR_LEN);
267 out.extend_from_slice(&bytes[..take]);
268 sector = self
269 .mini_fat
270 .get(sector as usize)
271 .copied()
272 .unwrap_or(ENDOFCHAIN);
273 steps += 1;
274 }
275 out.truncate(entry.size as usize);
276 return out;
277 }
278 read_chain(
279 &self.data,
280 self.sector_len,
281 &self.fat,
282 entry.start,
283 entry.size,
284 )
285 }
286}
287
288fn cfb(msg: &str) -> Error {
289 Error::Other(format!("compound file: {msg}"))
290}
291
292fn u16_at(bytes: &[u8], offset: usize) -> u16 {
293 u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
294}
295
296fn u32_at(bytes: &[u8], offset: usize) -> u32 {
297 u32::from_le_bytes([
298 bytes[offset],
299 bytes[offset + 1],
300 bytes[offset + 2],
301 bytes[offset + 3],
302 ])
303}
304
305fn sector_bytes(data: &[u8], sector_len: usize, sector: u32) -> Option<&[u8]> {
307 let start = (sector as usize + 1).checked_mul(sector_len)?;
308 let end = start.checked_add(sector_len)?;
309 match end <= data.len() {
310 true => Some(&data[start..end]),
311 false => data.get(start..).filter(|rest| !rest.is_empty()),
312 }
313}
314
315fn read_chain(data: &[u8], sector_len: usize, fat: &[u32], start: u32, size: u64) -> Vec<u8> {
317 let mut out = Vec::new();
318 let mut sector = start;
319 let mut steps = 0usize;
320 while sector <= MAXREGSECT && (out.len() as u64) < size && steps < MAX_CHAIN {
321 let Some(bytes) = sector_bytes(data, sector_len, sector) else {
322 break;
323 };
324 out.extend_from_slice(bytes);
325 sector = fat.get(sector as usize).copied().unwrap_or(ENDOFCHAIN);
326 steps += 1;
327 }
328 if size != u64::MAX {
329 out.truncate(size as usize);
330 }
331 out
332}
333
334fn parse_entry(raw: &[u8], major: u16) -> Entry {
335 let declared = usize::from(u16_at(raw, 0x40)).min(64);
336 let name_bytes = &raw[..declared];
337 let units: Vec<u16> = name_bytes
338 .as_chunks::<2>()
339 .0
340 .iter()
341 .map(|c| u16::from_le_bytes(*c))
342 .take_while(|&unit| unit != 0)
343 .collect();
344 let name = String::from_utf16_lossy(&units);
345 let kind = match raw[0x42] {
346 1 => EntryKind::Storage,
347 2 => EntryKind::Stream,
348 5 => EntryKind::Root,
349 _ => EntryKind::Unallocated,
350 };
351 let mut size = u64::from(u32_at(raw, 0x78)) | (u64::from(u32_at(raw, 0x7c)) << 32);
352 if major == 3 {
353 size &= 0xffff_ffff;
354 }
355 Entry {
356 name,
357 kind,
358 left: u32_at(raw, 0x44),
359 right: u32_at(raw, 0x48),
360 child: u32_at(raw, 0x4c),
361 start: u32_at(raw, 0x74),
362 size,
363 }
364}
365
366#[derive(Default)]
369pub struct Writer {
370 streams: Vec<(String, Vec<u8>)>,
371}
372
373impl Writer {
374 pub fn new() -> Self {
375 Self::default()
376 }
377
378 pub fn stream(mut self, name: &str, data: &[u8]) -> Self {
380 self.streams.push((name.to_string(), data.to_vec()));
381 self
382 }
383
384 pub fn build(&self) -> Vec<u8> {
385 const SECTOR: usize = 512;
386 let mut mini_stream: Vec<u8> = Vec::new();
387 let mut mini_fat: Vec<u32> = Vec::new();
388 let mut entries: Vec<(String, u32, u64, bool)> = Vec::new(); let mut regular: Vec<Vec<u8>> = Vec::new();
391 for (name, data) in &self.streams {
392 if (data.len() as u64) < DEFAULT_MINI_CUTOFF && !data.is_empty() {
393 let first = (mini_stream.len() / MINI_SECTOR_LEN) as u32;
394 let sectors = data.len().div_ceil(MINI_SECTOR_LEN);
395 mini_stream.extend_from_slice(data);
396 mini_stream.resize(
397 mini_stream.len().div_ceil(MINI_SECTOR_LEN) * MINI_SECTOR_LEN,
398 0,
399 );
400 for i in 0..sectors {
401 mini_fat.push(match i + 1 == sectors {
402 true => ENDOFCHAIN,
403 false => first + i as u32 + 1,
404 });
405 }
406 entries.push((name.clone(), first, data.len() as u64, true));
407 continue;
408 }
409 entries.push((name.clone(), 0, data.len() as u64, false));
410 regular.push(data.clone());
411 }
412 let directory_entries = 1 + entries.len();
413 let directory_sectors = directory_entries
414 .div_ceil(SECTOR / DIRECTORY_ENTRY_LEN)
415 .max(1);
416 let mini_fat_sectors = match mini_fat.is_empty() {
417 true => 0,
418 false => (mini_fat.len() * 4).div_ceil(SECTOR),
419 };
420 let mini_stream_sectors = mini_stream.len().div_ceil(SECTOR);
421 let regular_sectors: usize = regular.iter().map(|d| d.len().div_ceil(SECTOR)).sum();
422 let mut fat_sectors = 1usize;
423 loop {
424 let total = fat_sectors
425 + directory_sectors
426 + mini_fat_sectors
427 + mini_stream_sectors
428 + regular_sectors;
429 if total <= fat_sectors * (SECTOR / 4) {
430 break;
431 }
432 fat_sectors += 1;
433 }
434 let total_sectors = fat_sectors
435 + directory_sectors
436 + mini_fat_sectors
437 + mini_stream_sectors
438 + regular_sectors;
439 let mut fat: Vec<u32> = vec![FREESECT; fat_sectors * (SECTOR / 4)];
440 let mut next_sector = 0u32;
441 let mut allocate = |count: usize, fat: &mut Vec<u32>| -> u32 {
442 let start = next_sector;
443 for i in 0..count {
444 let s = start as usize + i;
445 fat[s] = match i + 1 == count {
446 true => ENDOFCHAIN,
447 false => start + i as u32 + 1,
448 };
449 }
450 next_sector += count as u32;
451 start
452 };
453 let fat_start = allocate(fat_sectors, &mut fat);
454 for i in 0..fat_sectors {
455 fat[fat_start as usize + i] = FATSECT;
456 }
457 let directory_start = allocate(directory_sectors, &mut fat);
458 let mini_fat_start = match mini_fat_sectors {
459 0 => ENDOFCHAIN,
460 n => allocate(n, &mut fat),
461 };
462 let mini_stream_start = match mini_stream_sectors {
463 0 => ENDOFCHAIN,
464 n => allocate(n, &mut fat),
465 };
466 let mut regular_iter = regular.iter();
467 for entry in entries.iter_mut() {
468 if entry.3 {
469 continue;
470 }
471 let data = regular_iter.next().expect("one regular stream per entry");
472 entry.1 = match data.is_empty() {
473 true => ENDOFCHAIN,
474 false => allocate(data.len().div_ceil(SECTOR), &mut fat),
475 };
476 }
477
478 let mut out = Vec::with_capacity((total_sectors + 1) * SECTOR);
479 out.extend_from_slice(&SIGNATURE);
480 out.extend_from_slice(&[0u8; 16]);
481 out.extend_from_slice(&0x003eu16.to_le_bytes());
482 out.extend_from_slice(&3u16.to_le_bytes());
483 out.extend_from_slice(&0xfffeu16.to_le_bytes());
484 out.extend_from_slice(&9u16.to_le_bytes());
485 out.extend_from_slice(&6u16.to_le_bytes());
486 out.extend_from_slice(&[0u8; 6]);
487 out.extend_from_slice(&0u32.to_le_bytes());
488 out.extend_from_slice(&(fat_sectors as u32).to_le_bytes());
489 out.extend_from_slice(&directory_start.to_le_bytes());
490 out.extend_from_slice(&0u32.to_le_bytes());
491 out.extend_from_slice(&(DEFAULT_MINI_CUTOFF as u32).to_le_bytes());
492 out.extend_from_slice(&mini_fat_start.to_le_bytes());
493 out.extend_from_slice(&(mini_fat_sectors as u32).to_le_bytes());
494 out.extend_from_slice(&ENDOFCHAIN.to_le_bytes());
495 out.extend_from_slice(&0u32.to_le_bytes());
496 for i in 0..HEADER_DIFAT_ENTRIES {
497 let value = match i < fat_sectors {
498 true => fat_start + i as u32,
499 false => FREESECT,
500 };
501 out.extend_from_slice(&value.to_le_bytes());
502 }
503 debug_assert_eq!(out.len(), HEADER_LEN);
504 for value in &fat {
505 out.extend_from_slice(&value.to_le_bytes());
506 }
507
508 let order = sorted_order(&entries);
509 let tree = balanced_tree(&order);
510 let mut directory = Vec::with_capacity(directory_sectors * SECTOR);
511 let root_child = tree.root.map_or(NOSTREAM, |id| id as u32 + 1);
512 directory.extend(directory_entry(
513 "Root Entry",
514 5,
515 NOSTREAM,
516 NOSTREAM,
517 root_child,
518 match mini_stream.is_empty() {
519 true => ENDOFCHAIN,
520 false => mini_stream_start,
521 },
522 mini_stream.len() as u64,
523 ));
524 for (index, (name, start, size, _)) in entries.iter().enumerate() {
525 let (left, right) = tree.links[index];
526 directory.extend(directory_entry(
527 name,
528 2,
529 left.map_or(NOSTREAM, |id| id as u32 + 1),
530 right.map_or(NOSTREAM, |id| id as u32 + 1),
531 NOSTREAM,
532 *start,
533 *size,
534 ));
535 }
536 while directory.len() < directory_sectors * SECTOR {
537 directory.extend(directory_entry("", 0, NOSTREAM, NOSTREAM, NOSTREAM, 0, 0));
538 }
539 out.extend_from_slice(&directory);
540 if mini_fat_sectors > 0 {
541 let mut bytes = Vec::with_capacity(mini_fat_sectors * SECTOR);
542 for value in &mini_fat {
543 bytes.extend_from_slice(&value.to_le_bytes());
544 }
545 bytes.resize(mini_fat_sectors * SECTOR, 0xff);
546 out.extend_from_slice(&bytes);
547 }
548 if mini_stream_sectors > 0 {
549 let mut bytes = mini_stream.clone();
550 bytes.resize(mini_stream_sectors * SECTOR, 0);
551 out.extend_from_slice(&bytes);
552 }
553 for data in ®ular {
554 let mut bytes = data.clone();
555 bytes.resize(data.len().div_ceil(SECTOR) * SECTOR, 0);
556 out.extend_from_slice(&bytes);
557 }
558 out
559 }
560}
561
562fn sorted_order(entries: &[(String, u32, u64, bool)]) -> Vec<usize> {
565 let key = |name: &str| -> (usize, Vec<u16>) {
566 let units: Vec<u16> = name
567 .encode_utf16()
568 .map(|unit| match char::from_u32(u32::from(unit)) {
569 Some(ch) if ch.is_ascii_lowercase() => unit - 32,
570 _ => unit,
571 })
572 .collect();
573 ((units.len() + 1) * 2, units)
574 };
575 let mut order: Vec<usize> = (0..entries.len()).collect();
576 order.sort_by_key(|&i| key(&entries[i].0));
577 order
578}
579
580struct Tree {
581 root: Option<usize>,
582 links: Vec<(Option<usize>, Option<usize>)>,
584}
585
586fn balanced_tree(order: &[usize]) -> Tree {
587 let mut links = vec![(None, None); order.len()];
588 let root = build_subtree(order, &mut links);
589 Tree { root, links }
590}
591
592fn build_subtree(order: &[usize], links: &mut [(Option<usize>, Option<usize>)]) -> Option<usize> {
593 if order.is_empty() {
594 return None;
595 }
596 let middle = order.len() / 2;
597 let node = order[middle];
598 let left = build_subtree(&order[..middle], links);
599 let right = build_subtree(&order[middle + 1..], links);
600 links[node] = (left, right);
601 Some(node)
602}
603
604fn directory_entry(
605 name: &str,
606 kind: u8,
607 left: u32,
608 right: u32,
609 child: u32,
610 start: u32,
611 size: u64,
612) -> Vec<u8> {
613 let mut entry = vec![0u8; DIRECTORY_ENTRY_LEN];
614 let units: Vec<u16> = name.encode_utf16().take(31).collect();
615 for (i, unit) in units.iter().enumerate() {
616 entry[i * 2..i * 2 + 2].copy_from_slice(&unit.to_le_bytes());
617 }
618 let name_len = match name.is_empty() {
619 true => 0u16,
620 false => (units.len() as u16 + 1) * 2,
621 };
622 entry[0x40..0x42].copy_from_slice(&name_len.to_le_bytes());
623 entry[0x42] = kind;
624 entry[0x43] = 1;
625 entry[0x44..0x48].copy_from_slice(&left.to_le_bytes());
626 entry[0x48..0x4c].copy_from_slice(&right.to_le_bytes());
627 entry[0x4c..0x50].copy_from_slice(&child.to_le_bytes());
628 entry[0x74..0x78].copy_from_slice(&start.to_le_bytes());
629 entry[0x78..0x80].copy_from_slice(&size.to_le_bytes());
630 entry
631}
632
633#[cfg(test)]
634mod tests {
635 use super::*;
636
637 #[test]
638 fn written_files_read_back_with_mini_and_regular_streams() {
639 let big: Vec<u8> = (0..10_000u32).map(|i| (i % 251) as u8).collect();
640 let bytes = Writer::new()
641 .stream("Current User", b"tiny")
642 .stream("PowerPoint Document", &big)
643 .stream("Pictures", &[7u8; 100])
644 .stream("\u{5}SummaryInformation", b"")
645 .build();
646 assert!(is_compound(&bytes));
647 assert_eq!(bytes.len() % 512, 0);
648 let compound = Compound::from_bytes(bytes).unwrap();
649 assert_eq!(compound.stream("Current User").unwrap(), b"tiny");
650 assert_eq!(compound.stream("powerpoint document").unwrap(), big);
651 assert_eq!(compound.stream("Pictures").unwrap(), vec![7u8; 100]);
652 assert_eq!(compound.stream("\u{5}SummaryInformation").unwrap(), b"");
653 assert!(compound.stream("Missing").is_none());
654 let mut paths = compound.stream_paths();
655 paths.sort();
656 assert_eq!(
657 paths,
658 [
659 "\u{5}SummaryInformation",
660 "Current User",
661 "Pictures",
662 "PowerPoint Document"
663 ]
664 );
665 }
666
667 #[test]
668 fn many_small_streams_span_several_directory_sectors() {
669 let mut writer = Writer::new();
670 for i in 0..40 {
671 writer = writer.stream(&format!("s{i}"), format!("payload {i}").as_bytes());
672 }
673 let compound = Compound::from_bytes(writer.build()).unwrap();
674 for i in 0..40 {
675 assert_eq!(
676 compound.stream(&format!("s{i}")).unwrap(),
677 format!("payload {i}").as_bytes()
678 );
679 }
680 assert_eq!(compound.stream_paths().len(), 40);
681 }
682
683 #[test]
684 fn rejects_non_compound_and_truncated_input() {
685 assert!(Compound::from_bytes(b"PK\x03\x04".to_vec()).is_err());
686 let mut header = vec![0u8; 600];
687 header[..8].copy_from_slice(&SIGNATURE);
688 assert!(Compound::from_bytes(header).is_err());
689 let bytes = Writer::new().stream("a", b"x").build();
690 let cut = bytes[..bytes.len() - 700].to_vec();
691 let compound = Compound::from_bytes(cut);
692 if let Ok(compound) = compound {
693 let _ = compound.stream("a");
694 }
695 }
696}