    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 *      https://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 static java.nio.charset.StandardCharsets.UTF_16;
    021
    022import java.nio.ByteBuffer;
    023import java.nio.CharBuffer;
    024import java.nio.charset.CharacterCodingException;
    025import java.nio.charset.Charset;
    026import java.nio.charset.CharsetEncoder;
    027import java.nio.charset.CoderResult;
    028import java.nio.charset.CodingErrorAction;
    029import java.text.BreakIterator;
    030import java.util.Arrays;
    031import java.util.Locale;
    032import java.util.Objects;
    033
    034/**
    035 * Enumerates file system details for operating systems, currently supporting the single use case of converting a file name String to a legal file name with
    036 * {@link #toLegalFileName(String, char)}.
    037 * <p>
    038 * 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.
    039 * </p>
    040 *
    041 * @since 2.7
    042 */
    043public enum FileSystem {
    044
    045    /**
    046     * Generic file system.
    047     */
    048    GENERIC(4096, false, false, 1020, 1024 * 1024, new int[] {
    049            // @formatter:off
    050            // ASCII NUL
    051            0
    052            // @formatter:on
    053    }, new String[] {}, false, false, '/', NameLengthStrategy.BYTES),
    054
    055    /**
    056     * Linux file system.
    057     */
    058    LINUX(8192, true, true, 255, 4096, new int[] {
    059            // KEEP THIS ARRAY SORTED!
    060            // @formatter:off
    061            // ASCII NUL
    062            0,
    063             '/'
    064            // @formatter:on
    065    }, new String[] {}, false, false, '/', NameLengthStrategy.BYTES),
    066
    067    /**
    068     * MacOS file system.
    069     */
    070    MAC_OSX(4096, true, true, 255, 1024, new int[] {
    071            // KEEP THIS ARRAY SORTED!
    072            // @formatter:off
    073            // ASCII NUL
    074            0,
    075            '/',
    076             ':'
    077            // @formatter:on
    078    }, new String[] {}, false, false, '/', NameLengthStrategy.BYTES),
    079
    080    /**
    081     * Windows file system.
    082     * <p>
    083     * The reserved characters are defined in the
    084     * <a href="https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file">Naming Conventions
    085     * (microsoft.com)</a>.
    086     * </p>
    087     *
    088     * @see <a href="https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file">Naming Conventions
    089     *      (microsoft.com)</a>
    090     * @see <a href="https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles">
    091     *      CreateFileA function - Consoles (microsoft.com)</a>
    092     */
    093    // @formatter:off
    094    WINDOWS(4096, false, true,
    095            255, 32767, // KEEP THIS ARRAY SORTED!
    096            new int[] {
    097                    // KEEP THIS ARRAY SORTED!
    098                    // ASCII NUL
    099                    0,
    100                    // 1-31 may be allowed in file streams
    101                    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,
    102                    29, 30, 31,
    103                    '"', '*', '/', ':', '<', '>', '?', '\\', '|'
    104            }, new String[] {
    105                    "AUX",
    106                    "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
    107                    "COM\u00b2", "COM\u00b3", "COM\u00b9", // Superscript 2 3 1 in that order
    108                    "CON", "CONIN$", "CONOUT$",
    109                    "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
    110                    "LPT\u00b2", "LPT\u00b3", "LPT\u00b9", // Superscript 2 3 1 in that order
    111                    "NUL", "PRN"
    112            }, true, true, '\\', NameLengthStrategy.UTF16_CODE_UNITS);
    113    // @formatter:on
    114
    115    /**
    116     * Strategy for measuring and truncating file or path names in different units.
    117     * Implementations measure length and can truncate to a specified limit.
    118     */
    119    enum NameLengthStrategy {
    120
    121        /** Length measured as encoded bytes. */
    122        BYTES {
    123            @Override
    124            int getLength(final CharSequence value, final Charset charset) {
    125                final CharsetEncoder enc = charset.newEncoder()
    126                        .onMalformedInput(CodingErrorAction.REPORT)
    127                        .onUnmappableCharacter(CodingErrorAction.REPORT);
    128                try {
    129                    return enc.encode(CharBuffer.wrap(value)).remaining();
    130                } catch (final CharacterCodingException e) {
    131                    // Unencodable, does not fit any byte limit.
    132                    return Integer.MAX_VALUE;
    133                }
    134            }
    135
    136            @Override
    137            CharSequence truncate(final CharSequence value, final int limit, final Charset charset) {
    138                final CharsetEncoder encoder = charset.newEncoder()
    139                        .onMalformedInput(CodingErrorAction.REPORT)
    140                        .onUnmappableCharacter(CodingErrorAction.REPORT);
    141                if (!encoder.canEncode(value)) {
    142                    throw new IllegalArgumentException("The value " + value + " cannot be encoded using " + charset.name());
    143                }
    144                // Fast path: if even the worst-case expansion fits, we're done.
    145                if (value.length() <= Math.floor(limit / encoder.maxBytesPerChar())) {
    146                    return value;
    147                }
    148                // Slow path: encode into a fixed-size byte buffer.
    149                // 1. Compute length of extension in bytes (if any).
    150                final CharSequence[] parts = splitExtension(value);
    151                final int extensionLength = getLength(parts[1], charset);
    152                if (extensionLength > 0 && extensionLength >= limit) {
    153                    // Extension itself does not fit
    154                    throw new IllegalArgumentException("The extension of " + value + " is too long to fit within " + limit + " bytes");
    155                }
    156                // 2. Compute the character part that fits within the remaining byte budget.
    157                final ByteBuffer byteBuffer = ByteBuffer.allocate(limit - extensionLength);
    158                final CharBuffer charBuffer = CharBuffer.wrap(parts[0]);
    159                // Encode until the first character that would exceed the byte budget.
    160                final CoderResult cr = encoder.encode(charBuffer, byteBuffer, true);
    161                if (cr.isUnderflow()) {
    162                    // Entire candidate fit within maxFileNameLength bytes.
    163                    return value;
    164                }
    165                final CharSequence truncated = safeTruncate(value, charBuffer.position());
    166                return extensionLength == 0 ? truncated : truncated.toString() + parts[1];
    167            }
    168        },
    169
    170        /** Length measured as UTF-16 code units (i.e., {@code CharSequence.length()}). */
    171        UTF16_CODE_UNITS {
    172            @Override
    173            int getLength(final CharSequence value, final Charset charset) {
    174                return value.length();
    175            }
    176
    177            @Override
    178            CharSequence truncate(final CharSequence value, final int limit, final Charset charset) {
    179                if (!UTF_16.newEncoder().canEncode(value)) {
    180                    throw new IllegalArgumentException("The value " + value + " can not be encoded using " + UTF_16.name());
    181                }
    182                // Fast path: no truncation needed.
    183                if (value.length() <= limit) {
    184                    return value;
    185                }
    186                // Slow path: truncate to limit.
    187                // 1. Compute length of extension in chars (if any).
    188                final CharSequence[] parts = splitExtension(value);
    189                final int extensionLength = parts[1].length();
    190                if (extensionLength > 0 && extensionLength >= limit) {
    191                    // Extension itself does not fit
    192                    throw new IllegalArgumentException("The extension of " + value + " is too long to fit within " + limit + " characters");
    193                }
    194                // 2. Truncate the non-extension part and append the extension (if any).
    195                final CharSequence truncated = safeTruncate(value, limit - extensionLength);
    196                return extensionLength == 0 ? truncated : truncated.toString() + parts[1];
    197            }
    198        };
    199
    200        /**
    201         * Gets the measured length in this strategy’s unit.
    202         *
    203         * @param value The value to measure, not null.
    204         * @param charset The charset to use when measuring in bytes.
    205         * @return The length in this strategy’s unit.
    206         */
    207        abstract int getLength(CharSequence value, Charset charset);
    208
    209        /**
    210         * Tests if the measured length is less or equal the {@code limit}.
    211         *
    212         * @param value The value to measure, not null.
    213         * @param limit The limit to compare to.
    214         * @param charset The charset to use when measuring in bytes.
    215         * @return {@code true} if the measured length is less or equal the {@code limit}, {@code false} otherwise.
    216         */
    217        final boolean isWithinLimit(final CharSequence value, final int limit, final Charset charset) {
    218            return getLength(value, charset) <= limit;
    219        }
    220
    221        /**
    222         * Truncates to {@code limit} in this strategy’s unit (no-op if already within limit).
    223         *
    224         * @param value The value to truncate, not null.
    225         * @param limit The limit to truncate to.
    226         * @param charset The charset to use when measuring in bytes.
    227         * @return The truncated value, not null.
    228         */
    229        abstract CharSequence truncate(CharSequence value, int limit, Charset charset);
    230    }
    231
    232    /**
    233     * Is {@code true} if this is Linux.
    234     * <p>
    235     * The field will return {@code false} if {@code OS_NAME} is {@code null}.
    236     * </p>
    237     */
    238    private static final boolean IS_OS_LINUX = getOsMatchesName("Linux");
    239
    240    /**
    241     * Is {@code true} if this is Mac.
    242     * <p>
    243     * The field will return {@code false} if {@code OS_NAME} is {@code null}.
    244     * </p>
    245     */
    246    private static final boolean IS_OS_MAC = getOsMatchesName("Mac");
    247
    248    /**
    249     * The prefix String for all Windows OS.
    250     */
    251    private static final String OS_NAME_WINDOWS_PREFIX = "Windows";
    252
    253    /**
    254     * Is {@code true} if this is Windows.
    255     * <p>
    256     * The field will return {@code false} if {@code OS_NAME} is {@code null}.
    257     * </p>
    258     */
    259    private static final boolean IS_OS_WINDOWS = getOsMatchesName(OS_NAME_WINDOWS_PREFIX);
    260
    261    /**
    262     * The current FileSystem.
    263     */
    264    private static final FileSystem CURRENT = current();
    265
    266    /**
    267     * Gets the current file system.
    268     *
    269     * @return the current file system.
    270     */
    271    private static FileSystem current() {
    272        if (IS_OS_LINUX) {
    273            return LINUX;
    274        }
    275        if (IS_OS_MAC) {
    276            return MAC_OSX;
    277        }
    278        if (IS_OS_WINDOWS) {
    279            return WINDOWS;
    280        }
    281        return GENERIC;
    282    }
    283
    284    /**
    285     * Gets the current file system.
    286     *
    287     * @return the current file system.
    288     */
    289    public static FileSystem getCurrent() {
    290        return CURRENT;
    291    }
    292
    293    /**
    294     * Decides if the operating system matches.
    295     *
    296     * @param osNamePrefix
    297     *            the prefix for the operating system name.
    298     * @return true if matches, or false if not or can't determine.
    299     */
    300    private static boolean getOsMatchesName(final String osNamePrefix) {
    301        return isOsNameMatch(getSystemProperty("os.name"), osNamePrefix);
    302    }
    303
    304    /**
    305     * Gets a System property, defaulting to {@code null} if the property cannot be read.
    306     * <p>
    307     * If a {@link SecurityException} is caught, the return value is {@code null} and a message is written to
    308     * {@code System.err}.
    309     * </p>
    310     *
    311     * @param property
    312     *            the system property name.
    313     * @return the system property value or {@code null} if a security problem occurs.
    314     */
    315    private static String getSystemProperty(final String property) {
    316        try {
    317            return System.getProperty(property);
    318        } catch (final SecurityException ex) {
    319            // we are not allowed to look at this property
    320            System.err.println("Caught a SecurityException reading the system property '" + property
    321                    + "'; the SystemUtils property value will default to null.");
    322            return null;
    323        }
    324    }
    325
    326    /*
    327     * Finds the index of the first dot in a CharSequence.
    328     */
    329    private static int indexOfFirstDot(final CharSequence cs) {
    330        if (cs instanceof String) {
    331            return ((String) cs).indexOf('.');
    332        }
    333        for (int i = 0; i < cs.length(); i++) {
    334            if (cs.charAt(i) == '.') {
    335                return i;
    336            }
    337        }
    338        return -1;
    339    }
    340
    341    /**
    342     * Decides if the operating system matches.
    343     * <p>
    344     * This method is package private instead of private to support unit test invocation.
    345     * </p>
    346     *
    347     * @param osName
    348     *            the actual OS name.
    349     * @param osNamePrefix
    350     *            the prefix for the expected OS name.
    351     * @return true if matches, or false if not or can't determine.
    352     */
    353    private static boolean isOsNameMatch(final String osName, final String osNamePrefix) {
    354        if (osName == null) {
    355            return false;
    356        }
    357        return osName.toUpperCase(Locale.ROOT).startsWith(osNamePrefix.toUpperCase(Locale.ROOT));
    358    }
    359
    360    /**
    361     * Null-safe replace.
    362     *
    363     * @param path the path to be changed, null ignored.
    364     * @param oldChar the old character.
    365     * @param newChar the new character.
    366     * @return the new path.
    367     */
    368    private static String replace(final String path, final char oldChar, final char newChar) {
    369        return path == null ? null : path.replace(oldChar, newChar);
    370    }
    371
    372    /**
    373     * Truncates a string respecting grapheme cluster boundaries.
    374     *
    375     * @param value The value to truncate.
    376     * @param limit The maximum length.
    377     * @return The truncated value.
    378     * @throws IllegalArgumentException If the first grapheme cluster is longer than the limit.
    379     */
    380    private static CharSequence safeTruncate(final CharSequence value, final int limit) {
    381        if (value.length() <= limit) {
    382            return value;
    383        }
    384        final BreakIterator boundary = BreakIterator.getCharacterInstance(Locale.ROOT);
    385        final String text = value.toString();
    386        boundary.setText(text);
    387        final int end = boundary.preceding(limit + 1);
    388        assert end != BreakIterator.DONE;
    389        if (end == 0) {
    390            final String limitMessage = limit <= 1 ? "1 character" : limit + " characters";
    391            throw new IllegalArgumentException("The value " + value + " can not be truncated to " + limitMessage
    392                    + " without breaking the first codepoint or grapheme cluster");
    393        }
    394        return text.substring(0, end);
    395    }
    396    static CharSequence[] splitExtension(final CharSequence value) {
    397        final int index = indexOfFirstDot(value);
    398        // An initial dot is not an extension
    399        return index < 1
    400                ? new CharSequence[] {value, ""}
    401                : new CharSequence[] {value.subSequence(0, index), value.subSequence(index, value.length())};
    402    }
    403    static CharSequence trimExtension(final CharSequence cs) {
    404        final int index = indexOfFirstDot(cs);
    405        // An initial dot is not an extension
    406        return index < 1 ? cs : cs.subSequence(0, index);
    407    }
    408    private final int blockSize;
    409    private final boolean casePreserving;
    410    private final boolean caseSensitive;
    411    private final int[] illegalFileNameChars;
    412    private final int maxFileNameLength;
    413    private final int maxPathLength;
    414    private final String[] reservedFileNames;
    415    private final boolean reservedFileNamesExtensions;
    416
    417    private final boolean supportsDriveLetter;
    418
    419    private final char nameSeparator;
    420
    421    private final char nameSeparatorOther;
    422
    423    private final NameLengthStrategy nameLengthStrategy;
    424
    425    /**
    426     * Constructs a new instance.
    427     *
    428     * @param blockSize file allocation block size in bytes.
    429     * @param caseSensitive Whether this file system is case-sensitive.
    430     * @param casePreserving Whether this file system is case-preserving.
    431     * @param maxFileLength The maximum length for file names. The file name does not include folders.
    432     * @param maxPathLength The maximum length of the path to a file. This can include folders.
    433     * @param illegalFileNameChars Illegal characters for this file system.
    434     * @param reservedFileNames The reserved file names.
    435     * @param reservedFileNamesExtensions The reserved file name extensions.
    436     * @param supportsDriveLetter Whether this file system support driver letters.
    437     * @param nameSeparator The name separator, '\\' on Windows, '/' on Linux.
    438     * @param nameLengthStrategy The strategy for measuring and truncating file and path names.
    439     */
    440    FileSystem(final int blockSize, final boolean caseSensitive, final boolean casePreserving,
    441        final int maxFileLength, final int maxPathLength, final int[] illegalFileNameChars,
    442        final String[] reservedFileNames, final boolean reservedFileNamesExtensions, final boolean supportsDriveLetter,
    443        final char nameSeparator, final NameLengthStrategy nameLengthStrategy) {
    444        this.blockSize = blockSize;
    445        this.maxFileNameLength = maxFileLength;
    446        this.maxPathLength = maxPathLength;
    447        this.illegalFileNameChars = Objects.requireNonNull(illegalFileNameChars, "illegalFileNameChars");
    448        this.reservedFileNames = Objects.requireNonNull(reservedFileNames, "reservedFileNames");
    449        //Arrays.sort(this.reservedFileNames);
    450        this.reservedFileNamesExtensions = reservedFileNamesExtensions;
    451        this.caseSensitive = caseSensitive;
    452        this.casePreserving = casePreserving;
    453        this.supportsDriveLetter = supportsDriveLetter;
    454        this.nameSeparator = nameSeparator;
    455        this.nameSeparatorOther = FilenameUtils.flipSeparator(nameSeparator);
    456        this.nameLengthStrategy = nameLengthStrategy;
    457    }
    458
    459    /**
    460     * Gets the file allocation block size in bytes.
    461     *
    462     * @return the file allocation block size in bytes.
    463     * @since 2.12.0
    464     */
    465    public int getBlockSize() {
    466        return blockSize;
    467    }
    468
    469    /**
    470     * Gets a cloned copy of the illegal characters for this file system.
    471     *
    472     * @return the illegal characters for this file system.
    473     */
    474    public char[] getIllegalFileNameChars() {
    475        final char[] chars = new char[illegalFileNameChars.length];
    476        for (int i = 0; i < illegalFileNameChars.length; i++) {
    477            chars[i] = (char) illegalFileNameChars[i];
    478        }
    479        return chars;
    480    }
    481
    482    /**
    483     * Gets a cloned copy of the illegal code points for this file system.
    484     *
    485     * @return the illegal code points for this file system.
    486     * @since 2.12.0
    487     */
    488    public int[] getIllegalFileNameCodePoints() {
    489        return this.illegalFileNameChars.clone();
    490    }
    491
    492    /**
    493     * Gets the maximum length for file names (excluding any folder path).
    494     *
    495     * <p>
    496     * This limit applies only to the file name itself, excluding any parent directories.
    497     * </p>
    498     *
    499     * <p>
    500     * The value is expressed in Java {@code char} units (UTF-16 code units).
    501     * </p>
    502     *
    503     * <p>
    504     * <strong>Note:</strong> Because many file systems enforce limits in <em>bytes</em> using a specific encoding rather than in UTF-16 code units, a name that
    505     * fits this limit may still be rejected by the underlying file system.
    506     * </p>
    507     *
    508     * <p>
    509     * Use {@link #isLegalFileName} to check whether a given name is valid for the current file system and charset.
    510     * </p>
    511     *
    512     * <p>
    513     * However, any file name longer than this limit is guaranteed to be invalid on the current file system.
    514     * </p>
    515     *
    516     * @return the maximum file name length in characters.
    517     */
    518    public int getMaxFileNameLength() {
    519        return maxFileNameLength;
    520    }
    521
    522    /**
    523     * Gets the maximum length for file paths (may include folders).
    524     *
    525     * <p>
    526     * This value is inclusive of all path components and separators. For a limit of each path component see {@link #getMaxFileNameLength()}.
    527     * </p>
    528     *
    529     * <p>
    530     * The value is expressed in Java {@code char} units (UTF-16 code units) and represents the longest path that can be safely passed to Java
    531     * {@link java.io.File} and {@link java.nio.file.Path} APIs.
    532     * </p>
    533     *
    534     * <p>
    535     * <strong>Note:</strong> many operating systems and file systems enforce path length limits in <em>bytes</em> using a specific encoding, rather than in
    536     * UTF-16 code units. As a result, a path that fits within this limit may still be rejected by the underlying platform.
    537     * </p>
    538     *
    539     * <p>
    540     * Conversely, any path longer than this limit is guaranteed to fail with at least some operating system API calls.
    541     * </p>
    542     *
    543     * @return the maximum file path length in characters.
    544     */
    545    public int getMaxPathLength() {
    546        return maxPathLength;
    547    }
    548
    549    NameLengthStrategy getNameLengthStrategy() {
    550        return nameLengthStrategy;
    551    }
    552
    553    /**
    554     * Gets the name separator, '\\' on Windows, '/' on Linux.
    555     *
    556     * @return '\\' on Windows, '/' on Linux.
    557     * @since 2.12.0
    558     */
    559    public char getNameSeparator() {
    560        return nameSeparator;
    561    }
    562
    563    /**
    564     * Gets a cloned copy of the reserved file names.
    565     *
    566     * @return the reserved file names.
    567     */
    568    public String[] getReservedFileNames() {
    569        return reservedFileNames.clone();
    570    }
    571
    572    /**
    573     * Tests whether this file system preserves case.
    574     *
    575     * @return Whether this file system preserves case.
    576     */
    577    public boolean isCasePreserving() {
    578        return casePreserving;
    579    }
    580
    581    /**
    582     * Tests whether this file system is case-sensitive.
    583     *
    584     * @return Whether this file system is case-sensitive.
    585     */
    586    public boolean isCaseSensitive() {
    587        return caseSensitive;
    588    }
    589
    590    /**
    591     * Tests if the given character is illegal in a file name, {@code false} otherwise.
    592     *
    593     * @param c
    594     *            the character to test.
    595     * @return {@code true} if the given character is illegal in a file name, {@code false} otherwise.
    596     */
    597    private boolean isIllegalFileNameChar(final int c) {
    598        return Arrays.binarySearch(illegalFileNameChars, c) >= 0;
    599    }
    600
    601    /**
    602     * Tests if a candidate file name (without a path) is a legal file name.
    603     *
    604     * <p>Takes a file name like {@code "filename.ext"} or {@code "filename"} and checks:</p>
    605     * <ul>
    606     * <li>if the file name length is legal</li>
    607     * <li>if the file name is not a reserved file name</li>
    608     * <li>if the file name does not contain illegal characters</li>
    609     * </ul>
    610     *
    611     * @param candidate
    612     *            A candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"}.
    613     * @return {@code true} if the candidate name is legal.
    614     */
    615    public boolean isLegalFileName(final CharSequence candidate) {
    616        return isLegalFileName(candidate, Charset.defaultCharset());
    617    }
    618
    619    /**
    620     * Tests if a candidate file name (without a path) is a legal file name.
    621     *
    622     * <p>Takes a file name like {@code "filename.ext"} or {@code "filename"} and checks:</p>
    623     * <ul>
    624     * <li>if the file name length is legal</li>
    625     * <li>if the file name is not a reserved file name</li>
    626     * <li>if the file name does not contain illegal characters</li>
    627     * </ul>
    628     *
    629     * @param candidate
    630     *            A candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"}.
    631     * @param charset
    632     *            The charset to use when the file name length is measured in bytes.
    633     * @return {@code true} if the candidate name is legal.
    634     * @since 2.21.0
    635     */
    636    public boolean isLegalFileName(final CharSequence candidate, final Charset charset) {
    637        return candidate != null
    638                && candidate.length() != 0
    639                && nameLengthStrategy.isWithinLimit(candidate, getMaxFileNameLength(), charset)
    640                && !isReservedFileName(candidate)
    641                && candidate.chars().noneMatch(this::isIllegalFileNameChar);
    642    }
    643
    644    /**
    645     * Tests whether the given string is a reserved file name.
    646     *
    647     * @param candidate
    648     *            the string to test.
    649     * @return {@code true} if the given string is a reserved file name.
    650     */
    651    public boolean isReservedFileName(final CharSequence candidate) {
    652        final CharSequence test = reservedFileNamesExtensions ? trimExtension(candidate) : candidate;
    653        return Arrays.binarySearch(reservedFileNames, test) >= 0;
    654    }
    655
    656    /**
    657     * Converts all separators to the Windows separator of backslash.
    658     *
    659     * @param path the path to be changed, null ignored.
    660     * @return the updated path.
    661     * @since 2.12.0
    662     */
    663    public String normalizeSeparators(final String path) {
    664        return replace(path, nameSeparatorOther, nameSeparator);
    665    }
    666
    667    /**
    668     * Tests whether this file system support driver letters.
    669     * <p>
    670     * Windows supports driver letters as do other operating systems. Whether these other OS's still support Java like
    671     * OS/2, is a different matter.
    672     * </p>
    673     *
    674     * @return whether this file system support driver letters.
    675     * @since 2.9.0
    676     * @see <a href="https://en.wikipedia.org/wiki/Drive_letter_assignment">Operating systems that use drive letter
    677     *      assignment</a>
    678     */
    679    public boolean supportsDriveLetter() {
    680        return supportsDriveLetter;
    681    }
    682
    683    /**
    684     * Converts a candidate file name (without a path) to a legal file name.
    685     *
    686     * <p>Takes a file name like {@code "filename.ext"} or {@code "filename"} and:</p>
    687     * <ul>
    688     *     <li>replaces illegal characters by the given replacement character</li>
    689     *     <li>truncates the name to {@link #getMaxFileNameLength()} if necessary</li>
    690     * </ul>
    691     *
    692     * @param candidate
    693     *            A candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"}.
    694     * @param replacement
    695     *            Illegal characters in the candidate name are replaced by this character.
    696     * @param charset
    697     *            The charset to use when the file name length is measured in bytes.
    698     * @return a String without illegal characters.
    699     * @since 2.21.0
    700     */
    701    public String toLegalFileName(final CharSequence candidate, final char replacement, final Charset charset) {
    702        Objects.requireNonNull(candidate, "candidate");
    703        if (candidate.length() == 0) {
    704            throw new IllegalArgumentException("The candidate file name is empty");
    705        }
    706        if (isIllegalFileNameChar(replacement)) {
    707            // %s does not work properly with NUL
    708            throw new IllegalArgumentException(String.format("The replacement character '%s' cannot be one of the %s illegal characters: %s",
    709                replacement == '\0' ? "\\0" : replacement, name(), Arrays.toString(illegalFileNameChars)));
    710        }
    711        final CharSequence truncated = nameLengthStrategy.truncate(candidate, getMaxFileNameLength(), charset);
    712        final int[] array = truncated.chars().map(i -> isIllegalFileNameChar(i) ? replacement : i).toArray();
    713        return new String(array, 0, array.length);
    714    }
    715
    716
    717    /**
    718     * Converts a candidate file name (without a path) to a legal file name.
    719     *
    720     * <p>Takes a file name like {@code "filename.ext"} or {@code "filename"} and:</p>
    721     * <ul>
    722     *     <li>replaces illegal characters by the given replacement character</li>
    723     *     <li>truncates the name to {@link #getMaxFileNameLength()} if necessary</li>
    724     * </ul>
    725     *
    726     * @param candidate
    727     *            A candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"}.
    728     * @param replacement
    729     *            Illegal characters in the candidate name are replaced by this character.
    730     * @return a String without illegal characters.
    731     */
    732    public String toLegalFileName(final String candidate, final char replacement) {
    733        return toLegalFileName(candidate, replacement, Charset.defaultCharset());
    734    }
    735
    736}