/*
 * Copyright (C) 2019 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 <climits>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <string>

#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <android-base/result.h>
#include <android-base/strings.h>

#include "linkerconfig/apex.h"
#include "linkerconfig/apexconfig.h"
#include "linkerconfig/baseconfig.h"
#include "linkerconfig/configparser.h"
#include "linkerconfig/context.h"
#include "linkerconfig/environment.h"
#include "linkerconfig/legacy.h"
#include "linkerconfig/log.h"
#include "linkerconfig/namespacebuilder.h"
#include "linkerconfig/recovery.h"
#include "linkerconfig/variableloader.h"
#include "linkerconfig/variables.h"

using android::base::ErrnoError;
using android::base::Error;
using android::base::Join;
using android::base::Result;
using android::linkerconfig::contents::Context;
using android::linkerconfig::modules::ApexInfo;
using android::linkerconfig::modules::Configuration;

namespace {
const static struct option program_options[] = {
    {"target", required_argument, 0, 't'},
    {"strict", no_argument, 0, 's'},
#ifndef __ANDROID__
    {"root", required_argument, 0, 'r'},
    {"vndk", required_argument, 0, 'v'},
    {"product_vndk", required_argument, 0, 'p'},
    {"recovery", no_argument, 0, 'y'},
#endif
    {"help", no_argument, 0, 'h'},
    {0, 0, 0, 0}};

struct ProgramArgs {
  std::string target_directory;
  bool strict;
  std::string root;
  std::string vndk_version;
  std::string product_vndk_version;
  bool is_recovery;
};

[[noreturn]] void PrintUsage(int status = EXIT_SUCCESS) {
  std::cerr << "Usage : linkerconfig [--target <target_directory>]"
               " [--strict]"
#ifndef __ANDROID__
               " --root <root dir>"
               " --vndk <vndk version>"
               " --product_vndk <product vndk version>"
               " --recovery"
#endif
               " [--help]"
            << std::endl;
  exit(status);
}

std::string RealPath(std::string_view path) {
  char resolved_path[PATH_MAX];
  if (realpath(path.data(), resolved_path) != nullptr) {
    return resolved_path;
  }
  PrintUsage(-1);
}

bool ParseArgs(int argc, char* argv[], ProgramArgs* args) {
  int parse_result;
  while ((parse_result = getopt_long(
              argc, argv, "t:sr:v:ep:hyl", program_options, NULL)) != -1) {
    switch (parse_result) {
      case 't':
        args->target_directory = optarg;
        break;
      case 's':
        args->strict = true;
        break;
      case 'r':
        args->root = RealPath(optarg);
        break;
      case 'v':
        args->vndk_version = optarg;
        break;
      case 'p':
        args->product_vndk_version = optarg;
        break;
      case 'y':
        args->is_recovery = true;
        break;
      case 'h':
        PrintUsage();
      default:
        return false;
    }
  }

  if (optind < argc) {
    return false;
  }

  return true;
}

void LoadVariables(ProgramArgs args) {
#ifndef __ANDROID__
  if (!args.is_recovery && args.root == "") {
    PrintUsage();
  }
  android::linkerconfig::modules::Variables::AddValue("ro.vndk.version",
                                                      args.vndk_version);
  android::linkerconfig::modules::Variables::AddValue(
      "ro.product.vndk.version", args.product_vndk_version);
#endif
  if (!args.is_recovery) {
    android::linkerconfig::generator::LoadVariables(args.root);
  }
}

Result<void> WriteConfigurationToFile(Configuration& conf,
                                      std::string file_path) {
  std::ostream* out = &std::cout;
  std::ofstream file_out;

  if (file_path != "") {
    file_out.open(file_path);
    if (file_out.fail()) {
      return ErrnoError() << "Failed to open file " << file_path;
    }
    out = &file_out;
  }

  android::linkerconfig::modules::ConfigWriter config_writer;

  conf.WriteConfig(config_writer);
  *out << config_writer.ToString();
  if (!out->good()) {
    return ErrnoError() << "Failed to write content to " << file_path;
  }

  return {};
}

Result<void> UpdatePermission([[maybe_unused]] const std::string& file_path) {
#ifdef __ANDROID__
  if (fchmodat(AT_FDCWD,
               file_path.c_str(),
               S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH,
               AT_SYMLINK_NOFOLLOW) < 0) {
    return ErrnoError() << "Failed to update permission of " << file_path;
  }
#endif

  return {};
}

Context GetContext(ProgramArgs args) {
  Context ctx;
  if (args.strict) {
    ctx.SetStrictMode(true);
  }
  if (!args.is_recovery) {
    auto apex_list = android::linkerconfig::modules::ScanActiveApexes(args.root);
    if (apex_list.ok()) {
      for (auto const& apex_item : *apex_list) {
        auto apex_info = apex_item.second;
        if (apex_info.has_bin || apex_info.has_lib) {
          ctx.AddApexModule(std::move(apex_info));
        }
      }
    } else {
      LOG(ERROR) << "Failed to scan APEX modules : " << apex_list.error();
    }
  }

  std::string system_config_path = args.root + "/system/etc/linker.config.pb";
  if (access(system_config_path.c_str(), F_OK) == 0) {
    auto system_config =
        android::linkerconfig::modules::ParseLinkerConfig(system_config_path);
    if (system_config.ok()) {
      ctx.SetSystemConfig(*system_config);
    } else {
      LOG(ERROR) << "Failed to read system config : " << system_config.error();
    }
  }
  return ctx;
}

Configuration GetConfiguration(Context& ctx) {
  if (android::linkerconfig::modules::IsRecoveryMode()) {
    return android::linkerconfig::contents::CreateRecoveryConfiguration(ctx);
  }

  if (android::linkerconfig::modules::IsLegacyDevice()) {
    return android::linkerconfig::contents::CreateLegacyConfiguration(ctx);
  }

  // Use base configuration in default
  return android::linkerconfig::contents::CreateBaseConfiguration(ctx);
}

Result<void> GenerateConfiguration(Configuration config,
                                   const std::string& dir_path,
                                   bool update_permission) {
  std::string file_path = "";
  if (dir_path != "") {
    file_path = dir_path + "/ld.config.txt";
  }

  auto write_config = WriteConfigurationToFile(config, file_path);
  if (!write_config.ok()) {
    return write_config;
  } else if (update_permission && file_path != "") {
    return UpdatePermission(file_path);
  }

  return {};
}

Result<void> GenerateBaseLinkerConfiguration(Context& ctx,
                                             const std::string& dir_path) {
  return GenerateConfiguration(GetConfiguration(ctx), dir_path, true);
}

Result<void> GenerateRecoveryLinkerConfiguration(Context& ctx,
                                                 const std::string& dir_path) {
  return GenerateConfiguration(
      android::linkerconfig::contents::CreateRecoveryConfiguration(ctx),
      dir_path,
      false);
}

Result<void> GenerateApexConfiguration(
    const std::string& base_dir, android::linkerconfig::contents::Context& ctx,
    const android::linkerconfig::modules::ApexInfo& target_apex) {
  std::string dir_path = base_dir + "/" + target_apex.name;
  if (auto ret = mkdir(dir_path.c_str(), 0755); ret != 0 && errno != EEXIST) {
    return ErrnoError() << "Failed to create directory " << dir_path;
  }

  return GenerateConfiguration(
      android::linkerconfig::contents::CreateApexConfiguration(ctx, target_apex),
      dir_path,
      true);
}

void GenerateApexConfigurations(Context& ctx, const std::string& dir_path) {
  for (auto const& apex_item : ctx.GetApexModules()) {
    if (apex_item.has_bin) {
      auto result = GenerateApexConfiguration(dir_path, ctx, apex_item);
      if (!result.ok()) {
        LOG(WARNING) << result.error();
      }
    }
  }
}

void GenerateApexLibrariesConfig(Context& ctx, const std::string& dir_path) {
  if (dir_path == "") {
    return;
  }
  const std::string file_path = dir_path + "/apex.libraries.config.txt";
  std::ofstream out(file_path);
  for (auto const& apex_item : ctx.GetApexModules()) {
    if (!apex_item.jni_libs.empty()) {
      out << "jni " << apex_item.namespace_name << " "
          << Join(apex_item.jni_libs, ":") << '\n';
    }
    if (!apex_item.public_libs.empty()) {
      out << "public " << apex_item.namespace_name << " "
          << Join(apex_item.public_libs, ":") << '\n';
    }
  }
  out.close();
  UpdatePermission(file_path);
}

void ExitOnFailure(Result<void> task) {
  if (!task.ok()) {
    LOG(FATAL) << task.error();
    exit(EXIT_FAILURE);
  }
}

#ifdef __ANDROID__
struct CombinedLogger {
  android::base::LogdLogger logd;

  void operator()(android::base::LogId id, android::base::LogSeverity severity,
                  const char* tag, const char* file, unsigned int line,
                  const char* message) {
    logd(id, severity, tag, file, line, message);
    KernelLogger(id, severity, tag, file, line, message);
  }
};
#endif
}  // namespace

int main(int argc, char* argv[]) {
  android::base::InitLogging(argv
#ifdef __ANDROID__
                             ,
                             CombinedLogger()
#endif
  );

  ProgramArgs args = {};

  if (!ParseArgs(argc, argv, &args)) {
    PrintUsage(EXIT_FAILURE);
  }

  if (!android::linkerconfig::modules::IsLegacyDevice() &&
      android::linkerconfig::modules::IsVndkLiteDevice()) {
    LOG(ERROR) << "Linkerconfig no longer supports VNDK-Lite configuration";
    exit(EXIT_FAILURE);
  }

  LoadVariables(args);
  Context ctx = GetContext(args);

  // when exec'ed from init, this is 077, which makes the subdirectories
  // inaccessible for others. set umask to 022 so that they can be
  // accessible.
  umask(022);

  if (args.is_recovery) {
    ExitOnFailure(
        GenerateRecoveryLinkerConfiguration(ctx, args.target_directory));
  } else {
    ExitOnFailure(GenerateBaseLinkerConfiguration(ctx, args.target_directory));
    GenerateApexConfigurations(ctx, args.target_directory);
    GenerateApexLibrariesConfig(ctx, args.target_directory);
  }

  return EXIT_SUCCESS;
}