EnumSet: crash various methods when given out-of-range enum values
After uploading the `FromEnumBitmaskIgnoresExtraBits` test that
exercises `ToEnumBitmask()` and `FromEnumBitmask()`, danakj@ pointed out
that silently ignoring out-of-range enum values may be incorrect.
It dawned on me how confusing that is when I saw that a test like the
following passes:
````
enum class SomeEnum {
Min,
Foo,
Max,
AfterMax,
};
using SomeEnumSet = EnumSet<SomeEnum, SomeEnum::Min, SomeEnum::Max>;
SomeEnumSet enums(SomeEnum::Foo, SomeEnum::AfterMax);
SomeEnumSet reconstructed;
for (SomeEnum val : enums)
reconstructed.Put(val);
EXPECT_EQ(enums, reconstructed);
````
The above test only passes because `SomeEnum::AfterMax` was ignored by
the constructor. Technically, `enums` and `reconstructed` are equal, but
neither contains `AfterMax`!
With this CL, the following methods crash when they receive out-of-range
enum values:
* The variadic constructor
* single_val_bitstring()
* Put()
* FromRange()
I'm intentionally leaving Remove() and Has() as no-ops when the input is
out of range. It seems reasonable that a caller may want to explicitly
check that their EnumSet does not contain Foo, even though Foo is out of
range.
Change-Id: I4c46059df206a789bfccd1bd7ab89aa8ca21a682
Bug: 1278383, 1278484
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3327788
Reviewed-by: danakj chromium <danakj@chromium.org>
Commit-Queue: Dan McArdle <dmcardle@chromium.org>
Cr-Commit-Position: refs/heads/main@{#952026}
diff --git a/base/containers/enum_set_unittest.cc b/base/containers/enum_set_unittest.cc
index f025c8a3..4ecbe7d 100644
--- a/base/containers/enum_set_unittest.cc
+++ b/base/containers/enum_set_unittest.cc
@@ -6,12 +6,16 @@
#include <stddef.h>
+#include "base/test/gtest_util.h"
+#include "testing/gtest/include/gtest/gtest-death-test.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace base {
namespace {
enum class TestEnum {
+ TEST_BELOW_MIN_NEGATIVE = -1,
+ TEST_BELOW_MIN = 0,
TEST_1 = 1,
TEST_MIN = TEST_1,
TEST_2,
@@ -19,7 +23,8 @@
TEST_4,
TEST_5,
TEST_MAX = TEST_5,
- TEST_6_OUT_OF_BOUNDS
+ TEST_6_OUT_OF_BOUNDS,
+ TEST_7_OUT_OF_BOUNDS
};
using TestEnumSet = EnumSet<TestEnum, TestEnum::TEST_MIN, TestEnum::TEST_MAX>;
@@ -35,9 +40,9 @@
TestEnumExtreme::TEST_MAX>;
class EnumSetTest : public ::testing::Test {};
+class EnumSetDeathTest : public ::testing::Test {};
TEST_F(EnumSetTest, ClassConstants) {
- TestEnumSet enums;
EXPECT_EQ(TestEnum::TEST_MIN, TestEnumSet::kMinValue);
EXPECT_EQ(TestEnum::TEST_MAX, TestEnumSet::kMaxValue);
EXPECT_EQ(static_cast<size_t>(5), TestEnumSet::kValueCount);
@@ -47,16 +52,21 @@
// evaluatable are really that way.
TEST_F(EnumSetTest, ConstexprsAreValid) {
static_assert(TestEnumSet::All().Has(TestEnum::TEST_2),
- "expected All() to be integral constant expression");
+ "Expected All() to be integral constant expression");
static_assert(TestEnumSet::FromRange(TestEnum::TEST_2, TestEnum::TEST_4)
.Has(TestEnum::TEST_2),
- "expected FromRange() to be integral constant expression");
+ "Expected FromRange() to be integral constant expression");
static_assert(TestEnumSet(TestEnum::TEST_2).Has(TestEnum::TEST_2),
- "expected TestEnumSet() to be integral constant expression");
+ "Expected TestEnumSet() to be integral constant expression");
static_assert(
TestEnumSet::FromEnumBitmask(1 << static_cast<uint64_t>(TestEnum::TEST_2))
.Has(TestEnum::TEST_2),
"Expected TestEnumSet() to be integral constant expression");
+ static_assert(
+ TestEnumSet::single_val_bitstring(TestEnum::TEST_1) == 1,
+ "Expected single_val_bitstring() to be integral constant expression");
+ static_assert(TestEnumSet::bitstring(TestEnum::TEST_1, TestEnum::TEST_2) == 3,
+ "Expected bitstring() to be integral constant expression");
}
TEST_F(EnumSetTest, DefaultConstructor) {
@@ -232,8 +242,7 @@
}
TEST_F(EnumSetTest, RangeBasedForLoop) {
- const TestEnumSet enums1(TestEnum::TEST_2, TestEnum::TEST_5,
- TestEnum::TEST_6_OUT_OF_BOUNDS);
+ const TestEnumSet enums1(TestEnum::TEST_2, TestEnum::TEST_5);
TestEnumSet enums2;
for (TestEnum e : enums1) {
enums2.Put(e);
@@ -242,8 +251,7 @@
}
TEST_F(EnumSetTest, IteratorComparisonOperators) {
- const TestEnumSet enums(TestEnum::TEST_2, TestEnum::TEST_4,
- TestEnum::TEST_6_OUT_OF_BOUNDS);
+ const TestEnumSet enums(TestEnum::TEST_2, TestEnum::TEST_4);
const auto first_it = enums.begin();
const auto second_it = ++enums.begin();
@@ -264,8 +272,7 @@
}
TEST_F(EnumSetTest, IteratorIncrementOperators) {
- const TestEnumSet enums(TestEnum::TEST_2, TestEnum::TEST_4,
- TestEnum::TEST_6_OUT_OF_BOUNDS);
+ const TestEnumSet enums(TestEnum::TEST_2, TestEnum::TEST_4);
const auto begin = enums.begin();
auto post_inc_it = begin;
@@ -348,5 +355,181 @@
EXPECT_EQ(TestEnumExtremeSet::FromEnumBitmask(val1), enums1);
}
+TEST_F(EnumSetTest, FromEnumBitmaskIgnoresExtraBits) {
+ const TestEnumSet kSets[] = {
+ TestEnumSet(),
+ TestEnumSet(TestEnum::TEST_MIN),
+ TestEnumSet(TestEnum::TEST_MAX),
+ TestEnumSet(TestEnum::TEST_MIN, TestEnum::TEST_MAX),
+ TestEnumSet(TestEnum::TEST_MIN, TestEnum::TEST_MAX),
+ TestEnumSet(TestEnum::TEST_2, TestEnum::TEST_4),
+ };
+ size_t i = 0;
+ for (const TestEnumSet& set : kSets) {
+ SCOPED_TRACE(i++);
+ const uint64_t val = set.ToEnumBitmask();
+
+ // Produce a bitstring for a single enum value. When `e` is in range
+ // relative to TestEnumSet, this function behaves identically to
+ // `single_val_bitstring`. When `e` is not in range, this function attempts
+ // to compute a value, while `single_val_bitstring` intentionally crashes.
+ auto single_val_bitstring = [](TestEnum e) -> uint64_t {
+ uint64_t shift_amount = static_cast<uint64_t>(e);
+ // Shifting left more than the number of bits in the lhs would be UB.
+ CHECK_LT(shift_amount, sizeof(uint64_t) * 8);
+ return 1ULL << shift_amount;
+ };
+
+ const uint64_t kJunkVals[] = {
+ // Add junk bits above TEST_MAX.
+ val | single_val_bitstring(TestEnum::TEST_6_OUT_OF_BOUNDS),
+ val | single_val_bitstring(TestEnum::TEST_7_OUT_OF_BOUNDS),
+ val | single_val_bitstring(TestEnum::TEST_6_OUT_OF_BOUNDS) |
+ single_val_bitstring(TestEnum::TEST_7_OUT_OF_BOUNDS),
+ // Add junk bits below TEST_MIN.
+ val | single_val_bitstring(TestEnum::TEST_BELOW_MIN),
+ };
+ for (uint64_t junk_val : kJunkVals) {
+ SCOPED_TRACE(junk_val);
+ ASSERT_NE(val, junk_val);
+
+ const TestEnumSet set_from_junk = TestEnumSet::FromEnumBitmask(junk_val);
+ EXPECT_EQ(set_from_junk, set);
+ EXPECT_EQ(set_from_junk.ToEnumBitmask(), set.ToEnumBitmask());
+
+ // Iterating both sets should produce the same sequence.
+ auto it1 = set.begin();
+ auto it2 = set_from_junk.begin();
+ while (it1 != set.end() && it2 != set_from_junk.end()) {
+ EXPECT_EQ(*it1, *it2);
+ ++it1;
+ ++it2;
+ }
+ EXPECT_TRUE(it1 == set.end());
+ EXPECT_TRUE(it2 == set_from_junk.end());
+ }
+ }
+}
+
+TEST_F(EnumSetDeathTest, SingleValBitstringCrashesOnOutOfRange) {
+ EXPECT_CHECK_DEATH(
+ TestEnumSet::single_val_bitstring(TestEnum::TEST_BELOW_MIN));
+ EXPECT_CHECK_DEATH(
+ TestEnumSet::single_val_bitstring(TestEnum::TEST_6_OUT_OF_BOUNDS));
+ EXPECT_CHECK_DEATH(
+ TestEnumSet::single_val_bitstring(TestEnum::TEST_7_OUT_OF_BOUNDS));
+}
+
+TEST_F(EnumSetDeathTest, SingleValBitstringEnumWithNegatives) {
+ enum class TestEnumNeg {
+ TEST_BELOW_MIN = -3,
+ TEST_A = -2,
+ TEST_MIN = TEST_A,
+ TEST_B = -1,
+ TEST_C = 0,
+ TEST_D = 1,
+ TEST_E = 2,
+ TEST_MAX = TEST_E,
+ TEST_F = 3,
+ };
+ // This EnumSet starts negative and ends positive.
+ using TestEnumWithNegSet =
+ EnumSet<TestEnumNeg, TestEnumNeg::TEST_MIN, TestEnumNeg::TEST_MAX>;
+
+ // Should crash because TEST_BELOW_MIN is not in range.
+ EXPECT_CHECK_DEATH(
+ TestEnumWithNegSet::single_val_bitstring(TestEnumNeg::TEST_BELOW_MIN));
+ // TEST_D is in range, but note that TEST_MIN is negative. This should work.
+ EXPECT_EQ(TestEnumWithNegSet::single_val_bitstring(TestEnumNeg::TEST_D),
+ 1u << 3);
+ // Even though TEST_A is negative, it is in range, so this should work.
+ EXPECT_EQ(TestEnumWithNegSet::single_val_bitstring(TestEnumNeg::TEST_A),
+ 1u << 0);
+}
+
+TEST_F(EnumSetDeathTest, SingleValBitstringEnumWithOnlyNegatives) {
+ enum class TestEnumNeg {
+ TEST_BELOW_MIN = -10,
+ TEST_A = -9,
+ TEST_MIN = TEST_A,
+ TEST_B = -8,
+ TEST_C = -7,
+ TEST_D = -6,
+ TEST_MAX = TEST_D,
+ TEST_F = -5,
+ };
+ // This EnumSet starts negative and ends negative.
+ using TestEnumWithNegSet =
+ EnumSet<TestEnumNeg, TestEnumNeg::TEST_MIN, TestEnumNeg::TEST_MAX>;
+
+ // Should crash because TEST_BELOW_MIN is not in range.
+ EXPECT_CHECK_DEATH(
+ TestEnumWithNegSet::single_val_bitstring(TestEnumNeg::TEST_BELOW_MIN));
+ // TEST_D is in range, but note that TEST_MIN is negative. This should work.
+ EXPECT_EQ(TestEnumWithNegSet::single_val_bitstring(TestEnumNeg::TEST_D),
+ 1u << 3);
+ // Even though TEST_A is negative, it is in range, so this should work.
+ EXPECT_EQ(TestEnumWithNegSet::single_val_bitstring(TestEnumNeg::TEST_A),
+ 1u << 0);
+}
+
+TEST_F(EnumSetDeathTest, VariadicConstructorCrashesOnOutOfRange) {
+ // Constructor should crash given out-of-range values.
+ EXPECT_CHECK_DEATH(TestEnumSet(TestEnum::TEST_BELOW_MIN).Empty());
+ EXPECT_CHECK_DEATH(TestEnumSet(TestEnum::TEST_BELOW_MIN_NEGATIVE).Empty());
+ EXPECT_CHECK_DEATH(TestEnumSet(TestEnum::TEST_6_OUT_OF_BOUNDS).Empty());
+}
+
+TEST_F(EnumSetDeathTest, FromRangeCrashesOnBadInputs) {
+ // FromRange crashes when the bounds are in range, but out of order.
+ EXPECT_CHECK_DEATH(
+ TestEnumSet().FromRange(TestEnum::TEST_3, TestEnum::TEST_1));
+
+ // FromRange crashes when the start value is out of range.
+ EXPECT_CHECK_DEATH(
+ TestEnumSet().FromRange(TestEnum::TEST_BELOW_MIN, TestEnum::TEST_1));
+ EXPECT_CHECK_DEATH(TestEnumSet().FromRange(TestEnum::TEST_BELOW_MIN_NEGATIVE,
+ TestEnum::TEST_1));
+ EXPECT_CHECK_DEATH(TestEnumSet().FromRange(TestEnum::TEST_6_OUT_OF_BOUNDS,
+ TestEnum::TEST_1));
+
+ // FromRange crashes when the end value is out of range.
+ EXPECT_CHECK_DEATH(
+ TestEnumSet().FromRange(TestEnum::TEST_3, TestEnum::TEST_BELOW_MIN));
+ EXPECT_CHECK_DEATH(TestEnumSet().FromRange(
+ TestEnum::TEST_3, TestEnum::TEST_BELOW_MIN_NEGATIVE));
+ EXPECT_CHECK_DEATH(TestEnumSet().FromRange(TestEnum::TEST_3,
+ TestEnum::TEST_6_OUT_OF_BOUNDS));
+
+ // Crashes when start and end are both out of range.
+ EXPECT_CHECK_DEATH(TestEnumSet().FromRange(TestEnum::TEST_7_OUT_OF_BOUNDS,
+ TestEnum::TEST_6_OUT_OF_BOUNDS));
+ EXPECT_CHECK_DEATH(TestEnumSet().FromRange(TestEnum::TEST_6_OUT_OF_BOUNDS,
+ TestEnum::TEST_7_OUT_OF_BOUNDS));
+}
+
+TEST_F(EnumSetDeathTest, PutCrashesOnOutOfRange) {
+ EXPECT_CHECK_DEATH(TestEnumSet().Put(TestEnum::TEST_BELOW_MIN));
+ EXPECT_CHECK_DEATH(TestEnumSet().Put(TestEnum::TEST_BELOW_MIN_NEGATIVE));
+ EXPECT_CHECK_DEATH(TestEnumSet().Put(TestEnum::TEST_6_OUT_OF_BOUNDS));
+ EXPECT_CHECK_DEATH(TestEnumSet().Put(TestEnum::TEST_7_OUT_OF_BOUNDS));
+}
+
+TEST_F(EnumSetDeathTest, PutRangeCrashesOnBadInputs) {
+ // Crashes when one input is out of range.
+ EXPECT_CHECK_DEATH(TestEnumSet().PutRange(TestEnum::TEST_BELOW_MIN_NEGATIVE,
+ TestEnum::TEST_BELOW_MIN));
+ EXPECT_CHECK_DEATH(
+ TestEnumSet().PutRange(TestEnum::TEST_3, TestEnum::TEST_7_OUT_OF_BOUNDS));
+
+ // Crashes when both inputs are out of range.
+ EXPECT_CHECK_DEATH(TestEnumSet().PutRange(TestEnum::TEST_6_OUT_OF_BOUNDS,
+ TestEnum::TEST_7_OUT_OF_BOUNDS));
+
+ // Crashes when inputs are out of order.
+ EXPECT_CHECK_DEATH(
+ TestEnumSet().PutRange(TestEnum::TEST_2, TestEnum::TEST_1));
+}
+
} // namespace
} // namespace base