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  
507