    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(false, false, Integer.MAX_VALUE, Integer.MAX_VALUE, new char[] { 0 }, new String[] {}, false),
    040
    041    /**
    042     * Linux file system.
    043     */
    044    LINUX(true, true, 255, 4096, new char[] {
    045            // KEEP THIS ARRAY SORTED!
    046            // @formatter:off
    047            // ASCII NUL
    048            0,
    049             '/'
    050            // @formatter:on
    051    }, new String[] {}, false),
    052
    053    /**
    054     * MacOS file system.
    055     */
    056    MAC_OSX(true, true, 255, 1024, new char[] {
    057            // KEEP THIS ARRAY SORTED!
    058            // @formatter:off
    059            // ASCII NUL
    060            0,
    061            '/',
    062             ':'
    063            // @formatter:on
    064    }, new String[] {}, 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     */
    077    WINDOWS(false, true, 255,
    078            32000, new char[] {
    079                    // KEEP THIS ARRAY SORTED!
    080                    // @formatter:off
    081                    // ASCII NUL
    082                    0,
    083                    // 1-31 may be allowed in file streams
    084                    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,
    085                    29, 30, 31,
    086                    '"', '*', '/', ':', '<', '>', '?', '\\', '|'
    087                    // @formatter:on
    088            }, // KEEP THIS ARRAY SORTED!
    089            new String[] { "AUX", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "CON", "LPT1",
    090                    "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "NUL", "PRN" }, true);
    091
    092    /**
    093     * <p>
    094     * Is {@code true} if this is Linux.
    095     * </p>
    096     * <p>
    097     * The field will return {@code false} if {@code OS_NAME} is {@code null}.
    098     * </p>
    099     */
    100    private static final boolean IS_OS_LINUX = getOsMatchesName("Linux");
    101
    102    /**
    103     * <p>
    104     * Is {@code true} if this is Mac.
    105     * </p>
    106     * <p>
    107     * The field will return {@code false} if {@code OS_NAME} is {@code null}.
    108     * </p>
    109     */
    110    private static final boolean IS_OS_MAC = getOsMatchesName("Mac");
    111
    112    /**
    113     * The prefix String for all Windows OS.
    114     */
    115    private static final String OS_NAME_WINDOWS_PREFIX = "Windows";
    116
    117    /**
    118     * <p>
    119     * Is {@code true} if this is Windows.
    120     * </p>
    121     * <p>
    122     * The field will return {@code false} if {@code OS_NAME} is {@code null}.
    123     * </p>
    124     */
    125    private static final boolean IS_OS_WINDOWS = getOsMatchesName(OS_NAME_WINDOWS_PREFIX);
    126
    127    /**
    128     * Gets the current file system.
    129     *
    130     * @return the current file system
    131     */
    132    public static FileSystem getCurrent() {
    133        if (IS_OS_LINUX) {
    134            return LINUX;
    135        }
    136        if (IS_OS_MAC) {
    137            return FileSystem.MAC_OSX;
    138        }
    139        if (IS_OS_WINDOWS) {
    140            return FileSystem.WINDOWS;
    141        }
    142        return GENERIC;
    143    }
    144
    145    /**
    146     * Decides if the operating system matches.
    147     *
    148     * @param osNamePrefix
    149     *            the prefix for the os name
    150     * @return true if matches, or false if not or can't determine
    151     */
    152    private static boolean getOsMatchesName(final String osNamePrefix) {
    153        return isOsNameMatch(getSystemProperty("os.name"), osNamePrefix);
    154    }
    155
    156    /**
    157     * <p>
    158     * Gets a System property, defaulting to {@code null} if the property cannot be read.
    159     * </p>
    160     * <p>
    161     * If a {@code SecurityException} is caught, the return value is {@code null} and a message is written to
    162     * {@code System.err}.
    163     * </p>
    164     *
    165     * @param property
    166     *            the system property name
    167     * @return the system property value or {@code null} if a security problem occurs
    168     */
    169    private static String getSystemProperty(final String property) {
    170        try {
    171            return System.getProperty(property);
    172        } catch (final SecurityException ex) {
    173            // we are not allowed to look at this property
    174            System.err.println("Caught a SecurityException reading the system property '" + property
    175                    + "'; the SystemUtils property value will default to null.");
    176            return null;
    177        }
    178    }
    179
    180    /**
    181     * Decides if the operating system matches.
    182     * <p>
    183     * This method is package private instead of private to support unit test invocation.
    184     * </p>
    185     *
    186     * @param osName
    187     *            the actual OS name
    188     * @param osNamePrefix
    189     *            the prefix for the expected OS name
    190     * @return true if matches, or false if not or can't determine
    191     */
    192    private static boolean isOsNameMatch(final String osName, final String osNamePrefix) {
    193        if (osName == null) {
    194            return false;
    195        }
    196        return osName.toUpperCase(Locale.ROOT).startsWith(osNamePrefix.toUpperCase(Locale.ROOT));
    197    }
    198
    199    private final boolean casePreserving;
    200    private final boolean caseSensitive;
    201    private final char[] illegalFileNameChars;
    202    private final int maxFileNameLength;
    203    private final int maxPathLength;
    204    private final String[] reservedFileNames;
    205    private final boolean supportsDriveLetter;
    206
    207    /**
    208     * Constructs a new instance.
    209     *
    210     * @param caseSensitive Whether this file system is case sensitive.
    211     * @param casePreserving Whether this file system is case preserving.
    212     * @param maxFileLength The maximum length for file names. The file name does not include folders.
    213     * @param maxPathLength The maximum length of the path to a file. This can include folders.
    214     * @param illegalFileNameChars Illegal characters for this file system.
    215     * @param reservedFileNames The reserved file names.
    216     * @param supportsDriveLetter Whether this file system support driver letters.
    217     */
    218    FileSystem(final boolean caseSensitive, final boolean casePreserving, final int maxFileLength,
    219        final int maxPathLength, final char[] illegalFileNameChars, final String[] reservedFileNames,
    220        final boolean supportsDriveLetter) {
    221        this.maxFileNameLength = maxFileLength;
    222        this.maxPathLength = maxPathLength;
    223        this.illegalFileNameChars = Objects.requireNonNull(illegalFileNameChars, "illegalFileNameChars");
    224        this.reservedFileNames = Objects.requireNonNull(reservedFileNames, "reservedFileNames");
    225        this.caseSensitive = caseSensitive;
    226        this.casePreserving = casePreserving;
    227        this.supportsDriveLetter = supportsDriveLetter;
    228    }
    229
    230    /**
    231     * Gets a cloned copy of the illegal characters for this file system.
    232     *
    233     * @return the illegal characters for this file system.
    234     */
    235    public char[] getIllegalFileNameChars() {
    236        return this.illegalFileNameChars.clone();
    237    }
    238
    239    /**
    240     * Gets the maximum length for file names. The file name does not include folders.
    241     *
    242     * @return the maximum length for file names.
    243     */
    244    public int getMaxFileNameLength() {
    245        return maxFileNameLength;
    246    }
    247
    248    /**
    249     * Gets the maximum length of the path to a file. This can include folders.
    250     *
    251     * @return the maximum length of the path to a file.
    252     */
    253    public int getMaxPathLength() {
    254        return maxPathLength;
    255    }
    256
    257    /**
    258     * Gets a cloned copy of the reserved file names.
    259     *
    260     * @return the reserved file names.
    261     */
    262    public String[] getReservedFileNames() {
    263        return reservedFileNames.clone();
    264    }
    265
    266    /**
    267     * Whether this file system preserves case.
    268     *
    269     * @return Whether this file system preserves case.
    270     */
    271    public boolean isCasePreserving() {
    272        return casePreserving;
    273    }
    274
    275    /**
    276     * Whether this file system is case-sensitive.
    277     *
    278     * @return Whether this file system is case-sensitive.
    279     */
    280    public boolean isCaseSensitive() {
    281        return caseSensitive;
    282    }
    283
    284    /**
    285     * Returns {@code true} if the given character is illegal in a file name, {@code false} otherwise.
    286     *
    287     * @param c
    288     *            the character to test
    289     * @return {@code true} if the given character is illegal in a file name, {@code false} otherwise.
    290     */
    291    private boolean isIllegalFileNameChar(final char c) {
    292        return Arrays.binarySearch(illegalFileNameChars, c) >= 0;
    293    }
    294
    295    /**
    296     * Checks if a candidate file name (without a path) such as {@code "filename.ext"} or {@code "filename"} is a
    297     * potentially legal file name. If the file name length exceeds {@link #getMaxFileNameLength()}, or if it contains
    298     * an illegal character then the check fails.
    299     *
    300     * @param candidate
    301     *            a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"}
    302     * @return {@code true} if the candidate name is legal
    303     */
    304    public boolean isLegalFileName(final CharSequence candidate) {
    305        if (candidate == null || candidate.length() == 0 || candidate.length() > maxFileNameLength) {
    306            return false;
    307        }
    308        if (isReservedFileName(candidate)) {
    309            return false;
    310        }
    311        for (int i = 0; i < candidate.length(); i++) {
    312            if (isIllegalFileNameChar(candidate.charAt(i))) {
    313                return false;
    314            }
    315        }
    316        return true;
    317    }
    318
    319    /**
    320     * Returns whether the given string is a reserved file name.
    321     *
    322     * @param candidate
    323     *            the string to test
    324     * @return {@code true} if the given string is a reserved file name.
    325     */
    326    public boolean isReservedFileName(final CharSequence candidate) {
    327        return Arrays.binarySearch(reservedFileNames, candidate) >= 0;
    328    }
    329
    330    /**
    331     * Tests whether this file system support driver letters.
    332     * <p>
    333     * Windows supports driver letters as do other operating systems. Whether these other OS's still support Java like
    334     * OS/2, is a different matter.
    335     * </p>
    336     *
    337     * @return whether this file system support driver letters.
    338     * @since 2.9.0
    339     * @see <a href="https://en.wikipedia.org/wiki/Drive_letter_assignment">Operating systems that use drive letter
    340     *      assignment</a>
    341     */
    342    public boolean supportsDriveLetter() {
    343        return supportsDriveLetter;
    344    }
    345
    346    /**
    347     * Converts a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} to a legal file
    348     * name. Illegal characters in the candidate name are replaced by the {@code replacement} character. If the file
    349     * name length exceeds {@link #getMaxFileNameLength()}, then the name is truncated to
    350     * {@link #getMaxFileNameLength()}.
    351     *
    352     * @param candidate
    353     *            a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"}
    354     * @param replacement
    355     *            Illegal characters in the candidate name are replaced by this character
    356     * @return a String without illegal characters
    357     */
    358    public String toLegalFileName(final String candidate, final char replacement) {
    359        if (isIllegalFileNameChar(replacement)) {
    360            throw new IllegalArgumentException(
    361                    String.format("The replacement character '%s' cannot be one of the %s illegal characters: %s",
    362                            // %s does not work properly with NUL
    363                            replacement == '\0' ? "\\0" : replacement, name(), Arrays.toString(illegalFileNameChars)));
    364        }
    365        final String truncated = candidate.length() > maxFileNameLength ? candidate.substring(0, maxFileNameLength)
    366                : candidate;
    367        boolean changed = false;
    368        final char[] charArray = truncated.toCharArray();
    369        for (int i = 0; i < charArray.length; i++) {
    370            if (isIllegalFileNameChar(charArray[i])) {
    371                charArray[i] = replacement;
    372                changed = true;
    373            }
    374        }
    375        return changed ? String.valueOf(charArray) : truncated;
    376    }
    377}