diff --git a/_codeql_detected_source_root b/_codeql_detected_source_root new file mode 120000 index 0000000..945c9b4 --- /dev/null +++ b/_codeql_detected_source_root @@ -0,0 +1 @@ +. \ No newline at end of file diff --git a/runtime/cpp/emboss_array_view.h b/runtime/cpp/emboss_array_view.h index fa8ccd3..f7f680b 100644 --- a/runtime/cpp/emboss_array_view.h +++ b/runtime/cpp/emboss_array_view.h @@ -17,6 +17,7 @@ #define EMBOSS_RUNTIME_CPP_EMBOSS_ARRAY_VIEW_H_ #include +#include #include #include #include @@ -276,6 +277,75 @@ class GenericArrayView final { return parameters_ == other.parameters_ && buffer_ == other.buffer_; } + // Packed bit array support: UnpackedSizeInBytes calculates the size needed + // for an unpacked buffer when unpacking to TargetBits per element. + // Only available for byte arrays (kAddressableUnitSize == 8). + template <::std::size_t TargetBits, int N = 0> + typename ::std::enable_if<((void)N, kAddressableUnitSize == 8), + ::std::size_t>::type + UnpackedSizeInBytes() const { + static_assert(TargetBits > 0 && TargetBits <= 64, + "TargetBits must be between 1 and 64"); + static_assert(TargetBits % 8 == 0, + "TargetBits must be a multiple of 8"); + return ElementCount() * (TargetBits / 8); + } + + // Unpacks packed bit data from this array to an external buffer. + // TargetBits specifies the bit width of each unpacked element (must be 8, 16, + // 32, or 64). SourceBits specifies the bit width of each packed element in + // this array (e.g., 12 for 12-bit packed data). + // Returns true if successful, false if buffer is too small or parameters are + // invalid. Only available for byte arrays (kAddressableUnitSize == 8). + template <::std::size_t TargetBits, ::std::size_t SourceBits, int N = 0> + typename ::std::enable_if<((void)N, kAddressableUnitSize == 8), bool>::type + UnpackTo(::std::uint8_t *output_buffer, ::std::size_t output_buffer_size) const { + static_assert(TargetBits == 8 || TargetBits == 16 || TargetBits == 32 || + TargetBits == 64, + "TargetBits must be 8, 16, 32, or 64"); + static_assert(SourceBits > 0 && SourceBits <= 64, + "SourceBits must be between 1 and 64"); + static_assert(SourceBits <= TargetBits, + "SourceBits must be <= TargetBits"); + + if (!Ok()) return false; + + // Calculate how many SourceBits elements fit in the buffer + const ::std::size_t total_bits = SizeInBytes() * 8; + const ::std::size_t element_count = total_bits / SourceBits; + const ::std::size_t required_size = element_count * (TargetBits / 8); + if (output_buffer_size < required_size) return false; + + return UnpackToImpl(output_buffer, element_count); + } + + // Packs data from an external buffer into this array's packed format. + // SourceBits specifies the bit width of each element in the input buffer + // (must be 8, 16, 32, or 64). TargetBits specifies the bit width of each + // packed element in this array (e.g., 12 for 12-bit packed data). + // Returns true if successful, false if parameters are invalid. + // Only available for byte arrays (kAddressableUnitSize == 8). + template <::std::size_t SourceBits, ::std::size_t TargetBits, int N = 0> + typename ::std::enable_if<((void)N, kAddressableUnitSize == 8), bool>::type + PackFrom(const ::std::uint8_t *input_buffer, ::std::size_t element_count) { + static_assert(SourceBits == 8 || SourceBits == 16 || SourceBits == 32 || + SourceBits == 64, + "SourceBits must be 8, 16, 32, or 64"); + static_assert(TargetBits > 0 && TargetBits <= 64, + "TargetBits must be between 1 and 64"); + static_assert(TargetBits <= SourceBits, + "TargetBits must be <= SourceBits"); + + if (!buffer_.Ok()) return false; + + // Check if buffer can hold element_count * TargetBits bits + const ::std::size_t required_bits = element_count * TargetBits; + const ::std::size_t available_bits = SizeInBytes() * 8; + if (available_bits < required_bits) return false; + + return PackFromImpl(input_buffer, element_count); + } + private: // This uses the same technique to select the correct definition of // SizeOfBuffer() as in the SizeInBits()/SizeInBytes() selection above. @@ -292,6 +362,109 @@ class GenericArrayView final { return SizeInBits(); } + // Helper function to unpack packed bits to a wider format. + // Uses a naive implementation that extracts bits one element at a time. + template <::std::size_t TargetBits, ::std::size_t SourceBits> + bool UnpackToImpl(::std::uint8_t *output_buffer, + ::std::size_t element_count) const { + const ::std::uint8_t *input = + reinterpret_cast(buffer_.data()); + if (input == nullptr) return false; + + ::std::size_t bit_offset = 0; + for (::std::size_t i = 0; i < element_count; ++i) { + // Extract SourceBits from the packed array + ::std::uint64_t value = 0; + for (::std::size_t bit = 0; bit < SourceBits; ++bit) { + // Safe: bit is always < SourceBits <= 64, so bit < 64 + const ::std::size_t byte_index = (bit_offset + bit) / 8; + const ::std::size_t bit_index = (bit_offset + bit) % 8; + if ((input[byte_index] >> bit_index) & 1) { + value |= (static_cast(1) << bit); + } + } + bit_offset += SourceBits; + + // Write the value to the output buffer + if (TargetBits == 8) { + output_buffer[i] = static_cast(value); + } else if (TargetBits == 16) { + ::std::uint16_t *output16 = + reinterpret_cast(output_buffer); + output16[i] = static_cast(value); + } else if (TargetBits == 32) { + ::std::uint32_t *output32 = + reinterpret_cast(output_buffer); + output32[i] = static_cast(value); + } else if (TargetBits == 64) { + ::std::uint64_t *output64 = + reinterpret_cast(output_buffer); + output64[i] = value; + } + } + return true; + } + + // Helper function to pack data from a wider format to packed bits. + // Uses a naive implementation that packs bits one element at a time. + template <::std::size_t SourceBits, ::std::size_t TargetBits> + bool PackFromImpl(const ::std::uint8_t *input_buffer, + ::std::size_t element_count) { + ::std::uint8_t *output = + reinterpret_cast(buffer_.data()); + if (output == nullptr) return false; + + // Calculate and verify output buffer size + const ::std::size_t output_bytes = (element_count * TargetBits + 7) / 8; + const ::std::size_t available_bytes = buffer_.SizeInBytes(); + if (output_bytes > available_bytes) return false; + + // Clear the output buffer first + for (::std::size_t i = 0; i < output_bytes; ++i) { + output[i] = 0; + } + + ::std::size_t bit_offset = 0; + for (::std::size_t i = 0; i < element_count; ++i) { + // Read the value from the input buffer + ::std::uint64_t value = 0; + if (SourceBits == 8) { + value = input_buffer[i]; + } else if (SourceBits == 16) { + const ::std::uint16_t *input16 = + reinterpret_cast(input_buffer); + value = input16[i]; + } else if (SourceBits == 32) { + const ::std::uint32_t *input32 = + reinterpret_cast(input_buffer); + value = input32[i]; + } else if (SourceBits == 64) { + const ::std::uint64_t *input64 = + reinterpret_cast(input_buffer); + value = input64[i]; + } + + // Mask the value to TargetBits + const ::std::uint64_t mask = + (TargetBits == 64) ? ::std::uint64_t(-1) + : ((static_cast(1) + << TargetBits) - + 1); + value &= mask; + + // Pack TargetBits into the output array + for (::std::size_t bit = 0; bit < TargetBits; ++bit) { + const ::std::size_t byte_index = (bit_offset + bit) / 8; + const ::std::size_t bit_index = (bit_offset + bit) % 8; + if ((value >> bit) & 1) { + output[byte_index] |= (1 << bit_index); + } + } + bit_offset += TargetBits; + } + return true; + } + // This mess is needed to expand the parameters_ tuple into individual // arguments to the ElementView constructor. If parameters_ has M elements, // then: diff --git a/runtime/cpp/test/emboss_array_view_test.cc b/runtime/cpp/test/emboss_array_view_test.cc index 1f632e8..d904f5e 100644 --- a/runtime/cpp/test/emboss_array_view_test.cc +++ b/runtime/cpp/test/emboss_array_view_test.cc @@ -279,6 +279,170 @@ TEST(ArrayView, TextFormatOutput_MultilineComment) { } } +TEST(ArrayView, PackedBitArray_UnpackTo_12BitTo16Bit) { + // Test unpacking 12-bit packed data to 16-bit containers + // 3 elements of 12 bits each = 36 bits = 4.5 bytes, uses 5 bytes + // But ElementCount will be (6*8)/12 = 4 elements + ::std::uint8_t packed_bytes[6] = { + 0x34, 0x82, 0x67, 0xBC, 0x0A, 0x00 // Packed: 0x234, 0x678, 0xABC + }; + + auto byte_array = ArrayView, ReadWriteContiguousBuffer, 1>{ + ReadWriteContiguousBuffer{packed_bytes, sizeof packed_bytes}}; + + // Unpack to 16-bit buffer - will unpack 4 elements (48 bits / 12 bits each) + ::std::uint16_t output[4] = {0}; + bool success = byte_array.UnpackTo<16, 12>( + reinterpret_cast<::std::uint8_t *>(output), sizeof output); + EXPECT_TRUE(success); + + // Verify unpacked values (4 elements from 6 bytes) + EXPECT_EQ(0x234, output[0]); + EXPECT_EQ(0x678, output[1]); + EXPECT_EQ(0xABC, output[2]); + EXPECT_EQ(0x000, output[3]); // Last element is padding +} + +TEST(ArrayView, PackedBitArray_PackFrom_16BitTo12Bit) { + // Test packing 16-bit data to 12-bit packed format + ::std::uint16_t input[3] = {0x234, 0x678, 0xABC}; + + // Need 3 * 12 bits = 36 bits = 4.5 bytes, use 5 bytes buffer + ::std::uint8_t packed_bytes[5] = {0}; + auto byte_array = ArrayView, ReadWriteContiguousBuffer, 1>{ + ReadWriteContiguousBuffer{packed_bytes, sizeof packed_bytes}}; + + bool success = byte_array.PackFrom<16, 12>( + reinterpret_cast(input), 3); + EXPECT_TRUE(success); + + // Verify packed values + EXPECT_EQ(0x34, packed_bytes[0]); + EXPECT_EQ(0x82, packed_bytes[1]); + EXPECT_EQ(0x67, packed_bytes[2]); + EXPECT_EQ(0xBC, packed_bytes[3]); + EXPECT_EQ(0x0A, packed_bytes[4]); +} + +TEST(ArrayView, PackedBitArray_RoundTrip_12BitTo16Bit) { + // Test round-trip: pack 16-bit to 12-bit, then unpack back to 16-bit + ::std::uint16_t original[4] = {0x001, 0x123, 0x456, 0x789}; + + // Pack to 12-bit (4 * 12 bits = 48 bits = 6 bytes) + ::std::uint8_t packed[6] = {0}; + auto packed_array = ArrayView, ReadWriteContiguousBuffer, 1>{ + ReadWriteContiguousBuffer{packed, sizeof packed}}; + + EXPECT_TRUE(packed_array.PackFrom<16, 12>( + reinterpret_cast(original), 4)); + + // Verify packed bytes match expected + EXPECT_EQ(0x01, packed[0]); + EXPECT_EQ(0x30, packed[1]); + EXPECT_EQ(0x12, packed[2]); + EXPECT_EQ(0x56, packed[3]); + EXPECT_EQ(0x94, packed[4]); + EXPECT_EQ(0x78, packed[5]); + + // Unpack back to 16-bit + ::std::uint16_t unpacked[4] = {0}; + EXPECT_TRUE(packed_array.UnpackTo<16, 12>( + reinterpret_cast<::std::uint8_t *>(unpacked), sizeof unpacked)); + + // Verify values match (masked to 12 bits) + EXPECT_EQ(0x001, unpacked[0]); + EXPECT_EQ(0x123, unpacked[1]); + EXPECT_EQ(0x456, unpacked[2]); + EXPECT_EQ(0x789, unpacked[3]); +} + +TEST(ArrayView, PackedBitArray_UnpackTo_8BitTo16Bit) { + // Test unpacking 8-bit to 16-bit (simple case) + ::std::uint8_t bytes[4] = {0x12, 0x34, 0x56, 0x78}; + auto byte_array = ArrayView, ReadWriteContiguousBuffer, 1>{ + ReadWriteContiguousBuffer{bytes, sizeof bytes}}; + + ::std::uint16_t output[4] = {0}; + bool success = byte_array.UnpackTo<16, 8>( + reinterpret_cast<::std::uint8_t *>(output), sizeof output); + EXPECT_TRUE(success); + + EXPECT_EQ(0x12, output[0]); + EXPECT_EQ(0x34, output[1]); + EXPECT_EQ(0x56, output[2]); + EXPECT_EQ(0x78, output[3]); +} + +TEST(ArrayView, PackedBitArray_UnpackTo_10BitTo16Bit) { + // Test unpacking 10-bit packed data to 16-bit containers + // 4 elements * 10 bits = 40 bits = 5 bytes + ::std::uint8_t packed_bytes[5] = { + 0xFF, 0x03, // First element: 0x3FF (all 10 bits set) + 0x00, 0x00, // Second element: 0x000 + 0x55 // Third element: partial (only bottom bits) + }; + + auto byte_array = ArrayView, ReadWriteContiguousBuffer, 1>{ + ReadWriteContiguousBuffer{packed_bytes, sizeof packed_bytes}}; + + ::std::uint16_t output[4] = {0}; + bool success = byte_array.UnpackTo<16, 10>( + reinterpret_cast<::std::uint8_t *>(output), sizeof output); + EXPECT_TRUE(success); + + EXPECT_EQ(0x3FF, output[0]); + EXPECT_EQ(0x000, output[1]); + EXPECT_EQ(0x155, output[2]); +} + +TEST(ArrayView, PackedBitArray_PackFrom_BufferTooSmall) { + // Test that packing fails when buffer is too small + ::std::uint16_t input[4] = {0x123, 0x456, 0x789, 0xABC}; + + // Buffer is too small for 4 * 12 bits + ::std::uint8_t packed_bytes[4] = {0}; // Need 6 bytes, only have 4 + auto byte_array = ArrayView, ReadWriteContiguousBuffer, 1>{ + ReadWriteContiguousBuffer{packed_bytes, sizeof packed_bytes}}; + + // This should fail because buffer is too small + bool success = byte_array.PackFrom<16, 12>( + reinterpret_cast(input), 4); + EXPECT_FALSE(success); +} + +TEST(ArrayView, PackedBitArray_UnpackTo_BufferTooSmall) { + // Test that unpacking fails when output buffer is too small + ::std::uint8_t packed_bytes[6] = {0x34, 0x12, 0x78, 0x56, 0xBC, 0x9A}; + auto byte_array = ArrayView, ReadWriteContiguousBuffer, 1>{ + ReadWriteContiguousBuffer{packed_bytes, sizeof packed_bytes}}; + + // Output buffer too small + ::std::uint16_t output[2] = {0}; // Need 3 elements, only have 2 + bool success = byte_array.UnpackTo<16, 12>( + reinterpret_cast<::std::uint8_t *>(output), sizeof output); + EXPECT_FALSE(success); +} + +TEST(ArrayView, PackedBitArray_UnpackedSizeInBytes) { + // Test UnpackedSizeInBytes calculation for byte array + // The array has 6 UInt:8 elements, so ElementCount() = 6 + ::std::uint8_t bytes[6] = {0}; + auto byte_array = ArrayView, ReadWriteContiguousBuffer, 1>{ + ReadWriteContiguousBuffer{bytes, sizeof bytes}}; + + // 6 elements * 1 byte per element = 6 bytes for 8-bit target + EXPECT_EQ(6, byte_array.UnpackedSizeInBytes<8>()); + + // 6 elements * 2 bytes per element = 12 bytes for 16-bit target + EXPECT_EQ(12, byte_array.UnpackedSizeInBytes<16>()); + + // 6 elements * 4 bytes per element = 24 bytes for 32-bit target + EXPECT_EQ(24, byte_array.UnpackedSizeInBytes<32>()); + + // 6 elements * 8 bytes per element = 48 bytes for 64-bit target + EXPECT_EQ(48, byte_array.UnpackedSizeInBytes<64>()); +} + } // namespace test } // namespace support } // namespace emboss