diff --git a/src/internal/chain.rs b/src/internal/chain.rs index 8042175..91a73d8 100644 --- a/src/internal/chain.rs +++ b/src/internal/chain.rs @@ -1,4 +1,4 @@ -use crate::internal::{consts, Allocator, Sector, SectorInit}; +use crate::internal::{consts, Allocator, SectorInit}; use std::cmp; use std::io::{self, Read, Seek, SeekFrom, Write}; @@ -37,6 +37,11 @@ impl<'a, F> Chain<'a, F> { self.sector_ids.first().copied().unwrap_or(consts::END_OF_CHAIN) } + /// The IDs of the sectors in this chain, in order. + pub fn sector_ids(&self) -> &[u32] { + &self.sector_ids + } + pub fn num_sectors(&self) -> usize { self.sector_ids.len() } @@ -46,36 +51,6 @@ impl<'a, F> Chain<'a, F> { } } -impl<'a, F: Seek> Chain<'a, F> { - pub fn into_subsector( - self, - subsector_index: u32, - subsector_len: usize, - offset_within_subsector: u64, - ) -> io::Result> { - debug_assert!(offset_within_subsector <= subsector_len as u64); - debug_assert_eq!(self.allocator.sector_len() % subsector_len, 0); - let subsectors_per_sector = - self.allocator.sector_len() / subsector_len; - let sector_index_within_chain = - subsector_index as usize / subsectors_per_sector; - let subsector_index_within_sector = - subsector_index % (subsectors_per_sector as u32); - let sector_id = *self - .sector_ids - .get(sector_index_within_chain) - .ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidData, "invalid sector id") - })?; - self.allocator.seek_within_subsector( - sector_id, - subsector_index_within_sector, - subsector_len, - offset_within_subsector, - ) - } -} - impl<'a, F: Write + Seek> Chain<'a, F> { /// Resizes the chain to the minimum number of sectors large enough to old /// `new_len` bytes, allocating or freeing sectors as needed. diff --git a/src/internal/directory.rs b/src/internal/directory.rs index ad89ee4..e06b0a7 100644 --- a/src/internal/directory.rs +++ b/src/internal/directory.rs @@ -3,7 +3,7 @@ use crate::internal::{ SectorInit, Timestamp, Validation, Version, }; use crate::WriteLeNumber; -use fnv::FnvHashSet; +use fnv::{FnvHashMap, FnvHashSet}; use std::cmp::Ordering; use std::io::{self, Seek, SeekFrom, Write}; @@ -24,6 +24,11 @@ pub struct Directory { allocator: Allocator, dir_entries: Vec, dir_start_sector: u32, + /// Every entry's stream ID by `(parent stream ID, name key)`. The + /// sibling tree is not kept balanced (many writers, this one included, + /// append to it), so walking it makes a lookup cost `O(siblings)`; the + /// index makes it `O(1)` whatever shape the tree is in. + name_index: FnvHashMap<(u32, (usize, String)), u32>, } impl Directory { @@ -33,11 +38,63 @@ impl Directory { dir_start_sector: u32, validation: Validation, ) -> io::Result> { - let directory = Directory { allocator, dir_entries, dir_start_sector }; + let mut directory = Directory { + allocator, + dir_entries, + dir_start_sector, + name_index: FnvHashMap::default(), + }; directory.validate(validation)?; + directory.build_name_index(); Ok(directory) } + /// Moves the index entries of the sibling tree rooted at `child` — the + /// children of one storage — from `old_parent` to `new_parent`. + fn rekey_children( + &mut self, + child: u32, + old_parent: u32, + new_parent: u32, + ) { + let mut stack = vec![child]; + while let Some(stream_id) = stack.pop() { + if stream_id == consts::NO_STREAM { + continue; + } + let dir_entry = self.dir_entry(stream_id); + let key = internal::path::name_key(&dir_entry.name); + stack.push(dir_entry.left_sibling); + stack.push(dir_entry.right_sibling); + self.name_index.remove(&(old_parent, key.clone())); + self.name_index.insert((new_parent, key), stream_id); + } + } + + /// Fills `name_index` from the sibling trees, visiting every entry once. + fn build_name_index(&mut self) { + let mut index = FnvHashMap::default(); + let mut stack = vec![(consts::ROOT_STREAM_ID, consts::ROOT_STREAM_ID)]; + while let Some((stream_id, parent_id)) = stack.pop() { + let dir_entry = self.dir_entry(stream_id); + if stream_id != consts::ROOT_STREAM_ID { + index.insert( + (parent_id, internal::path::name_key(&dir_entry.name)), + stream_id, + ); + } + for sibling in [dir_entry.left_sibling, dir_entry.right_sibling] { + if sibling != consts::NO_STREAM { + stack.push((sibling, parent_id)); + } + } + if dir_entry.child != consts::NO_STREAM { + stack.push((dir_entry.child, stream_id)); + } + } + self.name_index = index; + } + pub fn version(&self) -> Version { self.allocator.version() } @@ -57,18 +114,8 @@ impl Directory { pub fn stream_id_for_name_chain(&self, names: &[&str]) -> Option { let mut stream_id = consts::ROOT_STREAM_ID; for name in names.iter() { - stream_id = self.dir_entry(stream_id).child; - loop { - if stream_id == consts::NO_STREAM { - return None; - } - let dir_entry = self.dir_entry(stream_id); - match internal::path::compare_names(name, &dir_entry.name) { - Ordering::Equal => break, - Ordering::Less => stream_id = dir_entry.left_sibling, - Ordering::Greater => stream_id = dir_entry.right_sibling, - } - } + let key = (stream_id, internal::path::name_key(name)); + stream_id = *self.name_index.get(&key)?; } Some(stream_id) } @@ -197,6 +244,29 @@ impl Directory { } impl Directory { + pub fn seek_within_sector( + &mut self, + sector_id: u32, + offset_within_sector: u64, + ) -> io::Result> { + self.allocator.seek_within_sector(sector_id, offset_within_sector) + } + + pub fn seek_within_subsector( + &mut self, + sector_id: u32, + subsector_index_within_sector: u32, + subsector_len: usize, + offset_within_subsector: u64, + ) -> io::Result> { + self.allocator.seek_within_subsector( + sector_id, + subsector_index_within_sector, + subsector_len, + offset_within_subsector, + ) + } + pub fn seek_within_header( &mut self, offset_within_header: u64, @@ -311,6 +381,8 @@ impl Directory { } } // TODO: rebalance tree + self.name_index + .insert((parent_id, internal::path::name_key(name)), stream_id); // Write new entry to underyling file. self.write_dir_entry(stream_id)?; @@ -338,6 +410,7 @@ impl Directory { } } debug_assert_eq!(self.dir_entry(stream_id).child, consts::NO_STREAM); + self.name_index.remove(&(parent_id, internal::path::name_key(name))); // Restructure the tree. let mut replacement_id = consts::NO_STREAM; @@ -369,6 +442,14 @@ impl Directory { pred_entry.left_sibling = left_sibling; pred_entry.right_sibling = right_sibling; pred_entry.write_to(&mut self.seek_to_dir_entry(stream_id)?)?; + // The predecessor now lives in this slot; its old slot is the + // one that ends up freed. If it is a storage, its children were + // indexed under the old slot and follow it. + self.name_index.insert( + (parent_id, internal::path::name_key(&pred_entry.name)), + stream_id, + ); + self.rekey_children(pred_entry.child, predecessor_id, stream_id); *self.dir_entry_mut(stream_id) = pred_entry; stream_id = predecessor_id; } diff --git a/src/internal/minialloc.rs b/src/internal/minialloc.rs index 2828bba..499c0d0 100644 --- a/src/internal/minialloc.rs +++ b/src/internal/minialloc.rs @@ -1,4 +1,4 @@ -use std::io::{self, Seek, SeekFrom, Write}; +use std::io::{self, Seek, Write}; use std::mem::size_of; use fnv::FnvHashSet; @@ -27,6 +27,16 @@ pub struct MiniAllocator { minifat: Vec, minifat_start_sector: u32, free_mini_sectors: Vec, + /// The sector IDs of the mini stream's chain, in order, once walked. + /// Every access to a mini sector needs the regular sector it lives in; + /// walking the FAT chain from the start on each access made reading or + /// writing `n` small streams cost `O(n²)`. The chain only ever grows + /// (see `append_mini_sector`), so it is walked once and extended in + /// place. + mini_stream_sectors: Option>, + /// The sector IDs of the MiniFAT's chain, kept the same way for + /// `set_minifat`. + minifat_sectors: Option>, } impl MiniAllocator { @@ -41,6 +51,8 @@ impl MiniAllocator { minifat, minifat_start_sector, free_mini_sectors: Vec::new(), + mini_stream_sectors: None, + minifat_sectors: None, }; minialloc.validate(validation)?; Ok(minialloc) @@ -156,6 +168,47 @@ impl MiniAllocator { } impl MiniAllocator { + /// Returns the sector IDs of the chain starting at `start_sector_id`, + /// walking it once and caching the result in `cache`. + fn cached_chain_sectors<'a>( + directory: &mut Directory, + cache: &'a mut Option>, + start_sector_id: u32, + ) -> io::Result<&'a [u32]> { + if cache.is_none() { + let sector_ids = if start_sector_id == consts::END_OF_CHAIN { + Vec::new() + } else { + directory + .open_chain(start_sector_id, SectorInit::Fat)? + .sector_ids() + .to_vec() + }; + *cache = Some(sector_ids); + } + Ok(cache.as_deref().unwrap()) + } + + /// The sector IDs of the mini stream's chain, in order. + fn mini_stream_sectors(&mut self) -> io::Result<&[u32]> { + let start_sector = self.directory.root_dir_entry().start_sector; + Self::cached_chain_sectors( + &mut self.directory, + &mut self.mini_stream_sectors, + start_sector, + ) + } + + /// The sector IDs of the MiniFAT's chain, in order. + fn minifat_sectors(&mut self) -> io::Result<&[u32]> { + let start_sector = self.minifat_start_sector; + Self::cached_chain_sectors( + &mut self.directory, + &mut self.minifat_sectors, + start_sector, + ) + } + pub fn seek_within_mini_sector( &mut self, mini_sector: u32, @@ -164,13 +217,20 @@ impl MiniAllocator { debug_assert!( offset_within_mini_sector < consts::MINI_SECTOR_LEN as u64 ); - let mini_stream_start_sector = - self.directory.root_dir_entry().start_sector; - let chain = self - .directory - .open_chain(mini_stream_start_sector, SectorInit::Fat)?; - chain.into_subsector( - mini_sector, + let mini_sectors_per_sector = + (self.directory.sector_len() / consts::MINI_SECTOR_LEN) as u32; + let sector_index = (mini_sector / mini_sectors_per_sector) as usize; + let mini_sector_within_sector = mini_sector % mini_sectors_per_sector; + let sector_id = self + .mini_stream_sectors()? + .get(sector_index) + .copied() + .ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "invalid sector id") + })?; + self.directory.seek_within_subsector( + sector_id, + mini_sector_within_sector, consts::MINI_SECTOR_LEN, offset_within_mini_sector, ) @@ -180,6 +240,12 @@ impl MiniAllocator { impl MiniAllocator { /// Given the start sector of a chain, deallocates the entire chain. pub fn free_chain(&mut self, start_sector_id: u32) -> io::Result<()> { + if start_sector_id == self.directory.root_dir_entry().start_sector { + self.mini_stream_sectors = None; + } + if start_sector_id == self.minifat_start_sector { + self.minifat_sectors = None; + } self.directory.free_chain(start_sector_id) } @@ -263,16 +329,19 @@ impl MiniAllocator { debug_assert!(self.minifat.is_empty()); self.minifat_start_sector = self.directory.begin_chain(SectorInit::Fat)?; + self.minifat_sectors = Some(vec![self.minifat_start_sector]); let mut header = self.directory.seek_within_header(60)?; header.write_le_u32(self.minifat_start_sector)?; header.write_le_u32(1)?; } else if self.minifat.len() % minifat_entries_per_sector == 0 { - let start = self.minifat_start_sector; - self.directory.extend_chain(start, SectorInit::Fat)?; - let num_minifat_sectors = self - .directory - .open_chain(start, SectorInit::Fat)? - .num_sectors() as u32; + // Extending from the chain's last sector avoids walking it from + // the start; `extend_chain` accepts any sector of the chain. + let last_sector = *self.minifat_sectors()?.last().unwrap(); + let new_sector = + self.directory.extend_chain(last_sector, SectorInit::Fat)?; + let sectors = self.minifat_sectors.as_mut().unwrap(); + sectors.push(new_sector); + let num_minifat_sectors = sectors.len() as u32; let mut header = self.directory.seek_within_header(64)?; header.write_le_u32(num_minifat_sectors)?; } @@ -293,19 +362,26 @@ impl MiniAllocator { // If the mini stream doesn't have room for new mini sector, add // another regular sector to its chain. - let new_start_sector = - if mini_stream_start_sector == consts::END_OF_CHAIN { - debug_assert_eq!(mini_stream_len, 0); - self.directory.begin_chain(SectorInit::Zero)? - } else { - if mini_stream_len % sector_len as u64 == 0 { - self.directory.extend_chain( - mini_stream_start_sector, - SectorInit::Zero, - )?; - } - mini_stream_start_sector - }; + let new_start_sector = if mini_stream_start_sector + == consts::END_OF_CHAIN + { + debug_assert_eq!(mini_stream_len, 0); + let start_sector = self.directory.begin_chain(SectorInit::Zero)?; + self.mini_stream_sectors = Some(vec![start_sector]); + start_sector + } else { + if mini_stream_len % sector_len as u64 == 0 { + // Extending from the chain's last sector avoids walking + // it from the start; `extend_chain` accepts any sector + // of the chain. + let last_sector = *self.mini_stream_sectors()?.last().unwrap(); + let new_sector = self + .directory + .extend_chain(last_sector, SectorInit::Zero)?; + self.mini_stream_sectors.as_mut().unwrap().push(new_sector); + } + mini_stream_start_sector + }; // Update length of mini stream in root directory entry. self.directory.with_root_dir_entry_mut(|dir_entry| { @@ -369,13 +445,23 @@ impl MiniAllocator { /// underlying file. The `index` must be <= `self.minifat.len()`. fn set_minifat(&mut self, index: u32, value: u32) -> io::Result<()> { debug_assert!(index as usize <= self.minifat.len()); - let mut chain = self - .directory - .open_chain(self.minifat_start_sector, SectorInit::Fat)?; let offset = (index as u64) * size_of::() as u64; - debug_assert!(chain.len() >= offset + size_of::() as u64); - chain.seek(SeekFrom::Start(offset))?; - chain.write_le_u32(value)?; + let sector_len = self.directory.sector_len() as u64; + let sector_index = (offset / sector_len) as usize; + let offset_within_sector = offset % sector_len; + let sector_id = + self.minifat_sectors()?.get(sector_index).copied().ok_or_else( + || { + io::Error::new( + io::ErrorKind::InvalidData, + "MiniFAT sector missing", + ) + }, + )?; + let mut sector = self + .directory + .seek_within_sector(sector_id, offset_within_sector)?; + sector.write_le_u32(value)?; if (index as usize) == self.minifat.len() { self.minifat.push(value); } else { diff --git a/src/internal/path.rs b/src/internal/path.rs index 83d96f8..510f127 100644 --- a/src/internal/path.rs +++ b/src/internal/path.rs @@ -37,6 +37,16 @@ fn cfb_uppercase_char(c: char) -> char { case_mapper.simple_uppercase(c) } +/// The key under which a name is indexed: two names get the same key +/// exactly when `compare_names` calls them equal (same UTF-16 length, same +/// characters once uppercased the CFB way). +pub fn name_key(name: &str) -> (usize, String) { + ( + name.encode_utf16().count(), + name.chars().map(cfb_uppercase_char).collect(), + ) +} + /// Compares two directory entry names according to CFB ordering, which is /// case-insensitive, and which always puts shorter names before longer names, /// as encoded in UTF-16 (i.e. [shortlex diff --git a/tests/mini_streams.rs b/tests/mini_streams.rs new file mode 100644 index 0000000..cf951ee --- /dev/null +++ b/tests/mini_streams.rs @@ -0,0 +1,100 @@ +use cfb::CompoundFile; +use std::io::{Cursor, Read, Write}; + +/// Writes `count` mini streams of `len` bytes each, named `s{i}`, whose +/// contents identify the stream. +fn write_streams( + comp: &mut CompoundFile>>, + count: u32, + len: usize, +) { + for i in 0..count { + let data = vec![(i % 251) as u8; len]; + let mut stream = comp.create_stream(format!("/s{i}")).unwrap(); + stream.write_all(&data).unwrap(); + } +} + +fn assert_stream( + comp: &mut CompoundFile>>, + i: u32, + len: usize, +) { + let mut data = Vec::new(); + comp.open_stream(format!("/s{i}")) + .unwrap() + .read_to_end(&mut data) + .unwrap(); + assert_eq!(data.len(), len, "stream s{} length", i); + assert!( + data.iter().all(|&b| b == (i % 251) as u8), + "stream s{} contents", + i + ); +} + +/// Many small streams grow the mini stream across many regular sectors, +/// freeing some shrinks it, and adding more grows it again; every stream +/// must read back intact through all of it, whether the file is the one +/// being written or reopened from its bytes. +#[test] +fn many_mini_streams_grow_shrink_and_regrow() { + let mut comp = CompoundFile::create(Cursor::new(Vec::new())).unwrap(); + write_streams(&mut comp, 800, 300); + for i in 0..800 { + assert_stream(&mut comp, i, 300); + } + + // Free every third stream, then add a second batch. + for i in (0..800).step_by(3) { + comp.remove_stream(format!("/s{i}")).unwrap(); + } + for i in 800..1000 { + let data = vec![(i % 251) as u8; 300]; + let mut stream = comp.create_stream(format!("/s{i}")).unwrap(); + stream.write_all(&data).unwrap(); + } + comp.flush().unwrap(); + + let check = |comp: &mut CompoundFile>>| { + for i in 0..1000u32 { + if i < 800 && i % 3 == 0 { + assert!( + !comp.is_stream(format!("/s{i}")), + "s{} was removed", + i + ); + } else { + assert_stream(comp, i, 300); + } + } + }; + check(&mut comp); + + // The same file reopened from its bytes. + let bytes = comp.into_inner().into_inner(); + let mut reopened = CompoundFile::open(Cursor::new(bytes)).unwrap(); + check(&mut reopened); +} + +/// A mini stream that grows past the mini-stream cutoff moves to the regular +/// sectors; a stream truncated back below it returns. Both transitions keep +/// the other mini streams readable. +#[test] +fn a_stream_crossing_the_mini_cutoff_keeps_its_neighbours_intact() { + let mut comp = CompoundFile::create(Cursor::new(Vec::new())).unwrap(); + write_streams(&mut comp, 100, 200); + + let mut big = comp.create_stream("/big").unwrap(); + big.write_all(&[7u8; 100]).unwrap(); + big.write_all(&vec![7u8; 10_000]).unwrap(); + big.set_len(50).unwrap(); + drop(big); + + for i in 0..100 { + assert_stream(&mut comp, i, 200); + } + let mut data = Vec::new(); + comp.open_stream("/big").unwrap().read_to_end(&mut data).unwrap(); + assert_eq!(data, vec![7u8; 50]); +} diff --git a/tests/name_index.rs b/tests/name_index.rs new file mode 100644 index 0000000..318a22f --- /dev/null +++ b/tests/name_index.rs @@ -0,0 +1,89 @@ +use cfb::CompoundFile; +use std::io::{Cursor, Read, Write}; + +/// Every name a walk reports resolves through the path lookup, in its own +/// spelling and in another case, and a name that was removed does not — +/// on the file being written and on it reopened, with a directory large +/// enough that the sibling tree is deep. +#[test] +fn every_walked_name_resolves_and_removed_names_do_not() { + let mut comp = CompoundFile::create(Cursor::new(Vec::new())).unwrap(); + for i in 0..600 { + let storage = format!("/Storage{i}"); + comp.create_storage(&storage).unwrap(); + for name in ["Data", "Parameters", "Wide"] { + let mut stream = + comp.create_stream(format!("{storage}/{name}")).unwrap(); + stream.write_all(&[i as u8; 40]).unwrap(); + } + } + for i in (0..600).step_by(4) { + comp.remove_stream(format!("/Storage{i}/Wide")).unwrap(); + } + for i in (0..600).step_by(8) { + comp.remove_storage_all(format!("/Storage{i}")).unwrap(); + comp.create_storage(format!("/storage{i}")).unwrap(); + let mut stream = + comp.create_stream(format!("/storage{i}/data")).unwrap(); + stream.write_all(&[7u8; 8]).unwrap(); + } + comp.flush().unwrap(); + + fn check(comp: &mut CompoundFile>>) { + let walked: Vec<(String, bool)> = comp + .walk() + .filter(|e| !e.is_root()) + .map(|e| (e.path().to_string_lossy().into_owned(), e.is_stream())) + .collect(); + assert!(walked.len() > 1500, "walked {} entries", walked.len()); + for (path, is_stream) in &walked { + assert!(comp.exists(path), "{} exists", path); + assert_eq!(comp.is_stream(path), *is_stream, "{path} kind"); + assert_eq!(comp.is_storage(path), !*is_stream, "{path} kind"); + let other_case = path.to_uppercase(); + assert!(comp.exists(&other_case), "{} exists", other_case); + assert_eq!(comp.is_stream(&other_case), *is_stream); + } + for i in (0..600).step_by(4) { + if i % 8 != 0 { + assert!(!comp.exists(format!("/Storage{i}/Wide")), "removed"); + assert!(comp.is_stream(format!("/Storage{i}/Data")), "kept"); + } + } + for i in (0..600).step_by(8) { + // "data" was created again under the new storage of the same + // name; "Parameters" was not. + assert!( + !comp.exists(format!("/Storage{i}/Parameters")), + "removed" + ); + let mut data = Vec::new(); + comp.open_stream(format!("/STORAGE{i}/DATA")) + .unwrap() + .read_to_end(&mut data) + .unwrap(); + assert_eq!(data, vec![7u8; 8], "re-added under another case"); + } + } + check(&mut comp); + + let bytes = comp.into_inner().into_inner(); + let mut reopened = CompoundFile::open(Cursor::new(bytes)).unwrap(); + check(&mut reopened); +} + +/// A removed name can be created again, and a name that differs from an +/// existing one only in case is the same entry, not a second one. +#[test] +fn a_name_is_one_entry_in_any_case() { + let mut comp = CompoundFile::create(Cursor::new(Vec::new())).unwrap(); + comp.create_storage("/Dir").unwrap(); + comp.create_stream("/Dir/File").unwrap().write_all(b"one").unwrap(); + assert!(comp.create_new_stream("/dir/FILE").is_err(), "same entry"); + comp.remove_stream("/DIR/file").unwrap(); + assert!(!comp.exists("/Dir/File")); + comp.create_stream("/dir/file").unwrap().write_all(b"two").unwrap(); + let mut data = Vec::new(); + comp.open_stream("/Dir/FILE").unwrap().read_to_end(&mut data).unwrap(); + assert_eq!(data, b"two"); +} diff --git a/tests/name_index_moves.rs b/tests/name_index_moves.rs new file mode 100644 index 0000000..864ec94 --- /dev/null +++ b/tests/name_index_moves.rs @@ -0,0 +1,49 @@ +use cfb::CompoundFile; +use std::io::{Cursor, Read, Write}; + +/// Removing a sibling with two subtrees fills its slot with its in-order +/// predecessor. When that predecessor is a storage, the storage's children +/// move with it and must stay reachable by path — before and after the file +/// is reopened. +#[test] +fn a_storage_moved_into_a_removed_siblings_slot_keeps_its_children() { + let mut comp = CompoundFile::create(Cursor::new(Vec::new())).unwrap(); + // "foo" is the first child; "baz" sorts before it (left), "quux" after + // it (right), so removing "foo" moves "baz" — and its children. + comp.create_storage("/foo").unwrap(); + comp.create_storage("/baz").unwrap(); + comp.create_storage("/quux").unwrap(); + comp.create_storage("/baz/inner").unwrap(); + comp.create_stream("/baz/blarg").unwrap().write_all(b"blarg").unwrap(); + comp.create_stream("/baz/inner/deep").unwrap().write_all(b"deep").unwrap(); + comp.remove_storage("/foo").unwrap(); + + fn check(comp: &mut CompoundFile>>) { + assert!(!comp.exists("/foo")); + assert!(comp.is_storage("/baz")); + assert!(comp.is_storage("/quux")); + assert!(comp.is_storage("/baz/inner")); + let mut data = Vec::new(); + comp.open_stream("/BAZ/blarg") + .unwrap() + .read_to_end(&mut data) + .unwrap(); + assert_eq!(data, b"blarg"); + data.clear(); + comp.open_stream("/baz/inner/DEEP") + .unwrap() + .read_to_end(&mut data) + .unwrap(); + assert_eq!(data, b"deep"); + } + check(&mut comp); + // The moved storage can be edited under its new slot. + comp.remove_stream("/baz/blarg").unwrap(); + assert!(!comp.exists("/baz/blarg")); + comp.create_stream("/baz/blarg").unwrap().write_all(b"blarg").unwrap(); + check(&mut comp); + + let bytes = comp.into_inner().into_inner(); + let mut reopened = CompoundFile::open_strict(Cursor::new(bytes)).unwrap(); + check(&mut reopened); +}