blob: c2059d259c1445acf2ff8adf4ebccf2cf02b368e [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.
#ifndef NET_HTTP_CACHE_BODY_DECOMPRESSOR_H_
#define NET_HTTP_CACHE_BODY_DECOMPRESSOR_H_
#include <memory>
#include <optional>
#include "base/check_op.h"
#include "base/containers/span.h"
#include "base/memory/scoped_refptr.h"
#include "net/base/net_export.h"
#if !defined(NET_DISABLE_ZSTD)
#include "third_party/zstd/src/lib/zstd.h"
#endif
namespace net {
class IOBuffer;
// Encapsulates zstd streaming decompression for reading compressed cache
// entry bodies.
//
// This class owns all zstd decompression state (ZSTD_DCtx, intermediate
// input buffer, leftover tracking, byte counters, frame-complete flag) and
// exposes a minimal interface for HttpCache::Transaction to call at
// well-defined points in its state machine. All zstd-specific logic is
// confined here.
//
// Usage (see HttpCache::Transaction::DoCacheReadData /
// DoCacheReadDataComplete for the real wiring):
// decompressor_ = std::make_unique<CacheBodyDecompressor>();
// if (!decompressor_->Init()) { /* error */ }
// ...
// // In DoCacheReadData(), before each disk read:
// if (decompressor_->has_leftover()) {
// // Drain bytes zstd already holds from a prior disk read. Skip disk.
// } else {
// decompressor_->EnsureInputBuffer(read_buf_len_);
// entry->ReadData(..., decompressor_->input_buffer(), ..., callback);
// }
// ...
// // In DoCacheReadDataComplete():
// int consumed = 0;
// int out = decompressor_->has_leftover()
// ? decompressor_->DecompressLeftover(read_buf, len, &consumed)
// : decompressor_->Decompress(bytes_read, read_buf, len, &consumed);
// read_offset_ += consumed;
//
// // On EOF (disk read returned 0):
// if (!decompressor_->frame_complete()) { /* truncated, fail */ }
//
// Thread-safety: Not thread-safe. Must be used on a single sequence.
class NET_EXPORT_PRIVATE CacheBodyDecompressor {
public:
CacheBodyDecompressor();
~CacheBodyDecompressor();
CacheBodyDecompressor(const CacheBodyDecompressor&) = delete;
CacheBodyDecompressor& operator=(const CacheBodyDecompressor&) = delete;
// Initializes the zstd decompression context.
// Returns true on success, false if initialization fails.
bool Init();
// Returns the input buffer that callers should read compressed data into.
// The buffer is allocated by EnsureInputBuffer() and reused across calls.
IOBuffer* input_buffer() const { return input_buffer_.get(); }
// Ensures the input buffer is at least `min_size` bytes, allocating or
// growing as needed.
//
// Precondition: has_leftover() must be false. Reallocating a buffer that
// still holds unconsumed bytes would silently drop them.
void EnsureInputBuffer(size_t min_size);
// Returns true if there are unconsumed compressed bytes from a previous
// Decompress()/DecompressLeftover() call. When true, the caller should
// call DecompressLeftover() (no disk read) instead of reading new bytes
// from disk.
bool has_leftover() const { return leftover_len_ > 0; }
// Returns the number of unconsumed compressed bytes in input_buffer().
size_t leftover_len() const { return leftover_len_; }
// Decompresses the first `bytes_read` bytes of input_buffer() into
// `output_buf` (up to `output_buf_len` bytes). Intended to be called after
// a fresh disk read filled input_buffer() with `bytes_read` compressed
// bytes.
//
// Returns:
// > 0: number of decompressed bytes written to `output_buf`
// 0: zstd consumed input but produced no output (e.g., while parsing
// a frame header). The caller should read more compressed data
// from disk and call again. The decompressor enforces
// kMaxConsecutiveZeroOutputCount internally, so the caller does
// not need a separate guard against zero-output infinite loops.
// < 0: decompression error (ERR_CACHE_READ_FAILURE). Includes zstd
// errors, the case where zstd neither consumed input nor produced
// output (would otherwise infinite-loop), and exceeding the
// consecutive zero-output limit.
//
// `bytes_consumed_out` is set to the number of compressed input bytes
// consumed by zstd. The caller uses this to advance its disk read offset.
// Any remaining bytes are tracked internally and reported via
// has_leftover()/leftover_len() for the next DecompressLeftover() call.
int Decompress(size_t bytes_read,
IOBuffer* output_buf,
size_t output_buf_len,
size_t* bytes_consumed_out);
// Decompresses the leftover bytes tracked from a previous call into
// `output_buf`. Precondition: has_leftover() is true.
//
// Returns the same values as Decompress(). `bytes_consumed_out` is set
// to the number of additional leftover bytes consumed on this call.
int DecompressLeftover(IOBuffer* output_buf,
size_t output_buf_len,
size_t* bytes_consumed_out);
// Returns the number of consecutive Decompress()/DecompressLeftover() calls
// that produced 0 output bytes. Exposed for diagnostics / tests only — the
// limit is enforced internally by DoDecompress() which returns an error as
// soon as the count exceeds kMaxConsecutiveZeroOutputCount. Callers do not
// need to check this themselves.
int consecutive_zero_output_count() const {
return consecutive_zero_output_count_;
}
// Upper bound on consecutive zero-output decompression calls before we
// declare the stream corrupt. Zstd frame headers and metadata blocks can
// legitimately consume input without producing output for one or two calls,
// but a sustained run indicates a malformed stream or an infinite loop. A
// well-formed frame should never need more than a handful of zero-output
// iterations before producing data, so this bound is intentionally tight.
static constexpr int kMaxConsecutiveZeroOutputCount = 20;
// Hard cap on decompressed output, applied even when Content-Length is
// missing or lies. Disk backends already reject entries larger than 2 GB,
// so this only fires on zstd-bomb input.
static constexpr int64_t kMaxDecompressedBodySize = 2LL * 1024 * 1024 * 1024;
// Returns true if zstd signaled that the compressed frame has been fully
// decoded (i.e., ZSTD_decompressStream returned 0). The caller should check
// this at EOF — if the disk read returns 0 but the frame is not complete,
// the cache entry is truncated.
bool frame_complete() const { return frame_complete_; }
// Sets an upper bound on decompressed output. When set, DoDecompress()
// returns ERR_CACHE_READ_FAILURE as soon as total_output_bytes_ would
// exceed this value, catching over-decompression mid-stream rather than
// only at EOF.
// Call after Init() and before the first Decompress()/DecompressLeftover().
//
// `len` must be non-negative. A negative sentinel would silently invert
// the over-decompression check (every output byte would exceed the bound),
// so we CHECK rather than tolerate it.
void set_expected_content_length(int64_t len) {
CHECK_GE(len, 0);
expected_content_length_ = len;
}
// Total decompressed output bytes produced across all calls.
int64_t total_output_bytes() const { return total_output_bytes_; }
// Resets all state. Safe to call even if Init() was never called.
void Reset();
private:
// Core decompression logic shared by Decompress() and DecompressLeftover().
// `input_offset` is the absolute offset into input_buffer_ where `input`
// starts, used to compute leftover offsets without pointer arithmetic.
int DoDecompress(base::span<const uint8_t> input,
size_t input_offset,
IOBuffer* output_buf,
size_t output_buf_len,
size_t* bytes_consumed_out);
#if !defined(NET_DISABLE_ZSTD)
struct ZstdDCtxDeleter {
void operator()(ZSTD_DCtx* ctx) const { ZSTD_freeDCtx(ctx); }
};
std::unique_ptr<ZSTD_DCtx, ZstdDCtxDeleter> dctx_;
#endif
scoped_refptr<IOBuffer> input_buffer_;
// Leftover tracking: unconsumed compressed bytes from previous disk read.
size_t leftover_offset_ = 0;
size_t leftover_len_ = 0;
int consecutive_zero_output_count_ = 0;
int64_t total_output_bytes_ = 0;
std::optional<int64_t> expected_content_length_;
bool frame_complete_ = false;
};
} // namespace net
#endif // NET_HTTP_CACHE_BODY_DECOMPRESSOR_H_