1 /*
   2  * Copyright (c) 2001, 2018, 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             String np;
 332             if (dir != null) {
 333                 /* When resolving a directory-relative path that refers to a
 334                    drive other than the current drive, insist that the caller
 335                    have read permission on the result */
 336                 String p = drive + (':' + dir + slashify(path.substring(2)));
 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         SecurityManager sm = System.getSecurityManager();
 355         if (sm != null) {
 356             sm.checkPropertyAccess("user.dir");
 357         }
 358         return normalize(userDir);
 359     }
 360 
 361     private String getDrive(String path) {
 362         int pl = prefixLength(path);
 363         return (pl == 3) ? path.substring(0, 2) : null;
 364     }
 365 
 366     private static String[] driveDirCache = new String[26];
 367 
 368     private static int driveIndex(char d) {
 369         if ((d >= 'a') && (d <= 'z')) return d - 'a';
 370         if ((d >= 'A') && (d <= 'Z')) return d - 'A';
 371         return -1;
 372     }
 373 
 374     private native String getDriveDirectory(int drive);
 375 
 376     private String getDriveDirectory(char drive) {
 377         int i = driveIndex(drive);
 378         if (i < 0) return null;
 379         String s = driveDirCache[i];
 380         if (s != null) return s;
 381         s = getDriveDirectory(i + 1);
 382         driveDirCache[i] = s;
 383         return s;
 384     }
 385 
 386     // Caches for canonicalization results to improve startup performance.
 387     // The first cache handles repeated canonicalizations of the same path
 388     // name. The prefix cache handles repeated canonicalizations within the
 389     // same directory, and must not create results differing from the true
 390     // canonicalization algorithm in canonicalize_md.c. For this reason the
 391     // prefix cache is conservative and is not used for complex path names.
 392     private final ExpiringCache cache;
 393     private final ExpiringCache prefixCache;
 394 
 395     @Override
 396     public String canonicalize(String path) throws IOException {
 397         // If path is a drive letter only then skip canonicalization
 398         int len = path.length();
 399         if ((len == 2) &&
 400             (isLetter(path.charAt(0))) &&
 401             (path.charAt(1) == ':')) {
 402             char c = path.charAt(0);
 403             if ((c >= 'A') && (c <= 'Z'))
 404                 return path;
 405             return "" + ((char) (c-32)) + ':';
 406         } else if ((len == 3) &&
 407                    (isLetter(path.charAt(0))) &&
 408                    (path.charAt(1) == ':') &&
 409                    (path.charAt(2) == '\\')) {
 410             char c = path.charAt(0);
 411             if ((c >= 'A') && (c <= 'Z'))
 412                 return path;
 413             return "" + ((char) (c-32)) + ':' + '\\';
 414         }
 415         if (!useCanonCaches) {
 416             return canonicalize0(path);
 417         } else {
 418             String res = cache.get(path);
 419             if (res == null) {
 420                 String dir = null;
 421                 String resDir = null;
 422                 if (useCanonPrefixCache) {
 423                     dir = parentOrNull(path);
 424                     if (dir != null) {
 425                         resDir = prefixCache.get(dir);
 426                         if (resDir != null) {
 427                             /*
 428                              * Hit only in prefix cache; full path is canonical,
 429                              * but we need to get the canonical name of the file
 430                              * in this directory to get the appropriate
 431                              * capitalization
 432                              */
 433                             String filename = path.substring(1 + dir.length());
 434                             res = canonicalizeWithPrefix(resDir, filename);
 435                             cache.put(dir + File.separatorChar + filename, res);
 436                         }
 437                     }
 438                 }
 439                 if (res == null) {
 440                     res = canonicalize0(path);
 441                     cache.put(path, res);
 442                     if (useCanonPrefixCache && dir != null) {
 443                         resDir = parentOrNull(res);
 444                         if (resDir != null) {
 445                             File f = new File(res);
 446                             if (f.exists() && !f.isDirectory()) {
 447                                 prefixCache.put(dir, resDir);
 448                             }
 449                         }
 450                     }
 451                 }
 452             }
 453             return res;
 454         }
 455     }
 456 
 457     private native String canonicalize0(String path)
 458             throws IOException;
 459 
 460     private String canonicalizeWithPrefix(String canonicalPrefix,
 461             String filename) throws IOException
 462     {
 463         return canonicalizeWithPrefix0(canonicalPrefix,
 464                 canonicalPrefix + File.separatorChar + filename);
 465     }
 466 
 467     // Run the canonicalization operation assuming that the prefix
 468     // (everything up to the last filename) is canonical; just gets
 469     // the canonical name of the last element of the path
 470     private native String canonicalizeWithPrefix0(String canonicalPrefix,
 471             String pathWithCanonicalPrefix)
 472             throws IOException;
 473 
 474     // Best-effort attempt to get parent of this path; used for
 475     // optimization of filename canonicalization. This must return null for
 476     // any cases where the code in canonicalize_md.c would throw an
 477     // exception or otherwise deal with non-simple pathnames like handling
 478     // of "." and "..". It may conservatively return null in other
 479     // situations as well. Returning null will cause the underlying
 480     // (expensive) canonicalization routine to be called.
 481     private static String parentOrNull(String path) {
 482         if (path == null) return null;
 483         char sep = File.separatorChar;
 484         char altSep = '/';
 485         int last = path.length() - 1;
 486         int idx = last;
 487         int adjacentDots = 0;
 488         int nonDotCount = 0;
 489         while (idx > 0) {
 490             char c = path.charAt(idx);
 491             if (c == '.') {
 492                 if (++adjacentDots >= 2) {
 493                     // Punt on pathnames containing . and ..
 494                     return null;
 495                 }
 496                 if (nonDotCount == 0) {
 497                     // Punt on pathnames ending in a .
 498                     return null;
 499                 }
 500             } else if (c == sep) {
 501                 if (adjacentDots == 1 && nonDotCount == 0) {
 502                     // Punt on pathnames containing . and ..
 503                     return null;
 504                 }
 505                 if (idx == 0 ||
 506                     idx >= last - 1 ||
 507                     path.charAt(idx - 1) == sep ||
 508                     path.charAt(idx - 1) == altSep) {
 509                     // Punt on pathnames containing adjacent slashes
 510                     // toward the end
 511                     return null;
 512                 }
 513                 return path.substring(0, idx);
 514             } else if (c == altSep) {
 515                 // Punt on pathnames containing both backward and
 516                 // forward slashes
 517                 return null;
 518             } else if (c == '*' || c == '?') {
 519                 // Punt on pathnames containing wildcards
 520                 return null;
 521             } else {
 522                 ++nonDotCount;
 523                 adjacentDots = 0;
 524             }
 525             --idx;
 526         }
 527         return null;
 528     }
 529 
 530     /* -- Attribute accessors -- */
 531 
 532     @Override
 533     public native int getBooleanAttributes(File f);
 534 
 535     @Override
 536     public native boolean checkAccess(File f, int access);
 537 
 538     @Override
 539     public native long getLastModifiedTime(File f);
 540 
 541     @Override
 542     public native long getLength(File f);
 543 
 544     @Override
 545     public native boolean setPermission(File f, int access, boolean enable,
 546             boolean owneronly);
 547 
 548     /* -- File operations -- */
 549 
 550     @Override
 551     public native boolean createFileExclusively(String path)
 552             throws IOException;
 553 
 554     @Override
 555     public native String[] list(File f);
 556 
 557     @Override
 558     public native boolean createDirectory(File f);
 559 
 560     @Override
 561     public native boolean setLastModifiedTime(File f, long time);
 562 
 563     @Override
 564     public native boolean setReadOnly(File f);
 565 
 566     @Override
 567     public boolean delete(File f) {
 568         // Keep canonicalization caches in sync after file deletion
 569         // and renaming operations. Could be more clever than this
 570         // (i.e., only remove/update affected entries) but probably
 571         // not worth it since these entries expire after 30 seconds
 572         // anyway.
 573         if (useCanonCaches) {
 574             cache.clear();
 575         }
 576         if (useCanonPrefixCache) {
 577             prefixCache.clear();
 578         }
 579         return delete0(f);
 580     }
 581 
 582     private native boolean delete0(File f);
 583 
 584     @Override
 585     public boolean rename(File f1, File f2) {
 586         // Keep canonicalization caches in sync after file deletion
 587         // and renaming operations. Could be more clever than this
 588         // (i.e., only remove/update affected entries) but probably
 589         // not worth it since these entries expire after 30 seconds
 590         // anyway.
 591         if (useCanonCaches) {
 592             cache.clear();
 593         }
 594         if (useCanonPrefixCache) {
 595             prefixCache.clear();
 596         }
 597         return rename0(f1, f2);
 598     }
 599 
 600     private native boolean rename0(File f1, File f2);
 601 
 602     /* -- Filesystem interface -- */
 603 
 604     @Override
 605     public File[] listRoots() {
 606         return BitSet
 607             .valueOf(new long[] {listRoots0()})
 608             .stream()
 609             .mapToObj(i -> new File((char)('A' + i) + ":" + slash))
 610             .filter(f -> access(f.getPath()) && f.exists())
 611             .toArray(File[]::new);
 612     }
 613 
 614     private static native int listRoots0();
 615 
 616     private boolean access(String path) {
 617         try {
 618             SecurityManager security = System.getSecurityManager();
 619             if (security != null) security.checkRead(path);
 620             return true;
 621         } catch (SecurityException x) {
 622             return false;
 623         }
 624     }
 625 
 626     /* -- Disk usage -- */
 627 
 628     @Override
 629     public long getSpace(File f, int t) {
 630         if (f.exists()) {
 631             return getSpace0(f, t);
 632         }
 633         return 0;
 634     }
 635 
 636     private native long getSpace0(File f, int t);
 637 
 638     /* -- Basic infrastructure -- */
 639 
 640     // Obtain maximum file component length from GetVolumeInformation which
 641     // expects the path to be null or a root component ending in a backslash
 642     private native int getNameMax0(String path);
 643 
 644     public int getNameMax(String path) {
 645         String s = null;
 646         if (path != null) {
 647             File f = new File(path);
 648             if (f.isAbsolute()) {
 649                 Path root = f.toPath().getRoot();
 650                 if (root != null) {
 651                     s = root.toString();
 652                     if (!s.endsWith("\\")) {
 653                         s = s + "\\";
 654                     }
 655                 }
 656             }
 657         }
 658         return getNameMax0(s);
 659     }
 660 
 661     @Override
 662     public int compare(File f1, File f2) {
 663         return f1.getPath().compareToIgnoreCase(f2.getPath());
 664     }
 665 
 666     @Override
 667     public int hashCode(File f) {
 668         /* Could make this more efficient: String.hashCodeIgnoreCase */
 669         return f.getPath().toLowerCase(Locale.ENGLISH).hashCode() ^ 1234321;
 670     }
 671 
 672     private static native void initIDs();
 673 
 674     static {
 675         initIDs();
 676     }
 677 }