Skip to main content

java_diff_utils_rs/unifieddiff/
unified_diff_reader.rs

1//! Parser for reading and building a `UnifiedDiff` from text streams or readers.
2
3use regex::Regex;
4use std::io::{BufRead, BufReader, Read};
5
6use super::unified_diff::UnifiedDiff;
7use super::unified_diff_file::UnifiedDiffFile;
8use crate::patch::change_delta::ChangeDelta;
9use crate::patch::chunk::Chunk;
10use crate::patch::delete_delta::DeleteDelta;
11use crate::patch::delta::Delta;
12use crate::patch::equal_delta::EqualDelta;
13use crate::patch::insert_delta::InsertDelta;
14use crate::unifieddiff::unified_diff_parser_exception::UnifiedDiffParserException;
15
16lazy_static::lazy_static! {
17    static ref UNIFIED_DIFF_CHUNK_REGEXP: Regex =
18        Regex::new(r"^@@\s+-(?:(\d+)(?:,(\d+))?)\s+\+(?:(\d+)(?:,(\d+))?)\s+@@").unwrap();
19    static ref TIMESTAMP_REGEXP: Regex =
20        Regex::new(r"(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}\.\d{3,})(?: [+-]\d+)?").unwrap();
21
22    static ref DIFF_COMMAND_RE: Regex = Regex::new(r"^diff\s").unwrap();
23    static ref SIMILARITY_INDEX_RE: Regex = Regex::new(r"^similarity index (\d+)%$").unwrap();
24    static ref INDEX_RE: Regex = Regex::new(r"^index\s[\da-zA-Z]+\.\.[\da-zA-Z]+(\s(\d+))?$").unwrap();
25    static ref FROM_FILE_RE: Regex = Regex::new(r"^---\s").unwrap();
26    static ref TO_FILE_RE: Regex = Regex::new(r"^\+\+\+\s").unwrap();
27    static ref RENAME_FROM_RE: Regex = Regex::new(r"^rename\sfrom\s(.+)$").unwrap();
28    static ref RENAME_TO_RE: Regex = Regex::new(r"^rename\sto\s(.+)$").unwrap();
29    static ref COPY_FROM_RE: Regex = Regex::new(r"^copy\sfrom\s(.+)$").unwrap();
30    static ref COPY_TO_RE: Regex = Regex::new(r"^copy\sto\s(.+)$").unwrap();
31    static ref NEW_FILE_MODE_RE: Regex = Regex::new(r"^new\sfile\smode\s(\d+)").unwrap();
32    static ref DELETED_FILE_MODE_RE: Regex = Regex::new(r"^deleted\sfile\smode\s(\d+)").unwrap();
33    static ref OLD_MODE_RE: Regex = Regex::new(r"^old\smode\s(\d+)").unwrap();
34    static ref NEW_MODE_RE: Regex = Regex::new(r"^new\smode\s(\d+)").unwrap();
35    static ref BINARY_ADDED_RE: Regex = Regex::new(r"^Binary\sfiles\s/dev/null\sand\sb/(.+)\sdiffer").unwrap();
36    static ref BINARY_DELETED_RE: Regex = Regex::new(r"^Binary\sfiles\sa/(.+)\sand\s/dev/null\sdiffer").unwrap();
37    static ref BINARY_EDITED_RE: Regex = Regex::new(r"^Binary\sfiles\sa/(.+)\sand\sb/(.+)\sdiffer").unwrap();
38
39    static ref LINE_NORMAL_RE: Regex = Regex::new(r"^\s").unwrap();
40    static ref LINE_DEL_RE: Regex = Regex::new(r"^-").unwrap();
41    static ref LINE_ADD_RE: Regex = Regex::new(r"^\+").unwrap();
42}
43
44struct InternalUnifiedDiffReader<R: Read> {
45    reader: BufReader<R>,
46    last_line: Option<String>,
47}
48
49impl<R: Read> InternalUnifiedDiffReader<R> {
50    fn new(reader: R) -> Self {
51        Self {
52            reader: BufReader::new(reader),
53            last_line: None,
54        }
55    }
56
57    fn read_line(&mut self) -> std::io::Result<Option<String>> {
58        let mut line = String::new();
59        let bytes_read = self.reader.read_line(&mut line)?;
60        if bytes_read == 0 {
61            self.last_line = None;
62            return Ok(None);
63        }
64        if line.ends_with('\n') {
65            line.pop();
66            if line.ends_with('\r') {
67                line.pop();
68            }
69        }
70        self.last_line = Some(line.clone());
71        Ok(Some(line))
72    }
73
74    fn last_line(&self) -> Option<&str> {
75        self.last_line.as_deref()
76    }
77}
78
79/// Main parser for Unified Diff format.
80pub struct UnifiedDiffReader<R: Read> {
81    reader: InternalUnifiedDiffReader<R>,
82    data: UnifiedDiff,
83    actual_file: Option<UnifiedDiffFile>,
84
85    // State for parsing chunks
86    original_txt: Vec<String>,
87    revised_txt: Vec<String>,
88    add_line_idx_list: Vec<usize>,
89    del_line_idx_list: Vec<usize>,
90    old_ln: usize,
91    old_size: usize,
92    new_ln: usize,
93    new_size: usize,
94    del_line_idx: usize,
95    add_line_idx: usize,
96}
97
98impl<R: Read> UnifiedDiffReader<R> {
99    pub fn new(reader: R) -> Self {
100        Self {
101            reader: InternalUnifiedDiffReader::new(reader),
102            data: UnifiedDiff::default(),
103            actual_file: None,
104            original_txt: Vec::new(),
105            revised_txt: Vec::new(),
106            add_line_idx_list: Vec::new(),
107            del_line_idx_list: Vec::new(),
108            old_ln: 0,
109            old_size: 0,
110            new_ln: 0,
111            new_size: 0,
112            del_line_idx: 0,
113            add_line_idx: 0,
114        }
115    }
116
117    pub fn parse_file_names(line: &str) -> (String, String) {
118        let split: Vec<&str> = line.split(' ').collect();
119        let from = Regex::new(r"^a/")
120            .unwrap()
121            .replace(split.get(2).copied().unwrap_or(""), "")
122            .to_string();
123        let to = Regex::new(r"^b/")
124            .unwrap()
125            .replace(split.get(3).copied().unwrap_or(""), "")
126            .to_string();
127        (from, to)
128    }
129
130    pub fn extract_file_name(line: &str) -> String {
131        let mut clean_line = line.to_string();
132        if let Some(m) = TIMESTAMP_REGEXP.find(line) {
133            clean_line = clean_line[..m.start()].to_string();
134        }
135        let first_part = clean_line.split('\t').next().unwrap_or(&clean_line);
136        let sliced = if first_part.len() >= 4 {
137            &first_part[4..]
138        } else {
139            first_part
140        };
141
142        Regex::new(r"^(a|b|old|new)/")
143            .unwrap()
144            .replace(sliced, "")
145            .trim()
146            .to_string()
147    }
148
149    pub fn extract_timestamp(line: &str) -> Option<String> {
150        TIMESTAMP_REGEXP.find(line).map(|m| m.as_str().to_string())
151    }
152
153    /// Helper static function to parse an input stream into a `UnifiedDiff`.
154    pub fn parse_unified_diff(reader: R) -> Result<UnifiedDiff, UnifiedDiffParserException> {
155        let mut parser = UnifiedDiffReader::new(reader);
156        parser.parse()
157    }
158
159    pub fn parse(&mut self) -> Result<UnifiedDiff, UnifiedDiffParserException> {
160        let mut current_line = self
161            .reader
162            .read_line()
163            .map_err(|e| UnifiedDiffParserException::new(e.to_string()))?;
164
165        while let Some(ref line_str) = current_line {
166            let mut header_txt = String::new();
167
168            // Header parsing loop
169            let mut line_opt = Some(line_str.clone());
170            while let Some(ref line) = line_opt {
171                if self.valid_file_header_line(line) {
172                    break;
173                } else {
174                    header_txt.push_str(line);
175                    header_txt.push('\n');
176                }
177                line_opt = self
178                    .reader
179                    .read_line()
180                    .map_err(|e| UnifiedDiffParserException::new(e.to_string()))?;
181            }
182
183            if !header_txt.is_empty() {
184                self.data.set_header(header_txt);
185            }
186
187            current_line = line_opt;
188
189            if let Some(ref line) = current_line {
190                if !UNIFIED_DIFF_CHUNK_REGEXP.is_match(line) {
191                    self.init_file_if_necessary()?;
192
193                    while let Some(ref l) = current_line {
194                        if UNIFIED_DIFF_CHUNK_REGEXP.is_match(l) {
195                            break;
196                        }
197
198                        if !self.process_file_header_line(l) {
199                            return Err(UnifiedDiffParserException::new(
200                                "expected file start line not found",
201                            ));
202                        }
203
204                        current_line = self
205                            .reader
206                            .read_line()
207                            .map_err(|e| UnifiedDiffParserException::new(e.to_string()))?;
208                    }
209                }
210            }
211
212            if let Some(ref line) = current_line {
213                self.process_chunk_line(line)?;
214
215                while let Some(mut l) = self
216                    .reader
217                    .read_line()
218                    .map_err(|e| UnifiedDiffParserException::new(e.to_string()))?
219                {
220                    l = self.check_for_no_new_line_at_the_end_of_the_file(l)?;
221
222                    if !self.process_data_line(&l) {
223                        return Err(UnifiedDiffParserException::new(
224                            "expected data line not found",
225                        ));
226                    }
227
228                    if (self.original_txt.len() == self.old_size
229                        && self.revised_txt.len() == self.new_size)
230                        || (self.old_size == 0
231                            && self.new_size == 0
232                            && self.original_txt.len() == self.old_ln
233                            && self.revised_txt.len() == self.new_ln)
234                    {
235                        self.finalize_chunk();
236                        break;
237                    }
238                }
239
240                current_line = self
241                    .reader
242                    .read_line()
243                    .map_err(|e| UnifiedDiffParserException::new(e.to_string()))?;
244
245                if let Some(l) = current_line {
246                    let checked = self.check_for_no_new_line_at_the_end_of_the_file(l)?;
247                    current_line = Some(checked);
248                }
249            }
250
251            if let Some(ref l) = current_line {
252                if l.starts_with("--") && !l.starts_with("---") {
253                    break;
254                }
255            } else {
256                break;
257            }
258        }
259
260        // Tail parsing
261        let mut tail_txt = String::new();
262        while let Some(line) = self
263            .reader
264            .read_line()
265            .map_err(|e| UnifiedDiffParserException::new(e.to_string()))?
266        {
267            if !tail_txt.is_empty() {
268                tail_txt.push('\n');
269            }
270            tail_txt.push_str(&line);
271        }
272
273        if !tail_txt.is_empty() {
274            self.data.set_tail_txt(tail_txt);
275        }
276
277        // Flush last file into result dataset
278        if let Some(file) = self.actual_file.take() {
279            self.data.add_file(file);
280        }
281
282        Ok(self.data.clone())
283    }
284
285    fn check_for_no_new_line_at_the_end_of_the_file(
286        &mut self,
287        line: String,
288    ) -> Result<String, UnifiedDiffParserException> {
289        if line == r"\ No newline at end of file" {
290            if let Some(ref mut file) = self.actual_file {
291                file.set_no_new_line_at_the_end_of_the_file(true);
292            }
293            let next_line = self
294                .reader
295                .read_line()
296                .map_err(|e| UnifiedDiffParserException::new(e.to_string()))?;
297            Ok(next_line.unwrap_or_default())
298        } else {
299            Ok(line)
300        }
301    }
302
303    fn init_file_if_necessary(&mut self) -> Result<(), UnifiedDiffParserException> {
304        if !self.original_txt.is_empty() || !self.revised_txt.is_empty() {
305            return Err(UnifiedDiffParserException::new("Invalid state in reader"));
306        }
307
308        if let Some(file) = self.actual_file.take() {
309            self.data.add_file(file);
310        }
311        self.actual_file = Some(UnifiedDiffFile::new());
312        Ok(())
313    }
314
315    fn valid_file_header_line(&self, line: &str) -> bool {
316        DIFF_COMMAND_RE.is_match(line)
317            || SIMILARITY_INDEX_RE.is_match(line)
318            || INDEX_RE.is_match(line)
319            || FROM_FILE_RE.is_match(line)
320            || TO_FILE_RE.is_match(line)
321            || RENAME_FROM_RE.is_match(line)
322            || RENAME_TO_RE.is_match(line)
323            || COPY_FROM_RE.is_match(line)
324            || COPY_TO_RE.is_match(line)
325            || NEW_FILE_MODE_RE.is_match(line)
326            || DELETED_FILE_MODE_RE.is_match(line)
327            || OLD_MODE_RE.is_match(line)
328            || NEW_MODE_RE.is_match(line)
329            || BINARY_ADDED_RE.is_match(line)
330            || BINARY_DELETED_RE.is_match(line)
331            || BINARY_EDITED_RE.is_match(line)
332            || UNIFIED_DIFF_CHUNK_REGEXP.is_match(line)
333    }
334
335    fn process_file_header_line(&mut self, line: &str) -> bool {
336        if let Some(captures) = DIFF_COMMAND_RE.captures(line) {
337            let _ = captures;
338            if let Some(last_line) = self.reader.last_line() {
339                let (from, to) = Self::parse_file_names(last_line);
340                if let Some(ref mut file) = self.actual_file {
341                    file.set_from_file(from);
342                    file.set_to_file(to);
343                    file.set_diff_command(line);
344                }
345            }
346            return true;
347        }
348
349        if let Some(captures) = SIMILARITY_INDEX_RE.captures(line) {
350            if let Some(val) = captures.get(1).and_then(|m| m.as_str().parse::<i32>().ok()) {
351                if let Some(ref mut file) = self.actual_file {
352                    file.set_similarity_index(Some(val));
353                }
354            }
355            return true;
356        }
357
358        if INDEX_RE.is_match(line) {
359            if let Some(ref mut file) = self.actual_file {
360                if line.len() >= 6 {
361                    file.set_index(&line[6..]);
362                }
363            }
364            return true;
365        }
366
367        if FROM_FILE_RE.is_match(line) {
368            let name = Self::extract_file_name(line);
369            let ts = Self::extract_timestamp(line);
370            if let Some(ref mut file) = self.actual_file {
371                file.set_from_file(name);
372                if let Some(t) = ts {
373                    file.set_from_timestamp(t);
374                }
375            }
376            return true;
377        }
378
379        if TO_FILE_RE.is_match(line) {
380            let name = Self::extract_file_name(line);
381            let ts = Self::extract_timestamp(line);
382            if let Some(ref mut file) = self.actual_file {
383                file.set_to_file(name);
384                if let Some(t) = ts {
385                    file.set_to_timestamp(t);
386                }
387            }
388            return true;
389        }
390
391        if let Some(captures) = RENAME_FROM_RE.captures(line) {
392            if let Some(m) = captures.get(1) {
393                if let Some(ref mut file) = self.actual_file {
394                    file.set_rename_from(m.as_str());
395                }
396            }
397            return true;
398        }
399
400        if let Some(captures) = RENAME_TO_RE.captures(line) {
401            if let Some(m) = captures.get(1) {
402                if let Some(ref mut file) = self.actual_file {
403                    file.set_rename_to(m.as_str());
404                }
405            }
406            return true;
407        }
408
409        if let Some(captures) = COPY_FROM_RE.captures(line) {
410            if let Some(m) = captures.get(1) {
411                if let Some(ref mut file) = self.actual_file {
412                    file.set_copy_from(m.as_str());
413                }
414            }
415            return true;
416        }
417
418        if let Some(captures) = COPY_TO_RE.captures(line) {
419            if let Some(m) = captures.get(1) {
420                if let Some(ref mut file) = self.actual_file {
421                    file.set_copy_to(m.as_str());
422                }
423            }
424            return true;
425        }
426
427        if let Some(captures) = NEW_FILE_MODE_RE.captures(line) {
428            if let Some(m) = captures.get(1) {
429                if let Some(ref mut file) = self.actual_file {
430                    file.set_new_file_mode(m.as_str());
431                }
432            }
433            return true;
434        }
435
436        if let Some(captures) = DELETED_FILE_MODE_RE.captures(line) {
437            if let Some(m) = captures.get(1) {
438                if let Some(ref mut file) = self.actual_file {
439                    file.set_deleted_file_mode(m.as_str());
440                }
441            }
442            return true;
443        }
444
445        if let Some(captures) = OLD_MODE_RE.captures(line) {
446            if let Some(m) = captures.get(1) {
447                if let Some(ref mut file) = self.actual_file {
448                    file.set_old_mode(m.as_str());
449                }
450            }
451            return true;
452        }
453
454        if let Some(captures) = NEW_MODE_RE.captures(line) {
455            if let Some(m) = captures.get(1) {
456                if let Some(ref mut file) = self.actual_file {
457                    file.set_new_mode(m.as_str());
458                }
459            }
460            return true;
461        }
462
463        if let Some(captures) = BINARY_ADDED_RE.captures(line) {
464            if let Some(m) = captures.get(1) {
465                if let Some(ref mut file) = self.actual_file {
466                    file.set_binary_added(m.as_str());
467                }
468            }
469            return true;
470        }
471
472        if let Some(captures) = BINARY_DELETED_RE.captures(line) {
473            if let Some(m) = captures.get(1) {
474                if let Some(ref mut file) = self.actual_file {
475                    file.set_binary_deleted(m.as_str());
476                }
477            }
478            return true;
479        }
480
481        if let Some(captures) = BINARY_EDITED_RE.captures(line) {
482            if let Some(m) = captures.get(1) {
483                if let Some(ref mut file) = self.actual_file {
484                    file.set_binary_edited(m.as_str());
485                }
486            }
487            return true;
488        }
489
490        false
491    }
492
493    fn process_chunk_line(&mut self, line: &str) -> Result<(), UnifiedDiffParserException> {
494        if let Some(captures) = UNIFIED_DIFF_CHUNK_REGEXP.captures(line) {
495            self.old_ln = captures
496                .get(1)
497                .and_then(|m| m.as_str().parse::<usize>().ok())
498                .unwrap_or(1);
499            self.old_size = captures
500                .get(2)
501                .and_then(|m| m.as_str().parse::<usize>().ok())
502                .unwrap_or(1);
503            self.new_ln = captures
504                .get(3)
505                .and_then(|m| m.as_str().parse::<usize>().ok())
506                .unwrap_or(1);
507            self.new_size = captures
508                .get(4)
509                .and_then(|m| m.as_str().parse::<usize>().ok())
510                .unwrap_or(1);
511
512            if self.old_ln == 0 {
513                self.old_ln = 1;
514            }
515            if self.new_ln == 0 {
516                self.new_ln = 1;
517            }
518            Ok(())
519        } else {
520            Err(UnifiedDiffParserException::new("Invalid chunk header"))
521        }
522    }
523
524    fn process_data_line(&mut self, line: &str) -> bool {
525        if LINE_NORMAL_RE.is_match(line) {
526            let cline = &line[1..];
527            self.original_txt.push(cline.to_string());
528            self.revised_txt.push(cline.to_string());
529            self.del_line_idx += 1;
530            self.add_line_idx += 1;
531            true
532        } else if LINE_ADD_RE.is_match(line) {
533            let cline = &line[1..];
534            self.revised_txt.push(cline.to_string());
535            self.add_line_idx += 1;
536            self.add_line_idx_list
537                .push(self.new_ln - 1 + self.add_line_idx);
538            true
539        } else if LINE_DEL_RE.is_match(line) {
540            let cline = &line[1..];
541            self.original_txt.push(cline.to_string());
542            self.del_line_idx += 1;
543            self.del_line_idx_list
544                .push(self.old_ln - 1 + self.del_line_idx);
545            true
546        } else {
547            false
548        }
549    }
550
551    fn finalize_chunk(&mut self) {
552        if !self.original_txt.is_empty() || !self.revised_txt.is_empty() {
553            let has_deletes = !self.del_line_idx_list.is_empty();
554            let has_inserts = !self.add_line_idx_list.is_empty();
555            let has_context = self.original_txt.len() != self.del_line_idx_list.len()
556                || self.revised_txt.len() != self.add_line_idx_list.len();
557
558            let orig_chunk = Chunk::new(
559                self.old_ln.saturating_sub(1),
560                self.original_txt.clone(),
561                Some(self.del_line_idx_list.clone()),
562            );
563            let rev_chunk = Chunk::new(
564                self.new_ln.saturating_sub(1),
565                self.revised_txt.clone(),
566                Some(self.add_line_idx_list.clone()),
567            );
568
569            let delta: Delta<String> = if has_context || (has_deletes && has_inserts) {
570                ChangeDelta::new(orig_chunk, rev_chunk).into()
571            } else if has_deletes {
572                DeleteDelta::new(orig_chunk, rev_chunk).into()
573            } else if has_inserts {
574                InsertDelta::new(orig_chunk, rev_chunk).into()
575            } else {
576                EqualDelta::new(orig_chunk, rev_chunk).into()
577            };
578
579            if let Some(ref mut file) = self.actual_file {
580                file.patch_mut().add_delta(delta);
581            }
582
583            self.old_ln = 0;
584            self.new_ln = 0;
585            self.original_txt.clear();
586            self.revised_txt.clear();
587            self.add_line_idx_list.clear();
588            self.del_line_idx_list.clear();
589            self.del_line_idx = 0;
590            self.add_line_idx = 0;
591        }
592    }
593}