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