diff --git a/src/lib.rs b/src/lib.rs index c71d737..c1e6380 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -700,6 +700,44 @@ impl CompoundFile { max_buffer_size, }) } + /// Writes a compacted copy of this compound file to `dest`, and returns + /// the copy. `dest` should be empty; it is written from the start. + /// + /// The copy has the same version and holds the same storages and streams, + /// with the same CLSIDs, state bits, and timestamps, but takes up only as + /// much space as those need. A compound file never shrinks in place, so + /// the sectors freed by removing or shrinking streams stay in the file + /// until it is copied like this. This is the approach Windows takes as + /// well: compacting a file means copying its root storage into a new one. + pub fn copy_to( + &mut self, + dest: W, + ) -> io::Result> { + let mut copy = + CompoundFile::create_with_version(self.version(), dest)?; + let entries: Vec = self.walk().collect(); + for entry in entries.iter() { + if entry.is_stream() { + let mut source = self.open_stream(entry.path())?; + let mut target = copy.create_stream(entry.path())?; + io::copy(&mut source, &mut target)?; + } else if !entry.is_root() { + copy.create_storage(entry.path())?; + } + } + // Fill in the metadata once the tree is complete, so that nothing + // done while building it can overwrite what was copied. + for entry in entries.iter() { + copy.set_state_bits(entry.path(), entry.state_bits())?; + if !entry.is_stream() { + copy.set_storage_clsid(entry.path(), *entry.clsid())?; + copy.set_created_time(entry.path(), entry.created())?; + copy.set_modified_time(entry.path(), entry.modified())?; + } + } + copy.flush()?; + Ok(copy) + } } impl CompoundFile { diff --git a/tests/copy_to.rs b/tests/copy_to.rs new file mode 100644 index 0000000..e455f8f --- /dev/null +++ b/tests/copy_to.rs @@ -0,0 +1,160 @@ +use cfb::{CompoundFile, Version}; +use std::io::{Cursor, Read, Seek, SeekFrom, Write}; +use std::path::PathBuf; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use uuid::Uuid; + +//===========================================================================// + +fn pattern(len: usize, seed: u8) -> Vec { + (0..len).map(|i| (i as u8).wrapping_mul(37).wrapping_add(seed)).collect() +} + +fn epoch_plus(secs: u64) -> SystemTime { + UNIX_EPOCH + Duration::from_secs(secs) +} + +/// Builds a file with nested storages, mini and regular streams, metadata on +/// several entries, and wasted space from a removed and a shrunk stream. +fn build_source(version: Version) -> CompoundFile>> { + let mut comp = + CompoundFile::create_with_version(version, Cursor::new(Vec::new())) + .unwrap(); + comp.create_storage("/a").unwrap(); + comp.create_storage_all("/a/b/c").unwrap(); + comp.create_stream("/small").unwrap().write_all(&pattern(100, 1)).unwrap(); + comp.create_stream("/a/empty").unwrap(); + comp.create_stream("/a/b/c/mid") + .unwrap() + .write_all(&pattern(5000, 2)) + .unwrap(); + { + let mut stream = comp.create_stream("/a/large").unwrap(); + stream.write_all(&pattern(200_000, 4)).unwrap(); + stream.set_len(100_000).unwrap(); + } + // Written last, so that nothing reuses its sectors once it is gone. + comp.create_stream("/junk") + .unwrap() + .write_all(&pattern(300_000, 3)) + .unwrap(); + comp.remove_stream("/junk").unwrap(); + comp.set_storage_clsid("/", Uuid::from_u128(0x1111)).unwrap(); + comp.set_storage_clsid("/a/b", Uuid::from_u128(0x2222)).unwrap(); + comp.set_state_bits("/small", 0x1234).unwrap(); + comp.set_state_bits("/a", 0x5678).unwrap(); + comp.set_created_time("/a", epoch_plus(1_000_000)).unwrap(); + comp.set_modified_time("/a/b/c", epoch_plus(2_000_000)).unwrap(); + comp.set_modified_time("/", epoch_plus(3_000_000)).unwrap(); + comp +} + +type Snapshot = Vec<(PathBuf, bool, u64, Uuid, u32, SystemTime, SystemTime)>; + +fn snapshot(comp: &CompoundFile) -> Snapshot { + comp.walk() + .map(|e| { + ( + e.path().to_path_buf(), + e.is_stream(), + e.len(), + *e.clsid(), + e.state_bits(), + e.created(), + e.modified(), + ) + }) + .collect() +} + +fn stream_contents( + comp: &mut CompoundFile, +) -> Vec<(PathBuf, Vec)> { + let paths: Vec = comp + .walk() + .filter(|e| e.is_stream()) + .map(|e| e.path().to_path_buf()) + .collect(); + paths + .into_iter() + .map(|path| { + let mut data = Vec::new(); + comp.open_stream(&path).unwrap().read_to_end(&mut data).unwrap(); + (path, data) + }) + .collect() +} + +//===========================================================================// + +#[test] +fn copy_preserves_tree_metadata_and_contents() { + for version in [Version::V3, Version::V4] { + let mut source = build_source(version); + let mut copy = source.copy_to(Cursor::new(Vec::new())).unwrap(); + assert_eq!(copy.version(), version); + assert_eq!(snapshot(©), snapshot(&source)); + assert_eq!(stream_contents(&mut copy), stream_contents(&mut source)); + assert_eq!( + copy.entry("/a/large").unwrap().len(), + 100_000, + "shrunk stream keeps its new length" + ); + + // The copy must also hold up on disk, under strict validation. + let mut reopened = + CompoundFile::open_strict(copy.into_inner()).unwrap(); + assert_eq!(snapshot(&reopened), snapshot(&source)); + assert_eq!( + stream_contents(&mut reopened), + stream_contents(&mut source) + ); + } +} + +#[test] +fn copy_drops_the_space_of_removed_and_shrunk_streams() { + let mut source = build_source(Version::V4); + let copy = source.copy_to(Cursor::new(Vec::new())).unwrap(); + let source_len = source.into_inner().into_inner().len(); + let copy_len = copy.into_inner().into_inner().len(); + // The source still holds every sector the removed stream and the shrunk + // one ever used; the copy holds only the 105 KB that is live plus a few + // sectors of metadata. + assert!(source_len > 400_000, "source is {} bytes", source_len); + assert!(copy_len < 140_000, "copy is {} bytes", copy_len); +} + +#[test] +fn copy_from_a_read_only_source() { + let bytes = build_source(Version::V3).into_inner().into_inner(); + // A `Cursor<&[u8]>` can be read and seeked but not written. + let mut source = + CompoundFile::open(Cursor::new(bytes.as_slice())).unwrap(); + let mut copy = source.copy_to(Cursor::new(Vec::new())).unwrap(); + assert_eq!(snapshot(©), snapshot(&source)); + assert_eq!(stream_contents(&mut copy), stream_contents(&mut source)); +} + +#[test] +fn copy_of_an_empty_file() { + let mut source = CompoundFile::create(Cursor::new(Vec::new())).unwrap(); + let copy = source.copy_to(Cursor::new(Vec::new())).unwrap(); + assert_eq!(snapshot(©), snapshot(&source)); + CompoundFile::open_strict(copy.into_inner()).unwrap(); +} + +#[test] +fn copy_leaves_the_source_readable() { + let mut source = build_source(Version::V3); + let before = snapshot(&source); + source.copy_to(Cursor::new(Vec::new())).unwrap(); + assert_eq!(snapshot(&source), before); + let mut stream = source.open_stream("/a/b/c/mid").unwrap(); + stream.seek(SeekFrom::Start(4000)).unwrap(); + let mut tail = Vec::new(); + stream.read_to_end(&mut tail).unwrap(); + assert_eq!(tail, pattern(5000, 2)[4000..]); +} + +//===========================================================================//