/*
 * Copyright (C) 2021 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 <filesystem>
#include <optional>

#include <android-base/properties.h>
#include <android-base/strings.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <kver/kernel_release.h>
#include <vintf/VintfObject.h>
#include <vintf/parse_string.h>

#include "ramdisk_utils.h"

using android::base::GetBoolProperty;
using android::base::GetProperty;
using android::kver::KernelRelease;
using android::vintf::Level;
using android::vintf::RuntimeInfo;
using android::vintf::Version;
using android::vintf::VintfObject;
using testing::IsSupersetOf;

// Returns true iff the device has the specified feature.
static bool deviceSupportsFeature(const char *feature) {
  bool device_supports_feature = false;
  FILE *p = popen("pm list features", "re");
  if (p) {
    char *line = NULL;
    size_t len = 0;
    while (getline(&line, &len, p) > 0) {
      if (strstr(line, feature)) {
        device_supports_feature = true;
        break;
      }
    }
    if (line) {
      free(line);
      line = NULL;
    }
    pclose(p);
  }
  return device_supports_feature;
}

static bool isTV() {
  return deviceSupportsFeature("android.software.leanback");
}

std::optional<std::string> get_config(
    const std::map<std::string, std::string>& configs, const std::string& key) {
  auto it = configs.find(key);
  if (it == configs.end()) {
    return std::nullopt;
  }
  return it->second;
}

class GenericBootImageTest : public testing::Test {
 public:
  void SetUp() override {
    auto vintf = VintfObject::GetInstance();
    ASSERT_NE(nullptr, vintf);
    runtime_info = vintf->getRuntimeInfo(RuntimeInfo::FetchFlag::CPU_VERSION |
                                         RuntimeInfo::FetchFlag::CONFIG_GZ);
    ASSERT_NE(nullptr, runtime_info);

    const auto& configs = runtime_info->kernelConfigs();
    if (get_config(configs, "CONFIG_ARM") == "y") {
      GTEST_SKIP() << "Skipping on 32-bit ARM devices";
    } else if (get_config(configs, "CONFIG_X86") == "y" ||
               get_config(configs, "CONFIG_X86_64") == "y") {
      GTEST_SKIP() << "Skipping on X86 & X86_64 devices";
    }
    // Technically, the test should also be skipped on CONFIG_X86 and
    // CONFIG_X86_64, and only run on CONFIG_ARM64,
    // but we want to keep this test passing on virtual
    // device targets, and we don't have any requests to skip this test
    // on x86 / x86_64 as of 2022-06-07.

    int firstApiLevel = std::stoi(android::base::GetProperty("ro.product.first_api_level", "0"));
    if (isTV() && firstApiLevel <= __ANDROID_API_T__) {
      GTEST_SKIP() << "Skipping on TV devices";
    }
  }
  std::shared_ptr<const RuntimeInfo> runtime_info;
};

TEST_F(GenericBootImageTest, KernelReleaseFormat) {
  // On "GKI 2.0" with 5.10+ kernels, VTS runs once with the device kernel,
  // so this test is meaningful.
  if (runtime_info->kernelVersion().dropMinor() < Version{5, 10}) {
    GTEST_SKIP() << "Exempt generic kernel image (GKI) test on kernel "
                 << runtime_info->kernelVersion()
                 << ". Only required on 5.10+.";
  }

  const std::string& release = runtime_info->osRelease();
  ASSERT_TRUE(
      KernelRelease::Parse(release, true /* allow_suffix */).has_value())
      << "Kernel release '" << release
      << "' does not have generic kernel image (GKI) release format. It must "
         "match this regex:\n"
      << R"(^(?P<w>\d+)[.](?P<x>\d+)[.](?P<y>\d+)-(?P<z>android\d+)-(?P<k>\d+).*$)"
      << "\nExample: 5.4.42-android12-0-something";
}

std::set<std::string> GetRequirementBySdkLevel(uint32_t target_sdk_level) {
  // Files which must be present in generic ramdisk. This list acts as a lower
  // bound for device's ramdisk.
  static const std::map<uint32_t, std::set<std::string>> required_by_level = {
      {0, {"init", "system/etc/ramdisk/build.prop"}},  // or some other number?
      {
          __ANDROID_API_T__,
          {"system/bin/snapuserd", "system/etc/init/snapuserd.rc"},
      }};
  std::set<std::string> res;
  for (const auto& [level, requirements] : required_by_level) {
    if (level > target_sdk_level) {
      break;
    }
    res.insert(requirements.begin(), requirements.end());
  }
  return res;
}

std::set<std::string> GetAllowListBySdkLevel(uint32_t target_sdk_level) {
  // Files that are allowed in generic ramdisk(but not necessarily required)
  // This list acts as an upper bound for what the device's ramdisk can possibly
  // contain.
  static const std::map<uint32_t, std::set<std::string>> allow_by_level = {
      {__ANDROID_API_T__, {"system/bin/snapuserd_ramdisk"}},
      {__ANDROID_API_U__, {"dev/console", "dev/null", "dev/urandom"}},
  };
  auto res = GetRequirementBySdkLevel(target_sdk_level);
  for (const auto& [level, requirements] : allow_by_level) {
    if (level > target_sdk_level) {
      break;
    }
    res.insert(requirements.begin(), requirements.end());
  }
  return res;
}

TEST_F(GenericBootImageTest, GenericRamdisk) {
  // On "GKI 2.0" with 5.10+ kernels, VTS runs once with the device kernel,
  // so this test is meaningful.
  if (runtime_info->kernelVersion().dropMinor() < Version{5, 10}) {
    GTEST_SKIP() << "Exempt generic ramdisk test on kernel "
                 << runtime_info->kernelVersion()
                 << ". Only required on 5.10+.";
    return;
  }

  using std::filesystem::recursive_directory_iterator;

  std::string slot_suffix = GetProperty("ro.boot.slot_suffix", "");
  // Launching devices with T+ using android13+ kernels have the ramdisk in
  // init_boot instead of boot
  std::string error_msg;
  const auto kernel_level =
      VintfObject::GetInstance()->getKernelLevel(&error_msg);
  ASSERT_NE(Level::UNSPECIFIED, kernel_level) << error_msg;
  std::string boot_path;
  if (kernel_level >= Level::T) {
    int first_api_level = android::base::GetIntProperty(
        "ro.board.first_api_level",
        android::base::GetIntProperty("ro.vendor.api_level", 1000000));
    if (first_api_level >= __ANDROID_API_T__) {
      boot_path = "/dev/block/by-name/init_boot" + slot_suffix;
    } else {
      // This is the case of a device launched before Android 13 that is
      // upgrading its kernel to android13+. These devices can't add an
      // init_boot partition and need to include the equivalent ramdisk
      // functionality somewhere outside of boot.img (most likely in the
      // vendor_boot image). Since we don't know where to look, or which files
      // will be present, we can skip the rest of this test case.
      GTEST_SKIP() << "Exempt generic ramdisk test on upgrading device that "
                   << "launched before Android 13 and is now using an Android "
                   << "13+ kernel.";
      return;
    }
  } else {
    boot_path = "/dev/block/by-name/boot" + slot_suffix;
  }
  if (0 != access(boot_path.c_str(), R_OK)) {
    int saved_errno = errno;
    FAIL() << "Can't access " << boot_path << ": " << strerror(saved_errno);
    return;
  }

  const auto extracted_ramdisk = android::ExtractRamdiskToDirectory(boot_path);
  ASSERT_TRUE(extracted_ramdisk.ok())
      << "Failed to extract ramdisk: " << extracted_ramdisk.error();

  std::set<std::string> actual_files;
  const std::filesystem::path extracted_ramdisk_path((*extracted_ramdisk)->path);
  for (auto& p : recursive_directory_iterator(extracted_ramdisk_path)) {
    if (p.is_directory()) continue;
    auto rel_path = p.path().lexically_relative(extracted_ramdisk_path);
    actual_files.insert(rel_path.string());
  }

  const auto sdk_level =
      android::base::GetIntProperty("ro.bootimage.build.version.sdk", 0);
  const std::set<std::string> generic_ramdisk_required_list =
      GetRequirementBySdkLevel(sdk_level);
  std::set<std::string> generic_ramdisk_allow_list =
      GetAllowListBySdkLevel(sdk_level);

  const bool is_debuggable = GetBoolProperty("ro.debuggable", false);
  if (is_debuggable) {
    const std::set<std::string> debuggable_allowlist{
        "adb_debug.prop",
        "force_debuggable",
        "userdebug_plat_sepolicy.cil",
    };
    generic_ramdisk_allow_list.insert(debuggable_allowlist.begin(),
                                      debuggable_allowlist.end());
  }
  EXPECT_THAT(actual_files, IsSupersetOf(generic_ramdisk_required_list))
      << "Missing files required by " << (is_debuggable ? "debuggable " : "")
      << "generic ramdisk";
  EXPECT_THAT(generic_ramdisk_allow_list, IsSupersetOf(actual_files))
      << "Contains files disallowed by " << (is_debuggable ? "debuggable " : "")
      << "generic ramdisk";
}
