Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions _codeql_detected_source_root
173 changes: 173 additions & 0 deletions runtime/cpp/emboss_array_view.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#define EMBOSS_RUNTIME_CPP_EMBOSS_ARRAY_VIEW_H_

#include <cstddef>
#include <cstdint>
#include <iterator>
#include <tuple>
#include <type_traits>
Expand Down Expand Up @@ -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<TargetBits, SourceBits>(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<SourceBits, TargetBits>(input_buffer, element_count);
}

private:
// This uses the same technique to select the correct definition of
// SizeOfBuffer() as in the SizeInBits()/SizeInBytes() selection above.
Expand All @@ -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<const ::std::uint8_t *>(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</**/ ::std::uint64_t>(1) << bit);
}
}
bit_offset += SourceBits;

// Write the value to the output buffer
if (TargetBits == 8) {
output_buffer[i] = static_cast</**/ ::std::uint8_t>(value);
} else if (TargetBits == 16) {
::std::uint16_t *output16 =
reinterpret_cast</**/ ::std::uint16_t *>(output_buffer);
output16[i] = static_cast</**/ ::std::uint16_t>(value);
} else if (TargetBits == 32) {
::std::uint32_t *output32 =
reinterpret_cast</**/ ::std::uint32_t *>(output_buffer);
output32[i] = static_cast</**/ ::std::uint32_t>(value);
} else if (TargetBits == 64) {
::std::uint64_t *output64 =
reinterpret_cast</**/ ::std::uint64_t *>(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</**/ ::std::uint8_t *>(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<const ::std::uint16_t *>(input_buffer);
value = input16[i];
} else if (SourceBits == 32) {
const ::std::uint32_t *input32 =
reinterpret_cast<const ::std::uint32_t *>(input_buffer);
value = input32[i];
} else if (SourceBits == 64) {
const ::std::uint64_t *input64 =
reinterpret_cast<const ::std::uint64_t *>(input_buffer);
value = input64[i];
}

// Mask the value to TargetBits
const ::std::uint64_t mask =
(TargetBits == 64) ? ::std::uint64_t(-1)
: ((static_cast</**/ ::std::uint64_t>(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:
Expand Down
164 changes: 164 additions & 0 deletions runtime/cpp/test/emboss_array_view_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<FixedUIntView<8>, 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<FixedUIntView<8>, ReadWriteContiguousBuffer, 1>{
ReadWriteContiguousBuffer{packed_bytes, sizeof packed_bytes}};

bool success = byte_array.PackFrom<16, 12>(
reinterpret_cast<const ::std::uint8_t *>(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<FixedUIntView<8>, ReadWriteContiguousBuffer, 1>{
ReadWriteContiguousBuffer{packed, sizeof packed}};

EXPECT_TRUE(packed_array.PackFrom<16, 12>(
reinterpret_cast<const ::std::uint8_t *>(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<FixedUIntView<8>, 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<FixedUIntView<8>, 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<FixedUIntView<8>, 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<const ::std::uint8_t *>(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<FixedUIntView<8>, 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<FixedUIntView<8>, 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