text: // Copyright 2019 The Fuchsia 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 <lib/ftl/ndm-driver.h>
  #include <string.h>
  
  #include <optional>
  #include <vector>
  
  #include <gtest/gtest.h>
  
  #include "ftl.h"
  #include "ndm/ndmp.h"
  
  namespace {
  
  constexpr uint32_t kNumBlocks = 30;
  constexpr uint32_t kPagesPerBlock = 16;
  constexpr uint32_t kPageSize = 4096;
  constexpr uint32_t kOobSize = 16;
  constexpr uint32_t kBlockSize = kPageSize * kPagesPerBlock;
  
  constexpr uint32_t kControlPage0 = (kNumBlocks - 1) * kPagesPerBlock;
  constexpr uint32_t kControlPage1 = (kNumBlocks - 2) * kPagesPerBlock;
  
  constexpr ftl::VolumeOptions kDefaultOptions = {kNumBlocks, 2, kBlockSize, kPageSize, kOobSize, 0};
  
  class NdmRamDriver final : public ftl::NdmBaseDriver {
   public:
    NdmRamDriver(const ftl::VolumeOptions options = kDefaultOptions,
                 FtlLogger logger = ftl::DefaultLogger())
        : NdmBaseDriver(logger), options_(options) {}
  
    const uint8_t* data(uint32_t page_num) const { return &volume_[page_num * kPageSize]; }
    NDM ndm() { return GetNdmForTest(); }
    void format_using_v2(bool value) { format_using_v2_ = value; }
  
    // Goes through the normal logic to create a volume with user data info.
    const char* CreateVolume() { return CreateNdmVolume(nullptr, options_, true); }
  
    // NdmDriver interface:
    const char* Init() final;
    const char* Attach(const ftl::Volume* ftl_volume) final;
    bool Detach() final;
    int NandRead(uint32_t start_page, uint32_t page_count, void* page_buffer, void* oob_buffer) final;
    int NandWrite(uint32_t start_page, uint32_t page_count, const void* page_buffer,
                  const void* oob_buffer) final;
    int NandErase(uint32_t page_num) final;
    int IsBadBlock(uint32_t page_num) final { return ftl::kFalse; }
    bool IsEmptyPage(uint32_t page_num, const uint8_t* data, const uint8_t* spare) final {
      return IsEmptyPageImpl(data, kPageSize, spare, kOobSize);
    }
    uint32_t PageSize() final { return kPageSize; }
    uint8_t SpareSize() final { return kOobSize; }
  
   private:
    std::vector<uint8_t> volume_;
    ftl::VolumeOptions options_;
    bool format_using_v2_ = true;
  };
  
  const char* NdmRamDriver::Init() {
    size_t volume_size = (kPageSize + kOobSize) * kPagesPerBlock * kNumBlocks;
    volume_.resize(volume_size);
    memset(volume_.data(), 0xff, volume_size);
    return nullptr;
  }
  
  const char* NdmRamDriver::Attach(const ftl::Volume* ftl_volume) {
    if (!GetNdmForTest()) {
      IsNdmDataPresent(options_, format_using_v2_);
    }
    return GetNdmForTest() ? nullptr : "Failed to add device";
  }
  
  bool NdmRamDriver::Detach() { return RemoveNdmVolume(); }
  
  int NdmRamDriver::NandRead(uint32_t start_page, uint32_t page_count, void* page_buffer,
                             void* oob_buffer) {
    uint8_t* data = reinterpret_cast<uint8_t*>(page_buffer);
    uint8_t* spare = reinterpret_cast<uint8_t*>(oob_buffer);
  
    if (data) {
      memcpy(data, &volume_[start_page * kPageSize], page_count * kPageSize);
    }
  
    if (spare) {
      uint32_t oob_offset = kPagesPerBlock * kNumBlocks * kPageSize;
      memcpy(spare, &volume_[oob_offset + start_page * kOobSize], page_count * kOobSize);
    }
    return ftl::kNdmOk;
  }
  
  int NdmRamDriver::NandWrite(uint32_t start_page, uint32_t page_count, const void* page_buffer,
                              const void* oob_buffer) {
    const uint8_t* data = reinterpret_cast<const uint8_t*>(page_buffer);
    const uint8_t* spare = reinterpret_cast<const uint8_t*>(oob_buffer);
    ZX_ASSERT(data);
    ZX_ASSERT(spare);
  
    uint32_t oob_offset = kPagesPerBlock * kNumBlocks * kPageSize;
    memcpy(&volume_[start_page * kPageSize], data, page_count * kPageSize);
    memcpy(&volume_[oob_offset + start_page * kOobSize], spare, page_count * kOobSize);
    return ftl::kNdmOk;
  }
  
  int NdmRamDriver::NandErase(uint32_t page_num) {
    ZX_ASSERT(page_num % kPagesPerBlock == 0);
  
    uint32_t oob_offset = kPagesPerBlock * kNumBlocks * kPageSize;
    memset(&volume_[page_num * kPageSize], 0xFF, kBlockSize);
    memset(&volume_[oob_offset + page_num * kOobSize], 0xFF, kPagesPerBlock * kOobSize);
  
    return ftl::kNdmOk;
  }
  
  class NdmTest : public ::testing::Test {
   public:
    void SetUp() override {
      ASSERT_TRUE(ftl::InitModules());
      ASSERT_EQ(nullptr, ndm_driver_.Init());
      ASSERT_EQ(nullptr, ndm_driver_.Attach(nullptr));
    }
  
   protected:
    NdmRamDriver ndm_driver_;
  };
  
  struct HeaderV1 {
    uint16_t current_location;
    uint16_t last_location;
    int32_t sequence_num;
    uint32_t crc;
    int32_t num_blocks;
    int32_t block_size;
    int32_t control_block0;
    int32_t control_block1;
    int32_t free_virt_block;
    int32_t free_control_block;
    int32_t transfer_to_block;
  };
  
  struct HeaderV2 {
    uint16_t major_version;
    uint16_t minor_version;
    HeaderV1 v1;
  };
  
  class NdmTestOldFormat : public NdmTest {
   public:
    void SetUp() override {
      ASSERT_TRUE(ftl::InitModules());
      ASSERT_EQ(nullptr, ndm_driver_.Init());
      ndm_driver_.format_using_v2(false);
      ASSERT_EQ(nullptr, ndm_driver_.Attach(nullptr));
    }
  };
  
  TEST_F(NdmTestOldFormat, WritesVersion1) {
    auto header = reinterpret_cast<const HeaderV1*>(ndm_driver_.data(kControlPage0));
    EXPECT_EQ(1, header->current_location);
    EXPECT_EQ(1, header->last_location);
    EXPECT_EQ(0, header->sequence_num);
    EXPECT_EQ(kNumBlocks, static_cast<uint32_t>(header->num_blocks));
    EXPECT_EQ(kPageSize * kPagesPerBlock, static_cast<uint32_t>(header->block_size));
    EXPECT_EQ(kNumBlocks - 1, static_cast<uint32_t>(header->control_block0));
    EXPECT_EQ(kNumBlocks - 2, static_cast<uint32_t>(header->control_block1));
    EXPECT_EQ(kNumBlocks - 4, static_cast<uint32_t>(header->free_virt_block));
    EXPECT_EQ(kNumBlocks - 3, static_cast<uint32_t>(header->free_control_block));
    EXPECT_EQ(-1, header->transfer_to_block);
  }
  
  TEST_F(NdmTest, WritesVersion2) {
    auto header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage0));
    EXPECT_EQ(2, header->major_version);
    EXPECT_EQ(0, header->minor_version);
    EXPECT_EQ(1, header->v1.current_location);
    EXPECT_EQ(1, header->v1.last_location);
    EXPECT_EQ(0, header->v1.sequence_num);
    EXPECT_EQ(kNumBlocks, static_cast<uint32_t>(header->v1.num_blocks));
    EXPECT_EQ(kPageSize * kPagesPerBlock, static_cast<uint32_t>(header->v1.block_size));
    EXPECT_EQ(kNumBlocks - 1, static_cast<uint32_t>(header->v1.control_block0));
    EXPECT_EQ(kNumBlocks - 2, static_cast<uint32_t>(header->v1.control_block1));
    EXPECT_EQ(kNumBlocks - 4, static_cast<uint32_t>(header->v1.free_virt_block));
    EXPECT_EQ(kNumBlocks - 3, static_cast<uint32_t>(header->v1.free_control_block));
    EXPECT_EQ(-1, header->v1.transfer_to_block);
  }
  
  TEST_F(NdmTest, OnlyOneControlBlock) {
    auto header = reinterpret_cast<const HeaderV1*>(ndm_driver_.data(kControlPage0 + 1));
    EXPECT_EQ(0xffff, header->current_location);
  
    header = reinterpret_cast<const HeaderV1*>(ndm_driver_.data(kControlPage1));
    EXPECT_EQ(0xffff, header->current_location);
  }
  
  TEST_F(NdmTestOldFormat, NoVersion2) {
    NDMPartition partition = {};
    partition.num_blocks = ndmGetNumVBlocks(ndm_driver_.ndm());
    ASSERT_EQ(0, ndmWritePartition(ndm_driver_.ndm(), &partition, 0, "foo"));
  
    EXPECT_NE(nullptr, ndmGetPartition(ndm_driver_.ndm(), 0));
    EXPECT_EQ(nullptr, ndmGetPartitionInfo(ndm_driver_.ndm()));
  }
  
  TEST_F(NdmTest, UsesVersion2) {
    NDMPartitionInfo partition = {};
    uint32_t partition_size = ndmGetNumVBlocks(ndm_driver_.ndm());
    partition.basic_data.num_blocks = partition_size;
    strcpy(partition.basic_data.name, "foo");
    ASSERT_EQ(0, ndmWritePartitionInfo(ndm_driver_.ndm(), &partition));
  
    EXPECT_NE(nullptr, ndmGetPartition(ndm_driver_.ndm(), 0));
  
    const NDMPartitionInfo* info = ndmGetPartitionInfo(ndm_driver_.ndm());
    ASSERT_NE(nullptr, info);
    EXPECT_EQ(0u, info->basic_data.first_block);
    EXPECT_EQ(partition_size, info->basic_data.num_blocks);
    EXPECT_EQ(0u, info->user_data.data_size);
    EXPECT_STREQ("foo", info->basic_data.name);
  }
  
  TEST_F(NdmTest, SavesVersion2) {
    NDMPartitionInfo partition = {};
    partition.basic_data.num_blocks = ndmGetNumVBlocks(ndm_driver_.ndm());
    ASSERT_EQ(0, ndmWritePartitionInfo(ndm_driver_.ndm(), &partition));
    ASSERT_EQ(0, ndmSavePartitionTable(ndm_driver_.ndm()));
  
    auto header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage0 + 1));
    EXPECT_EQ(2, header->major_version);
    EXPECT_EQ(0, header->minor_version);
    EXPECT_EQ(1, header->v1.current_location);
    EXPECT_EQ(1, header->v1.last_location);
    EXPECT_EQ(1, header->v1.sequence_num);
    EXPECT_EQ(kNumBlocks, static_cast<uint32_t>(header->v1.num_blocks));
    EXPECT_EQ(kPageSize * kPagesPerBlock, static_cast<uint32_t>(header->v1.block_size));
    EXPECT_EQ(kNumBlocks - 1, static_cast<uint32_t>(header->v1.control_block0));
    EXPECT_EQ(kNumBlocks - 2, static_cast<uint32_t>(header->v1.control_block1));
    EXPECT_EQ(kNumBlocks - 4, static_cast<uint32_t>(header->v1.free_virt_block));
    EXPECT_EQ(kNumBlocks - 3, static_cast<uint32_t>(header->v1.free_control_block));
    EXPECT_EQ(-1, header->v1.transfer_to_block);
  }
  
  TEST_F(NdmTest, OnlyOneV2ControlBlock) {
    NDMPartitionInfo partition = {};
    partition.basic_data.num_blocks = ndmGetNumVBlocks(ndm_driver_.ndm());
    ASSERT_EQ(0, ndmWritePartitionInfo(ndm_driver_.ndm(), &partition));
    ASSERT_EQ(0, ndmSavePartitionTable(ndm_driver_.ndm()));
  
    auto header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage0 + 2));
    EXPECT_EQ(0xffff, header->major_version);
  
    header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage1));
    EXPECT_EQ(0xffff, header->major_version);
  }
  
  TEST_F(NdmTest, SavesUpdatedPartitionData) {
    NDMPartitionInfo partition = {};
    partition.basic_data.num_blocks = ndmGetNumVBlocks(ndm_driver_.ndm());
    ASSERT_EQ(0, ndmWritePartitionInfo(ndm_driver_.ndm(), &partition));
  
    // Write three new control blocks.
    ASSERT_EQ(0, ndmSavePartitionTable(ndm_driver_.ndm()));
    ASSERT_EQ(0, ndmSavePartitionTable(ndm_driver_.ndm()));
    ASSERT_EQ(0, ndmSavePartitionTable(ndm_driver_.ndm()));
  
    auto header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage0 + 1));
    EXPECT_EQ(2, header->major_version);
    EXPECT_EQ(1, header->v1.sequence_num);
  
    header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage0 + 2));
    EXPECT_EQ(2, header->major_version);
    EXPECT_EQ(2, header->v1.sequence_num);
  
    header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage0 + 3));
    EXPECT_EQ(2, header->major_version);
    EXPECT_EQ(3, header->v1.sequence_num);
  
    header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage0 + 4));
    EXPECT_EQ(0xffff, header->major_version);
  
    header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage1));
    EXPECT_EQ(0xffff, header->major_version);
  }
  
  union PartitionInfo {
    NDMPartitionInfo ndm;
    struct {
      NDMPartition basic_data;
      uint32_t data_size;
      uint32_t data;
    } exploded;
  };
  static_assert(sizeof(NDMPartition) + sizeof(uint32_t) == sizeof(NDMPartitionInfo));
  static_assert(sizeof(NDMPartitionInfo) + sizeof(uint32_t) == sizeof(PartitionInfo));
  
  // Tests that the user portion of the partition info can grow.
  TEST_F(NdmTest, UpdatesUserData) {
    NDMPartitionInfo partition = {};
    partition.basic_data.num_blocks = ndmGetNumVBlocks(ndm_driver_.ndm());
    ASSERT_EQ(0, ndmWritePartitionInfo(ndm_driver_.ndm(), &partition));
    ASSERT_EQ(0, ndmSavePartitionTable(ndm_driver_.ndm()));
  
    // Reinitialize NDM.
    EXPECT_TRUE(ndm_driver_.Detach());
    ASSERT_EQ(nullptr, ndm_driver_.Attach(nullptr));
  
    // Redefine the partition.
    PartitionInfo new_info = {};
    new_info.exploded.basic_data = partition.basic_data;
    new_info.exploded.data_size = sizeof(new_info.exploded.data);
    new_info.exploded.data = 42;
    ASSERT_EQ(0, ndmWritePartitionInfo(ndm_driver_.ndm(), &new_info.ndm));
    ASSERT_EQ(0, ndmSavePartitionTable(ndm_driver_.ndm()));
  
    // Read the latest version from disk.
    EXPECT_TRUE(ndm_driver_.Detach());
    ASSERT_EQ(nullptr, ndm_driver_.Attach(nullptr));
  
    const NDMPartitionInfo* info = ndmGetPartitionInfo(ndm_driver_.ndm());
    ASSERT_NE(nullptr, info);
    ASSERT_EQ(sizeof(new_info.exploded.data), info->user_data.data_size);
  
    auto actual_info = reinterpret_cast<const PartitionInfo*>(info);
    EXPECT_EQ(42u, actual_info->exploded.data);
  
    // Verify the expected disk layout.
    auto header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage0 + 1));
    EXPECT_EQ(2, header->major_version);
    EXPECT_EQ(1, header->v1.sequence_num);
  
    header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage1));
    EXPECT_EQ(2, header->major_version);
    EXPECT_EQ(2, header->v1.sequence_num);
  
    header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage0 + 2));
    EXPECT_EQ(0xffff, header->major_version);
  
    header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage1 + 1));
    EXPECT_EQ(0xffff, header->major_version);
  }
  
  TEST_F(NdmTest, BaseDriverSavesConfig) {
    ASSERT_EQ(nullptr, ndm_driver_.CreateVolume());
  
    const NDMPartitionInfo* info = ndmGetPartitionInfo(ndm_driver_.ndm());
    ASSERT_NE(nullptr, info);
    ASSERT_GE(info->user_data.data_size, 96u);  // Size of the first version of the data.
  
    const ftl::VolumeOptions* options = ndm_driver_.GetSavedOptions();
    ASSERT_NE(nullptr, options);
    ASSERT_EQ(memcmp(&kDefaultOptions, options, sizeof(*options)), 0);
  }
  
  class NdmReadOnlyTest : public ::testing::Test {
   public:
    void SetUp() override {
      ASSERT_TRUE(ftl::InitModules());
      ftl::VolumeOptions options = kDefaultOptions;
      options.flags |= ftl::kReadOnlyInit;
      ndm_driver_.reset(new NdmRamDriver(options));
      ASSERT_EQ(nullptr, ndm_driver_->Init());
      buffer_ = std::vector<uint8_t>(kPageSize, 0xff);
    }
  
   protected:
    std::unique_ptr<NdmRamDriver> ndm_driver_;
    std::vector<uint8_t> buffer_;
  };
  
  // An NDM control block version 1, stored on page 29.
  constexpr uint32_t kControl29V1[] = {
      0x00010001, 0x00000000, 0x4efa26dd, 0x0000001e, 0x00010000, 0x0000001d, 0x0000001c, 0x0000001a,
      0x0000001b, 0xffffffff, 0x00000000, 0x0000001e, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff};
  
  constexpr uint32_t kControlOob[] = {0x4d444eff, 0x31304154, 0xffffffff, 0x00ffffff};
  
  TEST_F(NdmReadOnlyTest, Version1Only) {
    memcpy(buffer_.data(), kControl29V1, sizeof(kControl29V1));
    ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage0, 1, buffer_.data(), kControlOob));
  
    ASSERT_EQ(nullptr, ndm_driver_->CreateVolume());
  }
  
  // An NDM control block version 2.0, stored on page 29.
  constexpr uint32_t kControl29V2[] = {
      0x00000002, 0x00010001, 0x00000000, 0x061cc64a, 0x0000001e, 0x00010000, 0x0000001d, 0x0000001c,
      0x0000001a, 0x0000001b, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000, 0x0000001e, 0xffffffff};
  
  TEST_F(NdmReadOnlyTest, Version2Only) {
    memcpy(buffer_.data(), kControl29V2, sizeof(kControl29V2));
    ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage0, 1, buffer_.data(), kControlOob));
  
    ASSERT_EQ(nullptr, ndm_driver_->CreateVolume());
  }
  
  // An NDM control block version 2.0, stored on page 28, with partition data.
  constexpr uint32_t kControl28V2[] = {
      0x00000002, 0x00010001, 0x00000001, 0x41220f07, 0x0000001e, 0x00010000, 0x0000001d, 0x0000001c,
      0x0000001a, 0x0000001b, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000001, 0x0000001e, 0xffffffff,
      0xffffffff, 0x00000000, 0x0000001a, 0x006c7466, 0x00000000, 0x00000000, 0x00000000, 0x00000060,
      0x00000001, 0x00000004, 0x00000006, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,
      0x0000001e, 0x00000002, 0x00010000, 0x00001000, 0x00000010, 0x00000000, 0x00000000, 0x00000000,
      0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000};
  
  TEST_F(NdmReadOnlyTest, UpgradedVersion2) {
    memcpy(buffer_.data(), kControl29V1, sizeof(kControl29V1));
    ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage0, 1, buffer_.data(), kControlOob));
  
    memcpy(buffer_.data(), kControl28V2, sizeof(kControl28V2));
    ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage1, 1, buffer_.data(), kControlOob));
    ASSERT_EQ(nullptr, ndm_driver_->CreateVolume());
  }
  
  // An NDM control block version 1, stored on page 29, with one factory bad block and
  // a second bad block in the process of being relocated.
  constexpr uint32_t kControlBlockTransferV1[] = {
      0x00010001, 0x00000001, 0xcd0deda6, 0x0000001e, 0x00010000, 0x0000001d, 0x0000001c, 0xffffffff,
      0xffffffff, 0x0000001b, 0x00000003, 0x0000000d, 0x00000102, 0x00000000, 0x00001e00, 0x00000300,
      0x00001b00, 0xffffff00, 0xffffffff, 0x000000ff, 0x00001a00, 0x6c746600, 0x00000000, 0x00000000,
      0x00000000, 0xffffff00, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff};
  
  TEST_F(NdmReadOnlyTest, InTransferV1) {
    memcpy(buffer_.data(), kControlBlockTransferV1, sizeof(kControlBlockTransferV1));
    ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage0, 1, buffer_.data(), kControlOob));
  
    ASSERT_NE(nullptr, ndm_driver_->CreateVolume());
    ASSERT_EQ(NDM_BAD_BLK_RECOV, GetFsErrCode());
  }
  
  // An NDM control block version 1, stored on page 29, with one factory bad block and
  // one translated bad block.
  constexpr uint32_t kControlBlockBadBlocksV1[] = {
      0x00010001, 0x00000002, 0x64342dc5, 0x0000001e, 0x00010000, 0x0000001d, 0x0000001c, 0xffffffff,
      0xffffffff, 0xffffffff, 0x00000001, 0x00000000, 0x0000001e, 0x00000003, 0x0000001b, 0xffffffff,
      0xffffffff, 0x00000000, 0x0000001a, 0x006c7466, 0x00000000, 0x00000000, 0x00000000, 0xffffffff};
  
  TEST_F(NdmReadOnlyTest, BadBlocksV1) {
    memcpy(buffer_.data(), kControlBlockBadBlocksV1, sizeof(kControlBlockBadBlocksV1));
    ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage0, 1, buffer_.data(), kControlOob));
  
    ASSERT_EQ(nullptr, ndm_driver_->CreateVolume());
    EXPECT_EQ(2u, ndm_driver_->ndm()->num_bad_blks);
  }
  
  // An NDM control block version 2.0, stored on page 29, with one factory bad block and
  // a second bad block in the process of being relocated.
  constexpr uint32_t kControlBlockTransferV2[] = {
      0x00000002, 0x00010001, 0x00000001, 0xdc1fd63c, 0x0000001e, 0x00010000, 0x0000001d, 0x0000001c,
      0xffffffff, 0xffffffff, 0x0000001b, 0x00000003, 0x0000000d, 0x00000001, 0x00000000, 0x0000001e,
      0x00000003, 0x0000001b, 0xffffffff, 0xffffffff, 0x00000000, 0x0000001a, 0x006c7466, 0x00000000,
      0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff};
  
  TEST_F(NdmReadOnlyTest, InTransferV2) {
    memcpy(buffer_.data(), kControlBlockTransferV2, sizeof(kControlBlockTransferV2));
    ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage0, 1, buffer_.data(), kControlOob));
  
    ASSERT_NE(nullptr, ndm_driver_->CreateVolume());
    ASSERT_EQ(NDM_BAD_BLK_RECOV, GetFsErrCode());
  }
  
  // An NDM control block version 2.0, stored on page 29, with one factory bad block and
  // one translated bad block.
  constexpr uint32_t kControlBlockBadBlocksV2[] = {
      0x00000002, 0x00010001, 0x00000002, 0x01148752, 0x0000001e, 0x00010000, 0x0000001d, 0x0000001c,
      0xffffffff, 0xffffffff, 0xffffffff, 0x00000003, 0x0000000d, 0x00000001, 0x00000000, 0x0000001e,
      0x00000003, 0x0000001b, 0xffffffff, 0xffffffff, 0x00000000, 0x0000001a, 0x006c7466, 0x00000000,
      0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff};
  
  TEST(NdmReadOnlyTestWithLogger, BadBlocksV2) {
    static bool logger_called = false;
    logger_called = false;
  
    class LoggerHelper {
     public:
      static void Log(const char*, int, const char* _, ...) __PRINTFLIKE(3, 4) {
        logger_called = true;
      }
    };
  
    FtlLogger logger;
    logger.trace = &LoggerHelper::Log;
    logger.debug = &LoggerHelper::Log;
    logger.info = &LoggerHelper::Log;
    logger.warn = &LoggerHelper::Log;
    logger.error = &LoggerHelper::Log;
  
    ASSERT_TRUE(ftl::InitModules());
    ftl::VolumeOptions options = kDefaultOptions;
    options.flags |= ftl::kReadOnlyInit;
    auto ndm_driver = std::make_unique<NdmRamDriver>(options, logger);
    ASSERT_EQ(nullptr, ndm_driver->Init());
    std::vector<uint8_t> buffer(kPageSize, 0xff);
  
    memcpy(buffer.data(), kControlBlockBadBlocksV2, sizeof(kControlBlockBadBlocksV2));
    ASSERT_EQ(ftl::kNdmOk, ndm_driver->NandWrite(kControlPage0, 1, buffer.data(), kControlOob));
  
    ASSERT_EQ(nullptr, ndm_driver->CreateVolume());
    EXPECT_EQ(2u, ndm_driver->ndm()->num_bad_blks);
    EXPECT_TRUE(logger_called);
  }
  
  }  // namespace
  