chromium/chrome/browser/smart_card/smart_card_reader_tracker_impl_unittest.cc

// Copyright 2024 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/browser/smart_card/smart_card_reader_tracker_impl.h"
#include "base/run_loop.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/time/time_override.h"
#include "content/browser/smart_card/mock_smart_card_context_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/smart_card/smart_card.mojom.h"

using base::test::TestFuture;
using content::MockSmartCardContextFactory;
using device::mojom::SmartCardContext;
using device::mojom::SmartCardError;
using device::mojom::SmartCardReaderStateFlags;
using device::mojom::SmartCardReaderStateIn;
using device::mojom::SmartCardReaderStateOut;
using device::mojom::SmartCardReaderStateOutPtr;
using device::mojom::SmartCardResult;
using device::mojom::SmartCardStatusChangeResult;
using device::mojom::SmartCardSuccess;

using testing::_;
using testing::ElementsAre;
using testing::InSequence;
using testing::StrictMock;
using testing::UnorderedElementsAre;

// Used by Google Test to print human-readable error messages involving
// ReaderInfo comparisons.
void PrintTo(const SmartCardReaderTracker::ReaderInfo& reader,
             std::ostream* os) {
  *os << "ReaderInfo(" << reader.name;

  if (reader.unavailable) {
    *os << ", unavailable";
  }

  if (reader.empty) {
    *os << ", empty";
  }

  if (reader.present) {
    *os << ", present";
  }

  if (reader.exclusive) {
    *os << ", exclusive";
  }

  if (reader.inuse) {
    *os << ", inuse";
  }

  if (reader.mute) {
    *os << ", mute";
  }

  if (reader.unpowered) {
    *os << ", unpowered";
  }

  *os << ", event_count=" << reader.event_count;

  *os << ", ATR{";
  bool first = true;
  for (uint8_t num : reader.answer_to_reset) {
    if (!first) {
      *os << ",";
    }
    // Treat it as a number instead of as a char.
    *os << unsigned(num);
    first = false;
  }
  *os << "})";
}

namespace {

void ExpectUnaware(const SmartCardReaderStateIn& state_in) {
  // This is the only field expected to be set/true.
  EXPECT_TRUE(state_in.current_state->unaware);

  // All the rest should be false or 0.
  EXPECT_FALSE(state_in.current_state->ignore);
  EXPECT_FALSE(state_in.current_state->changed);
  EXPECT_FALSE(state_in.current_state->unknown);
  EXPECT_FALSE(state_in.current_state->unavailable);
  EXPECT_FALSE(state_in.current_state->empty);
  EXPECT_FALSE(state_in.current_state->present);
  EXPECT_FALSE(state_in.current_state->exclusive);
  EXPECT_FALSE(state_in.current_state->inuse);
  EXPECT_FALSE(state_in.current_state->mute);
  EXPECT_FALSE(state_in.current_state->unpowered);
  EXPECT_EQ(state_in.current_count, 0);
}

void ExpectEmpty(const SmartCardReaderStateFlags& state_flags) {
  EXPECT_FALSE(state_flags.unaware);
  EXPECT_FALSE(state_flags.ignore);
  EXPECT_FALSE(state_flags.changed);
  EXPECT_FALSE(state_flags.unknown);
  EXPECT_FALSE(state_flags.unavailable);

  // The only flag expected to be true.
  EXPECT_TRUE(state_flags.empty);

  EXPECT_FALSE(state_flags.present);
  EXPECT_FALSE(state_flags.exclusive);
  EXPECT_FALSE(state_flags.inuse);
  EXPECT_FALSE(state_flags.mute);
  EXPECT_FALSE(state_flags.unpowered);
}

void ExpectInUse(const SmartCardReaderStateFlags& state_flags) {
  EXPECT_FALSE(state_flags.unaware);
  EXPECT_FALSE(state_flags.ignore);
  EXPECT_FALSE(state_flags.changed);
  EXPECT_FALSE(state_flags.unknown);
  EXPECT_FALSE(state_flags.unavailable);
  EXPECT_FALSE(state_flags.empty);

  // There's a card present in the reader.
  EXPECT_TRUE(state_flags.present);

  EXPECT_FALSE(state_flags.exclusive);

  // And that card is in use.
  EXPECT_TRUE(state_flags.inuse);

  EXPECT_FALSE(state_flags.mute);
  EXPECT_FALSE(state_flags.unpowered);
}

enum class StateFlags {
  kEmpty,
  kNowEmpty,
  kInUse,
  kNowUnknown,
};

enum class ReaderState {
  kEmpty,
  kInUse,
};

SmartCardReaderStateOutPtr CreateStateOut(std::string name,
                                          StateFlags state,
                                          uint16_t event_count = 0,
                                          std::vector<uint8_t> atr = {}) {
  auto state_flags = SmartCardReaderStateFlags::New();
  switch (state) {
    case StateFlags::kEmpty:
      state_flags->empty = true;
      break;
    case StateFlags::kNowEmpty:
      state_flags->changed = true;
      state_flags->empty = true;
      break;
    case StateFlags::kInUse:
      state_flags->present = true;
      state_flags->inuse = true;
      break;
    case StateFlags::kNowUnknown:
      state_flags->changed = true;
      state_flags->unknown = true;
      break;
  }

  return SmartCardReaderStateOut::New(std::move(name), std::move(state_flags),
                                      event_count, std::move(atr));
}

SmartCardReaderTracker::ReaderInfo CreateReaderInfo(
    std::string name,
    ReaderState state,
    uint16_t event_count = 0,
    std::vector<uint8_t> atr = {}) {
  SmartCardReaderTracker::ReaderInfo info;
  info.name = std::move(name);
  info.event_count = event_count;
  info.answer_to_reset = std::move(atr);

  switch (state) {
    case ReaderState::kEmpty:
      info.empty = true;
      break;
    case ReaderState::kInUse:
      info.present = true;
      info.inuse = true;
      break;
  }

  return info;
}

void ReportStateOut(SmartCardContext::GetStatusChangeCallback callback,
                    SmartCardReaderStateOutPtr s) {
  std::vector<SmartCardReaderStateOutPtr> states_out;
  states_out.push_back(std::move(s));

  auto result =
      SmartCardStatusChangeResult::NewReaderStates(std::move(states_out));

  std::move(callback).Run(std::move(result));
}

void ReportStateOut(SmartCardContext::GetStatusChangeCallback callback,
                    SmartCardReaderStateOutPtr s1,
                    SmartCardReaderStateOutPtr s2) {
  std::vector<SmartCardReaderStateOutPtr> states_out;
  states_out.push_back(std::move(s1));
  states_out.push_back(std::move(s2));

  auto result =
      SmartCardStatusChangeResult::NewReaderStates(std::move(states_out));

  std::move(callback).Run(std::move(result));
}

class SmartCardReaderTrackerImplTest : public testing::Test {
 protected:
  base::test::TaskEnvironment task_environment_;
};

class MockTrackerObserver : public SmartCardReaderTracker::Observer {
 public:
  MOCK_METHOD(void,
              OnReaderRemoved,
              (const std::string& reader_name),
              (override));

  MOCK_METHOD(void,
              OnReaderChanged,
              (const SmartCardReaderTracker::ReaderInfo& reader_info),
              (override));

  MOCK_METHOD(void, OnError, (SmartCardError error), (override));
};

TEST_F(SmartCardReaderTrackerImplTest, ReaderChanged) {
  MockSmartCardContextFactory mock_context_factory;
  SmartCardReaderTrackerImpl tracker(mock_context_factory.GetRemote());
  StrictMock<MockTrackerObserver> observer;

  {
    InSequence s;

    // Request what readers are currently available.
    mock_context_factory.ExpectListReaders({"Reader A", "Reader B"});

    // Request the state of each of those readers.
    EXPECT_CALL(mock_context_factory, GetStatusChange(_, _, _))
        .WillOnce(
            [](base::TimeDelta timeout,
               std::vector<device::mojom::SmartCardReaderStateInPtr> states_in,
               SmartCardContext::GetStatusChangeCallback callback) {
              ASSERT_EQ(states_in.size(), 2U);

              ASSERT_EQ(states_in[0]->reader, "Reader A");
              ExpectUnaware(*states_in[0]);

              ASSERT_EQ(states_in[1]->reader, "Reader B");
              ExpectUnaware(*states_in[1]);

              ReportStateOut(
                  std::move(callback),
                  CreateStateOut("Reader A", StateFlags::kEmpty),
                  CreateStateOut("Reader B", StateFlags::kInUse,
                                 /*event_count=*/1,
                                 std::vector<uint8_t>({1u, 2u, 3u, 4u})));
            });

    // Request to be notified of state changes on those readers.
    // SmartCardContext reports that "Reader B" has changed (card was removed,
    // thus it's now empty).
    EXPECT_CALL(mock_context_factory, GetStatusChange(_, _, _))
        .WillOnce(
            [](base::TimeDelta timeout,
               std::vector<device::mojom::SmartCardReaderStateInPtr> states_in,
               SmartCardContext::GetStatusChangeCallback callback) {
              ASSERT_EQ(states_in.size(), 2U);

              EXPECT_EQ(states_in[0]->reader, "Reader A");
              ExpectEmpty(*states_in[0]->current_state);
              EXPECT_EQ(states_in[0]->current_count, 0);

              EXPECT_EQ(states_in[1]->reader, "Reader B");
              ExpectInUse(*states_in[1]->current_state);
              EXPECT_EQ(states_in[1]->current_count, 1);

              ReportStateOut(std::move(callback),
                             CreateStateOut("Reader A", StateFlags::kEmpty),
                             // Reader B has changed. It's now empty as well.
                             CreateStateOut("Reader B", StateFlags::kNowEmpty,
                                            /*event_count=*/2));
            });

    EXPECT_CALL(observer, OnReaderChanged(
                              CreateReaderInfo("Reader B", ReaderState::kEmpty,
                                               /*event_count=*/2, {})));

    ////
    // Now rinse and repeat

    // Request what readers are currently available.
    // Still the same readers.
    mock_context_factory.ExpectListReaders({"Reader A", "Reader B"});

    // Since ListReaders did not return any reader unknown to the tracker,
    // it will now skip to waiting to be notified on any changes.
    EXPECT_CALL(mock_context_factory, GetStatusChange(_, _, _))
        .WillOnce(
            [](base::TimeDelta timeout,
               std::vector<device::mojom::SmartCardReaderStateInPtr> states_in,
               SmartCardContext::GetStatusChangeCallback callback) {
              ASSERT_EQ(states_in.size(), 2U);

              EXPECT_EQ(states_in[0]->reader, "Reader A");
              ExpectEmpty(*states_in[0]->current_state);
              EXPECT_EQ(states_in[0]->current_count, 0);

              // Note that the tracker now states that Reader B is empty as
              // well.
              EXPECT_EQ(states_in[1]->reader, "Reader B");
              ExpectEmpty(*states_in[1]->current_state);
              EXPECT_EQ(states_in[1]->current_count, 2);

              std::vector<SmartCardReaderStateOutPtr> states_out;

              // Give an error, which will cause the tracker to go back into
              // Uninitialized state, making no further queries. Letting us end
              // this test.
              std::move(callback).Run(SmartCardStatusChangeResult::NewError(
                  SmartCardError::kNoService));
            });

    EXPECT_CALL(observer, OnError(SmartCardError::kNoService));

    // This unrecoverable failure should cause the tracker to drop its smart
    // card context and stop tracking.
    EXPECT_CALL(mock_context_factory, ContextDisconnected());
  }

  TestFuture<std::optional<std::vector<SmartCardReaderTracker::ReaderInfo>>>
      start_future;
  tracker.Start(&observer, start_future.GetCallback());

  std::optional<std::vector<SmartCardReaderTracker::ReaderInfo>> readers =
      start_future.Take();
  ASSERT_TRUE(readers.has_value());

  EXPECT_THAT(*readers,
              UnorderedElementsAre(
                  CreateReaderInfo("Reader A", ReaderState::kEmpty),
                  CreateReaderInfo("Reader B", ReaderState::kInUse,
                                   /*event_count=*/1,
                                   std::vector<uint8_t>({1u, 2u, 3u, 4u}))));

  base::RunLoop run_loop;
  run_loop.RunUntilIdle();
}

// If Start() is called while tracking is already taking place and
// kMinRefreshInterval has elapsed since tracking has started, it should cause
// tracking to be restarted. ie, a new list of readers being fetched, etc.
TEST_F(SmartCardReaderTrackerImplTest, Restart) {
  MockSmartCardContextFactory mock_context_factory;
  SmartCardReaderTrackerImpl tracker(mock_context_factory.GetRemote());
  TestFuture<SmartCardContext::GetStatusChangeCallback>
      tracking_get_status_callback;
  StrictMock<MockTrackerObserver> observer;

  {
    InSequence s;

    // Request what readers are currently available.
    mock_context_factory.ExpectListReaders({"Reader A", "Reader B"});

    // Request the state of each of those readers.
    EXPECT_CALL(mock_context_factory, GetStatusChange(_, _, _))
        .WillOnce(
            [](base::TimeDelta timeout,
               std::vector<device::mojom::SmartCardReaderStateInPtr> states_in,
               SmartCardContext::GetStatusChangeCallback callback) {
              ASSERT_EQ(states_in.size(), 2U);
              ASSERT_EQ(states_in[0]->reader, "Reader A");
              ExpectUnaware(*states_in[0]);

              ASSERT_EQ(states_in[1]->reader, "Reader B");
              ExpectUnaware(*states_in[1]);

              ReportStateOut(
                  std::move(callback),
                  CreateStateOut("Reader A", StateFlags::kEmpty),
                  CreateStateOut("Reader B", StateFlags::kInUse,
                                 /*event_count=*/1,
                                 std::vector<uint8_t>({1u, 2u, 3u, 4u})));
            });

    // Request to be notified of state changes on those readers.
    EXPECT_CALL(mock_context_factory, GetStatusChange(_, _, _))
        .WillOnce(
            [&tracking_get_status_callback](
                base::TimeDelta timeout,
                std::vector<device::mojom::SmartCardReaderStateInPtr> states_in,
                SmartCardContext::GetStatusChangeCallback callback) {
              ASSERT_EQ(states_in.size(), 2U);

              EXPECT_EQ(states_in[0]->reader, "Reader A");
              ExpectEmpty(*states_in[0]->current_state);
              EXPECT_EQ(states_in[0]->current_count, 0);

              EXPECT_EQ(states_in[1]->reader, "Reader B");
              ExpectInUse(*states_in[1]->current_state);
              EXPECT_EQ(states_in[1]->current_count, 1);

              // Handle that in the upcoming Cancel() call.
              tracking_get_status_callback.SetValue(std::move(callback));
            });

    EXPECT_CALL(mock_context_factory, Cancel(_))
        .WillOnce([&tracking_get_status_callback](
                      SmartCardContext::CancelCallback callback) {
          // The cancel call succeeded.
          std::move(callback).Run(
              SmartCardResult::NewSuccess(SmartCardSuccess::kOk));

          // And the ongoing GetStatusChange() PC/SC call was correspondingly
          // canceled.
          tracking_get_status_callback.Take().Run(
              SmartCardStatusChangeResult::NewError(
                  SmartCardError::kCancelled));
        });

    ////
    // Now rinse and repeat

    // Request what readers are currently available.
    // Still the same readers.
    mock_context_factory.ExpectListReaders({"Reader A", "Reader B"});

    // Since ListReaders did not return any reader unknown to the tracker,
    // it will now skip to waiting to be notified on any changes.
    EXPECT_CALL(mock_context_factory, GetStatusChange(_, _, _))
        .WillOnce(
            [](base::TimeDelta timeout,
               std::vector<device::mojom::SmartCardReaderStateInPtr> states_in,
               SmartCardContext::GetStatusChangeCallback callback) {
              ASSERT_EQ(states_in.size(), 2U);

              EXPECT_EQ(states_in[0]->reader, "Reader A");
              ExpectEmpty(*states_in[0]->current_state);
              EXPECT_EQ(states_in[0]->current_count, 0);

              EXPECT_EQ(states_in[1]->reader, "Reader B");
              ExpectInUse(*states_in[1]->current_state);
              EXPECT_EQ(states_in[1]->current_count, 1);

              std::vector<SmartCardReaderStateOutPtr> states_out;

              // Give an error, which will cause the tracker to go back into
              // Uninitialized state, making no further queries. Letting us end
              // this test.
              std::move(callback).Run(
                  device::mojom::SmartCardStatusChangeResult::NewError(
                      SmartCardError::kNoService));
            });

    // The error given by the last GetStatusChange() call.
    EXPECT_CALL(observer, OnError(SmartCardError::kNoService));

    // This unrecoverable failure should cause the tracker to drop its smart
    // card context and stop tracking.
    EXPECT_CALL(mock_context_factory, ContextDisconnected());
  }

  {
    base::subtle::ScopedTimeClockOverrides time_override(
        []() { return base::Time::FromSecondsSinceUnixEpoch(0); }, nullptr,
        nullptr);

    TestFuture<std::optional<std::vector<SmartCardReaderTracker::ReaderInfo>>>
        start_future;
    // The first start() call, at t0.
    tracker.Start(&observer, start_future.GetCallback());

    std::optional<std::vector<SmartCardReaderTracker::ReaderInfo>> readers =
        start_future.Take();
    ASSERT_TRUE(readers.has_value());
    EXPECT_THAT(*readers,
                UnorderedElementsAre(
                    CreateReaderInfo("Reader A", ReaderState::kEmpty),
                    CreateReaderInfo("Reader B", ReaderState::kInUse,
                                     /*event_count=*/1,
                                     std::vector<uint8_t>({1u, 2u, 3u, 4u}))));
  }

  // Wait until SmartCardReaderTracerImpl is in the Tracking state, waiting
  // for the GetStatusChange() call result.
  ASSERT_TRUE(tracking_get_status_callback.Wait());

  {
    base::subtle::ScopedTimeClockOverrides time_override(
        []() {
          return base::Time::FromSecondsSinceUnixEpoch(
              SmartCardReaderTrackerImpl::kMinRefreshInterval.InSecondsF() * 2);
        },
        nullptr, nullptr);

    TestFuture<std::optional<std::vector<SmartCardReaderTracker::ReaderInfo>>>
        start_future;
    // The second start() call at t0 + (kMinRefreshInterval*2).
    // Will make the tracker cancel the currently outstanding GetStatusChange()
    // request and restart from the ListReaders() call.
    tracker.Start(&observer, start_future.GetCallback());

    std::optional<std::vector<SmartCardReaderTracker::ReaderInfo>> readers =
        start_future.Take();
    ASSERT_TRUE(readers.has_value());
    EXPECT_THAT(*readers,
                UnorderedElementsAre(
                    CreateReaderInfo("Reader A", ReaderState::kEmpty),
                    CreateReaderInfo("Reader B", ReaderState::kInUse,
                                     /*event_count=*/1,
                                     std::vector<uint8_t>({1u, 2u, 3u, 4u}))));
  }

  base::RunLoop run_loop;
  run_loop.RunUntilIdle();
}

// Test that tracker will cancel its outstanding GetStatusChange() request and
// stop tracking when the last observer leaves.
TEST_F(SmartCardReaderTrackerImplTest, StopWhenTracking) {
  MockSmartCardContextFactory mock_context_factory;
  SmartCardReaderTrackerImpl tracker(mock_context_factory.GetRemote());
  TestFuture<SmartCardContext::GetStatusChangeCallback>
      tracking_get_status_callback;
  StrictMock<MockTrackerObserver> observer;

  {
    InSequence s;

    // Request what readers are currently available.
    mock_context_factory.ExpectListReaders({"Reader A"});

    // Request the state of each of those readers.
    EXPECT_CALL(mock_context_factory, GetStatusChange(_, _, _))
        .WillOnce(
            [](base::TimeDelta timeout,
               std::vector<device::mojom::SmartCardReaderStateInPtr> states_in,
               SmartCardContext::GetStatusChangeCallback callback) {
              ASSERT_EQ(states_in.size(), 1U);
              ASSERT_EQ(states_in[0]->reader, "Reader A");
              ExpectUnaware(*states_in[0]);

              ReportStateOut(std::move(callback),
                             CreateStateOut("Reader A", StateFlags::kEmpty));
            });

    // Request to be notified of state changes on those readers.
    EXPECT_CALL(mock_context_factory, GetStatusChange(_, _, _))
        .WillOnce(
            [&tracking_get_status_callback](
                base::TimeDelta timeout,
                std::vector<device::mojom::SmartCardReaderStateInPtr> states_in,
                SmartCardContext::GetStatusChangeCallback callback) {
              ASSERT_EQ(states_in.size(), 1U);

              EXPECT_EQ(states_in[0]->reader, "Reader A");
              ExpectEmpty(*states_in[0]->current_state);
              EXPECT_EQ(states_in[0]->current_count, 0);

              // Handle that in the upcoming Cancel() call.
              tracking_get_status_callback.SetValue(std::move(callback));
            });

    // When tracker.Stop() is called, the tracker should cancel the outstanding
    // GetStatusChange() request.
    EXPECT_CALL(mock_context_factory, Cancel(_))
        .WillOnce([&tracking_get_status_callback](
                      SmartCardContext::CancelCallback callback) {
          // The cancel call succeeded.
          std::move(callback).Run(
              SmartCardResult::NewSuccess(SmartCardSuccess::kOk));

          // And the ongoing GetStatusChange() PC/SC call was correspondingly
          // canceled.
          tracking_get_status_callback.Take().Run(
              SmartCardStatusChangeResult::NewError(
                  SmartCardError::kCancelled));
        });

    // Tracker should then drop its smart card context as it will no longer
    // track readers.
    EXPECT_CALL(mock_context_factory, ContextDisconnected());
  }

  // Start()
  {
    TestFuture<std::optional<std::vector<SmartCardReaderTracker::ReaderInfo>>>
        start_future;
    tracker.Start(&observer, start_future.GetCallback());
    ASSERT_TRUE(start_future.Wait());
  }

  // Wait until the tracker is in Tracking state, with an outstanding
  // GetStatusChange request.
  ASSERT_TRUE(tracking_get_status_callback.Wait());

  // Then Stop()
  tracker.Stop(&observer);

  base::RunLoop run_loop;
  run_loop.RunUntilIdle();
}

TEST_F(SmartCardReaderTrackerImplTest, ReaderRemoved) {
  MockSmartCardContextFactory mock_context_factory;
  SmartCardReaderTrackerImpl tracker(mock_context_factory.GetRemote());
  StrictMock<MockTrackerObserver> observer;

  {
    InSequence s;

    // Request what readers are currently available.
    mock_context_factory.ExpectListReaders({"Reader A"});

    // Request the state of each of those readers.
    EXPECT_CALL(mock_context_factory, GetStatusChange(_, _, _))
        .WillOnce(
            [](base::TimeDelta timeout,
               std::vector<device::mojom::SmartCardReaderStateInPtr> states_in,
               SmartCardContext::GetStatusChangeCallback callback) {
              ASSERT_EQ(states_in.size(), 1U);
              ASSERT_EQ(states_in[0]->reader, "Reader A");
              ExpectUnaware(*states_in[0]);

              ReportStateOut(std::move(callback),
                             CreateStateOut("Reader A", StateFlags::kEmpty));
            });

    // Request to be notified of state changes on those readers.
    EXPECT_CALL(mock_context_factory, GetStatusChange(_, _, _))
        .WillOnce(
            [](base::TimeDelta timeout,
               std::vector<device::mojom::SmartCardReaderStateInPtr> states_in,
               SmartCardContext::GetStatusChangeCallback callback) {
              ASSERT_EQ(states_in.size(), 1U);

              EXPECT_EQ(states_in[0]->reader, "Reader A");
              ExpectEmpty(*states_in[0]->current_state);
              EXPECT_EQ(states_in[0]->current_count, 0);

              // Reader A has been removed. It's now unknown.
              ReportStateOut(
                  std::move(callback),
                  CreateStateOut("Reader A", StateFlags::kNowUnknown));
            });

    // Tracker will notify observers about this removal.
    EXPECT_CALL(observer, OnReaderRemoved("Reader A"));

    // As there are no readers left, tracker should stop on its own and drop its
    // smart card context.
    EXPECT_CALL(mock_context_factory, ContextDisconnected());
  }

  // Start()
  {
    TestFuture<std::optional<std::vector<SmartCardReaderTracker::ReaderInfo>>>
        start_future;
    tracker.Start(&observer, start_future.GetCallback());
    ASSERT_TRUE(start_future.Wait());
  }

  base::RunLoop run_loop;
  run_loop.RunUntilIdle();
}

}  // namespace