ObjectChecker: Disallow ".git." and ".git<space>"

Windows treats "foo." and "foo " as "foo". The ".git" directory is
special, as it contains metadata for a local Git repository. Disallow
variations that Windows considers to be the same.

Change-Id: I28eb48859a95a89111b4987c91de97557e3bb539
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutMaliciousPathTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutMaliciousPathTest.java
index d29a75e..ca3e066 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutMaliciousPathTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutMaliciousPathTest.java
@@ -186,10 +186,7 @@ public void testMaliciousGitPathEndSpaceWindows() throws Exception {
 
 	@Test
 	public void testMaliciousGitPathEndSpaceUnixOk() throws Exception {
-		if (File.separatorChar == '\\')
-			return; // cannot emulate Unix on Windows for this test
-		((MockSystemReader) SystemReader.getInstance()).setUnix();
-		testMaliciousPathGoodFirstCheckout(".git ", "konfig");
+		testMaliciousPathBadFirstCheckout(".git ", "konfig");
 	}
 
 	@Test
@@ -212,10 +209,7 @@ public void testMaliciousGitPathEndDotWindows3() throws Exception {
 
 	@Test
 	public void testMaliciousGitPathEndDotUnixOk() throws Exception {
-		if (File.separatorChar == '\\')
-			return; // cannot emulate Unix on Windows for this test
-		((MockSystemReader) SystemReader.getInstance()).setUnix();
-		testMaliciousPathGoodFirstCheckout(".git.", "konfig");
+		testMaliciousPathBadFirstCheckout(".git.", "konfig");
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java
index f3db2f6..aa17ac3 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java
@@ -1308,6 +1308,102 @@ public void testInvalidTreeNameIsMixedCaseGit() {
 	}
 
 	@Test
+	public void testInvalidTreeNameIsDotGitDot() {
+		StringBuilder b = new StringBuilder();
+		entry(b, "100644 .git.");
+		byte[] data = Constants.encodeASCII(b.toString());
+		try {
+			checker.checkTree(data);
+			fail("incorrectly accepted an invalid tree");
+		} catch (CorruptObjectException e) {
+			assertEquals("invalid name '.git.'", e.getMessage());
+		}
+	}
+
+	@Test
+	public void testValidTreeNameIsDotGitDotDot()
+			throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
+		entry(b, "100644 .git..");
+		checker.checkTree(Constants.encodeASCII(b.toString()));
+	}
+
+	@Test
+	public void testInvalidTreeNameIsDotGitSpace() {
+		StringBuilder b = new StringBuilder();
+		entry(b, "100644 .git ");
+		byte[] data = Constants.encodeASCII(b.toString());
+		try {
+			checker.checkTree(data);
+			fail("incorrectly accepted an invalid tree");
+		} catch (CorruptObjectException e) {
+			assertEquals("invalid name '.git '", e.getMessage());
+		}
+	}
+
+	@Test
+	public void testInvalidTreeNameIsDotGitSomething()
+			throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
+		entry(b, "100644 .gitfoobar");
+		byte[] data = Constants.encodeASCII(b.toString());
+		checker.checkTree(data);
+	}
+
+	@Test
+	public void testInvalidTreeNameIsDotGitSomethingSpaceSomething()
+			throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
+		entry(b, "100644 .gitfoo bar");
+		byte[] data = Constants.encodeASCII(b.toString());
+		checker.checkTree(data);
+	}
+
+	@Test
+	public void testInvalidTreeNameIsDotGitSomethingDot()
+			throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
+		entry(b, "100644 .gitfoobar.");
+		byte[] data = Constants.encodeASCII(b.toString());
+		checker.checkTree(data);
+	}
+
+	@Test
+	public void testInvalidTreeNameIsDotGitSomethingDotDot()
+			throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
+		entry(b, "100644 .gitfoobar..");
+		byte[] data = Constants.encodeASCII(b.toString());
+		checker.checkTree(data);
+	}
+
+	@Test
+	public void testInvalidTreeNameIsDotGitDotSpace() {
+		StringBuilder b = new StringBuilder();
+		entry(b, "100644 .git. ");
+		byte[] data = Constants.encodeASCII(b.toString());
+		try {
+			checker.checkTree(data);
+			fail("incorrectly accepted an invalid tree");
+		} catch (CorruptObjectException e) {
+			assertEquals("invalid name '.git. '", e.getMessage());
+		}
+	}
+
+	@Test
+	public void testInvalidTreeNameIsDotGitSpaceDot() {
+		StringBuilder b = new StringBuilder();
+		entry(b, "100644 .git . ");
+		byte[] data = Constants.encodeASCII(b.toString());
+		try {
+			checker.checkTree(data);
+			fail("incorrectly accepted an invalid tree");
+		} catch (CorruptObjectException e) {
+			assertEquals("invalid name '.git . '", e.getMessage());
+		}
+	}
+
+	@Test
 	public void testInvalidTreeTruncatedInName() {
 		final StringBuilder b = new StringBuilder();
 		b.append("100644 b");
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java
index 92b4e39..1e4163e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java
@@ -453,7 +453,13 @@ private void checkPathSegment2(byte[] raw, int ptr, int end)
 					throw new CorruptObjectException("invalid name '..'");
 				break;
 			case 4:
-				if (isDotGit(raw, ptr + 1))
+				if (isGit(raw, ptr + 1))
+					throw new CorruptObjectException(String.format(
+							"invalid name '%s'",
+							RawParseUtils.decode(raw, ptr, end)));
+				break;
+			default:
+				if (end - ptr > 4 && isNormalizedGit(raw, ptr + 1, end))
 					throw new CorruptObjectException(String.format(
 							"invalid name '%s'",
 							RawParseUtils.decode(raw, ptr, end)));
@@ -540,12 +546,30 @@ private static boolean isInvalidOnWindows(byte c) {
 		return 1 <= c && c <= 31;
 	}
 
-	private static boolean isDotGit(byte[] buf, int p) {
+	private static boolean isGit(byte[] buf, int p) {
 		return toLower(buf[p]) == 'g'
 				&& toLower(buf[p + 1]) == 'i'
 				&& toLower(buf[p + 2]) == 't';
 	}
 
+	private static boolean isNormalizedGit(byte[] raw, int ptr, int end) {
+		if (isGit(raw, ptr)) {
+			int dots = 0;
+			boolean space = false;
+			int p = end - 1;
+			for (; (ptr + 2) < p; p--) {
+				if (raw[p] == '.')
+					dots++;
+				else if (raw[p] == ' ')
+					space = true;
+				else
+					break;
+			}
+			return p == ptr + 2 && (dots == 1 || space);
+		}
+		return false;
+	}
+
 	private static char toLower(byte b) {
 		if ('A' <= b && b <= 'Z')
 			return (char) (b + ('a' - 'A'));