001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.io;
018
019import java.io.File;
020import java.io.IOException;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.Stack;
024
025/**
026 * General filename and filepath manipulation utilities.
027 * <p>
028 * When dealing with filenames you can hit problems when moving from a Windows
029 * based development machine to a Unix based production machine.
030 * This class aims to help avoid those problems.
031 * <p>
032 * <b>NOTE</b>: You may be able to avoid using this class entirely simply by
033 * using JDK {@link java.io.File File} objects and the two argument constructor
034 * {@link java.io.File#File(java.io.File, java.lang.String) File(File,String)}.
035 * <p>
036 * Most methods on this class are designed to work the same on both Unix and Windows.
037 * Those that don't include 'System', 'Unix' or 'Windows' in their name.
038 * <p>
039 * Most methods recognise both separators (forward and back), and both
040 * sets of prefixes. See the javadoc of each method for details.
041 * <p>
042 * This class defines six components within a filename
043 * (example C:\dev\project\file.txt):
044 * <ul>
045 * <li>the prefix - C:\</li>
046 * <li>the path - dev\project\</li>
047 * <li>the full path - C:\dev\project\</li>
048 * <li>the name - file.txt</li>
049 * <li>the base name - file</li>
050 * <li>the extension - txt</li>
051 * </ul>
052 * Note that this class works best if directory filenames end with a separator.
053 * If you omit the last separator, it is impossible to determine if the filename
054 * corresponds to a file or a directory. As a result, we have chosen to say
055 * it corresponds to a file.
056 * <p>
057 * This class only supports Unix and Windows style names.
058 * Prefixes are matched as follows:
059 * <pre>
060 * Windows:
061 * a\b\c.txt           --&gt; ""          --&gt; relative
062 * \a\b\c.txt          --&gt; "\"         --&gt; current drive absolute
063 * C:a\b\c.txt         --&gt; "C:"        --&gt; drive relative
064 * C:\a\b\c.txt        --&gt; "C:\"       --&gt; absolute
065 * \\server\a\b\c.txt  --&gt; "\\server\" --&gt; UNC
066 *
067 * Unix:
068 * a/b/c.txt           --&gt; ""          --&gt; relative
069 * /a/b/c.txt          --&gt; "/"         --&gt; absolute
070 * ~/a/b/c.txt         --&gt; "~/"        --&gt; current user
071 * ~                   --&gt; "~/"        --&gt; current user (slash added)
072 * ~user/a/b/c.txt     --&gt; "~user/"    --&gt; named user
073 * ~user               --&gt; "~user/"    --&gt; named user (slash added)
074 * </pre>
075 * Both prefix styles are matched always, irrespective of the machine that you are
076 * currently running on.
077 * <p>
078 * Origin of code: Excalibur, Alexandria, Tomcat, Commons-Utils.
079 *
080 * @since 1.1
081 */
082public class FilenameUtils {
083
084    private static final int NOT_FOUND = -1;
085
086    /**
087     * The extension separator character.
088     * @since 1.4
089     */
090    public static final char EXTENSION_SEPARATOR = '.';
091
092    /**
093     * The extension separator String.
094     * @since 1.4
095     */
096    public static final String EXTENSION_SEPARATOR_STR = Character.toString(EXTENSION_SEPARATOR);
097
098    /**
099     * The Unix separator character.
100     */
101    private static final char UNIX_SEPARATOR = '/';
102
103    /**
104     * The Windows separator character.
105     */
106    private static final char WINDOWS_SEPARATOR = '\\';
107
108    /**
109     * The system separator character.
110     */
111    private static final char SYSTEM_SEPARATOR = File.separatorChar;
112
113    /**
114     * The separator character that is the opposite of the system separator.
115     */
116    private static final char OTHER_SEPARATOR;
117    static {
118        if (isSystemWindows()) {
119            OTHER_SEPARATOR = UNIX_SEPARATOR;
120        } else {
121            OTHER_SEPARATOR = WINDOWS_SEPARATOR;
122        }
123    }
124
125    /**
126     * Instances should NOT be constructed in standard programming.
127     */
128    public FilenameUtils() {
129        super();
130    }
131
132    //-----------------------------------------------------------------------
133    /**
134     * Determines if Windows file system is in use.
135     *
136     * @return true if the system is Windows
137     */
138    static boolean isSystemWindows() {
139        return SYSTEM_SEPARATOR == WINDOWS_SEPARATOR;
140    }
141
142    //-----------------------------------------------------------------------
143    /**
144     * Checks if the character is a separator.
145     *
146     * @param ch  the character to check
147     * @return true if it is a separator character
148     */
149    private static boolean isSeparator(final char ch) {
150        return ch == UNIX_SEPARATOR || ch == WINDOWS_SEPARATOR;
151    }
152
153    //-----------------------------------------------------------------------
154    /**
155     * Normalizes a path, removing double and single dot path steps.
156     * <p>
157     * This method normalizes a path to a standard format.
158     * The input may contain separators in either Unix or Windows format.
159     * The output will contain separators in the format of the system.
160     * <p>
161     * A trailing slash will be retained.
162     * A double slash will be merged to a single slash (but UNC names are handled).
163     * A single dot path segment will be removed.
164     * A double dot will cause that path segment and the one before to be removed.
165     * If the double dot has no parent path segment to work with, {@code null}
166     * is returned.
167     * <p>
168     * The output will be the same on both Unix and Windows except
169     * for the separator character.
170     * <pre>
171     * /foo//               --&gt;   /foo/
172     * /foo/./              --&gt;   /foo/
173     * /foo/../bar          --&gt;   /bar
174     * /foo/../bar/         --&gt;   /bar/
175     * /foo/../bar/../baz   --&gt;   /baz
176     * //foo//./bar         --&gt;   /foo/bar
177     * /../                 --&gt;   null
178     * ../foo               --&gt;   null
179     * foo/bar/..           --&gt;   foo/
180     * foo/../../bar        --&gt;   null
181     * foo/../bar           --&gt;   bar
182     * //server/foo/../bar  --&gt;   //server/bar
183     * //server/../bar      --&gt;   null
184     * C:\foo\..\bar        --&gt;   C:\bar
185     * C:\..\bar            --&gt;   null
186     * ~/foo/../bar/        --&gt;   ~/bar/
187     * ~/../bar             --&gt;   null
188     * </pre>
189     * (Note the file separator returned will be correct for Windows/Unix)
190     *
191     * @param filename  the filename to normalize, null returns null
192     * @return the normalized filename, or null if invalid. Null bytes inside string will be removed
193     */
194    public static String normalize(final String filename) {
195        return doNormalize(filename, SYSTEM_SEPARATOR, true);
196    }
197    /**
198     * Normalizes a path, removing double and single dot path steps.
199     * <p>
200     * This method normalizes a path to a standard format.
201     * The input may contain separators in either Unix or Windows format.
202     * The output will contain separators in the format specified.
203     * <p>
204     * A trailing slash will be retained.
205     * A double slash will be merged to a single slash (but UNC names are handled).
206     * A single dot path segment will be removed.
207     * A double dot will cause that path segment and the one before to be removed.
208     * If the double dot has no parent path segment to work with, {@code null}
209     * is returned.
210     * <p>
211     * The output will be the same on both Unix and Windows except
212     * for the separator character.
213     * <pre>
214     * /foo//               --&gt;   /foo/
215     * /foo/./              --&gt;   /foo/
216     * /foo/../bar          --&gt;   /bar
217     * /foo/../bar/         --&gt;   /bar/
218     * /foo/../bar/../baz   --&gt;   /baz
219     * //foo//./bar         --&gt;   /foo/bar
220     * /../                 --&gt;   null
221     * ../foo               --&gt;   null
222     * foo/bar/..           --&gt;   foo/
223     * foo/../../bar        --&gt;   null
224     * foo/../bar           --&gt;   bar
225     * //server/foo/../bar  --&gt;   //server/bar
226     * //server/../bar      --&gt;   null
227     * C:\foo\..\bar        --&gt;   C:\bar
228     * C:\..\bar            --&gt;   null
229     * ~/foo/../bar/        --&gt;   ~/bar/
230     * ~/../bar             --&gt;   null
231     * </pre>
232     * The output will be the same on both Unix and Windows including
233     * the separator character.
234     *
235     * @param filename  the filename to normalize, null returns null
236     * @param unixSeparator {@code true} if a unix separator should
237     * be used or {@code false} if a windows separator should be used.
238     * @return the normalized filename, or null if invalid. Null bytes inside string will be removed
239     * @since 2.0
240     */
241    public static String normalize(final String filename, final boolean unixSeparator) {
242        final char separator = unixSeparator ? UNIX_SEPARATOR : WINDOWS_SEPARATOR;
243        return doNormalize(filename, separator, true);
244    }
245
246    //-----------------------------------------------------------------------
247    /**
248     * Normalizes a path, removing double and single dot path steps,
249     * and removing any final directory separator.
250     * <p>
251     * This method normalizes a path to a standard format.
252     * The input may contain separators in either Unix or Windows format.
253     * The output will contain separators in the format of the system.
254     * <p>
255     * A trailing slash will be removed.
256     * A double slash will be merged to a single slash (but UNC names are handled).
257     * A single dot path segment will be removed.
258     * A double dot will cause that path segment and the one before to be removed.
259     * If the double dot has no parent path segment to work with, {@code null}
260     * is returned.
261     * <p>
262     * The output will be the same on both Unix and Windows except
263     * for the separator character.
264     * <pre>
265     * /foo//               --&gt;   /foo
266     * /foo/./              --&gt;   /foo
267     * /foo/../bar          --&gt;   /bar
268     * /foo/../bar/         --&gt;   /bar
269     * /foo/../bar/../baz   --&gt;   /baz
270     * //foo//./bar         --&gt;   /foo/bar
271     * /../                 --&gt;   null
272     * ../foo               --&gt;   null
273     * foo/bar/..           --&gt;   foo
274     * foo/../../bar        --&gt;   null
275     * foo/../bar           --&gt;   bar
276     * //server/foo/../bar  --&gt;   //server/bar
277     * //server/../bar      --&gt;   null
278     * C:\foo\..\bar        --&gt;   C:\bar
279     * C:\..\bar            --&gt;   null
280     * ~/foo/../bar/        --&gt;   ~/bar
281     * ~/../bar             --&gt;   null
282     * </pre>
283     * (Note the file separator returned will be correct for Windows/Unix)
284     *
285     * @param filename  the filename to normalize, null returns null
286     * @return the normalized filename, or null if invalid. Null bytes inside string will be removed
287     */
288    public static String normalizeNoEndSeparator(final String filename) {
289        return doNormalize(filename, SYSTEM_SEPARATOR, false);
290    }
291
292    /**
293     * Normalizes a path, removing double and single dot path steps,
294     * and removing any final directory separator.
295     * <p>
296     * This method normalizes a path to a standard format.
297     * The input may contain separators in either Unix or Windows format.
298     * The output will contain separators in the format specified.
299     * <p>
300     * A trailing slash will be removed.
301     * A double slash will be merged to a single slash (but UNC names are handled).
302     * A single dot path segment will be removed.
303     * A double dot will cause that path segment and the one before to be removed.
304     * If the double dot has no parent path segment to work with, {@code null}
305     * is returned.
306     * <p>
307     * The output will be the same on both Unix and Windows including
308     * the separator character.
309     * <pre>
310     * /foo//               --&gt;   /foo
311     * /foo/./              --&gt;   /foo
312     * /foo/../bar          --&gt;   /bar
313     * /foo/../bar/         --&gt;   /bar
314     * /foo/../bar/../baz   --&gt;   /baz
315     * //foo//./bar         --&gt;   /foo/bar
316     * /../                 --&gt;   null
317     * ../foo               --&gt;   null
318     * foo/bar/..           --&gt;   foo
319     * foo/../../bar        --&gt;   null
320     * foo/../bar           --&gt;   bar
321     * //server/foo/../bar  --&gt;   //server/bar
322     * //server/../bar      --&gt;   null
323     * C:\foo\..\bar        --&gt;   C:\bar
324     * C:\..\bar            --&gt;   null
325     * ~/foo/../bar/        --&gt;   ~/bar
326     * ~/../bar             --&gt;   null
327     * </pre>
328     *
329     * @param filename  the filename to normalize, null returns null
330     * @param unixSeparator {@code true} if a unix separator should
331     * be used or {@code false} if a windows separator should be used.
332     * @return the normalized filename, or null if invalid. Null bytes inside string will be removed
333     * @since 2.0
334     */
335    public static String normalizeNoEndSeparator(final String filename, final boolean unixSeparator) {
336         final char separator = unixSeparator ? UNIX_SEPARATOR : WINDOWS_SEPARATOR;
337        return doNormalize(filename, separator, false);
338    }
339
340    /**
341     * Internal method to perform the normalization.
342     *
343     * @param filename  the filename
344     * @param separator The separator character to use
345     * @param keepSeparator  true to keep the final separator
346     * @return the normalized filename. Null bytes inside string will be removed.
347     */
348    private static String doNormalize(final String filename, final char separator, final boolean keepSeparator) {
349        if (filename == null) {
350            return null;
351        }
352
353        failIfNullBytePresent(filename);
354
355        int size = filename.length();
356        if (size == 0) {
357            return filename;
358        }
359        final int prefix = getPrefixLength(filename);
360        if (prefix < 0) {
361            return null;
362        }
363
364        final char[] array = new char[size + 2];  // +1 for possible extra slash, +2 for arraycopy
365        filename.getChars(0, filename.length(), array, 0);
366
367        // fix separators throughout
368        final char otherSeparator = separator == SYSTEM_SEPARATOR ? OTHER_SEPARATOR : SYSTEM_SEPARATOR;
369        for (int i = 0; i < array.length; i++) {
370            if (array[i] == otherSeparator) {
371                array[i] = separator;
372            }
373        }
374
375        // add extra separator on the end to simplify code below
376        boolean lastIsDirectory = true;
377        if (array[size - 1] != separator) {
378            array[size++] = separator;
379            lastIsDirectory = false;
380        }
381
382        // adjoining slashes
383        for (int i = prefix + 1; i < size; i++) {
384            if (array[i] == separator && array[i - 1] == separator) {
385                System.arraycopy(array, i, array, i - 1, size - i);
386                size--;
387                i--;
388            }
389        }
390
391        // dot slash
392        for (int i = prefix + 1; i < size; i++) {
393            if (array[i] == separator && array[i - 1] == '.' &&
394                    (i == prefix + 1 || array[i - 2] == separator)) {
395                if (i == size - 1) {
396                    lastIsDirectory = true;
397                }
398                System.arraycopy(array, i + 1, array, i - 1, size - i);
399                size -=2;
400                i--;
401            }
402        }
403
404        // double dot slash
405        outer:
406        for (int i = prefix + 2; i < size; i++) {
407            if (array[i] == separator && array[i - 1] == '.' && array[i - 2] == '.' &&
408                    (i == prefix + 2 || array[i - 3] == separator)) {
409                if (i == prefix + 2) {
410                    return null;
411                }
412                if (i == size - 1) {
413                    lastIsDirectory = true;
414                }
415                int j;
416                for (j = i - 4 ; j >= prefix; j--) {
417                    if (array[j] == separator) {
418                        // remove b/../ from a/b/../c
419                        System.arraycopy(array, i + 1, array, j + 1, size - i);
420                        size -= i - j;
421                        i = j + 1;
422                        continue outer;
423                    }
424                }
425                // remove a/../ from a/../c
426                System.arraycopy(array, i + 1, array, prefix, size - i);
427                size -= i + 1 - prefix;
428                i = prefix + 1;
429            }
430        }
431
432        if (size <= 0) {  // should never be less than 0
433            return "";
434        }
435        if (size <= prefix) {  // should never be less than prefix
436            return new String(array, 0, size);
437        }
438        if (lastIsDirectory && keepSeparator) {
439            return new String(array, 0, size);  // keep trailing separator
440        }
441        return new String(array, 0, size - 1);  // lose trailing separator
442    }
443
444    //-----------------------------------------------------------------------
445    /**
446     * Concatenates a filename to a base path using normal command line style rules.
447     * <p>
448     * The effect is equivalent to resultant directory after changing
449     * directory to the first argument, followed by changing directory to
450     * the second argument.
451     * <p>
452     * The first argument is the base path, the second is the path to concatenate.
453     * The returned path is always normalized via {@link #normalize(String)},
454     * thus <code>..</code> is handled.
455     * <p>
456     * If <code>pathToAdd</code> is absolute (has an absolute prefix), then
457     * it will be normalized and returned.
458     * Otherwise, the paths will be joined, normalized and returned.
459     * <p>
460     * The output will be the same on both Unix and Windows except
461     * for the separator character.
462     * <pre>
463     * /foo/ + bar          --&gt;   /foo/bar
464     * /foo + bar           --&gt;   /foo/bar
465     * /foo + /bar          --&gt;   /bar
466     * /foo + C:/bar        --&gt;   C:/bar
467     * /foo + C:bar         --&gt;   C:bar (*)
468     * /foo/a/ + ../bar     --&gt;   foo/bar
469     * /foo/ + ../../bar    --&gt;   null
470     * /foo/ + /bar         --&gt;   /bar
471     * /foo/.. + /bar       --&gt;   /bar
472     * /foo + bar/c.txt     --&gt;   /foo/bar/c.txt
473     * /foo/c.txt + bar     --&gt;   /foo/c.txt/bar (!)
474     * </pre>
475     * (*) Note that the Windows relative drive prefix is unreliable when
476     * used with this method.
477     * (!) Note that the first parameter must be a path. If it ends with a name, then
478     * the name will be built into the concatenated path. If this might be a problem,
479     * use {@link #getFullPath(String)} on the base path argument.
480     *
481     * @param basePath  the base path to attach to, always treated as a path
482     * @param fullFilenameToAdd  the filename (or path) to attach to the base
483     * @return the concatenated path, or null if invalid.  Null bytes inside string will be removed
484     */
485    public static String concat(final String basePath, final String fullFilenameToAdd) {
486        final int prefix = getPrefixLength(fullFilenameToAdd);
487        if (prefix < 0) {
488            return null;
489        }
490        if (prefix > 0) {
491            return normalize(fullFilenameToAdd);
492        }
493        if (basePath == null) {
494            return null;
495        }
496        final int len = basePath.length();
497        if (len == 0) {
498            return normalize(fullFilenameToAdd);
499        }
500        final char ch = basePath.charAt(len - 1);
501        if (isSeparator(ch)) {
502            return normalize(basePath + fullFilenameToAdd);
503        } else {
504            return normalize(basePath + '/' + fullFilenameToAdd);
505        }
506    }
507
508    /**
509     * Determines whether the {@code parent} directory contains the {@code child} element (a file or directory).
510     * <p>
511     * The files names are expected to be normalized.
512     * </p>
513     *
514     * Edge cases:
515     * <ul>
516     * <li>A {@code directory} must not be null: if null, throw IllegalArgumentException</li>
517     * <li>A directory does not contain itself: return false</li>
518     * <li>A null child file is not contained in any parent: return false</li>
519     * </ul>
520     *
521     * @param canonicalParent
522     *            the file to consider as the parent.
523     * @param canonicalChild
524     *            the file to consider as the child.
525     * @return true is the candidate leaf is under by the specified composite. False otherwise.
526     * @throws IOException
527     *             if an IO error occurs while checking the files.
528     * @since 2.2
529     * @see FileUtils#directoryContains(File, File)
530     */
531    public static boolean directoryContains(final String canonicalParent, final String canonicalChild)
532            throws IOException {
533
534        // Fail fast against NullPointerException
535        if (canonicalParent == null) {
536            throw new IllegalArgumentException("Directory must not be null");
537        }
538
539        if (canonicalChild == null) {
540            return false;
541        }
542
543        if (IOCase.SYSTEM.checkEquals(canonicalParent, canonicalChild)) {
544            return false;
545        }
546
547        return IOCase.SYSTEM.checkStartsWith(canonicalChild, canonicalParent);
548    }
549
550    //-----------------------------------------------------------------------
551    /**
552     * Converts all separators to the Unix separator of forward slash.
553     *
554     * @param path  the path to be changed, null ignored
555     * @return the updated path
556     */
557    public static String separatorsToUnix(final String path) {
558        if (path == null || path.indexOf(WINDOWS_SEPARATOR) == NOT_FOUND) {
559            return path;
560        }
561        return path.replace(WINDOWS_SEPARATOR, UNIX_SEPARATOR);
562    }
563
564    /**
565     * Converts all separators to the Windows separator of backslash.
566     *
567     * @param path  the path to be changed, null ignored
568     * @return the updated path
569     */
570    public static String separatorsToWindows(final String path) {
571        if (path == null || path.indexOf(UNIX_SEPARATOR) == NOT_FOUND) {
572            return path;
573        }
574        return path.replace(UNIX_SEPARATOR, WINDOWS_SEPARATOR);
575    }
576
577    /**
578     * Converts all separators to the system separator.
579     *
580     * @param path  the path to be changed, null ignored
581     * @return the updated path
582     */
583    public static String separatorsToSystem(final String path) {
584        if (path == null) {
585            return null;
586        }
587        if (isSystemWindows()) {
588            return separatorsToWindows(path);
589        } else {
590            return separatorsToUnix(path);
591        }
592    }
593
594    //-----------------------------------------------------------------------
595    /**
596     * Returns the length of the filename prefix, such as <code>C:/</code> or <code>~/</code>.
597     * <p>
598     * This method will handle a file in either Unix or Windows format.
599     * <p>
600     * The prefix length includes the first slash in the full filename
601     * if applicable. Thus, it is possible that the length returned is greater
602     * than the length of the input string.
603     * <pre>
604     * Windows:
605     * a\b\c.txt           --&gt; ""          --&gt; relative
606     * \a\b\c.txt          --&gt; "\"         --&gt; current drive absolute
607     * C:a\b\c.txt         --&gt; "C:"        --&gt; drive relative
608     * C:\a\b\c.txt        --&gt; "C:\"       --&gt; absolute
609     * \\server\a\b\c.txt  --&gt; "\\server\" --&gt; UNC
610     * \\\a\b\c.txt        --&gt;  error, length = -1
611     *
612     * Unix:
613     * a/b/c.txt           --&gt; ""          --&gt; relative
614     * /a/b/c.txt          --&gt; "/"         --&gt; absolute
615     * ~/a/b/c.txt         --&gt; "~/"        --&gt; current user
616     * ~                   --&gt; "~/"        --&gt; current user (slash added)
617     * ~user/a/b/c.txt     --&gt; "~user/"    --&gt; named user
618     * ~user               --&gt; "~user/"    --&gt; named user (slash added)
619     * //server/a/b/c.txt  --&gt; "//server/"
620     * ///a/b/c.txt        --&gt; error, length = -1
621     * </pre>
622     * <p>
623     * The output will be the same irrespective of the machine that the code is running on.
624     * ie. both Unix and Windows prefixes are matched regardless.
625     *
626     * Note that a leading // (or \\) is used to indicate a UNC name on Windows.
627     * These must be followed by a server name, so double-slashes are not collapsed
628     * to a single slash at the start of the filename.
629     *
630     * @param filename  the filename to find the prefix in, null returns -1
631     * @return the length of the prefix, -1 if invalid or null
632     */
633    public static int getPrefixLength(final String filename) {
634        if (filename == null) {
635            return NOT_FOUND;
636        }
637        final int len = filename.length();
638        if (len == 0) {
639            return 0;
640        }
641        char ch0 = filename.charAt(0);
642        if (ch0 == ':') {
643            return NOT_FOUND;
644        }
645        if (len == 1) {
646            if (ch0 == '~') {
647                return 2;  // return a length greater than the input
648            }
649            return isSeparator(ch0) ? 1 : 0;
650        } else {
651            if (ch0 == '~') {
652                int posUnix = filename.indexOf(UNIX_SEPARATOR, 1);
653                int posWin = filename.indexOf(WINDOWS_SEPARATOR, 1);
654                if (posUnix == NOT_FOUND && posWin == NOT_FOUND) {
655                    return len + 1;  // return a length greater than the input
656                }
657                posUnix = posUnix == NOT_FOUND ? posWin : posUnix;
658                posWin = posWin == NOT_FOUND ? posUnix : posWin;
659                return Math.min(posUnix, posWin) + 1;
660            }
661            final char ch1 = filename.charAt(1);
662            if (ch1 == ':') {
663                ch0 = Character.toUpperCase(ch0);
664                if (ch0 >= 'A' && ch0 <= 'Z') {
665                    if (len == 2 || isSeparator(filename.charAt(2)) == false) {
666                        return 2;
667                    }
668                    return 3;
669                } else if (ch0 == UNIX_SEPARATOR) {
670                    return 1;
671                }
672                return NOT_FOUND;
673
674            } else if (isSeparator(ch0) && isSeparator(ch1)) {
675                int posUnix = filename.indexOf(UNIX_SEPARATOR, 2);
676                int posWin = filename.indexOf(WINDOWS_SEPARATOR, 2);
677                if (posUnix == NOT_FOUND && posWin == NOT_FOUND || posUnix == 2 || posWin == 2) {
678                    return NOT_FOUND;
679                }
680                posUnix = posUnix == NOT_FOUND ? posWin : posUnix;
681                posWin = posWin == NOT_FOUND ? posUnix : posWin;
682                return Math.min(posUnix, posWin) + 1;
683            } else {
684                return isSeparator(ch0) ? 1 : 0;
685            }
686        }
687    }
688
689    /**
690     * Returns the index of the last directory separator character.
691     * <p>
692     * This method will handle a file in either Unix or Windows format.
693     * The position of the last forward or backslash is returned.
694     * <p>
695     * The output will be the same irrespective of the machine that the code is running on.
696     *
697     * @param filename  the filename to find the last path separator in, null returns -1
698     * @return the index of the last separator character, or -1 if there
699     * is no such character
700     */
701    public static int indexOfLastSeparator(final String filename) {
702        if (filename == null) {
703            return NOT_FOUND;
704        }
705        final int lastUnixPos = filename.lastIndexOf(UNIX_SEPARATOR);
706        final int lastWindowsPos = filename.lastIndexOf(WINDOWS_SEPARATOR);
707        return Math.max(lastUnixPos, lastWindowsPos);
708    }
709
710    /**
711     * Returns the index of the last extension separator character, which is a dot.
712     * <p>
713     * This method also checks that there is no directory separator after the last dot. To do this it uses
714     * {@link #indexOfLastSeparator(String)} which will handle a file in either Unix or Windows format.
715     * </p>
716     * <p>
717     * The output will be the same irrespective of the machine that the code is running on.
718     * </p>
719     *
720     * @param filename
721     *            the filename to find the last extension separator in, null returns -1
722     * @return the index of the last extension separator character, or -1 if there is no such character
723     */
724    public static int indexOfExtension(final String filename) {
725        if (filename == null) {
726            return NOT_FOUND;
727        }
728        final int extensionPos = filename.lastIndexOf(EXTENSION_SEPARATOR);
729        final int lastSeparator = indexOfLastSeparator(filename);
730        return lastSeparator > extensionPos ? NOT_FOUND : extensionPos;
731    }
732
733    //-----------------------------------------------------------------------
734    /**
735     * Gets the prefix from a full filename, such as <code>C:/</code>
736     * or <code>~/</code>.
737     * <p>
738     * This method will handle a file in either Unix or Windows format.
739     * The prefix includes the first slash in the full filename where applicable.
740     * <pre>
741     * Windows:
742     * a\b\c.txt           --&gt; ""          --&gt; relative
743     * \a\b\c.txt          --&gt; "\"         --&gt; current drive absolute
744     * C:a\b\c.txt         --&gt; "C:"        --&gt; drive relative
745     * C:\a\b\c.txt        --&gt; "C:\"       --&gt; absolute
746     * \\server\a\b\c.txt  --&gt; "\\server\" --&gt; UNC
747     *
748     * Unix:
749     * a/b/c.txt           --&gt; ""          --&gt; relative
750     * /a/b/c.txt          --&gt; "/"         --&gt; absolute
751     * ~/a/b/c.txt         --&gt; "~/"        --&gt; current user
752     * ~                   --&gt; "~/"        --&gt; current user (slash added)
753     * ~user/a/b/c.txt     --&gt; "~user/"    --&gt; named user
754     * ~user               --&gt; "~user/"    --&gt; named user (slash added)
755     * </pre>
756     * <p>
757     * The output will be the same irrespective of the machine that the code is running on.
758     * ie. both Unix and Windows prefixes are matched regardless.
759     *
760     * @param filename  the filename to query, null returns null
761     * @return the prefix of the file, null if invalid. Null bytes inside string will be removed
762     */
763    public static String getPrefix(final String filename) {
764        if (filename == null) {
765            return null;
766        }
767        final int len = getPrefixLength(filename);
768        if (len < 0) {
769            return null;
770        }
771        if (len > filename.length()) {
772            failIfNullBytePresent(filename + UNIX_SEPARATOR);
773            return filename + UNIX_SEPARATOR;
774        }
775        final String path = filename.substring(0, len);
776        failIfNullBytePresent(path);
777        return path;
778    }
779
780    /**
781     * Gets the path from a full filename, which excludes the prefix.
782     * <p>
783     * This method will handle a file in either Unix or Windows format.
784     * The method is entirely text based, and returns the text before and
785     * including the last forward or backslash.
786     * <pre>
787     * C:\a\b\c.txt --&gt; a\b\
788     * ~/a/b/c.txt  --&gt; a/b/
789     * a.txt        --&gt; ""
790     * a/b/c        --&gt; a/b/
791     * a/b/c/       --&gt; a/b/c/
792     * </pre>
793     * <p>
794     * The output will be the same irrespective of the machine that the code is running on.
795     * <p>
796     * This method drops the prefix from the result.
797     * See {@link #getFullPath(String)} for the method that retains the prefix.
798     *
799     * @param filename  the filename to query, null returns null
800     * @return the path of the file, an empty string if none exists, null if invalid.
801     * Null bytes inside string will be removed
802     */
803    public static String getPath(final String filename) {
804        return doGetPath(filename, 1);
805    }
806
807    /**
808     * Gets the path from a full filename, which excludes the prefix, and
809     * also excluding the final directory separator.
810     * <p>
811     * This method will handle a file in either Unix or Windows format.
812     * The method is entirely text based, and returns the text before the
813     * last forward or backslash.
814     * <pre>
815     * C:\a\b\c.txt --&gt; a\b
816     * ~/a/b/c.txt  --&gt; a/b
817     * a.txt        --&gt; ""
818     * a/b/c        --&gt; a/b
819     * a/b/c/       --&gt; a/b/c
820     * </pre>
821     * <p>
822     * The output will be the same irrespective of the machine that the code is running on.
823     * <p>
824     * This method drops the prefix from the result.
825     * See {@link #getFullPathNoEndSeparator(String)} for the method that retains the prefix.
826     *
827     * @param filename  the filename to query, null returns null
828     * @return the path of the file, an empty string if none exists, null if invalid.
829     * Null bytes inside string will be removed
830     */
831    public static String getPathNoEndSeparator(final String filename) {
832        return doGetPath(filename, 0);
833    }
834
835    /**
836     * Does the work of getting the path.
837     *
838     * @param filename  the filename
839     * @param separatorAdd  0 to omit the end separator, 1 to return it
840     * @return the path. Null bytes inside string will be removed
841     */
842    private static String doGetPath(final String filename, final int separatorAdd) {
843        if (filename == null) {
844            return null;
845        }
846        final int prefix = getPrefixLength(filename);
847        if (prefix < 0) {
848            return null;
849        }
850        final int index = indexOfLastSeparator(filename);
851        final int endIndex = index+separatorAdd;
852        if (prefix >= filename.length() || index < 0 || prefix >= endIndex) {
853            return "";
854        }
855        final String path = filename.substring(prefix, endIndex);
856        failIfNullBytePresent(path);
857        return path;
858    }
859
860    /**
861     * Gets the full path from a full filename, which is the prefix + path.
862     * <p>
863     * This method will handle a file in either Unix or Windows format.
864     * The method is entirely text based, and returns the text before and
865     * including the last forward or backslash.
866     * <pre>
867     * C:\a\b\c.txt --&gt; C:\a\b\
868     * ~/a/b/c.txt  --&gt; ~/a/b/
869     * a.txt        --&gt; ""
870     * a/b/c        --&gt; a/b/
871     * a/b/c/       --&gt; a/b/c/
872     * C:           --&gt; C:
873     * C:\          --&gt; C:\
874     * ~            --&gt; ~/
875     * ~/           --&gt; ~/
876     * ~user        --&gt; ~user/
877     * ~user/       --&gt; ~user/
878     * </pre>
879     * <p>
880     * The output will be the same irrespective of the machine that the code is running on.
881     *
882     * @param filename  the filename to query, null returns null
883     * @return the path of the file, an empty string if none exists, null if invalid
884     */
885    public static String getFullPath(final String filename) {
886        return doGetFullPath(filename, true);
887    }
888
889    /**
890     * Gets the full path from a full filename, which is the prefix + path,
891     * and also excluding the final directory separator.
892     * <p>
893     * This method will handle a file in either Unix or Windows format.
894     * The method is entirely text based, and returns the text before the
895     * last forward or backslash.
896     * <pre>
897     * C:\a\b\c.txt --&gt; C:\a\b
898     * ~/a/b/c.txt  --&gt; ~/a/b
899     * a.txt        --&gt; ""
900     * a/b/c        --&gt; a/b
901     * a/b/c/       --&gt; a/b/c
902     * C:           --&gt; C:
903     * C:\          --&gt; C:\
904     * ~            --&gt; ~
905     * ~/           --&gt; ~
906     * ~user        --&gt; ~user
907     * ~user/       --&gt; ~user
908     * </pre>
909     * <p>
910     * The output will be the same irrespective of the machine that the code is running on.
911     *
912     * @param filename  the filename to query, null returns null
913     * @return the path of the file, an empty string if none exists, null if invalid
914     */
915    public static String getFullPathNoEndSeparator(final String filename) {
916        return doGetFullPath(filename, false);
917    }
918
919    /**
920     * Does the work of getting the path.
921     *
922     * @param filename  the filename
923     * @param includeSeparator  true to include the end separator
924     * @return the path
925     */
926    private static String doGetFullPath(final String filename, final boolean includeSeparator) {
927        if (filename == null) {
928            return null;
929        }
930        final int prefix = getPrefixLength(filename);
931        if (prefix < 0) {
932            return null;
933        }
934        if (prefix >= filename.length()) {
935            if (includeSeparator) {
936                return getPrefix(filename);  // add end slash if necessary
937            } else {
938                return filename;
939            }
940        }
941        final int index = indexOfLastSeparator(filename);
942        if (index < 0) {
943            return filename.substring(0, prefix);
944        }
945        int end = index + (includeSeparator ?  1 : 0);
946        if (end == 0) {
947            end++;
948        }
949        return filename.substring(0, end);
950    }
951
952    /**
953     * Gets the name minus the path from a full filename.
954     * <p>
955     * This method will handle a file in either Unix or Windows format.
956     * The text after the last forward or backslash is returned.
957     * <pre>
958     * a/b/c.txt --&gt; c.txt
959     * a.txt     --&gt; a.txt
960     * a/b/c     --&gt; c
961     * a/b/c/    --&gt; ""
962     * </pre>
963     * <p>
964     * The output will be the same irrespective of the machine that the code is running on.
965     *
966     * @param filename  the filename to query, null returns null
967     * @return the name of the file without the path, or an empty string if none exists.
968     * Null bytes inside string will be removed
969     */
970    public static String getName(final String filename) {
971        if (filename == null) {
972            return null;
973        }
974        failIfNullBytePresent(filename);
975        final int index = indexOfLastSeparator(filename);
976        return filename.substring(index + 1);
977    }
978
979    /**
980     * Check the input for null bytes, a sign of unsanitized data being passed to to file level functions.
981     *
982     * This may be used for poison byte attacks.
983     * @param path the path to check
984     */
985    private static void failIfNullBytePresent(final String path) {
986        final int len = path.length();
987        for (int i = 0; i < len; i++) {
988            if (path.charAt(i) == 0) {
989                throw new IllegalArgumentException("Null byte present in file/path name. There are no " +
990                        "known legitimate use cases for such data, but several injection attacks may use it");
991            }
992        }
993    }
994
995    /**
996     * Gets the base name, minus the full path and extension, from a full filename.
997     * <p>
998     * This method will handle a file in either Unix or Windows format.
999     * The text after the last forward or backslash and before the last dot is returned.
1000     * <pre>
1001     * a/b/c.txt --&gt; c
1002     * a.txt     --&gt; a
1003     * a/b/c     --&gt; c
1004     * a/b/c/    --&gt; ""
1005     * </pre>
1006     * <p>
1007     * The output will be the same irrespective of the machine that the code is running on.
1008     *
1009     * @param filename  the filename to query, null returns null
1010     * @return the name of the file without the path, or an empty string if none exists. Null bytes inside string
1011     * will be removed
1012     */
1013    public static String getBaseName(final String filename) {
1014        return removeExtension(getName(filename));
1015    }
1016
1017    /**
1018     * Gets the extension of a filename.
1019     * <p>
1020     * This method returns the textual part of the filename after the last dot.
1021     * There must be no directory separator after the dot.
1022     * <pre>
1023     * foo.txt      --&gt; "txt"
1024     * a/b/c.jpg    --&gt; "jpg"
1025     * a/b.txt/c    --&gt; ""
1026     * a/b/c        --&gt; ""
1027     * </pre>
1028     * <p>
1029     * The output will be the same irrespective of the machine that the code is running on.
1030     *
1031     * @param filename the filename to retrieve the extension of.
1032     * @return the extension of the file or an empty string if none exists or {@code null}
1033     * if the filename is {@code null}.
1034     */
1035    public static String getExtension(final String filename) {
1036        if (filename == null) {
1037            return null;
1038        }
1039        final int index = indexOfExtension(filename);
1040        if (index == NOT_FOUND) {
1041            return "";
1042        } else {
1043            return filename.substring(index + 1);
1044        }
1045    }
1046
1047    //-----------------------------------------------------------------------
1048    /**
1049     * Removes the extension from a filename.
1050     * <p>
1051     * This method returns the textual part of the filename before the last dot.
1052     * There must be no directory separator after the dot.
1053     * <pre>
1054     * foo.txt    --&gt; foo
1055     * a\b\c.jpg  --&gt; a\b\c
1056     * a\b\c      --&gt; a\b\c
1057     * a.b\c      --&gt; a.b\c
1058     * </pre>
1059     * <p>
1060     * The output will be the same irrespective of the machine that the code is running on.
1061     *
1062     * @param filename  the filename to query, null returns null
1063     * @return the filename minus the extension
1064     */
1065    public static String removeExtension(final String filename) {
1066        if (filename == null) {
1067            return null;
1068        }
1069        failIfNullBytePresent(filename);
1070
1071        final int index = indexOfExtension(filename);
1072        if (index == NOT_FOUND) {
1073            return filename;
1074        } else {
1075            return filename.substring(0, index);
1076        }
1077    }
1078
1079    //-----------------------------------------------------------------------
1080    /**
1081     * Checks whether two filenames are equal exactly.
1082     * <p>
1083     * No processing is performed on the filenames other than comparison,
1084     * thus this is merely a null-safe case-sensitive equals.
1085     *
1086     * @param filename1  the first filename to query, may be null
1087     * @param filename2  the second filename to query, may be null
1088     * @return true if the filenames are equal, null equals null
1089     * @see IOCase#SENSITIVE
1090     */
1091    public static boolean equals(final String filename1, final String filename2) {
1092        return equals(filename1, filename2, false, IOCase.SENSITIVE);
1093    }
1094
1095    /**
1096     * Checks whether two filenames are equal using the case rules of the system.
1097     * <p>
1098     * No processing is performed on the filenames other than comparison.
1099     * The check is case-sensitive on Unix and case-insensitive on Windows.
1100     *
1101     * @param filename1  the first filename to query, may be null
1102     * @param filename2  the second filename to query, may be null
1103     * @return true if the filenames are equal, null equals null
1104     * @see IOCase#SYSTEM
1105     */
1106    public static boolean equalsOnSystem(final String filename1, final String filename2) {
1107        return equals(filename1, filename2, false, IOCase.SYSTEM);
1108    }
1109
1110    //-----------------------------------------------------------------------
1111    /**
1112     * Checks whether two filenames are equal after both have been normalized.
1113     * <p>
1114     * Both filenames are first passed to {@link #normalize(String)}.
1115     * The check is then performed in a case-sensitive manner.
1116     *
1117     * @param filename1  the first filename to query, may be null
1118     * @param filename2  the second filename to query, may be null
1119     * @return true if the filenames are equal, null equals null
1120     * @see IOCase#SENSITIVE
1121     */
1122    public static boolean equalsNormalized(final String filename1, final String filename2) {
1123        return equals(filename1, filename2, true, IOCase.SENSITIVE);
1124    }
1125
1126    /**
1127     * Checks whether two filenames are equal after both have been normalized
1128     * and using the case rules of the system.
1129     * <p>
1130     * Both filenames are first passed to {@link #normalize(String)}.
1131     * The check is then performed case-sensitive on Unix and
1132     * case-insensitive on Windows.
1133     *
1134     * @param filename1  the first filename to query, may be null
1135     * @param filename2  the second filename to query, may be null
1136     * @return true if the filenames are equal, null equals null
1137     * @see IOCase#SYSTEM
1138     */
1139    public static boolean equalsNormalizedOnSystem(final String filename1, final String filename2) {
1140        return equals(filename1, filename2, true, IOCase.SYSTEM);
1141    }
1142
1143    /**
1144     * Checks whether two filenames are equal, optionally normalizing and providing
1145     * control over the case-sensitivity.
1146     *
1147     * @param filename1  the first filename to query, may be null
1148     * @param filename2  the second filename to query, may be null
1149     * @param normalized  whether to normalize the filenames
1150     * @param caseSensitivity  what case sensitivity rule to use, null means case-sensitive
1151     * @return true if the filenames are equal, null equals null
1152     * @since 1.3
1153     */
1154    public static boolean equals(
1155            String filename1, String filename2,
1156            final boolean normalized, IOCase caseSensitivity) {
1157
1158        if (filename1 == null || filename2 == null) {
1159            return filename1 == null && filename2 == null;
1160        }
1161        if (normalized) {
1162            filename1 = normalize(filename1);
1163            filename2 = normalize(filename2);
1164            if (filename1 == null || filename2 == null) {
1165                throw new NullPointerException(
1166                    "Error normalizing one or both of the file names");
1167            }
1168        }
1169        if (caseSensitivity == null) {
1170            caseSensitivity = IOCase.SENSITIVE;
1171        }
1172        return caseSensitivity.checkEquals(filename1, filename2);
1173    }
1174
1175    //-----------------------------------------------------------------------
1176    /**
1177     * Checks whether the extension of the filename is that specified.
1178     * <p>
1179     * This method obtains the extension as the textual part of the filename
1180     * after the last dot. There must be no directory separator after the dot.
1181     * The extension check is case-sensitive on all platforms.
1182     *
1183     * @param filename  the filename to query, null returns false
1184     * @param extension  the extension to check for, null or empty checks for no extension
1185     * @return true if the filename has the specified extension
1186     * @throws java.lang.IllegalArgumentException if the supplied filename contains null bytes
1187     */
1188    public static boolean isExtension(final String filename, final String extension) {
1189        if (filename == null) {
1190            return false;
1191        }
1192        failIfNullBytePresent(filename);
1193
1194        if (extension == null || extension.isEmpty()) {
1195            return indexOfExtension(filename) == NOT_FOUND;
1196        }
1197        final String fileExt = getExtension(filename);
1198        return fileExt.equals(extension);
1199    }
1200
1201    /**
1202     * Checks whether the extension of the filename is one of those specified.
1203     * <p>
1204     * This method obtains the extension as the textual part of the filename
1205     * after the last dot. There must be no directory separator after the dot.
1206     * The extension check is case-sensitive on all platforms.
1207     *
1208     * @param filename  the filename to query, null returns false
1209     * @param extensions  the extensions to check for, null checks for no extension
1210     * @return true if the filename is one of the extensions
1211     * @throws java.lang.IllegalArgumentException if the supplied filename contains null bytes
1212     */
1213    public static boolean isExtension(final String filename, final String[] extensions) {
1214        if (filename == null) {
1215            return false;
1216        }
1217        failIfNullBytePresent(filename);
1218
1219        if (extensions == null || extensions.length == 0) {
1220            return indexOfExtension(filename) == NOT_FOUND;
1221        }
1222        final String fileExt = getExtension(filename);
1223        for (final String extension : extensions) {
1224            if (fileExt.equals(extension)) {
1225                return true;
1226            }
1227        }
1228        return false;
1229    }
1230
1231    /**
1232     * Checks whether the extension of the filename is one of those specified.
1233     * <p>
1234     * This method obtains the extension as the textual part of the filename
1235     * after the last dot. There must be no directory separator after the dot.
1236     * The extension check is case-sensitive on all platforms.
1237     *
1238     * @param filename  the filename to query, null returns false
1239     * @param extensions  the extensions to check for, null checks for no extension
1240     * @return true if the filename is one of the extensions
1241     * @throws java.lang.IllegalArgumentException if the supplied filename contains null bytes
1242     */
1243    public static boolean isExtension(final String filename, final Collection<String> extensions) {
1244        if (filename == null) {
1245            return false;
1246        }
1247        failIfNullBytePresent(filename);
1248
1249        if (extensions == null || extensions.isEmpty()) {
1250            return indexOfExtension(filename) == NOT_FOUND;
1251        }
1252        final String fileExt = getExtension(filename);
1253        for (final String extension : extensions) {
1254            if (fileExt.equals(extension)) {
1255                return true;
1256            }
1257        }
1258        return false;
1259    }
1260
1261    //-----------------------------------------------------------------------
1262    /**
1263     * Checks a filename to see if it matches the specified wildcard matcher,
1264     * always testing case-sensitive.
1265     * <p>
1266     * The wildcard matcher uses the characters '?' and '*' to represent a
1267     * single or multiple (zero or more) wildcard characters.
1268     * This is the same as often found on Dos/Unix command lines.
1269     * The check is case-sensitive always.
1270     * <pre>
1271     * wildcardMatch("c.txt", "*.txt")      --&gt; true
1272     * wildcardMatch("c.txt", "*.jpg")      --&gt; false
1273     * wildcardMatch("a/b/c.txt", "a/b/*")  --&gt; true
1274     * wildcardMatch("c.txt", "*.???")      --&gt; true
1275     * wildcardMatch("c.txt", "*.????")     --&gt; false
1276     * </pre>
1277     * N.B. the sequence "*?" does not work properly at present in match strings.
1278     *
1279     * @param filename  the filename to match on
1280     * @param wildcardMatcher  the wildcard string to match against
1281     * @return true if the filename matches the wildcard string
1282     * @see IOCase#SENSITIVE
1283     */
1284    public static boolean wildcardMatch(final String filename, final String wildcardMatcher) {
1285        return wildcardMatch(filename, wildcardMatcher, IOCase.SENSITIVE);
1286    }
1287
1288    /**
1289     * Checks a filename to see if it matches the specified wildcard matcher
1290     * using the case rules of the system.
1291     * <p>
1292     * The wildcard matcher uses the characters '?' and '*' to represent a
1293     * single or multiple (zero or more) wildcard characters.
1294     * This is the same as often found on Dos/Unix command lines.
1295     * The check is case-sensitive on Unix and case-insensitive on Windows.
1296     * <pre>
1297     * wildcardMatch("c.txt", "*.txt")      --&gt; true
1298     * wildcardMatch("c.txt", "*.jpg")      --&gt; false
1299     * wildcardMatch("a/b/c.txt", "a/b/*")  --&gt; true
1300     * wildcardMatch("c.txt", "*.???")      --&gt; true
1301     * wildcardMatch("c.txt", "*.????")     --&gt; false
1302     * </pre>
1303     * N.B. the sequence "*?" does not work properly at present in match strings.
1304     *
1305     * @param filename  the filename to match on
1306     * @param wildcardMatcher  the wildcard string to match against
1307     * @return true if the filename matches the wildcard string
1308     * @see IOCase#SYSTEM
1309     */
1310    public static boolean wildcardMatchOnSystem(final String filename, final String wildcardMatcher) {
1311        return wildcardMatch(filename, wildcardMatcher, IOCase.SYSTEM);
1312    }
1313
1314    /**
1315     * Checks a filename to see if it matches the specified wildcard matcher
1316     * allowing control over case-sensitivity.
1317     * <p>
1318     * The wildcard matcher uses the characters '?' and '*' to represent a
1319     * single or multiple (zero or more) wildcard characters.
1320     * N.B. the sequence "*?" does not work properly at present in match strings.
1321     *
1322     * @param filename  the filename to match on
1323     * @param wildcardMatcher  the wildcard string to match against
1324     * @param caseSensitivity  what case sensitivity rule to use, null means case-sensitive
1325     * @return true if the filename matches the wildcard string
1326     * @since 1.3
1327     */
1328    public static boolean wildcardMatch(final String filename, final String wildcardMatcher, IOCase caseSensitivity) {
1329        if (filename == null && wildcardMatcher == null) {
1330            return true;
1331        }
1332        if (filename == null || wildcardMatcher == null) {
1333            return false;
1334        }
1335        if (caseSensitivity == null) {
1336            caseSensitivity = IOCase.SENSITIVE;
1337        }
1338        final String[] wcs = splitOnTokens(wildcardMatcher);
1339        boolean anyChars = false;
1340        int textIdx = 0;
1341        int wcsIdx = 0;
1342        final Stack<int[]> backtrack = new Stack<>();
1343
1344        // loop around a backtrack stack, to handle complex * matching
1345        do {
1346            if (backtrack.size() > 0) {
1347                final int[] array = backtrack.pop();
1348                wcsIdx = array[0];
1349                textIdx = array[1];
1350                anyChars = true;
1351            }
1352
1353            // loop whilst tokens and text left to process
1354            while (wcsIdx < wcs.length) {
1355
1356                if (wcs[wcsIdx].equals("?")) {
1357                    // ? so move to next text char
1358                    textIdx++;
1359                    if (textIdx > filename.length()) {
1360                        break;
1361                    }
1362                    anyChars = false;
1363
1364                } else if (wcs[wcsIdx].equals("*")) {
1365                    // set any chars status
1366                    anyChars = true;
1367                    if (wcsIdx == wcs.length - 1) {
1368                        textIdx = filename.length();
1369                    }
1370
1371                } else {
1372                    // matching text token
1373                    if (anyChars) {
1374                        // any chars then try to locate text token
1375                        textIdx = caseSensitivity.checkIndexOf(filename, textIdx, wcs[wcsIdx]);
1376                        if (textIdx == NOT_FOUND) {
1377                            // token not found
1378                            break;
1379                        }
1380                        final int repeat = caseSensitivity.checkIndexOf(filename, textIdx + 1, wcs[wcsIdx]);
1381                        if (repeat >= 0) {
1382                            backtrack.push(new int[] {wcsIdx, repeat});
1383                        }
1384                    } else {
1385                        // matching from current position
1386                        if (!caseSensitivity.checkRegionMatches(filename, textIdx, wcs[wcsIdx])) {
1387                            // couldnt match token
1388                            break;
1389                        }
1390                    }
1391
1392                    // matched text token, move text index to end of matched token
1393                    textIdx += wcs[wcsIdx].length();
1394                    anyChars = false;
1395                }
1396
1397                wcsIdx++;
1398            }
1399
1400            // full match
1401            if (wcsIdx == wcs.length && textIdx == filename.length()) {
1402                return true;
1403            }
1404
1405        } while (backtrack.size() > 0);
1406
1407        return false;
1408    }
1409
1410    /**
1411     * Splits a string into a number of tokens.
1412     * The text is split by '?' and '*'.
1413     * Where multiple '*' occur consecutively they are collapsed into a single '*'.
1414     *
1415     * @param text  the text to split
1416     * @return the array of tokens, never null
1417     */
1418    static String[] splitOnTokens(final String text) {
1419        // used by wildcardMatch
1420        // package level so a unit test may run on this
1421
1422        if (text.indexOf('?') == NOT_FOUND && text.indexOf('*') == NOT_FOUND) {
1423            return new String[] { text };
1424        }
1425
1426        final char[] array = text.toCharArray();
1427        final ArrayList<String> list = new ArrayList<>();
1428        final StringBuilder buffer = new StringBuilder();
1429        char prevChar = 0;
1430        for (final char ch : array) {
1431            if (ch == '?' || ch == '*') {
1432                if (buffer.length() != 0) {
1433                    list.add(buffer.toString());
1434                    buffer.setLength(0);
1435                }
1436                if (ch == '?') {
1437                    list.add("?");
1438                } else if (prevChar != '*') {// ch == '*' here; check if previous char was '*'
1439                    list.add("*");
1440                }
1441            } else {
1442                buffer.append(ch);
1443            }
1444            prevChar = ch;
1445        }
1446        if (buffer.length() != 0) {
1447            list.add(buffer.toString());
1448        }
1449
1450        return list.toArray( new String[ list.size() ] );
1451    }
1452
1453}