]> source.dussan.org Git - jgit.git/commitdiff
Check for duplicate names after folding case in ObjectChecker 80/23280/2
authorShawn Pearce <spearce@spearce.org>
Wed, 12 Mar 2014 20:59:29 +0000 (13:59 -0700)
committerShawn Pearce <spearce@spearce.org>
Wed, 12 Mar 2014 23:06:10 +0000 (16:06 -0700)
Mac OS X and Windows filesystems are generally case insensitive and
will fold 'a' and 'A' to the same directory entry. If the checker is
enforcing safe semantics for these platforms, track all names and
look for duplicates after folding case and normalizing to NFC.

Change-Id: I170b6f649a72d6ef322b7254943d4c604a8d25b9

org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java

index 06745650ca8250be4a055f8c71d2518f1c5d1313..3f58b7501f2438b04fea777db835d343f1e9c092 100644 (file)
@@ -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;
@@ -1449,6 +1450,61 @@ 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 {
index 38fd0a1eb6b23a9c2e0a0353912670d4f3393de1..1b135a924c62865a46c5225905b95864bec91c2b 100644 (file)
@@ -48,7 +48,12 @@ 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;
@@ -348,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;
@@ -373,7 +381,10 @@ public class ObjectChecker {
                        if (ptr == sz || raw[ptr] != 0)
                                throw new CorruptObjectException("truncated in name");
                        checkPathSegment2(raw, thisNameB, ptr);
-                       if (duplicateName(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) {
@@ -558,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;
+                       }
+               }
+       }
 }