chromium/chromeos/ash/components/trash_service/public/cpp/trash_service_unittest.cc

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

#include "chromeos/ash/components/trash_service/public/cpp/trash_service.h"

#include <limits.h>

#include <memory>
#include <string_view>
#include <utility>
#include <vector>

#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/rand_util.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/mock_callback.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "chromeos/ash/components/trash_service/public/mojom/trash_service.mojom.h"
#include "chromeos/ash/components/trash_service/trash_service_impl.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash::trash_service {

using ::base::test::RunClosure;
using ::testing::_;

namespace {

// A struct with default data used by tests to enable testing invalid fields
// with all remaining data being valid.
struct TrashInfoContents {
  std::string header = "[Trash Info]";
  std::string path_line = "Path=/foo/bar.txt";
  std::string date_line = "DeletionDate=2022-07-18T10:13:00.000Z";

  base::FilePath restore_path{"/foo/bar.txt"};

  std::string ToString() const {
    return base::StrCat({header, "\n", path_line, "\n", date_line});
  }

  base::Time GetDeletionDate() const {
    std::vector<std::string_view> key_value =
        base::SplitStringPiece(std::string_view(date_line), "=",
                               base::WhitespaceHandling::TRIM_WHITESPACE,
                               base::SplitResult::SPLIT_WANT_ALL);
    EXPECT_EQ(2UL, key_value.size());
    base::Time time;
    EXPECT_TRUE(base::Time::FromUTCString(key_value[1].data(), &time));
    return time;
  }
};

}  // namespace

class TrashServiceTest : public ::testing::Test {
 public:
  void SetUp() override {
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
    test_dir_ = temp_dir_.GetPath();

    trash_impl_ = std::make_unique<TrashServiceImpl>(
        trash_service_remote_.BindNewPipeAndPassReceiver());
  }

  void ExpectParsingFailedForFileContents(const std::string& file_contents) {
    const base::FilePath file_path = test_dir_.Append("foo.txt.trashinfo");
    ASSERT_TRUE(base::WriteFile(file_path, file_contents));
    base::File trash_info_file(file_path,
                               base::File::FLAG_OPEN | base::File::FLAG_READ);

    base::MockCallback<ParseTrashInfoCallback> complete_callback;
    base::RunLoop run_loop;
    EXPECT_CALL(complete_callback, Run(base::File::FILE_ERROR_FAILED,
                                       base::FilePath(), base::Time()))
        .WillOnce(RunClosure(run_loop.QuitClosure()));

    trash_impl_->ParseTrashInfoFile(std::move(trash_info_file),
                                    complete_callback.Get());
    run_loop.Run();
  }

  void ExpectParsingSucceedsForFileContents(
      const TrashInfoContents& file_contents) {
    const base::FilePath file_path = test_dir_.Append("foo.txt.trashinfo");
    ASSERT_TRUE(base::WriteFile(file_path, file_contents.ToString()));
    base::File trash_info_file(file_path,
                               base::File::FLAG_OPEN | base::File::FLAG_READ);

    base::MockCallback<ParseTrashInfoCallback> complete_callback;
    base::RunLoop run_loop;
    EXPECT_CALL(complete_callback,
                Run(base::File::FILE_OK, file_contents.restore_path,
                    file_contents.GetDeletionDate()))
        .WillOnce(RunClosure(run_loop.QuitClosure()));

    trash_impl_->ParseTrashInfoFile(std::move(trash_info_file),
                                    complete_callback.Get());
    run_loop.Run();
  }

 protected:
  base::test::TaskEnvironment task_environment_;

  base::ScopedTempDir temp_dir_;
  base::FilePath test_dir_;

  std::unique_ptr<TrashServiceImpl> trash_impl_;
  mojo::Remote<mojom::TrashService> trash_service_remote_;
};

TEST_F(TrashServiceTest, NonexistingFileShouldReturnNotFound) {
  const base::FilePath file_path_does_not_exist =
      test_dir_.Append("foo.txt.trashinfo");
  base::File trash_info_file(file_path_does_not_exist,
                             base::File::FLAG_OPEN | base::File::FLAG_READ);

  base::MockCallback<ParseTrashInfoCallback> complete_callback;
  base::RunLoop run_loop;
  EXPECT_CALL(complete_callback, Run(base::File::FILE_ERROR_NOT_FOUND, _, _))
      .WillOnce(RunClosure(run_loop.QuitClosure()));

  trash_impl_->ParseTrashInfoFile(std::move(trash_info_file),
                                  complete_callback.Get());
  run_loop.Run();
}

TEST_F(TrashServiceTest, PathExceedingMaxAllowableLengthShouldFail) {
  // Create a valid path that exceeds `PATH_MAX`.
  TrashInfoContents file_contents;
  file_contents.restore_path =
      base::FilePath("/foo").Append(std::string(PATH_MAX, 'f')).Append("foo");
  file_contents.path_line =
      base::StrCat({"Path=", file_contents.restore_path.value()});

  // Setup the test file as a well-formed file but with a path that will cause
  // the read buffer to go over.
  ExpectParsingFailedForFileContents(file_contents.ToString());
}

TEST_F(TrashServiceTest, ValidFileWithExtraDataIgnoresOverflow) {
  TrashInfoContents contents;
  const base::FilePath file_path = test_dir_.Append("foo.txt.trashinfo");

  // Append 1024 random bytes to the end of the trashinfo file, this data
  // should be ignored when parsing.
  ASSERT_TRUE(base::WriteFile(file_path,
                              base::StrCat({contents.ToString(), "\n",
                                            base::RandBytesAsString(1024)})));
  base::File trash_info_file(file_path,
                             base::File::FLAG_OPEN | base::File::FLAG_READ);

  base::MockCallback<ParseTrashInfoCallback> complete_callback;
  base::RunLoop run_loop;
  EXPECT_CALL(complete_callback, Run(base::File::FILE_OK, contents.restore_path,
                                     contents.GetDeletionDate()))
      .WillOnce(RunClosure(run_loop.QuitClosure()));

  trash_impl_->ParseTrashInfoFile(std::move(trash_info_file),
                                  complete_callback.Get());
  run_loop.Run();
}

TEST_F(TrashServiceTest, InvalidTrashInfoHeaderScenarios) {
  // Invalid header.
  {
    TrashInfoContents contents;
    contents.header = "invalid header";
    ExpectParsingFailedForFileContents(contents.ToString());
  }

  // Valid structure but misspelled.
  {
    TrashInfoContents contents;
    contents.header = "[Trash Imfo]";
    ExpectParsingFailedForFileContents(contents.ToString());
  }

  // First line is the "Path=" key value pair.
  {
    TrashInfoContents contents;
    std::string file_contents =
        base::StrCat({contents.path_line, "\n", contents.date_line});
    ExpectParsingFailedForFileContents(file_contents);
  }
}

TEST_F(TrashServiceTest, InvalidPathKeyValueScenarios) {
  // No path key value pair.
  {
    TrashInfoContents contents;
    std::string file_contents =
        base::StrCat({contents.header, "\n", contents.date_line});
    ExpectParsingFailedForFileContents(file_contents);
  }

  // Create a too-long path where each component is valid.
  std::string long_path;
  for (int i = 0; long_path.size() < PATH_MAX; ++i) {
    long_path += '/';
    long_path.append(200, static_cast<char>('a' + i));
  }
  long_path.resize(PATH_MAX);

  const std::vector<std::string> lines = {{
      "/foo/bar",                // Missing "Path=" key.
      "Patn=/foo/bar",           // Misspelled "Path=" key.
      "Path=/foo/../bar",        // Path references parent.
      "Path=/foo/%2e%2E/bar",    // Path references parent in a sneaky way.
      "Path=/foo/./bar",         // Path references current dir.
      "Path=/foo/%2e/bar",       // Path references current dir in a sneaky way.
      "Path=relative/path.txt",  // Relative file path.
      "Path=",                   // Empty path.
      "Path=bar"                 // Relative folder path.
      "Path=foo/bar"             // Relative path.
      "Path=/",                  // Root path.
      "Path=%2f",                // Root path in a sneaky way.
      "Path=/////",              // Root path.
      "Path=//server/foo/bar",   // UNC-style path.
      "Path=/foo%00/bar",        // Embedded NUL byte.
      "Path=/foo%ff/bar",        // Non UTF-8.
      base::StrCat(
          {"Path=/", std::string(NAME_MAX + 1, 'x')}),  // Long component.
      base::StrCat({"Path=", long_path}),               // Long path.
  }};

  for (const std::string& line : lines) {
    TrashInfoContents contents;
    contents.path_line = line;
    ExpectParsingFailedForFileContents(contents.ToString());
  }
}

TEST_F(TrashServiceTest, InvalidDeletionDateKeyValueScenarios) {
  // No deletion date key value pair.
  {
    TrashInfoContents contents;
    std::string file_contents =
        base::StrCat({contents.header, "\n", contents.path_line});
    ExpectParsingFailedForFileContents(file_contents);
  }

  const std::vector<std::string> kInvalidDeletionDates = {{
      "2022-07-18T10:13:00.000Z",              // Missing "DeletionDate=" key.
      "DeletedDate=2022-07-18T10:13:00.000Z",  // Misspelled "DeletionDate="
                                               // key.
      "DeletionDate=2022-07-1810:13:00.000Z",  // Not the required size (missing
                                               // the T character for time).
      "DeletionDate=abcdefghijklmnopqrstuvw",  // Same ISO-8601 size but invalid
                                               // date.
  }};
  for (const auto& date : kInvalidDeletionDates) {
    TrashInfoContents contents;
    contents.date_line = date;
    ExpectParsingFailedForFileContents(contents.ToString());
  }
}

TEST_F(TrashServiceTest, ValidPathKeyValueScenarios) {
  TrashInfoContents contents;

  const std::vector<std::string> kValidPaths = {{
      "Path=/foo/bar.txt",      // Happy path.
      "   Path=/foo/bar.txt",   // Leading whitespace is ignored.
      "Path=/foo/bar.txt    ",  // Trailing whitespace is ignored.
  }};
  for (const auto& path : kValidPaths) {
    contents.path_line = path;
    ExpectParsingSucceedsForFileContents(contents);
  }

  contents.path_line = "Path=/%09new%0aline%25%20";
  contents.restore_path = base::FilePath("/\tnew\nline% ");
  ExpectParsingSucceedsForFileContents(contents);
}

TEST_F(TrashServiceTest, ValidDeletionDateKeyValueScenarios) {
  const std::vector<std::string> kValidDeletionDates = {{
      "DeletionDate=2022-07-18T10:13:00.000Z",      // Happy path.
      "   DeletionDate=2022-07-18T10:13:00.000Z",   // Leading whitespace is
                                                    // ignored.
      "DeletionDate=2022-07-18T10:13:00.000Z    ",  // Trailing whitespace is
                                                    // ignored.
  }};
  for (const auto& date : kValidDeletionDates) {
    TrashInfoContents contents;
    contents.date_line = date;
    ExpectParsingSucceedsForFileContents(contents);
  }
}

}  // namespace ash::trash_service