Key a TLV input like the read side, drop tagId, copy TLV octets at parse, and export only Tlvs, TlvInputs and Tlv
Mirror / push (push) Has been cancelled
Test / lint (pull_request) Successful in 29s
Test / test (18) (pull_request) Successful in 31s
Test / test (20) (pull_request) Successful in 31s
Test / test (22) (pull_request) Successful in 37s
Test / test (24) (pull_request) Successful in 31s
Test / test (26) (pull_request) Successful in 38s
Mirror / push (push) Has been cancelled
Test / lint (pull_request) Successful in 29s
Test / test (18) (pull_request) Successful in 31s
Test / test (20) (pull_request) Successful in 31s
Test / test (22) (pull_request) Successful in 37s
Test / test (24) (pull_request) Successful in 31s
Test / test (26) (pull_request) Successful in 38s
This commit is contained in:
+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'];
|
||||
|
||||
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. */
|
||||
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 string ? 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. */
|
||||
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<K> } & Record<`${number}`, UnknownTlv>;
|
||||
|
||||
type KnownTlvs = { [K in TlvName]?: OneTlv<K> };
|
||||
|
||||
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<K> }[TlvName] | UnknownTlv;
|
||||
|
||||
type Alternate = keyof typeof alternates;
|
||||
|
||||
type Canonical<K extends Alternate | TlvName> = K extends Alternate ? (typeof alternates)[K]['tag'] : K;
|
||||
|
||||
export type TlvInputs = {
|
||||
[K in Alternate | TlvName]?: { tagId?: number | undefined; tagValue: TlvWriteValue<Canonical<K>> };
|
||||
} & Record<string, TlvInput>;
|
||||
/** 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<Canonical<K>> } }
|
||||
& 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<string, TlvInput> | 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<string, unknown>): record is Tlvs {
|
||||
function isTlvs(record: Record<string, unknown>): 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 }> {
|
||||
|
||||
@@ -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.
|
||||
|
||||
+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 { 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. */
|
||||
|
||||
+16
-31
@@ -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<C extends CommandName>(
|
||||
|
||||
type ResolvedBody = {
|
||||
params: Record<string, ParamValue | undefined>;
|
||||
tlvs: Record<string, TlvInput> | undefined;
|
||||
tlvs: TlvInputs | undefined;
|
||||
};
|
||||
|
||||
function codingOf(params: Record<string, ParamValue | undefined>): 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<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. */
|
||||
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<string, TlvInput> | 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<string, ParamValue | undefined>,
|
||||
tlvs: Record<string, TlvInput> | undefined,
|
||||
tlvs: TlvInputs | undefined,
|
||||
definition: CommandDefinition,
|
||||
): Result<ResolvedBody> {
|
||||
const message = writtenBody(definition, params.short_message);
|
||||
@@ -197,7 +182,7 @@ function buildBody(
|
||||
cmdName: CommandName,
|
||||
cmdStatus: ErrorName,
|
||||
params: Record<string, ParamValue | undefined>,
|
||||
tlvs: Record<string, TlvInput> | 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<string, ParamValue | undefined>,
|
||||
tlvs: Record<string, TlvInput> | undefined,
|
||||
tlvs: TlvInputs | undefined,
|
||||
): Result<{ buffer: Buffer }> {
|
||||
const definition = cmds[cmdName];
|
||||
|
||||
|
||||
+2
-8
@@ -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<string, ParamValue> = {};
|
||||
const tlvs: Record<string, unknown> = {};
|
||||
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user