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