/* 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 "updateutils_win.h"
#include <errno.h>
#include <shlwapi.h>
#include <string.h>

#ifdef MOZ_CLANG_PLUGIN
#  define MOZ_RUNINIT __attribute__((annotate("moz_global_var")))
#else
#  define MOZ_RUNINIT
#endif

/**
 * Note: The reason that these functions are separated from those in
 *       updatehelper.h/updatehelper.cpp is that those functions are strictly
 *       used within the updater, whereas changing functions in updateutils_win
 *       will have effects reaching beyond application update.
 */

// This section implements the minimum set of dirent APIs used by updater.cpp on
// Windows.  If updater.cpp is modified to use more of this API, we need to
// implement those parts here too.
MOZ_RUNINIT static dirent gDirEnt;

DIR::DIR(const WCHAR* path) : findHandle(INVALID_HANDLE_VALUE) {
  memset(name, 0, sizeof(name));
  wcsncpy(name, path, sizeof(name) / sizeof(name[0]));
  wcsncat(name, L"\\*", sizeof(name) / sizeof(name[0]) - wcslen(name) - 1);
}

DIR::~DIR() {
  if (findHandle != INVALID_HANDLE_VALUE) {
    FindClose(findHandle);
  }
}

dirent::dirent() { d_name[0] = L'\0'; }

DIR* opendir(const WCHAR* path) { return new DIR(path); }

int closedir(DIR* dir) {
  delete dir;
  return 0;
}

dirent* readdir(DIR* dir) {
  WIN32_FIND_DATAW data;
  if (dir->findHandle != INVALID_HANDLE_VALUE) {
    BOOL result = FindNextFileW(dir->findHandle, &data);
    if (!result) {
      if (GetLastError() != ERROR_NO_MORE_FILES) {
        errno = ENOENT;
      }
      return nullptr;
    }
  } else {
    // Reading the first directory entry
    dir->findHandle = FindFirstFileW(dir->name, &data);
    if (dir->findHandle == INVALID_HANDLE_VALUE) {
      if (GetLastError() == ERROR_FILE_NOT_FOUND) {
        errno = ENOENT;
      } else {
        errno = EBADF;
      }
      return nullptr;
    }
  }
  size_t direntBufferLength =
      sizeof(gDirEnt.d_name) / sizeof(gDirEnt.d_name[0]);
  wcsncpy(gDirEnt.d_name, data.cFileName, direntBufferLength);
  // wcsncpy does not guarantee a null-terminated string if the source string is
  // too long.
  gDirEnt.d_name[direntBufferLength - 1] = '\0';
  return &gDirEnt;
}

/**
 * Joins a base directory path with a filename.
 *
 * @param  base  The base directory path of size MAX_PATH + 1
 * @param  extra The filename to append
 * @return TRUE if the file name was successful appended to base
 */
BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra) {
  if (wcslen(base) + wcslen(extra) >= MAX_PATH) {
    return FALSE;
  }

  return PathAppendW(base, extra);
}

/**
 * Obtains a uuid as a wide string.
 *
 * @param  outBuf
 *         A buffer of size MAX_PATH + 1 to store the result.
 * @return TRUE if successful
 */
BOOL GetUUIDString(LPWSTR outBuf) {
  UUID uuid;
  RPC_WSTR uuidString = nullptr;

  // Note: the return value of UuidCreate should always be RPC_S_OK on systems
  // after Win2K / Win2003 due to the network hardware address no longer being
  // used to create the UUID.
  if (UuidCreate(&uuid) != RPC_S_OK) {
    return FALSE;
  }
  if (UuidToStringW(&uuid, &uuidString) != RPC_S_OK) {
    return FALSE;
  }
  if (!uuidString) {
    return FALSE;
  }

  if (wcslen(reinterpret_cast<LPCWSTR>(uuidString)) > MAX_PATH) {
    return FALSE;
  }
  wcsncpy(outBuf, reinterpret_cast<LPCWSTR>(uuidString), MAX_PATH + 1);
  RpcStringFreeW(&uuidString);

  return TRUE;
}

/**
 * Build a temporary file path whose name component is a UUID.
 *
 * @param  basePath  The base directory path for the temp file
 * @param  prefix    Optional prefix for the beginning of the file name
 * @param  tmpPath   Output full path, with the base directory and the file
 * name. Must already have been allocated with size >= MAX_PATH.
 * @return TRUE if tmpPath was successfully filled in, FALSE on errors
 */
BOOL GetUUIDTempFilePath(LPCWSTR basePath, LPCWSTR prefix, LPWSTR tmpPath) {
  WCHAR filename[MAX_PATH + 1] = {L"\0"};
  if (prefix) {
    if (wcslen(prefix) > MAX_PATH) {
      return FALSE;
    }
    wcsncpy(filename, prefix, MAX_PATH + 1);
  }

  WCHAR tmpFileNameString[MAX_PATH + 1] = {L"\0"};
  if (!GetUUIDString(tmpFileNameString)) {
    return FALSE;
  }

  size_t tmpFileNameStringLen = wcslen(tmpFileNameString);
  if (wcslen(filename) + tmpFileNameStringLen > MAX_PATH) {
    return FALSE;
  }
  wcsncat(filename, tmpFileNameString, tmpFileNameStringLen);

  size_t basePathLen = wcslen(basePath);
  if (basePathLen > MAX_PATH) {
    return FALSE;
  }
  // Use basePathLen + 1 so wcsncpy will add null termination and if a caller
  // doesn't allocate MAX_PATH + 1 for tmpPath this won't fail when there is
  // actually enough space allocated.
  wcsncpy(tmpPath, basePath, basePathLen + 1);
  if (!PathAppendSafe(tmpPath, filename)) {
    return FALSE;
  }

  return TRUE;
}
