Type each known TLV's value by its tag, on read and on write #30
+7
-3
@@ -60,9 +60,13 @@
|
|||||||
`{ tagValue: [value] }` for them and refuse a lone value before anything goes out.
|
`{ 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`:
|
- `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
|
`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
|
type for its tag fails to compile. Annotate with `Tlvs` or `TlvInputs` where you wrote
|
||||||
that narrowed `tagValue` with `typeof` still compiles; a `Record<string, Tlv>` annotation does not,
|
`Record<string, Tlv>` or `Record<string, TlvInput>`; `TlvInput` is gone.
|
||||||
so use `Tlvs`.
|
|
||||||
|
**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.
|
- `cmds.broadcast_sm_resp.tlvMap` is removed; nothing read it.
|
||||||
|
|
||||||
## 0.5.0
|
## 0.5.0
|
||||||
|
|||||||
@@ -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
|
`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
|
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.
|
`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.
|
- **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
|
- **`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).
|
a `larvitutils` one, and is silent by default: [README](README.md#logging).
|
||||||
|
|||||||
@@ -630,8 +630,9 @@ if (isCommand(pduObj, 'submit_sm')) {
|
|||||||
naming the character, its code point and where it is.
|
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
|
- 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.
|
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
|
- `tlvs` is keyed and typed like `pduObj.tlvs`, as `TlvInputs`: `{ message_state: { tagValue: 2 } }`,
|
||||||
compile. A tag the table does not define takes a `tagId`.
|
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
|
- The five repeatable TLVs take an array, written as one TLV per element; a lone value or an empty
|
||||||
array is refused.
|
array is refused.
|
||||||
- A `Buffer` goes out exactly as given under any `data_coding`: binary payloads, hand-built user
|
- A `Buffer` goes out exactly as given under any `data_coding`: binary payloads, hand-built user
|
||||||
|
|||||||
+32
-31
@@ -104,55 +104,54 @@ type Repeated<K extends TlvName, V> = Specs[K] extends { multiple: true } ? V[]
|
|||||||
|
|
||||||
type WireValue<K extends TlvName> = Specs[K]['type']['default'];
|
type WireValue<K extends TlvName> = Specs[K]['type']['default'];
|
||||||
|
|
||||||
export type TlvReadValue<K extends TlvName> = Repeated<K, WireValue<K>>;
|
type ReadValue<K extends TlvName> = Repeated<K, WireValue<K>>;
|
||||||
|
|
||||||
/** 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 a lone octet field text, which goes out as latin1. */
|
||||||
export type TlvWriteValue<K extends TlvName> = Specs[K] extends { multiple: true } ? TlvReadValue<K>
|
type WriteValue<K extends TlvName> = Specs[K] extends { multiple: true } ? ReadValue<K>
|
||||||
: WireValue<K> extends number ? number
|
: WireValue<K> extends number ? number
|
||||||
: WireValue<K> extends string ? number | string
|
: WireValue<K> extends string ? number | string
|
||||||
: Buffer | number | string;
|
: Buffer | number | string;
|
||||||
|
|
||||||
type OneTlv<K extends TlvName> = { tagId: number; tagName: K; tagValue: TlvReadValue<K> };
|
type KnownTlv<K extends TlvName> = { tagId: number; tagName: K; tagValue: ReadValue<K> };
|
||||||
|
|
||||||
export type Tlv<K extends TlvName = TlvName> = { [N in K]: OneTlv<N> }[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. */
|
/** Keyed by tag name, or by its decimal id where the table defines no name. */
|
||||||
export type UnknownTlv = { tagId: number; tagName: undefined; tagValue: Buffer };
|
export type Tlvs = { [K in TlvName]?: KnownTlv<K> } & Record<`${number}`, UnknownTlv>;
|
||||||
|
|
||||||
type KnownTlvs = { [K in TlvName]?: OneTlv<K> };
|
export type Tlv = { [K in TlvName]: KnownTlv<K> }[TlvName] | UnknownTlv;
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Alternate = keyof typeof alternates;
|
type Alternate = keyof typeof alternates;
|
||||||
|
|
||||||
type Canonical<K extends Alternate | TlvName> = K extends Alternate ? (typeof alternates)[K]['tag'] : K;
|
type Canonical<K extends Alternate | TlvName> = K extends Alternate ? (typeof alternates)[K]['tag'] : K;
|
||||||
|
|
||||||
export type TlvInputs = {
|
/** Keyed like `Tlvs`, by tag name or by the decimal id of a tag the table does not define. */
|
||||||
[K in Alternate | TlvName]?: { tagId?: number | undefined; tagValue: TlvWriteValue<Canonical<K>> };
|
export type TlvInputs = { [K in Alternate | TlvName]?: { tagValue: WriteValue<Canonical<K>> } }
|
||||||
} & Record<string, TlvInput>;
|
& Record<`${number}`, { tagValue: Buffer | number | string }>;
|
||||||
|
|
||||||
export function tagIdOf(name: string, input: TlvInput): Result<{ tagId: number }> {
|
function tagIdOf(name: string, input: object): Result<{ tagId: number }> {
|
||||||
const tagId = input.tagId ?? tlvs[name]?.id;
|
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 (tagId === undefined) {
|
|
||||||
return { err: new Error(`TLV "${name}": unknown tag name, give it a tagId`) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Number.isInteger(tagId) || tagId < 0 || tagId > 0xFFFF) {
|
if (Object.hasOwn(tlvs, name)) return { tagId: tlvs[name]?.id ?? 0 };
|
||||||
return { err: new Error(`TLV "${name}": tagId ${String(tagId)} out of range 0-65535`) };
|
|
||||||
|
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. */
|
/** Each TLV as its four octet header and the value the tag's own wire type writes. */
|
||||||
export function writeTlvs(inputs: Record<string, TlvInput> | undefined): Result<{ chunks: Buffer[] }> {
|
export function writeTlvs(inputs: TlvInputs | undefined): Result<{ chunks: Buffer[] }> {
|
||||||
const chunks: Buffer[] = [];
|
const chunks: Buffer[] = [];
|
||||||
|
|
||||||
for (const [name, input] of Object.entries(inputs ?? {})) {
|
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 };
|
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 {
|
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);
|
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. */
|
function isTlvs(record: Record<string, unknown>): record is Tlvs {
|
||||||
export function isTlvs(record: Record<string, unknown>): record is Tlvs {
|
|
||||||
return Object.entries(record).every(([key, tlv]) => isTlv(key, tlv));
|
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 };
|
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 }> {
|
export function parseTlvs(pdu: Buffer, start: number): Result<{ offset: number; tlvs: Tlvs }> {
|
||||||
|
|||||||
@@ -32,16 +32,6 @@ export function tlvOctets(value: TlvValue): number {
|
|||||||
return octets;
|
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
|
* 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.
|
* re-derive a length that could disagree with what was actually written.
|
||||||
|
|||||||
+2
-2
@@ -64,10 +64,10 @@ export type { CommandName, PduParams, PduParamsInput } from './defs/commands.ts'
|
|||||||
export type { ConstGroup, MessageState, SubmitMessagingMode } from './defs/constants.ts';
|
export type { ConstGroup, MessageState, SubmitMessagingMode } from './defs/constants.ts';
|
||||||
export type { Encoding, EncodingName, Unencodable } from './defs/encodings.ts';
|
export type { Encoding, EncodingName, Unencodable } from './defs/encodings.ts';
|
||||||
export type { ErrorName } from './defs/errors.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 { PduHeader } from './pdu-refusal.ts';
|
||||||
export type { SplitOptions } from './message.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';
|
export type { DestAddress, ParamValue, TlvValue, UnsuccessSme, WireType } from './defs/types.ts';
|
||||||
|
|
||||||
/** The spec tables, grouped the way `larvitsmpp.defs` was in 0.4.0. */
|
/** The spec tables, grouped the way `larvitsmpp.defs` was in 0.4.0. */
|
||||||
|
|||||||
+16
-31
@@ -3,14 +3,14 @@ import type { ErrorName } from './defs/errors.ts';
|
|||||||
import type { ParamValue } from './defs/types.ts';
|
import type { ParamValue } from './defs/types.ts';
|
||||||
import type { PduHeader } from './pdu-refusal.ts';
|
import type { PduHeader } from './pdu-refusal.ts';
|
||||||
import type { Result, VoidResult } from './result.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 { PduRefusedError, framingRefusal } from './pdu-refusal.ts';
|
||||||
import { cmds, commandNameById, respNameFor } from './defs/commands.ts';
|
import { cmds, commandNameById, respNameFor } from './defs/commands.ts';
|
||||||
import { hasUdh } from './defs/constants.ts';
|
import { hasUdh } from './defs/constants.ts';
|
||||||
import { decodeMessage, encodeBody } from './message.ts';
|
import { decodeMessage, encodeBody } from './message.ts';
|
||||||
import { errorNameById, errors, isErrorName } from './defs/errors.ts';
|
import { errorNameById, errors, isErrorName } from './defs/errors.ts';
|
||||||
import { paramNumber, valueText } from './defs/types.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. */
|
/** The highest sequence number this library hands out; SMPP 3.4 4.7.1 reserves 0x7fffffff. */
|
||||||
export const maxSeqNr = 2147483646;
|
export const maxSeqNr = 2147483646;
|
||||||
@@ -46,7 +46,7 @@ export type PduObject = {
|
|||||||
tlvs: Tlvs;
|
tlvs: Tlvs;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type { TlvInput, TlvInputs };
|
export type { TlvInputs };
|
||||||
|
|
||||||
const respBit = 0x80000000;
|
const respBit = 0x80000000;
|
||||||
|
|
||||||
@@ -68,30 +68,13 @@ export function isCommand<C extends CommandName>(
|
|||||||
|
|
||||||
type ResolvedBody = {
|
type ResolvedBody = {
|
||||||
params: Record<string, ParamValue | undefined>;
|
params: Record<string, ParamValue | undefined>;
|
||||||
tlvs: Record<string, TlvInput> | undefined;
|
tlvs: TlvInputs | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
function codingOf(params: Record<string, ParamValue | undefined>): number | undefined {
|
function codingOf(params: Record<string, ParamValue | undefined>): number | undefined {
|
||||||
return typeof params.data_coding === 'number' ? params.data_coding : 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<string, TlvInput> | 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. */
|
/** messageOctets() reads short_message wherever it holds an octet, and the TLV only where it does not. */
|
||||||
function carriesOctets(value: ParamValue | undefined): boolean {
|
function carriesOctets(value: ParamValue | undefined): boolean {
|
||||||
return Buffer.isBuffer(value) && value.length > 0;
|
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. */
|
/** Encoded in place, settling data_coding where `settles` says no mandatory field will carry it. */
|
||||||
function resolveCarried(
|
function resolveCarried(
|
||||||
resolved: ResolvedBody,
|
resolved: ResolvedBody,
|
||||||
inputs: Record<string, TlvInput> | undefined,
|
inputs: TlvInputs | undefined,
|
||||||
dataCoding: number | undefined,
|
dataCoding: number | undefined,
|
||||||
settles: boolean,
|
settles: boolean,
|
||||||
): VoidResult {
|
): VoidResult {
|
||||||
for (const carried of carriedBodies(inputs)) {
|
const text = inputs?.message_payload?.tagValue;
|
||||||
const encoded = encodeBody(carried.text, dataCoding);
|
|
||||||
|
|
||||||
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 {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -125,7 +110,7 @@ function resolveCarried(
|
|||||||
/** data_coding names the alphabet of the body, and short_message settles it where it carries octets. */
|
/** data_coding names the alphabet of the body, and short_message settles it where it carries octets. */
|
||||||
function resolveBody(
|
function resolveBody(
|
||||||
params: Record<string, ParamValue | undefined>,
|
params: Record<string, ParamValue | undefined>,
|
||||||
tlvs: Record<string, TlvInput> | undefined,
|
tlvs: TlvInputs | undefined,
|
||||||
definition: CommandDefinition,
|
definition: CommandDefinition,
|
||||||
): Result<ResolvedBody> {
|
): Result<ResolvedBody> {
|
||||||
const message = writtenBody(definition, params.short_message);
|
const message = writtenBody(definition, params.short_message);
|
||||||
@@ -197,7 +182,7 @@ function buildBody(
|
|||||||
cmdName: CommandName,
|
cmdName: CommandName,
|
||||||
cmdStatus: ErrorName,
|
cmdStatus: ErrorName,
|
||||||
params: Record<string, ParamValue | undefined>,
|
params: Record<string, ParamValue | undefined>,
|
||||||
tlvs: Record<string, TlvInput> | undefined,
|
tlvs: TlvInputs | undefined,
|
||||||
): Result<{ body: Buffer }> {
|
): Result<{ body: Buffer }> {
|
||||||
if (errors[cmdStatus] !== 0 && definition.id >= respBit) return { body: Buffer.alloc(0) };
|
if (errors[cmdStatus] !== 0 && definition.id >= respBit) return { body: Buffer.alloc(0) };
|
||||||
|
|
||||||
@@ -221,7 +206,7 @@ function buildPdu(
|
|||||||
cmdStatus: ErrorName,
|
cmdStatus: ErrorName,
|
||||||
seqNr: number,
|
seqNr: number,
|
||||||
params: Record<string, ParamValue | undefined>,
|
params: Record<string, ParamValue | undefined>,
|
||||||
tlvs: Record<string, TlvInput> | undefined,
|
tlvs: TlvInputs | undefined,
|
||||||
): Result<{ buffer: Buffer }> {
|
): Result<{ buffer: Buffer }> {
|
||||||
const definition = cmds[cmdName];
|
const definition = cmds[cmdName];
|
||||||
|
|
||||||
|
|||||||
+2
-8
@@ -1,27 +1,21 @@
|
|||||||
import type { ParamValue } from './defs/types.ts';
|
import type { ParamValue } from './defs/types.ts';
|
||||||
import type { PduObject } from './pdu.ts';
|
import type { PduObject } from './pdu.ts';
|
||||||
import { detachedTlv, tlvOctets } from './defs/types.ts';
|
import { tlvOctets } from './defs/types.ts';
|
||||||
import { isTlvs } from './defs/tlvs.ts';
|
|
||||||
|
|
||||||
/** Wire reads hand back views, so retaining one PDU would pin the whole chunk it arrived in. */
|
/** Wire reads hand back views, so retaining one PDU would pin the whole chunk it arrived in. */
|
||||||
export function detach(pduObj: PduObject): PduObject {
|
export function detach(pduObj: PduObject): PduObject {
|
||||||
const params: Record<string, ParamValue> = {};
|
const params: Record<string, ParamValue> = {};
|
||||||
const tlvs: Record<string, unknown> = {};
|
|
||||||
|
|
||||||
for (const [name, value] of Object.entries(pduObj.params)) {
|
for (const [name, value] of Object.entries(pduObj.params)) {
|
||||||
params[name] = Buffer.isBuffer(value) ? Buffer.from(value) : value;
|
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.
|
// short_message holds the same octets wherever it was not decoded, so one copy covers both.
|
||||||
const octets = Buffer.isBuffer(params.short_message)
|
const octets = Buffer.isBuffer(params.short_message)
|
||||||
? params.short_message
|
? params.short_message
|
||||||
: pduObj.shortMessageOctets && Buffer.from(pduObj.shortMessageOctets);
|
: 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.
|
// Measured heap beyond the octets, so a PDU of empty fields or empty TLVs is not free.
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@ import { consts } from '../src/defs/constants.ts';
|
|||||||
import { dlrFromPdu, parseReceipt, receiptCodes } from '../src/dlr.ts';
|
import { dlrFromPdu, parseReceipt, receiptCodes } from '../src/dlr.ts';
|
||||||
import { encodeMessage } from '../src/message.ts';
|
import { encodeMessage } from '../src/message.ts';
|
||||||
import { objToPdu, pduToObj } from '../src/pdu.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';
|
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(
|
function deliverSm(
|
||||||
message: Buffer | string,
|
message: Buffer | string,
|
||||||
tlvs?: Record<string, TlvInput>,
|
tlvs?: TlvInputs,
|
||||||
esmClass: number = consts.ESM_CLASS.MC_DELIVERY_RECEIPT,
|
esmClass: number = consts.ESM_CLASS.MC_DELIVERY_RECEIPT,
|
||||||
dataCoding = 0,
|
dataCoding = 0,
|
||||||
): PduObject {
|
): PduObject {
|
||||||
|
|||||||
@@ -95,8 +95,8 @@ describe('our encoder against the reference parser', () => {
|
|||||||
},
|
},
|
||||||
seqNr: 77,
|
seqNr: 77,
|
||||||
tlvs: {
|
tlvs: {
|
||||||
message_state: { tagId: 0x0427, tagValue: 2 },
|
message_state: { tagValue: 2 },
|
||||||
receipted_message_id: { tagId: 0x001E, tagValue: 'abc123' },
|
receipted_message_id: { tagValue: 'abc123' },
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
|
|||||||
import test, { describe } from 'node:test';
|
import test, { describe } from 'node:test';
|
||||||
import type { Dlr, Receipt } from '../src/dlr.ts';
|
import type { Dlr, Receipt } from '../src/dlr.ts';
|
||||||
import type { MessageDlr } from '../src/session.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 { bindToSmsc, dummySmsc } from './dummy-smsc.ts';
|
||||||
import { consts } from '../src/defs/constants.ts';
|
import { consts } from '../src/defs/constants.ts';
|
||||||
import { dlrFromPdu, parseReceipt, receiptCodes, transientStates } from '../src/dlr.ts';
|
import { dlrFromPdu, parseReceipt, receiptCodes, transientStates } from '../src/dlr.ts';
|
||||||
@@ -16,7 +16,7 @@ import { objToPdu, pduToObj } from '../src/pdu.ts';
|
|||||||
|
|
||||||
function deliverSm(
|
function deliverSm(
|
||||||
body: string,
|
body: string,
|
||||||
tlvs?: Record<string, TlvInput>,
|
tlvs?: TlvInputs,
|
||||||
esmClass: number = consts.ESM_CLASS.MC_DELIVERY_RECEIPT,
|
esmClass: number = consts.ESM_CLASS.MC_DELIVERY_RECEIPT,
|
||||||
): PduObject {
|
): PduObject {
|
||||||
const { buffer } = objToPdu({
|
const { buffer } = objToPdu({
|
||||||
@@ -53,7 +53,7 @@ type ReceiptFixture = {
|
|||||||
name: string;
|
name: string;
|
||||||
receipt: Receipt;
|
receipt: Receipt;
|
||||||
source: string;
|
source: string;
|
||||||
tlvs?: Record<string, TlvInput>;
|
tlvs?: TlvInputs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fixtures: readonly ReceiptFixture[] = [
|
const fixtures: readonly ReceiptFixture[] = [
|
||||||
|
|||||||
+41
-22
@@ -3,6 +3,7 @@ import test, { describe } from 'node:test';
|
|||||||
import { PduRefusedError, refusalAnswer } from '../src/pdu-refusal.ts';
|
import { PduRefusedError, refusalAnswer } from '../src/pdu-refusal.ts';
|
||||||
import { isCommand, isResp, objToPdu, pduReturn, pduToObj } from '../src/pdu.ts';
|
import { isCommand, isResp, objToPdu, pduReturn, pduToObj } from '../src/pdu.ts';
|
||||||
import { paramText } from '../src/defs/types.ts';
|
import { paramText } from '../src/defs/types.ts';
|
||||||
|
import { tlvsById } from '../src/defs/tlvs.ts';
|
||||||
|
|
||||||
function encode(...args: Parameters<typeof objToPdu>): Buffer {
|
function encode(...args: Parameters<typeof objToPdu>): Buffer {
|
||||||
const { buffer, err } = objToPdu(...args);
|
const { buffer, err } = objToPdu(...args);
|
||||||
@@ -377,7 +378,7 @@ describe('TLVs', () => {
|
|||||||
},
|
},
|
||||||
seqNr: 393,
|
seqNr: 393,
|
||||||
tlvs: {
|
tlvs: {
|
||||||
5142: { tagId: 5142, tagValue: Buffer.from('blajfoo', 'ascii') },
|
5142: { tagValue: Buffer.from('blajfoo', 'ascii') },
|
||||||
receipted_message_id: { tagValue: '293f293' },
|
receipted_message_id: { tagValue: '293f293' },
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -432,25 +433,22 @@ describe('TLVs', () => {
|
|||||||
assert.deepEqual(pduObj.tlvs.source_port, { tagId: 0x020A, tagName: 'source_port', tagValue: 1234 });
|
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', () => {
|
test('refuses a TLV keyed any way but by its name, or by its decimal id where the table names none', () => {
|
||||||
const { buffer, err } = objToPdu({
|
const params = { destination_addr: '46709771337', short_message: 'hi', source_addr: '46701113311' };
|
||||||
cmdName: 'deliver_sm',
|
const refusals = [
|
||||||
params: { destination_addr: '46709771337', short_message: 'hi', source_addr: '46701113311' },
|
// @ts-expect-error nils is no tag name
|
||||||
tlvs: { nils: { tagValue: 'blajfoo' } },
|
{ 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);
|
for (const { built: { buffer, err }, reason } of refusals) {
|
||||||
assert.ok(err instanceof Error);
|
assert.equal(buffer, undefined);
|
||||||
});
|
assert.ok(err instanceof Error);
|
||||||
|
assert.match(err.message, reason);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('refuses a TLV too long for the two octet length field', () => {
|
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', () => {
|
test('types each known TLV by its tag, and an unknown one as octets', () => {
|
||||||
const pduObj = decode(encode({
|
const pduObj = decode(encode({
|
||||||
cmdName: 'deliver_sm',
|
cmdName: 'deliver_sm',
|
||||||
params: { destination_addr: '46709771337', short_message: 'hi', source_addr: '46701113311' },
|
params: { destination_addr: '46709771337', short_message: 'hi', source_addr: '46701113311' },
|
||||||
tlvs: {
|
tlvs: {
|
||||||
5142: { tagId: 5142, tagValue: Buffer.from('01', 'hex') },
|
5142: { tagValue: Buffer.from('01', 'hex') },
|
||||||
message_state: { tagValue: 2 },
|
message_state: { tagValue: 2 },
|
||||||
receipted_message_id: { tagValue: '0199d8a4-5e2c-7b3f-9a61-c4e07f2d8b15' },
|
receipted_message_id: { tagValue: '0199d8a4-5e2c-7b3f-9a61-c4e07f2d8b15' },
|
||||||
},
|
},
|
||||||
@@ -541,8 +560,8 @@ describe('TLVs', () => {
|
|||||||
},
|
},
|
||||||
seqNr: 323,
|
seqNr: 323,
|
||||||
tlvs: {
|
tlvs: {
|
||||||
message_state: { tagId: 1063, tagValue: 2 },
|
message_state: { tagValue: 2 },
|
||||||
receipted_message_id: { tagId: 30, tagValue: 450 },
|
receipted_message_id: { tagValue: 450 },
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
+5
-22
@@ -173,18 +173,17 @@ describe('a body the PDU\'s own data_coding cannot carry', () => {
|
|||||||
assert.match(built.err.message, /U\+3042/);
|
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({
|
const built = objToPdu({
|
||||||
cmdName: 'data_sm',
|
cmdName: 'data_sm',
|
||||||
params: { data_coding: 0x03, destination_addr: to, source_addr: from },
|
params: { data_coding: 0x08, destination_addr: to, source_addr: from },
|
||||||
tlvs: { body: { tagId: 0x0424, tagValue: 'あいう' } },
|
tlvs: { 1060: { tagValue: 'あいう' }, message_payload: { tagValue: 'あいう' } },
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.ok(built.err instanceof Error);
|
assert.ok(built.err instanceof Error);
|
||||||
assert.equal(built.buffer, undefined);
|
assert.equal(built.buffer, undefined);
|
||||||
assert.match(built.err.message, /"body"/);
|
assert.match(built.err.message, /"1060"/);
|
||||||
assert.match(built.err.message, /LATIN1/);
|
assert.match(built.err.message, /message_payload/);
|
||||||
assert.match(built.err.message, /U\+3042/);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('leaves data_coding to short_message wherever it carries octets, as messageOctets() reads it', () => {
|
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);
|
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', () => {
|
test('leaves the alphabet to the body TLV wherever short_message carries no octets', () => {
|
||||||
for (const short of [undefined, '', Buffer.alloc(0)]) {
|
for (const short of [undefined, '', Buffer.alloc(0)]) {
|
||||||
const built = objToPdu({
|
const built = objToPdu({
|
||||||
|
|||||||
Reference in New Issue
Block a user