/* Copyright (c) 2023, Google Inc.
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */

#include "file_util.h"

#include <stdlib.h>

#if defined(OPENSSL_WINDOWS)
OPENSSL_MSVC_PRAGMA(warning(push, 3))
#include <windows.h>
OPENSSL_MSVC_PRAGMA(warning(pop))
#else
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#endif

#include <openssl/rand.h>

#include "test_util.h"


BSSL_NAMESPACE_BEGIN

#if defined(OPENSSL_WINDOWS)
static void PrintLastError(const char *s) {
  DWORD error = GetLastError();
  char *buffer;
  DWORD len = FormatMessageA(
      FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER, 0, error, 0,
      reinterpret_cast<char *>(&buffer), 0, nullptr);
  std::string msg = "unknown error";
  if (len > 0) {
    msg.assign(buffer, len);
    while (!msg.empty() && (msg.back() == '\r' || msg.back() == '\n')) {
      msg.resize(msg.size() - 1);
    }
  }
  LocalFree(buffer);
  fprintf(stderr, "%s: %s (0x%lx)\n", s, msg.c_str(), error);
}
#endif  // OPENSSL_WINDOWS

// GetTempDir returns the path to the temporary directory, or the empty string
// on error. On success, the result will include the directory separator.
static std::string GetTempDir() {
#if defined(OPENSSL_WINDOWS)
  char buf[MAX_PATH + 1];
  DWORD len = GetTempPathA(sizeof(buf), buf);
  return std::string(buf, len);
#else
  const char *tmpdir = getenv("TMPDIR");
  if (tmpdir != nullptr && *tmpdir != '\0') {
    std::string ret = tmpdir;
    if (ret.back() != '/') {
      ret.push_back('/');
    }
    return ret;
  }
#if defined(OPENSSL_ANDROID)
  return "/data/local/tmp/";
#else
  return "/tmp/";
#endif
#endif
}

bool SkipTempFileTests() {
#if defined(OPENSSL_ANDROID)
  // When running in an APK context, /data/local/tmp is unreadable. Android
  // versions before https://android-review.googlesource.com/c/1821337 do not
  // set TMPDIR to a suitable replacement.
  if (getenv("TMPDIR") == nullptr) {
    static bool should_skip = [] {
      TemporaryFile file;
      return !file.Init();
    }();
    if (should_skip) {
      fprintf(stderr, "Skipping tests with temporary files.\n");
      return true;
    }
  }
#endif
  return false;
}

TemporaryFile::~TemporaryFile() {
#if defined(OPENSSL_WINDOWS)
  if (!path_.empty() && !DeleteFileA(path_.c_str())) {
    PrintLastError("Could not delete file");
  }
#else
  if (!path_.empty() && unlink(path_.c_str()) != 0) {
    perror("Could not delete file");
  }
#endif
}

bool TemporaryFile::Init(bssl::Span<const uint8_t> content) {
  std::string temp_dir = GetTempDir();
  if (temp_dir.empty()) {
    return false;
  }

#if defined(OPENSSL_WINDOWS)
  char path[MAX_PATH];
  if (GetTempFileNameA(temp_dir.c_str(), "bssl",
                       /*uUnique=*/0, path) == 0) {
    PrintLastError("Could not create temporary");
    return false;
  }
  path_ = path;
#else
  std::string path = temp_dir + "bssl_tmp_file.XXXXXX";
  // TODO(davidben): Use |path.data()| when we require C++17.
  int fd = mkstemp(&path[0]);
  if (fd < 0) {
    perror("Could not create temporary file");
    return false;
  }
  close(fd);
  path_ = std::move(path);
#endif

  ScopedFILE file = Open("wb");
  if (file == nullptr) {
    perror("Could not open temporary file");
    return false;
  }
  if (!content.empty() &&
      fwrite(content.data(), content.size(), /*nitems=*/1, file.get()) != 1) {
    perror("Could not write temporary file");
    return false;
  }
  return true;
}

ScopedFILE TemporaryFile::Open(const char *mode) const {
  if (path_.empty()) {
    return nullptr;
  }
  return ScopedFILE(fopen(path_.c_str(), mode));
}

ScopedFD TemporaryFile::OpenFD(int flags) const {
  if (path_.empty()) {
    return ScopedFD();
  }
#if defined(OPENSSL_WINDOWS)
  return ScopedFD(_open(path_.c_str(), flags));
#else
  return ScopedFD(open(path_.c_str(), flags));
#endif
}

TemporaryDirectory::~TemporaryDirectory() {
  if (path_.empty()) {
    return;
  }

#if defined(OPENSSL_WINDOWS)
  for (const auto &file : files_) {
    if (!DeleteFileA(GetFilePath(file).c_str())) {
      PrintLastError("Could not delete file");
    }
  }
  if (!RemoveDirectoryA(path_.c_str())) {
    PrintLastError("Could not delete directory");
  }
#else
  for (const auto &file : files_) {
    if (unlink(GetFilePath(file).c_str()) != 0) {
      perror("Could not delete file");
    }
  }
  if (rmdir(path_.c_str()) != 0) {
    perror("Could not delete directory");
  }
#endif
}

bool TemporaryDirectory::Init() {
  std::string path = GetTempDir();
  if (path.empty()) {
    return false;
  }

#if defined(OPENSSL_WINDOWS)
  // For simplicity, just try the first candidate and assume the directory
  // doesn't exist. 128 bits of cryptographically secure randomness is plenty.
  uint8_t buf[16];
  RAND_bytes(buf, sizeof(buf));
  path += "bssl_tmp_dir." + EncodeHex(buf);
  if (!CreateDirectoryA(path.c_str(), /*lpSecurityAttributes=*/nullptr)) {
    perror("Could not make temporary directory");
    return false;
  }
#else
  path += "bssl_tmp_dir.XXXXXX";
  // TODO(davidben): Use |path.data()| when we require C++17.
  if (mkdtemp(&path[0]) == nullptr) {
    perror("Could not make temporary directory");
    return false;
  }
#endif

  path_ = std::move(path);
  return true;
}

bool TemporaryDirectory::AddFile(const std::string &filename,
                                 bssl::Span<const uint8_t> content) {
  if (path_.empty()) {
    return false;
  }

  ScopedFILE file(fopen(GetFilePath(filename).c_str(), "wb"));
  if (file == nullptr) {
    perror("Could not open temporary file");
    return false;
  }
  if (!content.empty() &&
      fwrite(content.data(), content.size(), /*nitems=*/1, file.get()) != 1) {
    perror("Could not write temporary file");
    return false;
  }

  files_.insert(filename);
  return true;
}

BSSL_NAMESPACE_END
