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