Trace Redaction - Create foundation for trace redaction

In order to set-up the foundation for migrating the trace redaction work
into the Perfetto repo, the following three work items had to be
executed together.

1. Set-up directory and build files for future work
2. Define key data types (context + primitive)
3. Define execution pattern

To demonstrate how everything works, this implements primitives
that find a target package in the package list and removes all
other packages.

Bug: 318576092
Change-Id: I5fca1102abc6bb02e433453817c56270023368e2
diff --git a/Android.bp b/Android.bp
index 73487c9..02b3c6b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -2387,6 +2387,8 @@
         ":perfetto_src_trace_processor_util_stdlib",
         ":perfetto_src_trace_processor_util_util",
         ":perfetto_src_trace_processor_util_zip_reader",
+        ":perfetto_src_trace_redaction_integrationtests",
+        ":perfetto_src_trace_redaction_trace_redaction",
         ":perfetto_src_traced_probes_android_game_intervention_list_android_game_intervention_list",
         ":perfetto_src_traced_probes_android_log_android_log",
         ":perfetto_src_traced_probes_android_system_property_android_system_property",
@@ -12428,6 +12430,34 @@
     ],
 }
 
+// GN: //src/trace_redaction:integrationtests
+filegroup {
+    name: "perfetto_src_trace_redaction_integrationtests",
+    srcs: [
+        "src/trace_redaction/trace_redactor_integrationtest.cc",
+    ],
+}
+
+// GN: //src/trace_redaction:trace_redaction
+filegroup {
+    name: "perfetto_src_trace_redaction_trace_redaction",
+    srcs: [
+        "src/trace_redaction/find_package_uid.cc",
+        "src/trace_redaction/prune_package_list.cc",
+        "src/trace_redaction/trace_redaction_framework.cc",
+        "src/trace_redaction/trace_redactor.cc",
+    ],
+}
+
+// GN: //src/trace_redaction:unittests
+filegroup {
+    name: "perfetto_src_trace_redaction_unittests",
+    srcs: [
+        "src/trace_redaction/find_package_uid_unittest.cc",
+        "src/trace_redaction/prune_package_list_unittest.cc",
+    ],
+}
+
 // GN: //src/traceconv:gen_cc_trace_descriptor
 genrule {
     name: "perfetto_src_traceconv_gen_cc_trace_descriptor",
@@ -13901,6 +13931,8 @@
         ":perfetto_src_trace_processor_util_unittests",
         ":perfetto_src_trace_processor_util_util",
         ":perfetto_src_trace_processor_util_zip_reader",
+        ":perfetto_src_trace_redaction_trace_redaction",
+        ":perfetto_src_trace_redaction_unittests",
         ":perfetto_src_traceconv_lib",
         ":perfetto_src_traceconv_pprofbuilder",
         ":perfetto_src_traceconv_unittests",
diff --git a/gn/perfetto_integrationtests.gni b/gn/perfetto_integrationtests.gni
index 3e65e20..969c977 100644
--- a/gn/perfetto_integrationtests.gni
+++ b/gn/perfetto_integrationtests.gni
@@ -50,6 +50,8 @@
       [ "src/trace_processor:integrationtests" ]
 }
 
+perfetto_integrationtests_targets += [ "src/trace_redaction:integrationtests" ]
+
 if (enable_perfetto_traced_relay) {
   perfetto_integrationtests_targets += [ "src/traced_relay:integrationtests" ]
 }
diff --git a/gn/perfetto_unittests.gni b/gn/perfetto_unittests.gni
index 2769a63..f1d46ba 100644
--- a/gn/perfetto_unittests.gni
+++ b/gn/perfetto_unittests.gni
@@ -85,3 +85,5 @@
 if (enable_perfetto_traced_relay) {
   perfetto_unittests_targets += [ "src/traced_relay:unittests" ]
 }
+
+perfetto_unittests_targets += [ "src/trace_redaction:unittests" ]
diff --git a/src/trace_redaction/BUILD.gn b/src/trace_redaction/BUILD.gn
new file mode 100644
index 0000000..4536331
--- /dev/null
+++ b/src/trace_redaction/BUILD.gn
@@ -0,0 +1,72 @@
+# 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/test.gni")
+
+source_set("trace_redaction") {
+  sources = [
+    "find_package_uid.cc",
+    "find_package_uid.h",
+    "prune_package_list.cc",
+    "prune_package_list.h",
+    "trace_redaction_framework.cc",
+    "trace_redaction_framework.h",
+    "trace_redactor.cc",
+    "trace_redactor.h",
+  ]
+  deps = [
+    "../../gn:default_deps",
+    "../../include/perfetto/base",
+    "../../include/perfetto/ext/base",
+    "../../include/perfetto/protozero:protozero",
+    "../../include/perfetto/trace_processor:storage",
+    "../../protos/perfetto/trace:non_minimal_cpp",
+    "../../protos/perfetto/trace:non_minimal_zero",
+    "../../protos/perfetto/trace/android:cpp",
+    "../../protos/perfetto/trace/android:zero",
+    "../trace_processor:storage_minimal",
+  ]
+}
+
+source_set("integrationtests") {
+  testonly = true
+  sources = [ "trace_redactor_integrationtest.cc" ]
+  deps = [
+    ":trace_redaction",
+    "../../gn:default_deps",
+    "../../gn:gtest_and_gmock",
+    "../../include/perfetto/ext/base",
+    "../../protos/perfetto/trace:non_minimal_zero",
+    "../../protos/perfetto/trace/android:zero",
+    "../base:test_support",
+  ]
+}
+
+perfetto_unittest_source_set("unittests") {
+  testonly = true
+  sources = [
+    "find_package_uid_unittest.cc",
+    "prune_package_list_unittest.cc",
+  ]
+  deps = [
+    ":trace_redaction",
+    "../../gn:default_deps",
+    "../../gn:gtest_and_gmock",
+    "../../protos/perfetto/trace:non_minimal_cpp",
+    "../../protos/perfetto/trace/android:cpp",
+    "../../protos/perfetto/trace/ps:cpp",
+    "../../protos/perfetto/trace/ps:zero",
+    "../base:test_support",
+  ]
+}
diff --git a/src/trace_redaction/find_package_uid.cc b/src/trace_redaction/find_package_uid.cc
new file mode 100644
index 0000000..d069fe2
--- /dev/null
+++ b/src/trace_redaction/find_package_uid.cc
@@ -0,0 +1,67 @@
+/*
+ * 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_redaction/find_package_uid.h"
+
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "protos/perfetto/trace/android/packages_list.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
+namespace perfetto::trace_redaction {
+namespace {
+
+using PackagesList_PackageInfo = protos::pbzero::PackagesList_PackageInfo;
+using PackagesList = protos::pbzero::PackagesList;
+
+}  // namespace
+
+FindPackageUid::FindPackageUid() = default;
+
+FindPackageUid::~FindPackageUid() = default;
+
+base::StatusOr<CollectPrimitive::ContinueCollection> FindPackageUid::Collect(
+    const protos::pbzero::TracePacket_Decoder& packet,
+    Context* context) const {
+  if (context->package_name.empty()) {
+    return base::ErrStatus("FindPackageUid: missing package name.");
+  }
+
+  // Skip package and move onto the next packet.
+  if (!packet.has_packages_list()) {
+    return ContinueCollection::kNextPacket;
+  }
+
+  const PackagesList::Decoder pkg_list_decoder(packet.packages_list());
+
+  for (auto pkg_it = pkg_list_decoder.packages(); pkg_it; ++pkg_it) {
+    PackagesList_PackageInfo::Decoder pkg_info(*pkg_it);
+
+    if (pkg_info.has_name() && pkg_info.has_uid()) {
+      // Package names should be lowercase, but this check is meant to be more
+      // forgiving.
+      if (base::StringView(context->package_name)
+              .CaseInsensitiveEq(pkg_info.name())) {
+        context->package_uid = NormalizeUid(pkg_info.uid());
+        return ContinueCollection::kRetire;
+      }
+    }
+  }
+
+  return ContinueCollection::kNextPacket;
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/find_package_uid.h b/src/trace_redaction/find_package_uid.h
new file mode 100644
index 0000000..4ae34d4
--- /dev/null
+++ b/src/trace_redaction/find_package_uid.h
@@ -0,0 +1,42 @@
+/*
+ * 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_REDACTION_FIND_PACKAGE_UID_H_
+#define SRC_TRACE_REDACTION_FIND_PACKAGE_UID_H_
+
+#include "perfetto/ext/base/status_or.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+namespace perfetto::trace_redaction {
+
+// Writes the uid for the package matching `Context.package_name`. Returns
+// `kStop` with a package is found, otherwrise `kContinue`. If a package is not
+// found, `Context.package_uid` will remain unset and a later primitive will
+// need to report the failure.
+class FindPackageUid final : public CollectPrimitive {
+ public:
+  FindPackageUid();
+  ~FindPackageUid() override;
+
+  base::StatusOr<ContinueCollection> Collect(
+      const protos::pbzero::TracePacket::Decoder& packet,
+      Context* context) const override;
+};
+
+}  // namespace perfetto::trace_redaction
+
+#endif  // SRC_TRACE_REDACTION_FIND_PACKAGE_UID_H_
diff --git a/src/trace_redaction/find_package_uid_unittest.cc b/src/trace_redaction/find_package_uid_unittest.cc
new file mode 100644
index 0000000..44f0a45
--- /dev/null
+++ b/src/trace_redaction/find_package_uid_unittest.cc
@@ -0,0 +1,239 @@
+
+/*
+ * 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 "find_package_uid.h"
+
+#include <cstdint>
+#include <string>
+
+#include "protos/perfetto/trace/android/packages_list.gen.h"
+#include "protos/perfetto/trace/ps/process_tree.gen.h"
+#include "protos/perfetto/trace/ps/process_tree.pbzero.h"
+#include "protos/perfetto/trace/trace_packet.gen.h"
+#include "src/base/test/status_matchers.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto::trace_redaction {
+
+namespace {
+
+// packet {
+//   trusted_uid: 9999
+//   trusted_packet_sequence_id: 2
+//   previous_packet_dropped: true
+//   packages_list {
+//     packages {
+//       name: "com.shannon.qualifiednetworksservice"
+//       uid: 10201
+//       debuggable: false
+//       profileable_from_shell: false
+//       version_code: 131
+//     }
+//     packages {
+//       name: "com.google.android.uvexposurereporter"
+//       uid: 10205
+//       debuggable: false
+//       profileable_from_shell: false
+//       version_code: 34
+//     }
+//     packages {
+//       name: "com.android.internal.display.cutout.emulation.noCutout"
+//       uid: 10007
+//       debuggable: false
+//       profileable_from_shell: false
+//       version_code: 1
+//     }
+//     packages {
+//       name: "com.google.android.networkstack.tethering"
+//       uid: 1073
+//       debuggable: false
+//       profileable_from_shell: false
+//       version_code: 34
+//     }
+//     packages {
+//       name: "com.amazon.mShop.android.shopping"
+//       uid: 10303
+//       debuggable: false
+//       profileable_from_shell: false
+//       version_code: 1241261011
+//     }
+//   }
+//   trusted_pid: 1085
+//   first_packet_on_sequence: true
+// }
+//
+// packet {
+//   process_tree {
+//     processes {
+//       pid: 23022
+//       ppid: 1
+//       cmdline: "/vendor/bin/hw/wpa_supplicant"
+//       cmdline: "-O/data/vendor/wifi/wpa/sockets"
+//       cmdline: "-dd"
+//       cmdline: "-g@android:wpa_wlan0"
+//       uid: 1010
+//     }
+//     threads {
+//       tid: 6382
+//       tgid: 18176
+//     }
+//     threads {
+//      tid: 18419
+//       tgid: 18176
+//     }
+//     threads {
+//       tid: 18434
+//       tgid: 18176
+//     }
+//     collection_end_timestamp: 333724398314653
+//   }
+//   trusted_uid: 9999
+//   timestamp: 333724396714922
+//   trusted_packet_sequence_id: 3
+//   trusted_pid: 1085
+// }
+
+void AddPackage(const std::string& name,
+                uint64_t uid,
+                int version_code,
+                protos::gen::PackagesList* list) {
+  auto* pkg = list->add_packages();
+  pkg->set_name(name);
+  pkg->set_uid(uid);
+  pkg->set_version_code(version_code);
+}
+
+void AddProcess(protos::gen::ProcessTree* process_tree) {
+  auto* p0 = process_tree->add_processes();
+  p0->set_pid(23022);
+  p0->set_ppid(1);
+  p0->add_cmdline("/vendor/bin/hw/wpa_supplicant");
+  p0->add_cmdline("-O/data/vendor/wifi/wpa/sockets");
+  p0->add_cmdline("-dd");
+  p0->add_cmdline("-g@android:wpa_wlan0");
+  p0->set_uid(1010);
+}
+
+void AddThread(int32_t tid,
+               int32_t ttid,
+               protos::gen::ProcessTree* process_tree) {
+  auto* thread = process_tree->add_threads();
+  thread->set_tid(tid);
+  thread->set_tgid(ttid);
+}
+
+std::string CreatePackageListPacket() {
+  protos::gen::TracePacket packet;
+  packet.set_trusted_uid(9999);
+  packet.set_trusted_packet_sequence_id(2);
+  packet.set_previous_packet_dropped(true);
+  packet.set_trusted_pid(1085);
+  packet.set_first_packet_on_sequence(true);
+
+  auto* package_list = packet.mutable_packages_list();
+
+  AddPackage("com.shannon.qualifiednetworksservice", 10201, 131, package_list);
+  AddPackage("com.google.android.uvexposurereporter", 10205, 34, package_list);
+  AddPackage("com.android.internal.display.cutout.emulation.noCutout", 10007, 1,
+             package_list);
+  AddPackage("com.google.android.networkstack.tethering", 1073, 34,
+             package_list);
+  AddPackage("com.amazon.mShop.android.shopping", 10303, 1241261011,
+             package_list);
+
+  return packet.SerializeAsString();
+}
+
+std::string CreateProcessTreePacket() {
+  protos::gen::TracePacket packet;
+  packet.set_trusted_uid(9999);
+  packet.set_timestamp(333724396714922);
+  packet.set_trusted_packet_sequence_id(3);
+  packet.set_trusted_pid(1085);
+
+  auto* tree = packet.mutable_process_tree();
+  tree->set_collection_end_timestamp(333724398314653);
+
+  AddProcess(tree);
+  AddThread(6382, 18176, tree);
+  AddThread(18419, 18176, tree);
+  AddThread(18434, 18176, tree);
+
+  return packet.SerializeAsString();
+}
+}  // namespace
+
+TEST(FindPackageUidTest, FindsUidInPackageList) {
+  const auto packet = CreatePackageListPacket();
+
+  Context context;
+  context.package_name = "com.google.android.uvexposurereporter";
+
+  const FindPackageUid find;
+
+  const auto decoder = protos::pbzero::TracePacket::Decoder(packet);
+
+  ASSERT_OK_AND_ASSIGN(auto status, find.Collect(decoder, &context));
+  ASSERT_EQ(status, CollectPrimitive::ContinueCollection::kRetire);
+
+  ASSERT_TRUE(context.package_uid.has_value());
+  ASSERT_EQ(NormalizeUid(context.package_uid.value()), NormalizeUid(10205));
+}
+
+TEST(FindPackageUidTest, ContinuesOverNonPackageList) {
+  const auto packet = CreateProcessTreePacket();
+
+  Context context;
+  context.package_name = "com.google.android.uvexposurereporter";
+
+  const FindPackageUid find;
+
+  const auto decoder = protos::pbzero::TracePacket::Decoder(packet);
+
+  ASSERT_OK_AND_ASSIGN(auto status, find.Collect(decoder, &context));
+  ASSERT_EQ(status, CollectPrimitive::ContinueCollection::kNextPacket);
+
+  ASSERT_FALSE(context.package_uid.has_value());
+}
+
+TEST(FindPackageUidTest, ContinuesOverPackageListWithOutPackageName) {
+  const auto packet = CreatePackageListPacket();
+
+  Context context;
+  context.package_name = "com.not.a.packagename";
+
+  const FindPackageUid find;
+
+  const auto decoder = protos::pbzero::TracePacket::Decoder(packet);
+
+  ASSERT_OK_AND_ASSIGN(auto status, find.Collect(decoder, &context));
+  ASSERT_EQ(status, CollectPrimitive::ContinueCollection::kNextPacket);
+
+  ASSERT_FALSE(context.package_uid.has_value());
+}
+
+TEST(FindPackageUidTest, MissingPackageNameReturnsError) {
+  const auto packet = CreatePackageListPacket();
+
+  Context context;
+  const FindPackageUid find;
+
+  const auto decoder = protos::pbzero::TracePacket::Decoder(packet);
+  ASSERT_FALSE(find.Collect(decoder, &context).ok());
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/prune_package_list.cc b/src/trace_redaction/prune_package_list.cc
new file mode 100644
index 0000000..52c1394
--- /dev/null
+++ b/src/trace_redaction/prune_package_list.cc
@@ -0,0 +1,59 @@
+/*
+ * 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_redaction/prune_package_list.h"
+
+#include "protos/perfetto/trace/android/packages_list.gen.h"
+
+namespace perfetto::trace_redaction {
+
+PrunePackageList::PrunePackageList() = default;
+PrunePackageList::~PrunePackageList() = default;
+
+base::Status PrunePackageList::Transform(const Context& context,
+                                         std::string* packet) const {
+  if (!context.package_uid.has_value()) {
+    return base::ErrStatus("PrunePackageList: missing package uid.");
+  }
+
+  if (protos::pbzero::TracePacket::Decoder trace_packet_decoder(*packet);
+      !trace_packet_decoder.has_packages_list()) {
+    return base::OkStatus();
+  }
+
+  auto normalized_uid = NormalizeUid(context.package_uid.value());
+
+  protos::gen::TracePacket mutable_packet;
+  mutable_packet.ParseFromString(*packet);
+
+  auto* packages = mutable_packet.mutable_packages_list()->mutable_packages();
+
+  // Remove all entries that don't match the uid. After this, one or more
+  // packages will be left in the list (multiple packages can share a uid).
+  packages->erase(
+      std::remove_if(
+          packages->begin(), packages->end(),
+          [normalized_uid](const protos::gen::PackagesList::PackageInfo& info) {
+            return NormalizeUid(info.uid()) != normalized_uid;
+          }),
+      packages->end());
+
+  packet->assign(mutable_packet.SerializeAsString());
+
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/prune_package_list.h b/src/trace_redaction/prune_package_list.h
new file mode 100644
index 0000000..cf1298f
--- /dev/null
+++ b/src/trace_redaction/prune_package_list.h
@@ -0,0 +1,39 @@
+/*
+ * 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_REDACTION_PRUNE_PACKAGE_LIST_H_
+#define SRC_TRACE_REDACTION_PRUNE_PACKAGE_LIST_H_
+
+#include <string>
+
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+namespace perfetto::trace_redaction {
+
+// Removes all package list entries that don't match `Context.package_uid`.
+// Returns `base::ErrStatus()` if `Context.package_uid` was not set.
+class PrunePackageList final : public TransformPrimitive {
+ public:
+  PrunePackageList();
+  ~PrunePackageList() override;
+
+  base::Status Transform(const Context& context,
+                         std::string* packet) const override;
+};
+
+}  // namespace perfetto::trace_redaction
+
+#endif  // SRC_TRACE_REDACTION_PRUNE_PACKAGE_LIST_H_
diff --git a/src/trace_redaction/prune_package_list_unittest.cc b/src/trace_redaction/prune_package_list_unittest.cc
new file mode 100644
index 0000000..6a9af6c
--- /dev/null
+++ b/src/trace_redaction/prune_package_list_unittest.cc
@@ -0,0 +1,182 @@
+
+/*
+ * 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 <cstdint>
+#include <string>
+
+#include "protos/perfetto/trace/android/packages_list.gen.h"
+#include "protos/perfetto/trace/ps/process_tree.gen.h"
+#include "protos/perfetto/trace/trace_packet.gen.h"
+#include "src/trace_redaction/prune_package_list.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto::trace_redaction {
+
+namespace {
+using PackageList = protos::gen::PackagesList;
+using TracePacket = protos::gen::TracePacket;
+using ProcessTree = protos::gen::ProcessTree;
+
+constexpr uint64_t kPackageUid = 1037;
+constexpr std::string_view kPackageName =
+    "com.google.android.networkstack.tethering";
+
+void AddPackage(uint64_t uid, std::string_view name, PackageList* list) {
+  auto* package = list->add_packages();
+  package->set_uid(uid);
+  package->set_name(std::string(name));
+}
+
+std::string CreateTestPacket() {
+  auto packet = std::make_unique<TracePacket>();
+
+  packet->set_trusted_uid(9999);
+  packet->set_trusted_packet_sequence_id(2);
+  packet->set_previous_packet_dropped(true);
+
+  auto* packages = packet->mutable_packages_list();
+  AddPackage(10205, "com.google.android.uvexposurereporter", packages);
+  AddPackage(10007, "com.android.internal.display.cutout.emulation.noCutout",
+             packages);
+  AddPackage(kPackageUid, kPackageName, packages);
+  AddPackage(10367, "com.android.systemui.clocks.metro", packages);
+
+  return packet->SerializeAsString();
+}
+
+// packet {
+//   process_tree {
+//     processes {
+//       pid: 23022
+//       ppid: 1
+//       cmdline: "/vendor/bin/hw/wpa_supplicant"
+//       cmdline: "-O/data/vendor/wifi/wpa/sockets"
+//       cmdline: "-dd"
+//       cmdline: "-g@android:wpa_wlan0"
+//       uid: 1010
+//     }
+//     threads {
+//       tid: 6382
+//       tgid: 18176
+//     }
+//     threads {
+//      tid: 18419
+//       tgid: 18176
+//     }
+//     threads {
+//       tid: 18434
+//       tgid: 18176
+//     }
+//     collection_end_timestamp: 333724398314653
+//   }
+//   trusted_uid: 9999
+//   timestamp: 333724396714922
+//   trusted_packet_sequence_id: 3
+//   trusted_pid: 1085
+// }
+std::string CreateNoPackageListPacket() {
+  auto packet = std::make_unique<TracePacket>();
+
+  packet->set_trusted_uid(9999);
+  packet->set_timestamp(333724396714922);
+  packet->set_trusted_packet_sequence_id(3);
+  packet->set_trusted_pid(1085);
+
+  ProcessTree* tree = packet->mutable_process_tree();
+
+  auto* p0 = tree->add_processes();
+  p0->set_pid(23022);
+  p0->set_ppid(1);
+  p0->add_cmdline("/vendor/bin/hw/wpa_supplicant");
+  p0->add_cmdline("-O/data/vendor/wifi/wpa/sockets");
+  p0->add_cmdline("-dd");
+  p0->add_cmdline("-g@android:wpa_wlan0");
+  p0->set_uid(1010);
+
+  auto* t0 = tree->add_threads();
+  t0->set_tid(6382);
+  t0->set_tgid(18176);
+
+  auto* t1 = tree->add_threads();
+  t1->set_tid(18419);
+  t1->set_tgid(18176);
+
+  auto* t2 = tree->add_threads();
+  t2->set_tid(18434);
+  t2->set_tgid(18176);
+
+  tree->set_collection_end_timestamp(333724398314653);
+
+  return packet->SerializeAsString();
+}
+}  // namespace
+
+TEST(PrunePackageListTest, ReturnsErrorWhenPackageUidIsMissing) {
+  auto before = CreateTestPacket();
+
+  const Context context;
+  const PrunePackageList prune;
+  ASSERT_FALSE(prune.Transform(context, &before).ok());
+}
+
+TEST(PrunePackageListTest, NoopWhenThereIsNoPackageList) {
+  Context context;
+  context.package_uid.emplace(1037);
+
+  const auto before = CreateNoPackageListPacket();
+  auto after = CreateNoPackageListPacket();
+
+  ASSERT_EQ(before, after);
+
+  const PrunePackageList prune;
+  ASSERT_TRUE(prune.Transform(context, &after).ok());
+
+  // The buffer should have changed.
+  ASSERT_EQ(before, after);
+}
+
+// PrunePackageList should not drop packets, instead it should drop individual
+// PackageInfo entries.
+TEST(PrunePackageListTest, RemovesPackagesInfoFromPackageList) {
+  Context context;
+  context.package_uid.emplace(1037);
+
+  const auto before = CreateTestPacket();
+  auto after = CreateTestPacket();
+
+  ASSERT_EQ(before, after);
+
+  const PrunePackageList prune;
+  ASSERT_TRUE(prune.Transform(context, &after).ok());
+
+  // The buffer should have changed.
+  ASSERT_NE(before, after);
+
+  protos::gen::TracePacket after_packet;
+  after_packet.ParseFromString(after);
+
+  ASSERT_TRUE(after_packet.has_packages_list());
+  ASSERT_EQ(1, after_packet.packages_list().packages_size());
+
+  ASSERT_TRUE(after_packet.packages_list().packages().at(0).has_uid());
+  ASSERT_EQ(kPackageUid, after_packet.packages_list().packages().at(0).uid());
+
+  ASSERT_TRUE(after_packet.packages_list().packages().at(0).has_name());
+  ASSERT_EQ(kPackageName, after_packet.packages_list().packages().at(0).name());
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/trace_redaction_framework.cc b/src/trace_redaction/trace_redaction_framework.cc
new file mode 100644
index 0000000..e3b729a
--- /dev/null
+++ b/src/trace_redaction/trace_redaction_framework.cc
@@ -0,0 +1,27 @@
+/*
+ * 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_redaction/trace_redaction_framework.h"
+
+namespace perfetto::trace_redaction {
+
+CollectPrimitive::~CollectPrimitive() = default;
+
+BuildPrimitive::~BuildPrimitive() = default;
+
+TransformPrimitive::~TransformPrimitive() = default;
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/trace_redaction_framework.h b/src/trace_redaction/trace_redaction_framework.h
new file mode 100644
index 0000000..c9e7fb4
--- /dev/null
+++ b/src/trace_redaction/trace_redaction_framework.h
@@ -0,0 +1,138 @@
+/*
+ * 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_REDACTION_TRACE_REDACTION_FRAMEWORK_H_
+#define SRC_TRACE_REDACTION_TRACE_REDACTION_FRAMEWORK_H_
+
+#include <string>
+#include <vector>
+
+#include "perfetto/ext/base/status_or.h"
+#include "protos/perfetto/trace/trace_packet.gen.h"
+#include "protos/perfetto/trace/trace_packet.pbzero.h"
+
+namespace perfetto::trace_redaction {
+
+// Multiple packages can share the same name. This is common when a device has
+// multiple users. When this happens, each instance shares the 5 least
+// significant digits.
+constexpr uint64_t NormalizeUid(uint64_t uid) {
+  return uid % 1000000;
+}
+
+// Primitives should be stateless. All state should be stored in the context.
+// Primitives should depend on data in the context, not the origin of the data.
+// This allows primitives to be swapped out or work together to populate data
+// needed by another primitive.
+//
+// For this to work, primitives are divided into three types:
+//
+//  `CollectPrimitive` :  Reads data from trace packets and saves low-level data
+//                        in the context.
+//
+//  `BuildPrimitive` :    Reads low-level data from the context and builds
+//                        high-level (read-optimized) data structures.
+//
+//  `TransformPrimitive`: Reads high-level data from the context and modifies
+//                        trace packets.
+class Context {
+ public:
+  // The package that should not be redacted. This must be populated before
+  // running any primitives.
+  std::string package_name;
+
+  // The package list maps a package name to a uid. It is possible for multiple
+  // package names to map to the same uid, for example:
+  //
+  //    packages {
+  //      name: "com.google.android.gms"
+  //      uid: 10113
+  //      debuggable: false
+  //      profileable_from_shell: false
+  //      version_code: 235013038
+  //    }
+  //    packages {
+  //      name: "com.google.android.gsf"
+  //      uid: 10113
+  //      debuggable: false
+  //      profileable_from_shell: false
+  //      version_code: 34
+  //    }
+  //
+  // The process tree maps processes to packages via the uid value. However
+  // multiple processes can map to the same uid, only differed by some multiple
+  // of 100000, for example:
+  //
+  //    processes {
+  //      pid: 18176
+  //      ppid: 904
+  //      cmdline: "com.google.android.gms.persistent"
+  //      uid: 10113
+  //    }
+  //    processes {
+  //      pid: 21388
+  //      ppid: 904
+  //      cmdline: "com.google.android.gms.persistent"
+  //      uid: 1010113
+  //    }
+  std::optional<uint64_t> package_uid;
+};
+
+// Responsible for extracting low-level data from the trace and storing it in
+// the context.
+class CollectPrimitive {
+ public:
+  // When a collect primitive has collected all necessary information, it can
+  // stop processing packets by returning kRetire. If the primitives wants to
+  // continue processing packets, it will return kNextPacket.
+  //
+  // If a collector encounters an unrecoverable error, base::ErrStatus() is
+  // returned.
+  enum class ContinueCollection : bool { kRetire = false, kNextPacket = true };
+
+  virtual ~CollectPrimitive();
+
+  // Processes a packet and writes low-level data to the context. Returns
+  // kContinue if the primitive wants more data (i.e. next packet).
+  virtual base::StatusOr<ContinueCollection> Collect(
+      const protos::pbzero::TracePacket::Decoder& packet,
+      Context* context) const = 0;
+};
+
+// Responsible for converting low-level data from the context and storing it in
+// the context (high-level data).
+class BuildPrimitive {
+ public:
+  virtual ~BuildPrimitive();
+
+  // Reads low-level data from the context and writes high-level data to the
+  // context.
+  virtual base::Status Build(Context* context) const = 0;
+};
+
+// Responsible for modifying trace packets using data from the context.
+class TransformPrimitive {
+ public:
+  virtual ~TransformPrimitive();
+
+  // Modifies a packet using data from the context.
+  virtual base::Status Transform(const Context& context,
+                                 std::string* packet) const = 0;
+};
+
+}  // namespace perfetto::trace_redaction
+
+#endif  // SRC_TRACE_REDACTION_TRACE_REDACTION_FRAMEWORK_H_
diff --git a/src/trace_redaction/trace_redactor.cc b/src/trace_redaction/trace_redactor.cc
new file mode 100644
index 0000000..eb20c27
--- /dev/null
+++ b/src/trace_redaction/trace_redactor.cc
@@ -0,0 +1,215 @@
+/*
+ * 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_redaction/trace_redactor.h"
+
+#include <sys/mman.h>
+#include <unistd.h>
+#include <cstddef>
+#include <cstdio>
+#include <string>
+#include <string_view>
+#include <vector>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/scoped_file.h"
+#include "perfetto/protozero/scattered_heap_buffer.h"
+#include "perfetto/trace_processor/trace_blob.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+#include "protos/perfetto/trace/trace.pbzero.h"
+
+namespace perfetto::trace_redaction {
+namespace {
+
+using Trace = protos::pbzero::Trace;
+using TracePacket = protos::pbzero::TracePacket;
+
+// Basic scoped objects (similar to base::ScopedResource) to make sure page
+// resources are cleaned-up.
+struct MappedFile {
+  void* start_address;
+  size_t size;
+
+  MappedFile() : start_address(MAP_FAILED), size(0) {}
+
+  ~MappedFile() {
+    if (start_address != MAP_FAILED) {
+      munmap(start_address, size);
+    }
+  }
+
+  MappedFile(const MappedFile&) = delete;
+  MappedFile& operator=(const MappedFile&) = delete;
+  MappedFile(MappedFile&&) = delete;
+  MappedFile& operator=(MappedFile&&) = delete;
+};
+
+}  // namespace
+
+TraceRedactor::TraceRedactor() = default;
+
+TraceRedactor::~TraceRedactor() = default;
+
+base::Status TraceRedactor::Redact(std::string_view source_filename,
+                                   std::string_view dest_filename,
+                                   Context* context) const {
+  const std::string source_filename_str(source_filename);
+
+  base::ScopedFile file_handle(base::OpenFile(source_filename_str, O_RDONLY));
+
+  if (!file_handle) {
+    return base::ErrStatus("Failed to read trace from disk: %s",
+                           source_filename_str.c_str());
+  }
+
+  auto file_size_offset = lseek(*file_handle, 0, SEEK_END);
+
+  if (file_size_offset <= 0) {
+    return base::ErrStatus("Could not determine trace size (%s)",
+                           source_filename_str.c_str());
+  }
+
+  lseek(*file_handle, 0, SEEK_SET);
+
+  MappedFile page;
+  page.size = static_cast<size_t>(file_size_offset);
+  page.start_address =
+      mmap(nullptr, page.size, PROT_READ, MAP_PRIVATE, *file_handle, 0);
+
+  if (page.start_address == MAP_FAILED) {
+    return base::ErrStatus("Failed to map pages for trace (%zu bytes)",
+                           page.size);
+  }
+
+  trace_processor::TraceBlobView whole_view(
+      trace_processor::TraceBlob::FromMmap(page.start_address, page.size));
+
+  if (auto status = Collect(context, whole_view); !status.ok()) {
+    return status;
+  }
+
+  if (auto status = Build(context); !status.ok()) {
+    return status;
+  }
+
+  if (auto status = Transform(*context, whole_view, std::string(dest_filename));
+      !status.ok()) {
+    return status;
+  }
+
+  return base::OkStatus();
+}
+
+base::Status TraceRedactor::Collect(
+    Context* context,
+    const trace_processor::TraceBlobView& view) const {
+  // Mask, marking which collectors should be ran. When a collector no longer
+  // needs to run, the value will be null.
+  std::vector<const CollectPrimitive*> collectors;
+  collectors.reserve(collectors_.size());
+
+  for (const auto& collector : collectors_) {
+    collectors.push_back(collector.get());
+  }
+
+  const Trace::Decoder trace_decoder(view.data(), view.length());
+
+  for (auto packet_it = trace_decoder.packet(); packet_it; ++packet_it) {
+    const TracePacket::Decoder packet(packet_it->as_bytes());
+
+    for (auto cit = collectors.begin(); cit != collectors.end();) {
+      auto status = (*cit)->Collect(packet, context);
+
+      if (!status.ok()) {
+        return status.status();
+      }
+
+      // If this collector has returned `kStop`, it means that it (and it alone)
+      // no longer needs to run. The driver (TraceRedactor) should not invoke it
+      // on any future packets.
+      if (status.value() == CollectPrimitive::ContinueCollection::kRetire) {
+        cit = collectors.erase(cit);
+      } else {
+        ++cit;
+      }
+    }
+
+    // If all the collectors have found what they were looking for, then there
+    // is no reason to continue through the trace.
+    if (collectors.empty()) {
+      break;
+    }
+  }
+
+  return base::OkStatus();
+}
+
+base::Status TraceRedactor::Build(Context* context) const {
+  for (const auto& builder : builders_) {
+    if (auto status = builder->Build(context); !status.ok()) {
+      return status;
+    }
+  }
+
+  return base::OkStatus();
+}
+
+base::Status TraceRedactor::Transform(
+    const Context& context,
+    const trace_processor::TraceBlobView& view,
+    const std::string& dest_file) const {
+  std::ignore = context;
+  const auto dest_fd = base::OpenFile(dest_file, O_RDWR | O_CREAT, 0666);
+
+  if (dest_fd.get() == -1) {
+    return base::ErrStatus(
+        "Failed to open destination file; can't write redacted trace.");
+  }
+
+  const Trace::Decoder trace_decoder(view.data(), view.length());
+  for (auto packet_it = trace_decoder.packet(); packet_it; ++packet_it) {
+    auto packet = packet_it->as_std_string();
+
+    for (const auto& transformer : transformers_) {
+      if (auto status = transformer->Transform(context, &packet);
+          !status.ok()) {
+        return status;
+      }
+    }
+
+    protozero::HeapBuffered<> serializer;
+    auto* packet_message =
+        serializer->BeginNestedMessage<TracePacket>(Trace::kPacketFieldNumber);
+    packet_message->AppendRawProtoBytes(packet.data(), packet.size());
+    packet_message->Finalize();
+    serializer->Finalize();
+
+    auto encoded_packet = serializer.SerializeAsString();
+
+    if (const auto exported_data = base::WriteAll(
+            dest_fd.get(), encoded_packet.data(), encoded_packet.size());
+        exported_data <= 0) {
+      return base::ErrStatus("Failed to write redacted trace to disk");
+    }
+  }
+
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/trace_redactor.h b/src/trace_redaction/trace_redactor.h
new file mode 100644
index 0000000..99a8832
--- /dev/null
+++ b/src/trace_redaction/trace_redactor.h
@@ -0,0 +1,96 @@
+/*
+ * 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_REDACTION_TRACE_REDACTOR_H_
+#define SRC_TRACE_REDACTION_TRACE_REDACTOR_H_
+
+#include <string_view>
+
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_redaction/trace_redaction_framework.h"
+
+namespace perfetto::trace_redaction {
+
+// Removes sensitive information from Perfetto traces by executing collect,
+// build, and transforms primtives in the correct order.
+//
+// The caller is responsible for adding all neccessary primitives. Primitives
+// are not directly dependent on each other, but rather dependent on the
+// information inside of the context.
+class TraceRedactor {
+ public:
+  TraceRedactor();
+  virtual ~TraceRedactor();
+
+  // Entry point for redacting a trace. Regardless of success/failure, `context`
+  // will contain the current state.
+  base::Status Redact(std::string_view source_filename,
+                      std::string_view dest_filename,
+                      Context* context) const;
+
+  std::vector<std::unique_ptr<CollectPrimitive>>* collectors() {
+    return &collectors_;
+  }
+
+  std::vector<std::unique_ptr<BuildPrimitive>>* builders() {
+    return &builders_;
+  }
+
+  std::vector<std::unique_ptr<TransformPrimitive>>* transformers() {
+    return &transformers_;
+  }
+
+ private:
+  // Run all collectors on a packet because moving to the next package.
+  //
+  // ```
+  //  with context:
+  //   for packet in packets:
+  //     for collector in collectors:
+  //       collector(context, packet)
+  // ```
+  base::Status Collect(Context* context,
+                       const trace_processor::TraceBlobView& view) const;
+
+  // Runs builders once.
+  //
+  // ```
+  //  with context:
+  //   for builder in builders:
+  //      builder(context)
+  // ```
+  base::Status Build(Context* context) const;
+
+  // Runs all transformers on a packet before moving to the next package.
+  //
+  // ```
+  //  with context:
+  //   for packet in packets:
+  //     for transform in transformers:
+  //       transform(context, packet)
+  // ```
+  base::Status Transform(const Context& context,
+                         const trace_processor::TraceBlobView& view,
+                         const std::string& dest_file) const;
+
+  std::vector<std::unique_ptr<CollectPrimitive>> collectors_;
+  std::vector<std::unique_ptr<BuildPrimitive>> builders_;
+  std::vector<std::unique_ptr<TransformPrimitive>> transformers_;
+};
+
+}  // namespace perfetto::trace_redaction
+
+#endif  // SRC_TRACE_REDACTION_TRACE_REDACTOR_H_
diff --git a/src/trace_redaction/trace_redactor_integrationtest.cc b/src/trace_redaction/trace_redactor_integrationtest.cc
new file mode 100644
index 0000000..c30f2f8
--- /dev/null
+++ b/src/trace_redaction/trace_redactor_integrationtest.cc
@@ -0,0 +1,114 @@
+/*
+ * 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 "perfetto/ext/base/file_utils.h"
+#include "perfetto/ext/base/temp_file.h"
+#include "src/base/test/utils.h"
+#include "src/trace_redaction/find_package_uid.h"
+#include "src/trace_redaction/prune_package_list.h"
+#include "src/trace_redaction/trace_redactor.h"
+#include "test/gtest_and_gmock.h"
+
+#include "protos/perfetto/trace/android/packages_list.pbzero.h"
+#include "protos/perfetto/trace/trace.pbzero.h"
+
+namespace perfetto::trace_redaction {
+
+namespace {
+using PackagesList = protos::pbzero::PackagesList;
+using PackageInfo = protos::pbzero::PackagesList::PackageInfo;
+using Trace = protos::pbzero::Trace;
+using TracePacket = protos::pbzero::TracePacket;
+
+constexpr std::string_view kTracePath =
+    "test/data/trace_redaction_jank_high_cpu.pftrace";
+
+// "com.google.android.settings.intelligence" will have one package, but two
+// processes will reference it. When doing so, they will use two different
+// uids (multiples of 1,000,000).
+constexpr std::string_view kPackageName =
+    "com.google.android.settings.intelligence";
+constexpr uint64_t kPackageUid = 10118;
+
+class TraceRedactorIntegrationTest : public testing::Test {
+ public:
+  TraceRedactorIntegrationTest() = default;
+  ~TraceRedactorIntegrationTest() override = default;
+
+ protected:
+  void SetUp() override {
+    src_trace_ = base::GetTestDataPath(std::string(kTracePath));
+    dest_trace_ = std::make_unique<base::TempFile>(base::TempFile::Create());
+  }
+
+  const std::string& src_trace() const { return src_trace_; }
+
+  const std::string& dest_trace() const { return dest_trace_->path(); }
+
+ private:
+  std::string src_trace_;
+  std::unique_ptr<base::TempFile> dest_trace_;
+};
+
+TEST_F(TraceRedactorIntegrationTest, FindsPackageAndFiltersPackageList) {
+  TraceRedactor redaction;
+  redaction.collectors()->emplace_back(new FindPackageUid());
+  redaction.transformers()->emplace_back(new PrunePackageList());
+
+  Context context;
+  context.package_name = kPackageName;
+
+  auto result = redaction.Redact(src_trace(), dest_trace(), &context);
+
+  ASSERT_TRUE(result.ok()) << result.message();
+
+  std::string redacted_buffer;
+  ASSERT_TRUE(base::ReadFile(dest_trace(), &redacted_buffer));
+
+  // Collect package info from the trace.
+  std::vector<protozero::ConstBytes> infos;
+
+  Trace::Decoder trace_decoder(redacted_buffer);
+
+  for (auto packet_it = trace_decoder.packet(); packet_it; ++packet_it) {
+    TracePacket::Decoder packet_decoder(*packet_it);
+
+    if (packet_decoder.has_packages_list()) {
+      PackagesList::Decoder list_it(packet_decoder.packages_list());
+
+      for (auto info_it = list_it.packages(); info_it; ++info_it) {
+        infos.push_back(*info_it);
+      }
+    }
+  }
+
+  ASSERT_EQ(infos.size(), 1u);
+
+  PackageInfo::Decoder info(infos[0]);
+
+  ASSERT_TRUE(info.has_name());
+  ASSERT_EQ(info.name().ToStdString(), kPackageName);
+
+  ASSERT_TRUE(info.has_uid());
+  ASSERT_EQ(NormalizeUid(info.uid()), NormalizeUid(kPackageUid));
+
+  ASSERT_TRUE(context.package_uid.has_value());
+  ASSERT_EQ(NormalizeUid(context.package_uid.value()),
+            NormalizeUid(kPackageUid));
+}
+
+}  // namespace
+}  // namespace perfetto::trace_redaction
diff --git a/test/data/trace_redaction_jank_high_cpu.pftrace.sha256 b/test/data/trace_redaction_jank_high_cpu.pftrace.sha256
new file mode 100644
index 0000000..93dd891
--- /dev/null
+++ b/test/data/trace_redaction_jank_high_cpu.pftrace.sha256
@@ -0,0 +1 @@
+5ea2ff312b5061bf23c2da43380295ab1b207902ffa01462a35d665c56bb4e97
\ No newline at end of file