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.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       * Encoding to use for filenames, defaults to the platform's
64       * default encoding.
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      // shadow of the above if the value is altered in execute
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       * @deprecated Use {@link Archiver#setDuplicateBehavior(String)} instead.
93       */
94      @Deprecated
95      protected final String duplicate = Archiver.DUPLICATES_SKIP;
96  
97      /**
98       * true when we are adding new files into the Zip file, as opposed
99       * to adding back the unchanged files
100      */
101     protected boolean addingNewFiles = false;
102 
103     // Renamed version of original file, if it exists
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      * If true, emulate Sun's jar utility by not adding parent directories;
157      * optional, defaults to false.
158      *
159      * @param f true to emilate sun jar utility
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         //noinspection deprecation
191         if (!Archiver.DUPLICATES_SKIP.equals(duplicate)) {
192             //noinspection deprecation
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         // Whether or not an actual update is required -
216         // we don't need to update if the original file doesn't exist
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         // Add the new files to the archive.
260         addResources(iter, zOut);
261 
262         // If we've been successful on an update, delete the
263         // temporary file
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      * Gets the {@code UnicodeExtraFieldPolicy} to apply.
274      *
275      * @return {@link ZipArchiveOutputStream.UnicodeExtraFieldPolicy#NEVER}, if the effective encoding is
276      * UTF-8; {@link ZipArchiveOutputStream.UnicodeExtraFieldPolicy#ALWAYS}, if the effective encoding is not
277      * UTF-8.
278      *
279      * @see #getEncoding()
280      */
281     private ZipArchiveOutputStream.UnicodeExtraFieldPolicy getUnicodeExtraFieldPolicy() {
282         // Copied from ZipEncodingHelper.isUTF8()
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         // Using ZipArchiveOutputStream.UnicodeExtraFieldPolicy.NOT_ENCODEABLE as a fallback makes no sense here.
301         // If the encoding is UTF-8 and a name is not encodeable using UTF-8, the Info-ZIP Unicode Path extra field
302         // is not encodeable as well. If the effective encoding is not UTF-8, we always add the extra field. If it is
303         // UTF-8, we never add the extra field.
304         return utf8
305                 ? ZipArchiveOutputStream.UnicodeExtraFieldPolicy.NEVER
306                 : ZipArchiveOutputStream.UnicodeExtraFieldPolicy.ALWAYS;
307     }
308 
309     /**
310      * Add the given resources.
311      *
312      * @param resources the resources to add
313      * @param zOut the stream to write to
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      * Ensure all parent dirs of a given entry have been added.
343      * <p/>
344      * This method is computed in terms of the potentially remapped entry (that may be disconnected from the file system)
345      * we do not *relly* know the entry's connection to the file system so establishing the attributes of the parents can
346      * be impossible and is not really supported.
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                 // the
363                 // At this point we could do something like read the atr
364                 final PlexusIoResource res = new AnonymousResource(f);
365                 zipDir(res, zOut, dir, archiveEntry.getDefaultDirMode(), encoding);
366             }
367         }
368     }
369 
370     /**
371      * Adds a new entry to the archive, takes care of duplicates as well.
372      *
373      * @param in the stream to read data for the entry from.
374      * @param zOut the stream to write to.
375      * @param vPath the name this entry shall have in the archive.
376      * @param lastModified last modification time for the entry.
377      * @param fromArchive the original archive we are copying this
378      * @param symlinkDestination
379      * @param addInParallel Indicates if the entry should be add in parallel.
380      * If set to {@code false} it is added synchronously.
381      * If the entry is symbolic link this parameter is ignored.
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      * Method that gets called when adding from java.io.File instances.
417      * <p>
418      * This implementation delegates to the six-arg version.</p>
419      *
420      * @param entry the file to add to the archive
421      * @param zOut the stream to write to
422      * @param vPath the name this entry shall have in the archive
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      * Set the ZipEntry dostime using the date if specified otherwise the original time.
458      *
459      * <p>Zip archives store file modification times with a granularity of two seconds, so the times will either be
460      * rounded up or down. If you round down, the archive will always seem out-of-date when you rerun the task, so the
461      * default is to round up. Rounding up may lead to a different type of problems like JSPs inside a web archive that
462      * seem to be slightly more recent than precompiled pages, rendering precompilation useless.
463      * plexus-archiver chooses to round up.
464      *
465      * @param zipEntry to set the last modified time
466      * @param lastModifiedTime to set in the zip entry only if {@link #getLastModifiedTime()} returns null
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              * ZipOutputStream.putNextEntry expects the ZipEntry to
495              * know its size and the CRC sum before you start writing
496              * the data when using STORED mode - unless it is seekable.
497              *
498              * This forces us to process the data twice.
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                 // ZIPs store time with a granularity of 2 seconds, round up
508                 setZipEntryTime(ze, System.currentTimeMillis());
509             }
510             if (!isSymlink) {
511                 ze.setSize(0);
512                 ze.setMethod(ZipArchiveEntry.STORED);
513                 // This is faintly ridiculous:
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      * Create an empty zip file
540      *
541      * @param zipFile The file
542      *
543      * @return true for historic reasons
544      */
545     @SuppressWarnings({"JavaDoc"})
546     protected boolean createEmptyZip(File zipFile) throws ArchiverException {
547         // In this case using java.util.zip will not work
548         // because it does not permit a zero-entry archive.
549         // Must create it manually.
550         getLogger().info("Note: creating empty " + archiveType + " archive " + zipFile);
551 
552         try (OutputStream os = Streams.fileOutputStream(zipFile.toPath())) {
553             // Cf. PKZIP specification.
554             byte[] empty = new byte[22];
555             empty[0] = 80; // P
556             empty[1] = 75; // K
557             empty[2] = 5;
558             empty[3] = 6;
559             // remainder zeros
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      * Do any clean up necessary to allow this instance to be used again.
569      * <p>
570      * When we get here, the Zip file has been closed and all we
571      * need to do is to reset some globals.</p>
572      * <p>
573      * This method will only reset globals that have been changed
574      * during execute(), it will not alter the attributes or nested
575      * child elements. If you want to reset the instance so that you
576      * can later zip a completely different set of files, you must use
577      * the reset method.</p>
578      *
579      * @see #reset
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      * Makes this instance reset all attributes to their default
596      * values and forget all children.
597      *
598      * @see #cleanUp
599      */
600     public void reset() {
601         setDestFile(null);
602         //        duplicate = "add";
603         archiveType = "zip";
604         doCompress = true;
605         doUpdate = false;
606         doFilesonly = false;
607         encoding = null;
608     }
609 
610     /**
611      * method for subclasses to override
612      *
613      * @param zOut The output stream
614      */
615     protected void initZipOutputStream(ConcurrentJarCreator zOut) throws ArchiverException, IOException {}
616 
617     /**
618      * method for subclasses to override
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         // delete a bogus ZIP file (but only if it's not the original one)
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         // Close the output stream.
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             // If we're in this finally clause because of an
661             // exception, we don't really care if there's an
662             // exception when closing the stream. E.g. if it
663             // throws "ZIP file must have at least one entry",
664             // because an exception happened before we added
665             // any files, then we must swallow this
666             // exception. Otherwise, the error that's reported
667             // will be the close() error, which is not the
668             // real cause of the problem.
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         // timestamp of zip entries at zip storage level ignores timezone: managed in ZipEntry.setTime,
688         // that turns javaToDosTime: need to revert the operation here to get reproducible
689         // zip entry time
690         return FileTime.fromMillis(dosToJavaTime(lastModifiedTime.toMillis()));
691     }
692 
693     /**
694      * Converts DOS time to Java time (number of milliseconds since epoch).
695      *
696      * @see java.util.zip.ZipEntry#setTime
697      * @see java.util.zip.ZipUtils#dosToJavaTime
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 }