Add a tool for analyzing feature files from centipede.

PiperOrigin-RevId: 792747602
diff --git a/centipede/tools/BUILD b/centipede/tools/BUILD
new file mode 100644
index 0000000..5afb027
--- /dev/null
+++ b/centipede/tools/BUILD
@@ -0,0 +1,36 @@
+# Copyright 2025 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.
+
+package(default_compatible_with = ["//buildenv/target:non_prod"])
+
+licenses(["notice"])
+
+# Debugging tools
+
+cc_binary(
+    name = "feature_analyzer",
+    srcs = ["feature_analyzer.cc"],
+    deps = [
+        "@abseil-cpp//absl/flags:flag",
+        "@abseil-cpp//absl/flags:parse",
+        "@abseil-cpp//absl/log",
+        "@abseil-cpp//absl/log:check",
+        "@abseil-cpp//absl/status",
+        "@abseil-cpp//absl/status:statusor",
+        "@com_google_fuzztest//centipede:feature",
+        "@com_google_fuzztest//centipede:util",
+        "@com_google_fuzztest//common:blob_file",
+        "@com_google_fuzztest//common:defs",
+    ],
+)
diff --git a/centipede/tools/feature_analyzer.cc b/centipede/tools/feature_analyzer.cc
new file mode 100644
index 0000000..e95bca7
--- /dev/null
+++ b/centipede/tools/feature_analyzer.cc
@@ -0,0 +1,104 @@
+// 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.
+
+// A simple tool for analyzing a centipede features file.
+//
+// Extracts all features for a given user domain and writes them to LOG(INFO)
+//
+// Usage:
+//  feature_analyzer --user_domain=1 \
+//  --feature_file=/tmp/wd/features.000000 >  out.txt
+//
+#include <cstddef>
+#include <iostream>
+#include <string>
+
+#include "absl/flags/flag.h"
+#include "absl/flags/parse.h"
+#include "absl/log/check.h"
+#include "absl/log/log.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "./centipede/feature.h"
+#include "./centipede/util.h"
+#include "./common/blob_file.h"
+#include "./common/defs.h"
+
+ABSL_FLAG(std::string, feature_file, "", "Path to the feature file to read");
+
+ABSL_FLAG(size_t, user_domain, 0, "User Feature Domain to section out");
+
+namespace fuzztest::internal {
+
+// Read all the features from the file and return them to the caller.
+absl::StatusOr<FeatureVec> ReadFeaturesFile(absl::string_view features_path) {
+  auto features_reader = DefaultBlobFileReaderFactory();
+  absl::Status read_status = features_reader->Open(features_path);
+  if (!read_status.ok()) {
+    std::cerr << "Could not read from file: " << features_path << std::endl;
+    return read_status;
+  }
+  ByteSpan blob;
+  FeatureVec aggregated_features;
+  while (features_reader->Read(blob).ok()) {
+    FeatureVec features;
+    UnpackFeaturesAndHash(blob, &features);
+    for (auto feature : features) {
+      aggregated_features.push_back(feature);
+    }
+  }
+  return aggregated_features;
+}
+
+absl::Status FeatureAnalyzerMain() {
+  size_t user_domain = absl::GetFlag(FLAGS_user_domain);
+  CHECK_LT(user_domain, feature_domains::kUserDomains.size());
+  size_t goal_domain = feature_domains::kUserDomains[user_domain].domain_id();
+
+  absl::StatusOr<FeatureVec> features_or =
+      ReadFeaturesFile(absl::GetFlag(FLAGS_feature_file));
+  if (!features_or.ok()) {
+    return features_or.status();
+  }
+
+  for (auto feature : *features_or) {
+    size_t domain_id = feature_domains::Domain::FeatureToDomainId(feature);
+    size_t index_in_domain =
+        feature_domains::Domain::FeatureToIndexInDomain(feature);
+    if (domain_id == goal_domain) {
+      // Since a feature in user domain zero with an id of zero would typically
+      // be skipped, silifuzz increments all of the values for domain zero by 1
+      // so that they are not skipped due to being all zeros.
+      if (user_domain == 0) {
+        index_in_domain--;
+      }
+      std::cout << "domain: " << domain_id << " feature: " << index_in_domain
+                << std::endl;
+    }
+  }
+  std::cout << "total number of features: " << features_or->size() << std::endl;
+  return absl::OkStatus();
+}
+
+}  // namespace fuzztest::internal
+
+int main(int argc, char* argv[]) {
+  absl::ParseCommandLine(argc, argv);
+  auto status = fuzztest::internal::FeatureAnalyzerMain();
+  if (!status.ok()) {
+    std::cerr << status.error_message() << std::endl;
+    return 1;
+  }
+  return 0;
+}