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