From e00dd1e5a37bed0de964b353d47a4bd7cdff532f Mon Sep 17 00:00:00 2001 From: Tammo Spalink Date: Tue, 28 Jul 2009 20:35:36 +0800 Subject: [PATCH] Make GSM 7-bit encoding properly deal with initial padding. For CDMA, clean up the GSM encapsulation to properly align user data payload after the user data header. Addresses http://buganizer/issue?id=2007011 --- .../internal/telephony/GsmAlphabet.java | 63 ++---- .../telephony/cdma/sms/BearerData.java | 181 +++++++++++------- .../com/android/unit_tests/CdmaSmsTest.java | 50 ++++- 3 files changed, 180 insertions(+), 114 deletions(-) diff --git a/telephony/java/com/android/internal/telephony/GsmAlphabet.java b/telephony/java/com/android/internal/telephony/GsmAlphabet.java index e8095e172b86e..461b69454215a 100644 --- a/telephony/java/com/android/internal/telephony/GsmAlphabet.java +++ b/telephony/java/com/android/internal/telephony/GsmAlphabet.java @@ -183,15 +183,9 @@ public class GsmAlphabet { } int headerBits = (header.length + 1) * 8; - int headerSeptets = headerBits / 7; - headerSeptets += (headerBits % 7) > 0 ? 1 : 0; + int headerSeptets = (headerBits + 6) / 7; - int sz = data.length(); - int septetCount; - septetCount = countGsmSeptets(data, true) + headerSeptets; - - byte[] ret = stringToGsm7BitPacked(data, 0, septetCount, - (headerSeptets*7), true); + byte[] ret = stringToGsm7BitPacked(data, headerSeptets, true); // Paste in the header ret[1] = (byte)header.length; @@ -215,7 +209,7 @@ public class GsmAlphabet { */ public static byte[] stringToGsm7BitPacked(String data) throws EncodeException { - return stringToGsm7BitPacked(data, 0, -1, 0, true); + return stringToGsm7BitPacked(data, 0, true); } /** @@ -228,58 +222,37 @@ public class GsmAlphabet { * septets. * * @param data the text to convert to septets - * @param dataOffset the character offset in data to start the encoding from - * @param maxSeptets the maximum number of septets to convert, or -1 for no - * enforced maximum. - * @param startingBitOffset the number of padding bits to put before - * the start of the first septet at the begining of the array + * @param startingSeptetOffset the number of padding septets to put before + * the character data at the begining of the array * @param throwException If true, throws EncodeException on invalid char. * If false, replaces unencodable char with GSM alphabet space char. * * @throws EncodeException if String is too large to encode */ - public static byte[] stringToGsm7BitPacked(String data, int dataOffset, - int maxSeptets, int startingBitOffset, boolean throwException) - throws EncodeException { - - int sz = data.length(); - int septetCount; - if (maxSeptets == -1) { - septetCount = countGsmSeptets(data, true); - } else { - septetCount = maxSeptets; + public static byte[] stringToGsm7BitPacked(String data, int startingSeptetOffset, + boolean throwException) throws EncodeException { + int dataLen = data.length(); + int septetCount = countGsmSeptets(data, throwException) + startingSeptetOffset; + if (septetCount > 255) { + throw new EncodeException("Payload cannot exceed 255 septets"); } - - if(septetCount > 0xff) { - throw new EncodeException("Payload cannot exceed " + Short.MAX_VALUE - + " septets"); - } - - // Enough for all the septets and the length 2 byte prefix - byte[] ret = new byte[1 + (((septetCount * 7) + 7) / 8)]; - - int bitOffset = startingBitOffset; - int septets = startingBitOffset/7; - for (int i = dataOffset; i < sz && septets < septetCount; i++, bitOffset += 7) { + int byteCount = ((septetCount * 7) + 7) / 8; + byte[] ret = new byte[byteCount + 1]; // Include space for one byte length prefix. + for (int i = 0, septets = startingSeptetOffset, bitOffset = startingSeptetOffset * 7; + i < dataLen && septets < septetCount; + i++, bitOffset += 7) { char c = data.charAt(i); - int v = GsmAlphabet.charToGsm(c, throwException); if (v == GSM_EXTENDED_ESCAPE) { - // Lookup the extended char - v = GsmAlphabet.charToGsmExtended(c); - + v = GsmAlphabet.charToGsmExtended(c); // Lookup the extended char. packSmsChar(ret, bitOffset, GSM_EXTENDED_ESCAPE); bitOffset += 7; septets++; } - packSmsChar(ret, bitOffset, v); septets++; } - - // See check for > 0xff above - ret[0] = (byte)septets; - + ret[0] = (byte) (septetCount); // Validated by check above. return ret; } diff --git a/telephony/java/com/android/internal/telephony/cdma/sms/BearerData.java b/telephony/java/com/android/internal/telephony/cdma/sms/BearerData.java index 7f3b473826a34..65754fced6f35 100644 --- a/telephony/java/com/android/internal/telephony/cdma/sms/BearerData.java +++ b/telephony/java/com/android/internal/telephony/cdma/sms/BearerData.java @@ -455,53 +455,114 @@ public final class BearerData { } } - private static int calcUdhSeptetPadding(int userDataHeaderLen) { - int udhBits = userDataHeaderLen * 8; - int udhSeptets = (udhBits + 6) / 7; - int paddingBits = (udhSeptets * 7) - udhBits; - return paddingBits; + private static class Gsm7bitCodingResult { + int septets; + byte[] data; } - private static byte[] encode7bitGsm(String msg, int paddingBits) + private static Gsm7bitCodingResult encode7bitGsm(String msg, int septetOffset, boolean force) throws CodingException { try { /* * TODO(cleanup): It would be nice if GsmAlphabet provided * an option to produce just the data without prepending - * the length. + * the septet count, as this function is really just a + * wrapper to strip that off. Not to mention that the + * septet count is generally known prior to invocation of + * the encoder. Note that it cannot be derived from the + * resulting array length, since that cannot distinguish + * if the last contains either 1 or 8 valid bits. + * + * TODO(cleanup): The BitwiseXStreams could also be + * extended with byte-wise reversed endianness read/write + * routines to allow a corresponding implementation of + * stringToGsm7BitPacked, and potentially directly support + * access to the main bitwise stream from encode/decode. */ - byte []fullData = GsmAlphabet.stringToGsm7BitPacked(msg, 0, -1, paddingBits, true); - byte []data = new byte[fullData.length - 1]; - System.arraycopy(fullData, 1, data, 0, fullData.length - 1); - return data; + byte[] fullData = GsmAlphabet.stringToGsm7BitPacked(msg, septetOffset, !force); + Gsm7bitCodingResult result = new Gsm7bitCodingResult(); + result.data = new byte[fullData.length - 1]; + System.arraycopy(fullData, 1, result.data, 0, fullData.length - 1); + result.septets = fullData[0]; + return result; } catch (com.android.internal.telephony.EncodeException ex) { throw new CodingException("7bit GSM encode failed: " + ex); } } + private static void encode7bitEms(UserData uData, byte[] udhData, boolean force) + throws CodingException + { + int udhBytes = udhData.length + 1; // Add length octet. + int udhSeptets = ((udhBytes * 8) + 6) / 7; + Gsm7bitCodingResult gcr = encode7bitGsm(uData.payloadStr, udhSeptets, force); + uData.msgEncoding = UserData.ENCODING_GSM_7BIT_ALPHABET; + uData.numFields = gcr.septets; + uData.payload = gcr.data; + uData.payload[0] = (byte)udhData.length; + System.arraycopy(udhData, 0, uData.payload, 1, udhData.length); + } + + private static void encode16bitEms(UserData uData, byte[] udhData) + throws CodingException + { + byte[] payload = encodeUtf16(uData.payloadStr); + int udhBytes = udhData.length + 1; // Add length octet. + int udhCodeUnits = (udhBytes + 1) / 2; + int udhPadding = udhBytes % 2; + int payloadCodeUnits = payload.length / 2; + uData.numFields = udhCodeUnits + payloadCodeUnits; + uData.payload = new byte[uData.numFields * 2]; + uData.payload[0] = (byte)udhData.length; + System.arraycopy(udhData, 0, uData.payload, 1, udhData.length); + System.arraycopy(payload, 0, uData.payload, udhBytes + udhPadding, payload.length); + } + + private static void encodeEmsUserDataPayload(UserData uData) + throws CodingException + { + byte[] headerData = SmsHeader.toByteArray(uData.userDataHeader); + if (uData.msgEncodingSet) { + if (uData.msgEncoding == UserData.ENCODING_GSM_7BIT_ALPHABET) { + encode7bitEms(uData, headerData, true); + } else if (uData.msgEncoding == UserData.ENCODING_UNICODE_16) { + encode16bitEms(uData, headerData); + } else { + throw new CodingException("unsupported EMS user data encoding (" + + uData.msgEncoding + ")"); + } + } else { + try { + encode7bitEms(uData, headerData, false); + } catch (CodingException ex) { + encode16bitEms(uData, headerData); + } + } + } + private static void encodeUserDataPayload(UserData uData) throws CodingException { - // TODO(cleanup): UDH can only occur in EMS mode, meaning - // encapsulation of GSM encoding, and so the logic here should - // be refactored to more cleanly reflect this constraint. + if ((uData.payloadStr == null) && (uData.msgEncoding != UserData.ENCODING_OCTET)) { + Log.e(LOG_TAG, "user data with null payloadStr"); + uData.payloadStr = ""; + } - byte[] headerData = null; - if (uData.userDataHeader != null) headerData = SmsHeader.toByteArray(uData.userDataHeader); - int headerDataLen = (headerData == null) ? 0 : headerData.length + 1; // + length octet + if (uData.userDataHeader != null) { + encodeEmsUserDataPayload(uData); + return; + } - byte[] payloadData; - int codeUnitCount; if (uData.msgEncodingSet) { if (uData.msgEncoding == UserData.ENCODING_OCTET) { if (uData.payload == null) { Log.e(LOG_TAG, "user data with octet encoding but null payload"); - payloadData = new byte[0]; - codeUnitCount = 0; + uData.payload = new byte[0]; + uData.numFields = 0; } else { - payloadData = uData.payload; - codeUnitCount = uData.payload.length; + uData.payload = uData.payload; + uData.numFields = uData.payload.length; } } else { if (uData.payloadStr == null) { @@ -509,65 +570,48 @@ public final class BearerData { uData.payloadStr = ""; } if (uData.msgEncoding == UserData.ENCODING_GSM_7BIT_ALPHABET) { - int paddingBits = calcUdhSeptetPadding(headerDataLen); - payloadData = encode7bitGsm(uData.payloadStr, paddingBits); - codeUnitCount = ((payloadData.length + headerDataLen) * 8) / 7; + Gsm7bitCodingResult gcr = encode7bitGsm(uData.payloadStr, 0, true); + uData.payload = gcr.data; + uData.numFields = gcr.septets; } else if (uData.msgEncoding == UserData.ENCODING_7BIT_ASCII) { - payloadData = encode7bitAscii(uData.payloadStr, true); - codeUnitCount = uData.payloadStr.length(); + uData.payload = encode7bitAscii(uData.payloadStr, true); + uData.numFields = uData.payloadStr.length(); } else if (uData.msgEncoding == UserData.ENCODING_UNICODE_16) { - payloadData = encodeUtf16(uData.payloadStr); - codeUnitCount = uData.payloadStr.length(); + uData.payload = encodeUtf16(uData.payloadStr); + uData.numFields = uData.payloadStr.length(); } else { throw new CodingException("unsupported user data encoding (" + uData.msgEncoding + ")"); } } } else { - if (uData.payloadStr == null) { - Log.e(LOG_TAG, "user data with null payloadStr"); - uData.payloadStr = ""; - } try { - if (headerData == null) { - payloadData = encode7bitAscii(uData.payloadStr, false); - codeUnitCount = uData.payloadStr.length(); - uData.msgEncoding = UserData.ENCODING_7BIT_ASCII; - } else { - // If there is a header, we are in EMS mode, in - // which case we use GSM encodings. - int paddingBits = calcUdhSeptetPadding(headerDataLen); - payloadData = encode7bitGsm(uData.payloadStr, paddingBits); - codeUnitCount = ((payloadData.length + headerDataLen) * 8) / 7; - uData.msgEncoding = UserData.ENCODING_GSM_7BIT_ALPHABET; - } + uData.payload = encode7bitAscii(uData.payloadStr, false); + uData.msgEncoding = UserData.ENCODING_7BIT_ASCII; } catch (CodingException ex) { - payloadData = encodeUtf16(uData.payloadStr); - codeUnitCount = uData.payloadStr.length(); + uData.payload = encodeUtf16(uData.payloadStr); uData.msgEncoding = UserData.ENCODING_UNICODE_16; } + uData.numFields = uData.payloadStr.length(); uData.msgEncodingSet = true; } - - int totalLength = payloadData.length + headerDataLen; - if (totalLength > SmsMessage.MAX_USER_DATA_BYTES) { - throw new CodingException("encoded user data too large (" + totalLength + - " > " + SmsMessage.MAX_USER_DATA_BYTES + " bytes)"); - } - - uData.numFields = codeUnitCount; - uData.payload = new byte[totalLength]; - if (headerData != null) { - uData.payload[0] = (byte)headerData.length; - System.arraycopy(headerData, 0, uData.payload, 1, headerData.length); - } - System.arraycopy(payloadData, 0, uData.payload, headerDataLen, payloadData.length); } private static void encodeUserData(BearerData bData, BitwiseOutputStream outStream) throws BitwiseOutputStream.AccessException, CodingException { + /* + * TODO(cleanup): Do we really need to set userData.payload as + * a side effect of encoding? If not, we could avoid data + * copies by passing outStream directly. + */ encodeUserDataPayload(bData.userData); + if (bData.userData.payload.length > SmsMessage.MAX_USER_DATA_BYTES) { + throw new CodingException("encoded user data too large (" + + bData.userData.payload.length + + " > " + SmsMessage.MAX_USER_DATA_BYTES + " bytes)"); + } + /** * XXX/TODO: figure out what the right answer is WRT padding bits * @@ -846,6 +890,9 @@ public final class BearerData { private static String decodeUtf16(byte[] data, int offset, int numFields) throws CodingException { + // Start reading from the next 16-bit aligned boundry after offset. + int padding = offset % 2; + numFields -= (offset + padding) / 2; try { return new String(data, offset, numFields * 2, "utf-16be"); } catch (java.io.UnsupportedEncodingException ex) { @@ -889,11 +936,11 @@ public final class BearerData { private static String decode7bitGsm(byte[] data, int offset, int numFields) throws CodingException { - int paddingBits = calcUdhSeptetPadding(offset); - numFields -= (((offset * 8) + paddingBits) / 7); - // TODO: It seems wrong that only Gsm7 bit encodings would - // take into account the header in numFields calculations. - // This should be verified. + // Start reading from the next 7-bit aligned boundry after offset. + int offsetBits = offset * 8; + int offsetSeptets = (offsetBits + 6) / 7; + numFields -= offsetSeptets; + int paddingBits = (offsetSeptets * 7) - offsetBits; String result = GsmAlphabet.gsm7BitPackedToString(data, offset, numFields, paddingBits); if (result == null) { throw new CodingException("7bit GSM decoding failed"); diff --git a/tests/AndroidTests/src/com/android/unit_tests/CdmaSmsTest.java b/tests/AndroidTests/src/com/android/unit_tests/CdmaSmsTest.java index 365fee8b1c573..02af547201184 100644 --- a/tests/AndroidTests/src/com/android/unit_tests/CdmaSmsTest.java +++ b/tests/AndroidTests/src/com/android/unit_tests/CdmaSmsTest.java @@ -18,9 +18,11 @@ package com.android.unit_tests; import com.android.internal.telephony.GsmAlphabet; import com.android.internal.telephony.SmsHeader; +import com.android.internal.telephony.cdma.SmsMessage; import com.android.internal.telephony.cdma.sms.BearerData; import com.android.internal.telephony.cdma.sms.UserData; import com.android.internal.telephony.cdma.sms.CdmaSmsAddress; +import com.android.internal.telephony.SmsMessageBase.TextEncodingDetails; import com.android.internal.util.BitwiseInputStream; import com.android.internal.util.BitwiseOutputStream; import com.android.internal.util.HexDump; @@ -28,12 +30,12 @@ import com.android.internal.util.HexDump; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; + import java.util.Iterator; import java.lang.Integer; -import android.util.Log; - public class CdmaSmsTest extends AndroidTestCase { private final static String LOG_TAG = "CDMA"; @@ -151,6 +153,9 @@ public class CdmaSmsTest extends AndroidTestCase { userData.payloadStr = "Test \n standard \r SMS"; revBearerData = BearerData.decode(BearerData.encode(bearerData)); assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + userData.payloadStr = ""; + revBearerData = BearerData.decode(BearerData.encode(bearerData)); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); } @SmallTest @@ -172,6 +177,21 @@ public class CdmaSmsTest extends AndroidTestCase { assertEquals(userData.msgEncoding, revBearerData.userData.msgEncoding); assertEquals(userData.payloadStr.length(), revBearerData.userData.numFields); assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + userData.payloadStr = "1234567"; + revBearerData = BearerData.decode(BearerData.encode(bearerData)); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + userData.payloadStr = ""; + revBearerData = BearerData.decode(BearerData.encode(bearerData)); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + userData.payloadStr = "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "1234567890"; + revBearerData = BearerData.decode(BearerData.encode(bearerData)); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + userData.payloadStr = "Test \u007f illegal \u0000 SMS chars"; + revBearerData = BearerData.decode(BearerData.encode(bearerData)); + assertEquals("Test illegal SMS chars", revBearerData.userData.payloadStr); userData.payloadStr = "More @ testing\nis great^|^~woohoo"; revBearerData = BearerData.decode(BearerData.encode(bearerData)); assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); @@ -220,6 +240,12 @@ public class CdmaSmsTest extends AndroidTestCase { assertEquals(userData.msgEncoding, revBearerData.userData.msgEncoding); assertEquals(userData.payloadStr.length(), revBearerData.userData.numFields); assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + userData.payloadStr = "1234567"; + revBearerData = BearerData.decode(BearerData.encode(bearerData)); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + userData.payloadStr = ""; + revBearerData = BearerData.decode(BearerData.encode(bearerData)); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); } @SmallTest @@ -784,4 +810,24 @@ public class CdmaSmsTest extends AndroidTestCase { assertEquals(bd4.userData.payloadStr, "ABCDEFG"); } + @SmallTest + public void testUserDataHeaderWithEightCharMsg() throws Exception { + BearerData bearerData = new BearerData(); + bearerData.messageType = BearerData.MESSAGE_TYPE_DELIVER; + bearerData.messageId = 55; + SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef(); + concatRef.refNumber = 0xEE; + concatRef.msgCount = 2; + concatRef.seqNumber = 2; + concatRef.isEightBits = true; + SmsHeader smsHeader = new SmsHeader(); + smsHeader.concatRef = concatRef; + UserData userData = new UserData(); + userData.payloadStr = "01234567"; + userData.userDataHeader = smsHeader; + bearerData.userData = userData; + byte[] encodedSms = BearerData.encode(bearerData); + BearerData revBearerData = BearerData.decode(encodedSms); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + } }