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

#include "extensions/renderer/api/runtime_hooks_delegate.h"

#include <array>
#include <string_view>
#include <vector>

#include "base/check.h"
#include "base/containers/span.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/types/expected.h"
#include "build/build_config.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/v8_value_converter.h"
#include "extensions/common/api/messaging/message.h"
#include "extensions/common/api/messaging/messaging_util.h"
#include "extensions/common/api/messaging/signing_certificate.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/manifest.h"
#include "extensions/common/manifest_handlers/externally_connectable.h"
#include "extensions/common/manifest_handlers/web_accessible_resources_info.h"
#include "extensions/common/mojom/message_port.mojom-shared.h"
#include "extensions/renderer/api/messaging/message_target.h"
#include "extensions/renderer/api/messaging/messaging_util.h"
#include "extensions/renderer/api/messaging/native_renderer_messaging_service.h"
#include "extensions/renderer/bindings/api_binding_types.h"
#include "extensions/renderer/bindings/js_runner.h"
#include "extensions/renderer/extension_frame_helper.h"
#include "extensions/renderer/get_script_context.h"
#include "extensions/renderer/renderer_extension_registry.h"
#include "extensions/renderer/script_context.h"
#include "extensions/renderer/v8_helpers.h"
#include "gin/converter.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "v8/include/v8-function-callback.h"
#include "v8/include/v8-function.h"
#include "v8/include/v8-isolate.h"
#include "v8/include/v8-object.h"
#include "v8/include/v8-primitive.h"
#include "v8/include/v8-template.h"

namespace extensions {

namespace {
using RequestResult = APIBindingHooks::RequestResult;

// Handler for the extensionId property on chrome.runtime.
void GetExtensionId(v8::Local<v8::Name> property_name,
                    const v8::PropertyCallbackInfo<v8::Value>& info) {
  v8::Isolate* isolate = info.GetIsolate();
  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::Context> context =
      info.Holder()->GetCreationContextChecked(isolate);

  ScriptContext* script_context = GetScriptContextFromV8Context(context);
  // This could potentially be invoked after the script context is removed
  // (unlike the handler calls, which should only be invoked for valid
  // contexts).
  if (script_context && script_context->extension()) {
    info.GetReturnValue().Set(
        gin::StringToSymbol(isolate, script_context->extension()->id()));
  }
}

// Handler for the dynamicId property on chrome.runtime.
void GetDynamicId(v8::Local<v8::Name> property_name,
                  const v8::PropertyCallbackInfo<v8::Value>& info) {
  v8::Isolate* isolate = info.GetIsolate();
  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::Context> context =
      info.Holder()->GetCreationContextChecked(isolate);

  ScriptContext* script_context = GetScriptContextFromV8Context(context);
  // This could potentially be invoked after the script context is removed
  // (unlike the handler calls, which should only be invoked for valid
  // contexts).
  if (script_context && script_context->extension()) {
    info.GetReturnValue().Set(
        gin::StringToSymbol(isolate, script_context->extension()->guid()));
  }
}

void EmptySetter(v8::Local<v8::Name> name,
                 v8::Local<v8::Value> value,
                 const v8::PropertyCallbackInfo<v8::Boolean>& info) {
  // Empty setter is required to keep the native data property in "accessor"
  // state even in case the value is updated by user code.
}

constexpr char kGetManifest[] = "runtime.getManifest";
constexpr char kGetVersion[] = "runtime.getVersion";
constexpr char kGetURL[] = "runtime.getURL";
constexpr char kConnect[] = "runtime.connect";
constexpr char kConnectNative[] = "runtime.connectNative";
constexpr char kSendMessage[] = "runtime.sendMessage";
constexpr char kSendNativeMessage[] = "runtime.sendNativeMessage";
constexpr char kGetBackgroundPage[] = "runtime.getBackgroundPage";
constexpr char kGetPackageDirectoryEntry[] = "runtime.getPackageDirectoryEntry";
constexpr char kRequestUpdateCheck[] = "runtime.requestUpdateCheck";

// The custom callback supplied to runtime.getBackgroundPage to find and return
// the background page to the original callback.
void GetBackgroundPageCallback(
    const v8::FunctionCallbackInfo<v8::Value>& info) {
  v8::Isolate* isolate = info.GetIsolate();
  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::Context> context =
      info.This()->GetCreationContextChecked(isolate);

  // Custom callbacks are called with the arguments of the callback function and
  // the response from the API. Since the custom callback here handles all the
  // logic we should have just received the callback function to call with the
  // result.
  DCHECK_EQ(1, info.Length());
  CHECK(info[0]->IsFunction());

  // The ScriptContext should always be valid, because otherwise the
  // getBackgroundPage() request should have been invalidated (and this should
  // never run).
  ScriptContext* script_context = GetScriptContextFromV8ContextChecked(context);

  v8::Local<v8::Value> background_page =
      ExtensionFrameHelper::GetV8BackgroundPageMainFrame(
          isolate, script_context->extension()->id());
  v8::Local<v8::Value> args[] = {background_page};
  script_context->SafeCallFunction(info[0].As<v8::Function>(), std::size(args),
                                   args);
}

// Function used by runtime.requestUpdateCheck to modify the arguments returned
// for an asynchronous request, splitting the returned object into separate
// arguments for callback based requests, with the version wrapped in a details
// object.
// Note: This is to allow the promise version of the API to return a single
// object, while still supporting the previous callback version which expects
// multiple parameters to be passed to the callback.
v8::LocalVector<v8::Value> MassageRequestUpdateCheckResults(
    const v8::LocalVector<v8::Value>& result_args,
    v8::Local<v8::Context> context,
    binding::AsyncResponseType async_type) {
  // If this is not a callback based API call, we don't need to modify anything.
  if (async_type != binding::AsyncResponseType::kCallback) {
    return result_args;
  }

  DCHECK_EQ(1u, result_args.size());
  DCHECK(result_args[0]->IsObject());
  v8::Local<v8::Object> result_obj = result_args[0].As<v8::Object>();

  // The object sent back has two properties on it which we need to split into
  // two separate arguments.
  v8::Local<v8::Value> status;
  bool success =
      v8_helpers::GetProperty(context, result_obj, "status", &status);
  DCHECK(success);
  v8::Local<v8::Value> version;
  success = v8_helpers::GetProperty(context, result_obj, "version", &version);
  DCHECK(success);

  // Version is wrapped as a parameter on a details object.
  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  v8::Local<v8::Object> details = v8::Object::New(isolate);
  auto key = gin::StringToV8(isolate, "version");
  details->CreateDataProperty(context, key, version).Check();
  return v8::LocalVector<v8::Value>(isolate, {status, details});
}

// Constructs a URL string from an `id` and `path`. `id` is directly used as
// the host and can be a static or dynamic (GUID) identifier.
GURL UrlFromPathAndId(const std::string& id, const std::string& path) {
  std::string maybe_slash = !path.empty() && path[0] == '/' ? "" : "/";
  std::string url = base::StrCat(
      {kExtensionScheme, url::kStandardSchemeSeparator, id, maybe_slash, path});
  return GURL(url);
}

// Returns the serialization format to use for the given target extension.
// The decision logic is as follows:
// 1. If the sender is an extension, the sender's extension serialization
//    preference is used to determine the format.
// 2. If the target extension is not installed, returns JSON.
// 3. If the target extension is installed but not externally connectable from
//    the current context, returns JSON.
// 4. If the target extension is installed and externally connectable, the
//    target extension is used to determine the format.
//
//  Note: #2 and #3 are purposefully the same to prevent senders like
//  untrusted web pages from discovering what extensions are installed by
//  changing their message content.
//  TODO(crbug.com/40321352): Consider in the above cases if we should instead
//  not serialize the message at all and return an error. For the
//  purposes of minimizing changes to messaging outside of structured clone
//  serialization we're returning JSON for now.
mojom::SerializationFormat GetSerializationFormat(
    ScriptContext* script_context,
    const std::string& target_id,
    mojom::ChannelType channel_type) {
  if (const Extension* sender_extension = script_context->extension()) {
    return messaging_util::GetSerializationFormat(sender_extension,
                                                  channel_type);
  }

  const Extension* installed_target_extension =
      RendererExtensionRegistry::Get()->GetByID(target_id);
  if (!installed_target_extension) {
    // Extension not installed.
    return mojom::SerializationFormat::kJson;
  }

  const ExternallyConnectableInfo* info =
      ExternallyConnectableInfo::Get(installed_target_extension);
  if (!info || !info->matches.MatchesURL(script_context->url())) {
    // Extension installed, but not externally connectable from context.
    return mojom::SerializationFormat::kJson;
  }

  // Extension installed and externally connectable from context so use the
  // target extension's serialization format.
  return messaging_util::GetSerializationFormat(installed_target_extension,
                                                channel_type);
}

base::expected<std::string, std::string> GetNativeApplicationName(
    ScriptContext* script_context,
    v8::Local<v8::Value> target_arg) {
  v8::Isolate* isolate = script_context->isolate();

  if (target_arg->IsString()) {
#if BUILDFLAG(IS_ANDROID)
    const Extension* extension = script_context->extension();
    CHECK(extension);

    if (!Manifest::IsUnpackedLocation(extension->location())) {
      return base::unexpected(
          "Native messaging on Android requires packed extensions to specify a "
          "NativeMessageTarget object.");
    }
#endif
    return base::ok(gin::V8ToString(isolate, target_arg));
  }

  if (target_arg->IsObject()) {
    v8::Local<v8::Context> v8_context = script_context->v8_context();
    v8::Local<v8::Object> target_object = target_arg.As<v8::Object>();
    v8::Local<v8::Value> application_val;
    if (!target_object->Get(v8_context, gin::StringToV8(isolate, "application"))
             .ToLocal(&application_val) ||
        !application_val->IsString()) {
      return base::unexpected(
          "Native messaging requires a valid 'application' string in the "
          "target object.");
    }
    return base::ok(gin::V8ToString(isolate, application_val));
  }

  return base::unexpected("Invalid native messaging target.");
}

#if BUILDFLAG(IS_ANDROID)
// Parses a SHA-256 certificate string into a 32-byte array, stripping any
// ':' or '-' separators. Returns an error message if the format is invalid.
base::expected<SigningCertificate, std::string> ParseSHA256Certificate(
    std::string_view cert_str) {
  std::string stripped;
  base::RemoveChars(cert_str, ":-", &stripped);

  // Android's PackageManager.hasSigningCertificate (CERT_INPUT_SHA256) expects
  // a 32-byte SHA-256 digest. We accept a case-insensitive 64-character hex
  // string with optional ':' or '-' separators.
  // https://developer.android.com/reference/android/content/pm/PackageManager
  SigningCertificate cert_bytes;
  if (stripped.size() != cert_bytes.size() * 2 ||
      !base::HexStringToSpan(stripped, cert_bytes)) {
    return base::unexpected("Malformed certificate string.");
  }

  return base::ok(cert_bytes);
}

// Extracts and validates the 'androidCertificates' field from `target_arg`.
// Packed extensions on Android are required to provide at least one valid
// certificate. Returns an empty vector if omitted for unpacked extensions.
base::expected<SigningCertificates, std::string> GetCertificates(
    ScriptContext* script_context,
    v8::Local<v8::Value> target_arg) {
  const Extension* extension = script_context->extension();
  CHECK(extension);

  bool is_packed = !Manifest::IsUnpackedLocation(extension->location());

  // `target_arg` not being an object means no certificates were specified. But
  // this path should not be reached by packed extensions (should be caught in
  // GetNativeApplicationName).
  if (!target_arg->IsObject()) {
    CHECK(!is_packed);
    return base::ok(SigningCertificates());
  }

  v8::Local<v8::Context> v8_context = script_context->v8_context();
  v8::Local<v8::Object> target_object = target_arg.As<v8::Object>();
  v8::Isolate* isolate = script_context->isolate();

  v8::Local<v8::Value> certs_val;
  if (!target_object
           ->Get(v8_context, gin::StringToV8(isolate, "androidCertificates"))
           .ToLocal(&certs_val) ||
      certs_val->IsNullOrUndefined()) {
    if (is_packed) {
      return base::unexpected(
          "Packed extensions on Android must specify at least one expected "
          "signing certificate in 'androidCertificates'.");
    }
    return base::ok(SigningCertificates());
  }

  if (!certs_val->IsArray()) {
    return base::unexpected(
        "Property 'androidCertificates' must be an array of strings.");
  }

  v8::Local<v8::Array> certs_array = certs_val.As<v8::Array>();
  uint32_t length = certs_array->Length();
  if (length == 0) {
    if (is_packed) {
      return base::unexpected(
          "Packed extensions on Android must specify at least one expected "
          "signing certificate in 'androidCertificates'.");
    }
    return base::ok(SigningCertificates());
  }

  SigningCertificates certificates;
  certificates.reserve(length);

  for (uint32_t i = 0; i < length; ++i) {
    v8::Local<v8::Value> element;
    if (!certs_array->Get(v8_context, i).ToLocal(&element) ||
        !element->IsString()) {
      return base::unexpected(base::StringPrintf(
          "Signing certificate at index %u is malformed.", i));
    }

    std::string cert_str = gin::V8ToString(isolate, element);
    auto parsed_cert = ParseSHA256Certificate(cert_str);
    if (!parsed_cert.has_value()) {
      return base::unexpected(base::StringPrintf(
          "Signing certificate at index %u is malformed.", i));
    }

    certificates.push_back(*parsed_cert);
  }

  return base::ok(std::move(certificates));
}
#endif  // BUILDFLAG(IS_ANDROID)

}  // namespace

RuntimeHooksDelegate::RuntimeHooksDelegate(
    NativeRendererMessagingService* messaging_service)
    : messaging_service_(messaging_service) {}
RuntimeHooksDelegate::~RuntimeHooksDelegate() = default;

// static
RequestResult RuntimeHooksDelegate::GetURL(
    ScriptContext* script_context,
    const v8::LocalVector<v8::Value>& arguments) {
  DCHECK_EQ(1u, arguments.size());
  DCHECK(arguments[0]->IsString());
  DCHECK(script_context->extension());

  v8::Isolate* isolate = script_context->isolate();
  std::string path = gin::V8ToString(isolate, arguments[0]);
  const auto* extension = script_context->extension();

  GURL url = UrlFromPathAndId(extension->id(), path);
  // GURL considers any possible path valid. Since the argument is only appended
  // as part of the path, there should be no way this could conceivably fail.
  DCHECK(url.is_valid());

  if (WebAccessibleResourcesInfo::ShouldUseDynamicUrl(extension,
                                                      url.GetPath())) {
    GURL::Replacements replacements;
    replacements.SetHostStr(extension->guid());
    url = url.ReplaceComponents(replacements);
  }

  RequestResult result(RequestResult::HANDLED);
  DCHECK(url.is_valid());
  result.return_value = gin::StringToV8(isolate, url.spec());
  return result;
}

RequestResult RuntimeHooksDelegate::HandleRequest(
    const std::string& method_name,
    const APISignature* signature,
    v8::Local<v8::Context> context,
    v8::LocalVector<v8::Value>* arguments,
    const APITypeReferenceMap& refs) {
  using Handler = RequestResult (RuntimeHooksDelegate::*)(
      ScriptContext*, const APISignature::V8ParseResult&);
  static const struct {
    Handler handler;
    std::string_view method;
  } kHandlers[] = {
      {&RuntimeHooksDelegate::HandleSendMessage, kSendMessage},
      {&RuntimeHooksDelegate::HandleConnect, kConnect},
      {&RuntimeHooksDelegate::HandleGetURL, kGetURL},
      {&RuntimeHooksDelegate::HandleGetManifest, kGetManifest},
      {&RuntimeHooksDelegate::HandleGetVersion, kGetVersion},
      {&RuntimeHooksDelegate::HandleConnectNative, kConnectNative},
      {&RuntimeHooksDelegate::HandleSendNativeMessage, kSendNativeMessage},
      {&RuntimeHooksDelegate::HandleGetBackgroundPage, kGetBackgroundPage},
      {&RuntimeHooksDelegate::HandleGetPackageDirectoryEntryCallback,
       kGetPackageDirectoryEntry},
      {&RuntimeHooksDelegate::HandleRequestUpdateCheck, kRequestUpdateCheck},
  };

  ScriptContext* script_context = GetScriptContextFromV8ContextChecked(context);

  Handler handler = nullptr;
  for (const auto& handler_entry : kHandlers) {
    if (handler_entry.method == method_name) {
      handler = handler_entry.handler;
      break;
    }
  }

  if (!handler)
    return RequestResult(RequestResult::NOT_HANDLED);

  bool should_massage = false;
  bool allow_options = false;
  if (method_name == kSendMessage) {
    should_massage = true;
    allow_options = true;
  } else if (method_name == kSendNativeMessage) {
    should_massage = true;
  }

  if (should_massage) {
    messaging_util::MassageSendMessageArguments(v8::Isolate::GetCurrent(),
                                                allow_options, arguments);
  }

  APISignature::V8ParseResult parse_result =
      signature->ParseArgumentsToV8(context, *arguments, refs);
  if (!parse_result.succeeded()) {
    RequestResult result(RequestResult::INVALID_INVOCATION);
    result.error = std::move(*parse_result.error);
    return result;
  }

  return (this->*handler)(script_context, parse_result);
}

void RuntimeHooksDelegate::InitializeTemplate(
    v8::Isolate* isolate,
    v8::Local<v8::ObjectTemplate> object_template,
    const APITypeReferenceMap& type_refs) {
  object_template->SetNativeDataProperty(gin::StringToSymbol(isolate, "id"),
                                         &GetExtensionId, &EmptySetter);
  object_template->SetNativeDataProperty(
      gin::StringToSymbol(isolate, "dynamicId"), &GetDynamicId, &EmptySetter);
}

RequestResult RuntimeHooksDelegate::HandleGetManifest(
    ScriptContext* script_context,
    const APISignature::V8ParseResult& parse_result) {
  DCHECK_EQ(binding::AsyncResponseType::kNone, parse_result.async_type);
  CHECK(script_context->extension());
  CHECK(script_context->extension()->manifest());
  CHECK(script_context->extension()->manifest()->value());

  RequestResult result(RequestResult::HANDLED);
  result.return_value = content::V8ValueConverter::Create()->ToV8Value(
      *script_context->extension()->manifest()->value(),
      script_context->v8_context());

  return result;
}

RequestResult RuntimeHooksDelegate::HandleGetVersion(
    ScriptContext* script_context,
    const APISignature::V8ParseResult& parse_result) {
  DCHECK_EQ(binding::AsyncResponseType::kNone, parse_result.async_type);
  CHECK(script_context->extension());

  RequestResult result(RequestResult::HANDLED);
  result.return_value = content::V8ValueConverter::Create()->ToV8Value(
      script_context->extension()->VersionString(),
      script_context->v8_context());

  return result;
}

RequestResult RuntimeHooksDelegate::HandleGetURL(
    ScriptContext* script_context,
    const APISignature::V8ParseResult& parse_result) {
  DCHECK_EQ(binding::AsyncResponseType::kNone, parse_result.async_type);
  return GetURL(script_context, *parse_result.arguments);
}

RequestResult RuntimeHooksDelegate::HandleSendMessage(
    ScriptContext* script_context,
    const APISignature::V8ParseResult& parse_result) {
  const v8::LocalVector<v8::Value>& arguments = *parse_result.arguments;
  DCHECK_EQ(4u, arguments.size());

  std::string target_id;
  std::string error;
  if (!messaging_util::GetTargetExtensionId(script_context, arguments[0],
                                            "runtime.sendMessage", &target_id,
                                            &error)) {
    RequestResult result(RequestResult::INVALID_INVOCATION);
    result.error = std::move(error);
    return result;
  }

  v8::Local<v8::Context> v8_context = script_context->v8_context();

  v8::Local<v8::Value> v8_message = arguments[1];
  mojom::ChannelType channel_type = mojom::ChannelType::kSendMessage;
  std::optional<Message> message = messaging_util::MessageFromV8(
      v8_context, v8_message,
      GetSerializationFormat(script_context, target_id, channel_type), &error);
  if (!message) {
    RequestResult result(RequestResult::INVALID_INVOCATION);
    result.error = std::move(error);
    return result;
  }

  // Note: arguments[2] is the options argument. However, the only available
  // option for sendMessage() is includeTlsChannelId. That option has no effect
  // since M72, but it is still part of the public spec for compatibility and is
  // parsed into |arguments|. See crbug.com/1045232.

  v8::Local<v8::Function> response_callback;
  if (!arguments[3]->IsNull())
    response_callback = arguments[3].As<v8::Function>();

  v8::Local<v8::Promise> promise = messaging_service_->SendOneTimeMessage(
      script_context, MessageTarget::ForExtension(target_id), channel_type,
      std::move(*message), parse_result.async_type, response_callback);
  DCHECK_EQ(parse_result.async_type == binding::AsyncResponseType::kPromise,
            !promise.IsEmpty())
      << "SendOneTimeMessage should only return a Promise for promise based "
         "API calls, otherwise it should be empty";

  RequestResult result(RequestResult::HANDLED);
  if (parse_result.async_type == binding::AsyncResponseType::kPromise) {
    result.return_value = promise;
  }
  return result;
}

RequestResult RuntimeHooksDelegate::HandleSendNativeMessage(
    ScriptContext* script_context,
    const APISignature::V8ParseResult& parse_result) {
  const v8::LocalVector<v8::Value>& arguments = *parse_result.arguments;
  DCHECK_EQ(3u, arguments.size());

  base::expected<std::string, std::string> application_name =
      GetNativeApplicationName(script_context, arguments[0]);
  if (!application_name.has_value()) {
    RequestResult result(RequestResult::INVALID_INVOCATION);
    result.error = std::move(application_name.error());
    return result;
  }

  v8::Local<v8::Value> v8_message = arguments[1];
  DCHECK(!v8_message.IsEmpty());

  mojom::ChannelType channel_type = mojom::ChannelType::kNative;
  std::string error;
  std::optional<Message> message = messaging_util::MessageFromV8(
      script_context->v8_context(), v8_message,
      messaging_util::GetSerializationFormat(script_context->extension(),
                                             channel_type),
      &error);
  if (!message) {
    RequestResult result(RequestResult::INVALID_INVOCATION);
    result.error = std::move(error);
    return result;
  }

  SigningCertificates android_certificates;
#if BUILDFLAG(IS_ANDROID)
  // Signing certificates are only parsed for Android.
  auto certificates_or_error = GetCertificates(script_context, arguments[0]);
  if (!certificates_or_error.has_value()) {
    RequestResult result(RequestResult::INVALID_INVOCATION);
    result.error = std::move(certificates_or_error.error());
    return result;
  }

  android_certificates = std::move(*certificates_or_error);
#endif

  v8::Local<v8::Function> response_callback;
  if (!arguments[2]->IsNull())
    response_callback = arguments[2].As<v8::Function>();

  // TODO(crbug.com/552709714): Refactor native application name and
  // certificates into a struct so an Android-specific field doesn't show up in
  // function signatures that simply pass it along.
  v8::Local<v8::Promise> promise = messaging_service_->SendOneTimeMessage(
      script_context,
      MessageTarget::ForNativeApp(*application_name,
                                  std::move(android_certificates)),
      channel_type, std::move(*message), parse_result.async_type,
      response_callback);
  DCHECK_EQ(parse_result.async_type == binding::AsyncResponseType::kPromise,
            !promise.IsEmpty())
      << "SendOneTimeMessage should only return a Promise for promise based "
         "API calls, otherwise it should be empty";

  RequestResult result(RequestResult::HANDLED);
  if (parse_result.async_type == binding::AsyncResponseType::kPromise) {
    result.return_value = promise;
  }
  return result;
}

RequestResult RuntimeHooksDelegate::HandleConnect(
    ScriptContext* script_context,
    const APISignature::V8ParseResult& parse_result) {
  const v8::LocalVector<v8::Value>& arguments = *parse_result.arguments;
  DCHECK_EQ(2u, arguments.size());
  DCHECK_EQ(binding::AsyncResponseType::kNone, parse_result.async_type);

  std::string target_id;
  std::string error;
  if (!messaging_util::GetTargetExtensionId(script_context, arguments[0],
                                            "runtime.connect", &target_id,
                                            &error)) {
    RequestResult result(RequestResult::INVALID_INVOCATION);
    result.error = std::move(error);
    return result;
  }

  messaging_util::MessageOptions options;
  if (!arguments[1]->IsNull()) {
    options = messaging_util::ParseMessageOptions(
        script_context->v8_context(), arguments[1].As<v8::Object>(),
        messaging_util::PARSE_CHANNEL_NAME);
  }

  GinPort* port = messaging_service_->Connect(
      script_context, MessageTarget::ForExtension(target_id),
      options.channel_name,
      GetSerializationFormat(script_context, target_id,
                             mojom::ChannelType::kConnect));
  DCHECK(port);
  DCHECK_EQ(binding::AsyncResponseType::kNone, parse_result.async_type);

  RequestResult result(RequestResult::HANDLED);
  result.return_value =
      port->GetWrapper(script_context->isolate()).ToLocalChecked();
  return result;
}

RequestResult RuntimeHooksDelegate::HandleConnectNative(
    ScriptContext* script_context,
    const APISignature::V8ParseResult& parse_result) {
  const v8::LocalVector<v8::Value>& arguments = *parse_result.arguments;
  DCHECK_EQ(1u, arguments.size());
  DCHECK_EQ(binding::AsyncResponseType::kNone, parse_result.async_type);

  base::expected<std::string, std::string> application_name =
      GetNativeApplicationName(script_context, arguments[0]);
  if (!application_name.has_value()) {
    RequestResult result(RequestResult::INVALID_INVOCATION);
    result.error = std::move(application_name.error());
    return result;
  }

  SigningCertificates android_certificates;
#if BUILDFLAG(IS_ANDROID)
  // Signing certificates are only parsed for Android.
  auto certificates_or_error = GetCertificates(script_context, arguments[0]);
  if (!certificates_or_error.has_value()) {
    RequestResult result(RequestResult::INVALID_INVOCATION);
    result.error = std::move(certificates_or_error.error());
    return result;
  }

  android_certificates = std::move(*certificates_or_error);
#endif

  // TODO(crbug.com/552709714): Refactor native application name and
  // certificates into a struct so an Android-specific field doesn't show up in
  // function signatures that simply pass it along.
  GinPort* port = messaging_service_->Connect(
      script_context,
      MessageTarget::ForNativeApp(*application_name,
                                  std::move(android_certificates)),
      std::string(),
      messaging_util::GetSerializationFormat(script_context->extension(),
                                             mojom::ChannelType::kNative));

  RequestResult result(RequestResult::HANDLED);
  result.return_value =
      port->GetWrapper(script_context->isolate()).ToLocalChecked();
  return result;
}

RequestResult RuntimeHooksDelegate::HandleGetBackgroundPage(
    ScriptContext* script_context,
    const APISignature::V8ParseResult& parse_result) {
  DCHECK(script_context->extension());

  RequestResult result(RequestResult::NOT_HANDLED);
  if (!v8::Function::New(script_context->v8_context(),
                         &GetBackgroundPageCallback)
           .ToLocal(&result.custom_callback)) {
    return RequestResult(RequestResult::THROWN);
  }

  return result;
}

RequestResult RuntimeHooksDelegate::HandleGetPackageDirectoryEntryCallback(
    ScriptContext* script_context,
    const APISignature::V8ParseResult& parse_result) {
  // TODO(devlin): This is basically just copied and translated from
  // the JS bindings, and still relies on the custom JS bindings for
  // getBindDirectoryEntryCallback. This entire API is a bit crazy, and needs
  // some help.
  v8::Isolate* isolate = script_context->isolate();
  v8::Local<v8::Context> v8_context = script_context->v8_context();

  v8::MaybeLocal<v8::Value> maybe_custom_callback;
  {  // Begin natives enabled scope (for requiring the module).
    ModuleSystem::NativesEnabledScope enable_natives(
        script_context->module_system());
    content::RenderFrame* background_page =
        ExtensionFrameHelper::GetBackgroundPageFrame(
            script_context->extension()->id());

    // The JS function will sometimes use the background page's context to do
    // some work (see also
    // extensions/renderer/resources/file_entry_binding_util.js).  In order to
    // allow native code to run in the background page, we'll also need a
    // NativesEnabledScope for that context.
    DCHECK(v8_context == isolate->GetCurrentContext());
    std::optional<ModuleSystem::NativesEnabledScope> background_page_natives;
    if (background_page &&
        background_page != script_context->GetRenderFrame() &&
        blink::WebFrame::ScriptCanAccess(isolate,
                                         background_page->GetWebFrame())) {
      ScriptContext* background_page_script_context =
          GetScriptContextFromV8Context(
              background_page->GetWebFrame()->MainWorldScriptContext());
      if (background_page_script_context) {
        background_page_natives.emplace(
            background_page_script_context->module_system());
      }
    }

    v8::Local<v8::Object> file_entry_binding_util;
    // ModuleSystem::Require can return an empty Maybe when it fails for any
    // number of reasons. It *shouldn't* ever throw, but it is technically
    // possible. This makes the handling the failure result complicated. Since
    // this shouldn't happen at all, bail and consider it handled if it fails.
    if (!script_context->module_system()
             ->Require("fileEntryBindingUtil")
             .ToLocal(&file_entry_binding_util)) {
      NOTREACHED();
    }

    v8::Local<v8::Value> get_bind_directory_entry_callback_value;
    if (!file_entry_binding_util
             ->Get(v8_context, gin::StringToSymbol(
                                   isolate, "getBindDirectoryEntryCallback"))
             .ToLocal(&get_bind_directory_entry_callback_value)) {
      NOTREACHED();
    }

    if (!get_bind_directory_entry_callback_value->IsFunction()) {
      NOTREACHED();
    }

    v8::Local<v8::Function> get_bind_directory_entry_callback =
        get_bind_directory_entry_callback_value.As<v8::Function>();

    maybe_custom_callback =
        JSRunner::Get(v8_context)
            ->RunJSFunctionSync(get_bind_directory_entry_callback, v8_context,
                                {});
  }  // End modules enabled scope.
  v8::Local<v8::Value> callback;
  if (!maybe_custom_callback.ToLocal(&callback)) {
    NOTREACHED();
  }

  if (!callback->IsFunction()) {
    NOTREACHED();
  }

  RequestResult result(RequestResult::NOT_HANDLED);
  result.custom_callback = callback.As<v8::Function>();
  return result;
}

RequestResult RuntimeHooksDelegate::HandleRequestUpdateCheck(
    ScriptContext* script_context,
    const APISignature::V8ParseResult& parse_result) {
  return RequestResult(RequestResult::NOT_HANDLED,
                       v8::Local<v8::Function>() /*custom_callback*/,
                       base::BindOnce(MassageRequestUpdateCheckResults));
}

}  // namespace extensions
