diff --git a/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/BitArrayDeclarationParserTest.java b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/BitArrayDeclarationParserTest.java new file mode 100644 index 0000000000..66ba7a508c --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/BitArrayDeclarationParserTest.java @@ -0,0 +1,343 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.ctf.core.tests.types; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import org.eclipse.tracecompass.ctf.core.CTFException; +import org.eclipse.tracecompass.ctf.core.event.io.BitBuffer; +import org.eclipse.tracecompass.ctf.core.event.types.BitArrayDeclaration; +import org.eclipse.tracecompass.ctf.core.trace.CTFTrace; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.CTFJsonMetadataNode; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.JsonStructureFieldMemberMetadataNode; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.ParseException; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.bitarray.BitArrayDeclarationParser; +import org.eclipse.tracecompass.internal.ctf.core.utils.JsonMetadataStrings; +import org.junit.Test; + +import com.google.gson.JsonObject; + +/** + * Test class for the fixed-length bit array declaration parser in CTF2, + * following the specification: https://diamon.org/ctf/#fl-ba-fc + * + * @author Arnaud Fiorini + */ +public class BitArrayDeclarationParserTest { + + // ------------------------------------------------------------------------ + // Helpers + // ------------------------------------------------------------------------ + + private static BitArrayDeclaration parse(JsonObject fieldClass) throws ParseException { + return BitArrayDeclarationParser.INSTANCE.parse(createNode(fieldClass), new BitArrayDeclarationParser.Param(new CTFTrace())); + } + + private static JsonStructureFieldMemberMetadataNode createNode(JsonObject fieldClass) { + return new JsonStructureFieldMemberMetadataNode(null, JsonMetadataStrings.FIXED_LENGTH_BIT_ARRAY, "test", "bits", fieldClass); + } + + /** + * Create a fixed-length bit array field class, the null properties are + * omitted + */ + private static JsonObject bitArray(Integer length, String byteOrder) { + JsonObject bitArray = new JsonObject(); + bitArray.addProperty("type", "fixed-length-bit-array"); + if (length != null) { + bitArray.addProperty("length", length); + } + if (byteOrder != null) { + bitArray.addProperty("byte-order", byteOrder); + } + return bitArray; + } + + // ------------------------------------------------------------------------ + // Tests + // ------------------------------------------------------------------------ + + /** + * Test parsing a fixed-length little-endian bit array field class with + * only the required properties + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testLittleEndian() throws ParseException { + BitArrayDeclaration result = parse(bitArray(16, "little-endian")); + + assertNotNull(result); + assertEquals(16, result.getLength()); + assertEquals(ByteOrder.LITTLE_ENDIAN, result.getByteOrder()); + /* The default alignment is 1 */ + assertEquals(1, result.getAlignment()); + } + + /** + * Test parsing a fixed-length big-endian bit array field class + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testBigEndian() throws ParseException { + BitArrayDeclaration result = parse(bitArray(5, "big-endian")); + + assertNotNull(result); + assertEquals(5, result.getLength()); + assertEquals(ByteOrder.BIG_ENDIAN, result.getByteOrder()); + assertEquals(1, result.getAlignment()); + } + + /** + * Test parsing a fixed-length bit array field class with an alignment + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testAlignment() throws ParseException { + JsonObject fieldClass = bitArray(32, "little-endian"); + fieldClass.addProperty("alignment", 32); + BitArrayDeclaration result = parse(fieldClass); + + assertEquals(32, result.getAlignment()); + } + + /** + * Test parsing a fixed-length bit array field class of one bit, the + * smallest valid length + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testOneBit() throws ParseException { + assertEquals(1, parse(bitArray(1, "little-endian")).getLength()); + } + + /** + * Test that the optional bit-order property is accepted with both of its + * valid values + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testBitOrder() throws ParseException { + JsonObject fieldClass = bitArray(8, "little-endian"); + fieldClass.addProperty("bit-order", "first-to-last"); + assertEquals(8, parse(fieldClass).getLength()); + + fieldClass = bitArray(8, "big-endian"); + fieldClass.addProperty("bit-order", "last-to-first"); + assertEquals(8, parse(fieldClass).getLength()); + } + + /** + * Parse a 4-bit field class with the given byte order and bit order, and + * decode one byte with it + */ + private static byte[] decodeNibble(String byteOrder, String bitOrder, byte data) throws ParseException, CTFException { + JsonObject fieldClass = bitArray(4, byteOrder); + if (bitOrder != null) { + fieldClass.addProperty("bit-order", bitOrder); + } + BitArrayDeclaration declaration = parse(fieldClass); + return declaration.createDefinition(null, "bits", new BitBuffer(ByteBuffer.wrap(new byte[] { data }))).getValue(); + } + + /** + * Test the bit order with a little-endian byte order. The bits are decoded + * from the least significant bit of the byte, so the first decoded bit of + * 0x01 is 1 followed by three 0. + * + * @throws ParseException + * if parsing fails + * @throws CTFException + * if decoding fails + */ + @Test + public void testBitOrderLittleEndian() throws ParseException, CTFException { + /* Default is first-to-last: the first decoded bit is element 0 */ + assertArrayEquals(new byte[] { 0x01 }, decodeNibble("little-endian", null, (byte) 0x01)); + assertArrayEquals(new byte[] { 0x01 }, decodeNibble("little-endian", "first-to-last", (byte) 0x01)); + /* The first decoded bit is the last element */ + assertArrayEquals(new byte[] { 0x08 }, decodeNibble("little-endian", "last-to-first", (byte) 0x01)); + } + + /** + * Test the bit order with a big-endian byte order. The bits are decoded + * from the most significant bit of the byte, so the first decoded bit of + * 0x80 is 1 followed by three 0. + * + * @throws ParseException + * if parsing fails + * @throws CTFException + * if decoding fails + */ + @Test + public void testBitOrderBigEndian() throws ParseException, CTFException { + /* Default is last-to-first: the first decoded bit is the last element */ + assertArrayEquals(new byte[] { 0x08 }, decodeNibble("big-endian", null, (byte) 0x80)); + assertArrayEquals(new byte[] { 0x08 }, decodeNibble("big-endian", "last-to-first", (byte) 0x80)); + /* The first decoded bit is element 0 */ + assertArrayEquals(new byte[] { 0x01 }, decodeNibble("big-endian", "first-to-last", (byte) 0x80)); + } + + /** + * Test that the optional attributes and extensions properties are accepted + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testAttributesAndExtensions() throws ParseException { + JsonObject attributes = new JsonObject(); + JsonObject namespace = new JsonObject(); + namespace.addProperty("info", "bits"); + attributes.add("my.tracer", namespace); + + JsonObject fieldClass = bitArray(8, "little-endian"); + fieldClass.add("attributes", attributes); + fieldClass.add("extensions", new JsonObject()); + + BitArrayDeclaration result = parse(fieldClass); + assertEquals(8, result.getLength()); + assertEquals(ByteOrder.LITTLE_ENDIAN, result.getByteOrder()); + } + + /** + * Test that the length property is required + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testMissingLength() throws ParseException { + parse(bitArray(null, "little-endian")); + } + + /** + * Test that the length must be greater than zero + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testZeroLength() throws ParseException { + parse(bitArray(0, "little-endian")); + } + + /** + * Test that a negative length is rejected + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testNegativeLength() throws ParseException { + parse(bitArray(-1, "little-endian")); + } + + /** + * Test that the byte-order property is required + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testMissingByteOrder() throws ParseException { + parse(bitArray(8, null)); + } + + /** + * Test that an invalid byte order is rejected + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testInvalidByteOrder() throws ParseException { + parse(bitArray(8, "middle-endian")); + } + + /** + * Test that the alignment must be a power of two + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testNonPowerOfTwoAlignment() throws ParseException { + JsonObject fieldClass = bitArray(8, "little-endian"); + fieldClass.addProperty("alignment", 3); + parse(fieldClass); + } + + /** + * Test that the alignment must be positive + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testZeroAlignment() throws ParseException { + JsonObject fieldClass = bitArray(8, "little-endian"); + fieldClass.addProperty("alignment", 0); + parse(fieldClass); + } + + /** + * Test that the bit-order property must be "first-to-last" or + * "last-to-first" + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testInvalidBitOrder() throws ParseException { + JsonObject fieldClass = bitArray(8, "little-endian"); + fieldClass.addProperty("bit-order", "middle-out"); + parse(fieldClass); + } + + /** + * Test that the fixed-length bit array field class is not supported in + * CTF 1.8 + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testCTF1NotSupported() throws ParseException { + BitArrayDeclarationParser.INSTANCE.parse(new CTFJsonMetadataNode(null, "bit-array", ""), + new BitArrayDeclarationParser.Param(new CTFTrace())); + } + + /** + * Test that the parser requires its own parameter type + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = IllegalArgumentException.class) + public void testInvalidParam() throws ParseException { + BitArrayDeclarationParser.INSTANCE.parse(createNode(bitArray(8, "little-endian")), null); + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/BitArrayDeclarationTest.java b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/BitArrayDeclarationTest.java new file mode 100644 index 0000000000..c4ac749198 --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/BitArrayDeclarationTest.java @@ -0,0 +1,106 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.ctf.core.tests.types; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.nio.ByteOrder; +import java.util.List; +import java.util.Map; + +import org.eclipse.tracecompass.ctf.core.event.types.BitArrayDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.BitMapDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.BitOrder; +import org.eclipse.tracecompass.ctf.core.event.types.BooleanDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.IntegerRange; +import org.junit.Test; + +/** + * Test for the fixed-length bit array declaration class in CTF2, following the + * specification: https://diamon.org/ctf/#fl-ba-fc + * + * @author Arnaud Fiorini + */ +@SuppressWarnings("javadoc") +public class BitArrayDeclarationTest { + + @Test + public void constructorTest() { + assertNotNull(new BitArrayDeclaration(1, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1)); + assertNotNull(new BitArrayDeclaration(64, ByteOrder.BIG_ENDIAN, BitOrder.LAST_TO_FIRST, 8)); + assertNotNull(new BitArrayDeclaration(Integer.MAX_VALUE, ByteOrder.BIG_ENDIAN, BitOrder.LAST_TO_FIRST, 8)); + } + + /** + * The length must be greater than zero + */ + @Test(expected = IllegalArgumentException.class) + public void zeroLengthTest() { + new BitArrayDeclaration(0, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1); + } + + /** + * The length must be greater than zero + */ + @Test(expected = IllegalArgumentException.class) + public void negativeLengthTest() { + new BitArrayDeclaration(-8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1); + } + + @Test + public void getterTest() { + BitArrayDeclaration declaration = new BitArrayDeclaration(12, ByteOrder.BIG_ENDIAN, BitOrder.LAST_TO_FIRST, 16); + assertEquals(12, declaration.getLength()); + assertEquals(ByteOrder.BIG_ENDIAN, declaration.getByteOrder()); + assertEquals(BitOrder.LAST_TO_FIRST, declaration.getBitOrder()); + assertEquals(16, declaration.getAlignment()); + assertEquals(12, declaration.getMaximumSize()); + } + + @Test + public void toStringTest() { + BitArrayDeclaration declaration = new BitArrayDeclaration(12, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1); + String result = declaration.toString(); + assertTrue(result.contains("bit array")); + assertTrue(result.contains("length:12")); + assertTrue(result.contains("byte order:" + ByteOrder.LITTLE_ENDIAN)); + assertTrue(result.contains("bit order:" + BitOrder.FIRST_TO_LAST)); + assertTrue(result.contains("alignment:1")); + } + + @Test + public void binaryEquivalentTest() { + BitArrayDeclaration a = new BitArrayDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8); + BitArrayDeclaration b = new BitArrayDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8); + BitArrayDeclaration c = new BitArrayDeclaration(16, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8); + BitArrayDeclaration d = new BitArrayDeclaration(8, ByteOrder.BIG_ENDIAN, BitOrder.LAST_TO_FIRST, 8); + BitArrayDeclaration e = new BitArrayDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1); + BooleanDeclaration f = new BooleanDeclaration(8, ByteOrder.LITTLE_ENDIAN, 8); + BitMapDeclaration g = new BitMapDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8, Map.of("flag", List.of(new IntegerRange(0, 0)))); + BitArrayDeclaration h = new BitArrayDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.LAST_TO_FIRST, 8); + BitArrayDeclaration i = new BitArrayDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8); + + assertTrue(a.isBinaryEquivalent(a)); + assertTrue(a.isBinaryEquivalent(b)); + assertTrue(b.isBinaryEquivalent(a)); + assertFalse(a.isBinaryEquivalent(c)); + assertFalse(a.isBinaryEquivalent(d)); + assertFalse(a.isBinaryEquivalent(e)); + assertFalse(a.isBinaryEquivalent(f)); + assertFalse(a.isBinaryEquivalent(g)); + assertFalse(a.isBinaryEquivalent(h)); + assertTrue(a.isBinaryEquivalent(i)); + assertFalse(a.isBinaryEquivalent(null)); + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/BitArrayDefinitionTest.java b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/BitArrayDefinitionTest.java new file mode 100644 index 0000000000..4d860282ed --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/BitArrayDefinitionTest.java @@ -0,0 +1,295 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.ctf.core.tests.types; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.tracecompass.ctf.core.CTFException; +import org.eclipse.tracecompass.ctf.core.event.io.BitBuffer; +import org.eclipse.tracecompass.ctf.core.event.types.BitArrayDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.BitArrayDefinition; +import org.eclipse.tracecompass.ctf.core.event.types.BitOrder; +import org.junit.Test; + +/** + * Tests for the fixed-length bit array definition class in CTF2, following the + * specification: https://diamon.org/ctf/#fl-ba-fc + * + * @author Arnaud Fiorini + */ +@SuppressWarnings("javadoc") +public class BitArrayDefinitionTest { + + private static final @NonNull String FIELD_NAME = "bits"; + + private static BitBuffer createBuffer(byte... data) { + ByteBuffer byb = ByteBuffer.wrap(data); + return new BitBuffer(byb); + } + + private static BitArrayDefinition read(BitArrayDeclaration declaration, BitBuffer input) throws CTFException { + return declaration.createDefinition(null, FIELD_NAME, Objects.requireNonNull(input)); + } + + private static BitArrayDefinition read(BitArrayDeclaration declaration, byte... data) throws CTFException { + return read(declaration, createBuffer(data)); + } + + /** + * Within a little-endian byte, the bits are decoded from the least + * significant to the most significant bits + */ + @Test + public void testLittleEndianSubByte() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(4, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1); + BitBuffer input = createBuffer((byte) 0xa5); + assertArrayEquals(new byte[] { 0x05 }, read(declaration, input).getValue()); + assertArrayEquals(new byte[] { 0x0a }, read(declaration, input).getValue()); + } + + /** + * Within a big-endian byte, the bits are decoded from the most significant + * to the least significant bits + */ + @Test + public void testBigEndianSubByte() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(4, ByteOrder.BIG_ENDIAN, BitOrder.LAST_TO_FIRST, 1); + BitBuffer input = createBuffer((byte) 0xa5); + assertArrayEquals(new byte[] { 0x0a }, read(declaration, input).getValue()); + assertArrayEquals(new byte[] { 0x05 }, read(declaration, input).getValue()); + } + + @Test + public void testLittleEndianMultiByte() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(12, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1); + assertArrayEquals(new byte[] { 0x34, 0x02 }, read(declaration, (byte) 0x34, (byte) 0x12).getValue()); + } + + @Test + public void testBigEndianMultiByte() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(12, ByteOrder.BIG_ENDIAN, BitOrder.LAST_TO_FIRST, 1); + assertArrayEquals(new byte[] { 0x23, 0x01 }, read(declaration, (byte) 0x12, (byte) 0x34).getValue()); + } + + @Test + public void testLittleEndian64Bits() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(64, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8); + BitArrayDefinition definition = read(declaration, (byte) 0x08, (byte) 0x07, (byte) 0x06, (byte) 0x05, + (byte) 0x04, (byte) 0x03, (byte) 0x02, (byte) 0x81); + assertArrayEquals(new byte[] { 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, (byte) 0x81 }, definition.getValue()); + assertTrue(definition.isBitSet(63)); + assertTrue(definition.isBitSet(3)); + assertFalse(definition.isBitSet(0)); + } + + @Test + public void testBigEndian64Bits() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(64, ByteOrder.BIG_ENDIAN, BitOrder.LAST_TO_FIRST, 8); + BitArrayDefinition definition = read(declaration, (byte) 0x81, (byte) 0x02, (byte) 0x03, (byte) 0x04, + (byte) 0x05, (byte) 0x06, (byte) 0x07, (byte) 0x08); + assertArrayEquals(new byte[] { 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, (byte) 0x81 }, definition.getValue()); + } + + /** + * The length of a bit array is only required to be greater than zero, so + * a field longer than 64 bits must be decoded + */ + @Test + public void testLittleEndianLongerThan64Bits() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(72, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8); + BitBuffer input = createBuffer((byte) 0x01, (byte) 0, (byte) 0, (byte) 0, + (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0x80); + BitArrayDefinition definition = read(declaration, input); + assertTrue(definition.isBitSet(0)); + assertFalse(definition.isBitSet(1)); + assertFalse(definition.isBitSet(64)); + assertTrue(definition.isBitSet(71)); + assertEquals(72, input.position()); + assertEquals("0b1" + "0".repeat(70) + "1", definition.toString()); + } + + /** + * The length of a bit array is only required to be greater than zero, so + * a field longer than 64 bits must be decoded + */ + @Test + public void testBigEndianLongerThan64Bits() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(72, ByteOrder.BIG_ENDIAN, BitOrder.LAST_TO_FIRST, 8); + BitBuffer input = createBuffer((byte) 0x80, (byte) 0, (byte) 0, (byte) 0, + (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0x01); + BitArrayDefinition definition = read(declaration, input); + assertTrue(definition.isBitSet(0)); + assertFalse(definition.isBitSet(1)); + assertTrue(definition.isBitSet(71)); + assertEquals(72, input.position()); + } + + /** + * With the last-to-first bit order, the first decoded bit of a + * little-endian field is the last element of the bit array + */ + @Test + public void testLittleEndianLastToFirst() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(12, ByteOrder.LITTLE_ENDIAN, BitOrder.LAST_TO_FIRST, 1); + /* Decoded bits, in order: 1100 0000 0010 */ + BitArrayDefinition definition = read(declaration, (byte) 0x03, (byte) 0x04); + assertArrayEquals(new byte[] { 0x02, 0x0c }, definition.getValue()); + } + + /** + * With the first-to-last bit order, the first decoded bit of a big-endian + * field is the first element of the bit array + */ + @Test + public void testBigEndianFirstToLast() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(12, ByteOrder.BIG_ENDIAN, BitOrder.FIRST_TO_LAST, 1); + /* Decoded bits, in order: 1100 0000 0010 */ + BitArrayDefinition definition = read(declaration, (byte) 0xc0, (byte) 0x20); + assertArrayEquals(new byte[] { 0x03, 0x04 }, definition.getValue()); + } + + /** + * The bit order applies to the whole field, not to each 64-bit chunk read + * from the buffer + */ + @Test + public void testLittleEndianLastToFirstLongerThan64Bits() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(72, ByteOrder.LITTLE_ENDIAN, BitOrder.LAST_TO_FIRST, 8); + /* Decoded bits 0, 1 and 64 are set */ + BitArrayDefinition definition = read(declaration, (byte) 0x03, (byte) 0, (byte) 0, (byte) 0, + (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0x01); + assertTrue(definition.isBitSet(71)); + assertTrue(definition.isBitSet(70)); + assertTrue(definition.isBitSet(7)); + assertFalse(definition.isBitSet(0)); + assertFalse(definition.isBitSet(64)); + assertEquals("0b11" + "0".repeat(62) + "10000000", definition.toString()); + } + + /** + * The bit order applies to the whole field, not to each 64-bit chunk read + * from the buffer + */ + @Test + public void testBigEndianFirstToLastLongerThan64Bits() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(72, ByteOrder.BIG_ENDIAN, BitOrder.FIRST_TO_LAST, 8); + /* Decoded bits 0, 1 and 64 are set */ + BitArrayDefinition definition = read(declaration, (byte) 0xc0, (byte) 0, (byte) 0, (byte) 0, + (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0x80); + assertTrue(definition.isBitSet(0)); + assertTrue(definition.isBitSet(1)); + assertTrue(definition.isBitSet(64)); + assertFalse(definition.isBitSet(71)); + assertFalse(definition.isBitSet(7)); + } + + /** + * A bit array has no integer type semantics, the most significant bit is + * never a sign bit + */ + @Test + public void testNoSignExtension() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8); + BitArrayDefinition definition = read(declaration, (byte) 0xff); + assertArrayEquals(new byte[] { (byte) 0xff }, definition.getValue()); + } + + /** + * The first bit of an instance is aligned relative to the beginning of the + * packet + */ + @Test + public void testAlignment() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 16); + BitBuffer input = createBuffer((byte) 0xff, (byte) 0xff, (byte) 0x42); + input.position(3); + assertArrayEquals(new byte[] { 0x42 }, read(declaration, input).getValue()); + assertEquals(24, input.position()); + } + + /** + * The default alignment of 1 does not skip any bit + */ + @Test + public void testDefaultAlignment() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(3, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1); + BitBuffer input = createBuffer((byte) 0b0101_1010); + input.position(1); + assertArrayEquals(new byte[] { 0x05 }, read(declaration, input).getValue()); + assertEquals(4, input.position()); + } + + /** + * The byte order of the input buffer is restored after reading + */ + @Test + public void testByteOrderRestored() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(8, ByteOrder.BIG_ENDIAN, BitOrder.LAST_TO_FIRST, 8); + BitBuffer input = new BitBuffer(ByteBuffer.wrap(new byte[] { 1 }), ByteOrder.LITTLE_ENDIAN); + read(declaration, input); + assertEquals(ByteOrder.LITTLE_ENDIAN, input.getByteOrder()); + } + + @Test + public void testIsBitSet() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8); + BitArrayDefinition definition = read(declaration, (byte) 0b1000_0101); + assertTrue(definition.isBitSet(0)); + assertFalse(definition.isBitSet(1)); + assertTrue(definition.isBitSet(2)); + assertFalse(definition.isBitSet(6)); + assertTrue(definition.isBitSet(7)); + /* Out of range */ + assertFalse(definition.isBitSet(-1)); + assertFalse(definition.isBitSet(8)); + assertFalse(definition.isBitSet(64)); + } + + /** + * The string contains all the bits of the field, including the leading + * zeros + */ + @Test + public void testToString() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8); + BitArrayDefinition definition = read(declaration, (byte) 0b0000_0101); + assertEquals("0b00000101", definition.toString()); + + declaration = new BitArrayDeclaration(3, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1); + assertEquals("0b000", read(declaration, (byte) 0).toString()); + } + + @Test + public void testGetDeclaration() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8); + BitArrayDefinition definition = read(declaration, (byte) 1); + assertSame(declaration, definition.getDeclaration()); + assertEquals(8, definition.size()); + } + + /** + * Reading past the end of the buffer fails + */ + @Test(expected = CTFException.class) + public void testBufferTooSmall() throws CTFException { + BitArrayDeclaration declaration = new BitArrayDeclaration(16, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8); + read(declaration, (byte) 1); + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/BitMapDeclarationParserTest.java b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/BitMapDeclarationParserTest.java new file mode 100644 index 0000000000..7d2c95169c --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/BitMapDeclarationParserTest.java @@ -0,0 +1,642 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.ctf.core.tests.types; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.List; +import java.util.Map; + +import org.eclipse.tracecompass.ctf.core.CTFException; +import org.eclipse.tracecompass.ctf.core.event.io.BitBuffer; +import org.eclipse.tracecompass.ctf.core.event.types.BitMapDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.BitMapDefinition; +import org.eclipse.tracecompass.ctf.core.event.types.IntegerRange; +import org.eclipse.tracecompass.ctf.core.trace.CTFTrace; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.CTFJsonMetadataNode; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.JsonStructureFieldMemberMetadataNode; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.ParseException; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.bitmap.BitMapDeclarationParser; +import org.eclipse.tracecompass.internal.ctf.core.utils.JsonMetadataStrings; +import org.junit.Test; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +/** + * Test class for the fixed-length bit map declaration parser in CTF2, + * following the specification: https://diamon.org/ctf/#fl-bm-fc + * + * @author Arnaud Fiorini + */ +public class BitMapDeclarationParserTest { + + // ------------------------------------------------------------------------ + // Helpers + // ------------------------------------------------------------------------ + + private static BitMapDeclaration parse(JsonObject fieldClass) throws ParseException { + return BitMapDeclarationParser.INSTANCE.parse(createNode(fieldClass), new BitMapDeclarationParser.Param(new CTFTrace())); + } + + private static JsonStructureFieldMemberMetadataNode createNode(JsonObject fieldClass) { + return new JsonStructureFieldMemberMetadataNode(null, JsonMetadataStrings.FIXED_LENGTH_BIT_MAP, "test", "flags", fieldClass); + } + + /** + * Create an integer range set from pairs of lower and upper bit indexes + */ + private static JsonArray ranges(long... bounds) { + JsonArray rangeSet = new JsonArray(); + for (int i = 0; i < bounds.length; i += 2) { + JsonArray range = new JsonArray(); + range.add(bounds[i]); + range.add(bounds[i + 1]); + rangeSet.add(range); + } + return rangeSet; + } + + /** + * Create bit map flags with a single flag + */ + private static JsonObject flags(String name, JsonArray ranges) { + JsonObject flags = new JsonObject(); + flags.add(name, ranges); + return flags; + } + + /** + * Create a fixed-length bit map field class, the null properties are + * omitted + */ + private static JsonObject bitMap(Integer length, String byteOrder, JsonObject flags) { + JsonObject bitMap = new JsonObject(); + bitMap.addProperty("type", "fixed-length-bit-map"); + if (length != null) { + bitMap.addProperty("length", length); + } + if (byteOrder != null) { + bitMap.addProperty("byte-order", byteOrder); + } + if (flags != null) { + bitMap.add("flags", flags); + } + return bitMap; + } + + /** + * Create a valid fixed-length bit map field class with a single flag on + * bit 0 + */ + private static JsonObject bitMap(Integer length, String byteOrder) { + return bitMap(length, byteOrder, flags("first", ranges(0, 0))); + } + + private static void assertRange(long start, long end, IntegerRange range) { + assertEquals(start, range.getStart()); + assertEquals(end, range.getEnd()); + } + + // ------------------------------------------------------------------------ + // Inherited bit array properties + // ------------------------------------------------------------------------ + + /** + * Test parsing a fixed-length little-endian bit map field class with only + * the required properties + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testLittleEndian() throws ParseException { + BitMapDeclaration result = parse(bitMap(16, "little-endian")); + + assertNotNull(result); + assertEquals(16, result.getLength()); + assertEquals(ByteOrder.LITTLE_ENDIAN, result.getByteOrder()); + /* The default alignment is 1 */ + assertEquals(1, result.getAlignment()); + assertEquals(List.of("first"), List.copyOf(result.getFlags().keySet())); + } + + /** + * Test parsing a fixed-length big-endian bit map field class + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testBigEndian() throws ParseException { + BitMapDeclaration result = parse(bitMap(5, "big-endian")); + + assertNotNull(result); + assertEquals(5, result.getLength()); + assertEquals(ByteOrder.BIG_ENDIAN, result.getByteOrder()); + assertEquals(1, result.getAlignment()); + } + + /** + * Test parsing a fixed-length bit map field class with an alignment + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testAlignment() throws ParseException { + JsonObject fieldClass = bitMap(32, "little-endian"); + fieldClass.addProperty("alignment", 32); + + assertEquals(32, parse(fieldClass).getAlignment()); + } + + /** + * Test parsing a fixed-length bit map field class of one bit, the smallest + * valid length + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testOneBit() throws ParseException { + assertEquals(1, parse(bitMap(1, "little-endian")).getLength()); + } + + /** + * Test that the optional bit-order property is accepted with both of its + * valid values + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testBitOrder() throws ParseException { + JsonObject fieldClass = bitMap(8, "little-endian"); + fieldClass.addProperty("bit-order", "first-to-last"); + assertEquals(8, parse(fieldClass).getLength()); + + fieldClass = bitMap(8, "big-endian"); + fieldClass.addProperty("bit-order", "last-to-first"); + assertEquals(8, parse(fieldClass).getLength()); + } + + /** + * Parse a 4-bit field class with a flag on each bit, with the given byte + * order and bit order, and decode one byte with it + */ + private static BitMapDefinition decodeNibble(String byteOrder, String bitOrder, byte data) throws ParseException, CTFException { + JsonObject flags = new JsonObject(); + flags.add("bit0", ranges(0, 0)); + flags.add("bit1", ranges(1, 1)); + flags.add("bit2", ranges(2, 2)); + flags.add("bit3", ranges(3, 3)); + JsonObject fieldClass = bitMap(4, byteOrder, flags); + if (bitOrder != null) { + fieldClass.addProperty("bit-order", bitOrder); + } + BitMapDeclaration declaration = parse(fieldClass); + return (BitMapDefinition) declaration.createDefinition(null, "flags", new BitBuffer(ByteBuffer.wrap(new byte[] { data }))); + } + + /** + * Test the bit order with a little-endian byte order. The bits are decoded + * from the least significant bit of the byte, so the first decoded bit of + * 0x01 is 1 followed by three 0. + * + * @throws ParseException + * if parsing fails + * @throws CTFException + * if decoding fails + */ + @Test + public void testBitOrderLittleEndian() throws ParseException, CTFException { + /* Default is first-to-last: the first decoded bit is element 0 */ + assertEquals(List.of("bit0"), decodeNibble("little-endian", null, (byte) 0x01).getActiveFlags()); + assertEquals(List.of("bit0"), decodeNibble("little-endian", "first-to-last", (byte) 0x01).getActiveFlags()); + /* The first decoded bit is the last element */ + assertEquals(List.of("bit3"), decodeNibble("little-endian", "last-to-first", (byte) 0x01).getActiveFlags()); + } + + /** + * Test the bit order with a big-endian byte order. The bits are decoded + * from the most significant bit of the byte, so the first decoded bit of + * 0x80 is 1 followed by three 0. + * + * @throws ParseException + * if parsing fails + * @throws CTFException + * if decoding fails + */ + @Test + public void testBitOrderBigEndian() throws ParseException, CTFException { + /* Default is last-to-first: the first decoded bit is the last element */ + assertEquals(List.of("bit3"), decodeNibble("big-endian", null, (byte) 0x80).getActiveFlags()); + assertEquals(List.of("bit3"), decodeNibble("big-endian", "last-to-first", (byte) 0x80).getActiveFlags()); + /* The first decoded bit is element 0 */ + assertEquals(List.of("bit0"), decodeNibble("big-endian", "first-to-last", (byte) 0x80).getActiveFlags()); + } + + /** + * Test that the optional attributes and extensions properties are accepted + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testAttributesAndExtensions() throws ParseException { + JsonObject attributes = new JsonObject(); + JsonObject namespace = new JsonObject(); + namespace.addProperty("info", "flags"); + attributes.add("my.tracer", namespace); + + JsonObject fieldClass = bitMap(8, "little-endian"); + fieldClass.add("attributes", attributes); + fieldClass.add("extensions", new JsonObject()); + + BitMapDeclaration result = parse(fieldClass); + assertEquals(8, result.getLength()); + assertEquals(ByteOrder.LITTLE_ENDIAN, result.getByteOrder()); + assertEquals(1, result.getFlags().size()); + } + + /** + * Test that the length property is required + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testMissingLength() throws ParseException { + parse(bitMap(null, "little-endian")); + } + + /** + * Test that the length must be greater than zero + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testZeroLength() throws ParseException { + parse(bitMap(0, "little-endian")); + } + + /** + * Test that a negative length is rejected + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testNegativeLength() throws ParseException { + parse(bitMap(-1, "little-endian")); + } + + /** + * Test that the byte-order property is required + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testMissingByteOrder() throws ParseException { + parse(bitMap(8, null)); + } + + /** + * Test that an invalid byte order is rejected + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testInvalidByteOrder() throws ParseException { + parse(bitMap(8, "middle-endian")); + } + + /** + * Test that the bit-order property must be "first-to-last" or + * "last-to-first" + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testInvalidBitOrder() throws ParseException { + JsonObject fieldClass = bitMap(8, "little-endian"); + fieldClass.addProperty("bit-order", "middle-out"); + parse(fieldClass); + } + + /** + * Test that the alignment must be a power of two + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testNonPowerOfTwoAlignment() throws ParseException { + JsonObject fieldClass = bitMap(8, "little-endian"); + fieldClass.addProperty("alignment", 3); + parse(fieldClass); + } + + /** + * Test that the alignment must be positive + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testZeroAlignment() throws ParseException { + JsonObject fieldClass = bitMap(8, "little-endian"); + fieldClass.addProperty("alignment", 0); + parse(fieldClass); + } + + // ------------------------------------------------------------------------ + // Flags + // ------------------------------------------------------------------------ + + /** + * Test parsing several flags, each with one or more bit ranges. The flags + * keep their declaration order. + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testFlags() throws ParseException { + JsonObject flags = new JsonObject(); + flags.add("active", ranges(0, 0)); + flags.add("mode", ranges(1, 3, 6, 6)); + flags.add("error", ranges(7, 7)); + + BitMapDeclaration result = parse(bitMap(8, "little-endian", flags)); + Map> parsedFlags = result.getFlags(); + + assertEquals(List.of("active", "mode", "error"), List.copyOf(parsedFlags.keySet())); + + List active = parsedFlags.get("active"); + assertNotNull(active); + assertEquals(1, active.size()); + assertRange(0, 0, active.get(0)); + + List mode = parsedFlags.get("mode"); + assertNotNull(mode); + assertEquals(2, mode.size()); + assertRange(1, 3, mode.get(0)); + assertRange(6, 6, mode.get(1)); + + List error = parsedFlags.get("error"); + assertNotNull(error); + assertEquals(1, error.size()); + assertRange(7, 7, error.get(0)); + } + + /** + * Test that a bit may have more than one associated name + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testOverlappingFlags() throws ParseException { + JsonObject flags = new JsonObject(); + flags.add("low", ranges(0, 3)); + flags.add("first", ranges(0, 0)); + + BitMapDeclaration result = parse(bitMap(4, "little-endian", flags)); + assertEquals(2, result.getFlags().size()); + List low = result.getFlags().get("low"); + List first = result.getFlags().get("first"); + assertNotNull(low); + assertRange(0, 3, low.get(0)); + assertNotNull(first); + assertRange(0, 0, first.get(0)); + } + + /** + * Test that a flag can cover the last bit of the bit map + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testFlagOnLastBit() throws ParseException { + BitMapDeclaration result = parse(bitMap(8, "little-endian", flags("last", ranges(7, 7)))); + List last = result.getFlags().get("last"); + assertNotNull(last); + assertRange(7, 7, last.get(0)); + } + + /** + * Test that a flag can cover all the bits of the bit map + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testFlagOnAllBits() throws ParseException { + BitMapDeclaration result = parse(bitMap(8, "little-endian", flags("any", ranges(0, 7)))); + List any = result.getFlags().get("any"); + assertNotNull(any); + assertRange(0, 7, any.get(0)); + } + + /** + * Test that a flag can name a bit beyond the 64th bit of a long bit map + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testFlagBeyond64Bits() throws ParseException { + BitMapDeclaration result = parse(bitMap(72, "little-endian", flags("high", ranges(70, 71)))); + assertEquals(72, result.getLength()); + List high = result.getFlags().get("high"); + assertNotNull(high); + assertRange(70, 71, high.get(0)); + } + + /** + * Test that the flags property is required + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testMissingFlags() throws ParseException { + parse(bitMap(8, "little-endian", null)); + } + + /** + * Test that the flags property must contain one or more properties + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testEmptyFlags() throws ParseException { + parse(bitMap(8, "little-endian", new JsonObject())); + } + + /** + * Test that the flags property must be a JSON object + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testFlagsNotAnObject() throws ParseException { + JsonObject fieldClass = bitMap(8, "little-endian", null); + fieldClass.add("flags", ranges(0, 0)); + parse(fieldClass); + } + + /** + * Test that the bit ranges of a flag must be an integer range set + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testFlagRangesNotAnArray() throws ParseException { + JsonObject flags = new JsonObject(); + flags.addProperty("first", 0); + parse(bitMap(8, "little-endian", flags)); + } + + /** + * Test that an integer range must contain exactly a lower and an upper + * value + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testRangeWithOneValue() throws ParseException { + JsonArray range = new JsonArray(); + range.add(0); + JsonArray rangeSet = new JsonArray(); + rangeSet.add(range); + parse(bitMap(8, "little-endian", flags("first", rangeSet))); + } + + /** + * Test that an integer range must contain exactly a lower and an upper + * value + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testRangeWithThreeValues() throws ParseException { + JsonArray range = new JsonArray(); + range.add(0); + range.add(1); + range.add(2); + JsonArray rangeSet = new JsonArray(); + rangeSet.add(range); + parse(bitMap(8, "little-endian", flags("first", rangeSet))); + } + + /** + * Test that an integer range must be a JSON array + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testRangeNotAnArray() throws ParseException { + JsonArray rangeSet = new JsonArray(); + rangeSet.add(3); + parse(bitMap(8, "little-endian", flags("first", rangeSet))); + } + + /** + * Test that the lower value of a range cannot be greater than its upper + * value + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testRangeLowerGreaterThanUpper() throws ParseException { + parse(bitMap(8, "little-endian", flags("first", ranges(3, 1)))); + } + + /** + * Test that a bit index cannot be negative + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testNegativeBitIndex() throws ParseException { + parse(bitMap(8, "little-endian", flags("first", ranges(-1, 0)))); + } + + /** + * Test that a bit index must be less than the length of the bit map + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testBitIndexEqualToLength() throws ParseException { + parse(bitMap(8, "little-endian", flags("first", ranges(0, 8)))); + } + + /** + * Test that a bit index must be less than the length of the bit map, in + * any range of the flag + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testBitIndexOutOfRangeInSecondRange() throws ParseException { + parse(bitMap(8, "little-endian", flags("first", ranges(0, 0, 9, 10)))); + } + + // ------------------------------------------------------------------------ + // Type and parameters + // ------------------------------------------------------------------------ + + /** + * Test that the fixed-length bit map field class is not supported in + * CTF 1.8 + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testCTF1NotSupported() throws ParseException { + BitMapDeclarationParser.INSTANCE.parse(new CTFJsonMetadataNode(null, "bit-map", ""), + new BitMapDeclarationParser.Param(new CTFTrace())); + } + + /** + * Test that the parser requires its own parameter type + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = IllegalArgumentException.class) + public void testInvalidParam() throws ParseException { + BitMapDeclarationParser.INSTANCE.parse(createNode(bitMap(8, "little-endian")), null); + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/BitMapDeclarationTest.java b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/BitMapDeclarationTest.java new file mode 100644 index 0000000000..22d4d6f35d --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/BitMapDeclarationTest.java @@ -0,0 +1,178 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.ctf.core.tests.types; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.tracecompass.ctf.core.CTFException; +import org.eclipse.tracecompass.ctf.core.event.io.BitBuffer; +import org.eclipse.tracecompass.ctf.core.event.types.BitArrayDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.BitArrayDefinition; +import org.eclipse.tracecompass.ctf.core.event.types.BitMapDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.BitMapDefinition; +import org.eclipse.tracecompass.ctf.core.event.types.BitOrder; +import org.eclipse.tracecompass.ctf.core.event.types.IntegerRange; +import org.junit.Test; + +/** + * Test for the fixed-length bit map declaration class in CTF2, following the + * specification: https://diamon.org/ctf/#fl-bm-fc + * + * @author Arnaud Fiorini + */ +@SuppressWarnings("javadoc") +public class BitMapDeclarationTest { + + private static Map> flags() { + Map> flags = new LinkedHashMap<>(); + flags.put("active", List.of(new IntegerRange(0, 0))); + flags.put("mode", List.of(new IntegerRange(1, 3), new IntegerRange(6, 6))); + flags.put("error", List.of(new IntegerRange(7, 7))); + return flags; + } + + @Test + public void constructorTest() { + assertNotNull(new BitMapDeclaration(1, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1, Map.of("flag", List.of(new IntegerRange(0, 0))))); + assertNotNull(new BitMapDeclaration(8, ByteOrder.BIG_ENDIAN, BitOrder.LAST_TO_FIRST, 8, flags())); + } + + /** + * The length must be greater than zero + */ + @Test(expected = IllegalArgumentException.class) + public void zeroLengthTest() { + new BitMapDeclaration(0, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1, flags()); + } + + /** + * The length must be greater than zero + */ + @Test(expected = IllegalArgumentException.class) + public void negativeLengthTest() { + new BitMapDeclaration(-8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1, flags()); + } + + /** + * The flags must contain one or more flags + */ + @Test(expected = IllegalArgumentException.class) + public void emptyFlagsTest() { + new BitMapDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1, Map.of()); + } + + @Test + public void getterTest() { + BitMapDeclaration declaration = new BitMapDeclaration(12, ByteOrder.BIG_ENDIAN, BitOrder.LAST_TO_FIRST, 16, flags()); + assertEquals(12, declaration.getLength()); + assertEquals(ByteOrder.BIG_ENDIAN, declaration.getByteOrder()); + assertEquals(16, declaration.getAlignment()); + assertEquals(12, declaration.getMaximumSize()); + } + + /** + * The flags keep their declaration order and their ranges + */ + @Test + public void getFlagsTest() { + BitMapDeclaration declaration = new BitMapDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1, flags()); + Map> result = declaration.getFlags(); + assertEquals(List.of("active", "mode", "error"), List.copyOf(result.keySet())); + List mode = result.get("mode"); + assertNotNull(mode); + assertEquals(2, mode.size()); + assertEquals(1, mode.get(0).getStart()); + assertEquals(3, mode.get(0).getEnd()); + assertEquals(6, mode.get(1).getStart()); + assertEquals(6, mode.get(1).getEnd()); + } + + /** + * A modification of the map given to the constructor does not change the + * declaration + */ + @Test + public void flagsCopiedTest() { + Map> flags = flags(); + BitMapDeclaration declaration = new BitMapDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1, flags); + flags.put("other", List.of(new IntegerRange(5, 5))); + flags.remove("active"); + assertEquals(List.of("active", "mode", "error"), List.copyOf(declaration.getFlags().keySet())); + } + + @Test(expected = UnsupportedOperationException.class) + public void flagsUnmodifiableTest() { + BitMapDeclaration declaration = new BitMapDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1, flags()); + declaration.getFlags().put("other", List.of(new IntegerRange(5, 5))); + } + + /** + * A bit may have more than one associated name + */ + @Test + public void overlappingFlagsTest() { + Map> flags = new LinkedHashMap<>(); + flags.put("low", List.of(new IntegerRange(0, 3))); + flags.put("first", List.of(new IntegerRange(0, 0))); + BitMapDeclaration declaration = new BitMapDeclaration(4, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1, flags); + assertEquals(2, declaration.getFlags().size()); + } + + /** + * Reading a bit map creates a bit map definition + */ + @Test + public void createDefinitionTest() throws CTFException { + BitMapDeclaration declaration = new BitMapDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8, flags()); + BitArrayDefinition definition = declaration.createDefinition(null, "flags", new BitBuffer(ByteBuffer.wrap(new byte[] { 1 }))); + assertTrue(definition instanceof BitMapDefinition); + } + + @Test + public void toStringTest() { + BitMapDeclaration declaration = new BitMapDeclaration(12, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1, flags()); + String result = declaration.toString(); + assertTrue(result.contains("bit map")); + assertTrue(result.contains("length:12")); + assertTrue(result.contains("active")); + assertTrue(result.contains("mode")); + assertTrue(result.contains("error")); + } + + @Test + public void binaryEquivalentTest() { + BitMapDeclaration a = new BitMapDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8, flags()); + BitMapDeclaration b = new BitMapDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8, flags()); + BitMapDeclaration c = new BitMapDeclaration(16, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8, flags()); + BitMapDeclaration d = new BitMapDeclaration(8, ByteOrder.BIG_ENDIAN, BitOrder.LAST_TO_FIRST, 8, flags()); + BitMapDeclaration e = new BitMapDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1, flags()); + BitArrayDeclaration f = new BitArrayDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8); + + assertTrue(a.isBinaryEquivalent(a)); + assertTrue(a.isBinaryEquivalent(b)); + assertTrue(b.isBinaryEquivalent(a)); + assertFalse(a.isBinaryEquivalent(c)); + assertFalse(a.isBinaryEquivalent(d)); + assertFalse(a.isBinaryEquivalent(e)); + assertFalse(a.isBinaryEquivalent(f)); + assertFalse(f.isBinaryEquivalent(a)); + assertFalse(a.isBinaryEquivalent(null)); + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/BitMapDefinitionTest.java b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/BitMapDefinitionTest.java new file mode 100644 index 0000000000..c3fa3bf650 --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/BitMapDefinitionTest.java @@ -0,0 +1,353 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.ctf.core.tests.types; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.tracecompass.ctf.core.CTFException; +import org.eclipse.tracecompass.ctf.core.event.io.BitBuffer; +import org.eclipse.tracecompass.ctf.core.event.types.BitMapDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.BitMapDefinition; +import org.eclipse.tracecompass.ctf.core.event.types.BitOrder; +import org.eclipse.tracecompass.ctf.core.event.types.IntegerRange; +import org.junit.Test; + +/** + * Tests for the fixed-length bit map definition class in CTF2, following the + * specification: https://diamon.org/ctf/#fl-bm-fc + * + * @author Arnaud Fiorini + */ +@SuppressWarnings("javadoc") +public class BitMapDefinitionTest { + + private static final @NonNull String FIELD_NAME = "flags"; + + private static BitBuffer createBuffer(byte... data) { + ByteBuffer byb = ByteBuffer.wrap(data); + return new BitBuffer(byb); + } + + private static BitMapDefinition read(BitMapDeclaration declaration, BitBuffer input) throws CTFException { + return (BitMapDefinition) declaration.createDefinition(null, FIELD_NAME, input); + } + + private static BitMapDefinition read(BitMapDeclaration declaration, byte... data) throws CTFException { + return read(declaration, createBuffer(data)); + } + + /** + * Flags of an 8-bit map: "active" on bit 0, "mode" on bits 1 to 3 and 6, + * "error" on bit 7 + */ + private static Map> flags() { + Map> flags = new LinkedHashMap<>(); + flags.put("active", List.of(new IntegerRange(0, 0))); + flags.put("mode", List.of(new IntegerRange(1, 3), new IntegerRange(6, 6))); + flags.put("error", List.of(new IntegerRange(7, 7))); + return flags; + } + + private static BitMapDeclaration declaration(ByteOrder byteOrder) { + return new BitMapDeclaration(8, byteOrder, BitOrder.fromByteOrder(byteOrder), 8, flags()); + } + + /** + * A flag is active when its single bit is set + */ + @Test + public void testSingleBitFlag() throws CTFException { + BitMapDefinition definition = read(declaration(ByteOrder.LITTLE_ENDIAN), (byte) 0b0000_0001); + assertEquals(List.of("active"), definition.getActiveFlags()); + assertTrue(definition.isFlagActive("active")); + assertFalse(definition.isFlagActive("mode")); + assertFalse(definition.isFlagActive("error")); + } + + /** + * A flag with a range of bits is active when any of its bits is set + */ + @Test + public void testRangeFlag() throws CTFException { + BitMapDeclaration declaration = declaration(ByteOrder.LITTLE_ENDIAN); + assertEquals(List.of("mode"), read(declaration, (byte) 0b0000_0010).getActiveFlags()); + assertEquals(List.of("mode"), read(declaration, (byte) 0b0000_0100).getActiveFlags()); + assertEquals(List.of("mode"), read(declaration, (byte) 0b0000_1000).getActiveFlags()); + assertEquals(List.of("mode"), read(declaration, (byte) 0b0000_1110).getActiveFlags()); + } + + /** + * A flag with several ranges is active when a bit of any of its ranges is + * set + */ + @Test + public void testMultiRangeFlag() throws CTFException { + BitMapDefinition definition = read(declaration(ByteOrder.LITTLE_ENDIAN), (byte) 0b0100_0000); + assertEquals(List.of("mode"), definition.getActiveFlags()); + } + + /** + * A bit which is not named by any flag does not activate any flag + */ + @Test + public void testUnnamedBit() throws CTFException { + BitMapDefinition definition = read(declaration(ByteOrder.LITTLE_ENDIAN), (byte) 0b0011_0000); + assertEquals(List.of(), definition.getActiveFlags()); + assertTrue(definition.isBitSet(4)); + assertTrue(definition.isBitSet(5)); + } + + @Test + public void testNoActiveFlag() throws CTFException { + BitMapDefinition definition = read(declaration(ByteOrder.LITTLE_ENDIAN), (byte) 0); + assertEquals(List.of(), definition.getActiveFlags()); + assertFalse(definition.isFlagActive("active")); + assertFalse(definition.isFlagActive("mode")); + assertFalse(definition.isFlagActive("error")); + } + + /** + * The active flags are in declaration order + */ + @Test + public void testAllActiveFlags() throws CTFException { + BitMapDefinition definition = read(declaration(ByteOrder.LITTLE_ENDIAN), (byte) 0xff); + assertEquals(List.of("active", "mode", "error"), definition.getActiveFlags()); + } + + /** + * The active flags are in declaration order, not in bit order + */ + @Test + public void testDeclarationOrder() throws CTFException { + Map> flags = new LinkedHashMap<>(); + flags.put("high", List.of(new IntegerRange(7, 7))); + flags.put("low", List.of(new IntegerRange(0, 0))); + BitMapDeclaration declaration = new BitMapDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8, flags); + assertEquals(List.of("high", "low"), read(declaration, (byte) 0b1000_0001).getActiveFlags()); + } + + /** + * A bit may have more than one associated name, all of them are active + * when the bit is set + */ + @Test + public void testOverlappingFlags() throws CTFException { + Map> flags = new LinkedHashMap<>(); + flags.put("low", List.of(new IntegerRange(0, 3))); + flags.put("first", List.of(new IntegerRange(0, 0))); + flags.put("second", List.of(new IntegerRange(1, 1))); + BitMapDeclaration declaration = new BitMapDeclaration(4, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1, flags); + assertEquals(List.of("low", "first"), read(declaration, (byte) 0b0001).getActiveFlags()); + assertEquals(List.of("low", "second"), read(declaration, (byte) 0b0010).getActiveFlags()); + assertEquals(List.of("low"), read(declaration, (byte) 0b1000).getActiveFlags()); + } + + /** + * An unknown flag name is never active + */ + @Test + public void testUnknownFlag() throws CTFException { + BitMapDefinition definition = read(declaration(ByteOrder.LITTLE_ENDIAN), (byte) 0xff); + assertFalse(definition.isFlagActive("unknown")); + } + + /** + * Within a little-endian byte, the bits are decoded from the least + * significant to the most significant bits + */ + @Test + public void testLittleEndianSubByte() throws CTFException { + Map> flags = new LinkedHashMap<>(); + flags.put("first", List.of(new IntegerRange(0, 0))); + flags.put("last", List.of(new IntegerRange(3, 3))); + BitMapDeclaration declaration = new BitMapDeclaration(4, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1, flags); + BitBuffer input = createBuffer((byte) 0b1000_0001); + assertEquals(List.of("first"), read(declaration, input).getActiveFlags()); + assertEquals(List.of("last"), read(declaration, input).getActiveFlags()); + } + + /** + * Within a big-endian byte, the bits are decoded from the most significant + * to the least significant bits + */ + @Test + public void testBigEndianSubByte() throws CTFException { + Map> flags = new LinkedHashMap<>(); + flags.put("first", List.of(new IntegerRange(0, 0))); + flags.put("last", List.of(new IntegerRange(3, 3))); + BitMapDeclaration declaration = new BitMapDeclaration(4, ByteOrder.BIG_ENDIAN, BitOrder.LAST_TO_FIRST, 1, flags); + BitBuffer input = createBuffer((byte) 0b1000_0001); + assertEquals(List.of("last"), read(declaration, input).getActiveFlags()); + assertEquals(List.of("first"), read(declaration, input).getActiveFlags()); + } + + @Test + public void testLittleEndianMultiByte() throws CTFException { + Map> flags = new LinkedHashMap<>(); + flags.put("low", List.of(new IntegerRange(0, 7))); + flags.put("high", List.of(new IntegerRange(8, 15))); + BitMapDeclaration declaration = new BitMapDeclaration(16, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8, flags); + BitMapDefinition definition = read(declaration, (byte) 0, (byte) 0x01); + assertArrayEquals(new byte[] { 0x00, 0x01 }, definition.getValue()); + assertEquals(List.of("high"), definition.getActiveFlags()); + } + + @Test + public void testBigEndianMultiByte() throws CTFException { + Map> flags = new LinkedHashMap<>(); + flags.put("low", List.of(new IntegerRange(0, 7))); + flags.put("high", List.of(new IntegerRange(8, 15))); + BitMapDeclaration declaration = new BitMapDeclaration(16, ByteOrder.BIG_ENDIAN, BitOrder.LAST_TO_FIRST, 8, flags); + BitMapDefinition definition = read(declaration, (byte) 0, (byte) 0x01); + assertArrayEquals(new byte[] { 0x01, 0x00 }, definition.getValue()); + assertEquals(List.of("low"), definition.getActiveFlags()); + } + + /** + * The most significant bit of a 64-bit map is a regular bit + */ + @Test + public void test64Bits() throws CTFException { + Map> flags = new LinkedHashMap<>(); + flags.put("first", List.of(new IntegerRange(0, 0))); + flags.put("last", List.of(new IntegerRange(63, 63))); + BitMapDeclaration declaration = new BitMapDeclaration(64, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8, flags); + BitMapDefinition definition = read(declaration, (byte) 0, (byte) 0, (byte) 0, (byte) 0, + (byte) 0, (byte) 0, (byte) 0, (byte) 0x80); + assertEquals(List.of("last"), definition.getActiveFlags()); + } + + /** + * The length of a bit map is only required to be greater than zero, so + * flags beyond the 64th bit must be decoded + */ + @Test + public void testLittleEndianLongerThan64Bits() throws CTFException { + Map> flags = new LinkedHashMap<>(); + flags.put("first", List.of(new IntegerRange(0, 0))); + flags.put("middle", List.of(new IntegerRange(64, 64))); + flags.put("last", List.of(new IntegerRange(71, 71))); + BitMapDeclaration declaration = new BitMapDeclaration(72, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8, flags); + BitBuffer input = createBuffer((byte) 0x01, (byte) 0, (byte) 0, (byte) 0, + (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0x80); + BitMapDefinition definition = read(declaration, input); + assertEquals(List.of("first", "last"), definition.getActiveFlags()); + assertEquals(72, input.position()); + } + + /** + * The length of a bit map is only required to be greater than zero, so + * flags beyond the 64th bit must be decoded + */ + @Test + public void testBigEndianLongerThan64Bits() throws CTFException { + Map> flags = new LinkedHashMap<>(); + flags.put("first", List.of(new IntegerRange(0, 0))); + flags.put("middle", List.of(new IntegerRange(64, 64))); + flags.put("last", List.of(new IntegerRange(71, 71))); + BitMapDeclaration declaration = new BitMapDeclaration(72, ByteOrder.BIG_ENDIAN, BitOrder.LAST_TO_FIRST, 8, flags); + BitBuffer input = createBuffer((byte) 0x80, (byte) 0, (byte) 0, (byte) 0, + (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0x01); + BitMapDefinition definition = read(declaration, input); + assertEquals(List.of("first", "last"), definition.getActiveFlags()); + assertEquals(72, input.position()); + } + + /** + * The first bit of an instance is aligned relative to the beginning of the + * packet + */ + @Test + public void testAlignment() throws CTFException { + BitMapDeclaration declaration = new BitMapDeclaration(8, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 16, flags()); + BitBuffer input = createBuffer((byte) 0xff, (byte) 0xff, (byte) 0x80); + input.position(3); + assertEquals(List.of("error"), read(declaration, input).getActiveFlags()); + assertEquals(24, input.position()); + } + + /** + * The default alignment of 1 does not skip any bit + */ + @Test + public void testDefaultAlignment() throws CTFException { + Map> flags = new LinkedHashMap<>(); + flags.put("first", List.of(new IntegerRange(0, 0))); + flags.put("second", List.of(new IntegerRange(1, 1))); + BitMapDeclaration declaration = new BitMapDeclaration(2, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 1, flags); + BitBuffer input = createBuffer((byte) 0b0000_0100); + input.position(1); + assertEquals(List.of("second"), read(declaration, input).getActiveFlags()); + assertEquals(3, input.position()); + } + + /** + * The byte order of the input buffer is restored after reading + */ + @Test + public void testByteOrderRestored() throws CTFException { + BitBuffer input = new BitBuffer(ByteBuffer.wrap(new byte[] { 1 }), ByteOrder.LITTLE_ENDIAN); + read(declaration(ByteOrder.BIG_ENDIAN), input); + assertEquals(ByteOrder.LITTLE_ENDIAN, input.getByteOrder()); + } + + /** + * A bit map field is a bit array field, its bits remain accessible + */ + @Test + public void testBitArrayValue() throws CTFException { + BitMapDefinition definition = read(declaration(ByteOrder.LITTLE_ENDIAN), (byte) 0b1000_0101); + assertArrayEquals(new byte[] { (byte) 0x85 }, definition.getValue()); + assertTrue(definition.isBitSet(0)); + assertFalse(definition.isBitSet(1)); + assertTrue(definition.isBitSet(2)); + assertTrue(definition.isBitSet(7)); + assertFalse(definition.isBitSet(8)); + } + + /** + * The string contains the bits and the names of the active flags + */ + @Test + public void testToString() throws CTFException { + BitMapDefinition definition = read(declaration(ByteOrder.LITTLE_ENDIAN), (byte) 0b1000_0001); + assertEquals("0b10000001 (active | error)", definition.toString()); + } + + @Test + public void testGetDeclaration() throws CTFException { + BitMapDeclaration declaration = declaration(ByteOrder.LITTLE_ENDIAN); + BitMapDefinition definition = read(declaration, (byte) 1); + assertSame(declaration, definition.getDeclaration()); + assertEquals(8, definition.size()); + } + + /** + * Reading past the end of the buffer fails + */ + @Test(expected = CTFException.class) + public void testBufferTooSmall() throws CTFException { + Map> flags = Map.of("flag", List.of(new IntegerRange(0, 0))); + read(new BitMapDeclaration(16, ByteOrder.LITTLE_ENDIAN, BitOrder.FIRST_TO_LAST, 8, flags), (byte) 1); + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/FieldClassAliasTest.java b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/FieldClassAliasTest.java new file mode 100644 index 0000000000..2b7dedb40c --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/FieldClassAliasTest.java @@ -0,0 +1,650 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.ctf.core.tests.types; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import org.eclipse.tracecompass.ctf.core.CTFException; +import org.eclipse.tracecompass.ctf.core.event.IEventDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.IDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.IntegerDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.OptionalDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.StructDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.VariantDeclaration; +import org.eclipse.tracecompass.ctf.core.trace.CTFTrace; +import org.eclipse.tracecompass.ctf.core.trace.ICTFStream; +import org.eclipse.tracecompass.internal.ctf.core.event.types.ArrayDeclaration; +import org.eclipse.tracecompass.internal.ctf.core.event.types.SequenceDeclaration; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +/** + * Test the CTF2 field class aliases, following the specification: + * https://diamon.org/ctf/#field-class-alias-frag + * + * The following JSON object properties may refer to the effective field class + * of a previously occurring field class alias using its name: + *
    + *
  • field class alias fragment: field-class
  • + *
  • trace class fragment: packet-header-field-class
  • + *
  • data stream class fragment: packet-context-field-class, + * event-record-header-field-class and + * event-record-common-context-field-class
  • + *
  • event record class fragment: specific-context-field-class and + * payload-field-class
  • + *
  • structure field member class: field-class
  • + *
  • abstract array field class: element-field-class
  • + *
  • optional field class: field-class
  • + *
  • variant field class option: field-class
  • + *
+ * + * Each test writes the metadata stream of a trace without data streams and + * checks the declarations created when opening the trace. + * + * @author Arnaud Fiorini + */ +public class FieldClassAliasTest { + + private static final char RECORD_SEPARATOR = '\u001e'; + + /** + * Temporary folder holding the trace + */ + @Rule + public TemporaryFolder fFolder = new TemporaryFolder(); + + // ------------------------------------------------------------------------ + // Helpers + // ------------------------------------------------------------------------ + + /** + * Write the metadata stream made of the given fragments in a new trace + * directory and open the trace + * + * @param fragments + * the fragments of the metadata stream, in order + * @return the opened trace + * @throws IOException + * if the metadata stream cannot be written + * @throws CTFException + * if the trace cannot be opened + */ + private CTFTrace openTrace(JsonObject... fragments) throws IOException, CTFException { + File traceDirectory = fFolder.newFolder(); + StringBuilder metadata = new StringBuilder(); + for (JsonObject fragment : fragments) { + metadata.append(RECORD_SEPARATOR).append(fragment.toString()); + } + Files.writeString(new File(traceDirectory, "metadata").toPath(), metadata.toString(), StandardCharsets.UTF_8); + return new CTFTrace(traceDirectory); + } + + /** + * Create a CTF2 preamble fragment + */ + private static JsonObject preamble() { + JsonObject preamble = new JsonObject(); + preamble.addProperty("type", "preamble"); + preamble.addProperty("version", 2); + return preamble; + } + + /** + * Create a trace class fragment without packet header + */ + private static JsonObject traceClass() { + JsonObject traceClass = new JsonObject(); + traceClass.addProperty("type", "trace-class"); + return traceClass; + } + + /** + * Create the data stream class fragment of ID 0 without field classes + */ + private static JsonObject dataStreamClass() { + JsonObject dataStreamClass = new JsonObject(); + dataStreamClass.addProperty("type", "data-stream-class"); + dataStreamClass.addProperty("id", 0); + return dataStreamClass; + } + + /** + * Create an event record class fragment of data stream class 0 without + * payload + */ + private static JsonObject eventRecordClass() { + JsonObject eventRecordClass = new JsonObject(); + eventRecordClass.addProperty("type", "event-record-class"); + eventRecordClass.addProperty("id", 0); + eventRecordClass.addProperty("data-stream-class-id", 0); + eventRecordClass.addProperty("name", "event"); + return eventRecordClass; + } + + /** + * Create an event record class fragment of data stream class 0 with the + * given payload member classes + */ + private static JsonObject eventRecordClass(JsonObject... payloadMembers) { + JsonObject eventRecordClass = eventRecordClass(); + eventRecordClass.add("payload-field-class", structure(payloadMembers)); + return eventRecordClass; + } + + /** + * Create a field class alias fragment + * + * @param name + * the name of the alias + * @param fieldClass + * the aliased field class, or a reference to another alias + */ + private static JsonObject fieldClassAlias(String name, JsonElement fieldClass) { + JsonObject alias = new JsonObject(); + alias.addProperty("type", "field-class-alias"); + alias.addProperty("name", name); + alias.add("field-class", fieldClass); + return alias; + } + + /** + * Create a little-endian, byte-aligned fixed-length unsigned integer field + * class + * + * @param length + * the length in bits + */ + private static JsonObject unsignedInteger(int length) { + JsonObject integer = new JsonObject(); + integer.addProperty("type", "fixed-length-unsigned-integer"); + integer.addProperty("length", length); + integer.addProperty("byte-order", "little-endian"); + integer.addProperty("alignment", 8); + return integer; + } + + /** + * Create a structure field class + * + * @param members + * the member classes, in order + */ + private static JsonObject structure(JsonObject... members) { + JsonArray memberClasses = new JsonArray(); + for (JsonObject member : members) { + memberClasses.add(member); + } + JsonObject structure = new JsonObject(); + structure.addProperty("type", "structure"); + structure.add("member-classes", memberClasses); + return structure; + } + + /** + * Create a structure field member class + * + * @param name + * the name of the member + * @param fieldClass + * the field class of the member, or a reference to an alias + */ + private static JsonObject member(String name, JsonElement fieldClass) { + JsonObject member = new JsonObject(); + member.addProperty("name", name); + member.add("field-class", fieldClass); + return member; + } + + /** + * Create a reference to a field class alias + */ + private static JsonPrimitive aliasRef(String name) { + return new JsonPrimitive(name); + } + + /** + * Create a field location of a field at the root of the event record + * payload + * + * @param fieldName + * the name of the field + */ + private static JsonObject payloadLocation(String fieldName) { + JsonArray path = new JsonArray(); + path.add(fieldName); + JsonObject location = new JsonObject(); + location.addProperty("origin", "event-record-payload"); + location.add("path", path); + return location; + } + + /** + * Create an integer range set containing a single value + * + * @param value + * the value + */ + private static JsonArray singleValueRange(long value) { + JsonArray range = new JsonArray(); + range.add(value); + range.add(value); + JsonArray ranges = new JsonArray(); + ranges.add(range); + return ranges; + } + + /** + * Field class alias named "uint8" of an 8-bit unsigned integer + */ + private static JsonObject uint8Alias() { + return fieldClassAlias("uint8", unsignedInteger(8)); + } + + /** + * Field class alias named "uint16" of a 16-bit unsigned integer + */ + private static JsonObject uint16Alias() { + return fieldClassAlias("uint16", unsignedInteger(16)); + } + + /** + * Structure alias named "value-struct" whose single member is named + * "value" and refers to the "uint16" alias + */ + private static JsonObject structAlias() { + return fieldClassAlias("value-struct", structure(member("value", aliasRef("uint16")))); + } + + /** + * Get the single event record class of data stream class 0 + */ + private static IEventDeclaration getEvent(CTFTrace trace) { + ICTFStream stream = trace.getStream(0L); + assertNotNull(stream); + IEventDeclaration event = stream.getEventDeclarations().get(0); + assertNotNull(event); + return event; + } + + /** + * Get the declaration of a field of the payload of the single event record + * class + */ + private static IDeclaration getPayloadField(CTFTrace trace, String fieldName) { + StructDeclaration payload = getEvent(trace).getFields(); + assertNotNull(payload); + IDeclaration field = payload.getField(fieldName); + assertNotNull("Missing payload field " + fieldName, field); + return field; + } + + /** + * Assert that a declaration is an integer of the expected length + */ + private static void assertInteger(IDeclaration declaration, int expectedLength) { + assertTrue(declaration instanceof IntegerDeclaration); + assertEquals(expectedLength, ((IntegerDeclaration) declaration).getLength()); + } + + /** + * Assert that a structure is the one of {@link #structAlias()} + */ + private static void assertValueStruct(IDeclaration declaration) { + assertTrue(declaration instanceof StructDeclaration); + IDeclaration value = ((StructDeclaration) declaration).getField("value"); + assertNotNull(value); + assertInteger(value, 16); + } + + // ------------------------------------------------------------------------ + // Field class alias fragment + // ------------------------------------------------------------------------ + + /** + * The field-class property of a field class alias fragment may refer to a + * previously occurring field class alias + * + * @throws IOException + * if the metadata stream cannot be written + * @throws CTFException + * if the trace cannot be opened + */ + @Test + public void testFieldClassAliasOfAlias() throws IOException, CTFException { + JsonObject byteAlias = fieldClassAlias("byte", aliasRef("uint8")); + CTFTrace trace = openTrace(preamble(), uint8Alias(), byteAlias, traceClass(), dataStreamClass(), + eventRecordClass(member("field", aliasRef("byte")))); + + assertInteger(getPayloadField(trace, "field"), 8); + } + + /** + * A field class alias may be a structure using other aliases. Structures + * nested in the payload are flattened, the member of the aliased structure + * is then named "field.value". + * + * @throws IOException + * if the metadata stream cannot be written + * @throws CTFException + * if the trace cannot be opened + */ + @Test + public void testStructureFieldClassAlias() throws IOException, CTFException { + CTFTrace trace = openTrace(preamble(), uint16Alias(), structAlias(), traceClass(), dataStreamClass(), + eventRecordClass(member("field", aliasRef("value-struct")))); + + assertInteger(getPayloadField(trace, "field.value"), 16); + } + + // ------------------------------------------------------------------------ + // Trace class fragment + // ------------------------------------------------------------------------ + + /** + * The packet-header-field-class property of a trace class fragment may + * refer to a field class alias + * + * @throws IOException + * if the metadata stream cannot be written + * @throws CTFException + * if the trace cannot be opened + */ + @Test + public void testPacketHeaderFieldClass() throws IOException, CTFException { + JsonObject traceClass = traceClass(); + traceClass.add("packet-header-field-class", aliasRef("value-struct")); + CTFTrace trace = openTrace(preamble(), uint16Alias(), structAlias(), traceClass, dataStreamClass(), + eventRecordClass()); + + assertValueStruct(trace.getPacketHeader()); + } + + // ------------------------------------------------------------------------ + // Data stream class fragment + // ------------------------------------------------------------------------ + + /** + * The packet-context-field-class property of a data stream class fragment + * may refer to a field class alias + * + * @throws IOException + * if the metadata stream cannot be written + * @throws CTFException + * if the trace cannot be opened + */ + @Test + public void testPacketContextFieldClass() throws IOException, CTFException { + JsonObject dataStreamClass = dataStreamClass(); + dataStreamClass.add("packet-context-field-class", aliasRef("value-struct")); + CTFTrace trace = openTrace(preamble(), uint16Alias(), structAlias(), traceClass(), dataStreamClass, + eventRecordClass()); + + assertValueStruct(trace.getStream(0L).getPacketContextDecl()); + } + + /** + * The event-record-header-field-class property of a data stream class + * fragment may refer to a field class alias + * + * @throws IOException + * if the metadata stream cannot be written + * @throws CTFException + * if the trace cannot be opened + */ + @Test + public void testEventRecordHeaderFieldClass() throws IOException, CTFException { + JsonObject dataStreamClass = dataStreamClass(); + dataStreamClass.add("event-record-header-field-class", aliasRef("value-struct")); + CTFTrace trace = openTrace(preamble(), uint16Alias(), structAlias(), traceClass(), dataStreamClass, + eventRecordClass()); + + assertValueStruct(trace.getStream(0L).getEventHeaderDeclaration()); + } + + /** + * The event-record-common-context-field-class property of a data stream + * class fragment may refer to a field class alias + * + * @throws IOException + * if the metadata stream cannot be written + * @throws CTFException + * if the trace cannot be opened + */ + @Test + public void testEventRecordCommonContextFieldClass() throws IOException, CTFException { + JsonObject dataStreamClass = dataStreamClass(); + dataStreamClass.add("event-record-common-context-field-class", aliasRef("value-struct")); + CTFTrace trace = openTrace(preamble(), uint16Alias(), structAlias(), traceClass(), dataStreamClass, + eventRecordClass()); + + assertValueStruct(trace.getStream(0L).getEventContextDecl()); + } + + // ------------------------------------------------------------------------ + // Event record class fragment + // ------------------------------------------------------------------------ + + /** + * The specific-context-field-class property of an event record class + * fragment may refer to a field class alias + * + * @throws IOException + * if the metadata stream cannot be written + * @throws CTFException + * if the trace cannot be opened + */ + @Test + public void testSpecificContextFieldClass() throws IOException, CTFException { + JsonObject eventRecordClass = eventRecordClass(); + eventRecordClass.add("specific-context-field-class", aliasRef("value-struct")); + CTFTrace trace = openTrace(preamble(), uint16Alias(), structAlias(), traceClass(), dataStreamClass(), eventRecordClass); + + assertValueStruct(getEvent(trace).getContext()); + } + + /** + * The payload-field-class property of an event record class fragment may + * refer to a field class alias + * + * @throws IOException + * if the metadata stream cannot be written + * @throws CTFException + * if the trace cannot be opened + */ + @Test + public void testPayloadFieldClass() throws IOException, CTFException { + JsonObject eventRecordClass = eventRecordClass(); + eventRecordClass.add("payload-field-class", aliasRef("value-struct")); + CTFTrace trace = openTrace(preamble(), uint16Alias(), structAlias(), traceClass(), dataStreamClass(), eventRecordClass); + + assertValueStruct(getEvent(trace).getFields()); + } + + // ------------------------------------------------------------------------ + // Field classes + // ------------------------------------------------------------------------ + + /** + * The field-class property of a structure field member class may refer to + * a field class alias + * + * @throws IOException + * if the metadata stream cannot be written + * @throws CTFException + * if the trace cannot be opened + */ + @Test + public void testStructureMemberFieldClass() throws IOException, CTFException { + CTFTrace trace = openTrace(preamble(), uint8Alias(), uint16Alias(), traceClass(), dataStreamClass(), + eventRecordClass(member("small", aliasRef("uint8")), member("big", aliasRef("uint16")))); + + assertInteger(getPayloadField(trace, "small"), 8); + assertInteger(getPayloadField(trace, "big"), 16); + } + + /** + * The element-field-class property of a static-length array field class + * may refer to a field class alias + * + * @throws IOException + * if the metadata stream cannot be written + * @throws CTFException + * if the trace cannot be opened + */ + @Test + public void testStaticLengthArrayElementFieldClass() throws IOException, CTFException { + JsonObject array = new JsonObject(); + array.addProperty("type", "static-length-array"); + array.addProperty("length", 4); + array.add("element-field-class", aliasRef("uint16")); + CTFTrace trace = openTrace(preamble(), uint16Alias(), traceClass(), dataStreamClass(), + eventRecordClass(member("array", array))); + + IDeclaration declaration = getPayloadField(trace, "array"); + assertTrue(declaration instanceof ArrayDeclaration); + assertEquals(4, ((ArrayDeclaration) declaration).getLength()); + assertInteger(((ArrayDeclaration) declaration).getElementType(), 16); + } + + /** + * The element-field-class property of a dynamic-length array field class + * may refer to a field class alias + * + * @throws IOException + * if the metadata stream cannot be written + * @throws CTFException + * if the trace cannot be opened + */ + @Test + public void testDynamicLengthArrayElementFieldClass() throws IOException, CTFException { + JsonObject array = new JsonObject(); + array.addProperty("type", "dynamic-length-array"); + array.add("length-field-location", payloadLocation("length")); + array.add("element-field-class", aliasRef("uint16")); + CTFTrace trace = openTrace(preamble(), uint8Alias(), uint16Alias(), traceClass(), dataStreamClass(), + eventRecordClass(member("length", aliasRef("uint8")), member("array", array))); + + IDeclaration declaration = getPayloadField(trace, "array"); + assertTrue(declaration instanceof SequenceDeclaration); + assertEquals("length", ((SequenceDeclaration) declaration).getLengthName()); + assertInteger(((SequenceDeclaration) declaration).getElementType(), 16); + } + + /** + * The field-class property of an optional field class may refer to a field + * class alias + * + * @throws IOException + * if the metadata stream cannot be written + * @throws CTFException + * if the trace cannot be opened + */ + @Test + public void testOptionalFieldClass() throws IOException, CTFException { + JsonObject optional = new JsonObject(); + optional.addProperty("type", "optional"); + optional.add("selector-field-location", payloadLocation("selector")); + optional.add("selector-field-ranges", singleValueRange(1)); + optional.add("field-class", aliasRef("uint16")); + CTFTrace trace = openTrace(preamble(), uint8Alias(), uint16Alias(), traceClass(), dataStreamClass(), + eventRecordClass(member("selector", aliasRef("uint8")), member("optional", optional))); + + IDeclaration declaration = getPayloadField(trace, "optional"); + assertTrue(declaration instanceof OptionalDeclaration); + assertInteger(((OptionalDeclaration) declaration).getFieldDeclaration(), 16); + } + + /** + * The field-class property of a variant field class option may refer to a + * field class alias + * + * @throws IOException + * if the metadata stream cannot be written + * @throws CTFException + * if the trace cannot be opened + */ + @Test + public void testVariantOptionFieldClass() throws IOException, CTFException { + JsonObject smallOption = member("small", aliasRef("uint8")); + smallOption.add("selector-field-ranges", singleValueRange(0)); + JsonObject bigOption = member("big", aliasRef("uint16")); + bigOption.add("selector-field-ranges", singleValueRange(1)); + JsonArray options = new JsonArray(); + options.add(smallOption); + options.add(bigOption); + + JsonObject variant = new JsonObject(); + variant.addProperty("type", "variant"); + variant.add("selector-field-location", payloadLocation("selector")); + variant.add("options", options); + CTFTrace trace = openTrace(preamble(), uint8Alias(), uint16Alias(), traceClass(), dataStreamClass(), + eventRecordClass(member("selector", aliasRef("uint8")), member("variant", variant))); + + IDeclaration declaration = getPayloadField(trace, "variant"); + assertTrue(declaration instanceof VariantDeclaration); + VariantDeclaration variantDeclaration = (VariantDeclaration) declaration; + assertInteger(variantDeclaration.getFields().get("small"), 8); + assertInteger(variantDeclaration.getFields().get("big"), 16); + } + + // ------------------------------------------------------------------------ + // Alias resolution rules + // ------------------------------------------------------------------------ + + /** + * A field class may only refer to a previously occurring field class alias + * + * @throws IOException + * if the metadata stream cannot be written + */ + @Test + public void testAliasMustPreviouslyOccur() throws IOException { + try { + openTrace(preamble(), traceClass(), dataStreamClass(), + eventRecordClass(member("field", aliasRef("uint8"))), + uint8Alias()); + fail("A field class cannot refer to an alias defined after it"); + } catch (CTFException e) { + // Expected + } + } + + /** + * A string which is not one of the properties listed by the specification + * is not replaced, even if it is the name of a field class alias + * + * @throws IOException + * if the metadata stream cannot be written + * @throws CTFException + * if the trace cannot be opened + */ + @Test + public void testOnlyFieldClassPropertiesAreResolved() throws IOException, CTFException { + CTFTrace trace = openTrace(preamble(), uint8Alias(), traceClass(), dataStreamClass(), + eventRecordClass(member("uint8", aliasRef("uint8")))); + + assertInteger(getPayloadField(trace, "uint8"), 8); + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/OptionalDeclarationParserTest.java b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/OptionalDeclarationParserTest.java new file mode 100644 index 0000000000..394290fa87 --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/OptionalDeclarationParserTest.java @@ -0,0 +1,319 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.ctf.core.tests.types; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.eclipse.tracecompass.ctf.core.event.metadata.DeclarationScope; +import org.eclipse.tracecompass.ctf.core.event.types.IntegerDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.IntegerRange; +import org.eclipse.tracecompass.ctf.core.event.types.OptionalDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.StringDeclaration; +import org.eclipse.tracecompass.ctf.core.trace.CTFTrace; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.CTFJsonMetadataNode; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.JsonStructureFieldMemberMetadataNode; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.ParseException; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.optional.OptionalDeclarationParser; +import org.eclipse.tracecompass.internal.ctf.core.event.types.ArrayDeclaration; +import org.eclipse.tracecompass.internal.ctf.core.utils.JsonMetadataStrings; +import org.junit.Test; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +/** + * Test class for the optional declaration parser in CTF2, following the + * specification: https://diamon.org/ctf/#opt-fc + * + * @author Arnaud Fiorini + */ +public class OptionalDeclarationParserTest { + + // ------------------------------------------------------------------------ + // Helpers + // ------------------------------------------------------------------------ + + private static OptionalDeclaration parse(JsonStructureFieldMemberMetadataNode node) throws ParseException { + return OptionalDeclarationParser.INSTANCE.parse(node, + new OptionalDeclarationParser.Param(new CTFTrace(), new DeclarationScope(null, "test"))); + } + + private static OptionalDeclaration parse(JsonObject fieldClass) throws ParseException { + return parse(createNode(null, fieldClass)); + } + + private static JsonStructureFieldMemberMetadataNode createNode(CTFJsonMetadataNode parent, JsonObject fieldClass) { + return new JsonStructureFieldMemberMetadataNode(parent, JsonMetadataStrings.OPTIONAL, "test", "ip", fieldClass); + } + + /** + * Create a selector field location in the event record payload + */ + private static JsonObject selectorFieldLocation(String... path) { + JsonArray pathArray = new JsonArray(); + for (String element : path) { + pathArray.add(element); + } + JsonObject location = new JsonObject(); + location.addProperty("origin", "event-record-payload"); + location.add("path", pathArray); + return location; + } + + /** + * Create an integer range set from pairs of lower and upper bounds + */ + private static JsonArray ranges(long... bounds) { + JsonArray ranges = new JsonArray(); + for (int i = 0; i < bounds.length; i += 2) { + JsonArray range = new JsonArray(); + range.add(bounds[i]); + range.add(bounds[i + 1]); + ranges.add(range); + } + return ranges; + } + + private static JsonObject nullTerminatedString() { + JsonObject string = new JsonObject(); + string.addProperty("type", "null-terminated-string"); + return string; + } + + /** + * Create an optional field class, the null properties are omitted + */ + private static JsonObject optional(JsonObject location, JsonArray selectorRanges, JsonElement fieldClass) { + JsonObject optional = new JsonObject(); + optional.addProperty("type", "optional"); + if (location != null) { + optional.add("selector-field-location", location); + } + if (selectorRanges != null) { + optional.add("selector-field-ranges", selectorRanges); + } + if (fieldClass != null) { + optional.add("field-class", fieldClass); + } + return optional; + } + + /** + * Optional null-terminated string with a fixed-length signed integer + * selector field class (example 61 of the specification) + */ + private static JsonObject integerSelectorExample() { + return optional(selectorFieldLocation("has-ip"), ranges(-12, -12, -5, 0, 15, 35), nullTerminatedString()); + } + + // ------------------------------------------------------------------------ + // Tests + // ------------------------------------------------------------------------ + + /** + * Test parsing an optional static-length array field class with a boolean + * selector field class (example 60 of the specification) + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testBooleanSelector() throws ParseException { + JsonObject element = new JsonObject(); + element.addProperty("type", "fixed-length-unsigned-integer"); + element.addProperty("length", 8); + element.addProperty("byte-order", "little-endian"); + element.addProperty("alignment", 8); + + JsonObject array = new JsonObject(); + array.addProperty("type", "static-length-array"); + array.add("element-field-class", element); + array.addProperty("length", 16); + + OptionalDeclaration result = parse(optional(selectorFieldLocation("has-ip"), null, array)); + + assertNotNull(result); + assertEquals("has-ip", result.getSelectorName()); + assertNull(result.getSelectorRanges()); + assertTrue(result.getFieldDeclaration() instanceof ArrayDeclaration); + ArrayDeclaration arrayDeclaration = (ArrayDeclaration) result.getFieldDeclaration(); + assertEquals(16, arrayDeclaration.getLength()); + assertTrue(arrayDeclaration.getElementType() instanceof IntegerDeclaration); + } + + /** + * Test parsing an optional null-terminated string field class with a + * fixed-length signed integer selector field class (example 61 of the + * specification) + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testIntegerSelector() throws ParseException { + OptionalDeclaration result = parse(integerSelectorExample()); + + assertNotNull(result); + assertEquals("has-ip", result.getSelectorName()); + assertTrue(result.getFieldDeclaration() instanceof StringDeclaration); + + List ranges = result.getSelectorRanges(); + assertNotNull(ranges); + assertEquals(3, ranges.size()); + assertEquals(-12, ranges.get(0).getStart()); + assertEquals(-12, ranges.get(0).getEnd()); + assertEquals(-5, ranges.get(1).getStart()); + assertEquals(0, ranges.get(1).getEnd()); + assertEquals(15, ranges.get(2).getStart()); + assertEquals(35, ranges.get(2).getEnd()); + } + + /** + * Test that the selector name is the last element of the selector field + * location path + * + * @throws ParseException + * if parsing fails + */ + @Test + public void testSelectorLocationPath() throws ParseException { + OptionalDeclaration result = parse(optional(selectorFieldLocation("header", "flags", "enabled"), null, nullTerminatedString())); + + assertEquals("enabled", result.getSelectorName()); + } + + /** + * Test that an unknown field class alias is rejected + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testUnknownFieldClassAlias() throws ParseException { + CTFJsonMetadataNode root = new CTFJsonMetadataNode(null, "root", ""); + parse(createNode(root, optional(selectorFieldLocation("has-value"), null, new JsonPrimitive("unknown")))); + } + + /** + * Test that the field-class property is required + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testMissingFieldClass() throws ParseException { + parse(optional(selectorFieldLocation("has-ip"), null, null)); + } + + /** + * Test that the field-class property must be a field class or a string + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testInvalidFieldClass() throws ParseException { + parse(optional(selectorFieldLocation("has-ip"), null, new JsonPrimitive(42))); + } + + /** + * Test that the selector-field-location property is required + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testMissingSelectorFieldLocation() throws ParseException { + parse(optional(null, null, nullTerminatedString())); + } + + /** + * Test that the selector-field-location path must not be empty + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testEmptySelectorFieldLocationPath() throws ParseException { + parse(optional(selectorFieldLocation(), null, nullTerminatedString())); + } + + /** + * Test that the selector-field-ranges property must not be empty + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testEmptySelectorFieldRanges() throws ParseException { + parse(optional(selectorFieldLocation("has-ip"), ranges(), nullTerminatedString())); + } + + /** + * Test that a selector field range with a lower bound greater than its + * upper bound is rejected + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testInvalidSelectorFieldRange() throws ParseException { + parse(optional(selectorFieldLocation("has-ip"), ranges(5, 2), nullTerminatedString())); + } + + /** + * Test that a malformed selector field range is rejected + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testMalformedSelectorFieldRange() throws ParseException { + JsonArray range = new JsonArray(); + range.add(1); + range.add(2); + range.add(3); + JsonArray selectorRanges = new JsonArray(); + selectorRanges.add(range); + + parse(optional(selectorFieldLocation("has-ip"), selectorRanges, nullTerminatedString())); + } + + /** + * Test that the optional field class is not supported in CTF 1.8 + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = ParseException.class) + public void testCTF1NotSupported() throws ParseException { + OptionalDeclarationParser.INSTANCE.parse(new CTFJsonMetadataNode(null, "optional", ""), + new OptionalDeclarationParser.Param(new CTFTrace(), new DeclarationScope(null, "test"))); + } + + /** + * Test that the parser requires its own parameter type + * + * @throws ParseException + * if parsing fails + */ + @Test(expected = IllegalArgumentException.class) + public void testInvalidParam() throws ParseException { + OptionalDeclarationParser.INSTANCE.parse(createNode(null, integerSelectorExample()), null); + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/OptionalDeclarationTest.java b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/OptionalDeclarationTest.java new file mode 100644 index 0000000000..d3f4566f58 --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/OptionalDeclarationTest.java @@ -0,0 +1,82 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.ctf.core.tests.types; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.nio.ByteOrder; +import java.util.List; + +import org.eclipse.tracecompass.ctf.core.event.types.BooleanDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.Encoding; +import org.eclipse.tracecompass.ctf.core.event.types.IntegerDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.IntegerRange; +import org.eclipse.tracecompass.ctf.core.event.types.OptionalDeclaration; +import org.junit.Test; + +/** + * Test for the optional declaration class in CTF2 + * + * @author Arnaud Fiorini + */ +@SuppressWarnings("javadoc") +public class OptionalDeclarationTest { + + private static final IntegerDeclaration UINT_32 = IntegerDeclaration.createDeclaration(32, false, 10, ByteOrder.LITTLE_ENDIAN, Encoding.NONE, "", 8, null); + private static final List RANGES = List.of(new IntegerRange(-12, -12), new IntegerRange(-5, 0), new IntegerRange(15, 35)); + + @Test + public void constructorTest() { + assertNotNull(new OptionalDeclaration("has-ip", UINT_32, null)); + assertNotNull(new OptionalDeclaration("has-ip", UINT_32, RANGES)); + } + + @Test + public void getterTest() { + OptionalDeclaration declaration = new OptionalDeclaration("has-ip", UINT_32, RANGES); + assertEquals("has-ip", declaration.getSelectorName()); + assertSame(UINT_32, declaration.getFieldDeclaration()); + List selectorRanges = declaration.getSelectorRanges(); + assertNotNull(selectorRanges); + assertEquals(3, selectorRanges.size()); + assertEquals(15, selectorRanges.get(2).getStart()); + assertEquals(35, selectorRanges.get(2).getEnd()); + /* The enabled field aligns itself */ + assertEquals(1, declaration.getAlignment()); + assertEquals(32, declaration.getMaximumSize()); + + assertNull(new OptionalDeclaration("has-ip", UINT_32, null).getSelectorRanges()); + } + + @Test + public void binaryEquivalentTest() { + OptionalDeclaration a = new OptionalDeclaration("has-ip", UINT_32, RANGES); + OptionalDeclaration b = new OptionalDeclaration("has-ip", UINT_32, List.copyOf(RANGES)); + OptionalDeclaration c = new OptionalDeclaration("other", UINT_32, RANGES); + OptionalDeclaration d = new OptionalDeclaration("has-ip", IntegerDeclaration.UINT_8_DECL, RANGES); + OptionalDeclaration e = new OptionalDeclaration("has-ip", UINT_32, null); + BooleanDeclaration f = new BooleanDeclaration(32, ByteOrder.LITTLE_ENDIAN, 8); + + assertTrue(a.isBinaryEquivalent(a)); + assertTrue(a.isBinaryEquivalent(b)); + assertTrue(b.isBinaryEquivalent(a)); + assertFalse(a.isBinaryEquivalent(c)); + assertFalse(a.isBinaryEquivalent(d)); + assertFalse(a.isBinaryEquivalent(e)); + assertFalse(a.isBinaryEquivalent(f)); + assertFalse(a.isBinaryEquivalent(null)); + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/OptionalDefinitionTest.java b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/OptionalDefinitionTest.java new file mode 100644 index 0000000000..cf7d890ed9 --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/OptionalDefinitionTest.java @@ -0,0 +1,272 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.ctf.core.tests.types; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.tracecompass.ctf.core.CTFException; +import org.eclipse.tracecompass.ctf.core.event.io.BitBuffer; +import org.eclipse.tracecompass.ctf.core.event.types.BooleanDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.Definition; +import org.eclipse.tracecompass.ctf.core.event.types.Encoding; +import org.eclipse.tracecompass.ctf.core.event.types.IDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.IntegerDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.IntegerDefinition; +import org.eclipse.tracecompass.ctf.core.event.types.IntegerRange; +import org.eclipse.tracecompass.ctf.core.event.types.OptionalDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.OptionalDefinition; +import org.eclipse.tracecompass.ctf.core.event.types.StringDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.StringDefinition; +import org.eclipse.tracecompass.ctf.core.event.types.StructDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.StructDefinition; +import org.junit.Test; + +/** + * Tests for the optional definition class in CTF2, following the + * specification: https://diamon.org/ctf/#opt-fc + * + * Each test decodes a structure made of a selector field, an optional field + * and a trailing 8-bit field which validates that a disabled optional field + * is a zero-bit field. + * + * @author Arnaud Fiorini + */ +@SuppressWarnings("javadoc") +public class OptionalDefinitionTest { + + private static final @NonNull String SELECTOR = "has-ip"; + private static final @NonNull String OPTIONAL = "ip"; + private static final @NonNull String TAIL = "tail"; + + private static final @NonNull IntegerDeclaration UINT_8 = IntegerDeclaration.createDeclaration(8, false, 10, ByteOrder.LITTLE_ENDIAN, Encoding.NONE, "", 8, null); + private static final @NonNull IntegerDeclaration INT_8 = IntegerDeclaration.createDeclaration(8, true, 10, ByteOrder.LITTLE_ENDIAN, Encoding.NONE, "", 8, null); + private static final @NonNull IntegerDeclaration UINT_16 = IntegerDeclaration.createDeclaration(16, false, 10, ByteOrder.LITTLE_ENDIAN, Encoding.NONE, "", 8, null); + private static final @NonNull IntegerDeclaration UINT_64 = IntegerDeclaration.createDeclaration(64, false, 10, ByteOrder.LITTLE_ENDIAN, Encoding.NONE, "", 8, null); + private static final @NonNull BooleanDeclaration BOOL_8 = new BooleanDeclaration(8, ByteOrder.LITTLE_ENDIAN, 8); + private static final @NonNull StringDeclaration STRING = StringDeclaration.getStringDeclaration(Encoding.UTF8); + + private static final List RANGES = List.of(new IntegerRange(-12, -12), new IntegerRange(-5, 0), new IntegerRange(15, 35)); + + private static StructDefinition getStructDefinition(@NonNull IDeclaration selector, @NonNull OptionalDeclaration optional, byte... data) throws CTFException { + StructDeclaration struct = new StructDeclaration(8); + struct.addField(SELECTOR, selector); + struct.addField(OPTIONAL, optional); + struct.addField(TAIL, UINT_8); + ByteBuffer byb = ByteBuffer.wrap(data); + byb.order(ByteOrder.LITTLE_ENDIAN); + return struct.createDefinition(null, "payload", new BitBuffer(byb)); + } + + private static OptionalDefinition getOptionalDefinition(StructDefinition struct) { + Definition definition = struct.getDefinition(OPTIONAL); + assertTrue(definition instanceof OptionalDefinition); + return (OptionalDefinition) definition; + } + + private static long getTail(StructDefinition struct) { + Definition definition = struct.getDefinition(TAIL); + assertTrue(definition instanceof IntegerDefinition); + return ((IntegerDefinition) definition).getValue(); + } + + private static void assertEnabled(StructDefinition struct, long expectedValue) { + OptionalDefinition optional = getOptionalDefinition(struct); + assertTrue(optional.isEnabled()); + Definition field = optional.getField(); + assertNotNull(field); + assertTrue(field instanceof IntegerDefinition); + assertEquals(expectedValue, ((IntegerDefinition) field).getValue()); + assertEquals(String.valueOf(expectedValue), optional.toString()); + } + + private static void assertDisabled(StructDefinition struct) { + OptionalDefinition optional = getOptionalDefinition(struct); + assertFalse(optional.isEnabled()); + assertNull(optional.getField()); + assertEquals(0, optional.size()); + assertEquals("null", optional.toString()); + } + + /** + * An instance is enabled when a boolean selector is true + */ + @Test + public void testBooleanSelectorTrue() throws CTFException { + OptionalDeclaration declaration = new OptionalDeclaration(SELECTOR, UINT_16, null); + StructDefinition struct = getStructDefinition(BOOL_8, declaration, (byte) 1, (byte) 0x34, (byte) 0x12, (byte) 7); + assertEnabled(struct, 0x1234); + assertEquals(7, getTail(struct)); + } + + /** + * An instance is disabled when a boolean selector is false, and it is a + * zero-bit field + */ + @Test + public void testBooleanSelectorFalse() throws CTFException { + OptionalDeclaration declaration = new OptionalDeclaration(SELECTOR, UINT_16, null); + StructDefinition struct = getStructDefinition(BOOL_8, declaration, (byte) 0, (byte) 7, (byte) 0x12, (byte) 0x34); + assertDisabled(struct); + assertEquals(7, getTail(struct)); + } + + /** + * An instance is enabled when an unsigned integer selector is in the + * selector field ranges + */ + @Test + public void testUnsignedIntegerSelectorInRange() throws CTFException { + OptionalDeclaration declaration = new OptionalDeclaration(SELECTOR, UINT_8, List.of(new IntegerRange(1, 3), new IntegerRange(10, 10))); + for (int selector : new int[] { 1, 2, 3, 10 }) { + StructDefinition struct = getStructDefinition(UINT_8, declaration, (byte) selector, (byte) 42, (byte) 7); + assertEnabled(struct, 42); + assertEquals(7, getTail(struct)); + } + } + + /** + * An instance is disabled when an unsigned integer selector is not in the + * selector field ranges + */ + @Test + public void testUnsignedIntegerSelectorOutOfRange() throws CTFException { + OptionalDeclaration declaration = new OptionalDeclaration(SELECTOR, UINT_8, List.of(new IntegerRange(1, 3), new IntegerRange(10, 10))); + for (int selector : new int[] { 0, 4, 9, 11, 255 }) { + StructDefinition struct = getStructDefinition(UINT_8, declaration, (byte) selector, (byte) 7, (byte) 42); + assertDisabled(struct); + assertEquals(7, getTail(struct)); + } + } + + /** + * Test a signed integer selector with the ranges + */ + @Test + public void testSignedIntegerSelector() throws CTFException { + OptionalDeclaration declaration = new OptionalDeclaration(SELECTOR, UINT_8, RANGES); + for (int selector : new int[] { -12, -5, -1, 0, 15, 20, 35 }) { + StructDefinition struct = getStructDefinition(INT_8, declaration, (byte) selector, (byte) 42, (byte) 7); + assertEnabled(struct, 42); + assertEquals(7, getTail(struct)); + } + for (int selector : new int[] { -128, -13, -11, -6, 1, 14, 36, 127 }) { + StructDefinition struct = getStructDefinition(INT_8, declaration, (byte) selector, (byte) 7, (byte) 42); + assertDisabled(struct); + assertEquals(7, getTail(struct)); + } + } + + /** + * Test a variable-length unsigned integer selector + */ + @Test + public void testVariableLengthIntegerSelector() throws CTFException { + IntegerDeclaration varint = IntegerDeclaration.createVarintDeclaration(false, 10, null, true); + OptionalDeclaration declaration = new OptionalDeclaration(SELECTOR, UINT_8, List.of(new IntegerRange(128, 300))); + + /* 200 is encoded as 0xc8 0x01 in LEB128 */ + StructDefinition struct = getStructDefinition(varint, declaration, (byte) 0xc8, (byte) 0x01, (byte) 42, (byte) 7); + assertEnabled(struct, 42); + assertEquals(7, getTail(struct)); + + /* 100 is encoded as 0x64 in LEB128 */ + struct = getStructDefinition(varint, declaration, (byte) 0x64, (byte) 7, (byte) 42); + assertDisabled(struct); + assertEquals(7, getTail(struct)); + } + + /** + * An unsigned 64-bit selector value greater than Long.MAX_VALUE is not in + * any selector field range + */ + @Test + public void testLargeUnsignedIntegerSelector() throws CTFException { + OptionalDeclaration declaration = new OptionalDeclaration(SELECTOR, UINT_8, List.of(new IntegerRange(-1, -1), new IntegerRange(0, Long.MAX_VALUE))); + byte[] data = { -1, -1, -1, -1, -1, -1, -1, -1, 7, 42 }; + StructDefinition struct = getStructDefinition(UINT_64, declaration, data); + assertDisabled(struct); + assertEquals(7, getTail(struct)); + } + + /** + * Test an optional null-terminated string + */ + @Test + public void testOptionalString() throws CTFException { + OptionalDeclaration declaration = new OptionalDeclaration(SELECTOR, STRING, RANGES); + StructDefinition struct = getStructDefinition(INT_8, declaration, (byte) 20, (byte) 'i', (byte) 'p', (byte) 0, (byte) 7); + OptionalDefinition optional = getOptionalDefinition(struct); + assertTrue(optional.isEnabled()); + Definition optionalField = optional.getField(); + assertNotNull(optionalField); + assertTrue(optionalField instanceof StringDefinition); + assertEquals("ip", ((StringDefinition) optionalField).getValue()); + assertEquals(7, getTail(struct)); + } + + /** + * Test the getters of the definition + */ + @Test + public void testGetDeclaration() throws CTFException { + OptionalDeclaration declaration = new OptionalDeclaration(SELECTOR, UINT_8, null); + StructDefinition struct = getStructDefinition(BOOL_8, declaration, (byte) 1, (byte) 42, (byte) 7); + OptionalDefinition optional = getOptionalDefinition(struct); + assertSame(declaration, optional.getDeclaration()); + assertEquals(8, optional.size()); + } + + /** + * The selector field must exist + */ + @Test(expected = CTFException.class) + public void testMissingSelector() throws CTFException { + OptionalDeclaration declaration = new OptionalDeclaration("unknown", UINT_8, null); + getStructDefinition(BOOL_8, declaration, (byte) 1, (byte) 42, (byte) 7); + } + + /** + * The selector field must be a boolean or an integer field + */ + @Test(expected = CTFException.class) + public void testInvalidSelectorType() throws CTFException { + OptionalDeclaration declaration = new OptionalDeclaration(SELECTOR, UINT_8, RANGES); + getStructDefinition(STRING, declaration, (byte) 'a', (byte) 0, (byte) 42, (byte) 7); + } + + /** + * The selector field ranges are required with an integer selector + */ + @Test(expected = CTFException.class) + public void testIntegerSelectorWithoutRanges() throws CTFException { + OptionalDeclaration declaration = new OptionalDeclaration(SELECTOR, UINT_8, null); + getStructDefinition(UINT_8, declaration, (byte) 1, (byte) 42, (byte) 7); + } + + /** + * The selector field must be looked up in a scope + */ + @Test(expected = CTFException.class) + public void testNoScope() throws CTFException { + OptionalDeclaration declaration = new OptionalDeclaration(SELECTOR, UINT_8, null); + declaration.createDefinition(null, OPTIONAL, new BitBuffer(ByteBuffer.wrap(new byte[] { 42 }))); + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/TypeAliasParserTest.java b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/TypeAliasParserTest.java index 9b78311580..5305761cb1 100644 --- a/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/TypeAliasParserTest.java +++ b/ctf/org.eclipse.tracecompass.ctf.core.tests/src/org/eclipse/tracecompass/ctf/core/tests/types/TypeAliasParserTest.java @@ -10,6 +10,7 @@ package org.eclipse.tracecompass.ctf.core.tests.types; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -17,11 +18,15 @@ import org.eclipse.tracecompass.ctf.core.event.types.FloatDeclaration; import org.eclipse.tracecompass.ctf.core.event.types.IDeclaration; import org.eclipse.tracecompass.ctf.core.event.types.IntegerDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.OptionalDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.StringDeclaration; import org.eclipse.tracecompass.ctf.core.trace.CTFTrace; import org.eclipse.tracecompass.internal.ctf.core.event.metadata.JsonStructureFieldMemberMetadataNode; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.ParseException; import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.TypeAliasParser; import org.junit.Test; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; @@ -33,10 +38,10 @@ public class TypeAliasParserTest { /** * Test parsing fixed-length floating point field class * - * @throws Exception if parsing fails + * @throws ParseException if parsing fails */ @Test - public void testFixedLengthFloatingPointParsing() throws Exception { + public void testFixedLengthFloatingPointParsing() throws ParseException { CTFTrace trace = new CTFTrace(); DeclarationScope scope = new DeclarationScope(null, "test"); @@ -61,10 +66,10 @@ public void testFixedLengthFloatingPointParsing() throws Exception { /** * Test parsing static-length string field class * - * @throws Exception if parsing fails + * @throws ParseException if parsing fails */ @Test - public void testStaticLengthStringParsing() throws Exception { + public void testStaticLengthStringParsing() throws ParseException { CTFTrace trace = new CTFTrace(); DeclarationScope scope = new DeclarationScope(null, "test"); @@ -84,10 +89,10 @@ public void testStaticLengthStringParsing() throws Exception { /** * Test parsing dynamic-length string field class * - * @throws Exception if parsing fails + * @throws ParseException if parsing fails */ @Test - public void testDynamicLengthStringParsing() throws Exception { + public void testDynamicLengthStringParsing() throws ParseException { // Test that the parser doesn't throw an exception for dynamic length strings // The actual parsing logic may not be fully implemented yet CTFTrace trace = new CTFTrace(); @@ -112,4 +117,40 @@ public void testDynamicLengthStringParsing() throws Exception { assertNotNull(result); } + /** + * Test parsing optional field class + * + * @throws ParseException if parsing fails + */ + @Test + public void testOptionalParsing() throws ParseException { + CTFTrace trace = new CTFTrace(); + DeclarationScope scope = new DeclarationScope(null, "test"); + + JsonObject selectorLocation = new JsonObject(); + selectorLocation.add("origin", new JsonPrimitive("event-record-payload")); + JsonArray path = new JsonArray(); + path.add("has-ip"); + selectorLocation.add("path", path); + + JsonObject optionalFieldClass = new JsonObject(); + optionalFieldClass.add("type", new JsonPrimitive("null-terminated-string")); + + JsonObject fieldClass = new JsonObject(); + fieldClass.add("type", new JsonPrimitive("optional")); + fieldClass.add("selector-field-location", selectorLocation); + fieldClass.add("field-class", optionalFieldClass); + + JsonStructureFieldMemberMetadataNode node = new JsonStructureFieldMemberMetadataNode(null, "optional", "test", "optional_field", fieldClass); + + IDeclaration result = TypeAliasParser.INSTANCE.parse(node, + new TypeAliasParser.Param(trace, scope)); + + assertNotNull(result); + assertTrue(result instanceof OptionalDeclaration); + + OptionalDeclaration optionalDecl = (OptionalDeclaration) result; + assertTrue(optionalDecl.getFieldDeclaration() instanceof StringDeclaration); + assertEquals("has-ip", optionalDecl.getSelectorName()); + } } diff --git a/ctf/org.eclipse.tracecompass.ctf.core/META-INF/MANIFEST.MF b/ctf/org.eclipse.tracecompass.ctf.core/META-INF/MANIFEST.MF index bbb94f1a6f..9f966672cb 100644 --- a/ctf/org.eclipse.tracecompass.ctf.core/META-INF/MANIFEST.MF +++ b/ctf/org.eclipse.tracecompass.ctf.core/META-INF/MANIFEST.MF @@ -20,6 +20,8 @@ Export-Package: org.eclipse.tracecompass.ctf.core, org.eclipse.tracecompass.internal.ctf.core.event;x-friends:="org.eclipse.tracecompass.ctf.core.tests,org.eclipse.tracecompass.tmf.ctf.core.tests", org.eclipse.tracecompass.internal.ctf.core.event.metadata;x-friends:="org.eclipse.tracecompass.ctf.core.tests", org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl;x-friends:="org.eclipse.tracecompass.ctf.core.tests", + org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.bitarray;x-friends:="org.eclipse.tracecompass.ctf.core.tests", + org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.bitmap;x-friends:="org.eclipse.tracecompass.ctf.core.tests", org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.bool;x-friends:="org.eclipse.tracecompass.ctf.core.tests", org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.callsite;x-internal:=true, org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.enumeration;x-friends:="org.eclipse.tracecompass.ctf.core.tests", @@ -27,6 +29,7 @@ Export-Package: org.eclipse.tracecompass.ctf.core, org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.event;x-friends:="org.eclipse.tracecompass.ctf.core.tests", org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.floatingpoint;x-friends:="org.eclipse.tracecompass.ctf.core.tests", org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.integer;x-friends:="org.eclipse.tracecompass.ctf.core.tests", + org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.optional;x-friends:="org.eclipse.tracecompass.ctf.core.tests", org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.stream;x-friends:="org.eclipse.tracecompass.ctf.core.tests", org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.string;x-friends:="org.eclipse.tracecompass.ctf.core.tests", org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.struct;x-friends:="org.eclipse.tracecompass.ctf.core.tests", diff --git a/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/BitArrayDeclaration.java b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/BitArrayDeclaration.java new file mode 100644 index 0000000000..3401c42c6a --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/BitArrayDeclaration.java @@ -0,0 +1,166 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.ctf.core.event.types; + +import java.nio.ByteOrder; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.tracecompass.ctf.core.CTFException; +import org.eclipse.tracecompass.ctf.core.event.io.BitBuffer; +import org.eclipse.tracecompass.ctf.core.event.scope.IDefinitionScope; + +/** + * A CTF2 declaration of a fixed-length bit array field. + * + * A fixed-length bit array is a simple array of contiguous bits, without any + * attached integer value meaning. The bits are numbered from 0 (least + * significant) to length - 1 (most significant). + * + * @since 5.3 + * @author Arnaud Fiorini + */ +public class BitArrayDeclaration extends Declaration { + + private final int fLength; + private final long fAlignment; + private final ByteOrder fByteOrder; + private final BitOrder fBitOrder; + + /** + * Constructor + * + * @param length + * the number of bits of the bit array, between 1 and + * Integer.MAX_VALUE + * @param byteOrder + * the order in which the bytes should be read + * @param bitOrder + * the order in which the bits should be read + * @param alignment + * alignment of the first bit of field relative to the beginning + * of the packet + */ + public BitArrayDeclaration(int length, ByteOrder byteOrder, BitOrder bitOrder, long alignment) { + if (length <= 0) { + throw new IllegalArgumentException("Unsupported bit array length: " + length); //$NON-NLS-1$ + } + fLength = length; + fByteOrder = byteOrder; + fBitOrder = bitOrder; + fAlignment = alignment; + } + + @Override + public @NonNull BitArrayDefinition createDefinition(IDefinitionScope definitionScope, @NonNull String fieldName, @NonNull BitBuffer input) throws CTFException { + ByteOrder byteOrder = input.getByteOrder(); + input.setByteOrder(fByteOrder); + byte[] value = new byte[(int) ((fLength + 7L) / Byte.SIZE)]; + try { + alignRead(input); + /* + * The buffer reads the bits in the natural order of the byte + * order: the first decoded bit is the least significant bit of a + * little-endian chunk and the most significant bit of a big-endian + * chunk. Reverse the chunk when the bit order is not the natural + * one. + */ + boolean reverse = fBitOrder != BitOrder.fromByteOrder(fByteOrder); + int read = 0; + while (read < fLength) { + int chunkLength = Math.min(Long.SIZE, fLength - read); + long chunk = input.get(chunkLength, false); + if (reverse) { + chunk = Long.reverse(chunk) >>> (Long.SIZE - chunkLength); + } + int offset = (fBitOrder == BitOrder.FIRST_TO_LAST) ? read : fLength - read - chunkLength; + setBits(value, offset, chunk, chunkLength); + read += chunkLength; + } + } finally { + input.setByteOrder(byteOrder); + } + return createDefinition(definitionScope, fieldName, value); + } + + private static void setBits(byte[] value, int offset, long bits, int length) { + for (int i = 0; i < length; i++) { + if (((bits >>> i) & 1L) != 0) { + int index = offset + i; + value[index / Byte.SIZE] |= (byte) (1 << (index % Byte.SIZE)); + } + } + } + + /** + * Create the definition once the bits have been read + * + * @param definitionScope + * the definition scope + * @param fieldName + * the field name + * @param value + * the bits that were read, element i of the bit array being + * bit (i % 8) of byte (i / 8) + * @return the definition + */ + protected @NonNull BitArrayDefinition createDefinition(IDefinitionScope definitionScope, @NonNull String fieldName, byte[] value) { + return new BitArrayDefinition(this, definitionScope, fieldName, value); + } + + /** + * @return the number of bits of the bit array + */ + public int getLength() { + return fLength; + } + + /** + * @return the byte order + */ + public ByteOrder getByteOrder() { + return fByteOrder; + } + + /** + * @return first to last or last to first + */ + public BitOrder getBitOrder() { + return fBitOrder; + } + + @Override + public long getAlignment() { + return fAlignment; + } + + @Override + public int getMaximumSize() { + return fLength; + } + + @Override + public String toString() { + /* Only used for debugging */ + return "[declaration] bit array[length:" + fLength + ", byte order:" + fByteOrder + ", bit order:" + fBitOrder + ", alignment:" + fAlignment + ']'; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ + } + + @Override + public boolean isBinaryEquivalent(IDeclaration other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + BitArrayDeclaration o = (BitArrayDeclaration) other; + return fLength == o.fLength && fAlignment == o.fAlignment && fByteOrder.equals(o.fByteOrder) && fBitOrder == o.fBitOrder; + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/BitArrayDefinition.java b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/BitArrayDefinition.java new file mode 100644 index 0000000000..7b8a8fd748 --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/BitArrayDefinition.java @@ -0,0 +1,80 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.ctf.core.event.types; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.tracecompass.ctf.core.event.scope.IDefinitionScope; + +/** + * A CTF2 fixed-length bit array definition. + * + * @since 5.3 + * @author Arnaud Fiorini + */ +public class BitArrayDefinition extends Definition { + + private final byte[] fValue; + + /** + * Constructor + * + * @param declaration + * the parent declaration + * @param definitionScope + * the parent scope + * @param fieldName + * the field name + * @param value + * the bits of the field, element i of the bit array being bit + * (i % 8) of byte (i / 8) + */ + public BitArrayDefinition(@NonNull BitArrayDeclaration declaration, IDefinitionScope definitionScope, @NonNull String fieldName, byte[] value) { + super(declaration, definitionScope, fieldName); + fValue = value; + } + + @Override + public BitArrayDeclaration getDeclaration() { + return (BitArrayDeclaration) super.getDeclaration(); + } + + /** + * @return the bits of the field, element i of the bit array being bit + * (i % 8) of byte (i / 8) + */ + public byte[] getValue() { + return fValue; + } + + /** + * Check whether a bit is set + * + * @param index + * the element index in the bit array + * @return true if the bit is set, false if it is cleared or out of range + */ + public boolean isBitSet(long index) { + if (index < 0 || index >= getDeclaration().getLength()) { + return false; + } + return ((fValue[(int) (index / Byte.SIZE)] >>> (index % Byte.SIZE)) & 1) != 0; + } + + @Override + public String toString() { + int length = getDeclaration().getLength(); + StringBuilder sb = new StringBuilder("0b"); //$NON-NLS-1$ + for (int i = length - 1; i >= 0; i--) { + sb.append(isBitSet(i) ? '1' : '0'); + } + return sb.toString(); + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/BitMapDeclaration.java b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/BitMapDeclaration.java new file mode 100644 index 0000000000..34cb809848 --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/BitMapDeclaration.java @@ -0,0 +1,78 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.ctf.core.event.types; + +import java.nio.ByteOrder; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.tracecompass.ctf.core.event.scope.IDefinitionScope; + +/** + * A CTF2 declaration of a fixed-length bit map field. + * + * A fixed-length bit map is a fixed-length bit array with named flags. Each + * flag is associated with a set of bit index ranges: a flag is active when at + * least one of the bits of its ranges is set. + * + * @since 5.3 + * @author Arnaud Fiorini + */ +public class BitMapDeclaration extends BitArrayDeclaration { + + private final Map> fFlags; + + /** + * Constructor + * + * @param length + * the number of bits of the bit map + * @param byteOrder + * the order in which the bytes should be read + * @param bitOrder + * the order in which the bits should be read + * @param alignment + * alignment of the first bit of field relative to the beginning + * of the packet + * @param flags + * the flags, mapping a flag name to the ranges of bit indexes + * of this flag + */ + public BitMapDeclaration(int length, ByteOrder byteOrder, BitOrder bitOrder, long alignment, Map> flags) { + if (flags.isEmpty()) { + throw new IllegalArgumentException("Flags must not be empty."); //$NON-NLS-1$ + } + super(length, byteOrder, bitOrder, alignment); + fFlags = Collections.unmodifiableMap(new LinkedHashMap<>(flags)); + } + + @Override + protected @NonNull BitMapDefinition createDefinition(IDefinitionScope definitionScope, @NonNull String fieldName, byte[] value) { + return new BitMapDefinition(this, definitionScope, fieldName, value); + } + + /** + * @return the flags, mapping a flag name to the ranges of bit indexes of + * this flag + */ + public Map> getFlags() { + return fFlags; + } + + @Override + public String toString() { + /* Only used for debugging */ + return "[declaration] bit map[length:" + getLength() + ", flags:" + fFlags.keySet() + ']'; //$NON-NLS-1$ //$NON-NLS-2$ + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/BitMapDefinition.java b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/BitMapDefinition.java new file mode 100644 index 0000000000..c9b147b093 --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/BitMapDefinition.java @@ -0,0 +1,96 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.ctf.core.event.types; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.tracecompass.ctf.core.event.scope.IDefinitionScope; + +/** + * A CTF2 fixed-length bit map definition. + * + * @since 5.3 + * @author Arnaud Fiorini + */ +public class BitMapDefinition extends BitArrayDefinition { + + private final List fActiveFlags; + + /** + * Constructor + * + * @param declaration + * the parent declaration + * @param definitionScope + * the parent scope + * @param fieldName + * the field name + * @param value + * the bits of the field, element i of the bit array being bit + * (i % 8) of byte (i / 8) + */ + public BitMapDefinition(@NonNull BitMapDeclaration declaration, IDefinitionScope definitionScope, @NonNull String fieldName, byte[] value) { + super(declaration, definitionScope, fieldName, value); + List activeFlags = new ArrayList<>(); + for (Entry> flag : declaration.getFlags().entrySet()) { + if (isAnyBitSet(flag.getValue())) { + activeFlags.add(flag.getKey()); + } + } + fActiveFlags = List.copyOf(activeFlags); + } + + private boolean isAnyBitSet(List ranges) { + for (IntegerRange range : ranges) { + long end = Math.min(range.getEnd(), getDeclaration().getLength() - 1L); + for (long i = Math.max(range.getStart(), 0); i <= end; i++) { + if (isBitSet(i)) { + return true; + } + } + } + return false; + } + + @Override + public BitMapDeclaration getDeclaration() { + return (BitMapDeclaration) super.getDeclaration(); + } + + /** + * Get the names of the active flags, in declaration order. A flag is active + * when at least one of its bits is set. + * + * @return the active flag names + */ + public List getActiveFlags() { + return fActiveFlags; + } + + /** + * Check if a flag is active + * + * @param flagName + * the flag name + * @return true if the flag is active + */ + public boolean isFlagActive(String flagName) { + return fActiveFlags.contains(flagName); + } + + @Override + public String toString() { + return super.toString() + " (" + String.join(" | ", fActiveFlags) + ')'; //$NON-NLS-1$ //$NON-NLS-2$ + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/BitOrder.java b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/BitOrder.java new file mode 100644 index 0000000000..b812047c4d --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/BitOrder.java @@ -0,0 +1,43 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.ctf.core.event.types; + +import java.nio.ByteOrder; + +/** + * The bit order of a CTF2 fixed-length bit array field, as described in + * https://diamon.org/ctf/#fl-ba-fc + * + * @since 5.3 + * @author Arnaud Fiorini + */ +public enum BitOrder { + /** + * The first bit of the field is the least significant bit + */ + FIRST_TO_LAST, + /** + * The first bit of the field is the most significant bit + */ + LAST_TO_FIRST; + + /** + * Gets the default bit order for a given byte order: first-to-last for + * little-endian and last-to-first for big-endian. + * + * @param byteOrder + * the byte order of the field + * @return the default bit order + */ + public static BitOrder fromByteOrder(ByteOrder byteOrder) { + return ByteOrder.BIG_ENDIAN.equals(byteOrder) ? LAST_TO_FIRST : FIRST_TO_LAST; + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/OptionalDeclaration.java b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/OptionalDeclaration.java new file mode 100644 index 0000000000..0a5c7e5ae7 --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/OptionalDeclaration.java @@ -0,0 +1,168 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.ctf.core.event.types; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.tracecompass.ctf.core.CTFException; +import org.eclipse.tracecompass.ctf.core.event.io.BitBuffer; +import org.eclipse.tracecompass.ctf.core.event.scope.IDefinitionScope; + +/** + * A CTF2 declaration of an optional field. + * + * An optional field is, depending on the value of a previously decoded + * selector field, either an instance of a given field class (enabled) or a + * zero-bit field (disabled). + *
    + *
  • If the selector field is a boolean field, the optional field is enabled + * when the selector is true.
  • + *
  • If the selector field is an integer field, the optional field is enabled + * when the selector value is within the selector ranges.
  • + *
+ * + * @since 5.3 + * @author Arnaud Fiorini + */ +public class OptionalDeclaration extends Declaration { + + private final String fSelectorName; + private final IDeclaration fFieldDeclaration; + private final @Nullable List fSelectorRanges; + + /** + * Constructor + * + * @param selectorName + * the name of the selector field + * @param fieldDeclaration + * the declaration of the selector field + * @param selectorRanges + * the ranges of the selector values enabling the optional + * field, required for an integer selector, null for a boolean + * selector + */ + public OptionalDeclaration(String selectorName, IDeclaration fieldDeclaration, @Nullable List selectorRanges) { + fSelectorName = selectorName; + fFieldDeclaration = fieldDeclaration; + fSelectorRanges = selectorRanges == null ? null : List.copyOf(selectorRanges); + } + + @Override + public @NonNull OptionalDefinition createDefinition(IDefinitionScope definitionScope, @NonNull String fieldName, @NonNull BitBuffer input) throws CTFException { + if (definitionScope == null) { + throw new CTFException("Optional field " + fieldName + " has no scope to look up its selector " + fSelectorName); //$NON-NLS-1$ //$NON-NLS-2$ + } + IDefinition selector = definitionScope.lookupDefinition(fSelectorName); + Definition field = null; + if (isEnabled(selector)) { + field = fFieldDeclaration.createDefinition(definitionScope, fieldName, input); + } + return new OptionalDefinition(this, definitionScope, fieldName, field); + } + + private boolean isEnabled(@Nullable IDefinition definitionScope) throws CTFException { + if (definitionScope == null) { + throw new CTFException("Optional selector field not found: " + fSelectorName); //$NON-NLS-1$ + } + if (definitionScope instanceof BooleanDefinition booleanDefinition) { + return booleanDefinition.getValue(); + } + if (!(definitionScope instanceof IntegerDefinition)) { + throw new CTFException("Optional selector field must be a boolean or an integer: " + fSelectorName); //$NON-NLS-1$ + } + IntegerDefinition integerDefinition = (IntegerDefinition) definitionScope; + Long value = integerDefinition.getIntegerValue(); + List ranges = fSelectorRanges; + if (ranges == null) { + throw new CTFException("Optional field with an integer selector requires selector ranges: " + fSelectorName); //$NON-NLS-1$ + } + if (value < 0 && !integerDefinition.getDeclaration().isSigned()) { + /* Unsigned value above Long.MAX_VALUE, outside of any range */ + return false; + } + for (IntegerRange range : ranges) { + if (range.getStart() <= value && value <= range.getEnd()) { + return true; + } + } + return false; + } + + /** + * @return the name of the selector field + */ + public String getSelectorName() { + return fSelectorName; + } + + /** + * @return the declaration of the field when the optional is enabled + */ + public IDeclaration getFieldDeclaration() { + return fFieldDeclaration; + } + + /** + * @return the selector ranges enabling the optional field, null for a + * boolean selector + */ + public @Nullable List getSelectorRanges() { + return fSelectorRanges; + } + + @Override + public long getAlignment() { + return 1; + } + + @Override + public int getMaximumSize() { + return fFieldDeclaration.getMaximumSize(); + } + + @Override + public String toString() { + /* Only used for debugging */ + return "[declaration] optional[selector:" + fSelectorName + ", field:" + fFieldDeclaration + ']'; //$NON-NLS-1$ //$NON-NLS-2$ + } + + @Override + public boolean isBinaryEquivalent(IDeclaration other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + OptionalDeclaration o = (OptionalDeclaration) other; + return fSelectorName.equals(o.fSelectorName) + && rangesEqual(fSelectorRanges, o.fSelectorRanges) + && fFieldDeclaration.isBinaryEquivalent(o.fFieldDeclaration); + } + + private static boolean rangesEqual(@Nullable List a, @Nullable List b) { + if (a == null || b == null) { + return a == b; + } + if (a.size() != b.size()) { + return false; + } + for (int i = 0; i < a.size(); i++) { + if (a.get(i).getStart() != b.get(i).getStart() || a.get(i).getEnd() != b.get(i).getEnd()) { + return false; + } + } + return true; + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/OptionalDefinition.java b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/OptionalDefinition.java new file mode 100644 index 0000000000..43c4978e6f --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/event/types/OptionalDefinition.java @@ -0,0 +1,78 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.ctf.core.event.types; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.tracecompass.ctf.core.event.scope.IDefinitionScope; + +/** + * A CTF2 optional field definition. + * + * It wraps the field definition when the optional field is enabled, and holds + * nothing when it is disabled. + * + * @since 5.3 + * @author Arnaud Fiorini + */ +public class OptionalDefinition extends Definition { + + private final @Nullable Definition fField; + + /** + * Constructor + * + * @param declaration + * the parent declaration + * @param definitionScope + * the parent scope + * @param fieldName + * the field name + * @param field + * the field definition if the optional is enabled, null + * otherwise + */ + public OptionalDefinition(@NonNull OptionalDeclaration declaration, IDefinitionScope definitionScope, @NonNull String fieldName, @Nullable Definition field) { + super(declaration, definitionScope, fieldName); + fField = field; + } + + @Override + public OptionalDeclaration getDeclaration() { + return (OptionalDeclaration) super.getDeclaration(); + } + + /** + * @return true if the optional field is enabled + */ + public boolean isEnabled() { + return fField != null; + } + + /** + * @return the field definition if the optional is enabled, null otherwise + */ + public @Nullable Definition getField() { + return fField; + } + + @Override + public long size() { + Definition field = fField; + return field == null ? 0 : field.size(); + } + + @Override + public String toString() { + Definition field = fField; + return field == null ? "null" : field.toString(); //$NON-NLS-1$ + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/trace/Metadata.java b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/trace/Metadata.java index 28998113aa..1ee4ca0b3b 100644 --- a/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/trace/Metadata.java +++ b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/ctf/core/trace/Metadata.java @@ -39,6 +39,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.UUID; import org.antlr.runtime.ANTLRReaderStream; @@ -99,6 +100,20 @@ public class Metadata { private static final List TEXT_ONLY_METADATA_HEADER_PREFIXES = List.of(MetadataStrings.TYPE_ALIAS, MetadataStrings.TRACE, MetadataStrings.ENV); + /** + * Properties whose value may be the name of a previously occurring field + * class alias, see https://diamon.org/ctf/#fca-frag + */ + private static final Set FIELD_CLASS_PROPERTIES = Set.of( + JsonMetadataStrings.FIELD_CLASS, + JsonMetadataStrings.ELEMENT_FIELD_CLASS, + JsonMetadataStrings.PACKET_HEADER_FIELD_CLASS, + JsonMetadataStrings.PACKET_CONTEXT_FIELD_CLASS, + JsonMetadataStrings.EVENT_RECORD_HEADER_FIELD_CLASS, + JsonMetadataStrings.EVENT_RECORD_COMMON_CONTEXT_FIELD_CLASS, + JsonMetadataStrings.SPECIFIC_CONTEXT_FIELD_CLASS, + JsonMetadataStrings.PAYLOAD_FIELD_CLASS); + private static final int PREVALIDATION_SIZE = 8; private static final int BITS_PER_BYTE = Byte.SIZE; @@ -323,7 +338,7 @@ private static ICTFMetadataNode parseJsonToTree(String json) throws CTFException private static void expandAliases(JsonObject obj, Map metadata) { for (String key : obj.keySet()) { JsonElement value = obj.get(key); - if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isString() && metadata.containsKey(value.getAsString())) { + if (FIELD_CLASS_PROPERTIES.contains(key) && value.isJsonPrimitive() && value.getAsJsonPrimitive().isString() && metadata.containsKey(value.getAsString())) { obj.add(key, metadata.get(value.getAsString())); } else if (value.isJsonObject()) { expandAliases(value.getAsJsonObject(), metadata); diff --git a/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/MetadataStrings.java b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/MetadataStrings.java index 114ff91097..c24ee0f465 100644 --- a/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/MetadataStrings.java +++ b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/MetadataStrings.java @@ -74,6 +74,10 @@ public interface MetadataStrings { String LITTLE_ENDIAN = "little-endian"; /** Big endian for CTF2 */ String BIG_ENDIAN = "big-endian"; + /** Bit order first-to-last for CTF2 */ + String FIRST_TO_LAST = "first-to-last"; + /** Bit order last-to-first for CTF2 */ + String LAST_TO_FIRST = "last-to-first"; /** Alignment of a field */ String ALIGN = "align"; /** Mantissa digits */ diff --git a/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/tsdl/BitOrderParser.java b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/tsdl/BitOrderParser.java new file mode 100644 index 0000000000..0ff86d669f --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/tsdl/BitOrderParser.java @@ -0,0 +1,63 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl; + +import org.eclipse.tracecompass.ctf.core.event.types.BitOrder; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.CTFJsonMetadataNode; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.ICommonTreeParser; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.MetadataStrings; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.ParseException; +import org.eclipse.tracecompass.internal.ctf.core.event.types.ICTFMetadataNode; + +/** + * The bit order of any field class that uses the bit order property is either + * "first-to-last" or "last-to-first". It is an optional property: when it is + * not specified, it defaults to "first-to-last" for little-endian fields and + * "last-to-first" for big-endian fields. + * + * @author Arnaud Fiorini + */ +public final class BitOrderParser implements ICommonTreeParser { + + /** + * Instance + */ + public static final BitOrderParser INSTANCE = new BitOrderParser(); + + private static final String INVALID_VALUE_FOR_BIT_ORDER = "Invalid value for bit order"; //$NON-NLS-1$ + + private BitOrderParser() { + } + + /** + * Gets the value of a "bit-order" property. + * + * @param bitOrderTree + * A json node containing the bit order string. + * + * @return The "bit-order" value. + * @throws ParseException + * if the value is invalid + */ + @Override + public final BitOrder parse(ICTFMetadataNode bitOrderTree, ICommonTreeParserParameter param) throws ParseException { + if (!(bitOrderTree instanceof CTFJsonMetadataNode)) { + throw new ParseException("The bit order property is not supported in CTF 1.8."); //$NON-NLS-1$ + } + String stringValue = bitOrderTree.getText(); + if (MetadataStrings.FIRST_TO_LAST.equals(stringValue)) { + return BitOrder.FIRST_TO_LAST; + } else if (MetadataStrings.LAST_TO_FIRST.equals(stringValue)) { + return BitOrder.LAST_TO_FIRST; + } + throw new ParseException(INVALID_VALUE_FOR_BIT_ORDER + " : " + stringValue); //$NON-NLS-1$ + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/tsdl/TypeAliasParser.java b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/tsdl/TypeAliasParser.java index c17466515a..c2454fb0d7 100644 --- a/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/tsdl/TypeAliasParser.java +++ b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/tsdl/TypeAliasParser.java @@ -24,12 +24,15 @@ import org.eclipse.tracecompass.internal.ctf.core.event.metadata.JsonFieldClassAliasMetadataNode; import org.eclipse.tracecompass.internal.ctf.core.event.metadata.JsonStructureFieldMemberMetadataNode; import org.eclipse.tracecompass.internal.ctf.core.event.metadata.ParseException; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.bitarray.BitArrayDeclarationParser; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.bitmap.BitMapDeclarationParser; import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.bool.BooleanDeclarationParser; import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.dynamicarray.DynamicLengthArrayParser; import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.dynamicstring.DynamicLengthStringParser; import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.enumeration.EnumParser; import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.floatingpoint.FloatDeclarationParser; import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.integer.IntegerDeclarationParser; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.optional.OptionalDeclarationParser; import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.staticarray.StaticLengthArrayParser; import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.staticstring.StaticLengthStringParser; import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.string.StringDeclarationParser; @@ -194,16 +197,16 @@ public IDeclaration parse(ICTFMetadataNode typealias, ICommonTreeParserParameter targetDeclaration = DynamicLengthArrayParser.INSTANCE.parse(typealias, new DynamicLengthArrayParser.Param(trace, scope)); break; case JsonMetadataStrings.FIXED_LENGTH_BIT_ARRAY: - // TODO: Handle fixed length bit arrays + targetDeclaration = BitArrayDeclarationParser.INSTANCE.parse(typealias, new BitArrayDeclarationParser.Param(trace)); break; case JsonMetadataStrings.FIXED_LENGTH_BIT_MAP: - // TODO: handle fixed length bit map + targetDeclaration = BitMapDeclarationParser.INSTANCE.parse(typealias, new BitMapDeclarationParser.Param(trace)); break; case JsonMetadataStrings.STRUCTURE: targetDeclaration = StructParser.INSTANCE.parse(typealias, new StructParser.Param(trace, null, scope)); break; case JsonMetadataStrings.OPTIONAL: - // TODO: handle optional + targetDeclaration = OptionalDeclarationParser.INSTANCE.parse(typealias, new OptionalDeclarationParser.Param(trace, scope)); break; case JsonMetadataStrings.FIXED_LENGTH_FLOATING_POINT: targetDeclaration = FloatDeclarationParser.INSTANCE.parse(typealias, new FloatDeclarationParser.Param(trace)); diff --git a/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/tsdl/bitarray/BitArrayDeclarationParser.java b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/tsdl/bitarray/BitArrayDeclarationParser.java new file mode 100644 index 0000000000..1647cea8de --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/tsdl/bitarray/BitArrayDeclarationParser.java @@ -0,0 +1,119 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.bitarray; + +import java.nio.ByteOrder; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.tracecompass.ctf.core.event.types.BitArrayDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.BitOrder; +import org.eclipse.tracecompass.ctf.core.trace.CTFTrace; +import org.eclipse.tracecompass.ctf.parser.CTFParser; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.CTFJsonMetadataNode; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.ICommonTreeParser; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.JsonStructureFieldMemberMetadataNode; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.ParseException; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.AlignmentParser; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.BitOrderParser; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.ByteOrderParser; +import org.eclipse.tracecompass.internal.ctf.core.event.types.ICTFMetadataNode; +import org.eclipse.tracecompass.internal.ctf.core.utils.JsonMetadataStrings; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** + * According to the documentation: + * https://diamon.org/ctf/#fl-ba-fc + * + * A fixed-length bit array field class describes fixed-length bit array + * fields, which are simple arrays of contiguous bits without any integer value + * meaning. + * + * The length, byte order are required properties. The bit order is optional + * and defaults to the natural bit order of the byte order. + * + * @author Arnaud Fiorini + */ +public final class BitArrayDeclarationParser implements ICommonTreeParser { + + /** + * Parameter object with only a trace in it + */ + @NonNullByDefault + public static final class Param implements ICommonTreeParserParameter { + private final CTFTrace fTrace; + + /** + * Constructor + * + * @param trace + * the trace + */ + public Param(CTFTrace trace) { + fTrace = trace; + } + } + + /** + * Instance + */ + public static final BitArrayDeclarationParser INSTANCE = new BitArrayDeclarationParser(); + + private BitArrayDeclarationParser() { + } + + @Override + public @NonNull BitArrayDeclaration parse(ICTFMetadataNode bitArray, ICommonTreeParserParameter param) throws ParseException { + if (!(param instanceof Param)) { + throw new IllegalArgumentException("Param must be a " + Param.class.getCanonicalName()); //$NON-NLS-1$ + } + CTFTrace trace = ((Param) param).fTrace; + + long alignment = 1; + int length = 0; + + if (!(bitArray instanceof JsonStructureFieldMemberMetadataNode member)) { + throw new ParseException("The fixed-length bit array field class is not supported in CTF 1.8."); //$NON-NLS-1$ + } + JsonElement fieldClassElement = member.getFieldClass(); + if (fieldClassElement == null || !fieldClassElement.isJsonObject()) { + throw new ParseException(getClass().getName() + " fieldclass must be a json object."); //$NON-NLS-1$ + } + JsonObject fieldClass = fieldClassElement.getAsJsonObject(); + + if (!fieldClass.has(JsonMetadataStrings.LENGTH)) { + throw new ParseException("The length is a required property for the fixed-length bit array field class."); //$NON-NLS-1$ + } + length = fieldClass.get(JsonMetadataStrings.LENGTH).getAsInt(); + if (length <= 0) { + throw new ParseException("Unsupported fixed-length bit array length: " + length); //$NON-NLS-1$ + } + if (!fieldClass.has(JsonMetadataStrings.BYTE_ORDER)) { + throw new ParseException("The byte order is a required property for the fixed-length bit array field class."); //$NON-NLS-1$ + } + CTFJsonMetadataNode bo = new CTFJsonMetadataNode(bitArray, CTFParser.tokenNames[CTFParser.UNARY_EXPRESSION_STRING], fieldClass.get(JsonMetadataStrings.BYTE_ORDER).getAsString()); + ByteOrder byteOrder = ByteOrderParser.INSTANCE.parse(bo, new ByteOrderParser.Param(trace)); + + BitOrder bitOrder = BitOrder.fromByteOrder(byteOrder); + if (fieldClass.has(JsonMetadataStrings.BIT_ORDER)) { + CTFJsonMetadataNode bitOrderNode = new CTFJsonMetadataNode(bitArray, CTFParser.tokenNames[CTFParser.UNARY_EXPRESSION_STRING], fieldClass.get(JsonMetadataStrings.BIT_ORDER).getAsString()); + bitOrder = BitOrderParser.INSTANCE.parse(bitOrderNode, null); + } + + if (fieldClass.has(JsonMetadataStrings.ALIGNMENT)) { + alignment = AlignmentParser.INSTANCE.parse(bitArray, null); + } + + return new BitArrayDeclaration(length, byteOrder, bitOrder, alignment); + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/tsdl/bitmap/BitMapDeclarationParser.java b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/tsdl/bitmap/BitMapDeclarationParser.java new file mode 100644 index 0000000000..021f234109 --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/tsdl/bitmap/BitMapDeclarationParser.java @@ -0,0 +1,123 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.bitmap; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.tracecompass.ctf.core.event.types.BitArrayDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.BitMapDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.IntegerRange; +import org.eclipse.tracecompass.ctf.core.trace.CTFTrace; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.ICommonTreeParser; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.JsonStructureFieldMemberMetadataNode; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.ParseException; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.bitarray.BitArrayDeclarationParser; +import org.eclipse.tracecompass.internal.ctf.core.event.types.ICTFMetadataNode; +import org.eclipse.tracecompass.internal.ctf.core.utils.JsonMetadataStrings; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** + * According to the documentation: + * https://diamon.org/ctf/#fl-bm-fc + * + * A fixed-length bit map field class is a fixed-length bit array field class + * which associates names to bits with its required flags property. Each flag + * maps a name to an integer range set of bit indexes + * + * @author Arnaud Fiorini + */ +public final class BitMapDeclarationParser implements ICommonTreeParser { + + /** + * Parameter object with only a trace in it + */ + @NonNullByDefault + public static final class Param implements ICommonTreeParserParameter { + private final CTFTrace fTrace; + + /** + * Constructor + * + * @param trace + * the trace + */ + public Param(CTFTrace trace) { + fTrace = trace; + } + } + + /** + * Instance + */ + public static final BitMapDeclarationParser INSTANCE = new BitMapDeclarationParser(); + + private BitMapDeclarationParser() { + } + + @Override + public @NonNull BitMapDeclaration parse(ICTFMetadataNode bitMap, ICommonTreeParserParameter param) throws ParseException { + if (!(param instanceof Param)) { + throw new IllegalArgumentException("Param must be a " + Param.class.getCanonicalName()); //$NON-NLS-1$ + } + CTFTrace trace = ((Param) param).fTrace; + + if (!(bitMap instanceof JsonStructureFieldMemberMetadataNode member)) { + throw new ParseException("The fixed-length bit map field class is not supported in CTF 1.8."); //$NON-NLS-1$ + } + /* A bit map is a bit array with flags */ + BitArrayDeclaration bitArray = BitArrayDeclarationParser.INSTANCE.parse(bitMap, new BitArrayDeclarationParser.Param(trace)); + JsonObject fieldClass = member.getFieldClass().getAsJsonObject(); + + JsonElement flagsElement = fieldClass.get(JsonMetadataStrings.FLAGS); + if (flagsElement == null || !flagsElement.isJsonObject() || flagsElement.getAsJsonObject().isEmpty()) { + throw new ParseException("The flags property of a fixed-length bit map field class must be a non-empty JSON object."); //$NON-NLS-1$ + } + + Map> flags = new LinkedHashMap<>(); + for (Entry flag : flagsElement.getAsJsonObject().entrySet()) { + flags.put(flag.getKey(), parseBitRanges(flag.getKey(), flag.getValue(), bitArray.getLength())); + } + if (flags.isEmpty()) { + throw new ParseException("The flags of a bit map field class must contain one or more properties."); //$NON-NLS-1$ + } + + return new BitMapDeclaration(bitArray.getLength(), bitArray.getByteOrder(), bitArray.getBitOrder(), bitArray.getAlignment(), flags); + } + + private static List parseBitRanges(String flagName, JsonElement rangesElement, int length) throws ParseException { + if (!rangesElement.isJsonArray() || rangesElement.getAsJsonArray().isEmpty()) { + throw new ParseException("The bit ranges of flag " + flagName + " must be a non-empty JSON array."); //$NON-NLS-1$ //$NON-NLS-2$ + } + List ranges = new ArrayList<>(); + for (JsonElement rangeElement : rangesElement.getAsJsonArray()) { + if (!rangeElement.isJsonArray() || rangeElement.getAsJsonArray().size() != 2) { + throw new ParseException("Invalid bit range for flag " + flagName + ": " + rangeElement); //$NON-NLS-1$ //$NON-NLS-2$ + } + JsonArray rangeArray = rangeElement.getAsJsonArray(); + long start = rangeArray.get(0).getAsLong(); + long end = rangeArray.get(1).getAsLong(); + if (start < 0 || start > end || end >= length) { + throw new ParseException("Invalid bit range for flag " + flagName + ": " + rangeElement); //$NON-NLS-1$ //$NON-NLS-2$ + } + ranges.add(new IntegerRange(start, end)); + } + return ranges; + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/tsdl/optional/OptionalDeclarationParser.java b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/tsdl/optional/OptionalDeclarationParser.java new file mode 100644 index 0000000000..63a0fd1fec --- /dev/null +++ b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/event/metadata/tsdl/optional/OptionalDeclarationParser.java @@ -0,0 +1,149 @@ +/********************************************************************** + * Copyright (c) 2026 École Polytechnique de Montréal + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License 2.0 which + * accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +package org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.optional; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.tracecompass.ctf.core.event.metadata.DeclarationScope; +import org.eclipse.tracecompass.ctf.core.event.types.IDeclaration; +import org.eclipse.tracecompass.ctf.core.event.types.IntegerRange; +import org.eclipse.tracecompass.ctf.core.event.types.OptionalDeclaration; +import org.eclipse.tracecompass.ctf.core.trace.CTFTrace; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.AbstractScopedCommonTreeParser; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.JsonStructureFieldMemberMetadataNode; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.ParseException; +import org.eclipse.tracecompass.internal.ctf.core.event.metadata.tsdl.TypeAliasParser; +import org.eclipse.tracecompass.internal.ctf.core.event.types.ICTFMetadataNode; +import org.eclipse.tracecompass.internal.ctf.core.utils.JsonMetadataStrings; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** + * According to the documentation: + * https://diamon.org/ctf/#opt-fc + * + * An optional field class describes optional fields. An optional field is, + * depending on the value of a previously decoded selector field, an instance + * of the field class of the field-class property or a zero-bit field. + * + * The selector field is located with the required selector-field-location + * property. If the selector field is an integer field, the + * selector-field-ranges property is required and the optional field is enabled + * when the value of the selector field is within those ranges. If the + * selector field is a boolean field, the optional field is enabled when the + * selector field is true. + * + * @author Arnaud Fiorini + */ +public final class OptionalDeclarationParser extends AbstractScopedCommonTreeParser { + + /** + * Parameters for the optional parser + */ + @NonNullByDefault + public static final class Param implements ICommonTreeParserParameter { + private final CTFTrace fTrace; + private final DeclarationScope fScope; + + /** + * Parameter constructor + * + * @param trace + * the trace + * @param scope + * the declaration scope + */ + public Param(CTFTrace trace, DeclarationScope scope) { + fTrace = trace; + fScope = scope; + } + } + + /** + * Instance + */ + public static final OptionalDeclarationParser INSTANCE = new OptionalDeclarationParser(); + + private OptionalDeclarationParser() { + } + + @Override + public OptionalDeclaration parse(ICTFMetadataNode node, ICommonTreeParserParameter param) throws ParseException { + if (!(param instanceof Param)) { + throw new IllegalArgumentException("Param must be a " + Param.class.getCanonicalName()); //$NON-NLS-1$ + } + if (!(node instanceof JsonStructureFieldMemberMetadataNode member)) { + throw new ParseException("The optional field class is not supported in CTF 1.8."); //$NON-NLS-1$ + } + JsonElement fieldClassElement = member.getFieldClass(); + if (fieldClassElement == null || !fieldClassElement.isJsonObject()) { + throw new ParseException(getClass().getName() + " fieldclass must be a json object."); //$NON-NLS-1$ + } + JsonObject fieldClass = fieldClassElement.getAsJsonObject(); + + JsonElement selectorLocation = fieldClass.get(JsonMetadataStrings.SELECTOR_FIELD_LOCATION); + if (selectorLocation == null || !selectorLocation.isJsonObject()) { + throw new ParseException("Optional field class requires a selector-field-location property"); //$NON-NLS-1$ + } + JsonElement pathElement = selectorLocation.getAsJsonObject().get(JsonMetadataStrings.PATH); + if (pathElement == null || !pathElement.isJsonArray() || pathElement.getAsJsonArray().isEmpty()) { + throw new ParseException("Optional field class selector-field-location requires a non-empty path"); //$NON-NLS-1$ + } + JsonArray path = pathElement.getAsJsonArray(); + String selectorName = path.get(path.size() - 1).getAsString(); + + JsonElement optionalFieldClass = fieldClass.get(JsonMetadataStrings.FIELD_CLASS); + if (optionalFieldClass == null || !optionalFieldClass.isJsonObject()) { + throw new ParseException("Optional field class requires a field-class property"); //$NON-NLS-1$ + } + + List selectorRanges = null; + JsonElement rangesElement = fieldClass.get(JsonMetadataStrings.SELECTOR_FIELD_RANGES); + if (rangesElement != null) { + try { + selectorRanges = parseRanges(rangesElement); + } catch (ArithmeticException exception) { + throw new ParseException("The selector field range contained an invalid integer: " + exception.getMessage()); //$NON-NLS-1$ + } + } + + CTFTrace trace = ((Param) param).fTrace; + DeclarationScope scope = ((Param) param).fScope; + JsonStructureFieldMemberMetadataNode fieldNode = new JsonStructureFieldMemberMetadataNode(node, "", "", "", optionalFieldClass.getAsJsonObject()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + IDeclaration fieldDeclaration = TypeAliasParser.INSTANCE.parse(fieldNode, new TypeAliasParser.Param(trace, scope)); + + return new OptionalDeclaration(selectorName, fieldDeclaration, selectorRanges); + } + + private static List parseRanges(JsonElement rangesElement) throws ParseException { + if (!rangesElement.isJsonArray() || rangesElement.getAsJsonArray().isEmpty()) { + throw new ParseException("Optional field class selector-field-ranges must be a non-empty JSON array"); //$NON-NLS-1$ + } + List ranges = new ArrayList<>(); + for (JsonElement rangeElement : rangesElement.getAsJsonArray()) { + if (!rangeElement.isJsonArray() || rangeElement.getAsJsonArray().size() != 2) { + throw new ParseException("Invalid selector field range: " + rangeElement); //$NON-NLS-1$ + } + JsonArray rangeArray = rangeElement.getAsJsonArray(); + long start = rangeArray.get(0).getAsBigInteger().longValueExact(); + long end = rangeArray.get(1).getAsBigInteger().longValueExact(); + if (start > end) { + throw new ParseException("Invalid selector field range: " + rangeElement); //$NON-NLS-1$ + } + ranges.add(new IntegerRange(start, end)); + } + return ranges; + } +} diff --git a/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/utils/JsonMetadataStrings.java b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/utils/JsonMetadataStrings.java index ff6b45b7e7..11c1d66807 100644 --- a/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/utils/JsonMetadataStrings.java +++ b/ctf/org.eclipse.tracecompass.ctf.core/src/org/eclipse/tracecompass/internal/ctf/core/utils/JsonMetadataStrings.java @@ -279,6 +279,39 @@ private JsonMetadataStrings() { */ public static final String ELEMENT_FIELD_CLASS = "element-field-class"; //$NON-NLS-1$ + /** + * Field string for the packet header field class of a trace class + */ + public static final String PACKET_HEADER_FIELD_CLASS = "packet-header-field-class"; //$NON-NLS-1$ + + /** + * Field string for the packet context field class of a data stream class + */ + public static final String PACKET_CONTEXT_FIELD_CLASS = "packet-context-field-class"; //$NON-NLS-1$ + + /** + * Field string for the event record header field class of a data stream + * class + */ + public static final String EVENT_RECORD_HEADER_FIELD_CLASS = "event-record-header-field-class"; //$NON-NLS-1$ + + /** + * Field string for the event record common context field class of a data + * stream class + */ + public static final String EVENT_RECORD_COMMON_CONTEXT_FIELD_CLASS = "event-record-common-context-field-class"; //$NON-NLS-1$ + + /** + * Field string for the specific context field class of an event record + * class + */ + public static final String SPECIFIC_CONTEXT_FIELD_CLASS = "specific-context-field-class"; //$NON-NLS-1$ + + /** + * Field string for the payload field class of an event record class + */ + public static final String PAYLOAD_FIELD_CLASS = "payload-field-class"; //$NON-NLS-1$ + /** * Field string for member classes in a structure */ @@ -299,9 +332,30 @@ private JsonMetadataStrings() { */ public static final String BYTE_ORDER = "byte-order"; //$NON-NLS-1$ + /** + * Field string for bit order + */ + public static final String BIT_ORDER = "bit-order"; //$NON-NLS-1$ + /** * Field string for the metadata type of a blob */ public static final String MEDIA_TYPE = "media-type"; //$NON-NLS-1$ + /** + * Field string for the flags of a fixed-length bit map field class + */ + public static final String FLAGS = "flags"; //$NON-NLS-1$ + + /** + * Field string for the selector field location of an optional or variant + * field class + */ + public static final String SELECTOR_FIELD_LOCATION = "selector-field-location"; //$NON-NLS-1$ + + /** + * Field string for the selector field ranges of an optional field class + */ + public static final String SELECTOR_FIELD_RANGES = "selector-field-ranges"; //$NON-NLS-1$ + }