View Javadoc
1   /*
2    * Copyright (C) 2008-2010, Google Inc.
3    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
4    * and other copyright owners as documented in the project's IP log.
5    *
6    * This program and the accompanying materials are made available
7    * under the terms of the Eclipse Distribution License v1.0 which
8    * accompanies this distribution, is reproduced below, and is
9    * available at http://www.eclipse.org/org/documents/edl-v10.php
10   *
11   * All rights reserved.
12   *
13   * Redistribution and use in source and binary forms, with or
14   * without modification, are permitted provided that the following
15   * conditions are met:
16   *
17   * - Redistributions of source code must retain the above copyright
18   *   notice, this list of conditions and the following disclaimer.
19   *
20   * - Redistributions in binary form must reproduce the above
21   *   copyright notice, this list of conditions and the following
22   *   disclaimer in the documentation and/or other materials provided
23   *   with the distribution.
24   *
25   * - Neither the name of the Eclipse Foundation, Inc. nor the
26   *   names of its contributors may be used to endorse or promote
27   *   products derived from this software without specific prior
28   *   written permission.
29   *
30   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
31   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
32   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
33   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
34   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
35   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
36   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
37   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
38   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
39   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
40   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
41   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
42   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43   */
44  
45  package org.eclipse.jgit.lib;
46  
47  import static org.eclipse.jgit.util.RawParseUtils.match;
48  import static org.eclipse.jgit.util.RawParseUtils.nextLF;
49  import static org.eclipse.jgit.util.RawParseUtils.parseBase10;
50  
51  import java.lang.reflect.InvocationTargetException;
52  import java.lang.reflect.Method;
53  import java.text.MessageFormat;
54  import java.util.HashSet;
55  import java.util.Locale;
56  import java.util.Set;
57  
58  import org.eclipse.jgit.errors.CorruptObjectException;
59  import org.eclipse.jgit.internal.JGitText;
60  import org.eclipse.jgit.util.MutableInteger;
61  import org.eclipse.jgit.util.RawParseUtils;
62  
63  /**
64   * Verifies that an object is formatted correctly.
65   * <p>
66   * Verifications made by this class only check that the fields of an object are
67   * formatted correctly. The ObjectId checksum of the object is not verified, and
68   * connectivity links between objects are also not verified. Its assumed that
69   * the caller can provide both of these validations on its own.
70   * <p>
71   * Instances of this class are not thread safe, but they may be reused to
72   * perform multiple object validations.
73   */
74  public class ObjectChecker {
75  	/** Header "tree " */
76  	public static final byte[] tree = Constants.encodeASCII("tree "); //$NON-NLS-1$
77  
78  	/** Header "parent " */
79  	public static final byte[] parent = Constants.encodeASCII("parent "); //$NON-NLS-1$
80  
81  	/** Header "author " */
82  	public static final byte[] author = Constants.encodeASCII("author "); //$NON-NLS-1$
83  
84  	/** Header "committer " */
85  	public static final byte[] committer = Constants.encodeASCII("committer "); //$NON-NLS-1$
86  
87  	/** Header "encoding " */
88  	public static final byte[] encoding = Constants.encodeASCII("encoding "); //$NON-NLS-1$
89  
90  	/** Header "object " */
91  	public static final byte[] object = Constants.encodeASCII("object "); //$NON-NLS-1$
92  
93  	/** Header "type " */
94  	public static final byte[] type = Constants.encodeASCII("type "); //$NON-NLS-1$
95  
96  	/** Header "tag " */
97  	public static final byte[] tag = Constants.encodeASCII("tag "); //$NON-NLS-1$
98  
99  	/** Header "tagger " */
100 	public static final byte[] tagger = Constants.encodeASCII("tagger "); //$NON-NLS-1$
101 
102 	private final MutableObjectId tempId = new MutableObjectId();
103 
104 	private final MutableInteger ptrout = new MutableInteger();
105 
106 	private boolean allowZeroMode;
107 
108 	private boolean allowInvalidPersonIdent;
109 	private boolean windows;
110 	private boolean macosx;
111 
112 	/**
113 	 * Enable accepting leading zero mode in tree entries.
114 	 * <p>
115 	 * Some broken Git libraries generated leading zeros in the mode part of
116 	 * tree entries. This is technically incorrect but gracefully allowed by
117 	 * git-core. JGit rejects such trees by default, but may need to accept
118 	 * them on broken histories.
119 	 *
120 	 * @param allow allow leading zero mode.
121 	 * @return {@code this}.
122 	 * @since 3.4
123 	 */
124 	public ObjectChecker setAllowLeadingZeroFileMode(boolean allow) {
125 		allowZeroMode = allow;
126 		return this;
127 	}
128 
129 	/**
130 	 * Enable accepting invalid author, committer and tagger identities.
131 	 * <p>
132 	 * Some broken Git versions/libraries allowed users to create commits and
133 	 * tags with invalid formatting between the name, email and timestamp.
134 	 *
135 	 * @param allow
136 	 *            if true accept invalid person identity strings.
137 	 * @return {@code this}.
138 	 * @since 4.0
139 	 */
140 	public ObjectChecker setAllowInvalidPersonIdent(boolean allow) {
141 		allowInvalidPersonIdent = allow;
142 		return this;
143 	}
144 
145 	/**
146 	 * Restrict trees to only names legal on Windows platforms.
147 	 * <p>
148 	 * Also rejects any mixed case forms of reserved names ({@code .git}).
149 	 *
150 	 * @param win true if Windows name checking should be performed.
151 	 * @return {@code this}.
152 	 * @since 3.4
153 	 */
154 	public ObjectChecker setSafeForWindows(boolean win) {
155 		windows = win;
156 		return this;
157 	}
158 
159 	/**
160 	 * Restrict trees to only names legal on Mac OS X platforms.
161 	 * <p>
162 	 * Rejects any mixed case forms of reserved names ({@code .git})
163 	 * for users working on HFS+ in case-insensitive (default) mode.
164 	 *
165 	 * @param mac true if Mac OS X name checking should be performed.
166 	 * @return {@code this}.
167 	 * @since 3.4
168 	 */
169 	public ObjectChecker setSafeForMacOS(boolean mac) {
170 		macosx = mac;
171 		return this;
172 	}
173 
174 	/**
175 	 * Check an object for parsing errors.
176 	 *
177 	 * @param objType
178 	 *            type of the object. Must be a valid object type code in
179 	 *            {@link Constants}.
180 	 * @param raw
181 	 *            the raw data which comprises the object. This should be in the
182 	 *            canonical format (that is the format used to generate the
183 	 *            ObjectId of the object). The array is never modified.
184 	 * @throws CorruptObjectException
185 	 *             if an error is identified.
186 	 */
187 	public void check(final int objType, final byte[] raw)
188 			throws CorruptObjectException {
189 		switch (objType) {
190 		case Constants.OBJ_COMMIT:
191 			checkCommit(raw);
192 			break;
193 		case Constants.OBJ_TAG:
194 			checkTag(raw);
195 			break;
196 		case Constants.OBJ_TREE:
197 			checkTree(raw);
198 			break;
199 		case Constants.OBJ_BLOB:
200 			checkBlob(raw);
201 			break;
202 		default:
203 			throw new CorruptObjectException(MessageFormat.format(
204 					JGitText.get().corruptObjectInvalidType2,
205 					Integer.valueOf(objType)));
206 		}
207 	}
208 
209 	private int id(final byte[] raw, final int ptr) {
210 		try {
211 			tempId.fromString(raw, ptr);
212 			return ptr + Constants.OBJECT_ID_STRING_LENGTH;
213 		} catch (IllegalArgumentException e) {
214 			return -1;
215 		}
216 	}
217 
218 	private int personIdent(final byte[] raw, int ptr) {
219 		if (allowInvalidPersonIdent)
220 			return nextLF(raw, ptr) - 1;
221 
222 		final int emailB = nextLF(raw, ptr, '<');
223 		if (emailB == ptr || raw[emailB - 1] != '<')
224 			return -1;
225 
226 		final int emailE = nextLF(raw, emailB, '>');
227 		if (emailE == emailB || raw[emailE - 1] != '>')
228 			return -1;
229 		if (emailE == raw.length || raw[emailE] != ' ')
230 			return -1;
231 
232 		parseBase10(raw, emailE + 1, ptrout); // when
233 		ptr = ptrout.value;
234 		if (emailE + 1 == ptr)
235 			return -1;
236 		if (ptr == raw.length || raw[ptr] != ' ')
237 			return -1;
238 
239 		parseBase10(raw, ptr + 1, ptrout); // tz offset
240 		if (ptr + 1 == ptrout.value)
241 			return -1;
242 		return ptrout.value;
243 	}
244 
245 	/**
246 	 * Check a commit for errors.
247 	 *
248 	 * @param raw
249 	 *            the commit data. The array is never modified.
250 	 * @throws CorruptObjectException
251 	 *             if any error was detected.
252 	 */
253 	public void checkCommit(final byte[] raw) throws CorruptObjectException {
254 		int ptr = 0;
255 
256 		if ((ptr = match(raw, ptr, tree)) < 0)
257 			throw new CorruptObjectException(
258 					JGitText.get().corruptObjectNotreeHeader);
259 		if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n')
260 			throw new CorruptObjectException(
261 					JGitText.get().corruptObjectInvalidTree);
262 
263 		while (match(raw, ptr, parent) >= 0) {
264 			ptr += parent.length;
265 			if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n')
266 				throw new CorruptObjectException(
267 						JGitText.get().corruptObjectInvalidParent);
268 		}
269 
270 		if ((ptr = match(raw, ptr, author)) < 0)
271 			throw new CorruptObjectException(
272 					JGitText.get().corruptObjectNoAuthor);
273 		if ((ptr = personIdent(raw, ptr)) < 0 || raw[ptr++] != '\n')
274 			throw new CorruptObjectException(
275 					JGitText.get().corruptObjectInvalidAuthor);
276 
277 		if ((ptr = match(raw, ptr, committer)) < 0)
278 			throw new CorruptObjectException(
279 					JGitText.get().corruptObjectNoCommitter);
280 		if ((ptr = personIdent(raw, ptr)) < 0 || raw[ptr++] != '\n')
281 			throw new CorruptObjectException(
282 					JGitText.get().corruptObjectInvalidCommitter);
283 	}
284 
285 	/**
286 	 * Check an annotated tag for errors.
287 	 *
288 	 * @param raw
289 	 *            the tag data. The array is never modified.
290 	 * @throws CorruptObjectException
291 	 *             if any error was detected.
292 	 */
293 	public void checkTag(final byte[] raw) throws CorruptObjectException {
294 		int ptr = 0;
295 
296 		if ((ptr = match(raw, ptr, object)) < 0)
297 			throw new CorruptObjectException(
298 					JGitText.get().corruptObjectNoObjectHeader);
299 		if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n')
300 			throw new CorruptObjectException(
301 					JGitText.get().corruptObjectInvalidObject);
302 
303 		if ((ptr = match(raw, ptr, type)) < 0)
304 			throw new CorruptObjectException(
305 					JGitText.get().corruptObjectNoTypeHeader);
306 		ptr = nextLF(raw, ptr);
307 
308 		if ((ptr = match(raw, ptr, tag)) < 0)
309 			throw new CorruptObjectException(
310 					JGitText.get().corruptObjectNoTagHeader);
311 		ptr = nextLF(raw, ptr);
312 
313 		if ((ptr = match(raw, ptr, tagger)) > 0) {
314 			if ((ptr = personIdent(raw, ptr)) < 0 || raw[ptr++] != '\n')
315 				throw new CorruptObjectException(
316 						JGitText.get().corruptObjectInvalidTagger);
317 		}
318 	}
319 
320 	private static int lastPathChar(final int mode) {
321 		return FileMode.TREE.equals(mode) ? '/' : '\0';
322 	}
323 
324 	private static int pathCompare(final byte[] raw, int aPos, final int aEnd,
325 			final int aMode, int bPos, final int bEnd, final int bMode) {
326 		while (aPos < aEnd && bPos < bEnd) {
327 			final int cmp = (raw[aPos++] & 0xff) - (raw[bPos++] & 0xff);
328 			if (cmp != 0)
329 				return cmp;
330 		}
331 
332 		if (aPos < aEnd)
333 			return (raw[aPos] & 0xff) - lastPathChar(bMode);
334 		if (bPos < bEnd)
335 			return lastPathChar(aMode) - (raw[bPos] & 0xff);
336 		return 0;
337 	}
338 
339 	private static boolean duplicateName(final byte[] raw,
340 			final int thisNamePos, final int thisNameEnd) {
341 		final int sz = raw.length;
342 		int nextPtr = thisNameEnd + 1 + Constants.OBJECT_ID_LENGTH;
343 		for (;;) {
344 			int nextMode = 0;
345 			for (;;) {
346 				if (nextPtr >= sz)
347 					return false;
348 				final byte c = raw[nextPtr++];
349 				if (' ' == c)
350 					break;
351 				nextMode <<= 3;
352 				nextMode += c - '0';
353 			}
354 
355 			final int nextNamePos = nextPtr;
356 			for (;;) {
357 				if (nextPtr == sz)
358 					return false;
359 				final byte c = raw[nextPtr++];
360 				if (c == 0)
361 					break;
362 			}
363 			if (nextNamePos + 1 == nextPtr)
364 				return false;
365 
366 			final int cmp = pathCompare(raw, thisNamePos, thisNameEnd,
367 					FileMode.TREE.getBits(), nextNamePos, nextPtr - 1, nextMode);
368 			if (cmp < 0)
369 				return false;
370 			else if (cmp == 0)
371 				return true;
372 
373 			nextPtr += Constants.OBJECT_ID_LENGTH;
374 		}
375 	}
376 
377 	/**
378 	 * Check a canonical formatted tree for errors.
379 	 *
380 	 * @param raw
381 	 *            the raw tree data. The array is never modified.
382 	 * @throws CorruptObjectException
383 	 *             if any error was detected.
384 	 */
385 	public void checkTree(final byte[] raw) throws CorruptObjectException {
386 		final int sz = raw.length;
387 		int ptr = 0;
388 		int lastNameB = 0, lastNameE = 0, lastMode = 0;
389 		Set<String> normalized = windows || macosx
390 				? new HashSet<String>()
391 				: null;
392 
393 		while (ptr < sz) {
394 			int thisMode = 0;
395 			for (;;) {
396 				if (ptr == sz)
397 					throw new CorruptObjectException(
398 							JGitText.get().corruptObjectTruncatedInMode);
399 				final byte c = raw[ptr++];
400 				if (' ' == c)
401 					break;
402 				if (c < '0' || c > '7')
403 					throw new CorruptObjectException(
404 							JGitText.get().corruptObjectInvalidModeChar);
405 				if (thisMode == 0 && c == '0' && !allowZeroMode)
406 					throw new CorruptObjectException(
407 							JGitText.get().corruptObjectInvalidModeStartsZero);
408 				thisMode <<= 3;
409 				thisMode += c - '0';
410 			}
411 
412 			if (FileMode.fromBits(thisMode).getObjectType() == Constants.OBJ_BAD)
413 				throw new CorruptObjectException(MessageFormat.format(
414 						JGitText.get().corruptObjectInvalidMode2,
415 						Integer.valueOf(thisMode)));
416 
417 			final int thisNameB = ptr;
418 			ptr = scanPathSegment(raw, ptr, sz);
419 			if (ptr == sz || raw[ptr] != 0)
420 				throw new CorruptObjectException(
421 						JGitText.get().corruptObjectTruncatedInName);
422 			checkPathSegment2(raw, thisNameB, ptr);
423 			if (normalized != null) {
424 				if (!normalized.add(normalize(raw, thisNameB, ptr)))
425 					throw new CorruptObjectException(
426 							JGitText.get().corruptObjectDuplicateEntryNames);
427 			} else if (duplicateName(raw, thisNameB, ptr))
428 				throw new CorruptObjectException(
429 						JGitText.get().corruptObjectDuplicateEntryNames);
430 
431 			if (lastNameB != 0) {
432 				final int cmp = pathCompare(raw, lastNameB, lastNameE,
433 						lastMode, thisNameB, ptr, thisMode);
434 				if (cmp > 0)
435 					throw new CorruptObjectException(
436 							JGitText.get().corruptObjectIncorrectSorting);
437 			}
438 
439 			lastNameB = thisNameB;
440 			lastNameE = ptr;
441 			lastMode = thisMode;
442 
443 			ptr += 1 + Constants.OBJECT_ID_LENGTH;
444 			if (ptr > sz)
445 				throw new CorruptObjectException(
446 						JGitText.get().corruptObjectTruncatedInObjectId);
447 		}
448 	}
449 
450 	private int scanPathSegment(byte[] raw, int ptr, int end)
451 			throws CorruptObjectException {
452 		for (; ptr < end; ptr++) {
453 			byte c = raw[ptr];
454 			if (c == 0)
455 				return ptr;
456 			if (c == '/')
457 				throw new CorruptObjectException(
458 						JGitText.get().corruptObjectNameContainsSlash);
459 			if (windows && isInvalidOnWindows(c)) {
460 				if (c > 31)
461 					throw new CorruptObjectException(String.format(
462 							JGitText.get().corruptObjectNameContainsChar,
463 							Byte.valueOf(c)));
464 				throw new CorruptObjectException(String.format(
465 						JGitText.get().corruptObjectNameContainsByte,
466 						Integer.valueOf(c & 0xff)));
467 			}
468 		}
469 		return ptr;
470 	}
471 
472 	/**
473 	 * Check tree path entry for validity.
474 	 * <p>
475 	 * Unlike {@link #checkPathSegment(byte[], int, int)}, this version
476 	 * scans a multi-directory path string such as {@code "src/main.c"}.
477 	 *
478 	 * @param path path string to scan.
479 	 * @throws CorruptObjectException path is invalid.
480 	 * @since 3.6
481 	 */
482 	public void checkPath(String path) throws CorruptObjectException {
483 		byte[] buf = Constants.encode(path);
484 		checkPath(buf, 0, buf.length);
485 	}
486 
487 	/**
488 	 * Check tree path entry for validity.
489 	 * <p>
490 	 * Unlike {@link #checkPathSegment(byte[], int, int)}, this version
491 	 * scans a multi-directory path string such as {@code "src/main.c"}.
492 	 *
493 	 * @param raw buffer to scan.
494 	 * @param ptr offset to first byte of the name.
495 	 * @param end offset to one past last byte of name.
496 	 * @throws CorruptObjectException path is invalid.
497 	 * @since 3.6
498 	 */
499 	public void checkPath(byte[] raw, int ptr, int end)
500 			throws CorruptObjectException {
501 		int start = ptr;
502 		for (; ptr < end; ptr++) {
503 			if (raw[ptr] == '/') {
504 				checkPathSegment(raw, start, ptr);
505 				start = ptr + 1;
506 			}
507 		}
508 		checkPathSegment(raw, start, end);
509 	}
510 
511 	/**
512 	 * Check tree path entry for validity.
513 	 *
514 	 * @param raw buffer to scan.
515 	 * @param ptr offset to first byte of the name.
516 	 * @param end offset to one past last byte of name.
517 	 * @throws CorruptObjectException name is invalid.
518 	 * @since 3.4
519 	 */
520 	public void checkPathSegment(byte[] raw, int ptr, int end)
521 			throws CorruptObjectException {
522 		int e = scanPathSegment(raw, ptr, end);
523 		if (e < end && raw[e] == 0)
524 			throw new CorruptObjectException(
525 					JGitText.get().corruptObjectNameContainsNullByte);
526 		checkPathSegment2(raw, ptr, end);
527 	}
528 
529 	private void checkPathSegment2(byte[] raw, int ptr, int end)
530 			throws CorruptObjectException {
531 		if (ptr == end)
532 			throw new CorruptObjectException(
533 					JGitText.get().corruptObjectNameZeroLength);
534 		if (raw[ptr] == '.') {
535 			switch (end - ptr) {
536 			case 1:
537 				throw new CorruptObjectException(
538 						JGitText.get().corruptObjectNameDot);
539 			case 2:
540 				if (raw[ptr + 1] == '.')
541 					throw new CorruptObjectException(
542 							JGitText.get().corruptObjectNameDotDot);
543 				break;
544 			case 4:
545 				if (isGit(raw, ptr + 1))
546 					throw new CorruptObjectException(String.format(
547 							JGitText.get().corruptObjectInvalidName,
548 							RawParseUtils.decode(raw, ptr, end)));
549 				break;
550 			default:
551 				if (end - ptr > 4 && isNormalizedGit(raw, ptr + 1, end))
552 					throw new CorruptObjectException(String.format(
553 							JGitText.get().corruptObjectInvalidName,
554 							RawParseUtils.decode(raw, ptr, end)));
555 			}
556 		} else if (isGitTilde1(raw, ptr, end)) {
557 			throw new CorruptObjectException(String.format(
558 					JGitText.get().corruptObjectInvalidName,
559 					RawParseUtils.decode(raw, ptr, end)));
560 		}
561 
562 		if (macosx && isMacHFSGit(raw, ptr, end))
563 			throw new CorruptObjectException(String.format(
564 					JGitText.get().corruptObjectInvalidNameIgnorableUnicode,
565 					RawParseUtils.decode(raw, ptr, end)));
566 
567 		if (windows) {
568 			// Windows ignores space and dot at end of file name.
569 			if (raw[end - 1] == ' ' || raw[end - 1] == '.')
570 				throw new CorruptObjectException(String.format(
571 						JGitText.get().corruptObjectInvalidNameEnd,
572 						Character.valueOf(((char) raw[end - 1]))));
573 			if (end - ptr >= 3)
574 				checkNotWindowsDevice(raw, ptr, end);
575 		}
576 	}
577 
578 	// Mac's HFS+ folds permutations of ".git" and Unicode ignorable characters
579 	// to ".git" therefore we should prevent such names
580 	private static boolean isMacHFSGit(byte[] raw, int ptr, int end)
581 			throws CorruptObjectException {
582 		boolean ignorable = false;
583 		byte[] git = new byte[] { '.', 'g', 'i', 't' };
584 		int g = 0;
585 		while (ptr < end) {
586 			switch (raw[ptr]) {
587 			case (byte) 0xe2: // http://www.utf8-chartable.de/unicode-utf8-table.pl?start=8192
588 				checkTruncatedIgnorableUTF8(raw, ptr, end);
589 				switch (raw[ptr + 1]) {
590 				case (byte) 0x80:
591 					switch (raw[ptr + 2]) {
592 					case (byte) 0x8c:	// U+200C 0xe2808c ZERO WIDTH NON-JOINER
593 					case (byte) 0x8d:	// U+200D 0xe2808d ZERO WIDTH JOINER
594 					case (byte) 0x8e:	// U+200E 0xe2808e LEFT-TO-RIGHT MARK
595 					case (byte) 0x8f:	// U+200F 0xe2808f RIGHT-TO-LEFT MARK
596 					case (byte) 0xaa:	// U+202A 0xe280aa LEFT-TO-RIGHT EMBEDDING
597 					case (byte) 0xab:	// U+202B 0xe280ab RIGHT-TO-LEFT EMBEDDING
598 					case (byte) 0xac:	// U+202C 0xe280ac POP DIRECTIONAL FORMATTING
599 					case (byte) 0xad:	// U+202D 0xe280ad LEFT-TO-RIGHT OVERRIDE
600 					case (byte) 0xae:	// U+202E 0xe280ae RIGHT-TO-LEFT OVERRIDE
601 						ignorable = true;
602 						ptr += 3;
603 						continue;
604 					default:
605 						return false;
606 					}
607 				case (byte) 0x81:
608 					switch (raw[ptr + 2]) {
609 					case (byte) 0xaa:	// U+206A 0xe281aa INHIBIT SYMMETRIC SWAPPING
610 					case (byte) 0xab:	// U+206B 0xe281ab ACTIVATE SYMMETRIC SWAPPING
611 					case (byte) 0xac:	// U+206C 0xe281ac INHIBIT ARABIC FORM SHAPING
612 					case (byte) 0xad:	// U+206D 0xe281ad ACTIVATE ARABIC FORM SHAPING
613 					case (byte) 0xae:	// U+206E 0xe281ae NATIONAL DIGIT SHAPES
614 					case (byte) 0xaf:	// U+206F 0xe281af NOMINAL DIGIT SHAPES
615 						ignorable = true;
616 						ptr += 3;
617 						continue;
618 					default:
619 						return false;
620 					}
621 				default:
622 					return false;
623 				}
624 			case (byte) 0xef: // http://www.utf8-chartable.de/unicode-utf8-table.pl?start=65024
625 				checkTruncatedIgnorableUTF8(raw, ptr, end);
626 				// U+FEFF 0xefbbbf ZERO WIDTH NO-BREAK SPACE
627 				if ((raw[ptr + 1] == (byte) 0xbb)
628 						&& (raw[ptr + 2] == (byte) 0xbf)) {
629 					ignorable = true;
630 					ptr += 3;
631 					continue;
632 				}
633 				return false;
634 			default:
635 				if (g == 4)
636 					return false;
637 				if (raw[ptr++] != git[g++])
638 					return false;
639 			}
640 		}
641 		if (g == 4 && ignorable)
642 			return true;
643 		return false;
644 	}
645 
646 	private static void checkTruncatedIgnorableUTF8(byte[] raw, int ptr, int end)
647 			throws CorruptObjectException {
648 		if ((ptr + 2) >= end)
649 			throw new CorruptObjectException(MessageFormat.format(
650 					JGitText.get().corruptObjectInvalidNameInvalidUtf8,
651 					toHexString(raw, ptr, end)));
652 	}
653 
654 	private static String toHexString(byte[] raw, int ptr, int end) {
655 		StringBuilder b = new StringBuilder("0x"); //$NON-NLS-1$
656 		for (int i = ptr; i < end; i++)
657 			b.append(String.format("%02x", Byte.valueOf(raw[i]))); //$NON-NLS-1$
658 		return b.toString();
659 	}
660 
661 	private static void checkNotWindowsDevice(byte[] raw, int ptr, int end)
662 			throws CorruptObjectException {
663 		switch (toLower(raw[ptr])) {
664 		case 'a': // AUX
665 			if (end - ptr >= 3
666 					&& toLower(raw[ptr + 1]) == 'u'
667 					&& toLower(raw[ptr + 2]) == 'x'
668 					&& (end - ptr == 3 || raw[ptr + 3] == '.'))
669 				throw new CorruptObjectException(
670 						JGitText.get().corruptObjectInvalidNameAux);
671 			break;
672 
673 		case 'c': // CON, COM[1-9]
674 			if (end - ptr >= 3
675 					&& toLower(raw[ptr + 2]) == 'n'
676 					&& toLower(raw[ptr + 1]) == 'o'
677 					&& (end - ptr == 3 || raw[ptr + 3] == '.'))
678 				throw new CorruptObjectException(
679 						JGitText.get().corruptObjectInvalidNameCon);
680 			if (end - ptr >= 4
681 					&& toLower(raw[ptr + 2]) == 'm'
682 					&& toLower(raw[ptr + 1]) == 'o'
683 					&& isPositiveDigit(raw[ptr + 3])
684 					&& (end - ptr == 4 || raw[ptr + 4] == '.'))
685 				throw new CorruptObjectException(String.format(
686 						JGitText.get().corruptObjectInvalidNameCom,
687 						Character.valueOf(((char) raw[ptr + 3]))));
688 			break;
689 
690 		case 'l': // LPT[1-9]
691 			if (end - ptr >= 4
692 					&& toLower(raw[ptr + 1]) == 'p'
693 					&& toLower(raw[ptr + 2]) == 't'
694 					&& isPositiveDigit(raw[ptr + 3])
695 					&& (end - ptr == 4 || raw[ptr + 4] == '.'))
696 				throw new CorruptObjectException(String.format(
697 						JGitText.get().corruptObjectInvalidNameLpt,
698 						Character.valueOf(((char) raw[ptr + 3]))));
699 			break;
700 
701 		case 'n': // NUL
702 			if (end - ptr >= 3
703 					&& toLower(raw[ptr + 1]) == 'u'
704 					&& toLower(raw[ptr + 2]) == 'l'
705 					&& (end - ptr == 3 || raw[ptr + 3] == '.'))
706 				throw new CorruptObjectException(
707 						JGitText.get().corruptObjectInvalidNameNul);
708 			break;
709 
710 		case 'p': // PRN
711 			if (end - ptr >= 3
712 					&& toLower(raw[ptr + 1]) == 'r'
713 					&& toLower(raw[ptr + 2]) == 'n'
714 					&& (end - ptr == 3 || raw[ptr + 3] == '.'))
715 				throw new CorruptObjectException(
716 						JGitText.get().corruptObjectInvalidNamePrn);
717 			break;
718 		}
719 	}
720 
721 	private static boolean isInvalidOnWindows(byte c) {
722 		// Windows disallows "special" characters in a path component.
723 		switch (c) {
724 		case '"':
725 		case '*':
726 		case ':':
727 		case '<':
728 		case '>':
729 		case '?':
730 		case '\\':
731 		case '|':
732 			return true;
733 		}
734 		return 1 <= c && c <= 31;
735 	}
736 
737 	private static boolean isGit(byte[] buf, int p) {
738 		return toLower(buf[p]) == 'g'
739 				&& toLower(buf[p + 1]) == 'i'
740 				&& toLower(buf[p + 2]) == 't';
741 	}
742 
743 	private static boolean isGitTilde1(byte[] buf, int p, int end) {
744 		if (end - p != 5)
745 			return false;
746 		return toLower(buf[p]) == 'g' && toLower(buf[p + 1]) == 'i'
747 				&& toLower(buf[p + 2]) == 't' && buf[p + 3] == '~'
748 				&& buf[p + 4] == '1';
749 	}
750 
751 	private static boolean isNormalizedGit(byte[] raw, int ptr, int end) {
752 		if (isGit(raw, ptr)) {
753 			int dots = 0;
754 			boolean space = false;
755 			int p = end - 1;
756 			for (; (ptr + 2) < p; p--) {
757 				if (raw[p] == '.')
758 					dots++;
759 				else if (raw[p] == ' ')
760 					space = true;
761 				else
762 					break;
763 			}
764 			return p == ptr + 2 && (dots == 1 || space);
765 		}
766 		return false;
767 	}
768 
769 	private static char toLower(byte b) {
770 		if ('A' <= b && b <= 'Z')
771 			return (char) (b + ('a' - 'A'));
772 		return (char) b;
773 	}
774 
775 	private static boolean isPositiveDigit(byte b) {
776 		return '1' <= b && b <= '9';
777 	}
778 
779 	/**
780 	 * Check a blob for errors.
781 	 *
782 	 * @param raw
783 	 *            the blob data. The array is never modified.
784 	 * @throws CorruptObjectException
785 	 *             if any error was detected.
786 	 */
787 	public void checkBlob(final byte[] raw) throws CorruptObjectException {
788 		// We can always assume the blob is valid.
789 	}
790 
791 	private String normalize(byte[] raw, int ptr, int end) {
792 		String n = RawParseUtils.decode(raw, ptr, end).toLowerCase(Locale.US);
793 		return macosx ? Normalizer.normalize(n) : n;
794 	}
795 
796 	private static class Normalizer {
797 		// TODO Simplify invocation to Normalizer after dropping Java 5.
798 		private static final Method normalize;
799 		private static final Object nfc;
800 		static {
801 			Method method;
802 			Object formNfc;
803 			try {
804 				Class<?> formClazz = Class.forName("java.text.Normalizer$Form"); //$NON-NLS-1$
805 				formNfc = formClazz.getField("NFC").get(null); //$NON-NLS-1$
806 				method = Class.forName("java.text.Normalizer") //$NON-NLS-1$
807 					.getMethod("normalize", CharSequence.class, formClazz); //$NON-NLS-1$
808 			} catch (ClassNotFoundException e) {
809 				method = null;
810 				formNfc = null;
811 			} catch (NoSuchFieldException e) {
812 				method = null;
813 				formNfc = null;
814 			} catch (NoSuchMethodException e) {
815 				method = null;
816 				formNfc = null;
817 			} catch (SecurityException e) {
818 				method = null;
819 				formNfc = null;
820 			} catch (IllegalArgumentException e) {
821 				method = null;
822 				formNfc = null;
823 			} catch (IllegalAccessException e) {
824 				method = null;
825 				formNfc = null;
826 			}
827 			normalize = method;
828 			nfc = formNfc;
829 		}
830 
831 		static String normalize(String in) {
832 			if (normalize == null)
833 				return in;
834 			try {
835 				return (String) normalize.invoke(null, in, nfc);
836 			} catch (IllegalAccessException e) {
837 				return in;
838 			} catch (InvocationTargetException e) {
839 				if (e.getCause() instanceof RuntimeException)
840 					throw (RuntimeException) e.getCause();
841 				if (e.getCause() instanceof Error)
842 					throw (Error) e.getCause();
843 				return in;
844 			}
845 		}
846 	}
847 }