blob: e60a29661dd40f8c68672a07eddedf9cc41b978a [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/http/cache_body_compressor.h"
#include <algorithm>
#include "base/check.h"
#include "base/check_op.h"
#include "base/logging.h"
#include "base/numerics/safe_conversions.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
#if !defined(NET_DISABLE_ZSTD_COMPRESS)
#include "third_party/zstd/src/lib/zstd.h"
#endif
namespace net {
#if !defined(NET_DISABLE_ZSTD_COMPRESS)
void CacheBodyCompressor::ZstdCCtxDeleter::operator()(ZSTD_CCtx* ctx) const {
ZSTD_freeCCtx(ctx);
}
#endif
CacheBodyCompressor::CacheBodyCompressor() = default;
CacheBodyCompressor::~CacheBodyCompressor() {
Reset();
}
bool CacheBodyCompressor::Init() {
#if !defined(NET_DISABLE_ZSTD_COMPRESS)
// TODO(crbug.com/406988080): Add UmaHistogramEnumeration for zstd error
// telemetry.
if (!cctx_) {
cctx_.reset(ZSTD_createCCtx());
if (!cctx_) {
DVLOG(1) << "Failed to create zstd compression context";
return false;
}
// Level 1 minimizes CPU overhead; cache writes happen inline on the network
// IO path so latency matters more than squeezing out the last few percent
// of ratio. Level 1's default windowLog=19 (512 KB) is well within the
// decompressor's ZSTD_d_windowLogMax=23 limit.
size_t result =
ZSTD_CCtx_setParameter(cctx_.get(), ZSTD_c_compressionLevel, 1);
if (ZSTD_isError(result)) {
DVLOG(1) << "Failed to set zstd compression level: "
<< ZSTD_getErrorName(result);
cctx_.reset();
return false;
}
}
// Pre-allocate the output buffer. 32 KB matches the default read buffer
// size used by HttpCache::Transaction (kDefaultCacheReadBufLen), so in the
// common case the buffer is allocated exactly once.
//
// ZSTD_compressBound() is documented for single-shot compression; for
// streaming, the actual output per call can vary unpredictably. We use it
// as a reasonable initial estimate and rely on the retry loop in Compress()
// to grow the buffer if needed. This keeps all zstd complexity inside this
// class so HttpCache::Writers (which is already complex) stays clean.
constexpr size_t kDefaultCompressInputSize = 32 * 1024;
output_buffer_ = base::MakeRefCounted<IOBufferWithSize>(
base::checked_cast<int>(ZSTD_compressBound(kDefaultCompressInputSize)));
output_length_ = 0;
total_input_bytes_ = 0;
return true;
#else
return false;
#endif
}
int CacheBodyCompressor::Compress(base::span<const uint8_t> input,
bool is_last_chunk) {
#if !defined(NET_DISABLE_ZSTD_COMPRESS)
CHECK(cctx_);
if (max_uncompressed_size_for_testing_.has_value() &&
total_input_bytes_ + static_cast<int64_t>(input.size()) >
*max_uncompressed_size_for_testing_) {
output_length_ = 0;
return ERR_CACHE_COMPRESSION_FAILURE;
}
ZSTD_inBuffer zstd_input = {input.data(), input.size(), 0};
ZSTD_outBuffer zstd_output = {output_buffer_->data(),
static_cast<size_t>(output_buffer_->size()), 0};
ZSTD_EndDirective mode = is_last_chunk ? ZSTD_e_end : ZSTD_e_continue;
// Loop until all input is consumed AND (for is_last_chunk) the frame
// epilogue is fully flushed. In the common case this completes in one
// iteration; the loop handles the rare case where the output buffer is too
// small for streaming flush of internally-buffered data.
while (true) {
size_t remaining =
ZSTD_compressStream2(cctx_.get(), &zstd_output, &zstd_input, mode);
if (ZSTD_isError(remaining)) {
DVLOG(1) << "zstd compression error: " << ZSTD_getErrorName(remaining);
output_length_ = 0;
return ERR_CACHE_COMPRESSION_FAILURE;
}
bool input_done = (zstd_input.pos == zstd_input.size);
bool frame_done = (is_last_chunk && remaining == 0) || !is_last_chunk;
if (input_done && frame_done) {
break;
}
// Output buffer was too small — grow it and retry.
GrowOutputBufferSizeByAtLeast(remaining, zstd_output.pos);
zstd_output.dst = output_buffer_->data();
zstd_output.size = static_cast<size_t>(output_buffer_->size());
}
total_input_bytes_ += static_cast<int64_t>(input.size());
output_length_ = zstd_output.pos;
return base::checked_cast<int>(output_length_);
#else
return ERR_CACHE_COMPRESSION_FAILURE;
#endif
}
int CacheBodyCompressor::Finalize() {
#if !defined(NET_DISABLE_ZSTD_COMPRESS)
CHECK(cctx_);
CHECK(output_buffer_);
ZSTD_inBuffer zstd_input = {nullptr, 0, 0};
ZSTD_outBuffer zstd_output = {output_buffer_->data(),
static_cast<size_t>(output_buffer_->size()), 0};
size_t remaining =
ZSTD_compressStream2(cctx_.get(), &zstd_output, &zstd_input, ZSTD_e_end);
if (ZSTD_isError(remaining)) {
DVLOG(1) << "zstd finalization error: " << ZSTD_getErrorName(remaining);
output_length_ = 0;
return ERR_CACHE_COMPRESSION_FAILURE;
}
output_length_ = zstd_output.pos;
// `remaining` > 0 means zstd still has more output pending — the caller
// needs to write the current output_buffer() and call Finalize() again.
// `remaining` == 0 means the frame is complete.
return base::checked_cast<int>(remaining);
#else
return ERR_CACHE_COMPRESSION_FAILURE;
#endif
}
void CacheBodyCompressor::Reset() {
#if !defined(NET_DISABLE_ZSTD_COMPRESS)
if (cctx_) {
ZSTD_CCtx_reset(cctx_.get(), ZSTD_reset_session_only);
}
#endif
output_buffer_ = nullptr;
output_length_ = 0;
total_input_bytes_ = 0;
}
#if !defined(NET_DISABLE_ZSTD_COMPRESS)
void CacheBodyCompressor::GrowOutputBufferSizeByAtLeast(
size_t needed,
size_t bytes_already_written) {
size_t old_size = static_cast<size_t>(output_buffer_->size());
CHECK_GE(old_size, 16u);
size_t min_growth = std::max(needed, old_size / 16);
size_t new_size = old_size + std::max(min_growth, size_t{1});
auto new_buffer =
base::MakeRefCounted<IOBufferWithSize>(base::checked_cast<int>(new_size));
if (bytes_already_written > 0) {
new_buffer->span()
.first(bytes_already_written)
.copy_from(output_buffer_->span().first(bytes_already_written));
}
output_buffer_ = std::move(new_buffer);
}
#endif
} // namespace net