use std::collections::HashSet;
use std::io::{Read, Seek, Write};
use crate::read::Archive;
use crate::write::{WriteOptions, Writer};
use crate::{ArchivePath, Error, Result};
use super::operation::Operation;
#[must_use = "edit result should be checked to verify operation completed as expected"]
#[derive(Debug, Clone, Default)]
pub struct EditResult {
pub entries_kept: usize,
pub entries_renamed: usize,
pub entries_deleted: usize,
pub entries_updated: usize,
pub entries_added: usize,
pub total_bytes: u64,
pub packed_bytes: u64,
}
impl EditResult {
pub fn total_entries(&self) -> usize {
self.entries_kept + self.entries_renamed + self.entries_updated + self.entries_added
}
pub fn compression_ratio(&self) -> f64 {
if self.total_bytes == 0 {
1.0
} else {
self.packed_bytes as f64 / self.total_bytes as f64
}
}
}
pub struct ArchiveEditor<R: Read + Seek> {
archive: Archive<R>,
operations: Vec<Operation>,
options: WriteOptions,
}
impl<R: Read + Seek> ArchiveEditor<R> {
pub fn new(archive: Archive<R>) -> Self {
Self {
archive,
operations: Vec::new(),
options: WriteOptions::default(),
}
}
pub fn with_options(mut self, options: WriteOptions) -> Self {
self.options = options;
self
}
pub fn pending_operations(&self) -> usize {
self.operations.len()
}
pub fn has_pending_operations(&self) -> bool {
!self.operations.is_empty()
}
pub fn clear_operations(&mut self) {
self.operations.clear();
}
pub fn rename(&mut self, from: &str, to: &str) -> Result<()> {
let from_path = ArchivePath::new(from)?;
let to_path = ArchivePath::new(to)?;
if !self.entry_exists(&from_path) {
return Err(Error::EntryNotFound {
path: from.to_string(),
});
}
if self.entry_exists(&to_path) {
return Err(Error::EntryExists {
path: to.to_string(),
});
}
self.operations.push(Operation::Rename {
from: from_path,
to: to_path,
});
Ok(())
}
pub fn delete(&mut self, path: &str) -> Result<()> {
let archive_path = ArchivePath::new(path)?;
if !self.entry_exists(&archive_path) {
return Err(Error::EntryNotFound {
path: path.to_string(),
});
}
self.operations
.push(Operation::Delete { path: archive_path });
Ok(())
}
pub fn update(&mut self, path: &str, data: impl Into<Vec<u8>>) -> Result<()> {
let archive_path = ArchivePath::new(path)?;
if !self.entry_exists(&archive_path) {
return Err(Error::EntryNotFound {
path: path.to_string(),
});
}
self.operations.push(Operation::Update {
path: archive_path,
data: data.into(),
});
Ok(())
}
pub fn add(&mut self, path: ArchivePath, data: impl Into<Vec<u8>>) -> Result<()> {
if self.entry_exists(&path) {
return Err(Error::EntryExists {
path: path.as_str().to_string(),
});
}
self.operations.push(Operation::Add {
path,
data: data.into(),
});
Ok(())
}
pub fn apply<W: Write + Seek>(mut self, output: W) -> Result<EditResult> {
let mut result = EditResult::default();
let deleted_paths = self.collect_deleted_paths();
let renamed_paths = self.collect_renamed_paths();
let updated_paths = self.collect_updated_paths();
let entry_infos: Vec<_> = self
.archive
.entries()
.iter()
.enumerate()
.map(|(idx, e)| (idx, e.path.clone(), e.is_directory))
.collect();
let mut writer = Writer::create(output)?.options(self.options.clone());
for (entry_idx, entry_path, is_directory) in entry_infos {
let path_str = entry_path.as_str();
if deleted_paths.contains(path_str) {
result.entries_deleted += 1;
continue;
}
if is_directory {
continue;
}
if let Some(new_path) = renamed_paths.get(path_str) {
let data = self.archive.extract_entry_to_vec_by_index(entry_idx)?;
writer.add_bytes(new_path.clone(), &data)?;
result.entries_renamed += 1;
result.total_bytes += data.len() as u64;
continue;
}
if let Some(new_data) = updated_paths.get(path_str) {
writer.add_bytes(entry_path.clone(), new_data)?;
result.entries_updated += 1;
result.total_bytes += new_data.len() as u64;
continue;
}
let data = self.archive.extract_entry_to_vec_by_index(entry_idx)?;
writer.add_bytes(entry_path.clone(), &data)?;
result.entries_kept += 1;
result.total_bytes += data.len() as u64;
}
for op in &self.operations {
if let Operation::Add { path, data } = op {
writer.add_bytes(path.clone(), data)?;
result.entries_added += 1;
result.total_bytes += data.len() as u64;
}
}
let (write_result, _) = writer.finish_into_inner()?;
result.packed_bytes = write_result.compressed_size;
Ok(result)
}
fn entry_exists(&self, path: &ArchivePath) -> bool {
let path_str = path.as_str();
for entry in self.archive.entries() {
if entry.path.as_str() == path_str {
return true;
}
}
for op in &self.operations {
if let Operation::Add { path: add_path, .. } = op {
if add_path.as_str() == path_str {
return true;
}
}
}
false
}
fn collect_deleted_paths(&self) -> HashSet<String> {
self.operations
.iter()
.filter_map(|op| {
if let Operation::Delete { path } = op {
Some(path.as_str().to_string())
} else {
None
}
})
.collect()
}
fn collect_renamed_paths(&self) -> std::collections::HashMap<String, ArchivePath> {
self.operations
.iter()
.filter_map(|op| {
if let Operation::Rename { from, to } = op {
Some((from.as_str().to_string(), to.clone()))
} else {
None
}
})
.collect()
}
fn collect_updated_paths(&self) -> std::collections::HashMap<String, Vec<u8>> {
self.operations
.iter()
.filter_map(|op| {
if let Operation::Update { path, data } = op {
Some((path.as_str().to_string(), data.clone()))
} else {
None
}
})
.collect()
}
}
pub trait EditableArchive<R: Read + Seek>: Sized {
fn edit(self) -> ArchiveEditor<R>;
}
impl<R: Read + Seek> EditableArchive<R> for Archive<R> {
fn edit(self) -> ArchiveEditor<R> {
ArchiveEditor::new(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_edit_result_defaults() {
let result = EditResult::default();
assert_eq!(result.total_entries(), 0);
assert_eq!(result.compression_ratio(), 1.0);
}
#[test]
fn test_edit_result_total_entries() {
let result = EditResult {
entries_kept: 5,
entries_renamed: 2,
entries_updated: 1,
entries_added: 3,
..Default::default()
};
assert_eq!(result.total_entries(), 11);
}
#[test]
fn test_edit_result_compression_ratio() {
let result = EditResult {
total_bytes: 1000,
packed_bytes: 500,
..Default::default()
};
assert!((result.compression_ratio() - 0.5).abs() < 0.001);
}
}