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