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 }