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 }