chromium/chrome/utility/safe_browsing/mac/hfs_unittest.cc

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

#include "chrome/utility/safe_browsing/mac/hfs.h"

#include <stddef.h>
#include <stdint.h>

#include <array>
#include <memory>
#include <string_view>

#include "base/containers/span.h"
#include "base/files/file.h"
#include "base/logging.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/utility/safe_browsing/mac/dmg_test_utils.h"
#include "chrome/utility/safe_browsing/mac/read_stream.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace safe_browsing {
namespace dmg {
namespace {

class HFSIteratorTest : public testing::Test {
 public:
  void GetTargetFiles(bool case_sensitive,
                      std::set<std::u16string>* files,
                      std::set<std::u16string>* dirs) {
    const auto kBaseFiles = std::to_array({
        u"first/second/third/fourth/fifth/random",
        u"first/second/third/fourth/Hello World",
        u"first/second/third/symlink-random",
        u"first/second/goat-output.txt",
        u"first/unicode_name",
        u"README.txt",
        u".metadata_never_index",
    });

    const auto kBaseDirs = std::to_array({
        u"first/second/third/fourth/fifth",
        u"first/second/third/fourth",
        u"first/second/third",
        u"first/second",
        u"first",
        u".Trashes",
    });

    const std::u16string dmg_name = u"SafeBrowsingDMG/";

    for (size_t i = 0; i < std::size(kBaseFiles); ++i)
      files->insert(dmg_name + kBaseFiles[i]);

    files->insert(dmg_name + u"first/second/" + u"Tĕsẗ 🐐 ");

    dirs->insert(dmg_name.substr(0, dmg_name.size() - 1));
    for (size_t i = 0; i < std::size(kBaseDirs); ++i)
      dirs->insert(dmg_name + kBaseDirs[i]);

    if (case_sensitive) {
      files->insert(u"SafeBrowsingDMG/first/second/third/fourth/hEllo wOrld");
    }
  }

  void TestTargetFiles(safe_browsing::dmg::HFSIterator* hfs_reader,
                       bool case_sensitive) {
    std::set<std::u16string> files, dirs;
    GetTargetFiles(case_sensitive, &files, &dirs);

    ASSERT_TRUE(hfs_reader->Open());
    while (hfs_reader->Next()) {
      std::u16string path = hfs_reader->GetPath();
      // Skip over .fseventsd files.
      if (path.find(u"SafeBrowsingDMG/.fseventsd") != std::u16string::npos) {
        continue;
      }
      if (hfs_reader->IsDirectory())
        EXPECT_TRUE(dirs.erase(path)) << path;
      else
        EXPECT_TRUE(files.erase(path)) << path;
    }

    EXPECT_EQ(0u, files.size());
    for (const auto& file : files) {
      ADD_FAILURE() << "Unexpected missing file " << file;
    }
  }
};

TEST_F(HFSIteratorTest, HFSPlus) {
  base::File file;
  ASSERT_NO_FATAL_FAILURE(test::GetTestFile("hfs_plus.img", &file));

  FileReadStream stream(file.GetPlatformFile());
  HFSIterator hfs_reader(&stream);
  TestTargetFiles(&hfs_reader, false);
}

TEST_F(HFSIteratorTest, HFSXCaseSensitive) {
  base::File file;
  ASSERT_NO_FATAL_FAILURE(test::GetTestFile("hfsx_case_sensitive.img", &file));

  FileReadStream stream(file.GetPlatformFile());
  HFSIterator hfs_reader(&stream);
  TestTargetFiles(&hfs_reader, true);
}

class HFSFileReadTest : public testing::TestWithParam<const char*> {
 protected:
  void SetUp() override {
    ASSERT_NO_FATAL_FAILURE(test::GetTestFile(GetParam(), &hfs_file_));

    hfs_stream_ = std::make_unique<FileReadStream>(hfs_file_.GetPlatformFile());
    hfs_reader_ = std::make_unique<HFSIterator>(hfs_stream_.get());
    ASSERT_TRUE(hfs_reader_->Open());
  }

  bool GoToFile(const char16_t* name) {
    while (hfs_reader_->Next()) {
      if (EndsWith(hfs_reader_->GetPath(), name,
                   base::CompareCase::SENSITIVE)) {
        return true;
      }
    }
    return false;
  }

  HFSIterator* hfs_reader() { return hfs_reader_.get(); }

 private:
  base::File hfs_file_;
  std::unique_ptr<FileReadStream> hfs_stream_;
  std::unique_ptr<HFSIterator> hfs_reader_;
};

TEST_P(HFSFileReadTest, ReadReadme) {
  ASSERT_TRUE(GoToFile(u"README.txt"));

  std::unique_ptr<ReadStream> stream = hfs_reader()->GetReadStream();
  ASSERT_TRUE(stream.get());

  EXPECT_FALSE(hfs_reader()->IsSymbolicLink());
  EXPECT_FALSE(hfs_reader()->IsHardLink());
  EXPECT_FALSE(hfs_reader()->IsDecmpfsCompressed());

  std::vector<uint8_t> buffer(4, 0);

  // Read the first four bytes.
  EXPECT_TRUE(stream->ReadExact(buffer));
  const uint8_t expected[] = { 'T', 'h', 'i', 's' };
  EXPECT_EQ(0, memcmp(expected, &buffer[0], sizeof(expected)));
  buffer.clear();

  // Rewind back to the start.
  EXPECT_EQ(0, stream->Seek(0, SEEK_SET));

  // Read the entire file now.
  auto maybe_data = ReadEntireStream(*stream);
  ASSERT_TRUE(maybe_data.has_value());
  EXPECT_EQ(
      "This is a test HFS+ filesystem generated by "
      "chrome/test/data/safe_browsing/dmg/make_hfs.sh.\n",
      base::as_string_view(maybe_data.value()));
  EXPECT_EQ(92u, maybe_data->size());
}

TEST_P(HFSFileReadTest, ReadRandom) {
  ASSERT_TRUE(GoToFile(u"fifth/random"));

  std::unique_ptr<ReadStream> stream = hfs_reader()->GetReadStream();
  ASSERT_TRUE(stream.get());

  EXPECT_FALSE(hfs_reader()->IsSymbolicLink());
  EXPECT_FALSE(hfs_reader()->IsHardLink());
  EXPECT_FALSE(hfs_reader()->IsDecmpfsCompressed());

  auto maybe_data = ReadEntireStream(*stream);
  ASSERT_TRUE(maybe_data.has_value());
  EXPECT_EQ(768u, maybe_data->size());
}

TEST_P(HFSFileReadTest, Symlink) {
  ASSERT_TRUE(GoToFile(u"symlink-random"));

  std::unique_ptr<ReadStream> stream = hfs_reader()->GetReadStream();
  ASSERT_TRUE(stream.get());

  EXPECT_TRUE(hfs_reader()->IsSymbolicLink());
  EXPECT_FALSE(hfs_reader()->IsHardLink());
  EXPECT_FALSE(hfs_reader()->IsDecmpfsCompressed());

  auto maybe_data = ReadEntireStream(*stream);
  ASSERT_TRUE(maybe_data.has_value());

  EXPECT_EQ("fourth/fifth/random", base::as_string_view(maybe_data.value()));
}

TEST_P(HFSFileReadTest, HardLink) {
  ASSERT_TRUE(GoToFile(u"unicode_name"));

  EXPECT_FALSE(hfs_reader()->IsSymbolicLink());
  EXPECT_TRUE(hfs_reader()->IsHardLink());
  EXPECT_FALSE(hfs_reader()->IsDecmpfsCompressed());
}

TEST_P(HFSFileReadTest, DecmpfsFile) {
  ASSERT_TRUE(GoToFile(u"first/second/goat-output.txt"));

  std::unique_ptr<ReadStream> stream = hfs_reader()->GetReadStream();
  ASSERT_TRUE(stream.get());

  EXPECT_FALSE(hfs_reader()->IsSymbolicLink());
  EXPECT_FALSE(hfs_reader()->IsHardLink());
  EXPECT_TRUE(hfs_reader()->IsDecmpfsCompressed());

  auto maybe_data = ReadEntireStream(*stream);
  ASSERT_TRUE(maybe_data.has_value());
  EXPECT_EQ(0u, maybe_data->size());
}

INSTANTIATE_TEST_SUITE_P(HFSIteratorTest,
                         HFSFileReadTest,
                         testing::Values("hfs_plus.img",
                                         "hfsx_case_sensitive.img"));

}  // namespace
}  // namespace dmg
}  // namespace safe_browsing