View Javadoc
1   /*
2    * Copyright (C) 2008-2009, Google Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.patch;
45  
46  import static org.eclipse.jgit.lib.Constants.encodeASCII;
47  import static org.eclipse.jgit.patch.FileHeader.NEW_NAME;
48  import static org.eclipse.jgit.patch.FileHeader.OLD_NAME;
49  import static org.eclipse.jgit.patch.FileHeader.isHunkHdr;
50  import static org.eclipse.jgit.util.RawParseUtils.match;
51  import static org.eclipse.jgit.util.RawParseUtils.nextLF;
52  
53  import java.io.IOException;
54  import java.io.InputStream;
55  import java.util.ArrayList;
56  import java.util.List;
57  
58  import org.eclipse.jgit.internal.JGitText;
59  import org.eclipse.jgit.util.TemporaryBuffer;
60  
61  /**
62   * A parsed collection of {@link org.eclipse.jgit.patch.FileHeader}s from a
63   * unified diff patch file
64   */
65  public class Patch {
66  	static final byte[] DIFF_GIT = encodeASCII("diff --git "); //$NON-NLS-1$
67  
68  	private static final byte[] DIFF_CC = encodeASCII("diff --cc "); //$NON-NLS-1$
69  
70  	private static final byte[] DIFF_COMBINED = encodeASCII("diff --combined "); //$NON-NLS-1$
71  
72  	private static final byte[][] BIN_HEADERS = new byte[][] {
73  			encodeASCII("Binary files "), encodeASCII("Files "), }; //$NON-NLS-1$ //$NON-NLS-2$
74  
75  	private static final byte[] BIN_TRAILER = encodeASCII(" differ\n"); //$NON-NLS-1$
76  
77  	private static final byte[] GIT_BINARY = encodeASCII("GIT binary patch\n"); //$NON-NLS-1$
78  
79  	static final byte[] SIG_FOOTER = encodeASCII("-- \n"); //$NON-NLS-1$
80  
81  	/** The files, in the order they were parsed out of the input. */
82  	private final List<FileHeader> files;
83  
84  	/** Formatting errors, if any were identified. */
85  	private final List<FormatError> errors;
86  
87  	/**
88  	 * Create an empty patch.
89  	 */
90  	public Patch() {
91  		files = new ArrayList<>();
92  		errors = new ArrayList<>(0);
93  	}
94  
95  	/**
96  	 * Add a single file to this patch.
97  	 * <p>
98  	 * Typically files should be added by parsing the text through one of this
99  	 * class's parse methods.
100 	 *
101 	 * @param fh
102 	 *            the header of the file.
103 	 */
104 	public void addFile(FileHeader fh) {
105 		files.add(fh);
106 	}
107 
108 	/**
109 	 * Get list of files described in the patch, in occurrence order.
110 	 *
111 	 * @return list of files described in the patch, in occurrence order.
112 	 */
113 	public List<? extends FileHeader> getFiles() {
114 		return files;
115 	}
116 
117 	/**
118 	 * Add a formatting error to this patch script.
119 	 *
120 	 * @param err
121 	 *            the error description.
122 	 */
123 	public void addError(FormatError err) {
124 		errors.add(err);
125 	}
126 
127 	/**
128 	 * Get collection of formatting errors.
129 	 *
130 	 * @return collection of formatting errors, if any.
131 	 */
132 	public List<FormatError> getErrors() {
133 		return errors;
134 	}
135 
136 	/**
137 	 * Parse a patch received from an InputStream.
138 	 * <p>
139 	 * Multiple parse calls on the same instance will concatenate the patch
140 	 * data, but each parse input must start with a valid file header (don't
141 	 * split a single file across parse calls).
142 	 *
143 	 * @param is
144 	 *            the stream to read the patch data from. The stream is read
145 	 *            until EOF is reached.
146 	 * @throws java.io.IOException
147 	 *             there was an error reading from the input stream.
148 	 */
149 	public void parse(InputStream is) throws IOException {
150 		final byte[] buf = readFully(is);
151 		parse(buf, 0, buf.length);
152 	}
153 
154 	private static byte[] readFully(InputStream is) throws IOException {
155 		try (TemporaryBuffer b = new TemporaryBuffer.Heap(Integer.MAX_VALUE)) {
156 			b.copy(is);
157 			return b.toByteArray();
158 		}
159 	}
160 
161 	/**
162 	 * Parse a patch stored in a byte[].
163 	 * <p>
164 	 * Multiple parse calls on the same instance will concatenate the patch
165 	 * data, but each parse input must start with a valid file header (don't
166 	 * split a single file across parse calls).
167 	 *
168 	 * @param buf
169 	 *            the buffer to parse.
170 	 * @param ptr
171 	 *            starting position to parse from.
172 	 * @param end
173 	 *            1 past the last position to end parsing. The total length to
174 	 *            be parsed is <code>end - ptr</code>.
175 	 */
176 	public void parse(byte[] buf, int ptr, int end) {
177 		while (ptr < end)
178 			ptr = parseFile(buf, ptr, end);
179 	}
180 
181 	private int parseFile(byte[] buf, int c, int end) {
182 		while (c < end) {
183 			if (isHunkHdr(buf, c, end) >= 1) {
184 				// If we find a disconnected hunk header we might
185 				// have missed a file header previously. The hunk
186 				// isn't valid without knowing where it comes from.
187 				//
188 				error(buf, c, JGitText.get().hunkDisconnectedFromFile);
189 				c = nextLF(buf, c);
190 				continue;
191 			}
192 
193 			// Valid git style patch?
194 			//
195 			if (match(buf, c, DIFF_GIT) >= 0)
196 				return parseDiffGit(buf, c, end);
197 			if (match(buf, c, DIFF_CC) >= 0)
198 				return parseDiffCombined(DIFF_CC, buf, c, end);
199 			if (match(buf, c, DIFF_COMBINED) >= 0)
200 				return parseDiffCombined(DIFF_COMBINED, buf, c, end);
201 
202 			// Junk between files? Leading junk? Traditional
203 			// (non-git generated) patch?
204 			//
205 			final int n = nextLF(buf, c);
206 			if (n >= end) {
207 				// Patches cannot be only one line long. This must be
208 				// trailing junk that we should ignore.
209 				//
210 				return end;
211 			}
212 
213 			if (n - c < 6) {
214 				// A valid header must be at least 6 bytes on the
215 				// first line, e.g. "--- a/b\n".
216 				//
217 				c = n;
218 				continue;
219 			}
220 
221 			if (match(buf, c, OLD_NAME) >= 0 && match(buf, n, NEW_NAME) >= 0) {
222 				// Probably a traditional patch. Ensure we have at least
223 				// a "@@ -0,0" smelling line next. We only check the "@@ -".
224 				//
225 				final int f = nextLF(buf, n);
226 				if (f >= end)
227 					return end;
228 				if (isHunkHdr(buf, f, end) == 1)
229 					return parseTraditionalPatch(buf, c, end);
230 			}
231 
232 			c = n;
233 		}
234 		return c;
235 	}
236 
237 	private int parseDiffGit(byte[] buf, int start, int end) {
238 		final FileHeader fh = new FileHeader(buf, start);
239 		int ptr = fh.parseGitFileName(start + DIFF_GIT.length, end);
240 		if (ptr < 0)
241 			return skipFile(buf, start);
242 
243 		ptr = fh.parseGitHeaders(ptr, end);
244 		ptr = parseHunks(fh, ptr, end);
245 		fh.endOffset = ptr;
246 		addFile(fh);
247 		return ptr;
248 	}
249 
250 	private int parseDiffCombined(final byte[] hdr, final byte[] buf,
251 			final int start, final int end) {
252 		final CombinedFileHeader fh = new CombinedFileHeader(buf, start);
253 		int ptr = fh.parseGitFileName(start + hdr.length, end);
254 		if (ptr < 0)
255 			return skipFile(buf, start);
256 
257 		ptr = fh.parseGitHeaders(ptr, end);
258 		ptr = parseHunks(fh, ptr, end);
259 		fh.endOffset = ptr;
260 		addFile(fh);
261 		return ptr;
262 	}
263 
264 	private int parseTraditionalPatch(final byte[] buf, final int start,
265 			final int end) {
266 		final FileHeader fh = new FileHeader(buf, start);
267 		int ptr = fh.parseTraditionalHeaders(start, end);
268 		ptr = parseHunks(fh, ptr, end);
269 		fh.endOffset = ptr;
270 		addFile(fh);
271 		return ptr;
272 	}
273 
274 	private static int skipFile(byte[] buf, int ptr) {
275 		ptr = nextLF(buf, ptr);
276 		if (match(buf, ptr, OLD_NAME) >= 0)
277 			ptr = nextLF(buf, ptr);
278 		return ptr;
279 	}
280 
281 	private int parseHunks(FileHeader fh, int c, int end) {
282 		final byte[] buf = fh.buf;
283 		while (c < end) {
284 			// If we see a file header at this point, we have all of the
285 			// hunks for our current file. We should stop and report back
286 			// with this position so it can be parsed again later.
287 			//
288 			if (match(buf, c, DIFF_GIT) >= 0)
289 				break;
290 			if (match(buf, c, DIFF_CC) >= 0)
291 				break;
292 			if (match(buf, c, DIFF_COMBINED) >= 0)
293 				break;
294 			if (match(buf, c, OLD_NAME) >= 0)
295 				break;
296 			if (match(buf, c, NEW_NAME) >= 0)
297 				break;
298 
299 			if (isHunkHdr(buf, c, end) == fh.getParentCount()) {
300 				final HunkHeader h = fh.newHunkHeader(c);
301 				h.parseHeader();
302 				c = h.parseBody(this, end);
303 				h.endOffset = c;
304 				fh.addHunk(h);
305 				if (c < end) {
306 					switch (buf[c]) {
307 					case '@':
308 					case 'd':
309 					case '\n':
310 						break;
311 					default:
312 						if (match(buf, c, SIG_FOOTER) < 0)
313 							warn(buf, c, JGitText.get().unexpectedHunkTrailer);
314 					}
315 				}
316 				continue;
317 			}
318 
319 			final int eol = nextLF(buf, c);
320 			if (fh.getHunks().isEmpty() && match(buf, c, GIT_BINARY) >= 0) {
321 				fh.patchType = FileHeader.PatchType.GIT_BINARY;
322 				return parseGitBinary(fh, eol, end);
323 			}
324 
325 			if (fh.getHunks().isEmpty() && BIN_TRAILER.length < eol - c
326 					&& match(buf, eol - BIN_TRAILER.length, BIN_TRAILER) >= 0
327 					&& matchAny(buf, c, BIN_HEADERS)) {
328 				// The patch is a binary file diff, with no deltas.
329 				//
330 				fh.patchType = FileHeader.PatchType.BINARY;
331 				return eol;
332 			}
333 
334 			// Skip this line and move to the next. Its probably garbage
335 			// after the last hunk of a file.
336 			//
337 			c = eol;
338 		}
339 
340 		if (fh.getHunks().isEmpty()
341 				&& fh.getPatchType() == FileHeader.PatchType.UNIFIED
342 				&& !fh.hasMetaDataChanges()) {
343 			// Hmm, an empty patch? If there is no metadata here we
344 			// really have a binary patch that we didn't notice above.
345 			//
346 			fh.patchType = FileHeader.PatchType.BINARY;
347 		}
348 
349 		return c;
350 	}
351 
352 	private int parseGitBinary(FileHeader fh, int c, int end) {
353 		final BinaryHunk postImage = new BinaryHunk(fh, c);
354 		final int nEnd = postImage.parseHunk(c, end);
355 		if (nEnd < 0) {
356 			// Not a binary hunk.
357 			//
358 			error(fh.buf, c, JGitText.get().missingForwardImageInGITBinaryPatch);
359 			return c;
360 		}
361 		c = nEnd;
362 		postImage.endOffset = c;
363 		fh.forwardBinaryHunk = postImage;
364 
365 		final BinaryHunk preImage = new BinaryHunk(fh, c);
366 		final int oEnd = preImage.parseHunk(c, end);
367 		if (oEnd >= 0) {
368 			c = oEnd;
369 			preImage.endOffset = c;
370 			fh.reverseBinaryHunk = preImage;
371 		}
372 
373 		return c;
374 	}
375 
376 	void warn(byte[] buf, int ptr, String msg) {
377 		addError(new FormatError(buf, ptr, FormatError.Severity.WARNING, msg));
378 	}
379 
380 	void error(byte[] buf, int ptr, String msg) {
381 		addError(new FormatError(buf, ptr, FormatError.Severity.ERROR, msg));
382 	}
383 
384 	private static boolean matchAny(final byte[] buf, final int c,
385 			final byte[][] srcs) {
386 		for (byte[] s : srcs) {
387 			if (match(buf, c, s) >= 0)
388 				return true;
389 		}
390 		return false;
391 	}
392 }