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 *   https://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.compressors;
020
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.OutputStream;
024import java.security.AccessController;
025import java.security.PrivilegedAction;
026import java.util.Collections;
027import java.util.ServiceLoader;
028import java.util.Set;
029import java.util.SortedMap;
030import java.util.TreeMap;
031
032import org.apache.commons.compress.compressors.brotli.BrotliCompressorInputStream;
033import org.apache.commons.compress.compressors.brotli.BrotliUtils;
034import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
035import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
036import org.apache.commons.compress.compressors.deflate.DeflateCompressorInputStream;
037import org.apache.commons.compress.compressors.deflate.DeflateCompressorOutputStream;
038import org.apache.commons.compress.compressors.deflate64.Deflate64CompressorInputStream;
039import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
040import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
041import org.apache.commons.compress.compressors.lz4.BlockLZ4CompressorInputStream;
042import org.apache.commons.compress.compressors.lz4.BlockLZ4CompressorOutputStream;
043import org.apache.commons.compress.compressors.lz4.FramedLZ4CompressorInputStream;
044import org.apache.commons.compress.compressors.lz4.FramedLZ4CompressorOutputStream;
045import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
046import org.apache.commons.compress.compressors.lzma.LZMACompressorOutputStream;
047import org.apache.commons.compress.compressors.lzma.LZMAUtils;
048import org.apache.commons.compress.compressors.pack200.Pack200CompressorInputStream;
049import org.apache.commons.compress.compressors.pack200.Pack200CompressorOutputStream;
050import org.apache.commons.compress.compressors.snappy.FramedSnappyCompressorInputStream;
051import org.apache.commons.compress.compressors.snappy.FramedSnappyCompressorOutputStream;
052import org.apache.commons.compress.compressors.snappy.SnappyCompressorInputStream;
053import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
054import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream;
055import org.apache.commons.compress.compressors.xz.XZUtils;
056import org.apache.commons.compress.compressors.z.ZCompressorInputStream;
057import org.apache.commons.compress.compressors.zstandard.ZstdCompressorInputStream;
058import org.apache.commons.compress.compressors.zstandard.ZstdCompressorOutputStream;
059import org.apache.commons.compress.compressors.zstandard.ZstdUtils;
060import org.apache.commons.compress.utils.IOUtils;
061import org.apache.commons.compress.utils.Sets;
062import org.apache.commons.lang3.StringUtils;
063
064/**
065 * <p>
066 * Creates a Compressor[In|Out]putStreams from names. To add other implementations you should extend CompressorStreamFactory and override the
067 * appropriate methods (and call their implementation from super of course).
068 * </p>
069 *
070 * Example (Compressing a file):
071 *
072 * <pre>
073 * final OutputStream out = Files.newOutputStream(output.toPath());
074 * CompressorOutputStream cos = new CompressorStreamFactory().createCompressorOutputStream(CompressorStreamFactory.BZIP2, out);
075 * IOUtils.copy(Files.newInputStream(input.toPath()), cos);
076 * cos.close();
077 * </pre>
078 *
079 * Example (Decompressing a file):
080 *
081 * <pre>
082 * final InputStream is = Files.newInputStream(input.toPath());
083 * CompressorInputStream in = new CompressorStreamFactory().createCompressorInputStream(CompressorStreamFactory.BZIP2, is);
084 * IOUtils.copy(in, Files.newOutputStream(output.toPath()));
085 * in.close();
086 * </pre>
087 *
088 * @Immutable provided that the deprecated method setDecompressConcatenated is not used.
089 * @ThreadSafe even if the deprecated method setDecompressConcatenated is used
090 */
091public class CompressorStreamFactory implements CompressorStreamProvider {
092
093    private static final CompressorStreamFactory SINGLETON = new CompressorStreamFactory();
094
095    /**
096     * Constant (value {@value}) used to identify the BROTLI compression algorithm.
097     *
098     * @since 1.14
099     */
100    public static final String BROTLI = "br";
101
102    /**
103     * Constant (value {@value}) used to identify the BZIP2 compression algorithm.
104     *
105     * @since 1.1
106     */
107    public static final String BZIP2 = "bzip2";
108
109    /**
110     * Constant (value {@value}) used to identify the GZIP compression algorithm.
111     *
112     * @since 1.1
113     */
114    public static final String GZIP = "gz";
115
116    /**
117     * Constant (value {@value}) used to identify the PACK200 compression algorithm.
118     *
119     * @since 1.3
120     */
121    public static final String PACK200 = "pack200";
122
123    /**
124     * Constant (value {@value}) used to identify the XZ compression method.
125     *
126     * @since 1.4
127     */
128    public static final String XZ = "xz";
129
130    /**
131     * Constant (value {@value}) used to identify the LZMA compression method.
132     *
133     * @since 1.6
134     */
135    public static final String LZMA = "lzma";
136
137    /**
138     * Constant (value {@value}) used to identify the "framed" Snappy compression method.
139     *
140     * @since 1.7
141     */
142    public static final String SNAPPY_FRAMED = "snappy-framed";
143
144    /**
145     * Constant (value {@value}) used to identify the "raw" Snappy compression method. Not supported as an output stream type.
146     *
147     * @since 1.7
148     */
149    public static final String SNAPPY_RAW = "snappy-raw";
150
151    /**
152     * Constant (value {@value}) used to identify the traditional Unix compress method. Not supported as an output stream type.
153     *
154     * @since 1.7
155     */
156    public static final String Z = "z";
157
158    /**
159     * Constant (value {@value}) used to identify the Deflate compress method.
160     *
161     * @since 1.9
162     */
163    public static final String DEFLATE = "deflate";
164
165    /**
166     * Constant (value {@value}) used to identify the Deflate64 compress method.
167     *
168     * @since 1.16
169     */
170    public static final String DEFLATE64 = "deflate64";
171
172    /**
173     * Constant (value {@value}) used to identify the block LZ4 compression method.
174     *
175     * @since 1.14
176     */
177    public static final String LZ4_BLOCK = "lz4-block";
178
179    /**
180     * Constant (value {@value}) used to identify the frame LZ4 compression method.
181     *
182     * @since 1.14
183     */
184    public static final String LZ4_FRAMED = "lz4-framed";
185
186    /**
187     * Constant (value {@value}) used to identify the Zstandard compression algorithm. Not supported as an output stream type.
188     *
189     * @since 1.16
190     */
191    public static final String ZSTANDARD = "zstd";
192
193    private static final String YOU_NEED_BROTLI_DEC = youNeed("Google Brotli Dec", "https://github.com/google/brotli/");
194    private static final String YOU_NEED_XZ_JAVA = youNeed("XZ for Java", "https://tukaani.org/xz/java.html");
195    private static final String YOU_NEED_ZSTD_JNI = youNeed("Zstd JNI", "https://github.com/luben/zstd-jni");
196
197    private static final Set<String> ALL_NAMES = Sets.newHashSet(BZIP2, GZIP, PACK200, SNAPPY_FRAMED, Z, DEFLATE, XZ, LZMA, LZ4_FRAMED, ZSTANDARD);
198
199    private static Iterable<CompressorStreamProvider> archiveStreamProviderIterable() {
200        return ServiceLoader.load(CompressorStreamProvider.class, ClassLoader.getSystemClassLoader());
201    }
202
203    /**
204     * Detects the type of compressor stream.
205     *
206     * @param inputStream input stream
207     * @return type of compressor stream detected
208     * @throws CompressorException      if no compressor stream type was detected or if something else went wrong
209     * @throws IllegalArgumentException if stream is null or does not support mark
210     * @since 1.14
211     */
212    public static String detect(final InputStream inputStream) throws CompressorException {
213        return detect(inputStream, ALL_NAMES);
214    }
215
216    /**
217     * Detects the type of compressor stream while limiting the type to the provided set of compressor names.
218     *
219     * @param inputStream     input stream
220     * @param compressorNames compressor names to limit autodetection
221     * @return type of compressor stream detected
222     * @throws CompressorException      if no compressor stream type was detected or if something else went wrong
223     * @throws IllegalArgumentException if stream is null or does not support mark
224     */
225    static String detect(final InputStream inputStream, final Set<String> compressorNames) throws CompressorException {
226        if (inputStream == null) {
227            throw new IllegalArgumentException("Stream must not be null.");
228        }
229        if (compressorNames == null || compressorNames.isEmpty()) {
230            throw new IllegalArgumentException("Compressor names cannot be null or empty");
231        }
232        if (!inputStream.markSupported()) {
233            throw new IllegalArgumentException("Mark is not supported.");
234        }
235        final byte[] signature = new byte[12];
236        inputStream.mark(signature.length);
237        int signatureLength = -1;
238        try {
239            signatureLength = IOUtils.readFully(inputStream, signature);
240            inputStream.reset();
241        } catch (final IOException e) {
242            throw new CompressorException("Failed to read signature.", e);
243        }
244        if (compressorNames.contains(BZIP2) && BZip2CompressorInputStream.matches(signature, signatureLength)) {
245            return BZIP2;
246        }
247        if (compressorNames.contains(GZIP) && GzipCompressorInputStream.matches(signature, signatureLength)) {
248            return GZIP;
249        }
250        if (compressorNames.contains(PACK200) && Pack200CompressorInputStream.matches(signature, signatureLength)) {
251            return PACK200;
252        }
253        if (compressorNames.contains(SNAPPY_FRAMED) && FramedSnappyCompressorInputStream.matches(signature, signatureLength)) {
254            return SNAPPY_FRAMED;
255        }
256        if (compressorNames.contains(Z) && ZCompressorInputStream.matches(signature, signatureLength)) {
257            return Z;
258        }
259        if (compressorNames.contains(DEFLATE) && DeflateCompressorInputStream.matches(signature, signatureLength)) {
260            return DEFLATE;
261        }
262        if (compressorNames.contains(XZ) && XZUtils.matches(signature, signatureLength)) {
263            return XZ;
264        }
265        if (compressorNames.contains(LZMA) && LZMAUtils.matches(signature, signatureLength)) {
266            return LZMA;
267        }
268        if (compressorNames.contains(LZ4_FRAMED) && FramedLZ4CompressorInputStream.matches(signature, signatureLength)) {
269            return LZ4_FRAMED;
270        }
271        if (compressorNames.contains(ZSTANDARD) && ZstdUtils.matches(signature, signatureLength)) {
272            return ZSTANDARD;
273        }
274        throw new CompressorException("No Compressor found for the stream signature.");
275    }
276
277    /**
278     * Constructs a new sorted map from input stream provider names to provider objects.
279     *
280     * <p>
281     * 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
282     * supported provider have the same name then the resulting map will contain just one of them; which one it will contain is not specified.
283     * </p>
284     *
285     * <p>
286     * 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
287     * is provided for applications that need to enumerate all of the available providers, for example to allow user provider selection.
288     * </p>
289     *
290     * <p>
291     * This method may return different results at different times if new providers are dynamically made available to the current Java virtual machine.
292     * </p>
293     *
294     * @return An immutable, map from names to provider objects
295     * @since 1.13
296     */
297    public static SortedMap<String, CompressorStreamProvider> findAvailableCompressorInputStreamProviders() {
298        return AccessController.doPrivileged((PrivilegedAction<SortedMap<String, CompressorStreamProvider>>) () -> {
299            final TreeMap<String, CompressorStreamProvider> map = new TreeMap<>();
300            putAll(SINGLETON.getInputStreamCompressorNames(), SINGLETON, map);
301            archiveStreamProviderIterable().forEach(provider -> putAll(provider.getInputStreamCompressorNames(), provider, map));
302            return map;
303        });
304    }
305
306    /**
307     * Constructs a new sorted map from output stream provider names to provider objects.
308     *
309     * <p>
310     * 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
311     * supported provider have the same name then the resulting map will contain just one of them; which one it will contain is not specified.
312     * </p>
313     *
314     * <p>
315     * 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
316     * is provided for applications that need to enumerate all of the available providers, for example to allow user provider selection.
317     * </p>
318     *
319     * <p>
320     * This method may return different results at different times if new providers are dynamically made available to the current Java virtual machine.
321     * </p>
322     *
323     * @return An immutable, map from names to provider objects
324     * @since 1.13
325     */
326    public static SortedMap<String, CompressorStreamProvider> findAvailableCompressorOutputStreamProviders() {
327        return AccessController.doPrivileged((PrivilegedAction<SortedMap<String, CompressorStreamProvider>>) () -> {
328            final TreeMap<String, CompressorStreamProvider> map = new TreeMap<>();
329            putAll(SINGLETON.getOutputStreamCompressorNames(), SINGLETON, map);
330            archiveStreamProviderIterable().forEach(provider -> putAll(provider.getOutputStreamCompressorNames(), provider, map));
331            return map;
332        });
333    }
334
335    /**
336     * Gets the string used to identify the {@link #BROTLI} compression algorithm.
337     *
338     * @return the string used to identify the {@link #BROTLI} compression algorithm.
339     */
340    public static String getBrotli() {
341        return BROTLI;
342    }
343
344    /**
345     * Gets the string used to identify the {@link #BZIP2} compression algorithm.
346     *
347     * @return the string used to identify the {@link #BZIP2} compression algorithm.
348     */
349    public static String getBzip2() {
350        return BZIP2;
351    }
352
353    /**
354     * Gets the string used to identify the {@link #DEFLATE} compression algorithm.
355     *
356     * @return the string used to identify the {@link #DEFLATE} compression algorithm.
357     */
358    public static String getDeflate() {
359        return DEFLATE;
360    }
361
362    /**
363     * Gets the string used to identify the {@link #DEFLATE64} compression algorithm.
364     *
365     * @return the string used to identify the {@link #DEFLATE64} compression algorithm.
366     * @since 1.16
367     */
368    public static String getDeflate64() {
369        return DEFLATE64;
370    }
371
372    /**
373     * Gets the string used to identify the {@link #GZIP} compression algorithm.
374     *
375     * @return the string used to identify the {@link #GZIP} compression algorithm.
376     */
377    public static String getGzip() {
378        return GZIP;
379    }
380
381    /**
382     * Gets the string used to identify the {@link #LZ4_BLOCK} compression algorithm.
383     *
384     * @return the string used to identify the {@link #LZ4_BLOCK} compression algorithm.
385     */
386    public static String getLZ4Block() {
387        return LZ4_BLOCK;
388    }
389
390    /**
391     * Gets the string used to identify the {@link #GZIP} compression algorithm.
392     *
393     * @return the string used to identify the {@link #GZIP} compression algorithm.
394     */
395    public static String getLZ4Framed() {
396        return LZ4_FRAMED;
397    }
398
399    /**
400     * Gets the string used to identify the {@link #LZMA} compression algorithm.
401     *
402     * @return the string used to identify the {@link #LZMA} compression algorithm.
403     */
404    public static String getLzma() {
405        return LZMA;
406    }
407
408    /**
409     * Gets the string used to identify the {@link #PACK200} compression algorithm.
410     *
411     * @return the string used to identify the {@link #PACK200} compression algorithm.
412     */
413    public static String getPack200() {
414        return PACK200;
415    }
416
417    /**
418     * Gets singleton instance.
419     *
420     * @return the singleton instance.
421     */
422    public static CompressorStreamFactory getSingleton() {
423        return SINGLETON;
424    }
425
426    /**
427     * Gets the string used to identify the {@link #SNAPPY_FRAMED} compression algorithm.
428     *
429     * @return the string used to identify the {@link #SNAPPY_FRAMED} compression algorithm.
430     */
431    public static String getSnappyFramed() {
432        return SNAPPY_FRAMED;
433    }
434
435    /**
436     * Gets the string used to identify the {@link #SNAPPY_RAW} compression algorithm.
437     *
438     * @return the string used to identify the {@link #SNAPPY_RAW} compression algorithm.
439     */
440    public static String getSnappyRaw() {
441        return SNAPPY_RAW;
442    }
443
444    /**
445     * Gets the string used to identify the {@link #XZ} compression algorithm.
446     *
447     * @return the string used to identify the {@link #XZ} compression algorithm.
448     */
449    public static String getXz() {
450        return XZ;
451    }
452
453    /**
454     * Gets the string used to identify the {@link #Z} compression algorithm.
455     *
456     * @return the string used to identify the {@link #Z} compression algorithm.
457     */
458    public static String getZ() {
459        return Z;
460    }
461
462    /**
463     * Gets the string used to identify the {@link #ZSTANDARD} compression algorithm.
464     *
465     * @return the string used to identify the {@link #ZSTANDARD} compression algorithm.
466     */
467    public static String getZstandard() {
468        return ZSTANDARD;
469    }
470
471    static void putAll(final Set<String> names, final CompressorStreamProvider provider, final TreeMap<String, CompressorStreamProvider> map) {
472        names.forEach(name -> map.put(toKey(name), provider));
473    }
474
475    private static String toKey(final String name) {
476        return StringUtils.toRootUpperCase(name);
477    }
478
479    private static String youNeed(final String name, final String url) {
480        return " In addition to Apache Commons Compress you need the " + name + " library - see " + url;
481    }
482
483    /**
484     * If true, decompress until the end of the input. If false, stop after the first stream and leave the input position to point to the next byte after the
485     * stream
486     */
487    private final Boolean decompressUntilEof;
488    // This is Boolean so setDecompressConcatenated can determine whether it has
489    // been set by the ctor
490    // once the setDecompressConcatenated method has been removed, it can revert
491    // to boolean
492
493    private SortedMap<String, CompressorStreamProvider> compressorInputStreamProviders;
494
495    private SortedMap<String, CompressorStreamProvider> compressorOutputStreamProviders;
496
497    /**
498     * If true, decompress until the end of the input. If false, stop after the first stream and leave the input position to point to the next byte after the
499     * stream
500     */
501    private volatile boolean decompressConcatenated;
502
503    private final int memoryLimitInKb;
504
505    /**
506     * Constructs an instance with the decompress Concatenated option set to false.
507     */
508    public CompressorStreamFactory() {
509        this.decompressUntilEof = null;
510        this.memoryLimitInKb = -1;
511    }
512
513    /**
514     * Constructs an instance with the provided decompress Concatenated option.
515     *
516     * @param decompressUntilEOF if true, decompress until the end of the input; if false, stop after the first stream and leave the input position to point to
517     *                           the next byte after the stream. This setting applies to the gzip, bzip2 and XZ formats only.
518     * @since 1.10
519     */
520    public CompressorStreamFactory(final boolean decompressUntilEOF) {
521        this(decompressUntilEOF, -1);
522    }
523
524    /**
525     * Constructs an instance with the provided decompress Concatenated option.
526     *
527     * @param decompressUntilEOF if true, decompress until the end of the input; if false, stop after the first stream and leave the input position to point to
528     *                           the next byte after the stream. This setting applies to the gzip, bzip2 and XZ formats only.
529     * @param memoryLimitInKb    Some streams require allocation of potentially significant byte arrays/tables, and they can offer checks to prevent OOMs on
530     *                           corrupt files. Set the maximum allowed memory allocation in KBs.
531     *
532     * @since 1.14
533     */
534    public CompressorStreamFactory(final boolean decompressUntilEOF, final int memoryLimitInKb) {
535        this.decompressUntilEof = decompressUntilEOF;
536        // Also copy to existing variable so can continue to use that as the
537        // current value
538        this.decompressConcatenated = decompressUntilEOF;
539        this.memoryLimitInKb = memoryLimitInKb;
540    }
541
542    /**
543     * Creates a compressor input stream from an input stream, auto-detecting the compressor type from the first few bytes of the stream. The InputStream must
544     * support marks, like BufferedInputStream.
545     *
546     * @param in the input stream
547     * @return the compressor input stream
548     * @throws CompressorException      if the compressor name is not known
549     * @throws IllegalArgumentException if the stream is null or does not support mark
550     * @since 1.1
551     */
552    public CompressorInputStream createCompressorInputStream(final InputStream in) throws CompressorException {
553        return createCompressorInputStream(detect(in), in);
554    }
555
556    /**
557     * Creates a compressor input stream from an input stream, auto-detecting the compressor type from the first few bytes of the stream while limiting the
558     * detected type to the provided set of compressor names. The InputStream must support marks, like BufferedInputStream.
559     *
560     * @param in              the input stream
561     * @param compressorNames compressor names to limit autodetection
562     * @return the compressor input stream
563     * @throws CompressorException      if the autodetected compressor is not in the provided set of compressor names
564     * @throws IllegalArgumentException if the stream is null or does not support mark
565     * @since 1.25.0
566     */
567    public CompressorInputStream createCompressorInputStream(final InputStream in, final Set<String> compressorNames) throws CompressorException {
568        return createCompressorInputStream(detect(in, compressorNames), in);
569    }
570
571    /**
572     * Creates a compressor input stream from a compressor name and an input stream.
573     *
574     * @param name of the compressor, i.e. {@value #GZIP}, {@value #BZIP2}, {@value #XZ}, {@value #LZMA}, {@value #PACK200}, {@value #SNAPPY_RAW},
575     *             {@value #SNAPPY_FRAMED}, {@value #Z}, {@value #LZ4_BLOCK}, {@value #LZ4_FRAMED}, {@value #ZSTANDARD}, {@value #DEFLATE64} or
576     *             {@value #DEFLATE}
577     * @param in   the input stream
578     * @return compressor input stream
579     * @throws CompressorException      if the compressor name is not known or not available, or if there's an IOException or MemoryLimitException thrown during
580     *                                  initialization
581     * @throws IllegalArgumentException if the name or input stream is null
582     */
583    public CompressorInputStream createCompressorInputStream(final String name, final InputStream in) throws CompressorException {
584        return createCompressorInputStream(name, in, decompressConcatenated);
585    }
586
587    @Override
588    public CompressorInputStream createCompressorInputStream(final String name, final InputStream in, final boolean actualDecompressConcatenated)
589            throws CompressorException {
590        if (name == null || in == null) {
591            throw new IllegalArgumentException("Compressor name and stream must not be null.");
592        }
593        try {
594            if (GZIP.equalsIgnoreCase(name)) {
595                // @formatter:off
596                return GzipCompressorInputStream.builder()
597                        .setInputStream(in)
598                        .setDecompressConcatenated(actualDecompressConcatenated)
599                        .get();
600                // @formatter:on
601            }
602            if (BZIP2.equalsIgnoreCase(name)) {
603                return new BZip2CompressorInputStream(in, actualDecompressConcatenated);
604            }
605            if (BROTLI.equalsIgnoreCase(name)) {
606                if (!BrotliUtils.isBrotliCompressionAvailable()) {
607                    throw new CompressorException("Brotli compression is not available." + YOU_NEED_BROTLI_DEC);
608                }
609                return new BrotliCompressorInputStream(in);
610            }
611            if (XZ.equalsIgnoreCase(name)) {
612                if (!XZUtils.isXZCompressionAvailable()) {
613                    throw new CompressorException("XZ compression is not available." + YOU_NEED_XZ_JAVA);
614                }
615                // @formatter:off
616                return XZCompressorInputStream.builder()
617                        .setInputStream(in)
618                        .setDecompressConcatenated(actualDecompressConcatenated)
619                        .setMemoryLimitKiB(memoryLimitInKb)
620                        .get();
621                // @formatter:on
622            }
623            if (ZSTANDARD.equalsIgnoreCase(name)) {
624                if (!ZstdUtils.isZstdCompressionAvailable()) {
625                    throw new CompressorException("Zstandard compression is not available." + YOU_NEED_ZSTD_JNI);
626                }
627                return new ZstdCompressorInputStream(in);
628            }
629            if (LZMA.equalsIgnoreCase(name)) {
630                if (!LZMAUtils.isLZMACompressionAvailable()) {
631                    throw new CompressorException("LZMA compression is not available" + YOU_NEED_XZ_JAVA);
632                }
633                return LZMACompressorInputStream.builder().setInputStream(in).setMemoryLimitKiB(memoryLimitInKb).get();
634            }
635            if (PACK200.equalsIgnoreCase(name)) {
636                return new Pack200CompressorInputStream(in);
637            }
638            if (SNAPPY_RAW.equalsIgnoreCase(name)) {
639                return new SnappyCompressorInputStream(in);
640            }
641            if (SNAPPY_FRAMED.equalsIgnoreCase(name)) {
642                return new FramedSnappyCompressorInputStream(in);
643            }
644            if (Z.equalsIgnoreCase(name)) {
645                return new ZCompressorInputStream(in, memoryLimitInKb);
646            }
647            if (DEFLATE.equalsIgnoreCase(name)) {
648                return new DeflateCompressorInputStream(in);
649            }
650            if (DEFLATE64.equalsIgnoreCase(name)) {
651                return new Deflate64CompressorInputStream(in);
652            }
653            if (LZ4_BLOCK.equalsIgnoreCase(name)) {
654                return new BlockLZ4CompressorInputStream(in);
655            }
656            if (LZ4_FRAMED.equalsIgnoreCase(name)) {
657                return new FramedLZ4CompressorInputStream(in, actualDecompressConcatenated);
658            }
659
660        } catch (final IOException e) {
661            throw new CompressorException("Could not create CompressorInputStream.", e);
662        }
663        final CompressorStreamProvider compressorStreamProvider = getCompressorInputStreamProviders().get(toKey(name));
664        if (compressorStreamProvider != null) {
665            return compressorStreamProvider.createCompressorInputStream(name, in, actualDecompressConcatenated);
666        }
667        throw new CompressorException("Compressor: " + name + " not found.");
668    }
669
670    /**
671     * Creates a compressor output stream from a compressor name and an output stream.
672     *
673     * @param name the compressor name, i.e. {@value #GZIP}, {@value #BZIP2}, {@value #XZ}, {@value #PACK200}, {@value #SNAPPY_FRAMED}, {@value #LZ4_BLOCK},
674     *             {@value #LZ4_FRAMED}, {@value #ZSTANDARD} or {@value #DEFLATE}
675     * @param out  the output stream
676     * @return the compressor output stream
677     * @throws CompressorException      if the archiver name is not known
678     * @throws IllegalArgumentException if the archiver name or stream is null
679     */
680    @SuppressWarnings("unchecked")
681    @Override
682    public CompressorOutputStream<? extends OutputStream> createCompressorOutputStream(final String name, final OutputStream out) throws CompressorException {
683        if (name == null || out == null) {
684            throw new IllegalArgumentException("Compressor name and stream must not be null.");
685        }
686        try {
687            if (GZIP.equalsIgnoreCase(name)) {
688                return new GzipCompressorOutputStream(out);
689            }
690            if (BZIP2.equalsIgnoreCase(name)) {
691                return new BZip2CompressorOutputStream(out);
692            }
693            if (XZ.equalsIgnoreCase(name)) {
694                return new XZCompressorOutputStream(out);
695            }
696            if (PACK200.equalsIgnoreCase(name)) {
697                return new Pack200CompressorOutputStream(out);
698            }
699            if (LZMA.equalsIgnoreCase(name)) {
700                return new LZMACompressorOutputStream(out);
701            }
702            if (DEFLATE.equalsIgnoreCase(name)) {
703                return new DeflateCompressorOutputStream(out);
704            }
705            if (SNAPPY_FRAMED.equalsIgnoreCase(name)) {
706                return new FramedSnappyCompressorOutputStream(out);
707            }
708            if (LZ4_BLOCK.equalsIgnoreCase(name)) {
709                return new BlockLZ4CompressorOutputStream(out);
710            }
711            if (LZ4_FRAMED.equalsIgnoreCase(name)) {
712                return new FramedLZ4CompressorOutputStream(out);
713            }
714            if (ZSTANDARD.equalsIgnoreCase(name)) {
715                return new ZstdCompressorOutputStream(out);
716            }
717        } catch (final IOException e) {
718            throw new CompressorException("Could not create CompressorOutputStream.", e);
719        }
720        final CompressorStreamProvider compressorStreamProvider = getCompressorOutputStreamProviders().get(toKey(name));
721        if (compressorStreamProvider != null) {
722            return compressorStreamProvider.createCompressorOutputStream(name, out);
723        }
724        throw new CompressorException("Compressor: " + name + " not found.");
725    }
726
727    /**
728     * Gets a sorted map of compression input stream providers.
729     *
730     * @return a sorted map of compression input stream providers.
731     */
732    public SortedMap<String, CompressorStreamProvider> getCompressorInputStreamProviders() {
733        if (compressorInputStreamProviders == null) {
734            compressorInputStreamProviders = Collections.unmodifiableSortedMap(findAvailableCompressorInputStreamProviders());
735        }
736        return compressorInputStreamProviders;
737    }
738
739    /**
740     * Gets a sorted map of compression output stream providers.
741     *
742     * @return a sorted map of compression output stream providers.
743     */
744    public SortedMap<String, CompressorStreamProvider> getCompressorOutputStreamProviders() {
745        if (compressorOutputStreamProviders == null) {
746            compressorOutputStreamProviders = Collections.unmodifiableSortedMap(findAvailableCompressorOutputStreamProviders());
747        }
748        return compressorOutputStreamProviders;
749    }
750
751    /** For tests. */
752    boolean getDecompressConcatenated() {
753        return decompressConcatenated;
754    }
755
756    /**
757     * Tests whether we decompress until the end of the input. If false, stop after the first stream and leave the input position to point to the next byte
758     * after the stream.
759     *
760     * @return whether we decompress until the end of the input.
761     */
762    public Boolean getDecompressUntilEOF() {
763        return decompressUntilEof;
764    }
765
766    @Override
767    public Set<String> getInputStreamCompressorNames() {
768        return Sets.newHashSet(GZIP, BROTLI, BZIP2, XZ, LZMA, PACK200, DEFLATE, SNAPPY_RAW, SNAPPY_FRAMED, Z, LZ4_BLOCK, LZ4_FRAMED, ZSTANDARD, DEFLATE64);
769    }
770
771    @Override
772    public Set<String> getOutputStreamCompressorNames() {
773        return Sets.newHashSet(GZIP, BZIP2, XZ, LZMA, PACK200, DEFLATE, SNAPPY_FRAMED, LZ4_BLOCK, LZ4_FRAMED, ZSTANDARD);
774    }
775
776    /**
777     * Sets whether to decompress the full input or only the first stream in formats supporting multiple concatenated input streams.
778     *
779     * <p>
780     * This setting applies to the gzip, bzip2 and XZ formats only.
781     * </p>
782     *
783     * @param decompressConcatenated if true, decompress until the end of the input; if false, stop after the first stream and leave the input position to point
784     *                               to the next byte after the stream
785     * @since 1.5
786     * @deprecated 1.10 use the {@link #CompressorStreamFactory(boolean)} constructor instead
787     * @throws IllegalStateException if the constructor {@link #CompressorStreamFactory(boolean)} was used to create the factory
788     */
789    @Deprecated
790    public void setDecompressConcatenated(final boolean decompressConcatenated) {
791        if (this.decompressUntilEof != null) {
792            throw new IllegalStateException("Cannot override the setting defined by the constructor");
793        }
794        this.decompressConcatenated = decompressConcatenated;
795    }
796
797}