// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/disk_cache/simple/simple_version_upgrade.h"

#include <stdint.h>
#include <string>

#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/format_macros.h"
#include "base/strings/stringprintf.h"
#include "net/base/net_errors.h"
#include "net/disk_cache/disk_cache.h"
#include "net/disk_cache/simple/simple_backend_version.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

// Same as |disk_cache::kSimpleInitialMagicNumber|.
const uint64_t kSimpleInitialMagicNumber = UINT64_C(0xfcfb6d1ba7725c30);

// The "fake index" file that cache backends use to distinguish whether the
// cache belongs to one backend or another.
const char kFakeIndexFileName[] = "index";

// Same as |SimpleIndexFile::kIndexDirectory|.
const char kIndexDirName[] = "index-dir";

// Same as |SimpleIndexFile::kIndexFileName|.
const char kIndexFileName[] = "the-real-index";

bool WriteFakeIndexFileV8(const base::FilePath& cache_path) {
  disk_cache::FakeIndexData data;
  data.version = 8;
  data.initial_magic_number = kSimpleInitialMagicNumber;
  data.zero = 0;
  data.zero2 = 0;
  const base::FilePath file_name = cache_path.AppendASCII("index");
  return base::WriteFile(file_name, base::byte_span_from_ref(data));
}

bool WriteFakeIndexFile(const base::FilePath& cache_path,
                        uint32_t version,
                        int encryption_status) {
  disk_cache::FakeIndexData data;
  data.version = version;
  data.initial_magic_number = kSimpleInitialMagicNumber;
  data.zero = 0;
  data.zero2 = 0;
  data.encryption_status = encryption_status;
  const base::FilePath file_name = cache_path.AppendASCII(kFakeIndexFileName);
  return base::WriteFile(file_name, base::byte_span_from_ref(data));
}

// Mock `BackendFileOperations` to expose and change the encryption field for
// testing.
class TestFileOperations : public disk_cache::TrivialFileOperations {
 public:
  explicit TestFileOperations(bool is_encrypted)
      : is_encrypted_(is_encrypted) {}

  bool IsEncrypted() const override { return is_encrypted_; }

 private:
  const bool is_encrypted_;
};

TEST(SimpleVersionUpgradeTest, FailsToMigrateBackwards) {
  base::ScopedTempDir cache_dir;
  ASSERT_TRUE(cache_dir.CreateUniqueTempDir());
  const base::FilePath cache_path = cache_dir.GetPath();

  disk_cache::FakeIndexData data;
  data.version = 100500;
  data.initial_magic_number = kSimpleInitialMagicNumber;
  data.zero = 0;
  data.zero2 = 0;
  const base::FilePath file_name = cache_path.AppendASCII(kFakeIndexFileName);
  ASSERT_TRUE(base::WriteFile(file_name, base::byte_span_from_ref(data)));
  disk_cache::TrivialFileOperations file_operations;
  EXPECT_EQ(disk_cache::SimpleCacheConsistencyResult::kVersionFromTheFuture,
            disk_cache::UpgradeSimpleCacheOnDisk(&file_operations,
                                                 cache_dir.GetPath()));
}

TEST(SimpleVersionUpgradeTest, ExperimentBacktoDefault) {
  base::ScopedTempDir cache_dir;
  ASSERT_TRUE(cache_dir.CreateUniqueTempDir());
  const base::FilePath cache_path = cache_dir.GetPath();

  disk_cache::FakeIndexData data;
  data.version = disk_cache::kSimpleVersion;
  data.initial_magic_number = kSimpleInitialMagicNumber;
  data.zero = 2;
  data.zero2 = 4;
  const base::FilePath file_name = cache_path.AppendASCII(kFakeIndexFileName);
  ASSERT_TRUE(base::WriteFile(file_name, base::byte_span_from_ref(data)));

  disk_cache::TrivialFileOperations file_operations;
  // The cache needs to transition from a deprecated experiment back to not
  // having one.
  EXPECT_EQ(disk_cache::SimpleCacheConsistencyResult::kBadZeroCheck,
            disk_cache::UpgradeSimpleCacheOnDisk(&file_operations,
                                                 cache_dir.GetPath()));
}

TEST(SimpleVersionUpgradeTest, WritesEncryptionStatusToFakeIndexFile) {
  base::ScopedTempDir cache_dir;
  ASSERT_TRUE(cache_dir.CreateUniqueTempDir());
  const base::FilePath cache_path = cache_dir.GetPath();
  const base::FilePath fake_index_path =
      cache_path.AppendASCII(kFakeIndexFileName);

  TestFileOperations file_operations(true);
  ASSERT_EQ(disk_cache::SimpleCacheConsistencyResult::kOKCreated,
            disk_cache::UpgradeSimpleCacheOnDisk(&file_operations, cache_path));

  disk_cache::FakeIndexData data;
  ASSERT_TRUE(base::ReadFile(fake_index_path, base::byte_span_from_ref(data)));
  EXPECT_EQ(1, data.encryption_status);
  EXPECT_EQ(disk_cache::kSimpleVersion, data.version);
}

TEST(SimpleVersionUpgradeTest, NoUpgradeNeeded) {
  base::ScopedTempDir cache_dir;
  ASSERT_TRUE(cache_dir.CreateUniqueTempDir());
  const base::FilePath cache_path = cache_dir.GetPath();

  // Create a fake index file with current version.
  ASSERT_TRUE(WriteFakeIndexFile(cache_path, disk_cache::kSimpleVersion, 0));

  disk_cache::TrivialFileOperations file_operations;
  EXPECT_EQ(disk_cache::SimpleCacheConsistencyResult::kOKNoUpgrade,
            disk_cache::UpgradeSimpleCacheOnDisk(&file_operations, cache_path));
}

TEST(SimpleVersionUpgradeTest, EncryptionStatusMismatch) {
  auto run_and_check = [](int cache_encrypted_status,
                          bool backend_is_encrypted) {
    base::ScopedTempDir cache_dir;
    ASSERT_TRUE(cache_dir.CreateUniqueTempDir());
    const base::FilePath cache_path = cache_dir.GetPath();

    // Create a fake index file with the specified encryption status.
    ASSERT_TRUE(WriteFakeIndexFile(cache_path, disk_cache::kSimpleVersion,
                                   cache_encrypted_status));

    // Simulate opening the cache with a backend of a different encryption
    // status.
    TestFileOperations file_operations(backend_is_encrypted);
    EXPECT_EQ(
        disk_cache::SimpleCacheConsistencyResult::kEncryptionStatusMismatch,
        disk_cache::UpgradeSimpleCacheOnDisk(&file_operations,
                                             cache_dir.GetPath()));
  };

  run_and_check(0, true);
  run_and_check(1, false);
}

TEST(SimpleVersionUpgradeTest, FakeIndexVersionGetsUpdated) {
  base::ScopedTempDir cache_dir;
  ASSERT_TRUE(cache_dir.CreateUniqueTempDir());
  const base::FilePath cache_path = cache_dir.GetPath();

  ASSERT_TRUE(WriteFakeIndexFileV8(cache_path));

  const std::string file_contents("incorrectly serialized data");
  const base::FilePath index_file = cache_path.AppendASCII(kIndexFileName);
  ASSERT_TRUE(base::WriteFile(index_file, file_contents));

  disk_cache::TrivialFileOperations file_operations;
  // Upgrade.
  ASSERT_EQ(disk_cache::SimpleCacheConsistencyResult::kOKUpgraded,
            disk_cache::UpgradeSimpleCacheOnDisk(&file_operations, cache_path));

  // Check that the version in the fake index file is updated.
  std::string new_fake_index_contents;
  ASSERT_TRUE(base::ReadFileToString(cache_path.AppendASCII(kFakeIndexFileName),
                                     &new_fake_index_contents));
  const disk_cache::FakeIndexData* fake_index_header;
  EXPECT_EQ(sizeof(*fake_index_header), new_fake_index_contents.size());
  // TODO(crbug.com/428945428): Fix unsafe uses of std::string::data().
  fake_index_header =
      UNSAFE_TODO(reinterpret_cast<const disk_cache::FakeIndexData*>(
          new_fake_index_contents.data()));
  EXPECT_EQ(disk_cache::kSimpleVersion, fake_index_header->version);
  EXPECT_EQ(kSimpleInitialMagicNumber, fake_index_header->initial_magic_number);
}

TEST(SimpleVersionUpgradeTest, DeleteAllIndexFilesWhenCacheIsEmpty) {
  const std::string kCorruptData("corrupt");

  base::ScopedTempDir cache_dir;
  ASSERT_TRUE(cache_dir.CreateUniqueTempDir());
  const base::FilePath cache_path = cache_dir.GetPath();

  const base::FilePath fake_index = cache_path.AppendASCII(kFakeIndexFileName);
  ASSERT_TRUE(base::WriteFile(fake_index, kCorruptData));

  const base::FilePath index_path = cache_path.AppendASCII(kIndexDirName);
  ASSERT_TRUE(base::CreateDirectory(index_path));

  const base::FilePath index = index_path.AppendASCII(kIndexFileName);
  ASSERT_TRUE(base::WriteFile(index, kCorruptData));

  EXPECT_TRUE(disk_cache::DeleteIndexFilesIfCacheIsEmpty(cache_path));
  EXPECT_TRUE(base::PathExists(cache_path));
  EXPECT_TRUE(base::IsDirectoryEmpty(cache_path));
}

TEST(SimpleVersionUpgradeTest, DoesNotDeleteIndexFilesWhenCacheIsNotEmpty) {
  const std::string kCorruptData("corrupt");

  base::ScopedTempDir cache_dir;
  ASSERT_TRUE(cache_dir.CreateUniqueTempDir());
  const base::FilePath cache_path = cache_dir.GetPath();

  const base::FilePath fake_index = cache_path.AppendASCII(kFakeIndexFileName);
  ASSERT_TRUE(base::WriteFile(fake_index, kCorruptData));

  const base::FilePath index_path = cache_path.AppendASCII(kIndexDirName);
  ASSERT_TRUE(base::CreateDirectory(index_path));

  const base::FilePath index = index_path.AppendASCII(kIndexFileName);
  ASSERT_TRUE(base::WriteFile(index, kCorruptData));

  const base::FilePath entry_file = cache_path.AppendASCII("01234567_0");
  ASSERT_TRUE(base::WriteFile(entry_file, kCorruptData));

  EXPECT_FALSE(disk_cache::DeleteIndexFilesIfCacheIsEmpty(cache_path));
  EXPECT_TRUE(base::PathExists(cache_path));
  EXPECT_FALSE(base::IsDirectoryEmpty(cache_path));
  EXPECT_TRUE(base::PathExists(fake_index));
  EXPECT_TRUE(base::PathExists(index_path));
  EXPECT_TRUE(base::PathExists(index));
  EXPECT_TRUE(base::PathExists(entry_file));
}

}  // namespace
