--- /dev/null
+// Copyright (C) 2003 Adam Megacz <adam@ibex.org> all rights reserved.
+//
+// You may modify, copy, and redistribute this code under the terms of
+// the GNU Library Public License version 2.1, with the exception of
+// the portion of clause 6a after the semicolon (aka the "obnoxious
+// relink clause")
+
+package org.ibex.util;
+
+import java.io.*;
+import java.util.*;
+import java.util.zip.*;
+
+/** Reads a CAB file structure */
+public class CAB {
+
+ /** reads a CAB file, parses it, and returns an InputStream representing the named file */
+ public static InputStream getFileInputStream(InputStream is, String fileName) throws IOException, EOFException {
+ return getFileInputStream(is, 0, fileName);
+ }
+
+ public static InputStream getFileInputStream(InputStream is, int skipHeaders, String fileName) throws IOException, EOFException {
+ DataInputStream dis = new DataInputStream(is);
+ CFHEADER h = new CFHEADER();
+
+ while (skipHeaders > 0) { CFHEADER.seekMSCF(dis); skipHeaders--; }
+
+ try {
+ h.read(dis);
+ } catch (CFHEADER.BogusHeaderException bhe) {
+ throw new EOFException();
+ }
+
+ for(int i=0; i<h.folders.length; i++) {
+ CFFOLDER f = new CFFOLDER(h);
+ try {
+ f.read(dis);
+ } catch (CFFOLDER.UnsupportedCompressionTypeException ucte) {
+ throw ucte;
+ }
+ }
+
+ for(int i=0; i<h.files.length; i++) {
+ CFFILE f = new CFFILE(h);
+ f.read(dis);
+ }
+
+ for(int i=0; i<h.folders.length; i++) {
+ InputStream is2 = new CFFOLDERInputStream(h.folders[i], dis);
+ for(int j=0; j<h.folders[i].files.size(); j++) {
+ CFFILE file = (CFFILE)h.folders[i].files.elementAt(j);
+ if (file.fileName.equals(fileName)) return new LimitStream(is2, file.fileSize);
+ byte[] b = new byte[file.fileSize];
+ int read = is2.read(b);
+ }
+ }
+
+ return null;
+ }
+
+ private static class LimitStream extends FilterInputStream {
+ int limit;
+ public LimitStream(InputStream is, int limit) {
+ super(is);
+ this.limit = limit;
+ }
+ public int read() throws IOException {
+ if (limit == 0) return -1;
+ int ret = super.read();
+ if (ret != -1) limit--;
+ return ret;
+ }
+ public int read(byte[] b, int off, int len) throws IOException {
+ if (len > limit) len = limit;
+ if (limit == 0) return -1;
+ int ret = super.read(b, off, len);
+ limit -= ret;
+ return ret;
+ }
+ }
+
+ /** Encapsulates a CFHEADER entry */
+ public static class CFHEADER {
+ byte[] reserved1 = new byte[4]; // reserved
+ int fileSize = 0; // size of this cabinet file in bytes
+ byte[] reserved2 = new byte[4]; // reserved
+ int offsetOfFirstCFFILEEntry; // offset of the first CFFILE entry
+ byte[] reserved3 = new byte[4]; // reserved
+ byte versionMinor = 3; // cabinet file format version, minor
+ byte versionMajor = 1; // cabinet file format version, major
+ boolean prevCAB = false; // true iff there is a cabinet before this one in a sequence
+ boolean nextCAB = false; // true iff there is a cabinet after this one in a sequence
+ boolean hasReserved = false; // true iff the cab has per-{cabinet, folder, block} reserved areas
+ int setID = 0; // must be the same for all cabinets in a set
+ int indexInCabinetSet = 0; // number of this cabinet file in a set
+ byte perCFFOLDERReservedSize = 0; // (optional) size of per-folder reserved area
+ byte perDatablockReservedSize = 0; // (optional) size of per-datablock reserved area
+ byte[] perCabinetReservedArea = null; // per-cabinet reserved area
+ String previousCabinet = null; // name of previous cabinet file in a set
+ String previousDisk = null; // name of previous disk in a set
+ String nextCabinet = null; // name of next cabinet in a set
+ String nextDisk = null; // name of next disk in a set
+
+ CFFOLDER[] folders = new CFFOLDER[0];
+ CFFILE[] files = new CFFILE[0];
+
+ int readCFFOLDERs = 0; // the number of folders read in so far
+ int readCFFILEs = 0; // the number of folders read in so far
+
+ public CFHEADER() { }
+
+ public void print(PrintStream ps) {
+ ps.println("CAB CFFILE CFHEADER v" + ((int)versionMajor) + "." + ((int)versionMinor));
+ ps.println(" total file size = " + fileSize);
+ ps.println(" offset of first file = " + offsetOfFirstCFFILEEntry);
+ ps.println(" total folders = " + folders.length);
+ ps.println(" total files = " + files.length);
+ ps.println(" flags = 0x" +
+ Integer.toString((prevCAB ? 0x1 : 0x0) |
+ (nextCAB ? 0x2 : 0x0) |
+ (hasReserved ? 0x4 : 0x0), 16) + " [ " +
+ (prevCAB ? "prev " : "") +
+ (nextCAB ? "next " : "") +
+ (hasReserved ? "reserve_present " : "") + "]");
+ ps.println(" set id = " + setID);
+ ps.println(" index in set = " + indexInCabinetSet);
+ ps.println(" header reserved area #1 =" +
+ " 0x" + Integer.toString(reserved1[0], 16) +
+ " 0x" + Integer.toString(reserved1[1], 16) +
+ " 0x" + Integer.toString(reserved1[2], 16) +
+ " 0x" + Integer.toString(reserved1[3], 16));
+ ps.println(" header reserved area #2 =" +
+ " 0x" + Integer.toString(reserved2[0], 16) +
+ " 0x" + Integer.toString(reserved2[1], 16) +
+ " 0x" + Integer.toString(reserved2[2], 16) +
+ " 0x" + Integer.toString(reserved2[3], 16));
+ ps.println(" header reserved area #3 =" +
+ " 0x" + Integer.toString(reserved3[0], 16) +
+ " 0x" + Integer.toString(reserved3[1], 16) +
+ " 0x" + Integer.toString(reserved3[2], 16) +
+ " 0x" + Integer.toString(reserved3[3], 16));
+ if (hasReserved) {
+ if (perCabinetReservedArea != null) {
+ ps.print(" per-cabinet reserved area = ");
+ for(int i=0; i<perCabinetReservedArea.length; i++)
+ ps.print(((perCabinetReservedArea[i] & 0xff) < 16 ? "0" : "") +
+ Integer.toString(perCabinetReservedArea[i] & 0xff, 16) + " ");
+ ps.println();
+ }
+ ps.println(" per folder reserved area = " + perCFFOLDERReservedSize + " bytes");
+ ps.println(" per block reserved area = " + perDatablockReservedSize + " bytes");
+ }
+ }
+
+ public String toString() {
+ return
+ "[ CAB CFFILE CFHEADER v" +
+ ((int)versionMajor) + "." + ((int)versionMinor) + ", " +
+ fileSize + " bytes, " +
+ folders.length + " folders, " +
+ files.length + " files] ";
+ }
+
+ /** fills in all fields in the header and positions the stream at the first folder */
+ public void read(DataInputStream dis) throws IOException, BogusHeaderException {
+ seekMSCF(dis);
+ dis.readFully(reserved1);
+
+ byte[] headerHashable = new byte[28];
+ dis.readFully(headerHashable);
+ DataInputStream hhis = new DataInputStream(new ByteArrayInputStream(headerHashable));
+
+ fileSize = readLittleInt(hhis);
+ hhis.readFully(reserved2);
+ offsetOfFirstCFFILEEntry = readLittleInt(hhis);
+ hhis.readFully(reserved3);
+ versionMinor = hhis.readByte();
+ versionMajor = hhis.readByte();
+ folders = new CFFOLDER[readLittleShort(hhis)];
+ files = new CFFILE[readLittleShort(hhis)];
+ int flags = readLittleShort(hhis);
+ prevCAB = (flags & 0x0001) != 0;
+ nextCAB = (flags & 0x0002) != 0;
+ hasReserved = (flags & 0x0004) != 0;
+ setID = readLittleShort(hhis);
+ indexInCabinetSet = readLittleShort(hhis);
+
+ if (offsetOfFirstCFFILEEntry < 0 || fileSize < 0) {
+ throw new BogusHeaderException();
+ }
+
+ if (hasReserved) {
+ perCabinetReservedArea = new byte[readLittleShort(dis)];
+ perCFFOLDERReservedSize = dis.readByte();
+ perDatablockReservedSize = dis.readByte();
+ if (perCabinetReservedArea.length > 0)
+ dis.readFully(perCabinetReservedArea);
+ }
+
+ try {
+ if (prevCAB) {
+ previousCabinet = readZeroTerminatedString(dis);
+ previousDisk = readZeroTerminatedString(dis);
+ }
+ if (nextCAB) {
+ nextCabinet = readZeroTerminatedString(dis);
+ nextDisk = readZeroTerminatedString(dis);
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new BogusHeaderException();
+ }
+ }
+
+ public static void seekMSCF(DataInputStream dis) throws EOFException, IOException
+ {
+ int state;
+ // skip up to and including the 'MSCF' signature
+ state = 0;
+ while (state != 4) {
+ // M
+ while (state == 0 && dis.readByte() != 0x4D) { }
+ state = 1;
+ // S
+ switch (dis.readByte()) {
+ case 0x53 : state = 2; break;
+ case 0x4D : state = 1; continue;
+ default : state = 0; continue;
+ }
+ // C
+ if (dis.readByte() == 0x43) { state = 3; }
+ else { state = 0; continue; }
+ // F
+ if (dis.readByte() == 0x46) { state = 4; }
+ else { state = 0; }
+ }
+ }
+
+ public static class BogusHeaderException extends IOException {}
+ }
+
+ /** Encapsulates a CFFOLDER entry */
+ public static class CFFOLDER {
+ public static final int COMPRESSION_NONE = 0;
+ public static final int COMPRESSION_MSZIP = 1;
+ public static final int COMPRESSION_QUANTUM = 2;
+ public static final int COMPRESSION_LZX = 3;
+
+ int firstBlockOffset = 0; // offset of first data block within this folder
+ int numBlocks = 0; // number of data blocks
+ int compressionType = 0; // compression type for this folder
+ byte[] reservedArea = null; // per-folder reserved area
+ int indexInCFHEADER = 0; // our index in CFHEADER.folders
+ Vector files = new Vector();
+
+ private CFHEADER header = null;
+
+ public CFFOLDER(CFHEADER header) { this.header = header; }
+
+ public String toString() {
+ return "[ CAB CFFOLDER, " + numBlocks + " data blocks, compression type " +
+ compressionName(compressionType) +
+ ", " + reservedArea.length + " bytes of reserved data ]";
+ }
+
+ public void read(DataInputStream dis) throws IOException, UnsupportedCompressionTypeException {
+ firstBlockOffset = readLittleInt(dis);
+ numBlocks = readLittleShort(dis);
+ compressionType = readLittleShort(dis) & 0x000F;
+ if (compressionType != COMPRESSION_MSZIP) {
+ throw new UnsupportedCompressionTypeException(compressionType);
+ }
+ reservedArea = new byte[header.perCFFOLDERReservedSize];
+ if (reservedArea.length > 0) dis.readFully(reservedArea);
+ indexInCFHEADER = header.readCFFOLDERs++;
+ header.folders[indexInCFHEADER] = this;
+ }
+
+ public static String compressionName(int type) {
+ switch (type) {
+ case COMPRESSION_NONE:
+ return "NONE";
+ case COMPRESSION_MSZIP:
+ return "MSZIP";
+ case COMPRESSION_QUANTUM:
+ return "QUANTUM";
+ case COMPRESSION_LZX:
+ return "LZX";
+ default:
+ return "<Unknown type " + type + ">";
+ }
+ }
+
+ public static class UnsupportedCompressionTypeException extends IOException {
+ private int compressionType;
+
+ UnsupportedCompressionTypeException(int type) {
+ compressionType = type;
+ }
+ public String toString() {
+ return "UnsupportedCompressionTypeException: no support for compression type " + compressionName(compressionType);
+ }
+ }
+ }
+
+ /** Encapsulates a CFFILE entry */
+ public static class CFFILE {
+ int fileSize = 0; // size of this file
+ int uncompressedOffsetInCFFOLDER = 0; // offset of this file within the folder, not accounting for compression
+ int folderIndex = 0; // index of the CFFOLDER we belong to
+ Date date = null; // modification date
+ int attrs = 0; // attrs
+ boolean readOnly = false; // read-only flag
+ boolean hidden = false; // hidden flag
+ boolean system = false; // system flag
+ boolean arch = false; // archive flag
+ boolean runAfterExec = false; // true if file should be run during extraction
+ boolean UTFfileName = false; // true if filename is UTF-encoded
+ String fileName = null; // filename
+ int indexInCFHEADER = 0; // our index in CFHEADER.files
+ CFFOLDER folder = null; // the folder we belong to
+ private CFHEADER header = null;
+ File myFile;
+
+ public CFFILE(CFHEADER header) { this.header = header; }
+
+ public CFFILE(File f, String pathName) throws IOException {
+ fileSize = (int)f.length();
+ folderIndex = 0;
+ date = new java.util.Date(f.lastModified());
+ fileName = pathName;
+ myFile = f;
+ }
+
+ public String toString() {
+ return "[ CAB CFFILE: " + fileName + ", " + fileSize + " bytes [ " +
+ (readOnly ? "readonly " : "") +
+ (system ? "system " : "") +
+ (hidden ? "hidden " : "") +
+ (arch ? "arch " : "") +
+ (runAfterExec ? "run_after_exec " : "") +
+ (UTFfileName ? "UTF_filename " : "") +
+ "]";
+ }
+
+ public void read(DataInputStream dis) throws IOException {
+ fileSize = readLittleInt(dis);
+ uncompressedOffsetInCFFOLDER = readLittleInt(dis);
+ folderIndex = readLittleShort(dis);
+ readLittleShort(dis); // date
+ readLittleShort(dis); // time
+ attrs = readLittleShort(dis);
+ readOnly = (attrs & 0x1) != 0;
+ hidden = (attrs & 0x2) != 0;
+ system = (attrs & 0x4) != 0;
+ arch = (attrs & 0x20) != 0;
+ runAfterExec = (attrs & 0x40) != 0;
+ UTFfileName = (attrs & 0x80) != 0;
+ fileName = readZeroTerminatedString(dis);
+
+ indexInCFHEADER = header.readCFFILEs++;
+ header.files[indexInCFHEADER] = this;
+ folder = header.folders[folderIndex];
+ folder.files.addElement(this);
+ }
+ }
+
+
+
+
+ // Compressing Input and Output Streams ///////////////////////////////////////////////
+
+ /** an InputStream that decodes CFDATA blocks belonging to a CFFOLDER */
+ private static class CFFOLDERInputStream extends InputStream {
+ CFFOLDER folder;
+ DataInputStream dis;
+ InputStream iis = null;
+
+ byte[] compressed = new byte[128 * 1024];
+ byte[] uncompressed = new byte[256 * 1024];
+
+ public CFFOLDERInputStream(CFFOLDER f, DataInputStream dis) {
+ this.folder = f;
+ this.dis = dis;
+ }
+
+ InputStream readBlock() throws IOException {
+ int checksum = readLittleInt(dis);
+ int compressedBytes = readLittleShort(dis);
+ int unCompressedBytes = readLittleShort(dis);
+ byte[] reserved = new byte[/*folder.header.perDatablockReservedSize*/0];
+ if (reserved.length > 0) dis.readFully(reserved);
+ if (dis.readByte() != 0x43) throw new CABException("malformed block header");
+ if (dis.readByte() != 0x4B) throw new CABException("malformed block header");
+
+ dis.readFully(compressed, 0, compressedBytes - 2);
+
+ Inflater i = new Inflater(true);
+ i.setInput(compressed, 0, compressedBytes - 2);
+
+ if (unCompressedBytes > uncompressed.length) uncompressed = new byte[unCompressedBytes];
+ try { i.inflate(uncompressed, 0, uncompressed.length);
+ } catch (DataFormatException dfe) {
+ dfe.printStackTrace();
+ throw new CABException(dfe.toString());
+ }
+ return new ByteArrayInputStream(uncompressed, 0, unCompressedBytes);
+ }
+
+ public int available() throws IOException { return iis == null ? 0 : iis.available(); }
+ public void close() throws IOException { iis.close(); }
+ public void mark(int i) { }
+ public boolean markSupported() { return false; }
+ public void reset() { }
+
+ public long skip(long l) throws IOException {
+ if (iis == null) iis = readBlock();
+ int ret = 0;
+ while (l > ret) {
+ long numread = iis.skip(l - ret);
+ if (numread == 0 || numread == -1) iis = readBlock();
+ else ret += numread;
+ }
+ return ret;
+ }
+
+ public int read(byte[] b, int off, int len) throws IOException {
+ if (iis == null) iis = readBlock();
+ int ret = 0;
+ while (len > ret) {
+ int numread = iis.read(b, off + ret, len - ret);
+ if (numread == 0 || numread == -1) iis = readBlock();
+ else ret += numread;
+ }
+ return ret;
+ }
+
+ public int read() throws IOException {
+ if (iis == null) iis = readBlock();
+ int ret = iis.read();
+ if (ret == -1) {
+ iis = readBlock();
+ ret = iis.read();
+ }
+ return ret;
+ }
+ }
+
+
+
+ // Misc Stuff //////////////////////////////////////////////////////////////
+
+ public static String readZeroTerminatedString(DataInputStream dis) throws IOException {
+ int numBytes = 0;
+ byte[] b = new byte[256];
+ while(true) {
+ byte next = dis.readByte();
+ if (next == 0x0) return new String(b, 0, numBytes);
+ b[numBytes++] = next;
+ }
+ }
+
+ public static int readLittleInt(DataInputStream dis) throws IOException {
+ int lowest = (int)(dis.readByte() & 0xff);
+ int low = (int)(dis.readByte() & 0xff);
+ int high = (int)(dis.readByte() & 0xff);
+ int highest = (int)(dis.readByte() & 0xff);
+ return (highest << 24) | (high << 16) | (low << 8) | lowest;
+ }
+
+ public static int readLittleShort(DataInputStream dis) throws IOException {
+ int low = (int)(dis.readByte() & 0xff);
+ int high = (int)(dis.readByte() & 0xff);
+ return (high << 8) | low;
+ }
+
+ public static class CABException extends IOException {
+ public CABException(String s) { super(s); }
+ }
+
+
+ /** scratch space for isToByteArray() */
+ static byte[] workspace = new byte[16 * 1024];
+
+ /** Trivial method to completely read an InputStream */
+ public static synchronized byte[] isToByteArray(InputStream is) throws IOException {
+ int pos = 0;
+ while (true) {
+ int numread = is.read(workspace, pos, workspace.length - pos);
+ if (numread == -1) break;
+ else if (pos + numread < workspace.length) pos += numread;
+ else {
+ pos += numread;
+ byte[] temp = new byte[workspace.length * 2];
+ System.arraycopy(workspace, 0, temp, 0, workspace.length);
+ workspace = temp;
+ }
+ }
+ byte[] ret = new byte[pos];
+ System.arraycopy(workspace, 0, ret, 0, pos);
+ return ret;
+ }
+
+
+}
+