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.isHunkHdr;
48  import static org.eclipse.jgit.patch.FileHeader.NEW_NAME;
49  import static org.eclipse.jgit.patch.FileHeader.OLD_NAME;
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  /** A parsed collection of {@link FileHeader}s from a unified diff patch file */
62  public class Patch {
63  	static final byte[] DIFF_GIT = encodeASCII("diff --git "); //$NON-NLS-1$
64  
65  	private static final byte[] DIFF_CC = encodeASCII("diff --cc "); //$NON-NLS-1$
66  
67  	private static final byte[] DIFF_COMBINED = encodeASCII("diff --combined "); //$NON-NLS-1$
68  
69  	private static final byte[][] BIN_HEADERS = new byte[][] {
70  			encodeASCII("Binary files "), encodeASCII("Files "), }; //$NON-NLS-1$ //$NON-NLS-2$
71  
72  	private static final byte[] BIN_TRAILER = encodeASCII(" differ\n"); //$NON-NLS-1$
73  
74  	private static final byte[] GIT_BINARY = encodeASCII("GIT binary patch\n"); //$NON-NLS-1$
75  
76  	static final byte[] SIG_FOOTER = encodeASCII("-- \n"); //$NON-NLS-1$
77  
78  	/** The files, in the order they were parsed out of the input. */
79  	private final List<FileHeader> files;
80  
81  	/** Formatting errors, if any were identified. */
82  	private final List<FormatError> errors;
83  
84  	/** Create an empty patch. */
85  	public Patch() {
86  		files = new ArrayList<FileHeader>();
87  		errors = new ArrayList<FormatError>(0);
88  	}
89  
90  	/**
91  	 * Add a single file to this patch.
92  	 * <p>
93  	 * Typically files should be added by parsing the text through one of this
94  	 * class's parse methods.
95  	 *
96  	 * @param fh
97  	 *            the header of the file.
98  	 */
99  	public void addFile(final FileHeader fh) {
100 		files.add(fh);
101 	}
102 
103 	/** @return list of files described in the patch, in occurrence order. */
104 	public List<? extends FileHeader> getFiles() {
105 		return files;
106 	}
107 
108 	/**
109 	 * Add a formatting error to this patch script.
110 	 *
111 	 * @param err
112 	 *            the error description.
113 	 */
114 	public void addError(final FormatError err) {
115 		errors.add(err);
116 	}
117 
118 	/** @return collection of formatting errors, if any. */
119 	public List<FormatError> getErrors() {
120 		return errors;
121 	}
122 
123 	/**
124 	 * Parse a patch received from an InputStream.
125 	 * <p>
126 	 * Multiple parse calls on the same instance will concatenate the patch
127 	 * data, but each parse input must start with a valid file header (don't
128 	 * split a single file across parse calls).
129 	 *
130 	 * @param is
131 	 *            the stream to read the patch data from. The stream is read
132 	 *            until EOF is reached.
133 	 * @throws IOException
134 	 *             there was an error reading from the input stream.
135 	 */
136 	public void parse(final InputStream is) throws IOException {
137 		final byte[] buf = readFully(is);
138 		parse(buf, 0, buf.length);
139 	}
140 
141 	private static byte[] readFully(final InputStream is) throws IOException {
142 		TemporaryBuffer b = new TemporaryBuffer.Heap(Integer.MAX_VALUE);
143 		b.copy(is);
144 		b.close();
145 		return b.toByteArray();
146 	}
147 
148 	/**
149 	 * Parse a patch stored in a byte[].
150 	 * <p>
151 	 * Multiple parse calls on the same instance will concatenate the patch
152 	 * data, but each parse input must start with a valid file header (don't
153 	 * split a single file across parse calls).
154 	 *
155 	 * @param buf
156 	 *            the buffer to parse.
157 	 * @param ptr
158 	 *            starting position to parse from.
159 	 * @param end
160 	 *            1 past the last position to end parsing. The total length to
161 	 *            be parsed is <code>end - ptr</code>.
162 	 */
163 	public void parse(final byte[] buf, int ptr, final int end) {
164 		while (ptr < end)
165 			ptr = parseFile(buf, ptr, end);
166 	}
167 
168 	private int parseFile(final byte[] buf, int c, final int end) {
169 		while (c < end) {
170 			if (isHunkHdr(buf, c, end) >= 1) {
171 				// If we find a disconnected hunk header we might
172 				// have missed a file header previously. The hunk
173 				// isn't valid without knowing where it comes from.
174 				//
175 				error(buf, c, JGitText.get().hunkDisconnectedFromFile);
176 				c = nextLF(buf, c);
177 				continue;
178 			}
179 
180 			// Valid git style patch?
181 			//
182 			if (match(buf, c, DIFF_GIT) >= 0)
183 				return parseDiffGit(buf, c, end);
184 			if (match(buf, c, DIFF_CC) >= 0)
185 				return parseDiffCombined(DIFF_CC, buf, c, end);
186 			if (match(buf, c, DIFF_COMBINED) >= 0)
187 				return parseDiffCombined(DIFF_COMBINED, buf, c, end);
188 
189 			// Junk between files? Leading junk? Traditional
190 			// (non-git generated) patch?
191 			//
192 			final int n = nextLF(buf, c);
193 			if (n >= end) {
194 				// Patches cannot be only one line long. This must be
195 				// trailing junk that we should ignore.
196 				//
197 				return end;
198 			}
199 
200 			if (n - c < 6) {
201 				// A valid header must be at least 6 bytes on the
202 				// first line, e.g. "--- a/b\n".
203 				//
204 				c = n;
205 				continue;
206 			}
207 
208 			if (match(buf, c, OLD_NAME) >= 0 && match(buf, n, NEW_NAME) >= 0) {
209 				// Probably a traditional patch. Ensure we have at least
210 				// a "@@ -0,0" smelling line next. We only check the "@@ -".
211 				//
212 				final int f = nextLF(buf, n);
213 				if (f >= end)
214 					return end;
215 				if (isHunkHdr(buf, f, end) == 1)
216 					return parseTraditionalPatch(buf, c, end);
217 			}
218 
219 			c = n;
220 		}
221 		return c;
222 	}
223 
224 	private int parseDiffGit(final byte[] buf, final int start, final int end) {
225 		final FileHeader fh = new FileHeader(buf, start);
226 		int ptr = fh.parseGitFileName(start + DIFF_GIT.length, end);
227 		if (ptr < 0)
228 			return skipFile(buf, start);
229 
230 		ptr = fh.parseGitHeaders(ptr, end);
231 		ptr = parseHunks(fh, ptr, end);
232 		fh.endOffset = ptr;
233 		addFile(fh);
234 		return ptr;
235 	}
236 
237 	private int parseDiffCombined(final byte[] hdr, final byte[] buf,
238 			final int start, final int end) {
239 		final CombinedFileHeader fh = new CombinedFileHeader(buf, start);
240 		int ptr = fh.parseGitFileName(start + hdr.length, end);
241 		if (ptr < 0)
242 			return skipFile(buf, start);
243 
244 		ptr = fh.parseGitHeaders(ptr, end);
245 		ptr = parseHunks(fh, ptr, end);
246 		fh.endOffset = ptr;
247 		addFile(fh);
248 		return ptr;
249 	}
250 
251 	private int parseTraditionalPatch(final byte[] buf, final int start,
252 			final int end) {
253 		final FileHeader fh = new FileHeader(buf, start);
254 		int ptr = fh.parseTraditionalHeaders(start, end);
255 		ptr = parseHunks(fh, ptr, end);
256 		fh.endOffset = ptr;
257 		addFile(fh);
258 		return ptr;
259 	}
260 
261 	private static int skipFile(final byte[] buf, int ptr) {
262 		ptr = nextLF(buf, ptr);
263 		if (match(buf, ptr, OLD_NAME) >= 0)
264 			ptr = nextLF(buf, ptr);
265 		return ptr;
266 	}
267 
268 	private int parseHunks(final FileHeader fh, int c, final int end) {
269 		final byte[] buf = fh.buf;
270 		while (c < end) {
271 			// If we see a file header at this point, we have all of the
272 			// hunks for our current file. We should stop and report back
273 			// with this position so it can be parsed again later.
274 			//
275 			if (match(buf, c, DIFF_GIT) >= 0)
276 				break;
277 			if (match(buf, c, DIFF_CC) >= 0)
278 				break;
279 			if (match(buf, c, DIFF_COMBINED) >= 0)
280 				break;
281 			if (match(buf, c, OLD_NAME) >= 0)
282 				break;
283 			if (match(buf, c, NEW_NAME) >= 0)
284 				break;
285 
286 			if (isHunkHdr(buf, c, end) == fh.getParentCount()) {
287 				final HunkHeader h = fh.newHunkHeader(c);
288 				h.parseHeader();
289 				c = h.parseBody(this, end);
290 				h.endOffset = c;
291 				fh.addHunk(h);
292 				if (c < end) {
293 					switch (buf[c]) {
294 					case '@':
295 					case 'd':
296 					case '\n':
297 						break;
298 					default:
299 						if (match(buf, c, SIG_FOOTER) < 0)
300 							warn(buf, c, JGitText.get().unexpectedHunkTrailer);
301 					}
302 				}
303 				continue;
304 			}
305 
306 			final int eol = nextLF(buf, c);
307 			if (fh.getHunks().isEmpty() && match(buf, c, GIT_BINARY) >= 0) {
308 				fh.patchType = FileHeader.PatchType.GIT_BINARY;
309 				return parseGitBinary(fh, eol, end);
310 			}
311 
312 			if (fh.getHunks().isEmpty() && BIN_TRAILER.length < eol - c
313 					&& match(buf, eol - BIN_TRAILER.length, BIN_TRAILER) >= 0
314 					&& matchAny(buf, c, BIN_HEADERS)) {
315 				// The patch is a binary file diff, with no deltas.
316 				//
317 				fh.patchType = FileHeader.PatchType.BINARY;
318 				return eol;
319 			}
320 
321 			// Skip this line and move to the next. Its probably garbage
322 			// after the last hunk of a file.
323 			//
324 			c = eol;
325 		}
326 
327 		if (fh.getHunks().isEmpty()
328 				&& fh.getPatchType() == FileHeader.PatchType.UNIFIED
329 				&& !fh.hasMetaDataChanges()) {
330 			// Hmm, an empty patch? If there is no metadata here we
331 			// really have a binary patch that we didn't notice above.
332 			//
333 			fh.patchType = FileHeader.PatchType.BINARY;
334 		}
335 
336 		return c;
337 	}
338 
339 	private int parseGitBinary(final FileHeader fh, int c, final int end) {
340 		final BinaryHunk postImage = new BinaryHunk(fh, c);
341 		final int nEnd = postImage.parseHunk(c, end);
342 		if (nEnd < 0) {
343 			// Not a binary hunk.
344 			//
345 			error(fh.buf, c, JGitText.get().missingForwardImageInGITBinaryPatch);
346 			return c;
347 		}
348 		c = nEnd;
349 		postImage.endOffset = c;
350 		fh.forwardBinaryHunk = postImage;
351 
352 		final BinaryHunk preImage = new BinaryHunk(fh, c);
353 		final int oEnd = preImage.parseHunk(c, end);
354 		if (oEnd >= 0) {
355 			c = oEnd;
356 			preImage.endOffset = c;
357 			fh.reverseBinaryHunk = preImage;
358 		}
359 
360 		return c;
361 	}
362 
363 	void warn(final byte[] buf, final int ptr, final String msg) {
364 		addError(new FormatError(buf, ptr, FormatError.Severity.WARNING, msg));
365 	}
366 
367 	void error(final byte[] buf, final int ptr, final String msg) {
368 		addError(new FormatError(buf, ptr, FormatError.Severity.ERROR, msg));
369 	}
370 
371 	private static boolean matchAny(final byte[] buf, final int c,
372 			final byte[][] srcs) {
373 		for (final byte[] s : srcs) {
374 			if (match(buf, c, s) >= 0)
375 				return true;
376 		}
377 		return false;
378 	}
379 }