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