1use std::borrow::Cow;
4
5use super::{Hunk, HunkLine};
6use crate::error::ApplyError;
7
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
10pub struct ApplyOptions {
11 pub reverse: bool,
13}
14
15#[derive(Debug, Clone)]
19struct LineRecord<'a> {
20 raw: Cow<'a, [u8]>,
21 consumed: bool,
23}
24
25#[derive(Debug, Clone)]
27struct WorkingBlob<'a> {
28 rows: Vec<LineRecord<'a>>,
29}
30
31impl<'a> WorkingBlob<'a> {
32 fn decode(data: &'a [u8]) -> Self {
33 let mut rows = Vec::with_capacity(memchr::memchr_iter(b'\n', data).count() + 1);
34 let mut start = 0usize;
35 for i in memchr::memchr_iter(b'\n', data) {
36 rows.push(LineRecord {
37 raw: Cow::Borrowed(&data[start..=i]),
38 consumed: false,
39 });
40 start = i + 1;
41 }
42 if start < data.len() {
43 rows.push(LineRecord {
44 raw: Cow::Borrowed(&data[start..]),
45 consumed: false,
46 });
47 }
48 Self { rows }
49 }
50
51 fn encode(&self) -> Vec<u8> {
52 let mut out = Vec::with_capacity(self.rows.iter().map(|row| row.raw.len()).sum());
53 for row in &self.rows {
54 out.extend_from_slice(&row.raw);
55 }
56 out
57 }
58
59 fn len(&self) -> usize {
60 self.rows.len()
61 }
62}
63
64#[derive(Debug, Clone)]
66struct LineEdit {
67 match_rows: Vec<Vec<u8>>,
68 replace_rows: Vec<Vec<u8>>,
69 stable_prefix: usize,
70 stable_suffix: usize,
71}
72
73#[derive(Debug, Clone, Copy)]
75struct ScanHint {
76 start_row: usize,
78 only_at_front: bool,
80 only_at_back: bool,
82}
83
84pub fn apply_hunks_to_blob(
91 original: &[u8],
92 hunks: &[Hunk],
93 opts: &ApplyOptions,
94) -> Result<Vec<u8>, ApplyError> {
95 let mut working = WorkingBlob::decode(original);
96 for (hunk_index, hunk) in hunks.iter().enumerate() {
97 let edit = line_edit_from_hunk(hunk, opts.reverse);
98 let insert_only = if opts.reverse {
99 hunk.new_count == 0
100 } else {
101 hunk.old_count == 0
102 };
103 let hint = scan_hint_for_hunk(hunk, &edit, opts.reverse);
104 let Some(at) = locate_and_apply(&mut working, &edit, hint, hunk, hunk_index, insert_only)?
105 else {
106 return Err(ApplyError::ContextMismatch {
107 hunk_index,
108 expected_line: if opts.reverse {
109 hunk.old_start
110 } else {
111 hunk.new_start
112 },
113 });
114 };
115 let _ = at;
116 }
117 Ok(working.encode())
118}
119
120fn scan_hint_for_hunk(hunk: &Hunk, edit: &LineEdit, reverse: bool) -> ScanHint {
121 let (header_line, only_at_front) = if reverse {
122 (hunk.old_start, hunk.new_start <= 1)
123 } else {
124 (hunk.new_start, hunk.old_start <= 1)
125 };
126 let start_row = if header_line == 0 {
127 0
128 } else {
129 header_line.saturating_sub(1)
130 };
131 ScanHint {
132 start_row,
133 only_at_front,
134 only_at_back: edit.stable_suffix == 0 && edit.stable_prefix > 0,
139 }
140}
141
142fn line_edit_from_hunk(hunk: &Hunk, reverse: bool) -> LineEdit {
143 let mut match_rows = Vec::new();
144 let mut replace_rows = Vec::new();
145 let mut stable_prefix = 0usize;
146 let mut stable_suffix = 0usize;
147 let mut in_change = false;
148
149 let mut i = 0usize;
150 while i < hunk.lines.len() {
151 let (tag, text) = match &hunk.lines[i] {
152 HunkLine::Context(s) => ("ctx", s.as_str()),
153 HunkLine::Add(s) => ("add", s.as_str()),
154 HunkLine::Remove(s) => ("del", s.as_str()),
155 HunkLine::NoNewline => {
156 i += 1;
157 continue;
158 }
159 };
160 let tag = if reverse {
161 match tag {
162 "add" => "del",
163 "del" => "add",
164 other => other,
165 }
166 } else {
167 tag
168 };
169
170 let with_nl = !matches!(hunk.lines.get(i + 1), Some(HunkLine::NoNewline));
171 let row = hunk_text_as_row(text, with_nl);
172
173 match tag {
174 "ctx" => {
175 if !in_change {
176 stable_prefix += 1;
177 }
178 stable_suffix += 1;
179 match_rows.push(row.clone());
180 replace_rows.push(row);
181 }
182 "del" => {
183 in_change = true;
184 stable_suffix = 0;
185 match_rows.push(row);
186 }
187 "add" => {
188 in_change = true;
189 stable_suffix = 0;
190 replace_rows.push(row);
191 }
192 _ => {}
193 }
194 i += 1;
195 }
196
197 if !in_change {
198 stable_suffix = stable_prefix;
199 }
200
201 LineEdit {
202 match_rows,
203 replace_rows,
204 stable_prefix,
205 stable_suffix,
206 }
207}
208
209fn hunk_text_as_row(text: &str, with_newline: bool) -> Vec<u8> {
210 let mut row = text.as_bytes().to_vec();
211 if with_newline {
212 row.push(b'\n');
213 }
214 row
215}
216
217fn locate_and_apply(
218 working: &mut WorkingBlob,
219 edit: &LineEdit,
220 hint: ScanHint,
221 hunk: &Hunk,
222 _hunk_index: usize,
223 insert_only: bool,
224) -> Result<Option<usize>, ApplyError> {
225 let mut candidate = edit.clone();
226 let mut hint = hint;
227 let mut prefix = edit.stable_prefix;
228 let mut suffix = edit.stable_suffix;
229 let mut pin_front = hint.only_at_front;
230 let mut pin_back = hint.only_at_back;
231
232 loop {
233 if candidate.match_rows.is_empty() {
234 if insert_only && !candidate.replace_rows.is_empty() {
235 let row = insert_only_row(hint, working, hunk);
236 splice_edit(working, row, &candidate);
237 return Ok(Some(row));
238 }
239 if prefix == 0 && suffix == 0 {
240 return Ok(None);
241 }
242 } else if let Some(row) =
243 search_outward(working, &candidate.match_rows, hint, pin_front, pin_back)
244 {
245 splice_edit(working, row, &candidate);
246 return Ok(Some(row));
247 }
248
249 if pin_front || pin_back {
250 pin_front = false;
251 pin_back = false;
252 continue;
253 }
254 if prefix == 0 && suffix == 0 {
255 return Ok(None);
256 }
257 if prefix >= suffix {
258 trim_edit_front(&mut candidate);
259 prefix -= 1;
260 hint.start_row = hint.start_row.saturating_sub(1);
261 }
262 if suffix > prefix {
263 trim_edit_back(&mut candidate);
264 suffix -= 1;
265 }
266 }
267}
268
269fn trim_edit_front(edit: &mut LineEdit) {
270 if !edit.match_rows.is_empty() {
271 edit.match_rows.remove(0);
272 }
273 if !edit.replace_rows.is_empty() {
274 edit.replace_rows.remove(0);
275 }
276}
277
278fn trim_edit_back(edit: &mut LineEdit) {
279 if !edit.match_rows.is_empty() {
280 edit.match_rows.pop();
281 }
282 if !edit.replace_rows.is_empty() {
283 edit.replace_rows.pop();
284 }
285}
286
287fn insert_only_row(hint: ScanHint, working: &WorkingBlob, hunk: &Hunk) -> usize {
288 let len = working.len();
289 let at = hint.start_row.min(len);
290 if hint.only_at_back
291 && hunk.old_count == 0
292 && hunk.old_start > 0
293 && hunk.old_start >= len
294 && hunk.new_start >= hunk.old_start
295 {
296 return len;
297 }
298 at
299}
300
301fn search_outward(
302 working: &WorkingBlob,
303 pattern: &[Vec<u8>],
304 hint: ScanHint,
305 pin_front: bool,
306 pin_back: bool,
307) -> Option<usize> {
308 let width = pattern.len();
309 debug_assert!(!pattern.is_empty());
310 let mut anchor = hint.start_row.min(working.len());
311 if pin_front {
312 anchor = 0;
313 } else if pin_back {
314 anchor = working.len().saturating_sub(width);
315 }
316
317 let mut low = anchor;
318 let mut high = anchor;
319 let mut cursor = anchor;
320
321 let attempts = working.len().saturating_add(width).saturating_add(1);
322 for step in 0..attempts {
323 if rows_equal_pattern(working, cursor, pattern, pin_front, pin_back) {
324 return Some(cursor);
325 }
326
327 let can_low = low > 0;
328 let can_high = high < working.len();
329 if !can_low && !can_high {
330 break;
331 }
332
333 if step & 1 == 1 {
334 if can_low {
335 low -= 1;
336 cursor = low;
337 }
338 } else if can_high {
339 high += 1;
340 cursor = high;
341 }
342 }
343 None
344}
345
346fn rows_equal_pattern(
347 working: &WorkingBlob,
348 at: usize,
349 pattern: &[Vec<u8>],
350 pin_front: bool,
351 pin_back: bool,
352) -> bool {
353 if pin_front && at != 0 {
354 return false;
355 }
356 if pin_back && at + pattern.len() != working.len() {
357 return false;
358 }
359 if at + pattern.len() > working.len() {
360 return false;
361 }
362 for (offset, expected) in pattern.iter().enumerate() {
363 let row = &working.rows[at + offset];
364 if row.consumed {
365 return false;
366 }
367 if row.raw.as_ref() != expected.as_slice() {
368 return false;
369 }
370 }
371 true
372}
373
374fn splice_edit(working: &mut WorkingBlob, at: usize, edit: &LineEdit) {
375 let remove = edit
376 .match_rows
377 .len()
378 .min(working.rows.len().saturating_sub(at));
379 let new_rows = edit.replace_rows.iter().map(|raw| LineRecord {
380 raw: Cow::Owned(raw.clone()),
381 consumed: true,
382 });
383 working.rows.splice(at..at + remove, new_rows);
384}