diff --git a/plc4j/spi/buffers/api/src/main/java/org/apache/plc4x/java/spi/buffers/api/AbstractBuffer.java b/plc4j/spi/buffers/api/src/main/java/org/apache/plc4x/java/spi/buffers/api/AbstractBuffer.java index 2abffcf7a5e..54e8ce5e42e 100644 --- a/plc4j/spi/buffers/api/src/main/java/org/apache/plc4x/java/spi/buffers/api/AbstractBuffer.java +++ b/plc4j/spi/buffers/api/src/main/java/org/apache/plc4x/java/spi/buffers/api/AbstractBuffer.java @@ -20,16 +20,21 @@ import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.HashMap; import java.util.Map; -import java.util.Stack; public abstract class AbstractBuffer implements Buffer { - protected final Stack context; + // Used as a stack (push/pop/peek). ArrayDeque instead of java.util.Stack: a buffer is a + // single-threaded, per-message scratch object (positionInBits and the backing array are + // themselves unsynchronized), so Stack's synchronization (it extends the synchronized Vector) + // guards nothing here while adding a monitor enter/exit to getContext() on every field. + protected final Deque context; public AbstractBuffer(WithOption... options) { - context = new Stack<>(); + context = new ArrayDeque<>(); context.push(options); } diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/AbstractBufferByteBased.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/AbstractBufferByteBased.java index 111e5855b42..9017ec0c3fe 100644 --- a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/AbstractBufferByteBased.java +++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/AbstractBufferByteBased.java @@ -65,7 +65,31 @@ protected ByteOrder getByteOrder(WithOption... options) { return byteOrder.get(); } } - return new ByteOrderBigEndian(); + return ByteOrderBigEndian.INSTANCE; + } + + // ---- Byte-aligned integer fast-path helpers (shared by Read/Write byte buffers) ---- + + /** + * Structural fast-path eligibility for the byte-aligned integer fast path: no per-field + * options, the cursor on an absolute byte boundary (see {@link #isAligned()}), and a + * whole number of bytes requested. The caller combines this with EXACT-CLASS checks + * ({@code getClass() == ...}, not {@code instanceof}) on the ALREADY RESOLVED encoding/byte + * order (plain binary / two's-complement, big-endian) so resolution happens exactly once per + * field, and a registered subclass with overridden codec behaviour falls through to the + * virtual-dispatch slow path instead of being silently bypassed by the fast path. + */ + protected boolean isByteAlignedWholeBytes(int numBits, WithOption[] options) { + // isAligned() tests the ABSOLUTE bit index (startBit + positionInBits) — the same predicate the + // readBits/writeBits whole-byte fast paths use — so a non-byte-aligned sub-buffer correctly + // falls through to the generic path (the aligned fast paths index by (startBit+positionInBits)/8). + return options.length == 0 && isAligned() && (numBits & 7) == 0; + } + + /** Big-endian two's-complement sign extension of the low {@code numBits} of {@code raw}. */ + protected static long signExtend(long raw, int numBits) { + int shift = 64 - numBits; + return (raw << shift) >> shift; } protected Optional getUnsignedIntegerEncoding(WithOption... options) { @@ -156,8 +180,16 @@ protected void ensureAvailable(int bitsNeeded) throws BufferException { } } + /** + * Whether the current cursor sits on a byte boundary of the BACKING ARRAY. This must be tested on + * the absolute bit index ({@code startBit + positionInBits}), not on {@code positionInBits} alone: + * a sub-buffer created at a non-byte-aligned offset (see {@code createSubBuffer}) has a non-zero + * {@code startBit} while its own {@code positionInBits} is 0. The whole-byte {@code arraycopy} + * fast paths in {@code readBits}/{@code writeBits} index the backing array by + * {@code (startBit + positionInBits) / 8}, so only absolute alignment makes that copy correct. + */ protected boolean isAligned() { - return (positionInBits % 8) == 0; + return ((startBit + positionInBits) % 8) == 0; } } diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferByteBased.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferByteBased.java index 380252533e6..bf4089ddcd5 100644 --- a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferByteBased.java +++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferByteBased.java @@ -21,11 +21,15 @@ import org.apache.plc4x.java.spi.buffers.api.ReadBuffer; import org.apache.plc4x.java.spi.buffers.api.WithOption; import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException; +import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrder; +import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderBigEndian; import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderManager; import org.apache.plc4x.java.spi.buffers.bytebased.encoding.Encoding; import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingDefault; import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingManager; import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingRaw; +import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingTwosComplement; +import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingUnsignedBinary; import java.math.BigDecimal; import java.math.BigInteger; @@ -91,6 +95,23 @@ public byte[] readBits(int numBits, WithOption... options) throws BufferExceptio } } + /** + * Reads {@code numBits} (a whole number of bytes) big-endian from the current byte-aligned + * position into a long and advances the position. Callers gate this behind the fast-path + * eligibility checks; no intermediate byte[] or per-field ByteOrder object is allocated. + */ + private long readAlignedBytesBE(int numBits) throws BufferException { + ensureAvailable(numBits); + int byteIndex = (startBit + positionInBits) / 8; + int numBytes = numBits / 8; + long v = 0; + for (int i = 0; i < numBytes; i++) { + v = (v << 8) | (buffer[byteIndex + i] & 0xFF); + } + positionInBits += numBits; + return v; + } + @Override public byte readUnsignedByte(int numBits, WithOption... options) throws BufferException { if (numBits < 1 || numBits > 7) { @@ -118,10 +139,17 @@ public short readUnsignedShort(int numBits, WithOption... options) throws Buffer Optional encodingOptional = getUnsignedIntegerEncoding(options); Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for unsigned integer values")); + ByteOrder byteOrder = getByteOrder(options); + if (isByteAlignedWholeBytes(numBits, options) + && encoding.getClass() == EncodingUnsignedBinary.class + && byteOrder.getClass() == ByteOrderBigEndian.class) { + return (short) readAlignedBytesBE(numBits); + } + if(encoding instanceof EncodingDefault encodingDefault) { ensureAvailable(numBits); byte[] bytes = readBits(numBits); - bytes = getByteOrder(options).process(bytes); + bytes = byteOrder.process(bytes); return encodingDefault.decodeShort(numBits, bytes); } else if(encoding instanceof EncodingRaw encodingRaw) { return encodingRaw.decodeShort(numBits, this); @@ -137,10 +165,19 @@ public int readUnsignedInt(int numBits, WithOption... options) throws BufferExce Optional encodingOptional = getUnsignedIntegerEncoding(options); Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for unsigned integer values")); + ByteOrder byteOrder = getByteOrder(options); + // Fast path: byte-aligned whole-byte plain-binary big-endian read straight from the backing + // array; encoding/byte order are resolved once above and reused by the slow path below. + if (isByteAlignedWholeBytes(numBits, options) + && encoding.getClass() == EncodingUnsignedBinary.class + && byteOrder.getClass() == ByteOrderBigEndian.class) { + return (int) readAlignedBytesBE(numBits); + } + if(encoding instanceof EncodingDefault encodingDefault) { ensureAvailable(numBits); byte[] bytes = readBits(numBits); - bytes = getByteOrder(options).process(bytes); + bytes = byteOrder.process(bytes); return encodingDefault.decodeInt(numBits, bytes); } else if(encoding instanceof EncodingRaw encodingRaw) { return encodingRaw.decodeInt(numBits, this); @@ -156,10 +193,17 @@ public long readUnsignedLong(int numBits, WithOption... options) throws BufferEx Optional encodingOptional = getUnsignedIntegerEncoding(options); Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for unsigned integer values")); + ByteOrder byteOrder = getByteOrder(options); + if (isByteAlignedWholeBytes(numBits, options) + && encoding.getClass() == EncodingUnsignedBinary.class + && byteOrder.getClass() == ByteOrderBigEndian.class) { + return readAlignedBytesBE(numBits); + } + if(encoding instanceof EncodingDefault encodingDefault) { ensureAvailable(numBits); byte[] bytes = readBits(numBits); - bytes = getByteOrder(options).process(bytes); + bytes = byteOrder.process(bytes); return encodingDefault.decodeLong(numBits, bytes); } else if(encoding instanceof EncodingRaw encodingRaw) { return encodingRaw.decodeLong(numBits, this); @@ -213,10 +257,17 @@ public short readSignedShort(int numBits, WithOption... options) throws BufferEx Optional encodingOptional = getSignedIntegerEncoding(options); Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for signed integer values")); + ByteOrder byteOrder = getByteOrder(options); + if (isByteAlignedWholeBytes(numBits, options) + && encoding.getClass() == EncodingTwosComplement.class + && byteOrder.getClass() == ByteOrderBigEndian.class) { + return (short) signExtend(readAlignedBytesBE(numBits), numBits); + } + if(encoding instanceof EncodingDefault encodingDefault) { ensureAvailable(numBits); byte[] bytes = readBits(numBits); - bytes = getByteOrder(options).process(bytes); + bytes = byteOrder.process(bytes); return encodingDefault.decodeShort(numBits, bytes); } else if(encoding instanceof EncodingRaw encodingRaw) { return encodingRaw.decodeShort(numBits, this); @@ -232,10 +283,17 @@ public int readSignedInt(int numBits, WithOption... options) throws BufferExcept Optional encodingOptional = getSignedIntegerEncoding(options); Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for signed integer values")); + ByteOrder byteOrder = getByteOrder(options); + if (isByteAlignedWholeBytes(numBits, options) + && encoding.getClass() == EncodingTwosComplement.class + && byteOrder.getClass() == ByteOrderBigEndian.class) { + return (int) signExtend(readAlignedBytesBE(numBits), numBits); + } + if(encoding instanceof EncodingDefault encodingDefault) { ensureAvailable(numBits); byte[] bytes = readBits(numBits); - bytes = getByteOrder(options).process(bytes); + bytes = byteOrder.process(bytes); return encodingDefault.decodeInt(numBits, bytes); } else if(encoding instanceof EncodingRaw encodingRaw) { return encodingRaw.decodeInt(numBits, this); @@ -251,10 +309,17 @@ public long readSignedLong(int numBits, WithOption... options) throws BufferExce Optional encodingOptional = getSignedIntegerEncoding(options); Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for signed integer values")); + ByteOrder byteOrder = getByteOrder(options); + if (isByteAlignedWholeBytes(numBits, options) + && encoding.getClass() == EncodingTwosComplement.class + && byteOrder.getClass() == ByteOrderBigEndian.class) { + return signExtend(readAlignedBytesBE(numBits), numBits); + } + if(encoding instanceof EncodingDefault encodingDefault) { ensureAvailable(numBits); byte[] bytes = readBits(numBits); - bytes = getByteOrder(options).process(bytes); + bytes = byteOrder.process(bytes); return encodingDefault.decodeLong(numBits, bytes); } else if(encoding instanceof EncodingRaw encodingRaw) { return encodingRaw.decodeLong(numBits, this); diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/WriteBufferByteBased.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/WriteBufferByteBased.java index be82f1d3b48..035fb42d66a 100644 --- a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/WriteBufferByteBased.java +++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/WriteBufferByteBased.java @@ -22,11 +22,14 @@ import org.apache.plc4x.java.spi.buffers.api.WriteBuffer; import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException; import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrder; +import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderBigEndian; import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderManager; import org.apache.plc4x.java.spi.buffers.bytebased.encoding.Encoding; import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingDefault; import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingManager; import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingRaw; +import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingTwosComplement; +import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingUnsignedBinary; import java.math.BigDecimal; import java.math.BigInteger; @@ -48,8 +51,12 @@ public WriteBufferByteBased(byte[] buffer, int startBit, int sizeInBits, WithOpt @Override public void writeBit(boolean value, WithOption... options) throws BufferException { ensureAvailable(1); - int byteIndex = positionInBits / 8; - int bitIndex = positionInBits % 8; + // Index by the ABSOLUTE bit position (startBit + positionInBits), like readBit and the + // whole-byte arraycopy path in writeBits — a buffer constructed with a non-zero startBit + // must not write from the start of the backing array. + int absoluteBitIndex = startBit + positionInBits; + int byteIndex = absoluteBitIndex / 8; + int bitIndex = absoluteBitIndex % 8; if (value) { buffer[byteIndex] |= (byte) (0x80 >> bitIndex); } @@ -82,6 +89,23 @@ public void writeBits(int numBits, byte[] value, WithOption... options) throws B } } + /** + * Writes the low {@code numBits} (a whole number of bytes) of {@code value} big-endian into the + * backing array at the current byte-aligned position and advances the position. Callers gate this + * behind the fast-path eligibility checks; no intermediate byte[] or ByteOrder object is allocated. + * Unlike {@code readAlignedBytesBE} this method performs NO capacity check of its own — every + * caller invokes {@code ensureAvailable(numBits)} before the fast-path branch, and that call must + * not be removed as "redundant" with the slow path's. + */ + private void writeAlignedBytesBE(int numBits, long value) { + int byteIndex = (startBit + positionInBits) / 8; + int numBytes = numBits / 8; + for (int i = 0; i < numBytes; i++) { + buffer[byteIndex + i] = (byte) ((value >>> ((numBytes - 1 - i) * 8)) & 0xFF); + } + positionInBits += numBits; + } + @Override public void writeUnsignedByte(int numBits, byte value, WithOption... options) throws BufferException { if (numBits < 1 || numBits > 7) { @@ -123,10 +147,18 @@ public void writeUnsignedShort(int numBits, short value, WithOption... options) Optional encodingOptional = getUnsignedIntegerEncoding(options); Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for unsigned integer values")); + ByteOrder byteOrder = getByteOrder(options); + if (isByteAlignedWholeBytes(numBits, options) + && encoding.getClass() == EncodingUnsignedBinary.class + && byteOrder.getClass() == ByteOrderBigEndian.class) { + writeAlignedBytesBE(numBits, value); + return; + } + if(encoding instanceof EncodingDefault encodingDefault) { ensureAvailable(numBits); byte[] bytes = encodingDefault.encodeShort(numBits, value); - bytes = getByteOrder(options).process(bytes); + bytes = byteOrder.process(bytes); writeBits(numBits, bytes); } else if(encoding instanceof EncodingRaw encodingRaw) { byte[] bytes = encodingRaw.encodeShort(numBits, value); @@ -150,10 +182,20 @@ public void writeUnsignedInt(int numBits, int value, WithOption... options) thro Optional encodingOptional = getUnsignedIntegerEncoding(options); Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for unsigned integer values")); + ByteOrder byteOrder = getByteOrder(options); + // Fast path: byte-aligned whole-byte plain-binary big-endian write straight into the backing + // array; encoding/byte order are resolved once above and reused by the slow path below. + if (isByteAlignedWholeBytes(numBits, options) + && encoding.getClass() == EncodingUnsignedBinary.class + && byteOrder.getClass() == ByteOrderBigEndian.class) { + writeAlignedBytesBE(numBits, value); + return; + } + if(encoding instanceof EncodingDefault encodingDefault) { ensureAvailable(numBits); byte[] bytes = encodingDefault.encodeInt(numBits, value); - bytes = getByteOrder(options).process(bytes); + bytes = byteOrder.process(bytes); writeBits(numBits, bytes); } else if(encoding instanceof EncodingRaw encodingRaw) { byte[] bytes = encodingRaw.encodeInt(numBits, value); @@ -177,10 +219,18 @@ public void writeUnsignedLong(int numBits, long value, WithOption... options) th Optional encodingOptional = getUnsignedIntegerEncoding(options); Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for unsigned integer values")); + ByteOrder byteOrder = getByteOrder(options); + if (isByteAlignedWholeBytes(numBits, options) + && encoding.getClass() == EncodingUnsignedBinary.class + && byteOrder.getClass() == ByteOrderBigEndian.class) { + writeAlignedBytesBE(numBits, value); + return; + } + if(encoding instanceof EncodingDefault encodingDefault) { ensureAvailable(numBits); byte[] bytes = encodingDefault.encodeLong(numBits, value); - bytes = getByteOrder(options).process(bytes); + bytes = byteOrder.process(bytes); writeBits(numBits, bytes); } else if(encoding instanceof EncodingRaw encodingRaw) { byte[] bytes = encodingRaw.encodeLong(numBits, value); @@ -259,10 +309,18 @@ public void writeSignedShort(int numBits, short value, WithOption... options) th Optional encodingOptional = getSignedIntegerEncoding(options); Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for signed integer values")); + ByteOrder byteOrder = getByteOrder(options); + if (isByteAlignedWholeBytes(numBits, options) + && encoding.getClass() == EncodingTwosComplement.class + && byteOrder.getClass() == ByteOrderBigEndian.class) { + writeAlignedBytesBE(numBits, value); + return; + } + if(encoding instanceof EncodingDefault encodingDefault) { ensureAvailable(numBits); byte[] bytes = encodingDefault.encodeShort(numBits, value); - bytes = getByteOrder(options).process(bytes); + bytes = byteOrder.process(bytes); writeBits(numBits, bytes); } else if(encoding instanceof EncodingRaw encodingRaw) { byte[] bytes = encodingRaw.encodeShort(numBits, value); @@ -287,10 +345,18 @@ public void writeSignedInt(int numBits, int value, WithOption... options) throws Optional encodingOptional = getSignedIntegerEncoding(options); Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for signed integer values")); + ByteOrder byteOrder = getByteOrder(options); + if (isByteAlignedWholeBytes(numBits, options) + && encoding.getClass() == EncodingTwosComplement.class + && byteOrder.getClass() == ByteOrderBigEndian.class) { + writeAlignedBytesBE(numBits, value); + return; + } + if(encoding instanceof EncodingDefault encodingDefault) { ensureAvailable(numBits); byte[] bytes = encodingDefault.encodeInt(numBits, value); - bytes = getByteOrder(options).process(bytes); + bytes = byteOrder.process(bytes); writeBits(numBits, bytes); } else if(encoding instanceof EncodingRaw encodingRaw) { byte[] bytes = encodingRaw.encodeInt(numBits, value); @@ -315,10 +381,18 @@ public void writeSignedLong(int numBits, long value, WithOption... options) thro Optional encodingOptional = getSignedIntegerEncoding(options); Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for signed integer values")); + ByteOrder byteOrder = getByteOrder(options); + if (isByteAlignedWholeBytes(numBits, options) + && encoding.getClass() == EncodingTwosComplement.class + && byteOrder.getClass() == ByteOrderBigEndian.class) { + writeAlignedBytesBE(numBits, value); + return; + } + if(encoding instanceof EncodingDefault encodingDefault) { ensureAvailable(numBits); byte[] bytes = encodingDefault.encodeLong(numBits, value); - bytes = getByteOrder(options).process(bytes); + bytes = byteOrder.process(bytes); writeBits(numBits, bytes); } else if(encoding instanceof EncodingRaw encodingRaw) { byte[] bytes = encodingRaw.encodeLong(numBits, value); diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrderBigEndian.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrderBigEndian.java index 462d367f584..6b97a98415e 100644 --- a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrderBigEndian.java +++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrderBigEndian.java @@ -27,6 +27,9 @@ public class ByteOrderBigEndian implements ByteOrder { private static final WithOption OPTION = WithByteBasedOption.WithByteOrder(NAME); + /** Stateless (process() is the identity), so one shared instance is safe to reuse. */ + public static final ByteOrderBigEndian INSTANCE = new ByteOrderBigEndian(); + public static WithOption optionByteOrderBigEndian() { return OPTION; } diff --git a/plc4j/spi/buffers/byte/src/test/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferByteBasedTest.java b/plc4j/spi/buffers/byte/src/test/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferByteBasedTest.java index f92a47d7ab5..07183c195d8 100644 --- a/plc4j/spi/buffers/byte/src/test/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferByteBasedTest.java +++ b/plc4j/spi/buffers/byte/src/test/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferByteBasedTest.java @@ -21,6 +21,7 @@ import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException; import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderBigEndian; import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderLittleEndian; +import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingBCD; import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingIEEE754; import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingTwosComplement; import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingUTF8; @@ -56,6 +57,52 @@ void testReadBitSimpleUnaligned() throws Exception { assertEquals(0, buffer.getRemainingBits()); } + // Guards the byte-aligned unsigned-int fast path: a BCD default encoding must NOT be treated as + // plain binary. 0x12 0x34 decodes to 1234 (BCD), not 0x1234 = 4660 (binary shift). + @Test + void byteAlignedReadHonoursBcdEncodingNotBinaryFastPath() throws Exception { + byte[] data = {0x12, 0x34}; + ReadBufferByteBased buffer = new ReadBufferByteBased(data, EncodingBCD.optionEncodingBCD()); + assertEquals(1234, buffer.readUnsignedInt(16)); + } + + // The two's-complement byte-aligned fast path must sign-extend: 0xFFFE -> -2, not 65534. + @Test + void byteAlignedSignedReadIsSignExtended() throws Exception { + ReadBufferByteBased b1 = new ReadBufferByteBased(new byte[]{(byte) 0xFF, (byte) 0xFE}, + EncodingTwosComplement.optionEncodingTwosComplement()); + assertEquals((short) -2, b1.readSignedShort(16)); + ReadBufferByteBased b2 = new ReadBufferByteBased(new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFE}, + EncodingTwosComplement.optionEncodingTwosComplement()); + assertEquals(-2, b2.readSignedInt(32)); + } + + // Positive counterpart to the BCD guard: the plain unsigned-binary byte-aligned fast path must + // return the same value the slow path would. 0x12 0x34 -> 0x1234, 0x12 0x34 0x56 0x78 -> 0x12345678. + @Test + void byteAlignedUnsignedReadUsesFastPath() throws Exception { + ReadBufferByteBased b16 = new ReadBufferByteBased(new byte[]{0x12, 0x34}, + EncodingUnsignedBinary.optionEncodingUnsignedBinary()); + assertEquals(0x1234, b16.readUnsignedInt(16)); + ReadBufferByteBased b32 = new ReadBufferByteBased(new byte[]{0x12, 0x34, 0x56, 0x78}, + EncodingUnsignedBinary.optionEncodingUnsignedBinary()); + assertEquals(0x12345678L, b32.readUnsignedLong(32)); + } + + // The fast path must key off the ABSOLUTE bit index (startBit + positionInBits), not + // positionInBits alone. A sub-buffer created at a non-byte-aligned offset has startBit % 8 != 0 + // while its own positionInBits is 0; reading a whole-byte int must still land on the right bits. + // bits 4..19 of A1 23 45 = 0001 0010 0011 0100 = 0x1234 (NOT bytes 0..1 = 0xA123). + @Test + void byteAlignedFastPathRespectsNonByteAlignedSubBufferStartBit() throws Exception { + ReadBufferByteBased buffer = new ReadBufferByteBased( + new byte[]{(byte) 0xA1, 0x23, 0x45, 0x60}, + EncodingUnsignedBinary.optionEncodingUnsignedBinary()); + buffer.readUnsignedInt(4); // advance to absolute bit 4 + ReadBufferByteBased sub = buffer.createSubBuffer(16); // startBit = 4 (non-byte-aligned) + assertEquals(0x1234, sub.readUnsignedInt(16)); + } + // readBits @Test void testReadBitsNestedSubBufferUnaligned() throws Exception { diff --git a/plc4j/spi/buffers/byte/src/test/java/org/apache/plc4x/java/spi/buffers/bytebased/WriteBufferByteBasedTest.java b/plc4j/spi/buffers/byte/src/test/java/org/apache/plc4x/java/spi/buffers/bytebased/WriteBufferByteBasedTest.java index da4743d7eb0..d95ad1ebb41 100644 --- a/plc4j/spi/buffers/byte/src/test/java/org/apache/plc4x/java/spi/buffers/bytebased/WriteBufferByteBasedTest.java +++ b/plc4j/spi/buffers/byte/src/test/java/org/apache/plc4x/java/spi/buffers/bytebased/WriteBufferByteBasedTest.java @@ -22,6 +22,7 @@ import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException; import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderBigEndian; import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderLittleEndian; +import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingBCD; import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingIEEE754; import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingTwosComplement; import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingUTF8; @@ -56,6 +57,47 @@ void testWriteBitSimpleUnaligned() throws Exception { assertEquals((byte) 0b10101100, result[0]); } + // Guards the byte-aligned unsigned-int fast path: a BCD default encoding must NOT be treated as + // plain binary. 1234 must be BCD-encoded to 0x12 0x34, not binary 0x04 0xD2. + @Test + void byteAlignedWriteHonoursBcdEncodingNotBinaryFastPath() throws Exception { + WriteBufferByteBased buffer = new WriteBufferByteBased(new byte[2], EncodingBCD.optionEncodingBCD()); + buffer.writeUnsignedInt(16, 1234); + assertArrayEquals(new byte[]{0x12, 0x34}, buffer.getBytes()); + } + + // The two's-complement byte-aligned fast path must emit two's complement: -2 -> 0xFF 0xFE. + @Test + void byteAlignedSignedWriteIsTwosComplement() throws Exception { + WriteBufferByteBased buffer = new WriteBufferByteBased(new byte[2], EncodingTwosComplement.optionEncodingTwosComplement()); + buffer.writeSignedShort(16, (short) -2); + assertArrayEquals(new byte[]{(byte) 0xFF, (byte) 0xFE}, buffer.getBytes()); + } + + // Positive counterpart to the BCD guard: the plain unsigned-binary byte-aligned fast path must + // emit big-endian bytes. 0x1234 -> 0x12 0x34, 0x12345678 -> 0x12 0x34 0x56 0x78. + @Test + void byteAlignedUnsignedWriteUsesFastPath() throws Exception { + WriteBufferByteBased b16 = new WriteBufferByteBased(new byte[2], EncodingUnsignedBinary.optionEncodingUnsignedBinary()); + b16.writeUnsignedInt(16, 0x1234); + assertArrayEquals(new byte[]{0x12, 0x34}, b16.getBytes()); + WriteBufferByteBased b32 = new WriteBufferByteBased(new byte[4], EncodingUnsignedBinary.optionEncodingUnsignedBinary()); + b32.writeUnsignedLong(32, 0x12345678L); + assertArrayEquals(new byte[]{0x12, 0x34, 0x56, 0x78}, b32.getBytes()); + } + + // Mirror of the read-side sub-buffer regression: writes must honour a non-byte-aligned startBit. + // With startBit = 4 the position is not byte-aligned, so the write goes bit-by-bit; writeBit must + // index the backing array by the ABSOLUTE bit position (startBit + positionInBits), like readBit. + // 0x1234 into bits 4..19 of a zeroed 3-byte array = 0x01 0x23 0x40. + @Test + void writeHonoursNonByteAlignedStartBit() throws Exception { + WriteBufferByteBased buffer = new WriteBufferByteBased(new byte[3], 4, 16, + EncodingUnsignedBinary.optionEncodingUnsignedBinary()); + buffer.writeUnsignedInt(16, 0x1234); + assertArrayEquals(new byte[]{0x01, 0x23, 0x40}, buffer.getBytes()); + } + // writeBits @Test void testWriteBitsNestedSubBufferUnaligned() throws Exception {