Force path parts in intent filter with deeplinks to have leading slash.

Bug: 241114745
Test: Added and verified affected atests pass
Change-Id: I1aa5148b1b0722e9fde33be6cbb6277ebfb6e10d
This commit is contained in:
Brandon Liu
2022-10-19 18:51:07 +00:00
parent 316f451fb6
commit 3cff2550ca
2 changed files with 427 additions and 0 deletions

View File

@@ -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"];

View File

@@ -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