diff options
5 files changed, 567 insertions, 159 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 380defaa08..3f58b7501f 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 @@ -49,6 +49,7 @@ import static java.lang.Long.valueOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; +import java.io.UnsupportedEncodingException; import java.text.MessageFormat; import org.eclipse.jgit.errors.CorruptObjectException; @@ -1035,6 +1036,14 @@ public class ObjectCheckerTest { } @Test + public void testValidPosixTree() throws CorruptObjectException { + checkOneName("a<b>c:d|e"); + checkOneName("test "); + checkOneName("test."); + checkOneName("NUL"); + } + + @Test public void testValidTreeSorting1() throws CorruptObjectException { final StringBuilder b = new StringBuilder(); entry(b, "100644 fooaaa"); @@ -1109,6 +1118,14 @@ public class ObjectCheckerTest { } @Test + public void testAcceptTreeModeWithZero() throws CorruptObjectException { + StringBuilder b = new StringBuilder(); + entry(b, "040000 a"); + checker.setAllowLeadingZeroFileMode(true); + checker.checkTree(Constants.encodeASCII(b.toString())); + } + + @Test public void testInvalidTreeModeStartsWithZero1() { final StringBuilder b = new StringBuilder(); entry(b, "0 a"); @@ -1265,6 +1282,47 @@ public class ObjectCheckerTest { } @Test + public void testInvalidTreeNameIsGit() { + 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 testInvalidTreeNameIsMixedCaseGitWindows() { + StringBuilder b = new StringBuilder(); + entry(b, "100644 .GiT"); + byte[] data = Constants.encodeASCII(b.toString()); + try { + checker.setSafeForWindows(true); + checker.checkTree(data); + fail("incorrectly accepted an invalid tree"); + } catch (CorruptObjectException e) { + assertEquals("invalid name '.GiT'", e.getMessage()); + } + } + + @Test + public void testInvalidTreeNameIsMixedCaseGitMacOS() { + StringBuilder b = new StringBuilder(); + entry(b, "100644 .GiT"); + byte[] data = Constants.encodeASCII(b.toString()); + try { + checker.setSafeForMacOS(true); + 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"); @@ -1392,6 +1450,158 @@ public class ObjectCheckerTest { } } + @Test + public void testInvalidTreeDuplicateNames5() + throws UnsupportedEncodingException { + StringBuilder b = new StringBuilder(); + entry(b, "100644 a"); + entry(b, "100644 A"); + byte[] data = b.toString().getBytes("UTF-8"); + try { + checker.setSafeForWindows(true); + checker.checkTree(data); + fail("incorrectly accepted an invalid tree"); + } catch (CorruptObjectException e) { + assertEquals("duplicate entry names", e.getMessage()); + } + } + + @Test + public void testInvalidTreeDuplicateNames6() + throws UnsupportedEncodingException { + StringBuilder b = new StringBuilder(); + entry(b, "100644 a"); + entry(b, "100644 A"); + byte[] data = b.toString().getBytes("UTF-8"); + try { + checker.setSafeForMacOS(true); + checker.checkTree(data); + fail("incorrectly accepted an invalid tree"); + } catch (CorruptObjectException e) { + assertEquals("duplicate entry names", e.getMessage()); + } + } + + @Test + public void testInvalidTreeDuplicateNames7() + throws UnsupportedEncodingException { + try { + Class.forName("java.text.Normalizer"); + } catch (ClassNotFoundException e) { + // Ignore this test on Java 5 platform. + return; + } + + StringBuilder b = new StringBuilder(); + entry(b, "100644 \u00C1"); + entry(b, "100644 \u004a\u0301"); + byte[] data = b.toString().getBytes("UTF-8"); + try { + checker.setSafeForMacOS(true); + checker.checkTree(data); + fail("incorrectly accepted an invalid tree"); + } catch (CorruptObjectException e) { + assertEquals("duplicate entry names", e.getMessage()); + } + } + + @Test + public void testRejectNulInPathSegment() { + try { + checker.checkPathSegment(Constants.encodeASCII("a\u0000b"), 0, 3); + fail("incorrectly accepted NUL in middle of name"); + } catch (CorruptObjectException e) { + assertEquals("name contains byte 0x00", e.getMessage()); + } + } + + @Test + public void testRejectSpaceAtEndOnWindows() { + checker.setSafeForWindows(true); + try { + checkOneName("test "); + fail("incorrectly accepted space at end"); + } catch (CorruptObjectException e) { + assertEquals("invalid name ends with ' '", e.getMessage()); + } + } + + @Test + public void testRejectDotAtEndOnWindows() { + checker.setSafeForWindows(true); + try { + checkOneName("test."); + fail("incorrectly accepted dot at end"); + } catch (CorruptObjectException e) { + assertEquals("invalid name ends with '.'", e.getMessage()); + } + } + + @Test + public void testRejectDevicesOnWindows() { + checker.setSafeForWindows(true); + + String[] bad = { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", + "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", + "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" }; + for (String b : bad) { + try { + checkOneName(b); + fail("incorrectly accepted " + b); + } catch (CorruptObjectException e) { + assertEquals("invalid name '" + b + "'", e.getMessage()); + } + try { + checkOneName(b + ".txt"); + fail("incorrectly accepted " + b + ".txt"); + } catch (CorruptObjectException e) { + assertEquals("invalid name '" + b + "'", e.getMessage()); + } + } + } + + @Test + public void testRejectInvalidWindowsCharacters() { + checker.setSafeForWindows(true); + rejectName('<'); + rejectName('>'); + rejectName(':'); + rejectName('"'); + rejectName('/'); + rejectName('\\'); + rejectName('|'); + rejectName('?'); + rejectName('*'); + + for (int i = 1; i <= 31; i++) + rejectName((byte) i); + } + + private void rejectName(char c) { + try { + checkOneName("te" + c + "st"); + fail("incorrectly accepted with " + c); + } catch (CorruptObjectException e) { + assertEquals("name contains '" + c + "'", e.getMessage()); + } + } + + private void rejectName(byte c) { + String h = Integer.toHexString(c); + try { + checkOneName("te" + ((char) c) + "st"); + fail("incorrectly accepted with 0x" + h); + } catch (CorruptObjectException e) { + assertEquals("name contains byte 0x" + h, e.getMessage()); + } + } + + private void checkOneName(String name) throws CorruptObjectException { + StringBuilder b = new StringBuilder(); + entry(b, "100644 " + name); + checker.checkTree(Constants.encodeASCII(b.toString())); + } + private static void entry(final StringBuilder b, final String modeName) { b.append(modeName); b.append('\0'); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java index 44b4276905..5275b4c898 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java @@ -62,6 +62,7 @@ import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.CoreConfig.AutoCRLF; import org.eclipse.jgit.lib.CoreConfig.SymLinks; import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectChecker; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectReader; @@ -1164,26 +1165,13 @@ public class DirCacheCheckout { entry.setLength((int) ol.getSize()); } - private static byte[][] forbidden; - static { - String[] list = getSortedForbiddenFileNames(); - forbidden = new byte[list.length][]; - for (int i = 0; i < list.length; ++i) - forbidden[i] = Constants.encodeASCII(list[i]); - } - - static String[] getSortedForbiddenFileNames() { - String[] list = new String[] { "AUX", "COM1", "COM2", "COM3", "COM4", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ - "COM5", "COM6", "COM7", "COM8", "COM9", "CON", "LPT1", "LPT2", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$ - "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "NUL", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$ - "PRN" }; //$NON-NLS-1$ - return list; - } - private static void checkValidPath(CanonicalTreeParser t) throws InvalidPathException { + ObjectChecker chk = new ObjectChecker() + .setSafeForWindows(SystemReader.getInstance().isWindows()) + .setSafeForMacOS(SystemReader.getInstance().isMacOS()); for (CanonicalTreeParser i = t; i != null; i = i.getParent()) - checkValidPathSegment(i); + checkValidPathSegment(chk, i); } /** @@ -1195,119 +1183,36 @@ public class DirCacheCheckout { * @since 3.3 */ public static void checkValidPath(String path) throws InvalidPathException { - boolean isWindows = SystemReader.getInstance().isWindows(); - boolean isOSX = SystemReader.getInstance().isMacOS(); - boolean ignCase = isOSX || isWindows; + ObjectChecker chk = new ObjectChecker() + .setSafeForWindows(SystemReader.getInstance().isWindows()) + .setSafeForMacOS(SystemReader.getInstance().isMacOS()); byte[] bytes = Constants.encode(path); int segmentStart = 0; - for (int i = 0; i < bytes.length; i++) { - if (bytes[i] == '/') { - checkValidPathSegment(isWindows, ignCase, bytes, segmentStart, - i, path); - segmentStart = i + 1; - } - } - if (segmentStart < bytes.length) - checkValidPathSegment(isWindows, ignCase, bytes, segmentStart, - bytes.length, path); - } - - private static void checkValidPathSegment(CanonicalTreeParser t) - throws InvalidPathException { - boolean isWindows = SystemReader.getInstance().isWindows(); - boolean isOSX = SystemReader.getInstance().isMacOS(); - boolean ignCase = isOSX || isWindows; - - int ptr = t.getNameOffset(); - byte[] raw = t.getEntryPathBuffer(); - int end = ptr + t.getNameLength(); - - checkValidPathSegment(isWindows, ignCase, raw, ptr, end, - t.getEntryPathString()); - } - - private static void checkValidPathSegment(boolean isWindows, - boolean ignCase, byte[] raw, int ptr, int end, String path) { - // Validate path component at this level of the tree - int start = ptr; - while (ptr < end) { - if (raw[ptr] == '/') - throw new InvalidPathException( - JGitText.get().invalidPathContainsSeparator, "/", path); //$NON-NLS-1$ - if (isWindows) { - if (raw[ptr] == '\\') - throw new InvalidPathException( - JGitText.get().invalidPathContainsSeparator, - "\\", path); //$NON-NLS-1$ - if (raw[ptr] == ':') - throw new InvalidPathException( - JGitText.get().invalidPathContainsSeparator, - ":", path); //$NON-NLS-1$ - } - ptr++; - } - // '.' and '..' are invalid here - if (ptr - start == 1) { - if (raw[start] == '.') - throw new InvalidPathException(path); - } else if (ptr - start == 2) { - if (raw[start] == '.') - if (raw[start + 1] == '.') - throw new InvalidPathException(path); - } else if (ptr - start == 4) { - // .git (possibly case insensitive) is disallowed - if (raw[start] == '.') - if (raw[start + 1] == 'g' || (ignCase && raw[start + 1] == 'G')) - if (raw[start + 2] == 'i' - || (ignCase && raw[start + 2] == 'I')) - if (raw[start + 3] == 't' - || (ignCase && raw[start + 3] == 'T')) - throw new InvalidPathException(path); - } - if (isWindows) { - // Space or period at end of file name is ignored by Windows. - // Treat this as a bad path for now. We may want to handle - // this as case insensitivity in the future. - if (ptr > 0) { - if (raw[ptr - 1] == '.') - throw new InvalidPathException( - JGitText.get().invalidPathPeriodAtEndWindows, path); - if (raw[ptr - 1] == ' ') - throw new InvalidPathException( - JGitText.get().invalidPathSpaceAtEndWindows, path); - } - - int i; - // Bad names, eliminate suffix first - for (i = start; i < ptr; ++i) - if (raw[i] == '.') - break; - int len = i - start; - if (len == 3 || len == 4) { - for (int j = 0; j < forbidden.length; ++j) { - if (forbidden[j].length == len) { - if (toUpper(raw[start]) < forbidden[j][0]) - break; - int k; - for (k = 0; k < len; ++k) { - if (toUpper(raw[start + k]) != forbidden[j][k]) - break; - } - if (k == len) - throw new InvalidPathException( - JGitText.get().invalidPathReservedOnWindows, - RawParseUtils.decode(forbidden[j]), path); - } + try { + for (int i = 0; i < bytes.length; i++) { + if (bytes[i] == '/') { + chk.checkPathSegment(bytes, segmentStart, i); + segmentStart = i + 1; } } + chk.checkPathSegment(bytes, segmentStart, bytes.length); + } catch (CorruptObjectException e) { + throw new InvalidPathException(e.getMessage()); } } - private static byte toUpper(byte b) { - if (b >= 'a' && b <= 'z') - return (byte) (b - ('a' - 'A')); - return b; + private static void checkValidPathSegment(ObjectChecker chk, + CanonicalTreeParser t) throws InvalidPathException { + try { + int ptr = t.getNameOffset(); + int end = ptr + t.getNameLength(); + chk.checkPathSegment(t.getEntryPathBuffer(), ptr, end); + } catch (CorruptObjectException err) { + String path = t.getEntryPathString(); + InvalidPathException i = new InvalidPathException(path); + i.initCause(err); + throw i; + } } - } 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 bb67befae1..1b135a924c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java @@ -48,11 +48,17 @@ import static org.eclipse.jgit.util.RawParseUtils.match; import static org.eclipse.jgit.util.RawParseUtils.nextLF; import static org.eclipse.jgit.util.RawParseUtils.parseBase10; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.text.MessageFormat; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.util.MutableInteger; +import org.eclipse.jgit.util.RawParseUtils; /** * Verifies that an object is formatted correctly. @@ -97,6 +103,56 @@ public class ObjectChecker { private final MutableInteger ptrout = new MutableInteger(); + private boolean allowZeroMode; + private boolean windows; + private boolean macosx; + + /** + * Enable accepting leading zero mode in tree entries. + * <p> + * Some broken Git libraries generated leading zeros in the mode part of + * 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. + * + * @param allow allow leading zero mode. + * @return {@code this}. + * @since 3.4 + */ + public ObjectChecker setAllowLeadingZeroFileMode(boolean allow) { + allowZeroMode = allow; + return this; + } + + /** + * Restrict trees to only names legal on Windows platforms. + * <p> + * Also rejects any mixed case forms of reserved names ({@code .git}). + * + * @param win true if Windows name checking should be performed. + * @return {@code this}. + * @since 3.4 + */ + public ObjectChecker setSafeForWindows(boolean win) { + windows = win; + return this; + } + + /** + * Restrict trees to only names legal on Mac OS X platforms. + * <p> + * Rejects any mixed case forms of reserved names ({@code .git}) + * for users working on HFS+ in case-insensitive (default) mode. + * + * @param mac true if Mac OS X name checking should be performed. + * @return {@code this}. + * @since 3.4 + */ + public ObjectChecker setSafeForMacOS(boolean mac) { + macosx = mac; + return this; + } + /** * Check an object for parsing errors. * @@ -297,6 +353,9 @@ public class ObjectChecker { final int sz = raw.length; int ptr = 0; int lastNameB = 0, lastNameE = 0, lastMode = 0; + Set<String> normalized = windows || macosx + ? new HashSet<String>() + : null; while (ptr < sz) { int thisMode = 0; @@ -308,7 +367,7 @@ public class ObjectChecker { break; if (c < '0' || c > '7') throw new CorruptObjectException("invalid mode character"); - if (thisMode == 0 && c == '0') + if (thisMode == 0 && c == '0' && !allowZeroMode) throw new CorruptObjectException("mode starts with '0'"); thisMode <<= 3; thisMode += c - '0'; @@ -318,44 +377,187 @@ public class ObjectChecker { throw new CorruptObjectException("invalid mode " + thisMode); final int thisNameB = ptr; - for (;;) { - if (ptr == sz) - throw new CorruptObjectException("truncated in name"); - final byte c = raw[ptr++]; - if (c == 0) - break; - if (c == '/') - throw new CorruptObjectException("name contains '/'"); - } - if (thisNameB + 1 == ptr) - throw new CorruptObjectException("zero length name"); - if (raw[thisNameB] == '.') { - final int nameLen = (ptr - 1) - thisNameB; - if (nameLen == 1) - throw new CorruptObjectException("invalid name '.'"); - if (nameLen == 2 && raw[thisNameB + 1] == '.') - throw new CorruptObjectException("invalid name '..'"); - } - if (duplicateName(raw, thisNameB, ptr - 1)) + ptr = scanPathSegment(raw, ptr, sz); + if (ptr == sz || raw[ptr] != 0) + throw new CorruptObjectException("truncated in name"); + checkPathSegment2(raw, thisNameB, ptr); + if (normalized != null) { + if (normalized.add(normalize(raw, thisNameB, ptr))) + throw new CorruptObjectException("duplicate entry names"); + } else if (duplicateName(raw, thisNameB, ptr)) throw new CorruptObjectException("duplicate entry names"); if (lastNameB != 0) { final int cmp = pathCompare(raw, lastNameB, lastNameE, - lastMode, thisNameB, ptr - 1, thisMode); + lastMode, thisNameB, ptr, thisMode); if (cmp > 0) throw new CorruptObjectException("incorrectly sorted"); } lastNameB = thisNameB; - lastNameE = ptr - 1; + lastNameE = ptr; lastMode = thisMode; - ptr += Constants.OBJECT_ID_LENGTH; + ptr += 1 + Constants.OBJECT_ID_LENGTH; if (ptr > sz) throw new CorruptObjectException("truncated in object id"); } } + private int scanPathSegment(byte[] raw, int ptr, int end) + throws CorruptObjectException { + for (; ptr < end; ptr++) { + byte c = raw[ptr]; + if (c == 0) + return ptr; + if (c == '/') + throw new CorruptObjectException("name contains '/'"); + if (windows && isInvalidOnWindows(c)) { + if (c > 31) + throw new CorruptObjectException(String.format( + "name contains '%c'", c)); + throw new CorruptObjectException(String.format( + "name contains byte 0x%x", c & 0xff)); + } + } + return ptr; + } + + /** + * Check tree path entry for validity. + * + * @param raw buffer to scan. + * @param ptr offset to first byte of the name. + * @param end offset to one past last byte of name. + * @throws CorruptObjectException name is invalid. + * @since 3.4 + */ + public void checkPathSegment(byte[] raw, int ptr, int end) + throws CorruptObjectException { + int e = scanPathSegment(raw, ptr, end); + if (e < end && raw[e] == 0) + throw new CorruptObjectException("name contains byte 0x00"); + checkPathSegment2(raw, ptr, end); + } + + private void checkPathSegment2(byte[] raw, int ptr, int end) + throws CorruptObjectException { + if (ptr == end) + throw new CorruptObjectException("zero length name"); + if (raw[ptr] == '.') { + switch (end - ptr) { + case 1: + throw new CorruptObjectException("invalid name '.'"); + case 2: + if (raw[ptr + 1] == '.') + throw new CorruptObjectException("invalid name '..'"); + break; + case 4: + if (isDotGit(raw, ptr + 1)) + throw new CorruptObjectException(String.format( + "invalid name '%s'", + 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("invalid name ends with '" + + ((char) raw[end - 1]) + "'"); + if (end - ptr >= 3) + checkNotWindowsDevice(raw, ptr, end); + } + } + + private static void checkNotWindowsDevice(byte[] raw, int ptr, int end) + 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("invalid name 'AUX'"); + 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("invalid name 'CON'"); + 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("invalid name 'COM" + + ((char) raw[ptr + 3]) + "'"); + break; + + case 'l': // LPT[1-9] + if (end - ptr >= 4 + && toLower(raw[ptr + 1]) == 'p' + && toLower(raw[ptr + 2]) == 't' + && isPositiveDigit(raw[ptr + 3]) + && (end - ptr == 4 || raw[ptr + 4] == '.')) + throw new CorruptObjectException("invalid name 'LPT" + + ((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("invalid name 'NUL'"); + 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("invalid name 'PRN'"); + break; + } + } + + private static boolean isInvalidOnWindows(byte c) { + // Windows disallows "special" characters in a path component. + switch (c) { + case '"': + case '*': + case ':': + case '<': + case '>': + case '?': + case '\\': + case '|': + return true; + } + return 1 <= c && c <= 31; + } + + private boolean isDotGit(byte[] buf, int p) { + if (windows || macosx) + return toLower(buf[p]) == 'g' + && toLower(buf[p + 1]) == 'i' + && toLower(buf[p + 2]) == 't'; + return buf[p] == 'g' && buf[p + 1] == 'i' && buf[p + 2] == 't'; + } + + private static char toLower(byte b) { + if ('A' <= b && b <= 'Z') + return (char) (b + ('a' - 'A')); + return (char) b; + } + + private static boolean isPositiveDigit(byte b) { + return '1' <= b && b <= '9'; + } + /** * Check a blob for errors. * @@ -367,4 +569,61 @@ public class ObjectChecker { public void checkBlob(final byte[] raw) throws CorruptObjectException { // We can always assume the blob is valid. } + + private String normalize(byte[] raw, int ptr, int end) { + String n = RawParseUtils.decode(raw, ptr, end).toLowerCase(Locale.US); + return macosx ? Normalizer.normalize(n) : n; + } + + private static class Normalizer { + // TODO Simplify invocation to Normalizer after dropping Java 5. + private static final Method normalize; + private static final Object nfc; + static { + Method method; + Object formNfc; + try { + Class<?> formClazz = Class.forName("java.text.Normalizer$Form"); //$NON-NLS-1$ + formNfc = formClazz.getField("NFC").get(null); //$NON-NLS-1$ + method = Class.forName("java.text.Normalizer") //$NON-NLS-1$ + .getMethod("normalize", CharSequence.class, formClazz); //$NON-NLS-1$ + } catch (ClassNotFoundException e) { + method = null; + formNfc = null; + } catch (NoSuchFieldException e) { + method = null; + formNfc = null; + } catch (NoSuchMethodException e) { + method = null; + formNfc = null; + } catch (SecurityException e) { + method = null; + formNfc = null; + } catch (IllegalArgumentException e) { + method = null; + formNfc = null; + } catch (IllegalAccessException e) { + method = null; + formNfc = null; + } + normalize = method; + nfc = formNfc; + } + + static String normalize(String in) { + if (normalize == null) + return in; + try { + return (String) normalize.invoke(null, in, nfc); + } catch (IllegalAccessException e) { + return in; + } catch (InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException) + throw (RuntimeException) e.getCause(); + if (e.getCause() instanceof Error) + throw (Error) e.getCause(); + return in; + } + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java index 68b3262a0b..ff45b1cda3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java @@ -74,6 +74,7 @@ import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Config.SectionParser; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectChecker; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdSubclassMap; import org.eclipse.jgit.lib.ObjectInserter; @@ -160,7 +161,7 @@ public abstract class BaseReceivePack { private boolean expectDataAfterPackFooter; /** Should an incoming transfer validate objects? */ - private boolean checkReceivedObjects; + private ObjectChecker objectChecker; /** Should an incoming transfer permit create requests? */ private boolean allowCreates; @@ -254,7 +255,7 @@ public abstract class BaseReceivePack { walk = new RevWalk(db); final ReceiveConfig cfg = db.getConfig().get(ReceiveConfig.KEY); - checkReceivedObjects = cfg.checkReceivedObjects; + objectChecker = cfg.newObjectChecker(); allowCreates = cfg.allowCreates; allowDeletes = cfg.allowDeletes; allowNonFastForwards = cfg.allowNonFastForwards; @@ -273,18 +274,26 @@ public abstract class BaseReceivePack { }; final boolean checkReceivedObjects; + final boolean allowLeadingZeroFileMode; + final boolean safeForWindows; + final boolean safeForMacOS; final boolean allowCreates; - final boolean allowDeletes; - final boolean allowNonFastForwards; - final boolean allowOfsDelta; ReceiveConfig(final Config config) { - checkReceivedObjects = config.getBoolean("receive", "fsckobjects", //$NON-NLS-1$ //$NON-NLS-2$ - false); + checkReceivedObjects = config.getBoolean( + "receive", "fsckobjects", //$NON-NLS-1$ //$NON-NLS-2$ + config.getBoolean("transfer", "fsckobjects", false)); //$NON-NLS-1$ //$NON-NLS-2$ + allowLeadingZeroFileMode = checkReceivedObjects + && config.getBoolean("fsck", "allowLeadingZeroFileMode", false); //$NON-NLS-1$ //$NON-NLS-2$ + safeForWindows = checkReceivedObjects + && config.getBoolean("fsck", "safeForWindows", false); //$NON-NLS-1$ //$NON-NLS-2$ + safeForMacOS = checkReceivedObjects + && config.getBoolean("fsck", "safeForMacOS", false); //$NON-NLS-1$ //$NON-NLS-2$ + allowCreates = true; allowDeletes = !config.getBoolean("receive", "denydeletes", false); //$NON-NLS-1$ //$NON-NLS-2$ allowNonFastForwards = !config.getBoolean("receive", //$NON-NLS-1$ @@ -292,6 +301,15 @@ public abstract class BaseReceivePack { allowOfsDelta = config.getBoolean("repack", "usedeltabaseoffset", //$NON-NLS-1$ //$NON-NLS-2$ true); } + + ObjectChecker newObjectChecker() { + if (!checkReceivedObjects) + return null; + return new ObjectChecker() + .setAllowLeadingZeroFileMode(allowLeadingZeroFileMode) + .setSafeForWindows(safeForWindows) + .setSafeForMacOS(safeForMacOS); + } } /** @@ -481,16 +499,29 @@ public abstract class BaseReceivePack { * of the connection. */ public boolean isCheckReceivedObjects() { - return checkReceivedObjects; + return objectChecker != null; } /** * @param check * true to enable checking received objects; false to assume all * received objects are valid. + * @see #setObjectChecker(ObjectChecker) */ public void setCheckReceivedObjects(final boolean check) { - checkReceivedObjects = check; + if (check && objectChecker == null) + setObjectChecker(new ObjectChecker()); + else if (!check && objectChecker != null) + setObjectChecker(null); + } + + /** + * @param impl if non-null the object checking instance to verify each + * received object with; null to disable object checking. + * @since 3.4 + */ + public void setObjectChecker(ObjectChecker impl) { + objectChecker = impl; } /** @return true if the client can request refs to be created. */ @@ -983,7 +1014,7 @@ public abstract class BaseReceivePack { parser.setCheckEofAfterPackFooter(!biDirectionalPipe && !isExpectDataAfterPackFooter()); parser.setExpectDataAfterPackFooter(isExpectDataAfterPackFooter()); - parser.setObjectChecking(isCheckReceivedObjects()); + parser.setObjectChecker(objectChecker); parser.setLockMessage(lockMsg); parser.setMaxObjectSizeLimit(maxObjectSizeLimit); packLock = parser.parse(receiving, resolving); 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 3a08cd35df..b00d607eee 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java @@ -63,7 +63,7 @@ public class TransferConfig { } }; - private final boolean fsckObjects; + private final boolean fetchFsck; private final boolean allowTipSha1InWant; private final String[] hideRefs; @@ -72,7 +72,10 @@ public class TransferConfig { } private TransferConfig(final Config rc) { - fsckObjects = rc.getBoolean("receive", "fsckobjects", false); //$NON-NLS-1$ //$NON-NLS-2$ + fetchFsck = rc.getBoolean( + "fetch", "fsckobjects", //$NON-NLS-1$ //$NON-NLS-2$ + rc.getBoolean("transfer", "fsckobjects", false)); //$NON-NLS-1$ //$NON-NLS-2$ + allowTipSha1InWant = rc.getBoolean( "uploadpack", "allowtipsha1inwant", false); //$NON-NLS-1$ //$NON-NLS-2$ hideRefs = rc.getStringList("uploadpack", null, "hiderefs"); //$NON-NLS-1$ //$NON-NLS-2$ @@ -82,7 +85,7 @@ public class TransferConfig { * @return strictly verify received objects? */ public boolean isFsckObjects() { - return fsckObjects; + return fetchFsck; } /** |