1 /*
2  * Copyright (C) 2010 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 package com.android.vcard;
17 
18 import android.util.Log;
19 
20 import com.android.vcard.exception.VCardException;
21 
22 import java.io.IOException;
23 import java.util.Set;
24 
25 /**
26  * <p>
27  * Basic implementation achieving vCard 3.0 parsing.
28  * </p>
29  * <p>
30  * This class inherits vCard 2.1 implementation since technically they are similar,
31  * while specifically there's logical no relevance between them.
32  * So that developers are not confused with the inheritance,
33  * {@link VCardParser_V30} does not inherit {@link VCardParser_V21}, while
34  * {@link VCardParserImpl_V30} inherits {@link VCardParserImpl_V21}.
35  * </p>
36  * @hide
37  */
38 /* package */ class VCardParserImpl_V30 extends VCardParserImpl_V21 {
39     private static final String LOG_TAG = VCardConstants.LOG_TAG;
40 
41     private String mPreviousLine;
42     private boolean mEmittedAgentWarning = false;
43 
VCardParserImpl_V30()44     public VCardParserImpl_V30() {
45         super();
46     }
47 
VCardParserImpl_V30(int vcardType)48     public VCardParserImpl_V30(int vcardType) {
49         super(vcardType);
50     }
51 
52     @Override
getVersion()53     protected int getVersion() {
54         return VCardConfig.VERSION_30;
55     }
56 
57     @Override
getVersionString()58     protected String getVersionString() {
59         return VCardConstants.VERSION_V30;
60     }
61 
62     @Override
peekLine()63     protected String peekLine() throws IOException {
64         if (mPreviousLine != null) {
65             String ret = mPreviousLine;
66             return ret;
67         } else {
68             return mReader.peekLine();
69         }
70     }
71 
72     @Override
getLine()73     protected String getLine() throws IOException {
74         if (mPreviousLine != null) {
75             String ret = mPreviousLine;
76             mPreviousLine = null;
77             return ret;
78         } else {
79             return mReader.readLine();
80         }
81     }
82 
83     /**
84      * vCard 3.0 requires that the line with space at the beginning of the line
85      * must be combined with previous line.
86      */
87     @Override
getNonEmptyLine()88     protected String getNonEmptyLine() throws IOException, VCardException {
89         String line;
90         StringBuilder builder = null;
91         while ((line = mReader.readLine()) != null) {
92             // Skip empty lines in order to accomodate implementations that
93             // send line termination variations such as \r\r\n.
94             if (line.length() == 0) {
95                 continue;
96             } else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') {
97                 // RFC 2425 describes line continuation as \r\n followed by
98                 // a single ' ' or '\t' whitespace character.
99                 if (builder == null) {
100                     builder = new StringBuilder();
101                 }
102                 if (mPreviousLine != null) {
103                     builder.append(mPreviousLine);
104                     mPreviousLine = null;
105                 }
106                 builder.append(line.substring(1));
107             } else {
108                 if (builder != null || mPreviousLine != null) {
109                     break;
110                 }
111                 mPreviousLine = line;
112             }
113         }
114 
115         String ret = null;
116         if (builder != null) {
117             ret = builder.toString();
118         } else if (mPreviousLine != null) {
119             ret = mPreviousLine;
120         }
121         mPreviousLine = line;
122         if (ret == null) {
123             throw new VCardException("Reached end of buffer.");
124         }
125         return ret;
126     }
127 
128     /*
129      * vcard = [group "."] "BEGIN" ":" "VCARD" 1 * CRLF
130      *         1 * (contentline)
131      *         ;A vCard object MUST include the VERSION, FN and N types.
132      *         [group "."] "END" ":" "VCARD" 1 * CRLF
133      */
134     @Override
readBeginVCard(boolean allowGarbage)135     protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException {
136         // TODO: vCard 3.0 supports group.
137         return super.readBeginVCard(allowGarbage);
138     }
139 
140     /**
141      * vCard 3.0 allows iana-token as paramType, while vCard 2.1 does not.
142      */
143     @Override
handleParams(VCardProperty propertyData, final String params)144     protected void handleParams(VCardProperty propertyData, final String params)
145             throws VCardException {
146         try {
147             super.handleParams(propertyData, params);
148         } catch (VCardException e) {
149             // maybe IANA type
150             String[] strArray = params.split("=", 2);
151             if (strArray.length == 2) {
152                 handleAnyParam(propertyData, strArray[0], strArray[1]);
153             } else {
154                 // Must not come here in the current implementation.
155                 throw new VCardException(
156                         "Unknown params value: " + params);
157             }
158         }
159     }
160 
161     @Override
handleAnyParam( VCardProperty propertyData, final String paramName, final String paramValue)162     protected void handleAnyParam(
163             VCardProperty propertyData, final String paramName, final String paramValue) {
164         splitAndPutParam(propertyData, paramName, paramValue);
165     }
166 
167     @Override
handleParamWithoutName(VCardProperty property, final String paramValue)168     protected void handleParamWithoutName(VCardProperty property, final String paramValue) {
169         handleType(property, paramValue);
170     }
171 
172     /*
173      *  vCard 3.0 defines
174      *
175      *  param         = param-name "=" param-value *("," param-value)
176      *  param-name    = iana-token / x-name
177      *  param-value   = ptext / quoted-string
178      *  quoted-string = DQUOTE QSAFE-CHAR DQUOTE
179      *  QSAFE-CHAR    = WSP / %x21 / %x23-7E / NON-ASCII
180      *                ; Any character except CTLs, DQUOTE
181      *
182      *  QSAFE-CHAR must not contain DQUOTE, including escaped one (\").
183      */
184     @Override
handleType(VCardProperty property, final String paramValue)185     protected void handleType(VCardProperty property, final String paramValue) {
186         splitAndPutParam(property, VCardConstants.PARAM_TYPE, paramValue);
187     }
188 
189     /**
190      * Splits parameter values into pieces in accordance with vCard 3.0 specification and
191      * puts pieces into mInterpreter.
192      */
193     /*
194      *  param-value   = ptext / quoted-string
195      *  quoted-string = DQUOTE QSAFE-CHAR DQUOTE
196      *  QSAFE-CHAR    = WSP / %x21 / %x23-7E / NON-ASCII
197      *                ; Any character except CTLs, DQUOTE
198      *
199      *  QSAFE-CHAR must not contain DQUOTE, including escaped one (\")
200      */
splitAndPutParam(VCardProperty property, String paramName, String paramValue)201     private void splitAndPutParam(VCardProperty property, String paramName, String paramValue) {
202         // "comma,separated:inside.dquote",pref
203         //   -->
204         // - comma,separated:inside.dquote
205         // - pref
206         //
207         // Note: Though there's a code, we don't need to take much care of
208         // wrongly-added quotes like the example above, as they induce
209         // parse errors at the top level (when splitting a line into parts).
210         StringBuilder builder = null;  // Delay initialization.
211         boolean insideDquote = false;
212         final int length = paramValue.length();
213         for (int i = 0; i < length; i++) {
214             final char ch = paramValue.charAt(i);
215             if (ch == '"') {
216                 if (insideDquote) {
217                     // End of Dquote.
218                     property.addParameter(paramName, encodeParamValue(builder.toString()));
219                     builder = null;
220                     insideDquote = false;
221                 } else {
222                     if (builder != null) {
223                         if (builder.length() > 0) {
224                             // e.g.
225                             // pref"quoted"
226                             Log.w(LOG_TAG, "Unexpected Dquote inside property.");
227                         } else {
228                             // e.g.
229                             // pref,"quoted"
230                             // "quoted",pref
231                             property.addParameter(paramName, encodeParamValue(builder.toString()));
232                         }
233                     }
234                     insideDquote = true;
235                 }
236             } else if (ch == ',' && !insideDquote) {
237                 if (builder == null) {
238                     Log.w(LOG_TAG, "Comma is used before actual string comes. (" +
239                             paramValue + ")");
240                 } else {
241                     property.addParameter(paramName, encodeParamValue(builder.toString()));
242                     builder = null;
243                 }
244             } else {
245                 // To stop creating empty StringBuffer at the end of parameter,
246                 // we delay creating this object until this point.
247                 if (builder == null) {
248                     builder = new StringBuilder();
249                 }
250                 builder.append(ch);
251             }
252         }
253         if (insideDquote) {
254             // e.g.
255             // "non-quote-at-end
256             Log.d(LOG_TAG, "Dangling Dquote.");
257         }
258         if (builder != null) {
259             if (builder.length() == 0) {
260                 Log.w(LOG_TAG, "Unintended behavior. We must not see empty StringBuilder " +
261                         "at the end of parameter value parsing.");
262             } else {
263                 property.addParameter(paramName, encodeParamValue(builder.toString()));
264             }
265         }
266     }
267 
268     /**
269      * Encode a param value using UTF-8.
270      */
encodeParamValue(String paramValue)271     protected String encodeParamValue(String paramValue) {
272         return VCardUtils.convertStringCharset(
273                 paramValue, VCardConfig.DEFAULT_INTERMEDIATE_CHARSET, "UTF-8");
274     }
275 
276     @Override
handleAgent(VCardProperty property)277     protected void handleAgent(VCardProperty property) {
278         // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.1.
279         //
280         // e.g.
281         // AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n
282         //  TITLE:Area Administrator\, Assistant\n EMAIL\;TYPE=INTERN\n
283         //  ET:jfriday@host.com\nEND:VCARD\n
284         //
285         // TODO: fix this.
286         //
287         // issue:
288         //  vCard 3.0 also allows this as an example.
289         //
290         // AGENT;VALUE=uri:
291         //  CID:JQPUBLIC.part3.960129T083020.xyzMail@host3.com
292         //
293         // This is not vCard. Should we support this?
294         //
295         // Just ignore the line for now, since we cannot know how to handle it...
296         if (!mEmittedAgentWarning) {
297             Log.w(LOG_TAG, "AGENT in vCard 3.0 is not supported yet. Ignore it");
298             mEmittedAgentWarning = true;
299         }
300     }
301 
302     /**
303      * This is only called from handlePropertyValue(), which has already
304      * read the first line of this property. With v3.0, the getNonEmptyLine()
305      * routine has already concatenated all following continuation lines.
306      * The routine is implemented in the V21 parser to concatenate v2.1 style
307      * data blocks, but is unnecessary here.
308      */
309     @Override
getBase64(final String firstString)310     protected String getBase64(final String firstString)
311             throws IOException, VCardException {
312         return firstString;
313     }
314 
315     /**
316      * ESCAPED-CHAR = "\\" / "\;" / "\," / "\n" / "\N")
317      *              ; \\ encodes \, \n or \N encodes newline
318      *              ; \; encodes ;, \, encodes ,
319      *
320      * Note: Apple escapes ':' into '\:' while does not escape '\'
321      */
322     @Override
maybeUnescapeText(final String text)323     protected String maybeUnescapeText(final String text) {
324         return unescapeText(text);
325     }
326 
unescapeText(final String text)327     public static String unescapeText(final String text) {
328         StringBuilder builder = new StringBuilder();
329         final int length = text.length();
330         for (int i = 0; i < length; i++) {
331             char ch = text.charAt(i);
332             if (ch == '\\' && i < length - 1) {
333                 final char next_ch = text.charAt(++i);
334                 if (next_ch == 'n' || next_ch == 'N') {
335                     builder.append("\n");
336                 } else {
337                     builder.append(next_ch);
338                 }
339             } else {
340                 builder.append(ch);
341             }
342         }
343         return builder.toString();
344     }
345 
346     @Override
maybeUnescapeCharacter(final char ch)347     protected String maybeUnescapeCharacter(final char ch) {
348         return unescapeCharacter(ch);
349     }
350 
unescapeCharacter(final char ch)351     public static String unescapeCharacter(final char ch) {
352         if (ch == 'n' || ch == 'N') {
353             return "\n";
354         } else {
355             return String.valueOf(ch);
356         }
357     }
358 
359     @Override
getKnownPropertyNameSet()360     protected Set<String> getKnownPropertyNameSet() {
361         return VCardParser_V30.sKnownPropertyNameSet;
362     }
363 }
364