///
import type { UsbmuxCandidate } from './webusb';
import { getBulkEndpoints, transferIn, transferOutWithZlp } from './webusb';
const PROTO_VERSION = 0;
const PROTO_SETUP = 2;
const PROTO_TCP = 6;
const MAGIC = 0xfeedface;
const FLAG_SYN = 0x02;
const FLAG_RST = 0x04;
const FLAG_ACK = 0x10;
const FIRST_SPORT = 49152;
export type UsbmuxSession = {
device: USBDevice;
candidate: UsbmuxCandidate;
inEndpoint: ReturnType['inEndpoint'];
outEndpoint: ReturnType['outEndpoint'];
muxVersion: number;
txSeq: number;
rxSeq: number;
nextSport: number;
streams: Map;
writeChain: Promise;
closed: boolean;
};
export type UsbmuxStream = {
session: UsbmuxSession;
sport: number;
dport: number;
seq: number;
ack: number;
queue: Uint8Array[];
waiters: Array<(value: Uint8Array) => void>;
error?: Error;
opened: Promise;
resolveOpened: () => void;
rejectOpened: (error: Error) => void;
};
export async function createUsbmuxSession(device: USBDevice, candidate: UsbmuxCandidate) {
const { inEndpoint, outEndpoint } = getBulkEndpoints(candidate);
const versionPayload = new Uint8Array(12);
const versionView = new DataView(versionPayload.buffer);
versionView.setUint32(0, 2);
versionView.setUint32(4, 0);
versionView.setUint32(8, 0);
await transferOutWithZlp(device, outEndpoint, buildV1Packet(PROTO_VERSION, versionPayload));
const versionPacket = await readPacket(device, inEndpoint, 1);
const version = new DataView(versionPacket.payload.buffer, versionPacket.payload.byteOffset).getUint32(0);
const session: UsbmuxSession = {
device,
candidate,
inEndpoint,
outEndpoint,
muxVersion: version,
txSeq: 0,
rxSeq: 0xffff,
nextSport: FIRST_SPORT,
streams: new Map(),
writeChain: Promise.resolve(),
closed: false,
};
if (version >= 2) {
await sendMux(session, PROTO_SETUP, new Uint8Array([0x07]));
}
void readLoop(session);
return session;
}
export async function openStream(session: UsbmuxSession, port: number) {
if (session.closed) {
throw new Error('usbmux session is closed.');
}
let resolveOpened!: () => void;
let rejectOpened!: (error: Error) => void;
const stream: UsbmuxStream = {
session,
sport: session.nextSport++,
dport: port,
seq: 0,
ack: 0,
queue: [],
waiters: [],
opened: new Promise((resolve, reject) => {
resolveOpened = resolve;
rejectOpened = reject;
}),
resolveOpened,
rejectOpened,
};
session.streams.set(streamKey(port, stream.sport), stream);
await sendTcp(stream, FLAG_SYN, new Uint8Array());
await stream.opened;
return stream;
}
export async function sendStreamData(stream: UsbmuxStream, bytes: Uint8Array) {
if (stream.session.closed) {
throw new Error('usbmux session is closed.');
}
await sendTcp(stream, FLAG_ACK, bytes);
stream.seq += bytes.byteLength;
}
export async function receiveStreamData(stream: UsbmuxStream) {
if (stream.queue.length > 0) {
return stream.queue.shift()!;
}
if (stream.error) {
throw stream.error;
}
return new Promise((resolve) => {
stream.waiters.push(resolve);
});
}
export function closeUsbmuxSession(session: UsbmuxSession) {
session.closed = true;
for (const stream of session.streams.values()) {
stream.error = new Error('usbmux session closed');
stream.rejectOpened(stream.error);
while (stream.waiters.length > 0) {
stream.waiters.shift()!(new Uint8Array());
}
}
session.streams.clear();
}
async function sendTcp(stream: UsbmuxStream, flags: number, payload: Uint8Array) {
const tcp = new Uint8Array(20 + payload.byteLength);
const view = new DataView(tcp.buffer);
view.setUint16(0, stream.sport);
view.setUint16(2, stream.dport);
view.setUint32(4, stream.seq);
view.setUint32(8, stream.ack);
view.setUint8(12, 0x50);
view.setUint8(13, flags);
view.setUint16(14, 512);
tcp.set(payload, 20);
await sendMux(stream.session, PROTO_TCP, tcp);
}
async function sendMux(session: UsbmuxSession, protocol: number, payload: Uint8Array) {
if (session.closed) {
throw new Error('usbmux session is closed.');
}
session.writeChain = session.writeChain.then(async () => {
if (session.closed) return;
const packet =
session.muxVersion >= 2 ?
buildV2Packet(protocol, payload, session.txSeq++, session.rxSeq)
: buildV1Packet(protocol, payload);
await transferOutWithZlp(session.device, session.outEndpoint, packet);
});
return session.writeChain;
}
async function readLoop(session: UsbmuxSession) {
try {
for (;;) {
if (session.closed) return;
const packet = await readPacket(session.device, session.inEndpoint, session.muxVersion);
if (session.closed) return;
if (packet.rxSeq !== undefined) {
session.rxSeq = packet.rxSeq;
}
if (packet.protocol !== PROTO_TCP) {
continue;
}
const tcp = parseTcp(packet.payload);
const stream = session.streams.get(streamKey(tcp.sport, tcp.dport));
if (!stream) {
continue;
}
if (tcp.flags & FLAG_RST) {
stream.error = new Error(`Device reset stream ${stream.dport}`);
stream.rejectOpened(stream.error);
while (stream.waiters.length > 0) {
stream.waiters.shift()!(new Uint8Array());
}
session.streams.delete(streamKey(stream.dport, stream.sport));
continue;
}
if ((tcp.flags & (FLAG_SYN | FLAG_ACK)) === (FLAG_SYN | FLAG_ACK)) {
stream.seq += 1;
stream.ack = tcp.seq + 1;
await sendTcp(stream, FLAG_ACK, new Uint8Array());
stream.resolveOpened();
continue;
}
if (tcp.payload.byteLength === 0) {
continue;
}
stream.ack = tcp.seq + tcp.payload.byteLength;
await sendTcp(stream, FLAG_ACK, new Uint8Array());
if (stream.waiters.length > 0) {
stream.waiters.shift()!(tcp.payload);
} else {
stream.queue.push(tcp.payload);
}
}
} catch (error) {
if (!session.closed) {
closeUsbmuxSession(session);
}
}
}
async function readPacket(device: USBDevice, endpoint: UsbmuxSession['inEndpoint'], version: number) {
for (;;) {
const bytes = await transferIn(device, endpoint);
const packet = parseMux(bytes, version);
if (version === 1 && packet.protocol !== PROTO_VERSION) continue;
return packet;
}
}
function buildV1Packet(protocol: number, payload: Uint8Array) {
const bytes = new Uint8Array(8 + payload.byteLength);
const view = new DataView(bytes.buffer);
view.setUint32(0, protocol);
view.setUint32(4, bytes.byteLength);
bytes.set(payload, 8);
return bytes;
}
function buildV2Packet(protocol: number, payload: Uint8Array, txSeq: number, rxSeq: number) {
const bytes = new Uint8Array(16 + payload.byteLength);
const view = new DataView(bytes.buffer);
view.setUint32(0, protocol);
view.setUint32(4, bytes.byteLength);
view.setUint32(8, MAGIC);
view.setUint16(12, txSeq);
view.setUint16(14, rxSeq);
bytes.set(payload, 16);
return bytes;
}
function parseMux(bytes: Uint8Array, version: number) {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const protocol = view.getUint32(0);
const length = view.getUint32(4);
const headerSize = version >= 2 ? 16 : 8;
return {
protocol,
length,
rxSeq: version >= 2 ? view.getUint16(14) : undefined,
payload: bytes.slice(headerSize, length),
};
}
function parseTcp(bytes: Uint8Array) {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const dataOffset = (view.getUint8(12) >> 4) * 4;
return {
sport: view.getUint16(0),
dport: view.getUint16(2),
seq: view.getUint32(4),
ack: view.getUint32(8),
flags: view.getUint8(13),
payload: bytes.slice(dataOffset),
};
}
function streamKey(devicePort: number, hostPort: number) {
return `${devicePort}:${hostPort}`;
}