From d21fa58e1e4825f498c546417a33a4b60c978b87 Mon Sep 17 00:00:00 2001 From: Alejandro Valerio Date: Mon, 15 Dec 2025 08:55:12 -0600 Subject: [PATCH 1/2] - Making BOOLEAN explicitly unsupported for per-value encryption and always defaults to per-block encryption. - Updated EncryptionSequencer to check explicitly for BOOLEAN and RLE_DICTIONARY, and updated comments. --- src/server/encryption_sequencer.cpp | 20 ++++--- src/server/encryption_sequencer_test.cpp | 66 ++++++++++++++++++++++++ src/server/parquet_utils.cpp | 15 +++++- src/server/parquet_utils_test.cpp | 42 +++++++++++++++ 4 files changed, 134 insertions(+), 9 deletions(-) diff --git a/src/server/encryption_sequencer.cpp b/src/server/encryption_sequencer.cpp index 0204d25..aa82c36 100644 --- a/src/server/encryption_sequencer.cpp +++ b/src/server/encryption_sequencer.cpp @@ -129,8 +129,9 @@ bool DataBatchEncryptionSequencer::ConvertAndEncrypt(const std::vector& /* * Note on try-catch block: - * - When fully done, ConvertAndEncrypt will support per-value encryption for all cases. - * - This try-catch block is transitional to allow features to be developed incrementally until all features are + * - When fully done, ConvertAndEncrypt will support per-value encryption for all cases, except for + * (1) BOOLEAN datatype and (2) RLE_DICTIONARY encoding. + * - This try-catch block allows features to be developed incrementally until all features are * complete: Compressions, Encodings, Page types, Datatypes. * - During development if a feature is not yet supported, UnsupportedExceptions are caught and the fallback to * per-block encryption is used. @@ -160,18 +161,21 @@ bool DataBatchEncryptionSequencer::ConvertAndEncrypt(const std::vector& // Allow fallback to per-block encryption, only for explicitly unsupported conditions. See note above. catch (const DBPSUnsupportedException& e) { - // Compression: Only UNCOMPRESSED and SNAPPY are supported + // Compression: Only UNCOMPRESSED and SNAPPY are currently supported const bool is_compression_supported = (compression_ == CompressionCodec::UNCOMPRESSED || compression_ == CompressionCodec::SNAPPY); - // Format: Only PLAIN is supported - const bool is_format_supported = (format_ == Format::PLAIN); + // Format: Only PLAIN is currently supported + // RLE_DICTIONARY is permanently unsupported for per-value encryption since the values are not present in the + // `plaintext` data, only references to them. + const bool is_format_supported = (format_ == Format::PLAIN && format_ != Format::RLE_DICTIONARY); // Page type: All are supported (DATA_PAGE_V1, DATA_PAGE_V2, DICTIONARY_PAGE) const bool is_page_supported = true; - - // Datatype: All datatypes are supported. - const bool is_datatype_supported = true; + + // Datatype: All datatypes are supported except BOOLEAN. + // BOOLEAN is permanently unsupported for per-value encryption and always defaults to per-block encryption. + const bool is_datatype_supported = (datatype_ != Type::BOOLEAN); if (is_compression_supported && is_format_supported && is_page_supported && is_datatype_supported) { // All conditions are supported, therefore an DBPSUnsupportedException exception should not have happened. diff --git a/src/server/encryption_sequencer_test.cpp b/src/server/encryption_sequencer_test.cpp index 38c6cd5..6bc3bbf 100644 --- a/src/server/encryption_sequencer_test.cpp +++ b/src/server/encryption_sequencer_test.cpp @@ -417,6 +417,72 @@ TEST(EncryptionSequencer, ResultStorage) { } +// Test BOOLEAN type uses per-block encryption (not per-value) +TEST(EncryptionSequencer, BooleanTypeUsesPerBlockEncryption) { + // BOOLEAN is permanently unsupported for per-value operations + // and always defaults to per-block encryption + std::vector boolean_data = {0xB4, 0xFF, 0x00}; // some boolean bit-packed data + + DataBatchEncryptionSequencer sequencer( + "bool_column", + Type::BOOLEAN, + std::nullopt, + CompressionCodec::UNCOMPRESSED, + Format::PLAIN, + {{"page_type", "DICTIONARY_PAGE"}}, + CompressionCodec::UNCOMPRESSED, + "test_key", + "test_user", + "{}", + {} + ); + + bool result = sequencer.ConvertAndEncrypt(boolean_data); + ASSERT_TRUE(result) << "BOOLEAN encryption failed: " << sequencer.error_stage_ << " - " << sequencer.error_message_; + + // Verify it used per-block encryption mode + ASSERT_TRUE(sequencer.encryption_metadata_.count("encrypt_mode_dict_page") > 0); + EXPECT_EQ(sequencer.encryption_metadata_.at("encrypt_mode_dict_page"), "per_block"); + + // Verify round-trip works + bool decrypt_result = sequencer.ConvertAndDecrypt(sequencer.encrypted_result_); + ASSERT_TRUE(decrypt_result) << "BOOLEAN decryption failed: " << sequencer.error_stage_ << " - " << sequencer.error_message_; + EXPECT_EQ(sequencer.decrypted_result_, boolean_data); +} + +// Test RLE_DICTIONARY format uses per-block encryption (not per-value) +TEST(EncryptionSequencer, RleDictionaryFormatUsesPerBlockEncryption) { + // RLE_DICTIONARY is permanently unsupported for per-value operations + // since the values are not present in the data, only references to them + std::vector rle_dict_data = {0x02, 0x00, 0x00, 0x00, 0x01}; // some RLE dictionary encoded data + + DataBatchEncryptionSequencer sequencer( + "dict_column", + Type::INT32, + std::nullopt, + CompressionCodec::UNCOMPRESSED, + Format::RLE_DICTIONARY, + {{"page_type", "DICTIONARY_PAGE"}}, + CompressionCodec::UNCOMPRESSED, + "test_key", + "test_user", + "{}", + {} + ); + + bool result = sequencer.ConvertAndEncrypt(rle_dict_data); + ASSERT_TRUE(result) << "RLE_DICTIONARY encryption failed: " << sequencer.error_stage_ << " - " << sequencer.error_message_; + + // Verify it used per-block encryption mode + ASSERT_TRUE(sequencer.encryption_metadata_.count("encrypt_mode_dict_page") > 0); + EXPECT_EQ(sequencer.encryption_metadata_.at("encrypt_mode_dict_page"), "per_block"); + + // Verify round-trip works + bool decrypt_result = sequencer.ConvertAndDecrypt(sequencer.encrypted_result_); + ASSERT_TRUE(decrypt_result) << "RLE_DICTIONARY decryption failed: " << sequencer.error_stage_ << " - " << sequencer.error_message_; + EXPECT_EQ(sequencer.decrypted_result_, rle_dict_data); +} + // Test FIXED_LEN_BYTE_ARRAY validation TEST(EncryptionSequencer, FixedLenByteArrayValidation) { diff --git a/src/server/parquet_utils.cpp b/src/server/parquet_utils.cpp index 15bebad..9cc7905 100644 --- a/src/server/parquet_utils.cpp +++ b/src/server/parquet_utils.cpp @@ -129,6 +129,8 @@ inline static size_t GetFixedElemSizeOrThrow(Type::type datatype, const std::opt } case Type::UNDEFINED: return 1; + case Type::BOOLEAN: + throw InvalidInputException("BOOLEAN is bit-sized; not fixed byte-sized"); case Type::BYTE_ARRAY: throw InvalidInputException("BYTE_ARRAY is variable-length; not fixed-size"); default: @@ -146,13 +148,19 @@ std::vector SliceValueBytesIntoRawBytes( // RLE_DICTIONARY is not supported for per-value operations since the values themselves are not present in the data, // only references to them. if (format == Format::RLE_DICTIONARY) { - throw DBPSUnsupportedException("Unsupported format: RLE_DICTIONARY is not supported for per-value operations"); + throw DBPSUnsupportedException("Unsupported format: RLE_DICTIONARY is not supported for per-value operations " + "since values are not present in the data, only references to them."); } if (format != Format::PLAIN) { throw DBPSUnsupportedException("On SliceValueBytesIntoRawBytes, unsupported format: " + std::string(to_string(format))); } + // BOOLEAN: boolean values are bit-encoded and not expanded as bytes + if (datatype == Type::BOOLEAN) { + throw DBPSUnsupportedException("On SliceValueBytesIntoRawBytes, BOOLEAN datatype is not supported for converting to bytes."); + } + // Variable-length BYTE_ARRAY: parse [4-byte len][bytes...] elements in order. // This is the Parquet specific encoding for BYTE_ARRAY. if (datatype == Type::BYTE_ARRAY) { @@ -211,6 +219,11 @@ std::vector CombineRawBytesIntoValueBytes( throw DBPSUnsupportedException("On CombineRawBytesIntoValueBytes, unsupported format: " + std::string(to_string(format))); } + // BOOLEAN: boolean values are bit-encoded and not expanded as bytes + if (datatype == Type::BOOLEAN) { + throw DBPSUnsupportedException("On CombineRawBytesIntoValueBytes, BOOLEAN datatype is not supported for converting to bytes."); + } + if (datatype == Type::BYTE_ARRAY) { std::vector out; size_t total = 0; diff --git a/src/server/parquet_utils_test.cpp b/src/server/parquet_utils_test.cpp index b628473..4e9b388 100644 --- a/src/server/parquet_utils_test.cpp +++ b/src/server/parquet_utils_test.cpp @@ -222,6 +222,13 @@ TEST(ParquetUtils, ParseValueBytesIntoTypedList_UnsupportedFormat) { DBPSUnsupportedException); } +TEST(ParquetUtils, ParseValueBytesIntoTypedList_BOOLEAN_Throws) { + // BOOLEAN type is not supported for per-value parsing + std::vector bytes = {0xB4}; // 8 boolean values bit-packed + EXPECT_THROW(ParseValueBytesIntoTypedList(bytes, Type::BOOLEAN, std::nullopt, Format::PLAIN), + DBPSUnsupportedException); +} + TEST(ParquetUtils, ParseValueBytesIntoTypedList_InvalidDataSize) { std::vector bytes = {0x01, 0x02, 0x03}; // 3 bytes, not divisible by sizeof(int32_t) EXPECT_THROW(ParseValueBytesIntoTypedList(bytes, Type::INT32, std::nullopt, Format::PLAIN), @@ -236,6 +243,22 @@ TEST(ParquetUtils, SliceValueBytesIntoRawBytes_INT32) { EXPECT_EQ(out[1], (std::vector{0x0D,0x0C,0x0B,0x0A})); } +TEST(ParquetUtils, SliceValueBytesIntoRawBytes_BOOLEAN_Throws) { + // BOOLEAN is bit-packed and not supported for per-value slicing + std::vector bytes = {0xB4}; // 8 boolean values bit-packed + EXPECT_THROW( + SliceValueBytesIntoRawBytes(bytes, Type::BOOLEAN, std::nullopt, Format::PLAIN), + DBPSUnsupportedException); +} + +TEST(ParquetUtils, SliceValueBytesIntoRawBytes_BOOLEAN_MultipleBytes_Throws) { + // Multiple bytes of boolean data + std::vector bytes = {0xFF, 0x00, 0xAA, 0x55}; // 32 boolean values + EXPECT_THROW( + SliceValueBytesIntoRawBytes(bytes, Type::BOOLEAN, std::nullopt, Format::PLAIN), + DBPSUnsupportedException); +} + TEST(ParquetUtils, SliceValueBytesIntoRawBytes_INT96) { std::vector bytes = { 0x01,0x02,0x03,0x04, @@ -290,6 +313,25 @@ TEST(ParquetUtils, CombineRawBytesIntoValueBytes_INT32) { EXPECT_EQ(out, (std::vector{0x04,0x03,0x02,0x01, 0x0D,0x0C,0x0B,0x0A})); } +TEST(ParquetUtils, CombineRawBytesIntoValueBytes_BOOLEAN_Throws) { + // BOOLEAN is bit-packed and not supported for per-value combining + std::vector elems = { + {0x01}, // single byte representing boolean value(s) + {0x00} + }; + EXPECT_THROW( + CombineRawBytesIntoValueBytes(elems, Type::BOOLEAN, std::nullopt, Format::PLAIN), + DBPSUnsupportedException); +} + +TEST(ParquetUtils, CombineRawBytesIntoValueBytes_BOOLEAN_EmptyInput_Throws) { + // Even empty input should throw for BOOLEAN type + std::vector elems; + EXPECT_THROW( + CombineRawBytesIntoValueBytes(elems, Type::BOOLEAN, std::nullopt, Format::PLAIN), + DBPSUnsupportedException); +} + TEST(ParquetUtils, CombineRawBytesIntoValueBytes_BYTE_ARRAY) { std::vector elems = { {'h','i'}, From 69a4b4e9a9b075e60ac7b04ec851bfbcdd4924d4 Mon Sep 17 00:00:00 2001 From: Alejandro Valerio Date: Mon, 15 Dec 2025 13:32:50 -0600 Subject: [PATCH 2/2] - Comment updates for clarity. --- src/server/encryption_sequencer.cpp | 4 ++-- src/server/encryption_sequencer_test.cpp | 12 +++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/server/encryption_sequencer.cpp b/src/server/encryption_sequencer.cpp index aa82c36..4e25c3d 100644 --- a/src/server/encryption_sequencer.cpp +++ b/src/server/encryption_sequencer.cpp @@ -166,7 +166,7 @@ bool DataBatchEncryptionSequencer::ConvertAndEncrypt(const std::vector& compression_ == CompressionCodec::SNAPPY); // Format: Only PLAIN is currently supported - // RLE_DICTIONARY is permanently unsupported for per-value encryption since the values are not present in the + // RLE_DICTIONARY is not supported for per-value encryption since the values are not present in the // `plaintext` data, only references to them. const bool is_format_supported = (format_ == Format::PLAIN && format_ != Format::RLE_DICTIONARY); @@ -174,7 +174,7 @@ bool DataBatchEncryptionSequencer::ConvertAndEncrypt(const std::vector& const bool is_page_supported = true; // Datatype: All datatypes are supported except BOOLEAN. - // BOOLEAN is permanently unsupported for per-value encryption and always defaults to per-block encryption. + // BOOLEAN is not supported for per-value encryption and always defaults to per-block encryption. const bool is_datatype_supported = (datatype_ != Type::BOOLEAN); if (is_compression_supported && is_format_supported && is_page_supported && is_datatype_supported) { diff --git a/src/server/encryption_sequencer_test.cpp b/src/server/encryption_sequencer_test.cpp index 6bc3bbf..d55fa28 100644 --- a/src/server/encryption_sequencer_test.cpp +++ b/src/server/encryption_sequencer_test.cpp @@ -419,8 +419,7 @@ TEST(EncryptionSequencer, ResultStorage) { // Test BOOLEAN type uses per-block encryption (not per-value) TEST(EncryptionSequencer, BooleanTypeUsesPerBlockEncryption) { - // BOOLEAN is permanently unsupported for per-value operations - // and always defaults to per-block encryption + // BOOLEAN is not supported for per-value encryption and always defaults to per-block encryption std::vector boolean_data = {0xB4, 0xFF, 0x00}; // some boolean bit-packed data DataBatchEncryptionSequencer sequencer( @@ -440,8 +439,8 @@ TEST(EncryptionSequencer, BooleanTypeUsesPerBlockEncryption) { bool result = sequencer.ConvertAndEncrypt(boolean_data); ASSERT_TRUE(result) << "BOOLEAN encryption failed: " << sequencer.error_stage_ << " - " << sequencer.error_message_; - // Verify it used per-block encryption mode - ASSERT_TRUE(sequencer.encryption_metadata_.count("encrypt_mode_dict_page") > 0); + // Verify per-block encryption mode as used. + ASSERT_TRUE(sequencer.encryption_metadata_.count("encrypt_mode_dict_page") == 1); EXPECT_EQ(sequencer.encryption_metadata_.at("encrypt_mode_dict_page"), "per_block"); // Verify round-trip works @@ -452,8 +451,7 @@ TEST(EncryptionSequencer, BooleanTypeUsesPerBlockEncryption) { // Test RLE_DICTIONARY format uses per-block encryption (not per-value) TEST(EncryptionSequencer, RleDictionaryFormatUsesPerBlockEncryption) { - // RLE_DICTIONARY is permanently unsupported for per-value operations - // since the values are not present in the data, only references to them + // RLE_DICTIONARY is not supported for per-value encryption since the values are not present in the data, only references to them std::vector rle_dict_data = {0x02, 0x00, 0x00, 0x00, 0x01}; // some RLE dictionary encoded data DataBatchEncryptionSequencer sequencer( @@ -474,7 +472,7 @@ TEST(EncryptionSequencer, RleDictionaryFormatUsesPerBlockEncryption) { ASSERT_TRUE(result) << "RLE_DICTIONARY encryption failed: " << sequencer.error_stage_ << " - " << sequencer.error_message_; // Verify it used per-block encryption mode - ASSERT_TRUE(sequencer.encryption_metadata_.count("encrypt_mode_dict_page") > 0); + ASSERT_TRUE(sequencer.encryption_metadata_.count("encrypt_mode_dict_page") == 1); EXPECT_EQ(sequencer.encryption_metadata_.at("encrypt_mode_dict_page"), "per_block"); // Verify round-trip works