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(false, false, Integer.MAX_VALUE, Integer.MAX_VALUE, new char[] { 0 }, new String[] {}, false), 040 041 /** 042 * Linux file system. 043 */ 044 LINUX(true, true, 255, 4096, new char[] { 045 // KEEP THIS ARRAY SORTED! 046 // @formatter:off 047 // ASCII NUL 048 0, 049 '/' 050 // @formatter:on 051 }, new String[] {}, false), 052 053 /** 054 * MacOS file system. 055 */ 056 MAC_OSX(true, true, 255, 1024, new char[] { 057 // KEEP THIS ARRAY SORTED! 058 // @formatter:off 059 // ASCII NUL 060 0, 061 '/', 062 ':' 063 // @formatter:on 064 }, new String[] {}, 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 */ 077 WINDOWS(false, true, 255, 078 32000, new char[] { 079 // KEEP THIS ARRAY SORTED! 080 // @formatter:off 081 // ASCII NUL 082 0, 083 // 1-31 may be allowed in file streams 084 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, 085 29, 30, 31, 086 '"', '*', '/', ':', '<', '>', '?', '\\', '|' 087 // @formatter:on 088 }, // KEEP THIS ARRAY SORTED! 089 new String[] { "AUX", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "CON", "LPT1", 090 "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "NUL", "PRN" }, true); 091 092 /** 093 * <p> 094 * Is {@code true} if this is Linux. 095 * </p> 096 * <p> 097 * The field will return {@code false} if {@code OS_NAME} is {@code null}. 098 * </p> 099 */ 100 private static final boolean IS_OS_LINUX = getOsMatchesName("Linux"); 101 102 /** 103 * <p> 104 * Is {@code true} if this is Mac. 105 * </p> 106 * <p> 107 * The field will return {@code false} if {@code OS_NAME} is {@code null}. 108 * </p> 109 */ 110 private static final boolean IS_OS_MAC = getOsMatchesName("Mac"); 111 112 /** 113 * The prefix String for all Windows OS. 114 */ 115 private static final String OS_NAME_WINDOWS_PREFIX = "Windows"; 116 117 /** 118 * <p> 119 * Is {@code true} if this is Windows. 120 * </p> 121 * <p> 122 * The field will return {@code false} if {@code OS_NAME} is {@code null}. 123 * </p> 124 */ 125 private static final boolean IS_OS_WINDOWS = getOsMatchesName(OS_NAME_WINDOWS_PREFIX); 126 127 /** 128 * Gets the current file system. 129 * 130 * @return the current file system 131 */ 132 public static FileSystem getCurrent() { 133 if (IS_OS_LINUX) { 134 return LINUX; 135 } 136 if (IS_OS_MAC) { 137 return FileSystem.MAC_OSX; 138 } 139 if (IS_OS_WINDOWS) { 140 return FileSystem.WINDOWS; 141 } 142 return GENERIC; 143 } 144 145 /** 146 * Decides if the operating system matches. 147 * 148 * @param osNamePrefix 149 * the prefix for the os name 150 * @return true if matches, or false if not or can't determine 151 */ 152 private static boolean getOsMatchesName(final String osNamePrefix) { 153 return isOsNameMatch(getSystemProperty("os.name"), osNamePrefix); 154 } 155 156 /** 157 * <p> 158 * Gets a System property, defaulting to {@code null} if the property cannot be read. 159 * </p> 160 * <p> 161 * If a {@code SecurityException} is caught, the return value is {@code null} and a message is written to 162 * {@code System.err}. 163 * </p> 164 * 165 * @param property 166 * the system property name 167 * @return the system property value or {@code null} if a security problem occurs 168 */ 169 private static String getSystemProperty(final String property) { 170 try { 171 return System.getProperty(property); 172 } catch (final SecurityException ex) { 173 // we are not allowed to look at this property 174 System.err.println("Caught a SecurityException reading the system property '" + property 175 + "'; the SystemUtils property value will default to null."); 176 return null; 177 } 178 } 179 180 /** 181 * Decides if the operating system matches. 182 * <p> 183 * This method is package private instead of private to support unit test invocation. 184 * </p> 185 * 186 * @param osName 187 * the actual OS name 188 * @param osNamePrefix 189 * the prefix for the expected OS name 190 * @return true if matches, or false if not or can't determine 191 */ 192 private static boolean isOsNameMatch(final String osName, final String osNamePrefix) { 193 if (osName == null) { 194 return false; 195 } 196 return osName.toUpperCase(Locale.ROOT).startsWith(osNamePrefix.toUpperCase(Locale.ROOT)); 197 } 198 199 private final boolean casePreserving; 200 private final boolean caseSensitive; 201 private final char[] illegalFileNameChars; 202 private final int maxFileNameLength; 203 private final int maxPathLength; 204 private final String[] reservedFileNames; 205 private final boolean supportsDriveLetter; 206 207 /** 208 * Constructs a new instance. 209 * 210 * @param caseSensitive Whether this file system is case sensitive. 211 * @param casePreserving Whether this file system is case preserving. 212 * @param maxFileLength The maximum length for file names. The file name does not include folders. 213 * @param maxPathLength The maximum length of the path to a file. This can include folders. 214 * @param illegalFileNameChars Illegal characters for this file system. 215 * @param reservedFileNames The reserved file names. 216 * @param supportsDriveLetter Whether this file system support driver letters. 217 */ 218 FileSystem(final boolean caseSensitive, final boolean casePreserving, final int maxFileLength, 219 final int maxPathLength, final char[] illegalFileNameChars, final String[] reservedFileNames, 220 final boolean supportsDriveLetter) { 221 this.maxFileNameLength = maxFileLength; 222 this.maxPathLength = maxPathLength; 223 this.illegalFileNameChars = Objects.requireNonNull(illegalFileNameChars, "illegalFileNameChars"); 224 this.reservedFileNames = Objects.requireNonNull(reservedFileNames, "reservedFileNames"); 225 this.caseSensitive = caseSensitive; 226 this.casePreserving = casePreserving; 227 this.supportsDriveLetter = supportsDriveLetter; 228 } 229 230 /** 231 * Gets a cloned copy of the illegal characters for this file system. 232 * 233 * @return the illegal characters for this file system. 234 */ 235 public char[] getIllegalFileNameChars() { 236 return this.illegalFileNameChars.clone(); 237 } 238 239 /** 240 * Gets the maximum length for file names. The file name does not include folders. 241 * 242 * @return the maximum length for file names. 243 */ 244 public int getMaxFileNameLength() { 245 return maxFileNameLength; 246 } 247 248 /** 249 * Gets the maximum length of the path to a file. This can include folders. 250 * 251 * @return the maximum length of the path to a file. 252 */ 253 public int getMaxPathLength() { 254 return maxPathLength; 255 } 256 257 /** 258 * Gets a cloned copy of the reserved file names. 259 * 260 * @return the reserved file names. 261 */ 262 public String[] getReservedFileNames() { 263 return reservedFileNames.clone(); 264 } 265 266 /** 267 * Whether this file system preserves case. 268 * 269 * @return Whether this file system preserves case. 270 */ 271 public boolean isCasePreserving() { 272 return casePreserving; 273 } 274 275 /** 276 * Whether this file system is case-sensitive. 277 * 278 * @return Whether this file system is case-sensitive. 279 */ 280 public boolean isCaseSensitive() { 281 return caseSensitive; 282 } 283 284 /** 285 * Returns {@code true} if the given character is illegal in a file name, {@code false} otherwise. 286 * 287 * @param c 288 * the character to test 289 * @return {@code true} if the given character is illegal in a file name, {@code false} otherwise. 290 */ 291 private boolean isIllegalFileNameChar(final char c) { 292 return Arrays.binarySearch(illegalFileNameChars, c) >= 0; 293 } 294 295 /** 296 * Checks if a candidate file name (without a path) such as {@code "filename.ext"} or {@code "filename"} is a 297 * potentially legal file name. If the file name length exceeds {@link #getMaxFileNameLength()}, or if it contains 298 * an illegal character then the check fails. 299 * 300 * @param candidate 301 * a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} 302 * @return {@code true} if the candidate name is legal 303 */ 304 public boolean isLegalFileName(final CharSequence candidate) { 305 if (candidate == null || candidate.length() == 0 || candidate.length() > maxFileNameLength) { 306 return false; 307 } 308 if (isReservedFileName(candidate)) { 309 return false; 310 } 311 for (int i = 0; i < candidate.length(); i++) { 312 if (isIllegalFileNameChar(candidate.charAt(i))) { 313 return false; 314 } 315 } 316 return true; 317 } 318 319 /** 320 * Returns whether the given string is a reserved file name. 321 * 322 * @param candidate 323 * the string to test 324 * @return {@code true} if the given string is a reserved file name. 325 */ 326 public boolean isReservedFileName(final CharSequence candidate) { 327 return Arrays.binarySearch(reservedFileNames, candidate) >= 0; 328 } 329 330 /** 331 * Tests whether this file system support driver letters. 332 * <p> 333 * Windows supports driver letters as do other operating systems. Whether these other OS's still support Java like 334 * OS/2, is a different matter. 335 * </p> 336 * 337 * @return whether this file system support driver letters. 338 * @since 2.9.0 339 * @see <a href="https://en.wikipedia.org/wiki/Drive_letter_assignment">Operating systems that use drive letter 340 * assignment</a> 341 */ 342 public boolean supportsDriveLetter() { 343 return supportsDriveLetter; 344 } 345 346 /** 347 * Converts a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} to a legal file 348 * name. Illegal characters in the candidate name are replaced by the {@code replacement} character. If the file 349 * name length exceeds {@link #getMaxFileNameLength()}, then the name is truncated to 350 * {@link #getMaxFileNameLength()}. 351 * 352 * @param candidate 353 * a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} 354 * @param replacement 355 * Illegal characters in the candidate name are replaced by this character 356 * @return a String without illegal characters 357 */ 358 public String toLegalFileName(final String candidate, final char replacement) { 359 if (isIllegalFileNameChar(replacement)) { 360 throw new IllegalArgumentException( 361 String.format("The replacement character '%s' cannot be one of the %s illegal characters: %s", 362 // %s does not work properly with NUL 363 replacement == '\0' ? "\\0" : replacement, name(), Arrays.toString(illegalFileNameChars))); 364 } 365 final String truncated = candidate.length() > maxFileNameLength ? candidate.substring(0, maxFileNameLength) 366 : candidate; 367 boolean changed = false; 368 final char[] charArray = truncated.toCharArray(); 369 for (int i = 0; i < charArray.length; i++) { 370 if (isIllegalFileNameChar(charArray[i])) { 371 charArray[i] = replacement; 372 changed = true; 373 } 374 } 375 return changed ? String.valueOf(charArray) : truncated; 376 } 377}