OSDN Git Service

Add new source files
[armadillo/armadillo1.git] / src / jp / sfjp / armadillo / archive / lzh / LzhHeader.java
1 package jp.sfjp.armadillo.archive.lzh;
2
3 import java.io.*;
4 import java.nio.*;
5 import java.nio.channels.*;
6 import java.util.*;
7
8 /**
9  * LZH archive header.
10  * This class supports header level 0, 1, 2.
11  */
12 public final class LzhHeader {
13
14     /**
15      * <code>Header level 0.</code>
16      */
17     public static final byte HEADER_LEVEL_0 = 0;
18     /**
19      * <code>Header level 1.</code>
20      */
21     public static final byte HEADER_LEVEL_1 = 1;
22     /**
23      * <code>Header level 2.</code>
24      */
25     public static final byte HEADER_LEVEL_2 = 2;
26
27     private static final int BUFFER_SIZE = 1024;
28     private static final long MAX_DATA_SIZE = 0xFFFFFFFFL;
29     private static final long UINT_MASK = 0xFFFFFFFFL;
30     private static final int USHORT_MASK = 0xFFFF;
31     private static final int UBYTE_MASK = 0xFF;
32     private static final int LEVEL_OFFSET = 20;
33     private static final byte FILETYPE_FILE = 0x20;
34     private static final byte PLATFORM_JAVA = 'J';
35     private static final byte PATH_DELIMITER = (byte)0xFF;
36     private static final String ISO_8859_1 = "iso-8859-1";
37     private static final String ERROR_PREFIX = "invalid header: ";
38
39     private final ByteBuffer buffer;
40
41     public LzhHeader() {
42         this.buffer = ByteBuffer.allocate(BUFFER_SIZE).order(ByteOrder.LITTLE_ENDIAN);
43     }
44
45     public LzhEntry read(InputStream is) throws IOException {
46         buffer.clear();
47         buffer.limit(LEVEL_OFFSET + 1);
48         Channels.newChannel(is).read(buffer);
49         int readLength = buffer.position();
50         if (readLength == 0
51             || (readLength == 1 && buffer.get(0) == 0x00)
52             || (readLength <= LEVEL_OFFSET + 1 && buffer.getShort(0) == 0x00))
53             return null;
54         if (readLength != LEVEL_OFFSET + 1)
55             return null; // warn: next header length
56         int level = buffer.get(LEVEL_OFFSET);
57         buffer.rewind();
58         int headerLength;
59         switch (level) {
60             case 0:
61             case 1:
62                 headerLength = (buffer.get() & UBYTE_MASK) + 2;
63                 break;
64             case 2:
65                 headerLength = buffer.getShort() & USHORT_MASK;
66                 break;
67             default:
68                 throw new LzhException("unsupported header level (=" + level + ")");
69         }
70         if (headerLength == 0)
71             return null;
72         assert headerLength >= 0 && headerLength < BUFFER_SIZE : "header length = " + headerLength;
73         buffer.limit(headerLength);
74         buffer.position(LEVEL_OFFSET + 1);
75         Channels.newChannel(is).read(buffer);
76         if (buffer.position() != headerLength)
77             throw new LzhException(ERROR_PREFIX + "header length = " + headerLength);
78         LzhEntry entry;
79         switch (level) {
80             case 0:
81                 entry = readLevel0();
82                 break;
83             case 1:
84                 entry = readLevel1(is);
85                 break;
86             case 2:
87                 entry = readLevel2();
88                 break;
89             default:
90                 throw new IllegalStateException("unexpected state");
91         }
92         switch (level) {
93             case 0:
94             case 1:
95                 entry.headerLength += 2;
96                 break;
97             default:
98         }
99         assert entry.headerLength == headerLength;
100         return entry;
101     }
102
103     @SuppressWarnings("unused")
104     private LzhEntry readLevel0() throws LzhException {
105         // Header Level 0
106         final byte headerLength; // length of this header
107         final byte checksum; // checksum of header (SUM)
108         final byte[] method; // compression method
109         final int skipSize; // compressed size (skip size)
110         final int size; // uncompressed size
111         final short mtime; // last modified time (MS-DOS time)
112         final short mdate; // last modified date (MS-DOS time)
113         final byte attribute; // file attribute
114         final byte level; // header level
115         final byte nameLength; // length of path name
116         final byte[] name; // path name
117         final short crc; // checksum of file (CRC16)
118         final short extend; // -
119         // ---
120         buffer.position(0);
121         headerLength = buffer.get();
122         checksum = buffer.get();
123         method = getBytes(5);
124         skipSize = buffer.getInt();
125         size = buffer.getInt();
126         mtime = buffer.getShort();
127         mdate = buffer.getShort();
128         attribute = buffer.get();
129         level = buffer.get();
130         nameLength = buffer.get();
131         name = getBytes(nameLength);
132         crc = buffer.getShort();
133         assert level == HEADER_LEVEL_0;
134         LzhEntry entry = new LzhEntry(0);
135         entry.headerLength = headerLength;
136         entry.checksum = checksum & UBYTE_MASK;
137         assert (method[0] & 0x7F) == method[0];
138         assert (method[1] & 0x7F) == method[1];
139         assert (method[2] & 0x7F) == method[2];
140         assert (method[3] & 0x7F) == method[3];
141         assert (method[4] & 0x7F) == method[4];
142         entry.method = new String(method);
143         entry.compressedSize = skipSize;
144         entry.size = size;
145         entry.setFtime(mdate, mtime);
146         entry.attribute = attribute;
147         entry.setName(name);
148         entry.crc = crc;
149         entry.calculatedChecksum = calculateChecksum(2, buffer.limit());
150         return entry;
151     }
152
153     private LzhEntry readLevel1(InputStream is) throws IOException {
154         // Header Level 1
155         final byte headerLength; // length of this header
156         final byte checksum; // checksum of header (SUM)
157         final byte[] method; // compression method
158         final int skipSize; // skip size
159         final int size; // uncompressed size
160         final short mtime; // last modified time (MS-DOS time)
161         final short mdate; // last modified date (MS-DOS time)
162         final byte reserved; // reserved
163         final byte level; // header level
164         final byte nameLength; // length of path name
165         final byte[] name; // path name
166         final short crc; // checksum of file (CRC16)
167         final byte osIdentifier; // OS identifier which compressed this
168         // ---
169         buffer.position(0);
170         LzhEntry entry = new LzhEntry(1);
171         headerLength = buffer.get();
172         checksum = buffer.get();
173         method = getBytes(5);
174         skipSize = buffer.getInt();
175         size = buffer.getInt();
176         mtime = buffer.getShort();
177         mdate = buffer.getShort();
178         reserved = buffer.get();
179         level = buffer.get();
180         nameLength = buffer.get();
181         name = getBytes(nameLength);
182         crc = buffer.getShort();
183         osIdentifier = buffer.get();
184         assert level == HEADER_LEVEL_1;
185         assert reserved == FILETYPE_FILE;
186         entry.headerLength = headerLength;
187         entry.checksum = checksum;
188         assert (method[0] & 0x7F) == method[0];
189         assert (method[1] & 0x7F) == method[1];
190         assert (method[2] & 0x7F) == method[2];
191         assert (method[3] & 0x7F) == method[3];
192         assert (method[4] & 0x7F) == method[4];
193         entry.method = new String(method);
194         entry.size = size;
195         entry.setFtime(mdate, mtime);
196         entry.setName(name);
197         entry.crc = crc;
198         entry.osIdentifier = osIdentifier;
199         entry.calculatedChecksum = calculateChecksum(2, buffer.limit());
200         int extendedHeaderSize = 0;
201         entry.compressedSize = skipSize - extendedHeaderSize;
202         return entry;
203     }
204
205     private LzhEntry readLevel2() throws IOException {
206         // Header Level 2
207         final short headerLength; // length of this header
208         final byte[] method; // compression method
209         final int compressedSize; // compressed size
210         final int size; // uncompressed size
211         final int mtime; // last modified timestamp (POSIX time)
212         final byte reserved; // reserved
213         final byte level; // header level
214         final short crc; // checksum of file (CRC16)
215         final byte osIdentifier; // OS identifier which compressed this
216         final short firstExHeaderLength; // length of next (first) extend header
217         // ---
218         buffer.position(0);
219         headerLength = buffer.getShort();
220         method = getBytes(5);
221         compressedSize = buffer.getInt();
222         size = buffer.getInt();
223         mtime = buffer.getInt();
224         reserved = buffer.get();
225         level = buffer.get();
226         crc = buffer.getShort();
227         osIdentifier = buffer.get();
228         firstExHeaderLength = buffer.getShort();
229         assert level == HEADER_LEVEL_2;
230         assert reserved == FILETYPE_FILE;
231         LzhEntry entry = new LzhEntry(2);
232         entry.headerLength = headerLength;
233         assert (method[0] & 0x7F) == method[0];
234         assert (method[1] & 0x7F) == method[1];
235         assert (method[2] & 0x7F) == method[2];
236         assert (method[3] & 0x7F) == method[3];
237         assert (method[4] & 0x7F) == method[4];
238         entry.method = new String(method);
239         entry.compressedSize = compressedSize;
240         entry.size = size;
241         entry.setTimeT(mtime);
242         entry.crc = crc;
243         entry.osIdentifier = osIdentifier;
244         int warningCount = 0;
245         ByteArrayOutputStream x01 = new ByteArrayOutputStream();
246         ByteArrayOutputStream x02 = new ByteArrayOutputStream();
247         short nextHeaderLength = firstExHeaderLength;
248         while (nextHeaderLength > 0) {
249             final byte identifier = buffer.get();
250             final int length = nextHeaderLength - 3; // datalength = len - 1(id) - 2(nextlen)
251             buffer.mark();
252             switch (identifier) {
253                 case 0x00: // common
254                     buffer.getShort(); // header CRC
255                     break;
256                 case 0x01: // file name
257                     x01.write(nextPath(length));
258                     break;
259                 case 0x02: // dir name
260                     x02.write(nextPath(length));
261                     break;
262                 case 0x39: // plural disk
263                     ++warningCount;
264                     break;
265                 case 0x41: // MS Windows timestamp
266                     buffer.getLong(); // ctime
267                     entry.setFileTime(buffer.getLong());
268                     buffer.getLong(); // atime
269                     break;
270                 case 0x42: // MS filesize
271                     entry.compressedSize = buffer.getLong();
272                     entry.size = buffer.getLong();
273                     break;
274                 case 0x54: // UNIX time_t
275                     entry.setTimeT(buffer.getInt());
276                     break;
277                 case 0x40: // MS attribute
278                 case 0x50: // UNIX permission
279                 case 0x51: // UNIX uid gid
280                 case 0x52: // UNIX group
281                 case 0x53: // UNIX user
282                     break;
283                 case 0x3F: // comment
284                 default:
285                     // ignore
286             }
287             buffer.reset();
288             buffer.position(buffer.position() + length);
289             nextHeaderLength = buffer.getShort();
290         }
291         ByteArrayOutputStream name = new ByteArrayOutputStream();
292         if (x02.size() > 0) {
293             name.write(x02.toByteArray());
294             if (!name.toString().endsWith("/"))
295                 name.write('/');
296         }
297         if (x01.size() > 0)
298             name.write(x01.toByteArray());
299         assert (x01.size() == 0) == entry.isDirectory();
300         entry.setName(name.toByteArray());
301         assert warningCount == 0;
302         return entry;
303     }
304
305     public void write(OutputStream os, LzhEntry entry) throws IOException {
306         if (!entry.method.matches("-l[hz][a-z0-9]-"))
307             throw new LzhException("invalid compression type: " + entry.method);
308         if (entry.compressedSize > Integer.MAX_VALUE)
309             throw new LzhException("too large compressed size: " + entry.compressedSize);
310         if (entry.size > Integer.MAX_VALUE)
311             throw new LzhException("too large size: " + entry.size);
312         if (entry.getNameAsBytes().length > Short.MAX_VALUE)
313             throw new LzhException("too long file name: length=" + entry.size);
314         if (entry.isDirectory())
315             entry.method = LzhMethod.LHD;
316         buffer.clear();
317         switch (entry.headerLevel) {
318             case HEADER_LEVEL_0:
319                 writeLevel0(entry);
320                 break;
321             case HEADER_LEVEL_1:
322                 writeLevel1(entry);
323                 break;
324             case HEADER_LEVEL_2:
325                 writeLevel2(entry);
326                 break;
327             default:
328                 throw new IllegalArgumentException(ERROR_PREFIX
329                                                    + "header-level="
330                                                    + entry.headerLevel);
331         }
332         buffer.flip();
333         Channels.newChannel(os).write(buffer);
334     }
335
336     @SuppressWarnings("unused")
337     private void writeLevel0(LzhEntry entry) throws IOException {
338         // Header Level 0
339         final byte headerLength; // length of this header
340         final byte checksum; // checksum of header (SUM)
341         final byte[] method; // compression method
342         final int skipSize; // skip size
343         final int size; // uncompressed size
344         final short mtime; // last modified time (MS-DOS time)
345         final short mdate; // last modified date (MS-DOS time)
346         final byte attribute; // file attribute
347         final byte level; // header level
348         final byte nameLength; // length of path name
349         final byte[] name; // path name
350         final short crc; // checksum of file (CRC16)
351         final short extend; // -
352         // ---
353         assert entry.compressedSize >= 0 && entry.compressedSize <= MAX_DATA_SIZE;
354         assert entry.size >= 0 && entry.size <= MAX_DATA_SIZE;
355         assert entry.attribute == FILETYPE_FILE;
356         name = entry.getNameAsBytes();
357         assert name.length <= 0xFF;
358         method = entry.method.getBytes(ISO_8859_1);
359         assert method.length == 5;
360         size = (int)(entry.size & UINT_MASK);
361         final int ftime = entry.getFTime();
362         mtime = (short)(ftime & USHORT_MASK);
363         mdate = (short)((ftime << 16) & USHORT_MASK);
364         attribute = entry.attribute;
365         level = HEADER_LEVEL_0;
366         nameLength = (byte)(name.length & UBYTE_MASK);
367         crc = entry.crc;
368         assert buffer.position() == 0;
369         buffer.putShort((short)0); // skip
370         buffer.put(method);
371         buffer.position(11); // skip
372         buffer.putInt(size);
373         buffer.putShort(mtime);
374         buffer.putShort(mdate);
375         buffer.put(attribute);
376         buffer.put(level);
377         buffer.put(nameLength);
378         buffer.put(name);
379         buffer.putShort(crc);
380         final int extendedHeaderSize = 0; // not supported
381         final int endp = buffer.position();
382         if ((endp - 2) > 0xFF)
383             throw new LzhException("invalid header length: " + (endp - 2));
384         headerLength = (byte)((endp - 2) & UBYTE_MASK);
385         checksum = (byte)(calculateChecksum(2, endp) & UBYTE_MASK);
386         if (entry.compressedSize > 0)
387             skipSize = calculateSkipSize(entry, size, extendedHeaderSize);
388         else
389             skipSize = 0;
390         buffer.put(0, headerLength);
391         buffer.put(1, checksum);
392         buffer.putInt(7, skipSize);
393     }
394
395     @SuppressWarnings("unused")
396     private void writeLevel1(LzhEntry entry) throws IOException {
397         // Header Level 1
398         final byte headerLength; // length of this header
399         final byte checksum; // checksum of header (SUM)
400         final byte[] method; // compression method
401         final int skipSize; // skip size
402         final int size; // uncompressed size
403         final short mtime; // last modified time (MS-DOS time)
404         final short mdate; // last modified date (MS-DOS time)
405         final byte reserved; // reserved
406         final byte level; // header level
407         final byte nameLength; // length of path name
408         final byte[] name; // path name
409         final short crc; // checksum of file (CRC16)
410         final byte osIdentifier; // OS identifier which compressed this
411         // ---
412         assert entry.compressedSize >= 0 && entry.compressedSize <= Integer.MAX_VALUE;
413         assert entry.size >= 0 && entry.size <= Integer.MAX_VALUE;
414         name = entry.getNameAsBytes();
415         assert name.length <= 0xFF;
416         method = entry.method.getBytes(ISO_8859_1);
417         assert method.length == 5;
418         assert entry.size >= 0 && entry.size <= MAX_DATA_SIZE;
419         size = (int)(entry.size & UINT_MASK);
420         final int ftime = entry.getFTime();
421         mtime = (short)(ftime & USHORT_MASK);
422         mdate = (short)((ftime << 16) & USHORT_MASK);
423         reserved = FILETYPE_FILE;
424         level = HEADER_LEVEL_1;
425         nameLength = (byte)(name.length & UBYTE_MASK);
426         osIdentifier = PLATFORM_JAVA; // fixed value in writing
427         assert buffer.position() == 0;
428         buffer.putShort((short)0); // skip
429         buffer.put(method);
430         buffer.putInt(11); // skip
431         buffer.putInt(size);
432         buffer.putShort(mtime);
433         buffer.putShort(mdate);
434         buffer.put(reserved);
435         buffer.put(level);
436         buffer.put(nameLength);
437         buffer.put(name);
438         buffer.putShort((short)0); // skip
439         buffer.put(osIdentifier);
440         final short extendedHeaderSize = 0;
441         buffer.putShort(extendedHeaderSize);
442         buffer.put((byte)0);
443         final int endp = buffer.position();
444         if ((endp - 2) > 0xFF)
445             throw new LzhException("invalid header length: " + (endp - 2));
446         headerLength = (byte)((endp - 2) & UBYTE_MASK);
447         checksum = (byte)calculateChecksum(2, endp);
448         if (entry.compressedSize > 0)
449             skipSize = calculateSkipSize(entry, size, extendedHeaderSize);
450         else
451             skipSize = 0;
452         buffer.put(0, headerLength);
453         buffer.put(1, checksum);
454         buffer.putInt(7, skipSize);
455     }
456
457     @SuppressWarnings("unused")
458     private void writeLevel2(LzhEntry entry) throws IOException {
459         // Header Level 2
460         final short headerLength; // length of this header
461         final byte[] method; // compression method
462         final int compressedSize; // compressed size
463         final int size; // uncompressed size
464         final int mtime; // last modified timestamp (POSIX time)
465         final byte reserved; // reserved
466         final byte level; // header level
467         final short crc; // checksum of file (CRC16)
468         final byte osIdentifier; // OS identifier which compressed this
469         final short firstExHeaderLength; // length of next (first) extend header
470         // ---
471         assert entry.compressedSize >= 0 && entry.compressedSize <= MAX_DATA_SIZE;
472         assert entry.size >= 0 && entry.size <= MAX_DATA_SIZE;
473         byte[] nameb = entry.getNameAsBytes();
474         assert nameb.length <= 0xFF;
475         method = entry.method.getBytes(ISO_8859_1);
476         assert method.length == 5;
477         final boolean isDirectory = entry.isDirectory();
478         assert isDirectory == entry.method.equals(LzhMethod.LHD);
479         compressedSize = isDirectory ? 0 : (int)(entry.compressedSize & UINT_MASK);
480         size = isDirectory ? 0 : (int)(entry.size & UINT_MASK);
481         mtime = entry.getTimeT();
482         reserved = FILETYPE_FILE;
483         level = HEADER_LEVEL_2;
484         crc = entry.crc;
485         osIdentifier = PLATFORM_JAVA; // fixed value in writing
486         firstExHeaderLength = 0;
487         assert buffer.position() == 0;
488         buffer.putShort((short)0); // skip
489         buffer.put(method);
490         buffer.putInt(compressedSize);
491         buffer.putInt(size);
492         buffer.putInt(mtime);
493         buffer.put(reserved);
494         buffer.put(level);
495         buffer.putShort(crc);
496         buffer.put(osIdentifier);
497         /* extended */
498         // dividing dir and name
499         int lastDelimiterOffset = -1;
500         for (int i = 0; i < nameb.length; i++)
501             if (nameb[i] == '/' || nameb[i] == '\\') {
502                 nameb[i] = PATH_DELIMITER;
503                 lastDelimiterOffset = i;
504             }
505         final int nameOffset = (lastDelimiterOffset < 0) ? 0 : lastDelimiterOffset + 1;
506         // 0x01 file name
507         final int nameLength = (isDirectory) ? 0 : nameb.length - nameOffset;
508         buffer.putShort((short)(nameLength + 3));
509         buffer.put((byte)0x01);
510         if (nameLength > 0)
511             buffer.put(nameb, nameOffset, nameLength);
512         if (nameOffset > 0) {
513             // 0x02 dir name
514             byte[] dirb = Arrays.copyOf(nameb, nameb.length + 1);
515             final int dirLength;
516             if (isDirectory) {
517                 if (nameb[nameb.length - 1] != PATH_DELIMITER) {
518                     dirLength = dirb.length;
519                     dirb[nameb.length - 1] = PATH_DELIMITER;
520                 }
521                 else
522                     dirLength = nameb.length;
523             }
524             else
525                 dirLength = nameOffset;
526             buffer.putShort((short)(dirLength + 3));
527             buffer.put((byte)0x02);
528             buffer.put(dirb, 0, dirLength);
529         }
530         // 0x41 MS Windows timestamp
531         buffer.putShort((short)27);
532         buffer.put((byte)0x41);
533         buffer.putLong(entry.getFileTime());
534         buffer.putLong(entry.getFileTime());
535         buffer.putLong(entry.getFileTime());
536         // 0x00 common
537         buffer.putShort((short)6);
538         buffer.put((byte)0x00);
539         final int crcIndex = buffer.position();
540         buffer.putShort((short)0);
541         buffer.put((byte)0x00); // ??
542         // no next
543         buffer.putShort((short)0);
544         // header size
545         short headerLength0 = 0;
546         headerLength0 += buffer.position();
547         if (headerLength0 % 256 == 0) {
548             buffer.put((byte)0x00);
549             ++headerLength0;
550             assert headerLength0 >= 0;
551         }
552         headerLength = headerLength0;
553         LzhChecksum cksum = new LzhChecksum();
554         cksum.reset();
555         cksum.update(buffer.array(), 0, headerLength);
556         buffer.putShort(0, headerLength);
557         buffer.putShort(crcIndex, cksum.getShortValue());
558     }
559
560     private int calculateChecksum(int start, int end) {
561         int sum = 0;
562         for (int i = start; i < end; i++)
563             sum += buffer.get(i);
564         return sum;
565     }
566
567     private static int calculateSkipSize(LzhEntry entry, int headerSize, int extendedHeaderSize) throws LzhException {
568         assert entry.compressedSize >= 0 && entry.compressedSize <= Integer.MAX_VALUE;
569         assert entry.size >= 0 && entry.size <= Integer.MAX_VALUE;
570         assert headerSize >= 0 && extendedHeaderSize >= 0;
571         int skipSize = 0;
572         if (entry.getMethod().isCompressing()) {
573             skipSize += entry.compressedSize;
574             skipSize -= headerSize;
575         }
576         else
577             skipSize += entry.size;
578         skipSize += extendedHeaderSize;
579         assert skipSize >= 0 : "skip size = " + skipSize;
580         return skipSize;
581     }
582
583     private byte[] getBytes(int length) {
584         byte[] bytes = new byte[length];
585         for (int i = 0; i < bytes.length; i++)
586             bytes[i] = buffer.get();
587         return bytes;
588     }
589
590     private byte[] nextPath(int length) {
591         byte[] bytes = new byte[length];
592         buffer.get(bytes);
593         for (int i = 0; i < length; i++)
594             if ((bytes[i] & PATH_DELIMITER) == PATH_DELIMITER)
595                 bytes[i] = '/';
596         return bytes;
597     }
598
599 }