039951e69b
* Regression tests for a truncated TLV tail refused rather than accepted * Refuse a PDU whose optional parameters do not end on command_length * Assert the bare TLV header refusal against jsmpp instead of recording it as a defect * Note the truncated TLV tail defect as fixed in the java-client findings * Derive the padding position, share the bare TLV fixture and trim the decision record * Regression tests for a PDU whose trailing C-Octet String a peer left out * An absent trailing C-Octet String consumes no octet, so a bodyless PDU still parses * Bound the TLV loop by the buffer it was given rather than a second spelling of its length * Answer the stability review's questions in the record and pin the array contract
277 lines
10 KiB
TypeScript
277 lines
10 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test, { describe } from 'node:test';
|
|
import type { DestAddress, UnsuccessSme } from '../src/defs/types.ts';
|
|
import { tlvs } from '../src/defs/tlvs.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), { bytesRead: 1, value: 0x65 });
|
|
assert.deepEqual(types.int8.size(0x65), { size: 1 });
|
|
assert.deepEqual(types.int8.write(0x65, target, 0), {});
|
|
assert.deepEqual(target, Buffer.from([0x65]));
|
|
});
|
|
|
|
test('int16 reads 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), { bytesRead: 2, value: 0x0565 });
|
|
types.int16.write(0x0565, target, 0);
|
|
assert.deepEqual(target, Buffer.from([0x05, 0x65]));
|
|
});
|
|
|
|
test('int32 reads 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), { bytesRead: 4, value: 0x10024045 });
|
|
types.int32.write(0x10024045, target, 0);
|
|
assert.deepEqual(target, Buffer.from([0x10, 0x02, 0x40, 0x45]));
|
|
});
|
|
|
|
// 0.4.0 let Node throw straight out of writeUInt8 for these.
|
|
test('rejects values the field cannot hold', () => {
|
|
assert.ok(types.int8.write(256, Buffer.alloc(1), 0).err instanceof Error);
|
|
assert.ok(types.int8.write(-1, Buffer.alloc(1), 0).err instanceof Error);
|
|
assert.ok(types.int16.write(1.5, Buffer.alloc(2), 0).err instanceof Error);
|
|
assert.ok(types.int8.write('nope', Buffer.alloc(1), 0).err instanceof Error);
|
|
});
|
|
});
|
|
|
|
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), { bytesRead: 9, value: expected });
|
|
});
|
|
|
|
// The length is one octet, so a longer value has nowhere to say how long it is.
|
|
test('refuses a value longer than the length octet can count', () => {
|
|
const tooLong = 'x'.repeat(256);
|
|
|
|
assert.ok(types.string.size(tooLong).err instanceof Error);
|
|
assert.ok(types.string.write(tooLong, Buffer.alloc(300), 0).err instanceof Error);
|
|
});
|
|
|
|
test('sizes as the string plus its length octet', () => {
|
|
assert.deepEqual(types.string.size(expected), { size: 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), { bytesRead: 9, value: expected });
|
|
});
|
|
|
|
test('sizes as the string plus its NULL terminator', () => {
|
|
assert.deepEqual(types.cstring.size(expected), { size: 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.deepEqual(types.cstring.size(123), { size: 4 });
|
|
});
|
|
|
|
test('refuses a string with no terminator rather than running off the end', () => {
|
|
assert.ok(types.cstring.read(Buffer.from('abcd'), 0).err instanceof Error);
|
|
});
|
|
|
|
test('refuses one that starts past the end instead of inventing an empty value', () => {
|
|
assert.ok(types.cstring.read(encoded, encoded.length + 1).err instanceof Error);
|
|
assert.ok(types.cstring.read(encoded, -1).err instanceof Error);
|
|
|
|
// At the end exactly the field is absent, not corrupt, and consumes no octet: counting one
|
|
// puts every later offset past the declared end.
|
|
assert.deepEqual(types.cstring.read(encoded, encoded.length), { bytesRead: 0, value: '' });
|
|
});
|
|
});
|
|
|
|
describe('integer TLVs', () => {
|
|
const encoded = Buffer.from([0x00, 0x00, 0x01, 0x02]);
|
|
|
|
// The TLV header's length is what the parser skips, so it is what the value must be read at.
|
|
test('read the width the TLV header declares', () => {
|
|
assert.deepEqual(types.tlv.int8.read(encoded, 0, 4), { bytesRead: 4, value: 0x00000102 });
|
|
assert.deepEqual(types.tlv.int16.read(encoded, 2, 2), { bytesRead: 2, value: 0x0102 });
|
|
assert.deepEqual(types.tlv.int32.read(encoded, 3, 1), { bytesRead: 1, value: 0x02 });
|
|
assert.deepEqual(types.tlv.int16.read(encoded, 2), { bytesRead: 2, value: 0x0102 });
|
|
});
|
|
|
|
test('refuse a length no integer field can have', () => {
|
|
assert.ok(types.tlv.int8.read(encoded, 0, 0).err instanceof Error);
|
|
assert.ok(types.tlv.int16.read(encoded, 0, 3).err instanceof Error);
|
|
assert.ok(types.tlv.int32.read(encoded, 0, 8).err instanceof Error);
|
|
});
|
|
|
|
test('stay bounds-checked at the declared width', () => {
|
|
assert.ok(types.tlv.int8.read(Buffer.alloc(2), 0, 4).err instanceof Error);
|
|
});
|
|
|
|
// SMPP 3.4 5.3.2.7-8: the two telematics ids are deliberately different widths.
|
|
test('are the width the spec gives each tag', () => {
|
|
assert.equal(tlvs.source_telematics_id.type, types.tlv.int8);
|
|
assert.equal(tlvs.dest_telematics_id.type, types.tlv.int16);
|
|
});
|
|
});
|
|
|
|
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), {
|
|
bytesRead: 8,
|
|
value: expected,
|
|
});
|
|
});
|
|
|
|
test('sizes a binary field in octets', () => {
|
|
assert.deepEqual(types.buffer.size(expected), { size: 8 });
|
|
});
|
|
|
|
// 0.4.0 subtracted one whenever the last octet was 0x00, so a UCS2 message ending in a
|
|
// character like U+4E00 was allocated one octet short while sm_length still reported the full
|
|
// length — the PDU went out corrupt.
|
|
test('counts a trailing NULL octet like any other', () => {
|
|
assert.deepEqual(types.buffer.size(Buffer.from([0x4E, 0x00])), { size: 2 });
|
|
});
|
|
|
|
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), {
|
|
bytesRead: 13,
|
|
value: expected,
|
|
});
|
|
|
|
// A structure that ran out counts the octets that were there, never the terminator that was not.
|
|
assert.deepEqual(types.dest_address_array.read(Buffer.from([0x01, 0x01, 0x00, 0x00]), 0), {
|
|
bytesRead: 4,
|
|
value: [{ dest_addr_npi: 0, dest_addr_ton: 0, destination_addr: '' }],
|
|
});
|
|
});
|
|
|
|
test('sizes every dest_address structure', () => {
|
|
assert.deepEqual(types.dest_address_array.size(expected), { size: 13 });
|
|
});
|
|
|
|
test('writes every dest_address structure', () => {
|
|
const target = Buffer.alloc(13);
|
|
|
|
types.dest_address_array.write(expected, target, 0);
|
|
|
|
assert.deepEqual(target, encoded);
|
|
});
|
|
|
|
test('refuses a field value the wire cannot hold instead of throwing', () => {
|
|
const badTon: DestAddress[] = [{ dest_addr_npi: 0, dest_addr_ton: 999, destination_addr: '123' }];
|
|
const tooMany: DestAddress[] = Array.from({ length: 300 }, () => ({ dl_name: 'a' }));
|
|
|
|
assert.ok(types.dest_address_array.write(badTon, Buffer.alloc(8), 0).err instanceof Error);
|
|
assert.ok(types.dest_address_array.write(tooMany, Buffer.alloc(901), 0).err instanceof Error);
|
|
});
|
|
});
|
|
|
|
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), {
|
|
bytesRead: 21,
|
|
value: expected,
|
|
});
|
|
});
|
|
|
|
test('sizes every unsuccess_sme structure', () => {
|
|
assert.deepEqual(types.unsuccess_sme_array.size(expected), { size: 21 });
|
|
});
|
|
|
|
test('writes every unsuccess_sme structure', () => {
|
|
const target = Buffer.alloc(21);
|
|
|
|
types.unsuccess_sme_array.write(expected, target, 0);
|
|
|
|
assert.deepEqual(target, encoded);
|
|
});
|
|
|
|
test('refuses a field value the wire cannot hold instead of throwing', () => {
|
|
const badStatus: UnsuccessSme[] = [
|
|
{ dest_addr_npi: 0, dest_addr_ton: 0, destination_addr: 'abc', error_status_code: 0x1FFFFFFFF },
|
|
];
|
|
const badTon: UnsuccessSme[] = [
|
|
{ dest_addr_npi: 0, dest_addr_ton: 999, destination_addr: 'abc', error_status_code: 0 },
|
|
];
|
|
|
|
assert.ok(types.unsuccess_sme_array.write(badStatus, Buffer.alloc(11), 0).err instanceof Error);
|
|
assert.ok(types.unsuccess_sme_array.write(badTon, Buffer.alloc(11), 0).err instanceof Error);
|
|
});
|
|
});
|
|
|
|
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);
|
|
assert.ok(types.dest_address_array.read(Buffer.from([0x05, 0x01]), 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);
|
|
});
|
|
});
|