This is an automated email from the ASF dual-hosted git repository. ggregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-compress.git
commit 5829240bfd3531639767e40ddf9608b84d3b235c Author: Gary Gregory <garydgreg...@gmail.com> AuthorDate: Fri Apr 18 10:37:38 2025 -0400 Add org.apache.commons.compress.compressors.xz.XZCompressorInputStream.builder/Builder() - Add org.apache.commons.compress.compressors.xz.XZCompressorOutputStream.builder/Builder() - Add org.apache.commons.compress.compressors.xz.XZCompressorRoundtripTest --- pom.xml | 5 + src/changes/changes.xml | 2 + .../compressors/CompressorStreamFactory.java | 15 ++- .../compressors/xz/XZCompressorInputStream.java | 119 ++++++++++++++++++--- .../compressors/xz/XZCompressorOutputStream.java | 86 ++++++++++++++- .../compressors/xz/XZCompressorRoundtripTest.java | 117 ++++++++++++++++++++ 6 files changed, 327 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index 00e317295..4db05eb31 100644 --- a/pom.xml +++ b/pom.xml @@ -210,6 +210,11 @@ Brotli, Zstandard and ar, cpio, jar, tar, zip, dump, 7z, arj. <artifactId>commons-lang3</artifactId> <version>3.17.0</version> </dependency> + <dependency> + <groupId>org.junit-pioneer</groupId> + <artifactId>junit-pioneer</artifactId> + <scope>test</scope> + </dependency> </dependencies> <scm> <connection>scm:git:https://gitbox.apache.org/repos/asf/commons-compress.git</connection> diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 86c2aaea6..0ee09ff17 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -112,6 +112,8 @@ The <action> type attribute can be add,update,fix,remove. <action type="add" issue="COMPRESS-697" dev="ggregory" due-to="Fredrik Kjellberg, Gary Gregory">Move BitStream.nextBit() method to BitInputStream #663.</action> <action type="add" dev="ggregory" due-to="Gary Gregory">Add org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream.builder/Builder().</action> <action type="add" dev="ggregory" due-to="Gary Gregory">Add org.apache.commons.compress.compressors.lzma.LZMACompressorOutputStream.builder/Builder().</action> + <action type="add" dev="ggregory" due-to="Gary Gregory">Add org.apache.commons.compress.compressors.xz.XZCompressorInputStream.builder/Builder().</action> + <action type="add" dev="ggregory" due-to="Gary Gregory">Add org.apache.commons.compress.compressors.xz.XZCompressorOutputStream.builder/Builder().</action> <!-- UPDATE --> <action type="update" dev="sebb">Bump Commons Parent from 79 to 81</action> <action type="update" dev="ggregory" due-to="Dependabot, Gary Gregory">Bump org.apache.commons:commons-parent from 72 to 79 #563, #567, #574, #582, #587, #595.</action> diff --git a/src/main/java/org/apache/commons/compress/compressors/CompressorStreamFactory.java b/src/main/java/org/apache/commons/compress/compressors/CompressorStreamFactory.java index c6b1b0993..c02a90097 100644 --- a/src/main/java/org/apache/commons/compress/compressors/CompressorStreamFactory.java +++ b/src/main/java/org/apache/commons/compress/compressors/CompressorStreamFactory.java @@ -592,7 +592,12 @@ public CompressorInputStream createCompressorInputStream(final String name, fina } try { if (GZIP.equalsIgnoreCase(name)) { - return GzipCompressorInputStream.builder().setInputStream(in).setDecompressConcatenated(actualDecompressConcatenated).get(); + // @formatter:off + return GzipCompressorInputStream.builder() + .setInputStream(in) + .setDecompressConcatenated(actualDecompressConcatenated) + .get(); + // @formatter:on } if (BZIP2.equalsIgnoreCase(name)) { return new BZip2CompressorInputStream(in, actualDecompressConcatenated); @@ -607,7 +612,13 @@ public CompressorInputStream createCompressorInputStream(final String name, fina if (!XZUtils.isXZCompressionAvailable()) { throw new CompressorException("XZ compression is not available." + YOU_NEED_XZ_JAVA); } - return new XZCompressorInputStream(in, actualDecompressConcatenated, memoryLimitInKb); + // @formatter:off + return XZCompressorInputStream.builder() + .setInputStream(in) + .setDecompressConcatenated(actualDecompressConcatenated) + .setMemoryLimitKiB(memoryLimitInKb) + .get(); + // @formatter:on } if (ZSTANDARD.equalsIgnoreCase(name)) { if (!ZstdUtils.isZstdCompressionAvailable()) { diff --git a/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorInputStream.java b/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorInputStream.java index 9b7a88ef9..de7dc24c3 100644 --- a/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorInputStream.java +++ b/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorInputStream.java @@ -23,19 +23,103 @@ import org.apache.commons.compress.MemoryLimitException; import org.apache.commons.compress.compressors.CompressorInputStream; +import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream; +import org.apache.commons.compress.compressors.lzma.LZMACompressorOutputStream; import org.apache.commons.compress.utils.InputStreamStatistics; +import org.apache.commons.io.build.AbstractStreamBuilder; import org.apache.commons.io.input.BoundedInputStream; +import org.tukaani.xz.LZMA2Options; import org.tukaani.xz.SingleXZInputStream; import org.tukaani.xz.XZ; import org.tukaani.xz.XZInputStream; +// @formatter:off /** * XZ decompressor. - * + * <p> + * For example: + * </p> + * <pre>{@code + * XZCompressorInputStream s = XZCompressorInputStream.builder() + * .setPath(path) + * .setDecompressConcatenated(false) + * .setMemoryLimitKiB(-1) + * .get(); + * } + * </pre> * @since 1.4 */ +// @formatter:on public class XZCompressorInputStream extends CompressorInputStream implements InputStreamStatistics { + // @formatter:off + /** + * Builds a new {@link LZMACompressorInputStream}. + * + * <p> + * For example: + * </p> + * <pre>{@code + * XZCompressorInputStream s = XZCompressorInputStream.builder() + * .setPath(path) + * .setDecompressConcatenated(false) + * .setMemoryLimitKiB(-1) + * .get(); + * } + * </pre> + * + * @see #get() + * @see LZMA2Options + * @since 1.28.0 + */ + // @formatter:on + public static class Builder extends AbstractStreamBuilder<XZCompressorInputStream, Builder> { + + private int memoryLimitKiB = -1; + private boolean decompressConcatenated; + + @Override + public XZCompressorInputStream get() throws IOException { + return new XZCompressorInputStream(this); + } + + /** + * Whether to decompress until the end of the input. + * + * @param decompressConcatenated if true, decompress until the end of the input; if false, stop after the first .xz stream and leave the input position + * to point to the next byte after the .xz stream + * @return this instance. + */ + public Builder setDecompressConcatenated(final boolean decompressConcatenated) { + this.decompressConcatenated = decompressConcatenated; + return this; + } + + + /** + * Sets a working memory threshold in kibibytes (KiB). + * + * @param memoryLimitKiB The memory limit used when reading blocks. The memory usage limit is expressed in kibibytes (KiB) or {@code -1} to impose no + * memory usage limit. If the estimated memory limit is exceeded on {@link #read()}, a {@link MemoryLimitException} is thrown. + * @return this instance. + */ + public Builder setMemoryLimitKiB(final int memoryLimitKiB) { + this.memoryLimitKiB = memoryLimitKiB; + return this; + } + } + + /** + * Constructs a new builder of {@link LZMACompressorOutputStream}. + * + * @return a new builder of {@link LZMACompressorOutputStream}. + * @since 1.28.0 + */ + public static Builder builder() { + return new Builder(); + } + + /** * Checks if the signature matches what is expected for a .xz file. * @@ -61,6 +145,16 @@ public static boolean matches(final byte[] signature, final int length) { private final InputStream in; + @SuppressWarnings("resource") // Caller closes + private XZCompressorInputStream(final Builder builder) throws IOException { + countingStream = BoundedInputStream.builder().setInputStream(builder.getInputStream()).get(); + if (builder.decompressConcatenated) { + in = new XZInputStream(countingStream, builder.memoryLimitKiB); + } else { + in = new SingleXZInputStream(countingStream, builder.memoryLimitKiB); + } + } + /** * Creates a new input stream that decompresses XZ-compressed data from the specified input stream. This doesn't support concatenated .xz files. * @@ -69,7 +163,7 @@ public static boolean matches(final byte[] signature, final int length) { * this implementation, or the underlying {@code inputStream} throws an exception */ public XZCompressorInputStream(final InputStream inputStream) throws IOException { - this(inputStream, false); + this(builder().setInputStream(inputStream)); } /** @@ -81,9 +175,11 @@ public XZCompressorInputStream(final InputStream inputStream) throws IOException * * @throws IOException if the input is not in the .xz format, the input is corrupt or truncated, the .xz headers specify options that are not supported by * this implementation, or the underlying {@code inputStream} throws an exception + * @deprecated Use {@link #builder()}. */ + @Deprecated public XZCompressorInputStream(final InputStream inputStream, final boolean decompressConcatenated) throws IOException { - this(inputStream, decompressConcatenated, -1); + this(builder().setInputStream(inputStream).setDecompressConcatenated(decompressConcatenated)); } /** @@ -92,23 +188,22 @@ public XZCompressorInputStream(final InputStream inputStream, final boolean deco * @param inputStream where to read the compressed data * @param decompressConcatenated if true, decompress until the end of the input; if false, stop after the first .xz stream and leave the input position to * point to the next byte after the .xz stream - * @param memoryLimitInKb memory limit used when reading blocks. If the estimated memory limit is exceeded on {@link #read()}, a - * {@link MemoryLimitException} is thrown. + * @param memoryLimitKiB The memory limit used when reading blocks. The memory usage limit is expressed in kibibytes (KiB) or {@code -1} to impose + * no memory usage limit. If the estimated memory limit is exceeded on {@link #read()}, a {@link MemoryLimitException} is + * thrown. * * @throws IOException if the input is not in the .xz format, the input is corrupt or truncated, the .xz headers specify options that are not supported by * this implementation, or the underlying {@code inputStream} throws an exception * + * @deprecated Use {@link #builder()}. * @since 1.14 */ - public XZCompressorInputStream(final InputStream inputStream, final boolean decompressConcatenated, final int memoryLimitInKb) throws IOException { - countingStream = BoundedInputStream.builder().setInputStream(inputStream).get(); - if (decompressConcatenated) { - in = new XZInputStream(countingStream, memoryLimitInKb); - } else { - in = new SingleXZInputStream(countingStream, memoryLimitInKb); - } + @Deprecated + public XZCompressorInputStream(final InputStream inputStream, final boolean decompressConcatenated, final int memoryLimitKiB) throws IOException { + this(builder().setInputStream(inputStream).setDecompressConcatenated(decompressConcatenated).setMemoryLimitKiB(memoryLimitKiB)); } + @Override public int available() throws IOException { return in.available(); diff --git a/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorOutputStream.java b/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorOutputStream.java index f59958da1..23a04d60f 100644 --- a/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorOutputStream.java +++ b/src/main/java/org/apache/commons/compress/compressors/xz/XZCompressorOutputStream.java @@ -22,13 +22,25 @@ import java.io.OutputStream; import org.apache.commons.compress.compressors.CompressorOutputStream; +import org.apache.commons.io.build.AbstractStreamBuilder; import org.tukaani.xz.LZMA2Options; import org.tukaani.xz.XZOutputStream; +// @formatter:off /** * Compresses an output stream using the XZ and LZMA2 compression options. + * <p> + * For example: + * </p> + * <pre>{@code + * XZCompressorOutputStream s = XZCompressorOutputStream.builder() + * .setPath(path) + * .setLzma2Options(new LZMA2Options(...)) + * .get(); + * } + * </pre> * - * <em>Calling flush()</em> + * <h2>Calling flush</h2> * <p> * Calling {@link #flush()} flushes the encoder and calls {@code outputStream.flush()}. All buffered pending data will then be decompressible from the output * stream. Calling this function very often may increase the compressed file size a lot. @@ -36,17 +48,83 @@ * * @since 1.4 */ +// @formatter:on public class XZCompressorOutputStream extends CompressorOutputStream<XZOutputStream> { + // @formatter:off + /** + * Builds a new {@link XZCompressorOutputStream}. + * + * <p> + * For example: + * </p> + * <pre>{@code + * XZCompressorOutputStream s = XZCompressorOutputStream.builder() + * .setPath(path) + * .setLzma2Options(new LZMA2Options(...)) + * .get(); + * } + * </pre> + * + * @see #get() + * @since 1.28.0 + */ + // @formatter:on + public static class Builder extends AbstractStreamBuilder<XZCompressorOutputStream, Builder> { + + private LZMA2Options lzma2Options = new LZMA2Options(); + + /** + * Constructs a new builder of {@link XZCompressorOutputStream}. + */ + public Builder() { + // empty + } + + @Override + public XZCompressorOutputStream get() throws IOException { + return new XZCompressorOutputStream(this); + } + + /** + * Sets LZMA options. + * <p> + * Passing {@code null} resets to the default value {@link LZMA2Options#LZMA2Options()}. + * </p> + * + * @param lzma2Options LZMA options. + * @return this instance. + */ + public Builder setLzma2Options(final LZMA2Options lzma2Options) { + this.lzma2Options = lzma2Options != null ? lzma2Options : new LZMA2Options(); + return this; + } + + } + + /** + * Constructs a new builder of {@link XZCompressorOutputStream}. + * + * @return a new builder of {@link XZCompressorOutputStream}. + * @since 1.28.0 + */ + public static Builder builder() { + return new Builder(); + } + + @SuppressWarnings("resource") // Caller closes + private XZCompressorOutputStream(final Builder builder) throws IOException { + super(new XZOutputStream(builder.getOutputStream(), builder.lzma2Options)); + } + /** * Creates a new XZ compressor using the default LZMA2 options. This is equivalent to {@code XZCompressorOutputStream(outputStream, 6)}. * * @param outputStream the stream to wrap * @throws IOException on error */ - @SuppressWarnings("resource") // Caller closes public XZCompressorOutputStream(final OutputStream outputStream) throws IOException { - super(new XZOutputStream(outputStream, new LZMA2Options())); + this(builder().setOutputStream(outputStream)); } /** @@ -62,7 +140,9 @@ public XZCompressorOutputStream(final OutputStream outputStream) throws IOExcept * @param outputStream the stream to wrap * @param preset the preset * @throws IOException on error + * @deprecated Use {@link #builder()}. */ + @Deprecated @SuppressWarnings("resource") // Caller closes public XZCompressorOutputStream(final OutputStream outputStream, final int preset) throws IOException { super(new XZOutputStream(outputStream, new LZMA2Options(preset))); diff --git a/src/test/java/org/apache/commons/compress/compressors/xz/XZCompressorRoundtripTest.java b/src/test/java/org/apache/commons/compress/compressors/xz/XZCompressorRoundtripTest.java new file mode 100644 index 000000000..9b24e9710 --- /dev/null +++ b/src/test/java/org/apache/commons/compress/compressors/xz/XZCompressorRoundtripTest.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.commons.compress.compressors.xz; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junitpioneer.jupiter.cartesian.CartesianTest; +import org.junitpioneer.jupiter.cartesian.CartesianTest.Values; +import org.tukaani.xz.LZMA2Options; +import org.tukaani.xz.XZOutputStream; + +/** + * Tests for class {@link XZCompressorOutputStream} and {@link XZCompressorInputStream}. + * + * @see XZCompressorOutputStream + * @see XZCompressorInputStream + */ +public class XZCompressorRoundtripTest { + + @TempDir + static Path tempDir; + + private void roundtrip(final Path outPath, final LZMA2Options options, final boolean decompressConcatenated, final int memoryLimitKiB) throws IOException { + final String data = "Hello World!"; + // @formatter:off + try (XZCompressorOutputStream out = XZCompressorOutputStream.builder() + .setPath(outPath) + .setLzma2Options(options) + .get()) { + out.writeUtf8(data); + } + try (XZCompressorInputStream out = XZCompressorInputStream.builder() + .setPath(outPath) + .setDecompressConcatenated(decompressConcatenated) + .setMemoryLimitKiB(memoryLimitKiB) + .get()) { + assertEquals(data, IOUtils.toString(out, StandardCharsets.UTF_8)); + } + // @formatter:on + try (XZCompressorInputStream out = new XZCompressorInputStream(Files.newInputStream(outPath))) { + assertEquals(data, IOUtils.toString(out, StandardCharsets.UTF_8)); + } + // deprecated + try (XZCompressorOutputStream out = new XZCompressorOutputStream(new XZOutputStream(Files.newOutputStream(outPath), options))) { + out.writeUtf8(data); + } + } + + @Test + public void testBuilderOptionsAll() throws IOException { + final int dictSize = LZMA2Options.DICT_SIZE_MIN; + final int lc = LZMA2Options.LC_LP_MAX - 4; + final int lp = LZMA2Options.LC_LP_MAX - 4; + final int pb = LZMA2Options.PB_MAX; + final int mode = LZMA2Options.MODE_NORMAL; + final int niceLen = LZMA2Options.NICE_LEN_MIN; + final int mf = LZMA2Options.MF_BT4; + final int depthLimit = 50; + roundtrip(tempDir.resolve("out.xz"), new LZMA2Options(dictSize, lc, lp, pb, mode, niceLen, mf, depthLimit), false, -1); + } + + @CartesianTest + public void testBuilderOptions(@Values(ints = { LZMA2Options.PRESET_MAX, LZMA2Options.PRESET_MIN, LZMA2Options.PRESET_DEFAULT }) final int preset, + @Values(booleans = { false, true }) final boolean decompressConcatenated, @Values(ints = { -1, 100_000 }) final int memoryLimitKiB) + throws IOException { + roundtrip(tempDir.resolve("out.xz"), new LZMA2Options(preset), false, -1); + } + + @CartesianTest + public void testBuilderOptionsDefgault(@Values(booleans = { false, true }) final boolean decompressConcatenated, + @Values(ints = { -1, 100_000 }) final int memoryLimitKiB) throws IOException { + roundtrip(tempDir.resolve("out.xz"), new LZMA2Options(), decompressConcatenated, memoryLimitKiB); + } + + @Test + public void testBuilderPath() throws IOException { + // This test does not use LZMA2Options + final String data = "Hello World!"; + final Path outPath = tempDir.resolve("out.xz"); + try (XZCompressorOutputStream out = XZCompressorOutputStream.builder().setPath(outPath).get()) { + out.writeUtf8(data); + } + // deprecated + try (XZCompressorInputStream out = new XZCompressorInputStream(Files.newInputStream(outPath), false)) { + assertEquals(data, IOUtils.toString(out, StandardCharsets.UTF_8)); + } + // deprecated + try (XZCompressorInputStream out = new XZCompressorInputStream(Files.newInputStream(outPath), false, -1)) { + assertEquals(data, IOUtils.toString(out, StandardCharsets.UTF_8)); + } + } +}