1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.codehaus.plexus.archiver.zip;
18
19 import java.io.ByteArrayInputStream;
20 import java.io.File;
21 import java.io.IOException;
22 import java.io.OutputStream;
23 import java.io.UncheckedIOException;
24 import java.nio.ByteBuffer;
25 import java.nio.charset.Charset;
26 import java.nio.charset.StandardCharsets;
27 import java.nio.file.attribute.FileTime;
28 import java.util.Calendar;
29 import java.util.Deque;
30 import java.util.HashMap;
31 import java.util.Hashtable;
32 import java.util.Locale;
33 import java.util.Map;
34 import java.util.TimeZone;
35 import java.util.concurrent.ExecutionException;
36 import java.util.zip.CRC32;
37
38 import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
39 import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
40 import org.apache.commons.compress.archivers.zip.ZipEncoding;
41 import org.apache.commons.compress.archivers.zip.ZipEncodingHelper;
42 import org.apache.commons.compress.parallel.InputStreamSupplier;
43 import org.codehaus.plexus.archiver.AbstractArchiver;
44 import org.codehaus.plexus.archiver.ArchiveEntry;
45 import org.codehaus.plexus.archiver.Archiver;
46 import org.codehaus.plexus.archiver.ArchiverException;
47 import org.codehaus.plexus.archiver.ResourceIterator;
48 import org.codehaus.plexus.archiver.UnixStat;
49 import org.codehaus.plexus.archiver.exceptions.EmptyArchiveException;
50 import org.codehaus.plexus.archiver.util.ResourceUtils;
51 import org.codehaus.plexus.archiver.util.Streams;
52 import org.codehaus.plexus.components.io.functions.SymlinkDestinationSupplier;
53 import org.codehaus.plexus.components.io.resources.PlexusIoResource;
54 import org.codehaus.plexus.util.FileUtils;
55
56 import static org.codehaus.plexus.archiver.util.Streams.bufferedOutputStream;
57 import static org.codehaus.plexus.archiver.util.Streams.fileOutputStream;
58
59 @SuppressWarnings({"UnusedDeclaration"})
60 public abstract class AbstractZipArchiver extends AbstractArchiver {
61
62 private String comment;
63
64
65
66
67
68 private String encoding = "UTF8";
69
70 private boolean doCompress = true;
71
72 private boolean recompressAddedZips = true;
73
74 private boolean doUpdate = false;
75
76
77 private boolean savedDoUpdate = false;
78
79 protected String archiveType = "zip";
80
81 private boolean doFilesonly = false;
82
83 protected final Hashtable<String, String> entries = new Hashtable<String, String>();
84
85 protected final AddedDirs addedDirs = new AddedDirs();
86
87 private static final long EMPTY_CRC = new CRC32().getValue();
88
89 protected boolean doubleFilePass = false;
90
91 protected boolean skipWriting = false;
92
93
94
95
96 @Deprecated
97 protected final String duplicate = Archiver.DUPLICATES_SKIP;
98
99
100
101
102
103 protected boolean addingNewFiles = false;
104
105
106 private File renamedFile = null;
107
108 private File zipFile;
109
110 private boolean success;
111
112 private ConcurrentJarCreator zOut;
113
114 protected ZipArchiveOutputStream zipArchiveOutputStream;
115
116 public String getComment() {
117 return comment;
118 }
119
120 public void setComment(String comment) {
121 this.comment = comment;
122 }
123
124 public String getEncoding() {
125 return encoding;
126 }
127
128 public void setEncoding(String encoding) {
129 this.encoding = encoding;
130 }
131
132 public void setCompress(boolean compress) {
133 this.doCompress = compress;
134 }
135
136 public boolean isCompress() {
137 return doCompress;
138 }
139
140 public boolean isRecompressAddedZips() {
141 return recompressAddedZips;
142 }
143
144 public void setRecompressAddedZips(boolean recompressAddedZips) {
145 this.recompressAddedZips = recompressAddedZips;
146 }
147
148 public void setUpdateMode(boolean update) {
149 this.doUpdate = update;
150 savedDoUpdate = doUpdate;
151 }
152
153 public boolean isInUpdateMode() {
154 return doUpdate;
155 }
156
157
158
159
160
161
162
163 public void setFilesonly(boolean f) {
164 doFilesonly = f;
165 }
166
167 public boolean isFilesonly() {
168 return doFilesonly;
169 }
170
171 @Override
172 protected void execute() throws ArchiverException, IOException {
173 if (!checkForced()) {
174 return;
175 }
176
177 if (doubleFilePass) {
178 skipWriting = true;
179 createArchiveMain();
180 skipWriting = false;
181 createArchiveMain();
182 } else {
183 createArchiveMain();
184 }
185
186 finalizeZipOutputStream(zOut);
187 }
188
189 protected void finalizeZipOutputStream(ConcurrentJarCreator zOut) throws IOException, ArchiverException {}
190
191 private void createArchiveMain() throws ArchiverException, IOException {
192
193 if (!Archiver.DUPLICATES_SKIP.equals(duplicate)) {
194
195 setDuplicateBehavior(duplicate);
196 }
197
198 ResourceIterator iter = getResources();
199 if (!iter.hasNext() && !hasVirtualFiles()) {
200 throw new EmptyArchiveException("archive cannot be empty");
201 }
202
203 zipFile = getDestFile();
204
205 if (zipFile == null) {
206 throw new ArchiverException("You must set the destination " + archiveType + "file.");
207 }
208
209 if (zipFile.exists() && !zipFile.isFile()) {
210 throw new ArchiverException(zipFile + " isn't a file.");
211 }
212
213 if (zipFile.exists() && !zipFile.canWrite()) {
214 throw new ArchiverException(zipFile + " is read-only.");
215 }
216
217
218
219 addingNewFiles = true;
220
221 if (doUpdate && !zipFile.exists()) {
222 doUpdate = false;
223 getLogger().debug("ignoring update attribute as " + archiveType + " doesn't exist.");
224 }
225
226 success = false;
227
228 if (doUpdate) {
229 renamedFile = FileUtils.createTempFile("zip", ".tmp", zipFile.getParentFile());
230 renamedFile.deleteOnExit();
231
232 try {
233 FileUtils.rename(zipFile, renamedFile);
234 } catch (SecurityException e) {
235 getLogger().debug(e.toString());
236 throw new ArchiverException(
237 "Not allowed to rename old file (" + zipFile.getAbsolutePath() + ") to temporary file", e);
238 } catch (IOException e) {
239 getLogger().debug(e.toString());
240 throw new ArchiverException(
241 "Unable to rename old file (" + zipFile.getAbsolutePath() + ") to temporary file", e);
242 }
243 }
244
245 String action = doUpdate ? "Updating " : "Building ";
246
247 getLogger().info(action + archiveType + ": " + zipFile.getAbsolutePath());
248
249 if (!skipWriting) {
250 zipArchiveOutputStream = new ZipArchiveOutputStream(bufferedOutputStream(fileOutputStream(zipFile, "zip")));
251 zipArchiveOutputStream.setEncoding(encoding);
252 zipArchiveOutputStream.setCreateUnicodeExtraFields(this.getUnicodeExtraFieldPolicy());
253 zipArchiveOutputStream.setMethod(
254 doCompress ? ZipArchiveOutputStream.DEFLATED : ZipArchiveOutputStream.STORED);
255
256 zOut = new ConcurrentJarCreator(
257 recompressAddedZips, Runtime.getRuntime().availableProcessors());
258 }
259 initZipOutputStream(zOut);
260
261
262 addResources(iter, zOut);
263
264
265
266 if (doUpdate) {
267 if (!renamedFile.delete()) {
268 getLogger().warn("Warning: unable to delete temporary file " + renamedFile.getName());
269 }
270 }
271 success = true;
272 }
273
274
275
276
277
278
279
280
281
282
283 private ZipArchiveOutputStream.UnicodeExtraFieldPolicy getUnicodeExtraFieldPolicy() {
284
285 String effectiveEncoding = this.getEncoding();
286
287 if (effectiveEncoding == null) {
288 effectiveEncoding = Charset.defaultCharset().name();
289 }
290
291 boolean utf8 = StandardCharsets.UTF_8.name().equalsIgnoreCase(effectiveEncoding);
292
293 if (!utf8) {
294 for (String alias : StandardCharsets.UTF_8.aliases()) {
295 if (alias.equalsIgnoreCase(effectiveEncoding)) {
296 utf8 = true;
297 break;
298 }
299 }
300 }
301
302
303
304
305
306 return utf8
307 ? ZipArchiveOutputStream.UnicodeExtraFieldPolicy.NEVER
308 : ZipArchiveOutputStream.UnicodeExtraFieldPolicy.ALWAYS;
309 }
310
311
312
313
314
315
316
317 @SuppressWarnings({"JavaDoc"})
318 protected final void addResources(ResourceIterator resources, ConcurrentJarCreator zOut)
319 throws IOException, ArchiverException {
320 while (resources.hasNext()) {
321 ArchiveEntry entry = resources.next();
322 String name = entry.getName();
323 name = name.replace(File.separatorChar, '/');
324
325 if ("".equals(name)) {
326 continue;
327 }
328
329 if (entry.getResource().isDirectory() && !name.endsWith("/")) {
330 name = name + "/";
331 }
332
333 addParentDirs(entry, null, name, zOut);
334
335 if (entry.getResource().isFile()) {
336 zipFile(entry, zOut, name);
337 } else {
338 zipDir(entry.getResource(), zOut, name, entry.getMode(), encoding);
339 }
340 }
341 }
342
343
344
345
346
347
348
349
350 @SuppressWarnings({"JavaDoc"})
351 private void addParentDirs(ArchiveEntry archiveEntry, File baseDir, String entry, ConcurrentJarCreator zOut)
352 throws IOException {
353 if (!doFilesonly && getIncludeEmptyDirs()) {
354 Deque<String> directories = addedDirs.asStringDeque(entry);
355
356 while (!directories.isEmpty()) {
357 String dir = directories.pop();
358 File f;
359 if (baseDir != null) {
360 f = new File(baseDir, dir);
361 } else {
362 f = new File(dir);
363 }
364
365
366 final PlexusIoResource res = new AnonymousResource(f);
367 zipDir(res, zOut, dir, archiveEntry.getDefaultDirMode(), encoding);
368 }
369 }
370 }
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385 @SuppressWarnings({"JavaDoc"})
386 protected void zipFile(
387 InputStreamSupplier in,
388 ConcurrentJarCreator zOut,
389 String vPath,
390 long lastModified,
391 File fromArchive,
392 int mode,
393 String symlinkDestination,
394 boolean addInParallel)
395 throws IOException, ArchiverException {
396 getLogger().debug("adding entry " + vPath);
397
398 entries.put(vPath, vPath);
399
400 if (!skipWriting) {
401 ZipArchiveEntry ze = new ZipArchiveEntry(vPath);
402 setZipEntryTime(ze, lastModified);
403
404 ze.setMethod(doCompress ? ZipArchiveEntry.DEFLATED : ZipArchiveEntry.STORED);
405 ze.setUnixMode(UnixStat.FILE_FLAG | mode);
406
407 if (ze.isUnixSymlink()) {
408 final byte[] bytes = encodeArchiveEntry(symlinkDestination, getEncoding());
409 InputStreamSupplier payload = () -> new ByteArrayInputStream(bytes);
410 zOut.addArchiveEntry(ze, payload, true);
411 } else {
412 zOut.addArchiveEntry(ze, in, addInParallel);
413 }
414 }
415 }
416
417
418
419
420
421
422
423
424
425
426 @SuppressWarnings({"JavaDoc"})
427 protected void zipFile(final ArchiveEntry entry, ConcurrentJarCreator zOut, String vPath)
428 throws IOException, ArchiverException {
429 final PlexusIoResource resource = entry.getResource();
430 if (ResourceUtils.isSame(resource, getDestFile())) {
431 throw new ArchiverException("A zip file cannot include itself");
432 }
433
434 final boolean b = entry.getResource() instanceof SymlinkDestinationSupplier;
435 String symlinkTarget = b ? ((SymlinkDestinationSupplier) entry.getResource()).getSymlinkDestination() : null;
436 InputStreamSupplier in = () -> {
437 try {
438 return entry.getInputStream();
439 } catch (IOException e) {
440 throw new UncheckedIOException(e);
441 }
442 };
443 try {
444 zipFile(
445 in,
446 zOut,
447 vPath,
448 resource.getLastModified(),
449 null,
450 entry.getMode(),
451 symlinkTarget,
452 !entry.shouldAddSynchronously());
453 } catch (IOException e) {
454 throw new ArchiverException("IOException when zipping r" + entry.getName() + ": " + e.getMessage(), e);
455 }
456 }
457
458
459
460
461
462
463
464
465
466
467
468
469
470 protected void setZipEntryTime(ZipArchiveEntry zipEntry, long lastModifiedTime) {
471 if (getLastModifiedTime() != null) {
472 lastModifiedTime = getLastModifiedTime().toMillis();
473 }
474
475 zipEntry.setTime(lastModifiedTime + 1999);
476 }
477
478 protected void zipDir(PlexusIoResource dir, ConcurrentJarCreator zOut, String vPath, int mode, String encodingToUse)
479 throws IOException {
480 if (addedDirs.update(vPath)) {
481 return;
482 }
483
484 getLogger().debug("adding directory " + vPath);
485
486 if (!skipWriting) {
487 final boolean isSymlink = dir instanceof SymlinkDestinationSupplier;
488
489 if (isSymlink && vPath.endsWith(File.separator)) {
490 vPath = vPath.substring(0, vPath.length() - 1);
491 }
492
493 ZipArchiveEntry ze = new ZipArchiveEntry(vPath);
494
495
496
497
498
499
500
501
502 if (isSymlink) {
503 mode = UnixStat.LINK_FLAG | mode;
504 }
505
506 if (dir != null && dir.isExisting()) {
507 setZipEntryTime(ze, dir.getLastModified());
508 } else {
509
510 setZipEntryTime(ze, System.currentTimeMillis());
511 }
512 if (!isSymlink) {
513 ze.setSize(0);
514 ze.setMethod(ZipArchiveEntry.STORED);
515
516 ze.setCrc(EMPTY_CRC);
517 }
518 ze.setUnixMode(mode);
519
520 if (!isSymlink) {
521 zOut.addArchiveEntry(ze, () -> Streams.EMPTY_INPUTSTREAM, true);
522 } else {
523 String symlinkDestination = ((SymlinkDestinationSupplier) dir).getSymlinkDestination();
524 final byte[] bytes = encodeArchiveEntry(symlinkDestination, encodingToUse);
525 ze.setMethod(ZipArchiveEntry.DEFLATED);
526 zOut.addArchiveEntry(ze, () -> new ByteArrayInputStream(bytes), true);
527 }
528 }
529 }
530
531 private byte[] encodeArchiveEntry(String payload, String encoding) throws IOException {
532 ZipEncoding enc = ZipEncodingHelper.getZipEncoding(encoding);
533 ByteBuffer encodedPayloadByteBuffer = enc.encode(payload);
534 byte[] encodedPayloadBytes = new byte[encodedPayloadByteBuffer.limit()];
535 encodedPayloadByteBuffer.get(encodedPayloadBytes);
536
537 return encodedPayloadBytes;
538 }
539
540
541
542
543
544
545
546
547 @SuppressWarnings({"JavaDoc"})
548 protected boolean createEmptyZip(File zipFile) throws ArchiverException {
549
550
551
552 getLogger().info("Note: creating empty " + archiveType + " archive " + zipFile);
553
554 try (OutputStream os = Streams.fileOutputStream(zipFile.toPath())) {
555
556 byte[] empty = new byte[22];
557 empty[0] = 80;
558 empty[1] = 75;
559 empty[2] = 5;
560 empty[3] = 6;
561
562 os.write(empty);
563 } catch (IOException ioe) {
564 throw new ArchiverException("Could not create empty ZIP archive " + "(" + ioe.getMessage() + ")", ioe);
565 }
566 return true;
567 }
568
569
570
571
572
573
574
575
576
577
578 @Override
579 @Deprecated
580 public Map<String, ArchiveEntry> getFiles() {
581 Map<String, ArchiveEntry> files = super.getFiles();
582 Map<String, ArchiveEntry> normalizedFiles = new HashMap<>();
583
584 for (Map.Entry<String, ArchiveEntry> entry : files.entrySet()) {
585 String normalizedPath = entry.getKey().replace(File.separatorChar, '/');
586 normalizedFiles.put(normalizedPath, entry.getValue());
587 }
588
589 return normalizedFiles;
590 }
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606 @Override
607 protected void cleanUp() throws IOException {
608 super.cleanUp();
609 addedDirs.clear();
610 entries.clear();
611 addingNewFiles = false;
612 doUpdate = savedDoUpdate;
613 success = false;
614 zOut = null;
615 renamedFile = null;
616 zipFile = null;
617 }
618
619
620
621
622
623
624
625 public void reset() {
626 setDestFile(null);
627
628 archiveType = "zip";
629 doCompress = true;
630 doUpdate = false;
631 doFilesonly = false;
632 encoding = null;
633 }
634
635
636
637
638
639
640 protected void initZipOutputStream(ConcurrentJarCreator zOut) throws ArchiverException, IOException {}
641
642
643
644
645 @Override
646 public boolean isSupportingForced() {
647 return true;
648 }
649
650 @Override
651 protected boolean revert(StringBuffer messageBuffer) {
652 int initLength = messageBuffer.length();
653
654
655 if ((!doUpdate || renamedFile != null) && !zipFile.delete()) {
656 messageBuffer.append(" (and the archive is probably corrupt but I could not delete it)");
657 }
658
659 if (doUpdate && renamedFile != null) {
660 try {
661 FileUtils.rename(renamedFile, zipFile);
662 } catch (IOException e) {
663 messageBuffer.append(" (and I couldn't rename the temporary file ");
664 messageBuffer.append(renamedFile.getName());
665 messageBuffer.append(" back)");
666 }
667 }
668
669 return messageBuffer.length() == initLength;
670 }
671
672 @Override
673 protected void close() throws IOException {
674
675 try {
676 if (zipArchiveOutputStream != null) {
677 if (zOut != null) {
678 zOut.writeTo(zipArchiveOutputStream);
679 } else {
680 zipArchiveOutputStream.close();
681 }
682 zipArchiveOutputStream = null;
683 }
684 } catch (IOException ex) {
685
686
687
688
689
690
691
692
693
694 if (success) {
695 throw ex;
696 }
697
698 } catch (InterruptedException e) {
699 throw new IOException("InterruptedException exception", e.getCause());
700 } catch (ExecutionException e) {
701 throw new IOException("Execution exception", e.getCause());
702 }
703 }
704
705 @Override
706 protected String getArchiveType() {
707 return archiveType;
708 }
709
710 @Override
711 protected FileTime normalizeLastModifiedTime(FileTime lastModifiedTime) {
712
713
714
715 return FileTime.fromMillis(dosToJavaTime(lastModifiedTime.toMillis()));
716 }
717
718
719
720
721
722
723
724 private static long dosToJavaTime(long dosTime) {
725 Calendar cal = Calendar.getInstance(TimeZone.getDefault(), Locale.ROOT);
726 if (dosTime < MIN_DOS_JAVA_TIME) {
727 dosTime = MIN_DOS_JAVA_TIME;
728 }
729 cal.setTimeInMillis(dosTime);
730 return dosTime - (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET));
731 }
732
733
734
735 private static final long MIN_DOS_JAVA_TIME = 1000 * 14 * 3600;
736 }