// 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