Skip to content

Commit 6e2ac19

Browse files
committed
[IO-555] Recasting this issue away from a new method in FilenameUtils to
a solution to convert Strings to legal files names with the new enum FileSystem.
1 parent 5e2ace2 commit 6e2ac19

4 files changed

Lines changed: 278 additions & 89 deletions

File tree

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.commons.io;
19+
20+
import java.util.Arrays;
21+
import java.util.Objects;
22+
23+
/**
24+
* Abstracts an OS' file system details, currently supporting the single use case of converting a file name String to a
25+
* legal file name with {@link #toLegalFileName(String, char)}.
26+
* <p>
27+
* The starting point of any operation is {@link #getCurrent()} which gets you the enum for the file system that matches
28+
* the OS hosting the running JVM.
29+
* </p>
30+
*
31+
* @since 2.7
32+
*/
33+
public enum FileSystem {
34+
35+
LINUX(255, 4096, new char[] {
36+
// KEEP THIS ARRAY SORTED!
37+
// @formatter:off
38+
// ASCII NULL
39+
0,
40+
'/'
41+
// @formatter:on
42+
}),
43+
44+
MAC_OSX(255, 1024, new char[] {
45+
// KEEP THIS ARRAY SORTED!
46+
// @formatter:off
47+
// ASCII NULL
48+
0,
49+
'/',
50+
':'
51+
// @formatter:on
52+
}),
53+
54+
GENERIC(Integer.MAX_VALUE, Integer.MAX_VALUE, new char[] { 0 }),
55+
56+
WINDOWS(255, 32000, new char[] {
57+
// KEEP THIS ARRAY SORTED!
58+
// @formatter:off
59+
// ASCII NULL
60+
0,
61+
// 1-31 may be allowed in file streams
62+
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
63+
29, 30, 31,
64+
'"', '*', '/', ':', '<', '>', '?', '\\', '|'
65+
// @formatter:on
66+
});
67+
68+
/**
69+
* The prefix String for all Windows OS.
70+
*/
71+
private static final String OS_NAME_WINDOWS_PREFIX = "Windows";
72+
73+
/**
74+
* <p>
75+
* Is {@code true} if this is Windows.
76+
* </p>
77+
* <p>
78+
* The field will return {@code false} if {@code OS_NAME} is {@code null}.
79+
* </p>
80+
*/
81+
private static final boolean IS_OS_WINDOWS = getOsMatchesName(OS_NAME_WINDOWS_PREFIX);
82+
83+
/**
84+
* <p>
85+
* Is {@code true} if this is Linux.
86+
* </p>
87+
* <p>
88+
* The field will return {@code false} if {@code OS_NAME} is {@code null}.
89+
* </p>
90+
*/
91+
private static final boolean IS_OS_LINUX = getOsMatchesName("Linux") || getOsMatchesName("LINUX");
92+
93+
/**
94+
* <p>
95+
* Is {@code true} if this is Mac.
96+
* </p>
97+
* <p>
98+
* The field will return {@code false} if {@code OS_NAME} is {@code null}.
99+
* </p>
100+
*/
101+
private static final boolean IS_OS_MAC = getOsMatchesName("Mac");
102+
103+
private static final String OS_NAME = getSystemProperty("os.name");
104+
105+
public static FileSystem getCurrent() {
106+
if (IS_OS_LINUX) {
107+
return LINUX;
108+
}
109+
if (IS_OS_MAC) {
110+
return FileSystem.MAC_OSX;
111+
}
112+
if (IS_OS_WINDOWS) {
113+
return FileSystem.WINDOWS;
114+
}
115+
return GENERIC;
116+
}
117+
118+
/**
119+
* Decides if the operating system matches.
120+
*
121+
* @param osNamePrefix
122+
* the prefix for the os name
123+
* @return true if matches, or false if not or can't determine
124+
*/
125+
private static boolean getOsMatchesName(final String osNamePrefix) {
126+
return isOsNameMatch(OS_NAME, osNamePrefix);
127+
}
128+
129+
/**
130+
* <p>
131+
* Gets a System property, defaulting to {@code null} if the property cannot be read.
132+
* </p>
133+
* <p>
134+
* If a {@code SecurityException} is caught, the return value is {@code null} and a message is written to
135+
* {@code System.err}.
136+
* </p>
137+
*
138+
* @param property
139+
* the system property name
140+
* @return the system property value or {@code null} if a security problem occurs
141+
*/
142+
private static String getSystemProperty(final String property) {
143+
try {
144+
return System.getProperty(property);
145+
} catch (final SecurityException ex) {
146+
// we are not allowed to look at this property
147+
System.err.println("Caught a SecurityException reading the system property '" + property
148+
+ "'; the SystemUtils property value will default to null.");
149+
return null;
150+
}
151+
}
152+
153+
/**
154+
* Decides if the operating system matches.
155+
* <p>
156+
* This method is package private instead of private to support unit test invocation.
157+
* </p>
158+
*
159+
* @param osName
160+
* the actual OS name
161+
* @param osNamePrefix
162+
* the prefix for the expected OS name
163+
* @return true if matches, or false if not or can't determine
164+
*/
165+
private static boolean isOsNameMatch(final String osName, final String osNamePrefix) {
166+
if (osName == null) {
167+
return false;
168+
}
169+
return osName.startsWith(osNamePrefix);
170+
}
171+
172+
private final char[] illegalFileNameChars;
173+
private final int maxFileNameLength;
174+
175+
private final int maxPathLength;
176+
177+
private FileSystem(final int maxFileLength, final int maxPathLength, final char[] illegalFileNameChars) {
178+
this.maxFileNameLength = maxFileLength;
179+
this.maxPathLength = maxPathLength;
180+
this.illegalFileNameChars = Objects.requireNonNull(illegalFileNameChars, "illegalFileNameChars");
181+
}
182+
183+
public char[] getIllegalFileNameChars() {
184+
return this.illegalFileNameChars.clone();
185+
}
186+
187+
public int getMaxFileNameLength() {
188+
return maxFileNameLength;
189+
}
190+
191+
public int getMaxPathLength() {
192+
return maxPathLength;
193+
}
194+
195+
private boolean isIllegalFileNameChar(final char c) {
196+
return Arrays.binarySearch(illegalFileNameChars, c) >= 0;
197+
}
198+
199+
/**
200+
* Converts a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} to a legal file
201+
* name. Illegal characters in the candidate name are replaced by the {@code replacement} character. If the file
202+
* name exceeds {@link #getMaxFileNameLength()}, then the name is truncated to {@link #getMaxFileNameLength()}.
203+
*
204+
* @param candidate
205+
* a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"}
206+
* @param replacement
207+
* Illegal characters in the candidate name are replaced by this character
208+
* @return a String without illegal characters
209+
*/
210+
public String toLegalFileName(final String candidate, final char replacement) {
211+
if (isIllegalFileNameChar(replacement)) {
212+
throw new IllegalArgumentException(
213+
String.format("The replacement character '%s' cannot be one of the %s illegal characters: %s",
214+
replacement, name(), Arrays.toString(illegalFileNameChars)));
215+
}
216+
final String truncated = candidate.length() > maxFileNameLength ? candidate.substring(0, maxFileNameLength)
217+
: candidate;
218+
boolean changed = false;
219+
final char[] charArray = truncated.toCharArray();
220+
for (int i = 0; i < charArray.length; i++) {
221+
if (isIllegalFileNameChar(charArray[i])) {
222+
charArray[i] = replacement;
223+
changed = true;
224+
}
225+
}
226+
return changed ? String.valueOf(charArray) : truncated;
227+
}
228+
}

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

Lines changed: 0 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.io.File;
2020
import java.io.IOException;
2121
import java.util.ArrayList;
22-
import java.util.Arrays;
2322
import java.util.Collection;
2423
import java.util.Stack;
2524

@@ -101,39 +100,6 @@ public class FilenameUtils {
101100
*/
102101
private static final char UNIX_SEPARATOR = '/';
103102

104-
/**
105-
* The characters that are illegal in Windows file names.
106-
*
107-
* <ul>
108-
* <li>&lt; (less than</li>
109-
* <li>&gt; (greater than</li>
110-
* <li>: (colon</li>
111-
* <li>" (double quote</li>
112-
* <li>/ (forward slash</li>
113-
* <li>\ (backslash</li>
114-
* <li>| (vertical bar or pipe</li>
115-
* <li>? (question mark</li>
116-
* <li>* (asterisk</li>
117-
* <li>ASCII NUL (0)</li>
118-
* <li>Integer characters 1 through 31</li>
119-
* </ul>
120-
*
121-
* @since 2.7
122-
* @see <a href="https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx">Naming Files,
123-
* Paths, and Namespaces</a>
124-
*/
125-
private static final char[] WINDOWS_ILLEGAL_FILE_NAME_CHARS = {
126-
// KEEP THIS ARRAY SORTED!
127-
// @formatter:off
128-
// ASCII NULL
129-
0,
130-
// 1-31 may be allowed in file streams
131-
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
132-
29, 30, 31,
133-
'"', '*', '/', ':', '<', '>', '?', '\\', '|'
134-
// @formatter:on
135-
};
136-
137103
/**
138104
* The Windows separator character.
139105
*/
@@ -1292,39 +1258,6 @@ public static boolean isExtension(final String filename, final Collection<String
12921258
return false;
12931259
}
12941260

1295-
/**
1296-
* Checks whether the given character is illegal in a Windows file name.
1297-
* <p>
1298-
* The illegal characters are:
1299-
* </p>
1300-
* <ul>
1301-
* <li>&lt; (less than</li>
1302-
* <li>&gt; (greater than</li>
1303-
* <li>: (colon</li>
1304-
* <li>" (double quote</li>
1305-
* <li>/ (forward slash</li>
1306-
* <li>\ (backslash</li>
1307-
* <li>| (vertical bar or pipe</li>
1308-
* <li>? (question mark</li>
1309-
* <li>* (asterisk</li>
1310-
* <li>ASCII NUL (0)</li>
1311-
* <li>Integer characters 1 through 31</li>
1312-
* <li>There may be other characters that the file system does not allow. Please see
1313-
* <a href="https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx">Naming Files, Paths,
1314-
* and Namespaces</a></li>
1315-
* </ul>
1316-
*
1317-
* @see <a href="https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx">Naming Files,
1318-
* Paths, and Namespaces</a>
1319-
* @param c
1320-
* the character to check
1321-
* @return {@code true} if the given character is illegal
1322-
* @since 2.7
1323-
*/
1324-
public static boolean isIllegalWindowsFileName(final char c) {
1325-
return Arrays.binarySearch(WINDOWS_ILLEGAL_FILE_NAME_CHARS, c) >= 0;
1326-
}
1327-
13281261
//-----------------------------------------------------------------------
13291262
/**
13301263
* Checks a filename to see if it matches the specified wildcard matcher,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.commons.io;
19+
20+
import java.util.Arrays;
21+
22+
import org.junit.Assert;
23+
import org.junit.Test;
24+
25+
public class FileSystemTestCase {
26+
27+
@Test
28+
public void testToLegalFileNameWindows() {
29+
FileSystem fs = FileSystem.WINDOWS;
30+
char replacement = '-';
31+
for (char i = 0; i < 32; i++) {
32+
Assert.assertEquals(replacement, fs.toLegalFileName(String.valueOf(i), replacement).charAt(0));
33+
}
34+
char[] illegal = new char[] { '<', '>', ':', '"', '/', '\\', '|', '?', '*' };
35+
Arrays.sort(illegal);
36+
System.out.println(Arrays.toString(illegal));
37+
for (char i = 0; i < illegal.length; i++) {
38+
Assert.assertEquals(replacement, fs.toLegalFileName(String.valueOf(i), replacement).charAt(0));
39+
}
40+
for (char i = 'a'; i < 'z'; i++) {
41+
Assert.assertEquals(i, fs.toLegalFileName(String.valueOf(i), replacement).charAt(0));
42+
}
43+
for (char i = 'A'; i < 'Z'; i++) {
44+
Assert.assertEquals(i, fs.toLegalFileName(String.valueOf(i), replacement).charAt(0));
45+
}
46+
for (char i = '0'; i < '9'; i++) {
47+
Assert.assertEquals(i, fs.toLegalFileName(String.valueOf(i), replacement).charAt(0));
48+
}
49+
}
50+
}

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

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -246,28 +246,6 @@ public void testNormalize() throws Exception {
246246
assertEquals(SEP + SEP + "server" + SEP + "", FilenameUtils.normalize("//server/"));
247247
}
248248

249-
@Test
250-
public void testIsIllegalWindowsFileName() {
251-
for (char i = 0; i < 32; i++) {
252-
assertTrue(FilenameUtils.isIllegalWindowsFileName(i));
253-
}
254-
char[] illegal = new char[] { '<', '>', ':', '"', '/', '\\', '|', '?', '*' };
255-
Arrays.sort(illegal);
256-
System.out.println(Arrays.toString(illegal));
257-
for (char i = 0; i < illegal.length; i++) {
258-
assertTrue(FilenameUtils.isIllegalWindowsFileName(illegal[i]));
259-
}
260-
for (char i = 'a'; i < 'z'; i++) {
261-
assertFalse("i = " + (int) i, FilenameUtils.isIllegalWindowsFileName(i));
262-
}
263-
for (char i = 'A'; i < 'Z'; i++) {
264-
assertFalse("i = " + (int) i, FilenameUtils.isIllegalWindowsFileName(i));
265-
}
266-
for (char i = '0'; i < '9'; i++) {
267-
assertFalse("i = " + (int) i, FilenameUtils.isIllegalWindowsFileName(i));
268-
}
269-
}
270-
271249
@Test
272250
public void testNormalize_with_nullbytes() throws Exception {
273251
try {

0 commit comments

Comments
 (0)