blob: 63f04b35a82fc9b1f693e58577fd89ab98487064 [file] [log] [blame]
// Copyright 2018 The Chromium 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 "chrome/browser/media/webrtc/webrtc_event_log_manager_common.h"
#include <cctype>
#include <limits>
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/strings/stringprintf.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "base/unguessable_token.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_process_host.h"
#include "third_party/zlib/zlib.h"
namespace webrtc_event_logging {
using BrowserContextId = WebRtcEventLogPeerConnectionKey::BrowserContextId;
const size_t kWebRtcEventLogManagerUnlimitedFileSize = 0;
const size_t kWebRtcEventLogIdLength = 32;
// Be careful not to change these without updating the number of characters
// reserved in the filename. See kWebAppIdLength.
const size_t kMinWebRtcEventLogWebAppId = 1;
const size_t kMaxWebRtcEventLogWebAppId = 99;
// Sentinel value for an invalid web-app ID.
const size_t kInvalidWebRtcEventLogWebAppId = 0;
static_assert(kInvalidWebRtcEventLogWebAppId < kMinWebRtcEventLogWebAppId ||
kInvalidWebRtcEventLogWebAppId > kMaxWebRtcEventLogWebAppId,
"Sentinel value must be distinct from legal values.");
const char kRemoteBoundWebRtcEventLogFileNamePrefix[] = "webrtc_event_log";
// Important! These values may be relied on by web-apps. Do not change.
const char kStartRemoteLoggingFailureFeatureDisabled[] = "Feature disabled.";
const char kStartRemoteLoggingFailureUnlimitedSizeDisallowed[] =
"Unlimited size disallowed.";
const char kStartRemoteLoggingFailureMaxSizeTooSmall[] = "Max size too small.";
const char kStartRemoteLoggingFailureMaxSizeTooLarge[] =
"Excessively large max log size.";
const char kStartRemoteLoggingFailureIllegalWebAppId[] = "Illegal web-app ID.";
const char kStartRemoteLoggingFailureUnknownOrInactivePeerConnection[] =
"Unknown or inactive peer connection.";
const char kStartRemoteLoggingFailureAlreadyLogging[] = "Already logging.";
const char kStartRemoteLoggingFailureGeneric[] = "Unspecified error.";
const BrowserContextId kNullBrowserContextId =
reinterpret_cast<BrowserContextId>(nullptr);
namespace {
constexpr int kDefaultMemLevel = 8;
constexpr size_t kGzipHeaderBytes = 15;
constexpr size_t kGzipFooterBytes = 10;
constexpr size_t kWebAppIdLength = 2;
// Tracks budget over a resource (such as bytes allowed in a file, etc.).
// Allows an unlimited budget.
class Budget {
public:
// If !max.has_value(), the budget is unlimited.
explicit Budget(base::Optional<size_t> max) : max_(max), current_(0) {}
// Check whether the budget allows consuming an additional |consumed| of
// the resource.
bool ConsumeAllowed(size_t consumed) const {
if (!max_.has_value()) {
return true;
}
DCHECK_LE(current_, max_.value());
const size_t after_consumption = current_ + consumed;
if (after_consumption < current_) {
return false; // Wrap-around.
} else if (after_consumption > max_.value()) {
return false; // Budget exceeded.
} else {
return true;
}
}
// Checks whether the budget has been completely used up.
bool Exhausted() const { return !ConsumeAllowed(0); }
// Consume an additional |consumed| of the resource.
void Consume(size_t consumed) {
DCHECK(ConsumeAllowed(consumed));
current_ += consumed;
}
private:
const base::Optional<size_t> max_;
size_t current_;
};
// Writes a log to a file while observing a maximum size.
class BaseLogFileWriter : public LogFileWriter {
public:
// If !max_file_size_bytes.has_value(), an unlimited writer is created.
// If it has a value, it must be at least MinFileSizeBytes().
BaseLogFileWriter(const base::FilePath& path,
base::Optional<size_t> max_file_size_bytes);
~BaseLogFileWriter() override;
bool Init() override;
const base::FilePath& path() const override;
bool MaxSizeReached() const override;
bool Write(const std::string& input) override;
bool Close() override;
void Delete() override;
protected:
// * Logs are created PRE_INIT.
// * If Init() is successful (potentially writing some header to the log),
// the log becomes ACTIVE.
// * Any error puts the log into an unrecoverable ERRORED state. When an
// errored file is Close()-ed, it is deleted.
// * If Write() is ever denied because of budget constraintss, the file
// becomes FULL. Only metadata is then allowed (subject to its own budget).
// * Closing an ACTIVE or FULL file puts it into CLOSED, at which point the
// file may be used. (Note that closing itself might also yield an error,
// which would put the file into ERRORED, then deleted.)
// * Closed files may be DELETED.
enum class State { PRE_INIT, ACTIVE, FULL, CLOSED, ERRORED, DELETED };
// Setter/getter for |state_|.
void SetState(State state);
State state() const { return state_; }
// Checks whether the budget allows writing an additional |bytes|.
bool WithinBudget(size_t bytes) const;
// Writes |input| to the file.
// May only be called on ACTIVE or FULL files (for FULL files, only metadata
// such as compression footers, etc., may be written; the budget must still
// be respected).
// It's up to the caller to respect the budget; this will DCHECK on it.
// Returns |true| if writing was successful. |false| indicates an
// unrecoverable error; the file must be discarded.
bool WriteInternal(const std::string& input, bool metadata);
// Finalizes the file (writes metadata such as compression footer, if any).
// Reports whether the file was successfully finalized. Those which weren't
// should be discarded.
virtual bool Finalize();
private:
scoped_refptr<base::SequencedTaskRunner> task_runner_;
const base::FilePath path_;
base::File file_; // Populated by Init().
State state_;
Budget budget_;
};
BaseLogFileWriter::BaseLogFileWriter(const base::FilePath& path,
base::Optional<size_t> max_file_size_bytes)
: task_runner_(base::SequencedTaskRunnerHandle::Get()),
path_(path),
state_(State::PRE_INIT),
budget_(max_file_size_bytes) {}
BaseLogFileWriter::~BaseLogFileWriter() {
if (!task_runner_->RunsTasksInCurrentSequence()) {
// Chrome shut-down. The original task_runner_ is no longer running, so
// no risk of concurrent access or races.
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
task_runner_ = base::SequencedTaskRunnerHandle::Get();
}
if (state() != State::CLOSED && state() != State::DELETED) {
Close();
}
}
bool BaseLogFileWriter::Init() {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
DCHECK_EQ(state(), State::PRE_INIT);
// TODO(crbug.com/775415): Use a temporary filename which will indicate
// incompletion, and rename to something that is eligible for upload only
// on an orderly and successful Close().
// Attempt to create the file.
constexpr int file_flags = base::File::FLAG_CREATE | base::File::FLAG_WRITE |
base::File::FLAG_EXCLUSIVE_WRITE;
file_.Initialize(path_, file_flags);
if (!file_.IsValid() || !file_.created()) {
LOG(WARNING) << "Couldn't create remote-bound WebRTC event log file.";
if (!base::DeleteFile(path_, /*recursive=*/false)) {
LOG(ERROR) << "Failed to delete " << path_ << ".";
}
SetState(State::ERRORED);
return false;
}
SetState(State::ACTIVE);
return true;
}
const base::FilePath& BaseLogFileWriter::path() const {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
return path_;
}
bool BaseLogFileWriter::MaxSizeReached() const {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
DCHECK_EQ(state(), State::ACTIVE);
return !WithinBudget(1);
}
bool BaseLogFileWriter::Write(const std::string& input) {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
DCHECK_EQ(state(), State::ACTIVE);
DCHECK(!MaxSizeReached());
if (input.empty()) {
return true;
}
if (!WithinBudget(input.length())) {
SetState(State::FULL);
return false;
}
const bool did_write = WriteInternal(input, /*metadata=*/false);
if (!did_write) {
SetState(State::ERRORED);
}
return did_write;
}
bool BaseLogFileWriter::Close() {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
DCHECK_NE(state(), State::CLOSED);
DCHECK_NE(state(), State::DELETED);
const bool result = ((state() != State::ERRORED) && Finalize());
if (result) {
file_.Flush();
file_.Close();
SetState(State::CLOSED);
} else {
Delete(); // Changes the state to DELETED.
}
return result;
}
void BaseLogFileWriter::Delete() {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
DCHECK_NE(state(), State::DELETED);
// The file should be closed before deletion. However, we do not want to go
// through Finalize() and any potential production of a compression footer,
// etc., since we'll be discarding the file anyway.
if (state() != State::CLOSED) {
file_.Close();
}
if (!base::DeleteFile(path_, /*recursive=*/false)) {
LOG(ERROR) << "Failed to delete " << path_ << ".";
}
SetState(State::DELETED);
}
void BaseLogFileWriter::SetState(State state) {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
state_ = state;
}
bool BaseLogFileWriter::WithinBudget(size_t bytes) const {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
return budget_.ConsumeAllowed(bytes);
}
bool BaseLogFileWriter::WriteInternal(const std::string& input, bool metadata) {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
DCHECK(state() == State::ACTIVE || (state() == State::FULL && metadata));
DCHECK(WithinBudget(input.length()));
// base::File's interface does not allow writing more than
// numeric_limits<int>::max() bytes at a time.
DCHECK_LE(input.length(),
static_cast<size_t>(std::numeric_limits<int>::max()));
const int input_len = static_cast<int>(input.length());
int written = file_.WriteAtCurrentPos(input.c_str(), input_len);
if (written != input_len) {
LOG(WARNING) << "WebRTC event log couldn't be written to the "
"locally stored file in its entirety.";
return false;
}
budget_.Consume(static_cast<size_t>(written));
return true;
}
bool BaseLogFileWriter::Finalize() {
DCHECK(task_runner_->RunsTasksInCurrentSequence());
DCHECK_NE(state(), State::CLOSED);
DCHECK_NE(state(), State::DELETED);
DCHECK_NE(state(), State::ERRORED);
return true;
}
// Writes a GZIP-compressed log to a file while observing a maximum size.
class GzippedLogFileWriter : public BaseLogFileWriter {
public:
GzippedLogFileWriter(const base::FilePath& path,
base::Optional<size_t> max_file_size_bytes,
std::unique_ptr<LogCompressor> compressor);
~GzippedLogFileWriter() override = default;
bool Init() override;
bool MaxSizeReached() const override;
bool Write(const std::string& input) override;
protected:
bool Finalize() override;
private:
std::unique_ptr<LogCompressor> compressor_;
};
GzippedLogFileWriter::GzippedLogFileWriter(
const base::FilePath& path,
base::Optional<size_t> max_file_size_bytes,
std::unique_ptr<LogCompressor> compressor)
: BaseLogFileWriter(path, max_file_size_bytes),
compressor_(std::move(compressor)) {
// Factory validates size before instantiation.
DCHECK(!max_file_size_bytes.has_value() ||
max_file_size_bytes.value() >= kGzipOverheadBytes);
}
bool GzippedLogFileWriter::Init() {
if (!BaseLogFileWriter::Init()) {
// Super-class should SetState on its own.
return false;
}
std::string header;
compressor_->CreateHeader(&header);
const bool result = WriteInternal(header, /*metadata=*/true);
if (!result) {
SetState(State::ERRORED);
}
return result;
}
bool GzippedLogFileWriter::MaxSizeReached() const {
DCHECK_EQ(state(), State::ACTIVE);
// Note that the overhead used (footer only) assumes state() is State::ACTIVE,
// as DCHECKed above.
return !WithinBudget(1 + kGzipFooterBytes);
}
bool GzippedLogFileWriter::Write(const std::string& input) {
DCHECK_EQ(state(), State::ACTIVE);
DCHECK(!MaxSizeReached());
if (input.empty()) {
return true;
}
std::string compressed_input;
const auto result = compressor_->Compress(input, &compressed_input);
switch (result) {
case LogCompressor::Result::OK: {
// |compressor_| guarantees |compressed_input| is within-budget.
bool did_write = WriteInternal(compressed_input, /*metadata=*/false);
if (!did_write) {
SetState(State::ERRORED);
}
return did_write;
}
case LogCompressor::Result::DISALLOWED: {
SetState(State::FULL);
return false;
}
case LogCompressor::Result::ERROR_ENCOUNTERED: {
SetState(State::ERRORED);
return false;
}
}
NOTREACHED();
return false; // Appease compiler.
}
bool GzippedLogFileWriter::Finalize() {
DCHECK_NE(state(), State::CLOSED);
DCHECK_NE(state(), State::DELETED);
DCHECK_NE(state(), State::ERRORED);
std::string footer;
if (!compressor_->CreateFooter(&footer)) {
LOG(WARNING) << "Compression footer could not be produced.";
SetState(State::ERRORED);
return false;
}
// |compressor_| guarantees |footer| is within-budget.
if (!WriteInternal(footer, /*metadata=*/true)) {
LOG(WARNING) << "Footer could not be written.";
SetState(State::ERRORED);
return false;
}
return true;
}
// Concrete implementation of LogCompressor using GZIP.
class GzipLogCompressor : public LogCompressor {
public:
GzipLogCompressor(
base::Optional<size_t> max_size_bytes,
std::unique_ptr<CompressedSizeEstimator> compressed_size_estimator);
~GzipLogCompressor() override;
void CreateHeader(std::string* output) override;
Result Compress(const std::string& input, std::string* output) override;
bool CreateFooter(std::string* output) override;
private:
// * A compressed log starts out empty (PRE_HEADER).
// * Once the header is produced, the stream is ACTIVE.
// * If it is ever detected that compressing the next input would exceed the
// budget, that input is NOT compressed, and the state becomes FULL, from
// which only writing the footer or discarding the file are allowed.
// * Writing the footer is allowed on an ACTIVE or FULL stream. Then, the
// stream is effectively closed.
// * Any error puts the stream into ERRORED. An errored stream can only
// be discarded.
enum class State { PRE_HEADER, ACTIVE, FULL, POST_FOOTER, ERRORED };
// Returns the budget left after reserving the GZIP overhead.
// Optionals without a value, both in the parameters as well as in the
// return value of the function, signal an unlimited amount.
static base::Optional<size_t> SizeAfterOverheadReservation(
base::Optional<size_t> max_size_bytes);
// Compresses |input| into |output|, while observing the budget (unless
// !budgeted). If |last|, also closes the stream.
Result CompressInternal(const std::string& input,
std::string* output,
bool budgeted,
bool last);
// Compresses the input data already in |stream_| into |output|.
bool Deflate(int flush, std::string* output);
State state_;
Budget budget_;
std::unique_ptr<CompressedSizeEstimator> compressed_size_estimator_;
z_stream stream_;
};
GzipLogCompressor::GzipLogCompressor(
base::Optional<size_t> max_size_bytes,
std::unique_ptr<CompressedSizeEstimator> compressed_size_estimator)
: state_(State::PRE_HEADER),
budget_(SizeAfterOverheadReservation(max_size_bytes)),
compressed_size_estimator_(std::move(compressed_size_estimator)) {
memset(&stream_, 0, sizeof(z_stream));
// Using (MAX_WBITS + 16) triggers the creation of a GZIP header.
const int result =
deflateInit2(&stream_, Z_DEFAULT_COMPRESSION, Z_DEFLATED, MAX_WBITS + 16,
kDefaultMemLevel, Z_DEFAULT_STRATEGY);
DCHECK_EQ(result, Z_OK);
}
GzipLogCompressor::~GzipLogCompressor() {
const int result = deflateEnd(&stream_);
// Z_DATA_ERROR reports that the stream was not properly terminated,
// but nevertheless correctly released. That happens when we don't
// write the footer.
DCHECK(result == Z_OK ||
(result == Z_DATA_ERROR && state_ != State::POST_FOOTER));
}
void GzipLogCompressor::CreateHeader(std::string* output) {
DCHECK(output);
DCHECK(output->empty());
DCHECK_EQ(state_, State::PRE_HEADER);
const Result result = CompressInternal(std::string(), output,
/*budgeted=*/false, /*last=*/false);
DCHECK_EQ(result, Result::OK);
DCHECK_EQ(output->size(), kGzipHeaderBytes);
state_ = State::ACTIVE;
}
LogCompressor::Result GzipLogCompressor::Compress(const std::string& input,
std::string* output) {
DCHECK_EQ(state_, State::ACTIVE);
if (input.empty()) {
return Result::OK;
}
const auto result =
CompressInternal(input, output, /*budgeted=*/true, /*last=*/false);
switch (result) {
case Result::OK:
return result;
case Result::DISALLOWED:
state_ = State::FULL;
return result;
case Result::ERROR_ENCOUNTERED:
state_ = State::ERRORED;
return result;
}
NOTREACHED();
return Result::ERROR_ENCOUNTERED; // Appease compiler.
}
bool GzipLogCompressor::CreateFooter(std::string* output) {
DCHECK(output);
DCHECK(output->empty());
DCHECK(state_ == State::ACTIVE || state_ == State::FULL);
const Result result = CompressInternal(std::string(), output,
/*budgeted=*/false, /*last=*/true);
if (result != Result::OK) { // !budgeted -> Result::DISALLOWED impossible.
DCHECK_EQ(result, Result::ERROR_ENCOUNTERED);
// An error message was logged by CompressInternal().
state_ = State::ERRORED;
return false;
}
if (output->length() != kGzipFooterBytes) {
LOG(ERROR) << "Incorrect footer size (" << output->length() << ").";
state_ = State::ERRORED;
return false;
}
state_ = State::POST_FOOTER;
return true;
}
base::Optional<size_t> GzipLogCompressor::SizeAfterOverheadReservation(
base::Optional<size_t> max_size_bytes) {
if (!max_size_bytes.has_value()) {
return base::Optional<size_t>();
} else {
DCHECK_GE(max_size_bytes.value(), kGzipHeaderBytes + kGzipFooterBytes);
return max_size_bytes.value() - (kGzipHeaderBytes + kGzipFooterBytes);
}
}
LogCompressor::Result GzipLogCompressor::CompressInternal(
const std::string& input,
std::string* output,
bool budgeted,
bool last) {
DCHECK(output);
DCHECK(output->empty());
DCHECK(state_ == State::PRE_HEADER || state_ == State::ACTIVE ||
(!budgeted && state_ == State::FULL));
// Avoid writing to |output| unless the return value is OK.
std::string temp_output;
if (budgeted) {
const size_t estimated_compressed_size =
compressed_size_estimator_->EstimateCompressedSize(input);
if (!budget_.ConsumeAllowed(estimated_compressed_size)) {
return Result::DISALLOWED;
}
}
if (last) {
DCHECK(input.empty());
stream_.next_in = nullptr;
} else {
stream_.next_in = reinterpret_cast<z_const Bytef*>(input.c_str());
}
DCHECK_LE(input.length(),
static_cast<size_t>(std::numeric_limits<uInt>::max()));
stream_.avail_in = static_cast<uInt>(input.length());
const bool result = Deflate(last ? Z_FINISH : Z_SYNC_FLUSH, &temp_output);
stream_.next_in = nullptr; // Avoid dangling pointers.
if (!result) {
// An error message was logged by Deflate().
return Result::ERROR_ENCOUNTERED;
}
if (budgeted) {
if (!budget_.ConsumeAllowed(temp_output.length())) {
LOG(WARNING) << "Compressed size was above estimate and unexpectedly "
"exceeded the budget.";
return Result::ERROR_ENCOUNTERED;
}
budget_.Consume(temp_output.length());
}
std::swap(*output, temp_output);
return Result::OK;
}
bool GzipLogCompressor::Deflate(int flush, std::string* output) {
DCHECK((flush != Z_FINISH && stream_.next_in != nullptr) ||
(flush == Z_FINISH && stream_.next_in == nullptr));
DCHECK(output->empty());
bool success = true; // Result of this method.
int z_result; // Result of the zlib function.
size_t total_compressed_size = 0;
do {
// Allocate some additional buffer.
constexpr uInt kCompressionBuffer = 4 * 1024;
output->resize(total_compressed_size + kCompressionBuffer);
// This iteration should write directly beyond previous iterations' last
// written byte.
stream_.next_out =
reinterpret_cast<uint8_t*>(&((*output)[total_compressed_size]));
stream_.avail_out = kCompressionBuffer;
z_result = deflate(&stream_, flush);
DCHECK_GE(kCompressionBuffer, stream_.avail_out);
const size_t compressed_size = kCompressionBuffer - stream_.avail_out;
if (flush != Z_FINISH) {
if (z_result != Z_OK) {
LOG(ERROR) << "Compression failed (" << z_result << ").";
success = false;
break;
}
} else { // flush == Z_FINISH
// End of the stream; we expect the footer to be exactly the size which
// we've set aside for it.
if (z_result != Z_STREAM_END || compressed_size != kGzipFooterBytes) {
LOG(ERROR) << "Compression failed (" << z_result << ", "
<< compressed_size << ").";
success = false;
break;
}
}
total_compressed_size += compressed_size;
} while (stream_.avail_out == 0 && z_result != Z_STREAM_END);
stream_.next_out = nullptr; // Avoid dangling pointers.
if (success) {
output->resize(total_compressed_size);
} else {
output->clear();
}
return success;
}
// Given a string with a textual representation of a web-app ID, return the
// ID in integer form. If the textual representation does not name a valid
// web-app ID, return kInvalidWebRtcEventLogWebAppId.
size_t ExtractWebAppId(base::StringPiece str) {
DCHECK_EQ(str.length(), kWebAppIdLength);
// Avoid leading '+', etc.
for (size_t i = 0; i < str.length(); i++) {
if (!std::isdigit(str[i])) {
return kInvalidWebRtcEventLogWebAppId;
}
}
size_t result;
if (!base::StringToSizeT(str, &result) ||
result < kMinWebRtcEventLogWebAppId ||
result > kMaxWebRtcEventLogWebAppId) {
return kInvalidWebRtcEventLogWebAppId;
}
return result;
}
} // namespace
const size_t kGzipOverheadBytes = kGzipHeaderBytes + kGzipFooterBytes;
const base::FilePath::CharType kWebRtcEventLogUncompressedExtension[] =
FILE_PATH_LITERAL("log");
const base::FilePath::CharType kWebRtcEventLogGzippedExtension[] =
FILE_PATH_LITERAL("log.gz");
const base::FilePath::CharType kWebRtcEventLogHistoryExtension[] =
FILE_PATH_LITERAL("hist");
size_t BaseLogFileWriterFactory::MinFileSizeBytes() const {
// No overhead incurred; data written straight to the file without metadata.
return 0;
}
base::FilePath::StringPieceType BaseLogFileWriterFactory::Extension() const {
return kWebRtcEventLogUncompressedExtension;
}
std::unique_ptr<LogFileWriter> BaseLogFileWriterFactory::Create(
const base::FilePath& path,
base::Optional<size_t> max_file_size_bytes) const {
if (max_file_size_bytes.has_value() &&
max_file_size_bytes.value() < MinFileSizeBytes()) {
LOG(WARNING) << "Max size (" << max_file_size_bytes.value()
<< ") below minimum size (" << MinFileSizeBytes() << ").";
return nullptr;
}
auto result = std::make_unique<BaseLogFileWriter>(path, max_file_size_bytes);
if (!result->Init()) {
// Error logged by Init.
result.reset(); // Destructor deletes errored files.
}
return result;
}
std::unique_ptr<CompressedSizeEstimator>
DefaultGzippedSizeEstimator::Factory::Create() const {
return std::make_unique<DefaultGzippedSizeEstimator>();
}
size_t DefaultGzippedSizeEstimator::EstimateCompressedSize(
const std::string& input) const {
// This estimation is not tight. Since we expect to produce logs of
// several MBs, overshooting the estimation by one KB should be
// very safe and still relatively efficient.
constexpr size_t kOverheadOverUncompressedSizeBytes = 1000;
return input.length() + kOverheadOverUncompressedSizeBytes;
}
GzipLogCompressorFactory::GzipLogCompressorFactory(
std::unique_ptr<CompressedSizeEstimator::Factory> estimator_factory)
: estimator_factory_(std::move(estimator_factory)) {}
GzipLogCompressorFactory::~GzipLogCompressorFactory() = default;
size_t GzipLogCompressorFactory::MinSizeBytes() const {
return kGzipOverheadBytes;
}
std::unique_ptr<LogCompressor> GzipLogCompressorFactory::Create(
base::Optional<size_t> max_size_bytes) const {
if (max_size_bytes.has_value() && max_size_bytes.value() < MinSizeBytes()) {
LOG(WARNING) << "Max size (" << max_size_bytes.value()
<< ") below minimum size (" << MinSizeBytes() << ").";
return nullptr;
}
return std::make_unique<GzipLogCompressor>(max_size_bytes,
estimator_factory_->Create());
}
GzippedLogFileWriterFactory::GzippedLogFileWriterFactory(
std::unique_ptr<GzipLogCompressorFactory> gzip_compressor_factory)
: gzip_compressor_factory_(std::move(gzip_compressor_factory)) {}
GzippedLogFileWriterFactory::~GzippedLogFileWriterFactory() = default;
size_t GzippedLogFileWriterFactory::MinFileSizeBytes() const {
// Only the compression's own overhead is incurred.
return gzip_compressor_factory_->MinSizeBytes();
}
base::FilePath::StringPieceType GzippedLogFileWriterFactory::Extension() const {
return kWebRtcEventLogGzippedExtension;
}
std::unique_ptr<LogFileWriter> GzippedLogFileWriterFactory::Create(
const base::FilePath& path,
base::Optional<size_t> max_file_size_bytes) const {
if (max_file_size_bytes.has_value() &&
max_file_size_bytes.value() < MinFileSizeBytes()) {
LOG(WARNING) << "Size below allowed minimum.";
return nullptr;
}
auto gzip_compressor = gzip_compressor_factory_->Create(max_file_size_bytes);
if (!gzip_compressor) {
// The factory itself will have logged an error.
return nullptr;
}
auto result = std::make_unique<GzippedLogFileWriter>(
path, max_file_size_bytes, std::move(gzip_compressor));
if (!result->Init()) {
// Error logged by Init.
result.reset(); // Destructor deletes errored files.
}
return result;
}
// Create a random identifier of 32 hexadecimal (uppercase) characters.
std::string CreateWebRtcEventLogId() {
// UnguessableToken's interface makes no promisses over case. We therefore
// convert, even if the current implementation does not require it.
std::string log_id =
base::ToUpperASCII(base::UnguessableToken::Create().ToString());
DCHECK_EQ(log_id.size(), kWebRtcEventLogIdLength);
DCHECK_EQ(log_id.find_first_not_of("0123456789ABCDEF"), std::string::npos);
return log_id;
}
BrowserContextId GetBrowserContextId(
const content::BrowserContext* browser_context) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
return reinterpret_cast<BrowserContextId>(browser_context);
}
BrowserContextId GetBrowserContextId(int render_process_id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
content::RenderProcessHost* const host =
content::RenderProcessHost::FromID(render_process_id);
content::BrowserContext* const browser_context =
host ? host->GetBrowserContext() : nullptr;
return GetBrowserContextId(browser_context);
}
base::FilePath GetRemoteBoundWebRtcEventLogsDir(
const base::FilePath& browser_context_dir) {
const base::FilePath::CharType kRemoteBoundLogSubDirectory[] =
FILE_PATH_LITERAL("webrtc_event_logs");
return browser_context_dir.Append(kRemoteBoundLogSubDirectory);
}
base::FilePath WebRtcEventLogPath(
const base::FilePath& remote_logs_dir,
const std::string& log_id,
size_t web_app_id,
const base::FilePath::StringPieceType& extension) {
DCHECK_GE(web_app_id, kMinWebRtcEventLogWebAppId);
DCHECK_LE(web_app_id, kMaxWebRtcEventLogWebAppId);
static_assert(kWebAppIdLength == 2u, "Fix the code below.");
const std::string web_app_id_str = base::StringPrintf("%02zu", web_app_id);
DCHECK_EQ(web_app_id_str.length(), kWebAppIdLength);
const std::string filename =
std::string(kRemoteBoundWebRtcEventLogFileNamePrefix) + "_" +
web_app_id_str + "_" + log_id;
return remote_logs_dir.AppendASCII(filename).AddExtension(extension);
}
bool IsValidRemoteBoundLogFilename(const std::string& filename) {
// The -1 is because of the implict \0.
const size_t kPrefixLength =
base::size(kRemoteBoundWebRtcEventLogFileNamePrefix) - 1;
// [prefix]_[web_app_id]_[log_id]
const size_t expected_length =
kPrefixLength + 1 + kWebAppIdLength + 1 + kWebRtcEventLogIdLength;
if (filename.length() != expected_length) {
return false;
}
size_t index = 0;
// Expect prefix.
if (filename.find(kRemoteBoundWebRtcEventLogFileNamePrefix) != index) {
return false;
}
index += kPrefixLength;
// Expect underscore between prefix and web-app ID.
if (filename[index] != '_') {
return false;
}
index += 1;
// Expect web-app-ID.
const size_t web_app_id =
ExtractWebAppId(base::StringPiece(&filename[index], kWebAppIdLength));
if (web_app_id == kInvalidWebRtcEventLogWebAppId) {
return false;
}
index += kWebAppIdLength;
// Expect underscore between web-app ID and log ID.
if (filename[index] != '_') {
return false;
}
index += 1;
// Expect log ID.
const std::string log_id = filename.substr(index);
DCHECK_EQ(log_id.length(), kWebRtcEventLogIdLength);
const char* const log_id_chars = "0123456789ABCDEF";
if (filename.find_first_not_of(log_id_chars, index) != std::string::npos) {
return false;
}
return true;
}
bool IsValidRemoteBoundLogFilePath(const base::FilePath& path) {
const std::string filename = path.BaseName().RemoveExtension().MaybeAsASCII();
return IsValidRemoteBoundLogFilename(filename);
}
base::FilePath GetWebRtcEventLogHistoryFilePath(const base::FilePath& path) {
// TODO(crbug.com/775415): Check for validity (after fixing unit tests).
return path.RemoveExtension().AddExtension(kWebRtcEventLogHistoryExtension);
}
std::string ExtractRemoteBoundWebRtcEventLogLocalIdFromPath(
const base::FilePath& path) {
const std::string filename = path.BaseName().RemoveExtension().MaybeAsASCII();
if (!IsValidRemoteBoundLogFilename(filename)) {
LOG(WARNING) << "Invalid remote-bound WebRTC event log filename.";
return std::string();
}
DCHECK_GE(filename.length(), kWebRtcEventLogIdLength);
return filename.substr(filename.length() - kWebRtcEventLogIdLength);
}
size_t ExtractRemoteBoundWebRtcEventLogWebAppIdFromPath(
const base::FilePath& path) {
const std::string filename = path.BaseName().RemoveExtension().MaybeAsASCII();
if (!IsValidRemoteBoundLogFilename(filename)) {
LOG(WARNING) << "Invalid remote-bound WebRTC event log filename.";
return kInvalidWebRtcEventLogWebAppId;
}
// The -1 is because of the implict \0.
const size_t kPrefixLength =
base::size(kRemoteBoundWebRtcEventLogFileNamePrefix) - 1;
// The +1 is for the underscore between the prefix and the web-app ID.
// Length verified by above call to IsValidRemoteBoundLogFilename().
DCHECK_GE(filename.length(), kPrefixLength + 1 + kWebAppIdLength);
base::StringPiece id_str(&filename[kPrefixLength + 1], kWebAppIdLength);
return ExtractWebAppId(id_str);
}
} // namespace webrtc_event_logging