1 /******************************************************************************* 2 * Copyright (c) 2006, 2018 IBM Corporation and others. 3 * 4 * This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * which accompanies this distribution, and is available at 7 * https://www.eclipse.org/legal/epl-2.0/ 8 * 9 * SPDX-License-Identifier: EPL-2.0 10 * 11 * Contributors: 12 * IBM Corporation - initial API and implementation 13 *******************************************************************************/ 14 package org.eclipse.compare.internal.core.patch; 15 16 import java.io.BufferedReader; 17 import java.io.IOException; 18 import java.text.*; 19 import java.util.*; 20 import java.util.regex.Pattern; 21 22 import org.eclipse.compare.patch.IFilePatch2; 23 import org.eclipse.core.runtime.*; 24 25 public class PatchReader { 26 private static final boolean DEBUG= false; 27 28 private static final String DEV_NULL= "/dev/null"; //$NON-NLS-1$ 29 30 protected static final String MARKER_TYPE= "org.eclipse.compare.rejectedPatchMarker"; //$NON-NLS-1$ 31 32 // diff formats 33 // private static final int CONTEXT= 0; 34 // private static final int ED= 1; 35 // private static final int NORMAL= 2; 36 // private static final int UNIFIED= 3; 37 38 // we recognize the following date/time formats 39 private DateFormat[] fDateFormats= new DateFormat[] { 40 new SimpleDateFormat("EEE MMM dd kk:mm:ss yyyy"), //$NON-NLS-1$ 41 new SimpleDateFormat("yyyy/MM/dd kk:mm:ss"), //$NON-NLS-1$ 42 new SimpleDateFormat("EEE MMM dd kk:mm:ss yyyy", Locale.US) //$NON-NLS-1$ 43 }; 44 45 private boolean fIsWorkspacePatch; 46 private boolean fIsGitPatch; 47 private DiffProject[] fDiffProjects; 48 private FilePatch2[] fDiffs; 49 50 // API for writing new multi-project patch format 51 public static final String MULTIPROJECTPATCH_HEADER= "### Eclipse Workspace Patch"; //$NON-NLS-1$ 52 53 public static final String MULTIPROJECTPATCH_VERSION= "1.0"; //$NON-NLS-1$ 54 55 public static final String MULTIPROJECTPATCH_PROJECT= "#P"; //$NON-NLS-1$ 56 57 private static final Pattern GIT_PATCH_PATTERN= Pattern.compile("^diff --git a/.+ b/.+[\r\n]+$"); //$NON-NLS-1$ 58 59 /** 60 * Create a patch reader for the default date formats. 61 */ PatchReader()62 public PatchReader() { 63 // nothing here 64 } 65 66 /** 67 * Create a patch reader for the given date formats. 68 * 69 * @param dateFormats 70 * Array of <code>DateFormat</code>s to be used when 71 * extracting dates from the patch. 72 */ PatchReader(DateFormat[] dateFormats)73 public PatchReader(DateFormat[] dateFormats) { 74 this(); 75 this.fDateFormats = dateFormats; 76 } 77 parse(BufferedReader reader)78 public void parse(BufferedReader reader) throws IOException { 79 List<FilePatch2> diffs= new ArrayList<>(); 80 HashMap<String, DiffProject> diffProjects= new HashMap<>(4); 81 String line= null; 82 boolean reread= false; 83 String diffArgs= null; 84 String fileName= null; 85 // no project means this is a single patch,create a placeholder project for now 86 // which will be replaced by the target selected by the user in the preview pane 87 String projectName= ""; //$NON-NLS-1$ 88 this.fIsWorkspacePatch= false; 89 this.fIsGitPatch = false; 90 91 LineReader lr= new LineReader(reader); 92 lr.ignoreSingleCR(); // Don't treat single CRs as line feeds to be consistent with command line patch 93 // Test for our format 94 line= lr.readLine(); 95 if (line != null && line.startsWith(PatchReader.MULTIPROJECTPATCH_HEADER)) { 96 this.fIsWorkspacePatch= true; 97 } else { 98 parse(lr, line); 99 return; 100 } 101 102 // read leading garbage 103 while (true) { 104 if (!reread) 105 line= lr.readLine(); 106 reread= false; 107 if (line == null) 108 break; 109 if (line.length() < 4) 110 continue; // too short 111 112 if (line.startsWith(PatchReader.MULTIPROJECTPATCH_PROJECT)) { 113 projectName= line.substring(2).trim(); 114 continue; 115 } 116 117 if (line.startsWith("Index: ")) { //$NON-NLS-1$ 118 fileName= line.substring(7).trim(); 119 continue; 120 } 121 if (line.startsWith("diff")) { //$NON-NLS-1$ 122 diffArgs= line.substring(4).trim(); 123 continue; 124 } 125 126 if (line.startsWith("--- ")) { //$NON-NLS-1$ 127 // if there is no current project or 128 // the current project doesn't equal the newly parsed project 129 // reset the current project to the newly parsed one, create a new DiffProject 130 // and add it to the array 131 DiffProject diffProject; 132 if (!diffProjects.containsKey(projectName)) { 133 diffProject= new DiffProject(projectName); 134 diffProjects.put(projectName, diffProject); 135 } else { 136 diffProject= diffProjects.get(projectName); 137 } 138 139 line= readUnifiedDiff(diffs, lr, line, diffArgs, fileName, diffProject); 140 diffArgs= fileName= null; 141 reread= true; 142 } 143 } 144 145 lr.close(); 146 147 this.fDiffProjects= diffProjects.values().toArray(new DiffProject[diffProjects.size()]); 148 this.fDiffs = diffs.toArray(new FilePatch2[diffs.size()]); 149 } 150 createFileDiff(IPath oldPath, long oldDate, IPath newPath, long newDate)151 protected FilePatch2 createFileDiff(IPath oldPath, long oldDate, 152 IPath newPath, long newDate) { 153 return new FilePatch2(oldPath, oldDate, newPath, newDate); 154 } 155 readUnifiedDiff(List<FilePatch2> diffs, LineReader lr, String line, String diffArgs, String fileName, DiffProject diffProject)156 private String readUnifiedDiff(List<FilePatch2> diffs, LineReader lr, String line, String diffArgs, String fileName, DiffProject diffProject) throws IOException { 157 List<FilePatch2> newDiffs= new ArrayList<>(); 158 String nextLine= readUnifiedDiff(newDiffs, lr, line, diffArgs, fileName); 159 for (FilePatch2 diff : newDiffs) { 160 diffProject.add(diff); 161 diffs.add(diff); 162 } 163 return nextLine; 164 } 165 parse(LineReader lr, String line)166 public void parse(LineReader lr, String line) throws IOException { 167 List<FilePatch2> diffs= new ArrayList<>(); 168 boolean reread= false; 169 String diffArgs= null; 170 String fileName= null; 171 List<String> headerLines = new ArrayList<>(); 172 boolean foundDiff= false; 173 174 // read leading garbage 175 reread= line!=null; 176 while (true) { 177 if (!reread) 178 line= lr.readLine(); 179 reread= false; 180 if (line == null) 181 break; 182 183 // remember some infos 184 if (line.startsWith("Index: ")) { //$NON-NLS-1$ 185 fileName= line.substring(7).trim(); 186 } else if (line.startsWith("diff")) { //$NON-NLS-1$ 187 if (!foundDiff && GIT_PATCH_PATTERN.matcher(line).matches()) 188 this.fIsGitPatch= true; 189 foundDiff= true; 190 diffArgs= line.substring(4).trim(); 191 } else if (line.startsWith("--- ")) { //$NON-NLS-1$ 192 line= readUnifiedDiff(diffs, lr, line, diffArgs, fileName); 193 if (!headerLines.isEmpty()) 194 setHeader(diffs.get(diffs.size() - 1), headerLines); 195 diffArgs= fileName= null; 196 reread= true; 197 } else if (line.startsWith("*** ")) { //$NON-NLS-1$ 198 line= readContextDiff(diffs, lr, line, diffArgs, fileName); 199 if (!headerLines.isEmpty()) 200 setHeader(diffs.get(diffs.size() - 1), headerLines); 201 diffArgs= fileName= null; 202 reread= true; 203 } 204 205 // Any lines we read here are header lines. 206 // However, if reread is set, we will add them to the header on the next pass through 207 if (!reread) { 208 headerLines.add(line); 209 } 210 } 211 212 lr.close(); 213 214 this.fDiffs = diffs.toArray(new FilePatch2[diffs.size()]); 215 } 216 setHeader(FilePatch2 diff, List<String> headerLines)217 private void setHeader(FilePatch2 diff, List<String> headerLines) { 218 String header = LineReader.createString(false, headerLines); 219 diff.setHeader(header); 220 headerLines.clear(); 221 } 222 223 /* 224 * Returns the next line that does not belong to this diff 225 */ readUnifiedDiff(List<FilePatch2> diffs, LineReader reader, String line, String args, String fileName)226 protected String readUnifiedDiff(List<FilePatch2> diffs, LineReader reader, String line, String args, String fileName) throws IOException { 227 228 String[] oldArgs= split(line.substring(4)); 229 230 // read info about new file 231 line= reader.readLine(); 232 if (line == null || !line.startsWith("+++ ")) //$NON-NLS-1$ 233 return line; 234 235 String[] newArgs= split(line.substring(4)); 236 237 FilePatch2 diff = createFileDiff(extractPath(oldArgs, 0, fileName), 238 extractDate(oldArgs, 1), extractPath(newArgs, 0, fileName), 239 extractDate(newArgs, 1)); 240 diffs.add(diff); 241 242 int[] oldRange= new int[2]; 243 int[] newRange= new int[2]; 244 int remainingOld= -1; // remaining old lines for current hunk 245 int remainingNew= -1; // remaining new lines for current hunk 246 List<String> lines= new ArrayList<>(); 247 248 boolean encounteredPlus = false; 249 boolean encounteredMinus = false; 250 boolean encounteredSpace = false; 251 252 try { 253 // read lines of hunk 254 while (true) { 255 256 line= reader.readLine(); 257 if (line == null) 258 return null; 259 260 if (reader.lineContentLength(line) == 0) { 261 //System.out.println("Warning: found empty line in hunk; ignored"); 262 //lines.add(' ' + line); 263 continue; 264 } 265 266 char c= line.charAt(0); 267 if (remainingOld == 0 && remainingNew == 0 && c != '@' && c != '\\') { 268 return line; 269 } 270 271 switch (c) { 272 case '@': 273 if (line.startsWith("@@ ")) { //$NON-NLS-1$ 274 // flush old hunk 275 if (lines.size() > 0) { 276 Hunk.createHunk(diff, oldRange, newRange, lines, encounteredPlus, encounteredMinus, encounteredSpace); 277 lines.clear(); 278 } 279 280 // format: @@ -oldStart,oldLength +newStart,newLength @@ 281 extractPair(line, '-', oldRange); 282 extractPair(line, '+', newRange); 283 remainingOld= oldRange[1]; 284 remainingNew= newRange[1]; 285 continue; 286 } 287 break; 288 case ' ': 289 encounteredSpace= true; 290 remainingOld--; 291 remainingNew--; 292 lines.add(line); 293 continue; 294 case '+': 295 encounteredPlus= true; 296 remainingNew--; 297 lines.add(line); 298 continue; 299 case '-': 300 encounteredMinus= true; 301 remainingOld--; 302 lines.add(line); 303 continue; 304 case '\\': 305 if (line.indexOf("newline at end") > 0) { //$NON-NLS-1$ 306 int lastIndex= lines.size(); 307 if (lastIndex > 0) { 308 line= lines.get(lastIndex - 1); 309 int end= line.length() - 1; 310 char lc= line.charAt(end); 311 if (lc == '\n') { 312 end--; 313 if (end > 0 && line.charAt(end) == '\r') 314 end--; 315 } else if (lc == '\r') { 316 end--; 317 } 318 line= line.substring(0, end + 1); 319 lines.set(lastIndex - 1, line); 320 } 321 continue; 322 } 323 break; 324 case '#': 325 break; 326 case 'I': 327 if (line.indexOf("Index:") == 0) //$NON-NLS-1$ 328 break; 329 //$FALL-THROUGH$ 330 case 'd': 331 if (line.indexOf("diff ") == 0) //$NON-NLS-1$ 332 break; 333 //$FALL-THROUGH$ 334 case 'B': 335 if (line.indexOf("Binary files differ") == 0) //$NON-NLS-1$ 336 break; 337 //$FALL-THROUGH$ 338 default: 339 break; 340 } 341 return line; 342 } 343 } finally { 344 if (lines.size() > 0) 345 Hunk.createHunk(diff, oldRange, newRange, lines, encounteredPlus, encounteredMinus, encounteredSpace); 346 } 347 } 348 349 /* 350 * Returns the next line that does not belong to this diff 351 */ readContextDiff(List<FilePatch2> diffs, LineReader reader, String line, String args, String fileName)352 private String readContextDiff(List<FilePatch2> diffs, LineReader reader, String line, String args, String fileName) throws IOException { 353 354 String[] oldArgs= split(line.substring(4)); 355 356 // read info about new file 357 line= reader.readLine(); 358 if (line == null || !line.startsWith("--- ")) //$NON-NLS-1$ 359 return line; 360 361 String[] newArgs= split(line.substring(4)); 362 363 FilePatch2 diff = createFileDiff(extractPath(oldArgs, 0, fileName), 364 extractDate(oldArgs, 1), extractPath(newArgs, 0, fileName), 365 extractDate(newArgs, 1)); 366 diffs.add(diff); 367 368 int[] oldRange= new int[2]; 369 int[] newRange= new int[2]; 370 List<String> oldLines= new ArrayList<>(); 371 List<String> newLines= new ArrayList<>(); 372 List<String> lines= oldLines; 373 374 375 boolean encounteredPlus = false; 376 boolean encounteredMinus = false; 377 boolean encounteredSpace = false; 378 379 try { 380 // read lines of hunk 381 while (true) { 382 383 line= reader.readLine(); 384 if (line == null) 385 return line; 386 387 int l= line.length(); 388 if (l == 0) 389 continue; 390 if (l > 1) { 391 switch (line.charAt(0)) { 392 case '*': 393 if (line.startsWith("***************")) { // new hunk //$NON-NLS-1$ 394 // flush old hunk 395 if (oldLines.size() > 0 || newLines.size() > 0) { 396 Hunk.createHunk(diff, oldRange, newRange, unifyLines(oldLines, newLines), encounteredPlus, encounteredMinus, encounteredSpace); 397 oldLines.clear(); 398 newLines.clear(); 399 } 400 continue; 401 } 402 if (line.startsWith("*** ")) { // old range //$NON-NLS-1$ 403 // format: *** oldStart,oldEnd *** 404 extractPair(line, ' ', oldRange); 405 if (oldRange[0] == 0) { 406 oldRange[1] = 0; // In case of the file addition 407 } else { 408 oldRange[1] = oldRange[1] - oldRange[0] + 1; 409 } 410 lines= oldLines; 411 continue; 412 } 413 break; 414 case ' ': // context line 415 if (line.charAt(1) == ' ') { 416 lines.add(line); 417 continue; 418 } 419 break; 420 case '+': // addition 421 if (line.charAt(1) == ' ') { 422 encounteredPlus = true; 423 lines.add(line); 424 continue; 425 } 426 break; 427 case '!': // change 428 if (line.charAt(1) == ' ') { 429 encounteredSpace = true; 430 lines.add(line); 431 continue; 432 } 433 break; 434 case '-': 435 if (line.charAt(1) == ' ') { // deletion 436 encounteredMinus = true; 437 lines.add(line); 438 continue; 439 } 440 if (line.startsWith("--- ")) { // new range //$NON-NLS-1$ 441 // format: *** newStart,newEnd *** 442 extractPair(line, ' ', newRange); 443 if (newRange[0] == 0) { 444 newRange[1] = 0; // In case of the file removal 445 } else { 446 newRange[1] = newRange[1] - newRange[0] + 1; 447 } 448 lines= newLines; 449 continue; 450 } 451 break; 452 default: 453 break; 454 } 455 } 456 return line; 457 } 458 } finally { 459 // flush last hunk 460 if (oldLines.size() > 0 || newLines.size() > 0) 461 Hunk.createHunk(diff, oldRange, newRange, unifyLines(oldLines, newLines), encounteredPlus, encounteredMinus, encounteredSpace); 462 } 463 } 464 465 /* 466 * Creates a List of lines in the unified format from 467 * two Lists of lines in the 'classic' format. 468 */ unifyLines(List<String> oldLines, List<String> newLines)469 private List<String> unifyLines(List<String> oldLines, List<String> newLines) { 470 List<String> result= new ArrayList<>(); 471 472 String[] ol= oldLines.toArray(new String[oldLines.size()]); 473 String[] nl= newLines.toArray(new String[newLines.size()]); 474 475 int oi= 0, ni= 0; 476 477 while (true) { 478 479 char oc= 0; 480 String o= null; 481 if (oi < ol.length) { 482 o= ol[oi]; 483 oc= o.charAt(0); 484 } 485 486 char nc= 0; 487 String n= null; 488 if (ni < nl.length) { 489 n= nl[ni]; 490 nc= n.charAt(0); 491 } 492 493 // EOF 494 if (oc == 0 && nc == 0) 495 break; 496 497 // deletion in old 498 if (oc == '-') { 499 do { 500 result.add('-' + o.substring(2)); 501 oi++; 502 if (oi >= ol.length) 503 break; 504 o= ol[oi]; 505 } while (o.charAt(0) == '-'); 506 continue; 507 } 508 509 // addition in new 510 if (nc == '+') { 511 do { 512 result.add('+' + n.substring(2)); 513 ni++; 514 if (ni >= nl.length) 515 break; 516 n= nl[ni]; 517 } while (n.charAt(0) == '+'); 518 continue; 519 } 520 521 // differing lines on both sides 522 if (oc == '!' && nc == '!') { 523 // remove old 524 do { 525 result.add('-' + o.substring(2)); 526 oi++; 527 if (oi >= ol.length) 528 break; 529 o= ol[oi]; 530 } while (o.charAt(0) == '!'); 531 532 // add new 533 do { 534 result.add('+' + n.substring(2)); 535 ni++; 536 if (ni >= nl.length) 537 break; 538 n= nl[ni]; 539 } while (n.charAt(0) == '!'); 540 541 continue; 542 } 543 544 // context lines 545 if (oc == ' ' && nc == ' ') { 546 do { 547 Assert.isTrue(o.equals(n), "non matching context lines"); //$NON-NLS-1$ 548 result.add(' ' + o.substring(2)); 549 oi++; 550 ni++; 551 if (oi >= ol.length || ni >= nl.length) 552 break; 553 o= ol[oi]; 554 n= nl[ni]; 555 } while (o.charAt(0) == ' ' && n.charAt(0) == ' '); 556 continue; 557 } 558 559 if (oc == ' ') { 560 do { 561 result.add(' ' + o.substring(2)); 562 oi++; 563 if (oi >= ol.length) 564 break; 565 o= ol[oi]; 566 } while (o.charAt(0) == ' '); 567 continue; 568 } 569 570 if (nc == ' ') { 571 do { 572 result.add(' ' + n.substring(2)); 573 ni++; 574 if (ni >= nl.length) 575 break; 576 n= nl[ni]; 577 } while (n.charAt(0) == ' '); 578 continue; 579 } 580 581 Assert.isTrue(false, "unexpected char <" + oc + "> <" + nc + ">"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ 582 } 583 584 return result; 585 } 586 587 /* 588 * @return the parsed time/date in milliseconds or IFilePatch.DATE_UNKNOWN 589 * (0) on error 590 */ extractDate(String[] args, int n)591 private long extractDate(String[] args, int n) { 592 if (n < args.length) { 593 String line= args[n]; 594 for (DateFormat dateFormat : this.fDateFormats) { 595 dateFormat.setLenient(true); 596 try { 597 Date date = dateFormat.parse(line); 598 return date.getTime(); 599 } catch (ParseException ex) { 600 // silently ignored 601 } 602 } 603 // System.err.println("can't parse date: <" + line + ">"); 604 } 605 return IFilePatch2.DATE_UNKNOWN; 606 } 607 608 /* 609 * Returns null if file name is "/dev/null". 610 */ extractPath(String[] args, int n, String path2)611 private IPath extractPath(String[] args, int n, String path2) { 612 if (n < args.length) { 613 String path= args[n]; 614 if (DEV_NULL.equals(path)) 615 return null; 616 int pos= path.lastIndexOf(':'); 617 if (pos >= 0) 618 path= path.substring(0, pos); 619 if (path2 != null && !path2.equals(path)) { 620 if (DEBUG) System.out.println("path mismatch: " + path2); //$NON-NLS-1$ 621 path= path2; 622 } 623 return new Path(path); 624 } 625 return null; 626 } 627 628 /* 629 * Tries to extract two integers separated by a comma. 630 * The parsing of the line starts at the position after 631 * the first occurrence of the given character start an ends 632 * at the first blank (or the end of the line). 633 * If only a single number is found this is assumed to be the start of a one line range. 634 * If an error occurs the range -1,-1 is returned. 635 */ extractPair(String line, char start, int[] pair)636 private void extractPair(String line, char start, int[] pair) { 637 pair[0]= pair[1]= -1; 638 int startPos= line.indexOf(start); 639 if (startPos < 0) { 640 if (DEBUG) System.out.println("parsing error in extractPair: couldn't find \'" + start + "\'"); //$NON-NLS-1$ //$NON-NLS-2$ 641 return; 642 } 643 line= line.substring(startPos+1); 644 int endPos= line.indexOf(' '); 645 if (endPos < 0) { 646 if (DEBUG) System.out.println("parsing error in extractPair: couldn't find end blank"); //$NON-NLS-1$ 647 return; 648 } 649 line= line.substring(0, endPos); 650 int comma= line.indexOf(','); 651 if (comma >= 0) { 652 pair[0]= Integer.parseInt(line.substring(0, comma)); 653 pair[1]= Integer.parseInt(line.substring(comma+1)); 654 } else { // abbreviated form for one line patch 655 pair[0]= Integer.parseInt(line); 656 pair[1]= 1; 657 } 658 } 659 660 /* 661 * Breaks the given string into tab separated substrings. 662 * Leading and trailing whitespace is removed from each token. 663 */ split(String line)664 private String[] split(String line) { 665 List<String> l= new ArrayList<>(); 666 StringTokenizer st= new StringTokenizer(line, "\t"); //$NON-NLS-1$ 667 while (st.hasMoreElements()) { 668 String token= st.nextToken().trim(); 669 if (token.length() > 0) 670 l.add(token); 671 } 672 return l.toArray(new String[l.size()]); 673 } 674 isWorkspacePatch()675 public boolean isWorkspacePatch() { 676 return this.fIsWorkspacePatch; 677 } 678 isGitPatch()679 public boolean isGitPatch() { 680 return this.fIsGitPatch; 681 } 682 getDiffProjects()683 public DiffProject[] getDiffProjects() { 684 return this.fDiffProjects; 685 } 686 getDiffs()687 public FilePatch2[] getDiffs() { 688 return this.fDiffs; 689 } 690 getAdjustedDiffs()691 public FilePatch2[] getAdjustedDiffs() { 692 if (!isWorkspacePatch() || this.fDiffs.length == 0) 693 return this.fDiffs; 694 List<FilePatch2> result = new ArrayList<>(); 695 for (FilePatch2 diff : this.fDiffs) { 696 result.add(diff.asRelativeDiff()); 697 } 698 return result.toArray(new FilePatch2[result.size()]); 699 } 700 701 } 702