| /* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
| /* |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details: |
| * |
| * Copyright (C) 2011 - 2012 Red Hat, Inc. |
| * Copyright (C) 2012 Google, Inc. |
| */ |
| |
| #include <ctype.h> |
| #include <string.h> |
| |
| #include <glib.h> |
| |
| #include <ModemManager.h> |
| #define _LIBMM_INSIDE_MM |
| #include <libmm-glib.h> |
| |
| #include "mm-common-helpers.h" |
| #include "mm-helper-enums-types.h" |
| #include "mm-sms-part-3gpp.h" |
| #include "mm-charsets.h" |
| #include "mm-log.h" |
| |
| #define PDU_SIZE 200 |
| |
| #define SMS_TP_MTI_MASK 0x03 |
| #define SMS_TP_MTI_SMS_DELIVER 0x00 |
| #define SMS_TP_MTI_SMS_SUBMIT 0x01 |
| #define SMS_TP_MTI_SMS_STATUS_REPORT 0x02 |
| |
| #define SMS_NUMBER_TYPE_MASK 0x70 |
| #define SMS_NUMBER_TYPE_UNKNOWN 0x00 |
| #define SMS_NUMBER_TYPE_INTL 0x10 |
| #define SMS_NUMBER_TYPE_ALPHA 0x50 |
| |
| #define SMS_NUMBER_PLAN_MASK 0x0f |
| #define SMS_NUMBER_PLAN_TELEPHONE 0x01 |
| |
| #define SMS_TP_MMS 0x04 |
| #define SMS_TP_SRI 0x20 |
| #define SMS_TP_UDHI 0x40 |
| #define SMS_TP_RP 0x80 |
| |
| #define SMS_DCS_CODING_MASK 0xec |
| #define SMS_DCS_CODING_DEFAULT 0x00 |
| #define SMS_DCS_CODING_8BIT 0x04 |
| #define SMS_DCS_CODING_UCS2 0x08 |
| |
| #define SMS_DCS_CLASS_VALID 0x10 |
| #define SMS_DCS_CLASS_MASK 0x03 |
| |
| #define SMS_TIMESTAMP_LEN 7 |
| #define SMS_MIN_PDU_LEN (7 + SMS_TIMESTAMP_LEN) |
| |
| static char sms_bcd_chars[] = "0123456789*#abc\0\0"; |
| |
| static void |
| sms_semi_octets_to_bcd_string (char *dest, const guint8 *octets, int num_octets) |
| { |
| int i; |
| |
| for (i = 0 ; i < num_octets; i++) { |
| *dest++ = sms_bcd_chars[octets[i] & 0xf]; |
| *dest++ = sms_bcd_chars[(octets[i] >> 4) & 0xf]; |
| } |
| *dest++ = '\0'; |
| } |
| |
| static gboolean |
| char_to_bcd (char in, guint8 *out) |
| { |
| guint32 z; |
| |
| if (isdigit (in)) { |
| *out = in - 0x30; |
| return TRUE; |
| } |
| |
| for (z = 10; z < 16; z++) { |
| if (in == sms_bcd_chars[z]) { |
| *out = z; |
| return TRUE; |
| } |
| } |
| return FALSE; |
| } |
| |
| static gsize |
| sms_string_to_bcd_semi_octets (guint8 *buf, gsize buflen, const char *string) |
| { |
| guint i; |
| guint8 bcd; |
| gsize addrlen, slen; |
| |
| addrlen = slen = strlen (string); |
| if (addrlen % 2) |
| addrlen++; |
| g_return_val_if_fail (buflen >= addrlen, 0); |
| |
| for (i = 0; i < addrlen; i += 2) { |
| if (!char_to_bcd (string[i], &bcd)) |
| return 0; |
| buf[i / 2] = bcd & 0xF; |
| |
| if (i >= slen - 1) { |
| /* PDU address gets padded with 0xF if string is odd length */ |
| bcd = 0xF; |
| } else if (!char_to_bcd (string[i + 1], &bcd)) |
| return 0; |
| buf[i / 2] |= bcd << 4; |
| } |
| return addrlen / 2; |
| } |
| |
| /* len is in semi-octets */ |
| static gchar * |
| sms_decode_address (const guint8 *address, |
| gint len_digits, |
| GError **error) |
| { |
| guint8 addrtype, addrplan; |
| gchar *utf8; |
| |
| addrtype = address[0] & SMS_NUMBER_TYPE_MASK; |
| addrplan = address[0] & SMS_NUMBER_PLAN_MASK; |
| address++; |
| |
| if (addrtype == SMS_NUMBER_TYPE_ALPHA) { |
| g_autoptr(GByteArray) unpacked_array = NULL; |
| guint8 *unpacked = NULL; |
| guint32 unpacked_len; |
| |
| unpacked = mm_charset_gsm_unpack (address, (len_digits * 4) / 7, 0, &unpacked_len); |
| unpacked_array = g_byte_array_new_take (unpacked, unpacked_len); |
| utf8 = mm_modem_charset_bytearray_to_utf8 (unpacked_array, MM_MODEM_CHARSET_GSM, FALSE, error); |
| } else if (addrtype == SMS_NUMBER_TYPE_INTL && |
| addrplan == SMS_NUMBER_PLAN_TELEPHONE) { |
| /* International telphone number, format as "+1234567890" */ |
| utf8 = g_malloc (len_digits + 3); /* '+' + digits + possible trailing 0xf + NUL */ |
| utf8[0] = '+'; |
| sms_semi_octets_to_bcd_string (utf8 + 1, address, (len_digits + 1) / 2); |
| } else { |
| /* |
| * All non-alphanumeric types and plans are just digits, but |
| * don't apply any special formatting if we don't know the |
| * format. |
| */ |
| utf8 = g_malloc (len_digits + 2); /* digits + possible trailing 0xf + NUL */ |
| sms_semi_octets_to_bcd_string (utf8, address, (len_digits + 1) / 2); |
| } |
| |
| return utf8; |
| } |
| |
| static gchar * |
| sms_decode_timestamp (const guint8 *timestamp, |
| GError **error) |
| { |
| /* ISO8601 format: YYYY-MM-DDTHH:MM:SS+HHMM */ |
| guint year, month, day, hour, minute, second; |
| gint quarters, offset_minutes; |
| |
| year = 2000 + ((timestamp[0] & 0xf) * 10) + ((timestamp[0] >> 4) & 0xf); |
| month = ((timestamp[1] & 0xf) * 10) + ((timestamp[1] >> 4) & 0xf); |
| day = ((timestamp[2] & 0xf) * 10) + ((timestamp[2] >> 4) & 0xf); |
| hour = ((timestamp[3] & 0xf) * 10) + ((timestamp[3] >> 4) & 0xf); |
| minute = ((timestamp[4] & 0xf) * 10) + ((timestamp[4] >> 4) & 0xf); |
| second = ((timestamp[5] & 0xf) * 10) + ((timestamp[5] >> 4) & 0xf); |
| quarters = ((timestamp[6] & 0x7) * 10) + ((timestamp[6] >> 4) & 0xf); |
| offset_minutes = quarters * 15; |
| if (timestamp[6] & 0x08) |
| offset_minutes = -1 * offset_minutes; |
| |
| return mm_new_iso8601_time (year, month, day, hour, |
| minute, second, TRUE, offset_minutes, error); |
| } |
| |
| static MMSmsEncoding |
| sms_encoding_type (int dcs) |
| { |
| MMSmsEncoding scheme = MM_SMS_ENCODING_UNKNOWN; |
| |
| switch ((dcs >> 4) & 0xf) { |
| /* General data coding group */ |
| case 0: case 1: |
| case 2: case 3: |
| switch (dcs & 0x0c) { |
| case 0x08: |
| scheme = MM_SMS_ENCODING_UCS2; |
| break; |
| case 0x00: |
| /* reserved - spec says to treat it as default alphabet */ |
| /* Fall through */ |
| case 0x0c: |
| scheme = MM_SMS_ENCODING_GSM7; |
| break; |
| case 0x04: |
| scheme = MM_SMS_ENCODING_8BIT; |
| break; |
| default: |
| g_assert_not_reached (); |
| } |
| break; |
| |
| /* Message waiting group (default alphabet) */ |
| case 0xc: |
| case 0xd: |
| scheme = MM_SMS_ENCODING_GSM7; |
| break; |
| |
| /* Message waiting group (UCS2 alphabet) */ |
| case 0xe: |
| scheme = MM_SMS_ENCODING_UCS2; |
| break; |
| |
| /* Data coding/message class group */ |
| case 0xf: |
| switch (dcs & 0x04) { |
| case 0x00: |
| scheme = MM_SMS_ENCODING_GSM7; |
| break; |
| case 0x04: |
| scheme = MM_SMS_ENCODING_8BIT; |
| break; |
| default: |
| g_assert_not_reached (); |
| } |
| break; |
| |
| /* Reserved coding group values - spec says to treat it as default alphabet */ |
| default: |
| scheme = MM_SMS_ENCODING_GSM7; |
| break; |
| } |
| |
| return scheme; |
| } |
| |
| gchar * |
| mm_sms_decode_text (const guint8 *text, |
| int len, |
| MMSmsEncoding encoding, |
| int bit_offset, |
| gpointer log_object, |
| GError **error) |
| { |
| if (!text || len == 0) { |
| g_set_error (error, |
| MM_CORE_ERROR, |
| MM_CORE_ERROR_FAILED, |
| "Skipping SMS text: SMS text has no elements to decode"); |
| return NULL; |
| } |
| |
| if (encoding == MM_SMS_ENCODING_GSM7) { |
| g_autoptr(GByteArray) unpacked_array = NULL; |
| guint8 *unpacked = NULL; |
| guint32 unpacked_len; |
| gchar *utf8; |
| |
| unpacked = mm_charset_gsm_unpack ((const guint8 *) text, len, bit_offset, &unpacked_len); |
| unpacked_array = g_byte_array_new_take (unpacked, unpacked_len); |
| utf8 = mm_modem_charset_bytearray_to_utf8 (unpacked_array, MM_MODEM_CHARSET_GSM, FALSE, error); |
| if (utf8) |
| mm_obj_dbg (log_object, "converted SMS part text from GSM-7 to UTF-8: %s", utf8); |
| return utf8; |
| } |
| |
| /* Always assume UTF-16 instead of UCS-2! */ |
| if (encoding == MM_SMS_ENCODING_UCS2) { |
| g_autoptr(GByteArray) bytearray = NULL; |
| gchar *utf8; |
| |
| bytearray = g_byte_array_append (g_byte_array_sized_new (len), (const guint8 *)text, len); |
| utf8 = mm_modem_charset_bytearray_to_utf8 (bytearray, MM_MODEM_CHARSET_UTF16, FALSE, error); |
| if (utf8) |
| mm_obj_dbg (log_object, "converted SMS part text from UTF-16BE to UTF-8: %s", utf8); |
| return utf8; |
| } |
| |
| g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, |
| "Couldn't convert SMS part contents from %s to UTF-8", |
| mm_sms_encoding_get_string (encoding)); |
| return NULL; |
| } |
| |
| static guint |
| relative_to_validity (guint8 relative) |
| { |
| if (relative <= 143) |
| return (relative + 1) * 5; |
| |
| if (relative <= 167) |
| return 720 + (relative - 143) * 30; |
| |
| return (relative - 166) * 1440; |
| } |
| |
| static guint8 |
| validity_to_relative (guint validity) |
| { |
| if (validity == 0) |
| return 167; /* 24 hours */ |
| |
| if (validity <= 720) { |
| /* 5 minute units up to 12 hours */ |
| if (validity % 5) |
| validity += 5; |
| return (validity / 5) - 1; |
| } |
| |
| if (validity > 720 && validity <= 1440) { |
| /* 12 hours + 30 minute units up to 1 day */ |
| if (validity % 30) |
| validity += 30; /* round up to next 30 minutes */ |
| validity = MIN (validity, 1440); |
| return 143 + ((validity - 720) / 30); |
| } |
| |
| if (validity > 1440 && validity <= 43200) { |
| /* 2 days up to 1 month */ |
| if (validity % 1440) |
| validity += 1440; /* round up to next day */ |
| validity = MIN (validity, 43200); |
| return 167 + ((validity - 1440) / 1440); |
| } |
| |
| /* 43200 = 30 days in minutes |
| * 10080 = 7 days in minutes |
| * 635040 = 63 weeks in minutes |
| * 40320 = 4 weeks in minutes |
| */ |
| if (validity > 43200 && validity <= 635040) { |
| /* 5 weeks up to 63 weeks */ |
| if (validity % 10080) |
| validity += 10080; /* round up to next week */ |
| validity = MIN (validity, 635040); |
| return 196 + ((validity - 40320) / 10080); |
| } |
| |
| return 255; /* 63 weeks */ |
| } |
| |
| MMSmsPart * |
| mm_sms_part_3gpp_new_from_pdu (guint index, |
| const gchar *hexpdu, |
| gpointer log_object, |
| GError **error) |
| { |
| g_autofree guint8 *pdu = NULL; |
| gsize pdu_len; |
| |
| /* Convert PDU from hex to binary */ |
| pdu = mm_utils_hexstr2bin (hexpdu, -1, &pdu_len, error); |
| if (!pdu) { |
| g_prefix_error (error, "Couldn't convert 3GPP PDU from hex to binary: "); |
| return NULL; |
| } |
| |
| return mm_sms_part_3gpp_new_from_binary_pdu (index, pdu, pdu_len, log_object, FALSE, error); |
| } |
| |
| MMSmsPart * |
| mm_sms_part_3gpp_new_from_binary_pdu (guint index, |
| const guint8 *pdu, |
| gsize pdu_len, |
| gpointer log_object, |
| gboolean transfer_route, |
| GError **error) |
| { |
| MMSmsPart *sms_part; |
| guint8 pdu_type; |
| guint offset; |
| guint smsc_addr_size_bytes; |
| guint tp_addr_size_digits; |
| guint tp_addr_size_bytes; |
| guint8 validity_format = 0; |
| gboolean has_udh = FALSE; |
| /* The following offsets are OPTIONAL, as STATUS REPORTs may not have |
| * them; we use '0' to indicate their absence */ |
| guint tp_pid_offset = 0; |
| guint tp_dcs_offset = 0; |
| guint tp_user_data_len_offset = 0; |
| MMSmsEncoding user_data_encoding = MM_SMS_ENCODING_UNKNOWN; |
| gchar *address; |
| |
| /* Create the new MMSmsPart */ |
| sms_part = mm_sms_part_new (index, MM_SMS_PDU_TYPE_UNKNOWN); |
| |
| if (index != SMS_PART_INVALID_INDEX) |
| mm_obj_dbg (log_object, "parsing PDU (%u)...", index); |
| else |
| mm_obj_dbg (log_object, "parsing PDU..."); |
| |
| #define PDU_SIZE_CHECK(required_size, check_descr_str) \ |
| if (pdu_len < required_size) { \ |
| g_set_error (error, \ |
| MM_CORE_ERROR, \ |
| MM_CORE_ERROR_FAILED, \ |
| "PDU too short, %s: %" G_GSIZE_FORMAT " < %u", \ |
| check_descr_str, \ |
| pdu_len, \ |
| required_size); \ |
| mm_sms_part_free (sms_part); \ |
| return NULL; \ |
| } |
| |
| offset = 0; |
| |
| if (!transfer_route) { |
| /* ---------------------------------------------------------------------- */ |
| /* SMSC, in address format, precedes the TPDU |
| * First byte represents the number of BYTES for the address value */ |
| PDU_SIZE_CHECK (1, "cannot read SMSC address length"); |
| smsc_addr_size_bytes = pdu[offset++]; |
| if (smsc_addr_size_bytes > 0) { |
| PDU_SIZE_CHECK (offset + smsc_addr_size_bytes, "cannot read SMSC address"); |
| /* SMSC may not be given in DELIVER PDUs */ |
| address = sms_decode_address (&pdu[1], 2 * (smsc_addr_size_bytes - 1), error); |
| if (!address) { |
| g_prefix_error (error, "Couldn't read SMSC address: "); |
| mm_sms_part_free (sms_part); |
| return NULL; |
| } |
| mm_sms_part_take_smsc (sms_part, g_steal_pointer (&address)); |
| mm_obj_dbg (log_object, " SMSC address parsed: '%s'", mm_sms_part_get_smsc (sms_part)); |
| offset += smsc_addr_size_bytes; |
| } else |
| mm_obj_dbg (log_object, " no SMSC address given"); |
| } else |
| mm_obj_dbg (log_object, " This is a transfer-route message"); |
| |
| /* ---------------------------------------------------------------------- */ |
| /* TP-MTI (1 byte) */ |
| PDU_SIZE_CHECK (offset + 1, "cannot read TP-MTI"); |
| |
| pdu_type = (pdu[offset] & SMS_TP_MTI_MASK); |
| switch (pdu_type) { |
| case SMS_TP_MTI_SMS_DELIVER: |
| mm_obj_dbg (log_object, " deliver type PDU detected"); |
| mm_sms_part_set_pdu_type (sms_part, MM_SMS_PDU_TYPE_DELIVER); |
| break; |
| case SMS_TP_MTI_SMS_SUBMIT: |
| mm_obj_dbg (log_object, " submit type PDU detected"); |
| mm_sms_part_set_pdu_type (sms_part, MM_SMS_PDU_TYPE_SUBMIT); |
| break; |
| case SMS_TP_MTI_SMS_STATUS_REPORT: |
| mm_obj_dbg (log_object, " status report type PDU detected"); |
| mm_sms_part_set_pdu_type (sms_part, MM_SMS_PDU_TYPE_STATUS_REPORT); |
| break; |
| default: |
| mm_sms_part_free (sms_part); |
| g_set_error (error, |
| MM_CORE_ERROR, |
| MM_CORE_ERROR_FAILED, |
| "Unhandled message type: 0x%02x", |
| pdu_type); |
| return NULL; |
| } |
| |
| /* Delivery report was requested? */ |
| if (pdu[offset] & 0x20) |
| mm_sms_part_set_delivery_report_request (sms_part, TRUE); |
| |
| /* PDU with validity? (only in SUBMIT PDUs) */ |
| if (pdu_type == SMS_TP_MTI_SMS_SUBMIT) |
| validity_format = pdu[offset] & 0x18; |
| |
| /* PDU with user data header? */ |
| if (pdu[offset] & 0x40) |
| has_udh = TRUE; |
| |
| offset++; |
| |
| /* ---------------------------------------------------------------------- */ |
| /* TP-MR (1 byte, in STATUS_REPORT and SUBMIT PDUs */ |
| if (pdu_type == SMS_TP_MTI_SMS_STATUS_REPORT || |
| pdu_type == SMS_TP_MTI_SMS_SUBMIT) { |
| PDU_SIZE_CHECK (offset + 1, "cannot read message reference"); |
| |
| mm_obj_dbg (log_object, " message reference: %u", (guint)pdu[offset]); |
| mm_sms_part_set_message_reference (sms_part, pdu[offset]); |
| offset++; |
| } |
| |
| /* ---------------------------------------------------------------------- */ |
| /* TP-DA or TP-OA or TP-RA |
| * First byte represents the number of DIGITS in the number. |
| * Round the sender address length up to an even number of |
| * semi-octets, and thus an integral number of octets. |
| */ |
| PDU_SIZE_CHECK (offset + 1, "cannot read number of digits in number"); |
| tp_addr_size_digits = pdu[offset++]; |
| tp_addr_size_bytes = (tp_addr_size_digits + 1) >> 1; |
| mm_obj_dbg (log_object, " address size: %u digits (%u bytes)", |
| tp_addr_size_digits, tp_addr_size_bytes); |
| |
| if (tp_addr_size_bytes == 0) { |
| g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, |
| "Couldn't read address: field missing"); |
| mm_sms_part_free (sms_part); |
| return NULL; |
| } |
| /* +1 due to the Type of Address byte */ |
| PDU_SIZE_CHECK (offset + 1 + tp_addr_size_bytes, "cannot read number"); |
| address = sms_decode_address (&pdu[offset], tp_addr_size_digits, error); |
| if (!address) { |
| g_prefix_error (error, "Couldn't read address: "); |
| mm_sms_part_free (sms_part); |
| return NULL; |
| } |
| mm_sms_part_take_number (sms_part, g_steal_pointer (&address)); |
| mm_obj_dbg (log_object, " number parsed: %s", mm_sms_part_get_number (sms_part)); |
| offset += (1 + tp_addr_size_bytes); |
| |
| /* ---------------------------------------------------------------------- */ |
| /* Get timestamps and indexes for TP-PID, TP-DCS and TP-UDL/TP-UD */ |
| |
| if (pdu_type == SMS_TP_MTI_SMS_DELIVER) { |
| gchar *str = NULL; |
| |
| PDU_SIZE_CHECK (offset + 9, |
| "cannot read PID/DCS/Timestamp"); /* 1+1+7=9 */ |
| |
| /* ------ TP-PID (1 byte) ------ */ |
| tp_pid_offset = offset++; |
| |
| /* ------ TP-DCS (1 byte) ------ */ |
| tp_dcs_offset = offset++; |
| |
| /* ------ Timestamp (7 bytes) ------ */ |
| str = sms_decode_timestamp (&pdu[offset], error); |
| if (!str) { |
| mm_sms_part_free (sms_part); |
| return NULL; |
| } |
| mm_sms_part_take_timestamp (sms_part, str); |
| offset += 7; |
| |
| tp_user_data_len_offset = offset; |
| } else if (pdu_type == SMS_TP_MTI_SMS_SUBMIT) { |
| PDU_SIZE_CHECK (offset + 2 + !!validity_format, |
| "cannot read PID/DCS/Validity"); /* 1+1=2 */ |
| |
| /* ------ TP-PID (1 byte) ------ */ |
| tp_pid_offset = offset++; |
| |
| /* ------ TP-DCS (1 byte) ------ */ |
| tp_dcs_offset = offset++; |
| |
| /* ----------- TP-Validity-Period (1 byte) ----------- */ |
| if (validity_format) { |
| switch (validity_format) { |
| case 0x10: |
| mm_obj_dbg (log_object, " validity available, format relative"); |
| mm_sms_part_set_validity_relative (sms_part, |
| relative_to_validity (pdu[offset])); |
| offset++; |
| break; |
| case 0x08: |
| /* TODO: support enhanced format; GSM 03.40 */ |
| mm_obj_dbg (log_object, " validity available, format enhanced (not implemented)"); |
| /* 7 bytes for enhanced validity */ |
| offset += 7; |
| break; |
| case 0x18: |
| /* TODO: support absolute format; GSM 03.40 */ |
| mm_obj_dbg (log_object, " validity available, format absolute (not implemented)"); |
| /* 7 bytes for absolute validity */ |
| offset += 7; |
| break; |
| default: |
| /* Cannot happen as we AND with the 0x18 mask */ |
| g_assert_not_reached (); |
| } |
| } |
| |
| tp_user_data_len_offset = offset; |
| } else if (pdu_type == SMS_TP_MTI_SMS_STATUS_REPORT) { |
| gchar *str = NULL; |
| |
| /* We have 2 timestamps in status report PDUs: |
| * first, the timestamp for when the PDU was received in the SMSC |
| * second, the timestamp for when the PDU was forwarded by the SMSC |
| */ |
| PDU_SIZE_CHECK (offset + 15, "cannot read Timestamps/TP-STATUS"); /* 7+7+1=15 */ |
| |
| /* ------ Timestamp (7 bytes) ------ */ |
| str = sms_decode_timestamp (&pdu[offset], error); |
| if (!str) { |
| mm_sms_part_free (sms_part); |
| return NULL; |
| } |
| mm_sms_part_take_timestamp (sms_part, str); |
| offset += 7; |
| |
| /* ------ Discharge Timestamp (7 bytes) ------ */ |
| str = sms_decode_timestamp (&pdu[offset], error); |
| if (!str) { |
| mm_sms_part_free (sms_part); |
| return NULL; |
| } |
| mm_sms_part_take_discharge_timestamp (sms_part, str); |
| offset += 7; |
| |
| /* ----- TP-STATUS (1 byte) ------ */ |
| mm_obj_dbg (log_object, " delivery state: %u", (guint)pdu[offset]); |
| mm_sms_part_set_delivery_state (sms_part, pdu[offset]); |
| offset++; |
| |
| /* ------ TP-PI (1 byte) OPTIONAL ------ */ |
| if (offset < pdu_len) { |
| guint next_optional_field_offset = offset + 1; |
| |
| /* TP-PID? */ |
| if (pdu[offset] & 0x01) |
| tp_pid_offset = next_optional_field_offset++; |
| |
| /* TP-DCS? */ |
| if (pdu[offset] & 0x02) |
| tp_dcs_offset = next_optional_field_offset++; |
| |
| /* TP-UserData? */ |
| if (pdu[offset] & 0x04) |
| tp_user_data_len_offset = next_optional_field_offset; |
| } |
| } else |
| g_assert_not_reached (); |
| |
| mm_obj_dbg (log_object, " timestamp: %s", mm_sms_part_get_timestamp (sms_part)); |
| |
| if (tp_pid_offset > 0) { |
| PDU_SIZE_CHECK (tp_pid_offset + 1, "cannot read TP-PID"); |
| mm_obj_dbg (log_object, " PID: %u", (guint)pdu[tp_pid_offset]); |
| } |
| |
| /* Grab user data encoding and message class */ |
| if (tp_dcs_offset > 0) { |
| PDU_SIZE_CHECK (tp_dcs_offset + 1, "cannot read TP-DCS"); |
| |
| /* Encoding given in the 'alphabet' bits */ |
| user_data_encoding = sms_encoding_type (pdu[tp_dcs_offset]); |
| switch (user_data_encoding) { |
| case MM_SMS_ENCODING_GSM7: |
| mm_obj_dbg (log_object, " user data encoding is GSM7"); |
| break; |
| case MM_SMS_ENCODING_UCS2: |
| mm_obj_dbg (log_object, " user data encoding is UCS2"); |
| break; |
| case MM_SMS_ENCODING_8BIT: |
| mm_obj_dbg (log_object, " user data encoding is 8bit"); |
| break; |
| case MM_SMS_ENCODING_UNKNOWN: |
| mm_obj_dbg (log_object, " user data encoding is unknown"); |
| break; |
| default: |
| g_assert_not_reached (); |
| |
| } |
| mm_sms_part_set_encoding (sms_part, user_data_encoding); |
| |
| /* Class */ |
| if (pdu[tp_dcs_offset] & SMS_DCS_CLASS_VALID) |
| mm_sms_part_set_class (sms_part, |
| pdu[tp_dcs_offset] & SMS_DCS_CLASS_MASK); |
| } |
| |
| if (tp_user_data_len_offset > 0) { |
| guint tp_user_data_size_elements; |
| guint tp_user_data_size_bytes; |
| guint tp_user_data_offset; |
| guint bit_offset; |
| |
| PDU_SIZE_CHECK (tp_user_data_len_offset + 1, "cannot read TP-UDL"); |
| tp_user_data_size_elements = pdu[tp_user_data_len_offset]; |
| mm_obj_dbg (log_object, " user data length: %u elements", tp_user_data_size_elements); |
| |
| if (user_data_encoding == MM_SMS_ENCODING_GSM7) |
| tp_user_data_size_bytes = (7 * (tp_user_data_size_elements + 1 )) / 8; |
| else |
| tp_user_data_size_bytes = tp_user_data_size_elements; |
| mm_obj_dbg (log_object, " user data length: %u bytes", tp_user_data_size_bytes); |
| |
| tp_user_data_offset = tp_user_data_len_offset + 1; |
| PDU_SIZE_CHECK (tp_user_data_offset + tp_user_data_size_bytes, "cannot read TP-UD"); |
| |
| bit_offset = 0; |
| if (has_udh) { |
| guint udhl_elements; |
| guint udhl, end; |
| |
| PDU_SIZE_CHECK (tp_user_data_offset + 1, "cannot read UDH length"); |
| udhl = pdu[tp_user_data_offset] + 1; |
| end = tp_user_data_offset + udhl; |
| |
| PDU_SIZE_CHECK (tp_user_data_offset + udhl, "cannot read UDH"); |
| |
| for (offset = tp_user_data_offset + 1; (offset + 1) < end;) { |
| guint8 ie_id, ie_len; |
| |
| ie_id = pdu[offset++]; |
| ie_len = pdu[offset++]; |
| |
| switch (ie_id) { |
| case 0x00: |
| if (offset + 2 >= end) |
| break; |
| /* |
| * Ignore the IE if one of the following is true: |
| * - it claims to be part 0 of M |
| * - it claims to be part N of M, N > M |
| */ |
| if (pdu[offset + 2] == 0 || |
| pdu[offset + 2] > pdu[offset + 1]) |
| break; |
| |
| mm_sms_part_set_concat_reference (sms_part, pdu[offset]); |
| mm_sms_part_set_concat_max (sms_part, pdu[offset + 1]); |
| mm_sms_part_set_concat_sequence (sms_part, pdu[offset + 2]); |
| break; |
| case 0x08: |
| if (offset + 3 >= end) |
| break; |
| /* Concatenated short message, 16-bit reference */ |
| if (pdu[offset + 3] == 0 || |
| pdu[offset + 3] > pdu[offset + 2]) |
| break; |
| |
| mm_sms_part_set_concat_reference (sms_part, (pdu[offset] << 8) | pdu[offset + 1]); |
| mm_sms_part_set_concat_max (sms_part,pdu[offset + 2]); |
| mm_sms_part_set_concat_sequence (sms_part, pdu[offset + 3]); |
| break; |
| default: |
| break; |
| } |
| |
| offset += ie_len; |
| } |
| |
| /* |
| * Move past the user data headers to prevent it from being |
| * decoded into garbage text. |
| */ |
| tp_user_data_offset += udhl; |
| tp_user_data_size_bytes -= udhl; |
| if (user_data_encoding == MM_SMS_ENCODING_GSM7) { |
| /* |
| * Find the number of bits we need to add to the length of the |
| * user data to get a multiple of 7 (the padding). |
| */ |
| bit_offset = (7 - udhl % 7) % 7; |
| udhl_elements = (udhl * 8 + bit_offset) / 7; |
| } else |
| udhl_elements = udhl; |
| |
| if (udhl_elements >= tp_user_data_size_elements) { |
| g_set_error (error, MM_CORE_ERROR, MM_CORE_ERROR_FAILED, |
| "udhl length (%u) is greater than data size (%u)", |
| udhl_elements, tp_user_data_size_elements); |
| mm_sms_part_free (sms_part); |
| return NULL; |
| } |
| tp_user_data_size_elements -= udhl_elements; |
| } |
| |
| switch (user_data_encoding) { |
| case MM_SMS_ENCODING_GSM7: |
| case MM_SMS_ENCODING_UCS2: |
| { |
| gchar *text; |
| |
| /* Otherwise if it's 7-bit or UCS2 we can decode it */ |
| mm_obj_dbg (log_object, "decoding SMS text with %u elements", tp_user_data_size_elements); |
| text = mm_sms_decode_text (&pdu[tp_user_data_offset], |
| tp_user_data_size_elements, |
| user_data_encoding, |
| bit_offset, |
| log_object, |
| error); |
| if (!text) { |
| mm_sms_part_free (sms_part); |
| return NULL; |
| } |
| mm_sms_part_take_text (sms_part, text); |
| break; |
| } |
| case MM_SMS_ENCODING_8BIT: |
| case MM_SMS_ENCODING_UNKNOWN: |
| default: |
| { |
| GByteArray *raw; |
| |
| mm_obj_dbg (log_object, "skipping SMS text: unknown encoding (0x%02X)", user_data_encoding); |
| |
| PDU_SIZE_CHECK (tp_user_data_offset + tp_user_data_size_bytes, "cannot read user data"); |
| |
| /* 8-bit encoding is usually binary data, and we have no idea what |
| * actual encoding the data is in so we can't convert it. |
| */ |
| raw = g_byte_array_sized_new (tp_user_data_size_bytes); |
| g_byte_array_append (raw, &pdu[tp_user_data_offset], tp_user_data_size_bytes); |
| mm_sms_part_take_data (sms_part, raw); |
| break; |
| } |
| } |
| } |
| |
| return sms_part; |
| } |
| |
| /** |
| * mm_sms_part_3gpp_encode_address: |
| * |
| * @address: the phone number to encode |
| * @buf: the buffer to encode @address in |
| * @buflen: the size of @buf |
| * @is_smsc: if %TRUE encode size as number of octets of address information, |
| * otherwise if %FALSE encode size as number of digits of @address |
| * |
| * Returns: the size in bytes of the data added to @buf |
| **/ |
| guint |
| mm_sms_part_3gpp_encode_address (const gchar *address, |
| guint8 *buf, |
| gsize buflen, |
| gboolean is_smsc) |
| { |
| gsize len; |
| |
| g_return_val_if_fail (address != NULL, 0); |
| g_return_val_if_fail (buf != NULL, 0); |
| g_return_val_if_fail (buflen >= 2, 0); |
| |
| /* Handle number type & plan */ |
| buf[1] = 0x80; /* Bit 7 always 1 */ |
| if (address[0] == '+') { |
| buf[1] |= SMS_NUMBER_TYPE_INTL; |
| address++; |
| } |
| buf[1] |= SMS_NUMBER_PLAN_TELEPHONE; |
| |
| len = sms_string_to_bcd_semi_octets (&buf[2], buflen, address); |
| |
| if (is_smsc) |
| buf[0] = len + 1; /* addr length + size byte */ |
| else |
| buf[0] = strlen (address); /* number of digits in address */ |
| |
| return len ? len + 2 : 0; /* addr length + size byte + number type/plan */ |
| } |
| |
| /** |
| * mm_sms_part_3gpp_get_submit_pdu: |
| * |
| * @part: the SMS message part |
| * @out_pdulen: on success, the size of the returned PDU in bytes |
| * @out_msgstart: on success, the byte index in the returned PDU where the |
| * message starts (ie, skipping the SMSC length byte and address, if present) |
| * @error: on error, filled with the error that occurred |
| * |
| * Constructs a single-part SMS message with the given details, preferring to |
| * use the UCS2 character set when the message will fit, otherwise falling back |
| * to the GSM character set. |
| * |
| * Returns: the constructed PDU data on success, or %NULL on error |
| **/ |
| guint8 * |
| mm_sms_part_3gpp_get_submit_pdu (MMSmsPart *part, |
| guint *out_pdulen, |
| guint *out_msgstart, |
| gpointer log_object, |
| GError **error) |
| { |
| guint8 *pdu; |
| guint len, offset = 0; |
| guint shift = 0; |
| guint8 *udl_ptr; |
| MMSmsEncoding encoding; |
| |
| g_return_val_if_fail (mm_sms_part_get_number (part) != NULL, NULL); |
| g_return_val_if_fail (mm_sms_part_get_text (part) != NULL || mm_sms_part_get_data (part) != NULL, NULL); |
| |
| if (mm_sms_part_get_pdu_type (part) != MM_SMS_PDU_TYPE_SUBMIT) { |
| g_set_error (error, |
| MM_MESSAGE_ERROR, |
| MM_MESSAGE_ERROR_INVALID_PDU_PARAMETER, |
| "Invalid PDU type to generate a 'submit' PDU: '%s'", |
| mm_sms_pdu_type_get_string (mm_sms_part_get_pdu_type (part))); |
| return NULL; |
| } |
| |
| mm_obj_dbg (log_object, "creating PDU for part..."); |
| |
| /* Build up the PDU */ |
| pdu = g_malloc0 (PDU_SIZE); |
| |
| if (mm_sms_part_get_smsc (part)) { |
| mm_obj_dbg (log_object, " adding SMSC to PDU..."); |
| len = mm_sms_part_3gpp_encode_address (mm_sms_part_get_smsc (part), pdu, PDU_SIZE, TRUE); |
| if (len == 0) { |
| g_set_error (error, |
| MM_MESSAGE_ERROR, |
| MM_MESSAGE_ERROR_INVALID_PDU_PARAMETER, |
| "Invalid SMSC address '%s'", mm_sms_part_get_smsc (part)); |
| goto error; |
| } |
| offset += len; |
| } else { |
| /* No SMSC, use default */ |
| pdu[offset++] = 0x00; |
| } |
| |
| if (out_msgstart) |
| *out_msgstart = offset; |
| |
| /* ----------- First BYTE ----------- */ |
| pdu[offset] = 0; |
| |
| /* TP-VP present; format RELATIVE */ |
| if (mm_sms_part_get_validity_relative (part) > 0) { |
| mm_obj_dbg (log_object, " adding validity to PDU..."); |
| pdu[offset] |= 0x10; |
| } |
| |
| /* Concatenation sequence only found in multipart SMS */ |
| if (mm_sms_part_get_concat_sequence (part)) { |
| mm_obj_dbg (log_object, " adding UDHI to PDU..."); |
| pdu[offset] |= 0x40; /* UDHI */ |
| } |
| |
| /* Delivery report requested in singlepart messages or in the last PDU of |
| * multipart messages */ |
| if (mm_sms_part_get_delivery_report_request (part) && |
| (!mm_sms_part_get_concat_sequence (part) || |
| mm_sms_part_get_concat_max (part) == mm_sms_part_get_concat_sequence (part))) { |
| mm_obj_dbg (log_object, " requesting delivery report..."); |
| pdu[offset] |= 0x20; |
| } |
| |
| /* TP-MTI = SMS-SUBMIT */ |
| pdu[offset++] |= 0x01; |
| |
| |
| /* ----------- TP-MR (1 byte) ----------- */ |
| |
| pdu[offset++] = 0x00; /* TP-Message-Reference: filled by device */ |
| |
| /* ----------- Destination address ----------- */ |
| |
| len = mm_sms_part_3gpp_encode_address (mm_sms_part_get_number (part), &pdu[offset], PDU_SIZE - offset, FALSE); |
| if (len == 0) { |
| g_set_error (error, |
| MM_MESSAGE_ERROR, |
| MM_MESSAGE_ERROR_INVALID_PDU_PARAMETER, |
| "Invalid number '%s'", mm_sms_part_get_number (part)); |
| goto error; |
| } |
| offset += len; |
| |
| /* ----------- TP-PID (1 byte) ----------- */ |
| |
| pdu[offset++] = 0x00; |
| |
| /* ----------- TP-DCS (1 byte) ----------- */ |
| pdu[offset] = 0x00; |
| |
| if (mm_sms_part_get_class (part) >= 0 && mm_sms_part_get_class (part) <= 3) { |
| mm_obj_dbg (log_object, " using class %d...", mm_sms_part_get_class (part)); |
| pdu[offset] |= SMS_DCS_CLASS_VALID; |
| pdu[offset] |= mm_sms_part_get_class (part); |
| } |
| |
| encoding = mm_sms_part_get_encoding (part); |
| |
| switch (encoding) { |
| case MM_SMS_ENCODING_UCS2: |
| mm_obj_dbg (log_object, " using UCS2 encoding..."); |
| pdu[offset] |= SMS_DCS_CODING_UCS2; |
| break; |
| case MM_SMS_ENCODING_GSM7: |
| mm_obj_dbg (log_object, " using GSM7 encoding..."); |
| pdu[offset] |= SMS_DCS_CODING_DEFAULT; /* GSM */ |
| break; |
| case MM_SMS_ENCODING_8BIT: |
| case MM_SMS_ENCODING_UNKNOWN: |
| default: |
| mm_obj_dbg (log_object, " using 8bit encoding..."); |
| pdu[offset] |= SMS_DCS_CODING_8BIT; |
| break; |
| } |
| offset++; |
| |
| /* ----------- TP-Validity-Period (1 byte): 4 days ----------- */ |
| /* Only if TP-VPF was set in first byte */ |
| |
| if (mm_sms_part_get_validity_relative (part) > 0) |
| pdu[offset++] = validity_to_relative (mm_sms_part_get_validity_relative (part)); |
| |
| /* ----------- TP-User-Data-Length ----------- */ |
| /* Set to zero initially, and keep a ptr for easy access later */ |
| udl_ptr = &pdu[offset]; |
| pdu[offset++] = 0; |
| |
| /* Build UDH */ |
| if (mm_sms_part_get_concat_sequence (part)) { |
| mm_obj_dbg (log_object, " adding UDH header in PDU... (reference: %u, max: %u, sequence: %u)", |
| mm_sms_part_get_concat_reference (part), |
| mm_sms_part_get_concat_max (part), |
| mm_sms_part_get_concat_sequence (part)); |
| pdu[offset++] = 0x05; /* udh len */ |
| pdu[offset++] = 0x00; /* mid */ |
| pdu[offset++] = 0x03; /* data len */ |
| pdu[offset++] = (guint8)mm_sms_part_get_concat_reference (part); |
| pdu[offset++] = (guint8)mm_sms_part_get_concat_max (part); |
| pdu[offset++] = (guint8)mm_sms_part_get_concat_sequence (part); |
| |
| /* if a UDH is present and the data encoding is the default 7-bit |
| * alphabet, the user data must be 7-bit word aligned after the |
| * UDH. This means up to 6 bits of zeros need to be inserted at the |
| * start of the message. |
| * |
| * In our case the UDH is 6 bytes long, 48bits. The next multiple of |
| * 7 is therefore 49, so we only need to include one bit of padding. |
| */ |
| shift = 1; |
| } |
| |
| if (encoding == MM_SMS_ENCODING_GSM7) { |
| g_autoptr(GByteArray) unpacked = NULL; |
| g_autofree guint8 *packed = NULL; |
| guint32 packlen = 0; |
| |
| unpacked = mm_modem_charset_bytearray_from_utf8 (mm_sms_part_get_text (part), MM_MODEM_CHARSET_GSM, FALSE, error); |
| if (!unpacked) |
| goto error; |
| |
| if (unpacked->len == 0) { |
| g_set_error_literal (error, |
| MM_MESSAGE_ERROR, |
| MM_MESSAGE_ERROR_INVALID_PDU_PARAMETER, |
| "Failed to convert message text to GSM"); |
| goto error; |
| } |
| |
| /* Set real data length, in septets |
| * If we had UDH, add 7 septets |
| */ |
| *udl_ptr = mm_sms_part_get_concat_sequence (part) ? (7 + unpacked->len) : unpacked->len; |
| mm_obj_dbg (log_object, " user data length is %u septets (%s UDH)", |
| *udl_ptr, |
| mm_sms_part_get_concat_sequence (part) ? "with" : "without"); |
| |
| packed = mm_charset_gsm_pack (unpacked->data, unpacked->len, shift, &packlen); |
| if (!packed || packlen == 0) { |
| g_set_error_literal (error, |
| MM_MESSAGE_ERROR, |
| MM_MESSAGE_ERROR_INVALID_PDU_PARAMETER, |
| "Failed to pack message text to GSM"); |
| goto error; |
| } |
| |
| if (offset + packlen > PDU_SIZE) { |
| g_set_error (error, |
| MM_MESSAGE_ERROR, |
| MM_MESSAGE_ERROR_INVALID_PDU_PARAMETER, |
| "Packed user data is too large for PDU (want %d bytes total, have %d)", |
| offset + packlen, PDU_SIZE); |
| goto error; |
| } |
| |
| memcpy (&pdu[offset], packed, packlen); |
| offset += packlen; |
| } else if (encoding == MM_SMS_ENCODING_UCS2) { |
| g_autoptr(GByteArray) array = NULL; |
| g_autoptr(GError) inner_error = NULL; |
| |
| /* Always assume UTF-16 instead of UCS-2! */ |
| array = mm_modem_charset_bytearray_from_utf8 (mm_sms_part_get_text (part), MM_MODEM_CHARSET_UTF16, FALSE, &inner_error); |
| if (!array) { |
| g_set_error (error, |
| MM_MESSAGE_ERROR, |
| MM_MESSAGE_ERROR_INVALID_PDU_PARAMETER, |
| "Failed to convert message text to UTF-16: %s", |
| inner_error->message); |
| goto error; |
| } |
| |
| /* Set real data length, in octets |
| * If we had UDH, add 6 octets |
| */ |
| *udl_ptr = mm_sms_part_get_concat_sequence (part) ? (6 + array->len) : array->len; |
| mm_obj_dbg (log_object, " user data length is %u octets (%s UDH)", |
| *udl_ptr, |
| mm_sms_part_get_concat_sequence (part) ? "with" : "without"); |
| |
| if (offset + array->len > PDU_SIZE) { |
| g_set_error (error, |
| MM_MESSAGE_ERROR, |
| MM_MESSAGE_ERROR_INVALID_PDU_PARAMETER, |
| "User data is too large for PDU (want %d bytes total, have %d)", |
| offset + array->len, PDU_SIZE); |
| goto error; |
| } |
| |
| memcpy (&pdu[offset], array->data, array->len); |
| offset += array->len; |
| } else if (mm_sms_part_get_encoding (part) == MM_SMS_ENCODING_8BIT) { |
| const GByteArray *data; |
| |
| data = mm_sms_part_get_data (part); |
| |
| /* Set real data length, in octets |
| * If we had UDH, add 6 octets |
| */ |
| *udl_ptr = mm_sms_part_get_concat_sequence (part) ? (6 + data->len) : data->len; |
| mm_obj_dbg (log_object, " binary user data length is %u octets (%s UDH)", |
| *udl_ptr, |
| mm_sms_part_get_concat_sequence (part) ? "with" : "without"); |
| |
| if (offset + data->len > PDU_SIZE) { |
| g_set_error (error, |
| MM_MESSAGE_ERROR, |
| MM_MESSAGE_ERROR_INVALID_PDU_PARAMETER, |
| "User data is too large for PDU (want %d bytes total, have %d)", |
| offset + data->len, PDU_SIZE); |
| goto error; |
| } |
| |
| memcpy (&pdu[offset], data->data, data->len); |
| offset += data->len; |
| } else |
| g_assert_not_reached (); |
| |
| if (out_pdulen) |
| *out_pdulen = offset; |
| return pdu; |
| |
| error: |
| g_free (pdu); |
| return NULL; |
| } |
| |
| GByteArray ** |
| mm_sms_part_3gpp_util_split_data (const guint8 *data, |
| gsize data_len) |
| { |
| GByteArray **out; |
| |
| /* Some info about the rules for splitting. |
| * |
| * The User Data can be up to 140 bytes in the SMS part: |
| * 0) If we only need one chunk, it can be of up to 140 bytes. |
| * If we need more than one chunk, these have to be of 140 - 6 = 134 |
| * bytes each, as we need place for the UDH header. |
| */ |
| |
| if (data_len <= 140) { |
| out = g_new0 (GByteArray *, 2); |
| out[0] = g_byte_array_append (g_byte_array_sized_new (data_len), |
| data, |
| data_len); |
| } else { |
| guint n_chunks; |
| guint i; |
| guint j; |
| |
| n_chunks = data_len / 134; |
| if (data_len % 134 != 0) |
| n_chunks ++; |
| |
| out = g_new0 (GByteArray *, n_chunks + 1); |
| for (i = 0, j = 0; i < n_chunks; i++, j+= 134) { |
| out[i] = g_byte_array_append (g_byte_array_sized_new (134), |
| &data[j], |
| MIN (data_len - j, 134)); |
| } |
| } |
| |
| return out; |
| } |