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