aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/org/apache/commons/io/FileSystem.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/org/apache/commons/io/FileSystem.java')
-rw-r--r--src/main/java/org/apache/commons/io/FileSystem.java514
1 files changed, 514 insertions, 0 deletions
diff --git a/src/main/java/org/apache/commons/io/FileSystem.java b/src/main/java/org/apache/commons/io/FileSystem.java
new file mode 100644
index 00000000..42f51672
--- /dev/null
+++ b/src/main/java/org/apache/commons/io/FileSystem.java
@@ -0,0 +1,514 @@
+/*
+ * 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.Locale;
+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 {
+
+ /**
+ * Generic file system.
+ */
+ GENERIC(false, false, Integer.MAX_VALUE, Integer.MAX_VALUE, new int[] { 0 }, new String[] {}, false, false, '/'),
+
+ /**
+ * Linux file system.
+ */
+ LINUX(true, true, 255, 4096, new int[] {
+ // KEEP THIS ARRAY SORTED!
+ // @formatter:off
+ // ASCII NUL
+ 0,
+ '/'
+ // @formatter:on
+ }, new String[] {}, false, false, '/'),
+
+ /**
+ * MacOS file system.
+ */
+ MAC_OSX(true, true, 255, 1024, new int[] {
+ // KEEP THIS ARRAY SORTED!
+ // @formatter:off
+ // ASCII NUL
+ 0,
+ '/',
+ ':'
+ // @formatter:on
+ }, new String[] {}, false, false, '/'),
+
+ /**
+ * Windows file system.
+ * <p>
+ * The reserved characters are defined in the
+ * <a href="https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file">Naming Conventions
+ * (microsoft.com)</a>.
+ * </p>
+ *
+ * @see <a href="https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file">Naming Conventions
+ * (microsoft.com)</a>
+ * @see <a href="https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles">
+ * CreateFileA function - Consoles (microsoft.com)</a>
+ */
+ WINDOWS(false, true, 255,
+ 32000, new int[] {
+ // KEEP THIS ARRAY SORTED!
+ // @formatter:off
+ // ASCII NUL
+ 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
+ }, // KEEP THIS ARRAY SORTED!
+ new String[] { "AUX", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "CON", "CONIN$", "CONOUT$",
+ "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "NUL", "PRN" }, true, true, '\\');
+
+ /**
+ * <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");
+
+ /**
+ * <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");
+
+ /**
+ * 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);
+
+ /**
+ * The current FileSystem.
+ */
+ private static final FileSystem CURRENT = current();
+
+ /**
+ * Gets the current file system.
+ *
+ * @return the current file system
+ */
+ private static FileSystem current() {
+ if (IS_OS_LINUX) {
+ return LINUX;
+ }
+ if (IS_OS_MAC) {
+ return MAC_OSX;
+ }
+ if (IS_OS_WINDOWS) {
+ return WINDOWS;
+ }
+ return GENERIC;
+ }
+
+ /**
+ * Gets the current file system.
+ *
+ * @return the current file system
+ */
+ public static FileSystem getCurrent() {
+ return CURRENT;
+ }
+
+ /**
+ * 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(getSystemProperty("os.name"), osNamePrefix);
+ }
+
+ /**
+ * <p>
+ * Gets a System property, defaulting to {@code null} if the property cannot be read.
+ * </p>
+ * <p>
+ * If a {@link 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;
+ }
+ }
+
+ /**
+ * Copied from Apache Commons Lang CharSequenceUtils.
+ *
+ * Returns the index within {@code cs} of the first occurrence of the
+ * specified character, starting the search at the specified index.
+ * <p>
+ * If a character with value {@code searchChar} occurs in the
+ * character sequence represented by the {@code cs}
+ * object at an index no smaller than {@code start}, then
+ * the index of the first such occurrence is returned. For values
+ * of {@code searchChar} in the range from 0 to 0xFFFF (inclusive),
+ * this is the smallest value <i>k</i> such that:
+ * </p>
+ * <blockquote><pre>
+ * (this.charAt(<i>k</i>) == searchChar) &amp;&amp; (<i>k</i> &gt;= start)
+ * </pre></blockquote>
+ * is true. For other values of {@code searchChar}, it is the
+ * smallest value <i>k</i> such that:
+ * <blockquote><pre>
+ * (this.codePointAt(<i>k</i>) == searchChar) &amp;&amp; (<i>k</i> &gt;= start)
+ * </pre></blockquote>
+ * <p>
+ * is true. In either case, if no such character occurs inm {@code cs}
+ * at or after position {@code start}, then
+ * {@code -1} is returned.
+ * </p>
+ * <p>
+ * There is no restriction on the value of {@code start}. If it
+ * is negative, it has the same effect as if it were zero: the entire
+ * {@link CharSequence} may be searched. If it is greater than
+ * the length of {@code cs}, it has the same effect as if it were
+ * equal to the length of {@code cs}: {@code -1} is returned.
+ * </p>
+ * <p>All indices are specified in {@code char} values
+ * (Unicode code units).
+ * </p>
+ *
+ * @param cs the {@link CharSequence} to be processed, not null
+ * @param searchChar the char to be searched for
+ * @param start the start index, negative starts at the string start
+ * @return the index where the search char was found, -1 if not found
+ * @since 3.6 updated to behave more like {@link String}
+ */
+ private static int indexOf(final CharSequence cs, final int searchChar, int start) {
+ if (cs instanceof String) {
+ return ((String) cs).indexOf(searchChar, start);
+ }
+ final int sz = cs.length();
+ if (start < 0) {
+ start = 0;
+ }
+ if (searchChar < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
+ for (int i = start; i < sz; i++) {
+ if (cs.charAt(i) == searchChar) {
+ return i;
+ }
+ }
+ return -1;
+ }
+ //supplementary characters (LANG1300)
+ if (searchChar <= Character.MAX_CODE_POINT) {
+ final char[] chars = Character.toChars(searchChar);
+ for (int i = start; i < sz - 1; i++) {
+ final char high = cs.charAt(i);
+ final char low = cs.charAt(i + 1);
+ if (high == chars[0] && low == chars[1]) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * 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.toUpperCase(Locale.ROOT).startsWith(osNamePrefix.toUpperCase(Locale.ROOT));
+ }
+
+ /**
+ * Null-safe replace.
+ *
+ * @param path the path to be changed, null ignored.
+ * @param oldChar the old character.
+ * @param newChar the new character.
+ * @return the new path.
+ */
+ private static String replace(final String path, final char oldChar, final char newChar) {
+ return path == null ? null : path.replace(oldChar, newChar);
+ }
+ private final boolean casePreserving;
+ private final boolean caseSensitive;
+ private final int[] illegalFileNameChars;
+ private final int maxFileNameLength;
+ private final int maxPathLength;
+ private final String[] reservedFileNames;
+ private final boolean reservedFileNamesExtensions;
+ private final boolean supportsDriveLetter;
+ private final char nameSeparator;
+
+ private final char nameSeparatorOther;
+
+ /**
+ * Constructs a new instance.
+ *
+ * @param caseSensitive Whether this file system is case-sensitive.
+ * @param casePreserving Whether this file system is case-preserving.
+ * @param maxFileLength The maximum length for file names. The file name does not include folders.
+ * @param maxPathLength The maximum length of the path to a file. This can include folders.
+ * @param illegalFileNameChars Illegal characters for this file system.
+ * @param reservedFileNames The reserved file names.
+ * @param reservedFileNamesExtensions TODO
+ * @param supportsDriveLetter Whether this file system support driver letters.
+ * @param nameSeparator The name separator, '\\' on Windows, '/' on Linux.
+ */
+ FileSystem(final boolean caseSensitive, final boolean casePreserving, final int maxFileLength,
+ final int maxPathLength, final int[] illegalFileNameChars, final String[] reservedFileNames,
+ final boolean reservedFileNamesExtensions, final boolean supportsDriveLetter, final char nameSeparator) {
+ this.maxFileNameLength = maxFileLength;
+ this.maxPathLength = maxPathLength;
+ this.illegalFileNameChars = Objects.requireNonNull(illegalFileNameChars, "illegalFileNameChars");
+ this.reservedFileNames = Objects.requireNonNull(reservedFileNames, "reservedFileNames");
+ this.reservedFileNamesExtensions = reservedFileNamesExtensions;
+ this.caseSensitive = caseSensitive;
+ this.casePreserving = casePreserving;
+ this.supportsDriveLetter = supportsDriveLetter;
+ this.nameSeparator = nameSeparator;
+ this.nameSeparatorOther = FilenameUtils.flipSeparator(nameSeparator);
+ }
+
+ /**
+ * Gets a cloned copy of the illegal characters for this file system.
+ *
+ * @return the illegal characters for this file system.
+ */
+ public char[] getIllegalFileNameChars() {
+ final char[] chars = new char[illegalFileNameChars.length];
+ for (int i = 0; i < illegalFileNameChars.length; i++) {
+ chars[i] = (char) illegalFileNameChars[i];
+ }
+ return chars;
+ }
+
+ /**
+ * Gets a cloned copy of the illegal code points for this file system.
+ *
+ * @return the illegal code points for this file system.
+ * @since 2.12.0
+ */
+ public int[] getIllegalFileNameCodePoints() {
+ return this.illegalFileNameChars.clone();
+ }
+
+ /**
+ * Gets the maximum length for file names. The file name does not include folders.
+ *
+ * @return the maximum length for file names.
+ */
+ public int getMaxFileNameLength() {
+ return maxFileNameLength;
+ }
+
+ /**
+ * Gets the maximum length of the path to a file. This can include folders.
+ *
+ * @return the maximum length of the path to a file.
+ */
+ public int getMaxPathLength() {
+ return maxPathLength;
+ }
+
+ /**
+ * Gets the name separator, '\\' on Windows, '/' on Linux.
+ *
+ * @return '\\' on Windows, '/' on Linux.
+ *
+ * @since 2.12.0
+ */
+ public char getNameSeparator() {
+ return nameSeparator;
+ }
+
+ /**
+ * Gets a cloned copy of the reserved file names.
+ *
+ * @return the reserved file names.
+ */
+ public String[] getReservedFileNames() {
+ return reservedFileNames.clone();
+ }
+
+ /**
+ * Tests whether this file system preserves case.
+ *
+ * @return Whether this file system preserves case.
+ */
+ public boolean isCasePreserving() {
+ return casePreserving;
+ }
+
+ /**
+ * Tests whether this file system is case-sensitive.
+ *
+ * @return Whether this file system is case-sensitive.
+ */
+ public boolean isCaseSensitive() {
+ return caseSensitive;
+ }
+
+ /**
+ * Tests if the given character is illegal in a file name, {@code false} otherwise.
+ *
+ * @param c
+ * the character to test
+ * @return {@code true} if the given character is illegal in a file name, {@code false} otherwise.
+ */
+ private boolean isIllegalFileNameChar(final int c) {
+ return Arrays.binarySearch(illegalFileNameChars, c) >= 0;
+ }
+
+ /**
+ * Tests if a candidate file name (without a path) such as {@code "filename.ext"} or {@code "filename"} is a
+ * potentially legal file name. If the file name length exceeds {@link #getMaxFileNameLength()}, or if it contains
+ * an illegal character then the check fails.
+ *
+ * @param candidate
+ * a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"}
+ * @return {@code true} if the candidate name is legal
+ */
+ public boolean isLegalFileName(final CharSequence candidate) {
+ if (candidate == null || candidate.length() == 0 || candidate.length() > maxFileNameLength) {
+ return false;
+ }
+ if (isReservedFileName(candidate)) {
+ return false;
+ }
+ return candidate.chars().noneMatch(this::isIllegalFileNameChar);
+ }
+
+ /**
+ * Tests whether the given string is a reserved file name.
+ *
+ * @param candidate
+ * the string to test
+ * @return {@code true} if the given string is a reserved file name.
+ */
+ public boolean isReservedFileName(final CharSequence candidate) {
+ final CharSequence test = reservedFileNamesExtensions ? trimExtension(candidate) : candidate;
+ return Arrays.binarySearch(reservedFileNames, test) >= 0;
+ }
+
+ /**
+ * Converts all separators to the Windows separator of backslash.
+ *
+ * @param path the path to be changed, null ignored
+ * @return the updated path
+ * @since 2.12.0
+ */
+ public String normalizeSeparators(final String path) {
+ return replace(path, nameSeparatorOther, nameSeparator);
+ }
+
+ /**
+ * Tests whether this file system support driver letters.
+ * <p>
+ * Windows supports driver letters as do other operating systems. Whether these other OS's still support Java like
+ * OS/2, is a different matter.
+ * </p>
+ *
+ * @return whether this file system support driver letters.
+ * @since 2.9.0
+ * @see <a href="https://en.wikipedia.org/wiki/Drive_letter_assignment">Operating systems that use drive letter
+ * assignment</a>
+ */
+ public boolean supportsDriveLetter() {
+ return supportsDriveLetter;
+ }
+
+ /**
+ * 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 length 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)) {
+ // %s does not work properly with NUL
+ throw new IllegalArgumentException(String.format("The replacement character '%s' cannot be one of the %s illegal characters: %s",
+ replacement == '\0' ? "\\0" : replacement, name(), Arrays.toString(illegalFileNameChars)));
+ }
+ final String truncated = candidate.length() > maxFileNameLength ? candidate.substring(0, maxFileNameLength) : candidate;
+ final int[] array = truncated.chars().map(i -> isIllegalFileNameChar(i) ? replacement : i).toArray();
+ return new String(array, 0, array.length);
+ }
+
+ CharSequence trimExtension(final CharSequence cs) {
+ final int index = indexOf(cs, '.', 0);
+ return index < 0 ? cs : cs.subSequence(0, index);
+ }
+}