chromium/content/browser/font_unique_name_lookup/font_unique_name_lookup_android_unittest.cc

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

#include "content/browser/font_unique_name_lookup/font_unique_name_lookup_android.h"

#include <functional>
#include <memory>

#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/functional/function_ref.h"
#include "base/strings/string_util.h"
#include "content/browser/font_unique_name_lookup/name_table_ffi.rs.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/font_unique_name_lookup/font_table_matcher.h"

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<base::FilePath> AndroidFontFilesList() {
  std::vector<base::FilePath> 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);
      }
    }
  }
  return font_files;
}

std::vector<base::FilePath> SplitFontFilesList(
    const std::vector<base::FilePath> 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<base::FilePath>(start_copy, end_copy);
}

void TruncateFileToLength(const base::FilePath& file_path,
                          int64_t truncated_length) {
  base::File file_to_truncate(
      file_path, base::File::FLAG_OPEN | base::File::Flags::FLAG_WRITE);

  ASSERT_TRUE(file_to_truncate.IsValid());
  ASSERT_TRUE(file_to_truncate.SetLength(truncated_length));
}

}  // 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_->DuplicateMemoryRegion().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_->DuplicateMemoryRegion().Map());
  ASSERT_GT(matcher_after_load.AvailableFonts(), 0u);
}

TEST_F(FontUniqueNameLookupTest, TestHandleFailedRead) {
  ASSERT_FALSE(base::PathExists(
      font_unique_name_lookup_->TableCacheFilePathForTesting()));
  ASSERT_FALSE(font_unique_name_lookup_->LoadFromFile());
  ASSERT_TRUE(font_unique_name_lookup_->UpdateTable());
  base::ReadOnlySharedMemoryMapping mapping =
      font_unique_name_lookup_->DuplicateMemoryRegion().Map();
  blink::FontTableMatcher matcher(mapping);

  // AOSP Android Kitkat has 81 fonts, the Kitkat bot seems to have 74,
  // Marshmallow has 149, Oreo 247. There are other variants that are built
  // with fewer fonts however. Be safer and assume 10 maximum.
  ASSERT_GT(matcher.AvailableFonts(), 10u);
  ASSERT_TRUE(font_unique_name_lookup_->PersistToFile());
  ASSERT_TRUE(base::PathExists(
      font_unique_name_lookup_->TableCacheFilePathForTesting()));
  int64_t file_size;
  ASSERT_TRUE(base::GetFileSize(
      font_unique_name_lookup_->TableCacheFilePathForTesting(), &file_size));
  // For 10 fonts, assume we have at least 256 bytes of data, it's
  // around 30k in practice on Kitkat with 81 fonts.
  ASSERT_GT(file_size, 256);
  ASSERT_TRUE(font_unique_name_lookup_->LoadFromFile());

  // For each truncated size, reading must fail, otherwise we successfully read
  // a truncated protobuf.
  for (int64_t truncated_size = file_size - 1; truncated_size >= 0;
       truncated_size -= file_size) {
    TruncateFileToLength(
        font_unique_name_lookup_->TableCacheFilePathForTesting(),
        truncated_size);
    ASSERT_FALSE(font_unique_name_lookup_->LoadFromFile());
  }
}

TEST_F(FontUniqueNameLookupTest, TestMatchPostScriptName) {
  if (base::android::BuildInfo::GetInstance()->sdk_int() >=
      base::android::SdkVersion::SDK_VERSION_S) {
    // TODO(crbug.com/40203471): Fonts identified by
    // kRobotoCondensedBoldItalicNames do not seem to be available on Android
    // 12, SDK level 31, Android S.
    return;
  }
  ASSERT_TRUE(font_unique_name_lookup_->UpdateTable());
  blink::FontTableMatcher matcher(
      font_unique_name_lookup_->DuplicateMemoryRegion().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_->DuplicateMemoryRegion().Map());
  std::vector<std::string> ttc_postscript_names = {
      "NotoSansCJKjp-Regular",     "NotoSansCJKkr-Regular",
      "NotoSansCJKsc-Regular",     "NotoSansCJKtc-Regular",
      "NotoSansMonoCJKjp-Regular", "NotoSansMonoCJKkr-Regular",
      "NotoSansMonoCJKsc-Regular", "NotoSansMonoCJKtc-Regular",
  };
  // In Android 11 the font file contains addition HK variants as part of the
  // TrueType collection.
  if (base::android::BuildInfo::GetInstance()->sdk_int() >=
      base::android::SdkVersion::SDK_VERSION_R) {
    ttc_postscript_names = std::vector<std::string>(
        {"NotoSansCJKjp-Regular", "NotoSansCJKkr-Regular",
         "NotoSansCJKsc-Regular", "NotoSansCJKtc-Regular",
         "NotoSansCJKhk-Regular", "NotoSansMonoCJKjp-Regular",
         "NotoSansMonoCJKkr-Regular", "NotoSansMonoCJKsc-Regular",
         "NotoSansMonoCJKtc-Regular", "NotoSansMonoCJKhk-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) {
  if (base::android::BuildInfo::GetInstance()->sdk_int() >=
      base::android::SdkVersion::SDK_VERSION_S) {
    // TODO(crbug.com/40203471): Fonts identified by
    // kRobotoCondensedBoldItalicNames do not seem to be available on Android
    // 12, SDK level 31, Android S.
    return;
  }
  ASSERT_TRUE(font_unique_name_lookup_->UpdateTable());
  blink::FontTableMatcher matcher(
      font_unique_name_lookup_->DuplicateMemoryRegion().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);
}

TEST_F(FontUniqueNameLookupTest, DontMatchOtherNames) {
  ASSERT_TRUE(font_unique_name_lookup_->UpdateTable());
  blink::FontTableMatcher matcher(
      font_unique_name_lookup_->DuplicateMemoryRegion().Map());
  // Name id 9 is the designer field, which we must not match against.
  auto match_result = matcher.MatchName("Christian Robertson");
  ASSERT_FALSE(match_result);
  // Name id 13 contains the license, which we also must not match.
  match_result =
      matcher.MatchName("Licensed under the Apache License, Version 2.0");
  ASSERT_FALSE(match_result);
}

namespace {
int64_t GetOffsetFirstTable(base::File& font_file) {
  std::vector<uint8_t> bytes;
  bytes.resize(font_file.GetLength());
  CHECK(font_file.ReadAndCheck(0, bytes));
  rust::Slice<const uint8_t> bytes_slice(bytes.data(), bytes.size());
  return name_table_access::offset_first_table(bytes_slice);
}

}  // namespace

// Creates a temp directory and copies Android font files to this
// directory. Provides two methods to inject faults into the font files 1)
// ZeroOutHeader removes the font's or font collection's header
// 2) ZeroAfterHeader 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 ZeroHeader() {
    ForEachCopiedFontFile([](base::File& font_file) {
      const size_t offset_first_table = GetOffsetFirstTable(font_file);

      std::vector<uint8_t> zeroes;
      zeroes.resize(offset_first_table, 0);
      CHECK(font_file.WriteAndCheck(0, zeroes));
    });
  }

  // Overwrite the data in the font file with zeroes from after the table
  // records until the end of the file.
  void ZeroAfterHeader() {
    ForEachCopiedFontFile([](base::File& font_file) {
      const size_t offset_first_table = GetOffsetFirstTable(font_file);
      std::vector<uint8_t> zeroes;
      zeroes.resize(font_file.GetLength() - offset_first_table, 0);
      CHECK(font_file.WriteAndCheck(offset_first_table, zeroes));
    });
  }

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

 private:
  void ForEachCopiedFontFile(
      base::FunctionRef<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<base::FilePath> 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);
      }
    }
  }
  base::ScopedTempDir temp_dir_;
  std::vector<base::FilePath> 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_.ZeroAfterHeader();
  ASSERT_TRUE(font_unique_name_lookup_->UpdateTable());
  blink::FontTableMatcher matcher_after_update(
      font_unique_name_lookup_->DuplicateMemoryRegion().Map());
  ASSERT_EQ(matcher_after_update.AvailableFonts(), 0u);
}

TEST_F(FaultInjectingFontUniqueNameLookupTest, TestZeroedHeader) {
  font_file_corruptor_.ZeroHeader();
  ASSERT_TRUE(font_unique_name_lookup_->UpdateTable());
  blink::FontTableMatcher matcher_after_update(
      font_unique_name_lookup_->DuplicateMemoryRegion().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_->DuplicateMemoryRegion().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_->DuplicateMemoryRegion().Map());
  ASSERT_GT(matcher_initial.AvailableFonts(), 0u);
  ASSERT_TRUE(matcher_initial.FontListIsDisjointFrom(matcher_second_half));
}

}  // namespace content