001// Generated by delombok at Sat Feb 25 17:58:34 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 var 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 var 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        var size = filename.length();
337        if (size == 0) {
338            return filename;
339        }
340        final var prefix = getPrefixLength(filename);
341        if (prefix < 0) {
342            return null;
343        }
344        final var 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 var otherSeparator = separator == SYSTEM_SEPARATOR ? OTHER_SEPARATOR : SYSTEM_SEPARATOR;
348        for (var 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        var lastIsDirectory = true;
355        if (array[size - 1] != separator) {
356            array[size] = separator;
357            size++;
358            lastIsDirectory = false;
359        }
360        // adjoining slashes
361        for (var i = prefix + 1; i < size; i++) {
362            if (array[i] == separator && array[i - 1] == separator) {
363                System.arraycopy(array, i, array, i - 1, size - i);
364                size--;
365                i--;
366            }
367        }
368        // dot slash
369        for (var i = prefix + 1; i < size; i++) {
370            if (array[i] == separator && array[i - 1] == '.' && (i == prefix + 1 || array[i - 2] == separator)) {
371                if (i == size - 1) {
372                    lastIsDirectory = true;
373                }
374                System.arraycopy(array, i + 1, array, i - 1, size - i);
375                size -= 2;
376                i--;
377            }
378        }
379        // double dot slash
380        outer:
381        for (var i = prefix + 2; i < size; i++) {
382            if (array[i] == separator && array[i - 1] == '.' && array[i - 2] == '.' && (i == prefix + 2 || array[i - 3] == separator)) {
383                if (i == prefix + 2) {
384                    return null;
385                }
386                if (i == size - 1) {
387                    lastIsDirectory = true;
388                }
389                int j;
390                for (j = i - 4; j >= prefix; j--) {
391                    if (array[j] == separator) {
392                        // remove b/../ from a/b/../c
393                        System.arraycopy(array, i + 1, array, j + 1, size - i);
394                        size -= i - j;
395                        i = j + 1;
396                        continue outer;
397                    }
398                }
399                // remove a/../ from a/../c
400                System.arraycopy(array, i + 1, array, prefix, size - i);
401                size -= i + 1 - prefix;
402                i = prefix + 1;
403            }
404        }
405        if (size <= 0) {
406            // should never be less than 0
407            return "";
408        }
409        if (size <= prefix || lastIsDirectory && keepSeparator) {
410            return new String(array, 0, size); // keep trailing separator
411        }
412        return new String(array, 0, size - 1); // lose trailing separator
413    }
414
415    // -----------------------------------------------------------------------
416    /**
417     * Concatenates a filename to a base path using normal command line style rules.
418     * <p>
419     * The effect is equivalent to resultant directory after changing
420     * directory to the first argument, followed by changing directory to
421     * the second argument.
422     * <p>
423     * The first argument is the base path, the second is the path to concatenate.
424     * The returned path is always normalized via {@link #normalize(String)},
425     * thus <code>..</code> is handled.
426     * <p>
427     * If <code>pathToAdd</code> is absolute (has an absolute prefix), then
428     * it will be normalized and returned.
429     * Otherwise, the paths will be joined, normalized and returned.
430     * <p>
431     * The output will be the same on both Unix and Windows except
432     * for the separator character.
433     *
434     * <pre>
435     * /foo/ + bar          --&gt;   /foo/bar
436     * /foo + bar           --&gt;   /foo/bar
437     * /foo + /bar          --&gt;   /bar
438     * /foo + C:/bar        --&gt;   C:/bar
439     * /foo + C:bar         --&gt;   C:bar (*)
440     * /foo/a/ + ../bar     --&gt;   foo/bar
441     * /foo/ + ../../bar    --&gt;   null
442     * /foo/ + /bar         --&gt;   /bar
443     * /foo/.. + /bar       --&gt;   /bar
444     * /foo + bar/c.txt     --&gt;   /foo/bar/c.txt
445     * /foo/c.txt + bar     --&gt;   /foo/c.txt/bar (!)
446     * </pre>
447     *
448     * (*) Note that the Windows relative drive prefix is unreliable when
449     * used with this method.
450     * (!) Note that the first parameter must be a path. If it ends with a name, then
451     * the name will be built into the concatenated path. If this might be a problem,
452     * use {@link #getFullPath(String)} on the base path argument.
453     *
454     * @param basePath the base path to attach to, always treated as a path
455     * @param fullFilenameToAdd the filename (or path) to attach to the base
456     * @return the concatenated path, or null if invalid. Null bytes inside string will be removed
457     */
458    public static String concat(final String basePath, final String fullFilenameToAdd) {
459        final var prefix = getPrefixLength(fullFilenameToAdd);
460        if (prefix < 0) {
461            return null;
462        }
463        if (prefix > 0) {
464            return normalize(fullFilenameToAdd);
465        }
466        if (basePath == null) {
467            return null;
468        }
469        final var len = basePath.length();
470        if (len == 0) {
471            return normalize(fullFilenameToAdd);
472        }
473        final var ch = basePath.charAt(len - 1);
474        if (isSeparator(ch)) {
475            return normalize(basePath + fullFilenameToAdd);
476        }
477        return normalize(basePath + '/' + fullFilenameToAdd);
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        }
551        return separatorsToUnix(path);
552    }
553
554    // -----------------------------------------------------------------------
555    // owolff: original code
556    /**
557     * Returns the length of the filename prefix, such as <code>C:/</code> or <code>~/</code>.
558     * <p>
559     * This method will handle a file in either Unix or Windows format.
560     * <p>
561     * The prefix length includes the first slash in the full filename
562     * if applicable. Thus, it is possible that the length returned is greater
563     * than the length of the input string.
564     *
565     * <pre>
566     * Windows:
567     * a\b\c.txt           --&gt; ""          --&gt; relative
568     * \a\b\c.txt          --&gt; "\"         --&gt; current drive absolute
569     * C:a\b\c.txt         --&gt; "C:"        --&gt; drive relative
570     * C:\a\b\c.txt        --&gt; "C:\"       --&gt; absolute
571     * \\server\a\b\c.txt  --&gt; "\\server\" --&gt; UNC
572     * \\\a\b\c.txt        --&gt;  error, length = -1
573     *
574     * Unix:
575     * a/b/c.txt           --&gt; ""          --&gt; relative
576     * /a/b/c.txt          --&gt; "/"         --&gt; absolute
577     * ~/a/b/c.txt         --&gt; "~/"        --&gt; current user
578     * ~                   --&gt; "~/"        --&gt; current user (slash added)
579     * ~user/a/b/c.txt     --&gt; "~user/"    --&gt; named user
580     * ~user               --&gt; "~user/"    --&gt; named user (slash added)
581     * //server/a/b/c.txt  --&gt; "//server/"
582     * ///a/b/c.txt        --&gt; error, length = -1
583     * </pre>
584     * <p>
585     * The output will be the same irrespective of the machine that the code is running on.
586     * i.e. both Unix and Windows prefixes are matched regardless.
587     * <p>
588     * Note that a leading // (or \\) is used to indicate a UNC name on Windows.
589     * These must be followed by a server name, so double-slashes are not collapsed
590     * to a single slash at the start of the filename.
591     *
592     * @param filename the filename to find the prefix in, null returns -1
593     * @return the length of the prefix, -1 if invalid or null
594     */
595    @SuppressWarnings({"squid:S3776"})
596    public static int getPrefixLength(final String filename) {
597        if (filename == null) {
598            return NOT_FOUND;
599        }
600        final var len = filename.length();
601        if (len == 0) {
602            return 0;
603        }
604        var ch0 = filename.charAt(0);
605        if (ch0 == ':') {
606            return NOT_FOUND;
607        }
608        if (len == 1) {
609            if (ch0 == '~') {
610                return 2; // return a length greater than the input
611            }
612            return isSeparator(ch0) ? 1 : 0;
613        }
614        if (ch0 == '~') {
615            var posUnix = filename.indexOf(UNIX_SEPARATOR, 1);
616            var posWin = filename.indexOf(WINDOWS_SEPARATOR, 1);
617            if (posUnix == NOT_FOUND && posWin == NOT_FOUND) {
618                return len + 1; // return a length greater than the input
619            }
620            posUnix = posUnix == NOT_FOUND ? posWin : posUnix;
621            posWin = posWin == NOT_FOUND ? posUnix : posWin;
622            return Math.min(posUnix, posWin) + 1;
623        }
624        final var ch1 = filename.charAt(1);
625        if (ch1 == ':') {
626            ch0 = Character.toUpperCase(ch0);
627            if (ch0 >= 'A' && ch0 <= 'Z') {
628                if (len == 2 || !isSeparator(filename.charAt(2))) {
629                    return 2;
630                }
631                return 3;
632            }
633            if (ch0 == UNIX_SEPARATOR) {
634                return 1;
635            }
636            return NOT_FOUND;
637        }
638        if (!isSeparator(ch0) || !isSeparator(ch1)) {
639            return isSeparator(ch0) ? 1 : 0;
640        }
641        var posUnix = filename.indexOf(UNIX_SEPARATOR, 2);
642        var posWin = filename.indexOf(WINDOWS_SEPARATOR, 2);
643        if (posUnix == NOT_FOUND && posWin == NOT_FOUND || posUnix == 2 || posWin == 2) {
644            return NOT_FOUND;
645        }
646        posUnix = posUnix == NOT_FOUND ? posWin : posUnix;
647        posWin = posWin == NOT_FOUND ? posUnix : posWin;
648        return Math.min(posUnix, posWin) + 1;
649    }
650
651    /**
652     * Returns the index of the last directory separator character.
653     * <p>
654     * This method will handle a file in either Unix or Windows format.
655     * The position of the last forward or backslash is returned.
656     * <p>
657     * The output will be the same irrespective of the machine that the code is running on.
658     *
659     * @param filename the filename to find the last path separator in, null returns -1
660     * @return the index of the last separator character, or -1 if there
661     *         is no such character
662     */
663    public static int indexOfLastSeparator(final String filename) {
664        if (filename == null) {
665            return NOT_FOUND;
666        }
667        final var lastUnixPos = filename.lastIndexOf(UNIX_SEPARATOR);
668        final var lastWindowsPos = filename.lastIndexOf(WINDOWS_SEPARATOR);
669        return Math.max(lastUnixPos, lastWindowsPos);
670    }
671
672    /**
673     * Returns the index of the last extension separator character, which is a dot.
674     * <p>
675     * This method also checks that there is no directory separator after the last dot. To do this
676     * it uses
677     * {@link #indexOfLastSeparator(String)} which will handle a file in either Unix or Windows
678     * format.
679     * </p>
680     * <p>
681     * The output will be the same irrespective of the machine that the code is running on.
682     * </p>
683     *
684     * @param filename
685     *            the filename to find the last extension separator in, null returns -1
686     * @return the index of the last extension separator character, or -1 if there is no such
687     *         character
688     */
689    public static int indexOfExtension(final String filename) {
690        if (filename == null) {
691            return NOT_FOUND;
692        }
693        final var extensionPos = filename.lastIndexOf(EXTENSION_SEPARATOR);
694        final var lastSeparator = indexOfLastSeparator(filename);
695        return lastSeparator > extensionPos ? NOT_FOUND : extensionPos;
696    }
697
698    // -----------------------------------------------------------------------
699    /**
700     * Gets the prefix from a full filename, such as <code>C:/</code>
701     * or <code>~/</code>.
702     * <p>
703     * This method will handle a file in either Unix or Windows format.
704     * The prefix includes the first slash in the full filename where applicable.
705     *
706     * <pre>
707     * Windows:
708     * a\b\c.txt           --&gt; ""          --&gt; relative
709     * \a\b\c.txt          --&gt; "\"         --&gt; current drive absolute
710     * C:a\b\c.txt         --&gt; "C:"        --&gt; drive relative
711     * C:\a\b\c.txt        --&gt; "C:\"       --&gt; absolute
712     * \\server\a\b\c.txt  --&gt; "\\server\" --&gt; UNC
713     *
714     * Unix:
715     * a/b/c.txt           --&gt; ""          --&gt; relative
716     * /a/b/c.txt          --&gt; "/"         --&gt; absolute
717     * ~/a/b/c.txt         --&gt; "~/"        --&gt; current user
718     * ~                   --&gt; "~/"        --&gt; current user (slash added)
719     * ~user/a/b/c.txt     --&gt; "~user/"    --&gt; named user
720     * ~user               --&gt; "~user/"    --&gt; named user (slash added)
721     * </pre>
722     * <p>
723     * The output will be the same irrespective of the machine that the code is running on.
724     * i.e. both Unix and Windows prefixes are matched regardless.
725     *
726     * @param filename the filename to query, null returns null
727     * @return the prefix of the file, null if invalid. Null bytes inside string will be removed
728     */
729    public static String getPrefix(final String filename) {
730        if (filename == null) {
731            return null;
732        }
733        final var len = getPrefixLength(filename);
734        if (len < 0) {
735            return null;
736        }
737        if (len > filename.length()) {
738            failIfNullBytePresent(filename + UNIX_SEPARATOR);
739            return filename + UNIX_SEPARATOR;
740        }
741        final var path = filename.substring(0, len);
742        failIfNullBytePresent(path);
743        return path;
744    }
745
746    /**
747     * Gets the path from a full filename, which excludes the prefix.
748     * <p>
749     * This method will handle a file in either Unix or Windows format.
750     * The method is entirely text based, and returns the text before and
751     * including the last forward or backslash.
752     *
753     * <pre>
754     * C:\a\b\c.txt --&gt; a\b\
755     * ~/a/b/c.txt  --&gt; a/b/
756     * a.txt        --&gt; ""
757     * a/b/c        --&gt; a/b/
758     * a/b/c/       --&gt; a/b/c/
759     * </pre>
760     * <p>
761     * The output will be the same irrespective of the machine that the code is running on.
762     * <p>
763     * This method drops the prefix from the result.
764     * See {@link #getFullPath(String)} for the method that retains the prefix.
765     *
766     * @param filename the filename to query, null returns null
767     * @return the path of the file, an empty string if none exists, null if invalid.
768     *         Null bytes inside string will be removed
769     */
770    public static String getPath(final String filename) {
771        return doGetPath(filename, 1);
772    }
773
774    /**
775     * Gets the path from a full filename, which excludes the prefix, and
776     * also excluding the final directory separator.
777     * <p>
778     * This method will handle a file in either Unix or Windows format.
779     * The method is entirely text based, and returns the text before the
780     * last forward or backslash.
781     *
782     * <pre>
783     * C:\a\b\c.txt --&gt; a\b
784     * ~/a/b/c.txt  --&gt; a/b
785     * a.txt        --&gt; ""
786     * a/b/c        --&gt; a/b
787     * a/b/c/       --&gt; a/b/c
788     * </pre>
789     * <p>
790     * The output will be the same irrespective of the machine that the code is running on.
791     * <p>
792     * This method drops the prefix from the result.
793     * See {@link #getFullPathNoEndSeparator(String)} for the method that retains the prefix.
794     *
795     * @param filename the filename to query, null returns null
796     * @return the path of the file, an empty string if none exists, null if invalid.
797     *         Null bytes inside string will be removed
798     */
799    public static String getPathNoEndSeparator(final String filename) {
800        return doGetPath(filename, 0);
801    }
802
803    /**
804     * Does the work of getting the path.
805     *
806     * @param filename the filename
807     * @param separatorAdd 0 to omit the end separator, 1 to return it
808     * @return the path. Null bytes inside string will be removed
809     */
810    private static String doGetPath(final String filename, final int separatorAdd) {
811        if (filename == null) {
812            return null;
813        }
814        final var prefix = getPrefixLength(filename);
815        if (prefix < 0) {
816            return null;
817        }
818        final var index = indexOfLastSeparator(filename);
819        final var endIndex = index + separatorAdd;
820        if (prefix >= filename.length() || index < 0 || prefix >= endIndex) {
821            return "";
822        }
823        final var path = filename.substring(prefix, endIndex);
824        failIfNullBytePresent(path);
825        return path;
826    }
827
828    /**
829     * Gets the full path from a full filename, which is the prefix + path.
830     * <p>
831     * This method will handle a file in either Unix or Windows format.
832     * The method is entirely text based, and returns the text before and
833     * including the last forward or backslash.
834     *
835     * <pre>
836     * C:\a\b\c.txt --&gt; C:\a\b\
837     * ~/a/b/c.txt  --&gt; ~/a/b/
838     * a.txt        --&gt; ""
839     * a/b/c        --&gt; a/b/
840     * a/b/c/       --&gt; a/b/c/
841     * C:           --&gt; C:
842     * C:\          --&gt; C:\
843     * ~            --&gt; ~/
844     * ~/           --&gt; ~/
845     * ~user        --&gt; ~user/
846     * ~user/       --&gt; ~user/
847     * </pre>
848     * <p>
849     * The output will be the same irrespective of the machine that the code is running on.
850     *
851     * @param filename the filename to query, null returns null
852     * @return the path of the file, an empty string if none exists, null if invalid
853     */
854    public static String getFullPath(final String filename) {
855        return doGetFullPath(filename, true);
856    }
857
858    /**
859     * Gets the full path from a full filename, which is the prefix + path,
860     * and also excluding the final directory separator.
861     * <p>
862     * This method will handle a file in either Unix or Windows format.
863     * The method is entirely text based, and returns the text before the
864     * last forward or backslash.
865     *
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 getFullPathNoEndSeparator(final String filename) {
886        return doGetFullPath(filename, false);
887    }
888
889    /**
890     * Does the work of getting the path.
891     *
892     * @param filename the filename
893     * @param includeSeparator true to include the end separator
894     * @return the path
895     */
896    private static String doGetFullPath(final String filename, final boolean includeSeparator) {
897        if (filename == null) {
898            return null;
899        }
900        final var prefix = getPrefixLength(filename);
901        if (prefix < 0) {
902            return null;
903        }
904        if (prefix >= filename.length()) {
905            if (includeSeparator) {
906                return getPrefix(filename); // add end slash if necessary
907            }
908            return filename;
909        }
910        final var index = indexOfLastSeparator(filename);
911        if (index < 0) {
912            return filename.substring(0, prefix);
913        }
914        var end = index + (includeSeparator ? 1 : 0);
915        if (end == 0) {
916            end++;
917        }
918        return filename.substring(0, end);
919    }
920
921    /**
922     * Gets the name minus the path from a full filename.
923     * <p>
924     * This method will handle a file in either Unix or Windows format.
925     * The text after the last forward or backslash is returned.
926     *
927     * <pre>
928     * a/b/c.txt --&gt; c.txt
929     * a.txt     --&gt; a.txt
930     * a/b/c     --&gt; c
931     * a/b/c/    --&gt; ""
932     * </pre>
933     * <p>
934     * The output will be the same irrespective of the machine that the code is running on.
935     *
936     * @param filename the filename to query, null returns null
937     * @return the name of the file without the path, or an empty string if none exists.
938     *         Null bytes inside string will be removed
939     */
940    public static String getName(final String filename) {
941        if (filename == null) {
942            return null;
943        }
944        failIfNullBytePresent(filename);
945        final var index = indexOfLastSeparator(filename);
946        return filename.substring(index + 1);
947    }
948
949    /**
950     * Check the input for null bytes, a sign of unsanitized data being passed to file level
951     * functions.
952     * <p>
953     * This may be used for poison byte attacks.
954     *
955     * @param path the path to check
956     */
957    private static void failIfNullBytePresent(final String path) {
958        final var len = path.length();
959        for (var i = 0; i < len; i++) {
960            if (path.charAt(i) == 0) {
961                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");
962            }
963        }
964    }
965
966    /**
967     * Gets the base name, minus the full path and extension, from a full filename.
968     * <p>
969     * This method will handle a file in either Unix or Windows format.
970     * The text after the last forward or backslash and before the last dot is returned.
971     *
972     * <pre>
973     * a/b/c.txt --&gt; c
974     * a.txt     --&gt; a
975     * a/b/c     --&gt; c
976     * a/b/c/    --&gt; ""
977     * </pre>
978     * <p>
979     * The output will be the same irrespective of the machine that the code is running on.
980     *
981     * @param filename the filename to query, null returns null
982     * @return the name of the file without the path, or an empty string if none exists. Null bytes
983     *         inside string
984     *         will be removed
985     */
986    public static String getBaseName(final String filename) {
987        return removeExtension(getName(filename));
988    }
989
990    /**
991     * Gets the extension of a filename.
992     * <p>
993     * This method returns the textual part of the filename after the last dot.
994     * There must be no directory separator after the dot.
995     *
996     * <pre>
997     * foo.txt      --&gt; "txt"
998     * a/b/c.jpg    --&gt; "jpg"
999     * a/b.txt/c    --&gt; ""
1000     * a/b/c        --&gt; ""
1001     * </pre>
1002     * <p>
1003     * The output will be the same irrespective of the machine that the code is running on.
1004     *
1005     * @param filename the filename to retrieve the extension of.
1006     * @return the extension of the file or an empty string if none exists or {@code null}
1007     *         if the filename is {@code null}.
1008     */
1009    public static String getExtension(final String filename) {
1010        if (filename == null) {
1011            return null;
1012        }
1013        final var index = indexOfExtension(filename);
1014        if (index == NOT_FOUND) {
1015            return "";
1016        }
1017        return filename.substring(index + 1);
1018    }
1019
1020    // -----------------------------------------------------------------------
1021    /**
1022     * Removes the extension from a filename.
1023     * <p>
1024     * This method returns the textual part of the filename before the last dot.
1025     * There must be no directory separator after the dot.
1026     *
1027     * <pre>
1028     * foo.txt    --&gt; foo
1029     * a\b\c.jpg  --&gt; a\b\c
1030     * a\b\c      --&gt; a\b\c
1031     * a.b\c      --&gt; a.b\c
1032     * </pre>
1033     * <p>
1034     * The output will be the same irrespective of the machine that the code is running on.
1035     *
1036     * @param filename the filename to query, null returns null
1037     * @return the filename minus the extension
1038     */
1039    public static String removeExtension(final String filename) {
1040        if (filename == null) {
1041            return null;
1042        }
1043        failIfNullBytePresent(filename);
1044        final var index = indexOfExtension(filename);
1045        if (index == NOT_FOUND) {
1046            return filename;
1047        }
1048        return filename.substring(0, index);
1049    }
1050
1051    // -----------------------------------------------------------------------
1052    /**
1053     * Checks whether two filenames are equal exactly.
1054     * <p>
1055     * No processing is performed on the filenames other than comparison,
1056     * thus this is merely a null-safe case-sensitive equals.
1057     *
1058     * @param filename1 the first filename to query, may be null
1059     * @param filename2 the second filename to query, may be null
1060     * @return true if the filenames are equal, null equals null
1061     */
1062    public static boolean equals(final String filename1, final String filename2) {
1063        return equals(filename1, filename2, false, IOCase.SENSITIVE);
1064    }
1065
1066    /**
1067     * Checks whether two filenames are equal using the case rules of the system.
1068     * <p>
1069     * No processing is performed on the filenames other than comparison.
1070     * The check is case-sensitive on Unix and case-insensitive on Windows.
1071     *
1072     * @param filename1 the first filename to query, may be null
1073     * @param filename2 the second filename to query, may be null
1074     * @return true if the filenames are equal, null equals null
1075     */
1076    public static boolean equalsOnSystem(final String filename1, final String filename2) {
1077        return equals(filename1, filename2, false, IOCase.SYSTEM);
1078    }
1079
1080    // -----------------------------------------------------------------------
1081    /**
1082     * Checks whether two filenames are equal after both have been normalized.
1083     * <p>
1084     * Both filenames are first passed to {@link #normalize(String)}.
1085     * The check is then performed in a case-sensitive manner.
1086     *
1087     * @param filename1 the first filename to query, may be null
1088     * @param filename2 the second filename to query, may be null
1089     * @return true if the filenames are equal, null equals null
1090     */
1091    public static boolean equalsNormalized(final String filename1, final String filename2) {
1092        return equals(filename1, filename2, true, IOCase.SENSITIVE);
1093    }
1094
1095    /**
1096     * Checks whether two filenames are equal after both have been normalized
1097     * and using the case rules of the system.
1098     * <p>
1099     * Both filenames are first passed to {@link #normalize(String)}.
1100     * The check is then performed case-sensitive on Unix and
1101     * case-insensitive on Windows.
1102     *
1103     * @param filename1 the first filename to query, may be null
1104     * @param filename2 the second filename to query, may be null
1105     * @return true if the filenames are equal, null equals null
1106     */
1107    public static boolean equalsNormalizedOnSystem(final String filename1, final String filename2) {
1108        return equals(filename1, filename2, true, IOCase.SYSTEM);
1109    }
1110
1111    /**
1112     * Checks whether two filenames are equal, optionally normalizing and providing
1113     * control over the case-sensitivity.
1114     *
1115     * @param filename1 the first filename to query, may be null
1116     * @param filename2 the second filename to query, may be null
1117     * @param normalized whether to normalize the filenames
1118     * @param caseSensitivity what case sensitivity rule to use, null means case-sensitive
1119     * @return true if the filenames are equal, null equals null
1120     */
1121    public static boolean equals(String filename1, String filename2, final boolean normalized, IOCase caseSensitivity) {
1122        if (filename1 == null || filename2 == null) {
1123            return filename1 == null && filename2 == null;
1124        }
1125        if (normalized) {
1126            filename1 = normalize(filename1);
1127            filename2 = normalize(filename2);
1128            if (filename1 == null || filename2 == null) {
1129                throw new NullPointerException("Error normalizing one or both of the file names");
1130            }
1131        }
1132        if (caseSensitivity == null) {
1133            caseSensitivity = IOCase.SENSITIVE;
1134        }
1135        return caseSensitivity.checkEquals(filename1, filename2);
1136    }
1137
1138    // -----------------------------------------------------------------------
1139    /**
1140     * Checks whether the extension of the filename is that specified.
1141     * <p>
1142     * This method obtains the extension as the textual part of the filename
1143     * after the last dot. There must be no directory separator after the dot.
1144     * The extension check is case-sensitive on all platforms.
1145     *
1146     * @param filename the filename to query, null returns false
1147     * @param extension the extension to check for, null or empty checks for no extension
1148     * @return true if the filename has the specified extension
1149     * @throws java.lang.IllegalArgumentException if the supplied filename contains null bytes
1150     */
1151    public static boolean isExtension(final String filename, final String extension) {
1152        if (filename == null) {
1153            return false;
1154        }
1155        failIfNullBytePresent(filename);
1156        if (extension == null || extension.isEmpty()) {
1157            return indexOfExtension(filename) == NOT_FOUND;
1158        }
1159        final var fileExt = getExtension(filename);
1160        return fileExt.equals(extension);
1161    }
1162
1163    /**
1164     * Checks whether the extension of the filename is one of those specified.
1165     * <p>
1166     * This method obtains the extension as the textual part of the filename
1167     * after the last dot. There must be no directory separator after the dot.
1168     * The extension check is case-sensitive on all platforms.
1169     *
1170     * @param filename the filename to query, null returns false
1171     * @param extensions the extensions to check for, null checks for no extension
1172     * @return true if the filename is one of the extensions
1173     * @throws java.lang.IllegalArgumentException if the supplied filename contains null bytes
1174     */
1175    public static boolean isExtension(final String filename, final String... extensions) {
1176        if (filename == null) {
1177            return false;
1178        }
1179        failIfNullBytePresent(filename);
1180        if (extensions == null || extensions.length == 0) {
1181            return indexOfExtension(filename) == NOT_FOUND;
1182        }
1183        final var fileExt = getExtension(filename);
1184        for (final String extension : extensions) {
1185            if (fileExt.equals(extension)) {
1186                return true;
1187            }
1188        }
1189        return false;
1190    }
1191
1192    /**
1193     * Checks whether the extension of the filename is one of those specified.
1194     * <p>
1195     * This method obtains the extension as the textual part of the filename
1196     * after the last dot. There must be no directory separator after the dot.
1197     * The extension check is case-sensitive on all platforms.
1198     *
1199     * @param filename the filename to query, null returns false
1200     * @param extensions the extensions to check for, null checks for no extension
1201     * @return true if the filename is one of the extensions
1202     * @throws java.lang.IllegalArgumentException if the supplied filename contains null bytes
1203     */
1204    public static boolean isExtension(final String filename, final Collection<String> extensions) {
1205        if (filename == null) {
1206            return false;
1207        }
1208        failIfNullBytePresent(filename);
1209        if (extensions == null || extensions.isEmpty()) {
1210            return indexOfExtension(filename) == NOT_FOUND;
1211        }
1212        final var fileExt = getExtension(filename);
1213        for (final String extension : extensions) {
1214            if (fileExt.equals(extension)) {
1215                return true;
1216            }
1217        }
1218        return false;
1219    }
1220
1221    // -----------------------------------------------------------------------
1222    /**
1223     * Checks a filename to see if it matches the specified wildcard matcher,
1224     * always testing case-sensitive.
1225     * <p>
1226     * The wildcard matcher uses the characters '?' and '*' to represent a
1227     * single or multiple (zero or more) wildcard characters.
1228     * This is the same as often found on Dos/Unix command lines.
1229     * The check is case-sensitive always.
1230     *
1231     * <pre>
1232     * wildcardMatch("c.txt", "*.txt")      --&gt; true
1233     * wildcardMatch("c.txt", "*.jpg")      --&gt; false
1234     * wildcardMatch("a/b/c.txt", "a/b/*")  --&gt; true
1235     * wildcardMatch("c.txt", "*.???")      --&gt; true
1236     * wildcardMatch("c.txt", "*.????")     --&gt; false
1237     * </pre>
1238     *
1239     * N.B. the sequence "*?" does not work properly at present in match strings.
1240     *
1241     * @param filename the filename to match on
1242     * @param wildcardMatcher the wildcard string to match against
1243     * @return true if the filename matches the wildcard string
1244     */
1245    public static boolean wildcardMatch(final String filename, final String wildcardMatcher) {
1246        return wildcardMatch(filename, wildcardMatcher, IOCase.SENSITIVE);
1247    }
1248
1249    /**
1250     * Checks a filename to see if it matches the specified wildcard matcher
1251     * using the case rules of the system.
1252     * <p>
1253     * The wildcard matcher uses the characters '?' and '*' to represent a
1254     * single or multiple (zero or more) wildcard characters.
1255     * This is the same as often found on Dos/Unix command lines.
1256     * The check is case-sensitive on Unix and case-insensitive on Windows.
1257     *
1258     * <pre>
1259     * wildcardMatch("c.txt", "*.txt")      --&gt; true
1260     * wildcardMatch("c.txt", "*.jpg")      --&gt; false
1261     * wildcardMatch("a/b/c.txt", "a/b/*")  --&gt; true
1262     * wildcardMatch("c.txt", "*.???")      --&gt; true
1263     * wildcardMatch("c.txt", "*.????")     --&gt; false
1264     * </pre>
1265     *
1266     * N.B. the sequence "*?" does not work properly at present in match strings.
1267     *
1268     * @param filename the filename to match on
1269     * @param wildcardMatcher the wildcard string to match against
1270     * @return true if the filename matches the wildcard string
1271     */
1272    public static boolean wildcardMatchOnSystem(final String filename, final String wildcardMatcher) {
1273        return wildcardMatch(filename, wildcardMatcher, IOCase.SYSTEM);
1274    }
1275
1276    // owolff: original code
1277    /**
1278     * Checks a filename to see if it matches the specified wildcard matcher
1279     * allowing control over case-sensitivity.
1280     * <p>
1281     * The wildcard matcher uses the characters '?' and '*' to represent a
1282     * single or multiple (zero or more) wildcard characters.
1283     * N.B. the sequence "*?" does not work properly at present in match strings.
1284     *
1285     * @param filename the filename to match on
1286     * @param wildcardMatcher the wildcard string to match against
1287     * @param caseSensitivity what case sensitivity rule to use, null means case-sensitive
1288     * @return true if the filename matches the wildcard string
1289     */
1290    @SuppressWarnings({"squid:S3776", "squid:S135"})
1291    public static boolean wildcardMatch(final String filename, final String wildcardMatcher, IOCase caseSensitivity) {
1292        if (filename == null && wildcardMatcher == null) {
1293            return true;
1294        }
1295        if (filename == null || wildcardMatcher == null) {
1296            return false;
1297        }
1298        if (caseSensitivity == null) {
1299            caseSensitivity = IOCase.SENSITIVE;
1300        }
1301        final var wcs = splitOnTokens(wildcardMatcher);
1302        var anyChars = false;
1303        var textIdx = 0;
1304        var wcsIdx = 0;
1305        final Deque<int[]> backtrack = new ArrayDeque<>();
1306        // loop around a backtrack stack, to handle complex * matching
1307        do {
1308            if (!backtrack.isEmpty()) {
1309                final var array = backtrack.pop();
1310                wcsIdx = array[0];
1311                textIdx = array[1];
1312                anyChars = true;
1313            }
1314            // loop whilst tokens and text left to process
1315            while (wcsIdx < wcs.length) {
1316                if (wcs[wcsIdx].equals("?")) {
1317                    // ? so move to next text char
1318                    textIdx++;
1319                    if (textIdx > filename.length()) {
1320                        break;
1321                    }
1322                    anyChars = false;
1323                } else if (wcs[wcsIdx].equals("*")) {
1324                    // set any chars status
1325                    anyChars = true;
1326                    if (wcsIdx == wcs.length - 1) {
1327                        textIdx = filename.length();
1328                    }
1329                } else {
1330                    // matching text token
1331                    if (anyChars) {
1332                        // any chars then try to locate text token
1333                        textIdx = caseSensitivity.checkIndexOf(filename, textIdx, wcs[wcsIdx]);
1334                        if (textIdx == NOT_FOUND) {
1335                            // token not found
1336                            break;
1337                        }
1338                        final var repeat = caseSensitivity.checkIndexOf(filename, textIdx + 1, wcs[wcsIdx]);
1339                        if (repeat >= 0) {
1340                            backtrack.push(new int[] {wcsIdx, repeat});
1341                        }
1342                    } else  // matching from current position
1343                    if (!caseSensitivity.checkRegionMatches(filename, textIdx, wcs[wcsIdx])) {
1344                        // couldn't match token
1345                        break;
1346                    }
1347                    // matched text token, move text index to end of matched token
1348                    textIdx += wcs[wcsIdx].length();
1349                    anyChars = false;
1350                }
1351                wcsIdx++;
1352            }
1353            // full match
1354            if (wcsIdx == wcs.length && textIdx == filename.length()) {
1355                return true;
1356            }
1357        } while (!backtrack.isEmpty());
1358        return false;
1359    }
1360
1361    /**
1362     * Splits a string into a number of tokens.
1363     * The text is split by '?' and '*'.
1364     * Where multiple '*' occur consecutively they are collapsed into a single '*'.
1365     *
1366     * @param text the text to split
1367     * @return the array of tokens, never null
1368     */
1369    static String[] splitOnTokens(final String text) {
1370        // used by wildcardMatch
1371        // package level so a unit test may run on this
1372        if (text.indexOf('?') == NOT_FOUND && text.indexOf('*') == NOT_FOUND) {
1373            return new String[] {text};
1374        }
1375        final var array = text.toCharArray();
1376        final var list = new ArrayList<String>();
1377        final var buffer = new StringBuilder();
1378        char prevChar = 0;
1379        for (final char ch : array) {
1380            if (ch == '?' || ch == '*') {
1381                if (buffer.length() != 0) {
1382                    list.add(buffer.toString());
1383                    buffer.setLength(0);
1384                }
1385                if (ch == '?') {
1386                    list.add("?");
1387                } else if (prevChar != '*') {
1388                    // ch == '*' here; check if previous char was '*'
1389                    list.add("*");
1390                }
1391            } else {
1392                buffer.append(ch);
1393            }
1394            prevChar = ch;
1395        }
1396        if (buffer.length() != 0) {
1397            list.add(buffer.toString());
1398        }
1399        return list.toArray(new String[list.size()]);
1400    }
1401
1402    @java.lang.SuppressWarnings("all")
1403    @lombok.Generated
1404    private FilenameUtils() {
1405        throw new java.lang.UnsupportedOperationException("This is a utility class and cannot be instantiated");
1406    }
1407}