// 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/trash_service_impl.h"
#include <limits.h>
#include <string_view>
#include <utility>
#include <vector>
#include "base/check_op.h"
#include "base/containers/span.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/functional/callback.h"
#include "base/strings/escape.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
namespace ash::trash_service {
namespace {
// Represents the expected token on the first line of the .trashinfo file.
constexpr std::string_view kTrashInfoHeaderToken = "[Trash Info]";
// Represents the expected token starting the second line of the .trashinfo
// file.
constexpr std::string_view kPathToken = "Path=";
// Represents the expected token starting the third line of the .trashinfo file.
constexpr std::string_view kDeletionDateToken = "DeletionDate=";
// The "DeletionDate=" line contains 24 bytes representing a well formed
// ISO-8601 date string, e.g. "2022-07-18T10:13:00.000Z".
constexpr size_t kISO8601Size = 24;
// Helper function to invoke the supplied callback with an error and empty
// restore path and deletion date.
void InvokeCallbackWithError(
base::File::Error error,
TrashServiceImpl::ParseTrashInfoFileCallback callback) {
std::move(callback).Run(error, base::FilePath(), base::Time());
}
// Helper function to return `base::File::FILE_ERROR_FAILED` to the supplied
// callback.
void InvokeCallbackWithFailed(
TrashServiceImpl::ParseTrashInfoFileCallback callback) {
InvokeCallbackWithError(base::File::FILE_ERROR_FAILED, std::move(callback));
}
// Extracts and validates the path from a line coming from the `.trashinfo`
// file. Returns an empty path on error.
base::FilePath ValidateAndCreateRestorePath(std::string_view line) {
// The final newline character should already have been stripped.
DCHECK(!line.ends_with('\n'));
if (!line.starts_with(kPathToken)) {
LOG(ERROR) << "Line does not start with '" << kPathToken << "'";
return base::FilePath();
}
line.remove_prefix(kPathToken.size());
const std::string unescaped = base::UnescapeBinaryURLComponent(line);
if (unescaped.size() >= PATH_MAX) {
LOG(ERROR) << "Extracted path is too long";
return base::FilePath();
}
if (unescaped.find('\0') != std::string::npos) {
LOG(ERROR) << "Extracted path contains a NUL byte";
return base::FilePath();
}
if (!base::IsStringUTF8(unescaped)) {
LOG(ERROR) << "Extracted path is not a valid UTF-8 string";
return base::FilePath();
}
const base::FilePath path(std::move(unescaped));
const std::vector<std::string> components = path.GetComponents();
base::span<const std::string> parts = components;
// The first part should be "/".
if (parts.empty() || parts.front() != "/") {
LOG(ERROR) << "Extracted path is not absolute";
return base::FilePath();
}
// Pop the first part.
parts = parts.subspan(1);
if (parts.empty()) {
LOG(ERROR) << "Extracted path is just the root path";
return base::FilePath();
}
// Validate each remaining part.
for (const std::string& part : parts) {
if (part == "." || part == ".." || part.size() > NAME_MAX) {
LOG(ERROR) << "Extracted path contains an invalid component";
return base::FilePath();
}
}
return path;
}
// Extracts and validates the deletion date from a line coming from the
// `.trashinfo` file. Returns a default-created `Time` on error.
base::Time ValidateAndCreateDeletionDate(std::string_view line) {
// The final newline character should already have been stripped.
DCHECK(!line.ends_with('\n'));
if (!line.starts_with(kDeletionDateToken)) {
LOG(ERROR) << "Line does not start with '" << kDeletionDateToken << "'";
return base::Time();
}
line.remove_prefix(kDeletionDateToken.size());
base::Time date;
if (line.size() != kISO8601Size ||
!base::Time::FromUTCString(std::string(line).c_str(), &date)) {
LOG(ERROR) << "Cannot parse date";
return base::Time();
}
return date;
}
} // namespace
TrashServiceImpl::TrashServiceImpl(
mojo::PendingReceiver<mojom::TrashService> receiver) {
receivers_.Add(this, std::move(receiver));
}
TrashServiceImpl::~TrashServiceImpl() = default;
void TrashServiceImpl::ParseTrashInfoFile(base::File trash_info_file,
ParseTrashInfoFileCallback callback) {
if (!trash_info_file.IsValid()) {
InvokeCallbackWithError(trash_info_file.error_details(),
std::move(callback));
return;
}
// Read the file up to the max buffer. In the event of a read error continue
// trying to parse as this may represent the case where the buffer was
// exceeded yet `file_contents` contains valid data after that point so
// continue parsing.
std::string file_contents;
base::ScopedFILE read_only_stream(
base::FileToFILE(std::move(trash_info_file), "r"));
constexpr size_t kMaxSize = kTrashInfoHeaderToken.size() + kPathToken.size() +
PATH_MAX * 3 /* URL-escaping */ +
kDeletionDateToken.size() + kISO8601Size +
3 /* newline characters */;
const bool ok = base::ReadStreamToStringWithMaxSize(read_only_stream.get(),
kMaxSize, &file_contents);
if (!ok && file_contents.size() < kMaxSize) {
LOG(ERROR) << "Cannot read trash info file";
InvokeCallbackWithFailed(std::move(callback));
return;
}
// Split the lines up and ignoring any empty lines in between. Only the first
// 3 non-empty lines are useful to validate again.
std::vector<std::string_view> lines = base::SplitStringPiece(
file_contents, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
if (lines.size() < 3) {
LOG(ERROR) << "Trash info file only contains " << lines.size() << " lines";
InvokeCallbackWithFailed(std::move(callback));
return;
}
// The Trash spec says, "The implementation MUST ignore any other lines in
// this file, except the first line (must be [Trash Info]) and these two
// key/value pairs". Therefore we only iterate over the first 3 lines ignoring
// the remaining.
if (lines[0] != kTrashInfoHeaderToken) {
LOG(ERROR) << "Invalid trash info header: " << lines[0];
InvokeCallbackWithFailed(std::move(callback));
return;
}
base::FilePath restore_path = ValidateAndCreateRestorePath(lines[1]);
if (restore_path.empty()) {
InvokeCallbackWithFailed(std::move(callback));
return;
}
base::Time deletion_date = ValidateAndCreateDeletionDate(lines[2]);
if (deletion_date.is_null()) {
InvokeCallbackWithFailed(std::move(callback));
return;
}
std::move(callback).Run(base::File::FILE_OK, std::move(restore_path),
std::move(deletion_date));
}
} // namespace ash::trash_service