diff options
Diffstat (limited to 'tools')
58 files changed, 1093 insertions, 1162 deletions
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp index 0d6dc3522d24..7323b0f4c14f 100644 --- a/tools/aapt2/Android.bp +++ b/tools/aapt2/Android.bp @@ -47,6 +47,7 @@ cc_defaults { "-Wno-missing-field-initializers", "-fno-exceptions", "-fno-rtti", + "-Wno-deprecated-declarations", ], target: { windows: { diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp index 9cfb85d7cd73..6a17ef85a755 100644 --- a/tools/aapt2/Debug.cpp +++ b/tools/aapt2/Debug.cpp @@ -265,6 +265,16 @@ void Debug::PrintTable(const ResourceTable& table, const DebugPrintTableOptions& ValueHeadlinePrinter headline_printer(package.name, printer); ValueBodyPrinter body_printer(package.name, printer); + auto& dynamicRefTable = table.GetReferencedPackages(); + if (!dynamicRefTable.empty()) { + printer->Println(StringPrintf("DynamicRefTable entryCount=%d", int(dynamicRefTable.size()))); + printer->Indent(); + for (auto&& [id, name] : dynamicRefTable) { + printer->Println(StringPrintf("0x%02x -> %s", id, name.c_str())); + } + printer->Undent(); + } + printer->Print("Package name="); printer->Print(package.name); if (package.id) { diff --git a/tools/aapt2/LoadedApk.h b/tools/aapt2/LoadedApk.h index 4cd7eae0a5e2..27c354a7b08e 100644 --- a/tools/aapt2/LoadedApk.h +++ b/tools/aapt2/LoadedApk.h @@ -40,10 +40,8 @@ enum ApkFormat { }; // Info about an APK loaded in memory. -class LoadedApk { +class LoadedApk final { public: - virtual ~LoadedApk() = default; - // Loads both binary and proto APKs from disk. static std::unique_ptr<LoadedApk> LoadApkFromPath(android::StringPiece path, android::IDiagnostics* diag); @@ -96,8 +94,8 @@ class LoadedApk { * Writes the APK on disk at the given path, while also removing the resource * files that are not referenced in the resource table. */ - virtual bool WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options, - IArchiveWriter* writer); + bool WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options, + IArchiveWriter* writer); /** * Writes the APK on disk at the given path, while also removing the resource files that are not @@ -108,9 +106,9 @@ class LoadedApk { * original manifest will be written. The manifest is only required if the contents of the new APK * have been modified in a way that require the AndroidManifest.xml to also be modified. */ - virtual bool WriteToArchive(IAaptContext* context, ResourceTable* split_table, - const TableFlattenerOptions& options, FilterChain* filters, - IArchiveWriter* writer, xml::XmlResource* manifest = nullptr); + bool WriteToArchive(IAaptContext* context, ResourceTable* split_table, + const TableFlattenerOptions& options, FilterChain* filters, + IArchiveWriter* writer, xml::XmlResource* manifest = nullptr); /** Loads the file as an xml document. */ std::unique_ptr<xml::XmlResource> LoadXml(const std::string& file_path, diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp index fa9a98f136cb..6af39b739e9b 100644 --- a/tools/aapt2/ResourceParser.cpp +++ b/tools/aapt2/ResourceParser.cpp @@ -800,7 +800,7 @@ std::unique_ptr<Item> ResourceParser::ParseXml(const FlattenedXmlSubTree& xmlsub // Process the raw value. std::unique_ptr<Item> processed_item = ResourceUtils::TryParseItemForAttribute( - xmlsub_tree.raw_value, type_mask, on_create_reference); + &diag, xmlsub_tree.raw_value, type_mask, on_create_reference); if (processed_item) { // Fix up the reference. if (auto ref = ValueCast<Reference>(processed_item.get())) { diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h index bb286a8abdaa..61e399c7ab68 100644 --- a/tools/aapt2/ResourceTable.h +++ b/tools/aapt2/ResourceTable.h @@ -307,6 +307,11 @@ class ResourceTable { // order. ResourceTableView GetPartitionedView(const ResourceTableViewOptions& options = {}) const; + using ReferencedPackages = std::map<uint8_t, std::string>; + const ReferencedPackages& GetReferencedPackages() const { + return included_packages_; + } + struct SearchResult { ResourceTablePackage* package; ResourceTableType* type; @@ -342,7 +347,7 @@ class ResourceTable { // Set of dynamic packages that this table may reference. Their package names get encoded // into the resources.arsc along with their compile-time assigned IDs. - std::map<size_t, std::string> included_packages_; + ReferencedPackages included_packages_; private: DISALLOW_COPY_AND_ASSIGN(ResourceTable); diff --git a/tools/aapt2/ResourceUtils.cpp b/tools/aapt2/ResourceUtils.cpp index 5a118a902963..d358df98ada6 100644 --- a/tools/aapt2/ResourceUtils.cpp +++ b/tools/aapt2/ResourceUtils.cpp @@ -619,7 +619,7 @@ uint32_t AndroidTypeToAttributeTypeMask(uint16_t type) { } std::unique_ptr<Item> TryParseItemForAttribute( - StringPiece value, uint32_t type_mask, + android::IDiagnostics* diag, StringPiece value, uint32_t type_mask, const std::function<bool(const ResourceName&)>& on_create_reference) { using android::ResTable_map; @@ -670,8 +670,32 @@ std::unique_ptr<Item> TryParseItemForAttribute( // Try parsing this as a float. auto floating_point = TryParseFloat(value); if (floating_point) { + // Only check if the parsed result lost precision when the parsed item is + // android::Res_value::TYPE_FLOAT and there is other possible types saved in type_mask, like + // ResTable_map::TYPE_INTEGER. if (type_mask & AndroidTypeToAttributeTypeMask(floating_point->value.dataType)) { - return std::move(floating_point); + const bool mayOnlyBeFloat = (type_mask & ~float_mask) == 0; + const bool parsedAsFloat = floating_point->value.dataType == android::Res_value::TYPE_FLOAT; + if (!mayOnlyBeFloat && parsedAsFloat) { + float f = reinterpret_cast<float&>(floating_point->value.data); + std::u16string str16 = android::util::Utf8ToUtf16(util::TrimWhitespace(value)); + double d; + if (android::ResTable::stringToDouble(str16.data(), str16.size(), d)) { + // Parse as a float only if the difference between float and double parsed from the + // same string is smaller than 1, otherwise return as raw string. + if (fabs(f - d) < 1) { + return std::move(floating_point); + } else { + if (diag->IsVerbose()) { + diag->Note(android::DiagMessage() + << "precision lost greater than 1 while parsing float " << value + << ", return a raw string"); + } + } + } + } else { + return std::move(floating_point); + } } } } @@ -683,12 +707,12 @@ std::unique_ptr<Item> TryParseItemForAttribute( * allows. */ std::unique_ptr<Item> TryParseItemForAttribute( - StringPiece str, const Attribute* attr, + android::IDiagnostics* diag, StringPiece str, const Attribute* attr, const std::function<bool(const ResourceName&)>& on_create_reference) { using android::ResTable_map; const uint32_t type_mask = attr->type_mask; - auto value = TryParseItemForAttribute(str, type_mask, on_create_reference); + auto value = TryParseItemForAttribute(diag, str, type_mask, on_create_reference); if (value) { return value; } diff --git a/tools/aapt2/ResourceUtils.h b/tools/aapt2/ResourceUtils.h index f30f4acfec7a..50fc87900162 100644 --- a/tools/aapt2/ResourceUtils.h +++ b/tools/aapt2/ResourceUtils.h @@ -200,11 +200,11 @@ std::unique_ptr<BinaryPrimitive> TryParseFlagSymbol(const Attribute* enum_attr, * reference to an ID that must be created (@+id/foo). */ std::unique_ptr<Item> TryParseItemForAttribute( - android::StringPiece value, const Attribute* attr, + android::IDiagnostics* diag, android::StringPiece value, const Attribute* attr, const std::function<bool(const ResourceName&)>& on_create_reference = {}); std::unique_ptr<Item> TryParseItemForAttribute( - android::StringPiece value, uint32_t type_mask, + android::IDiagnostics* diag, android::StringPiece value, uint32_t type_mask, const std::function<bool(const ResourceName&)>& on_create_reference = {}); uint32_t AndroidTypeToAttributeTypeMask(uint16_t type); diff --git a/tools/aapt2/ResourceUtils_test.cpp b/tools/aapt2/ResourceUtils_test.cpp index 568871a4d66e..4cba04de72ee 100644 --- a/tools/aapt2/ResourceUtils_test.cpp +++ b/tools/aapt2/ResourceUtils_test.cpp @@ -217,17 +217,46 @@ TEST(ResourceUtilsTest, EmptyIsBinaryPrimitive) { } TEST(ResourceUtilsTest, ItemsWithWhitespaceAreParsedCorrectly) { - EXPECT_THAT(ResourceUtils::TryParseItemForAttribute(" 12\n ", ResTable_map::TYPE_INTEGER), + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + EXPECT_THAT(ResourceUtils::TryParseItemForAttribute(context->GetDiagnostics(), " 12\n ", + ResTable_map::TYPE_INTEGER), Pointee(ValueEq(BinaryPrimitive(Res_value::TYPE_INT_DEC, 12u)))); - EXPECT_THAT(ResourceUtils::TryParseItemForAttribute(" true\n ", ResTable_map::TYPE_BOOLEAN), + EXPECT_THAT(ResourceUtils::TryParseItemForAttribute(context->GetDiagnostics(), " true\n ", + ResTable_map::TYPE_BOOLEAN), Pointee(ValueEq(BinaryPrimitive(Res_value::TYPE_INT_BOOLEAN, 0xffffffffu)))); const float expected_float = 12.0f; const uint32_t expected_float_flattened = *(uint32_t*)&expected_float; - EXPECT_THAT(ResourceUtils::TryParseItemForAttribute(" 12.0\n ", ResTable_map::TYPE_FLOAT), + EXPECT_THAT(ResourceUtils::TryParseItemForAttribute(context->GetDiagnostics(), " 12.0\n ", + ResTable_map::TYPE_FLOAT), Pointee(ValueEq(BinaryPrimitive(Res_value::TYPE_FLOAT, expected_float_flattened)))); } +TEST(ResourceUtilsTest, FloatAndBigIntegerParsedCorrectly) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + const float expected_float = 0.125f; + const uint32_t expected_float_flattened = *(uint32_t*)&expected_float; + EXPECT_THAT(ResourceUtils::TryParseItemForAttribute(context->GetDiagnostics(), "0.125", + ResTable_map::TYPE_FLOAT), + Pointee(ValueEq(BinaryPrimitive(Res_value::TYPE_FLOAT, expected_float_flattened)))); + + const float special_float = 1.0f; + const uint32_t special_float_flattened = *(uint32_t*)&special_float; + EXPECT_THAT(ResourceUtils::TryParseItemForAttribute(context->GetDiagnostics(), "1.0", + ResTable_map::TYPE_FLOAT), + Pointee(ValueEq(BinaryPrimitive(Res_value::TYPE_FLOAT, special_float_flattened)))); + + EXPECT_EQ(ResourceUtils::TryParseItemForAttribute(context->GetDiagnostics(), "1099511627776", + ResTable_map::TYPE_INTEGER), + std::unique_ptr<Item>(nullptr)); + + const float big_float = 1099511627776.0f; + const uint32_t big_flattened = *(uint32_t*)&big_float; + EXPECT_THAT(ResourceUtils::TryParseItemForAttribute(context->GetDiagnostics(), "1099511627776", + ResTable_map::TYPE_FLOAT), + Pointee(ValueEq(BinaryPrimitive(Res_value::TYPE_FLOAT, big_flattened)))); +} + TEST(ResourceUtilsTest, ParseSdkVersionWithCodename) { EXPECT_THAT(ResourceUtils::ParseSdkVersion("Q"), Eq(std::optional<int>(10000))); EXPECT_THAT(ResourceUtils::ParseSdkVersion("Q.fingerprint"), Eq(std::optional<int>(10000))); diff --git a/tools/aapt2/ResourceValues.cpp b/tools/aapt2/ResourceValues.cpp index a5754e0d168f..166b01bd9154 100644 --- a/tools/aapt2/ResourceValues.cpp +++ b/tools/aapt2/ResourceValues.cpp @@ -439,6 +439,21 @@ static std::string ComplexToString(uint32_t complex_value, bool fraction) { return str; } +// This function is designed to using different specifier to print different floats, +// which can print more accurate format rather than using %g only. +const char* BinaryPrimitive::DecideFormat(float f) { + // if the float is either too big or too tiny, print it in scientific notation. + // eg: "10995116277760000000000" to 1.099512e+22, "0.00000000001" to 1.000000e-11 + if (fabs(f) > std::numeric_limits<int64_t>::max() || fabs(f) < 1e-10) { + return "%e"; + // Else if the number is an integer exactly, print it without trailing zeros. + // eg: "1099511627776" to 1099511627776 + } else if (int64_t(f) == f) { + return "%.0f"; + } + return "%g"; +} + void BinaryPrimitive::PrettyPrint(Printer* printer) const { using ::android::Res_value; switch (value.dataType) { @@ -470,7 +485,9 @@ void BinaryPrimitive::PrettyPrint(Printer* printer) const { break; case Res_value::TYPE_FLOAT: - printer->Print(StringPrintf("%g", *reinterpret_cast<const float*>(&value.data))); + float f; + f = *reinterpret_cast<const float*>(&value.data); + printer->Print(StringPrintf(DecideFormat(f), f)); break; case Res_value::TYPE_DIMENSION: diff --git a/tools/aapt2/ResourceValues.h b/tools/aapt2/ResourceValues.h index 6f9dccbd3bcc..5192c2be1f98 100644 --- a/tools/aapt2/ResourceValues.h +++ b/tools/aapt2/ResourceValues.h @@ -284,6 +284,7 @@ struct BinaryPrimitive : public TransformableItem<BinaryPrimitive, BaseItem<Bina bool Equals(const Value* value) const override; bool Flatten(android::Res_value* out_value) const override; void Print(std::ostream* out) const override; + static const char* DecideFormat(float f); void PrettyPrint(text::Printer* printer) const override; }; diff --git a/tools/aapt2/SdkConstants.cpp b/tools/aapt2/SdkConstants.cpp index a766bd437120..83f2eb31aa57 100644 --- a/tools/aapt2/SdkConstants.cpp +++ b/tools/aapt2/SdkConstants.cpp @@ -16,20 +16,24 @@ #include "SdkConstants.h" +#include <stdint.h> + #include <algorithm> #include <string> -#include <unordered_set> -#include <vector> +#include <string_view> using android::StringPiece; +using namespace std::literals; namespace aapt { -static ApiVersion sDevelopmentSdkLevel = 10000; -static const auto sDevelopmentSdkCodeNames = std::unordered_set<StringPiece>( - {"Q", "R", "S", "Sv2", "Tiramisu", "UpsideDownCake", "VanillaIceCream"}); +static constexpr ApiVersion sDevelopmentSdkLevel = 10000; +static constexpr StringPiece sDevelopmentSdkCodeNames[] = { + "Q"sv, "R"sv, "S"sv, "Sv2"sv, "Tiramisu"sv, "UpsideDownCake"sv, "VanillaIceCream"sv}; + +static constexpr auto sPrivacySandboxSuffix = "PrivacySandbox"sv; -static const std::vector<std::pair<uint16_t, ApiVersion>> sAttrIdMap = { +static constexpr std::pair<uint16_t, ApiVersion> sAttrIdMap[] = { {0x021c, 1}, {0x021d, 2}, {0x0269, SDK_CUPCAKE}, @@ -62,25 +66,37 @@ static const std::vector<std::pair<uint16_t, ApiVersion>> sAttrIdMap = { {0x064c, SDK_S_V2}, }; -static bool less_entry_id(const std::pair<uint16_t, ApiVersion>& p, uint16_t entryId) { - return p.first < entryId; -} +static_assert(std::is_sorted(std::begin(sAttrIdMap), std::end(sAttrIdMap), + [](auto&& l, auto&& r) { return l.first < r.first; })); ApiVersion FindAttributeSdkLevel(const ResourceId& id) { if (id.package_id() != 0x01 || id.type_id() != 0x01) { return 0; } - auto iter = std::lower_bound(sAttrIdMap.begin(), sAttrIdMap.end(), id.entry_id(), less_entry_id); - if (iter == sAttrIdMap.end()) { + const auto it = + std::lower_bound(std::begin(sAttrIdMap), std::end(sAttrIdMap), id.entry_id(), + [](const auto& pair, uint16_t entryId) { return pair.first < entryId; }); + if (it == std::end(sAttrIdMap)) { return SDK_LOLLIPOP_MR1; } - return iter->second; + return it->second; } std::optional<ApiVersion> GetDevelopmentSdkCodeNameVersion(StringPiece code_name) { - return (sDevelopmentSdkCodeNames.find(code_name) == sDevelopmentSdkCodeNames.end()) - ? std::optional<ApiVersion>() - : sDevelopmentSdkLevel; + const auto it = + std::find_if(std::begin(sDevelopmentSdkCodeNames), std::end(sDevelopmentSdkCodeNames), + [code_name](const auto& item) { return code_name.starts_with(item); }); + if (it == std::end(sDevelopmentSdkCodeNames)) { + return {}; + } + if (code_name.size() == it->size()) { + return sDevelopmentSdkLevel; + } + if (code_name.size() == it->size() + sPrivacySandboxSuffix.size() && + code_name.ends_with(sPrivacySandboxSuffix)) { + return sDevelopmentSdkLevel; + } + return {}; } } // namespace aapt diff --git a/tools/aapt2/SdkConstants_test.cpp b/tools/aapt2/SdkConstants_test.cpp index 61f4d71b7fb2..0f645ffc1e8b 100644 --- a/tools/aapt2/SdkConstants_test.cpp +++ b/tools/aapt2/SdkConstants_test.cpp @@ -28,4 +28,24 @@ TEST(SdkConstantsTest, NonFrameworkAttributeIsSdk0) { EXPECT_EQ(0, FindAttributeSdkLevel(ResourceId(0x7f010345))); } +TEST(SdkConstantsTest, GetDevelopmentSdkCodeNameVersionValid) { + EXPECT_EQ(std::optional<ApiVersion>(10000), GetDevelopmentSdkCodeNameVersion("Q")); + EXPECT_EQ(std::optional<ApiVersion>(10000), GetDevelopmentSdkCodeNameVersion("VanillaIceCream")); +} + +TEST(SdkConstantsTest, GetDevelopmentSdkCodeNameVersionPrivacySandbox) { + EXPECT_EQ(std::optional<ApiVersion>(10000), GetDevelopmentSdkCodeNameVersion("QPrivacySandbox")); + EXPECT_EQ(std::optional<ApiVersion>(10000), + GetDevelopmentSdkCodeNameVersion("VanillaIceCreamPrivacySandbox")); +} + +TEST(SdkConstantsTest, GetDevelopmentSdkCodeNameVersionInvalid) { + EXPECT_EQ(std::optional<ApiVersion>(), GetDevelopmentSdkCodeNameVersion("A")); + EXPECT_EQ(std::optional<ApiVersion>(), GetDevelopmentSdkCodeNameVersion("Sv3")); + EXPECT_EQ(std::optional<ApiVersion>(), + GetDevelopmentSdkCodeNameVersion("VanillaIceCream_PrivacySandbox")); + EXPECT_EQ(std::optional<ApiVersion>(), GetDevelopmentSdkCodeNameVersion("PrivacySandbox")); + EXPECT_EQ(std::optional<ApiVersion>(), GetDevelopmentSdkCodeNameVersion("QQQQQQQQQQQQQQQ")); +} + } // namespace aapt diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp index 03f9715fb265..b5c290ec8dad 100644 --- a/tools/aapt2/cmd/Compile.cpp +++ b/tools/aapt2/cmd/Compile.cpp @@ -186,7 +186,20 @@ static bool CompileTable(IAaptContext* context, const CompileOptions& options, // These are created as weak symbols, and are only generated from default // configuration // strings and plurals. - PseudolocaleGenerator pseudolocale_generator; + std::string grammatical_gender_values; + std::string grammatical_gender_ratio; + if (options.pseudo_localize_gender_values) { + grammatical_gender_values = options.pseudo_localize_gender_values.value(); + } else { + grammatical_gender_values = "f,m,n"; + } + if (options.pseudo_localize_gender_ratio) { + grammatical_gender_ratio = options.pseudo_localize_gender_ratio.value(); + } else { + grammatical_gender_ratio = "1.0"; + } + PseudolocaleGenerator pseudolocale_generator(grammatical_gender_values, + grammatical_gender_ratio); if (!pseudolocale_generator.Consume(context, &table)) { return false; } @@ -597,6 +610,7 @@ class CompileContext : public IAaptContext { void SetVerbose(bool val) { verbose_ = val; + diagnostics_->SetVerbose(val); } bool IsVerbose() override { diff --git a/tools/aapt2/cmd/Compile.h b/tools/aapt2/cmd/Compile.h index 14a730a1b1a0..22890fc53d5c 100644 --- a/tools/aapt2/cmd/Compile.h +++ b/tools/aapt2/cmd/Compile.h @@ -35,6 +35,8 @@ struct CompileOptions { std::optional<std::string> res_dir; std::optional<std::string> res_zip; std::optional<std::string> generate_text_symbols_path; + std::optional<std::string> pseudo_localize_gender_values; + std::optional<std::string> pseudo_localize_gender_ratio; std::optional<Visibility::Level> visibility; bool pseudolocalize = false; bool no_png_crunch = false; @@ -76,6 +78,15 @@ class CompileCommand : public Command { AddOptionalFlag("--source-path", "Sets the compiled resource file source file path to the given string.", &options_.source_path); + AddOptionalFlag("--pseudo-localize-gender-values", + "Sets the gender values to pick up for generating grammatical gender strings, " + "gender values should be f, m, or n, which are shortcuts for feminine, " + "masculine and neuter, and split with comma.", + &options_.pseudo_localize_gender_values); + AddOptionalFlag("--pseudo-localize-gender-ratio", + "Sets the ratio of resources to generate grammatical gender strings for. The " + "ratio has to be a float number between 0 and 1.", + &options_.pseudo_localize_gender_ratio); } int Action(const std::vector<std::string>& args) override; diff --git a/tools/aapt2/cmd/Compile_test.cpp b/tools/aapt2/cmd/Compile_test.cpp index 3464a7662c60..8880089d0e20 100644 --- a/tools/aapt2/cmd/Compile_test.cpp +++ b/tools/aapt2/cmd/Compile_test.cpp @@ -236,9 +236,24 @@ TEST_F(CompilerTest, DoNotTranslateTest) { // The first string (000) is translatable, the second is not // ar-XB uses "\u200F\u202E...\u202C\u200F" std::vector<std::string> expected_translatable = { - "000", "111", // default locale - "[000 one]", // en-XA - "\xE2\x80\x8F\xE2\x80\xAE" "000" "\xE2\x80\xAC\xE2\x80\x8F", // ar-XB + "(F)[000 one]", // en-XA-feminine + "(F)\xE2\x80\x8F\xE2\x80\xAE" + "000" + "\xE2\x80\xAC\xE2\x80\x8F", // ar-XB-feminine + "(M)[000 one]", // en-XA-masculine + "(M)\xE2\x80\x8F\xE2\x80\xAE" + "000" + "\xE2\x80\xAC\xE2\x80\x8F", // ar-XB-masculine + "(N)[000 one]", // en-XA-neuter + "(N)\xE2\x80\x8F\xE2\x80\xAE" + "000" + "\xE2\x80\xAC\xE2\x80\x8F", // ar-XB-neuter + "000", // default locale + "111", // default locale + "[000 one]", // en-XA + "\xE2\x80\x8F\xE2\x80\xAE" + "000" + "\xE2\x80\xAC\xE2\x80\x8F", // ar-XB }; AssertTranslations(this, "foo", expected_translatable); AssertTranslations(this, "foo_donottranslate", expected_translatable); diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp index 7381a85f4339..387dcfe2ddf3 100644 --- a/tools/aapt2/cmd/Convert.cpp +++ b/tools/aapt2/cmd/Convert.cpp @@ -425,6 +425,7 @@ int ConvertCommand::Action(const std::vector<std::string>& args) { if (force_sparse_encoding_) { table_flattener_options_.sparse_entries = SparseEntriesMode::Forced; } + table_flattener_options_.use_compact_entries = enable_compact_entries_; if (resources_config_path_) { if (!ExtractResourceConfig(*resources_config_path_, &context, table_flattener_options_)) { return 1; diff --git a/tools/aapt2/cmd/Convert.h b/tools/aapt2/cmd/Convert.h index 15fe11fd91cc..9452e588953e 100644 --- a/tools/aapt2/cmd/Convert.h +++ b/tools/aapt2/cmd/Convert.h @@ -46,6 +46,10 @@ class ConvertCommand : public Command { "This decreases APK size at the cost of resource retrieval performance.\n" "Applies sparse encoding to all resources regardless of minSdk.", &force_sparse_encoding_); + AddOptionalSwitch( + "--enable-compact-entries", + "This decreases APK size by using compact resource entries for simple data types.", + &enable_compact_entries_); AddOptionalSwitch("--keep-raw-values", android::base::StringPrintf("Preserve raw attribute values in xml files when using the" " '%s' output format", kOutputFormatBinary), @@ -85,6 +89,7 @@ class ConvertCommand : public Command { bool verbose_ = false; bool enable_sparse_encoding_ = false; bool force_sparse_encoding_ = false; + bool enable_compact_entries_ = false; std::optional<std::string> resources_config_path_; }; diff --git a/tools/aapt2/cmd/Dump.cpp b/tools/aapt2/cmd/Dump.cpp index 71b08022f688..864af06f187e 100644 --- a/tools/aapt2/cmd/Dump.cpp +++ b/tools/aapt2/cmd/Dump.cpp @@ -112,6 +112,7 @@ class DumpContext : public IAaptContext { void SetVerbose(bool val) { verbose_ = val; + diagnostics_.SetVerbose(val); } int GetMinSdkVersion() override { diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 97404fc69af2..eb4e38c5f35f 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -148,6 +148,7 @@ class LinkContext : public IAaptContext { void SetVerbose(bool val) { verbose_ = val; + diagnostics_->SetVerbose(val); } int GetMinSdkVersion() override { diff --git a/tools/aapt2/cmd/Link_test.cpp b/tools/aapt2/cmd/Link_test.cpp index 725a1b86f616..7096f5cc54e3 100644 --- a/tools/aapt2/cmd/Link_test.cpp +++ b/tools/aapt2/cmd/Link_test.cpp @@ -441,8 +441,8 @@ static void BuildNonFinalizedSDK(const std::string& apk_path, const std::string& R"(<resources> <public type="attr" name="finalized_res" id="0x01010001"/> - <!-- S staged attributes (support staged resources in the same type id) --> - <staging-public-group type="attr" first-id="0x01010050"> + <!-- S staged attributes (Not support staged resources in the same type id) --> + <staging-public-group type="attr" first-id="0x01fc0050"> <public name="staged_s_res" /> </staging-public-group> @@ -480,8 +480,8 @@ static void BuildFinalizedSDK(const std::string& apk_path, const std::string& ja <public type="attr" name="staged_s2_res" id="0x01010003"/> <public type="string" name="staged_s_string" id="0x01020000"/> - <!-- S staged attributes (support staged resources in the same type id) --> - <staging-public-group-final type="attr" first-id="0x01010050"> + <!-- S staged attributes (Not support staged resources in the same type id) --> + <staging-public-group-final type="attr" first-id="0x01fc0050"> <public name="staged_s_res" /> </staging-public-group-final> @@ -551,7 +551,7 @@ TEST_F(LinkTest, StagedAndroidApi) { EXPECT_THAT(android_r_contents, HasSubstr("public static final int finalized_res=0x01010001;")); EXPECT_THAT( android_r_contents, - HasSubstr("public static final int staged_s_res; static { staged_s_res=0x01010050; }")); + HasSubstr("public static final int staged_s_res; static { staged_s_res=0x01fc0050; }")); EXPECT_THAT( android_r_contents, HasSubstr("public static final int staged_s_string; static { staged_s_string=0x01fd0080; }")); @@ -583,7 +583,7 @@ TEST_F(LinkTest, StagedAndroidApi) { result = am.GetResourceId("android:attr/staged_s_res"); ASSERT_TRUE(result.has_value()); - EXPECT_THAT(*result, Eq(0x01010050)); + EXPECT_THAT(*result, Eq(0x01fc0050)); result = am.GetResourceId("android:string/staged_s_string"); ASSERT_TRUE(result.has_value()); diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index dbe79701bf5c..f045dad6d11a 100644 --- a/tools/aapt2/cmd/Optimize.cpp +++ b/tools/aapt2/cmd/Optimize.cpp @@ -101,6 +101,7 @@ class OptimizeContext : public IAaptContext { void SetVerbose(bool val) { verbose_ = val; + diagnostics_.SetVerbose(val); } void SetMinSdkVersion(int sdk_version) { diff --git a/tools/aapt2/compile/IdAssigner.cpp b/tools/aapt2/compile/IdAssigner.cpp index b3f98a9d3e30..5421abde3689 100644 --- a/tools/aapt2/compile/IdAssigner.cpp +++ b/tools/aapt2/compile/IdAssigner.cpp @@ -37,6 +37,7 @@ using Result = expected<T, std::string>; template <typename Id, typename Key> struct NextIdFinder { + std::map<Id, Key> pre_assigned_ids_; explicit NextIdFinder(Id start_id = 0u) : next_id_(start_id){}; // Attempts to reserve an identifier for the specified key. @@ -55,7 +56,6 @@ struct NextIdFinder { Id next_id_; bool next_id_called_ = false; bool exhausted_ = false; - std::map<Id, Key> pre_assigned_ids_; typename std::map<Id, Key>::iterator next_preassigned_id_; }; @@ -158,7 +158,7 @@ bool IdAssigner::Consume(IAaptContext* context, ResourceTable* table) { } if (assigned_id_map_) { - // Reserve all the IDs mentioned in the stable ID map. That way we won't assig IDs that were + // Reserve all the IDs mentioned in the stable ID map. That way we won't assign IDs that were // listed in the map if they don't exist in the table. for (const auto& stable_id_entry : *assigned_id_map_) { const ResourceName& pre_assigned_name = stable_id_entry.first; @@ -191,6 +191,11 @@ bool IdAssigner::Consume(IAaptContext* context, ResourceTable* table) { } namespace { +static const std::string_view staged_type_overlap_error = + "Staged public resource type IDs have conflict with non staged public resources type " + "IDs, please restart staged resource type ID assignment at 0xff in public-staging.xml " + "and also delete all the overlapping groups in public-final.xml"; + template <typename Id, typename Key> Result<Id> NextIdFinder<Id, Key>::ReserveId(Key key, Id id) { CHECK(!next_id_called_) << "ReserveId cannot be called after NextId"; @@ -282,8 +287,20 @@ bool IdAssignerContext::ReserveId(const ResourceName& name, ResourceId id, // another type. auto assign_result = type_id_finder_.ReserveId(key, id.type_id()); if (!assign_result.has_value()) { - diag->Error(android::DiagMessage() << "can't assign ID " << id << " to resource " << name - << " because type " << assign_result.error()); + auto pre_assigned_type = type_id_finder_.pre_assigned_ids_[id.type_id()].type; + bool pre_assigned_type_staged = + non_staged_type_ids_.find(pre_assigned_type) == non_staged_type_ids_.end(); + auto hex_type_id = fmt::format("{:#04x}", (int)id.type_id()); + bool current_type_staged = visibility.staged_api; + diag->Error(android::DiagMessage() + << "can't assign type ID " << hex_type_id << " to " + << (current_type_staged ? "staged type " : "non staged type ") << name.type.type + << " because this type ID have been assigned to " + << (pre_assigned_type_staged ? "staged type " : "non staged type ") + << pre_assigned_type); + if (pre_assigned_type_staged || current_type_staged) { + diag->Error(android::DiagMessage() << staged_type_overlap_error); + } return false; } type = types_.emplace(key, TypeGroup(package_id_, id.type_id())).first; @@ -298,6 +315,20 @@ bool IdAssignerContext::ReserveId(const ResourceName& name, ResourceId id, << " because type already has ID " << std::hex << (int)id.type_id()); return false; } + } else { + // Ensure that staged public resources cannot have the same type name and type id with + // non staged public resources. + auto non_staged_type = non_staged_type_ids_.find(name.type.type); + if (non_staged_type != non_staged_type_ids_.end() && non_staged_type->second == id.type_id()) { + diag->Error( + android::DiagMessage() + << "can`t assign type ID " << fmt::format("{:#04x}", (int)id.type_id()) + << " to staged type " << name.type.type << " because type ID " + << fmt::format("{:#04x}", (int)id.type_id()) + << " already has been assigned to a non staged resource type with the same type name"); + diag->Error(android::DiagMessage() << staged_type_overlap_error); + return false; + } } auto assign_result = type->second.ReserveId(name, id); diff --git a/tools/aapt2/compile/IdAssigner_test.cpp b/tools/aapt2/compile/IdAssigner_test.cpp index 8911dad39470..ce45b7c1df04 100644 --- a/tools/aapt2/compile/IdAssigner_test.cpp +++ b/tools/aapt2/compile/IdAssigner_test.cpp @@ -117,14 +117,28 @@ TEST_F(IdAssignerTests, FailWhenTypeHasTwoNonStagedIds) { } TEST_F(IdAssignerTests, FailWhenTypeHasTwoNonStagedIdsRegardlessOfStagedId) { - auto table = test::ResourceTableBuilder() - .AddSimple("android:attr/foo", ResourceId(0x01050000)) - .AddSimple("android:attr/bar", ResourceId(0x01ff0006)) - .Add(NewResourceBuilder("android:attr/staged_baz") - .SetId(0x01ff0000) - .SetVisibility({.staged_api = true}) - .Build()) - .Build(); + auto table = + test::ResourceTableBuilder() + .AddSimple("android:attr/foo", ResourceId(0x01050000)) + .AddSimple("android:attr/bar", ResourceId(0x01ff0006)) + .Add(NewResourceBuilder("android:attr/staged_baz") + .SetId(0x01ff0000) + .SetVisibility({.staged_api = true, .level = Visibility::Level::kPublic}) + .Build()) + .Build(); + IdAssigner assigner; + ASSERT_FALSE(assigner.Consume(context.get(), table.get())); +} + +TEST_F(IdAssignerTests, FailWhenTypeHaveBothStagedAndNonStagedIds) { + auto table = + test::ResourceTableBuilder() + .AddSimple("android:attr/foo", ResourceId(0x01010000)) + .Add(NewResourceBuilder("android:bool/staged_baz") + .SetId(0x01010001) + .SetVisibility({.staged_api = true, .level = Visibility::Level::kPublic}) + .Build()) + .Build(); IdAssigner assigner; ASSERT_FALSE(assigner.Consume(context.get(), table.get())); } diff --git a/tools/aapt2/compile/PseudolocaleGenerator.cpp b/tools/aapt2/compile/PseudolocaleGenerator.cpp index 09a8560f984a..8143052f4376 100644 --- a/tools/aapt2/compile/PseudolocaleGenerator.cpp +++ b/tools/aapt2/compile/PseudolocaleGenerator.cpp @@ -16,11 +16,15 @@ #include "compile/PseudolocaleGenerator.h" +#include <stdint.h> + #include <algorithm> +#include <random> #include "ResourceTable.h" #include "ResourceValues.h" #include "ValueVisitor.h" +#include "androidfw/ResourceTypes.h" #include "androidfw/Util.h" #include "compile/Pseudolocalizer.h" #include "util/Util.h" @@ -293,8 +297,85 @@ class Visitor : public ValueVisitor { Pseudolocalizer localizer_; }; +class GrammaticalGenderVisitor : public ValueVisitor { + public: + std::unique_ptr<Value> value; + std::unique_ptr<Item> item; + + GrammaticalGenderVisitor(android::StringPool* pool, uint8_t grammaticalInflection) + : pool_(pool), grammaticalInflection_(grammaticalInflection) { + } + + void Visit(Plural* plural) override { + CloningValueTransformer cloner(pool_); + std::unique_ptr<Plural> grammatical_gendered = util::make_unique<Plural>(); + for (size_t i = 0; i < plural->values.size(); i++) { + if (plural->values[i]) { + GrammaticalGenderVisitor sub_visitor(pool_, grammaticalInflection_); + plural->values[i]->Accept(&sub_visitor); + if (sub_visitor.item) { + grammatical_gendered->values[i] = std::move(sub_visitor.item); + } else { + grammatical_gendered->values[i] = plural->values[i]->Transform(cloner); + } + } + } + grammatical_gendered->SetSource(plural->GetSource()); + grammatical_gendered->SetWeak(true); + value = std::move(grammatical_gendered); + } + + std::string AddGrammaticalGenderPrefix(const std::string_view& original_string) { + std::string result; + switch (grammaticalInflection_) { + case android::ResTable_config::GRAMMATICAL_GENDER_MASCULINE: + result = std::string("(M)") + std::string(original_string); + break; + case android::ResTable_config::GRAMMATICAL_GENDER_FEMININE: + result = std::string("(F)") + std::string(original_string); + break; + case android::ResTable_config::GRAMMATICAL_GENDER_NEUTER: + result = std::string("(N)") + std::string(original_string); + break; + default: + result = std::string(original_string); + break; + } + return result; + } + + void Visit(String* string) override { + std::string prefixed_string = AddGrammaticalGenderPrefix(std::string(*string->value)); + std::unique_ptr<String> grammatical_gendered = + util::make_unique<String>(pool_->MakeRef(prefixed_string)); + grammatical_gendered->SetSource(string->GetSource()); + grammatical_gendered->SetWeak(true); + item = std::move(grammatical_gendered); + } + + void Visit(StyledString* string) override { + std::string prefixed_string = AddGrammaticalGenderPrefix(std::string(string->value->value)); + android::StyleString new_string; + new_string.str = std::move(prefixed_string); + for (const android::StringPool::Span& span : string->value->spans) { + new_string.spans.emplace_back(android::Span{*span.name, span.first_char, span.last_char}); + } + std::unique_ptr<StyledString> grammatical_gendered = + util::make_unique<StyledString>(pool_->MakeRef(new_string)); + grammatical_gendered->SetSource(string->GetSource()); + grammatical_gendered->SetWeak(true); + item = std::move(grammatical_gendered); + } + + private: + DISALLOW_COPY_AND_ASSIGN(GrammaticalGenderVisitor); + android::StringPool* pool_; + uint8_t grammaticalInflection_; +}; + ConfigDescription ModifyConfigForPseudoLocale(const ConfigDescription& base, - Pseudolocalizer::Method m) { + Pseudolocalizer::Method m, + uint8_t grammaticalInflection) { ConfigDescription modified = base; switch (m) { case Pseudolocalizer::Method::kAccent: @@ -313,12 +394,64 @@ ConfigDescription ModifyConfigForPseudoLocale(const ConfigDescription& base, default: break; } + modified.grammaticalInflection = grammaticalInflection; return modified; } +void GrammaticalGender(ResourceConfigValue* original_value, + ResourceConfigValue* localized_config_value, android::StringPool* pool, + ResourceEntry* entry, const Pseudolocalizer::Method method, + uint8_t grammaticalInflection) { + GrammaticalGenderVisitor visitor(pool, grammaticalInflection); + localized_config_value->value->Accept(&visitor); + + std::unique_ptr<Value> grammatical_gendered_value; + if (visitor.value) { + grammatical_gendered_value = std::move(visitor.value); + } else if (visitor.item) { + grammatical_gendered_value = std::move(visitor.item); + } + if (!grammatical_gendered_value) { + return; + } + + ConfigDescription config = + ModifyConfigForPseudoLocale(original_value->config, method, grammaticalInflection); + + ResourceConfigValue* grammatical_gendered_config_value = + entry->FindOrCreateValue(config, original_value->product); + if (!grammatical_gendered_config_value->value) { + // Only use auto-generated pseudo-localization if none is defined. + grammatical_gendered_config_value->value = std::move(grammatical_gendered_value); + } +} + +const uint32_t MASK_MASCULINE = 1; // Bit mask for masculine +const uint32_t MASK_FEMININE = 2; // Bit mask for feminine +const uint32_t MASK_NEUTER = 4; // Bit mask for neuter + +void GrammaticalGenderIfNeeded(ResourceConfigValue* original_value, ResourceConfigValue* new_value, + android::StringPool* pool, ResourceEntry* entry, + const Pseudolocalizer::Method method, uint32_t gender_state) { + if (gender_state & MASK_FEMININE) { + GrammaticalGender(original_value, new_value, pool, entry, method, + android::ResTable_config::GRAMMATICAL_GENDER_FEMININE); + } + + if (gender_state & MASK_MASCULINE) { + GrammaticalGender(original_value, new_value, pool, entry, method, + android::ResTable_config::GRAMMATICAL_GENDER_MASCULINE); + } + + if (gender_state & MASK_NEUTER) { + GrammaticalGender(original_value, new_value, pool, entry, method, + android::ResTable_config::GRAMMATICAL_GENDER_NEUTER); + } +} + void PseudolocalizeIfNeeded(const Pseudolocalizer::Method method, ResourceConfigValue* original_value, android::StringPool* pool, - ResourceEntry* entry) { + ResourceEntry* entry, uint32_t gender_state, bool gender_flag) { Visitor visitor(pool, method); original_value->value->Accept(&visitor); @@ -333,8 +466,8 @@ void PseudolocalizeIfNeeded(const Pseudolocalizer::Method method, return; } - ConfigDescription config_with_accent = - ModifyConfigForPseudoLocale(original_value->config, method); + ConfigDescription config_with_accent = ModifyConfigForPseudoLocale( + original_value->config, method, android::ResTable_config::GRAMMATICAL_GENDER_ANY); ResourceConfigValue* new_config_value = entry->FindOrCreateValue(config_with_accent, original_value->product); @@ -342,6 +475,9 @@ void PseudolocalizeIfNeeded(const Pseudolocalizer::Method method, // Only use auto-generated pseudo-localization if none is defined. new_config_value->value = std::move(localized_value); } + if (gender_flag) { + GrammaticalGenderIfNeeded(original_value, new_config_value, pool, entry, method, gender_state); + } } // A value is pseudolocalizable if it does not define a locale (or is the default locale) and is @@ -356,16 +492,71 @@ static bool IsPseudolocalizable(ResourceConfigValue* config_value) { } // namespace +bool ParseGenderValuesAndSaveState(const std::string& grammatical_gender_values, + uint32_t* gender_state, android::IDiagnostics* diag) { + std::vector<std::string> values = util::SplitAndLowercase(grammatical_gender_values, ','); + for (size_t i = 0; i < values.size(); i++) { + if (values[i].length() != 0) { + if (values[i] == "f") { + *gender_state |= MASK_FEMININE; + } else if (values[i] == "m") { + *gender_state |= MASK_MASCULINE; + } else if (values[i] == "n") { + *gender_state |= MASK_NEUTER; + } else { + diag->Error(android::DiagMessage() << "Invalid grammatical gender value: " << values[i]); + return false; + } + } + } + return true; +} + +bool ParseGenderRatio(const std::string& grammatical_gender_ratio, float* gender_ratio, + android::IDiagnostics* diag) { + const char* input = grammatical_gender_ratio.c_str(); + char* endPtr; + errno = 0; + *gender_ratio = strtof(input, &endPtr); + if (endPtr == input || *endPtr != '\0' || errno == ERANGE || *gender_ratio < 0 || + *gender_ratio > 1) { + diag->Error(android::DiagMessage() + << "Invalid grammatical gender ratio: " << grammatical_gender_ratio + << ", must be a real number between 0 and 1"); + return false; + } + return true; +} + bool PseudolocaleGenerator::Consume(IAaptContext* context, ResourceTable* table) { + uint32_t gender_state = 0; + if (!ParseGenderValuesAndSaveState(grammatical_gender_values_, &gender_state, + context->GetDiagnostics())) { + return false; + } + + float gender_ratio = 0; + if (!ParseGenderRatio(grammatical_gender_ratio_, &gender_ratio, context->GetDiagnostics())) { + return false; + } + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution<> distrib(0.0, 1.0); + for (auto& package : table->packages) { for (auto& type : package->types) { for (auto& entry : type->entries) { + bool gender_flag = false; + if (distrib(gen) < gender_ratio) { + gender_flag = true; + } std::vector<ResourceConfigValue*> values = entry->FindValuesIf(IsPseudolocalizable); for (ResourceConfigValue* value : values) { PseudolocalizeIfNeeded(Pseudolocalizer::Method::kAccent, value, &table->string_pool, - entry.get()); + entry.get(), gender_state, gender_flag); PseudolocalizeIfNeeded(Pseudolocalizer::Method::kBidi, value, &table->string_pool, - entry.get()); + entry.get(), gender_state, gender_flag); } } } diff --git a/tools/aapt2/compile/PseudolocaleGenerator.h b/tools/aapt2/compile/PseudolocaleGenerator.h index 44e6e3e86f92..ce92008cdba1 100644 --- a/tools/aapt2/compile/PseudolocaleGenerator.h +++ b/tools/aapt2/compile/PseudolocaleGenerator.h @@ -27,8 +27,19 @@ std::unique_ptr<StyledString> PseudolocalizeStyledString(StyledString* string, Pseudolocalizer::Method method, android::StringPool* pool); -struct PseudolocaleGenerator : public IResourceTableConsumer { - bool Consume(IAaptContext* context, ResourceTable* table) override; +class PseudolocaleGenerator : public IResourceTableConsumer { + public: + explicit PseudolocaleGenerator(std::string grammatical_gender_values, + std::string grammatical_gender_ratio) + : grammatical_gender_values_(std::move(grammatical_gender_values)), + grammatical_gender_ratio_(std::move(grammatical_gender_ratio)) { + } + + bool Consume(IAaptContext* context, ResourceTable* table); + + private: + std::string grammatical_gender_values_; + std::string grammatical_gender_ratio_; }; } // namespace aapt diff --git a/tools/aapt2/compile/PseudolocaleGenerator_test.cpp b/tools/aapt2/compile/PseudolocaleGenerator_test.cpp index 2f90cbf722c2..1477ebf8473d 100644 --- a/tools/aapt2/compile/PseudolocaleGenerator_test.cpp +++ b/tools/aapt2/compile/PseudolocaleGenerator_test.cpp @@ -197,7 +197,7 @@ TEST(PseudolocaleGeneratorTest, PseudolocalizeOnlyDefaultConfigs) { val->SetTranslatable(false); std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); - PseudolocaleGenerator generator; + PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0")); ASSERT_TRUE(generator.Consume(context.get(), table.get())); // Normal pseudolocalization should take place. @@ -249,7 +249,7 @@ TEST(PseudolocaleGeneratorTest, PluralsArePseudolocalized) { expected->values = {util::make_unique<String>(table->string_pool.MakeRef("[žéŕö one]")), util::make_unique<String>(table->string_pool.MakeRef("[öñé one]"))}; - PseudolocaleGenerator generator; + PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0")); ASSERT_TRUE(generator.Consume(context.get(), table.get())); const auto* actual = test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo", @@ -287,7 +287,7 @@ TEST(PseudolocaleGeneratorTest, RespectUntranslateableSections) { context->GetDiagnostics())); } - PseudolocaleGenerator generator; + PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0")); ASSERT_TRUE(generator.Consume(context.get(), table.get())); StyledString* new_styled_string = test::GetValueForConfig<StyledString>( @@ -305,4 +305,213 @@ TEST(PseudolocaleGeneratorTest, RespectUntranslateableSections) { EXPECT_NE(std::string::npos, new_string->value->find("world")); } +TEST(PseudolocaleGeneratorTest, PseudolocalizeGrammaticalGenderForString) { + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder().AddString("android:string/foo", "foo").Build(); + + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0")); + ASSERT_TRUE(generator.Consume(context.get(), table.get())); + + String* locale = test::GetValueForConfig<String>(table.get(), "android:string/foo", + test::ParseConfigOrDie("en-rXA")); + ASSERT_NE(nullptr, locale); + + // Grammatical gendered string + auto config_feminine = test::ParseConfigOrDie("en-rXA-feminine"); + config_feminine.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + String* feminine = + test::GetValueForConfig<String>(table.get(), "android:string/foo", config_feminine); + ASSERT_NE(nullptr, feminine); + EXPECT_EQ(std::string("(F)") + *locale->value, *feminine->value); + + auto config_masculine = test::ParseConfigOrDie("en-rXA-masculine"); + config_masculine.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + String* masculine = + test::GetValueForConfig<String>(table.get(), "android:string/foo", config_masculine); + ASSERT_NE(nullptr, masculine); + EXPECT_EQ(std::string("(M)") + *locale->value, *masculine->value); + + auto config_neuter = test::ParseConfigOrDie("en-rXA-neuter"); + config_neuter.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + String* neuter = + test::GetValueForConfig<String>(table.get(), "android:string/foo", config_neuter); + ASSERT_NE(nullptr, neuter); + EXPECT_EQ(std::string("(N)") + *locale->value, *neuter->value); +} + +TEST(PseudolocaleGeneratorTest, PseudolocalizeGrammaticalGenderForPlural) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder().Build(); + std::unique_ptr<Plural> plural = util::make_unique<Plural>(); + plural->values = {util::make_unique<String>(table->string_pool.MakeRef("zero")), + util::make_unique<String>(table->string_pool.MakeRef("one"))}; + ASSERT_TRUE(table->AddResource(NewResourceBuilder(test::ParseNameOrDie("com.pkg:plurals/foo")) + .SetValue(std::move(plural)) + .Build(), + context->GetDiagnostics())); + PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0")); + ASSERT_TRUE(generator.Consume(context.get(), table.get())); + + Plural* actual = test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo", + test::ParseConfigOrDie("en-rXA")); + ASSERT_NE(nullptr, actual); + + // Grammatical gendered Plural + auto config_feminine = test::ParseConfigOrDie("en-rXA-feminine"); + config_feminine.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + Plural* actual_feminine = + test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo", config_feminine); + for (size_t i = 0; i < actual->values.size(); i++) { + if (actual->values[i]) { + String* locale = ValueCast<String>(actual->values[i].get()); + String* feminine = ValueCast<String>(actual_feminine->values[i].get()); + EXPECT_EQ(std::string("(F)") + *locale->value, *feminine->value); + } + } + + auto config_masculine = test::ParseConfigOrDie("en-rXA-masculine"); + config_masculine.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + Plural* actual_masculine = + test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo", config_masculine); + ASSERT_NE(nullptr, actual_masculine); + for (size_t i = 0; i < actual->values.size(); i++) { + if (actual->values[i]) { + String* locale = ValueCast<String>(actual->values[i].get()); + String* masculine = ValueCast<String>(actual_masculine->values[i].get()); + EXPECT_EQ(std::string("(M)") + *locale->value, *masculine->value); + } + } + + auto config_neuter = test::ParseConfigOrDie("en-rXA-neuter"); + config_neuter.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + Plural* actual_neuter = + test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo", config_neuter); + for (size_t i = 0; i < actual->values.size(); i++) { + if (actual->values[i]) { + String* locale = ValueCast<String>(actual->values[i].get()); + String* neuter = ValueCast<String>(actual_neuter->values[i].get()); + EXPECT_EQ(std::string("(N)") + *locale->value, *neuter->value); + } + } +} + +TEST(PseudolocaleGeneratorTest, PseudolocalizeGrammaticalGenderForStyledString) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder().Build(); + android::StyleString original_style; + original_style.str = "Hello world!"; + original_style.spans = {android::Span{"i", 1, 10}}; + + std::unique_ptr<StyledString> original = + util::make_unique<StyledString>(table->string_pool.MakeRef(original_style)); + ASSERT_TRUE(table->AddResource(NewResourceBuilder(test::ParseNameOrDie("android:string/foo")) + .SetValue(std::move(original)) + .Build(), + context->GetDiagnostics())); + PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0")); + ASSERT_TRUE(generator.Consume(context.get(), table.get())); + + StyledString* locale = test::GetValueForConfig<StyledString>(table.get(), "android:string/foo", + test::ParseConfigOrDie("en-rXA")); + ASSERT_NE(nullptr, locale); + EXPECT_EQ(1, locale->value->spans.size()); + EXPECT_EQ(std::string("i"), *locale->value->spans[0].name); + + // Grammatical gendered StyledString + auto config_feminine = test::ParseConfigOrDie("en-rXA-feminine"); + config_feminine.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + StyledString* feminine = + test::GetValueForConfig<StyledString>(table.get(), "android:string/foo", config_feminine); + ASSERT_NE(nullptr, feminine); + EXPECT_EQ(1, feminine->value->spans.size()); + EXPECT_EQ(std::string("i"), *feminine->value->spans[0].name); + EXPECT_EQ(std::string("(F)") + locale->value->value, feminine->value->value); + + auto config_masculine = test::ParseConfigOrDie("en-rXA-masculine"); + config_masculine.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + StyledString* masculine = + test::GetValueForConfig<StyledString>(table.get(), "android:string/foo", config_masculine); + ASSERT_NE(nullptr, masculine); + EXPECT_EQ(1, masculine->value->spans.size()); + EXPECT_EQ(std::string("i"), *masculine->value->spans[0].name); + EXPECT_EQ(std::string("(M)") + locale->value->value, masculine->value->value); + + auto config_neuter = test::ParseConfigOrDie("en-rXA-neuter"); + config_neuter.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + StyledString* neuter = + test::GetValueForConfig<StyledString>(table.get(), "android:string/foo", config_neuter); + ASSERT_NE(nullptr, neuter); + EXPECT_EQ(1, neuter->value->spans.size()); + EXPECT_EQ(std::string("i"), *neuter->value->spans[0].name); + EXPECT_EQ(std::string("(N)") + locale->value->value, neuter->value->value); +} + +TEST(PseudolocaleGeneratorTest, GrammaticalGenderForCertainValues) { + // single gender value + std::unique_ptr<ResourceTable> table_0 = + test::ResourceTableBuilder().AddString("android:string/foo", "foo").Build(); + + std::unique_ptr<IAaptContext> context_0 = test::ContextBuilder().Build(); + PseudolocaleGenerator generator_0(std::string("f"), std::string("1.0")); + ASSERT_TRUE(generator_0.Consume(context_0.get(), table_0.get())); + + String* locale_0 = test::GetValueForConfig<String>(table_0.get(), "android:string/foo", + test::ParseConfigOrDie("en-rXA")); + ASSERT_NE(nullptr, locale_0); + + auto config_feminine = test::ParseConfigOrDie("en-rXA-feminine"); + config_feminine.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + String* feminine_0 = + test::GetValueForConfig<String>(table_0.get(), "android:string/foo", config_feminine); + ASSERT_NE(nullptr, feminine_0); + EXPECT_EQ(std::string("(F)") + *locale_0->value, *feminine_0->value); + + auto config_masculine = test::ParseConfigOrDie("en-rXA-masculine"); + config_masculine.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + String* masculine_0 = + test::GetValueForConfig<String>(table_0.get(), "android:string/foo", config_masculine); + EXPECT_EQ(nullptr, masculine_0); + + auto config_neuter = test::ParseConfigOrDie("en-rXA-neuter"); + config_neuter.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + String* neuter_0 = + test::GetValueForConfig<String>(table_0.get(), "android:string/foo", config_neuter); + EXPECT_EQ(nullptr, neuter_0); + + // multiple gender values + std::unique_ptr<ResourceTable> table_1 = + test::ResourceTableBuilder().AddString("android:string/foo", "foo").Build(); + + std::unique_ptr<IAaptContext> context_1 = test::ContextBuilder().Build(); + PseudolocaleGenerator generator_1(std::string("f,n"), std::string("1.0")); + ASSERT_TRUE(generator_1.Consume(context_1.get(), table_1.get())); + + String* locale_1 = test::GetValueForConfig<String>(table_1.get(), "android:string/foo", + test::ParseConfigOrDie("en-rXA")); + ASSERT_NE(nullptr, locale_1); + + String* feminine_1 = + test::GetValueForConfig<String>(table_1.get(), "android:string/foo", config_feminine); + ASSERT_NE(nullptr, feminine_1); + EXPECT_EQ(std::string("(F)") + *locale_1->value, *feminine_1->value); + + String* masculine_1 = + test::GetValueForConfig<String>(table_1.get(), "android:string/foo", config_masculine); + EXPECT_EQ(nullptr, masculine_1); + + String* neuter_1 = + test::GetValueForConfig<String>(table_1.get(), "android:string/foo", config_neuter); + ASSERT_NE(nullptr, neuter_1); + EXPECT_EQ(std::string("(N)") + *locale_1->value, *neuter_1->value); + + // invalid gender value + std::unique_ptr<ResourceTable> table_2 = + test::ResourceTableBuilder().AddString("android:string/foo", "foo").Build(); + + std::unique_ptr<IAaptContext> context_2 = test::ContextBuilder().Build(); + PseudolocaleGenerator generator_2(std::string("invald,"), std::string("1.0")); + ASSERT_FALSE(generator_2.Consume(context_2.get(), table_2.get())); +} + } // namespace aapt diff --git a/tools/aapt2/dump/DumpManifest.cpp b/tools/aapt2/dump/DumpManifest.cpp index a43bf1b60f42..6bf265d2e363 100644 --- a/tools/aapt2/dump/DumpManifest.cpp +++ b/tools/aapt2/dump/DumpManifest.cpp @@ -837,9 +837,9 @@ class UsesSdkBadging : public ManifestExtractor::Element { void Print(text::Printer* printer) override { if (min_sdk) { - printer->Print(StringPrintf("sdkVersion:'%d'\n", *min_sdk)); + printer->Print(StringPrintf("minSdkVersion:'%d'\n", *min_sdk)); } else if (min_sdk_name) { - printer->Print(StringPrintf("sdkVersion:'%s'\n", min_sdk_name->data())); + printer->Print(StringPrintf("minSdkVersion:'%s'\n", min_sdk_name->data())); } if (max_sdk) { printer->Print(StringPrintf("maxSdkVersion:'%d'\n", *max_sdk)); diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index 8c594ba553a0..f05611048caa 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -68,9 +68,8 @@ struct OverlayableChunk { class PackageFlattener { public: PackageFlattener(IAaptContext* context, const ResourceTablePackageView& package, - const std::map<size_t, std::string>* shared_libs, - SparseEntriesMode sparse_entries, - bool compact_entries, + const ResourceTable::ReferencedPackages* shared_libs, + SparseEntriesMode sparse_entries, bool compact_entries, bool collapse_key_stringpool, const std::set<ResourceName>& name_collapse_exemptions, bool deduplicate_entry_values) @@ -145,10 +144,9 @@ class PackageFlattener { // 2) the entries will be accessed on platforms U+, and // 3) all entry keys can be encoded in 16 bits bool UseCompactEntries(const ConfigDescription& config, std::vector<FlatEntry>* entries) const { - return compact_entries_ && - (context_->GetMinSdkVersion() > SDK_TIRAMISU || config.sdkVersion > SDK_TIRAMISU) && - std::none_of(entries->cbegin(), entries->cend(), - [](const auto& e) { return e.entry_key >= std::numeric_limits<uint16_t>::max(); }); + return compact_entries_ && context_->GetMinSdkVersion() > SDK_TIRAMISU && + std::none_of(entries->cbegin(), entries->cend(), + [](const auto& e) { return e.entry_key >= std::numeric_limits<uint16_t>::max(); }); } std::unique_ptr<ResEntryWriter> GetResEntryWriter(bool dedup, bool compact, BigBuffer* buffer) { @@ -548,7 +546,7 @@ class PackageFlattener { IAaptContext* context_; android::IDiagnostics* diag_; const ResourceTablePackageView package_; - const std::map<size_t, std::string>* shared_libs_; + const ResourceTable::ReferencedPackages* shared_libs_; SparseEntriesMode sparse_entries_; bool compact_entries_; android::StringPool type_pool_; diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp index 09ef9bddd3bd..e1a3013c07bb 100644 --- a/tools/aapt2/format/proto/ProtoDeserialize.cpp +++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp @@ -916,7 +916,7 @@ std::unique_ptr<Item> DeserializeItemFromPb(const pb::Item& pb_item, } break; case pb::Primitive::kIntDecimalValue: { val.dataType = android::Res_value::TYPE_INT_DEC; - val.data = static_cast<uint32_t>(pb_prim.int_decimal_value()); + val.data = static_cast<int32_t>(pb_prim.int_decimal_value()); } break; case pb::Primitive::kIntHexadecimalValue: { val.dataType = android::Res_value::TYPE_INT_HEX; diff --git a/tools/aapt2/format/proto/ProtoSerialize_test.cpp b/tools/aapt2/format/proto/ProtoSerialize_test.cpp index afb83562b129..fa8860ff69eb 100644 --- a/tools/aapt2/format/proto/ProtoSerialize_test.cpp +++ b/tools/aapt2/format/proto/ProtoSerialize_test.cpp @@ -250,6 +250,7 @@ TEST(ProtoSerializeTest, SerializeSinglePackage) { } TEST(ProtoSerializeTest, SerializeAndDeserializeXml) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); xml::Element element; element.line_number = 22; element.column_number = 23; @@ -269,8 +270,8 @@ TEST(ProtoSerializeTest, SerializeAndDeserializeXml) { attr.namespace_uri = xml::kSchemaAndroid; attr.value = "23dp"; attr.compiled_attribute = xml::AaptAttribute(Attribute{}, ResourceId(0x01010000)); - attr.compiled_value = - ResourceUtils::TryParseItemForAttribute(attr.value, android::ResTable_map::TYPE_DIMENSION); + attr.compiled_value = ResourceUtils::TryParseItemForAttribute( + context->GetDiagnostics(), attr.value, android::ResTable_map::TYPE_DIMENSION); attr.compiled_value->SetSource(android::Source().WithLine(25)); element.attributes.push_back(std::move(attr)); diff --git a/tools/aapt2/integration-tests/DumpTest/built_with_aapt_expected.txt b/tools/aapt2/integration-tests/DumpTest/built_with_aapt_expected.txt index cc0b3bf5d2fb..d14e5fb5c477 100644 --- a/tools/aapt2/integration-tests/DumpTest/built_with_aapt_expected.txt +++ b/tools/aapt2/integration-tests/DumpTest/built_with_aapt_expected.txt @@ -1,5 +1,5 @@ package: name='com.aapt.app' versionCode='222' versionName='222' platformBuildVersionName='12' platformBuildVersionCode='32' compileSdkVersion='32' compileSdkVersionCodename='12' -sdkVersion:'22' +minSdkVersion:'22' targetSdkVersion:'32' application: label='App' icon='' feature-group: label='' diff --git a/tools/aapt2/integration-tests/DumpTest/components_expected.txt b/tools/aapt2/integration-tests/DumpTest/components_expected.txt index 9c81fb83ca15..93cce0a29024 100644 --- a/tools/aapt2/integration-tests/DumpTest/components_expected.txt +++ b/tools/aapt2/integration-tests/DumpTest/components_expected.txt @@ -1,5 +1,5 @@ package: name='com.example.bundletool.minimal' versionCode='1' versionName='1.0' platformBuildVersionName='12' platformBuildVersionCode='31' compileSdkVersion='31' compileSdkVersionCodename='12' -sdkVersion:'21' +minSdkVersion:'21' targetSdkVersion:'31' uses-configuration: reqTouchScreen='3' reqKeyboardType='2' reqHardKeyboard='-1' reqNavigation='3' reqFiveWayNav='-1' supports-gl-texture:'GL_OES_compressed_paletted_texture' diff --git a/tools/aapt2/integration-tests/DumpTest/minimal_expected.txt b/tools/aapt2/integration-tests/DumpTest/minimal_expected.txt index 85ab5d80cd39..aafca68443a9 100644 --- a/tools/aapt2/integration-tests/DumpTest/minimal_expected.txt +++ b/tools/aapt2/integration-tests/DumpTest/minimal_expected.txt @@ -1,5 +1,5 @@ package: name='com.lato.bubblegirl' versionCode='33' versionName='1.0.0' platformBuildVersionName='8.1.0' platformBuildVersionCode='27' -sdkVersion:'19' +minSdkVersion:'19' targetSdkVersion:'26' application-label:'Bubble Girl' application-label-ar:'Bubble Girl' diff --git a/tools/aapt2/integration-tests/DumpTest/multiple_uses_sdk_expected.txt b/tools/aapt2/integration-tests/DumpTest/multiple_uses_sdk_expected.txt index 85e8d0a3cbba..f48f381e3012 100644 --- a/tools/aapt2/integration-tests/DumpTest/multiple_uses_sdk_expected.txt +++ b/tools/aapt2/integration-tests/DumpTest/multiple_uses_sdk_expected.txt @@ -1,5 +1,5 @@ package: name='com.test.e17wmultiapknexus' versionCode='107' versionName='14' platformBuildVersionName='2.3.3' platformBuildVersionCode='10' -sdkVersion:'1' +minSdkVersion:'1' application-label:'w45wmultiapknexus_10' application-icon-120:'res/drawable-ldpi-v4/icon.png' application-icon-160:'res/drawable-mdpi-v4/icon.png' diff --git a/tools/aapt2/link/ReferenceLinker.cpp b/tools/aapt2/link/ReferenceLinker.cpp index 9dadfb26a3f8..c69b32513167 100644 --- a/tools/aapt2/link/ReferenceLinker.cpp +++ b/tools/aapt2/link/ReferenceLinker.cpp @@ -164,8 +164,8 @@ std::unique_ptr<Item> ReferenceLinkerTransformer::TransformItem(const Reference* std::unique_ptr<Item> ReferenceLinkerTransformer::ParseValueWithAttribute( std::unique_ptr<Item> value, const Attribute* attr) { if (RawString* raw_string = ValueCast<RawString>(value.get())) { - std::unique_ptr<Item> transformed = - ResourceUtils::TryParseItemForAttribute(*raw_string->value, attr); + std::unique_ptr<Item> transformed = ResourceUtils::TryParseItemForAttribute( + context_->GetDiagnostics(), *raw_string->value, attr); // If we could not parse as any specific type, try a basic STRING. if (!transformed && (attr->type_mask & android::ResTable_map::TYPE_STRING)) { diff --git a/tools/aapt2/link/XmlReferenceLinker.cpp b/tools/aapt2/link/XmlReferenceLinker.cpp index d2e9bd770a31..aec7ceb82c0f 100644 --- a/tools/aapt2/link/XmlReferenceLinker.cpp +++ b/tools/aapt2/link/XmlReferenceLinker.cpp @@ -90,7 +90,8 @@ class XmlVisitor : public xml::PackageAwareVisitor { attribute = &attr.compiled_attribute.value().attribute; } - attr.compiled_value = ResourceUtils::TryParseItemForAttribute(attr.value, attribute); + attr.compiled_value = ResourceUtils::TryParseItemForAttribute(context_->GetDiagnostics(), + attr.value, attribute); if (attr.compiled_value) { // With a compiledValue, we must resolve the reference and assign it an ID. attr.compiled_value->SetSource(source); diff --git a/tools/aapt2/optimize/Obfuscator.cpp b/tools/aapt2/optimize/Obfuscator.cpp index 8f12f735736e..903cdf852566 100644 --- a/tools/aapt2/optimize/Obfuscator.cpp +++ b/tools/aapt2/optimize/Obfuscator.cpp @@ -40,9 +40,9 @@ Obfuscator::Obfuscator(OptimizeOptions& optimizeOptions) collapse_key_stringpool_(optimizeOptions.table_flattener_options.collapse_key_stringpool) { } -std::string ShortenFileName(android::StringPiece file_path, int output_length) { +std::string Obfuscator::ShortenFileName(android::StringPiece file_path, int output_length) { std::size_t hash_num = std::hash<android::StringPiece>{}(file_path); - std::string result = ""; + std::string result; // Convert to (modified) base64 so that it is a proper file path. for (int i = 0; i < output_length; i++) { uint8_t sextet = hash_num & 0x3f; @@ -52,10 +52,33 @@ std::string ShortenFileName(android::StringPiece file_path, int output_length) { return result; } +static std::string RenameDisallowedFileNames(const std::string& file_name) { + // We are renaming shortened file names to make sure they not a reserved file name in Windows. + // See: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file. We are renaming + // "COM" and "LPT" too because we are appending a number in case of hash collisions; "COM1", + // "COM2", etc. are reserved names. + static const char* const reserved_windows_names[] = {"CON", "PRN", "AUX", "NUL", "COM", "LPT"}; + if (file_name.length() == 3) { + // Need to convert the file name to uppercase as Windows is case insensitive. E.g., "NuL", + // "nul", and "NUl" are also reserved. + std::string result_upper_cased(3, 0); + std::transform(file_name.begin(), file_name.end(), result_upper_cased.begin(), + [](unsigned char c) { return std::toupper(c); }); + for (auto reserved_windows_name : reserved_windows_names) { + if (result_upper_cased == reserved_windows_name) { + // Simple solution to make it a non-reserved name is to add an underscore + return "_" + file_name; + } + } + } + + return file_name; +} + // Return the optimal hash length such that at most 10% of resources collide in // their shortened path. // Reference: http://matt.might.net/articles/counting-hash-collisions/ -int OptimalShortenedLength(int num_resources) { +static int OptimalShortenedLength(int num_resources) { if (num_resources > 4000) { return 3; } else { @@ -63,8 +86,8 @@ int OptimalShortenedLength(int num_resources) { } } -std::string GetShortenedPath(android::StringPiece shortened_filename, - android::StringPiece extension, int collision_count) { +static std::string GetShortenedPath(android::StringPiece shortened_filename, + android::StringPiece extension, int collision_count) { std::string shortened_path = std::string("res/") += shortened_filename; if (collision_count > 0) { shortened_path += std::to_string(collision_count); @@ -82,9 +105,9 @@ struct PathComparator { } }; -static bool HandleShortenFilePaths(ResourceTable* table, - std::map<std::string, std::string>& shortened_path_map, - const std::set<ResourceName>& path_shorten_exemptions) { +bool Obfuscator::HandleShortenFilePaths(ResourceTable* table, + std::map<std::string, std::string>& shortened_path_map, + const std::set<ResourceName>& path_shorten_exemptions) { // used to detect collisions std::unordered_set<std::string> shortened_paths; std::set<FileReference*, PathComparator> file_refs; @@ -112,7 +135,8 @@ static bool HandleShortenFilePaths(ResourceTable* table, // Android detects ColorStateLists via pathname, skip res/color* if (util::StartsWith(res_subdir, "res/color")) continue; - std::string shortened_filename = ShortenFileName(*file_ref->path, num_chars); + std::string shortened_filename = + RenameDisallowedFileNames(ShortenFileName(*file_ref->path, num_chars)); int collision_count = 0; std::string shortened_path = GetShortenedPath(shortened_filename, extension, collision_count); while (shortened_paths.find(shortened_path) != shortened_paths.end()) { diff --git a/tools/aapt2/optimize/Obfuscator.h b/tools/aapt2/optimize/Obfuscator.h index 5ccf54383aae..79d7e088d1cc 100644 --- a/tools/aapt2/optimize/Obfuscator.h +++ b/tools/aapt2/optimize/Obfuscator.h @@ -53,7 +53,14 @@ class Obfuscator : public IResourceTableConsumer { const ResourceNamedType& type_name, const ResourceTableEntryView& entry, const android::base::function_ref<void(Result, const ResourceName&)> onObfuscate); + protected: + virtual std::string ShortenFileName(android::StringPiece file_path, int output_length); + private: + bool HandleShortenFilePaths(ResourceTable* table, + std::map<std::string, std::string>& shortened_path_map, + const std::set<ResourceName>& path_shorten_exemptions); + TableFlattenerOptions& options_; const bool shorten_resource_paths_; const bool collapse_key_stringpool_; diff --git a/tools/aapt2/optimize/Obfuscator_test.cpp b/tools/aapt2/optimize/Obfuscator_test.cpp index 940cf1096f92..c3429e0fc1d6 100644 --- a/tools/aapt2/optimize/Obfuscator_test.cpp +++ b/tools/aapt2/optimize/Obfuscator_test.cpp @@ -19,6 +19,7 @@ #include <map> #include <memory> #include <string> +#include <utility> #include "ResourceTable.h" #include "android-base/file.h" @@ -26,6 +27,7 @@ using ::aapt::test::GetValue; using ::testing::AnyOf; +using ::testing::Contains; using ::testing::Eq; using ::testing::HasSubstr; using ::testing::IsFalse; @@ -33,6 +35,10 @@ using ::testing::IsTrue; using ::testing::Not; using ::testing::NotNull; +namespace aapt { + +namespace { + android::StringPiece GetExtension(android::StringPiece path) { auto iter = std::find(path.begin(), path.end(), '.'); return android::StringPiece(iter, path.end() - iter); @@ -45,7 +51,22 @@ void FillTable(aapt::test::ResourceTableBuilder& builder, int start, int end) { } } -namespace aapt { +class FakeObfuscator : public Obfuscator { + public: + explicit FakeObfuscator(OptimizeOptions& optimize_options, + const std::unordered_map<std::string, std::string>& shortened_name_map) + : Obfuscator(optimize_options), shortened_name_map_(shortened_name_map) { + } + + protected: + std::string ShortenFileName(android::StringPiece file_path, int output_length) override { + return shortened_name_map_[std::string(file_path)]; + } + + private: + std::unordered_map<std::string, std::string> shortened_name_map_; + DISALLOW_COPY_AND_ASSIGN(FakeObfuscator); +}; TEST(ObfuscatorTest, FileRefPathsChangedInResourceTable) { std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); @@ -127,7 +148,7 @@ TEST(ObfuscatorTest, SkipPathShortenExemptions) { EXPECT_THAT(path_map.find("res/drawables/xmlfile2.xml"), Not(Eq(path_map.end()))); FileReference* ref = GetValue<FileReference>(table.get(), "android:drawable/xmlfile"); - EXPECT_THAT(ref, NotNull()); + ASSERT_THAT(ref, NotNull()); ASSERT_THAT(HasFailure(), IsFalse()); // The path of first drawable in exemption was not changed EXPECT_THAT("res/drawables/xmlfile.xml", Eq(*ref->path)); @@ -161,13 +182,78 @@ TEST(ObfuscatorTest, KeepExtensions) { ASSERT_THAT(path_map.find("res/drawable/xmlfile.xml"), Not(Eq(path_map.end()))); ASSERT_THAT(path_map.find("res/drawable/pngfile.png"), Not(Eq(path_map.end()))); - auto shortend_xml_path = path_map[original_xml_path]; - auto shortend_png_path = path_map[original_png_path]; - EXPECT_THAT(GetExtension(path_map[original_xml_path]), Eq(android::StringPiece(".xml"))); EXPECT_THAT(GetExtension(path_map[original_png_path]), Eq(android::StringPiece(".png"))); } +TEST(ObfuscatorTest, ShortenedToReservedWindowsNames) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + + std::string original_path_1 = "res/drawable/pngfile_1.png"; + std::string original_path_2 = "res/drawable/pngfile_2.png"; + std::string original_path_3 = "res/drawable/pngfile_3.png"; + std::string original_path_4 = "res/drawable/pngfile_4.png"; + std::string original_path_5 = "res/drawable/pngfile_5.png"; + std::string original_path_6 = "res/drawable/pngfile_6.png"; + std::string original_path_7 = "res/drawable/pngfile_7.png"; + std::string original_path_8 = "res/drawable/pngfile_8.png"; + std::string original_path_9 = "res/drawable/pngfile_9.png"; + + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .AddFileReference("android:drawable/pngfile_1", original_path_1) + .AddFileReference("android:drawable/pngfile_2", original_path_2) + .AddFileReference("android:drawable/pngfile_3", original_path_3) + .AddFileReference("android:drawable/pngfile_4", original_path_4) + .AddFileReference("android:drawable/pngfile_5", original_path_5) + .AddFileReference("android:drawable/pngfile_6", original_path_6) + .AddFileReference("android:drawable/pngfile_7", original_path_7) + .AddFileReference("android:drawable/pngfile_8", original_path_8) + .AddFileReference("android:drawable/pngfile_9", original_path_9) + .Build(); + + OptimizeOptions options{.shorten_resource_paths = true}; + std::map<std::string, std::string>& path_map = options.table_flattener_options.shortened_path_map; + auto obfuscator = FakeObfuscator( + options, + { + {original_path_1, "CON"}, + {original_path_2, "Prn"}, + {original_path_3, "AuX"}, + {original_path_4, "nul"}, + {original_path_5, "cOM"}, + {original_path_6, "lPt"}, + {original_path_7, "lPt"}, + {original_path_8, "lPt"}, // 6, 7, and 8 will be appended with a number to disambiguate + {original_path_9, "F0o"}, // This one is not reserved + }); + ASSERT_TRUE(obfuscator.Consume(context.get(), table.get())); + + // Expect that the path map is populated + ASSERT_THAT(path_map.find(original_path_1), Not(Eq(path_map.end()))); + ASSERT_THAT(path_map.find(original_path_2), Not(Eq(path_map.end()))); + ASSERT_THAT(path_map.find(original_path_3), Not(Eq(path_map.end()))); + ASSERT_THAT(path_map.find(original_path_4), Not(Eq(path_map.end()))); + ASSERT_THAT(path_map.find(original_path_5), Not(Eq(path_map.end()))); + ASSERT_THAT(path_map.find(original_path_6), Not(Eq(path_map.end()))); + ASSERT_THAT(path_map.find(original_path_7), Not(Eq(path_map.end()))); + ASSERT_THAT(path_map.find(original_path_8), Not(Eq(path_map.end()))); + ASSERT_THAT(path_map.find(original_path_9), Not(Eq(path_map.end()))); + + EXPECT_THAT(path_map[original_path_1], Eq("res/_CON.png")); + EXPECT_THAT(path_map[original_path_2], Eq("res/_Prn.png")); + EXPECT_THAT(path_map[original_path_3], Eq("res/_AuX.png")); + EXPECT_THAT(path_map[original_path_4], Eq("res/_nul.png")); + EXPECT_THAT(path_map[original_path_5], Eq("res/_cOM.png")); + EXPECT_THAT(path_map[original_path_9], Eq("res/F0o.png")); + + std::set<std::string> lpt_shortened_names{path_map[original_path_6], path_map[original_path_7], + path_map[original_path_8]}; + EXPECT_THAT(lpt_shortened_names, Contains("res/_lPt.png")); + EXPECT_THAT(lpt_shortened_names, Contains("res/_lPt1.png")); + EXPECT_THAT(lpt_shortened_names, Contains("res/_lPt2.png")); +} + TEST(ObfuscatorTest, DeterministicallyHandleCollisions) { std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); @@ -247,7 +333,9 @@ TEST(ObfuscatorTest, DumpIdResourceMap) { ASSERT_TRUE(Obfuscator(options).Consume(context.get(), table.get())); // Expect that the id resource name map is populated + ASSERT_THAT(id_resource_map.find(0x7f020000), Not(Eq(id_resource_map.end()))); EXPECT_THAT(id_resource_map.at(0x7f020000), Eq("mycolor")); + ASSERT_THAT(id_resource_map.find(0x7f030000), Not(Eq(id_resource_map.end()))); EXPECT_THAT(id_resource_map.at(0x7f030000), Eq("mystring")); EXPECT_THAT(id_resource_map.find(0x7f030001), Eq(id_resource_map.end())); EXPECT_THAT(id_resource_map.find(0x7f030002), Eq(id_resource_map.end())); @@ -300,17 +388,18 @@ TEST(ObfuscatorTest, WriteObfuscationMapInProtocolBufferFormat) { ASSERT_TRUE(obfuscator.Consume(test::ContextBuilder().Build().get(), getProtocolBufferTableUnderTest().get())); - obfuscator.WriteObfuscationMap("obfuscated_map.pb"); + const auto map_path = testing::TempDir() + "/obfuscated_map.pb"; + ASSERT_TRUE(obfuscator.WriteObfuscationMap(map_path)); std::string pbOut; - android::base::ReadFileToString("obfuscated_map.pb", &pbOut, false /* follow_symlinks */); + ASSERT_TRUE(android::base::ReadFileToString(map_path, &pbOut, false /* follow_symlinks */)); EXPECT_THAT(pbOut, HasSubstr("drawable/xmlfile.xml")); EXPECT_THAT(pbOut, HasSubstr("drawable/pngfile.png")); EXPECT_THAT(pbOut, HasSubstr("mycolor")); EXPECT_THAT(pbOut, HasSubstr("mystring")); pb::ResourceMappings resourceMappings; - EXPECT_THAT(resourceMappings.ParseFromString(pbOut), IsTrue()); - EXPECT_THAT(resourceMappings.collapsed_names().resource_names_size(), Eq(2)); + ASSERT_THAT(resourceMappings.ParseFromString(pbOut), IsTrue()); + ASSERT_THAT(resourceMappings.collapsed_names().resource_names_size(), Eq(2)); auto& resource_names = resourceMappings.collapsed_names().resource_names(); EXPECT_THAT(resource_names.at(0).name(), AnyOf(Eq("mycolor"), Eq("mystring"))); EXPECT_THAT(resource_names.at(1).name(), AnyOf(Eq("mycolor"), Eq("mystring"))); @@ -328,11 +417,14 @@ TEST(ObfuscatorTest, WriteObfuscatingMapWithNonEnabledOption) { ASSERT_TRUE(obfuscator.Consume(test::ContextBuilder().Build().get(), getProtocolBufferTableUnderTest().get())); - obfuscator.WriteObfuscationMap("obfuscated_map.pb"); + const auto map_path = testing::TempDir() + "/obfuscated_map.pb"; + ASSERT_TRUE(obfuscator.WriteObfuscationMap(map_path)); std::string pbOut; - android::base::ReadFileToString("obfuscated_map.pb", &pbOut, false /* follow_symlinks */); + ASSERT_TRUE(android::base::ReadFileToString(map_path, &pbOut, false /* follow_symlinks */)); ASSERT_THAT(pbOut, Eq("")); } +} // namespace + } // namespace aapt diff --git a/tools/aapt2/trace/TraceBuffer.cpp b/tools/aapt2/trace/TraceBuffer.cpp index da5373936306..0988c313b65b 100644 --- a/tools/aapt2/trace/TraceBuffer.cpp +++ b/tools/aapt2/trace/TraceBuffer.cpp @@ -36,116 +36,142 @@ constexpr char kBegin = 'B'; constexpr char kEnd = 'E'; struct TracePoint { + char type; pid_t tid; int64_t time; std::string tag; - char type; }; std::vector<TracePoint> traces; +bool enabled = true; +constinit std::chrono::steady_clock::time_point startTime = {}; int64_t GetTime() noexcept { auto now = std::chrono::steady_clock::now(); - return std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count(); + if (startTime == decltype(tracebuffer::startTime){}) { + startTime = now; + } + return std::chrono::duration_cast<std::chrono::microseconds>(now - startTime).count(); } -} // namespace anonymous - -void AddWithTime(const std::string& tag, char type, int64_t time) noexcept { - TracePoint t = {getpid(), time, tag, type}; - traces.emplace_back(t); +void AddWithTime(std::string tag, char type, int64_t time) noexcept { + TracePoint t = {type, getpid(), time, std::move(tag)}; + traces.emplace_back(std::move(t)); } -void Add(const std::string& tag, char type) noexcept { - AddWithTime(tag, type, GetTime()); +void Add(std::string tag, char type) noexcept { + AddWithTime(std::move(tag), type, GetTime()); } - - - void Flush(const std::string& basePath) { - TRACE_CALL(); if (basePath.empty()) { return; } + BeginTrace(__func__); // We can't do much here, only record that it happened. - std::stringstream s; + std::ostringstream s; s << basePath << aapt::file::sDirSep << "report_aapt2_" << getpid() << ".json"; FILE* f = android::base::utf8::fopen(s.str().c_str(), "a"); if (f == nullptr) { return; } - for(const TracePoint& trace : traces) { - fprintf(f, "{\"ts\" : \"%" PRIu64 "\", \"ph\" : \"%c\", \"tid\" : \"%d\" , \"pid\" : \"%d\", " - "\"name\" : \"%s\" },\n", trace.time, trace.type, 0, trace.tid, trace.tag.c_str()); + // Wrap the trace in a JSON array [] to make Chrome/Perfetto UI handle it. + char delimiter = '['; + for (const TracePoint& trace : traces) { + fprintf(f, + "%c{\"ts\" : \"%" PRIu64 + "\", \"ph\" : \"%c\", \"tid\" : \"%d\" , \"pid\" : \"%d\", \"name\" : \"%s\" }\n", + delimiter, trace.time, trace.type, 0, trace.tid, trace.tag.c_str()); + delimiter = ','; + } + if (!traces.empty()) { + fprintf(f, "]"); } fclose(f); traces.clear(); } +} // namespace + } // namespace tracebuffer -void BeginTrace(const std::string& tag) { - tracebuffer::Add(tag, tracebuffer::kBegin); +void BeginTrace(std::string tag) { + if (!tracebuffer::enabled) return; + tracebuffer::Add(std::move(tag), tracebuffer::kBegin); +} + +void EndTrace(std::string tag) { + if (!tracebuffer::enabled) return; + tracebuffer::Add(std::move(tag), tracebuffer::kEnd); +} + +bool Trace::enable(bool value) { + return tracebuffer::enabled = value; } -void EndTrace() { - tracebuffer::Add("", tracebuffer::kEnd); +Trace::Trace(const char* tag) { + if (!tracebuffer::enabled) return; + tag_.assign(tag); + tracebuffer::Add(tag_, tracebuffer::kBegin); } -Trace::Trace(const std::string& tag) { - tracebuffer::Add(tag, tracebuffer::kBegin); +Trace::Trace(std::string tag) : tag_(std::move(tag)) { + if (!tracebuffer::enabled) return; + tracebuffer::Add(tag_, tracebuffer::kBegin); } -Trace::Trace(const std::string& tag, const std::vector<android::StringPiece>& args) { - std::stringstream s; +template <class SpanOfStrings> +std::string makeTag(std::string_view tag, const SpanOfStrings& args) { + std::ostringstream s; s << tag; - s << " "; - for (auto& arg : args) { - s << arg; - s << " "; + if (!args.empty()) { + for (const auto& arg : args) { + s << ' '; + s << arg; + } } - tracebuffer::Add(s.str(), tracebuffer::kBegin); + return std::move(s).str(); +} + +Trace::Trace(std::string_view tag, const std::vector<android::StringPiece>& args) { + if (!tracebuffer::enabled) return; + tag_ = makeTag(tag, args); + tracebuffer::Add(tag_, tracebuffer::kBegin); } Trace::~Trace() { - tracebuffer::Add("", tracebuffer::kEnd); + if (!tracebuffer::enabled) return; + tracebuffer::Add(std::move(tag_), tracebuffer::kEnd); } -FlushTrace::FlushTrace(const std::string& basepath, const std::string& tag) - : basepath_(basepath) { - tracebuffer::Add(tag, tracebuffer::kBegin); +FlushTrace::FlushTrace(std::string_view basepath, std::string_view tag) { + if (!Trace::enable(!basepath.empty())) return; + basepath_.assign(basepath); + tag_.assign(tag); + tracebuffer::Add(tag_, tracebuffer::kBegin); } -FlushTrace::FlushTrace(const std::string& basepath, const std::string& tag, - const std::vector<android::StringPiece>& args) : basepath_(basepath) { - std::stringstream s; - s << tag; - s << " "; - for (auto& arg : args) { - s << arg; - s << " "; - } - tracebuffer::Add(s.str(), tracebuffer::kBegin); +FlushTrace::FlushTrace(std::string_view basepath, std::string_view tag, + const std::vector<android::StringPiece>& args) { + if (!Trace::enable(!basepath.empty())) return; + basepath_.assign(basepath); + tag_ = makeTag(tag, args); + tracebuffer::Add(tag_, tracebuffer::kBegin); } -FlushTrace::FlushTrace(const std::string& basepath, const std::string& tag, - const std::vector<std::string>& args) : basepath_(basepath){ - std::stringstream s; - s << tag; - s << " "; - for (auto& arg : args) { - s << arg; - s << " "; - } - tracebuffer::Add(s.str(), tracebuffer::kBegin); +FlushTrace::FlushTrace(std::string_view basepath, std::string_view tag, + const std::vector<std::string>& args) { + if (!Trace::enable(!basepath.empty())) return; + basepath_.assign(basepath); + tag_ = makeTag(tag, args); + tracebuffer::Add(tag_, tracebuffer::kBegin); } FlushTrace::~FlushTrace() { - tracebuffer::Add("", tracebuffer::kEnd); + if (!tracebuffer::enabled) return; + tracebuffer::Add(std::move(tag_), tracebuffer::kEnd); tracebuffer::Flush(basepath_); } -} // namespace aapt - +} // namespace aapt diff --git a/tools/aapt2/trace/TraceBuffer.h b/tools/aapt2/trace/TraceBuffer.h index ba751dd72f41..f0333d1fe071 100644 --- a/tools/aapt2/trace/TraceBuffer.h +++ b/tools/aapt2/trace/TraceBuffer.h @@ -17,41 +17,50 @@ #ifndef AAPT_TRACEBUFFER_H #define AAPT_TRACEBUFFER_H +#include <androidfw/StringPiece.h> + #include <string> +#include <string_view> #include <vector> -#include <androidfw/StringPiece.h> - namespace aapt { // Record timestamps for beginning and end of a task and generate systrace json fragments. // This is an in-process ftrace which has the advantage of being platform independent. // These methods are NOT thread-safe since aapt2 is not multi-threaded. -// Convenience RIAA object to automatically finish an event when object goes out of scope. +// Convenience RAII object to automatically finish an event when object goes out of scope. class Trace { public: - Trace(const std::string& tag); - Trace(const std::string& tag, const std::vector<android::StringPiece>& args); - ~Trace(); + Trace(const char* tag); + Trace(std::string tag); + Trace(std::string_view tag, const std::vector<android::StringPiece>& args); + ~Trace(); + + static bool enable(bool value = true); + +private: + std::string tag_; }; // Manual markers. -void BeginTrace(const std::string& tag); -void EndTrace(); +void BeginTrace(std::string tag); +void EndTrace(std::string tag); // A main trace is required to flush events to disk. Events are formatted in systrace // json format. class FlushTrace { public: - explicit FlushTrace(const std::string& basepath, const std::string& tag); - explicit FlushTrace(const std::string& basepath, const std::string& tag, - const std::vector<android::StringPiece>& args); - explicit FlushTrace(const std::string& basepath, const std::string& tag, - const std::vector<std::string>& args); - ~FlushTrace(); + explicit FlushTrace(std::string_view basepath, std::string_view tag); + explicit FlushTrace(std::string_view basepath, std::string_view tag, + const std::vector<android::StringPiece>& args); + explicit FlushTrace(std::string_view basepath, std::string_view tag, + const std::vector<std::string>& args); + ~FlushTrace(); + private: std::string basepath_; + std::string tag_; }; #define TRACE_CALL() Trace __t(__func__) diff --git a/tools/aapt2/util/Util.cpp b/tools/aapt2/util/Util.cpp index be877660ef72..3d83caf29bba 100644 --- a/tools/aapt2/util/Util.cpp +++ b/tools/aapt2/util/Util.cpp @@ -21,6 +21,7 @@ #include <string> #include <vector> +#include "android-base/parseint.h" #include "android-base/stringprintf.h" #include "android-base/strings.h" #include "androidfw/BigBuffer.h" @@ -229,14 +230,29 @@ std::string GetToolFingerprint() { static const char* const sMinorVersion = "19"; // The build id of aapt2 binary. - static std::string sBuildId = android::build::GetBuildNumber(); - - if (android::base::StartsWith(sBuildId, "eng.")) { - time_t now = time(0); - tm* ltm = localtime(&now); + static const std::string sBuildId = [] { + std::string buildNumber = android::build::GetBuildNumber(); + + if (android::base::StartsWith(buildNumber, "eng.")) { + // android::build::GetBuildNumber() returns something like "eng.user.20230725.214219" where + // the latter two parts are "yyyyMMdd.HHmmss" at build time. Use "yyyyMM" in the fingerprint. + std::vector<std::string> parts = util::Split(buildNumber, '.'); + int buildYear; + int buildMonth; + if (parts.size() < 3 || parts[2].length() < 6 || + !android::base::ParseInt(parts[2].substr(0, 4), &buildYear) || + !android::base::ParseInt(parts[2].substr(4, 2), &buildMonth)) { + // Fallback to localtime() if GetBuildNumber() returns an unexpected output. + time_t now = time(0); + tm* ltm = localtime(&now); + buildYear = 1900 + ltm->tm_year; + buildMonth = 1 + ltm->tm_mon; + } - sBuildId = android::base::StringPrintf("eng.%d%d", 1900 + ltm->tm_year, 1 + ltm->tm_mon); - } + buildNumber = android::base::StringPrintf("eng.%04d%02d", buildYear, buildMonth); + } + return buildNumber; + }(); return android::base::StringPrintf("%s.%s-%s", sMajorVersion, sMinorVersion, sBuildId.c_str()); } diff --git a/tools/hoststubgen/OWNERS b/tools/hoststubgen/OWNERS new file mode 100644 index 000000000000..a8c5321307d1 --- /dev/null +++ b/tools/hoststubgen/OWNERS @@ -0,0 +1,3 @@ +omakoto@google.com +jsharkey@google.com +jaggies@google.com diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt index e03d92ab44a0..f1727b78f135 100644 --- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/Constants.kt @@ -30,6 +30,7 @@ const val BINDER_CLASS = "android.os.Binder" const val IINTERFACE_INTERFACE = "android.os.IInterface" const val AIDL_PERMISSION_HELPER_SUFFIX = "_enforcePermission" +const val PERMISSION_PREFIX_LITERAL = "android.permission." /** * If a non java (e.g. c++) backend is enabled, the @EnforcePermission diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt index 0baac2c7aacf..3a95df9b2773 100644 --- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt @@ -40,6 +40,8 @@ import org.jetbrains.uast.UElement import org.jetbrains.uast.UMethod import org.jetbrains.uast.toUElement +import java.util.EnumSet + /** * Lint Detector that ensures that any method overriding a method annotated * with @EnforcePermission is also annotated with the exact same annotation. @@ -93,7 +95,7 @@ class EnforcePermissionDetector : Detector(), SourceCodeScanner { val v1 = ConstantEvaluator.evaluate(context, value1) val v2 = ConstantEvaluator.evaluate(context, value2) if (v1 != null && v2 != null) { - if (v1 != v2) { + if (v1 != v2 && !isOneShortPermissionOfOther(v1, v2)) { return false } } else { @@ -105,7 +107,7 @@ class EnforcePermissionDetector : Detector(), SourceCodeScanner { for (j in children1.indices) { val c1 = ConstantEvaluator.evaluate(context, children1[j]) val c2 = ConstantEvaluator.evaluate(context, children2[j]) - if (c1 != c2) { + if (c1 != c2 && !isOneShortPermissionOfOther(c1, c2)) { return false } } @@ -114,6 +116,12 @@ class EnforcePermissionDetector : Detector(), SourceCodeScanner { return true } + private fun isOneShortPermissionOfOther( + permission1: Any?, + permission2: Any? + ): Boolean = permission1 == (permission2 as? String)?.removePrefix(PERMISSION_PREFIX_LITERAL) || + permission2 == (permission1 as? String)?.removePrefix(PERMISSION_PREFIX_LITERAL) + private fun compareMethods( context: JavaContext, element: UElement, @@ -206,7 +214,7 @@ class EnforcePermissionDetector : Detector(), SourceCodeScanner { severity = Severity.ERROR, implementation = Implementation( EnforcePermissionDetector::class.java, - Scope.JAVA_FILE_SCOPE + EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) ) ) @@ -219,7 +227,7 @@ class EnforcePermissionDetector : Detector(), SourceCodeScanner { severity = Severity.ERROR, implementation = Implementation( EnforcePermissionDetector::class.java, - Scope.JAVA_FILE_SCOPE + EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) ) ) } diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt index df13af516514..cbbf91b49ded 100644 --- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt @@ -34,6 +34,8 @@ import org.jetbrains.uast.UExpression import org.jetbrains.uast.UMethod import org.jetbrains.uast.skipParenthesizedExprDown +import java.util.EnumSet + class EnforcePermissionHelperDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes(): List<Class<out UElement?>> = listOf(UMethod::class.java) @@ -117,7 +119,7 @@ class EnforcePermissionHelperDetector : Detector(), SourceCodeScanner { severity = Severity.ERROR, implementation = Implementation( EnforcePermissionHelperDetector::class.java, - Scope.JAVA_FILE_SCOPE + EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) ) ) @@ -130,7 +132,7 @@ class EnforcePermissionHelperDetector : Detector(), SourceCodeScanner { severity = Severity.ERROR, implementation = Implementation( EnforcePermissionDetector::class.java, - Scope.JAVA_FILE_SCOPE + EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES) ) ) diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt index c7be36efd991..22f749eee36b 100644 --- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt @@ -53,10 +53,9 @@ class SimpleManualPermissionEnforcementDetector : AidlImplementationDetector() { lintFix ) - // TODO(b/265014041): turn on errors once all code that would cause one is fixed - // if (enforcePermissionFix.errorLevel) { - // incident.overrideSeverity(Severity.ERROR) - // } + if (enforcePermissionFix.errorLevel) { + incident.overrideSeverity(Severity.ERROR) + } context.report(incident) } diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt index 75b00737a168..b3dacbdf57a6 100644 --- a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt @@ -316,20 +316,55 @@ class EnforcePermissionDetectorTest : LintDetectorTest() { overrides the method Stub.testMethod which is annotated with @EnforcePermission. The same annotation must be used on Default.testMethod [MissingEnforcePermissionAnnotation] public void testMethod() {} ~~~~~~~~~~ - 1 errors, 0 warnings + 1 errors, 0 warnings """.addLineContinuation() ) } - fun testDoesDetectIssuesShortStringsNotAllowed() { + fun testDoesNotDetectIssuesShortStringsAllowedInChildAndParent() { lint().files(java( """ package test.pkg; import android.annotation.EnforcePermission; public class TestClass121 extends IFooMethod.Stub { @Override + @EnforcePermission("READ_PHONE_STATE") + public void testMethod() {} + @Override + @EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) + public void testMethodParentShortPermission() {} + @Override @EnforcePermission(anyOf={"INTERNET", "READ_PHONE_STATE"}) public void testMethodAnyLiteral() {} + @Override + @EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}) + public void testMethodAnyLiteralParentsShortPermission() {} + } + """).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testDoesDetectIssuesWrongShortStringsInChildAndParent() { + lint().files(java( + """ + package test.pkg; + import android.annotation.EnforcePermission; + public class TestClass121 extends IFooMethod.Stub { + @Override + @EnforcePermission("READ_WRONG_PHONE_STATE") + public void testMethod() {} + @Override + @EnforcePermission(android.Manifest.permission.READ_WRONG_PHONE_STATE) + public void testMethodParentShortPermission() {} + @Override + @EnforcePermission(anyOf={"WRONG_INTERNET", "READ_PHONE_STATE"}) + public void testMethodAnyLiteral() {} + @Override + @EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_WRONG_PHONE_STATE}) + public void testMethodAnyLiteralParentsShortPermission() {} } """).indented(), *stubs @@ -337,14 +372,19 @@ class EnforcePermissionDetectorTest : LintDetectorTest() { .run() .expect( """ - src/test/pkg/TestClass121.java:6: Error: The method \ - TestClass121.testMethodAnyLiteral is annotated with @EnforcePermission(anyOf={"INTERNET", "READ_PHONE_STATE"}) \ - which differs from the overridden method Stub.testMethodAnyLiteral: \ - @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). \ - The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] - public void testMethodAnyLiteral() {} - ~~~~~~~~~~~~~~~~~~~~ - 1 errors, 0 warnings + src/test/pkg/TestClass121.java:6: Error: The method TestClass121.testMethod is annotated with @EnforcePermission("READ_WRONG_PHONE_STATE") which differs from the overridden method Stub.testMethod: @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] + public void testMethod() {} + ~~~~~~~~~~ + src/test/pkg/TestClass121.java:9: Error: The method TestClass121.testMethodParentShortPermission is annotated with @EnforcePermission(android.Manifest.permission.READ_WRONG_PHONE_STATE) which differs from the overridden method Stub.testMethodParentShortPermission: @android.annotation.EnforcePermission("READ_PHONE_STATE"). The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] + public void testMethodParentShortPermission() {} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass121.java:12: Error: The method TestClass121.testMethodAnyLiteral is annotated with @EnforcePermission(anyOf={"WRONG_INTERNET", "READ_PHONE_STATE"}) which differs from the overridden method Stub.testMethodAnyLiteral: @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] + public void testMethodAnyLiteral() {} + ~~~~~~~~~~~~~~~~~~~~ + src/test/pkg/TestClass121.java:15: Error: The method TestClass121.testMethodAnyLiteralParentsShortPermission is annotated with @EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_WRONG_PHONE_STATE}) which differs from the overridden method Stub.testMethodAnyLiteralParentsShortPermission: @android.annotation.EnforcePermission(anyOf={"INTERNET", "READ_PHONE_STATE"}). The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation] + public void testMethodAnyLiteralParentsShortPermission() {} + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 4 errors, 0 warnings """.addLineContinuation() ) } @@ -360,12 +400,18 @@ class EnforcePermissionDetectorTest : LintDetectorTest() { @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) public void testMethod() {} @Override + @android.annotation.EnforcePermission("READ_PHONE_STATE") + public void testMethodParentShortPermission() {} + @Override @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}) public void testMethodAny() {} @Override @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}) public void testMethodAnyLiteral() {} @Override + @android.annotation.EnforcePermission(anyOf={"INTERNET", "READ_PHONE_STATE"}) + public void testMethodAnyLiteralParentsShortPermission() {} + @Override @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}) public void testMethodAll() {} @Override @@ -374,10 +420,14 @@ class EnforcePermissionDetectorTest : LintDetectorTest() { } @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) public void testMethod(); + @android.annotation.EnforcePermission("READ_PHONE_STATE") + public void testMethodParentShortPermission(); @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}) public void testMethodAny() {} @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}) public void testMethodAnyLiteral() {} + @android.annotation.EnforcePermission(anyOf={"INTERNET", "READ_PHONE_STATE"}) + public void testMethodAnyLiteralParentsShortPermission() {} @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}) public void testMethodAll() {} @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}) @@ -404,6 +454,7 @@ class EnforcePermissionDetectorTest : LintDetectorTest() { package android.Manifest; class permission { public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE"; + public static final String READ_WRONG_PHONE_STATE = "android.permission.READ_WRONG_PHONE_STATE"; public static final String NFC = "android.permission.NFC"; public static final String INTERNET = "android.permission.INTERNET"; } diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt index 6b8e72cf9222..9170e752934f 100644 --- a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt @@ -51,10 +51,10 @@ class SimpleManualPermissionEnforcementDetectorTest : LintDetectorTest() { .run() .expect( """ - src/Foo.java:7: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + src/Foo.java:7: Error: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] mContext.enforceCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo"); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 0 errors, 1 warnings + 1 errors, 0 warnings """ ) .expectFixDiffs( @@ -168,10 +168,10 @@ class SimpleManualPermissionEnforcementDetectorTest : LintDetectorTest() { .run() .expect( """ - src/Foo.java:8: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + src/Foo.java:8: Error: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] mContext.enforceCallingOrSelfPermission( ^ - 0 errors, 1 warnings + 1 errors, 0 warnings """ ) .expectFixDiffs( @@ -209,10 +209,10 @@ class SimpleManualPermissionEnforcementDetectorTest : LintDetectorTest() { .run() .expect( """ - src/Foo.java:8: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + src/Foo.java:8: Error: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] mContext.enforceCallingOrSelfPermission(android.Manifest.permission.READ_CONTACTS, "foo"); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 0 errors, 1 warnings + 1 errors, 0 warnings """ ) .expectFixDiffs( @@ -252,10 +252,10 @@ class SimpleManualPermissionEnforcementDetectorTest : LintDetectorTest() { .run() .expect( """ - src/Foo.java:10: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + src/Foo.java:10: Error: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] mContext.enforceCallingOrSelfPermission( ^ - 0 errors, 1 warnings + 1 errors, 0 warnings """ ) .expectFixDiffs( @@ -414,10 +414,10 @@ class SimpleManualPermissionEnforcementDetectorTest : LintDetectorTest() { .run() .expect( """ - src/Foo.java:14: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + src/Foo.java:14: Error: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] helper(); ~~~~~~~~~ - 0 errors, 1 warnings + 1 errors, 0 warnings """ ) .expectFixDiffs( @@ -506,10 +506,10 @@ class SimpleManualPermissionEnforcementDetectorTest : LintDetectorTest() { .run() .expect( """ - src/Foo.java:16: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + src/Foo.java:16: Error: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] mContext.enforceCallingOrSelfPermission("FOO", "foo"); ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 0 errors, 1 warnings + 1 errors, 0 warnings """ ) .expectFixDiffs( @@ -558,10 +558,10 @@ class SimpleManualPermissionEnforcementDetectorTest : LintDetectorTest() { .run() .expect( """ - src/Foo.java:19: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + src/Foo.java:19: Error: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] helperHelper(); ~~~~~~~~~~~~~~~ - 0 errors, 1 warnings + 1 errors, 0 warnings """ ) .expectFixDiffs( @@ -599,10 +599,10 @@ class SimpleManualPermissionEnforcementDetectorTest : LintDetectorTest() { .run() .expect( """ - src/Foo.java:7: Warning: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + src/Foo.java:7: Error: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] if (mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo") ^ - 0 errors, 1 warnings + 1 errors, 0 warnings """ ) .expectFixDiffs( diff --git a/tools/streaming_proto/OWNERS b/tools/streaming_proto/OWNERS new file mode 100644 index 000000000000..5f6e59f82c8a --- /dev/null +++ b/tools/streaming_proto/OWNERS @@ -0,0 +1 @@ +mwachens@google.com diff --git a/tools/streaming_proto/cpp/main.cpp b/tools/streaming_proto/cpp/main.cpp index fe9a438d81d7..905ed354049b 100644 --- a/tools/streaming_proto/cpp/main.cpp +++ b/tools/streaming_proto/cpp/main.cpp @@ -119,9 +119,8 @@ write_message(stringstream& text, const DescriptorProto& message, const string& text << endl; } -static void -write_header_file(CodeGeneratorResponse* response, const FileDescriptorProto& file_descriptor) -{ +static void write_header_file(const string& request_parameter, CodeGeneratorResponse* response, + const FileDescriptorProto& file_descriptor) { stringstream text; text << "// Generated by protoc-gen-cppstream. DO NOT MODIFY." << endl; @@ -159,6 +158,9 @@ write_header_file(CodeGeneratorResponse* response, const FileDescriptorProto& fi text << endl; text << "#endif // " << header << endl; + if (request_parameter.find("experimental_allow_proto3_optional") != string::npos) { + response->set_supported_features(CodeGeneratorResponse::FEATURE_PROTO3_OPTIONAL); + } CodeGeneratorResponse::File* file_response = response->add_file(); file_response->set_name(make_filename(file_descriptor)); file_response->set_content(text.str()); @@ -182,7 +184,7 @@ int main(int argc, char const *argv[]) for (int i=0; i<N; i++) { const FileDescriptorProto& file_descriptor = request.proto_file(i); if (should_generate_for_file(request, file_descriptor.name())) { - write_header_file(&response, file_descriptor); + write_header_file(request.parameter(), &response, file_descriptor); } } diff --git a/tools/xmlpersistence/Android.bp b/tools/xmlpersistence/Android.bp deleted file mode 100644 index 0b6dba626794..000000000000 --- a/tools/xmlpersistence/Android.bp +++ /dev/null @@ -1,20 +0,0 @@ -package { - // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "frameworks_base_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: ["frameworks_base_license"], -} - -java_binary_host { - name: "xmlpersistence_cli", - manifest: "manifest.txt", - srcs: [ - "src/**/*.kt", - ], - static_libs: [ - "javaparser-symbol-solver", - "javapoet", - ], -} diff --git a/tools/xmlpersistence/OWNERS b/tools/xmlpersistence/OWNERS deleted file mode 100644 index 4f4d06a32676..000000000000 --- a/tools/xmlpersistence/OWNERS +++ /dev/null @@ -1 +0,0 @@ -zhanghai@google.com diff --git a/tools/xmlpersistence/manifest.txt b/tools/xmlpersistence/manifest.txt deleted file mode 100644 index 6d9771998efc..000000000000 --- a/tools/xmlpersistence/manifest.txt +++ /dev/null @@ -1 +0,0 @@ -Main-class: MainKt diff --git a/tools/xmlpersistence/src/main/kotlin/Generator.kt b/tools/xmlpersistence/src/main/kotlin/Generator.kt deleted file mode 100644 index 8e62388c860f..000000000000 --- a/tools/xmlpersistence/src/main/kotlin/Generator.kt +++ /dev/null @@ -1,577 +0,0 @@ -/* - * Copyright (C) 2020 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 com.squareup.javapoet.ClassName -import com.squareup.javapoet.FieldSpec -import com.squareup.javapoet.JavaFile -import com.squareup.javapoet.MethodSpec -import com.squareup.javapoet.NameAllocator -import com.squareup.javapoet.ParameterSpec -import com.squareup.javapoet.TypeSpec -import java.io.File -import java.io.FileInputStream -import java.io.FileNotFoundException -import java.io.FileOutputStream -import java.io.IOException -import java.nio.charset.StandardCharsets -import java.time.Year -import java.util.Objects -import javax.lang.model.element.Modifier - -// JavaPoet only supports line comments, and can't add a newline after file level comments. -val FILE_HEADER = """ - /* - * Copyright (C) ${Year.now().value} 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. - */ - - // Generated by xmlpersistence. DO NOT MODIFY! - // CHECKSTYLE:OFF Generated code - // @formatter:off -""".trimIndent() + "\n\n" - -private val atomicFileType = ClassName.get("android.util", "AtomicFile") - -fun generate(persistence: PersistenceInfo): JavaFile { - val distinctClassFields = persistence.root.allClassFields.distinctBy { it.type } - val type = TypeSpec.classBuilder(persistence.name) - .addJavadoc( - """ - Generated class implementing XML persistence for${'$'}W{@link $1T}. - <p> - This class provides atomicity for persistence via {@link $2T}, however it does not provide - thread safety, so please bring your own synchronization mechanism. - """.trimIndent(), persistence.root.type, atomicFileType - ) - .addModifiers(Modifier.PUBLIC, Modifier.FINAL) - .addField(generateFileField()) - .addMethod(generateConstructor()) - .addMethod(generateReadMethod(persistence.root)) - .addMethod(generateParseMethod(persistence.root)) - .addMethods(distinctClassFields.map { generateParseClassMethod(it) }) - .addMethod(generateWriteMethod(persistence.root)) - .addMethod(generateSerializeMethod(persistence.root)) - .addMethods(distinctClassFields.map { generateSerializeClassMethod(it) }) - .addMethod(generateDeleteMethod()) - .build() - return JavaFile.builder(persistence.root.type.packageName(), type) - .skipJavaLangImports(true) - .indent(" ") - .build() -} - -private val nonNullType = ClassName.get("android.annotation", "NonNull") - -private fun generateFileField(): FieldSpec = - FieldSpec.builder(atomicFileType, "mFile", Modifier.PRIVATE, Modifier.FINAL) - .addAnnotation(nonNullType) - .build() - -private fun generateConstructor(): MethodSpec = - MethodSpec.constructorBuilder() - .addJavadoc( - """ - Create an instance of this class. - - @param file the XML file for persistence - """.trimIndent() - ) - .addModifiers(Modifier.PUBLIC) - .addParameter( - ParameterSpec.builder(File::class.java, "file").addAnnotation(nonNullType).build() - ) - .addStatement("mFile = new \$1T(file)", atomicFileType) - .build() - -private val nullableType = ClassName.get("android.annotation", "Nullable") - -private val xmlPullParserType = ClassName.get("org.xmlpull.v1", "XmlPullParser") - -private val xmlType = ClassName.get("android.util", "Xml") - -private val xmlPullParserExceptionType = ClassName.get("org.xmlpull.v1", "XmlPullParserException") - -private fun generateReadMethod(rootField: ClassFieldInfo): MethodSpec = - MethodSpec.methodBuilder("read") - .addJavadoc( - """ - Read${'$'}W{@link $1T}${'$'}Wfrom${'$'}Wthe${'$'}WXML${'$'}Wfile. - - @return the persisted${'$'}W{@link $1T},${'$'}Wor${'$'}W{@code null}${'$'}Wif${'$'}Wthe${'$'}WXML${'$'}Wfile${'$'}Wdoesn't${'$'}Wexist - @throws IllegalArgumentException if an error occurred while reading - """.trimIndent(), rootField.type - ) - .addAnnotation(nullableType) - .addModifiers(Modifier.PUBLIC) - .returns(rootField.type) - .addControlFlow("try (\$1T inputStream = mFile.openRead())", FileInputStream::class.java) { - addStatement("final \$1T parser = \$2T.newPullParser()", xmlPullParserType, xmlType) - addStatement("parser.setInput(inputStream, null)") - addStatement("return parse(parser)") - nextControlFlow("catch (\$1T e)", FileNotFoundException::class.java) - addStatement("return null") - nextControlFlow( - "catch (\$1T | \$2T e)", IOException::class.java, xmlPullParserExceptionType - ) - addStatement("throw new IllegalArgumentException(e)") - } - .build() - -private val ClassFieldInfo.allClassFields: List<ClassFieldInfo> - get() = - mutableListOf<ClassFieldInfo>().apply { - this += this@allClassFields - for (field in fields) { - when (field) { - is ClassFieldInfo -> this += field.allClassFields - is ListFieldInfo -> this += field.element.allClassFields - else -> {} - } - } - } - -private fun generateParseMethod(rootField: ClassFieldInfo): MethodSpec = - MethodSpec.methodBuilder("parse") - .addAnnotation(nonNullType) - .addModifiers(Modifier.PRIVATE, Modifier.STATIC) - .returns(rootField.type) - .addParameter( - ParameterSpec.builder(xmlPullParserType, "parser").addAnnotation(nonNullType).build() - ) - .addExceptions(listOf(ClassName.get(IOException::class.java), xmlPullParserExceptionType)) - .apply { - addStatement("int type") - addStatement("int depth") - addStatement("int innerDepth = parser.getDepth() + 1") - addControlFlow( - "while ((type = parser.next()) != \$1T.END_DOCUMENT\$W" - + "&& ((depth = parser.getDepth()) >= innerDepth || type != \$1T.END_TAG))", - xmlPullParserType - ) { - addControlFlow( - "if (depth > innerDepth || type != \$1T.START_TAG)", xmlPullParserType - ) { - addStatement("continue") - } - addControlFlow( - "if (\$1T.equals(parser.getName(),\$W\$2S))", Objects::class.java, - rootField.tagName - ) { - addStatement("return \$1L(parser)", rootField.parseMethodName) - } - } - addStatement( - "throw new IllegalArgumentException(\$1S)", - "Missing root tag <${rootField.tagName}>" - ) - } - .build() - -private fun generateParseClassMethod(classField: ClassFieldInfo): MethodSpec = - MethodSpec.methodBuilder(classField.parseMethodName) - .addAnnotation(nonNullType) - .addModifiers(Modifier.PRIVATE, Modifier.STATIC) - .returns(classField.type) - .addParameter( - ParameterSpec.builder(xmlPullParserType, "parser").addAnnotation(nonNullType).build() - ) - .apply { - val (attributeFields, tagFields) = classField.fields - .partition { it is PrimitiveFieldInfo || it is StringFieldInfo } - if (tagFields.isNotEmpty()) { - addExceptions( - listOf(ClassName.get(IOException::class.java), xmlPullParserExceptionType) - ) - } - val nameAllocator = NameAllocator().apply { - newName("parser") - newName("type") - newName("depth") - newName("innerDepth") - } - for (field in attributeFields) { - val variableName = nameAllocator.newName(field.variableName, field) - when (field) { - is PrimitiveFieldInfo -> { - val stringVariableName = - nameAllocator.newName("${field.variableName}String") - addStatement( - "final String \$1L =\$Wparser.getAttributeValue(null,\$W\$2S)", - stringVariableName, field.attributeName - ) - if (field.isRequired) { - addControlFlow("if (\$1L == null)", stringVariableName) { - addStatement( - "throw new IllegalArgumentException(\$1S)", - "Missing attribute \"${field.attributeName}\"" - ) - } - } - val boxedType = field.type.box() - val parseTypeMethodName = if (field.type.isPrimitive) { - "parse${field.type.toString().capitalize()}" - } else { - "valueOf" - } - if (field.isRequired) { - addStatement( - "final \$1T \$2L =\$W\$3T.\$4L($5L)", field.type, variableName, - boxedType, parseTypeMethodName, stringVariableName - ) - } else { - addStatement( - "final \$1T \$2L =\$W$3L != null ?\$W\$4T.\$5L($3L)\$W: null", - field.type, variableName, stringVariableName, boxedType, - parseTypeMethodName - ) - } - } - is StringFieldInfo -> - addStatement( - "final String \$1L =\$Wparser.getAttributeValue(null,\$W\$2S)", - variableName, field.attributeName - ) - else -> error(field) - } - } - if (tagFields.isNotEmpty()) { - for (field in tagFields) { - val variableName = nameAllocator.newName(field.variableName, field) - when (field) { - is ClassFieldInfo -> - addStatement("\$1T \$2L =\$Wnull", field.type, variableName) - is ListFieldInfo -> - addStatement( - "final \$1T \$2L =\$Wnew \$3T<>()", field.type, variableName, - ArrayList::class.java - ) - else -> error(field) - } - } - addStatement("int type") - addStatement("int depth") - addStatement("int innerDepth = parser.getDepth() + 1") - addControlFlow( - "while ((type = parser.next()) != \$1T.END_DOCUMENT\$W" - + "&& ((depth = parser.getDepth()) >= innerDepth || type != \$1T.END_TAG))", - xmlPullParserType - ) { - addControlFlow( - "if (depth > innerDepth || type != \$1T.START_TAG)", xmlPullParserType - ) { - addStatement("continue") - } - addControlFlow("switch (parser.getName())") { - for (field in tagFields) { - addControlFlow("case \$1S:", field.tagName) { - val variableName = nameAllocator.get(field) - when (field) { - is ClassFieldInfo -> { - addControlFlow("if (\$1L != null)", variableName) { - addStatement( - "throw new IllegalArgumentException(\$1S)", - "Duplicate tag \"${field.tagName}\"" - ) - } - addStatement( - "\$1L =\$W\$2L(parser)", variableName, - field.parseMethodName - ) - addStatement("break") - } - is ListFieldInfo -> { - val elementNameAllocator = nameAllocator.clone() - val elementVariableName = elementNameAllocator.newName( - field.element.xmlName!!.toLowerCamelCase() - ) - addStatement( - "final \$1T \$2L =\$W\$3L(parser)", field.element.type, - elementVariableName, field.element.parseMethodName - ) - addStatement( - "\$1L.add(\$2L)", variableName, elementVariableName - ) - addStatement("break") - } - else -> error(field) - } - } - } - } - } - } - for (field in tagFields.filter { it is ClassFieldInfo && it.isRequired }) { - addControlFlow("if ($1L == null)", nameAllocator.get(field)) { - addStatement( - "throw new IllegalArgumentException(\$1S)", "Missing tag <${field.tagName}>" - ) - } - } - addStatement( - classField.fields.joinToString(",\$W", "return new \$1T(", ")") { - nameAllocator.get(it) - }, classField.type - ) - } - .build() - -private val ClassFieldInfo.parseMethodName: String - get() = "parse${type.simpleName().toUpperCamelCase()}" - -private val xmlSerializerType = ClassName.get("org.xmlpull.v1", "XmlSerializer") - -private fun generateWriteMethod(rootField: ClassFieldInfo): MethodSpec = - MethodSpec.methodBuilder("write") - .apply { - val nameAllocator = NameAllocator().apply { - newName("outputStream") - newName("serializer") - } - val parameterName = nameAllocator.newName(rootField.variableName) - addJavadoc( - """ - Write${'$'}W{@link $1T}${'$'}Wto${'$'}Wthe${'$'}WXML${'$'}Wfile. - - @param $2L the${'$'}W{@link ${'$'}1T}${'$'}Wto${'$'}Wpersist - """.trimIndent(), rootField.type, parameterName - ) - addAnnotation(nullableType) - addModifiers(Modifier.PUBLIC) - addParameter( - ParameterSpec.builder(rootField.type, parameterName) - .addAnnotation(nonNullType) - .build() - ) - addStatement("\$1T outputStream = null", FileOutputStream::class.java) - addControlFlow("try") { - addStatement("outputStream = mFile.startWrite()") - addStatement( - "final \$1T serializer =\$W\$2T.newSerializer()", xmlSerializerType, xmlType - ) - addStatement( - "serializer.setOutput(outputStream, \$1T.UTF_8.name())", - StandardCharsets::class.java - ) - addStatement( - "serializer.setFeature(\$1S, true)", - "http://xmlpull.org/v1/doc/features.html#indent-output" - ) - addStatement("serializer.startDocument(null, true)") - addStatement("serialize(serializer,\$W\$1L)", parameterName) - addStatement("serializer.endDocument()") - addStatement("mFile.finishWrite(outputStream)") - nextControlFlow("catch (Exception e)") - addStatement("e.printStackTrace()") - addStatement("mFile.failWrite(outputStream)") - } - } - .build() - -private fun generateSerializeMethod(rootField: ClassFieldInfo): MethodSpec = - MethodSpec.methodBuilder("serialize") - .addModifiers(Modifier.PRIVATE, Modifier.STATIC) - .addParameter( - ParameterSpec.builder(xmlSerializerType, "serializer") - .addAnnotation(nonNullType) - .build() - ) - .apply { - val nameAllocator = NameAllocator().apply { newName("serializer") } - val parameterName = nameAllocator.newName(rootField.variableName) - addParameter( - ParameterSpec.builder(rootField.type, parameterName) - .addAnnotation(nonNullType) - .build() - ) - addException(IOException::class.java) - addStatement("serializer.startTag(null, \$1S)", rootField.tagName) - addStatement("\$1L(serializer, \$2L)", rootField.serializeMethodName, parameterName) - addStatement("serializer.endTag(null, \$1S)", rootField.tagName) - } - .build() - -private fun generateSerializeClassMethod(classField: ClassFieldInfo): MethodSpec = - MethodSpec.methodBuilder(classField.serializeMethodName) - .addModifiers(Modifier.PRIVATE, Modifier.STATIC) - .addParameter( - ParameterSpec.builder(xmlSerializerType, "serializer") - .addAnnotation(nonNullType) - .build() - ) - .apply { - val nameAllocator = NameAllocator().apply { - newName("serializer") - newName("i") - } - val parameterName = nameAllocator.newName(classField.serializeParameterName) - addParameter( - ParameterSpec.builder(classField.type, parameterName) - .addAnnotation(nonNullType) - .build() - ) - addException(IOException::class.java) - val (attributeFields, tagFields) = classField.fields - .partition { it is PrimitiveFieldInfo || it is StringFieldInfo } - for (field in attributeFields) { - val variableName = "$parameterName.${field.name}" - if (!field.isRequired) { - beginControlFlow("if (\$1L != null)", variableName) - } - when (field) { - is PrimitiveFieldInfo -> { - if (field.isRequired && !field.type.isPrimitive) { - addControlFlow("if (\$1L == null)", variableName) { - addStatement( - "throw new IllegalArgumentException(\$1S)", - "Field \"${field.name}\" is null" - ) - } - } - val stringVariableName = - nameAllocator.newName("${field.variableName}String") - addStatement( - "final String \$1L =\$WString.valueOf(\$2L)", stringVariableName, - variableName - ) - addStatement( - "serializer.attribute(null, \$1S, \$2L)", field.attributeName, - stringVariableName - ) - } - is StringFieldInfo -> { - if (field.isRequired) { - addControlFlow("if (\$1L == null)", variableName) { - addStatement( - "throw new IllegalArgumentException(\$1S)", - "Field \"${field.name}\" is null" - ) - } - } - addStatement( - "serializer.attribute(null, \$1S, \$2L)", field.attributeName, - variableName - ) - } - else -> error(field) - } - if (!field.isRequired) { - endControlFlow() - } - } - for (field in tagFields) { - val variableName = "$parameterName.${field.name}" - if (field.isRequired) { - addControlFlow("if (\$1L == null)", variableName) { - addStatement( - "throw new IllegalArgumentException(\$1S)", - "Field \"${field.name}\" is null" - ) - } - } - when (field) { - is ClassFieldInfo -> { - addStatement("serializer.startTag(null, \$1S)", field.tagName) - addStatement( - "\$1L(serializer, \$2L)", field.serializeMethodName, variableName - ) - addStatement("serializer.endTag(null, \$1S)", field.tagName) - } - is ListFieldInfo -> { - val sizeVariableName = nameAllocator.newName("${field.variableName}Size") - addStatement( - "final int \$1L =\$W\$2L.size()", sizeVariableName, variableName - ) - addControlFlow("for (int i = 0;\$Wi < \$1L;\$Wi++)", sizeVariableName) { - val elementNameAllocator = nameAllocator.clone() - val elementVariableName = elementNameAllocator.newName( - field.element.xmlName!!.toLowerCamelCase() - ) - addStatement( - "final \$1T \$2L =\$W\$3L.get(i)", field.element.type, - elementVariableName, variableName - ) - addControlFlow("if (\$1L == null)", elementVariableName) { - addStatement( - "throw new IllegalArgumentException(\$1S\$W+ i\$W+ \$2S)", - "Field element \"${field.name}[", "]\" is null" - ) - } - addStatement("serializer.startTag(null, \$1S)", field.element.tagName) - addStatement( - "\$1L(serializer,\$W\$2L)", field.element.serializeMethodName, - elementVariableName - ) - addStatement("serializer.endTag(null, \$1S)", field.element.tagName) - } - } - else -> error(field) - } - } - } - .build() - -private val ClassFieldInfo.serializeMethodName: String - get() = "serialize${type.simpleName().toUpperCamelCase()}" - -private val ClassFieldInfo.serializeParameterName: String - get() = type.simpleName().toLowerCamelCase() - -private val FieldInfo.variableName: String - get() = name.toLowerCamelCase() - -private val FieldInfo.attributeName: String - get() { - check(this is PrimitiveFieldInfo || this is StringFieldInfo) - return xmlNameOrName.toLowerCamelCase() - } - -private val FieldInfo.tagName: String - get() { - check(this is ClassFieldInfo || this is ListFieldInfo) - return xmlNameOrName.toLowerKebabCase() - } - -private val FieldInfo.xmlNameOrName: String - get() = xmlName ?: name - -private fun generateDeleteMethod(): MethodSpec = - MethodSpec.methodBuilder("delete") - .addJavadoc("Delete the XML file, if any.") - .addModifiers(Modifier.PUBLIC) - .addStatement("mFile.delete()") - .build() - -private inline fun MethodSpec.Builder.addControlFlow( - controlFlow: String, - vararg args: Any, - block: MethodSpec.Builder.() -> Unit -): MethodSpec.Builder { - beginControlFlow(controlFlow, *args) - block() - endControlFlow() - return this -} diff --git a/tools/xmlpersistence/src/main/kotlin/Main.kt b/tools/xmlpersistence/src/main/kotlin/Main.kt deleted file mode 100644 index e271f8cb9361..000000000000 --- a/tools/xmlpersistence/src/main/kotlin/Main.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2020 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 java.io.File -import java.nio.file.Files - -fun main(args: Array<String>) { - val showUsage = args.isEmpty() || when (args.singleOrNull()) { - "-h", "--help" -> true - else -> false - } - if (showUsage) { - usage() - return - } - - val files = args.flatMap { - File(it).walk().filter { it.isFile && it.extension == "java" }.map { it.toPath() } - } - val persistences = parse(files) - for (persistence in persistences) { - val file = generate(persistence) - Files.newBufferedWriter(persistence.path).use { - it.write(FILE_HEADER) - file.writeTo(it) - } - } -} - -private fun usage() { - println("Usage: xmlpersistence <FILES>") -} diff --git a/tools/xmlpersistence/src/main/kotlin/Parser.kt b/tools/xmlpersistence/src/main/kotlin/Parser.kt deleted file mode 100644 index 3ea12a9aa389..000000000000 --- a/tools/xmlpersistence/src/main/kotlin/Parser.kt +++ /dev/null @@ -1,248 +0,0 @@ -/* - * Copyright (C) 2020 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 com.github.javaparser.JavaParser -import com.github.javaparser.ParseProblemException -import com.github.javaparser.ParseResult -import com.github.javaparser.ParserConfiguration -import com.github.javaparser.ast.Node -import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration -import com.github.javaparser.ast.body.FieldDeclaration -import com.github.javaparser.ast.body.TypeDeclaration -import com.github.javaparser.ast.expr.AnnotationExpr -import com.github.javaparser.ast.expr.Expression -import com.github.javaparser.ast.expr.NormalAnnotationExpr -import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr -import com.github.javaparser.ast.expr.StringLiteralExpr -import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration -import com.github.javaparser.resolution.types.ResolvedPrimitiveType -import com.github.javaparser.resolution.types.ResolvedReferenceType -import com.github.javaparser.symbolsolver.JavaSymbolSolver -import com.github.javaparser.symbolsolver.javaparsermodel.declarations.JavaParserClassDeclaration -import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver -import com.github.javaparser.symbolsolver.resolution.typesolvers.MemoryTypeSolver -import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver -import com.squareup.javapoet.ClassName -import com.squareup.javapoet.ParameterizedTypeName -import com.squareup.javapoet.TypeName -import java.nio.file.Path -import java.util.Optional - -class PersistenceInfo( - val name: String, - val root: ClassFieldInfo, - val path: Path -) - -sealed class FieldInfo { - abstract val name: String - abstract val xmlName: String? - abstract val type: TypeName - abstract val isRequired: Boolean -} - -class PrimitiveFieldInfo( - override val name: String, - override val xmlName: String?, - override val type: TypeName, - override val isRequired: Boolean -) : FieldInfo() - -class StringFieldInfo( - override val name: String, - override val xmlName: String?, - override val isRequired: Boolean -) : FieldInfo() { - override val type: TypeName = ClassName.get(String::class.java) -} - -class ClassFieldInfo( - override val name: String, - override val xmlName: String?, - override val type: ClassName, - override val isRequired: Boolean, - val fields: List<FieldInfo> -) : FieldInfo() - -class ListFieldInfo( - override val name: String, - override val xmlName: String?, - override val type: ParameterizedTypeName, - val element: ClassFieldInfo -) : FieldInfo() { - override val isRequired: Boolean = true -} - -fun parse(files: List<Path>): List<PersistenceInfo> { - val typeSolver = CombinedTypeSolver().apply { add(ReflectionTypeSolver()) } - val javaParser = JavaParser(ParserConfiguration() - .setSymbolResolver(JavaSymbolSolver(typeSolver))) - val compilationUnits = files.map { javaParser.parse(it).getOrThrow() } - val memoryTypeSolver = MemoryTypeSolver().apply { - for (compilationUnit in compilationUnits) { - for (typeDeclaration in compilationUnit.getNodesByClass<TypeDeclaration<*>>()) { - val name = typeDeclaration.fullyQualifiedName.getOrNull() ?: continue - addDeclaration(name, typeDeclaration.resolve()) - } - } - } - typeSolver.add(memoryTypeSolver) - return mutableListOf<PersistenceInfo>().apply { - for (compilationUnit in compilationUnits) { - val classDeclarations = compilationUnit - .getNodesByClass<ClassOrInterfaceDeclaration>() - .filter { !it.isInterface && (!it.isNestedType || it.isStatic) } - this += classDeclarations.mapNotNull { parsePersistenceInfo(it) } - } - } -} - -private fun parsePersistenceInfo(classDeclaration: ClassOrInterfaceDeclaration): PersistenceInfo? { - val annotation = classDeclaration.getAnnotationByName("XmlPersistence").getOrNull() - ?: return null - val rootClassName = classDeclaration.nameAsString - val name = annotation.getMemberValue("value")?.stringLiteralValue - ?: "${rootClassName}Persistence" - val rootXmlName = classDeclaration.getAnnotationByName("XmlName").getOrNull() - ?.getMemberValue("value")?.stringLiteralValue - val root = parseClassFieldInfo( - rootXmlName ?: rootClassName, rootXmlName, true, classDeclaration - ) - val path = classDeclaration.findCompilationUnit().get().storage.get().path - .resolveSibling("$name.java") - return PersistenceInfo(name, root, path) -} - -private fun parseClassFieldInfo( - name: String, - xmlName: String?, - isRequired: Boolean, - classDeclaration: ClassOrInterfaceDeclaration -): ClassFieldInfo { - val fields = classDeclaration.fields.filterNot { it.isStatic }.map { parseFieldInfo(it) } - val type = classDeclaration.resolve().typeName - return ClassFieldInfo(name, xmlName, type, isRequired, fields) -} - -private fun parseFieldInfo(field: FieldDeclaration): FieldInfo { - require(field.isPublic && field.isFinal) - val variable = field.variables.single() - val name = variable.nameAsString - val annotations = field.annotations + variable.type.annotations - val annotation = annotations.getByName("XmlName") - val xmlName = annotation?.getMemberValue("value")?.stringLiteralValue - val isRequired = annotations.getByName("NonNull") != null - return when (val type = variable.type.resolve()) { - is ResolvedPrimitiveType -> { - val primitiveType = type.typeName - PrimitiveFieldInfo(name, xmlName, primitiveType, true) - } - is ResolvedReferenceType -> { - when (type.qualifiedName) { - Boolean::class.javaObjectType.name, Byte::class.javaObjectType.name, - Short::class.javaObjectType.name, Char::class.javaObjectType.name, - Integer::class.javaObjectType.name, Long::class.javaObjectType.name, - Float::class.javaObjectType.name, Double::class.javaObjectType.name -> - PrimitiveFieldInfo(name, xmlName, type.typeName, isRequired) - String::class.java.name -> StringFieldInfo(name, xmlName, isRequired) - List::class.java.name -> { - requireNotNull(xmlName) - val elementType = type.typeParametersValues().single() - require(elementType is ResolvedReferenceType) - val listType = ParameterizedTypeName.get( - ClassName.get(List::class.java), elementType.typeName - ) - val element = parseClassFieldInfo( - "(element)", xmlName, true, elementType.classDeclaration - ) - ListFieldInfo(name, xmlName, listType, element) - } - else -> parseClassFieldInfo(name, xmlName, isRequired, type.classDeclaration) - } - } - else -> error(type) - } -} - -private fun <T> ParseResult<T>.getOrThrow(): T = - if (isSuccessful) { - result.get() - } else { - throw ParseProblemException(problems) - } - -private inline fun <reified T : Node> Node.getNodesByClass(): List<T> = - getNodesByClass(T::class.java) - -private fun <T : Node> Node.getNodesByClass(klass: Class<T>): List<T> = mutableListOf<T>().apply { - if (klass.isInstance(this@getNodesByClass)) { - this += klass.cast(this@getNodesByClass) - } - for (childNode in childNodes) { - this += childNode.getNodesByClass(klass) - } -} - -private fun <T> Optional<T>.getOrNull(): T? = orElse(null) - -private fun List<AnnotationExpr>.getByName(name: String): AnnotationExpr? = - find { it.name.identifier == name } - -private fun AnnotationExpr.getMemberValue(name: String): Expression? = - when (this) { - is NormalAnnotationExpr -> pairs.find { it.nameAsString == name }?.value - is SingleMemberAnnotationExpr -> if (name == "value") memberValue else null - else -> null - } - -private val Expression.stringLiteralValue: String - get() { - require(this is StringLiteralExpr) - return value - } - -private val ResolvedReferenceType.classDeclaration: ClassOrInterfaceDeclaration - get() { - val resolvedClassDeclaration = typeDeclaration - require(resolvedClassDeclaration is JavaParserClassDeclaration) - return resolvedClassDeclaration.wrappedNode - } - -private val ResolvedPrimitiveType.typeName: TypeName - get() = - when (this) { - ResolvedPrimitiveType.BOOLEAN -> TypeName.BOOLEAN - ResolvedPrimitiveType.BYTE -> TypeName.BYTE - ResolvedPrimitiveType.SHORT -> TypeName.SHORT - ResolvedPrimitiveType.CHAR -> TypeName.CHAR - ResolvedPrimitiveType.INT -> TypeName.INT - ResolvedPrimitiveType.LONG -> TypeName.LONG - ResolvedPrimitiveType.FLOAT -> TypeName.FLOAT - ResolvedPrimitiveType.DOUBLE -> TypeName.DOUBLE - } - -// This doesn't support type parameters. -private val ResolvedReferenceType.typeName: TypeName - get() = typeDeclaration.typeName - -private val ResolvedReferenceTypeDeclaration.typeName: ClassName - get() { - val packageName = packageName - val classNames = className.split(".") - val topLevelClassName = classNames.first() - val nestedClassNames = classNames.drop(1) - return ClassName.get(packageName, topLevelClassName, *nestedClassNames.toTypedArray()) - } diff --git a/tools/xmlpersistence/src/main/kotlin/StringCaseExtensions.kt b/tools/xmlpersistence/src/main/kotlin/StringCaseExtensions.kt deleted file mode 100644 index b4bdbba7170b..000000000000 --- a/tools/xmlpersistence/src/main/kotlin/StringCaseExtensions.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2020 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 java.util.Locale - -private val camelHumpBoundary = Regex( - "-" - + "|_" - + "|(?<=[0-9])(?=[^0-9])" - + "|(?<=[A-Z])(?=[^A-Za-z]|[A-Z][a-z])" - + "|(?<=[a-z])(?=[^a-z])" -) - -private fun String.toCamelHumps(): List<String> = split(camelHumpBoundary) - -fun String.toUpperCamelCase(): String = - toCamelHumps().joinToString("") { it.toLowerCase(Locale.ROOT).capitalize(Locale.ROOT) } - -fun String.toLowerCamelCase(): String = toUpperCamelCase().decapitalize(Locale.ROOT) - -fun String.toUpperKebabCase(): String = - toCamelHumps().joinToString("-") { it.toUpperCase(Locale.ROOT) } - -fun String.toLowerKebabCase(): String = - toCamelHumps().joinToString("-") { it.toLowerCase(Locale.ROOT) } - -fun String.toUpperSnakeCase(): String = - toCamelHumps().joinToString("_") { it.toUpperCase(Locale.ROOT) } - -fun String.toLowerSnakeCase(): String = - toCamelHumps().joinToString("_") { it.toLowerCase(Locale.ROOT) } |