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