1 /*
  2  * Copyright (c) 2001, 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.io.File;
 29 import java.nio.file.Path;
 30 import java.util.BitSet;
 31 import java.util.Locale;
 32 import java.util.Properties;
 33 import sun.security.action.GetPropertyAction;
 34 
 35 /**
 36  * Unicode-aware FileSystem for Windows NT/2000.
 37  *
 38  * @author Konstantin Kladko
 39  * @since 1.4
 40  */
 41 class WinNTFileSystem extends FileSystem {
 42 
 43     private final char slash;
 44     private final char altSlash;
 45     private final char semicolon;
 46     private final String userDir;
 47 
 48     public WinNTFileSystem() {
 49         Properties props = GetPropertyAction.privilegedGetProperties();
 50         slash = props.getProperty("file.separator").charAt(0);
 51         semicolon = props.getProperty("path.separator").charAt(0);
 52         altSlash = (this.slash == '\\') ? '/' : '\\';
 53         userDir = normalize(props.getProperty("user.dir"));
 54         cache = useCanonCaches ? new ExpiringCache() : null;
 55         prefixCache = useCanonPrefixCache ? new ExpiringCache() : null;
 56     }
 57 
 58     private boolean isSlash(char c) {
 59         return (c == '\\') || (c == '/');
 60     }
 61 
 62     private boolean isLetter(char c) {
 63         return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z'));
 64     }
 65 
 66     private String slashify(String p) {
 67         if (!p.isEmpty() && p.charAt(0) != slash) return slash + p;
 68         else return p;
 69     }
 70 
 71     /* -- Normalization and construction -- */
 72 
 73     @Override
 74     public char getSeparator() {
 75         return slash;
 76     }
 77 
 78     @Override
 79     public char getPathSeparator() {
 80         return semicolon;
 81     }
 82 
 83     /* Check that the given pathname is normal.  If not, invoke the real
 84        normalizer on the part of the pathname that requires normalization.
 85        This way we iterate through the whole pathname string only once. */
 86     @Override
 87     public String normalize(String path) {
 88         int n = path.length();
 89         char slash = this.slash;
 90         char altSlash = this.altSlash;
 91         char prev = 0;
 92         for (int i = 0; i < n; i++) {
 93             char c = path.charAt(i);
 94             if (c == altSlash)
 95                 return normalize(path, n, (prev == slash) ? i - 1 : i);
 96             if ((c == slash) && (prev == slash) && (i > 1))
 97                 return normalize(path, n, i - 1);
 98             if ((c == ':') && (i > 1))
 99                 return normalize(path, n, 0);
100             prev = c;
101         }
102         if (prev == slash) return normalize(path, n, n - 1);
103         return path;
104     }
105 
106     /* Normalize the given pathname, whose length is len, starting at the given
107        offset; everything before this offset is already normal. */
108     private String normalize(String path, int len, int off) {
109         if (len == 0) return path;
110         if (off < 3) off = 0;   /* Avoid fencepost cases with UNC pathnames */
111         int src;
112         char slash = this.slash;
113         StringBuilder sb = new StringBuilder(len);
114 
115         if (off == 0) {
116             /* Complete normalization, including prefix */
117             src = normalizePrefix(path, len, sb);
118         } else {
119             /* Partial normalization */
120             src = off;
121             sb.append(path, 0, off);
122         }
123 
124         /* Remove redundant slashes from the remainder of the path, forcing all
125            slashes into the preferred slash */
126         while (src < len) {
127             char c = path.charAt(src++);
128             if (isSlash(c)) {
129                 while ((src < len) && isSlash(path.charAt(src))) src++;
130                 if (src == len) {
131                     /* Check for trailing separator */
132                     int sn = sb.length();
133                     if ((sn == 2) && (sb.charAt(1) == ':')) {
134                         /* "z:\\" */
135                         sb.append(slash);
136                         break;
137                     }
138                     if (sn == 0) {
139                         /* "\\" */
140                         sb.append(slash);
141                         break;
142                     }
143                     if ((sn == 1) && (isSlash(sb.charAt(0)))) {
144                         /* "\\\\" is not collapsed to "\\" because "\\\\" marks
145                            the beginning of a UNC pathname.  Even though it is
146                            not, by itself, a valid UNC pathname, we leave it as
147                            is in order to be consistent with the win32 APIs,
148                            which treat this case as an invalid UNC pathname
149                            rather than as an alias for the root directory of
150                            the current drive. */
151                         sb.append(slash);
152                         break;
153                     }
154                     /* Path does not denote a root directory, so do not append
155                        trailing slash */
156                     break;
157                 } else {
158                     sb.append(slash);
159                 }
160             } else {
161                 sb.append(c);
162             }
163         }
164 
165         return sb.toString();
166     }
167 
168     /* A normal Win32 pathname contains no duplicate slashes, except possibly
169        for a UNC prefix, and does not end with a slash.  It may be the empty
170        string.  Normalized Win32 pathnames have the convenient property that
171        the length of the prefix almost uniquely identifies the type of the path
172        and whether it is absolute or relative:
173 
174            0  relative to both drive and directory
175            1  drive-relative (begins with '\\')
176            2  absolute UNC (if first char is '\\'),
177                 else directory-relative (has form "z:foo")
178            3  absolute local pathname (begins with "z:\\")
179      */
180     private int normalizePrefix(String path, int len, StringBuilder sb) {
181         int src = 0;
182         while ((src < len) && isSlash(path.charAt(src))) src++;
183         char c;
184         if ((len - src >= 2)
185             && isLetter(c = path.charAt(src))
186             && path.charAt(src + 1) == ':') {
187             /* Remove leading slashes if followed by drive specifier.
188                This hack is necessary to support file URLs containing drive
189                specifiers (e.g., "file://c:/path").  As a side effect,
190                "/c:/path" can be used as an alternative to "c:/path". */
191             sb.append(c);
192             sb.append(':');
193             src += 2;
194         } else {
195             src = 0;
196             if ((len >= 2)
197                 && isSlash(path.charAt(0))
198                 && isSlash(path.charAt(1))) {
199                 /* UNC pathname: Retain first slash; leave src pointed at
200                    second slash so that further slashes will be collapsed
201                    into the second slash.  The result will be a pathname
202                    beginning with "\\\\" followed (most likely) by a host
203                    name. */
204                 src = 1;
205                 sb.append(slash);
206             }
207         }
208         return src;
209     }
210 
211     @Override
212     public int prefixLength(String path) {
213         char slash = this.slash;
214         int n = path.length();
215         if (n == 0) return 0;
216         char c0 = path.charAt(0);
217         char c1 = (n > 1) ? path.charAt(1) : 0;
218         if (c0 == slash) {
219             if (c1 == slash) return 2;  /* Absolute UNC pathname "\\\\foo" */
220             return 1;                   /* Drive-relative "\\foo" */
221         }
222         if (isLetter(c0) && (c1 == ':')) {
223             if ((n > 2) && (path.charAt(2) == slash))
224                 return 3;               /* Absolute local pathname "z:\\foo" */
225             return 2;                   /* Directory-relative "z:foo" */
226         }
227         return 0;                       /* Completely relative */
228     }
229 
230     @Override
231     public String resolve(String parent, String child) {
232         int pn = parent.length();
233         if (pn == 0) return child;
234         int cn = child.length();
235         if (cn == 0) return parent;
236 
237         String c = child;
238         int childStart = 0;
239         int parentEnd = pn;
240 
241         boolean isDirectoryRelative =
242             pn == 2 && isLetter(parent.charAt(0)) && parent.charAt(1) == ':';
243 
244         if ((cn > 1) && (c.charAt(0) == slash)) {
245             if (c.charAt(1) == slash) {
246                 /* Drop prefix when child is a UNC pathname */
247                 childStart = 2;
248             } else if (!isDirectoryRelative) {
249                 /* Drop prefix when child is drive-relative */
250                 childStart = 1;
251 
252             }
253             if (cn == childStart) { // Child is double slash
254                 if (parent.charAt(pn - 1) == slash)
255                     return parent.substring(0, pn - 1);
256                 return parent;
257             }
258         }
259 
260         if (parent.charAt(pn - 1) == slash)
261             parentEnd--;
262 
263         int strlen = parentEnd + cn - childStart;
264         char[] theChars = null;
265         if (child.charAt(childStart) == slash || isDirectoryRelative) {
266             theChars = new char[strlen];
267             parent.getChars(0, parentEnd, theChars, 0);
268             child.getChars(childStart, cn, theChars, parentEnd);
269         } else {
270             theChars = new char[strlen + 1];
271             parent.getChars(0, parentEnd, theChars, 0);
272             theChars[parentEnd] = slash;
273             child.getChars(childStart, cn, theChars, parentEnd + 1);
274         }
275         return new String(theChars);
276     }
277 
278     @Override
279     public String getDefaultParent() {
280         return ("" + slash);
281     }
282 
283     @Override
284     public String fromURIPath(String path) {
285         String p = path;
286         if ((p.length() > 2) && (p.charAt(2) == ':')) {
287             // "/c:/foo" --> "c:/foo"
288             p = p.substring(1);
289             // "c:/foo/" --> "c:/foo", but "c:/" --> "c:/"
290             if ((p.length() > 3) && p.endsWith("/"))
291                 p = p.substring(0, p.length() - 1);
292         } else if ((p.length() > 1) && p.endsWith("/")) {
293             // "/foo/" --> "/foo"
294             p = p.substring(0, p.length() - 1);
295         }
296         return p;
297     }
298 
299     /* -- Path operations -- */
300 
301     @Override
302     public boolean isAbsolute(File f) {
303         int pl = f.getPrefixLength();
304         return (((pl == 2) && (f.getPath().charAt(0) == slash))
305                 || (pl == 3));
306     }
307 
308     @Override
309     public String resolve(File f) {
310         String path = f.getPath();
311         int pl = f.getPrefixLength();
312         if ((pl == 2) && (path.charAt(0) == slash))
313             return path;                        /* UNC */
314         if (pl == 3)
315             return path;                        /* Absolute local */
316         if (pl == 0)
317             return getUserPath() + slashify(path); /* Completely relative */
318         if (pl == 1) {                          /* Drive-relative */
319             String up = getUserPath();
320             String ud = getDrive(up);
321             if (ud != null) return ud + path;
322             return up + path;                   /* User dir is a UNC path */
323         }
324         if (pl == 2) {                          /* Directory-relative */
325             String up = getUserPath();
326             String ud = getDrive(up);
327             if ((ud != null) && path.startsWith(ud))
328                 return up + slashify(path.substring(2));
329             char drive = path.charAt(0);
330             String dir = getDriveDirectory(drive);
331             if (dir != null) {
332                 /* When resolving a directory-relative path that refers to a
333                    drive other than the current drive, insist that the caller
334                    have read permission on the result */
335                 String p = drive + (':' + dir + slashify(path.substring(2)));
336                 @SuppressWarnings("removal")
337                 SecurityManager security = System.getSecurityManager();
338                 try {
339                     if (security != null) security.checkRead(p);
340                 } catch (SecurityException x) {
341                     /* Don't disclose the drive's directory in the exception */
342                     throw new SecurityException("Cannot resolve path " + path);
343                 }
344                 return p;
345             }
346             return drive + ":" + slashify(path.substring(2)); /* fake it */
347         }
348         throw new InternalError("Unresolvable path: " + path);
349     }
350 
351     private String getUserPath() {
352         /* For both compatibility and security,
353            we must look this up every time */
354         @SuppressWarnings("removal")
355         SecurityManager sm = System.getSecurityManager();
356         if (sm != null) {
357             sm.checkPropertyAccess("user.dir");
358         }
359         return userDir;
360     }
361 
362     private String getDrive(String path) {
363         int pl = prefixLength(path);
364         return (pl == 3) ? path.substring(0, 2) : null;
365     }
366 
367     private static String[] driveDirCache = new String[26];
368 
369     private static int driveIndex(char d) {
370         if ((d >= 'a') && (d <= 'z')) return d - 'a';
371         if ((d >= 'A') && (d <= 'Z')) return d - 'A';
372         return -1;
373     }
374 
375     private native String getDriveDirectory(int drive);
376 
377     private String getDriveDirectory(char drive) {
378         int i = driveIndex(drive);
379         if (i < 0) return null;
380         String s = driveDirCache[i];
381         if (s != null) return s;
382         s = getDriveDirectory(i + 1);
383         driveDirCache[i] = s;
384         return s;
385     }
386 
387     // Caches for canonicalization results to improve startup performance.
388     // The first cache handles repeated canonicalizations of the same path
389     // name. The prefix cache handles repeated canonicalizations within the
390     // same directory, and must not create results differing from the true
391     // canonicalization algorithm in canonicalize_md.c. For this reason the
392     // prefix cache is conservative and is not used for complex path names.
393     private final ExpiringCache cache;
394     private final ExpiringCache prefixCache;
395 
396     @Override
397     public String canonicalize(String path) throws IOException {
398         // If path is a drive letter only then skip canonicalization
399         int len = path.length();
400         if ((len == 2) &&
401             (isLetter(path.charAt(0))) &&
402             (path.charAt(1) == ':')) {
403             char c = path.charAt(0);
404             if ((c >= 'A') && (c <= 'Z'))
405                 return path;
406             return "" + ((char) (c-32)) + ':';
407         } else if ((len == 3) &&
408                    (isLetter(path.charAt(0))) &&
409                    (path.charAt(1) == ':') &&
410                    (path.charAt(2) == '\\')) {
411             char c = path.charAt(0);
412             if ((c >= 'A') && (c <= 'Z'))
413                 return path;
414             return "" + ((char) (c-32)) + ':' + '\\';
415         }
416         if (!useCanonCaches) {
417             return canonicalize0(path);
418         } else {
419             String res = cache.get(path);
420             if (res == null) {
421                 String dir = null;
422                 String resDir = null;
423                 if (useCanonPrefixCache) {
424                     dir = parentOrNull(path);
425                     if (dir != null) {
426                         resDir = prefixCache.get(dir);
427                         if (resDir != null) {
428                             /*
429                              * Hit only in prefix cache; full path is canonical,
430                              * but we need to get the canonical name of the file
431                              * in this directory to get the appropriate
432                              * capitalization
433                              */
434                             String filename = path.substring(1 + dir.length());
435                             res = canonicalizeWithPrefix(resDir, filename);
436                             cache.put(dir + File.separatorChar + filename, res);
437                         }
438                     }
439                 }
440                 if (res == null) {
441                     res = canonicalize0(path);
442                     cache.put(path, res);
443                     if (useCanonPrefixCache && dir != null) {
444                         resDir = parentOrNull(res);
445                         if (resDir != null) {
446                             File f = new File(res);
447                             if (f.exists() && !f.isDirectory()) {
448                                 prefixCache.put(dir, resDir);
449                             }
450                         }
451                     }
452                 }
453             }
454             return res;
455         }
456     }
457 
458     private native String canonicalize0(String path)
459             throws IOException;
460 
461     private String canonicalizeWithPrefix(String canonicalPrefix,
462             String filename) throws IOException
463     {
464         return canonicalizeWithPrefix0(canonicalPrefix,
465                 canonicalPrefix + File.separatorChar + filename);
466     }
467 
468     // Run the canonicalization operation assuming that the prefix
469     // (everything up to the last filename) is canonical; just gets
470     // the canonical name of the last element of the path
471     private native String canonicalizeWithPrefix0(String canonicalPrefix,
472             String pathWithCanonicalPrefix)
473             throws IOException;
474 
475     // Best-effort attempt to get parent of this path; used for
476     // optimization of filename canonicalization. This must return null for
477     // any cases where the code in canonicalize_md.c would throw an
478     // exception or otherwise deal with non-simple pathnames like handling
479     // of "." and "..". It may conservatively return null in other
480     // situations as well. Returning null will cause the underlying
481     // (expensive) canonicalization routine to be called.
482     private static String parentOrNull(String path) {
483         if (path == null) return null;
484         char sep = File.separatorChar;
485         char altSep = '/';
486         int last = path.length() - 1;
487         int idx = last;
488         int adjacentDots = 0;
489         int nonDotCount = 0;
490         while (idx > 0) {
491             char c = path.charAt(idx);
492             if (c == '.') {
493                 if (++adjacentDots >= 2) {
494                     // Punt on pathnames containing . and ..
495                     return null;
496                 }
497                 if (nonDotCount == 0) {
498                     // Punt on pathnames ending in a .
499                     return null;
500                 }
501             } else if (c == sep) {
502                 if (adjacentDots == 1 && nonDotCount == 0) {
503                     // Punt on pathnames containing . and ..
504                     return null;
505                 }
506                 if (idx == 0 ||
507                     idx >= last - 1 ||
508                     path.charAt(idx - 1) == sep ||
509                     path.charAt(idx - 1) == altSep) {
510                     // Punt on pathnames containing adjacent slashes
511                     // toward the end
512                     return null;
513                 }
514                 return path.substring(0, idx);
515             } else if (c == altSep) {
516                 // Punt on pathnames containing both backward and
517                 // forward slashes
518                 return null;
519             } else if (c == '*' || c == '?') {
520                 // Punt on pathnames containing wildcards
521                 return null;
522             } else {
523                 ++nonDotCount;
524                 adjacentDots = 0;
525             }
526             --idx;
527         }
528         return null;
529     }
530 
531     /* -- Attribute accessors -- */
532 
533     @Override
534     public native int getBooleanAttributes(File f);
535 
536     @Override
537     public native boolean checkAccess(File f, int access);
538 
539     @Override
540     public native long getLastModifiedTime(File f);
541 
542     @Override
543     public native long getLength(File f);
544 
545     @Override
546     public native boolean setPermission(File f, int access, boolean enable,
547             boolean owneronly);
548 
549     /* -- File operations -- */
550 
551     @Override
552     public native boolean createFileExclusively(String path)
553             throws IOException;
554 
555     @Override
556     public native String[] list(File f);
557 
558     @Override
559     public native boolean createDirectory(File f);
560 
561     @Override
562     public native boolean setLastModifiedTime(File f, long time);
563 
564     @Override
565     public native boolean setReadOnly(File f);
566 
567     @Override
568     public boolean delete(File f) {
569         // Keep canonicalization caches in sync after file deletion
570         // and renaming operations. Could be more clever than this
571         // (i.e., only remove/update affected entries) but probably
572         // not worth it since these entries expire after 30 seconds
573         // anyway.
574         if (useCanonCaches) {
575             cache.clear();
576         }
577         if (useCanonPrefixCache) {
578             prefixCache.clear();
579         }
580         return delete0(f);
581     }
582 
583     private native boolean delete0(File f);
584 
585     @Override
586     public boolean rename(File f1, File f2) {
587         // Keep canonicalization caches in sync after file deletion
588         // and renaming operations. Could be more clever than this
589         // (i.e., only remove/update affected entries) but probably
590         // not worth it since these entries expire after 30 seconds
591         // anyway.
592         if (useCanonCaches) {
593             cache.clear();
594         }
595         if (useCanonPrefixCache) {
596             prefixCache.clear();
597         }
598         return rename0(f1, f2);
599     }
600 
601     private native boolean rename0(File f1, File f2);
602 
603     /* -- Filesystem interface -- */
604 
605     @Override
606     public File[] listRoots() {
607         return BitSet
608             .valueOf(new long[] {listRoots0()})
609             .stream()
610             .mapToObj(i -> new File((char)('A' + i) + ":" + slash))
611             .filter(f -> access(f.getPath()) && f.exists())
612             .toArray(File[]::new);
613     }
614 
615     private static native int listRoots0();
616 
617     private boolean access(String path) {
618         try {
619             @SuppressWarnings("removal")
620             SecurityManager security = System.getSecurityManager();
621             if (security != null) security.checkRead(path);
622             return true;
623         } catch (SecurityException x) {
624             return false;
625         }
626     }
627 
628     /* -- Disk usage -- */
629 
630     @Override
631     public long getSpace(File f, int t) {
632         if (f.exists()) {
633             return getSpace0(f, t);
634         }
635         return 0;
636     }
637 
638     private native long getSpace0(File f, int t);
639 
640     /* -- Basic infrastructure -- */
641 
642     // Obtain maximum file component length from GetVolumeInformation which
643     // expects the path to be null or a root component ending in a backslash
644     private native int getNameMax0(String path);
645 
646     @Override
647     public int getNameMax(String path) {
648         String s = null;
649         if (path != null) {
650             File f = new File(path);
651             if (f.isAbsolute()) {
652                 Path root = f.toPath().getRoot();
653                 if (root != null) {
654                     s = root.toString();
655                     if (!s.endsWith("\\")) {
656                         s = s + "\\";
657                     }
658                 }
659             }
660         }
661         return getNameMax0(s);
662     }
663 
664     @Override
665     public int compare(File f1, File f2) {
666         return f1.getPath().compareToIgnoreCase(f2.getPath());
667     }
668 
669     @Override
670     public int hashCode(File f) {
671         /* Could make this more efficient: String.hashCodeIgnoreCase */
672         return f.getPath().toLowerCase(Locale.ENGLISH).hashCode() ^ 1234321;
673     }
674 
675     private static native void initIDs();
676 
677     static {
678         initIDs();
679     }
680 }