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