diff --git a/trunk/src/com/jpexs/browsers/cache/CacheEntry.java b/trunk/src/com/jpexs/browsers/cache/CacheEntry.java new file mode 100644 index 000000000..4ed27c190 --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/CacheEntry.java @@ -0,0 +1,91 @@ +package com.jpexs.browsers.cache; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.zip.GZIPInputStream; +import java.util.zip.InflaterInputStream; + +/** + * + * @author JPEXS + */ +public abstract class CacheEntry { + + public abstract String getRequestURL(); + + public abstract Map getResponseHeaders(); + + public abstract String getStatusLine(); + + public abstract String getRequestMethod(); + + public abstract InputStream getResponseRawDataStream(); + + public InputStream getResponseDataStream() { + String contentLengthStr = getHeader("Content-Length"); + int contentLength = -1; + if (contentLengthStr != null) { + try { + contentLength = Integer.parseInt(contentLengthStr); + } catch (NumberFormatException nex) { + } + } + final InputStream rawIs = getResponseRawDataStream(); + InputStream is = rawIs; + if (contentLength > -1) { + is = new LimitedInputStream(is, contentLength); + } + + String encoding = getHeader("Content-Encoding"); + if (encoding != null) { + switch (encoding) { + case "gzip": + try { + is = new GZIPInputStream(is); + } catch (IOException ex) { + is = null; + //ignore + } + break; + case "deflate": + is = new InflaterInputStream(is); + break; + default: //unknown + return null; + } + } + if ("chunked".equals(getHeader("Transfer-Encoding"))) { + is = new ChunkedInputStream(is); + } + return is; + } + + @Override + public String toString() { + return getRequestURL(); + } + + public int getStatusCode() { + String st = getStatusLine(); + if (st == null) { + return 0; + } + String parts[] = st.split(" "); + try { + return Integer.parseInt(parts[1]); + } catch (NumberFormatException nfe) { + return 0; + } + } + + public String getHeader(String header) { + Map m = getResponseHeaders(); + for (String k : m.keySet()) { + if (k.toLowerCase().equals(header.toLowerCase())) { + return m.get(k); + } + } + return null; + } +} diff --git a/trunk/src/com/jpexs/browsers/cache/CacheImplementation.java b/trunk/src/com/jpexs/browsers/cache/CacheImplementation.java new file mode 100644 index 000000000..6bda54b4c --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/CacheImplementation.java @@ -0,0 +1,14 @@ +package com.jpexs.browsers.cache; + +import java.util.List; + +/** + * + * @author JPEXS + */ +public interface CacheImplementation { + + public List getEntries(); + + public void refresh(); +} diff --git a/trunk/src/com/jpexs/browsers/cache/CacheReader.java b/trunk/src/com/jpexs/browsers/cache/CacheReader.java new file mode 100644 index 000000000..8dc32ed49 --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/CacheReader.java @@ -0,0 +1,29 @@ +package com.jpexs.browsers.cache; + +import com.jpexs.browsers.cache.chrome.ChromeCache; +import com.jpexs.browsers.cache.firefox.FirefoxCache; + +/** + * + * @author JPEXS + */ +public class CacheReader { + + public static final String BROWSER_FIREFOX = "firefox"; + public static final String BROWSER_CHROME = "chrome"; + + public static String[] availableBrowsers() { + return new String[]{BROWSER_FIREFOX, BROWSER_CHROME}; + } + + public static CacheImplementation getBrowserCache(String browser) { + switch (browser) { + case BROWSER_CHROME: + return ChromeCache.getInstance(); + case BROWSER_FIREFOX: + return FirefoxCache.getInstance(); + default: + throw new IllegalArgumentException("Invalid browser:" + browser); + } + } +} diff --git a/trunk/src/com/jpexs/browsers/cache/ChunkedInputStream.java b/trunk/src/com/jpexs/browsers/cache/ChunkedInputStream.java new file mode 100644 index 000000000..809047877 --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/ChunkedInputStream.java @@ -0,0 +1,80 @@ +package com.jpexs.browsers.cache; + +import java.io.IOException; +import java.io.InputStream; + +/** + * + * @author JPEXS + */ +public class ChunkedInputStream extends InputStream { + + private InputStream is; + private int chunkPos = 0; + private int chunkLen = 0; + private boolean end = false; + private boolean first = true; + + public ChunkedInputStream(InputStream is) { + this.is = is; + } + + @Override + public int read() throws IOException { + if (end) { + return -1; + } + if (chunkPos >= chunkLen) { + if (!first) { + if (is.read() != '\r') { + throw new IOException("Invalid chunk"); + } + if (is.read() != '\n') { + throw new IOException("Invalid chunk"); + } + } + String lenStr = readLine(); + try { + chunkLen = Integer.parseInt(lenStr); + } catch (NumberFormatException nfe) { + throw new IOException("Invalid chunk"); + } + if (chunkLen == 0) { + is.read(); // \r + is.read(); // \n + end = true; + return -1; + } + chunkPos = 0; + first = false; + } + + chunkPos++; + return is.read(); + } + + private String readLine() throws IOException { + int i; + boolean inr = false; + String ret = ""; + while ((i = is.read()) > -1) { + if (inr) { + inr = false; + if (i == '\n') { + break; + } else { + ret += "\r"; + } + } + if (i == '\r') { + inr = true; + continue; + } + ret += (char) i; + } + if (inr) { + ret += "\r"; + } + return ret; + } +} diff --git a/trunk/src/com/jpexs/browsers/cache/LimitedInputStream.java b/trunk/src/com/jpexs/browsers/cache/LimitedInputStream.java new file mode 100644 index 000000000..03a0a98f9 --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/LimitedInputStream.java @@ -0,0 +1,38 @@ +package com.jpexs.browsers.cache; + +import java.io.IOException; +import java.io.InputStream; + +/** + * + * @author JPEXS + */ +public class LimitedInputStream extends InputStream { + + private InputStream is; + private int pos = 0; + private int limit; + + public LimitedInputStream(InputStream is, int limit) { + this.is = is; + this.limit = limit; + } + + @Override + public int read() throws IOException { + if (pos >= limit) { + return -1; + } + pos++; + return is.read(); + } + + @Override + public int available() throws IOException { + int avail = is.available(); + if (pos + avail > limit) { + avail = limit - pos; + } + return avail; + } +} diff --git a/trunk/src/com/jpexs/browsers/cache/RafInputStream.java b/trunk/src/com/jpexs/browsers/cache/RafInputStream.java new file mode 100644 index 000000000..1f78ba645 --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/RafInputStream.java @@ -0,0 +1,32 @@ +package com.jpexs.browsers.cache; + +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @author JPEXS + */ +public class RafInputStream extends InputStream { + + private RandomAccessFile raf; + private long pos = 0; + + public RafInputStream(RandomAccessFile raf) { + this.raf = raf; + try { + pos = raf.getFilePointer(); + } catch (IOException ex) { + Logger.getLogger(RafInputStream.class.getName()).log(Level.SEVERE, null, ex); + } + } + + @Override + public int read() throws IOException { + raf.seek(pos++); + return raf.read(); + } +} diff --git a/trunk/src/com/jpexs/browsers/cache/chrome/BlockFileHeader.java b/trunk/src/com/jpexs/browsers/cache/chrome/BlockFileHeader.java new file mode 100644 index 000000000..28f48083b --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/chrome/BlockFileHeader.java @@ -0,0 +1,53 @@ +package com.jpexs.browsers.cache.chrome; + +import java.io.IOException; +import java.io.InputStream; + +/** + * + * @author JPEXS + */ +public class BlockFileHeader { + + static final int kBlockHeaderSize = 8192; // Two pages: almost 64k entries + static final int kMaxBlocks = (kBlockHeaderSize - 80) * 8; + long magic; // c3 ca 04 c1 + long version; // 00 00 02 00 + int this_file; + int next_file; + int entry_size; + int num_entries; + int max_entries; + int empty[] = new int[4]; + int hints[] = new int[4]; + int updating; + int user[] = new int[5]; + long allocation_map[] = new long[kMaxBlocks / 32]; + + public BlockFileHeader(InputStream is) throws IOException { + IndexInputStream iis = new IndexInputStream(is); + magic = iis.readUInt32(); + version = iis.readUInt32(); + this_file = iis.readInt16(); + next_file = iis.readInt16(); + entry_size = iis.readInt32(); + num_entries = iis.readInt32(); + max_entries = iis.readInt32(); + empty = new int[4]; + for (int i = 0; i < 4; i++) { + empty[i] = iis.readInt32(); + } + hints = new int[4]; + for (int i = 0; i < 4; i++) { + hints[i] = iis.readInt32(); + } + updating = iis.readInt32(); + user = new int[5]; + for (int i = 0; i < 5; i++) { + user[i] = iis.readInt32(); + } + for (int i = 0; i < allocation_map.length; i++) { + allocation_map[i] = iis.readUInt32(); + } + } +} diff --git a/trunk/src/com/jpexs/browsers/cache/chrome/CacheAddr.java b/trunk/src/com/jpexs/browsers/cache/chrome/CacheAddr.java new file mode 100644 index 000000000..4eeffda08 --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/chrome/CacheAddr.java @@ -0,0 +1,106 @@ +package com.jpexs.browsers.cache.chrome; + +import com.jpexs.browsers.cache.RafInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.util.Map; + +/** + * + * @author JPEXS + */ +public class CacheAddr { + + public static final int EXTERNAL = 0; + public static final int RANKINGS = 1; + public static final int BLOCK_256 = 2; + public static final int BLOCK_1K = 3; + public static final int BLOCK_4K = 4; + public static final int BLOCK_FILES = 5; + public static final int BLOCK_ENTRIES = 6; + public static final int BLOCK_EVICTED = 7; + public static final String blockNames[] = new String[]{"EXTERNAL", "RANKINGS", "BLOCK_256", "BLOCK_1K", "BLOCK_4K", "BLOCK_FILES", "BLOCK_ENTRIES", "BLOCK_EVICTED"}; + public static final int blockSizes[] = new int[]{0, 36, 256, 1024, 4096, 8, 104, 48}; + public static final long kInitializedMask = 0x80000000L; + public static final long kFileTypeMask = 0x70000000L; + public static final int kFileTypeOffset = 28; + public static final long kReservedBitsMask = 0x0c000000L; + public static final long kNumBlocksMask = 0x03000000L; + public static final int kNumBlocksOffset = 24; + public static final long kFileSelectorMask = 0x00ff0000L; + public static final int FileSelectorOffset = 16; + public static final long kStartBlockMask = 0x0000FFFFL; + public static final long kFileNameMask = 0x0FFFFFFFL; + public boolean initialized; + public int fileType; + public int numBlocks; + public int fileSelector; + public int startBlock; + public int fileName; + public long val; + public File rootPath; + private Map dataFiles; + private File externalFilesDir; + + public CacheAddr(InputStream is, File rootPath, Map dataFiles, File externalFilesDir) throws IOException { + this.dataFiles = dataFiles; + this.rootPath = rootPath; + this.externalFilesDir = externalFilesDir; + IndexInputStream iis = new IndexInputStream(is); + val = iis.readUInt32(); + initialized = (val & kInitializedMask) == kInitializedMask; + fileType = (int) ((val & kFileTypeMask) >> kFileTypeOffset); + if (fileType == EXTERNAL) { + fileName = (int) (val & kFileNameMask); + } else { + numBlocks = (int) ((val & kNumBlocksMask) >> kNumBlocksOffset); + fileSelector = (int) ((val & kFileSelectorMask) >> FileSelectorOffset); + startBlock = (int) (val & kStartBlockMask); + } + } + + @Override + public String toString() { + + String ft = blockNames[fileType]; + if (fileType == EXTERNAL) { + return ft + ":" + fileName; + } + if (!initialized) { + return "uninitialized"; + } + return ft + ": numBlocks " + numBlocks + " fileSelector " + fileSelector + " startBlock " + startBlock; + } + + public InputStream getInputStream() throws IOException { + if (!initialized) { + return null; + } + switch (fileType) { + case EXTERNAL: + String fileNameStr = Long.toHexString(fileName); + while (fileNameStr.length() < 6) { + fileNameStr = "0" + fileNameStr; + } + fileNameStr = "f_" + fileNameStr; + return new RafInputStream(new RandomAccessFile(new File(externalFilesDir, fileNameStr), "r")); + case BLOCK_1K: + case BLOCK_256: + case BLOCK_4K: + + RandomAccessFile raf; + + if (dataFiles.containsKey(fileSelector)) { + raf = dataFiles.get(fileSelector); + } else { + raf = new RandomAccessFile(rootPath + "\\data_" + fileSelector, "r"); + dataFiles.put(fileSelector, raf); + } + raf.seek(BlockFileHeader.kBlockHeaderSize + startBlock * blockSizes[fileType]); + return new RafInputStream(raf); + } + return null; + } +} diff --git a/trunk/src/com/jpexs/browsers/cache/chrome/ChromeCache.java b/trunk/src/com/jpexs/browsers/cache/chrome/ChromeCache.java new file mode 100644 index 000000000..768bc2172 --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/chrome/ChromeCache.java @@ -0,0 +1,175 @@ +package com.jpexs.browsers.cache.chrome; + +import com.jpexs.browsers.cache.CacheEntry; +import com.jpexs.browsers.cache.CacheImplementation; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @author JPEXS + */ +public class ChromeCache implements CacheImplementation { + + private static ChromeCache instance; + private File tempDir; + private List dataFiles; + private File indexFile; + + private ChromeCache() { + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + free(); + } + }); + } + + public static ChromeCache getInstance() { + if (instance == null) { + instance = new ChromeCache(); + } + return instance; + } + private boolean loaded = false; + private Index index; + + @Override + public List getEntries() { + if (!loaded) { + refresh(); + } + List ret = new ArrayList<>(); + try { + List entries = index.getEntries(); + for (EntryStore en : entries) { + if (en.state == EntryStore.ENTRY_NORMAL) { + String key = en.getKey(); + if (key != null && !key.trim().equals("")) { + ret.add(en); + } + } + } + } catch (IOException ex) { + Logger.getLogger(ChromeCache.class.getName()).log(Level.SEVERE, null, ex); + return null; + } + return ret; + } + + @Override + public void refresh() { + free(); + File cacheDir = null; + try { + cacheDir = getCacheDirectory(); + } catch (IOException ex) { + Logger.getLogger(ChromeCache.class.getName()).log(Level.SEVERE, null, ex); + } + if (cacheDir == null) { + return; + } + File systemTempDir = new File(System.getProperty("java.io.tmpdir")); + File originalIndexFile = new File(cacheDir + File.separator + "index"); + tempDir = new File(systemTempDir, "cacheView" + System.identityHashCode(this)); + tempDir.mkdir(); + indexFile = new File(tempDir, "index"); + try { + Files.copy(originalIndexFile.toPath(), indexFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException ex) { + Logger.getLogger(ChromeCache.class.getName()).log(Level.SEVERE, null, ex); + return; + } + File originalDataFile; + dataFiles = new ArrayList<>(); + for (int i = 0; (originalDataFile = new File(cacheDir, "data_" + i)).exists(); i++) { + File dataFile = new File(tempDir, "data_" + i); + dataFiles.add(dataFile); + try { + Files.copy(originalDataFile.toPath(), dataFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException ex) { + Logger.getLogger(ChromeCache.class.getName()).log(Level.SEVERE, null, ex); + return; + } + } + try { + index = new Index(indexFile, cacheDir); + } catch (IOException ex) { + Logger.getLogger(ChromeCache.class.getName()).log(Level.SEVERE, null, ex); + } + loaded = true; + } + + private enum OSId { + + WINDOWS, OSX, UNIX + } + + private static OSId getOSId() { + PrivilegedAction doGetOSName = new PrivilegedAction() { + @Override + public String run() { + return System.getProperty("os.name"); + } + }; + OSId id = OSId.UNIX; + String osName = AccessController.doPrivileged(doGetOSName); + if (osName != null) { + if (osName.toLowerCase().startsWith("mac os x")) { + id = OSId.OSX; + } else if (osName.contains("Windows")) { + id = OSId.WINDOWS; + } + } + return id; + } + + public static File getCacheDirectory() throws IOException { + String userHome = null; + File ret = null; + try { + userHome = System.getProperty("user.home"); + } catch (SecurityException ignore) { + } + if (userHome != null) { + OSId osId = getOSId(); + if (osId == OSId.WINDOWS) { + ret = new File(userHome + "\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Cache\\"); + if (!ret.exists()) { + ret = new File(userHome + "\\Local Settings\\Application Data\\Google\\Chrome\\User Data\\Default\\Cache"); + } + } else if (osId == OSId.OSX) { + ret = new File(userHome + "/Library/Caches/Google/Chrome/Default/Cache"); + } else { + ret = new File(userHome + "/.config/google-chrome/Default/Application Cache/Cache/"); + if (!ret.exists()) { + ret = new File(userHome + "/.cache/chromium/Default/Cache"); + } + } + } + if ((ret != null) && !ret.exists()) { + return null; + } + return ret; + + } + + private void free() { + if (loaded) { + index.free(); + indexFile.delete(); + for (File d : dataFiles) { + d.delete(); + } + tempDir.delete(); + } + } +} diff --git a/trunk/src/com/jpexs/browsers/cache/chrome/EntryFlags.java b/trunk/src/com/jpexs/browsers/cache/chrome/EntryFlags.java new file mode 100644 index 000000000..ada1b8dff --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/chrome/EntryFlags.java @@ -0,0 +1,11 @@ +package com.jpexs.browsers.cache.chrome; + +/** + * + * @author JPEXS + */ +public class EntryFlags { + + public static final int PARENT_ENTRY = 1; + public static final int CHILD_ENTRY = 1 << 1; +} diff --git a/trunk/src/com/jpexs/browsers/cache/chrome/EntryState.java b/trunk/src/com/jpexs/browsers/cache/chrome/EntryState.java new file mode 100644 index 000000000..69140884a --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/chrome/EntryState.java @@ -0,0 +1,12 @@ +package com.jpexs.browsers.cache.chrome; + +/** + * + * @author JPEXS + */ +public class EntryState { + + public static final int ENTRY_NORMAL = 0; + public static final int ENTRY_EVICTED = 1; + public static final int ENTRY_DOOMED = 2; +} diff --git a/trunk/src/com/jpexs/browsers/cache/chrome/EntryStore.java b/trunk/src/com/jpexs/browsers/cache/chrome/EntryStore.java new file mode 100644 index 000000000..7d15fa3b6 --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/chrome/EntryStore.java @@ -0,0 +1,137 @@ +package com.jpexs.browsers.cache.chrome; + +import com.jpexs.browsers.cache.CacheEntry; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @author JPEXS + */ +public class EntryStore extends CacheEntry { + + public static final int ENTRY_NORMAL = 0; + public static final int ENTRY_EVICTED = 1; // The entry was recently evicted from the cache. + public static final int ENTRY_DOOMED = 2; // The entry was doomed + public static final int PARENT_ENTRY = 1; // This entry has children (sparse) entries. + public static final int CHILD_ENTRY = 1 << 1; + public long hash; // Full hash of the key. + public CacheAddr next; // Next entry with the same hash or bucket. + public CacheAddr rankings_node; // Rankings node for this entry. + public int reuse_count; // How often is this entry used. + public int refetch_count; // How often is this fetched from the net. + public int state; // Current state. + public long creation_time; + public int key_len; + public CacheAddr long_key; // Optional address of a long key. + public int data_size[] = new int[4]; // We can store up to 4 data streams for each + public CacheAddr data_addr[] = new CacheAddr[4]; // entry. + public long flags; // Any combination of EntryFlags. + public int pad[] = new int[4]; + public long self_hash; // The hash of EntryStore up to this point. + public byte key[] = new byte[256 - 24 * 4]; // null terminated + + public EntryStore(InputStream is, File rootDir, Map dataFiles, File externalFilesDir) throws IOException { + IndexInputStream iis = new IndexInputStream(is); + hash = iis.readUInt32(); + next = new CacheAddr(is, rootDir, dataFiles, externalFilesDir); + rankings_node = new CacheAddr(is, rootDir, dataFiles, externalFilesDir); + reuse_count = iis.readInt32(); + refetch_count = iis.readInt32(); + state = iis.readInt32(); + creation_time = iis.readUInt64(); + key_len = iis.readInt32(); + long_key = new CacheAddr(is, rootDir, dataFiles, externalFilesDir); + data_size = new int[4]; + for (int i = 0; i < 4; i++) { + data_size[i] = iis.readInt32(); + } + data_addr = new CacheAddr[4]; + for (int i = 0; i < 4; i++) { + data_addr[i] = new CacheAddr(is, rootDir, dataFiles, externalFilesDir); + } + flags = iis.readUInt32(); + pad = new int[4]; + for (int i = 0; i < 4; i++) { + pad[i] = iis.readInt32(); + } + self_hash = iis.readUInt32(); + key = new byte[256 - 24 * 4]; + iis.read(key); + } + + public HttpResponseInfo getResponseInfo() { + try { + InputStream is = data_addr[0].getInputStream(); + if (is == null) { + return null; + } + return new HttpResponseInfo(is); + } catch (IOException ex) { + Logger.getLogger(EntryStore.class.getName()).log(Level.SEVERE, null, ex); + return null; + } + } + + public int getResponseDataSize() { + return data_size[1]; + } + + @Override + public InputStream getResponseRawDataStream() { + try { + return data_addr[1].getInputStream(); + } catch (IOException ex) { + Logger.getLogger(EntryStore.class.getName()).log(Level.SEVERE, null, ex); + } + return null; + } + + public String getKey() { + if (key_len < 0) { + return null; + } + return new String(key, 0, key_len > key.length || key_len < 0 ? key.length : key_len); + } + + @Override + public String getRequestURL() { + return getKey(); + } + + @Override + public Map getResponseHeaders() { + HttpResponseInfo ri = getResponseInfo(); + if (ri == null) { + return new HashMap<>(); + } + List headers = ri.headers; + Map ret = new HashMap<>(); + for (int h = 1; h < headers.size(); h++) { + String hs = headers.get(h); + if (hs.contains(":")) { + String hp[] = hs.split(":"); + ret.put(hp[0].trim(), hp[1].trim()); + } + } + return ret; + } + + @Override + public String getStatusLine() { + HttpResponseInfo ri = getResponseInfo(); + return ri.headers.get(0); + } + + @Override + public String getRequestMethod() { + return "GET"; + } +} diff --git a/trunk/src/com/jpexs/browsers/cache/chrome/HttpResponseInfo.java b/trunk/src/com/jpexs/browsers/cache/chrome/HttpResponseInfo.java new file mode 100644 index 000000000..e59f95a61 --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/chrome/HttpResponseInfo.java @@ -0,0 +1,92 @@ +package com.jpexs.browsers.cache.chrome; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author JPEXS + */ +public class HttpResponseInfo { + + public long flags; + public int version; + public long request_time; + public long response_time; + public long payload_size; + public List headers; + // The version of the response info used when persisting response info. + public static final int RESPONSE_INFO_VERSION = 3; + // The minimum version supported for deserializing response info. + public static final int RESPONSE_INFO_MINIMUM_VERSION = 1; + // We reserve up to 8 bits for the version number. + public static final int RESPONSE_INFO_VERSION_MASK = 0xFF; + // This bit is set if the response info has a cert at the end. + // Version 1 serialized only the end-entity certificate, while subsequent + // versions include the available certificate chain. + public static final int RESPONSE_INFO_HAS_CERT = 1 << 8; + // This bit is set if the response info has a security-bits field (security + // strength, in bits, of the SSL connection) at the end. + public static final int RESPONSE_INFO_HAS_SECURITY_BITS = 1 << 9; + // This bit is set if the response info has a cert status at the end. + public static final int RESPONSE_INFO_HAS_CERT_STATUS = 1 << 10; + // This bit is set if the response info has vary header data. + public static final int RESPONSE_INFO_HAS_VARY_DATA = 1 << 11; + // This bit is set if the request was cancelled before completion. + public static final int RESPONSE_INFO_TRUNCATED = 1 << 12; + // This bit is set if the response was received via SPDY. + public static final int RESPONSE_INFO_WAS_SPDY = 1 << 13; + // This bit is set if the request has NPN negotiated. + public static final int RESPONSE_INFO_WAS_NPN = 1 << 14; + // This bit is set if the request was fetched via an explicit proxy. + public static final int RESPONSE_INFO_WAS_PROXY = 1 << 15; + // This bit is set if the response info has an SSL connection status field. + // This contains the ciphersuite used to fetch the resource as well as the + // protocol version, compression method and whether SSLv3 fallback was used. + public static final int RESPONSE_INFO_HAS_SSL_CONNECTION_STATUS = 1 << 16; + // This bit is set if the response info has protocol version. + public static final int RESPONSE_INFO_HAS_NPN_NEGOTIATED_PROTOCOL = 1 << 17; + // This bit is set if the response info has connection info. + public static final int RESPONSE_INFO_HAS_CONNECTION_INFO = 1 << 18; + // This bit is set if the request has http authentication. + public static final int RESPONSE_INFO_USE_HTTP_AUTHENTICATION = 1 << 19; + + public String getHeaderValue(String header) { + for (String h : headers) { + if (h.contains(":")) { + String keyval[] = h.split(":"); + String key = keyval[0].trim().toLowerCase(); + String val = keyval[1].trim(); + if (header.toLowerCase().equals(key)) { + return val; + } + } + } + return null; + } + + public HttpResponseInfo(InputStream is) throws IOException { + IndexInputStream iis = new IndexInputStream(is); + payload_size = iis.readUInt32(); + flags = iis.readInt(); + version = (int) (flags & RESPONSE_INFO_VERSION_MASK); + if (version < RESPONSE_INFO_MINIMUM_VERSION || version > RESPONSE_INFO_VERSION) { + throw new RuntimeException("unexpected response info version: " + version); + } + request_time = iis.readInt64(); + response_time = iis.readInt64(); + String headersStr = iis.readString(); + headers = new ArrayList<>(); + int nulpos; + while ((nulpos = headersStr.indexOf(0)) > 0) { + String h = headersStr.substring(0, nulpos); + headersStr = headersStr.substring(nulpos + 1); + headers.add(h); + } + + //TODO: Read SSL info + + } +} diff --git a/trunk/src/com/jpexs/browsers/cache/chrome/Index.java b/trunk/src/com/jpexs/browsers/cache/chrome/Index.java new file mode 100644 index 000000000..15e6ed5b6 --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/chrome/Index.java @@ -0,0 +1,65 @@ +package com.jpexs.browsers.cache.chrome; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @author JPEXS + */ +public class Index { + + IndexHeader header; + CacheAddr table[]; + public static final int kIndexTablesize = 0x10000; + public File rootDir; + private Map dataFiles; + private File externalFilesDir; + + public void free() { + for (RandomAccessFile r : dataFiles.values()) { + try { + r.close(); + } catch (IOException ex) { + } + } + } + + public List getEntries() throws IOException { + List ret = new ArrayList<>(); + for (CacheAddr ca : table) { + InputStream is = ca.getInputStream(); + if (is != null) { + EntryStore es = new EntryStore(is, rootDir, dataFiles, externalFilesDir); + ret.add(es); + } + } + return ret; + } + + public Index(File file, File externalFilesDir) throws IOException { + dataFiles = new HashMap<>(); + this.externalFilesDir = externalFilesDir; + FileInputStream is = new FileInputStream(file); + rootDir = file.getParentFile(); + header = new IndexHeader(is, rootDir, dataFiles, externalFilesDir); + int tsize = kIndexTablesize; + if (header.table_len > 0) { + tsize = header.table_len; + } + table = new CacheAddr[tsize]; + for (int i = 0; i < tsize; i++) { + table[i] = new CacheAddr(is, rootDir, dataFiles, externalFilesDir); + } + is.close(); + } +} diff --git a/trunk/src/com/jpexs/browsers/cache/chrome/IndexHeader.java b/trunk/src/com/jpexs/browsers/cache/chrome/IndexHeader.java new file mode 100644 index 000000000..4337391a2 --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/chrome/IndexHeader.java @@ -0,0 +1,48 @@ +package com.jpexs.browsers.cache.chrome; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.util.Map; + +/** + * + * @author JPEXS + */ +public class IndexHeader { + + long magic; //c3 ca 03 c1 + long version; //01 00 02 00 + int num_entries; + int num_bytes; + int last_file; + int this_id; + CacheAddr stats; + int table_len; + int crash; + int experiment; + long create_time; + int pad[] = new int[52]; + LruData lru; + + public IndexHeader(InputStream is, File rootDir, Map dataFiles, File externalFilesDir) throws IOException { + IndexInputStream iis = new IndexInputStream(is); + magic = iis.readUInt32(); + version = iis.readUInt32(); + num_entries = iis.readInt32(); + num_bytes = iis.readInt32(); + last_file = iis.readInt32(); + this_id = iis.readInt32(); + stats = new CacheAddr(iis, rootDir, dataFiles, externalFilesDir); + table_len = iis.readInt32(); + crash = iis.readInt32(); + experiment = iis.readInt32(); + create_time = iis.readUInt64(); + pad = new int[52]; + for (int i = 0; i < 52; i++) { + pad[i] = iis.readInt32(); + } + lru = new LruData(is, rootDir, dataFiles, externalFilesDir); + } +} diff --git a/trunk/src/com/jpexs/browsers/cache/chrome/IndexInputStream.java b/trunk/src/com/jpexs/browsers/cache/chrome/IndexInputStream.java new file mode 100644 index 000000000..aeb4890c2 --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/chrome/IndexInputStream.java @@ -0,0 +1,63 @@ +package com.jpexs.browsers.cache.chrome; + +import java.io.IOException; +import java.io.InputStream; + +/** + * + * @author JPEXS + */ +public class IndexInputStream extends InputStream { + + private InputStream is; + public long pos = 0; + + public long getPos() { + return pos; + } + + public IndexInputStream(InputStream is) { + this.is = is; + } + + @Override + public int read() throws IOException { + int r = is.read(); + //System.out.print("$"+Integer.toHexString(r)); + pos++; + return r; + } + + public long readUInt32() throws IOException { + long r = (((long) read()) + ((long) (read() << 8)) + ((long) (read() << 16)) + ((long) (read() << 24))) & 0xffffffffL; + //System.out.println(""); + return r; + } + + public long readUInt64() throws IOException { + return readUInt32() + (readUInt32() << 32); + } + + public long readInt64() throws IOException { + return readUInt64(); //FIXME + } + + public int readInt32() throws IOException { + return (int) readUInt32(); + } + + public int readInt16() throws IOException { + return (short) ((int) read() + (int) (read() << 8)); + } + + public long readInt() throws IOException { + return readInt32(); + } + + public String readString() throws IOException { + int len = (int) readInt(); + byte data[] = new byte[len]; + read(data); + return new String(data); + } +} diff --git a/trunk/src/com/jpexs/browsers/cache/chrome/LruData.java b/trunk/src/com/jpexs/browsers/cache/chrome/LruData.java new file mode 100644 index 000000000..a06ad6e9f --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/chrome/LruData.java @@ -0,0 +1,55 @@ +package com.jpexs.browsers.cache.chrome; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.util.Map; + +/** + * + * @author JPEXS + */ +public class LruData { + + int pad1[] = new int[2]; + int filled; + int sizes[] = new int[5]; + CacheAddr heads[] = new CacheAddr[5]; + CacheAddr tails[] = new CacheAddr[5]; + CacheAddr transaction; + int operation; + int operation_list; + int pad2[] = new int[7]; + + public LruData(InputStream is, File rootDir, Map dataFiles, File externalFilesDir) throws IOException { + IndexInputStream iis = new IndexInputStream(is); + pad1 = new int[2]; + pad1[0] = iis.readInt32(); + pad1[1] = iis.readInt32(); + filled = iis.readInt32(); + sizes = new int[5]; + for (int i = 0; i < 5; i++) { + sizes[i] = iis.readInt32(); + } + sizes = new int[5]; + for (int i = 0; i < 5; i++) { + sizes[i] = iis.readInt32(); + } + heads = new CacheAddr[5]; + for (int i = 0; i < 5; i++) { + heads[i] = new CacheAddr(is, rootDir, dataFiles, externalFilesDir); + } + tails = new CacheAddr[5]; + for (int i = 0; i < 5; i++) { + tails[i] = new CacheAddr(is, rootDir, dataFiles, externalFilesDir); + } + transaction = new CacheAddr(is, rootDir, dataFiles, externalFilesDir); + operation = iis.readInt32(); + operation_list = iis.readInt32(); + pad2 = new int[7]; + for (int i = 0; i < 7; i++) { + pad2[i] = iis.readInt32(); + } + } +} diff --git a/trunk/src/com/jpexs/browsers/cache/chrome/RankingsNode.java b/trunk/src/com/jpexs/browsers/cache/chrome/RankingsNode.java new file mode 100644 index 000000000..36dd093a3 --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/chrome/RankingsNode.java @@ -0,0 +1,16 @@ +package com.jpexs.browsers.cache.chrome; + +/** + * + * @author JPEXS + */ +public class RankingsNode { + + public long last_used; + public long last_modified; + CacheAddr next; + CacheAddr prev; + CacheAddr contents; + int dirty; + long self_hash; +} diff --git a/trunk/src/com/jpexs/browsers/cache/firefox/CacheInputStream.java b/trunk/src/com/jpexs/browsers/cache/firefox/CacheInputStream.java new file mode 100644 index 000000000..f0eb66729 --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/firefox/CacheInputStream.java @@ -0,0 +1,35 @@ +package com.jpexs.browsers.cache.firefox; + +import java.io.IOException; +import java.io.InputStream; + +/** + * + * @author JPEXS + */ +public class CacheInputStream extends InputStream { + + private InputStream is; + + @Override + public int available() throws IOException { + return is.available(); + } + + public CacheInputStream(InputStream is) { + this.is = is; + } + + @Override + public int read() throws IOException { + return is.read(); + } + + public long readInt32() throws IOException { + return (read() << 24) + (read() << 16) + (read() << 8) + read(); + } + + public int readInt16() throws IOException { + return (read() << 8) + read(); + } +} diff --git a/trunk/src/com/jpexs/browsers/cache/firefox/CacheMap.java b/trunk/src/com/jpexs/browsers/cache/firefox/CacheMap.java new file mode 100644 index 000000000..1d2f73f5a --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/firefox/CacheMap.java @@ -0,0 +1,57 @@ +package com.jpexs.browsers.cache.firefox; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * + * @author JPEXS + */ +public class CacheMap { + + public long version; + public long datasize; + public long entryCount; + public long dirtyFlag; + public long recordCount; + public long evictionRank[]; + public long bucketUsage[]; + public List mapBuckets; + + public CacheMap(File file) throws IOException { + File cacheDir = file.getParentFile(); + CacheInputStream cis = new CacheInputStream(new FileInputStream(file)); + version = cis.readInt32(); + datasize = cis.readInt32(); + entryCount = cis.readInt32(); + dirtyFlag = cis.readInt32(); + recordCount = cis.readInt32(); + evictionRank = new long[32]; + for (int i = 0; i < evictionRank.length; i++) { + evictionRank[i] = cis.readInt32(); + } + bucketUsage = new long[32]; + for (int i = 0; i < bucketUsage.length; i++) { + bucketUsage[i] = cis.readInt32(); + } + mapBuckets = new ArrayList<>(); + Map cacheFiles = new HashMap<>(); + for (int i = 1; i <= 3; i++) { + cacheFiles.put(i, new RandomAccessFile(new File(cacheDir, "_CACHE_00" + i + "_"), "r")); + } + while (cis.available() > 0) { + MapBucket mb = new MapBucket(cis, cacheDir, cacheFiles); + if (mb.hash == 0) { + continue; + } + MetaData m = mb.getMetaData(); + mapBuckets.add(mb); + } + } +} diff --git a/trunk/src/com/jpexs/browsers/cache/firefox/FirefoxCache.java b/trunk/src/com/jpexs/browsers/cache/firefox/FirefoxCache.java new file mode 100644 index 000000000..596779068 --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/firefox/FirefoxCache.java @@ -0,0 +1,136 @@ +package com.jpexs.browsers.cache.firefox; + +import com.jpexs.browsers.cache.CacheEntry; +import com.jpexs.browsers.cache.CacheImplementation; +import java.io.File; +import java.io.IOException; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @author JPEXS + */ +public class FirefoxCache implements CacheImplementation { + + private static FirefoxCache instance; + + private FirefoxCache() { + } + + public static FirefoxCache getInstance() { + if (instance == null) { + instance = new FirefoxCache(); + } + return instance; + } + private boolean loaded = false; + private CacheMap map; + + @Override + public void refresh() { + File dir = getCacheDirectory(); + File cacheMapFile = new File(dir, "_CACHE_MAP_"); + try { + map = new CacheMap(cacheMapFile); + } catch (IOException ex) { + Logger.getLogger(FirefoxCache.class.getName()).log(Level.SEVERE, null, ex); + } + loaded = true; + } + + @Override + public List getEntries() { + if (!loaded) { + refresh(); + } + if (map == null) { + return null; + } + List ret = new ArrayList<>(); + + ret.addAll(map.mapBuckets); + return ret; + } + + private enum OSId { + + WINDOWS, OSX, UNIX + } + + private static OSId getOSId() { + PrivilegedAction doGetOSName = new PrivilegedAction() { + @Override + public String run() { + return System.getProperty("os.name"); + } + }; + OSId id = OSId.UNIX; + String osName = AccessController.doPrivileged(doGetOSName); + if (osName != null) { + if (osName.toLowerCase().startsWith("mac os x")) { + id = OSId.OSX; + } else if (osName.contains("Windows")) { + id = OSId.WINDOWS; + } + } + return id; + } + + public static File getProfileDirectory() { + File profilesDir = getProfilesDirectory(); + if (profilesDir == null) { + return null; + } + File profiles[] = profilesDir.listFiles(); + File profileDir = null; + for (File f : profiles) { + if (f.isDirectory()) { + if (f.getName().matches("[a-z0-9]+\\.default")) { + profileDir = f; + break; + } + } + } + return profileDir; + } + + public static File getCacheDirectory() { + File profileDir = getProfileDirectory(); + File cacheDir = null; + if (profileDir != null) { + cacheDir = new File(profileDir, "Cache"); + } + return cacheDir; + } + + public static File getProfilesDirectory() { + String userHome = null; + File profilesDir = null; + try { + userHome = System.getProperty("user.home"); + } catch (SecurityException ignore) { + } + if (userHome != null) { + OSId osId = getOSId(); + if (osId == OSId.WINDOWS) { + profilesDir = new File(userHome + "\\AppData\\Local\\Mozilla\\Firefox\\Profiles"); + if (!profilesDir.exists()) { + profilesDir = new File(userHome + "\\Local Settings\\Application Data\\Mozilla\\Firefox\\Profiles"); + } + } else if (osId == OSId.OSX) { + profilesDir = new File(userHome + "/Library/Caches/Firefox/Profiles"); + } else { + profilesDir = new File(userHome + "/.mozilla/firefox"); + } + } + if ((profilesDir == null) || !profilesDir.exists()) { + return null; + } + return profilesDir; + } +} diff --git a/trunk/src/com/jpexs/browsers/cache/firefox/Location.java b/trunk/src/com/jpexs/browsers/cache/firefox/Location.java new file mode 100644 index 000000000..d647d598b --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/firefox/Location.java @@ -0,0 +1,100 @@ +package com.jpexs.browsers.cache.firefox; + +import com.jpexs.browsers.cache.RafInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.util.Map; + +/** + * + * @author JPEXS + */ +public class Location { + + public int locationSelector; + public int extraBlocks; + public long blockNumber; + public int fileGeneration; + public int fileSize; + public boolean isMetadata; + public long hash; + private File rootDir; + public static final long eReservedMask = 0x4C000000L; + public static final long eLocationSelectorMask = 0x30000000L; + public static final int eLocationSelectorOffset = 28; + public static final long eExtraBlocksMask = 0x03000000L; + public static final int eExtraBlocksOffset = 24; + public static final long eBlockNumberMask = 0x00FFFFFFL; + public static final long eFileGenerationMask = 0x000000FFL; + public static final long eFileSizeMask = 0x00FFFF00L; + public static final int eFileSizeOffset = 8; + public static final long eFileReservedMask = 0x4F000000L; + + public static int size_shift(int idx) { + return (2 * ((idx) - 1)); + } + + public static int bitmapSizeForIndex(int idx) { + return ((idx > 0) ? (131072 >> size_shift(idx)) : 0); + } + + public static int blockSizeForIndex(int idx) { + return ((idx > 0) ? (256 << size_shift(idx)) : 0); + } + /* + #define BLOCK_SIZE_FOR_INDEX(idx) ((idx) ? (256 << SIZE_SHIFT(idx)) : 0) + #define BITMAP_SIZE_FOR_INDEX(idx) ((idx) ? (131072 >> SIZE_SHIFT(idx)) : 0) + */ + private Map dataFiles; + + public InputStream getInputStream() throws IOException { + String fileName = getFileName(); + if (locationSelector > 0) { + RandomAccessFile raf = dataFiles.get(locationSelector); + raf.seek(bitmapSizeForIndex(locationSelector) / 8 + blockSizeForIndex(locationSelector) * blockNumber); + return new RafInputStream(raf); + } + return new FileInputStream(new File(rootDir, fileName)); + } + + public Location(long val, boolean isMetadata, long hash, File rootDir, Map dataFiles) { + this.hash = hash; + this.dataFiles = dataFiles; + this.rootDir = rootDir; + this.isMetadata = isMetadata; + locationSelector = (int) ((val & eLocationSelectorMask) >> eLocationSelectorOffset); + if (locationSelector > 0) { + extraBlocks = (int) ((val & eExtraBlocksMask) >> eExtraBlocksOffset); + blockNumber = (int) (val & eBlockNumberMask); + } else { + fileSize = (int) ((val & eFileSizeMask) >> eFileSizeOffset); + fileGeneration = (int) (val & eFileGenerationMask); + } + } + + public String getFileName() { + if (locationSelector > 0) { + return "_CACHE_00" + locationSelector + "_"; + } + String hashHex = Long.toHexString(hash & 0xffffffffL).toUpperCase(); + while (hashHex.length() < 8) { + hashHex = "0" + hashHex; + } + String genHex = Integer.toHexString(fileGeneration); + while (genHex.length() < 2) { + genHex = "0" + genHex; + } + return hashHex.charAt(0) + File.separator + hashHex.substring(1, 1 + 2) + File.separator + hashHex.substring(3) + (isMetadata ? "m" : "d") + genHex; + } + + @Override + public String toString() { + if (locationSelector > 0) { + return getFileName() + " block " + blockNumber + " extraBlocks " + extraBlocks; + } + return getFileName(); + } +} diff --git a/trunk/src/com/jpexs/browsers/cache/firefox/MapBucket.java b/trunk/src/com/jpexs/browsers/cache/firefox/MapBucket.java new file mode 100644 index 000000000..c17a22375 --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/firefox/MapBucket.java @@ -0,0 +1,109 @@ +package com.jpexs.browsers.cache.firefox; + +import com.jpexs.browsers.cache.CacheEntry; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * + * @author JPEXS + */ +public class MapBucket extends CacheEntry { + + public long hash; + public long enviction; + public Location dataLocation; + public Location metadataLocation; + private MetaData metadata; + + public MapBucket(InputStream is, File rootDir, Map dataFiles) throws IOException { + CacheInputStream cis = new CacheInputStream(is); + hash = cis.readInt32(); + enviction = cis.readInt32(); + dataLocation = new Location(cis.readInt32(), false, hash, rootDir, dataFiles); + metadataLocation = new Location(cis.readInt32(), true, hash, rootDir, dataFiles); + } + + public InputStream getMetaDataStream() throws IOException { + return metadataLocation.getInputStream(); + } + + public MetaData getMetaData() { + if (metadata == null) { + try { + metadata = new MetaData(getMetaDataStream()); + } catch (IOException ex) { + Logger.getLogger(MapBucket.class.getName()).log(Level.SEVERE, null, ex); + } + } + return metadata; + } + + @Override + public String getRequestURL() { + String req = null; + MetaData m = getMetaData(); + if (m == null) { + return null; + } + req = m.request; + if (req == null) { + return null; + } + if (req.startsWith("HTTP:")) { + req = req.substring("HTTP:".length()); + } + return req; + } + + @Override + public Map getResponseHeaders() { + MetaData m = getMetaData(); + if (m == null) { + return null; + } + String responseHead = m.response.get("response-head"); + String headers[] = responseHead.split("\r\n"); + Map ret = new HashMap<>(); + for (int h = 1; h < headers.length; h++) { + String hs = headers[h]; + if (hs.contains(":")) { + String hp[] = hs.split(":"); + ret.put(hp[0].trim(), hp[1].trim()); + } + } + return ret; + } + + @Override + public String getStatusLine() { + MetaData m = getMetaData(); + if (m == null) { + return null; + } + String responseHead = m.response.get("response-head"); + String headers[] = responseHead.split("\r\n"); + return headers[0]; + } + + @Override + public String getRequestMethod() { + return "GET"; //No POST caching in Firefox + } + + @Override + public InputStream getResponseRawDataStream() { + try { + return dataLocation.getInputStream(); + } catch (IOException ex) { + Logger.getLogger(MapBucket.class.getName()).log(Level.SEVERE, null, ex); + } + return null; + } +} diff --git a/trunk/src/com/jpexs/browsers/cache/firefox/MetaData.java b/trunk/src/com/jpexs/browsers/cache/firefox/MetaData.java new file mode 100644 index 000000000..d8813777e --- /dev/null +++ b/trunk/src/com/jpexs/browsers/cache/firefox/MetaData.java @@ -0,0 +1,60 @@ +package com.jpexs.browsers.cache.firefox; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +/** + * + * @author JPEXS + */ +public class MetaData { + + public int majorVersion; + public int minorVersion; + public long location; + public long fetchCount; + public long firstFetchTime; + public long lastFetchTime; + public long expireTime; + public long dataSize; + public long requestSize; + public long infoSize; + public String request; + public Map response; + + public MetaData(InputStream is) throws IOException { + CacheInputStream cis = new CacheInputStream(is); + majorVersion = cis.readInt16(); + minorVersion = cis.readInt16(); + location = cis.readInt32(); + fetchCount = cis.readInt32(); + firstFetchTime = cis.readInt32(); + lastFetchTime = cis.readInt32(); + expireTime = cis.readInt32(); + dataSize = cis.readInt32(); + requestSize = cis.readInt32(); + infoSize = cis.readInt32(); + byte req[] = new byte[(int) requestSize]; + cis.read(req); + request = new String(req, 0, (int) requestSize - 1/*Ends with char 0*/); + byte res[] = new byte[(int) infoSize]; + cis.read(res); + String responseStr = new String(res); + int nulpos; + boolean inKey = true; + String key = null; + response = new HashMap<>(); + while ((nulpos = responseStr.indexOf(0)) > 0) { + String v = responseStr.substring(0, nulpos); + responseStr = responseStr.substring(nulpos + 1); + if (inKey) { + key = v; + } else { + response.put(key, v); + } + inKey = !inKey; + } + } +} diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/LoadFromCacheFrame.java b/trunk/src/com/jpexs/decompiler/flash/gui/LoadFromCacheFrame.java new file mode 100644 index 000000000..b71b81dc8 --- /dev/null +++ b/trunk/src/com/jpexs/decompiler/flash/gui/LoadFromCacheFrame.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2013 JPEXS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.jpexs.decompiler.flash.gui; + +import com.jpexs.browsers.cache.CacheEntry; +import com.jpexs.browsers.cache.CacheImplementation; +import com.jpexs.browsers.cache.CacheReader; +import com.jpexs.decompiler.flash.Configuration; +import com.jpexs.helpers.Helper; +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Vector; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import static javax.swing.JFrame.EXIT_ON_CLOSE; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.SwingConstants; +import javax.swing.filechooser.FileFilter; + +/** + * + * @author JPEXS + */ +public class LoadFromCacheFrame extends AppFrame implements ActionListener { + + private JList list; + private JTextField searchField; + private List caches; + private List entries; + + public LoadFromCacheFrame() { + setSize(800, 600); + View.setWindowIcon(this); + View.centerScreen(this); + setTitle(translate("dialog.title")); + setDefaultCloseOperation(HIDE_ON_CLOSE); + Container cnt = getContentPane(); + list = new JList<>(); + list.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() > 1) { + openSWF(); + } + } + }); + searchField = new JTextField(); + searchField.addKeyListener(new KeyListener() { + @Override + public void keyTyped(KeyEvent e) { + filter(); + } + + @Override + public void keyPressed(KeyEvent e) { + } + + @Override + public void keyReleased(KeyEvent e) { + } + }); + cnt.setLayout(new BorderLayout()); + cnt.add(searchField, BorderLayout.NORTH); + cnt.add(new JScrollPane(list), BorderLayout.CENTER); + + JPanel bottomPanel = new JPanel(); + bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.Y_AXIS)); + + JPanel buttonsPanel = new JPanel(new FlowLayout()); + + JButton openButton = new JButton(translate("button.open")); + openButton.setActionCommand("OPEN"); + openButton.addActionListener(this); + buttonsPanel.add(openButton); + + JButton saveButton = new JButton(translate("button.save")); + saveButton.setActionCommand("SAVE"); + saveButton.addActionListener(this); + buttonsPanel.add(saveButton); + + JButton refreshButton = new JButton(translate("button.refresh")); + refreshButton.setActionCommand("REFRESH"); + refreshButton.addActionListener(this); + buttonsPanel.add(refreshButton); + + JPanel browsersPanel = new JPanel(new FlowLayout()); + browsersPanel.add(new JLabel(translate("supported.browsers"))); + JLabel chromeLabel = new JLabel("Google Chrome", View.getIcon("chrome16"), JLabel.CENTER); + JLabel firefoxLabel = new JLabel("Mozilla Firefox*", View.getIcon("firefox16"), JLabel.CENTER); + browsersPanel.add(chromeLabel); + browsersPanel.add(firefoxLabel); + buttonsPanel.setAlignmentX(0.5f); + + bottomPanel.add(buttonsPanel); + browsersPanel.setAlignmentX(0.5f); + bottomPanel.add(browsersPanel); + JLabel infoLabel = new JLabel(translate("info.closed")); + infoLabel.setAlignmentX(0.5f); + bottomPanel.add(infoLabel); + cnt.add(bottomPanel, BorderLayout.SOUTH); + refresh(); + } + + private void refresh() { + if (caches == null) { + caches = new ArrayList<>(); + for (String b : CacheReader.availableBrowsers()) { + caches.add(CacheReader.getBrowserCache(b)); + } + } else { + for (CacheImplementation c : caches) { + c.refresh(); + } + } + entries = new ArrayList<>(); + for (CacheImplementation c : caches) { + for (CacheEntry en : c.getEntries()) { + String contentType = en.getHeader("Content-Type"); + if ("application/x-shockwave-flash".equals(contentType)) { + entries.add(en); + } + } + } + filter(); + } + + @SuppressWarnings("unchecked") + private void filter() { + String search = searchField.getText(); + List filtered = new ArrayList<>(); + for (CacheEntry en : entries) { + if (search.equals("") || en.getRequestURL().contains(search)) { + filtered.add(en); + } + } + list.setListData(new Vector(filtered)); + } + + private static String entryToFileName(CacheEntry en) { + String ret = en.getRequestURL(); + //Strip parameters + if (ret.contains("?")) { + ret = ret.substring(0, ret.indexOf("?")); + } + //Strip path + if (ret.contains("/")) { + ret = ret.substring(ret.lastIndexOf("/") + 1); + } + return ret; + } + + private void openSWF() { + CacheEntry en = list.getSelectedValue(); + if (en != null) { + BufferedInputStream str = new BufferedInputStream(en.getResponseDataStream()); + str.mark(Integer.MAX_VALUE); + Main.openFile(entryToFileName(en), str); + } + } + + @Override + public void actionPerformed(ActionEvent e) { + switch (e.getActionCommand()) { + case "REFRESH": + refresh(); + break; + case "OPEN": + openSWF(); + break; + case "SAVE": + List selected = list.getSelectedValuesList(); + if (!selected.isEmpty()) { + JFileChooser fc = new JFileChooser(); + fc.setCurrentDirectory(new File(Configuration.getConfig("lastSaveDir", "."))); + if (selected.size() > 1) { + fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + } else { + fc.setSelectedFile(new File(Configuration.getConfig("lastSaveDir", "."), entryToFileName(selected.get(0)))); + fc.setFileFilter(new FileFilter() { + @Override + public boolean accept(File f) { + return (f.getName().endsWith(".swf")) || (f.isDirectory()); + } + + @Override + public String getDescription() { + return AppStrings.translate("filter.swf"); + } + }); + } + fc.setAcceptAllFileFilterUsed(false); + JFrame f = new JFrame(); + View.setWindowIcon(f); + int returnVal = fc.showSaveDialog(f); + if (returnVal == JFileChooser.APPROVE_OPTION) { + File file = Helper.fixDialogFile(fc.getSelectedFile()); + try { + if (selected.size() == 1) { + Helper.saveStream(selected.get(0).getResponseDataStream(), file); + } else { + for (CacheEntry sel : selected) { + Helper.saveStream(selected.get(0).getResponseDataStream(), new File(file, entryToFileName(sel))); + } + } + Configuration.setConfig("lastSaveDir", file.getParentFile().getAbsolutePath()); + } catch (IOException ex) { + View.showMessageDialog(null, translate("error.file.write")); + } + } + } + break; + } + } +} \ No newline at end of file diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/Main.java b/trunk/src/com/jpexs/decompiler/flash/gui/Main.java index 72921afd3..8365702d1 100644 --- a/trunk/src/com/jpexs/decompiler/flash/gui/Main.java +++ b/trunk/src/com/jpexs/decompiler/flash/gui/Main.java @@ -99,8 +99,16 @@ public class Main { private static final int UPDATE_SYSTEM_MAJOR = 1; private static final int UPDATE_SYSTEM_MINOR = 0; public static LoadFromMemoryFrame loadFromMemoryFrame; + public static LoadFromCacheFrame loadFromCacheFrame; public static boolean readOnly = false; + public static void loadFromCache() { + if (loadFromCacheFrame == null) { + loadFromCacheFrame = new LoadFromCacheFrame(); + } + loadFromCacheFrame.setVisible(true); + } + public static void loadFromMemory() { if (loadFromMemoryFrame == null) { loadFromMemoryFrame = new LoadFromMemoryFrame(); @@ -341,6 +349,10 @@ public class Main { loadFromMemoryFrame.setVisible(false); loadFromMemoryFrame = null; } + if (loadFromCacheFrame != null) { + loadFromCacheFrame.setVisible(false); + loadFromCacheFrame = null; + } reloadSWF(); } diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/MainFrame.java b/trunk/src/com/jpexs/decompiler/flash/gui/MainFrame.java index 0f189f978..7d7b76484 100644 --- a/trunk/src/com/jpexs/decompiler/flash/gui/MainFrame.java +++ b/trunk/src/com/jpexs/decompiler/flash/gui/MainFrame.java @@ -421,18 +421,23 @@ public class MainFrame extends AppRibbonFrame implements ActionListener, TreeSel JCommandButton searchCommandButton = new JCommandButton(fixCommandTitle(translate("menu.tools.searchas")), View.getResizableIcon("search32")); assignListener(searchCommandButton, "SEARCHAS"); - JCommandButton proxyCommandButton = new JCommandButton(fixCommandTitle(translate("menu.tools.proxy")), View.getResizableIcon("proxy32")); - assignListener(proxyCommandButton, "SHOWPROXY"); JCommandButton gotoDocumentClassCommandButton = new JCommandButton(fixCommandTitle(translate("menu.tools.gotodocumentclass")), View.getResizableIcon("gotomainclass32")); assignListener(gotoDocumentClassCommandButton, "GOTODOCUMENTCLASS"); - JCommandButton loadMemoryCommandButton = new JCommandButton(fixCommandTitle(translate("menu.tools.searchmemory")), View.getResizableIcon("open_process32")); + JCommandButton proxyCommandButton = new JCommandButton(fixCommandTitle(translate("menu.tools.proxy")), View.getResizableIcon("proxy16")); + assignListener(proxyCommandButton, "SHOWPROXY"); + + JCommandButton loadMemoryCommandButton = new JCommandButton(fixCommandTitle(translate("menu.tools.searchmemory")), View.getResizableIcon("loadmemory16")); assignListener(loadMemoryCommandButton, "LOADMEMORY"); + JCommandButton loadCacheCommandButton = new JCommandButton(fixCommandTitle(translate("menu.tools.searchcache")), View.getResizableIcon("loadcache16")); + assignListener(loadCacheCommandButton, "LOADCACHE"); + toolsBand.addCommandButton(searchCommandButton, RibbonElementPriority.TOP); - toolsBand.addCommandButton(proxyCommandButton, RibbonElementPriority.TOP); toolsBand.addCommandButton(gotoDocumentClassCommandButton, RibbonElementPriority.TOP); - toolsBand.addCommandButton(loadMemoryCommandButton, RibbonElementPriority.TOP); + toolsBand.addCommandButton(proxyCommandButton, RibbonElementPriority.MEDIUM); + toolsBand.addCommandButton(loadMemoryCommandButton, RibbonElementPriority.MEDIUM); + toolsBand.addCommandButton(loadCacheCommandButton, RibbonElementPriority.MEDIUM); if (!ProcessTools.toolsAvailable()) { loadMemoryCommandButton.setEnabled(false); } @@ -607,6 +612,11 @@ public class MainFrame extends AppRibbonFrame implements ActionListener, TreeSel return; } } + if (Main.loadFromCacheFrame != null) { + if (Main.loadFromCacheFrame.isVisible()) { + return; + } + } Main.exit(); } }); @@ -964,7 +974,7 @@ public class MainFrame extends AppRibbonFrame implements ActionListener, TreeSel if (!Helper.contains(selectionRows, row)) { tagTree.setSelectionRow(row); } - + TreePath[] paths = tagTree.getSelectionPaths(); if (paths == null || paths.length == 0) { return; @@ -2270,6 +2280,9 @@ public class MainFrame extends AppRibbonFrame implements ActionListener, TreeSel case "LOADMEMORY": Main.loadFromMemory(); break; + case "LOADCACHE": + Main.loadFromCache(); + break; case "SHOWERRORLOG": errorLogFrame.setVisible(true); break; @@ -2464,7 +2477,7 @@ public class MainFrame extends AppRibbonFrame implements ActionListener, TreeSel tagsToRemove.add((Tag) tag); } } - + if (tagsToRemove.size() == 1) { Tag tag = tagsToRemove.get(0); if (View.showConfirmDialog(this, translate("message.confirm.remove").replace("%item%", tag.toString()), translate("message.confirm"), JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE) == JOptionPane.YES_OPTION) { @@ -2991,7 +3004,7 @@ public class MainFrame extends AppRibbonFrame implements ActionListener, TreeSel showCard(CARDFLASHPANEL); parametersPanel.setVisible(false); if (flashPanel != null) { - Color backgroundColor=View.DEFAULT_BACKGROUND_COLOR; + Color backgroundColor = View.DEFAULT_BACKGROUND_COLOR; for (Tag t : swf.tags) { if (t instanceof SetBackgroundColorTag) { backgroundColor = ((SetBackgroundColorTag) t).backgroundColor.toColor(); @@ -3002,12 +3015,12 @@ public class MainFrame extends AppRibbonFrame implements ActionListener, TreeSel if (tempFile != null) { tempFile.delete(); } - try{ + try { tempFile = File.createTempFile("temp", ".swf"); tempFile.deleteOnExit(); swf.saveTo(new FileOutputStream(tempFile)); flashPanel.displaySWF(tempFile.getAbsolutePath(), backgroundColor, swf.frameRate); - }catch(IOException iex){ + } catch (IOException iex) { Logger.getLogger(MainFrame.class.getName()).log(Level.SEVERE, "Cannot create tempfile", iex); } } @@ -3379,7 +3392,7 @@ public class MainFrame extends AppRibbonFrame implements ActionListener, TreeSel private List> getExpandedNodes(JTree tree) { List> expandedNodes = new ArrayList<>(); int rowCount = tree.getRowCount(); - for (int i = 0; i < rowCount; i++){ + for (int i = 0; i < rowCount; i++) { TreePath path = tree.getPathForRow(i); if (tree.isExpanded(path)) { List pathAsStringList = new ArrayList<>(); @@ -3391,27 +3404,27 @@ public class MainFrame extends AppRibbonFrame implements ActionListener, TreeSel } return expandedNodes; } - + private void expandTreeNodes(JTree tree, List> pathsToExpand) { - for(List pathAsStringList : pathsToExpand) { + for (List pathAsStringList : pathsToExpand) { expandTreeNode(tree, pathAsStringList); } } - + private void expandTreeNode(JTree tree, List pathAsStringList) { TreeModel model = tree.getModel(); Object node = model.getRoot(); - + if (pathAsStringList.isEmpty()) { return; } if (!pathAsStringList.get(0).equals(node.toString())) { return; } - + List path = new ArrayList<>(); path.add(node); - + for (int i = 1; i < pathAsStringList.size(); i++) { String name = pathAsStringList.get(i); int childCount = model.getChildCount(node); @@ -3428,7 +3441,7 @@ public class MainFrame extends AppRibbonFrame implements ActionListener, TreeSel TreePath tp = new TreePath(path.toArray(new Object[path.size()])); tree.expandPath(tp); } - + public void setEditText(boolean edit) { textValue.setEditable(edit); textSaveButton.setVisible(edit); diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/graphics/chrome16.png b/trunk/src/com/jpexs/decompiler/flash/gui/graphics/chrome16.png new file mode 100644 index 000000000..2adb345d1 Binary files /dev/null and b/trunk/src/com/jpexs/decompiler/flash/gui/graphics/chrome16.png differ diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/graphics/firefox16.png b/trunk/src/com/jpexs/decompiler/flash/gui/graphics/firefox16.png new file mode 100644 index 000000000..71fb37ef4 Binary files /dev/null and b/trunk/src/com/jpexs/decompiler/flash/gui/graphics/firefox16.png differ diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/graphics/loadcache16.png b/trunk/src/com/jpexs/decompiler/flash/gui/graphics/loadcache16.png new file mode 100644 index 000000000..61a8556c4 Binary files /dev/null and b/trunk/src/com/jpexs/decompiler/flash/gui/graphics/loadcache16.png differ diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/graphics/loadmemory16.png b/trunk/src/com/jpexs/decompiler/flash/gui/graphics/loadmemory16.png new file mode 100644 index 000000000..5cc2b0dd3 Binary files /dev/null and b/trunk/src/com/jpexs/decompiler/flash/gui/graphics/loadmemory16.png differ diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/graphics/open_process32.png b/trunk/src/com/jpexs/decompiler/flash/gui/graphics/loadmemory32.png similarity index 100% rename from trunk/src/com/jpexs/decompiler/flash/gui/graphics/open_process32.png rename to trunk/src/com/jpexs/decompiler/flash/gui/graphics/loadmemory32.png diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/locales/LoadFromCacheFrame.properties b/trunk/src/com/jpexs/decompiler/flash/gui/locales/LoadFromCacheFrame.properties new file mode 100644 index 000000000..75b4f8caa --- /dev/null +++ b/trunk/src/com/jpexs/decompiler/flash/gui/locales/LoadFromCacheFrame.properties @@ -0,0 +1,21 @@ +# Copyright (C) 2013 JPEXS +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +button.open = Open +button.save = Save +button.refresh = Refresh +dialog.title = Search browsers cache +supported.browsers = Supported browsers: +info.closed = *This browser saves data to disk cache after application exit so close browser first. \ No newline at end of file diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/locales/LoadFromCacheFrame_cs.properties b/trunk/src/com/jpexs/decompiler/flash/gui/locales/LoadFromCacheFrame_cs.properties new file mode 100644 index 000000000..47948263c --- /dev/null +++ b/trunk/src/com/jpexs/decompiler/flash/gui/locales/LoadFromCacheFrame_cs.properties @@ -0,0 +1,21 @@ +# Copyright (C) 2013 JPEXS +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +button.open = Otev\u0159\u00edt +button.save = Ulo\u017eit +button.refresh = Obnovit +dialog.title = Prohledat cache prohl\u00ed\u017ee\u010d\u016f +supported.browsers = Podporovan\u00e9 prohl\u00ed\u017ee\u010de: +info.closed = *Tento prohl\u00ed\u017ee\u010d ukl\u00e1d\u00e1 data do diskov\u00e9 cache a\u017e po ukon\u010den\u00ed aplikace tak\u017ee ho nejd\u0159\u00edve ukon\u010dete. \ No newline at end of file diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/locales/LoadFromMemoryFrame_cs.properties b/trunk/src/com/jpexs/decompiler/flash/gui/locales/LoadFromMemoryFrame_cs.properties index 9b76228c4..38c731b75 100644 --- a/trunk/src/com/jpexs/decompiler/flash/gui/locales/LoadFromMemoryFrame_cs.properties +++ b/trunk/src/com/jpexs/decompiler/flash/gui/locales/LoadFromMemoryFrame_cs.properties @@ -17,7 +17,7 @@ dialog.title = Hledat v pam\u011bti button.open = Otev\u0159\u00edt button.select = Vybrat button.refresh = Obnovit seznam -noprocess = Nebyl vybr\u00e1n process +noprocess = Nebyl vybr\u00e1n proces searching = Prohled\u00e1v\u00e1n\u00ed... swfitem = [SWF verze %version% velikost %size%] notfound = \u017d\u00e1dn\u00e9 SWF nebylo nalezeno \ No newline at end of file diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties b/trunk/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties index 1fd735e50..e6e4e1a27 100644 --- a/trunk/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties +++ b/trunk/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties @@ -353,3 +353,5 @@ preview.pause = Pause preview.stop = Stop message.confirm.removemultiple = Are you sure you want to remove %count% items\n and all objects which depend on it? + +menu.tools.searchcache = Search browsers cache \ No newline at end of file diff --git a/trunk/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties b/trunk/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties index f861c0dbc..b37bbf2a1 100644 --- a/trunk/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties +++ b/trunk/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties @@ -358,3 +358,5 @@ preview.pause = Pauza preview.stop = Zastavit message.confirm.removemultiple = Opravdu chcete odebrat %count% polo\u017eek\n a v\u0161echny objekty kter\u00e9 na nich z\u00e1vis\u00ed? + +menu.tools.searchcache = Prohledat cache prohl\u00ed\u017ee\u010d\u016f \ No newline at end of file diff --git a/trunk/src/com/jpexs/helpers/Helper.java b/trunk/src/com/jpexs/helpers/Helper.java index 1add4c3ca..706ab15f1 100644 --- a/trunk/src/com/jpexs/helpers/Helper.java +++ b/trunk/src/com/jpexs/helpers/Helper.java @@ -25,6 +25,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.UnsupportedEncodingException; @@ -560,4 +561,14 @@ public class Helper { } return false; } + + public static void saveStream(InputStream is,File output) throws IOException { + byte[] buf=new byte[1024]; + int cnt; + try (FileOutputStream fos = new FileOutputStream(output)) { + while((cnt=is.read(buf))>0){ + fos.write(buf,0,cnt); + } + } + } }