2003/09/24 07:33:33
[org.ibex.core.git] / src / org / xwt / util / CAB.java
1 // Copyright 2002 Adam Megacz, see the COPYING file for licensing [GPL]
2 package org.xwt.util;
3
4 import java.io.*;
5 import java.util.*;
6 import java.util.zip.*;
7 import java.math.*;
8
9 /** Reads a CAB file structure */
10 public class CAB {
11
12     /** reads a CAB file, parses it, and returns an InputStream representing the named file */
13     public static InputStream getFileInputStream(InputStream is, String fileName, boolean mscfAlreadyConsumed) throws IOException {
14         DataInputStream dis = new DataInputStream(is);
15         CFHEADER h = new CFHEADER();
16         h.read(dis, mscfAlreadyConsumed);
17
18         for(int i=0; i<h.folders.length; i++) {
19             CFFOLDER f = new CFFOLDER(h);
20             f.read(dis);
21         }
22
23         for(int i=0; i<h.files.length; i++) {
24             CFFILE f = new CFFILE(h);
25             f.read(dis);
26         }
27         
28         for(int i=0; i<h.folders.length; i++) {
29             InputStream is2 = new CFFOLDERInputStream(h.folders[i], dis);
30             for(int j=0; j<h.folders[i].files.size(); j++) {
31                 CFFILE file = (CFFILE)h.folders[i].files.elementAt(j);
32                 if (file.fileName.equals(fileName)) return new LimitStream(is2, file.fileSize);
33                 byte[] b = new byte[file.fileSize];
34                 int read = is2.read(b);
35             }
36         }
37
38         return null;
39     }
40
41     private static class LimitStream extends FilterInputStream {
42         int limit;
43         public LimitStream(InputStream is, int limit) {
44             super(is);
45             this.limit = limit;
46         }
47         public int read() throws IOException {
48             if (limit == 0) return -1;
49             int ret = super.read();
50             if (ret != -1) limit--;
51             return ret;
52         }
53         public int read(byte[] b, int off, int len) throws IOException {
54             if (len > limit) len = limit;
55             if (limit == 0) return -1;
56             int ret = super.read(b, off, len);
57             limit -= ret;
58             return ret;
59         }
60     }
61     
62     /** Encapsulates a CFHEADER entry */
63     public static class CFHEADER {
64         byte[]   reserved1 = new byte[4];       // reserved
65         int      fileSize = 0;                  // size of this cabinet file in bytes
66         byte[]   reserved2 = new byte[4];       // reserved
67         int      offsetOfFirstCFFILEEntry;      // offset of the first CFFILE entry
68         byte[]   reserved3 = new byte[4];       // reserved
69         byte     versionMinor = 3;              // cabinet file format version, minor
70         byte     versionMajor = 1;              // cabinet file format version, major
71         boolean  prevCAB = false;               // true iff there is a cabinet before this one in a sequence
72         boolean  nextCAB = false;               // true iff there is a cabinet after this one in a sequence
73         boolean  hasReserved = false;           // true iff the cab has per-{cabinet, folder, block} reserved areas
74         int      setID = 0;                     // must be the same for all cabinets in a set
75         int      indexInCabinetSet = 0;         // number of this cabinet file in a set
76         byte     perCFFOLDERReservedSize = 0;   // (optional) size of per-folder reserved area
77         byte     perDatablockReservedSize = 0;  // (optional) size of per-datablock reserved area
78         byte[]   perCabinetReservedArea = null; // per-cabinet reserved area
79         String   previousCabinet = null;        // name of previous cabinet file in a set
80         String   previousDisk = null;           // name of previous disk in a set
81         String   nextCabinet = null;            // name of next cabinet in a set
82         String   nextDisk = null;               // name of next disk in a set
83
84         CFFOLDER[] folders = new CFFOLDER[0];
85         CFFILE[] files = new CFFILE[0];
86
87         int readCFFOLDERs = 0;                    // the number of folders read in so far
88         int readCFFILEs = 0;                      // the number of folders read in so far
89
90         public CFHEADER() { }
91
92         public void print(PrintStream ps) {
93             ps.println("CAB CFFILE CFHEADER v" + ((int)versionMajor) + "." + ((int)versionMinor));
94             ps.println("    total file size               = " + fileSize);
95             ps.println("    offset of first file          = " + offsetOfFirstCFFILEEntry);
96             ps.println("    total folders                 = " + folders.length);
97             ps.println("    total files                   = " + files.length);
98             ps.println("    flags                         = 0x" +
99                        Integer.toString((prevCAB ? 0x1 : 0x0) |
100                                         (nextCAB ? 0x2 : 0x0) |
101                                         (hasReserved ? 0x4 : 0x0), 16) + " [ " +
102                        (prevCAB ? "prev " : "") + 
103                        (nextCAB ? "next " : "") + 
104                        (hasReserved ? "reserve_present " : "") + "]");
105             ps.println("    set id                        = " + setID);
106             ps.println("    index in set                  = " + indexInCabinetSet);
107             ps.println("    header reserved area #1       =" +
108                        " 0x" + Integer.toString(reserved1[0], 16) +
109                        " 0x" + Integer.toString(reserved1[1], 16) +
110                        " 0x" + Integer.toString(reserved1[2], 16) +
111                        " 0x" + Integer.toString(reserved1[3], 16));
112             ps.println("    header reserved area #2       =" +
113                        " 0x" + Integer.toString(reserved2[0], 16) +
114                        " 0x" + Integer.toString(reserved2[1], 16) +
115                        " 0x" + Integer.toString(reserved2[2], 16) +
116                        " 0x" + Integer.toString(reserved2[3], 16));
117             ps.println("    header reserved area #3       =" +
118                        " 0x" + Integer.toString(reserved3[0], 16) +
119                        " 0x" + Integer.toString(reserved3[1], 16) +
120                        " 0x" + Integer.toString(reserved3[2], 16) +
121                        " 0x" + Integer.toString(reserved3[3], 16));
122             if (hasReserved) {
123                 if (perCabinetReservedArea != null) {
124                     ps.print("    per-cabinet reserved area     = ");
125                     for(int i=0; i<perCabinetReservedArea.length; i++)
126                         ps.print(((perCabinetReservedArea[i] & 0xff) < 16 ? "0" : "") +
127                                  Integer.toString(perCabinetReservedArea[i] & 0xff, 16) + " ");
128                     ps.println();
129                 }
130                 ps.println("    per folder  reserved area     = " + perCFFOLDERReservedSize + " bytes");
131                 ps.println("    per block   reserved area     = " + perDatablockReservedSize + " bytes");
132             }
133         }
134
135         public String toString() {
136             return
137                 "[ CAB CFFILE CFHEADER v" +
138                 ((int)versionMajor) + "." + ((int)versionMinor) + ", " +
139                 fileSize + " bytes, " +
140                 folders.length + " folders, " +
141                 files.length + " files] ";
142         }
143
144         /** fills in all fields in the header and positions the stream at the first folder */
145         public void read(DataInputStream dis, boolean mscfAlreadyConsumed) throws IOException {
146             if (!mscfAlreadyConsumed) {
147                 if (dis.readByte() != 0x4d) throw new CABException("First byte of header signature malformed");
148                 if (dis.readByte() != 0x53) throw new CABException("Second byte of header signature malformed");
149                 if (dis.readByte() != 0x43) throw new CABException("Third byte of header signature malformed");
150                 if (dis.readByte() != 0x46) throw new CABException("Fourth byte of header signature malformed");
151             }
152             dis.readFully(reserved1);
153
154             byte[] headerHashable = new byte[28];
155             dis.readFully(headerHashable);
156             DataInputStream hhis = new DataInputStream(new ByteArrayInputStream(headerHashable));
157
158             fileSize = readLittleInt(hhis);
159             hhis.readFully(reserved2);
160             offsetOfFirstCFFILEEntry = readLittleInt(hhis);
161             hhis.readFully(reserved3);
162             versionMinor = hhis.readByte();
163             versionMajor = hhis.readByte();
164             folders = new CFFOLDER[readLittleShort(hhis)];
165             files = new CFFILE[readLittleShort(hhis)];
166             int flags = readLittleShort(hhis);
167             prevCAB = (flags & 0x0001) != 0;
168             nextCAB = (flags & 0x0002) != 0;
169             hasReserved = (flags & 0x0004) != 0;
170             setID = readLittleShort(hhis);
171             indexInCabinetSet = readLittleShort(hhis);
172
173             if (hasReserved) {
174                 perCabinetReservedArea = new byte[readLittleShort(dis)];
175                 perCFFOLDERReservedSize = dis.readByte();
176                 perDatablockReservedSize = dis.readByte();
177                 if (perCabinetReservedArea.length > 0)
178                     dis.readFully(perCabinetReservedArea);
179             }
180             if (prevCAB) {
181                 previousCabinet = readZeroTerminatedString(dis);
182                 previousDisk = readZeroTerminatedString(dis);
183             }
184             if (nextCAB) {
185                 nextCabinet = readZeroTerminatedString(dis);
186                 nextDisk = readZeroTerminatedString(dis);
187             }
188         }
189     }
190
191     /** Encapsulates a CFFOLDER entry */
192     public static class CFFOLDER {
193         int      firstBlockOffset = 0;          // offset of first data block within this folder
194         int      numBlocks = 0;                 // number of data blocks
195         int      compressionType = 0;           // compression type for this folder
196         byte[]   reservedArea = null;           // per-folder reserved area
197         int      indexInCFHEADER = 0;           // our index in CFHEADER.folders
198         Vector   files = new Vector();
199
200         private CFHEADER header = null;
201
202         public CFFOLDER(CFHEADER header) { this.header = header; }
203
204         public String toString() {
205             return "[ CAB CFFOLDER, " + numBlocks + " data blocks, compression type " +
206                 (compressionType == 1 ? "MSZIP" : "unknown") +
207                 ", " + reservedArea.length + " bytes of reserved data ]";
208         }
209
210         public void read(DataInputStream dis) throws IOException {
211             firstBlockOffset = readLittleInt(dis);
212             numBlocks = readLittleShort(dis);
213             compressionType = readLittleShort(dis);
214             reservedArea = new byte[header.perCFFOLDERReservedSize];
215             if (reservedArea.length > 0) dis.readFully(reservedArea);
216             indexInCFHEADER = header.readCFFOLDERs++;
217             header.folders[indexInCFHEADER] = this;
218         }
219     }
220
221     /** Encapsulates a CFFILE entry */
222     public static class CFFILE {
223         int fileSize = 0;                       // size of this file
224         int uncompressedOffsetInCFFOLDER = 0;   // offset of this file within the folder, not accounting for compression
225         int folderIndex = 0;                    // index of the CFFOLDER we belong to
226         Date date = null;                       // modification date
227         int attrs = 0;                          // attrs
228         boolean readOnly = false;               // read-only flag
229         boolean hidden = false;                 // hidden flag
230         boolean system = false;                 // system flag
231         boolean arch = false;                   // archive flag
232         boolean runAfterExec = false;           // true if file should be run during extraction
233         boolean UTFfileName = false;            // true if filename is UTF-encoded
234         String fileName = null;                 // filename
235         int indexInCFHEADER = 0;                // our index in CFHEADER.files
236         CFFOLDER folder = null;                 // the folder we belong to
237         private CFHEADER header = null;
238         File myFile;
239
240         public CFFILE(CFHEADER header) { this.header = header; }
241
242         public CFFILE(File f, String pathName) throws IOException {
243             fileSize = (int)f.length();
244             folderIndex = 0;
245             date = new java.util.Date(f.lastModified());
246             fileName = pathName;
247             myFile = f;
248         }
249
250         public String toString() {
251             return "[ CAB CFFILE: " + fileName + ", " + fileSize + " bytes [ " +
252                 (readOnly ? "readonly " : "") +
253                 (system ? "system " : "") +
254                 (hidden ? "hidden " : "") +
255                 (arch ? "arch " : "") +
256                 (runAfterExec ? "run_after_exec " : "") +
257                 (UTFfileName ? "UTF_filename " : "") +
258                 "]";
259         }
260
261         public void read(DataInputStream dis) throws IOException {
262             fileSize = readLittleInt(dis);
263             uncompressedOffsetInCFFOLDER = readLittleInt(dis);
264             folderIndex = readLittleShort(dis);
265             readLittleShort(dis);   // date
266             readLittleShort(dis);   // time
267             attrs = readLittleShort(dis);
268             readOnly = (attrs & 0x1) != 0;
269             hidden = (attrs & 0x2) != 0;
270             system = (attrs & 0x4) != 0;
271             arch = (attrs & 0x20) != 0;
272             runAfterExec = (attrs & 0x40) != 0;
273             UTFfileName = (attrs & 0x80) != 0;
274             fileName = readZeroTerminatedString(dis);
275
276             indexInCFHEADER = header.readCFFILEs++;
277             header.files[indexInCFHEADER] = this;
278             folder = header.folders[folderIndex];
279             folder.files.addElement(this);
280         }
281     }
282
283
284
285
286     // Compressing Input and Output Streams ///////////////////////////////////////////////
287
288     /** an InputStream that decodes CFDATA blocks belonging to a CFFOLDER */
289     private static class CFFOLDERInputStream extends InputStream {
290         CFFOLDER folder;
291         DataInputStream dis;
292         InputStream iis = null;
293
294         byte[] compressed = new byte[128 * 1024];
295         byte[] uncompressed = new byte[256 * 1024];
296
297         public CFFOLDERInputStream(CFFOLDER f, DataInputStream dis) {
298             this.folder = f;
299             this.dis = dis;
300         }
301
302         InputStream readBlock() throws IOException {
303             int checksum = readLittleInt(dis);
304             int compressedBytes = readLittleShort(dis);
305             int unCompressedBytes = readLittleShort(dis);
306             byte[] reserved = new byte[/*folder.header.perDatablockReservedSize*/0];
307             if (reserved.length > 0) dis.readFully(reserved);
308             if (dis.readByte() != 0x43) throw new CABException("malformed block header");
309             if (dis.readByte() != 0x4B) throw new CABException("malformed block header");
310
311             dis.readFully(compressed, 0, compressedBytes - 2);
312
313             Inflater i = new Inflater(true);
314             i.setInput(compressed, 0, compressedBytes - 2);
315             
316             if (unCompressedBytes > uncompressed.length) uncompressed = new byte[unCompressedBytes];
317             try { i.inflate(uncompressed, 0, uncompressed.length);
318             } catch (DataFormatException dfe) {
319                 dfe.printStackTrace();
320                 throw new CABException(dfe.toString());
321             }
322             return new ByteArrayInputStream(uncompressed, 0, unCompressedBytes);
323         }
324
325         public int available() throws IOException { return iis == null ? 0 : iis.available(); }
326         public void close() throws IOException { iis.close(); }
327         public void mark(int i) { }
328         public boolean markSupported() { return false; }
329         public void reset() { }
330
331         public long skip(long l) throws IOException {
332             if (iis == null) iis = readBlock();
333             int ret = 0;
334             while (l > ret) {
335                 long numread = iis.skip(l - ret);
336                 if (numread == 0 || numread == -1) iis = readBlock();
337                 else ret += numread;
338             }
339             return ret;
340         }
341         
342         public int read(byte[] b, int off, int len) throws IOException {
343             if (iis == null) iis = readBlock();
344             int ret = 0;
345             while (len > ret) {
346                 int numread = iis.read(b, off + ret, len - ret);
347                 if (numread == 0 || numread == -1) iis = readBlock();
348                 else ret += numread;
349             }
350             return ret;
351         }
352
353         public int read() throws IOException {
354             if (iis == null) iis = readBlock();
355             int ret = iis.read();
356             if (ret == -1) {
357                 iis = readBlock();
358                 ret = iis.read();
359             }
360             return ret;
361         }
362     }
363
364
365
366     // Misc Stuff //////////////////////////////////////////////////////////////
367
368     public static String readZeroTerminatedString(DataInputStream dis) throws IOException {
369         int numBytes = 0;
370         byte[] b = new byte[256];
371         while(true) {
372             byte next = dis.readByte();
373             if (next == 0x0) return new String(b, 0, numBytes);
374             b[numBytes++] = next;
375         }
376     }
377     
378     public static int readLittleInt(DataInputStream dis) throws IOException {
379         int lowest = (int)(dis.readByte() & 0xff);
380         int low = (int)(dis.readByte() & 0xff);
381         int high = (int)(dis.readByte() & 0xff);
382         int highest = (int)(dis.readByte() & 0xff);
383         return (highest << 24) | (high << 16) | (low << 8) | lowest;
384     }
385
386     public static int readLittleShort(DataInputStream dis) throws IOException {
387         int low = (int)(dis.readByte() & 0xff);
388         int high = (int)(dis.readByte() & 0xff);
389         return (high << 8) | low;
390     }
391
392     public static class CABException extends IOException {
393         public CABException(String s) { super(s); }
394     }
395
396
397     /** scratch space for isToByteArray() */
398     static byte[] workspace = new byte[16 * 1024];
399
400     /** Trivial method to completely read an InputStream */
401     public static synchronized byte[] isToByteArray(InputStream is) throws IOException {
402         int pos = 0;
403         while (true) {
404             int numread = is.read(workspace, pos, workspace.length - pos);
405             if (numread == -1) break;
406             else if (pos + numread < workspace.length) pos += numread;
407             else {
408                 pos += numread;
409                 byte[] temp = new byte[workspace.length * 2];
410                 System.arraycopy(workspace, 0, temp, 0, workspace.length);
411                 workspace = temp;
412             }
413         }
414         byte[] ret = new byte[pos];
415         System.arraycopy(workspace, 0, ret, 0, pos);
416         return ret;
417     }
418
419
420 }
421