diff --git a/avro/src/main/java/tools/jackson/dataformat/avro/deser/JacksonAvroParserImpl.java b/avro/src/main/java/tools/jackson/dataformat/avro/deser/JacksonAvroParserImpl.java index 5f52d5dac..afcf17819 100644 --- a/avro/src/main/java/tools/jackson/dataformat/avro/deser/JacksonAvroParserImpl.java +++ b/avro/src/main/java/tools/jackson/dataformat/avro/deser/JacksonAvroParserImpl.java @@ -64,7 +64,6 @@ private static boolean _checkVarHandleAvailable() { } } - /* /********************************************************************** /* Input source config diff --git a/avro/src/test/java/module-info.java b/avro/src/test/java/module-info.java index f87d05414..f52d241bb 100644 --- a/avro/src/test/java/module-info.java +++ b/avro/src/test/java/module-info.java @@ -19,6 +19,7 @@ opens tools.jackson.dataformat.avro; opens tools.jackson.dataformat.avro.annotation; opens tools.jackson.dataformat.avro.constraints; + opens tools.jackson.dataformat.avro.deser; opens tools.jackson.dataformat.avro.dos; opens tools.jackson.dataformat.avro.fuzz; opens tools.jackson.dataformat.avro.gen; diff --git a/avro/src/test/java/tools/jackson/dataformat/avro/AvroNumberTest.java b/avro/src/test/java/tools/jackson/dataformat/avro/AvroNumberTest.java index 6901d03f7..5fa329ae2 100644 --- a/avro/src/test/java/tools/jackson/dataformat/avro/AvroNumberTest.java +++ b/avro/src/test/java/tools/jackson/dataformat/avro/AvroNumberTest.java @@ -138,4 +138,60 @@ private void _verifyGetNumberTypeFail(JsonParser p, String token) throws Excepti // In 2.x got exception; in 3.x null assertNull(p.getNumberType()); } + + // Characterization of `decodeFloat()`/`decodeDouble()` (see `JacksonAvroParserImpl`) + // across IEEE-754 boundary values, both from a plain byte array (single read) + // and from a `LimitingInputStream` (forces the buffer-refill/`_loadToHaveAtLeast` + // path to be exercised for every 4/8-byte read). + private static final float[] FLOAT_EDGE_CASES = new float[] { + 0.0f, -0.0f, 1.0f, -1.0f, + Float.MIN_VALUE, Float.MAX_VALUE, Float.MIN_NORMAL, + Float.NaN, Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY, + 123.456f, -123.456f + }; + + private static final double[] DOUBLE_EDGE_CASES = new double[] { + 0.0, -0.0, 1.0, -1.0, + Double.MIN_VALUE, Double.MAX_VALUE, Double.MIN_NORMAL, + Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, + 123456.789, -123456.789 + }; + + @Test + public void testFloatEdgeCasesRoundTrip() throws Exception + { + AvroSchema schema = MAPPER.schemaFor(Numbers.class); + for (float f : FLOAT_EDGE_CASES) { + Numbers input = new Numbers(0, 0L, f, 0.0); + byte[] bytes = MAPPER.writer(schema).writeValueAsBytes(input); + + Numbers direct = MAPPER.readerFor(Numbers.class).with(schema).readValue(bytes); + assertEquals(Float.floatToRawIntBits(f), Float.floatToRawIntBits(direct.f), + "direct read of " + f); + + Numbers chunked = MAPPER.readerFor(Numbers.class).with(schema) + .readValue(LimitingInputStream.wrap(bytes, 42)); + assertEquals(Float.floatToRawIntBits(f), Float.floatToRawIntBits(chunked.f), + "chunked read of " + f); + } + } + + @Test + public void testDoubleEdgeCasesRoundTrip() throws Exception + { + AvroSchema schema = MAPPER.schemaFor(Numbers.class); + for (double d : DOUBLE_EDGE_CASES) { + Numbers input = new Numbers(0, 0L, 0.0f, d); + byte[] bytes = MAPPER.writer(schema).writeValueAsBytes(input); + + Numbers direct = MAPPER.readerFor(Numbers.class).with(schema).readValue(bytes); + assertEquals(Double.doubleToRawLongBits(d), Double.doubleToRawLongBits(direct.d), + "direct read of " + d); + + Numbers chunked = MAPPER.readerFor(Numbers.class).with(schema) + .readValue(LimitingInputStream.wrap(bytes, 42)); + assertEquals(Double.doubleToRawLongBits(d), Double.doubleToRawLongBits(chunked.d), + "chunked read of " + d); + } + } } diff --git a/avro/src/test/java/tools/jackson/dataformat/avro/deser/AvroVarHandleUtilTest.java b/avro/src/test/java/tools/jackson/dataformat/avro/deser/AvroVarHandleUtilTest.java new file mode 100644 index 000000000..151e763ac --- /dev/null +++ b/avro/src/test/java/tools/jackson/dataformat/avro/deser/AvroVarHandleUtilTest.java @@ -0,0 +1,82 @@ +package tools.jackson.dataformat.avro.deser; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Direct characterization of {@link AvroVarHandleUtil}'s little-endian + * byte-array reads, independent of the full Avro parser round trip. + */ +public class AvroVarHandleUtilTest +{ + // On any JDK 17+ runtime (this project's baseline) VarHandle byte-array + // views are always available. + @Test + public void testIsAvailable() { + assertTrue(AvroVarHandleUtil.isAvailable()); + } + + @Test + public void testGetIntLEZero() { + byte[] buf = new byte[] { 0, 0, 0, 0 }; + assertEquals(0, AvroVarHandleUtil.getIntLE(buf, 0)); + } + + @Test + public void testGetIntLEPositive() { + // little-endian encoding of 0x04030201 + byte[] buf = new byte[] { 0x01, 0x02, 0x03, 0x04 }; + assertEquals(0x04030201, AvroVarHandleUtil.getIntLE(buf, 0)); + } + + @Test + public void testGetIntLENegative() { + // little-endian encoding of 0xFFFFFFFF (-1) and 0x80000000 (Integer.MIN_VALUE) + byte[] allOnes = new byte[] { (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF }; + assertEquals(-1, AvroVarHandleUtil.getIntLE(allOnes, 0)); + + byte[] minValue = new byte[] { 0x00, 0x00, 0x00, (byte) 0x80 }; + assertEquals(Integer.MIN_VALUE, AvroVarHandleUtil.getIntLE(minValue, 0)); + } + + @Test + public void testGetIntLEAtOffset() { + // leading padding byte followed by little-endian 0x04030201 + byte[] buf = new byte[] { (byte) 0xAA, 0x01, 0x02, 0x03, 0x04 }; + assertEquals(0x04030201, AvroVarHandleUtil.getIntLE(buf, 1)); + } + + @Test + public void testGetLongLEZero() { + byte[] buf = new byte[8]; + assertEquals(0L, AvroVarHandleUtil.getLongLE(buf, 0)); + } + + @Test + public void testGetLongLEPositive() { + // little-endian encoding of 0x0807060504030201L + byte[] buf = new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }; + assertEquals(0x0807060504030201L, AvroVarHandleUtil.getLongLE(buf, 0)); + } + + @Test + public void testGetLongLENegative() { + byte[] allOnes = new byte[] { + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF }; + assertEquals(-1L, AvroVarHandleUtil.getLongLE(allOnes, 0)); + + byte[] minValue = new byte[] { 0, 0, 0, 0, 0, 0, 0, (byte) 0x80 }; + assertEquals(Long.MIN_VALUE, AvroVarHandleUtil.getLongLE(minValue, 0)); + } + + @Test + public void testGetLongLEAtOffset() { + byte[] buf = new byte[] { + (byte) 0xAA, (byte) 0xBB, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }; + assertEquals(0x0807060504030201L, AvroVarHandleUtil.getLongLE(buf, 2)); + } +}