9c83dd1d99
Mirror / push (push) Successful in 6s
Test / lint (pull_request) Successful in 19s
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 28s
Test / test (26) (pull_request) Successful in 30s
203 lines
9.2 KiB
TypeScript
203 lines
9.2 KiB
TypeScript
import type { WireType } from './types.ts';
|
|
import type { Result } from '../result.ts';
|
|
import { tlv } from './types.ts';
|
|
|
|
/** What one TLV on the wire reads as. */
|
|
export type TlvScalar = Buffer | number | string;
|
|
|
|
export type TlvDefinition = {
|
|
id: number;
|
|
multiple?: boolean;
|
|
tag: string;
|
|
type: WireType<TlvScalar>;
|
|
};
|
|
|
|
/** 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<TlvScalar> } }>(
|
|
definitions: T,
|
|
): T => definitions;
|
|
|
|
// Ordered by tag id, mirroring the SMPP 5.0 TLV table.
|
|
const specs = tlvSpecs({
|
|
dest_addr_subunit: { id: 0x0005, tag: 'dest_addr_subunit', type: tlv.int8 },
|
|
dest_network_type: { id: 0x0006, tag: 'dest_network_type', type: tlv.int8 },
|
|
dest_bearer_type: { id: 0x0007, tag: 'dest_bearer_type', type: tlv.int8 },
|
|
dest_telematics_id: { id: 0x0008, tag: 'dest_telematics_id', type: tlv.int16 },
|
|
source_addr_subunit: { id: 0x000D, tag: 'source_addr_subunit', type: tlv.int8 },
|
|
source_network_type: { id: 0x000E, tag: 'source_network_type', type: tlv.int8 },
|
|
source_bearer_type: { id: 0x000F, tag: 'source_bearer_type', type: tlv.int8 },
|
|
source_telematics_id: { id: 0x0010, tag: 'source_telematics_id', type: tlv.int8 },
|
|
qos_time_to_live: { id: 0x0017, tag: 'qos_time_to_live', type: tlv.int32 },
|
|
payload_type: { id: 0x0019, tag: 'payload_type', type: tlv.int8 },
|
|
additional_status_info_text: { id: 0x001D, tag: 'additional_status_info_text', type: tlv.cstring },
|
|
receipted_message_id: { id: 0x001E, tag: 'receipted_message_id', type: tlv.cstring },
|
|
ms_msg_wait_facilities: { id: 0x0030, tag: 'ms_msg_wait_facilities', type: tlv.int8 },
|
|
privacy_indicator: { id: 0x0201, tag: 'privacy_indicator', type: tlv.int8 },
|
|
source_subaddress: { id: 0x0202, tag: 'source_subaddress', type: tlv.buffer },
|
|
dest_subaddress: { id: 0x0203, tag: 'dest_subaddress', type: tlv.buffer },
|
|
user_message_reference: { id: 0x0204, tag: 'user_message_reference', type: tlv.int16 },
|
|
user_response_code: { id: 0x0205, tag: 'user_response_code', type: tlv.int8 },
|
|
source_port: { id: 0x020A, tag: 'source_port', type: tlv.int16 },
|
|
dest_port: { id: 0x020B, tag: 'dest_port', type: tlv.int16 },
|
|
sar_msg_ref_num: { id: 0x020C, tag: 'sar_msg_ref_num', type: tlv.int16 },
|
|
language_indicator: { id: 0x020D, tag: 'language_indicator', type: tlv.int8 },
|
|
sar_total_segments: { id: 0x020E, tag: 'sar_total_segments', type: tlv.int8 },
|
|
sar_segment_seqnum: { id: 0x020F, tag: 'sar_segment_seqnum', type: tlv.int8 },
|
|
sc_interface_version: { id: 0x0210, tag: 'sc_interface_version', type: tlv.int8 },
|
|
callback_num_pres_ind: { id: 0x0302, multiple: true, tag: 'callback_num_pres_ind', type: tlv.int8 },
|
|
callback_num_atag: { id: 0x0303, multiple: true, tag: 'callback_num_atag', type: tlv.buffer },
|
|
number_of_messages: { id: 0x0304, tag: 'number_of_messages', type: tlv.int8 },
|
|
callback_num: { id: 0x0381, multiple: true, tag: 'callback_num', type: tlv.buffer },
|
|
dpf_result: { id: 0x0420, tag: 'dpf_result', type: tlv.int8 },
|
|
set_dpf: { id: 0x0421, tag: 'set_dpf', type: tlv.int8 },
|
|
ms_availability_status: { id: 0x0422, tag: 'ms_availability_status', type: tlv.int8 },
|
|
network_error_code: { id: 0x0423, tag: 'network_error_code', type: tlv.buffer },
|
|
message_payload: { id: 0x0424, tag: 'message_payload', type: tlv.buffer },
|
|
delivery_failure_reason: { id: 0x0425, tag: 'delivery_failure_reason', type: tlv.int8 },
|
|
more_messages_to_send: { id: 0x0426, tag: 'more_messages_to_send', type: tlv.int8 },
|
|
message_state: { id: 0x0427, tag: 'message_state', type: tlv.int8 },
|
|
congestion_state: { id: 0x0428, tag: 'congestion_state', type: tlv.int8 },
|
|
ussd_service_op: { id: 0x0501, tag: 'ussd_service_op', type: tlv.int8 },
|
|
broadcast_channel_indicator: { id: 0x0600, tag: 'broadcast_channel_indicator', type: tlv.int8 },
|
|
broadcast_content_type: { id: 0x0601, tag: 'broadcast_content_type', type: tlv.buffer },
|
|
broadcast_content_type_info: { id: 0x0602, tag: 'broadcast_content_type_info', type: tlv.string },
|
|
broadcast_message_class: { id: 0x0603, tag: 'broadcast_message_class', type: tlv.int8 },
|
|
broadcast_rep_num: { id: 0x0604, tag: 'broadcast_rep_num', type: tlv.int16 },
|
|
broadcast_frequency_interval: { id: 0x0605, tag: 'broadcast_frequency_interval', type: tlv.buffer },
|
|
broadcast_area_identifier: { id: 0x0606, multiple: true, tag: 'broadcast_area_identifier', type: tlv.buffer },
|
|
broadcast_error_status: { id: 0x0607, multiple: true, tag: 'broadcast_error_status', type: tlv.int32 },
|
|
broadcast_area_success: { id: 0x0608, tag: 'broadcast_area_success', type: tlv.int8 },
|
|
broadcast_end_time: { id: 0x0609, tag: 'broadcast_end_time', type: tlv.string },
|
|
broadcast_service_group: { id: 0x060A, tag: 'broadcast_service_group', type: tlv.string },
|
|
billing_identification: { id: 0x060B, tag: 'billing_identification', type: tlv.buffer },
|
|
source_network_id: { id: 0x060D, tag: 'source_network_id', type: tlv.cstring },
|
|
dest_network_id: { id: 0x060E, tag: 'dest_network_id', type: tlv.cstring },
|
|
source_node_id: { id: 0x060F, tag: 'source_node_id', type: tlv.string },
|
|
dest_node_id: { id: 0x0610, tag: 'dest_node_id', type: tlv.string },
|
|
dest_addr_np_resolution: { id: 0x0611, tag: 'dest_addr_np_resolution', type: tlv.int8 },
|
|
dest_addr_np_information: { id: 0x0612, tag: 'dest_addr_np_information', type: tlv.string },
|
|
dest_addr_np_country: { id: 0x0613, tag: 'dest_addr_np_country', type: tlv.int32 },
|
|
display_time: { id: 0x1201, tag: 'display_time', type: tlv.int8 },
|
|
sms_signal: { id: 0x1203, tag: 'sms_signal', type: tlv.int16 },
|
|
ms_validity: { id: 0x1204, tag: 'ms_validity', type: tlv.buffer },
|
|
alert_on_message_delivery: { id: 0x130C, tag: 'alert_on_message_delivery', type: tlv.int8 },
|
|
its_reply_type: { id: 0x1380, tag: 'its_reply_type', type: tlv.int8 },
|
|
its_session_info: { id: 0x1383, tag: 'its_session_info', type: tlv.buffer },
|
|
});
|
|
|
|
export type TlvName = keyof typeof specs;
|
|
|
|
export const tlvs: Record<TlvName, TlvDefinition> & Record<string, TlvDefinition> = {
|
|
...specs,
|
|
// Alternate spellings; the definition behind each keeps its canonical name.
|
|
alert_on_msg_delivery: specs.alert_on_message_delivery,
|
|
failed_broadcast_area_identifier: specs.broadcast_area_identifier,
|
|
};
|
|
|
|
export const tlvsById: Record<number, TlvDefinition> = {};
|
|
|
|
for (const definition of Object.values<TlvDefinition>(specs)) {
|
|
tlvsById[definition.id] = definition;
|
|
}
|
|
|
|
/** Fallback for tags this table does not know: keep the raw octets. */
|
|
export const tlvDefault: WireType<TlvScalar> = tlv.buffer;
|
|
|
|
/** A list for a tag its definition marks `multiple`, one copy per list entry, and a single value otherwise. */
|
|
export type TlvValue = TlvScalar | TlvScalar[];
|
|
|
|
export type Tlv = {
|
|
tagId: number;
|
|
tagName: string | undefined;
|
|
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: TlvValue;
|
|
};
|
|
|
|
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`) };
|
|
}
|
|
|
|
if (!Number.isInteger(tagId) || tagId < 0 || tagId > 0xFFFF) {
|
|
return { err: new Error(`TLV "${name}": tagId ${String(tagId)} out of range 0-65535`) };
|
|
}
|
|
|
|
return { tagId };
|
|
}
|
|
|
|
function copiesOf(name: string, definition: TlvDefinition | undefined, value: TlvValue): Result<{ copies: TlvScalar[] }> {
|
|
const multiple = definition?.multiple === true;
|
|
|
|
if (Array.isArray(value) && multiple) {
|
|
return value.length > 0
|
|
? { copies: value }
|
|
: { err: new Error(`TLV "${name}": give at least one value, or leave the tag out`) };
|
|
}
|
|
|
|
if (!Array.isArray(value) && !multiple) return { copies: [value] };
|
|
|
|
return {
|
|
err: new Error(multiple
|
|
? `TLV "${name}": the tag may repeat, so give its value as a list`
|
|
: `TLV "${name}": the tag may not repeat, so give its value alone rather than as a list`),
|
|
};
|
|
}
|
|
|
|
function writeTlv(name: string, tagId: number, type: WireType<TlvScalar>, value: TlvScalar): Result<{ chunk: Buffer }> {
|
|
const sized = type.size(value);
|
|
|
|
if (sized.err) {
|
|
return { err: new Error(`TLV "${name}": ${sized.err.message}`) };
|
|
}
|
|
|
|
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(tagId, 0);
|
|
chunk.writeUInt16BE(sized.size, 2);
|
|
|
|
const written = type.write(value, chunk, 4);
|
|
|
|
if (written.err) {
|
|
return { err: new Error(`TLV "${name}": ${written.err.message}`) };
|
|
}
|
|
|
|
return { chunk };
|
|
}
|
|
|
|
/** 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[] }> {
|
|
const chunks: Buffer[] = [];
|
|
|
|
for (const [name, input] of Object.entries(inputs ?? {})) {
|
|
const tag = tagIdOf(name, input);
|
|
|
|
if (tag.err) return { err: tag.err };
|
|
|
|
const definition = tlvsById[tag.tagId];
|
|
const listed = copiesOf(name, definition, input.tagValue);
|
|
|
|
if (listed.err) return { err: listed.err };
|
|
|
|
for (const value of listed.copies) {
|
|
const written = writeTlv(name, tag.tagId, definition?.type ?? tlvDefault, value);
|
|
|
|
if (written.err) return { err: written.err };
|
|
|
|
chunks.push(written.chunk);
|
|
}
|
|
}
|
|
|
|
return { chunks };
|
|
}
|