• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 final static String TAG = "BmessageParser";
39     private final static boolean DBG = false;
40 
41     private final static String CRLF = "\r\n";
42 
43     private final static Property BEGIN_BMSG = new Property("BEGIN", "BMSG");
44     private final static Property END_BMSG = new Property("END", "BMSG");
45 
46     private final static Property BEGIN_VCARD = new Property("BEGIN", "VCARD");
47     private final static Property END_VCARD = new Property("END", "VCARD");
48 
49     private final static Property BEGIN_BENV = new Property("BEGIN", "BENV");
50     private final static Property END_BENV = new Property("END", "BENV");
51 
52     private final static Property BEGIN_BBODY = new Property("BEGIN", "BBODY");
53     private final static Property END_BBODY = new Property("END", "BBODY");
54 
55     private final static Property BEGIN_MSG = new Property("BEGIN", "MSG");
56     private final static Property END_MSG = new Property("END", "MSG");
57 
58     private final static 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 final static 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     static public 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             } else if (prop.name.equals("ENCODING")) {
266                 mBmsg.mBbodyEncoding = prop.value;
267 
268             } else if (prop.name.equals("CHARSET")) {
269                 mBmsg.mBbodyCharset = prop.value;
270 
271             } else if (prop.name.equals("LANGUAGE")) {
272                 mBmsg.mBbodyLanguage = prop.value;
273 
274             } else if (prop.name.equals("LENGTH")) {
275                 try {
276                     mBmsg.mBbodyLength = Integer.parseInt(prop.value);
277                 } catch (NumberFormatException e) {
278                     throw new ParseException("Invalid LENGTH value", mParser.pos());
279                 }
280 
281             }
282 
283         } while (!prop.equals(BEGIN_MSG));
284 
285         /*
286          * check that the charset is always set to UTF-8. We expect only text transfer (in lieu with
287          * the MAPv12 specifying only RFC2822 (text only) for MMS/EMAIL and SMS do not support
288          * non-text content. If the charset is not set to UTF-8, it is safe to set the message as
289          * empty. We force the getMessage (see Client) to only call getMessage with
290          * UTF-8 as the MCE is not obliged to support native charset.
291          */
292         if (!"UTF-8".equals(mBmsg.mBbodyCharset)) {
293             Log.e(TAG, "The charset was not set to charset UTF-8: " + mBmsg.mBbodyCharset);
294         }
295 
296         /*
297          * <bmessage-body-content>::={ "BEGIN:MSG"<CRLF> 'message'<CRLF>
298          * "END:MSG"<CRLF> }
299          */
300 
301         int messageLen = mBmsg.mBbodyLength - MSG_CONTAINER_LEN;
302         int offset = messageLen + CRLF_LEN;
303         int restartPos = mParser.pos() + offset;
304         /*
305          * length is specified in bytes so we need to convert from unicode
306          * string back to bytes array
307          */
308         String remng = mParser.remaining();
309         byte[] data = remng.getBytes();
310 
311         /* restart parsing from after 'message'<CRLF> */
312         mParser = new BmsgTokenizer(new String(data, offset, data.length - offset), restartPos);
313 
314         prop = mParser.next(true);
315 
316         if (prop != null) {
317             if (prop.equals(END_MSG)) {
318                 if ("UTF-8".equals(mBmsg.mBbodyCharset)) {
319                     mBmsg.mMessage = new String(data, 0, messageLen, StandardCharsets.UTF_8);
320                 } else {
321                     mBmsg.mMessage = null;
322                 }
323             } else {
324                 /* Handle possible exception for incorrect LENGTH value
325                  * from MSE while parsing  GET Message response */
326                 Log.e(TAG, "Prop Invalid: " + prop.toString());
327                 Log.e(TAG, "Possible Invalid LENGTH value");
328                 throw expected(END_MSG);
329             }
330         } else {
331             data = null;
332 
333             /*
334              * now we check if bMessage can be parsed if LENGTH is handled as
335              * number of characters instead of number of bytes
336              */
337             if (offset < 0 || offset > remng.length()) {
338                 /* Handle possible exception for incorrect LENGTH value
339                  * from MSE while parsing  GET Message response */
340                 throw new ParseException("Invalid LENGTH value", mParser.pos());
341             }
342 
343             Log.w(TAG, "byte LENGTH seems to be invalid, trying with char length");
344 
345             mParser = new BmsgTokenizer(remng.substring(offset));
346 
347             prop = mParser.next();
348 
349             if (!prop.equals(END_MSG)) {
350                 throw expected(END_MSG);
351             }
352 
353             if ("UTF-8".equals(mBmsg.mBbodyCharset)) {
354                 mBmsg.mMessage = remng.substring(0, messageLen);
355             } else {
356                 mBmsg.mMessage = null;
357             }
358         }
359 
360         prop = mParser.next();
361 
362         if (!prop.equals(END_BBODY)) {
363             throw expected(END_BBODY);
364         }
365 
366         return mParser.next();
367     }
368 
extractVcard(StringBuilder out)369     private Property extractVcard(StringBuilder out) throws IOException, ParseException {
370         Property prop;
371 
372         out.append(BEGIN_VCARD).append(CRLF);
373 
374         do {
375             prop = mParser.next();
376             out.append(prop).append(CRLF);
377         } while (!prop.equals(END_VCARD));
378 
379         return mParser.next();
380     }
381 
parseVcard(String str)382     private VCardEntry parseVcard(String str) throws IOException, ParseException {
383         VCardEntry vcard = null;
384 
385         try {
386             VCardParser p = new VCardParser_V21();
387             VCardEntryConstructor c = new VCardEntryConstructor();
388             VcardHandler handler = new VcardHandler();
389             c.addEntryHandler(handler);
390             p.addInterpreter(c);
391             p.parse(new ByteArrayInputStream(str.getBytes()));
392 
393             vcard = handler.vcard;
394 
395         } catch (VCardVersionException e1) {
396             try {
397                 VCardParser p = new VCardParser_V30();
398                 VCardEntryConstructor c = new VCardEntryConstructor();
399                 VcardHandler handler = new VcardHandler();
400                 c.addEntryHandler(handler);
401                 p.addInterpreter(c);
402                 p.parse(new ByteArrayInputStream(str.getBytes()));
403 
404                 vcard = handler.vcard;
405 
406             } catch (VCardVersionException e2) {
407                 // will throw below
408             } catch (VCardException e2) {
409                 // will throw below
410             }
411 
412         } catch (VCardException e1) {
413             // will throw below
414         }
415 
416         if (vcard == null) {
417             throw new ParseException("Cannot parse vCard object (neither 2.1 nor 3.0?)",
418                     mParser.pos());
419         }
420 
421         return vcard;
422     }
423 
424     private class VcardHandler implements VCardEntryHandler {
425         VCardEntry vcard;
426 
427         @Override
onStart()428         public void onStart() {
429         }
430 
431         @Override
onEntryCreated(VCardEntry entry)432         public void onEntryCreated(VCardEntry entry) {
433             vcard = entry;
434         }
435 
436         @Override
onEnd()437         public void onEnd() {
438         }
439     }
440 }
441