    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018package org.apache.commons.io;
    019
    020import java.util.Arrays;
    021import java.util.Locale;
    022import java.util.Objects;
    023
    024/**
    025 * Abstracts an OS' file system details, currently supporting the single use case of converting a file name String to a
    026 * legal file name with {@link #toLegalFileName(String, char)}.
    027 * <p>
    028 * The starting point of any operation is {@link #getCurrent()} which gets you the enum for the file system that matches
    029 * the OS hosting the running JVM.
    030 * </p>
    031 *
    032 * @since 2.7
    033 */
    034public enum FileSystem {
    035
    036    /**
    037     * Generic file system.
    038     */
    039    GENERIC(4096, false, false, Integer.MAX_VALUE, Integer.MAX_VALUE, new int[] { 0 }, new String[] {}, false, false, '/'),
    040
    041    /**
    042     * Linux file system.
    043     */
    044    LINUX(8192, true, true, 255, 4096, new int[] {
    045            // KEEP THIS ARRAY SORTED!
    046            // @formatter:off
    047            // ASCII NUL
    048            0,
    049             '/'
    050            // @formatter:on
    051    }, new String[] {}, false, false, '/'),
    052
    053    /**
    054     * MacOS file system.
    055     */
    056    MAC_OSX(4096, true, true, 255, 1024, new int[] {
    057            // KEEP THIS ARRAY SORTED!
    058            // @formatter:off
    059            // ASCII NUL
    060            0,
    061            '/',
    062             ':'
    063            // @formatter:on
    064    }, new String[] {}, false, false, '/'),
    065
    066    /**
    067     * Windows file system.
    068     * <p>
    069     * The reserved characters are defined in the
    070     * <a href="https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file">Naming Conventions
    071     * (microsoft.com)</a>.
    072     * </p>
    073     *
    074     * @see <a href="https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file">Naming Conventions
    075     *      (microsoft.com)</a>
    076     * @see <a href="https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles">
    077     *      CreateFileA function - Consoles (microsoft.com)</a>
    078     */
    079    WINDOWS(4096, false, true,
    080            255, 32000, // KEEP THIS ARRAY SORTED!
    081            new int[] {
    082                    // KEEP THIS ARRAY SORTED!
    083                    // @formatter:off
    084                    // ASCII NUL
    085                    0,
    086                    // 1-31 may be allowed in file streams
    087                    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,
    088                    29, 30, 31,
    089                    '"', '*', '/', ':', '<', '>', '?', '\\', '|'
    090                    // @formatter:on
    091            }, new String[] { "AUX", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "CON", "CONIN$", "CONOUT$",
    092                            "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "NUL", "PRN" }, true, true, '\\');
    093
    094    /**
    095     * <p>
    096     * Is {@code true} if this is Linux.
    097     * </p>
    098     * <p>
    099     * The field will return {@code false} if {@code OS_NAME} is {@code null}.
    100     * </p>
    101     */
    102    private static final boolean IS_OS_LINUX = getOsMatchesName("Linux");
    103
    104    /**
    105     * <p>
    106     * Is {@code true} if this is Mac.
    107     * </p>
    108     * <p>
    109     * The field will return {@code false} if {@code OS_NAME} is {@code null}.
    110     * </p>
    111     */
    112    private static final boolean IS_OS_MAC = getOsMatchesName("Mac");
    113
    114    /**
    115     * The prefix String for all Windows OS.
    116     */
    117    private static final String OS_NAME_WINDOWS_PREFIX = "Windows";
    118
    119    /**
    120     * <p>
    121     * Is {@code true} if this is Windows.
    122     * </p>
    123     * <p>
    124     * The field will return {@code false} if {@code OS_NAME} is {@code null}.
    125     * </p>
    126     */
    127    private static final boolean IS_OS_WINDOWS = getOsMatchesName(OS_NAME_WINDOWS_PREFIX);
    128
    129    /**
    130     * The current FileSystem.
    131     */
    132    private static final FileSystem CURRENT = current();
    133
    134    /**
    135     * Gets the current file system.
    136     *
    137     * @return the current file system
    138     */
    139    private static FileSystem current() {
    140        if (IS_OS_LINUX) {
    141            return LINUX;
    142        }
    143        if (IS_OS_MAC) {
    144            return MAC_OSX;
    145        }
    146        if (IS_OS_WINDOWS) {
    147            return WINDOWS;
    148        }
    149        return GENERIC;
    150    }
    151
    152    /**
    153     * Gets the current file system.
    154     *
    155     * @return the current file system
    156     */
    157    public static FileSystem getCurrent() {
    158        return CURRENT;
    159    }
    160
    161    /**
    162     * Decides if the operating system matches.
    163     *
    164     * @param osNamePrefix
    165     *            the prefix for the os name
    166     * @return true if matches, or false if not or can't determine
    167     */
    168    private static boolean getOsMatchesName(final String osNamePrefix) {
    169        return isOsNameMatch(getSystemProperty("os.name"), osNamePrefix);
    170    }
    171
    172    /**
    173     * <p>
    174     * Gets a System property, defaulting to {@code null} if the property cannot be read.
    175     * </p>
    176     * <p>
    177     * If a {@link SecurityException} is caught, the return value is {@code null} and a message is written to
    178     * {@code System.err}.
    179     * </p>
    180     *
    181     * @param property
    182     *            the system property name
    183     * @return the system property value or {@code null} if a security problem occurs
    184     */
    185    private static String getSystemProperty(final String property) {
    186        try {
    187            return System.getProperty(property);
    188        } catch (final SecurityException ex) {
    189            // we are not allowed to look at this property
    190            System.err.println("Caught a SecurityException reading the system property '" + property
    191                    + "'; the SystemUtils property value will default to null.");
    192            return null;
    193        }
    194    }
    195
    196    /**
    197     * Copied from Apache Commons Lang CharSequenceUtils.
    198     *
    199     * Returns the index within {@code cs} of the first occurrence of the
    200     * specified character, starting the search at the specified index.
    201     * <p>
    202     * If a character with value {@code searchChar} occurs in the
    203     * character sequence represented by the {@code cs}
    204     * object at an index no smaller than {@code start}, then
    205     * the index of the first such occurrence is returned. For values
    206     * of {@code searchChar} in the range from 0 to 0xFFFF (inclusive),
    207     * this is the smallest value <i>k</i> such that:
    208     * </p>
    209     * <blockquote><pre>
    210     * (this.charAt(<i>k</i>) == searchChar) &amp;&amp; (<i>k</i> &gt;= start)
    211     * </pre></blockquote>
    212     * is true. For other values of {@code searchChar}, it is the
    213     * smallest value <i>k</i> such that:
    214     * <blockquote><pre>
    215     * (this.codePointAt(<i>k</i>) == searchChar) &amp;&amp; (<i>k</i> &gt;= start)
    216     * </pre></blockquote>
    217     * <p>
    218     * is true. In either case, if no such character occurs inm {@code cs}
    219     * at or after position {@code start}, then
    220     * {@code -1} is returned.
    221     * </p>
    222     * <p>
    223     * There is no restriction on the value of {@code start}. If it
    224     * is negative, it has the same effect as if it were zero: the entire
    225     * {@link CharSequence} may be searched. If it is greater than
    226     * the length of {@code cs}, it has the same effect as if it were
    227     * equal to the length of {@code cs}: {@code -1} is returned.
    228     * </p>
    229     * <p>All indices are specified in {@code char} values
    230     * (Unicode code units).
    231     * </p>
    232     *
    233     * @param cs  the {@link CharSequence} to be processed, not null
    234     * @param searchChar  the char to be searched for
    235     * @param start  the start index, negative starts at the string start
    236     * @return the index where the search char was found, -1 if not found
    237     * @since 3.6 updated to behave more like {@link String}
    238     */
    239    private static int indexOf(final CharSequence cs, final int searchChar, int start) {
    240        if (cs instanceof String) {
    241            return ((String) cs).indexOf(searchChar, start);
    242        }
    243        final int sz = cs.length();
    244        if (start < 0) {
    245            start = 0;
    246        }
    247        if (searchChar < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
    248            for (int i = start; i < sz; i++) {
    249                if (cs.charAt(i) == searchChar) {
    250                    return i;
    251                }
    252            }
    253            return -1;
    254        }
    255        //supplementary characters (LANG1300)
    256        if (searchChar <= Character.MAX_CODE_POINT) {
    257            final char[] chars = Character.toChars(searchChar);
    258            for (int i = start; i < sz - 1; i++) {
    259                final char high = cs.charAt(i);
    260                final char low = cs.charAt(i + 1);
    261                if (high == chars[0] && low == chars[1]) {
    262                    return i;
    263                }
    264            }
    265        }
    266        return -1;
    267    }
    268
    269    /**
    270     * Decides if the operating system matches.
    271     * <p>
    272     * This method is package private instead of private to support unit test invocation.
    273     * </p>
    274     *
    275     * @param osName
    276     *            the actual OS name
    277     * @param osNamePrefix
    278     *            the prefix for the expected OS name
    279     * @return true if matches, or false if not or can't determine
    280     */
    281    private static boolean isOsNameMatch(final String osName, final String osNamePrefix) {
    282        if (osName == null) {
    283            return false;
    284        }
    285        return osName.toUpperCase(Locale.ROOT).startsWith(osNamePrefix.toUpperCase(Locale.ROOT));
    286    }
    287
    288    /**
    289     * Null-safe replace.
    290     *
    291     * @param path the path to be changed, null ignored.
    292     * @param oldChar the old character.
    293     * @param newChar the new character.
    294     * @return the new path.
    295     */
    296    private static String replace(final String path, final char oldChar, final char newChar) {
    297        return path == null ? null : path.replace(oldChar, newChar);
    298    }
    299
    300    private final int blockSize;
    301    private final boolean casePreserving;
    302    private final boolean caseSensitive;
    303    private final int[] illegalFileNameChars;
    304    private final int maxFileNameLength;
    305    private final int maxPathLength;
    306    private final String[] reservedFileNames;
    307    private final boolean reservedFileNamesExtensions;
    308    private final boolean supportsDriveLetter;
    309    private final char nameSeparator;
    310    private final char nameSeparatorOther;
    311
    312    /**
    313     * Constructs a new instance.
    314     *
    315     * @param blockSize file allocation block size in bytes.
    316     * @param caseSensitive Whether this file system is case-sensitive.
    317     * @param casePreserving Whether this file system is case-preserving.
    318     * @param maxFileLength The maximum length for file names. The file name does not include folders.
    319     * @param maxPathLength The maximum length of the path to a file. This can include folders.
    320     * @param illegalFileNameChars Illegal characters for this file system.
    321     * @param reservedFileNames The reserved file names.
    322     * @param reservedFileNamesExtensions TODO
    323     * @param supportsDriveLetter Whether this file system support driver letters.
    324     * @param nameSeparator The name separator, '\\' on Windows, '/' on Linux.
    325     */
    326    FileSystem(final int blockSize, final boolean caseSensitive, final boolean casePreserving,
    327        final int maxFileLength, final int maxPathLength, final int[] illegalFileNameChars,
    328        final String[] reservedFileNames, final boolean reservedFileNamesExtensions, final boolean supportsDriveLetter, final char nameSeparator) {
    329        this.blockSize = blockSize;
    330        this.maxFileNameLength = maxFileLength;
    331        this.maxPathLength = maxPathLength;
    332        this.illegalFileNameChars = Objects.requireNonNull(illegalFileNameChars, "illegalFileNameChars");
    333        this.reservedFileNames = Objects.requireNonNull(reservedFileNames, "reservedFileNames");
    334        this.reservedFileNamesExtensions = reservedFileNamesExtensions;
    335        this.caseSensitive = caseSensitive;
    336        this.casePreserving = casePreserving;
    337        this.supportsDriveLetter = supportsDriveLetter;
    338        this.nameSeparator = nameSeparator;
    339        this.nameSeparatorOther = FilenameUtils.flipSeparator(nameSeparator);
    340    }
    341
    342    /**
    343     * Gets the file allocation block size in bytes.
    344     * @return the file allocation block size in bytes.
    345     *
    346     * @since 2.12.0
    347     */
    348    public int getBlockSize() {
    349        return blockSize;
    350    }
    351
    352    /**
    353     * Gets a cloned copy of the illegal characters for this file system.
    354     *
    355     * @return the illegal characters for this file system.
    356     */
    357    public char[] getIllegalFileNameChars() {
    358        final char[] chars = new char[illegalFileNameChars.length];
    359        for (int i = 0; i < illegalFileNameChars.length; i++) {
    360            chars[i] = (char) illegalFileNameChars[i];
    361        }
    362        return chars;
    363    }
    364
    365    /**
    366     * Gets a cloned copy of the illegal code points for this file system.
    367     *
    368     * @return the illegal code points for this file system.
    369     * @since 2.12.0
    370     */
    371    public int[] getIllegalFileNameCodePoints() {
    372        return this.illegalFileNameChars.clone();
    373    }
    374
    375    /**
    376     * Gets the maximum length for file names. The file name does not include folders.
    377     *
    378     * @return the maximum length for file names.
    379     */
    380    public int getMaxFileNameLength() {
    381        return maxFileNameLength;
    382    }
    383
    384    /**
    385     * Gets the maximum length of the path to a file. This can include folders.
    386     *
    387     * @return the maximum length of the path to a file.
    388     */
    389    public int getMaxPathLength() {
    390        return maxPathLength;
    391    }
    392
    393    /**
    394     * Gets the name separator, '\\' on Windows, '/' on Linux.
    395     *
    396     * @return '\\' on Windows, '/' on Linux.
    397     *
    398     * @since 2.12.0
    399     */
    400    public char getNameSeparator() {
    401        return nameSeparator;
    402    }
    403
    404    /**
    405     * Gets a cloned copy of the reserved file names.
    406     *
    407     * @return the reserved file names.
    408     */
    409    public String[] getReservedFileNames() {
    410        return reservedFileNames.clone();
    411    }
    412
    413    /**
    414     * Tests whether this file system preserves case.
    415     *
    416     * @return Whether this file system preserves case.
    417     */
    418    public boolean isCasePreserving() {
    419        return casePreserving;
    420    }
    421
    422    /**
    423     * Tests whether this file system is case-sensitive.
    424     *
    425     * @return Whether this file system is case-sensitive.
    426     */
    427    public boolean isCaseSensitive() {
    428        return caseSensitive;
    429    }
    430
    431    /**
    432     * Tests if the given character is illegal in a file name, {@code false} otherwise.
    433     *
    434     * @param c
    435     *            the character to test
    436     * @return {@code true} if the given character is illegal in a file name, {@code false} otherwise.
    437     */
    438    private boolean isIllegalFileNameChar(final int c) {
    439        return Arrays.binarySearch(illegalFileNameChars, c) >= 0;
    440    }
    441
    442    /**
    443     * Tests if a candidate file name (without a path) such as {@code "filename.ext"} or {@code "filename"} is a
    444     * potentially legal file name. If the file name length exceeds {@link #getMaxFileNameLength()}, or if it contains
    445     * an illegal character then the check fails.
    446     *
    447     * @param candidate
    448     *            a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"}
    449     * @return {@code true} if the candidate name is legal
    450     */
    451    public boolean isLegalFileName(final CharSequence candidate) {
    452        if (candidate == null || candidate.length() == 0 || candidate.length() > maxFileNameLength) {
    453            return false;
    454        }
    455        if (isReservedFileName(candidate)) {
    456            return false;
    457        }
    458        return candidate.chars().noneMatch(this::isIllegalFileNameChar);
    459    }
    460
    461    /**
    462     * Tests whether the given string is a reserved file name.
    463     *
    464     * @param candidate
    465     *            the string to test
    466     * @return {@code true} if the given string is a reserved file name.
    467     */
    468    public boolean isReservedFileName(final CharSequence candidate) {
    469        final CharSequence test = reservedFileNamesExtensions ? trimExtension(candidate) : candidate;
    470        return Arrays.binarySearch(reservedFileNames, test) >= 0;
    471    }
    472
    473    /**
    474     * Converts all separators to the Windows separator of backslash.
    475     *
    476     * @param path the path to be changed, null ignored
    477     * @return the updated path
    478     * @since 2.12.0
    479     */
    480    public String normalizeSeparators(final String path) {
    481        return replace(path, nameSeparatorOther, nameSeparator);
    482    }
    483
    484    /**
    485     * Tests whether this file system support driver letters.
    486     * <p>
    487     * Windows supports driver letters as do other operating systems. Whether these other OS's still support Java like
    488     * OS/2, is a different matter.
    489     * </p>
    490     *
    491     * @return whether this file system support driver letters.
    492     * @since 2.9.0
    493     * @see <a href="https://en.wikipedia.org/wiki/Drive_letter_assignment">Operating systems that use drive letter
    494     *      assignment</a>
    495     */
    496    public boolean supportsDriveLetter() {
    497        return supportsDriveLetter;
    498    }
    499
    500    /**
    501     * Converts a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} to a legal file
    502     * name. Illegal characters in the candidate name are replaced by the {@code replacement} character. If the file
    503     * name length exceeds {@link #getMaxFileNameLength()}, then the name is truncated to
    504     * {@link #getMaxFileNameLength()}.
    505     *
    506     * @param candidate
    507     *            a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"}
    508     * @param replacement
    509     *            Illegal characters in the candidate name are replaced by this character
    510     * @return a String without illegal characters
    511     */
    512    public String toLegalFileName(final String candidate, final char replacement) {
    513        if (isIllegalFileNameChar(replacement)) {
    514            // %s does not work properly with NUL
    515            throw new IllegalArgumentException(String.format("The replacement character '%s' cannot be one of the %s illegal characters: %s",
    516                replacement == '\0' ? "\\0" : replacement, name(), Arrays.toString(illegalFileNameChars)));
    517        }
    518        final String truncated = candidate.length() > maxFileNameLength ? candidate.substring(0, maxFileNameLength) : candidate;
    519        final int[] array = truncated.chars().map(i -> isIllegalFileNameChar(i) ? replacement : i).toArray();
    520        return new String(array, 0, array.length);
    521    }
    522
    523    CharSequence trimExtension(final CharSequence cs) {
    524        final int index = indexOf(cs, '.', 0);
    525        return index < 0 ? cs : cs.subSequence(0, index);
    526    }
    527}