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

#include "third_party/blink/renderer/modules/native_io/native_io_file_sync.h"

#include <limits>

#include "base/numerics/safe_conversions.h"
#include "build/build_config.h"
#include "third_party/blink/public/mojom/native_io/native_io.mojom-blink.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/execution_context/execution_context_lifecycle_observer.h"
#include "third_party/blink/renderer/core/typed_arrays/array_buffer_view_helpers.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer_view.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_data_view.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_typed_array.h"
#include "third_party/blink/renderer/modules/native_io/native_io_capacity_tracker.h"
#include "third_party/blink/renderer/modules/native_io/native_io_error.h"
#include "third_party/blink/renderer/modules/native_io/native_io_file_utils.h"
#include "third_party/blink/renderer/platform/bindings/exception_code.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"

#if defined(OS_MAC)
#include "base/mac/mac_util.h"
#endif

namespace blink {

NativeIOFileSync::NativeIOFileSync(
    base::File backing_file,
    int64_t backing_file_length,
    HeapMojoRemote<mojom::blink::NativeIOFileHost> backend_file,
    NativeIOCapacityTracker* capacity_tracker,
    ExecutionContext* execution_context)
    : backing_file_(std::move(backing_file)),
      file_length_(backing_file_length),
      backend_file_(std::move(backend_file)),
      capacity_tracker_(capacity_tracker) {
  DCHECK_GE(backing_file_length, 0);
  DCHECK(capacity_tracker);
  backend_file_.set_disconnect_handler(WTF::Bind(
      &NativeIOFileSync::OnBackendDisconnect, WrapWeakPersistent(this)));
}

NativeIOFileSync::~NativeIOFileSync() = default;

void NativeIOFileSync::close() {
  backing_file_.Close();

  if (!backend_file_.is_bound()) {
    // If the backend went away, it already considers the file closed. Nothing
    // to report here.
    return;
  }
  backend_file_->Close();
}

uint64_t NativeIOFileSync::getLength(ExceptionState& exception_state) {
  if (!backing_file_.IsValid()) {
    ThrowNativeIOWithError(exception_state,
                           mojom::blink::NativeIOError::New(
                               mojom::blink::NativeIOErrorType::kInvalidState,
                               "NativeIOHost backend went away"));
    return 0;
  }
  int64_t length = backing_file_.GetLength();
  if (length < 0) {
    ThrowNativeIOWithError(exception_state, backing_file_.GetLastFileError());
    return 0;
  }
  DCHECK_EQ(file_length_, length)
      << "The file size should equal the actual length";
  // getLength returns an unsigned integer, which is different from e.g.,
  // base::File and POSIX. The uses for negative integers are error handling,
  // which is done through exceptions, and seeking from an offset without type
  // conversions, which is not supported by NativeIO.
  return base::as_unsigned(length);
}

void NativeIOFileSync::setLength(uint64_t new_length,
                                 ExceptionState& exception_state) {
  if (!base::IsValueInRangeForNumericType<int64_t>(new_length)) {
    // TODO(rstz): Consider throwing QuotaExceededError here.
    exception_state.ThrowTypeError("Length out of bounds");
    return;
  }
  if (!backing_file_.IsValid()) {
    ThrowNativeIOWithError(exception_state,
                           mojom::blink::NativeIOError::New(
                               mojom::blink::NativeIOErrorType::kInvalidState,
                               "NativeIOHost backend went away"));
    return;
  }
  int64_t expected_length = base::as_signed(new_length);

  DCHECK_GE(expected_length, 0);
  DCHECK_GE(file_length_, 0);
  static_assert(0 - std::numeric_limits<int32_t>::max() >=
                    std::numeric_limits<int32_t>::min(),
                "The `length_delta` computation below may overflow");
  // Since both values are non-negative, the arithmetic will not overflow.
  int64_t length_delta = expected_length - file_length_;

  if (length_delta > 0) {
    if (!capacity_tracker_->ChangeAvailableCapacity(-length_delta)) {
      ThrowNativeIOWithError(exception_state,
                             mojom::blink::NativeIOError::New(
                                 mojom::blink::NativeIOErrorType::kNoSpace,
                                 "No capacity available for this operation"));
      return;
    }
    file_length_ = expected_length;
  }

#if defined(OS_MAC)
  // On macOS < 10.15, a sandboxing limitation causes failures in ftruncate()
  // syscalls issued from renderers. For this reason, base::File::SetLength()
  // fails in the renderer. We work around this problem by calling ftruncate()
  // in the browser process. See crbug.com/1084565.
  if (!base::mac::IsAtLeastOS10_15()) {
    // Our system has at most one handle to a file, so we can avoid reasoning
    // through the implications of multiple handles pointing to the same file.
    //
    // To preserve this invariant, we pass this file's handle to the browser
    // process during the SetLength() mojo call, and the browser passes it back
    // when the call completes.
    mojom::blink::NativeIOErrorPtr set_length_result;
    int64_t actual_length;
    backend_file_->SetLength(base::as_signed(expected_length),
                             std::move(backing_file_), &backing_file_,
                             &actual_length, &set_length_result);
    DCHECK(backing_file_.IsValid()) << "browser returned closed file";

    if (actual_length >= 0) {
      DCHECK_LE(actual_length, file_length_)
          << "file_length_ should be an upper bound during I/O";
      if (actual_length < file_length_) {
        // For successful length decreases, this logic returns freed up
        // capacity. For unsuccessful length increases, this logic returns
        // unused capacity.
        bool change_success = capacity_tracker_->ChangeAvailableCapacity(
            file_length_ - actual_length);
        DCHECK(change_success) << "Capacity increases should always succeed";
        file_length_ = actual_length;
      }
    } else {
      DCHECK(set_length_result->type !=
             mojom::blink::NativeIOErrorType::kSuccess);
      // base::File::SetLength() failed. Then, attempting to File::GetLength()
      // failed as well. We don't have a reliable measure of the file's length,
      // and the file descriptor is probably unusable. Force-closing the file
      // without reclaiming any capacity minimizes the risk of overusing our
      // allocation.
      backing_file_.Close();
      if (backend_file_.is_bound())
        backend_file_->Close();
    }

    if (set_length_result->type != mojom::blink::NativeIOErrorType::kSuccess) {
      ThrowNativeIOWithError(exception_state, std::move(set_length_result));
      return;
    }
    return;
  }
#endif  // defined(OS_MAC)

  base::File::Error set_length_result = base::File::FILE_OK;
  int64_t actual_length = expected_length;

  if (!backing_file_.SetLength(expected_length)) {
    set_length_result = backing_file_.GetLastFileError();
    actual_length = backing_file_.GetLength();
    if (actual_length < 0)
      set_length_result = backing_file_.GetLastFileError();
  }

  if (actual_length >= 0) {
    DCHECK_LE(actual_length, file_length_)
        << "file_length_ should be an upper bound during I/O";
    if (actual_length < file_length_) {
      // For successful length decreases, this logic returns freed up
      // capacity. For unsuccessful length increases, this logic returns
      // unused capacity.
      bool change_success = capacity_tracker_->ChangeAvailableCapacity(
          file_length_ - actual_length);
      DCHECK(change_success) << "Capacity increases should always succeed";
      file_length_ = actual_length;
    }
  } else {
    DCHECK(set_length_result != base::File::FILE_OK);
    // base::File::SetLength() failed. Then, attempting to File::GetLength()
    // failed as well. We don't have a reliable measure of the file's length,
    // and the file descriptor is probably unusable. Force-closing the file
    // without reclaiming any capacity minimizes the risk of overusing our
    // allocation.
    backing_file_.Close();
    if (backend_file_.is_bound())
      backend_file_->Close();
  }
  if (set_length_result != base::File::FILE_OK) {
    ThrowNativeIOWithError(exception_state, set_length_result);
    return;
  }
}

NativeIOReadResult* NativeIOFileSync::read(ScriptState* script_state,
                                           NotShared<DOMArrayBufferView> buffer,
                                           uint64_t file_offset,
                                           ExceptionState& exception_state) {
  int read_size = NativeIOOperationSize(*buffer);

  NativeIOReadResult* read_result = MakeGarbageCollected<NativeIOReadResult>();
  read_result->setReadBytes(0);
  DOMArrayBufferView* result_buffer =
      TransferToNewArrayBufferView(script_state->GetIsolate(), buffer);
  if (!result_buffer) {
    exception_state.ThrowTypeError("Could not transfer buffer");
    return read_result;
  }
  DCHECK(buffer->IsDetached());

  read_result->setBuffer(NotShared<DOMArrayBufferView>(result_buffer));

  if (!backing_file_.IsValid()) {
    ThrowNativeIOWithError(exception_state,
                           mojom::blink::NativeIOError::New(
                               mojom::blink::NativeIOErrorType::kInvalidState,
                               "The file was already closed"));
    return read_result;
  }

  char* result_buffer_data = static_cast<char*>(result_buffer->BaseAddress());
  int read_bytes =
      backing_file_.Read(file_offset, result_buffer_data, read_size);
  if (read_bytes < 0) {
    ThrowNativeIOWithError(exception_state, backing_file_.GetLastFileError());
    return read_result;
  }

  read_result->setReadBytes(read_bytes);
  return read_result;
}

NativeIOWriteResult* NativeIOFileSync::write(
    ScriptState* script_state,
    NotShared<DOMArrayBufferView> buffer,
    uint64_t file_offset,
    ExceptionState& exception_state) {
  int write_size = NativeIOOperationSize(*buffer);

  NativeIOWriteResult* write_result =
      MakeGarbageCollected<NativeIOWriteResult>();
  write_result->setWrittenBytes(0);

  DOMArrayBufferView* result_buffer =
      TransferToNewArrayBufferView(script_state->GetIsolate(), buffer);
  if (!result_buffer) {
    exception_state.ThrowTypeError("Could not transfer buffer");
    return write_result;
  }
  DCHECK(buffer->IsDetached());

  write_result->setBuffer(NotShared<DOMArrayBufferView>(result_buffer));

  if (!backing_file_.IsValid()) {
    ThrowNativeIOWithError(exception_state,
                           mojom::blink::NativeIOError::New(
                               mojom::blink::NativeIOErrorType::kInvalidState,
                               "The file was already closed"));
    return write_result;
  }

  int64_t write_end_offset;
  if (!base::CheckAdd(file_offset, write_size)
           .AssignIfValid(&write_end_offset)) {
    ThrowNativeIOWithError(exception_state,
                           mojom::blink::NativeIOError::New(
                               mojom::blink::NativeIOErrorType::kNoSpace,
                               "No capacity available for this operation"));
    return write_result;
  }

  DCHECK_GE(write_end_offset, 0);
  DCHECK_GE(file_length_, 0);
  static_assert(0 - std::numeric_limits<int32_t>::max() >=
                    std::numeric_limits<int32_t>::min(),
                "The `length_delta` computation below may overflow");
  // Since both values are positive, the arithmetic will not overflow.
  int64_t length_delta = write_end_offset - file_length_;
  // The available capacity must be reduced before performing an I/O operation
  // that increases the file length. This prevents double-spending by concurrent
  // I/O operations on different files.
  if (length_delta > 0) {
    if (!capacity_tracker_->ChangeAvailableCapacity(-length_delta)) {
      ThrowNativeIOWithError(exception_state,
                             mojom::blink::NativeIOError::New(
                                 mojom::blink::NativeIOErrorType::kNoSpace,
                                 "No capacity available for this operation"));
      return write_result;
    }
    file_length_ = write_end_offset;
  }

  base::File::Error write_error = base::File::FILE_OK;
  int64_t actual_file_length_on_failure = file_length_;

  char* result_buffer_data = static_cast<char*>(result_buffer->BaseAddress());
  int written_bytes =
      backing_file_.Write(file_offset, result_buffer_data, write_size);
  if (written_bytes < 0) {
    write_error = backing_file_.GetLastFileError();
    actual_file_length_on_failure = backing_file_.GetLength();
  }
  if (write_error != base::File::FILE_OK || written_bytes < write_size) {
    if (actual_file_length_on_failure >= 0) {
      DCHECK_LE(actual_file_length_on_failure, file_length_)
          << "file_length_ should be an upper bound during I/O";
      if (actual_file_length_on_failure < file_length_) {
        bool change_success = capacity_tracker_->ChangeAvailableCapacity(
            file_length_ - actual_file_length_on_failure);
        DCHECK(change_success) << "Capacity increases should always succeed";
        file_length_ = actual_file_length_on_failure;
      }
    } else {
      DCHECK(write_error != base::File::FILE_OK);
      // base::File::Write() failed. Then, attempting to File::GetLength()
      // failed as well. We don't have a reliable measure of the file's length,
      // and the file descriptor is probably unusable. Force-closing the file
      // without reclaiming any capacity minimizes the risk of overusing our
      // allocation.
      backing_file_.Close();
      if (backend_file_.is_bound())
        backend_file_->Close();
    }
  }

  if (write_error != base::File::FILE_OK) {
    ThrowNativeIOWithError(exception_state, write_error);
    return write_result;
  }

  write_result->setWrittenBytes(written_bytes);
  return write_result;
}

void NativeIOFileSync::flush(ExceptionState& exception_state) {
  // This implementation of flush attempts to physically store the data it has
  // written on disk. This behaviour might change in the future.
  if (!backing_file_.IsValid()) {
    ThrowNativeIOWithError(exception_state,
                           mojom::blink::NativeIOError::New(
                               mojom::blink::NativeIOErrorType::kInvalidState,
                               "The file was already closed"));
    return;
  }
  if (!backing_file_.Flush())
    ThrowNativeIOWithError(exception_state, backing_file_.GetLastFileError());
}

void NativeIOFileSync::Trace(Visitor* visitor) const {
  visitor->Trace(backend_file_);
  visitor->Trace(capacity_tracker_);
  ScriptWrappable::Trace(visitor);
}

void NativeIOFileSync::OnBackendDisconnect() {
  backend_file_.reset();
  backing_file_.Close();
}

}  // namespace blink
