blob: c3eb16a71e77d0f2ce4696133d27ac9bc7cf163a [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import { HeaderKind } from './message/header.type.js';
import {
serializeHeaders,
serializeHeaderValue,
deserializeHeaderValue,
HeaderKeyFactory,
type HeaderValue,
type ParsedHeaderValue,
} from './message/header.utils.js';
/** Maximum number of key-value entries in one options block. */
export const MAX_OPTIONS = 1024;
/**
* Maximum total byte length of an encoded options block.
*
* Mirrors the Rust `MAX_OPTIONS_BYTES`, which in turn mirrors the user-headers
* budget: options ride that codec and inherit its limit.
*/
export const MAX_OPTIONS_BYTES = 100 * 1000;
/**
* Key and value length bound, in encoded bytes rather than characters.
* Inherited from the header-field codec rather than being an options-specific
* rule: the server refuses a block carrying a field outside this range.
*/
const MAX_HEADER_FIELD_LENGTH = 255;
/** A resource option entry: UTF-8 string key with a typed value. */
export type OptionEntry = {
key: string,
value: HeaderValue
};
/** Deserialized options block keyed by option name. */
export type ParsedOptions = Record<string, ParsedHeaderValue>;
/** Result of deserializing a length-prefixed options block. */
export type OptionsDeserialized = {
/** Number of bytes consumed, length prefix included */
bytesRead: number,
/** Deserialized options */
options: ParsedOptions
};
/**
* Keeps the last entry for each key, preserving first-seen order.
*
* A block carrying a key twice is refused whole by wire validation, so callers
* that append their own entries ahead of the typed ones rely on this to let the
* typed value win.
*/
export const dedupeOptions = (options: OptionEntry[]): OptionEntry[] => {
const byKey = new Map<string, OptionEntry>();
for (const entry of options)
byKey.set(entry.key, entry);
return [...byKey.values()];
};
/**
* Rejects a key or value the codec cannot express.
*
* `serializeHeaders` writes whatever field length it is handed, so without
* this the block leaves here well-formed and comes back as a generic server
* error naming neither the key nor the bound it broke.
*/
const checkFieldLength = (length: number, field: string): void => {
if (length < 1 || length > MAX_HEADER_FIELD_LENGTH)
throw new Error(
`Invalid option ${field} length: ${length} bytes, ` +
`must be between 1 and ${MAX_HEADER_FIELD_LENGTH}`);
};
/**
* Serializes resource options into a TLV block.
* Reuses the user-headers TLV encoding: each field is
* `[kind:u8][len:u32_le][bytes]`, alternating key, value.
* Empty options serialize to zero bytes.
*
* @param options - Option entries to serialize
* @returns Serialized options block
* @throws Error if an options constraint is violated
*/
export const serializeOptions = (options: OptionEntry[]): Buffer => {
if (options.length > MAX_OPTIONS)
throw new Error(
`Options block has ${options.length} entries, exceeds maximum ${MAX_OPTIONS}`);
for (const { key, value } of options) {
checkFieldLength(Buffer.byteLength(key), `key '${key}'`);
checkFieldLength(
serializeHeaderValue(value).length, `value for key '${key}'`);
}
const block = serializeHeaders(options.map(({ key, value }) =>
({ key: HeaderKeyFactory.String(key), value })));
if (block.length > MAX_OPTIONS_BYTES)
throw new Error(
`Options block is ${block.length} bytes, exceeds maximum ${MAX_OPTIONS_BYTES}`);
return block;
};
/**
* Deserializes a bare options TLV block spanning `[pos, end)`.
*
* @param p - Buffer containing the options block
* @param pos - Starting position of the block
* @param end - End position of the block (exclusive)
* @returns Deserialized options keyed by option name
* @throws Error if a key is not a string or the block is malformed
*/
export const deserializeOptions = (
p: Buffer, pos = 0, end = p.length
): ParsedOptions => {
const options: ParsedOptions = {};
while (pos < end) {
const keyKind = p.readUInt8(pos);
if (keyKind !== HeaderKind.String)
throw new Error(`Option key kind ${keyKind} is not a string`);
const keyLength = p.readUInt32LE(pos + 1);
if (keyLength < 1 || keyLength > MAX_HEADER_FIELD_LENGTH)
throw new Error(
`Invalid option key length: ${keyLength}, ` +
`must be between 1 and ${MAX_HEADER_FIELD_LENGTH}`);
if (pos + 5 + keyLength > end)
throw new Error('Option key overruns the block');
const key = p.subarray(pos + 5, pos + 5 + keyLength).toString();
pos += 5 + keyLength;
const valueKind = p.readUInt8(pos);
const valueLength = p.readUInt32LE(pos + 1);
if (pos + 5 + valueLength > end)
throw new Error(`Option value for key '${key}' overruns the block`);
const valueBytes = p.subarray(pos + 5, pos + 5 + valueLength);
pos += 5 + valueLength;
let value: ParsedHeaderValue;
try {
value = deserializeHeaderValue(valueKind, valueBytes);
} catch {
// Unknown value kinds stay raw bytes, mirroring the wire
// forward-compatibility contract for options.
value = valueBytes;
}
options[key] = value;
}
return options;
};
/**
* Deserializes a `u32_le`-length-prefixed options block at `pos`.
*
* @param p - Buffer containing `[options_len:u32_le][options TLV]`
* @param pos - Starting position of the length prefix
* @returns Bytes consumed (prefix included) and deserialized options
*/
export const deserializePrefixedOptions = (
p: Buffer, pos = 0
): OptionsDeserialized => {
const length = p.readUInt32LE(pos);
const end = pos + 4 + length;
// Without this, `subarray` clamps a truncated block silently: a known-kind
// value throws inside `deserializeHeaderValue`, the forward-compat catch
// swallows it and hands back raw bytes, and `bytesRead` over-reports so every
// later field decodes from the wrong offset.
if (end > p.length)
throw new Error(
`Options block overruns the payload: ${length} bytes declared at ${pos}, ` +
`${p.length - pos - 4} available`);
const options = deserializeOptions(p, pos + 4, end);
return { bytesRead: 4 + length, options };
};