134 lines
3.7 KiB
TypeScript
134 lines
3.7 KiB
TypeScript
const MUX_MAGIC_1 = 0x50; // 'P'
|
|
const MUX_MAGIC_2 = 0x58; // 'X'
|
|
const MUX_VERSION = 1;
|
|
const HEADER_SIZE = 24;
|
|
const UINT32_MAX = 0xffffffff;
|
|
const TWO_POW_32 = 0x1_0000_0000;
|
|
|
|
export const BinaryMuxConstants = {
|
|
magic1: MUX_MAGIC_1,
|
|
magic2: MUX_MAGIC_2,
|
|
version: MUX_VERSION,
|
|
headerSize: HEADER_SIZE,
|
|
} as const;
|
|
|
|
export const enum BinaryMuxChannel {
|
|
Terminal = 1,
|
|
FileTransfer = 2,
|
|
}
|
|
|
|
export const enum TerminalBinaryMessageType {
|
|
InputUtf8 = 1,
|
|
OutputUtf8 = 2,
|
|
Ack = 3,
|
|
}
|
|
|
|
export const enum TerminalBinaryFlags {
|
|
Replay = 1,
|
|
}
|
|
|
|
export interface BinaryMuxFrame {
|
|
channel: number;
|
|
messageType: number;
|
|
streamId: number;
|
|
offset: number;
|
|
flags?: number;
|
|
payload?: Uint8Array;
|
|
}
|
|
|
|
export function asUint8Array(data: unknown): Uint8Array | null {
|
|
if (typeof data === "string") {
|
|
if (typeof TextEncoder !== "undefined") {
|
|
return new TextEncoder().encode(data);
|
|
}
|
|
if (typeof Buffer !== "undefined") {
|
|
return new Uint8Array(Buffer.from(data, "utf8"));
|
|
}
|
|
const out = new Uint8Array(data.length);
|
|
for (let i = 0; i < data.length; i += 1) {
|
|
out[i] = data.charCodeAt(i) & 0xff;
|
|
}
|
|
return out;
|
|
}
|
|
if (data instanceof Uint8Array) {
|
|
return data;
|
|
}
|
|
if (typeof ArrayBuffer !== "undefined" && data instanceof ArrayBuffer) {
|
|
return new Uint8Array(data);
|
|
}
|
|
if (ArrayBuffer.isView(data)) {
|
|
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
}
|
|
if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) {
|
|
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function isLikelyBinaryMuxFrame(data: Uint8Array): boolean {
|
|
if (data.byteLength < HEADER_SIZE) {
|
|
return false;
|
|
}
|
|
return data[0] === MUX_MAGIC_1 && data[1] === MUX_MAGIC_2 && data[2] === MUX_VERSION;
|
|
}
|
|
|
|
export function encodeBinaryMuxFrame(frame: BinaryMuxFrame): Uint8Array {
|
|
const payload = frame.payload ?? new Uint8Array(0);
|
|
const out = new Uint8Array(HEADER_SIZE + payload.byteLength);
|
|
const view = new DataView(out.buffer, out.byteOffset, out.byteLength);
|
|
const flags = frame.flags ?? 0;
|
|
|
|
if (!Number.isFinite(frame.streamId) || frame.streamId < 0 || frame.streamId > UINT32_MAX) {
|
|
throw new Error(`Invalid streamId: ${frame.streamId}`);
|
|
}
|
|
if (!Number.isFinite(frame.offset) || frame.offset < 0) {
|
|
throw new Error(`Invalid offset: ${frame.offset}`);
|
|
}
|
|
|
|
const offsetHi = Math.floor(frame.offset / TWO_POW_32);
|
|
const offsetLo = frame.offset >>> 0;
|
|
if (offsetHi > UINT32_MAX) {
|
|
throw new Error(`Offset too large: ${frame.offset}`);
|
|
}
|
|
|
|
view.setUint8(0, MUX_MAGIC_1);
|
|
view.setUint8(1, MUX_MAGIC_2);
|
|
view.setUint8(2, MUX_VERSION);
|
|
view.setUint8(3, frame.channel & 0xff);
|
|
view.setUint8(4, frame.messageType & 0xff);
|
|
view.setUint8(5, flags & 0xff);
|
|
view.setUint8(6, 0);
|
|
view.setUint8(7, 0);
|
|
view.setUint32(8, frame.streamId >>> 0);
|
|
view.setUint32(12, offsetHi >>> 0);
|
|
view.setUint32(16, offsetLo);
|
|
view.setUint32(20, payload.byteLength >>> 0);
|
|
|
|
out.set(payload, HEADER_SIZE);
|
|
return out;
|
|
}
|
|
|
|
export function decodeBinaryMuxFrame(data: Uint8Array): BinaryMuxFrame | null {
|
|
if (!isLikelyBinaryMuxFrame(data)) {
|
|
return null;
|
|
}
|
|
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
const payloadLength = view.getUint32(20);
|
|
if (payloadLength !== data.byteLength - HEADER_SIZE) {
|
|
return null;
|
|
}
|
|
|
|
const offsetHi = view.getUint32(12);
|
|
const offsetLo = view.getUint32(16);
|
|
const offset = offsetHi * TWO_POW_32 + offsetLo;
|
|
|
|
return {
|
|
channel: view.getUint8(3),
|
|
messageType: view.getUint8(4),
|
|
flags: view.getUint8(5),
|
|
streamId: view.getUint32(8),
|
|
offset,
|
|
payload: data.subarray(HEADER_SIZE),
|
|
};
|
|
}
|