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.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(4096, false, false, Integer.MAX_VALUE, Integer.MAX_VALUE, new int[] { 0 }, new String[] {}, false, false, '/'),
- /**
- * Linux file system.
- */
- LINUX(8192, 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(4096, 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>
- */
- // @formatter:off
- WINDOWS(4096, false, true,
- 255, 32000, // KEEP THIS ARRAY SORTED!
- new int[] {
- // KEEP THIS ARRAY SORTED!
- // 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,
- '"', '*', '/', ':', '<', '>', '?', '\\', '|'
- }, new String[] {
- "AUX",
- "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
- "COM\u00b2", "COM\u00b3", "COM\u00b9", // Superscript 2 3 1 in that order
- "CON", "CONIN$", "CONOUT$",
- "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
- "LPT\u00b2", "LPT\u00b3", "LPT\u00b9", // Superscript 2 3 1 in that order
- "NUL", "PRN"
- }, true, true, '\\');
- // @formatter:on
- /**
- * <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 <em>k</em> such that:
- * </p>
- * <blockquote><pre>
- * (this.charAt(<em>k</em>) == searchChar) && (<em>k</em> >= start)
- * </pre></blockquote>
- * is true. For other values of {@code searchChar}, it is the
- * smallest value <em>k</em> such that:
- * <blockquote><pre>
- * (this.codePointAt(<em>k</em>) == searchChar) && (<em>k</em> >= start)
- * </pre></blockquote>
- * <p>
- * is true. In either case, if no such character occurs in {@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 int blockSize;
- 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 blockSize file allocation block size in bytes.
- * @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 int blockSize, 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.blockSize = blockSize;
- this.maxFileNameLength = maxFileLength;
- this.maxPathLength = maxPathLength;
- this.illegalFileNameChars = Objects.requireNonNull(illegalFileNameChars, "illegalFileNameChars");
- this.reservedFileNames = Objects.requireNonNull(reservedFileNames, "reservedFileNames");
- //Arrays.sort(this.reservedFileNames);
- this.reservedFileNamesExtensions = reservedFileNamesExtensions;
- this.caseSensitive = caseSensitive;
- this.casePreserving = casePreserving;
- this.supportsDriveLetter = supportsDriveLetter;
- this.nameSeparator = nameSeparator;
- this.nameSeparatorOther = FilenameUtils.flipSeparator(nameSeparator);
- }
- /**
- * Gets the file allocation block size in bytes.
- * @return the file allocation block size in bytes.
- * @since 2.12.0
- */
- public int getBlockSize() {
- return blockSize;
- }
- /**
- * 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);
- }
- }