This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-compress.git
The following commit(s) were added to refs/heads/master by this push:
new 778fc583a Add reusable Huffman decoder. (#792)
778fc583a is described below
commit 778fc583a2bd14d82be1bd619c9319070e4bd997
Author: Gary Gregory <[email protected]>
AuthorDate: Tue Aug 4 08:21:13 2026 -0400
Add reusable Huffman decoder. (#792)
Rework of stale PR #720.
---
.../bzip2/BZip2CompressorInputStream.java | 192 +++-------------
.../compressors/deflate64/Deflate64Decoder.java | 114 ++--------
.../commons/compress/huffman/HuffmanDecoder.java | 248 +++++++++++++++++++++
.../commons/compress/huffman/package-info.java | 25 +++
.../bzip2/BZip2CompressorInputStreamTest.java | 26 +--
.../Deflate64CompressorInputStreamTest.java | 2 +-
.../deflate64/Deflate64DecoderTest.java | 4 +-
.../compress/huffman/HuffmanDecoderTest.java | 102 +++++++++
8 files changed, 428 insertions(+), 285 deletions(-)
diff --git
a/src/main/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorInputStream.java
b/src/main/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorInputStream.java
index 36b37e6c1..6e8eee45e 100644
---
a/src/main/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorInputStream.java
+++
b/src/main/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorInputStream.java
@@ -31,6 +31,7 @@
import org.apache.commons.compress.compressors.CompressorException;
import org.apache.commons.compress.compressors.CompressorInputStream;
+import org.apache.commons.compress.huffman.HuffmanDecoder;
import org.apache.commons.compress.utils.BitInputStream;
import org.apache.commons.compress.utils.InputStreamStatistics;
import org.apache.commons.io.IOUtils;
@@ -60,16 +61,17 @@ static final class Data {
*/
final int[] unzftab = new int[256]; // 1024 byte
- // Needs indexes from 0 to MAX_CODE_LEN inclusive.
- final int[][] limit = new int[N_GROUPS][MAX_CODE_LEN + 1];
- // Needs indexes from 0 to MAX_CODE_LEN + 1 inclusive.
- final int[][] base = new int[N_GROUPS][MAX_CODE_LEN + 2];
- final int[][] perm = new int[N_GROUPS][MAX_ALPHA_SIZE]; // 6192 byte
- final int[] minLens = new int[N_GROUPS]; // 24 byte
+ /**
+ * Huffman decoding tables.
+ */
+ final HuffmanDecoder[] huffmanDecoders = new HuffmanDecoder[N_GROUPS];
+ /**
+ * Number of non-null {@link #huffmanDecoders}.
+ */
+ int huffmanDecodersCount;
final int[] cftab = new int[257]; // 1028 byte
final char[] getAndMoveToFrontDecode_yy = new char[256]; // 512 byte
- final char[][] temp_charArray2d = new char[N_GROUPS][MAX_ALPHA_SIZE];
// 3096
// byte
final byte[] recvDecodingTables_pos = new byte[N_GROUPS]; // 6 byte
// ---------------
@@ -159,85 +161,6 @@ private static void checkBounds(final int checkVal, final
int limitExclusive, fi
}
}
- /**
- * Builds the Huffman decoding tables for use by {@code
recvDecodingTables()}.
- *
- * @param alphaSize The alphabet size, guaranteed by the caller to be in
the range [2, 258]
- * (RUNA, RUNB, 255 byte values, and EOB).
- * @param nGroups The number of Huffman coding groups, guaranteed by the
caller to be in the range [0, 6].
- * @param dataShadow The data structure into which the tables are built;
requires
- * {@code temp_charArray2d} to be initialized.
- */
- static void createHuffmanDecodingTables(final int alphaSize, final int
nGroups, final Data dataShadow) {
- final char[][] len = dataShadow.temp_charArray2d;
- final int[] minLens = dataShadow.minLens;
- final int[][] limit = dataShadow.limit;
- final int[][] base = dataShadow.base;
- final int[][] perm = dataShadow.perm;
-
- for (int t = 0; t < nGroups; t++) {
- final char[] len_t = len[t];
- int minLen = len_t[0];
- int maxLen = len_t[0];
- for (int i = 1; i < alphaSize; i++) {
- final char lent = len_t[i];
- if (lent > maxLen) {
- maxLen = lent;
- }
- if (lent < minLen) {
- minLen = lent;
- }
- }
- hbCreateDecodeTables(limit[t], base[t], perm[t], len[t], minLen,
maxLen, alphaSize);
- minLens[t] = minLen;
- }
- }
-
- /**
- * Called by createHuffmanDecodingTables() exclusively.
- *
- * @param minLen minimum code length in the range [1, {@value
MAX_CODE_LEN}] guaranteed by the caller.
- * @param maxLen maximum code length in the range [1, {@value
MAX_CODE_LEN}] guaranteed by the caller.
- */
- private static void hbCreateDecodeTables(final int[] limit, final int[]
base, final int[] perm, final char[] length, final int minLen, final int maxLen,
- final int alphaSize) {
- for (int i = minLen, pp = 0; i <= maxLen; i++) {
- for (int j = 0; j < alphaSize; j++) {
- if (length[j] == i) {
- perm[pp++] = j;
- }
- }
- }
- // Ensure the arrays were not reused.
- Arrays.fill(base, 0);
- Arrays.fill(limit, minLen, maxLen + 1, 0);
- // Compute histogram of code lengths, shifted by 1.
- for (int i = 0; i < alphaSize; i++) {
- final int len = length[i] + 1;
- base[len]++;
- }
- // Compute cumulative counts: base[len] = # of codes with length < len.
- // In other terms: base[len] = index of the first code in the `perm`
table.
- for (int len = 1; len < base.length; len++) {
- base[len] += base[len - 1];
- }
- // Compute the last code for each length.
- int vec = 0;
- for (int len = minLen; len <= maxLen; len++) {
- // increment by the number of length `len` codes
- vec += base[len + 1] - base[len];
- // vec is now the last code of length `len` + 1
- limit[len] = vec - 1;
- vec <<= 1;
- }
- // Compute the bias between code value and table index.
- // base[minLen] cannot be computed using this rule, since limit[minLen
- 1] does not exist,
- // but has already the correct value 0.
- for (int len = minLen + 1; len <= maxLen; len++) {
- base[len] = (limit[len - 1] + 1 << 1) - base[len];
- }
- }
-
private static void makeMaps(final Data data) throws IOException {
final boolean[] inUse = data.inUse;
final byte[] seqToUnseq = data.seqToUnseq;
@@ -335,27 +258,24 @@ static void recvDecodingTables(final BitInputStream bin,
final Data dataShadow)
selector[i] = tmp;
}
- final char[][] len = dataShadow.temp_charArray2d;
-
- /* Now the coding tables */
+ /* Now the Huffman coding tables */
for (int t = 0; t < nGroups; t++) {
+ final int[] codeLengths = new int[alphaSize];
int curr = bsR(bin, 5);
- final char[] len_t = len[t];
for (int i = 0; i < alphaSize; i++) {
while (bsGetBit(bin)) {
curr += bsGetBit(bin) ? -1 : 1;
}
- // Same condition as in bzip2
- if (curr < 1 || curr > MAX_CODE_LEN) {
- throw new CompressorException(
- "Corrupted input, code length value out of range
[%d, %d]: %d", 1, MAX_CODE_LEN, curr);
- }
- len_t[i] = (char) curr;
+ codeLengths[i] = curr;
+ }
+ try {
+ // Same limits as in the reference C implementation of bzip2
+ dataShadow.huffmanDecoders[t] = new
HuffmanDecoder(codeLengths, alphaSize, 1, MAX_CODE_LEN);
+ } catch (final IllegalArgumentException e) {
+ throw new CompressorException("Invalid Huffman data: " +
e.getMessage(), e);
}
}
-
- // finally create the Huffman tables
- createHuffmanDecodingTables(alphaSize, nGroups, dataShadow);
+ dataShadow.huffmanDecodersCount = nGroups;
}
// Variables used by setup* methods exclusively
@@ -477,10 +397,6 @@ private void getAndMoveToFrontDecode() throws IOException {
final byte[] selector = dataShadow.selector;
final byte[] seqToUnseq = dataShadow.seqToUnseq;
final char[] yy = dataShadow.getAndMoveToFrontDecode_yy;
- final int[] minLens = dataShadow.minLens;
- final int[][] limit = dataShadow.limit;
- final int[][] base = dataShadow.base;
- final int[][] perm = dataShadow.perm;
final int limitLast = this.blockSize100k * 100000;
/*
* Setting up the unzftab entries here is not strictly necessary, but
it does save having to do it later in a separate pass, and so saves a block's
@@ -490,18 +406,14 @@ private void getAndMoveToFrontDecode() throws IOException
{
yy[i] = (char) i;
unzftab[i] = 0;
}
- int groupNo = 0;
int groupPos = G_SIZE - 1;
final int eob = dataShadow.inUseCount + 1;
- int nextSym = getAndMoveToFrontDecode0();
int lastShadow = -1;
+ // Initialize group, selector and huffmanDecoder
+ int groupNo = 0;
int zt = selector[groupNo] & 0xff;
- // All arrays have the same length
- checkBounds(zt, base.length, "zt");
- int[] base_zt = base[zt];
- int[] limit_zt = limit[zt];
- int[] perm_zt = perm[zt];
- int minLens_zt = minLens[zt];
+ HuffmanDecoder currentDecoder = getHuffmanDecoder(dataShadow, zt);
+ int nextSym = currentDecoder.decodeSymbol(bin);
while (nextSym != eob) {
if (nextSym == RUNA || nextSym == RUNB) {
int s = -1;
@@ -517,25 +429,11 @@ private void getAndMoveToFrontDecode() throws IOException
{
groupPos = G_SIZE - 1;
checkBounds(++groupNo, selector.length, "groupNo");
zt = selector[groupNo] & 0xff;
- // All arrays have the same length
- checkBounds(zt, base.length, "zt");
- base_zt = base[zt];
- limit_zt = limit[zt];
- perm_zt = perm[zt];
- minLens_zt = minLens[zt];
+ currentDecoder = getHuffmanDecoder(dataShadow, zt);
} else {
groupPos--;
}
- int zn = minLens_zt;
- checkBounds(zn, limit_zt.length, "zn");
- int zvec = bsR(bin, zn);
- while (zvec > limit_zt[zn]) {
- checkBounds(++zn, limit_zt.length, "zn");
- zvec = zvec << 1 | bsR(bin, 1);
- }
- final int tmp = zvec - base_zt[zn];
- checkBounds(tmp, perm_zt.length, "zvec");
- nextSym = perm_zt[tmp];
+ nextSym = currentDecoder.decodeSymbol(bin);
}
checkBounds(s, this.data.ll8.length, "s");
final int yy0 = yy[0];
@@ -573,47 +471,16 @@ private void getAndMoveToFrontDecode() throws IOException
{
groupPos = G_SIZE - 1;
checkBounds(++groupNo, selector.length, "groupNo");
zt = selector[groupNo] & 0xff;
- // All arrays have the same length
- checkBounds(zt, base.length, "zt");
- base_zt = base[zt];
- limit_zt = limit[zt];
- perm_zt = perm[zt];
- minLens_zt = minLens[zt];
+ currentDecoder = getHuffmanDecoder(dataShadow, zt);
} else {
groupPos--;
}
- int zn = minLens_zt;
- checkBounds(zn, limit_zt.length, "zn");
- int zvec = bsR(bin, zn);
- while (zvec > limit_zt[zn]) {
- checkBounds(++zn, limit_zt.length, "zn");
- zvec = zvec << 1 | bsR(bin, 1);
- }
- final int idx = zvec - base_zt[zn];
- checkBounds(idx, perm_zt.length, "zvec");
- nextSym = perm_zt[idx];
+ nextSym = currentDecoder.decodeSymbol(bin);
}
}
this.last = lastShadow;
}
- private int getAndMoveToFrontDecode0() throws IOException {
- final Data dataShadow = this.data;
- final int zt = dataShadow.selector[0] & 0xff;
- checkBounds(zt, dataShadow.limit.length, "zt");
- final int[] limit_zt = dataShadow.limit[zt];
- int zn = dataShadow.minLens[zt];
- checkBounds(zn, limit_zt.length, "zn");
- int zvec = bsR(bin, zn);
- while (zvec > limit_zt[zn]) {
- checkBounds(++zn, limit_zt.length, "zn");
- zvec = zvec << 1 | bsR(bin, 1);
- }
- final int tmp = zvec - dataShadow.base[zt][zn];
- checkBounds(tmp, dataShadow.perm[zt].length, "zvec");
- return dataShadow.perm[zt][tmp];
- }
-
/**
* @since 1.17
*/
@@ -924,4 +791,9 @@ private int setupRandPartC() throws IOException {
this.su_count = 0;
return setupRandPartA();
}
+
+ private static HuffmanDecoder getHuffmanDecoder(final Data dataShadow,
final int zt) throws IOException {
+ checkBounds(zt, dataShadow.huffmanDecodersCount, "zt");
+ return dataShadow.huffmanDecoders[zt];
+ }
}
diff --git
a/src/main/java/org/apache/commons/compress/compressors/deflate64/Deflate64Decoder.java
b/src/main/java/org/apache/commons/compress/compressors/deflate64/Deflate64Decoder.java
index 2bf8a53b5..d451da937 100644
---
a/src/main/java/org/apache/commons/compress/compressors/deflate64/Deflate64Decoder.java
+++
b/src/main/java/org/apache/commons/compress/compressors/deflate64/Deflate64Decoder.java
@@ -31,6 +31,7 @@
import java.util.Arrays;
import org.apache.commons.compress.compressors.CompressorException;
+import org.apache.commons.compress.huffman.HuffmanDecoder;
import org.apache.commons.compress.utils.BitInputStream;
import org.apache.commons.compress.utils.ExactMath;
import org.apache.commons.lang3.ArrayFill;
@@ -41,37 +42,6 @@
*/
class Deflate64Decoder implements Closeable {
- private static final class BinaryTreeNode {
- private final int bits;
- int literal = -1;
- BinaryTreeNode leftNode;
- BinaryTreeNode rightNode;
-
- private BinaryTreeNode(final int bits) {
- this.bits = bits;
- }
-
- void leaf(final int symbol) {
- literal = symbol;
- leftNode = null;
- rightNode = null;
- }
-
- BinaryTreeNode left() {
- if (leftNode == null && literal == -1) {
- leftNode = new BinaryTreeNode(bits + 1);
- }
- return leftNode;
- }
-
- BinaryTreeNode right() {
- if (rightNode == null && literal == -1) {
- rightNode = new BinaryTreeNode(bits + 1);
- }
- return rightNode;
- }
- }
-
private abstract static class DecoderState {
abstract int available() throws IOException;
@@ -134,8 +104,8 @@ void recordToBuffer(final int distance, final int length,
final byte[] buff) {
private final class HuffmanCodes extends DecoderState {
private boolean endOfBlock;
private final Deflate64State state;
- private final BinaryTreeNode lengthTree;
- private final BinaryTreeNode distanceTree;
+ private final HuffmanDecoder symbolDecoder;
+ private final HuffmanDecoder distanceDecoder;
private int runBufferPos;
private byte[] runBuffer = ArrayUtils.EMPTY_BYTE_ARRAY;
@@ -143,8 +113,8 @@ private final class HuffmanCodes extends DecoderState {
HuffmanCodes(final Deflate64State state, final int[] lengths, final
int[] distance) {
this.state = state;
- lengthTree = buildTree(lengths);
- distanceTree = buildTree(distance);
+ symbolDecoder = new HuffmanDecoder(lengths);
+ distanceDecoder = new HuffmanDecoder(distance);
}
@Override
@@ -170,7 +140,7 @@ private int decodeNext(final byte[] b, final int off, final
int len) throws IOEx
int result = copyFromRunBuffer(b, off, len);
while (result < len) {
- final int symbol = nextSymbol(reader, lengthTree);
+ final int symbol = symbolDecoder.decodeSymbol(reader);
if (symbol < 256) {
if (symbol < 0) {
throw new CompressorException("Invalid Deflate64
literal/length code %,d", symbol);
@@ -185,7 +155,7 @@ private int decodeNext(final byte[] b, final int off, final
int len) throws IOEx
final int runXtra = runMask & 0x1F;
run = ExactMath.add(run, readBits(runXtra));
- final int distSym = nextSymbol(reader, distanceTree);
+ final int distSym = distanceDecoder.decodeSymbol(reader);
if (distSym < 0 || distSym >= DISTANCE_TABLE.length) {
throw new CompressorException("Invalid Deflate64
distance code %,d", distSym);
}
@@ -219,10 +189,7 @@ boolean hasData() {
@Override
int read(final byte[] b, final int off, final int len) throws
IOException {
- if (len == 0) {
- return 0;
- }
- return decodeNext(b, off, len);
+ return len == 0 ? 0 : decodeNext(b, off, len);
}
@Override
@@ -243,7 +210,7 @@ boolean hasData() {
}
@Override
- int read(final byte[] b, final int off, final int len) throws
IOException {
+ int read(final byte[] b, final int off, final int len) {
if (len == 0) {
return 0;
}
@@ -380,71 +347,16 @@ Deflate64State state() {
FIXED_DISTANCE = ArrayFill.fill(new int[32], 5);
}
- private static BinaryTreeNode buildTree(final int[] litTable) {
- final int[] literalCodes = getCodes(litTable);
-
- final BinaryTreeNode root = new BinaryTreeNode(0);
-
- for (int i = 0; i < litTable.length; i++) {
- final int len = litTable[i];
- if (len != 0) {
- BinaryTreeNode node = root;
- final int lit = literalCodes[len - 1];
- for (int p = len - 1; p >= 0; p--) {
- final int bit = lit & 1 << p;
- node = bit == 0 ? node.left() : node.right();
- if (node == null) {
- throw new IllegalStateException("node doesn't exist in
Huffman tree");
- }
- }
- node.leaf(i);
- literalCodes[len - 1]++;
- }
- }
- return root;
- }
-
- private static int[] getCodes(final int[] litTable) {
- int max = 0;
- int[] blCount = new int[65];
-
- for (final int aLitTable : litTable) {
- if (aLitTable < 0 || aLitTable > 64) {
- throw new IllegalArgumentException("Invalid code " + aLitTable
+ " in literal table");
- }
- max = Math.max(max, aLitTable);
- blCount[aLitTable]++;
- }
- blCount = Arrays.copyOf(blCount, max + 1);
-
- int code = 0;
- final int[] nextCode = new int[max + 1];
- for (int i = 0; i <= max; i++) {
- code = code + blCount[i] << 1;
- nextCode[i] = code;
- }
-
- return nextCode;
- }
-
- private static int nextSymbol(final BitInputStream reader, final
BinaryTreeNode tree) throws IOException {
- BinaryTreeNode node = tree;
- while (node != null && node.literal == -1) {
- final long bit = readBits(reader, 1);
- node = bit == 0 ? node.leftNode : node.rightNode;
- }
- return node != null ? node.literal : -1;
- }
-
private static void populateDynamicTables(final BitInputStream reader,
final int[] literals, final int[] distances) throws IOException {
final int codeLengths = (int) (readBits(reader, 4) + 4);
- final int[] codeLengthValues = new int[19];
+ final int[] codeLengthValues = new int[CODE_LENGTHS_ORDER.length];
for (int cLen = 0; cLen < codeLengths; cLen++) {
codeLengthValues[CODE_LENGTHS_ORDER[cLen]] = (int)
readBits(reader, 3);
}
- final BinaryTreeNode codeLengthTree = buildTree(codeLengthValues);
+ // Decoder to decode the code lengths for the literal and distance
tables
+ final HuffmanDecoder codeLengthDecoder = new
HuffmanDecoder(codeLengthValues);
final int[] auxBuffer = new int[literals.length + distances.length];
@@ -456,7 +368,7 @@ private static void populateDynamicTables(final
BitInputStream reader, final int
auxBuffer[off++] = value;
length--;
} else {
- final int symbol = nextSymbol(reader, codeLengthTree);
+ final int symbol = codeLengthDecoder.decodeSymbol(reader);
if (symbol < 16) {
value = symbol;
auxBuffer[off++] = value;
diff --git
a/src/main/java/org/apache/commons/compress/huffman/HuffmanDecoder.java
b/src/main/java/org/apache/commons/compress/huffman/HuffmanDecoder.java
new file mode 100644
index 000000000..6ac24f2c4
--- /dev/null
+++ b/src/main/java/org/apache/commons/compress/huffman/HuffmanDecoder.java
@@ -0,0 +1,248 @@
+/*
+ * 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.huffman;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.ByteOrder;
+import java.util.Objects;
+
+import org.apache.commons.compress.compressors.CompressorException;
+import org.apache.commons.compress.utils.BitInputStream;
+
+/**
+ * Canonical Huffman decoder.
+ * <p>
+ * This class builds decoding tables from an array of code lengths (one entry
per symbol) and then decodes symbols from a {@link BitInputStream}. The code set
+ * is expected to be a <em>complete prefix code</em>; i.e., the code lengths
must satisfy Kraft's equality.
+ * </p>
+ *
+ * <h2>Usage</h2>
+ *
+ * <pre>{@code
+ * int[] codeLengths = ...; // length per symbol (0 => unused)
+ * int symbolCount = codeLengths.length;
+ * int maxLen = 15; // maximum non-zero code length in codeLengths
+ * HuffmanDecoder dec = new HuffmanDecoder(codeLengths, symbolCount, maxLen);
+ * int sym = dec.decodeSymbol(bitIn);
+ * }</pre>
+ *
+ * <h2>Thread-safety</h2> Instances are immutable after construction and may
be safely shared between threads.
+ *
+ * @since 1.29.0
+ */
+public final class HuffmanDecoder {
+
+ /**
+ * Maximum code length supported by this implementation.
+ */
+ private static final int MAX_SUPPORTED_CODE_LENGTH = 30;
+
+ /** Minimum non-zero code length */
+ private final int minLength;
+
+ /** Maximum non-zero code length */
+ private final int maxLength;
+
+ /**
+ * Symbols in canonical order (by length, then by symbol).
+ */
+ private final int[] sorted;
+
+ /**
+ * For each code length, the bias between code values and indices into the
sorted symbol table.
+ */
+ private final int[] bias;
+
+ /**
+ * For each code length, the largest left-justified code of that length.
+ */
+ private final int[] limit;
+
+ /**
+ * Constructs a decoder from canonical code lengths.
+ * <p>
+ * The {@code codeLengths} array provides, for each symbol index {@code i}
in {@code [0, codeLengthSize)}, the length (in bits) of that symbol's code. A
+ * value of {@code 0} marks an unused symbol. All non-zero lengths must be
{@code <= maxCodeLength}.
+ * </p>
+ *
+ * @param codeLengths code length per symbol; {@code 0} means the symbol
is not used; not {@code null}.
+ * @throws NullPointerException if {@code codeLengths} is {@code null}.
+ * @throws IllegalArgumentException if any code length is out of range [0,
30].
+ */
+ public HuffmanDecoder(final int[] codeLengths) {
+ this(codeLengths, codeLengths.length, 0, MAX_SUPPORTED_CODE_LENGTH);
+ }
+
+ /**
+ * Constructs a decoder from canonical code lengths.
+ * <p>
+ * The {@code codeLengths} array provides, for each symbol index {@code i}
in {@code [0, codeLengthSize)}, the length (in bits) of that symbol's code. A
+ * value of {@code 0} marks an unused symbol. All non-zero lengths must be
{@code <= maxCodeLength}.
+ * </p>
+ *
+ * @param codeLengths code length per symbol; {@code 0} means the
symbol is not used; not {@code null}.
+ * @param codeLengthSize number of symbols to read from {@code
codeLengths} (must be {@code > 0} and {@code <= codeLengths.length}).
+ * @param minCodeLength minimum allowed code length present in {@code
codeLengths}.
+ * @param maxCodeLength maximum allowed code length present in {@code
codeLengths}.
+ * @throws NullPointerException if {@code codeLengths} is {@code null}.
+ * @throws IllegalArgumentException if {@code codeLengthSize} is out of
range, if any code length is out of range or if {@code maxCodeLength} exceeds
the
+ * implementation limit (30).
+ */
+ public HuffmanDecoder(final int[] codeLengths, final int codeLengthSize,
final int minCodeLength, final int maxCodeLength) throws
IllegalArgumentException {
+ Objects.requireNonNull(codeLengths, "codeLengths");
+ if (maxCodeLength > MAX_SUPPORTED_CODE_LENGTH) {
+ throw new IllegalArgumentException(String.format("maxCodeLength
(%d) exceeds supported limit (%d)", maxCodeLength, MAX_SUPPORTED_CODE_LENGTH));
+ }
+ if (codeLengthSize <= 0) {
+ throw new IllegalArgumentException(String.format("codeLengthSize
must be > 0; was %d", codeLengthSize));
+ }
+ if (codeLengths.length < codeLengthSize) {
+ throw new IllegalArgumentException(String.format("codeLengthSize
(%d) exceeds codeLengths.length (%d)", codeLengthSize, codeLengths.length));
+ }
+ // Validate and find min/max lengths
+ int min = maxCodeLength;
+ int max = minCodeLength;
+ for (int i = 0; i < codeLengthSize; i++) {
+ final int len = codeLengths[i];
+ if (len < minCodeLength || len > maxCodeLength) {
+ throw new IllegalArgumentException(
+ String.format("Invalid code length at symbol %d: %d
(expected in [%d, %d])", i, len, minCodeLength, maxCodeLength));
+ }
+ if (len == 0) {
+ continue; // unused symbol
+ }
+ if (len < min) {
+ min = len;
+ }
+ if (len > max) {
+ max = len;
+ }
+ }
+ this.minLength = min;
+ this.maxLength = max;
+ // Allocate outputs; we reuse them as scratch inside fillCodeTable
+ this.bias = new int[max + 1];
+ this.limit = new int[max + 1];
+ this.sorted = new int[codeLengthSize];
+ // Arrays are zero-initialized; no additional temps needed.
+ fillCodeTable(codeLengths, minLength, max, codeLengthSize, bias,
limit, sorted);
+ }
+
+ /**
+ * Gets the minimum code length (in bits) for this code set.
+ *
+ * @return minimum code length (in bits).
+ */
+ public int getMinLength() {
+ return minLength;
+ }
+
+ /**
+ * Gets the maximum code length (in bits) for this code set.
+ *
+ * @return maximum code length (in bits).
+ */
+ public int getMaxLength() {
+ return maxLength;
+ }
+
+ /**
+ * Builds canonical decode tables.
+ */
+ private static void fillCodeTable(final int[] codeLengths, final int
minLen, final int maxLen, final int codeLengthSize, final int[] bias,
+ final int[] limit, final int[] sorted) {
+ // 1) Histogram of code lengths
+ final int[] count = new int[maxLen + 1];
+ for (int symbol = 0; symbol < codeLengthSize; symbol++) {
+ final int len = codeLengths[symbol];
+ if (len == 0) {
+ continue;
+ }
+ count[codeLengths[symbol]]++;
+ }
+ // 2) Generate starting offsets into sorted symbol table
+ // The offsets are biased by -1 to simplify code in the next step
+ final int[] offset = new int[maxLen + 1];
+ offset[0] = -1;
+ for (int len = 1; len <= maxLen; len++) {
+ offset[len] = offset[len - 1] + count[len - 1];
+ }
+ // 3) Build table of symbols sorted by length, then by symbol
+ // Adjust offsets to point to the last element of each length
+ for (int symbol = 0; symbol < codeLengthSize; symbol++) {
+ final int len = codeLengths[symbol];
+ if (len == 0) {
+ continue;
+ }
+ sorted[++offset[len]] = symbol;
+ }
+ // 4) Compute the largest left-justified code for each length
+ int firstCode = 0;
+ for (int len = minLen; len <= maxLen; len++) {
+ firstCode += count[len];
+ limit[len] = firstCode - 1;
+ firstCode <<= 1; // prepare for next length
+ }
+ // 5) Compute the bias for each length
+ for (int len = minLen; len <= maxLen; len++) {
+ bias[len] = limit[len] - offset[len];
+ }
+ }
+
+ /**
+ * Decodes one symbol from the input bitstream.
+ *
+ * @param in the source of bits (MSB-first) to read from.
+ * @return the decoded symbol index.
+ * @throws EOFException if the input ends in the middle of a Huffman code
word.
+ * @throws IOException if an I/O error occurs while reading from {@code
in}.
+ */
+ public int decodeSymbol(final BitInputStream in) throws IOException {
+ int len = minLength;
+ int code = readBitsFully(in, len);
+ while (len <= maxLength && code > limit[len]) {
+ final int b = readBit(in);
+ code = code << 1 | b;
+ len++;
+ }
+ if (len > maxLength) {
+ throw new CompressorException("Invalid Huffman code: " + code);
+ }
+ return sorted[code - bias[len]];
+ }
+
+ private static int readBit(final BitInputStream in) throws IOException {
+ final int bit = in.readBit();
+ if (bit < 0) {
+ throw new EOFException("Truncated Huffman bit stream");
+ }
+ return bit;
+ }
+
+ private static int readBitsFully(final BitInputStream in, final int
numBits) throws IOException {
+ final int code = (int) in.readBits(numBits);
+ if (code < 0) {
+ throw new EOFException("Truncated Huffman bit stream");
+ }
+ // Adjust for bit order
+ return in.getByteOrder() == ByteOrder.BIG_ENDIAN ? code :
Integer.reverse(code) >>> 32 - numBits;
+ }
+}
diff --git
a/src/main/java/org/apache/commons/compress/huffman/package-info.java
b/src/main/java/org/apache/commons/compress/huffman/package-info.java
new file mode 100644
index 000000000..3fc53a9c1
--- /dev/null
+++ b/src/main/java/org/apache/commons/compress/huffman/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+/**
+ * Huffman decoder package.
+ *
+ * @since 1.29.0
+ */
+package org.apache.commons.compress.huffman;
diff --git
a/src/test/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorInputStreamTest.java
b/src/test/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorInputStreamTest.java
index 0095cf5c3..edbc21d36 100644
---
a/src/test/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorInputStreamTest.java
+++
b/src/test/java/org/apache/commons/compress/compressors/bzip2/BZip2CompressorInputStreamTest.java
@@ -103,22 +103,6 @@ private BitInputStream prepareDecodingTables(final int
codeLength) {
return new BitInputStream(new
ByteArrayInputStream(stream.toByteArray()), ByteOrder.BIG_ENDIAN);
}
- @Test
- void testCreateHuffmanDecodingTablesWithLargeAlphaSize() {
- final Data data = new Data(1);
- // Use a codeLengths array with length equal to MAX_ALPHA_SIZE (258)
to test array bounds.
- final char[] codeLengths = new char[258];
- for (int i = 0; i < codeLengths.length; i++) {
- // Use all code lengths within valid range [1, 20]
- codeLengths[i] = (char) (i % MAX_CODE_LEN + 1);
- }
- data.temp_charArray2d[0] = codeLengths;
- assertDoesNotThrow(
- () ->
BZip2CompressorInputStream.createHuffmanDecodingTables(codeLengths.length, 1,
data),
- "createHuffmanDecodingTables should not throw for valid
codeLengths array of MAX_ALPHA_SIZE");
- assertEquals(data.minLens[0], 1, "Minimum code length should be 1");
- }
-
@Test
void testFinishClose() throws Exception {
// Create a big random piece of data
@@ -222,14 +206,12 @@ void testRecvDecodingTablesWithOutOfRangeCodeLength(final
int codeLength) throws
void testRecvDecodingTablesWithValidCodeLength(final int codeLength)
throws IOException {
try (BitInputStream tables = prepareDecodingTables(codeLength)) {
final Data data = new Data(1);
-
- assertDoesNotThrow(
- () ->
BZip2CompressorInputStream.recvDecodingTables(tables, data),
+ assertDoesNotThrow(() ->
BZip2CompressorInputStream.recvDecodingTables(tables, data),
"Should accept code length " + codeLength + " within [" +
MIN_CODE_LEN + ", " + MAX_CODE_LEN + "]");
-
// We encoded 2 Huffman groups; both minLens should equal the
encoded codeLength
- assertEquals(codeLength, data.minLens[0], "Group 0 min code length
mismatch");
- assertEquals(codeLength, data.minLens[1], "Group 1 min code length
mismatch");
+ assertEquals(2, data.huffmanDecodersCount, "Expected 2 Huffman
groups");
+ assertEquals(codeLength, data.huffmanDecoders[0].getMinLength(),
"Group 0 min code length mismatch");
+ assertEquals(codeLength, data.huffmanDecoders[1].getMinLength(),
"Group 1 min code length mismatch");
}
}
diff --git
a/src/test/java/org/apache/commons/compress/compressors/deflate64/Deflate64CompressorInputStreamTest.java
b/src/test/java/org/apache/commons/compress/compressors/deflate64/Deflate64CompressorInputStreamTest.java
index 33ae5c4d5..809dcdd63 100644
---
a/src/test/java/org/apache/commons/compress/compressors/deflate64/Deflate64CompressorInputStreamTest.java
+++
b/src/test/java/org/apache/commons/compress/compressors/deflate64/Deflate64CompressorInputStreamTest.java
@@ -173,7 +173,7 @@ void
testShouldThrowIOExceptionInsteadOfRuntimeExceptionCOMPRESS526() {
*/
@Test
void testShouldThrowIOExceptionInsteadOfRuntimeExceptionCOMPRESS527() {
- assertThrows(CompressorException.class,
+ assertThrows(EOFException.class,
() -> fuzzingTest(new int[] { 0x50, 0x4b, 0x03, 0x04, 0x14,
0x00, 0x00, 0x00, 0x09, 0x00, 0x84, 0xb6, 0xba, 0x46, 0x72, 0xb6, 0xfe, 0x77,
0x4a,
0x00, 0x00, 0x00, 0x6b, 0x00, 0x00, 0x00, 0x03, 0x00,
0x1c, 0x00, 0x62, 0x62, 0x62, 0x55, 0x54, 0x09, 0x00, 0x03, 0xe7, 0xce, 0x64,
0x55, 0xf3, 0xce, 0x64, 0x55, 0x75, 0x78, 0x0b, 0x00,
0x01, 0x04, 0x5c, 0xf9, 0x01, 0x00, 0x04, 0x88, 0x13, 0x00, 0x00, 0x1d, 0x8b,
diff --git
a/src/test/java/org/apache/commons/compress/compressors/deflate64/Deflate64DecoderTest.java
b/src/test/java/org/apache/commons/compress/compressors/deflate64/Deflate64DecoderTest.java
index 4b609a519..f171fecf5 100644
---
a/src/test/java/org/apache/commons/compress/compressors/deflate64/Deflate64DecoderTest.java
+++
b/src/test/java/org/apache/commons/compress/compressors/deflate64/Deflate64DecoderTest.java
@@ -20,6 +20,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.ByteArrayInputStream;
@@ -58,7 +59,8 @@ void
testDecodeDynamicHuffmanBlockRejectsInvalidDistanceCode() throws Exception
final int len = decoder.decode(result);
fail("Should have failed but returned " + len + " entries: " +
Arrays.toString(Arrays.copyOf(result, len)));
});
- assertEquals("Invalid Deflate64 distance code -1", e.getMessage());
+ final String message = e.getMessage();
+ assertTrue(message.startsWith("Invalid Huffman code"), message);
}
}
diff --git
a/src/test/java/org/apache/commons/compress/huffman/HuffmanDecoderTest.java
b/src/test/java/org/apache/commons/compress/huffman/HuffmanDecoderTest.java
new file mode 100644
index 000000000..898656bd3
--- /dev/null
+++ b/src/test/java/org/apache/commons/compress/huffman/HuffmanDecoderTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.huffman;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.apache.commons.compress.utils.BitInputStream;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class HuffmanDecoderTest {
+
+ @Test
+ void testCreateHuffmanDecodingTablesWithLargeAlphaSize() {
+ // Use a codeLengths array with length equal to MAX_ALPHA_SIZE (258)
to test array bounds.
+ final int[] codeLengths = new int[258];
+ for (int i = 0; i < codeLengths.length; i++) {
+ // Use all code lengths within valid range [1, 20]
+ codeLengths[i] = (char) (i % 20 + 1);
+ }
+ final HuffmanDecoder decoder = assertDoesNotThrow(() -> new
HuffmanDecoder(codeLengths, codeLengths.length, 1, 20),
+ "HuffmanDecoder constructor should not throw for valid
codeLengths array of MAX_ALPHA_SIZE");
+ assertEquals(decoder.getMinLength(), 1, "Minimum code length should be
1");
+ assertEquals(decoder.getMaxLength(), 20, "Maximum code length should
be 20");
+ }
+
+ static Stream<Arguments> testDecodeSymbols() {
+ // @formatter:off
+ return Stream.of(
+ // Simple case
+ // Symbol 0: 10
+ // Symbol 1: 11
+ // Symbol 3: 0
+ Arguments.of(
+ new int[] {2, 2, 0, 1},
+ new byte[] {(byte) 0b10_11_0_0_0_0},
+ Arrays.asList(0, 1, 3, 3, 3, 3),
+ ByteOrder.BIG_ENDIAN),
+ Arguments.of(
+ new int[] {2, 2, 0, 1},
+ new byte[] {(byte) 0b01_11_0_0_0_0},
+ Arrays.asList(3, 3, 3, 3, 1, 0),
+ ByteOrder.LITTLE_ENDIAN),
+ // Across byte boundary
+ // Symbol 0: 10
+ // Symbol 1: 11
+ // Symbol 2: 0
+ Arguments.of(
+ new int[] {2, 2, 1, 0},
+ new byte[] {(byte) 0b0_11_10_11_1, (byte)
0b0_11_10_11_0},
+ Arrays.asList(2, 1, 0, 1, 0, 1, 0, 1, 2),
+ ByteOrder.BIG_ENDIAN),
+ Arguments.of(
+ new int[] {2, 2, 1, 0},
+ new byte[] {(byte) 0b1_11_01_11_0, (byte)
0b0_11_01_11_0},
+ Arrays.asList(2, 1, 0, 1, 0, 1, 0, 1, 2),
+ ByteOrder.LITTLE_ENDIAN));
+ // @formatter:on
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void testDecodeSymbols(final int[] codeLengths, final byte[] inputData,
final List<Integer> expectedSymbols, final ByteOrder byteOrder) throws
IOException {
+ final HuffmanDecoder decoder = new HuffmanDecoder(codeLengths);
+ final Collection<Integer> actualSymbols = new ArrayList<>();
+ try (BitInputStream in = new BitInputStream(new
ByteArrayInputStream(inputData), byteOrder)) {
+ for (int i = 0; i < expectedSymbols.size(); i++) {
+ actualSymbols.add(decoder.decodeSymbol(in));
+ }
+ }
+ assertEquals(expectedSymbols, actualSymbols, "Decoded symbols do not
match expected symbols");
+ }
+}