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