Move flag reading from worker to runner_utils with some tests.

This is to prepare for consolidating the flag reading with runner/sancov libraries.

PiperOrigin-RevId: 974515199
diff --git a/centipede/BUILD b/centipede/BUILD
index dcaabf1..2062db8 100644
--- a/centipede/BUILD
+++ b/centipede/BUILD
@@ -1730,6 +1730,15 @@
     ],
 )
 
+cc_test(
+    name = "runner_utils_test",
+    srcs = ["runner_utils_test.cc"],
+    deps = [
+        ":runner_utils",
+        "@googletest//:gtest_main",
+    ],
+)
+
 cc_binary(
     name = "command_test_helper",
     srcs = ["command_test_helper.cc"],
diff --git a/centipede/engine_worker.cc b/centipede/engine_worker.cc
index 3c95801..80281ef 100644
--- a/centipede/engine_worker.cc
+++ b/centipede/engine_worker.cc
@@ -99,82 +99,39 @@
     std::_Exit(1);
   }
 }
-
-struct WorkerFlags {
-  bool present;
-  // length of the flags string, excluding the ending '\0'.
-  size_t len;
-  const char* str;
-};
+const char* absl_nullable GetWorkerFlagsEnv() {
+  static const char* flags = []() -> const char* {
+    // TODO(xinhaoyuan): Rename the env name to FUZZTEST_WORKER_FLAGS.
+    if (const char* env = std::getenv("CENTIPEDE_RUNNER_FLAGS")) {
+      WorkerLog("Worker flags: ", env);
+      char* env_copy = strdup(env);
+      if (env_copy == nullptr) {
+        // This should rarely happen.
+        WorkerLog("Failed to copy the flags env due to allocation failure");
+        std::_Exit(1);
+      }
+      return env_copy;
+    }
+    return nullptr;
+  }();
+  return flags;
+}
 
 // The first call of this function must be outside of signal handlers since it
 // allocates memory (enforced by `WorkerInitEarly`). After that it would be
 // signal-safe.
-//
-// The worker flags format is `:(NAME=VALUE|SWITCH:)+`. `GetWorkerFlags`
-// replaces `:` with '\0' so that we can get null-terminated strings of VALUE
-// without copying them, which is important for signal-safety.
-const WorkerFlags& GetWorkerFlags() {
-  static auto worker_flags = []() -> WorkerFlags {
-    // TODO(xinhaoyuan): Rename the env name to FUZZTEST_WORKER_FLAGS.
-    const char* env_flags = std::getenv("CENTIPEDE_RUNNER_FLAGS");
-    if (env_flags == nullptr) {
-      return {};
-    }
-    const size_t len = strlen(env_flags);
-    char* str = reinterpret_cast<char*>(malloc(len + 1));
-    if (str == nullptr) {
-      WorkerLog("Cannot allocate the worker flags", LogLnSync{});
+const EngineFlagHelper& GetWorkerFlags() {
+  static ExplicitLifetime<EngineFlagHelper> worker_flags;
+  [[maybe_unused]] static bool construct_once = [] {
+    worker_flags.Construct(GetWorkerFlagsEnv());
+    if (worker_flags->HasAllocationFailure()) {
+      // This should rarely happen.
+      WorkerLog("Failed to process the flags due to allocation failure.");
       std::_Exit(1);
     }
-    memcpy(str, env_flags, len);
-    str[len] = 0;
-    WorkerLog("Got worker flags ", std::string_view{str, len}, LogLnSync{});
-    // Post-processing to make '\0' as the separator, making each item as a
-    // null-terminating string to be used without copying it.
-    for (size_t i = 0; i < len; ++i) {
-      if (str[i] == ':') str[i] = 0;
-    }
-    return WorkerFlags{true, len, str};
+    return true;
   }();
-  return worker_flags;
-}
-
-// `header` should be in the form of `FLAG_NAME=`.
-//
-// Extracts "value" as a null-terminated string from "\0FLAG_NAME=value\0" in
-// the flags. Returns nullptr if it is not found.
-const char* GetWorkerFlag(std::string_view header) {
-  if (header.empty()) return nullptr;
-  const auto& worker_flags = GetWorkerFlags();
-  if (!worker_flags.present) return nullptr;
-  const auto flags = std::string_view{worker_flags.str, worker_flags.len};
-  size_t pos = 0;
-  while (pos = flags.find(header, pos),
-         pos != flags.npos && pos + header.size() < flags.size()) {
-    if (pos > 0 && flags[pos - 1] == '\0') {
-      return worker_flags.str + pos + header.size();
-    }
-    pos += header.size();
-  }
-  return nullptr;
-}
-
-// Checks whether "\0{name}\0" exists in the flags.
-bool HasWorkerSwitchFlag(std::string_view name) {
-  if (name.empty()) return false;
-  const auto& worker_flags = GetWorkerFlags();
-  if (!worker_flags.present) return false;
-  const auto flags = std::string_view{worker_flags.str, worker_flags.len};
-  size_t pos = 0;
-  while (pos = flags.find(name, pos),
-         pos != flags.npos && pos + name.size() < flags.size()) {
-    if (pos > 0 && flags[pos - 1] == '\0' && flags[pos + name.size()] == '\0') {
-      return true;
-    }
-    pos += name.size();
-  }
-  return false;
+  return *worker_flags;
 }
 
 template <typename... C>
@@ -277,8 +234,8 @@
                              std::string_view message) {
   bool ignored = GetWorkerState().has_failure_output.exchange(true);
   if (!ignored) {
-    if (const char* failure_description_path =
-            GetWorkerFlag(kWorkerFailureDescriptionPathFlagHeader);
+    if (const char* failure_description_path = GetWorkerFlags().GetStringFlag(
+            kWorkerFailureDescriptionPathFlagHeader);
         failure_description_path != nullptr) {
       TrySetFileContents(failure_description_path,
                          /*append=*/false, prefix, message);
@@ -322,8 +279,8 @@
   if (!ignored) {
     WorkerCheck(WorkerEmitFailureOutput(/*prefix=*/"", description),
                 "Failed to emit failure output for the finding");
-    if (const char* finding_signature_path =
-            GetWorkerFlag(kWorkerFailureSignaturePathFlagHeader);
+    if (const char* finding_signature_path = GetWorkerFlags().GetStringFlag(
+            kWorkerFailureSignaturePathFlagHeader);
         finding_signature_path != nullptr) {
       TrySetFileContents(finding_signature_path,
                          /*append=*/false, signature);
@@ -351,7 +308,7 @@
 
 __attribute__((constructor(200))) void WorkerInitEarly() {
   const char* persistent_mode_socket_path =
-      GetWorkerFlag(kWorkerPersistentModeSocketPathFlagHeader);
+      GetWorkerFlags().GetStringFlag(kWorkerPersistentModeSocketPathFlagHeader);
   if (persistent_mode_socket_path == nullptr) return;
   persistent_mode_socket = socket(AF_UNIX, SOCK_STREAM, 0);
   if (persistent_mode_socket < 0) {
@@ -409,11 +366,11 @@
 
 BlobSequence* GetInputsBlobSequence() {
   static auto result = []() -> BlobSequence* {
-    if (!HasWorkerSwitchFlag("shmem")) {
+    if (!GetWorkerFlags().HasSwitchFlag("shmem")) {
       return nullptr;
     }
     const char* input_path =
-        GetWorkerFlag(kWorkerInputsBlobSequencePathFlagHeader);
+        GetWorkerFlags().GetStringFlag(kWorkerInputsBlobSequencePathFlagHeader);
     WorkerCheck(input_path != nullptr, "inputs blob sequence is missing");
     return new SharedMemoryBlobSequence(input_path);
   }();
@@ -422,11 +379,11 @@
 
 BlobSequence* GetOutputsBlobSequence() {
   static auto result = []() -> BlobSequence* {
-    if (!HasWorkerSwitchFlag("shmem")) {
+    if (!GetWorkerFlags().HasSwitchFlag("shmem")) {
       return nullptr;
     }
-    const char* output_path =
-        GetWorkerFlag(kWorkerOutputsBlobSequencePathFlagHeader);
+    const char* output_path = GetWorkerFlags().GetStringFlag(
+        kWorkerOutputsBlobSequencePathFlagHeader);
     WorkerCheck(output_path != nullptr, "outputs blob sequence is missing");
     return new SharedMemoryBlobSequence(output_path);
   }();
@@ -435,12 +392,9 @@
 
 int GetCrossOverLevel() {
   static int result = []() {
-    const char* cross_over_level_str = GetWorkerFlag(kWorkerCrossOverLevel);
-    if (cross_over_level_str != nullptr) {
-      const int parsed =
-          atoi(cross_over_level_str);  // NOLINT: can't use strto64, etc.
-      if (0 <= parsed && parsed <= 100) return parsed;
-    }
+    const uint64_t cross_over_level =
+        GetWorkerFlags().GetIntFlag(kWorkerCrossOverLevel, 50);
+    if (cross_over_level <= 100) return static_cast<int>(cross_over_level);
     // Default
     return 50;
   }();
@@ -449,16 +403,16 @@
 
 std::optional<WorkerAction> GetWorkerAction() {
   static auto worker_action = []() -> std::optional<WorkerAction> {
-    if (HasWorkerSwitchFlag("dump_configuration")) {
+    if (GetWorkerFlags().HasSwitchFlag("dump_configuration")) {
       return WorkerAction::kNoOp;
     }
-    if (HasWorkerSwitchFlag("dump_binary_id")) {
+    if (GetWorkerFlags().HasSwitchFlag("dump_binary_id")) {
       return WorkerAction::kGetBinaryId;
     }
-    if (HasWorkerSwitchFlag("list_tests")) {
+    if (GetWorkerFlags().HasSwitchFlag("list_tests")) {
       return WorkerAction::kListTests;
     }
-    if (HasWorkerSwitchFlag("dump_seed_inputs")) {
+    if (GetWorkerFlags().HasSwitchFlag("dump_seed_inputs")) {
       return WorkerAction::kTestGetSeeds;
     }
     auto* inputs_blobseq = GetInputsBlobSequence();
@@ -499,7 +453,7 @@
 void WorkerDoGetBinaryId(const FuzzTestAdapterManager& manager) {
   if (GetWorkerState().saved_binary_id.exchange(true)) return;
   const char* binary_id_output_path =
-      GetWorkerFlag(kWorkerBinaryIdOutputFlagHeader);
+      GetWorkerFlags().GetStringFlag(kWorkerBinaryIdOutputFlagHeader);
   WorkerCheck(binary_id_output_path != nullptr,
               "binary ID output path is not set");
   std::vector<uint8_t> binary_id;
@@ -512,7 +466,7 @@
 
 void WorkerDoListCurrentTest(std::string_view test_name) {
   const char* test_listing_output_path =
-      GetWorkerFlag(kWorkerTestListingOutputFlagHeader);
+      GetWorkerFlags().GetStringFlag(kWorkerTestListingOutputFlagHeader);
   WorkerCheck(test_listing_output_path != nullptr,
               "binary ID output path is not set");
   TrySetFileContents(test_listing_output_path,
@@ -536,7 +490,7 @@
   }
 
   static const char* output_dir =
-      GetWorkerFlag(kWorkerTestGetSeedsOutputDirFlagHeader);
+      GetWorkerFlags().GetStringFlag(kWorkerTestGetSeedsOutputDirFlagHeader);
   WorkerCheck(output_dir != nullptr, "seeds output path must be specified");
 
   for (size_t i = 0; i < seed_handles.size(); ++i) {
@@ -845,7 +799,7 @@
 
 const char* FuzzTestWorkerGetTestName() {
   static auto test_name = []() -> const char* {
-    return GetWorkerFlag(kWorkerTestNameFlagHeader);
+    return GetWorkerFlags().GetStringFlag(kWorkerTestNameFlagHeader);
   }();
   return test_name;
 }
@@ -872,15 +826,12 @@
         // to happen when the stdout/stderr are not redirected to a file.
         (void)ftruncate(fd, 0);
       }
-      WorkerLog(
-          "FuzzTest engine worker (",
-          req == PersistentModeRequest::kExit ? "exiting persistent mode"
-                                              : "persistent mode batch",
-          "); flags: ",
-          GetWorkerFlags().present
-              ? std::string_view{GetWorkerFlags().str, GetWorkerFlags().len}
-              : "",
-          LogLnSync{});
+      WorkerLog("FuzzTest engine worker (",
+                req == PersistentModeRequest::kExit ? "exiting persistent mode"
+                                                    : "persistent mode batch",
+                "); flags: ",
+                GetWorkerFlagsEnv() != nullptr ? GetWorkerFlagsEnv() : "",
+                LogLnSync{});
     }
     if (req == PersistentModeRequest::kExit) break;
     WorkerCheck(req == PersistentModeRequest::kRunBatch,
@@ -917,10 +868,9 @@
 }
 
 FuzzTestWorkerStatus WorkerRun(const FuzzTestAdapterManager& manager) {
-  const auto& flags = GetWorkerFlags();
-  WorkerCheck(flags.present, "worker flags must present");
+  WorkerCheck(GetWorkerFlagsEnv() != nullptr, "worker flags must present");
 
-  if (HasWorkerSwitchFlag("dump_configuration")) {
+  if (GetWorkerFlags().HasSwitchFlag("dump_configuration")) {
     return kFuzzTestWorkerSuccess;
   }
 
@@ -1018,13 +968,14 @@
 namespace {
 
 using ::fuzztest::internal::GetWorkerFlags;
+using ::fuzztest::internal::GetWorkerFlagsEnv;
 using ::fuzztest::internal::WorkerCheck;
 using ::fuzztest::internal::WorkerRun;
 
 }  // namespace
 
 int FuzzTestWorkerIsRequired() {
-  static int result = GetWorkerFlags().present &&
+  static int result = GetWorkerFlagsEnv() != nullptr &&
                       fuzztest::internal::GetWorkerAction().has_value();
   return result;
 }
diff --git a/centipede/runner_utils.h b/centipede/runner_utils.h
index 4b85afa..fb28cb5 100644
--- a/centipede/runner_utils.h
+++ b/centipede/runner_utils.h
@@ -17,9 +17,13 @@
 
 #include <sys/stat.h>
 
+#include <cstddef>
 #include <cstdint>
 #include <cstdio>
+#include <cstdlib>
+#include <cstring>
 #include <new>
+#include <string_view>
 #include <vector>
 
 #include "absl/base/nullability.h"
@@ -125,6 +129,78 @@
   alignas(T) unsigned char space_[sizeof(T)];
 };
 
+// Helper class for processing and reading the engine flags.
+class EngineFlagHelper {
+ public:
+  // Constructs the helper for a C-string `flags` with the format of :(ENTRY:)+.
+  explicit EngineFlagHelper(const char* absl_nullable flags)
+      : flags_(nullptr), size_(0), has_allocation_failure_(false) {
+    if (flags == nullptr) return;
+    flags_ = strdup(flags);
+    if (flags_ == nullptr) {
+      has_allocation_failure_ = true;
+      return;
+    }
+    size_ = strlen(flags_);
+    // Post-processing to make '\0' as the separator, making each item as a
+    // null-terminating string to be used without copying it.
+    for (size_t i = 0; i < size_; ++i) {
+      if (flags_[i] == ':') flags_[i] = 0;
+    }
+  }
+
+  EngineFlagHelper(const EngineFlagHelper&) = delete;
+  EngineFlagHelper& operator=(const EngineFlagHelper&) = delete;
+
+  ~EngineFlagHelper() {
+    if (flags_) {
+      free(flags_);
+    }
+  }
+
+  bool HasAllocationFailure() const { return has_allocation_failure_; }
+
+  bool HasSwitchFlag(std::string_view name) const {
+    if (name.empty() || flags_ == nullptr) return false;
+    const auto flags = std::string_view{flags_, size_};
+    size_t pos = 0;
+    while (pos = flags.find(name, pos),
+           pos != flags.npos && pos + name.size() < flags.size()) {
+      if (pos > 0 && flags[pos - 1] == '\0' &&
+          flags[pos + name.size()] == '\0') {
+        return true;
+      }
+      pos += name.size();
+    }
+    return false;
+  }
+
+  uint64_t GetIntFlag(std::string_view header, uint64_t default_value) const {
+    const char* absl_nullable flag = GetStringFlag(header);
+    if (flag == nullptr) return default_value;
+    return atoll(flag);  // NOLINT: can't use strto64, etc.
+  }
+
+  const char* absl_nullable GetStringFlag(std::string_view header) const {
+    if (header.empty() || flags_ == nullptr) return nullptr;
+    const auto flags = std::string_view{flags_, size_};
+    size_t pos = 0;
+    while (pos = flags.find(header, pos),
+           pos != flags.npos && pos + header.size() < flags.size()) {
+      if (pos > 0 && flags[pos - 1] == '\0') {
+        return flags.data() + pos + header.size();
+      }
+      pos += header.size();
+    }
+    return nullptr;
+  }
+
+ private:
+  char* absl_nullable flags_;
+  size_t size_;
+  bool has_allocation_failure_;
+};
+
 }  // namespace fuzztest::internal
 
 #endif  // THIRD_PARTY_CENTIPEDE_RUNNER_UTILS_H_
diff --git a/centipede/runner_utils_test.cc b/centipede/runner_utils_test.cc
new file mode 100644
index 0000000..39367f4
--- /dev/null
+++ b/centipede/runner_utils_test.cc
@@ -0,0 +1,47 @@
+// Copyright 2026 Google LLC
+//
+// 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/runner_utils.h"
+
+#include <string_view>
+
+#include "gtest/gtest.h"
+
+namespace fuzztest::internal {
+namespace {
+
+TEST(RunnerUtilsTest, EngineFlagHelperWorksWithoutFlags) {
+  EngineFlagHelper helper(nullptr);
+  EXPECT_FALSE(helper.HasSwitchFlag("foo"));
+  EXPECT_EQ(helper.GetIntFlag("bar=", 42), 42);
+  EXPECT_EQ(helper.GetStringFlag("baz="), nullptr);
+}
+
+TEST(RunnerUtilsTest, EngineFlagHelperWorksWithFlags) {
+  EngineFlagHelper helper(":flag1:flag2=123:str=hello:");
+  EXPECT_TRUE(helper.HasSwitchFlag("flag1"));
+  EXPECT_FALSE(helper.HasSwitchFlag("flag"));
+  EXPECT_FALSE(helper.HasSwitchFlag("flag1_extra"));
+  EXPECT_FALSE(helper.HasSwitchFlag("flag2"));
+  EXPECT_FALSE(helper.HasSwitchFlag("missing"));
+
+  EXPECT_EQ(helper.GetIntFlag("flag2=", 0), 123);
+  EXPECT_EQ(helper.GetIntFlag("missing=", 999), 999);
+
+  EXPECT_STREQ(helper.GetStringFlag("str="), "hello");
+  EXPECT_EQ(helper.GetStringFlag("missing="), nullptr);
+}
+
+}  // namespace
+}  // namespace fuzztest::internal