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

#include <android-base/file.h>
#include <android-base/logging.h>
#include <android-base/stringprintf.h>
#include <android-base/strings.h>
#include <openssl/sha.h>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <unordered_set>

#include "apex_file.h"
#include "status.h"
#include "status_or.h"
#include "string_log.h"

namespace android {
namespace apex {
namespace shim {

namespace fs = std::filesystem;

namespace {

static constexpr const char* kApexCtsShimPackage = "com.android.apex.cts.shim";
static constexpr const char* kHashFileName = "hash.txt";
static constexpr const int kBufSize = 1024;
static constexpr const char* kApexManifestFileName = "apex_manifest.json";
static constexpr const char* kEtcFolderName = "etc";
static constexpr const char* kLostFoundFolderName = "lost+found";
static constexpr const fs::perms kFordbiddenFilePermissions =
    fs::perms::owner_exec | fs::perms::group_exec | fs::perms::others_exec;

StatusOr<std::string> CalculateSha512(const std::string& path) {
  using StatusT = StatusOr<std::string>;
  LOG(DEBUG) << "Calculating SHA512 of " << path;
  SHA512_CTX ctx;
  SHA512_Init(&ctx);
  std::ifstream apex(path, std::ios::binary);
  if (apex.bad()) {
    return StatusT::MakeError(StringLog() << "Failed to open " << path);
  }
  char buf[kBufSize];
  while (!apex.eof()) {
    apex.read(buf, kBufSize);
    if (apex.bad()) {
      return StatusT::MakeError(StringLog() << "Failed to read " << path);
    }
    int bytes_read = apex.gcount();
    SHA512_Update(&ctx, buf, bytes_read);
  }
  uint8_t hash[SHA512_DIGEST_LENGTH];
  SHA512_Final(hash, &ctx);
  std::stringstream ss;
  ss << std::hex;
  for (int i = 0; i < SHA512_DIGEST_LENGTH; i++) {
    ss << std::setw(2) << std::setfill('0') << static_cast<int>(hash[i]);
  }
  return StatusT(ss.str());
}

StatusOr<std::vector<std::string>> ReadSha512(const std::string& path) {
  using android::base::ReadFileToString;
  using android::base::StringPrintf;
  using StatusT = StatusOr<std::vector<std::string>>;
  const std::string& file_path =
      StringPrintf("%s/%s/%s", path.c_str(), kEtcFolderName, kHashFileName);
  LOG(DEBUG) << "Reading SHA512 from " << file_path;
  std::string hash;
  if (!ReadFileToString(file_path, &hash, false /* follows symlinks */)) {
    return StatusT::MakeError(PStringLog() << "Failed to read " << file_path);
  }
  return StatusT(android::base::Split(hash, "\n"));
}

Status IsRegularFile(const fs::directory_entry& entry) {
  const fs::path& path = entry.path();
  std::error_code ec;
  fs::file_status status = entry.status(ec);
  if (ec) {
    return Status::Fail(StringLog()
                        << "Failed to stat " << path << " : " << ec);
  }
  if (!fs::is_regular_file(status)) {
    return Status::Fail(StringLog() << path << " is not a file");
  }
  if ((status.permissions() & kFordbiddenFilePermissions) != fs::perms::none) {
    return Status::Fail(StringLog() << path << " has illegal permissions");
  }
  // TODO: consider checking that file only contains ascii characters.
  return Status::Success();
}

Status IsHashTxt(const fs::directory_entry& entry) {
  LOG(DEBUG) << "Checking if " << entry.path() << " is an allowed file";
  const Status& status = IsRegularFile(entry);
  if (!status.Ok()) {
    return status;
  }
  if (entry.path().filename() != kHashFileName) {
    return Status::Fail(StringLog() << "Illegal file " << entry.path());
  }
  return Status::Success();
}

Status IsWhitelistedTopLevelEntry(const fs::directory_entry& entry) {
  LOG(DEBUG) << "Checking if " << entry.path() << " is an allowed directory";
  std::error_code ec;
  const fs::path& path = entry.path();
  if (path.filename() == kLostFoundFolderName) {
    bool is_empty = fs::is_empty(path, ec);
    if (ec) {
      return Status::Fail(StringLog()
                          << "Failed to scan " << path << " : " << ec);
    }
    if (is_empty) {
      return Status::Success();
    } else {
      return Status::Fail(StringLog() << path << " is not empty");
    }
  } else if (path.filename() == kEtcFolderName) {
    auto iter = fs::directory_iterator(path, ec);
    if (ec) {
      return Status::Fail(StringLog()
                          << "Failed to scan " << path << " : " << ec);
    }
    bool is_empty = fs::is_empty(path, ec);
    if (ec) {
      return Status::Fail(StringLog()
                          << "Failed to scan " << path << " : " << ec);
    }
    if (is_empty) {
      return Status::Fail(StringLog()
                          << path << " should contain " << kHashFileName);
    }
    // TODO: change to non-throwing iterator.
    while (iter != fs::end(iter)) {
      const Status& status = IsHashTxt(*iter);
      if (!status.Ok()) {
        return status;
      }
      iter = iter.increment(ec);
      if (ec) {
        return Status::Fail(StringLog()
                            << "Failed to scan " << path << " : " << ec);
      }
    }
    return Status::Success();
  } else if (path.filename() == kApexManifestFileName) {
    return IsRegularFile(entry);
  } else {
    return Status::Fail(StringLog() << "Illegal entry " << path);
  }
}

}  // namespace

bool IsShimApex(const ApexFile& apex_file) {
  return apex_file.GetManifest().name() == kApexCtsShimPackage;
}

Status ValidateShimApex(const std::string& mount_point,
                        const ApexFile& apex_file) {
  LOG(DEBUG) << "Validating shim apex " << mount_point;
  const ApexManifest& manifest = apex_file.GetManifest();
  if (!manifest.preinstallhook().empty() ||
      !manifest.postinstallhook().empty()) {
    return Status::Fail(
        "Shim apex is not allowed to have pre or post install hooks");
  }
  std::error_code ec;
  auto iter = fs::directory_iterator(mount_point, ec);
  if (ec) {
    return Status::Fail(StringLog()
                        << "Failed to scan " << mount_point << " : " << ec);
  }
  // Unfortunately fs::directory_iterator::operator++ can throw an exception,
  // which means that it's impossible to use range-based for loop here.
  // TODO: wrap into a non-throwing iterator to support range-based for loop.
  while (iter != fs::end(iter)) {
    const Status& status = IsWhitelistedTopLevelEntry(*iter);
    if (!status.Ok()) {
      return status;
    }
    iter = iter.increment(ec);
    if (ec) {
      return Status::Fail(StringLog()
                          << "Failed to scan " << mount_point << " : " << ec);
    }
  }
  return Status::Success();
}

Status ValidateUpdate(const std::string& system_apex_path,
                      const std::string& new_apex_path) {
  LOG(DEBUG) << "Validating update of shim apex to " << new_apex_path
             << " using system shim apex " << system_apex_path;
  auto allowed = ReadSha512(system_apex_path);
  if (!allowed.Ok()) {
    return allowed.ErrorStatus();
  }
  auto actual = CalculateSha512(new_apex_path);
  if (!actual.Ok()) {
    return actual.ErrorStatus();
  }
  auto it = std::find(allowed->begin(), allowed->end(), *actual);
  if (it == allowed->end()) {
    return Status::Fail(StringLog()
                        << new_apex_path << " has unexpected SHA512 hash "
                        << *actual);
  }
  return Status::Success();
}

}  // namespace shim
}  // namespace apex
}  // namespace android