java_diff_utils_rs/unifieddiff/
unified_diff_reader.rs1use 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
79pub struct UnifiedDiffReader<R: Read> {
81 reader: InternalUnifiedDiffReader<R>,
82 data: UnifiedDiff,
83 actual_file: Option<UnifiedDiffFile>,
84
85 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 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 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 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 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}