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.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.Locale;
028import java.util.ServiceLoader;
029import java.util.Set;
030import java.util.SortedMap;
031import java.util.TreeMap;
032
033import org.apache.commons.compress.compressors.brotli.BrotliCompressorInputStream;
034import org.apache.commons.compress.compressors.brotli.BrotliUtils;
035import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
036import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
037import org.apache.commons.compress.compressors.deflate.DeflateCompressorInputStream;
038import org.apache.commons.compress.compressors.deflate.DeflateCompressorOutputStream;
039import org.apache.commons.compress.compressors.deflate64.Deflate64CompressorInputStream;
040import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
041import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
042import org.apache.commons.compress.compressors.lz4.BlockLZ4CompressorInputStream;
043import org.apache.commons.compress.compressors.lz4.BlockLZ4CompressorOutputStream;
044import org.apache.commons.compress.compressors.lz4.FramedLZ4CompressorInputStream;
045import org.apache.commons.compress.compressors.lz4.FramedLZ4CompressorOutputStream;
046import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
047import org.apache.commons.compress.compressors.lzma.LZMACompressorOutputStream;
048import org.apache.commons.compress.compressors.lzma.LZMAUtils;
049import org.apache.commons.compress.compressors.pack200.Pack200CompressorInputStream;
050import org.apache.commons.compress.compressors.pack200.Pack200CompressorOutputStream;
051import org.apache.commons.compress.compressors.snappy.FramedSnappyCompressorInputStream;
052import org.apache.commons.compress.compressors.snappy.FramedSnappyCompressorOutputStream;
053import org.apache.commons.compress.compressors.snappy.SnappyCompressorInputStream;
054import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
055import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream;
056import org.apache.commons.compress.compressors.xz.XZUtils;
057import org.apache.commons.compress.compressors.z.ZCompressorInputStream;
058import org.apache.commons.compress.compressors.zstandard.ZstdCompressorInputStream;
059import org.apache.commons.compress.compressors.zstandard.ZstdCompressorOutputStream;
060import org.apache.commons.compress.compressors.zstandard.ZstdUtils;
061import org.apache.commons.compress.utils.IOUtils;
062import org.apache.commons.compress.utils.Sets;
063
064/**
065 * <p>
066 * Factory to create 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     *
211     * @since 1.14
212     */
213    public static String detect(final InputStream inputStream) throws CompressorException {
214        return detect(inputStream, ALL_NAMES);
215    }
216
217    /**
218     * Detects the type of compressor stream while limiting the type to the provided set of compressor names.
219     *
220     * @param inputStream     input stream
221     * @param compressorNames compressor names to limit autodetection
222     * @return type of compressor stream detected
223     * @throws CompressorException      if no compressor stream type was detected or if something else went wrong
224     * @throws IllegalArgumentException if stream is null or does not support mark
225     */
226    static String detect(final InputStream inputStream, final Set<String> compressorNames) throws CompressorException {
227        if (inputStream == null) {
228            throw new IllegalArgumentException("Stream must not be null.");
229        }
230
231        if (compressorNames == null || compressorNames.isEmpty()) {
232            throw new IllegalArgumentException("Compressor names cannot be null or empty");
233        }
234
235        if (!inputStream.markSupported()) {
236            throw new IllegalArgumentException("Mark is not supported.");
237        }
238
239        final byte[] signature = new byte[12];
240        inputStream.mark(signature.length);
241        int signatureLength = -1;
242        try {
243            signatureLength = IOUtils.readFully(inputStream, signature);
244            inputStream.reset();
245        } catch (final IOException e) {
246            throw new CompressorException("IOException while reading signature.", e);
247        }
248
249        if (compressorNames.contains(BZIP2) && BZip2CompressorInputStream.matches(signature, signatureLength)) {
250            return BZIP2;
251        }
252
253        if (compressorNames.contains(GZIP) && GzipCompressorInputStream.matches(signature, signatureLength)) {
254            return GZIP;
255        }
256
257        if (compressorNames.contains(PACK200) && Pack200CompressorInputStream.matches(signature, signatureLength)) {
258            return PACK200;
259        }
260
261        if (compressorNames.contains(SNAPPY_FRAMED) && FramedSnappyCompressorInputStream.matches(signature, signatureLength)) {
262            return SNAPPY_FRAMED;
263        }
264
265        if (compressorNames.contains(Z) && ZCompressorInputStream.matches(signature, signatureLength)) {
266            return Z;
267        }
268
269        if (compressorNames.contains(DEFLATE) && DeflateCompressorInputStream.matches(signature, signatureLength)) {
270            return DEFLATE;
271        }
272
273        if (compressorNames.contains(XZ) && XZUtils.matches(signature, signatureLength)) {
274            return XZ;
275        }
276
277        if (compressorNames.contains(LZMA) && LZMAUtils.matches(signature, signatureLength)) {
278            return LZMA;
279        }
280
281        if (compressorNames.contains(LZ4_FRAMED) && FramedLZ4CompressorInputStream.matches(signature, signatureLength)) {
282            return LZ4_FRAMED;
283        }
284
285        if (compressorNames.contains(ZSTANDARD) && ZstdUtils.matches(signature, signatureLength)) {
286            return ZSTANDARD;
287        }
288
289        throw new CompressorException("No Compressor found for the stream signature.");
290    }
291
292    /**
293     * Constructs a new sorted map from input stream provider names to provider objects.
294     *
295     * <p>
296     * 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
297     * supported provider have the same name then the resulting map will contain just one of them; which one it will contain is not specified.
298     * </p>
299     *
300     * <p>
301     * 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
302     * is provided for applications that need to enumerate all of the available providers, for example to allow user provider selection.
303     * </p>
304     *
305     * <p>
306     * This method may return different results at different times if new providers are dynamically made available to the current Java virtual machine.
307     * </p>
308     *
309     * @return An immutable, map from names to provider objects
310     * @since 1.13
311     */
312    public static SortedMap<String, CompressorStreamProvider> findAvailableCompressorInputStreamProviders() {
313        return AccessController.doPrivileged((PrivilegedAction<SortedMap<String, CompressorStreamProvider>>) () -> {
314            final TreeMap<String, CompressorStreamProvider> map = new TreeMap<>();
315            putAll(SINGLETON.getInputStreamCompressorNames(), SINGLETON, map);
316            archiveStreamProviderIterable().forEach(provider -> putAll(provider.getInputStreamCompressorNames(), provider, map));
317            return map;
318        });
319    }
320
321    /**
322     * Constructs a new sorted map from output stream provider names to provider objects.
323     *
324     * <p>
325     * 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
326     * supported provider have the same name then the resulting map will contain just one of them; which one it will contain is not specified.
327     * </p>
328     *
329     * <p>
330     * 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
331     * is provided for applications that need to enumerate all of the available providers, for example to allow user provider selection.
332     * </p>
333     *
334     * <p>
335     * This method may return different results at different times if new providers are dynamically made available to the current Java virtual machine.
336     * </p>
337     *
338     * @return An immutable, map from names to provider objects
339     * @since 1.13
340     */
341    public static SortedMap<String, CompressorStreamProvider> findAvailableCompressorOutputStreamProviders() {
342        return AccessController.doPrivileged((PrivilegedAction<SortedMap<String, CompressorStreamProvider>>) () -> {
343            final TreeMap<String, CompressorStreamProvider> map = new TreeMap<>();
344            putAll(SINGLETON.getOutputStreamCompressorNames(), SINGLETON, map);
345            archiveStreamProviderIterable().forEach(provider -> putAll(provider.getOutputStreamCompressorNames(), provider, map));
346            return map;
347        });
348    }
349
350    public static String getBrotli() {
351        return BROTLI;
352    }
353
354    public static String getBzip2() {
355        return BZIP2;
356    }
357
358    public static String getDeflate() {
359        return DEFLATE;
360    }
361
362    /**
363     * @since 1.16
364     * @return the constant {@link #DEFLATE64}
365     */
366    public static String getDeflate64() {
367        return DEFLATE64;
368    }
369
370    public static String getGzip() {
371        return GZIP;
372    }
373
374    public static String getLZ4Block() {
375        return LZ4_BLOCK;
376    }
377
378    public static String getLZ4Framed() {
379        return LZ4_FRAMED;
380    }
381
382    public static String getLzma() {
383        return LZMA;
384    }
385
386    public static String getPack200() {
387        return PACK200;
388    }
389
390    public static CompressorStreamFactory getSingleton() {
391        return SINGLETON;
392    }
393
394    public static String getSnappyFramed() {
395        return SNAPPY_FRAMED;
396    }
397
398    public static String getSnappyRaw() {
399        return SNAPPY_RAW;
400    }
401
402    public static String getXz() {
403        return XZ;
404    }
405
406    public static String getZ() {
407        return Z;
408    }
409
410    public static String getZstandard() {
411        return ZSTANDARD;
412    }
413
414    static void putAll(final Set<String> names, final CompressorStreamProvider provider, final TreeMap<String, CompressorStreamProvider> map) {
415        names.forEach(name -> map.put(toKey(name), provider));
416    }
417
418    private static String toKey(final String name) {
419        return name.toUpperCase(Locale.ROOT);
420    }
421
422    private static String youNeed(final String name, final String url) {
423        return " In addition to Apache Commons Compress you need the " + name + " library - see " + url;
424    }
425
426    /**
427     * 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
428     * stream
429     */
430    private final Boolean decompressUntilEOF;
431    // This is Boolean so setDecompressConcatenated can determine whether it has
432    // been set by the ctor
433    // once the setDecompressConcatenated method has been removed, it can revert
434    // to boolean
435
436    private SortedMap<String, CompressorStreamProvider> compressorInputStreamProviders;
437
438    private SortedMap<String, CompressorStreamProvider> compressorOutputStreamProviders;
439
440    /**
441     * 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
442     * stream
443     */
444    private volatile boolean decompressConcatenated;
445
446    private final int memoryLimitInKb;
447
448    /**
449     * Constructs an instance with the decompress Concatenated option set to false.
450     */
451    public CompressorStreamFactory() {
452        this.decompressUntilEOF = null;
453        this.memoryLimitInKb = -1;
454    }
455
456    /**
457     * Constructs an instance with the provided decompress Concatenated option.
458     *
459     * @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
460     *                           the next byte after the stream. This setting applies to the gzip, bzip2 and XZ formats only.
461     * @since 1.10
462     */
463    public CompressorStreamFactory(final boolean decompressUntilEOF) {
464        this(decompressUntilEOF, -1);
465    }
466
467    /**
468     * Constructs an instance with the provided decompress Concatenated option.
469     *
470     * @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
471     *                           the next byte after the stream. This setting applies to the gzip, bzip2 and XZ formats only.
472     * @param memoryLimitInKb    Some streams require allocation of potentially significant byte arrays/tables, and they can offer checks to prevent OOMs on
473     *                           corrupt files. Set the maximum allowed memory allocation in KBs.
474     *
475     * @since 1.14
476     */
477    public CompressorStreamFactory(final boolean decompressUntilEOF, final int memoryLimitInKb) {
478        this.decompressUntilEOF = decompressUntilEOF;
479        // Also copy to existing variable so can continue to use that as the
480        // current value
481        this.decompressConcatenated = decompressUntilEOF;
482        this.memoryLimitInKb = memoryLimitInKb;
483    }
484
485    /**
486     * 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
487     * support marks, like BufferedInputStream.
488     *
489     * @param in the input stream
490     * @return the compressor input stream
491     * @throws CompressorException      if the compressor name is not known
492     * @throws IllegalArgumentException if the stream is null or does not support mark
493     * @since 1.1
494     */
495    public CompressorInputStream createCompressorInputStream(final InputStream in) throws CompressorException {
496        return createCompressorInputStream(detect(in), in);
497    }
498
499    /**
500     * 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
501     * detected type to the provided set of compressor names. The InputStream must support marks, like BufferedInputStream.
502     *
503     * @param in              the input stream
504     * @param compressorNames compressor names to limit autodetection
505     * @return the compressor input stream
506     * @throws CompressorException      if the autodetected compressor is not in the provided set of compressor names
507     * @throws IllegalArgumentException if the stream is null or does not support mark
508     * @since 1.25.0
509     */
510    public CompressorInputStream createCompressorInputStream(final InputStream in, final Set<String> compressorNames) throws CompressorException {
511        return createCompressorInputStream(detect(in, compressorNames), in);
512    }
513
514    /**
515     * Creates a compressor input stream from a compressor name and an input stream.
516     *
517     * @param name of the compressor, i.e. {@value #GZIP}, {@value #BZIP2}, {@value #XZ}, {@value #LZMA}, {@value #PACK200}, {@value #SNAPPY_RAW},
518     *             {@value #SNAPPY_FRAMED}, {@value #Z}, {@value #LZ4_BLOCK}, {@value #LZ4_FRAMED}, {@value #ZSTANDARD}, {@value #DEFLATE64} or
519     *             {@value #DEFLATE}
520     * @param in   the input stream
521     * @return compressor input stream
522     * @throws CompressorException      if the compressor name is not known or not available, or if there's an IOException or MemoryLimitException thrown during
523     *                                  initialization
524     * @throws IllegalArgumentException if the name or input stream is null
525     */
526    public CompressorInputStream createCompressorInputStream(final String name, final InputStream in) throws CompressorException {
527        return createCompressorInputStream(name, in, decompressConcatenated);
528    }
529
530    @Override
531    public CompressorInputStream createCompressorInputStream(final String name, final InputStream in, final boolean actualDecompressConcatenated)
532            throws CompressorException {
533        if (name == null || in == null) {
534            throw new IllegalArgumentException("Compressor name and stream must not be null.");
535        }
536
537        try {
538
539            if (GZIP.equalsIgnoreCase(name)) {
540                return new GzipCompressorInputStream(in, actualDecompressConcatenated);
541            }
542
543            if (BZIP2.equalsIgnoreCase(name)) {
544                return new BZip2CompressorInputStream(in, actualDecompressConcatenated);
545            }
546
547            if (BROTLI.equalsIgnoreCase(name)) {
548                if (!BrotliUtils.isBrotliCompressionAvailable()) {
549                    throw new CompressorException("Brotli compression is not available." + YOU_NEED_BROTLI_DEC);
550                }
551                return new BrotliCompressorInputStream(in);
552            }
553
554            if (XZ.equalsIgnoreCase(name)) {
555                if (!XZUtils.isXZCompressionAvailable()) {
556                    throw new CompressorException("XZ compression is not available." + YOU_NEED_XZ_JAVA);
557                }
558                return new XZCompressorInputStream(in, actualDecompressConcatenated, memoryLimitInKb);
559            }
560
561            if (ZSTANDARD.equalsIgnoreCase(name)) {
562                if (!ZstdUtils.isZstdCompressionAvailable()) {
563                    throw new CompressorException("Zstandard compression is not available." + YOU_NEED_ZSTD_JNI);
564                }
565                return new ZstdCompressorInputStream(in);
566            }
567
568            if (LZMA.equalsIgnoreCase(name)) {
569                if (!LZMAUtils.isLZMACompressionAvailable()) {
570                    throw new CompressorException("LZMA compression is not available" + YOU_NEED_XZ_JAVA);
571                }
572                return new LZMACompressorInputStream(in, memoryLimitInKb);
573            }
574
575            if (PACK200.equalsIgnoreCase(name)) {
576                return new Pack200CompressorInputStream(in);
577            }
578
579            if (SNAPPY_RAW.equalsIgnoreCase(name)) {
580                return new SnappyCompressorInputStream(in);
581            }
582
583            if (SNAPPY_FRAMED.equalsIgnoreCase(name)) {
584                return new FramedSnappyCompressorInputStream(in);
585            }
586
587            if (Z.equalsIgnoreCase(name)) {
588                return new ZCompressorInputStream(in, memoryLimitInKb);
589            }
590
591            if (DEFLATE.equalsIgnoreCase(name)) {
592                return new DeflateCompressorInputStream(in);
593            }
594
595            if (DEFLATE64.equalsIgnoreCase(name)) {
596                return new Deflate64CompressorInputStream(in);
597            }
598
599            if (LZ4_BLOCK.equalsIgnoreCase(name)) {
600                return new BlockLZ4CompressorInputStream(in);
601            }
602
603            if (LZ4_FRAMED.equalsIgnoreCase(name)) {
604                return new FramedLZ4CompressorInputStream(in, actualDecompressConcatenated);
605            }
606
607        } catch (final IOException e) {
608            throw new CompressorException("Could not create CompressorInputStream.", e);
609        }
610        final CompressorStreamProvider compressorStreamProvider = getCompressorInputStreamProviders().get(toKey(name));
611        if (compressorStreamProvider != null) {
612            return compressorStreamProvider.createCompressorInputStream(name, in, actualDecompressConcatenated);
613        }
614
615        throw new CompressorException("Compressor: " + name + " not found.");
616    }
617
618    /**
619     * Creates a compressor output stream from a compressor name and an output stream.
620     *
621     * @param name the compressor name, i.e. {@value #GZIP}, {@value #BZIP2}, {@value #XZ}, {@value #PACK200}, {@value #SNAPPY_FRAMED}, {@value #LZ4_BLOCK},
622     *             {@value #LZ4_FRAMED}, {@value #ZSTANDARD} or {@value #DEFLATE}
623     * @param out  the output stream
624     * @return the compressor output stream
625     * @throws CompressorException      if the archiver name is not known
626     * @throws IllegalArgumentException if the archiver name or stream is null
627     */
628    @Override
629    public CompressorOutputStream createCompressorOutputStream(final String name, final OutputStream out) throws CompressorException {
630        if (name == null || out == null) {
631            throw new IllegalArgumentException("Compressor name and stream must not be null.");
632        }
633
634        try {
635
636            if (GZIP.equalsIgnoreCase(name)) {
637                return new GzipCompressorOutputStream(out);
638            }
639
640            if (BZIP2.equalsIgnoreCase(name)) {
641                return new BZip2CompressorOutputStream(out);
642            }
643
644            if (XZ.equalsIgnoreCase(name)) {
645                return new XZCompressorOutputStream(out);
646            }
647
648            if (PACK200.equalsIgnoreCase(name)) {
649                return new Pack200CompressorOutputStream(out);
650            }
651
652            if (LZMA.equalsIgnoreCase(name)) {
653                return new LZMACompressorOutputStream(out);
654            }
655
656            if (DEFLATE.equalsIgnoreCase(name)) {
657                return new DeflateCompressorOutputStream(out);
658            }
659
660            if (SNAPPY_FRAMED.equalsIgnoreCase(name)) {
661                return new FramedSnappyCompressorOutputStream(out);
662            }
663
664            if (LZ4_BLOCK.equalsIgnoreCase(name)) {
665                return new BlockLZ4CompressorOutputStream(out);
666            }
667
668            if (LZ4_FRAMED.equalsIgnoreCase(name)) {
669                return new FramedLZ4CompressorOutputStream(out);
670            }
671
672            if (ZSTANDARD.equalsIgnoreCase(name)) {
673                return new ZstdCompressorOutputStream(out);
674            }
675        } catch (final IOException e) {
676            throw new CompressorException("Could not create CompressorOutputStream", e);
677        }
678        final CompressorStreamProvider compressorStreamProvider = getCompressorOutputStreamProviders().get(toKey(name));
679        if (compressorStreamProvider != null) {
680            return compressorStreamProvider.createCompressorOutputStream(name, out);
681        }
682        throw new CompressorException("Compressor: " + name + " not found.");
683    }
684
685    public SortedMap<String, CompressorStreamProvider> getCompressorInputStreamProviders() {
686        if (compressorInputStreamProviders == null) {
687            compressorInputStreamProviders = Collections.unmodifiableSortedMap(findAvailableCompressorInputStreamProviders());
688        }
689        return compressorInputStreamProviders;
690    }
691
692    public SortedMap<String, CompressorStreamProvider> getCompressorOutputStreamProviders() {
693        if (compressorOutputStreamProviders == null) {
694            compressorOutputStreamProviders = Collections.unmodifiableSortedMap(findAvailableCompressorOutputStreamProviders());
695        }
696        return compressorOutputStreamProviders;
697    }
698
699    /** For tests. */
700    boolean getDecompressConcatenated() {
701        return decompressConcatenated;
702    }
703
704    public Boolean getDecompressUntilEOF() {
705        return decompressUntilEOF;
706    }
707
708    @Override
709    public Set<String> getInputStreamCompressorNames() {
710        return Sets.newHashSet(GZIP, BROTLI, BZIP2, XZ, LZMA, PACK200, DEFLATE, SNAPPY_RAW, SNAPPY_FRAMED, Z, LZ4_BLOCK, LZ4_FRAMED, ZSTANDARD, DEFLATE64);
711    }
712
713    @Override
714    public Set<String> getOutputStreamCompressorNames() {
715        return Sets.newHashSet(GZIP, BZIP2, XZ, LZMA, PACK200, DEFLATE, SNAPPY_FRAMED, LZ4_BLOCK, LZ4_FRAMED, ZSTANDARD);
716    }
717
718    /**
719     * Sets whether to decompress the full input or only the first stream in formats supporting multiple concatenated input streams.
720     *
721     * <p>
722     * This setting applies to the gzip, bzip2 and XZ formats only.
723     * </p>
724     *
725     * @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
726     *                               to the next byte after the stream
727     * @since 1.5
728     * @deprecated 1.10 use the {@link #CompressorStreamFactory(boolean)} constructor instead
729     * @throws IllegalStateException if the constructor {@link #CompressorStreamFactory(boolean)} was used to create the factory
730     */
731    @Deprecated
732    public void setDecompressConcatenated(final boolean decompressConcatenated) {
733        if (this.decompressUntilEOF != null) {
734            throw new IllegalStateException("Cannot override the setting defined by the constructor");
735        }
736        this.decompressConcatenated = decompressConcatenated;
737    }
738
739}