// Pure helpers for header-shape conversion between the panel's internal // HeaderEntry[] form and Xray's V2-style header map. Extracted from // XrayCommonClass.toHeaders / .toV2Headers so callers can stop relying on // the class hierarchy. Behavior is byte-equivalent to the legacy methods — // the shadow tests in src/test/headers.test.ts pin that. export interface HeaderEntry { name: string; value: string; } export type V2HeaderMap = Record; // Expand a V2-style header map into the panel's flat HeaderEntry[]. A // header whose value is an array yields one entry per item, preserving // order; a string value yields a single entry. Non-object inputs (null, // undefined, primitives) yield []. export function toHeaders(v2Headers: unknown): HeaderEntry[] { const out: HeaderEntry[] = []; if (!v2Headers || typeof v2Headers !== 'object') return out; const map = v2Headers as Record; for (const key of Object.keys(map)) { const values = map[key]; if (typeof values === 'string') { out.push({ name: key, value: values }); } else if (Array.isArray(values)) { for (const v of values) { if (typeof v === 'string') out.push({ name: key, value: v }); } } } return out; } // Collapse a HeaderEntry[] back into a V2-style header map. When `arr` is // true (the default — matches Xray's TCP/WS/HTTP request/response shape), // duplicate header names accumulate into a string[]. When false (used for // WS/HTTPUpgrade/xHTTP top-level headers, sockopt portMap, etc.), the // last value wins. Entries with empty name or value are skipped — same as // the legacy ObjectUtil.isEmpty() filter. export function toV2Headers(headers: HeaderEntry[], arr: boolean = true): V2HeaderMap { const out: V2HeaderMap = {}; for (const { name, value } of headers) { if (name == null || name === '' || value == null || value === '') continue; if (!(name in out)) { out[name] = arr ? [value] : value; continue; } const existing = out[name]; if (arr && Array.isArray(existing)) { existing.push(value); } else { out[name] = value; } } return out; }