From eb055c04ce731d531e274fad47b06742b6831743 Mon Sep 17 00:00:00 2001 From: Gleb Pesin Date: Mon, 10 Aug 2026 20:04:28 +0000 Subject: [PATCH 1/2] sms_tool: support UTF-8 SMS submission Encode non-ASCII UTF-8 input as UCS-2. Normalize leading plus signs in international numbers. Reject malformed input instead of submitting a broken PDU. Add deterministic ASCII and UCS-2 submit test vectors. Make the send path propagate encoding and modem errors. --- README.md | 2 +- pdu_lib/Makefile | 8 +++- pdu_lib/pdu.c | 117 +++++++++++++++++++++++++++++++++++++++++---- pdu_lib/pdu_test.c | 64 +++++++++++++++++++++++++ sms_main.c | 62 ++++++++++++++++-------- 5 files changed, 220 insertions(+), 33 deletions(-) create mode 100644 pdu_lib/pdu_test.c diff --git a/README.md b/README.md index a2ea468..fd884be 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ SMS Tool for 3G/4G/5G modem * sms tool for various of 3g/4g modem * read sms as raw pdu or decoded text -* support ucs2 decoding +* support ucs2 decoding and UTF-8/UCS-2 sending Usage: ---------------- diff --git a/pdu_lib/Makefile b/pdu_lib/Makefile index 9564987..d04df60 100644 --- a/pdu_lib/Makefile +++ b/pdu_lib/Makefile @@ -15,7 +15,11 @@ ucs2_to_utf8: pdu_decoder: pdu.o pdu_decoder.o ucs2_to_utf8 $(CC) $(CFLAGS) ucs2_to_utf8.o pdu.o pdu_decoder.o -lm -o pdu_decoder clean: - rm -f *.o pdu_decoder -test: clean ALL + rm -f *.o pdu_decoder pdu_test +test: clean ALL pdu_test echo "0891683108501405F8240BA10156686616F60008414090912385235C6D4191CF6C4752A860015BC67801FF1A00350030003900360036FF0C4EB2FF0C8BB05F9762BD59566BCF592990FD8981676554E6FF0C611F89C9597D76848BDD63A883507ED960A87684670B53CBFF019884795D60A84E2D5956FF01"|./pdu_decoder echo "0891683108501405F8640BA10156686616F6000841400100957423830608048A3002026B21767B5F556D4191CF6C475373900100356D4191CF5E01FF0853EF63620035004D6D4191CFFF09FF0C731B623394FE63A5FF1A0068007400740070003A002F002F007300680061006B0065002E00730064002E006300680069006E0061006D006F00620069006C0065002E0063006F006D30025C714E1C79FB52A8"|./pdu_decoder + ./pdu_test + +pdu_test: pdu.o pdu_test.o + $(CC) $(CFLAGS) pdu.o pdu_test.o -lm -o pdu_test diff --git a/pdu_lib/pdu.c b/pdu_lib/pdu.c index d1d282b..fa369a8 100644 --- a/pdu_lib/pdu.c +++ b/pdu_lib/pdu.c @@ -30,6 +30,7 @@ enum { SMS_SUBMIT = 0x11, SMS_MAX_7BIT_TEXT_LENGTH = 160, + SMS_MAX_UCS2_TEXT_LENGTH = 70, }; // Swap decimal digits of a number (e.g. 12 -> 21). @@ -208,7 +209,7 @@ static const int latin1_to_gsm7bits[256] = { }; static int -AsciiToG7bit(const char* buffer, int buffer_length, unsigned char* output_buffer) +AsciiToG7bit(const char* buffer, int buffer_length, char* output_buffer) { int i, j, val; @@ -247,7 +248,7 @@ EncodePhoneNumber(const char* phone_number, unsigned char* output_buffer, int bu int i = 0; for (; i < phone_number_length; ++i) { - if (phone_number[i] < '0' && phone_number[i] > '9') + if (phone_number[i] < '0' || phone_number[i] > '9') return -1; if (i % 2 == 0) { @@ -262,6 +263,67 @@ EncodePhoneNumber(const char* phone_number, unsigned char* output_buffer, int bu return output_buffer_length; } +/* + * Convert a valid UTF-8 string to UCS-2 (UTF-16BE without surrogate pairs). + * SMS UCS-2 user data is limited to 140 octets for a single message. + */ +static int +Utf8ToUcs2(const char* input, unsigned char* output_buffer, int buffer_size) +{ + const unsigned char* p = (const unsigned char*)input; + int output_buffer_length = 0; + + while (*p) { + unsigned int codepoint; + + if (*p < 0x80) { + codepoint = *p++; + } else if (*p >= 0xC2 && *p <= 0xDF) { + if ((p[1] & 0xC0) != 0x80) + return -1; + codepoint = ((*p & 0x1F) << 6) | (p[1] & 0x3F); + p += 2; + } else if (*p >= 0xE0 && *p <= 0xEF) { + if ((p[1] & 0xC0) != 0x80 || (p[2] & 0xC0) != 0x80) + return -1; + if ((*p == 0xE0 && p[1] < 0xA0) || + (*p == 0xED && p[1] >= 0xA0)) + return -1; + codepoint = ((*p & 0x0F) << 12) | + ((p[1] & 0x3F) << 6) | (p[2] & 0x3F); + p += 3; + } else { + /* UCS-2 cannot represent four-byte UTF-8 code points. */ + return -1; + } + + if (output_buffer_length + 2 > buffer_size) + return -1; + output_buffer[output_buffer_length++] = codepoint >> 8; + output_buffer[output_buffer_length++] = codepoint & 0xFF; + } + + return output_buffer_length; +} + +static int +NormalizePhoneNumber(const char* phone_number, const char** digits, int* international) +{ + if (!phone_number || !*phone_number) + return -1; + + *international = phone_number[0] == '+'; + *digits = phone_number + *international; + if (!**digits) + return -1; + + const size_t length = strlen(*digits); + if (length > 255) + return -1; + + return length; +} + // Decode a digit based phone number for SMS based format. static int DecodePhoneNumber(const unsigned char* buffer, int phone_number_length, char* output_phone_number) @@ -282,7 +344,7 @@ int pdu_encode(const char* service_center_number, const char* phone_number, const char* sms_text, unsigned char* output_buffer, int buffer_size) { - if (buffer_size < 2) + if (!phone_number || !sms_text || !output_buffer || buffer_size < 2) return -1; int output_buffer_length = 0; @@ -290,10 +352,15 @@ pdu_encode(const char* service_center_number, const char* phone_number, const ch // 1. Set SMS center number. int length = 0; if (service_center_number && strlen(service_center_number) > 0) { + const char* service_center_digits; + int service_center_international; + if (NormalizePhoneNumber(service_center_number, &service_center_digits, + &service_center_international) < 0) + return -1; output_buffer[1] = TYPE_OF_ADDRESS_INTERNATIONAL_PHONE; - length = EncodePhoneNumber(service_center_number, + length = EncodePhoneNumber(service_center_digits, output_buffer + 2, buffer_size - 2); - if (length < 0 && length >= 254) + if (length < 0 || length >= 254) return -1; length++; // Add type of address. } @@ -307,29 +374,60 @@ pdu_encode(const char* service_center_number, const char* phone_number, const ch output_buffer[output_buffer_length++] = 0x00; // Message reference. // 3. Set phone number. - output_buffer[output_buffer_length] = strlen(phone_number); + const char* phone_number_digits; + int phone_number_international; + const int phone_number_length = NormalizePhoneNumber(phone_number, + &phone_number_digits, + &phone_number_international); + if (phone_number_length < 0) + return -1; + output_buffer[output_buffer_length] = phone_number_length; - if (strlen(phone_number) < 6) { + if (!phone_number_international && phone_number_length < 6) { output_buffer[output_buffer_length + 1] = TYPE_OF_ADDRESS_UNKNOWN; } else { output_buffer[output_buffer_length + 1] = TYPE_OF_ADDRESS_INTERNATIONAL_PHONE; } - length = EncodePhoneNumber(phone_number, + length = EncodePhoneNumber(phone_number_digits, output_buffer + output_buffer_length + 2, buffer_size - output_buffer_length - 2); + if (length < 0) + return -1; output_buffer_length += length + 2; if (output_buffer_length + 4 > buffer_size) return -1; // Check if it has space for four more bytes. + /* Use UCS-2 whenever the UTF-8 input is not pure ASCII. */ + int use_ucs2 = 0; + for (const unsigned char* p = (const unsigned char*)sms_text; *p; ++p) { + if (*p & 0x80) { + use_ucs2 = 1; + break; + } + } + // 4. Protocol identifiers. output_buffer[output_buffer_length++] = 0x00; // TP-PID: Protocol identifier. - output_buffer[output_buffer_length++] = 0x00; // TP-DCS: Data coding scheme. + output_buffer[output_buffer_length++] = use_ucs2 ? 0x08 : 0x00; output_buffer[output_buffer_length++] = 0xB0; // TP-VP: Validity: 10 days // 5. SMS message. int sms_text_length = strlen(sms_text); + if (use_ucs2) { + unsigned char sms_text_ucs2[2 * SMS_MAX_UCS2_TEXT_LENGTH]; + sms_text_length = Utf8ToUcs2(sms_text, sms_text_ucs2, + sizeof(sms_text_ucs2)); + if (sms_text_length < 0 || + output_buffer_length + 1 + sms_text_length > buffer_size) + return -1; + output_buffer[output_buffer_length++] = sms_text_length; + memcpy(output_buffer + output_buffer_length, sms_text_ucs2, + sms_text_length); + return output_buffer_length + sms_text_length; + } + char sms_text_7bit[2*SMS_MAX_7BIT_TEXT_LENGTH]; sms_text_length = AsciiToG7bit(sms_text, sms_text_length, sms_text_7bit); if (sms_text_length > SMS_MAX_7BIT_TEXT_LENGTH) @@ -445,4 +543,3 @@ int pdu_decode(const unsigned char* buffer, int buffer_length, return output_sms_text_length; } - diff --git a/pdu_lib/pdu_test.c b/pdu_lib/pdu_test.c new file mode 100644 index 0000000..6078999 --- /dev/null +++ b/pdu_lib/pdu_test.c @@ -0,0 +1,64 @@ +#include "pdu.h" + +#include +#include + +static int expect_pdu(const char* name, const char* number, const char* text, + const unsigned char* expected, size_t expected_length) +{ + unsigned char actual[SMS_MAX_PDU_LENGTH]; + int actual_length = pdu_encode("", number, text, actual, sizeof(actual)); + + if (actual_length != (int)expected_length || + memcmp(actual, expected, expected_length) != 0) { + fprintf(stderr, "%s: encoded PDU does not match\n", name); + return 1; + } + return 0; +} + +int main(void) +{ + static const unsigned char ascii_pdu[] = { + 0x00, 0x11, 0x00, 0x0A, 0x91, 0x21, 0x43, 0x65, 0x87, 0x09, + 0x00, 0x00, 0xB0, 0x05, 0xE8, 0x32, 0x9B, 0xFD, 0x06, + }; + static const unsigned char ucs2_pdu[] = { + 0x00, 0x11, 0x00, 0x0B, 0x91, 0x97, 0x50, 0x25, 0x00, 0x53, + 0xF3, 0x00, 0x08, 0xB0, 0x2C, 0x04, 0x14, 0x04, 0x38, 0x04, + 0x3C, 0x04, 0x30, 0x00, 0x2C, 0x00, 0x20, 0x04, 0x3F, 0x04, + 0x38, 0x04, 0x48, 0x04, 0x35, 0x04, 0x42, 0x00, 0x20, 0x04, + 0x42, 0x04, 0x35, 0x04, 0x31, 0x04, 0x35, 0x00, 0x20, 0x00, + 0x4E, 0x00, 0x4C, 0x00, 0x36, 0x00, 0x37, 0x00, 0x38, + }; + unsigned char pdu[SMS_MAX_PDU_LENGTH]; + char long_ucs2[71 * 2 + 1]; + int failed = 0; + + failed |= expect_pdu("ASCII", "+1234567890", "hello", + ascii_pdu, sizeof(ascii_pdu)); + failed |= expect_pdu("UCS-2", "+79055200353", + "Дима, пишет тебе NL678", + ucs2_pdu, sizeof(ucs2_pdu)); + + if (pdu_encode("", "+12x34", "test", pdu, sizeof(pdu)) >= 0) { + fprintf(stderr, "invalid destination was accepted\n"); + failed = 1; + } + if (pdu_encode("", "+1234", "\xF0\x9F\x98\x80", pdu, + sizeof(pdu)) >= 0) { + fprintf(stderr, "non-UCS-2 code point was accepted\n"); + failed = 1; + } + for (int i = 0; i < 71; ++i) { + long_ucs2[2 * i] = (char)0xC2; + long_ucs2[2 * i + 1] = (char)0xA3; + } + long_ucs2[sizeof(long_ucs2) - 1] = '\0'; + if (pdu_encode("", "+1234", long_ucs2, pdu, sizeof(pdu)) >= 0) { + fprintf(stderr, "overlong UCS-2 message was accepted\n"); + failed = 1; + } + + return failed; +} diff --git a/sms_main.c b/sms_main.c index 0fdd7c6..d8155a5 100644 --- a/sms_main.c +++ b/sms_main.c @@ -194,8 +194,6 @@ int main(int argc, char* argv[]) { if(argc < 3) usage(); - if(strlen(argv[2]) > 160) - fprintf(stderr,"sms message too long: '%s'\n", argv[2]); }else if (!strcmp("delete",argv[0])) { if(argc < 2) @@ -222,20 +220,26 @@ int main(int argc, char* argv[]) // open the port port = open(dev, O_RDWR|O_NONBLOCK|O_NOCTTY); - if (port < 0) - fprintf(stderr,"open(%s)\n", dev); + if (port < 0) { + fprintf(stderr, "open(%s): %s\n", dev, strerror(errno)); + return 1; + } setserial(baudrate); atexit(resetserial); close(port); port = open(dev, O_RDWR|O_NOCTTY); - if (port < 0) - fprintf(stderr,"reopen(%s)\n", dev); + if (port < 0) { + fprintf(stderr, "reopen(%s): %s\n", dev, strerror(errno)); + return 1; + } FILE* pf = fdopen(port, "w"); FILE* pfi = fdopen(port, "r"); - if (!pf || ! pfi) - fprintf(stderr,"open port failed\n"); + if (!pf || !pfi) { + fprintf(stderr, "fdopen(%s): %s\n", dev, strerror(errno)); + return 1; + } if(setvbuf(pf, NULL, _IOLBF, 0)) { fprintf(stderr, "failed to make serial port linebuffered\n"); @@ -245,8 +249,11 @@ int main(int argc, char* argv[]) if (!strcmp("send", argv[0])) { int pdu_len = pdu_encode("", argv[1], argv[2], pdu, sizeof(pdu)); - if (pdu_len < 0) - fprintf(stderr,"error encoding to PDU: %s \"%s\n", argv[1], argv[2]); + if (pdu_len < 0) { + fprintf(stderr, "error encoding to PDU: %s \"%s\"\n", + argv[1], argv[2]); + return 1; + } const int pdu_len_except_smsc = pdu_len - 1 - pdu[0]; snprintf(cmdstr, sizeof(cmdstr), "AT+CMGS=%d\r\n", pdu_len_except_smsc); @@ -256,36 +263,51 @@ int main(int argc, char* argv[]) sprintf(pdustr+2*i, "%02X", pdu[i]); sprintf(pdustr+2*i, "%c\r\n", 0x1A); // End PDU mode with Ctrl-Z. - fputs("AT+CMGF=0\r\n", pf); + alarm(30); + if (fputs("AT+CMGF=0\r\n", pf) == EOF) + return 1; + int pdu_mode_ready = 0; while(fgets(buf, sizeof(buf), pfi)) { - if(starts_with("OK", buf)) + if(starts_with("OK", buf)) { + pdu_mode_ready = 1; break; + } + if(starts_with("ERROR", buf) || starts_with("+CMS ERROR:", buf)) { + fprintf(stderr, "failed to enable PDU mode: %s", buf); + return 1; + } } - fputs(cmdstr, pf); + if (!pdu_mode_ready) { + fprintf(stderr, "no response while enabling PDU mode\n"); + return 1; + } + if (fputs(cmdstr, pf) == EOF) + return 1; sleep(1); - fputs(pdustr, pf); + if (fputs(pdustr, pf) == EOF) + return 1; - alarm(5); errno = 0; while(fgets(buf, sizeof(buf), pfi)) { if(starts_with("+CMGS:", buf)) { - printf("sms sent sucessfully: %s", buf + 7); + alarm(0); + printf("sms sent successfully: %s", buf + 7); return 0; } else if(starts_with("+CMS ERROR:", buf)) { fprintf(stderr,"sms not sent, code: %s\n", buf + 11); + return 1; } else if(starts_with("ERROR", buf)) { fprintf(stderr,"sms not sent, command error\n"); - } else if(starts_with("OK", buf)) - { - return 0; + return 1; } } - fprintf(stderr,"reading port\n"); + fprintf(stderr, "reading port: %s\n", strerror(errno)); + return 1; } if (!strcmp("recv", argv[0])) From fa434b8263bf59f46049f1caeed74a128d2a88f7 Mon Sep 17 00:00:00 2001 From: Gleb Pesin Date: Mon, 10 Aug 2026 20:21:23 +0000 Subject: [PATCH 2/2] sms_tool: split long messages into SMS parts Add concatenated SMS submission with an 8-bit UDH reference. Use 153 septets per GSM-7 part and 67 characters per UCS-2 part. Keep GSM extension pairs together at part boundaries. Add multipart packing, boundary and 255-part limit tests. --- README.md | 1 + pdu_lib/pdu.c | 246 +++++++++++++++++++++++++++++++++++++-------- pdu_lib/pdu.h | 10 ++ pdu_lib/pdu_test.c | 165 ++++++++++++++++++++++++++++++ sms_main.c | 86 ++++++++++------ 5 files changed, 436 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index fd884be..77e8b8d 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ SMS Tool for 3G/4G/5G modem * sms tool for various of 3g/4g modem * read sms as raw pdu or decoded text * support ucs2 decoding and UTF-8/UCS-2 sending +* split long GSM-7 and UCS-2 messages into concatenated SMS parts Usage: ---------------- diff --git a/pdu_lib/pdu.c b/pdu_lib/pdu.c index fa369a8..996f37b 100644 --- a/pdu_lib/pdu.c +++ b/pdu_lib/pdu.c @@ -11,6 +11,8 @@ #include "pdu.h" +#include +#include #include #include #include @@ -31,6 +33,9 @@ enum { SMS_MAX_7BIT_TEXT_LENGTH = 160, SMS_MAX_UCS2_TEXT_LENGTH = 70, + SMS_MAX_7BIT_PART_LENGTH = 153, + SMS_MAX_UCS2_PART_LENGTH = 67, + SMS_CONCAT_UDH_LENGTH = 6, }; // Swap decimal digits of a number (e.g. 12 -> 21). @@ -61,6 +66,11 @@ SwapDecimalNibble(const unsigned char x) int EncodePDUMessage(const char* sms_text, int sms_text_length, unsigned char* output_buffer, int buffer_size) { + if (sms_text_length < 0) + return -1; + if (sms_text_length == 0) + return 0; + // Check if output buffer is big enough. if ((sms_text_length * 7 + 7) / 8 > buffer_size) return -1; @@ -209,7 +219,7 @@ static const int latin1_to_gsm7bits[256] = { }; static int -AsciiToG7bit(const char* buffer, int buffer_length, char* output_buffer) +AsciiToG7bit(const char* buffer, int buffer_length, unsigned char* output_buffer) { int i, j, val; @@ -324,6 +334,118 @@ NormalizePhoneNumber(const char* phone_number, const char** digits, int* interna return length; } +static int +EncodeSmsText(const char* sms_text, unsigned char** encoded_text, int* use_ucs2) +{ + const size_t input_length = strlen(sms_text); + if (input_length > (size_t)(INT_MAX - 1) / 2) + return -1; + + *use_ucs2 = 0; + for (const unsigned char* p = (const unsigned char*)sms_text; *p; ++p) { + if (*p & 0x80) { + *use_ucs2 = 1; + break; + } + } + + const size_t buffer_size = input_length * 2 + 1; + *encoded_text = malloc(buffer_size); + if (!*encoded_text) + return -1; + + int length; + if (*use_ucs2) { + length = Utf8ToUcs2(sms_text, *encoded_text, buffer_size); + } else { + length = AsciiToG7bit(sms_text, input_length, *encoded_text); + } + if (length < 0) { + free(*encoded_text); + *encoded_text = NULL; + } + + return length; +} + +static int +GetTextPart(const unsigned char* encoded_text, int encoded_length, + int use_ucs2, int part_number, int* total_parts, + int* part_offset, int* part_length) +{ + if (part_number < 1) + return -1; + + const int single_part_limit = use_ucs2 ? + 2 * SMS_MAX_UCS2_TEXT_LENGTH : SMS_MAX_7BIT_TEXT_LENGTH; + if (encoded_length <= single_part_limit) { + *total_parts = 1; + if (part_number != 1) + return -1; + *part_offset = 0; + *part_length = encoded_length; + return 0; + } + + const int multipart_limit = use_ucs2 ? + 2 * SMS_MAX_UCS2_PART_LENGTH : SMS_MAX_7BIT_PART_LENGTH; + int offset = 0; + int count = 0; + int requested_offset = -1; + int requested_length = -1; + + while (offset < encoded_length) { + int length = encoded_length - offset; + if (length > multipart_limit) + length = multipart_limit; + /* Keep a GSM extension escape and its character in the same PDU. */ + if (!use_ucs2 && offset + length < encoded_length && + encoded_text[offset + length - 1] == GSM_7BITS_ESCAPE) + length--; + if (length <= 0 || ++count > 255) + return -1; + if (count == part_number) { + requested_offset = offset; + requested_length = length; + } + offset += length; + } + + *total_parts = count; + if (requested_offset < 0) + return -1; + *part_offset = requested_offset; + *part_length = requested_length; + return 0; +} + +static int +EncodeMultipartGsm7(const unsigned char* text, int text_length, + const unsigned char* udh, unsigned char* output_buffer, + int buffer_size) +{ + const int header_septets = (SMS_CONCAT_UDH_LENGTH * 8 + 6) / 7; + const int user_data_length = header_septets + text_length; + const int output_length = (user_data_length * 7 + 7) / 8; + if (output_length > buffer_size) + return -1; + + memset(output_buffer, 0, output_length); + memcpy(output_buffer, udh, SMS_CONCAT_UDH_LENGTH); + + for (int i = 0; i < text_length; ++i) { + const int bit_position = header_septets * 7 + i * 7; + const int byte_position = bit_position / 8; + const int shift = bit_position % 8; + output_buffer[byte_position] |= (text[i] & BITMASK_7BITS) << shift; + if (shift > 1) + output_buffer[byte_position + 1] |= + (text[i] & BITMASK_7BITS) >> (8 - shift); + } + + return output_length; +} + // Decode a digit based phone number for SMS based format. static int DecodePhoneNumber(const unsigned char* buffer, int phone_number_length, char* output_phone_number) @@ -339,14 +461,33 @@ DecodePhoneNumber(const unsigned char* buffer, int phone_number_length, char* ou return phone_number_length; } -// Encode a SMS message to PDU +// Encode one part of an SMS message to PDU. int -pdu_encode(const char* service_center_number, const char* phone_number, const char* sms_text, - unsigned char* output_buffer, int buffer_size) +pdu_encode_multipart(const char* service_center_number, + const char* phone_number, const char* sms_text, + unsigned char reference_number, int part_number, + int* total_parts, unsigned char* output_buffer, + int buffer_size) { - if (!phone_number || !sms_text || !output_buffer || buffer_size < 2) + if (!phone_number || !sms_text || !total_parts || !output_buffer || + buffer_size < 2) + return -1; + + unsigned char* encoded_text = NULL; + int use_ucs2; + const int encoded_length = EncodeSmsText(sms_text, &encoded_text, &use_ucs2); + if (encoded_length < 0) return -1; + int part_offset; + int part_length; + if (GetTextPart(encoded_text, encoded_length, use_ucs2, part_number, + total_parts, &part_offset, &part_length) < 0) { + free(encoded_text); + return -1; + } + const int multipart = *total_parts > 1; + int output_buffer_length = 0; // 1. Set SMS center number. @@ -356,21 +497,21 @@ pdu_encode(const char* service_center_number, const char* phone_number, const ch int service_center_international; if (NormalizePhoneNumber(service_center_number, &service_center_digits, &service_center_international) < 0) - return -1; + goto error; output_buffer[1] = TYPE_OF_ADDRESS_INTERNATIONAL_PHONE; length = EncodePhoneNumber(service_center_digits, output_buffer + 2, buffer_size - 2); if (length < 0 || length >= 254) - return -1; + goto error; length++; // Add type of address. } output_buffer[0] = length; output_buffer_length = length + 1; if (output_buffer_length + 4 > buffer_size) - return -1; // Check if it has space for four more bytes. + goto error; // 2. Set type of message. - output_buffer[output_buffer_length++] = SMS_SUBMIT; + output_buffer[output_buffer_length++] = SMS_SUBMIT | (multipart ? 0x40 : 0); output_buffer[output_buffer_length++] = 0x00; // Message reference. // 3. Set phone number. @@ -380,7 +521,7 @@ pdu_encode(const char* service_center_number, const char* phone_number, const ch &phone_number_digits, &phone_number_international); if (phone_number_length < 0) - return -1; + goto error; output_buffer[output_buffer_length] = phone_number_length; if (!phone_number_international && phone_number_length < 6) { @@ -393,20 +534,10 @@ pdu_encode(const char* service_center_number, const char* phone_number, const ch output_buffer + output_buffer_length + 2, buffer_size - output_buffer_length - 2); if (length < 0) - return -1; + goto error; output_buffer_length += length + 2; if (output_buffer_length + 4 > buffer_size) - return -1; // Check if it has space for four more bytes. - - - /* Use UCS-2 whenever the UTF-8 input is not pure ASCII. */ - int use_ucs2 = 0; - for (const unsigned char* p = (const unsigned char*)sms_text; *p; ++p) { - if (*p & 0x80) { - use_ucs2 = 1; - break; - } - } + goto error; // 4. Protocol identifiers. output_buffer[output_buffer_length++] = 0x00; // TP-PID: Protocol identifier. @@ -414,33 +545,64 @@ pdu_encode(const char* service_center_number, const char* phone_number, const ch output_buffer[output_buffer_length++] = 0xB0; // TP-VP: Validity: 10 days // 5. SMS message. - int sms_text_length = strlen(sms_text); + unsigned char udh[SMS_CONCAT_UDH_LENGTH] = { + 0x05, 0x00, 0x03, reference_number, *total_parts, part_number, + }; + int user_data_header_length = multipart ? SMS_CONCAT_UDH_LENGTH : 0; if (use_ucs2) { - unsigned char sms_text_ucs2[2 * SMS_MAX_UCS2_TEXT_LENGTH]; - sms_text_length = Utf8ToUcs2(sms_text, sms_text_ucs2, - sizeof(sms_text_ucs2)); - if (sms_text_length < 0 || - output_buffer_length + 1 + sms_text_length > buffer_size) - return -1; - output_buffer[output_buffer_length++] = sms_text_length; - memcpy(output_buffer + output_buffer_length, sms_text_ucs2, - sms_text_length); - return output_buffer_length + sms_text_length; + const int user_data_length = user_data_header_length + part_length; + if (output_buffer_length + 1 + user_data_length > buffer_size) + goto error; + output_buffer[output_buffer_length++] = user_data_length; + if (multipart) { + memcpy(output_buffer + output_buffer_length, udh, + sizeof(udh)); + output_buffer_length += sizeof(udh); + } + memcpy(output_buffer + output_buffer_length, + encoded_text + part_offset, part_length); + output_buffer_length += part_length; + free(encoded_text); + return output_buffer_length; } - char sms_text_7bit[2*SMS_MAX_7BIT_TEXT_LENGTH]; - sms_text_length = AsciiToG7bit(sms_text, sms_text_length, sms_text_7bit); - if (sms_text_length > SMS_MAX_7BIT_TEXT_LENGTH) - return -1; - output_buffer[output_buffer_length++] = sms_text_length; - length = EncodePDUMessage(sms_text_7bit, sms_text_length, - output_buffer + output_buffer_length, - buffer_size - output_buffer_length); + if (multipart) { + const int header_septets = (SMS_CONCAT_UDH_LENGTH * 8 + 6) / 7; + output_buffer[output_buffer_length++] = header_septets + part_length; + length = EncodeMultipartGsm7(encoded_text + part_offset, part_length, + udh, + output_buffer + output_buffer_length, + buffer_size - output_buffer_length); + } else { + output_buffer[output_buffer_length++] = part_length; + length = EncodePDUMessage((const char*)encoded_text, part_length, + output_buffer + output_buffer_length, + buffer_size - output_buffer_length); + } if (length < 0) - return -1; + goto error; output_buffer_length += length; + free(encoded_text); return output_buffer_length; + +error: + free(encoded_text); + return -1; +} + +// Encode a single-part SMS message to PDU. +int +pdu_encode(const char* service_center_number, const char* phone_number, + const char* sms_text, unsigned char* output_buffer, int buffer_size) +{ + int total_parts = 0; + const int length = pdu_encode_multipart(service_center_number, phone_number, + sms_text, 0, 1, &total_parts, + output_buffer, buffer_size); + if (length < 0 || total_parts > 1) + return -1; + return length; } int pdu_decode(const unsigned char* buffer, int buffer_length, diff --git a/pdu_lib/pdu.h b/pdu_lib/pdu.h index 734a393..bc94341 100644 --- a/pdu_lib/pdu.h +++ b/pdu_lib/pdu.h @@ -21,6 +21,16 @@ enum { SMS_MAX_PDU_LENGTH = 256 }; int pdu_encode(const char* service_center_number, const char* phone_number, const char* text, unsigned char* pdu, int pdu_size); +/* + * Encode one part of an SMS message. The first call should use part_number 1; + * total_parts is then set to the number of PDUs required. Calls for subsequent + * parts must use the same reference_number. + */ +int pdu_encode_multipart(const char* service_center_number, + const char* phone_number, const char* text, + unsigned char reference_number, int part_number, + int* total_parts, unsigned char* pdu, int pdu_size); + /* * Decode an SMS message. Output the decoded message into the sms text buffer. * Returns the length of the SMS dencoded message or a negative number in diff --git a/pdu_lib/pdu_test.c b/pdu_lib/pdu_test.c index 6078999..ef4eb51 100644 --- a/pdu_lib/pdu_test.c +++ b/pdu_lib/pdu_test.c @@ -1,6 +1,7 @@ #include "pdu.h" #include +#include #include static int expect_pdu(const char* name, const char* number, const char* text, @@ -17,6 +18,159 @@ static int expect_pdu(const char* name, const char* number, const char* text, return 0; } +static int gsm7_at(const unsigned char* user_data, int septet) +{ + const int bit_position = 49 + septet * 7; + const int byte_position = bit_position / 8; + const int shift = bit_position % 8; + unsigned int value = user_data[byte_position] >> shift; + if (shift > 1) + value |= user_data[byte_position + 1] << (8 - shift); + return value & 0x7F; +} + +static int test_ucs2_multipart(void) +{ + unsigned char pdu[SMS_MAX_PDU_LENGTH]; + char text[71 * 2 + 1]; + int failed = 0; + int total_parts; + + for (int i = 0; i < 71; ++i) { + text[2 * i] = (char)0xC2; + text[2 * i + 1] = (char)0xA3; + } + text[sizeof(text) - 1] = '\0'; + + int length = pdu_encode_multipart("", "+1234567890", text, 0x42, 1, + &total_parts, pdu, sizeof(pdu)); + static const unsigned char first_header[] = {0x05, 0x00, 0x03, 0x42, 0x02, 0x01}; + if (length != 154 || total_parts != 2 || pdu[1] != 0x51 || + pdu[11] != 0x08 || pdu[13] != 140 || + memcmp(pdu + 14, first_header, sizeof(first_header)) != 0) { + fprintf(stderr, "invalid first UCS-2 multipart PDU\n"); + failed = 1; + } + for (int i = 0; i < 67; ++i) { + if (pdu[20 + 2 * i] != 0x00 || pdu[21 + 2 * i] != 0xA3) { + fprintf(stderr, "invalid UCS-2 payload in first part\n"); + failed = 1; + break; + } + } + + length = pdu_encode_multipart("", "+1234567890", text, 0x42, 2, + &total_parts, pdu, sizeof(pdu)); + static const unsigned char second_data[] = { + 0x05, 0x00, 0x03, 0x42, 0x02, 0x02, + 0x00, 0xA3, 0x00, 0xA3, 0x00, 0xA3, 0x00, 0xA3, + }; + if (length != 28 || pdu[13] != sizeof(second_data) || + memcmp(pdu + 14, second_data, sizeof(second_data)) != 0) { + fprintf(stderr, "invalid second UCS-2 multipart PDU\n"); + failed = 1; + } + + return failed; +} + +static int test_gsm7_multipart(void) +{ + unsigned char pdu[SMS_MAX_PDU_LENGTH]; + char text[162]; + int failed = 0; + int total_parts; + + memset(text, 'A', sizeof(text) - 1); + text[sizeof(text) - 1] = '\0'; + int length = pdu_encode_multipart("", "+1234567890", text, 0x24, 1, + &total_parts, pdu, sizeof(pdu)); + static const unsigned char first_header[] = {0x05, 0x00, 0x03, 0x24, 0x02, 0x01}; + if (length != 154 || total_parts != 2 || pdu[1] != 0x51 || + pdu[11] != 0x00 || pdu[13] != 160 || + memcmp(pdu + 14, first_header, sizeof(first_header)) != 0) { + fprintf(stderr, "invalid first GSM-7 multipart PDU\n"); + failed = 1; + } + for (int i = 0; i < 153; ++i) { + if (gsm7_at(pdu + 14, i) != 'A') { + fprintf(stderr, "invalid packed GSM-7 payload in first part\n"); + failed = 1; + break; + } + } + + length = pdu_encode_multipart("", "+1234567890", text, 0x24, 2, + &total_parts, pdu, sizeof(pdu)); + if (length != 28 || pdu[13] != 15 || pdu[19] != 0x02) { + fprintf(stderr, "invalid second GSM-7 multipart PDU\n"); + failed = 1; + } + for (int i = 0; i < 8; ++i) { + if (gsm7_at(pdu + 14, i) != 'A') { + fprintf(stderr, "invalid packed GSM-7 payload in second part\n"); + failed = 1; + break; + } + } + + return failed; +} + +static int test_gsm7_escape_boundary(void) +{ + unsigned char pdu[SMS_MAX_PDU_LENGTH]; + char text[161]; + int total_parts; + + memset(text, 'A', 152); + text[152] = '{'; + memset(text + 153, 'B', 7); + text[160] = '\0'; + + int length = pdu_encode_multipart("", "+1234567890", text, 0x55, 1, + &total_parts, pdu, sizeof(pdu)); + if (length < 0 || total_parts != 2 || pdu[13] != 159) + return 1; + length = pdu_encode_multipart("", "+1234567890", text, 0x55, 2, + &total_parts, pdu, sizeof(pdu)); + if (length < 0 || gsm7_at(pdu + 14, 0) != 0x1B || + gsm7_at(pdu + 14, 1) != 0x28) { + fprintf(stderr, "GSM-7 extension pair was split between parts\n"); + return 1; + } + return 0; +} + +static int test_part_count_limit(void) +{ + const size_t max_length = 153 * 255; + char* text = malloc(max_length + 2); + unsigned char pdu[SMS_MAX_PDU_LENGTH]; + int total_parts; + int failed = 0; + + if (!text) + return 1; + memset(text, 'A', max_length + 1); + text[max_length] = '\0'; + if (pdu_encode_multipart("", "+1234567890", text, 0x66, 255, + &total_parts, pdu, sizeof(pdu)) < 0 || + total_parts != 255) { + fprintf(stderr, "255-part message was rejected\n"); + failed = 1; + } + text[max_length] = 'A'; + text[max_length + 1] = '\0'; + if (pdu_encode_multipart("", "+1234567890", text, 0x66, 1, + &total_parts, pdu, sizeof(pdu)) >= 0) { + fprintf(stderr, "256-part message was accepted\n"); + failed = 1; + } + free(text); + return failed; +} + int main(void) { static const unsigned char ascii_pdu[] = { @@ -40,6 +194,10 @@ int main(void) failed |= expect_pdu("UCS-2", "+79055200353", "Дима, пишет тебе NL678", ucs2_pdu, sizeof(ucs2_pdu)); + failed |= test_ucs2_multipart(); + failed |= test_gsm7_multipart(); + failed |= test_gsm7_escape_boundary(); + failed |= test_part_count_limit(); if (pdu_encode("", "+12x34", "test", pdu, sizeof(pdu)) >= 0) { fprintf(stderr, "invalid destination was accepted\n"); @@ -59,6 +217,13 @@ int main(void) fprintf(stderr, "overlong UCS-2 message was accepted\n"); failed = 1; } + int total_parts; + if (pdu_encode_multipart("", "+1234", long_ucs2, 0x11, 1, + &total_parts, pdu, sizeof(pdu)) < 0 || + total_parts != 2) { + fprintf(stderr, "multipart UCS-2 message was rejected\n"); + failed = 1; + } return failed; } diff --git a/sms_main.c b/sms_main.c index d8155a5..d0c96c9 100644 --- a/sms_main.c +++ b/sms_main.c @@ -248,21 +248,18 @@ int main(int argc, char* argv[]) char buf[1024]; if (!strcmp("send", argv[0])) { - int pdu_len = pdu_encode("", argv[1], argv[2], pdu, sizeof(pdu)); + const unsigned char reference_number = + (unsigned char)(time(NULL) ^ getpid()); + int total_parts = 0; + int pdu_len = pdu_encode_multipart("", argv[1], argv[2], + reference_number, 1, &total_parts, + pdu, sizeof(pdu)); if (pdu_len < 0) { fprintf(stderr, "error encoding to PDU: %s \"%s\"\n", argv[1], argv[2]); return 1; } - const int pdu_len_except_smsc = pdu_len - 1 - pdu[0]; - snprintf(cmdstr, sizeof(cmdstr), "AT+CMGS=%d\r\n", pdu_len_except_smsc); - - int i; - for (i = 0; i < pdu_len; ++i) - sprintf(pdustr+2*i, "%02X", pdu[i]); - sprintf(pdustr+2*i, "%c\r\n", 0x1A); // End PDU mode with Ctrl-Z. - alarm(30); if (fputs("AT+CMGF=0\r\n", pf) == EOF) return 1; @@ -281,33 +278,62 @@ int main(int argc, char* argv[]) fprintf(stderr, "no response while enabling PDU mode\n"); return 1; } - if (fputs(cmdstr, pf) == EOF) - return 1; - sleep(1); - if (fputs(pdustr, pf) == EOF) - return 1; + for (int part_number = 1; part_number <= total_parts; ++part_number) { + if (part_number > 1) { + int encoded_total_parts; + pdu_len = pdu_encode_multipart("", argv[1], argv[2], + reference_number, part_number, + &encoded_total_parts, pdu, + sizeof(pdu)); + if (pdu_len < 0 || encoded_total_parts != total_parts) { + fprintf(stderr, "error encoding SMS part %d/%d\n", + part_number, total_parts); + return 1; + } + } - errno = 0; + const int pdu_len_except_smsc = pdu_len - 1 - pdu[0]; + snprintf(cmdstr, sizeof(cmdstr), "AT+CMGS=%d\r\n", + pdu_len_except_smsc); + int i; + for (i = 0; i < pdu_len; ++i) + sprintf(pdustr + 2 * i, "%02X", pdu[i]); + sprintf(pdustr + 2 * i, "%c\r\n", 0x1A); - while(fgets(buf, sizeof(buf), pfi)) - { - if(starts_with("+CMGS:", buf)) - { - alarm(0); - printf("sms sent successfully: %s", buf + 7); - return 0; - } else if(starts_with("+CMS ERROR:", buf)) - { - fprintf(stderr,"sms not sent, code: %s\n", buf + 11); + alarm(30); + if (fputs(cmdstr, pf) == EOF) return 1; - } else if(starts_with("ERROR", buf)) - { - fprintf(stderr,"sms not sent, command error\n"); + sleep(1); + if (fputs(pdustr, pf) == EOF) + return 1; + + errno = 0; + int cmgs_received = 0; + while(fgets(buf, sizeof(buf), pfi)) { + if(starts_with("+CMGS:", buf)) { + cmgs_received = 1; + if (total_parts == 1) + printf("sms sent successfully: %s", buf + 7); + else + printf("sms part %d/%d sent successfully: %s", + part_number, total_parts, buf + 7); + } else if(starts_with("+CMS ERROR:", buf)) { + fprintf(stderr,"sms not sent, code: %s\n", buf + 11); + return 1; + } else if(starts_with("ERROR", buf)) { + fprintf(stderr,"sms not sent, command error\n"); + return 1; + } else if(starts_with("OK", buf) && cmgs_received) { + break; + } + } + if (!cmgs_received) { + fprintf(stderr, "reading port: %s\n", strerror(errno)); return 1; } } - fprintf(stderr, "reading port: %s\n", strerror(errno)); - return 1; + alarm(0); + return 0; } if (!strcmp("recv", argv[0]))