Fix Parser crashes on unknown tags/pages

Fix Parser so doesn't crash when sees an unknown tag on a known code page.
Check for unknown code page and throw parse exception.  Remove unused code.
Make class variables private where unused outside class.  Change tag stack
to ArrayDeque from fixed size static array. Check for unsupported WBXML
features: string tables, unsupported global tokens, and attributes. Throw
EasParserExceptions for all parse errors so Exchange can catch them and not
crash. Add unit tests.

Bug:14651154
Bug:14673019
Change-Id: If71be29391466985eccff90db8601bbdfde2658b
diff --git a/src/com/android/exchange/adapter/Parser.java b/src/com/android/exchange/adapter/Parser.java
index aced374..5830436 100644
--- a/src/com/android/exchange/adapter/Parser.java
+++ b/src/com/android/exchange/adapter/Parser.java
@@ -30,11 +30,26 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
 
 /**
  * Extremely fast and lightweight WBXML parser, implementing only the subset of WBXML that
- * EAS uses (as defined in the EAS specification)
+ * EAS uses (as defined in the EAS specification).
+ *
+ * Supports:
+ *      WBXML tokens to encode XML tags
+ *      WBXML code pages to support multiple XML namespaces
+ *      Inline strings
+ *      Opaque data
+ *
+ * Does not support: (throws EasParserException)
+ *      String tables
+ *      Entities
+ *      Processing instructions
+ *      Attribute encoding
  *
  */
 public abstract class Parser {
@@ -48,8 +63,8 @@
     public static final int START = 2;
     public static final int END = 3;
     public static final int TEXT = 4;
+    public static final int OPAQUE = 5;
     public static final int END_DOCUMENT = 3;
-    private static final int NOT_FETCHED = Integer.MIN_VALUE;
     private static final int NOT_ENDED = Integer.MIN_VALUE;
     private static final int EOF_BYTE = -1;
 
@@ -67,9 +82,6 @@
     // The current tag depth
     private int depth;
 
-    // The upcoming (saved) id from the stream
-    private int nextId = NOT_FETCHED;
-
     // The current tag table (i.e. the tag table for the current page)
     private String[] tagTable;
 
@@ -79,39 +91,51 @@
     // The stack of names of tags being processed; used when debug = true
     private String[] nameArray = new String[32];
 
+    public class Tag {
+        public int index;
+        // Whether the tag is associated with content (a value)
+        public boolean noContent;
+        public String name;
+
+        public Tag(int id) {
+            // The tag is in the low 6 bits
+            index = id & Tags.PAGE_MASK;
+            // If the high bit is set, there is content (a value) to be read
+            noContent = (id & Wbxml.WITH_CONTENT) == 0;
+            if (index < TAG_BASE) {
+                name = "unsupported-WBXML";
+            } else if (tagTable == null || index - TAG_BASE >= tagTable.length) {
+                name = "unknown";
+            } else {
+                name = tagTable[index - TAG_BASE];
+            }
+        }
+    }
+
     // The stack of tags being processed
-    private int[] startTagArray = new int[32];
+    private Deque<Tag> startTagArray = new ArrayDeque<Tag>();
 
-    // The following vars are available to all to avoid method calls that represent the state of
-    // the parser at any given time
-    public int endTag = NOT_ENDED;
+    private Tag startTag;
 
-    public int startTag;
+    private Tag endTag;
 
     // The type of the last token read
-    public int type;
+    private int type;
 
     // The current page
-    public int page;
+    private int page;
 
     // The current tag
     public int tag;
 
-    // The name of the current tag
-    public String name;
-
     // Whether the current tag is associated with content (a value)
     public boolean noContent;
 
-    // The value read, as a String.  Only one of text or num will be valid, depending on whether the
-    // value was requested as a String or an int (to avoid wasted effort in parsing)
-    public String text;
-
-    // The value read, as an int
-    public int num;
+    // The value read, as a String
+    private String text;
 
     // The value read, as bytes
-    public byte[] bytes;
+    private byte[] bytes;
 
     // TODO: Define a new parse exception type rather than lumping these in as IOExceptions.
 
@@ -185,6 +209,7 @@
      *
      * @param val the desired state for debug output
      */
+    @VisibleForTesting
     public void setDebug(boolean val) {
         logging = val;
     }
@@ -218,67 +243,88 @@
     }
 
     /**
-     * Return the value of the current tag, as a byte array.  Note that the result of this call
-     * is indeterminate, and possibly null, if the value of the tag is not a byte array
+     * Return the value of the current tag, as a byte array. Throws EasParserException
+     * if neither opaque nor text data is present. Never returns null--returns
+     * an empty byte[] array for empty data.
      *
      * @return the byte array value of the current tag
      * @throws IOException
      */
     public byte[] getValueBytes() throws IOException {
-        getValue();
-        return bytes;
+        final String name = startTag.name;
+
+        getNext();
+        // This means there was no value given, just <Foo/>; we'll return empty array
+        if (type == END) {
+            log("No value for tag: " + name);
+            return new byte[0];
+        } else if (type != OPAQUE && type != TEXT) {
+            throw new EasParserException("Expected OPAQUE or TEXT data for tag " + name);
+        }
+
+        // Save the value
+        final byte[] val = type == OPAQUE ? bytes : text.getBytes("UTF-8");
+        // Read the next token; it had better be the end of the current tag
+        getNext();
+        // If not, throw an exception
+        if (type != END) {
+            throw new EasParserException("No END found for tag " + name);
+        }
+        return val;
     }
 
     /**
-     * Return the value of the current tag, as a String.  Note that the result of this call is
-     * indeterminate, and possibly null, if the value of the tag is not an immediate string
+     * Return the value of the current tag, as a String. Throws EasParserException
+     * for non-text data. Never returns null--returns an empty string if no data.
      *
      * @return the String value of the current tag
      * @throws IOException
      */
     public String getValue() throws IOException {
-        // The false argument tells getNext to return the value as a String
-        getNext(false);
+        final String name = startTag.name;
+
+        getNext();
         // This means there was no value given, just <Foo/>; we'll return empty string for now
         if (type == END) {
-            if (logging) {
-                log("No value for tag: " + tagTable[startTag - TAG_BASE]);
-            }
+            log("No value for tag: " + name);
             return "";
+        } else if (type != TEXT) {
+            throw new EasParserException("Expected TEXT data for tag " + name);
         }
+
         // Save the value
-        String val = text;
+        final String val = text;
         // Read the next token; it had better be the end of the current tag
-        getNext(false);
+        getNext();
         // If not, throw an exception
         if (type != END) {
-            throw new IOException("No END found!");
+            throw new EasParserException("No END found for tag " + name);
         }
         return val;
     }
 
     /**
-     * Return the value of the current tag, as an integer.  Note that the value of this call is
-     * indeterminate if the value of this tag is not an immediate string parsed as an integer
+     * Return the value of the current tag, as an integer. Throws EasParserException
+     * for non text data, and text data that doesn't parse as an integer. Returns
+     * 0 for empty data.
      *
      * @return the integer value of the current tag
      * @throws IOException
      */
-   public int getValueInt() throws IOException {
-        // The true argument to getNext indicates the desire for an integer return value
-        getNext(true);
-        if (type == END) {
+    public int getValueInt() throws IOException {
+        String val = getValue();
+        if (val.length() == 0) {
             return 0;
         }
-        // Save the value
-        int val = num;
-        // Read the next token; it had better be the end of the current tag
-        getNext(false);
-        // If not, throw an exception
-        if (type != END) {
-            throw new IOException("No END found!");
+
+        final String name = startTag.name;
+        int num;
+        try {
+            num = Integer.parseInt(val);
+        } catch (NumberFormatException e) {
+            throw new EasParserException("Tag " + name + ": " + e.getMessage());
         }
-        return val;
+        return num;
     }
 
     /**
@@ -294,19 +340,19 @@
      */
     public int nextTag(int endingTag) throws IOException {
         // Lose the page information
-        endTag = endingTag &= Tags.PAGE_MASK;
-        while (getNext(false) != DONE) {
+        endTag = new Tag(endingTag);
+        while (getNext() != DONE) {
             // If we're a start, set tag to include the page and return it
             if (type == START) {
-                tag = page | startTag;
+                tag = page | startTag.index;
                 return tag;
             // If we're at the ending tag we're looking for, return the END signal
-            } else if (type == END && startTag == endTag) {
+            } else if (type == END && startTag.index == endTag.index) {
                 return END;
             }
         }
         // We're at end of document here.  If we're looking for it, return END_DOCUMENT
-        if (endTag == START_DOCUMENT) {
+        if (endTag.index == START_DOCUMENT) {
             return END_DOCUMENT;
         }
         // Otherwise, we've prematurely hit end of document, so exception out
@@ -322,10 +368,10 @@
      * @throws IOException
      */
     public void skipTag() throws IOException {
-        int thisTag = startTag;
+        int thisTag = startTag.index;
         // Just loop until we hit the end of the current tag
-        while (getNext(false) != DONE) {
-            if (type == END && startTag == thisTag) {
+        while (getNext() != DONE) {
+            if (type == END && startTag.index == thisTag) {
                 return;
             }
         }
@@ -335,17 +381,6 @@
     }
 
     /**
-     * Retrieve the next token from the input stream
-     *
-     * @return the token found
-     * @throws IOException
-     */
-    public int nextToken() throws IOException {
-        getNext(false);
-        return type;
-    }
-
-    /**
      * Initializes the parser with an input stream; reads the first 4 bytes (which are always the
      * same in EAS, and then sets the tag table to point to page 0 (by definition, the starting
      * page).
@@ -358,13 +393,19 @@
         if ((in != null) && initialize) {
             // If we fail on the very first byte, report an empty stream
             try {
-                readByte(); // version
+                final int version = readByte(); // version
+                if (version > 3) {
+                    log("WBXML version " + version + " is newer than supported version 3");
+                }
             } catch (EofException e) {
                 throw new EmptyStreamException();
             }
-            readInt();  // ?
+            readInt();  // public identifier
             readInt();  // 106 (UTF-8)
-            readInt();  // string table length
+            final int stringTableLength = readInt();  // string table length
+            if (stringTableLength != 0) {
+                throw new EasParserException("WBXML string table unsupported");
+            }
         }
         tagTable = tagTables[0];
     }
@@ -380,16 +421,28 @@
     }
 
     void log(String str) {
+        if (!logging) {
+            return;
+        }
         int cr = str.indexOf('\n');
         if (cr > 0) {
             str = str.substring(0, cr);
         }
-        LogUtils.v(LOG_TAG, str);
+        final char [] charArray = new char[startTagArray.size() * 2];
+        Arrays.fill(charArray, ' ');
+        final String indent = new String(charArray);
+        LogUtils.v(LOG_TAG, indent + str);
         if (Eas.FILE_LOG) {
             FileLogger.log(LOG_TAG, str);
         }
     }
 
+    void logVerbose(final String str) {
+        if (LOG_VERBOSE) {
+            log(str);
+        }
+    }
+
     protected void pushTag(int id) {
         page = id >> Tags.PAGE_SHIFT;
         tagTable = tagTables[page];
@@ -397,28 +450,17 @@
     }
 
     private void pop() {
-        if (logging) {
-            name = nameArray[depth];
-            log("</" + name + '>');
-        }
         // Retrieve the now-current startTag from our stack
-        startTag = endTag = startTagArray[depth];
-        depth--;
+        startTag = endTag = startTagArray.removeFirst();
+        log("</" + startTag.name + '>');
     }
 
     private void push(int id) {
-        // The tag is in the low 6 bits
-        startTag = id & 0x3F;
-        // If the high bit is set, there is content (a value) to be read
-        noContent = (id & 0x40) == 0;
-        depth++;
-        if (logging) {
-            name = tagTable[startTag - TAG_BASE];
-            nameArray[depth] = name;
-            log("<" + name + (noContent ? '/' : "") + '>');
-        }
+        startTag = new Tag(id);
+        noContent = startTag.noContent;
+        log("<" + startTag.name + (startTag.noContent ? '/' : "") + '>');
         // Save the startTag to our stack
-        startTagArray[depth] = startTag;
+        startTagArray.addFirst(startTag);
     }
 
     /**
@@ -426,36 +468,37 @@
      * that has been retrieved - START (start of tag), END (end of tag), DONE (end of stream), or
      * TEXT (the value of a tag)
      *
-     * @param asInt whether a TEXT value should be parsed as a String or an int.
      * @return the type of data retrieved
      * @throws IOException
      */
-    private final int getNext(boolean asInt) throws IOException {
+    private final int getNext() throws IOException {
+        bytes = null;
+        text = null;
+
         if (noContent) {
-            nameArray[depth--] = null;
+            startTagArray.removeFirst();
             type = END;
             noContent = false;
             return type;
         }
 
-        text = null;
-        name = null;
-
-        int id = nextId ();
+        int id = read();
         while (id == Wbxml.SWITCH_PAGE) {
-            nextId = NOT_FETCHED;
             // Get the new page number
             int pg = readByte();
             // Save the shifted page to add into the startTag in nextTag
             page = pg << Tags.PAGE_SHIFT;
-            if (LOG_VERBOSE) {
-                log("Page: " + page);
-            }
             // Retrieve the current tag table
-            tagTable = tagTables[pg];
-            id = nextId();
+            if (pg >= tagTables.length) {
+                // Unknown code page. These seem to happen mostly because of
+                // invalid data from the server so throw an exception here.
+                throw new EasParserException("Unknown code page " + pg);
+            } else {
+                tagTable = tagTables[pg];
+            }
+            logVerbose((tagTable == null ? "Unknown " : "") + "Page: " + page);
+            id = read();
         }
-        nextId = NOT_FETCHED;
 
         switch (id) {
             case EOF_BYTE:
@@ -471,31 +514,30 @@
             case Wbxml.STR_I:
                 // Inline string
                 type = TEXT;
-                if (asInt) {
-                    num = readInlineInt();
-                } else {
-                    text = readInlineString();
-                }
-                if (logging) {
-                    name = tagTable[startTag - TAG_BASE];
-                    log(name + ": " + (asInt ? Integer.toString(num) : text));
-                }
+                text = readInlineString();
+                log(startTag.name + ": " + text);
                 break;
 
             case Wbxml.OPAQUE:
                 // Integer length + opaque data
+                type = OPAQUE;
                 int length = readInt();
                 bytes = new byte[length];
                 for (int i = 0; i < length; i++) {
                     bytes[i] = (byte)readByte();
                 }
-                if (logging) {
-                    name = tagTable[startTag - TAG_BASE];
-                    log(name + ": (opaque:" + length + ") ");
-                }
+                log(startTag.name + ": (opaque:" + length + ") ");
                 break;
 
             default:
+                if ((id & Tags.PAGE_MASK) < TAG_BASE) {
+                    throw new EasParserException(String.format(
+                                    "Unhandled WBXML global token 0x%02X", id));
+                }
+                if ((id & Wbxml.WITH_ATTRIBUTES) != 0) {
+                    throw new EasParserException(String.format(
+                                    "Attributes unsupported, tag 0x%02X", id));
+                }
                 type = START;
                 push(id);
         }
@@ -517,19 +559,10 @@
         if (capture) {
             captureArray.add(i);
         }
-        if (LOG_VERBOSE) {
-            log("Byte: " + i);
-        }
+        logVerbose("Byte: " + i);
         return i;
     }
 
-    private int nextId() throws IOException {
-        if (nextId == NOT_FETCHED) {
-            nextId = read();
-        }
-        return nextId;
-    }
-
     private int readByte() throws IOException {
         int i = read();
         if (i == EOF_BYTE) {
@@ -538,31 +571,6 @@
         return i;
     }
 
-    /**
-     * Read an integer from the stream; this is called when the parser knows that what follows is
-     * an inline string representing an integer (e.g. the Read tag in Email has a value known to
-     * be either "0" or "1")
-     *
-     * @return the integer as parsed from the stream
-     * @throws IOException
-     */
-    private int readInlineInt() throws IOException {
-        int result = 0;
-
-        while (true) {
-            int i = readByte();
-            // Inline strings are always terminated with a zero byte
-            if (i == 0) {
-                return result;
-            }
-            if (i >= '0' && i <= '9') {
-                result = (result * 10) + (i - '0');
-            } else {
-                throw new IOException("Non integer");
-            }
-        }
-    }
-
     private int readInt() throws IOException {
         int result = 0;
         int i;
diff --git a/src/com/android/exchange/adapter/Wbxml.java b/src/com/android/exchange/adapter/Wbxml.java
index b8ae789..d96b928 100644
--- a/src/com/android/exchange/adapter/Wbxml.java
+++ b/src/com/android/exchange/adapter/Wbxml.java
@@ -44,8 +44,9 @@
     static public final int EXT_0 = 0x0c0;
     static public final int EXT_1 = 0x0c1;
     static public final int EXT_2 = 0x0c2;
-    static public final int OPAQUE = 0x0c3; 
+    static public final int OPAQUE = 0x0c3;
     static public final int LITERAL_AC = 0x0c4;
 
+    static public final int WITH_ATTRIBUTES = 0x80;
     static public final int WITH_CONTENT = 0x40;
 }
diff --git a/tests/src/com/android/exchange/adapter/FolderSyncParserTests.java b/tests/src/com/android/exchange/adapter/FolderSyncParserTests.java
index 3afc299..51ddbf4 100644
--- a/tests/src/com/android/exchange/adapter/FolderSyncParserTests.java
+++ b/tests/src/com/android/exchange/adapter/FolderSyncParserTests.java
@@ -221,7 +221,7 @@
                 return line.substring(start + 2).trim();
             } finally {
                 if (nextTag(0) != END) {
-                    throw new IOException("Value not followed by end tag: " + name);
+                    throw new IOException("Value not followed by end tag");
                 }
             }
         }
diff --git a/tests/src/com/android/exchange/adapter/ParserTest.java b/tests/src/com/android/exchange/adapter/ParserTest.java
new file mode 100644
index 0000000..b643623
--- /dev/null
+++ b/tests/src/com/android/exchange/adapter/ParserTest.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.TextUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.util.Deque;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+
+@SmallTest
+public class ParserTest extends AndroidTestCase {
+    public class TestParser extends Parser {
+        private Deque<Object> mExpectedData;
+
+        public TestParser(InputStream in, Object[] expectedData) throws IOException{
+            super(in);
+            setDebug(true);
+            mExpectedData = expectedData == null ? null
+                : new ArrayDeque<Object>(Arrays.asList(expectedData));
+        }
+
+        @Override
+        public boolean parse() throws IOException {
+            int tag;
+            while((tag = nextTag(START_DOCUMENT)) != END_DOCUMENT) {
+                if (tag == 0x0B) {
+                    final String strVal = getValue();
+                    if (mExpectedData != null) {
+                        final String expectedStrVal = (String) mExpectedData.removeFirst();
+                        assertEquals(expectedStrVal, strVal);
+                    }
+                } else if (tag == 0x0C) {
+                    final int intVal = getValueInt();
+                    if (mExpectedData != null) {
+                        final Integer expectedIntVal = (Integer) mExpectedData.removeFirst();
+                        assertEquals(expectedIntVal.intValue(), intVal);
+                    }
+                } else if (tag == 0x0D) {
+                    final byte[] bytes = getValueBytes();
+                    if (mExpectedData != null) {
+                        final String expectedHexStr = stripString((String) mExpectedData.removeFirst());
+                        final String hexStr = byteArrayToHexString(bytes);
+                        assertEquals(expectedHexStr, hexStr);
+                    }
+                }
+            }
+            return true;
+        }
+    }
+
+    final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
+    private static String byteArrayToHexString(byte[] bytes) {
+        char[] hexChars = new char[bytes.length * 2];
+        for ( int j = 0; j < bytes.length; j++ ) {
+            int v = bytes[j] & 0xFF;
+            hexChars[j * 2] = hexArray[v >>> 4];
+            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+        }
+        return new String(hexChars);
+    }
+
+    private static String stripString(String str) {
+        return TextUtils.join("", str.split(" "));
+    }
+
+    private static byte[] hexStringToByteArray(String hexStr) {
+        final String s = TextUtils.join("", hexStr.split(" "));
+        final int len = s.length();
+        final byte[] data = new byte[len / 2];
+        for (int i = 0; i < len; i += 2) {
+            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+                    + Character.digit(s.charAt(i+1), 16));
+        }
+        return data;
+    }
+
+    private static InputStream getTestInputStream(final String hexStr) {
+        final byte[] byteArray = hexStringToByteArray(hexStr);
+        return new ByteArrayInputStream(byteArray);
+    }
+
+    private void testParserHelper(String wbxmlStr) throws Exception {
+        testParserHelper(wbxmlStr, null);
+    }
+
+    private void testParserHelper(String wbxmlStr, Object[] expectedData) throws Exception {
+        final Parser parser = new TestParser(getTestInputStream(wbxmlStr), expectedData);
+        parser.parse();
+    }
+
+    @SmallTest
+    public void testParser() throws Exception {
+        // Test parser with sample valid data
+        final String wbxmlDataStr =
+            "03 01 6A 00 45 5C 4F 50 03 43 6F 6E 74 61 63 74 73 00 01 4B 03 32 00 01 52 03 32 00 01 4E 03 " +
+            "31 00 01 56 47 4D 03 32 3A 31 00 01 5D 00 11 4A 46 03 31 00 01 4C 03 30 00 01 4D 03 31 00 01 " +
+            "01 00 01 5E 03 46 75 6E 6B 2C 20 44 6F 6E 00 01 5F 03 44 6F 6E 00 01 69 03 46 75 6E 6B 00 01 " +
+            "00 11 56 03 31 00 01 01 01 01 01 01 01";
+        testParserHelper(wbxmlDataStr);
+    }
+
+    @SmallTest
+    public void testUnsupportedWbxmlTag() throws Exception {
+        // Test parser with unsupported Wbxml tag (EXT_2 = 0xC2)
+        final String unsupportedWbxmlTag = "03 01 6A 00 45 5F C2 05 11 22 33 44 00 01 01";
+        try {
+            testParserHelper(unsupportedWbxmlTag);
+            fail("Expected EasParserException for unsupported tag 0xC2");
+        } catch (Parser.EasParserException e) {
+            // expected
+        }
+    }
+
+    @SmallTest
+    public void testUnknownCodePage() throws Exception {
+        // Test parser with non existent code page 64 (0x40)
+        final String unknownCodePage = "03 01 6A 00 45 00 40 4A 03 31 00 01 01";
+        try {
+            testParserHelper(unknownCodePage);
+            fail("Expected EasParserException for unknown code page 64");
+        } catch (Parser.EasParserException e) {
+            // expected
+        }
+    }
+
+    @SmallTest
+    public void testUnknownTag() throws Exception {
+        // Test parser with valid code page (0x00) but non existent tag (0x3F)
+        final String unknownTag = "03 01 6A 00 45 7F 03 31 00 01 01";
+        testParserHelper(unknownTag);
+    }
+
+    @SmallTest
+    public void testTextParsing() throws Exception {
+        // Expect text; has text data "DF"
+        final String textTagWithTextData = "03 01 6A 00 45 4B 03 44 46 00 01 01";
+        testParserHelper(textTagWithTextData, new Object[] {"DF"});
+
+        // Expect text; has tag with no content: <Tag/>
+        final String textTagNoContent = "03 01 6A 00 45 0B 01";
+        testParserHelper(textTagNoContent, new Object[] {""});
+
+        // Expect text; has tag and end tag with no value: <Tag></Tag>
+        final String emptyTextTag = "03 01 6A 00 45 4B 01 01";
+        testParserHelper(emptyTextTag, new Object[] {""});
+
+        // Expect text; has opaque data {0x11, 0x22, 0x33}
+        final String textTagWithOpaqueData = "03 01 6A 00 45 4B C3 03 11 22 33 01 01";
+        try {
+            testParserHelper(textTagWithOpaqueData);
+            fail("Expected EasParserException for trying to read opaque data as text");
+        } catch (Parser.EasParserException e) {
+            // expected
+        }
+    }
+
+    @SmallTest
+    public void testIntegerStringParsing() throws Exception {
+        // Expect int; has text data "1"
+        final String intTagWithIntData = "03 01 6A 00 45 4C 03 31 00 01 01";
+        testParserHelper(intTagWithIntData, new Object[] {1});
+
+        // Expect int; has tag with no content: <Tag/>
+        final String intTagNoContent = "03 01 6A 00 45 0C 01";
+        testParserHelper(intTagNoContent, new Object[] {0});
+
+        // Expect int; has tag and end tag with no value: <Tag></Tag>
+        final String emptyIntTag = "03 01 6A 00 45 4C 01 01";
+        testParserHelper(emptyIntTag, new Object[] {0});
+
+        // Expect int; has text data "DF"
+        final String intTagWithTextData = "03 01 6A 00 45 4C 03 44 46 00 01 01";
+        try {
+            testParserHelper(intTagWithTextData);
+            fail("Expected EasParserException for nonnumeric char 'D'");
+        } catch (Parser.EasParserException e) {
+            // expected
+        }
+    }
+
+    @SmallTest
+    public void testOpaqueDataParsing() throws Exception {
+        // Expect opaque; has opaque data {0x11, 0x22, 0x33}
+        final String opaqueTagWithOpaqueData = "03 01 6A 00 45 4D C3 03 11 22 33 01 01";
+        testParserHelper(opaqueTagWithOpaqueData, new Object[] {"11 22 33"});
+
+        // Expect opaque; has tag with no content: <Tag/>
+        final String opaqueTagNoContent = "03 01 6A 00 45 0D 01";
+        testParserHelper(opaqueTagNoContent, new Object[] {""});
+
+        // Expect opaque; has tag and end tag with no value: <Tag></Tag>
+        final String emptyOpaqueTag = "03 01 6A 00 45 4D 01 01";
+        testParserHelper(emptyOpaqueTag, new Object[] {""});
+
+        // Expect opaque; has text data "DF"
+        final String opaqueTagWithTextData = "03 01 6A 00 45 4D 03 44 46 00 01 01";
+        testParserHelper(opaqueTagWithTextData, new Object[] {"44 46"});
+    }
+
+    @SmallTest
+    public void testMalformedData() throws Exception {
+        final String malformedData = "03 01 6A 00 45 4B 03 11 22 00 00 33 00 01 01";
+        try {
+            testParserHelper(malformedData);
+            fail("Expected EasParserException for improperly escaped text data");
+        } catch (Parser.EasParserException e) {
+            // expected
+        }
+    }
+
+    @SmallTest
+    public void testAttributeTag() throws Exception {
+        // Test parser with known tag with attributes
+        final String tagWithAttributes = "03 01 6A 00 45 DF 06 01 03 31 00 01 01";
+        try {
+            testParserHelper(tagWithAttributes);
+            fail("Expected EasParserException for tag with attributes 0xDF");
+        } catch (Parser.EasParserException e) {
+            // expected
+        }
+    }
+}
diff --git a/tests/src/com/android/exchange/adapter/ProvisionParserTests.java b/tests/src/com/android/exchange/adapter/ProvisionParserTests.java
index bfd078b..77ff05b 100644
--- a/tests/src/com/android/exchange/adapter/ProvisionParserTests.java
+++ b/tests/src/com/android/exchange/adapter/ProvisionParserTests.java
@@ -31,8 +31,11 @@
  */
 @SmallTest
 public class ProvisionParserTests extends SyncAdapterTestCase {
+    // <Sync><Status>1</Status></Sync>
+    private final byte[] wbxmlBytes = new byte[] {
+        0x03, 0x01, 0x6A, 0x00, 0x45, 0x4E, 0x03, 0x31, 0x00, 0x01, 0x01};
     private final ByteArrayInputStream mTestInputStream =
-        new ByteArrayInputStream("ABCDEFG".getBytes());
+        new ByteArrayInputStream(wbxmlBytes);
 
     // A good sample of an Exchange 2003 (WAP) provisioning document for end-to-end testing
     private String mWapProvisioningDoc1 =
diff --git a/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java b/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java
index f3a45e9..aff4c54 100644
--- a/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java
+++ b/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java
@@ -72,7 +72,10 @@
      * @return the InputStream
      */
     public InputStream getTestInputStream() {
-        return new ByteArrayInputStream(new byte[] {0, 0, 0, 0, 0});
+        // <Sync><Status>1</Status></Sync>
+        final byte[] wbxmlBytes = new byte[] {
+            0x03, 0x01, 0x6A, 0x00, 0x45, 0x4E, 0x03, 0x31, 0x00, 0x01, 0x01};
+        return new ByteArrayInputStream(wbxmlBytes);
     }
 
     EasSyncService getTestService() {