Read every occurrence of a repeatable TLV, and drop the unread tlvMap
Test / lint (pull_request) Successful in 20s
Test / test (18) (pull_request) Successful in 30s
Test / test (20) (pull_request) Successful in 28s
Test / test (22) (pull_request) Successful in 30s
Test / test (24) (pull_request) Successful in 29s
Mirror / push (push) Successful in 4s
Test / test (26) (pull_request) Successful in 30s

This commit is contained in:
2026-09-24 00:13:38 +02:00
parent c5ef4c4703
commit 10877c9f5f
11 changed files with 151 additions and 56 deletions
-2
View File
@@ -4,7 +4,6 @@ import { buffer, cstring, dest_address_array, int8, unsuccess_sme_array } from '
type CommandSpec = {
id: number;
params?: Record<string, WireType>;
tlvMap?: Record<string, string>;
};
const bindParams = {
@@ -59,7 +58,6 @@ const specs = {
broadcast_sm_resp: {
id: 0x80000111,
params: { message_id: cstring },
tlvMap: { broadcast_area_identifier: 'failed_broadcast_area_identifier' },
},
cancel_broadcast_sm: {
id: 0x00000113,
+44 -24
View File
@@ -1,4 +1,4 @@
import type { ParamValue, WireType } from './types.ts';
import type { ParamValue, TlvValue, WireType } from './types.ts';
import type { Result } from '../result.ts';
import { tlv } from './types.ts';
@@ -103,13 +103,13 @@ export const tlvDefault: WireType = tlv.buffer;
export type Tlv = {
tagId: number;
tagName: string | undefined;
tagValue: ParamValue;
tagValue: TlvValue;
};
export type TlvInput = {
/** Resolved from the record key; pass it for a tag the TLV table does not define. */
tagId?: number | undefined;
tagValue: ParamValue;
tagValue: TlvValue;
};
export function tagIdOf(name: string, input: TlvInput): Result<{ tagId: number }> {
@@ -135,30 +135,50 @@ export function writeTlvs(inputs: Record<string, TlvInput> | undefined): Result<
if (tag.err) return { err: tag.err };
const type = tlvsById[tag.tagId]?.type ?? tlvDefault;
const sized = type.size(input.tagValue);
const definition = tlvsById[tag.tagId];
const values = occurrences(input.tagValue, definition?.multiple === true);
if (sized.err) {
return { err: new Error(`TLV "${name}": ${sized.err.message}`) };
if (values.err) return { err: new Error(`TLV "${name}": ${values.err.message}`) };
for (const value of values.values) {
const chunk = writeTlv(tag.tagId, definition?.type ?? tlvDefault, value);
if (chunk.err) return { err: new Error(`TLV "${name}": ${chunk.err.message}`) };
chunks.push(chunk.chunk);
}
if (sized.size > 0xffff) {
return { err: new Error(`TLV "${name}": ${String(sized.size)} octets overflow the two octet length`) };
}
const chunk = Buffer.alloc(sized.size + 4);
chunk.writeUInt16BE(tag.tagId, 0);
chunk.writeUInt16BE(sized.size, 2);
const written = type.write(input.tagValue, chunk, 4);
if (written.err) {
return { err: new Error(`TLV "${name}": ${written.err.message}`) };
}
chunks.push(chunk);
}
return { chunks };
}
function occurrences(value: TlvValue, multiple: boolean): Result<{ values: ParamValue[] }> {
if (!multiple) {
return Array.isArray(value) ? { err: new Error('takes one value, not an array') } : { values: [value] };
}
if (!Array.isArray(value)) return { err: new Error('is repeatable, give an array of its values') };
if (value.length === 0) return { err: new Error('holds no values, omit it instead') };
return { values: value };
}
function writeTlv(tagId: number, type: WireType, value: ParamValue): Result<{ chunk: Buffer }> {
const sized = type.size(value);
if (sized.err) return { err: sized.err };
if (sized.size > 0xffff) {
return { err: new Error(`${String(sized.size)} octets overflow the two octet length`) };
}
const chunk = Buffer.alloc(sized.size + 4);
chunk.writeUInt16BE(tagId, 0);
chunk.writeUInt16BE(sized.size, 2);
const written = type.write(value, chunk, 4);
return written.err ? { err: written.err } : { chunk };
}
+4 -1
View File
@@ -14,6 +14,9 @@ export type UnsuccessSme = {
export type ParamValue = Buffer | DestAddress[] | UnsuccessSme[] | number | string;
/** A tag defined `multiple` holds every occurrence, in wire order; any other tag holds one value. */
export type TlvValue = Buffer | Buffer[] | number | number[] | string;
/**
* 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.
@@ -26,7 +29,7 @@ export type WireType<T extends ParamValue = ParamValue> = {
};
/** Renders a parameter as text without ever falling back to "[object Object]". */
export function paramText(value: ParamValue | undefined): string {
export function paramText(value: ParamValue | TlvValue | undefined): string {
if (typeof value === 'string') return value;
if (typeof value === 'number') return value.toString();
if (Buffer.isBuffer(value)) return value.toString('latin1');
+4 -4
View File
@@ -1,5 +1,5 @@
import type { MessageState } from './defs/constants.ts';
import type { ParamValue } from './defs/types.ts';
import type { TlvValue } from './defs/types.ts';
import type { PduObject } from './pdu.ts';
import type { SmsIdFormat } from './sms-id.ts';
import { consts, constsById, hasUdh, messageTypeOf } from './defs/constants.ts';
@@ -150,7 +150,7 @@ const smeMessageTypes: readonly number[] = [
consts.ESM_CLASS.USER_ACKNOWLEDGEMENT,
];
function nonEmptyText(value: ParamValue | undefined): string | undefined {
function nonEmptyText(value: TlvValue | undefined): string | undefined {
return typeof value === 'string' && value !== '' ? value : undefined;
}
@@ -178,7 +178,7 @@ function receiptBody(pduObj: PduObject): string {
}
function receiptId(
tlvId: ParamValue | undefined,
tlvId: TlvValue | undefined,
receipt: Receipt | undefined,
format: SmsIdFormat,
): string | undefined {
@@ -198,7 +198,7 @@ function isMessageState(name: string | undefined): name is MessageState {
/** The state TLV wins where it names a state we know; an unnameable one leaves the body to say. */
function receiptStatus(
tlvState: ParamValue | undefined,
tlvState: TlvValue | undefined,
receipt: Receipt | undefined,
): { statusId: number; statusMsg: MessageState | undefined } {
const scraped = receiptStates[receipt?.stat?.toUpperCase() ?? ''];
+1 -1
View File
@@ -68,7 +68,7 @@ export type { PduObject, PduObjectInput, TlvInput } from './pdu.ts';
export type { PduHeader } from './pdu-refusal.ts';
export type { SplitOptions } from './message.ts';
export type { Tlv, TlvDefinition, TlvName } from './defs/tlvs.ts';
export type { DestAddress, ParamValue, 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. */
export { defs } from './defs/index.ts';
+46 -17
View File
@@ -1,6 +1,6 @@
import type { CommandDefinition, CommandName, PduParams, PduParamsInput } from './defs/commands.ts';
import type { ErrorName } from './defs/errors.ts';
import type { ParamValue } from './defs/types.ts';
import type { ParamValue, TlvValue } from './defs/types.ts';
import type { PduHeader } from './pdu-refusal.ts';
import type { Result, VoidResult } from './result.ts';
import type { Tlv, TlvInput } from './defs/tlvs.ts';
@@ -262,30 +262,59 @@ export function objToPdu<C extends CommandName>(obj: PduObjectInput<C>): Result<
);
}
type Repeats = { buffers: Map<string, Buffer[]>; numbers: Map<string, number[]> };
/** A repeatable tag's occurrences so far, `value` appended; a value no TLV type reads is undefined. */
function tlvValue(value: ParamValue, key: string, multiple: boolean | undefined, repeats: Repeats): TlvValue | undefined {
if (Buffer.isBuffer(value)) return multiple === true ? appended(repeats.buffers, key, value) : value;
if (typeof value === 'number') return multiple === true ? appended(repeats.numbers, key, value) : value;
if (typeof value === 'string' && multiple !== true) return value;
return undefined;
}
function appended<T>(lists: Map<string, T[]>, key: string, value: T): T[] {
const list = lists.get(key) ?? [];
list.push(value);
lists.set(key, list);
return list;
}
function readTlv(pdu: Buffer, offset: number, repeats: Repeats): Result<{ key: string; octets: number; tlv: Tlv }> {
const tagId = pdu.readUInt16BE(offset);
const tagLength = pdu.readUInt16BE(offset + 2);
if (offset + 4 + tagLength > pdu.length) {
return { err: new Error(`TLV ${String(tagId)} runs past the end of the PDU`) };
}
const definition = tlvsById[tagId];
const read = (definition?.type ?? tlvDefault).read(pdu, offset + 4, tagLength);
if (read.err) return { err: read.err };
const key = definition?.tag ?? tagId.toString();
const tagValue = tlvValue(read.value, key, definition?.multiple, repeats);
if (tagValue === undefined) return { err: new Error(`TLV ${String(tagId)} read as a value no TLV holds`) };
return { key, octets: 4 + tagLength, tlv: { tagId, tagName: definition?.tag, tagValue } };
}
function parseTlvs(pdu: Buffer, start: number): Result<{ offset: number; tlvs: Record<string, Tlv> }> {
const repeats: Repeats = { buffers: new Map(), numbers: new Map() };
const tlvs: Record<string, Tlv> = {};
let offset = start;
while (offset + 4 <= pdu.length) {
const tagId = pdu.readUInt16BE(offset);
const tagLength = pdu.readUInt16BE(offset + 2);
if (offset + 4 + tagLength > pdu.length) {
return { err: new Error(`TLV ${String(tagId)} runs past the end of the PDU`) };
}
const definition = tlvsById[tagId];
const read = (definition?.type ?? tlvDefault).read(pdu, offset + 4, tagLength);
const read = readTlv(pdu, offset, repeats);
if (read.err) return { err: read.err };
tlvs[definition?.tag ?? tagId.toString()] = {
tagId,
tagName: definition?.tag,
tagValue: read.value,
};
offset += 4 + tagLength;
tlvs[read.key] = read.tlv;
offset += read.octets;
}
return { offset, tlvs };