View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.io;
19  
20  import static java.nio.charset.StandardCharsets.UTF_16;
21  
22  import java.nio.ByteBuffer;
23  import java.nio.CharBuffer;
24  import java.nio.charset.CharacterCodingException;
25  import java.nio.charset.Charset;
26  import java.nio.charset.CharsetEncoder;
27  import java.nio.charset.CoderResult;
28  import java.nio.charset.CodingErrorAction;
29  import java.text.BreakIterator;
30  import java.util.Arrays;
31  import java.util.Locale;
32  import java.util.Objects;
33  
34  /**
35   * Abstracts an OS' file system details, currently supporting the single use case of converting a file name String to a
36   * legal file name with {@link #toLegalFileName(String, char)}.
37   * <p>
38   * The starting point of any operation is {@link #getCurrent()} which gets you the enum for the file system that matches
39   * the OS hosting the running JVM.
40   * </p>
41   *
42   * @since 2.7
43   */
44  public enum FileSystem {
45  
46      /**
47       * Generic file system.
48       */
49      GENERIC(4096, false, false, 1020, 1024 * 1024, new int[] {
50              // @formatter:off
51              // ASCII NUL
52              0
53              // @formatter:on
54      }, new String[] {}, false, false, '/', NameLengthStrategy.BYTES),
55  
56      /**
57       * Linux file system.
58       */
59      LINUX(8192, true, true, 255, 4096, new int[] {
60              // KEEP THIS ARRAY SORTED!
61              // @formatter:off
62              // ASCII NUL
63              0,
64               '/'
65              // @formatter:on
66      }, new String[] {}, false, false, '/', NameLengthStrategy.BYTES),
67  
68      /**
69       * MacOS file system.
70       */
71      MAC_OSX(4096, true, true, 255, 1024, new int[] {
72              // KEEP THIS ARRAY SORTED!
73              // @formatter:off
74              // ASCII NUL
75              0,
76              '/',
77               ':'
78              // @formatter:on
79      }, new String[] {}, false, false, '/', NameLengthStrategy.BYTES),
80  
81      /**
82       * Windows file system.
83       * <p>
84       * The reserved characters are defined in the
85       * <a href="https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file">Naming Conventions
86       * (microsoft.com)</a>.
87       * </p>
88       *
89       * @see <a href="https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file">Naming Conventions
90       *      (microsoft.com)</a>
91       * @see <a href="https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles">
92       *      CreateFileA function - Consoles (microsoft.com)</a>
93       */
94      // @formatter:off
95      WINDOWS(4096, false, true,
96              255, 32767, // KEEP THIS ARRAY SORTED!
97              new int[] {
98                      // KEEP THIS ARRAY SORTED!
99                      // ASCII NUL
100                     0,
101                     // 1-31 may be allowed in file streams
102                     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,
103                     29, 30, 31,
104                     '"', '*', '/', ':', '<', '>', '?', '\\', '|'
105             }, new String[] {
106                     "AUX",
107                     "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
108                     "COM\u00b2", "COM\u00b3", "COM\u00b9", // Superscript 2 3 1 in that order
109                     "CON", "CONIN$", "CONOUT$",
110                     "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
111                     "LPT\u00b2", "LPT\u00b3", "LPT\u00b9", // Superscript 2 3 1 in that order
112                     "NUL", "PRN"
113             }, true, true, '\\', NameLengthStrategy.UTF16_CODE_UNITS);
114     // @formatter:on
115 
116     /**
117      * Strategy for measuring and truncating file or path names in different units.
118      * Implementations measure length and can truncate to a specified limit.
119      */
120     enum NameLengthStrategy {
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 os 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 }