1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.bluetooth.mapclient; 18 19 import android.util.Log; 20 21 import com.android.bluetooth.mapclient.BmsgTokenizer.Property; 22 import com.android.vcard.VCardEntry; 23 import com.android.vcard.VCardEntryConstructor; 24 import com.android.vcard.VCardEntryHandler; 25 import com.android.vcard.VCardParser; 26 import com.android.vcard.VCardParser_V21; 27 import com.android.vcard.VCardParser_V30; 28 import com.android.vcard.exception.VCardException; 29 import com.android.vcard.exception.VCardVersionException; 30 31 import java.io.ByteArrayInputStream; 32 import java.io.IOException; 33 import java.nio.charset.StandardCharsets; 34 import java.text.ParseException; 35 36 /* BMessage as defined by MAP_SPEC_V101 Section 3.1.3 Message format (x-bt/message) */ 37 class BmessageParser { 38 private static final String TAG = "BmessageParser"; 39 private static final boolean DBG = false; 40 41 private static final String CRLF = "\r\n"; 42 43 private static final Property BEGIN_BMSG = new Property("BEGIN", "BMSG"); 44 private static final Property END_BMSG = new Property("END", "BMSG"); 45 46 private static final Property BEGIN_VCARD = new Property("BEGIN", "VCARD"); 47 private static final Property END_VCARD = new Property("END", "VCARD"); 48 49 private static final Property BEGIN_BENV = new Property("BEGIN", "BENV"); 50 private static final Property END_BENV = new Property("END", "BENV"); 51 52 private static final Property BEGIN_BBODY = new Property("BEGIN", "BBODY"); 53 private static final Property END_BBODY = new Property("END", "BBODY"); 54 55 private static final Property BEGIN_MSG = new Property("BEGIN", "MSG"); 56 private static final Property END_MSG = new Property("END", "MSG"); 57 58 private static final int CRLF_LEN = 2; 59 60 /* 61 * length of "container" for 'message' in bmessage-body-content: 62 * BEGIN:MSG<CRLF> + <CRLF> + END:MSG<CRFL> 63 */ 64 private static final int MSG_CONTAINER_LEN = 22; 65 private final Bmessage mBmsg; 66 private BmsgTokenizer mParser; 67 BmessageParser()68 private BmessageParser() { 69 mBmsg = new Bmessage(); 70 } 71 createBmessage(String str)72 public static Bmessage createBmessage(String str) { 73 BmessageParser p = new BmessageParser(); 74 75 if (DBG) { 76 Log.d(TAG, "actual wired contents: " + str); 77 } 78 79 try { 80 p.parse(str); 81 } catch (IOException e) { 82 Log.e(TAG, "I/O exception when parsing bMessage", e); 83 return null; 84 } catch (ParseException e) { 85 Log.e(TAG, "Cannot parse bMessage", e); 86 return null; 87 } 88 89 return p.mBmsg; 90 } 91 expected(Property... props)92 private ParseException expected(Property... props) { 93 boolean first = true; 94 StringBuilder sb = new StringBuilder(); 95 96 for (Property prop : props) { 97 if (!first) { 98 sb.append(" or "); 99 } 100 sb.append(prop); 101 first = false; 102 } 103 104 return new ParseException("Expected: " + sb.toString(), mParser.pos()); 105 } 106 parse(String str)107 private void parse(String str) throws IOException, ParseException { 108 Property prop; 109 110 /* 111 * <bmessage-object>::= { "BEGIN:BMSG" <CRLF> <bmessage-property> 112 * [<bmessage-originator>]* <bmessage-envelope> "END:BMSG" <CRLF> } 113 */ 114 mParser = new BmsgTokenizer(str + CRLF); 115 116 prop = mParser.next(); 117 if (!prop.equals(BEGIN_BMSG)) { 118 throw expected(BEGIN_BMSG); 119 } 120 121 prop = parseProperties(); 122 123 while (prop.equals(BEGIN_VCARD)) { 124 /* <bmessage-originator>::= <vcard> <CRLF> */ 125 126 StringBuilder vcard = new StringBuilder(); 127 prop = extractVcard(vcard); 128 129 VCardEntry entry = parseVcard(vcard.toString()); 130 mBmsg.mOriginators.add(entry); 131 } 132 133 if (!prop.equals(BEGIN_BENV)) { 134 throw expected(BEGIN_BENV); 135 } 136 137 prop = parseEnvelope(1); 138 139 if (!prop.equals(END_BMSG)) { 140 throw expected(END_BENV); 141 } 142 143 /* 144 * there should be no meaningful data left in stream here so we just 145 * ignore whatever is left 146 */ 147 mParser = null; 148 } 149 parseProperties()150 private Property parseProperties() throws ParseException { 151 Property prop; 152 /* 153 * <bmessage-property>::=<bmessage-version-property> 154 * <bmessage-readstatus-property> <bmessage-type-property> 155 * <bmessage-folder-property> <bmessage-version-property>::="VERSION:" 156 * <common-digit>*"."<common-digit>* <CRLF> 157 * <bmessage-readstatus-property>::="STATUS:" 'readstatus' <CRLF> 158 * <bmessage-type-property>::="TYPE:" 'type' <CRLF> 159 * <bmessage-folder-property>::="FOLDER:" 'foldername' <CRLF> 160 */ 161 do { 162 prop = mParser.next(); 163 164 if (prop.name.equals("VERSION")) { 165 mBmsg.mBmsgVersion = prop.value; 166 167 } else if (prop.name.equals("STATUS")) { 168 for (Bmessage.Status s : Bmessage.Status.values()) { 169 if (prop.value.equals(s.toString())) { 170 mBmsg.mBmsgStatus = s; 171 break; 172 } 173 } 174 175 } else if (prop.name.equals("TYPE")) { 176 for (Bmessage.Type t : Bmessage.Type.values()) { 177 if (prop.value.equals(t.toString())) { 178 mBmsg.mBmsgType = t; 179 break; 180 } 181 } 182 183 } else if (prop.name.equals("FOLDER")) { 184 mBmsg.mBmsgFolder = prop.value; 185 186 } 187 188 } while (!prop.equals(BEGIN_VCARD) && !prop.equals(BEGIN_BENV)); 189 190 return prop; 191 } 192 parseEnvelope(int level)193 private Property parseEnvelope(int level) throws IOException, ParseException { 194 Property prop; 195 196 /* 197 * we can support as many nesting level as we want, but MAP spec clearly 198 * defines that there should be no more than 3 levels. so we verify it 199 * here. 200 */ 201 202 if (level > 3) { 203 throw new ParseException("bEnvelope is nested more than 3 times", mParser.pos()); 204 } 205 206 /* 207 * <bmessage-envelope> ::= { "BEGIN:BENV" <CRLF> [<bmessage-recipient>]* 208 * <bmessage-envelope> | <bmessage-content> "END:BENV" <CRLF> } 209 */ 210 211 prop = mParser.next(); 212 213 while (prop.equals(BEGIN_VCARD)) { 214 215 /* <bmessage-originator>::= <vcard> <CRLF> */ 216 217 StringBuilder vcard = new StringBuilder(); 218 prop = extractVcard(vcard); 219 220 if (level == 1) { 221 VCardEntry entry = parseVcard(vcard.toString()); 222 mBmsg.mRecipients.add(entry); 223 } 224 } 225 226 if (prop.equals(BEGIN_BENV)) { 227 prop = parseEnvelope(level + 1); 228 229 } else if (prop.equals(BEGIN_BBODY)) { 230 prop = parseBody(); 231 232 } else { 233 throw expected(BEGIN_BENV, BEGIN_BBODY); 234 } 235 236 if (!prop.equals(END_BENV)) { 237 throw expected(END_BENV); 238 } 239 240 return mParser.next(); 241 } 242 parseBody()243 private Property parseBody() throws IOException, ParseException { 244 Property prop; 245 246 /* 247 * <bmessage-content>::= { "BEGIN:BBODY"<CRLF> [<bmessage-body-part-ID> 248 * <CRLF>] <bmessage-body-property> <bmessage-body-content>* <CRLF> 249 * "END:BBODY"<CRLF> } <bmessage-body-part-ID>::="PARTID:" 'Part-ID' 250 * <bmessage-body-property>::=[<bmessage-body-encoding-property>] 251 * [<bmessage-body-charset-property>] 252 * [<bmessage-body-language-property>] 253 * <bmessage-body-content-length-property> 254 * <bmessage-body-encoding-property>::="ENCODING:"'encoding' <CRLF> 255 * <bmessage-body-charset-property>::="CHARSET:"'charset' <CRLF> 256 * <bmessage-body-language-property>::="LANGUAGE:"'language' <CRLF> 257 * <bmessage-body-content-length-property>::= "LENGTH:" <common-digit>* 258 * <CRLF> 259 */ 260 261 do { 262 prop = mParser.next(); 263 264 if (prop.name.equals("PARTID")) { 265 // Do nothing 266 } else if (prop.name.equals("ENCODING")) { 267 mBmsg.mBbodyEncoding = prop.value; 268 269 } else if (prop.name.equals("CHARSET")) { 270 mBmsg.mBbodyCharset = prop.value; 271 272 } else if (prop.name.equals("LANGUAGE")) { 273 mBmsg.mBbodyLanguage = prop.value; 274 275 } else if (prop.name.equals("LENGTH")) { 276 try { 277 mBmsg.mBbodyLength = Integer.parseInt(prop.value); 278 } catch (NumberFormatException e) { 279 throw new ParseException("Invalid LENGTH value", mParser.pos()); 280 } 281 282 } 283 284 } while (!prop.equals(BEGIN_MSG)); 285 286 /* 287 * check that the charset is always set to UTF-8. We expect only text transfer (in lieu with 288 * the MAPv12 specifying only RFC2822 (text only) for MMS/EMAIL and SMS do not support 289 * non-text content. If the charset is not set to UTF-8, it is safe to set the message as 290 * empty. We force the getMessage (see Client) to only call getMessage with 291 * UTF-8 as the MCE is not obliged to support native charset. 292 * 293 * 2020-06-01: we could now expect MMS to be more than text, e.g., image-only, so charset 294 * not always UTF-8, downgrading log message from ERROR to DEBUG. 295 */ 296 if (!"UTF-8".equals(mBmsg.mBbodyCharset)) { 297 Log.d(TAG, "The charset was not set to charset UTF-8: " + mBmsg.mBbodyCharset); 298 } 299 300 /* 301 * <bmessage-body-content>::={ "BEGIN:MSG"<CRLF> 'message'<CRLF> 302 * "END:MSG"<CRLF> } 303 */ 304 305 int messageLen = mBmsg.mBbodyLength - MSG_CONTAINER_LEN; 306 int offset = messageLen + CRLF_LEN; 307 int restartPos = mParser.pos() + offset; 308 /* 309 * length is specified in bytes so we need to convert from unicode 310 * string back to bytes array 311 */ 312 String remng = mParser.remaining(); 313 byte[] data = remng.getBytes(); 314 315 if (offset < 0 || offset > data.length) { 316 /* Handle possible exception for incorrect LENGTH value 317 * from MSE while parsing end of props */ 318 throw new ParseException("Invalid LENGTH value", mParser.pos()); 319 } 320 321 /* restart parsing from after 'message'<CRLF> */ 322 mParser = new BmsgTokenizer(new String(data, offset, data.length - offset), restartPos); 323 324 prop = mParser.next(true); 325 326 if (prop != null) { 327 if (prop.equals(END_MSG)) { 328 if ("UTF-8".equals(mBmsg.mBbodyCharset)) { 329 mBmsg.mMessage = new String(data, 0, messageLen, StandardCharsets.UTF_8); 330 } else { 331 mBmsg.mMessage = new String(data, 0, messageLen); 332 } 333 } else { 334 /* Handle possible exception for incorrect LENGTH value 335 * from MSE while parsing GET Message response */ 336 Log.e(TAG, "Prop Invalid: " + prop.toString()); 337 Log.e(TAG, "Possible Invalid LENGTH value"); 338 throw expected(END_MSG); 339 } 340 } else { 341 data = null; 342 343 /* 344 * now we check if bMessage can be parsed if LENGTH is handled as 345 * number of characters instead of number of bytes 346 */ 347 if (offset < 0 || offset > remng.length()) { 348 /* Handle possible exception for incorrect LENGTH value 349 * from MSE while parsing GET Message response */ 350 throw new ParseException("Invalid LENGTH value", mParser.pos()); 351 } 352 353 Log.w(TAG, "byte LENGTH seems to be invalid, trying with char length"); 354 355 mParser = new BmsgTokenizer(remng.substring(offset)); 356 357 prop = mParser.next(); 358 359 if (!prop.equals(END_MSG)) { 360 throw expected(END_MSG); 361 } 362 363 if ("UTF-8".equals(mBmsg.mBbodyCharset)) { 364 mBmsg.mMessage = remng.substring(0, messageLen); 365 } else { 366 mBmsg.mMessage = null; 367 } 368 } 369 370 prop = mParser.next(); 371 372 if (!prop.equals(END_BBODY)) { 373 throw expected(END_BBODY); 374 } 375 376 return mParser.next(); 377 } 378 extractVcard(StringBuilder out)379 private Property extractVcard(StringBuilder out) throws IOException, ParseException { 380 Property prop; 381 382 out.append(BEGIN_VCARD).append(CRLF); 383 384 do { 385 prop = mParser.next(); 386 out.append(prop).append(CRLF); 387 } while (!prop.equals(END_VCARD)); 388 389 return mParser.next(); 390 } 391 parseVcard(String str)392 private VCardEntry parseVcard(String str) throws IOException, ParseException { 393 VCardEntry vcard = null; 394 395 try { 396 VCardParser p = new VCardParser_V21(); 397 VCardEntryConstructor c = new VCardEntryConstructor(); 398 VcardHandler handler = new VcardHandler(); 399 c.addEntryHandler(handler); 400 p.addInterpreter(c); 401 p.parse(new ByteArrayInputStream(str.getBytes())); 402 403 vcard = handler.vcard; 404 405 } catch (VCardVersionException e1) { 406 try { 407 VCardParser p = new VCardParser_V30(); 408 VCardEntryConstructor c = new VCardEntryConstructor(); 409 VcardHandler handler = new VcardHandler(); 410 c.addEntryHandler(handler); 411 p.addInterpreter(c); 412 p.parse(new ByteArrayInputStream(str.getBytes())); 413 414 vcard = handler.vcard; 415 416 } catch (VCardVersionException e2) { 417 // will throw below 418 } catch (VCardException e2) { 419 // will throw below 420 } 421 422 } catch (VCardException e1) { 423 // will throw below 424 } 425 426 if (vcard == null) { 427 throw new ParseException("Cannot parse vCard object (neither 2.1 nor 3.0?)", 428 mParser.pos()); 429 } 430 431 return vcard; 432 } 433 434 private class VcardHandler implements VCardEntryHandler { 435 public VCardEntry vcard; 436 437 @Override onStart()438 public void onStart() { 439 } 440 441 @Override onEntryCreated(VCardEntry entry)442 public void onEntryCreated(VCardEntry entry) { 443 vcard = entry; 444 } 445 446 @Override onEnd()447 public void onEnd() { 448 } 449 } 450 } 451