@jonaskello-forks/amqp-client
Version:
AMQP 0-9-1 client, both for browsers (WebSocket) and node (TCP Socket)
377 lines (354 loc) • 14.2 kB
text/typescript
import { AMQPProperties, Field } from './amqp-properties.js'
/**
* An extended DataView, with AMQP protocol specific methods.
* Set methods returns bytes written.
* Get methods returns the value read and how many bytes it used.
* @ignore
*/
export class AMQPView extends DataView {
private static decoder = new TextDecoder()
private static encoder = new TextEncoder()
getUint64(byteOffset: number, littleEndian?: boolean) : number {
// split 64-bit number into two 32-bit (4-byte) parts
const left = this.getUint32(byteOffset, littleEndian)
const right = this.getUint32(byteOffset + 4, littleEndian)
// combine the two 32-bit values
const combined = littleEndian ? left + 2**32 * right : 2**32 * left + right
if (!Number.isSafeInteger(combined))
console.warn(combined, 'exceeds MAX_SAFE_INTEGER. Precision may be lost')
return combined
}
setUint64(byteOffset: number, value: number, littleEndian?: boolean) : void {
this.setBigUint64(byteOffset, BigInt(value), littleEndian)
}
getInt64(byteOffset: number, littleEndian?: boolean) : number {
return Number(this.getBigInt64(byteOffset, littleEndian))
}
setInt64(byteOffset: number, value: number, littleEndian?: boolean) : void {
this.setBigInt64(byteOffset, BigInt(value), littleEndian)
}
getShortString(byteOffset: number): [string, number] {
const len = this.getUint8(byteOffset)
byteOffset += 1
if (typeof Buffer !== "undefined") {
const text = Buffer.from(this.buffer, this.byteOffset + byteOffset, len).toString()
return [text, len + 1]
} else {
const view = new Uint8Array(this.buffer, this.byteOffset + byteOffset, len)
const text = AMQPView.decoder.decode(view)
return [text, len + 1]
}
}
setShortString(byteOffset: number, string: string) : number {
if (typeof Buffer !== "undefined") {
const len = Buffer.byteLength(string)
if (len > 255) throw new Error(`Short string too long, ${len} bytes: ${string.substring(0, 255)}...`)
this.setUint8(byteOffset, len)
byteOffset += 1
Buffer.from(this.buffer, this.byteOffset + byteOffset, len).write(string)
return len + 1
} else {
const utf8 = AMQPView.encoder.encode(string)
const len = utf8.byteLength
if (len > 255) throw new Error(`Short string too long, ${len} bytes: ${string.substring(0, 255)}...`)
this.setUint8(byteOffset, len)
byteOffset += 1
const view = new Uint8Array(this.buffer, this.byteOffset + byteOffset)
view.set(utf8)
return len + 1
}
}
getLongString(byteOffset: number, littleEndian?: boolean): [string, number] {
const len = this.getUint32(byteOffset, littleEndian)
byteOffset += 4
if (typeof Buffer !== "undefined") {
const text = Buffer.from(this.buffer, this.byteOffset + byteOffset, len).toString()
return [text, len + 4]
} else {
const view = new Uint8Array(this.buffer, this.byteOffset + byteOffset, len)
const text = AMQPView.decoder.decode(view)
return [text, len + 4]
}
}
setLongString(byteOffset: number, string: string, littleEndian?: boolean) : number {
if (typeof Buffer !== "undefined") {
const len = Buffer.byteLength(string)
this.setUint32(byteOffset, len, littleEndian)
byteOffset += 4
Buffer.from(this.buffer, this.byteOffset + byteOffset, len).write(string)
return len + 4
} else {
const utf8 = AMQPView.encoder.encode(string)
const len = utf8.byteLength
this.setUint32(byteOffset, len, littleEndian)
byteOffset += 4
const view = new Uint8Array(this.buffer, this.byteOffset + byteOffset)
view.set(utf8)
return len + 4
}
}
getProperties(byteOffset: number, littleEndian?: boolean): [AMQPProperties, number] {
let j = byteOffset
const flags = this.getUint16(j, littleEndian); j += 2
const props: AMQPProperties = {}
if ((flags & 0x8000) > 0) {
const [contentType, len] = this.getShortString(j); j += len
props.contentType = contentType
}
if ((flags & 0x4000) > 0) {
const [contentEncoding, len] = this.getShortString(j); j += len
props.contentEncoding = contentEncoding
}
if ((flags & 0x2000) > 0) {
const [headers, len] = this.getTable(j, littleEndian); j += len
props.headers = headers
}
if ((flags & 0x1000) > 0) {
props.deliveryMode = this.getUint8(j); j += 1
}
if ((flags & 0x0800) > 0) {
props.priority = this.getUint8(j); j += 1
}
if ((flags & 0x0400) > 0) {
const [correlationId, len] = this.getShortString(j); j += len
props.correlationId = correlationId
}
if ((flags & 0x0200) > 0) {
const [replyTo, len] = this.getShortString(j); j += len
props.replyTo = replyTo
}
if ((flags & 0x0100) > 0) {
const [expiration, len] = this.getShortString(j); j += len
props.expiration = expiration
}
if ((flags & 0x0080) > 0) {
const [messageId, len] = this.getShortString(j); j += len
props.messageId = messageId
}
if ((flags & 0x0040) > 0) {
props.timestamp = new Date(this.getInt64(j, littleEndian) * 1000); j += 8
}
if ((flags & 0x0020) > 0) {
const [type, len] = this.getShortString(j); j += len
props.type = type
}
if ((flags & 0x0010) > 0) {
const [userId, len] = this.getShortString(j); j += len
props.userId = userId
}
if ((flags & 0x0008) > 0) {
const [appId, len] = this.getShortString(j); j += len
props.appId = appId
}
const len = j - byteOffset
return [props, len]
}
setProperties(byteOffset: number, properties: AMQPProperties, littleEndian?: boolean): number {
let j = byteOffset
let flags = 0
if (properties.contentType) flags = flags | 0x8000
if (properties.contentEncoding) flags = flags | 0x4000
if (properties.headers) flags = flags | 0x2000
if (properties.deliveryMode) flags = flags | 0x1000
if (properties.priority) flags = flags | 0x0800
if (properties.correlationId) flags = flags | 0x0400
if (properties.replyTo) flags = flags | 0x0200
if (properties.expiration) flags = flags | 0x0100
if (properties.messageId) flags = flags | 0x0080
if (properties.timestamp) flags = flags | 0x0040
if (properties.type) flags = flags | 0x0020
if (properties.userId) flags = flags | 0x0010
if (properties.appId) flags = flags | 0x0008
this.setUint16(j, flags, littleEndian)
j += 2
if (properties.contentType) {
j += this.setShortString(j, properties.contentType)
}
if (properties.contentEncoding) {
j += this.setShortString(j, properties.contentEncoding)
}
if (properties.headers) {
j += this.setTable(j, properties.headers)
}
if (properties.deliveryMode) {
this.setUint8(j, properties.deliveryMode); j += 1
}
if (properties.priority) {
this.setUint8(j, properties.priority); j += 1
}
if (properties.correlationId) {
j += this.setShortString(j, properties.correlationId)
}
if (properties.replyTo) {
j += this.setShortString(j, properties.replyTo)
}
if (properties.expiration) {
j += this.setShortString(j, properties.expiration)
}
if (properties.messageId) {
j += this.setShortString(j, properties.messageId)
}
if (properties.timestamp) { // Date
const unixEpoch = Math.floor(Number(properties.timestamp) / 1000)
this.setInt64(j, unixEpoch, littleEndian); j += 8
}
if (properties.type) {
j += this.setShortString(j, properties.type)
}
if (properties.userId) {
j += this.setShortString(j, properties.userId)
}
if (properties.appId) {
j += this.setShortString(j, properties.appId)
}
const len = j - byteOffset
return len
}
getTable(byteOffset: number, littleEndian?: boolean): [Record<string, Field>, number] {
const table: Record<string, Field> = {}
let i = byteOffset
const len = this.getUint32(byteOffset, littleEndian); i += 4
for (; i < byteOffset + 4 + len;) {
const [k, strLen] = this.getShortString(i); i += strLen
const [v, vLen] = this.getField(i, littleEndian); i += vLen
table[k] = v
}
return [table, len + 4]
}
setTable(byteOffset: number, table : Record<string, Field>, littleEndian?: boolean) : number {
// skip the first 4 bytes which are for the size
let i = byteOffset + 4
for (const [key, value] of Object.entries(table)) {
if (value === undefined) continue
i += this.setShortString(i, key)
i += this.setField(i, value, littleEndian)
}
this.setUint32(byteOffset, i - byteOffset - 4, littleEndian) // update prefix length
return i - byteOffset
}
getField(byteOffset: number, littleEndian?: boolean): [Field, number] {
let i = byteOffset
const k = this.getUint8(i); i += 1
const type = String.fromCharCode(k)
let v
let len
switch (type) {
case 't': v = this.getUint8(i) === 1; i += 1; break
case 'b': v = this.getInt8(i); i += 1; break
case 'B': v = this.getUint8(i); i += 1; break
case 's': v = this.getInt16(i, littleEndian); i += 2; break
case 'u': v = this.getUint16(i, littleEndian); i += 2; break
case 'I': v = this.getInt32(i, littleEndian); i += 4; break
case 'i': v = this.getUint32(i, littleEndian); i += 4; break
case 'l': v = this.getInt64(i, littleEndian); i += 8; break
case 'f': v = this.getFloat32(i, littleEndian); i += 4; break
case 'd': v = this.getFloat64(i, littleEndian); i += 8; break
case 'S': [v, len] = this.getLongString(i, littleEndian); i += len; break
case 'F': [v, len] = this.getTable(i, littleEndian); i += len; break
case 'A': [v, len] = this.getArray(i, littleEndian); i += len; break
case 'x': [v, len] = this.getByteArray(i, littleEndian); i += len; break
case 'T': v = new Date(this.getInt64(i, littleEndian) * 1000); i += 8; break
case 'V': v = null; break
case 'D': {
const scale = this.getUint8(i); i += 1
const value = this.getUint32(i, littleEndian); i += 4
v = value / 10**scale
break
}
default:
throw `Field type '${k}' not supported`
}
return [v, i - byteOffset]
}
setField(byteOffset: number, field: Field,
littleEndian?: boolean) : number {
let i = byteOffset
switch (typeof field) {
case "string":
this.setUint8(i, 'S'.charCodeAt(0)); i += 1
i += this.setLongString(i, field as string, littleEndian)
break
case "boolean":
this.setUint8(i, 't'.charCodeAt(0)); i += 1
this.setUint8(i, field ? 1 : 0); i += 1
break
case "bigint":
this.setUint8(i, 'l'.charCodeAt(0)); i += 1
this.setBigInt64(i, field as bigint, littleEndian); i += 8
break
case "number":
if (Number.isInteger(field)) {
if (-(2**32) < field && field < 2**32) {
this.setUint8(i, 'I'.charCodeAt(0)); i += 1
this.setInt32(i, field, littleEndian); i += 4
} else {
this.setUint8(i, 'l'.charCodeAt(0)); i += 1
this.setInt64(i, field, littleEndian); i += 8
}
} else { // float
if (-(2**32) < field && field < 2**32) {
this.setUint8(i, 'f'.charCodeAt(0)); i += 1
this.setFloat32(i, field, littleEndian); i += 4
} else {
this.setUint8(i, 'd'.charCodeAt(0)); i += 1
this.setFloat64(i, field, littleEndian); i += 8
}
}
break
case "object":
if (Array.isArray(field)) {
this.setUint8(i, 'A'.charCodeAt(0)); i += 1
i += this.setArray(i, field, littleEndian)
} else if (field instanceof Uint8Array) {
this.setUint8(i, 'x'.charCodeAt(0)); i += 1
i += this.setByteArray(i, field)
} else if (field instanceof ArrayBuffer) {
this.setUint8(i, 'x'.charCodeAt(0)); i += 1
i += this.setByteArray(i, new Uint8Array(field))
} else if (field instanceof Date) {
this.setUint8(i, 'T'.charCodeAt(0)); i += 1
const unixEpoch = Math.floor(Number(field) / 1000)
this.setInt64(i, unixEpoch, littleEndian); i += 8
} else if (field === null || field === undefined) {
this.setUint8(i, 'V'.charCodeAt(0)); i += 1
} else { // hopefully it's a hash like object
this.setUint8(i, 'F'.charCodeAt(0)); i += 1
i += this.setTable(i, field as Record<string, Field>, littleEndian)
}
break
default:
throw `Unsupported field type '${field}'`
}
return i - byteOffset
}
getArray(byteOffset: number, littleEndian?: boolean): [Field[], number] {
const len = this.getUint32(byteOffset, littleEndian); byteOffset += 4
const endOffset = byteOffset + len
const v = []
for (; byteOffset < endOffset;) {
const [field, fieldLen] = this.getField(byteOffset, littleEndian); byteOffset += fieldLen
v.push(field)
}
return [v, len + 4]
}
setArray(byteOffset: number, array: Field[], littleEndian?: boolean) : number {
const start = byteOffset
byteOffset += 4 // update the length later
array.forEach((e) => {
byteOffset += this.setField(byteOffset, e, littleEndian)
})
this.setUint32(start, byteOffset - start - 4, littleEndian) // update length
return byteOffset - start
}
getByteArray(byteOffset: number, littleEndian?: boolean): [Uint8Array, number] {
const len = this.getUint32(byteOffset, littleEndian); byteOffset += 4
const v = new Uint8Array(this.buffer, this.byteOffset + byteOffset, len)
return [v, len + 4]
}
setByteArray(byteOffset: number, data: Uint8Array, littleEndian?: boolean) : number {
this.setUint32(byteOffset, data.byteLength, littleEndian); byteOffset += 4
const view = new Uint8Array(this.buffer, this.byteOffset + byteOffset, data.byteLength)
view.set(data)
return data.byteLength + 4
}
}