dbus-sdk
Version:
A Node.js SDK for interacting with DBus, enabling seamless service calling and exposure with TypeScript support
587 lines (586 loc) • 28.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DBusBufferEncoder = void 0;
const DBusMessageEndianness_1 = require("./enums/DBusMessageEndianness");
const Errors_1 = require("./Errors");
const DBusSignedValue_1 = require("./DBusSignedValue");
/**
* A class for encoding data into a binary buffer following the DBus wire format.
* Supports various DBus data types with proper alignment and endianness handling.
* This class provides methods to encode basic types (e.g., integers, strings) and
* container types (e.g., arrays, structs) as per the DBus specification.
*/
class DBusBufferEncoder {
/**
* Constructor for DBusBufferEncoder.
* Initializes the encoder with the specified endianness and an optional initial buffer.
* Optionally aligns the buffer to a specified boundary if provided.
*
* @param endianness - The byte order for encoding (little-endian or big-endian, default: Little Endian).
* @param initBuffer - An initial buffer to start with, if any (default: empty buffer).
* @param alignment - An initial alignment requirement, if specified, to align the buffer start.
*/
constructor(endianness = DBusMessageEndianness_1.DBusMessageEndianness.LE, initBuffer, alignment) {
/**
* The endianness used for encoding DBus messages.
* Defaults to little-endian (LE) as it is the most common in DBus implementations.
* Determines the byte order for multi-byte values in the message.
*/
this.endianness = DBusMessageEndianness_1.DBusMessageEndianness.LE;
this.endianness = endianness;
this.buffer = initBuffer ? initBuffer : Buffer.alloc(0);
if (alignment)
this.align(alignment);
}
/**
* Aligns the buffer to the specified byte boundary.
* Adds padding bytes (zeros) to ensure the buffer length meets the alignment requirement,
* which is necessary for certain DBus data types.
*
* @param alignment - The byte boundary to align to (e.g., 1, 2, 4, 8).
* @returns The instance itself for method chaining.
* @protected
*/
align(alignment) {
// Calculate the remainder and required padding for alignment
const remainder = this.buffer.length % alignment;
if (remainder === 0)
return this; // Buffer is already aligned, no action needed
const padding = alignment - remainder;
const paddingBuffer = Buffer.alloc(padding, 0); // Create padding with zeros
this.buffer = Buffer.concat([this.buffer, paddingBuffer]); // Append padding to the buffer
return this;
}
/**
* Encodes a BYTE type value into the buffer.
* BYTE is an 8-bit unsigned integer with no specific alignment requirement (1-byte alignment).
*
* @param value - The byte value to encode (0-255).
* @returns The instance itself for method chaining.
*/
writeByte(value) {
this.align(1);
const buffer = Buffer.alloc(1);
buffer.writeUInt8(value & 0xFF, 0); // Ensure value is in the range 0-255
this.buffer = Buffer.concat([this.buffer, buffer]);
return this;
}
/**
* Encodes a BOOLEAN type value into the buffer.
* BOOLEAN is stored as a 32-bit unsigned integer (0 for false, 1 for true) and requires 4-byte alignment.
*
* @param value - The boolean value to encode (true or false).
* @returns The instance itself for method chaining.
*/
writeBoolean(value) {
this.align(4);
const buffer = Buffer.alloc(4); // BOOLEAN occupies 4 bytes
const intValue = value ? 1 : 0; // Encode true as 1, false as 0
if (this.endianness === DBusMessageEndianness_1.DBusMessageEndianness.LE) {
buffer.writeUInt32LE(intValue, 0); // Little-endian byte order
}
else {
buffer.writeUInt32BE(intValue, 0); // Big-endian byte order
}
this.buffer = Buffer.concat([this.buffer, buffer]);
return this;
}
/**
* Encodes an INT16 type value into the buffer.
* INT16 is a 16-bit signed integer and requires 2-byte alignment.
*
* @param value - The 16-bit signed integer value to encode.
* @returns The instance itself for method chaining.
*/
writeInt16(value) {
this.align(2);
const buffer = Buffer.alloc(2); // INT16 occupies 2 bytes
if (this.endianness === DBusMessageEndianness_1.DBusMessageEndianness.LE) {
buffer.writeInt16LE(value, 0); // Little-endian byte order
}
else {
buffer.writeInt16BE(value, 0); // Big-endian byte order
}
this.buffer = Buffer.concat([this.buffer, buffer]);
return this;
}
/**
* Encodes a UINT16 type value into the buffer.
* UINT16 is a 16-bit unsigned integer and requires 2-byte alignment.
*
* @param value - The 16-bit unsigned integer value to encode.
* @returns The instance itself for method chaining.
*/
writeUInt16(value) {
this.align(2);
const buffer = Buffer.alloc(2); // UINT16 occupies 2 bytes
if (this.endianness === DBusMessageEndianness_1.DBusMessageEndianness.LE) {
buffer.writeUInt16LE(value, 0); // Little-endian byte order
}
else {
buffer.writeUInt16BE(value, 0); // Big-endian byte order
}
this.buffer = Buffer.concat([this.buffer, buffer]);
return this;
}
/**
* Encodes an INT32 type value into the buffer.
* INT32 is a 32-bit signed integer and requires 4-byte alignment.
*
* @param value - The 32-bit signed integer value to encode.
* @returns The instance itself for method chaining.
*/
writeInt32(value) {
this.align(4);
const buffer = Buffer.alloc(4); // INT32 occupies 4 bytes
if (this.endianness === DBusMessageEndianness_1.DBusMessageEndianness.LE) {
buffer.writeInt32LE(value, 0); // Little-endian byte order
}
else {
buffer.writeInt32BE(value, 0); // Big-endian byte order
}
this.buffer = Buffer.concat([this.buffer, buffer]);
return this;
}
/**
* Encodes a UINT32 type value into the buffer.
* UINT32 is a 32-bit unsigned integer and requires 4-byte alignment.
*
* @param value - The 32-bit unsigned integer value to encode.
* @returns The instance itself for method chaining.
*/
writeUInt32(value) {
this.align(4);
const buffer = Buffer.alloc(4); // UINT32 occupies 4 bytes
if (this.endianness === DBusMessageEndianness_1.DBusMessageEndianness.LE) {
buffer.writeUInt32LE(value, 0); // Little-endian byte order
}
else {
buffer.writeUInt32BE(value, 0); // Big-endian byte order
}
this.buffer = Buffer.concat([this.buffer, buffer]);
return this;
}
/**
* Encodes an INT64 type value into the buffer.
* INT64 is a 64-bit signed integer and requires 8-byte alignment.
*
* @param value - The 64-bit signed integer value to encode, provided as a bigint.
* @returns The instance itself for method chaining.
*/
writeInt64(value) {
this.align(8);
const buffer = Buffer.alloc(8); // INT64 occupies 8 bytes
if (this.endianness === DBusMessageEndianness_1.DBusMessageEndianness.LE) {
buffer.writeBigInt64LE(value, 0); // Little-endian byte order
}
else {
buffer.writeBigInt64BE(value, 0); // Big-endian byte order
}
this.buffer = Buffer.concat([this.buffer, buffer]);
return this;
}
/**
* Encodes a UINT64 type value into the buffer.
* UINT64 is a 64-bit unsigned integer and requires 8-byte alignment.
*
* @param value - The 64-bit unsigned integer value to encode, provided as a bigint.
* @returns The instance itself for method chaining.
*/
writeUInt64(value) {
this.align(8);
const buffer = Buffer.alloc(8); // UINT64 occupies 8 bytes
if (this.endianness === DBusMessageEndianness_1.DBusMessageEndianness.LE) {
buffer.writeBigUInt64LE(value, 0); // Little-endian byte order
}
else {
buffer.writeBigUInt64BE(value, 0); // Big-endian byte order
}
this.buffer = Buffer.concat([this.buffer, buffer]);
return this;
}
/**
* Encodes a DOUBLE type value into the buffer.
* DOUBLE is a 64-bit double-precision floating-point number and requires 8-byte alignment.
*
* @param value - The double-precision floating-point value to encode.
* @returns The instance itself for method chaining.
*/
writeDouble(value) {
this.align(8);
const buffer = Buffer.alloc(8); // DOUBLE occupies 8 bytes
if (this.endianness === DBusMessageEndianness_1.DBusMessageEndianness.LE) {
buffer.writeDoubleLE(value, 0); // Little-endian byte order
}
else {
buffer.writeDoubleBE(value, 0); // Big-endian byte order
}
this.buffer = Buffer.concat([this.buffer, buffer]);
return this;
}
/**
* Encodes a UNIX_FD type value into the buffer.
* UNIX_FD is a 32-bit unsigned integer representing a file descriptor index and requires 4-byte alignment.
*
* @param fdIndex - The file descriptor index to encode.
* @returns The instance itself for method chaining.
*/
writeUnixFD(fdIndex) {
this.align(4);
const buffer = Buffer.alloc(4); // UNIX_FD occupies 4 bytes
if (this.endianness === DBusMessageEndianness_1.DBusMessageEndianness.LE) {
buffer.writeUInt32LE(fdIndex, 0); // Little-endian byte order
}
else {
buffer.writeUInt32BE(fdIndex, 0); // Big-endian byte order
}
this.buffer = Buffer.concat([this.buffer, buffer]);
return this;
}
/**
* Encodes a STRING type value into the buffer.
* STRING consists of a 32-bit length field followed by UTF-8 encoded characters and a null terminator.
* The length field requires 4-byte alignment.
*
* @param value - The string value to encode.
* @returns The instance itself for method chaining.
*/
writeString(value) {
this.align(4);
const stringBuffer = Buffer.from(value, 'utf8'); // Convert string to UTF-8 encoded buffer
const length = stringBuffer.length; // Get byte length of the string
const totalLength = 4 + length + 1; // 4-byte length field + string content + 1-byte null terminator
const buffer = Buffer.alloc(totalLength); // Allocate buffer for total length
// Write length field as a 32-bit unsigned integer
if (this.endianness === DBusMessageEndianness_1.DBusMessageEndianness.LE) {
buffer.writeUInt32LE(length, 0); // Little-endian byte order
}
else {
buffer.writeUInt32BE(length, 0); // Big-endian byte order
}
// Write string content
stringBuffer.copy(buffer, 4); // Copy string content starting at offset 4
// Write null terminator at the end
buffer.writeUInt8(0, 4 + length); // Write null byte after string content
this.buffer = Buffer.concat([this.buffer, buffer]);
return this;
}
/**
* Encodes an OBJECT_PATH type value into the buffer.
* OBJECT_PATH is a string with specific formatting rules, stored like STRING with a 32-bit length field.
* The length field requires 4-byte alignment.
*
* @param value - The object path string to encode.
* @returns The instance itself for method chaining.
* @throws {ObjectPathError} If the object path does not conform to DBus specification formatting rules.
*/
writeObjectPath(value) {
// Validate object path format according to DBus specification
/**
* Fix bug: Invalid DBus Object Path Validation - Incorrectly Rejects Digit-Starting Elements
* @see https://github.com/myq1991/node-dbus-sdk/issues/2
*/
const objectPathRegex = /^\/(?:[a-zA-Z0-9_]+(?:\/[a-zA-Z0-9_]+)*)?$/;
if (!objectPathRegex.test(value))
throw new Errors_1.ObjectPathError(`Invalid DBus object path: "${value}". Object path must start with '/' and consist of elements separated by '/', where each element starts with a letter or underscore and contains only letters, numbers, or underscores.`);
this.align(4);
const pathBuffer = Buffer.from(value, 'utf8'); // Convert path to UTF-8 encoded buffer
const length = pathBuffer.length; // Get byte length of the path
const totalLength = 4 + length + 1; // 4-byte length field + path content + 1-byte null terminator
const buffer = Buffer.alloc(totalLength); // Allocate buffer for total length
// Write length field as a 32-bit unsigned integer
if (this.endianness === DBusMessageEndianness_1.DBusMessageEndianness.LE) {
buffer.writeUInt32LE(length, 0); // Little-endian byte order
}
else {
buffer.writeUInt32BE(length, 0); // Big-endian byte order
}
// Write path content
pathBuffer.copy(buffer, 4); // Copy path content starting at offset 4
// Write null terminator at the end
buffer.writeUInt8(0, 4 + length); // Write null byte after path content
this.buffer = Buffer.concat([this.buffer, buffer]);
return this;
}
/**
* Encodes a SIGNATURE type value into the buffer.
* SIGNATURE is a string of type codes with a 1-byte length field and no specific alignment requirement.
*
* @param value - The signature string to encode.
* @returns The instance itself for method chaining.
* @throws {SignatureError} If the signature length exceeds the maximum allowed (255 bytes).
*/
writeSignature(value) {
this.align(1);
const signatureBuffer = Buffer.from(value, 'utf8'); // Convert signature string to UTF-8 encoded buffer
const length = signatureBuffer.length; // Get byte length of the signature string
// Validate signature length does not exceed 255 bytes (SIGNATURE length field is 8-bit unsigned integer)
if (length > 255)
throw new Errors_1.SignatureError(`DBus signature length exceeds maximum of 255 bytes: "${value}"`);
const totalLength = 1 + length + 1; // 1-byte length field + signature content + 1-byte null terminator
const buffer = Buffer.alloc(totalLength); // Allocate buffer for total length
// Write length field as an 8-bit unsigned integer
buffer.writeUInt8(length, 0);
// Write signature string content
signatureBuffer.copy(buffer, 1); // Copy signature content starting at offset 1
// Write null terminator at the end
buffer.writeUInt8(0, 1 + length); // Write null byte after signature content
this.buffer = Buffer.concat([this.buffer, buffer]);
return this;
}
/**
* Encodes an ARRAY type value into the buffer.
* ARRAY starts with a 32-bit length field (total byte length of array data) and requires 4-byte alignment.
* Additional alignment may be needed for specific array element types (e.g., dictionary entries).
*
* @param signedValues - Array elements, each associated with a signature as DBusSignedValue instances.
* @param arrayItemSignature - Optional signature of array elements to determine additional alignment needs.
* @returns The instance itself for method chaining.
*/
writeArray(signedValues, arrayItemSignature) {
this.align(4);
// Create a temporary encoder to encode array content and calculate its length
const contentEncoder = new DBusBufferEncoder(this.endianness, Buffer.alloc(0), 1);
// Encode each element in the array
for (const signedValue of signedValues) {
contentEncoder.writeSignedValue(signedValue);
}
// Get the byte length of the encoded array content
const contentBuffer = contentEncoder.getBuffer();
const contentLength = contentBuffer.length;
// Write length field as a 4-byte unsigned integer
const lengthBuffer = Buffer.alloc(4);
if (this.endianness === DBusMessageEndianness_1.DBusMessageEndianness.LE) {
lengthBuffer.writeUInt32LE(contentLength, 0); // Little-endian byte order
}
else {
lengthBuffer.writeUInt32BE(contentLength, 0); // Big-endian byte order
}
// Append length field to the main buffer
this.buffer = Buffer.concat([this.buffer, lengthBuffer]);
if (arrayItemSignature)
switch (arrayItemSignature) {
case '{':
case '(':
this.align(8); // Special alignment for dictionary entries or structs
}
// Append array content to the main buffer
this.buffer = Buffer.concat([this.buffer, contentBuffer]);
return this;
}
/**
* Encodes a STRUCT type value into the buffer.
* STRUCT is a sequence of fields and requires 8-byte alignment at the start.
*
* @param signedValues - Struct fields, each associated with a signature as DBusSignedValue instances.
* @returns The instance itself for method chaining.
*/
writeStruct(signedValues) {
this.align(8);
// Encode each field of the struct in sequence
for (const signedValue of signedValues) {
this.writeSignedValue(signedValue);
}
return this;
}
/**
* Encodes a DICT_ENTRY type value into the buffer.
* DICT_ENTRY is a key-value pair used in dictionaries and requires 8-byte alignment at the start.
*
* @param signedValues - Dictionary entry as a key-value pair, must contain exactly two elements (key and value), each as a DBusSignedValue.
* @returns The instance itself for method chaining.
* @throws {SignatureError} If the dictionary entry does not contain exactly two elements.
*/
writeDictEntry(signedValues) {
this.align(8);
// Ensure dictionary entry contains exactly two elements (key and value)
if (signedValues.length !== 2) {
throw new Errors_1.SignatureError(`Dictionary entry must contain exactly 2 elements (key and value), got ${signedValues.length}`);
}
// Encode key and value in sequence
this.writeSignedValue(signedValues[0]); // Encode key
this.writeSignedValue(signedValues[1]); // Encode value
return this;
}
/**
* Encodes a VARIANT type value into the buffer.
* VARIANT is a dynamic type container with a signature field followed by data, requiring no specific alignment (1-byte alignment).
*
* @param signedValue - Variant value, associated with a signature as a DBusSignedValue.
* @returns The instance itself for method chaining.
*/
writeVariant(signedValue) {
this.align(1);
// Reconstruct the complete signature string for the variant's internal value
const signature = this.buildSignature(signedValue);
// Write the type signature of the variant
this.writeSignature(signature);
// Write the actual content of the variant
this.writeSignedValue(signedValue);
return this;
}
/**
* Builds the complete signature string for a given signed value.
* Recursively handles nested structures like arrays, structs, dictionaries, and variants.
*
* @param signedValue - The signed value to build a signature for.
* @returns The complete signature string representing the type structure of the value.
* @throws {SignatureError} If the signature cannot be built due to invalid or unsupported types.
* @private
*/
buildSignature(signedValue) {
// Basic types return their signature directly
const basicTypes = ['y', 'b', 'n', 'q', 'u', 'i', 'g', 's', 'o', 'x', 't', 'd', 'h'];
if (basicTypes.includes(signedValue.$signature)) {
return signedValue.$signature;
}
// Handle container types recursively
switch (signedValue.$signature) {
case 'a': {
// Array: Check if the array is empty or elements have consistent signatures
const values = signedValue.$value;
if (values.length === 0) {
throw new Errors_1.SignatureError('Cannot build signature for empty array in variant');
}
// Get the signature of the first element
const firstElementSignature = this.buildSignature(values[0]);
// Check if all elements have the same signature
const isConsistent = values.every(val => this.buildSignature(val) === firstElementSignature);
if (isConsistent) {
// If consistent, return 'a' followed by the element's signature
return `a${firstElementSignature}`;
}
else {
// If inconsistent, check if it matches a dictionary structure like 'a{sv}'
// Assume dictionary array if elements are dict entries with key-value pairs
const isDictArray = values.every(val => val.$signature === '{');
if (isDictArray) {
// Check the structure of the dictionary entries
const firstDictEntry = values[0].$value;
if (firstDictEntry.length === 2) {
// Get signatures of key and value from the first entry
const firstKeySignature = this.buildSignature(firstDictEntry[0]);
const firstValueSignature = this.buildSignature(firstDictEntry[1]);
// Check if all dictionary entries have consistent key signatures
const areKeysConsistent = values.every(val => {
const [key] = val.$value;
return this.buildSignature(key) === firstKeySignature;
});
// Check if all dictionary entries have consistent value signatures
const areValuesConsistent = values.every(val => {
const [, value] = val.$value;
return this.buildSignature(value) === firstValueSignature;
});
// Build the dictionary signature based on consistency
const keySignature = areKeysConsistent ? firstKeySignature : 'v';
const valueSignature = areValuesConsistent ? firstValueSignature : 'v';
return `a{${keySignature}${valueSignature}}`;
}
}
// If not a consistent dictionary array or other recognized structure, throw error
throw new Errors_1.SignatureError('Cannot build signature for array with inconsistent element types in variant');
}
}
case '(': {
// Struct: signature is '(' followed by field signatures and ')'
const fieldSignedValues = signedValue.$value;
const fieldSignatures = fieldSignedValues.map(field => this.buildSignature(field)).join('');
return `(${fieldSignatures})`;
}
case '{': {
// Dictionary entry: signature is '{' followed by key and value signatures and '}'
const [keySignedValue, valueSignedValue] = signedValue.$value;
const keySignature = this.buildSignature(keySignedValue);
const valueSignature = this.buildSignature(valueSignedValue);
return `{${keySignature}${valueSignature}}`;
}
case 'v': {
// Variant: signature is 'v' directly, not the internal value's signature
return 'v';
}
default:
throw new Errors_1.SignatureError(`Cannot build signature for unsupported type: ${signedValue.$signature}`);
}
}
/**
* Encodes a value based on its DBus type signature.
* Routes the encoding to the appropriate method based on the signature of the provided DBusSignedValue.
*
* @param signedValue - The value to encode, associated with a DBus signature as a DBusSignedValue.
* @returns The instance itself for method chaining.
* @throws {SignatureError} If the type signature is unsupported.
*/
writeSignedValue(signedValue) {
// Route encoding based on the signature type
switch (signedValue.$signature) {
// Basic data types
case 'y':
return this.writeByte(signedValue.$value);
case 'b':
return this.writeBoolean(signedValue.$value);
case 'n':
return this.writeInt16(signedValue.$value);
case 'q':
return this.writeUInt16(signedValue.$value);
case 'u':
return this.writeUInt32(signedValue.$value);
case 'i':
return this.writeInt32(signedValue.$value);
case 'g':
return this.writeSignature(signedValue.$value);
case 's':
return this.writeString(signedValue.$value);
case 'o':
return this.writeObjectPath(signedValue.$value);
case 'x':
return this.writeInt64(BigInt(signedValue.$value));
case 't':
return this.writeUInt64(BigInt(signedValue.$value));
case 'd':
return this.writeDouble(signedValue.$value);
case 'h':
return this.writeUnixFD(signedValue.$value);
// Container data types
case 'a':
return this.writeArray(signedValue.$value, signedValue.$arrayItemSignature);
case '(':
return this.writeStruct(signedValue.$value);
case '{':
return this.writeDictEntry(signedValue.$value);
case 'v':
return this.writeVariant(signedValue.$value);
default:
throw new Errors_1.SignatureError(`Unsupported type: ${signedValue.$signature}`);
}
}
/**
* Retrieves the current encoded buffer.
* Returns the buffer containing all data encoded so far.
*
* @returns The current buffer with encoded data.
*/
getBuffer() {
return this.buffer;
}
/**
* Encodes a value or set of values based on a DBus signature.
* Parses the input value(s) into DBusSignedValue instances based on the signature and encodes them into the buffer.
*
* @param signature - The DBus signature defining the type(s) of the value(s) to encode.
* @param value - The value(s) to encode, can be raw data or already wrapped as DBusSignedValue(s).
* @param debug - If true, logs the parsed DBusSignedValue instances for debugging purposes (default: false).
* @returns The encoded buffer containing the data.
*/
encode(signature, value, debug = false) {
// Parse the input value(s) into signed values based on the signature
const signedValues = DBusSignedValue_1.DBusSignedValue.parse(signature, value);
if (debug)
console.log(JSON.stringify(signedValues, null, 2));
// Encode each signed value
for (const signedValue of signedValues) {
this.writeSignedValue(signedValue);
}
return this.buffer;
}
}
exports.DBusBufferEncoder = DBusBufferEncoder;