001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.io; 019 020import java.util.Arrays; 021import java.util.Locale; 022import java.util.Objects; 023 024/** 025 * Abstracts an OS' file system details, currently supporting the single use case of converting a file name String to a 026 * legal file name with {@link #toLegalFileName(String, char)}. 027 * <p> 028 * The starting point of any operation is {@link #getCurrent()} which gets you the enum for the file system that matches 029 * the OS hosting the running JVM. 030 * </p> 031 * 032 * @since 2.7 033 */ 034public enum FileSystem { 035 036 /** 037 * Generic file system. 038 */ 039 GENERIC(4096, false, false, Integer.MAX_VALUE, Integer.MAX_VALUE, new int[] { 0 }, new String[] {}, false, false, '/'), 040 041 /** 042 * Linux file system. 043 */ 044 LINUX(8192, true, true, 255, 4096, new int[] { 045 // KEEP THIS ARRAY SORTED! 046 // @formatter:off 047 // ASCII NUL 048 0, 049 '/' 050 // @formatter:on 051 }, new String[] {}, false, false, '/'), 052 053 /** 054 * MacOS file system. 055 */ 056 MAC_OSX(4096, true, true, 255, 1024, new int[] { 057 // KEEP THIS ARRAY SORTED! 058 // @formatter:off 059 // ASCII NUL 060 0, 061 '/', 062 ':' 063 // @formatter:on 064 }, new String[] {}, false, false, '/'), 065 066 /** 067 * Windows file system. 068 * <p> 069 * The reserved characters are defined in the 070 * <a href="https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file">Naming Conventions 071 * (microsoft.com)</a>. 072 * </p> 073 * 074 * @see <a href="https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file">Naming Conventions 075 * (microsoft.com)</a> 076 * @see <a href="https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles"> 077 * CreateFileA function - Consoles (microsoft.com)</a> 078 */ 079 // @formatter:off 080 WINDOWS(4096, false, true, 081 255, 32000, // KEEP THIS ARRAY SORTED! 082 new int[] { 083 // KEEP THIS ARRAY SORTED! 084 // ASCII NUL 085 0, 086 // 1-31 may be allowed in file streams 087 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, 088 29, 30, 31, 089 '"', '*', '/', ':', '<', '>', '?', '\\', '|' 090 }, new String[] { 091 "AUX", 092 "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", 093 "COM\u00b2", "COM\u00b3", "COM\u00b9", // Superscript 2 3 1 in that order 094 "CON", "CONIN$", "CONOUT$", 095 "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", 096 "LPT\u00b2", "LPT\u00b3", "LPT\u00b9", // Superscript 2 3 1 in that order 097 "NUL", "PRN" 098 }, true, true, '\\'); 099 // @formatter:on 100 101 /** 102 * <p> 103 * Is {@code true} if this is Linux. 104 * </p> 105 * <p> 106 * The field will return {@code false} if {@code OS_NAME} is {@code null}. 107 * </p> 108 */ 109 private static final boolean IS_OS_LINUX = getOsMatchesName("Linux"); 110 111 /** 112 * <p> 113 * Is {@code true} if this is Mac. 114 * </p> 115 * <p> 116 * The field will return {@code false} if {@code OS_NAME} is {@code null}. 117 * </p> 118 */ 119 private static final boolean IS_OS_MAC = getOsMatchesName("Mac"); 120 121 /** 122 * The prefix String for all Windows OS. 123 */ 124 private static final String OS_NAME_WINDOWS_PREFIX = "Windows"; 125 126 /** 127 * <p> 128 * Is {@code true} if this is Windows. 129 * </p> 130 * <p> 131 * The field will return {@code false} if {@code OS_NAME} is {@code null}. 132 * </p> 133 */ 134 private static final boolean IS_OS_WINDOWS = getOsMatchesName(OS_NAME_WINDOWS_PREFIX); 135 136 /** 137 * The current FileSystem. 138 */ 139 private static final FileSystem CURRENT = current(); 140 141 /** 142 * Gets the current file system. 143 * 144 * @return the current file system 145 */ 146 private static FileSystem current() { 147 if (IS_OS_LINUX) { 148 return LINUX; 149 } 150 if (IS_OS_MAC) { 151 return MAC_OSX; 152 } 153 if (IS_OS_WINDOWS) { 154 return WINDOWS; 155 } 156 return GENERIC; 157 } 158 159 /** 160 * Gets the current file system. 161 * 162 * @return the current file system 163 */ 164 public static FileSystem getCurrent() { 165 return CURRENT; 166 } 167 168 /** 169 * Decides if the operating system matches. 170 * 171 * @param osNamePrefix 172 * the prefix for the os name 173 * @return true if matches, or false if not or can't determine 174 */ 175 private static boolean getOsMatchesName(final String osNamePrefix) { 176 return isOsNameMatch(getSystemProperty("os.name"), osNamePrefix); 177 } 178 179 /** 180 * <p> 181 * Gets a System property, defaulting to {@code null} if the property cannot be read. 182 * </p> 183 * <p> 184 * If a {@link SecurityException} is caught, the return value is {@code null} and a message is written to 185 * {@code System.err}. 186 * </p> 187 * 188 * @param property 189 * the system property name 190 * @return the system property value or {@code null} if a security problem occurs 191 */ 192 private static String getSystemProperty(final String property) { 193 try { 194 return System.getProperty(property); 195 } catch (final SecurityException ex) { 196 // we are not allowed to look at this property 197 System.err.println("Caught a SecurityException reading the system property '" + property 198 + "'; the SystemUtils property value will default to null."); 199 return null; 200 } 201 } 202 203 /** 204 * Copied from Apache Commons Lang CharSequenceUtils. 205 * 206 * Returns the index within {@code cs} of the first occurrence of the 207 * specified character, starting the search at the specified index. 208 * <p> 209 * If a character with value {@code searchChar} occurs in the 210 * character sequence represented by the {@code cs} 211 * object at an index no smaller than {@code start}, then 212 * the index of the first such occurrence is returned. For values 213 * of {@code searchChar} in the range from 0 to 0xFFFF (inclusive), 214 * this is the smallest value <em>k</em> such that: 215 * </p> 216 * <blockquote><pre> 217 * (this.charAt(<em>k</em>) == searchChar) && (<em>k</em> >= start) 218 * </pre></blockquote> 219 * is true. For other values of {@code searchChar}, it is the 220 * smallest value <em>k</em> such that: 221 * <blockquote><pre> 222 * (this.codePointAt(<em>k</em>) == searchChar) && (<em>k</em> >= start) 223 * </pre></blockquote> 224 * <p> 225 * is true. In either case, if no such character occurs in {@code cs} 226 * at or after position {@code start}, then 227 * {@code -1} is returned. 228 * </p> 229 * <p> 230 * There is no restriction on the value of {@code start}. If it 231 * is negative, it has the same effect as if it were zero: the entire 232 * {@link CharSequence} may be searched. If it is greater than 233 * the length of {@code cs}, it has the same effect as if it were 234 * equal to the length of {@code cs}: {@code -1} is returned. 235 * </p> 236 * <p>All indices are specified in {@code char} values 237 * (Unicode code units). 238 * </p> 239 * 240 * @param cs the {@link CharSequence} to be processed, not null 241 * @param searchChar the char to be searched for 242 * @param start the start index, negative starts at the string start 243 * @return the index where the search char was found, -1 if not found 244 * @since 3.6 updated to behave more like {@link String} 245 */ 246 private static int indexOf(final CharSequence cs, final int searchChar, int start) { 247 if (cs instanceof String) { 248 return ((String) cs).indexOf(searchChar, start); 249 } 250 final int sz = cs.length(); 251 if (start < 0) { 252 start = 0; 253 } 254 if (searchChar < Character.MIN_SUPPLEMENTARY_CODE_POINT) { 255 for (int i = start; i < sz; i++) { 256 if (cs.charAt(i) == searchChar) { 257 return i; 258 } 259 } 260 return -1; 261 } 262 //supplementary characters (LANG1300) 263 if (searchChar <= Character.MAX_CODE_POINT) { 264 final char[] chars = Character.toChars(searchChar); 265 for (int i = start; i < sz - 1; i++) { 266 final char high = cs.charAt(i); 267 final char low = cs.charAt(i + 1); 268 if (high == chars[0] && low == chars[1]) { 269 return i; 270 } 271 } 272 } 273 return -1; 274 } 275 276 /** 277 * Decides if the operating system matches. 278 * <p> 279 * This method is package private instead of private to support unit test invocation. 280 * </p> 281 * 282 * @param osName 283 * the actual OS name 284 * @param osNamePrefix 285 * the prefix for the expected OS name 286 * @return true if matches, or false if not or can't determine 287 */ 288 private static boolean isOsNameMatch(final String osName, final String osNamePrefix) { 289 if (osName == null) { 290 return false; 291 } 292 return osName.toUpperCase(Locale.ROOT).startsWith(osNamePrefix.toUpperCase(Locale.ROOT)); 293 } 294 295 /** 296 * Null-safe replace. 297 * 298 * @param path the path to be changed, null ignored. 299 * @param oldChar the old character. 300 * @param newChar the new character. 301 * @return the new path. 302 */ 303 private static String replace(final String path, final char oldChar, final char newChar) { 304 return path == null ? null : path.replace(oldChar, newChar); 305 } 306 307 private final int blockSize; 308 private final boolean casePreserving; 309 private final boolean caseSensitive; 310 private final int[] illegalFileNameChars; 311 private final int maxFileNameLength; 312 private final int maxPathLength; 313 private final String[] reservedFileNames; 314 private final boolean reservedFileNamesExtensions; 315 private final boolean supportsDriveLetter; 316 private final char nameSeparator; 317 private final char nameSeparatorOther; 318 319 /** 320 * Constructs a new instance. 321 * 322 * @param blockSize file allocation block size in bytes. 323 * @param caseSensitive Whether this file system is case-sensitive. 324 * @param casePreserving Whether this file system is case-preserving. 325 * @param maxFileLength The maximum length for file names. The file name does not include folders. 326 * @param maxPathLength The maximum length of the path to a file. This can include folders. 327 * @param illegalFileNameChars Illegal characters for this file system. 328 * @param reservedFileNames The reserved file names. 329 * @param reservedFileNamesExtensions TODO 330 * @param supportsDriveLetter Whether this file system support driver letters. 331 * @param nameSeparator The name separator, '\\' on Windows, '/' on Linux. 332 */ 333 FileSystem(final int blockSize, final boolean caseSensitive, final boolean casePreserving, 334 final int maxFileLength, final int maxPathLength, final int[] illegalFileNameChars, 335 final String[] reservedFileNames, final boolean reservedFileNamesExtensions, final boolean supportsDriveLetter, final char nameSeparator) { 336 this.blockSize = blockSize; 337 this.maxFileNameLength = maxFileLength; 338 this.maxPathLength = maxPathLength; 339 this.illegalFileNameChars = Objects.requireNonNull(illegalFileNameChars, "illegalFileNameChars"); 340 this.reservedFileNames = Objects.requireNonNull(reservedFileNames, "reservedFileNames"); 341 //Arrays.sort(this.reservedFileNames); 342 this.reservedFileNamesExtensions = reservedFileNamesExtensions; 343 this.caseSensitive = caseSensitive; 344 this.casePreserving = casePreserving; 345 this.supportsDriveLetter = supportsDriveLetter; 346 this.nameSeparator = nameSeparator; 347 this.nameSeparatorOther = FilenameUtils.flipSeparator(nameSeparator); 348 } 349 350 /** 351 * Gets the file allocation block size in bytes. 352 * @return the file allocation block size in bytes. 353 * 354 * @since 2.12.0 355 */ 356 public int getBlockSize() { 357 return blockSize; 358 } 359 360 /** 361 * Gets a cloned copy of the illegal characters for this file system. 362 * 363 * @return the illegal characters for this file system. 364 */ 365 public char[] getIllegalFileNameChars() { 366 final char[] chars = new char[illegalFileNameChars.length]; 367 for (int i = 0; i < illegalFileNameChars.length; i++) { 368 chars[i] = (char) illegalFileNameChars[i]; 369 } 370 return chars; 371 } 372 373 /** 374 * Gets a cloned copy of the illegal code points for this file system. 375 * 376 * @return the illegal code points for this file system. 377 * @since 2.12.0 378 */ 379 public int[] getIllegalFileNameCodePoints() { 380 return this.illegalFileNameChars.clone(); 381 } 382 383 /** 384 * Gets the maximum length for file names. The file name does not include folders. 385 * 386 * @return the maximum length for file names. 387 */ 388 public int getMaxFileNameLength() { 389 return maxFileNameLength; 390 } 391 392 /** 393 * Gets the maximum length of the path to a file. This can include folders. 394 * 395 * @return the maximum length of the path to a file. 396 */ 397 public int getMaxPathLength() { 398 return maxPathLength; 399 } 400 401 /** 402 * Gets the name separator, '\\' on Windows, '/' on Linux. 403 * 404 * @return '\\' on Windows, '/' on Linux. 405 * 406 * @since 2.12.0 407 */ 408 public char getNameSeparator() { 409 return nameSeparator; 410 } 411 412 /** 413 * Gets a cloned copy of the reserved file names. 414 * 415 * @return the reserved file names. 416 */ 417 public String[] getReservedFileNames() { 418 return reservedFileNames.clone(); 419 } 420 421 /** 422 * Tests whether this file system preserves case. 423 * 424 * @return Whether this file system preserves case. 425 */ 426 public boolean isCasePreserving() { 427 return casePreserving; 428 } 429 430 /** 431 * Tests whether this file system is case-sensitive. 432 * 433 * @return Whether this file system is case-sensitive. 434 */ 435 public boolean isCaseSensitive() { 436 return caseSensitive; 437 } 438 439 /** 440 * Tests if the given character is illegal in a file name, {@code false} otherwise. 441 * 442 * @param c 443 * the character to test 444 * @return {@code true} if the given character is illegal in a file name, {@code false} otherwise. 445 */ 446 private boolean isIllegalFileNameChar(final int c) { 447 return Arrays.binarySearch(illegalFileNameChars, c) >= 0; 448 } 449 450 /** 451 * Tests if a candidate file name (without a path) such as {@code "filename.ext"} or {@code "filename"} is a 452 * potentially legal file name. If the file name length exceeds {@link #getMaxFileNameLength()}, or if it contains 453 * an illegal character then the check fails. 454 * 455 * @param candidate 456 * a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} 457 * @return {@code true} if the candidate name is legal 458 */ 459 public boolean isLegalFileName(final CharSequence candidate) { 460 if (candidate == null || candidate.length() == 0 || candidate.length() > maxFileNameLength) { 461 return false; 462 } 463 if (isReservedFileName(candidate)) { 464 return false; 465 } 466 return candidate.chars().noneMatch(this::isIllegalFileNameChar); 467 } 468 469 /** 470 * Tests whether the given string is a reserved file name. 471 * 472 * @param candidate 473 * the string to test 474 * @return {@code true} if the given string is a reserved file name. 475 */ 476 public boolean isReservedFileName(final CharSequence candidate) { 477 final CharSequence test = reservedFileNamesExtensions ? trimExtension(candidate) : candidate; 478 return Arrays.binarySearch(reservedFileNames, test) >= 0; 479 } 480 481 /** 482 * Converts all separators to the Windows separator of backslash. 483 * 484 * @param path the path to be changed, null ignored 485 * @return the updated path 486 * @since 2.12.0 487 */ 488 public String normalizeSeparators(final String path) { 489 return replace(path, nameSeparatorOther, nameSeparator); 490 } 491 492 /** 493 * Tests whether this file system support driver letters. 494 * <p> 495 * Windows supports driver letters as do other operating systems. Whether these other OS's still support Java like 496 * OS/2, is a different matter. 497 * </p> 498 * 499 * @return whether this file system support driver letters. 500 * @since 2.9.0 501 * @see <a href="https://en.wikipedia.org/wiki/Drive_letter_assignment">Operating systems that use drive letter 502 * assignment</a> 503 */ 504 public boolean supportsDriveLetter() { 505 return supportsDriveLetter; 506 } 507 508 /** 509 * Converts a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} to a legal file 510 * name. Illegal characters in the candidate name are replaced by the {@code replacement} character. If the file 511 * name length exceeds {@link #getMaxFileNameLength()}, then the name is truncated to 512 * {@link #getMaxFileNameLength()}. 513 * 514 * @param candidate 515 * a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} 516 * @param replacement 517 * Illegal characters in the candidate name are replaced by this character 518 * @return a String without illegal characters 519 */ 520 public String toLegalFileName(final String candidate, final char replacement) { 521 if (isIllegalFileNameChar(replacement)) { 522 // %s does not work properly with NUL 523 throw new IllegalArgumentException(String.format("The replacement character '%s' cannot be one of the %s illegal characters: %s", 524 replacement == '\0' ? "\\0" : replacement, name(), Arrays.toString(illegalFileNameChars))); 525 } 526 final String truncated = candidate.length() > maxFileNameLength ? candidate.substring(0, maxFileNameLength) : candidate; 527 final int[] array = truncated.chars().map(i -> isIllegalFileNameChar(i) ? replacement : i).toArray(); 528 return new String(array, 0, array.length); 529 } 530 531 CharSequence trimExtension(final CharSequence cs) { 532 final int index = indexOf(cs, '.', 0); 533 return index < 0 ? cs : cs.subSequence(0, index); 534 } 535}