/*
 * 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 "dexoptanalyzer.h"

#include <iostream>
#include <string>
#include <string_view>

#include "android-base/stringprintf.h"
#include "android-base/strings.h"
#include "base/compiler_filter.h"
#include "base/file_utils.h"
#include "base/logging.h"  // For InitLogging.
#include "base/mutex.h"
#include "base/os.h"
#include "base/string_view_cpp20.h"
#include "base/utils.h"
#include "class_linker.h"
#include "class_loader_context.h"
#include "dex/dex_file.h"
#include "gc/heap.h"
#include "gc/space/image_space.h"
#include "noop_compiler_callbacks.h"
#include "oat.h"
#include "oat_file_assistant.h"
#include "runtime.h"
#include "thread-inl.h"
#include "vdex_file.h"

namespace art {
namespace dexoptanalyzer {

static int original_argc;
static char** original_argv;

static std::string CommandLine() {
  std::vector<std::string> command;
  command.reserve(original_argc);
  for (int i = 0; i < original_argc; ++i) {
    command.push_back(original_argv[i]);
  }
  return android::base::Join(command, ' ');
}

static void UsageErrorV(const char* fmt, va_list ap) {
  std::string error;
  android::base::StringAppendV(&error, fmt, ap);
  LOG(ERROR) << error;
}

static void UsageError(const char* fmt, ...) {
  va_list ap;
  va_start(ap, fmt);
  UsageErrorV(fmt, ap);
  va_end(ap);
}

NO_RETURN static void Usage(const char *fmt, ...) {
  va_list ap;
  va_start(ap, fmt);
  UsageErrorV(fmt, ap);
  va_end(ap);

  UsageError("Command: %s", CommandLine().c_str());
  UsageError("  Performs a dexopt analysis on the given dex file and returns whether or not");
  UsageError("  the dex file needs to be dexopted.");
  UsageError("Usage: dexoptanalyzer [options]...");
  UsageError("");
  UsageError("  --dex-file=<filename>: the dex file which should be analyzed.");
  UsageError("");
  UsageError("  --isa=<string>: the instruction set for which the analysis should be performed.");
  UsageError("");
  UsageError("  --compiler-filter=<string>: the target compiler filter to be used as reference");
  UsageError("       when deciding if the dex file needs to be optimized.");
  UsageError("");
  UsageError("  --assume-profile-changed: assumes the profile information has changed");
  UsageError("       when deciding if the dex file needs to be optimized.");
  UsageError("");
  UsageError("  --image=<filename>: optional, the image to be used to decide if the associated");
  UsageError("       oat file is up to date. Defaults to $ANDROID_ROOT/framework/boot.art.");
  UsageError("       Example: --image=/system/framework/boot.art");
  UsageError("");
  UsageError("  --runtime-arg <argument>: used to specify various arguments for the runtime,");
  UsageError("      such as initial heap size, maximum heap size, and verbose output.");
  UsageError("      Use a separate --runtime-arg switch for each argument.");
  UsageError("      Example: --runtime-arg -Xms256m");
  UsageError("");
  UsageError("  --android-data=<directory>: optional, the directory which should be used as");
  UsageError("       android-data. By default ANDROID_DATA env variable is used.");
  UsageError("");
  UsageError("  --oat-fd=number: file descriptor of the oat file which should be analyzed");
  UsageError("");
  UsageError("  --vdex-fd=number: file descriptor of the vdex file corresponding to the oat file");
  UsageError("");
  UsageError("  --zip-fd=number: specifies a file descriptor corresponding to the dex file.");
  UsageError("");
  UsageError("  --downgrade: optional, if the purpose of dexopt is to downgrade the dex file");
  UsageError("       By default, dexopt considers upgrade case.");
  UsageError("");
  UsageError("  --class-loader-context=<string spec>: a string specifying the intended");
  UsageError("      runtime loading context for the compiled dex files.");
  UsageError("");
  UsageError("  --class-loader-context-fds=<fds>: a colon-separated list of file descriptors");
  UsageError("      for dex files in --class-loader-context. Their order must be the same as");
  UsageError("      dex files in flattened class loader context.");
  UsageError("");
  UsageError("  --flatten-class-loader-context: parse --class-loader-context, flatten it and");
  UsageError("      print a colon-separated list of its dex files to standard output. Dexopt");
  UsageError("      needed analysis is not performed when this option is set.");
  UsageError("");
  UsageError("  --validate-bcp: validates the boot class path files (.art, .oat, .vdex).");
  UsageError("      Requires --isa and --image options to locate artifacts.");
  UsageError("");
  UsageError("Return code:");
  UsageError("  To make it easier to integrate with the internal tools this command will make");
  UsageError("    available its result (dexoptNeeded) as the exit/return code. i.e. it will not");
  UsageError("    return 0 for success and a non zero values for errors as the conventional");
  UsageError("    commands. The following return codes are possible:");
  UsageError("        kNoDexOptNeeded = 0");
  UsageError("        kDex2OatFromScratch = 1");
  UsageError("        kDex2OatForBootImageOat = 2");
  UsageError("        kDex2OatForFilterOat = 3");
  UsageError("        kDex2OatForBootImageOdex = 4");
  UsageError("        kDex2OatForFilterOdex = 5");

  UsageError("        kErrorInvalidArguments = 101");
  UsageError("        kErrorCannotCreateRuntime = 102");
  UsageError("        kErrorUnknownDexOptNeeded = 103");
  UsageError("");

  exit(static_cast<int>(ReturnCode::kErrorInvalidArguments));
}

class DexoptAnalyzer final {
 public:
  DexoptAnalyzer() :
      only_flatten_context_(false),
      only_validate_bcp_(false),
      assume_profile_changed_(false),
      downgrade_(false) {}

  void ParseArgs(int argc, char **argv) {
    original_argc = argc;
    original_argv = argv;

    Locks::Init();
    InitLogging(argv, Runtime::Abort);
    // Skip over the command name.
    argv++;
    argc--;

    if (argc == 0) {
      Usage("No arguments specified");
    }

    for (int i = 0; i < argc; ++i) {
      const char* raw_option = argv[i];
      const std::string_view option(raw_option);
      if (option == "--assume-profile-changed") {
        assume_profile_changed_ = true;
      } else if (StartsWith(option, "--dex-file=")) {
        dex_file_ = std::string(option.substr(strlen("--dex-file=")));
      } else if (StartsWith(option, "--compiler-filter=")) {
        const char* filter_str = raw_option + strlen("--compiler-filter=");
        if (!CompilerFilter::ParseCompilerFilter(filter_str, &compiler_filter_)) {
          Usage("Invalid compiler filter '%s'", raw_option);
        }
      } else if (StartsWith(option, "--isa=")) {
        const char* isa_str = raw_option + strlen("--isa=");
        isa_ = GetInstructionSetFromString(isa_str);
        if (isa_ == InstructionSet::kNone) {
          Usage("Invalid isa '%s'", raw_option);
        }
      } else if (StartsWith(option, "--image=")) {
        image_ = std::string(option.substr(strlen("--image=")));
      } else if (option == "--runtime-arg") {
        if (i + 1 == argc) {
          Usage("Missing argument for --runtime-arg\n");
        }
        ++i;
        runtime_args_.push_back(argv[i]);
      } else if (StartsWith(option, "--android-data=")) {
        // Overwrite android-data if needed (oat file assistant relies on a valid directory to
        // compute dalvik-cache folder). This is mostly used in tests.
        const char* new_android_data = raw_option + strlen("--android-data=");
        setenv("ANDROID_DATA", new_android_data, 1);
      } else if (option == "--downgrade") {
        downgrade_ = true;
      } else if (StartsWith(option, "--oat-fd=")) {
        oat_fd_ = std::stoi(std::string(option.substr(strlen("--oat-fd="))), nullptr, 0);
        if (oat_fd_ < 0) {
          Usage("Invalid --oat-fd %d", oat_fd_);
        }
      } else if (StartsWith(option, "--vdex-fd=")) {
        vdex_fd_ = std::stoi(std::string(option.substr(strlen("--vdex-fd="))), nullptr, 0);
        if (vdex_fd_ < 0) {
          Usage("Invalid --vdex-fd %d", vdex_fd_);
        }
      } else if (StartsWith(option, "--zip-fd=")) {
        zip_fd_ = std::stoi(std::string(option.substr(strlen("--zip-fd="))), nullptr, 0);
        if (zip_fd_ < 0) {
          Usage("Invalid --zip-fd %d", zip_fd_);
        }
      } else if (StartsWith(option, "--class-loader-context=")) {
        context_str_ = std::string(option.substr(strlen("--class-loader-context=")));
      } else if (StartsWith(option, "--class-loader-context-fds=")) {
        std::string str_context_fds_arg =
            std::string(option.substr(strlen("--class-loader-context-fds=")));
        std::vector<std::string> str_fds = android::base::Split(str_context_fds_arg, ":");
        for (const std::string& str_fd : str_fds) {
          context_fds_.push_back(std::stoi(str_fd, nullptr, 0));
          if (context_fds_.back() < 0) {
            Usage("Invalid --class-loader-context-fds %s", str_context_fds_arg.c_str());
          }
        }
      } else if (option == "--flatten-class-loader-context") {
        only_flatten_context_ = true;
      } else if (option == "--validate-bcp") {
        only_validate_bcp_ = true;
      } else {
        Usage("Unknown argument '%s'", raw_option);
      }
    }

    if (image_.empty()) {
      // If we don't receive the image, try to use the default one.
      // Tests may specify a different image (e.g. core image).
      std::string error_msg;
      image_ = GetDefaultBootImageLocation(&error_msg);

      if (image_.empty()) {
        LOG(ERROR) << error_msg;
        Usage("--image unspecified and ANDROID_ROOT not set or image file does not exist.");
      }
    }
  }

  bool CreateRuntime() const {
    RuntimeOptions options;
    // The image could be custom, so make sure we explicitly pass it.
    std::string img = "-Ximage:" + image_;
    options.push_back(std::make_pair(img, nullptr));
    // The instruction set of the image should match the instruction set we will test.
    const void* isa_opt = reinterpret_cast<const void*>(GetInstructionSetString(isa_));
    options.push_back(std::make_pair("imageinstructionset", isa_opt));
    // Explicit runtime args.
    for (const char* runtime_arg : runtime_args_) {
      options.push_back(std::make_pair(runtime_arg, nullptr));
    }
     // Disable libsigchain. We don't don't need it to evaluate DexOptNeeded status.
    options.push_back(std::make_pair("-Xno-sig-chain", nullptr));
    // Pretend we are a compiler so that we can re-use the same infrastructure to load a different
    // ISA image and minimize the amount of things that get started.
    NoopCompilerCallbacks callbacks;
    options.push_back(std::make_pair("compilercallbacks", &callbacks));
    // Make sure we don't attempt to relocate. The tool should only retrieve the DexOptNeeded
    // status and not attempt to relocate the boot image.
    options.push_back(std::make_pair("-Xnorelocate", nullptr));

    if (!Runtime::Create(options, false)) {
      LOG(ERROR) << "Unable to initialize runtime";
      return false;
    }
    // Runtime::Create acquired the mutator_lock_ that is normally given away when we
    // Runtime::Start. Give it away now.
    Thread::Current()->TransitionFromRunnableToSuspended(kNative);

    return true;
  }

  ReturnCode GetDexOptNeeded() const {
    if (!CreateRuntime()) {
      return ReturnCode::kErrorCannotCreateRuntime;
    }
    std::unique_ptr<Runtime> runtime(Runtime::Current());

    // Only when the runtime is created can we create the class loader context: the
    // class loader context will open dex file and use the MemMap global lock that the
    // runtime owns.
    std::unique_ptr<ClassLoaderContext> class_loader_context;
    if (!context_str_.empty()) {
      class_loader_context = ClassLoaderContext::Create(context_str_);
      if (class_loader_context == nullptr) {
        Usage("Invalid --class-loader-context '%s'", context_str_.c_str());
      }
    }
    if (class_loader_context != nullptr) {
      size_t dir_index = dex_file_.rfind('/');
      std::string classpath_dir = (dir_index != std::string::npos)
          ? dex_file_.substr(0, dir_index)
          : "";

      if (!class_loader_context->OpenDexFiles(classpath_dir,
                                              context_fds_,
                                              /*only_read_checksums=*/ true)) {
        return ReturnCode::kDex2OatFromScratch;
      }
    }

    std::unique_ptr<OatFileAssistant> oat_file_assistant;
    oat_file_assistant = std::make_unique<OatFileAssistant>(dex_file_.c_str(),
                                                            isa_,
                                                            class_loader_context.get(),
                                                            /*load_executable=*/ false,
                                                            /*only_load_system_executable=*/ false,
                                                            vdex_fd_,
                                                            oat_fd_,
                                                            zip_fd_);
    // Always treat elements of the bootclasspath as up-to-date.
    // TODO(calin): this check should be in OatFileAssistant.
    if (oat_file_assistant->IsInBootClassPath()) {
      return ReturnCode::kNoDexOptNeeded;
    }

    int dexoptNeeded = oat_file_assistant->GetDexOptNeeded(compiler_filter_,
                                                           assume_profile_changed_,
                                                           downgrade_);

    // Convert OatFileAssistant codes to dexoptanalyzer codes.
    switch (dexoptNeeded) {
      case OatFileAssistant::kNoDexOptNeeded: return ReturnCode::kNoDexOptNeeded;
      case OatFileAssistant::kDex2OatFromScratch: return ReturnCode::kDex2OatFromScratch;
      case OatFileAssistant::kDex2OatForBootImage: return ReturnCode::kDex2OatForBootImageOat;
      case OatFileAssistant::kDex2OatForFilter: return ReturnCode::kDex2OatForFilterOat;

      case -OatFileAssistant::kDex2OatForBootImage: return ReturnCode::kDex2OatForBootImageOdex;
      case -OatFileAssistant::kDex2OatForFilter: return ReturnCode::kDex2OatForFilterOdex;
      default:
        LOG(ERROR) << "Unknown dexoptNeeded " << dexoptNeeded;
        return ReturnCode::kErrorUnknownDexOptNeeded;
    }
  }

  // Validates the boot classpath and boot classpath extensions by checking the image checksums,
  // the oat files and the vdex files.
  //
  // Returns `ReturnCode::kNoDexOptNeeded` when all the files are up-to-date,
  // `ReturnCode::kDex2OatFromScratch` if any of the files are missing or out-of-date, and
  // `ReturnCode::kErrorCannotCreateRuntime` if the files could not be tested due to problem
  // creating a runtime.
  ReturnCode ValidateBcp() const {
    using ImageSpace = gc::space::ImageSpace;

    if (!CreateRuntime()) {
      return ReturnCode::kErrorCannotCreateRuntime;
    }
    std::unique_ptr<Runtime> runtime(Runtime::Current());

    auto dex_files = ArrayRef<const DexFile* const>(runtime->GetClassLinker()->GetBootClassPath());
    auto boot_image_spaces = ArrayRef<ImageSpace* const>(runtime->GetHeap()->GetBootImageSpaces());
    const std::string checksums = ImageSpace::GetBootClassPathChecksums(boot_image_spaces,
                                                                        dex_files);

    std::string error_msg;
    const std::vector<std::string>& bcp = runtime->GetBootClassPath();
    const std::vector<std::string>& bcp_locations = runtime->GetBootClassPathLocations();
    const std::vector<std::string>& image_locations = runtime->GetImageLocations();
    const std::string bcp_locations_path = android::base::Join(bcp_locations, ':');
    if (!ImageSpace::VerifyBootClassPathChecksums(checksums,
                                                  bcp_locations_path,
                                                  ArrayRef<const std::string>(image_locations),
                                                  ArrayRef<const std::string>(bcp_locations),
                                                  ArrayRef<const std::string>(bcp),
                                                  runtime->GetInstructionSet(),
                                                  &error_msg)) {
      LOG(INFO) << "Failed to verify boot class path checksums: " << error_msg;
      return ReturnCode::kDex2OatFromScratch;
    }

    const auto& image_spaces = runtime->GetHeap()->GetBootImageSpaces();
    size_t bcp_component_count = 0;
    for (const auto& image_space : image_spaces) {
      if (!image_space->GetImageHeader().IsValid()) {
        LOG(INFO) << "Image header is not valid: " << image_space->GetImageFilename();
        return ReturnCode::kDex2OatFromScratch;
      }
      const OatFile* oat_file = image_space->GetOatFile();
      if (oat_file == nullptr) {
        const std::string oat_path = ReplaceFileExtension(image_space->GetImageFilename(), "oat");
        LOG(INFO) << "Oat file missing: " << oat_path;
        return ReturnCode::kDex2OatFromScratch;
      }
      if (!oat_file->GetOatHeader().IsValid() ||
          !ImageSpace::ValidateOatFile(*oat_file, &error_msg)) {
        LOG(INFO) << "Oat file is not valid: " << oat_file->GetLocation() << " " << error_msg;
        return ReturnCode::kDex2OatFromScratch;
      }
      const VdexFile* vdex_file = oat_file->GetVdexFile();
      if (vdex_file == nullptr || !vdex_file->IsValid()) {
        LOG(INFO) << "Vdex file is not valid : " << oat_file->GetLocation();
        return ReturnCode::kDex2OatFromScratch;
      }
      bcp_component_count += image_space->GetComponentCount();
    }

    // If the number of components encountered in the image spaces does not match the number
    // of components expected from the boot classpath locations then something is missing.
    if (bcp_component_count != bcp_locations.size()) {
      for (size_t i = bcp_component_count; i < bcp_locations.size(); ++i) {
        LOG(INFO) << "Missing image file for " << bcp_locations[i];
      }
      return ReturnCode::kDex2OatFromScratch;
    }

    return ReturnCode::kNoDexOptNeeded;
  }

  ReturnCode FlattenClassLoaderContext() const {
    DCHECK(only_flatten_context_);
    if (context_str_.empty()) {
      return ReturnCode::kErrorInvalidArguments;
    }

    std::unique_ptr<ClassLoaderContext> context = ClassLoaderContext::Create(context_str_);
    if (context == nullptr) {
      Usage("Invalid --class-loader-context '%s'", context_str_.c_str());
    }

    std::cout << context->FlattenDexPaths() << std::flush;
    return ReturnCode::kFlattenClassLoaderContextSuccess;
  }

  ReturnCode Run() const {
    if (only_flatten_context_) {
      return FlattenClassLoaderContext();
    } else if (only_validate_bcp_) {
      return ValidateBcp();
    } else {
      return GetDexOptNeeded();
    }
  }

 private:
  std::string dex_file_;
  InstructionSet isa_;
  CompilerFilter::Filter compiler_filter_;
  std::string context_str_;
  bool only_flatten_context_;
  bool only_validate_bcp_;
  bool assume_profile_changed_;
  bool downgrade_;
  std::string image_;
  std::vector<const char*> runtime_args_;
  int oat_fd_ = -1;
  int vdex_fd_ = -1;
  // File descriptor corresponding to apk, dex_file, or zip.
  int zip_fd_ = -1;
  std::vector<int> context_fds_;
};

static ReturnCode dexoptAnalyze(int argc, char** argv) {
  DexoptAnalyzer analyzer;

  // Parse arguments. Argument mistakes will lead to exit(kErrorInvalidArguments) in UsageError.
  analyzer.ParseArgs(argc, argv);
  return analyzer.Run();
}

}  // namespace dexoptanalyzer
}  // namespace art

int main(int argc, char **argv) {
  art::dexoptanalyzer::ReturnCode return_code = art::dexoptanalyzer::dexoptAnalyze(argc, argv);
  return static_cast<int>(return_code);
}
