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
diff --git a/telephony/java/com/android/internal/telephony/GsmAlphabet.java b/telephony/java/com/android/internal/telephony/GsmAlphabet.java
index e8095e1..461b694 100644
--- a/telephony/java/com/android/internal/telephony/GsmAlphabet.java
+++ b/telephony/java/com/android/internal/telephony/GsmAlphabet.java
@@ -183,15 +183,9 @@
}
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 static byte[] stringToGsm7BitPacked(String data)
throws EncodeException {
- return stringToGsm7BitPacked(data, 0, -1, 0, true);
+ return stringToGsm7BitPacked(data, 0, true);
}
/**
@@ -228,58 +222,37 @@
* 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 7f3b473..65754fc 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 @@
}
}
- 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 @@
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 @@
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 @@
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 365fee8..02af547 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 @@
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 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 @@
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 @@
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 @@
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 @@
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);
+ }
}