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 * Abstracts an OS' file system details, currently supporting the single use case of converting a file name String to a 036 * legal file name with {@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 039 * the OS hosting the running JVM. 040 * </p> 041 * 042 * @since 2.7 043 */ 044public enum FileSystem { 045 046 /** 047 * Generic file system. 048 */ 049 GENERIC(4096, false, false, 1020, 1024 * 1024, new int[] { 050 // @formatter:off 051 // ASCII NUL 052 0 053 // @formatter:on 054 }, new String[] {}, false, false, '/', NameLengthStrategy.BYTES), 055 056 /** 057 * Linux file system. 058 */ 059 LINUX(8192, true, true, 255, 4096, new int[] { 060 // KEEP THIS ARRAY SORTED! 061 // @formatter:off 062 // ASCII NUL 063 0, 064 '/' 065 // @formatter:on 066 }, new String[] {}, false, false, '/', NameLengthStrategy.BYTES), 067 068 /** 069 * MacOS file system. 070 */ 071 MAC_OSX(4096, true, true, 255, 1024, new int[] { 072 // KEEP THIS ARRAY SORTED! 073 // @formatter:off 074 // ASCII NUL 075 0, 076 '/', 077 ':' 078 // @formatter:on 079 }, new String[] {}, false, false, '/', NameLengthStrategy.BYTES), 080 081 /** 082 * Windows file system. 083 * <p> 084 * The reserved characters are defined in the 085 * <a href="https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file">Naming Conventions 086 * (microsoft.com)</a>. 087 * </p> 088 * 089 * @see <a href="https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file">Naming Conventions 090 * (microsoft.com)</a> 091 * @see <a href="https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles"> 092 * CreateFileA function - Consoles (microsoft.com)</a> 093 */ 094 // @formatter:off 095 WINDOWS(4096, false, true, 096 255, 32767, // KEEP THIS ARRAY SORTED! 097 new int[] { 098 // KEEP THIS ARRAY SORTED! 099 // 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}