]> source.dussan.org Git - jgit.git/commitdiff
Validate paths during DirCheckout 17/4617/16
authorRobin Rosenberg <robin.rosenberg@dewire.com>
Mon, 23 Apr 2012 20:37:50 +0000 (22:37 +0200)
committerMatthias Sohn <matthias.sohn@sap.com>
Mon, 23 Apr 2012 20:37:50 +0000 (22:37 +0200)
DirCacheCheckout and CanonicalTreeParser cooperate. CanonicalTreeParser
can detect malformed, potentially malicious tree entries and sets a
flag, while DirCacheCheckout refuses to work with such paths.

Malicious tree entries are ".", "..", ".git" (case insensitive), any
name containing '/' and (on Windows '\') and also (on Windows)
any paths ending in a combination of '.' or space or containing a ':'.
We also forbid all special names like "con" etc on Windows.

Some of the test can execute on any platform by enabling partial
platform emulation.

A new runtime exception, InvalidPathException, is introduced. For
backwards compatibility it extends InvalidArgumentException.

Change-Id: I86199105814b63d4340e5de0e471d0da6b579ead
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java
org.eclipse.jgit.test/META-INF/MANIFEST.MF
org.eclipse.jgit.test/pom.xml
org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutMaliciousPathTest.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
org.eclipse.jgit/src/org/eclipse/jgit/dircache/InvalidPathException.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java
org.eclipse.jgit/src/org/eclipse/jgit/treewalk/CanonicalTreeParser.java

index 23bf5632cce7a40a379f19fcb4ca347007753bdd..deca341067459c01a0ab90f0e27178534277fdb9 100644 (file)
@@ -92,6 +92,7 @@ public class MockSystemReader extends SystemReader {
                init(Constants.GIT_COMMITTER_EMAIL_KEY);
                userGitConfig = new MockConfig(null, null);
                systemGitConfig = new MockConfig(null, null);
+               setCurrentPlatform();
        }
 
        private void init(final String n) {
index 05b524d3812cbb3c54506561ed9f76bc39a87506..5db483b1fa84970d3ca731203bbe67395ddca581 100644 (file)
@@ -40,6 +40,7 @@ Import-Package: org.eclipse.jgit.api;version="[2.0.0,2.1.0)",
  org.eclipse.jgit.util;version="[2.0.0,2.1.0)",
  org.eclipse.jgit.util.io;version="[2.0.0,2.1.0)",
  org.hamcrest;version="[1.1.0,2.0.0)",
+ org.hamcrest.text.pattern;version="[1.1.0,2.0.0)",
  org.junit;version="[4.4.0,5.0.0)",
  org.junit.experimental.theories;version="[4.4.0,5.0.0)",
  org.junit.runner;version="[4.4.0,5.0.0)"
index 6d5f38acb77ef2ca2f8c475bc1a965eb0e360dc6..969e205b0b758ba66252097391d36c671f5f18f7 100644 (file)
       <scope>test</scope>
     </dependency>
 
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-library</artifactId>
+      <scope>test</scope>
+      <version>[1.1.0,2.0.0)</version>
+    </dependency>
+
     <dependency>
       <groupId>org.eclipse.jgit</groupId>
       <artifactId>org.eclipse.jgit</artifactId>
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutMaliciousPathTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutMaliciousPathTest.java
new file mode 100644 (file)
index 0000000..65d0418
--- /dev/null
@@ -0,0 +1,420 @@
+/*
+ * Copyright (C) 2011, Robin Rosenberg <robin.rosenberg@dewire.com>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v1.0 which accompanies this
+ * distribution, is reproduced below, and is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.lib;
+
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.CheckoutConflictException;
+import org.eclipse.jgit.api.errors.InvalidRefNameException;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
+import org.eclipse.jgit.api.errors.RefNotFoundException;
+import org.eclipse.jgit.dircache.InvalidPathException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.junit.MockSystemReader;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.SystemReader;
+import org.junit.Test;
+
+public class DirCacheCheckoutMaliciousPathTest extends RepositoryTestCase {
+       protected ObjectId theHead;
+       protected ObjectId theMerge;
+
+       @Test
+       public void testMaliciousAbsolutePathIsOk() throws Exception {
+               testMaliciousPathGoodFirstCheckout("ok");
+       }
+
+       @Test
+       public void testMaliciousAbsolutePathIsOkSecondCheckout() throws Exception {
+               testMaliciousPathGoodSecondCheckout("ok");
+       }
+
+       @Test
+       public void testMaliciousAbsolutePathIsOkTwoLevels() throws Exception {
+               testMaliciousPathGoodSecondCheckout("a", "ok");
+       }
+
+       @Test
+       public void testMaliciousAbsolutePath() throws Exception {
+               testMaliciousPathBadFirstCheckout("/tmp/x");
+       }
+
+       @Test
+       public void testMaliciousAbsolutePathSecondCheckout() throws Exception {
+               testMaliciousPathBadSecondCheckout("/tmp/x");
+       }
+
+       @Test
+       public void testMaliciousAbsolutePathTwoLevelsFirstBad() throws Exception {
+               testMaliciousPathBadFirstCheckout("/tmp/x", "y");
+       }
+
+       @Test
+       public void testMaliciousAbsolutePathTwoLevelsSecondBad() throws Exception {
+               testMaliciousPathBadFirstCheckout("y", "/tmp/x");
+       }
+
+       @Test
+       public void testMaliciousAbsoluteCurDrivePathWindows() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setWindows();
+               testMaliciousPathBadFirstCheckout("\\somepath");
+       }
+
+       @Test
+       public void testMaliciousAbsoluteCurDrivePathWindowsOnUnix()
+                       throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setUnix();
+               testMaliciousPathGoodFirstCheckout("\\somepath");
+       }
+
+       @Test
+       public void testMaliciousAbsoluteUNCPathWindows1() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setWindows();
+               testMaliciousPathBadFirstCheckout("\\\\somepath");
+       }
+
+       @Test
+       public void testMaliciousAbsoluteUNCPathWindows1OnUnix() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setUnix();
+               testMaliciousPathGoodFirstCheckout("\\\\somepath");
+       }
+
+       @Test
+       public void testMaliciousAbsoluteUNCPathWindows2() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setWindows();
+               testMaliciousPathBadFirstCheckout("\\/somepath");
+       }
+
+       @Test
+       public void testMaliciousAbsoluteUNCPathWindows2OnUnix() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setUnix();
+               testMaliciousPathBadFirstCheckout("\\/somepath");
+       }
+
+       @Test
+       public void testMaliciousAbsoluteWindowsPath1() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setWindows();
+               testMaliciousPathBadFirstCheckout("c:\\temp\\x");
+       }
+
+       @Test
+       public void testMaliciousAbsoluteWindowsPath1OnUnix() throws Exception {
+               if (File.separatorChar == '\\')
+                       return; // cannot emulate Unix on Windows for this test
+               ((MockSystemReader) SystemReader.getInstance()).setUnix();
+               testMaliciousPathGoodFirstCheckout("c:\\temp\\x");
+       }
+
+       @Test
+       public void testMaliciousAbsoluteWindowsPath2() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setCurrentPlatform();
+               testMaliciousPathBadFirstCheckout("c:/temp/x");
+       }
+
+       @Test
+       public void testMaliciousGitPath1() throws Exception {
+               testMaliciousPathBadFirstCheckout(".git/konfig");
+       }
+
+       @Test
+       public void testMaliciousGitPath2() throws Exception {
+               testMaliciousPathBadFirstCheckout(".git", "konfig");
+       }
+
+       @Test
+       public void testMaliciousGitPath1Case() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setWindows(); // or OS X
+               testMaliciousPathBadFirstCheckout(".Git/konfig");
+       }
+
+       @Test
+       public void testMaliciousGitPath2Case() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setWindows(); // or OS X
+               testMaliciousPathBadFirstCheckout(".gIt", "konfig");
+       }
+
+       @Test
+       public void testMaliciousGitPath3Case() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setWindows(); // or OS X
+               testMaliciousPathBadFirstCheckout(".giT", "konfig");
+       }
+
+       @Test
+       public void testMaliciousGitPathEndSpaceWindows() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setWindows();
+               testMaliciousPathBadFirstCheckout(".git ", "konfig");
+       }
+
+       @Test
+       public void testMaliciousGitPathEndSpaceUnixOk() throws Exception {
+               if (File.separatorChar == '\\')
+                       return; // cannot emulate Unix on Windows for this test
+               ((MockSystemReader) SystemReader.getInstance()).setUnix();
+               testMaliciousPathGoodFirstCheckout(".git ", "konfig");
+       }
+
+       @Test
+       public void testMaliciousGitPathEndDotWindows1() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setWindows();
+               testMaliciousPathBadFirstCheckout(".git.", "konfig");
+       }
+
+       @Test
+       public void testMaliciousGitPathEndDotWindows2() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setWindows();
+               testMaliciousPathBadFirstCheckout(".f.");
+       }
+
+       @Test
+       public void testMaliciousGitPathEndDotWindows3() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setWindows();
+               testMaliciousPathGoodFirstCheckout(".f");
+       }
+
+       @Test
+       public void testMaliciousGitPathEndDotUnixOk() throws Exception {
+               if (File.separatorChar == '\\')
+                       return; // cannot emulate Unix on Windows for this test
+               ((MockSystemReader) SystemReader.getInstance()).setUnix();
+               testMaliciousPathGoodFirstCheckout(".git.", "konfig");
+       }
+
+       @Test
+       public void testMaliciousPathDotDot() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setCurrentPlatform();
+               testMaliciousPathBadFirstCheckout("..", "no");
+       }
+
+       @Test
+       public void testMaliciousPathDot() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setCurrentPlatform();
+               testMaliciousPathBadFirstCheckout(".", "no");
+       }
+
+       @Test
+       public void testMaliciousPathEmpty() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setCurrentPlatform();
+               testMaliciousPathBadFirstCheckout("", "no");
+       }
+
+       @Test
+       public void testMaliciousWindowsADS() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setWindows();
+               testMaliciousPathBadFirstCheckout("some:path");
+       }
+
+       @Test
+       public void testMaliciousWindowsADSOnUnix() throws Exception {
+               if (File.separatorChar == '\\')
+                       return; // cannot emulate Unix on Windows for this test
+               ((MockSystemReader) SystemReader.getInstance()).setUnix();
+               testMaliciousPathGoodFirstCheckout("some:path");
+       }
+
+       @Test
+       public void testForbiddenNamesOnWindowsEgCon() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setWindows();
+               testMaliciousPathBadFirstCheckout("con");
+       }
+
+       @Test
+       public void testForbiddenNamesOnWindowsEgConDotSuffix() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setWindows();
+               testMaliciousPathBadFirstCheckout("con.txt");
+       }
+
+       @Test
+       public void testForbiddenNamesOnWindowsEgLpt1() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setWindows();
+               testMaliciousPathBadFirstCheckout("lpt1");
+       }
+
+       @Test
+       public void testForbiddenNamesOnWindowsEgLpt1DotSuffix() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setWindows();
+               testMaliciousPathBadFirstCheckout("lpt1.txt");
+       }
+
+       @Test
+       public void testForbiddenNamesOnWindowsEgDotCon() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setWindows();
+               testMaliciousPathGoodFirstCheckout(".con");
+       }
+
+       @Test
+       public void testForbiddenNamesOnWindowsEgLpr() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setWindows();
+               testMaliciousPathGoodFirstCheckout("lpt"); // good name
+       }
+
+       @Test
+       public void testForbiddenNamesOnWindowsEgCon1() throws Exception {
+               ((MockSystemReader) SystemReader.getInstance()).setWindows();
+               testMaliciousPathGoodFirstCheckout("con1"); // good name
+       }
+
+       @Test
+       public void testForbiddenWindowsNamesOnUnixEgCon() throws Exception {
+               if (File.separatorChar == '\\')
+                       return; // cannot emulate Unix on Windows for this test
+               testMaliciousPathGoodFirstCheckout("con");
+       }
+
+       @Test
+       public void testForbiddenWindowsNamesOnUnixEgLpt1() throws Exception {
+               if (File.separatorChar == '\\')
+                       return; // cannot emulate Unix on Windows for this test
+               testMaliciousPathGoodFirstCheckout("lpt1");
+       }
+
+       private void testMaliciousPathBadFirstCheckout(String... paths)
+                       throws Exception {
+               testMaliciousPath(false, false, paths);
+       }
+
+       private void testMaliciousPathBadSecondCheckout(String... paths) throws Exception {
+               testMaliciousPath(false, true, paths);
+       }
+
+       private void testMaliciousPathGoodFirstCheckout(String... paths)
+                       throws Exception {
+               testMaliciousPath(true, false, paths);
+       }
+
+       private void testMaliciousPathGoodSecondCheckout(String... paths) throws Exception {
+               testMaliciousPath(true, true, paths);
+       }
+
+       /**
+        * Create a bad tree and tries to check it out
+        *
+        * @param good
+        *            true if we expect this to pass
+        * @param secondCheckout
+        *            perform the actual test on the second checkout
+        * @param path
+        *            to the blob, one or more levels
+        * @throws IOException
+        * @throws RefAlreadyExistsException
+        * @throws RefNotFoundException
+        * @throws InvalidRefNameException
+        * @throws MissingObjectException
+        * @throws IncorrectObjectTypeException
+        * @throws CheckoutConflictException
+        * @throws JGitInternalException
+        */
+       private void testMaliciousPath(boolean good, boolean secondCheckout, String... path)
+                       throws IOException, RefAlreadyExistsException,
+                       RefNotFoundException, InvalidRefNameException,
+                       MissingObjectException, IncorrectObjectTypeException,
+                       JGitInternalException, CheckoutConflictException {
+               Git git = new Git(db);
+               ObjectInserter newObjectInserter;
+               newObjectInserter = git.getRepository().newObjectInserter();
+               ObjectId blobId = newObjectInserter.insert(Constants.OBJ_BLOB,
+                               "data".getBytes());
+               newObjectInserter = git.getRepository().newObjectInserter();
+               FileMode mode = FileMode.REGULAR_FILE;
+               ObjectId insertId = blobId;
+               for (int i = path.length - 1; i >= 0; --i) {
+                       TreeFormatter treeFormatter = new TreeFormatter();
+                       treeFormatter.append("goodpath", mode, insertId);
+                       insertId = newObjectInserter.insert(treeFormatter);
+                       mode = FileMode.TREE;
+               }
+               newObjectInserter = git.getRepository().newObjectInserter();
+               CommitBuilder commitBuilder = new CommitBuilder();
+               commitBuilder.setAuthor(author);
+               commitBuilder.setCommitter(committer);
+               commitBuilder.setMessage("foo#1");
+               commitBuilder.setTreeId(insertId);
+               ObjectId firstCommitId = newObjectInserter.insert(commitBuilder);
+
+               newObjectInserter = git.getRepository().newObjectInserter();
+               mode = FileMode.REGULAR_FILE;
+               insertId = blobId;
+               for (int i = path.length - 1; i >= 0; --i) {
+                       TreeFormatter treeFormatter = new TreeFormatter();
+                       treeFormatter.append(path[i], mode, insertId);
+                       insertId = newObjectInserter.insert(treeFormatter);
+                       mode = FileMode.TREE;
+               }
+
+               // Create another commit
+               commitBuilder = new CommitBuilder();
+               commitBuilder.setAuthor(author);
+               commitBuilder.setCommitter(committer);
+               commitBuilder.setMessage("foo#2");
+               commitBuilder.setTreeId(insertId);
+               commitBuilder.setParentId(firstCommitId);
+               ObjectId commitId = newObjectInserter.insert(commitBuilder);
+
+               RevWalk revWalk = new RevWalk(git.getRepository());
+               if (!secondCheckout)
+                       git.checkout().setStartPoint(revWalk.parseCommit(firstCommitId))
+                                       .setName("refs/heads/master").setCreateBranch(true).call();
+               try {
+                       if (secondCheckout) {
+                               git.checkout().setStartPoint(revWalk.parseCommit(commitId))
+                                               .setName("refs/heads/master").setCreateBranch(true)
+                                               .call();
+                       } else {
+                               git.branchCreate().setName("refs/heads/next")
+                                               .setStartPoint(commitId.name()).call();
+                               git.checkout().setName("refs/heads/next")
+                                               .call();
+                       }
+                       if (!good)
+                               fail("Checkout of Tree " + Arrays.asList(path) + " should fail");
+               } catch (InvalidPathException e) {
+                       if (good)
+                               throw e;
+                       assertThat(e.getMessage(), startsWith("Invalid path: "));
+               }
+       }
+
+}
index 859f611737146f5d6f9f6b79dcd7dd537d57b897..22a8558328944cf2af5adf43abfc400fc217cc0e 100644 (file)
@@ -58,12 +58,13 @@ import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.IndexWriteException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.CoreConfig.AutoCRLF;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.CoreConfig.AutoCRLF;
 import org.eclipse.jgit.treewalk.AbstractTreeIterator;
 import org.eclipse.jgit.treewalk.CanonicalTreeParser;
 import org.eclipse.jgit.treewalk.EmptyTreeIterator;
@@ -75,6 +76,7 @@ import org.eclipse.jgit.treewalk.WorkingTreeOptions;
 import org.eclipse.jgit.treewalk.filter.PathFilter;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FileUtils;
+import org.eclipse.jgit.util.SystemReader;
 import org.eclipse.jgit.util.io.AutoCRLFOutputStream;
 
 /**
@@ -304,6 +306,8 @@ public class DirCacheCheckout {
        void processEntry(CanonicalTreeParser m, DirCacheBuildIterator i,
                        WorkingTreeIterator f) throws IOException {
                if (m != null) {
+                       if (!isValidPath(m))
+                               throw new InvalidPathException(m.getEntryPathString());
                        // There is an entry in the merge commit. Means: we want to update
                        // what's currently in the index and working-tree to that one
                        if (i == null) {
@@ -506,12 +510,15 @@ public class DirCacheCheckout {
         * @throws IOException
         */
 
-       void processEntry(AbstractTreeIterator h, AbstractTreeIterator m,
+       void processEntry(CanonicalTreeParser h, CanonicalTreeParser m,
                        DirCacheBuildIterator i, WorkingTreeIterator f) throws IOException {
                DirCacheEntry dce = i != null ? i.getDirCacheEntry() : null;
 
                String name = walk.getPathString();
 
+               if (m != null && !isValidPath(m))
+                       throw new InvalidPathException(m.getEntryPathString());
+
                if (i == null && m == null && h == null) {
                        // File/Directory conflict case #20
                        if (walk.isDirectoryFileConflict())
@@ -988,4 +995,103 @@ public class DirCacheCheckout {
                else
                        entry.setLength((int) ol.getSize());
        }
+
+       private static byte[][] forbidden;
+       static {
+               String[] list = new String[] { "AUX", "COM1", "COM2", "COM3", "COM4",
+                               "COM5", "COM6", "COM7", "COM8", "COM9", "CON", "LPT1", "LPT2",
+                               "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "NUL",
+                               "PRN" };
+               forbidden = new byte[list.length][];
+               for (int i = 0; i < list.length; ++i)
+                       forbidden[i] = Constants.encodeASCII(list[i]);
+       }
+
+       private static boolean isValidPath(CanonicalTreeParser t) {
+               for (CanonicalTreeParser i = t; i != null; i = i.getParent())
+                       if (!isValidPathSegment(i))
+                               return false;
+               return true;
+       }
+
+       private static boolean isValidPathSegment(CanonicalTreeParser t) {
+               boolean isWindows = "Windows".equals(SystemReader.getInstance()
+                               .getProperty("os.name"));
+               boolean isOSX = "Mac OS X".equals(SystemReader.getInstance()
+                               .getProperty("os.name"));
+               boolean ignCase = isOSX || isWindows;
+
+               int ptr = t.getNameOffset();
+               byte[] raw = t.getEntryPathBuffer();
+               int end = ptr + t.getNameLength();
+
+               // Validate path component at this level of the tree
+               int start = ptr;
+               while (ptr < end) {
+                       if (raw[ptr] == '/')
+                               return false;
+                       if (isWindows) {
+                               if (raw[ptr] == '\\')
+                                       return false;
+                               if (raw[ptr] == ':')
+                                       return false;
+                       }
+                       ptr++;
+               }
+               // '.' and '.'' are invalid here
+               if (ptr - start == 1) {
+                       if (raw[start] == '.')
+                               return false;
+               } else if (ptr - start == 2) {
+                       if (raw[start] == '.')
+                               if (raw[start + 1] == '.')
+                                       return false;
+               } 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'))
+                                                       return false;
+               }
+               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 (raw[ptr - 1] == '.' || raw[ptr - 1] == ' ')
+                               return false;
+                       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)
+                                                       return false;
+                                       }
+                               }
+                       }
+               }
+
+               return true;
+       }
+
+       private static byte toUpper(byte b) {
+               if (b >= 'a' && b <= 'z')
+                       return (byte) (b - ('a' - 'A'));
+               return b;
+       }
+
 }
index 08cc9a8d52f5d4475273c426fc3465903c55cd93..d16f7d4f2fd15a2e058bdac2d5ad8925f0e63b18 100644 (file)
@@ -64,6 +64,7 @@ import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.MutableInteger;
 import org.eclipse.jgit.util.NB;
+import org.eclipse.jgit.util.SystemReader;
 
 /**
  * A single file (or stage of a file) in a {@link DirCache}.
@@ -261,8 +262,7 @@ public class DirCacheEntry {
        @SuppressWarnings("boxing")
        public DirCacheEntry(final byte[] newPath, final int stage) {
                if (!isValidPath(newPath))
-                       throw new IllegalArgumentException(MessageFormat.format(JGitText.get().invalidPath
-                                       , toString(newPath)));
+                       throw new InvalidPathException(toString(newPath));
                if (stage < 0 || 3 < stage)
                        throw new IllegalArgumentException(MessageFormat.format(JGitText.get().invalidStageForPath
                                        , stage, toString(newPath)));
@@ -716,7 +716,14 @@ public class DirCacheEntry {
                                else
                                        return false;
                                break;
-
+                       case '\\':
+                       case ':':
+                               // Tree's never have a backslash in them, not even on Windows
+                               // but even there we regard it as an invalid path
+                               if ("Windows".equals(SystemReader.getInstance().getProperty(
+                                               "os.name")))
+                                       return false;
+                               //$FALL-THROUGH$
                        default:
                                componentHasChars = true;
                        }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/InvalidPathException.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/InvalidPathException.java
new file mode 100644 (file)
index 0000000..155519f
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2011, Robin Rosenberg
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.dircache;
+
+import java.text.MessageFormat;
+
+import org.eclipse.jgit.internal.JGitText;
+
+/**
+ * Thrown when JGit detects and refuses to use an invalid path
+ */
+public class InvalidPathException extends IllegalArgumentException {
+
+       private static final long serialVersionUID = 1L;
+
+       /**
+        * @param path
+        */
+       public InvalidPathException(String path) {
+               super(MessageFormat.format(JGitText.get().invalidPath, path));
+       }
+}
index df3dac39192731dbf4b2f593fd95fd0ecbe5bb38..ca5fec2a9a661fa51af87a68a84bd9d7699094a0 100644 (file)
@@ -49,6 +49,7 @@ import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.CharBuffer;
 
+import org.eclipse.jgit.dircache.DirCacheCheckout;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.lib.Constants;
@@ -647,10 +648,23 @@ public abstract class AbstractTreeIterator {
        }
 
        /**
-        * Get the name component of the current entry path into the provided buffer.
+        * JGit internal API for use by {@link DirCacheCheckout}
         *
-        * @param buffer the buffer to get the name into, it is assumed that buffer can hold the name
-        * @param offset the offset of the name in the buffer
+        * @return start of name component part within {@link #getEntryPathBuffer()}
+        */
+       public int getNameOffset() {
+               return pathOffset;
+       }
+
+       /**
+        * Get the name component of the current entry path into the provided
+        * buffer.
+        *
+        * @param buffer
+        *            the buffer to get the name into, it is assumed that buffer can
+        *            hold the name
+        * @param offset
+        *            the offset of the name in the buffer
         * @see #getNameLength()
         */
        public void getName(byte[] buffer, int offset) {
index 1e49d380a80041efbd893fcf05995fdd0e3849d1..57bbd64ba59b3a25328a8e2d3b9cc496cbd30380 100644 (file)
@@ -108,6 +108,14 @@ public class CanonicalTreeParser extends AbstractTreeIterator {
                super(p);
        }
 
+       /**
+        * @return the parent of this tree parser
+        * @internal
+        */
+       public CanonicalTreeParser getParent() {
+               return (CanonicalTreeParser) parent;
+       }
+
        /**
         * Reset this parser to walk through the given tree data.
         *