diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index 0c7473f708..bede528f8a 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -185,7 +185,7 @@ public final class FilePath implements Serializable { */ public FilePath(VirtualChannel channel, String remote) { this.channel = channel; - this.remote = remote; + this.remote = normalize(remote); } /** @@ -197,7 +197,7 @@ public final class FilePath implements Serializable { */ public FilePath(File localPath) { this.channel = null; - this.remote = localPath.getPath(); + this.remote = normalize(localPath.getPath()); } /** @@ -209,12 +209,12 @@ public final class FilePath implements Serializable { this.channel = base.channel; if(isAbsolute(rel)) { // absolute - this.remote = rel; + this.remote = normalize(rel); } else if(base.isUnix()) { - this.remote = base.remote+'/'+rel; + this.remote = normalize(base.remote+'/'+rel); } else { - this.remote = base.remote+'\\'+rel; + this.remote = normalize(base.remote+'\\'+rel); } } @@ -224,6 +224,57 @@ public final class FilePath implements Serializable { private static final Pattern DRIVE_PATTERN = Pattern.compile("[A-Za-z]:[\\\\/].*"); + /** + * {@link File#getParent()} etc cannot handle ".." and "." in the path component very well, + * so remove them. + */ + private static String normalize(String path) { + if (path.indexOf('.')<0) return path; // common case + + path = _normalize(path,'/'); + path = _normalize(path,'\\'); + + // if the normalization results in a bare drive letter, like "C:", it needs to be fixed up to "C:\\" + if (path.length()==2 && path.charAt(1)==':') path+='\\'; + return path; + } + + private static String _normalize(String path, char separator) { + List tokens = new ArrayList(); + int s=0; // start of the next token + while (true) { + int idx = path.indexOf(separator,s); + if (idx<0) { + tokens.add(path.substring(s)); + break; + } + tokens.add(path.substring(s,idx)); + s = idx+1; + } + + boolean modified=false; + + for (int i=0; i0) { + tokens.remove(i); + tokens.remove(i-1); + i--; + modified = true; + } else + i++; + } + + if (modified) + path = Util.join(tokens,""+separator); + + return path; + } + /** * Checks if the remote path is Unix. */ diff --git a/core/src/test/java/hudson/FilePathTest.java b/core/src/test/java/hudson/FilePathTest.java index c1fc0a7463..da96db52c3 100644 --- a/core/src/test/java/hudson/FilePathTest.java +++ b/core/src/test/java/hudson/FilePathTest.java @@ -23,6 +23,7 @@ */ package hudson; +import hudson.remoting.VirtualChannel; import hudson.util.NullStream; import java.io.File; @@ -71,4 +72,32 @@ public class FilePathTest extends ChannelTestCase { Util.deleteRecursive(tmp); } } + + public void testNormalization() throws Exception { + compare("abc/def\\ghi","abc/def\\ghi"); // allow mixed separators + + {// basic '.' trimming + compare("./abc/def","abc/def"); + compare("abc/./def","abc/def"); + compare("abc/def/.","abc/def"); + + compare(".\\abc\\def","abc\\def"); + compare("abc\\.\\def","abc\\def"); + compare("abc\\def\\.","abc\\def"); + } + + compare("abc/../def","def"); + compare("abc/def/../../ghi","ghi"); + compare("abc/./def/../././../ghi","ghi"); // interleaving . and .. + + compare("../abc/def","../abc/def"); // uncollapsible .. + compare("abc/def/..","abc"); + + compare("c:\\abc\\..","c:\\"); // we want c:\\, not c: + compare("c:\\abc\\def\\..","c:\\abc"); + } + + private void compare(String original, String answer) { + assertEquals(answer,new FilePath((VirtualChannel)null,original).getRemote()); + } }