use std::{collections::HashSet, io};
use byteorder::{WriteBytesExt as _, LE};
use indexmap::IndexMap;
use ltk_hash::{BinHash, WriteBytesExt as _};
use ltk_io_ext::WriterExt as _;
use crate::{
stream::{BinStream, ObjectEntry},
tree::write::WRITE_VERSION,
BinKind, BinObject, Error,
};
#[derive(Debug, Clone, PartialEq)]
pub struct BinDelta {
replaced: IndexMap<BinHash, BinObject>,
removed: HashSet<BinHash>,
appended: IndexMap<BinHash, BinObject>,
dependencies: Option<Vec<String>>,
}
impl Default for BinDelta {
fn default() -> Self {
Self {
replaced: IndexMap::new(),
removed: HashSet::new(),
appended: IndexMap::new(),
dependencies: None,
}
}
}
impl BinDelta {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn replace(&mut self, object: BinObject) -> Option<BinObject> {
self.removed.remove(&object.path_hash);
self.replaced.insert(object.path_hash, object)
}
pub fn remove(&mut self, path_hash: impl Into<BinHash>) -> Option<BinObject> {
let path_hash = path_hash.into();
self.removed.insert(path_hash);
self.replaced.shift_remove(&path_hash)
}
pub fn append(&mut self, object: BinObject) -> Option<BinObject> {
self.appended.insert(object.path_hash, object)
}
pub fn set_dependencies(&mut self, dependencies: impl IntoIterator<Item = impl Into<String>>) {
self.dependencies = Some(dependencies.into_iter().map(Into::into).collect());
}
#[must_use]
pub fn replacement(&self, path_hash: impl Into<BinHash>) -> Option<&BinObject> {
self.replaced.get(&path_hash.into())
}
#[must_use]
pub fn is_removed(&self, path_hash: impl Into<BinHash>) -> bool {
self.removed.contains(&path_hash.into())
}
#[must_use]
pub fn appended(&self) -> indexmap::map::Values<'_, BinHash, BinObject> {
self.appended.values()
}
#[must_use]
pub fn dependencies(&self) -> Option<&[String]> {
self.dependencies.as_deref()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.replaced.is_empty()
&& self.removed.is_empty()
&& self.appended.is_empty()
&& self.dependencies.is_none()
}
}
enum Row<'d> {
Untouched(ObjectEntry),
Edited(&'d BinObject),
}
impl Row<'_> {
fn class_hash(&self) -> BinHash {
match self {
Row::Untouched(entry) => entry.class_hash,
Row::Edited(object) => object.class_hash,
}
}
}
impl<R: io::Read + io::Seek> BinStream<R> {
pub fn write_patched<W: io::Write>(
&mut self,
delta: &BinDelta,
out: &mut W,
) -> Result<(), Error> {
if self.numbering().is_legacy() {
return Err(Error::DeltaLegacyNumbering);
}
let toc = self.toc()?;
if let Some(&missing) = delta
.replaced
.keys()
.chain(&delta.removed)
.find(|hash| toc.entry(**hash).is_none())
{
return Err(Error::DeltaMissingObject(missing));
}
if let Some(&duplicate) = delta
.appended
.keys()
.find(|hash| toc.entry(**hash).is_some() && !delta.removed.contains(hash))
{
return Err(Error::DeltaDuplicateObject(duplicate));
}
let rows: Vec<Row<'_>> = toc
.entries()
.iter()
.filter(|entry| !delta.removed.contains(&entry.path_hash))
.map(|entry| match delta.replaced.get(&entry.path_hash) {
Some(object) => Row::Edited(object),
None => Row::Untouched(*entry),
})
.chain(delta.appended.values().map(Row::Edited))
.collect();
let mut objects = self.objects();
while let Some(mut object) = objects.next()? {
if object.view()?.numbering().is_legacy() {
return Err(Error::DeltaLegacyNumbering);
}
}
let dependencies = match &delta.dependencies {
Some(dependencies) => dependencies.as_slice(),
None => self.dependencies(),
};
out.write_all(&BinKind::Prop.magic())?;
out.write_u32::<LE>(WRITE_VERSION)?;
out.write_u32::<LE>(count(dependencies.len())?)?;
for dependency in dependencies {
out.write_len_prefixed_string::<LE, _>(dependency)?;
}
out.write_u32::<LE>(count(rows.len())?)?;
for row in &rows {
out.write_bin_hash::<LE>(row.class_hash())?;
}
let mut encoded = io::Cursor::new(Vec::new());
for row in rows {
match row {
Row::Untouched(entry) => self.copy_range(entry.byte_range(), out)?,
Row::Edited(object) => {
encoded.get_mut().clear();
encoded.set_position(0);
object.to_writer(&mut encoded)?;
out.write_all(encoded.get_ref())?;
}
}
}
Ok(())
}
}
fn count(len: usize) -> Result<u32, Error> {
u32::try_from(len).map_err(|_| {
Error::IOError(io::Error::new(
io::ErrorKind::InvalidInput,
format!("{len} entries do not fit a u32 count"),
))
})
}
#[cfg(test)]
mod tests;