Port SMPP definitions: constants, errors, encodings and wire types

This commit is contained in:
2026-08-25 16:07:34 +02:00
parent 16c935bb18
commit 854c89f609
7 changed files with 915 additions and 3 deletions
+124
View File
@@ -0,0 +1,124 @@
export type EncodingName = 'ASCII' | 'FLASH' | 'LATIN1' | 'UCS2';
export type Encoding = {
decode: (buffer: Uint8Array) => string;
encode: (value: string) => Buffer;
match: (value: string) => boolean;
};
const gsmChars =
'@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\x1BÆæßÉ !"#¤%&\'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà';
const gsmRegex =
/^[@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\x1BÆæßÉ !"#¤%&'()*+,\-./0-9:;<=>?¡A-ZÄÖÑܧ¿a-zäöñüà\f^{}\\[~\]|€]*$/;
const gsmExtended = /[\f^{}\\[~\]|€]/g;
const gsmEscaped = /\x1B([\nΛ()/<=>¡e])/g;
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);
}
{
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]);
}
}
const ascii: Encoding = {
decode(buffer) {
let result = '';
for (const byte of buffer) {
result += gsmChars[byte] ?? ' ';
}
return result.replace(gsmEscaped, (match, escaped: string) => gsmExtChars.get(escaped) ?? match);
},
encode(value) {
const escaped = value.replace(gsmExtended, match => `\x1B${gsmExtChars.get(match) ?? ''}`);
const result: number[] = [];
for (const char of escaped) {
result.push(gsmCharCodes.get(char) ?? 0x20);
}
return Buffer.from(result);
},
match(value) {
return gsmRegex.test(value);
},
};
const latin1: Encoding = {
decode(buffer) {
return Buffer.from(buffer).toString('latin1');
},
encode(value) {
return Buffer.from(value, 'latin1');
},
// Deliberately never selected by detect(); Latin-1 is decoded when a peer asks for it, never
// chosen for outgoing messages.
match() {
return false;
},
};
const ucs2: Encoding = {
decode(buffer) {
return Buffer.from(buffer).swap16().toString('utf16le');
},
encode(value) {
return Buffer.from(value, 'utf16le').swap16();
},
match() {
return true;
},
};
export const encodings: Record<EncodingName, Encoding> = {
ASCII: ascii,
FLASH: ascii,
LATIN1: latin1,
UCS2: ucs2,
};
export function detect(value: string): EncodingName {
if (encodings.ASCII.match(value)) return 'ASCII';
if (encodings.LATIN1.match(value)) return 'LATIN1';
return 'UCS2';
}
/**
* SMPP data_coding is a flat table for 0x00-0x0E, but the 0x1X and 0xFX ranges carry a GSM message
* class and encode the alphabet in bits 3-2 (or bit 2) instead — which is how a flash UCS2 message
* arrives as 0x18. Alphabets with no codec here fall back to ASCII.
*/
export function encodingByDataCoding(dataCoding: number): EncodingName {
if ((dataCoding & 0xF0) === 0x10) {
return ((dataCoding >> 2) & 0x03) === 0x02 ? 'UCS2' : 'ASCII';
}
if ((dataCoding & 0xF0) === 0xF0) {
return 'ASCII';
}
if (dataCoding === 0x03) return 'LATIN1';
if (dataCoding === 0x08) return 'UCS2';
return 'ASCII';
}