move config pb <> txt into src/trace_config_utils

I'm extracting a library that can deal with txt<>pb conversion
for the TraceConfig. I'm planning to expose this in wasm and use
it in the Record UI, to get rid of a lot of hacks that try to
replicate the same logic in typescript.

Bug: 377651301
Change-Id: Ia9771603b8fd875f3fe9417f0d79b27ded58d826
diff --git a/Android.bp b/Android.bp
index 5d85a3d..d6e26f4 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1233,11 +1233,11 @@
         ":perfetto_src_ipc_client",
         ":perfetto_src_ipc_common",
         ":perfetto_src_perfetto_cmd_bugreport_path",
-        ":perfetto_src_perfetto_cmd_pbtxt_to_pb",
         ":perfetto_src_perfetto_cmd_perfetto_cmd",
         ":perfetto_src_perfetto_cmd_protos_cpp_gen",
         ":perfetto_src_perfetto_cmd_trigger_producer",
         ":perfetto_src_protozero_protozero",
+        ":perfetto_src_trace_config_utils_txt_to_pb",
         ":perfetto_src_tracing_common",
         ":perfetto_src_tracing_core_core",
         ":perfetto_src_tracing_ipc_common",
@@ -1303,8 +1303,8 @@
         "perfetto_protos_perfetto_trace_track_event_zero_gen_headers",
         "perfetto_protos_perfetto_trace_translation_zero_gen_headers",
         "perfetto_src_base_version_gen_h",
-        "perfetto_src_perfetto_cmd_gen_cc_config_descriptor",
         "perfetto_src_perfetto_cmd_protos_cpp_gen_headers",
+        "perfetto_src_trace_config_utils_gen_cc_config_descriptor",
     ],
     defaults: [
         "perfetto_defaults",
@@ -11218,29 +11218,6 @@
     name: "perfetto_src_perfetto_cmd_bugreport_path",
 }
 
-// GN: //src/perfetto_cmd:gen_cc_config_descriptor
-genrule {
-    name: "perfetto_src_perfetto_cmd_gen_cc_config_descriptor",
-    srcs: [
-        ":perfetto_protos_perfetto_config_descriptor",
-    ],
-    cmd: "$(location tools/gen_cc_proto_descriptor.py) --gen_dir=$(genDir) --cpp_out=$(out) $(in)",
-    out: [
-        "src/perfetto_cmd/config.descriptor.h",
-    ],
-    tool_files: [
-        "tools/gen_cc_proto_descriptor.py",
-    ],
-}
-
-// GN: //src/perfetto_cmd:pbtxt_to_pb
-filegroup {
-    name: "perfetto_src_perfetto_cmd_pbtxt_to_pb",
-    srcs: [
-        "src/perfetto_cmd/pbtxt_to_pb.cc",
-    ],
-}
-
 // GN: //src/perfetto_cmd:perfetto_cmd
 filegroup {
     name: "perfetto_src_perfetto_cmd_perfetto_cmd",
@@ -11318,7 +11295,6 @@
     srcs: [
         "src/perfetto_cmd/config_unittest.cc",
         "src/perfetto_cmd/packet_writer_unittest.cc",
-        "src/perfetto_cmd/pbtxt_to_pb_unittest.cc",
     ],
 }
 
@@ -12264,6 +12240,46 @@
     ],
 }
 
+// GN: //src/trace_config_utils:gen_cc_config_descriptor
+genrule {
+    name: "perfetto_src_trace_config_utils_gen_cc_config_descriptor",
+    srcs: [
+        ":perfetto_protos_perfetto_config_descriptor",
+    ],
+    cmd: "$(location tools/gen_cc_proto_descriptor.py) --gen_dir=$(genDir) --cpp_out=$(out) $(in)",
+    out: [
+        "src/trace_config_utils/config.descriptor.h",
+    ],
+    tool_files: [
+        "tools/gen_cc_proto_descriptor.py",
+    ],
+}
+
+// GN: //src/trace_config_utils:pb_to_txt
+filegroup {
+    name: "perfetto_src_trace_config_utils_pb_to_txt",
+    srcs: [
+        "src/trace_config_utils/pb_to_txt.cc",
+    ],
+}
+
+// GN: //src/trace_config_utils:txt_to_pb
+filegroup {
+    name: "perfetto_src_trace_config_utils_txt_to_pb",
+    srcs: [
+        "src/trace_config_utils/txt_to_pb.cc",
+    ],
+}
+
+// GN: //src/trace_config_utils:unittests
+filegroup {
+    name: "perfetto_src_trace_config_utils_unittests",
+    srcs: [
+        "src/trace_config_utils/pb_to_txt_unittest.cc",
+        "src/trace_config_utils/txt_to_pb_unittest.cc",
+    ],
+}
+
 // GN: //src/trace_processor/containers:containers
 filegroup {
     name: "perfetto_src_trace_processor_containers_containers",
@@ -15593,7 +15609,6 @@
         ":perfetto_src_kernel_utils_syscall_table",
         ":perfetto_src_kernel_utils_unittests",
         ":perfetto_src_perfetto_cmd_bugreport_path",
-        ":perfetto_src_perfetto_cmd_pbtxt_to_pb",
         ":perfetto_src_perfetto_cmd_perfetto_cmd",
         ":perfetto_src_perfetto_cmd_protos_cpp_gen",
         ":perfetto_src_perfetto_cmd_trigger_producer",
@@ -15645,6 +15660,9 @@
         ":perfetto_src_protozero_unittests",
         ":perfetto_src_shared_lib_intern_map",
         ":perfetto_src_shared_lib_unittests",
+        ":perfetto_src_trace_config_utils_pb_to_txt",
+        ":perfetto_src_trace_config_utils_txt_to_pb",
+        ":perfetto_src_trace_config_utils_unittests",
         ":perfetto_src_trace_processor_containers_containers",
         ":perfetto_src_trace_processor_containers_unittests",
         ":perfetto_src_trace_processor_db_column_column",
@@ -15963,7 +15981,6 @@
         "perfetto_src_base_version_gen_h",
         "perfetto_src_ipc_test_messages_cpp_gen_headers",
         "perfetto_src_ipc_test_messages_ipc_gen_headers",
-        "perfetto_src_perfetto_cmd_gen_cc_config_descriptor",
         "perfetto_src_perfetto_cmd_protos_cpp_gen_headers",
         "perfetto_src_protozero_testing_messages_cpp_gen_headers",
         "perfetto_src_protozero_testing_messages_lite_gen_headers",
@@ -15974,6 +15991,7 @@
         "perfetto_src_protozero_testing_messages_subpackage_lite_gen_headers",
         "perfetto_src_protozero_testing_messages_subpackage_zero_gen_headers",
         "perfetto_src_protozero_testing_messages_zero_gen_headers",
+        "perfetto_src_trace_config_utils_gen_cc_config_descriptor",
         "perfetto_src_trace_processor_gen_cc_test_messages_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_android_track_event_descriptor",
         "perfetto_src_trace_processor_importers_proto_gen_cc_chrome_track_event_descriptor",
diff --git a/BUILD b/BUILD
index c1ffd78..fdb6c73 100644
--- a/BUILD
+++ b/BUILD
@@ -266,13 +266,13 @@
         ":include_perfetto_base_base",
         ":include_perfetto_public_abi_base",
         ":include_perfetto_public_base",
-        ":src_perfetto_cmd_pbtxt_to_pb",
         ":src_protozero_filtering_bytecode_common",
         ":src_protozero_filtering_bytecode_generator",
         ":src_protozero_filtering_bytecode_parser",
         ":src_protozero_filtering_filter_util",
         ":src_protozero_filtering_message_filter",
         ":src_protozero_filtering_string_filter",
+        ":src_trace_config_utils_txt_to_pb",
         "src/tools/proto_filter/proto_filter.cc",
     ],
     deps = [
@@ -293,7 +293,7 @@
         ":protozero",
         ":src_base_base",
         ":src_base_version",
-        ":src_perfetto_cmd_gen_cc_config_descriptor",
+        ":src_trace_config_utils_gen_cc_config_descriptor",
     ] + PERFETTO_CONFIG.deps.protobuf_full,
 )
 
@@ -1412,26 +1412,6 @@
     ],
 )
 
-# GN target: //src/perfetto_cmd:gen_cc_config_descriptor
-perfetto_cc_proto_descriptor(
-    name = "src_perfetto_cmd_gen_cc_config_descriptor",
-    deps = [
-        ":protos_perfetto_config_descriptor",
-    ],
-    outs = [
-        "src/perfetto_cmd/config.descriptor.h",
-    ],
-)
-
-# GN target: //src/perfetto_cmd:pbtxt_to_pb
-perfetto_filegroup(
-    name = "src_perfetto_cmd_pbtxt_to_pb",
-    srcs = [
-        "src/perfetto_cmd/pbtxt_to_pb.cc",
-        "src/perfetto_cmd/pbtxt_to_pb.h",
-    ],
-)
-
 # GN target: //src/perfetto_cmd:perfetto_cmd
 perfetto_filegroup(
     name = "src_perfetto_cmd_perfetto_cmd",
@@ -1582,6 +1562,26 @@
     ],
 )
 
+# GN target: //src/trace_config_utils:gen_cc_config_descriptor
+perfetto_cc_proto_descriptor(
+    name = "src_trace_config_utils_gen_cc_config_descriptor",
+    deps = [
+        ":protos_perfetto_config_descriptor",
+    ],
+    outs = [
+        "src/trace_config_utils/config.descriptor.h",
+    ],
+)
+
+# GN target: //src/trace_config_utils:txt_to_pb
+perfetto_filegroup(
+    name = "src_trace_config_utils_txt_to_pb",
+    srcs = [
+        "src/trace_config_utils/txt_to_pb.cc",
+        "src/trace_config_utils/txt_to_pb.h",
+    ],
+)
+
 # GN target: //src/trace_processor/containers:containers
 perfetto_cc_library(
     name = "src_trace_processor_containers_containers",
@@ -6484,9 +6484,9 @@
         ":src_android_stats_android_stats",
         ":src_android_stats_perfetto_atoms",
         ":src_perfetto_cmd_bugreport_path",
-        ":src_perfetto_cmd_pbtxt_to_pb",
         ":src_perfetto_cmd_perfetto_cmd",
         ":src_perfetto_cmd_trigger_producer",
+        ":src_trace_config_utils_txt_to_pb",
         ":src_tracing_common",
         ":src_tracing_core_core",
         ":src_tracing_ipc_common",
@@ -6554,8 +6554,8 @@
         ":protozero",
         ":src_base_base",
         ":src_base_version",
-        ":src_perfetto_cmd_gen_cc_config_descriptor",
         ":src_perfetto_cmd_protos_cpp",
+        ":src_trace_config_utils_gen_cc_config_descriptor",
     ],
 )
 
diff --git a/BUILD.gn b/BUILD.gn
index 7dd9d31..b495c20 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -57,7 +57,10 @@
 }
 
 if (enable_perfetto_traceconv) {
-  all_targets += [ "src/traceconv" ]
+  all_targets += [
+    "src/traceconv",
+    "src/trace_config_utils",
+  ]
   if (is_cross_compiling) {
     # In many cross-compilation scenarios (typically Android) developers expect
     # the host version of traceconv to be available somewhere in out/, so
@@ -66,7 +69,10 @@
     # cc_binary_host("traceconv") target in Android.bp.
     # Note that when cross-compiling the host executable will be available in
     # out/xxx/gcc_like_host/traceconv NOT just out/xxx/traceconv.
-    all_targets += [ "src/traceconv($host_toolchain)" ]
+    all_targets += [
+      "src/traceconv($host_toolchain)",
+      "src/trace_config_utils($host_toolchain)",
+    ]
   }
 }
 
diff --git a/gn/perfetto_unittests.gni b/gn/perfetto_unittests.gni
index f54ef7a..d1590c6 100644
--- a/gn/perfetto_unittests.gni
+++ b/gn/perfetto_unittests.gni
@@ -75,6 +75,7 @@
 
 if (enable_perfetto_trace_processor) {
   perfetto_unittests_targets += [ "src/trace_processor:unittests" ]
+  perfetto_unittests_targets += [ "src/trace_config_utils:unittests" ]
 
   if (enable_perfetto_trace_processor_sqlite) {
     perfetto_unittests_targets += [ "src/trace_processor/metrics:unittests" ]
diff --git a/src/perfetto_cmd/BUILD.gn b/src/perfetto_cmd/BUILD.gn
index 4cfe0ea..0edf72a 100644
--- a/src/perfetto_cmd/BUILD.gn
+++ b/src/perfetto_cmd/BUILD.gn
@@ -13,7 +13,6 @@
 # limitations under the License.
 
 import("../../gn/perfetto.gni")
-import("../../gn/perfetto_cc_proto_descriptor.gni")
 import("../../gn/proto_library.gni")
 import("../../gn/test.gni")
 
@@ -50,8 +49,6 @@
   ]
   deps = [
     ":bugreport_path",
-    ":gen_cc_config_descriptor",
-    ":pbtxt_to_pb",
     ":trigger_producer",
     "../../gn:default_deps",
     "../../protos/perfetto/common:cpp",
@@ -62,6 +59,7 @@
     "../base",
     "../base:version",
     "../protozero",
+    "../trace_config_utils:txt_to_pb",
     "../tracing/ipc/consumer",
   ]
   sources = [
@@ -72,6 +70,8 @@
     "perfetto_cmd.cc",
     "perfetto_cmd.h",
   ]
+  assert_no_deps = [ "../trace_processor/*" ]
+
   if (is_android) {
     deps += [ "../android_internal:lazy_library_loader" ]
     sources += [ "perfetto_cmd_android.cc" ]
@@ -87,25 +87,6 @@
   ]
 }
 
-source_set("pbtxt_to_pb") {
-  deps = [
-    ":gen_cc_config_descriptor",
-    "../../gn:default_deps",
-    "../../protos/perfetto/common:cpp",
-    "../base",
-    "../protozero",
-  ]
-  sources = [
-    "pbtxt_to_pb.cc",
-    "pbtxt_to_pb.h",
-  ]
-}
-
-perfetto_cc_proto_descriptor("gen_cc_config_descriptor") {
-  descriptor_name = "config.descriptor"
-  descriptor_target = "../../protos/perfetto/config:descriptor"
-}
-
 source_set("trigger_perfetto_cmd") {
   public_deps = [
     ":protos_cpp",
@@ -143,7 +124,6 @@
   testonly = true
   public_deps = []
   deps = [
-    ":pbtxt_to_pb",
     ":perfetto_cmd",
     "../../gn:default_deps",
     "../../gn:gtest_and_gmock",
@@ -157,6 +137,5 @@
   sources = [
     "config_unittest.cc",
     "packet_writer_unittest.cc",
-    "pbtxt_to_pb_unittest.cc",
   ]
 }
diff --git a/src/perfetto_cmd/pbtxt_to_pb.h b/src/perfetto_cmd/pbtxt_to_pb.h
deleted file mode 100644
index 6b9db32..0000000
--- a/src/perfetto_cmd/pbtxt_to_pb.h
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * 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
- *
- *      http://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 SRC_PERFETTO_CMD_PBTXT_TO_PB_H_
-#define SRC_PERFETTO_CMD_PBTXT_TO_PB_H_
-
-#include <stdint.h>
-
-#include <string>
-#include <vector>
-
-namespace perfetto {
-
-class ErrorReporter {
- public:
-  ErrorReporter();
-  virtual ~ErrorReporter();
-  virtual void AddError(size_t row,
-                        size_t column,
-                        size_t size,
-                        const std::string& message) = 0;
-};
-
-std::vector<uint8_t> PbtxtToPb(const std::string& input,
-                               ErrorReporter* reporter);
-
-}  // namespace perfetto
-
-#endif  // SRC_PERFETTO_CMD_PBTXT_TO_PB_H_
diff --git a/src/perfetto_cmd/perfetto_cmd.cc b/src/perfetto_cmd/perfetto_cmd.cc
index 31da575..74f4b64 100644
--- a/src/perfetto_cmd/perfetto_cmd.cc
+++ b/src/perfetto_cmd/perfetto_cmd.cc
@@ -77,8 +77,8 @@
 #include "src/perfetto_cmd/bugreport_path.h"
 #include "src/perfetto_cmd/config.h"
 #include "src/perfetto_cmd/packet_writer.h"
-#include "src/perfetto_cmd/pbtxt_to_pb.h"
 #include "src/perfetto_cmd/trigger_producer.h"
+#include "src/trace_config_utils/txt_to_pb.h"
 
 #include "protos/perfetto/common/ftrace_descriptor.gen.h"
 #include "protos/perfetto/common/tracing_service_state.gen.h"
@@ -100,60 +100,15 @@
 const uint32_t kOnTraceDataTimeoutMs = 3000;
 const uint32_t kCloneTimeoutMs = 30000;
 
-class LoggingErrorReporter : public ErrorReporter {
- public:
-  LoggingErrorReporter(std::string file_name, const char* config)
-      : file_name_(std::move(file_name)), config_(config) {}
-
-  void AddError(size_t row,
-                size_t column,
-                size_t length,
-                const std::string& message) override {
-    parsed_successfully_ = false;
-    std::string line = ExtractLine(row - 1).ToStdString();
-    if (!line.empty() && line[line.length() - 1] == '\n') {
-      line.erase(line.length() - 1);
-    }
-
-    std::string guide(column + length, ' ');
-    for (size_t i = column; i < column + length; i++) {
-      guide[i - 1] = i == column ? '^' : '~';
-    }
-    fprintf(stderr, "%s:%zu:%zu error: %s\n", file_name_.c_str(), row, column,
-            message.c_str());
-    fprintf(stderr, "%s\n", line.c_str());
-    fprintf(stderr, "%s\n", guide.c_str());
-  }
-
-  bool Success() const { return parsed_successfully_; }
-
- private:
-  base::StringView ExtractLine(size_t line) {
-    const char* start = config_;
-    const char* end = config_;
-
-    for (size_t i = 0; i < line + 1; i++) {
-      start = end;
-      char c;
-      while ((c = *end++) && c != '\n')
-        ;
-    }
-    return base::StringView(start, static_cast<size_t>(end - start));
-  }
-
-  bool parsed_successfully_ = true;
-  std::string file_name_;
-  const char* config_;
-};
-
 bool ParseTraceConfigPbtxt(const std::string& file_name,
                            const std::string& pbtxt,
                            TraceConfig* config) {
-  LoggingErrorReporter reporter(file_name, pbtxt.c_str());
-  std::vector<uint8_t> buf = PbtxtToPb(pbtxt, &reporter);
-  if (!reporter.Success())
+  auto res = TraceConfigTxtToPb(pbtxt, file_name);
+  if (!res.ok()) {
+    fprintf(stderr, "%s\n", res.status().c_message());
     return false;
-  if (!config->ParseFromArray(buf.data(), buf.size()))
+  }
+  if (!config->ParseFromArray(res->data(), res->size()))
     return false;
   return true;
 }
diff --git a/src/tools/proto_filter/BUILD.gn b/src/tools/proto_filter/BUILD.gn
index a9bb97a..e2e1d16 100644
--- a/src/tools/proto_filter/BUILD.gn
+++ b/src/tools/proto_filter/BUILD.gn
@@ -22,11 +22,11 @@
     "../../../protos/perfetto/config:cpp",
     "../../base",
     "../../base:version",
-    "../../perfetto_cmd:pbtxt_to_pb",
     "../../protozero",
     "../../protozero/filtering:bytecode_generator",
     "../../protozero/filtering:filter_util",
     "../../protozero/filtering:message_filter",
+    "../../trace_config_utils:txt_to_pb",
   ]
   sources = [ "proto_filter.cc" ]
 }
diff --git a/src/tools/proto_filter/proto_filter.cc b/src/tools/proto_filter/proto_filter.cc
index b3a9572..3994896 100644
--- a/src/tools/proto_filter/proto_filter.cc
+++ b/src/tools/proto_filter/proto_filter.cc
@@ -20,9 +20,9 @@
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/version.h"
 #include "protos/perfetto/config/trace_config.gen.h"
-#include "src/perfetto_cmd/pbtxt_to_pb.h"
 #include "src/protozero/filtering/filter_util.h"
 #include "src/protozero/filtering/message_filter.h"
+#include "src/trace_config_utils/txt_to_pb.h"
 
 namespace perfetto {
 namespace proto_filter {
@@ -81,52 +81,6 @@
                -f /tmp/bytecode
 )";
 
-class LoggingErrorReporter : public ErrorReporter {
- public:
-  LoggingErrorReporter(std::string file_name, const char* config)
-      : file_name_(file_name), config_(config) {}
-
-  void AddError(size_t row,
-                size_t column,
-                size_t length,
-                const std::string& message) override {
-    parsed_successfully_ = false;
-    std::string line = ExtractLine(row - 1).ToStdString();
-    if (!line.empty() && line[line.length() - 1] == '\n') {
-      line.erase(line.length() - 1);
-    }
-
-    std::string guide(column + length, ' ');
-    for (size_t i = column; i < column + length; i++) {
-      guide[i - 1] = i == column ? '^' : '~';
-    }
-    fprintf(stderr, "%s:%zu:%zu error: %s\n", file_name_.c_str(), row, column,
-            message.c_str());
-    fprintf(stderr, "%s\n", line.c_str());
-    fprintf(stderr, "%s\n", guide.c_str());
-  }
-
-  bool Success() const { return parsed_successfully_; }
-
- private:
-  base::StringView ExtractLine(size_t line) {
-    const char* start = config_;
-    const char* end = config_;
-
-    for (size_t i = 0; i < line + 1; i++) {
-      start = end;
-      char c;
-      while ((c = *end++) && c != '\n')
-        ;
-    }
-    return base::StringView(start, static_cast<size_t>(end - start));
-  }
-
-  bool parsed_successfully_ = true;
-  std::string file_name_;
-  const char* config_;
-};
-
 using TraceFilter = protos::gen::TraceConfig::TraceFilter;
 std::optional<protozero::StringFilter::Policy> ConvertPolicy(
     TraceFilter::StringFilterPolicy policy) {
@@ -309,12 +263,13 @@
       PERFETTO_ELOG("Could not open config file %s", config_in.c_str());
       return 1;
     }
-    LoggingErrorReporter reporter(config_in, config_data.c_str());
-    auto config_bytes = PbtxtToPb(config_data, &reporter);
-    if (!reporter.Success()) {
+    auto res = TraceConfigTxtToPb(config_data, config_in);
+    if (!res.ok()) {
+      fprintf(stderr, "%s\n", res.status().c_message());
       return 1;
     }
 
+    std::vector<uint8_t>& config_bytes = res.value();
     protos::gen::TraceConfig config;
     config.ParseFromArray(config_bytes.data(), config_bytes.size());
 
diff --git a/src/trace_config_utils/BUILD.gn b/src/trace_config_utils/BUILD.gn
new file mode 100644
index 0000000..586b2de
--- /dev/null
+++ b/src/trace_config_utils/BUILD.gn
@@ -0,0 +1,99 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# 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
+#
+#      http://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.
+
+import("../../gn/perfetto.gni")
+import("../../gn/perfetto_cc_proto_descriptor.gni")
+import("../../gn/test.gni")
+import("../../gn/wasm.gni")
+
+source_set("txt_to_pb") {
+  deps = [
+    ":gen_cc_config_descriptor",
+    "../../gn:default_deps",
+    "../../protos/perfetto/common:cpp",
+    "../../protos/perfetto/config:cpp",
+    "../base",
+    "../protozero",
+  ]
+  sources = [
+    "txt_to_pb.cc",
+    "txt_to_pb.h",
+  ]
+}
+
+source_set("pb_to_txt") {
+  deps = [
+    ":gen_cc_config_descriptor",
+    "../../gn:default_deps",
+    "../base",
+    "../protozero",
+    "../trace_processor/util:descriptors",
+    "../trace_processor/util:protozero_to_text",
+  ]
+  sources = [
+    "pb_to_txt.cc",
+    "pb_to_txt.h",
+  ]
+}
+
+source_set("main") {
+  deps = [
+    ":pb_to_txt",
+    ":txt_to_pb",
+    "../../gn:default_deps",
+    "../../include/perfetto/ext/base:base",
+  ]
+  sources = [ "main.cc" ]
+}
+
+perfetto_cc_proto_descriptor("gen_cc_config_descriptor") {
+  descriptor_name = "config.descriptor"
+  descriptor_target = "../../protos/perfetto/config:descriptor"
+}
+
+executable("trace_config_utils") {
+  testonly = true
+  deps = [
+    ":main",
+    "../../gn:default_deps",
+  ]
+}
+
+if (enable_perfetto_ui) {
+  wasm_lib("trace_config_utils_wasm") {
+    name = "trace_config_utils"
+    deps = [
+      ":main",
+      "../../gn:default_deps",
+    ]
+  }
+}
+
+perfetto_unittest_source_set("unittests") {
+  testonly = true
+  deps = [
+    ":pb_to_txt",
+    ":txt_to_pb",
+    "../../gn:default_deps",
+    "../../gn:gtest_and_gmock",
+    "../../protos/perfetto/config:cpp",
+    "../../protos/perfetto/config/ftrace:cpp",
+    "../../protos/perfetto/trace:cpp",
+    "../base",
+  ]
+  sources = [
+    "pb_to_txt_unittest.cc",
+    "txt_to_pb_unittest.cc",
+  ]
+}
diff --git a/src/trace_config_utils/main.cc b/src/trace_config_utils/main.cc
new file mode 100644
index 0000000..95e641e
--- /dev/null
+++ b/src/trace_config_utils/main.cc
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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
+ *
+ *      http://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 <stdio.h>
+#include <string.h>
+
+#include "perfetto/ext/base/file_utils.h"
+#include "src/trace_config_utils/pb_to_txt.h"
+#include "src/trace_config_utils/txt_to_pb.h"
+
+namespace {
+void PrintUsage(const char* argv0) {
+  printf(R"(
+Converts a TraceConfig from pbtxt to proto-encoded bytes and viceversa
+
+Usage: %s  txt_to_pb | pb_to_txt < in > out
+)",
+         argv0);
+}
+
+}  // namespace
+
+int main(int argc, char** argv) {
+  using namespace ::perfetto;
+
+  if (argc < 2) {
+    PrintUsage(argv[0]);
+    return 1;
+  }
+
+  const char* cmd = argv[1];
+  std::string in_data;
+  if (argc == 2) {
+    base::ReadFileStream(stdin, &in_data);
+  } else {
+    bool ok = base::ReadFile(argv[2], &in_data);
+    if (!ok) {
+      printf("Failed to open input file %s\n", argv[2]);
+      return 1;
+    }
+  }
+
+  if (strcmp(cmd, "txt_to_pb") == 0) {
+    base::StatusOr<std::vector<uint8_t>> res = TraceConfigTxtToPb(in_data);
+    if (!res.ok()) {
+      printf("%s\n", res.status().c_message());
+      return 1;
+    }
+    fwrite(res->data(), res->size(), 1, stdout);
+    return 0;
+  }
+
+  if (strcmp(cmd, "pb_to_txt") == 0) {
+    std::string txt = TraceConfigPbToTxt(in_data.data(), in_data.size());
+    printf("%s\n", txt.c_str());
+    return 0;
+  }
+
+  PrintUsage(argv[0]);
+  return 1;
+}
diff --git a/src/trace_config_utils/pb_to_txt.cc b/src/trace_config_utils/pb_to_txt.cc
new file mode 100644
index 0000000..da84f6c
--- /dev/null
+++ b/src/trace_config_utils/pb_to_txt.cc
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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
+ *
+ *      http://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 "src/trace_config_utils/pb_to_txt.h"
+#include "src/trace_config_utils/config.descriptor.h"
+#include "src/trace_processor/util/descriptors.h"
+#include "src/trace_processor/util/protozero_to_text.h"
+
+namespace perfetto {
+
+std::string TraceConfigPbToTxt(const void* data, size_t size) {
+  trace_processor::DescriptorPool pool;
+  pool.AddFromFileDescriptorSet(kConfigDescriptor.data(),
+                                kConfigDescriptor.size());
+
+  return trace_processor::protozero_to_text::ProtozeroToText(
+      pool, ".perfetto.protos.TraceConfig",
+      protozero::ConstBytes{static_cast<const uint8_t*>(data), size},
+      trace_processor::protozero_to_text::kIncludeNewLines);
+}
+
+}  // namespace perfetto
diff --git a/src/trace_config_utils/pb_to_txt.h b/src/trace_config_utils/pb_to_txt.h
new file mode 100644
index 0000000..7fec31a
--- /dev/null
+++ b/src/trace_config_utils/pb_to_txt.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * 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
+ *
+ *      http://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 SRC_TRACE_CONFIG_UTILS_PB_TO_TXT_H_
+#define SRC_TRACE_CONFIG_UTILS_PB_TO_TXT_H_
+
+#include <stddef.h>
+#include <string>
+
+namespace perfetto {
+
+std::string TraceConfigPbToTxt(const void* data, size_t size);
+
+}
+#endif  // SRC_TRACE_CONFIG_UTILS_PB_TO_TXT_H_
diff --git a/src/trace_config_utils/pb_to_txt_unittest.cc b/src/trace_config_utils/pb_to_txt_unittest.cc
new file mode 100644
index 0000000..ec7db25
--- /dev/null
+++ b/src/trace_config_utils/pb_to_txt_unittest.cc
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * 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
+ *
+ *      http://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 "src/trace_config_utils/pb_to_txt.h"
+
+#include "protos/perfetto/config/trace_config.gen.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace {
+
+using protos::gen::TraceConfig;
+
+TEST(PbToTxtTest, EmptyTraceConfig) {
+  TraceConfig tc;
+  std::vector<uint8_t> data = tc.SerializeAsArray();
+  std::string txt = TraceConfigPbToTxt(data.data(), data.size());
+  EXPECT_EQ(txt, "");
+}
+
+TEST(PbToTxtTest, ValidTraceConfig) {
+  TraceConfig tc;
+  tc.set_duration_ms(1234);
+  tc.set_trace_uuid_lsb(INT64_MAX);
+  tc.set_trace_uuid_msb(1234567890124LL);
+  auto* buf = tc.add_buffers();
+  buf->set_size_kb(4096);
+  buf->set_fill_policy(TraceConfig::BufferConfig::RING_BUFFER);
+  tc.set_write_into_file(true);
+
+  std::vector<uint8_t> data = tc.SerializeAsArray();
+  std::string txt = TraceConfigPbToTxt(data.data(), data.size());
+  EXPECT_EQ(txt, R"(buffers {
+  size_kb: 4096
+  fill_policy: RING_BUFFER
+}
+duration_ms: 1234
+write_into_file: true
+trace_uuid_msb: 1234567890124
+trace_uuid_lsb: 9223372036854775807)");
+}
+
+}  // namespace
+}  // namespace perfetto
diff --git a/src/perfetto_cmd/pbtxt_to_pb.cc b/src/trace_config_utils/txt_to_pb.cc
similarity index 91%
rename from src/perfetto_cmd/pbtxt_to_pb.cc
rename to src/trace_config_utils/txt_to_pb.cc
index 1f11a21..9c2e374 100644
--- a/src/perfetto_cmd/pbtxt_to_pb.cc
+++ b/src/trace_config_utils/txt_to_pb.cc
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#include "src/perfetto_cmd/pbtxt_to_pb.h"
+#include "src/trace_config_utils/txt_to_pb.h"
 
 #include <ctype.h>
 #include <limits>
@@ -32,9 +32,9 @@
 #include "perfetto/protozero/message.h"
 #include "perfetto/protozero/message_handle.h"
 #include "perfetto/protozero/scattered_heap_buffer.h"
-#include "src/perfetto_cmd/config.descriptor.h"
 
 #include "protos/perfetto/common/descriptor.gen.h"
+#include "src/trace_config_utils/config.descriptor.h"
 
 namespace perfetto {
 constexpr char kConfigProtoName[] = ".perfetto.protos.TraceConfig";
@@ -143,6 +143,58 @@
   std::set<std::string> seen_fields;
 };
 
+class ErrorReporter {
+ public:
+  ErrorReporter(std::string file_name, const char* config)
+      : file_name_(std::move(file_name)), config_(config) {}
+
+  void AddError(size_t row,
+                size_t column,
+                size_t length,
+                const std::string& message) {
+    // Protobuf uses 1-indexed for row and column. Although in some rare cases
+    // they can be 0 if it can't locate the error.
+    row = row > 0 ? row - 1 : 0;
+    column = column > 0 ? column - 1 : 0;
+    parsed_successfully_ = false;
+    std::string line = ExtractLine(row).ToStdString();
+    if (!line.empty() && line[line.length() - 1] == '\n') {
+      line.erase(line.length() - 1);
+    }
+
+    std::string guide(column + length, ' ');
+    for (size_t i = column; i < column + length; i++) {
+      guide[i] = i == column ? '^' : '~';
+    }
+    error_ += file_name_ + ":" + std::to_string(row+1) + ":" +
+              std::to_string(column + 1) + " error: " + message + "\n";
+    error_ += line + "\n";
+    error_ += guide + "\n";
+  }
+
+  bool success() const { return parsed_successfully_; }
+  const std::string& error() const { return error_; }
+
+ private:
+  base::StringView ExtractLine(size_t line) {
+    const char* start = config_;
+    const char* end = config_;
+
+    for (size_t i = 0; i < line + 1; i++) {
+      start = end;
+      char c;
+      while ((c = *end++) && c != '\n')
+        ;
+    }
+    return base::StringView(start, static_cast<size_t>(end - start));
+  }
+
+  bool parsed_successfully_ = true;
+  std::string file_name_;
+  std::string error_;
+  const char* config_;
+};
+
 class ParserDelegate {
  public:
   ParserDelegate(
@@ -704,11 +756,9 @@
 
 }  // namespace
 
-ErrorReporter::ErrorReporter() = default;
-ErrorReporter::~ErrorReporter() = default;
-
-std::vector<uint8_t> PbtxtToPb(const std::string& input,
-                               ErrorReporter* reporter) {
+perfetto::base::StatusOr<std::vector<uint8_t>> TraceConfigTxtToPb(
+    const std::string& input,
+    const std::string& file_name) {
   std::map<std::string, const DescriptorProto*> name_to_descriptor;
   std::map<std::string, const EnumDescriptorProto*> name_to_enum;
   FileDescriptorSet file_descriptor_set;
@@ -736,10 +786,13 @@
   PERFETTO_CHECK(descriptor);
 
   protozero::HeapBuffered<protozero::Message> message;
-  ParserDelegate delegate(descriptor, message.get(), reporter,
+  ErrorReporter reporter(file_name, input.c_str());
+  ParserDelegate delegate(descriptor, message.get(), &reporter,
                           std::move(name_to_descriptor),
                           std::move(name_to_enum));
   Parse(input, &delegate);
+  if (!reporter.success())
+    return base::ErrStatus("%s", reporter.error().c_str());
   return message.SerializeAsArray();
 }
 
diff --git a/src/trace_config_utils/txt_to_pb.h b/src/trace_config_utils/txt_to_pb.h
new file mode 100644
index 0000000..3cf71ea
--- /dev/null
+++ b/src/trace_config_utils/txt_to_pb.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * 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
+ *
+ *      http://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 SRC_TRACE_CONFIG_UTILS_TXT_TO_PB_H_
+#define SRC_TRACE_CONFIG_UTILS_TXT_TO_PB_H_
+
+#include "perfetto/ext/base/status_or.h"
+
+#include <stdint.h>
+
+#include <string>
+#include <vector>
+
+namespace perfetto {
+
+base::StatusOr<std::vector<uint8_t>> TraceConfigTxtToPb(
+    const std::string& input,
+    const std::string& file_name = "-");
+
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_CONFIG_UTILS_TXT_TO_PB_H_
diff --git a/src/perfetto_cmd/pbtxt_to_pb_unittest.cc b/src/trace_config_utils/txt_to_pb_unittest.cc
similarity index 65%
rename from src/perfetto_cmd/pbtxt_to_pb_unittest.cc
rename to src/trace_config_utils/txt_to_pb_unittest.cc
index 1b2f1d0..0b14feb 100644
--- a/src/perfetto_cmd/pbtxt_to_pb_unittest.cc
+++ b/src/trace_config_utils/txt_to_pb_unittest.cc
@@ -14,60 +14,44 @@
  * limitations under the License.
  */
 
-#include "src/perfetto_cmd/pbtxt_to_pb.h"
+#include "src/trace_config_utils/txt_to_pb.h"
 
 #include <memory>
 #include <string>
 
 #include "test/gtest_and_gmock.h"
 
-#include "perfetto/tracing/core/data_source_config.h"
-#include "perfetto/tracing/core/trace_config.h"
-
+#include "protos/perfetto/config/data_source_config.gen.h"
 #include "protos/perfetto/config/ftrace/ftrace_config.gen.h"
 #include "protos/perfetto/config/test_config.gen.h"
+#include "protos/perfetto/config/trace_config.gen.h"
 
 namespace perfetto {
 namespace {
 
 using ::testing::Contains;
 using ::testing::ElementsAre;
+using ::testing::HasSubstr;
 using ::testing::StrictMock;
-
-class MockErrorReporter : public ErrorReporter {
- public:
-  MockErrorReporter() {}
-  ~MockErrorReporter() override = default;
-  MOCK_METHOD(void,
-              AddError,
-              (size_t line,
-               size_t column_start,
-               size_t column_end,
-               const std::string& message),
-              (override));
-};
+using TraceConfig = ::perfetto::protos::gen::TraceConfig;
 
 TraceConfig ToProto(const std::string& input) {
-  StrictMock<MockErrorReporter> reporter;
-  std::vector<uint8_t> output = PbtxtToPb(input, &reporter);
-  EXPECT_FALSE(output.empty());
+  base::StatusOr<std::vector<uint8_t>> output = TraceConfigTxtToPb(input);
+  EXPECT_TRUE(output.ok());
+  EXPECT_FALSE(output->empty());
   TraceConfig config;
-  config.ParseFromArray(output.data(), output.size());
+  config.ParseFromArray(output->data(), output->size());
   return config;
 }
 
-void ToErrors(const std::string& input, MockErrorReporter* reporter) {
-  std::vector<uint8_t> output = PbtxtToPb(input, reporter);
-}
-
-TEST(PbtxtToPb, OneField) {
+TEST(TxtToPbTest, OneField) {
   TraceConfig config = ToProto(R"(
     duration_ms: 1234
   )");
   EXPECT_EQ(config.duration_ms(), 1234u);
 }
 
-TEST(PbtxtToPb, TwoFields) {
+TEST(TxtToPbTest, TwoFields) {
   TraceConfig config = ToProto(R"(
     duration_ms: 1234
     file_write_period_ms: 5678
@@ -76,14 +60,14 @@
   EXPECT_EQ(config.file_write_period_ms(), 5678u);
 }
 
-TEST(PbtxtToPb, Enum) {
+TEST(TxtToPbTest, Enum) {
   TraceConfig config = ToProto(R"(
 compression_type: COMPRESSION_TYPE_DEFLATE
 )");
   EXPECT_EQ(config.compression_type(), 1);
 }
 
-TEST(PbtxtToPb, LastCharacters) {
+TEST(TxtToPbTest, LastCharacters) {
   EXPECT_EQ(ToProto(R"(
 duration_ms: 123;)")
                 .duration_ms(),
@@ -121,7 +105,7 @@
             1);
 }
 
-TEST(PbtxtToPb, Semicolons) {
+TEST(TxtToPbTest, Semicolons) {
   TraceConfig config = ToProto(R"(
     duration_ms: 1234;
     file_write_period_ms: 5678;
@@ -130,7 +114,7 @@
   EXPECT_EQ(config.file_write_period_ms(), 5678u);
 }
 
-TEST(PbtxtToPb, NestedMessage) {
+TEST(TxtToPbTest, NestedMessage) {
   TraceConfig config = ToProto(R"(
     buffers: {
       size_kb: 123
@@ -140,7 +124,7 @@
   EXPECT_EQ(config.buffers()[0].size_kb(), 123u);
 }
 
-TEST(PbtxtToPb, SplitNested) {
+TEST(TxtToPbTest, SplitNested) {
   TraceConfig config = ToProto(R"(
     buffers: {
       size_kb: 1
@@ -156,7 +140,7 @@
   EXPECT_EQ(config.duration_ms(), 1000u);
 }
 
-TEST(PbtxtToPb, MultipleNestedMessage) {
+TEST(TxtToPbTest, MultipleNestedMessage) {
   TraceConfig config = ToProto(R"(
     buffers: {
       size_kb: 1
@@ -170,7 +154,7 @@
   EXPECT_EQ(config.buffers()[1].size_kb(), 2u);
 }
 
-TEST(PbtxtToPb, NestedMessageCrossFile) {
+TEST(TxtToPbTest, NestedMessageCrossFile) {
   TraceConfig config = ToProto(R"(
 data_sources {
   config {
@@ -186,7 +170,7 @@
   ASSERT_EQ(ftrace_config.drain_period_ms(), 42u);
 }
 
-TEST(PbtxtToPb, Booleans) {
+TEST(TxtToPbTest, Booleans) {
   TraceConfig config = ToProto(R"(
     write_into_file: false; deferred_start: true;
   )");
@@ -194,7 +178,7 @@
   EXPECT_EQ(config.deferred_start(), true);
 }
 
-TEST(PbtxtToPb, Comments) {
+TEST(TxtToPbTest, Comments) {
   TraceConfig config = ToProto(R"(
     write_into_file: false # deferred_start: true;
     buffers# 1
@@ -218,7 +202,7 @@
   EXPECT_EQ(config.deferred_start(), false);
 }
 
-TEST(PbtxtToPb, Enums) {
+TEST(TxtToPbTest, Enums) {
   TraceConfig config = ToProto(R"(
     buffers: {
       fill_policy: RING_BUFFER
@@ -228,7 +212,7 @@
   EXPECT_EQ(config.buffers()[0].fill_policy(), kRingBuffer);
 }
 
-TEST(PbtxtToPb, AllFieldTypes) {
+TEST(TxtToPbTest, AllFieldTypes) {
   TraceConfig config = ToProto(R"(
 data_sources {
   config {
@@ -271,7 +255,7 @@
   ASSERT_EQ(fields.field_bytes(), "14");
 }
 
-TEST(PbtxtToPb, LeadingDots) {
+TEST(TxtToPbTest, LeadingDots) {
   TraceConfig config = ToProto(R"(
 data_sources {
   config {
@@ -290,7 +274,7 @@
   ASSERT_FLOAT_EQ(fields.field_float(), .2f);
 }
 
-TEST(PbtxtToPb, NegativeNumbers) {
+TEST(TxtToPbTest, NegativeNumbers) {
   TraceConfig config = ToProto(R"(
 data_sources {
   config {
@@ -325,17 +309,17 @@
   ASSERT_EQ(fields.field_sint32(), -10);
 }
 
-TEST(PbtxtToPb, EofEndsNumeric) {
+TEST(TxtToPbTest, EofEndsNumeric) {
   TraceConfig config = ToProto(R"(duration_ms: 1234)");
   EXPECT_EQ(config.duration_ms(), 1234u);
 }
 
-TEST(PbtxtToPb, EofEndsIdentifier) {
+TEST(TxtToPbTest, EofEndsIdentifier) {
   TraceConfig config = ToProto(R"(enable_extra_guardrails: true)");
   EXPECT_EQ(config.enable_extra_guardrails(), true);
 }
 
-TEST(PbtxtToPb, ExampleConfig) {
+TEST(TxtToPbTest, ExampleConfig) {
   TraceConfig config = ToProto(R"(
 buffers {
   size_kb: 100024
@@ -394,7 +378,7 @@
   EXPECT_EQ(config.producers()[0].producer_name(), "perfetto.traced_probes");
 }
 
-TEST(PbtxtToPb, Strings) {
+TEST(TxtToPbTest, Strings) {
   TraceConfig config = ToProto(R"(
 data_sources {
   config {
@@ -423,155 +407,139 @@
   EXPECT_THAT(events, Contains("\0127_\03422.\177"));
 }
 
-TEST(PbtxtToPb, UnknownField) {
-  MockErrorReporter reporter;
-  EXPECT_CALL(reporter,
-              AddError(2, 5, 11,
-                       "No field named \"not_a_label\" in proto TraceConfig"));
-  ToErrors(R"(
+TEST(TxtToPbTest, UnknownField) {
+  auto res = TraceConfigTxtToPb(R"(
     not_a_label: false
-  )",
-           &reporter);
+  )");
+  EXPECT_FALSE(res.ok());
+  EXPECT_THAT(res.status().message(),
+              HasSubstr("No field named \"not_a_label\" in proto TraceConfig"));
 }
 
-TEST(PbtxtToPb, UnknownNestedField) {
-  MockErrorReporter reporter;
-  EXPECT_CALL(
-      reporter,
-      AddError(
-          4, 5, 16,
-          "No field named \"not_a_field_name\" in proto DataSourceConfig"));
-  ToErrors(R"(
+TEST(TxtToPbTest, UnknownNestedField) {
+  auto res = TraceConfigTxtToPb(R"(
 data_sources {
   config {
     not_a_field_name {
     }
   }
 }
-  )",
-           &reporter);
+  )");
+  EXPECT_FALSE(res.ok());
+  EXPECT_THAT(
+      res.status().message(),
+      HasSubstr(
+          "No field named \"not_a_field_name\" in proto DataSourceConfig"));
 }
 
-TEST(PbtxtToPb, BadBoolean) {
-  MockErrorReporter reporter;
-  EXPECT_CALL(reporter, AddError(2, 22, 3,
-                                 "Expected 'true' or 'false' for boolean field "
-                                 "write_into_file in proto TraceConfig instead "
-                                 "saw 'foo'"));
-  ToErrors(R"(
+TEST(TxtToPbTest, BadBoolean) {
+  auto res = TraceConfigTxtToPb(R"(
     write_into_file: foo;
-  )",
-           &reporter);
+  )");
+  EXPECT_FALSE(res.ok());
+  EXPECT_THAT(res.status().message(),
+              HasSubstr("Expected 'true' or 'false' for boolean field "
+                        "write_into_file in proto TraceConfig instead "
+                        "saw 'foo'"));
 }
 
-TEST(PbtxtToPb, MissingBoolean) {
-  MockErrorReporter reporter;
-  EXPECT_CALL(reporter, AddError(3, 3, 0, "Unexpected end of input"));
-  ToErrors(R"(
+TEST(TxtToPbTest, MissingBoolean) {
+  auto res = TraceConfigTxtToPb(R"(
     write_into_file:
-  )",
-           &reporter);
+  )");
+  EXPECT_FALSE(res.ok());
+  EXPECT_THAT(res.status().message(), HasSubstr("Unexpected end of input"));
 }
 
-TEST(PbtxtToPb, RootProtoMustNotEndWithBrace) {
-  MockErrorReporter reporter;
-  EXPECT_CALL(reporter, AddError(2, 5, 0, "Unmatched closing brace"));
-  ToErrors(R"(
-    }
-  )",
-           &reporter);
+TEST(TxtToPbTest, RootProtoMustNotEndWithBrace) {
+  auto res = TraceConfigTxtToPb("  }");
+  EXPECT_FALSE(res.ok());
+  EXPECT_THAT(res.status().message(), HasSubstr("Unmatched closing brace"));
 }
 
-TEST(PbtxtToPb, SawNonRepeatedFieldTwice) {
-  MockErrorReporter reporter;
-  EXPECT_CALL(
-      reporter,
-      AddError(3, 5, 15,
-               "Saw non-repeating field 'write_into_file' more than once"));
-  ToErrors(R"(
+TEST(TxtToPbTest, SawNonRepeatedFieldTwice) {
+  auto res = TraceConfigTxtToPb(R"(
     write_into_file: true;
     write_into_file: true;
-  )",
-           &reporter);
+  )");
+  EXPECT_FALSE(res.ok());
+  EXPECT_THAT(
+      res.status().message(),
+      HasSubstr("Saw non-repeating field 'write_into_file' more than once"));
 }
 
-TEST(PbtxtToPb, WrongTypeBoolean) {
-  MockErrorReporter reporter;
-  EXPECT_CALL(reporter,
-              AddError(2, 18, 4,
-                       "Expected value of type uint32 for field duration_ms in "
-                       "proto TraceConfig instead saw 'true'"));
-  ToErrors(R"(
+TEST(TxtToPbTest, WrongTypeBoolean) {
+  auto res = TraceConfigTxtToPb(R"(
     duration_ms: true;
-  )",
-           &reporter);
+  )");
+  EXPECT_FALSE(res.ok());
+  EXPECT_THAT(
+      res.status().message(),
+      HasSubstr("Expected value of type uint32 for field duration_ms in "
+                "proto TraceConfig instead saw 'true'"));
 }
 
-TEST(PbtxtToPb, WrongTypeNumber) {
-  MockErrorReporter reporter;
-  EXPECT_CALL(reporter,
-              AddError(2, 14, 3,
-                       "Expected value of type message for field buffers in "
-                       "proto TraceConfig instead saw '100'"));
-  ToErrors(R"(
+TEST(TxtToPbTest, WrongTypeNumber) {
+  auto res = TraceConfigTxtToPb(R"(
     buffers: 100;
-  )",
-           &reporter);
+  )");
+  EXPECT_FALSE(res.ok());
+  EXPECT_THAT(res.status().message(),
+              HasSubstr("Expected value of type message for field buffers in "
+                        "proto TraceConfig instead saw '100'"));
 }
 
-TEST(PbtxtToPb, NestedMessageDidNotTerminate) {
-  MockErrorReporter reporter;
-  EXPECT_CALL(reporter, AddError(2, 15, 0, "Nested message not closed"));
-  ToErrors(R"(
-    buffers: {)",
-           &reporter);
+TEST(TxtToPbTest, NestedMessageDidNotTerminate) {
+  auto res = TraceConfigTxtToPb(R"(
+    buffers: {
+  )");
+  EXPECT_FALSE(res.ok());
+  EXPECT_THAT(res.status().message(), HasSubstr("Nested message not closed"));
 }
 
-TEST(PbtxtToPb, BadEscape) {
-  MockErrorReporter reporter;
-  EXPECT_CALL(reporter, AddError(5, 23, 2,
-                                 "Unknown string escape in ftrace_events in "
-                                 "proto FtraceConfig: '\\p'"));
-  ToErrors(R"(
-data_sources {
-  config {
-    ftrace_config {
-      ftrace_events: "\p"
+TEST(TxtToPbTest, BadEscape) {
+  auto res = TraceConfigTxtToPb(R"(
+  data_sources {
+    config {
+      ftrace_config {
+        ftrace_events: "\p"
+      }
     }
-  }
-})",
-           &reporter);
+  })");
+  EXPECT_FALSE(res.ok());
+  EXPECT_THAT(res.status().message(),
+              HasSubstr("Unknown string escape in ftrace_events in "
+                        "proto FtraceConfig: '\\p'"));
 }
 
-TEST(PbtxtToPb, BadEnumValue) {
-  MockErrorReporter reporter;
-  EXPECT_CALL(reporter, AddError(1, 18, 3,
-                                 "Unexpected value 'FOO' for enum field "
-                                 "compression_type in proto TraceConfig"));
-  ToErrors(R"(compression_type: FOO)", &reporter);
+TEST(TxtToPbTest, BadEnumValue) {
+  auto res = TraceConfigTxtToPb("compression_type: FOO");
+  EXPECT_FALSE(res.ok());
+  EXPECT_THAT(res.status().message(),
+              HasSubstr("Unexpected value 'FOO' for enum field "
+                        "compression_type in proto TraceConfig"));
 }
 
-TEST(PbtxtToPb, UnexpectedBracket) {
-  MockErrorReporter reporter;
-  EXPECT_CALL(reporter, AddError(1, 0, 0, "Unexpected character '{'"));
-  ToErrors(R"({)", &reporter);
+TEST(TxtToPbTest, UnexpectedBracket) {
+  auto res = TraceConfigTxtToPb("{");
+  EXPECT_FALSE(res.ok());
+  EXPECT_THAT(res.status().message(), HasSubstr("Unexpected character '{'"));
 }
 
-TEST(PbtxtToPb, UnknownNested) {
-  MockErrorReporter reporter;
-  EXPECT_CALL(reporter, AddError(1, 0, 3,
-                                 "No field named \"foo\" in "
-                                 "proto TraceConfig"));
-  ToErrors(R"(foo {}; bar: 42)", &reporter);
+TEST(TxtToPbTest, UnknownNested) {
+  auto res = TraceConfigTxtToPb("foo {}; bar: 42");
+  EXPECT_FALSE(res.ok());
+  EXPECT_THAT(res.status().message(), HasSubstr("No field named \"foo\" in "
+                                                "proto TraceConfig"));
 }
 
 // TODO(hjd): Add these tests.
-// TEST(PbtxtToPb, WrongTypeString)
-// TEST(PbtxtToPb, OverflowOnIntegers)
-// TEST(PbtxtToPb, NegativeNumbersForUnsignedInt)
-// TEST(PbtxtToPb, UnterminatedString) {
-// TEST(PbtxtToPb, NumberIsEof)
-// TEST(PbtxtToPb, OneOf)
+// TEST(TxtToPbTest, WrongTypeString)
+// TEST(TxtToPbTest, OverflowOnIntegers)
+// TEST(TxtToPbTest, NegativeNumbersForUnsignedInt)
+// TEST(TxtToPbTest, UnterminatedString) {
+// TEST(TxtToPbTest, NumberIsEof)
+// TEST(TxtToPbTest, OneOf)
 
 }  // namespace
 }  // namespace perfetto
diff --git a/ui/BUILD.gn b/ui/BUILD.gn
index 8c6d41a..2b47c79 100644
--- a/ui/BUILD.gn
+++ b/ui/BUILD.gn
@@ -22,6 +22,7 @@
 group("ui") {
   deps = [
     ":ui_build($host_toolchain)",
+    "../src/trace_config_utils:trace_config_utils.wasm($wasm_toolchain)",
     "../src/trace_processor:trace_processor.wasm($wasm_toolchain)",
     "../src/traceconv:traceconv.wasm($wasm_toolchain)",
   ]
diff --git a/ui/build.js b/ui/build.js
index 7d3c87d..0bf73f5 100644
--- a/ui/build.js
+++ b/ui/build.js
@@ -86,7 +86,7 @@
   startHttpServer: false,
   httpServerListenHost: '127.0.0.1',
   httpServerListenPort: 10000,
-  wasmModules: ['trace_processor', 'traceconv'],
+  wasmModules: ['trace_processor', 'traceconv', 'trace_config_utils'],
   crossOriginIsolation: false,
   testFilter: '',
   noOverrideGnArgs: false,