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

659     private native boolean delete0(File f);
660 
661     @Override
662     public boolean rename(File f1, File f2) {
663         // Keep canonicalization caches in sync after file deletion
664         // and renaming operations. Could be more clever than this
665         // (i.e., only remove/update affected entries) but probably
666         // not worth it since these entries expire after 30 seconds
667         // anyway.
668         if (useCanonCaches) {
669             cache.clear();
670         }
671         if (useCanonPrefixCache) {
672             prefixCache.clear();
673         }
674         if (Thread.currentThread().isVirtual()) {
675             return Blocker.managedBlock(() -> rename0(f1, f2));
676         } else {
677             return rename0(f1, f2);
678         }
679     }

680     private native boolean rename0(File f1, File f2);
681 
682     /* -- Filesystem interface -- */
683 
684     @Override
685     public File[] listRoots() {
686         return BitSet
687             .valueOf(new long[] {listRoots0()})
688             .stream()
689             .mapToObj(i -> new File((char)('A' + i) + ":" + slash))
690             .filter(f -> access(f.getPath()) && f.exists())
691             .toArray(File[]::new);
692     }

693     private static native int listRoots0();
694 
695     private boolean access(String path) {
696         try {
697             @SuppressWarnings("removal")
698             SecurityManager security = System.getSecurityManager();
699             if (security != null) security.checkRead(path);
700             return true;
701         } catch (SecurityException x) {
702             return false;
703         }
704     }
705 
706     /* -- Disk usage -- */
707 
708     @Override
709     public long getSpace(File f, int t) {
710         if (f.exists()) {
711             return getSpace0(f, t);
712         }
713         return 0;
714     }

715     private native long getSpace0(File f, int t);
716 
717     /* -- Basic infrastructure -- */
718 
719     // Obtain maximum file component length from GetVolumeInformation which
720     // expects the path to be null or a root component ending in a backslash
721     private native int getNameMax0(String path);
722 
723     @Override
724     public int getNameMax(String path) {
725         String s = null;
726         if (path != null) {
727             File f = new File(path);
728             if (f.isAbsolute()) {
729                 Path root = f.toPath().getRoot();
730                 if (root != null) {
731                     s = root.toString();
732                     if (!s.endsWith("\\")) {
733                         s = s + "\\";
734                     }
735                 }
736             }
737         }
738         return getNameMax0(s);
739     }
740 
741     @Override
742     public int compare(File f1, File f2) {
743         return f1.getPath().compareToIgnoreCase(f2.getPath());
744     }
745 
746     @Override
747     public int hashCode(File f) {
748         /* Could make this more efficient: String.hashCodeIgnoreCase */
749         return f.getPath().toLowerCase(Locale.ENGLISH).hashCode() ^ 1234321;
750     }
751 
752     private static native void initIDs();
753 
754     static {
755         initIDs();
756     }
757 }
--- EOF ---