From 42d7565ba9b97effdee3f737e32541b26b7341ab Mon Sep 17 00:00:00 2001 From: Robin Rosenberg Date: Mon, 23 Apr 2012 22:37:50 +0200 Subject: [PATCH] Validate paths during DirCheckout 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 --- .../eclipse/jgit/junit/MockSystemReader.java | 1 + org.eclipse.jgit.test/META-INF/MANIFEST.MF | 1 + org.eclipse.jgit.test/pom.xml | 7 + .../DirCacheCheckoutMaliciousPathTest.java | 420 ++++++++++++++++++ .../jgit/dircache/DirCacheCheckout.java | 110 ++++- .../eclipse/jgit/dircache/DirCacheEntry.java | 13 +- .../jgit/dircache/InvalidPathException.java | 63 +++ .../jgit/treewalk/AbstractTreeIterator.java | 20 +- .../jgit/treewalk/CanonicalTreeParser.java | 8 + 9 files changed, 635 insertions(+), 8 deletions(-) create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutMaliciousPathTest.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/dircache/InvalidPathException.java diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java index 23bf5632c..deca34106 100644 --- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java +++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java @@ -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) { diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF index 05b524d38..5db483b1f 100644 --- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF @@ -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)" diff --git a/org.eclipse.jgit.test/pom.xml b/org.eclipse.jgit.test/pom.xml index 6d5f38acb..969e205b0 100644 --- a/org.eclipse.jgit.test/pom.xml +++ b/org.eclipse.jgit.test/pom.xml @@ -69,6 +69,13 @@ test + + org.hamcrest + hamcrest-library + test + [1.1.0,2.0.0) + + org.eclipse.jgit org.eclipse.jgit 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 index 000000000..65d0418b3 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutMaliciousPathTest.java @@ -0,0 +1,420 @@ +/* + * 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.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: ")); + } + } + +} 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 859f61173..22a855832 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java @@ -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; + } + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java index 08cc9a8d5..d16f7d4f2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java @@ -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 index 000000000..155519f28 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/InvalidPathException.java @@ -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)); + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java index df3dac391..ca5fec2a9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java @@ -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) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/CanonicalTreeParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/CanonicalTreeParser.java index 1e49d380a..57bbd64ba 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/CanonicalTreeParser.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/CanonicalTreeParser.java @@ -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. *