No public description

PiperOrigin-RevId: 668035717
diff --git a/centipede/BUILD b/centipede/BUILD
index 8ef1ee0..37d94a0 100644
--- a/centipede/BUILD
+++ b/centipede/BUILD
@@ -58,6 +58,7 @@
     srcs = ["seed_corpus_maker.cc"],
     deps = [
         ":config_init",
+        ":seed_corpus_config_proto_lib",
         ":seed_corpus_maker_flags",
         ":seed_corpus_maker_lib",
         ":util",
@@ -65,7 +66,9 @@
         "@com_google_absl//absl/flags:flag",
         "@com_google_absl//absl/log",
         "@com_google_absl//absl/log:check",
+        "@com_google_absl//absl/status",
         "@com_google_fuzztest//common:remote_file",
+        "@com_google_fuzztest//common:status_macros",
     ],
 )
 
@@ -799,7 +802,6 @@
         ":pc_info",
         ":periodic_action",
         ":runner_result",
-        ":seed_corpus_config_cc_proto",
         ":seed_corpus_maker_lib",
         ":stats",
         ":thread_pool",
@@ -1091,7 +1093,6 @@
         ":corpus_io",
         ":feature",
         ":rusage_profiler",
-        ":seed_corpus_config_cc_proto",
         ":thread_pool",
         ":util",
         ":workdir",
@@ -1108,6 +1109,25 @@
         "@com_google_fuzztest//common:logging",
         "@com_google_fuzztest//common:remote_file",
         "@com_google_fuzztest//common:status_macros",
+    ],
+)
+
+# Utilities for seed corpus config proto.
+cc_library(
+    name = "seed_corpus_config_proto_lib",
+    srcs = ["seed_corpus_config_proto_lib.cc"],
+    hdrs = ["seed_corpus_config_proto_lib.h"],
+    deps = [
+        ":seed_corpus_config_cc_proto",
+        ":seed_corpus_maker_lib",
+        ":workdir",
+        "@com_google_absl//absl/log",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/status:statusor",
+        "@com_google_absl//absl/strings",
+        "@com_google_fuzztest//common:logging",
+        "@com_google_fuzztest//common:remote_file",
+        "@com_google_fuzztest//common:status_macros",
         "@com_google_protobuf//:protobuf",
     ],
 )
@@ -1640,6 +1660,7 @@
     deps = [
         ":feature",
         ":seed_corpus_config_cc_proto",
+        ":seed_corpus_config_proto_lib",
         ":seed_corpus_maker_lib",
         ":workdir",
         "@com_google_absl//absl/log:check",
@@ -1653,6 +1674,24 @@
 )
 
 cc_test(
+    name = "seed_corpus_config_proto_lib_test",
+    srcs = ["seed_corpus_config_proto_lib_test.cc"],
+    deps = [
+        ":seed_corpus_config_cc_proto",
+        ":seed_corpus_config_proto_lib",
+        ":workdir",
+        "@com_google_absl//absl/log:check",
+        "@com_google_absl//absl/strings",
+        "@com_google_fuzztest//common:logging",
+        "@com_google_fuzztest//common:status_macros",
+        "@com_google_fuzztest//common:test_util",
+        "@com_google_fuzztest//fuzztest",
+        "@com_google_fuzztest//fuzztest:fuzztest_gtest_main",
+        "@com_google_protobuf//:protobuf",
+    ],
+)
+
+cc_test(
     name = "coverage_test",
     srcs = ["coverage_test.cc"],
     data = [
diff --git a/centipede/centipede_interface.cc b/centipede/centipede_interface.cc
index 88abafc..67435e3 100644
--- a/centipede/centipede_interface.cc
+++ b/centipede/centipede_interface.cc
@@ -55,7 +55,6 @@
 #include "./centipede/pc_info.h"
 #include "./centipede/periodic_action.h"
 #include "./centipede/runner_result.h"
-#include "./centipede/seed_corpus_config.pb.h"
 #include "./centipede/seed_corpus_maker_lib.h"
 #include "./centipede/stats.h"
 #include "./centipede/thread_pool.h"
@@ -348,21 +347,21 @@
                                      std::string_view src_dir) {
   const WorkDir workdir{env};
   SeedCorpusConfig seed_corpus_config;
-  SeedCorpusSource &src = *seed_corpus_config.mutable_sources()->Add();
-  src.set_dir_glob(src_dir);
-  src.set_num_recent_dirs(1);
+  SeedCorpusSource &src = seed_corpus_config.sources.emplace_back();
+  src.dir_glob = src_dir;
+  src.num_recent_dirs = 1;
   // We're using the previously distilled corpus files as seeds.
-  src.set_shard_rel_glob(
+  src.shard_rel_glob =
       std::filesystem::path{workdir.DistilledCorpusFiles().AllShardsGlob()}
-          .filename());
-  src.set_sampled_fraction(1.0);
-  SeedCorpusDestination &dst = *seed_corpus_config.mutable_destination();
-  dst.set_dir_path(env.workdir);
+          .filename();
+  src.sampled_fraction_or_count = 1.0f;
+  SeedCorpusDestination &dst = seed_corpus_config.destination;
+  dst.dir_path = env.workdir;
   // We're seeding the current corpus files.
-  dst.set_shard_rel_glob(
-      std::filesystem::path{workdir.CorpusFiles().AllShardsGlob()}.filename());
-  dst.set_shard_index_digits(WorkDir::kDigitsInShardIndex);
-  dst.set_num_shards(env.num_threads);
+  dst.shard_rel_glob =
+      std::filesystem::path{workdir.CorpusFiles().AllShardsGlob()}.filename();
+  dst.shard_index_digits = WorkDir::kDigitsInShardIndex;
+  dst.num_shards = env.num_threads;
   return seed_corpus_config;
 }
 
diff --git a/centipede/seed_corpus_config.proto b/centipede/seed_corpus_config.proto
index 8abc201..44e1ae7 100644
--- a/centipede/seed_corpus_config.proto
+++ b/centipede/seed_corpus_config.proto
@@ -18,7 +18,7 @@
 
 syntax = "proto3";
 
-package centipede;
+package centipede.proto;
 
 // Describes a seed corpus source as a set of directories matching a glob in
 // combination with a relative shard file glob searched under each of those
diff --git a/centipede/seed_corpus_config_proto_lib.cc b/centipede/seed_corpus_config_proto_lib.cc
new file mode 100644
index 0000000..3a7a00c
--- /dev/null
+++ b/centipede/seed_corpus_config_proto_lib.cc
@@ -0,0 +1,135 @@
+// Copyright 2024 The Centipede Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+#include "./centipede/seed_corpus_config_proto_lib.h"
+
+#include <filesystem>  // NOLINT
+#include <string>
+#include <string_view>
+#include <utility>
+#include <variant>
+
+#include "absl/log/log.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/str_cat.h"
+#include "./centipede/seed_corpus_config.pb.h"
+#include "./centipede/seed_corpus_maker_lib.h"
+#include "./centipede/workdir.h"
+#include "./common/logging.h"
+#include "./common/remote_file.h"
+#include "./common/status_macros.h"
+#include "google/protobuf/text_format.h"
+
+namespace centipede {
+
+namespace fs = std::filesystem;
+
+absl::StatusOr<proto::SeedCorpusConfig> ResolveSeedCorpusConfigProto(  //
+    std::string_view config_spec,                                      //
+    std::string_view override_out_dir) {
+  std::string config_str;
+  std::string base_dir;
+
+  if (config_spec.empty()) {
+    return absl::InvalidArgumentError(
+        "Unable to ResolveSeedCorpusConfig() with empty config_spec");
+  }
+
+  if (RemotePathExists(config_spec)) {
+    LOG(INFO) << "Config spec points at an existing file; trying to parse "
+                 "textproto config from it: "
+              << VV(config_spec);
+    RETURN_IF_NOT_OK(RemoteFileGetContents(config_spec, config_str));
+    LOG(INFO) << "Raw config read from file:\n" << config_str;
+    base_dir = std::filesystem::path{config_spec}.parent_path();
+  } else {
+    LOG(INFO) << "Config spec is not a file, or file doesn't exist; trying to "
+                 "parse textproto config verbatim: "
+              << VV(config_spec);
+    config_str = config_spec;
+    base_dir = fs::current_path();
+  }
+
+  proto::SeedCorpusConfig config;
+  if (!google::protobuf::TextFormat::ParseFromString(config_str, &config)) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Unable to parse config_str: ", config_str));
+  }
+  if (config.sources_size() > 0 != config.has_destination()) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("Non-empty config must have both source(s) and "
+                     "destination, config_spec: ",
+                     config_spec, ", config: ", config));
+  }
+  LOG(INFO) << "Parsed config:\n" << config;
+
+  // Resolve relative `source.dir_glob`s in the config to absolute ones.
+  for (auto& src : *config.mutable_sources()) {
+    auto* dir = src.mutable_dir_glob();
+    if (dir->empty() || !fs::path{*dir}.is_absolute()) {
+      *dir = fs::path{base_dir} / *dir;
+    }
+  }
+
+  // Set `destination.dir_path` to `override_out_dir`, if the latter is
+  // non-empty, or resolve a relative `destination.dir_path` to an absolute one.
+  if (config.has_destination()) {
+    auto* dir = config.mutable_destination()->mutable_dir_path();
+    if (!override_out_dir.empty()) {
+      *dir = override_out_dir;
+    } else if (dir->empty() || !fs::path{*dir}.is_absolute()) {
+      *dir = fs::path{base_dir} / *dir;
+    }
+  }
+
+  if (config.destination().shard_index_digits() == 0) {
+    config.mutable_destination()->set_shard_index_digits(
+        WorkDir::kDigitsInShardIndex);
+  }
+
+  LOG(INFO) << "Resolved config:\n" << config;
+
+  return config;
+}
+
+SeedCorpusConfig CreateSeedCorpusConfigFromProto(
+    const proto::SeedCorpusConfig& config_proto) {
+  SeedCorpusConfig config;
+  for (const auto& source_proto : config_proto.sources()) {
+    SeedCorpusSource source;
+    source.dir_glob = source_proto.dir_glob();
+    source.num_recent_dirs = source_proto.num_recent_dirs();
+    source.shard_rel_glob = source_proto.shard_rel_glob();
+    switch (source_proto.sample_size_case()) {
+      case proto::SeedCorpusSource::kSampledFraction:
+        source.sampled_fraction_or_count = source_proto.sampled_fraction();
+        break;
+      case proto::SeedCorpusSource::kSampledCount:
+        source.sampled_fraction_or_count = source_proto.sampled_count();
+        break;
+      case proto::SeedCorpusSource::SAMPLE_SIZE_NOT_SET:
+        break;
+    }
+    config.sources.push_back(std::move(source));
+  }
+  config.destination.dir_path = config_proto.destination().dir_path();
+  config.destination.shard_rel_glob =
+      config_proto.destination().shard_rel_glob();
+  config.destination.shard_index_digits =
+      config_proto.destination().shard_index_digits();
+  config.destination.num_shards = config_proto.destination().num_shards();
+  return config;
+}
+
+}  // namespace centipede
diff --git a/centipede/seed_corpus_config_proto_lib.h b/centipede/seed_corpus_config_proto_lib.h
new file mode 100644
index 0000000..3a494c7
--- /dev/null
+++ b/centipede/seed_corpus_config_proto_lib.h
@@ -0,0 +1,44 @@
+// Copyright 2024 The Centipede Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef THIRD_PARTY_CENTIPEDE_SEED_CORPUS_CONFIG_PROTO_LIB_H_
+#define THIRD_PARTY_CENTIPEDE_SEED_CORPUS_CONFIG_PROTO_LIB_H_
+
+#include <string_view>
+
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "./centipede/seed_corpus_config.pb.h"
+#include "./centipede/seed_corpus_maker_lib.h"
+
+namespace centipede {
+
+// If a file with `config_spec` path exists, tries to parse it as a
+// `SeedCorpusConfig` textproto. Otherwise, tries to parse `config_spec` as a
+// verbatim `SeedCorpusConfig` textproto. Resolves any relative paths and globs
+// in the config fields to absolute ones, using as the base dir either the
+// file's parent dir (if `config_spec` is a file) or the current dir otherwise.
+// If `override_out_dir` is non-empty, it overrides `destination.dir_path` in
+// the resolved config.
+absl::StatusOr<proto::SeedCorpusConfig> ResolveSeedCorpusConfigProto(  //
+    std::string_view config_spec,                                      //
+    std::string_view override_out_dir = "");
+
+// Creates the native `SeedCorpusConfig` from `config_proto`;
+SeedCorpusConfig CreateSeedCorpusConfigFromProto(
+    const proto::SeedCorpusConfig& config_proto);
+
+}  // namespace centipede
+
+#endif  // THIRD_PARTY_CENTIPEDE_SEED_CORPUS_CONFIG_PROTO_LIB_H_
diff --git a/centipede/seed_corpus_config_proto_lib_test.cc b/centipede/seed_corpus_config_proto_lib_test.cc
new file mode 100644
index 0000000..a6aa722
--- /dev/null
+++ b/centipede/seed_corpus_config_proto_lib_test.cc
@@ -0,0 +1,132 @@
+// Copyright 2024 The Centipede Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "./centipede/seed_corpus_config_proto_lib.h"
+
+#include <cstddef>
+#include <sstream>
+
+#include "gtest/gtest.h"
+#include "./fuzztest/fuzztest.h"
+#include "absl/log/check.h"
+#include "absl/strings/substitute.h"
+#include "./centipede/seed_corpus_config.pb.h"
+#include "./centipede/workdir.h"
+#include "./common/logging.h"  // IWYU pragma: keep
+#include "./common/status_macros.h"
+#include "./common/test_util.h"
+#include "google/protobuf/text_format.h"
+#include "google/protobuf/util/message_differencer.h"
+
+namespace centipede {
+namespace {
+
+using ::google::protobuf::TextFormat;
+using ::google::protobuf::util::DefaultFieldComparator;
+using ::google::protobuf::util::MessageDifferencer;
+
+inline constexpr auto kIdxDigits = WorkDir::kDigitsInShardIndex;
+
+proto::SeedCorpusConfig ParseSeedCorpusConfigProto(
+    std::string_view config_str) {
+  proto::SeedCorpusConfig config_proto;
+  CHECK(TextFormat::ParseFromString(config_str, &config_proto));
+  return config_proto;
+}
+
+std::string PrintSeedCorpusConfigProtoToString(
+    const proto::SeedCorpusConfig& config_proto) {
+  std::string config_str;
+  TextFormat::PrintToString(config_proto, &config_str);
+  return config_str;
+}
+
+TEST(SeedCorpusMakerLibTest, ResolveConfig) {
+  const std::string test_dir = GetTestTempDir(test_info_->name());
+
+  // `ResolveSeedCorpusConfig()` should use the CWD to resolve relative paths.
+  chdir(test_dir.c_str());
+
+  constexpr size_t kNumShards = 3;
+  constexpr std::string_view kSrcSubDir = "src/dir";
+  constexpr std::string_view kDstSubDir = "dest/dir";
+  const std::string_view kConfigStr = R"pb(
+    sources {
+      dir_glob: "./$0"
+      shard_rel_glob: "corpus.*"
+      num_recent_dirs: 1
+      sampled_fraction: 0.5
+    }
+    destination {
+      #
+      dir_path: "./$1"
+      shard_rel_glob: "corpus.*"
+      num_shards: $2
+    }
+  )pb";
+  const std::string_view kExpectedConfigStr = R"pb(
+    sources {
+      dir_glob: "$0/./$1"
+      shard_rel_glob: "corpus.*"
+      num_recent_dirs: 1
+      sampled_fraction: 0.5
+    }
+    destination {
+      dir_path: "$0/./$2"
+      shard_rel_glob: "corpus.*"
+      num_shards: $3
+      shard_index_digits: $4
+    }
+  )pb";
+
+  const proto::SeedCorpusConfig resolved_config_proto =
+      ValueOrDie(ResolveSeedCorpusConfigProto(  //
+          absl::Substitute(kConfigStr, kSrcSubDir, kDstSubDir, kNumShards)));
+
+  const proto::SeedCorpusConfig expected_config_proto =
+      ParseSeedCorpusConfigProto(  //
+          absl::Substitute(kExpectedConfigStr, test_dir, kSrcSubDir, kDstSubDir,
+                           kNumShards, kIdxDigits));
+
+  ASSERT_EQ(PrintSeedCorpusConfigProtoToString(resolved_config_proto),
+            PrintSeedCorpusConfigProtoToString(expected_config_proto));
+}
+
+void SeedCorpusConfigProtoConversionRoundTrip(
+    const proto::SeedCorpusConfig& config_proto) {
+  std::ostringstream os;
+  os << CreateSeedCorpusConfigFromProto(config_proto);
+  const std::string stringified_config = os.str();
+  const proto::SeedCorpusConfig parsed_config_proto =
+      ParseSeedCorpusConfigProto(stringified_config);
+  MessageDifferencer diff;
+  diff.set_message_field_comparison(MessageDifferencer::EQUIVALENT);
+  DefaultFieldComparator comparator;
+  comparator.set_treat_nan_as_equal(true);
+  comparator.set_float_comparison(
+      DefaultFieldComparator::FloatComparison::APPROXIMATE);
+  comparator.SetDefaultFractionAndMargin(0.0001, 0.0001);
+  diff.set_field_comparator(&comparator);
+  std::string diff_out;
+  diff.ReportDifferencesToString(&diff_out);
+  const bool is_equal = diff.Compare(config_proto, parsed_config_proto);
+  ASSERT_TRUE(is_equal) << config_proto << " is different than "
+                        << parsed_config_proto << ": " << diff_out;
+}
+
+FUZZ_TEST(SeedCorpusConfigProtoLibFuzzTest,
+          SeedCorpusConfigProtoConversionRoundTrip);
+
+}  // namespace
+}  // namespace centipede
diff --git a/centipede/seed_corpus_maker.cc b/centipede/seed_corpus_maker.cc
index 4cc6a45..fab1352 100644
--- a/centipede/seed_corpus_maker.cc
+++ b/centipede/seed_corpus_maker.cc
@@ -15,16 +15,45 @@
 #include <cstdlib>
 #include <filesystem>  // NOLINT
 #include <string>
+#include <string_view>
 
 #include "absl/base/nullability.h"
 #include "absl/flags/flag.h"
 #include "absl/log/check.h"
 #include "absl/log/log.h"
+#include "absl/status/status.h"
 #include "./centipede/config_init.h"
+#include "./centipede/seed_corpus_config_proto_lib.h"
 #include "./centipede/seed_corpus_maker_flags.h"
 #include "./centipede/seed_corpus_maker_lib.h"
 #include "./centipede/util.h"
 #include "./common/remote_file.h"
+#include "./common/status_macros.h"
+
+namespace centipede {
+namespace {
+
+absl::Status GenerateSeedCorpusFromConfigProto(  //
+    std::string_view config_spec,                //
+    std::string_view coverage_binary_name,       //
+    std::string_view coverage_binary_hash,       //
+    std::string_view override_out_dir) {
+  // Resolve the config.
+  ASSIGN_OR_RETURN_IF_NOT_OK(
+      const proto::SeedCorpusConfig config_proto,
+      ResolveSeedCorpusConfigProto(config_spec, override_out_dir));
+  if (config_proto.sources_size() == 0 || !config_proto.has_destination()) {
+    LOG(WARNING) << "Config is empty: skipping seed corpus generation";
+    return absl::OkStatus();
+  }
+  RETURN_IF_NOT_OK(GenerateSeedCorpusFromConfig(  //
+      CreateSeedCorpusConfigFromProto(config_proto), coverage_binary_name,
+      coverage_binary_hash));
+  return absl::OkStatus();
+}
+
+}  // namespace
+}  // namespace centipede
 
 int main(int argc, absl::Nonnull<char**> argv) {
   (void)centipede::config::InitRuntime(argc, argv);
@@ -46,7 +75,7 @@
               << " from actual file at --coverage_binary_path=" << binary_path;
   }
 
-  QCHECK_OK(centipede::GenerateSeedCorpusFromConfig(  //
+  QCHECK_OK(centipede::GenerateSeedCorpusFromConfigProto(  //
       config, binary_name, binary_hash, override_out_dir));
 
   return EXIT_SUCCESS;
diff --git a/centipede/seed_corpus_maker_lib.cc b/centipede/seed_corpus_maker_lib.cc
index 4ebd677..ce73a4b 100644
--- a/centipede/seed_corpus_maker_lib.cc
+++ b/centipede/seed_corpus_maker_lib.cc
@@ -12,8 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// The Centipede seed corpus maker. Following the input text proto config in the
-// ./seed_corpus_config.proto format, selects a sample of fuzzing inputs from N
+// The Centipede seed corpus maker. It selects a sample of fuzzing inputs from N
 // Centipede workdirs and writes them out to a new set of Centipede corpus file
 // shards.
 
@@ -23,16 +22,20 @@
 #include <atomic>
 #include <cmath>
 #include <cstddef>
+#include <cstdint>
 #include <cstdio>
 #include <cstdlib>
 #include <filesystem>  // NOLINT
 #include <functional>
+#include <iostream>
 #include <iterator>
 #include <memory>
 #include <numeric>
+#include <sstream>
 #include <string>
 #include <string_view>
 #include <utility>
+#include <variant>
 #include <vector>
 
 #include "absl/log/check.h"
@@ -40,6 +43,7 @@
 #include "absl/random/random.h"
 #include "absl/status/status.h"
 #include "absl/status/statusor.h"
+#include "absl/strings/escaping.h"
 #include "absl/strings/match.h"
 #include "absl/strings/str_cat.h"
 #include "absl/strings/str_format.h"
@@ -48,7 +52,6 @@
 #include "./centipede/corpus_io.h"
 #include "./centipede/feature.h"
 #include "./centipede/rusage_profiler.h"
-#include "./centipede/seed_corpus_config.pb.h"
 #include "./centipede/thread_pool.h"
 #include "./centipede/util.h"
 #include "./centipede/workdir.h"
@@ -57,7 +60,6 @@
 #include "./common/logging.h"
 #include "./common/remote_file.h"
 #include "./common/status_macros.h"
-#include "google/protobuf/text_format.h"
 
 // TODO(ussuri): Implement a smarter on-the-fly sampling to avoid having to
 //  load all of a source's elements into RAM only to pick some of them. That
@@ -81,72 +83,36 @@
 
 }  // namespace
 
-absl::StatusOr<SeedCorpusConfig> ResolveSeedCorpusConfig(  //
-    std::string_view config_spec,                          //
-    std::string_view override_out_dir) {
-  std::string config_str;
-  std::string base_dir;
-
-  if (config_spec.empty()) {
-    return absl::InvalidArgumentError(
-        "Unable to ResolveSeedCorpusConfig() with empty config_spec");
+std::ostream& operator<<(std::ostream& os, const SeedCorpusSource& source) {
+  os << "dir_glob: \"" << absl::CEscape(source.dir_glob)
+     << "\" num_recent_dirs: " << source.num_recent_dirs
+     << " shard_rel_glob: \"" << absl::CEscape(source.shard_rel_glob) << "\"";
+  if (std::holds_alternative<float>(source.sampled_fraction_or_count)) {
+    os << " sampled_fraction: "
+       << std::get<float>(source.sampled_fraction_or_count);
+  } else if (std::holds_alternative<uint32_t>(
+                 source.sampled_fraction_or_count)) {
+    os << " sampled_count: "
+       << std::get<uint32_t>(source.sampled_fraction_or_count);
   }
+  return os;
+}
 
-  if (RemotePathExists(config_spec)) {
-    LOG(INFO) << "Config spec points at an existing file; trying to parse "
-                 "textproto config from it: "
-              << VV(config_spec);
-    RETURN_IF_NOT_OK(RemoteFileGetContents(config_spec, config_str));
-    LOG(INFO) << "Raw config read from file:\n" << config_str;
-    base_dir = std::filesystem::path{config_spec}.parent_path();
-  } else {
-    LOG(INFO) << "Config spec is not a file, or file doesn't exist; trying to "
-                 "parse textproto config verbatim: "
-              << VV(config_spec);
-    config_str = config_spec;
-    base_dir = fs::current_path();
+std::ostream& operator<<(std::ostream& os,
+                         const SeedCorpusDestination& destination) {
+  os << "dir_path: \"" << absl::CEscape(destination.dir_path)
+     << "\" shard_rel_glob: \"" << absl::CEscape(destination.shard_rel_glob)
+     << "\" shard_index_digits: " << destination.shard_index_digits
+     << " num_shards: " << destination.num_shards;
+  return os;
+}
+
+std::ostream& operator<<(std::ostream& os, const SeedCorpusConfig& config) {
+  for (const auto& source : config.sources) {
+    os << "sources { " << source << " }";
   }
-
-  SeedCorpusConfig config;
-  if (!google::protobuf::TextFormat::ParseFromString(config_str, &config)) {
-    return absl::InvalidArgumentError(
-        absl::StrCat("Unable to parse config_str: ", config_str));
-  }
-  if (config.sources_size() > 0 != config.has_destination()) {
-    return absl::InvalidArgumentError(
-        absl::StrCat("Non-empty config must have both source(s) and "
-                     "destination, config_spec: ",
-                     config_spec, ", config: ", config));
-  }
-  LOG(INFO) << "Parsed config:\n" << config;
-
-  // Resolve relative `source.dir_glob`s in the config to absolute ones.
-  for (auto& src : *config.mutable_sources()) {
-    auto* dir = src.mutable_dir_glob();
-    if (dir->empty() || !fs::path{*dir}.is_absolute()) {
-      *dir = fs::path{base_dir} / *dir;
-    }
-  }
-
-  // Set `destination.dir_path` to `override_out_dir`, if the latter is
-  // non-empty, or resolve a relative `destination.dir_path` to an absolute one.
-  if (config.has_destination()) {
-    auto* dir = config.mutable_destination()->mutable_dir_path();
-    if (!override_out_dir.empty()) {
-      *dir = override_out_dir;
-    } else if (dir->empty() || !fs::path{*dir}.is_absolute()) {
-      *dir = fs::path{base_dir} / *dir;
-    }
-  }
-
-  if (config.destination().shard_index_digits() == 0) {
-    config.mutable_destination()->set_shard_index_digits(
-        WorkDir::kDigitsInShardIndex);
-  }
-
-  LOG(INFO) << "Resolved config:\n" << config;
-
-  return config;
+  os << "destination { " << config.destination << " }";
+  return os;
 }
 
 // TODO(ussuri): Refactor into smaller functions.
@@ -173,14 +139,14 @@
   // `source.num_recent_dirs()` most recent ones.
 
   std::vector<std::string> src_dirs;
-  RETURN_IF_NOT_OK(RemoteGlobMatch(source.dir_glob(), src_dirs));
+  RETURN_IF_NOT_OK(RemoteGlobMatch(source.dir_glob, src_dirs));
   LOG(INFO) << "Found " << src_dirs.size() << " corpus dir(s) matching "
-            << source.dir_glob();
+            << source.dir_glob;
   // Sort in the ascending lexicographical order. We expect that dir names
   // contain timestamps and therefore will be sorted from oldest to newest.
   std::sort(src_dirs.begin(), src_dirs.end(), std::less<std::string>());
-  if (source.num_recent_dirs() < src_dirs.size()) {
-    src_dirs.erase(src_dirs.begin(), src_dirs.end() - source.num_recent_dirs());
+  if (source.num_recent_dirs < src_dirs.size()) {
+    src_dirs.erase(src_dirs.begin(), src_dirs.end() - source.num_recent_dirs);
     LOG(INFO) << "Selected " << src_dirs.size() << " corpus dir(s)";
   }
 
@@ -188,7 +154,7 @@
 
   std::vector<std::string> corpus_shard_fnames;
   for (const auto& dir : src_dirs) {
-    const std::string shards_glob = fs::path{dir} / source.shard_rel_glob();
+    const std::string shards_glob = fs::path{dir} / source.shard_rel_glob;
     // NOTE: `RemoteGlobMatch` appends to the output list.
     const auto prev_num_shards = corpus_shard_fnames.size();
     RETURN_IF_NOT_OK(RemoteGlobMatch(shards_glob, corpus_shard_fnames));
@@ -196,10 +162,10 @@
               << " shard(s) matching " << shards_glob;
   }
   LOG(INFO) << "Found " << corpus_shard_fnames.size()
-            << " shard(s) total in source " << source.dir_glob();
+            << " shard(s) total in source " << source.dir_glob;
 
   if (corpus_shard_fnames.empty()) {
-    LOG(WARNING) << "Skipping empty source " << source.dir_glob();
+    LOG(WARNING) << "Skipping empty source " << source.dir_glob;
     return absl::OkStatus();
   }
 
@@ -288,27 +254,25 @@
 
   LOG(INFO) << "Read total of " << src_elts.size() << " elements ("
             << src_num_features << " with features) from source "
-            << source.dir_glob();
+            << source.dir_glob;
 
   // Extract a sample of the elements of the size specified in
-  // `source.sample_size()`.
+  // `source.sample_size`.
 
   size_t sample_size = 0;
-  switch (source.sample_size_case()) {
-    case SeedCorpusSource::kSampledFraction:
-      if (source.sampled_fraction() <= 0.0 || source.sampled_fraction() > 1) {
-        return absl::InvalidArgumentError(
-            absl::StrCat("sampled_fraction must be in (0, 1], got ",
-                         source.sampled_fraction()));
-      }
-      sample_size = std::llrint(src_elts.size() * source.sampled_fraction());
-      break;
-    case SeedCorpusSource::kSampledCount:
-      sample_size = std::min<size_t>(src_elts.size(), source.sampled_count());
-      break;
-    case SeedCorpusSource::SAMPLE_SIZE_NOT_SET:
-      sample_size = src_elts.size();
-      break;
+  if (std::holds_alternative<float>(source.sampled_fraction_or_count)) {
+    const auto& fraction = std::get<float>(source.sampled_fraction_or_count);
+    if (fraction <= 0.0 || fraction > 1) {
+      return absl::InvalidArgumentError(
+          absl::StrCat("sampled_fraction must be in (0, 1], got ", fraction));
+    }
+    sample_size = std::llrint(src_elts.size() * fraction);
+  } else if (std::holds_alternative<uint32_t>(
+                 source.sampled_fraction_or_count)) {
+    const auto count = std::get<uint32_t>(source.sampled_fraction_or_count);
+    sample_size = std::min<size_t>(src_elts.size(), count);
+  } else {
+    sample_size = src_elts.size();
   }
 
   if (sample_size < src_elts.size()) {
@@ -357,7 +321,7 @@
         "Collected seed corpus turned out to be empty: verify config / "
         "sources");
   }
-  if (destination.dir_path().empty()) {
+  if (destination.dir_path.empty()) {
     return absl::InvalidArgumentError(
         "Unable to write seed corpus to empty destination path");
   }
@@ -371,21 +335,21 @@
             << " seed corpus elements to destination:\n"
             << destination;
 
-  if (destination.num_shards() <= 0) {
+  if (destination.num_shards <= 0) {
     return absl::InvalidArgumentError(
         "Requested number of destination shards must be > 0");
   }
-  if (!absl::StrContains(destination.shard_rel_glob(), "*")) {
+  if (!absl::StrContains(destination.shard_rel_glob, "*")) {
     return absl::InvalidArgumentError(
         absl::StrCat("Destination shard pattern must contain '*', got ",
-                     destination.shard_rel_glob()));
+                     destination.shard_rel_glob));
   }
 
   // Compute shard sizes. If the elements can't be evenly divided between the
   // requested number of shards, distribute the N excess elements between the
   // first N shards.
   const size_t num_shards =
-      std::min<size_t>(destination.num_shards(), elements.size());
+      std::min<size_t>(destination.num_shards, elements.size());
   CHECK_GT(num_shards, 0);
   const size_t shard_size = elements.size() / num_shards;
   std::vector<size_t> shard_sizes(num_shards, shard_size);
@@ -419,11 +383,11 @@
         // them, and possibly retire
         // `SeedCorpusDestination::shard_index_digits`).
         const std::string shard_idx =
-            absl::StrFormat("%0*d", destination.shard_index_digits(), shard);
-        const std::string corpus_rel_fname = absl::StrReplaceAll(
-            destination.shard_rel_glob(), {{"*", shard_idx}});
+            absl::StrFormat("%0*d", destination.shard_index_digits, shard);
+        const std::string corpus_rel_fname =
+            absl::StrReplaceAll(destination.shard_rel_glob, {{"*", shard_idx}});
         const std::string corpus_fname =
-            fs::path{destination.dir_path()} / corpus_rel_fname;
+            fs::path{destination.dir_path} / corpus_rel_fname;
 
         const auto work_dir = WorkDir::FromCorpusShardPath(  //
             corpus_fname, coverage_binary_name, coverage_binary_hash);
@@ -450,9 +414,9 @@
                 << ShardPathsForLogging(corpus_fname, features_fname);
 
         // Features files are always saved in a subdir of the workdir
-        // (== `destination.dir_path()` here), which might not exist yet, so we
+        // (== `destination.dir_path` here), which might not exist yet, so we
         // create it. Corpus files are saved in the workdir directly, but we
-        // also create it in case `destination.shard_rel_glob()` contains some
+        // also create it in case `destination.shard_rel_glob` contains some
         // dirs (not really intended for that, but the end-user may do that).
         for (const auto& fname : {corpus_fname, features_fname}) {
           if (!fname.empty()) {
@@ -516,59 +480,42 @@
   LOG(INFO) << "Wrote total of " << elements.size() << " elements ("
             << dst_elts_with_features
             << " with precomputed features) to destination "
-            << destination.dir_path();
-  return absl::OkStatus();
-}
-
-absl::Status GenerateSeedCorpusFromConfig(  //
-    std::string_view config_spec,           //
-    std::string_view coverage_binary_name,  //
-    std::string_view coverage_binary_hash,  //
-    std::string_view override_out_dir) {
-  // Resolve the config.
-  ASSIGN_OR_RETURN_IF_NOT_OK(
-      const SeedCorpusConfig config,
-      ResolveSeedCorpusConfig(config_spec, override_out_dir));
-  if (config.sources_size() == 0 || !config.has_destination()) {
-    LOG(WARNING) << "Config is empty: skipping seed corpus generation";
-    return absl::OkStatus();
-  }
-  RETURN_IF_NOT_OK(GenerateSeedCorpusFromConfig(  //
-      config, coverage_binary_name, coverage_binary_hash, override_out_dir));
+            << destination.dir_path;
   return absl::OkStatus();
 }
 
 absl::Status GenerateSeedCorpusFromConfig(  //
     const SeedCorpusConfig& config,         //
     std::string_view coverage_binary_name,  //
-    std::string_view coverage_binary_hash,  //
-    std::string_view override_out_dir) {
+    std::string_view coverage_binary_hash) {
   // Pre-create the destination dir early to catch possible misspellings etc.
-  if (!RemotePathExists(config.destination().dir_path())) {
-    RETURN_IF_NOT_OK(RemoteMkdir(config.destination().dir_path()));
+  if (!RemotePathExists(config.destination.dir_path)) {
+    RETURN_IF_NOT_OK(RemoteMkdir(config.destination.dir_path));
   }
 
   // Dump the config to the debug info dir in the destination.
   const WorkDir workdir{
-      config.destination().dir_path(),
+      config.destination.dir_path,
       coverage_binary_name,
       coverage_binary_hash,
       /*my_shard_index=*/0,
   };
   const std::filesystem::path debug_info_dir = workdir.DebugInfoDirPath();
   RETURN_IF_NOT_OK(RemoteMkdir(debug_info_dir.c_str()));
+  std::ostringstream os;
+  os << config;
   RETURN_IF_NOT_OK(RemoteFileSetContents(
-      (debug_info_dir / "seeding.cfg").c_str(), absl::StrCat(config)));
+      (debug_info_dir / "seeding.cfg").c_str(), os.str()));
 
   InputAndFeaturesVec elements;
 
   // Read and sample elements from the sources.
-  for (const auto& source : config.sources()) {
+  for (const auto& source : config.sources) {
     RETURN_IF_NOT_OK(SampleSeedCorpusElementsFromSource(  //
         source, coverage_binary_name, coverage_binary_hash, elements));
   }
   LOG(INFO) << "Sampled " << elements.size() << " elements from "
-            << config.sources_size() << " seed corpus source(s)";
+            << config.sources.size() << " seed corpus source(s)";
 
   // Write the sampled elements to the destination.
   if (elements.empty()) {
@@ -577,7 +524,7 @@
   } else {
     RETURN_IF_NOT_OK(WriteSeedCorpusElementsToDestination(  //
         elements, coverage_binary_name, coverage_binary_hash,
-        config.destination()));
+        config.destination));
     LOG(INFO) << "Wrote " << elements.size()
               << " elements to seed corpus destination";
   }
diff --git a/centipede/seed_corpus_maker_lib.h b/centipede/seed_corpus_maker_lib.h
index fe1736d..8299818 100644
--- a/centipede/seed_corpus_maker_lib.h
+++ b/centipede/seed_corpus_maker_lib.h
@@ -15,32 +15,62 @@
 #ifndef THIRD_PARTY_CENTIPEDE_SEED_CORPUS_MAKER_LIB_H_
 #define THIRD_PARTY_CENTIPEDE_SEED_CORPUS_MAKER_LIB_H_
 
+#include <iostream>
 #include <string_view>
 #include <utility>
+#include <variant>
 #include <vector>
 
 #include "absl/status/status.h"
 #include "absl/status/statusor.h"
 #include "./centipede/feature.h"
-#include "./centipede/seed_corpus_config.pb.h"
 #include "./common/defs.h"
 
 namespace centipede {
 
+// Native struct used by the seed corpus library for seed corpus source.
+//
+// Currently this is mirroring the `proto::SeedCorpusSource` proto. But in the
+// future it may change with the core seeding API.
+struct SeedCorpusSource {
+  std::string dir_glob;
+  uint32_t num_recent_dirs;
+  std::string shard_rel_glob;
+  std::variant<float, uint32_t> sampled_fraction_or_count;
+
+  friend std::ostream& operator<<(std::ostream& os,
+                                  const SeedCorpusSource& source);
+};
+
+// Native struct used by the seed corpus library for seed corpus destination.
+//
+// Currently this is mirroring the `proto::SeedCorpusDestination` proto. But in
+// the future it may change with the core seeding API.
+struct SeedCorpusDestination {
+  std::string dir_path;
+  std::string shard_rel_glob;
+  uint32_t shard_index_digits;
+  uint32_t num_shards;
+
+  friend std::ostream& operator<<(std::ostream& os,
+                                  const SeedCorpusDestination& destination);
+};
+
+// Native struct used by the seed corpus library for seed corpus configuration.
+//
+// Currently this is mirroring the `proto::SeedCorpusConfig` proto. But in the
+// future it may change with the core seeding API.
+struct SeedCorpusConfig {
+  std::vector<SeedCorpusSource> sources;
+  SeedCorpusDestination destination;
+
+  friend std::ostream& operator<<(std::ostream& os,
+                                  const SeedCorpusConfig& config);
+};
+
 using InputAndFeatures = std::pair<ByteArray, FeatureVec>;
 using InputAndFeaturesVec = std::vector<InputAndFeatures>;
 
-// If a file with `config_spec` path exists, tries to parse it as a
-// `SeedCorpusConfig` textproto. Otherwise, tries to parse `config_spec` as a
-// verbatim `SeedCorpusConfig` textproto. Resolves any relative paths and globs
-// in the config fields to absolute ones, using as the base dir either the
-// file's parent dir (if `config_spec` is a file) or the current dir otherwise.
-// If `override_out_dir` is non-empty, it overrides `destination.dir_path` in
-// the resolved config.
-absl::StatusOr<SeedCorpusConfig> ResolveSeedCorpusConfig(  //
-    std::string_view config_spec,                          //
-    std::string_view override_out_dir = "");
-
 // Extracts a sample of corpus elements from `source` and appends the results to
 // `elements`. `source` defines the locations of the corpus shards and the size
 // of the sample.
@@ -74,14 +104,9 @@
     const SeedCorpusDestination& destination);
 
 // Reads and samples seed corpus elements from all the sources and writes the
-// results to the destination, as defined in `config_spec`. `config_spec` can be
-// either a `silifuzz.ccmp.SeedCorpusConfig` textproto file (local or remote) or
-// a verbatim `silifuzz.ccmp.SeedCorpusConfig` string. The paths and globs in
-// the proto can be relative paths: in that case, they are resolved to absolute
-// using either the file's parent dir (if `config_spec` is a file) or the
-// current dir (if `config_spec` is a verbatim string) as the base dir. If
-// `override_out_dir` is non-empty, it overrides `destination.dir_path`
-// specified in `config_spec`.
+// results to the destination, as defined in `config`. The paths and globs in
+// `config` can be relative paths: in that case, they are resolved to absolute
+// using as the base dir.
 //
 // `coverage_binary_name` should be the basename of the coverage binary for
 // which the seed corpus is to be created, and the `coverage_binary_hash` should
@@ -90,17 +115,9 @@
 // <coverage_binary_name>-<coverage_binary_hash> subdir of the source to the
 // same subdir of the destination.
 absl::Status GenerateSeedCorpusFromConfig(  //
-    std::string_view config_spec,           //
-    std::string_view coverage_binary_name,  //
-    std::string_view coverage_binary_hash,  //
-    std::string_view override_out_dir = "");
-
-// Same as above but accepts a `SeedCorpusConfig` directly.
-absl::Status GenerateSeedCorpusFromConfig(  //
     const SeedCorpusConfig& config,         //
     std::string_view coverage_binary_name,  //
-    std::string_view coverage_binary_hash,  //
-    std::string_view override_out_dir = "");
+    std::string_view coverage_binary_hash);
 
 }  // namespace centipede
 
diff --git a/centipede/seed_corpus_maker_lib_test.cc b/centipede/seed_corpus_maker_lib_test.cc
index 22a7aa2..8fa3660 100644
--- a/centipede/seed_corpus_maker_lib_test.cc
+++ b/centipede/seed_corpus_maker_lib_test.cc
@@ -28,6 +28,7 @@
 #include "absl/strings/substitute.h"
 #include "./centipede/feature.h"
 #include "./centipede/seed_corpus_config.pb.h"
+#include "./centipede/seed_corpus_config_proto_lib.h"
 #include "./centipede/workdir.h"
 #include "./common/logging.h"  // IWYU pragma: keep
 #include "./common/status_macros.h"
@@ -38,23 +39,18 @@
 namespace {
 
 namespace fs = std::filesystem;
-using google::protobuf::TextFormat;
-using testing::IsSubsetOf;
+
+using ::google::protobuf::TextFormat;
+using ::testing::IsSubsetOf;
 
 inline constexpr auto kIdxDigits = WorkDir::kDigitsInShardIndex;
 
 enum ShardType { kNormal, kDistilled };
 
-SeedCorpusConfig ParseSeedCorpusConfig(std::string_view config_str) {
-  SeedCorpusConfig config;
-  CHECK(TextFormat::ParseFromString(config_str, &config));
-  return config;
-}
-
-std::string PrintSeedCorpusConfigToString(const SeedCorpusConfig& config) {
-  std::string config_str;
-  TextFormat::PrintToString(config, &config_str);
-  return config_str;
+SeedCorpusConfig CreateTestSeedCorpusConfig(std::string_view config_str) {
+  proto::SeedCorpusConfig config_proto;
+  CHECK(TextFormat::ParseFromString(config_str, &config_proto));
+  return CreateSeedCorpusConfigFromProto(config_proto);
 }
 
 void VerifyShardsExist(            //
@@ -103,59 +99,8 @@
       << VV(workdir);
 }
 
-TEST(SeedCorpusMakerLibTest, ResolveConfig) {
-  const std::string test_dir = GetTestTempDir(test_info_->name());
-
-  // `ResolveSeedCorpusConfig()` should use the CWD to resolve relative paths.
-  chdir(test_dir.c_str());
-
-  constexpr size_t kNumShards = 3;
-  constexpr std::string_view kSrcSubDir = "src/dir";
-  constexpr std::string_view kDstSubDir = "dest/dir";
-  const std::string_view kConfigStr = R"pb(
-    sources {
-      dir_glob: "./$0"
-      shard_rel_glob: "corpus.*"
-      num_recent_dirs: 1
-      sampled_fraction: 0.5
-    }
-    destination {
-      #
-      dir_path: "./$1"
-      shard_rel_glob: "corpus.*"
-      num_shards: $2
-    }
-  )pb";
-  const std::string_view kExpectedConfigStr = R"pb(
-    sources {
-      dir_glob: "$0/./$1"
-      shard_rel_glob: "corpus.*"
-      num_recent_dirs: 1
-      sampled_fraction: 0.5
-    }
-    destination {
-      dir_path: "$0/./$2"
-      shard_rel_glob: "corpus.*"
-      num_shards: $3
-      shard_index_digits: $4
-    }
-  )pb";
-
-  const SeedCorpusConfig resolved_config =
-      ValueOrDie(ResolveSeedCorpusConfig(  //
-          absl::Substitute(kConfigStr, kSrcSubDir, kDstSubDir, kNumShards)));
-
-  const SeedCorpusConfig expected_config = ParseSeedCorpusConfig(  //
-      absl::Substitute(kExpectedConfigStr, test_dir, kSrcSubDir, kDstSubDir,
-                       kNumShards, kIdxDigits));
-
-  ASSERT_EQ(PrintSeedCorpusConfigToString(resolved_config),
-            PrintSeedCorpusConfigToString(expected_config));
-}
-
 TEST(SeedCorpusMakerLibTest, RoundTripWriteReadWrite) {
   const fs::path test_dir = GetTestTempDir(test_info_->name());
-  // `ResolveSeedCorpusConfig()` should use the CWD to resolve relative paths.
   chdir(test_dir.c_str());
 
   const InputAndFeaturesVec kElements = {
@@ -171,7 +116,6 @@
   constexpr std::string_view kCovHash = "hash";
   constexpr std::string_view kRelDir1 = "dir/foo";
   constexpr std::string_view kRelDir2 = "dir/bar";
-  constexpr std::string_view kRelDir3 = "dir/kuq";
 
   // Test `WriteSeedCorpusElementsToDestination()`. This also creates a seed
   // source for the subsequent tests.
@@ -185,10 +129,10 @@
         shard_index_digits: $3
       }
     )pb";
-    const SeedCorpusConfig config = ParseSeedCorpusConfig(absl::Substitute(
+    const SeedCorpusConfig config = CreateTestSeedCorpusConfig(absl::Substitute(
         kConfigStr, kRelDir1, kCovBin, kNumShards, kIdxDigits));
     ASSERT_OK(WriteSeedCorpusElementsToDestination(  //
-        kElements, kCovBin, kCovHash, config.destination()));
+        kElements, kCovBin, kCovHash, config.destination));
     const std::string workdir = (test_dir / kRelDir1).c_str();
     ASSERT_NO_FATAL_FAILURE(VerifyShardsExist(  //
         workdir, kCovBin, kCovHash, kNumShards, ShardType::kDistilled));
@@ -207,11 +151,11 @@
     )pb";
 
     for (const double fraction : {1.0, 0.5, 0.2}) {
-      const SeedCorpusConfig config = ParseSeedCorpusConfig(
+      const SeedCorpusConfig config = CreateTestSeedCorpusConfig(
           absl::Substitute(kConfigStr, kRelDir1, kCovBin, fraction));
       InputAndFeaturesVec elements;
       ASSERT_OK(SampleSeedCorpusElementsFromSource(  //
-          config.sources(0), kCovBin, kCovHash, elements));
+          config.sources[0], kCovBin, kCovHash, elements));
       // NOTE: 1.0 has a precise double representation, so `==` is fine.
       ASSERT_EQ(elements.size(), std::llrint(kElements.size() * fraction))
           << VV(fraction);
@@ -238,26 +182,18 @@
       }
     )pb";
 
-    const std::string config_str = absl::Substitute(  //
-        kConfigStr, kRelDir1, kCovBin, kRelDir2, kNumShards, kIdxDigits);
+    const SeedCorpusConfig config =
+        CreateTestSeedCorpusConfig(absl::Substitute(  //
+            kConfigStr, kRelDir1, kCovBin, kRelDir2, kNumShards, kIdxDigits));
 
     {
       ASSERT_OK(GenerateSeedCorpusFromConfig(  //
-          config_str, kCovBin, kCovHash, ""));
+          config, kCovBin, kCovHash));
       const std::string workdir = (test_dir / kRelDir2).c_str();
       ASSERT_NO_FATAL_FAILURE(VerifyDumpedConfig(workdir, kCovBin, kCovHash));
       ASSERT_NO_FATAL_FAILURE(VerifyShardsExist(  //
           workdir, kCovBin, kCovHash, kNumShards, ShardType::kNormal));
     }
-
-    {
-      ASSERT_OK(GenerateSeedCorpusFromConfig(  //
-          config_str, kCovBin, kCovHash, kRelDir3));
-      const std::string workdir = (test_dir / kRelDir3).c_str();
-      ASSERT_NO_FATAL_FAILURE(VerifyDumpedConfig(workdir, kCovBin, kCovHash));
-      ASSERT_NO_FATAL_FAILURE(VerifyShardsExist(  //
-          workdir, kCovBin, kCovHash, kNumShards, ShardType::kNormal));
-    }
   }
 }
 
diff --git a/codelab/escaping_test.cc b/codelab/escaping_test.cc
index 5e5310e..65caf4d 100644
--- a/codelab/escaping_test.cc
+++ b/codelab/escaping_test.cc
@@ -15,7 +15,7 @@
 #include "./escaping.h"
 
 #include "gtest/gtest.h"
-#include "fuzztest/fuzztest.h"
+#include "./fuzztest/fuzztest.h"
 
 namespace codelab {
 namespace {