diff --git a/CHANGELOG.md b/CHANGELOG.md index 009b357..334e5b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,9 +63,11 @@ 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 TLV input is keyed by its name, or by its decimal id where the table names none, and a `tagId` + that disagrees with its key is refused.** Write `{ 5142: { tagValue } }` for a vendor tag, not + `{ vendor: { tagId: 5142, … } }`; `{ message_state: { tagId: 5, … } }` used to go out as tag 5. A + parsed PDU's `tlvs` still relay as they are. A number for an octet TLV, vendor tags included, is + refused, where it went out as its ASCII digits. A decimal key naming a tag the table knows, `{ 1063: … }`, is refused in favour of the name, and so are `alert_on_msg_delivery` and `failed_broadcast_area_identifier` in favour of `alert_on_message_delivery` and `broadcast_area_identifier`, the names they read back under. diff --git a/MIGRATION.md b/MIGRATION.md index 3d01adc..77d4253 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -30,8 +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 } }`. +- **A TLV is keyed by its name, or by its decimal id where the table names none**, and a `tagId` + disagreeing with its key is refused: `{ 5142: { tagValue } }`, not + `{ vendor: { tagId: 5142, tagValue } }`. A number for an octet TLV is refused; give a Buffer. Write `alert_on_message_delivery` and `broadcast_area_identifier`, the names they read back under, for `alert_on_msg_delivery` and `failed_broadcast_area_identifier`. - **The `error` event is `sessionError`**, and `serverError` on the server handle. diff --git a/README.md b/README.md index 38aefcb..864aa30 100644 --- a/README.md +++ b/README.md @@ -630,9 +630,10 @@ 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 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. +- `tlvs` is keyed and typed like `pduObj.tlvs`, as `TlvInputs`, so a parsed PDU's `tlvs` relay as + they are: `{ 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, a `tagId` disagreeing with its + key, and a number for an octet TLV 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 b3c6dcb..74a557a 100644 --- a/src/defs/tlvs.ts +++ b/src/defs/tlvs.ts @@ -106,7 +106,7 @@ type WireValue = Specs[K]['type']['default']; type ReadValue = Repeated>; -/** A lone text field also takes a number, and a lone octet field text, which goes out as latin1. */ +/** A lone text field also takes a number, and an octet field text, which goes out as latin1. */ type WriteValue = Specs[K] extends { multiple: true } ? ReadValue : WireValue extends number ? number : WireValue extends string ? number | string @@ -118,13 +118,13 @@ type KnownTlv = { tagId: number; tagName: K; tagValue: ReadVa 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>; +export type Tlvs = { [K in TlvName]?: KnownTlv } & Partial>; export type Tlv = { [K in TlvName]: KnownTlv }[TlvName] | UnknownTlv; /** Keyed like `Tlvs`, by tag name or by the decimal id of a tag the table does not define. */ export type TlvInputs = { [K in TlvName]?: { tagValue: WriteValue } } - & Record<`${number}`, { tagValue: Buffer | number | string }>; + & Partial>; function isTlvInput(input: unknown): input is { tagValue: TlvValue } { if (typeof input !== 'object' || input === null || !('tagValue' in input)) return false; @@ -136,16 +136,8 @@ function isTlvInput(input: unknown): input is { tagValue: TlvValue } { return value.every(one => Buffer.isBuffer(one)) || value.every(one => typeof one === 'number'); } -function entryOf(name: string, input: unknown): Result<{ tagId: number; tagValue: TlvValue }> { - if (!isTlvInput(input)) { - return { err: new Error(`TLV "${name}": give it as { tagValue }, holding a Buffer, a number, a string, or an array of Buffers or of numbers`) }; - } - - 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 (isTlvName(name)) return { tagId: specs[name].id, tagValue: input.tagValue }; +function keyedTagId(name: string): Result<{ tagId: number }> { + if (isTlvName(name)) return { tagId: specs[name].id }; const alternate = Object.hasOwn(tlvs, name) ? tlvs[name] : undefined; @@ -161,9 +153,24 @@ function entryOf(name: string, input: unknown): Result<{ tagId: number; tagValue const known = tlvsById[tagId]; - return known - ? { err: new Error(`TLV "${name}": the table names this tag ${known.tag}, key it by that`) } - : { tagId, tagValue: input.tagValue }; + return known ? { err: new Error(`TLV "${name}": the table names this tag ${known.tag}, key it by that`) } : { tagId }; +} + +/** The key names the tag; a `tagId` beside it, as a parsed TLV carries, has to agree. */ +function entryOf(name: string, input: unknown): Result<{ tagId: number; tagValue: TlvValue }> { + if (!isTlvInput(input)) { + return { err: new Error(`TLV "${name}": give it as { tagValue }, holding a Buffer, a number, a string, or an array of Buffers or of numbers`) }; + } + + const keyed = keyedTagId(name); + + if (keyed.err) return { err: keyed.err }; + + if ('tagId' in input && input.tagId !== keyed.tagId) { + return { err: new Error(`TLV "${name}": tagId ${String(input.tagId)} is not the tag its key names, ${String(keyed.tagId)}; drop the tagId`) }; + } + + return { tagId: keyed.tagId, tagValue: input.tagValue }; } /** Each TLV as its four octet header and the value the tag's own wire type writes. */ @@ -205,6 +212,10 @@ function occurrences(value: TlvValue, multiple: boolean): Result<{ values: Param } function writeTlv(tagId: number, type: WireType, value: ParamValue): Result<{ chunk: Buffer }> { + if (type === tlvDefault && typeof value === 'number') { + return { err: new Error('holds octets, which a number would write as its digits; give a Buffer or a string') }; + } + const sized = type.size(value); if (sized.err) return { err: sized.err }; diff --git a/src/retained-pdu.ts b/src/retained-pdu.ts index 50e429c..c46e2c9 100644 --- a/src/retained-pdu.ts +++ b/src/retained-pdu.ts @@ -38,6 +38,8 @@ export function retainedOctets(pduObj: PduObject): number { } for (const tlv of Object.values(pduObj.tlvs)) { + if (tlv === undefined) continue; + const listed = Array.isArray(tlv.tagValue) ? tlv.tagValue.length : 0; octets += tlvOctets(tlv.tagValue) + (1 + listed) * tlvObjectOverhead; diff --git a/test/pdu.test.ts b/test/pdu.test.ts index 52d2723..dc373d3 100644 --- a/test/pdu.test.ts +++ b/test/pdu.test.ts @@ -443,7 +443,10 @@ describe('TLVs', () => { 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: { 5142: { tagId: 5143, tagValue: 'blajfoo' } } }), reason: /tagId 5143/ }, + // @ts-expect-error the key names the tag + { built: objToPdu({ cmdName: 'deliver_sm', params, tlvs: { message_state: { tagId: 5, tagValue: 2 } } }), reason: /tagId 5/ }, + { built: objToPdu({ cmdName: 'deliver_sm', params, tlvs: { 5142: { tagValue: 300 } } }), reason: /Buffer/ }, { 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/ }, @@ -537,6 +540,21 @@ describe('TLVs', () => { } }); + test('relays a parsed PDU\'s TLVs back out as they arrived', () => { + const params = { destination_addr: '46709771337', short_message: 'hi', source_addr: '46701113311' }; + const parsed = decode(encode({ + cmdName: 'deliver_sm', + params, + tlvs: { + 5142: { tagValue: Buffer.from('01', 'hex') }, + callback_num: { tagValue: [Buffer.from('0146709771337', 'hex')] }, + receipted_message_id: { tagValue: '0199d8a4-5e2c-7b3f-9a61-c4e07f2d8b15' }, + }, + })); + + assert.deepEqual(decode(encode({ cmdName: 'deliver_sm', params, tlvs: parsed.tlvs })).tlvs, parsed.tlvs); + }); + test('types each known TLV by its tag, and an unknown one as octets', () => { const pduObj = decode(encode({ cmdName: 'deliver_sm',