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