chromium/chrome/utility/safe_browsing/mac/udif_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/udif.h"

#include <hfs/hfs_format.h>
#include <libkern/OSByteOrder.h>
#include <stddef.h>
#include <stdint.h>

#include <array>

#include "base/containers/span.h"
#include "base/files/file.h"
#include "base/memory/raw_ptr_exclusion.h"
#include "base/strings/stringprintf.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 {

constexpr std::array<const std::string_view, 8> kGPTExpectedPartitions = {
    "MBR",
    "Primary GPT Header",
    "Primary GPT Tabler",
    "Apple_Free",
    "Apple_HFS",
    "Apple_Free",
    "Backup GPT Table",
    "Backup GPT Header"};

constexpr std::array<std::string_view, 1> kNoPartitionMap = {"Apple_HFS"};

constexpr std::array<std::string_view, 4> kAPMExpectedPartitions = {
    "DDM",
    "Apple_partition_map",
    "Apple_HFS",
    "Apple_Free",
};

struct UDIFTestCase {
  enum ExpectedResults : uint16_t {
    ALL_FAIL = 0,
    UDIF_PARSE = 1 << 1,
    GET_HFS_STREAM = 1 << 2,
    READ_UDIF_DATA = 1 << 3,

    ALL_PASS = static_cast<uint16_t>(-1),  // All bits set.
  };

  // The disk image file to open.
  const char* file_name;

  base::span<const std::string_view> expected_partitions;

  // A bitmask of ExpectedResults. As the parser currently only supports
  // certain UDIF features, this is used to properly test expectations.
  int expected_results;

  // Generates a human-friendly name for the parameterized test.
  static std::string GetTestName(
      const testing::TestParamInfo<UDIFTestCase>& test) {
    std::string file = test.param.file_name;
    return file.substr(0, file.find('.'));
  }
};

class UDIFParserTest : public testing::TestWithParam<UDIFTestCase> {
 protected:
  void RunReadAllTest(size_t buffer_size) {
    const UDIFTestCase& test_case = GetParam();
    if (!(test_case.expected_results & UDIFTestCase::READ_UDIF_DATA)) {
      return;
    }

    base::File file;
    ASSERT_NO_FATAL_FAILURE(test::GetTestFile(test_case.file_name, &file));

    safe_browsing::dmg::FileReadStream file_stream(file.GetPlatformFile());
    safe_browsing::dmg::UDIFParser udif(&file_stream);
    ASSERT_TRUE(udif.Parse());

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

    for (size_t i = 0; i < udif.GetNumberOfPartitions(); ++i) {
      SCOPED_TRACE(base::StringPrintf("partition %zu", i));

      size_t total_size = udif.GetPartitionSize(i);
      size_t total_bytes_read = 0;
      std::unique_ptr<ReadStream> stream = udif.GetPartitionReadStream(i);

      bool success = false;
      do {
        size_t bytes_read = 0;
        success = stream->Read(buffer, &bytes_read);
        total_bytes_read += bytes_read;
        EXPECT_TRUE(success);
        EXPECT_TRUE(bytes_read == buffer_size ||
                    total_bytes_read == total_size)
            << "bytes_read = " << bytes_read;
      } while (total_bytes_read < total_size && success);
    }
  }
};

TEST_P(UDIFParserTest, ParseUDIF) {
  const UDIFTestCase& test_case = GetParam();

  base::File file;
  ASSERT_NO_FATAL_FAILURE(test::GetTestFile(test_case.file_name, &file));

  safe_browsing::dmg::FileReadStream file_stream(file.GetPlatformFile());
  safe_browsing::dmg::UDIFParser udif(&file_stream);

  bool expected_parse_success =
      UDIFTestCase::UDIF_PARSE & test_case.expected_results;
  ASSERT_EQ(expected_parse_success, udif.Parse());
  if (!expected_parse_success)
    return;

  EXPECT_EQ(test_case.expected_partitions.size(), udif.GetNumberOfPartitions());

  for (size_t i = 0; i < udif.GetNumberOfPartitions(); ++i) {
    SCOPED_TRACE(base::StringPrintf("partition %zu", i));
    std::unique_ptr<ReadStream> stream = udif.GetPartitionReadStream(i);

    // Apple_HFS will match both HFS and HFSX.
    if (udif.GetPartitionType(i).find("Apple_HFS") != std::string::npos) {
      ASSERT_EQ(
          (UDIFTestCase::GET_HFS_STREAM & test_case.expected_results) != 0,
          stream.get() != nullptr);
      if (!stream)
        continue;

      EXPECT_EQ(1024, stream->Seek(1024, SEEK_SET));

      HFSPlusVolumeHeader header = {0};
      bool expect_read_success =
          test_case.expected_results & UDIFTestCase::READ_UDIF_DATA;
      EXPECT_EQ(expect_read_success, stream->ReadType(header));
      if (!expect_read_success)
        continue;

      size_t size = udif.GetPartitionSize(i);
      off_t offset = stream->Seek(-1024, SEEK_END);
      ASSERT_GE(offset, 0);
      EXPECT_EQ(size - 1024, static_cast<size_t>(offset));

      HFSPlusVolumeHeader alternate_header = {0};
      EXPECT_TRUE(stream->ReadType(alternate_header));

      EXPECT_EQ(0, memcmp(&header, &alternate_header, sizeof(header)));
      EXPECT_EQ(kHFSPlusSigWord, OSSwapBigToHostInt16(header.signature));
    }

    if (test_case.expected_results & UDIFTestCase::READ_UDIF_DATA) {
      EXPECT_EQ(0, stream->Seek(0, SEEK_SET));
      size_t partition_size = udif.GetPartitionSize(i);
      std::vector<uint8_t> data(partition_size, 0);
      EXPECT_TRUE(stream->ReadExact(data));
    }
  }
}


// These tests ensure that reading the entire partition stream with different
// buffer sizes (and thus unaligned UDIF chunks) all succeed.

TEST_P(UDIFParserTest, ReadAll_8) {
  RunReadAllTest(8);
}

TEST_P(UDIFParserTest, ReadAll_512) {
  RunReadAllTest(512);
}

TEST_P(UDIFParserTest, ReadAll_1000) {
  RunReadAllTest(1000);
}

TEST_P(UDIFParserTest, ReadAll_4444) {
  RunReadAllTest(4444);
}

TEST_P(UDIFParserTest, ReadAll_8181) {
  RunReadAllTest(8181);
}

TEST_P(UDIFParserTest, ReadAll_100000) {
  RunReadAllTest(100000);
}

constexpr UDIFTestCase cases[] = {
    {"dmg_UDBZ_GPTSPUD.dmg", kGPTExpectedPartitions, UDIFTestCase::ALL_PASS},
    {"dmg_UDBZ_NONE.dmg", kNoPartitionMap, UDIFTestCase::ALL_PASS},
    {"dmg_UDBZ_SPUD.dmg", kAPMExpectedPartitions, UDIFTestCase::ALL_PASS},
    {"dmg_UDCO_GPTSPUD.dmg", kGPTExpectedPartitions,
     // ADC compression not supported.
     UDIFTestCase::UDIF_PARSE | UDIFTestCase::GET_HFS_STREAM},
    {"dmg_UDCO_NONE.dmg", kNoPartitionMap,
     // ADC compression not supported.
     UDIFTestCase::UDIF_PARSE | UDIFTestCase::GET_HFS_STREAM},
    {"dmg_UDCO_SPUD.dmg", kAPMExpectedPartitions,
     // ADC compression not supported.
     UDIFTestCase::UDIF_PARSE | UDIFTestCase::GET_HFS_STREAM},
    {"dmg_UDRO_GPTSPUD.dmg", kGPTExpectedPartitions, UDIFTestCase::ALL_PASS},
    {"dmg_UDRO_NONE.dmg", kNoPartitionMap, UDIFTestCase::ALL_PASS},
    {"dmg_UDRO_SPUD.dmg", kAPMExpectedPartitions, UDIFTestCase::ALL_PASS},
    {"dmg_UDRW_GPTSPUD.dmg", kGPTExpectedPartitions,
     // UDRW not supported.
     UDIFTestCase::ALL_FAIL},
    {"dmg_UDRW_NONE.dmg", kNoPartitionMap,
     // UDRW not supported.
     UDIFTestCase::ALL_FAIL},
    {"dmg_UDRW_SPUD.dmg", kAPMExpectedPartitions,
     // UDRW not supported.
     UDIFTestCase::ALL_FAIL},
    {"dmg_UDSP_GPTSPUD.sparseimage", kGPTExpectedPartitions,
     // Sparse images not supported.
     UDIFTestCase::ALL_FAIL},
    {"dmg_UDSP_NONE.sparseimage", kNoPartitionMap,
     // UDRW not supported.
     UDIFTestCase::ALL_FAIL},
    {"dmg_UDSP_SPUD.sparseimage", kAPMExpectedPartitions,
     // Sparse images not supported.
     UDIFTestCase::ALL_FAIL},
    {"dmg_UDTO_GPTSPUD.cdr", kGPTExpectedPartitions,
     // CD/DVD format not supported.
     UDIFTestCase::ALL_FAIL},
    {"dmg_UDTO_NONE.cdr", kNoPartitionMap,
     // CD/DVD format not supported.
     UDIFTestCase::ALL_FAIL},
    {"dmg_UDTO_SPUD.cdr", kAPMExpectedPartitions,
     // CD/DVD format not supported.
     UDIFTestCase::ALL_FAIL},
    {"dmg_UDZO_GPTSPUD.dmg", kGPTExpectedPartitions, UDIFTestCase::ALL_PASS},
    {"dmg_UDZO_SPUD.dmg", kAPMExpectedPartitions, UDIFTestCase::ALL_PASS},
    {"dmg_UFBI_GPTSPUD.dmg", kGPTExpectedPartitions, UDIFTestCase::ALL_PASS},
    {"dmg_UFBI_SPUD.dmg", kAPMExpectedPartitions, UDIFTestCase::ALL_PASS},
};

INSTANTIATE_TEST_SUITE_P(UDIFParserTest,
                         UDIFParserTest,
                         testing::ValuesIn(cases),
                         UDIFTestCase::GetTestName);

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