aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java210
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java151
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java305
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java51
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java9
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;
}
/**