From 939eda7269fc200d0e30d17c0e723246b811722e Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sun, 27 Sep 2026 15:18:08 +0200 Subject: [PATCH] Key a TLV input like the read side, drop tagId, copy TLV octets at parse, and export only Tlvs, TlvInputs and Tlv --- CHANGELOG.md | 10 ++++-- MIGRATION.md | 3 ++ README.md | 5 +-- src/defs/tlvs.ts | 63 +++++++++++++++++----------------- src/defs/types.ts | 10 ------ src/index.ts | 4 +-- src/pdu.ts | 47 +++++++++---------------- src/retained-pdu.ts | 10 ++---- test/dlr.test.ts | 4 +-- test/interop.test.ts | 4 +-- test/operator-receipts.test.ts | 6 ++-- test/pdu.test.ts | 63 ++++++++++++++++++++++------------ test/unsendable.test.ts | 27 +++------------ 13 files changed, 118 insertions(+), 138 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f286cb2..4b74c7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,9 +60,13 @@ `{ tagValue: [value] }` for them and refuse a lone value before anything goes out. - `pduObj.tlvs` and every `tlvs` input are typed per tag, as `Tlvs` and `TlvInputs`: `receipted_message_id` reads as a `string`, `callback_num` as a `Buffer[]`, and a value of the wrong - type for its tag fails to compile. An unknown tag reads as a `Buffer` under its decimal id. Code - that narrowed `tagValue` with `typeof` still compiles; a `Record` annotation does not, - so use `Tlvs`. + type for its tag fails to compile. Annotate with `Tlvs` or `TlvInputs` where you wrote + `Record` or `Record`; `TlvInput` is gone. + + **A TLV input is keyed by its name, or by its decimal id where the table names none, and `tagId` + is refused.** Write `{ 5142: { tagValue } }` for a vendor tag, not `{ vendor: { tagId: 5142, … } }`. + A name and a `tagId` could disagree, and `{ message_state: { tagId: 5, … } }` went out as tag 5. + A decimal key naming a tag the table knows, `{ 1063: … }`, is refused in favour of the name. - `cmds.broadcast_sm_resp.tlvMap` is removed; nothing read it. ## 0.5.0 diff --git a/MIGRATION.md b/MIGRATION.md index 35b8b09..b21f6af 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -30,6 +30,9 @@ shape is the same, connect, send, listen for delivery reports, with callbacks re `consts.MESSAGING_MODE`**, which also names `SMSC_DEFAULT`. They are bits 1-0 of `esm_class`, not whole values of it. Read them from the new group, or pass `messagingMode` to `sendSms()`. A stale `consts.ESM_CLASS.STORE_FORWARD` reads `undefined`, which OR-s into an `esm_class` carrying no mode. +- **A TLV is keyed by its name, or by its decimal id where the table names none**, and `tagId` on + an input is refused: `{ 5142: { tagValue } }`, not `{ vendor: { tagId: 5142, tagValue } }`. + Unknown tags read back under the same decimal key. - **The `error` event is `sessionError`**, and `serverError` on the server handle. - **`log`** takes any object with `debug`, `error`, `info`, `verbose` and `warn` methods instead of a `larvitutils` one, and is silent by default: [README](README.md#logging). diff --git a/README.md b/README.md index 12673d4..38aefcb 100644 --- a/README.md +++ b/README.md @@ -630,8 +630,9 @@ if (isCommand(pduObj, 'submit_sm')) { naming the character, its code point and where it is. - Every text field is latin1: addresses, `system_id`, `message_id`, `service_type` and the C-Octet String TLVs. A character past `U+00FF` is refused, as is a `U+0000` in a C-Octet String. -- `tlvs` is typed per tag, as `TlvInputs`, so `{ message_state: { tagValue: 'ENROUTE' } }` fails to - compile. A tag the table does not define takes a `tagId`. +- `tlvs` is keyed and typed like `pduObj.tlvs`, as `TlvInputs`: `{ message_state: { tagValue: 2 } }`, + or `{ 5142: { tagValue: octets } }` for a tag the table does not define. Any other key, a decimal + id the table names, and a `tagId` are refused. - The five repeatable TLVs take an array, written as one TLV per element; a lone value or an empty array is refused. - A `Buffer` goes out exactly as given under any `data_coding`: binary payloads, hand-built user diff --git a/src/defs/tlvs.ts b/src/defs/tlvs.ts index da2472f..6417009 100644 --- a/src/defs/tlvs.ts +++ b/src/defs/tlvs.ts @@ -104,55 +104,54 @@ type Repeated = Specs[K] extends { multiple: true } ? V[] type WireValue = Specs[K]['type']['default']; -export type TlvReadValue = Repeated>; +type ReadValue = Repeated>; /** A lone text field also takes a number, and a lone octet field text, which goes out as latin1. */ -export type TlvWriteValue = Specs[K] extends { multiple: true } ? TlvReadValue +type WriteValue = Specs[K] extends { multiple: true } ? ReadValue : WireValue extends number ? number : WireValue extends string ? number | string : Buffer | number | string; -type OneTlv = { tagId: number; tagName: K; tagValue: TlvReadValue }; +type KnownTlv = { tagId: number; tagName: K; tagValue: ReadValue }; -export type Tlv = { [N in K]: OneTlv }[K]; +/** A tag the TLV table does not define. */ +type UnknownTlv = { tagId: number; tagName: undefined; tagValue: Buffer }; -/** A tag the TLV table does not define, keyed by its decimal id. */ -export type UnknownTlv = { tagId: number; tagName: undefined; tagValue: Buffer }; +/** Keyed by tag name, or by its decimal id where the table defines no name. */ +export type Tlvs = { [K in TlvName]?: KnownTlv } & Record<`${number}`, UnknownTlv>; -type KnownTlvs = { [K in TlvName]?: OneTlv }; - -export type Tlvs = KnownTlvs & Record<`${number}`, UnknownTlv>; - -export type TlvInput = { - /** Resolved from the record key; pass it for a tag the TLV table does not define. */ - tagId?: number | undefined; - tagValue: TlvValue; -}; +export type Tlv = { [K in TlvName]: KnownTlv }[TlvName] | UnknownTlv; type Alternate = keyof typeof alternates; type Canonical = K extends Alternate ? (typeof alternates)[K]['tag'] : K; -export type TlvInputs = { - [K in Alternate | TlvName]?: { tagId?: number | undefined; tagValue: TlvWriteValue> }; -} & Record; +/** Keyed like `Tlvs`, by tag name or by the decimal id of a tag the table does not define. */ +export type TlvInputs = { [K in Alternate | TlvName]?: { tagValue: WriteValue> } } + & Record<`${number}`, { tagValue: Buffer | number | string }>; -export function tagIdOf(name: string, input: TlvInput): Result<{ tagId: number }> { - const tagId = input.tagId ?? tlvs[name]?.id; - - if (tagId === undefined) { - return { err: new Error(`TLV "${name}": unknown tag name, give it a tagId`) }; +function tagIdOf(name: string, input: object): Result<{ tagId: number }> { + if ('tagId' in input) { + return { err: new Error(`TLV "${name}": key it by its name, or a tag the table does not define by its decimal id, instead of giving a tagId`) }; } - if (!Number.isInteger(tagId) || tagId < 0 || tagId > 0xFFFF) { - return { err: new Error(`TLV "${name}": tagId ${String(tagId)} out of range 0-65535`) }; + if (Object.hasOwn(tlvs, name)) return { tagId: tlvs[name]?.id ?? 0 }; + + if (!/^(0|[1-9]\d*)$/.test(name)) { + return { err: new Error(`TLV "${name}": unknown tag name; key a tag the table does not define by its decimal id`) }; } - return { tagId }; + const tagId = Number(name); + + if (tagId > 0xFFFF) return { err: new Error(`TLV "${name}": tag id out of range 0-65535`) }; + + const known = tlvsById[tagId]; + + return known ? { err: new Error(`TLV "${name}": the table names this tag ${known.tag}, key it by that`) } : { tagId }; } /** Each TLV as its four octet header and the value the tag's own wire type writes. */ -export function writeTlvs(inputs: Record | undefined): Result<{ chunks: Buffer[] }> { +export function writeTlvs(inputs: TlvInputs | undefined): Result<{ chunks: Buffer[] }> { const chunks: Buffer[] = []; for (const [name, input] of Object.entries(inputs ?? {})) { @@ -223,7 +222,10 @@ function readTlv(pdu: Buffer, offset: number): Result<{ octets: number; occurren if (read.err) return { err: read.err }; - return { occurrence: { definition, tagId, value: read.value }, octets: 4 + tagLength }; + // Copied, so holding a TLV pins no more than its own octets. + const value = Buffer.isBuffer(read.value) ? Buffer.from(read.value) : read.value; + + return { occurrence: { definition, tagId, value }, octets: 4 + tagLength }; } function isTlvName(name: string): name is TlvName { @@ -249,8 +251,7 @@ function isTlv(key: string, tlv: unknown): boolean { return tlv.tagName === key && isTlvName(key) && readsAs(specs[key], tlv.tagValue); } -/** Every entry keyed by its tag name, or an unknown tag by its decimal id, holding what its table type reads. */ -export function isTlvs(record: Record): record is Tlvs { +function isTlvs(record: Record): record is Tlvs { return Object.entries(record).every(([key, tlv]) => isTlv(key, tlv)); } @@ -276,7 +277,7 @@ function keyedTlvs(occurrences: Occurrence[]): Result<{ tlvs: Tlvs }> { tlvs[key] = { tagId, tagName: key, tagValue: values }; } - return isTlvs(tlvs) ? { tlvs } : { err: new Error('A TLV did not read as its table type') }; + return isTlvs(tlvs) ? { tlvs } : { err: new Error('A TLV did not read as its table type, a defect in this library') }; } export function parseTlvs(pdu: Buffer, start: number): Result<{ offset: number; tlvs: Tlvs }> { diff --git a/src/defs/types.ts b/src/defs/types.ts index 7f3acf5..6b70ab1 100644 --- a/src/defs/types.ts +++ b/src/defs/types.ts @@ -32,16 +32,6 @@ export function tlvOctets(value: TlvValue): number { return octets; } -/** A value holding no view into the PDU it was read from. */ -export function detachedTlv(value: TlvValue): TlvValue { - if (Buffer.isBuffer(value)) return Buffer.from(value); - if (!Array.isArray(value)) return value; - - const buffers = value.filter(one => Buffer.isBuffer(one)); - - return buffers.length === value.length ? buffers.map(one => Buffer.from(one)) : value; -} - /** * One field on the wire. `read` reports how many octets it consumed so callers never have to * re-derive a length that could disagree with what was actually written. diff --git a/src/index.ts b/src/index.ts index 96cf427..837f3f0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -64,10 +64,10 @@ export type { CommandName, PduParams, PduParamsInput } from './defs/commands.ts' export type { ConstGroup, MessageState, SubmitMessagingMode } from './defs/constants.ts'; export type { Encoding, EncodingName, Unencodable } from './defs/encodings.ts'; export type { ErrorName } from './defs/errors.ts'; -export type { PduObject, PduObjectInput, TlvInput, TlvInputs } from './pdu.ts'; +export type { PduObject, PduObjectInput, TlvInputs } from './pdu.ts'; export type { PduHeader } from './pdu-refusal.ts'; export type { SplitOptions } from './message.ts'; -export type { Tlv, TlvDefinition, TlvName, TlvReadValue, TlvWriteValue, Tlvs, UnknownTlv } from './defs/tlvs.ts'; +export type { Tlv, TlvDefinition, TlvName, Tlvs } from './defs/tlvs.ts'; export type { DestAddress, ParamValue, TlvValue, UnsuccessSme, WireType } from './defs/types.ts'; /** The spec tables, grouped the way `larvitsmpp.defs` was in 0.4.0. */ diff --git a/src/pdu.ts b/src/pdu.ts index be649db..fd0651a 100644 --- a/src/pdu.ts +++ b/src/pdu.ts @@ -3,14 +3,14 @@ import type { ErrorName } from './defs/errors.ts'; import type { ParamValue } from './defs/types.ts'; import type { PduHeader } from './pdu-refusal.ts'; import type { Result, VoidResult } from './result.ts'; -import type { TlvInput, TlvInputs, Tlvs } from './defs/tlvs.ts'; +import type { TlvInputs, Tlvs } from './defs/tlvs.ts'; import { PduRefusedError, framingRefusal } from './pdu-refusal.ts'; import { cmds, commandNameById, respNameFor } from './defs/commands.ts'; import { hasUdh } from './defs/constants.ts'; import { decodeMessage, encodeBody } from './message.ts'; import { errorNameById, errors, isErrorName } from './defs/errors.ts'; import { paramNumber, valueText } from './defs/types.ts'; -import { parseTlvs, tagIdOf, tlvs, writeTlvs } from './defs/tlvs.ts'; +import { parseTlvs, writeTlvs } from './defs/tlvs.ts'; /** The highest sequence number this library hands out; SMPP 3.4 4.7.1 reserves 0x7fffffff. */ export const maxSeqNr = 2147483646; @@ -46,7 +46,7 @@ export type PduObject = { tlvs: Tlvs; }; -export type { TlvInput, TlvInputs }; +export type { TlvInputs }; const respBit = 0x80000000; @@ -68,30 +68,13 @@ export function isCommand( type ResolvedBody = { params: Record; - tlvs: Record | undefined; + tlvs: TlvInputs | undefined; }; function codingOf(params: Record): number | undefined { return typeof params.data_coding === 'number' ? params.data_coding : undefined; } -type CarriedBody = { name: string; text: string; tlv: TlvInput }; - -/** Every entry carrying body text, under whatever names their tagIds are keyed to. */ -function carriedBodies(input: Record | undefined): CarriedBody[] { - const carried: CarriedBody[] = []; - - for (const [name, tlv] of Object.entries(input ?? {})) { - const tag = tagIdOf(name, tlv); - - if (!tag.err && tag.tagId === tlvs.message_payload.id && typeof tlv.tagValue === 'string') { - carried.push({ name, text: tlv.tagValue, tlv }); - } - } - - return carried; -} - /** messageOctets() reads short_message wherever it holds an octet, and the TLV only where it does not. */ function carriesOctets(value: ParamValue | undefined): boolean { return Buffer.isBuffer(value) && value.length > 0; @@ -105,19 +88,21 @@ function writtenBody(definition: CommandDefinition, value: ParamValue | undefine /** Encoded in place, settling data_coding where `settles` says no mandatory field will carry it. */ function resolveCarried( resolved: ResolvedBody, - inputs: Record | undefined, + inputs: TlvInputs | undefined, dataCoding: number | undefined, settles: boolean, ): VoidResult { - for (const carried of carriedBodies(inputs)) { - const encoded = encodeBody(carried.text, dataCoding); + const text = inputs?.message_payload?.tagValue; - if (encoded.err) return { err: new Error(`TLV "${carried.name}": ${encoded.err.message}`) }; + if (typeof text !== 'string') return {}; - if (settles) resolved.params.data_coding = encoded.dataCoding; + const encoded = encodeBody(text, dataCoding); - resolved.tlvs = { ...resolved.tlvs, [carried.name]: { ...carried.tlv, tagValue: encoded.buffer } }; - } + if (encoded.err) return { err: new Error(`TLV "message_payload": ${encoded.err.message}`) }; + + if (settles) resolved.params.data_coding = encoded.dataCoding; + + resolved.tlvs = { ...resolved.tlvs, message_payload: { tagValue: encoded.buffer } }; return {}; } @@ -125,7 +110,7 @@ function resolveCarried( /** data_coding names the alphabet of the body, and short_message settles it where it carries octets. */ function resolveBody( params: Record, - tlvs: Record | undefined, + tlvs: TlvInputs | undefined, definition: CommandDefinition, ): Result { const message = writtenBody(definition, params.short_message); @@ -197,7 +182,7 @@ function buildBody( cmdName: CommandName, cmdStatus: ErrorName, params: Record, - tlvs: Record | undefined, + tlvs: TlvInputs | undefined, ): Result<{ body: Buffer }> { if (errors[cmdStatus] !== 0 && definition.id >= respBit) return { body: Buffer.alloc(0) }; @@ -221,7 +206,7 @@ function buildPdu( cmdStatus: ErrorName, seqNr: number, params: Record, - tlvs: Record | undefined, + tlvs: TlvInputs | undefined, ): Result<{ buffer: Buffer }> { const definition = cmds[cmdName]; diff --git a/src/retained-pdu.ts b/src/retained-pdu.ts index e42647d..50e429c 100644 --- a/src/retained-pdu.ts +++ b/src/retained-pdu.ts @@ -1,27 +1,21 @@ import type { ParamValue } from './defs/types.ts'; import type { PduObject } from './pdu.ts'; -import { detachedTlv, tlvOctets } from './defs/types.ts'; -import { isTlvs } from './defs/tlvs.ts'; +import { tlvOctets } from './defs/types.ts'; /** Wire reads hand back views, so retaining one PDU would pin the whole chunk it arrived in. */ export function detach(pduObj: PduObject): PduObject { const params: Record = {}; - const tlvs: Record = {}; for (const [name, value] of Object.entries(pduObj.params)) { params[name] = Buffer.isBuffer(value) ? Buffer.from(value) : value; } - for (const [name, tlv] of Object.entries(pduObj.tlvs)) { - tlvs[name] = { ...tlv, tagValue: detachedTlv(tlv.tagValue) }; - } - // short_message holds the same octets wherever it was not decoded, so one copy covers both. const octets = Buffer.isBuffer(params.short_message) ? params.short_message : pduObj.shortMessageOctets && Buffer.from(pduObj.shortMessageOctets); - return { ...pduObj, params, shortMessageOctets: octets, tlvs: isTlvs(tlvs) ? tlvs : pduObj.tlvs }; + return { ...pduObj, params, shortMessageOctets: octets }; } // Measured heap beyond the octets, so a PDU of empty fields or empty TLVs is not free. diff --git a/test/dlr.test.ts b/test/dlr.test.ts index 8e3e9a7..fa6ec84 100644 --- a/test/dlr.test.ts +++ b/test/dlr.test.ts @@ -4,7 +4,7 @@ import { consts } from '../src/defs/constants.ts'; import { dlrFromPdu, parseReceipt, receiptCodes } from '../src/dlr.ts'; import { encodeMessage } from '../src/message.ts'; import { objToPdu, pduToObj } from '../src/pdu.ts'; -import type { PduObject, TlvInput } from '../src/pdu.ts'; +import type { PduObject, TlvInputs } from '../src/pdu.ts'; const receiptText = 'id:0195f0c7 sub:001 dlvrd:001 submit date:2508251430 done date:2508251431 stat:DELIVRD err:000 text:hello there'; @@ -14,7 +14,7 @@ const textReceipt = `id:${textReceiptId} sub:001 dlvrd:001 submit date:250905143 function deliverSm( message: Buffer | string, - tlvs?: Record, + tlvs?: TlvInputs, esmClass: number = consts.ESM_CLASS.MC_DELIVERY_RECEIPT, dataCoding = 0, ): PduObject { diff --git a/test/interop.test.ts b/test/interop.test.ts index dbefb68..2e5d905 100644 --- a/test/interop.test.ts +++ b/test/interop.test.ts @@ -95,8 +95,8 @@ describe('our encoder against the reference parser', () => { }, seqNr: 77, tlvs: { - message_state: { tagId: 0x0427, tagValue: 2 }, - receipted_message_id: { tagId: 0x001E, tagValue: 'abc123' }, + message_state: { tagValue: 2 }, + receipted_message_id: { tagValue: 'abc123' }, }, })); diff --git a/test/operator-receipts.test.ts b/test/operator-receipts.test.ts index 21eec8c..36dd655 100644 --- a/test/operator-receipts.test.ts +++ b/test/operator-receipts.test.ts @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import test, { describe } from 'node:test'; import type { Dlr, Receipt } from '../src/dlr.ts'; import type { MessageDlr } from '../src/session.ts'; -import type { PduObject, TlvInput } from '../src/pdu.ts'; +import type { PduObject, TlvInputs } from '../src/pdu.ts'; import { bindToSmsc, dummySmsc } from './dummy-smsc.ts'; import { consts } from '../src/defs/constants.ts'; import { dlrFromPdu, parseReceipt, receiptCodes, transientStates } from '../src/dlr.ts'; @@ -16,7 +16,7 @@ import { objToPdu, pduToObj } from '../src/pdu.ts'; function deliverSm( body: string, - tlvs?: Record, + tlvs?: TlvInputs, esmClass: number = consts.ESM_CLASS.MC_DELIVERY_RECEIPT, ): PduObject { const { buffer } = objToPdu({ @@ -53,7 +53,7 @@ type ReceiptFixture = { name: string; receipt: Receipt; source: string; - tlvs?: Record; + tlvs?: TlvInputs; }; const fixtures: readonly ReceiptFixture[] = [ diff --git a/test/pdu.test.ts b/test/pdu.test.ts index 1d9b433..5e6388d 100644 --- a/test/pdu.test.ts +++ b/test/pdu.test.ts @@ -3,6 +3,7 @@ import test, { describe } from 'node:test'; import { PduRefusedError, refusalAnswer } from '../src/pdu-refusal.ts'; import { isCommand, isResp, objToPdu, pduReturn, pduToObj } from '../src/pdu.ts'; import { paramText } from '../src/defs/types.ts'; +import { tlvsById } from '../src/defs/tlvs.ts'; function encode(...args: Parameters): Buffer { const { buffer, err } = objToPdu(...args); @@ -377,7 +378,7 @@ describe('TLVs', () => { }, seqNr: 393, tlvs: { - 5142: { tagId: 5142, tagValue: Buffer.from('blajfoo', 'ascii') }, + 5142: { tagValue: Buffer.from('blajfoo', 'ascii') }, receipted_message_id: { tagValue: '293f293' }, }, })); @@ -432,25 +433,22 @@ describe('TLVs', () => { assert.deepEqual(pduObj.tlvs.source_port, { tagId: 0x020A, tagName: 'source_port', tagValue: 1234 }); }); - test('refuses an unknown tag name rather than putting a wrong tag on the wire', () => { - const { buffer, err } = objToPdu({ - cmdName: 'deliver_sm', - params: { destination_addr: '46709771337', short_message: 'hi', source_addr: '46701113311' }, - tlvs: { nils: { tagValue: 'blajfoo' } }, - }); + test('refuses a TLV keyed any way but by its name, or by its decimal id where the table names none', () => { + const params = { destination_addr: '46709771337', short_message: 'hi', source_addr: '46701113311' }; + const refusals = [ + // @ts-expect-error nils is no tag name + { built: objToPdu({ cmdName: 'deliver_sm', params, tlvs: { nils: { tagValue: 'blajfoo' } } }), reason: /decimal id/ }, + { built: objToPdu({ cmdName: 'deliver_sm', params, tlvs: { 5142: { tagId: 5142, tagValue: 'blajfoo' } } }), reason: /instead of giving a tagId/ }, + { built: objToPdu({ cmdName: 'deliver_sm', params, tlvs: { 65536: { tagValue: 'blajfoo' } } }), reason: /out of range/ }, + { built: objToPdu({ cmdName: 'deliver_sm', params, tlvs: { '05142': { tagValue: 'blajfoo' } } }), reason: /decimal id/ }, + { built: objToPdu({ cmdName: 'deliver_sm', params, tlvs: { 1063: { tagValue: 2 } } }), reason: /message_state/ }, + ]; - assert.equal(buffer, undefined); - assert.ok(err instanceof Error); - }); - - test('refuses a tag id that does not fit the two octet field', () => { - const { err } = objToPdu({ - cmdName: 'deliver_sm', - params: { destination_addr: '46709771337', short_message: 'hi', source_addr: '46701113311' }, - tlvs: { nils: { tagId: 0x10000, tagValue: 'blajfoo' } }, - }); - - assert.ok(err instanceof Error); + for (const { built: { buffer, err }, reason } of refusals) { + assert.equal(buffer, undefined); + assert.ok(err instanceof Error); + assert.match(err.message, reason); + } }); test('refuses a TLV too long for the two octet length field', () => { @@ -509,12 +507,33 @@ describe('TLVs', () => { } }); + test('reads every tag in the table as the type its entry declares', () => { + const bare = encode({ cmdName: 'deliver_sm', params: { destination_addr: '46709771337', source_addr: '46701113311' } }); + + for (const { id, tag, type } of Object.values(tlvsById)) { + const sized = type.size(type.default); + + assert.ok(sized.size !== undefined); + + const tlv = Buffer.alloc(4 + sized.size); + + tlv.writeUInt16BE(id, 0); + tlv.writeUInt16BE(sized.size, 2); + assert.equal(type.write(type.default, tlv, 4).err, undefined); + + const pdu = Buffer.concat([bare, tlv]); + + pdu.writeUInt32BE(pdu.length, 0); + assert.ok(Object.hasOwn(decode(pdu).tlvs, tag), tag); + } + }); + test('types each known TLV by its tag, and an unknown one as octets', () => { const pduObj = decode(encode({ cmdName: 'deliver_sm', params: { destination_addr: '46709771337', short_message: 'hi', source_addr: '46701113311' }, tlvs: { - 5142: { tagId: 5142, tagValue: Buffer.from('01', 'hex') }, + 5142: { tagValue: Buffer.from('01', 'hex') }, message_state: { tagValue: 2 }, receipted_message_id: { tagValue: '0199d8a4-5e2c-7b3f-9a61-c4e07f2d8b15' }, }, @@ -541,8 +560,8 @@ describe('TLVs', () => { }, seqNr: 323, tlvs: { - message_state: { tagId: 1063, tagValue: 2 }, - receipted_message_id: { tagId: 30, tagValue: 450 }, + message_state: { tagValue: 2 }, + receipted_message_id: { tagValue: 450 }, }, })); diff --git a/test/unsendable.test.ts b/test/unsendable.test.ts index f01aef4..3108910 100644 --- a/test/unsendable.test.ts +++ b/test/unsendable.test.ts @@ -173,18 +173,17 @@ describe('a body the PDU\'s own data_coding cannot carry', () => { assert.match(built.err.message, /U\+3042/); }); - test('refuses the body TLV under whatever name the caller keyed its tagId to', () => { + test('refuses the body TLV keyed by its id, so only message_payload is written as text', () => { const built = objToPdu({ cmdName: 'data_sm', - params: { data_coding: 0x03, destination_addr: to, source_addr: from }, - tlvs: { body: { tagId: 0x0424, tagValue: 'あいう' } }, + params: { data_coding: 0x08, destination_addr: to, source_addr: from }, + tlvs: { 1060: { tagValue: 'あいう' }, message_payload: { tagValue: 'あいう' } }, }); assert.ok(built.err instanceof Error); assert.equal(built.buffer, undefined); - assert.match(built.err.message, /"body"/); - assert.match(built.err.message, /LATIN1/); - assert.match(built.err.message, /U\+3042/); + assert.match(built.err.message, /"1060"/); + assert.match(built.err.message, /message_payload/); }); test('leaves data_coding to short_message wherever it carries octets, as messageOctets() reads it', () => { @@ -205,22 +204,6 @@ describe('a body the PDU\'s own data_coding cannot carry', () => { assert.deepEqual(messageOctets(pduObj), short); }); - test('encodes every entry carrying the body tag, so a second one cannot go out truncated', () => { - const built = objToPdu({ - cmdName: 'data_sm', - params: { data_coding: 0x08, destination_addr: to, source_addr: from }, - tlvs: { alias: { tagId: 0x0424, tagValue: 'あいう' }, message_payload: { tagValue: 'あいう' } }, - }); - - assert.equal(built.err, undefined); - assert.ok(built.buffer); - - const hex = built.buffer.toString('hex'); - - assert.equal(hex.split('304230443046').length - 1, 2, 'both entries carry the UCS2 octets'); - assert.ok(!hex.includes('424446'), 'no entry goes out as the low octets of its code points'); - }); - test('leaves the alphabet to the body TLV wherever short_message carries no octets', () => { for (const short of [undefined, '', Buffer.alloc(0)]) { const built = objToPdu({