1
2
3
4
5 package net.java.truevfs.comp.zip;
6
7 import java.util.Formatter;
8 import javax.annotation.CheckForNull;
9 import javax.annotation.Nullable;
10 import javax.annotation.concurrent.NotThreadSafe;
11 import static net.java.truevfs.comp.zip.Constants.EMPTY;
12 import static net.java.truevfs.comp.zip.Constants.FORCE_ZIP64_EXT;
13 import static net.java.truevfs.comp.zip.ExtraField.WINZIP_AES_ID;
14 import static net.java.truevfs.comp.zip.ExtraField.ZIP64_HEADER_ID;
15 import static net.java.truevfs.comp.zip.LittleEndian.readLong;
16 import static net.java.truevfs.comp.zip.LittleEndian.writeLong;
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 @NotThreadSafe
37 public class ZipEntry implements Cloneable {
38
39
40 private static final int PLATFORM = 1, METHOD = 1 << 1,
41 CRC = 1 << 2, DTIME = 1 << 6,
42 EATTR = 1 << 7;
43
44
45 public static final byte UNKNOWN = -1;
46
47
48 public static final short PLATFORM_FAT = 0;
49
50
51 public static final short PLATFORM_UNIX = 3;
52
53
54
55
56
57
58 public static final int STORED = 0;
59
60
61
62
63
64
65 public static final int DEFLATED = 8;
66
67
68
69
70
71
72 public static final int BZIP2 = 12;
73
74
75
76
77 static final int WINZIP_AES = 99;
78
79
80 static final int GPBF_ENCRYPTED = 1;
81
82 static final int GPBF_DATA_DESCRIPTOR = 1 << 3;
83 static final int GPBF_UTF8 = 1 << 11;
84
85
86
87
88
89 public static final long MIN_DOS_TIME = DateTimeConverter.MIN_DOS_TIME;
90
91
92
93
94
95 public static final long MAX_DOS_TIME = DateTimeConverter.MAX_DOS_TIME;
96
97 private byte init;
98 private String name;
99 private byte platform;
100 private short general;
101 private short method;
102 private int dtime;
103 private int crc;
104 private long csize = UNKNOWN;
105 private long size = UNKNOWN;
106 private int eattr;
107
108
109 private long offset = UNKNOWN;
110
111
112
113
114
115
116 private @CheckForNull ExtraFields fields;
117
118
119 private @CheckForNull String comment;
120
121
122 public ZipEntry(final String name) {
123 UShort.check(name.length());
124 this.name = name;
125 }
126
127
128
129
130
131 @SuppressWarnings("AccessingNonPublicFieldOfAnotherObject")
132 protected ZipEntry(final String name, final ZipEntry template) {
133 UShort.check(name.length());
134 this.init = template.init;
135 this.name = name;
136 this.platform = template.platform;
137 this.general = template.general;
138 this.method = template.method;
139 this.dtime = template.dtime;
140 this.crc = template.crc;
141 this.csize = template.csize;
142 this.size = template.size;
143 this.eattr = template.eattr;
144 this.offset = template.offset;
145 final ExtraFields templateFields = template.fields;
146 this.fields = templateFields == null ? null : templateFields.clone();
147 this.comment = template.comment;
148 }
149
150 @Override
151 @SuppressWarnings("AccessingNonPublicFieldOfAnotherObject")
152 public ZipEntry clone() {
153 final ZipEntry entry;
154 try {
155 entry = (ZipEntry) super.clone();
156 } catch (CloneNotSupportedException ex) {
157 throw new AssertionError(ex);
158 }
159 final ExtraFields fields = this.fields;
160 entry.fields = fields == null ? null : fields.clone();
161 return entry;
162 }
163
164 private boolean isInit(final int mask) { return 0 != (init & mask); }
165
166 private void setInit(final int mask, final boolean init) {
167 if (init)
168 this.init |= mask;
169 else
170 this.init &= ~mask;
171 }
172
173
174 public final String getName() { return name; }
175
176
177
178
179
180 public final boolean isDirectory() { return name.endsWith("/"); }
181
182 public final int getPlatform() {
183 return isInit(PLATFORM) ? platform & UByte.MAX_VALUE : UNKNOWN;
184 }
185
186 public final void setPlatform(final int platform) {
187 final boolean known = UNKNOWN != platform;
188 if (known) {
189 UByte.check(platform, name, "Platform out of range");
190 this.platform = (byte) platform;
191 } else {
192 this.platform = 0;
193 }
194 setInit(PLATFORM, known);
195 }
196
197 final int getRawPlatform() { return platform & UByte.MAX_VALUE; }
198
199 final void setRawPlatform(final int platform) {
200 assert UByte.check(platform);
201 this.platform = (byte) platform;
202 setInit(PLATFORM, true);
203 }
204
205 final int getRawVersionNeededToExtract() {
206 final int method = getRawMethod();
207 return BZIP2 == method
208 ? 46
209 : isZip64ExtensionsRequired()
210 ? 45
211 : DEFLATED == method || isDirectory()
212 ? 20
213 : 10;
214 }
215
216
217 final int getGeneralPurposeBitFlags() {
218 return general & UShort.MAX_VALUE;
219 }
220
221
222 final void setGeneralPurposeBitFlags(final int general) {
223 assert UShort.check(general);
224 this.general = (short) general;
225 }
226
227
228 final boolean getGeneralPurposeBitFlag(final int mask) {
229 return 0 != (general & mask);
230 }
231
232
233 final void setGeneralPurposeBitFlag(final int mask, final boolean bit) {
234 if (bit)
235 general |= mask;
236 else
237 general &= ~mask;
238 }
239
240
241
242
243
244
245
246 public final boolean isEncrypted() {
247 return getGeneralPurposeBitFlag(GPBF_ENCRYPTED);
248 }
249
250
251
252
253
254
255
256
257
258
259
260 public final void setEncrypted(boolean encrypted) {
261 setGeneralPurposeBitFlag(GPBF_ENCRYPTED, encrypted);
262 }
263
264
265
266
267
268
269 public final void clearEncryption() {
270 setEncrypted(false);
271 final WinZipAesExtraField field
272 = (WinZipAesExtraField) removeExtraField(WINZIP_AES_ID);
273 if (WINZIP_AES == getRawMethod())
274 setRawMethod(null == field ? UNKNOWN : field.getMethod());
275 }
276
277
278
279
280
281
282
283 public final int getMethod() {
284 return isInit(METHOD) ? method & UShort.MAX_VALUE : UNKNOWN;
285 }
286
287
288
289
290
291
292
293
294
295
296 public final void setMethod(final int method) {
297 switch (method) {
298 case WINZIP_AES:
299
300
301 case STORED:
302 case DEFLATED:
303 case BZIP2:
304 this.method = (short) method;
305 setInit(METHOD, true);
306 break;
307 case UNKNOWN:
308 this.method = 0;
309 setInit(METHOD, false);
310 break;
311 default:
312 throw new IllegalArgumentException(
313 name + " (unsupported compression method " + method + ")");
314 }
315 }
316
317 final int getRawMethod() { return method & UShort.MAX_VALUE; }
318
319 final void setRawMethod(final int method) {
320 assert UShort.check(method);
321 this.method = (short) method;
322 setInit(METHOD, true);
323 }
324
325 public final long getTime() {
326 if (!isInit(DTIME)) return UNKNOWN;
327 return getDateTimeConverter().toJavaTime(dtime & UInt.MAX_VALUE);
328 }
329
330 public final void setTime(final long jtime) {
331 final boolean known = UNKNOWN != jtime;
332 if (known) {
333 this.dtime = (int) getDateTimeConverter().toDosTime(jtime);
334 } else {
335 this.dtime = 0;
336 }
337 setInit(DTIME, known);
338 }
339
340 final long getRawTime() { return dtime & UInt.MAX_VALUE; }
341
342 final void setRawTime(final long dtime) {
343 assert UInt.check(dtime);
344 this.dtime = (int) dtime;
345 setInit(DTIME, true);
346 }
347
348
349
350
351
352
353
354
355
356
357
358 protected DateTimeConverter getDateTimeConverter() {
359 return DateTimeConverter.JAR;
360 }
361
362 public final long getCrc() {
363 return isInit(CRC) ? crc & UInt.MAX_VALUE : UNKNOWN;
364 }
365
366 public final void setCrc(final long crc) {
367 final boolean known = UNKNOWN != crc;
368 if (known) {
369 UInt.check(crc, name, "CRC-32 out of range");
370 this.crc = (int) crc;
371 } else {
372 this.crc = 0;
373 }
374 setInit(CRC, known);
375 }
376
377 final long getRawCrc() { return crc & UInt.MAX_VALUE; }
378
379 final void setRawCrc(final long crc) {
380 assert UInt.check(crc);
381 this.crc = (int) crc;
382 setInit(CRC, true);
383 }
384
385
386
387
388
389
390 public final long getCompressedSize() { return csize; }
391
392
393
394
395
396
397
398
399
400
401 public final void setCompressedSize(final long csize) {
402 if (UNKNOWN != csize)
403 ULong.check(csize, name, "Compressed Size out of range");
404 this.csize = csize;
405 }
406
407 final long getRawCompressedSize() {
408 final long csize = this.csize;
409 if (UNKNOWN == csize) return 0;
410 return FORCE_ZIP64_EXT || UInt.MAX_VALUE <= csize
411 ? UInt.MAX_VALUE
412 : csize;
413 }
414
415 final void setRawCompressedSize(final long csize) {
416 assert ULong.check(csize);
417 this.csize = csize;
418 }
419
420
421
422
423
424
425 public final long getSize() { return size; }
426
427
428
429
430
431
432
433
434
435
436 public final void setSize(final long size) {
437 if (UNKNOWN != size)
438 ULong.check(size, name, "Uncompressed Size out of range");
439 this.size = size;
440 }
441
442 final long getRawSize() {
443 final long size = this.size;
444 if (UNKNOWN == size) return 0;
445 return FORCE_ZIP64_EXT || UInt.MAX_VALUE <= size
446 ? UInt.MAX_VALUE
447 : size;
448 }
449
450 final void setRawSize(final long size) {
451 assert ULong.check(size);
452 this.size = size;
453 }
454
455
456
457
458
459
460 public final long getExternalAttributes() {
461 return isInit(EATTR) ? eattr & UInt.MAX_VALUE : UNKNOWN;
462 }
463
464
465
466
467
468
469 public final void setExternalAttributes(final long eattr) {
470 final boolean known = UNKNOWN != eattr;
471 if (known) {
472 UInt.check(eattr, name, "external file attributes out of range");
473 this.eattr = (int) eattr;
474 } else {
475 this.eattr = 0;
476 }
477 setInit(EATTR, known);
478 }
479
480 final long getRawExternalAttributes() {
481 if (!isInit(EATTR)) return isDirectory() ? 0x10 : 0;
482 return eattr & UInt.MAX_VALUE;
483 }
484
485 final void setRawExternalAttributes(final long eattr) {
486 assert UInt.check(eattr);
487 this.eattr = (int) eattr;
488 setInit(EATTR, true);
489 }
490
491 final long getOffset() { return offset; }
492
493 final long getRawOffset() {
494 final long offset = this.offset;
495 if (UNKNOWN == offset) return 0;
496 return FORCE_ZIP64_EXT || UInt.MAX_VALUE <= offset
497 ? UInt.MAX_VALUE
498 : offset;
499 }
500
501 final void setRawOffset(final long offset) {
502 assert ULong.check(offset);
503 this.offset = offset;
504 }
505
506 final @Nullable ExtraField getExtraField(int headerId) {
507 final ExtraFields fields = this.fields;
508 return fields == null ? null : fields.get(headerId);
509 }
510
511 final @Nullable ExtraField addExtraField(final ExtraField field) {
512 assert null != field;
513 ExtraFields fields = this.fields;
514 if (null == fields) this.fields = fields = new ExtraFields();
515 return fields.add(field);
516 }
517
518 final @Nullable ExtraField removeExtraField(final int headerId) {
519 final ExtraFields fields = this.fields;
520 return null != fields ? fields.remove(headerId) : null;
521 }
522
523
524
525
526
527
528
529
530
531 public final byte[] getExtra() { return getExtraFields(false); }
532
533
534
535
536
537
538
539
540
541
542
543
544
545 public final void setExtra(final @CheckForNull byte[] buf)
546 throws IllegalArgumentException {
547 if (null != buf) {
548 UShort.check(buf.length, "extra fields too large", null);
549 setExtraFields(buf, false);
550 } else {
551 this.fields = null;
552 }
553 }
554
555
556
557
558
559
560
561
562 final byte[] getRawExtraFields() { return getExtraFields(true); }
563
564
565
566
567
568
569
570
571
572 final void setRawExtraFields(final byte[] buf)
573 throws IllegalArgumentException {
574 setExtraFields(buf, true);
575 }
576
577 private byte[] getExtraFields(final boolean zip64) {
578 ExtraFields fields = this.fields;
579 if (zip64) {
580 final ExtraField field = composeZip64ExtraField();
581 if (null != field) {
582 fields = null != fields ? fields.clone() : new ExtraFields();
583 fields.add(field);
584 }
585 } else {
586 assert null == fields || null == fields.get(ZIP64_HEADER_ID);
587 }
588 return null == fields ? EMPTY : fields.getDataBlock();
589 }
590
591
592
593
594
595 private void setExtraFields(final byte[] buf, final boolean zip64)
596 throws IllegalArgumentException {
597 assert UShort.check(buf.length);
598 if (0 < buf.length) {
599 final ExtraFields fields = new ExtraFields();
600 try {
601 fields.readFrom(buf, 0, buf.length);
602 if (zip64) parseZip64ExtraField(fields);
603 } catch (final IndexOutOfBoundsException ex) {
604 throw new IllegalArgumentException(ex);
605 }
606 fields.remove(ZIP64_HEADER_ID);
607 this.fields = 0 < fields.size() ? fields : null;
608 } else {
609 this.fields = null;
610 }
611 }
612
613
614
615
616
617
618
619 private @CheckForNull ExtraField composeZip64ExtraField() {
620 final byte[] data = new byte[3 * 8];
621 int off = 0;
622
623 final long size = getSize();
624 if (FORCE_ZIP64_EXT && UNKNOWN != size || UInt.MAX_VALUE <= size) {
625 writeLong(size, data, off);
626 off += 8;
627 }
628
629 final long csize = getCompressedSize();
630 if (FORCE_ZIP64_EXT && UNKNOWN != csize || UInt.MAX_VALUE <= csize) {
631 writeLong(csize, data, off);
632 off += 8;
633 }
634
635 final long offset = getOffset();
636 if (FORCE_ZIP64_EXT && UNKNOWN != offset || UInt.MAX_VALUE <= offset) {
637 writeLong(offset, data, off);
638 off += 8;
639 }
640
641 final ExtraField field;
642 if (off > 0) {
643 field = new DefaultExtraField(ZIP64_HEADER_ID);
644 field.readFrom(data, 0, off);
645 } else {
646 field = null;
647 }
648 return field;
649 }
650
651
652
653
654
655
656 private void parseZip64ExtraField(final ExtraFields fields)
657 throws IndexOutOfBoundsException {
658 final ExtraField ef = fields.get(ZIP64_HEADER_ID);
659 if (null == ef) return;
660 final byte[] data = ef.getDataBlock();
661 int off = 0;
662
663 final long size = getRawSize();
664 if (UInt.MAX_VALUE <= size) {
665 assert UInt.MAX_VALUE == size;
666 setRawSize(readLong(data, off));
667 off += 8;
668 }
669
670 final long csize = getRawCompressedSize();
671 if (UInt.MAX_VALUE <= csize) {
672 assert UInt.MAX_VALUE == csize;
673 setRawCompressedSize(readLong(data, off));
674 off += 8;
675 }
676
677 final long offset = getRawOffset();
678 if (UInt.MAX_VALUE <= offset) {
679 assert UInt.MAX_VALUE == offset;
680 setRawOffset(readLong(data, off));
681
682 }
683 }
684
685 public final @CheckForNull String getComment() { return comment; }
686
687
688
689
690
691
692
693
694
695
696
697 public final void setComment(final @CheckForNull String comment) {
698 if (null != comment)
699 UShort.check(comment.length(), name, "Comment too long");
700 this.comment = comment;
701 }
702
703 final String getRawComment() {
704 final String comment = this.comment;
705 return null != comment ? comment : "";
706 }
707
708 final void setRawComment(final String comment) {
709 assert UShort.check(comment.length());
710 this.comment = comment;
711 }
712
713 final boolean isDataDescriptorRequired() {
714 return UNKNOWN == (getCrc() | getCompressedSize() | getSize());
715 }
716
717 final boolean isZip64ExtensionsRequired() {
718
719
720 if (FORCE_ZIP64_EXT) return true;
721 return UInt.MAX_VALUE <= getCompressedSize()
722 || UInt.MAX_VALUE <= getSize()
723 || UInt.MAX_VALUE <= getOffset();
724 }
725
726
727
728
729
730 @Override
731 public String toString() {
732 final StringBuilder s = new StringBuilder(256);
733 final Formatter f = new Formatter(s)
734 .format("%s[name=%s", getClass().getName(), getName());
735 long value;
736 if (UNKNOWN != (value = getGeneralPurposeBitFlags()))
737 f.format(", gpbf=0x%04X", value);
738 if (UNKNOWN != (value = getMethod()))
739 f.format(", method=%d", value);
740 if (UNKNOWN != (value = getTime()))
741 f.format(", time=%tc", value);
742 if (UNKNOWN != (value = getCrc()))
743 f.format(", crc=0x%08X", value);
744 if (UNKNOWN != (value = getCompressedSize()))
745 f.format(", compressedSize=%d", value);
746 if (UNKNOWN != (value = getSize()))
747 f.format(", size=%d", value);
748 if (UNKNOWN != (value = getExternalAttributes()))
749 f.format(", ea=0x%08X", value);
750 {
751 final String comment = getComment();
752 if (null != comment)
753 f.format(", comment=\"%s\"", comment);
754 }
755 return s.append("]").toString();
756 }
757 }