/*
* Copyright (c) 2003-2024 Rony Shapiro <ronys@pwsafe.org>.
* All rights reserved. Use of the code is allowed under the
* Artistic License 2.0 terms, as specified in the LICENSE file
* distributed with this code, or available from
* http://www.opensource.org/licenses/artistic-license-2.0.php
*/

/**
 * \file Windows-specific implementation of file.h
 */

#ifndef __WX__
#include <afx.h>
#endif

#include <Windows.h>
#include <LMCONS.H> // for UNLEN definition
#include <shellapi.h>
#include <shlwapi.h>

#include <io.h>
#include <sys/stat.h>
#include <fstream>

#include "../typedefs.h"
#include "../file.h"
#include "../dir.h"
#include "../env.h"
#include "../debug.h"

#include "../../core/core.h"

const TCHAR pws_os::PathSeparator = _T('\\');

bool pws_os::FileExists(const stringT &filename)
{
  struct _stat statbuf;
  int status;

  status = _tstat(filename.c_str(), &statbuf);
  return (status == 0 && (statbuf.st_mode & _S_IFREG)); // stat() succeeded and we're a regular file (not a directory)
}

bool pws_os::FileExists(const stringT &filename, bool &bReadOnly)
{
  bool retval;
  bReadOnly = false;

  retval = pws_os::FileExists(filename); // false if not found or if a directory
  if (retval) {
    bReadOnly = (_taccess(filename.c_str(), W_OK) != 0);
  }
  return retval;
}

void pws_os::AddDrive(stringT &path)
{
  // Adds a drive letter to the path if not there, unless
  // empty string  or it's a UNC path (\\host\sharename...)
  using namespace pws_os;
  if (path.empty())
    return;
  if (!(path[0] == '\\' && path[1] == '\\')) {
    stringT drive, dir, file, ext;
    splitpath(path, drive, dir, file, ext);

    if (drive.empty()) {
      const stringT exedir = getexecdir();
      stringT exeDrive, dummy;
      splitpath(exedir, exeDrive, dummy, dummy, dummy);
      path = makepath(exeDrive, dir, file, ext);
    }
  }
}

static bool FileOP(const stringT &src, const stringT &dst,
                   UINT wFunc)
{
  // wrapper for SHFileOperation() for moving or copying from src to dst
  // create any intervening directories as necessary & automatically
  TCHAR szSource[_MAX_PATH + 1];
  TCHAR szDestination[_MAX_PATH + 1];

  // SHFileOperation() acts very oddly if files are missing a drive
  // (eg, renames to pwsafeN.psa instead of pwsafe.ibak)
  
  stringT srcD(src), dstD(dst);
  pws_os::AddDrive(srcD);
  pws_os::AddDrive(dstD);

  if (srcD.length() >= _MAX_PATH || dstD.length() >= _MAX_PATH)
    return false;

  const TCHAR *lpsz_current = srcD.c_str();
  const TCHAR *lpsz_new = dstD.c_str();

  _tcscpy_s(szSource, _MAX_PATH, lpsz_current);
  _tcscpy_s(szDestination, _MAX_PATH, lpsz_new);

  // Must end with double NULL
  szSource[srcD.length() + 1] = TCHAR('\0');
  szDestination[dstD.length() + 1] = TCHAR('\0');

  SHFILEOPSTRUCT sfop;
  memset(&sfop, 0, sizeof(SHFILEOPSTRUCT));
  sfop.hwnd = GetActiveWindow();
  sfop.wFunc = wFunc;
  sfop.pFrom = szSource;
  sfop.pTo = szDestination;
  sfop.fFlags = FOF_NOCONFIRMATION | FOF_NOCONFIRMMKDIR | FOF_SILENT | FOF_NOERRORUI | FOF_NOCOPYSECURITYATTRIBS;

  int retval = SHFileOperation(&sfop);
  return (retval == 0);
}

bool pws_os::RenameFile(const stringT &oldname, const stringT &newname)
{
  DeleteFile(newname.c_str()); // otherwise rename may fail if newname exists
  return FileOP(oldname, newname, FO_MOVE); // FO_RENAME fails across directories
}

extern bool pws_os::CopyAFile(const stringT &from, const stringT &to)
{
  return FileOP(from, to, FO_COPY);
}

bool pws_os::DeleteAFile(const stringT &filename)
{
  return DeleteFile(filename.c_str()) == TRUE;
}

void pws_os::FindFiles(const stringT &filter, std::vector<stringT> &res)
{
  res.clear();
  _tfinddata_t fileinfo;
  intptr_t handle = _tfindfirst(filter.c_str(), &fileinfo);
  if (handle == -1)
    return;

  do {
    res.push_back(LPCTSTR(fileinfo.name));
  } while (_tfindnext(handle, &fileinfo) == 0);

  _findclose(handle);
}

/*
* The file lock/unlock functions were first implemented (in 2.08)
* with Posix semantics (using open(_O_CREATE|_O_EXCL) to detect
* an existing lock.
* This fails to check liveness of the locker process, specifically,
* if a user just turns off her PC, the lock file will remain.
* So, I'm  re-implementing using the Win32 API, whose semantics
* supposedly protect against this scenario.
* Thanks to Frank (xformer) for discussion on the subject.
*/

static stringT GetLockFileName(const stringT &filename)
{
  ASSERT(!filename.empty());
  // derive lock filename from filename
  /*
   * If the filename ends with .cfg, then we add .plk to it, e.g., foo.cfg.plk
   * otherwise we replace the suffix with .plk, e.g., foo.psafe3 -> foo.plk
   * This fixes a bug while maintaining bwd compat.
   */
  stringT retval;
  if (filename.length() > 4 && filename.substr(filename.length() - 4) == _T(".cfg"))
    retval = filename;
  else
    retval = filename.substr(0, filename.find_last_of(TCHAR('.')));
  retval += _T(".plk");
  return retval;
}

static void GetLocker(const stringT &lock_filename, stringT &locker)
{
  locker = _T("Unable to determine locker");
  // read locker data ("user@machine:nnnnnnnn") from file
  TCHAR lockerStr[UNLEN + MAX_COMPUTERNAME_LENGTH + 11];
  // flags here counter (my) intuition, but see
  // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/fileio/base/creating_and_opening_files.asp
  HANDLE h2 = ::CreateFile(lock_filename.c_str(),
                           GENERIC_READ,
                           FILE_SHARE_WRITE,
                           NULL,
                           OPEN_EXISTING,
                           (FILE_ATTRIBUTE_NORMAL |
                            // (Lockheed Martin) Secure Coding  11-14-2007
                            SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION),
                           NULL);
  // Make sure it's a file and not a pipe.  (Lockheed Martin) Secure Coding  11-14-2007
  if (h2 != INVALID_HANDLE_VALUE) {
    if (::GetFileType( h2 ) != FILE_TYPE_DISK) {
      ::CloseHandle( h2 );
      h2 = INVALID_HANDLE_VALUE;
    }
  }
  // End of Change.  (Lockheed Martin) Secure Coding  11-14-2007
 
  if (h2 != INVALID_HANDLE_VALUE) {
    DWORD bytesRead;
    (void)::ReadFile(h2, lockerStr, sizeof(lockerStr) - 1,
                     &bytesRead, nullptr);
    CloseHandle(h2);
    if (bytesRead > 0) {
      lockerStr[bytesRead / sizeof(TCHAR)] = TCHAR('\0');
      locker = lockerStr;
    } // read info from lock file
  }
}

bool pws_os::LockFile(const stringT &filename, stringT &locker, 
                      HANDLE &lockFileHandle)
{
  const stringT lock_filename = GetLockFileName(filename);
  stringT s_locker;
  const stringT user = pws_os::getusername();
  const stringT host = pws_os::gethostname();
  const stringT pid = pws_os::getprocessid();

  if (lockFileHandle != INVALID_HANDLE_VALUE) {
    if (FileExists(filename)) { // Can this be false?
      GetLocker(filename, locker);
      return false;
    }
    // here if file not found but handle appears valid - unlock and then lock.
    pws_os::UnlockFile(filename, lockFileHandle);
  }

  // Since ::CreateFile can't create directories, we need to check it exists
  // first and, if not, try and create it.
  // This is primarily for the config directory in the local APPDATA directory
  // but will also be called for the database lock file - and since the database
  // is already there, it is a bit of a redundant check but easier than coding
  // for every different situation.
  stringT sDrive, sDir, sName, sExt;
  pws_os::splitpath(lock_filename, sDrive, sDir, sName, sExt);
  stringT sNewDir = sDrive + sDir;
  DWORD dwAttrib = GetFileAttributes(sNewDir.c_str());
  DWORD dwerr(0);
  if (dwAttrib == INVALID_FILE_ATTRIBUTES)
    dwerr = GetLastError();

  BOOL brc(TRUE);
  if (dwerr == ERROR_FILE_NOT_FOUND || 
      ((dwAttrib != INVALID_FILE_ATTRIBUTES) &&
      !(dwAttrib & FILE_ATTRIBUTE_DIRECTORY))) {
    SECURITY_ATTRIBUTES secatt = {0};
    secatt.nLength = sizeof(secatt);
    brc = ::CreateDirectory(sNewDir.c_str(), &secatt);
  }

  // Obviously, if we can't create the directory - don't bother trying to
  // create the lock file!
  if (brc) {
    lockFileHandle = ::CreateFile(lock_filename.c_str(),
                                  GENERIC_WRITE,
                                  FILE_SHARE_READ,
                                  NULL,
                                  CREATE_ALWAYS, // rely on share to fail if exists!
                                  FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH | 
                                  // (Lockheed Martin) Secure Coding  11-14-2007
                                  SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION,
                                  NULL);

    // Make sure it's a file and not a pipe.  (Lockheed Martin) Secure Coding  11-14-2007
    if (lockFileHandle != INVALID_HANDLE_VALUE) {
      if (::GetFileType( lockFileHandle ) != FILE_TYPE_DISK) {
        ::CloseHandle( lockFileHandle );
        lockFileHandle = INVALID_HANDLE_VALUE;
      }
    }
    // End of Change.  (Lockheed Martin) Secure Coding  11-14-2007
  }

  if (lockFileHandle == INVALID_HANDLE_VALUE) {
    DWORD error = GetLastError();
    switch (error) {
    case ERROR_SHARING_VIOLATION: // already open by a live process
      GetLocker(lock_filename, s_locker);
      locker = s_locker.c_str();
      break;
    default:
    {
      // Give detailed error message, if possible
      LPTSTR lpMsgBuf = nullptr;
      if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
                        nullptr,
                        error,
                        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                        (LPTSTR)&lpMsgBuf,
                        0, nullptr) != 0) {
        locker = lpMsgBuf;
        LocalFree(lpMsgBuf);
      } else { // should never happen!
        LoadAString(locker, IDSC_NOLOCKACCESS); // better than nothing
      }
      break;
    }
    } // switch (error)
    return false;
  } else { // valid filehandle, write our info
    DWORD numWrit, sumWrit;
    BOOL write_status;
    write_status = ::WriteFile(lockFileHandle,
                               user.c_str(), (DWORD)(user.length() * sizeof(TCHAR)),
                               &sumWrit, nullptr);
    write_status &= ::WriteFile(lockFileHandle,
                                _T("@"), (DWORD)(sizeof(TCHAR)),
                                &numWrit, nullptr);
    sumWrit += numWrit;
    write_status &= ::WriteFile(lockFileHandle,
                                host.c_str(), (DWORD)(host.length() * sizeof(TCHAR)),
                                &numWrit, nullptr);
    sumWrit += numWrit;
    write_status &= ::WriteFile(lockFileHandle,
                                _T(":"), (DWORD)(sizeof(TCHAR)),
                                &numWrit, nullptr);
    sumWrit += numWrit;
    write_status &= ::WriteFile(lockFileHandle,
                                pid.c_str(), (DWORD)(pid.length() * sizeof(TCHAR)),
                                &numWrit, nullptr);
    sumWrit += numWrit;
    ASSERT(sumWrit > 0);
    return (write_status == TRUE);
  }
}

void pws_os::UnlockFile(const stringT &filename,
                        HANDLE &lockFileHandle)
{
  const stringT user = pws_os::getusername();
  const stringT host = pws_os::gethostname();
  const stringT pid = pws_os::getprocessid();

  // Use Win32 API for locking - supposedly better at
  // detecting dead locking processes
  if (lockFileHandle != INVALID_HANDLE_VALUE) {
    stringT locker;
    const stringT lock_filename = GetLockFileName(filename);
    const stringT cs_me = user + _T("@") + host + _T(":") + pid;
    GetLocker(lock_filename, locker);

    CloseHandle(lockFileHandle);
    lockFileHandle = INVALID_HANDLE_VALUE;
    DeleteFile(lock_filename.c_str());
  }
}

bool pws_os::IsLockedFile(const stringT &filename)
{
  const stringT lock_filename = GetLockFileName(filename);
  // under this scheme, we need to actually try to open the file to determine
  // if it's locked.
  HANDLE h = CreateFile(lock_filename.c_str(),
                        GENERIC_WRITE,
                        FILE_SHARE_READ,
                        NULL,
                        OPEN_EXISTING, // don't create one!
                        FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH |
                        // (Lockheed Martin) Secure Coding  11-14-2007
                        SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION,
                        NULL);
 
  // Make sure it's a file and not a pipe.  (Lockheed Martin) Secure Coding  11-14-2007
  if (h != INVALID_HANDLE_VALUE) {
    if (::GetFileType( h ) != FILE_TYPE_DISK) {
      ::CloseHandle( h );
      h = INVALID_HANDLE_VALUE;
    }
  }
  // End of Change.  (Lockheed Martin) Secure Coding  11-14-2007
 
  if (h == INVALID_HANDLE_VALUE) {
    DWORD error = GetLastError();
    if (error == ERROR_SHARING_VIOLATION)
      return true;
    else
      return false; // couldn't open it, probably doesn't exist.
  } else {
    CloseHandle(h); // here if exists but lockable.
    return false;
  }
}

std::FILE *pws_os::FOpen(const stringT &filename, const TCHAR *mode)
{
  std::FILE *fd = NULL;
  if (!filename.empty()) {
	  _tfopen_s(&fd, filename.c_str(), mode);
  } else { // set to stdin/stdout, depending on mode[0] (r/w/a)
	  fd = mode[0] == L'r' ? stdin : stdout;
  }
  return fd;
}

int pws_os::FClose(std::FILE *fd, const bool &bIsWrite)
{
  if (fd != NULL) {
    if (bIsWrite) {
      // Flush the data buffers
      // fflush returns 0 if the buffer was successfully flushed.
      // A return value of EOF indicates an error.
      int rc = fflush(fd);

      // Don't bother trying FlushFileBuffers if fflush failed
      if (rc == 0) {
        // Windows FlushFileBuffers == Linux fsync
        int ifileno = _fileno(fd);

        if (ifileno != INVALID_FILE_DESCRIPTOR) {
          intptr_t iosfhandle = _get_osfhandle(ifileno);

          if ((HANDLE)iosfhandle != INVALID_HANDLE_VALUE) {
            BOOL brc = FlushFileBuffers((HANDLE)iosfhandle);

            if (brc == FALSE) {
              pws_os::IssueError(_T("FlushFileBuffers on close of file on removable device"), false);
            }
          } // iosfhandle
        } // ifileno
      }  // fflush rc
    }

    // Now close file
    // fclose returns 0 if the stream is successfully closed or EOF to indicate an error.
    return fclose(fd);
  } else {
    return 0;
  }
}

size_t pws_os::fileLength(std::FILE *fp) {
  if (fp != nullptr) {
    auto pos = _ftelli64(fp);
    _fseeki64(fp, 0, SEEK_END);
    auto len = _ftelli64(fp);
    _fseeki64(fp, pos, SEEK_SET);
    return static_cast<size_t>(len);
  } else
    return 0;
}

bool pws_os::GetFileTimes(const stringT &filename,
      time_t &atime, time_t &ctime, time_t &mtime)
{
  struct _stati64 info;
  int rc = _wstati64(filename.c_str(), &info);
  if (rc == 0) {
    atime = info.st_atime;
    ctime = info.st_ctime;
    mtime = info.st_mtime;
    return true;
  } else {
    return false;
  }
}

void TimetToFileTime(time_t t, FILETIME *pft)
{
  LONGLONG ll = Int32x32To64(t, 10000000) + 116444736000000000;
  pft->dwLowDateTime = (DWORD)ll;
  pft->dwHighDateTime = ll >> 32;
}

bool pws_os::SetFileTimes(const stringT &filename,
  time_t ctime, time_t mtime, time_t atime)
{
  FILETIME fctime, fmtime, fatime;

  if (ctime == 0 && mtime == 0 && atime == 0)
    return true;  // Nothing to do!

  // Convert to file time format
  TimetToFileTime(ctime, &fctime);
  TimetToFileTime(mtime, &fmtime);
  TimetToFileTime(atime, &fatime);

  // Now set file times
  HANDLE hFile;
  hFile = CreateFile(filename.c_str(), FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ, NULL,
    OPEN_EXISTING, 0, NULL);

  if (hFile != INVALID_HANDLE_VALUE) {
    SetFileTime(hFile, ctime != 0 ? &fctime : nullptr, atime == 0 ? &fatime : nullptr, mtime != 0 ? &fmtime : nullptr);
    CloseHandle(hFile);
    return true;
  } else {
    return false;
  }
}
