Port SMPP definitions: constants, errors, encodings and wire types

This commit is contained in:
2026-08-25 16:07:34 +02:00
parent 16c935bb18
commit 854c89f609
7 changed files with 915 additions and 3 deletions
+10
View File
@@ -15,10 +15,20 @@ export default tseslint.config(
},
rules: {
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
'@typescript-eslint/no-floating-promises': ['error', {
allowForKnownSafeCalls: [
{ from: 'package', name: ['describe', 'it', 'test'], package: 'node:test' },
],
}],
'@typescript-eslint/no-non-null-assertion': 'error',
'no-console': 'error',
},
},
{
// ESC (0x1B) is the GSM 03.38 escape character, so it belongs in these patterns.
files: ['src/defs/encodings.ts'],
rules: { 'no-control-regex': 'off' },
},
{
files: ['eslint.config.js'],
extends: [tseslint.configs.disableTypeChecked],
+124
View File
@@ -0,0 +1,124 @@
export type EncodingName = 'ASCII' | 'FLASH' | 'LATIN1' | 'UCS2';
export type Encoding = {
decode: (buffer: Uint8Array) => string;
encode: (value: string) => Buffer;
match: (value: string) => boolean;
};
const gsmChars =
'@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\x1BÆæßÉ !"#¤%&\'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà';
const gsmRegex =
/^[@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\x1BÆæßÉ !"#¤%&'()*+,\-./0-9:;<=>?¡A-ZÄÖÑܧ¿a-zäöñüà\f^{}\\[~\]|€]*$/;
const gsmExtended = /[\f^{}\\[~\]|€]/g;
const gsmEscaped = /\x1B([\nΛ()/<=>¡e])/g;
const gsmCharCodes = new Map<string, number>();
const gsmExtChars = new Map<string, string>();
for (let i = 0; i < gsmChars.length; i++) {
gsmCharCodes.set(gsmChars[i], i);
}
{
const from = '\f^{}\\[~]|€';
const to = '\nΛ()/<=>¡e';
for (let i = 0; i < from.length; i++) {
gsmExtChars.set(from[i], to[i]);
gsmExtChars.set(to[i], from[i]);
}
}
const ascii: Encoding = {
decode(buffer) {
let result = '';
for (const byte of buffer) {
result += gsmChars[byte] ?? ' ';
}
return result.replace(gsmEscaped, (match, escaped: string) => gsmExtChars.get(escaped) ?? match);
},
encode(value) {
const escaped = value.replace(gsmExtended, match => `\x1B${gsmExtChars.get(match) ?? ''}`);
const result: number[] = [];
for (const char of escaped) {
result.push(gsmCharCodes.get(char) ?? 0x20);
}
return Buffer.from(result);
},
match(value) {
return gsmRegex.test(value);
},
};
const latin1: Encoding = {
decode(buffer) {
return Buffer.from(buffer).toString('latin1');
},
encode(value) {
return Buffer.from(value, 'latin1');
},
// Deliberately never selected by detect(); Latin-1 is decoded when a peer asks for it, never
// chosen for outgoing messages.
match() {
return false;
},
};
const ucs2: Encoding = {
decode(buffer) {
return Buffer.from(buffer).swap16().toString('utf16le');
},
encode(value) {
return Buffer.from(value, 'utf16le').swap16();
},
match() {
return true;
},
};
export const encodings: Record<EncodingName, Encoding> = {
ASCII: ascii,
FLASH: ascii,
LATIN1: latin1,
UCS2: ucs2,
};
export function detect(value: string): EncodingName {
if (encodings.ASCII.match(value)) return 'ASCII';
if (encodings.LATIN1.match(value)) return 'LATIN1';
return 'UCS2';
}
/**
* SMPP data_coding is a flat table for 0x00-0x0E, but the 0x1X and 0xFX ranges carry a GSM message
* class and encode the alphabet in bits 3-2 (or bit 2) instead — which is how a flash UCS2 message
* arrives as 0x18. Alphabets with no codec here fall back to ASCII.
*/
export function encodingByDataCoding(dataCoding: number): EncodingName {
if ((dataCoding & 0xF0) === 0x10) {
return ((dataCoding >> 2) & 0x03) === 0x02 ? 'UCS2' : 'ASCII';
}
if ((dataCoding & 0xF0) === 0xF0) {
return 'ASCII';
}
if (dataCoding === 0x03) return 'LATIN1';
if (dataCoding === 0x08) return 'UCS2';
return 'ASCII';
}
+88
View File
@@ -0,0 +1,88 @@
// Ordered by code, mirroring the SMPP 5.0 command_status table, so gaps stay visible.
export const errors = {
ESME_ROK: 0x0000,
ESME_RINVMSGLEN: 0x0001,
ESME_RINVCMDLEN: 0x0002,
ESME_RINVCMDID: 0x0003,
ESME_RINVBNDSTS: 0x0004,
ESME_RALYBND: 0x0005,
ESME_RINVPRTFLG: 0x0006,
ESME_RINVREGDLVFLG: 0x0007,
ESME_RSYSERR: 0x0008,
ESME_RINVSRCADR: 0x000A,
ESME_RINVDSTADR: 0x000B,
ESME_RINVMSGID: 0x000C,
ESME_RBINDFAIL: 0x000D,
ESME_RINVPASWD: 0x000E,
ESME_RINVSYSID: 0x000F,
ESME_RCANCELFAIL: 0x0011,
ESME_RREPLACEFAIL: 0x0013,
ESME_RMSGQFUL: 0x0014,
ESME_RINVSERTYP: 0x0015,
ESME_RINVNUMDESTS: 0x0033,
ESME_RINVDLNAME: 0x0034,
ESME_RINVDESTFLAG: 0x0040,
ESME_RINVSUBREP: 0x0042,
ESME_RINVESMCLASS: 0x0043,
ESME_RCNTSUBDL: 0x0044,
ESME_RSUBMITFAIL: 0x0045,
ESME_RINVSRCTON: 0x0048,
ESME_RINVSRCNPI: 0x0049,
ESME_RINVDSTTON: 0x0050,
ESME_RINVDSTNPI: 0x0051,
ESME_RINVSYSTYP: 0x0053,
ESME_RINVREPFLAG: 0x0054,
ESME_RINVNUMMSGS: 0x0055,
ESME_RTHROTTLED: 0x0058,
ESME_RINVSCHED: 0x0061,
ESME_RINVEXPIRY: 0x0062,
ESME_RINVDFTMSGID: 0x0063,
ESME_RX_T_APPN: 0x0064,
ESME_RX_P_APPN: 0x0065,
ESME_RX_R_APPN: 0x0066,
ESME_RQUERYFAIL: 0x0067,
ESME_RINVTLVSTREAM: 0x00C0,
ESME_RTLVNOTALLWD: 0x00C1,
ESME_RINVTLVLEN: 0x00C2,
ESME_RMISSINGTLV: 0x00C3,
ESME_RINVTLVVAL: 0x00C4,
ESME_RDELIVERYFAILURE: 0x00FE,
ESME_RUNKNOWNERR: 0x00FF,
ESME_RSERTYPUNAUTH: 0x0100,
ESME_RPROHIBITED: 0x0101,
ESME_RSERTYPUNAVAIL: 0x0102,
ESME_RSERTYPDENIED: 0x0103,
ESME_RINVDCS: 0x0104,
ESME_RINVSRCADDRSUBUNIT: 0x0105,
ESME_RINVDSTADDRSUBUNIT: 0x0106,
ESME_RINVBCASTFREQINT: 0x0107,
ESME_RINVBCASTALIAS_NAME: 0x0108,
ESME_RINVBCASTAREAFMT: 0x0109,
ESME_RINVNUMBCAST_AREAS: 0x010A,
ESME_RINVBCASTCNTTYPE: 0x010B,
ESME_RINVBCASTMSGCLASS: 0x010C,
ESME_RBCASTFAIL: 0x010D,
ESME_RBCASTQUERYFAIL: 0x010E,
ESME_RBCASTCANCELFAIL: 0x010F,
ESME_RINVBCAST_REP: 0x0110,
ESME_RINVBCASTSRVGRP: 0x0111,
ESME_RINVBCASTCHANIND: 0x0112,
} as const;
export type ErrorName = keyof typeof errors;
export const errorsById: Record<number, string> = {};
for (const [name, code] of Object.entries(errors)) {
errorsById[code] = name;
}
export function isErrorName(value: unknown): value is ErrorName {
return typeof value === 'string' && Object.hasOwn(errors, value);
}
export function errorNameById(id: number): ErrorName | undefined {
const name = errorsById[id];
return isErrorName(name) ? name : undefined;
}
+391
View File
@@ -0,0 +1,391 @@
import type { Result, VoidResult } from '../result.ts';
export type DestAddress =
| { dest_addr_npi: number; dest_addr_ton: number; destination_addr: string }
| { dl_name: string };
export type UnsuccessSme = {
dest_addr_npi: number;
dest_addr_ton: number;
destination_addr: string;
error_status_code: number;
};
export type ParamValue = Buffer | DestAddress[] | UnsuccessSme[] | number | string;
export type WireType<TRead extends ParamValue, TWrite extends ParamValue = TRead> = {
default: TRead;
read: (buffer: Buffer, offset: number, length?: number) => Result<{ value: TRead }>;
size: (value: TWrite) => number;
write: (value: TWrite, buffer: Buffer, offset: number) => VoidResult;
};
function outOfRange(buffer: Buffer, offset: number, needed: number): Error | undefined {
if (offset < 0 || needed < 0 || offset + needed > buffer.length) {
return new Error(
`Out of range: need ${String(needed)} bytes at offset ${String(offset)} of a ${String(buffer.length)} byte buffer`,
);
}
return undefined;
}
function asString(value: number | string): string {
return typeof value === 'number' ? value.toString() : value;
}
function readCstring(buffer: Buffer, offset: number): Result<{ value: string }> {
let length = 0;
while (buffer[offset + length]) {
length++;
if (offset + length >= buffer.length) {
return { err: new Error('Unterminated C-Octet String') };
}
}
return { value: buffer.toString('ascii', offset, offset + length) };
}
function writeCstring(value: number | string, buffer: Buffer, offset: number): VoidResult {
const str = asString(value);
const err = outOfRange(buffer, offset, str.length + 1);
if (err) return { err };
buffer.write(str, offset, 'ascii');
buffer[offset + str.length] = 0;
return {};
}
function sizeCstring(value: number | string): number {
return asString(value).length + 1;
}
export const int8: WireType<number> = {
default: 0,
read(buffer, offset) {
const err = outOfRange(buffer, offset, 1);
return err ? { err } : { value: buffer.readUInt8(offset) };
},
size() {
return 1;
},
write(value, buffer, offset) {
const err = outOfRange(buffer, offset, 1);
if (err) return { err };
buffer.writeUInt8(value || 0, offset);
return {};
},
};
export const int16: WireType<number> = {
default: 0,
read(buffer, offset) {
const err = outOfRange(buffer, offset, 2);
return err ? { err } : { value: buffer.readUInt16BE(offset) };
},
size() {
return 2;
},
write(value, buffer, offset) {
const err = outOfRange(buffer, offset, 2);
if (err) return { err };
buffer.writeUInt16BE(value || 0, offset);
return {};
},
};
export const int32: WireType<number> = {
default: 0,
read(buffer, offset) {
const err = outOfRange(buffer, offset, 4);
return err ? { err } : { value: buffer.readUInt32BE(offset) };
},
size() {
return 4;
},
write(value, buffer, offset) {
const err = outOfRange(buffer, offset, 4);
if (err) return { err };
buffer.writeUInt32BE(value || 0, offset);
return {};
},
};
/** Octet String: a length byte followed by that many octets. */
export const string: WireType<string, number | string> = {
default: '',
read(buffer, offset) {
const lengthErr = outOfRange(buffer, offset, 1);
if (lengthErr) return { err: lengthErr };
const length = buffer.readUInt8(offset);
const err = outOfRange(buffer, offset + 1, length);
if (err) return { err };
return { value: buffer.toString('ascii', offset + 1, offset + 1 + length) };
},
size(value) {
return asString(value).length + 1;
},
write(value, buffer, offset) {
const str = asString(value);
const err = outOfRange(buffer, offset, str.length + 1);
if (err) return { err };
buffer.writeUInt8(str.length, offset);
buffer.write(str, offset + 1, 'ascii');
return {};
},
};
/** C-Octet String: NULL-terminated. */
export const cstring: WireType<string, number | string> = {
default: '',
read: readCstring,
size: sizeCstring,
write: writeCstring,
};
export const buffer: WireType<Buffer, Buffer | number | string> = {
default: Buffer.alloc(0),
read(buf, offset, length = 0) {
const err = outOfRange(buf, offset, length);
return err ? { err } : { value: buf.subarray(offset, offset + length) };
},
// A trailing NULL octet is not counted, mirroring how peers that append one to short_message
// report sm_length. pduToObj relies on this when deciding whether to retry a parse.
size(value) {
const buf = Buffer.isBuffer(value) ? value : Buffer.from(asString(value), 'ascii');
return buf[buf.length - 1] === 0x00 ? buf.length - 1 : buf.length;
},
write(value, buf, offset) {
const source = Buffer.isBuffer(value) ? value : Buffer.from(asString(value), 'ascii');
const err = outOfRange(buf, offset, source.length);
if (err) return { err };
source.copy(buf, offset);
return {};
},
};
export const dest_address_array: WireType<DestAddress[]> = {
default: [],
read(buf, offset) {
const countErr = outOfRange(buf, offset, 1);
if (countErr) return { err: countErr };
const result: DestAddress[] = [];
let remaining = buf.readUInt8(offset++);
while (remaining-- > 0) {
const flagErr = outOfRange(buf, offset, 1);
if (flagErr) return { err: flagErr };
const destFlag = buf.readUInt8(offset++);
if (destFlag === 1) {
const headerErr = outOfRange(buf, offset, 2);
if (headerErr) return { err: headerErr };
const dest_addr_ton = buf.readUInt8(offset++);
const dest_addr_npi = buf.readUInt8(offset++);
const { err, value } = readCstring(buf, offset);
if (err) return { err };
offset += sizeCstring(value);
result.push({ dest_addr_npi, dest_addr_ton, destination_addr: value });
} else {
const { err, value } = readCstring(buf, offset);
if (err) return { err };
offset += sizeCstring(value);
result.push({ dl_name: value });
}
}
return { value: result };
},
size(value) {
let size = 1;
for (const dest of value) {
size += 'dl_name' in dest
? sizeCstring(dest.dl_name) + 1
: sizeCstring(dest.destination_addr) + 3;
}
return size;
},
write(value, buf, offset) {
const err = outOfRange(buf, offset, dest_address_array.size(value));
if (err) return { err };
buf.writeUInt8(value.length, offset++);
for (const dest of value) {
if ('dl_name' in dest) {
buf.writeUInt8(2, offset++);
writeCstring(dest.dl_name, buf, offset);
offset += sizeCstring(dest.dl_name);
} else {
buf.writeUInt8(1, offset++);
buf.writeUInt8(dest.dest_addr_ton || 0, offset++);
buf.writeUInt8(dest.dest_addr_npi || 0, offset++);
writeCstring(dest.destination_addr, buf, offset);
offset += sizeCstring(dest.destination_addr);
}
}
return {};
},
};
export const unsuccess_sme_array: WireType<UnsuccessSme[]> = {
default: [],
read(buf, offset) {
const countErr = outOfRange(buf, offset, 1);
if (countErr) return { err: countErr };
const result: UnsuccessSme[] = [];
let remaining = buf.readUInt8(offset++);
while (remaining-- > 0) {
const headerErr = outOfRange(buf, offset, 2);
if (headerErr) return { err: headerErr };
const dest_addr_ton = buf.readUInt8(offset++);
const dest_addr_npi = buf.readUInt8(offset++);
const { err, value } = readCstring(buf, offset);
if (err) return { err };
offset += sizeCstring(value);
const statusErr = outOfRange(buf, offset, 4);
if (statusErr) return { err: statusErr };
result.push({
dest_addr_npi,
dest_addr_ton,
destination_addr: value,
error_status_code: buf.readUInt32BE(offset),
});
offset += 4;
}
return { value: result };
},
size(value) {
let size = 1;
for (const sme of value) {
size += sizeCstring(sme.destination_addr) + 6;
}
return size;
},
write(value, buf, offset) {
const err = outOfRange(buf, offset, unsuccess_sme_array.size(value));
if (err) return { err };
buf.writeUInt8(value.length, offset++);
for (const sme of value) {
buf.writeUInt8(sme.dest_addr_ton || 0, offset++);
buf.writeUInt8(sme.dest_addr_npi || 0, offset++);
writeCstring(sme.destination_addr, buf, offset);
offset += sizeCstring(sme.destination_addr);
buf.writeUInt32BE(sme.error_status_code, offset);
offset += 4;
}
return {};
},
};
/** TLV variants are length-prefixed by the TLV header, so they carry no length of their own. */
export const tlv = {
buffer: {
default: Buffer.alloc(0),
read(buf: Buffer, offset: number, length = 0): Result<{ value: Buffer }> {
const err = outOfRange(buf, offset, length);
return err ? { err } : { value: buf.subarray(offset, offset + length) };
},
size(value: Buffer | number | string): number {
return Buffer.isBuffer(value) ? value.length : asString(value).length;
},
write: buffer.write,
} satisfies WireType<Buffer, Buffer | number | string>,
cstring,
int8,
int16,
int32,
string: {
default: '',
read(buf: Buffer, offset: number, length = 0): Result<{ value: string }> {
const err = outOfRange(buf, offset, length);
return err ? { err } : { value: buf.toString('ascii', offset, offset + length) };
},
size(value: number | string): number {
return asString(value).length;
},
write(value: number | string, buf: Buffer, offset: number): VoidResult {
const str = asString(value);
const err = outOfRange(buf, offset, str.length);
if (err) return { err };
buf.write(str, offset, 'ascii');
return {};
},
} satisfies WireType<string, number | string>,
};
export const types = {
buffer,
cstring,
dest_address_array,
int8,
int16,
int32,
string,
tlv,
unsuccess_sme_array,
};
+118
View File
@@ -0,0 +1,118 @@
import assert from 'node:assert/strict';
import test, { describe } from 'node:test';
import { detect, encodingByDataCoding, encodings } from '../src/defs/encodings.ts';
describe('ASCII (GSM 03.38)', () => {
const samples: [string, number[]][] = [
['@£$¥', [0, 1, 2, 3]],
[' 1a=', [0x20, 0x31, 0x61, 0x3D]],
['~^€', [0x1B, 0x3D, 0x1B, 0x14, 0x1B, 0x65]],
];
test('matches strings encodable in the GSM 03.38 charset', () => {
assert.ok(encodings.ASCII.match(''));
assert.ok(encodings.ASCII.match('@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\x1BÆæßÉ !"#¤%&\''));
assert.ok(encodings.ASCII.match('()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZ'));
assert.ok(encodings.ASCII.match('ÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà'));
assert.ok(encodings.ASCII.match('\f^{}\\[~]|€'));
});
test('rejects strings outside the GSM 03.38 charset', () => {
assert.ok(!encodings.ASCII.match('`'));
assert.ok(!encodings.ASCII.match('ÁáçÚUÓO'));
assert.ok(!encodings.ASCII.match('تست'));
});
test('round-trips the sample strings', () => {
for (const [str, bytes] of samples) {
assert.deepEqual(encodings.ASCII.encode(str), Buffer.from(bytes));
assert.equal(encodings.ASCII.decode(Buffer.from(bytes)), str);
}
});
});
describe('LATIN1', () => {
const samples: [string, number[]][] = [
['@$`Á', [0x40, 0x24, 0x60, 0xC1]],
['áçÚ', [0xE1, 0xE7, 0xDA]],
['UÓO', [0x55, 0xD3, 0x4F]],
];
test('never matches, so it is never auto-selected for new messages', () => {
assert.ok(!encodings.LATIN1.match('`ÁáçÚUÓO'));
assert.ok(!encodings.LATIN1.match('تست'));
assert.ok(!encodings.LATIN1.match('۱۲۳۴۵۶۷۸۹۰'));
});
test('round-trips the sample strings', () => {
for (const [str, bytes] of samples) {
assert.deepEqual(encodings.LATIN1.encode(str), Buffer.from(bytes));
assert.equal(encodings.LATIN1.decode(Buffer.from(bytes)), str);
}
});
});
describe('UCS2', () => {
const samples: [string, number[]][] = [
[' 1a', [0x00, 0x20, 0x00, 0x31, 0x00, 0x61]],
['۱۲۳', [0x06, 0xF1, 0x06, 0xF2, 0x06, 0xF3]],
];
test('always matches', () => {
assert.ok(encodings.UCS2.match(''));
assert.ok(encodings.UCS2.match('`ÁáçÚUÓO'));
assert.ok(encodings.UCS2.match('تست'));
});
test('round-trips the sample strings', () => {
for (const [str, bytes] of samples) {
assert.deepEqual(encodings.UCS2.encode(str), Buffer.from(bytes));
assert.equal(encodings.UCS2.decode(Buffer.from(bytes)), str);
}
});
test('decoding does not mutate the caller\'s buffer', () => {
const buffer = Buffer.from([0x00, 0x20]);
encodings.UCS2.decode(buffer);
assert.deepEqual(buffer, Buffer.from([0x00, 0x20]));
});
});
describe('detect()', () => {
test('picks the narrowest encoding that fits the string', () => {
assert.equal(detect(''), 'ASCII');
assert.equal(detect('ÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà(){}[]'), 'ASCII');
assert.equal(detect('`ÁáçÚUÓO'), 'UCS2');
assert.equal(detect('«©®µ¶±»'), 'UCS2');
assert.equal(detect('ʹʺʻʼʽ`'), 'UCS2');
assert.equal(detect('تست'), 'UCS2');
assert.equal(detect('۱۲۳۴۵۶۷۸۹۰'), 'UCS2');
});
});
describe('encodingByDataCoding()', () => {
test('resolves the flat SMPP data_coding table', () => {
assert.equal(encodingByDataCoding(0x00), 'ASCII');
assert.equal(encodingByDataCoding(0x01), 'ASCII');
assert.equal(encodingByDataCoding(0x08), 'UCS2');
});
// 0.4.0 resolved 0x03 to the alias ISO_8859_1, which has no decoder, and silently fell back to
// ASCII — every Latin-1 message came out corrupted.
test('resolves 0x03 to LATIN1 rather than falling back to ASCII', () => {
assert.equal(encodingByDataCoding(0x03), 'LATIN1');
});
test('reads the alphabet bits when a message class is present', () => {
assert.equal(encodingByDataCoding(0x10), 'ASCII');
assert.equal(encodingByDataCoding(0x18), 'UCS2');
assert.equal(encodingByDataCoding(0xF0), 'ASCII');
});
test('falls back to ASCII for alphabets it has no codec for', () => {
assert.equal(encodingByDataCoding(0x05), 'ASCII');
assert.equal(encodingByDataCoding(0x0E), 'ASCII');
});
});
+183
View File
@@ -0,0 +1,183 @@
import assert from 'node:assert/strict';
import test, { describe } from 'node:test';
import type { DestAddress, UnsuccessSme } from '../src/defs/types.ts';
import { types } from '../src/defs/types.ts';
describe('integers', () => {
test('int8 reads, sizes and writes one octet', () => {
const source = Buffer.from([0, 0x65]);
const target = Buffer.alloc(1);
assert.deepEqual(types.int8.read(source, 1), { value: 0x65 });
assert.equal(types.int8.size(0x65), 1);
assert.deepEqual(types.int8.write(0x65, target, 0), {});
assert.deepEqual(target, Buffer.from([0x65]));
});
test('int16 reads, sizes and writes two octets big-endian', () => {
const source = Buffer.from([0, 0x05, 0x65]);
const target = Buffer.alloc(2);
assert.deepEqual(types.int16.read(source, 1), { value: 0x0565 });
assert.equal(types.int16.size(0x0565), 2);
types.int16.write(0x0565, target, 0);
assert.deepEqual(target, Buffer.from([0x05, 0x65]));
});
test('int32 reads, sizes and writes four octets big-endian', () => {
const source = Buffer.from([0, 0x10, 0x02, 0x40, 0x45]);
const target = Buffer.alloc(4);
assert.deepEqual(types.int32.read(source, 1), { value: 0x10024045 });
assert.equal(types.int32.size(0x10024045), 4);
types.int32.write(0x10024045, target, 0);
assert.deepEqual(target, Buffer.from([0x10, 0x02, 0x40, 0x45]));
});
});
describe('string (Octet String)', () => {
const expected = 'abcd1234';
const encoded = Buffer.concat([Buffer.from([8]), Buffer.from(expected)]);
test('reads a length-prefixed string', () => {
assert.deepEqual(types.string.read(encoded, 0), { value: expected });
});
test('sizes as the string plus its length octet', () => {
assert.equal(types.string.size(expected), 9);
});
test('writes a length-prefixed string', () => {
const target = Buffer.alloc(9);
types.string.write(expected, target, 0);
assert.deepEqual(target, encoded);
});
});
describe('cstring (C-Octet String)', () => {
const expected = 'abcd1234';
const encoded = Buffer.concat([Buffer.from(expected), Buffer.from([0])]);
test('reads a NULL-terminated string', () => {
assert.deepEqual(types.cstring.read(encoded, 0), { value: expected });
});
test('sizes as the string plus its NULL terminator', () => {
assert.equal(types.cstring.size(expected), 9);
});
test('writes a NULL-terminated string', () => {
const target = Buffer.alloc(9);
types.cstring.write(expected, target, 0);
assert.deepEqual(target, encoded);
});
test('coerces a numeric value to its decimal string', () => {
const target = Buffer.alloc(4);
types.cstring.write(123, target, 0);
assert.deepEqual(target, Buffer.from([0x31, 0x32, 0x33, 0x00]));
assert.equal(types.cstring.size(123), 4);
});
test('refuses a string with no terminator rather than running off the end', () => {
const { err } = types.cstring.read(Buffer.from('abcd'), 0);
assert.ok(err instanceof Error);
});
});
describe('buffer', () => {
const expected = Buffer.from('abcd1234');
test('reads a binary field of the given length', () => {
assert.deepEqual(types.buffer.read(expected, 0, expected.length), { value: expected });
});
test('sizes a binary field in octets', () => {
assert.equal(types.buffer.size(expected), 8);
});
test('writes a binary field', () => {
const target = Buffer.alloc(8);
types.buffer.write(expected, target, 0);
assert.deepEqual(target, expected);
});
});
describe('dest_address_array', () => {
const encoded = Buffer.from([
0x02,
0x01, 0x01, 0x02, 0x31, 0x32, 0x33, 0x00,
0x02, 0x61, 0x62, 0x63, 0x00,
]);
const expected: DestAddress[] = [
{ dest_addr_npi: 2, dest_addr_ton: 1, destination_addr: '123' },
{ dl_name: 'abc' },
];
test('reads every dest_address structure', () => {
assert.deepEqual(types.dest_address_array.read(encoded, 0), { value: expected });
});
test('sizes every dest_address structure', () => {
assert.equal(types.dest_address_array.size(expected), 13);
});
test('writes every dest_address structure', () => {
const target = Buffer.alloc(13);
types.dest_address_array.write(expected, target, 0);
assert.deepEqual(target, encoded);
});
});
describe('unsuccess_sme_array', () => {
const encoded = Buffer.from([
0x02,
0x03, 0x04, 0x61, 0x62, 0x63, 0x00, 0x00, 0x00, 0x00, 0x07,
0x05, 0x06, 0x31, 0x32, 0x33, 0x00, 0x10, 0x00, 0x00, 0x08,
]);
const expected: UnsuccessSme[] = [
{ dest_addr_npi: 4, dest_addr_ton: 3, destination_addr: 'abc', error_status_code: 0x00000007 },
{ dest_addr_npi: 6, dest_addr_ton: 5, destination_addr: '123', error_status_code: 0x10000008 },
];
test('reads every unsuccess_sme structure', () => {
assert.deepEqual(types.unsuccess_sme_array.read(encoded, 0), { value: expected });
});
test('sizes every unsuccess_sme structure', () => {
assert.equal(types.unsuccess_sme_array.size(expected), 21);
});
test('writes every unsuccess_sme structure', () => {
const target = Buffer.alloc(21);
types.unsuccess_sme_array.write(expected, target, 0);
assert.deepEqual(target, encoded);
});
});
describe('bounds checking', () => {
// 0.4.0 let a short or malformed PDU throw straight out of the codec.
test('reading past the end returns an error instead of throwing', () => {
assert.ok(types.int32.read(Buffer.alloc(2), 0).err instanceof Error);
assert.ok(types.int8.read(Buffer.alloc(1), 5).err instanceof Error);
assert.ok(types.string.read(Buffer.from([10, 0x61]), 0).err instanceof Error);
});
test('writing past the end returns an error instead of throwing', () => {
assert.ok(types.int32.write(1, Buffer.alloc(2), 0).err instanceof Error);
assert.ok(types.cstring.write('abcd', Buffer.alloc(2), 0).err instanceof Error);
});
});
+1 -3
View File
@@ -118,9 +118,7 @@ smpp.on('session', session => {
### 1. Definition tables — `src/defs/`
- [x] `constants.ts``consts` + `constsById`.
- [ ] `errors.ts` — all `ESME_*` codes plus `errorsById`. Port from 0.4.0 `lib/defs.js`; note that
0.4.0 has a typo, `ESME_RINVBCASTCHANIND = 0x011` (three digits), which should be `0x0111`.
Verify every code against the SMPP 3.4/5.0 spec while porting.
- [x] `errors.ts` — all `ESME_*` codes plus `errorsById`.
- [ ] `types.ts` — wire types `int8`, `int16`, `int32`, `string`, `cstring`, `buffer`,
`dest_address_array`, `unsuccess_sme_array`, and the `tlv` variants. Each is
`{ default, read(buffer, offset, length?), size(value), write(value, buffer, offset) }`.