1 /*
  2  * Copyright (c) 1998, 2021, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.  Oracle designates this
  8  * particular file as subject to the "Classpath" exception as provided
  9  * by Oracle in the LICENSE file that accompanied this code.
 10  *
 11  * This code is distributed in the hope that it will be useful, but WITHOUT
 12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 14  * version 2 for more details (a copy is included in the LICENSE file that
 15  * accompanied this code).
 16  *
 17  * You should have received a copy of the GNU General Public License version
 18  * 2 along with this work; if not, write to the Free Software Foundation,
 19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 20  *
 21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 22  * or visit www.oracle.com if you need additional information or have any
 23  * questions.
 24  */
 25 
 26 package java.io;
 27 
 28 import java.util.Properties;
 29 import jdk.internal.misc.Blocker;
 30 import jdk.internal.util.StaticProperty;
 31 import sun.security.action.GetPropertyAction;
 32 
 33 class UnixFileSystem extends FileSystem {
 34 
 35     private final char slash;
 36     private final char colon;
 37     private final String javaHome;
 38     private final String userDir;
 39 
 40     public UnixFileSystem() {
 41         Properties props = GetPropertyAction.privilegedGetProperties();
 42         slash = props.getProperty("file.separator").charAt(0);
 43         colon = props.getProperty("path.separator").charAt(0);
 44         javaHome = StaticProperty.javaHome();
 45         userDir = StaticProperty.userDir();
 46         cache = useCanonCaches ? new ExpiringCache() : null;
 47         javaHomePrefixCache = useCanonPrefixCache ? new ExpiringCache() : null;
 48     }
 49 
 50 
 51     /* -- Normalization and construction -- */
 52 
 53     @Override
 54     public char getSeparator() {
 55         return slash;
 56     }
 57 
 58     @Override
 59     public char getPathSeparator() {
 60         return colon;
 61     }
 62 
 63     /* A normal Unix pathname contains no duplicate slashes and does not end
 64        with a slash.  It may be the empty string. */
 65 
 66     /**
 67      * Normalize the given pathname, starting at the given
 68      * offset; everything before off is already normal, and there's at least
 69      * one duplicate or trailing slash to be removed
 70      */
 71     private String normalize(String pathname, int off) {
 72         int n = pathname.length();
 73         while ((n > off) && (pathname.charAt(n - 1) == '/')) n--;
 74         if (n == 0) return "/";
 75         if (n == off) return pathname.substring(0, off);
 76 
 77         StringBuilder sb = new StringBuilder(n);
 78         if (off > 0) sb.append(pathname, 0, off);
 79         char prevChar = 0;
 80         for (int i = off; i < n; i++) {
 81             char c = pathname.charAt(i);
 82             if ((prevChar == '/') && (c == '/')) continue;
 83             sb.append(c);
 84             prevChar = c;
 85         }
 86         return sb.toString();
 87     }
 88 
 89     /* Check that the given pathname is normal.  If not, invoke the real
 90        normalizer on the part of the pathname that requires normalization.
 91        This way we iterate through the whole pathname string only once. */
 92     @Override
 93     public String normalize(String pathname) {
 94         int doubleSlash = pathname.indexOf("//");
 95         if (doubleSlash >= 0) {
 96             return normalize(pathname, doubleSlash);
 97         }
 98         if (pathname.endsWith("/")) {
 99             return normalize(pathname, pathname.length() - 1);
100         }
101         return pathname;
102     }
103 
104     @Override
105     public int prefixLength(String pathname) {
106         return pathname.startsWith("/") ? 1 : 0;
107     }
108 
109     @Override
110     public String resolve(String parent, String child) {
111         if (child.isEmpty()) return parent;
112         if (child.charAt(0) == '/') {
113             if (parent.equals("/")) return child;
114             return parent + child;
115         }
116         if (parent.equals("/")) return parent + child;
117         return parent + '/' + child;
118     }
119 
120     @Override
121     public String getDefaultParent() {
122         return "/";
123     }
124 
125     @Override
126     public String fromURIPath(String path) {
127         String p = path;
128         if (p.endsWith("/") && (p.length() > 1)) {
129             // "/foo/" --> "/foo", but "/" --> "/"
130             p = p.substring(0, p.length() - 1);
131         }
132         return p;
133     }
134 
135 
136     /* -- Path operations -- */
137 
138     @Override
139     public boolean isAbsolute(File f) {
140         return (f.getPrefixLength() != 0);
141     }
142 
143     @Override
144     public String resolve(File f) {
145         if (isAbsolute(f)) return f.getPath();
146         @SuppressWarnings("removal")
147         SecurityManager sm = System.getSecurityManager();
148         if (sm != null) {
149             sm.checkPropertyAccess("user.dir");
150         }
151         return resolve(userDir, f.getPath());
152     }
153 
154     // Caches for canonicalization results to improve startup performance.
155     // The first cache handles repeated canonicalizations of the same path
156     // name. The prefix cache handles repeated canonicalizations within the
157     // same directory, and must not create results differing from the true
158     // canonicalization algorithm in canonicalize_md.c. For this reason the
159     // prefix cache is conservative and is not used for complex path names.
160     private final ExpiringCache cache;
161     // On Unix symlinks can jump anywhere in the file system, so we only
162     // treat prefixes in java.home as trusted and cacheable in the
163     // canonicalization algorithm
164     private final ExpiringCache javaHomePrefixCache;
165 
166     @Override
167     public String canonicalize(String path) throws IOException {
168         if (!useCanonCaches) {
169             if (Thread.currentThread().isVirtual()) {
170                 return Blocker.managedBlock(() -> canonicalize0(path));
171             } else {
172                 return canonicalize0(path);
173             }
174         } else {
175             String res = cache.get(path);
176             if (res == null) {
177                 String dir = null;
178                 String resDir;
179                 if (useCanonPrefixCache) {
180                     // Note that this can cause symlinks that should
181                     // be resolved to a destination directory to be
182                     // resolved to the directory they're contained in
183                     dir = parentOrNull(path);
184                     if (dir != null) {
185                         resDir = javaHomePrefixCache.get(dir);
186                         if (resDir != null) {
187                             // Hit only in prefix cache; full path is canonical
188                             String filename = path.substring(1 + dir.length());
189                             res = resDir + slash + filename;
190                             cache.put(dir + slash + filename, res);
191                         }
192                     }
193                 }
194                 if (res == null) {
195                     if (Thread.currentThread().isVirtual()) {
196                         res = Blocker.managedBlock(() -> canonicalize0(path));
197                     } else {
198                         res = canonicalize0(path);
199                     }
200                     cache.put(path, res);
201                     if (useCanonPrefixCache &&
202                         dir != null && dir.startsWith(javaHome)) {
203                         resDir = parentOrNull(res);
204                         // Note that we don't allow a resolved symlink
205                         // to elsewhere in java.home to pollute the
206                         // prefix cache (java.home prefix cache could
207                         // just as easily be a set at this point)
208                         if (resDir != null && resDir.equals(dir)) {
209                             File f = new File(res);
210                             if (f.exists() && !f.isDirectory()) {
211                                 javaHomePrefixCache.put(dir, resDir);
212                             }
213                         }
214                     }
215                 }
216             }
217             return res;
218         }
219     }
220     private native String canonicalize0(String path) throws IOException;
221     // Best-effort attempt to get parent of this path; used for
222     // optimization of filename canonicalization. This must return null for
223     // any cases where the code in canonicalize_md.c would throw an
224     // exception or otherwise deal with non-simple pathnames like handling
225     // of "." and "..". It may conservatively return null in other
226     // situations as well. Returning null will cause the underlying
227     // (expensive) canonicalization routine to be called.
228     static String parentOrNull(String path) {
229         if (path == null) return null;
230         char sep = File.separatorChar;
231         int last = path.length() - 1;
232         int idx = last;
233         int adjacentDots = 0;
234         int nonDotCount = 0;
235         while (idx > 0) {
236             char c = path.charAt(idx);
237             if (c == '.') {
238                 if (++adjacentDots >= 2) {
239                     // Punt on pathnames containing . and ..
240                     return null;
241                 }
242             } else if (c == sep) {
243                 if (adjacentDots == 1 && nonDotCount == 0) {
244                     // Punt on pathnames containing . and ..
245                     return null;
246                 }
247                 if (idx == 0 ||
248                     idx >= last - 1 ||
249                     path.charAt(idx - 1) == sep) {
250                     // Punt on pathnames containing adjacent slashes
251                     // toward the end
252                     return null;
253                 }
254                 return path.substring(0, idx);
255             } else {
256                 ++nonDotCount;
257                 adjacentDots = 0;
258             }
259             --idx;
260         }
261         return null;
262     }
263 
264     /* -- Attribute accessors -- */
265 
266     private native int getBooleanAttributes0(File f);
267 
268     @Override
269     public int getBooleanAttributes(File f) {
270         int rv;
271         if (Thread.currentThread().isVirtual()) {
272             rv = Blocker.managedBlock(() -> getBooleanAttributes0(f));
273         } else {
274             rv = getBooleanAttributes0(f);
275         }
276         return rv | isHidden(f);
277     }
278 
279     @Override
280     public boolean hasBooleanAttributes(File f, int attributes) {
281         int rv;
282         if (Thread.currentThread().isVirtual()) {
283             rv = Blocker.managedBlock(() -> getBooleanAttributes0(f));
284         } else {
285             rv = getBooleanAttributes0(f);
286         }
287         if ((attributes & BA_HIDDEN) != 0) {
288             rv |= isHidden(f);
289         }
290         return (rv & attributes) == attributes;
291     }
292 
293     private static int isHidden(File f) {
294         return f.getName().startsWith(".") ? BA_HIDDEN : 0;
295     }
296 
297     @Override
298     public boolean checkAccess(File f, int access) {
299         if (Thread.currentThread().isVirtual()) {
300             return Blocker.managedBlock(() -> checkAccess0(f, access));
301         } else {
302             return checkAccess0(f, access);
303         }
304     }
305     private native boolean checkAccess0(File f, int access);
306 
307     @Override
308     public long getLastModifiedTime(File f) {
309         if (Thread.currentThread().isVirtual()) {
310             return Blocker.managedBlock(() -> getLastModifiedTime0(f));
311         } else {
312             return getLastModifiedTime0(f);
313         }
314     }
315     private native long getLastModifiedTime0(File f);
316 
317     @Override
318     public long getLength(File f) {
319         if (Thread.currentThread().isVirtual()) {
320             return Blocker.managedBlock(() -> getLength0(f));
321         } else {
322             return getLength0(f);
323         }
324     }
325     private native long getLength0(File f);
326 
327     @Override
328     public boolean setPermission(File f, int access, boolean enable, boolean owneronly) {
329         if (Thread.currentThread().isVirtual()) {
330             return Blocker.managedBlock(() -> setPermission0(f, access, enable, owneronly));
331         } else {
332             return setPermission0(f, access, enable, owneronly);
333         }
334     }
335     private native boolean setPermission0(File f, int access, boolean enable, boolean owneronly);
336 
337     /* -- File operations -- */
338 
339     @Override
340     public boolean createFileExclusively(String path) throws IOException {
341         if (Thread.currentThread().isVirtual()) {
342             return Blocker.managedBlock(() -> createFileExclusively0(path));
343         } else {
344             return createFileExclusively0(path);
345         }
346     }
347     private native boolean createFileExclusively0(String path) throws IOException;
348 
349     @Override
350     public boolean delete(File f) {
351         // Keep canonicalization caches in sync after file deletion
352         // and renaming operations. Could be more clever than this
353         // (i.e., only remove/update affected entries) but probably
354         // not worth it since these entries expire after 30 seconds
355         // anyway.
356         if (useCanonCaches) {
357             cache.clear();
358         }
359         if (useCanonPrefixCache) {
360             javaHomePrefixCache.clear();
361         }
362         if (Thread.currentThread().isVirtual()) {
363             return Blocker.managedBlock(() -> delete0(f));
364         } else {
365             return delete0(f);
366         }
367     }
368     private native boolean delete0(File f);
369 
370     @Override
371     public String[] list(File f) {
372         if (Thread.currentThread().isVirtual()) {
373             return Blocker.managedBlock(() -> list0(f));
374         } else {
375             return list0(f);
376         }
377     }
378     private native String[] list0(File f);
379 
380     @Override
381     public boolean createDirectory(File f) {
382         if (Thread.currentThread().isVirtual()) {
383             return Blocker.managedBlock(() -> createDirectory0(f));
384         } else {
385             return createDirectory0(f);
386         }
387     }
388     private native boolean createDirectory0(File f);
389 
390     @Override
391     public boolean rename(File f1, File f2) {
392         // Keep canonicalization caches in sync after file deletion
393         // and renaming operations. Could be more clever than this
394         // (i.e., only remove/update affected entries) but probably
395         // not worth it since these entries expire after 30 seconds
396         // anyway.
397         if (useCanonCaches) {
398             cache.clear();
399         }
400         if (useCanonPrefixCache) {
401             javaHomePrefixCache.clear();
402         }
403         if (Thread.currentThread().isVirtual()) {
404             return Blocker.managedBlock(() -> rename0(f1, f2));
405         } else {
406             return rename0(f1, f2);
407         }
408     }
409     private native boolean rename0(File f1, File f2);
410 
411     @Override
412     public boolean setLastModifiedTime(File f, long time) {
413         if (Thread.currentThread().isVirtual()) {
414             return Blocker.managedBlock(() -> setLastModifiedTime0(f, time));
415         } else {
416             return setLastModifiedTime0(f, time);
417         }
418     }
419     private native boolean setLastModifiedTime0(File f, long time);
420 
421     @Override
422     public boolean setReadOnly(File f) {
423         if (Thread.currentThread().isVirtual()) {
424             return Blocker.managedBlock(() -> setReadOnly0(f));
425         } else {
426             return setReadOnly0(f);
427         }
428     }
429     private native boolean setReadOnly0(File f);
430 
431     /* -- Filesystem interface -- */
432 
433     @Override
434     public File[] listRoots() {
435         try {
436             @SuppressWarnings("removal")
437             SecurityManager security = System.getSecurityManager();
438             if (security != null) {
439                 security.checkRead("/");
440             }
441             return new File[] { new File("/") };
442         } catch (SecurityException x) {
443             return new File[0];
444         }
445     }
446 
447     /* -- Disk usage -- */
448 
449     @Override
450     public long getSpace(File f, int t) {
451         if (Thread.currentThread().isVirtual()) {
452             return Blocker.managedBlock(() -> getSpace0(f, t));
453         } else {
454             return getSpace0(f, t);
455         }
456     }
457     private native long getSpace0(File f, int t);
458 
459     /* -- Basic infrastructure -- */
460 
461     private native long getNameMax0(String path);
462 
463     @Override
464     public int getNameMax(String path) {
465         long nameMax = getNameMax0(path);
466         if (nameMax > Integer.MAX_VALUE) {
467             nameMax = Integer.MAX_VALUE;
468         }
469         return (int)nameMax;
470     }
471 
472     @Override
473     public int compare(File f1, File f2) {
474         return f1.getPath().compareTo(f2.getPath());
475     }
476 
477     @Override
478     public int hashCode(File f) {
479         return f.getPath().hashCode() ^ 1234321;
480     }
481 
482 
483     private static native void initIDs();
484 
485     static {
486         initIDs();
487     }
488 }