• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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) &amp;&amp; (<i>k</i> &gt;= 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) &amp;&amp; (<i>k</i> &gt;= 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