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