util: Add integer left shift helpers

The helpers are used in the following commits to increase the safety of
conversions during cache size calculations.

Co-authored-by: Ryan Ofsky <ryan@ofsky.org>
Co-authored-by: stickies-v <stickies-v@protonmail.com>
This commit is contained in:
TheCharlatan 2025-01-09 11:25:43 +01:00
parent 8bd5f8a38c
commit c03a2795a8
No known key found for this signature in database
GPG key ID: 9B79B45691DB4173
3 changed files with 155 additions and 0 deletions

View file

@ -13,6 +13,7 @@
#include <test/util/setup_common.h>
#include <uint256.h>
#include <util/bitdeque.h>
#include <util/byte_units.h>
#include <util/fs.h>
#include <util/fs_helpers.h>
#include <util/moneystr.h>
@ -1877,4 +1878,100 @@ BOOST_AUTO_TEST_CASE(clearshrink_test)
}
}
template <typename T>
void TestCheckedLeftShift()
{
constexpr auto MAX{std::numeric_limits<T>::max()};
// Basic operations
BOOST_CHECK_EQUAL(CheckedLeftShift<T>(0, 1), 0);
BOOST_CHECK_EQUAL(CheckedLeftShift<T>(0, 127), 0);
BOOST_CHECK_EQUAL(CheckedLeftShift<T>(1, 1), 2);
BOOST_CHECK_EQUAL(CheckedLeftShift<T>(2, 2), 8);
BOOST_CHECK_EQUAL(CheckedLeftShift<T>(MAX >> 1, 1), MAX - 1);
// Max left shift
BOOST_CHECK_EQUAL(CheckedLeftShift<T>(1, std::numeric_limits<T>::digits - 1), MAX / 2 + 1);
// Overflow cases
BOOST_CHECK(!CheckedLeftShift<T>((MAX >> 1) + 1, 1));
BOOST_CHECK(!CheckedLeftShift<T>(MAX, 1));
BOOST_CHECK(!CheckedLeftShift<T>(1, std::numeric_limits<T>::digits));
BOOST_CHECK(!CheckedLeftShift<T>(1, std::numeric_limits<T>::digits + 1));
if constexpr (std::is_signed_v<T>) {
constexpr auto MIN{std::numeric_limits<T>::min()};
// Negative input
BOOST_CHECK_EQUAL(CheckedLeftShift<T>(-1, 1), -2);
BOOST_CHECK_EQUAL(CheckedLeftShift<T>((MIN >> 2), 1), MIN / 2);
BOOST_CHECK_EQUAL(CheckedLeftShift<T>((MIN >> 1) + 1, 1), MIN + 2);
BOOST_CHECK_EQUAL(CheckedLeftShift<T>(MIN >> 1, 1), MIN);
// Overflow negative
BOOST_CHECK(!CheckedLeftShift<T>((MIN >> 1) - 1, 1));
BOOST_CHECK(!CheckedLeftShift<T>(MIN >> 1, 2));
BOOST_CHECK(!CheckedLeftShift<T>(-1, 100));
}
}
template <typename T>
void TestSaturatingLeftShift()
{
constexpr auto MAX{std::numeric_limits<T>::max()};
// Basic operations
BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(0, 1), 0);
BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(0, 127), 0);
BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(1, 1), 2);
BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(2, 2), 8);
BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(MAX >> 1, 1), MAX - 1);
// Max left shift
BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(1, std::numeric_limits<T>::digits - 1), MAX / 2 + 1);
// Saturation cases
BOOST_CHECK_EQUAL(SaturatingLeftShift<T>((MAX >> 1) + 1, 1), MAX);
BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(MAX, 1), MAX);
BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(1, std::numeric_limits<T>::digits), MAX);
BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(1, std::numeric_limits<T>::digits + 1), MAX);
if constexpr (std::is_signed_v<T>) {
constexpr auto MIN{std::numeric_limits<T>::min()};
// Negative input
BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(-1, 1), -2);
BOOST_CHECK_EQUAL(SaturatingLeftShift<T>((MIN >> 2), 1), MIN / 2);
BOOST_CHECK_EQUAL(SaturatingLeftShift<T>((MIN >> 1) + 1, 1), MIN + 2);
BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(MIN >> 1, 1), MIN);
// Saturation negative
BOOST_CHECK_EQUAL(SaturatingLeftShift<T>((MIN >> 1) - 1, 1), MIN);
BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(MIN >> 1, 2), MIN);
BOOST_CHECK_EQUAL(SaturatingLeftShift<T>(-1, 100), MIN);
}
}
BOOST_AUTO_TEST_CASE(checked_left_shift_test)
{
TestCheckedLeftShift<uint8_t>();
TestCheckedLeftShift<int8_t>();
TestCheckedLeftShift<size_t>();
TestCheckedLeftShift<uint64_t>();
TestCheckedLeftShift<int64_t>();
}
BOOST_AUTO_TEST_CASE(saturating_left_shift_test)
{
TestSaturatingLeftShift<uint8_t>();
TestSaturatingLeftShift<int8_t>();
TestSaturatingLeftShift<size_t>();
TestSaturatingLeftShift<uint64_t>();
TestSaturatingLeftShift<int64_t>();
}
BOOST_AUTO_TEST_CASE(mib_string_literal_test)
{
BOOST_CHECK_EQUAL(0_MiB, 0);
BOOST_CHECK_EQUAL(1_MiB, 1024 * 1024);
const auto max_mib{std::numeric_limits<size_t>::max() >> 20};
BOOST_CHECK_EXCEPTION(operator""_MiB(static_cast<unsigned long long>(max_mib) + 1), std::overflow_error, HasReason("MiB value too large for size_t byte conversion"));
}
BOOST_AUTO_TEST_SUITE_END()

22
src/util/byte_units.h Normal file
View file

@ -0,0 +1,22 @@
// Copyright (c) 2025-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef BITCOIN_UTIL_BYTE_UNITS_H
#define BITCOIN_UTIL_BYTE_UNITS_H
#include <util/overflow.h>
#include <stdexcept>
//! Overflow-safe conversion of MiB to bytes.
constexpr size_t operator"" _MiB(unsigned long long mebibytes)
{
auto bytes{CheckedLeftShift(mebibytes, 20)};
if (!bytes || *bytes > std::numeric_limits<size_t>::max()) {
throw std::overflow_error("MiB value too large for size_t byte conversion");
}
return *bytes;
}
#endif // BITCOIN_UTIL_BYTE_UNITS_H

View file

@ -5,6 +5,8 @@
#ifndef BITCOIN_UTIL_OVERFLOW_H
#define BITCOIN_UTIL_OVERFLOW_H
#include <climits>
#include <concepts>
#include <limits>
#include <optional>
#include <type_traits>
@ -47,4 +49,38 @@ template <class T>
return i + j;
}
/**
* @brief Left bit shift with overflow checking.
* @param input The input value to be left shifted.
* @param shift The number of bits to left shift.
* @return (input * 2^shift) or nullopt if it would not fit in the return type.
*/
template <std::integral T>
constexpr std::optional<T> CheckedLeftShift(T input, unsigned shift) noexcept
{
if (shift == 0 || input == 0) return input;
// Avoid undefined c++ behaviour if shift is >= number of bits in T.
if (shift >= sizeof(T) * CHAR_BIT) return std::nullopt;
// If input << shift is too big to fit in T, return nullopt.
if (input > (std::numeric_limits<T>::max() >> shift)) return std::nullopt;
if (input < (std::numeric_limits<T>::min() >> shift)) return std::nullopt;
return input << shift;
}
/**
* @brief Left bit shift with safe minimum and maximum values.
* @param input The input value to be left shifted.
* @param shift The number of bits to left shift.
* @return (input * 2^shift) clamped to fit between the lowest and highest
* representable values of the type T.
*/
template <std::integral T>
constexpr T SaturatingLeftShift(T input, unsigned shift) noexcept
{
if (auto result{CheckedLeftShift(input, shift)}) return *result;
// If input << shift is too big to fit in T, return biggest positive or negative
// number that fits.
return input < 0 ? std::numeric_limits<T>::min() : std::numeric_limits<T>::max();
}
#endif // BITCOIN_UTIL_OVERFLOW_H