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