Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3cff2550 authored by Brandon Liu's avatar Brandon Liu
Browse files

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

Bug: 241114745
Test: Added and verified affected atests pass
Change-Id: I1aa5148b1b0722e9fde33be6cbb6277ebfb6e10d
parent 316f451f
Loading
Loading
Loading
Loading
+86 −0
Original line number Diff line number Diff line
@@ -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"];
+341 −0
Original line number Diff line number Diff line
@@ -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