Read the GSM 03.38 message class where the spec puts it (#95)

* Regression tests for the GSM 03.38 message class behind sms.flash

* Read the GSM 03.38 message class where the spec puts it, and refuse a flash message no alphabet can carry

* Keep the code span in the message-class decision on one line

* Stop offering the flash message class as an alphabet, and check the encoding option by name

* Settle every refusable send option in one check, and record what the review left open

* Say what the flash refusal and the encoding option actually do

* Keep the README off the alphabet whose long messages do not fit
This commit is contained in:
2026-09-09 16:40:32 +02:00
committed by GitHub
parent bd466d393a
commit e80b07167c
11 changed files with 422 additions and 31 deletions
+28 -11
View File
@@ -1,4 +1,4 @@
export type EncodingName = 'ASCII' | 'FLASH' | 'LATIN1' | 'UCS2';
export type EncodingName = 'ASCII' | 'LATIN1' | 'UCS2';
export type Encoding = {
decode: (buffer: Uint8Array) => string;
@@ -104,11 +104,16 @@ const ucs2: Encoding = {
export const encodings: Record<EncodingName, Encoding> = {
ASCII: ascii,
FLASH: ascii,
LATIN1: latin1,
UCS2: ucs2,
};
export const encodingNames: readonly string[] = Object.keys(encodings);
export function isEncodingName(value: unknown): value is EncodingName {
return typeof value === 'string' && Object.hasOwn(encodings, value);
}
export function detect(value: string): EncodingName {
if (encodings.ASCII.match(value)) return 'ASCII';
if (encodings.LATIN1.match(value)) return 'LATIN1';
@@ -116,21 +121,33 @@ export function detect(value: string): EncodingName {
return 'UCS2';
}
/** The 0x1X and 0xFX ranges carry a GSM message class and put the alphabet in bits 3-2 or bit 2. */
function messageClassEncoding(dataCoding: number): EncodingName | undefined {
if ((dataCoding & 0xF0) === 0x10) {
const alphabet = (dataCoding >> 2) & 0x03;
if (alphabet === 0x01) return 'LATIN1';
return alphabet === 0x02 ? 'UCS2' : 'ASCII';
/**
* The GSM 03.38 section 4 message class a `data_coding` octet carries, in bits 1-0, or undefined
* where its coding group carries none. Below 0x80 bit 4 says whether one is there; 0xF0 always is.
*/
export function messageClassOf(dataCoding: number): number | undefined {
if ((dataCoding & 0x80) === 0) {
return (dataCoding & 0x10) === 0x10 ? dataCoding & 0x03 : undefined;
}
return (dataCoding & 0xF0) === 0xF0 ? dataCoding & 0x03 : undefined;
}
// A class is the only evidence a peer below 0x80 is spelling 03.38 rather than SMPP's flat table,
// which contradicts it and wins: 0x03 is Latin-1 here, GSM 7-bit there.
/** A class group puts the alphabet in bits 3-2, or in bit 2 alone above 0xF0. */
function messageClassEncoding(dataCoding: number): EncodingName | undefined {
if (messageClassOf(dataCoding) === undefined) return undefined;
if ((dataCoding & 0xF0) === 0xF0) {
return (dataCoding & 0x04) === 0x04 ? 'LATIN1' : 'ASCII';
}
return undefined;
const alphabet = (dataCoding >> 2) & 0x03;
if (alphabet === 0x01) return 'LATIN1';
return alphabet === 0x02 ? 'UCS2' : 'ASCII';
}
/**