// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "storage/browser/file_system/sandbox_file_stream_writer.h"

#include <stdint.h>

#include <limits>
#include <tuple>
#include <utility>

#include "base/functional/bind.h"
#include "base/memory/weak_ptr.h"
#include "base/task/sequenced_task_runner.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
#include "storage/browser/file_system/file_observers.h"
#include "storage/browser/file_system/file_system_context.h"
#include "storage/browser/file_system/file_system_operation_runner.h"
#include "storage/browser/file_system/memory_file_stream_writer.h"
#include "storage/browser/file_system/obfuscated_file_util_memory_delegate.h"
#include "storage/browser/file_system/sandbox_file_system_backend_delegate.h"
#include "storage/browser/quota/quota_manager_proxy.h"
#include "storage/common/file_system/file_system_util.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"

namespace storage {

namespace {

// Adjust the |quota| value to make the remaining quota calculation easier. This
// allows us to simply compare written bytes against the adjusted quota.
int64_t AdjustQuotaForOverlap(int64_t quota,
                              int64_t file_offset,
                              int64_t file_size) {
  if (quota < 0)
    quota = 0;
  // |overlap| can be negative if |file_offset| is past the end of the file.
  // Negative |overlap| ensures null bytes between the end of the file and the
  // |file_offset| are counted towards the file's quota.
  int64_t overlap = file_size - file_offset;
  if (overlap < 0 || std::numeric_limits<int64_t>::max() - overlap > quota)
    quota += overlap;
  return quota;
}

}  // namespace

SandboxFileStreamWriter::SandboxFileStreamWriter(
    FileSystemContext* file_system_context,
    const FileSystemURL& url,
    int64_t initial_offset,
    const UpdateObserverList& observers)
    : file_system_context_(file_system_context),
      url_(url),
      initial_offset_(initial_offset),
      observers_(observers),
      file_size_(0),
      total_bytes_written_(0),
      allowed_bytes_to_write_(0),
      has_pending_operation_(false),
      default_quota_(std::numeric_limits<int64_t>::max()) {
  DCHECK(url_.is_valid());
}

SandboxFileStreamWriter::~SandboxFileStreamWriter() = default;

int SandboxFileStreamWriter::Write(net::IOBuffer* buf,
                                   int buf_len,
                                   net::CompletionOnceCallback callback) {
  DCHECK(!write_callback_);
  has_pending_operation_ = true;
  if (file_writer_) {
    const int result = WriteInternal(buf, buf_len);
    if (result == net::ERR_IO_PENDING)
      write_callback_ = std::move(callback);
    return result;
  }

  write_callback_ = std::move(callback);
  net::CompletionOnceCallback write_task = base::BindOnce(
      &SandboxFileStreamWriter::DidInitializeForWrite,
      weak_factory_.GetWeakPtr(), base::RetainedRef(buf), buf_len);
  file_system_context_->operation_runner()->CreateSnapshotFile(
      url_, base::BindOnce(&SandboxFileStreamWriter::DidCreateSnapshotFile,
                           weak_factory_.GetWeakPtr(), std::move(write_task)));
  return net::ERR_IO_PENDING;
}

int SandboxFileStreamWriter::Cancel(net::CompletionOnceCallback callback) {
  if (!has_pending_operation_)
    return net::ERR_UNEXPECTED;

  DCHECK(!callback.is_null());
  cancel_callback_ = std::move(callback);
  return net::ERR_IO_PENDING;
}

int SandboxFileStreamWriter::WriteInternal(net::IOBuffer* buf, int buf_len) {
  // allowed_bytes_to_write could be negative if the file size is
  // greater than the current (possibly new) quota.
  DCHECK(total_bytes_written_ <= allowed_bytes_to_write_ ||
         allowed_bytes_to_write_ < 0);
  if (total_bytes_written_ >= allowed_bytes_to_write_) {
    const int out_of_quota = net::ERR_FILE_NO_SPACE;
    DidWrite(out_of_quota);
    return out_of_quota;
  }

  if (buf_len > allowed_bytes_to_write_ - total_bytes_written_)
    buf_len = allowed_bytes_to_write_ - total_bytes_written_;

  DCHECK(file_writer_.get());
  const int result =
      file_writer_->Write(buf, buf_len,
                          base::BindOnce(&SandboxFileStreamWriter::DidWrite,
                                         weak_factory_.GetWeakPtr()));
  if (result != net::ERR_IO_PENDING)
    DidWrite(result);
  return result;
}

void SandboxFileStreamWriter::DidCreateSnapshotFile(
    net::CompletionOnceCallback callback,
    base::File::Error file_error,
    const base::File::Info& file_info,
    const base::FilePath& platform_path,
    scoped_refptr<ShareableFileReference> file_ref) {
  DCHECK(!file_ref.get());

  if (CancelIfRequested())
    return;
  if (file_error != base::File::FILE_OK) {
    std::move(callback).Run(net::FileErrorToNetError(file_error));
    return;
  }
  if (file_info.is_directory) {
    // We should not be writing to a directory.
    std::move(callback).Run(net::ERR_ACCESS_DENIED);
    return;
  }
  file_size_ = file_info.size;

  DCHECK(!file_writer_.get());

  if (file_system_context_->is_incognito()) {
    base::WeakPtr<ObfuscatedFileUtilMemoryDelegate> memory_file_util_delegate =
        file_system_context_->sandbox_delegate()->memory_file_util_delegate();
    file_writer_ = std::make_unique<MemoryFileStreamWriter>(
        file_system_context_->default_file_task_runner(),
        memory_file_util_delegate, platform_path, initial_offset_);

  } else {
    file_writer_ = FileStreamWriter::CreateForLocalFile(
        file_system_context_->default_file_task_runner(), platform_path,
        initial_offset_, FileStreamWriter::OPEN_EXISTING_FILE);
  }
  const scoped_refptr<QuotaManagerProxy>& quota_manager_proxy =
      file_system_context_->quota_manager_proxy();
  if (!quota_manager_proxy) {
    // If we don't have the quota manager or the requested filesystem type
    // does not support quota, we should be able to let it go.
    allowed_bytes_to_write_ = default_quota_;
    std::move(callback).Run(net::OK);
    return;
  }

  BucketLocator bucket = url_.bucket().value_or(
      BucketLocator::ForDefaultBucket(url_.storage_key()));

  DCHECK(quota_manager_proxy);
  quota_manager_proxy->GetBucketSpaceRemaining(
      bucket, base::SequencedTaskRunner::GetCurrentDefault(),
      base::BindOnce(&SandboxFileStreamWriter::DidGetBucketSpaceRemaining,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
}

void SandboxFileStreamWriter::DidGetBucketSpaceRemaining(
    net::CompletionOnceCallback callback,
    storage::QuotaErrorOr<int64_t> space_remaining) {
  if (CancelIfRequested())
    return;
  if (!space_remaining.has_value()) {
    LOG(WARNING) << "Got unexpected quota error";
    std::move(callback).Run(net::ERR_FAILED);
    return;
  }

  allowed_bytes_to_write_ = space_remaining.value();
  std::move(callback).Run(net::OK);
}

void SandboxFileStreamWriter::DidInitializeForWrite(net::IOBuffer* buf,
                                                    int buf_len,
                                                    int init_status) {
  if (CancelIfRequested())
    return;
  if (init_status != net::OK) {
    has_pending_operation_ = false;
    std::move(write_callback_).Run(init_status);
    return;
  }
  allowed_bytes_to_write_ = AdjustQuotaForOverlap(allowed_bytes_to_write_,
                                                  initial_offset_, file_size_);
  const int result = WriteInternal(buf, buf_len);
  if (result == net::ERR_IO_PENDING)
    DCHECK(!write_callback_.is_null());
}

void SandboxFileStreamWriter::DidWrite(int write_response) {
  DCHECK(has_pending_operation_);
  has_pending_operation_ = false;

  if (write_response <= 0) {
    // TODO(crbug.com/40134326): Consider listening explicitly for out
    // of space errors instead of surfacing all write errors to quota.
    const scoped_refptr<QuotaManagerProxy>& quota_manager_proxy =
        file_system_context_->quota_manager_proxy();
    if (quota_manager_proxy) {
      quota_manager_proxy->OnClientWriteFailed(url_.storage_key());
    }
    if (CancelIfRequested())
      return;
    if (write_callback_)
      std::move(write_callback_).Run(write_response);
    return;
  }

  if (total_bytes_written_ + write_response + initial_offset_ > file_size_) {
    int64_t overlapped = file_size_ - total_bytes_written_ - initial_offset_;
    // If writing past the end of a file, the distance seeked past the file
    // needs to be accounted for. This adjustment should only be made for the
    // first such write (when |total_bytes_written_| is 0).
    if (overlapped < 0 && total_bytes_written_ != 0)
      overlapped = 0;
    observers_.Notify(&FileUpdateObserver::OnUpdate, url_,
                      write_response - overlapped);
  }
  total_bytes_written_ += write_response;

  if (CancelIfRequested())
    return;
  if (write_callback_)
    std::move(write_callback_).Run(write_response);
}

bool SandboxFileStreamWriter::CancelIfRequested() {
  if (cancel_callback_.is_null())
    return false;

  has_pending_operation_ = false;
  std::move(cancel_callback_).Run(net::OK);
  return true;
}

int SandboxFileStreamWriter::Flush(FlushMode flush_mode,
                                   net::CompletionOnceCallback callback) {
  DCHECK(!has_pending_operation_);
  DCHECK(cancel_callback_.is_null());

  // Write() is not called yet, so there's nothing to flush.
  if (!file_writer_)
    return net::OK;

  has_pending_operation_ = true;
  int result = file_writer_->Flush(
      flush_mode,
      base::BindOnce(&SandboxFileStreamWriter::DidFlush,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
  if (result != net::ERR_IO_PENDING)
    has_pending_operation_ = false;
  return result;
}

void SandboxFileStreamWriter::DidFlush(net::CompletionOnceCallback callback,
                                       int result) {
  DCHECK(has_pending_operation_);

  if (CancelIfRequested())
    return;
  has_pending_operation_ = false;
  std::move(callback).Run(result);
}

}  // namespace storage
