| // 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 |