diff --git a/core/src/main/java/hudson/FilePath.java b/core/src/main/java/hudson/FilePath.java index 06d593ea51..9ba406daf1 100644 --- a/core/src/main/java/hudson/FilePath.java +++ b/core/src/main/java/hudson/FilePath.java @@ -1,7 +1,8 @@ /* * The MIT License * - * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Eric Lefevre-Ardant, Erik Ramfelt, Michael B. Donohue + * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, + * Eric Lefevre-Ardant, Erik Ramfelt, Michael B. Donohue, Alan Harder * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -86,6 +87,7 @@ import java.util.List; import java.util.StringTokenizer; import java.util.Arrays; import java.util.Comparator; +import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -222,59 +224,66 @@ public final class FilePath implements Serializable { return rel.startsWith("/") || DRIVE_PATTERN.matcher(rel).matches(); } - private static final Pattern DRIVE_PATTERN = Pattern.compile("[A-Za-z]:[\\\\/].*"); + private static final Pattern DRIVE_PATTERN = Pattern.compile("[A-Za-z]:[\\\\/].*"), + ABSOLUTE_PREFIX_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; + StringBuilder buf = new StringBuilder(); + // Check for prefix designating absolute path + Matcher m = ABSOLUTE_PREFIX_PATTERN.matcher(path); + if (m.find()) { + buf.append(m.group(1)); + path = path.substring(m.end()); } - - boolean modified=false; - - for (int i=0; i 0; + // Split remaining path into tokens, trimming any duplicate or trailing separators + List tokens = new ArrayList(); + int s = 0, end = path.length(); + for (int i = 0; i < end; i++) { + char c = path.charAt(i); + if (c == '/' || c == '\\') { + tokens.add(path.substring(s, i)); + s = i; + // Skip any extra separator chars + while (++i < end && ((c = path.charAt(i)) == '/' || c == '\\')) { } + // Add token for separator unless we reached the end + if (i < end) tokens.add(path.substring(s, s+1)); + s = i; + } + } + if (s < end) tokens.add(path.substring(s)); + // Look through tokens for "." or ".." + for (int i = 0; i < tokens.size();) { String token = tokens.get(i); if (token.equals(".")) { tokens.remove(i); - modified = true; + if (tokens.size() > 0) + tokens.remove(i > 0 ? i - 1 : i); + } else if (token.equals("..")) { + if (i == 0) { + // If absolute path, just remove: /../something + // If relative path, not collapsible so leave as-is + tokens.remove(0); + if (tokens.size() > 0) token += tokens.remove(0); + if (!isAbsolute) buf.append(token); + } else { + // Normalize: remove something/.. plus separator before/after + i -= 2; + for (int j = 0; j < 2; j++) tokens.remove(i); + if (i > 0) tokens.remove(i-1); + else if (tokens.size() > 0) tokens.remove(0); + } } else - if (token.equals("..") && i>0) { - tokens.remove(i); - tokens.remove(i-1); - i--; - modified = true; - } else - i++; + i += 2; } - - if (modified) { - path = Util.join(tokens,""+separator); - if (path.length()==0) path="."; // "../abc" -> "." - } - - return path; + // Recombine tokens + for (String token : tokens) buf.append(token); + if (buf.length() == 0) buf.append('.'); + return buf.toString(); } /** diff --git a/core/src/test/java/hudson/FilePathTest.java b/core/src/test/java/hudson/FilePathTest.java index 41061119dd..5285134e3a 100644 --- a/core/src/test/java/hudson/FilePathTest.java +++ b/core/src/test/java/hudson/FilePathTest.java @@ -1,7 +1,7 @@ /* * The MIT License * - * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi + * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Alan Harder * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -113,8 +113,30 @@ public class FilePathTest extends ChannelTestCase { compare("c:\\abc\\..","c:\\"); // we want c:\\, not c: compare("c:\\abc\\def\\..","c:\\abc"); + compare("/abc/../","/"); compare("abc/..","."); compare(".","."); + + // @Bug(5951) + compare("C:\\Hudson\\jobs\\foo\\workspace/../../otherjob/workspace/build.xml", + "C:\\Hudson\\jobs/otherjob/workspace/build.xml"); + // Other cases that failed before + compare("../../abc/def","../../abc/def"); + compare("..\\..\\abc\\def","..\\..\\abc\\def"); + compare("/abc//../def","/def"); + compare("c:\\abc\\\\..\\def","c:\\def"); + compare("/../abc/def","/abc/def"); + compare("c:\\..\\abc\\def","c:\\abc\\def"); + compare("abc/def/","abc/def"); + compare("abc\\def\\","abc\\def"); + // The new code can collapse extra separator chars + compare("abc//def/\\//\\ghi","abc/def/ghi"); + compare("\\\\host\\\\abc\\\\\\def","\\\\host\\abc\\def"); // don't collapse for \\ prefix + compare("\\\\\\foo","\\\\foo"); + compare("//foo","/foo"); + // Other edge cases + compare("abc/def/../../../ghi","../ghi"); + compare("\\abc\\def\\..\\..\\..\\ghi\\","\\ghi"); } private void compare(String original, String answer) {