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 * http://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 java.util.Arrays; 21 import java.util.Locale; 22 import java.util.Objects; 23 24 /** 25 * Abstracts an OS' file system details, currently supporting the single use case of converting a file name String to a 26 * legal file name with {@link #toLegalFileName(String, char)}. 27 * <p> 28 * The starting point of any operation is {@link #getCurrent()} which gets you the enum for the file system that matches 29 * the OS hosting the running JVM. 30 * </p> 31 * 32 * @since 2.7 33 */ 34 public enum FileSystem { 35 36 /** 37 * Generic file system. 38 */ 39 GENERIC(false, false, Integer.MAX_VALUE, Integer.MAX_VALUE, new int[] { 0 }, new String[] {}, false, false, '/'), 40 41 /** 42 * Linux file system. 43 */ 44 LINUX(true, true, 255, 4096, new int[] { 45 // KEEP THIS ARRAY SORTED! 46 // @formatter:off 47 // ASCII NUL 48 0, 49 '/' 50 // @formatter:on 51 }, new String[] {}, false, false, '/'), 52 53 /** 54 * MacOS file system. 55 */ 56 MAC_OSX(true, true, 255, 1024, new int[] { 57 // KEEP THIS ARRAY SORTED! 58 // @formatter:off 59 // ASCII NUL 60 0, 61 '/', 62 ':' 63 // @formatter:on 64 }, new String[] {}, false, false, '/'), 65 66 /** 67 * Windows file system. 68 * <p> 69 * The reserved characters are defined in the 70 * <a href="https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file">Naming Conventions 71 * (microsoft.com)</a>. 72 * </p> 73 * 74 * @see <a href="https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file">Naming Conventions 75 * (microsoft.com)</a> 76 * @see <a href="https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#consoles"> 77 * CreateFileA function - Consoles (microsoft.com)</a> 78 */ 79 WINDOWS(false, true, 255, 80 32000, new int[] { 81 // KEEP THIS ARRAY SORTED! 82 // @formatter:off 83 // ASCII NUL 84 0, 85 // 1-31 may be allowed in file streams 86 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, 87 29, 30, 31, 88 '"', '*', '/', ':', '<', '>', '?', '\\', '|' 89 // @formatter:on 90 }, // KEEP THIS ARRAY SORTED! 91 new String[] { "AUX", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "CON", "CONIN$", "CONOUT$", 92 "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "NUL", "PRN" }, true, true, '\\'); 93 94 /** 95 * <p> 96 * Is {@code true} if this is Linux. 97 * </p> 98 * <p> 99 * The field will return {@code false} if {@code OS_NAME} is {@code null}. 100 * </p> 101 */ 102 private static final boolean IS_OS_LINUX = getOsMatchesName("Linux"); 103 104 /** 105 * <p> 106 * Is {@code true} if this is Mac. 107 * </p> 108 * <p> 109 * The field will return {@code false} if {@code OS_NAME} is {@code null}. 110 * </p> 111 */ 112 private static final boolean IS_OS_MAC = getOsMatchesName("Mac"); 113 114 /** 115 * The prefix String for all Windows OS. 116 */ 117 private static final String OS_NAME_WINDOWS_PREFIX = "Windows"; 118 119 /** 120 * <p> 121 * Is {@code true} if this is Windows. 122 * </p> 123 * <p> 124 * The field will return {@code false} if {@code OS_NAME} is {@code null}. 125 * </p> 126 */ 127 private static final boolean IS_OS_WINDOWS = getOsMatchesName(OS_NAME_WINDOWS_PREFIX); 128 129 /** 130 * The current FileSystem. 131 */ 132 private static final FileSystem CURRENT = current(); 133 134 /** 135 * Gets the current file system. 136 * 137 * @return the current file system 138 */ current()139 private static FileSystem current() { 140 if (IS_OS_LINUX) { 141 return LINUX; 142 } 143 if (IS_OS_MAC) { 144 return MAC_OSX; 145 } 146 if (IS_OS_WINDOWS) { 147 return WINDOWS; 148 } 149 return GENERIC; 150 } 151 152 /** 153 * Gets the current file system. 154 * 155 * @return the current file system 156 */ getCurrent()157 public static FileSystem getCurrent() { 158 return CURRENT; 159 } 160 161 /** 162 * Decides if the operating system matches. 163 * 164 * @param osNamePrefix 165 * the prefix for the os name 166 * @return true if matches, or false if not or can't determine 167 */ getOsMatchesName(final String osNamePrefix)168 private static boolean getOsMatchesName(final String osNamePrefix) { 169 return isOsNameMatch(getSystemProperty("os.name"), osNamePrefix); 170 } 171 172 /** 173 * <p> 174 * Gets a System property, defaulting to {@code null} if the property cannot be read. 175 * </p> 176 * <p> 177 * If a {@link SecurityException} is caught, the return value is {@code null} and a message is written to 178 * {@code System.err}. 179 * </p> 180 * 181 * @param property 182 * the system property name 183 * @return the system property value or {@code null} if a security problem occurs 184 */ getSystemProperty(final String property)185 private static String getSystemProperty(final String property) { 186 try { 187 return System.getProperty(property); 188 } catch (final SecurityException ex) { 189 // we are not allowed to look at this property 190 System.err.println("Caught a SecurityException reading the system property '" + property 191 + "'; the SystemUtils property value will default to null."); 192 return null; 193 } 194 } 195 196 /** 197 * Copied from Apache Commons Lang CharSequenceUtils. 198 * 199 * Returns the index within {@code cs} of the first occurrence of the 200 * specified character, starting the search at the specified index. 201 * <p> 202 * If a character with value {@code searchChar} occurs in the 203 * character sequence represented by the {@code cs} 204 * object at an index no smaller than {@code start}, then 205 * the index of the first such occurrence is returned. For values 206 * of {@code searchChar} in the range from 0 to 0xFFFF (inclusive), 207 * this is the smallest value <i>k</i> such that: 208 * </p> 209 * <blockquote><pre> 210 * (this.charAt(<i>k</i>) == searchChar) && (<i>k</i> >= start) 211 * </pre></blockquote> 212 * is true. For other values of {@code searchChar}, it is the 213 * smallest value <i>k</i> such that: 214 * <blockquote><pre> 215 * (this.codePointAt(<i>k</i>) == searchChar) && (<i>k</i> >= start) 216 * </pre></blockquote> 217 * <p> 218 * is true. In either case, if no such character occurs inm {@code cs} 219 * at or after position {@code start}, then 220 * {@code -1} is returned. 221 * </p> 222 * <p> 223 * There is no restriction on the value of {@code start}. If it 224 * is negative, it has the same effect as if it were zero: the entire 225 * {@link CharSequence} may be searched. If it is greater than 226 * the length of {@code cs}, it has the same effect as if it were 227 * equal to the length of {@code cs}: {@code -1} is returned. 228 * </p> 229 * <p>All indices are specified in {@code char} values 230 * (Unicode code units). 231 * </p> 232 * 233 * @param cs the {@link CharSequence} to be processed, not null 234 * @param searchChar the char to be searched for 235 * @param start the start index, negative starts at the string start 236 * @return the index where the search char was found, -1 if not found 237 * @since 3.6 updated to behave more like {@link String} 238 */ indexOf(final CharSequence cs, final int searchChar, int start)239 private static int indexOf(final CharSequence cs, final int searchChar, int start) { 240 if (cs instanceof String) { 241 return ((String) cs).indexOf(searchChar, start); 242 } 243 final int sz = cs.length(); 244 if (start < 0) { 245 start = 0; 246 } 247 if (searchChar < Character.MIN_SUPPLEMENTARY_CODE_POINT) { 248 for (int i = start; i < sz; i++) { 249 if (cs.charAt(i) == searchChar) { 250 return i; 251 } 252 } 253 return -1; 254 } 255 //supplementary characters (LANG1300) 256 if (searchChar <= Character.MAX_CODE_POINT) { 257 final char[] chars = Character.toChars(searchChar); 258 for (int i = start; i < sz - 1; i++) { 259 final char high = cs.charAt(i); 260 final char low = cs.charAt(i + 1); 261 if (high == chars[0] && low == chars[1]) { 262 return i; 263 } 264 } 265 } 266 return -1; 267 } 268 269 /** 270 * Decides if the operating system matches. 271 * <p> 272 * This method is package private instead of private to support unit test invocation. 273 * </p> 274 * 275 * @param osName 276 * the actual OS name 277 * @param osNamePrefix 278 * the prefix for the expected OS name 279 * @return true if matches, or false if not or can't determine 280 */ isOsNameMatch(final String osName, final String osNamePrefix)281 private static boolean isOsNameMatch(final String osName, final String osNamePrefix) { 282 if (osName == null) { 283 return false; 284 } 285 return osName.toUpperCase(Locale.ROOT).startsWith(osNamePrefix.toUpperCase(Locale.ROOT)); 286 } 287 288 /** 289 * Null-safe replace. 290 * 291 * @param path the path to be changed, null ignored. 292 * @param oldChar the old character. 293 * @param newChar the new character. 294 * @return the new path. 295 */ replace(final String path, final char oldChar, final char newChar)296 private static String replace(final String path, final char oldChar, final char newChar) { 297 return path == null ? null : path.replace(oldChar, newChar); 298 } 299 private final boolean casePreserving; 300 private final boolean caseSensitive; 301 private final int[] illegalFileNameChars; 302 private final int maxFileNameLength; 303 private final int maxPathLength; 304 private final String[] reservedFileNames; 305 private final boolean reservedFileNamesExtensions; 306 private final boolean supportsDriveLetter; 307 private final char nameSeparator; 308 309 private final char nameSeparatorOther; 310 311 /** 312 * Constructs a new instance. 313 * 314 * @param caseSensitive Whether this file system is case-sensitive. 315 * @param casePreserving Whether this file system is case-preserving. 316 * @param maxFileLength The maximum length for file names. The file name does not include folders. 317 * @param maxPathLength The maximum length of the path to a file. This can include folders. 318 * @param illegalFileNameChars Illegal characters for this file system. 319 * @param reservedFileNames The reserved file names. 320 * @param reservedFileNamesExtensions TODO 321 * @param supportsDriveLetter Whether this file system support driver letters. 322 * @param nameSeparator The name separator, '\\' on Windows, '/' on Linux. 323 */ FileSystem(final boolean caseSensitive, final boolean casePreserving, final int maxFileLength, final int maxPathLength, final int[] illegalFileNameChars, final String[] reservedFileNames, final boolean reservedFileNamesExtensions, final boolean supportsDriveLetter, final char nameSeparator)324 FileSystem(final boolean caseSensitive, final boolean casePreserving, final int maxFileLength, 325 final int maxPathLength, final int[] illegalFileNameChars, final String[] reservedFileNames, 326 final boolean reservedFileNamesExtensions, final boolean supportsDriveLetter, final char nameSeparator) { 327 this.maxFileNameLength = maxFileLength; 328 this.maxPathLength = maxPathLength; 329 this.illegalFileNameChars = Objects.requireNonNull(illegalFileNameChars, "illegalFileNameChars"); 330 this.reservedFileNames = Objects.requireNonNull(reservedFileNames, "reservedFileNames"); 331 this.reservedFileNamesExtensions = reservedFileNamesExtensions; 332 this.caseSensitive = caseSensitive; 333 this.casePreserving = casePreserving; 334 this.supportsDriveLetter = supportsDriveLetter; 335 this.nameSeparator = nameSeparator; 336 this.nameSeparatorOther = FilenameUtils.flipSeparator(nameSeparator); 337 } 338 339 /** 340 * Gets a cloned copy of the illegal characters for this file system. 341 * 342 * @return the illegal characters for this file system. 343 */ getIllegalFileNameChars()344 public char[] getIllegalFileNameChars() { 345 final char[] chars = new char[illegalFileNameChars.length]; 346 for (int i = 0; i < illegalFileNameChars.length; i++) { 347 chars[i] = (char) illegalFileNameChars[i]; 348 } 349 return chars; 350 } 351 352 /** 353 * Gets a cloned copy of the illegal code points for this file system. 354 * 355 * @return the illegal code points for this file system. 356 * @since 2.12.0 357 */ getIllegalFileNameCodePoints()358 public int[] getIllegalFileNameCodePoints() { 359 return this.illegalFileNameChars.clone(); 360 } 361 362 /** 363 * Gets the maximum length for file names. The file name does not include folders. 364 * 365 * @return the maximum length for file names. 366 */ getMaxFileNameLength()367 public int getMaxFileNameLength() { 368 return maxFileNameLength; 369 } 370 371 /** 372 * Gets the maximum length of the path to a file. This can include folders. 373 * 374 * @return the maximum length of the path to a file. 375 */ getMaxPathLength()376 public int getMaxPathLength() { 377 return maxPathLength; 378 } 379 380 /** 381 * Gets the name separator, '\\' on Windows, '/' on Linux. 382 * 383 * @return '\\' on Windows, '/' on Linux. 384 * 385 * @since 2.12.0 386 */ getNameSeparator()387 public char getNameSeparator() { 388 return nameSeparator; 389 } 390 391 /** 392 * Gets a cloned copy of the reserved file names. 393 * 394 * @return the reserved file names. 395 */ getReservedFileNames()396 public String[] getReservedFileNames() { 397 return reservedFileNames.clone(); 398 } 399 400 /** 401 * Tests whether this file system preserves case. 402 * 403 * @return Whether this file system preserves case. 404 */ isCasePreserving()405 public boolean isCasePreserving() { 406 return casePreserving; 407 } 408 409 /** 410 * Tests whether this file system is case-sensitive. 411 * 412 * @return Whether this file system is case-sensitive. 413 */ isCaseSensitive()414 public boolean isCaseSensitive() { 415 return caseSensitive; 416 } 417 418 /** 419 * Tests if the given character is illegal in a file name, {@code false} otherwise. 420 * 421 * @param c 422 * the character to test 423 * @return {@code true} if the given character is illegal in a file name, {@code false} otherwise. 424 */ isIllegalFileNameChar(final int c)425 private boolean isIllegalFileNameChar(final int c) { 426 return Arrays.binarySearch(illegalFileNameChars, c) >= 0; 427 } 428 429 /** 430 * Tests if a candidate file name (without a path) such as {@code "filename.ext"} or {@code "filename"} is a 431 * potentially legal file name. If the file name length exceeds {@link #getMaxFileNameLength()}, or if it contains 432 * an illegal character then the check fails. 433 * 434 * @param candidate 435 * a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} 436 * @return {@code true} if the candidate name is legal 437 */ isLegalFileName(final CharSequence candidate)438 public boolean isLegalFileName(final CharSequence candidate) { 439 if (candidate == null || candidate.length() == 0 || candidate.length() > maxFileNameLength) { 440 return false; 441 } 442 if (isReservedFileName(candidate)) { 443 return false; 444 } 445 return candidate.chars().noneMatch(this::isIllegalFileNameChar); 446 } 447 448 /** 449 * Tests whether the given string is a reserved file name. 450 * 451 * @param candidate 452 * the string to test 453 * @return {@code true} if the given string is a reserved file name. 454 */ isReservedFileName(final CharSequence candidate)455 public boolean isReservedFileName(final CharSequence candidate) { 456 final CharSequence test = reservedFileNamesExtensions ? trimExtension(candidate) : candidate; 457 return Arrays.binarySearch(reservedFileNames, test) >= 0; 458 } 459 460 /** 461 * Converts all separators to the Windows separator of backslash. 462 * 463 * @param path the path to be changed, null ignored 464 * @return the updated path 465 * @since 2.12.0 466 */ normalizeSeparators(final String path)467 public String normalizeSeparators(final String path) { 468 return replace(path, nameSeparatorOther, nameSeparator); 469 } 470 471 /** 472 * Tests whether this file system support driver letters. 473 * <p> 474 * Windows supports driver letters as do other operating systems. Whether these other OS's still support Java like 475 * OS/2, is a different matter. 476 * </p> 477 * 478 * @return whether this file system support driver letters. 479 * @since 2.9.0 480 * @see <a href="https://en.wikipedia.org/wiki/Drive_letter_assignment">Operating systems that use drive letter 481 * assignment</a> 482 */ supportsDriveLetter()483 public boolean supportsDriveLetter() { 484 return supportsDriveLetter; 485 } 486 487 /** 488 * Converts a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} to a legal file 489 * name. Illegal characters in the candidate name are replaced by the {@code replacement} character. If the file 490 * name length exceeds {@link #getMaxFileNameLength()}, then the name is truncated to 491 * {@link #getMaxFileNameLength()}. 492 * 493 * @param candidate 494 * a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} 495 * @param replacement 496 * Illegal characters in the candidate name are replaced by this character 497 * @return a String without illegal characters 498 */ toLegalFileName(final String candidate, final char replacement)499 public String toLegalFileName(final String candidate, final char replacement) { 500 if (isIllegalFileNameChar(replacement)) { 501 // %s does not work properly with NUL 502 throw new IllegalArgumentException(String.format("The replacement character '%s' cannot be one of the %s illegal characters: %s", 503 replacement == '\0' ? "\\0" : replacement, name(), Arrays.toString(illegalFileNameChars))); 504 } 505 final String truncated = candidate.length() > maxFileNameLength ? candidate.substring(0, maxFileNameLength) : candidate; 506 final int[] array = truncated.chars().map(i -> isIllegalFileNameChar(i) ? replacement : i).toArray(); 507 return new String(array, 0, array.length); 508 } 509 trimExtension(final CharSequence cs)510 CharSequence trimExtension(final CharSequence cs) { 511 final int index = indexOf(cs, '.', 0); 512 return index < 0 ? cs : cs.subSequence(0, index); 513 } 514 } 515