// 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
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<compression_block>::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);
}
#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"
}
}
- 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 << "]"
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;
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,
*/
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:
--- /dev/null
+// -*- 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 <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <vector>
+#include <sys/types.h>
+
+#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<size_t>& 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<size_t>& 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;
+}
+
${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
// 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<BlockCrypt> crypt,
- std::vector<size_t> 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<size_t> parts = {10 * 1024 * 1024};
- auto crypt = std::make_unique<BlockCryptNone>(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<size_t> 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<size_t> parts = {part_size, part_size, part_size};
- auto crypt = std::make_unique<BlockCryptNone>(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<size_t> 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<size_t> parts = {encrypted_per_part, encrypted_per_part, encrypted_per_part};
-
- auto crypt = std::make_unique<BlockCryptNoneAEAD>();
- 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<size_t> 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<size_t> parts = {part_size, part_size, part_size};
- auto crypt = std::make_unique<BlockCryptNone>(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<size_t> parts = {1024 * 1024, 3 * 1024 * 1024, 2 * 1024 * 1024};
- auto crypt = std::make_unique<BlockCryptNone>(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<size_t> parts = {};
- auto crypt = std::make_unique<BlockCryptNone>(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);
}
--- /dev/null
+// -*- 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 <gtest/gtest.h>
+#include "rgw_range_projection.h"
+
+using namespace std;
+
+// helpers to build synthetic compression block maps
+
+static RGWCompressionInfo make_cs_info(
+ const vector<pair<uint64_t, uint64_t>>& 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<size_t> 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<size_t> 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<size_t> 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<size_t> 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));
+}
+