files_diff/
patch.rs

1use rkyv::Archive;
2
3use crate::{compress::CompressAlgorithm, hash, Error};
4
5/// Algorithms available for generating binary diffs.
6///
7/// Each algorithm offers different tradeoffs between patch size, generation
8/// speed, and application speed.
9///
10/// # Example
11/// ```rust
12/// use files_diff::{diff, DiffAlgorithm, CompressAlgorithm};
13///
14/// // Use rsync for fast differing of similar files
15/// let rsync_patch = diff(
16///     b"original",
17///     b"modified",
18///     DiffAlgorithm::Rsync020,
19///     CompressAlgorithm::None
20/// )?;
21///
22/// // Use bidiff for potentially smaller patches
23/// let bidiff_patch = diff(
24///     b"original",
25///     b"modified",
26///     DiffAlgorithm::Bidiff1,
27///     CompressAlgorithm::Zstd
28/// )?;
29/// # Ok::<(), files_diff::Error>(())
30/// ```
31#[derive(Archive, rkyv::Deserialize, rkyv::Serialize, Debug, PartialEq, Clone, Copy, Eq, Hash)]
32#[rkyv(derive(Debug, PartialEq))]
33pub enum DiffAlgorithm {
34    /// Fast-rsync algorithm version 0.2.0.
35    /// Optimized for files that are mostly similar.
36    Rsync020,
37
38    /// Bidirectional diff algorithm version 1.
39    /// May produce smaller patches for very different files.
40    Bidiff1,
41}
42
43impl std::fmt::Display for DiffAlgorithm {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        write!(f, "{:?}", self)
46    }
47}
48
49/// A patch that can transform one file into another.
50///
51/// Contains all the information needed to verify and apply a patch,
52/// including source and target file hashes for integrity validation.
53///
54/// # Example
55/// ```rust
56/// use files_diff::{diff, apply, DiffAlgorithm, CompressAlgorithm};
57///
58/// let source = b"original content";
59/// let target = b"modified content";
60///
61/// // Generate a patch
62/// let patch = diff(
63///     source,
64///     target,
65///     DiffAlgorithm::Rsync020,
66///     CompressAlgorithm::Zstd
67/// )?;
68///
69/// // Verify source hash matches
70/// assert_eq!(files_diff::hash(source), patch.before_hash);
71///
72/// // Apply patch and verify result
73/// let result = apply(source, &patch)?;
74/// assert_eq!(files_diff::hash(&result), patch.after_hash);
75/// # Ok::<(), files_diff::Error>(())
76/// ```
77#[derive(Archive, rkyv::Deserialize, rkyv::Serialize, Debug, PartialEq)]
78#[rkyv(derive(Debug))]
79pub struct Patch {
80    /// Algorithm used to generate this patch
81    pub diff_algorithm: DiffAlgorithm,
82    /// Compression method used for the patch data
83    pub compress_algorithm: CompressAlgorithm,
84    /// MD5 hash of the source file
85    pub before_hash: String,
86    /// MD5 hash of the target file
87    pub after_hash: String,
88    /// The actual patch data
89    pub patch: Vec<u8>,
90}
91
92impl Patch {
93    /// Returns the total size in bytes of this patch.
94    pub fn get_size(&self) -> usize {
95        self.patch.len()
96            + self.before_hash.len()
97            + self.after_hash.len()
98            + std::mem::size_of::<CompressAlgorithm>()
99            + std::mem::size_of::<DiffAlgorithm>()
100    }
101
102    /// Serializes this patch to a byte vector.
103    pub fn to_bytes(&self) -> Result<Vec<u8>, Error> {
104        Ok(rkyv::to_bytes::<rkyv::rancor::Error>(self)
105            .map_err(Error::SerializeError)?
106            .to_vec())
107    }
108
109    /// Deserializes a patch from a byte vector.
110    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
111        rkyv::from_bytes::<_, rkyv::rancor::Error>(bytes).map_err(Error::DeserializeError)
112    }
113}
114
115/// Type alias for filenames in patch sets
116pub type Filename = String;
117
118/// Operations that can be performed on a file in a patch set.
119///
120/// Used primarily for zip archive differing to track changes to individual files
121/// within the archive.
122///
123/// # Example
124/// ```no_run
125/// use files_diff::{diff_zip, DiffAlgorithm, CompressAlgorithm};
126///
127/// let patch_set = diff_zip(
128///     "before.zip".to_string(),
129///     "after.zip".to_string(),
130///     DiffAlgorithm::Rsync020,
131///     CompressAlgorithm::Zstd
132/// )?;
133///
134/// # Ok::<(), files_diff::Error>(())
135/// ```
136#[derive(Archive, rkyv::Deserialize, rkyv::Serialize, Debug, PartialEq)]
137#[rkyv(derive(Debug))]
138pub enum Operation {
139    /// File was modified - contains patch to transform it
140    Patch(Patch),
141    /// File is new or completely different - contains full file contents
142    PutFile(Vec<u8>),
143    /// File was removed in the target
144    DeleteFile,
145    /// File is identical in source and target
146    FileStaysSame,
147}
148
149impl Operation {
150    /// Returns the size in bytes of this operation's data.
151    pub fn get_size(&self) -> usize {
152        match self {
153            Operation::Patch(patch) => patch.get_size(),
154            Operation::PutFile(file) => file.len(),
155            Operation::DeleteFile => 0,
156            Operation::FileStaysSame => 0,
157        }
158    }
159}
160
161#[derive(Archive, rkyv::Deserialize, rkyv::Serialize, Debug, PartialEq)]
162#[rkyv(derive(Debug))]
163pub struct Operations(pub(crate) Vec<(Filename, Operation)>);
164
165impl Operations {
166    pub(crate) fn to_bytes(&self) -> Result<Vec<u8>, Error> {
167        Ok(rkyv::to_bytes::<rkyv::rancor::Error>(self)
168            .map_err(Error::SerializeError)?
169            .to_vec())
170    }
171
172    pub(crate) fn hash(&self) -> Result<String, Error> {
173        Ok(hash(&self.to_bytes()?))
174    }
175}
176
177/// A collection of file operations that transform one archive into another.
178///
179/// Contains all the operations needed to transform a zip archive into a
180/// target version, tracking changes to individual files within the archive.
181///
182/// # Example
183/// ```no_run
184/// use files_diff::{diff_zip, apply_zip, DiffAlgorithm, CompressAlgorithm};
185///
186/// // Generate patches for all files in the zip
187/// let patch_set = diff_zip(
188///     "source.zip".to_string(),
189///     "target.zip".to_string(),
190///     DiffAlgorithm::Rsync020,
191///     CompressAlgorithm::Zstd
192/// )?;
193///
194/// // Apply all patches to transform the zip
195/// apply_zip("source.zip", patch_set, "result.zip".to_string())?;
196/// # Ok::<(), files_diff::Error>(())
197/// ```
198#[derive(Archive, rkyv::Deserialize, rkyv::Serialize, Debug, PartialEq)]
199#[rkyv(derive(Debug))]
200pub struct PatchSet {
201    /// The operations that transform the source zip into the target zip
202    pub operations: Operations,
203    /// The hash of the source zip
204    pub hash_before: String,
205    /// The hash of the operations
206    pub operations_hash: String,
207}
208
209impl PatchSet {
210    /// Returns the total size in bytes of all operations in this patch set.
211    pub fn get_size(&self) -> usize {
212        self.operations
213            .0
214            .iter()
215            .map(|(filename, op)| filename.len() + op.get_size())
216            .sum::<usize>()
217            + self.hash_before.len()
218            + self.operations_hash.len()
219    }
220
221    /// Serializes this patch set to a byte vector.
222    pub fn to_bytes(&self) -> Result<Vec<u8>, Error> {
223        Ok(rkyv::to_bytes::<rkyv::rancor::Error>(self)
224            .map_err(Error::SerializeError)?
225            .to_vec())
226    }
227
228    /// Deserializes a patch set from a byte vector.
229    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
230        rkyv::from_bytes::<_, rkyv::rancor::Error>(bytes).map_err(Error::DeserializeError)
231    }
232}