Skip to content

Commit ed01f16

Browse files
author
Niall Pemberton
committed
IO-166 Fix URL decoding in FileUtils.toFile() - thanks to Benjamin Bentmann for the patch
git-svn-id: https://svn.apache.org/repos/asf/commons/proper/io/trunk@1002457 13f79535-47bb-0310-9956-ffa450edef68
1 parent 58db1d1 commit ed01f16

2 files changed

Lines changed: 97 additions & 17 deletions

File tree

src/java/org/apache/commons/io/FileUtils.java

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
import java.io.OutputStream;
2727
import java.net.URL;
2828
import java.net.URLConnection;
29+
import java.nio.ByteBuffer;
2930
import java.nio.channels.FileChannel;
31+
import java.nio.charset.Charset;
3032
import java.util.ArrayList;
3133
import java.util.Collection;
3234
import java.util.Date;
@@ -113,6 +115,11 @@ public FileUtils() {
113115
*/
114116
public static final File[] EMPTY_FILE_ARRAY = new File[0];
115117

118+
/**
119+
* The UTF-8 character set, used to decode octets in URLs.
120+
*/
121+
private static final Charset UTF8 = Charset.forName("UTF-8");
122+
116123
//-----------------------------------------------------------------------
117124
/**
118125
* Returns the path to the system temporary directory.
@@ -515,28 +522,70 @@ public static boolean contentEquals(File file1, File file2) throws IOException {
515522
* <p>
516523
* From version 1.1 this method will decode the URL.
517524
* Syntax such as <code>file:///my%20docs/file.txt</code> will be
518-
* correctly decoded to <code>/my docs/file.txt</code>.
525+
* correctly decoded to <code>/my docs/file.txt</code>. Starting with version
526+
* 1.5, this method uses UTF-8 to decode percent-encoded octets to characters.
527+
* Additionally, malformed percent-encoded octets are handled leniently by
528+
* passing them through literally.
519529
*
520530
* @param url the file URL to convert, <code>null</code> returns <code>null</code>
521531
* @return the equivalent <code>File</code> object, or <code>null</code>
522532
* if the URL's protocol is not <code>file</code>
523-
* @throws IllegalArgumentException if the file is incorrectly encoded
524533
*/
525534
public static File toFile(URL url) {
526-
if (url == null || !url.getProtocol().equals("file")) {
535+
if (url == null || !"file".equalsIgnoreCase(url.getProtocol())) {
527536
return null;
528537
} else {
529538
String filename = url.getFile().replace('/', File.separatorChar);
530-
int pos =0;
531-
while ((pos = filename.indexOf('%', pos)) >= 0) {
532-
if (pos + 2 < filename.length()) {
533-
String hexStr = filename.substring(pos + 1, pos + 3);
534-
char ch = (char) Integer.parseInt(hexStr, 16);
535-
filename = filename.substring(0, pos) + ch + filename.substring(pos + 3);
539+
filename = decodeUrl(filename);
540+
return new File(filename);
541+
}
542+
}
543+
544+
/**
545+
* Decodes the specified URL as per RFC 3986, i.e. transforms
546+
* percent-encoded octets to characters by decoding with the UTF-8 character
547+
* set. This function is primarily intended for usage with
548+
* {@link java.net.URL} which unfortunately does not enforce proper URLs. As
549+
* such, this method will leniently accept invalid characters or malformed
550+
* percent-encoded octets and simply pass them literally through to the
551+
* result string. Except for rare edge cases, this will make unencoded URLs
552+
* pass through unaltered.
553+
*
554+
* @param url The URL to decode, may be <code>null</code>.
555+
* @return The decoded URL or <code>null</code> if the input was
556+
* <code>null</code>.
557+
*/
558+
static String decodeUrl(String url) {
559+
String decoded = url;
560+
if (url != null && url.indexOf('%') >= 0) {
561+
int n = url.length();
562+
StringBuffer buffer = new StringBuffer();
563+
ByteBuffer bytes = ByteBuffer.allocate(n);
564+
for (int i = 0; i < n;) {
565+
if (url.charAt(i) == '%') {
566+
try {
567+
do {
568+
byte octet = (byte) Integer.parseInt(url.substring(i + 1, i + 3), 16);
569+
bytes.put(octet);
570+
i += 3;
571+
} while (i < n && url.charAt(i) == '%');
572+
continue;
573+
} catch (RuntimeException e) {
574+
// malformed percent-encoded octet, fall through and
575+
// append characters literally
576+
} finally {
577+
if (bytes.position() > 0) {
578+
bytes.flip();
579+
buffer.append(UTF8.decode(bytes).toString());
580+
bytes.clear();
581+
}
582+
}
536583
}
584+
buffer.append(url.charAt(i++));
537585
}
538-
return new File(filename);
586+
decoded = buffer.toString();
539587
}
588+
return decoded;
540589
}
541590

542591
/**

src/test/org/apache/commons/io/FileUtilsTestCase.java

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -283,9 +283,9 @@ public void testToFile1() throws Exception {
283283
}
284284

285285
public void testToFile2() throws Exception {
286-
URL url = new URL("file", null, "a/b/c/file%20n%61me.tx%74");
286+
URL url = new URL("file", null, "a/b/c/file%20n%61me%2520.tx%74");
287287
File file = FileUtils.toFile(url);
288-
assertEquals(true, file.toString().indexOf("file name.txt") >= 0);
288+
assertEquals(true, file.toString().indexOf("file name%20.txt") >= 0);
289289
}
290290

291291
public void testToFile3() throws Exception {
@@ -294,11 +294,42 @@ public void testToFile3() throws Exception {
294294
}
295295

296296
public void testToFile4() throws Exception {
297-
URL url = new URL("file", null, "a/b/c/file%2Xn%61me.txt");
298-
try {
299-
FileUtils.toFile(url);
300-
fail();
301-
} catch (IllegalArgumentException ex) {}
297+
URL url = new URL("file", null, "a/b/c/file%%20%me.txt%");
298+
File file = FileUtils.toFile(url);
299+
assertEquals(true, file.toString().indexOf("file% %me.txt%") >= 0);
300+
}
301+
302+
public void testToFileUtf8() throws Exception {
303+
URL url = new URL("file", null, "/home/%C3%A4%C3%B6%C3%BC%C3%9F");
304+
File file = FileUtils.toFile(url);
305+
assertEquals(true, file.toString().indexOf("\u00E4\u00F6\u00FC\u00DF") >= 0);
306+
}
307+
308+
public void testDecodeUrl() {
309+
assertEquals("", FileUtils.decodeUrl(""));
310+
assertEquals("foo", FileUtils.decodeUrl("foo"));
311+
assertEquals("+", FileUtils.decodeUrl("+"));
312+
assertEquals("% ", FileUtils.decodeUrl("%25%20"));
313+
assertEquals("%20", FileUtils.decodeUrl("%2520"));
314+
assertEquals("jar:file:/C:/dir/sub dir/1.0/foo-1.0.jar!/org/Bar.class", FileUtils
315+
.decodeUrl("jar:file:/C:/dir/sub%20dir/1.0/foo-1.0.jar!/org/Bar.class"));
316+
}
317+
318+
public void testDecodeUrlLenient() {
319+
assertEquals(" ", FileUtils.decodeUrl(" "));
320+
assertEquals("\u00E4\u00F6\u00FC\u00DF", FileUtils.decodeUrl("\u00E4\u00F6\u00FC\u00DF"));
321+
assertEquals("%", FileUtils.decodeUrl("%"));
322+
assertEquals("% ", FileUtils.decodeUrl("%%20"));
323+
assertEquals("%2", FileUtils.decodeUrl("%2"));
324+
assertEquals("%2G", FileUtils.decodeUrl("%2G"));
325+
}
326+
327+
public void testDecodeUrlNullSafe() {
328+
assertNull(FileUtils.decodeUrl(null));
329+
}
330+
331+
public void testDecodeUrlEncodingUtf8() {
332+
assertEquals("\u00E4\u00F6\u00FC\u00DF", FileUtils.decodeUrl("%C3%A4%C3%B6%C3%BC%C3%9F"));
302333
}
303334

304335
// toFiles

0 commit comments

Comments
 (0)