1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
|
// 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
|