AAPT2: Compile --zip flag

Added a --zip flag similar to --dir that allows resources to be passed
into "aapt2 compile" using a zip file.

Also refactored Compile.cpp to be easier to mock and test in the future.

Bug: 74574557
Test: aapt2_tests
Change-Id: Idb90cb97e23a219525bdead38220cbf7bc6f3cab
diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp
index 2ba2cf7..62c19fb 100644
--- a/tools/aapt2/cmd/Compile.cpp
+++ b/tools/aapt2/cmd/Compile.cpp
@@ -41,8 +41,10 @@
 #include "format/proto/ProtoSerialize.h"
 #include "io/BigBufferStream.h"
 #include "io/FileStream.h"
+#include "io/FileSystem.h"
 #include "io/StringStream.h"
 #include "io/Util.h"
+#include "io/ZipArchive.h"
 #include "util/Files.h"
 #include "util/Maybe.h"
 #include "util/Util.h"
@@ -135,81 +137,20 @@
   return name.str();
 }
 
-static bool IsHidden(const StringPiece& filename) {
-  return util::StartsWith(filename, ".");
-}
-
-// Walks the res directory structure, looking for resource files.
-static bool LoadInputFilesFromDir(IAaptContext* context, const CompileOptions& options,
-                                  std::vector<ResourcePathData>* out_path_data) {
-  const std::string& root_dir = options.res_dir.value();
-  std::unique_ptr<DIR, decltype(closedir)*> d(opendir(root_dir.data()), closedir);
-  if (!d) {
-    context->GetDiagnostics()->Error(DiagMessage(root_dir) << "failed to open directory: "
-                                                           << SystemErrorCodeToString(errno));
-    return false;
-  }
-
-  while (struct dirent* entry = readdir(d.get())) {
-    if (IsHidden(entry->d_name)) {
-      continue;
-    }
-
-    std::string prefix_path = root_dir;
-    file::AppendPath(&prefix_path, entry->d_name);
-
-    if (file::GetFileType(prefix_path) != file::FileType::kDirectory) {
-      continue;
-    }
-
-    std::unique_ptr<DIR, decltype(closedir)*> subdir(opendir(prefix_path.data()), closedir);
-    if (!subdir) {
-      context->GetDiagnostics()->Error(DiagMessage(prefix_path) << "failed to open directory: "
-                                                                << SystemErrorCodeToString(errno));
-      return false;
-    }
-
-    while (struct dirent* leaf_entry = readdir(subdir.get())) {
-      if (IsHidden(leaf_entry->d_name)) {
-        continue;
-      }
-
-      std::string full_path = prefix_path;
-      file::AppendPath(&full_path, leaf_entry->d_name);
-
-      std::string err_str;
-      Maybe<ResourcePathData> path_data = ExtractResourcePathData(full_path, &err_str);
-      if (!path_data) {
-        context->GetDiagnostics()->Error(DiagMessage(full_path) << err_str);
-        return false;
-      }
-
-      out_path_data->push_back(std::move(path_data.value()));
-    }
-  }
-
-  // File-system directory enumeration order is platform-dependent. Sort the result to remove any
-  // inconsistencies between platforms.
-  std::sort(
-      out_path_data->begin(), out_path_data->end(),
-      [](const ResourcePathData& a, const ResourcePathData& b) { return a.source < b.source; });
-  return true;
-}
-
 static bool CompileTable(IAaptContext* context, const CompileOptions& options,
-                         const ResourcePathData& path_data, IArchiveWriter* writer,
+                         const ResourcePathData& path_data, io::IFile* file, IArchiveWriter* writer,
                          const std::string& output_path) {
   ResourceTable table;
   {
-    FileInputStream fin(path_data.source.path);
-    if (fin.HadError()) {
+    auto fin = file->OpenInputStream();
+    if (fin->HadError()) {
       context->GetDiagnostics()->Error(DiagMessage(path_data.source)
-                                       << "failed to open file: " << fin.GetError());
+          << "failed to open file: " << fin->GetError());
       return false;
     }
 
     // Parse the values file from XML.
-    xml::XmlPullParser xml_parser(&fin);
+    xml::XmlPullParser xml_parser(fin.get());
 
     ResourceParserOptions parser_options;
     parser_options.error_on_positional_arguments = !options.legacy_mode;
@@ -222,7 +163,7 @@
     parser_options.visibility = options.visibility;
 
     ResourceParser res_parser(context->GetDiagnostics(), &table, path_data.source, path_data.config,
-                              parser_options);
+        parser_options);
     if (!res_parser.Parse(&xml_parser)) {
       return false;
     }
@@ -408,7 +349,7 @@
 }
 
 static bool CompileXml(IAaptContext* context, const CompileOptions& options,
-                       const ResourcePathData& path_data, IArchiveWriter* writer,
+                       const ResourcePathData& path_data, io::IFile* file, IArchiveWriter* writer,
                        const std::string& output_path) {
   if (context->IsVerbose()) {
     context->GetDiagnostics()->Note(DiagMessage(path_data.source) << "compiling XML");
@@ -416,18 +357,17 @@
 
   std::unique_ptr<xml::XmlResource> xmlres;
   {
-    FileInputStream fin(path_data.source.path);
-    if (fin.HadError()) {
+    auto fin = file->OpenInputStream();
+    if (fin->HadError()) {
       context->GetDiagnostics()->Error(DiagMessage(path_data.source)
-                                       << "failed to open file: " << fin.GetError());
+                                       << "failed to open file: " << fin->GetError());
       return false;
     }
 
-    xmlres = xml::Inflate(&fin, context->GetDiagnostics(), path_data.source);
-  }
-
-  if (!xmlres) {
-    return false;
+    xmlres = xml::Inflate(fin.get(), context->GetDiagnostics(), path_data.source);
+    if (!xmlres) {
+      return false;
+    }
   }
 
   xmlres->file.name = ResourceName({}, *ParseResourceType(path_data.resource_dir), path_data.name);
@@ -508,7 +448,7 @@
 }
 
 static bool CompilePng(IAaptContext* context, const CompileOptions& options,
-                       const ResourcePathData& path_data, IArchiveWriter* writer,
+                       const ResourcePathData& path_data, io::IFile* file, IArchiveWriter* writer,
                        const std::string& output_path) {
   if (context->IsVerbose()) {
     context->GetDiagnostics()->Note(DiagMessage(path_data.source) << "compiling PNG");
@@ -522,15 +462,17 @@
   res_file.type = ResourceFile::Type::kPng;
 
   {
-    std::string content;
-    if (!android::base::ReadFileToString(path_data.source.path, &content,
-                                         true /*follow_symlinks*/)) {
-      context->GetDiagnostics()->Error(DiagMessage(path_data.source)
-                                       << "failed to open file: "
-                                       << SystemErrorCodeToString(errno));
+    auto data = file->OpenAsData();
+    if (!data) {
+      context->GetDiagnostics()->Error(DiagMessage(path_data.source) << "failed to open file ");
       return false;
     }
 
+    // Read the file as a string
+    char buffer_2[data->size()];
+    memcpy(&buffer_2, data->data(), data->size());
+    StringPiece content(buffer_2, data->size());
+
     BigBuffer crunched_png_buffer(4096);
     io::BigBufferOutputStream crunched_png_buffer_out(&crunched_png_buffer);
 
@@ -598,7 +540,7 @@
     if (context->IsVerbose()) {
       // For debugging only, use the legacy PNG cruncher and compare the resulting file sizes.
       // This will help catch exotic cases where the new code may generate larger PNGs.
-      std::stringstream legacy_stream(content);
+      std::stringstream legacy_stream(content.to_string());
       BigBuffer legacy_buffer(4096);
       Png png(context->GetDiagnostics());
       if (!png.process(path_data.source, &legacy_stream, &legacy_buffer, {})) {
@@ -612,41 +554,31 @@
   }
 
   io::BigBufferInputStream buffer_in(&buffer);
-  if (!WriteHeaderAndDataToWriter(output_path, res_file, &buffer_in, writer,
-                                  context->GetDiagnostics())) {
-    return false;
-  }
-  return true;
+  return WriteHeaderAndDataToWriter(output_path, res_file, &buffer_in, writer,
+      context->GetDiagnostics());
 }
 
 static bool CompileFile(IAaptContext* context, const CompileOptions& options,
-                        const ResourcePathData& path_data, IArchiveWriter* writer,
+                        const ResourcePathData& path_data, io::IFile* file, IArchiveWriter* writer,
                         const std::string& output_path) {
   if (context->IsVerbose()) {
     context->GetDiagnostics()->Note(DiagMessage(path_data.source) << "compiling file");
   }
 
-  BigBuffer buffer(256);
   ResourceFile res_file;
   res_file.name = ResourceName({}, *ParseResourceType(path_data.resource_dir), path_data.name);
   res_file.config = path_data.config;
   res_file.source = path_data.source;
   res_file.type = ResourceFile::Type::kUnknown;
 
-  std::string error_str;
-  Maybe<android::FileMap> f = file::MmapPath(path_data.source.path, &error_str);
-  if (!f) {
-    context->GetDiagnostics()->Error(DiagMessage(path_data.source) << "failed to mmap file: "
-                                     << error_str);
+  auto data = file->OpenAsData();
+  if (!data) {
+    context->GetDiagnostics()->Error(DiagMessage(path_data.source) << "failed to open file ");
     return false;
   }
 
-  io::MmappedData mmapped_in(std::move(f.value()));
-  if (!WriteHeaderAndDataToWriter(output_path, res_file, &mmapped_in, writer,
-                                  context->GetDiagnostics())) {
-    return false;
-  }
-  return true;
+  return WriteHeaderAndDataToWriter(output_path, res_file, data.get(), writer,
+      context->GetDiagnostics());
 }
 
 class CompileContext : public IAaptContext {
@@ -701,6 +633,79 @@
   bool verbose_ = false;
 };
 
+int Compile(IAaptContext* context, io::IFileCollection* inputs, IArchiveWriter* output_writer,
+             CompileOptions& options) {
+  bool error = false;
+
+  // Iterate over the input files in a stable, platform-independent manner
+  auto file_iterator  = inputs->Iterator();
+  while (file_iterator->HasNext()) {
+    auto file = file_iterator->Next();
+    std::string path = file->GetSource().path;
+
+    // Skip hidden input files
+    if (file::IsHidden(path)) {
+      continue;
+    }
+
+    if (!options.res_zip && !IsValidFile(context, path)) {
+      error = true;
+      continue;
+    }
+
+    // Extract resource type information from the full path
+    std::string err_str;
+    ResourcePathData path_data;
+    if (auto maybe_path_data = ExtractResourcePathData(path, &err_str)) {
+      path_data = maybe_path_data.value();
+    } else {
+      context->GetDiagnostics()->Error(DiagMessage(file->GetSource()) << err_str);
+      error = true;
+      continue;
+    }
+
+    // Determine how to compile the file based on its type.
+    auto compile_func = &CompileFile;
+    if (path_data.resource_dir == "values" && path_data.extension == "xml") {
+      compile_func = &CompileTable;
+      // We use a different extension (not necessary anymore, but avoids altering the existing
+      // build system logic).
+      path_data.extension = "arsc";
+
+    } else if (const ResourceType* type = ParseResourceType(path_data.resource_dir)) {
+      if (*type != ResourceType::kRaw) {
+        if (path_data.extension == "xml") {
+          compile_func = &CompileXml;
+        } else if ((!options.no_png_crunch && path_data.extension == "png")
+                   || path_data.extension == "9.png") {
+          compile_func = &CompilePng;
+        }
+      }
+    } else {
+      context->GetDiagnostics()->Error(DiagMessage()
+          << "invalid file path '" << path_data.source << "'");
+      error = true;
+      continue;
+    }
+
+    // Treat periods as a reserved character that should not be present in a file name
+    // Legacy support for AAPT which did not reserve periods
+    if (compile_func != &CompileFile && !options.legacy_mode
+        && std::count(path_data.name.begin(), path_data.name.end(), '.') != 0) {
+      error = true;
+      context->GetDiagnostics()->Error(DiagMessage(file->GetSource())
+                                                    << "file name cannot contain '.' other than for"
+                                                    << " specifying the extension");
+      continue;
+    }
+
+    const std::string out_path = BuildIntermediateContainerFilename(path_data);
+    error |= !compile_func(context, options, path_data, file, output_writer, out_path);
+  }
+
+  return error ? 1 : 0;
+}
+
 int CompileCommand::Action(const std::vector<std::string>& args) {
   CompileContext context(diagnostic_);
   context.SetVerbose(options_.verbose);
@@ -720,37 +725,55 @@
     }
   }
 
+  std::unique_ptr<io::IFileCollection> file_collection;
   std::unique_ptr<IArchiveWriter> archive_writer;
 
-  std::vector<ResourcePathData> input_data;
-  if (options_.res_dir) {
+  // Collect the resources files to compile
+  if (options_.res_dir && options_.res_zip) {
+    context.GetDiagnostics()->Error(DiagMessage()
+                                        << "only one of --dir and --zip can be specified");
+    return 1;
+  } else if (options_.res_dir) {
     if (!args.empty()) {
-      // Can't have both files and a resource directory.
       context.GetDiagnostics()->Error(DiagMessage() << "files given but --dir specified");
       Usage(&std::cerr);
       return 1;
     }
 
-    if (!LoadInputFilesFromDir(&context, options_, &input_data)) {
+    // Load the files from the res directory
+    std::string err;
+    file_collection = io::FileCollection::Create(options_.res_dir.value(), &err);
+    if (!file_collection) {
+      context.GetDiagnostics()->Error(DiagMessage(options_.res_dir.value()) << err);
       return 1;
     }
 
     archive_writer = CreateZipFileArchiveWriter(context.GetDiagnostics(), options_.output_path);
+  } else if (options_.res_zip) {
+    if (!args.empty()) {
+      context.GetDiagnostics()->Error(DiagMessage() << "files given but --zip specified");
+      Usage(&std::cerr);
+      return 1;
+    }
 
+    // Load a zip file containing a res directory
+    std::string err;
+    file_collection = io::ZipFileCollection::Create(options_.res_zip.value(), &err);
+    if (!file_collection) {
+      context.GetDiagnostics()->Error(DiagMessage(options_.res_zip.value()) << err);
+      return 1;
+    }
+
+    archive_writer = CreateZipFileArchiveWriter(context.GetDiagnostics(), options_.output_path);
   } else {
-    input_data.reserve(args.size());
+    auto collection = util::make_unique<io::FileCollection>();
 
     // Collect data from the path for each input file.
     for (const std::string& arg : args) {
-      std::string error_str;
-      if (Maybe<ResourcePathData> path_data = ExtractResourcePathData(arg, &error_str)) {
-        input_data.push_back(std::move(path_data.value()));
-      } else {
-        context.GetDiagnostics()->Error(DiagMessage() << error_str << " (" << arg << ")");
-        return 1;
-      }
+      collection->InsertFile(arg);
     }
 
+    file_collection = std::move(collection);
     archive_writer = CreateDirectoryArchiveWriter(context.GetDiagnostics(), options_.output_path);
   }
 
@@ -758,57 +781,7 @@
     return 1;
   }
 
-  bool error = false;
-  for (ResourcePathData& path_data : input_data) {
-    if (options_.verbose) {
-      context.GetDiagnostics()->Note(DiagMessage(path_data.source) << "processing");
-    }
-
-    if (!IsValidFile(&context, path_data.source.path)) {
-      error = true;
-      continue;
-    }
-
-    // Determine how to compile the file based on its type.
-    auto compile_func = &CompileFile;
-    if (path_data.resource_dir == "values" && path_data.extension == "xml") {
-      compile_func = &CompileTable;
-      // We use a different extension (not necessary anymore, but avoids altering the existing
-      // build system logic).
-      path_data.extension = "arsc";
-
-    } else if (const ResourceType* type = ParseResourceType(path_data.resource_dir)) {
-      if (*type != ResourceType::kRaw) {
-        if (path_data.extension == "xml") {
-          compile_func = &CompileXml;
-        } else if ((!options_.no_png_crunch && path_data.extension == "png")
-            || path_data.extension == "9.png") {
-          compile_func = &CompilePng;
-        }
-      }
-    } else {
-      context.GetDiagnostics()->Error(DiagMessage()
-          << "invalid file path '" << path_data.source << "'");
-      error = true;
-      continue;
-    }
-
-    // Treat periods as a reserved character that should not be present in a file name
-    // Legacy support for AAPT which did not reserve periods
-    if (compile_func != &CompileFile && !options_.legacy_mode
-        && std::count(path_data.name.begin(), path_data.name.end(), '.') != 0) {
-      error = true;
-      context.GetDiagnostics()->Error(DiagMessage() << "resource file '" << path_data.source.path
-                                                    << "' name cannot contain '.' other than for"
-                                                    << "specifying the extension");
-      continue;
-    }
-
-    // Compile the file.
-    const std::string out_path = BuildIntermediateContainerFilename(path_data);
-    error |= !compile_func(&context, options_, path_data, archive_writer.get(), out_path);
-  }
-  return error ? 1 : 0;
+  return Compile(&context, file_collection.get(), archive_writer.get(), options_);
 }
 
 }  // namespace aapt
diff --git a/tools/aapt2/cmd/Compile.h b/tools/aapt2/cmd/Compile.h
index 4151952..c429d5f 100644
--- a/tools/aapt2/cmd/Compile.h
+++ b/tools/aapt2/cmd/Compile.h
@@ -18,7 +18,8 @@
 #define AAPT2_COMPILE_H
 
 #include "androidfw/StringPiece.h"
-
+#include "format/Archive.h"
+#include "process/IResourceTableConsumer.h"
 #include "Command.h"
 #include "Diagnostics.h"
 #include "ResourceTable.h"
@@ -28,6 +29,7 @@
 struct CompileOptions {
   std::string output_path;
   Maybe<std::string> res_dir;
+  Maybe<std::string> res_zip;
   Maybe<std::string> generate_text_symbols_path;
   Maybe<Visibility::Level> visibility;
   bool pseudolocalize = false;
@@ -36,6 +38,7 @@
   bool verbose = false;
 };
 
+/** Parses flags and compiles resources to be used in linking.  */
 class CompileCommand : public Command {
  public:
   explicit CompileCommand(IDiagnostics* diagnostic) : Command("compile", "c"),
@@ -43,6 +46,8 @@
     SetDescription("Compiles resources to be linked into an apk.");
     AddRequiredFlag("-o", "Output path", &options_.output_path);
     AddOptionalFlag("--dir", "Directory to scan for resources", &options_.res_dir);
+    AddOptionalFlag("--zip", "Zip file containing the res directory to scan for resources",
+        &options_.res_zip);
     AddOptionalFlag("--output-text-symbols",
         "Generates a text file containing the resource symbols in the\n"
             "specified file", &options_.generate_text_symbols_path);
@@ -51,10 +56,10 @@
     AddOptionalSwitch("--no-crunch", "Disables PNG processing", &options_.no_png_crunch);
     AddOptionalSwitch("--legacy", "Treat errors that used to be valid in AAPT as warnings",
         &options_.legacy_mode);
-    AddOptionalSwitch("-v", "Enables verbose logging", &options_.verbose);
     AddOptionalFlag("--visibility",
         "Sets the visibility of the compiled resources to the specified\n"
             "level. Accepted levels: public, private, default", &visibility_);
+    AddOptionalSwitch("-v", "Enables verbose logging", &options_.verbose);
   }
 
   int Action(const std::vector<std::string>& args) override;
@@ -65,6 +70,8 @@
   Maybe<std::string> visibility_;
 };
 
+int Compile(IAaptContext* context, io::IFileCollection* inputs,
+             IArchiveWriter* output_writer, CompileOptions& options);
 }// namespace aapt
 
 #endif //AAPT2_COMPILE_H
diff --git a/tools/aapt2/cmd/Compile_test.cpp b/tools/aapt2/cmd/Compile_test.cpp
index d21addf..dd5198c 100644
--- a/tools/aapt2/cmd/Compile_test.cpp
+++ b/tools/aapt2/cmd/Compile_test.cpp
@@ -18,6 +18,7 @@
 
 #include "android-base/file.h"
 #include "io/StringStream.h"
+#include "io/ZipArchive.h"
 #include "java/AnnotationProcessor.h"
 #include "test/Test.h"
 
@@ -29,7 +30,6 @@
   args.push_back(path);
   args.push_back("-o");
   args.push_back(outDir);
-  args.push_back("-v");
   if (legacy) {
     args.push_back("--legacy");
   }
@@ -94,4 +94,56 @@
   ASSERT_EQ(remove(path5_out.c_str()), 0);
 }
 
-}
\ No newline at end of file
+TEST(CompilerTest, DirInput) {
+  StdErrDiagnostics diag;
+  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
+  const std::string kResDir = android::base::Dirname(android::base::GetExecutablePath())
+                            + "/integration-tests/CompileTest/DirInput/res";
+  const std::string kOutputFlata = android::base::Dirname(android::base::GetExecutablePath())
+                                 + "/integration-tests/CompileTest/DirInput/compiled.flata";
+  remove(kOutputFlata.c_str());
+
+  std::vector<android::StringPiece> args;
+  args.push_back("--dir");
+  args.push_back(kResDir);
+  args.push_back("-o");
+  args.push_back(kOutputFlata);
+  ASSERT_EQ(CompileCommand(&diag).Execute(args, &std::cerr), 0);
+
+  // Check for the presence of the compiled files
+  std::string err;
+  std::unique_ptr<io::ZipFileCollection> zip = io::ZipFileCollection::Create(kOutputFlata, &err);
+  ASSERT_NE(zip, nullptr) << err;
+  ASSERT_NE(zip->FindFile("drawable_image.png.flat"), nullptr);
+  ASSERT_NE(zip->FindFile("layout_layout.xml.flat"), nullptr);
+  ASSERT_NE(zip->FindFile("values_values.arsc.flat"), nullptr);
+  ASSERT_EQ(remove(kOutputFlata.c_str()), 0);
+}
+
+TEST(CompilerTest, ZipInput) {
+  StdErrDiagnostics diag;
+  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
+  const std::string kResZip = android::base::Dirname(android::base::GetExecutablePath())
+                            + "/integration-tests/CompileTest/ZipInput/res.zip";
+  const std::string kOutputFlata = android::base::Dirname(android::base::GetExecutablePath())
+                                 + "/integration-tests/CompileTest/ZipInput/compiled.flata";
+  remove(kOutputFlata.c_str());
+
+  std::vector<android::StringPiece> args;
+  args.push_back("--zip");
+  args.push_back(kResZip);
+  args.push_back("-o");
+  args.push_back(kOutputFlata);
+  ASSERT_EQ(CompileCommand(&diag).Execute(args, &std::cerr), 0);
+
+  // Check for the presence of the compiled files
+  std::string err;
+  std::unique_ptr<io::ZipFileCollection> zip = io::ZipFileCollection::Create(kOutputFlata, &err);
+  ASSERT_NE(zip, nullptr) << err;
+  ASSERT_NE(zip->FindFile("drawable_image.png.flat"), nullptr);
+  ASSERT_NE(zip->FindFile("layout_layout.xml.flat"), nullptr);
+  ASSERT_NE(zip->FindFile("values_values.arsc.flat"), nullptr);
+  ASSERT_EQ(remove(kOutputFlata.c_str()), 0);
+}
+
+} // namespace aapt
\ No newline at end of file
diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp
index d57eaa1..86b1f4c 100644
--- a/tools/aapt2/cmd/Convert.cpp
+++ b/tools/aapt2/cmd/Convert.cpp
@@ -105,10 +105,7 @@
   std::unique_ptr<io::IFileCollectionIterator> iterator = apk->GetFileCollection()->Iterator();
   while (iterator->HasNext()) {
     io::IFile* file = iterator->Next();
-
     std::string path = file->GetSource().path;
-    // The name of the path has the format "<zip-file-name>@<path-to-file>".
-    path = path.substr(path.find('@') + 1);
 
     // Manifest, resource table and resources have already been taken care of.
     if (path == kAndroidManifestPath ||