From: Matthew N. Heler Date: Sun, 29 Mar 2026 02:27:57 +0000 (-0500) Subject: rgw: add range projection helpers for encrypted and compressed objects X-Git-Tag: v21.0.1~125^2~2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=87c7c45ea6ad2d16976cce6f6ddd8cd14248ddef;p=ceph.git rgw: add range projection helpers for encrypted and compressed objects Add stateless helpers that project plaintext byte ranges to on-disk byte ranges for compressed and encrypted objects. fixup_range() delegates to these for range computation. Signed-off-by: Matthew N. Heler --- diff --git a/src/rgw/rgw_compression.cc b/src/rgw/rgw_compression.cc index 61b50d45394..6a3d3285fd4 100644 --- a/src/rgw/rgw_compression.cc +++ b/src/rgw/rgw_compression.cc @@ -2,6 +2,7 @@ // vim: ts=8 sw=2 sts=2 expandtab ft=cpp #include "rgw_compression.h" +#include "rgw_range_projection.h" #define dout_subsys ceph_subsys_rgw @@ -182,38 +183,18 @@ int RGWGetObj_Decompress::handle_data(bufferlist& bl, off_t bl_ofs, off_t bl_len int RGWGetObj_Decompress::fixup_range(off_t& ofs, off_t& end) { - if (partial_content) { - // if user set range, we need to calculate it in decompressed data - first_block = cs_info->blocks.begin(); last_block = cs_info->blocks.begin(); - if (cs_info->blocks.size() > 1) { - vector::iterator fb, lb; - // not bad to use auto for lambda, I think - auto cmp_u = [] (off_t ofs, const compression_block& e) { return (uint64_t)ofs < e.old_ofs; }; - auto cmp_l = [] (const compression_block& e, off_t ofs) { return e.old_ofs <= (uint64_t)ofs; }; - fb = upper_bound(cs_info->blocks.begin()+1, - cs_info->blocks.end(), - ofs, - cmp_u); - first_block = fb - 1; - lb = lower_bound(fb, - cs_info->blocks.end(), - end, - cmp_l); - last_block = lb - 1; - } - } else { - first_block = cs_info->blocks.begin(); last_block = cs_info->blocks.end() - 1; - } + auto result = project_compress_range(ofs, end, *cs_info, partial_content); - q_ofs = ofs - first_block->old_ofs; - q_len = end + 1 - ofs; - - ofs = first_block->new_ofs; - end = last_block->new_ofs + last_block->len - 1; - - cur_ofs = ofs; + first_block = cs_info->blocks.begin() + result.first_block_idx; + last_block = cs_info->blocks.begin() + result.last_block_idx; + q_ofs = result.q_ofs; + q_len = result.q_len; + cur_ofs = result.ofs; waiting.clear(); + ofs = result.ofs; + end = result.end; + return next->fixup_range(ofs, end); } diff --git a/src/rgw/rgw_crypt.cc b/src/rgw/rgw_crypt.cc index 4aa843f48db..ad796021a97 100644 --- a/src/rgw/rgw_crypt.cc +++ b/src/rgw/rgw_crypt.cc @@ -19,6 +19,7 @@ #include "crypto/crypto_accel.h" #include "crypto/crypto_plugin.h" #include "rgw/rgw_kms.h" +#include "rgw_range_projection.h" #include "rapidjson/document.h" #include "rapidjson/writer.h" #include "rapidjson/error/error.h" @@ -1525,56 +1526,23 @@ int RGWGetObj_BlockDecrypt::fixup_range(off_t& bl_ofs, off_t& bl_end) { } } - if (parts_len.size() > 0) { - // Multipart object: find parts containing start and end offsets - PartLocation start_loc = find_part_for_plaintext_offset(bl_ofs, false); - PartLocation end_loc = find_part_for_plaintext_offset(bl_end, true); // clamp to last + // Save the pre-encryption input offsets for filter state + off_t pre_enc_ofs = bl_ofs; + off_t pre_enc_end = bl_end; - // Block-align end within its part (in plaintext space) - size_t part_plaintext_end = encrypted_to_plaintext_size(parts_len[end_loc.part_idx]); - off_t rounded_end = std::min( - (off_t)((end_loc.offset_in_part & ~(block_size - 1)) + (block_size - 1)), - (off_t)(part_plaintext_end - 1)); + auto range = project_encrypt_range(bl_ofs, bl_end, block_size, + encrypted_block_size, encrypted_total_size, + parts_len); - // enc_begin_skip is offset within the starting block - enc_begin_skip = start_loc.offset_in_part & (block_size - 1); - ofs = bl_ofs - enc_begin_skip; - end = bl_end; + // Initialize filter state from projection result. + // ofs/end are plaintext-space offsets used by process(). + enc_begin_skip = range.enc_begin_skip; + ofs = pre_enc_ofs - enc_begin_skip; + end = pre_enc_end; + enc_ofs = range.ofs; - // Convert end offset: plaintext -> encrypted, then align to encrypted block - off_t enc_end = align_to_encrypted_block_end(logical_to_encrypted_offset(rounded_end)); - enc_end = std::min(enc_end, (off_t)(parts_len[end_loc.part_idx] - 1)); - bl_end = end_loc.cumulative_encrypted + enc_end; - - // Convert start offset: align in plaintext, then convert to encrypted - off_t aligned_start = std::max((off_t)0, start_loc.offset_in_part - enc_begin_skip); - bl_ofs = start_loc.cumulative_encrypted + logical_to_encrypted_offset(aligned_start); - - // Clamp start to end (handles invalid ranges) - bl_ofs = std::min(bl_ofs, bl_end); - enc_ofs = bl_ofs; - } - else - { - // Simple object (no multipart) - enc_begin_skip = bl_ofs & (block_size - 1); - ofs = bl_ofs & ~(block_size - 1); - end = bl_end; - - // Calculate block-aligned logical range - off_t aligned_start = bl_ofs & ~(block_size - 1); - off_t aligned_end = (bl_end & ~(block_size - 1)) + (block_size - 1); - - // Convert to encrypted offsets - bl_ofs = logical_to_encrypted_offset(aligned_start); - bl_end = align_to_encrypted_block_end(logical_to_encrypted_offset(aligned_end)); - enc_ofs = bl_ofs; - - // Clamp to actual encrypted object size - if (encrypted_total_size > 0 && bl_end >= encrypted_total_size) { - bl_end = encrypted_total_size - 1; - } - } + bl_ofs = range.ofs; + bl_end = range.end; ldpp_dout(this->dpp, 20) << "fixup_range [" << inp_ofs << "," << inp_end << "] => [" << bl_ofs << "," << bl_end << "]" @@ -1673,29 +1641,6 @@ int RGWGetObj_BlockDecrypt::process_part_boundaries(size_t& plain_part_ofs_out) return 0; } -RGWGetObj_BlockDecrypt::PartLocation -RGWGetObj_BlockDecrypt::find_part_for_plaintext_offset(off_t plaintext_ofs, bool clamp_to_last) const -{ - PartLocation loc = {0, plaintext_ofs, 0}; - - // If clamp_to_last, stop at second-to-last part (used for end offsets) - size_t limit = (clamp_to_last && parts_len.size() > 0) - ? (parts_len.size() - 1) - : parts_len.size(); - - while (loc.part_idx < limit) { - size_t part_plain = encrypted_to_plaintext_size(parts_len[loc.part_idx]); - if (loc.offset_in_part < (off_t)part_plain) { - break; // Found the part containing this offset - } - loc.offset_in_part -= part_plain; - loc.cumulative_encrypted += parts_len[loc.part_idx]; - loc.part_idx++; - } - - return loc; -} - int RGWGetObj_BlockDecrypt::handle_data(bufferlist& bl, off_t bl_ofs, off_t bl_len) { ldpp_dout(this->dpp, 25) << "Decrypt " << bl_len << " bytes" << " (encrypted_block_size=" << encrypted_block_size << ")" << dendl; diff --git a/src/rgw/rgw_crypt.h b/src/rgw/rgw_crypt.h index 9d431f5b710..386ad462ea0 100644 --- a/src/rgw/rgw_crypt.h +++ b/src/rgw/rgw_crypt.h @@ -186,6 +186,39 @@ inline uint64_t aead_plaintext_to_encrypted_offset(uint64_t plaintext_ofs) { return chunk_idx * AEAD_ENCRYPTED_CHUNK_SIZE + offset_in_chunk; } +/* + * Generic cipher geometry helpers (parameterized by block sizes). + * CBC: block_size == enc_block_size (identity). + * AEAD: enc_block_size > block_size (expansion). + */ + +inline off_t crypt_logical_to_enc_offset(off_t logical_ofs, + size_t block_size, + size_t enc_block_size) { + if (block_size == enc_block_size) return logical_ofs; + uint64_t chunk_idx = logical_ofs / block_size; + uint64_t offset_in_chunk = logical_ofs % block_size; + return chunk_idx * enc_block_size + offset_in_chunk; +} + +inline off_t crypt_align_enc_block_end(off_t enc_ofs, + size_t block_size, + size_t enc_block_size) { + if (block_size == enc_block_size) return enc_ofs; + return (enc_ofs / enc_block_size) * enc_block_size + (enc_block_size - 1); +} + +inline size_t crypt_enc_to_plaintext_size(size_t encrypted_size, + size_t block_size, + size_t enc_block_size) { + if (block_size == enc_block_size) return encrypted_size; + uint64_t full_chunks = encrypted_size / enc_block_size; + uint64_t remainder = encrypted_size % enc_block_size; + uint64_t tag_size = enc_block_size - block_size; + uint64_t partial = (remainder > tag_size) ? (remainder - tag_size) : 0; + return full_chunks * block_size + partial; +} + bool AES_256_ECB_encrypt(const DoutPrefixProvider* dpp, CephContext* cct, const uint8_t* key, @@ -246,53 +279,8 @@ class RGWGetObj_BlockDecrypt : public RGWGetObj_Filter { */ int process_part_boundaries(size_t& plain_part_ofs_out); - /** - * Result of finding which part contains a plaintext offset. - */ - struct PartLocation { - size_t part_idx; /**< Index into parts_len */ - off_t offset_in_part; /**< Plaintext offset within the part */ - off_t cumulative_encrypted; /**< Total encrypted bytes before this part */ - }; - - /** - * Find which part contains a given plaintext offset. - * @param plaintext_ofs The plaintext offset to locate - * @param clamp_to_last If true, stop at second-to-last part (for end offsets) - */ - PartLocation find_part_for_plaintext_offset(off_t plaintext_ofs, bool clamp_to_last) const; - - /** - * Align an encrypted offset up to the end of an encrypted block. - * For GCM: ensures we read complete blocks including auth tags. - */ - off_t align_to_encrypted_block_end(off_t enc_ofs) const { - if (block_size == encrypted_block_size) { - return enc_ofs; // CBC - no alignment needed - } - return (enc_ofs / encrypted_block_size) * encrypted_block_size + (encrypted_block_size - 1); - } - - /** - * Convert a logical (plaintext) offset to encrypted (storage) offset. - * For AEAD modes: accounts for auth tag overhead per chunk. - */ - off_t logical_to_encrypted_offset(off_t logical_ofs) const { - if (block_size == encrypted_block_size) { - return logical_ofs; // Non-AEAD (CBC) - no conversion needed - } - return aead_plaintext_to_encrypted_offset(logical_ofs); - } - - /** - * Convert an encrypted size to plaintext size. - * For AEAD modes: removes the auth tag overhead per chunk. - */ size_t encrypted_to_plaintext_size(size_t encrypted_size) const { - if (block_size == encrypted_block_size) { - return encrypted_size; // Non-AEAD (CBC) - no conversion needed - } - return aead_encrypted_to_plaintext_size(encrypted_size, dpp); + return crypt_enc_to_plaintext_size(encrypted_size, block_size, encrypted_block_size); } public: diff --git a/src/rgw/rgw_range_projection.h b/src/rgw/rgw_range_projection.h new file mode 100644 index 00000000000..7ddaff816c4 --- /dev/null +++ b/src/rgw/rgw_range_projection.h @@ -0,0 +1,207 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:nil -*- +// vim: ts=8 sw=2 sts=2 expandtab ft=cpp + +/* + * Ceph - scalable distributed file system + * + * Copyright (C) 2026 Red Hat, Inc. + * + * This is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License version 2.1, as published by the Free Software + * Foundation. See file COPYING. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "rgw_compression_types.h" +#include "rgw_crypt.h" + +// Stateless range projection: plaintext byte range -> on-disk byte range +struct DiskRange { + off_t ofs = 0; // disk start offset + off_t end = -1; // disk end offset (inclusive); end < ofs means empty + off_t skip = 0; // bytes to skip in first decompressed/decrypted block + uint64_t length = 0; // original plaintext request length + size_t enc_begin_skip = 0; // plaintext offset within the starting encryption block + + bool empty() const { return end < ofs; } + uint64_t disk_bytes() const { return empty() ? 0 : end - ofs + 1; } +}; + +// DiskRange plus block indices for decompression filter init +struct DecompressRange : DiskRange { + size_t first_block_idx = 0; + size_t last_block_idx = 0; + off_t q_ofs = 0; // offset within first decompressed block + uint64_t q_len = 0; // plaintext bytes requested +}; + +// Map plaintext range to compressed on-disk blocks. +inline DecompressRange project_compress_range( + off_t ofs, off_t end, + const RGWCompressionInfo& cs_info, + bool partial_content) +{ + DecompressRange r; + r.length = end + 1 - ofs; + + size_t first_idx = 0; + size_t last_idx = 0; + + if (partial_content) { + // user set a range — find the blocks that bracket it + first_idx = 0; + last_idx = 0; + if (cs_info.blocks.size() > 1) { + auto cmp_u = [](off_t o, const compression_block& e) { + return (uint64_t)o < e.old_ofs; + }; + auto cmp_l = [](const compression_block& e, off_t o) { + return e.old_ofs <= (uint64_t)o; + }; + auto fb = std::upper_bound(cs_info.blocks.begin() + 1, + cs_info.blocks.end(), + ofs, cmp_u); + first_idx = (fb - 1) - cs_info.blocks.begin(); + + auto lb = std::lower_bound(fb, + cs_info.blocks.end(), + end, cmp_l); + last_idx = (lb - 1) - cs_info.blocks.begin(); + } + } else { + // full object — use first and last blocks + first_idx = 0; + last_idx = cs_info.blocks.size() - 1; + } + + const auto& first_block = cs_info.blocks[first_idx]; + const auto& last_block = cs_info.blocks[last_idx]; + + r.first_block_idx = first_idx; + r.last_block_idx = last_idx; + r.q_ofs = ofs - first_block.old_ofs; + r.q_len = end + 1 - ofs; + r.skip = r.q_ofs; + + r.ofs = first_block.new_ofs; + r.end = last_block.new_ofs + last_block.len - 1; + + return r; +} + +// Find which multipart part contains a given plaintext offset. +inline void find_part_for_offset( + off_t plaintext_ofs, + const std::vector& parts_len, + size_t block_size, + size_t enc_block_size, + bool clamp_to_last, + size_t& part_idx, + off_t& offset_in_part, + off_t& cumulative_encrypted) +{ + part_idx = 0; + offset_in_part = plaintext_ofs; + cumulative_encrypted = 0; + + size_t limit = (clamp_to_last && parts_len.size() > 0) + ? (parts_len.size() - 1) + : parts_len.size(); + + while (part_idx < limit) { + size_t part_plain = crypt_enc_to_plaintext_size( + parts_len[part_idx], block_size, enc_block_size); + if (offset_in_part < (off_t)part_plain) { + break; + } + offset_in_part -= part_plain; + cumulative_encrypted += parts_len[part_idx]; + part_idx++; + } +} + +// Map plaintext range to encrypted on-disk bytes. +inline DiskRange project_encrypt_range( + off_t ofs, off_t end, + size_t block_size, + size_t enc_block_size, + uint64_t encrypted_total_size, + const std::vector& parts_len) +{ + DiskRange r; + r.length = end + 1 - ofs; + + if (parts_len.size() > 0) { + // multipart object + size_t start_part_idx; + off_t start_offset_in_part; + off_t start_cumulative; + find_part_for_offset(ofs, parts_len, block_size, enc_block_size, + false, start_part_idx, start_offset_in_part, + start_cumulative); + + size_t end_part_idx; + off_t end_offset_in_part; + off_t end_cumulative; + find_part_for_offset(end, parts_len, block_size, enc_block_size, + true, end_part_idx, end_offset_in_part, + end_cumulative); + + // block-align end within its part (in plaintext space) + size_t part_plaintext_end = crypt_enc_to_plaintext_size( + parts_len[end_part_idx], block_size, enc_block_size); + off_t rounded_end = std::min( + (off_t)((end_offset_in_part & ~(block_size - 1)) + (block_size - 1)), + (off_t)(part_plaintext_end - 1)); + + // enc_begin_skip is offset within the starting block + r.enc_begin_skip = start_offset_in_part & (block_size - 1); + r.skip = r.enc_begin_skip; + + // convert end offset: plaintext -> encrypted, align to encrypted block + off_t enc_end = crypt_align_enc_block_end( + crypt_logical_to_enc_offset(rounded_end, block_size, enc_block_size), + block_size, enc_block_size); + enc_end = std::min(enc_end, (off_t)(parts_len[end_part_idx] - 1)); + r.end = end_cumulative + enc_end; + + // convert start offset: align in plaintext, then to encrypted + off_t aligned_start = std::max((off_t)0, + start_offset_in_part - (off_t)r.enc_begin_skip); + r.ofs = start_cumulative + + crypt_logical_to_enc_offset(aligned_start, block_size, enc_block_size); + + // clamp start to end (handles invalid ranges) + r.ofs = std::min(r.ofs, r.end); + } else { + // simple (non-multipart) object + r.enc_begin_skip = ofs & (block_size - 1); + r.skip = r.enc_begin_skip; + + // block-align in plaintext space + off_t aligned_start = ofs & ~(block_size - 1); + off_t aligned_end = (end & ~(block_size - 1)) + (block_size - 1); + + // convert to encrypted offsets + r.ofs = crypt_logical_to_enc_offset(aligned_start, block_size, enc_block_size); + r.end = crypt_align_enc_block_end( + crypt_logical_to_enc_offset(aligned_end, block_size, enc_block_size), + block_size, enc_block_size); + + // clamp to actual encrypted object size + if (encrypted_total_size > 0 && r.end >= (off_t)encrypted_total_size) { + r.end = encrypted_total_size - 1; + } + } + + return r; +} + diff --git a/src/test/rgw/CMakeLists.txt b/src/test/rgw/CMakeLists.txt index c64e6f22068..c587b5834dc 100644 --- a/src/test/rgw/CMakeLists.txt +++ b/src/test/rgw/CMakeLists.txt @@ -230,6 +230,13 @@ target_link_libraries(unittest_rgw_crypto ${CRYPTO_LIBS} ) +add_executable(unittest_rgw_range_projection test_rgw_range_projection.cc) +add_ceph_unittest(unittest_rgw_range_projection) +target_link_libraries(unittest_rgw_range_projection + ${rgw_libs} + ${UNITTEST_LIBS} + ) + if(WITH_RADOSGW_RADOS) set(test_rgw_reshard_srcs test_rgw_reshard.cc) add_executable(unittest_rgw_reshard diff --git a/src/test/rgw/test_rgw_crypto.cc b/src/test/rgw/test_rgw_crypto.cc index 08e262e6269..f7cecb00b30 100644 --- a/src/test/rgw/test_rgw_crypto.cc +++ b/src/test/rgw/test_rgw_crypto.cc @@ -1357,217 +1357,128 @@ TEST(TestRGWCrypto, verify_AES_256_GCM_aad_offset_mismatch) // Test helper class to expose private methods for unit testing // (declared as friend in RGWGetObj_BlockDecrypt) -class TestableBlockDecrypt : public RGWGetObj_BlockDecrypt { -public: - // Re-export PartLocation struct for test code - using PartLocation = RGWGetObj_BlockDecrypt::PartLocation; - - TestableBlockDecrypt(const DoutPrefixProvider* dpp, - CephContext* cct, - RGWGetObj_Filter* next, - std::unique_ptr crypt, - std::vector parts_len) - : RGWGetObj_BlockDecrypt(dpp, cct, next, std::move(crypt), - std::move(parts_len), null_yield) {} - - // Wrapper to expose private method for testing - PartLocation find_part_for_plaintext_offset(off_t plaintext_ofs, bool clamp_to_last) const { - return RGWGetObj_BlockDecrypt::find_part_for_plaintext_offset(plaintext_ofs, clamp_to_last); - } -}; +// PartLocation tests use the free function find_part_for_offset() +// from rgw_range_projection.h instead of the removed class method. +#include "rgw_range_projection.h" TEST(TestRGWCrypto, verify_PartLocation_single_part) { - const NoDoutPrefix no_dpp(g_ceph_context, dout_subsys); - ut_get_sink get_sink; - std::vector parts = {10 * 1024 * 1024}; - auto crypt = std::make_unique(4096); - TestableBlockDecrypt decrypt(&no_dpp, g_ceph_context, &get_sink, - std::move(crypt), parts); - - auto loc = decrypt.find_part_for_plaintext_offset(0, false); - ASSERT_EQ(loc.part_idx, 0u); - ASSERT_EQ(loc.offset_in_part, 0); - ASSERT_EQ(loc.cumulative_encrypted, 0); - - loc = decrypt.find_part_for_plaintext_offset(1000, false); - ASSERT_EQ(loc.part_idx, 0u); - ASSERT_EQ(loc.offset_in_part, 1000); - ASSERT_EQ(loc.cumulative_encrypted, 0); - - loc = decrypt.find_part_for_plaintext_offset(10 * 1024 * 1024 - 1, false); - ASSERT_EQ(loc.part_idx, 0u); - ASSERT_EQ(loc.offset_in_part, 10 * 1024 * 1024 - 1); - ASSERT_EQ(loc.cumulative_encrypted, 0); + size_t idx; off_t ofs_in_part, cumulative; + + find_part_for_offset(0, parts, 4096, 4096, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 0u); + ASSERT_EQ(ofs_in_part, 0); + ASSERT_EQ(cumulative, 0); + + find_part_for_offset(1000, parts, 4096, 4096, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 0u); + ASSERT_EQ(ofs_in_part, 1000); + ASSERT_EQ(cumulative, 0); + + find_part_for_offset(10 * 1024 * 1024 - 1, parts, 4096, 4096, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 0u); + ASSERT_EQ(ofs_in_part, 10 * 1024 * 1024 - 1); + ASSERT_EQ(cumulative, 0); } TEST(TestRGWCrypto, verify_PartLocation_multipart_cbc) { - const NoDoutPrefix no_dpp(g_ceph_context, dout_subsys); - ut_get_sink get_sink; + const size_t ps = 5 * 1024 * 1024; + std::vector parts = {ps, ps, ps}; + size_t idx; off_t ofs_in_part, cumulative; + + find_part_for_offset(0, parts, 4096, 4096, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 0u); ASSERT_EQ(ofs_in_part, 0); ASSERT_EQ(cumulative, 0); - const size_t part_size = 5 * 1024 * 1024; - std::vector parts = {part_size, part_size, part_size}; - auto crypt = std::make_unique(4096); - TestableBlockDecrypt decrypt(&no_dpp, g_ceph_context, &get_sink, - std::move(crypt), parts); - - auto loc = decrypt.find_part_for_plaintext_offset(0, false); - ASSERT_EQ(loc.part_idx, 0u); - ASSERT_EQ(loc.offset_in_part, 0); - ASSERT_EQ(loc.cumulative_encrypted, 0); - - loc = decrypt.find_part_for_plaintext_offset(part_size - 1, false); - ASSERT_EQ(loc.part_idx, 0u); - ASSERT_EQ(loc.offset_in_part, (off_t)(part_size - 1)); - ASSERT_EQ(loc.cumulative_encrypted, 0); - - loc = decrypt.find_part_for_plaintext_offset(part_size, false); - ASSERT_EQ(loc.part_idx, 1u); - ASSERT_EQ(loc.offset_in_part, 0); - ASSERT_EQ(loc.cumulative_encrypted, (off_t)part_size); - - loc = decrypt.find_part_for_plaintext_offset(part_size + 1000, false); - ASSERT_EQ(loc.part_idx, 1u); - ASSERT_EQ(loc.offset_in_part, 1000); - ASSERT_EQ(loc.cumulative_encrypted, (off_t)part_size); - - loc = decrypt.find_part_for_plaintext_offset(2 * part_size, false); - ASSERT_EQ(loc.part_idx, 2u); - ASSERT_EQ(loc.offset_in_part, 0); - ASSERT_EQ(loc.cumulative_encrypted, (off_t)(2 * part_size)); - - loc = decrypt.find_part_for_plaintext_offset(3 * part_size - 100, false); - ASSERT_EQ(loc.part_idx, 2u); - ASSERT_EQ(loc.offset_in_part, (off_t)(part_size - 100)); - ASSERT_EQ(loc.cumulative_encrypted, (off_t)(2 * part_size)); + find_part_for_offset(ps - 1, parts, 4096, 4096, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 0u); ASSERT_EQ(ofs_in_part, (off_t)(ps - 1)); ASSERT_EQ(cumulative, 0); + + find_part_for_offset(ps, parts, 4096, 4096, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 1u); ASSERT_EQ(ofs_in_part, 0); ASSERT_EQ(cumulative, (off_t)ps); + + find_part_for_offset(ps + 1000, parts, 4096, 4096, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 1u); ASSERT_EQ(ofs_in_part, 1000); ASSERT_EQ(cumulative, (off_t)ps); + + find_part_for_offset(2 * ps, parts, 4096, 4096, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 2u); ASSERT_EQ(ofs_in_part, 0); ASSERT_EQ(cumulative, (off_t)(2 * ps)); + + find_part_for_offset(3 * ps - 100, parts, 4096, 4096, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 2u); ASSERT_EQ(ofs_in_part, (off_t)(ps - 100)); ASSERT_EQ(cumulative, (off_t)(2 * ps)); } TEST(TestRGWCrypto, verify_PartLocation_multipart_aead) { - const NoDoutPrefix no_dpp(g_ceph_context, dout_subsys); - ut_get_sink get_sink; + const size_t pp = 1024 * AEAD_CHUNK_SIZE; // 4MB plaintext per part + const size_t ep = aead_plaintext_to_encrypted_size(pp); + std::vector parts = {ep, ep, ep}; + size_t idx; off_t ofs_in_part, cumulative; + + find_part_for_offset(0, parts, 4096, 4112, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 0u); ASSERT_EQ(ofs_in_part, 0); ASSERT_EQ(cumulative, 0); + + find_part_for_offset(pp - 1, parts, 4096, 4112, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 0u); ASSERT_EQ(ofs_in_part, (off_t)(pp - 1)); ASSERT_EQ(cumulative, 0); + + find_part_for_offset(pp, parts, 4096, 4112, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 1u); ASSERT_EQ(ofs_in_part, 0); ASSERT_EQ(cumulative, (off_t)ep); - // For AEAD, parts_len contains ENCRYPTED sizes (with auth tag overhead) - const size_t plaintext_per_part = 1024 * AEAD_CHUNK_SIZE; // 4MB - const size_t encrypted_per_part = aead_plaintext_to_encrypted_size(plaintext_per_part); - std::vector parts = {encrypted_per_part, encrypted_per_part, encrypted_per_part}; - - auto crypt = std::make_unique(); - TestableBlockDecrypt decrypt(&no_dpp, g_ceph_context, &get_sink, - std::move(crypt), parts); - - auto loc = decrypt.find_part_for_plaintext_offset(0, false); - ASSERT_EQ(loc.part_idx, 0u); - ASSERT_EQ(loc.offset_in_part, 0); - ASSERT_EQ(loc.cumulative_encrypted, 0); - - loc = decrypt.find_part_for_plaintext_offset(plaintext_per_part - 1, false); - ASSERT_EQ(loc.part_idx, 0u); - ASSERT_EQ(loc.offset_in_part, (off_t)(plaintext_per_part - 1)); - ASSERT_EQ(loc.cumulative_encrypted, 0); - - loc = decrypt.find_part_for_plaintext_offset(plaintext_per_part, false); - ASSERT_EQ(loc.part_idx, 1u); - ASSERT_EQ(loc.offset_in_part, 0); - ASSERT_EQ(loc.cumulative_encrypted, (off_t)encrypted_per_part); - - loc = decrypt.find_part_for_plaintext_offset(plaintext_per_part + 8192, false); - ASSERT_EQ(loc.part_idx, 1u); - ASSERT_EQ(loc.offset_in_part, 8192); - ASSERT_EQ(loc.cumulative_encrypted, (off_t)encrypted_per_part); - - loc = decrypt.find_part_for_plaintext_offset(2 * plaintext_per_part, false); - ASSERT_EQ(loc.part_idx, 2u); - ASSERT_EQ(loc.offset_in_part, 0); - ASSERT_EQ(loc.cumulative_encrypted, (off_t)(2 * encrypted_per_part)); + find_part_for_offset(pp + 8192, parts, 4096, 4112, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 1u); ASSERT_EQ(ofs_in_part, 8192); ASSERT_EQ(cumulative, (off_t)ep); + + find_part_for_offset(2 * pp, parts, 4096, 4112, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 2u); ASSERT_EQ(ofs_in_part, 0); ASSERT_EQ(cumulative, (off_t)(2 * ep)); } TEST(TestRGWCrypto, verify_PartLocation_clamp_to_last) { - const NoDoutPrefix no_dpp(g_ceph_context, dout_subsys); - ut_get_sink get_sink; + const size_t ps = 5 * 1024 * 1024; + std::vector parts = {ps, ps, ps}; + size_t idx; off_t ofs_in_part, cumulative; + + find_part_for_offset(2 * ps + 1000, parts, 4096, 4096, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 2u); ASSERT_EQ(ofs_in_part, 1000); + + find_part_for_offset(2 * ps + 1000, parts, 4096, 4096, true, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 2u); ASSERT_EQ(ofs_in_part, 1000); - const size_t part_size = 5 * 1024 * 1024; - std::vector parts = {part_size, part_size, part_size}; - auto crypt = std::make_unique(4096); - TestableBlockDecrypt decrypt(&no_dpp, g_ceph_context, &get_sink, - std::move(crypt), parts); - - // Offset within object - both modes return the same - auto loc = decrypt.find_part_for_plaintext_offset(2 * part_size + 1000, false); - ASSERT_EQ(loc.part_idx, 2u); - ASSERT_EQ(loc.offset_in_part, 1000); - - loc = decrypt.find_part_for_plaintext_offset(2 * part_size + 1000, true); - ASSERT_EQ(loc.part_idx, 2u); - ASSERT_EQ(loc.offset_in_part, 1000); - - // Offset beyond all parts - without clamping returns invalid index - loc = decrypt.find_part_for_plaintext_offset(3 * part_size + 5000, false); - ASSERT_EQ(loc.part_idx, 3u); - ASSERT_EQ(loc.offset_in_part, 5000); - - // With clamping - stays at last valid part - loc = decrypt.find_part_for_plaintext_offset(3 * part_size + 5000, true); - ASSERT_EQ(loc.part_idx, 2u); - ASSERT_EQ(loc.offset_in_part, (off_t)(part_size + 5000)); + find_part_for_offset(3 * ps + 5000, parts, 4096, 4096, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 3u); ASSERT_EQ(ofs_in_part, 5000); + + find_part_for_offset(3 * ps + 5000, parts, 4096, 4096, true, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 2u); ASSERT_EQ(ofs_in_part, (off_t)(ps + 5000)); } TEST(TestRGWCrypto, verify_PartLocation_unequal_parts) { - const NoDoutPrefix no_dpp(g_ceph_context, dout_subsys); - ut_get_sink get_sink; - - // Parts: 1MB, 3MB, 2MB + // CBC parts: 1MB, 3MB, 2MB std::vector parts = {1024 * 1024, 3 * 1024 * 1024, 2 * 1024 * 1024}; - auto crypt = std::make_unique(4096); - TestableBlockDecrypt decrypt(&no_dpp, g_ceph_context, &get_sink, - std::move(crypt), parts); - - auto loc = decrypt.find_part_for_plaintext_offset(500 * 1024, false); - ASSERT_EQ(loc.part_idx, 0u); - ASSERT_EQ(loc.offset_in_part, 500 * 1024); - ASSERT_EQ(loc.cumulative_encrypted, 0); - - loc = decrypt.find_part_for_plaintext_offset(1536 * 1024, false); // 1.5MB - ASSERT_EQ(loc.part_idx, 1u); - ASSERT_EQ(loc.offset_in_part, 512 * 1024); - ASSERT_EQ(loc.cumulative_encrypted, 1024 * 1024); - - loc = decrypt.find_part_for_plaintext_offset(4608 * 1024, false); // 4.5MB - ASSERT_EQ(loc.part_idx, 2u); - ASSERT_EQ(loc.offset_in_part, 512 * 1024); - ASSERT_EQ(loc.cumulative_encrypted, 4 * 1024 * 1024); + size_t idx; off_t ofs_in_part, cumulative; + + find_part_for_offset(500 * 1024, parts, 4096, 4096, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 0u); ASSERT_EQ(ofs_in_part, 500 * 1024); ASSERT_EQ(cumulative, 0); + + find_part_for_offset(1536 * 1024, parts, 4096, 4096, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 1u); ASSERT_EQ(ofs_in_part, 512 * 1024); ASSERT_EQ(cumulative, 1024 * 1024); + + find_part_for_offset(4608 * 1024, parts, 4096, 4096, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 2u); ASSERT_EQ(ofs_in_part, 512 * 1024); ASSERT_EQ(cumulative, 4 * 1024 * 1024); } TEST(TestRGWCrypto, verify_PartLocation_no_manifest) { - // Single-part objects don't have a multipart manifest, so parts_len is empty. - // The code treats this as one logical part starting at offset 0. - const NoDoutPrefix no_dpp(g_ceph_context, dout_subsys); - ut_get_sink get_sink; - + // Empty parts = single-part object std::vector parts = {}; - auto crypt = std::make_unique(4096); - TestableBlockDecrypt decrypt(&no_dpp, g_ceph_context, &get_sink, - std::move(crypt), parts); - - auto loc = decrypt.find_part_for_plaintext_offset(0, false); - ASSERT_EQ(loc.part_idx, 0u); - ASSERT_EQ(loc.offset_in_part, 0); - ASSERT_EQ(loc.cumulative_encrypted, 0); - - loc = decrypt.find_part_for_plaintext_offset(1000, false); - ASSERT_EQ(loc.part_idx, 0u); - ASSERT_EQ(loc.offset_in_part, 1000); - ASSERT_EQ(loc.cumulative_encrypted, 0); - - loc = decrypt.find_part_for_plaintext_offset(10 * 1024 * 1024, false); - ASSERT_EQ(loc.part_idx, 0u); - ASSERT_EQ(loc.offset_in_part, 10 * 1024 * 1024); - ASSERT_EQ(loc.cumulative_encrypted, 0); + size_t idx; off_t ofs_in_part, cumulative; + + find_part_for_offset(0, parts, 4096, 4096, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 0u); ASSERT_EQ(ofs_in_part, 0); ASSERT_EQ(cumulative, 0); + + find_part_for_offset(1000, parts, 4096, 4096, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 0u); ASSERT_EQ(ofs_in_part, 1000); ASSERT_EQ(cumulative, 0); + + find_part_for_offset(10 * 1024 * 1024, parts, 4096, 4096, false, idx, ofs_in_part, cumulative); + ASSERT_EQ(idx, 0u); ASSERT_EQ(ofs_in_part, 10 * 1024 * 1024); ASSERT_EQ(cumulative, 0); } diff --git a/src/test/rgw/test_rgw_range_projection.cc b/src/test/rgw/test_rgw_range_projection.cc new file mode 100644 index 00000000000..a3215a98a3a --- /dev/null +++ b/src/test/rgw/test_rgw_range_projection.cc @@ -0,0 +1,338 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:nil -*- +// vim: ts=8 sw=2 sts=2 expandtab ft=cpp + +/* + * Ceph - scalable distributed file system + * + * Copyright (C) 2026 Red Hat, Inc. + * + * This is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License version 2.1, as published by the Free Software + * Foundation. See file COPYING. + */ + +#include +#include "rgw_range_projection.h" + +using namespace std; + +// helpers to build synthetic compression block maps + +static RGWCompressionInfo make_cs_info( + const vector>& blocks, + uint64_t orig_size, + const string& type = "zlib") +{ + /* + * Each pair is (uncompressed_size, compressed_size). + * We compute old_ofs / new_ofs cumulatively. + */ + RGWCompressionInfo cs; + cs.compression_type = type; + cs.orig_size = orig_size; + + uint64_t old_ofs = 0; + uint64_t new_ofs = 0; + for (auto& [plain_len, comp_len] : blocks) { + compression_block b; + b.old_ofs = old_ofs; + b.new_ofs = new_ofs; + b.len = comp_len; + cs.blocks.push_back(b); + old_ofs += plain_len; + new_ofs += comp_len; + } + return cs; +} + +TEST(DiskRange, EmptyWhenEndLessThanOfs) +{ + DiskRange r; + r.ofs = 10; + r.end = 5; + EXPECT_TRUE(r.empty()); + EXPECT_EQ(0u, r.disk_bytes()); +} + +TEST(DiskRange, NotEmptyWhenEndEqualsOfs) +{ + DiskRange r; + r.ofs = 10; + r.end = 10; + EXPECT_FALSE(r.empty()); + EXPECT_EQ(1u, r.disk_bytes()); +} + +TEST(DiskRange, DiskBytesCorrect) +{ + DiskRange r; + r.ofs = 100; + r.end = 199; + EXPECT_EQ(100u, r.disk_bytes()); +} + +TEST(DiskRange, DefaultIsEmpty) +{ + DiskRange r; + // default: ofs=0, end=-1 + EXPECT_TRUE(r.empty()); + EXPECT_EQ(0u, r.disk_bytes()); +} + +TEST(ProjectDecompress, FullObjectReturnsEntireCompressedRange) +{ + // 3 blocks: 4096 plain -> 2048 compressed each + auto cs = make_cs_info( + {{4096, 2048}, {4096, 2048}, {4096, 2048}}, + 12288); + + auto dr = project_compress_range(0, 12287, cs, false); + + // should span all blocks: disk [0, 6143] + EXPECT_EQ(0, dr.ofs); + EXPECT_EQ(6143, dr.end); + EXPECT_EQ(0u, dr.first_block_idx); + EXPECT_EQ(2u, dr.last_block_idx); + EXPECT_EQ(0, dr.q_ofs); + EXPECT_EQ(12288u, dr.q_len); + EXPECT_EQ(6144u, dr.disk_bytes()); +} + +TEST(ProjectDecompress, PartialRangeMapsToCorrectBlocks) +{ + // 4 blocks, each 4096 plain -> 2000 compressed + auto cs = make_cs_info( + {{4096, 2000}, {4096, 2000}, {4096, 2000}, {4096, 2000}}, + 16384); + + // request plaintext [4200, 8191] — entirely within block 1 + auto dr = project_compress_range(4200, 8191, cs, true); + + EXPECT_EQ(1u, dr.first_block_idx); + EXPECT_EQ(1u, dr.last_block_idx); + EXPECT_EQ(104, dr.q_ofs); // 4200 - 4096 + EXPECT_EQ(3992u, dr.q_len); // 8191 - 4200 + 1 + EXPECT_EQ(2000, dr.ofs); + EXPECT_EQ(3999, dr.end); +} + +TEST(ProjectDecompress, PartialRangeSpansMultipleBlocks) +{ + // 4 blocks, each 4096 plain -> 2000 compressed + auto cs = make_cs_info( + {{4096, 2000}, {4096, 2000}, {4096, 2000}, {4096, 2000}}, + 16384); + + // request plaintext [100, 8999] — spans blocks 0, 1, and 2 + auto dr = project_compress_range(100, 8999, cs, true); + + EXPECT_EQ(0u, dr.first_block_idx); + EXPECT_EQ(2u, dr.last_block_idx); + EXPECT_EQ(100, dr.q_ofs); + EXPECT_EQ(8900u, dr.q_len); + EXPECT_EQ(0, dr.ofs); + EXPECT_EQ(5999, dr.end); +} + +TEST(ProjectDecompress, SingleBlock) +{ + auto cs = make_cs_info({{8192, 4000}}, 8192); + + auto dr = project_compress_range(100, 999, cs, true); + + EXPECT_EQ(0u, dr.first_block_idx); + EXPECT_EQ(0u, dr.last_block_idx); + EXPECT_EQ(100, dr.q_ofs); + EXPECT_EQ(900u, dr.q_len); + EXPECT_EQ(0, dr.ofs); + EXPECT_EQ(3999, dr.end); + EXPECT_EQ(4000u, dr.disk_bytes()); +} + +TEST(ProjectDecryptCBC, AlignedToBlockBoundary) +{ + // [100, 999] with block_size=4096 -> aligned to [0, 4095] + auto dr = project_encrypt_range(100, 999, 4096, 4096, 8192, {}); + + EXPECT_EQ(0, dr.ofs); + EXPECT_EQ(4095, dr.end); + EXPECT_EQ(100u, dr.enc_begin_skip); + EXPECT_EQ(100u, dr.skip); + EXPECT_EQ(4096u, dr.disk_bytes()); +} + +TEST(ProjectDecryptCBC, FullObject) +{ + // full object [0, 8191] + auto dr = project_encrypt_range(0, 8191, 4096, 4096, 8192, {}); + + EXPECT_EQ(0, dr.ofs); + EXPECT_EQ(8191, dr.end); + EXPECT_EQ(0u, dr.enc_begin_skip); + EXPECT_EQ(0u, dr.skip); + EXPECT_EQ(8192u, dr.disk_bytes()); +} + +TEST(ProjectDecryptCBC, NoExpansion) +{ + // CBC: enc_block_size == block_size, so no expansion + // [4096, 8191] -> [4096, 8191] + auto dr = project_encrypt_range(4096, 8191, 4096, 4096, 16384, {}); + + EXPECT_EQ(4096, dr.ofs); + EXPECT_EQ(8191, dr.end); + EXPECT_EQ(0u, dr.enc_begin_skip); + EXPECT_EQ(4096u, dr.disk_bytes()); +} + +TEST(ProjectDecryptCBC, MultipartCrossesPartBoundary) +{ + // 2 parts, each 8192 bytes encrypted (=plaintext for CBC) + // request [4000, 12000] crosses parts + vector parts = {8192, 8192}; + auto dr = project_encrypt_range(4000, 12000, 4096, 4096, 16384, parts); + + // 4000 < 4096, so start is within first block of part 0 + EXPECT_EQ(4000u, dr.enc_begin_skip); + EXPECT_EQ(0, dr.ofs); + // end lands in part 1 at offset 3808, block-aligned to 4095, plus part 0 size + EXPECT_EQ(12287, dr.end); +} + +static const size_t GCM_BLOCK = 4096; +static const size_t GCM_ENC_BLOCK = 4112; // 4096 + 16 byte tag + +TEST(ProjectDecryptGCM, SizeExpansion) +{ + // plaintext [0, 4095] -> encrypted [0, 4111] + // 1 chunk of 4096 plain -> 4112 encrypted + auto dr = project_encrypt_range(0, 4095, GCM_BLOCK, GCM_ENC_BLOCK, 4112, {}); + + EXPECT_EQ(0, dr.ofs); + EXPECT_EQ(4111, dr.end); + EXPECT_EQ(0u, dr.enc_begin_skip); + EXPECT_EQ(4112u, dr.disk_bytes()); +} + +TEST(ProjectDecryptGCM, TailClampedToEncryptedTotalSize) +{ + // object with 2 chunks: total encrypted = 2 * 4112 = 8224 + // request plaintext [0, 8191] -> should clamp to [0, 8223] + auto dr = project_encrypt_range(0, 8191, GCM_BLOCK, GCM_ENC_BLOCK, 8224, {}); + + EXPECT_EQ(0, dr.ofs); + EXPECT_EQ(8223, dr.end); + EXPECT_EQ(8224u, dr.disk_bytes()); +} + +TEST(ProjectDecryptGCM, MidRangeAlignment) +{ + // plaintext [100, 4095] with 2 chunks (total 8224) + auto dr = project_encrypt_range(100, 4095, GCM_BLOCK, GCM_ENC_BLOCK, 8224, {}); + + EXPECT_EQ(0, dr.ofs); + EXPECT_EQ(4111, dr.end); + EXPECT_EQ(100u, dr.enc_begin_skip); + EXPECT_EQ(4112u, dr.disk_bytes()); +} + +TEST(ProjectDecryptGCM, MultipartCumulativeOffsets) +{ + // 2 parts: part0 = 4112 bytes (1 chunk), part1 = 8224 bytes (2 chunks) + // total encrypted = 12336 + // request plaintext [0, 4095]: all in part0 + vector parts = {4112, 8224}; + auto dr = project_encrypt_range(0, 4095, GCM_BLOCK, GCM_ENC_BLOCK, 12336, parts); + + // entirely within part 0 + EXPECT_EQ(0, dr.ofs); + EXPECT_EQ(4111, dr.end); + EXPECT_EQ(0u, dr.enc_begin_skip); +} + +TEST(ProjectDecryptGCM, MultipartSecondPart) +{ + // 2 parts: part0 = 4112 (1 plain chunk), part1 = 8224 (2 plain chunks) + // plaintext sizes: part0=4096, part1=8192 + // request plaintext [4096, 8191]: starts at part1 offset 0 + vector parts = {4112, 8224}; + auto dr = project_encrypt_range(4096, 8191, GCM_BLOCK, GCM_ENC_BLOCK, 12336, parts); + + // starts at beginning of part 1 (cumulative enc offset 4112) + EXPECT_EQ(4112, dr.ofs); + EXPECT_EQ(8223, dr.end); // 4112 + 4111 + EXPECT_EQ(0u, dr.enc_begin_skip); +} + +TEST(ProjectDecryptGCM, EndBeyondLastPartClamped) +{ + // 1 part of 4112 bytes (1 chunk, 4096 plain) + // request beyond: [0, 99999] + vector parts = {4112}; + auto dr = project_encrypt_range(0, 99999, GCM_BLOCK, GCM_ENC_BLOCK, 4112, parts); + + // end is clamped to the single part's encrypted size + EXPECT_EQ(0, dr.ofs); + EXPECT_EQ(4111, dr.end); +} + +TEST(ProjectDecryptGCM, InvalidRangeStartClampedToEnd) +{ + // start > total plaintext but within part index bounds + // For a simple (non-multipart) object: [50000, 50100] with total=4112 + auto dr = project_encrypt_range(50000, 50100, GCM_BLOCK, GCM_ENC_BLOCK, 4112, {}); + + // ofs expands well past encrypted_total_size, so ofs > end -> empty range + EXPECT_TRUE(dr.empty()); + EXPECT_EQ(0u, dr.disk_bytes()); +} + +TEST(CryptHelpers, LogicalToEncOffsetCBC) +{ + // CBC: identity + EXPECT_EQ(1000, crypt_logical_to_enc_offset(1000, 4096, 4096)); + EXPECT_EQ(0, crypt_logical_to_enc_offset(0, 4096, 4096)); + EXPECT_EQ(8192, crypt_logical_to_enc_offset(8192, 4096, 4096)); +} + +TEST(CryptHelpers, LogicalToEncOffsetGCM) +{ + // GCM: chunk 0 stays in [0..4095] + EXPECT_EQ(0, crypt_logical_to_enc_offset(0, 4096, 4112)); + EXPECT_EQ(4095, crypt_logical_to_enc_offset(4095, 4096, 4112)); + // chunk 1 starts at 4112 + EXPECT_EQ(4112, crypt_logical_to_enc_offset(4096, 4096, 4112)); + // chunk 2 starts at 8224 + EXPECT_EQ(8224, crypt_logical_to_enc_offset(8192, 4096, 4112)); +} + +TEST(CryptHelpers, AlignEncBlockEndCBC) +{ + // CBC: identity + EXPECT_EQ(4095, crypt_align_enc_block_end(4095, 4096, 4096)); + EXPECT_EQ(100, crypt_align_enc_block_end(100, 4096, 4096)); +} + +TEST(CryptHelpers, AlignEncBlockEndGCM) +{ + // GCM: rounds up to end of encrypted block + EXPECT_EQ(4111, crypt_align_enc_block_end(0, 4096, 4112)); + EXPECT_EQ(4111, crypt_align_enc_block_end(4095, 4096, 4112)); + EXPECT_EQ(8223, crypt_align_enc_block_end(4112, 4096, 4112)); +} + +TEST(CryptHelpers, EncToPlaintextSize) +{ + // CBC: identity + EXPECT_EQ(8192u, crypt_enc_to_plaintext_size(8192, 4096, 4096)); + // GCM: 1 chunk + EXPECT_EQ(4096u, crypt_enc_to_plaintext_size(4112, 4096, 4112)); + // GCM: 2 chunks + EXPECT_EQ(8192u, crypt_enc_to_plaintext_size(8224, 4096, 4112)); + // GCM: partial chunk (4100 - 16 tag = 4084 plaintext) + EXPECT_EQ(4084u, crypt_enc_to_plaintext_size(4100, 4096, 4112)); + // GCM: less than tag size -> 0 + EXPECT_EQ(0u, crypt_enc_to_plaintext_size(10, 4096, 4112)); +} +