Add TLV and command tables with per-command parameter types
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
import type { ParamValue, WireType } from './types.ts';
|
||||
import { buffer, cstring, dest_address_array, int8, unsuccess_sme_array } from './types.ts';
|
||||
|
||||
type CommandSpec = {
|
||||
defaults?: Record<string, ParamValue>;
|
||||
id: number;
|
||||
params?: Record<string, WireType>;
|
||||
tlvMap?: Record<string, string>;
|
||||
};
|
||||
|
||||
const bindParams = {
|
||||
system_id: cstring,
|
||||
password: cstring,
|
||||
system_type: cstring,
|
||||
interface_version: int8,
|
||||
addr_ton: int8,
|
||||
addr_npi: int8,
|
||||
address_range: cstring,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Key order inside each `params` object is the order the fields appear on the wire. Reordering
|
||||
* them corrupts every PDU of that command.
|
||||
*/
|
||||
const specs = {
|
||||
alert_notification: {
|
||||
id: 0x00000102,
|
||||
params: {
|
||||
source_addr_ton: int8,
|
||||
source_addr_npi: int8,
|
||||
source_addr: cstring,
|
||||
esme_addr_ton: int8,
|
||||
esme_addr_npi: int8,
|
||||
esme_addr: cstring,
|
||||
},
|
||||
},
|
||||
bind_receiver: { defaults: { interface_version: 0x50 }, id: 0x00000001, params: bindParams },
|
||||
bind_receiver_resp: { id: 0x80000001, params: { system_id: cstring } },
|
||||
bind_transmitter: { defaults: { interface_version: 0x50 }, id: 0x00000002, params: bindParams },
|
||||
bind_transmitter_resp: { id: 0x80000002, params: { system_id: cstring } },
|
||||
bind_transceiver: { defaults: { interface_version: 0x50 }, id: 0x00000009, params: bindParams },
|
||||
bind_transceiver_resp: { id: 0x80000009, params: { system_id: cstring } },
|
||||
broadcast_sm: {
|
||||
id: 0x00000111,
|
||||
params: {
|
||||
service_type: cstring,
|
||||
source_addr_ton: int8,
|
||||
source_addr_npi: int8,
|
||||
source_addr: cstring,
|
||||
message_id: cstring,
|
||||
priority_flag: int8,
|
||||
schedule_delivery_time: cstring,
|
||||
validity_period: cstring,
|
||||
replace_if_present_flag: int8,
|
||||
data_coding: int8,
|
||||
sm_default_msg_id: int8,
|
||||
},
|
||||
},
|
||||
broadcast_sm_resp: {
|
||||
id: 0x80000111,
|
||||
params: { message_id: cstring },
|
||||
tlvMap: { broadcast_area_identifier: 'failed_broadcast_area_identifier' },
|
||||
},
|
||||
cancel_broadcast_sm: {
|
||||
id: 0x00000113,
|
||||
params: {
|
||||
service_type: cstring,
|
||||
message_id: cstring,
|
||||
source_addr_ton: int8,
|
||||
source_addr_npi: int8,
|
||||
source_addr: cstring,
|
||||
},
|
||||
},
|
||||
cancel_broadcast_sm_resp: { id: 0x80000113 },
|
||||
cancel_sm: {
|
||||
id: 0x00000008,
|
||||
params: {
|
||||
service_type: cstring,
|
||||
message_id: cstring,
|
||||
source_addr_ton: int8,
|
||||
source_addr_npi: int8,
|
||||
source_addr: cstring,
|
||||
dest_addr_ton: int8,
|
||||
dest_addr_npi: int8,
|
||||
destination_addr: cstring,
|
||||
},
|
||||
},
|
||||
cancel_sm_resp: { id: 0x80000008 },
|
||||
data_sm: {
|
||||
id: 0x00000103,
|
||||
params: {
|
||||
service_type: cstring,
|
||||
source_addr_ton: int8,
|
||||
source_addr_npi: int8,
|
||||
source_addr: cstring,
|
||||
dest_addr_ton: int8,
|
||||
dest_addr_npi: int8,
|
||||
destination_addr: cstring,
|
||||
esm_class: int8,
|
||||
registered_delivery: int8,
|
||||
data_coding: int8,
|
||||
},
|
||||
},
|
||||
data_sm_resp: { id: 0x80000103, params: { message_id: cstring } },
|
||||
deliver_sm: {
|
||||
id: 0x00000005,
|
||||
params: {
|
||||
service_type: cstring,
|
||||
source_addr_ton: int8,
|
||||
source_addr_npi: int8,
|
||||
source_addr: cstring,
|
||||
dest_addr_ton: int8,
|
||||
dest_addr_npi: int8,
|
||||
destination_addr: cstring,
|
||||
esm_class: int8,
|
||||
protocol_id: int8,
|
||||
priority_flag: int8,
|
||||
schedule_delivery_time: cstring,
|
||||
validity_period: cstring,
|
||||
registered_delivery: int8,
|
||||
replace_if_present_flag: int8,
|
||||
data_coding: int8,
|
||||
sm_default_msg_id: int8,
|
||||
sm_length: int8,
|
||||
short_message: buffer,
|
||||
},
|
||||
},
|
||||
deliver_sm_resp: { id: 0x80000005, params: { message_id: cstring } },
|
||||
enquire_link: { id: 0x00000015 },
|
||||
enquire_link_resp: { id: 0x80000015 },
|
||||
generic_nack: { id: 0x80000000 },
|
||||
outbind: { id: 0x0000000B, params: { system_id: cstring, password: cstring } },
|
||||
query_broadcast_sm: {
|
||||
id: 0x00000112,
|
||||
params: {
|
||||
message_id: cstring,
|
||||
source_addr_ton: int8,
|
||||
source_addr_npi: int8,
|
||||
source_addr: cstring,
|
||||
},
|
||||
},
|
||||
query_broadcast_sm_resp: { id: 0x80000112, params: { message_id: cstring } },
|
||||
query_sm: {
|
||||
id: 0x00000003,
|
||||
params: {
|
||||
message_id: cstring,
|
||||
source_addr_ton: int8,
|
||||
source_addr_npi: int8,
|
||||
source_addr: cstring,
|
||||
},
|
||||
},
|
||||
query_sm_resp: {
|
||||
id: 0x80000003,
|
||||
params: {
|
||||
message_id: cstring,
|
||||
final_date: cstring,
|
||||
message_state: int8,
|
||||
error_code: int8,
|
||||
},
|
||||
},
|
||||
replace_sm: {
|
||||
id: 0x00000007,
|
||||
params: {
|
||||
message_id: cstring,
|
||||
source_addr_ton: int8,
|
||||
source_addr_npi: int8,
|
||||
source_addr: cstring,
|
||||
schedule_delivery_time: cstring,
|
||||
validity_period: cstring,
|
||||
registered_delivery: int8,
|
||||
sm_default_msg_id: int8,
|
||||
sm_length: int8,
|
||||
short_message: buffer,
|
||||
},
|
||||
},
|
||||
replace_sm_resp: { id: 0x80000007 },
|
||||
submit_multi: {
|
||||
id: 0x00000021,
|
||||
params: {
|
||||
service_type: cstring,
|
||||
source_addr_ton: int8,
|
||||
source_addr_npi: int8,
|
||||
source_addr: cstring,
|
||||
dest_address: dest_address_array,
|
||||
esm_class: int8,
|
||||
protocol_id: int8,
|
||||
priority_flag: int8,
|
||||
schedule_delivery_time: cstring,
|
||||
validity_period: cstring,
|
||||
registered_delivery: int8,
|
||||
replace_if_present_flag: int8,
|
||||
data_coding: int8,
|
||||
sm_default_msg_id: int8,
|
||||
short_message: buffer,
|
||||
},
|
||||
},
|
||||
submit_multi_resp: {
|
||||
id: 0x80000021,
|
||||
params: { message_id: cstring, unsuccess_sme: unsuccess_sme_array },
|
||||
},
|
||||
submit_sm: {
|
||||
id: 0x00000004,
|
||||
params: {
|
||||
service_type: cstring,
|
||||
source_addr_ton: int8,
|
||||
source_addr_npi: int8,
|
||||
source_addr: cstring,
|
||||
dest_addr_ton: int8,
|
||||
dest_addr_npi: int8,
|
||||
destination_addr: cstring,
|
||||
esm_class: int8,
|
||||
protocol_id: int8,
|
||||
priority_flag: int8,
|
||||
schedule_delivery_time: cstring,
|
||||
validity_period: cstring,
|
||||
registered_delivery: int8,
|
||||
replace_if_present_flag: int8,
|
||||
data_coding: int8,
|
||||
sm_default_msg_id: int8,
|
||||
sm_length: int8,
|
||||
short_message: buffer,
|
||||
},
|
||||
},
|
||||
submit_sm_resp: { id: 0x80000004, params: { message_id: cstring } },
|
||||
unbind: { id: 0x00000006 },
|
||||
unbind_resp: { id: 0x80000006 },
|
||||
} satisfies Record<string, CommandSpec>;
|
||||
|
||||
export type CommandName = keyof typeof specs;
|
||||
|
||||
type ParamsSpecOf<C extends CommandName> = (typeof specs)[C] extends { params: infer P } ? P : Record<never, never>;
|
||||
|
||||
/** Parameters as they come off the wire: every field the command defines, always present. */
|
||||
export type PduParams<C extends CommandName = CommandName> = {
|
||||
[K in keyof ParamsSpecOf<C>]: ParamsSpecOf<C>[K] extends WireType<infer V> ? V : never;
|
||||
};
|
||||
|
||||
/** Parameters callers supply: all optional, and numbers are accepted for the string fields. */
|
||||
export type PduParamsInput<C extends CommandName = CommandName> = {
|
||||
[K in keyof ParamsSpecOf<C>]?: ParamsSpecOf<C>[K] extends WireType<infer V>
|
||||
? V extends string ? number | string : V
|
||||
: never;
|
||||
};
|
||||
|
||||
export type CommandDefinition = CommandSpec & { command: string };
|
||||
|
||||
export const cmds: Record<string, CommandDefinition> = {};
|
||||
export const cmdsById: Record<number, CommandDefinition> = {};
|
||||
|
||||
for (const [command, spec] of Object.entries<CommandSpec>(specs)) {
|
||||
const definition: CommandDefinition = { ...spec, command };
|
||||
|
||||
cmds[command] = definition;
|
||||
cmdsById[spec.id] = definition;
|
||||
}
|
||||
|
||||
export function isCommandName(value: unknown): value is CommandName {
|
||||
return typeof value === 'string' && Object.hasOwn(specs, value);
|
||||
}
|
||||
|
||||
export function commandNameById(id: number): CommandName | undefined {
|
||||
const command = cmdsById[id]?.command;
|
||||
|
||||
return isCommandName(command) ? command : undefined;
|
||||
}
|
||||
+20
-10
@@ -15,21 +15,31 @@ const gsmRegex =
|
||||
const gsmExtended = /[\f^{}\\[~\]|€]/g;
|
||||
const gsmEscaped = /\x1B([\nΛ()/<=>¡e])/g;
|
||||
|
||||
// Characters reachable only via an ESC prefix, paired with the base character that follows it.
|
||||
const gsmExtendedPairs: [string, string][] = [
|
||||
['\f', '\n'],
|
||||
['^', 'Λ'],
|
||||
['{', '('],
|
||||
['}', ')'],
|
||||
['\\', '/'],
|
||||
['[', '<'],
|
||||
['~', '='],
|
||||
[']', '>'],
|
||||
['|', '¡'],
|
||||
['€', 'e'],
|
||||
];
|
||||
|
||||
const gsmCharCodes = new Map<string, number>();
|
||||
const gsmExtChars = new Map<string, string>();
|
||||
|
||||
for (let i = 0; i < gsmChars.length; i++) {
|
||||
gsmCharCodes.set(gsmChars[i], i);
|
||||
// Indexed by code unit rather than code point: every entry in the table is one octet on the wire.
|
||||
for (let code = 0; code < gsmChars.length; code++) {
|
||||
gsmCharCodes.set(gsmChars.charAt(code), code);
|
||||
}
|
||||
|
||||
{
|
||||
const from = '\f^{}\\[~]|€';
|
||||
const to = '\nΛ()/<=>¡e';
|
||||
|
||||
for (let i = 0; i < from.length; i++) {
|
||||
gsmExtChars.set(from[i], to[i]);
|
||||
gsmExtChars.set(to[i], from[i]);
|
||||
}
|
||||
for (const [extended, base] of gsmExtendedPairs) {
|
||||
gsmExtChars.set(extended, base);
|
||||
gsmExtChars.set(base, extended);
|
||||
}
|
||||
|
||||
const ascii: Encoding = {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import type { ParamValue, WireType } from './types.ts';
|
||||
import { tlv } from './types.ts';
|
||||
|
||||
export type TlvDefinition = {
|
||||
id: number;
|
||||
multiple?: boolean;
|
||||
tag: string;
|
||||
type: WireType;
|
||||
};
|
||||
|
||||
type TlvSpec = { id: number; multiple?: boolean; type: WireType };
|
||||
|
||||
// Ordered by tag id, mirroring the SMPP 5.0 TLV table.
|
||||
const specs = {
|
||||
dest_addr_subunit: { id: 0x0005, type: tlv.int8 },
|
||||
dest_network_type: { id: 0x0006, type: tlv.int8 },
|
||||
dest_bearer_type: { id: 0x0007, type: tlv.int8 },
|
||||
dest_telematics_id: { id: 0x0008, type: tlv.int16 },
|
||||
source_addr_subunit: { id: 0x000D, type: tlv.int8 },
|
||||
source_network_type: { id: 0x000E, type: tlv.int8 },
|
||||
source_bearer_type: { id: 0x000F, type: tlv.int8 },
|
||||
source_telematics_id: { id: 0x0010, type: tlv.int16 },
|
||||
qos_time_to_live: { id: 0x0017, type: tlv.int32 },
|
||||
payload_type: { id: 0x0019, type: tlv.int8 },
|
||||
additional_status_info_text: { id: 0x001D, type: tlv.cstring },
|
||||
receipted_message_id: { id: 0x001E, type: tlv.cstring },
|
||||
ms_msg_wait_facilities: { id: 0x0030, type: tlv.int8 },
|
||||
privacy_indicator: { id: 0x0201, type: tlv.int8 },
|
||||
source_subaddress: { id: 0x0202, type: tlv.buffer },
|
||||
dest_subaddress: { id: 0x0203, type: tlv.buffer },
|
||||
user_message_reference: { id: 0x0204, type: tlv.int16 },
|
||||
user_response_code: { id: 0x0205, type: tlv.int8 },
|
||||
source_port: { id: 0x020A, type: tlv.int16 },
|
||||
dest_port: { id: 0x020B, type: tlv.int16 },
|
||||
sar_msg_ref_num: { id: 0x020C, type: tlv.int16 },
|
||||
language_indicator: { id: 0x020D, type: tlv.int8 },
|
||||
sar_total_segments: { id: 0x020E, type: tlv.int8 },
|
||||
sar_segment_seqnum: { id: 0x020F, type: tlv.int8 },
|
||||
sc_interface_version: { id: 0x0210, type: tlv.int8 },
|
||||
callback_num_pres_ind: { id: 0x0302, multiple: true, type: tlv.int8 },
|
||||
callback_num_atag: { id: 0x0303, multiple: true, type: tlv.buffer },
|
||||
number_of_messages: { id: 0x0304, type: tlv.int8 },
|
||||
callback_num: { id: 0x0381, multiple: true, type: tlv.buffer },
|
||||
dpf_result: { id: 0x0420, type: tlv.int8 },
|
||||
set_dpf: { id: 0x0421, type: tlv.int8 },
|
||||
ms_availability_status: { id: 0x0422, type: tlv.int8 },
|
||||
network_error_code: { id: 0x0423, type: tlv.buffer },
|
||||
message_payload: { id: 0x0424, type: tlv.buffer },
|
||||
delivery_failure_reason: { id: 0x0425, type: tlv.int8 },
|
||||
more_messages_to_send: { id: 0x0426, type: tlv.int8 },
|
||||
message_state: { id: 0x0427, type: tlv.int8 },
|
||||
congestion_state: { id: 0x0428, type: tlv.int8 },
|
||||
ussd_service_op: { id: 0x0501, type: tlv.int8 },
|
||||
broadcast_channel_indicator: { id: 0x0600, type: tlv.int8 },
|
||||
broadcast_content_type: { id: 0x0601, type: tlv.buffer },
|
||||
broadcast_content_type_info: { id: 0x0602, type: tlv.string },
|
||||
broadcast_message_class: { id: 0x0603, type: tlv.int8 },
|
||||
broadcast_rep_num: { id: 0x0604, type: tlv.int16 },
|
||||
broadcast_frequency_interval: { id: 0x0605, type: tlv.buffer },
|
||||
broadcast_area_identifier: { id: 0x0606, multiple: true, type: tlv.buffer },
|
||||
broadcast_error_status: { id: 0x0607, multiple: true, type: tlv.int32 },
|
||||
broadcast_area_success: { id: 0x0608, type: tlv.int8 },
|
||||
broadcast_end_time: { id: 0x0609, type: tlv.string },
|
||||
broadcast_service_group: { id: 0x060A, type: tlv.string },
|
||||
billing_identification: { id: 0x060B, type: tlv.buffer },
|
||||
source_network_id: { id: 0x060D, type: tlv.cstring },
|
||||
dest_network_id: { id: 0x060E, type: tlv.cstring },
|
||||
source_node_id: { id: 0x060F, type: tlv.string },
|
||||
dest_node_id: { id: 0x0610, type: tlv.string },
|
||||
dest_addr_np_resolution: { id: 0x0611, type: tlv.int8 },
|
||||
dest_addr_np_information: { id: 0x0612, type: tlv.string },
|
||||
dest_addr_np_country: { id: 0x0613, type: tlv.int32 },
|
||||
display_time: { id: 0x1201, type: tlv.int8 },
|
||||
sms_signal: { id: 0x1203, type: tlv.int16 },
|
||||
ms_validity: { id: 0x1204, type: tlv.buffer },
|
||||
alert_on_message_delivery: { id: 0x130C, type: tlv.int8 },
|
||||
its_reply_type: { id: 0x1380, type: tlv.int8 },
|
||||
its_session_info: { id: 0x1383, type: tlv.buffer },
|
||||
} satisfies Record<string, TlvSpec>;
|
||||
|
||||
export type TlvName = keyof typeof specs;
|
||||
|
||||
export const tlvs: Record<string, TlvDefinition> = {};
|
||||
export const tlvsById: Record<number, TlvDefinition> = {};
|
||||
|
||||
for (const [tag, spec] of Object.entries<TlvSpec>(specs)) {
|
||||
const definition: TlvDefinition = { ...spec, tag };
|
||||
|
||||
tlvs[tag] = definition;
|
||||
tlvsById[spec.id] = definition;
|
||||
}
|
||||
|
||||
// Alternate spellings that resolve to the same tag; the definition keeps its canonical name.
|
||||
const aliases: Record<string, TlvName> = {
|
||||
alert_on_msg_delivery: 'alert_on_message_delivery',
|
||||
failed_broadcast_area_identifier: 'broadcast_area_identifier',
|
||||
};
|
||||
|
||||
for (const [alias, target] of Object.entries(aliases)) {
|
||||
tlvs[alias] = { ...specs[target], tag: target };
|
||||
}
|
||||
|
||||
/** Fallback for tags this table does not know: keep the raw octets. */
|
||||
export const tlvDefault: WireType = tlv.buffer;
|
||||
|
||||
export type Tlv = {
|
||||
tagId: number;
|
||||
tagName: string | undefined;
|
||||
tagValue: ParamValue;
|
||||
};
|
||||
+234
-165
@@ -13,28 +13,106 @@ export type UnsuccessSme = {
|
||||
|
||||
export type ParamValue = Buffer | DestAddress[] | UnsuccessSme[] | number | string;
|
||||
|
||||
export type WireType<TRead extends ParamValue, TWrite extends ParamValue = TRead> = {
|
||||
default: TRead;
|
||||
read: (buffer: Buffer, offset: number, length?: number) => Result<{ value: TRead }>;
|
||||
size: (value: TWrite) => number;
|
||||
write: (value: TWrite, buffer: Buffer, offset: number) => VoidResult;
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export type WireType<T extends ParamValue = ParamValue> = {
|
||||
default: T;
|
||||
read: (buffer: Buffer, offset: number, length?: number) => Result<{ bytesRead: number; value: T }>;
|
||||
size: (value: ParamValue) => Result<{ size: number }>;
|
||||
write: (value: ParamValue, buffer: Buffer, offset: number) => VoidResult;
|
||||
};
|
||||
|
||||
function outOfRange(buffer: Buffer, offset: number, needed: number): Error | undefined {
|
||||
if (offset < 0 || needed < 0 || offset + needed > buffer.length) {
|
||||
return new Error(
|
||||
`Out of range: need ${String(needed)} bytes at offset ${String(offset)} of a ${String(buffer.length)} byte buffer`,
|
||||
`Out of range: need ${String(needed)} octets at offset ${String(offset)} of a ${String(buffer.length)} octet buffer`,
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function asString(value: number | string): string {
|
||||
return typeof value === 'number' ? value.toString() : value;
|
||||
function wantInt(value: ParamValue, max: number): Result<{ int: number }> {
|
||||
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
||||
return { err: new Error(`Expected an integer, got ${JSON.stringify(value)}`) };
|
||||
}
|
||||
|
||||
if (value < 0 || value > max) {
|
||||
return { err: new Error(`Integer ${String(value)} out of range 0-${String(max)}`) };
|
||||
}
|
||||
|
||||
return { int: value };
|
||||
}
|
||||
|
||||
function readCstring(buffer: Buffer, offset: number): Result<{ value: string }> {
|
||||
function wantText(value: ParamValue): Result<{ text: string }> {
|
||||
if (typeof value === 'string') return { text: value };
|
||||
if (typeof value === 'number') return { text: value.toString() };
|
||||
|
||||
return { err: new Error(`Expected a string, got ${typeof value}`) };
|
||||
}
|
||||
|
||||
function wantBytes(value: ParamValue): Result<{ bytes: Buffer }> {
|
||||
if (Buffer.isBuffer(value)) return { bytes: value };
|
||||
|
||||
const { err, text } = wantText(value);
|
||||
|
||||
return err ? { err } : { bytes: Buffer.from(text, 'ascii') };
|
||||
}
|
||||
|
||||
function isDestAddress(value: unknown): value is DestAddress {
|
||||
if (typeof value !== 'object' || value === null) return false;
|
||||
|
||||
if ('dl_name' in value) return typeof value.dl_name === 'string';
|
||||
|
||||
return 'destination_addr' in value && typeof value.destination_addr === 'string';
|
||||
}
|
||||
|
||||
function isUnsuccessSme(value: unknown): value is UnsuccessSme {
|
||||
return typeof value === 'object'
|
||||
&& value !== null
|
||||
&& 'destination_addr' in value && typeof value.destination_addr === 'string'
|
||||
&& 'error_status_code' in value && typeof value.error_status_code === 'number';
|
||||
}
|
||||
|
||||
function wantDestAddresses(value: ParamValue): Result<{ addresses: DestAddress[] }> {
|
||||
if (!Array.isArray(value)) {
|
||||
return { err: new Error('Expected an array of dest_address structures') };
|
||||
}
|
||||
|
||||
const addresses: DestAddress[] = [];
|
||||
|
||||
for (const entry of value) {
|
||||
if (!isDestAddress(entry)) {
|
||||
return { err: new Error('Expected an array of dest_address structures') };
|
||||
}
|
||||
|
||||
addresses.push(entry);
|
||||
}
|
||||
|
||||
return { addresses };
|
||||
}
|
||||
|
||||
function wantUnsuccessSmes(value: ParamValue): Result<{ smes: UnsuccessSme[] }> {
|
||||
if (!Array.isArray(value)) {
|
||||
return { err: new Error('Expected an array of unsuccess_sme structures') };
|
||||
}
|
||||
|
||||
const smes: UnsuccessSme[] = [];
|
||||
|
||||
for (const entry of value) {
|
||||
if (!isUnsuccessSme(entry)) {
|
||||
return { err: new Error('Expected an array of unsuccess_sme structures') };
|
||||
}
|
||||
|
||||
smes.push(entry);
|
||||
}
|
||||
|
||||
return { smes };
|
||||
}
|
||||
|
||||
function readCstring(buffer: Buffer, offset: number): Result<{ bytesRead: number; value: string }> {
|
||||
let length = 0;
|
||||
|
||||
while (buffer[offset + length]) {
|
||||
@@ -45,90 +123,53 @@ function readCstring(buffer: Buffer, offset: number): Result<{ value: string }>
|
||||
}
|
||||
}
|
||||
|
||||
return { value: buffer.toString('ascii', offset, offset + length) };
|
||||
return { bytesRead: length + 1, value: buffer.toString('ascii', offset, offset + length) };
|
||||
}
|
||||
|
||||
function writeCstring(value: number | string, buffer: Buffer, offset: number): VoidResult {
|
||||
const str = asString(value);
|
||||
const err = outOfRange(buffer, offset, str.length + 1);
|
||||
function writeCstring(text: string, buffer: Buffer, offset: number): VoidResult {
|
||||
const err = outOfRange(buffer, offset, text.length + 1);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
buffer.write(str, offset, 'ascii');
|
||||
buffer[offset + str.length] = 0;
|
||||
buffer.write(text, offset, 'ascii');
|
||||
buffer[offset + text.length] = 0;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function sizeCstring(value: number | string): number {
|
||||
return asString(value).length + 1;
|
||||
function intType(octets: number, max: number, readAt: (b: Buffer, o: number) => number, writeAt: (b: Buffer, v: number, o: number) => void): WireType<number> {
|
||||
return {
|
||||
default: 0,
|
||||
read(buffer, offset) {
|
||||
const err = outOfRange(buffer, offset, octets);
|
||||
|
||||
return err ? { err } : { bytesRead: octets, value: readAt(buffer, offset) };
|
||||
},
|
||||
size() {
|
||||
return { size: octets };
|
||||
},
|
||||
write(value, buffer, offset) {
|
||||
const rangeErr = outOfRange(buffer, offset, octets);
|
||||
|
||||
if (rangeErr) return { err: rangeErr };
|
||||
|
||||
const { err, int } = wantInt(value, max);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
writeAt(buffer, int, offset);
|
||||
|
||||
return {};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const int8: WireType<number> = {
|
||||
default: 0,
|
||||
read(buffer, offset) {
|
||||
const err = outOfRange(buffer, offset, 1);
|
||||
export const int8 = intType(1, 0xFF, (b, o) => b.readUInt8(o), (b, v, o) => b.writeUInt8(v, o));
|
||||
export const int16 = intType(2, 0xFFFF, (b, o) => b.readUInt16BE(o), (b, v, o) => b.writeUInt16BE(v, o));
|
||||
export const int32 = intType(4, 0xFFFFFFFF, (b, o) => b.readUInt32BE(o), (b, v, o) => b.writeUInt32BE(v, o));
|
||||
|
||||
return err ? { err } : { value: buffer.readUInt8(offset) };
|
||||
},
|
||||
size() {
|
||||
return 1;
|
||||
},
|
||||
write(value, buffer, offset) {
|
||||
const err = outOfRange(buffer, offset, 1);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
buffer.writeUInt8(value || 0, offset);
|
||||
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
export const int16: WireType<number> = {
|
||||
default: 0,
|
||||
read(buffer, offset) {
|
||||
const err = outOfRange(buffer, offset, 2);
|
||||
|
||||
return err ? { err } : { value: buffer.readUInt16BE(offset) };
|
||||
},
|
||||
size() {
|
||||
return 2;
|
||||
},
|
||||
write(value, buffer, offset) {
|
||||
const err = outOfRange(buffer, offset, 2);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
buffer.writeUInt16BE(value || 0, offset);
|
||||
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
export const int32: WireType<number> = {
|
||||
default: 0,
|
||||
read(buffer, offset) {
|
||||
const err = outOfRange(buffer, offset, 4);
|
||||
|
||||
return err ? { err } : { value: buffer.readUInt32BE(offset) };
|
||||
},
|
||||
size() {
|
||||
return 4;
|
||||
},
|
||||
write(value, buffer, offset) {
|
||||
const err = outOfRange(buffer, offset, 4);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
buffer.writeUInt32BE(value || 0, offset);
|
||||
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
/** Octet String: a length byte followed by that many octets. */
|
||||
export const string: WireType<string, number | string> = {
|
||||
/** Octet String: a length octet followed by that many octets. */
|
||||
export const string: WireType<string> = {
|
||||
default: '',
|
||||
read(buffer, offset) {
|
||||
const lengthErr = outOfRange(buffer, offset, 1);
|
||||
@@ -140,58 +181,82 @@ export const string: WireType<string, number | string> = {
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
return { value: buffer.toString('ascii', offset + 1, offset + 1 + length) };
|
||||
return { bytesRead: length + 1, value: buffer.toString('ascii', offset + 1, offset + 1 + length) };
|
||||
},
|
||||
size(value) {
|
||||
return asString(value).length + 1;
|
||||
const { err, text } = wantText(value);
|
||||
|
||||
return err ? { err } : { size: text.length + 1 };
|
||||
},
|
||||
write(value, buffer, offset) {
|
||||
const str = asString(value);
|
||||
const err = outOfRange(buffer, offset, str.length + 1);
|
||||
const { err, text } = wantText(value);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
buffer.writeUInt8(str.length, offset);
|
||||
buffer.write(str, offset + 1, 'ascii');
|
||||
const rangeErr = outOfRange(buffer, offset, text.length + 1);
|
||||
|
||||
if (rangeErr) return { err: rangeErr };
|
||||
|
||||
buffer.writeUInt8(text.length, offset);
|
||||
buffer.write(text, offset + 1, 'ascii');
|
||||
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
/** C-Octet String: NULL-terminated. */
|
||||
export const cstring: WireType<string, number | string> = {
|
||||
export const cstring: WireType<string> = {
|
||||
default: '',
|
||||
read: readCstring,
|
||||
size: sizeCstring,
|
||||
write: writeCstring,
|
||||
size(value) {
|
||||
const { err, text } = wantText(value);
|
||||
|
||||
return err ? { err } : { size: text.length + 1 };
|
||||
},
|
||||
write(value, buffer, offset) {
|
||||
const { err, text } = wantText(value);
|
||||
|
||||
return err ? { err } : writeCstring(text, buffer, offset);
|
||||
},
|
||||
};
|
||||
|
||||
export const buffer: WireType<Buffer, Buffer | number | string> = {
|
||||
export const buffer: WireType<Buffer> = {
|
||||
default: Buffer.alloc(0),
|
||||
read(buf, offset, length = 0) {
|
||||
const err = outOfRange(buf, offset, length);
|
||||
|
||||
return err ? { err } : { value: buf.subarray(offset, offset + length) };
|
||||
return err ? { err } : { bytesRead: length, value: buf.subarray(offset, offset + length) };
|
||||
},
|
||||
// A trailing NULL octet is not counted, mirroring how peers that append one to short_message
|
||||
// report sm_length. pduToObj relies on this when deciding whether to retry a parse.
|
||||
size(value) {
|
||||
const buf = Buffer.isBuffer(value) ? value : Buffer.from(asString(value), 'ascii');
|
||||
const { bytes, err } = wantBytes(value);
|
||||
|
||||
return buf[buf.length - 1] === 0x00 ? buf.length - 1 : buf.length;
|
||||
return err ? { err } : { size: bytes.length };
|
||||
},
|
||||
write(value, buf, offset) {
|
||||
const source = Buffer.isBuffer(value) ? value : Buffer.from(asString(value), 'ascii');
|
||||
const err = outOfRange(buf, offset, source.length);
|
||||
const { bytes, err } = wantBytes(value);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
source.copy(buf, offset);
|
||||
const rangeErr = outOfRange(buf, offset, bytes.length);
|
||||
|
||||
if (rangeErr) return { err: rangeErr };
|
||||
|
||||
bytes.copy(buf, offset);
|
||||
|
||||
return {};
|
||||
},
|
||||
};
|
||||
|
||||
function sizeDestAddresses(addresses: DestAddress[]): number {
|
||||
let size = 1;
|
||||
|
||||
for (const dest of addresses) {
|
||||
size += 'dl_name' in dest ? dest.dl_name.length + 2 : dest.destination_addr.length + 4;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
export const dest_address_array: WireType<DestAddress[]> = {
|
||||
default: [],
|
||||
read(buf, offset) {
|
||||
@@ -199,7 +264,8 @@ export const dest_address_array: WireType<DestAddress[]> = {
|
||||
|
||||
if (countErr) return { err: countErr };
|
||||
|
||||
const result: DestAddress[] = [];
|
||||
const start = offset;
|
||||
const value: DestAddress[] = [];
|
||||
let remaining = buf.readUInt8(offset++);
|
||||
|
||||
while (remaining-- > 0) {
|
||||
@@ -216,53 +282,51 @@ export const dest_address_array: WireType<DestAddress[]> = {
|
||||
|
||||
const dest_addr_ton = buf.readUInt8(offset++);
|
||||
const dest_addr_npi = buf.readUInt8(offset++);
|
||||
const { err, value } = readCstring(buf, offset);
|
||||
const address = readCstring(buf, offset);
|
||||
|
||||
if (err) return { err };
|
||||
if (address.err) return { err: address.err };
|
||||
|
||||
offset += sizeCstring(value);
|
||||
result.push({ dest_addr_npi, dest_addr_ton, destination_addr: value });
|
||||
offset += address.bytesRead;
|
||||
value.push({ dest_addr_npi, dest_addr_ton, destination_addr: address.value });
|
||||
} else {
|
||||
const { err, value } = readCstring(buf, offset);
|
||||
const name = readCstring(buf, offset);
|
||||
|
||||
if (err) return { err };
|
||||
if (name.err) return { err: name.err };
|
||||
|
||||
offset += sizeCstring(value);
|
||||
result.push({ dl_name: value });
|
||||
offset += name.bytesRead;
|
||||
value.push({ dl_name: name.value });
|
||||
}
|
||||
}
|
||||
|
||||
return { value: result };
|
||||
return { bytesRead: offset - start, value };
|
||||
},
|
||||
size(value) {
|
||||
let size = 1;
|
||||
const { addresses, err } = wantDestAddresses(value);
|
||||
|
||||
for (const dest of value) {
|
||||
size += 'dl_name' in dest
|
||||
? sizeCstring(dest.dl_name) + 1
|
||||
: sizeCstring(dest.destination_addr) + 3;
|
||||
}
|
||||
|
||||
return size;
|
||||
return err ? { err } : { size: sizeDestAddresses(addresses) };
|
||||
},
|
||||
write(value, buf, offset) {
|
||||
const err = outOfRange(buf, offset, dest_address_array.size(value));
|
||||
const { addresses, err } = wantDestAddresses(value);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
buf.writeUInt8(value.length, offset++);
|
||||
const rangeErr = outOfRange(buf, offset, sizeDestAddresses(addresses));
|
||||
|
||||
for (const dest of value) {
|
||||
if (rangeErr) return { err: rangeErr };
|
||||
|
||||
buf.writeUInt8(addresses.length, offset++);
|
||||
|
||||
for (const dest of addresses) {
|
||||
if ('dl_name' in dest) {
|
||||
buf.writeUInt8(2, offset++);
|
||||
writeCstring(dest.dl_name, buf, offset);
|
||||
offset += sizeCstring(dest.dl_name);
|
||||
offset += dest.dl_name.length + 1;
|
||||
} else {
|
||||
buf.writeUInt8(1, offset++);
|
||||
buf.writeUInt8(dest.dest_addr_ton || 0, offset++);
|
||||
buf.writeUInt8(dest.dest_addr_npi || 0, offset++);
|
||||
buf.writeUInt8(dest.dest_addr_ton, offset++);
|
||||
buf.writeUInt8(dest.dest_addr_npi, offset++);
|
||||
writeCstring(dest.destination_addr, buf, offset);
|
||||
offset += sizeCstring(dest.destination_addr);
|
||||
offset += dest.destination_addr.length + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,6 +334,16 @@ export const dest_address_array: WireType<DestAddress[]> = {
|
||||
},
|
||||
};
|
||||
|
||||
function sizeUnsuccessSmes(smes: UnsuccessSme[]): number {
|
||||
let size = 1;
|
||||
|
||||
for (const sme of smes) {
|
||||
size += sme.destination_addr.length + 7;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
export const unsuccess_sme_array: WireType<UnsuccessSme[]> = {
|
||||
default: [],
|
||||
read(buf, offset) {
|
||||
@@ -277,7 +351,8 @@ export const unsuccess_sme_array: WireType<UnsuccessSme[]> = {
|
||||
|
||||
if (countErr) return { err: countErr };
|
||||
|
||||
const result: UnsuccessSme[] = [];
|
||||
const start = offset;
|
||||
const value: UnsuccessSme[] = [];
|
||||
let remaining = buf.readUInt8(offset++);
|
||||
|
||||
while (remaining-- > 0) {
|
||||
@@ -287,48 +362,48 @@ export const unsuccess_sme_array: WireType<UnsuccessSme[]> = {
|
||||
|
||||
const dest_addr_ton = buf.readUInt8(offset++);
|
||||
const dest_addr_npi = buf.readUInt8(offset++);
|
||||
const { err, value } = readCstring(buf, offset);
|
||||
const address = readCstring(buf, offset);
|
||||
|
||||
if (err) return { err };
|
||||
if (address.err) return { err: address.err };
|
||||
|
||||
offset += sizeCstring(value);
|
||||
offset += address.bytesRead;
|
||||
|
||||
const statusErr = outOfRange(buf, offset, 4);
|
||||
|
||||
if (statusErr) return { err: statusErr };
|
||||
|
||||
result.push({
|
||||
value.push({
|
||||
dest_addr_npi,
|
||||
dest_addr_ton,
|
||||
destination_addr: value,
|
||||
destination_addr: address.value,
|
||||
error_status_code: buf.readUInt32BE(offset),
|
||||
});
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
return { value: result };
|
||||
return { bytesRead: offset - start, value };
|
||||
},
|
||||
size(value) {
|
||||
let size = 1;
|
||||
const { err, smes } = wantUnsuccessSmes(value);
|
||||
|
||||
for (const sme of value) {
|
||||
size += sizeCstring(sme.destination_addr) + 6;
|
||||
}
|
||||
|
||||
return size;
|
||||
return err ? { err } : { size: sizeUnsuccessSmes(smes) };
|
||||
},
|
||||
write(value, buf, offset) {
|
||||
const err = outOfRange(buf, offset, unsuccess_sme_array.size(value));
|
||||
const { err, smes } = wantUnsuccessSmes(value);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
buf.writeUInt8(value.length, offset++);
|
||||
const rangeErr = outOfRange(buf, offset, sizeUnsuccessSmes(smes));
|
||||
|
||||
for (const sme of value) {
|
||||
buf.writeUInt8(sme.dest_addr_ton || 0, offset++);
|
||||
buf.writeUInt8(sme.dest_addr_npi || 0, offset++);
|
||||
if (rangeErr) return { err: rangeErr };
|
||||
|
||||
buf.writeUInt8(smes.length, offset++);
|
||||
|
||||
for (const sme of smes) {
|
||||
buf.writeUInt8(sme.dest_addr_ton, offset++);
|
||||
buf.writeUInt8(sme.dest_addr_npi, offset++);
|
||||
writeCstring(sme.destination_addr, buf, offset);
|
||||
offset += sizeCstring(sme.destination_addr);
|
||||
offset += sme.destination_addr.length + 1;
|
||||
buf.writeUInt32BE(sme.error_status_code, offset);
|
||||
offset += 4;
|
||||
}
|
||||
@@ -337,45 +412,39 @@ export const unsuccess_sme_array: WireType<UnsuccessSme[]> = {
|
||||
},
|
||||
};
|
||||
|
||||
/** TLV variants are length-prefixed by the TLV header, so they carry no length of their own. */
|
||||
/** TLV variants carry no length of their own; the TLV header supplies it. */
|
||||
export const tlv = {
|
||||
buffer: {
|
||||
default: Buffer.alloc(0),
|
||||
read(buf: Buffer, offset: number, length = 0): Result<{ value: Buffer }> {
|
||||
const err = outOfRange(buf, offset, length);
|
||||
|
||||
return err ? { err } : { value: buf.subarray(offset, offset + length) };
|
||||
},
|
||||
size(value: Buffer | number | string): number {
|
||||
return Buffer.isBuffer(value) ? value.length : asString(value).length;
|
||||
},
|
||||
write: buffer.write,
|
||||
} satisfies WireType<Buffer, Buffer | number | string>,
|
||||
buffer,
|
||||
cstring,
|
||||
int8,
|
||||
int16,
|
||||
int32,
|
||||
string: {
|
||||
default: '',
|
||||
read(buf: Buffer, offset: number, length = 0): Result<{ value: string }> {
|
||||
read(buf: Buffer, offset: number, length = 0) {
|
||||
const err = outOfRange(buf, offset, length);
|
||||
|
||||
return err ? { err } : { value: buf.toString('ascii', offset, offset + length) };
|
||||
return err ? { err } : { bytesRead: length, value: buf.toString('ascii', offset, offset + length) };
|
||||
},
|
||||
size(value: number | string): number {
|
||||
return asString(value).length;
|
||||
size(value: ParamValue) {
|
||||
const { err, text } = wantText(value);
|
||||
|
||||
return err ? { err } : { size: text.length };
|
||||
},
|
||||
write(value: number | string, buf: Buffer, offset: number): VoidResult {
|
||||
const str = asString(value);
|
||||
const err = outOfRange(buf, offset, str.length);
|
||||
write(value: ParamValue, buf: Buffer, offset: number) {
|
||||
const { err, text } = wantText(value);
|
||||
|
||||
if (err) return { err };
|
||||
|
||||
buf.write(str, offset, 'ascii');
|
||||
const rangeErr = outOfRange(buf, offset, text.length);
|
||||
|
||||
if (rangeErr) return { err: rangeErr };
|
||||
|
||||
buf.write(text, offset, 'ascii');
|
||||
|
||||
return {};
|
||||
},
|
||||
} satisfies WireType<string, number | string>,
|
||||
} satisfies WireType<string>,
|
||||
};
|
||||
|
||||
export const types = {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test, { describe } from 'node:test';
|
||||
import type { PduParams, PduParamsInput } from '../src/defs/commands.ts';
|
||||
import { cmds, cmdsById, commandNameById, isCommandName } from '../src/defs/commands.ts';
|
||||
|
||||
describe('command table', () => {
|
||||
test('every command is reachable by name and by id', () => {
|
||||
assert.equal(cmds.submit_sm?.id, 0x00000004);
|
||||
assert.equal(cmds.submit_sm_resp?.id, 0x80000004);
|
||||
assert.equal(cmdsById[0x00000004]?.command, 'submit_sm');
|
||||
assert.equal(cmdsById[0x80000009]?.command, 'bind_transceiver_resp');
|
||||
});
|
||||
|
||||
test('covers all 33 commands', () => {
|
||||
assert.equal(Object.keys(cmds).length, 33);
|
||||
});
|
||||
|
||||
test('bind commands default interface_version to 0x50', () => {
|
||||
assert.equal(cmds.bind_transceiver?.defaults?.interface_version, 0x50);
|
||||
});
|
||||
|
||||
// Wire order, not alphabetical order — reordering these corrupts every PDU.
|
||||
test('submit_sm parameters are in wire order', () => {
|
||||
assert.deepEqual(Object.keys(cmds.submit_sm?.params ?? {}), [
|
||||
'service_type',
|
||||
'source_addr_ton',
|
||||
'source_addr_npi',
|
||||
'source_addr',
|
||||
'dest_addr_ton',
|
||||
'dest_addr_npi',
|
||||
'destination_addr',
|
||||
'esm_class',
|
||||
'protocol_id',
|
||||
'priority_flag',
|
||||
'schedule_delivery_time',
|
||||
'validity_period',
|
||||
'registered_delivery',
|
||||
'replace_if_present_flag',
|
||||
'data_coding',
|
||||
'sm_default_msg_id',
|
||||
'sm_length',
|
||||
'short_message',
|
||||
]);
|
||||
});
|
||||
|
||||
test('resolves names from ids and guards unknown ones', () => {
|
||||
assert.equal(commandNameById(0x00000004), 'submit_sm');
|
||||
assert.equal(commandNameById(0x0BADF00D), undefined);
|
||||
assert.ok(isCommandName('deliver_sm'));
|
||||
assert.ok(!isCommandName('deliver_pizza'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('per-command parameter types', () => {
|
||||
test('narrow to the fields the command actually defines', () => {
|
||||
const params: PduParams<'submit_sm'> = {
|
||||
data_coding: 0,
|
||||
destination_addr: '46709771337',
|
||||
dest_addr_npi: 0,
|
||||
dest_addr_ton: 1,
|
||||
esm_class: 0,
|
||||
priority_flag: 0,
|
||||
protocol_id: 0,
|
||||
registered_delivery: 0,
|
||||
replace_if_present_flag: 0,
|
||||
schedule_delivery_time: '',
|
||||
service_type: '',
|
||||
short_message: Buffer.from('hello'),
|
||||
sm_default_msg_id: 0,
|
||||
sm_length: 5,
|
||||
source_addr: '46701113311',
|
||||
source_addr_npi: 0,
|
||||
source_addr_ton: 1,
|
||||
validity_period: '',
|
||||
};
|
||||
|
||||
// Fails to compile if short_message is not known to be a Buffer.
|
||||
assert.equal(params.short_message.toString(), 'hello');
|
||||
// Fails to compile if destination_addr is not known to be a string.
|
||||
assert.equal(params.destination_addr.length, 11);
|
||||
});
|
||||
|
||||
test('accept a partial input, coercing numbers into the string fields', () => {
|
||||
const input: PduParamsInput<'submit_sm_resp'> = { message_id: 2343 };
|
||||
|
||||
assert.equal(input.message_id, 2343);
|
||||
});
|
||||
});
|
||||
+43
-22
@@ -8,31 +8,37 @@ describe('integers', () => {
|
||||
const source = Buffer.from([0, 0x65]);
|
||||
const target = Buffer.alloc(1);
|
||||
|
||||
assert.deepEqual(types.int8.read(source, 1), { value: 0x65 });
|
||||
assert.equal(types.int8.size(0x65), 1);
|
||||
assert.deepEqual(types.int8.read(source, 1), { bytesRead: 1, value: 0x65 });
|
||||
assert.deepEqual(types.int8.size(0x65), { size: 1 });
|
||||
assert.deepEqual(types.int8.write(0x65, target, 0), {});
|
||||
assert.deepEqual(target, Buffer.from([0x65]));
|
||||
});
|
||||
|
||||
test('int16 reads, sizes and writes two octets big-endian', () => {
|
||||
test('int16 reads and writes two octets big-endian', () => {
|
||||
const source = Buffer.from([0, 0x05, 0x65]);
|
||||
const target = Buffer.alloc(2);
|
||||
|
||||
assert.deepEqual(types.int16.read(source, 1), { value: 0x0565 });
|
||||
assert.equal(types.int16.size(0x0565), 2);
|
||||
assert.deepEqual(types.int16.read(source, 1), { bytesRead: 2, value: 0x0565 });
|
||||
types.int16.write(0x0565, target, 0);
|
||||
assert.deepEqual(target, Buffer.from([0x05, 0x65]));
|
||||
});
|
||||
|
||||
test('int32 reads, sizes and writes four octets big-endian', () => {
|
||||
test('int32 reads and writes four octets big-endian', () => {
|
||||
const source = Buffer.from([0, 0x10, 0x02, 0x40, 0x45]);
|
||||
const target = Buffer.alloc(4);
|
||||
|
||||
assert.deepEqual(types.int32.read(source, 1), { value: 0x10024045 });
|
||||
assert.equal(types.int32.size(0x10024045), 4);
|
||||
assert.deepEqual(types.int32.read(source, 1), { bytesRead: 4, value: 0x10024045 });
|
||||
types.int32.write(0x10024045, target, 0);
|
||||
assert.deepEqual(target, Buffer.from([0x10, 0x02, 0x40, 0x45]));
|
||||
});
|
||||
|
||||
// 0.4.0 let Node throw straight out of writeUInt8 for these.
|
||||
test('rejects values the field cannot hold', () => {
|
||||
assert.ok(types.int8.write(256, Buffer.alloc(1), 0).err instanceof Error);
|
||||
assert.ok(types.int8.write(-1, Buffer.alloc(1), 0).err instanceof Error);
|
||||
assert.ok(types.int16.write(1.5, Buffer.alloc(2), 0).err instanceof Error);
|
||||
assert.ok(types.int8.write('nope', Buffer.alloc(1), 0).err instanceof Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('string (Octet String)', () => {
|
||||
@@ -40,11 +46,11 @@ describe('string (Octet String)', () => {
|
||||
const encoded = Buffer.concat([Buffer.from([8]), Buffer.from(expected)]);
|
||||
|
||||
test('reads a length-prefixed string', () => {
|
||||
assert.deepEqual(types.string.read(encoded, 0), { value: expected });
|
||||
assert.deepEqual(types.string.read(encoded, 0), { bytesRead: 9, value: expected });
|
||||
});
|
||||
|
||||
test('sizes as the string plus its length octet', () => {
|
||||
assert.equal(types.string.size(expected), 9);
|
||||
assert.deepEqual(types.string.size(expected), { size: 9 });
|
||||
});
|
||||
|
||||
test('writes a length-prefixed string', () => {
|
||||
@@ -61,11 +67,11 @@ describe('cstring (C-Octet String)', () => {
|
||||
const encoded = Buffer.concat([Buffer.from(expected), Buffer.from([0])]);
|
||||
|
||||
test('reads a NULL-terminated string', () => {
|
||||
assert.deepEqual(types.cstring.read(encoded, 0), { value: expected });
|
||||
assert.deepEqual(types.cstring.read(encoded, 0), { bytesRead: 9, value: expected });
|
||||
});
|
||||
|
||||
test('sizes as the string plus its NULL terminator', () => {
|
||||
assert.equal(types.cstring.size(expected), 9);
|
||||
assert.deepEqual(types.cstring.size(expected), { size: 9 });
|
||||
});
|
||||
|
||||
test('writes a NULL-terminated string', () => {
|
||||
@@ -82,13 +88,11 @@ describe('cstring (C-Octet String)', () => {
|
||||
types.cstring.write(123, target, 0);
|
||||
|
||||
assert.deepEqual(target, Buffer.from([0x31, 0x32, 0x33, 0x00]));
|
||||
assert.equal(types.cstring.size(123), 4);
|
||||
assert.deepEqual(types.cstring.size(123), { size: 4 });
|
||||
});
|
||||
|
||||
test('refuses a string with no terminator rather than running off the end', () => {
|
||||
const { err } = types.cstring.read(Buffer.from('abcd'), 0);
|
||||
|
||||
assert.ok(err instanceof Error);
|
||||
assert.ok(types.cstring.read(Buffer.from('abcd'), 0).err instanceof Error);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,11 +100,21 @@ describe('buffer', () => {
|
||||
const expected = Buffer.from('abcd1234');
|
||||
|
||||
test('reads a binary field of the given length', () => {
|
||||
assert.deepEqual(types.buffer.read(expected, 0, expected.length), { value: expected });
|
||||
assert.deepEqual(types.buffer.read(expected, 0, expected.length), {
|
||||
bytesRead: 8,
|
||||
value: expected,
|
||||
});
|
||||
});
|
||||
|
||||
test('sizes a binary field in octets', () => {
|
||||
assert.equal(types.buffer.size(expected), 8);
|
||||
assert.deepEqual(types.buffer.size(expected), { size: 8 });
|
||||
});
|
||||
|
||||
// 0.4.0 subtracted one whenever the last octet was 0x00, so a UCS2 message ending in a
|
||||
// character like U+4E00 was allocated one octet short while sm_length still reported the full
|
||||
// length — the PDU went out corrupt.
|
||||
test('counts a trailing NULL octet like any other', () => {
|
||||
assert.deepEqual(types.buffer.size(Buffer.from([0x4E, 0x00])), { size: 2 });
|
||||
});
|
||||
|
||||
test('writes a binary field', () => {
|
||||
@@ -124,11 +138,14 @@ describe('dest_address_array', () => {
|
||||
];
|
||||
|
||||
test('reads every dest_address structure', () => {
|
||||
assert.deepEqual(types.dest_address_array.read(encoded, 0), { value: expected });
|
||||
assert.deepEqual(types.dest_address_array.read(encoded, 0), {
|
||||
bytesRead: 13,
|
||||
value: expected,
|
||||
});
|
||||
});
|
||||
|
||||
test('sizes every dest_address structure', () => {
|
||||
assert.equal(types.dest_address_array.size(expected), 13);
|
||||
assert.deepEqual(types.dest_address_array.size(expected), { size: 13 });
|
||||
});
|
||||
|
||||
test('writes every dest_address structure', () => {
|
||||
@@ -152,11 +169,14 @@ describe('unsuccess_sme_array', () => {
|
||||
];
|
||||
|
||||
test('reads every unsuccess_sme structure', () => {
|
||||
assert.deepEqual(types.unsuccess_sme_array.read(encoded, 0), { value: expected });
|
||||
assert.deepEqual(types.unsuccess_sme_array.read(encoded, 0), {
|
||||
bytesRead: 21,
|
||||
value: expected,
|
||||
});
|
||||
});
|
||||
|
||||
test('sizes every unsuccess_sme structure', () => {
|
||||
assert.equal(types.unsuccess_sme_array.size(expected), 21);
|
||||
assert.deepEqual(types.unsuccess_sme_array.size(expected), { size: 21 });
|
||||
});
|
||||
|
||||
test('writes every unsuccess_sme structure', () => {
|
||||
@@ -174,6 +194,7 @@ describe('bounds checking', () => {
|
||||
assert.ok(types.int32.read(Buffer.alloc(2), 0).err instanceof Error);
|
||||
assert.ok(types.int8.read(Buffer.alloc(1), 5).err instanceof Error);
|
||||
assert.ok(types.string.read(Buffer.from([10, 0x61]), 0).err instanceof Error);
|
||||
assert.ok(types.dest_address_array.read(Buffer.from([0x05, 0x01]), 0).err instanceof Error);
|
||||
});
|
||||
|
||||
test('writing past the end returns an error instead of throwing', () => {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"rewriteRelativeImportExtensions": true,
|
||||
|
||||
Reference in New Issue
Block a user