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