Count and detach a repeatable TLV's occurrences in reassembly, and let only byte or integer tags repeat
Mirror / push (push) Successful in 4s
Test / lint (pull_request) Successful in 21s
Test / test (18) (pull_request) Successful in 29s
Test / test (20) (pull_request) Successful in 30s
Test / test (22) (pull_request) Successful in 35s
Test / test (24) (pull_request) Successful in 30s
Test / test (26) (pull_request) Successful in 29s
Mirror / push (push) Successful in 4s
Test / lint (pull_request) Successful in 21s
Test / test (18) (pull_request) Successful in 29s
Test / test (20) (pull_request) Successful in 30s
Test / test (22) (pull_request) Successful in 35s
Test / test (24) (pull_request) Successful in 30s
Test / test (26) (pull_request) Successful in 29s
This commit is contained in:
+72
-10
@@ -2,17 +2,14 @@ import type { ParamValue, TlvValue, WireType } from './types.ts';
|
||||
import type { Result } from '../result.ts';
|
||||
import { tlv } from './types.ts';
|
||||
|
||||
export type TlvDefinition = {
|
||||
id: number;
|
||||
multiple?: boolean;
|
||||
tag: string;
|
||||
type: WireType;
|
||||
};
|
||||
/** Only a tag read as octets or as a number may repeat, since its occurrences are listed as one of those. */
|
||||
type Definition<Tag> = { id: number; multiple?: false; tag: Tag; type: WireType<Buffer | number | string> }
|
||||
| { id: number; multiple: true; tag: Tag; type: WireType<Buffer> | WireType<number> };
|
||||
|
||||
export type TlvDefinition = Definition<string>;
|
||||
|
||||
/** The constraint keys every definition to its own name, so a `tag` that drifts fails to compile. */
|
||||
const tlvSpecs = <T extends { [K in keyof T]: { id: number; multiple?: boolean; tag: K; type: WireType } }>(
|
||||
definitions: T,
|
||||
): T => definitions;
|
||||
const tlvSpecs = <T extends { [K in keyof T]: Definition<K> }>(definitions: T): T => definitions;
|
||||
|
||||
// Ordered by tag id, mirroring the SMPP 5.0 TLV table.
|
||||
const specs = tlvSpecs({
|
||||
@@ -98,7 +95,7 @@ for (const definition of Object.values<TlvDefinition>(specs)) {
|
||||
}
|
||||
|
||||
/** Fallback for tags this table does not know: keep the raw octets. */
|
||||
export const tlvDefault: WireType = tlv.buffer;
|
||||
export const tlvDefault: WireType<Buffer> = tlv.buffer;
|
||||
|
||||
export type Tlv = {
|
||||
tagId: number;
|
||||
@@ -182,3 +179,68 @@ function writeTlv(tagId: number, type: WireType, value: ParamValue): Result<{ ch
|
||||
|
||||
return written.err ? { err: written.err } : { chunk };
|
||||
}
|
||||
|
||||
type Occurrence = { definition: TlvDefinition | undefined; tagId: number; value: Buffer | number | string };
|
||||
|
||||
function readTlv(pdu: Buffer, offset: number): Result<{ octets: number; occurrence: Occurrence }> {
|
||||
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 };
|
||||
|
||||
return { occurrence: { definition, tagId, value: read.value }, octets: 4 + tagLength };
|
||||
}
|
||||
|
||||
/** Keyed by tag name, a repeatable tag listing every occurrence in wire order and any other keeping its last. */
|
||||
function keyedTlvs(occurrences: Occurrence[]): Record<string, Tlv> {
|
||||
const repeated = new Map<string, { tagId: number; values: Occurrence['value'][] }>();
|
||||
const tlvs: Record<string, Tlv> = {};
|
||||
|
||||
for (const { definition, tagId, value } of occurrences) {
|
||||
const key = definition?.tag ?? tagId.toString();
|
||||
|
||||
if (definition?.multiple === true) {
|
||||
const entry = repeated.get(key) ?? { tagId, values: [] };
|
||||
|
||||
entry.values.push(value);
|
||||
repeated.set(key, entry);
|
||||
} else {
|
||||
tlvs[key] = { tagId, tagName: definition?.tag, tagValue: value };
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, { tagId, values }] of repeated) {
|
||||
const buffers = values.filter(value => Buffer.isBuffer(value));
|
||||
|
||||
tlvs[key] = {
|
||||
tagId,
|
||||
tagName: key,
|
||||
tagValue: buffers.length === values.length ? buffers : values.filter(value => typeof value === 'number'),
|
||||
};
|
||||
}
|
||||
|
||||
return tlvs;
|
||||
}
|
||||
|
||||
export function parseTlvs(pdu: Buffer, start: number): Result<{ offset: number; tlvs: Record<string, Tlv> }> {
|
||||
const occurrences: Occurrence[] = [];
|
||||
let offset = start;
|
||||
|
||||
while (offset + 4 <= pdu.length) {
|
||||
const read = readTlv(pdu, offset);
|
||||
|
||||
if (read.err) return { err: read.err };
|
||||
|
||||
occurrences.push(read.occurrence);
|
||||
offset += read.octets;
|
||||
}
|
||||
|
||||
return { offset, tlvs: keyedTlvs(occurrences) };
|
||||
}
|
||||
|
||||
@@ -17,6 +17,31 @@ export type ParamValue = Buffer | DestAddress[] | UnsuccessSme[] | number | stri
|
||||
/** A tag defined `multiple` holds every occurrence, in wire order; any other tag holds one value. */
|
||||
export type TlvValue = Buffer | Buffer[] | number | number[] | string;
|
||||
|
||||
/** Octets a value holds, counting a string by its length. */
|
||||
export function tlvOctets(value: TlvValue): number {
|
||||
if (Buffer.isBuffer(value)) return value.length;
|
||||
if (typeof value === 'string') return value.length;
|
||||
if (typeof value === 'number') return 0;
|
||||
|
||||
let octets = 0;
|
||||
|
||||
for (const one of value) {
|
||||
octets += typeof one === 'number' ? 0 : one.length;
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user