// deno-lint-ignore-file
// Copyright 2018-2026 the Deno authors. MIT license.
// Copyright Joyent and Node contributors. All rights reserved. MIT license.

"use strict";

(function () {
const { core, primordials } = __bootstrap;
const {
  ArrayIsArray,
  ArrayPrototypeConcat,
  ArrayPrototypeIncludes,
  ArrayPrototypeMap,
  ArrayPrototypePush,
  Error,
  MathMax,
  Number,
  NumberIsNaN,
  ObjectAssign,
  ObjectKeys,
  SafeSet,
  String,
  StringFromCharCode,
  StringPrototypeCharCodeAt,
  StringPrototypeSlice,
  StringPrototypeToLowerCase,
  Symbol,
} = primordials;

const { op_http2_error_string, op_http2_http_state } = core.ops;
const lazyHttpCommon = core.createLazyLoader("node:_http_common");
const lazyProcess = core.createLazyLoader("node:process");
const { codes, hideStackFrames } = core.loadExtScript(
  "ext:deno_node/internal/errors.ts",
);
const {
  HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS,
  HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE,
  HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD,
  HTTP2_HEADER_AGE,
  HTTP2_HEADER_AUTHORITY,
  HTTP2_HEADER_AUTHORIZATION,
  HTTP2_HEADER_CONNECTION,
  HTTP2_HEADER_CONTENT_ENCODING,
  HTTP2_HEADER_CONTENT_LANGUAGE,
  HTTP2_HEADER_CONTENT_LENGTH,
  HTTP2_HEADER_CONTENT_LOCATION,
  HTTP2_HEADER_CONTENT_MD5,
  HTTP2_HEADER_CONTENT_RANGE,
  HTTP2_HEADER_CONTENT_TYPE,
  HTTP2_HEADER_COOKIE,
  HTTP2_HEADER_DATE,
  HTTP2_HEADER_DNT,
  HTTP2_HEADER_ETAG,
  HTTP2_HEADER_EXPIRES,
  HTTP2_HEADER_FROM,
  HTTP2_HEADER_HOST,
  HTTP2_HEADER_HTTP2_SETTINGS,
  HTTP2_HEADER_IF_MATCH,
  HTTP2_HEADER_IF_MODIFIED_SINCE,
  HTTP2_HEADER_IF_NONE_MATCH,
  HTTP2_HEADER_IF_RANGE,
  HTTP2_HEADER_IF_UNMODIFIED_SINCE,
  HTTP2_HEADER_KEEP_ALIVE,
  HTTP2_HEADER_LAST_MODIFIED,
  HTTP2_HEADER_LOCATION,
  HTTP2_HEADER_MAX_FORWARDS,
  HTTP2_HEADER_METHOD,
  HTTP2_HEADER_PATH,
  HTTP2_HEADER_PROTOCOL,
  HTTP2_HEADER_PROXY_AUTHORIZATION,
  HTTP2_HEADER_PROXY_CONNECTION,
  HTTP2_HEADER_RANGE,
  HTTP2_HEADER_REFERER,
  HTTP2_HEADER_RETRY_AFTER,
  HTTP2_HEADER_SCHEME,
  HTTP2_HEADER_SET_COOKIE,
  HTTP2_HEADER_STATUS,
  HTTP2_HEADER_TE,
  HTTP2_HEADER_TK,
  HTTP2_HEADER_TRANSFER_ENCODING,
  HTTP2_HEADER_UPGRADE,
  HTTP2_HEADER_UPGRADE_INSECURE_REQUESTS,
  HTTP2_HEADER_USER_AGENT,
  HTTP2_HEADER_X_CONTENT_TYPE_OPTIONS,
  HTTP2_METHOD_CONNECT,
  HTTP2_METHOD_DELETE,
  HTTP2_METHOD_GET,
  HTTP2_METHOD_HEAD,
  NGHTTP2_NV_FLAG_NO_INDEX,
  NGHTTP2_NV_FLAG_NONE,
  NGHTTP2_SESSION_CLIENT,
  NGHTTP2_SESSION_SERVER,
} = core.loadExtScript("ext:deno_node/internal/http2/constants.ts");

const {
  ERR_HTTP2_CONNECT_AUTHORITY,
  ERR_HTTP2_CONNECT_PATH,
  ERR_HTTP2_CONNECT_SCHEME,
  ERR_HTTP2_HEADER_SINGLE_VALUE,
  ERR_HTTP2_INVALID_CONNECTION_HEADERS,
  ERR_HTTP2_INVALID_PSEUDOHEADER,
  ERR_HTTP2_INVALID_SETTING_VALUE,
  ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS,
  ERR_INVALID_ARG_TYPE,
  ERR_INVALID_HTTP_TOKEN,
} = codes;

const kAuthority = Symbol("authority");
const kSensitiveHeaders = Symbol("sensitiveHeaders");
const kSocket = Symbol("socket");
const kProtocol = Symbol("protocol");
const kProxySocket = Symbol("proxySocket");
const kRequest = Symbol("request");
const kStrictSingleValueFields = Symbol("strictSingleValueFields");

// This set is defined strictly by the HTTP/2 specification. Only
// :-prefixed headers defined by that specification may be added to
// this set.
const kValidPseudoHeaders = new SafeSet([
  HTTP2_HEADER_STATUS,
  HTTP2_HEADER_METHOD,
  HTTP2_HEADER_AUTHORITY,
  HTTP2_HEADER_SCHEME,
  HTTP2_HEADER_PATH,
  HTTP2_HEADER_PROTOCOL,
]);

// This set contains headers that are permitted to have only a single
// value. Multiple instances must not be specified.
const kSingleValueHeaders = new SafeSet([
  HTTP2_HEADER_STATUS,
  HTTP2_HEADER_METHOD,
  HTTP2_HEADER_AUTHORITY,
  HTTP2_HEADER_SCHEME,
  HTTP2_HEADER_PATH,
  HTTP2_HEADER_PROTOCOL,
  HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS,
  HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE,
  HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD,
  HTTP2_HEADER_AGE,
  HTTP2_HEADER_AUTHORIZATION,
  HTTP2_HEADER_CONTENT_ENCODING,
  HTTP2_HEADER_CONTENT_LANGUAGE,
  HTTP2_HEADER_CONTENT_LENGTH,
  HTTP2_HEADER_CONTENT_LOCATION,
  HTTP2_HEADER_CONTENT_MD5,
  HTTP2_HEADER_CONTENT_RANGE,
  HTTP2_HEADER_CONTENT_TYPE,
  HTTP2_HEADER_DATE,
  HTTP2_HEADER_DNT,
  HTTP2_HEADER_ETAG,
  HTTP2_HEADER_EXPIRES,
  HTTP2_HEADER_FROM,
  HTTP2_HEADER_HOST,
  HTTP2_HEADER_IF_MATCH,
  HTTP2_HEADER_IF_MODIFIED_SINCE,
  HTTP2_HEADER_IF_NONE_MATCH,
  HTTP2_HEADER_IF_RANGE,
  HTTP2_HEADER_IF_UNMODIFIED_SINCE,
  HTTP2_HEADER_LAST_MODIFIED,
  HTTP2_HEADER_LOCATION,
  HTTP2_HEADER_MAX_FORWARDS,
  HTTP2_HEADER_PROXY_AUTHORIZATION,
  HTTP2_HEADER_RANGE,
  HTTP2_HEADER_REFERER,
  HTTP2_HEADER_RETRY_AFTER,
  HTTP2_HEADER_TK,
  HTTP2_HEADER_UPGRADE_INSECURE_REQUESTS,
  HTTP2_HEADER_USER_AGENT,
  HTTP2_HEADER_X_CONTENT_TYPE_OPTIONS,
]);

// The HTTP methods in this set are specifically defined as assigning no
// meaning to the request payload. By default, unless the user explicitly
// overrides the endStream option on the request method, the endStream
// option will be defaulted to true when these methods are used.
const kNoPayloadMethods = new SafeSet([
  HTTP2_METHOD_DELETE,
  HTTP2_METHOD_GET,
  HTTP2_METHOD_HEAD,
]);

// The following ArrayBuffer instances are used to share memory more efficiently
// with the native binding side for a number of methods. These are not intended
// to be used directly by users in any way. The ArrayBuffers are created on
// the native side with values that are filled in on demand, the js code then
// reads those values out. The set of IDX constants that follow identify the
// relevant data positions within these buffers.
// NOTE: `op_http2_http_state()` returns Uint32Array/Float32Arrays that alias
// thread-local Rust buffers used by the native HTTP/2 binding. When this
// module is first evaluated during snapshot creation, the typed array's
// external backing pointer captured at snapshot time no longer references the
// runtime process's thread-local memory after deserialization, so writes from
// JS never reach Rust. Re-bind the buffers on first runtime use to point at
// the current process's thread-local memory.
let { settingsBuffer, optionsBuffer, sessionState, streamState } =
  op_http2_http_state();
let httpStateRuntimeReady = false;
function ensureHttpStateBuffers() {
  if (httpStateRuntimeReady) return;
  httpStateRuntimeReady = true;
  ({ settingsBuffer, optionsBuffer, sessionState, streamState } =
    op_http2_http_state());
}

const IDX_SETTINGS_HEADER_TABLE_SIZE = 0;
const IDX_SETTINGS_ENABLE_PUSH = 1;
const IDX_SETTINGS_INITIAL_WINDOW_SIZE = 2;
const IDX_SETTINGS_MAX_FRAME_SIZE = 3;
const IDX_SETTINGS_MAX_CONCURRENT_STREAMS = 4;
const IDX_SETTINGS_MAX_HEADER_LIST_SIZE = 5;
const IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL = 6;
const IDX_SETTINGS_FLAGS = 7;

// Maximum number of allowed additional settings
const MAX_ADDITIONAL_SETTINGS = 10;

const IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE = 0;
const IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH = 1;
const IDX_SESSION_STATE_NEXT_STREAM_ID = 2;
const IDX_SESSION_STATE_LOCAL_WINDOW_SIZE = 3;
const IDX_SESSION_STATE_LAST_PROC_STREAM_ID = 4;
const IDX_SESSION_STATE_REMOTE_WINDOW_SIZE = 5;
const IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE = 6;
const IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE = 7;
const IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE = 8;
const IDX_STREAM_STATE = 0;
const IDX_STREAM_STATE_WEIGHT = 1;
const IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT = 2;
const IDX_STREAM_STATE_LOCAL_CLOSE = 3;
const IDX_STREAM_STATE_REMOTE_CLOSE = 4;
const IDX_STREAM_STATE_LOCAL_WINDOW_SIZE = 5;

const IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE = 0;
const IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS = 1;
const IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH = 2;
const IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS = 3;
const IDX_OPTIONS_PADDING_STRATEGY = 4;
const IDX_OPTIONS_MAX_HEADER_LIST_PAIRS = 5;
const IDX_OPTIONS_MAX_OUTSTANDING_PINGS = 6;
const IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS = 7;
const IDX_OPTIONS_MAX_SESSION_MEMORY = 8;
const IDX_OPTIONS_MAX_SETTINGS = 9;
const IDX_OPTIONS_STREAM_RESET_RATE = 10;
const IDX_OPTIONS_STREAM_RESET_BURST = 11;
const IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION = 12;
const IDX_OPTIONS_FLAGS = 13;

function updateOptionsBuffer(options) {
  ensureHttpStateBuffers();
  let flags = 0;
  if (typeof options.maxDeflateDynamicTableSize === "number") {
    flags |= 1 << IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE;
    optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE] =
      options.maxDeflateDynamicTableSize;
  }
  if (typeof options.maxReservedRemoteStreams === "number") {
    flags |= 1 << IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS;
    optionsBuffer[IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS] =
      options.maxReservedRemoteStreams;
  }
  if (typeof options.maxSendHeaderBlockLength === "number") {
    flags |= 1 << IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH;
    optionsBuffer[IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH] =
      options.maxSendHeaderBlockLength;
  }
  if (typeof options.peerMaxConcurrentStreams === "number") {
    flags |= 1 << IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS;
    optionsBuffer[IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS] =
      options.peerMaxConcurrentStreams;
  }
  if (typeof options.paddingStrategy === "number") {
    flags |= 1 << IDX_OPTIONS_PADDING_STRATEGY;
    optionsBuffer[IDX_OPTIONS_PADDING_STRATEGY] = options.paddingStrategy;
  }
  if (typeof options.maxHeaderListPairs === "number") {
    flags |= 1 << IDX_OPTIONS_MAX_HEADER_LIST_PAIRS;
    optionsBuffer[IDX_OPTIONS_MAX_HEADER_LIST_PAIRS] =
      options.maxHeaderListPairs;
  }
  if (typeof options.maxOutstandingPings === "number") {
    flags |= 1 << IDX_OPTIONS_MAX_OUTSTANDING_PINGS;
    optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS] =
      options.maxOutstandingPings;
  }
  if (typeof options.maxOutstandingSettings === "number") {
    flags |= 1 << IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS;
    optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS] = MathMax(
      1,
      options.maxOutstandingSettings,
    );
  }
  if (typeof options.maxSessionMemory === "number") {
    flags |= 1 << IDX_OPTIONS_MAX_SESSION_MEMORY;
    optionsBuffer[IDX_OPTIONS_MAX_SESSION_MEMORY] = MathMax(
      1,
      options.maxSessionMemory,
    );
  }
  if (typeof options.maxSettings === "number") {
    flags |= 1 << IDX_OPTIONS_MAX_SETTINGS;
    optionsBuffer[IDX_OPTIONS_MAX_SETTINGS] = MathMax(1, options.maxSettings);
  }
  if (typeof options.streamResetRate === "number") {
    flags |= 1 << IDX_OPTIONS_STREAM_RESET_RATE;
    optionsBuffer[IDX_OPTIONS_STREAM_RESET_RATE] = MathMax(
      1,
      options.streamResetRate,
    );
  }
  if (typeof options.streamResetBurst === "number") {
    flags |= 1 << IDX_OPTIONS_STREAM_RESET_BURST;
    optionsBuffer[IDX_OPTIONS_STREAM_RESET_BURST] = MathMax(
      1,
      options.streamResetBurst,
    );
  }

  if (typeof options.strictFieldWhitespaceValidation === "boolean") {
    flags |= 1 << IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION;
    optionsBuffer[IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION] =
      options.strictFieldWhitespaceValidation === true ? 0 : 1;
  }

  optionsBuffer[IDX_OPTIONS_FLAGS] = flags;
}

function addCustomSettingsToObj() {
  ensureHttpStateBuffers();
  const toRet = {};
  const num = settingsBuffer[IDX_SETTINGS_FLAGS + 1];
  for (let i = 0; i < num; i++) {
    // deno-lint-ignore deno-internal/prefer-primordials
    toRet[settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * i + 1].toString()] =
      Number(settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * i + 2]);
  }
  return toRet;
}

function getDefaultSettings() {
  // Return HTTP/2 default settings as defined in RFC 7540
  // https://httpwg.org/specs/rfc7540.html#SettingValues
  return {
    __proto__: null,
    headerTableSize: 4096,
    enablePush: true,
    initialWindowSize: 65535, // 2^16 - 1
    maxFrameSize: 16384, // 2^14
    maxConcurrentStreams: 4294967295, // 2^32 - 1 (unlimited)
    maxHeaderListSize: 65535, // 2^16 - 1
    maxHeaderSize: 65535, // alias for maxHeaderListSize
    enableConnectProtocol: false,
  };
}

// Remote is a boolean. true to fetch remote settings, false to fetch local.
// this is only called internally
function getSettings(session, remote) {
  ensureHttpStateBuffers();
  if (remote) {
    session.remoteSettings();
  } else {
    session.localSettings();
  }

  const toRet = {
    headerTableSize: settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE],
    enablePush: !!settingsBuffer[IDX_SETTINGS_ENABLE_PUSH],
    initialWindowSize: settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE],
    maxFrameSize: settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE],
    maxConcurrentStreams: settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS],
    maxHeaderListSize: settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE],
    maxHeaderSize: settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE],
    enableConnectProtocol:
      !!settingsBuffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL],
  };
  if (settingsBuffer[IDX_SETTINGS_FLAGS + 1]) {
    toRet.customSettings = addCustomSettingsToObj();
  }
  return toRet;
}

function updateSettingsBuffer(settings) {
  ensureHttpStateBuffers();
  let flags = 0;
  let numCustomSettings = 0;

  if (typeof settings.customSettings === "object") {
    const customSettings = settings.customSettings;
    for (const setting in customSettings) {
      const val = customSettings[setting];
      if (typeof val === "number") {
        let set = false;
        const nsetting = Number(setting);
        if (
          NumberIsNaN(nsetting) ||
          typeof nsetting !== "number" ||
          0 >= nsetting ||
          nsetting > 0xffff
        ) {
          throw new ERR_HTTP2_INVALID_SETTING_VALUE.RangeError(
            "Range Error",
            nsetting,
            0,
            0xffff,
          );
        }
        if (
          NumberIsNaN(val) ||
          typeof val !== "number" ||
          0 >= val ||
          val > 0xffffffff
        ) {
          throw new ERR_HTTP2_INVALID_SETTING_VALUE.RangeError(
            "Range Error",
            val,
            0,
            0xffffffff,
          );
        }
        if (nsetting < IDX_SETTINGS_FLAGS) {
          set = true;
          switch (nsetting) {
            case IDX_SETTINGS_HEADER_TABLE_SIZE:
              flags |= 1 << IDX_SETTINGS_HEADER_TABLE_SIZE;
              settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE] = val;
              break;
            case IDX_SETTINGS_ENABLE_PUSH:
              flags |= 1 << IDX_SETTINGS_ENABLE_PUSH;
              settingsBuffer[IDX_SETTINGS_ENABLE_PUSH] = val;
              break;
            case IDX_SETTINGS_INITIAL_WINDOW_SIZE:
              flags |= 1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE;
              settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE] = val;
              break;
            case IDX_SETTINGS_MAX_FRAME_SIZE:
              flags |= 1 << IDX_SETTINGS_MAX_FRAME_SIZE;
              settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE] = val;
              break;
            case IDX_SETTINGS_MAX_CONCURRENT_STREAMS:
              flags |= 1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS;
              settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS] = val;
              break;
            case IDX_SETTINGS_MAX_HEADER_LIST_SIZE:
              flags |= 1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE;
              settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE] = val;
              break;
            case IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL:
              flags |= 1 << IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL;
              settingsBuffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL] = val;
              break;
            default:
              set = false;
              break;
          }
        }
        if (!set) { // not supported
          let i = 0;
          while (i < numCustomSettings) {
            if (
              settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * i + 1] === nsetting
            ) {
              settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * i + 2] = val;
              break;
            }
            i++;
          }
          if (i === numCustomSettings) {
            if (numCustomSettings === MAX_ADDITIONAL_SETTINGS) {
              throw new ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS();
            }

            settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * numCustomSettings + 1] =
              nsetting;
            settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * numCustomSettings + 2] =
              val;
            numCustomSettings++;
          }
        }
      }
    }
  }
  settingsBuffer[IDX_SETTINGS_FLAGS + 1] = numCustomSettings;

  if (typeof settings.headerTableSize === "number") {
    flags |= 1 << IDX_SETTINGS_HEADER_TABLE_SIZE;
    settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE] = settings.headerTableSize;
  }
  if (typeof settings.maxConcurrentStreams === "number") {
    flags |= 1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS;
    settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS] =
      settings.maxConcurrentStreams;
  }
  if (typeof settings.initialWindowSize === "number") {
    flags |= 1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE;
    settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE] =
      settings.initialWindowSize;
  }
  if (typeof settings.maxFrameSize === "number") {
    flags |= 1 << IDX_SETTINGS_MAX_FRAME_SIZE;
    settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE] = settings.maxFrameSize;
  }
  if (
    typeof settings.maxHeaderListSize === "number" ||
    typeof settings.maxHeaderSize === "number"
  ) {
    flags |= 1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE;
    if (
      settings.maxHeaderSize !== undefined &&
      (settings.maxHeaderSize !== settings.maxHeaderListSize)
    ) {
      lazyProcess().default.emitWarning(
        "settings.maxHeaderSize overwrite settings.maxHeaderListSize",
      );
      settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE] =
        settings.maxHeaderSize;
    } else {
      settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE] =
        settings.maxHeaderListSize;
    }
  }
  if (typeof settings.enablePush === "boolean") {
    flags |= 1 << IDX_SETTINGS_ENABLE_PUSH;
    settingsBuffer[IDX_SETTINGS_ENABLE_PUSH] = Number(settings.enablePush);
  }
  if (typeof settings.enableConnectProtocol === "boolean") {
    flags |= 1 << IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL;
    settingsBuffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL] = Number(
      settings.enableConnectProtocol,
    );
  }

  settingsBuffer[IDX_SETTINGS_FLAGS] = flags;
}

function remoteCustomSettingsToBuffer(remoteCustomSettings) {
  ensureHttpStateBuffers();
  if (remoteCustomSettings.length > MAX_ADDITIONAL_SETTINGS) {
    throw new ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS();
  }
  let numCustomSettings = 0;
  for (let i = 0; i < remoteCustomSettings.length; i++) {
    const nsetting = remoteCustomSettings[i];
    if (
      typeof nsetting === "number" && nsetting <= 0xffff &&
      nsetting >= 0
    ) {
      settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * numCustomSettings + 1] =
        nsetting;
      numCustomSettings++;
    } else {
      throw new ERR_HTTP2_INVALID_SETTING_VALUE.RangeError(
        "Range Error",
        nsetting,
        0,
        0xffff,
      );
    }
  }
  settingsBuffer[IDX_SETTINGS_FLAGS + 1] = numCustomSettings;
}

function getSessionState(session) {
  // Get state directly from session instead of using shared buffers
  // to avoid thread-local storage issues with snapshot serialization
  const state = session.getState();
  return {
    effectiveLocalWindowSize: state.effectiveLocalWindowSize,
    effectiveRecvDataLength: state.effectiveRecvDataLength,
    nextStreamID: state.nextStreamId,
    localWindowSize: state.localWindowSize,
    lastProcStreamID: state.lastProcStreamId,
    remoteWindowSize: state.remoteWindowSize,
    outboundQueueSize: state.outboundQueueSize,
    deflateDynamicTableSize: state.hdDeflateDynamicTableSize,
    inflateDynamicTableSize: state.hdInflateDynamicTableSize,
  };
}

function getStreamState(stream) {
  // Get state directly from stream instead of using shared buffers
  // to avoid thread-local storage issues with snapshot serialization
  const state = stream.getState();
  return {
    state: state.state,
    weight: state.weight,
    sumDependencyWeight: state.sumDependencyWeight,
    localClose: state.localClose,
    remoteClose: state.remoteClose,
    localWindowSize: state.localWindowSize,
  };
}

function isIllegalConnectionSpecificHeader(name, value) {
  switch (name) {
    case HTTP2_HEADER_CONNECTION:
    case HTTP2_HEADER_UPGRADE:
    case HTTP2_HEADER_HTTP2_SETTINGS:
    case HTTP2_HEADER_KEEP_ALIVE:
    case HTTP2_HEADER_PROXY_CONNECTION:
    case HTTP2_HEADER_TRANSFER_ENCODING:
      return true;
    case HTTP2_HEADER_TE:
      return value !== "trailers";
    default:
      return false;
  }
}

const assertValidPseudoHeader = hideStackFrames((key) => {
  if (!kValidPseudoHeaders.has(key)) {
    throw new ERR_HTTP2_INVALID_PSEUDOHEADER(key);
  }
});

const assertValidPseudoHeaderResponse = hideStackFrames((key) => {
  if (key !== ":status") {
    throw new ERR_HTTP2_INVALID_PSEUDOHEADER(key);
  }
});

const assertValidPseudoHeaderTrailer = hideStackFrames((key) => {
  throw new ERR_HTTP2_INVALID_PSEUDOHEADER(key);
});

/**
 * Takes a request headers array, validates it and sets defaults, and returns
 * the resulting headers in NgHeaders string list format.
 * @returns {object}
 */
function prepareRequestHeadersArray(headers, session) {
  let method;
  let scheme;
  let authority;
  let path;
  let protocol;

  // Extract the key psuedo header values from the headers array
  for (let i = 0; i < headers.length; i += 2) {
    if (headers[i][0] !== ":") {
      continue;
    }

    const header = StringPrototypeToLowerCase(headers[i]);
    const value = headers[i + 1];

    if (header === HTTP2_HEADER_METHOD) {
      method = value;
    } else if (header === HTTP2_HEADER_SCHEME) {
      scheme = value;
    } else if (header === HTTP2_HEADER_AUTHORITY) {
      authority = value;
    } else if (header === HTTP2_HEADER_PATH) {
      path = value;
    } else if (header === HTTP2_HEADER_PROTOCOL) {
      protocol = value;
    }
  }

  // We then build an array of any missing pseudo headers, to prepend
  // default values to the given header array:
  const additionalPsuedoHeaders = [];

  if (method === undefined) {
    method = HTTP2_METHOD_GET;
    ArrayPrototypePush(additionalPsuedoHeaders, HTTP2_HEADER_METHOD, method);
  }

  const connect = method === HTTP2_METHOD_CONNECT;

  if (!connect || protocol !== undefined) {
    if (authority === undefined && headers[HTTP2_HEADER_HOST] === undefined) {
      authority = session[kAuthority];
      ArrayPrototypePush(
        additionalPsuedoHeaders,
        HTTP2_HEADER_AUTHORITY,
        authority,
      );
    }
    if (scheme === undefined) {
      scheme = StringPrototypeSlice(session[kProtocol], 0, -1);
      ArrayPrototypePush(additionalPsuedoHeaders, HTTP2_HEADER_SCHEME, scheme);
    }
    if (path === undefined) {
      ArrayPrototypePush(additionalPsuedoHeaders, HTTP2_HEADER_PATH, "/");
    }
  } else {
    if (authority === undefined) {
      throw new ERR_HTTP2_CONNECT_AUTHORITY();
    }
    if (scheme !== undefined) {
      throw new ERR_HTTP2_CONNECT_SCHEME();
    }
    if (path !== undefined) {
      throw new ERR_HTTP2_CONNECT_PATH();
    }
  }

  const rawHeaders = additionalPsuedoHeaders.length
    ? ArrayPrototypeConcat(additionalPsuedoHeaders, headers)
    : headers;

  if (headers[kSensitiveHeaders] !== undefined) {
    rawHeaders[kSensitiveHeaders] = headers[kSensitiveHeaders];
  }

  const headersList = buildNgHeaderString(
    rawHeaders,
    assertValidPseudoHeader,
    session[kStrictSingleValueFields],
  );

  return {
    rawHeaders,
    headersList,
    scheme,
    authority: authority ?? headers[HTTP2_HEADER_HOST],
    method,
  };
}

/**
 * Takes a request headers object, validates it and sets defaults, and returns
 * the resulting headers in object format and NgHeaders string list format.
 * @returns {object}
 */
function prepareRequestHeadersObject(headers, session) {
  const headersObject = ObjectAssign({ __proto__: null }, headers);

  if (headersObject[HTTP2_HEADER_METHOD] === undefined) {
    headersObject[HTTP2_HEADER_METHOD] = HTTP2_METHOD_GET;
  }

  const connect = headersObject[HTTP2_HEADER_METHOD] === HTTP2_METHOD_CONNECT;

  if (!connect || headersObject[HTTP2_HEADER_PROTOCOL] !== undefined) {
    if (getAuthority(headersObject) === undefined) {
      headersObject[HTTP2_HEADER_AUTHORITY] = session[kAuthority];
    }
    if (headersObject[HTTP2_HEADER_SCHEME] === undefined) {
      headersObject[HTTP2_HEADER_SCHEME] = StringPrototypeSlice(
        session[kProtocol],
        0,
        -1,
      );
    }
    if (headersObject[HTTP2_HEADER_PATH] === undefined) {
      headersObject[HTTP2_HEADER_PATH] = "/";
    }
  } else {
    if (headersObject[HTTP2_HEADER_AUTHORITY] === undefined) {
      throw new ERR_HTTP2_CONNECT_AUTHORITY();
    }
    if (headersObject[HTTP2_HEADER_SCHEME] !== undefined) {
      throw new ERR_HTTP2_CONNECT_SCHEME();
    }
    if (headersObject[HTTP2_HEADER_PATH] !== undefined) {
      throw new ERR_HTTP2_CONNECT_PATH();
    }
  }

  const headersList = buildNgHeaderString(
    headersObject,
    assertValidPseudoHeader,
    session[kStrictSingleValueFields],
  );

  return {
    headersObject,
    headersList,
    scheme: headersObject[HTTP2_HEADER_SCHEME],
    authority: getAuthority(headersObject),
    method: headersObject[HTTP2_HEADER_METHOD],
  };
}

const emptyArray = [];
const kNeverIndexFlag = StringFromCharCode(NGHTTP2_NV_FLAG_NO_INDEX);
const kNoHeaderFlags = StringFromCharCode(NGHTTP2_NV_FLAG_NONE);
const kInvalidHeaderValueByte = StringFromCharCode(1);

// The native binding converts this serialized string to Latin-1 and uses a
// zero byte as the field delimiter. Replace value code units whose low byte is
// zero with another invalid HTTP/2 header byte so nghttp2 still rejects or
// drops the header through its normal path without interpreting value data as
// another serialized field.
function escapeNgHeaderValueZeroBytes(value) {
  let escaped = "";
  let start = 0;
  let changed = false;
  for (let i = 0; i < value.length; ++i) {
    if ((StringPrototypeCharCodeAt(value, i) & 0xff) !== 0) {
      continue;
    }
    changed = true;
    escaped += StringPrototypeSlice(value, start, i) +
      kInvalidHeaderValueByte;
    start = i + 1;
  }
  return changed ? escaped + StringPrototypeSlice(value, start) : value;
}

/**
 * Builds an NgHeader string + header count value, validating the header key
 * format, rejecting illegal header configurations, and marking sensitive headers
 * that should not be indexed en route. This takes either a flat map of
 * raw headers ([k1, v1, k2, v2]) or a header object ({ k1: v1, k2: [v2, v3] }).
 * @returns {[string, number]}
 */
function buildNgHeaderString(
  arrayOrMap,
  assertValuePseudoHeader = assertValidPseudoHeader,
  strictSingleValueFields?,
) {
  const checkIsHttpToken = lazyHttpCommon()._checkIsHttpToken;
  let headers = "";
  let pseudoHeaders = "";
  let count = 0;

  const singles = new SafeSet();
  const sensitiveHeaders = arrayOrMap[kSensitiveHeaders] || emptyArray;
  const neverIndex = ArrayPrototypeMap(
    sensitiveHeaders,
    (v) => StringPrototypeToLowerCase(v),
  );

  function processHeader(key, value) {
    key = StringPrototypeToLowerCase(key);
    const isSingleValueHeader = strictSingleValueFields &&
      kSingleValueHeaders.has(key);
    let isArray = ArrayIsArray(value);
    if (isArray) {
      switch (value.length) {
        case 0:
          return;
        case 1:
          value = String(value[0]);
          isArray = false;
          break;
        default:
          if (isSingleValueHeader) {
            throw new ERR_HTTP2_HEADER_SINGLE_VALUE(key);
          }
      }
    } else {
      value = String(value);
    }
    if (isSingleValueHeader) {
      if (singles.has(key)) {
        throw new ERR_HTTP2_HEADER_SINGLE_VALUE(key);
      }
      singles.add(key);
    }
    const flags = ArrayPrototypeIncludes(neverIndex, key)
      ? kNeverIndexFlag
      : kNoHeaderFlags;
    if (key[0] === ":") {
      const err = assertValuePseudoHeader(key);
      if (err !== undefined) {
        throw err;
      }
      value = escapeNgHeaderValueZeroBytes(value);
      pseudoHeaders += `${key}\0${value}\0${flags}`;
      count++;
      return;
    }
    if (!checkIsHttpToken(key)) {
      throw new ERR_INVALID_HTTP_TOKEN("Header name", key);
    }
    if (isIllegalConnectionSpecificHeader(key, value)) {
      throw new ERR_HTTP2_INVALID_CONNECTION_HEADERS(key);
    }
    if (isArray) {
      for (let j = 0; j < value.length; ++j) {
        const val = escapeNgHeaderValueZeroBytes(String(value[j]));
        headers += `${key}\0${val}\0${flags}`;
      }
      count += value.length;
      return;
    }
    value = escapeNgHeaderValueZeroBytes(value);
    headers += `${key}\0${value}\0${flags}`;
    count++;
  }

  if (ArrayIsArray(arrayOrMap)) {
    for (let i = 0; i < arrayOrMap.length; i += 2) {
      const key = arrayOrMap[i];
      const value = arrayOrMap[i + 1];
      if (value === undefined || key === "") {
        continue;
      }
      processHeader(key, value);
    }
  } else {
    const keys = ObjectKeys(arrayOrMap);
    for (let i = 0; i < keys.length; ++i) {
      const key = keys[i];
      const value = arrayOrMap[key];
      if (value === undefined || key === "") {
        continue;
      }
      processHeader(key, value);
    }
  }

  return [pseudoHeaders + headers, count];
}

// Messages for the custom error codes that nghttp2 callbacks can hand back
// from the native binding (see custom_recv_error_code in ext/node/ops/http2).
// Mirrors the templates in Node's lib/internal/errors.js.
const kCustomErrorMessages = {
  __proto__: null,
  ERR_HTTP2_TOO_MANY_INVALID_FRAMES: "Too many invalid HTTP/2 frames",
};

class NghttpError extends Error {
  constructor(integerCode, customErrorCode) {
    super(
      customErrorCode
        ? kCustomErrorMessages[customErrorCode] ?? customErrorCode
        : op_http2_error_string(integerCode),
    );
    this.code = customErrorCode || "ERR_HTTP2_ERROR";
    this.errno = integerCode;
  }

  toString() {
    return `${this.name} [${this.code}]: ${this.message}`;
  }
}

const assertIsObject = hideStackFrames((value, name, types) => {
  if (
    value !== undefined &&
    (value === null ||
      typeof value !== "object" ||
      ArrayIsArray(value))
  ) {
    throw new ERR_INVALID_ARG_TYPE(
      name,
      types || "Object",
      value,
    );
  }
});

const assertIsArray = hideStackFrames((value, name, types) => {
  if (
    value !== undefined &&
    (value === null ||
      !ArrayIsArray(value))
  ) {
    throw new ERR_INVALID_ARG_TYPE(
      name,
      types || "Array",
      value,
    );
  }
});

const assertWithinRange = hideStackFrames(
  (name, value, min = 0, max = Infinity) => {
    if (
      value !== undefined &&
      (typeof value !== "number" || value < min || value > max)
    ) {
      throw new ERR_HTTP2_INVALID_SETTING_VALUE.RangeError.HideStackFramesError(
        name,
        value,
        min,
        max,
      );
    }
  },
);

function toHeaderObject(headers, sensitiveHeaders) {
  const obj = { __proto__: null };
  for (let n = 0; n < headers.length; n += 2) {
    const name = headers[n];
    let value = headers[n + 1];
    if (name === HTTP2_HEADER_STATUS) {
      value |= 0;
    }
    const existing = obj[name];
    if (existing === undefined) {
      obj[name] = name === HTTP2_HEADER_SET_COOKIE ? [value] : value;
    } else if (!kSingleValueHeaders.has(name)) {
      switch (name) {
        case HTTP2_HEADER_COOKIE:
          // https://tools.ietf.org/html/rfc7540#section-8.1.2.5
          // "...If there are multiple Cookie header fields after decompression,
          //  these MUST be concatenated into a single octet string using the
          //  two-octet delimiter of 0x3B, 0x20 (the ASCII string "; ") before
          //  being passed into a non-HTTP/2 context."
          obj[name] = `${existing}; ${value}`;
          break;
        case HTTP2_HEADER_SET_COOKIE:
          // https://tools.ietf.org/html/rfc7230#section-3.2.2
          // "Note: In practice, the "Set-Cookie" header field ([RFC6265]) often
          // appears multiple times in a response message and does not use the
          // list syntax, violating the above requirements on multiple header
          // fields with the same name.  Since it cannot be combined into a
          // single field-value, recipients ought to handle "Set-Cookie" as a
          // special case while processing header fields."
          ArrayPrototypePush(existing, value);
          break;
        default:
          // https://tools.ietf.org/html/rfc7230#section-3.2.2
          // "A recipient MAY combine multiple header fields with the same field
          // name into one "field-name: field-value" pair, without changing the
          // semantics of the message, by appending each subsequent field value
          // to the combined field value in order, separated by a comma."
          obj[name] = `${existing}, ${value}`;
          break;
      }
    }
  }
  obj[kSensitiveHeaders] = sensitiveHeaders;
  return obj;
}

function isPayloadMeaningless(method) {
  return kNoPayloadMethods.has(method);
}

function sessionName(type) {
  switch (type) {
    case NGHTTP2_SESSION_CLIENT:
      return "client";
    case NGHTTP2_SESSION_SERVER:
      return "server";
    default:
      return "<invalid>";
  }
}

function getAuthority(headers) {
  // For non-CONNECT requests, HTTP/2 allows either :authority
  // or Host to be used equivalently. The first is preferred
  // when making HTTP/2 requests, and the latter is preferred
  // when converting from an HTTP/1 message.
  if (headers[HTTP2_HEADER_AUTHORITY] !== undefined) {
    return headers[HTTP2_HEADER_AUTHORITY];
  }
  if (headers[HTTP2_HEADER_HOST] !== undefined) {
    return headers[HTTP2_HEADER_HOST];
  }
}

return {
  assertIsArray,
  assertIsObject,
  assertValidPseudoHeader,
  assertValidPseudoHeaderResponse,
  assertValidPseudoHeaderTrailer,
  assertWithinRange,
  buildNgHeaderString,
  getAuthority,
  getDefaultSettings,
  getSessionState,
  getSettings,
  getStreamState,
  isPayloadMeaningless,
  kAuthority,
  kProtocol,
  kProxySocket,
  kRequest,
  kSensitiveHeaders,
  kSocket,
  kStrictSingleValueFields,
  MAX_ADDITIONAL_SETTINGS,
  NghttpError,
  prepareRequestHeadersArray,
  prepareRequestHeadersObject,
  remoteCustomSettingsToBuffer,
  sessionName,
  toHeaderObject,
  updateOptionsBuffer,
  updateSettingsBuffer,
  default: {
    assertIsObject,
    assertIsArray,
    assertValidPseudoHeader,
    assertValidPseudoHeaderResponse,
    assertValidPseudoHeaderTrailer,
    assertWithinRange,
    buildNgHeaderString,
    getAuthority,
    getDefaultSettings,
    getSessionState,
    getSettings,
    getStreamState,
    isPayloadMeaningless,
    kAuthority,
    kSensitiveHeaders,
    kSocket,
    kStrictSingleValueFields,
    kProtocol,
    kProxySocket,
    kRequest,
    MAX_ADDITIONAL_SETTINGS,
    NghttpError,
    prepareRequestHeadersArray,
    prepareRequestHeadersObject,
    remoteCustomSettingsToBuffer,
    sessionName,
    toHeaderObject,
    updateOptionsBuffer,
    updateSettingsBuffer,
  },
};
})();
