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