From 5766943f558c8fccc714bb3d0a96af70816a5545 Mon Sep 17 00:00:00 2001 From: Shane Farmer Date: Mon, 19 Jun 2017 12:52:04 -0700 Subject: [PATCH] AAPT2: Split APK by ABI. Added a FilterChain that can apply multiple filter steps to an APK file as it is being written to disk. The first filter applied is by ABI. If a library in the APK does not match the filter it is skipped. Added an AbiFilter that keeps files that are either not native libs or are for the set of wanted ABIs Test: ran unit tests locally Test: ran against an APK with ARM and x68 libs and diffed the results Change-Id: I3fb901d3de3513e85f2a2763a8e4487a28ed4881 --- tools/aapt2/Android.bp | 1 + tools/aapt2/LoadedApk.cpp | 13 +++ tools/aapt2/LoadedApk.h | 9 ++ tools/aapt2/cmd/Optimize.cpp | 92 +++++++++++++++++-- .../configuration/ConfigurationParser.cpp | 32 ++++--- .../aapt2/configuration/ConfigurationParser.h | 6 +- .../ConfigurationParser_test.cpp | 17 +++- tools/aapt2/filter/AbiFilter.cpp | 51 ++++++++++ tools/aapt2/filter/AbiFilter.h | 54 +++++++++++ tools/aapt2/filter/AbiFilter_test.cpp | 66 +++++++++++++ tools/aapt2/filter/Filter.h | 77 ++++++++++++++++ tools/aapt2/filter/Filter_test.cpp | 54 +++++++++++ 12 files changed, 450 insertions(+), 22 deletions(-) create mode 100644 tools/aapt2/filter/AbiFilter.cpp create mode 100644 tools/aapt2/filter/AbiFilter.h create mode 100644 tools/aapt2/filter/AbiFilter_test.cpp create mode 100644 tools/aapt2/filter/Filter.h create mode 100644 tools/aapt2/filter/Filter_test.cpp diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp index 1dc47f88ab927..d39a8a8237b7d 100644 --- a/tools/aapt2/Android.bp +++ b/tools/aapt2/Android.bp @@ -85,6 +85,7 @@ cc_library_host_static { "compile/Pseudolocalizer.cpp", "compile/XmlIdCollector.cpp", "configuration/ConfigurationParser.cpp", + "filter/AbiFilter.cpp", "filter/ConfigFilter.cpp", "flatten/Archive.cpp", "flatten/TableFlattener.cpp", diff --git a/tools/aapt2/LoadedApk.cpp b/tools/aapt2/LoadedApk.cpp index 8a8f8be205e7e..7e5efa15f61b8 100644 --- a/tools/aapt2/LoadedApk.cpp +++ b/tools/aapt2/LoadedApk.cpp @@ -57,6 +57,12 @@ std::unique_ptr LoadedApk::LoadApkFromPath(IAaptContext* context, bool LoadedApk::WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options, IArchiveWriter* writer) { + FilterChain empty; + return WriteToArchive(context, options, &empty, writer); +} + +bool LoadedApk::WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options, + FilterChain* filters, IArchiveWriter* writer) { std::set referenced_resources; // List the files being referenced in the resource table. for (auto& pkg : table_->packages) { @@ -89,6 +95,13 @@ bool LoadedApk::WriteToArchive(IAaptContext* context, const TableFlattenerOption continue; } + if (!filters->Keep(path)) { + if (context->IsVerbose()) { + context->GetDiagnostics()->Note(DiagMessage() << "Filtered '" << path << "' from APK."); + } + continue; + } + // The resource table needs to be re-serialized since it might have changed. if (path == "resources.arsc") { BigBuffer buffer(4096); diff --git a/tools/aapt2/LoadedApk.h b/tools/aapt2/LoadedApk.h index 59eb8161a868b..8aa9674aa2ed9 100644 --- a/tools/aapt2/LoadedApk.h +++ b/tools/aapt2/LoadedApk.h @@ -20,6 +20,7 @@ #include "androidfw/StringPiece.h" #include "ResourceTable.h" +#include "filter/Filter.h" #include "flatten/Archive.h" #include "flatten/TableFlattener.h" #include "io/ZipArchive.h" @@ -49,6 +50,14 @@ class LoadedApk { bool WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options, IArchiveWriter* writer); + /** + * Writes the APK on disk at the given path, while also removing the resource + * files that are not referenced in the resource table. The provided filter + * chain is applied to each entry in the APK file. + */ + bool WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options, + FilterChain* filters, IArchiveWriter* writer); + static std::unique_ptr LoadApkFromPath(IAaptContext* context, const android::StringPiece& path); diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index 194c0c80c2b23..0d69c892c103f 100644 --- a/tools/aapt2/cmd/Optimize.cpp +++ b/tools/aapt2/cmd/Optimize.cpp @@ -17,6 +17,7 @@ #include #include +#include "android-base/stringprintf.h" #include "androidfw/StringPiece.h" #include "Diagnostics.h" @@ -26,6 +27,8 @@ #include "SdkConstants.h" #include "ValueVisitor.h" #include "cmd/Util.h" +#include "configuration/ConfigurationParser.h" +#include "filter/AbiFilter.h" #include "flatten/TableFlattener.h" #include "flatten/XmlFlattener.h" #include "io/BigBufferInputStream.h" @@ -33,14 +36,21 @@ #include "optimize/ResourceDeduper.h" #include "optimize/VersionCollapser.h" #include "split/TableSplitter.h" +#include "util/Files.h" -using android::StringPiece; +using ::aapt::configuration::Abi; +using ::aapt::configuration::Artifact; +using ::aapt::configuration::Configuration; +using ::android::StringPiece; +using ::android::base::StringPrintf; namespace aapt { struct OptimizeOptions { // Path to the output APK. - std::string output_path; + Maybe output_path; + // Path to the output APK directory for splits. + Maybe output_dir; // Details of the app extracted from the AndroidManifest.xml AppInfo app_info; @@ -55,6 +65,9 @@ struct OptimizeOptions { std::vector split_constraints; TableFlattenerOptions table_flattener_options; + + // TODO: Come up with a better name for the Configuration struct. + Maybe configuration; }; class OptimizeContext : public IAaptContext { @@ -175,10 +188,52 @@ class OptimizeCommand { ++split_constraints_iter; } - std::unique_ptr writer = - CreateZipFileArchiveWriter(context_->GetDiagnostics(), options_.output_path); - if (!apk->WriteToArchive(context_, options_.table_flattener_options, writer.get())) { - return 1; + if (options_.configuration && options_.output_dir) { + Configuration& config = options_.configuration.value(); + + // For now, just write out the stripped APK since ABI splitting doesn't modify anything else. + for (const Artifact& artifact : config.artifacts) { + if (artifact.abi_group) { + const std::string& group = artifact.abi_group.value(); + + auto abi_group = config.abi_groups.find(group); + // TODO: Remove validation when configuration parser ensures referential integrity. + if (abi_group == config.abi_groups.end()) { + context_->GetDiagnostics()->Note( + DiagMessage() << "could not find referenced ABI group '" << group << "'"); + return 1; + } + FilterChain filters; + filters.AddFilter(AbiFilter::FromAbiList(abi_group->second)); + + const std::string& path = apk->GetSource().path; + const StringPiece ext = file::GetExtension(path); + const std::string name = path.substr(0, path.rfind(ext.to_string())); + + // Name is hard coded for now since only one split dimension is supported. + // TODO: Incorporate name generation into the configuration objects. + const std::string file_name = + StringPrintf("%s.%s%s", name.c_str(), group.c_str(), ext.data()); + std::string out = options_.output_dir.value(); + file::AppendPath(&out, file_name); + + std::unique_ptr writer = + CreateZipFileArchiveWriter(context_->GetDiagnostics(), out); + + if (!apk->WriteToArchive(context_, options_.table_flattener_options, &filters, + writer.get())) { + return 1; + } + } + } + } + + if (options_.output_path) { + std::unique_ptr writer = + CreateZipFileArchiveWriter(context_->GetDiagnostics(), options_.output_path.value()); + if (!apk->WriteToArchive(context_, options_.table_flattener_options, writer.get())) { + return 1; + } } return 0; @@ -214,8 +269,8 @@ class OptimizeCommand { if (file_ref->file == nullptr) { ResourceNameRef name(pkg->name, type->type, entry->name); context_->GetDiagnostics()->Warn(DiagMessage(file_ref->GetSource()) - << "file for resource " << name << " with config '" - << config_value->config << "' not found"); + << "file for resource " << name << " with config '" + << config_value->config << "' not found"); continue; } @@ -293,13 +348,16 @@ bool ExtractAppDataFromManifest(OptimizeContext* context, LoadedApk* apk, int Optimize(const std::vector& args) { OptimizeContext context; OptimizeOptions options; + Maybe config_path; Maybe target_densities; std::vector configs; std::vector split_args; bool verbose = false; Flags flags = Flags() - .RequiredFlag("-o", "Path to the output APK.", &options.output_path) + .OptionalFlag("-o", "Path to the output APK.", &options.output_path) + .OptionalFlag("-d", "Path to the output directory (for splits).", &options.output_dir) + .OptionalFlag("-x", "Path to XML configuration file.", &config_path) .OptionalFlag( "--target-densities", "Comma separated list of the screen densities that the APK will be optimized for.\n" @@ -369,6 +427,22 @@ int Optimize(const std::vector& args) { } } + if (config_path) { + if (!options.output_dir) { + context.GetDiagnostics()->Error( + DiagMessage() << "Output directory is required when using a configuration file"); + return 1; + } + std::string& path = config_path.value(); + Maybe for_path = ConfigurationParser::ForPath(path); + if (for_path) { + options.configuration = for_path.value().WithDiagnostics(context.GetDiagnostics()).Parse(); + } else { + context.GetDiagnostics()->Error(DiagMessage() << "Could not parse config file " << path); + return 1; + } + } + if (!ExtractAppDataFromManifest(&context, apk.get(), &options)) { return 1; } diff --git a/tools/aapt2/configuration/ConfigurationParser.cpp b/tools/aapt2/configuration/ConfigurationParser.cpp index 303a809fbaa9e..555cb35c0bb9a 100644 --- a/tools/aapt2/configuration/ConfigurationParser.cpp +++ b/tools/aapt2/configuration/ConfigurationParser.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -56,15 +57,15 @@ using ::aapt::xml::XmlActionExecutorPolicy; using ::aapt::xml::XmlNodeAction; using ::android::base::ReadFileToString; -const std::unordered_map kAbiMap = { - {"armeabi", Abi::kArmeV6}, - {"armeabi-v7a", Abi::kArmV7a}, - {"arm64-v8a", Abi::kArm64V8a}, - {"x86", Abi::kX86}, - {"x86_64", Abi::kX86_64}, - {"mips", Abi::kMips}, - {"mips64", Abi::kMips64}, - {"universal", Abi::kUniversal}, +const std::unordered_map kStringToAbiMap = { + {"armeabi", Abi::kArmeV6}, {"armeabi-v7a", Abi::kArmV7a}, {"arm64-v8a", Abi::kArm64V8a}, + {"x86", Abi::kX86}, {"x86_64", Abi::kX86_64}, {"mips", Abi::kMips}, + {"mips64", Abi::kMips64}, {"universal", Abi::kUniversal}, +}; +const std::map kAbiToStringMap = { + {Abi::kArmeV6, "armeabi"}, {Abi::kArmV7a, "armeabi-v7a"}, {Abi::kArm64V8a, "arm64-v8a"}, + {Abi::kX86, "x86"}, {Abi::kX86_64, "x86_64"}, {Abi::kMips, "mips"}, + {Abi::kMips64, "mips64"}, {Abi::kUniversal, "universal"}, }; constexpr const char* kAaptXmlNs = "http://schemas.android.com/tools/aapt"; @@ -102,7 +103,13 @@ class NamespaceVisitor : public xml::Visitor { } // namespace +namespace configuration { +const std::string& AbiToString(Abi abi) { + return kAbiToStringMap.find(abi)->second; +} + +} // namespace configuration /** Returns a ConfigurationParser for the file located at the provided path. */ Maybe ConfigurationParser::ForPath(const std::string& path) { @@ -175,6 +182,9 @@ Maybe ConfigurationParser::Parse() { return {}; } + // TODO: Validate all references in the configuration are valid. It should be safe to assume from + // this point on that any references from one section to another will be present. + return {config}; } @@ -201,7 +211,7 @@ ConfigurationParser::ActionHandler ConfigurationParser::artifact_handler_ = DiagMessage() << "Unknown artifact attribute: " << attr.name << " = " << attr.value); } } - config->artifacts[artifact.name] = artifact; + config->artifacts.push_back(artifact); return true; }; @@ -236,7 +246,7 @@ ConfigurationParser::ActionHandler ConfigurationParser::abi_group_handler_ = for (auto& node : child->children) { xml::Text* t; if ((t = NodeCast(node.get())) != nullptr) { - group.push_back(kAbiMap.at(TrimWhitespace(t->text).to_string())); + group.push_back(kStringToAbiMap.at(TrimWhitespace(t->text).to_string())); break; } } diff --git a/tools/aapt2/configuration/ConfigurationParser.h b/tools/aapt2/configuration/ConfigurationParser.h index 8b9c0853773bf..0435cbf72a229 100644 --- a/tools/aapt2/configuration/ConfigurationParser.h +++ b/tools/aapt2/configuration/ConfigurationParser.h @@ -62,6 +62,9 @@ enum class Abi { kUniversal }; +/** Helper method to convert an ABI to a string representing the path within the APK. */ +const std::string& AbiToString(Abi abi); + /** * Represents an individual locale. When a locale is included, it must be * declared from least specific to most specific, as a region does not make @@ -118,7 +121,8 @@ struct GlTexture { * AAPT2 XML configuration binary representation. */ struct Configuration { - std::unordered_map artifacts; + // TODO: Support named artifacts? + std::vector artifacts; Maybe artifact_format; Group abi_groups; diff --git a/tools/aapt2/configuration/ConfigurationParser_test.cpp b/tools/aapt2/configuration/ConfigurationParser_test.cpp index 8421ee3fcff0d..a8c385823fa90 100644 --- a/tools/aapt2/configuration/ConfigurationParser_test.cpp +++ b/tools/aapt2/configuration/ConfigurationParser_test.cpp @@ -196,7 +196,7 @@ TEST_F(ConfigurationParserTest, ArtifactAction) { EXPECT_EQ(1ul, config.artifacts.size()); - auto& artifact = config.artifacts.begin()->second; + auto& artifact = config.artifacts.front(); EXPECT_EQ("", artifact.name); // TODO: make this fail. EXPECT_EQ("arm", artifact.abi_group.value()); EXPECT_EQ("large", artifact.screen_density_group.value()); @@ -204,6 +204,21 @@ TEST_F(ConfigurationParserTest, ArtifactAction) { EXPECT_EQ("19", artifact.android_sdk_group.value()); EXPECT_EQ("dxt1", artifact.gl_texture_group.value()); EXPECT_EQ("low-latency", artifact.device_feature_group.value()); + + // Perform a second action to ensure we get 2 artifacts. + static constexpr const char* second = R"xml( + )xml"; + doc = test::BuildXmlDom(second); + + ok = artifact_handler_(&config, NodeCast(doc.get()->root.get()), &diag_); + ASSERT_TRUE(ok); + EXPECT_EQ(2ul, config.artifacts.size()); } TEST_F(ConfigurationParserTest, ArtifactFormatAction) { diff --git a/tools/aapt2/filter/AbiFilter.cpp b/tools/aapt2/filter/AbiFilter.cpp new file mode 100644 index 0000000000000..cb96235f98f9b --- /dev/null +++ b/tools/aapt2/filter/AbiFilter.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2017 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 "AbiFilter.h" + +#include + +#include "io/Util.h" + +namespace aapt { + +std::unique_ptr AbiFilter::FromAbiList(const std::vector& abi_list) { + std::unordered_set abi_set; + for (auto& abi : abi_list) { + abi_set.insert(configuration::AbiToString(abi)); + } + // Make unique by hand as the constructor is private. + return std::unique_ptr(new AbiFilter(abi_set)); +} + +bool AbiFilter::Keep(const std::string& path) { + // We only care about libraries. + if (!util::StartsWith(path, kLibPrefix)) { + return true; + } + + auto abi_end = path.find('/', kLibPrefixLen); + if (abi_end == std::string::npos) { + // Ignore any files in the top level lib directory. + return true; + } + + // Strip the lib/ prefix. + const std::string& path_abi = path.substr(kLibPrefixLen, abi_end - kLibPrefixLen); + return (abis_.find(path_abi) != abis_.end()); +} + +} // namespace aapt diff --git a/tools/aapt2/filter/AbiFilter.h b/tools/aapt2/filter/AbiFilter.h new file mode 100644 index 0000000000000..d875cb2b127b8 --- /dev/null +++ b/tools/aapt2/filter/AbiFilter.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2017 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. + */ + +#ifndef AAPT2_ABISPLITTER_H +#define AAPT2_ABISPLITTER_H + +#include +#include +#include +#include + +#include "configuration/ConfigurationParser.h" +#include "filter/Filter.h" + +namespace aapt { + +/** + * Filters native library paths by ABI. ABIs present in the filter list are kept and all over + * libraries are removed. The filter is only applied to native library paths (this under lib/). + */ +class AbiFilter : public IPathFilter { + public: + /** Factory method to create a filter from a list of configuration::Abi. */ + static std::unique_ptr FromAbiList(const std::vector& abi_list); + + /** Returns true if the path is for a native library in the list of desired ABIs. */ + bool Keep(const std::string& path) override; + + private: + explicit AbiFilter(std::unordered_set abis) : abis_(std::move(abis)) { + } + + /** The path prefix to where all native libs end up inside an APK file. */ + static constexpr const char* kLibPrefix = "lib/"; + static constexpr size_t kLibPrefixLen = 4; + const std::unordered_set abis_; +}; + +} // namespace aapt + +#endif // AAPT2_ABISPLITTER_H diff --git a/tools/aapt2/filter/AbiFilter_test.cpp b/tools/aapt2/filter/AbiFilter_test.cpp new file mode 100644 index 0000000000000..0c8ea3575a295 --- /dev/null +++ b/tools/aapt2/filter/AbiFilter_test.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2017 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 "filter/AbiFilter.h" + +#include + +#include "gtest/gtest.h" + +namespace aapt { +namespace { + +using ::aapt::configuration::Abi; + +struct TestData { + std::string path; + bool kept; +}; + +const TestData kTestData[] = { + /* Keep. */ + {"lib/mips/libnative.so", true}, + {"not/native/file.txt", true}, + // Not sure if this is a valid use case. + {"lib/listing.txt", true}, + {"lib/mips/foo/bar/baz.so", true}, + {"lib/mips/x86/foo.so", true}, + /* Discard. */ + {"lib/mips_horse/foo.so", false}, + {"lib/horse_mips/foo.so", false}, + {"lib/mips64/armeabi-v7a/foo.so", false}, + {"lib/mips64/x86_64/x86.so", false}, + {"lib/x86/libnative.so", false}, + {"lib/x86/foo/bar/baz.so", false}, + {"lib/x86/x86/foo.so", false}, + {"lib/x86_horse/foo.so", false}, + {"lib/horse_x86/foo.so", false}, + {"lib/x86/armeabi-v7a/foo.so", false}, + {"lib/x86_64/x86_64/x86.so", false}, +}; + +class AbiFilterTest : public ::testing::TestWithParam {}; + +TEST_P(AbiFilterTest, Keep) { + auto mips = AbiFilter::FromAbiList({Abi::kMips}); + const TestData& data = GetParam(); + EXPECT_EQ(mips->Keep(data.path), data.kept); +} + +INSTANTIATE_TEST_CASE_P(NativePaths, AbiFilterTest, ::testing::ValuesIn(kTestData)); + +} // namespace +} // namespace aapt diff --git a/tools/aapt2/filter/Filter.h b/tools/aapt2/filter/Filter.h new file mode 100644 index 0000000000000..d737dc92e87b4 --- /dev/null +++ b/tools/aapt2/filter/Filter.h @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2017 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. + */ + +#ifndef AAPT2_FILTER_H +#define AAPT2_FILTER_H + +#include +#include + +#include "util/Util.h" + +namespace aapt { + +/** A filter to be applied to a path segment. */ +class IPathFilter { + public: + ~IPathFilter() = default; + + /** Returns true if the path should be kept. */ + virtual bool Keep(const std::string& path) = 0; +}; + +/** + * Path filter that keeps anything that matches the provided prefix. + */ +class PrefixFilter : public IPathFilter { + public: + explicit PrefixFilter(std::string prefix) : prefix_(std::move(prefix)) { + } + + /** Returns true if the provided path matches the prefix. */ + bool Keep(const std::string& path) override { + return util::StartsWith(path, prefix_); + } + + private: + const std::string prefix_; +}; + +/** Applies a set of IPathFilters to a path and returns true iif all filters keep the path. */ +class FilterChain : public IPathFilter { + public: + /** Adds a filter to the list to be applied to each path. */ + void AddFilter(std::unique_ptr filter) { + filters_.push_back(std::move(filter)); + } + + /** Returns true if all filters keep the path. */ + bool Keep(const std::string& path) override { + for (auto& filter : filters_) { + if (!filter->Keep(path)) { + return false; + } + } + return true; + } + + private: + std::vector> filters_; +}; + +} // namespace aapt + +#endif // AAPT2_FILTER_H diff --git a/tools/aapt2/filter/Filter_test.cpp b/tools/aapt2/filter/Filter_test.cpp new file mode 100644 index 0000000000000..fb75a4b4d7c19 --- /dev/null +++ b/tools/aapt2/filter/Filter_test.cpp @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2017 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 "filter/Filter.h" + +#include + +#include "io/Util.h" + +#include "gtest/gtest.h" + +namespace aapt { +namespace { + +TEST(FilterChainTest, EmptyChain) { + FilterChain chain; + ASSERT_TRUE(chain.Keep("some/random/path")); +} + +TEST(FilterChainTest, SingleFilter) { + FilterChain chain; + chain.AddFilter(util::make_unique("keep/")); + + ASSERT_FALSE(chain.Keep("removed/path")); + ASSERT_TRUE(chain.Keep("keep/path/1")); + ASSERT_TRUE(chain.Keep("keep/path/2")); +} + +TEST(FilterChainTest, MultipleFilters) { + FilterChain chain; + chain.AddFilter(util::make_unique("keep/")); + chain.AddFilter(util::make_unique("keep/really/")); + + ASSERT_FALSE(chain.Keep("removed/path")); + ASSERT_FALSE(chain.Keep("/keep/really/wrong/prefix")); + ASSERT_FALSE(chain.Keep("keep/maybe/1")); + ASSERT_TRUE(chain.Keep("keep/really/1")); +} + +} // namespace +} // namespace aapt