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