diff options
| author | 2022-10-25 20:27:45 +0000 | |
|---|---|---|
| committer | 2022-10-25 20:27:45 +0000 | |
| commit | 669965a8cd178c1443db3f8f98cd0d3275e7241d (patch) | |
| tree | ced1b027f1bf926992b3dad6643bc6c82cf8bd8f | |
| parent | a950a6f9e4cd2d897ffa77adeeb42456f2622fe6 (diff) | |
| parent | 3cff2550cace59b26374a3eea76555915cc45dfb (diff) | |
Merge "Force path parts in intent filter with deeplinks to have leading slash."
| -rw-r--r-- | tools/aapt2/link/ManifestFixer.cpp | 86 | ||||
| -rw-r--r-- | tools/aapt2/link/ManifestFixer_test.cpp | 341 |
2 files changed, 427 insertions, 0 deletions
diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp index 5cee17e5f3f9..df09e47aa946 100644 --- a/tools/aapt2/link/ManifestFixer.cpp +++ b/tools/aapt2/link/ManifestFixer.cpp @@ -30,6 +30,91 @@ using android::StringPiece; namespace aapt { +// This is to detect whether an <intent-filter> contains deeplink. +// See https://developer.android.com/training/app-links/deep-linking. +static bool HasDeepLink(xml::Element* intent_filter_el) { + xml::Element* action_el = intent_filter_el->FindChild({}, "action"); + xml::Element* category_el = intent_filter_el->FindChild({}, "category"); + xml::Element* data_el = intent_filter_el->FindChild({}, "data"); + if (action_el == nullptr || category_el == nullptr || data_el == nullptr) { + return false; + } + + // Deeplinks must specify the ACTION_VIEW intent action. + constexpr const char* action_view = "android.intent.action.VIEW"; + if (intent_filter_el->FindChildWithAttribute({}, "action", xml::kSchemaAndroid, "name", + action_view) == nullptr) { + return false; + } + + // Deeplinks must have scheme included in <data> tag. + xml::Attribute* data_scheme_attr = data_el->FindAttribute(xml::kSchemaAndroid, "scheme"); + if (data_scheme_attr == nullptr || data_scheme_attr->value.empty()) { + return false; + } + + // Deeplinks must include BROWSABLE category. + constexpr const char* category_browsable = "android.intent.category.BROWSABLE"; + if (intent_filter_el->FindChildWithAttribute({}, "category", xml::kSchemaAndroid, "name", + category_browsable) == nullptr) { + return false; + } + return true; +} + +static bool VerifyDeeplinkPathAttribute(xml::Element* data_el, android::SourcePathDiagnostics* diag, + const std::string& attr_name) { + xml::Attribute* attr = data_el->FindAttribute(xml::kSchemaAndroid, attr_name); + if (attr != nullptr && !attr->value.empty()) { + StringPiece attr_value = attr->value; + const char* startChar = attr_value.begin(); + if (attr_name == "pathPattern") { + if (*startChar == '/' || *startChar == '.' || *startChar == '*') { + return true; + } else { + diag->Error(android::DiagMessage(data_el->line_number) + << "attribute 'android:" << attr_name << "' in <" << data_el->name + << "> tag has value of '" << attr_value + << "', it must be in a pattern start with '.' or '*', otherwise must start " + "with a leading slash '/'"); + return false; + } + } else { + if (*startChar == '/') { + return true; + } else { + diag->Error(android::DiagMessage(data_el->line_number) + << "attribute 'android:" << attr_name << "' in <" << data_el->name + << "> tag has value of '" << attr_value + << "', it must start with a leading slash '/'"); + return false; + } + } + } + return true; +} + +static bool VerifyDeepLinkIntentAction(xml::Element* intent_filter_el, + android::SourcePathDiagnostics* diag) { + if (!HasDeepLink(intent_filter_el)) { + return true; + } + + xml::Element* data_el = intent_filter_el->FindChild({}, "data"); + if (data_el != nullptr) { + if (!VerifyDeeplinkPathAttribute(data_el, diag, "path")) { + return false; + } + if (!VerifyDeeplinkPathAttribute(data_el, diag, "pathPrefix")) { + return false; + } + if (!VerifyDeeplinkPathAttribute(data_el, diag, "pathPattern")) { + return false; + } + } + return true; +} + static bool RequiredNameIsNotEmpty(xml::Element* el, android::SourcePathDiagnostics* diag) { xml::Attribute* attr = el->FindAttribute(xml::kSchemaAndroid, "name"); if (attr == nullptr) { @@ -323,6 +408,7 @@ bool ManifestFixer::BuildRules(xml::XmlActionExecutor* executor, android::IDiagn // Common <intent-filter> actions. xml::XmlNodeAction intent_filter_action; + intent_filter_action.Action(VerifyDeepLinkIntentAction); intent_filter_action["action"].Action(RequiredNameIsNotEmpty); intent_filter_action["category"].Action(RequiredNameIsNotEmpty); intent_filter_action["data"]; diff --git a/tools/aapt2/link/ManifestFixer_test.cpp b/tools/aapt2/link/ManifestFixer_test.cpp index 098d0be7f87d..cec9a1a5917e 100644 --- a/tools/aapt2/link/ManifestFixer_test.cpp +++ b/tools/aapt2/link/ManifestFixer_test.cpp @@ -1068,4 +1068,345 @@ TEST_F(ManifestFixerTest, ComponentPropertyOnlyOneAttributeDefined) { </manifest>)"; EXPECT_THAT(Verify(input), NotNull()); } + +TEST_F(ManifestFixerTest, IntentFilterActionMustHaveNonEmptyName) { + std::string input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <action android:name="" /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), IsNull()); + + input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <action /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), IsNull()); + + input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), NotNull()); +} + +TEST_F(ManifestFixerTest, IntentFilterCategoryMustHaveNonEmptyName) { + std::string input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <category android:name="" /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), IsNull()); + + input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <category /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), IsNull()); + + input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), NotNull()); +} + +TEST_F(ManifestFixerTest, IntentFilterPathMustStartWithLeadingSlashOnDeepLinks) { + // No DeepLink. + std::string input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <data /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), NotNull()); + + // No DeepLink, missing ACTION_VIEW. + input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" + android:host="www.example.com" + android:pathPrefix="pathPattern" /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), NotNull()); + + // DeepLink, missing DEFAULT category while DEFAULT is recommended but not required. + input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" + android:host="www.example.com" + android:pathPrefix="pathPattern" /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), IsNull()); + + // No DeepLink, missing BROWSABLE category. + input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <data android:scheme="http" + android:host="www.example.com" + android:pathPrefix="pathPattern" /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), NotNull()); + + // No DeepLink, missing 'android:scheme' in <data> tag. + input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:host="www.example.com" + android:pathPrefix="pathPattern" /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), NotNull()); + + // No DeepLink, <action> is ACTION_MAIN not ACTION_VIEW. + input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" + android:host="www.example.com" + android:pathPrefix="pathPattern" /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), NotNull()); + + // DeepLink with no leading slash in android:path. + input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" + android:host="www.example.com" + android:path="path" /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), IsNull()); + + // DeepLink with leading slash in android:path. + input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" + android:host="www.example.com" + android:path="/path" /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), NotNull()); + + // DeepLink with no leading slash in android:pathPrefix. + input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" + android:host="www.example.com" + android:pathPrefix="pathPrefix" /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), IsNull()); + + // DeepLink with leading slash in android:pathPrefix. + input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" + android:host="www.example.com" + android:pathPrefix="/pathPrefix" /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), NotNull()); + + // DeepLink with no leading slash in android:pathPattern. + input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" + android:host="www.example.com" + android:pathPattern="pathPattern" /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), IsNull()); + + // DeepLink with leading slash in android:pathPattern. + input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" + android:host="www.example.com" + android:pathPattern="/pathPattern" /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), NotNull()); + + // DeepLink with '.' start in pathPattern. + input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" + android:host="www.example.com" + android:pathPattern=".*\\.pathPattern" /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), NotNull()); + + // DeepLink with '*' start in pathPattern. + input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application> + <activity android:name=".MainActivity"> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" + android:host="www.example.com" + android:pathPattern="*" /> + </intent-filter> + </activity> + </application> + </manifest>)"; + EXPECT_THAT(Verify(input), NotNull()); +} } // namespace aapt |