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