// Copyright 2015 The Chromium Authors
// 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/webusb/usb_device.h"

#include <limits>
#include <optional>
#include <utility>

#include "base/containers/span.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/core/v8/to_v8_traits.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_union_arraybuffer_arraybufferview.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_usb_control_transfer_parameters.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_usb_direction.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer_view.h"
#include "third_party/blink/renderer/modules/webusb/usb.h"
#include "third_party/blink/renderer/modules/webusb/usb_configuration.h"
#include "third_party/blink/renderer/modules/webusb/usb_in_transfer_result.h"
#include "third_party/blink/renderer/modules/webusb/usb_isochronous_in_transfer_result.h"
#include "third_party/blink/renderer/modules/webusb/usb_isochronous_out_transfer_result.h"
#include "third_party/blink/renderer/modules/webusb/usb_out_transfer_result.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"

using device::mojom::blink::UsbClaimInterfaceResult;
using device::mojom::blink::UsbControlTransferParamsPtr;
using device::mojom::blink::UsbControlTransferRecipient;
using device::mojom::blink::UsbControlTransferType;
using device::mojom::blink::UsbDevice;
using device::mojom::blink::UsbDeviceInfoPtr;
using device::mojom::blink::UsbIsochronousPacketPtr;
using device::mojom::blink::UsbOpenDeviceError;
using device::mojom::blink::UsbTransferDirection;
using device::mojom::blink::UsbTransferStatus;

namespace blink {

namespace {

const char kAccessDeniedError[] = "Access denied.";
const char kPacketLengthsTooBig[] =
    "The total packet length exceeded the maximum size.";
const char kBufferSizeMismatch[] =
    "The data buffer size must match the total packet length.";
const char kDeviceStateChangeInProgress[] =
    "An operation that changes the device state is in progress.";
const char kDeviceDisconnected[] = "The device was disconnected.";
const char kInterfaceNotFound[] =
    "The interface number provided is not supported by the device in its "
    "current configuration.";
const char kInterfaceStateChangeInProgress[] =
    "An operation that changes interface state is in progress.";
const char kOpenRequired[] = "The device must be opened first.";
const char kProtectedInterfaceClassError[] =
    "The requested interface implements a protected class.";
const char kTransferPermissionDeniedError[] = "The transfer was not allowed.";

const int kUsbTransferLengthLimit = 32 * 1024 * 1024;

bool CheckFatalTransferStatus(ScriptPromiseResolverBase* resolver,
                              const UsbTransferStatus& status) {
  switch (status) {
    case UsbTransferStatus::TRANSFER_ERROR:
      resolver->RejectWithDOMException(DOMExceptionCode::kNetworkError,
                                       "A transfer error has occurred.");
      return true;
    case UsbTransferStatus::PERMISSION_DENIED:
      resolver->RejectWithSecurityError(kTransferPermissionDeniedError,
                                        kTransferPermissionDeniedError);
      return true;
    case UsbTransferStatus::TIMEOUT:
      resolver->RejectWithDOMException(DOMExceptionCode::kTimeoutError,
                                       "The transfer timed out.");
      return true;
    case UsbTransferStatus::CANCELLED:
      resolver->RejectWithDOMException(DOMExceptionCode::kAbortError,
                                       "The transfer was cancelled.");
      return true;
    case UsbTransferStatus::DISCONNECT:
      resolver->RejectWithDOMException(DOMExceptionCode::kNotFoundError,
                                       kDeviceDisconnected);
      return true;
    case UsbTransferStatus::COMPLETED:
    case UsbTransferStatus::STALLED:
    case UsbTransferStatus::BABBLE:
    case UsbTransferStatus::SHORT_PACKET:
      return false;
    default:
      NOTREACHED();
  }
}

V8USBTransferStatus ConvertTransferStatus(const UsbTransferStatus& status) {
  switch (status) {
    case UsbTransferStatus::COMPLETED:
    case UsbTransferStatus::SHORT_PACKET:
      return V8USBTransferStatus(V8USBTransferStatus::Enum::kOk);
    case UsbTransferStatus::STALLED:
      return V8USBTransferStatus(V8USBTransferStatus::Enum::kStall);
    case UsbTransferStatus::BABBLE:
      return V8USBTransferStatus(V8USBTransferStatus::Enum::kBabble);
    case UsbTransferStatus::TRANSFER_ERROR:
    case UsbTransferStatus::PERMISSION_DENIED:
    case UsbTransferStatus::TIMEOUT:
    case UsbTransferStatus::CANCELLED:
    case UsbTransferStatus::DISCONNECT:
      NOTREACHED();
  }
}

// Returns the sum of `packet_lengths`, or nullopt if the sum would overflow.
std::optional<uint32_t> TotalPacketLength(
    const Vector<unsigned>& packet_lengths) {
  uint32_t total_bytes = 0;
  for (const auto packet_length : packet_lengths) {
    // Check for overflow.
    if (std::numeric_limits<uint32_t>::max() - total_bytes < packet_length) {
      return std::nullopt;
    }
    total_bytes += packet_length;
  }
  return total_bytes;
}

bool ShouldRejectUsbTransferLength(size_t length,
                                   ExceptionState& exception_state) {
  if (!base::FeatureList::IsEnabled(
          blink::features::kWebUSBTransferSizeLimit)) {
    return false;
  }

  if (length <= kUsbTransferLengthLimit) {
    return false;
  }
  exception_state.ThrowDOMException(
      DOMExceptionCode::kDataError,
      String::Format(
          "The data buffer exceeded supported maximum size of %d bytes",
          kUsbTransferLengthLimit));
  return true;
}

}  // namespace

USBDevice::USBDevice(USB* parent,
                     UsbDeviceInfoPtr device_info,
                     mojo::PendingRemote<UsbDevice> device,
                     ExecutionContext* context)
    : ExecutionContextLifecycleObserver(context),
      parent_(parent),
      device_info_(std::move(device_info)),
      device_(context),
      opened_(false),
      device_state_change_in_progress_(false),
      configuration_index_(kNotFound) {
  device_.Bind(std::move(device),
               context->GetTaskRunner(TaskType::kMiscPlatformAPI));
  if (device_.is_bound()) {
    device_.set_disconnect_handler(
        BindOnce(&USBDevice::OnConnectionError, WrapWeakPersistent(this)));
  }

  for (wtf_size_t i = 0; i < Info().configurations.size(); ++i)
    configurations_.push_back(USBConfiguration::Create(this, i));

  wtf_size_t configuration_index =
      FindConfigurationIndex(Info().active_configuration);
  if (configuration_index != kNotFound)
    OnConfigurationSelected(true /* success */, configuration_index);
}

USBDevice::~USBDevice() {
  // |m_device| may still be valid but there should be no more outstanding
  // requests because each holds a persistent handle to this object.
  DCHECK(device_requests_.empty());
}

bool USBDevice::IsInterfaceClaimed(wtf_size_t configuration_index,
                                   wtf_size_t interface_index) const {
  return configuration_index_ != kNotFound &&
         configuration_index_ == configuration_index &&
         claimed_interfaces_[interface_index];
}

wtf_size_t USBDevice::SelectedAlternateInterfaceIndex(
    wtf_size_t interface_index) const {
  return selected_alternate_indices_[interface_index];
}

USBConfiguration* USBDevice::configuration() const {
  if (configuration_index_ == kNotFound)
    return nullptr;
  DCHECK_LT(configuration_index_, configurations_.size());
  return configurations_[configuration_index_].Get();
}

HeapVector<Member<USBConfiguration>> USBDevice::configurations() const {
  return configurations_;
}

ScriptPromise<IDLUndefined> USBDevice::open(ScriptState* script_state,
                                            ExceptionState& exception_state) {
  EnsureNoDeviceOrInterfaceChangeInProgress(exception_state);
  if (exception_state.HadException())
    return EmptyPromise();

  if (opened_)
    return ToResolvedUndefinedPromise(script_state);

  auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  device_state_change_in_progress_ = true;
  device_requests_.insert(resolver);
  device_->Open(BindOnce(&USBDevice::AsyncOpen, WrapPersistent(this),
                         WrapPersistent(resolver)));
  return promise;
}

ScriptPromise<IDLUndefined> USBDevice::close(ScriptState* script_state,
                                             ExceptionState& exception_state) {
  EnsureNoDeviceOrInterfaceChangeInProgress(exception_state);
  if (exception_state.HadException())
    return EmptyPromise();

  if (!opened_)
    return ToResolvedUndefinedPromise(script_state);

  auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  device_state_change_in_progress_ = true;
  device_requests_.insert(resolver);
  device_->Close(BindOnce(&USBDevice::AsyncClose, WrapPersistent(this),
                          WrapPersistent(resolver)));
  return promise;
}

ScriptPromise<IDLUndefined> USBDevice::forget(ScriptState* script_state,
                                              ExceptionState& exception_state) {
  if (!GetExecutionContext()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
                                      "Script context has shut down.");
    return EmptyPromise();
  }

  auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();
  parent_->ForgetDevice(device_info_->guid, BindOnce(&USBDevice::AsyncForget,
                                                     WrapPersistent(resolver)));

  return promise;
}

ScriptPromise<IDLUndefined> USBDevice::selectConfiguration(
    ScriptState* script_state,
    uint8_t configuration_value,
    ExceptionState& exception_state) {
  EnsureNoDeviceOrInterfaceChangeInProgress(exception_state);
  if (exception_state.HadException())
    return EmptyPromise();

  if (!opened_) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kOpenRequired);
    return EmptyPromise();
  }

  wtf_size_t configuration_index = FindConfigurationIndex(configuration_value);
  if (configuration_index == kNotFound) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotFoundError,
        "The configuration value provided is not supported by the device.");
    return EmptyPromise();
  }

  if (configuration_index_ == configuration_index)
    return ToResolvedUndefinedPromise(script_state);

  device_state_change_in_progress_ = true;

  auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  device_requests_.insert(resolver);
  device_->SetConfiguration(
      configuration_value,
      BindOnce(&USBDevice::AsyncSelectConfiguration, WrapPersistent(this),
               configuration_index, WrapPersistent(resolver)));
  return promise;
}

ScriptPromise<IDLUndefined> USBDevice::claimInterface(
    ScriptState* script_state,
    uint8_t interface_number,
    ExceptionState& exception_state) {
  EnsureDeviceConfigured(exception_state);
  if (exception_state.HadException())
    return EmptyPromise();

  wtf_size_t interface_index = FindInterfaceIndex(interface_number);
  if (interface_index == kNotFound) {
    exception_state.ThrowDOMException(DOMExceptionCode::kNotFoundError,
                                      kInterfaceNotFound);
    return EmptyPromise();
  }

  if (interface_state_change_in_progress_[interface_index]) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kInterfaceStateChangeInProgress);
    return EmptyPromise();
  }

  if (claimed_interfaces_[interface_index])
    return ToResolvedUndefinedPromise(script_state);

  interface_state_change_in_progress_[interface_index] = true;

  auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  device_requests_.insert(resolver);
  device_->ClaimInterface(
      interface_number,
      BindOnce(&USBDevice::AsyncClaimInterface, WrapPersistent(this),
               interface_index, WrapPersistent(resolver)));
  return promise;
}

ScriptPromise<IDLUndefined> USBDevice::releaseInterface(
    ScriptState* script_state,
    uint8_t interface_number,
    ExceptionState& exception_state) {
  EnsureDeviceConfigured(exception_state);
  if (exception_state.HadException())
    return EmptyPromise();

  wtf_size_t interface_index = FindInterfaceIndex(interface_number);
  if (interface_index == kNotFound) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotFoundError,
        "The interface number provided is not supported by the device in its "
        "current configuration.");
    return EmptyPromise();
  }

  if (interface_state_change_in_progress_[interface_index]) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kInterfaceStateChangeInProgress);
    return EmptyPromise();
  }

  if (!claimed_interfaces_[interface_index])
    return ToResolvedUndefinedPromise(script_state);

  // Mark this interface's endpoints unavailable while its state is
  // changing.
  SetEndpointsForInterface(interface_index, false);
  interface_state_change_in_progress_[interface_index] = true;

  auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  device_requests_.insert(resolver);
  device_->ReleaseInterface(
      interface_number,
      BindOnce(&USBDevice::AsyncReleaseInterface, WrapPersistent(this),
               interface_index, WrapPersistent(resolver)));
  return promise;
}

ScriptPromise<IDLUndefined> USBDevice::selectAlternateInterface(
    ScriptState* script_state,
    uint8_t interface_number,
    uint8_t alternate_setting,
    ExceptionState& exception_state) {
  EnsureInterfaceClaimed(interface_number, exception_state);
  if (exception_state.HadException())
    return EmptyPromise();

  // TODO(reillyg): This is duplicated work.
  wtf_size_t interface_index = FindInterfaceIndex(interface_number);
  DCHECK_NE(interface_index, kNotFound);
  wtf_size_t alternate_index =
      FindAlternateIndex(interface_index, alternate_setting);
  if (alternate_index == kNotFound) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotFoundError,
        "The alternate setting provided is not supported by the device in its "
        "current configuration.");
    return EmptyPromise();
  }

  // Mark this old alternate interface's endpoints unavailable while
  // the change is in progress.
  SetEndpointsForInterface(interface_index, false);
  interface_state_change_in_progress_[interface_index] = true;

  auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  device_requests_.insert(resolver);
  device_->SetInterfaceAlternateSetting(
      interface_number, alternate_setting,
      BindOnce(&USBDevice::AsyncSelectAlternateInterface, WrapPersistent(this),
               interface_index, alternate_index, WrapPersistent(resolver)));
  return promise;
}

ScriptPromise<USBInTransferResult> USBDevice::controlTransferIn(
    ScriptState* script_state,
    const USBControlTransferParameters* setup,
    uint16_t length,
    ExceptionState& exception_state) {
  EnsureNoDeviceOrInterfaceChangeInProgress(exception_state);
  if (exception_state.HadException())
    return EmptyPromise();

  if (!opened_) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kOpenRequired);
    return EmptyPromise();
  }

  auto parameters = ConvertControlTransferParameters(setup, exception_state);
  if (!parameters)
    return EmptyPromise();

  auto* resolver =
      MakeGarbageCollected<ScriptPromiseResolver<USBInTransferResult>>(
          script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  device_requests_.insert(resolver);
  device_->ControlTransferIn(
      std::move(parameters), length, 0,
      resolver->WrapCallbackInScriptScope(blink::BindOnce(
          &USBDevice::AsyncControlTransferIn, WrapPersistent(this))));
  return promise;
}

ScriptPromise<USBOutTransferResult> USBDevice::controlTransferOut(
    ScriptState* script_state,
    const USBControlTransferParameters* setup,
    ExceptionState& exception_state) {
  return controlTransferOut(script_state, setup, {}, exception_state);
}

ScriptPromise<USBOutTransferResult> USBDevice::controlTransferOut(
    ScriptState* script_state,
    const USBControlTransferParameters* setup,
    base::span<const uint8_t> data,
    ExceptionState& exception_state) {
  EnsureNoDeviceOrInterfaceChangeInProgress(exception_state);
  if (exception_state.HadException()) {
    return EmptyPromise();
  }

  if (!opened_) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kOpenRequired);
    return EmptyPromise();
  }

  auto parameters = ConvertControlTransferParameters(setup, exception_state);
  if (!parameters) {
    return EmptyPromise();
  }

  if (ShouldRejectUsbTransferLength(data.size(), exception_state)) {
    return EmptyPromise();
  }

  auto* resolver =
      MakeGarbageCollected<ScriptPromiseResolver<USBOutTransferResult>>(
          script_state, exception_state.GetContext());
  auto promise = resolver->Promise();
  device_requests_.insert(resolver);
  device_->ControlTransferOut(
      std::move(parameters), data, 0,
      resolver->WrapCallbackInScriptScope(
          BindOnce(&USBDevice::AsyncControlTransferOut, WrapPersistent(this),
                   static_cast<uint32_t>(data.size()))));
  return promise;
}

ScriptPromise<IDLUndefined> USBDevice::clearHalt(
    ScriptState* script_state,
    const V8USBDirection& direction,
    uint8_t endpoint_number,
    ExceptionState& exception_state) {
  UsbTransferDirection mojo_direction =
      direction.AsEnum() == V8USBDirection::Enum::kIn
          ? UsbTransferDirection::INBOUND
          : UsbTransferDirection::OUTBOUND;
  EnsureEndpointAvailable(mojo_direction == UsbTransferDirection::INBOUND,
                          endpoint_number, exception_state);
  if (exception_state.HadException())
    return EmptyPromise();

  auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  device_requests_.insert(resolver);
  device_->ClearHalt(mojo_direction, endpoint_number,
                     BindOnce(&USBDevice::AsyncClearHalt, WrapPersistent(this),
                              WrapPersistent(resolver)));
  return promise;
}

ScriptPromise<USBInTransferResult> USBDevice::transferIn(
    ScriptState* script_state,
    uint8_t endpoint_number,
    unsigned length,
    ExceptionState& exception_state) {
  if (ShouldRejectUsbTransferLength(length, exception_state)) {
    return EmptyPromise();
  }
  EnsureEndpointAvailable(/*in_transfer=*/true, endpoint_number,
                          exception_state);
  if (exception_state.HadException())
    return EmptyPromise();

  auto* resolver =
      MakeGarbageCollected<ScriptPromiseResolver<USBInTransferResult>>(
          script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  device_requests_.insert(resolver);
  device_->GenericTransferIn(
      endpoint_number, length, 0,
      resolver->WrapCallbackInScriptScope(
          blink::BindOnce(&USBDevice::AsyncTransferIn, WrapPersistent(this))));
  return promise;
}

ScriptPromise<USBOutTransferResult> USBDevice::transferOut(
    ScriptState* script_state,
    uint8_t endpoint_number,
    base::span<const uint8_t> data,
    ExceptionState& exception_state) {
  EnsureEndpointAvailable(/*in_transfer=*/false, endpoint_number,
                          exception_state);
  if (exception_state.HadException()) {
    return EmptyPromise();
  }

  if (ShouldRejectUsbTransferLength(data.size(), exception_state)) {
    return EmptyPromise();
  }

  auto* resolver =
      MakeGarbageCollected<ScriptPromiseResolver<USBOutTransferResult>>(
          script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  device_requests_.insert(resolver);
  device_->GenericTransferOut(
      endpoint_number, data, 0,
      resolver->WrapCallbackInScriptScope(
          BindOnce(&USBDevice::AsyncTransferOut, WrapPersistent(this),
                   static_cast<uint32_t>(data.size()))));
  return promise;
}

ScriptPromise<USBIsochronousInTransferResult> USBDevice::isochronousTransferIn(
    ScriptState* script_state,
    uint8_t endpoint_number,
    Vector<unsigned> packet_lengths,
    ExceptionState& exception_state) {
  EnsureEndpointAvailable(/*in_transfer=*/true, endpoint_number,
                          exception_state);
  if (exception_state.HadException())
    return EmptyPromise();

  std::optional<uint32_t> total_bytes = TotalPacketLength(packet_lengths);
  if (!total_bytes.has_value()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kDataError,
                                      kPacketLengthsTooBig);
    return EmptyPromise();
  }
  if (ShouldRejectUsbTransferLength(total_bytes.value(), exception_state)) {
    return EmptyPromise();
  }

  auto* resolver = MakeGarbageCollected<
      ScriptPromiseResolver<USBIsochronousInTransferResult>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  device_requests_.insert(resolver);
  device_->IsochronousTransferIn(
      endpoint_number, packet_lengths, 0,
      resolver->WrapCallbackInScriptScope(blink::BindOnce(
          &USBDevice::AsyncIsochronousTransferIn, WrapPersistent(this))));
  return promise;
}

ScriptPromise<USBIsochronousOutTransferResult>
USBDevice::isochronousTransferOut(ScriptState* script_state,
                                  uint8_t endpoint_number,
                                  base::span<const uint8_t> data,
                                  Vector<unsigned> packet_lengths,
                                  ExceptionState& exception_state) {
  EnsureEndpointAvailable(/*in_transfer=*/false, endpoint_number,
                          exception_state);
  if (exception_state.HadException()) {
    return EmptyPromise();
  }

  std::optional<uint32_t> total_bytes = TotalPacketLength(packet_lengths);
  if (!total_bytes.has_value()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kDataError,
                                      kPacketLengthsTooBig);
    return EmptyPromise();
  }
  if (ShouldRejectUsbTransferLength(total_bytes.value(), exception_state)) {
    return EmptyPromise();
  }
  if (total_bytes.value() != data.size()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kDataError,
                                      kBufferSizeMismatch);
    return EmptyPromise();
  }

  auto* resolver = MakeGarbageCollected<
      ScriptPromiseResolver<USBIsochronousOutTransferResult>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  device_requests_.insert(resolver);
  device_->IsochronousTransferOut(
      endpoint_number, data, packet_lengths, 0,
      resolver->WrapCallbackInScriptScope(BindOnce(
          &USBDevice::AsyncIsochronousTransferOut, WrapPersistent(this))));
  return promise;
}

ScriptPromise<IDLUndefined> USBDevice::reset(ScriptState* script_state,
                                             ExceptionState& exception_state) {
  EnsureNoDeviceOrInterfaceChangeInProgress(exception_state);
  if (exception_state.HadException())
    return EmptyPromise();

  if (!opened_) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kOpenRequired);
    return EmptyPromise();
  }

  auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<IDLUndefined>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  device_requests_.insert(resolver);
  device_state_change_in_progress_ = true;
  device_->Reset(BindOnce(&USBDevice::AsyncReset, WrapPersistent(this),
                          WrapPersistent(resolver)));
  return promise;
}

void USBDevice::ContextDestroyed() {
  device_requests_.clear();
}

void USBDevice::Trace(Visitor* visitor) const {
  visitor->Trace(parent_);
  visitor->Trace(device_);
  visitor->Trace(device_requests_);
  visitor->Trace(configurations_);
  ScriptWrappable::Trace(visitor);
  ExecutionContextLifecycleObserver::Trace(visitor);
}

wtf_size_t USBDevice::FindConfigurationIndex(
    uint8_t configuration_value) const {
  const auto& configurations = Info().configurations;
  for (wtf_size_t i = 0; i < configurations.size(); ++i) {
    if (configurations[i]->configuration_value == configuration_value)
      return i;
  }
  return kNotFound;
}

wtf_size_t USBDevice::FindInterfaceIndex(uint8_t interface_number) const {
  DCHECK_NE(configuration_index_, kNotFound);
  const auto& interfaces =
      Info().configurations[configuration_index_]->interfaces;
  for (wtf_size_t i = 0; i < interfaces.size(); ++i) {
    if (interfaces[i]->interface_number == interface_number)
      return i;
  }
  return kNotFound;
}

wtf_size_t USBDevice::FindAlternateIndex(uint32_t interface_index,
                                         uint8_t alternate_setting) const {
  DCHECK_NE(configuration_index_, kNotFound);
  const auto& alternates = Info()
                               .configurations[configuration_index_]
                               ->interfaces[interface_index]
                               ->alternates;
  for (wtf_size_t i = 0; i < alternates.size(); ++i) {
    if (alternates[i]->alternate_setting == alternate_setting)
      return i;
  }
  return kNotFound;
}

void USBDevice::EnsureNoDeviceChangeInProgress(
    ExceptionState& exception_state) const {
  if (!device_.is_bound()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kNotFoundError,
                                      kDeviceDisconnected);
    return;
  }

  if (device_state_change_in_progress_) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kDeviceStateChangeInProgress);
    return;
  }
}

void USBDevice::EnsureNoDeviceOrInterfaceChangeInProgress(
    ExceptionState& exception_state) const {
  EnsureNoDeviceChangeInProgress(exception_state);
  if (exception_state.HadException())
    return;

  if (AnyInterfaceChangeInProgress()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kInterfaceStateChangeInProgress);
    return;
  }
}

void USBDevice::EnsureDeviceConfigured(ExceptionState& exception_state) const {
  EnsureNoDeviceChangeInProgress(exception_state);
  if (exception_state.HadException())
    return;

  if (!opened_) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kOpenRequired);
    return;
  }

  if (configuration_index_ == kNotFound) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kInvalidStateError,
        "The device must have a configuration selected.");
    return;
  }
}

void USBDevice::EnsureInterfaceClaimed(uint8_t interface_number,
                                       ExceptionState& exception_state) const {
  EnsureDeviceConfigured(exception_state);
  if (exception_state.HadException())
    return;

  wtf_size_t interface_index = FindInterfaceIndex(interface_number);
  if (interface_index == kNotFound) {
    exception_state.ThrowDOMException(DOMExceptionCode::kNotFoundError,
                                      kInterfaceNotFound);
    return;
  }

  if (interface_state_change_in_progress_[interface_index]) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kInterfaceStateChangeInProgress);
    return;
  }

  if (!claimed_interfaces_[interface_index]) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kInvalidStateError,
        "The specified interface has not been claimed.");
    return;
  }
}

void USBDevice::EnsureEndpointAvailable(bool in_transfer,
                                        uint8_t endpoint_number,
                                        ExceptionState& exception_state) const {
  EnsureDeviceConfigured(exception_state);
  if (exception_state.HadException())
    return;

  if (endpoint_number == 0 || endpoint_number >= kEndpointsBitsNumber) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kIndexSizeError,
        "The specified endpoint number is out of range.");
    return;
  }

  auto& bit_vector = in_transfer ? in_endpoints_ : out_endpoints_;
  if (!bit_vector[endpoint_number - 1]) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotFoundError,
        "The specified endpoint is not part of a claimed and selected "
        "alternate interface.");
    return;
  }
}

bool USBDevice::AnyInterfaceChangeInProgress() const {
  for (wtf_size_t i = 0; i < interface_state_change_in_progress_.size(); ++i) {
    if (interface_state_change_in_progress_[i])
      return true;
  }
  return false;
}

UsbControlTransferParamsPtr USBDevice::ConvertControlTransferParameters(
    const USBControlTransferParameters* parameters,
    ExceptionState& exception_state) const {
  auto mojo_parameters = device::mojom::blink::UsbControlTransferParams::New();

  switch (parameters->requestType().AsEnum()) {
    case V8USBRequestType::Enum::kStandard:
      mojo_parameters->type = UsbControlTransferType::STANDARD;
      break;
    case V8USBRequestType::Enum::kClass:
      mojo_parameters->type = UsbControlTransferType::CLASS;
      break;
    case V8USBRequestType::Enum::kVendor:
      mojo_parameters->type = UsbControlTransferType::VENDOR;
      break;
  }

  switch (parameters->recipient().AsEnum()) {
    case V8USBRecipient::Enum::kDevice:
      mojo_parameters->recipient = UsbControlTransferRecipient::DEVICE;
      break;
    case V8USBRecipient::Enum::kInterface: {
      uint8_t interface_number = parameters->index() & 0xff;
      EnsureInterfaceClaimed(interface_number, exception_state);
      if (exception_state.HadException())
        return nullptr;

      mojo_parameters->recipient = UsbControlTransferRecipient::INTERFACE;
      break;
    }
    case V8USBRecipient::Enum::kEndpoint: {
      bool in_transfer = parameters->index() & 0x80;
      uint8_t endpoint_number = parameters->index() & 0x0f;
      EnsureEndpointAvailable(in_transfer, endpoint_number, exception_state);
      if (exception_state.HadException())
        return nullptr;

      mojo_parameters->recipient = UsbControlTransferRecipient::ENDPOINT;
      break;
    }
    case V8USBRecipient::Enum::kOther:
      mojo_parameters->recipient = UsbControlTransferRecipient::OTHER;
      break;
  }

  mojo_parameters->request = parameters->request();
  mojo_parameters->value = parameters->value();
  mojo_parameters->index = parameters->index();
  return mojo_parameters;
}

void USBDevice::SetEndpointsForInterface(wtf_size_t interface_index, bool set) {
  const auto& configuration = *Info().configurations[configuration_index_];
  const auto& interface = *configuration.interfaces[interface_index];
  const auto& alternate =
      *interface.alternates[selected_alternate_indices_[interface_index]];
  for (const auto& endpoint : alternate.endpoints) {
    uint8_t endpoint_number = endpoint->endpoint_number;
    if (endpoint_number == 0 || endpoint_number >= kEndpointsBitsNumber)
      continue;  // Ignore endpoints with invalid indices.
    auto& bit_vector = endpoint->direction == UsbTransferDirection::INBOUND
                           ? in_endpoints_
                           : out_endpoints_;
    if (set)
      bit_vector.set(endpoint_number - 1);
    else
      bit_vector.reset(endpoint_number - 1);
  }
}

void USBDevice::AsyncOpen(ScriptPromiseResolver<IDLUndefined>* resolver,
                          device::mojom::blink::UsbOpenDeviceResultPtr result) {
  MarkRequestComplete(resolver);

  if (result->is_success()) {
    OnDeviceOpenedOrClosed(/*opened=*/true);
    resolver->Resolve();
    return;
  }

  DCHECK(result->is_error());
  switch (result->get_error()) {
    case UsbOpenDeviceError::ACCESS_DENIED:
      OnDeviceOpenedOrClosed(false /* not opened */);
      resolver->RejectWithSecurityError(kAccessDeniedError, kAccessDeniedError);
      break;
    case UsbOpenDeviceError::ALREADY_OPEN:
      // This class keeps track of open state and won't try to open a device
      // that is already open.
      NOTREACHED();
  }
}

void USBDevice::AsyncClose(ScriptPromiseResolver<IDLUndefined>* resolver) {
  MarkRequestComplete(resolver);

  OnDeviceOpenedOrClosed(false /* closed */);
  resolver->Resolve();
}

void USBDevice::AsyncForget(ScriptPromiseResolver<IDLUndefined>* resolver) {
  resolver->Resolve();
}

void USBDevice::OnDeviceOpenedOrClosed(bool opened) {
  opened_ = opened;
  if (!opened_) {
    claimed_interfaces_.Fill(false);
    selected_alternate_indices_.Fill(0);
    in_endpoints_.reset();
    out_endpoints_.reset();
  }
  device_state_change_in_progress_ = false;
}

void USBDevice::AsyncSelectConfiguration(
    wtf_size_t configuration_index,
    ScriptPromiseResolver<IDLUndefined>* resolver,
    bool success) {
  MarkRequestComplete(resolver);

  OnConfigurationSelected(success, configuration_index);
  if (success) {
    resolver->Resolve();
  } else {
    resolver->RejectWithDOMException(DOMExceptionCode::kNetworkError,
                                     "Unable to set device configuration.");
  }
}

void USBDevice::OnConfigurationSelected(bool success,
                                        wtf_size_t configuration_index) {
  if (success) {
    configuration_index_ = configuration_index;
    wtf_size_t num_interfaces =
        Info().configurations[configuration_index_]->interfaces.size();
    claimed_interfaces_.resize(num_interfaces);
    claimed_interfaces_.Fill(false);
    interface_state_change_in_progress_.resize(num_interfaces);
    interface_state_change_in_progress_.Fill(false);
    selected_alternate_indices_.resize(num_interfaces);
    selected_alternate_indices_.Fill(0);
    in_endpoints_.reset();
    out_endpoints_.reset();
  }
  device_state_change_in_progress_ = false;
}

void USBDevice::AsyncClaimInterface(
    wtf_size_t interface_index,
    ScriptPromiseResolver<IDLUndefined>* resolver,
    device::mojom::blink::UsbClaimInterfaceResult result) {
  MarkRequestComplete(resolver);

  OnInterfaceClaimedOrUnclaimed(result == UsbClaimInterfaceResult::kSuccess,
                                interface_index);

  switch (result) {
    case UsbClaimInterfaceResult::kSuccess:
      resolver->Resolve();
      break;
    case UsbClaimInterfaceResult::kProtectedClass:
      GetExecutionContext()->AddConsoleMessage(
          MakeGarbageCollected<ConsoleMessage>(
              mojom::blink::ConsoleMessageSource::kJavaScript,
              mojom::blink::ConsoleMessageLevel::kWarning,
              "An attempt to claim a USB device interface "
              "has been blocked because it "
              "implements a protected interface class."));
      resolver->RejectWithSecurityError(kProtectedInterfaceClassError,
                                        kProtectedInterfaceClassError);
      break;
    case UsbClaimInterfaceResult::kFailure:
      resolver->RejectWithDOMException(DOMExceptionCode::kNetworkError,
                                       "Unable to claim interface.");
      break;
  }
}

void USBDevice::AsyncReleaseInterface(
    wtf_size_t interface_index,
    ScriptPromiseResolver<IDLUndefined>* resolver,
    bool success) {
  MarkRequestComplete(resolver);

  OnInterfaceClaimedOrUnclaimed(!success, interface_index);
  if (success) {
    resolver->Resolve();
  } else {
    resolver->RejectWithDOMException(DOMExceptionCode::kNetworkError,
                                     "Unable to release interface.");
  }
}

void USBDevice::OnInterfaceClaimedOrUnclaimed(bool claimed,
                                              wtf_size_t interface_index) {
  if (claimed) {
    claimed_interfaces_[interface_index] = true;
  } else {
    claimed_interfaces_[interface_index] = false;
    selected_alternate_indices_[interface_index] = 0;
  }
  SetEndpointsForInterface(interface_index, claimed);
  interface_state_change_in_progress_[interface_index] = false;
}

void USBDevice::AsyncSelectAlternateInterface(
    wtf_size_t interface_index,
    wtf_size_t alternate_index,
    ScriptPromiseResolver<IDLUndefined>* resolver,
    bool success) {
  MarkRequestComplete(resolver);

  if (success)
    selected_alternate_indices_[interface_index] = alternate_index;
  SetEndpointsForInterface(interface_index, success);
  interface_state_change_in_progress_[interface_index] = false;

  if (success) {
    resolver->Resolve();
  } else {
    resolver->RejectWithDOMException(DOMExceptionCode::kNetworkError,
                                     "Unable to set device interface.");
  }
}

void USBDevice::AsyncControlTransferIn(
    ScriptPromiseResolver<USBInTransferResult>* resolver,
    UsbTransferStatus status,
    base::span<const uint8_t> data) {
  MarkRequestComplete(resolver);

  if (CheckFatalTransferStatus(resolver, status))
    return;

  resolver->Resolve(
      USBInTransferResult::Create(ConvertTransferStatus(status), data));
}

void USBDevice::AsyncControlTransferOut(
    uint32_t transfer_length,
    ScriptPromiseResolver<USBOutTransferResult>* resolver,
    UsbTransferStatus status) {
  MarkRequestComplete(resolver);

  if (CheckFatalTransferStatus(resolver, status))
    return;

  resolver->Resolve(USBOutTransferResult::Create(ConvertTransferStatus(status),
                                                 transfer_length));
}

void USBDevice::AsyncClearHalt(ScriptPromiseResolver<IDLUndefined>* resolver,
                               bool success) {
  MarkRequestComplete(resolver);

  if (success) {
    resolver->Resolve();
  } else {
    resolver->RejectWithDOMException(DOMExceptionCode::kNetworkError,
                                     "Unable to clear endpoint.");
  }
}

void USBDevice::AsyncTransferIn(
    ScriptPromiseResolver<USBInTransferResult>* resolver,
    UsbTransferStatus status,
    base::span<const uint8_t> data) {
  MarkRequestComplete(resolver);

  if (CheckFatalTransferStatus(resolver, status))
    return;

  resolver->Resolve(
      USBInTransferResult::Create(ConvertTransferStatus(status), data));
}

void USBDevice::AsyncTransferOut(
    uint32_t transfer_length,
    ScriptPromiseResolver<USBOutTransferResult>* resolver,
    UsbTransferStatus status) {
  MarkRequestComplete(resolver);

  if (CheckFatalTransferStatus(resolver, status))
    return;

  resolver->Resolve(USBOutTransferResult::Create(ConvertTransferStatus(status),
                                                 transfer_length));
}

void USBDevice::AsyncIsochronousTransferIn(
    ScriptPromiseResolver<USBIsochronousInTransferResult>* resolver,
    base::span<const uint8_t> data,
    Vector<UsbIsochronousPacketPtr> mojo_packets) {
  MarkRequestComplete(resolver);

  DOMArrayBuffer* buffer = DOMArrayBuffer::Create(data);
  HeapVector<Member<USBIsochronousInTransferPacket>> packets;
  packets.reserve(mojo_packets.size());
  uint32_t byte_offset = 0;
  for (const auto& packet : mojo_packets) {
    if (CheckFatalTransferStatus(resolver, packet->status))
      return;

    DOMDataView* data_view = nullptr;
    if (buffer) {
      data_view =
          DOMDataView::Create(buffer, byte_offset, packet->transferred_length);
    }
    packets.push_back(USBIsochronousInTransferPacket::Create(
        ConvertTransferStatus(packet->status),
        NotShared<DOMDataView>(data_view)));
    byte_offset += packet->length;
  }
  resolver->Resolve(USBIsochronousInTransferResult::Create(buffer, packets));
}

void USBDevice::AsyncIsochronousTransferOut(
    ScriptPromiseResolver<USBIsochronousOutTransferResult>* resolver,
    Vector<UsbIsochronousPacketPtr> mojo_packets) {
  MarkRequestComplete(resolver);

  HeapVector<Member<USBIsochronousOutTransferPacket>> packets;
  packets.reserve(mojo_packets.size());
  for (const auto& packet : mojo_packets) {
    if (CheckFatalTransferStatus(resolver, packet->status))
      return;

    packets.push_back(USBIsochronousOutTransferPacket::Create(
        ConvertTransferStatus(packet->status), packet->transferred_length));
  }
  resolver->Resolve(USBIsochronousOutTransferResult::Create(packets));
}

void USBDevice::AsyncReset(ScriptPromiseResolver<IDLUndefined>* resolver,
                           bool success) {
  MarkRequestComplete(resolver);
  device_state_change_in_progress_ = false;

  if (success) {
    resolver->Resolve();
  } else {
    resolver->RejectWithDOMException(DOMExceptionCode::kNetworkError,
                                     "Unable to reset the device.");
  }
}

void USBDevice::OnConnectionError() {
  device_.reset();
  opened_ = false;

  for (auto& resolver : device_requests_) {
    ScriptState* script_state = resolver->GetScriptState();
    if (IsInParallelAlgorithmRunnable(resolver->GetExecutionContext(),
                                      script_state)) {
      ScriptState::Scope script_state_scope(script_state);
      resolver->RejectWithDOMException(DOMExceptionCode::kNotFoundError,
                                       kDeviceDisconnected);
    }
  }
  device_requests_.clear();
}

void USBDevice::MarkRequestComplete(ScriptPromiseResolverBase* resolver) {
  auto request_entry = device_requests_.find(resolver);
  // Since all callbacks are wrapped with a check that the execution context is
  // still valid we can guarantee that `device_requests_` hasn't been cleared
  // yet if we are in this function.
  CHECK(request_entry != device_requests_.end());
  device_requests_.erase(request_entry);
}

}  // namespace blink
