1 // Copyright (C) 2003 Adam Megacz <adam@ibex.org> all rights reserved.
3 // You may modify, copy, and redistribute this code under the terms of
4 // the GNU Library Public License version 2.1, with the exception of
5 // the portion of clause 6a after the semicolon (aka the "obnoxious
12 import java.util.zip.*;
14 /** Reads a CAB file structure */
17 /** reads a CAB file, parses it, and returns an InputStream representing the named file */
18 public static InputStream getFileInputStream(InputStream is, String fileName) throws IOException, EOFException {
19 return getFileInputStream(is, 0, fileName);
22 public static InputStream getFileInputStream(InputStream is, int skipHeaders, String fileName) throws IOException, EOFException {
23 DataInputStream dis = new DataInputStream(is);
24 CFHEADER h = new CFHEADER();
26 while (skipHeaders > 0) { CFHEADER.seekMSCF(dis); skipHeaders--; }
30 } catch (CFHEADER.BogusHeaderException bhe) {
31 throw new EOFException();
34 for(int i=0; i<h.folders.length; i++) {
35 CFFOLDER f = new CFFOLDER(h);
38 } catch (CFFOLDER.UnsupportedCompressionTypeException ucte) {
43 for(int i=0; i<h.files.length; i++) {
44 CFFILE f = new CFFILE(h);
48 for(int i=0; i<h.folders.length; i++) {
49 InputStream is2 = new CFFOLDERInputStream(h.folders[i], dis);
50 for(int j=0; j<h.folders[i].files.size(); j++) {
51 CFFILE file = (CFFILE)h.folders[i].files.elementAt(j);
52 if (file.fileName.equals(fileName)) return new LimitStream(is2, file.fileSize);
53 byte[] b = new byte[file.fileSize];
54 int read = is2.read(b);
61 private static class LimitStream extends FilterInputStream {
63 public LimitStream(InputStream is, int limit) {
67 public int read() throws IOException {
68 if (limit == 0) return -1;
69 int ret = super.read();
70 if (ret != -1) limit--;
73 public int read(byte[] b, int off, int len) throws IOException {
74 if (len > limit) len = limit;
75 if (limit == 0) return -1;
76 int ret = super.read(b, off, len);
82 /** Encapsulates a CFHEADER entry */
83 public static class CFHEADER {
84 byte[] reserved1 = new byte[4]; // reserved
85 int fileSize = 0; // size of this cabinet file in bytes
86 byte[] reserved2 = new byte[4]; // reserved
87 int offsetOfFirstCFFILEEntry; // offset of the first CFFILE entry
88 byte[] reserved3 = new byte[4]; // reserved
89 byte versionMinor = 3; // cabinet file format version, minor
90 byte versionMajor = 1; // cabinet file format version, major
91 boolean prevCAB = false; // true iff there is a cabinet before this one in a sequence
92 boolean nextCAB = false; // true iff there is a cabinet after this one in a sequence
93 boolean hasReserved = false; // true iff the cab has per-{cabinet, folder, block} reserved areas
94 int setID = 0; // must be the same for all cabinets in a set
95 int indexInCabinetSet = 0; // number of this cabinet file in a set
96 byte perCFFOLDERReservedSize = 0; // (optional) size of per-folder reserved area
97 byte perDatablockReservedSize = 0; // (optional) size of per-datablock reserved area
98 byte[] perCabinetReservedArea = null; // per-cabinet reserved area
99 String previousCabinet = null; // name of previous cabinet file in a set
100 String previousDisk = null; // name of previous disk in a set
101 String nextCabinet = null; // name of next cabinet in a set
102 String nextDisk = null; // name of next disk in a set
104 CFFOLDER[] folders = new CFFOLDER[0];
105 CFFILE[] files = new CFFILE[0];
107 int readCFFOLDERs = 0; // the number of folders read in so far
108 int readCFFILEs = 0; // the number of folders read in so far
110 public CFHEADER() { }
112 public void print(PrintStream ps) {
113 ps.println("CAB CFFILE CFHEADER v" + ((int)versionMajor) + "." + ((int)versionMinor));
114 ps.println(" total file size = " + fileSize);
115 ps.println(" offset of first file = " + offsetOfFirstCFFILEEntry);
116 ps.println(" total folders = " + folders.length);
117 ps.println(" total files = " + files.length);
118 ps.println(" flags = 0x" +
119 Integer.toString((prevCAB ? 0x1 : 0x0) |
120 (nextCAB ? 0x2 : 0x0) |
121 (hasReserved ? 0x4 : 0x0), 16) + " [ " +
122 (prevCAB ? "prev " : "") +
123 (nextCAB ? "next " : "") +
124 (hasReserved ? "reserve_present " : "") + "]");
125 ps.println(" set id = " + setID);
126 ps.println(" index in set = " + indexInCabinetSet);
127 ps.println(" header reserved area #1 =" +
128 " 0x" + Integer.toString(reserved1[0], 16) +
129 " 0x" + Integer.toString(reserved1[1], 16) +
130 " 0x" + Integer.toString(reserved1[2], 16) +
131 " 0x" + Integer.toString(reserved1[3], 16));
132 ps.println(" header reserved area #2 =" +
133 " 0x" + Integer.toString(reserved2[0], 16) +
134 " 0x" + Integer.toString(reserved2[1], 16) +
135 " 0x" + Integer.toString(reserved2[2], 16) +
136 " 0x" + Integer.toString(reserved2[3], 16));
137 ps.println(" header reserved area #3 =" +
138 " 0x" + Integer.toString(reserved3[0], 16) +
139 " 0x" + Integer.toString(reserved3[1], 16) +
140 " 0x" + Integer.toString(reserved3[2], 16) +
141 " 0x" + Integer.toString(reserved3[3], 16));
143 if (perCabinetReservedArea != null) {
144 ps.print(" per-cabinet reserved area = ");
145 for(int i=0; i<perCabinetReservedArea.length; i++)
146 ps.print(((perCabinetReservedArea[i] & 0xff) < 16 ? "0" : "") +
147 Integer.toString(perCabinetReservedArea[i] & 0xff, 16) + " ");
150 ps.println(" per folder reserved area = " + perCFFOLDERReservedSize + " bytes");
151 ps.println(" per block reserved area = " + perDatablockReservedSize + " bytes");
155 public String toString() {
157 "[ CAB CFFILE CFHEADER v" +
158 ((int)versionMajor) + "." + ((int)versionMinor) + ", " +
159 fileSize + " bytes, " +
160 folders.length + " folders, " +
161 files.length + " files] ";
164 /** fills in all fields in the header and positions the stream at the first folder */
165 public void read(DataInputStream dis) throws IOException, BogusHeaderException {
167 dis.readFully(reserved1);
169 byte[] headerHashable = new byte[28];
170 dis.readFully(headerHashable);
171 DataInputStream hhis = new DataInputStream(new ByteArrayInputStream(headerHashable));
173 fileSize = readLittleInt(hhis);
174 hhis.readFully(reserved2);
175 offsetOfFirstCFFILEEntry = readLittleInt(hhis);
176 hhis.readFully(reserved3);
177 versionMinor = hhis.readByte();
178 versionMajor = hhis.readByte();
179 folders = new CFFOLDER[readLittleShort(hhis)];
180 files = new CFFILE[readLittleShort(hhis)];
181 int flags = readLittleShort(hhis);
182 prevCAB = (flags & 0x0001) != 0;
183 nextCAB = (flags & 0x0002) != 0;
184 hasReserved = (flags & 0x0004) != 0;
185 setID = readLittleShort(hhis);
186 indexInCabinetSet = readLittleShort(hhis);
188 if (offsetOfFirstCFFILEEntry < 0 || fileSize < 0) {
189 throw new BogusHeaderException();
193 perCabinetReservedArea = new byte[readLittleShort(dis)];
194 perCFFOLDERReservedSize = dis.readByte();
195 perDatablockReservedSize = dis.readByte();
196 if (perCabinetReservedArea.length > 0)
197 dis.readFully(perCabinetReservedArea);
202 previousCabinet = readZeroTerminatedString(dis);
203 previousDisk = readZeroTerminatedString(dis);
206 nextCabinet = readZeroTerminatedString(dis);
207 nextDisk = readZeroTerminatedString(dis);
209 } catch (ArrayIndexOutOfBoundsException e) {
210 throw new BogusHeaderException();
214 public static void seekMSCF(DataInputStream dis) throws EOFException, IOException
217 // skip up to and including the 'MSCF' signature
221 while (state == 0 && dis.readByte() != 0x4D) { }
224 switch (dis.readByte()) {
225 case 0x53 : state = 2; break;
226 case 0x4D : state = 1; continue;
227 default : state = 0; continue;
230 if (dis.readByte() == 0x43) { state = 3; }
231 else { state = 0; continue; }
233 if (dis.readByte() == 0x46) { state = 4; }
238 public static class BogusHeaderException extends IOException {}
241 /** Encapsulates a CFFOLDER entry */
242 public static class CFFOLDER {
243 public static final int COMPRESSION_NONE = 0;
244 public static final int COMPRESSION_MSZIP = 1;
245 public static final int COMPRESSION_QUANTUM = 2;
246 public static final int COMPRESSION_LZX = 3;
248 int firstBlockOffset = 0; // offset of first data block within this folder
249 int numBlocks = 0; // number of data blocks
250 int compressionType = 0; // compression type for this folder
251 byte[] reservedArea = null; // per-folder reserved area
252 int indexInCFHEADER = 0; // our index in CFHEADER.folders
253 Vector files = new Vector();
255 private CFHEADER header = null;
257 public CFFOLDER(CFHEADER header) { this.header = header; }
259 public String toString() {
260 return "[ CAB CFFOLDER, " + numBlocks + " data blocks, compression type " +
261 compressionName(compressionType) +
262 ", " + reservedArea.length + " bytes of reserved data ]";
265 public void read(DataInputStream dis) throws IOException, UnsupportedCompressionTypeException {
266 firstBlockOffset = readLittleInt(dis);
267 numBlocks = readLittleShort(dis);
268 compressionType = readLittleShort(dis) & 0x000F;
269 if (compressionType != COMPRESSION_MSZIP) {
270 throw new UnsupportedCompressionTypeException(compressionType);
272 reservedArea = new byte[header.perCFFOLDERReservedSize];
273 if (reservedArea.length > 0) dis.readFully(reservedArea);
274 indexInCFHEADER = header.readCFFOLDERs++;
275 header.folders[indexInCFHEADER] = this;
278 public static String compressionName(int type) {
280 case COMPRESSION_NONE:
282 case COMPRESSION_MSZIP:
284 case COMPRESSION_QUANTUM:
286 case COMPRESSION_LZX:
289 return "<Unknown type " + type + ">";
293 public static class UnsupportedCompressionTypeException extends IOException {
294 private int compressionType;
296 UnsupportedCompressionTypeException(int type) {
297 compressionType = type;
299 public String toString() {
300 return "UnsupportedCompressionTypeException: no support for compression type " + compressionName(compressionType);
305 /** Encapsulates a CFFILE entry */
306 public static class CFFILE {
307 int fileSize = 0; // size of this file
308 int uncompressedOffsetInCFFOLDER = 0; // offset of this file within the folder, not accounting for compression
309 int folderIndex = 0; // index of the CFFOLDER we belong to
310 Date date = null; // modification date
311 int attrs = 0; // attrs
312 boolean readOnly = false; // read-only flag
313 boolean hidden = false; // hidden flag
314 boolean system = false; // system flag
315 boolean arch = false; // archive flag
316 boolean runAfterExec = false; // true if file should be run during extraction
317 boolean UTFfileName = false; // true if filename is UTF-encoded
318 String fileName = null; // filename
319 int indexInCFHEADER = 0; // our index in CFHEADER.files
320 CFFOLDER folder = null; // the folder we belong to
321 private CFHEADER header = null;
324 public CFFILE(CFHEADER header) { this.header = header; }
326 public CFFILE(File f, String pathName) throws IOException {
327 fileSize = (int)f.length();
329 date = new java.util.Date(f.lastModified());
334 public String toString() {
335 return "[ CAB CFFILE: " + fileName + ", " + fileSize + " bytes [ " +
336 (readOnly ? "readonly " : "") +
337 (system ? "system " : "") +
338 (hidden ? "hidden " : "") +
339 (arch ? "arch " : "") +
340 (runAfterExec ? "run_after_exec " : "") +
341 (UTFfileName ? "UTF_filename " : "") +
345 public void read(DataInputStream dis) throws IOException {
346 fileSize = readLittleInt(dis);
347 uncompressedOffsetInCFFOLDER = readLittleInt(dis);
348 folderIndex = readLittleShort(dis);
349 readLittleShort(dis); // date
350 readLittleShort(dis); // time
351 attrs = readLittleShort(dis);
352 readOnly = (attrs & 0x1) != 0;
353 hidden = (attrs & 0x2) != 0;
354 system = (attrs & 0x4) != 0;
355 arch = (attrs & 0x20) != 0;
356 runAfterExec = (attrs & 0x40) != 0;
357 UTFfileName = (attrs & 0x80) != 0;
358 fileName = readZeroTerminatedString(dis);
360 indexInCFHEADER = header.readCFFILEs++;
361 header.files[indexInCFHEADER] = this;
362 folder = header.folders[folderIndex];
363 folder.files.addElement(this);
370 // Compressing Input and Output Streams ///////////////////////////////////////////////
372 /** an InputStream that decodes CFDATA blocks belonging to a CFFOLDER */
373 private static class CFFOLDERInputStream extends InputStream {
376 InputStream iis = null;
378 byte[] compressed = new byte[128 * 1024];
379 byte[] uncompressed = new byte[256 * 1024];
381 public CFFOLDERInputStream(CFFOLDER f, DataInputStream dis) {
386 InputStream readBlock() throws IOException {
387 int checksum = readLittleInt(dis);
388 int compressedBytes = readLittleShort(dis);
389 int unCompressedBytes = readLittleShort(dis);
390 byte[] reserved = new byte[/*folder.header.perDatablockReservedSize*/0];
391 if (reserved.length > 0) dis.readFully(reserved);
392 if (dis.readByte() != 0x43) throw new CABException("malformed block header");
393 if (dis.readByte() != 0x4B) throw new CABException("malformed block header");
395 dis.readFully(compressed, 0, compressedBytes - 2);
397 Inflater i = new Inflater(true);
398 i.setInput(compressed, 0, compressedBytes - 2);
400 if (unCompressedBytes > uncompressed.length) uncompressed = new byte[unCompressedBytes];
401 try { i.inflate(uncompressed, 0, uncompressed.length);
402 } catch (DataFormatException dfe) {
403 dfe.printStackTrace();
404 throw new CABException(dfe.toString());
406 return new ByteArrayInputStream(uncompressed, 0, unCompressedBytes);
409 public int available() throws IOException { return iis == null ? 0 : iis.available(); }
410 public void close() throws IOException { iis.close(); }
411 public void mark(int i) { }
412 public boolean markSupported() { return false; }
413 public void reset() { }
415 public long skip(long l) throws IOException {
416 if (iis == null) iis = readBlock();
419 long numread = iis.skip(l - ret);
420 if (numread == 0 || numread == -1) iis = readBlock();
426 public int read(byte[] b, int off, int len) throws IOException {
427 if (iis == null) iis = readBlock();
430 int numread = iis.read(b, off + ret, len - ret);
431 if (numread == 0 || numread == -1) iis = readBlock();
437 public int read() throws IOException {
438 if (iis == null) iis = readBlock();
439 int ret = iis.read();
450 // Misc Stuff //////////////////////////////////////////////////////////////
452 public static String readZeroTerminatedString(DataInputStream dis) throws IOException {
454 byte[] b = new byte[256];
456 byte next = dis.readByte();
457 if (next == 0x0) return new String(b, 0, numBytes);
458 b[numBytes++] = next;
462 public static int readLittleInt(DataInputStream dis) throws IOException {
463 int lowest = (int)(dis.readByte() & 0xff);
464 int low = (int)(dis.readByte() & 0xff);
465 int high = (int)(dis.readByte() & 0xff);
466 int highest = (int)(dis.readByte() & 0xff);
467 return (highest << 24) | (high << 16) | (low << 8) | lowest;
470 public static int readLittleShort(DataInputStream dis) throws IOException {
471 int low = (int)(dis.readByte() & 0xff);
472 int high = (int)(dis.readByte() & 0xff);
473 return (high << 8) | low;
476 public static class CABException extends IOException {
477 public CABException(String s) { super(s); }
481 /** scratch space for isToByteArray() */
482 static byte[] workspace = new byte[16 * 1024];
484 /** Trivial method to completely read an InputStream */
485 public static synchronized byte[] isToByteArray(InputStream is) throws IOException {
488 int numread = is.read(workspace, pos, workspace.length - pos);
489 if (numread == -1) break;
490 else if (pos + numread < workspace.length) pos += numread;
493 byte[] temp = new byte[workspace.length * 2];
494 System.arraycopy(workspace, 0, temp, 0, workspace.length);
498 byte[] ret = new byte[pos];
499 System.arraycopy(workspace, 0, ret, 0, pos);