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

Commit 669965a8 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

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

parents a950a6f9 3cff2550
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