1use sley_core::{GitError, Result};
4
5use crate::{name, ws};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum HunkLine {
25 Context(Vec<u8>),
27 Insert(Vec<u8>),
29 Delete(Vec<u8>),
31}
32
33impl HunkLine {
34 pub fn content(&self) -> &[u8] {
36 match self {
37 Self::Context(bytes) | Self::Insert(bytes) | Self::Delete(bytes) => bytes,
38 }
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct Hunk {
50 pub old_start: usize,
51 pub old_len: usize,
52 pub new_start: usize,
53 pub new_len: usize,
54 pub lines: Vec<HunkLine>,
55 pub old_no_newline: bool,
57 pub new_no_newline: bool,
59 pub line_input_lines: Vec<usize>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct FilePatch {
68 pub old_path: Option<Vec<u8>>,
70 pub new_path: Option<Vec<u8>>,
72 pub old_mode: Option<u32>,
74 pub new_mode: Option<u32>,
76 pub hunks: Vec<Hunk>,
77 pub is_new: bool,
79 pub is_delete: bool,
81 pub is_rename: bool,
83 pub is_copy: bool,
85 pub similarity: Option<u8>,
87 pub dissimilarity: Option<u8>,
89 pub old_oid_hex: Option<Vec<u8>>,
93 pub new_oid_hex: Option<Vec<u8>>,
94 pub is_binary: bool,
98 pub binary: Option<BinaryPatch>,
103 pub is_toplevel_relative: bool,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct BinaryPatch {
115 pub forward: BinaryHunk,
116 pub reverse: Option<BinaryHunk>,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct BinaryHunk {
123 pub method: BinaryMethod,
124 pub origlen: usize,
127 pub deflated: Vec<u8>,
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum BinaryMethod {
134 Literal,
136 Delta,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
142pub enum ApplyOutcome {
143 Applied(Vec<u8>),
145 Rejected,
147}
148
149const MIN_FUZZ_CONTEXT: usize = usize::MAX;
160
161pub fn parse_unified_patch(input: &[u8]) -> Result<Vec<FilePatch>> {
169 parse_unified_patch_with_recount(input, false)
170}
171
172pub fn parse_unified_patch_with_recount(input: &[u8], recount: bool) -> Result<Vec<FilePatch>> {
175 parse_unified_patch_with_options(input, recount, &PatchPathOptions::default())
176}
177
178#[derive(Clone)]
181pub struct PatchPathOptions {
182 pub p_value: usize,
184 pub p_value_known: bool,
187 pub root: Vec<u8>,
189 pub prefix: Vec<u8>,
194}
195
196impl Default for PatchPathOptions {
197 fn default() -> Self {
198 PatchPathOptions {
199 p_value: 1,
200 p_value_known: false,
201 root: Vec::new(),
202 prefix: Vec::new(),
203 }
204 }
205}
206
207pub fn parse_unified_patch_with_options(
210 input: &[u8],
211 recount: bool,
212 options: &PatchPathOptions,
213) -> Result<Vec<FilePatch>> {
214 let lines = split_patch_lines(input);
215 let mut parser = PatchParser {
216 lines: &lines,
217 index: 0,
218 recount,
219 p_value: options.p_value,
220 p_value_known: options.p_value_known,
221 root: options.root.clone(),
222 prefix: options.prefix.clone(),
223 };
224 parser.parse()
225}
226
227pub fn apply_file_patch(base: &[u8], patch: &FilePatch) -> ApplyOutcome {
252 apply_file_patch_with_options(base, patch, &ApplyFileOptions::default())
253}
254
255#[derive(Clone, Default)]
258pub struct ApplyFileOptions {
259 pub unidiff_zero: bool,
262}
263
264pub fn reverse_file_patch(patch: &FilePatch) -> FilePatch {
268 let hunks = patch
269 .hunks
270 .iter()
271 .map(|hunk| {
272 let lines = hunk
273 .lines
274 .iter()
275 .map(|line| match line {
276 HunkLine::Context(b) => HunkLine::Context(b.clone()),
277 HunkLine::Insert(b) => HunkLine::Delete(b.clone()),
278 HunkLine::Delete(b) => HunkLine::Insert(b.clone()),
279 })
280 .collect();
281 Hunk {
282 old_start: hunk.new_start,
283 old_len: hunk.new_len,
284 new_start: hunk.old_start,
285 new_len: hunk.old_len,
286 lines,
287 old_no_newline: hunk.new_no_newline,
288 new_no_newline: hunk.old_no_newline,
289 line_input_lines: hunk.line_input_lines.clone(),
292 }
293 })
294 .collect();
295 let (old_mode, new_mode) = if patch.new_mode.is_some() || patch.is_delete {
299 (patch.new_mode, patch.old_mode)
300 } else {
301 (patch.old_mode, patch.new_mode)
302 };
303 FilePatch {
304 old_path: patch.new_path.clone(),
305 new_path: patch.old_path.clone(),
306 old_mode,
307 new_mode,
308 hunks,
309 is_new: patch.is_delete,
310 is_delete: patch.is_new,
311 is_rename: patch.is_rename,
312 is_copy: patch.is_copy,
313 similarity: patch.similarity,
314 dissimilarity: patch.dissimilarity,
315 old_oid_hex: patch.new_oid_hex.clone(),
318 new_oid_hex: patch.old_oid_hex.clone(),
319 is_binary: patch.is_binary,
320 binary: patch.binary.as_ref().map(|binary| BinaryPatch {
321 forward: binary
323 .reverse
324 .clone()
325 .unwrap_or_else(|| binary.forward.clone()),
326 reverse: Some(binary.forward.clone()),
327 }),
328 is_toplevel_relative: patch.is_toplevel_relative,
329 }
330}
331
332pub fn apply_file_patch_with_options(
334 base: &[u8],
335 patch: &FilePatch,
336 options: &ApplyFileOptions,
337) -> ApplyOutcome {
338 if patch.is_delete && patch.hunks.is_empty() {
340 return ApplyOutcome::Applied(Vec::new());
341 }
342 let base_for_match: &[u8] = if patch.is_new { b"" } else { base };
345
346 let mut image = split_blob_lines(base_for_match);
350
351 let mut running_offset: isize = 0;
354
355 for hunk in &patch.hunks {
356 match apply_one_hunk(&mut image, hunk, running_offset, options.unidiff_zero) {
357 Some(drift) => running_offset += drift,
358 None => return ApplyOutcome::Rejected,
359 }
360 }
361
362 ApplyOutcome::Applied(join_lines(&image))
363}
364
365#[derive(Debug, Clone, PartialEq, Eq)]
367pub struct RejectApply {
368 pub content: Vec<u8>,
370 pub rejected: Vec<usize>,
372}
373
374pub fn apply_file_patch_rejecting(
383 base: &[u8],
384 patch: &FilePatch,
385 options: &ApplyFileOptions,
386) -> RejectApply {
387 if patch.is_delete && patch.hunks.is_empty() {
388 return RejectApply {
389 content: Vec::new(),
390 rejected: Vec::new(),
391 };
392 }
393 let base_for_match: &[u8] = if patch.is_new { b"" } else { base };
394 let mut image = split_blob_lines(base_for_match);
395 let mut running_offset: isize = 0;
396 let mut rejected = Vec::new();
397 for (index, hunk) in patch.hunks.iter().enumerate() {
398 match apply_one_hunk(&mut image, hunk, running_offset, options.unidiff_zero) {
399 Some(drift) => running_offset += drift,
400 None => rejected.push(index),
401 }
402 }
403 RejectApply {
404 content: join_lines(&image),
405 rejected,
406 }
407}
408
409pub fn render_reject_hunk(hunk: &Hunk) -> Vec<u8> {
415 fn range(start: usize, count: usize) -> String {
416 if count == 1 {
417 start.to_string()
418 } else {
419 format!("{start},{count}")
420 }
421 }
422 let mut out = Vec::new();
423 out.extend_from_slice(b"@@ -");
424 out.extend_from_slice(range(hunk.old_start, hunk.old_len).as_bytes());
425 out.extend_from_slice(b" +");
426 out.extend_from_slice(range(hunk.new_start, hunk.new_len).as_bytes());
427 out.extend_from_slice(b" @@\n");
428 let last_old = hunk
431 .lines
432 .iter()
433 .rposition(|line| matches!(line, HunkLine::Context(_) | HunkLine::Delete(_)));
434 let last_new = hunk
435 .lines
436 .iter()
437 .rposition(|line| matches!(line, HunkLine::Context(_) | HunkLine::Insert(_)));
438 for (index, line) in hunk.lines.iter().enumerate() {
439 let (prefix, content) = match line {
440 HunkLine::Context(bytes) => (b' ', bytes),
441 HunkLine::Insert(bytes) => (b'+', bytes),
442 HunkLine::Delete(bytes) => (b'-', bytes),
443 };
444 out.push(prefix);
445 out.extend_from_slice(content);
446 out.push(b'\n');
447 let old_incomplete = hunk.old_no_newline && Some(index) == last_old;
448 let new_incomplete = hunk.new_no_newline && Some(index) == last_new;
449 if old_incomplete || new_incomplete {
450 out.extend_from_slice(b"\\ No newline at end of file\n");
451 }
452 }
453 out
454}
455
456#[derive(Clone, Copy)]
471pub struct WsApplyOptions {
472 pub unidiff_zero: bool,
474 pub ws_rule: ws::WsRule,
476 pub ws_fix: bool,
478 pub ws_ignore_change: bool,
480}
481
482pub enum WsApplyOutcome {
484 Applied {
486 content: Vec<u8>,
487 blank_at_eof_removed: usize,
488 },
489 Rejected,
491}
492
493#[derive(Clone)]
496struct WsImageLine {
497 content: Vec<u8>,
498 no_newline: bool,
499 common: bool,
500}
501
502impl WsImageLine {
503 fn bytes(&self) -> Vec<u8> {
504 let mut out = self.content.clone();
505 if !self.no_newline {
506 out.push(b'\n');
507 }
508 out
509 }
510}
511
512fn line_bytes(line: &Line) -> Vec<u8> {
513 let mut out = line.content.clone();
514 if !line.no_newline {
515 out.push(b'\n');
516 }
517 out
518}
519
520fn ws_line_from_bytes(bytes: Vec<u8>, common: bool) -> WsImageLine {
523 if bytes.last() == Some(&b'\n') {
524 WsImageLine {
525 content: bytes[..bytes.len() - 1].to_vec(),
526 no_newline: false,
527 common,
528 }
529 } else {
530 WsImageLine {
531 content: bytes,
532 no_newline: true,
533 common,
534 }
535 }
536}
537
538pub fn apply_file_patch_ws(
540 base: &[u8],
541 patch: &FilePatch,
542 opts: &WsApplyOptions,
543) -> WsApplyOutcome {
544 if patch.is_delete && patch.hunks.is_empty() {
545 return WsApplyOutcome::Applied {
546 content: Vec::new(),
547 blank_at_eof_removed: 0,
548 };
549 }
550 let base_for_match: &[u8] = if patch.is_new { b"" } else { base };
551 let mut image = split_blob_lines(base_for_match);
552 let mut running_offset: isize = 0;
553 let mut blank_removed = 0usize;
554 for hunk in &patch.hunks {
555 match apply_one_fragment_ws(&mut image, hunk, running_offset, opts, &mut blank_removed) {
556 Some(drift) => running_offset += drift,
557 None => return WsApplyOutcome::Rejected,
558 }
559 }
560 WsApplyOutcome::Applied {
561 content: join_lines(&image),
562 blank_at_eof_removed: blank_removed,
563 }
564}
565
566fn apply_one_fragment_ws(
567 image: &mut Vec<Line>,
568 hunk: &Hunk,
569 running_offset: isize,
570 opts: &WsApplyOptions,
571 blank_removed: &mut usize,
572) -> Option<isize> {
573 let blank_eof = opts.ws_rule & ws::WS_BLANK_AT_EOF != 0;
574 let mut preimage: Vec<WsImageLine> = Vec::new();
575 let mut postimage: Vec<WsImageLine> = Vec::new();
576 let mut leading = 0usize;
577 let mut trailing = 0usize;
578 let mut seen_change = false;
579 let mut new_blank_lines_at_end = 0usize;
582 for hl in &hunk.lines {
583 let mut added_blank_line = false;
584 let mut is_blank_context = false;
585 match hl {
586 HunkLine::Context(bytes) => {
587 if blank_eof && ws::ws_blank_line(bytes) {
588 is_blank_context = true;
589 }
590 preimage.push(WsImageLine {
591 content: bytes.clone(),
592 no_newline: false,
593 common: true,
594 });
595 postimage.push(WsImageLine {
596 content: bytes.clone(),
597 no_newline: false,
598 common: true,
599 });
600 if !seen_change {
601 leading += 1;
602 }
603 trailing += 1;
604 }
605 HunkLine::Delete(bytes) => {
606 preimage.push(WsImageLine {
607 content: bytes.clone(),
608 no_newline: false,
609 common: false,
610 });
611 seen_change = true;
612 trailing = 0;
613 }
614 HunkLine::Insert(bytes) => {
615 postimage.push(WsImageLine {
616 content: bytes.clone(),
617 no_newline: false,
618 common: false,
619 });
620 if blank_eof && ws::ws_blank_line(bytes) {
621 added_blank_line = true;
622 }
623 seen_change = true;
624 trailing = 0;
625 }
626 }
627 if added_blank_line {
628 new_blank_lines_at_end += 1;
629 } else if is_blank_context {
630 } else {
632 new_blank_lines_at_end = 0;
633 }
634 }
635 if hunk.old_no_newline
636 && let Some(last) = preimage.last_mut()
637 {
638 last.no_newline = true;
639 }
640 if hunk.new_no_newline
641 && let Some(last) = postimage.last_mut()
642 {
643 last.no_newline = true;
644 }
645
646 let mut match_beginning = hunk.old_start == 0 || (hunk.old_start == 1 && !opts.unidiff_zero);
647 let mut match_end = !opts.unidiff_zero && trailing == 0;
648
649 let mut expected = if preimage.is_empty() {
650 new_side_position(hunk, running_offset)
651 } else {
652 expected_position(hunk, running_offset)
653 };
654 let hunk_expected = expected;
655 let mut leading_v = leading;
656 let mut trailing_v = trailing;
657
658 let applied_pos = loop {
659 if let Some(pos) = find_pos_ws(
660 image,
661 &mut preimage,
662 &mut postimage,
663 expected,
664 opts,
665 match_beginning,
666 match_end,
667 ) {
668 break pos;
669 }
670 #[allow(clippy::absurd_extreme_comparisons)]
671 if leading_v <= MIN_FUZZ_CONTEXT && trailing_v <= MIN_FUZZ_CONTEXT {
672 return None;
673 }
674 if match_beginning || match_end {
675 match_beginning = false;
676 match_end = false;
677 continue;
678 }
679 if leading_v >= trailing_v {
680 preimage.remove(0);
681 postimage.remove(0);
682 expected -= 1;
683 leading_v -= 1;
684 }
685 if trailing_v > leading_v {
686 preimage.pop();
687 postimage.pop();
688 trailing_v -= 1;
689 }
690 };
691
692 if new_blank_lines_at_end > 0
695 && preimage.len() + applied_pos >= image.len()
696 && blank_eof
697 && opts.ws_fix
698 {
699 for _ in 0..new_blank_lines_at_end {
700 postimage.pop();
701 }
702 *blank_removed += new_blank_lines_at_end;
703 }
704
705 let preimage_limit = preimage.len().min(image.len() - applied_pos);
708 let replacement: Vec<Line> = postimage
709 .iter()
710 .map(|line| Line {
711 content: line.content.clone(),
712 no_newline: line.no_newline,
713 })
714 .collect();
715 image.splice(applied_pos..applied_pos + preimage_limit, replacement);
716 Some(applied_pos as isize - hunk_expected)
717}
718
719fn find_pos_ws(
723 image: &[Line],
724 preimage: &mut Vec<WsImageLine>,
725 postimage: &mut Vec<WsImageLine>,
726 expected: isize,
727 opts: &WsApplyOptions,
728 match_beginning: bool,
729 match_end: bool,
730) -> Option<usize> {
731 let line_nr = image.len();
732 let pre_nr = preimage.len();
733 let mut line: isize = if match_beginning {
734 0
735 } else if match_end {
736 line_nr as isize - pre_nr as isize
737 } else {
738 expected
739 };
740 if line < 0 {
741 line = 0;
742 }
743 if line as usize > line_nr {
744 line = line_nr as isize;
745 }
746 let start = line as usize;
747 let mut backwards = start;
748 let mut forwards = start;
749 let mut current = start;
750 let mut i: u64 = 0;
751 loop {
752 if match_fragment_ws(
753 image,
754 preimage,
755 postimage,
756 current,
757 opts,
758 match_beginning,
759 match_end,
760 ) {
761 return Some(current);
762 }
763 loop {
764 if backwards == 0 && forwards == line_nr {
765 return None;
766 }
767 if i & 1 == 1 {
768 if backwards == 0 {
769 i += 1;
770 continue;
771 }
772 backwards -= 1;
773 current = backwards;
774 } else {
775 if forwards == line_nr {
776 i += 1;
777 continue;
778 }
779 forwards += 1;
780 current = forwards;
781 }
782 break;
783 }
784 i += 1;
785 }
786}
787
788fn match_fragment_ws(
794 image: &[Line],
795 preimage: &mut Vec<WsImageLine>,
796 postimage: &mut Vec<WsImageLine>,
797 current_lno: usize,
798 opts: &WsApplyOptions,
799 match_beginning: bool,
800 match_end: bool,
801) -> bool {
802 let blank_eof = opts.ws_rule & ws::WS_BLANK_AT_EOF != 0;
803 let preimage_limit: usize;
804 if preimage.len() + current_lno <= image.len() {
805 preimage_limit = preimage.len();
806 if match_end && (preimage.len() + current_lno != image.len()) {
807 return false;
808 }
809 } else if opts.ws_fix && blank_eof {
810 preimage_limit = image.len() - current_lno;
813 } else {
814 return false;
815 }
816
817 if match_beginning && current_lno != 0 {
818 return false;
819 }
820
821 if preimage_limit == preimage.len() {
822 let mut exact = true;
824 if match_end && current_lno + preimage_limit != image.len() {
825 exact = false;
826 }
827 if exact {
828 for i in 0..preimage_limit {
829 let img = &image[current_lno + i];
830 let pre = &preimage[i];
831 if img.content != pre.content || img.no_newline != pre.no_newline {
832 exact = false;
833 break;
834 }
835 }
836 }
837 if exact {
838 return true;
839 }
840 } else {
841 let mut all_blank = true;
844 for line in preimage.iter().take(preimage_limit) {
845 if !line.content.iter().all(|&b| ws::is_space(b)) {
846 all_blank = false;
847 break;
848 }
849 }
850 if all_blank {
851 return false;
852 }
853 }
854
855 if opts.ws_ignore_change {
857 return fuzzy_match_ws(image, preimage, postimage, current_lno, preimage_limit);
858 }
859 if !opts.ws_fix {
860 return false;
861 }
862
863 let mut fixed: Vec<WsImageLine> = Vec::with_capacity(preimage.len());
866 for i in 0..preimage_limit {
867 let fixed_pre = ws::ws_fix_bytes(&preimage[i].bytes(), opts.ws_rule);
868 let fixed_tgt = ws::ws_fix_bytes(&line_bytes(&image[current_lno + i]), opts.ws_rule);
869 if fixed_pre != fixed_tgt {
870 return false;
871 }
872 fixed.push(ws_line_from_bytes(fixed_pre, preimage[i].common));
873 }
874 for line in preimage.iter().skip(preimage_limit) {
875 let fixed_pre = ws::ws_fix_bytes(&line.bytes(), opts.ws_rule);
876 if !fixed_pre.iter().all(|&b| ws::is_space(b)) {
877 return false;
878 }
879 fixed.push(ws_line_from_bytes(fixed_pre, line.common));
880 }
881 update_pre_post_images_ws(preimage, postimage, fixed);
882 true
883}
884
885fn fuzzy_match_ws(
889 image: &[Line],
890 preimage: &mut Vec<WsImageLine>,
891 postimage: &mut Vec<WsImageLine>,
892 current_lno: usize,
893 preimage_limit: usize,
894) -> bool {
895 for i in 0..preimage_limit {
896 if !fuzzy_matchlines(&line_bytes(&image[current_lno + i]), &preimage[i].bytes()) {
897 return false;
898 }
899 }
900 for line in preimage.iter().skip(preimage_limit) {
902 if !line.bytes().iter().all(|&b| ws::is_space(b)) {
903 return false;
904 }
905 }
906 let mut fixed: Vec<WsImageLine> = Vec::with_capacity(preimage.len());
909 for i in 0..preimage_limit {
910 let img = &image[current_lno + i];
911 fixed.push(WsImageLine {
912 content: img.content.clone(),
913 no_newline: img.no_newline,
914 common: preimage[i].common,
915 });
916 }
917 for line in preimage.iter().skip(preimage_limit) {
918 fixed.push(line.clone());
919 }
920 update_pre_post_images_ws(preimage, postimage, fixed);
921 true
922}
923
924fn fuzzy_matchlines(s1: &[u8], s2: &[u8]) -> bool {
927 let trim = |s: &[u8]| {
928 let mut end = s.len();
929 while end > 0 && (s[end - 1] == b'\r' || s[end - 1] == b'\n') {
930 end -= 1;
931 }
932 end
933 };
934 let end1 = trim(s1);
935 let end2 = trim(s2);
936 let (mut i, mut j) = (0usize, 0usize);
937 while i < end1 && j < end2 {
938 if ws::is_space(s1[i]) {
939 if !ws::is_space(s2[j]) {
940 return false;
941 }
942 while i < end1 && ws::is_space(s1[i]) {
943 i += 1;
944 }
945 while j < end2 && ws::is_space(s2[j]) {
946 j += 1;
947 }
948 } else if s1[i] != s2[j] {
949 return false;
950 } else {
951 i += 1;
952 j += 1;
953 }
954 }
955 i == end1 && j == end2
956}
957
958fn update_pre_post_images_ws(
964 preimage: &mut Vec<WsImageLine>,
965 postimage: &mut Vec<WsImageLine>,
966 fixed: Vec<WsImageLine>,
967) {
968 *preimage = fixed;
969 let mut new_post: Vec<WsImageLine> = Vec::with_capacity(postimage.len());
970 let mut ctx = 0usize;
971 for line in postimage.iter() {
972 if !line.common {
973 new_post.push(line.clone());
974 continue;
975 }
976 while ctx < preimage.len() && !preimage[ctx].common {
977 ctx += 1;
978 }
979 if ctx >= preimage.len() {
980 continue;
982 }
983 new_post.push(WsImageLine {
984 content: preimage[ctx].content.clone(),
985 no_newline: preimage[ctx].no_newline,
986 common: true,
987 });
988 ctx += 1;
989 }
990 *postimage = new_post;
991}
992
993fn apply_one_hunk(
1001 image: &mut Vec<Line>,
1002 hunk: &Hunk,
1003 running_offset: isize,
1004 unidiff_zero: bool,
1005) -> Option<isize> {
1006 let mut preimage: Vec<Line> = Vec::new();
1010 let mut postimage: Vec<Line> = Vec::new();
1011 let mut leading = 0usize; let mut trailing = 0usize; let mut seen_change = false;
1014 for hl in &hunk.lines {
1015 match hl {
1016 HunkLine::Context(bytes) => {
1017 preimage.push(Line {
1018 content: bytes.clone(),
1019 no_newline: false,
1020 });
1021 postimage.push(Line {
1022 content: bytes.clone(),
1023 no_newline: false,
1024 });
1025 if !seen_change {
1026 leading += 1;
1027 }
1028 trailing += 1;
1029 }
1030 HunkLine::Delete(bytes) => {
1031 preimage.push(Line {
1032 content: bytes.clone(),
1033 no_newline: false,
1034 });
1035 seen_change = true;
1036 trailing = 0;
1037 }
1038 HunkLine::Insert(bytes) => {
1039 postimage.push(Line {
1040 content: bytes.clone(),
1041 no_newline: false,
1042 });
1043 seen_change = true;
1044 trailing = 0;
1045 }
1046 }
1047 }
1048
1049 if hunk.old_no_newline
1053 && let Some(last) = preimage.last_mut()
1054 {
1055 last.no_newline = true;
1056 }
1057 if hunk.new_no_newline
1058 && let Some(last) = postimage.last_mut()
1059 {
1060 last.no_newline = true;
1061 }
1062
1063 let mut match_beginning = hunk.old_start == 0 || (hunk.old_start == 1 && !unidiff_zero);
1069 let mut match_end = !unidiff_zero && trailing == 0;
1070
1071 let mut expected = if preimage.is_empty() {
1080 new_side_position(hunk, running_offset)
1081 } else {
1082 expected_position(hunk, running_offset)
1083 };
1084 let hunk_expected = expected;
1087
1088 loop {
1089 if let Some(pos) = find_hunk_pos(image, &preimage, expected, match_beginning, match_end) {
1090 let take = preimage.len();
1092 let replacement: Vec<Line> = postimage.clone();
1093 image.splice(pos..pos + take, replacement);
1094 return Some(pos as isize - hunk_expected);
1095 }
1096
1097 #[allow(clippy::absurd_extreme_comparisons)]
1106 if leading <= MIN_FUZZ_CONTEXT && trailing <= MIN_FUZZ_CONTEXT {
1107 return None;
1108 }
1109
1110 if match_beginning || match_end {
1113 match_beginning = false;
1114 match_end = false;
1115 continue;
1116 }
1117
1118 if leading >= trailing {
1120 preimage.remove(0);
1122 postimage.remove(0);
1123 expected -= 1;
1124 leading -= 1;
1125 }
1126 if trailing > leading {
1127 preimage.pop();
1128 postimage.pop();
1129 trailing -= 1;
1130 }
1131 }
1132}
1133
1134#[derive(Debug, Clone, PartialEq, Eq)]
1136pub(crate) struct Line {
1137 pub(crate) content: Vec<u8>,
1138 pub(crate) no_newline: bool,
1139}
1140
1141pub(crate) fn split_blob_lines(data: &[u8]) -> Vec<Line> {
1146 let mut lines = Vec::new();
1147 let mut start = 0usize;
1148 while start < data.len() {
1149 match data[start..].iter().position(|&b| b == b'\n') {
1150 Some(rel) => {
1151 let end = start + rel;
1152 lines.push(Line {
1153 content: data[start..end].to_vec(),
1154 no_newline: false,
1155 });
1156 start = end + 1;
1157 }
1158 None => {
1159 lines.push(Line {
1160 content: data[start..].to_vec(),
1161 no_newline: true,
1162 });
1163 start = data.len();
1164 }
1165 }
1166 }
1167 lines
1168}
1169
1170fn join_lines(lines: &[Line]) -> Vec<u8> {
1172 let mut out = Vec::new();
1173 for line in lines {
1174 out.extend_from_slice(&line.content);
1175 if !line.no_newline {
1176 out.push(b'\n');
1177 }
1178 }
1179 out
1180}
1181
1182fn expected_position(hunk: &Hunk, running_offset: isize) -> isize {
1185 let base = if hunk.old_start == 0 {
1187 0
1188 } else {
1189 hunk.old_start as isize - 1
1190 };
1191 base + running_offset
1192}
1193
1194fn new_side_position(hunk: &Hunk, running_offset: isize) -> isize {
1197 let base = if hunk.new_start == 0 {
1198 0
1199 } else {
1200 hunk.new_start as isize - 1
1201 };
1202 base + running_offset
1203}
1204
1205fn find_hunk_pos(
1213 image: &[Line],
1214 preimage: &[Line],
1215 expected: isize,
1216 match_beginning: bool,
1217 match_end: bool,
1218) -> Option<usize> {
1219 let line_nr = image.len();
1220 let pre_nr = preimage.len();
1221
1222 let mut line: isize = if match_beginning {
1225 0
1226 } else if match_end {
1227 line_nr as isize - pre_nr as isize
1228 } else {
1229 expected
1230 };
1231 if line < 0 {
1232 line = 0;
1233 }
1234 if line as usize > line_nr {
1235 line = line_nr as isize;
1236 }
1237
1238 let start = line as usize;
1239 let mut backwards = start;
1240 let mut forwards = start;
1241 let mut current = start;
1242
1243 let mut i: u64 = 0;
1244 loop {
1245 if preimage_matches_at(image, preimage, current, match_beginning, match_end) {
1246 return Some(current);
1247 }
1248
1249 loop {
1250 if backwards == 0 && forwards == line_nr {
1252 return None;
1253 }
1254 if i & 1 == 1 {
1255 if backwards == 0 {
1257 i += 1;
1258 continue;
1259 }
1260 backwards -= 1;
1261 current = backwards;
1262 } else {
1263 if forwards == line_nr {
1265 i += 1;
1266 continue;
1267 }
1268 forwards += 1;
1269 current = forwards;
1270 }
1271 break;
1272 }
1273 i += 1;
1274 }
1275}
1276
1277fn preimage_matches_at(
1286 image: &[Line],
1287 preimage: &[Line],
1288 pos: usize,
1289 match_beginning: bool,
1290 match_end: bool,
1291) -> bool {
1292 if match_beginning && pos != 0 {
1293 return false;
1294 }
1295 if pos + preimage.len() > image.len() {
1297 return false;
1298 }
1299 if match_end && pos + preimage.len() != image.len() {
1300 return false;
1301 }
1302 for (i, pre) in preimage.iter().enumerate() {
1303 let img = &image[pos + i];
1304 if img.content != pre.content {
1305 return false;
1306 }
1307 if pre.no_newline != img.no_newline {
1313 return false;
1314 }
1315 }
1316 true
1317}
1318
1319fn split_patch_lines(input: &[u8]) -> Vec<&[u8]> {
1323 let mut lines = Vec::new();
1324 let mut start = 0usize;
1325 while start < input.len() {
1326 match input[start..].iter().position(|&b| b == b'\n') {
1327 Some(rel) => {
1328 let end = start + rel;
1329 lines.push(&input[start..end]);
1330 start = end + 1;
1331 }
1332 None => {
1333 lines.push(&input[start..]);
1334 start = input.len();
1335 }
1336 }
1337 }
1338 lines
1339}
1340
1341struct PatchParser<'a> {
1342 lines: &'a [&'a [u8]],
1343 index: usize,
1344 recount: bool,
1345 p_value: usize,
1348 p_value_known: bool,
1349 root: Vec<u8>,
1351 prefix: Vec<u8>,
1354}
1355
1356impl<'a> PatchParser<'a> {
1357 fn parse(&mut self) -> Result<Vec<FilePatch>> {
1358 let mut patches = Vec::new();
1359 while self.index < self.lines.len() {
1360 let line = self.lines[self.index];
1361 if line.starts_with(b"diff --git ") {
1362 patches.push(self.parse_file(Some(line))?);
1363 } else if line.starts_with(b"--- ") {
1364 patches.push(self.parse_file(None)?);
1366 } else if line.starts_with(b"@@ ") {
1367 return Err(GitError::InvalidFormat(
1368 "hunk header encountered before any file header".to_string(),
1369 ));
1370 } else {
1371 self.index += 1;
1373 }
1374 }
1375 Ok(patches)
1376 }
1377
1378 fn parse_file(&mut self, diff_line: Option<&[u8]>) -> Result<FilePatch> {
1382 match diff_line {
1383 Some(diff_line) => self.parse_git_file(diff_line),
1384 None => self.parse_traditional_file(),
1385 }
1386 }
1387
1388 fn p_minus_one(&self) -> usize {
1392 self.p_value.saturating_sub(1)
1393 }
1394
1395 fn parse_git_file(&mut self, diff_line: &[u8]) -> Result<FilePatch> {
1398 let mut patch = empty_file_patch();
1399 let rest = &diff_line[b"diff --git ".len()..];
1402 let mut def_name = name::git_header_name(self.p_value, rest);
1403 if let (Some(d), false) = (def_name.as_mut(), self.root.is_empty()) {
1404 let mut s = self.root.clone();
1405 s.extend_from_slice(d);
1406 *d = s;
1407 }
1408 self.index += 1;
1409
1410 patch.is_toplevel_relative = true;
1413
1414 let mut binary_seen = false;
1417
1418 while self.index < self.lines.len() {
1420 let line = self.lines[self.index];
1421 if line.starts_with(b"--- ") {
1422 self.parse_git_old_header(&line[b"--- ".len()..], &mut patch);
1423 self.index += 1;
1424 break;
1425 } else if line.starts_with(b"@@ ") {
1426 break;
1428 } else if line.starts_with(b"diff --git ") {
1429 break;
1431 } else if let Some(rest) = strip_prefix(line, b"old mode ") {
1432 patch.old_mode = Some(self.parse_mode_line(rest)?);
1433 } else if let Some(rest) = strip_prefix(line, b"new mode ") {
1434 patch.new_mode = Some(self.parse_mode_line(rest)?);
1435 } else if let Some(rest) = strip_prefix(line, b"new file mode ") {
1436 patch.is_new = true;
1437 patch.new_mode = Some(self.parse_mode_line(rest)?);
1438 patch.new_path = def_name.clone();
1439 } else if let Some(rest) = strip_prefix(line, b"deleted file mode ") {
1440 patch.is_delete = true;
1441 patch.old_mode = Some(self.parse_mode_line(rest)?);
1442 patch.old_path = def_name.clone();
1443 } else if let Some(rest) = strip_prefix(line, b"index ") {
1444 self.parse_index_line(rest, &mut patch)?;
1448 } else if let Some(rest) = strip_prefix(line, b"rename from ") {
1449 patch.is_rename = true;
1450 patch.old_path = name::find_name(rest, None, self.p_minus_one(), 0, &self.root);
1451 } else if let Some(rest) = strip_prefix(line, b"rename to ") {
1452 patch.is_rename = true;
1453 patch.new_path = name::find_name(rest, None, self.p_minus_one(), 0, &self.root);
1454 } else if let Some(rest) = strip_prefix(line, b"copy from ") {
1455 patch.is_copy = true;
1456 patch.old_path = name::find_name(rest, None, self.p_minus_one(), 0, &self.root);
1457 } else if let Some(rest) = strip_prefix(line, b"copy to ") {
1458 patch.is_copy = true;
1459 patch.new_path = name::find_name(rest, None, self.p_minus_one(), 0, &self.root);
1460 } else if let Some(rest) = strip_prefix(line, b"similarity index ") {
1461 patch.similarity = parse_percent(rest);
1462 } else if let Some(rest) = strip_prefix(line, b"dissimilarity index ") {
1463 patch.dissimilarity = parse_percent(rest);
1464 } else if line == b"GIT binary patch" {
1465 let gitbin_line = self.index + 1;
1467 patch.is_binary = true;
1468 patch.binary = Some(self.parse_binary_block(gitbin_line)?);
1469 binary_seen = true;
1470 break;
1471 } else if apply_is_binary_files_differ(line) {
1472 patch.is_binary = true;
1475 binary_seen = true;
1476 self.index += 1;
1477 break;
1478 } else {
1479 self.index += 1;
1481 continue;
1482 }
1483 self.index += 1;
1484 }
1485
1486 if !binary_seen
1488 && self.index < self.lines.len()
1489 && self.lines[self.index].starts_with(b"+++ ")
1490 {
1491 let line = self.lines[self.index];
1492 self.parse_git_new_header(&line[b"+++ ".len()..], &mut patch);
1493 self.index += 1;
1494 }
1495
1496 if patch.old_path.is_none() && patch.new_path.is_none() {
1499 match &def_name {
1500 Some(d) => {
1501 patch.old_path = Some(d.clone());
1502 patch.new_path = Some(d.clone());
1503 }
1504 None => {
1505 return Err(GitError::InvalidFormat(format!(
1506 "git diff header lacks filename information when removing {} \
1507 leading pathname components",
1508 self.p_value
1509 )));
1510 }
1511 }
1512 }
1513
1514 if !binary_seen {
1516 self.parse_hunks(&mut patch)?;
1517 }
1518 Ok(patch)
1519 }
1520
1521 fn parse_index_line(&self, rest: &[u8], patch: &mut FilePatch) -> Result<()> {
1524 let Some(dotdot) = find_subslice(rest, b"..") else {
1525 return Ok(());
1526 };
1527 let old = &rest[..dotdot];
1528 let after = &rest[dotdot + 2..];
1529 let (new, mode_part) = match after.iter().position(|&b| b == b' ') {
1531 Some(space) => (&after[..space], Some(&after[space + 1..])),
1532 None => (after, None),
1533 };
1534 if !old.is_empty() {
1535 patch.old_oid_hex = Some(old.to_vec());
1536 }
1537 if !new.is_empty() {
1538 patch.new_oid_hex = Some(new.to_vec());
1539 }
1540 if let Some(mode) = mode_part
1541 && !mode.is_empty()
1542 {
1543 patch.old_mode = Some(self.parse_mode_line(mode)?);
1544 }
1545 Ok(())
1546 }
1547
1548 fn parse_mode_line(&self, rest: &[u8]) -> Result<u32> {
1551 let mut value: u32 = 0;
1552 let mut i = 0;
1553 while i < rest.len() && (b'0'..=b'7').contains(&rest[i]) {
1554 value = value
1555 .checked_mul(8)
1556 .and_then(|value| value.checked_add((rest[i] - b'0') as u32))
1557 .ok_or_else(|| self.invalid_mode_error(rest))?;
1558 i += 1;
1559 }
1560 if i == 0 || (i < rest.len() && !rest[i].is_ascii_whitespace()) {
1561 return Err(self.invalid_mode_error(rest));
1562 }
1563 Ok(value)
1564 }
1565
1566 fn invalid_mode_error(&self, rest: &[u8]) -> GitError {
1567 GitError::InvalidFormat(format!(
1568 "invalid mode on line {}: {}",
1569 self.index + 1,
1570 lossy(rest)
1571 ))
1572 }
1573
1574 fn parse_binary_block(&mut self, gitbin_line: usize) -> Result<BinaryPatch> {
1579 self.index += 1;
1581 let forward = match self.parse_binary_hunk()? {
1582 Some(hunk) => hunk,
1583 None => {
1584 return Err(GitError::InvalidFormat(format!(
1585 "binary-unrecognized:{gitbin_line}"
1586 )));
1587 }
1588 };
1589 let reverse = self.parse_binary_hunk()?;
1590 Ok(BinaryPatch { forward, reverse })
1591 }
1592
1593 fn parse_binary_hunk(&mut self) -> Result<Option<BinaryHunk>> {
1596 if self.index >= self.lines.len() {
1597 return Ok(None);
1598 }
1599 let line = self.lines[self.index];
1600 let (method, num) = if let Some(rest) = strip_prefix(line, b"delta ") {
1601 (BinaryMethod::Delta, rest)
1602 } else if let Some(rest) = strip_prefix(line, b"literal ") {
1603 (BinaryMethod::Literal, rest)
1604 } else {
1605 return Ok(None);
1606 };
1607 let origlen = parse_leading_usize(num).map_err(|()| self.corrupt_binary_error())?;
1608 self.index += 1;
1609
1610 let mut deflated = Vec::new();
1611 loop {
1612 if self.index >= self.lines.len() {
1613 return Err(self.corrupt_binary_error());
1615 }
1616 let data = self.lines[self.index];
1617 if data.is_empty() {
1618 self.index += 1;
1620 break;
1621 }
1622 let len = data.len();
1625 if len < 6 || !(len - 1).is_multiple_of(5) {
1626 return Err(self.corrupt_binary_error());
1627 }
1628 let max_byte_length = (len - 1) / 5 * 4;
1629 let byte_length = match data[0] {
1630 b'A'..=b'Z' => (data[0] - b'A') as usize + 1,
1631 b'a'..=b'z' => (data[0] - b'a') as usize + 27,
1632 _ => return Err(self.corrupt_binary_error()),
1633 };
1634 if max_byte_length < byte_length || byte_length <= max_byte_length.saturating_sub(4) {
1635 return Err(self.corrupt_binary_error());
1636 }
1637 let decoded = decode_base85(&data[1..], byte_length)
1638 .ok_or_else(|| self.corrupt_binary_error())?;
1639 deflated.extend_from_slice(&decoded);
1640 self.index += 1;
1641 }
1642 Ok(Some(BinaryHunk {
1643 method,
1644 origlen,
1645 deflated,
1646 }))
1647 }
1648
1649 fn corrupt_binary_error(&self) -> GitError {
1650 GitError::InvalidFormat(format!("binary-corrupt:{}", self.index + 1))
1651 }
1652
1653 fn parse_git_old_header(&self, rest: &[u8], patch: &mut FilePatch) {
1654 if name::is_dev_null(rest) {
1655 patch.is_new = true;
1656 patch.old_path = None;
1657 } else if patch.old_path.is_none() {
1658 patch.old_path = name::find_name(rest, None, self.p_value, name::TERM_TAB, &self.root);
1659 }
1660 }
1661
1662 fn parse_git_new_header(&self, rest: &[u8], patch: &mut FilePatch) {
1663 if name::is_dev_null(rest) {
1664 patch.is_delete = true;
1665 patch.new_path = None;
1666 } else if patch.new_path.is_none() {
1667 patch.new_path = name::find_name(rest, None, self.p_value, name::TERM_TAB, &self.root);
1668 }
1669 }
1670
1671 fn parse_traditional_file(&mut self) -> Result<FilePatch> {
1675 let mut patch = empty_file_patch();
1676 let first_line = self.lines[self.index];
1677 let first = first_line[b"--- ".len()..].to_vec();
1678 self.index += 1;
1679 let second = if self.index < self.lines.len() && self.lines[self.index].starts_with(b"+++ ")
1680 {
1681 let s = self.lines[self.index][b"+++ ".len()..].to_vec();
1682 self.index += 1;
1683 Some(s)
1684 } else {
1685 None
1686 };
1687
1688 if let Some(second) = &second {
1689 if !self.p_value_known {
1690 let p0 = name::guess_p_value(&first, &self.root, &self.prefix);
1691 let q0 = name::guess_p_value(second, &self.root, &self.prefix);
1692 let p = if p0.is_none() { q0 } else { p0 };
1693 if let Some(pv) = p
1694 && Some(pv) == q0
1695 {
1696 self.p_value = pv;
1697 self.p_value_known = true;
1698 }
1699 }
1700
1701 let name = if name::is_dev_null(&first) {
1702 patch.is_new = true;
1703 let name = name::find_name_traditional(second, None, self.p_value, &self.root);
1704 patch.new_path = name.clone();
1705 name
1706 } else if name::is_dev_null(second) {
1707 patch.is_delete = true;
1708 let name = name::find_name_traditional(&first, None, self.p_value, &self.root);
1709 patch.old_path = name.clone();
1710 name
1711 } else {
1712 let first_name =
1713 name::find_name_traditional(&first, None, self.p_value, &self.root);
1714 let name = name::find_name_traditional(
1715 second,
1716 first_name.as_deref(),
1717 self.p_value,
1718 &self.root,
1719 );
1720 if name::has_epoch_timestamp(&first) {
1721 patch.is_new = true;
1722 patch.new_path = name.clone();
1723 } else if name::has_epoch_timestamp(second) {
1724 patch.is_delete = true;
1725 patch.old_path = name.clone();
1726 } else {
1727 patch.old_path = name.clone();
1728 patch.new_path = name.clone();
1729 }
1730 name
1731 };
1732 if name.is_none() {
1737 return Err(GitError::InvalidFormat(format!(
1738 "unable to find filename in patch at line {}",
1739 self.index
1740 )));
1741 }
1742 }
1743
1744 self.parse_hunks(&mut patch)?;
1745 Ok(patch)
1746 }
1747
1748 fn parse_hunks(&mut self, patch: &mut FilePatch) -> Result<()> {
1751 while self.index < self.lines.len() {
1752 let line = self.lines[self.index];
1753 if line.starts_with(b"@@ -") {
1759 let hunk = self.parse_hunk()?;
1760 patch.hunks.push(hunk);
1761 } else if line.starts_with(b"diff --git ") {
1762 break;
1763 } else if line.starts_with(b"--- ") {
1764 break;
1766 } else {
1767 self.index += 1;
1769 }
1770 }
1771 Ok(())
1772 }
1773
1774 fn parse_hunk(&mut self) -> Result<Hunk> {
1775 let header = self.lines[self.index];
1776 let (old_start, old_len, new_start, new_len) = parse_hunk_header(header)?;
1777 self.index += 1;
1778
1779 let mut hunk = Hunk {
1780 old_start,
1781 old_len,
1782 new_start,
1783 new_len,
1784 lines: Vec::new(),
1785 old_no_newline: false,
1786 new_no_newline: false,
1787 line_input_lines: Vec::new(),
1788 };
1789 let mut old_seen = 0usize;
1790 let mut new_seen = 0usize;
1791
1792 while self.index < self.lines.len() {
1793 if !self.recount && old_seen >= old_len && new_seen >= new_len {
1797 break;
1798 }
1799 let line = self.lines[self.index];
1800 if self.recount
1801 && (line.starts_with(b"@@ ")
1802 || line.starts_with(b"diff --git ")
1803 || line.starts_with(b"diff a/")
1804 || line.starts_with(b"--- "))
1805 {
1806 break;
1807 }
1808 if line.is_empty() {
1809 hunk.lines.push(HunkLine::Context(Vec::new()));
1813 hunk.line_input_lines.push(self.index + 1);
1814 old_seen += 1;
1815 new_seen += 1;
1816 self.index += 1;
1817 continue;
1818 }
1819 match line[0] {
1820 b' ' => {
1821 hunk.lines.push(HunkLine::Context(line[1..].to_vec()));
1822 hunk.line_input_lines.push(self.index + 1);
1823 old_seen += 1;
1824 new_seen += 1;
1825 }
1826 b'+' => {
1827 hunk.lines.push(HunkLine::Insert(line[1..].to_vec()));
1828 hunk.line_input_lines.push(self.index + 1);
1829 new_seen += 1;
1830 }
1831 b'-' => {
1832 hunk.lines.push(HunkLine::Delete(line[1..].to_vec()));
1833 hunk.line_input_lines.push(self.index + 1);
1834 old_seen += 1;
1835 }
1836 b'\\' => {
1837 self.mark_no_newline(&mut hunk);
1840 self.index += 1;
1841 continue;
1842 }
1843 _ => {
1844 return Err(GitError::InvalidFormat(format!(
1845 "corrupt-hunk-body:{}",
1846 self.index + 1
1847 )));
1848 }
1849 }
1850 self.index += 1;
1851 }
1852
1853 if self.index < self.lines.len() && self.lines[self.index].starts_with(b"\\") {
1856 self.mark_no_newline(&mut hunk);
1857 self.index += 1;
1858 }
1859
1860 if self.recount {
1861 hunk.old_len = old_seen;
1862 hunk.new_len = new_seen;
1863 } else if old_seen != old_len || new_seen != new_len {
1864 return Err(GitError::InvalidFormat(format!(
1865 "hunk body line counts mismatch: header declared -{old_len},+{new_len} \
1866 but body had -{old_seen},+{new_seen}"
1867 )));
1868 }
1869
1870 Ok(hunk)
1871 }
1872
1873 fn mark_no_newline(&self, hunk: &mut Hunk) {
1876 match hunk.lines.last() {
1877 Some(HunkLine::Context(_)) => {
1878 hunk.old_no_newline = true;
1879 hunk.new_no_newline = true;
1880 }
1881 Some(HunkLine::Insert(_)) => hunk.new_no_newline = true,
1882 Some(HunkLine::Delete(_)) => hunk.old_no_newline = true,
1883 None => {}
1884 }
1885 }
1886}
1887
1888fn empty_file_patch() -> FilePatch {
1890 FilePatch {
1891 old_path: None,
1892 new_path: None,
1893 old_mode: None,
1894 new_mode: None,
1895 hunks: Vec::new(),
1896 is_new: false,
1897 is_delete: false,
1898 is_rename: false,
1899 is_copy: false,
1900 similarity: None,
1901 dissimilarity: None,
1902 old_oid_hex: None,
1903 new_oid_hex: None,
1904 is_binary: false,
1905 binary: None,
1906 is_toplevel_relative: false,
1907 }
1908}
1909
1910fn parse_hunk_header(line: &[u8]) -> Result<(usize, usize, usize, usize)> {
1913 let err = || GitError::InvalidFormat(format!("malformed hunk header: {}", lossy(line)));
1914 let rest = strip_prefix(line, b"@@ ").ok_or_else(err)?;
1915 let close = find_subslice(rest, b" @@").ok_or_else(err)?;
1917 let ranges = &rest[..close];
1918 let mut parts = ranges.split(|&b| b == b' ').filter(|p| !p.is_empty());
1919 let old = parts.next().ok_or_else(err)?;
1920 let new = parts.next().ok_or_else(err)?;
1921 let old = strip_prefix(old, b"-").ok_or_else(err)?;
1922 let new = strip_prefix(new, b"+").ok_or_else(err)?;
1923 let (old_start, old_len) = parse_range(old).ok_or_else(err)?;
1924 let (new_start, new_len) = parse_range(new).ok_or_else(err)?;
1925 Ok((old_start, old_len, new_start, new_len))
1926}
1927
1928fn parse_range(range: &[u8]) -> Option<(usize, usize)> {
1930 match range.iter().position(|&b| b == b',') {
1931 Some(comma) => {
1932 let start = parse_usize(&range[..comma])?;
1933 let len = parse_usize(&range[comma + 1..])?;
1934 Some((start, len))
1935 }
1936 None => Some((parse_usize(range)?, 1)),
1937 }
1938}
1939
1940fn parse_usize(bytes: &[u8]) -> Option<usize> {
1941 if bytes.is_empty() {
1942 return None;
1943 }
1944 let mut value: usize = 0;
1945 for &b in bytes {
1946 if !b.is_ascii_digit() {
1947 return None;
1948 }
1949 value = value.checked_mul(10)?.checked_add((b - b'0') as usize)?;
1950 }
1951 Some(value)
1952}
1953
1954fn parse_percent(bytes: &[u8]) -> Option<u8> {
1955 let trimmed = trim_ascii_end(bytes)
1956 .strip_suffix(b"%")
1957 .unwrap_or(trim_ascii_end(bytes));
1958 let value = parse_usize(trimmed)?;
1959 u8::try_from(value).ok().filter(|value| *value <= 100)
1960}
1961
1962fn strip_prefix<'b>(line: &'b [u8], prefix: &[u8]) -> Option<&'b [u8]> {
1963 if line.starts_with(prefix) {
1964 Some(&line[prefix.len()..])
1965 } else {
1966 None
1967 }
1968}
1969
1970fn apply_is_binary_files_differ(line: &[u8]) -> bool {
1973 line.ends_with(b" differ")
1974 && (line.starts_with(b"Binary files ") || line.starts_with(b"Files "))
1975}
1976
1977pub(crate) fn parse_leading_usize(bytes: &[u8]) -> std::result::Result<usize, ()> {
1981 let mut value = 0usize;
1982 for &b in bytes {
1983 if !b.is_ascii_digit() {
1984 break;
1985 }
1986 value = value.checked_mul(10).ok_or(())?;
1987 value = value.checked_add((b - b'0') as usize).ok_or(())?;
1988 }
1989 Ok(value)
1990}
1991
1992const BASE85_ALPHABET: &[u8; 85] =
1994 b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~";
1995
1996fn base85_value(ch: u8) -> Option<u32> {
1997 BASE85_ALPHABET
1998 .iter()
1999 .position(|&c| c == ch)
2000 .map(|index| index as u32)
2001}
2002
2003fn decode_base85(buffer: &[u8], len: usize) -> Option<Vec<u8>> {
2007 let mut out = Vec::with_capacity(len);
2008 let mut pos = 0usize;
2009 let mut remaining = len;
2010 while remaining > 0 {
2011 let mut acc: u32 = 0;
2012 for _ in 0..4 {
2014 let de = base85_value(*buffer.get(pos)?)?;
2015 pos += 1;
2016 acc = acc * 85 + de;
2017 }
2018 let de = base85_value(*buffer.get(pos)?)?;
2019 pos += 1;
2020 if 0xffff_ffffu32 / 85 < acc {
2022 return None;
2023 }
2024 acc *= 85;
2025 if 0xffff_ffffu32 - de < acc {
2026 return None;
2027 }
2028 acc += de;
2029
2030 let cnt = remaining.min(4);
2031 remaining -= cnt;
2032 let bytes = acc.to_be_bytes();
2033 out.extend_from_slice(&bytes[..cnt]);
2034 }
2035 Some(out)
2036}
2037
2038pub fn git_patch_delta(base: &[u8], delta: &[u8]) -> Option<Vec<u8>> {
2043 let mut data = 0usize;
2044 let read_hdr_size = |data: &mut usize| -> Option<usize> {
2045 let mut size = 0usize;
2046 let mut shift = 0u32;
2047 loop {
2048 let cmd = *delta.get(*data)?;
2049 *data += 1;
2050 size |= ((cmd & 0x7f) as usize).checked_shl(shift)?;
2051 shift += 7;
2052 if cmd & 0x80 == 0 {
2053 break;
2054 }
2055 }
2056 Some(size)
2057 };
2058
2059 let base_size = read_hdr_size(&mut data)?;
2060 if base_size != base.len() {
2061 return None;
2062 }
2063 let result_size = read_hdr_size(&mut data)?;
2064 let mut out = Vec::with_capacity(sley_pack::inflate::bounded_inflate_reserve(
2065 result_size,
2066 delta.len(),
2067 ));
2068
2069 while data < delta.len() {
2070 let cmd = delta[data];
2071 data += 1;
2072 if cmd & 0x80 != 0 {
2073 let mut cp_off = 0usize;
2075 let mut cp_size = 0usize;
2076 if cmd & 0x01 != 0 {
2077 cp_off = *delta.get(data)? as usize;
2078 data += 1;
2079 }
2080 if cmd & 0x02 != 0 {
2081 cp_off |= (*delta.get(data)? as usize) << 8;
2082 data += 1;
2083 }
2084 if cmd & 0x04 != 0 {
2085 cp_off |= (*delta.get(data)? as usize) << 16;
2086 data += 1;
2087 }
2088 if cmd & 0x08 != 0 {
2089 cp_off |= (*delta.get(data)? as usize) << 24;
2090 data += 1;
2091 }
2092 if cmd & 0x10 != 0 {
2093 cp_size = *delta.get(data)? as usize;
2094 data += 1;
2095 }
2096 if cmd & 0x20 != 0 {
2097 cp_size |= (*delta.get(data)? as usize) << 8;
2098 data += 1;
2099 }
2100 if cmd & 0x40 != 0 {
2101 cp_size |= (*delta.get(data)? as usize) << 16;
2102 data += 1;
2103 }
2104 if cp_size == 0 {
2105 cp_size = 0x10000;
2106 }
2107 let end = cp_off.checked_add(cp_size)?;
2108 if end > base.len() || cp_size > result_size {
2109 return None;
2110 }
2111 out.extend_from_slice(&base[cp_off..end]);
2112 } else if cmd != 0 {
2113 let len = cmd as usize;
2115 let end = data.checked_add(len)?;
2116 if end > delta.len() {
2117 return None;
2118 }
2119 out.extend_from_slice(&delta[data..end]);
2120 data = end;
2121 } else {
2122 return None;
2124 }
2125 }
2126
2127 if data != delta.len() || out.len() != result_size {
2128 return None;
2129 }
2130 Some(out)
2131}
2132
2133fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
2134 if needle.is_empty() || needle.len() > haystack.len() {
2135 return None;
2136 }
2137 haystack
2138 .windows(needle.len())
2139 .position(|window| window == needle)
2140}
2141
2142fn trim_ascii_end(bytes: &[u8]) -> &[u8] {
2143 let mut end = bytes.len();
2144 while end > 0 && (bytes[end - 1] == b' ' || bytes[end - 1] == b'\r') {
2145 end -= 1;
2146 }
2147 &bytes[..end]
2148}
2149
2150fn lossy(bytes: &[u8]) -> String {
2151 String::from_utf8_lossy(bytes).into_owned()
2152}