Add WithRepeatedFieldsSoftMaxSize to Protobuf domains and WithSoftMaxSize to Container domains.

This new option allows users to disable the maximum size validation for repeated fields in Protobuf domains and for elements in Container domains. When enabled, the fuzzer will not fail corpus value validation even if a repeated field or container exceeds the default or configured maximum size.

PiperOrigin-RevId: 867828871
diff --git a/doc/domains-reference.md b/doc/domains-reference.md
index 1a8ab91..929f7ad 100644
--- a/doc/domains-reference.md
+++ b/doc/domains-reference.md
@@ -319,8 +319,17 @@
 
 ### Custom Container Size
 
-The size of any container domain can be customized using the `WithSize()`,
-`WithMinSize()` and `WithMaxSize()` setters.
+You can customize the size of any container domain with the following setters:
+
+*   `WithSize(S)`: sets container size to exactly `S`.
+*   `WithMinSize(S)`: sets minimum container size to `S`.
+*   `WithMaxSize(S)`: sets maximum container size to `S`.
+*   `WithSoftMaxSize(S)`: sets the maximum size to S and disables maximum size
+    validation. The domain accepts containers with more than S elements as valid
+    (e.g., when provided as seeds), but it only shrinks them during mutation.
+
+If `WithSoftMaxSize(N)` and `WithMaxSize(M)` are both used, the latter call
+overrides the former.
 
 For instance, to represent arbitrary integer vectors of size 42, we can use:
 
diff --git a/domain_tests/arbitrary_domains_protobuf_test.cc b/domain_tests/arbitrary_domains_protobuf_test.cc
index 3028ac8..a770b01 100644
--- a/domain_tests/arbitrary_domains_protobuf_test.cc
+++ b/domain_tests/arbitrary_domains_protobuf_test.cc
@@ -782,5 +782,76 @@
   std::cout << "ratio: " << multi_thread_time / single_thread_time << "\n";
 }
 
+TEST(ProtobufDomainTest, CorpusExceedingSoftMaxSizeIsValidWhileHardMaxIsNot) {
+  auto domain_with_soft_max_size =
+      Arbitrary<TestProtobuf>().WithRepeatedFieldsSoftMaxSize(100);
+  auto domain_with_hard_max_size =
+      Arbitrary<TestProtobuf>().WithRepeatedFieldsMaxSize(100);
+  TestProtobuf message;
+  for (int i = 0; i < 200; ++i) {
+    message.add_rep_i32(i);
+  }
+  auto corpus_with_soft_max_size = domain_with_soft_max_size.FromValue(message);
+  ASSERT_TRUE(corpus_with_soft_max_size.has_value());
+  EXPECT_TRUE(
+      domain_with_soft_max_size.ValidateCorpusValue(*corpus_with_soft_max_size)
+          .ok());
+
+  auto corpus_with_hard_max_size = domain_with_hard_max_size.FromValue(message);
+  ASSERT_TRUE(corpus_with_hard_max_size.has_value());
+  EXPECT_FALSE(
+      domain_with_hard_max_size.ValidateCorpusValue(*corpus_with_hard_max_size)
+          .ok());
+}
+
+TEST(ProtobufDomainTest,
+     NestedCorpusExceedingSoftMaxSizeIsValidAndMutationIsBounded) {
+  auto domain = Arbitrary<TestProtobuf>().WithRepeatedFieldsSoftMaxSize(100);
+  TestProtobuf message;
+  for (int i = 0; i < 200; ++i) {
+    message.add_rep_i32(i);
+  }
+  auto* subproto = message.add_rep_subproto();
+  for (int i = 0; i < 200; ++i) {
+    subproto->add_subproto_rep_i32(i);
+  }
+  auto corpus = domain.FromValue(message);
+  ASSERT_TRUE(corpus.has_value());
+  EXPECT_TRUE(domain.ValidateCorpusValue(*corpus).ok());
+
+  absl::BitGen bitgen;
+  for (int i = 0; i < 1000; ++i) {
+    domain.Mutate(*corpus, bitgen, {}, /*only_shrink=*/false);
+    auto mutated_message = domain.GetValue(*corpus);
+    EXPECT_LE(mutated_message.rep_i32_size(), 200);
+    if (mutated_message.rep_subproto_size() > 0) {
+      EXPECT_LE(mutated_message.rep_subproto(0).subproto_rep_i32_size(), 200);
+    }
+  }
+}
+
+TEST(ProtobufDomainTest, LastMaxSizeSettingWins) {
+  TestProtobuf message;
+  for (int i = 0; i < 200; ++i) {
+    message.add_rep_i32(i);
+  }
+
+  // Soft max size is overridden by hard max size.
+  auto domain1 = Arbitrary<TestProtobuf>()
+                     .WithRepeatedFieldsSoftMaxSize(100)
+                     .WithRepeatedFieldsMaxSize(100);
+  auto corpus1 = domain1.FromValue(message);
+  ASSERT_TRUE(corpus1.has_value());
+  EXPECT_FALSE(domain1.ValidateCorpusValue(*corpus1).ok());
+
+  // Hard max size is overridden by soft max size.
+  auto domain2 = Arbitrary<TestProtobuf>()
+                     .WithRepeatedFieldsMaxSize(100)
+                     .WithRepeatedFieldsSoftMaxSize(100);
+  auto corpus2 = domain2.FromValue(message);
+  ASSERT_TRUE(corpus2.has_value());
+  EXPECT_TRUE(domain2.ValidateCorpusValue(*corpus2).ok());
+}
+
 }  // namespace
 }  // namespace fuzztest
diff --git a/domain_tests/container_test.cc b/domain_tests/container_test.cc
index 74686c6..c599fa4 100644
--- a/domain_tests/container_test.cc
+++ b/domain_tests/container_test.cc
@@ -216,6 +216,60 @@
               IsInvalid("Invalid size: 2. Min size: 3"));
 }
 
+TEST(Container, CorpusValidationSucceedsWhenSizeExceedsSoftMaxSize) {
+  auto domain_soft = Arbitrary<std::vector<int>>().WithSoftMaxSize(2);
+  auto domain_hard = Arbitrary<std::vector<int>>().WithMaxSize(2);
+
+  std::vector<int> value = {1, 2, 3};
+  auto corpus_val_soft = domain_soft.FromValue(value);
+  ASSERT_TRUE(corpus_val_soft.has_value());
+  EXPECT_TRUE(domain_soft.ValidateCorpusValue(*corpus_val_soft).ok());
+
+  auto corpus_val_hard = domain_hard.FromValue(value);
+  ASSERT_TRUE(corpus_val_hard.has_value());
+  EXPECT_FALSE(domain_hard.ValidateCorpusValue(*corpus_val_hard).ok());
+}
+
+TEST(Container, MaxSizeAndSoftMaxSizeOverwriteEachOther) {
+  auto domain_hard =
+      Arbitrary<std::vector<int>>().WithSoftMaxSize(2).WithMaxSize(2);
+  auto domain_soft =
+      Arbitrary<std::vector<int>>().WithMaxSize(2).WithSoftMaxSize(2);
+
+  std::vector<int> value = {1, 2, 3};
+
+  auto corpus_val_hard = domain_hard.FromValue(value);
+  ASSERT_TRUE(corpus_val_hard.has_value());
+  EXPECT_FALSE(domain_hard.ValidateCorpusValue(*corpus_val_hard).ok());
+
+  auto corpus_val_soft = domain_soft.FromValue(value);
+  ASSERT_TRUE(corpus_val_soft.has_value());
+  EXPECT_TRUE(domain_soft.ValidateCorpusValue(*corpus_val_soft).ok());
+}
+
+TEST(ContainerDeathTest, MutationAbortsWhenSizeExceedsMaxSize) {
+  auto domain_hard = Arbitrary<std::vector<int>>().WithMaxSize(2);
+  std::vector<int> value = {1, 2, 3};
+  Value val(domain_hard, value);
+  absl::BitGen bitgen;
+
+  EXPECT_DEATH(
+      val.Mutate(domain_hard, bitgen, /*metadata=*/{}, /*only_shrink=*/false),
+      "Size 3 is not between 0 and 2");
+}
+
+TEST(ContainerTest, WithSoftMaxSizeMutationDoesNotGrowIfSizeExceedsLimit) {
+  auto domain_soft = Arbitrary<std::vector<int>>().WithSoftMaxSize(2);
+  std::vector<int> value = {1, 2, 3};
+  Value val(domain_soft, value);
+  absl::BitGen bitgen;
+
+  for (int i = 0; i < 100; ++i) {
+    val.Mutate(domain_soft, bitgen, /*metadata=*/{}, /*only_shrink=*/false);
+    EXPECT_LE(val.user_value.size(), 3);
+  }
+}
+
 TEST(Container, ValidationRejectsInvalidElements) {
   absl::BitGen bitgen;
 
diff --git a/fuzztest/internal/domains/container_of_impl.h b/fuzztest/internal/domains/container_of_impl.h
index d4202b8..0bb9d79 100644
--- a/fuzztest/internal/domains/container_of_impl.h
+++ b/fuzztest/internal/domains/container_of_impl.h
@@ -114,9 +114,15 @@
               const domain_implementor::MutationMetadata& metadata,
               bool only_shrink) {
     permanent_dict_candidate_ = std::nullopt;
-    FUZZTEST_CHECK(min_size() <= val.size() && val.size() <= max_size())
-        << "Size " << val.size() << " is not between " << min_size() << " and "
-        << max_size();
+    if (!validate_max_size()) {
+      FUZZTEST_CHECK(min_size() <= val.size())
+          << "Size " << val.size() << " is smaller than min size "
+          << min_size();
+    } else {
+      FUZZTEST_CHECK(min_size() <= val.size() && val.size() <= max_size())
+          << "Size " << val.size() << " is not between " << min_size()
+          << " and " << max_size();
+    }
 
     const bool can_shrink = val.size() > min_size();
     const bool can_grow = !only_shrink && val.size() < max_size();
@@ -232,6 +238,15 @@
         << "Maximal size " << s << " cannot be smaller than minimal size "
         << min_size_;
     max_size_ = s;
+    validate_max_size_ = true;
+    return Self();
+  }
+  Derived& WithSoftMaxSize(size_t s) {
+    FUZZTEST_PRECONDITION(min_size_ <= s)
+        << "Maximal size " << s << " cannot be smaller than minimal size "
+        << min_size_;
+    max_size_ = s;
+    validate_max_size_ = false;
     return Self();
   }
   Derived& WithDictionary(absl::Span<const value_type> manual_dict) {
@@ -327,7 +342,7 @@
       return absl::InvalidArgumentError(absl::StrCat(
           "Invalid size: ", corpus_value.size(), ". Min size: ", min_size()));
     }
-    if (corpus_value.size() > max_size()) {
+    if (validate_max_size() && corpus_value.size() > max_size()) {
       return absl::InvalidArgumentError(absl::StrCat(
           "Invalid size: ", corpus_value.size(), ". Max size: ", max_size()));
     }
@@ -355,6 +370,7 @@
                                                      OtherInnerDomain>& other) {
     min_size_ = other.min_size_;
     max_size_ = other.max_size_;
+    validate_max_size_ = other.validate_max_size_;
   }
 
  protected:
@@ -379,6 +395,7 @@
   size_t max_size() const {
     return max_size_.value_or(std::max(min_size_, kDefaultContainerMaxSize));
   }
+  bool validate_max_size() const { return validate_max_size_; }
 
  private:
   Derived& Self() { return static_cast<Derived&>(*this); }
@@ -395,6 +412,7 @@
   // DO NOT use directly. Use min_size() and max_size() instead.
   size_t min_size_ = 0;
   std::optional<size_t> max_size_ = std::nullopt;
+  bool validate_max_size_ = true;
 
   // Temporary memory dictionary. Collected from tracing the program
   // execution. It will always be empty if no execution_coverage_ is found,
diff --git a/fuzztest/internal/domains/protobuf_domain_impl.h b/fuzztest/internal/domains/protobuf_domain_impl.h
index 789ff79..3d5f25f 100644
--- a/fuzztest/internal/domains/protobuf_domain_impl.h
+++ b/fuzztest/internal/domains/protobuf_domain_impl.h
@@ -283,6 +283,19 @@
         {/*filter=*/std::move(filter), /*value=*/max_size});
   }
 
+  void EnableMaxSizeValidationForRepeatedFields(Filter filter) {
+    disable_max_size_validation_for_repeated_fields_.push_back(
+        {/*filter=*/std::move(filter), /*value=*/false});
+  }
+
+  void DisableMaxSizeValidationForRepeatedFields(Filter filter) {
+    disable_max_size_validation_for_repeated_fields_.push_back(
+        {/*filter=*/std::move(filter), /*value=*/true});
+  }
+  void DisableMaxSizeValidationForFields() {
+    DisableMaxSizeValidationForRepeatedFields(IncludeAll<FieldDescriptor>());
+  }
+
   OptionalPolicy GetOptionalPolicy(const FieldDescriptor* field) const {
     FUZZTEST_CHECK(!field->is_required() && !field->is_repeated())
         << "GetOptionalPolicy should apply to optional fields only!";
@@ -318,6 +331,15 @@
     return max;
   }
 
+  bool ShouldValidateMaxSize(const FieldDescriptor* repeated_field) const {
+    FUZZTEST_CHECK(repeated_field->is_repeated())
+        << "ShouldValidateMaxSize should apply to repeated "
+           "fields only!";
+    return !GetPolicyValue(disable_max_size_validation_for_repeated_fields_,
+                           repeated_field)
+                .value_or(false);
+  }
+
   std::optional<bool> IsFieldFinitelyRecursive(const FieldDescriptor* field) {
     return caches_->IsFieldFinitelyRecursive(field);
   }
@@ -490,6 +512,8 @@
   std::vector<FilterToValue<OptionalPolicy>> optional_policies_;
   std::vector<FilterToValue<int64_t>> min_repeated_fields_sizes_;
   std::vector<FilterToValue<int64_t>> max_repeated_fields_sizes_;
+  std::vector<FilterToValue<bool>>
+      disable_max_size_validation_for_repeated_fields_;
 
 #define FUZZTEST_INTERNAL_POLICY_MEMBERS(Camel, cpp)                        \
  private:                                                                   \
@@ -908,13 +932,27 @@
   }
 
   ProtobufDomainUntypedImpl&& WithRepeatedFieldsMaxSize(int64_t max_size) && {
-    policy_.SetMaxRepeatedFieldsSize(IncludeAll<FieldDescriptor>(), max_size);
-    return std::move(*this);
+    return std::move(*this).WithRepeatedFieldsMaxSize(
+        IncludeAll<FieldDescriptor>(), max_size);
   }
 
   ProtobufDomainUntypedImpl&& WithRepeatedFieldsMaxSize(
       std::function<bool(const FieldDescriptor*)> filter, int64_t max_size) && {
-    policy_.SetMaxRepeatedFieldsSize(std::move(filter), max_size);
+    policy_.SetMaxRepeatedFieldsSize(filter, max_size);
+    policy_.EnableMaxSizeValidationForRepeatedFields(std::move(filter));
+    return std::move(*this);
+  }
+
+  ProtobufDomainUntypedImpl&& WithRepeatedFieldsSoftMaxSize(
+      int64_t max_size) && {
+    return std::move(*this).WithRepeatedFieldsSoftMaxSize(
+        IncludeAll<FieldDescriptor>(), max_size);
+  }
+
+  ProtobufDomainUntypedImpl&& WithRepeatedFieldsSoftMaxSize(
+      std::function<bool(const FieldDescriptor*)> filter, int64_t max_size) && {
+    policy_.SetMaxRepeatedFieldsSize(filter, max_size);
+    policy_.DisableMaxSizeValidationForRepeatedFields(std::move(filter));
     return std::move(*this);
   }
 
@@ -1626,7 +1664,8 @@
       return ModifyDomainForRepeatedFieldRule(
           std::move(domain),
           use_policy ? policy_.GetMinRepeatedFieldSize(field) : std::nullopt,
-          use_policy ? policy_.GetMaxRepeatedFieldSize(field) : std::nullopt);
+          use_policy ? policy_.GetMaxRepeatedFieldSize(field) : std::nullopt,
+          use_policy ? policy_.ShouldValidateMaxSize(field) : true);
     } else if (IsRequired(field)) {
       return ModifyDomainForRequiredFieldRule(std::move(domain));
     } else {
@@ -1687,14 +1726,17 @@
 
   // Simple wrapper that converts a Domain<T> into a Domain<vector<T>>.
   template <typename T>
-  static auto ModifyDomainForRepeatedFieldRule(
-      const Domain<T>& d, std::optional<int64_t> min_size,
-      std::optional<int64_t> max_size) {
+  static auto ModifyDomainForRepeatedFieldRule(const Domain<T>& d,
+                                               std::optional<int64_t> min_size,
+                                               std::optional<int64_t> max_size,
+                                               bool validate_max_size) {
     auto result = ContainerOfImpl<std::vector<T>, Domain<T>>(d);
     if (min_size.has_value()) {
       result.WithMinSize(*min_size);
     }
-    if (max_size.has_value()) {
+    if (!validate_max_size && max_size.has_value()) {
+      result.WithSoftMaxSize(*max_size);
+    } else if (max_size.has_value()) {
       result.WithMaxSize(*max_size);
     }
     return result;
@@ -2148,17 +2190,31 @@
   }
 
   ProtobufDomainImpl&& WithRepeatedFieldsMaxSize(int64_t max_size) && {
-    inner_.GetPolicy().SetMaxRepeatedFieldsSize(IncludeAll<FieldDescriptor>(),
-                                                max_size);
-    return std::move(*this);
+    return std::move(*this).WithRepeatedFieldsMaxSize(
+        IncludeAll<FieldDescriptor>(), max_size);
   }
 
   ProtobufDomainImpl&& WithRepeatedFieldsMaxSize(
       std::function<bool(const FieldDescriptor*)> filter, int64_t max_size) && {
-    inner_.GetPolicy().SetMaxRepeatedFieldsSize(std::move(filter), max_size);
+    inner_.GetPolicy().SetMaxRepeatedFieldsSize(filter, max_size);
+    inner_.GetPolicy().EnableMaxSizeValidationForRepeatedFields(
+        std::move(filter));
     return std::move(*this);
   }
 
+  ProtobufDomainImpl&& WithRepeatedFieldsSoftMaxSize(
+      std::function<bool(const FieldDescriptor*)> filter, int64_t max_size) && {
+    inner_.GetPolicy().SetMaxRepeatedFieldsSize(filter, max_size);
+    inner_.GetPolicy().DisableMaxSizeValidationForRepeatedFields(
+        std::move(filter));
+    return std::move(*this);
+  }
+
+  ProtobufDomainImpl&& WithRepeatedFieldsSoftMaxSize(int64_t max_size) && {
+    return std::move(*this).WithRepeatedFieldsSoftMaxSize(
+        IncludeAll<FieldDescriptor>(), max_size);
+  }
+
   ProtobufDomainImpl&& WithFieldUnset(absl::string_view field) && {
     inner_.WithFieldNullness(field, OptionalPolicy::kAlwaysNull);
     return std::move(*this);