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}