UNPKG

node-sms-pdu

Version:

The node-sms-pdu is a SMS-SUBMIT PDU (Packet Data Unit) generator and SMS-SUBMIT/DELIVER PDU parser. This module supports the GSM 7-bit default alphabet encoding and the UCS-2 16-bit alphabet encoding. Besides, it supports the Concatenated (or Multipart o

450 lines (413 loc) 15.6 kB
/* ------------------------------------------------------------------ * node-sms-pdu - sms-pdu-user-data-gsm0338.js * * Copyright (c) 2020, Futomi Hatano, All rights reserved. * Released under the MIT license * Date: 2020-10-29 * ---------------------------------------------------------------- */ 'use strict'; class SmsPduUserDataGsm0338 { /* ------------------------------------------------------------------ * Constructor * * [Arguments] * - None * ---------------------------------------------------------------- */ constructor() { // SMS message size limit (bytes) this._MESSAGE_BYTE_LIMIT = 140; // --------------------------------------------------------------- // GSM 7-bit default alphabet and extension table // - The keys in the object represent GSM codes // - The values in the object represent Unicode scalar values, // that is, code points // - Reference // https://www.unicode.org/Public/MAPPINGS/ETSI/GSM0338.TXT // --------------------------------------------------------------- this._DECODE_TABLE = { 0x00: 0x0040, // @ (COMMERCIAL AT) 0x01: 0x00A3, // £ (POUND SIGN) 0x02: 0x0024, // $ (DOLLAR SIGN) 0x03: 0x00A5, // ¥ (YEN SIGN) 0x04: 0x00E8, // è (LATIN SMALL LETTER E WITH GRAVE) 0x05: 0x00E9, // é (LATIN SMALL LETTER E WITH ACUTE) 0x06: 0x00F9, // ù (LATIN SMALL LETTER U WITH GRAVE) 0x07: 0x00EC, // ì (LATIN SMALL LETTER I WITH GRAVE) 0x08: 0x00F2, // ò (LATIN SMALL LETTER O WITH GRAVE) 0x09: 0x00E7, // Ç (LATIN SMALL LETTER C WITH CEDILLA) 0x0A: 0x000A, // (LINE FEED) 0x0B: 0x00D8, // Ø (LATIN CAPITAL LETTER O WITH STROKE) 0x0C: 0x00F8, // ø (LATIN SMALL LETTER O WITH STROKE) 0x0D: 0x000D, // (CARRIAGE RETURN) 0x0E: 0x00C5, // Å (LATIN CAPITAL LETTER A WITH RING ABOVE) 0x0F: 0x00E5, // å (LATIN SMALL LETTER A WITH RING ABOVE) 0x10: 0x0394, // Δ (GREEK CAPITAL LETTER DELTA) 0x11: 0x005F, // _ (LOW LINE) 0x12: 0x03A6, // Φ (GREEK CAPITAL LETTER PHI) 0x13: 0x0393, // Γ (GREEK CAPITAL LETTER GAMMA) 0x14: 0x039B, // Λ (GREEK CAPITAL LETTER LAMDA) 0x15: 0x03A9, // Ω (GREEK CAPITAL LETTER OMEGA) 0x16: 0x03A0, // Π (GREEK CAPITAL LETTER PI) 0x17: 0x03A8, // Ψ (GREEK CAPITAL LETTER PSI) 0x18: 0x03A3, // Σ (GREEK CAPITAL LETTER SIGMA) 0x19: 0x0398, // Θ (GREEK CAPITAL LETTER THETA) 0x1A: 0x039E, // Ξ (GREEK CAPITAL LETTER XI) 0x1B: 0x00A0, // (ESCAPE TO EXTENSION TABLE (or displayed as NBSP, see note above)) 0x1C: 0x00C6, // Æ (LATIN CAPITAL LETTER AE) 0x1D: 0x00E6, // æ (LATIN SMALL LETTER AE) 0x1E: 0x00DF, // ß (LATIN SMALL LETTER SHARP S (German)) 0x1F: 0x00C9, // É (LATIN CAPITAL LETTER E WITH ACUTE) 0x20: 0x0020, // (SPACE) 0x21: 0x0021, // ! (EXCLAMATION MARK) 0x22: 0x0022, // " (QUOTATION MARK) 0x23: 0x0023, // # (NUMBER SIGN) 0x24: 0x00A4, // ¤ (CURRENCY SIGN) 0x25: 0x0025, // % (PERCENT SIGN) 0x26: 0x0026, // & (AMPERSAND) 0x27: 0x0027, // ' (APOSTROPHE) 0x28: 0x0028, // ( (LEFT PARENTHESIS) 0x29: 0x0029, // ) (RIGHT PARENTHESIS) 0x2A: 0x002A, // * (ASTERISK) 0x2B: 0x002B, // + (PLUS SIGN) 0x2C: 0x002C, // , (COMMA) 0x2D: 0x002D, // - (HYPHEN-MINUS) 0x2E: 0x002E, // . (FULL STOP) 0x2F: 0x002F, // / (SOLIDUS) 0x30: 0x0030, // 0 (DIGIT ZERO) 0x31: 0x0031, // 1 (DIGIT ONE) 0x32: 0x0032, // 2 (DIGIT TWO) 0x33: 0x0033, // 3 (DIGIT THREE) 0x34: 0x0034, // 4 (DIGIT FOUR) 0x35: 0x0035, // 5 (DIGIT FIVE) 0x36: 0x0036, // 6 (DIGIT SIX) 0x37: 0x0037, // 7 (DIGIT SEVEN) 0x38: 0x0038, // 8 (DIGIT EIGHT) 0x39: 0x0039, // 9 (DIGIT NINE) 0x3A: 0x003A, // : (COLON) 0x3B: 0x003B, // ; (SEMICOLON) 0x3C: 0x003C, // < (LESS-THAN SIGN) 0x3D: 0x003D, // = (EQUALS SIGN) 0x3E: 0x003E, // > (GREATER-THAN SIGN) 0x3F: 0x003F, // ? (QUESTION MARK) 0x40: 0x00A1, // ¡ (INVERTED EXCLAMATION MARK) 0x41: 0x0041, // A (LATIN CAPITAL LETTER A) 0x42: 0x0042, // B (LATIN CAPITAL LETTER B) 0x43: 0x0043, // C (LATIN CAPITAL LETTER C) 0x44: 0x0044, // D (LATIN CAPITAL LETTER D) 0x45: 0x0045, // E (LATIN CAPITAL LETTER E) 0x46: 0x0046, // F (LATIN CAPITAL LETTER F) 0x47: 0x0047, // G (LATIN CAPITAL LETTER G) 0x48: 0x0048, // H (LATIN CAPITAL LETTER H) 0x49: 0x0049, // I (LATIN CAPITAL LETTER I) 0x4A: 0x004A, // J (LATIN CAPITAL LETTER J) 0x4B: 0x004B, // K (LATIN CAPITAL LETTER K) 0x4C: 0x004C, // L (LATIN CAPITAL LETTER L) 0x4D: 0x004D, // M (LATIN CAPITAL LETTER M) 0x4E: 0x004E, // N (LATIN CAPITAL LETTER N) 0x4F: 0x004F, // O (LATIN CAPITAL LETTER O) 0x50: 0x0050, // P (LATIN CAPITAL LETTER P) 0x51: 0x0051, // Q (LATIN CAPITAL LETTER Q) 0x52: 0x0052, // R (LATIN CAPITAL LETTER R) 0x53: 0x0053, // S (LATIN CAPITAL LETTER S) 0x54: 0x0054, // T (LATIN CAPITAL LETTER T) 0x55: 0x0055, // U (LATIN CAPITAL LETTER U) 0x56: 0x0056, // V (LATIN CAPITAL LETTER V) 0x57: 0x0057, // W (LATIN CAPITAL LETTER W) 0x58: 0x0058, // X (LATIN CAPITAL LETTER X) 0x59: 0x0059, // Y (LATIN CAPITAL LETTER Y) 0x5A: 0x005A, // Z (LATIN CAPITAL LETTER Z) 0x5B: 0x00C4, // Ä (LATIN CAPITAL LETTER A WITH DIAERESIS) 0x5C: 0x00D6, // Ö (LATIN CAPITAL LETTER O WITH DIAERESIS) 0x5D: 0x00D1, // Ñ (LATIN CAPITAL LETTER N WITH TILDE) 0x5E: 0x00DC, // Ü (LATIN CAPITAL LETTER U WITH DIAERESIS) 0x5F: 0x00A7, // § (SECTION SIGN) 0x60: 0x00BF, // ¿ (INVERTED QUESTION MARK) 0x61: 0x0061, // a (LATIN SMALL LETTER A) 0x62: 0x0062, // b (LATIN SMALL LETTER B) 0x63: 0x0063, // c (LATIN SMALL LETTER C) 0x64: 0x0064, // d (LATIN SMALL LETTER D) 0x65: 0x0065, // e (LATIN SMALL LETTER E) 0x66: 0x0066, // f (LATIN SMALL LETTER F) 0x67: 0x0067, // g (LATIN SMALL LETTER G) 0x68: 0x0068, // h (LATIN SMALL LETTER H) 0x69: 0x0069, // i (LATIN SMALL LETTER I) 0x6A: 0x006A, // j (LATIN SMALL LETTER J) 0x6B: 0x006B, // k (LATIN SMALL LETTER K) 0x6C: 0x006C, // l (LATIN SMALL LETTER L) 0x6D: 0x006D, // m (LATIN SMALL LETTER M) 0x6E: 0x006E, // n (LATIN SMALL LETTER N) 0x6F: 0x006F, // o (LATIN SMALL LETTER O) 0x70: 0x0070, // p (LATIN SMALL LETTER P) 0x71: 0x0071, // q (LATIN SMALL LETTER Q) 0x72: 0x0072, // r (LATIN SMALL LETTER R) 0x73: 0x0073, // s (LATIN SMALL LETTER S) 0x74: 0x0074, // t (LATIN SMALL LETTER T) 0x75: 0x0075, // u (LATIN SMALL LETTER U) 0x76: 0x0076, // v (LATIN SMALL LETTER V) 0x77: 0x0077, // w (LATIN SMALL LETTER W) 0x78: 0x0078, // x (LATIN SMALL LETTER X) 0x79: 0x0079, // y (LATIN SMALL LETTER Y) 0x7A: 0x007A, // z (LATIN SMALL LETTER Z) 0x7B: 0x00E4, // ä (LATIN SMALL LETTER A WITH DIAERESIS) 0x7C: 0x00F6, // ö (LATIN SMALL LETTER O WITH DIAERESIS) 0x7D: 0x00F1, // ñ (LATIN SMALL LETTER N WITH TILDE) 0x7E: 0x00FC, // ü (LATIN SMALL LETTER U WITH DIAERESIS) 0x7F: 0x00E0, // à (LATIN SMALL LETTER A WITH GRAVE) }; this._DECODE_TABLE_EXTENSION = { 0x0A: 0x000C, // (FORM FEED) 0x14: 0x005E, // ^ (CIRCUMFLEX ACCENT) 0x28: 0x007B, // { (LEFT CURLY BRACKET) 0x29: 0x007D, // } (RIGHT CURLY BRACKET) 0x2F: 0x005C, // \ (REVERSE SOLIDUS) 0x3C: 0x005B, // [ (LEFT SQUARE BRACKET) 0x3D: 0x007E, // ~ (TILDE) 0x3E: 0x005D, // ] (RIGHT SQUARE BRACKET) 0x40: 0x007C, // | (VERTICAL LINE) 0x65: 0x20AC, // € (EURO SIGN) }; // Convert Unicode scalar values to node readable characters this._ENCODE_TABLE = {}; for (let [gcode, ucode] of Object.entries(this._DECODE_TABLE)) { this._ENCODE_TABLE[ucode] = parseInt(gcode, 10); } this._ENCODE_TABLE_EXTENSION = {}; for (let [gcode, ucode] of Object.entries(this._DECODE_TABLE_EXTENSION)) { this._ENCODE_TABLE_EXTENSION[ucode] = parseInt(gcode, 10); } } /* ------------------------------------------------------------------ * parse(buf, offset) * - Decode the UD (User Data) * * [Arguments] * - buf | Buffer | Required | Buffer object of the UD * - offset | Integer | Required | Byte offset (UDH) * * [Returen value] * - Decoded text * ---------------------------------------------------------------- */ decode(buf, offset) { let char_num = Math.floor((buf.length * 8) / 7); let text = ''; let escaped = false; for (let i = 0; i < char_num; i++) { let byte_offset = Math.ceil((i * 7) / 8) - 1; if ((i * 7) % 8 === 0) { byte_offset += 1; } if (byte_offset < offset) { continue; } let bit_offset = 8 - (((i + 1) * 7) % 8); bit_offset = bit_offset % 8; let char_code = 0; if (bit_offset === 0) { char_code = buf.readUInt8(byte_offset) >>> 1; } else if (bit_offset === 1) { char_code = buf.readUInt8(byte_offset) & 0b01111111; } else { let n0 = (buf.readUInt8(byte_offset + 1) << (bit_offset - 1)) & 0b01111111; let rbit_num = 7 - (8 - bit_offset); let n1 = buf.readUInt8(byte_offset) >>> (8 - rbit_num); char_code = n0 | n1; } if (char_code === 0x0D) { if (i === char_num - 1 && bit_offset === 0) { break; } } if (char_code === 0x1B) { if (escaped === true) { text += ' '; } else { escaped = true; } continue; } if (escaped === true) { escaped = false; if (this._DECODE_TABLE_EXTENSION[char_code]) { let c = this._DECODE_TABLE_EXTENSION[char_code]; text += String.fromCharCode(c); } else { text += '?'; } } else { if (this._DECODE_TABLE[char_code]) { let c = this._DECODE_TABLE[char_code]; text += String.fromCharCode(c); } else { text += '?'; } } } text = text.replace(/\@$/, ''); return text; } /* ------------------------------------------------------------------ * isAcceptable(text) * - Check if the specified text is a GSM 7 bit default alphabet text * or not * * [Arguments] * - text | String | Required | Text * * [Returen value] * - If the specified text is a GSM 7 bit default alphabet text, this * method returns `true`. Otherwise, this method returns `false`. * ---------------------------------------------------------------- */ isAcceptable(text) { if (!text || typeof (text) !== 'string') { throw new Error('The `text` is not a string.'); } let char_list = text.split(''); let result = true; for (let char of char_list) { let ccode = char.charCodeAt(0); let gcode = null; if (ccode in this._ENCODE_TABLE) { // Basic Character Set gcode = this._ENCODE_TABLE[ccode]; } else if (ccode in this._ENCODE_TABLE_EXTENSION) { // Basic Character Set Extension gcode = this._ENCODE_TABLE_EXTENSION[ccode]; } if (gcode === null) { result = false; break; } } return result; } /* ------------------------------------------------------------------ * encode(text) * - Encode the specified text to a SMS message body * * [Arguments] * - text | String | Required | Text * * - Array: * [ * { * length: 21, // UDL (Byte length of the UD) * buffer: <Buffer 00 01...> // Buffer object of the UD * }, * ... * ] * ---------------------------------------------------------------- */ encode(text) { if (!this.isAcceptable(text)) { throw new Error('The `text` includes a charcter which is not a GSM 7 bit default alphabet.'); } let char_list = text.split(''); // Create a 7bit code (septet) list let septet_list = []; for (let char of char_list) { let ccode = char.charCodeAt(0); if (ccode in this._ENCODE_TABLE) { // Basic Character Set septet_list.push(this._ENCODE_TABLE[ccode]); } else if (ccode in this._ENCODE_TABLE_EXTENSION) { // Basic Character Set Extension septet_list.push(0x1B); septet_list.push(this._ENCODE_TABLE_EXTENSION[ccode]); } } // Multipart let multipart_num = 1; if (septet_list.length > 160) { multipart_num = Math.floor(septet_list.length / 153); if (septet_list.length % 153) { multipart_num++; } if (multipart_num > 255) { throw new Error('The `text` is too long.'); } } let septet_group_list = []; if (multipart_num === 1) { septet_group_list.push(septet_list); } else { let group = []; while (true) { let septet = septet_list.shift(); if (!septet) { if (group.length > 0) { septet_group_list.push(group); } break; } group.push(septet); if (group.length === 153) { septet_group_list.push(group); group = []; } } } let list = []; for (let i = 0; i < septet_group_list.length; i++) { let septet_group = septet_group_list[i]; // Determine the byte length let bit_len = septet_group.length * 7; let byte_len = Math.floor(bit_len / 8); let rbit_len = 8 - (bit_len % 8); if (rbit_len > 0) { byte_len++; } // Set spare bits if (rbit_len === 7) { // Set to the 7-bit code of the CR control (also used as a padding filler) septet_group.push(0x0D); } // Create a Buffer object let buf = null; if (multipart_num > 1) { buf = Buffer.alloc(6 + byte_len); septet_group.unshift(0, 0, 0, 0, 0, 0, 0); } else { buf = Buffer.alloc(byte_len); } // UDL (User Data Length) : Septets let udl = septet_group.length; // Pack the 7bit codes in the Buffer object for (let i = 0; i < septet_group.length; i++) { let septet = septet_group[i]; let char_no = i + 1; let byte_offset = Math.floor((char_no * 7) / 8); let bit_offset = 8 - ((char_no * 7) % 8); if (char_no % 8 === 0) { byte_offset--; bit_offset = 0; }; if (bit_offset === 0) { let n = buf.readUInt8(byte_offset) | (septet << 1); buf.writeUInt8(n, byte_offset); } else if (bit_offset === 1) { let n = buf.readUInt8(byte_offset) | septet; buf.writeUInt8(n, byte_offset); } else { let n0 = buf.readUInt8(byte_offset) | (septet >>> (bit_offset - 1)); buf.writeUInt8(n0, byte_offset); let n1 = buf.readUInt8(byte_offset - 1) | ((septet << (8 - bit_offset + 1)) % 256); buf.writeUInt8(n1, byte_offset - 1); } } // UDH (User Data Header) if (multipart_num > 1) { buf.writeUInt8(0x05, 0); buf.writeUInt8(0x00, 1); buf.writeUInt8(0x03, 2); buf.writeUInt8(0x00, 3); buf.writeUInt8(multipart_num, 4); buf.writeUInt8(i + 1, 5); } list.push({ length: udl, buffer: buf }); } return list; } } module.exports = new SmsPduUserDataGsm0338();