View Javadoc
1   /**
2    *
3    * Copyright 2004 The Apache Software Foundation
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    * http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
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       * Encoding to use for filenames, defaults to the platform's
66       * default encoding.
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      // shadow of the above if the value is altered in execute
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       * @deprecated Use {@link Archiver#setDuplicateBehavior(String)} instead.
95       */
96      @Deprecated
97      protected final String duplicate = Archiver.DUPLICATES_SKIP;
98  
99      /**
100      * true when we are adding new files into the Zip file, as opposed
101      * to adding back the unchanged files
102      */
103     protected boolean addingNewFiles = false;
104 
105     // Renamed version of original file, if it exists
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      * If true, emulate Sun's jar utility by not adding parent directories;
159      * optional, defaults to false.
160      *
161      * @param f true to emilate sun jar utility
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         //noinspection deprecation
193         if (!Archiver.DUPLICATES_SKIP.equals(duplicate)) {
194             //noinspection deprecation
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         // Whether or not an actual update is required -
218         // we don't need to update if the original file doesn't exist
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         // Add the new files to the archive.
262         addResources(iter, zOut);
263 
264         // If we've been successful on an update, delete the
265         // temporary file
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      * Gets the {@code UnicodeExtraFieldPolicy} to apply.
276      *
277      * @return {@link ZipArchiveOutputStream.UnicodeExtraFieldPolicy#NEVER}, if the effective encoding is
278      * UTF-8; {@link ZipArchiveOutputStream.UnicodeExtraFieldPolicy#ALWAYS}, if the effective encoding is not
279      * UTF-8.
280      *
281      * @see #getEncoding()
282      */
283     private ZipArchiveOutputStream.UnicodeExtraFieldPolicy getUnicodeExtraFieldPolicy() {
284         // Copied from ZipEncodingHelper.isUTF8()
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         // Using ZipArchiveOutputStream.UnicodeExtraFieldPolicy.NOT_ENCODEABLE as a fallback makes no sense here.
303         // If the encoding is UTF-8 and a name is not encodeable using UTF-8, the Info-ZIP Unicode Path extra field
304         // is not encodeable as well. If the effective encoding is not UTF-8, we always add the extra field. If it is
305         // UTF-8, we never add the extra field.
306         return utf8
307                 ? ZipArchiveOutputStream.UnicodeExtraFieldPolicy.NEVER
308                 : ZipArchiveOutputStream.UnicodeExtraFieldPolicy.ALWAYS;
309     }
310 
311     /**
312      * Add the given resources.
313      *
314      * @param resources the resources to add
315      * @param zOut the stream to write to
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      * Ensure all parent dirs of a given entry have been added.
345      * <p/>
346      * This method is computed in terms of the potentially remapped entry (that may be disconnected from the file system)
347      * we do not *relly* know the entry's connection to the file system so establishing the attributes of the parents can
348      * be impossible and is not really supported.
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                 // the
365                 // At this point we could do something like read the atr
366                 final PlexusIoResource res = new AnonymousResource(f);
367                 zipDir(res, zOut, dir, archiveEntry.getDefaultDirMode(), encoding);
368             }
369         }
370     }
371 
372     /**
373      * Adds a new entry to the archive, takes care of duplicates as well.
374      *
375      * @param in the stream to read data for the entry from.
376      * @param zOut the stream to write to.
377      * @param vPath the name this entry shall have in the archive.
378      * @param lastModified last modification time for the entry.
379      * @param fromArchive the original archive we are copying this
380      * @param symlinkDestination
381      * @param addInParallel Indicates if the entry should be add in parallel.
382      * If set to {@code false} it is added synchronously.
383      * If the entry is symbolic link this parameter is ignored.
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      * Method that gets called when adding from java.io.File instances.
419      * <p>
420      * This implementation delegates to the six-arg version.</p>
421      *
422      * @param entry the file to add to the archive
423      * @param zOut the stream to write to
424      * @param vPath the name this entry shall have in the archive
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      * Set the ZipEntry dostime using the date if specified otherwise the original time.
460      *
461      * <p>Zip archives store file modification times with a granularity of two seconds, so the times will either be
462      * rounded up or down. If you round down, the archive will always seem out-of-date when you rerun the task, so the
463      * default is to round up. Rounding up may lead to a different type of problems like JSPs inside a web archive that
464      * seem to be slightly more recent than precompiled pages, rendering precompilation useless.
465      * plexus-archiver chooses to round up.
466      *
467      * @param zipEntry to set the last modified time
468      * @param lastModifiedTime to set in the zip entry only if {@link #getLastModifiedTime()} returns null
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              * ZipOutputStream.putNextEntry expects the ZipEntry to
497              * know its size and the CRC sum before you start writing
498              * the data when using STORED mode - unless it is seekable.
499              *
500              * This forces us to process the data twice.
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                 // ZIPs store time with a granularity of 2 seconds, round up
510                 setZipEntryTime(ze, System.currentTimeMillis());
511             }
512             if (!isSymlink) {
513                 ze.setSize(0);
514                 ze.setMethod(ZipArchiveEntry.STORED);
515                 // This is faintly ridiculous:
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      * Create an empty zip file
542      *
543      * @param zipFile The file
544      *
545      * @return true for historic reasons
546      */
547     @SuppressWarnings({"JavaDoc"})
548     protected boolean createEmptyZip(File zipFile) throws ArchiverException {
549         // In this case using java.util.zip will not work
550         // because it does not permit a zero-entry archive.
551         // Must create it manually.
552         getLogger().info("Note: creating empty " + archiveType + " archive " + zipFile);
553 
554         try (OutputStream os = Streams.fileOutputStream(zipFile.toPath())) {
555             // Cf. PKZIP specification.
556             byte[] empty = new byte[22];
557             empty[0] = 80; // P
558             empty[1] = 75; // K
559             empty[2] = 5;
560             empty[3] = 6;
561             // remainder zeros
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      * Returns a map of the files that have been added to the archive.
571      * This method is overridden to normalize path separators to forward slashes,
572      * as required by the ZIP file format specification.
573      *
574      * @return A map where keys are entry names with forward slashes as separators,
575      *         and values are the corresponding ArchiveEntry objects.
576      * @deprecated Use {@link #getResources()} instead.
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      * Do any clean up necessary to allow this instance to be used again.
594      * <p>
595      * When we get here, the Zip file has been closed and all we
596      * need to do is to reset some globals.</p>
597      * <p>
598      * This method will only reset globals that have been changed
599      * during execute(), it will not alter the attributes or nested
600      * child elements. If you want to reset the instance so that you
601      * can later zip a completely different set of files, you must use
602      * the reset method.</p>
603      *
604      * @see #reset
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      * Makes this instance reset all attributes to their default
621      * values and forget all children.
622      *
623      * @see #cleanUp
624      */
625     public void reset() {
626         setDestFile(null);
627         //        duplicate = "add";
628         archiveType = "zip";
629         doCompress = true;
630         doUpdate = false;
631         doFilesonly = false;
632         encoding = null;
633     }
634 
635     /**
636      * method for subclasses to override
637      *
638      * @param zOut The output stream
639      */
640     protected void initZipOutputStream(ConcurrentJarCreator zOut) throws ArchiverException, IOException {}
641 
642     /**
643      * method for subclasses to override
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         // delete a bogus ZIP file (but only if it's not the original one)
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         // Close the output stream.
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             // If we're in this finally clause because of an
686             // exception, we don't really care if there's an
687             // exception when closing the stream. E.g. if it
688             // throws "ZIP file must have at least one entry",
689             // because an exception happened before we added
690             // any files, then we must swallow this
691             // exception. Otherwise, the error that's reported
692             // will be the close() error, which is not the
693             // real cause of the problem.
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         // timestamp of zip entries at zip storage level ignores timezone: managed in ZipEntry.setTime,
713         // that turns javaToDosTime: need to revert the operation here to get reproducible
714         // zip entry time
715         return FileTime.fromMillis(dosToJavaTime(lastModifiedTime.toMillis()));
716     }
717 
718     /**
719      * Converts DOS time to Java time (number of milliseconds since epoch).
720      *
721      * @see java.util.zip.ZipEntry#setTime
722      * @see java.util.zip.ZipUtils#dosToJavaTime
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     // minimum DOS time that will give a positive Java time, whatever the current TZ is:
734     // biggest TZ offset is for Etc/GMT-14 https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
735     private static final long MIN_DOS_JAVA_TIME = 1000 * 14 * 3600;
736 }