From f7a73caea55515ee9ae521dca442ece0bc783263 Mon Sep 17 00:00:00 2001 From: Yu Bao Date: Fri, 14 Aug 2026 10:34:18 +0800 Subject: [PATCH 1/2] Validate deserialized fields in MergingDigest.fromBytes() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #227: MergingDigest.fromBytes() reads compression, n, bufferSize, and lastUsedCell from untrusted input without validation, allowing crafted byte sequences to trigger NegativeArraySizeException or ArrayIndexOutOfBoundsException (denial of service). This commit adds comprehensive validation: - compression must be in (0, 1e9] to prevent integer overflow - n/bufferSize/lastUsedCell must be non-negative and cross-validated - centroid count must not exceed buffer remaining bytes - centroid count must not exceed digest array capacity - all weights must be positive - centroid means must be in non-decreasing order - total weight must be >= centroid count All invalid inputs now throw IllegalArgumentException with descriptive messages. Includes 11 new test cases covering all three original crash triggers plus semantic validation and round-trip correctness. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../tdunning/math/stats/MergingDigest.java | 74 +++++++- .../math/stats/MergingDigestTest.java | 168 ++++++++++++++++++ 2 files changed, 239 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/com/tdunning/math/stats/MergingDigest.java b/core/src/main/java/com/tdunning/math/stats/MergingDigest.java index 03748e2..5b9e056 100644 --- a/core/src/main/java/com/tdunning/math/stats/MergingDigest.java +++ b/core/src/main/java/com/tdunning/math/stats/MergingDigest.java @@ -895,6 +895,12 @@ public void asSmallBytes(ByteBuffer buf) { } } + /** + * Upper bound on compression parameter to prevent integer overflow when computing + * internal array sizes. The value 2 * ceil(compression) + sizeFudge must fit in a positive int. + */ + static final double MAX_COMPRESSION = 1_000_000_000.0; + @SuppressWarnings("WeakerAccess") public static MergingDigest fromBytes(ByteBuffer buf) { int encoding = buf.getInt(); @@ -903,15 +909,44 @@ public static MergingDigest fromBytes(ByteBuffer buf) { double max = buf.getDouble(); double compression = buf.getDouble(); int n = buf.getInt(); + if (compression <= 0 || compression > MAX_COMPRESSION) { + throw new IllegalArgumentException( + "Invalid compression: " + compression + " (must be in (0, " + MAX_COMPRESSION + "])"); + } + if (n < 0) { + throw new IllegalArgumentException("Invalid centroid count: " + n); + } + if (n > buf.remaining() / 16) { + throw new IllegalArgumentException( + "Centroid count " + n + " requires more data than available in buffer"); + } MergingDigest r = new MergingDigest(compression); + if (n > r.weight.length) { + throw new IllegalArgumentException( + "Centroid count " + n + " exceeds digest capacity " + r.weight.length); + } r.setMinMax(min, max); r.lastUsedCell = n; + double prevMean = Double.NEGATIVE_INFINITY; for (int i = 0; i < n; i++) { r.weight[i] = buf.getDouble(); r.mean[i] = buf.getDouble(); - + if (r.weight[i] <= 0) { + throw new IllegalArgumentException( + "Non-positive weight " + r.weight[i] + " at centroid " + i); + } + if (r.mean[i] < prevMean) { + throw new IllegalArgumentException( + "Centroids not in non-decreasing order at index " + i + + ": " + prevMean + " > " + r.mean[i]); + } + prevMean = r.mean[i]; r.totalWeight += r.weight[i]; } + if (r.totalWeight < r.lastUsedCell) { + throw new IllegalArgumentException( + "Total weight " + r.totalWeight + " is less than centroid count " + r.lastUsedCell); + } return r; } else if (encoding == Encoding.SMALL_ENCODING.code) { double min = buf.getDouble(); @@ -919,15 +954,48 @@ public static MergingDigest fromBytes(ByteBuffer buf) { double compression = buf.getFloat(); int n = buf.getShort(); int bufferSize = buf.getShort(); + if (compression <= 0 || compression > MAX_COMPRESSION) { + throw new IllegalArgumentException( + "Invalid compression: " + compression + " (must be in (0, " + MAX_COMPRESSION + "])"); + } + if (n <= 0) { + throw new IllegalArgumentException("Invalid main buffer size: " + n); + } + if (bufferSize <= 0) { + throw new IllegalArgumentException("Invalid buffer size: " + bufferSize); + } MergingDigest r = new MergingDigest(compression, bufferSize, n); r.setMinMax(min, max); - r.lastUsedCell = buf.getShort(); + int lastUsedCell = buf.getShort(); + if (lastUsedCell < 0 || lastUsedCell > n) { + throw new IllegalArgumentException( + "Invalid lastUsedCell " + lastUsedCell + " (must be in [0, " + n + "])"); + } + if (lastUsedCell > buf.remaining() / 8) { + throw new IllegalArgumentException( + "lastUsedCell " + lastUsedCell + " requires more data than available in buffer"); + } + r.lastUsedCell = lastUsedCell; + double prevMean = Double.NEGATIVE_INFINITY; for (int i = 0; i < r.lastUsedCell; i++) { r.weight[i] = buf.getFloat(); r.mean[i] = buf.getFloat(); - + if (r.weight[i] <= 0) { + throw new IllegalArgumentException( + "Non-positive weight " + r.weight[i] + " at centroid " + i); + } + if (r.mean[i] < prevMean) { + throw new IllegalArgumentException( + "Centroids not in non-decreasing order at index " + i + + ": " + prevMean + " > " + r.mean[i]); + } + prevMean = r.mean[i]; r.totalWeight += r.weight[i]; } + if (r.lastUsedCell > 0 && r.totalWeight < r.lastUsedCell) { + throw new IllegalArgumentException( + "Total weight " + r.totalWeight + " is less than centroid count " + r.lastUsedCell); + } return r; } else { throw new IllegalStateException("Invalid format for serialized histogram"); diff --git a/core/src/test/java/com/tdunning/math/stats/MergingDigestTest.java b/core/src/test/java/com/tdunning/math/stats/MergingDigestTest.java index 34d20a5..57468a2 100644 --- a/core/src/test/java/com/tdunning/math/stats/MergingDigestTest.java +++ b/core/src/test/java/com/tdunning/math/stats/MergingDigestTest.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.*; //to freeze the tests with a particular seed, put the seed on the next line @@ -234,4 +235,171 @@ public void testAdversarial() throws FileNotFoundException { } } } + + // ---- Validation tests for fromBytes (issue #227) ---- + + @Test(expected = IllegalArgumentException.class) + public void testFromBytesSmallEncodingNegativeN() { + ByteBuffer buf = ByteBuffer.allocate(64).order(ByteOrder.BIG_ENDIAN); + buf.putInt(2); // SMALL_ENCODING + buf.putDouble(0.0); // min + buf.putDouble(1.0); // max + buf.putFloat(100.0f); // compression + buf.putShort((short) -5); // n (negative) + buf.putShort((short) 10); // bufferSize + buf.putShort((short) 0); // lastUsedCell + buf.flip(); + MergingDigest.fromBytes(buf); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromBytesVerboseEncodingNExceedsCapacity() { + ByteBuffer buf = ByteBuffer.allocate(4 + 8 + 8 + 8 + 4 + 100 * 16).order(ByteOrder.BIG_ENDIAN); + buf.putInt(1); // VERBOSE_ENCODING + buf.putDouble(0.0); // min + buf.putDouble(1.0); // max + buf.putDouble(1.0); // compression -> small capacity + buf.putInt(100); // n = 100, way more than capacity + for (int i = 0; i < 100; i++) { + buf.putDouble(1.0); // weight + buf.putDouble(i); // mean (non-decreasing) + } + buf.flip(); + MergingDigest.fromBytes(buf); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromBytesVerboseEncodingOverflowCompression() { + ByteBuffer buf = ByteBuffer.allocate(32).order(ByteOrder.BIG_ENDIAN); + buf.putInt(1); // VERBOSE_ENCODING + buf.putDouble(0.0); // min + buf.putDouble(1.0); // max + buf.putDouble(1.0e10); // compression (exceeds MAX_COMPRESSION) + buf.putInt(0); // n + buf.flip(); + MergingDigest.fromBytes(buf); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromBytesVerboseEncodingNegativeCompression() { + ByteBuffer buf = ByteBuffer.allocate(32).order(ByteOrder.BIG_ENDIAN); + buf.putInt(1); // VERBOSE_ENCODING + buf.putDouble(0.0); + buf.putDouble(1.0); + buf.putDouble(-50.0); // negative compression + buf.putInt(0); + buf.flip(); + MergingDigest.fromBytes(buf); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromBytesVerboseEncodingNegativeWeight() { + ByteBuffer buf = ByteBuffer.allocate(4 + 8 + 8 + 8 + 4 + 16).order(ByteOrder.BIG_ENDIAN); + buf.putInt(1); // VERBOSE_ENCODING + buf.putDouble(0.0); + buf.putDouble(1.0); + buf.putDouble(100.0); // compression + buf.putInt(1); // n = 1 + buf.putDouble(-1.0); // negative weight + buf.putDouble(5.0); // mean + buf.flip(); + MergingDigest.fromBytes(buf); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromBytesVerboseEncodingDecreasingMeans() { + ByteBuffer buf = ByteBuffer.allocate(4 + 8 + 8 + 8 + 4 + 2 * 16).order(ByteOrder.BIG_ENDIAN); + buf.putInt(1); // VERBOSE_ENCODING + buf.putDouble(0.0); + buf.putDouble(10.0); + buf.putDouble(100.0); // compression + buf.putInt(2); // n = 2 + buf.putDouble(1.0); // weight[0] + buf.putDouble(5.0); // mean[0] = 5 + buf.putDouble(1.0); // weight[1] + buf.putDouble(3.0); // mean[1] = 3, out of order + buf.flip(); + MergingDigest.fromBytes(buf); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromBytesSmallEncodingLastUsedCellExceedsN() { + ByteBuffer buf = ByteBuffer.allocate(64).order(ByteOrder.BIG_ENDIAN); + buf.putInt(2); // SMALL_ENCODING + buf.putDouble(0.0); + buf.putDouble(1.0); + buf.putFloat(100.0f); // compression + buf.putShort((short) 10); // n + buf.putShort((short) 50); // bufferSize + buf.putShort((short) 20); // lastUsedCell > n + buf.flip(); + MergingDigest.fromBytes(buf); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromBytesSmallEncodingNegativeWeight() { + ByteBuffer buf = ByteBuffer.allocate(64).order(ByteOrder.BIG_ENDIAN); + buf.putInt(2); // SMALL_ENCODING + buf.putDouble(0.0); + buf.putDouble(1.0); + buf.putFloat(100.0f); // compression + buf.putShort((short) 220); // n + buf.putShort((short) 500); // bufferSize + buf.putShort((short) 1); // lastUsedCell + buf.putFloat(-1.0f); // negative weight + buf.putFloat(5.0f); // mean + buf.flip(); + MergingDigest.fromBytes(buf); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromBytesSmallEncodingDecreasingMeans() { + ByteBuffer buf = ByteBuffer.allocate(128).order(ByteOrder.BIG_ENDIAN); + buf.putInt(2); // SMALL_ENCODING + buf.putDouble(0.0); + buf.putDouble(10.0); + buf.putFloat(100.0f); // compression + buf.putShort((short) 220); // n + buf.putShort((short) 500); // bufferSize + buf.putShort((short) 2); // lastUsedCell + buf.putFloat(1.0f); // weight[0] + buf.putFloat(5.0f); // mean[0] = 5 + buf.putFloat(1.0f); // weight[1] + buf.putFloat(3.0f); // mean[1] = 3, out of order + buf.flip(); + MergingDigest.fromBytes(buf); + } + + @Test + public void testFromBytesRoundTrip() { + // Valid digest should still round-trip correctly + MergingDigest original = new MergingDigest(100); + Random gen = new Random(42); + for (int i = 0; i < 1000; i++) { + original.add(gen.nextGaussian()); + } + ByteBuffer buf = ByteBuffer.allocate(original.byteSize()); + original.asBytes(buf); + buf.flip(); + MergingDigest restored = MergingDigest.fromBytes(buf); + assertEquals(original.size(), restored.size(), 0); + assertEquals(original.getMin(), restored.getMin(), 0); + assertEquals(original.getMax(), restored.getMax(), 0); + } + + @Test + public void testFromBytesSmallRoundTrip() { + MergingDigest original = new MergingDigest(100); + Random gen = new Random(42); + for (int i = 0; i < 1000; i++) { + original.add(gen.nextGaussian()); + } + ByteBuffer buf = ByteBuffer.allocate(original.smallByteSize()); + original.asSmallBytes(buf); + buf.flip(); + MergingDigest restored = MergingDigest.fromBytes(buf); + assertEquals(original.size(), restored.size(), 1); // float precision + assertEquals(original.getMin(), restored.getMin(), 0); + assertEquals(original.getMax(), restored.getMax(), 0); + } } \ No newline at end of file From 9949aef76f970ba89e424f4a07175050dc0a01d6 Mon Sep 17 00:00:00 2001 From: Yu Bao Date: Fri, 14 Aug 2026 10:42:03 +0800 Subject: [PATCH 2/2] Fix NaN/Infinity bypass and OOM from excessive compression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs in the initial validation: 1. NaN/Infinity bypass: IEEE 754 comparisons like `weight <= 0` and `mean < prevMean` silently pass NaN (all comparisons with NaN return false). Fix: use negated form `!(w > 0)` which correctly catches NaN, plus explicit `Double.isInfinite()` / `Double.isNaN()` checks. Extract into `checkWeight()` and `checkMean()` helper methods with Javadoc explaining the IEEE 754 rationale. 2. OOM from MAX_COMPRESSION = 1e9: compression of 1e9 causes ~2 billion element arrays (~16GB), trivially triggering OutOfMemoryError from a 32-byte input. Lower MAX_COMPRESSION to 1e6 (~2M array entries, ~16MB), which is generous for any real use case. Add 7 new test cases: NaN mean, NaN weight, Infinity weight, Infinity mean, NaN compression (verbose encoding), NaN weight and NaN mean (small encoding). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../tdunning/math/stats/MergingDigest.java | 66 +++++++----- .../math/stats/MergingDigestTest.java | 102 +++++++++++++++++- 2 files changed, 142 insertions(+), 26 deletions(-) diff --git a/core/src/main/java/com/tdunning/math/stats/MergingDigest.java b/core/src/main/java/com/tdunning/math/stats/MergingDigest.java index 5b9e056..13bd461 100644 --- a/core/src/main/java/com/tdunning/math/stats/MergingDigest.java +++ b/core/src/main/java/com/tdunning/math/stats/MergingDigest.java @@ -896,10 +896,40 @@ public void asSmallBytes(ByteBuffer buf) { } /** - * Upper bound on compression parameter to prevent integer overflow when computing - * internal array sizes. The value 2 * ceil(compression) + sizeFudge must fit in a positive int. + * Upper bound on compression parameter to prevent excessive memory allocation. + * With compression = 1_000_000, the internal arrays are roughly 2M entries (~16MB), + * which is generous for any real use case. Values beyond this are almost certainly + * from corrupted or malicious input. */ - static final double MAX_COMPRESSION = 1_000_000_000.0; + static final double MAX_COMPRESSION = 1_000_000.0; + + /** + * Checks that a weight value is finite and positive. + * Uses negated form {@code !(w > 0)} so that NaN is correctly rejected + * (since NaN > 0 is false, the negation catches it). + */ + private static void checkWeight(double w, int index) { + if (!(w > 0) || Double.isInfinite(w)) { + throw new IllegalArgumentException( + "Invalid weight " + w + " at centroid " + index + " (must be finite and positive)"); + } + } + + /** + * Checks that a mean value is finite and in non-decreasing order. + * Uses negated form {@code !(m >= prevMean)} so that NaN is correctly rejected. + */ + private static void checkMean(double m, double prevMean, int index) { + if (Double.isNaN(m) || Double.isInfinite(m)) { + throw new IllegalArgumentException( + "Invalid mean " + m + " at centroid " + index + " (must be finite)"); + } + if (!(m >= prevMean)) { + throw new IllegalArgumentException( + "Centroids not in non-decreasing order at index " + index + + ": " + prevMean + " > " + m); + } + } @SuppressWarnings("WeakerAccess") public static MergingDigest fromBytes(ByteBuffer buf) { @@ -909,9 +939,9 @@ public static MergingDigest fromBytes(ByteBuffer buf) { double max = buf.getDouble(); double compression = buf.getDouble(); int n = buf.getInt(); - if (compression <= 0 || compression > MAX_COMPRESSION) { + if (!(compression > 0) || compression > MAX_COMPRESSION) { throw new IllegalArgumentException( - "Invalid compression: " + compression + " (must be in (0, " + MAX_COMPRESSION + "])"); + "Invalid compression: " + compression + " (must be finite and in (0, " + MAX_COMPRESSION + "])"); } if (n < 0) { throw new IllegalArgumentException("Invalid centroid count: " + n); @@ -931,15 +961,8 @@ public static MergingDigest fromBytes(ByteBuffer buf) { for (int i = 0; i < n; i++) { r.weight[i] = buf.getDouble(); r.mean[i] = buf.getDouble(); - if (r.weight[i] <= 0) { - throw new IllegalArgumentException( - "Non-positive weight " + r.weight[i] + " at centroid " + i); - } - if (r.mean[i] < prevMean) { - throw new IllegalArgumentException( - "Centroids not in non-decreasing order at index " + i + - ": " + prevMean + " > " + r.mean[i]); - } + checkWeight(r.weight[i], i); + checkMean(r.mean[i], prevMean, i); prevMean = r.mean[i]; r.totalWeight += r.weight[i]; } @@ -954,9 +977,9 @@ public static MergingDigest fromBytes(ByteBuffer buf) { double compression = buf.getFloat(); int n = buf.getShort(); int bufferSize = buf.getShort(); - if (compression <= 0 || compression > MAX_COMPRESSION) { + if (!(compression > 0) || compression > MAX_COMPRESSION) { throw new IllegalArgumentException( - "Invalid compression: " + compression + " (must be in (0, " + MAX_COMPRESSION + "])"); + "Invalid compression: " + compression + " (must be finite and in (0, " + MAX_COMPRESSION + "])"); } if (n <= 0) { throw new IllegalArgumentException("Invalid main buffer size: " + n); @@ -980,15 +1003,8 @@ public static MergingDigest fromBytes(ByteBuffer buf) { for (int i = 0; i < r.lastUsedCell; i++) { r.weight[i] = buf.getFloat(); r.mean[i] = buf.getFloat(); - if (r.weight[i] <= 0) { - throw new IllegalArgumentException( - "Non-positive weight " + r.weight[i] + " at centroid " + i); - } - if (r.mean[i] < prevMean) { - throw new IllegalArgumentException( - "Centroids not in non-decreasing order at index " + i + - ": " + prevMean + " > " + r.mean[i]); - } + checkWeight(r.weight[i], i); + checkMean(r.mean[i], prevMean, i); prevMean = r.mean[i]; r.totalWeight += r.weight[i]; } diff --git a/core/src/test/java/com/tdunning/math/stats/MergingDigestTest.java b/core/src/test/java/com/tdunning/math/stats/MergingDigestTest.java index 57468a2..da7d124 100644 --- a/core/src/test/java/com/tdunning/math/stats/MergingDigestTest.java +++ b/core/src/test/java/com/tdunning/math/stats/MergingDigestTest.java @@ -274,7 +274,7 @@ public void testFromBytesVerboseEncodingOverflowCompression() { buf.putInt(1); // VERBOSE_ENCODING buf.putDouble(0.0); // min buf.putDouble(1.0); // max - buf.putDouble(1.0e10); // compression (exceeds MAX_COMPRESSION) + buf.putDouble(1.0e10); // compression (exceeds MAX_COMPRESSION of 1e6) buf.putInt(0); // n buf.flip(); MergingDigest.fromBytes(buf); @@ -370,6 +370,106 @@ public void testFromBytesSmallEncodingDecreasingMeans() { MergingDigest.fromBytes(buf); } + @Test(expected = IllegalArgumentException.class) + public void testFromBytesVerboseEncodingNaNMean() { + ByteBuffer buf = ByteBuffer.allocate(4 + 8 + 8 + 8 + 4 + 16).order(ByteOrder.BIG_ENDIAN); + buf.putInt(1); // VERBOSE_ENCODING + buf.putDouble(0.0); + buf.putDouble(1.0); + buf.putDouble(100.0); // compression + buf.putInt(1); // n = 1 + buf.putDouble(1.0); // valid weight + buf.putDouble(Double.NaN); // NaN mean + buf.flip(); + MergingDigest.fromBytes(buf); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromBytesVerboseEncodingNaNWeight() { + ByteBuffer buf = ByteBuffer.allocate(4 + 8 + 8 + 8 + 4 + 16).order(ByteOrder.BIG_ENDIAN); + buf.putInt(1); // VERBOSE_ENCODING + buf.putDouble(0.0); + buf.putDouble(1.0); + buf.putDouble(100.0); // compression + buf.putInt(1); // n = 1 + buf.putDouble(Double.NaN); // NaN weight + buf.putDouble(5.0); // mean + buf.flip(); + MergingDigest.fromBytes(buf); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromBytesVerboseEncodingInfinityWeight() { + ByteBuffer buf = ByteBuffer.allocate(4 + 8 + 8 + 8 + 4 + 16).order(ByteOrder.BIG_ENDIAN); + buf.putInt(1); // VERBOSE_ENCODING + buf.putDouble(0.0); + buf.putDouble(1.0); + buf.putDouble(100.0); // compression + buf.putInt(1); // n = 1 + buf.putDouble(Double.POSITIVE_INFINITY); // Infinity weight + buf.putDouble(5.0); // mean + buf.flip(); + MergingDigest.fromBytes(buf); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromBytesVerboseEncodingInfinityMean() { + ByteBuffer buf = ByteBuffer.allocate(4 + 8 + 8 + 8 + 4 + 16).order(ByteOrder.BIG_ENDIAN); + buf.putInt(1); // VERBOSE_ENCODING + buf.putDouble(0.0); + buf.putDouble(1.0); + buf.putDouble(100.0); // compression + buf.putInt(1); // n = 1 + buf.putDouble(1.0); // valid weight + buf.putDouble(Double.POSITIVE_INFINITY); // Infinity mean + buf.flip(); + MergingDigest.fromBytes(buf); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromBytesVerboseEncodingNaNCompression() { + ByteBuffer buf = ByteBuffer.allocate(32).order(ByteOrder.BIG_ENDIAN); + buf.putInt(1); // VERBOSE_ENCODING + buf.putDouble(0.0); + buf.putDouble(1.0); + buf.putDouble(Double.NaN); // NaN compression + buf.putInt(0); + buf.flip(); + MergingDigest.fromBytes(buf); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromBytesSmallEncodingNaNWeight() { + ByteBuffer buf = ByteBuffer.allocate(64).order(ByteOrder.BIG_ENDIAN); + buf.putInt(2); // SMALL_ENCODING + buf.putDouble(0.0); + buf.putDouble(1.0); + buf.putFloat(100.0f); // compression + buf.putShort((short) 220); // n + buf.putShort((short) 500); // bufferSize + buf.putShort((short) 1); // lastUsedCell + buf.putFloat(Float.NaN); // NaN weight + buf.putFloat(5.0f); // mean + buf.flip(); + MergingDigest.fromBytes(buf); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromBytesSmallEncodingNaNMean() { + ByteBuffer buf = ByteBuffer.allocate(64).order(ByteOrder.BIG_ENDIAN); + buf.putInt(2); // SMALL_ENCODING + buf.putDouble(0.0); + buf.putDouble(1.0); + buf.putFloat(100.0f); // compression + buf.putShort((short) 220); // n + buf.putShort((short) 500); // bufferSize + buf.putShort((short) 1); // lastUsedCell + buf.putFloat(1.0f); // valid weight + buf.putFloat(Float.NaN); // NaN mean + buf.flip(); + MergingDigest.fromBytes(buf); + } + @Test public void testFromBytesRoundTrip() { // Valid digest should still round-trip correctly