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

#include "testing/gtest/include/gtest/gtest.h"

#include "base/android/build_info.h"
#include "base/files/file.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/stl_util.h"
#include "base/strings/string_util.h"
#include "content/browser/font_unique_name_lookup/font_unique_name_lookup.h"
#include "third_party/blink/public/common/font_unique_name_lookup/font_table_matcher.h"

#include <functional>
#include <memory>

namespace {

static const char* const kAndroidFontPaths[] = {"/system/fonts",
                                                "/vendor/fonts"};

// Full font name, postscript name, filename.
static const char* const kRobotoCondensedBoldItalicNames[] = {
    "Roboto Condensed Bold Italic", "RobotoCondensed-BoldItalic",
    "RobotoCondensed-BoldItalic.ttf"};

std::vector<std::string> AndroidFontFilesList() {
  std::vector<std::string> font_files;
  for (const char* font_dir_path : kAndroidFontPaths) {
    base::FileEnumerator files_enumerator(
        base::MakeAbsoluteFilePath(base::FilePath(font_dir_path)), true,
        base::FileEnumerator::FILES);
    for (base::FilePath name = files_enumerator.Next(); !name.empty();
         name = files_enumerator.Next()) {
      if (name.Extension() == ".ttf" || name.Extension() == ".ttc" ||
          name.Extension() == ".otf") {
        font_files.push_back(name.value());
      }
    }
  }
  return font_files;
}

std::vector<std::string> SplitFontFilesList(
    const std::vector<std::string> font_files,
    bool return_second_half) {
  CHECK_GT(font_files.size(), 2u);
  auto start_copy = font_files.begin();
  auto end_copy = font_files.begin() + (font_files.size() / 2);
  if (return_second_half) {
    start_copy = end_copy;
    end_copy = font_files.end();
  }
  return std::vector<std::string>(start_copy, end_copy);
}

enum class TruncateLength { TruncateToZero, TruncateHalf };

void TruncateFile(const base::FilePath& file_path,
                  TruncateLength truncate_length) {
  base::File file_to_truncate(
      file_path, base::File::FLAG_OPEN | base::File::Flags::FLAG_WRITE);
  size_t truncate_to = truncate_length == TruncateLength::TruncateHalf
                           ? file_to_truncate.GetLength() / 2
                           : 0;
  file_to_truncate.SetLength(truncate_to);
}

}  // namespace

namespace content {

class FontUniqueNameLookupTest : public ::testing::Test {
 protected:
  void SetUp() override {
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
    font_unique_name_lookup_ =
        std::make_unique<FontUniqueNameLookup>(temp_dir_.GetPath());
  }

  base::ScopedTempDir temp_dir_;
  std::unique_ptr<FontUniqueNameLookup> font_unique_name_lookup_;
};

TEST_F(FontUniqueNameLookupTest, TestBuildLookup) {
  ASSERT_TRUE(font_unique_name_lookup_->UpdateTable());
  base::ReadOnlySharedMemoryMapping mapping =
      font_unique_name_lookup_->GetUniqueNameTableAsSharedMemoryRegion().Map();
  blink::FontTableMatcher matcher(mapping);
  ASSERT_GT(matcher.AvailableFonts(), 0u);
  ASSERT_TRUE(font_unique_name_lookup_->PersistToFile());
  ASSERT_TRUE(font_unique_name_lookup_->LoadFromFile());
  blink::FontTableMatcher matcher_after_load(
      font_unique_name_lookup_->GetUniqueNameTableAsSharedMemoryRegion().Map());
  ASSERT_GT(matcher_after_load.AvailableFonts(), 0u);
}

TEST_F(FontUniqueNameLookupTest, TestHandleFailedRead) {
  base::DeleteFile(font_unique_name_lookup_->TableCacheFilePathForTesting(),
                   false);
  ASSERT_FALSE(font_unique_name_lookup_->LoadFromFile());
  ASSERT_FALSE(font_unique_name_lookup_->IsValid());
  ASSERT_TRUE(font_unique_name_lookup_->UpdateTable());
  ASSERT_TRUE(font_unique_name_lookup_->IsValid());
  base::ReadOnlySharedMemoryMapping mapping =
      font_unique_name_lookup_->GetUniqueNameTableAsSharedMemoryRegion().Map();
  blink::FontTableMatcher matcher(mapping);
  ASSERT_GT(matcher.AvailableFonts(), 0u);
  ASSERT_TRUE(font_unique_name_lookup_->PersistToFile());
  ASSERT_TRUE(font_unique_name_lookup_->LoadFromFile());
  ASSERT_TRUE(font_unique_name_lookup_->IsValid());
  TruncateFile(font_unique_name_lookup_->TableCacheFilePathForTesting(),
               TruncateLength::TruncateHalf);
  ASSERT_FALSE(font_unique_name_lookup_->LoadFromFile());
  ASSERT_FALSE(font_unique_name_lookup_->IsValid());
  TruncateFile(font_unique_name_lookup_->TableCacheFilePathForTesting(),
               TruncateLength::TruncateToZero);
  ASSERT_FALSE(font_unique_name_lookup_->LoadFromFile());
  ASSERT_FALSE(font_unique_name_lookup_->IsValid());
}

TEST_F(FontUniqueNameLookupTest, TestMatchPostScriptName) {
  ASSERT_TRUE(font_unique_name_lookup_->UpdateTable());
  blink::FontTableMatcher matcher(
      font_unique_name_lookup_->GetUniqueNameTableAsSharedMemoryRegion().Map());
  ASSERT_GT(matcher.AvailableFonts(), 0u);
  auto match_result = matcher.MatchName(kRobotoCondensedBoldItalicNames[1]);
  ASSERT_TRUE(match_result);
  ASSERT_TRUE(EndsWith(match_result->font_path,
                       kRobotoCondensedBoldItalicNames[2],
                       base::CompareCase::SENSITIVE));
  base::File found_file(base::FilePath(match_result->font_path),
                        base::File::FLAG_OPEN | base::File::FLAG_READ);
  ASSERT_TRUE(found_file.IsValid());
  ASSERT_EQ(match_result->ttc_index, 0u);
}

TEST_F(FontUniqueNameLookupTest, TestMatchPostScriptNameTtc) {
  if (base::android::BuildInfo::GetInstance()->sdk_int() <
      base::android::SdkVersion::SDK_VERSION_NOUGAT) {
    // Pre-Nougat Android does not contain any .ttc files as system fonts.
    return;
  }
  ASSERT_TRUE(font_unique_name_lookup_->UpdateTable());
  blink::FontTableMatcher matcher(
      font_unique_name_lookup_->GetUniqueNameTableAsSharedMemoryRegion().Map());
  std::vector<std::string> ttc_postscript_names = {
      "NotoSansCJKjp-Regular",     "NotoSansCJKkr-Regular",
      "NotoSansCJKsc-Regular",     "NotoSansCJKtc-Regular",
      "NotoSansMonoCJKjp-Regular", "NotoSansMonoCJKkr-Regular",
      "NotoSansMonoCJKsc-Regular", "NotoSansMonoCJKtc-Regular",
  };
  for (size_t i = 0; i < ttc_postscript_names.size(); ++i) {
    auto match_result = matcher.MatchName(ttc_postscript_names[i]);
    ASSERT_TRUE(match_result);
    ASSERT_TRUE(EndsWith(match_result->font_path, "NotoSansCJK-Regular.ttc",
                         base::CompareCase::SENSITIVE));
    base::File found_file(base::FilePath(match_result->font_path),
                          base::File::FLAG_OPEN | base::File::FLAG_READ);
    ASSERT_TRUE(found_file.IsValid());
    ASSERT_EQ(match_result->ttc_index, i);
  }
}

TEST_F(FontUniqueNameLookupTest, TestMatchFullFontName) {
  ASSERT_TRUE(font_unique_name_lookup_->UpdateTable());
  blink::FontTableMatcher matcher(
      font_unique_name_lookup_->GetUniqueNameTableAsSharedMemoryRegion().Map());
  auto match_result = matcher.MatchName(kRobotoCondensedBoldItalicNames[0]);
  ASSERT_TRUE(match_result);
  ASSERT_TRUE(EndsWith(match_result->font_path,
                       kRobotoCondensedBoldItalicNames[2],
                       base::CompareCase::SENSITIVE));
  base::File found_file(base::FilePath(match_result->font_path),
                        base::File::FLAG_OPEN | base::File::Flags::FLAG_READ);
  ASSERT_TRUE(found_file.IsValid());
  ASSERT_EQ(match_result->ttc_index, 0u);
}

namespace {
size_t GetNumTables(base::File& font_file) {
  font_file.Seek(base::File::FROM_BEGIN, 5);
  uint8_t num_tables_bytes[2] = {};
  font_file.ReadAtCurrentPos(reinterpret_cast<char*>(num_tables_bytes),
                             base::size(num_tables_bytes));
  uint16_t num_tables =
      static_cast<uint16_t>(num_tables_bytes[0] + (num_tables_bytes[1] << 8));
  return num_tables;
};

const size_t kOffsetTableRecords = 13;
const size_t kSizeOneTableRecord = 16;

}  // namespace

// Creates a temp directory and copies Android font files to this
// directory. Provides two methods to inject faults into the font files 1)
// ZeroOutTableRecords writes a sequence of 0 to where the font table offset
// should be stored in the font file. 2) ZeroAfterTableIndex writes 0 from after
// the table records until the end of the file.
class FontFileCorruptor {
 public:
  FontFileCorruptor() {
    CHECK(temp_dir_.CreateUniqueTempDir());
    CopyPlatformFilesToTempDir();
  }

  // Overwrite the list of table records with 0.
  void ZeroOutTableRecords() {
    ForEachCopiedFontFile([](base::File& font_file) {
      // Read number of font tables, then zero out the table record structure.
      // https://docs.microsoft.com/en-us/typography/opentype/spec/font-file
      size_t num_tables = GetNumTables(font_file);
      CHECK_GT(num_tables, 0u);
      char garbage[kSizeOneTableRecord] = {0};
      for (size_t i = 0; i < num_tables; ++i) {
        CHECK_EQ(static_cast<int>(kSizeOneTableRecord),
                 font_file.Write(kOffsetTableRecords + i * kSizeOneTableRecord,
                                 garbage, base::size(garbage)));
      }
    });
  }

  // Overwrite the data in the font file with zeroes from after the table
  // records until the end of the file.
  void ZeroAfterTableIndex() {
    ForEachCopiedFontFile([](base::File& font_file) {
      size_t num_tables = GetNumTables(font_file);
      CHECK_GT(num_tables, 0u);
      const size_t offset_after_table_records =
          kOffsetTableRecords + num_tables * kSizeOneTableRecord;
      std::vector<char> zeroes;
      zeroes.resize(font_file.GetLength() - offset_after_table_records);
      std::fill(zeroes.begin(), zeroes.end(), 0);
      CHECK_EQ(static_cast<int>(zeroes.size()),
               font_file.Write(offset_after_table_records, zeroes.data(),
                               zeroes.size()));
    });
  }

  // Get the list of filenames copied to the temporary directory.
  std::vector<std::string> GetFontFilesList() { return copied_files_; }

 private:
  void ForEachCopiedFontFile(std::function<void(base::File&)> manipulate_file) {
    for (const auto& filename : copied_files_) {
      base::File font_file(base::FilePath(filename),
                           base::File::FLAG_OPEN | base::File::FLAG_READ |
                               base::File::FLAG_WRITE);
      manipulate_file(font_file);
    }
  }

  void CopyPlatformFilesToTempDir() {
    std::vector<std::string> platform_files = AndroidFontFilesList();
    for (auto& font_file : platform_files) {
      base::FilePath source_path(font_file);
      base::FilePath destination_path(temp_dir_.GetPath());
      destination_path = destination_path.Append(source_path.BaseName());
      if (base::CopyFile(source_path, destination_path))
        copied_files_.push_back(destination_path.value());
    }
  }
  base::ScopedTempDir temp_dir_;
  std::vector<std::string> copied_files_;
};

class FaultInjectingFontUniqueNameLookupTest : public ::testing::Test {
 protected:
  void SetUp() override {
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
    font_unique_name_lookup_ =
        std::make_unique<FontUniqueNameLookup>(temp_dir_.GetPath());
    font_unique_name_lookup_->SetFontFilePathsForTesting(
        font_file_corruptor_.GetFontFilesList());
  }

  base::ScopedTempDir temp_dir_;
  FontFileCorruptor font_file_corruptor_;
  std::unique_ptr<FontUniqueNameLookup> font_unique_name_lookup_;
};

TEST_F(FaultInjectingFontUniqueNameLookupTest, TestZeroedTableContents) {
  font_file_corruptor_.ZeroAfterTableIndex();
  ASSERT_TRUE(font_unique_name_lookup_->UpdateTable());
  blink::FontTableMatcher matcher_after_update(
      font_unique_name_lookup_->GetUniqueNameTableAsSharedMemoryRegion().Map());
  ASSERT_EQ(matcher_after_update.AvailableFonts(), 0u);
}

TEST_F(FaultInjectingFontUniqueNameLookupTest, TestZeroedTableIndex) {
  font_file_corruptor_.ZeroOutTableRecords();
  ASSERT_TRUE(font_unique_name_lookup_->UpdateTable());
  blink::FontTableMatcher matcher_after_update(
      font_unique_name_lookup_->GetUniqueNameTableAsSharedMemoryRegion().Map());
  ASSERT_EQ(matcher_after_update.AvailableFonts(), 0u);
}

class FontUniqueNameLookupUpdateTest : public ::testing::Test {
 protected:
  void SetUp() override {
    ASSERT_TRUE(lookup_table_storage_dir.CreateUniqueTempDir());
    font_unique_name_lookup_ = std::make_unique<FontUniqueNameLookup>(
        lookup_table_storage_dir.GetPath());
    font_unique_name_lookup_->SetFontFilePathsForTesting(
        SplitFontFilesList(AndroidFontFilesList(), false));
    font_unique_name_lookup_->SetAndroidBuildFingerprintForTesting("A");
  }

  base::ScopedTempDir lookup_table_storage_dir;
  std::unique_ptr<FontUniqueNameLookup> font_unique_name_lookup_;
};

TEST_F(FontUniqueNameLookupUpdateTest, CompareSets) {
  ASSERT_TRUE(font_unique_name_lookup_->UpdateTable());
  blink::FontTableMatcher matcher_initial(
      font_unique_name_lookup_->GetUniqueNameTableAsSharedMemoryRegion().Map());
  ASSERT_GT(matcher_initial.AvailableFonts(), 0u);
  font_unique_name_lookup_->SetFontFilePathsForTesting(
      SplitFontFilesList(AndroidFontFilesList(), true));
  // Set the Android build fingerprint to something different from what it's set
  // to in the test's SetUp method to trigger re-indexing.
  font_unique_name_lookup_->SetAndroidBuildFingerprintForTesting("B");
  font_unique_name_lookup_->UpdateTableIfNeeded();
  blink::FontTableMatcher matcher_second_half(
      font_unique_name_lookup_->GetUniqueNameTableAsSharedMemoryRegion().Map());
  ASSERT_GT(matcher_initial.AvailableFonts(), 0u);
  ASSERT_TRUE(matcher_initial.FontListIsDisjointFrom(matcher_second_half));
}

}  // namespace content
