diff options
Diffstat (limited to 'chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_common.cc')
-rw-r--r-- | chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_common.cc | 1013 |
1 files changed, 1013 insertions, 0 deletions
diff --git a/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_common.cc b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_common.cc new file mode 100644 index 00000000000..59ad7a2b45e --- /dev/null +++ b/chromium/chrome/browser/media/webrtc/webrtc_event_log_manager_common.cc @@ -0,0 +1,1013 @@ +// 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/metrics/histogram_functions.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 kStartRemoteLoggingFailureAlreadyLogging[] = "Already logging."; +const char kStartRemoteLoggingFailureDeadRenderProcessHost[] = + "RPH already dead."; +const char kStartRemoteLoggingFailureFeatureDisabled[] = "Feature disabled."; +const char kStartRemoteLoggingFailureFileCreationError[] = + "Could not create file."; +const char kStartRemoteLoggingFailureFilePathUsedHistory[] = + "Used history file path."; +const char kStartRemoteLoggingFailureFilePathUsedLog[] = "Used log file path."; +const char kStartRemoteLoggingFailureIllegalWebAppId[] = "Illegal web-app ID."; +const char kStartRemoteLoggingFailureLoggingDisabledBrowserContext[] = + "Disabled for browser context."; +const char kStartRemoteLoggingFailureMaxSizeTooLarge[] = + "Excessively large max log size."; +const char kStartRemoteLoggingFailureMaxSizeTooSmall[] = "Max size too small."; +const char kStartRemoteLoggingFailureNoAdditionalActiveLogsAllowed[] = + "No additional active logs allowed."; +const char kStartRemoteLoggingFailureOutputPeriodMsTooLarge[] = + "Excessively large output period (ms)."; +const char kStartRemoteLoggingFailureUnknownOrInactivePeerConnection[] = + "Unknown or inactive peer connection."; +const char kStartRemoteLoggingFailureUnlimitedSizeDisallowed[] = + "Unlimited size disallowed."; + +const BrowserContextId kNullBrowserContextId = + reinterpret_cast<BrowserContextId>(nullptr); + +void UmaRecordWebRtcEventLoggingApi(WebRtcEventLoggingApiUma result) { + base::UmaHistogramEnumeration("WebRtcEventLogging.Api", result); +} + +void UmaRecordWebRtcEventLoggingUpload(WebRtcEventLoggingUploadUma result) { + base::UmaHistogramEnumeration("WebRtcEventLogging.Upload", result); +} + +void UmaRecordWebRtcEventLoggingNetErrorType(int net_error) { + base::UmaHistogramSparse("WebRtcEventLogging.NetError", net_error); +} + +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 |