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