UNPKG

@chainsafe/libp2p-yamux

Version:
130 lines 4.95 kB
import { Uint8ArrayList } from 'uint8arraylist'; import { InvalidFrameError, InvalidStateError } from './errors.js'; import { FrameType, HEADER_LENGTH, YAMUX_VERSION } from './frame.js'; // used to bitshift in decoding // native bitshift can overflow into a negative number, so we bitshift by multiplying by a power of 2 const twoPow24 = 2 ** 24; /** * Decode a header from the front of a buffer * * @param data - Assumed to have enough bytes for a header */ export function decodeHeader(data) { if (data[0] !== YAMUX_VERSION) { throw new InvalidFrameError('Invalid frame version'); } return { type: data[1], flag: (data[2] << 8) + data[3], streamID: (data[4] * twoPow24) + (data[5] << 16) + (data[6] << 8) + data[7], length: (data[8] * twoPow24) + (data[9] << 16) + (data[10] << 8) + data[11] }; } /** * Decodes yamux frames from a source */ export class Decoder { source; /** Buffer for in-progress frames */ buffer; /** Used to sanity check against decoding while in an inconsistent state */ frameInProgress; constructor(source) { // Normally, when entering a for-await loop with an iterable/async iterable, the only ways to exit the loop are: // 1. exhaust the iterable // 2. throw an error - slow, undesirable if there's not actually an error // 3. break or return - calls the iterable's `return` method, finalizing the iterable, no more iteration possible // // In this case, we want to enter (and exit) a for-await loop per chunked data frame and continue processing the iterable. // To do this, we strip the `return` method from the iterator and can now `break` early and continue iterating. // Exiting the main for-await is still possible via 1. and 2. this.source = returnlessSource(source); this.buffer = new Uint8ArrayList(); this.frameInProgress = false; } /** * Emits frames from the decoder source. * * Note: If `readData` is emitted, it _must_ be called before the next iteration * Otherwise an error is thrown */ async *emitFrames() { for await (const chunk of this.source) { this.buffer.append(chunk); // Loop to consume as many bytes from the buffer as possible // Eg: when a single chunk contains several frames while (true) { const header = this.readHeader(); if (header === undefined) { break; } const { type, length } = header; if (type === FrameType.Data) { // This is a data frame, the frame body must still be read // `readData` must be called before the next iteration here this.frameInProgress = true; yield { header, readData: this.readBytes.bind(this, length) }; } else { yield { header }; } } } } readHeader() { // Sanity check to ensure a header isn't read when another frame is partially decoded // In practice this shouldn't happen if (this.frameInProgress) { throw new InvalidStateError('decoding frame already in progress'); } if (this.buffer.length < HEADER_LENGTH) { // not enough data yet return; } const header = decodeHeader(this.buffer.subarray(0, HEADER_LENGTH)); this.buffer.consume(HEADER_LENGTH); return header; } async readBytes(length) { if (this.buffer.length < length) { for await (const chunk of this.source) { this.buffer.append(chunk); if (this.buffer.length >= length) { // see note above, the iterator is not `return`ed here break; } } } const out = this.buffer.sublist(0, length); this.buffer.consume(length); // The next frame can now be decoded this.frameInProgress = false; return out; } } /** * Strip the `return` method from a `Source` */ export function returnlessSource(source) { if (source[Symbol.iterator] !== undefined) { const iterator = source[Symbol.iterator](); iterator.return = undefined; return { [Symbol.iterator]() { return iterator; } }; } else if (source[Symbol.asyncIterator] !== undefined) { const iterator = source[Symbol.asyncIterator](); iterator.return = undefined; return { [Symbol.asyncIterator]() { return iterator; } }; } else { throw new Error('a source must be either an iterable or an async iterable'); } } //# sourceMappingURL=decode.js.map