/* 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 <windows.h>
#include <winnetwk.h>

#include "mozilla/FileUtilsWin.h"
#include "nsCRTGlue.h"

#include "gtest/gtest.h"

class DriveMapping {
 public:
  explicit DriveMapping(const nsAString& aRemoteUNCPath);
  ~DriveMapping();

  bool Init();
  bool ChangeDriveLetter();
  wchar_t GetDriveLetter() { return mDriveLetter; }

 private:
  bool DoMapping();
  void Disconnect(wchar_t aDriveLetter);

  wchar_t mDriveLetter;
  nsString mRemoteUNCPath;
};

DriveMapping::DriveMapping(const nsAString& aRemoteUNCPath)
    : mDriveLetter(0), mRemoteUNCPath(aRemoteUNCPath) {}

bool DriveMapping::Init() {
  if (mDriveLetter) {
    return false;
  }
  return DoMapping();
}

bool DriveMapping::DoMapping() {
  wchar_t drvTemplate[] = L" :";
  NETRESOURCEW netRes = {0};
  netRes.dwType = RESOURCETYPE_DISK;
  netRes.lpLocalName = drvTemplate;
  netRes.lpRemoteName =
      reinterpret_cast<wchar_t*>(mRemoteUNCPath.BeginWriting());
  wchar_t driveLetter = L'D';
  DWORD result = NO_ERROR;
  do {
    drvTemplate[0] = driveLetter;
    result = WNetAddConnection2W(&netRes, nullptr, nullptr, CONNECT_TEMPORARY);
  } while (result == ERROR_ALREADY_ASSIGNED && ++driveLetter <= L'Z');
  if (result != NO_ERROR) {
    return false;
  }
  mDriveLetter = driveLetter;
  return true;
}

bool DriveMapping::ChangeDriveLetter() {
  wchar_t prevDriveLetter = mDriveLetter;
  bool result = DoMapping();
  MOZ_RELEASE_ASSERT(mDriveLetter != prevDriveLetter);
  if (result && prevDriveLetter) {
    Disconnect(prevDriveLetter);
  }
  return result;
}

void DriveMapping::Disconnect(wchar_t aDriveLetter) {
  wchar_t drvTemplate[] = {aDriveLetter, L':', L'\0'};
  DWORD result = WNetCancelConnection2W(drvTemplate, 0, TRUE);
  MOZ_RELEASE_ASSERT(result == NO_ERROR);
}

DriveMapping::~DriveMapping() {
  if (mDriveLetter) {
    Disconnect(mDriveLetter);
  }
}

bool DriveToNtPath(const wchar_t aDriveLetter, nsString& aNtPath) {
  const wchar_t drvTpl[] = {aDriveLetter, L':', L'\0'};
  aNtPath.SetLength(MAX_PATH);
  DWORD pathLen;
  while (true) {
    pathLen = QueryDosDeviceW(
        drvTpl, reinterpret_cast<wchar_t*>(aNtPath.BeginWriting()),
        aNtPath.Length());
    if (pathLen || GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
      break;
    }
    aNtPath.SetLength(aNtPath.Length() * 2);
  }
  if (!pathLen) {
    return false;
  }
  // aNtPath contains embedded NULLs, so we need to figure out the real length
  // via wcslen.
  aNtPath.SetLength(NS_strlen(aNtPath.get()));
  return true;
}

bool TestNtPathToDosPath(const wchar_t* aNtPath,
                         const wchar_t* aExpectedDosPath) {
  nsAutoString output;
  bool result = mozilla::NtPathToDosPath(nsDependentString(aNtPath), output);
  return result && output == reinterpret_cast<const nsAString::char_type*>(
                                 aExpectedDosPath);
}

TEST(NtPathToDosPath, Tests)
{
  nsAutoString cDrive;
  ASSERT_TRUE(DriveToNtPath(L'C', cDrive));

  // empty string
  EXPECT_TRUE(TestNtPathToDosPath(L"", L""));

  // non-existent device, must fail
  EXPECT_FALSE(
      TestNtPathToDosPath(L"\\Device\\ThisDeviceDoesNotExist\\Foo", nullptr));

  // base case
  nsAutoString testPath(cDrive);
  testPath.Append(L"\\Program Files");
  EXPECT_TRUE(TestNtPathToDosPath(testPath.get(), L"C:\\Program Files"));

  // short filename
  nsAutoString ntShortName(cDrive);
  ntShortName.Append(L"\\progra~1");
  EXPECT_TRUE(TestNtPathToDosPath(ntShortName.get(), L"C:\\Program Files"));

  // drive letters as symbolic links (NtCreateFile uses these)
  EXPECT_TRUE(TestNtPathToDosPath(L"\\??\\C:\\Foo", L"C:\\Foo"));

  // other symbolic links (should fail)
  EXPECT_FALSE(TestNtPathToDosPath(L"\\??\\MountPointManager", nullptr));

  // socket (should fail)
  EXPECT_FALSE(TestNtPathToDosPath(L"\\Device\\Afd\\Endpoint", nullptr));

  // UNC path (using MUP)
  EXPECT_TRUE(TestNtPathToDosPath(L"\\Device\\Mup\\127.0.0.1\\C$",
                                  L"\\\\127.0.0.1\\C$"));

  // UNC path (using LanmanRedirector)
  EXPECT_TRUE(TestNtPathToDosPath(L"\\Device\\LanmanRedirector\\127.0.0.1\\C$",
                                  L"\\\\127.0.0.1\\C$"));

  DriveMapping drvMapping(u"\\\\127.0.0.1\\C$"_ns);
  // Only run these tests if we were able to map; some machines don't have perms
  if (drvMapping.Init()) {
    wchar_t expected[] = L" :\\";
    expected[0] = drvMapping.GetDriveLetter();
    nsAutoString networkPath;
    ASSERT_TRUE(DriveToNtPath(drvMapping.GetDriveLetter(), networkPath));

    networkPath += u"\\";
    EXPECT_TRUE(TestNtPathToDosPath(networkPath.get(), expected));

    // NtPathToDosPath must correctly handle paths whose drive letter mapping
    // has changed. We need to test this because the APIs called by
    // NtPathToDosPath return different info if this has happened.
    ASSERT_TRUE(drvMapping.ChangeDriveLetter());

    expected[0] = drvMapping.GetDriveLetter();
    ASSERT_TRUE(DriveToNtPath(drvMapping.GetDriveLetter(), networkPath));

    networkPath += u"\\";
    EXPECT_TRUE(TestNtPathToDosPath(networkPath.get(), expected));
  }
}
