From 42cc64e86ef3352e0c7fb108cf4d7b9a404b1ec3 Mon Sep 17 00:00:00 2001 From: taqin Date: Tue, 30 Jun 2026 06:27:01 +0700 Subject: [PATCH] @ fix: message encode crash, DCS-aware decode, user-defined TLV round-trip - #229/#256: tolerate non-string short_message (number/null/undefined) in the message encode filter instead of throwing "Cannot read properties of undefined (reading message)" - #66/#252: resolve decode charset from the full Data Coding Scheme byte (GSM 03.38 message-class / MWI groups) instead of the naive low-nibble mask - #231: user-defined / unknown TLVs addressed by numeric tag id now round-trip on encode (PDU constructor + toBuffer), not only on decode Adds regression tests (test/fixes.js). @ --- lib/defs.js | 27 ++++++++++++++++++++- lib/pdu.js | 32 +++++++++++++++++++++++++ test/fixes.js | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 test/fixes.js diff --git a/lib/defs.js b/lib/defs.js index a1657ee..ac9debb 100644 --- a/lib/defs.js +++ b/lib/defs.js @@ -505,6 +505,14 @@ filters.message = { if (Buffer.isBuffer(value)) { return value; } + // Tolerate non-string scalars (e.g. a numeric short_message) and + // null/undefined, which previously threw "Cannot read properties of + // undefined (reading 'message')" (issues #229, #256). + if (value === null || value === undefined) { + value = ''; + } else if (typeof value === 'number' || typeof value === 'boolean') { + value = String(value); + } var message = typeof value === 'string' ? value : value.message; if (typeof message === 'string' && message) { var encoded = false; @@ -548,7 +556,24 @@ filters.message = { if (!Buffer.isBuffer(value) || !('data_coding' in this)) { return value; } - var encoding = this.data_coding & 0x0F; + // Resolve the encoding from the full Data Coding Scheme (DCS) byte, not + // just the low nibble. The naive `data_coding & 0x0F` is wrong when the + // DCS carries message-class / MWI / compression bits in the high nibble + // (GSM 03.38), causing mis-decoded / empty messages (issues #66, #252). + var dc = this.data_coding; + var enc = 0; + if (dc <= 0x0E) { + enc = dc & 0x0F; + } else if ((dc & 0xF0) === 0xF0) { + enc = (dc & 0x04) ? consts.ENCODING.BINARY : consts.ENCODING.ASCII; + } else if ((dc & 0xC0) === 0x00) { + var mode = (dc & 0x0C) >> 2; + enc = mode === 1 ? consts.ENCODING.BINARY : + mode === 2 ? consts.ENCODING.UCS2 : consts.ENCODING.ASCII; + } else if ((dc & 0xC0) === 0xC0) { + enc = ((dc & 0xE0) === 0xE0) ? consts.ENCODING.UCS2 : consts.ENCODING.ASCII; + } + var encoding = enc; if (!encoding) { encoding = encodings.default; } else { diff --git a/lib/pdu.js b/lib/pdu.js index 89450f7..8b22152 100644 --- a/lib/pdu.js +++ b/lib/pdu.js @@ -12,6 +12,22 @@ var pduHeadParams = [ 'sequence_number' ]; +// A user-defined / unknown TLV is addressed by its numeric tag id (e.g. 0x3C02) +// which becomes a numeric-string key on the PDU. Treat such keys as raw TLVs so +// they round-trip on encode, not only on decode (issue #231). +function isRawTlvKey(key, params) { + if (!/^[0-9]+$/.test(key)) return false; + var id = Number(key); + if (id < 0 || id > 0xFFFF) return false; + return !(key in params) && !tlvs[key]; +} + +function toRawTlvBuffer(value) { + if (Buffer.isBuffer(value)) return value; + if (value === null || value === undefined) return Buffer.alloc(0); + return Buffer.from(String(value), 'ascii'); +} + function PDU(command, options) { if (Buffer.isBuffer(command)) { return this.fromBuffer(command); @@ -38,6 +54,10 @@ function PDU(command, options) { for (var key in options) if (key in tlvs && !(key in params)) { this[key] = options[key]; } + // user-defined / unknown TLVs addressed by numeric tag id (issue #231) + for (var rkey in options) if (isRawTlvKey(rkey, params)) { + this[rkey] = options[rkey]; + } } PDU.commandLength = function(stream) { @@ -184,6 +204,9 @@ PDU.prototype.toBuffer = function() { values.forEach(function(value) { this.command_length += tlvs[key].type.size(value) + 4; }.bind(this)); + } else if (isRawTlvKey(key, params)) { + // user-defined / unknown TLV by numeric tag id (issue #231) + this.command_length += toRawTlvBuffer(this[key]).length + 4; } } var buffer = this._initBuffer(); @@ -205,6 +228,15 @@ PDU.prototype.toBuffer = function() { offset += length; }); } + for (var rkey in this) if (isRawTlvKey(rkey, params)) { + // user-defined / unknown TLV by numeric tag id (issue #231) + var raw = toRawTlvBuffer(this[rkey]); + buffer.writeUInt16BE(Number(rkey), offset); + buffer.writeUInt16BE(raw.length, offset + 2); + offset += 4; + raw.copy(buffer, offset); + offset += raw.length; + } return buffer; }; diff --git a/test/fixes.js b/test/fixes.js new file mode 100644 index 0000000..4e50916 --- /dev/null +++ b/test/fixes.js @@ -0,0 +1,66 @@ +var assert = require('assert'), + smpp = require('..'), + PDU = require('../lib/pdu').PDU, + Buffer = require('safer-buffer').Buffer; + +describe('issue fixes', function () { + + describe('#229 / #256 - tolerant short_message encoding', function () { + it('does not crash when short_message is a number', function () { + var pdu = new PDU('submit_sm', { destination_addr: '12345', short_message: 123 }); + var buf; + assert.doesNotThrow(function () { buf = pdu.toBuffer(); }); + assert.equal(new PDU(buf).short_message.message, '123'); + }); + + it('does not crash when short_message is undefined', function () { + var pdu = new PDU('submit_sm', { destination_addr: '12345' }); + pdu.short_message = undefined; + assert.doesNotThrow(function () { pdu.toBuffer(); }); + }); + + it('does not crash when short_message is null', function () { + var pdu = new PDU('submit_sm', { destination_addr: '12345', short_message: null }); + assert.doesNotThrow(function () { pdu.toBuffer(); }); + }); + }); + + describe('#66 / #252 - DCS-aware encoding detection on decode', function () { + function decodeWith(data_coding, buffer) { + return smpp.filters.message.decode.call({ data_coding: data_coding, esm_class: 0 }, buffer, false); + } + + it('decodes UCS2 (data_coding 0x08)', function () { + assert.equal(decodeWith(0x08, smpp.encodings.UCS2.encode('héllo')).message, 'héllo'); + }); + + it('treats DCS 0xF8 as default alphabet, not UCS2 (low-nibble trap)', function () { + assert.equal(decodeWith(0xF8, smpp.encodings.ASCII.encode('test')).message, 'test'); + }); + + it('treats DCS 0xF4 as binary (returns raw buffer)', function () { + var out = decodeWith(0xF4, Buffer.from([1, 2, 3])).message; + assert.ok(Buffer.isBuffer(out)); + assert.deepEqual([].slice.call(out), [1, 2, 3]); + }); + + it('keeps low data_coding values working (0x00 => default ASCII)', function () { + assert.equal(decodeWith(0x00, smpp.encodings.ASCII.encode('hi')).message, 'hi'); + }); + }); + + describe('#231 - user-defined TLVs round-trip by numeric tag', function () { + it('round-trips a custom TLV addressed by its numeric tag id', function () { + var pdu = new PDU('submit_sm', { destination_addr: '12345', short_message: 'x' }); + pdu[0x3C02] = Buffer.from('TEST', 'ascii'); + var round = new PDU(pdu.toBuffer()); + assert.ok(Buffer.isBuffer(round[0x3C02])); + assert.equal(round[0x3C02].toString('ascii'), 'TEST'); + }); + + it('accepts a custom TLV passed via constructor options', function () { + var pdu = new PDU('submit_sm', { destination_addr: '12345', short_message: 'x', 15362: Buffer.from('ALEX', 'ascii') }); + assert.equal(new PDU(pdu.toBuffer())[15362].toString('ascii'), 'ALEX'); + }); + }); +});