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