001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.commons.compress.archivers;
020
021import java.io.ByteArrayInputStream;
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.OutputStream;
025import java.security.AccessController;
026import java.security.PrivilegedAction;
027import java.util.Collections;
028import java.util.Locale;
029import java.util.ServiceLoader;
030import java.util.Set;
031import java.util.SortedMap;
032import java.util.TreeMap;
033
034import org.apache.commons.compress.archivers.ar.ArArchiveInputStream;
035import org.apache.commons.compress.archivers.ar.ArArchiveOutputStream;
036import org.apache.commons.compress.archivers.arj.ArjArchiveInputStream;
037import org.apache.commons.compress.archivers.cpio.CpioArchiveInputStream;
038import org.apache.commons.compress.archivers.cpio.CpioArchiveOutputStream;
039import org.apache.commons.compress.archivers.dump.DumpArchiveInputStream;
040import org.apache.commons.compress.archivers.jar.JarArchiveInputStream;
041import org.apache.commons.compress.archivers.jar.JarArchiveOutputStream;
042import org.apache.commons.compress.archivers.sevenz.SevenZFile;
043import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
044import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
045import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
046import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
047import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
048import org.apache.commons.compress.utils.IOUtils;
049import org.apache.commons.compress.utils.Sets;
050
051/**
052 * Factory to create Archive[In|Out]putStreams from names or the first bytes of the InputStream. In order to add other implementations, you should extend
053 * ArchiveStreamFactory and override the appropriate methods (and call their implementation from super of course).
054 *
055 * Compressing a ZIP-File:
056 *
057 * <pre>
058 * final OutputStream out = Files.newOutputStream(output.toPath());
059 * ArchiveOutputStream os = new ArchiveStreamFactory().createArchiveOutputStream(ArchiveStreamFactory.ZIP, out);
060 *
061 * os.putArchiveEntry(new ZipArchiveEntry("testdata/test1.xml"));
062 * IOUtils.copy(Files.newInputStream(file1.toPath()), os);
063 * os.closeArchiveEntry();
064 *
065 * os.putArchiveEntry(new ZipArchiveEntry("testdata/test2.xml"));
066 * IOUtils.copy(Files.newInputStream(file2.toPath()), os);
067 * os.closeArchiveEntry();
068 * os.close();
069 * </pre>
070 *
071 * Decompressing a ZIP-File:
072 *
073 * <pre>
074 * final InputStream is = Files.newInputStream(input.toPath());
075 * ArchiveInputStream in = new ArchiveStreamFactory().createArchiveInputStream(ArchiveStreamFactory.ZIP, is);
076 * ZipArchiveEntry entry = (ZipArchiveEntry) in.getNextEntry();
077 * OutputStream out = Files.newOutputStream(dir.toPath().resolve(entry.getName()));
078 * IOUtils.copy(in, out);
079 * out.close();
080 * in.close();
081 * </pre>
082 *
083 * @Immutable provided that the deprecated method setEntryEncoding is not used.
084 * @ThreadSafe even if the deprecated method setEntryEncoding is used
085 */
086public class ArchiveStreamFactory implements ArchiveStreamProvider {
087
088    private static final int TAR_HEADER_SIZE = 512;
089
090    private static final int TAR_TEST_ENTRY_COUNT = 10;
091
092    private static final int DUMP_SIGNATURE_SIZE = 32;
093
094    private static final int SIGNATURE_SIZE = 12;
095
096    /**
097     * The singleton instance using the platform default encoding.
098     *
099     * @since 1.21
100     */
101    public static final ArchiveStreamFactory DEFAULT = new ArchiveStreamFactory();
102
103    /**
104     * Constant (value {@value}) used to identify the APK archive format.
105     * <p>
106     * APK file extensions are .apk, .xapk, .apks, .apkm
107     * </p>
108     *
109     * @since 1.22
110     */
111    public static final String APK = "apk";
112
113    /**
114     * Constant (value {@value}) used to identify the XAPK archive format.
115     * <p>
116     * APK file extensions are .apk, .xapk, .apks, .apkm
117     * </p>
118     *
119     * @since 1.22
120     */
121    public static final String XAPK = "xapk";
122
123    /**
124     * Constant (value {@value}) used to identify the APKS archive format.
125     * <p>
126     * APK file extensions are .apk, .xapk, .apks, .apkm
127     * </p>
128     *
129     * @since 1.22
130     */
131    public static final String APKS = "apks";
132
133    /**
134     * Constant (value {@value}) used to identify the APKM archive format.
135     * <p>
136     * APK file extensions are .apk, .xapk, .apks, .apkm
137     * </p>
138     *
139     * @since 1.22
140     */
141    public static final String APKM = "apkm";
142
143    /**
144     * Constant (value {@value}) used to identify the AR archive format.
145     *
146     * @since 1.1
147     */
148    public static final String AR = "ar";
149
150    /**
151     * Constant (value {@value}) used to identify the ARJ archive format. Not supported as an output stream type.
152     *
153     * @since 1.6
154     */
155    public static final String ARJ = "arj";
156
157    /**
158     * Constant (value {@value}) used to identify the CPIO archive format.
159     *
160     * @since 1.1
161     */
162    public static final String CPIO = "cpio";
163
164    /**
165     * Constant (value {@value}) used to identify the Unix DUMP archive format. Not supported as an output stream type.
166     *
167     * @since 1.3
168     */
169    public static final String DUMP = "dump";
170
171    /**
172     * Constant (value {@value}) used to identify the JAR archive format.
173     *
174     * @since 1.1
175     */
176    public static final String JAR = "jar";
177
178    /**
179     * Constant used to identify the TAR archive format.
180     *
181     * @since 1.1
182     */
183    public static final String TAR = "tar";
184
185    /**
186     * Constant (value {@value}) used to identify the ZIP archive format.
187     *
188     * @since 1.1
189     */
190    public static final String ZIP = "zip";
191
192    /**
193     * Constant (value {@value}) used to identify the 7z archive format.
194     *
195     * @since 1.8
196     */
197    public static final String SEVEN_Z = "7z";
198
199    private static Iterable<ArchiveStreamProvider> archiveStreamProviderIterable() {
200        return ServiceLoader.load(ArchiveStreamProvider.class, ClassLoader.getSystemClassLoader());
201    }
202
203    /**
204     * Try to determine the type of Archiver
205     *
206     * @param in input stream
207     * @return type of archiver if found
208     * @throws ArchiveException if an archiver cannot be detected in the stream
209     * @since 1.14
210     */
211    public static String detect(final InputStream in) throws ArchiveException {
212        if (in == null) {
213            throw new IllegalArgumentException("Stream must not be null.");
214        }
215
216        if (!in.markSupported()) {
217            throw new IllegalArgumentException("Mark is not supported.");
218        }
219
220        final byte[] signature = new byte[SIGNATURE_SIZE];
221        in.mark(signature.length);
222        int signatureLength = -1;
223        try {
224            signatureLength = IOUtils.readFully(in, signature);
225            in.reset();
226        } catch (final IOException e) {
227            throw new ArchiveException("IOException while reading signature.", e);
228        }
229
230        // For now JAR files are detected as ZIP files.
231        if (ZipArchiveInputStream.matches(signature, signatureLength)) {
232            return ZIP;
233        }
234        // For now JAR files are detected as ZIP files.
235        if (JarArchiveInputStream.matches(signature, signatureLength)) {
236            return JAR;
237        }
238        if (ArArchiveInputStream.matches(signature, signatureLength)) {
239            return AR;
240        }
241        if (CpioArchiveInputStream.matches(signature, signatureLength)) {
242            return CPIO;
243        }
244        if (ArjArchiveInputStream.matches(signature, signatureLength)) {
245            return ARJ;
246        }
247        if (SevenZFile.matches(signature, signatureLength)) {
248            return SEVEN_Z;
249        }
250
251        // Dump needs a bigger buffer to check the signature;
252        final byte[] dumpsig = new byte[DUMP_SIGNATURE_SIZE];
253        in.mark(dumpsig.length);
254        try {
255            signatureLength = IOUtils.readFully(in, dumpsig);
256            in.reset();
257        } catch (final IOException e) {
258            throw new ArchiveException("IOException while reading dump signature", e);
259        }
260        if (DumpArchiveInputStream.matches(dumpsig, signatureLength)) {
261            return DUMP;
262        }
263
264        // Tar needs an even bigger buffer to check the signature; read the first block
265        final byte[] tarHeader = new byte[TAR_HEADER_SIZE];
266        in.mark(tarHeader.length);
267        try {
268            signatureLength = IOUtils.readFully(in, tarHeader);
269            in.reset();
270        } catch (final IOException e) {
271            throw new ArchiveException("IOException while reading tar signature", e);
272        }
273        if (TarArchiveInputStream.matches(tarHeader, signatureLength)) {
274            return TAR;
275        }
276
277        // COMPRESS-117
278        if (signatureLength >= TAR_HEADER_SIZE) {
279            try (TarArchiveInputStream inputStream = new TarArchiveInputStream(new ByteArrayInputStream(tarHeader))) {
280                // COMPRESS-191 - verify the header checksum
281                // COMPRESS-644 - do not allow zero byte file entries
282                TarArchiveEntry entry = inputStream.getNextEntry();
283                // try to find the first non-directory entry within the first 10 entries.
284                int count = 0;
285                while (entry != null && entry.isDirectory() && count++ < TAR_TEST_ENTRY_COUNT) {
286                    entry = inputStream.getNextEntry();
287                }
288                if (entry != null && entry.isCheckSumOK() && !entry.isDirectory() && entry.getSize() > 0 || count > 0) {
289                    return TAR;
290                }
291            } catch (final Exception e) { // NOPMD NOSONAR
292                // can generate IllegalArgumentException as well as IOException auto-detection, simply not a TAR ignored
293            }
294        }
295        throw new ArchiveException("No Archiver found for the stream signature");
296    }
297
298    /**
299     * Constructs a new sorted map from input stream provider names to provider objects.
300     *
301     * <p>
302     * The map returned by this method will have one entry for each provider for which support is available in the current Java virtual machine. If two or more
303     * supported provider have the same name then the resulting map will contain just one of them; which one it will contain is not specified.
304     * </p>
305     *
306     * <p>
307     * The invocation of this method, and the subsequent use of the resulting map, may cause time-consuming disk or network I/O operations to occur. This method
308     * is provided for applications that need to enumerate all of the available providers, for example to allow user provider selection.
309     * </p>
310     *
311     * <p>
312     * This method may return different results at different times if new providers are dynamically made available to the current Java virtual machine.
313     * </p>
314     *
315     * @return An immutable, map from names to provider objects
316     * @since 1.13
317     */
318    public static SortedMap<String, ArchiveStreamProvider> findAvailableArchiveInputStreamProviders() {
319        return AccessController.doPrivileged((PrivilegedAction<SortedMap<String, ArchiveStreamProvider>>) () -> {
320            final TreeMap<String, ArchiveStreamProvider> map = new TreeMap<>();
321            putAll(DEFAULT.getInputStreamArchiveNames(), DEFAULT, map);
322            archiveStreamProviderIterable().forEach(provider -> putAll(provider.getInputStreamArchiveNames(), provider, map));
323            return map;
324        });
325    }
326
327    /**
328     * Constructs a new sorted map from output stream provider names to provider objects.
329     *
330     * <p>
331     * The map returned by this method will have one entry for each provider for which support is available in the current Java virtual machine. If two or more
332     * supported provider have the same name then the resulting map will contain just one of them; which one it will contain is not specified.
333     * </p>
334     *
335     * <p>
336     * The invocation of this method, and the subsequent use of the resulting map, may cause time-consuming disk or network I/O operations to occur. This method
337     * is provided for applications that need to enumerate all of the available providers, for example to allow user provider selection.
338     * </p>
339     *
340     * <p>
341     * This method may return different results at different times if new providers are dynamically made available to the current Java virtual machine.
342     * </p>
343     *
344     * @return An immutable, map from names to provider objects
345     * @since 1.13
346     */
347    public static SortedMap<String, ArchiveStreamProvider> findAvailableArchiveOutputStreamProviders() {
348        return AccessController.doPrivileged((PrivilegedAction<SortedMap<String, ArchiveStreamProvider>>) () -> {
349            final TreeMap<String, ArchiveStreamProvider> map = new TreeMap<>();
350            putAll(DEFAULT.getOutputStreamArchiveNames(), DEFAULT, map);
351            archiveStreamProviderIterable().forEach(provider -> putAll(provider.getOutputStreamArchiveNames(), provider, map));
352            return map;
353        });
354    }
355
356    static void putAll(final Set<String> names, final ArchiveStreamProvider provider, final TreeMap<String, ArchiveStreamProvider> map) {
357        names.forEach(name -> map.put(toKey(name), provider));
358    }
359
360    private static String toKey(final String name) {
361        return name.toUpperCase(Locale.ROOT);
362    }
363
364    /**
365     * Entry encoding, null for the default.
366     */
367    private volatile String entryEncoding;
368
369    private SortedMap<String, ArchiveStreamProvider> archiveInputStreamProviders;
370
371    private SortedMap<String, ArchiveStreamProvider> archiveOutputStreamProviders;
372
373    /**
374     * Constructs an instance using the platform default encoding.
375     */
376    public ArchiveStreamFactory() {
377        this(null);
378    }
379
380    /**
381     * Constructs an instance using the specified encoding.
382     *
383     * @param encoding the encoding to be used.
384     *
385     * @since 1.10
386     */
387    public ArchiveStreamFactory(final String encoding) {
388        this.entryEncoding = encoding;
389    }
390
391    /**
392     * Creates an archive input stream from an input stream, autodetecting the archive type from the first few bytes of the stream. The InputStream must support
393     * marks, like BufferedInputStream.
394     *
395     * @param <I> The {@link ArchiveInputStream} type.
396     * @param in  the input stream
397     * @return the archive input stream
398     * @throws ArchiveException               if the archiver name is not known
399     * @throws StreamingNotSupportedException if the format cannot be read from a stream
400     * @throws IllegalArgumentException       if the stream is null or does not support mark
401     */
402    public <I extends ArchiveInputStream<? extends ArchiveEntry>> I createArchiveInputStream(final InputStream in) throws ArchiveException {
403        return createArchiveInputStream(detect(in), in);
404    }
405
406    /**
407     * Creates an archive input stream from an archiver name and an input stream.
408     *
409     * @param <I>          The {@link ArchiveInputStream} type.
410     * @param archiverName the archive name, i.e. {@value #AR}, {@value #ARJ}, {@value #ZIP}, {@value #TAR}, {@value #JAR}, {@value #CPIO}, {@value #DUMP} or
411     *                     {@value #SEVEN_Z}
412     * @param in           the input stream
413     * @return the archive input stream
414     * @throws ArchiveException               if the archiver name is not known
415     * @throws StreamingNotSupportedException if the format cannot be read from a stream
416     * @throws IllegalArgumentException       if the archiver name or stream is null
417     */
418    public <I extends ArchiveInputStream<? extends ArchiveEntry>> I createArchiveInputStream(final String archiverName, final InputStream in)
419            throws ArchiveException {
420        return createArchiveInputStream(archiverName, in, entryEncoding);
421    }
422
423    @SuppressWarnings("unchecked")
424    @Override
425    public <I extends ArchiveInputStream<? extends ArchiveEntry>> I createArchiveInputStream(final String archiverName, final InputStream in,
426            final String actualEncoding) throws ArchiveException {
427
428        if (archiverName == null) {
429            throw new IllegalArgumentException("Archiver name must not be null.");
430        }
431
432        if (in == null) {
433            throw new IllegalArgumentException("InputStream must not be null.");
434        }
435
436        if (AR.equalsIgnoreCase(archiverName)) {
437            return (I) new ArArchiveInputStream(in);
438        }
439        if (ARJ.equalsIgnoreCase(archiverName)) {
440            if (actualEncoding != null) {
441                return (I) new ArjArchiveInputStream(in, actualEncoding);
442            }
443            return (I) new ArjArchiveInputStream(in);
444        }
445        if (ZIP.equalsIgnoreCase(archiverName)) {
446            if (actualEncoding != null) {
447                return (I) new ZipArchiveInputStream(in, actualEncoding);
448            }
449            return (I) new ZipArchiveInputStream(in);
450        }
451        if (TAR.equalsIgnoreCase(archiverName)) {
452            if (actualEncoding != null) {
453                return (I) new TarArchiveInputStream(in, actualEncoding);
454            }
455            return (I) new TarArchiveInputStream(in);
456        }
457        if (JAR.equalsIgnoreCase(archiverName) || APK.equalsIgnoreCase(archiverName)) {
458            if (actualEncoding != null) {
459                return (I) new JarArchiveInputStream(in, actualEncoding);
460            }
461            return (I) new JarArchiveInputStream(in);
462        }
463        if (CPIO.equalsIgnoreCase(archiverName)) {
464            if (actualEncoding != null) {
465                return (I) new CpioArchiveInputStream(in, actualEncoding);
466            }
467            return (I) new CpioArchiveInputStream(in);
468        }
469        if (DUMP.equalsIgnoreCase(archiverName)) {
470            if (actualEncoding != null) {
471                return (I) new DumpArchiveInputStream(in, actualEncoding);
472            }
473            return (I) new DumpArchiveInputStream(in);
474        }
475        if (SEVEN_Z.equalsIgnoreCase(archiverName)) {
476            throw new StreamingNotSupportedException(SEVEN_Z);
477        }
478
479        final ArchiveStreamProvider archiveStreamProvider = getArchiveInputStreamProviders().get(toKey(archiverName));
480        if (archiveStreamProvider != null) {
481            return archiveStreamProvider.createArchiveInputStream(archiverName, in, actualEncoding);
482        }
483
484        throw new ArchiveException("Archiver: " + archiverName + " not found.");
485    }
486
487    /**
488     * Creates an archive output stream from an archiver name and an output stream.
489     *
490     * @param <O>          The {@link ArchiveOutputStream} type.
491     * @param archiverName the archive name, i.e. {@value #AR}, {@value #ZIP}, {@value #TAR}, {@value #JAR} or {@value #CPIO}
492     * @param out          the output stream
493     * @return the archive output stream
494     * @throws ArchiveException               if the archiver name is not known
495     * @throws StreamingNotSupportedException if the format cannot be written to a stream
496     * @throws IllegalArgumentException       if the archiver name or stream is null
497     */
498    public <O extends ArchiveOutputStream<? extends ArchiveEntry>> O createArchiveOutputStream(final String archiverName, final OutputStream out)
499            throws ArchiveException {
500        return createArchiveOutputStream(archiverName, out, entryEncoding);
501    }
502
503    @SuppressWarnings("unchecked")
504    @Override
505    public <O extends ArchiveOutputStream<? extends ArchiveEntry>> O createArchiveOutputStream(final String archiverName, final OutputStream out,
506            final String actualEncoding) throws ArchiveException {
507        if (archiverName == null) {
508            throw new IllegalArgumentException("Archiver name must not be null.");
509        }
510        if (out == null) {
511            throw new IllegalArgumentException("OutputStream must not be null.");
512        }
513
514        if (AR.equalsIgnoreCase(archiverName)) {
515            return (O) new ArArchiveOutputStream(out);
516        }
517        if (ZIP.equalsIgnoreCase(archiverName)) {
518            final ZipArchiveOutputStream zip = new ZipArchiveOutputStream(out);
519            if (actualEncoding != null) {
520                zip.setEncoding(actualEncoding);
521            }
522            return (O) zip;
523        }
524        if (TAR.equalsIgnoreCase(archiverName)) {
525            if (actualEncoding != null) {
526                return (O) new TarArchiveOutputStream(out, actualEncoding);
527            }
528            return (O) new TarArchiveOutputStream(out);
529        }
530        if (JAR.equalsIgnoreCase(archiverName)) {
531            if (actualEncoding != null) {
532                return (O) new JarArchiveOutputStream(out, actualEncoding);
533            }
534            return (O) new JarArchiveOutputStream(out);
535        }
536        if (CPIO.equalsIgnoreCase(archiverName)) {
537            if (actualEncoding != null) {
538                return (O) new CpioArchiveOutputStream(out, actualEncoding);
539            }
540            return (O) new CpioArchiveOutputStream(out);
541        }
542        if (SEVEN_Z.equalsIgnoreCase(archiverName)) {
543            throw new StreamingNotSupportedException(SEVEN_Z);
544        }
545
546        final ArchiveStreamProvider archiveStreamProvider = getArchiveOutputStreamProviders().get(toKey(archiverName));
547        if (archiveStreamProvider != null) {
548            return archiveStreamProvider.createArchiveOutputStream(archiverName, out, actualEncoding);
549        }
550
551        throw new ArchiveException("Archiver: " + archiverName + " not found.");
552    }
553
554    public SortedMap<String, ArchiveStreamProvider> getArchiveInputStreamProviders() {
555        if (archiveInputStreamProviders == null) {
556            archiveInputStreamProviders = Collections.unmodifiableSortedMap(findAvailableArchiveInputStreamProviders());
557        }
558        return archiveInputStreamProviders;
559    }
560
561    public SortedMap<String, ArchiveStreamProvider> getArchiveOutputStreamProviders() {
562        if (archiveOutputStreamProviders == null) {
563            archiveOutputStreamProviders = Collections.unmodifiableSortedMap(findAvailableArchiveOutputStreamProviders());
564        }
565        return archiveOutputStreamProviders;
566    }
567
568    /**
569     * Gets the encoding to use for arj, jar, ZIP, dump, cpio and tar files, or null for the archiver default.
570     *
571     * @return entry encoding, or null for the archiver default
572     * @since 1.5
573     */
574    public String getEntryEncoding() {
575        return entryEncoding;
576    }
577
578    @Override
579    public Set<String> getInputStreamArchiveNames() {
580        return Sets.newHashSet(AR, ARJ, ZIP, TAR, JAR, CPIO, DUMP, SEVEN_Z);
581    }
582
583    @Override
584    public Set<String> getOutputStreamArchiveNames() {
585        return Sets.newHashSet(AR, ZIP, TAR, JAR, CPIO, SEVEN_Z);
586    }
587
588    /**
589     * Sets the encoding to use for arj, jar, ZIP, dump, cpio and tar files. Use null for the archiver default.
590     *
591     * @param entryEncoding the entry encoding, null uses the archiver default.
592     * @since 1.5
593     * @deprecated 1.10 use {@link #ArchiveStreamFactory(String)} to specify the encoding
594     */
595    @Deprecated
596    public void setEntryEncoding(final String entryEncoding) {
597        this.entryEncoding = entryEncoding;
598    }
599
600}