| 
 | 1 | +import {StreamingOctetReader} from '@jsonjoy.com/util/lib/buffers/StreamingOctetReader';  | 
 | 2 | +import {WsFrameOpcode} from './constants';  | 
 | 3 | +import {WsFrameDecodingError} from './errors';  | 
 | 4 | +import {WsCloseFrame, WsFrameHeader, WsPingFrame, WsPongFrame} from './frames';  | 
 | 5 | + | 
 | 6 | +export class WsFrameDecoder {  | 
 | 7 | +  public readonly reader = new StreamingOctetReader();  | 
 | 8 | + | 
 | 9 | +  public push(uint8: Uint8Array): void {  | 
 | 10 | +    this.reader.push(uint8);  | 
 | 11 | +  }  | 
 | 12 | + | 
 | 13 | +  public readFrameHeader(): WsFrameHeader | undefined {  | 
 | 14 | +    try {  | 
 | 15 | +      const reader = this.reader;  | 
 | 16 | +      if (reader.size() < 2) return undefined;  | 
 | 17 | +      const b0 = reader.u8();  | 
 | 18 | +      const b1 = reader.u8();  | 
 | 19 | +      const fin = <0 | 1>(b0 >>> 7);  | 
 | 20 | +      const opcode = b0 & 0b1111;  | 
 | 21 | +      const maskBit = b1 >>> 7;  | 
 | 22 | +      let length = b1 & 0b01111111;  | 
 | 23 | +      if (length === 126) {  | 
 | 24 | +        if (reader.size() < 2) return undefined;  | 
 | 25 | +        length = (reader.u8() << 8) | reader.u8();  | 
 | 26 | +      } else if (length === 127) {  | 
 | 27 | +        if (reader.size() < 8) return undefined;  | 
 | 28 | +        reader.skip(4);  | 
 | 29 | +        length = reader.u32();  | 
 | 30 | +      }  | 
 | 31 | +      let mask: undefined | [number, number, number, number];  | 
 | 32 | +      if (maskBit) {  | 
 | 33 | +        if (reader.size() < 4) return undefined;  | 
 | 34 | +        mask = [reader.u8(), reader.u8(), reader.u8(), reader.u8()];  | 
 | 35 | +      }  | 
 | 36 | +      if (opcode >= WsFrameOpcode.MIN_CONTROL_OPCODE) {  | 
 | 37 | +        switch (opcode) {  | 
 | 38 | +          case WsFrameOpcode.CLOSE: {  | 
 | 39 | +            return new WsCloseFrame(fin, opcode, length, mask, 0, '');  | 
 | 40 | +          }  | 
 | 41 | +          case WsFrameOpcode.PING: {  | 
 | 42 | +            if (length > 125) throw new WsFrameDecodingError();  | 
 | 43 | +            const data = mask ? reader.bufXor(length, mask, 0) : reader.buf(length);  | 
 | 44 | +            return new WsPingFrame(fin, opcode, length, mask, data);  | 
 | 45 | +          }  | 
 | 46 | +          case WsFrameOpcode.PONG: {  | 
 | 47 | +            if (length > 125) throw new WsFrameDecodingError();  | 
 | 48 | +            const data = mask ? reader.bufXor(length, mask, 0) : reader.buf(length);  | 
 | 49 | +            return new WsPongFrame(fin, opcode, length, mask, data);  | 
 | 50 | +          }  | 
 | 51 | +          default: {  | 
 | 52 | +            throw new WsFrameDecodingError();  | 
 | 53 | +          }  | 
 | 54 | +        }  | 
 | 55 | +      }  | 
 | 56 | +      return new WsFrameHeader(fin, opcode, length, mask);  | 
 | 57 | +    } catch (err) {  | 
 | 58 | +      if (err instanceof RangeError) return undefined;  | 
 | 59 | +      throw err;  | 
 | 60 | +    }  | 
 | 61 | +  }  | 
 | 62 | + | 
 | 63 | +  /**  | 
 | 64 | +   * Read application data of a frame and copy it to the destination buffer.  | 
 | 65 | +   * Receives the frame header and the number of bytes that still need to be  | 
 | 66 | +   * copied, returns back the number of bytes that still need to be copied in  | 
 | 67 | +   * subsequent calls.  | 
 | 68 | +   *  | 
 | 69 | +   * @param frame Frame header.  | 
 | 70 | +   * @param remaining How many bytes are remaining to be copied.  | 
 | 71 | +   * @param dst The destination buffer to write to.  | 
 | 72 | +   * @param pos Position in the destination buffer to start writing to.  | 
 | 73 | +   * @returns The number of bytes that still need to be copied in the next call.  | 
 | 74 | +   */  | 
 | 75 | +  public readFrameData(frame: WsFrameHeader, remaining: number, dst: Uint8Array, pos: number): number {  | 
 | 76 | +    const reader = this.reader;  | 
 | 77 | +    const mask = frame.mask;  | 
 | 78 | +    const readSize = Math.min(reader.size(), remaining);  | 
 | 79 | +    if (!mask) reader.copy(readSize, dst, pos);  | 
 | 80 | +    else {  | 
 | 81 | +      const alreadyRead = frame.length - remaining;  | 
 | 82 | +      reader.copyXor(readSize, dst, pos, mask, alreadyRead);  | 
 | 83 | +    }  | 
 | 84 | +    return remaining - readSize;  | 
 | 85 | +  }  | 
 | 86 | + | 
 | 87 | +  public copyFrameData(frame: WsFrameHeader, dst: Uint8Array, pos: number): void {  | 
 | 88 | +    const reader = this.reader;  | 
 | 89 | +    const mask = frame.mask;  | 
 | 90 | +    const readSize = frame.length;  | 
 | 91 | +    if (!mask) reader.copy(readSize, dst, pos);  | 
 | 92 | +    else reader.copyXor(readSize, dst, pos, mask, 0);  | 
 | 93 | +  }  | 
 | 94 | + | 
 | 95 | +  /**  | 
 | 96 | +   * Reads application data of the CLOSE frame and sets the code and reason  | 
 | 97 | +   * properties of the frame.  | 
 | 98 | +   *  | 
 | 99 | +   * @param frame Close frame.  | 
 | 100 | +   */  | 
 | 101 | +  public readCloseFrameData(frame: WsCloseFrame): void {  | 
 | 102 | +    let length = frame.length;  | 
 | 103 | +    if (length > 125) throw new WsFrameDecodingError();  | 
 | 104 | +    let code = 0;  | 
 | 105 | +    let reason = '';  | 
 | 106 | +    if (length > 0) {  | 
 | 107 | +      if (length < 2) throw new WsFrameDecodingError();  | 
 | 108 | +      const reader = this.reader;  | 
 | 109 | +      const mask = frame.mask;  | 
 | 110 | +      const octet1 = reader.u8() ^ (mask ? mask[0] : 0);  | 
 | 111 | +      const octet2 = reader.u8() ^ (mask ? mask[1] : 0);  | 
 | 112 | +      code = (octet1 << 8) | octet2;  | 
 | 113 | +      length -= 2;  | 
 | 114 | +      if (length) reason = reader.utf8(length, mask ?? [0, 0, 0, 0], 2);  | 
 | 115 | +    }  | 
 | 116 | +    frame.code = code;  | 
 | 117 | +    frame.reason = reason;  | 
 | 118 | +  }  | 
 | 119 | +}  | 
0 commit comments