// Copyright 2025 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/disk_cache/sql/sql_entry_impl.h"

#include "base/functional/callback_helpers.h"
#include "net/base/features.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
#include "net/disk_cache/sql/entry_db_handle.h"
#include "net/disk_cache/sql/sql_backend_impl.h"
#include "net/disk_cache/sql/sql_shared_cache_handle.h"
#include "net/disk_cache/sql/sql_shared_cache_manager.h"

namespace disk_cache {

SqlEntryImpl::SqlEntryImpl(base::WeakPtr<SqlBackendImpl> backend,
                           CacheEntryKey key,
                           scoped_refptr<EntryDbHandle> db_handle,
                           base::Time last_used,
                           int64_t body_end,
                           scoped_refptr<net::GrowableIOBuffer> head)
    : backend_(backend),
      key_(key),
      db_handle_(std::move(db_handle)),
      last_used_(last_used),
      body_end_(body_end),
      head_(head ? std::move(head)
                 : base::MakeRefCounted<net::GrowableIOBuffer>()) {}

SqlEntryImpl::~SqlEntryImpl() {
  if (!backend_) {
    return;
  }

  if (doomed()) {
    backend_->ReleaseDoomedEntry(*this);
    return;
  }

  std::optional<int64_t> old_body_end;
  EntryWriteBuffer buffer;
  SqlWriteBufferMemoryMonitor::ScopedReservation reservation;

  if (TakeWriteBuffer(buffer, reservation)) {
    old_body_end = buffer.offset;
  }

  int64_t header_size_delta = 0;
  scoped_refptr<net::GrowableIOBuffer> head_to_send;
  bool metadata_update_needed = false;
  if (previous_header_size_in_storage_.has_value() || new_hints_.has_value()) {
    header_size_delta = static_cast<int64_t>(head_->size()) -
                        previous_header_size_in_storage_.value_or(0);
    head_to_send = head_;
    metadata_update_needed = true;
  } else if (last_used_modified_) {
    metadata_update_needed = true;
  }
  if (db_handle_->IsInitialState() || old_body_end.has_value() ||
      metadata_update_needed) {
    backend_->WriteEntryDataAndMetadata(
        key_, db_handle_, old_body_end, body_end_, std::move(buffer),
        last_used_, new_hints_, std::move(head_to_send), header_size_delta,
        base::DoNothingWithBoundArgs(std::move(reservation)));
  }

  backend_->ReleaseActiveEntry(*this);
}

void SqlEntryImpl::Doom() {
  if (doomed() || !backend_) {
    return;
  }
  backend_->DoomActiveEntry(*this);
}

void SqlEntryImpl::Close() {
  Release();
}

std::string SqlEntryImpl::GetKey() const {
  return key_.string();
}

base::Time SqlEntryImpl::GetLastUsed() const {
  return last_used_;
}

int64_t SqlEntryImpl::GetDataSize(int index) const {
  if (index != 0 && index != 1) {
    return net::ERR_INVALID_ARGUMENT;
  }
  if (index == 0) {
    return head_->size();
  }
  CHECK_EQ(index, 1);
  return body_end_;
}

int SqlEntryImpl::ReadData(int index,
                           int64_t offset,
                           IOBuffer* buf,
                           int buf_len,
                           CompletionOnceCallback callback) {
  UpdateLastUsed();
  if (index != 0 && index != 1) {
    return net::ERR_INVALID_ARGUMENT;
  }
  if (buf_len == 0) {
    return 0;
  }
  // Unlike WriteData, there is no overflow check for `offset + buf_len` here.
  // This is intentional. The read path is designed to be permissive: even if
  // the requested range would overflow, the underlying SqlPersistentStore will
  // truncate the read length to fit within the `int64_t` range, allowing a
  // partial read up to the maximum possible offset.
  //
  // TODO(crbug.com/422065015): To enable int64_t offset writes for stream 1 in
  // the SQL backend, the check for offset against int max should be moved to
  // the index == 0 logic path, as stream 1 of SQL backend is designed to handle
  // offsets larger than int max.
  if (!buf || buf_len < 0 || offset < 0 ||
      offset > std::numeric_limits<int>::max()) {
    return net::ERR_INVALID_ARGUMENT;
  }

  if (index == 1) {
    return ReadDataInternal(offset, buf, buf_len, std::move(callback),
                            /*sparse_reading=*/false);
  }
  // Ensure it's stream 0 (header).
  CHECK_EQ(index, 0);
  if (head_->size() <= offset) {
    return 0;
  }
  buf_len = std::min(buf_len, head_->size() - static_cast<int>(offset));
  buf->first(buf_len).copy_from_nonoverlapping(head_->span().subspan(
      static_cast<size_t>(offset), static_cast<size_t>(buf_len)));
  return buf_len;
}

int SqlEntryImpl::ReadDataInternal(int64_t offset,
                                   IOBuffer* buf,
                                   int buf_len,
                                   CompletionOnceCallback callback,
                                   bool sparse_reading) {
  if (!backend_) {
    return net::ERR_FAILED;
  }
  if (body_end_ <= offset) {
    return 0;
  }
  if (read_cache_buffer_) {
    const int64_t cache_end =
        read_cache_buffer_offset_ + read_cache_buffer_->size();
    if (offset >= read_cache_buffer_offset_ && offset < cache_end) {
      const int64_t relative_offset = offset - read_cache_buffer_offset_;
      const int copy_size =
          std::min(buf_len, static_cast<int>(cache_end - offset));
      base::as_writable_bytes(buf->span())
          .first(static_cast<size_t>(copy_size))
          .copy_from(base::as_bytes(read_cache_buffer_->span())
                         .subspan(static_cast<size_t>(relative_offset),
                                  static_cast<size_t>(copy_size)));
      return copy_size;
    }
  }

  if (!write_buffer_.buffers.empty()) {
    const int64_t write_buffer_end =
        write_buffer_.offset + static_cast<int64_t>(write_buffer_.size);
    // If the request is fully within the buffer, copy from the buffer.
    if (offset >= write_buffer_.offset &&
        offset + buf_len <= write_buffer_end) {
      int64_t relative_offset = offset - write_buffer_.offset;
      CHECK_GE(relative_offset, 0);
      CHECK_LE(relative_offset + buf_len,
               static_cast<int64_t>(write_buffer_.size));
      size_t bytes_copied = 0;
      for (const auto& chunk : write_buffer_.buffers) {
        if (relative_offset < static_cast<int64_t>(chunk->size())) {
          const size_t copy_size =
              std::min(static_cast<size_t>(buf_len - bytes_copied),
                       chunk->size() - static_cast<size_t>(relative_offset));
          base::as_writable_bytes(buf->span())
              .subspan(bytes_copied, copy_size)
              .copy_from(base::as_bytes(chunk->span())
                             .subspan(static_cast<size_t>(relative_offset),
                                      copy_size));
          bytes_copied += copy_size;
          relative_offset = 0;
          if (bytes_copied == static_cast<size_t>(buf_len)) {
            break;
          }
        } else {
          relative_offset -= chunk->size();
        }
      }
      return buf_len;
    }
    // If the request overlaps with the buffer, flush the buffer first.
    if (std::max(offset, write_buffer_.offset) <
        std::min(offset + buf_len, write_buffer_end)) {
      FlushBuffer(/*force_flush_for_creation=*/false);
    }
  }

  // When `db_handle_` is in the initial state, `write_buffer_.offset` must be 0
  // and `write_buffer_.size` must be `body_end_`.
  // In that case:
  // - If the request is beyond `write_buffer_`:
  //     0 is returned above (body_end_ <= offset).
  // - If the request is within `write_buffer_`:
  //     `buf_len` is returned above.
  // - If the request overlaps with `write_buffer_`:
  //     FlushBuffer is called above, transitioning to creating state in
  //     SqlBackendImpl::WriteEntryData.
  // Therefore, `db_handle_` cannot be in the initial state here.
  CHECK(!db_handle_->IsInitialState());

  auto read_callback = base::BindOnce(
      [](base::WeakPtr<SqlEntryImpl> self, CompletionOnceCallback callback,
         SqlPersistentStore::ReadResultOrError result) {
        if (!result.has_value()) {
          std::move(callback).Run(result.error() ==
                                          SqlPersistentStore::Error::kAborted
                                      ? net::ERR_ABORTED
                                      : net::ERR_FAILED);
          return;
        }
        if (self) {
          self->read_cache_buffer_ = std::move(result->cache_buffer);
          self->read_cache_buffer_offset_ = result->cache_buffer_offset;
        }
        std::move(callback).Run(result->read_bytes);
      },
      weak_factory_.GetWeakPtr(), std::move(callback));

  return backend_->ReadEntryData(key_, db_handle_, offset, buf, buf_len,
                                 body_end_, sparse_reading,
                                 std::move(read_callback));
}

int SqlEntryImpl::WriteData(int index,
                            int64_t offset,
                            IOBuffer* buf,
                            int buf_len,
                            CompletionOnceCallback callback,
                            bool truncate) {
  UpdateLastUsed();
  if ((index != 0 && index != 1) || (offset < 0) || (buf_len < 0) ||
      (!buf && buf_len > 0) || !base::CheckAdd(offset, buf_len).IsValid()) {
    return net::ERR_INVALID_ARGUMENT;
  }

  // TODO(crbug.com/422065015): To enable int64_t offset reads for stream 1 in
  // the SQL backend, the check should be moved to the index == 0 logic path, as
  // stream 1 of SQL backend is designed to handle offsets larger than int max.
  if (offset + buf_len > std::numeric_limits<int>::max()) {
    return net::ERR_INVALID_ARGUMENT;
  }

  if (index == 1) {
    return WriteDataInternal(offset, buf, buf_len, std::move(callback),
                             truncate, /*sparse_write=*/false);
  }
  CHECK_EQ(index, 0);

  // If this is the first write to the header, store its original size for later
  // persistence.
  if (!previous_header_size_in_storage_) {
    previous_header_size_in_storage_ = head_->size();
  }

  size_t u_offset = base::checked_cast<size_t>(offset);
  size_t u_buf_len = base::checked_cast<size_t>(buf_len);
  if (offset == 0 && truncate) {
    head_->SetCapacity(buf_len);
    if (buf_len) {
      head_->span().copy_from(buf->first(u_buf_len));
    }
  } else {
    const size_t original_size = head_->size();
    const size_t buffer_size =
        truncate ? u_offset + u_buf_len
                 : std::max(u_offset + u_buf_len, original_size);
    head_->SetCapacity(base::checked_cast<int>(buffer_size));

    // Fill any gap with zeros if writing beyond current size.
    const size_t fill_size =
        u_offset <= original_size ? 0 : u_offset - original_size;
    if (fill_size > 0) {
      std::ranges::fill(head_->span().subspan(original_size, fill_size), 0);
    }
    // Copy new data into the buffer.
    if (buf) {
      head_->span().subspan(u_offset).copy_prefix_from(buf->first(u_buf_len));
    }
  }
  return buf_len;
}

int SqlEntryImpl::WriteDataInternal(int64_t offset,
                                    IOBuffer* buf,
                                    int buf_len,
                                    CompletionOnceCallback callback,
                                    bool truncate,
                                    bool sparse_write) {
  if (!backend_) {
    return net::ERR_FAILED;
  }
  read_cache_buffer_.reset();
  read_cache_buffer_offset_ = -1;

  // Ignore zero-length writes that do not change the file size.
  if (buf_len == 0) {
    if (truncate ? (offset == body_end_) : (offset <= body_end_)) {
      return buf_len;
    }
  }

  // Try to buffer the write if it's a sequential append.
  if (offset == body_end_ && !sparse_write &&
      !db_handle_->shared_cache_resource_id()) {
    const int entry_limit =
        net::features::kSqlDiskCacheMaxWriteBufferSizePerEntry.Get();
    if (write_buffer_.size + buf_len > entry_limit) {
      FlushBuffer(/*force_flush_for_creation=*/false);
    }
    if (buf_len <= entry_limit && backend_->write_buffer_monitor().Allocate(
                                      buf_len, write_buffer_reservation_)) {
      if (write_buffer_.buffers.empty()) {
        write_buffer_.offset = offset;
      } else {
        // Ensure continuity.
        CHECK_EQ(
            write_buffer_.offset + static_cast<int64_t>(write_buffer_.size),
            offset);
      }
      write_buffer_.buffers.push_back(base::MakeRefCounted<net::VectorIOBuffer>(
          buf->first(static_cast<size_t>(buf_len))));
      write_buffer_.size += buf_len;

      body_end_ += buf_len;

      return buf_len;
    }
  }

  FlushBuffer(/*force_flush_for_creation=*/false);

  // The end of the current write must not cause an integer overflow. Callers
  // are responsible for validating this, so we CHECK it here.
  const int64_t end_offset = base::CheckAdd(offset, buf_len).ValueOrDie();
  // Calculate the new size of the body (stream 1). If `truncate` is true, the
  // new size is the end of the current write. Otherwise, the body is extended
  // if the write goes past the current end.
  const int64_t new_body_end =
      truncate ? end_offset : std::max(end_offset, body_end_);

  if (!sparse_write && new_body_end > backend_->MaxFileSize()) {
    return net::ERR_FAILED;
  }

  const auto old_body_end = body_end_;
  body_end_ = new_body_end;

  if (db_handle_->shared_cache_resource_id()) {
    if (offset == 0 && truncate) {
      // Complete overwrite. No need to copy.
      auto shared_cache_resource_id = *db_handle_->shared_cache_resource_id();
      db_handle_->set_shared_cache_resource_id(std::nullopt);
      db_handle_->SetSharedCacheBlobHandle(nullptr);
      db_handle_->SetSharedCacheHandle(nullptr);

      auto* manager = backend_->GetSharedCacheManager();
      CHECK(manager);
      manager->DeleteResources({shared_cache_resource_id},
                               base::NullCallback());
    } else {
      return backend_->CopySharedCacheToBlobTableAndWrite(
          key_, db_handle_, offset, base::WrapRefCounted(buf), buf_len,
          old_body_end, truncate, last_used_, sparse_write,
          head_ ? head_->size() : 0, std::move(callback));
    }
  }

  if (db_handle_->IsInitialState()) {
    // WriteEntryData creates an entry in the DB with `last_used_` set.
    // Unless `last_used_` is updated in the future, there is no need to write
    // to the DB again.
    last_used_modified_ = false;
  }

  return backend_->WriteEntryData(
      key_, db_handle_, old_body_end, body_end_,
      EntryWriteBuffer(buf, buf_len, offset), truncate, last_used_,
      sparse_write, head_ ? head_->size() : 0,
      /*copy_buffer_for_optimistic_write=*/true, std::move(callback));
}

void SqlEntryImpl::FlushBuffer(bool force_flush_for_creation) {
  CHECK(backend_);
  EntryWriteBuffer buffer;
  SqlWriteBufferMemoryMonitor::ScopedReservation reservation;

  if (!TakeWriteBuffer(buffer, reservation)) {
    if (force_flush_for_creation && db_handle_->IsInitialState()) {
      // Even if the write buffer is empty, if `force_flush_for_creation` is
      // true and `db_handle_` is in the initial state, create an entry in the
      // DB using WriteEntryDataAndMetadata.
      //
      // WriteEntryDataAndMetadata creates an entry in the DB with `last_used_`
      // set. Unless `last_used_` is updated in the future, there is no need to
      // write to the DB again.
      last_used_modified_ = false;
      backend_->WriteEntryDataAndMetadata(
          key_, db_handle_, /*old_body_end=*/std::nullopt, 0,
          EntryWriteBuffer(), last_used_,
          /*new_hints=*/std::nullopt, /*head_buffer=*/nullptr,
          /*header_size_delta*/ 0, base::DoNothing());
    }
    return;
  }
  if (db_handle_->IsInitialState()) {
    // WriteEntryData creates an entry in the DB with `last_used_` set.
    // Unless `last_used_` is updated in the future, there is no need to write
    // to the DB again.
    last_used_modified_ = false;
  }

  const int64_t offset = buffer.offset;

  // We pass copy_buffer_for_optimistic_write=false because we are passing
  // ownership of the write buffer to the backend.
  // Setting `sparse_write` to false, as FlushBuffer only flushes non-sparse
  // data.
  backend_->WriteEntryData(
      key_, db_handle_, /*old_body_end=*/offset,
      /*body_end=*/body_end_, std::move(buffer),
      /*truncate=*/false, last_used_,
      /*sparse_write=*/false, head_ ? head_->size() : 0,
      /*copy_buffer_for_optimistic_write=*/false,
      base::DoNothingWithBoundArgs(std::move(reservation)));
}

bool SqlEntryImpl::TakeWriteBuffer(
    EntryWriteBuffer& buffer,
    SqlWriteBufferMemoryMonitor::ScopedReservation& reservation) {
  if (write_buffer_.buffers.empty()) {
    return false;
  }

  buffer = std::move(write_buffer_);
  write_buffer_ = EntryWriteBuffer();
  reservation = std::move(write_buffer_reservation_);
  return true;
}

int SqlEntryImpl::ReadSparseData(int64_t offset,
                                 IOBuffer* buf,
                                 int buf_len,
                                 CompletionOnceCallback callback) {
  UpdateLastUsed();
  if (buf_len == 0) {
    return net::OK;
  }
  if (!buf || buf_len < 0 || offset < 0) {
    return net::ERR_INVALID_ARGUMENT;
  }
  return ReadDataInternal(offset, buf, buf_len, std::move(callback),
                          /*sparse_reading=*/true);
}

int SqlEntryImpl::WriteSparseData(int64_t offset,
                                  IOBuffer* buf,
                                  int buf_len,
                                  CompletionOnceCallback callback) {
  UpdateLastUsed();
  if ((offset < 0) || (buf_len < 0) || (!buf && buf_len > 0) ||
      !base::CheckAdd(offset, buf_len).IsValid()) {
    return net::ERR_INVALID_ARGUMENT;
  }
  return WriteDataInternal(offset, buf, buf_len, std::move(callback),
                           /*truncate=*/false, /*sparse_write=*/true);
}

RangeResult SqlEntryImpl::GetAvailableRange(int64_t offset,
                                            int len,
                                            RangeResultCallback callback) {
  if (!backend_) {
    return RangeResult(net::ERR_FAILED);
  }
  if (offset < 0 || len < 0) {
    return RangeResult(net::ERR_INVALID_ARGUMENT);
  }

  // Since the processing of GetAvailableRange is implemented only on the DB,
  // flush all write buffers.
  FlushBuffer(/*force_flush_for_creation=*/true);

  return backend_->GetEntryAvailableRange(key_, db_handle_, offset, len,
                                          std::move(callback));
}

bool SqlEntryImpl::CouldBeSparse() const {
  // SqlEntryImpl doesn't distinguish the stream 1 data and the sparse data.
  return true;
}

void SqlEntryImpl::CancelSparseIO() {
  // SqlEntryImpl doesn't distinguish the stream 1 data and the sparse data.
}

net::Error SqlEntryImpl::ReadyForSparseIO(CompletionOnceCallback callback) {
  // SqlEntryImpl doesn't distinguish the stream 1 data and the sparse data.
  return net::OK;
}

void SqlEntryImpl::SetEntryInMemoryData(uint8_t data) {
  const MemoryEntryDataHints hints(data);
  new_hints_ = hints;
  if (backend_) {
    backend_->SetEntryDataHints(key_, db_handle_, hints);
  }
}

void SqlEntryImpl::SetLastUsedTimeForTest(base::Time time) {
  last_used_ = time;
  last_used_modified_ = true;
}

void SqlEntryImpl::UpdateLastUsed() {
  last_used_ = base::Time::Now();
  last_used_modified_ = true;
}

bool SqlEntryImpl::doomed() const {
  return db_handle_->doomed();
}

}  // namespace disk_cache
