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.
ADD
src/main/java/org/apache/commons/io/FileSystem.java
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.io;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Abstracts an OS' file system details, currently supporting the single use case of converting a file name String to a
+ * legal file name with {@link #toLegalFileName(String, char)}.
+ * <p>
+ * The starting point of any operation is {@link #getCurrent()} which gets you the enum for the file system that matches
+ * the OS hosting the running JVM.
+ * </p>
+ *
+ * @since 2.7
+ */
+public enum FileSystem {
+
+ LINUX(255, 4096, new char[] {
+ // KEEP THIS ARRAY SORTED!
+ // @formatter:off
+ // ASCII NULL
+ 0,
+ '/'
+ // @formatter:on
+ }),
+
+ MAC_OSX(255, 1024, new char[] {
+ // KEEP THIS ARRAY SORTED!
+ // @formatter:off
+ // ASCII NULL
+ 0,
+ '/',
+ ':'
+ // @formatter:on
+ }),
+
+ GENERIC(Integer.MAX_VALUE, Integer.MAX_VALUE, new char[] { 0 }),
+
+ WINDOWS(255, 32000, new char[] {
+ // KEEP THIS ARRAY SORTED!
+ // @formatter:off
+ // ASCII NULL
+ 0,
+ // 1-31 may be allowed in file streams
+ 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,
+ 29, 30, 31,
+ '"', '*', '/', ':', '<', '>', '?', '\\', '|'
+ // @formatter:on
+ });
+
+ /**
+ * The prefix String for all Windows OS.
+ */
+ private static final String OS_NAME_WINDOWS_PREFIX = "Windows";
+
+ /**
+ * <p>
+ * Is {@code true} if this is Windows.
+ * </p>
+ * <p>
+ * The field will return {@code false} if {@code OS_NAME} is {@code null}.
+ * </p>
+ */
+ private static final boolean IS_OS_WINDOWS = getOsMatchesName(OS_NAME_WINDOWS_PREFIX);
+
+ /**
+ * <p>
+ * Is {@code true} if this is Linux.
+ * </p>
+ * <p>
+ * The field will return {@code false} if {@code OS_NAME} is {@code null}.
+ * </p>
+ */
+ private static final boolean IS_OS_LINUX = getOsMatchesName("Linux") || getOsMatchesName("LINUX");
+
+ /**
+ * <p>
+ * Is {@code true} if this is Mac.
+ * </p>
+ * <p>
+ * The field will return {@code false} if {@code OS_NAME} is {@code null}.
+ * </p>
+ */
+ private static final boolean IS_OS_MAC = getOsMatchesName("Mac");
+
+ private static final String OS_NAME = getSystemProperty("os.name");
+
+ public static FileSystem getCurrent() {
+ if (IS_OS_LINUX) {
+ return LINUX;
+ }
+ if (IS_OS_MAC) {
+ return FileSystem.MAC_OSX;
+ }
+ if (IS_OS_WINDOWS) {
+ return FileSystem.WINDOWS;
+ }
+ return GENERIC;
+ }
+
+ /**
+ * Decides if the operating system matches.
+ *
+ * @param osNamePrefix
+ * the prefix for the os name
+ * @return true if matches, or false if not or can't determine
+ */
+ private static boolean getOsMatchesName(final String osNamePrefix) {
+ return isOsNameMatch(OS_NAME, osNamePrefix);
+ }
+
+ /**
+ * <p>
+ * Gets a System property, defaulting to {@code null} if the property cannot be read.
+ * </p>
+ * <p>
+ * If a {@code SecurityException} is caught, the return value is {@code null} and a message is written to
+ * {@code System.err}.
+ * </p>
+ *
+ * @param property
+ * the system property name
+ * @return the system property value or {@code null} if a security problem occurs
+ */
+ private static String getSystemProperty(final String property) {
+ try {
+ return System.getProperty(property);
+ } catch (final SecurityException ex) {
+ // we are not allowed to look at this property
+ System.err.println("Caught a SecurityException reading the system property '" + property
+ + "'; the SystemUtils property value will default to null.");
+ return null;
+ }
+ }
+
+ /**
+ * Decides if the operating system matches.
+ * <p>
+ * This method is package private instead of private to support unit test invocation.
+ * </p>
+ *
+ * @param osName
+ * the actual OS name
+ * @param osNamePrefix
+ * the prefix for the expected OS name
+ * @return true if matches, or false if not or can't determine
+ */
+ private static boolean isOsNameMatch(final String osName, final String osNamePrefix) {
+ if (osName == null) {
+ return false;
+ }
+ return osName.startsWith(osNamePrefix);
+ }
+
+ private final char[] illegalFileNameChars;
+ private final int maxFileNameLength;
+
+ private final int maxPathLength;
+
+ private FileSystem(final int maxFileLength, final int maxPathLength, final char[] illegalFileNameChars) {
+ this.maxFileNameLength = maxFileLength;
+ this.maxPathLength = maxPathLength;
+ this.illegalFileNameChars = Objects.requireNonNull(illegalFileNameChars, "illegalFileNameChars");
+ }
+
+ public char[] getIllegalFileNameChars() {
+ return this.illegalFileNameChars.clone();
+ }
+
+ public int getMaxFileNameLength() {
+ return maxFileNameLength;
+ }
+
+ public int getMaxPathLength() {
+ return maxPathLength;
+ }
+
+ private boolean isIllegalFileNameChar(final char c) {
+ return Arrays.binarySearch(illegalFileNameChars, c) >= 0;
+ }
+
+ /**
+ * Converts a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} to a legal file
+ * name. Illegal characters in the candidate name are replaced by the {@code replacement} character. If the file
+ * name exceeds {@link #getMaxFileNameLength()}, then the name is truncated to {@link #getMaxFileNameLength()}.
+ *
+ * @param candidate
+ * a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"}
+ * @param replacement
+ * Illegal characters in the candidate name are replaced by this character
+ * @return a String without illegal characters
+ */
+ public String toLegalFileName(final String candidate, final char replacement) {
+ if (isIllegalFileNameChar(replacement)) {
+ throw new IllegalArgumentException(
+ String.format("The replacement character '%s' cannot be one of the %s illegal characters: %s",
+ replacement, name(), Arrays.toString(illegalFileNameChars)));
+ }
+ final String truncated = candidate.length() > maxFileNameLength ? candidate.substring(0, maxFileNameLength)
+ : candidate;
+ boolean changed = false;
+ final char[] charArray = truncated.toCharArray();
+ for (int i = 0; i < charArray.length; i++) {
+ if (isIllegalFileNameChar(charArray[i])) {
+ charArray[i] = replacement;
+ changed = true;
+ }
+ }
+ return changed ? String.valueOf(charArray) : truncated;
+ }
+}
\ No newline at end of file

MODIFY
src/main/java/org/apache/commons/io/FilenameUtils.java
src/main/java/org/apache/commons/io/FilenameUtils.java
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
import java.util.Stack;
*/
private static final char UNIX_SEPARATOR = '/';
- /**
- * The characters that are illegal in Windows file names.
- *
- * <ul>
- * <li>&lt; (less than</li>
- * <li>&gt; (greater than</li>
- * <li>: (colon</li>
- * <li>" (double quote</li>
- * <li>/ (forward slash</li>
- * <li>\ (backslash</li>
- * <li>| (vertical bar or pipe</li>
- * <li>? (question mark</li>
- * <li>* (asterisk</li>
- * <li>ASCII NUL (0)</li>
- * <li>Integer characters 1 through 31</li>
- * </ul>
- *
- * @since 2.7
- * @see <a href="https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx">Naming Files,
- * Paths, and Namespaces</a>
- */
- private static final char[] WINDOWS_ILLEGAL_FILE_NAME_CHARS = {
- // KEEP THIS ARRAY SORTED!
- // @formatter:off
- // ASCII NULL
- 0,
- // 1-31 may be allowed in file streams
- 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,
- 29, 30, 31,
- '"', '*', '/', ':', '<', '>', '?', '\\', '|'
- // @formatter:on
- };
-
/**
* The Windows separator character.
*/
@@ -1292,39 +1258,6 @@ public static boolean isExtension(final String filename, final Collection<String
return false;
}
- /**
- * Checks whether the given character is illegal in a Windows file name.
- * <p>
- * The illegal characters are:
- * </p>
- * <ul>
- * <li>&lt; (less than</li>
- * <li>&gt; (greater than</li>
- * <li>: (colon</li>
- * <li>" (double quote</li>
- * <li>/ (forward slash</li>
- * <li>\ (backslash</li>
- * <li>| (vertical bar or pipe</li>
- * <li>? (question mark</li>
- * <li>* (asterisk</li>
- * <li>ASCII NUL (0)</li>
- * <li>Integer characters 1 through 31</li>
- * <li>There may be other characters that the file system does not allow. Please see
- * <a href="https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx">Naming Files, Paths,
- * and Namespaces</a></li>
- * </ul>
- *
- * @see <a href="https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx">Naming Files,
- * Paths, and Namespaces</a>
- * @param c
- * the character to check
- * @return {@code true} if the given character is illegal
- * @since 2.7
- */
- public static boolean isIllegalWindowsFileName(final char c) {
- return Arrays.binarySearch(WINDOWS_ILLEGAL_FILE_NAME_CHARS, c) >= 0;
- }
-
//-----------------------------------------------------------------------
/**
* Checks a filename to see if it matches the specified wildcard matcher,

ADD
src/test/java/org/apache/commons/io/FileSystemTestCase.java
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.io;
+
+import java.util.Arrays;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class FileSystemTestCase {
+
+ @Test
+ public void testToLegalFileNameWindows() {
+ FileSystem fs = FileSystem.WINDOWS;
+ char replacement = '-';
+ for (char i = 0; i < 32; i++) {
+ Assert.assertEquals(replacement, fs.toLegalFileName(String.valueOf(i), replacement).charAt(0));
+ }
+ char[] illegal = new char[] { '<', '>', ':', '"', '/', '\\', '|', '?', '*' };
+ Arrays.sort(illegal);
+ System.out.println(Arrays.toString(illegal));
+ for (char i = 0; i < illegal.length; i++) {
+ Assert.assertEquals(replacement, fs.toLegalFileName(String.valueOf(i), replacement).charAt(0));
+ }
+ for (char i = 'a'; i < 'z'; i++) {
+ Assert.assertEquals(i, fs.toLegalFileName(String.valueOf(i), replacement).charAt(0));
+ }
+ for (char i = 'A'; i < 'Z'; i++) {
+ Assert.assertEquals(i, fs.toLegalFileName(String.valueOf(i), replacement).charAt(0));
+ }
+ for (char i = '0'; i < '9'; i++) {
+ Assert.assertEquals(i, fs.toLegalFileName(String.valueOf(i), replacement).charAt(0));
+ }
+ }
+}

MODIFY
src/test/java/org/apache/commons/io/FilenameUtilsTestCase.java
src/test/java/org/apache/commons/io/FilenameUtilsTestCase.java
@@ -246,28 +246,6 @@ public void testNormalize() throws Exception {
assertEquals(SEP + SEP + "server" + SEP + "", FilenameUtils.normalize("//server/"));
}
- @Test
- public void testIsIllegalWindowsFileName() {
- for (char i = 0; i < 32; i++) {
- assertTrue(FilenameUtils.isIllegalWindowsFileName(i));
- }
- char[] illegal = new char[] { '<', '>', ':', '"', '/', '\\', '|', '?', '*' };
- Arrays.sort(illegal);
- System.out.println(Arrays.toString(illegal));
- for (char i = 0; i < illegal.length; i++) {
- assertTrue(FilenameUtils.isIllegalWindowsFileName(illegal[i]));
- }
- for (char i = 'a'; i < 'z'; i++) {
- assertFalse("i = " + (int) i, FilenameUtils.isIllegalWindowsFileName(i));
- }
- for (char i = 'A'; i < 'Z'; i++) {
- assertFalse("i = " + (int) i, FilenameUtils.isIllegalWindowsFileName(i));
- }
- for (char i = '0'; i < '9'; i++) {
- assertFalse("i = " + (int) i, FilenameUtils.isIllegalWindowsFileName(i));
- }
- }
-
@Test
public void testNormalize_with_nullbytes() throws Exception {
try {

[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.