hap-nodejs
Version:
HAP-NodeJS is a Node.js implementation of HomeKit Accessory Server.
312 lines • 10.6 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.encode = encode;
exports.decode = decode;
exports.decodeWithLists = decodeWithLists;
exports.decodeList = decodeList;
exports.readUInt64LE = readUInt64LE;
exports.writeUInt32 = writeUInt32;
exports.readUInt32 = readUInt32;
exports.writeFloat32LE = writeFloat32LE;
exports.writeUInt16 = writeUInt16;
exports.readUInt16 = readUInt16;
exports.readVariableUIntLE = readVariableUIntLE;
exports.writeVariableUIntLE = writeVariableUIntLE;
const tslib_1 = require("tslib");
const assert_1 = tslib_1.__importDefault(require("assert"));
const hapCrypto = tslib_1.__importStar(require("../util/hapCrypto"));
/**
* Type Length Value encoding/decoding, used by HAP as a wire format.
* https://en.wikipedia.org/wiki/Type-length-value
*/
const EMPTY_TLV_TYPE = 0x00; // and empty tlv with id 0 is usually used as delimiter for tlv lists
/**
* @group TLV8
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function encode(type, data, ...args) {
const encodedTLVBuffers = [];
// coerce data to Buffer if needed
if (typeof data === "number") {
data = Buffer.from([data]);
}
else if (typeof data === "string") {
data = Buffer.from(data);
}
if (Array.isArray(data)) {
let first = true;
for (const entry of data) {
if (!first) {
encodedTLVBuffers.push(Buffer.from([EMPTY_TLV_TYPE, 0])); // push delimiter
}
first = false;
encodedTLVBuffers.push(encode(type, entry));
}
if (first) { // we have a zero length array!
encodedTLVBuffers.push(Buffer.from([type, 0]));
}
}
else if (data.length <= 255) {
encodedTLVBuffers.push(Buffer.concat([Buffer.from([type, data.length]), data]));
}
else { // otherwise it doesn't fit into one tlv entry, thus we push multiple
let leftBytes = data.length;
let currentIndex = 0;
for (; leftBytes > 0;) {
if (leftBytes >= 255) {
encodedTLVBuffers.push(Buffer.concat([Buffer.from([type, 0xFF]), data.slice(currentIndex, currentIndex + 255)]));
leftBytes -= 255;
currentIndex += 255;
}
else {
encodedTLVBuffers.push(Buffer.concat([Buffer.from([type, leftBytes]), data.slice(currentIndex)]));
leftBytes -= leftBytes;
}
}
}
// do we have more arguments to encode?
if (args.length >= 2) {
// chop off the first two arguments which we already processed, and process the rest recursively
const [nextType, nextData, ...nextArgs] = args;
const remainingTLVBuffer = encode(nextType, nextData, ...nextArgs);
// append the remaining encoded arguments directly to the buffer
encodedTLVBuffers.push(remainingTLVBuffer);
}
return Buffer.concat(encodedTLVBuffers);
}
/**
* This method is the legacy way of decoding tlv data.
* It will not properly decode multiple list of the same id.
* Should the decoder encounter multiple instances of the same id, it will just concatenate the buffer data.
*
* @param buffer - TLV8 data
*
* Note: Please use {@link decodeWithLists} which properly decodes list elements.
*
* @group TLV8
*/
function decode(buffer) {
(0, assert_1.default)(buffer instanceof Buffer, "Illegal argument. tlv.decode() expects Buffer type!");
const objects = {};
let leftLength = buffer.length;
let currentIndex = 0;
for (; leftLength > 0;) {
const type = buffer[currentIndex];
const length = buffer[currentIndex + 1];
currentIndex += 2;
leftLength -= 2;
const data = buffer.slice(currentIndex, currentIndex + length);
if (objects[type]) {
objects[type] = Buffer.concat([objects[type], data]);
}
else {
objects[type] = data;
}
currentIndex += length;
leftLength -= length;
}
return objects;
}
/**
* Decode a buffer coding TLV8 encoded entries.
*
* This method decodes multiple entries split by a TLV delimiter properly into Buffer arrays.
* It properly reassembles tlv entries if they were split across multiple entries due to exceeding the max tlv entry size of 255 bytes.
* @param buffer - The Buffer containing TLV8 encoded data.
*
* @group TLV8
*/
function decodeWithLists(buffer) {
const result = {};
let leftBytes = buffer.length;
let readIndex = 0;
let lastType = -1;
let lastLength = -1;
let lastItemWasDelimiter = false;
for (; leftBytes > 0;) {
const type = buffer.readUInt8(readIndex++);
const length = buffer.readUInt8(readIndex++);
leftBytes -= 2;
const data = buffer.slice(readIndex, readIndex + length);
readIndex += length;
leftBytes -= length;
if (type === 0 && length === 0) {
lastItemWasDelimiter = true;
continue;
}
const existing = result[type];
if (existing) { // there is already an item with the same type
if (lastItemWasDelimiter && lastType === type) { // list of tlv types
if (Array.isArray(existing)) {
existing.push(data);
}
else {
result[type] = [existing, data];
}
}
else if (lastType === type && lastLength === 255) { // tlv data got split into multiple entries as length exceeded 255
if (Array.isArray(existing)) {
// append to the last data blob in the array
const last = existing[existing.length - 1];
existing[existing.length - 1] = Buffer.concat([last, data]);
}
else {
result[type] = Buffer.concat([existing, data]);
}
}
else {
throw new Error(`Found duplicated tlv entry with type ${type} and length ${length} `
+ `(lastItemWasDelimiter: ${lastItemWasDelimiter}, lastType: ${lastType}, lastLength: ${lastLength})`);
}
}
else {
result[type] = data;
}
lastType = type;
lastLength = length;
lastItemWasDelimiter = false;
}
return result;
}
/**
* This method can be used to parse a TLV8 encoded list that was concatenated.
*
* If you are thinking about using this method, try to refactor the code to use {@link decodeWithLists} instead of {@link decode}.
* The single reason of this method's existence are the shortcomings {@link decode}, as it concatenates multiple tlv8 list entries
* into a single Buffer.
* This method can be used to undo that, by specifying the concatenated buffer and the tlv id of the element that should
* mark the beginning of a new tlv8 list entry.
*
* @param data - The concatenated tlv8 list entries (probably output of {@link decode}).
* @param entryStartId - The tlv id that marks the beginning of a new tlv8 entry.
*
* @group TLV8
*/
function decodeList(data, entryStartId) {
const objectsList = [];
let leftLength = data.length;
let currentIndex = 0;
let objects = undefined;
for (; leftLength > 0;) {
const type = data[currentIndex]; // T
const length = data[currentIndex + 1]; // L
const value = data.slice(currentIndex + 2, currentIndex + 2 + length); // V
if (type === entryStartId) { // we got the start of a new entry
if (objects !== undefined) { // save the previous entry
objectsList.push(objects);
}
objects = {};
}
if (objects === undefined) {
throw new Error("Error parsing tlv list: Encountered uninitialized storage object");
}
if (objects[type]) { // append to buffer if we have already data for this type
objects[type] = Buffer.concat([objects[type], value]);
}
else {
objects[type] = value;
}
currentIndex += 2 + length;
leftLength -= 2 + length;
}
if (objects !== undefined) {
objectsList.push(objects);
} // push last entry
return objectsList;
}
/**
* @group TLV8
*/
function readUInt64LE(buffer, offset = 0) {
const low = buffer.readUInt32LE(offset);
// javascript doesn't allow to shift by 32(?), therefore we multiply here
return buffer.readUInt32LE(offset + 4) * 0x100000000 + low;
}
/**
* `writeUint32LE`
* @group TLV8
*/
function writeUInt32(value) {
const buffer = Buffer.alloc(4);
buffer.writeUInt32LE(value, 0);
return buffer;
}
/**
* `readUInt32LE`
* @group TLV8
*/
function readUInt32(buffer) {
return buffer.readUInt32LE(0);
}
/**
* @group TLV8
*/
function writeFloat32LE(value) {
const buffer = Buffer.alloc(4);
buffer.writeFloatLE(value, 0);
return buffer;
}
/**
* `writeUInt16LE`
* @group TLV8
*/
function writeUInt16(value) {
const buffer = Buffer.alloc(2);
buffer.writeUInt16LE(value, 0);
return buffer;
}
/**
* `readUInt16LE`
* @group TLV8
*/
function readUInt16(buffer) {
return buffer.readUInt16LE(0);
}
/**
* Reads variable size unsigned integer {@link writeVariableUIntLE}.
* @param buffer - The buffer to read from. It must have exactly the size of the given integer.
* @group TLV8
*/
function readVariableUIntLE(buffer) {
switch (buffer.length) {
case 1:
return buffer.readUInt8(0);
case 2:
return buffer.readUInt16LE(0);
case 4:
return buffer.readUInt32LE(0);
case 8:
return readUInt64LE(buffer, 0);
default:
throw new Error("Can't read uint LE with length " + buffer.length);
}
}
/**
* Writes variable size unsigned integer.
* Either:
* - `UInt8`
* - `UInt16LE`
* - `UInt32LE`
* @param number
* @group TLV8
*/
function writeVariableUIntLE(number) {
(0, assert_1.default)(number >= 0, "Can't encode a negative integer as unsigned integer");
if (number <= 255) {
const buffer = Buffer.alloc(1);
buffer.writeUInt8(number, 0);
return buffer;
}
else if (number <= 65535) {
return writeUInt16(number);
}
else if (number <= 4294967295) {
return writeUInt32(number);
}
else {
const buffer = Buffer.alloc(8);
hapCrypto.writeUInt64LE(number, buffer, 0);
return buffer;
}
}
//# sourceMappingURL=tlv.js.map
;