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