Skip to content

Commit 947c01f

Browse files
committed
PR: IO-567
Throw an IllegalArgumentException in FilenameUtils.getExtension(String) for ADS names.
1 parent 459cebc commit 947c01f

4 files changed

Lines changed: 93 additions & 3 deletions

File tree

src/changes/changes.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ The <action> type attribute can be add,update,fix,remove.
140140
<action issue="IO-514" dev="pschumacher" type="remove">
141141
Remove org.apache.commons.io.Java7Support
142142
</action>
143+
<action issue="IO-567" dev="jochen" type="fix">
144+
Implement special case handling for NTFS ADS names: FilenameUtils.getExtension(String),
145+
and FilenameUtils.indexOfExtension(String) are now throwing an IllegalArgumentException,
146+
if the file name in question appears to identify an alternate data stream (Windows only).
147+
</action>
143148
</release>
144149

145150
<release version="2.5" date="2016-04-22" description="New features and bug fixes.">

src/main/java/org/apache/commons/io/FilenameUtils.java

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -716,15 +716,28 @@ public static int indexOfLastSeparator(final String filename) {
716716
* <p>
717717
* The output will be the same irrespective of the machine that the code is running on.
718718
* </p>
719+
* <b>Note:</b> This method used to have a hidden problem for names like "foo.exe:bar.txt".
720+
* In this case, the name wouldn't be the name of a file, but the identifier of an
721+
* alternate data stream (bar.txt) on the file foo.exe. The method used to return
722+
* ".txt" here, which would be misleading. Commons IO 2.7, and later versions, are throwing
723+
* at {@link NtfsAdsNameException} for names like this.
719724
*
720725
* @param filename
721726
* the filename to find the last extension separator in, null returns -1
722727
* @return the index of the last extension separator character, or -1 if there is no such character
728+
* @throws NtfsAdsNameException The filename argument
723729
*/
724-
public static int indexOfExtension(final String filename) {
730+
public static int indexOfExtension(final String filename) throws NtfsAdsNameException {
725731
if (filename == null) {
726732
return NOT_FOUND;
727733
}
734+
if (isSystemWindows()) {
735+
// Special handling for NTFS ADS: Don't accept colon in the filename.
736+
final int offset = filename.indexOf(':', getAdsCriticalOffset(filename));
737+
if (offset != -1) {
738+
throw new NtfsAdsNameException("NTFS ADS separator (':') in filename is forbidden.");
739+
}
740+
}
728741
final int extensionPos = filename.lastIndexOf(EXTENSION_SEPARATOR);
729742
final int lastSeparator = indexOfLastSeparator(filename);
730743
return lastSeparator > extensionPos ? NOT_FOUND : extensionPos;
@@ -1027,12 +1040,19 @@ public static String getBaseName(final String filename) {
10271040
* </pre>
10281041
* <p>
10291042
* The output will be the same irrespective of the machine that the code is running on.
1043+
* <b>Note:</b> This method used to have a hidden problem for names like "foo.exe:bar.txt".
1044+
* In this case, the name wouldn't be the name of a file, but the identifier of an
1045+
* alternate data stream (bar.txt) on the file foo.exe. The method used to return
1046+
* ".txt" here, which would be misleading. Commons IO 2.7, and later versions, are throwing
1047+
* at {@link NtfsAdsNameException} for names like this.
10301048
*
10311049
* @param filename the filename to retrieve the extension of.
10321050
* @return the extension of the file or an empty string if none exists or {@code null}
10331051
* if the filename is {@code null}.
1052+
* @throws NtfsAdsNameException <b>Windows only:/b> The filename parameter is, in fact,
1053+
* the identifier of an Alternate Data Stream, for example "foo.exe:bar.txt".
10341054
*/
1035-
public static String getExtension(final String filename) {
1055+
public static String getExtension(final String filename) throws NtfsAdsNameException {
10361056
if (filename == null) {
10371057
return null;
10381058
}
@@ -1044,6 +1064,25 @@ public static String getExtension(final String filename) {
10441064
}
10451065
}
10461066

1067+
private static int getAdsCriticalOffset(String filename) {
1068+
// Step 1: Remove leading path segments.
1069+
int offset1 = filename.lastIndexOf(SYSTEM_SEPARATOR);
1070+
int offset2 = filename.lastIndexOf(OTHER_SEPARATOR);
1071+
if (offset1 == -1) {
1072+
if (offset2 == -1) {
1073+
return 0;
1074+
} else {
1075+
return offset2 + 1;
1076+
}
1077+
} else {
1078+
if (offset2 == -1) {
1079+
return offset1+1;
1080+
} else {
1081+
return Math.max(offset1, offset2)+1;
1082+
}
1083+
}
1084+
}
1085+
10471086
//-----------------------------------------------------------------------
10481087
/**
10491088
* Removes the extension from a filename.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.apache.commons.io;
2+
3+
4+
/**
5+
* This exception is thrown, if an NTFS ADS name is passed to certain methods,
6+
* where it might affect the result. For example, if you pass a name like
7+
* "foo.exe:bar.txt" to {@link FilenameUtils#getExtension(String)}, then it
8+
* might return ".txt", which would be misleading, because the actual extension
9+
* would be ".txt".
10+
*/
11+
public class NtfsAdsNameException extends IllegalArgumentException {
12+
13+
private static final long serialVersionUID = -9158109384797441214L;
14+
15+
public NtfsAdsNameException(String pMessage) {
16+
super(pMessage);
17+
}
18+
}

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
import org.junit.Test;
3636
import org.junit.rules.TemporaryFolder;
3737

38+
import org.junit.Assert;
39+
3840
/**
3941
* This is used to test FilenameUtils for correctness.
4042
*
@@ -580,6 +582,20 @@ public void testIndexOfExtension() {
580582
assertEquals(-1, FilenameUtils.indexOfExtension("a\\b\\c"));
581583
assertEquals(-1, FilenameUtils.indexOfExtension("a/b.notextension/c"));
582584
assertEquals(-1, FilenameUtils.indexOfExtension("a\\b.notextension\\c"));
585+
586+
if (FilenameUtils.isSystemWindows()) {
587+
// Special case handling for NTFS ADS names
588+
try {
589+
FilenameUtils.indexOfExtension("foo.exe:bar.txt");
590+
throw new AssertionError("Expected Exception");
591+
} catch (IllegalArgumentException e) {
592+
assertEquals("NTFS ADS separator (':') in filename is forbidden.", e.getMessage());
593+
}
594+
} else {
595+
// Upwards compatibility on other systems
596+
assertEquals(11, FilenameUtils.indexOfExtension("foo.exe:bar.txt"));
597+
}
598+
583599
}
584600

585601
//-----------------------------------------------------------------------
@@ -862,6 +878,19 @@ public void testGetExtension() {
862878
assertEquals("", FilenameUtils.getExtension("a\\b\\c"));
863879
assertEquals("", FilenameUtils.getExtension("C:\\temp\\foo.bar\\README"));
864880
assertEquals("ext", FilenameUtils.getExtension("../filename.ext"));
881+
882+
if (FilenameUtils.isSystemWindows()) {
883+
// Special case handling for NTFS ADS names
884+
try {
885+
FilenameUtils.getExtension("foo.exe:bar.txt");
886+
throw new AssertionError("Expected Exception");
887+
} catch (IllegalArgumentException e) {
888+
assertEquals("NTFS ADS separator (':') in filename is forbidden.", e.getMessage());
889+
}
890+
} else {
891+
// Upwards compatibility:
892+
assertEquals("txt", FilenameUtils.getExtension("foo.exe:bar.txt"));
893+
}
865894
}
866895

867896
@Test
@@ -1082,5 +1111,4 @@ public void testIsExtensionCollection() {
10821111
assertFalse(FilenameUtils.isExtension("a.b\\file.txt", new ArrayList<>(Arrays.asList(new String[]{"TXT"}))));
10831112
assertFalse(FilenameUtils.isExtension("a.b\\file.txt", new ArrayList<>(Arrays.asList(new String[]{"TXT", "RTF"}))));
10841113
}
1085-
10861114
}

0 commit comments

Comments
 (0)