From ac41920a430941b9385889d19e0fc2764ba23474 Mon Sep 17 00:00:00 2001 From: Shawn Pearce Date: Tue, 29 Dec 2015 18:21:19 -0800 Subject: [PATCH] ObjectChecker: honor some git-core fsck.* options Accept some of the same section keys that fsck does in git-core, allowing repositories to skip over specific kinds of acceptable broken objects, e.g.: [fsck] duplicateEntries = ignore zeroPaddedFilemode = ignore The zeroPaddedFilemode = ignore is a synonym for the JGit specific allowLeadingZeroFileMode = true. Only accept the JGit key if git-core key was not specified. Change-Id: Idaed9310e2a5ce5511670ead1aaea2b30aac903c --- .../eclipse/jgit/lib/ObjectCheckerTest.java | 148 ++++- .../eclipse/jgit/internal/JGitText.properties | 9 +- .../jgit/errors/CorruptObjectException.java | 41 +- .../org/eclipse/jgit/internal/JGitText.java | 9 +- .../org/eclipse/jgit/lib/ObjectChecker.java | 505 ++++++++++++------ .../eclipse/jgit/transport/PackParser.java | 3 + .../jgit/transport/TransferConfig.java | 84 ++- 7 files changed, 597 insertions(+), 202 deletions(-) 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 9519303095..80230dccfa 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 @@ -53,7 +53,17 @@ import static org.eclipse.jgit.lib.Constants.OBJ_TAG; import static org.eclipse.jgit.lib.Constants.OBJ_TREE; import static org.eclipse.jgit.lib.Constants.encode; import static org.eclipse.jgit.lib.Constants.encodeASCII; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.DUPLICATE_ENTRIES; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.EMPTY_NAME; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.FULL_PATHNAME; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOT; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOTDOT; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOTGIT; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.NULL_SHA1; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.TREE_NOT_SORTED; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.ZERO_PADDED_FILEMODE; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; import static org.junit.Assert.fail; import java.io.UnsupportedEncodingException; @@ -130,7 +140,7 @@ public class ObjectCheckerTest { b.append("committer <> 0 +0000\n"); byte[] data = encodeASCII(b.toString()); - assertCorrupt("invalid author", OBJ_COMMIT, data); + assertCorrupt("bad date", OBJ_COMMIT, data); checker.setAllowInvalidPersonIdent(true); checker.checkCommit(data); @@ -146,7 +156,7 @@ public class ObjectCheckerTest { b.append("committer b 0 +0000\n"); byte[] data = encodeASCII(b.toString()); - assertCorrupt("invalid committer", OBJ_COMMIT, data); + assertCorrupt("bad date", OBJ_COMMIT, data); checker.setAllowInvalidPersonIdent(true); checker.checkCommit(data); @@ -300,7 +310,6 @@ public class ObjectCheckerTest { byte[] data = encodeASCII(b.toString()); assertCorrupt("invalid tree", OBJ_COMMIT, data); - assertSkipListRejects("invalid tree", OBJ_COMMIT, data); } @Test @@ -425,7 +434,7 @@ public class ObjectCheckerTest { b.append("author A. U. Thor 1 +0000\n"); byte[] data = encodeASCII(b.toString()); - assertCorrupt("invalid author", OBJ_COMMIT, data); + assertCorrupt("missing email", OBJ_COMMIT, data); assertSkipListAccepts(OBJ_COMMIT, data); } @@ -453,7 +462,7 @@ public class ObjectCheckerTest { b.append("author 1 +0000\n"); byte[] data = encodeASCII(b.toString()); - assertCorrupt("invalid author", OBJ_COMMIT, data); + assertCorrupt("missing email", OBJ_COMMIT, data); assertSkipListAccepts(OBJ_COMMIT, data); } @@ -467,7 +476,7 @@ public class ObjectCheckerTest { b.append("author a +0000\n"); byte[] data = encodeASCII(b.toString()); - assertCorrupt("invalid author", OBJ_COMMIT, data); + assertCorrupt("bad date", OBJ_COMMIT, data); assertSkipListAccepts(OBJ_COMMIT, data); } @@ -481,7 +490,7 @@ public class ObjectCheckerTest { b.append("author a \n"); byte[] data = encodeASCII(b.toString()); - assertCorrupt("invalid author", OBJ_COMMIT, data); + assertCorrupt("bad date", OBJ_COMMIT, data); assertSkipListAccepts(OBJ_COMMIT, data); } @@ -495,7 +504,7 @@ public class ObjectCheckerTest { b.append("author a z"); byte[] data = encodeASCII(b.toString()); - assertCorrupt("invalid author", OBJ_COMMIT, data); + assertCorrupt("bad date", OBJ_COMMIT, data); assertSkipListAccepts(OBJ_COMMIT, data); } @@ -509,7 +518,7 @@ public class ObjectCheckerTest { b.append("author a 1 z"); byte[] data = encodeASCII(b.toString()); - assertCorrupt("invalid author", OBJ_COMMIT, data); + assertCorrupt("bad time zone", OBJ_COMMIT, data); assertSkipListAccepts(OBJ_COMMIT, data); } @@ -524,7 +533,7 @@ public class ObjectCheckerTest { b.append("committer a <"); byte[] data = encodeASCII(b.toString()); - assertCorrupt("invalid committer", OBJ_COMMIT, data); + assertCorrupt("bad email", OBJ_COMMIT, data); assertSkipListAccepts(OBJ_COMMIT, data); } @@ -686,7 +695,7 @@ public class ObjectCheckerTest { b.append("tagger \n"); byte[] data = encodeASCII(b.toString()); - assertCorrupt("invalid tagger", OBJ_TAG, data); + assertCorrupt("missing email", OBJ_TAG, data); checker.setAllowInvalidPersonIdent(true); checker.checkTag(data); @@ -706,7 +715,7 @@ public class ObjectCheckerTest { b.append("tagger a < 1 +000\n"); byte[] data = encodeASCII(b.toString()); - assertCorrupt("invalid tagger", OBJ_TAG, data); + assertCorrupt("bad email", OBJ_TAG, data); assertSkipListAccepts(OBJ_TAG, data); } @@ -758,6 +767,17 @@ public class ObjectCheckerTest { checker.checkTree(encodeASCII(b.toString())); } + @Test + public void testNullSha1InTreeEntry() throws CorruptObjectException { + byte[] data = concat( + encodeASCII("100644 A"), new byte[] { '\0' }, + new byte[OBJECT_ID_LENGTH]); + assertCorrupt("entry points to null SHA-1", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(NULL_SHA1, true); + checker.checkTree(data); + } + @Test public void testValidPosixTree() throws CorruptObjectException { checkOneName("ac:d|e"); @@ -842,6 +862,9 @@ public class ObjectCheckerTest { checker.setAllowLeadingZeroFileMode(false); assertSkipListAccepts(OBJ_TREE, data); + + checker.setIgnore(ZERO_PADDED_FILEMODE, true); + checker.checkTree(data); } @Test @@ -905,39 +928,48 @@ public class ObjectCheckerTest { } @Test - public void testInvalidTreeNameContainsSlash() { + public void testInvalidTreeNameContainsSlash() + throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 a/b"); byte[] data = encodeASCII(b.toString()); assertCorrupt("name contains '/'", OBJ_TREE, data); - assertSkipListRejects("name contains '/'", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(FULL_PATHNAME, true); + checker.checkTree(data); } @Test - public void testInvalidTreeNameIsEmpty() { + public void testInvalidTreeNameIsEmpty() throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 "); byte[] data = encodeASCII(b.toString()); assertCorrupt("zero length name", OBJ_TREE, data); - assertSkipListRejects("zero length name", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(EMPTY_NAME, true); + checker.checkTree(data); } @Test - public void testInvalidTreeNameIsDot() { + public void testInvalidTreeNameIsDot() throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 ."); byte[] data = encodeASCII(b.toString()); assertCorrupt("invalid name '.'", OBJ_TREE, data); - assertSkipListRejects("invalid name '.'", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOT, true); + checker.checkTree(data); } @Test - public void testInvalidTreeNameIsDotDot() { + public void testInvalidTreeNameIsDotDot() throws CorruptObjectException { StringBuilder b = new StringBuilder(); entry(b, "100644 .."); byte[] data = encodeASCII(b.toString()); assertCorrupt("invalid name '..'", OBJ_TREE, data); - assertSkipListRejects("invalid name '..'", OBJ_TREE, data); + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTDOT, true); + checker.checkTree(data); } @Test @@ -947,6 +979,8 @@ public class ObjectCheckerTest { byte[] data = encodeASCII(b.toString()); assertCorrupt("invalid name '.git'", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test @@ -957,6 +991,8 @@ public class ObjectCheckerTest { byte[] data = encodeASCII(b.toString()); assertCorrupt("invalid name '.GiT'", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test @@ -974,6 +1010,8 @@ public class ObjectCheckerTest { "invalid name '.gi\u200Ct' contains ignorable Unicode characters", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test @@ -992,6 +1030,8 @@ public class ObjectCheckerTest { "invalid name '\u206B.git' contains ignorable Unicode characters", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test @@ -1010,12 +1050,22 @@ public class ObjectCheckerTest { "invalid name '.git\uFEFF' contains ignorable Unicode characters", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } - private static byte[] concat(byte[] b1, byte[] b2) { - byte[] data = new byte[b1.length + b2.length]; - System.arraycopy(b1, 0, data, 0, b1.length); - System.arraycopy(b2, 0, data, b1.length, b2.length); + private static byte[] concat(byte[]... b) { + int n = 0; + for (byte[] a : b) { + n += a.length; + } + + byte[] data = new byte[n]; + n = 0; + for (byte[] a : b) { + System.arraycopy(a, 0, data, n, a.length); + n += a.length; + } return data; } @@ -1096,6 +1146,8 @@ public class ObjectCheckerTest { byte[] data = encodeASCII(b.toString()); assertCorrupt("invalid name '.git.'", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test @@ -1114,6 +1166,8 @@ public class ObjectCheckerTest { byte[] data = encodeASCII(b.toString()); assertCorrupt("invalid name '.git '", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test @@ -1160,6 +1214,8 @@ public class ObjectCheckerTest { byte[] data = encodeASCII(b.toString()); assertCorrupt("invalid name '.git. '", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test @@ -1170,6 +1226,8 @@ public class ObjectCheckerTest { byte[] data = encodeASCII(b.toString()); assertCorrupt("invalid name '.git . '", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test @@ -1179,6 +1237,8 @@ public class ObjectCheckerTest { byte[] data = encodeASCII(b.toString()); assertCorrupt("invalid name 'GIT~1'", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test @@ -1188,6 +1248,8 @@ public class ObjectCheckerTest { byte[] data = encodeASCII(b.toString()); assertCorrupt("invalid name 'GiT~1'", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(HAS_DOTGIT, true); + checker.checkTree(data); } @Test @@ -1222,8 +1284,22 @@ public class ObjectCheckerTest { entry(b, "100644 foobar"); entry(b, "100644 fooaaa"); byte[] data = encodeASCII(b.toString()); + assertCorrupt("incorrectly sorted", OBJ_TREE, data); + + ObjectId id = idFor(OBJ_TREE, data); + try { + checker.check(id, OBJ_TREE, data); + fail("Did not throw CorruptObjectException"); + } catch (CorruptObjectException e) { + assertSame(TREE_NOT_SORTED, e.getErrorType()); + assertEquals("treeNotSorted: object " + id.name() + + ": incorrectly sorted", e.getMessage()); + } + assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(TREE_NOT_SORTED, true); + checker.checkTree(data); } @Test @@ -1234,6 +1310,8 @@ public class ObjectCheckerTest { byte[] data = encodeASCII(b.toString()); assertCorrupt("incorrectly sorted", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(TREE_NOT_SORTED, true); + checker.checkTree(data); } @Test @@ -1244,6 +1322,8 @@ public class ObjectCheckerTest { byte[] data = encodeASCII(b.toString()); assertCorrupt("incorrectly sorted", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(TREE_NOT_SORTED, true); + checker.checkTree(data); } @Test @@ -1254,6 +1334,8 @@ public class ObjectCheckerTest { byte[] data = encodeASCII(b.toString()); assertCorrupt("duplicate entry names", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(DUPLICATE_ENTRIES, true); + checker.checkTree(data); } @Test @@ -1264,6 +1346,8 @@ public class ObjectCheckerTest { byte[] data = encodeASCII(b.toString()); assertCorrupt("duplicate entry names", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(DUPLICATE_ENTRIES, true); + checker.checkTree(data); } @Test @@ -1274,6 +1358,8 @@ public class ObjectCheckerTest { byte[] data = encodeASCII(b.toString()); assertCorrupt("duplicate entry names", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(DUPLICATE_ENTRIES, true); + checker.checkTree(data); } @Test @@ -1288,30 +1374,36 @@ public class ObjectCheckerTest { byte[] data = encodeASCII(b.toString()); assertCorrupt("duplicate entry names", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(DUPLICATE_ENTRIES, true); + checker.checkTree(data); } @Test public void testInvalidTreeDuplicateNames5() throws UnsupportedEncodingException, CorruptObjectException { StringBuilder b = new StringBuilder(); - entry(b, "100644 a"); entry(b, "100644 A"); + entry(b, "100644 a"); byte[] data = b.toString().getBytes("UTF-8"); checker.setSafeForWindows(true); assertCorrupt("duplicate entry names", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(DUPLICATE_ENTRIES, true); + checker.checkTree(data); } @Test public void testInvalidTreeDuplicateNames6() throws UnsupportedEncodingException, CorruptObjectException { StringBuilder b = new StringBuilder(); - entry(b, "100644 a"); entry(b, "100644 A"); + entry(b, "100644 a"); byte[] data = b.toString().getBytes("UTF-8"); checker.setSafeForMacOS(true); assertCorrupt("duplicate entry names", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(DUPLICATE_ENTRIES, true); + checker.checkTree(data); } @Test @@ -1324,6 +1416,8 @@ public class ObjectCheckerTest { checker.setSafeForMacOS(true); assertCorrupt("duplicate entry names", OBJ_TREE, data); assertSkipListAccepts(OBJ_TREE, data); + checker.setIgnore(DUPLICATE_ENTRIES, true); + checker.checkTree(data); } @Test diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 840059d34f..992e10bad6 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -126,14 +126,15 @@ connectionFailed=connection failed connectionTimeOut=Connection time out: {0} contextMustBeNonNegative=context must be >= 0 corruptionDetectedReReadingAt=Corruption detected re-reading at {0} +corruptObjectBadDate=bad date +corruptObjectBadEmail=bad email corruptObjectBadStream=bad stream corruptObjectBadStreamCorruptHeader=bad stream, corrupt header +corruptObjectBadTimezone=bad time zone corruptObjectDuplicateEntryNames=duplicate entry names corruptObjectGarbageAfterSize=garbage after size corruptObjectIncorrectLength=incorrect length corruptObjectIncorrectSorting=incorrectly sorted -corruptObjectInvalidAuthor=invalid author -corruptObjectInvalidCommitter=invalid committer corruptObjectInvalidEntryMode=invalid entry mode corruptObjectInvalidMode=invalid mode corruptObjectInvalidModeChar=invalid mode character @@ -152,11 +153,11 @@ corruptObjectInvalidNameNul=invalid name 'NUL' corruptObjectInvalidNamePrn=invalid name 'PRN' corruptObjectInvalidObject=invalid object corruptObjectInvalidParent=invalid parent -corruptObjectInvalidTagger=invalid tagger corruptObjectInvalidTree=invalid tree corruptObjectInvalidType=invalid type corruptObjectInvalidType2=invalid type {0} corruptObjectMalformedHeader=malformed header: {0} +corruptObjectMissingEmail=missing email corruptObjectNameContainsByte=name contains byte 0x%x corruptObjectNameContainsChar=name contains '%c' corruptObjectNameContainsNullByte=name contains byte 0x00 @@ -182,6 +183,7 @@ corruptObjectPackfileChecksumIncorrect=Packfile checksum incorrect. corruptObjectTruncatedInMode=truncated in mode corruptObjectTruncatedInName=truncated in name corruptObjectTruncatedInObjectId=truncated in object id +corruptObjectZeroId=entry points to null SHA-1 couldNotCheckOutBecauseOfConflicts=Could not check out because of conflicts couldNotDeleteLockFileShouldNotHappen=Could not delete lock file. Should not happen couldNotDeleteTemporaryIndexFileShouldNotHappen=Could not delete temporary index file. Should not happen @@ -433,6 +435,7 @@ noXMLParserAvailable=No XML parser available. objectAtHasBadZlibStream=Object at {0} in {1} has bad zlib stream objectAtPathDoesNotHaveId=Object at path "{0}" does not have an id assigned. All object ids must be assigned prior to writing a tree. objectIsCorrupt=Object {0} is corrupt: {1} +objectIsCorrupt3={0}: object {1}: {2} objectIsNotA=Object {0} is not a {1}. objectNotFound=Object {0} not found. objectNotFoundIn=Object {0} not found in {1}. diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java index c6ea093750..e4db40b889 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java @@ -49,8 +49,10 @@ package org.eclipse.jgit.errors; import java.io.IOException; import java.text.MessageFormat; +import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectChecker; import org.eclipse.jgit.lib.ObjectId; /** @@ -59,6 +61,26 @@ import org.eclipse.jgit.lib.ObjectId; public class CorruptObjectException extends IOException { private static final long serialVersionUID = 1L; + private ObjectChecker.ErrorType errorType; + + /** + * Report a specific error condition discovered in an object. + * + * @param type + * type of error + * @param id + * identity of the bad object + * @param why + * description of the error. + * @since 4.2 + */ + public CorruptObjectException(ObjectChecker.ErrorType type, AnyObjectId id, + String why) { + super(MessageFormat.format(JGitText.get().objectIsCorrupt3, + type.getMessageId(), id.name(), why)); + this.errorType = type; + } + /** * Construct a CorruptObjectException for reporting a problem specified * object id @@ -66,8 +88,8 @@ public class CorruptObjectException extends IOException { * @param id * @param why */ - public CorruptObjectException(final AnyObjectId id, final String why) { - this(id.toObjectId(), why); + public CorruptObjectException(AnyObjectId id, String why) { + super(MessageFormat.format(JGitText.get().objectIsCorrupt, id.name(), why)); } /** @@ -77,7 +99,7 @@ public class CorruptObjectException extends IOException { * @param id * @param why */ - public CorruptObjectException(final ObjectId id, final String why) { + public CorruptObjectException(ObjectId id, String why) { super(MessageFormat.format(JGitText.get().objectIsCorrupt, id.name(), why)); } @@ -87,7 +109,7 @@ public class CorruptObjectException extends IOException { * * @param why */ - public CorruptObjectException(final String why) { + public CorruptObjectException(String why) { super(why); } @@ -105,4 +127,15 @@ public class CorruptObjectException extends IOException { super(why); initCause(cause); } + + /** + * Specific error condition identified by {@link ObjectChecker}. + * + * @return error condition or null. + * @since 4.2 + */ + @Nullable + public ObjectChecker.ErrorType getErrorType() { + return errorType; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index c476f1773e..7740a2bb80 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -185,14 +185,15 @@ public class JGitText extends TranslationBundle { /***/ public String connectionTimeOut; /***/ public String contextMustBeNonNegative; /***/ public String corruptionDetectedReReadingAt; + /***/ public String corruptObjectBadDate; + /***/ public String corruptObjectBadEmail; /***/ public String corruptObjectBadStream; /***/ public String corruptObjectBadStreamCorruptHeader; + /***/ public String corruptObjectBadTimezone; /***/ public String corruptObjectDuplicateEntryNames; /***/ public String corruptObjectGarbageAfterSize; /***/ public String corruptObjectIncorrectLength; /***/ public String corruptObjectIncorrectSorting; - /***/ public String corruptObjectInvalidAuthor; - /***/ public String corruptObjectInvalidCommitter; /***/ public String corruptObjectInvalidEntryMode; /***/ public String corruptObjectInvalidMode; /***/ public String corruptObjectInvalidModeChar; @@ -211,11 +212,11 @@ public class JGitText extends TranslationBundle { /***/ public String corruptObjectInvalidNamePrn; /***/ public String corruptObjectInvalidObject; /***/ public String corruptObjectInvalidParent; - /***/ public String corruptObjectInvalidTagger; /***/ public String corruptObjectInvalidTree; /***/ public String corruptObjectInvalidType; /***/ public String corruptObjectInvalidType2; /***/ public String corruptObjectMalformedHeader; + /***/ public String corruptObjectMissingEmail; /***/ public String corruptObjectNameContainsByte; /***/ public String corruptObjectNameContainsChar; /***/ public String corruptObjectNameContainsNullByte; @@ -241,6 +242,7 @@ public class JGitText extends TranslationBundle { /***/ public String corruptObjectTruncatedInMode; /***/ public String corruptObjectTruncatedInName; /***/ public String corruptObjectTruncatedInObjectId; + /***/ public String corruptObjectZeroId; /***/ public String corruptPack; /***/ public String couldNotCheckOutBecauseOfConflicts; /***/ public String couldNotDeleteLockFileShouldNotHappen; @@ -492,6 +494,7 @@ public class JGitText extends TranslationBundle { /***/ public String objectAtHasBadZlibStream; /***/ public String objectAtPathDoesNotHaveId; /***/ public String objectIsCorrupt; + /***/ public String objectIsCorrupt3; /***/ public String objectIsNotA; /***/ public String objectNotFound; /***/ public String objectNotFoundIn; 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 89a526911a..858a0385d0 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java @@ -44,25 +44,56 @@ package org.eclipse.jgit.lib; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH; +import static org.eclipse.jgit.lib.Constants.OBJ_BAD; import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT; import static org.eclipse.jgit.lib.Constants.OBJ_TAG; import static org.eclipse.jgit.lib.Constants.OBJ_TREE; -import static org.eclipse.jgit.util.RawParseUtils.match; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_DATE; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_EMAIL; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_OBJECT_SHA1; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_PARENT_SHA1; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_TIMEZONE; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_TREE_SHA1; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_UTF8; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.DUPLICATE_ENTRIES; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.EMPTY_NAME; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.FULL_PATHNAME; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOT; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOTDOT; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOTGIT; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_AUTHOR; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_COMMITTER; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_EMAIL; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_OBJECT; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_SPACE_BEFORE_DATE; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_TAG_ENTRY; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_TREE; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_TYPE_ENTRY; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.NULL_SHA1; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.TREE_NOT_SORTED; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.UNKNOWN_TYPE; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.WIN32_BAD_NAME; +import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.ZERO_PADDED_FILEMODE; import static org.eclipse.jgit.util.RawParseUtils.nextLF; import static org.eclipse.jgit.util.RawParseUtils.parseBase10; import java.text.MessageFormat; import java.text.Normalizer; +import java.util.EnumSet; import java.util.HashSet; import java.util.Locale; import java.util.Set; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.util.MutableInteger; import org.eclipse.jgit.util.RawParseUtils; +import org.eclipse.jgit.util.StringUtils; /** * Verifies that an object is formatted correctly. @@ -103,11 +134,65 @@ public class ObjectChecker { /** Header "tagger " */ public static final byte[] tagger = Constants.encodeASCII("tagger "); //$NON-NLS-1$ + /** + * Potential issues identified by the checker. + * + * @since 4.2 + */ + public enum ErrorType { + // @formatter:off + // These names match git-core so that fsck section keys also match. + /***/ NULL_SHA1, + /***/ DUPLICATE_ENTRIES, + /***/ TREE_NOT_SORTED, + /***/ ZERO_PADDED_FILEMODE, + /***/ EMPTY_NAME, + /***/ FULL_PATHNAME, + /***/ HAS_DOT, + /***/ HAS_DOTDOT, + /***/ HAS_DOTGIT, + /***/ BAD_OBJECT_SHA1, + /***/ BAD_PARENT_SHA1, + /***/ BAD_TREE_SHA1, + /***/ MISSING_AUTHOR, + /***/ MISSING_COMMITTER, + /***/ MISSING_OBJECT, + /***/ MISSING_TREE, + /***/ MISSING_TYPE_ENTRY, + /***/ MISSING_TAG_ENTRY, + /***/ BAD_DATE, + /***/ BAD_EMAIL, + /***/ BAD_TIMEZONE, + /***/ MISSING_EMAIL, + /***/ MISSING_SPACE_BEFORE_DATE, + /***/ UNKNOWN_TYPE, + + // These are unique to JGit. + /***/ WIN32_BAD_NAME, + /***/ BAD_UTF8; + // @formatter:on + + /** @return camelCaseVersion of the name. */ + public String getMessageId() { + String n = name(); + StringBuilder r = new StringBuilder(n.length()); + for (int i = 0; i < n.length(); i++) { + char c = n.charAt(i); + if (c != '_') { + r.append(StringUtils.toLowerCase(c)); + } else { + r.append(n.charAt(++i)); + } + } + return r.toString(); + } + } + private final MutableObjectId tempId = new MutableObjectId(); - private final MutableInteger ptrout = new MutableInteger(); + private final MutableInteger bufPtr = new MutableInteger(); + private EnumSet errors = EnumSet.allOf(ErrorType.class); private ObjectIdSet skipList; - private boolean allowZeroMode; private boolean allowInvalidPersonIdent; private boolean windows; private boolean macosx; @@ -126,6 +211,42 @@ public class ObjectChecker { return this; } + /** + * Configure error types to be ignored across all objects. + * + * @param ids + * error types to ignore. The caller's set is copied. + * @return {@code this} + * @since 4.2 + */ + public ObjectChecker setIgnore(@Nullable Set ids) { + errors = EnumSet.allOf(ErrorType.class); + if (ids != null) { + errors.removeAll(ids); + } + return this; + } + + /** + * Add message type to be ignored across all objects. + * + * @param id + * error type to ignore. + * @param ignore + * true to ignore this error; false to treat the error as an + * error and throw. + * @return {@code this} + * @since 4.2 + */ + public ObjectChecker setIgnore(ErrorType id, boolean ignore) { + if (ignore) { + errors.remove(id); + } else { + errors.add(id); + } + return this; + } + /** * Enable accepting leading zero mode in tree entries. *

@@ -133,14 +254,15 @@ public class ObjectChecker { * tree entries. This is technically incorrect but gracefully allowed by * git-core. JGit rejects such trees by default, but may need to accept * them on broken histories. + *

+ * Same as {@code setIgnore(ZERO_PADDED_FILEMODE, allow)}. * * @param allow allow leading zero mode. * @return {@code this}. * @since 3.4 */ public ObjectChecker setAllowLeadingZeroFileMode(boolean allow) { - allowZeroMode = allow; - return this; + return setIgnore(ZERO_PADDED_FILEMODE, allow); } /** @@ -238,50 +360,80 @@ public class ObjectChecker { checkBlob(raw); break; default: - throw new CorruptObjectException(MessageFormat.format( + report(UNKNOWN_TYPE, id, MessageFormat.format( JGitText.get().corruptObjectInvalidType2, Integer.valueOf(objType))); } } - private int id(final byte[] raw, final int ptr) { + private boolean checkId(byte[] raw) { + int p = bufPtr.value; try { - tempId.fromString(raw, ptr); - return ptr + Constants.OBJECT_ID_STRING_LENGTH; + tempId.fromString(raw, p); } catch (IllegalArgumentException e) { - return -1; + bufPtr.value = nextLF(raw, p); + return false; } + + p += OBJECT_ID_STRING_LENGTH; + if (raw[p] == '\n') { + bufPtr.value = p + 1; + return true; + } + bufPtr.value = nextLF(raw, p); + return false; } - private int personIdent(byte[] raw, int ptr, @Nullable AnyObjectId id) { - if (allowInvalidPersonIdent || skip(id)) - return nextLF(raw, ptr); + private void checkPersonIdent(byte[] raw, @Nullable AnyObjectId id) + throws CorruptObjectException { + if (allowInvalidPersonIdent) { + bufPtr.value = nextLF(raw, bufPtr.value); + return; + } - final int emailB = nextLF(raw, ptr, '<'); - if (emailB == ptr || raw[emailB - 1] != '<') - return -1; + final int emailB = nextLF(raw, bufPtr.value, '<'); + if (emailB == bufPtr.value || raw[emailB - 1] != '<') { + report(MISSING_EMAIL, id, JGitText.get().corruptObjectMissingEmail); + bufPtr.value = nextLF(raw, bufPtr.value); + return; + } final int emailE = nextLF(raw, emailB, '>'); - if (emailE == emailB || raw[emailE - 1] != '>') - return -1; - if (emailE == raw.length || raw[emailE] != ' ') - return -1; + if (emailE == emailB || raw[emailE - 1] != '>') { + report(BAD_EMAIL, id, JGitText.get().corruptObjectBadEmail); + bufPtr.value = nextLF(raw, bufPtr.value); + return; + } + if (emailE == raw.length || raw[emailE] != ' ') { + report(MISSING_SPACE_BEFORE_DATE, id, + JGitText.get().corruptObjectBadDate); + bufPtr.value = nextLF(raw, bufPtr.value); + return; + } - parseBase10(raw, emailE + 1, ptrout); // when - ptr = ptrout.value; - if (emailE + 1 == ptr) - return -1; - if (ptr == raw.length || raw[ptr] != ' ') - return -1; + parseBase10(raw, emailE + 1, bufPtr); // when + if (emailE + 1 == bufPtr.value || bufPtr.value == raw.length + || raw[bufPtr.value] != ' ') { + report(BAD_DATE, id, JGitText.get().corruptObjectBadDate); + bufPtr.value = nextLF(raw, bufPtr.value); + return; + } - parseBase10(raw, ptr + 1, ptrout); // tz offset - if (ptr + 1 == ptrout.value) - return -1; + int p = bufPtr.value + 1; + parseBase10(raw, p, bufPtr); // tz offset + if (p == bufPtr.value) { + report(BAD_TIMEZONE, id, JGitText.get().corruptObjectBadTimezone); + bufPtr.value = nextLF(raw, bufPtr.value); + return; + } - ptr = ptrout.value; - if (raw[ptr++] == '\n') - return ptr; - return -1; + p = bufPtr.value; + if (raw[p] == '\n') { + bufPtr.value = p + 1; + } else { + report(BAD_TIMEZONE, id, JGitText.get().corruptObjectBadTimezone); + bufPtr.value = nextLF(raw, p); + } } /** @@ -309,41 +461,31 @@ public class ObjectChecker { */ public void checkCommit(@Nullable AnyObjectId id, byte[] raw) throws CorruptObjectException { - int ptr = 0; + bufPtr.value = 0; - if ((ptr = match(raw, ptr, tree)) < 0) - throw new CorruptObjectException( - JGitText.get().corruptObjectNotreeHeader); - if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n') - throw new CorruptObjectException( - JGitText.get().corruptObjectInvalidTree); + if (!match(raw, tree)) { + report(MISSING_TREE, id, JGitText.get().corruptObjectNotreeHeader); + } else if (!checkId(raw)) { + report(BAD_TREE_SHA1, id, JGitText.get().corruptObjectInvalidTree); + } - while (match(raw, ptr, parent) >= 0) { - ptr += parent.length; - if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n') - throw new CorruptObjectException( + while (match(raw, parent)) { + if (!checkId(raw)) { + report(BAD_PARENT_SHA1, id, JGitText.get().corruptObjectInvalidParent); + } } - int p = match(raw, ptr, author); - if (p > ptr) { - if ((ptr = personIdent(raw, p, id)) < 0) { - throw new CorruptObjectException( - JGitText.get().corruptObjectInvalidAuthor); - } - } else if (!skip(id)) { - throw new CorruptObjectException( - JGitText.get().corruptObjectNoAuthor); + if (match(raw, author)) { + checkPersonIdent(raw, id); + } else { + report(MISSING_AUTHOR, id, JGitText.get().corruptObjectNoAuthor); } - p = match(raw, ptr, committer); - if (p > ptr) { - if ((ptr = personIdent(raw, p, id)) < 0) { - throw new CorruptObjectException( - JGitText.get().corruptObjectInvalidCommitter); - } - } else if (!skip(id)) { - throw new CorruptObjectException( + if (match(raw, committer)) { + checkPersonIdent(raw, id); + } else { + report(MISSING_COMMITTER, id, JGitText.get().corruptObjectNoCommitter); } } @@ -373,30 +515,29 @@ public class ObjectChecker { */ public void checkTag(@Nullable AnyObjectId id, byte[] raw) throws CorruptObjectException { - int ptr = 0; - - if ((ptr = match(raw, ptr, object)) < 0) - throw new CorruptObjectException( + bufPtr.value = 0; + if (!match(raw, object)) { + report(MISSING_OBJECT, id, JGitText.get().corruptObjectNoObjectHeader); - if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n') - throw new CorruptObjectException( + } else if (!checkId(raw)) { + report(BAD_OBJECT_SHA1, id, JGitText.get().corruptObjectInvalidObject); + } - if ((ptr = match(raw, ptr, type)) < 0) - throw new CorruptObjectException( + if (!match(raw, type)) { + report(MISSING_TYPE_ENTRY, id, JGitText.get().corruptObjectNoTypeHeader); - ptr = nextLF(raw, ptr); + } + bufPtr.value = nextLF(raw, bufPtr.value); - if (match(raw, ptr, tag) < 0 && !skip(id)) - throw new CorruptObjectException( + if (!match(raw, tag)) { + report(MISSING_TAG_ENTRY, id, JGitText.get().corruptObjectNoTagHeader); - ptr = nextLF(raw, ptr); + } + bufPtr.value = nextLF(raw, bufPtr.value); - if ((ptr = match(raw, ptr, tagger)) > 0) { - if ((ptr = personIdent(raw, ptr, id)) < 0) { - throw new CorruptObjectException( - JGitText.get().corruptObjectInvalidTagger); - } + if (match(raw, tagger)) { + checkPersonIdent(raw, id); } } @@ -485,82 +626,96 @@ public class ObjectChecker { final int sz = raw.length; int ptr = 0; int lastNameB = 0, lastNameE = 0, lastMode = 0; - boolean skip = skip(id); - Set normalized = !skip && (windows || macosx) + Set normalized = windows || macosx ? new HashSet() : null; while (ptr < sz) { int thisMode = 0; for (;;) { - if (ptr == sz) + if (ptr == sz) { throw new CorruptObjectException( JGitText.get().corruptObjectTruncatedInMode); + } final byte c = raw[ptr++]; if (' ' == c) break; - if (c < '0' || c > '7') + if (c < '0' || c > '7') { throw new CorruptObjectException( JGitText.get().corruptObjectInvalidModeChar); - if (thisMode == 0 && c == '0' && !allowZeroMode && !skip) - throw new CorruptObjectException( + } + if (thisMode == 0 && c == '0') { + report(ZERO_PADDED_FILEMODE, id, JGitText.get().corruptObjectInvalidModeStartsZero); + } thisMode <<= 3; thisMode += c - '0'; } - if (FileMode.fromBits(thisMode).getObjectType() == Constants.OBJ_BAD) + if (FileMode.fromBits(thisMode).getObjectType() == OBJ_BAD) { throw new CorruptObjectException(MessageFormat.format( JGitText.get().corruptObjectInvalidMode2, Integer.valueOf(thisMode))); + } final int thisNameB = ptr; - ptr = scanPathSegment(raw, ptr, sz); - if (ptr == sz || raw[ptr] != 0) + ptr = scanPathSegment(raw, ptr, sz, id); + if (ptr == sz || raw[ptr] != 0) { throw new CorruptObjectException( JGitText.get().corruptObjectTruncatedInName); - checkPathSegment2(raw, thisNameB, ptr, skip); + } + checkPathSegment2(raw, thisNameB, ptr, id); if (normalized != null) { - if (!normalized.add(normalize(raw, thisNameB, ptr))) - throw new CorruptObjectException( + if (!normalized.add(normalize(raw, thisNameB, ptr))) { + report(DUPLICATE_ENTRIES, id, JGitText.get().corruptObjectDuplicateEntryNames); - } else if (!skip && duplicateName(raw, thisNameB, ptr)) - throw new CorruptObjectException( + } + } else if (duplicateName(raw, thisNameB, ptr)) { + report(DUPLICATE_ENTRIES, id, JGitText.get().corruptObjectDuplicateEntryNames); + } - if (!skip && lastNameB != 0) { + if (lastNameB != 0) { final int cmp = pathCompare(raw, lastNameB, lastNameE, lastMode, thisNameB, ptr, thisMode); - if (cmp > 0) - throw new CorruptObjectException( + if (cmp > 0) { + report(TREE_NOT_SORTED, id, JGitText.get().corruptObjectIncorrectSorting); + } } lastNameB = thisNameB; lastNameE = ptr; lastMode = thisMode; - ptr += 1 + Constants.OBJECT_ID_LENGTH; - if (ptr > sz) + ptr += 1 + OBJECT_ID_LENGTH; + if (ptr > sz) { throw new CorruptObjectException( JGitText.get().corruptObjectTruncatedInObjectId); + } + if (ObjectId.zeroId().compareTo(raw, ptr - OBJECT_ID_LENGTH) == 0) { + report(NULL_SHA1, id, JGitText.get().corruptObjectZeroId); + } } } - private int scanPathSegment(byte[] raw, int ptr, int end) - throws CorruptObjectException { + private int scanPathSegment(byte[] raw, int ptr, int end, + @Nullable AnyObjectId id) throws CorruptObjectException { for (; ptr < end; ptr++) { byte c = raw[ptr]; - if (c == 0) + if (c == 0) { return ptr; - if (c == '/') - throw new CorruptObjectException( + } + if (c == '/') { + report(FULL_PATHNAME, id, JGitText.get().corruptObjectNameContainsSlash); + } if (windows && isInvalidOnWindows(c)) { - if (c > 31) + if (c > 31) { throw new CorruptObjectException(String.format( JGitText.get().corruptObjectNameContainsChar, Byte.valueOf(c))); + } throw new CorruptObjectException(String.format( JGitText.get().corruptObjectNameContainsByte, Integer.valueOf(c & 0xff))); @@ -578,8 +733,15 @@ public class ObjectChecker { return null; } - private boolean skip(@Nullable AnyObjectId id) { - return skipList != null && id != null && skipList.contains(id); + private void report(@NonNull ErrorType err, @Nullable AnyObjectId id, + String why) throws CorruptObjectException { + if (errors.contains(err) + && (id == null || skipList == null || !skipList.contains(id))) { + if (id != null) { + throw new CorruptObjectException(err, id, why); + } + throw new CorruptObjectException(why); + } } /** @@ -632,75 +794,82 @@ public class ObjectChecker { */ public void checkPathSegment(byte[] raw, int ptr, int end) throws CorruptObjectException { - int e = scanPathSegment(raw, ptr, end); + int e = scanPathSegment(raw, ptr, end, null); if (e < end && raw[e] == 0) throw new CorruptObjectException( JGitText.get().corruptObjectNameContainsNullByte); - checkPathSegment2(raw, ptr, end, false); + checkPathSegment2(raw, ptr, end, null); } - private void checkPathSegment2(byte[] raw, int ptr, int end, boolean skip) - throws CorruptObjectException { - if (ptr == end) - throw new CorruptObjectException( - JGitText.get().corruptObjectNameZeroLength); + private void checkPathSegment2(byte[] raw, int ptr, int end, + @Nullable AnyObjectId id) throws CorruptObjectException { + if (ptr == end) { + report(EMPTY_NAME, id, JGitText.get().corruptObjectNameZeroLength); + return; + } + if (raw[ptr] == '.') { switch (end - ptr) { case 1: - throw new CorruptObjectException( - JGitText.get().corruptObjectNameDot); + report(HAS_DOT, id, JGitText.get().corruptObjectNameDot); + break; case 2: - if (raw[ptr + 1] == '.') - throw new CorruptObjectException( + if (raw[ptr + 1] == '.') { + report(HAS_DOTDOT, id, JGitText.get().corruptObjectNameDotDot); + } break; case 4: - if (!skip && isGit(raw, ptr + 1)) - throw new CorruptObjectException(String.format( + if (isGit(raw, ptr + 1)) { + report(HAS_DOTGIT, id, String.format( JGitText.get().corruptObjectInvalidName, RawParseUtils.decode(raw, ptr, end))); + } break; default: - if (!skip && end - ptr > 4 - && isNormalizedGit(raw, ptr + 1, end)) - throw new CorruptObjectException(String.format( + if (end - ptr > 4 && isNormalizedGit(raw, ptr + 1, end)) { + report(HAS_DOTGIT, id, String.format( JGitText.get().corruptObjectInvalidName, RawParseUtils.decode(raw, ptr, end))); + } } - } else if (!skip && isGitTilde1(raw, ptr, end)) { - throw new CorruptObjectException(String.format( + } else if (isGitTilde1(raw, ptr, end)) { + report(HAS_DOTGIT, id, String.format( JGitText.get().corruptObjectInvalidName, RawParseUtils.decode(raw, ptr, end))); } - if (!skip) { - if (macosx && isMacHFSGit(raw, ptr, end)) - throw new CorruptObjectException(String.format( - JGitText.get().corruptObjectInvalidNameIgnorableUnicode, - RawParseUtils.decode(raw, ptr, end))); + if (macosx && isMacHFSGit(raw, ptr, end, id)) { + report(HAS_DOTGIT, id, String.format( + JGitText.get().corruptObjectInvalidNameIgnorableUnicode, + RawParseUtils.decode(raw, ptr, end))); + } - if (windows) { - // Windows ignores space and dot at end of file name. - if (raw[end - 1] == ' ' || raw[end - 1] == '.') - throw new CorruptObjectException(String.format( - JGitText.get().corruptObjectInvalidNameEnd, - Character.valueOf(((char) raw[end - 1])))); - if (end - ptr >= 3) - checkNotWindowsDevice(raw, ptr, end); + if (windows) { + // Windows ignores space and dot at end of file name. + if (raw[end - 1] == ' ' || raw[end - 1] == '.') { + report(WIN32_BAD_NAME, id, String.format( + JGitText.get().corruptObjectInvalidNameEnd, + Character.valueOf(((char) raw[end - 1])))); + } + if (end - ptr >= 3) { + checkNotWindowsDevice(raw, ptr, end, id); } } } // Mac's HFS+ folds permutations of ".git" and Unicode ignorable characters // to ".git" therefore we should prevent such names - private static boolean isMacHFSGit(byte[] raw, int ptr, int end) - throws CorruptObjectException { + private boolean isMacHFSGit(byte[] raw, int ptr, int end, + @Nullable AnyObjectId id) throws CorruptObjectException { boolean ignorable = false; byte[] git = new byte[] { '.', 'g', 'i', 't' }; int g = 0; while (ptr < end) { switch (raw[ptr]) { case (byte) 0xe2: // http://www.utf8-chartable.de/unicode-utf8-table.pl?start=8192 - checkTruncatedIgnorableUTF8(raw, ptr, end); + if (!checkTruncatedIgnorableUTF8(raw, ptr, end, id)) { + return false; + } switch (raw[ptr + 1]) { case (byte) 0x80: switch (raw[ptr + 2]) { @@ -737,7 +906,9 @@ public class ObjectChecker { return false; } case (byte) 0xef: // http://www.utf8-chartable.de/unicode-utf8-table.pl?start=65024 - checkTruncatedIgnorableUTF8(raw, ptr, end); + if (!checkTruncatedIgnorableUTF8(raw, ptr, end, id)) { + return false; + } // U+FEFF 0xefbbbf ZERO WIDTH NO-BREAK SPACE if ((raw[ptr + 1] == (byte) 0xbb) && (raw[ptr + 2] == (byte) 0xbf)) { @@ -758,12 +929,15 @@ public class ObjectChecker { return false; } - private static void checkTruncatedIgnorableUTF8(byte[] raw, int ptr, int end) - throws CorruptObjectException { - if ((ptr + 2) >= end) - throw new CorruptObjectException(MessageFormat.format( + private boolean checkTruncatedIgnorableUTF8(byte[] raw, int ptr, int end, + @Nullable AnyObjectId id) throws CorruptObjectException { + if ((ptr + 2) >= end) { + report(BAD_UTF8, id, MessageFormat.format( JGitText.get().corruptObjectInvalidNameInvalidUtf8, toHexString(raw, ptr, end))); + return false; + } + return true; } private static String toHexString(byte[] raw, int ptr, int end) { @@ -773,33 +947,36 @@ public class ObjectChecker { return b.toString(); } - private static void checkNotWindowsDevice(byte[] raw, int ptr, int end) - throws CorruptObjectException { + private void checkNotWindowsDevice(byte[] raw, int ptr, int end, + @Nullable AnyObjectId id) throws CorruptObjectException { switch (toLower(raw[ptr])) { case 'a': // AUX if (end - ptr >= 3 && toLower(raw[ptr + 1]) == 'u' && toLower(raw[ptr + 2]) == 'x' - && (end - ptr == 3 || raw[ptr + 3] == '.')) - throw new CorruptObjectException( + && (end - ptr == 3 || raw[ptr + 3] == '.')) { + report(WIN32_BAD_NAME, id, JGitText.get().corruptObjectInvalidNameAux); + } break; case 'c': // CON, COM[1-9] if (end - ptr >= 3 && toLower(raw[ptr + 2]) == 'n' && toLower(raw[ptr + 1]) == 'o' - && (end - ptr == 3 || raw[ptr + 3] == '.')) - throw new CorruptObjectException( + && (end - ptr == 3 || raw[ptr + 3] == '.')) { + report(WIN32_BAD_NAME, id, JGitText.get().corruptObjectInvalidNameCon); + } if (end - ptr >= 4 && toLower(raw[ptr + 2]) == 'm' && toLower(raw[ptr + 1]) == 'o' && isPositiveDigit(raw[ptr + 3]) - && (end - ptr == 4 || raw[ptr + 4] == '.')) - throw new CorruptObjectException(String.format( + && (end - ptr == 4 || raw[ptr + 4] == '.')) { + report(WIN32_BAD_NAME, id, String.format( JGitText.get().corruptObjectInvalidNameCom, Character.valueOf(((char) raw[ptr + 3])))); + } break; case 'l': // LPT[1-9] @@ -807,28 +984,31 @@ public class ObjectChecker { && toLower(raw[ptr + 1]) == 'p' && toLower(raw[ptr + 2]) == 't' && isPositiveDigit(raw[ptr + 3]) - && (end - ptr == 4 || raw[ptr + 4] == '.')) - throw new CorruptObjectException(String.format( + && (end - ptr == 4 || raw[ptr + 4] == '.')) { + report(WIN32_BAD_NAME, id, String.format( JGitText.get().corruptObjectInvalidNameLpt, Character.valueOf(((char) raw[ptr + 3])))); + } break; case 'n': // NUL if (end - ptr >= 3 && toLower(raw[ptr + 1]) == 'u' && toLower(raw[ptr + 2]) == 'l' - && (end - ptr == 3 || raw[ptr + 3] == '.')) - throw new CorruptObjectException( + && (end - ptr == 3 || raw[ptr + 3] == '.')) { + report(WIN32_BAD_NAME, id, JGitText.get().corruptObjectInvalidNameNul); + } break; case 'p': // PRN if (end - ptr >= 3 && toLower(raw[ptr + 1]) == 'r' && toLower(raw[ptr + 2]) == 'n' - && (end - ptr == 3 || raw[ptr + 3] == '.')) - throw new CorruptObjectException( + && (end - ptr == 3 || raw[ptr + 3] == '.')) { + report(WIN32_BAD_NAME, id, JGitText.get().corruptObjectInvalidNamePrn); + } break; } } @@ -881,6 +1061,15 @@ public class ObjectChecker { return false; } + private boolean match(byte[] b, byte[] src) { + int r = RawParseUtils.match(b, bufPtr.value, src); + if (r < 0) { + return false; + } + bufPtr.value = r; + return true; + } + private static char toLower(byte b) { if ('A' <= b && b <= 'Z') return (char) (b + ('a' - 'A')); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java index 42816bd689..b96fe885e1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java @@ -1051,6 +1051,9 @@ public abstract class PackParser { try { objCheck.check(id, type, data); } catch (CorruptObjectException e) { + if (e.getErrorType() != null) { + throw e; + } throw new CorruptObjectException(MessageFormat.format( JGitText.get().invalidObject, Constants.typeString(type), diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java index 2128f1f7e0..72c9c8b93e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java @@ -43,7 +43,11 @@ package org.eclipse.jgit.transport; +import static org.eclipse.jgit.util.StringUtils.equalsIgnoreCase; +import static org.eclipse.jgit.util.StringUtils.toLowerCase; + import java.io.File; +import java.util.EnumSet; import java.util.HashMap; import java.util.Map; @@ -62,6 +66,8 @@ import org.eclipse.jgit.util.SystemReader; * parameters. */ public class TransferConfig { + private static final String FSCK = "fsck"; //$NON-NLS-1$ + /** Key for {@link Config#get(SectionParser)}. */ public static final Config.SectionParser KEY = new SectionParser() { public TransferConfig parse(final Config cfg) { @@ -69,10 +75,14 @@ public class TransferConfig { } }; + enum FsckMode { + ERROR, WARN, IGNORE; + } + private final boolean fetchFsck; private final boolean receiveFsck; private final String fsckSkipList; - private final boolean allowLeadingZeroFileMode; + private final EnumSet ignore; private final boolean allowInvalidPersonIdent; private final boolean safeForWindows; private final boolean safeForMacOS; @@ -88,14 +98,44 @@ public class TransferConfig { boolean fsck = rc.getBoolean("transfer", "fsckobjects", false); //$NON-NLS-1$ //$NON-NLS-2$ fetchFsck = rc.getBoolean("fetch", "fsckobjects", fsck); //$NON-NLS-1$ //$NON-NLS-2$ receiveFsck = rc.getBoolean("receive", "fsckobjects", fsck); //$NON-NLS-1$ //$NON-NLS-2$ - fsckSkipList = rc.getString("fsck", null, "skipList"); //$NON-NLS-1$ //$NON-NLS-2$ - allowLeadingZeroFileMode = rc.getBoolean("fsck", "allowLeadingZeroFileMode", false); //$NON-NLS-1$ //$NON-NLS-2$ - allowInvalidPersonIdent = rc.getBoolean("fsck", "allowInvalidPersonIdent", false); //$NON-NLS-1$ //$NON-NLS-2$ - safeForWindows = rc.getBoolean("fsck", "safeForWindows", //$NON-NLS-1$ //$NON-NLS-2$ + fsckSkipList = rc.getString(FSCK, null, "skipList"); //$NON-NLS-1$ + allowInvalidPersonIdent = rc.getBoolean(FSCK, "allowInvalidPersonIdent", false); //$NON-NLS-1$ + safeForWindows = rc.getBoolean(FSCK, "safeForWindows", //$NON-NLS-1$ SystemReader.getInstance().isWindows()); - safeForMacOS = rc.getBoolean("fsck", "safeForMacOS", //$NON-NLS-1$ //$NON-NLS-2$ + safeForMacOS = rc.getBoolean(FSCK, "safeForMacOS", //$NON-NLS-1$ SystemReader.getInstance().isMacOS()); + ignore = EnumSet.noneOf(ObjectChecker.ErrorType.class); + EnumSet set = EnumSet + .noneOf(ObjectChecker.ErrorType.class); + for (String key : rc.getNames(FSCK)) { + if (equalsIgnoreCase(key, "skipList") //$NON-NLS-1$ + || equalsIgnoreCase(key, "allowLeadingZeroFileMode") //$NON-NLS-1$ + || equalsIgnoreCase(key, "allowInvalidPersonIdent") //$NON-NLS-1$ + || equalsIgnoreCase(key, "safeForWindows") //$NON-NLS-1$ + || equalsIgnoreCase(key, "safeForMacOS")) { //$NON-NLS-1$ + continue; + } + + ObjectChecker.ErrorType id = FsckKeyNameHolder.parse(key); + if (id != null) { + switch (rc.getEnum(FSCK, null, key, FsckMode.ERROR)) { + case ERROR: + ignore.remove(id); + break; + case WARN: + case IGNORE: + ignore.add(id); + break; + } + set.add(id); + } + } + if (!set.contains(ObjectChecker.ErrorType.ZERO_PADDED_FILEMODE) + && rc.getBoolean(FSCK, "allowLeadingZeroFileMode", false)) { //$NON-NLS-1$ + ignore.add(ObjectChecker.ErrorType.ZERO_PADDED_FILEMODE); + } + allowTipSha1InWant = rc.getBoolean( "uploadpack", "allowtipsha1inwant", false); //$NON-NLS-1$ //$NON-NLS-2$ allowReachableSha1InWant = rc.getBoolean( @@ -128,7 +168,7 @@ public class TransferConfig { return null; } return new ObjectChecker() - .setAllowLeadingZeroFileMode(allowLeadingZeroFileMode) + .setIgnore(ignore) .setAllowInvalidPersonIdent(allowInvalidPersonIdent) .setSafeForWindows(safeForWindows) .setSafeForMacOS(safeForMacOS) @@ -188,4 +228,34 @@ public class TransferConfig { } }; } + + static class FsckKeyNameHolder { + private static final Map errors; + + static { + errors = new HashMap<>(); + for (ObjectChecker.ErrorType m : ObjectChecker.ErrorType.values()) { + errors.put(keyNameFor(m.name()), m); + } + } + + @Nullable + static ObjectChecker.ErrorType parse(String key) { + return errors.get(toLowerCase(key)); + } + + private static String keyNameFor(String name) { + StringBuilder r = new StringBuilder(name.length()); + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (c != '_') { + r.append(c); + } + } + return toLowerCase(r.toString()); + } + + private FsckKeyNameHolder() { + } + } } -- 2.39.5