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 .Objects ;
22+
23+ /**
24+ * Abstracts an OS' file system details, currently supporting the single use case of converting a file name String to a
25+ * legal file name with {@link #toLegalFileName(String, char)}.
26+ * <p>
27+ * The starting point of any operation is {@link #getCurrent()} which gets you the enum for the file system that matches
28+ * the OS hosting the running JVM.
29+ * </p>
30+ *
31+ * @since 2.7
32+ */
33+ public enum FileSystem {
34+
35+ LINUX (255 , 4096 , new char [] {
36+ // KEEP THIS ARRAY SORTED!
37+ // @formatter:off
38+ // ASCII NULL
39+ 0 ,
40+ '/'
41+ // @formatter:on
42+ }),
43+
44+ MAC_OSX (255 , 1024 , new char [] {
45+ // KEEP THIS ARRAY SORTED!
46+ // @formatter:off
47+ // ASCII NULL
48+ 0 ,
49+ '/' ,
50+ ':'
51+ // @formatter:on
52+ }),
53+
54+ GENERIC (Integer .MAX_VALUE , Integer .MAX_VALUE , new char [] { 0 }),
55+
56+ WINDOWS (255 , 32000 , new char [] {
57+ // KEEP THIS ARRAY SORTED!
58+ // @formatter:off
59+ // ASCII NULL
60+ 0 ,
61+ // 1-31 may be allowed in file streams
62+ 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 ,
63+ 29 , 30 , 31 ,
64+ '"' , '*' , '/' , ':' , '<' , '>' , '?' , '\\' , '|'
65+ // @formatter:on
66+ });
67+
68+ /**
69+ * The prefix String for all Windows OS.
70+ */
71+ private static final String OS_NAME_WINDOWS_PREFIX = "Windows" ;
72+
73+ /**
74+ * <p>
75+ * Is {@code true} if this is Windows.
76+ * </p>
77+ * <p>
78+ * The field will return {@code false} if {@code OS_NAME} is {@code null}.
79+ * </p>
80+ */
81+ private static final boolean IS_OS_WINDOWS = getOsMatchesName (OS_NAME_WINDOWS_PREFIX );
82+
83+ /**
84+ * <p>
85+ * Is {@code true} if this is Linux.
86+ * </p>
87+ * <p>
88+ * The field will return {@code false} if {@code OS_NAME} is {@code null}.
89+ * </p>
90+ */
91+ private static final boolean IS_OS_LINUX = getOsMatchesName ("Linux" ) || getOsMatchesName ("LINUX" );
92+
93+ /**
94+ * <p>
95+ * Is {@code true} if this is Mac.
96+ * </p>
97+ * <p>
98+ * The field will return {@code false} if {@code OS_NAME} is {@code null}.
99+ * </p>
100+ */
101+ private static final boolean IS_OS_MAC = getOsMatchesName ("Mac" );
102+
103+ private static final String OS_NAME = getSystemProperty ("os.name" );
104+
105+ public static FileSystem getCurrent () {
106+ if (IS_OS_LINUX ) {
107+ return LINUX ;
108+ }
109+ if (IS_OS_MAC ) {
110+ return FileSystem .MAC_OSX ;
111+ }
112+ if (IS_OS_WINDOWS ) {
113+ return FileSystem .WINDOWS ;
114+ }
115+ return GENERIC ;
116+ }
117+
118+ /**
119+ * Decides if the operating system matches.
120+ *
121+ * @param osNamePrefix
122+ * the prefix for the os name
123+ * @return true if matches, or false if not or can't determine
124+ */
125+ private static boolean getOsMatchesName (final String osNamePrefix ) {
126+ return isOsNameMatch (OS_NAME , osNamePrefix );
127+ }
128+
129+ /**
130+ * <p>
131+ * Gets a System property, defaulting to {@code null} if the property cannot be read.
132+ * </p>
133+ * <p>
134+ * If a {@code SecurityException} is caught, the return value is {@code null} and a message is written to
135+ * {@code System.err}.
136+ * </p>
137+ *
138+ * @param property
139+ * the system property name
140+ * @return the system property value or {@code null} if a security problem occurs
141+ */
142+ private static String getSystemProperty (final String property ) {
143+ try {
144+ return System .getProperty (property );
145+ } catch (final SecurityException ex ) {
146+ // we are not allowed to look at this property
147+ System .err .println ("Caught a SecurityException reading the system property '" + property
148+ + "'; the SystemUtils property value will default to null." );
149+ return null ;
150+ }
151+ }
152+
153+ /**
154+ * Decides if the operating system matches.
155+ * <p>
156+ * This method is package private instead of private to support unit test invocation.
157+ * </p>
158+ *
159+ * @param osName
160+ * the actual OS name
161+ * @param osNamePrefix
162+ * the prefix for the expected OS name
163+ * @return true if matches, or false if not or can't determine
164+ */
165+ private static boolean isOsNameMatch (final String osName , final String osNamePrefix ) {
166+ if (osName == null ) {
167+ return false ;
168+ }
169+ return osName .startsWith (osNamePrefix );
170+ }
171+
172+ private final char [] illegalFileNameChars ;
173+ private final int maxFileNameLength ;
174+
175+ private final int maxPathLength ;
176+
177+ private FileSystem (final int maxFileLength , final int maxPathLength , final char [] illegalFileNameChars ) {
178+ this .maxFileNameLength = maxFileLength ;
179+ this .maxPathLength = maxPathLength ;
180+ this .illegalFileNameChars = Objects .requireNonNull (illegalFileNameChars , "illegalFileNameChars" );
181+ }
182+
183+ public char [] getIllegalFileNameChars () {
184+ return this .illegalFileNameChars .clone ();
185+ }
186+
187+ public int getMaxFileNameLength () {
188+ return maxFileNameLength ;
189+ }
190+
191+ public int getMaxPathLength () {
192+ return maxPathLength ;
193+ }
194+
195+ private boolean isIllegalFileNameChar (final char c ) {
196+ return Arrays .binarySearch (illegalFileNameChars , c ) >= 0 ;
197+ }
198+
199+ /**
200+ * Converts a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} to a legal file
201+ * name. Illegal characters in the candidate name are replaced by the {@code replacement} character. If the file
202+ * name exceeds {@link #getMaxFileNameLength()}, then the name is truncated to {@link #getMaxFileNameLength()}.
203+ *
204+ * @param candidate
205+ * a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"}
206+ * @param replacement
207+ * Illegal characters in the candidate name are replaced by this character
208+ * @return a String without illegal characters
209+ */
210+ public String toLegalFileName (final String candidate , final char replacement ) {
211+ if (isIllegalFileNameChar (replacement )) {
212+ throw new IllegalArgumentException (
213+ String .format ("The replacement character '%s' cannot be one of the %s illegal characters: %s" ,
214+ replacement , name (), Arrays .toString (illegalFileNameChars )));
215+ }
216+ final String truncated = candidate .length () > maxFileNameLength ? candidate .substring (0 , maxFileNameLength )
217+ : candidate ;
218+ boolean changed = false ;
219+ final char [] charArray = truncated .toCharArray ();
220+ for (int i = 0 ; i < charArray .length ; i ++) {
221+ if (isIllegalFileNameChar (charArray [i ])) {
222+ charArray [i ] = replacement ;
223+ changed = true ;
224+ }
225+ }
226+ return changed ? String .valueOf (charArray ) : truncated ;
227+ }
228+ }
0 commit comments