Skip to content

Commit a15f7d1

Browse files
committed
feat: Implemented signed and unsigned big-endian var-length integers
1 parent 124e5cb commit a15f7d1

File tree

4 files changed

+192
-11
lines changed

4 files changed

+192
-11
lines changed

plc4j/spi/src/main/java/org/apache/plc4x/java/spi/generation/ReadBufferByteBased.java

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131

3232
public class ReadBufferByteBased implements ReadBuffer, BufferCommons {
3333

34+
public static final long LAST_SEVEN_BITS = (byte) 0x7F;
35+
public static final long SEVENTH_BIT = (byte) 0x40;
36+
public static final long EIGHTH_BIT = (byte) 0x80;
37+
3438
private final MyDefaultBitInput bi;
3539
private ByteOrder byteOrder;
3640
private final int totalBytes;
@@ -311,7 +315,7 @@ public long readUnsignedLong(String logicalName, int bitLength, WithReaderArgs..
311315
value += (long) (digit * Math.pow(10, i));
312316
}
313317
return value;
314-
case "VARUDINT":
318+
case "VARUDINT": {
315319
long result = 0;
316320
int shift = 0;
317321
for (int i = 0; i < 4; i++) {
@@ -330,6 +334,7 @@ public long readUnsignedLong(String logicalName, int bitLength, WithReaderArgs..
330334
}
331335
}
332336
return result;
337+
}
333338
case "default":
334339
if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
335340
final long longValue = bi.readLong(true, bitLength);
@@ -469,10 +474,38 @@ public long readLong(String logicalName, int bitLength, WithReaderArgs... reader
469474
throw new ParseException("long can only contain max 64 bits");
470475
}
471476
try {
472-
if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
473-
return Long.reverseBytes(bi.readLong(false, bitLength));
477+
String encoding = extractEncoding(readerArgs).orElse("default");
478+
switch (encoding) {
479+
case "VARDINT": {
480+
long result = 0;
481+
for (int i = 0; i < 4; i++) {
482+
short b = bi.readShort(true, 8);
483+
484+
// if this is the first byte, and it's negative (the 7th bit is true)
485+
// initialize the result with a value where all bits are 1
486+
if((i == 0) && ((b & SEVENTH_BIT) != 0)) {
487+
result = -1L;
488+
}
489+
490+
// Add the lower 7 bits of b, shifted appropriately.
491+
result = result << 7;
492+
result |= ((long) b & LAST_SEVEN_BITS);
493+
// If the most significant bit is 0, this is the last byte.
494+
if ((b & EIGHTH_BIT) == 0) {
495+
break;
496+
}
497+
}
498+
return result;
499+
}
500+
case "default":
501+
if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
502+
return Long.reverseBytes(bi.readLong(false, bitLength));
503+
}
504+
return bi.readLong(false, bitLength);
505+
506+
default:
507+
throw new ParseException("unsupported encoding '" + encoding + "'");
474508
}
475-
return bi.readLong(false, bitLength);
476509
} catch (IOException e) {
477510
throw new ParseException("Error reading signed long", e);
478511
}

plc4j/spi/src/main/java/org/apache/plc4x/java/spi/generation/WriteBufferByteBased.java

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -284,11 +284,17 @@ public void writeUnsignedLong(String logicalName, int bitLength, long value, Wit
284284
}
285285
break;
286286
}
287-
case "VARUDINT":
287+
// It seems that normally var-length unsigned integers would be encoded little-endian.
288+
// However, for S7CommPlus we have a big-endian variant. If we ever encounter a LE
289+
// Protocol, we will need to update this into BE and LE variants.
290+
// https://en.wikipedia.org/wiki/Variable-length_quantity
291+
case "VARUDINT": {
288292
// Check that the provided value fits in the allowed bit length.
289-
long maxValue = (1L << bitLength) - 1;
290-
if (value > maxValue) {
291-
throw new SerializationException("Provided value of " + value + " exceeds the max value of " + maxValue);
293+
if (value < 0) {
294+
throw new SerializationException("Provided value of " + value + " exceeds the min value of 0");
295+
}
296+
if (value > 0xFFFFFF7FL) {
297+
throw new SerializationException("Provided value of " + value + " exceeds the max value of " + 0xFFFFFF7FL);
292298
}
293299
// Determine the number of 7-bit groups (bytes) required.
294300
int numBytes = 0;
@@ -309,6 +315,7 @@ public void writeUnsignedLong(String logicalName, int bitLength, long value, Wit
309315
bo.writeByte(false, 8, (byte) b);
310316
}
311317
break;
318+
}
312319
case "default":
313320
if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
314321
value = Long.reverseBytes(value) >> 32;
@@ -413,10 +420,44 @@ public void writeLong(String logicalName, int bitLength, long value, WithWriterA
413420
throw new SerializationException("long can only contain max 64 bits");
414421
}
415422
try {
416-
if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
417-
value = Long.reverseBytes(value);
423+
String encoding = extractEncoding(writerArgs).orElse("default");
424+
switch (encoding) {
425+
// https://en.wikipedia.org/wiki/Variable-length_quantity
426+
// The first byte of a var-length signed integer contains only 6 bits (the last 6)
427+
// the seventh bit more or less defines the sign (1 = negative, 0 = positive)
428+
// If the number fits in 6 bits, the eighth bit is not set and we're done.
429+
// If not the first 6 bits are output, the eighth bit
430+
case "VARDINT": {
431+
// Find out how any bytes are needed to serialize the current value
432+
boolean positive = value >= 0;
433+
int numBytes = 1;
434+
long tmpValue = value;
435+
while (tmpValue >> 6 != (positive ? 0 : -1)) {
436+
numBytes++;
437+
tmpValue >>= 7;
438+
}
439+
440+
// Serialise the bytes
441+
for (int i = numBytes - 1; i >= 0; i--) {
442+
tmpValue = value >> (7 * i) & 0x7F;
443+
if (i > 0) {
444+
tmpValue |= 0x80;
445+
} else {
446+
tmpValue &= 0x7F;
447+
}
448+
bo.writeShort(false, 8, (short) tmpValue);
449+
}
450+
break;
451+
}
452+
case "default":
453+
if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
454+
value = Long.reverseBytes(value);
455+
}
456+
bo.writeLong(false, bitLength, value);
457+
break;
458+
default:
459+
throw new SerializationException("unsupported encoding '" + encoding + "'");
418460
}
419-
bo.writeLong(false, bitLength, value);
420461
} catch (Exception e) {
421462
throw new SerializationException("Error writing signed long", e);
422463
}

plc4j/spi/src/test/java/org/apache/plc4x/java/spi/generation/ReadBufferTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
package org.apache.plc4x.java.spi.generation;
2020

21+
import org.apache.commons.codec.binary.Hex;
2122
import org.apache.plc4x.java.spi.codegen.WithOption;
2223
import org.junit.jupiter.api.Test;
2324

@@ -51,4 +52,17 @@ void readStringUtf8() throws ParseException {
5152

5253
assertEquals(value, answer);
5354
}
55+
56+
void readVarUint() throws ParseException {
57+
58+
}
59+
60+
@Test
61+
void readVarInt() throws Exception {
62+
byte[] serialized = Hex.decodeHex("8064");
63+
final ReadBuffer buffer = new ReadBufferByteBased(serialized);
64+
long answer = buffer.readLong("", 32, WithOption.WithEncoding("VARDINT"));
65+
assertEquals(100L, answer);
66+
}
67+
5468
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.plc4x.java.spi.generation;
20+
21+
import org.apache.commons.codec.binary.Hex;
22+
import org.apache.plc4x.java.spi.codegen.WithOption;
23+
import org.junit.jupiter.params.ParameterizedTest;
24+
import org.junit.jupiter.params.provider.CsvSource;
25+
26+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
27+
import static org.junit.jupiter.api.Assertions.assertEquals;
28+
29+
public class VarIntTest {
30+
31+
@ParameterizedTest
32+
@CsvSource({
33+
"'00', 0, 'Single‑byte encoding for 0'",
34+
"'01', 1, 'Single‑byte encoding for 1'",
35+
"'7F', 127, 'Single‑byte maximum (all 7 data bits set)'",
36+
"'8100', 128, 'Two‑byte encoding for 128; groups: [1, 0] (first byte 0x81, second byte 0x00)'",
37+
"'8148', 200, 'Two‑byte encoding for 200; groups: [1, 72] (0x81, 0x48)'",
38+
"'8600', 768, 'Two‑byte encoding for 768; groups: [6, 0] (0x86, 0x00) – as you indicated'",
39+
"'CE10', 10000, 'Two‑byte encoding for 10000; groups: [78, 16] (first byte: 78→0xCE, second: 16→0x10)'",
40+
"'FF7F', 16383, 'Two‑byte maximum (groups: [127, 127]; first byte 0xFF, second byte 0x7F)'",
41+
"'818000', 16384, 'Three‑byte encoding; smallest number needing three bytes; groups: [1, 0, 0]'",
42+
"'FFFF7F', 2097151, 'Three‑byte maximum; groups: [127, 127, 127] (encoded as 0xFF, 0xFF, 0x7F)'",
43+
"'82B19640', 5000000, 'Four‑byte encoding for 5000000; groups: [2, 49, 22, 64] (0x82, 0xB1, 0x96, 0x40)'",
44+
"'FFFFFF7F', 268435455, 'Four‑byte maximum; groups: [127, 127, 127, 127] (encoded as 0xFF, 0xFF, 0xFF, 0x7F)'",
45+
})
46+
void writeVarUintRoundtrip(String hexString, long expectedValue, String description) throws Exception {
47+
byte[] serialized = Hex.decodeHex(hexString);
48+
49+
// Parse the given array into a value
50+
ReadBufferByteBased readBuffer = new ReadBufferByteBased(serialized);
51+
long value = readBuffer.readUnsignedLong("", 32, WithOption.WithEncoding("VARUDINT"));
52+
assertEquals(expectedValue, value);
53+
54+
// Serialize the given value into a byte array
55+
WriteBufferByteBased writeBuffer = new WriteBufferByteBased(serialized.length);
56+
writeBuffer.writeUnsignedLong("", 32, expectedValue, WithOption.WithEncoding("VARUDINT"));
57+
byte[] result = writeBuffer.getBytes();
58+
assertArrayEquals(serialized, result, description);
59+
}
60+
61+
@ParameterizedTest
62+
@CsvSource({
63+
"'00', 0, '0 encoded in one byte.'",
64+
"'01', 1, 'Minimal positive value.'",
65+
"'3F', 63, 'Maximum positive (0x3F = 63).'",
66+
"'40', -64, '0x40 = 64 becomes 64–128 = –64.'",
67+
"'7F', -1, '0x7F = 127 becomes 127–128 = –1.'",
68+
"'8040', 64, 'Groups: 1 and 0 → (1<<7) + 0 = 128.'",
69+
"'8148', 200, 'Groups: 1 and 0x48 (72) → (1<<7) + 72 = 128 + 72 = 200.'",
70+
"'FF1C', -100, 'Groups: 0xFF (127) and 0x1C (28) → (127<<7) + 28 = 16284; since 16284 ≥ 8192, subtract 16384: 16284–16384 = –100.'",
71+
"'819C20', 20000, 'Groups: 1, 28, 32 → (1<<14) + (28<<7) + 32 = 16384 + 3584 + 32 = 20000.'",
72+
"'FEE360', -20000, 'Groups: 0xFE (126), 0xE3 (99), 0x60 (96) → (126<<14) + (99<<7) + 96 = 2077152; 2077152 – 2097152 = –20000.'",
73+
"'82B19640', 5000000, 'Groups: 2, 49, 22, 64 → (2<<21) + (49<<14) + (22<<7) + 64 = 5000000.'",
74+
"'FDCEE940', -5000000, 'Groups: 0xFD (125), 0xCF (79), 0xA8 (40), 0x00 (0) → (125<<21) + (79<<14) + (40<<7) + 0 = 263435456; 263435456 – 268435456 = –5000000.'",
75+
"'BFFFFF7F', 134217727, 'Maximum positive value in 28 bits. (Groups: 63, 127, 127, 127.)'",
76+
"'C0808000', -134217728, 'Minimum negative value in 28 bits. (Groups: 64, 0, 0, 0 → 64<<21 = 134217728; then 134217728 – 268435456 = –134217728.)'",
77+
})
78+
void testVarIntRoundtrip(String hexString, long expectedValue, String description) throws Exception {
79+
byte[] serialized = Hex.decodeHex(hexString);
80+
81+
// Parse the given array into a value
82+
ReadBufferByteBased readBuffer = new ReadBufferByteBased(serialized);
83+
long value = readBuffer.readLong("", 32, WithOption.WithEncoding("VARDINT"));
84+
assertEquals(expectedValue, value);
85+
86+
// Serialize the given value into a byte array
87+
WriteBufferByteBased buffer = new WriteBufferByteBased(serialized.length);
88+
buffer.writeLong("", 32, expectedValue, WithOption.WithEncoding("VARDINT"));
89+
byte[] result = buffer.getBytes();
90+
assertArrayEquals(serialized, result, description);
91+
}
92+
93+
}

0 commit comments

Comments
 (0)