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