• 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 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         if (!"UTF-8".equals(mBmsg.mBbodyCharset)) {
294             Log.e(TAG, "The charset was not set to charset UTF-8: " + mBmsg.mBbodyCharset);
295         }
296 
297         /*
298          * <bmessage-body-content>::={ "BEGIN:MSG"<CRLF> 'message'<CRLF>
299          * "END:MSG"<CRLF> }
300          */
301 
302         int messageLen = mBmsg.mBbodyLength - MSG_CONTAINER_LEN;
303         int offset = messageLen + CRLF_LEN;
304         int restartPos = mParser.pos() + offset;
305         /*
306          * length is specified in bytes so we need to convert from unicode
307          * string back to bytes array
308          */
309         String remng = mParser.remaining();
310         byte[] data = remng.getBytes();
311 
312         /* restart parsing from after 'message'<CRLF> */
313         mParser = new BmsgTokenizer(new String(data, offset, data.length - offset), restartPos);
314 
315         prop = mParser.next(true);
316 
317         if (prop != null) {
318             if (prop.equals(END_MSG)) {
319                 if ("UTF-8".equals(mBmsg.mBbodyCharset)) {
320                     mBmsg.mMessage = new String(data, 0, messageLen, StandardCharsets.UTF_8);
321                 } else {
322                     mBmsg.mMessage = null;
323                 }
324             } else {
325                 /* Handle possible exception for incorrect LENGTH value
326                  * from MSE while parsing  GET Message response */
327                 Log.e(TAG, "Prop Invalid: " + prop.toString());
328                 Log.e(TAG, "Possible Invalid LENGTH value");
329                 throw expected(END_MSG);
330             }
331         } else {
332             data = null;
333 
334             /*
335              * now we check if bMessage can be parsed if LENGTH is handled as
336              * number of characters instead of number of bytes
337              */
338             if (offset < 0 || offset > remng.length()) {
339                 /* Handle possible exception for incorrect LENGTH value
340                  * from MSE while parsing  GET Message response */
341                 throw new ParseException("Invalid LENGTH value", mParser.pos());
342             }
343 
344             Log.w(TAG, "byte LENGTH seems to be invalid, trying with char length");
345 
346             mParser = new BmsgTokenizer(remng.substring(offset));
347 
348             prop = mParser.next();
349 
350             if (!prop.equals(END_MSG)) {
351                 throw expected(END_MSG);
352             }
353 
354             if ("UTF-8".equals(mBmsg.mBbodyCharset)) {
355                 mBmsg.mMessage = remng.substring(0, messageLen);
356             } else {
357                 mBmsg.mMessage = null;
358             }
359         }
360 
361         prop = mParser.next();
362 
363         if (!prop.equals(END_BBODY)) {
364             throw expected(END_BBODY);
365         }
366 
367         return mParser.next();
368     }
369 
extractVcard(StringBuilder out)370     private Property extractVcard(StringBuilder out) throws IOException, ParseException {
371         Property prop;
372 
373         out.append(BEGIN_VCARD).append(CRLF);
374 
375         do {
376             prop = mParser.next();
377             out.append(prop).append(CRLF);
378         } while (!prop.equals(END_VCARD));
379 
380         return mParser.next();
381     }
382 
parseVcard(String str)383     private VCardEntry parseVcard(String str) throws IOException, ParseException {
384         VCardEntry vcard = null;
385 
386         try {
387             VCardParser p = new VCardParser_V21();
388             VCardEntryConstructor c = new VCardEntryConstructor();
389             VcardHandler handler = new VcardHandler();
390             c.addEntryHandler(handler);
391             p.addInterpreter(c);
392             p.parse(new ByteArrayInputStream(str.getBytes()));
393 
394             vcard = handler.vcard;
395 
396         } catch (VCardVersionException e1) {
397             try {
398                 VCardParser p = new VCardParser_V30();
399                 VCardEntryConstructor c = new VCardEntryConstructor();
400                 VcardHandler handler = new VcardHandler();
401                 c.addEntryHandler(handler);
402                 p.addInterpreter(c);
403                 p.parse(new ByteArrayInputStream(str.getBytes()));
404 
405                 vcard = handler.vcard;
406 
407             } catch (VCardVersionException e2) {
408                 // will throw below
409             } catch (VCardException e2) {
410                 // will throw below
411             }
412 
413         } catch (VCardException e1) {
414             // will throw below
415         }
416 
417         if (vcard == null) {
418             throw new ParseException("Cannot parse vCard object (neither 2.1 nor 3.0?)",
419                     mParser.pos());
420         }
421 
422         return vcard;
423     }
424 
425     private class VcardHandler implements VCardEntryHandler {
426         public VCardEntry vcard;
427 
428         @Override
onStart()429         public void onStart() {
430         }
431 
432         @Override
onEntryCreated(VCardEntry entry)433         public void onEntryCreated(VCardEntry entry) {
434             vcard = entry;
435         }
436 
437         @Override
onEnd()438         public void onEnd() {
439         }
440     }
441 }
442