1use std::borrow::Cow;
4
5use super::{BinaryPatchPayload, FilePatch, Hunk, HunkLine};
6use crate::error::{ApplyError, Error};
7
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
10pub struct BlobApplyOptions {
11 pub allow_binary_marker_only: bool,
13 pub reverse: bool,
15}
16
17pub fn apply_patch_to_blob(
22 base: &[u8],
23 patch: &FilePatch,
24 opts: &BlobApplyOptions,
25) -> std::result::Result<Vec<u8>, ApplyError> {
26 if patch.is_deleted {
27 let path = patch.effective_path().unwrap_or("<unknown>").to_string();
28 if opts.reverse {
29 return super::apply_hunks_to_blob(
30 base,
31 &patch.hunks,
32 &super::ApplyOptions { reverse: true },
33 )
34 .map_err(|e| match e {
35 ApplyError::PatchFailed { path, line } => ApplyError::PatchFailed { path, line },
36 other => other,
37 });
38 }
39 let result = apply_hunks_to_blob(base, &patch.hunks, &path)?;
40 if !result.is_empty() {
41 return Err(ApplyError::Failed {
42 path,
43 message: "deletion patch did not remove all index content".to_string(),
44 });
45 }
46 return Ok(Vec::new());
47 }
48 if let Some(payload) = patch.binary_patch.as_ref() {
49 return materialize_binary(base, payload);
50 }
51 if patch.is_binary {
52 if opts.allow_binary_marker_only {
53 return Ok(base.to_vec());
54 }
55 let path = patch.effective_path().unwrap_or("<unknown>").to_string();
56 return Err(ApplyError::BinaryDataMissing { path });
57 }
58 if patch.hunks.is_empty() {
59 return Ok(base.to_vec());
60 }
61 let path = patch.effective_path().unwrap_or("<unknown>").to_string();
62 if opts.reverse {
63 return super::apply_hunks_to_blob(
64 base,
65 &patch.hunks,
66 &super::ApplyOptions { reverse: true },
67 )
68 .map_err(|e| match e {
69 ApplyError::PatchFailed { path, line } => ApplyError::PatchFailed { path, line },
70 other => other,
71 });
72 }
73 apply_hunks_to_blob(base, &patch.hunks, &path)
74}
75
76fn materialize_binary(
77 base: &[u8],
78 payload: &BinaryPatchPayload,
79) -> std::result::Result<Vec<u8>, ApplyError> {
80 super::materialize_binary_patch_forward(payload, base).map_err(|e| match e {
81 Error::Apply(a) => a,
82 other => ApplyError::Failed {
83 path: String::new(),
84 message: other.to_string(),
85 },
86 })
87}
88
89type Line<'a> = Cow<'a, [u8]>;
91
92fn apply_hunks_to_blob(
93 base: &[u8],
94 hunks: &[Hunk],
95 path: &str,
96) -> std::result::Result<Vec<u8>, ApplyError> {
97 let mut lines = split_blob_lines(base);
98 let mut ordered: Vec<&Hunk> = hunks.iter().collect();
99 ordered.sort_by(|a, b| {
100 b.old_start
101 .cmp(&a.old_start)
102 .then(b.old_count.cmp(&a.old_count))
103 });
104
105 for hunk in ordered {
106 apply_one_hunk(&mut lines, hunk, path)?;
107 }
108 Ok(join_blob_lines(&lines))
109}
110
111fn apply_one_hunk<'a>(
112 lines: &mut Vec<Line<'a>>,
113 hunk: &'a Hunk,
114 path: &str,
115) -> std::result::Result<(), ApplyError> {
116 let (old_side, new_side) = hunk_old_new(hunk);
117 if old_side.is_empty() {
118 let insert_at = if hunk.old_start == 0 {
119 0
120 } else {
121 hunk.old_start.saturating_sub(1).min(lines.len())
122 };
123 lines.splice(insert_at..insert_at, new_side);
124 return Ok(());
125 }
126
127 let pos =
128 find_hunk_match(lines, &old_side, hunk.old_start.saturating_sub(1)).ok_or_else(|| {
129 ApplyError::PatchFailed {
130 path: path.to_string(),
131 line: hunk.first_body_line,
132 }
133 })?;
134 lines.splice(pos..pos.saturating_add(old_side.len()), new_side);
135 Ok(())
136}
137
138fn hunk_old_new(hunk: &Hunk) -> (Vec<Line<'_>>, Vec<Line<'_>>) {
139 let mut old_side = Vec::new();
140 let mut new_side = Vec::new();
141 let body = &hunk.lines;
142 for (idx, line) in body.iter().enumerate() {
143 if matches!(line, HunkLine::NoNewline) {
144 continue;
145 }
146 let with_newline = !matches!(body.get(idx + 1), Some(HunkLine::NoNewline));
147 match line {
148 HunkLine::Context(text) => {
149 let bytes = hunk_line_bytes(text, with_newline);
150 old_side.push(bytes.clone());
151 new_side.push(bytes);
152 }
153 HunkLine::Remove(text) => old_side.push(hunk_line_bytes(text, with_newline)),
154 HunkLine::Add(text) => new_side.push(hunk_line_bytes(text, with_newline)),
155 HunkLine::NoNewline => {}
156 }
157 }
158 (old_side, new_side)
159}
160
161fn hunk_line_bytes(text: &str, with_newline: bool) -> Line<'_> {
162 let bytes = text.as_bytes();
163 if !with_newline || bytes.last() == Some(&b'\n') {
164 return Cow::Borrowed(bytes);
165 }
166 let mut owned = Vec::with_capacity(bytes.len() + 1);
167 owned.extend_from_slice(bytes);
168 owned.push(b'\n');
169 Cow::Owned(owned)
170}
171
172fn find_hunk_match(lines: &[Line<'_>], old_side: &[Line<'_>], hint: usize) -> Option<usize> {
173 if old_side.is_empty() {
174 return Some(hint.min(lines.len()));
175 }
176 let search_from = hint.min(lines.len().saturating_sub(old_side.len()));
177 if lines
178 .get(search_from..search_from.saturating_add(old_side.len()))
179 .is_some_and(|window| window == old_side)
180 {
181 return Some(search_from);
182 }
183 for pos in 0..=lines.len().saturating_sub(old_side.len()) {
184 if lines[pos..pos + old_side.len()] == *old_side {
185 return Some(pos);
186 }
187 }
188 None
189}
190
191fn split_blob_lines(data: &[u8]) -> Vec<Line<'_>> {
192 data.split_inclusive(|&b| b == b'\n')
193 .map(Cow::Borrowed)
194 .collect()
195}
196
197fn join_blob_lines(lines: &[Line<'_>]) -> Vec<u8> {
198 let mut out = Vec::with_capacity(lines.iter().map(|l| l.len()).sum());
199 for line in lines {
200 out.extend_from_slice(line);
201 }
202 out
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::apply::parse_patch;
209
210 #[test]
211 fn blob_apply_single_line_change() {
212 let patch_text = "\
213diff --git a/a.txt b/a.txt
214--- a/a.txt
215+++ b/a.txt
216@@ -1 +1 @@
217-one
218+two
219";
220 let patches = parse_patch(patch_text, 1, "p", false, None).expect("parse");
221 let got = apply_patch_to_blob(b"one\n", &patches[0], &BlobApplyOptions::default())
222 .expect("apply");
223 assert_eq!(got, b"two\n");
224 }
225
226 #[test]
227 fn blob_apply_no_newline_preimage() {
228 let patch_text = "\
229diff --git a/f.txt b/f.txt
230--- a/f.txt
231+++ b/f.txt
232@@ -1 +1 @@
233-old
234\\ No newline at end of file
235+new
236";
237 let patches = parse_patch(patch_text, 1, "p", false, None).expect("parse");
238 let got =
239 apply_patch_to_blob(b"old", &patches[0], &BlobApplyOptions::default()).expect("apply");
240 assert_eq!(got, b"new\n");
241 }
242
243 #[test]
244 fn blob_apply_hunks_that_grow_and_shrink_the_image() {
245 let base = b"a\nb\nc\nd\ne\nf\ng\nh\n";
246 let patch_text = "\
247diff --git a/f.txt b/f.txt
248--- a/f.txt
249+++ b/f.txt
250@@ -1,2 +1,3 @@
251 a
252+a2
253 b
254@@ -6,3 +7,2 @@
255 f
256-g
257 h
258";
259 let patches = parse_patch(patch_text, 1, "p", false, None).expect("parse");
260 let got =
261 apply_patch_to_blob(base, &patches[0], &BlobApplyOptions::default()).expect("apply");
262 assert_eq!(got, b"a\na2\nb\nc\nd\ne\nf\nh\n");
263 }
264
265 #[test]
266 fn blob_apply_adds_missing_final_newline() {
267 let patch_text = "\
268diff --git a/f.txt b/f.txt
269--- a/f.txt
270+++ b/f.txt
271@@ -1,2 +1,2 @@
272 keep
273-tail
274\\ No newline at end of file
275+tail
276";
277 let patches = parse_patch(patch_text, 1, "p", false, None).expect("parse");
278 let got = apply_patch_to_blob(b"keep\ntail", &patches[0], &BlobApplyOptions::default())
279 .expect("apply");
280 assert_eq!(got, b"keep\ntail\n");
281 }
282
283 #[test]
284 fn blob_apply_finds_hunk_away_from_its_header_line() {
285 let patch_text = "\
286diff --git a/f.txt b/f.txt
287--- a/f.txt
288+++ b/f.txt
289@@ -1 +1 @@
290-x
291+y
292";
293 let patches = parse_patch(patch_text, 1, "p", false, None).expect("parse");
294 let got = apply_patch_to_blob(b"p\nq\nx\n", &patches[0], &BlobApplyOptions::default())
295 .expect("apply");
296 assert_eq!(got, b"p\nq\ny\n");
297 let err = apply_patch_to_blob(b"p\nq\n", &patches[0], &BlobApplyOptions::default())
298 .expect_err("no match");
299 assert!(matches!(err, ApplyError::PatchFailed { .. }), "{err:?}");
300 }
301
302 #[test]
303 fn split_and_join_round_trip_without_copying_lines() {
304 for data in [
305 &b""[..],
306 b"\n",
307 b"one",
308 b"one\ntwo",
309 b"one\ntwo\n",
310 b"\n\nx\n",
311 ] {
312 let lines = split_blob_lines(data);
313 assert!(lines.iter().all(|l| matches!(l, Cow::Borrowed(_))));
314 assert_eq!(join_blob_lines(&lines), data);
315 }
316 assert_eq!(split_blob_lines(b"a\nb").len(), 2);
317 }
318}