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

Commit 3f9ad6d8 authored by Mark Punzalan's avatar Mark Punzalan Committed by Automerger Merge Worker
Browse files

Merge "[aapt2] Implement FeatureFlagsFilter" into main am: 3fc4d396 am: 6095d3d1

parents 50777328 6095d3d1
Loading
Loading
Loading
Loading
+1 −0
Original line number Original line Diff line number Diff line
@@ -119,6 +119,7 @@ cc_library_host_static {
        "io/Util.cpp",
        "io/Util.cpp",
        "io/ZipArchive.cpp",
        "io/ZipArchive.cpp",
        "link/AutoVersioner.cpp",
        "link/AutoVersioner.cpp",
        "link/FeatureFlagsFilter.cpp",
        "link/ManifestFixer.cpp",
        "link/ManifestFixer.cpp",
        "link/NoDefaultResourceRemover.cpp",
        "link/NoDefaultResourceRemover.cpp",
        "link/PrivateAttributeMover.cpp",
        "link/PrivateAttributeMover.cpp",
+104 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright 2023 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.
 */

#include "link/FeatureFlagsFilter.h"

#include <string_view>

#include "androidfw/IDiagnostics.h"
#include "androidfw/Source.h"
#include "util/Util.h"
#include "xml/XmlDom.h"
#include "xml/XmlUtil.h"

using ::aapt::xml::Element;
using ::aapt::xml::Node;
using ::aapt::xml::NodeCast;

namespace aapt {

class FlagsVisitor : public xml::Visitor {
 public:
  explicit FlagsVisitor(android::IDiagnostics* diagnostics,
                        const FeatureFlagValues& feature_flag_values,
                        const FeatureFlagsFilterOptions& options)
      : diagnostics_(diagnostics), feature_flag_values_(feature_flag_values), options_(options) {
  }

  void Visit(xml::Element* node) override {
    std::erase_if(node->children,
                  [this](std::unique_ptr<xml::Node>& node) { return ShouldRemove(node); });
    VisitChildren(node);
  }

  bool HasError() const {
    return has_error_;
  }

 private:
  bool ShouldRemove(std::unique_ptr<xml::Node>& node) {
    if (const auto* el = NodeCast<Element>(node.get())) {
      auto* attr = el->FindAttribute(xml::kSchemaAndroid, "featureFlag");
      if (attr == nullptr) {
        return false;
      }

      bool negated = false;
      std::string_view flag_name = util::TrimWhitespace(attr->value);
      if (flag_name.starts_with('!')) {
        negated = true;
        flag_name = flag_name.substr(1);
      }

      if (auto it = feature_flag_values_.find(std::string(flag_name));
          it != feature_flag_values_.end()) {
        if (it->second.has_value()) {
          if (options_.remove_disabled_elements) {
            // Remove if flag==true && attr=="!flag" (negated) OR flag==false && attr=="flag"
            return *it->second == negated;
          }
        } else if (options_.flags_must_have_value) {
          diagnostics_->Error(android::DiagMessage(node->line_number)
                              << "attribute 'android:featureFlag' has flag '" << flag_name
                              << "' without a true/false value from --feature_flags parameter");
          has_error_ = true;
          return false;
        }
      } else if (options_.fail_on_unrecognized_flags) {
        diagnostics_->Error(android::DiagMessage(node->line_number)
                            << "attribute 'android:featureFlag' has flag '" << flag_name
                            << "' not found in flags from --feature_flags parameter");
        has_error_ = true;
        return false;
      }
    }

    return false;
  }

  android::IDiagnostics* diagnostics_;
  const FeatureFlagValues& feature_flag_values_;
  const FeatureFlagsFilterOptions& options_;
  bool has_error_ = false;
};

bool FeatureFlagsFilter::Consume(IAaptContext* context, xml::XmlResource* doc) {
  FlagsVisitor visitor(context->GetDiagnostics(), feature_flag_values_, options_);
  doc->root->Accept(&visitor);
  return !visitor.HasError();
}

}  // namespace aapt
+79 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright 2023 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.
 */

#pragma once

#include <optional>
#include <string>
#include <unordered_map>
#include <utility>

#include "android-base/macros.h"
#include "cmd/Util.h"
#include "process/IResourceTableConsumer.h"

namespace aapt {

struct FeatureFlagsFilterOptions {
  // If true, elements whose featureFlag values are false (i.e., disabled feature) will be removed.
  bool remove_disabled_elements = true;

  // If true, `Consume()` will return false (error) if a flag was found that is not in
  // `feature_flag_values`.
  bool fail_on_unrecognized_flags = true;

  // If true, `Consume()` will return false (error) if a flag was found whose value in
  // `feature_flag_values` is not defined (std::nullopt).
  bool flags_must_have_value = true;
};

// Looks for the `android:featureFlag` attribute in each XML element, validates the flag names and
// values, and removes elements according to the values in `feature_flag_values`. An element will be
// removed if the flag's given value is FALSE. A "!" before the flag name in the attribute indicates
// a boolean NOT operation, i.e., an element will be removed if the flag's given value is TRUE. For
// example, if the XML is the following:
//
//   <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
//     <permission android:name="FOO" android:featureFlag="!flag"
//                 android:protectionLevel="normal" />
//     <permission android:name="FOO" android:featureFlag="flag"
//                 android:protectionLevel="dangerous" />
//   </manifest>
//
// If `feature_flag_values` contains {"flag", true}, then the <permission> element with
// protectionLevel="normal" will be removed, and the <permission> element with
// protectionLevel="normal" will be kept.
//
// The `Consume()` function will return false if there is an invalid flag found (see
// FeatureFlagsFilterOptions for customizing the filter's validation behavior). Do not use the XML
// further if there are errors as there may be elements removed already.
class FeatureFlagsFilter : public IXmlResourceConsumer {
 public:
  explicit FeatureFlagsFilter(FeatureFlagValues feature_flag_values,
                              FeatureFlagsFilterOptions options)
      : feature_flag_values_(std::move(feature_flag_values)), options_(options) {
  }

  bool Consume(IAaptContext* context, xml::XmlResource* doc) override;

 private:
  DISALLOW_COPY_AND_ASSIGN(FeatureFlagsFilter);

  const FeatureFlagValues feature_flag_values_;
  const FeatureFlagsFilterOptions options_;
};

}  // namespace aapt
+236 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright 2023 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.
 */

#include "link/FeatureFlagsFilter.h"

#include <string_view>

#include "test/Test.h"

using ::testing::IsNull;
using ::testing::NotNull;

namespace aapt {

// Returns null if there was an error from FeatureFlagsFilter.
std::unique_ptr<xml::XmlResource> VerifyWithOptions(std::string_view str,
                                                    const FeatureFlagValues& feature_flag_values,
                                                    const FeatureFlagsFilterOptions& options) {
  std::unique_ptr<xml::XmlResource> doc = test::BuildXmlDom(str);
  FeatureFlagsFilter filter(feature_flag_values, options);
  if (filter.Consume(test::ContextBuilder().Build().get(), doc.get())) {
    return doc;
  }
  return {};
}

// Returns null if there was an error from FeatureFlagsFilter.
std::unique_ptr<xml::XmlResource> Verify(std::string_view str,
                                         const FeatureFlagValues& feature_flag_values) {
  return VerifyWithOptions(str, feature_flag_values, {});
}

TEST(FeatureFlagsFilterTest, NoFeatureFlagAttributes) {
  auto doc = Verify(R"EOF(
    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
      <permission android:name="FOO" />
    </manifest>)EOF",
                    {{"flag", false}});
  ASSERT_THAT(doc, NotNull());
  auto root = doc->root.get();
  ASSERT_THAT(root, NotNull());
  auto maybe_removed = root->FindChild({}, "permission");
  ASSERT_THAT(maybe_removed, NotNull());
}
TEST(FeatureFlagsFilterTest, RemoveElementWithDisabledFlag) {
  auto doc = Verify(R"EOF(
    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
      <permission android:name="FOO" android:featureFlag="flag" />
    </manifest>)EOF",
                    {{"flag", false}});
  ASSERT_THAT(doc, NotNull());
  auto root = doc->root.get();
  ASSERT_THAT(root, NotNull());
  auto maybe_removed = root->FindChild({}, "permission");
  ASSERT_THAT(maybe_removed, IsNull());
}

TEST(FeatureFlagsFilterTest, RemoveElementWithNegatedEnabledFlag) {
  auto doc = Verify(R"EOF(
    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
      <permission android:name="FOO" android:featureFlag="!flag" />
    </manifest>)EOF",
                    {{"flag", true}});
  ASSERT_THAT(doc, NotNull());
  auto root = doc->root.get();
  ASSERT_THAT(root, NotNull());
  auto maybe_removed = root->FindChild({}, "permission");
  ASSERT_THAT(maybe_removed, IsNull());
}

TEST(FeatureFlagsFilterTest, KeepElementWithEnabledFlag) {
  auto doc = Verify(R"EOF(
    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
      <permission android:name="FOO" android:featureFlag="flag" />
    </manifest>)EOF",
                    {{"flag", true}});
  ASSERT_THAT(doc, NotNull());
  auto root = doc->root.get();
  ASSERT_THAT(root, NotNull());
  auto maybe_removed = root->FindChild({}, "permission");
  ASSERT_THAT(maybe_removed, NotNull());
}

TEST(FeatureFlagsFilterTest, SideBySideEnabledAndDisabled) {
  auto doc = Verify(R"EOF(
    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
      <permission android:name="FOO" android:featureFlag="!flag"
                  android:protectionLevel="normal" />
      <permission android:name="FOO" android:featureFlag="flag"
                  android:protectionLevel="dangerous" />
    </manifest>)EOF",
                    {{"flag", true}});
  ASSERT_THAT(doc, NotNull());
  auto root = doc->root.get();
  ASSERT_THAT(root, NotNull());
  auto children = root->GetChildElements();
  ASSERT_EQ(children.size(), 1);
  auto attr = children[0]->FindAttribute(xml::kSchemaAndroid, "protectionLevel");
  ASSERT_THAT(attr, NotNull());
  ASSERT_EQ(attr->value, "dangerous");
}

TEST(FeatureFlagsFilterTest, RemoveDeeplyNestedElement) {
  auto doc = Verify(R"EOF(
    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
      <application>
        <provider />
        <activity>
          <layout android:featureFlag="!flag" />
        </activity>
      </application>
    </manifest>)EOF",
                    {{"flag", true}});
  ASSERT_THAT(doc, NotNull());
  auto root = doc->root.get();
  ASSERT_THAT(root, NotNull());
  auto application = root->FindChild({}, "application");
  ASSERT_THAT(application, NotNull());
  auto activity = application->FindChild({}, "activity");
  ASSERT_THAT(activity, NotNull());
  auto maybe_removed = activity->FindChild({}, "layout");
  ASSERT_THAT(maybe_removed, IsNull());
}

TEST(FeatureFlagsFilterTest, KeepDeeplyNestedElement) {
  auto doc = Verify(R"EOF(
    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
      <application>
        <provider />
        <activity>
          <layout android:featureFlag="flag" />
        </activity>
      </application>
    </manifest>)EOF",
                    {{"flag", true}});
  ASSERT_THAT(doc, NotNull());
  auto root = doc->root.get();
  ASSERT_THAT(root, NotNull());
  auto application = root->FindChild({}, "application");
  ASSERT_THAT(application, NotNull());
  auto activity = application->FindChild({}, "activity");
  ASSERT_THAT(activity, NotNull());
  auto maybe_removed = activity->FindChild({}, "layout");
  ASSERT_THAT(maybe_removed, NotNull());
}

TEST(FeatureFlagsFilterTest, FailOnEmptyFeatureFlagAttribute) {
  auto doc = Verify(R"EOF(
    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
      <permission android:name="FOO" android:featureFlag=" " />
    </manifest>)EOF",
                    {{"flag", false}});
  ASSERT_THAT(doc, IsNull());
}

TEST(FeatureFlagsFilterTest, FailOnFlagWithNoGivenValue) {
  auto doc = Verify(R"EOF(
    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
      <permission android:name="FOO" android:featureFlag="flag" />
    </manifest>)EOF",
                    {{"flag", std::nullopt}});
  ASSERT_THAT(doc, IsNull());
}

TEST(FeatureFlagsFilterTest, FailOnUnrecognizedFlag) {
  auto doc = Verify(R"EOF(
    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
      <permission android:name="FOO" android:featureFlag="unrecognized" />
    </manifest>)EOF",
                    {{"flag", true}});
  ASSERT_THAT(doc, IsNull());
}

TEST(FeatureFlagsFilterTest, FailOnMultipleValidationErrors) {
  auto doc = Verify(R"EOF(
    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
      <permission android:name="FOO" android:featureFlag="bar" />
      <permission android:name="FOO" android:featureFlag="unrecognized" />
    </manifest>)EOF",
                    {{"flag", std::nullopt}});
  ASSERT_THAT(doc, IsNull());
}

TEST(FeatureFlagsFilterTest, OptionRemoveDisabledElementsIsFalse) {
  auto doc = VerifyWithOptions(R"EOF(
    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
      <permission android:name="FOO" android:featureFlag="flag" />
    </manifest>)EOF",
                               {{"flag", false}}, {.remove_disabled_elements = false});
  ASSERT_THAT(doc, NotNull());
  auto root = doc->root.get();
  ASSERT_THAT(root, NotNull());
  auto maybe_removed = root->FindChild({}, "permission");
  ASSERT_THAT(maybe_removed, NotNull());
}

TEST(FeatureFlagsFilterTest, OptionFlagsMustHaveValueIsFalse) {
  auto doc = VerifyWithOptions(R"EOF(
    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
      <permission android:name="FOO" android:featureFlag="flag" />
    </manifest>)EOF",
                               {{"flag", std::nullopt}}, {.flags_must_have_value = false});
  ASSERT_THAT(doc, NotNull());
  auto root = doc->root.get();
  ASSERT_THAT(root, NotNull());
  auto maybe_removed = root->FindChild({}, "permission");
  ASSERT_THAT(maybe_removed, NotNull());
}

TEST(FeatureFlagsFilterTest, OptionFailOnUnrecognizedFlagsIsFalse) {
  auto doc = VerifyWithOptions(R"EOF(
    <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
      <permission android:name="FOO" android:featureFlag="unrecognized" />
    </manifest>)EOF",
                               {{"flag", true}}, {.fail_on_unrecognized_flags = false});
  ASSERT_THAT(doc, NotNull());
  auto root = doc->root.get();
  ASSERT_THAT(root, NotNull());
  auto maybe_removed = root->FindChild({}, "permission");
  ASSERT_THAT(maybe_removed, NotNull());
}

}  // namespace aapt