001package com.randomnoun.common; 002 003/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a 004 * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html) 005 */ 006 007import java.io.BufferedReader; 008import java.io.EOFException; 009import java.io.File; 010import java.io.FileInputStream; 011import java.io.FileNotFoundException; 012import java.io.FileOutputStream; 013import java.io.IOException; 014import java.io.InputStream; 015import java.io.InputStreamReader; 016import java.io.LineNumberReader; 017import java.security.MessageDigest; 018import java.security.NoSuchAlgorithmException; 019import java.text.SimpleDateFormat; 020import java.util.ArrayList; 021import java.util.Date; 022import java.util.List; 023import java.util.regex.Pattern; 024import java.util.zip.ZipEntry; 025import java.util.zip.ZipException; 026import java.util.zip.ZipInputStream; 027 028import org.apache.log4j.Logger; 029 030/** Find a resource recursively through all JARs, EARs, WARs, etc from 031 * the current directory down. 032 * 033 * <p>Command-line usage</p> 034 * 035 * <p>The following command-line arguments are recognised 036 * 037<table> 038<caption>Usage</caption> 039<tr><th> -h -? <td>displays this helptext 040<tr><th> -f <td>follow symlinks 041<tr><th> -a <td>show all resources found (i.e. do not use searchTerm) 042<tr><th> 043<tr><th colspan="2">Search criteria: 044<tr><th> -i <td>case-insensitive match 045<tr><th> -sc <td>if present, searchTerm matches within filename (default) 046<tr><th> -ss <td>if present, searchTerm matches start of filename 047<tr><th> -se <td>if present, searchTerm matches exact filename 048<tr><th> -sr <td>if present, searchTerm matches filename as a regular expression 049<tr><th> -mf n <td>max filesystem folder depth (0 = do not descend into subfolders) 050<tr><th> -ma n <td>max archive depth (0 = do not descend into archives) 051<tr><th> -x <td>if present, will attempt to recover if errors occur reading archives 052 (errors sent to stderr) 053<tr><th> 054<tr><th colspan="2">Action when resource found: 055<tr><th> -v <td>verbose; display filenames with file sizes and timestamps 056<tr><th> -vv <td>display MD5/SHA1 hashes of resources (NB: modifies display order) 057<tr><th> -d n <td>dump/display the contents of the n'th resource found 058<tr><th> -d all <td>dump the name and contents of all resources found 059<tr><th> -d names <td>dump just the names of all resources found (default) 060<tr><th> -d n1,n2...<td>dump the name and contents of the n1'th, n2'nd etc... resources found 061<tr><th> -dm n|all <td>as per -d, but performs manifest unmangling on resource (fixes linewraps) 062<tr><th> -dj n|all <td>as per -d, but performs class decompiling (requires jad to be in PATH) 063<tr><th> -c text <td>search for text in contents of resource (uses UTF-8 encoding) 064<tr><th> -ci text <td>case-insensitive search for text in contents of resources 065</table> 066 067 * 068 * <p><b>TODO</b> split CLI functionality into separate class 069 * <p><b>TODO</b> pass enough information to the callback classes to display somewhat sane progress bar 070 * <p><b>TODO</b> fix -dj switch + handle inner classes 071 * <p><b>TODO</b> rewrite jad to deal with annotations and other 1.5+ crap 072 * <p><b>TODO</b> -cs switches to change search behaviour within content 073 * 074 * @author knoxg 075 * 076 */ 077public class ResourceFinder { 078 079 /** Logger instance for this class */ 080 Logger logger = Logger.getLogger(ResourceFinder.class); 081 082 /** Match type used in {@link #matches(String)}} comparisons that tests whether 083 * the last component of a resource name contains a specified string; e.g. 084 * "abc/def.txt" will match against the searchTerm "ef" using this matchType. 085 */ 086 public final static int MATCHTYPE_CONTAINS = 0; 087 088 /** Match type used in {@link #matches(String)}} comparisons that tests whether 089 * the last component of a resource name starts with a specified string; e.g. 090 * "abc/def.txt" will match against the searchTerm "de" using this matchType. 091 */ 092 public final static int MATCHTYPE_STARTSWITH = 1; 093 094 /** Match type used in {@link #matches(String)}} comparisons that tests whether 095 * the last component of a resource name is equal to a specified regular expression; e.g. 096 * "abc/def.txt" will match against the searchTerm "def.txt" using this matchType. 097 */ 098 public final static int MATCHTYPE_EXACT = 2; 099 100 /** Match type used in {@link #matches(String)}} comparisons that tests whether 101 * the last component of a resource name matches a specified regular expression; e.g. 102 * "abc/def.txt" will match against the searchTerm ".*e.*" using this matchType. 103 */ 104 public final static int MATCHTYPE_REGEX = 3; 105 106 /** Resource being searched for */ 107 private String searchTerm; 108 109 /** If performing regex searches, the Pattern form of {@link #searchTerm} */ 110 private Pattern searchPattern; 111 112 /** File or directory from which search begins. If this is null, {@link #startInputStream} must be non-null, and vice versa */ 113 private File startDirectory; 114 115 /** ZipInputStream from which search begins. If this is null, {@link #startDirectory} must be non-null, and vice versa*/ 116 private ZipInputStream startInputStream; 117 118 /** A MATCHTYPE_* constant. */ 119 private int matchType; 120 121 /** If true, performs a case-insensitive match */ 122 private boolean ignoreCase = false; 123 124 /** If false, will prevent recursive search from following symbolic links */ 125 private boolean followSymlinks = false; 126 127 /** If true, will invoke the ResourceFinderCallback for every file in every archive iterated over 128 * (i.e. the {@link #searchTerm} will be ignored) */ 129 private boolean showAll = false; 130 131 /** If true, will attempt to recover processing after reading an invalid ZIP entry */ 132 private boolean ignoreErrors = false; 133 134 /** Maximum depth; -1 = no depth limit. See {@link #setMaxArchiveDepth(long)}. <i>(Not implemented)</i>*/ 135 private long maxArchiveDepth = -1; 136 137 /** Maximum folder depth; -1 = no depth limit. See {@link #setMaxFolderDepth(long)}. */ 138 private long maxFolderDepth = -1; 139 140 /** Current archive depth */ 141 private long currentArchiveDepth = -1; 142 143 /** Current folder depth */ 144 private long currentFolderDepth = -1; 145 146 /** Callback to be invoked on every resource that matches the search criteria */ 147 private ResourceFinderCallback callback; 148 149 /** If set to true, allows the search to be aborted whilst it is in progress */ 150 private transient boolean abort = false; 151 152 /** Regex to define which files will be opened via ZipInputStream. Will return true if the file ends with 153 * .zip, .sar, .jar, .war, .ear, .rar or .har. These are Java RARs (resource archives), not the other type 154 * of RAR. */ 155 private Pattern isArchivePattern = Pattern.compile( 156 ".*\\.([Zz][Ii][Pp]|" + 157 "[SsJjWwEeRrHh][Aa][Rr])$"); 158 159 /** Call this method within a {@link ResourceFinderCallback} to stop looking for resources */ 160 public void abort() { this.abort = true; } 161 162 /** Return the file or directory from which search begins. If this returns null, try {@link #getStartInputStream()} */ 163 public File getStartDirectory() { return startDirectory; } 164 165 /** Return the ZipInputStream from which search begins. If this returns null, try {@link #getStartDirectory()} */ 166 public InputStream getStartInputStream() { return startInputStream; } 167 168 /** Tests a resource name against the search criteria specified in this object 169 * 170 * @param resourceName the last component of a resource name 171 * 172 * @return true if the resource passes the search criteria, false otherwise 173 */ 174 public boolean matches(String resourceName) { 175 if (resourceName==null) { throw new NullPointerException("null string"); } 176 if (ignoreCase) { 177 switch(matchType) { 178 case MATCHTYPE_EXACT: return searchTerm.equalsIgnoreCase(resourceName); 179 case MATCHTYPE_REGEX: return searchPattern.matcher(resourceName).find(); 180 case MATCHTYPE_STARTSWITH: return resourceName.toUpperCase().startsWith(searchTerm); 181 case MATCHTYPE_CONTAINS: return resourceName.toUpperCase().contains(searchTerm); 182 default: throw new IllegalStateException("Illegal matchType '" + matchType + "'"); 183 } 184 } else { 185 switch(matchType) { 186 case MATCHTYPE_EXACT: return searchTerm.equals(resourceName); 187 case MATCHTYPE_REGEX: return searchPattern.matcher(resourceName).find(); 188 case MATCHTYPE_STARTSWITH: return resourceName.startsWith(searchTerm); 189 case MATCHTYPE_CONTAINS: return resourceName.contains(searchTerm); 190 default: throw new IllegalStateException("Illegal matchType '" + matchType + "'"); 191 } 192 } 193 } 194 195 196 /** An {@link java.io.InputStream} wrapper which updates an internal md5/sha1 digest 197 * as the stream is being read. 198 */ 199 public static class HashGeneratingInputStream extends InputStream { 200 InputStream is; 201 MessageDigest algorithm1, algorithm2; 202 203 public HashGeneratingInputStream(InputStream is) { 204 this.is = is; 205 try { 206 algorithm1 = MessageDigest.getInstance("MD5"); 207 algorithm2 = MessageDigest.getInstance("SHA-1"); 208 } catch (NoSuchAlgorithmException nsae) { 209 throw (IllegalStateException) new IllegalStateException( 210 "Invalid crypto config").initCause(nsae); 211 } 212 algorithm1.reset(); 213 algorithm2.reset(); 214 215 } 216 217 @Override 218 public int read() throws IOException { 219 int result = is.read(); 220 if (result != -1) { 221 algorithm1.update((byte) result); 222 algorithm2.update((byte) result); 223 } 224 return result; 225 } 226 public int available() throws IOException { 227 return is.available(); 228 } 229 public void close() { 230 // ignored; 231 } 232 public void mark(int readlimit) { 233 is.mark(readlimit); 234 } 235 public boolean markSupported() { 236 return is.markSupported(); 237 } 238 public int read(byte[] b) throws IOException { 239 int result = is.read(b); 240 if (result!=-1) { 241 algorithm1.update(b, 0, result); 242 algorithm2.update(b, 0, result); 243 } 244 return result; 245 } 246 public int read(byte[] b, int off, int len) throws IOException { 247 int result = is.read(b, off, len); 248 if (result!=-1) { 249 algorithm1.update(b, off, result); 250 algorithm2.update(b, off, result); 251 } 252 return result; 253 } 254 public void reset() throws IOException { 255 is.reset(); 256 } 257 public long skip(long n) throws IOException { 258 return is.skip(n); 259 } 260 /** Returns the MD5 digest of all input that has been read by this InputStream so far, 261 * in a hexadecimal String form */ 262 public String getMd5() { 263 byte messageDigest[] = algorithm1.digest(); 264 //System.err.println("md5 messageDigest is " + messageDigest.length + " bytes"); 265 StringBuffer hexString = new StringBuffer(); 266 for (int i=0;i<messageDigest.length;i++) { 267 hexString.append(Integer.toString( ( messageDigest[i] & 0xff ) + 0x100, 16).substring( 1 )); 268 } 269 return hexString.toString(); 270 } 271 /** Returns the SHA1 digest of all input that has been read by this InputStream so far, 272 * in a hexadecimal String form */ 273 public String getSha1() { 274 byte messageDigest[] = algorithm2.digest(); 275 //System.err.println("sha1 messageDigest is " + messageDigest.length + " bytes"); 276 StringBuffer hexString = new StringBuffer(); 277 for (int i=0;i<messageDigest.length;i++) { 278 hexString.append(Integer.toString( ( messageDigest[i] & 0xff ) + 0x100, 16).substring( 1 )); 279 } 280 return hexString.toString(); 281 } 282 } 283 284 285 public static class ResourceFinderCallbackResult { 286 boolean abort = false; 287 InputStream replaceInputStream = null; 288 } 289 290 /** A callback interface used when resources are found using this object 291 * 292 */ 293 public interface ResourceFinderCallback { 294 295 /** This method is only invoked for archive resources, before that archive has been read or 296 * recursed into. Both archives and standard files will be passed to the 297 * {@link #postProcess(String, long, long, InputStream, ResourceFinderCallbackResult)} method. 298 * 299 * @param resourceName full resource name 300 * @param filesize the size of the (uncompressed) resource, or -1 if this is unknown 301 * (some archives do not store this information) 302 * @param timestamp the timestamp of the resource 303 * @param inputStream an inputStream which can be used to retrieve the contents 304 * of the resource 305 * 306 * @return a ResourceFinderCallbackResult which can be used to abort the search or 307 * modify/wrap the inputStream being searched. 308 * 309 * @throws IOException 310 */ 311 public ResourceFinderCallbackResult preProcess(String resourceName, long filesize, long timestamp, 312 InputStream inputStream) throws IOException; 313 314 /** This method is invoked for each resource that matches the search criteria 315 * specified in the containing {@link ResourceFinder} class. This method 316 * is not responsible for closing the supplied inputStream. 317 * 318 * @param resourceName full resource name 319 * @param filesize the size of the (uncompressed) resource, or -1 if this is unknown 320 * (some archives do not store this information) 321 * @param timestamp the timestamp of the resource 322 * @param inputStream an inputStream which can be used to retrieve the contents 323 * of the resource 324 * 325 * @return a ResourceFinderCallbackResult which can be used to abort the search 326 * 327 * @throws IOException if an operation on the <code>inputStream</code> fails 328 */ 329 public ResourceFinderCallbackResult postProcess(String resourceName, long filesize, long timestamp, 330 InputStream inputStream, ResourceFinderCallbackResult preProcessResult) throws IOException; 331 332 } 333 334 /** Class which defines a callback which sends names and resource hashes to System.out. 335 */ 336 public static class HashingResourceFinderCallback implements ResourceFinderCallback { 337 338 int resourceIndex = 0; 339 boolean showHashes = false; 340 boolean ignoreErrors = false; 341 342 public HashingResourceFinderCallback(boolean ignoreErrors) { 343 this.ignoreErrors = ignoreErrors; 344 } 345 346 public void ignorableException(String message, Exception e) throws ZipException { 347 if (ignoreErrors) { 348 Logger.getLogger(HashingResourceFinderCallback.class).error(message, e); 349 } else { 350 throw (ZipException) new ZipException(message).initCause(e); 351 } 352 } 353 354 public ResourceFinderCallbackResult preProcess(String resourceName, long filesize, long timestamp, InputStream inputStream) throws IOException { 355 ResourceFinderCallbackResult rfcbr = new ResourceFinderCallbackResult(); 356 rfcbr.replaceInputStream = new HashGeneratingInputStream(inputStream); 357 return rfcbr; 358 } 359 360 public ResourceFinderCallbackResult postProcess(String resourceName, long filesize, long timestamp, InputStream inputStream, ResourceFinderCallbackResult preProcessResult) throws IOException { 361 ResourceFinderCallbackResult rfcbr = new ResourceFinderCallbackResult(); 362 try { 363 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 364 String md5, sha1; 365 if (inputStream instanceof HashGeneratingInputStream) { 366 HashGeneratingInputStream hgis = (HashGeneratingInputStream) inputStream; 367 // pump the rest of the bits through this stream 368 byte[] buffer = new byte[4096]; 369 try { 370 371 while (inputStream.read(buffer) != -1) { /* nothing */ } 372 } catch (EOFException eofe) { 373 ignorableException("Error reading zip resource '" + resourceName + "' for hash", eofe); 374 } catch (ZipException ze) { 375 // can trigger "java.util.zip.ZipException: invalid distance code" 376 ignorableException("Error reading zip resource '" + resourceName + "' for hash", ze); 377 } 378 379 md5 = hgis.getMd5(); 380 sha1 = hgis.getSha1(); 381 } else { 382 MessageDigest algorithm1, algorithm2; 383 try { 384 algorithm1 = MessageDigest.getInstance("MD5"); 385 algorithm2 = MessageDigest.getInstance("SHA1"); 386 } catch (NoSuchAlgorithmException nsae) { 387 throw (IllegalStateException) new IllegalStateException( 388 "Invalid crypto config").initCause(nsae); 389 } 390 algorithm1.reset(); 391 algorithm2.reset(); 392 byte[] buffer = new byte[4096]; 393 int bytesRead; 394 try { 395 while ((bytesRead = inputStream.read(buffer)) != -1) { 396 algorithm1.update(buffer, 0, bytesRead); 397 algorithm2.update(buffer, 0, bytesRead); 398 } 399 } catch (EOFException eofe) { 400 // can trigger "java.io.EOFException: Unexpected end of ZLIB input stream" errors 401 ignorableException("Error hashing zip resource '" + resourceName + "'", eofe); 402 } catch (ZipException ze) { 403 ignorableException("Error hashing zip resource '" + resourceName + "'", ze); 404 } 405 byte messageDigest1[] = algorithm1.digest(); 406 byte messageDigest2[] = algorithm2.digest(); 407 StringBuffer hexString1 = new StringBuffer(); 408 StringBuffer hexString2 = new StringBuffer(); 409 for (int i=0; i<messageDigest1.length; i++) { 410 hexString1.append(Integer.toString( ( messageDigest1[i] & 0xff ) + 0x100, 16).substring( 1 )); 411 } 412 for (int i=0; i<messageDigest2.length; i++) { 413 hexString2.append(Integer.toString( ( messageDigest2[i] & 0xff ) + 0x100, 16).substring( 1 )); 414 } 415 md5 = hexString1.toString(); 416 sha1 = hexString2.toString(); 417 } 418 System.out.println("[" + resourceIndex + "] " + resourceName + " " + (filesize==-1 ? "(unknown)" : String.valueOf(filesize)) + 419 " " + sdf.format(new Date(timestamp)) + " " + md5 + " " + sha1 ); 420 } catch (IllegalArgumentException iae) { 421 // not sure if this is needed any more 422 throw new IOException("IllegalArgumentException processing ZipInputStream", iae); 423 } 424 resourceIndex++; 425 return rfcbr; 426 } 427 } 428 429 430 /** Class which defines a callback which sends names and resources to System.out. 431 * 432 * <p>This class uses '#' as a separator between the filesystem and files contained 433 * within archives; e.g. test.jar#abc.txt refers to abc.txt in test.jar. 434 * 435 * <p>For comparison, Tangosol seems to use '!', includes a leading slash and 436 * includes a protocol-like identifier at the beginning 437 * (e.g. jar:file:test.jar!/abc.txt). If a constructor is supplied which only provides 438 * a ZipInputStream (i.e. no filename is available), then resources will be returned 439 * starting with the '#' character. 440 * 441 */ 442 public static class DisplayResourceFinderCallback implements ResourceFinderCallback { 443 444 public final static int DUMP_NAMES = -1; 445 public final static int DUMP_NAMES_AND_RESOURCES = -2; 446 public final static int DUMP_RESOURCES = 0; 447 448 int dumpType = 0; 449 List<Integer> dumpResourceNumbers; 450 int maxDumpResourceNumber = -1; 451 int resourceIndex = 0; 452 boolean verbose = false; 453 boolean manifests = false; 454 boolean decompile = false; 455 String searchContents = null; 456 boolean searchContentsIgnoreCase = false; 457 458 // with all the trimmings 459 public DisplayResourceFinderCallback(int dumpType, List<Integer> dumpResourceNumbers, boolean verbose, boolean manifests, boolean decompile, String searchContents, boolean searchContentsIgnoreCase) { 460 // System.out.println("2 searchContents is " + searchContents); 461 this.dumpResourceNumbers = dumpResourceNumbers; 462 this.dumpType = dumpType; 463 this.verbose = verbose; 464 this.manifests = manifests; 465 this.decompile = decompile; 466 this.searchContents = searchContents; 467 this.searchContentsIgnoreCase = searchContentsIgnoreCase; 468 if (dumpResourceNumbers != null) { 469 for (Integer drn : dumpResourceNumbers) { 470 maxDumpResourceNumber = Math.max(maxDumpResourceNumber, drn.intValue()); 471 } 472 } 473 } 474 475 private ResourceFinderCallbackResult dump(String resourceName, long filesize, long timestamp, InputStream inputStream) throws IOException { 476 ResourceFinderCallbackResult rfcr = new ResourceFinderCallbackResult(); 477 if ((dumpType == DUMP_NAMES_AND_RESOURCES || dumpType == DUMP_NAMES) && 478 (dumpResourceNumbers==null || dumpResourceNumbers.contains(new Integer(resourceIndex))) 479 ) 480 { 481 if (verbose) { 482 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 483 System.out.println("[" + resourceIndex + "] " + resourceName + " " + (filesize==-1 ? "(unknown)" : String.valueOf(filesize)) + " " + sdf.format(new Date(timestamp))); 484 } else if (searchContents == null) { 485 System.out.println("[" + resourceIndex + "] " + resourceName); 486 } else if (searchContents != null) { 487 int pos = -1; 488 // @TODO this assumes we never search for strings containing newlines 489 LineNumberReader lnr = new LineNumberReader(new InputStreamReader(inputStream)); 490 if (searchContentsIgnoreCase) { 491 searchContents = searchContents.toLowerCase(); 492 String line = lnr.readLine(); 493 if (line!=null) { 494 pos = line.toString().toLowerCase().indexOf(searchContents.toLowerCase()); 495 while (line!=null && pos==-1) { 496 line = lnr.readLine(); 497 pos = line==null ? -1 : line.toString().toLowerCase().indexOf(searchContents.toLowerCase()); 498 } 499 } 500 } else { 501 String line = lnr.readLine(); 502 if (line!=null) { 503 pos = line.toString().indexOf(searchContents); 504 while (line!=null && pos==-1) { 505 line = lnr.readLine(); 506 pos = line==null ? -1 : line.toString().indexOf(searchContents); 507 } 508 } 509 } 510 if (pos!=-1) { 511 System.out.println("[" + resourceIndex + "] [line " + lnr.getLineNumber() + ", col " + pos + "] " + resourceName); 512 } 513 } 514 } 515 if ((dumpType == DUMP_NAMES_AND_RESOURCES || dumpType == DUMP_RESOURCES) && 516 (dumpResourceNumbers==null || dumpResourceNumbers.contains(new Integer(resourceIndex))) 517 ) { 518 // InputStream is = ResourceFinder.getResourceStream(resourceName); 519 if (manifests) { 520 BufferedReader br = new BufferedReader(new InputStreamReader(inputStream)); 521 String line = br.readLine(); 522 while (line != null) { 523 int len = line.length(); 524 while (line.length() > 0 && line.charAt(0) == ' ') { 525 line = line.substring(1); 526 } 527 System.out.print(line); 528 if (len != 70) { System.out.println(); } 529 line = br.readLine(); 530 } 531 } else if (decompile) { 532 // hopefully this is deleted when the VM exits 533 // @TODO: we need to grab all inner classes for this class as well 534 File tmpFile = File.createTempFile("resourceFinder", ".class"); 535 FileOutputStream fos = new FileOutputStream(tmpFile); 536 StreamUtil.copyStream(inputStream, fos, 1024); 537 fos.close(); 538 try { 539 ProcessUtil processUtil = new ProcessUtil(); 540 String result = processUtil.exec(new String[] { "jad", "-lnc", "-p", tmpFile.getCanonicalPath() }); 541 System.out.println(result); 542 } catch (ProcessUtil.ProcessException pe) { 543 throw (IOException) new IOException("Problem executing jad").initCause(pe); 544 } 545 546 } else { 547 StreamUtil.copyStream(inputStream, System.out, 1024); 548 } 549 if (resourceIndex == maxDumpResourceNumber) { 550 // don't bother continuing this search if we're found the last resource being searched for 551 rfcr.abort = true; 552 } 553 554 // we don't insert an additional newline when dumping the contents of just one file 555 // so that stdout redirection still does something useful 556 if (dumpType == DUMP_NAMES_AND_RESOURCES && 557 (dumpResourceNumbers==null || dumpResourceNumbers.size()>1)) { 558 System.out.println(); 559 } 560 } 561 resourceIndex++; 562 return rfcr; 563 } 564 565 public ResourceFinderCallbackResult preProcess(String resourceName, long filesize, long timestamp, InputStream inputStream) throws IOException { 566 return dump(resourceName, filesize, timestamp, inputStream); 567 } 568 569 // won't be needing this one 570 public ResourceFinderCallbackResult postProcess(String resourceName, long filesize, long timestamp, InputStream inputStream, ResourceFinderCallbackResult preProcessResult) throws IOException { 571 if (preProcessResult==null) { return dump(resourceName, filesize, timestamp, inputStream); } 572 return null; 573 } 574 575 576 } 577 578 /** Creates a new resource finder object 579 * 580 * @param searchTerm resource being searched for 581 * @param matchType a MATCHTYPE_* constant denoting how the searchTerm is to be used to match against resource names 582 * @param ignoreCase if true, will perform a case insensitive search 583 * @param startDirectory directory from which search begins 584 * @param callback callback to be invoked on every resource that matches the search criteria 585 * 586 * @throws IOException if the start directory is invalid 587 */ 588 public ResourceFinder(String searchTerm, int matchType, boolean ignoreCase, File startDirectory, ResourceFinderCallback callback) throws IOException { 589 init(searchTerm, matchType, ignoreCase, callback); 590 this.startDirectory = startDirectory.getCanonicalFile(); // for symlink test 591 592 } 593 594 /** Common code to both the File and ZipInputStream constructors */ 595 private void init(String searchTerm, int matchType, boolean ignoreCase, 596 ResourceFinderCallback callback) 597 { 598 this.matchType = matchType; 599 this.ignoreCase = ignoreCase; 600 this.showAll = false; 601 this.ignoreErrors = false; 602 this.callback = callback; 603 604 // @TODO clean this up a bit 605 if (ignoreCase) { 606 switch (matchType) { 607 case MATCHTYPE_EXACT: break; 608 case MATCHTYPE_REGEX: searchPattern = Pattern.compile(searchTerm, Pattern.CASE_INSENSITIVE); break; 609 case MATCHTYPE_STARTSWITH: searchTerm = searchTerm.toUpperCase(); break; 610 case MATCHTYPE_CONTAINS: searchTerm = searchTerm.toUpperCase(); break; 611 default: throw new IllegalStateException("Illegal matchType '" + matchType + "'"); 612 } 613 } else { 614 switch (matchType) { 615 case MATCHTYPE_EXACT: break; 616 case MATCHTYPE_REGEX: searchPattern = Pattern.compile(searchTerm); break; 617 case MATCHTYPE_STARTSWITH: break; 618 case MATCHTYPE_CONTAINS: break; 619 default: throw new IllegalStateException("Illegal matchType '" + matchType + "'"); 620 } 621 } 622 this.searchTerm = searchTerm; 623 624 } 625 626 /** Sets whether to follow symbolic links during filesystem scans. By default symlinks will not be followed. 627 * 628 * @see #getFollowSymlinks() 629 * 630 * @param followSymlinks true if symbolic links should be followed during filesystem scans, false otherwise 631 */ 632 public void setFollowSymLinks(boolean followSymlinks) { 633 this.followSymlinks = followSymlinks; 634 } 635 636 /** Returns whether symbolic links will be followed during filesystem scans 637 * 638 * @see #setFollowSymLinks(boolean) 639 * 640 * @return whether symbolic links will be followed during filesystem scans 641 */ 642 public boolean getFollowSymlinks() { 643 return followSymlinks; 644 } 645 646 /** Sets whether the ResourceFinderCallback should be invoked for every file in every archive iterated over 647 * (i.e. to ignore the {@link #searchTerm} ). By default this flag is set to false. 648 * 649 * @see #getShowAll() 650 * 651 * @param showAll if true, will invoke the ResourceFinderCallback for every file in every archive iterated over 652 */ 653 public void setShowAll(boolean showAll) { 654 this.showAll = showAll; 655 } 656 657 /** Returns whether the ResourceFinderCallback will be invoked for every file in every archive iterated over 658 * 659 * @see #setShowAll(boolean) 660 * 661 * @return true if the ResourceFinderCallback will be invoked for every file in every archive iterated over, false otherwise 662 */ 663 public boolean getShowAll() { 664 return showAll; 665 } 666 667 /** Sets whether to ignore (some) exceptions encountered whilst processing ZipInputStreams. 668 * 669 * <p>Only EOFExceptions, ZipExceptions, IllegalArgumentExceptions and the push-back buffer 670 * IOException will be ignored if this flag is set. Ignored exceptions will still be logged. 671 * 672 * <p>By default, this flag is set to false. 673 * 674 * @see #getIgnoreErrors() 675 * 676 * @param ignoreErrors true to ignore exceptions as described above, false otherwise 677 */ 678 public void setIgnoreErrors(boolean ignoreErrors) { 679 this.ignoreErrors = ignoreErrors; 680 } 681 682 /** Returns true if exceptions will be ignored whilst processing ZipInputStreams 683 * 684 * @see #setIgnoreErrors(boolean) 685 * 686 * @return true if exceptions will be ignored whilst processing ZipInputStreams 687 */ 688 public boolean getIgnoreErrors() { 689 return ignoreErrors; 690 } 691 692 693 /** Creates a new resource finder object 694 * 695 * @param searchTerm resource being searched for 696 * @param matchType a MATCHTYPE_* constant denoting how the searchTerm is to be used to match against resource names 697 * @param ignoreCase if true, will perform a case insensitive search 698 * @param startInputStream stream from which search begins 699 * @param callback callback to be invoked on every resource that matches the search criteria 700 * 701 * @throws IOException if the start directory is invalid 702 */ 703 public ResourceFinder(String searchTerm, int matchType, boolean ignoreCase, ZipInputStream startInputStream, ResourceFinderCallback callback) throws IOException { 704 init(searchTerm, matchType, ignoreCase, callback); 705 this.startInputStream = startInputStream; 706 } 707 708 /** Searches and returns a list of resources matching the criteria defined 709 * in the constructor 710 * 711 * <p><b>TODO</b> the list returned by this object probably isn't accurate. 712 * 713 * @throws IOException 714 */ 715 public void find() throws IOException { 716 this.currentArchiveDepth = -1; // yet to enumerate initial folder / stream 717 if (startInputStream != null) { 718 //List<String> result = new ArrayList<String>(); 719 findResourceInZip(startInputStream, "#"); 720 return; 721 722 } else if (startDirectory.isFile()) { 723 //List<String> result = new ArrayList<String>(); 724 File file = startDirectory; 725 String name = file.getName(); 726 if (!followSymlinks && isLink(file )) { 727 // ignore symlinks 728 // System.out.println("shazbot"); 729 } else if (matches(name)) { 730 FileInputStream fis = new FileInputStream(file); 731 callback.postProcess(name, file.length(), file.lastModified(), fis, null); 732 fis.close(); 733 // perhaps make this another switch 734 /* 735 if (isArchive(name)) { 736 ZipInputStream zis = new ZipInputStream(new FileInputStream(file)); 737 result.addAll(findResourceInZip(zis, name + "#")); 738 } 739 */ 740 } else if (isArchive(name)) { 741 if (showAll) { 742 FileInputStream fis = new FileInputStream(file); 743 callback.postProcess(name, file.length(), file.lastModified(), fis, null); 744 fis.close(); 745 } 746 ZipInputStream zis = new ZipInputStream(new FileInputStream(file)); 747 zis.close(); 748 } else { 749 if (showAll) { 750 FileInputStream fis = new FileInputStream(file); 751 callback.postProcess(name, file.length(), file.lastModified(), fis, null); 752 fis.close(); 753 } 754 } 755 return; 756 } else { 757 findResourceInFolder(startDirectory, ""); 758 } 759 } 760 761 /** Returns true if the filename will be treated as an archive 762 * 763 * @param name a filename 764 * 765 * @return true if the file is an archive, false otherwise 766 */ 767 public boolean isArchive(String name) { 768 return isArchivePattern.matcher(name).matches(); 769 } 770 771 /** Determines whether a file is a symbolic link. 772 * (Copied from http://www.idiom.com/~zilla/Xfiles/javasymlinks.html) 773 * 774 * @param file file to test 775 * 776 * @return true if the file is a symbolic link, false otherwise 777 */ 778 public static boolean isLink(File file) throws IOException { 779 try { 780 if (!file.exists()) { 781 return true; 782 } else { 783 String cnnpath = file.getCanonicalPath(); 784 String abspath = file.getAbsolutePath(); 785 return !abspath.equals(cnnpath); 786 } 787 } catch(IOException ex) { 788 //System.err.println(ex); 789 return true; 790 } 791 } 792 793 /** Returns a list of resources within the supplied folder, subfolders, 794 * and archives contained within these folders 795 * 796 * @param folder the folder to search from 797 * @param prefix a prefix which is included in any results returned by this 798 * method 799 * 800 * @throws IOException 801 */ 802 public void findResourceInFolder(File folder, String prefix) throws IOException { 803 if (maxArchiveDepth!=-1 && currentArchiveDepth>=maxArchiveDepth) { 804 return; 805 } 806 // System.err.println("findResourceInFolder(" + prefix + "):" + currentArchiveDepth); 807 currentArchiveDepth++; 808 809 File[] folderContents = folder.listFiles(); 810 if (folderContents != null) { 811 for (File file : folderContents) { 812 813 // don't think any of these are going to work if we're calculating hashes as well. 814 // maybe it will. who knows. 815 816 String name = file.getName(); 817 // System.out.println("Filetest '" + name + "' against '" + resourceName + "' (fs=" + followSymlinks + ")"); 818 if (!followSymlinks && isLink(file)) { 819 // ignore symlinks 820 // System.out.println("shazbot"); 821 } else if (file.isDirectory() && (maxFolderDepth==-1 || currentFolderDepth+1 <= maxFolderDepth)) { 822 currentFolderDepth++; 823 findResourceInFolder(file, prefix + name + "/"); 824 currentFolderDepth--; 825 826 } else if (matches(name)) { 827 FileInputStream fis = new FileInputStream(file); 828 callback.postProcess(prefix + name, file.length(), file.lastModified(), fis, null); 829 fis.close(); 830 // perhaps make this another switch 831 if (isArchive(name)) { 832 fis = new FileInputStream(file); 833 ZipInputStream zis = new ZipInputStream(fis); 834 findResourceInZip(zis, prefix + name + "#"); 835 fis.close(); 836 } 837 } else if (isArchive(name)) { 838 if (showAll) { 839 FileInputStream fis = new FileInputStream(file); 840 callback.postProcess(prefix + name, file.length(), file.lastModified(), fis, null); 841 fis.close(); 842 } 843 FileInputStream fis = new FileInputStream(file); 844 ZipInputStream zis = new ZipInputStream(fis); 845 findResourceInZip(zis, prefix + name + "#"); 846 fis.close(); 847 } else { 848 if (showAll) { 849 FileInputStream fis = new FileInputStream(file); 850 callback.postProcess(prefix + name, file.length(), file.lastModified(), fis, null); 851 fis.close(); 852 } 853 } 854 if (abort) { break; } 855 } 856 } 857 858 currentArchiveDepth--; 859 } 860 861 /** If ignoreErrors is true, send a message to stderr with the 862 * exception message, otherwise throw an encapsulated ZipException 863 * 864 * @param message message describing exception 865 * @param e cause of the exception 866 */ 867 public void ignorableException(String message, Exception e) throws ZipException { 868 if (ignoreErrors) { 869 logger.error(message, e); 870 } else { 871 throw (ZipException) new ZipException(message).initCause(e); 872 } 873 } 874 875 /** Returns a list of resources within the supplied archive, 876 * and archives contained within this archive 877 * 878 * @param zipInputStream the archive to search 879 * @param prefix a prefix which is included in any results returned by this 880 * method. By convention, this prefix should end with a '#' to separate it 881 * from resources found within the resource. 882 * 883 * @throws IOException 884 */ 885 886 public void findResourceInZip(ZipInputStream zipInputStream, String prefix) throws IOException { 887 if (maxArchiveDepth!=-1 && currentArchiveDepth>=maxArchiveDepth) { 888 return; 889 } 890 // System.err.println("findResourceInZip(" + prefix + "):" + currentArchiveDepth); 891 currentArchiveDepth++; 892 893 // System.out.println("Searching in " + prefix); 894 ZipEntry zipEntry = null; 895 try { 896 zipEntry = zipInputStream.getNextEntry(); 897 } catch (EOFException oefe) { 898 ignorableException("Error retrieving first entry in zip '" + prefix.substring(0, prefix.length()-1) + "'", oefe); 899 currentArchiveDepth--; 900 return; 901 } catch (ZipException ze) { 902 ignorableException("Error retrieving first entry in zip '" + prefix.substring(0, prefix.length()-1) + "'", ze); 903 currentArchiveDepth--; 904 return; 905 } catch (IllegalArgumentException iae) { 906 // can occur in ZipInputStream.getUTF8String 907 ignorableException("Error retrieving first entry in zip '" + prefix.substring(0, prefix.length()-1) + "'", iae); 908 currentArchiveDepth--; 909 return; 910 } 911 while (zipEntry != null) { 912 String name = zipEntry.getName(); 913 String shortName = name; 914 // zipEntry.isDirectory(); // write these at the end ? skip them altogether ? 915 916 // on unix, it's possible to get directory entries (trailing '/'s) within ZIPs; on windows this doesn't seem to happen 917 while (shortName.endsWith("/")) { shortName = shortName.substring(0, shortName.length() - 1); } 918 while (shortName.endsWith("\\")) { shortName = shortName.substring(0, shortName.length() - 1); } 919 if (shortName.indexOf('/')!=-1) { shortName = shortName.substring(shortName.lastIndexOf('/') + 1); } 920 if (shortName.indexOf('\\')!=-1) { shortName = shortName.substring(shortName.lastIndexOf('\\') + 1); } 921 922 // may need to do case-sensitive match 923 /* commenting this out temporarily 924 925 if (showVersions && name.equalsIgnoreCase("META-INF/MANIFEST.MF")) { 926 // treat this as a property file. Which is wrong, because it's got insane line breaks 927 // but good enough for retrieving version data 928 929 Properties props = new Properties(); 930 props.load(zipInputStream); 931 if (props.getProperty("Specification-Version")!=null) { 932 // there's also an Implementation-Version, but this appears to be the same 933 // maven2 doesn't write these entries. perhaps. 934 // @TODO something 935 } 936 } 937 */ 938 939 InputStream inputStreamToProcess = zipInputStream; 940 941 // might just be easier to add .reset() to ZipInputStream 942 ResourceFinderCallbackResult rfcbResult = null; 943 if (isArchive(name)) { 944 try { 945 if (matches(shortName) || showAll) { 946 rfcbResult = callback.preProcess(prefix + name, zipEntry.getSize(), zipEntry.getTime(), inputStreamToProcess); 947 if (rfcbResult!=null && rfcbResult.replaceInputStream!=null) { 948 inputStreamToProcess = rfcbResult.replaceInputStream; 949 } 950 if (rfcbResult!=null && rfcbResult.abort) { 951 this.abort = true; break; 952 } 953 } 954 ZipInputStream zis = new ZipInputStream(inputStreamToProcess); 955 findResourceInZip(zis, prefix + name + "#"); 956 zipInputStream.closeEntry(); 957 } catch (EOFException eofe) { 958 // can trigger "java.io.EOFException: Unexpected end of ZLIB input stream" errors 959 ignorableException("Error reading zip '" + prefix + name + "'", eofe); 960 } catch (ZipException ze) { 961 ignorableException("Error reading zip '" + prefix + name + "'", ze); 962 } 963 } 964 if (matches(shortName) || showAll) { 965 rfcbResult = callback.postProcess(prefix + name, zipEntry.getSize(), zipEntry.getTime(), inputStreamToProcess, rfcbResult); 966 if (rfcbResult!=null && rfcbResult.abort) { 967 this.abort = true; break; 968 } 969 } 970 971 try { 972 zipEntry = zipInputStream.getNextEntry(); 973 } catch (EOFException oefe) { 974 ignorableException("Error retrieving next entry in zip '" + prefix.substring(0, prefix.length()-1) + "'; after '"+ name + "'", oefe); 975 break; 976 } catch (ZipException ze) { 977 ignorableException("Error retrieving next entry in zip '" + prefix.substring(0, prefix.length()-1) + "'; after '"+ name + "'", ze); 978 break; 979 } catch (IllegalArgumentException iae) { 980 // can occur in ZipInputStream.getUTF8String 981 ignorableException("Error retrieving next entry in zip '" + prefix.substring(0, prefix.length()-1) + "'; after '"+ name + "'", iae); 982 break; 983 } catch (IOException ioe) { 984 // may occur after dodgy CRCs: 985 // invalid entry CRC (expected 0xab633fa2 but got 0xc30a2df7) 986 // Exception in thread "main" java.io.IOException: Push back buffer is full 987 if (ioe.getMessage().contains("Push back buffer")) { 988 ignorableException("Error retrieving next entry in zip '" + prefix.substring(0, prefix.length()-1) + "'; after '"+ name + "'", ioe); 989 break; 990 } else { 991 currentArchiveDepth--; 992 throw ioe; 993 } 994 } 995 if (abort) { break ; } 996 } 997 currentArchiveDepth--; 998 } 999 1000 /** Returns a resource as an inputstream 1001 * 1002 * @param resourceName a resource name, as defined by the class javadoc 1003 * 1004 * @return the resource as an InputStream 1005 * 1006 * @throws FileNotFoundException the resource could not be found 1007 * @throws IOException the resource could not be read 1008 */ 1009 public static InputStream getResourceStream(String resourceName) throws IOException { 1010 int pos = resourceName.indexOf("#"); 1011 if (pos == -1) { 1012 return new FileInputStream(resourceName); 1013 } else { 1014 String filename = resourceName.substring(0, pos); 1015 String component = resourceName.substring(pos + 1); 1016 ZipInputStream zis = new ZipInputStream(new FileInputStream(filename)); 1017 return getResourceComponent(zis, component, resourceName); 1018 } 1019 } 1020 1021 /** Private method to recursively search within an archive for a file 1022 * 1023 * @param zipInputStream input stream to search 1024 * @param component resource name fragment, separated by '#' characters 1025 * @param fullResource full resource name (only used in exception messages) 1026 * 1027 * @return the input stream 1028 * 1029 * @throws IOException the input stream cannot be read 1030 */ 1031 public static InputStream getResourceComponent(ZipInputStream zipInputStream, String component, String fullResource) throws IOException { 1032 int pos = component.indexOf("#"); 1033 String filename = component; 1034 String subComponent = null; 1035 if (pos != -1) { 1036 filename = component.substring(0, pos); 1037 subComponent = component.substring(pos + 1); 1038 } 1039 ZipEntry zipEntry = zipInputStream.getNextEntry(); 1040 while (zipEntry != null) { 1041 if (zipEntry.getName().equals(filename)) { 1042 if (subComponent == null) { 1043 return zipInputStream; 1044 } else { 1045 return getResourceComponent(new ZipInputStream(zipInputStream), subComponent, fullResource); 1046 } 1047 } 1048 zipEntry = zipInputStream.getNextEntry(); 1049 } 1050 throw new FileNotFoundException("Could not find component '" + filename + "' in '" + fullResource + "'"); 1051 } 1052 1053 /** Sets the maximum number of times I'm going to recursively enter a 1054 * JAR/EAR/WAR/whatever. 1055 * 1056 * <ul> 1057 * <li>0 = none; i.e. will just perform a directory scan. 1058 * <li>1..n = will search up to n directories/archives deep 1059 * <li>-1 = infinite; i.e. will not perform depth checking 1060 * </ul> 1061 * 1062 * @param maxArchiveDepth maximum depth (-1=no limit, 0=will not recursive into JARs/WARs etc..) 1063 */ 1064 public void setMaxArchiveDepth(long maxArchiveDepth) { 1065 this.maxArchiveDepth = maxArchiveDepth; 1066 } 1067 1068 /** Sets the maximum folder depth to descend into the filesystem structure. 1069 * 1070 * <p>This will not limit folder depth within archives, only folder depth within the filesystem 1071 * 1072 * <p>This setting has no effect if using the InputStream constructor. 1073 * 1074 * <ul> 1075 * <li>0 = none; i.e. will just scan within the top-level folder. 1076 * <li>1..n = will search up to n folders deep 1077 * <li>-1 = infinite; i.e. will not perform folder depth checking 1078 * </ul> 1079 * 1080 * @param maxFolderDepth maximum depth (-1=no limit, 0=will not recurse into folders) 1081 */ 1082 public void setMaxFolderDepth(long maxFolderDepth) { 1083 this.maxFolderDepth = maxFolderDepth; 1084 } 1085 1086 1087 public static String usage() { 1088 return 1089 "Usage: \n" + 1090 " java " + ResourceFinder.class.getName() + " [options] searchTerm\n" + 1091 "or\n" + 1092 " java " + ResourceFinder.class.getName() + " [options] -a\n" + 1093 "where [options] are:\n" + 1094 " -h -? displays this helptext\n" + 1095 " -f follow symlinks\n" + 1096 " -a show all resources found (i.e. do not use searchTerm)\n" + 1097 "\n" + 1098 "Search criteria:\n" + 1099 " -i case-insensitive match\n" + 1100 " -sc if present, searchTerm matches within filename (default)\n" + 1101 " -ss if present, searchTerm matches start of filename\n" + 1102 " -se if present, searchTerm matches exact filename\n" + 1103 " -sr if present, searchTerm matches filename as a regular expression\n" + 1104 " -mf n max filesystem folder depth (0 = do not descend into subfolders)\n" + 1105 " -ma n max archive depth (0 = do not descend into archives)\n" + 1106 " -x if present, will attempt to recover if errors occur reading archives\n"+ 1107 " (errors sent to stderr)\n" + 1108 "\n" + 1109 "Action when resource found:\n" + 1110 " -v verbose; display filenames with file sizes and timestamps\n" + 1111 " -vv display MD5/SHA1 hashes of resources (NB: modifies display order)\n" + 1112 " -d n dump/display the contents of the n'th resource found\n" + 1113 " -d all dump the name and contents of all resources found\n" + 1114 " -d names dump just the names of all resources found (default)\n" + 1115 " -d n1,n2... dump the name and contents of the n1'th, n2'nd etc... resources found\n" + 1116 " -dm n|all as per -d, but performs manifest unmangling on resource (fixes linewraps)\n" + 1117 " -dj n|all as per -d, but performs class decompiling (requires jad to be in PATH)\n" + 1118 " -c text search for text in contents of resource (uses UTF-8 encoding)\n" + 1119 " -ci text case-insensitive search for text in contents of resources\n" + 1120 "\n" + 1121 "* A maximum of one -d switch should be present\n" + 1122 "* The -d and -c switches are mutually exclusive\n"; 1123 } 1124 1125 /** Command-line interface to this class 1126 * 1127 * @param args arguments 1128 * 1129 * @throws IOException 1130 */ 1131 public static void main(String args[]) throws IOException { 1132 String searchTerm; 1133 String searchContents = null; 1134 String dumpResource = ""; 1135 int dumpType = DisplayResourceFinderCallback.DUMP_NAMES; 1136 List<Integer> dumpResourceList = null; 1137 int argIndex = 0; 1138 int matchType = MATCHTYPE_CONTAINS; 1139 long maxArchiveDepth = -1; 1140 long maxFolderDepth = -1; 1141 boolean followSymlinks = false; 1142 boolean verbose = false; 1143 boolean showHashes = false; 1144 boolean showAll = false; 1145 boolean manifests = false; 1146 boolean decompile = false; 1147 boolean ignoreCase = false; 1148 boolean ignoreErrors = false; 1149 boolean searchContentsIgnoreCase = false; 1150 1151 if (args.length < 1) { 1152 System.out.println(usage()); 1153 throw new IllegalArgumentException("Expected resource search term or options"); 1154 } 1155 while (argIndex < args.length && args[argIndex].startsWith("-")) { 1156 if (args[argIndex].startsWith("-d")) { 1157 if (args[argIndex].equals("-dm")) { manifests = true; } 1158 if (args[argIndex].equals("-dj")) { decompile = true; } 1159 1160 dumpResource = args[argIndex + 1]; 1161 if (dumpResource.equals("all")) { 1162 dumpType = DisplayResourceFinderCallback.DUMP_NAMES_AND_RESOURCES; 1163 } else if (dumpResource.equals("names")) { 1164 dumpType = DisplayResourceFinderCallback.DUMP_NAMES; 1165 } else { 1166 dumpResourceList = new ArrayList<Integer>(); 1167 String[] resources = dumpResource.split(","); 1168 for (String resource : resources) { 1169 try { 1170 dumpResourceList.add(new Integer(resource)); 1171 } catch (NumberFormatException nfe) { 1172 // @TODO if it's not a number, then could use it as a resource id 1173 throw new IllegalArgumentException("Expected numeric resource id (found '" + dumpResource + "')"); 1174 } 1175 } 1176 if (dumpResourceList.size() == 1) { 1177 dumpType = DisplayResourceFinderCallback.DUMP_RESOURCES; 1178 } else { 1179 dumpType = DisplayResourceFinderCallback.DUMP_NAMES_AND_RESOURCES; 1180 } 1181 } 1182 // 1.6 method args = Arrays.copyOfRange(args, 2, args.length); 1183 argIndex += 2; 1184 1185 } else if (args[argIndex].equals("-mf")) { 1186 maxFolderDepth = Long.parseLong(args[argIndex + 1]); 1187 argIndex += 2; 1188 } else if (args[argIndex].equals("-ma")) { 1189 maxArchiveDepth = Long.parseLong(args[argIndex + 1]); 1190 argIndex += 2; 1191 } else if (args[argIndex].equals("-v")) { 1192 verbose = true; 1193 argIndex ++; 1194 } else if (args[argIndex].equals("-vv")) { 1195 verbose = true; 1196 showHashes = true; 1197 argIndex ++; 1198 } else if (args[argIndex].equals("-f")) { 1199 followSymlinks = true; 1200 argIndex ++; 1201 } else if (args[argIndex].equals("-a")) { 1202 showAll = true; 1203 argIndex ++; 1204 } else if (args[argIndex].equals("-sc")) { 1205 matchType = MATCHTYPE_CONTAINS; 1206 argIndex ++; 1207 } else if (args[argIndex].equals("-ss")) { 1208 matchType = MATCHTYPE_STARTSWITH; 1209 argIndex ++; 1210 } else if (args[argIndex].equals("-sr")) { 1211 matchType = MATCHTYPE_REGEX; 1212 argIndex ++; 1213 } else if (args[argIndex].equals("-se")) { 1214 matchType = MATCHTYPE_EXACT; 1215 argIndex ++; 1216 } else if (args[argIndex].equals("-c")) { 1217 searchContents = args[argIndex + 1]; 1218 // System.out.println("1 searchContents is " + searchContents); 1219 argIndex += 2; 1220 } else if (args[argIndex].equals("-ci")) { 1221 searchContents = args[argIndex + 1]; 1222 searchContentsIgnoreCase = true; 1223 // System.out.println("1 searchContents is " + searchContents); 1224 argIndex += 2; 1225 } else if (args[argIndex].equals("-i")) { 1226 ignoreCase = true; 1227 argIndex ++; 1228 } else if (args[argIndex].equals("-x")) { 1229 ignoreErrors = true; 1230 argIndex ++; 1231 } else if (args[argIndex].equals("-h") || args[argIndex].equals("-?")) { 1232 System.out.println(usage()); 1233 System.exit(0); 1234 } else { 1235 System.out.println(usage()); 1236 throw new IllegalArgumentException("Unknown switch '" + args[argIndex] + "' supplied"); 1237 } 1238 } 1239 1240 if (showAll) { 1241 searchTerm = "maguffin"; 1242 } else { 1243 if (args.length < argIndex + 1) { 1244 System.out.println(usage()); 1245 throw new IllegalArgumentException("Expected resource search term"); 1246 } 1247 searchTerm = args[argIndex++]; 1248 } 1249 1250 ResourceFinderCallback callback; 1251 if (showHashes) { 1252 callback = new HashingResourceFinderCallback(ignoreErrors); 1253 } else { 1254 callback = new DisplayResourceFinderCallback(dumpType, dumpResourceList, verbose, manifests, decompile, 1255 searchContents, searchContentsIgnoreCase); 1256 } 1257 1258 ResourceFinder resourceFinder = new ResourceFinder(searchTerm, matchType, ignoreCase, new File("."), callback); 1259 resourceFinder.setFollowSymLinks(followSymlinks); 1260 resourceFinder.setShowAll(showAll); 1261 resourceFinder.setIgnoreErrors(ignoreErrors); 1262 if (maxArchiveDepth != -1) { resourceFinder.setMaxArchiveDepth(maxArchiveDepth); } 1263 if (maxFolderDepth != -1) { resourceFinder.setMaxFolderDepth(maxFolderDepth); } 1264 1265 resourceFinder.find(); 1266 1267 1268 } 1269} 1270