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