Patch.java

  1. /*
  2.  * Copyright (C) 2008-2009, Google Inc. and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */

  10. package org.eclipse.jgit.patch;

  11. import static org.eclipse.jgit.lib.Constants.encodeASCII;
  12. import static org.eclipse.jgit.patch.FileHeader.NEW_NAME;
  13. import static org.eclipse.jgit.patch.FileHeader.OLD_NAME;
  14. import static org.eclipse.jgit.patch.FileHeader.isHunkHdr;
  15. import static org.eclipse.jgit.util.RawParseUtils.match;
  16. import static org.eclipse.jgit.util.RawParseUtils.nextLF;

  17. import java.io.IOException;
  18. import java.io.InputStream;
  19. import java.util.ArrayList;
  20. import java.util.List;

  21. import org.eclipse.jgit.internal.JGitText;
  22. import org.eclipse.jgit.util.TemporaryBuffer;

  23. /**
  24.  * A parsed collection of {@link org.eclipse.jgit.patch.FileHeader}s from a
  25.  * unified diff patch file
  26.  */
  27. public class Patch {
  28.     static final byte[] DIFF_GIT = encodeASCII("diff --git "); //$NON-NLS-1$

  29.     private static final byte[] DIFF_CC = encodeASCII("diff --cc "); //$NON-NLS-1$

  30.     private static final byte[] DIFF_COMBINED = encodeASCII("diff --combined "); //$NON-NLS-1$

  31.     private static final byte[][] BIN_HEADERS = new byte[][] {
  32.             encodeASCII("Binary files "), encodeASCII("Files "), }; //$NON-NLS-1$ //$NON-NLS-2$

  33.     private static final byte[] BIN_TRAILER = encodeASCII(" differ\n"); //$NON-NLS-1$

  34.     private static final byte[] GIT_BINARY = encodeASCII("GIT binary patch\n"); //$NON-NLS-1$

  35.     static final byte[] SIG_FOOTER = encodeASCII("-- \n"); //$NON-NLS-1$

  36.     /** The files, in the order they were parsed out of the input. */
  37.     private final List<FileHeader> files;

  38.     /** Formatting errors, if any were identified. */
  39.     private final List<FormatError> errors;

  40.     /**
  41.      * Create an empty patch.
  42.      */
  43.     public Patch() {
  44.         files = new ArrayList<>();
  45.         errors = new ArrayList<>(0);
  46.     }

  47.     /**
  48.      * Add a single file to this patch.
  49.      * <p>
  50.      * Typically files should be added by parsing the text through one of this
  51.      * class's parse methods.
  52.      *
  53.      * @param fh
  54.      *            the header of the file.
  55.      */
  56.     public void addFile(FileHeader fh) {
  57.         files.add(fh);
  58.     }

  59.     /**
  60.      * Get list of files described in the patch, in occurrence order.
  61.      *
  62.      * @return list of files described in the patch, in occurrence order.
  63.      */
  64.     public List<? extends FileHeader> getFiles() {
  65.         return files;
  66.     }

  67.     /**
  68.      * Add a formatting error to this patch script.
  69.      *
  70.      * @param err
  71.      *            the error description.
  72.      */
  73.     public void addError(FormatError err) {
  74.         errors.add(err);
  75.     }

  76.     /**
  77.      * Get collection of formatting errors.
  78.      *
  79.      * @return collection of formatting errors, if any.
  80.      */
  81.     public List<FormatError> getErrors() {
  82.         return errors;
  83.     }

  84.     /**
  85.      * Parse a patch received from an InputStream.
  86.      * <p>
  87.      * Multiple parse calls on the same instance will concatenate the patch
  88.      * data, but each parse input must start with a valid file header (don't
  89.      * split a single file across parse calls).
  90.      *
  91.      * @param is
  92.      *            the stream to read the patch data from. The stream is read
  93.      *            until EOF is reached.
  94.      * @throws java.io.IOException
  95.      *             there was an error reading from the input stream.
  96.      */
  97.     public void parse(InputStream is) throws IOException {
  98.         final byte[] buf = readFully(is);
  99.         parse(buf, 0, buf.length);
  100.     }

  101.     private static byte[] readFully(InputStream is) throws IOException {
  102.         try (TemporaryBuffer b = new TemporaryBuffer.Heap(Integer.MAX_VALUE)) {
  103.             b.copy(is);
  104.             return b.toByteArray();
  105.         }
  106.     }

  107.     /**
  108.      * Parse a patch stored in a byte[].
  109.      * <p>
  110.      * Multiple parse calls on the same instance will concatenate the patch
  111.      * data, but each parse input must start with a valid file header (don't
  112.      * split a single file across parse calls).
  113.      *
  114.      * @param buf
  115.      *            the buffer to parse.
  116.      * @param ptr
  117.      *            starting position to parse from.
  118.      * @param end
  119.      *            1 past the last position to end parsing. The total length to
  120.      *            be parsed is <code>end - ptr</code>.
  121.      */
  122.     public void parse(byte[] buf, int ptr, int end) {
  123.         while (ptr < end)
  124.             ptr = parseFile(buf, ptr, end);
  125.     }

  126.     private int parseFile(byte[] buf, int c, int end) {
  127.         while (c < end) {
  128.             if (isHunkHdr(buf, c, end) >= 1) {
  129.                 // If we find a disconnected hunk header we might
  130.                 // have missed a file header previously. The hunk
  131.                 // isn't valid without knowing where it comes from.
  132.                 //
  133.                 error(buf, c, JGitText.get().hunkDisconnectedFromFile);
  134.                 c = nextLF(buf, c);
  135.                 continue;
  136.             }

  137.             // Valid git style patch?
  138.             //
  139.             if (match(buf, c, DIFF_GIT) >= 0)
  140.                 return parseDiffGit(buf, c, end);
  141.             if (match(buf, c, DIFF_CC) >= 0)
  142.                 return parseDiffCombined(DIFF_CC, buf, c, end);
  143.             if (match(buf, c, DIFF_COMBINED) >= 0)
  144.                 return parseDiffCombined(DIFF_COMBINED, buf, c, end);

  145.             // Junk between files? Leading junk? Traditional
  146.             // (non-git generated) patch?
  147.             //
  148.             final int n = nextLF(buf, c);
  149.             if (n >= end) {
  150.                 // Patches cannot be only one line long. This must be
  151.                 // trailing junk that we should ignore.
  152.                 //
  153.                 return end;
  154.             }

  155.             if (n - c < 6) {
  156.                 // A valid header must be at least 6 bytes on the
  157.                 // first line, e.g. "--- a/b\n".
  158.                 //
  159.                 c = n;
  160.                 continue;
  161.             }

  162.             if (match(buf, c, OLD_NAME) >= 0 && match(buf, n, NEW_NAME) >= 0) {
  163.                 // Probably a traditional patch. Ensure we have at least
  164.                 // a "@@ -0,0" smelling line next. We only check the "@@ -".
  165.                 //
  166.                 final int f = nextLF(buf, n);
  167.                 if (f >= end)
  168.                     return end;
  169.                 if (isHunkHdr(buf, f, end) == 1)
  170.                     return parseTraditionalPatch(buf, c, end);
  171.             }

  172.             c = n;
  173.         }
  174.         return c;
  175.     }

  176.     private int parseDiffGit(byte[] buf, int start, int end) {
  177.         final FileHeader fh = new FileHeader(buf, start);
  178.         int ptr = fh.parseGitFileName(start + DIFF_GIT.length, end);
  179.         if (ptr < 0)
  180.             return skipFile(buf, start);

  181.         ptr = fh.parseGitHeaders(ptr, end);
  182.         ptr = parseHunks(fh, ptr, end);
  183.         fh.endOffset = ptr;
  184.         addFile(fh);
  185.         return ptr;
  186.     }

  187.     private int parseDiffCombined(final byte[] hdr, final byte[] buf,
  188.             final int start, final int end) {
  189.         final CombinedFileHeader fh = new CombinedFileHeader(buf, start);
  190.         int ptr = fh.parseGitFileName(start + hdr.length, end);
  191.         if (ptr < 0)
  192.             return skipFile(buf, start);

  193.         ptr = fh.parseGitHeaders(ptr, end);
  194.         ptr = parseHunks(fh, ptr, end);
  195.         fh.endOffset = ptr;
  196.         addFile(fh);
  197.         return ptr;
  198.     }

  199.     private int parseTraditionalPatch(final byte[] buf, final int start,
  200.             final int end) {
  201.         final FileHeader fh = new FileHeader(buf, start);
  202.         int ptr = fh.parseTraditionalHeaders(start, end);
  203.         ptr = parseHunks(fh, ptr, end);
  204.         fh.endOffset = ptr;
  205.         addFile(fh);
  206.         return ptr;
  207.     }

  208.     private static int skipFile(byte[] buf, int ptr) {
  209.         ptr = nextLF(buf, ptr);
  210.         if (match(buf, ptr, OLD_NAME) >= 0)
  211.             ptr = nextLF(buf, ptr);
  212.         return ptr;
  213.     }

  214.     private int parseHunks(FileHeader fh, int c, int end) {
  215.         final byte[] buf = fh.buf;
  216.         while (c < end) {
  217.             // If we see a file header at this point, we have all of the
  218.             // hunks for our current file. We should stop and report back
  219.             // with this position so it can be parsed again later.
  220.             //
  221.             if (match(buf, c, DIFF_GIT) >= 0)
  222.                 break;
  223.             if (match(buf, c, DIFF_CC) >= 0)
  224.                 break;
  225.             if (match(buf, c, DIFF_COMBINED) >= 0)
  226.                 break;
  227.             if (match(buf, c, OLD_NAME) >= 0)
  228.                 break;
  229.             if (match(buf, c, NEW_NAME) >= 0)
  230.                 break;

  231.             if (isHunkHdr(buf, c, end) == fh.getParentCount()) {
  232.                 final HunkHeader h = fh.newHunkHeader(c);
  233.                 h.parseHeader();
  234.                 c = h.parseBody(this, end);
  235.                 h.endOffset = c;
  236.                 fh.addHunk(h);
  237.                 if (c < end) {
  238.                     switch (buf[c]) {
  239.                     case '@':
  240.                     case 'd':
  241.                     case '\n':
  242.                         break;
  243.                     default:
  244.                         if (match(buf, c, SIG_FOOTER) < 0)
  245.                             warn(buf, c, JGitText.get().unexpectedHunkTrailer);
  246.                     }
  247.                 }
  248.                 continue;
  249.             }

  250.             final int eol = nextLF(buf, c);
  251.             if (fh.getHunks().isEmpty() && match(buf, c, GIT_BINARY) >= 0) {
  252.                 fh.patchType = FileHeader.PatchType.GIT_BINARY;
  253.                 return parseGitBinary(fh, eol, end);
  254.             }

  255.             if (fh.getHunks().isEmpty() && BIN_TRAILER.length < eol - c
  256.                     && match(buf, eol - BIN_TRAILER.length, BIN_TRAILER) >= 0
  257.                     && matchAny(buf, c, BIN_HEADERS)) {
  258.                 // The patch is a binary file diff, with no deltas.
  259.                 //
  260.                 fh.patchType = FileHeader.PatchType.BINARY;
  261.                 return eol;
  262.             }

  263.             // Skip this line and move to the next. Its probably garbage
  264.             // after the last hunk of a file.
  265.             //
  266.             c = eol;
  267.         }

  268.         if (fh.getHunks().isEmpty()
  269.                 && fh.getPatchType() == FileHeader.PatchType.UNIFIED
  270.                 && !fh.hasMetaDataChanges()) {
  271.             // Hmm, an empty patch? If there is no metadata here we
  272.             // really have a binary patch that we didn't notice above.
  273.             //
  274.             fh.patchType = FileHeader.PatchType.BINARY;
  275.         }

  276.         return c;
  277.     }

  278.     private int parseGitBinary(FileHeader fh, int c, int end) {
  279.         final BinaryHunk postImage = new BinaryHunk(fh, c);
  280.         final int nEnd = postImage.parseHunk(c, end);
  281.         if (nEnd < 0) {
  282.             // Not a binary hunk.
  283.             //
  284.             error(fh.buf, c, JGitText.get().missingForwardImageInGITBinaryPatch);
  285.             return c;
  286.         }
  287.         c = nEnd;
  288.         postImage.endOffset = c;
  289.         fh.forwardBinaryHunk = postImage;

  290.         final BinaryHunk preImage = new BinaryHunk(fh, c);
  291.         final int oEnd = preImage.parseHunk(c, end);
  292.         if (oEnd >= 0) {
  293.             c = oEnd;
  294.             preImage.endOffset = c;
  295.             fh.reverseBinaryHunk = preImage;
  296.         }

  297.         return c;
  298.     }

  299.     void warn(byte[] buf, int ptr, String msg) {
  300.         addError(new FormatError(buf, ptr, FormatError.Severity.WARNING, msg));
  301.     }

  302.     void error(byte[] buf, int ptr, String msg) {
  303.         addError(new FormatError(buf, ptr, FormatError.Severity.ERROR, msg));
  304.     }

  305.     private static boolean matchAny(final byte[] buf, final int c,
  306.             final byte[][] srcs) {
  307.         for (byte[] s : srcs) {
  308.             if (match(buf, c, s) >= 0)
  309.                 return true;
  310.         }
  311.         return false;
  312.     }
  313. }