diff --git a/lib/browser/mqtt_internal/client.ts b/lib/browser/mqtt_internal/client.ts new file mode 100644 index 000000000..c6d888f02 --- /dev/null +++ b/lib/browser/mqtt_internal/client.ts @@ -0,0 +1,4 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ diff --git a/lib/browser/mqtt_internal/decoder.ts b/lib/browser/mqtt_internal/decoder.ts new file mode 100644 index 000000000..861c7e76b --- /dev/null +++ b/lib/browser/mqtt_internal/decoder.ts @@ -0,0 +1,824 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +import {CrtError} from "../error"; +import * as mqtt5_packet from '../../common/mqtt5_packet'; +import * as vli from "./vli"; +import * as model from "./model"; +import {toUtf8} from "@aws-sdk/util-utf8-browser"; + +// utility functions for individual packet fields + +export function decode_boolean(payload: DataView, offset: number) : [boolean, number] { + return [payload.getUint8(offset) ? true : false, offset + 1]; +} + +export function decode_u8(payload: DataView, offset: number) : [number, number] { + return [payload.getUint8(offset), offset + 1]; +} + +export function decode_u16(payload: DataView, offset: number) : [number, number] { + return [payload.getUint16(offset), offset + 2]; +} + +export function decode_u32(payload: DataView, offset: number) : [number, number] { + return [payload.getUint32(offset), offset + 4]; +} + +export function decode_vli(payload: DataView, offset: number) : [number, number] { + let result = vli.decode_vli(payload, offset); + if (result.type == vli.VliDecodeResultType.Success) { + // @ts-ignore + return [result.value, result.nextOffset]; + } + + throw new CrtError("insufficient data to decode variable-length integer"); +} + +export function decode_length_prefixed_string(payload: DataView, offset: number) : [string, number] { + let [stringLength, index] = decode_u16(payload, offset); + return [toUtf8(new Uint8Array(payload.buffer, index, stringLength)), index + stringLength]; +} + +export function decode_bytes(payload: DataView, offset: number, length: number) : [ArrayBuffer, number] { + return [payload.buffer.slice(offset, offset + length), offset + length]; +} + +export function decode_length_prefixed_bytes(payload: DataView, offset: number) : [ArrayBuffer, number] { + let [bytesLength, index] = decode_u16(payload, offset); + return [payload.buffer.slice(index, index + bytesLength), index + bytesLength]; +} + +export function decode_user_property(payload: DataView, offset: number, userProperties: Array) : number { + let index: number = offset; + + let name : string = ""; + [name, index] = decode_length_prefixed_string(payload, index); + + let value : string = ""; + [value, index] = decode_length_prefixed_string(payload, index); + + userProperties.push({name: name, value: value}); + + return index; +} + +// MQTT 311 Packet decoding functions + +function decode_connack_packet_311(firstByte: number, payload: DataView) : model.ConnackPacketInternal { + if (payload.byteLength != 2) { + throw new CrtError("Connack packet invalid payload length"); + } + + let index : number = 0; + let flags : number = 0; + + let connack: model.ConnackPacketInternal = { + type: mqtt5_packet.PacketType.Connack, + sessionPresent: false, + reasonCode: mqtt5_packet.ConnectReasonCode.Success + }; + + [flags, index] = decode_u8(payload, index); + if ((flags & (~0x01)) != 0) { + throw new CrtError("Connack invalid flags"); + } + connack.sessionPresent = (flags & model.CONNACK_FLAGS_SESSION_PRESENT) != 0; + [connack.reasonCode, index] = decode_u8(payload, index); + + return connack; +} + +function decode_publish_packet_311(firstByte: number, payload: DataView) : model.PublishPacketInternal { + let index : number = 0; + + let publish: model.PublishPacketInternal = { + type: mqtt5_packet.PacketType.Publish, + qos: (firstByte >>> model.PUBLISH_FLAGS_QOS_SHIFT) & model.QOS_MASK, + duplicate: (firstByte & model.PUBLISH_FLAGS_DUPLICATE) ? true : false, + retain: (firstByte & model.PUBLISH_FLAGS_RETAIN) ? true : false, + topicName: "" + }; + + [publish.topicName, index] = decode_length_prefixed_string(payload, index); + + if (publish.qos > 0) { + [publish.packetId, index] = decode_u16(payload, index); + } + + if (index < payload.byteLength) { + [publish.payload, index] = decode_bytes(payload, index, payload.byteLength - index); + } + + return publish; +} + +function decode_puback_packet_311(firstByte: number, payload: DataView) : model.PubackPacketInternal { + if (payload.byteLength != 2) { + throw new CrtError("Puback packet with invalid payload length"); + } + + if (firstByte != model.PACKET_TYPE_FIRST_BYTE_PUBACK) { + throw new CrtError("Puback packet with invalid first byte: " + firstByte); + } + + let index : number = 0; + let puback: model.PubackPacketInternal = { + type: mqtt5_packet.PacketType.Puback, + packetId: 0, + reasonCode: mqtt5_packet.PubackReasonCode.Success, + }; + + [puback.packetId, index] = decode_u16(payload, index); + + return puback; +} + +function decode_suback_packet_311(firstByte: number, payload: DataView) : model.SubackPacketInternal { + if (firstByte != model.PACKET_TYPE_FIRST_BYTE_SUBACK) { + throw new CrtError("Suback packet with invalid first byte: " + firstByte); + } + + let index : number = 0; + let suback: model.SubackPacketInternal = { + type: mqtt5_packet.PacketType.Suback, + packetId: 0, + reasonCodes: new Array() + }; + + [suback.packetId, index] = decode_u16(payload, index); + + let reasonCodeCount = payload.byteLength - index; + for (let i = 0; i < reasonCodeCount; i++) { + let reasonCode: mqtt5_packet.SubackReasonCode = 0; + [reasonCode, index] = decode_u8(payload, index); + suback.reasonCodes.push(reasonCode); + } + + return suback; +} + +function decode_unsuback_packet_311(firstByte: number, payload: DataView) : model.UnsubackPacketInternal { + if (payload.byteLength != 2) { + throw new CrtError("Unsuback packet with invalid payload length"); + } + + if (firstByte != model.PACKET_TYPE_FIRST_BYTE_UNSUBACK) { + throw new CrtError("Unsuback packet with invalid first byte: " + firstByte); + } + + let index : number = 0; + let puback: model.UnsubackPacketInternal = { + type: mqtt5_packet.PacketType.Unsuback, + packetId: 0, + reasonCodes: [] // client will need to synthesize N successes based on original unsubscribe + }; + + [puback.packetId, index] = decode_u16(payload, index); + + return puback; +} + +function decode_pingresp_packet(firstByte: number, payload: DataView) : model.PingrespPacketInternal { + if (payload.byteLength != 0) { + throw new CrtError("Pingresp packet with invalid payload length"); + } + + if (firstByte != (model.PACKET_TYPE_PINGRESP_FULL_ENCODING >>> 8)) { + throw new CrtError("Pingresp packet with invalid first byte: " + firstByte); + } + + return { + type: mqtt5_packet.PacketType.Pingresp + }; +} + +// MQTT 5 packet decoders + +function decode_connack_properties(connack: model.ConnackPacketInternal, payload: DataView, offset: number, propertyLength: number) : number { + let index : number = offset; + let propertyCode : number = 0; + while (index < offset + propertyLength) { + [propertyCode, index] = decode_u8(payload, index); + switch (propertyCode) { + case model.SESSION_EXPIRY_INTERVAL_PROPERTY_CODE: + [connack.sessionExpiryInterval, index] = decode_u32(payload, index); + break; + + case model.RECEIVE_MAXIMUM_PROPERTY_CODE: + [connack.receiveMaximum, index] = decode_u16(payload, index); + break; + + case model.MAXIMUM_QOS_PROPERTY_CODE: + [connack.maximumQos, index] = decode_u8(payload, index); + break; + + case model.RETAIN_AVAILABLE_PROPERTY_CODE: + [connack.retainAvailable, index] = decode_boolean(payload, index); + break; + + case model.MAXIMUM_PACKET_SIZE_PROPERTY_CODE: + [connack.maximumPacketSize, index] = decode_u32(payload, index); + break; + + case model.ASSIGNED_CLIENT_IDENTIFIER_PROPERTY_CODE: + [connack.assignedClientIdentifier, index] = decode_length_prefixed_string(payload, index); + break; + + case model.TOPIC_ALIAS_MAXIMUM_PROPERTY_CODE: + [connack.topicAliasMaximum, index] = decode_u16(payload, index); + break; + + case model.REASON_STRING_PROPERTY_CODE: + [connack.reasonString, index] = decode_length_prefixed_string(payload, index); + break; + + case model.USER_PROPERTY_PROPERTY_CODE: + if (!connack.userProperties) { + connack.userProperties = new Array(); + } + index = decode_user_property(payload, index, connack.userProperties); + break; + + case model.WILDCARD_SUBSCRIPTIONS_AVAILABLE_PROPERTY_CODE: + [connack.wildcardSubscriptionsAvailable, index] = decode_boolean(payload, index); + break; + + case model.SUBSCRIPTION_IDENTIFIERS_AVAILABLE_PROPERTY_CODE: + [connack.subscriptionIdentifiersAvailable, index] = decode_boolean(payload, index); + break; + + case model.SHARED_SUBSCRIPTIONS_AVAILABLE_PROPERTY_CODE: + [connack.sharedSubscriptionsAvailable, index] = decode_boolean(payload, index); + break; + + case model.SERVER_KEEP_ALIVE_PROPERTY_CODE: + [connack.serverKeepAlive, index] = decode_u16(payload, index); + break; + + case model.RESPONSE_INFORMATION_PROPERTY_CODE: + [connack.responseInformation, index] = decode_length_prefixed_string(payload, index); + break; + + case model.SERVER_REFERENCE_PROPERTY_CODE: + [connack.serverReference, index] = decode_length_prefixed_string(payload, index); + break; + + case model.AUTHENTICATION_METHOD_PROPERTY_CODE: + [connack.authenticationMethod, index] = decode_length_prefixed_string(payload, index); + break; + + case model.AUTHENTICATION_DATA_PROPERTY_CODE: + [connack.authenticationData, index] = decode_length_prefixed_bytes(payload, index); + break; + + default: + throw new CrtError("Unknown Connack property code: " + propertyCode); + } + } + + if (index != offset + propertyLength) { + throw new CrtError("Connack packet mismatch between encoded properties and expected length"); + } + + return index; +} + +function decode_connack_packet_5(firstByte: number, payload: DataView) : model.ConnackPacketInternal { + if (firstByte != model.PACKET_TYPE_FIRST_BYTE_CONNACK) { + throw new CrtError("Connack with invalid first byte: " + firstByte); + } + + let index : number = 0; + let flags : number = 0; + + let connack: model.ConnackPacketInternal = { + type: mqtt5_packet.PacketType.Connack, + sessionPresent: false, + reasonCode: mqtt5_packet.ConnectReasonCode.Success + }; + + [flags, index] = decode_u8(payload, index); + connack.sessionPresent = (flags & model.CONNACK_FLAGS_SESSION_PRESENT) != 0; + [connack.reasonCode, index] = decode_u8(payload, index); + + let propertiesLength : number = 0; + [propertiesLength, index] = decode_vli(payload, index); + + index = decode_connack_properties(connack, payload, index, propertiesLength); + + if (index != payload.byteLength) { + throw new CrtError("Connect packet mismatch between payload and expected length"); + } + + return connack; +} + +function decode_publish_properties(publish: model.PublishPacketInternal, payload: DataView, offset: number, propertyLength: number) : number { + let index : number = offset; + let propertyCode : number = 0; + while (index < offset + propertyLength) { + [propertyCode, index] = decode_u8(payload, index); + switch (propertyCode) { + case model.PAYLOAD_FORMAT_INDICATOR_PROPERTY_CODE: + [publish.payloadFormat, index] = decode_u8(payload, index); + break; + + case model.MESSAGE_EXPIRY_INTERVAL_PROPERTY_CODE: + [publish.messageExpiryIntervalSeconds, index] = decode_u32(payload, index); + break; + + case model.TOPIC_ALIAS_PROPERTY_CODE: + [publish.topicAlias, index] = decode_u16(payload, index); + break; + + case model.RESPONSE_TOPIC_PROPERTY_CODE: + [publish.responseTopic, index] = decode_length_prefixed_string(payload, index); + break; + + case model.CORRELATION_DATA_PROPERTY_CODE: + [publish.correlationData, index] = decode_length_prefixed_bytes(payload, index); + break; + + case model.USER_PROPERTY_PROPERTY_CODE: + if (!publish.userProperties) { + publish.userProperties = new Array(); + } + index = decode_user_property(payload, index, publish.userProperties); + break; + + case model.SUBSCRIPTION_IDENTIFIER_PROPERTY_CODE: + if (!publish.subscriptionIdentifiers) { + publish.subscriptionIdentifiers = new Array(); + } + let subscriptionIdentifier : number = 0; + [subscriptionIdentifier, index] = decode_vli(payload, index); + publish.subscriptionIdentifiers.push(subscriptionIdentifier); + break; + + case model.CONTENT_TYPE_PROPERTY_CODE: + [publish.contentType, index] = decode_length_prefixed_string(payload, index); + break; + + default: + throw new CrtError("Unknown Publish property code: " + propertyCode); + } + } + + if (index != offset + propertyLength) { + throw new CrtError("Publish packet mismatch between encoded properties and expected length"); + } + + return index; +} + +function decode_publish_packet_5(firstByte: number, payload: DataView) : model.PublishPacketInternal { + let index : number = 0; + + let publish: model.PublishPacketInternal = { + type: mqtt5_packet.PacketType.Publish, + qos: (firstByte >>> model.PUBLISH_FLAGS_QOS_SHIFT) & model.QOS_MASK, + duplicate: (firstByte & model.PUBLISH_FLAGS_DUPLICATE) ? true : false, + retain: (firstByte & model.PUBLISH_FLAGS_RETAIN) ? true : false, + topicName: "" + }; + + [publish.topicName, index] = decode_length_prefixed_string(payload, index); + if (publish.qos > 0) { + [publish.packetId, index] = decode_u16(payload, index); + } + + let propertiesLength : number = 0; + [propertiesLength, index] = decode_vli(payload, index); + + index = decode_publish_properties(publish, payload, index, propertiesLength); + + if (index < payload.byteLength) { + [publish.payload, index] = decode_bytes(payload, index, payload.byteLength - index); + } + + return publish; +} + +function decode_puback_properties(puback: model.PubackPacketInternal, payload: DataView, offset: number, propertyLength: number) : number { + let index : number = offset; + let propertyCode : number = 0; + while (index < offset + propertyLength) { + [propertyCode, index] = decode_u8(payload, index); + switch (propertyCode) { + + case model.REASON_STRING_PROPERTY_CODE: + [puback.reasonString, index] = decode_length_prefixed_string(payload, index); + break; + + case model.USER_PROPERTY_PROPERTY_CODE: + if (!puback.userProperties) { + puback.userProperties = new Array(); + } + index = decode_user_property(payload, index, puback.userProperties); + break; + + default: + throw new CrtError("Unknown Puback property code: " + propertyCode); + } + } + + if (index != offset + propertyLength) { + throw new CrtError("Puback packet mismatch between encoded properties and expected length"); + } + + return index; +} + +function decode_puback_packet_5(firstByte: number, payload: DataView) : model.PubackPacketInternal { + if (firstByte != model.PACKET_TYPE_FIRST_BYTE_PUBACK) { + throw new CrtError("Puback packet with invalid first byte: " + firstByte); + } + + let index : number = 0; + let puback: model.PubackPacketInternal = { + type: mqtt5_packet.PacketType.Puback, + packetId: 0, + reasonCode: mqtt5_packet.PubackReasonCode.Success, + }; + + [puback.packetId, index] = decode_u16(payload, index); + + if (payload.byteLength > 2) { + [puback.reasonCode, index] = decode_u8(payload, index); + + if (payload.byteLength > 3) { + let propertiesLength: number = 0; + [propertiesLength, index] = decode_vli(payload, index); + + index = decode_puback_properties(puback, payload, index, propertiesLength); + } + } + + if (index != payload.byteLength) { + throw new CrtError("Puback packet mismatch between payload and expected length"); + } + + return puback; +} + +function decode_suback_properties(suback: model.SubackPacketInternal, payload: DataView, offset: number, propertyLength: number) : number { + let index : number = offset; + let propertyCode : number = 0; + while (index < offset + propertyLength) { + [propertyCode, index] = decode_u8(payload, index); + switch (propertyCode) { + + case model.REASON_STRING_PROPERTY_CODE: + [suback.reasonString, index] = decode_length_prefixed_string(payload, index); + break; + + case model.USER_PROPERTY_PROPERTY_CODE: + if (!suback.userProperties) { + suback.userProperties = new Array(); + } + index = decode_user_property(payload, index, suback.userProperties); + break; + + default: + throw new CrtError("Unknown Suback property code: " + propertyCode); + } + } + + if (index != offset + propertyLength) { + throw new CrtError("Suback packet mismatch between encoded properties and expected length"); + } + + return index; +} + +function decode_suback_packet_5(firstByte: number, payload: DataView) : model.SubackPacketInternal { + if (firstByte != model.PACKET_TYPE_FIRST_BYTE_SUBACK) { + throw new CrtError("Suback packet with invalid first byte: " + firstByte); + } + + let index : number = 0; + let suback: model.SubackPacketInternal = { + type: mqtt5_packet.PacketType.Suback, + packetId: 0, + reasonCodes: new Array() + }; + + [suback.packetId, index] = decode_u16(payload, index); + + let propertiesLength: number = 0; + [propertiesLength, index] = decode_vli(payload, index); + + index = decode_suback_properties(suback, payload, index, propertiesLength); + + let reasonCodeCount = payload.byteLength - index; + for (let i = 0; i < reasonCodeCount; i++) { + let reasonCode: mqtt5_packet.SubackReasonCode = 0; + [reasonCode, index] = decode_u8(payload, index); + suback.reasonCodes.push(reasonCode); + } + + return suback; +} + +function decode_unsuback_properties(unsuback: model.UnsubackPacketInternal, payload: DataView, offset: number, propertyLength: number) : number { + let index : number = offset; + let propertyCode : number = 0; + while (index < offset + propertyLength) { + [propertyCode, index] = decode_u8(payload, index); + switch (propertyCode) { + + case model.REASON_STRING_PROPERTY_CODE: + [unsuback.reasonString, index] = decode_length_prefixed_string(payload, index); + break; + + case model.USER_PROPERTY_PROPERTY_CODE: + if (!unsuback.userProperties) { + unsuback.userProperties = new Array(); + } + index = decode_user_property(payload, index, unsuback.userProperties); + break; + + default: + throw new CrtError("Unknown Unsuback property code: " + propertyCode); + } + } + + if (index != offset + propertyLength) { + throw new CrtError("Unsuback packet mismatch between encoded properties and expected length"); + } + + return index; +} + +function decode_unsuback_packet_5(firstByte: number, payload: DataView) : model.UnsubackPacketInternal { + if (firstByte != model.PACKET_TYPE_FIRST_BYTE_UNSUBACK) { + throw new CrtError("Unsuback packet with invalid first byte: " + firstByte); + } + + let index : number = 0; + let unsuback: model.UnsubackPacketInternal = { + type: mqtt5_packet.PacketType.Unsuback, + packetId: 0, + reasonCodes: new Array() + }; + + [unsuback.packetId, index] = decode_u16(payload, index); + + let propertiesLength: number = 0; + [propertiesLength, index] = decode_vli(payload, index); + + index = decode_unsuback_properties(unsuback, payload, index, propertiesLength); + + let reasonCodeCount = payload.byteLength - index; + for (let i = 0; i < reasonCodeCount; i++) { + let reasonCode: mqtt5_packet.UnsubackReasonCode = 0; + [reasonCode, index] = decode_u8(payload, index); + unsuback.reasonCodes.push(reasonCode); + } + + return unsuback; +} + +function decode_disconnect_properties(disconnect: mqtt5_packet.DisconnectPacket, payload: DataView, offset: number, propertyLength: number) : number { + let index : number = offset; + let propertyCode : number = 0; + while (index < offset + propertyLength) { + [propertyCode, index] = decode_u8(payload, index); + switch (propertyCode) { + + case model.SESSION_EXPIRY_INTERVAL_PROPERTY_CODE: + [disconnect.sessionExpiryIntervalSeconds, index] = decode_u32(payload, index); + break; + + case model.REASON_STRING_PROPERTY_CODE: + [disconnect.reasonString, index] = decode_length_prefixed_string(payload, index); + break; + + case model.USER_PROPERTY_PROPERTY_CODE: + if (!disconnect.userProperties) { + disconnect.userProperties = new Array(); + } + index = decode_user_property(payload, index, disconnect.userProperties); + break; + + case model.SERVER_REFERENCE_PROPERTY_CODE: + [disconnect.serverReference, index] = decode_length_prefixed_string(payload, index); + break; + + default: + throw new CrtError("Unknown Disconnect property code: " + propertyCode); + } + } + + if (index != offset + propertyLength) { + throw new CrtError("Disconnect packet mismatch between encoded properties and expected length"); + } + + return index; +} + +function decode_disconnect_packet_5(firstByte: number, payload: DataView) : model.DisconnectPacketInternal { + if (firstByte != (model.PACKET_TYPE_DISCONNECT_FULL_ENCODING_311 >>> 8)) { + throw new CrtError("Disconnect packet with invalid first byte: " + firstByte); + } + + let index : number = 0; + let disconnect: model.DisconnectPacketInternal = { + type: mqtt5_packet.PacketType.Disconnect, + reasonCode: mqtt5_packet.DisconnectReasonCode.NormalDisconnection + }; + + if (payload.byteLength > 0) { + [disconnect.reasonCode, index] = decode_u8(payload, index); + + if (payload.byteLength > 1) { + let propertiesLength: number = 0; + [propertiesLength, index] = decode_vli(payload, index); + + index = decode_disconnect_properties(disconnect, payload, index, propertiesLength); + } + } + + if (index != payload.byteLength) { + throw new CrtError("Disconnect packet mismatch between payload and expected length"); + } + + return disconnect; +} + +// Decoder implementation + +export type DecodingFunction = (firstByte: number, payload: DataView) => mqtt5_packet.IPacket; +export type DecodingFunctionSet = Map; + +// decoders for server-decoded packets are found in the spec file +export function build_client_decoding_function_set(mode: model.ProtocolMode) : DecodingFunctionSet { + switch (mode) { + case model.ProtocolMode.Mqtt311: + return new Map([ + [mqtt5_packet.PacketType.Connack, decode_connack_packet_311], + [mqtt5_packet.PacketType.Publish, decode_publish_packet_311], + [mqtt5_packet.PacketType.Puback, decode_puback_packet_311], + [mqtt5_packet.PacketType.Suback, decode_suback_packet_311], + [mqtt5_packet.PacketType.Unsuback, decode_unsuback_packet_311], + [mqtt5_packet.PacketType.Pingresp, decode_pingresp_packet], + ]); + + case model.ProtocolMode.Mqtt5: + return new Map([ + [mqtt5_packet.PacketType.Connack, decode_connack_packet_5], + [mqtt5_packet.PacketType.Publish, decode_publish_packet_5], + [mqtt5_packet.PacketType.Puback, decode_puback_packet_5], + [mqtt5_packet.PacketType.Suback, decode_suback_packet_5], + [mqtt5_packet.PacketType.Unsuback, decode_unsuback_packet_5], + [mqtt5_packet.PacketType.Disconnect, decode_disconnect_packet_5], + [mqtt5_packet.PacketType.Pingresp, decode_pingresp_packet], + ]); + + } + + throw new CrtError("Unsupported protocol"); +} + +enum DecoderStateType { + + /** + * We're waiting for the a byte to tell us what the next packet is + */ + PendingFirstByte, + + /** + * We're waiting for the VLI-encoded remaining length of the full packet + */ + PendingRemainingLength, + + /** + * We're waiting for the complete packet payload (determined by the remaining length field) + */ + PendingPayload +} + +/** + * Starting buffer size for the buffer used to hold the payload (or the remaining length VLI encoding). Grows + * as necessary. + */ +const DEFAULT_SCRATCH_BUFFER_SIZE : number = 16 * 1024; + +/** + * Decoder implementation. All failures surface as exceptions and are considered protocol-fatal (the connection + * must be dropped). + */ +export class Decoder { + + private state: DecoderStateType; + private scratchBuffer: ArrayBuffer = new ArrayBuffer(DEFAULT_SCRATCH_BUFFER_SIZE); + private scratchView: DataView = new DataView(this.scratchBuffer); + private scratchIndex: number = 0; + private remainingLength : number = 0; + private firstByte : number = 0; + + constructor(private decoders: DecodingFunctionSet) { + this.state = DecoderStateType.PendingFirstByte; + } + + reset() { + this.state = DecoderStateType.PendingFirstByte; + this.remainingLength = 0; + this.firstByte = 0; + this.scratchIndex = 0; + } + + decode(data: DataView) : Array { + let current_data = data; + let packets = new Array(); + + let current_state = this.state; + let next_state = this.state; + while (current_data.byteLength > 0 || current_state != next_state) { + current_state = this.state; + switch (this.state) { + case DecoderStateType.PendingFirstByte: + current_data = this._handle_first_byte(current_data); + break; + + case DecoderStateType.PendingRemainingLength: + current_data = this._handle_remaining_length(current_data); + break; + + case DecoderStateType.PendingPayload: + current_data = this._handle_pending_payload(current_data, packets); + break; + } + next_state = this.state; + } + + return packets; + } + + private _handle_first_byte(data: DataView) : DataView { + if (data.byteLength == 0) { + return data; + } + + this.firstByte = data.getUint8(0); + this.state = DecoderStateType.PendingRemainingLength; + this.scratchIndex = 0; + this.scratchView = new DataView(this.scratchBuffer); + + return new DataView(data.buffer, data.byteOffset + 1, data.byteLength - 1); + } + + private _handle_remaining_length(data: DataView) : DataView { + if (data.byteLength == 0) { + return data; + } + + let nextByte = data.getUint8(0); + this.scratchView.setUint8(this.scratchIndex++, nextByte); + + let result = vli.decode_vli(new DataView(this.scratchBuffer, 0, this.scratchIndex), 0); + if (result.type == vli.VliDecodeResultType.Success) { + // @ts-ignore + this.remainingLength = result.value; + this.scratchIndex = 0; + if (this.remainingLength > this.scratchBuffer.byteLength) { + this.scratchBuffer = new ArrayBuffer(this.remainingLength * 3 / 2); + } + this.scratchView = new DataView(this.scratchBuffer, 0, this.remainingLength); + this.state = DecoderStateType.PendingPayload; + } + + return new DataView(data.buffer, data.byteOffset + 1, data.byteLength - 1); + } + + private _handle_pending_payload(data: DataView, packets: Array) : DataView { + let bytesToCopy = Math.min(data.byteLength, this.remainingLength - this.scratchIndex); + if (bytesToCopy > 0) { + let sourceView = new Uint8Array(data.buffer, data.byteOffset, bytesToCopy); + let destView = new Uint8Array(this.scratchBuffer, this.scratchIndex, bytesToCopy); + destView.set(sourceView); + this.scratchIndex += bytesToCopy; + } + + if (this.scratchIndex == this.remainingLength) { + this.state = DecoderStateType.PendingFirstByte; + this.scratchView = new DataView(this.scratchBuffer, 0, this.remainingLength); + packets.push(this._decode_packet()); + } + + return new DataView(data.buffer, data.byteOffset + bytesToCopy, data.byteLength - bytesToCopy); + } + + private _decode_packet() : mqtt5_packet.IPacket { + let packetType = this.firstByte >>> 4; + let decoder = this.decoders.get(packetType); + if (!decoder) { + throw new CrtError("No decoder for packet type"); + } + + return decoder(this.firstByte, this.scratchView); + } +} \ No newline at end of file diff --git a/lib/browser/mqtt_internal/encode_decode.spec.ts b/lib/browser/mqtt_internal/encode_decode.spec.ts new file mode 100644 index 000000000..ca1bfb4d7 --- /dev/null +++ b/lib/browser/mqtt_internal/encode_decode.spec.ts @@ -0,0 +1,1959 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +import * as decoder from './decoder'; +import * as encoder from './encoder'; +import {ServiceResultType} from './encoder'; +import * as model from "./model"; +import * as vli from "./vli"; +import {CrtError} from "../error"; +import * as mqtt5_packet from '../../common/mqtt5_packet'; + +function encode_connack_packet311(steps: Array, packet: model.ConnackPacketBinary) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.PACKET_TYPE_FIRST_BYTE_CONNACK }); + steps.push({ type: encoder.EncodingStepType.U8, value: 0x02 }); + steps.push({ type: encoder.EncodingStepType.U8, value: packet.sessionPresent ? 1 : 0 }); + steps.push({ type: encoder.EncodingStepType.U8, value: packet.reasonCode }); +} + +function get_suback_packet_remaining_lengths311(packet: model.SubackPacketBinary) : number { + return 2 + packet.reasonCodes.length; +} + +function encode_suback_packet311(steps: Array, packet: model.SubackPacketBinary) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.PACKET_TYPE_FIRST_BYTE_SUBACK }); + steps.push({ type: encoder.EncodingStepType.VLI, value: get_suback_packet_remaining_lengths311(packet) }); + steps.push({ type: encoder.EncodingStepType.U16, value: packet.packetId }); + + for (let reasonCode of packet.reasonCodes) { + steps.push({ type: encoder.EncodingStepType.U8, value: reasonCode }); + } +} + +function encode_unsuback_packet311(steps: Array, packet: model.UnsubackPacketBinary) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.PACKET_TYPE_FIRST_BYTE_UNSUBACK }); + steps.push({ type: encoder.EncodingStepType.U8, value: 2 }); + steps.push({ type: encoder.EncodingStepType.U16, value: packet.packetId }); +} + +function encode_pingresp_packet(steps: Array) { + steps.push({ type: encoder.EncodingStepType.U16, value: model.PACKET_TYPE_PINGRESP_FULL_ENCODING }); +} + +function get_connack_packet_remaining_lengths5(packet: model.ConnackPacketBinary) : [number, number] { + let remaining_length: number = 2; // 1 byte flags, 1 byte reason code + let properties_length: number = 0; + + if (packet.sessionExpiryInterval != undefined) { + properties_length += 5; + } + + if (packet.receiveMaximum != undefined) { + properties_length += 3; + } + + if (packet.maximumQos != undefined) { + properties_length += 2; + } + + if (packet.retainAvailable != undefined) { + properties_length += 2; + } + + if (packet.maximumPacketSize != undefined) { + properties_length += 5; + } + + if (packet.assignedClientIdentifier != undefined) { + properties_length += 3 + packet.assignedClientIdentifier.byteLength; + } + + if (packet.topicAliasMaximum != undefined) { + properties_length += 3; + } + + if (packet.reasonString != undefined) { + properties_length += 3 + packet.reasonString.byteLength; + } + + if (packet.wildcardSubscriptionsAvailable != undefined) { + properties_length += 2; + } + + if (packet.subscriptionIdentifiersAvailable != undefined) { + properties_length += 2; + } + + if (packet.sharedSubscriptionsAvailable != undefined) { + properties_length += 2; + } + + if (packet.serverKeepAlive != undefined) { + properties_length += 3; + } + + if (packet.responseInformation != undefined) { + properties_length += 3 + packet.responseInformation.byteLength; + } + + if (packet.serverReference != undefined) { + properties_length += 3 + packet.serverReference.byteLength; + } + + if (packet.authenticationMethod != undefined) { + properties_length += 3 + packet.authenticationMethod.byteLength; + } + + if (packet.authenticationData != undefined) { + properties_length += 3 + packet.authenticationData.byteLength; + } + + properties_length += encoder.compute_user_properties_length(packet.userProperties); + + remaining_length += vli.get_vli_byte_length(properties_length) + properties_length; + + return [remaining_length, properties_length]; +} + +function encode_connack_properties(steps: Array, packet: model.ConnackPacketBinary) { + if (packet.sessionExpiryInterval != undefined) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.SESSION_EXPIRY_INTERVAL_PROPERTY_CODE }); + steps.push({ type: encoder.EncodingStepType.U32, value: packet.sessionExpiryInterval }); + } + + if (packet.receiveMaximum != undefined) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.RECEIVE_MAXIMUM_PROPERTY_CODE }); + steps.push({ type: encoder.EncodingStepType.U16, value: packet.receiveMaximum }); + } + + if (packet.maximumQos != undefined) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.MAXIMUM_QOS_PROPERTY_CODE }); + steps.push({ type: encoder.EncodingStepType.U8, value: packet.maximumQos }); + } + + if (packet.retainAvailable != undefined) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.RETAIN_AVAILABLE_PROPERTY_CODE }); + steps.push({ type: encoder.EncodingStepType.U8, value: packet.retainAvailable }); + } + + if (packet.maximumPacketSize != undefined) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.MAXIMUM_PACKET_SIZE_PROPERTY_CODE }); + steps.push({ type: encoder.EncodingStepType.U32, value: packet.maximumPacketSize }); + } + + if (packet.assignedClientIdentifier != undefined) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.ASSIGNED_CLIENT_IDENTIFIER_PROPERTY_CODE }); + encoder.encode_required_length_prefixed_array_buffer(steps, packet.assignedClientIdentifier); + } + + if (packet.topicAliasMaximum != undefined) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.TOPIC_ALIAS_MAXIMUM_PROPERTY_CODE }); + steps.push({ type: encoder.EncodingStepType.U16, value: packet.topicAliasMaximum }); + } + + if (packet.reasonString != undefined) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.REASON_STRING_PROPERTY_CODE }); + encoder.encode_required_length_prefixed_array_buffer(steps, packet.reasonString); + } + + if (packet.wildcardSubscriptionsAvailable != undefined) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.WILDCARD_SUBSCRIPTIONS_AVAILABLE_PROPERTY_CODE }); + steps.push({ type: encoder.EncodingStepType.U8, value: packet.wildcardSubscriptionsAvailable }); + } + + if (packet.subscriptionIdentifiersAvailable != undefined) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.SUBSCRIPTION_IDENTIFIERS_AVAILABLE_PROPERTY_CODE }); + steps.push({ type: encoder.EncodingStepType.U8, value: packet.subscriptionIdentifiersAvailable }); + } + + if (packet.sharedSubscriptionsAvailable != undefined) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.SHARED_SUBSCRIPTIONS_AVAILABLE_PROPERTY_CODE }); + steps.push({ type: encoder.EncodingStepType.U8, value: packet.sharedSubscriptionsAvailable }); + } + + if (packet.serverKeepAlive != undefined) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.SERVER_KEEP_ALIVE_PROPERTY_CODE }); + steps.push({ type: encoder.EncodingStepType.U16, value: packet.serverKeepAlive }); + } + + if (packet.responseInformation != undefined) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.RESPONSE_INFORMATION_PROPERTY_CODE }); + encoder.encode_required_length_prefixed_array_buffer(steps, packet.responseInformation); + } + + if (packet.serverReference != undefined) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.SERVER_REFERENCE_PROPERTY_CODE }); + encoder.encode_required_length_prefixed_array_buffer(steps, packet.serverReference); + } + + if (packet.authenticationMethod != undefined) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.AUTHENTICATION_METHOD_PROPERTY_CODE }); + encoder.encode_required_length_prefixed_array_buffer(steps, packet.authenticationMethod); + } + + if (packet.authenticationData != undefined) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.AUTHENTICATION_DATA_PROPERTY_CODE }); + encoder.encode_required_length_prefixed_array_buffer(steps, packet.authenticationData); + } + + encoder.encode_user_properties(steps, packet.userProperties); +} + +function encode_connack_packet5(steps: Array, packet: model.ConnackPacketBinary) { + let [remaining_length, properties_length] = get_connack_packet_remaining_lengths5(packet); + + steps.push({ type: encoder.EncodingStepType.U8, value: model.PACKET_TYPE_FIRST_BYTE_CONNACK }); + steps.push({ type: encoder.EncodingStepType.VLI, value: remaining_length }); + steps.push({ type: encoder.EncodingStepType.U8, value: packet.sessionPresent ? 1 : 0 }); + steps.push({ type: encoder.EncodingStepType.U8, value: packet.reasonCode }); + steps.push({ type: encoder.EncodingStepType.VLI, value: properties_length }); + + encode_connack_properties(steps, packet); +} + +function get_suback_packet_remaining_lengths5(packet: model.SubackPacketBinary) : [number, number] { + let remaining_length: number = 2; // packet id + let properties_length: number = 0; + + if (packet.reasonString != undefined) { + properties_length += 3 + packet.reasonString.byteLength; + } + + properties_length += encoder.compute_user_properties_length(packet.userProperties); + + remaining_length += properties_length + vli.get_vli_byte_length(properties_length); + remaining_length += packet.reasonCodes.length; + + return [remaining_length, properties_length]; +} + +function encode_suback_properties(steps: Array, packet: model.SubackPacketBinary) { + if (packet.reasonString != undefined) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.REASON_STRING_PROPERTY_CODE }); + encoder.encode_required_length_prefixed_array_buffer(steps, packet.reasonString); + } + + encoder.encode_user_properties(steps, packet.userProperties); +} + +function encode_suback_packet5(steps: Array, packet: model.SubackPacketBinary) { + let [remaining_length, properties_length] = get_suback_packet_remaining_lengths5(packet); + + steps.push({ type: encoder.EncodingStepType.U8, value: model.PACKET_TYPE_FIRST_BYTE_SUBACK }); + steps.push({ type: encoder.EncodingStepType.VLI, value: remaining_length }); + steps.push({ type: encoder.EncodingStepType.U16, value: packet.packetId }); + + steps.push({ type: encoder.EncodingStepType.VLI, value: properties_length }); + encode_suback_properties(steps, packet); + + for (let reason_code of packet.reasonCodes) { + steps.push({ type: encoder.EncodingStepType.U8, value: reason_code }); + } +} + +function get_unsuback_packet_remaining_lengths5(packet: model.UnsubackPacketBinary) : [number, number] { + let remaining_length: number = 2; // packet id + let properties_length: number = encoder.compute_user_properties_length(packet.userProperties); + + if (packet.reasonString != undefined) { + properties_length += 3 + packet.reasonString.byteLength; + } + + remaining_length += properties_length + vli.get_vli_byte_length(properties_length); + remaining_length += packet.reasonCodes.length; + + return [remaining_length, properties_length]; +} + +function encode_unsuback_properties(steps: Array, packet: model.UnsubackPacketBinary) { + if (packet.reasonString != undefined) { + steps.push({ type: encoder.EncodingStepType.U8, value: model.REASON_STRING_PROPERTY_CODE }); + encoder.encode_required_length_prefixed_array_buffer(steps, packet.reasonString); + } + + encoder.encode_user_properties(steps, packet.userProperties); +} + +function encode_unsuback_packet5(steps: Array, packet: model.UnsubackPacketBinary) { + let [remaining_length, properties_length] = get_unsuback_packet_remaining_lengths5(packet); + + steps.push({ type: encoder.EncodingStepType.U8, value: model.PACKET_TYPE_FIRST_BYTE_UNSUBACK }); + steps.push({ type: encoder.EncodingStepType.VLI, value: remaining_length }); + steps.push({ type: encoder.EncodingStepType.U16, value: packet.packetId }); + + steps.push({ type: encoder.EncodingStepType.VLI, value: properties_length }); + encode_unsuback_properties(steps, packet); + + for (let reason_code of packet.reasonCodes) { + steps.push({ type: encoder.EncodingStepType.U8, value: reason_code }); + } +} + +function apply_debug_encoders_to_encoding_function_set(encoders: encoder.EncodingFunctionSet, mode: model.ProtocolMode) { + switch(mode) { + case model.ProtocolMode.Mqtt5: + encoders.set(mqtt5_packet.PacketType.Connack, (steps, packet) => { encode_connack_packet5(steps, packet as model.ConnackPacketBinary); }); + encoders.set(mqtt5_packet.PacketType.Suback, (steps, packet) => { encode_suback_packet5(steps, packet as model.SubackPacketBinary); }); + encoders.set(mqtt5_packet.PacketType.Unsuback, (steps, packet) => { encode_unsuback_packet5(steps, packet as model.UnsubackPacketBinary); }); + encoders.set(mqtt5_packet.PacketType.Pingresp, (steps, packet) => { encode_pingresp_packet(steps); }); + return; + + case model.ProtocolMode.Mqtt311: + encoders.set(mqtt5_packet.PacketType.Connack, (steps, packet) => { encode_connack_packet311(steps, packet as model.ConnackPacketBinary); }); + encoders.set(mqtt5_packet.PacketType.Suback, (steps, packet) => { encode_suback_packet311(steps, packet as model.SubackPacketBinary); }); + encoders.set(mqtt5_packet.PacketType.Unsuback, (steps, packet) => { encode_unsuback_packet311(steps, packet as model.UnsubackPacketBinary); }); + encoders.set(mqtt5_packet.PacketType.Pingresp, (steps, packet) => { encode_pingresp_packet(steps); }); + return; + } + + throw new CrtError("Unsupported Protocol Mode"); +} + +function decode_pingreq_packet(firstByte: number, payload: DataView) : model.PingreqPacketInternal { + if (payload.byteLength != 0) { + throw new CrtError("Pingreq packet with invalid payload"); + } + + if (firstByte != (model.PACKET_TYPE_PINGREQ_FULL_ENCODING >>> 8)) { + throw new CrtError("Pingreq packet with invalid first byte: " + firstByte); + } + + return { + type: mqtt5_packet.PacketType.Pingreq + }; +} + +function decode_connect_packet311(firstByte: number, payload: DataView) : model.ConnectPacketInternal { + + if (firstByte != model.PACKET_TYPE_FIRST_BYTE_CONNECT) { + throw new CrtError("Connect(311) packet with invalid first byte: " + firstByte); + } + + let connect : model.ConnectPacketInternal = { + type: mqtt5_packet.PacketType.Connect, + keepAliveIntervalSeconds: 0, + clientId: "", + cleanStart: false + }; + + let index: number = 0; + let protocol: string = ""; + + [protocol, index] = decoder.decode_length_prefixed_string(payload, index); + if (protocol != "MQTT") { + throw new CrtError("Connect(311) packet with invalid protocol"); + } + + let protocolVersion: number = 0; + [protocolVersion, index] = decoder.decode_u8(payload, index); + if (protocolVersion != 4) { + throw new CrtError("Connect(311) packet with mismatched protocol version"); + } + + let flags: number = 0; + [flags, index] = decoder.decode_u8(payload, index); + + if (flags & model.CONNECT_FLAGS_CLEAN_SESSION) { + connect.cleanStart = true; + } + + [connect.keepAliveIntervalSeconds, index] = decoder.decode_u16(payload, index); + [connect.clientId, index] = decoder.decode_length_prefixed_string(payload, index); + + if (flags & model.CONNECT_FLAGS_HAS_WILL) { + let willTopic : string = ""; + let willPayload : ArrayBuffer | null = null; + + [willTopic, index] = decoder.decode_length_prefixed_string(payload, index); + [willPayload, index] = decoder.decode_length_prefixed_bytes(payload, index); + + connect.will = { + type: mqtt5_packet.PacketType.Publish, + topicName: willTopic, + payload: willPayload, + qos: (flags >>> model.CONNECT_FLAGS_QOS_SHIFT) & model.QOS_MASK, + retain: (flags & model.CONNECT_FLAGS_WILL_RETAIN) != 0 + }; + } + + if (flags & model.CONNECT_FLAGS_HAS_USERNAME) { + [connect.username, index] = decoder.decode_length_prefixed_string(payload, index); + } + + if (flags & model.CONNECT_FLAGS_HAS_PASSWORD) { + [connect.password, index] = decoder.decode_length_prefixed_bytes(payload, index); + } + + if (index != payload.byteLength) { + throw new CrtError("??"); + } + + return connect; +} + +function decode_subscribe_packet311(firstByte: number, payload: DataView) : model.SubscribePacketInternal { + if (firstByte != model.PACKET_TYPE_FIRST_BYTE_SUBSCRIBE) { + throw new CrtError("Subscribe(311) packet with invalid first byte: " + firstByte); + } + + let subscribe : model.SubscribePacketInternal = { + type: mqtt5_packet.PacketType.Subscribe, + packetId: 0, + subscriptions: new Array() + }; + + let index: number = 0; + + [subscribe.packetId, index] = decoder.decode_u16(payload, index); + + while (index < payload.byteLength) { + let subscription : mqtt5_packet.Subscription = { + topicFilter: "", + qos: 0 + }; + + [subscription.topicFilter, index] = decoder.decode_length_prefixed_string(payload, index); + [subscription.qos, index] = decoder.decode_u8(payload, index); + + subscribe.subscriptions.push(subscription); + } + + return subscribe; +} + +function decode_unsubscribe_packet311(firstByte: number, payload: DataView) : model.UnsubscribePacketInternal { + if (firstByte != model.PACKET_TYPE_FIRST_BYTE_UNSUBSCRIBE) { + throw new CrtError("Unsubscribe(311) packet with invalid first byte: " + firstByte); + } + + let unsubscribe : model.UnsubscribePacketInternal = { + type: mqtt5_packet.PacketType.Unsubscribe, + packetId: 0, + topicFilters: new Array() + }; + + let index: number = 0; + + [unsubscribe.packetId, index] = decoder.decode_u16(payload, index); + + while (index < payload.byteLength) { + let topicFilter : string = ""; + [topicFilter, index] = decoder.decode_length_prefixed_string(payload, index); + unsubscribe.topicFilters.push(topicFilter); + } + + return unsubscribe; +} + +function decode_disconnect_packet311(firstByte: number, payload: DataView) : mqtt5_packet.DisconnectPacket { + if (payload.byteLength != 0) { + throw new CrtError("Disconnect(311) packet with invalid payload"); + } + + if (firstByte != (model.PACKET_TYPE_DISCONNECT_FULL_ENCODING_311 >>> 8)) { + throw new CrtError("Disconnect(311) packet with invalid first byte: " + firstByte); + } + + return { + type: mqtt5_packet.PacketType.Disconnect, + reasonCode: mqtt5_packet.DisconnectReasonCode.NormalDisconnection + }; +} + +function decode_subscribe_properties(subscribe: model.SubscribePacketInternal, payload: DataView, offset: number, propertyLength: number) : number { + let index : number = offset; + let propertyCode : number = 0; + while (index < offset + propertyLength) { + [propertyCode, index] = decoder.decode_u8(payload, index); + switch (propertyCode) { + + case model.SUBSCRIPTION_IDENTIFIER_PROPERTY_CODE: + [subscribe.subscriptionIdentifier, index] = decoder.decode_vli(payload, index); + break; + + case model.USER_PROPERTY_PROPERTY_CODE: + if (!subscribe.userProperties) { + subscribe.userProperties = new Array(); + } + index = decoder.decode_user_property(payload, index, subscribe.userProperties); + break; + + default: + throw new CrtError("Unknown Subscribe property code: " + propertyCode); + } + } + + if (index != offset + propertyLength) { + throw new CrtError("Subscribe packet mismatch between encoded properties and expected length"); + } + + return index; +} + +function decode_subscribe_packet5(firstByte: number, payload: DataView) : model.SubscribePacketInternal { + if (firstByte != model.PACKET_TYPE_FIRST_BYTE_SUBSCRIBE) { + throw new CrtError("Subscribe(5) packet with invalid first byte: " + firstByte); + } + + let subscribe : model.SubscribePacketInternal = { + type: mqtt5_packet.PacketType.Subscribe, + packetId: 0, + subscriptions: new Array() + }; + + let index: number = 0; + [subscribe.packetId, index] = decoder.decode_u16(payload, index); + + let propertiesLength: number = 0; + [propertiesLength, index] = decoder.decode_vli(payload, index); + + index = decode_subscribe_properties(subscribe, payload, index, propertiesLength); + + while (index < payload.byteLength) { + let subscription : mqtt5_packet.Subscription = { + topicFilter: "", + qos: 0 + }; + + [subscription.topicFilter, index] = decoder.decode_length_prefixed_string(payload, index); + + let subscriptionFlags : number = 0; + [subscriptionFlags, index] = decoder.decode_u8(payload, index); + + subscription.qos = subscriptionFlags & model.QOS_MASK; + subscription.noLocal = (subscriptionFlags & model.SUBSCRIPTION_FLAGS_NO_LOCAL) != 0; + subscription.retainAsPublished = (subscriptionFlags & model.SUBSCRIPTION_FLAGS_RETAIN_AS_PUBLISHED) != 0; + subscription.retainHandlingType = (subscriptionFlags >>> model.SUBSCRIPTION_FLAGS_RETAIN_HANDLING_TYPE_SHIFT) & model.RETAIN_HANDLING_TYPE_SHIFT; + + subscribe.subscriptions.push(subscription); + } + + if (index != payload.byteLength) { + throw new CrtError("Subscribe packet mismatch between encoded subscriptions and expected length"); + } + + return subscribe; +} + +function decode_unsubscribe_properties(unsubscribe: model.UnsubscribePacketInternal, payload: DataView, offset: number, propertyLength: number) : number { + let index : number = offset; + let propertyCode : number = 0; + while (index < offset + propertyLength) { + [propertyCode, index] = decoder.decode_u8(payload, index); + switch (propertyCode) { + + case model.USER_PROPERTY_PROPERTY_CODE: + if (!unsubscribe.userProperties) { + unsubscribe.userProperties = new Array(); + } + index = decoder.decode_user_property(payload, index, unsubscribe.userProperties); + break; + + default: + throw new CrtError("Unknown Unsubscribe property code: " + propertyCode); + } + } + + if (index != offset + propertyLength) { + throw new CrtError("Unsubscribe packet mismatch between encoded properties and expected length"); + } + + return index; +} + +function decode_unsubscribe_packet5(firstByte: number, payload: DataView) : model.UnsubscribePacketInternal { + if (firstByte != model.PACKET_TYPE_FIRST_BYTE_UNSUBSCRIBE) { + throw new CrtError("Unsubscribe(5) packet with invalid first byte: " + firstByte); + } + + let unsubscribe : model.UnsubscribePacketInternal = { + type: mqtt5_packet.PacketType.Unsubscribe, + packetId: 0, + topicFilters: new Array() + }; + + let index: number = 0; + [unsubscribe.packetId, index] = decoder.decode_u16(payload, index); + + let propertiesLength: number = 0; + [propertiesLength, index] = decoder.decode_vli(payload, index); + + index = decode_unsubscribe_properties(unsubscribe, payload, index, propertiesLength); + + while (index < payload.byteLength) { + let topicFilter : string = ""; + [topicFilter, index] = decoder.decode_length_prefixed_string(payload, index); + unsubscribe.topicFilters.push(topicFilter); + } + + return unsubscribe; +} + +function decode_connect_properties(connect: model.ConnectPacketInternal, payload: DataView, offset: number, propertyLength: number) : number { + let index : number = offset; + let propertyCode : number = 0; + while (index < offset + propertyLength) { + [propertyCode, index] = decoder.decode_u8(payload, index); + switch (propertyCode) { + + case model.SESSION_EXPIRY_INTERVAL_PROPERTY_CODE: + [connect.sessionExpiryIntervalSeconds, index] = decoder.decode_u32(payload, index); + break; + + case model.RECEIVE_MAXIMUM_PROPERTY_CODE: + [connect.receiveMaximum, index] = decoder.decode_u16(payload, index); + break; + + case model.MAXIMUM_PACKET_SIZE_PROPERTY_CODE: + [connect.maximumPacketSizeBytes, index] = decoder.decode_u32(payload, index); + break; + + case model.TOPIC_ALIAS_MAXIMUM_PROPERTY_CODE: + [connect.topicAliasMaximum, index] = decoder.decode_u16(payload, index); + break; + + case model.REQUEST_RESPONSE_INFORMATION_PROPERTY_CODE: + [connect.requestResponseInformation, index] = decoder.decode_boolean(payload, index); + break; + + case model.REQUEST_PROBLEM_INFORMATION_PROPERTY_CODE: + [connect.requestProblemInformation, index] = decoder.decode_boolean(payload, index); + break; + + case model.USER_PROPERTY_PROPERTY_CODE: + if (!connect.userProperties) { + connect.userProperties = new Array(); + } + index = decoder.decode_user_property(payload, index, connect.userProperties); + break; + + case model.AUTHENTICATION_METHOD_PROPERTY_CODE: + [connect.authenticationMethod, index] = decoder.decode_length_prefixed_string(payload, index); + break; + + case model.AUTHENTICATION_DATA_PROPERTY_CODE: + [connect.authenticationData, index] = decoder.decode_length_prefixed_bytes(payload, index); + break; + + default: + throw new CrtError("Unknown Connect property code: " + propertyCode); + } + } + + if (index != offset + propertyLength) { + throw new CrtError("Connect packet mismatch between encoded properties and expected length"); + } + + return index; +} + +function decode_will_properties(connect: model.ConnectPacketInternal, will: model.PublishPacketInternal, payload: DataView, offset: number, propertyLength: number) : number { + let index : number = offset; + let propertyCode : number = 0; + while (index < offset + propertyLength) { + [propertyCode, index] = decoder.decode_u8(payload, index); + switch (propertyCode) { + + case model.WILL_DELAY_INTERVAL_PROPERTY_CODE: + [connect.willDelayIntervalSeconds, index] = decoder.decode_u32(payload, index); + break; + + case model.PAYLOAD_FORMAT_INDICATOR_PROPERTY_CODE: + [will.payloadFormat, index] = decoder.decode_u8(payload, index); + break; + + case model.MESSAGE_EXPIRY_INTERVAL_PROPERTY_CODE: + [will.messageExpiryIntervalSeconds, index] = decoder.decode_u32(payload, index); + break; + + case model.CONTENT_TYPE_PROPERTY_CODE: + [will.contentType, index] = decoder.decode_length_prefixed_string(payload, index); + break; + + case model.RESPONSE_TOPIC_PROPERTY_CODE: + [will.responseTopic, index] = decoder.decode_length_prefixed_string(payload, index); + break; + + case model.CORRELATION_DATA_PROPERTY_CODE: + [will.correlationData, index] = decoder.decode_length_prefixed_bytes(payload, index); + break; + + case model.USER_PROPERTY_PROPERTY_CODE: + if (!will.userProperties) { + will.userProperties = new Array(); + } + index = decoder.decode_user_property(payload, index, will.userProperties); + break; + + default: + throw new CrtError("Unknown will property code: " + propertyCode); + } + } + + if (index != offset + propertyLength) { + throw new CrtError("Will mismatch between encoded properties and expected length"); + } + + return index; +} + +function decode_connect_packet5(firstByte: number, payload: DataView) : model.ConnectPacketInternal { + if (firstByte != model.PACKET_TYPE_FIRST_BYTE_CONNECT) { + throw new CrtError("Connect(5) packet with invalid first byte: " + firstByte); + } + + let connect : model.ConnectPacketInternal = { + type: mqtt5_packet.PacketType.Connect, + keepAliveIntervalSeconds: 0, + clientId: "", + cleanStart: false + }; + + let index: number = 0; + let protocol: string = ""; + + [protocol, index] = decoder.decode_length_prefixed_string(payload, index); + if (protocol != "MQTT") { + throw new CrtError("Connect(5) packet with invalid protocol"); + } + + let protocolVersion: number = 0; + [protocolVersion, index] = decoder.decode_u8(payload, index); + if (protocolVersion != 5) { + throw new CrtError("Connect(5) packet with unexpected protocol version"); + } + + let flags: number = 0; + [flags, index] = decoder.decode_u8(payload, index); + + if (flags & model.CONNECT_FLAGS_CLEAN_SESSION) { + connect.cleanStart = true; + } + + [connect.keepAliveIntervalSeconds, index] = decoder.decode_u16(payload, index); + + let propertiesLength: number = 0; + [propertiesLength, index] = decoder.decode_vli(payload, index); + + index = decode_connect_properties(connect, payload, index, propertiesLength); + + [connect.clientId, index] = decoder.decode_length_prefixed_string(payload, index); + + if (flags & model.CONNECT_FLAGS_HAS_WILL) { + // @ts-ignore + let will : model.PublishPacketInternal = { + type: mqtt5_packet.PacketType.Publish, + }; + + let willPropertiesLength: number = 0; + [willPropertiesLength, index] = decoder.decode_vli(payload, index); + + index = decode_will_properties(connect, will, payload, index, willPropertiesLength); + + [will.topicName, index] = decoder.decode_length_prefixed_string(payload, index); + [will.payload, index] = decoder.decode_length_prefixed_bytes(payload, index); + will.qos = (flags >>> model.CONNECT_FLAGS_QOS_SHIFT) & model.QOS_MASK; + will.retain = (flags & model.CONNECT_FLAGS_WILL_RETAIN) != 0; + + connect.will = will; + } + + if (flags & model.CONNECT_FLAGS_HAS_USERNAME) { + [connect.username, index] = decoder.decode_length_prefixed_string(payload, index); + } + + if (flags & model.CONNECT_FLAGS_HAS_PASSWORD) { + [connect.password, index] = decoder.decode_length_prefixed_bytes(payload, index); + } + + if (index != payload.byteLength) { + throw new CrtError("Connect packet mismatch between payload and expected length"); + } + + return connect; +} + +function apply_debug_decoders_to_decoding_function_set(decoders: decoder.DecodingFunctionSet, mode: model.ProtocolMode) { + + switch(mode) { + case model.ProtocolMode.Mqtt5: + decoders.set(mqtt5_packet.PacketType.Pingreq, (firstByte, payload) => { return decode_pingreq_packet(firstByte, payload); }); + decoders.set(mqtt5_packet.PacketType.Subscribe, (firstByte, payload) => { return decode_subscribe_packet5(firstByte, payload); }); + decoders.set(mqtt5_packet.PacketType.Unsubscribe, (firstByte, payload) => { return decode_unsubscribe_packet5(firstByte, payload); }); + decoders.set(mqtt5_packet.PacketType.Connect, (firstByte, payload) => { return decode_connect_packet5(firstByte, payload); }); + return; + + case model.ProtocolMode.Mqtt311: + decoders.set(mqtt5_packet.PacketType.Pingreq, (firstByte, payload) => { return decode_pingreq_packet(firstByte, payload); }); + decoders.set(mqtt5_packet.PacketType.Subscribe, (firstByte, payload) => { return decode_subscribe_packet311(firstByte, payload); }); + decoders.set(mqtt5_packet.PacketType.Unsubscribe, (firstByte, payload) => { return decode_unsubscribe_packet311(firstByte, payload); }); + decoders.set(mqtt5_packet.PacketType.Connect, (firstByte, payload) => { return decode_connect_packet311(firstByte, payload); }); + decoders.set(mqtt5_packet.PacketType.Disconnect, (firstByte, payload) => { return decode_disconnect_packet311(firstByte, payload); }); + return; + } + + throw new CrtError("Unsupported Protocol Mode"); +} + +function optional_booleans_equal(lhs: boolean | undefined, rhs: boolean | undefined) : boolean { + if (lhs == undefined && rhs == undefined) { + return true; + } + + if (lhs != undefined && rhs != undefined) { + return lhs == rhs; + } + + return false; +} + +function optional_booleans_falsy_equal(lhs: boolean | undefined, rhs: boolean | undefined) : boolean { + if (lhs == undefined && rhs == undefined) { + return true; + } + + if (lhs != undefined && rhs != undefined) { + return lhs == rhs; + } + + return !lhs && !rhs; +} + +function optional_numbers_equal(lhs: number | undefined, rhs: number | undefined) : boolean { + if (lhs == undefined && rhs == undefined) { + return true; + } + + if (lhs != undefined && rhs != undefined) { + return lhs == rhs; + } + + return false; +} + +function optional_numbers_falsy_equal(lhs: number | undefined, rhs: number | undefined) : boolean { + if (lhs == undefined && rhs == undefined) { + return true; + } + + if (lhs != undefined && rhs != undefined) { + return lhs == rhs; + } + + return !lhs && !rhs; +} + +function optional_strings_equal(lhs: string | undefined, rhs: string | undefined) : boolean { + if (lhs == undefined && rhs == undefined) { + return true; + } + + if (lhs != undefined && rhs != undefined) { + return lhs === rhs; + } + + return (lhs == undefined || lhs.length == 0) && (rhs == undefined || rhs.length == 0); +} + +function buffers_equal(lhs: ArrayBuffer, rhs: ArrayBuffer) : boolean { + let lhs_view = new DataView(lhs); + let rhs_view = new DataView(rhs); + + if (lhs_view.byteLength != rhs_view.byteLength) { + return false; + } + + for (let i = 0; i < lhs_view.byteLength; i++) { + if (lhs_view.getUint8(i) != rhs_view.getUint8(i)) { + return false; + } + } + + return true; +} + +function optional_buffers_equal(lhs: ArrayBuffer | undefined, rhs: ArrayBuffer | undefined) : boolean { + if (lhs == undefined && rhs == undefined) { + return true; + } + + if (lhs != undefined && rhs != undefined) { + return buffers_equal(lhs, rhs); + } + + return (lhs == undefined || lhs.byteLength == 0) && (rhs == undefined || rhs.byteLength == 0); +} + +function user_properties_equal(lhs: Array | undefined, rhs: Array | undefined) : boolean { + if (lhs == undefined && rhs == undefined) { + return true; + } + + if (lhs != undefined && rhs != undefined) { + if (lhs.length != rhs.length) { + return false; + } + + for (let i = 0; i < lhs.length; i++) { + if (lhs[i].name !== rhs[i].name) { + return false; + } + + if (lhs[i].value !== rhs[i].value) { + return false; + } + } + + return true; + } + + return (lhs == undefined || lhs.length == 0) && (rhs == undefined || rhs.length == 0); +} + +function are_connect_packets_equal(lhs: model.ConnectPacketInternal, rhs: model.ConnectPacketInternal) : boolean { + return optional_booleans_equal(lhs.cleanStart, rhs.cleanStart) && + optional_numbers_equal(lhs.topicAliasMaximum, rhs.topicAliasMaximum) && + optional_strings_equal(lhs.authenticationMethod, rhs.authenticationMethod) && + optional_buffers_equal(lhs.authenticationData, rhs.authenticationData) && + optional_numbers_equal(lhs.keepAliveIntervalSeconds, rhs.keepAliveIntervalSeconds) && + optional_strings_equal(lhs.clientId, rhs.clientId) && + optional_strings_equal(lhs.username, rhs.username) && + optional_buffers_equal(binary_as_optional_buffer(lhs.password), binary_as_optional_buffer(rhs.password)) && + optional_numbers_equal(lhs.sessionExpiryIntervalSeconds, rhs.sessionExpiryIntervalSeconds) && + optional_booleans_equal(lhs.requestResponseInformation, rhs.requestResponseInformation) && + optional_booleans_equal(lhs.requestProblemInformation, rhs.requestProblemInformation) && + optional_numbers_equal(lhs.receiveMaximum, rhs.receiveMaximum) && + optional_numbers_equal(lhs.maximumPacketSizeBytes, rhs.maximumPacketSizeBytes) && + optional_numbers_equal(lhs.willDelayIntervalSeconds, rhs.willDelayIntervalSeconds) && + are_publish_packets_equal(lhs.will, rhs.will) && + user_properties_equal(lhs.userProperties, rhs.userProperties); +} + +function are_connack_packets_equal(lhs: model.ConnackPacketInternal, rhs: model.ConnackPacketInternal) : boolean { + return optional_strings_equal(lhs.authenticationMethod, rhs.authenticationMethod) && + optional_buffers_equal(lhs.authenticationData, rhs.authenticationData) && + lhs.sessionPresent == rhs.sessionPresent && + lhs.reasonCode == rhs.reasonCode && + optional_numbers_equal(lhs.sessionExpiryInterval, rhs.sessionExpiryInterval) && + optional_numbers_equal(lhs.receiveMaximum, rhs.receiveMaximum) && + optional_numbers_equal(lhs.maximumQos, rhs.maximumQos) && + optional_booleans_equal(lhs.retainAvailable, rhs.retainAvailable) && + optional_numbers_equal(lhs.maximumPacketSize, rhs.maximumPacketSize) && + optional_strings_equal(lhs.assignedClientIdentifier, rhs.assignedClientIdentifier) && + optional_numbers_equal(lhs.topicAliasMaximum, rhs.topicAliasMaximum) && + optional_strings_equal(lhs.reasonString, rhs.reasonString) && + optional_booleans_equal(lhs.wildcardSubscriptionsAvailable, rhs.wildcardSubscriptionsAvailable) && + optional_booleans_equal(lhs.subscriptionIdentifiersAvailable, rhs.subscriptionIdentifiersAvailable) && + optional_booleans_equal(lhs.sharedSubscriptionsAvailable, rhs.sharedSubscriptionsAvailable) && + optional_numbers_equal(lhs.serverKeepAlive, rhs.serverKeepAlive) && + optional_strings_equal(lhs.responseInformation, rhs.responseInformation) && + optional_strings_equal(lhs.serverReference, rhs.serverReference) && + user_properties_equal(lhs.userProperties, rhs.userProperties); +} + +function binary_as_optional_buffer(source: BinaryData | undefined) : ArrayBuffer | undefined { + if (source == undefined) { + return undefined; + } + + return source as ArrayBuffer; +} + +function payload_as_optional_buffer(source: mqtt5_packet.Payload | undefined) : ArrayBuffer | undefined { + if (source == undefined) { + return undefined; + } + + return source as ArrayBuffer; +} + +function number_arrays_equal(lhs: Array | undefined, rhs: Array | undefined) : boolean { + if (lhs == undefined && rhs == undefined) { + return true; + } + + if (lhs != undefined && rhs != undefined) { + if (lhs.length != rhs.length) { + return false; + } + + for (let i = 0; i < lhs.length; i++) { + if (lhs[i] != rhs[i]) { + return false; + } + } + + return true; + } + + return (lhs == undefined || lhs.length == 0) && (rhs == undefined || rhs.length == 0); +} + +function are_publish_packets_equal(lhs: mqtt5_packet.PublishPacket | undefined, rhs: mqtt5_packet.PublishPacket | undefined) : boolean { + if (lhs == undefined && rhs == undefined) { + return true; + } + + if (lhs != undefined && rhs != undefined) { + return lhs.topicName == rhs.topicName && + lhs.qos == rhs.qos && + optional_booleans_equal(lhs.retain, rhs.retain) && + optional_numbers_equal(lhs.payloadFormat, rhs.payloadFormat) && + optional_numbers_equal(lhs.messageExpiryIntervalSeconds, rhs.messageExpiryIntervalSeconds) && + optional_numbers_equal(lhs.topicAlias, rhs.topicAlias) && + optional_strings_equal(lhs.responseTopic, rhs.responseTopic) && + optional_buffers_equal(binary_as_optional_buffer(lhs.correlationData), binary_as_optional_buffer(rhs.correlationData)) && + optional_strings_equal(lhs.contentType, rhs.contentType) && + optional_buffers_equal(payload_as_optional_buffer(lhs.payload), payload_as_optional_buffer(rhs.payload)) && + number_arrays_equal(lhs.subscriptionIdentifiers, rhs.subscriptionIdentifiers) && + user_properties_equal(lhs.userProperties, rhs.userProperties); + } + + return false; +} + +function are_publish_internal_packets_equal(lhs: model.PublishPacketInternal | undefined, rhs: model.PublishPacketInternal | undefined) : boolean { + if (lhs == undefined && rhs == undefined) { + return true; + } + + if (lhs != undefined && rhs != undefined) { + return lhs.packetId == rhs.packetId && + lhs.duplicate == rhs.duplicate && + are_publish_packets_equal(lhs, rhs); + } + + return false; +} + +function are_puback_packets_equal(lhs: model.PubackPacketInternal, rhs: model.PubackPacketInternal) : boolean { + return lhs.packetId == rhs.packetId && + lhs.reasonCode == rhs.reasonCode && + optional_strings_equal(lhs.reasonString, rhs.reasonString) && + user_properties_equal(lhs.userProperties, rhs.userProperties); +} + +function subscriptions_equal(lhs: Array, rhs: Array) : boolean { + if (lhs.length != rhs.length) { + return false; + } + + for (let i = 0; i < lhs.length; i++) { + if (lhs[i].topicFilter !== rhs[i].topicFilter) { + return false; + } + + if (lhs[i].qos != rhs[i].qos) { + return false; + } + + if (!optional_booleans_falsy_equal(lhs[i].noLocal, rhs[i].noLocal)) { + return false; + } + + if (!optional_booleans_falsy_equal(lhs[i].retainAsPublished, rhs[i].retainAsPublished)) { + return false; + } + + if (!optional_numbers_falsy_equal(lhs[i].retainHandlingType, rhs[i].retainHandlingType)) { + return false; + } + } + + return true; +} + +function are_subscribe_packets_equal(lhs: model.SubscribePacketInternal, rhs: model.SubscribePacketInternal) : boolean { + return lhs.packetId == rhs.packetId && + optional_numbers_equal(lhs.subscriptionIdentifier, rhs.subscriptionIdentifier) && + subscriptions_equal(lhs.subscriptions, rhs.subscriptions) && + user_properties_equal(lhs.userProperties, rhs.userProperties); +} + +function are_suback_packets_equal(lhs: model.SubackPacketInternal, rhs: model.SubackPacketInternal) : boolean { + return lhs.packetId == rhs.packetId && + number_arrays_equal(lhs.reasonCodes, rhs.reasonCodes) && + optional_strings_equal(lhs.reasonString, rhs.reasonString) && + user_properties_equal(lhs.userProperties, rhs.userProperties); +} + +function string_arrays_equal(lhs: Array, rhs: Array) : boolean { + if (lhs.length != rhs.length) { + return false; + } + + for (let i = 0; i < lhs.length; i++) { + if (lhs[i] !== rhs[i]) { + return false; + } + } + + return true; +} + +function are_unsubscribe_packets_equal(lhs: model.UnsubscribePacketInternal, rhs: model.UnsubscribePacketInternal) : boolean { + return lhs.packetId == rhs.packetId && + string_arrays_equal(lhs.topicFilters, rhs.topicFilters) && + user_properties_equal(lhs.userProperties, rhs.userProperties); +} + +function are_unsuback_packets_equal(lhs: model.UnsubackPacketInternal, rhs: model.UnsubackPacketInternal) : boolean { + return lhs.packetId == rhs.packetId && + number_arrays_equal(lhs.reasonCodes, rhs.reasonCodes) && + optional_strings_equal(lhs.reasonString, rhs.reasonString) && + user_properties_equal(lhs.userProperties, rhs.userProperties); +} + +function are_disconnect_packets_equal(lhs: model.DisconnectPacketInternal, rhs: model.DisconnectPacketInternal) : boolean { + return lhs.reasonCode == rhs.reasonCode && + optional_numbers_equal(lhs.sessionExpiryIntervalSeconds, rhs.sessionExpiryIntervalSeconds) && + optional_strings_equal(lhs.reasonString, rhs.reasonString) && + optional_strings_equal(lhs.serverReference, rhs.serverReference) && + user_properties_equal(lhs.userProperties, rhs.userProperties); +} + +function are_packets_equal(lhs: mqtt5_packet.IPacket, rhs: mqtt5_packet.IPacket) : boolean { + if (lhs.type != rhs.type) { + return false; + } + + switch(lhs.type) { + case mqtt5_packet.PacketType.Pingreq: + case mqtt5_packet.PacketType.Pingresp: + return true; + + case mqtt5_packet.PacketType.Connect: + return are_connect_packets_equal(lhs as model.ConnectPacketInternal, rhs as model.ConnectPacketInternal); + + case mqtt5_packet.PacketType.Connack: + return are_connack_packets_equal(lhs as model.ConnackPacketInternal, rhs as model.ConnackPacketInternal); + + case mqtt5_packet.PacketType.Publish: + return are_publish_internal_packets_equal(lhs as model.PublishPacketInternal, rhs as model.PublishPacketInternal); + + case mqtt5_packet.PacketType.Puback: + return are_puback_packets_equal(lhs as model.PubackPacketInternal, rhs as model.PubackPacketInternal); + + case mqtt5_packet.PacketType.Subscribe: + return are_subscribe_packets_equal(lhs as model.SubscribePacketInternal, rhs as model.SubscribePacketInternal); + + case mqtt5_packet.PacketType.Suback: + return are_suback_packets_equal(lhs as model.SubackPacketInternal, rhs as model.SubackPacketInternal); + + case mqtt5_packet.PacketType.Unsubscribe: + return are_unsubscribe_packets_equal(lhs as model.UnsubscribePacketInternal, rhs as model.UnsubscribePacketInternal); + + case mqtt5_packet.PacketType.Unsuback: + return are_unsuback_packets_equal(lhs as model.UnsubackPacketInternal, rhs as model.UnsubackPacketInternal); + + case mqtt5_packet.PacketType.Disconnect: + return are_disconnect_packets_equal(lhs as model.DisconnectPacketInternal, rhs as model.DisconnectPacketInternal); + + default: + throw new CrtError("Unsupported packet type: " + lhs.type); + } +} + +function append_view(dest: DataView, source: DataView) : DataView { + if (source.byteLength > dest.byteLength) { + throw new CrtError("Buffer overrun"); + } + + for (let i = 0; i < source.byteLength; i++) { + dest.setUint8(i, source.getUint8(i)); + } + + return new DataView(dest.buffer, dest.byteOffset + source.byteLength, dest.byteLength - source.byteLength); +} + +function do_single_round_trip_encode_decode_test(packet: mqtt5_packet.IPacket, mode: model.ProtocolMode, packet_count: number, encode_buffer_size: number, decode_view_size: number) { + let encoder_set = encoder.build_client_encoding_function_set(mode); + apply_debug_encoders_to_encoding_function_set(encoder_set, mode); + + let packet_encoder = new encoder.Encoder(encoder_set); + + let decoder_set = decoder.build_client_decoding_function_set(mode); + apply_debug_decoders_to_decoding_function_set(decoder_set, mode); + + let packet_decoder = new decoder.Decoder(decoder_set); + let binary_packet = model.convert_packet_to_binary(packet); + + let stream_destination = new ArrayBuffer(1024 * 1024); + let stream_view = new DataView(stream_destination); + + let encode_buffer = new ArrayBuffer(encode_buffer_size); + let encode_view = new DataView(encode_buffer); + + for (let i = 0; i < packet_count; i++) { + packet_encoder.init_for_packet(binary_packet); + + let encode_result_state = ServiceResultType.InProgress; + while (encode_result_state != ServiceResultType.Complete) { + let encode_result = packet_encoder.service(encode_view); + encode_result_state = encode_result.type; + if (encode_result_state == ServiceResultType.Complete) { + encode_view = encode_result.nextView; + } else { + encode_view = new DataView(encode_buffer); + } + + if (encode_result.encodedView) { + stream_view = append_view(stream_view, encode_result.encodedView); + } + } + } + + let packets = new Array(); + let encoded_block = new DataView(stream_destination, 0, stream_view.byteOffset); + let current_index = 0; + while (current_index < encoded_block.byteLength) { + let slice_length = Math.min(decode_view_size, encoded_block.byteLength - current_index); + let decode_view = new DataView(encoded_block.buffer, current_index, slice_length); + let decoded_packets = packet_decoder.decode(decode_view); + for (let packet of decoded_packets) { + packets.push(packet); + } + + current_index += decode_view_size; + } + + expect(packets.length).toBe(packet_count); + for (let decoded_packet of packets) { + expect(are_packets_equal(packet, decoded_packet)).toBe(true); + } +} + +function do_fragmented_round_trip_encode_decode_test(packet: mqtt5_packet.IPacket, mode: model.ProtocolMode, packet_count: number) { + let encode_buffer_sizes = [4, 7, 13, 31, 127, 1027]; + let decode_view_sizes = [1, 2, 3, 5, 9, 17]; + + for (let encode_buffer_size of encode_buffer_sizes) { + for (let decode_view_size of decode_view_sizes) { + do_single_round_trip_encode_decode_test(packet, mode, packet_count, encode_buffer_size, decode_view_size); + } + } +} + +test('Pingreq - 311', () => { + let pingreq : model.PingreqPacketInternal = { + type: mqtt5_packet.PacketType.Pingreq + }; + + do_fragmented_round_trip_encode_decode_test(pingreq, model.ProtocolMode.Mqtt311, 20); +}); + +test('Pingreq - 5', () => { + let pingreq : model.PingreqPacketInternal = { + type: mqtt5_packet.PacketType.Pingreq + }; + + do_fragmented_round_trip_encode_decode_test(pingreq, model.ProtocolMode.Mqtt5, 20); +}); + +test('Pingresp - 311', () => { + let pingreq : model.PingrespPacketInternal = { + type: mqtt5_packet.PacketType.Pingreq + }; + + do_fragmented_round_trip_encode_decode_test(pingreq, model.ProtocolMode.Mqtt311, 20); +}); + +test('Pingresp - 5', () => { + let pingreq : model.PingrespPacketInternal = { + type: mqtt5_packet.PacketType.Pingreq + }; + + do_fragmented_round_trip_encode_decode_test(pingreq, model.ProtocolMode.Mqtt5, 20); +}); + +test('Puback - 311', () => { + let puback : model.PubackPacketInternal = { + type: mqtt5_packet.PacketType.Puback, + packetId: 5, + reasonCode: mqtt5_packet.PubackReasonCode.Success + }; + + do_fragmented_round_trip_encode_decode_test(puback, model.ProtocolMode.Mqtt311, 20); +}); + +test('Puback - Minimal 5', () => { + let puback : model.PubackPacketInternal = { + type: mqtt5_packet.PacketType.Puback, + packetId: 5, + reasonCode: mqtt5_packet.PubackReasonCode.Success + }; + + do_fragmented_round_trip_encode_decode_test(puback, model.ProtocolMode.Mqtt5, 20); +}); + +test('Puback - Minimal With Non-Zero ReasonCode 5', () => { + let puback : model.PubackPacketInternal = { + type: mqtt5_packet.PacketType.Puback, + packetId: 5, + reasonCode: mqtt5_packet.PubackReasonCode.NotAuthorized + }; + + do_fragmented_round_trip_encode_decode_test(puback, model.ProtocolMode.Mqtt5, 20); +}); + +function createDummyUserProperties() : Array { + return new Array( + {name: "First", value: "1"}, + {name: "Hello", value: "World"}, + {name: "Pineapple", value: "Sorbet"}, + ); +} + +test('Puback - Maximal 5', () => { + let puback : model.PubackPacketInternal = { + type: mqtt5_packet.PacketType.Puback, + packetId: 37, + reasonCode: mqtt5_packet.PubackReasonCode.UnspecifiedError, + reasonString: "LooksFunny", + userProperties: createDummyUserProperties() + }; + + do_fragmented_round_trip_encode_decode_test(puback, model.ProtocolMode.Mqtt5, 20); +}); + +test('Puback - Maximal Falsy 5', () => { + let puback : model.PubackPacketInternal = { + type: mqtt5_packet.PacketType.Puback, + packetId: 37, + reasonCode: mqtt5_packet.PubackReasonCode.UnspecifiedError, + reasonString: "", + userProperties: createDummyUserProperties() + }; + + do_fragmented_round_trip_encode_decode_test(puback, model.ProtocolMode.Mqtt5, 20); +}); + +test('Publish - Empty Payload 311', () => { + let publish : model.PublishPacketInternal = { + type: mqtt5_packet.PacketType.Publish, + qos: mqtt5_packet.QoS.AtMostOnce, + topicName: "foo/bar", + duplicate: true, + retain: true + }; + + do_fragmented_round_trip_encode_decode_test(publish, model.ProtocolMode.Mqtt311, 20); +}); + +test('Publish - With Payload 311', () => { + let encoder = new TextEncoder(); + let payload = encoder.encode("Something").buffer; + + let publish : model.PublishPacketInternal = { + type: mqtt5_packet.PacketType.Publish, + packetId: 7, + qos: mqtt5_packet.QoS.AtLeastOnce, + topicName: "hello/world", + duplicate: false, + retain: false, + payload: payload + }; + + do_fragmented_round_trip_encode_decode_test(publish, model.ProtocolMode.Mqtt311, 20); +}); + +test('Publish - Minimal Empty Payload 5', () => { + let publish : model.PublishPacketInternal = { + type: mqtt5_packet.PacketType.Publish, + qos: mqtt5_packet.QoS.ExactlyOnce, + packetId: 47, + topicName: "uff/dah", + duplicate: true, + retain: false + }; + + do_fragmented_round_trip_encode_decode_test(publish, model.ProtocolMode.Mqtt5, 20); +}); + +test('Publish - Minimal With Payload 5', () => { + let encoder = new TextEncoder(); + let payload = encoder.encode("Very Important Data").buffer; + + let publish : model.PublishPacketInternal = { + type: mqtt5_packet.PacketType.Publish, + qos: mqtt5_packet.QoS.ExactlyOnce, + packetId: 47, + topicName: "uff/dah/2", + duplicate: true, + retain: false, + payload: payload + }; + + do_fragmented_round_trip_encode_decode_test(publish, model.ProtocolMode.Mqtt5, 20); +}); + +test('Publish - Maximal Empty Payload 5', () => { + let publish : model.PublishPacketInternal = { + type: mqtt5_packet.PacketType.Publish, + qos: mqtt5_packet.QoS.AtMostOnce, + topicName: "uff/dah", + duplicate: true, + retain: false, + payloadFormat: mqtt5_packet.PayloadFormatIndicator.Utf8, + messageExpiryIntervalSeconds: 1020, + topicAlias: 5, + responseTopic: "uff/dah/accepted", + subscriptionIdentifiers: new Array(32, 255, 128 * 128 * 128 - 1, 128 * 128 * 128 + 1), + correlationData: new Uint8Array([1, 2, 3, 4, 5]).buffer, + contentType: "application/json", + userProperties: createDummyUserProperties() + }; + + do_fragmented_round_trip_encode_decode_test(publish, model.ProtocolMode.Mqtt5, 20); +}); + +test('Publish - Maximal Empty Payload Falsy 5', () => { + let publish : model.PublishPacketInternal = { + type: mqtt5_packet.PacketType.Publish, + qos: mqtt5_packet.QoS.AtMostOnce, + topicName: "uff/dah", + duplicate: false, + retain: false, + payloadFormat: mqtt5_packet.PayloadFormatIndicator.Bytes, + messageExpiryIntervalSeconds: 0, + topicAlias: 0, // protocol error, but doesn't matter here + responseTopic: "", + subscriptionIdentifiers: new Array(0, 255, 128 * 128 * 128 - 1, 128 * 128 * 128 + 1), + correlationData: new Uint8Array([]).buffer, + contentType: "", + userProperties: createDummyUserProperties() + }; + + do_fragmented_round_trip_encode_decode_test(publish, model.ProtocolMode.Mqtt5, 20); +}); + +test('Publish - Maximal With Payload 5', () => { + let encoder = new TextEncoder(); + let payload = encoder.encode("Very Important Data").buffer; + + let publish : model.PublishPacketInternal = { + type: mqtt5_packet.PacketType.Publish, + qos: mqtt5_packet.QoS.ExactlyOnce, + packetId: 47, + topicName: "uff/dah/api", + duplicate: false, + retain: true, + payload: payload, + payloadFormat: mqtt5_packet.PayloadFormatIndicator.Bytes, + messageExpiryIntervalSeconds: 53281, + topicAlias: 2, + responseTopic: "uff/dah/rejected", + subscriptionIdentifiers: new Array(1, 128 * 128 * 128 - 1, 128 * 128 * 128 + 1, 255), + correlationData: new Uint8Array([5, 4, 3, 2, 1]).buffer, + contentType: "application/xml", + userProperties: createDummyUserProperties() + }; + + do_fragmented_round_trip_encode_decode_test(publish, model.ProtocolMode.Mqtt5, 20); +}); + +test('Publish - Maximal With Payload Falsy 5', () => { + let payload = new Uint8Array([0]).buffer; + + let publish : model.PublishPacketInternal = { + type: mqtt5_packet.PacketType.Publish, + qos: mqtt5_packet.QoS.ExactlyOnce, + packetId: 47, + topicName: "", + duplicate: false, + retain: true, + payload: payload, + payloadFormat: mqtt5_packet.PayloadFormatIndicator.Bytes, + messageExpiryIntervalSeconds: 0, + topicAlias: 2, + responseTopic: "", + subscriptionIdentifiers: new Array(), + correlationData: new Uint8Array([]).buffer, + contentType: "", + userProperties: new Array() + }; + + do_fragmented_round_trip_encode_decode_test(publish, model.ProtocolMode.Mqtt5, 20); +}); + +test('Disconnect - 311', () => { + let disconnect : model.DisconnectPacketInternal = { + type: mqtt5_packet.PacketType.Disconnect, + reasonCode: mqtt5_packet.DisconnectReasonCode.NormalDisconnection, + }; + + do_fragmented_round_trip_encode_decode_test(disconnect, model.ProtocolMode.Mqtt311, 20); +}); + +test('Disconnect - Minimal zero reason code 5', () => { + let disconnect : model.DisconnectPacketInternal = { + type: mqtt5_packet.PacketType.Disconnect, + reasonCode: mqtt5_packet.DisconnectReasonCode.NormalDisconnection, + }; + + do_fragmented_round_trip_encode_decode_test(disconnect, model.ProtocolMode.Mqtt5, 20); +}); + +test('Disconnect - Minimal non-zero reason code 5', () => { + let disconnect : model.DisconnectPacketInternal = { + type: mqtt5_packet.PacketType.Disconnect, + reasonCode: mqtt5_packet.DisconnectReasonCode.KeepAliveTimeout, + }; + + do_fragmented_round_trip_encode_decode_test(disconnect, model.ProtocolMode.Mqtt5, 20); +}); + +test('Disconnect - Maximal 5', () => { + let disconnect : model.DisconnectPacketInternal = { + type: mqtt5_packet.PacketType.Disconnect, + reasonCode: mqtt5_packet.DisconnectReasonCode.NormalDisconnection, + reasonString: "Looks funny", + serverReference: "Somewhere else", + sessionExpiryIntervalSeconds: 255, + userProperties: createDummyUserProperties() + }; + + do_fragmented_round_trip_encode_decode_test(disconnect, model.ProtocolMode.Mqtt5, 20); +}); + +test('Disconnect - Maximal Falsy 5', () => { + let disconnect : model.DisconnectPacketInternal = { + type: mqtt5_packet.PacketType.Disconnect, + reasonCode: mqtt5_packet.DisconnectReasonCode.DisconnectWithWillMessage, + reasonString: "", + serverReference: "", + sessionExpiryIntervalSeconds: 0, + userProperties: createDummyUserProperties() + }; + + do_fragmented_round_trip_encode_decode_test(disconnect, model.ProtocolMode.Mqtt5, 20); +}); + +test('Subscribe - 311', () => { + let subscribe : model.SubscribePacketInternal = { + type: mqtt5_packet.PacketType.Subscribe, + packetId: 12, + subscriptions: new Array( + {topicFilter: "three", qos: mqtt5_packet.QoS.AtLeastOnce}, + {topicFilter: "fortysix/and/two", qos: mqtt5_packet.QoS.AtMostOnce}, + {topicFilter: "five", qos: mqtt5_packet.QoS.ExactlyOnce}, + ) + }; + + do_fragmented_round_trip_encode_decode_test(subscribe, model.ProtocolMode.Mqtt311, 20); +}); + +test('Subscribe - Minimal 5', () => { + let subscribe : model.SubscribePacketInternal = { + type: mqtt5_packet.PacketType.Subscribe, + packetId: 42, + subscriptions: new Array( + {topicFilter: "up", qos: mqtt5_packet.QoS.AtLeastOnce}, + {topicFilter: "fortysix/and/two", qos: mqtt5_packet.QoS.AtMostOnce}, + {topicFilter: "down", qos: mqtt5_packet.QoS.ExactlyOnce}, + ) + }; + + do_fragmented_round_trip_encode_decode_test(subscribe, model.ProtocolMode.Mqtt5, 20); +}); + +test('Subscribe - Maximal 5', () => { + let subscribe : model.SubscribePacketInternal = { + type: mqtt5_packet.PacketType.Subscribe, + packetId: 42, + subscriptions: new Array( + {topicFilter: "up", qos: mqtt5_packet.QoS.AtLeastOnce, noLocal: true, retainAsPublished : true, retainHandlingType: mqtt5_packet.RetainHandlingType.SendOnSubscribe}, + {topicFilter: "fortysix/and/two", qos: mqtt5_packet.QoS.AtMostOnce, noLocal: false, retainAsPublished : false, retainHandlingType: mqtt5_packet.RetainHandlingType.SendOnSubscribeIfNew}, + {topicFilter: "down", qos: mqtt5_packet.QoS.ExactlyOnce, noLocal: true, retainAsPublished : false, retainHandlingType: mqtt5_packet.RetainHandlingType.DontSend}, + ), + userProperties: createDummyUserProperties(), + subscriptionIdentifier: 47, + }; + + do_fragmented_round_trip_encode_decode_test(subscribe, model.ProtocolMode.Mqtt5, 20); +}); + +test('Subscribe - Maximal Falsy 5', () => { + let subscribe : model.SubscribePacketInternal = { + type: mqtt5_packet.PacketType.Subscribe, + packetId: 0, + subscriptions: new Array( + {topicFilter: "", qos: mqtt5_packet.QoS.AtLeastOnce, noLocal: true, retainAsPublished : true, retainHandlingType: mqtt5_packet.RetainHandlingType.SendOnSubscribe}, + {topicFilter: "fortysix/and/two", qos: mqtt5_packet.QoS.AtMostOnce, noLocal: false, retainAsPublished : false, retainHandlingType: mqtt5_packet.RetainHandlingType.SendOnSubscribeIfNew}, + {topicFilter: "down", qos: mqtt5_packet.QoS.ExactlyOnce, noLocal: true, retainAsPublished : false, retainHandlingType: mqtt5_packet.RetainHandlingType.DontSend}, + ), + userProperties: createDummyUserProperties(), + subscriptionIdentifier: 0, + }; + + do_fragmented_round_trip_encode_decode_test(subscribe, model.ProtocolMode.Mqtt5, 20); +}); + +test('Suback - 311', () => { + let suback : model.SubackPacketInternal = { + type: mqtt5_packet.PacketType.Suback, + packetId: 12, + reasonCodes: new Array( + mqtt5_packet.SubackReasonCode.GrantedQoS1, + mqtt5_packet.SubackReasonCode.GrantedQoS0, + mqtt5_packet.SubackReasonCode.GrantedQoS2, + 128 + ) + }; + + do_fragmented_round_trip_encode_decode_test(suback, model.ProtocolMode.Mqtt311, 20); +}); + +test('Suback - Minimal 5', () => { + let suback : model.SubackPacketInternal = { + type: mqtt5_packet.PacketType.Suback, + packetId: 53280, + reasonCodes: new Array( + mqtt5_packet.SubackReasonCode.GrantedQoS1, + mqtt5_packet.SubackReasonCode.GrantedQoS0, + mqtt5_packet.SubackReasonCode.NotAuthorized, + mqtt5_packet.SubackReasonCode.TopicFilterInvalid, + ) + }; + + do_fragmented_round_trip_encode_decode_test(suback, model.ProtocolMode.Mqtt5, 20); +}); + +test('Suback - Maximal 5', () => { + let suback : model.SubackPacketInternal = { + type: mqtt5_packet.PacketType.Suback, + packetId: 53280, + reasonCodes: new Array( + mqtt5_packet.SubackReasonCode.GrantedQoS1, + mqtt5_packet.SubackReasonCode.GrantedQoS0, + mqtt5_packet.SubackReasonCode.NotAuthorized, + mqtt5_packet.SubackReasonCode.TopicFilterInvalid, + ), + reasonString: "Not well", + userProperties: createDummyUserProperties(), + }; + + do_fragmented_round_trip_encode_decode_test(suback, model.ProtocolMode.Mqtt5, 20); +}); + +test('Suback - Maximal Falsy 5', () => { + let suback : model.SubackPacketInternal = { + type: mqtt5_packet.PacketType.Suback, + packetId: 0, + reasonCodes: new Array( + mqtt5_packet.SubackReasonCode.GrantedQoS1, + mqtt5_packet.SubackReasonCode.GrantedQoS0, + mqtt5_packet.SubackReasonCode.NotAuthorized, + mqtt5_packet.SubackReasonCode.TopicFilterInvalid, + ), + reasonString: "", + userProperties: new Array(), + }; + + do_fragmented_round_trip_encode_decode_test(suback, model.ProtocolMode.Mqtt5, 20); +}); + +test('Unsubscribe - 311', () => { + let unsubscribe : model.UnsubscribePacketInternal = { + type: mqtt5_packet.PacketType.Unsubscribe, + packetId: 12, + topicFilters: new Array("three", "fortysix/and/two", "squarepants") + }; + + do_fragmented_round_trip_encode_decode_test(unsubscribe, model.ProtocolMode.Mqtt311, 20); +}); + +test('Unsubscribe - Minimal 5', () => { + let unsubscribe : model.UnsubscribePacketInternal = { + type: mqtt5_packet.PacketType.Unsubscribe, + packetId: 12, + topicFilters: new Array("three", "fortysix/and/two", "squidward") + }; + + do_fragmented_round_trip_encode_decode_test(unsubscribe, model.ProtocolMode.Mqtt5, 20); +}); + +test('Unsubscribe - Maximal 5', () => { + let unsubscribe : model.UnsubscribePacketInternal = { + type: mqtt5_packet.PacketType.Unsubscribe, + packetId: 12, + topicFilters: new Array("three", "fortysix/and/two", "five"), + userProperties: createDummyUserProperties() + }; + + do_fragmented_round_trip_encode_decode_test(unsubscribe, model.ProtocolMode.Mqtt5, 20); +}); + +test('Unsubscribe - Falsy 5', () => { + let unsubscribe : model.UnsubscribePacketInternal = { + type: mqtt5_packet.PacketType.Unsubscribe, + packetId: 0, + topicFilters: new Array("three", "fortysix/and/two", "patrickstar"), + userProperties: new Array() + }; + + do_fragmented_round_trip_encode_decode_test(unsubscribe, model.ProtocolMode.Mqtt5, 20); +}); + +test('Unsuback - 311', () => { + let unsuback : model.UnsubackPacketInternal = { + type: mqtt5_packet.PacketType.Unsuback, + packetId: 12, + reasonCodes: new Array() + }; + + do_fragmented_round_trip_encode_decode_test(unsuback, model.ProtocolMode.Mqtt311, 20); +}); + +test('Unsuback - Minimal 5', () => { + let unsuback : model.UnsubackPacketInternal = { + type: mqtt5_packet.PacketType.Unsuback, + packetId: 12, + reasonCodes: new Array( + mqtt5_packet.UnsubackReasonCode.Success, + mqtt5_packet.UnsubackReasonCode.NoSubscriptionExisted, + mqtt5_packet.UnsubackReasonCode.NotAuthorized, + mqtt5_packet.UnsubackReasonCode.TopicFilterInvalid, + ) + }; + + do_fragmented_round_trip_encode_decode_test(unsuback, model.ProtocolMode.Mqtt5, 20); +}); + +test('Unsuback - Maximal 5', () => { + let unsuback : model.UnsubackPacketInternal = { + type: mqtt5_packet.PacketType.Unsuback, + packetId: 12, + reasonCodes: new Array( + mqtt5_packet.UnsubackReasonCode.Success, + mqtt5_packet.UnsubackReasonCode.NoSubscriptionExisted, + mqtt5_packet.UnsubackReasonCode.NotAuthorized, + mqtt5_packet.UnsubackReasonCode.TopicFilterInvalid, + ), + reasonString: "Ihavenoidea", + userProperties: createDummyUserProperties() + }; + + do_fragmented_round_trip_encode_decode_test(unsuback, model.ProtocolMode.Mqtt5, 20); +}); + +test('Unsuback - Maximal Falsy 5', () => { + let unsuback : model.UnsubackPacketInternal = { + type: mqtt5_packet.PacketType.Unsuback, + packetId: 0, + reasonCodes: new Array( + mqtt5_packet.UnsubackReasonCode.Success, + ), + reasonString: "", + userProperties: new Array() + }; + + do_fragmented_round_trip_encode_decode_test(unsuback, model.ProtocolMode.Mqtt5, 20); +}); + +test('Connect - Minimal 311', () => { + let connect : model.ConnectPacketInternal = { + type: mqtt5_packet.PacketType.Connect, + cleanStart: true, + keepAliveIntervalSeconds: 1200 + }; + + do_fragmented_round_trip_encode_decode_test(connect, model.ProtocolMode.Mqtt311, 20); +}); + +test('Connect - Maximal 311', () => { + let connect : model.ConnectPacketInternal = { + type: mqtt5_packet.PacketType.Connect, + cleanStart: true, + keepAliveIntervalSeconds: 1200, + clientId: "Spongebob", + username: "KrabbyPatty", + password: new Uint8Array([0, 1, 2, 3, 4]).buffer, + will: { + type: mqtt5_packet.PacketType.Publish, + topicName: "Bikini/Bottom", + payload: new Uint8Array([5, 6, 7, 8, 9]).buffer, + qos: mqtt5_packet.QoS.AtLeastOnce, + retain: true + }, + }; + + do_fragmented_round_trip_encode_decode_test(connect, model.ProtocolMode.Mqtt311, 20); +}); + +test('Connect - Maximal Falsy 311', () => { + let connect : model.ConnectPacketInternal = { + type: mqtt5_packet.PacketType.Connect, + cleanStart: false, + keepAliveIntervalSeconds: 0, + clientId: "", + username: "", + password: new Uint8Array([]).buffer, + will: { + type: mqtt5_packet.PacketType.Publish, + topicName: "", + payload: new Uint8Array([]).buffer, + qos: mqtt5_packet.QoS.AtMostOnce, + retain: false + }, + }; + + do_fragmented_round_trip_encode_decode_test(connect, model.ProtocolMode.Mqtt311, 20); +}); + +test('Connect - Minimal 5', () => { + let connect : model.ConnectPacketInternal = { + type: mqtt5_packet.PacketType.Connect, + cleanStart: true, + keepAliveIntervalSeconds: 1200 + }; + + do_fragmented_round_trip_encode_decode_test(connect, model.ProtocolMode.Mqtt5, 20); +}); + +test('Connect - Maximal 5', () => { + let connect : model.ConnectPacketInternal = { + type: mqtt5_packet.PacketType.Connect, + cleanStart: true, + keepAliveIntervalSeconds: 1200, + clientId: "Spongebob", + username: "KrabbyPatty", + password: new Uint8Array([0, 1, 2, 3, 4]).buffer, + topicAliasMaximum: 20, + authenticationMethod: "Secrethandshake", + authenticationData: new Uint8Array([40, 41, 42, 43, 44]).buffer, + willDelayIntervalSeconds: 30, + sessionExpiryIntervalSeconds: 600, + requestResponseInformation: true, + requestProblemInformation: false, + receiveMaximum: 100, + maximumPacketSizeBytes: 128 * 1024, + userProperties: createDummyUserProperties(), + will: { + type: mqtt5_packet.PacketType.Publish, + topicName: "Bikini/Bottom", + payload: new Uint8Array([5, 6, 7, 8, 9]).buffer, + qos: mqtt5_packet.QoS.AtLeastOnce, + retain: true, + payloadFormat: mqtt5_packet.PayloadFormatIndicator.Utf8, + messageExpiryIntervalSeconds: 3600, + contentType: "application/json", + responseTopic: "Krusty/Krab", + correlationData: new Uint8Array([65, 66, 68]).buffer, + userProperties: createDummyUserProperties(), + }, + }; + + do_fragmented_round_trip_encode_decode_test(connect, model.ProtocolMode.Mqtt5, 20); +}); + +test('Connect - Maximal Falsy 5', () => { + let connect : model.ConnectPacketInternal = { + type: mqtt5_packet.PacketType.Connect, + cleanStart: true, + keepAliveIntervalSeconds: 0, + clientId: "", + username: "", + password: new Uint8Array([]).buffer, + topicAliasMaximum: 0, + authenticationMethod: "", + authenticationData: new Uint8Array([]).buffer, + willDelayIntervalSeconds: 0, + sessionExpiryIntervalSeconds: 0, + requestResponseInformation: false, + requestProblemInformation: false, + receiveMaximum: 0, + maximumPacketSizeBytes: 0, + userProperties: new Array(), + will: { + type: mqtt5_packet.PacketType.Publish, + topicName: "", + qos: mqtt5_packet.QoS.AtMostOnce, + retain: false, + payloadFormat: mqtt5_packet.PayloadFormatIndicator.Bytes, + messageExpiryIntervalSeconds: 0, + contentType: "", + responseTopic: "", + correlationData: new Uint8Array([]).buffer, + userProperties: new Array(), + }, + }; + + do_fragmented_round_trip_encode_decode_test(connect, model.ProtocolMode.Mqtt5, 20); +}); + +test('Connack - 311', () => { + let connack : model.ConnackPacketInternal = { + type: mqtt5_packet.PacketType.Connack, + reasonCode: mqtt5_packet.ConnectReasonCode.Success, + sessionPresent: true + }; + + do_fragmented_round_trip_encode_decode_test(connack, model.ProtocolMode.Mqtt311, 20); +}); + +test('Connack - Minimal 5', () => { + let connack : model.ConnackPacketInternal = { + type: mqtt5_packet.PacketType.Connack, + reasonCode: mqtt5_packet.ConnectReasonCode.NotAuthorized, + sessionPresent: false + }; + + do_fragmented_round_trip_encode_decode_test(connack, model.ProtocolMode.Mqtt5, 20); +}); + +test('Connack - Maximal 5', () => { + let connack : model.ConnackPacketInternal = { + type: mqtt5_packet.PacketType.Connack, + reasonCode: mqtt5_packet.ConnectReasonCode.Success, + sessionPresent: true, + authenticationMethod: "Piglatin", + authenticationData: new Uint8Array([40, 41, 42, 43, 44]).buffer, + sessionExpiryInterval: 3600, + receiveMaximum: 100, + maximumQos: 1, + retainAvailable: true, + maximumPacketSize: 128 * 1024, + assignedClientIdentifier: "SpongebobSquarepants", + topicAliasMaximum: 20, + reasonString: "Nice", + wildcardSubscriptionsAvailable: true, + subscriptionIdentifiersAvailable: true, + sharedSubscriptionsAvailable: true, + serverKeepAlive: 1200, + responseInformation: "this/topic", + serverReference: "Guam.com", + userProperties: createDummyUserProperties() + }; + + do_fragmented_round_trip_encode_decode_test(connack, model.ProtocolMode.Mqtt5, 20); +}); + +test('Connack - Maximal Falsy 5', () => { + let connack : model.ConnackPacketInternal = { + type: mqtt5_packet.PacketType.Connack, + reasonCode: mqtt5_packet.ConnectReasonCode.Success, + sessionPresent: false, + authenticationMethod: "", + authenticationData: new Uint8Array([]).buffer, + sessionExpiryInterval: 0, + receiveMaximum: 0, + maximumQos: 0, + retainAvailable: false, + maximumPacketSize: 0, + assignedClientIdentifier: "", + topicAliasMaximum: 0, + reasonString: "", + wildcardSubscriptionsAvailable: false, + subscriptionIdentifiersAvailable: false, + sharedSubscriptionsAvailable: false, + serverKeepAlive: 0, + responseInformation: "", + serverReference: "", + userProperties: new Array() + }; + + do_fragmented_round_trip_encode_decode_test(connack, model.ProtocolMode.Mqtt5, 20); +}); diff --git a/lib/browser/mqtt_internal/encoder.ts b/lib/browser/mqtt_internal/encoder.ts new file mode 100644 index 000000000..d11512c05 --- /dev/null +++ b/lib/browser/mqtt_internal/encoder.ts @@ -0,0 +1,981 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +import {CrtError} from "../error"; +import * as model from "./model"; +import * as mqtt5_packet from '../../common/mqtt5_packet'; +import * as vli from "./vli"; + +/** + * The encoder works similarly to the native MQTT5 client in aws-c-mqtt: + * + * Encoding is a two-step process. + * + * The first step takes the finalized packet and pushes one or more "steps" + * onto a queue. Each step represents the encoding of a single primitive integer or range of bytes. + * + * The second step involves iterating the encoding steps and performing them on a mutable buffer that represents + * a range of bytes to write to the socket. + * + * If the buffer fills up, encoding is halted (byte ranges are clipped in place) and the buffer is considered + * ready to send to the socket. The client only has one buffer in-flight at once (the write completion callback + * must be invoked in order to continue encoding). + * + * There isn't a pressing need to do it this way other than familiarity (the minimal allocation and hot-buffer + * properties probably aren't particular impactful in JS). + */ + +// Encoding step model and helpers +export enum EncodingStepType { + U8, + U16, + U32, + VLI, + BYTES +} + +export interface EncodingStep { + type: EncodingStepType, + value: number | DataView +} + +export function encode_length_prefixed_array_buffer(steps: Array, source: ArrayBuffer | undefined) { + if (source) { + steps.push({ type: EncodingStepType.U16, value: source.byteLength }); + steps.push({ type: EncodingStepType.BYTES, value: new DataView(source) }); + } else { + steps.push({ type: EncodingStepType.U16, value: 0 }); + } +} + +export function encode_optional_length_prefixed_array_buffer(steps: Array, source: ArrayBuffer | undefined) { + if (source) { + steps.push({ type: EncodingStepType.U16, value: source.byteLength }); + steps.push({ type: EncodingStepType.BYTES, value: new DataView(source) }); + } +} + +export function encode_required_length_prefixed_array_buffer(steps: Array, source: ArrayBuffer) { + steps.push({ type: EncodingStepType.U16, value: source.byteLength }); + steps.push({ type: EncodingStepType.BYTES, value: new DataView(source) }); +} + +// MQTT 311 packet encoders + +function get_connect_packet_remaining_lengths311(packet: model.ConnectPacketBinary) : number { + let size: number = 12; // 0x00, 0x04, "MQTT", 0x04, Flags byte, Keep Alive u16, Client Id Length u16 + + if (packet.clientId) { + size += packet.clientId.byteLength; + } + + if (packet.will) { + size += 2 + packet.will.topicName.byteLength; + size += 2; // payload length which is 16 bit and not a VLI + if (packet.will.payload) { + size += packet.will.payload.byteLength; + } + } + + if (packet.username) { + size += 2 + packet.username.byteLength; + } + + if (packet.password) { + size += 2 + packet.password.byteLength; + } + + return size; +} + +function compute_connect_flags(packet: model.ConnectPacketBinary) : number { + let flags: number = 0; + + if (packet.username) { + flags |= model.CONNECT_FLAGS_HAS_USERNAME; + } + + if (packet.password) { + flags |= model.CONNECT_FLAGS_HAS_PASSWORD; + } + + if (packet.will) { + flags |= model.CONNECT_FLAGS_HAS_WILL; + flags |= ((packet.will.qos & model.QOS_MASK) << model.CONNECT_FLAGS_QOS_SHIFT); + + if (packet.will.retain) { + flags |= model.CONNECT_FLAGS_WILL_RETAIN; + } + } + + if (packet.cleanSession) { + flags |= model.CONNECT_FLAGS_CLEAN_SESSION; + } + + return flags; +} + +function encode_connect_packet311(steps: Array, packet: model.ConnectPacketBinary) { + steps.push({ type: EncodingStepType.U8, value: model.PACKET_TYPE_FIRST_BYTE_CONNECT }); + steps.push({ type: EncodingStepType.VLI, value: get_connect_packet_remaining_lengths311(packet) }); + steps.push({ type: EncodingStepType.BYTES, value: model.CONNECT_311_PROTOCOL_DATAVIEW }); + steps.push({ type: EncodingStepType.U8, value: compute_connect_flags(packet) }); + steps.push({ type: EncodingStepType.U16, value: packet.keepAliveIntervalSeconds }); + encode_length_prefixed_array_buffer(steps, packet.clientId); + + if (packet.will) { + encode_length_prefixed_array_buffer(steps, packet.will.topicName); + encode_length_prefixed_array_buffer(steps, packet.will.payload); + } + + encode_optional_length_prefixed_array_buffer(steps, packet.username); + encode_optional_length_prefixed_array_buffer(steps, packet.password); +} + +function compute_publish_flags(packet: model.PublishPacketBinary) { + let flags: number = 0; + + flags |= ((packet.qos & model.QOS_MASK) << model.PUBLISH_FLAGS_QOS_SHIFT); + + if (packet.retain) { + flags |= model.PUBLISH_FLAGS_RETAIN; + } + + if (packet.duplicate) { + flags |= model.PUBLISH_FLAGS_DUPLICATE; + } + + return flags; +} + +function get_publish_packet_remaining_lengths311(packet: model.PublishPacketBinary) : number { + let size: number = 2 + packet.topicName.byteLength; + + if (packet.qos > 0) { + size += 2; // packet id + } + + if (packet.payload) { + size += packet.payload.byteLength; + } + + return size; +} + +function encode_publish_packet311(steps: Array, packet: model.PublishPacketBinary) { + let flags = compute_publish_flags(packet); + + steps.push({ type: EncodingStepType.U8, value: flags | model.PACKET_TYPE_FIRST_BYTE_PUBLISH }); + steps.push({ type: EncodingStepType.VLI, value: get_publish_packet_remaining_lengths311(packet) }); + encode_length_prefixed_array_buffer(steps, packet.topicName); + + if (packet.qos > 0) { + if (packet.packetId) { + steps.push({type: EncodingStepType.U16, value: packet.packetId}); + } else { + throw new CrtError("Publish(311) packet with non-zero qos and invalid or missing packet id"); + } + } + + if (packet.payload && packet.payload.byteLength > 0) { + steps.push({ type: EncodingStepType.BYTES, value: new DataView(packet.payload) }); + } +} + +function encode_puback_packet311(steps: Array, packet: model.PubackPacketBinary) { + steps.push({ type: EncodingStepType.U8, value: model.PACKET_TYPE_FIRST_BYTE_PUBACK }); + steps.push({ type: EncodingStepType.U8, value: 0x02 }); + steps.push({ type: EncodingStepType.U16, value: packet.packetId }); +} + +function get_subscribe_packet_remaining_lengths311(packet: model.SubscribePacketBinary) : number { + let size: number = 2 + packet.subscriptions.length * 3; // 3 == 2 bytes of topic length + 1 byte of qos + + for (let subscription of packet.subscriptions) { + size += subscription.topicFilter.byteLength; + } + + return size; +} + +function encode_subscription311(steps: Array, subscription: model.SubscriptionBinary) { + encode_required_length_prefixed_array_buffer(steps, subscription.topicFilter); + steps.push({ type: EncodingStepType.U8, value: subscription.qos }); +} + +function encode_subscribe_packet311(steps: Array, packet: model.SubscribePacketBinary) { + steps.push({ type: EncodingStepType.U8, value: model.PACKET_TYPE_FIRST_BYTE_SUBSCRIBE }); + steps.push({ type: EncodingStepType.VLI, value: get_subscribe_packet_remaining_lengths311(packet) }); + steps.push({ type: EncodingStepType.U16, value: packet.packetId }); + + for (let subscription of packet.subscriptions) { + encode_subscription311(steps, subscription); + } +} + +function get_unsubscribe_packet_remaining_lengths311(packet: model.UnsubscribePacketBinary) : number { + let size: number = 2 + packet.topicFilters.length * 2; + + for (let topicFilter of packet.topicFilters) { + size += topicFilter.byteLength; + } + + return size; +} + +function encode_unsubscribe_packet311(steps: Array, packet: model.UnsubscribePacketBinary) { + steps.push({ type: EncodingStepType.U8, value: model.PACKET_TYPE_FIRST_BYTE_UNSUBSCRIBE }); + steps.push({ type: EncodingStepType.VLI, value: get_unsubscribe_packet_remaining_lengths311(packet) }); + steps.push({ type: EncodingStepType.U16, value: packet.packetId }); + + for (let topicFilter of packet.topicFilters) { + encode_required_length_prefixed_array_buffer(steps, topicFilter); + } +} + +function encode_pingreq_packet(steps: Array) { + steps.push({ type: EncodingStepType.U16, value: model.PACKET_TYPE_PINGREQ_FULL_ENCODING }); +} + +function encode_disconnect_packet311(steps: Array, packet: model.DisconnectPacketBinary) { + steps.push({ type: EncodingStepType.U16, value: model.PACKET_TYPE_DISCONNECT_FULL_ENCODING_311 }); +} + +// MQTT 5 packet encoders + +export function compute_user_properties_length(user_properties: Array | undefined) : number { + if (!user_properties) { + return 0; + } + + let length : number = 0; + for (let property of user_properties) { + // 5 = 1 for property code + 2 for name length + 2 for value length + length += 5 + property.name.byteLength + property.value.byteLength; + } + + return length; +} + +export function encode_user_properties(steps: Array, user_properties: Array | undefined) { + if (!user_properties) { + return; + } + + for (let user_property of user_properties) { + steps.push({ type: EncodingStepType.U8, value: model.USER_PROPERTY_PROPERTY_CODE }); + encode_required_length_prefixed_array_buffer(steps, user_property.name); + encode_required_length_prefixed_array_buffer(steps, user_property.value); + } +} + +function compute_will_properties_length(packet: model.ConnectPacketBinary) : number { + if (!packet.will) { + return 0; + } + + let length : number = compute_user_properties_length(packet.will.userProperties); + + if (packet.willDelayIntervalSeconds != undefined) { + length += 5; + } + + if (packet.will.payloadFormat != undefined) { + length += 2; + } + + if (packet.will.messageExpiryIntervalSeconds != undefined) { + length += 5; + } + + if (packet.will.contentType != undefined) { + length += 3 + packet.will.contentType.byteLength; + } + + if (packet.will.responseTopic != undefined) { + length += 3 + packet.will.responseTopic.byteLength; + } + + if (packet.will.correlationData != undefined) { + length += 3 + packet.will.correlationData.byteLength; + } + + return length; +} + +function compute_connect_properties_length(packet: model.ConnectPacketBinary) : number { + let length : number = compute_user_properties_length(packet.userProperties); + + if (packet.sessionExpiryIntervalSeconds != undefined) { + length += 5; + } + + if (packet.receiveMaximum != undefined) { + length += 3; + } + + if (packet.maximumPacketSizeBytes != undefined) { + length += 5; + } + + if (packet.topicAliasMaximum != undefined) { + length += 3; + } + + if (packet.requestResponseInformation != undefined) { + length += 2; + } + + if (packet.requestProblemInformation != undefined) { + length += 2; + } + + if (packet.authenticationMethod != undefined) { + length += 3 + packet.authenticationMethod.byteLength; + } + + if (packet.authenticationData != undefined) { + length += 3 + packet.authenticationData.byteLength; + } + + return length; +} + +function get_connect_packet_remaining_lengths5(packet: model.ConnectPacketBinary) : [number, number, number] { + let remaining_length: number = 12; // 0x00, 0x04, "MQTT", 0x05, Flags byte, Keep Alive u16, Client Id Length u16 + let properties_length: number = compute_connect_properties_length(packet); + let will_properties_length: number = compute_will_properties_length(packet); + + remaining_length += vli.get_vli_byte_length(properties_length) + properties_length; + + if (packet.clientId) { + remaining_length += packet.clientId.byteLength; + } + + if (packet.will) { + remaining_length += vli.get_vli_byte_length(will_properties_length) + will_properties_length + remaining_length += 2 + packet.will.topicName.byteLength; + remaining_length += 2; // payload length + if (packet.will.payload) { + remaining_length += packet.will.payload.byteLength; + } + } + + if (packet.username) { + remaining_length += 2 + packet.username.byteLength; + } + + if (packet.password) { + remaining_length += 2 + packet.password.byteLength; + } + + return [remaining_length, properties_length, will_properties_length]; +} + +function encode_connect_properties(steps: Array, packet: model.ConnectPacketBinary) { + if (packet.sessionExpiryIntervalSeconds != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.SESSION_EXPIRY_INTERVAL_PROPERTY_CODE }); + steps.push({ type: EncodingStepType.U32, value: packet.sessionExpiryIntervalSeconds }); + } + + if (packet.receiveMaximum != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.RECEIVE_MAXIMUM_PROPERTY_CODE }); + steps.push({ type: EncodingStepType.U16, value: packet.receiveMaximum }); + } + + if (packet.maximumPacketSizeBytes != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.MAXIMUM_PACKET_SIZE_PROPERTY_CODE }); + steps.push({ type: EncodingStepType.U32, value: packet.maximumPacketSizeBytes }); + } + + if (packet.topicAliasMaximum != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.TOPIC_ALIAS_MAXIMUM_PROPERTY_CODE }); + steps.push({ type: EncodingStepType.U16, value: packet.topicAliasMaximum }); + } + + if (packet.requestResponseInformation != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.REQUEST_RESPONSE_INFORMATION_PROPERTY_CODE }); + steps.push({ type: EncodingStepType.U8, value: packet.requestResponseInformation }); + } + + if (packet.requestProblemInformation != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.REQUEST_PROBLEM_INFORMATION_PROPERTY_CODE }); + steps.push({ type: EncodingStepType.U8, value: packet.requestProblemInformation }); + } + + if (packet.authenticationMethod != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.AUTHENTICATION_METHOD_PROPERTY_CODE }); + encode_required_length_prefixed_array_buffer(steps, packet.authenticationMethod); + } + + if (packet.authenticationData != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.AUTHENTICATION_DATA_PROPERTY_CODE }); + encode_required_length_prefixed_array_buffer(steps, packet.authenticationData); + } + + encode_user_properties(steps, packet.userProperties); +} + +function encode_will_properties(steps: Array, packet: model.ConnectPacketBinary) { + if (!packet.will) { + return; + } + + if (packet.willDelayIntervalSeconds != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.WILL_DELAY_INTERVAL_PROPERTY_CODE }); + steps.push({ type: EncodingStepType.U32, value: packet.willDelayIntervalSeconds }); + } + + if (packet.will.payloadFormat != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.PAYLOAD_FORMAT_INDICATOR_PROPERTY_CODE }); + steps.push({ type: EncodingStepType.U8, value: packet.will.payloadFormat }); + } + + if (packet.will.messageExpiryIntervalSeconds != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.MESSAGE_EXPIRY_INTERVAL_PROPERTY_CODE }); + steps.push({ type: EncodingStepType.U32, value: packet.will.messageExpiryIntervalSeconds }); + } + + if (packet.will.contentType != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.CONTENT_TYPE_PROPERTY_CODE }); + encode_required_length_prefixed_array_buffer(steps, packet.will.contentType); + } + + if (packet.will.responseTopic != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.RESPONSE_TOPIC_PROPERTY_CODE }); + encode_required_length_prefixed_array_buffer(steps, packet.will.responseTopic); + } + + if (packet.will.correlationData != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.CORRELATION_DATA_PROPERTY_CODE }); + encode_required_length_prefixed_array_buffer(steps, packet.will.correlationData); + } + + encode_user_properties(steps, packet.will.userProperties); +} + +function encode_connect_packet5(steps: Array, packet: model.ConnectPacketBinary) { + let [remaining_length, properties_length, will_properties_length] = get_connect_packet_remaining_lengths5(packet); + + steps.push({ type: EncodingStepType.U8, value: model.PACKET_TYPE_FIRST_BYTE_CONNECT }); + steps.push({ type: EncodingStepType.VLI, value: remaining_length }); + steps.push({ type: EncodingStepType.BYTES, value: model.CONNECT_5_PROTOCOL_DATAVIEW }); + steps.push({ type: EncodingStepType.U8, value: compute_connect_flags(packet) }); + steps.push({ type: EncodingStepType.U16, value: packet.keepAliveIntervalSeconds }); + + steps.push({ type: EncodingStepType.VLI, value: properties_length }); + encode_connect_properties(steps, packet); + + encode_length_prefixed_array_buffer(steps, packet.clientId); + + if (packet.will) { + steps.push({type: EncodingStepType.VLI, value: will_properties_length}); + encode_will_properties(steps, packet); + + encode_required_length_prefixed_array_buffer(steps, packet.will.topicName); + encode_length_prefixed_array_buffer(steps, packet.will.payload); + } + + encode_optional_length_prefixed_array_buffer(steps, packet.username); + encode_optional_length_prefixed_array_buffer(steps, packet.password); +} + +function get_publish_packet_remaining_lengths5(packet: model.PublishPacketBinary) : [number, number] { + let remaining_length: number = 2 + packet.topicName.byteLength; + if (packet.qos != 0) { + remaining_length += 2; + } + + let properties_length: number = 0; + + if (packet.payloadFormat != undefined) { + properties_length += 2; + } + + if (packet.messageExpiryIntervalSeconds != undefined) { + properties_length += 5; + } + + if (packet.topicAlias != undefined) { + properties_length += 3; + } + + if (packet.responseTopic != undefined) { + properties_length += 3 + packet.responseTopic.byteLength; + } + + if (packet.correlationData != undefined) { + properties_length += 3 + packet.correlationData.byteLength; + } + + if (packet.subscriptionIdentifiers != undefined) { + properties_length += packet.subscriptionIdentifiers.length; // each identifier is a separate property entry + for (let subscription_identifier of packet.subscriptionIdentifiers) { + properties_length += vli.get_vli_byte_length(subscription_identifier); + } + } + + if (packet.contentType != undefined) { + properties_length += 3 + packet.contentType.byteLength; + } + + properties_length += compute_user_properties_length(packet.userProperties); + + remaining_length += properties_length + vli.get_vli_byte_length(properties_length); + if (packet.payload) { + remaining_length += packet.payload.byteLength; + } + + return [remaining_length, properties_length]; +} + +function encode_publish_packet_properties(steps: Array, packet: model.PublishPacketBinary) { + if (packet.payloadFormat != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.PAYLOAD_FORMAT_INDICATOR_PROPERTY_CODE }); + steps.push({ type: EncodingStepType.U8, value: packet.payloadFormat }); + } + + if (packet.messageExpiryIntervalSeconds != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.MESSAGE_EXPIRY_INTERVAL_PROPERTY_CODE }); + steps.push({ type: EncodingStepType.U32, value: packet.messageExpiryIntervalSeconds }); + } + + if (packet.topicAlias != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.TOPIC_ALIAS_PROPERTY_CODE }); + steps.push({ type: EncodingStepType.U16, value: packet.topicAlias }); + } + + if (packet.responseTopic != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.RESPONSE_TOPIC_PROPERTY_CODE }); + encode_required_length_prefixed_array_buffer(steps, packet.responseTopic); + } + + if (packet.correlationData != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.CORRELATION_DATA_PROPERTY_CODE }); + encode_required_length_prefixed_array_buffer(steps, packet.correlationData); + } + + if (packet.subscriptionIdentifiers != undefined) { + for (let subscription_identifier of packet.subscriptionIdentifiers) { + steps.push({ type: EncodingStepType.U8, value: model.SUBSCRIPTION_IDENTIFIER_PROPERTY_CODE }); + steps.push({ type: EncodingStepType.VLI, value: subscription_identifier }); + } + } + + if (packet.contentType != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.CONTENT_TYPE_PROPERTY_CODE }); + encode_required_length_prefixed_array_buffer(steps, packet.contentType); + } + + encode_user_properties(steps, packet.userProperties); +} + +function encode_publish_packet5(steps: Array, packet: model.PublishPacketBinary) { + let [remaining_length, properties_length] = get_publish_packet_remaining_lengths5(packet); + + steps.push({ type: EncodingStepType.U8, value: model.PACKET_TYPE_FIRST_BYTE_PUBLISH | compute_publish_flags(packet) }); + steps.push({ type: EncodingStepType.VLI, value: remaining_length }); + encode_required_length_prefixed_array_buffer(steps, packet.topicName); + + if (packet.qos > 0) { + if (packet.packetId) { + steps.push({type: EncodingStepType.U16, value: packet.packetId}); + } else { + throw new CrtError("Publish(5) packet with non-zero qos and invalid or missing packet id"); + } + } + + steps.push({ type: EncodingStepType.VLI, value: properties_length }); + encode_publish_packet_properties(steps, packet); + + if (packet.payload && packet.payload.byteLength > 0) { + steps.push({ type: EncodingStepType.BYTES, value: new DataView(packet.payload) }); + } +} + +function get_puback_packet_remaining_lengths5(packet: model.PubackPacketBinary) : [number, number] { + let remaining_length: number = 3; // packet id + reason code + let properties_length: number = 0; + + if (packet.reasonString != undefined) { + properties_length += 3 + packet.reasonString.byteLength; + } + + properties_length += compute_user_properties_length(packet.userProperties); + + // note that the caller will adjust this down to 2 if the properties_length is 0 and the reason code is success + remaining_length += properties_length + vli.get_vli_byte_length(properties_length); + + return [remaining_length, properties_length]; +} + +function encode_puback_properties(steps: Array, packet: model.PubackPacketBinary) { + if (packet.reasonString != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.REASON_STRING_PROPERTY_CODE }); + encode_required_length_prefixed_array_buffer(steps, packet.reasonString); + } + + encode_user_properties(steps, packet.userProperties); +} + +function encode_puback_packet5(steps: Array, packet: model.PubackPacketBinary) { + let [remaining_length, properties_length] = get_puback_packet_remaining_lengths5(packet); + let truncated_packet : boolean = properties_length == 0 && packet.reasonCode == mqtt5_packet.PubackReasonCode.Success; + + steps.push({ type: EncodingStepType.U8, value: model.PACKET_TYPE_FIRST_BYTE_PUBACK }); + if (truncated_packet) { + steps.push({type: EncodingStepType.U8, value: 0x02}); + } else { + steps.push({type: EncodingStepType.VLI, value: remaining_length}); + } + steps.push({ type: EncodingStepType.U16, value: packet.packetId }); + + if (truncated_packet) { + return; + } + + steps.push({ type: EncodingStepType.U8, value: packet.reasonCode }); + + steps.push({ type: EncodingStepType.VLI, value: properties_length }); + encode_puback_properties(steps, packet); +} + +function get_subscribe_packet_remaining_lengths5(packet: model.SubscribePacketBinary) : [number, number] { + let remaining_length: number = 2; // packet id + let properties_length: number = 0; + + if (packet.subscriptionIdentifier != undefined) { + properties_length += 1 + vli.get_vli_byte_length(packet.subscriptionIdentifier); + } + + properties_length += compute_user_properties_length(packet.userProperties); + + remaining_length += properties_length + vli.get_vli_byte_length(properties_length); + + for (let subscription of packet.subscriptions) { + remaining_length += 3 + subscription.topicFilter.byteLength; + } + + return [remaining_length, properties_length]; +} + +function compute_subscription_flags5(subscription: model.SubscriptionBinary) : number { + let flags : number = subscription.qos; + if (subscription.noLocal) { + flags |= model.SUBSCRIPTION_FLAGS_NO_LOCAL; + } + + if (subscription.retainHandlingType) { + flags |= (subscription.retainHandlingType << model.SUBSCRIPTION_FLAGS_RETAIN_HANDLING_TYPE_SHIFT); + } + + if (subscription.retainAsPublished) { + flags |= model.SUBSCRIPTION_FLAGS_RETAIN_AS_PUBLISHED; + } + + return flags; +} + +function encode_subscription5(steps: Array, subscription: model.SubscriptionBinary) { + encode_required_length_prefixed_array_buffer(steps, subscription.topicFilter); + steps.push({ type: EncodingStepType.U8, value: compute_subscription_flags5(subscription) }); +} + +function encode_subscribe_properties(steps: Array, packet: model.SubscribePacketBinary) { + if (packet.subscriptionIdentifier != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.SUBSCRIPTION_IDENTIFIER_PROPERTY_CODE }); + steps.push({ type: EncodingStepType.VLI, value: packet.subscriptionIdentifier }); + } + + encode_user_properties(steps, packet.userProperties); +} + +function encode_subscribe_packet5(steps: Array, packet: model.SubscribePacketBinary) { + let [remaining_length, properties_length] = get_subscribe_packet_remaining_lengths5(packet); + + steps.push({ type: EncodingStepType.U8, value: model.PACKET_TYPE_FIRST_BYTE_SUBSCRIBE }); + steps.push({ type: EncodingStepType.VLI, value: remaining_length }); + steps.push({ type: EncodingStepType.U16, value: packet.packetId }); + + steps.push({ type: EncodingStepType.VLI, value: properties_length }); + encode_subscribe_properties(steps, packet); + + for (let subscription of packet.subscriptions) { + encode_subscription5(steps, subscription); + } +} + +function get_unsubscribe_packet_remaining_lengths5(packet: model.UnsubscribePacketBinary) : [number, number] { + let remaining_length: number = 2; // packet id + let properties_length: number = compute_user_properties_length(packet.userProperties); + + remaining_length += properties_length + vli.get_vli_byte_length(properties_length); + + for (let topic_filter of packet.topicFilters) { + remaining_length += 2 + topic_filter.byteLength; + } + + return [remaining_length, properties_length]; +} + +function encode_unsubscribe_properties(steps: Array, packet: model.UnsubscribePacketBinary) { + encode_user_properties(steps, packet.userProperties); +} + +function encode_unsubscribe_packet5(steps: Array, packet: model.UnsubscribePacketBinary) { + let [remaining_length, properties_length] = get_unsubscribe_packet_remaining_lengths5(packet); + + steps.push({ type: EncodingStepType.U8, value: model.PACKET_TYPE_FIRST_BYTE_UNSUBSCRIBE }); + steps.push({ type: EncodingStepType.VLI, value: remaining_length }); + steps.push({ type: EncodingStepType.U16, value: packet.packetId }); + + steps.push({ type: EncodingStepType.VLI, value: properties_length }); + encode_unsubscribe_properties(steps, packet); + + for (let topic_filter of packet.topicFilters) { + encode_required_length_prefixed_array_buffer(steps, topic_filter); + } +} + +function get_disconnect_packet_remaining_lengths5(packet: model.DisconnectPacketBinary) : [number, number] { + let remaining_length: number = 0; + let properties_length: number = compute_user_properties_length(packet.userProperties); + + if (packet.reasonString != undefined) { + properties_length += 3 + packet.reasonString.byteLength; + } + + if (packet.serverReference != undefined) { + properties_length += 3 + packet.serverReference.byteLength; + } + + if (packet.sessionExpiryIntervalSeconds != undefined) { + properties_length += 5; + } + + if (properties_length > 0) { + remaining_length += 1 + properties_length + vli.get_vli_byte_length(properties_length); // include reason code unconditionally + } else if (packet.reasonCode != mqtt5_packet.DisconnectReasonCode.NormalDisconnection) { + remaining_length += 1; + } + + return [remaining_length, properties_length]; +} + +function encode_disconnect_properties(steps: Array, packet: model.DisconnectPacketBinary) { + if (packet.reasonString != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.REASON_STRING_PROPERTY_CODE }); + encode_required_length_prefixed_array_buffer(steps, packet.reasonString); + } + + if (packet.serverReference != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.SERVER_REFERENCE_PROPERTY_CODE }); + encode_required_length_prefixed_array_buffer(steps, packet.serverReference); + } + + if (packet.sessionExpiryIntervalSeconds != undefined) { + steps.push({ type: EncodingStepType.U8, value: model.SESSION_EXPIRY_INTERVAL_PROPERTY_CODE }); + steps.push({ type: EncodingStepType.U32, value: packet.sessionExpiryIntervalSeconds }); + } + + encode_user_properties(steps, packet.userProperties); +} + +function encode_disconnect_packet5(steps: Array, packet: model.DisconnectPacketBinary) { + let [remaining_length, properties_length] = get_disconnect_packet_remaining_lengths5(packet); + + steps.push({ type: EncodingStepType.U8, value: model.PACKET_TYPE_FIRST_BYTE_DISCONNECT }); + steps.push({ type: EncodingStepType.VLI, value: remaining_length }); + + if (remaining_length > 0) { + steps.push({ type: EncodingStepType.U8, value: packet.reasonCode }); + if (remaining_length > 1) { + steps.push({ type: EncodingStepType.VLI, value: properties_length }); + encode_disconnect_properties(steps, packet); + } + } +} + +// Encoding Implementation + +export type EncodingFunction = (steps: Array, packet: model.IPacketBinary) => void; +export type EncodingFunctionSet = Map; + +// Encoders for packets sent by the client. Packets sent by the server have encoders defined in the spec file. +export function build_client_encoding_function_set(mode: model.ProtocolMode) : EncodingFunctionSet { + switch (mode) { + case model.ProtocolMode.Mqtt311: + return new Map([ + [mqtt5_packet.PacketType.Connect, (steps, packet) => { encode_connect_packet311(steps, packet as model.ConnectPacketBinary); }], + [mqtt5_packet.PacketType.Subscribe, (steps, packet) => { encode_subscribe_packet311(steps, packet as model.SubscribePacketBinary); }], + [mqtt5_packet.PacketType.Unsubscribe, (steps, packet) => { encode_unsubscribe_packet311(steps, packet as model.UnsubscribePacketBinary); }], + [mqtt5_packet.PacketType.Publish, (steps, packet) => { encode_publish_packet311(steps, packet as model.PublishPacketBinary); }], + [mqtt5_packet.PacketType.Puback, (steps, packet) => { encode_puback_packet311(steps, packet as model.PubackPacketBinary); }], + [mqtt5_packet.PacketType.Disconnect, (steps, packet) => { encode_disconnect_packet311(steps, packet as model.DisconnectPacketBinary); }], + [mqtt5_packet.PacketType.Pingreq, (steps, packet) => { encode_pingreq_packet(steps); }], + ]); + + case model.ProtocolMode.Mqtt5: + return new Map([ + [mqtt5_packet.PacketType.Connect, (steps, packet) => { encode_connect_packet5(steps, packet as model.ConnectPacketBinary); }], + [mqtt5_packet.PacketType.Subscribe, (steps, packet) => { encode_subscribe_packet5(steps, packet as model.SubscribePacketBinary); }], + [mqtt5_packet.PacketType.Unsubscribe, (steps, packet) => { encode_unsubscribe_packet5(steps, packet as model.UnsubscribePacketBinary); }], + [mqtt5_packet.PacketType.Publish, (steps, packet) => { encode_publish_packet5(steps, packet as model.PublishPacketBinary); }], + [mqtt5_packet.PacketType.Puback, (steps, packet) => { encode_puback_packet5(steps, packet as model.PubackPacketBinary); }], + [mqtt5_packet.PacketType.Disconnect, (steps, packet) => { encode_disconnect_packet5(steps, packet as model.DisconnectPacketBinary); }], + [mqtt5_packet.PacketType.Pingreq, (steps, packet) => { encode_pingreq_packet(steps); }], + ]); + + } + + throw new CrtError("Unsupported protocol"); +} + +function add_encoding_steps(encoders: EncodingFunctionSet, steps: Array, packet: model.IPacketBinary) { + if (steps.length > 0) { + throw new CrtError("Encoding steps already exist"); + } + + if (!packet.type) { + throw new CrtError("Undefined packet type for encoding"); + } + + let encoder = encoders.get(packet.type); + if (!encoder) { + throw new CrtError("Unsupported packet type for encoding"); + } + + encoder(steps, packet); +} + +interface ApplyEncodingStepResult { + nextBuffer: DataView, + step?: EncodingStep +} + +function apply_encoding_step(buffer: DataView, step: EncodingStep) : ApplyEncodingStepResult { + switch (step.type) { + case EncodingStepType.U8: + buffer.setUint8(0, step.value as number); + return { + nextBuffer: new DataView(buffer.buffer, buffer.byteOffset + 1, buffer.byteLength - 1) + }; + + case EncodingStepType.U16: + buffer.setUint16(0, step.value as number); + return { + nextBuffer: new DataView(buffer.buffer, buffer.byteOffset + 2, buffer.byteLength - 2) + }; + + case EncodingStepType.U32: + buffer.setUint32(0, step.value as number); + return { + nextBuffer: new DataView(buffer.buffer, buffer.byteOffset + 4, buffer.byteLength - 4) + }; + + case EncodingStepType.VLI: + return { + nextBuffer: vli.encode_vli(buffer, step.value as number) + }; + + case EncodingStepType.BYTES: + let source = step.value as DataView; + let amountToCopy = Math.min(buffer.byteLength, source.byteLength); + + const destArray = new Uint8Array(buffer.buffer, buffer.byteOffset); + const sourceArray = new Uint8Array(source.buffer, source.byteOffset, amountToCopy); + destArray.set(sourceArray); + let result : ApplyEncodingStepResult = { + nextBuffer: new DataView(buffer.buffer, buffer.byteOffset + amountToCopy, buffer.byteLength - amountToCopy) + }; + if (amountToCopy < source.byteLength) { + // ran out of room. Clip the step. It's the caller's responsibility to push the clipped step back + // into the queue. + result.step = { + type: EncodingStepType.BYTES, + value: new DataView(source.buffer, source.byteOffset + amountToCopy, source.byteLength - amountToCopy) + }; + } + return result; + + default: + throw new CrtError("Unknown encoding step type"); + } +} + + +export enum ServiceResultType { + Complete, + InProgress +} + +export interface ServiceResult { + type: ServiceResultType; + encodedView?: DataView; + nextView: DataView; +} + +/** + * Encoder implementation. All failures are surfaced as exceptions and considered protocol-fatal. + * + * The implementation assumes full, stringent validation has been performed prior to encoding (ie all packets are + * protocol-compliant). + */ +export class Encoder { + private packet: model.IPacketBinary | null = null; + private steps: Array = new Array(); + private currentStep: number = 0; + + constructor(private encoders : EncodingFunctionSet) { + } + + // called after connection establishment + reset() { + this.packet = null; + this.steps.length = 0; + this.currentStep = 0; + } + + // called on new packet ready and previous packet, if any, complete + init_for_packet(packet: model.IPacketBinary) { + this.packet = packet; + this.steps.length = 0; + this.currentStep = 0; + add_encoding_steps(this.encoders, this.steps, packet); + } + + service(dest: DataView) : ServiceResult { + if (!this.packet) { + return { + type: ServiceResultType.Complete, + nextView: dest + }; + } + + let startingOffset : number = dest.byteOffset; + let resultType : ServiceResultType = ServiceResultType.Complete; + + while (this.currentStep < this.steps.length) { + if (dest.byteLength < 4) { + resultType = ServiceResultType.InProgress; + break; + } + + let step_result = apply_encoding_step(dest, this.steps[this.currentStep]); + dest = step_result.nextBuffer; + if (step_result.step) { + this.steps[this.currentStep] = step_result.step; + } else { + this.currentStep += 1; + } + } + + return { + type: resultType, + encodedView: new DataView(dest.buffer, startingOffset, dest.byteOffset - startingOffset), + nextView: dest + }; + } +} \ No newline at end of file diff --git a/lib/browser/mqtt_internal/model.ts b/lib/browser/mqtt_internal/model.ts new file mode 100644 index 000000000..d53c56a24 --- /dev/null +++ b/lib/browser/mqtt_internal/model.ts @@ -0,0 +1,764 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +import {CrtError} from "../error"; +import * as mqtt5_packet from '../../common/mqtt5_packet'; + +export enum ProtocolMode { + Mqtt311, + Mqtt5 +} + +// A variety of constants related to the MQTT311 and MQTT5 protocols + +export const USER_PROPERTY_PROPERTY_CODE : number = 0x26; +export const SESSION_EXPIRY_INTERVAL_PROPERTY_CODE : number = 0x11; +export const RECEIVE_MAXIMUM_PROPERTY_CODE : number = 0x21; +export const MAXIMUM_PACKET_SIZE_PROPERTY_CODE : number = 0x27; +export const TOPIC_ALIAS_MAXIMUM_PROPERTY_CODE : number = 0x22; +export const REQUEST_RESPONSE_INFORMATION_PROPERTY_CODE : number = 0x19; +export const REQUEST_PROBLEM_INFORMATION_PROPERTY_CODE : number = 0x17; +export const WILL_DELAY_INTERVAL_PROPERTY_CODE : number = 0x18; +export const PAYLOAD_FORMAT_INDICATOR_PROPERTY_CODE : number = 0x01; +export const MESSAGE_EXPIRY_INTERVAL_PROPERTY_CODE : number = 0x02; +export const CONTENT_TYPE_PROPERTY_CODE : number = 0x03; +export const RESPONSE_TOPIC_PROPERTY_CODE : number = 0x08; +export const CORRELATION_DATA_PROPERTY_CODE : number = 0x09; +export const MAXIMUM_QOS_PROPERTY_CODE : number = 0x24; +export const RETAIN_AVAILABLE_PROPERTY_CODE : number = 0x25; +export const ASSIGNED_CLIENT_IDENTIFIER_PROPERTY_CODE : number = 0x12; +export const REASON_STRING_PROPERTY_CODE : number = 0x1F; +export const WILDCARD_SUBSCRIPTIONS_AVAILABLE_PROPERTY_CODE : number = 0x28; +export const SUBSCRIPTION_IDENTIFIERS_AVAILABLE_PROPERTY_CODE : number = 0x29; +export const SHARED_SUBSCRIPTIONS_AVAILABLE_PROPERTY_CODE : number = 0x2A; +export const SERVER_KEEP_ALIVE_PROPERTY_CODE : number = 0x13; +export const RESPONSE_INFORMATION_PROPERTY_CODE : number = 0x1A; +export const SERVER_REFERENCE_PROPERTY_CODE : number = 0x1C; +export const AUTHENTICATION_METHOD_PROPERTY_CODE: number = 0x15; +export const AUTHENTICATION_DATA_PROPERTY_CODE: number = 0x16; +export const TOPIC_ALIAS_PROPERTY_CODE : number = 0x23; +export const SUBSCRIPTION_IDENTIFIER_PROPERTY_CODE : number = 0x0B; + +export const CONNECT_311_PROTOCOL_BYTES = [0x00, 0x04, 0x4D, 0x51, 0x54, 0x54, 0x04]; +export const CONNECT_311_PROTOCOL_BUFFER = new Uint8Array(CONNECT_311_PROTOCOL_BYTES); +export const CONNECT_311_PROTOCOL_DATAVIEW = new DataView(CONNECT_311_PROTOCOL_BUFFER.buffer); +export const CONNECT_5_PROTOCOL_BYTES = [0x00, 0x04, 0x4D, 0x51, 0x54, 0x54, 0x05]; +export const CONNECT_5_PROTOCOL_BUFFER = new Uint8Array(CONNECT_5_PROTOCOL_BYTES); +export const CONNECT_5_PROTOCOL_DATAVIEW = new DataView(CONNECT_5_PROTOCOL_BUFFER.buffer); + +export const CONNECT_FLAGS_HAS_USERNAME : number = 0x80; +export const CONNECT_FLAGS_HAS_PASSWORD : number = 0x40; +export const CONNECT_FLAGS_HAS_WILL : number = 0x04; +export const CONNECT_FLAGS_QOS_SHIFT : number = 0x03; +export const CONNECT_FLAGS_WILL_RETAIN : number = 0x20; +export const CONNECT_FLAGS_CLEAN_SESSION : number = 0x02; + +export const CONNACK_FLAGS_SESSION_PRESENT : number = 0x01; + +export const PUBLISH_FLAGS_QOS_SHIFT : number = 0x01; +export const PUBLISH_FLAGS_RETAIN : number = 0x01; +export const PUBLISH_FLAGS_DUPLICATE : number = 0x08; + +export const SUBSCRIPTION_FLAGS_NO_LOCAL : number = 0x04; +export const SUBSCRIPTION_FLAGS_RETAIN_AS_PUBLISHED : number = 0x08; +export const SUBSCRIPTION_FLAGS_RETAIN_HANDLING_TYPE_SHIFT : number = 0x04; + +export const PACKET_TYPE_FIRST_BYTE_CONNECT : number = 0x10; +export const PACKET_TYPE_FIRST_BYTE_CONNACK : number = 0x20; +export const PACKET_TYPE_FIRST_BYTE_PUBLISH : number = 0x30; +export const PACKET_TYPE_FIRST_BYTE_PUBACK : number = 0x40; +export const PACKET_TYPE_FIRST_BYTE_SUBSCRIBE : number = 0x82; +export const PACKET_TYPE_FIRST_BYTE_SUBACK : number = 0x90; +export const PACKET_TYPE_FIRST_BYTE_UNSUBSCRIBE : number = 0xA2; +export const PACKET_TYPE_FIRST_BYTE_UNSUBACK : number = 0xB0; +export const PACKET_TYPE_FIRST_BYTE_DISCONNECT : number = 0xE0; + +export const PACKET_TYPE_PINGREQ_FULL_ENCODING : number = 0xC000; +export const PACKET_TYPE_PINGRESP_FULL_ENCODING : number = 0xD000; +export const PACKET_TYPE_DISCONNECT_FULL_ENCODING_311 : number = 0xE000; + +export const QOS_MASK : number = 0x03; +export const RETAIN_HANDLING_TYPE_SHIFT : number = 0x03; + +/* + * We specify two separate-but-related packet models in this module: + * + * 1. An internal model - extends packets defined in "common/mqtt5_packet.ts" with protocol-internal details like + * packet id, duplicate, and other fields that we don't want to put into the public packet model. This is the + * model we decode into (so technically these fields will be visible as properties on received packets, but + * far more importantly, they won't be required on outbound packets). + * + * 2. A binary model - a transformation of the internal packets to one where all field primitives are numbers or + * ArrayBuffers. This is the representation that the client will track persistently and output to the wire + * (ie, the encoder operates on the binary model). The binary model is needed due to the fact that Javascript + * does not have any API for computing the utf-8 length of a string other than by performing the encoding (due + * to the fact that strings are represented internally using a non-utf-8 encoding). By converting to and using + * a binary model, we only ever have to do the to-bytes conversion once (we need to know the lengths of all + * string-value fields before we even begin the encoding due to VLI remaining length calculations). + */ + +// Internal Model +export interface PublishPacketInternal extends mqtt5_packet.PublishPacket { + packetId?: number; + + duplicate: boolean; +} + +export interface PubackPacketInternal extends mqtt5_packet.PubackPacket { + packetId: number +} + +export interface SubscribePacketInternal extends mqtt5_packet.SubscribePacket { + packetId: number +} + +export interface SubackPacketInternal extends mqtt5_packet.SubackPacket { + packetId: number +} + +export interface UnsubscribePacketInternal extends mqtt5_packet.UnsubscribePacket { + packetId: number +} + +export interface UnsubackPacketInternal extends mqtt5_packet.UnsubackPacket { + packetId: number +} + +export interface ConnectPacketInternal extends mqtt5_packet.ConnectPacket { + cleanStart: boolean; + + topicAliasMaximum?: number; + + authenticationMethod?: string; + + authenticationData?: ArrayBuffer; +} + +export interface ConnackPacketInternal extends mqtt5_packet.ConnackPacket { + authenticationMethod?: string; + + authenticationData?: ArrayBuffer; +} + +export interface PingreqPacketInternal extends mqtt5_packet.IPacket { +} + +export interface PingrespPacketInternal extends mqtt5_packet.IPacket { +} + +export interface DisconnectPacketInternal extends mqtt5_packet.DisconnectPacket { +} + +// Binary Model +export interface IPacketBinary extends mqtt5_packet.IPacket { +} + +export interface UserPropertyBinary { + name: ArrayBuffer; + + value: ArrayBuffer; +} + +export interface PublishPacketBinary extends IPacketBinary { + packetId?: number; + + topicName: ArrayBuffer; + + payload?: ArrayBuffer; + + qos: number; + + duplicate?: number; + + retain?: number; + + payloadFormat?: number; + + messageExpiryIntervalSeconds?: number; + + topicAlias?: number; + + responseTopic?: ArrayBuffer; + + correlationData?: ArrayBuffer; + + subscriptionIdentifiers?: Array; + + contentType?: ArrayBuffer; + + userProperties?: Array; +} + +export interface PubackPacketBinary extends IPacketBinary { + packetId: number; + + reasonCode: number; + + reasonString?: ArrayBuffer; + + userProperties?: Array; +} + +export interface SubscriptionBinary { + topicFilter: ArrayBuffer; + qos: number; + noLocal?: number; + retainAsPublished?: number; + retainHandlingType?: number; +} + +export interface SubscribePacketBinary extends IPacketBinary { + packetId: number; + + subscriptions: Array; + + subscriptionIdentifier?: number; + + userProperties?: Array; +} + +export interface SubackPacketBinary extends IPacketBinary { + packetId: number; + + reasonCodes: Array; + + reasonString?: ArrayBuffer; + + userProperties?: Array; +} + +export interface UnsubscribePacketBinary extends IPacketBinary { + packetId: number; + + topicFilters: Array; + + userProperties?: Array; +} + +export interface UnsubackPacketBinary extends IPacketBinary { + packetId: number; + + reasonCodes: Array; + + reasonString?: ArrayBuffer; + + userProperties?: Array; +} + +export interface ConnectPacketBinary extends IPacketBinary { + cleanSession: number; + + keepAliveIntervalSeconds: number; + + clientId?: ArrayBuffer; + + username?: ArrayBuffer; + + password?: ArrayBuffer; + + sessionExpiryIntervalSeconds?: number; + + topicAliasMaximum?: number; + + requestResponseInformation?: number; + + requestProblemInformation?: number; + + receiveMaximum?: number; + + maximumPacketSizeBytes?: number; + + willDelayIntervalSeconds?: number; + + will?: PublishPacketBinary; + + authenticationMethod?: ArrayBuffer; + + authenticationData?: ArrayBuffer; + + userProperties?: Array; +} + +export interface ConnackPacketBinary extends IPacketBinary { + sessionPresent: number; + + reasonCode: number; + + sessionExpiryInterval?: number; + + receiveMaximum?: number; + + maximumQos?: number; + + retainAvailable?: number; + + maximumPacketSize?: number; + + assignedClientIdentifier?: ArrayBuffer; + + topicAliasMaximum?: number; + + reasonString?: ArrayBuffer; + + wildcardSubscriptionsAvailable?: number; + + subscriptionIdentifiersAvailable?: number; + + sharedSubscriptionsAvailable?: number; + + serverKeepAlive?: number; + + responseInformation?: ArrayBuffer; + + serverReference?: ArrayBuffer; + + authenticationMethod?: ArrayBuffer; + + authenticationData?: ArrayBuffer; + + userProperties?: Array; +} + +export interface PingreqPacketBinary extends IPacketBinary { +} + +export interface PingrespPacketBinary extends IPacketBinary { +} + +export interface DisconnectPacketBinary extends IPacketBinary { + reasonCode: number; + + sessionExpiryIntervalSeconds?: number; + + reasonString?: ArrayBuffer; + + serverReference?: ArrayBuffer; + + userProperties?: Array; +} + +function binary_data_to_array_buffer(data: BinaryData) : ArrayBuffer { + if (data instanceof ArrayBuffer) { + return data; + } else if (data instanceof Uint8Array) { + return data.buffer; + } else if (data instanceof Buffer) { + return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength); + } else { + throw new CrtError("Invalid binary data"); + } +} + +function convert_user_properties_to_binary(properties: Array) : Array { + let encoder = new TextEncoder(); + let internal_properties : Array = []; + + for (let property of properties) { + internal_properties.push({ + name: encoder.encode(property.name).buffer, + value: encoder.encode(property.value).buffer + }); + } + + return internal_properties; +} + +function convert_connect_packet_to_binary(packet: ConnectPacketInternal) : ConnectPacketBinary { + let encoder = new TextEncoder(); + let internal_packet : ConnectPacketBinary = { + type: mqtt5_packet.PacketType.Connect, + cleanSession: packet.cleanStart ? 1 : 0, + keepAliveIntervalSeconds: packet.keepAliveIntervalSeconds + }; + + if (packet.clientId != undefined) { + internal_packet.clientId = encoder.encode(packet.clientId).buffer; + } + + if (packet.username != undefined) { + internal_packet.username = encoder.encode(packet.username).buffer; + } + + if (packet.password != undefined) { + internal_packet.password = binary_data_to_array_buffer(packet.password); + } + + if (packet.topicAliasMaximum != undefined) { + internal_packet.topicAliasMaximum = packet.topicAliasMaximum; + } + + if (packet.sessionExpiryIntervalSeconds != undefined) { + internal_packet.sessionExpiryIntervalSeconds = packet.sessionExpiryIntervalSeconds; + } + + if (packet.requestResponseInformation != undefined) { + internal_packet.requestResponseInformation = packet.requestResponseInformation ? 1 : 0; + } + + if (packet.requestProblemInformation != undefined) { + internal_packet.requestProblemInformation = packet.requestProblemInformation ? 1 : 0; + } + + if (packet.receiveMaximum != undefined) { + internal_packet.receiveMaximum = packet.receiveMaximum; + } + + if (packet.maximumPacketSizeBytes != undefined) { + internal_packet.maximumPacketSizeBytes = packet.maximumPacketSizeBytes; + } + + if (packet.willDelayIntervalSeconds != undefined) { + internal_packet.willDelayIntervalSeconds = packet.willDelayIntervalSeconds; + } + + if (packet.will) { + internal_packet.will = convert_publish_packet_to_binary(packet.will as PublishPacketInternal); + } + + if (packet.authenticationMethod != undefined) { + internal_packet.authenticationMethod = encoder.encode(packet.authenticationMethod).buffer; + } + + if (packet.authenticationData != undefined) { + internal_packet.authenticationData = binary_data_to_array_buffer(packet.authenticationData); + } + + if (packet.userProperties != undefined) { + internal_packet.userProperties = convert_user_properties_to_binary(packet.userProperties); + } + + return internal_packet; +} + +function convert_connack_packet_to_binary(packet: ConnackPacketInternal) : ConnackPacketBinary { + let encoder = new TextEncoder(); + let internal_packet : ConnackPacketBinary = { + type: mqtt5_packet.PacketType.Connack, + sessionPresent: packet.sessionPresent ? 1 : 0, + reasonCode: packet.reasonCode + }; + + if (packet.sessionExpiryInterval != undefined) { + internal_packet.sessionExpiryInterval = packet.sessionExpiryInterval; + } + + if (packet.receiveMaximum != undefined) { + internal_packet.receiveMaximum = packet.receiveMaximum; + } + + if (packet.maximumQos != undefined) { + internal_packet.maximumQos = packet.maximumQos; + } + + if (packet.retainAvailable != undefined) { + internal_packet.retainAvailable = packet.retainAvailable ? 1 : 0; + } + + if (packet.maximumPacketSize != undefined) { + internal_packet.maximumPacketSize = packet.maximumPacketSize; + } + + if (packet.assignedClientIdentifier != undefined) { + internal_packet.assignedClientIdentifier = encoder.encode(packet.assignedClientIdentifier).buffer; + } + + if (packet.topicAliasMaximum != undefined) { + internal_packet.topicAliasMaximum = packet.topicAliasMaximum; + } + + if (packet.reasonString != undefined) { + internal_packet.reasonString = encoder.encode(packet.reasonString).buffer; + } + + if (packet.wildcardSubscriptionsAvailable != undefined) { + internal_packet.wildcardSubscriptionsAvailable = packet.wildcardSubscriptionsAvailable ? 1 : 0; + } + + if (packet.subscriptionIdentifiersAvailable != undefined) { + internal_packet.subscriptionIdentifiersAvailable = packet.subscriptionIdentifiersAvailable ? 1 : 0; + } + + if (packet.sharedSubscriptionsAvailable != undefined) { + internal_packet.sharedSubscriptionsAvailable = packet.sharedSubscriptionsAvailable ? 1 : 0; + } + + if (packet.serverKeepAlive != undefined) { + internal_packet.serverKeepAlive = packet.serverKeepAlive; + } + + if (packet.responseInformation != undefined) { + internal_packet.responseInformation = encoder.encode(packet.responseInformation).buffer; + } + + if (packet.serverReference != undefined) { + internal_packet.serverReference = encoder.encode(packet.serverReference).buffer; + } + + if (packet.authenticationMethod != undefined) { + internal_packet.authenticationMethod = encoder.encode(packet.authenticationMethod).buffer; + } + + if (packet.authenticationData != undefined) { + internal_packet.authenticationData = binary_data_to_array_buffer(packet.authenticationData); + } + + if (packet.userProperties != undefined) { + internal_packet.userProperties = convert_user_properties_to_binary(packet.userProperties); + } + + return internal_packet; +} + +function payload_to_array_buffer(payload: mqtt5_packet.Payload) : ArrayBuffer { + if (payload instanceof ArrayBuffer) { + return payload; + } else if (payload instanceof Uint8Array) { + return payload.buffer; + } else if (payload instanceof Buffer) { + return payload.buffer.slice(payload.byteOffset, payload.byteOffset + payload.byteLength); + } else if (typeof(payload) === 'string') { + let encoder = new TextEncoder(); + return encoder.encode(payload).buffer; + } else { + throw new CrtError("Invalid payload"); + } +} + +function convert_publish_packet_to_binary(packet: PublishPacketInternal) : PublishPacketBinary { + let encoder = new TextEncoder(); + let internal_packet : PublishPacketBinary = { + type: mqtt5_packet.PacketType.Publish, + packetId: packet.packetId, + topicName : encoder.encode(packet.topicName).buffer, + qos: packet.qos, + duplicate: packet.duplicate ? 1 : 0, + }; + + if (packet.payload != undefined) { + internal_packet.payload = payload_to_array_buffer(packet.payload); + } + + if (packet.retain != undefined) { + internal_packet.retain = packet.retain ? 1 : 0; + } + + if (packet.payloadFormat != undefined) { + internal_packet.payloadFormat = packet.payloadFormat; + } + + if (packet.messageExpiryIntervalSeconds != undefined) { + internal_packet.messageExpiryIntervalSeconds = packet.messageExpiryIntervalSeconds; + } + + if (packet.topicAlias != undefined) { + internal_packet.topicAlias = packet.topicAlias; + } + + if (packet.responseTopic != undefined) { + internal_packet.responseTopic = encoder.encode(packet.responseTopic).buffer; + } + + if (packet.correlationData != undefined) { + internal_packet.correlationData = binary_data_to_array_buffer(packet.correlationData); + } + + if (packet.subscriptionIdentifiers != undefined) { + internal_packet.subscriptionIdentifiers = packet.subscriptionIdentifiers; + } + + if (packet.contentType != undefined) { + internal_packet.contentType = encoder.encode(packet.contentType).buffer; + } + + if (packet.userProperties != undefined) { + internal_packet.userProperties = convert_user_properties_to_binary(packet.userProperties); + } + + return internal_packet; +} + +function convert_puback_packet_to_binary(packet: PubackPacketInternal) : PubackPacketBinary { + let encoder = new TextEncoder(); + let internal_packet : PubackPacketBinary = { + type: mqtt5_packet.PacketType.Puback, + packetId: packet.packetId, + reasonCode: packet.reasonCode + }; + + if (packet.reasonString != undefined) { + internal_packet.reasonString = encoder.encode(packet.reasonString).buffer; + } + + if (packet.userProperties != undefined) { + internal_packet.userProperties = convert_user_properties_to_binary(packet.userProperties); + } + + return internal_packet; +} + +function convert_subscription_to_binary(subscription: mqtt5_packet.Subscription) : SubscriptionBinary { + let encoder = new TextEncoder(); + + let internal_subscription : SubscriptionBinary = { + topicFilter: encoder.encode(subscription.topicFilter).buffer, + qos: subscription.qos + }; + + if (subscription.noLocal != undefined) { + internal_subscription.noLocal = subscription.noLocal ? 1 : 0; + } + + if (subscription.retainAsPublished != undefined) { + internal_subscription.retainAsPublished = subscription.retainAsPublished ? 1 : 0; + } + + if (subscription.retainHandlingType != undefined) { + internal_subscription.retainHandlingType = subscription.retainHandlingType; + } + + return internal_subscription; +} + +function convert_subscribe_packet_to_binary(packet: SubscribePacketInternal) : SubscribePacketBinary { + let internal_packet : SubscribePacketBinary = { + type: mqtt5_packet.PacketType.Subscribe, + packetId: packet.packetId, + subscriptions: [] + }; + + for (let subscription of packet.subscriptions) { + internal_packet.subscriptions.push(convert_subscription_to_binary(subscription)); + } + + if (packet.subscriptionIdentifier != undefined) { + internal_packet.subscriptionIdentifier = packet.subscriptionIdentifier; + } + + if (packet.userProperties != undefined) { + internal_packet.userProperties = convert_user_properties_to_binary(packet.userProperties); + } + + return internal_packet; +} + +function convert_suback_packet_to_binary(packet: SubackPacketInternal) : SubackPacketBinary { + let encoder = new TextEncoder(); + let internal_packet: SubackPacketBinary = { + type: mqtt5_packet.PacketType.Suback, + packetId: packet.packetId, + reasonCodes: packet.reasonCodes + }; + + if (packet.reasonString != undefined) { + internal_packet.reasonString = encoder.encode(packet.reasonString).buffer; + } + + if (packet.userProperties != undefined) { + internal_packet.userProperties = convert_user_properties_to_binary(packet.userProperties); + } + + return internal_packet; +} + +function convert_unsubscribe_packet_to_binary(packet: UnsubscribePacketInternal) : UnsubscribePacketBinary { + let encoder = new TextEncoder(); + let internal_packet: UnsubscribePacketBinary = { + type: mqtt5_packet.PacketType.Unsubscribe, + packetId: packet.packetId, + topicFilters: [] + }; + + for (let topicFilter of packet.topicFilters) { + internal_packet.topicFilters.push(encoder.encode(topicFilter).buffer); + } + + if (packet.userProperties != undefined) { + internal_packet.userProperties = convert_user_properties_to_binary(packet.userProperties); + } + + return internal_packet; +} + +function convert_unsuback_packet_to_binary(packet: UnsubackPacketInternal) : UnsubackPacketBinary { + let encoder = new TextEncoder(); + let internal_packet: UnsubackPacketBinary = { + type: mqtt5_packet.PacketType.Unsuback, + packetId: packet.packetId, + reasonCodes: packet.reasonCodes + }; + + if (packet.reasonString != undefined) { + internal_packet.reasonString = encoder.encode(packet.reasonString).buffer; + } + + if (packet.userProperties != undefined) { + internal_packet.userProperties = convert_user_properties_to_binary(packet.userProperties); + } + + return internal_packet; +} + +function convert_disconnect_packet_to_binary(packet: DisconnectPacketInternal) : DisconnectPacketBinary { + let encoder = new TextEncoder(); + let internal_packet : DisconnectPacketBinary = { + type: mqtt5_packet.PacketType.Disconnect, + reasonCode: packet.reasonCode + }; + + if (packet.sessionExpiryIntervalSeconds != undefined) { + internal_packet.sessionExpiryIntervalSeconds = packet.sessionExpiryIntervalSeconds; + } + + if (packet.serverReference != undefined) { + internal_packet.serverReference = encoder.encode(packet.serverReference).buffer; + } + + if (packet.reasonString != undefined) { + internal_packet.reasonString = encoder.encode(packet.reasonString).buffer; + } + + if (packet.userProperties != undefined) { + internal_packet.userProperties = convert_user_properties_to_binary(packet.userProperties); + } + + return internal_packet; +} + +export function convert_packet_to_binary(packet: mqtt5_packet.IPacket) : IPacketBinary { + if (!packet.type) { + throw new CrtError("Invalid packet type"); + } + + switch(packet.type) { + case mqtt5_packet.PacketType.Connect: + return convert_connect_packet_to_binary(packet as ConnectPacketInternal); + case mqtt5_packet.PacketType.Connack: + return convert_connack_packet_to_binary(packet as ConnackPacketInternal); + case mqtt5_packet.PacketType.Publish: + return convert_publish_packet_to_binary(packet as PublishPacketInternal); + case mqtt5_packet.PacketType.Puback: + return convert_puback_packet_to_binary(packet as PubackPacketInternal); + case mqtt5_packet.PacketType.Subscribe: + return convert_subscribe_packet_to_binary(packet as SubscribePacketInternal); + case mqtt5_packet.PacketType.Suback: + return convert_suback_packet_to_binary(packet as SubackPacketInternal); + case mqtt5_packet.PacketType.Unsubscribe: + return convert_unsubscribe_packet_to_binary(packet as UnsubscribePacketInternal); + case mqtt5_packet.PacketType.Unsuback: + return convert_unsuback_packet_to_binary(packet as UnsubackPacketInternal); + case mqtt5_packet.PacketType.Disconnect: + return convert_disconnect_packet_to_binary(packet as DisconnectPacketInternal); + case mqtt5_packet.PacketType.Pingreq: + return { + type: mqtt5_packet.PacketType.Pingreq + }; + case mqtt5_packet.PacketType.Pingresp: + return { + type: mqtt5_packet.PacketType.Pingresp + }; + default: + throw new CrtError("Unsupported packet type: "); + } +} diff --git a/lib/browser/mqtt_internal/protocol_state.ts b/lib/browser/mqtt_internal/protocol_state.ts new file mode 100644 index 000000000..c6d888f02 --- /dev/null +++ b/lib/browser/mqtt_internal/protocol_state.ts @@ -0,0 +1,4 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ diff --git a/lib/browser/mqtt_internal/validate.ts b/lib/browser/mqtt_internal/validate.ts new file mode 100644 index 000000000..c6d888f02 --- /dev/null +++ b/lib/browser/mqtt_internal/validate.ts @@ -0,0 +1,4 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ diff --git a/lib/browser/mqtt_internal/vli.spec.ts b/lib/browser/mqtt_internal/vli.spec.ts new file mode 100644 index 000000000..94f94b975 --- /dev/null +++ b/lib/browser/mqtt_internal/vli.spec.ts @@ -0,0 +1,275 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +import * as vli from "./vli"; + +test('VLI encoding length 1 checks', () => { + expect(vli.get_vli_byte_length(0)).toBe(1); + expect(vli.get_vli_byte_length(1)).toBe(1); + expect(vli.get_vli_byte_length(7)).toBe(1); + expect(vli.get_vli_byte_length(64)).toBe(1); + expect(vli.get_vli_byte_length(127)).toBe(1); +}); + +test('VLI encoding length 2 checks', () => { + expect(vli.get_vli_byte_length(128)).toBe(2); + expect(vli.get_vli_byte_length(129)).toBe(2); + expect(vli.get_vli_byte_length(256)).toBe(2); + expect(vli.get_vli_byte_length(128 * 128 - 1)).toBe(2); +}); + +test('VLI encoding length 3 checks', () => { + expect(vli.get_vli_byte_length(128 * 128)).toBe(3); + expect(vli.get_vli_byte_length(128 * 128 + 1)).toBe(3); + expect(vli.get_vli_byte_length(65537)).toBe(3); + expect(vli.get_vli_byte_length(128 * 128 * 128 - 1)).toBe(3); +}); + +test('VLI encoding length 4 checks', () => { + expect(vli.get_vli_byte_length(128 * 128 * 128)).toBe(4); + expect(vli.get_vli_byte_length(128 * 128 * 128 + 1)).toBe(4); + expect(vli.get_vli_byte_length(128 * 128 * 128 * 64)).toBe(4); + expect(vli.get_vli_byte_length(128 * 128 * 128 * 128 - 1)).toBe(4); +}); + +test('VLI encoding overflow checks', () => { + expect(() => { vli.get_vli_byte_length(128 * 128 * 128 * 128)}).toThrow("Invalid VLI value"); + expect(() => { vli.get_vli_byte_length(128 * 128 * 128 * 128 + 1)}).toThrow("Invalid VLI value"); + expect(() => { vli.get_vli_byte_length(128 * 128 * 128 * 128 * 2)}).toThrow("Invalid VLI value"); +}); + +test('VLI encoding 1 byte', () => { + let buffer = new ArrayBuffer(4); + let view1 = new DataView(buffer); + + let encode_result = vli.encode_vli(view1, 0); + expect(encode_result.byteLength).toBe(3); + expect(view1.getUint8(0)).toBe(0); + + encode_result = vli.encode_vli(view1, 1); + expect(encode_result.byteLength).toBe(3); + expect(view1.getUint8(0)).toBe(1); + + encode_result = vli.encode_vli(view1, 31); + expect(encode_result.byteLength).toBe(3); + expect(view1.getUint8(0)).toBe(31); + + encode_result = vli.encode_vli(view1, 127); + expect(encode_result.byteLength).toBe(3); + expect(view1.getUint8(0)).toBe(127); +}); + + +test('VLI encoding 2 byte', () => { + let buffer = new ArrayBuffer(4); + let view1 = new DataView(buffer); + + let encode_result = vli.encode_vli(view1, 128); + expect(encode_result.byteLength).toBe(2); + expect(view1.getUint8(0)).toBe(128); + expect(view1.getUint8(1)).toBe(1); + + encode_result = vli.encode_vli(view1, 129); + expect(encode_result.byteLength).toBe(2); + expect(view1.getUint8(0)).toBe(129); + expect(view1.getUint8(1)).toBe(1); + + encode_result = vli.encode_vli(view1, 255); + expect(encode_result.byteLength).toBe(2); + expect(view1.getUint8(0)).toBe(255); + expect(view1.getUint8(1)).toBe(1); + + encode_result = vli.encode_vli(view1, 256); + expect(encode_result.byteLength).toBe(2); + expect(view1.getUint8(0)).toBe(128); + expect(view1.getUint8(1)).toBe(2); + + encode_result = vli.encode_vli(view1, 128 * 128 - 1); + expect(encode_result.byteLength).toBe(2); + expect(view1.getUint8(0)).toBe(255); + expect(view1.getUint8(1)).toBe(127); +}); + +test('VLI encoding 3 byte', () => { + let buffer = new ArrayBuffer(4); + let view1 = new DataView(buffer); + + let encode_result = vli.encode_vli(view1, 128 * 128); + expect(encode_result.byteLength).toBe(1); + expect(view1.getUint8(0)).toBe(128); + expect(view1.getUint8(1)).toBe(128); + expect(view1.getUint8(2)).toBe(1); + + encode_result = vli.encode_vli(view1, 128 * 128 + 1); + expect(encode_result.byteLength).toBe(1); + expect(view1.getUint8(0)).toBe(129); + expect(view1.getUint8(1)).toBe(128); + expect(view1.getUint8(2)).toBe(1); + + encode_result = vli.encode_vli(view1, 128 * 128 + 127); + expect(encode_result.byteLength).toBe(1); + expect(view1.getUint8(0)).toBe(255); + expect(view1.getUint8(1)).toBe(128); + expect(view1.getUint8(2)).toBe(1); + + encode_result = vli.encode_vli(view1, 128 * 129); + expect(encode_result.byteLength).toBe(1); + expect(view1.getUint8(0)).toBe(128); + expect(view1.getUint8(1)).toBe(129); + expect(view1.getUint8(2)).toBe(1); + + encode_result = vli.encode_vli(view1, 128 * 128 * 128 - 1); + expect(encode_result.byteLength).toBe(1); + expect(view1.getUint8(0)).toBe(255); + expect(view1.getUint8(1)).toBe(255); + expect(view1.getUint8(2)).toBe(127); +}); + +test('VLI encoding 4 byte', () => { + let buffer = new ArrayBuffer(4); + let view1 = new DataView(buffer); + + let encode_result = vli.encode_vli(view1, 128 * 128 * 128); + expect(encode_result.byteLength).toBe(0); + expect(view1.getUint8(0)).toBe(128); + expect(view1.getUint8(1)).toBe(128); + expect(view1.getUint8(2)).toBe(128); + expect(view1.getUint8(3)).toBe(1); + + encode_result = vli.encode_vli(view1, 128 * 128 * 128 + 1); + expect(encode_result.byteLength).toBe(0); + expect(view1.getUint8(0)).toBe(129); + expect(view1.getUint8(1)).toBe(128); + expect(view1.getUint8(2)).toBe(128); + expect(view1.getUint8(3)).toBe(1); + + encode_result = vli.encode_vli(view1, 128 * 128 * 128 * 128 - 1); + expect(encode_result.byteLength).toBe(0); + expect(view1.getUint8(0)).toBe(255); + expect(view1.getUint8(1)).toBe(255); + expect(view1.getUint8(2)).toBe(255); + expect(view1.getUint8(3)).toBe(127); +}); + +test('VLI encoding overflow', () => { + let buffer = new ArrayBuffer(5); + let view1 = new DataView(buffer); + + expect(() => { vli.encode_vli(view1, 128 * 128 * 128 * 128) }).toThrow("Invalid VLI value"); +}); + + +test('VLI decoding - 1 byte', () => { + let buffer_0 = new Uint8Array([0]); + expect(vli.decode_vli(new DataView(buffer_0.buffer), 0).value).toBe(0); + + let buffer_1 = new Uint8Array([1]); + expect(vli.decode_vli(new DataView(buffer_1.buffer), 0).value).toBe(1); + + let buffer_63 = new Uint8Array([63]); + expect(vli.decode_vli(new DataView(buffer_63.buffer), 0).value).toBe(63); + + let buffer_127 = new Uint8Array([127]); + expect(vli.decode_vli(new DataView(buffer_127.buffer), 0).value).toBe(127); +}); + +test('VLI decoding - 2 byte', () => { + let buffer_128 = new Uint8Array([128, 1]); + expect(vli.decode_vli(new DataView(buffer_128.buffer), 0).value).toBe(128); + + let buffer_129 = new Uint8Array([129, 1]); + expect(vli.decode_vli(new DataView(buffer_129.buffer), 0).value).toBe(129); + + let buffer_255 = new Uint8Array([255, 1]); + expect(vli.decode_vli(new DataView(buffer_255.buffer), 0).value).toBe(255); + + let buffer_256 = new Uint8Array([128, 2]); + expect(vli.decode_vli(new DataView(buffer_256.buffer), 0).value).toBe(256); + + let buffer_1025 = new Uint8Array([129, 8]); + expect(vli.decode_vli(new DataView(buffer_1025.buffer), 0).value).toBe(1025); + + let buffer_16382 = new Uint8Array([254, 127]); + expect(vli.decode_vli(new DataView(buffer_16382.buffer), 0).value).toBe(16382); +}); + +test('VLI decoding - 3 byte', () => { + let buffer1 = new Uint8Array([128, 128, 1]); + expect(vli.decode_vli(new DataView(buffer1.buffer), 0).value).toBe(128 * 128); + + let buffer2 = new Uint8Array([129, 128, 1]); + expect(vli.decode_vli(new DataView(buffer2.buffer), 0).value).toBe(128 * 128 + 1); + + let buffer3 = new Uint8Array([255, 128, 1]); + expect(vli.decode_vli(new DataView(buffer3.buffer), 0).value).toBe(128 * 128 + 127); + + let buffer4 = new Uint8Array([128, 128, 2]); + expect(vli.decode_vli(new DataView(buffer4.buffer), 0).value).toBe(128 * 128 * 2); + + let buffer5 = new Uint8Array([255, 255, 127]); + expect(vli.decode_vli(new DataView(buffer5.buffer), 0).value).toBe(128 * 128 * 128 - 1); +}); + +test('VLI decoding - 4 byte', () => { + let buffer1 = new Uint8Array([128, 128, 128, 1]); + expect(vli.decode_vli(new DataView(buffer1.buffer), 0).value).toBe(128 * 128 * 128); + + let buffer2 = new Uint8Array([129, 128, 128, 1]); + expect(vli.decode_vli(new DataView(buffer2.buffer), 0).value).toBe(128 * 128 * 128 + 1); + + let buffer5 = new Uint8Array([255, 255, 255, 127]); + expect(vli.decode_vli(new DataView(buffer5.buffer), 0).value).toBe(128 * 128 * 128 * 128 - 1); +}); + +function do_round_trip_encode_decode_vli_test(value: number) { + let buffer = new ArrayBuffer(4); + + let encode_result = vli.encode_vli(new DataView(buffer), value); + let encoded_view = new DataView(buffer, 0, buffer.byteLength - encode_result.byteLength); + + expect(vli.decode_vli(encoded_view, 0).value).toBe(value); +} + +test('VLI round trip', () => { + do_round_trip_encode_decode_vli_test(0); + do_round_trip_encode_decode_vli_test(37); + do_round_trip_encode_decode_vli_test(199); + do_round_trip_encode_decode_vli_test(581); + do_round_trip_encode_decode_vli_test(3700); + do_round_trip_encode_decode_vli_test(31502); + do_round_trip_encode_decode_vli_test(278306); + do_round_trip_encode_decode_vli_test(26843545); +}); + +function do_encode_decode_multiple_vli_test(value: number, count: number) { + let buffer = new ArrayBuffer(count * 4); + let encoding_view = new DataView(buffer); + + for (let i = 0; i < count; i++) { + encoding_view = vli.encode_vli(encoding_view, value); + } + + let encoded_view = new DataView(buffer, 0, buffer.byteLength - encoding_view.byteLength); + let decode_count : number = 0; + let offset : number = 0; + while (offset < encoded_view.byteLength) { + let decode_result = vli.decode_vli(encoded_view, offset); + if (decode_result.type == vli.VliDecodeResultType.Success) { + expect(decode_result.value).toBe(value); + // @ts-ignore + offset = decode_result.nextOffset; + decode_count++; + } + } + + expect(decode_count).toBe(count); +} + +test('VLI Multiple', () => { + do_encode_decode_multiple_vli_test(42, 20); + do_encode_decode_multiple_vli_test(2000, 20); + do_encode_decode_multiple_vli_test(99999, 20); + do_encode_decode_multiple_vli_test(128 * 128 * 128 + 5, 20); +}); diff --git a/lib/browser/mqtt_internal/vli.ts b/lib/browser/mqtt_internal/vli.ts new file mode 100644 index 000000000..4dbc5856c --- /dev/null +++ b/lib/browser/mqtt_internal/vli.ts @@ -0,0 +1,81 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +import {CrtError} from "../error"; + +// assumes value is integral and non-negative +export function get_vli_byte_length(value: number) : number { + if (value < 128) { + return 1; + } else if (value < 16384) { + return 2; + } else if (value < 2097152) { + return 3; + } else if (value < 268435456) { + return 4; + } else { + throw new CrtError("Invalid VLI value"); + } +} + +// assumes value is integral and non-negative +export function encode_vli(dest: DataView, value: number) : DataView { + let i = 0; + + let hasMore = true; + while (hasMore) { + let byte = value & 0x7F; + value = value >>> 7; + hasMore = value > 0; + if (hasMore) { + byte = byte | 0x80; + if (i >= 3) { + throw new CrtError("Invalid VLI value"); + } + } + + dest.setUint8(i++, byte); + } + + return new DataView(dest.buffer, dest.byteOffset + i, dest.byteLength - i); +} + +export enum VliDecodeResultType { + Success, + MoreData, +} + +export interface VliDecodeResult { + type: VliDecodeResultType, + value?: number, + nextOffset?: number +} + +export function decode_vli(data: DataView, offset: number) : VliDecodeResult { + let value: number = 0; + let index: number = 0; + let shift: number = 0; + while (index < 4) { + let view_index = offset + index++; + let raw_byte = data.getUint8(view_index); + let masked_byte = raw_byte & 0x7F; + value += (masked_byte << shift); + if (masked_byte == raw_byte) { + return { + type: VliDecodeResultType.Success, + value: value, + nextOffset: offset + index + }; + } else if (view_index + 1 >= data.byteLength) { + return { + type: VliDecodeResultType.MoreData + }; + } + + shift += 7; + } + + throw new CrtError("Decoding failure - invalid VLI integer"); +} diff --git a/test/browser/jest.config.js b/test/browser/jest.config.js index b17b1247c..5d79eb9e4 100644 --- a/test/browser/jest.config.js +++ b/test/browser/jest.config.js @@ -4,7 +4,8 @@ module.exports = { testMatch: [ '/lib/common/*.spec.ts', '/lib/browser/*.spec.ts', - '/lib/browser/mqtt_request_response/*.spec.ts' + '/lib/browser/mqtt_request_response/*.spec.ts', + '/lib/browser/mqtt_internal/*.spec.ts' ], preset: 'jest-puppeteer', globals: {