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 --> "" --> relative 050 * \a\b\c.txt --> "\" --> current drive absolute 051 * C:a\b\c.txt --> "C:" --> drive relative 052 * C:\a\b\c.txt --> "C:\" --> absolute 053 * \\server\a\b\c.txt --> "\\server\" --> UNC 054 * 055 * Unix: 056 * a/b/c.txt --> "" --> relative 057 * /a/b/c.txt --> "/" --> absolute 058 * ~/a/b/c.txt --> "~/" --> current user 059 * ~ --> "~/" --> current user (slash added) 060 * ~user/a/b/c.txt --> "~user/" --> named user 061 * ~user --> "~user/" --> 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// --> /foo/ 146 * /foo/./ --> /foo/ 147 * /foo/../bar --> /bar 148 * /foo/../bar/ --> /bar/ 149 * /foo/../bar/../baz --> /baz 150 * //foo//./bar --> /foo/bar 151 * /../ --> null 152 * ../foo --> null 153 * foo/bar/.. --> foo/ 154 * foo/../../bar --> null 155 * foo/../bar --> bar 156 * //server/foo/../bar --> //server/bar 157 * //server/../bar --> null 158 * C:\foo\..\bar --> C:\bar 159 * C:\..\bar --> null 160 * ~/foo/../bar/ --> ~/bar/ 161 * ~/../bar --> 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// --> /foo/ 192 * /foo/./ --> /foo/ 193 * /foo/../bar --> /bar 194 * /foo/../bar/ --> /bar/ 195 * /foo/../bar/../baz --> /baz 196 * //foo//./bar --> /foo/bar 197 * /../ --> null 198 * ../foo --> null 199 * foo/bar/.. --> foo/ 200 * foo/../../bar --> null 201 * foo/../bar --> bar 202 * //server/foo/../bar --> //server/bar 203 * //server/../bar --> null 204 * C:\foo\..\bar --> C:\bar 205 * C:\..\bar --> null 206 * ~/foo/../bar/ --> ~/bar/ 207 * ~/../bar --> 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// --> /foo 244 * /foo/./ --> /foo 245 * /foo/../bar --> /bar 246 * /foo/../bar/ --> /bar 247 * /foo/../bar/../baz --> /baz 248 * //foo//./bar --> /foo/bar 249 * /../ --> null 250 * ../foo --> null 251 * foo/bar/.. --> foo 252 * foo/../../bar --> null 253 * foo/../bar --> bar 254 * //server/foo/../bar --> //server/bar 255 * //server/../bar --> null 256 * C:\foo\..\bar --> C:\bar 257 * C:\..\bar --> null 258 * ~/foo/../bar/ --> ~/bar 259 * ~/../bar --> 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// --> /foo 291 * /foo/./ --> /foo 292 * /foo/../bar --> /bar 293 * /foo/../bar/ --> /bar 294 * /foo/../bar/../baz --> /baz 295 * //foo//./bar --> /foo/bar 296 * /../ --> null 297 * ../foo --> null 298 * foo/bar/.. --> foo 299 * foo/../../bar --> null 300 * foo/../bar --> bar 301 * //server/foo/../bar --> //server/bar 302 * //server/../bar --> null 303 * C:\foo\..\bar --> C:\bar 304 * C:\..\bar --> null 305 * ~/foo/../bar/ --> ~/bar 306 * ~/../bar --> 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 --> /foo/bar 436 * /foo + bar --> /foo/bar 437 * /foo + /bar --> /bar 438 * /foo + C:/bar --> C:/bar 439 * /foo + C:bar --> C:bar (*) 440 * /foo/a/ + ../bar --> foo/bar 441 * /foo/ + ../../bar --> null 442 * /foo/ + /bar --> /bar 443 * /foo/.. + /bar --> /bar 444 * /foo + bar/c.txt --> /foo/bar/c.txt 445 * /foo/c.txt + bar --> /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 --> "" --> relative 568 * \a\b\c.txt --> "\" --> current drive absolute 569 * C:a\b\c.txt --> "C:" --> drive relative 570 * C:\a\b\c.txt --> "C:\" --> absolute 571 * \\server\a\b\c.txt --> "\\server\" --> UNC 572 * \\\a\b\c.txt --> error, length = -1 573 * 574 * Unix: 575 * a/b/c.txt --> "" --> relative 576 * /a/b/c.txt --> "/" --> absolute 577 * ~/a/b/c.txt --> "~/" --> current user 578 * ~ --> "~/" --> current user (slash added) 579 * ~user/a/b/c.txt --> "~user/" --> named user 580 * ~user --> "~user/" --> named user (slash added) 581 * //server/a/b/c.txt --> "//server/" 582 * ///a/b/c.txt --> 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 --> "" --> relative 709 * \a\b\c.txt --> "\" --> current drive absolute 710 * C:a\b\c.txt --> "C:" --> drive relative 711 * C:\a\b\c.txt --> "C:\" --> absolute 712 * \\server\a\b\c.txt --> "\\server\" --> UNC 713 * 714 * Unix: 715 * a/b/c.txt --> "" --> relative 716 * /a/b/c.txt --> "/" --> absolute 717 * ~/a/b/c.txt --> "~/" --> current user 718 * ~ --> "~/" --> current user (slash added) 719 * ~user/a/b/c.txt --> "~user/" --> named user 720 * ~user --> "~user/" --> 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 --> a\b\ 755 * ~/a/b/c.txt --> a/b/ 756 * a.txt --> "" 757 * a/b/c --> a/b/ 758 * a/b/c/ --> 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 --> a\b 784 * ~/a/b/c.txt --> a/b 785 * a.txt --> "" 786 * a/b/c --> a/b 787 * a/b/c/ --> 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 --> C:\a\b\ 837 * ~/a/b/c.txt --> ~/a/b/ 838 * a.txt --> "" 839 * a/b/c --> a/b/ 840 * a/b/c/ --> a/b/c/ 841 * C: --> C: 842 * C:\ --> C:\ 843 * ~ --> ~/ 844 * ~/ --> ~/ 845 * ~user --> ~user/ 846 * ~user/ --> ~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 --> C:\a\b 868 * ~/a/b/c.txt --> ~/a/b 869 * a.txt --> "" 870 * a/b/c --> a/b 871 * a/b/c/ --> a/b/c 872 * C: --> C: 873 * C:\ --> C:\ 874 * ~ --> ~ 875 * ~/ --> ~ 876 * ~user --> ~user 877 * ~user/ --> ~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 --> c.txt 929 * a.txt --> a.txt 930 * a/b/c --> c 931 * a/b/c/ --> "" 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 --> c 974 * a.txt --> a 975 * a/b/c --> c 976 * a/b/c/ --> "" 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 --> "txt" 998 * a/b/c.jpg --> "jpg" 999 * a/b.txt/c --> "" 1000 * a/b/c --> "" 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 --> foo 1029 * a\b\c.jpg --> a\b\c 1030 * a\b\c --> a\b\c 1031 * a.b\c --> 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") --> true 1233 * wildcardMatch("c.txt", "*.jpg") --> false 1234 * wildcardMatch("a/b/c.txt", "a/b/*") --> true 1235 * wildcardMatch("c.txt", "*.???") --> true 1236 * wildcardMatch("c.txt", "*.????") --> 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") --> true 1260 * wildcardMatch("c.txt", "*.jpg") --> false 1261 * wildcardMatch("a/b/c.txt", "a/b/*") --> true 1262 * wildcardMatch("c.txt", "*.???") --> true 1263 * wildcardMatch("c.txt", "*.????") --> 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}