Browse Source

don't run into infinite lopps caused by symbolic links. PR 45499.

git-svn-id: https://svn.apache.org/repos/asf/ant/core/trunk@693870 13f79535-47bb-0310-9956-ffa450edef68
master
Stefan Bodewig 17 years ago
parent
commit
1d3ca73f3c
4 changed files with 169 additions and 10 deletions
  1. +99
    -7
      src/main/org/apache/tools/ant/DirectoryScanner.java
  2. +13
    -0
      src/main/org/apache/tools/ant/types/AbstractFileSet.java
  3. +20
    -0
      src/main/org/apache/tools/ant/util/CollectionUtils.java
  4. +37
    -3
      src/tests/antunit/core/dirscanner-symlinks-test.xml

+ 99
- 7
src/main/org/apache/tools/ant/DirectoryScanner.java View File

@@ -27,6 +27,7 @@ import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.Stack;
import java.util.Vector; import java.util.Vector;


import org.apache.tools.ant.taskdefs.condition.Os; import org.apache.tools.ant.taskdefs.condition.Os;
@@ -37,6 +38,7 @@ import org.apache.tools.ant.types.selectors.FileSelector;
import org.apache.tools.ant.types.selectors.PathPattern; import org.apache.tools.ant.types.selectors.PathPattern;
import org.apache.tools.ant.types.selectors.SelectorScanner; import org.apache.tools.ant.types.selectors.SelectorScanner;
import org.apache.tools.ant.types.selectors.SelectorUtils; import org.apache.tools.ant.types.selectors.SelectorUtils;
import org.apache.tools.ant.util.CollectionUtils;
import org.apache.tools.ant.util.FileUtils; import org.apache.tools.ant.util.FileUtils;


/** /**
@@ -168,6 +170,8 @@ public class DirectoryScanner
"**/.DS_Store" "**/.DS_Store"
}; };


public static final int MAX_LEVELS_OF_SYMLINKS = 1;

/** Helper. */ /** Helper. */
private static final FileUtils FILE_UTILS = FileUtils.getFileUtils(); private static final FileUtils FILE_UTILS = FileUtils.getFileUtils();


@@ -375,6 +379,14 @@ public class DirectoryScanner
*/ */
private IllegalStateException illegal = null; private IllegalStateException illegal = null;


/**
* The maximum number of times a symbolic link may be followed
* during a scan.
*
* @since Ant 1.8.0
*/
private int maxLevelsOfSymlinks = MAX_LEVELS_OF_SYMLINKS;

/** /**
* Sole constructor. * Sole constructor.
*/ */
@@ -643,6 +655,16 @@ public class DirectoryScanner
this.followSymlinks = followSymlinks; this.followSymlinks = followSymlinks;
} }


/**
* The maximum number of times a symbolic link may be followed
* during a scan.
*
* @since Ant 1.8.0
*/
public void setMaxLevelsOfSymlinks(int max) {
maxLevelsOfSymlinks = max;
}

/** /**
* Set the list of include patterns to use. All '/' and '\' characters * Set the list of include patterns to use. All '/' and '\' characters
* are replaced by <code>File.separatorChar</code>, so the separator used * are replaced by <code>File.separatorChar</code>, so the separator used
@@ -1086,11 +1108,11 @@ public class DirectoryScanner
+ dir.getAbsolutePath() + "'"); + dir.getAbsolutePath() + "'");
} }
} }
scandir(dir, vpath, fast, newfiles);
scandir(dir, vpath, fast, newfiles, new Stack());
} }


private void scandir(File dir, String vpath, boolean fast, private void scandir(File dir, String vpath, boolean fast,
String[] newfiles) {
String[] newfiles, Stack directoryNamesFollowed) {
// avoid double scanning of directories, can only happen in fast mode // avoid double scanning of directories, can only happen in fast mode
if (fast && hasBeenScanned(vpath)) { if (fast && hasBeenScanned(vpath)) {
return; return;
@@ -1116,7 +1138,10 @@ public class DirectoryScanner
} }
} }
newfiles = (String[]) (noLinks.toArray(new String[noLinks.size()])); newfiles = (String[]) (noLinks.toArray(new String[noLinks.size()]));
} else {
directoryNamesFollowed.push(dir.getName());
} }

for (int i = 0; i < newfiles.length; i++) { for (int i = 0; i < newfiles.length; i++) {
String name = vpath + newfiles[i]; String name = vpath + newfiles[i];
File file = new File(dir, newfiles[i]); File file = new File(dir, newfiles[i]);
@@ -1129,20 +1154,39 @@ public class DirectoryScanner
filesNotIncluded.addElement(name); filesNotIncluded.addElement(name);
} }
} else { // dir } else { // dir

if (followSymlinks
&& causesIllegalSymlinkLoop(newfiles[i], dir,
directoryNamesFollowed)) {
// will be caught and redirected to Ant's logging system
System.err.println("skipping symbolic link "
+ file.getAbsolutePath()
+ " -- too many levels of symbolic"
+ " links.");
continue;
}

if (isIncluded(name)) { if (isIncluded(name)) {
accountForIncludedDir(name, file, fast, children);
accountForIncludedDir(name, file, fast, children,
directoryNamesFollowed);
} else { } else {
everythingIncluded = false; everythingIncluded = false;
dirsNotIncluded.addElement(name); dirsNotIncluded.addElement(name);
if (fast && couldHoldIncluded(name)) { if (fast && couldHoldIncluded(name)) {
scandir(file, name + File.separator, fast, children);
scandir(file, name + File.separator, fast, children,
directoryNamesFollowed);
} }
} }
if (!fast) { if (!fast) {
scandir(file, name + File.separator, fast, children);
scandir(file, name + File.separator, fast, children,
directoryNamesFollowed);
} }
} }
} }

if (followSymlinks) {
directoryNamesFollowed.pop();
}
} }


/** /**
@@ -1170,10 +1214,12 @@ public class DirectoryScanner
} }


private void accountForIncludedDir(String name, File file, boolean fast, private void accountForIncludedDir(String name, File file, boolean fast,
String[] children) {
String[] children,
Stack directoryNamesFollowed) {
processIncluded(name, file, dirsIncluded, dirsExcluded, dirsDeselected); processIncluded(name, file, dirsIncluded, dirsExcluded, dirsDeselected);
if (fast && couldHoldIncluded(name) && !contentsExcluded(name)) { if (fast && couldHoldIncluded(name) && !contentsExcluded(name)) {
scandir(file, name + File.separator, fast, children);
scandir(file, name + File.separator, fast, children,
directoryNamesFollowed);
} }
} }


@@ -1744,4 +1790,50 @@ public class DirectoryScanner
return (PathPattern[]) al.toArray(new PathPattern[al.size()]); return (PathPattern[]) al.toArray(new PathPattern[al.size()]);
} }


/**
* Would following the given directory cause a loop of symbolic
* links deeper than allowed?
*
* <p>Can only happen if the given directory has been seen at
* least more often than allowed during the current scan and it is
* a symbolic link and enough other occurences of the same name
* higher up are symbolic links that point to the same place.</p>
*
* @since Ant 1.8.0
*/
private boolean causesIllegalSymlinkLoop(String dirName, File parent,
Stack directoryNamesFollowed) {
try {
if (CollectionUtils.frequency(directoryNamesFollowed, dirName)
>= maxLevelsOfSymlinks
&& FILE_UTILS.isSymbolicLink(parent, dirName)) {

Stack s = (Stack) directoryNamesFollowed.clone();
ArrayList files = new ArrayList();
File f = FILE_UTILS.resolveFile(parent, dirName);
String target = f.getCanonicalPath();
files.add(target);

String relPath = "";
while (!s.empty()) {
relPath += "../";
String dir = (String) s.pop();
if (dirName.equals(dir)) {
f = FILE_UTILS.resolveFile(parent, relPath + dir);
files.add(f.getCanonicalPath());
if (CollectionUtils.frequency(files, target)
> maxLevelsOfSymlinks) {
return true;
}
}
}

}
return false;
} catch (IOException ex) {
throw new BuildException("Caught error while checking for"
+ " symbolic links", ex);
}
}

} }

+ 13
- 0
src/main/org/apache/tools/ant/types/AbstractFileSet.java View File

@@ -68,6 +68,7 @@ public abstract class AbstractFileSet extends DataType
private boolean caseSensitive = true; private boolean caseSensitive = true;
private boolean followSymlinks = true; private boolean followSymlinks = true;
private boolean errorOnMissingDir = true; private boolean errorOnMissingDir = true;
private int maxLevelsOfSymlinks = DirectoryScanner.MAX_LEVELS_OF_SYMLINKS;


/* cached DirectoryScanner instance for our own Project only */ /* cached DirectoryScanner instance for our own Project only */
private DirectoryScanner directoryScanner = null; private DirectoryScanner directoryScanner = null;
@@ -93,6 +94,7 @@ public abstract class AbstractFileSet extends DataType
this.caseSensitive = fileset.caseSensitive; this.caseSensitive = fileset.caseSensitive;
this.followSymlinks = fileset.followSymlinks; this.followSymlinks = fileset.followSymlinks;
this.errorOnMissingDir = fileset.errorOnMissingDir; this.errorOnMissingDir = fileset.errorOnMissingDir;
this.maxLevelsOfSymlinks = fileset.maxLevelsOfSymlinks;
setProject(fileset.getProject()); setProject(fileset.getProject());
} }


@@ -396,6 +398,16 @@ public abstract class AbstractFileSet extends DataType
? getRef(getProject()).isFollowSymlinks() : followSymlinks; ? getRef(getProject()).isFollowSymlinks() : followSymlinks;
} }


/**
* The maximum number of times a symbolic link may be followed
* during a scan.
*
* @since Ant 1.8.0
*/
public void setMaxLevelsOfSymlinks(int max) {
maxLevelsOfSymlinks = max;
}

/** /**
* Sets whether an error is thrown if a directory does not exist. * Sets whether an error is thrown if a directory does not exist.
* *
@@ -444,6 +456,7 @@ public abstract class AbstractFileSet extends DataType
setupDirectoryScanner(ds, p); setupDirectoryScanner(ds, p);
ds.setFollowSymlinks(followSymlinks); ds.setFollowSymlinks(followSymlinks);
ds.setErrorOnMissingDir(errorOnMissingDir); ds.setErrorOnMissingDir(errorOnMissingDir);
ds.setMaxLevelsOfSymlinks(maxLevelsOfSymlinks);
directoryScanner = (p == getProject()) ? ds : directoryScanner; directoryScanner = (p == getProject()) ? ds : directoryScanner;
} }
} }


+ 20
- 0
src/main/org/apache/tools/ant/util/CollectionUtils.java View File

@@ -222,4 +222,24 @@ public class CollectionUtils {


} }


/**
* Counts how often the given Object occurs in the given
* collection using equals() for comparison.
*
* @since Ant 1.8.0
*/
public static int frequency(Collection c, Object o) {
// same as Collections.frequency introduced with JDK 1.5
int freq = 0;
if (c != null) {
for (Iterator i = c.iterator(); i.hasNext(); ) {
Object test = i.next();
if (o == null ? test == null : o.equals(test)) {
freq++;
}
}
}
return freq;
}
} }

+ 37
- 3
src/tests/antunit/core/dirscanner-symlinks-test.xml View File

@@ -75,14 +75,26 @@
<au:assertFileDoesntExist file="${output}/file.txt"/> <au:assertFileDoesntExist file="${output}/file.txt"/>
</target> </target>


<target name="INFINITEtestLinkToParentFollow"
<target name="testLinkToParentFollow"
depends="checkOs, setUp, -link-to-parent" depends="checkOs, setUp, -link-to-parent"
if="unix"> if="unix">
<copy todir="${output}"> <copy todir="${output}">
<fileset dir="${base}" followsymlinks="true"/>
<fileset dir="${base}" followsymlinks="true" maxLevelsOfSymlinks="1"/>
</copy>
<symlink action="delete" link="${base}/A"/>
<au:assertFileExists file="${output}/A/B/file.txt"/>
<au:assertFileDoesntExist file="${output}/A/base/A/B/file.txt"/>
</target>

<target name="testLinkToParentFollowMax2"
depends="checkOs, setUp, -link-to-parent"
if="unix">
<copy todir="${output}">
<fileset dir="${base}" followsymlinks="true" maxLevelsOfSymlinks="2"/>
</copy> </copy>
<symlink action="delete" link="${base}/A"/> <symlink action="delete" link="${base}/A"/>
<au:assertFileExists file="${output}/A/B/file.txt"/> <au:assertFileExists file="${output}/A/B/file.txt"/>
<au:assertFileExists file="${output}/A/base/A/B/file.txt"/>
</target> </target>


<target name="testLinkToParentFollowWithInclude" <target name="testLinkToParentFollowWithInclude"
@@ -120,7 +132,7 @@
<au:assertFileDoesntExist file="${output}/A/B/file.txt"/> <au:assertFileDoesntExist file="${output}/A/B/file.txt"/>
</target> </target>


<target name="INFINITE testSillyLoopFollow"
<target name="testSillyLoopFollow"
depends="checkOs, setUp, -silly-loop" depends="checkOs, setUp, -silly-loop"
if="unix"> if="unix">
<copy todir="${output}"> <copy todir="${output}">
@@ -140,6 +152,28 @@
<au:assertFileDoesntExist file="${output}"/> <au:assertFileDoesntExist file="${output}"/>
</target> </target>


<target name="testRepeatedName"
depends="setUp">
<mkdir dir="${base}/A/A/A/A"/>
<touch file="${base}/A/A/A/A/file.txt"/>
<copy todir="${output}">
<fileset dir="${base}" followsymlinks="true" maxLevelsOfSymlinks="1"/>
</copy>
<au:assertFileExists file="${output}/A/A/A/A/file.txt"/>
</target>

<target name="testRepeatedNameWithLinkButNoLoop"
depends="checkOs, setUp"
if="unix">
<mkdir dir="${base}/A/A/A/B"/>
<touch file="${base}/A/A/A/B/file.txt"/>
<symlink link="${base}/A/A/A/A" resource="${base}/A/A/A/B"/>
<copy todir="${output}">
<fileset dir="${base}" followsymlinks="true" maxLevelsOfSymlinks="1"/>
</copy>
<au:assertFileExists file="${output}/A/A/A/A/file.txt"/>
</target>

<target name="-sibling" if="unix"> <target name="-sibling" if="unix">
<mkdir dir="${base}/A"/> <mkdir dir="${base}/A"/>
<touch file="${base}/A/file.txt"/> <touch file="${base}/A/file.txt"/>


Loading…
Cancel
Save