/* 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 "prio.h"
#include "prsystem.h"

#include "nsIFile.h"
#include "nsComponentManagerUtils.h"
#include "nsString.h"
#include "nsDirectoryServiceDefs.h"
#include "nsDirectoryServiceUtils.h"
#include "nsPrintfCString.h"

#ifdef XP_WIN
#  include <aclapi.h>
#  include "mozilla/RandomNum.h"
#  include "nsILocalFileWin.h"
#  include "nsLocalFile.h"
#  include "nsWindowsHelpers.h"
#else
#  include <limits.h>
#endif

#include <algorithm>

#include "gtest/gtest.h"
#include "mozilla/gtest/MozAssertions.h"

#ifdef XP_WIN
using namespace mozilla;
bool gTestWithPrefix_Win = false;
#endif

static bool VerifyResult(nsresult aRV, const char* aMsg) {
  bool failed = NS_FAILED(aRV);
  EXPECT_FALSE(failed) << aMsg << " rv=" << std::hex << (unsigned int)aRV;
  return !failed;
}

#ifdef XP_WIN
static void SetUseDOSDevicePathSyntax(nsIFile* aFile) {
  if (gTestWithPrefix_Win) {
    nsresult rv;
    nsCOMPtr<nsILocalFileWin> winFile = do_QueryInterface(aFile, &rv);
    VerifyResult(rv, "Querying nsILocalFileWin");

    MOZ_RELEASE_ASSERT(winFile);
    winFile->SetUseDOSDevicePathSyntax(true);
  }
}

static auto GetSecurityInfoStructured(nsIFile* aFile) {
  nsAutoString pathStr;
  MOZ_RELEASE_ASSERT(NS_SUCCEEDED(aFile->GetTarget(pathStr)));

  PACL pDacl = nullptr;
  AutoFreeSecurityDescriptor secDesc;
  DWORD errCode = ::GetNamedSecurityInfoW(
      pathStr.getW(), SE_FILE_OBJECT,
      DACL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION |
          GROUP_SECURITY_INFORMATION,
      nullptr, nullptr, &pDacl, nullptr, getter_Transfers(secDesc));
  MOZ_RELEASE_ASSERT(errCode == ERROR_SUCCESS && pDacl);

  return std::make_tuple(std::move(pathStr), WrapNotNull(pDacl),
                         std::move(secDesc));
}

// SECURITY_SID_SIZE() is not defined in mingw headers
#  if !defined(__MINGW32__)
static void AddAcesForRandomSidToDir(nsIFile* aDir) {
  auto [dirPath, pDirDacl, secDesc] = GetSecurityInfoStructured(aDir);

  constexpr BYTE kSubAuthorityCount = 4;
  BYTE randomSidBuffer[SECURITY_SID_SIZE(4)];
  ASSERT_TRUE(
      GenerateRandomBytesFromOS(randomSidBuffer, sizeof(randomSidBuffer)));
  auto* randomSid = reinterpret_cast<SID*>(randomSidBuffer);
  randomSid->Revision = SID_REVISION;
  randomSid->SubAuthorityCount = kSubAuthorityCount;
  randomSid->IdentifierAuthority = SECURITY_NULL_SID_AUTHORITY;
  ASSERT_TRUE(::IsValidSid(randomSid));

  EXPLICIT_ACCESS_W newAccess[2];
  newAccess[0].grfAccessMode = GRANT_ACCESS;
  newAccess[0].grfAccessPermissions = GENERIC_READ;
  newAccess[0].grfInheritance = SUB_OBJECTS_ONLY_INHERIT;
  ::BuildTrusteeWithSidW(&newAccess[0].Trustee, randomSid);
  newAccess[1].grfAccessMode = DENY_ACCESS;
  newAccess[1].grfAccessPermissions = GENERIC_WRITE;
  newAccess[1].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
  ::BuildTrusteeWithSidW(&newAccess[1].Trustee, randomSid);
  UniquePtr<ACL, LocalFreeDeleter> newDacl;
  ASSERT_EQ(::SetEntriesInAclW(std::size(newAccess), newAccess, pDirDacl,
                               getter_Transfers(newDacl)),
            (ULONG)ERROR_SUCCESS);

  ASSERT_EQ(::SetNamedSecurityInfoW(dirPath.get(), SE_FILE_OBJECT,
                                    DACL_SECURITY_INFORMATION, nullptr, nullptr,
                                    newDacl.get(), nullptr),
            (ULONG)ERROR_SUCCESS);
}
#  endif
#endif

static already_AddRefed<nsIFile> NewFile(nsIFile* aBase) {
  nsresult rv;
  nsCOMPtr<nsIFile> file;
  rv = NS_NewLocalFileWithFile(aBase, getter_AddRefs(file));
  VerifyResult(rv, "NS_NewLocalFileWithFile");

#ifdef XP_WIN
  SetUseDOSDevicePathSyntax(file);
#endif

  return file.forget();
}

template <typename char_type>
static nsTString<char_type> FixName(const char_type* aName) {
  nsTString<char_type> name;
  for (uint32_t i = 0; aName[i]; ++i) {
    char_type ch = aName[i];
    // PR_GetPathSeparator returns the wrong value on Mac so don't use it
#if defined(XP_WIN)
    if (ch == '/') {
      ch = '\\';
    }
#endif
    name.Append(ch);
  }
  return name;
}

// Test nsIFile::AppendNative, verifying that aName is not a valid file name
static bool TestInvalidFileName(nsIFile* aBase, const char* aName) {
  nsCOMPtr<nsIFile> file = NewFile(aBase);
  if (!file) return false;

  nsCString name = FixName(aName);
  nsresult rv = file->AppendNative(name);
  if (NS_SUCCEEDED(rv)) {
    EXPECT_NS_FAILED(rv) << "AppendNative with invalid filename " << name.get();
    return false;
  }

  return true;
}

// Test nsIFile::Create, verifying that the file exists and did not exist
// before, and leaving it there for future tests
static bool TestCreate(nsIFile* aBase, const char* aName, int32_t aType,
                       int32_t aPerm, nsIFile** aNewFile = nullptr) {
  nsCOMPtr<nsIFile> file = NewFile(aBase);
  if (!file) return false;

  nsCString name = FixName(aName);
  nsresult rv = file->AppendNative(name);
  if (!VerifyResult(rv, "AppendNative")) return false;

  bool exists;
  rv = file->Exists(&exists);
  if (!VerifyResult(rv, "Exists (before)")) return false;
  EXPECT_FALSE(exists) << "File " << name.get() << " already exists";
  if (exists) {
    return false;
  }

  rv = file->Create(aType, aPerm);
  if (!VerifyResult(rv, "Create")) return false;

  rv = file->Exists(&exists);
  if (!VerifyResult(rv, "Exists (after)")) return false;
  EXPECT_TRUE(exists) << "File " << name.get() << " was not created";
  if (!exists) {
    return false;
  }

  if (aNewFile) {
    file.forget(aNewFile);
  }

  return true;
}

// Test nsIFile::CreateUnique, verifying that the new file exists and if it
// existed before, the new file has a different name. The new file is left in
// place.
static bool TestCreateUnique(nsIFile* aBase, const char* aName, int32_t aType,
                             int32_t aPerm) {
  nsCOMPtr<nsIFile> file = NewFile(aBase);
  if (!file) return false;

  nsCString name = FixName(aName);
  nsresult rv = file->AppendNative(name);
  if (!VerifyResult(rv, "AppendNative")) return false;

  bool existsBefore;
  rv = file->Exists(&existsBefore);
  if (!VerifyResult(rv, "Exists (before)")) return false;

  rv = file->CreateUnique(aType, aPerm);
  if (!VerifyResult(rv, "Create")) return false;

  bool existsAfter;
  rv = file->Exists(&existsAfter);
  if (!VerifyResult(rv, "Exists (after)")) return false;
  EXPECT_TRUE(existsAfter) << "File " << name.get() << " was not created";
  if (!existsAfter) {
    return false;
  }

  if (existsBefore) {
    nsAutoCString leafName;
    rv = file->GetNativeLeafName(leafName);
    if (!VerifyResult(rv, "GetNativeLeafName")) return false;
    EXPECT_FALSE(leafName.Equals(name))
        << "File " << name.get() << " was not given a new name by CreateUnique";
    if (leafName.Equals(name)) {
      return false;
    }
  }

  return true;
}

// Test nsIFile::OpenNSPRFileDesc with DELETE_ON_CLOSE, verifying that the file
// exists and did not exist before, and leaving it there for future tests
static bool TestDeleteOnClose(nsIFile* aBase, const char* aName, int32_t aFlags,
                              int32_t aPerm) {
  nsCOMPtr<nsIFile> file = NewFile(aBase);
  if (!file) return false;

  nsCString name = FixName(aName);
  nsresult rv = file->AppendNative(name);
  if (!VerifyResult(rv, "AppendNative")) return false;

  bool exists;
  rv = file->Exists(&exists);
  if (!VerifyResult(rv, "Exists (before)")) return false;
  EXPECT_FALSE(exists) << "File " << name.get() << " already exists";
  if (exists) {
    return false;
  }

  PRFileDesc* fileDesc;
  rv = file->OpenNSPRFileDesc(aFlags | nsIFile::DELETE_ON_CLOSE, aPerm,
                              &fileDesc);
  if (!VerifyResult(rv, "OpenNSPRFileDesc")) return false;
  PRStatus status = PR_Close(fileDesc);
  EXPECT_EQ(status, PR_SUCCESS)
      << "File " << name.get() << " could not be closed";
  if (status != PR_SUCCESS) {
    return false;
  }

  rv = file->Exists(&exists);
  if (!VerifyResult(rv, "Exists (after)")) return false;
  EXPECT_FALSE(exists) << "File " << name.get() << " was not removed on close";
  if (exists) {
    return false;
  }

  return true;
}

// Test nsIFile::Remove, verifying that the file does not exist and did before
static bool TestRemove(nsIFile* aBase, const char* aName, bool aRecursive,
                       uint32_t aExpectedRemoveCount = 1) {
  nsCOMPtr<nsIFile> file = NewFile(aBase);
  if (!file) return false;

  nsCString name = FixName(aName);
  nsresult rv = file->AppendNative(name);
  if (!VerifyResult(rv, "AppendNative")) return false;

  bool exists;
  rv = file->Exists(&exists);
  if (!VerifyResult(rv, "Exists (before)")) return false;
  EXPECT_TRUE(exists);
  if (!exists) {
    return false;
  }

  uint32_t removeCount = 0;
  rv = file->Remove(aRecursive, &removeCount);
  if (!VerifyResult(rv, "Remove")) return false;
  EXPECT_EQ(removeCount, aExpectedRemoveCount) << "Removal count was wrong";

  rv = file->Exists(&exists);
  if (!VerifyResult(rv, "Exists (after)")) return false;
  EXPECT_FALSE(exists) << "File " << name.get() << " was not removed";
  if (exists) {
    return false;
  }

  return true;
}

// Test nsIFile::MoveToNative, verifying that the file did not exist at the new
// location before and does afterward, and that it does not exist at the old
// location anymore
static bool TestMove(nsIFile* aBase, nsIFile* aDestDir, const char* aName,
                     const char* aNewName) {
  nsCOMPtr<nsIFile> file = NewFile(aBase);
  if (!file) return false;

  nsCString name = FixName(aName);
  nsresult rv = file->AppendNative(name);
  if (!VerifyResult(rv, "AppendNative")) return false;

  bool exists;
  rv = file->Exists(&exists);
  if (!VerifyResult(rv, "Exists (before)")) return false;
  EXPECT_TRUE(exists);
  if (!exists) {
    return false;
  }

  nsCOMPtr<nsIFile> newFile = NewFile(file);
  nsCString newName = FixName(aNewName);
  rv = newFile->MoveToNative(aDestDir, newName);
  if (!VerifyResult(rv, "MoveToNative")) return false;

  rv = file->Exists(&exists);
  if (!VerifyResult(rv, "Exists (after)")) return false;
  EXPECT_FALSE(exists) << "File " << name.get() << " was not moved";
  if (exists) {
    return false;
  }

  file = NewFile(aDestDir);
  if (!file) return false;
  rv = file->AppendNative(newName);
  if (!VerifyResult(rv, "AppendNative")) return false;
  bool equal;
  rv = file->Equals(newFile, &equal);
  if (!VerifyResult(rv, "Equals")) return false;
  EXPECT_TRUE(equal) << "File object was not updated to destination";
  if (!equal) {
    return false;
  }

  rv = file->Exists(&exists);
  if (!VerifyResult(rv, "Exists (new after)")) return false;
  EXPECT_TRUE(exists) << "Destination file " << newName.get()
                      << " was not created";
  if (!exists) {
    return false;
  }

#ifdef XP_WIN
  // Ensure ACE inheritance is correct.
  auto [childPathStr, childDacl, childSecDesc] =
      GetSecurityInfoStructured(file);
  EXPECT_FALSE(childSecDesc->Control & SE_DACL_PROTECTED)
      << "SE_DACL_PROTECTED bit should not be set on the security descriptor";
  bool isDir = false;
  EXPECT_NS_SUCCEEDED(file->IsDirectory(&isDir));
  EXPECT_TRUE(nsLocalFile::ChildAclMatchesAclInheritedFromParent(
      childDacl, isDir, childSecDesc, aDestDir))
      << newName.get() << " ACL, does not match destination dir";
#endif

  return true;
}

// Test nsIFile::CopyToNative, verifying that the file did not exist at the new
// location before and does afterward, and that it does exist at the old
// location too
static bool TestCopy(nsIFile* aBase, nsIFile* aDestDir, const char* aName,
                     const char* aNewName) {
  nsCOMPtr<nsIFile> file = NewFile(aBase);
  if (!file) return false;

  nsCString name = FixName(aName);
  nsresult rv = file->AppendNative(name);
  if (!VerifyResult(rv, "AppendNative")) return false;

  bool exists;
  rv = file->Exists(&exists);
  if (!VerifyResult(rv, "Exists (before)")) return false;
  EXPECT_TRUE(exists);
  if (!exists) {
    return false;
  }

  nsCOMPtr<nsIFile> newFile = NewFile(file);
  nsCString newName = FixName(aNewName);
  rv = newFile->CopyToNative(aDestDir, newName);
  if (!VerifyResult(rv, "MoveToNative")) return false;
  bool equal;
  rv = file->Equals(newFile, &equal);
  if (!VerifyResult(rv, "Equals")) return false;
  EXPECT_TRUE(equal) << "File object updated unexpectedly";
  if (!equal) {
    return false;
  }

  rv = file->Exists(&exists);
  if (!VerifyResult(rv, "Exists (after)")) return false;
  EXPECT_TRUE(exists) << "File " << name.get() << " was removed";
  if (!exists) {
    return false;
  }

  file = NewFile(aDestDir);
  if (!file) return false;
  rv = file->AppendNative(newName);
  if (!VerifyResult(rv, "AppendNative")) return false;

  rv = file->Exists(&exists);
  if (!VerifyResult(rv, "Exists (new after)")) return false;
  EXPECT_TRUE(exists) << "Destination file " << newName.get()
                      << " was not created";
  if (!exists) {
    return false;
  }

  return true;
}

// Test nsIFile::GetParent
static bool TestParent(nsIFile* aBase, nsIFile* aStart) {
  nsCOMPtr<nsIFile> file = NewFile(aStart);
  if (!file) return false;

  nsCOMPtr<nsIFile> parent;
  nsresult rv = file->GetParent(getter_AddRefs(parent));
  VerifyResult(rv, "GetParent");

  bool equal;
  rv = parent->Equals(aBase, &equal);
  VerifyResult(rv, "Equals");
  EXPECT_TRUE(equal) << "Incorrect parent";
  if (!equal) {
    return false;
  }

  return true;
}

// Test nsIFile::Contains
static bool TestContains(nsIFile* aBase) {
  nsCOMPtr<nsIFile> sub = NewFile(aBase);
  if (!sub) return false;
  nsresult rv = sub->AppendNative(nsDependentCString("sub"));
  if (!VerifyResult(rv, "AppendNative sub")) return false;

  nsCOMPtr<nsIFile> subSlashSub = NewFile(sub);
  if (!subSlashSub) return false;
  rv = subSlashSub->AppendNative(nsDependentCString("sub"));
  if (!VerifyResult(rv, "AppendNative sub/sub")) return false;

  nsCOMPtr<nsIFile> subsub = NewFile(aBase);
  if (!subsub) return false;
  rv = subsub->AppendNative(nsDependentCString("subsub"));
  if (!VerifyResult(rv, "AppendNative subsub")) return false;

  bool contains;
  rv = sub->Contains(subsub, &contains);
  VerifyResult(rv, "sub contains subsub");
  EXPECT_FALSE(contains) << "sub contains subsub is false";

  rv = sub->Contains(sub, &contains);
  VerifyResult(rv, "sub contains sub (itself)");
  EXPECT_FALSE(contains) << "sub contains sub (itself) is false";

  rv = sub->Contains(subSlashSub, &contains);
  VerifyResult(rv, "sub contains sub/sub");
  EXPECT_TRUE(contains) << "sub contains sub/sub is true";

  // Regression test for bug 1975674: Contains null should not crash.
  rv = sub->Contains(nullptr, &contains);
  EXPECT_NS_FAILED(rv) << "Contains null fails";
  EXPECT_EQ(rv, NS_ERROR_INVALID_ARG) << "rv for contains(null)";

  return true;
}

// Test nsIFile::Normalize and native path setting/getting
static bool TestNormalizeNativePath(nsIFile* aBase, nsIFile* aStart) {
  nsCOMPtr<nsIFile> file = NewFile(aStart);
  if (!file) return false;

  auto path = file->NativePath();
#ifdef XP_WIN
  path.Append(FixName(u"/./.."));
  nsresult rv = file->InitWithPath(path);
  VerifyResult(rv, "InitWithPath");
#else
  path.Append(FixName("/./.."));
  nsresult rv = file->InitWithNativePath(path);
  VerifyResult(rv, "InitWithNativePath");
#endif
  rv = file->Normalize();
  VerifyResult(rv, "Normalize");
  path = file->NativePath();

  auto basePath = aBase->NativePath();
  VerifyResult(rv, "GetNativePath (base)");

  EXPECT_TRUE(path.Equals(basePath))
      << "Incorrect normalization: " << file->HumanReadablePath().get() << " - "
      << aBase->HumanReadablePath().get();
  if (!path.Equals(basePath)) {
    return false;
  }

  return true;
}

// Test nsIFile::GetDiskSpaceAvailable
static bool TestDiskSpaceAvailable(nsIFile* aBase) {
  nsCOMPtr<nsIFile> file = NewFile(aBase);
  if (!file) return false;

  int64_t diskSpaceAvailable = 0;
  nsresult rv = file->GetDiskSpaceAvailable(&diskSpaceAvailable);
  VerifyResult(rv, "GetDiskSpaceAvailable");

  EXPECT_GE(diskSpaceAvailable, 0);

  return true;
}

// Test nsIFile::GetDiskCapacity
static bool TestDiskCapacity(nsIFile* aBase) {
  nsCOMPtr<nsIFile> file = NewFile(aBase);
  if (!file) return false;

  int64_t diskCapacity = 0;
  nsresult rv = file->GetDiskCapacity(&diskCapacity);
  VerifyResult(rv, "GetDiskCapacity");

  EXPECT_GE(diskCapacity, 0);

  return true;
}

static void SetupAndTestFunctions(const nsAString& aDirName,
                                  bool aTestCreateUnique, bool aTestNormalize) {
  nsCOMPtr<nsIFile> base;
  nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(base));
  ASSERT_TRUE(VerifyResult(rv, "Getting temp directory"));

#ifdef XP_WIN
  SetUseDOSDevicePathSyntax(base);
#endif

  rv = base->Append(aDirName);
  ASSERT_TRUE(
      VerifyResult(rv, nsPrintfCString("Appending %s to temp directory name",
                                       NS_ConvertUTF16toUTF8(aDirName).get())
                           .get()));

  // Remove the directory in case tests failed and left it behind.
  // don't check result since it might not be there
  base->Remove(true);

  // Now create the working directory we're going to use
  rv = base->Create(nsIFile::DIRECTORY_TYPE, 0700);
  ASSERT_TRUE(VerifyResult(rv, "Creating temp directory"));

  // Now we can safely normalize the path
  if (aTestNormalize) {
    rv = base->Normalize();
    ASSERT_TRUE(VerifyResult(rv, "Normalizing temp directory name"));
  }

  // Initialize subdir object for later use
  nsCOMPtr<nsIFile> subdir = NewFile(base);
  ASSERT_TRUE(subdir);

  rv = subdir->AppendNative(nsDependentCString("subdir"));
  ASSERT_TRUE(VerifyResult(rv, "Appending 'subdir' to test dir name"));

  // ---------------
  // End setup code.
  // ---------------

  // Test leafName
  nsString leafName;
  rv = base->GetLeafName(leafName);
  ASSERT_TRUE(VerifyResult(rv, "Getting leafName"));
  ASSERT_TRUE(leafName.Equals(aDirName));

  // Test path parsing
  ASSERT_TRUE(TestInvalidFileName(base, "a/b"));
  ASSERT_TRUE(TestParent(base, subdir));

  // Test nsIFile::Contains
  ASSERT_TRUE(TestContains(base));

  // Test file creation
  ASSERT_TRUE(TestCreate(base, "file.txt", nsIFile::NORMAL_FILE_TYPE, 0600));
  ASSERT_TRUE(TestRemove(base, "file.txt", false));

  // Test directory creation
  ASSERT_TRUE(TestCreate(base, "subdir", nsIFile::DIRECTORY_TYPE, 0700));

  // Test move and copy in the base directory
  ASSERT_TRUE(TestCreate(base, "file.txt", nsIFile::NORMAL_FILE_TYPE, 0600));
  ASSERT_TRUE(TestMove(base, base, "file.txt", "file2.txt"));
  ASSERT_TRUE(TestCopy(base, base, "file2.txt", "file3.txt"));

  // Test moving across directories
  ASSERT_TRUE(TestMove(base, subdir, "file2.txt", "file2.txt"));

  // Test moving across directories and renaming at the same time
  ASSERT_TRUE(TestMove(subdir, base, "file2.txt", "file4.txt"));

  // SECURITY_SID_SIZE() is not defined in mingw headers
#if defined(XP_WIN) && !defined(__MINGW32__)
  // On Windows if we move a file or directory to a directory on the same volume
  // where the inherited ACLs differ between the source and target dirs, then we
  // retain the ACEs from the original dir. Add inherited ACEs for random SIDs
  // to subdir to ensure we reset the inheritance to just the target directory.
  AddAcesForRandomSidToDir(subdir);
  ASSERT_TRUE(TestCreate(subdir, "file8.txt", nsIFile::NORMAL_FILE_TYPE, 0600));
  ASSERT_TRUE(TestMove(subdir, base, "file8.txt", "file8.txt"));

  // Test that dir moves also get the ACL reset when required.
  ASSERT_TRUE(TestCreate(subdir, "subdir2", nsIFile::DIRECTORY_TYPE, 0700));
  ASSERT_TRUE(TestMove(subdir, base, "subdir2", "subdir2"));

  // If we set the SE_DACL_PROTECTED bit on the file's security descriptor then
  // no ACEs will be inherited in the moved file or the new ACL we create to
  // compare it with. So we need an extra test to catch this case.
  nsCOMPtr<nsIFile> file9;
  ASSERT_TRUE(TestCreate(subdir, "file9.txt", nsIFile::NORMAL_FILE_TYPE, 0600,
                         getter_AddRefs(file9)));
  auto [file9Path, pFile9Dacl, f9sd] = GetSecurityInfoStructured(file9);
  ASSERT_EQ(::SetNamedSecurityInfoW(
                file9Path.get(), SE_FILE_OBJECT,
                DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
                nullptr, nullptr, pFile9Dacl.get(), nullptr),
            (ULONG)ERROR_SUCCESS);
  auto [f9p, f9d, file9SecDescAfter] = GetSecurityInfoStructured(file9);
  ASSERT_TRUE(file9SecDescAfter->Control & SE_DACL_PROTECTED);
  ASSERT_TRUE(TestMove(subdir, base, "file9.txt", "file9.txt"));
#endif

  // Test copying across directories
  ASSERT_TRUE(TestCopy(base, subdir, "file4.txt", "file5.txt"));

  if (aTestNormalize) {
    // Run normalization tests while the directory exists
    ASSERT_TRUE(TestNormalizeNativePath(base, subdir));
  }

  // Test recursive directory removal
  ASSERT_TRUE(TestRemove(base, "subdir", true, 2));

  if (aTestCreateUnique) {
    ASSERT_TRUE(TestCreateUnique(base, "foo", nsIFile::NORMAL_FILE_TYPE, 0600));
    ASSERT_TRUE(TestCreateUnique(base, "foo", nsIFile::NORMAL_FILE_TYPE, 0600));
    ASSERT_TRUE(
        TestCreateUnique(base, "bar.xx", nsIFile::DIRECTORY_TYPE, 0700));
    ASSERT_TRUE(
        TestCreateUnique(base, "bar.xx", nsIFile::DIRECTORY_TYPE, 0700));
  }

  ASSERT_TRUE(
      TestDeleteOnClose(base, "file7.txt", PR_RDWR | PR_CREATE_FILE, 0600));

  ASSERT_TRUE(TestDiskSpaceAvailable(base));
  ASSERT_TRUE(TestDiskCapacity(base));

  // Clean up temporary stuff
  rv = base->Remove(true);
  VerifyResult(rv, "Cleaning up temp directory");
}

TEST(TestFile, Unprefixed)
{
#ifdef XP_WIN
  gTestWithPrefix_Win = false;
#endif

  SetupAndTestFunctions(u"mozfiletests"_ns,
                        /* aTestCreateUnique */ true,
                        /* aTestNormalize */ true);

#ifdef XP_WIN
  gTestWithPrefix_Win = true;
#endif
}

// This simulates what QM_NewLocalFile does (NS_NewLocalFiles and then
// SetUseDOSDevicePathSyntax if it's on Windows for NewFile)
TEST(TestFile, PrefixedOnWin)
{
  SetupAndTestFunctions(u"mozfiletests"_ns,
                        /* aTestCreateUnique */ true,
                        /* aTestNormalize */ true);
}

TEST(TestFile, PrefixedOnWin_PathExceedsMaxPath)
{
  // We want to verify if the prefix would allow as to create a file with over
  // 260 char for its path. However, on Windows, the maximum length of filename
  // is 255. Given the base file path and we are going append some other file
  // to the current base file, let's assume the file path will exceed 260 so
  // that we are able to verify if the prefix works or not.
  nsString dirName;
  dirName.AssignLiteral("mozfiletests");
  for (uint32_t i = 255 - dirName.Length(); i > 0; --i) {
    dirName.AppendLiteral("a");
  }

  // Bypass the test for CreateUnique because there is a check for the max
  // length of the root on all platforms.
  SetupAndTestFunctions(dirName, /* aTestCreateUnique */ false,
                        /* aTestNormalize */ true);
}

TEST(TestFile, PrefixedOnWin_ComponentEndsWithPeriod)
{
  // Bypass the normalization for this because it would strip the trailing
  // period.
  SetupAndTestFunctions(u"mozfiletests."_ns,
                        /* aTestCreateUnique */ true,
                        /* aTestNormalize */ false);
}

TEST(TestFile, GetRelativePath)
{
  nsCOMPtr<nsIFile> base;
  ASSERT_NS_SUCCEEDED(
      NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(base)));

  nsCOMPtr<nsIFile> child;
  ASSERT_NS_SUCCEEDED(base->Clone(getter_AddRefs(child)));
  ASSERT_NS_SUCCEEDED(child->AppendNative("places.sqlite"_ns));

  nsAutoCString result;

  // Path without \\?\ prefix: child relative to base is just the filename.
  ASSERT_NS_SUCCEEDED(child->GetRelativePath(base, result));
  EXPECT_STREQ(result.get(), "places.sqlite");

#ifdef XP_WIN
  // GetRelativePath strips the \\?\ prefix before comparing, so paths with
  // and without the prefix must yield the same result.
  nsCOMPtr<nsIFile> prefixedBase;
  ASSERT_NS_SUCCEEDED(base->Clone(getter_AddRefs(prefixedBase)));
  nsCOMPtr<nsILocalFileWin> winPrefixedBase = do_QueryInterface(prefixedBase);
  ASSERT_TRUE(winPrefixedBase);
  winPrefixedBase->SetUseDOSDevicePathSyntax(true);

  nsCOMPtr<nsIFile> prefixedChild;
  ASSERT_NS_SUCCEEDED(child->Clone(getter_AddRefs(prefixedChild)));
  nsCOMPtr<nsILocalFileWin> winPrefixedChild = do_QueryInterface(prefixedChild);
  ASSERT_TRUE(winPrefixedChild);
  winPrefixedChild->SetUseDOSDevicePathSyntax(true);

  // Both paths with \\?\ prefix.
  ASSERT_NS_SUCCEEDED(prefixedChild->GetRelativePath(prefixedBase, result));
  EXPECT_STREQ(result.get(), "places.sqlite");

  // Prefixed child, unprefixed base.
  ASSERT_NS_SUCCEEDED(prefixedChild->GetRelativePath(base, result));
  EXPECT_STREQ(result.get(), "places.sqlite");

  // Unprefixed child, prefixed base.
  ASSERT_NS_SUCCEEDED(child->GetRelativePath(prefixedBase, result));
  EXPECT_STREQ(result.get(), "places.sqlite");
#endif
}

// These match the limits in nsLocalFileCommon.cpp's CreateUnique.
static constexpr size_t kMaxUniqueFilenameLength = 250;
#ifdef XP_WIN
static constexpr int32_t kMaxUniquePathLength = MAX_PATH - 6;
#else
static constexpr int32_t kMaxUniquePathLength = PATH_MAX - 6;
#endif

TEST(TestFile, CreateUnique_TruncatesLongFilename)
{
  nsCOMPtr<nsIFile> base;
  nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(base));
  ASSERT_NS_SUCCEEDED(rv);

  rv = base->AppendNative("cu_trunc_leaf_test"_ns);
  ASSERT_NS_SUCCEEDED(rv);
  base->Remove(true);
  rv = base->Create(nsIFile::DIRECTORY_TYPE, 0700);
  ASSERT_NS_SUCCEEDED(rv);

  // 260-char root + ".txt" = 264-char leaf, exceeds kMaxUniqueFilenameLength.
  // CreateUnique should truncate the root so the leaf fits.
  {
    nsCOMPtr<nsIFile> file = NewFile(base);
    ASSERT_TRUE(file);

    nsAutoCString name;
    for (int i = 0; i < 260; ++i) {
      name.Append('a');
    }
    name.AppendLiteral(".txt");

    rv = file->AppendNative(name);
    ASSERT_NS_SUCCEEDED(rv);

    rv = file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
    ASSERT_NS_SUCCEEDED(rv);

    bool exists;
    rv = file->Exists(&exists);
    ASSERT_NS_SUCCEEDED(rv);
    EXPECT_TRUE(exists);

    nsAutoCString leafName;
    rv = file->GetNativeLeafName(leafName);
    ASSERT_NS_SUCCEEDED(rv);
    EXPECT_LE(leafName.Length(), kMaxUniqueFilenameLength);
    EXPECT_TRUE(StringEndsWith(leafName, ".txt"_ns));
  }

  // 260-char leaf with no extension.
  {
    nsCOMPtr<nsIFile> file = NewFile(base);
    ASSERT_TRUE(file);

    nsAutoCString name;
    for (int i = 0; i < 260; ++i) {
      name.Append('b');
    }

    rv = file->AppendNative(name);
    ASSERT_NS_SUCCEEDED(rv);

    rv = file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
    ASSERT_NS_SUCCEEDED(rv);

    bool exists;
    rv = file->Exists(&exists);
    ASSERT_NS_SUCCEEDED(rv);
    EXPECT_TRUE(exists);

    nsAutoCString leafName;
    rv = file->GetNativeLeafName(leafName);
    ASSERT_NS_SUCCEEDED(rv);
    EXPECT_LE(leafName.Length(), kMaxUniqueFilenameLength);
  }

  base->Remove(true);
}

TEST(TestFile, CreateUnique_TruncatesLongPath)
{
  nsCOMPtr<nsIFile> base;
  nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(base));
  ASSERT_NS_SUCCEEDED(rv);

  rv = base->AppendNative("cu_trunc_path_test"_ns);
  ASSERT_NS_SUCCEEDED(rv);
  base->Remove(true);
  rv = base->Create(nsIFile::DIRECTORY_TYPE, 0700);
  ASSERT_NS_SUCCEEDED(rv);
  rv = base->Normalize();
  ASSERT_NS_SUCCEEDED(rv);

  // Build nested directories until the path is long enough that appending a
  // moderate leaf name (well under kMaxUniqueFilenameLength) pushes the total
  // past kMaxUniquePathLength.
  const int32_t kLeafLen = 104;  // 100-char root + ".dat"
  const int32_t kOvershoot = 50;

  nsCOMPtr<nsIFile> deepDir = NewFile(base);
  ASSERT_TRUE(deepDir);

#ifdef XP_WIN
  nsAutoString dirPath;
  rv = deepDir->GetPath(dirPath);
#else
  nsAutoCString dirPath;
  rv = deepDir->GetNativePath(dirPath);
#endif
  ASSERT_NS_SUCCEEDED(rv);

  int32_t targetDirLen = kMaxUniquePathLength - kLeafLen - 1 + kOvershoot;

  while (static_cast<int32_t>(dirPath.Length()) < targetDirLen) {
    int32_t remaining = targetDirLen - dirPath.Length() - 1;
    int32_t nameLen = std::min(remaining, 200);
    if (nameLen < 1) break;

    nsAutoCString dirName;
    for (int32_t i = 0; i < nameLen; ++i) {
      dirName.Append('d');
    }

    rv = deepDir->AppendNative(dirName);
    ASSERT_NS_SUCCEEDED(rv);
    rv = deepDir->Create(nsIFile::DIRECTORY_TYPE, 0700);
    ASSERT_NS_SUCCEEDED(rv);

#ifdef XP_WIN
    rv = deepDir->GetPath(dirPath);
#else
    rv = deepDir->GetNativePath(dirPath);
#endif
    ASSERT_NS_SUCCEEDED(rv);
  }

  // The leaf name is short enough for kMaxUniqueFilenameLength, but the total
  // path would exceed kMaxUniquePathLength.
  {
    nsCOMPtr<nsIFile> file = NewFile(deepDir);
    ASSERT_TRUE(file);

    nsAutoCString name;
    for (int i = 0; i < 100; ++i) {
      name.Append('f');
    }
    name.AppendLiteral(".dat");

    rv = file->AppendNative(name);
    ASSERT_NS_SUCCEEDED(rv);

#ifdef XP_WIN
    nsAutoString pathBefore;
    rv = file->GetPath(pathBefore);
#else
    nsAutoCString pathBefore;
    rv = file->GetNativePath(pathBefore);
#endif
    ASSERT_NS_SUCCEEDED(rv);
    ASSERT_GT(static_cast<int32_t>(pathBefore.Length()), kMaxUniquePathLength)
        << "Test setup: path should exceed the limit before CreateUnique";

    rv = file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
    ASSERT_NS_SUCCEEDED(rv);

    bool exists;
    rv = file->Exists(&exists);
    ASSERT_NS_SUCCEEDED(rv);
    EXPECT_TRUE(exists);

#ifdef XP_WIN
    nsAutoString pathAfter;
    rv = file->GetPath(pathAfter);
#else
    nsAutoCString pathAfter;
    rv = file->GetNativePath(pathAfter);
#endif
    ASSERT_NS_SUCCEEDED(rv);
    EXPECT_LE(static_cast<int32_t>(pathAfter.Length()), kMaxUniquePathLength);

    nsAutoCString leafName;
    rv = file->GetNativeLeafName(leafName);
    ASSERT_NS_SUCCEEDED(rv);
    EXPECT_TRUE(StringEndsWith(leafName, ".dat"_ns));
    EXPECT_LT(leafName.Length(), static_cast<size_t>(kLeafLen));
  }

  base->Remove(true);
}
