
com.randomnoun.common.ResourceFinder Maven / Gradle / Ivy
package com.randomnoun.common;
/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
* BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
*/
import java.io.BufferedReader;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;
import org.apache.log4j.Logger;
/** Find a resource recursively through all JARs, EARs, WARs, etc from
* the current directory down.
*
* Command-line usage
*
* The following command-line arguments are recognised
*
-h -? displays this helptext
-f follow symlinks
-a show all resources found (i.e. do not use searchTerm)
Search criteria:
-i case-insensitive match
-sc if present, searchTerm matches within filename (default)
-ss if present, searchTerm matches start of filename
-se if present, searchTerm matches exact filename
-sr if present, searchTerm matches filename as a regular expression
-mf n max filesystem folder depth (0 = do not descend into subfolders)
-ma n max archive depth (0 = do not descend into archives)
-x if present, will attempt to recover if errors occur reading archives
(errors sent to stderr)
Action when resource found:
-v verbose; display filenames with file sizes and timestamps
-vv display MD5/SHA1 hashes of resources (NB: modifies display order)
-d n dump/display the contents of the n'th resource found
-d all dump the name and contents of all resources found
-d names dump just the names of all resources found (default)
-d n1,n2... dump the name and contents of the n1'th, n2'nd etc... resources found
-dm n|all as per -d, but performs manifest unmangling on resource (fixes linewraps)
-dj n|all as per -d, but performs class decompiling (requires jad to be in PATH)
-c text search for text in contents of resource (uses UTF-8 encoding)
-ci text case-insensitive search for text in contents of resources
*
* @TODO split CLI functionality into separate class
* @TODO pass enough information to the callback classes to display somewhat sane progress bar
* @TODO fix -dj switch + handle inner classes
* @TODO rewrite jad to deal with annotations and other 1.5+ crap
* @TODO -cs switches to change search behaviour within content
*
* @author knoxg
* @version $Id: ResourceFinder.java,v 1.2 2015-01-11 21:04:40 knoxg Exp $
*/
public class ResourceFinder {
/** CVS revision identifier */
public static final String _revision = "$Id: ResourceFinder.java,v 1.2 2015-01-11 21:04:40 knoxg Exp $";
/** Logger instance for this class */
Logger logger = Logger.getLogger(ResourceFinder.class);
/** Match type used in {@link #matches(String)}} comparisons that tests whether
* the last component of a resource name contains a specified string; e.g.
* "abc/def.txt" will match against the searchTerm "ef" using this matchType.
*/
public final static int MATCHTYPE_CONTAINS = 0;
/** Match type used in {@link #matches(String)}} comparisons that tests whether
* the last component of a resource name starts with a specified string; e.g.
* "abc/def.txt" will match against the searchTerm "de" using this matchType.
*/
public final static int MATCHTYPE_STARTSWITH = 1;
/** Match type used in {@link #matches(String)}} comparisons that tests whether
* the last component of a resource name is equal to a specified regular expression; e.g.
* "abc/def.txt" will match against the searchTerm "def.txt" using this matchType.
*/
public final static int MATCHTYPE_EXACT = 2;
/** Match type used in {@link #matches(String)}} comparisons that tests whether
* the last component of a resource name matches a specified regular expression; e.g.
* "abc/def.txt" will match against the searchTerm ".*e.*" using this matchType.
*/
public final static int MATCHTYPE_REGEX = 3;
/** Resource being searched for */
private String searchTerm;
/** If performing regex searches, the Pattern form of {@link #searchTerm} */
private Pattern searchPattern;
/** File or directory from which search begins. If this is null, {@link #startInputStream} must be non-null, and vice versa */
private File startDirectory;
/** ZipInputStream from which search begins. If this is null, {@link #startDirectory} must be non-null, and vice versa*/
private ZipInputStream startInputStream;
/** A MATCHTYPE_* constant. */
private int matchType;
/** If true, performs a case-insensitive match */
private boolean ignoreCase = false;
/** If false, will prevent recursive search from following symbolic links */
private boolean followSymlinks = false;
/** If true, will invoke the ResourceFinderCallback for every file in every archive iterated over
* (i.e. the {@link #searchTerm} will be ignored) */
private boolean showAll = false;
/** If true, will attempt to recover processing after reading an invalid ZIP entry */
private boolean ignoreErrors = false;
/** Maximum depth; -1 = no depth limit. See {@link #setMaxArchiveDepth(long)}. (Not implemented)*/
private long maxArchiveDepth = -1;
/** Maximum folder depth; -1 = no depth limit. See {@link #setMaxFolderDepth(long)}. */
private long maxFolderDepth = -1;
/** Current archive depth */
private long currentArchiveDepth = -1;
/** Current folder depth */
private long currentFolderDepth = -1;
/** Callback to be invoked on every resource that matches the search criteria */
private ResourceFinderCallback callback;
/** If set to true, allows the search to be aborted whilst it is in progress */
private transient boolean abort = false;
/** Regex to define which files will be opened via ZipInputStream. Will return true if the file ends with
* .zip, .sar, .jar, .war, .ear, .rar or .har. These are Java RARs (resource archives), not the other type
* of RAR. */
private Pattern isArchivePattern = Pattern.compile(
".*\\.([Zz][Ii][Pp]|" +
"[SsJjWwEeRrHh][Aa][Rr])$");
/** Call this method within a {@link ResourceFinderCallback} to stop looking for resources */
public void abort() { this.abort = true; }
/** Return the file or directory from which search begins. If this returns null, try {@link #getStartInputStream()} */
public File getStartDirectory() { return startDirectory; }
/** Return the ZipInputStream from which search begins. If this returns null, try {@link #getStartDirectory()} */
public InputStream getStartInputStream() { return startInputStream; }
/** Tests a resource name against the search criteria specified in this object
*
* @param resourceName the last component of a resource name
*
* @return true if the resource passes the search criteria, false otherwise
*/
public boolean matches(String resourceName) {
if (resourceName==null) { throw new NullPointerException("null string"); }
if (ignoreCase) {
switch(matchType) {
case MATCHTYPE_EXACT: return searchTerm.equalsIgnoreCase(resourceName);
case MATCHTYPE_REGEX: return searchPattern.matcher(resourceName).find();
case MATCHTYPE_STARTSWITH: return resourceName.toUpperCase().startsWith(searchTerm);
case MATCHTYPE_CONTAINS: return resourceName.toUpperCase().contains(searchTerm);
default: throw new IllegalStateException("Illegal matchType '" + matchType + "'");
}
} else {
switch(matchType) {
case MATCHTYPE_EXACT: return searchTerm.equals(resourceName);
case MATCHTYPE_REGEX: return searchPattern.matcher(resourceName).find();
case MATCHTYPE_STARTSWITH: return resourceName.startsWith(searchTerm);
case MATCHTYPE_CONTAINS: return resourceName.contains(searchTerm);
default: throw new IllegalStateException("Illegal matchType '" + matchType + "'");
}
}
}
/** An {@link java.io.InputStream} wrapper which updates an internal md5/sha1 digest
* as the stream is being read.
*/
public static class HashGeneratingInputStream extends InputStream {
InputStream is;
MessageDigest algorithm1, algorithm2;
public HashGeneratingInputStream(InputStream is) {
this.is = is;
try {
algorithm1 = MessageDigest.getInstance("MD5");
algorithm2 = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException nsae) {
throw (IllegalStateException) new IllegalStateException(
"Invalid crypto config").initCause(nsae);
}
algorithm1.reset();
algorithm2.reset();
}
@Override
public int read() throws IOException {
int result = is.read();
if (result != -1) {
algorithm1.update((byte) result);
algorithm2.update((byte) result);
}
return result;
}
public int available() throws IOException {
return is.available();
}
public void close() {
// ignored;
}
public void mark(int readlimit) {
is.mark(readlimit);
}
public boolean markSupported() {
return is.markSupported();
}
public int read(byte[] b) throws IOException {
int result = is.read(b);
if (result!=-1) {
algorithm1.update(b, 0, result);
algorithm2.update(b, 0, result);
}
return result;
}
public int read(byte[] b, int off, int len) throws IOException {
int result = is.read(b, off, len);
if (result!=-1) {
algorithm1.update(b, off, result);
algorithm2.update(b, off, result);
}
return result;
}
public void reset() throws IOException {
is.reset();
}
public long skip(long n) throws IOException {
return is.skip(n);
}
/** Returns the MD5 digest of all input that has been read by this InputStream so far,
* in a hexadecimal String form */
public String getMd5() {
byte messageDigest[] = algorithm1.digest();
//System.err.println("md5 messageDigest is " + messageDigest.length + " bytes");
StringBuffer hexString = new StringBuffer();
for (int i=0;iinputStream fails
*/
public ResourceFinderCallbackResult postProcess(String resourceName, long filesize, long timestamp,
InputStream inputStream, ResourceFinderCallbackResult preProcessResult) throws IOException;
}
/** Class which defines a callback which sends names and resource hashes to System.out.
*/
public static class HashingResourceFinderCallback implements ResourceFinderCallback {
int resourceIndex = 0;
boolean showHashes = false;
public HashingResourceFinderCallback() {
}
public ResourceFinderCallbackResult preProcess(String resourceName, long filesize, long timestamp, InputStream inputStream) throws IOException {
ResourceFinderCallbackResult rfcbr = new ResourceFinderCallbackResult();
rfcbr.replaceInputStream = new HashGeneratingInputStream(inputStream);
return rfcbr;
}
public ResourceFinderCallbackResult postProcess(String resourceName, long filesize, long timestamp, InputStream inputStream, ResourceFinderCallbackResult preProcessResult) throws IOException {
ResourceFinderCallbackResult rfcbr = new ResourceFinderCallbackResult();
try {
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
String md5, sha1;
if (inputStream instanceof HashGeneratingInputStream) {
HashGeneratingInputStream hgis = (HashGeneratingInputStream) inputStream;
// pump the rest of the bits through this stream
byte[] buffer = new byte[4096];
while (inputStream.read(buffer) != -1) { /* nothing */ }
md5 = hgis.getMd5();
sha1 = hgis.getSha1();
} else {
MessageDigest algorithm1, algorithm2;
try {
algorithm1 = MessageDigest.getInstance("MD5");
algorithm2 = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException nsae) {
throw (IllegalStateException) new IllegalStateException(
"Invalid crypto config").initCause(nsae);
}
algorithm1.reset();
algorithm2.reset();
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
algorithm1.update(buffer, 0, bytesRead);
algorithm2.update(buffer, 0, bytesRead);
}
byte messageDigest1[] = algorithm1.digest();
byte messageDigest2[] = algorithm2.digest();
StringBuffer hexString1 = new StringBuffer();
StringBuffer hexString2 = new StringBuffer();
for (int i=0; iThis class uses '#' as a separator between the filesystem and files contained
* within archives; e.g. test.jar#abc.txt refers to abc.txt in test.jar.
*
* For comparison, Tangosol seems to use '!', includes a leading slash and
* includes a protocol-like identifier at the beginning
* (e.g. jar:file:test.jar!/abc.txt). If a constructor is supplied which only provides
* a ZipInputStream (i.e. no filename is available), then resources will be returned
* starting with the '#' character.
*
*/
public static class DisplayResourceFinderCallback implements ResourceFinderCallback {
public final static int DUMP_NAMES = -1;
public final static int DUMP_NAMES_AND_RESOURCES = -2;
public final static int DUMP_RESOURCES = 0;
int dumpType = 0;
List dumpResourceNumbers;
int maxDumpResourceNumber = -1;
int resourceIndex = 0;
boolean verbose = false;
boolean manifests = false;
boolean decompile = false;
String searchContents = null;
boolean searchContentsIgnoreCase = false;
// with all the trimmings
public DisplayResourceFinderCallback(int dumpType, List dumpResourceNumbers, boolean verbose, boolean manifests, boolean decompile, String searchContents, boolean searchContentsIgnoreCase) {
// System.out.println("2 searchContents is " + searchContents);
this.dumpResourceNumbers = dumpResourceNumbers;
this.dumpType = dumpType;
this.verbose = verbose;
this.manifests = manifests;
this.decompile = decompile;
this.searchContents = searchContents;
this.searchContentsIgnoreCase = searchContentsIgnoreCase;
if (dumpResourceNumbers != null) {
for (Integer drn : dumpResourceNumbers) {
maxDumpResourceNumber = Math.max(maxDumpResourceNumber, drn.intValue());
}
}
}
private ResourceFinderCallbackResult dump(String resourceName, long filesize, long timestamp, InputStream inputStream) throws IOException {
ResourceFinderCallbackResult rfcr = new ResourceFinderCallbackResult();
if ((dumpType == DUMP_NAMES_AND_RESOURCES || dumpType == DUMP_NAMES) &&
(dumpResourceNumbers==null || dumpResourceNumbers.contains(new Integer(resourceIndex)))
)
{
if (verbose) {
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
System.out.println("[" + resourceIndex + "] " + resourceName + " " + (filesize==-1 ? "(unknown)" : String.valueOf(filesize)) + " " + sdf.format(new Date(timestamp)));
} else if (searchContents == null) {
System.out.println("[" + resourceIndex + "] " + resourceName);
} else if (searchContents != null) {
int pos = -1;
// @TODO this assumes we never search for strings containing newlines
LineNumberReader lnr = new LineNumberReader(new InputStreamReader(inputStream));
if (searchContentsIgnoreCase) {
searchContents = searchContents.toLowerCase();
String line = lnr.readLine();
if (line!=null) {
pos = line.toString().toLowerCase().indexOf(searchContents.toLowerCase());
while (line!=null && pos==-1) {
line = lnr.readLine();
pos = line==null ? -1 : line.toString().toLowerCase().indexOf(searchContents.toLowerCase());
}
}
} else {
String line = lnr.readLine();
if (line!=null) {
pos = line.toString().indexOf(searchContents);
while (line!=null && pos==-1) {
line = lnr.readLine();
pos = line==null ? -1 : line.toString().indexOf(searchContents);
}
}
}
if (pos!=-1) {
System.out.print("[" + resourceIndex + "] [line " + lnr.getLineNumber() + ", col " + pos + "] " + resourceName);
}
}
}
if ((dumpType == DUMP_NAMES_AND_RESOURCES || dumpType == DUMP_RESOURCES) &&
(dumpResourceNumbers==null || dumpResourceNumbers.contains(new Integer(resourceIndex)))
) {
// InputStream is = ResourceFinder.getResourceStream(resourceName);
if (manifests) {
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String line = br.readLine();
while (line != null) {
int len = line.length();
while (line.length() > 0 && line.charAt(0) == ' ') {
line = line.substring(1);
}
System.out.print(line);
if (len != 70) { System.out.println(); }
line = br.readLine();
}
} else if (decompile) {
// hopefully this is deleted when the VM exits
// @TODO: we need to grab all inner classes for this class as well
throw new UnsupportedOperationException("Decompilation is disabled until I bring 'ProcessUtil' into this package. jad hasn't worked in years, anyway.");
/*
File tmpFile = File.createTempFile("resourceFinder", ".class");
FileOutputStream fos = new FileOutputStream(tmpFile);
StreamUtil.copyStream(inputStream, fos, 1024);
fos.close();
try {
String result = ProcessUtil.exec(new String[] { "jad", "-lnc", "-p", tmpFile.getCanonicalPath() });
System.out.println(result);
} catch (ProcessUtil.ProcessException pe) {
throw (IOException) new IOException("Problem executing jad").initCause(pe);
}
*/
} else {
StreamUtil.copyStream(inputStream, System.out, 1024);
}
if (resourceIndex == maxDumpResourceNumber) {
// don't bother continuing this search if we're found the last resource being searched for
rfcr.abort = true;
}
// we don't insert an additional newline when dumping the contents of just one file
// so that stdout redirection still does something useful
if (dumpType == DUMP_NAMES_AND_RESOURCES &&
(dumpResourceNumbers==null || dumpResourceNumbers.size()>1)) {
System.out.println();
}
}
resourceIndex++;
return rfcr;
}
public ResourceFinderCallbackResult preProcess(String resourceName, long filesize, long timestamp, InputStream inputStream) throws IOException {
return dump(resourceName, filesize, timestamp, inputStream);
}
// won't be needing this one
public ResourceFinderCallbackResult postProcess(String resourceName, long filesize, long timestamp, InputStream inputStream, ResourceFinderCallbackResult preProcessResult) throws IOException {
if (preProcessResult==null) { return dump(resourceName, filesize, timestamp, inputStream); }
return null;
}
}
/** Creates a new resource finder object
*
* @param searchTerm resource being searched for
* @param matchType a MATCHTYPE_* constant denoting how the searchTerm is to be used to match against resource names
* @param ignoreCase if true, will perform a case insensitive search
* @param startDirectory directory from which search begins
* @param callback callback to be invoked on every resource that matches the search criteria
*
* @throws IOException if the start directory is invalid
*/
public ResourceFinder(String searchTerm, int matchType, boolean ignoreCase, File startDirectory, ResourceFinderCallback callback) throws IOException {
init(searchTerm, matchType, ignoreCase, callback);
this.startDirectory = startDirectory.getCanonicalFile(); // for symlink test
}
/** Common code to both the File and ZipInputStream constructors */
private void init(String searchTerm, int matchType, boolean ignoreCase,
ResourceFinderCallback callback)
{
this.matchType = matchType;
this.ignoreCase = ignoreCase;
this.showAll = false;
this.ignoreErrors = false;
this.callback = callback;
// @TODO clean this up a bit
if (ignoreCase) {
switch (matchType) {
case MATCHTYPE_EXACT: break;
case MATCHTYPE_REGEX: searchPattern = Pattern.compile(searchTerm, Pattern.CASE_INSENSITIVE); break;
case MATCHTYPE_STARTSWITH: searchTerm = searchTerm.toUpperCase(); break;
case MATCHTYPE_CONTAINS: searchTerm = searchTerm.toUpperCase(); break;
default: throw new IllegalStateException("Illegal matchType '" + matchType + "'");
}
} else {
switch (matchType) {
case MATCHTYPE_EXACT: break;
case MATCHTYPE_REGEX: searchPattern = Pattern.compile(searchTerm); break;
case MATCHTYPE_STARTSWITH: break;
case MATCHTYPE_CONTAINS: break;
default: throw new IllegalStateException("Illegal matchType '" + matchType + "'");
}
}
this.searchTerm = searchTerm;
}
/** Sets whether to follow symbolic links during filesystem scans. By default symlinks will not be followed.
*
* @see #getFollowSymlinks()
*
* @param followSymlinks true if symbolic links should be followed during filesystem scans, false otherwise
*/
public void setFollowSymLinks(boolean followSymlinks) {
this.followSymlinks = followSymlinks;
}
/** Returns whether symbolic links will be followed during filesystem scans
*
* @see #setFollowSymLinks(boolean)
*
* @return whether symbolic links will be followed during filesystem scans
*/
public boolean getFollowSymlinks() {
return followSymlinks;
}
/** Sets whether the ResourceFinderCallback should be invoked for every file in every archive iterated over
* (i.e. to ignore the {@link #searchTerm} ). By default this flag is set to false.
*
* @see #getShowAll()
*
* @param showAll if true, will invoke the ResourceFinderCallback for every file in every archive iterated over
*/
public void setShowAll(boolean showAll) {
this.showAll = showAll;
}
/** Returns whether the ResourceFinderCallback will be invoked for every file in every archive iterated over
*
* @see #setShowAll(boolean)
*
* @return true if the ResourceFinderCallback will be invoked for every file in every archive iterated over, false otherwise
*/
public boolean getShowAll() {
return showAll;
}
/** Sets whether to ignore (some) exceptions encountered whilst processing ZipInputStreams.
*
* Only EOFExceptions, ZipExceptions, IllegalArgumentExceptions and the push-back buffer
* IOException will be ignored if this flag is set. Ignored exceptions will still be logged.
*
*
By default, this flag is set to false.
*
* @see #getIgnoreErrors()
*
* @param ignoreErrors true to ignore exceptions as described above, false otherwise
*/
public void setIgnoreErrors(boolean ignoreErrors) {
this.ignoreErrors = ignoreErrors;
}
/** Returns true if exceptions will be ignored whilst processing ZipInputStreams
*
* @see #setIgnoreErrors(boolean)
*
* @return true if exceptions will be ignored whilst processing ZipInputStreams
*/
public boolean getIgnoreErrors() {
return ignoreErrors;
}
/** Creates a new resource finder object
*
* @param searchTerm resource being searched for
* @param matchType a MATCHTYPE_* constant denoting how the searchTerm is to be used to match against resource names
* @param ignoreCase if true, will perform a case insensitive search
* @param startDirectory directory from which search begins
* @param callback callback to be invoked on every resource that matches the search criteria
*
* @throws IOException if the start directory is invalid
*/
public ResourceFinder(String searchTerm, int matchType, boolean ignoreCase, ZipInputStream startInputStream, ResourceFinderCallback callback) throws IOException {
init(searchTerm, matchType, ignoreCase, callback);
this.startInputStream = startInputStream;
}
/** Searches and returns a list of resources matching the criteria defined
* in the constructor
*
* @TODO the list returned by this object probably isn't accurate.
*
* @return a List of Strings, in the syntax defined by the class javadoc
*
* @throws IOException
*/
public void find() throws IOException {
this.currentArchiveDepth = -1; // yet to enumerate initial folder / stream
if (startInputStream != null) {
//List result = new ArrayList();
findResourceInZip(startInputStream, "#");
return;
} else if (startDirectory.isFile()) {
//List result = new ArrayList();
File file = startDirectory;
String name = file.getName();
if (!followSymlinks && isLink(file )) {
// ignore symlinks
// System.out.println("shazbot");
} else if (matches(name)) {
FileInputStream fis = new FileInputStream(file);
callback.postProcess(name, file.length(), file.lastModified(), fis, null);
fis.close();
// perhaps make this another switch
/*
if (isArchive(name)) {
ZipInputStream zis = new ZipInputStream(new FileInputStream(file));
result.addAll(findResourceInZip(zis, name + "#"));
}
*/
} else if (isArchive(name)) {
if (showAll) {
FileInputStream fis = new FileInputStream(file);
callback.postProcess(name, file.length(), file.lastModified(), fis, null);
fis.close();
}
ZipInputStream zis = new ZipInputStream(new FileInputStream(file));
zis.close();
} else {
if (showAll) {
FileInputStream fis = new FileInputStream(file);
callback.postProcess(name, file.length(), file.lastModified(), fis, null);
fis.close();
}
}
return;
} else {
findResourceInFolder(startDirectory, "");
}
}
/** Returns true if the filename will be treated as an archive
*
* @param name a filename
*
* @return true if the file is an archive, false otherwise
*/
public boolean isArchive(String name) {
return isArchivePattern.matcher(name).matches();
}
/** Determines whether a file is a symbolic link.
* (Copied from http://www.idiom.com/~zilla/Xfiles/javasymlinks.html)
*
* @param file file to test
*
* @return true if the file is a symbolic link, false otherwise
*/
public static boolean isLink(File file) throws IOException {
try {
if (!file.exists()) {
return true;
} else {
String cnnpath = file.getCanonicalPath();
String abspath = file.getAbsolutePath();
return !abspath.equals(cnnpath);
}
} catch(IOException ex) {
//System.err.println(ex);
return true;
}
}
/** Returns a list of resources within the supplied folder, subfolders,
* and archives contained within these folders
*
* @param folder the folder to search from
* @param prefix a prefix which is included in any results returned by this
* method
*
* @return a List of Strings, in the syntax defined in the class javadoc
*
* @throws IOException
*/
public void findResourceInFolder(File folder, String prefix) throws IOException {
if (maxArchiveDepth!=-1 && currentArchiveDepth>=maxArchiveDepth) {
return;
}
// System.err.println("findResourceInFolder(" + prefix + "):" + currentArchiveDepth);
currentArchiveDepth++;
File[] folderContents = folder.listFiles();
if (folderContents != null) {
for (File file : folderContents) {
// don't think any of these are going to work if we're calculating hashes as well.
// maybe it will. who knows.
String name = file.getName();
// System.out.println("Filetest '" + name + "' against '" + resourceName + "' (fs=" + followSymlinks + ")");
if (!followSymlinks && isLink(file)) {
// ignore symlinks
// System.out.println("shazbot");
} else if (file.isDirectory() && (maxFolderDepth==-1 || currentFolderDepth+1 <= maxFolderDepth)) {
currentFolderDepth++;
findResourceInFolder(file, prefix + name + "/");
currentFolderDepth--;
} else if (matches(name)) {
FileInputStream fis = new FileInputStream(file);
callback.postProcess(prefix + name, file.length(), file.lastModified(), fis, null);
fis.close();
// perhaps make this another switch
if (isArchive(name)) {
fis = new FileInputStream(file);
ZipInputStream zis = new ZipInputStream(fis);
findResourceInZip(zis, prefix + name + "#");
fis.close();
}
} else if (isArchive(name)) {
if (showAll) {
FileInputStream fis = new FileInputStream(file);
callback.postProcess(prefix + name, file.length(), file.lastModified(), fis, null);
fis.close();
}
FileInputStream fis = new FileInputStream(file);
ZipInputStream zis = new ZipInputStream(fis);
findResourceInZip(zis, prefix + name + "#");
fis.close();
} else {
if (showAll) {
FileInputStream fis = new FileInputStream(file);
callback.postProcess(prefix + name, file.length(), file.lastModified(), fis, null);
fis.close();
}
}
if (abort) { break; }
}
}
currentArchiveDepth--;
}
/** If ignoreErrors is true, send a message to stderr with the
* exception message, otherwise throw an encapsulated ZipException
*
* @param message message describing exception
* @param e cause of the exception
*/
public void ignorableException(String message, Exception e) throws ZipException {
if (ignoreErrors) {
logger.error(message, e);
} else {
throw (ZipException) new ZipException(message).initCause(e);
}
}
/** Returns a list of resources within the supplied archive,
* and archives contained within this archive
*
* @param zipInputStream the archive to search
* @param prefix a prefix which is included in any results returned by this
* method. By convention, this prefix should end with a '#' to separate it
* from resources found within the resource.
*
* @return a List of Strings, in the syntax defined in the class javadoc
*
* @throws IOException
*/
public void findResourceInZip(ZipInputStream zipInputStream, String prefix) throws IOException {
if (maxArchiveDepth!=-1 && currentArchiveDepth>=maxArchiveDepth) {
return;
}
// System.err.println("findResourceInZip(" + prefix + "):" + currentArchiveDepth);
currentArchiveDepth++;
// System.out.println("Searching in " + prefix);
ZipEntry zipEntry = null;
try {
zipEntry = zipInputStream.getNextEntry();
} catch (EOFException oefe) {
ignorableException("Error retrieving first entry in zip '" + prefix.substring(0, prefix.length()-1) + "'", oefe);
currentArchiveDepth--;
return;
} catch (ZipException ze) {
ignorableException("Error retrieving first entry in zip '" + prefix.substring(0, prefix.length()-1) + "'", ze);
currentArchiveDepth--;
return;
} catch (IllegalArgumentException iae) {
// can occur in ZipInputStream.getUTF8String
ignorableException("Error retrieving first entry in zip '" + prefix.substring(0, prefix.length()-1) + "'", iae);
currentArchiveDepth--;
return;
}
while (zipEntry != null) {
String name = zipEntry.getName();
String shortName = name;
// on unix, it's possible to get directory entries (trailing '/'s) within ZIPs; on windows this doesn't seem to happen
while (shortName.endsWith("/")) { shortName = shortName.substring(0, shortName.length() - 1); }
while (shortName.endsWith("\\")) { shortName = shortName.substring(0, shortName.length() - 1); }
if (shortName.indexOf('/')!=-1) { shortName = shortName.substring(shortName.lastIndexOf('/') + 1); }
if (shortName.indexOf('\\')!=-1) { shortName = shortName.substring(shortName.lastIndexOf('\\') + 1); }
// may need to do case-sensitive match
/* commenting this out temporarily
if (showVersions && name.equalsIgnoreCase("META-INF/MANIFEST.MF")) {
// treat this as a property file. Which is wrong, because it's got insane line breaks
// but good enough for retrieving version data
Properties props = new Properties();
props.load(zipInputStream);
if (props.getProperty("Specification-Version")!=null) {
// there's also an Implementation-Version, but this appears to be the same
// maven2 doesn't write these entries. perhaps.
// @TODO something
}
}
*/
InputStream inputStreamToProcess = zipInputStream;
// might just be easier to add .reset() to ZipInputStream
ResourceFinderCallbackResult rfcbResult = null;
if (isArchive(name)) {
try {
if (matches(shortName) || showAll) {
rfcbResult = callback.preProcess(prefix + name, zipEntry.getSize(), zipEntry.getTime(), inputStreamToProcess);
if (rfcbResult!=null && rfcbResult.replaceInputStream!=null) {
inputStreamToProcess = rfcbResult.replaceInputStream;
}
if (rfcbResult!=null && rfcbResult.abort) {
this.abort = true; break;
}
}
ZipInputStream zis = new ZipInputStream(inputStreamToProcess);
findResourceInZip(zis, prefix + name + "#");
zipInputStream.closeEntry();
} catch (EOFException eofe) {
// can trigger "java.io.EOFException: Unexpected end of ZLIB input stream" errors
ignorableException("Error reading zip '" + prefix + name + "'", eofe);
} catch (ZipException ze) {
ignorableException("Error reading zip '" + prefix + name + "'", ze);
}
}
if (matches(shortName) || showAll) {
rfcbResult = callback.postProcess(prefix + name, zipEntry.getSize(), zipEntry.getTime(), inputStreamToProcess, rfcbResult);
if (rfcbResult!=null && rfcbResult.abort) {
this.abort = true; break;
}
}
try {
zipEntry = zipInputStream.getNextEntry();
} catch (EOFException oefe) {
ignorableException("Error retrieving next entry in zip '" + prefix.substring(0, prefix.length()-1) + "'; after '"+ name + "'", oefe);
break;
} catch (ZipException ze) {
ignorableException("Error retrieving next entry in zip '" + prefix.substring(0, prefix.length()-1) + "'; after '"+ name + "'", ze);
break;
} catch (IllegalArgumentException iae) {
// can occur in ZipInputStream.getUTF8String
ignorableException("Error retrieving next entry in zip '" + prefix.substring(0, prefix.length()-1) + "'; after '"+ name + "'", iae);
break;
} catch (IOException ioe) {
// may occur after dodgy CRCs:
// invalid entry CRC (expected 0xab633fa2 but got 0xc30a2df7)
// Exception in thread "main" java.io.IOException: Push back buffer is full
if (ioe.getMessage().contains("Push back buffer")) {
ignorableException("Error retrieving next entry in zip '" + prefix.substring(0, prefix.length()-1) + "'; after '"+ name + "'", ioe);
break;
} else {
currentArchiveDepth--;
throw ioe;
}
}
if (abort) { break ; }
}
currentArchiveDepth--;
}
/** Returns a resource as an inputstream
*
* @param resourceName a resource name, as defined by the class javadoc
*
* @return the resource as an InputStream
*
* @throws FileNotFoundException the resource could not be found
* @throws IOException the resource could not be read
*/
public static InputStream getResourceStream(String resourceName) throws IOException {
int pos = resourceName.indexOf("#");
if (pos == -1) {
return new FileInputStream(resourceName);
} else {
String filename = resourceName.substring(0, pos);
String component = resourceName.substring(pos + 1);
ZipInputStream zis = new ZipInputStream(new FileInputStream(filename));
return getResourceComponent(zis, component, resourceName);
}
}
/** Private method to recursively search within an archive for a file
*
* @param zipInputStream input stream to search
* @param component resource name fragment, separated by '#' characters
* @param fullResource full resource name (only used in exception messages)
*
* @return the input stream
*
* @throws IOException the input stream cannot be read
*/
public static InputStream getResourceComponent(ZipInputStream zipInputStream, String component, String fullResource) throws IOException {
int pos = component.indexOf("#");
String filename = component;
String subComponent = null;
if (pos != -1) {
filename = component.substring(0, pos);
subComponent = component.substring(pos + 1);
}
ZipEntry zipEntry = zipInputStream.getNextEntry();
while (zipEntry != null) {
if (zipEntry.getName().equals(filename)) {
if (subComponent == null) {
return zipInputStream;
} else {
return getResourceComponent(new ZipInputStream(zipInputStream), subComponent, fullResource);
}
}
zipEntry = zipInputStream.getNextEntry();
}
throw new FileNotFoundException("Could not find component '" + filename + "' in '" + fullResource + "'");
}
/** Sets the maximum number of times I'm going to recursively enter a
* JAR/EAR/WAR/whatever.
*
*
* - 0 = none; i.e. will just perform a directory scan.
*
- 1..n = will search up to n directories/archives deep
*
- -1 = infinite; i.e. will not perform depth checking
*
*
* @param depth maximum depth (-1=no limit, 0=will not recursive into JARs/WARs etc..)
*/
public void setMaxArchiveDepth(long maxArchiveDepth) {
this.maxArchiveDepth = maxArchiveDepth;
}
/** Sets the maximum folder depth to descend into the filesystem structure.
*
* This will not limit folder depth within archives, only folder depth within the filesystem
*
*
This setting has no effect if using the InputStream constructor.
*
*
* - 0 = none; i.e. will just scan within the top-level folder.
*
- 1..n = will search up to n folders deep
*
- -1 = infinite; i.e. will not perform folder depth checking
*
*
* @param depth maximum depth (-1=no limit, 0=will not recurse into folders)
*/
public void setMaxFolderDepth(long maxFolderDepth) {
this.maxFolderDepth = maxFolderDepth;
}
public static String usage() {
return
"Usage: \n" +
" java " + ResourceFinder.class.getName() + " [options] searchTerm\n" +
"or\n" +
" java " + ResourceFinder.class.getName() + " [options] -a\n" +
"where [options] are:\n" +
" -h -? displays this helptext\n" +
" -f follow symlinks\n" +
" -a show all resources found (i.e. do not use searchTerm)\n" +
"\n" +
"Search criteria:\n" +
" -i case-insensitive match\n" +
" -sc if present, searchTerm matches within filename (default)\n" +
" -ss if present, searchTerm matches start of filename\n" +
" -se if present, searchTerm matches exact filename\n" +
" -sr if present, searchTerm matches filename as a regular expression\n" +
" -mf n max filesystem folder depth (0 = do not descend into subfolders)\n" +
" -ma n max archive depth (0 = do not descend into archives)\n" +
" -x if present, will attempt to recover if errors occur reading archives\n"+
" (errors sent to stderr)\n" +
"\n" +
"Action when resource found:\n" +
" -v verbose; display filenames with file sizes and timestamps\n" +
" -vv display MD5/SHA1 hashes of resources (NB: modifies display order)\n" +
" -d n dump/display the contents of the n'th resource found\n" +
" -d all dump the name and contents of all resources found\n" +
" -d names dump just the names of all resources found (default)\n" +
" -d n1,n2... dump the name and contents of the n1'th, n2'nd etc... resources found\n" +
" -dm n|all as per -d, but performs manifest unmangling on resource (fixes linewraps)\n" +
" -dj n|all as per -d, but performs class decompiling (requires jad to be in PATH)\n" +
" -c text search for text in contents of resource (uses UTF-8 encoding)\n" +
" -ci text case-insensitive search for text in contents of resources\n" +
"\n" +
"* A maximum of one -d switch should be present\n" +
"* The -d and -c switches are mutually exclusive\n";
}
/** Command-line interface to this class
*
* @param args arguments
*
* @throws IOException
*/
public static void main(String args[]) throws IOException {
String searchTerm;
String searchContents = null;
String dumpResource = "";
int dumpType = DisplayResourceFinderCallback.DUMP_NAMES;
List dumpResourceList = null;
int argIndex = 0;
int matchType = MATCHTYPE_CONTAINS;
long maxArchiveDepth = -1;
long maxFolderDepth = -1;
boolean followSymlinks = false;
boolean verbose = false;
boolean showHashes = false;
boolean showAll = false;
boolean manifests = false;
boolean decompile = false;
boolean ignoreCase = false;
boolean ignoreErrors = false;
boolean searchContentsIgnoreCase = false;
if (args.length < 1) {
System.out.println(usage());
throw new IllegalArgumentException("Expected resource search term or options");
}
while (argIndex < args.length && args[argIndex].startsWith("-")) {
if (args[argIndex].startsWith("-d")) {
if (args[argIndex].equals("-dm")) { manifests = true; }
if (args[argIndex].equals("-dj")) { decompile = true; }
dumpResource = args[argIndex + 1];
if (dumpResource.equals("all")) {
dumpType = DisplayResourceFinderCallback.DUMP_NAMES_AND_RESOURCES;
} else if (dumpResource.equals("names")) {
dumpType = DisplayResourceFinderCallback.DUMP_NAMES;
} else {
dumpResourceList = new ArrayList();
String[] resources = dumpResource.split(",");
for (String resource : resources) {
try {
dumpResourceList.add(new Integer(resource));
} catch (NumberFormatException nfe) {
// @TODO if it's not a number, then could use it as a resource id
throw new IllegalArgumentException("Expected numeric resource id (found '" + dumpResource + "')");
}
}
if (dumpResourceList.size() == 1) {
dumpType = DisplayResourceFinderCallback.DUMP_RESOURCES;
} else {
dumpType = DisplayResourceFinderCallback.DUMP_NAMES_AND_RESOURCES;
}
}
// 1.6 method args = Arrays.copyOfRange(args, 2, args.length);
argIndex += 2;
} else if (args[argIndex].equals("-mf")) {
maxFolderDepth = Long.parseLong(args[argIndex + 1]);
argIndex += 2;
} else if (args[argIndex].equals("-ma")) {
maxArchiveDepth = Long.parseLong(args[argIndex + 1]);
argIndex += 2;
} else if (args[argIndex].equals("-v")) {
verbose = true;
argIndex ++;
} else if (args[argIndex].equals("-vv")) {
verbose = true;
showHashes = true;
argIndex ++;
} else if (args[argIndex].equals("-f")) {
followSymlinks = true;
argIndex ++;
} else if (args[argIndex].equals("-a")) {
showAll = true;
argIndex ++;
} else if (args[argIndex].equals("-sc")) {
matchType = MATCHTYPE_CONTAINS;
argIndex ++;
} else if (args[argIndex].equals("-ss")) {
matchType = MATCHTYPE_STARTSWITH;
argIndex ++;
} else if (args[argIndex].equals("-sr")) {
matchType = MATCHTYPE_REGEX;
argIndex ++;
} else if (args[argIndex].equals("-se")) {
matchType = MATCHTYPE_EXACT;
argIndex ++;
} else if (args[argIndex].equals("-c")) {
searchContents = args[argIndex + 1];
// System.out.println("1 searchContents is " + searchContents);
argIndex += 2;
} else if (args[argIndex].equals("-ci")) {
searchContents = args[argIndex + 1];
searchContentsIgnoreCase = true;
// System.out.println("1 searchContents is " + searchContents);
argIndex += 2;
} else if (args[argIndex].equals("-i")) {
ignoreCase = true;
argIndex ++;
} else if (args[argIndex].equals("-x")) {
ignoreErrors = true;
argIndex ++;
} else if (args[argIndex].equals("-h") || args[argIndex].equals("-?")) {
System.out.println(usage());
System.exit(0);
} else {
System.out.println(usage());
throw new IllegalArgumentException("Unknown switch '" + args[argIndex] + "' supplied");
}
}
if (showAll) {
searchTerm = "maguffin";
} else {
if (args.length < argIndex + 1) {
System.out.println(usage());
throw new IllegalArgumentException("Expected resource search term");
}
searchTerm = args[argIndex++];
}
ResourceFinderCallback callback;
if (showHashes) {
callback = new HashingResourceFinderCallback();
} else {
callback = new DisplayResourceFinderCallback(dumpType, dumpResourceList, verbose, manifests, decompile,
searchContents, searchContentsIgnoreCase);
}
ResourceFinder resourceFinder = new ResourceFinder(searchTerm, matchType, ignoreCase, new File("."), callback);
resourceFinder.setFollowSymLinks(followSymlinks);
resourceFinder.setShowAll(showAll);
resourceFinder.setIgnoreErrors(ignoreErrors);
if (maxArchiveDepth != -1) { resourceFinder.setMaxArchiveDepth(maxArchiveDepth); }
if (maxFolderDepth != -1) { resourceFinder.setMaxFolderDepth(maxFolderDepth); }
resourceFinder.find();
}
}