1 package jp.sfjp.armadillo.archive.lzh;
5 import java.nio.channels.*;
10 * This class supports header level 0, 1, 2.
12 public final class LzhHeader {
15 * <code>Header level 0.</code>
17 public static final byte HEADER_LEVEL_0 = 0;
19 * <code>Header level 1.</code>
21 public static final byte HEADER_LEVEL_1 = 1;
23 * <code>Header level 2.</code>
25 public static final byte HEADER_LEVEL_2 = 2;
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: ";
39 private final ByteBuffer buffer;
42 this.buffer = ByteBuffer.allocate(BUFFER_SIZE).order(ByteOrder.LITTLE_ENDIAN);
45 public LzhEntry read(InputStream is) throws IOException {
47 buffer.limit(LEVEL_OFFSET + 1);
48 Channels.newChannel(is).read(buffer);
49 int readLength = buffer.position();
51 || (readLength == 1 && buffer.get(0) == 0x00)
52 || (readLength <= LEVEL_OFFSET + 1 && buffer.getShort(0) == 0x00))
54 if (readLength != LEVEL_OFFSET + 1)
55 return null; // warn: next header length
56 int level = buffer.get(LEVEL_OFFSET);
62 headerLength = (buffer.get() & UBYTE_MASK) + 2;
65 headerLength = buffer.getShort() & USHORT_MASK;
68 throw new LzhException("unsupported header level (=" + level + ")");
70 if (headerLength == 0)
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);
84 entry = readLevel1(is);
90 throw new IllegalStateException("unexpected state");
95 entry.headerLength += 2;
99 assert entry.headerLength == headerLength;
103 @SuppressWarnings("unused")
104 private LzhEntry readLevel0() throws LzhException {
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; // -
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;
145 entry.setFtime(mdate, mtime);
146 entry.attribute = attribute;
149 entry.calculatedChecksum = calculateChecksum(2, buffer.limit());
153 private LzhEntry readLevel1(InputStream is) throws IOException {
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
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);
195 entry.setFtime(mdate, mtime);
198 entry.osIdentifier = osIdentifier;
199 entry.calculatedChecksum = calculateChecksum(2, buffer.limit());
200 int extendedHeaderSize = 0;
201 entry.compressedSize = skipSize - extendedHeaderSize;
205 private LzhEntry readLevel2() throws IOException {
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
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;
241 entry.setTimeT(mtime);
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)
252 switch (identifier) {
254 buffer.getShort(); // header CRC
256 case 0x01: // file name
257 x01.write(nextPath(length));
259 case 0x02: // dir name
260 x02.write(nextPath(length));
262 case 0x39: // plural disk
265 case 0x41: // MS Windows timestamp
266 buffer.getLong(); // ctime
267 entry.setFileTime(buffer.getLong());
268 buffer.getLong(); // atime
270 case 0x42: // MS filesize
271 entry.compressedSize = buffer.getLong();
272 entry.size = buffer.getLong();
274 case 0x54: // UNIX time_t
275 entry.setTimeT(buffer.getInt());
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
283 case 0x3F: // comment
288 buffer.position(buffer.position() + length);
289 nextHeaderLength = buffer.getShort();
291 ByteArrayOutputStream name = new ByteArrayOutputStream();
292 if (x02.size() > 0) {
293 name.write(x02.toByteArray());
294 if (!name.toString().endsWith("/"))
298 name.write(x01.toByteArray());
299 assert (x01.size() == 0) == entry.isDirectory();
300 entry.setName(name.toByteArray());
301 assert warningCount == 0;
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;
317 switch (entry.headerLevel) {
328 throw new IllegalArgumentException(ERROR_PREFIX
330 + entry.headerLevel);
333 Channels.newChannel(os).write(buffer);
336 @SuppressWarnings("unused")
337 private void writeLevel0(LzhEntry entry) throws IOException {
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; // -
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);
368 assert buffer.position() == 0;
369 buffer.putShort((short)0); // skip
371 buffer.position(11); // skip
373 buffer.putShort(mtime);
374 buffer.putShort(mdate);
375 buffer.put(attribute);
377 buffer.put(nameLength);
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);
390 buffer.put(0, headerLength);
391 buffer.put(1, checksum);
392 buffer.putInt(7, skipSize);
395 @SuppressWarnings("unused")
396 private void writeLevel1(LzhEntry entry) throws IOException {
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
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
430 buffer.putInt(11); // skip
432 buffer.putShort(mtime);
433 buffer.putShort(mdate);
434 buffer.put(reserved);
436 buffer.put(nameLength);
438 buffer.putShort((short)0); // skip
439 buffer.put(osIdentifier);
440 final short extendedHeaderSize = 0;
441 buffer.putShort(extendedHeaderSize);
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);
452 buffer.put(0, headerLength);
453 buffer.put(1, checksum);
454 buffer.putInt(7, skipSize);
457 @SuppressWarnings("unused")
458 private void writeLevel2(LzhEntry entry) throws IOException {
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
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;
485 osIdentifier = PLATFORM_JAVA; // fixed value in writing
486 firstExHeaderLength = 0;
487 assert buffer.position() == 0;
488 buffer.putShort((short)0); // skip
490 buffer.putInt(compressedSize);
492 buffer.putInt(mtime);
493 buffer.put(reserved);
495 buffer.putShort(crc);
496 buffer.put(osIdentifier);
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;
505 final int nameOffset = (lastDelimiterOffset < 0) ? 0 : lastDelimiterOffset + 1;
507 final int nameLength = (isDirectory) ? 0 : nameb.length - nameOffset;
508 buffer.putShort((short)(nameLength + 3));
509 buffer.put((byte)0x01);
511 buffer.put(nameb, nameOffset, nameLength);
512 if (nameOffset > 0) {
514 byte[] dirb = Arrays.copyOf(nameb, nameb.length + 1);
517 if (nameb[nameb.length - 1] != PATH_DELIMITER) {
518 dirLength = dirb.length;
519 dirb[nameb.length - 1] = PATH_DELIMITER;
522 dirLength = nameb.length;
525 dirLength = nameOffset;
526 buffer.putShort((short)(dirLength + 3));
527 buffer.put((byte)0x02);
528 buffer.put(dirb, 0, dirLength);
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());
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); // ??
543 buffer.putShort((short)0);
545 short headerLength0 = 0;
546 headerLength0 += buffer.position();
547 if (headerLength0 % 256 == 0) {
548 buffer.put((byte)0x00);
550 assert headerLength0 >= 0;
552 headerLength = headerLength0;
553 LzhChecksum cksum = new LzhChecksum();
555 cksum.update(buffer.array(), 0, headerLength);
556 buffer.putShort(0, headerLength);
557 buffer.putShort(crcIndex, cksum.getShortValue());
560 private int calculateChecksum(int start, int end) {
562 for (int i = start; i < end; i++)
563 sum += buffer.get(i);
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;
572 if (entry.getMethod().isCompressing()) {
573 skipSize += entry.compressedSize;
574 skipSize -= headerSize;
577 skipSize += entry.size;
578 skipSize += extendedHeaderSize;
579 assert skipSize >= 0 : "skip size = " + skipSize;
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();
590 private byte[] nextPath(int length) {
591 byte[] bytes = new byte[length];
593 for (int i = 0; i < length; i++)
594 if ((bytes[i] & PATH_DELIMITER) == PATH_DELIMITER)