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