/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "ChromiumCDMAdapter.h"

#include <utility>

#include "GMPLog.h"
#include "WidevineUtils.h"
#include "content_decryption_module.h"
#include "content_decryption_module_ext.h"
#include "gmp-api/gmp-entrypoints.h"
#include "gmp-api/gmp-video-codec.h"
#include "mozilla/HelperMacros.h"
#include "mozilla/PodOperations.h"
#include "mozilla/dom/KeySystemNames.h"

#ifdef XP_WIN
#  include <strsafe.h>
#  include <windows.h>

#  include <unordered_map>
#  include <vector>

#  include "WinUtils.h"
#  include "nsWindowsDllInterceptor.h"
#else
#  include <fcntl.h>
#  include <sys/stat.h>
#  include <sys/types.h>
#  include <unistd.h>
#endif

const GMPPlatformAPI* sPlatform = nullptr;

namespace mozilla {

#ifdef XP_WIN
static void InitializeHooks();
#endif

ChromiumCDMAdapter::ChromiumCDMAdapter(
    nsTArray<std::pair<nsCString, nsCString>>&& aHostPathPairs) {
#ifdef XP_WIN
  InitializeHooks();
#endif
  PopulateHostFiles(std::move(aHostPathPairs));
}

void ChromiumCDMAdapter::SetAdaptee(PRLibrary* aLib) { mLib = aLib; }

void* ChromiumCdmHost(int aHostInterfaceVersion, void* aUserData) {
  GMP_LOG_DEBUG("ChromiumCdmHostFunc({}, {})", aHostInterfaceVersion,
                fmt::ptr(aUserData));
  if (aHostInterfaceVersion != cdm::Host_11::kVersion) {
    return nullptr;
  }
  return aUserData;
}

void* ChromiumCdmHostCompat(int aHostInterfaceVersion, void* aUserData) {
  GMP_LOG_DEBUG("ChromiumCdmHostCompatFunc({}, {})", aHostInterfaceVersion,
                fmt::ptr(aUserData));
  if (aHostInterfaceVersion != cdm::Host_10::kVersion) {
    return nullptr;
  }
  return aUserData;
}

#ifdef MOZILLA_OFFICIAL
static cdm::HostFile TakeToCDMHostFile(HostFileData& aHostFileData) {
  return cdm::HostFile(aHostFileData.mBinary.Path().get(),
                       aHostFileData.mBinary.TakePlatformFile(),
                       aHostFileData.mSig.TakePlatformFile());
}
#endif

GMPErr ChromiumCDMAdapter::GMPInit(const GMPPlatformAPI* aPlatformAPI) {
  GMP_LOG_DEBUG("ChromiumCDMAdapter::GMPInit");
  sPlatform = aPlatformAPI;
  if (NS_WARN_IF(!mLib)) {
    MOZ_CRASH("Missing library!");
    return GMPGenericErr;
  }

#ifdef MOZILLA_OFFICIAL
  // Note: we must call the VerifyCdmHost_0 function if it's present before
  // we call the initialize function.
  auto verify = reinterpret_cast<decltype(::VerifyCdmHost_0)*>(
      PR_FindFunctionSymbol(mLib, MOZ_STRINGIFY(VerifyCdmHost_0)));
  if (verify) {
    nsTArray<cdm::HostFile> files;
    for (HostFileData& hostFile : mHostFiles) {
      files.AppendElement(TakeToCDMHostFile(hostFile));
    }
    bool result = verify(files.Elements(), files.Length());
    GMP_LOG_DEBUG("{} VerifyCdmHost_0 returned {}", __func__, result);
    MOZ_DIAGNOSTIC_ASSERT(result, "Verification failed!");
  }
#endif

  auto init = reinterpret_cast<decltype(::INITIALIZE_CDM_MODULE)*>(
      PR_FindFunctionSymbol(mLib, MOZ_STRINGIFY(INITIALIZE_CDM_MODULE)));
  if (!init) {
    MOZ_CRASH("Missing init method!");
    return GMPGenericErr;
  }

  GMP_LOG_DEBUG(MOZ_STRINGIFY(INITIALIZE_CDM_MODULE) "()");
  init();

  return GMPNoErr;
}

GMPErr ChromiumCDMAdapter::GMPGetAPI(const char* aAPIName, void* aHostAPI,
                                     void** aPluginAPI,
                                     const nsACString& aKeySystem) {
  MOZ_ASSERT(
      aKeySystem.EqualsLiteral(kWidevineKeySystemName) ||
          aKeySystem.EqualsLiteral(kClearKeyKeySystemName) ||
          aKeySystem.EqualsLiteral(kClearKeyWithProtectionQueryKeySystemName) ||
          aKeySystem.EqualsLiteral("fake"),
      "Should not get an unrecognized key system. Why didn't it get "
      "blocked by MediaKeySystemAccess?");
  GMP_LOG_DEBUG("ChromiumCDMAdapter::GMPGetAPI({}, 0x{}, 0x{}, {}) this=0x{}",
                aAPIName, fmt::ptr(aHostAPI), fmt::ptr(aPluginAPI),
                PromiseFlatCString(aKeySystem).get(), fmt::ptr(this));

  int version;
  GetCdmHostFunc getCdmHostFunc;
  if (!strcmp(aAPIName, CHROMIUM_CDM_API)) {
    version = cdm::ContentDecryptionModule_11::kVersion;
    getCdmHostFunc = &ChromiumCdmHost;
  } else if (!strcmp(aAPIName, CHROMIUM_CDM_API_BACKWARD_COMPAT)) {
    version = cdm::ContentDecryptionModule_10::kVersion;
    getCdmHostFunc = &ChromiumCdmHostCompat;
  } else {
    MOZ_ASSERT_UNREACHABLE("We only support and expect cdm10/11!");
    GMP_LOG_DEBUG(
        "ChromiumCDMAdapter::GMPGetAPI({}, 0x{}, 0x{}) this=0x{} got "
        "unsupported CDM version!",
        aAPIName, fmt::ptr(aHostAPI), fmt::ptr(aPluginAPI), fmt::ptr(this));
    return GMPGenericErr;
  }

  auto create = reinterpret_cast<decltype(::CreateCdmInstance)*>(
      PR_FindFunctionSymbol(mLib, "CreateCdmInstance"));
  if (!create) {
    GMP_LOG_DEBUG(
        "ChromiumCDMAdapter::GMPGetAPI({}, 0x{}, 0x{}) this=0x{} "
        "FAILED to find CreateCdmInstance",
        aAPIName, fmt::ptr(aHostAPI), fmt::ptr(aPluginAPI), fmt::ptr(this));
    return GMPGenericErr;
  }

  void* cdm = create(version, aKeySystem.BeginReading(), aKeySystem.Length(),
                     getCdmHostFunc, aHostAPI);
  if (!cdm) {
    GMP_LOG_DEBUG(
        "ChromiumCDMAdapter::GMPGetAPI({}, 0x{}, 0x{}) this=0x{} "
        "FAILED to create cdm version {}",
        aAPIName, fmt::ptr(aHostAPI), fmt::ptr(aPluginAPI), fmt::ptr(this),
        version);
    return GMPGenericErr;
  }
  GMP_LOG_DEBUG("cdm: 0x{}, version: {}", fmt::ptr(cdm), version);
  *aPluginAPI = cdm;

  return *aPluginAPI ? GMPNoErr : GMPNotImplementedErr;
}

void ChromiumCDMAdapter::GMPShutdown() {
  GMP_LOG_DEBUG("ChromiumCDMAdapter::GMPShutdown()");

  decltype(::DeinitializeCdmModule)* deinit;
  deinit =
      (decltype(deinit))(PR_FindFunctionSymbol(mLib, "DeinitializeCdmModule"));
  if (deinit) {
    GMP_LOG_DEBUG("DeinitializeCdmModule()");
    deinit();
  }
}

/* static */
bool ChromiumCDMAdapter::Supports(int32_t aModuleVersion,
                                  int32_t aInterfaceVersion,
                                  int32_t aHostVersion) {
  return aModuleVersion == CDM_MODULE_VERSION &&
         ((aInterfaceVersion == cdm::ContentDecryptionModule_11::kVersion &&
           aHostVersion == cdm::Host_11::kVersion) ||
          (aInterfaceVersion == cdm::ContentDecryptionModule_10::kVersion &&
           aHostVersion == cdm::Host_10::kVersion));
}

#ifdef XP_WIN

MOZ_RUNINIT static WindowsDllInterceptor sKernel32Intercept;

typedef DWORD(WINAPI* QueryDosDeviceWFnPtr)(_In_opt_ LPCWSTR lpDeviceName,
                                            _Out_ LPWSTR lpTargetPath,
                                            _In_ DWORD ucchMax);

static WindowsDllInterceptor::FuncHookType<QueryDosDeviceWFnPtr>
    sOriginalQueryDosDeviceWFnPtr;

static std::unordered_map<std::wstring, std::wstring>* sDeviceNames = nullptr;

DWORD WINAPI QueryDosDeviceWHook(LPCWSTR lpDeviceName, LPWSTR lpTargetPath,
                                 DWORD ucchMax) {
  if (!sDeviceNames) {
    return 0;
  }
  std::wstring name = std::wstring(lpDeviceName);
  auto iter = sDeviceNames->find(name);
  if (iter == sDeviceNames->end()) {
    return 0;
  }
  const std::wstring& device = iter->second;
  if (device.size() + 1 > ucchMax) {
    return 0;
  }
  PodCopy(lpTargetPath, device.c_str(), device.size());
  lpTargetPath[device.size()] = 0;
  GMP_LOG_DEBUG("QueryDosDeviceWHook {} -> {}",
                NS_ConvertUTF16toUTF8(lpDeviceName).get(),
                NS_ConvertUTF16toUTF8(lpTargetPath).get());
  return device.size();
}

static std::vector<std::wstring> GetDosDeviceNames() {
  std::vector<std::wstring> v;
  std::vector<wchar_t> buf;
  buf.resize(1024);
  DWORD rv = GetLogicalDriveStringsW(buf.size(), buf.data());
  if (rv == 0 || rv > buf.size()) {
    return v;
  }

  // buf will be a list of null terminated strings, with the last string
  // being 0 length.
  const wchar_t* p = buf.data();
  const wchar_t* end = &buf.back();
  size_t l;
  while (p < end && (l = wcsnlen_s(p, end - p)) > 0) {
    // The string is of the form "C:\". We need to strip off the trailing
    // backslash.
    std::wstring drive = std::wstring(p, p + l);
    if (drive.back() == '\\') {
      drive.erase(drive.end() - 1);
    }
    v.push_back(std::move(drive));
    p += l + 1;
  }
  return v;
}

static std::wstring GetDeviceMapping(const std::wstring& aDosDeviceName) {
  wchar_t buf[MAX_PATH] = {0};
  DWORD rv = QueryDosDeviceW(aDosDeviceName.c_str(), buf, MAX_PATH);
  if (!rv) {
    return std::wstring(L"");
  }
  return std::wstring(buf, buf + rv);
}

static void InitializeHooks() {
  static bool initialized = false;
  if (initialized) {
    return;
  }
  initialized = true;
  sDeviceNames = new std::unordered_map<std::wstring, std::wstring>();
  for (const std::wstring& name : GetDosDeviceNames()) {
    sDeviceNames->emplace(name, GetDeviceMapping(name));
  }

  sKernel32Intercept.Init("kernelbase.dll");
  sOriginalQueryDosDeviceWFnPtr.Set(sKernel32Intercept, "QueryDosDeviceW",
                                    &QueryDosDeviceWHook);
}
#endif

HostFile::HostFile(HostFile&& aOther)
    : mPath(aOther.mPath), mFile(aOther.TakePlatformFile()) {}

HostFile::~HostFile() {
  if (mFile != cdm::kInvalidPlatformFile) {
#ifdef XP_WIN
    CloseHandle(mFile);
#else
    close(mFile);
#endif
    mFile = cdm::kInvalidPlatformFile;
  }
}

#ifdef XP_WIN
HostFile::HostFile(const nsCString& aPath)
    : mPath(NS_ConvertUTF8toUTF16(aPath)) {
  HANDLE handle =
      CreateFileW(mPath.get(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
                  NULL, OPEN_EXISTING, 0, NULL);
  mFile = (handle == INVALID_HANDLE_VALUE) ? cdm::kInvalidPlatformFile : handle;
}
#endif

#ifndef XP_WIN
HostFile::HostFile(const nsCString& aPath) : mPath(aPath) {
  // Note: open() returns -1 on failure; i.e. kInvalidPlatformFile.
  mFile = open(aPath.get(), O_RDONLY);
}
#endif

cdm::PlatformFile HostFile::TakePlatformFile() {
  cdm::PlatformFile f = mFile;
  mFile = cdm::kInvalidPlatformFile;
  return f;
}

void ChromiumCDMAdapter::PopulateHostFiles(
    nsTArray<std::pair<nsCString, nsCString>>&& aHostPathPairs) {
  for (const auto& pair : aHostPathPairs) {
    mHostFiles.AppendElement(HostFileData(mozilla::HostFile(pair.first),
                                          mozilla::HostFile(pair.second)));
  }
}

}  // namespace mozilla
