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