diff --git a/BUILD b/BUILD index 164570305ac..60947897b3a 100644 --- a/BUILD +++ b/BUILD @@ -568,7 +568,6 @@ perfetto_cc_library( ":src_trace_processor_util_proto_to_args_parser", ":src_trace_processor_util_protozero_to_json", ":src_trace_processor_util_protozero_to_text", - ":src_trace_processor_util_simple_json_parser", ":src_trace_processor_util_simple_json_serializer", ":src_trace_processor_util_sql_argument", ":src_trace_processor_util_sql_bundle", @@ -689,6 +688,7 @@ perfetto_cc_library( ":src_trace_processor_plugins_wattson_gen_tpu_curves", ":src_trace_processor_trace_summary_gen_cc_trace_summary_descriptor", ":src_trace_processor_util_build_id", + ":src_trace_processor_util_simple_json_parser", ] + PERFETTO_CONFIG.deps.sqlite + PERFETTO_CONFIG.deps.sqlite_ext_percentile + PERFETTO_CONFIG.deps.zlib + @@ -884,7 +884,6 @@ perfetto_cc_library( ":src_trace_processor_util_proto_to_args_parser", ":src_trace_processor_util_protozero_to_json", ":src_trace_processor_util_protozero_to_text", - ":src_trace_processor_util_simple_json_parser", ":src_trace_processor_util_simple_json_serializer", ":src_trace_processor_util_sql_argument", ":src_trace_processor_util_sql_bundle", @@ -1015,6 +1014,7 @@ perfetto_cc_library( ":src_trace_processor_trace_summary_gen_cc_trace_summary_descriptor", ":src_trace_processor_util_build_id", ":src_trace_processor_util_deobfuscation_deobfuscator", + ":src_trace_processor_util_simple_json_parser", ":src_trace_processor_util_symbolizer_symbolize_database", ":src_trace_processor_util_symbolizer_symbolizer", ":src_traceconv_gen_cc_android_extension_descriptor", @@ -5708,6 +5708,7 @@ perfetto_cc_library( ":protos_perfetto_trace_track_event_zero", ":protos_perfetto_trace_translation_zero", ":protozero", + ":src_trace_processor_util_simple_json_parser", ], linkstatic = True, ) @@ -6051,12 +6052,20 @@ perfetto_filegroup( ) # GN target: //src/trace_processor/util:simple_json_parser -perfetto_filegroup( +perfetto_cc_library( name = "src_trace_processor_util_simple_json_parser", srcs = [ + ":src_trace_processor_util_json_parser", "src/trace_processor/util/simple_json_parser.cc", + ], + hdrs = [ + ":include_perfetto_base_base", + ":include_perfetto_ext_base_base", + ":include_perfetto_public_abi_base", + ":include_perfetto_public_base", "src/trace_processor/util/simple_json_parser.h", ], + linkstatic = True, ) # GN target: //src/trace_processor/util:simple_json_serializer @@ -11611,7 +11620,6 @@ perfetto_cc_library( ":src_trace_processor_util_proto_to_args_parser", ":src_trace_processor_util_protozero_to_json", ":src_trace_processor_util_protozero_to_text", - ":src_trace_processor_util_simple_json_parser", ":src_trace_processor_util_simple_json_serializer", ":src_trace_processor_util_sql_argument", ":src_trace_processor_util_sql_bundle", @@ -11733,6 +11741,7 @@ perfetto_cc_library( ":src_trace_processor_plugins_wattson_gen_tpu_curves", ":src_trace_processor_trace_summary_gen_cc_trace_summary_descriptor", ":src_trace_processor_util_build_id", + ":src_trace_processor_util_simple_json_parser", ] + PERFETTO_CONFIG.deps.sqlite + PERFETTO_CONFIG.deps.sqlite_ext_percentile + PERFETTO_CONFIG.deps.zlib + @@ -11949,7 +11958,6 @@ perfetto_cc_binary( ":src_trace_processor_util_proto_to_args_parser", ":src_trace_processor_util_protozero_to_json", ":src_trace_processor_util_protozero_to_text", - ":src_trace_processor_util_simple_json_parser", ":src_trace_processor_util_simple_json_serializer", ":src_trace_processor_util_sql_argument", ":src_trace_processor_util_sql_bundle", @@ -12062,6 +12070,7 @@ perfetto_cc_binary( ":src_trace_processor_trace_summary_gen_cc_trace_summary_descriptor", ":src_trace_processor_util_build_id", ":src_trace_processor_util_deobfuscation_deobfuscator", + ":src_trace_processor_util_simple_json_parser", ":src_trace_processor_util_symbolizer_symbolize_database", ":src_trace_processor_util_symbolizer_symbolizer", ":src_traceconv_gen_cc_android_extension_descriptor", diff --git a/protos/perfetto/trace/profiling/deobfuscation.proto b/protos/perfetto/trace/profiling/deobfuscation.proto index 677a7e0ae91..76b711a163f 100644 --- a/protos/perfetto/trace/profiling/deobfuscation.proto +++ b/protos/perfetto/trace/profiling/deobfuscation.proto @@ -50,6 +50,22 @@ message ObfuscatedClass { // fields. repeated ObfuscatedMember obfuscated_members = 3; repeated ObfuscatedMember obfuscated_methods = 4; + + // Information about a single class merged by optimization into this + // obfuscated class. + message MergedClass { + optional string name = 1; + optional int32 class_id = 2; + optional MergedClasses merged_classes = 3; + } + + // Set of classes merged into this obfuscated class by optimization. + message MergedClasses { + optional string class_id_field_name = 1; + repeated MergedClass merged_classes = 2; + } + + optional MergedClasses merged_classes = 5; } message DeobfuscationMapping { diff --git a/src/trace_processor/util/BUILD.gn b/src/trace_processor/util/BUILD.gn index ae53b210987..96ccc3a3c56 100644 --- a/src/trace_processor/util/BUILD.gn +++ b/src/trace_processor/util/BUILD.gn @@ -317,8 +317,8 @@ source_set("json_parser") { ] } -# Header-only JSON streaming parser. No external dependencies. -source_set("simple_json_parser") { +# JSON streaming parser. +perfetto_component("simple_json_parser") { sources = [ "simple_json_parser.cc", "simple_json_parser.h", diff --git a/src/trace_processor/util/deobfuscation/BUILD.gn b/src/trace_processor/util/deobfuscation/BUILD.gn index fd09164c52e..bbab748b52e 100644 --- a/src/trace_processor/util/deobfuscation/BUILD.gn +++ b/src/trace_processor/util/deobfuscation/BUILD.gn @@ -22,6 +22,7 @@ perfetto_component("deobfuscator") { "deobfuscator.h", ] deps = [ + "..:simple_json_parser", "../../../../gn:default_deps", "../../../../include/perfetto/ext/base:base", "../../../../protos/perfetto/trace:zero", @@ -36,6 +37,8 @@ perfetto_unittest_source_set("unittests") { ":deobfuscator", "../../../../gn:default_deps", "../../../../gn:gtest_and_gmock", + "../../../../protos/perfetto/trace:zero", + "../../../../protos/perfetto/trace/profiling:zero", "../../../base", "../../../base:test_support", ] diff --git a/src/trace_processor/util/deobfuscation/deobfuscator.cc b/src/trace_processor/util/deobfuscation/deobfuscator.cc index 11a6c3e63cf..0011ee3337f 100644 --- a/src/trace_processor/util/deobfuscation/deobfuscator.cc +++ b/src/trace_processor/util/deobfuscation/deobfuscator.cc @@ -15,6 +15,7 @@ */ #include "src/trace_processor/util/deobfuscation/deobfuscator.h" +#include "src/trace_processor/util/simple_json_parser.h" #include @@ -25,6 +26,7 @@ #include "perfetto/ext/base/file_utils.h" #include "perfetto/ext/base/scoped_file.h" #include "perfetto/ext/base/string_splitter.h" +#include "perfetto/ext/base/string_utils.h" #include "perfetto/protozero/scattered_heap_buffer.h" #include "protos/perfetto/trace/profiling/deobfuscation.pbzero.h" @@ -35,6 +37,18 @@ namespace perfetto { namespace profiling { namespace { +using SimpleJsonParser = perfetto::trace_processor::json::SimpleJsonParser; +using FieldResult = perfetto::trace_processor::json::FieldResult; + +// Json keys used to parse merged classes. +constexpr std::string_view kMergedClassesId = + "com.android.tools.r8.mergedClasses"; +constexpr std::string_view kIdKey = "id"; +constexpr std::string_view kClassNameKey = "name"; +constexpr std::string_view kClassIdKey = "class_id"; +constexpr std::string_view kClassIdFieldKey = "class_id_field"; +constexpr std::string_view kMergedClassesKey = "merged_classes"; + struct ProguardClass { std::string obfuscated_name; std::string deobfuscated_name; @@ -78,6 +92,107 @@ std::optional ParseClass(std::string line) { std::move(deobfuscated_name)}; } +// Forward declaration. +base::Status ParseMergedClass(SimpleJsonParser& parser, + ObfuscatedClass::MergedClass& out); + +// Parses common fields for MergedClasses (class_id_field and merged_classes). +FieldResult ParseMergedClassesField(SimpleJsonParser& parser, + std::string_view key, + ObfuscatedClass::MergedClasses& mcs) { + if (key == kClassIdFieldKey) { + if (auto val = parser.GetString()) { + mcs.class_id_field_name = std::string(*val); + } else { + return base::Status("Expected class id field name."); + } + return FieldResult::Handled{}; + } + if (key == kMergedClassesKey) { + if (!parser.IsArray()) { + return base::Status("Expected array for merged_classes."); + } + base::Status array_status = + parser.ForEachArrayElement([&]() -> base::Status { + ObfuscatedClass::MergedClass mc; + RETURN_IF_ERROR(ParseMergedClass(parser, mc)); + mcs.merged_classes.push_back(std::move(mc)); + return base::OkStatus(); + }); + return FieldResult(array_status); + } + return FieldResult::Skip{}; +} + +// Parses a single MergedClass entry from R8 mergedClasses JSON. +base::Status ParseMergedClass(SimpleJsonParser& parser, + ObfuscatedClass::MergedClass& out) { + return parser.ForEachField([&](std::string_view key) -> FieldResult { + if (key == kClassNameKey) { + if (auto val = parser.GetString()) { + out.name = std::string(*val); + } else { + return base::Status("Expected class name."); + } + return FieldResult::Handled{}; + } + if (key == kClassIdKey) { + if (auto val_str = parser.GetString()) { + if (auto parsed_val = base::StringViewToInt32(*val_str)) { + out.class_id = *parsed_val; + } else { + return base::Status("Invalid class id format in string."); + } + } else if (auto val_int = parser.GetInt64(); + val_int.has_value() && + val_int.value() >= std::numeric_limits::min() && + val_int.value() <= std::numeric_limits::max()) { + out.class_id = static_cast(*val_int); + } else { + return base::Status("Expected class id."); + } + return FieldResult::Handled{}; + } + FieldResult res = + ParseMergedClassesField(parser, key, out.nested_merged_classes); + if (!res.handled) { + PERFETTO_DLOG("Unknown field in merged class JSON: %.*s", + static_cast(key.size()), key.data()); + } + return res; + }); +} + +// Parses R8 `com.android.tools.r8.mergedClasses` JSON comment string in +// Proguard mapping. +base::Status ParseMergedClassesComment(std::string_view json_str, + ObfuscatedClass& target_class) { + SimpleJsonParser parser(json_str); + RETURN_IF_ERROR(parser.Parse()); + + bool is_merged_classes_id = false; + ObfuscatedClass::MergedClasses mcs; + + base::Status s = + parser.ForEachField([&](std::string_view key) -> FieldResult { + if (key == kIdKey) { + if (auto val = parser.GetString()) { + if (*val == kMergedClassesId) { + is_merged_classes_id = true; + } + } + return FieldResult::Handled{}; + } + return ParseMergedClassesField(parser, key, mcs); + }); + RETURN_IF_ERROR(s); + + if (is_merged_classes_id) { + *target_class.mutable_merged_classes() = std::move(mcs); + } + return base::OkStatus(); +} + enum class ProguardMemberType { kField, kMethod, @@ -332,9 +447,25 @@ std::map ObfuscatedClass::deobfuscated_methods() // file format we are parsing. base::Status ProguardParser::AddLine(std::string line) { auto first_ch_pos = line.find_first_not_of(" \t"); - if (first_ch_pos == std::string::npos || line[first_ch_pos] == '#') + if (first_ch_pos == std::string::npos) return base::Status(); + if (line[first_ch_pos] == '#') { + if (current_class_ == nullptr) { + return base::Status(); + } + size_t json_start = line.find('{'); + if (json_start != std::string::npos) { + std::string_view json_sv = std::string_view(line).substr(json_start); + base::Status s = ParseMergedClassesComment(json_sv, *current_class_); + if (!s.ok()) { + PERFETTO_DLOG("Failed to parse merged classes comment: %s\non line %s", + s.message().c_str(), line.c_str()); + } + } + return base::Status(); + } + bool is_member = line[0] == ' '; if (is_member && !current_class_) { return base::Status( @@ -405,6 +536,35 @@ bool ProguardParser::AddLines(std::string contents) { return true; } +static void SerializeMergedClasses( + const profiling::ObfuscatedClass::MergedClasses& src, + perfetto::protos::pbzero::ObfuscatedClass::MergedClasses* dest) { + if (!src.class_id_field_name.empty()) { + dest->set_class_id_field_name(src.class_id_field_name); + } + for (const auto& mc : src.merged_classes) { + auto* dest_mc = dest->add_merged_classes(); + if (!mc.name.empty()) { + dest_mc->set_name(mc.name); + } + if (mc.class_id.has_value()) { + dest_mc->set_class_id(*mc.class_id); + } + if (!mc.nested_merged_classes.merged_classes.empty()) { + SerializeMergedClasses(mc.nested_merged_classes, + dest_mc->set_merged_classes()); + } + } +} + +static void SerializeTopLevelMergedClasses( + const profiling::ObfuscatedClass::MergedClasses& src, + perfetto::protos::pbzero::ObfuscatedClass* dest) { + if (!src.merged_classes.empty()) { + SerializeMergedClasses(src, dest->set_merged_classes()); + } +} + void MakeDeobfuscationPackets( const std::string& package_name, const std::map& mapping, @@ -447,6 +607,7 @@ void MakeDeobfuscationPackets( proto_member->set_source_line_end(*method.source_line_end); } } + SerializeTopLevelMergedClasses(cls.merged_classes(), proto_class); } callback(trace.SerializeAsString()); } diff --git a/src/trace_processor/util/deobfuscation/deobfuscator.h b/src/trace_processor/util/deobfuscation/deobfuscator.h index 1d560b221dc..b4d083f78b7 100644 --- a/src/trace_processor/util/deobfuscation/deobfuscator.h +++ b/src/trace_processor/util/deobfuscation/deobfuscator.h @@ -46,6 +46,22 @@ struct MethodMapping { class ObfuscatedClass { public: + struct MergedClass; + + // Represents R8 merged classes mapping information for an obfuscated class. + struct MergedClasses { + std::string class_id_field_name; + std::vector merged_classes; + }; + + // Represents an individual class merged by R8 into an obfuscated target + // class. + struct MergedClass { + std::string name; + std::optional class_id; + MergedClasses nested_merged_classes; + }; + explicit ObfuscatedClass(std::string d) : deobfuscated_name_(std::move(d)) {} const std::string& deobfuscated_name() const { return deobfuscated_name_; } @@ -73,10 +89,15 @@ class ObfuscatedClass { return method_mappings_; } + const MergedClasses& merged_classes() const { return merged_classes_; } + + MergedClasses* mutable_merged_classes() { return &merged_classes_; } + private: std::string deobfuscated_name_; std::map deobfuscated_fields_; std::vector method_mappings_; + MergedClasses merged_classes_; }; class ProguardParser { diff --git a/src/trace_processor/util/deobfuscation/deobfuscator_unittest.cc b/src/trace_processor/util/deobfuscation/deobfuscator_unittest.cc index 3086a4254ab..eadc24dc5b4 100644 --- a/src/trace_processor/util/deobfuscation/deobfuscator_unittest.cc +++ b/src/trace_processor/util/deobfuscation/deobfuscator_unittest.cc @@ -19,6 +19,9 @@ #include #include +#include "protos/perfetto/trace/profiling/deobfuscation.pbzero.h" +#include "protos/perfetto/trace/trace.pbzero.h" +#include "protos/perfetto/trace/trace_packet.pbzero.h" #include "test/gtest_and_gmock.h" namespace perfetto::profiling { @@ -256,6 +259,219 @@ Example$$Class -> C: EXPECT_THAT(cls.deobfuscated_methods(), testing::IsEmpty()); } +TEST(ProguardParserTest, MergedClassesJsonComment) { + ProguardParser p; + const char input[] = R"( +com.example.Merged -> z: +# {"id":"com.android.tools.r8.mergedClasses", "merged_classes": [{ "name": "com.example.ClassA" }, { "name": "com.example.ClassB" }] } +)"; + + ASSERT_TRUE(p.AddLines(std::string(input))); + auto mapping = p.ConsumeMapping(); + ASSERT_THAT(mapping, ElementsAre(Pair("z", _))); + const auto& cls = mapping.find("z")->second; + EXPECT_EQ(cls.deobfuscated_name(), "com.example.Merged"); + + const auto& mcs = cls.merged_classes(); + EXPECT_TRUE(mcs.class_id_field_name.empty()); + ASSERT_EQ(mcs.merged_classes.size(), 2u); + + EXPECT_EQ(mcs.merged_classes[0].name, "com.example.ClassA"); + EXPECT_FALSE(mcs.merged_classes[0].class_id.has_value()); + EXPECT_TRUE( + mcs.merged_classes[0].nested_merged_classes.merged_classes.empty()); + + EXPECT_EQ(mcs.merged_classes[1].name, "com.example.ClassB"); + EXPECT_FALSE(mcs.merged_classes[1].class_id.has_value()); + EXPECT_TRUE( + mcs.merged_classes[1].nested_merged_classes.merged_classes.empty()); +} + +TEST(ProguardParserTest, MergedClassesJsonCommentClassIdTypes) { + ProguardParser p; + const char input[] = R"( +com.example.Merged -> z: +# {"id":"com.android.tools.r8.mergedClasses", "class_id_field": "$cid", "merged_classes": [{ "name": "ClassPos", "class_id": 100 }, { "name": "ClassNeg", "class_id": -10 }, { "name": "ClassZero", "class_id": 0 }, { "name": "ClassMax", "class_id": 2147483647 }, { "name": "ClassMin", "class_id": -2147483648 }, { "name": "ClassNoId" }] } +)"; + + ASSERT_TRUE(p.AddLines(std::string(input))); + auto mapping = p.ConsumeMapping(); + ASSERT_THAT(mapping, ElementsAre(Pair("z", _))); + const auto& cls = mapping.find("z")->second; + EXPECT_EQ(cls.deobfuscated_name(), "com.example.Merged"); + + const auto& mcs = cls.merged_classes(); + EXPECT_EQ(mcs.class_id_field_name, "$cid"); + ASSERT_EQ(mcs.merged_classes.size(), 6u); + + EXPECT_EQ(mcs.merged_classes[0].name, "ClassPos"); + EXPECT_EQ(mcs.merged_classes[0].class_id, 100); + + EXPECT_EQ(mcs.merged_classes[1].name, "ClassNeg"); + EXPECT_EQ(mcs.merged_classes[1].class_id, -10); + + EXPECT_EQ(mcs.merged_classes[2].name, "ClassZero"); + EXPECT_EQ(mcs.merged_classes[2].class_id, 0); + + EXPECT_EQ(mcs.merged_classes[3].name, "ClassMax"); + EXPECT_EQ(mcs.merged_classes[3].class_id, 2147483647); + + EXPECT_EQ(mcs.merged_classes[4].name, "ClassMin"); + EXPECT_EQ(mcs.merged_classes[4].class_id, -2147483648); + + EXPECT_EQ(mcs.merged_classes[5].name, "ClassNoId"); + EXPECT_FALSE(mcs.merged_classes[5].class_id.has_value()); +} + +TEST(ProguardParserTest, MergedClassesJsonCommentWithKindAndClassId) { + ProguardParser p; + const char input[] = R"( +com.example.ClassA -> a: +# {"id":"unrelated"} +# {"id":"com.android.tools.r8.mergedClasses", "class_id_field": "$cid", "kind": "horizontal", "merged_classes": [{ "name": "com.example.ClassA", "class_id": "0" }, { "name": "com.example.ClassB", "class_id": "1" }] } +)"; + + ASSERT_TRUE(p.AddLines(std::string(input))); + auto mapping = p.ConsumeMapping(); + ASSERT_THAT(mapping, ElementsAre(Pair("a", _))); + const auto& cls = mapping.find("a")->second; + EXPECT_EQ(cls.deobfuscated_name(), "com.example.ClassA"); + + const auto& mcs = cls.merged_classes(); + EXPECT_EQ(mcs.class_id_field_name, "$cid"); + ASSERT_EQ(mcs.merged_classes.size(), 2u); + + EXPECT_EQ(mcs.merged_classes[0].name, "com.example.ClassA"); + EXPECT_EQ(mcs.merged_classes[0].class_id, 0); + EXPECT_TRUE( + mcs.merged_classes[0].nested_merged_classes.merged_classes.empty()); + + EXPECT_EQ(mcs.merged_classes[1].name, "com.example.ClassB"); + EXPECT_EQ(mcs.merged_classes[1].class_id, 1); + EXPECT_TRUE( + mcs.merged_classes[1].nested_merged_classes.merged_classes.empty()); +} + +TEST(ProguardParserTest, MergedClassesJsonCommentNested) { + ProguardParser p; + const char input[] = R"( +com.example.Merged -> z: +# {"id":"com.android.tools.r8.mergedClasses", "class_id_field": "$cid$1", "kind": "horizontal", "merged_classes": [{ "class_id": "0", "class_id_field": "$cid", "merged_classes": [{ "name": "com.example.ClassA", "class_id": "0" }, { "name": "com.example.ClassB", "class_id": "1" }] }, { "name": "com.example.ClassC", "class_id": "1" }] } +)"; + + ASSERT_TRUE(p.AddLines(std::string(input))); + auto mapping = p.ConsumeMapping(); + ASSERT_THAT(mapping, ElementsAre(Pair("z", _))); + const auto& cls = mapping.find("z")->second; + EXPECT_EQ(cls.deobfuscated_name(), "com.example.Merged"); + + const auto& mcs = cls.merged_classes(); + EXPECT_EQ(mcs.class_id_field_name, "$cid$1"); + ASSERT_EQ(mcs.merged_classes.size(), 2u); + + const auto& mc0 = mcs.merged_classes[0]; + EXPECT_TRUE(mc0.name.empty()); + EXPECT_EQ(mc0.class_id, 0); + EXPECT_EQ(mc0.nested_merged_classes.class_id_field_name, "$cid"); + ASSERT_EQ(mc0.nested_merged_classes.merged_classes.size(), 2u); + + EXPECT_EQ(mc0.nested_merged_classes.merged_classes[0].name, + "com.example.ClassA"); + EXPECT_EQ(mc0.nested_merged_classes.merged_classes[0].class_id, 0); + EXPECT_TRUE(mc0.nested_merged_classes.merged_classes[0] + .nested_merged_classes.merged_classes.empty()); + + EXPECT_EQ(mc0.nested_merged_classes.merged_classes[1].name, + "com.example.ClassB"); + EXPECT_EQ(mc0.nested_merged_classes.merged_classes[1].class_id, 1); + EXPECT_TRUE(mc0.nested_merged_classes.merged_classes[1] + .nested_merged_classes.merged_classes.empty()); + + const auto& mc1 = mcs.merged_classes[1]; + EXPECT_EQ(mc1.name, "com.example.ClassC"); + EXPECT_EQ(mc1.class_id, 1); + EXPECT_TRUE(mc1.nested_merged_classes.merged_classes.empty()); +} + +TEST(ProguardParserTest, MergedClassesJsonCommentUnrelatedId) { + ProguardParser p; + const char input[] = R"( +com.example.Class -> a: +# {"id":"com.android.tools.r8.synthesized", "merged_classes": [{ "name": "com.example.Other", "class_id": "0" }] } +)"; + + ASSERT_TRUE(p.AddLines(std::string(input))); + auto mapping = p.ConsumeMapping(); + ASSERT_THAT(mapping, ElementsAre(Pair("a", _))); + const auto& cls = mapping.find("a")->second; + EXPECT_TRUE(cls.merged_classes().merged_classes.empty()); +} + +TEST(ProguardParserTest, MergedClassesJsonCommentSyntaxError) { + ProguardParser p; + const char input[] = R"( +com.example.ClassA -> a: +# {"id":"com.android.tools.r8.mergedClasses", "class_id_field": } +)"; + ASSERT_TRUE(p.AddLines(std::string(input))); + auto mapping = p.ConsumeMapping(); + ASSERT_THAT(mapping, ElementsAre(Pair("a", _))); + EXPECT_TRUE( + mapping.find("a")->second.merged_classes().merged_classes.empty()); +} + +TEST(ProguardParserTest, MergedClassesJsonCommentNonNumericClassId) { + ProguardParser p; + const char input[] = R"( +com.example.ClassB -> b: +# {"id":"com.android.tools.r8.mergedClasses", "merged_classes": [{ "name": "com.example.Sub", "class_id": "not_a_number" }] } +)"; + ASSERT_TRUE(p.AddLines(std::string(input))); + auto mapping = p.ConsumeMapping(); + ASSERT_THAT(mapping, ElementsAre(Pair("b", _))); + EXPECT_TRUE( + mapping.find("b")->second.merged_classes().merged_classes.empty()); +} + +TEST(ProguardParserTest, MergedClassesJsonCommentClassIdOutOfRange) { + ProguardParser p; + const char input[] = R"( +com.example.ClassC -> c: +# {"id":"com.android.tools.r8.mergedClasses", "merged_classes": [{ "name": "com.example.Sub", "class_id": 999999999999999999 }]} +)"; + ASSERT_TRUE(p.AddLines(std::string(input))); + auto mapping = p.ConsumeMapping(); + ASSERT_THAT(mapping, ElementsAre(Pair("c", _))); + EXPECT_TRUE( + mapping.find("c")->second.merged_classes().merged_classes.empty()); +} + +TEST(ProguardParserTest, MergedClassesJsonCommentInvalidClassName) { + ProguardParser p; + const char input[] = R"( +com.example.ClassD -> d: +# {"id":"com.android.tools.r8.mergedClasses", "merged_classes": [{ "name": 12345, "class_id": 1 }]} +)"; + ASSERT_TRUE(p.AddLines(std::string(input))); + auto mapping = p.ConsumeMapping(); + ASSERT_THAT(mapping, ElementsAre(Pair("d", _))); + EXPECT_TRUE( + mapping.find("d")->second.merged_classes().merged_classes.empty()); +} + +TEST(ProguardParserTest, MergedClassesJsonCommentNonArrayMergedClasses) { + ProguardParser p; + const char input[] = R"( +com.example.ClassE -> e: +# {"id":"com.android.tools.r8.mergedClasses", "merged_classes": "not_an_array"} +)"; + ASSERT_TRUE(p.AddLines(std::string(input))); + auto mapping = p.ConsumeMapping(); + ASSERT_THAT(mapping, ElementsAre(Pair("e", _))); + EXPECT_TRUE( + mapping.find("e")->second.merged_classes().merged_classes.empty()); +} + // ============================================================================= // R8 Retrace Compatibility Tests //