Implement rule binary serializer

Bug: 143697198
Test: atest FrameworksServicesTests:RuleBinarySerializerTest
Change-Id: Ia804137d8b127a3b2ef8a7b692b7903360fdbe8c
diff --git a/services/core/java/com/android/server/integrity/model/BitOutputStream.java b/services/core/java/com/android/server/integrity/model/BitOutputStream.java
new file mode 100644
index 0000000..ecb9189
--- /dev/null
+++ b/services/core/java/com/android/server/integrity/model/BitOutputStream.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2019 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.server.integrity.model;
+
+import java.util.BitSet;
+
+/** A wrapper class for writing a stream of bits. */
+public class BitOutputStream {
+
+    private BitSet mBitSet;
+    private int mIndex;
+
+    public BitOutputStream() {
+        mBitSet = new BitSet();
+        mIndex = 0;
+    }
+
+    /**
+     * Set the next number of bits in the stream to value.
+     *
+     * @param numOfBits The number of bits used to represent the value.
+     * @param value The value to convert to bits.
+     */
+    public void setNext(int numOfBits, int value) {
+        if (numOfBits <= 0) {
+            return;
+        }
+        int offset = 1 << (numOfBits - 1);
+        while (numOfBits-- > 0) {
+            mBitSet.set(mIndex, (value & offset) != 0);
+            offset >>= 1;
+            mIndex++;
+        }
+    }
+
+    /**
+     * Set the next bit in the stream to value.
+     *
+     * @param value The value to set the bit to.
+     */
+    public void setNext(boolean value) {
+        mBitSet.set(mIndex, value);
+        mIndex++;
+    }
+
+    /** Set the next bit in the stream to true. */
+    public void setNext() {
+        setNext(/* value= */ true);
+    }
+
+    /** Convert BitSet in big-endian to ByteArray in big-endian. */
+    public byte[] toByteArray() {
+        int bitSetSize = mBitSet.length();
+        int numOfBytes = bitSetSize / 8;
+        if (bitSetSize % 8 != 0) {
+            numOfBytes++;
+        }
+        byte[] bytes = new byte[numOfBytes];
+        for (int i = 0; i < mBitSet.length(); i++) {
+            if (mBitSet.get(i)) {
+                bytes[i / 8] |= 1 << (7 - (i % 8));
+            }
+        }
+        return bytes;
+    }
+
+    /** Clear the stream. */
+    public void clear() {
+        mBitSet.clear();
+        mIndex = 0;
+    }
+}
diff --git a/services/core/java/com/android/server/integrity/parser/RuleBinaryParser.java b/services/core/java/com/android/server/integrity/parser/RuleBinaryParser.java
index aad177e..54c0bd8 100644
--- a/services/core/java/com/android/server/integrity/parser/RuleBinaryParser.java
+++ b/services/core/java/com/android/server/integrity/parser/RuleBinaryParser.java
@@ -25,13 +25,13 @@
 public class RuleBinaryParser implements RuleParser {
 
     @Override
-    public List<Rule> parse(byte[] ruleBytes) {
+    public List<Rule> parse(byte[] ruleBytes)  throws RuleParseException {
         // TODO: Implement binary text parser.
         return null;
     }
 
     @Override
-    public List<Rule> parse(InputStream inputStream) {
+    public List<Rule> parse(InputStream inputStream) throws RuleParseException {
         // TODO: Implement stream parser.
         return null;
     }
diff --git a/services/core/java/com/android/server/integrity/serializer/RuleBinarySerializer.java b/services/core/java/com/android/server/integrity/serializer/RuleBinarySerializer.java
index d4f41eb..f782f71 100644
--- a/services/core/java/com/android/server/integrity/serializer/RuleBinarySerializer.java
+++ b/services/core/java/com/android/server/integrity/serializer/RuleBinarySerializer.java
@@ -16,24 +16,157 @@
 
 package com.android.server.integrity.serializer;
 
+import android.content.integrity.AtomicFormula;
+import android.content.integrity.CompoundFormula;
+import android.content.integrity.Formula;
 import android.content.integrity.Rule;
 
+import com.android.server.integrity.model.BitOutputStream;
+
+import java.io.ByteArrayOutputStream;
 import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.Optional;
 
-/** A helper class to serialize rules from the {@link Rule} model to Xml representation. */
+/** A helper class to serialize rules from the {@link Rule} model to Binary representation. */
 public class RuleBinarySerializer implements RuleSerializer {
 
+    public static final int FORMAT_VERSION_BITS = 5;
+    public static final int EFFECT_BITS = 3;
+    public static final int KEY_BITS = 4;
+    public static final int OPERATOR_BITS = 3;
+    public static final int CONNECTOR_BITS = 2;
+    public static final int SEPARATOR_BITS = 2;
+    public static final int VALUE_SIZE_BITS = 5;
+
+    public static final int ATOMIC_FORMULA_START = 0;
+    public static final int COMPOUND_FORMULA_START = 1;
+    public static final int COMPOUND_FORMULA_END = 2;
+
+    public static final int DEFAULT_FORMAT_VERSION = 1;
+
+    // Get the byte representation for a list of rules, and write them to an output stream.
     @Override
     public void serialize(
-            List<Rule> rules, Optional<Integer> formatVersion, OutputStream outputStream) {
-        // TODO: Implement stream serializer.
+            List<Rule> rules, Optional<Integer> formatVersion, OutputStream outputStream)
+            throws RuleSerializeException {
+        try {
+            BitOutputStream bitOutputStream = new BitOutputStream();
+
+            int formatVersionValue = formatVersion.orElse(DEFAULT_FORMAT_VERSION);
+            bitOutputStream.setNext(FORMAT_VERSION_BITS, formatVersionValue);
+            outputStream.write(bitOutputStream.toByteArray());
+
+            for (Rule rule : rules) {
+                bitOutputStream.clear();
+                serializeRule(rule, bitOutputStream);
+                outputStream.write(bitOutputStream.toByteArray());
+            }
+        } catch (Exception e) {
+            throw new RuleSerializeException(e.getMessage(), e);
+        }
     }
 
+    // Get the byte representation for a list of rules.
     @Override
-    public byte[] serialize(List<Rule> rules, Optional<Integer> formatVersion) {
-        // TODO: Implement text serializer.
-        return null;
+    public byte[] serialize(List<Rule> rules, Optional<Integer> formatVersion)
+            throws RuleSerializeException {
+        try {
+            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+            serialize(rules, formatVersion, byteArrayOutputStream);
+            return byteArrayOutputStream.toByteArray();
+        } catch (Exception e) {
+            throw new RuleSerializeException(e.getMessage(), e);
+        }
+    }
+
+    private void serializeRule(Rule rule, BitOutputStream bitOutputStream) {
+        if (rule == null) {
+            throw new IllegalArgumentException("Null rule can not be serialized");
+        }
+
+        // Start with a '1' bit to mark the start of a rule.
+        bitOutputStream.setNext();
+
+        serializeFormula(rule.getFormula(), bitOutputStream);
+        bitOutputStream.setNext(EFFECT_BITS, rule.getEffect());
+
+        // End with a '1' bit to mark the end of a rule.
+        bitOutputStream.setNext();
+    }
+
+    private void serializeFormula(Formula formula, BitOutputStream bitOutputStream) {
+        if (formula instanceof AtomicFormula) {
+            serializeAtomicFormula((AtomicFormula) formula, bitOutputStream);
+        } else if (formula instanceof CompoundFormula) {
+            serializeCompoundFormula((CompoundFormula) formula, bitOutputStream);
+        } else {
+            throw new IllegalArgumentException(
+                    String.format("Invalid formula type: %s", formula.getClass()));
+        }
+    }
+
+    private void serializeCompoundFormula(
+            CompoundFormula compoundFormula, BitOutputStream bitOutputStream) {
+        if (compoundFormula == null) {
+            throw new IllegalArgumentException("Null compound formula can not be serialized");
+        }
+
+        bitOutputStream.setNext(SEPARATOR_BITS, COMPOUND_FORMULA_START);
+        bitOutputStream.setNext(CONNECTOR_BITS, compoundFormula.getConnector());
+        for (Formula formula : compoundFormula.getFormulas()) {
+            serializeFormula(formula, bitOutputStream);
+        }
+        bitOutputStream.setNext(SEPARATOR_BITS, COMPOUND_FORMULA_END);
+    }
+
+    private void serializeAtomicFormula(
+            AtomicFormula atomicFormula, BitOutputStream bitOutputStream) {
+        if (atomicFormula == null) {
+            throw new IllegalArgumentException("Null atomic formula can not be serialized");
+        }
+
+        bitOutputStream.setNext(SEPARATOR_BITS, ATOMIC_FORMULA_START);
+        bitOutputStream.setNext(KEY_BITS, atomicFormula.getKey());
+        if (atomicFormula instanceof AtomicFormula.StringAtomicFormula) {
+            AtomicFormula.StringAtomicFormula stringAtomicFormula =
+                    (AtomicFormula.StringAtomicFormula) atomicFormula;
+            bitOutputStream.setNext(OPERATOR_BITS, AtomicFormula.EQ);
+            serializeValue(
+                    stringAtomicFormula.getValue(),
+                    stringAtomicFormula.getIsHashedValue(),
+                    bitOutputStream);
+        } else if (atomicFormula instanceof AtomicFormula.IntAtomicFormula) {
+            AtomicFormula.IntAtomicFormula intAtomicFormula =
+                    (AtomicFormula.IntAtomicFormula) atomicFormula;
+            bitOutputStream.setNext(OPERATOR_BITS, intAtomicFormula.getOperator());
+            serializeValue(
+                    String.valueOf(intAtomicFormula.getValue()),
+                    /* isHashedValue= */ false,
+                    bitOutputStream);
+        } else if (atomicFormula instanceof AtomicFormula.BooleanAtomicFormula) {
+            AtomicFormula.BooleanAtomicFormula booleanAtomicFormula =
+                    (AtomicFormula.BooleanAtomicFormula) atomicFormula;
+            bitOutputStream.setNext(OPERATOR_BITS, AtomicFormula.EQ);
+            serializeValue(
+                    booleanAtomicFormula.getValue() ? "1" : "0",
+                    /* isHashedValue= */ false,
+                    bitOutputStream);
+        } else {
+            throw new IllegalArgumentException(
+                    String.format("Invalid atomic formula type: %s", atomicFormula.getClass()));
+        }
+    }
+
+    private void serializeValue(
+            String value, boolean isHashedValue, BitOutputStream bitOutputStream) {
+        byte[] valueBytes = value.getBytes(StandardCharsets.UTF_8);
+
+        bitOutputStream.setNext(isHashedValue);
+        bitOutputStream.setNext(VALUE_SIZE_BITS, valueBytes.length);
+        for (byte valueByte : valueBytes) {
+            bitOutputStream.setNext(/* numOfBits= */ 8, valueByte);
+        }
     }
 }
diff --git a/services/core/java/com/android/server/integrity/serializer/RuleXmlSerializer.java b/services/core/java/com/android/server/integrity/serializer/RuleXmlSerializer.java
index 3ec9cf2..72068ce 100644
--- a/services/core/java/com/android/server/integrity/serializer/RuleXmlSerializer.java
+++ b/services/core/java/com/android/server/integrity/serializer/RuleXmlSerializer.java
@@ -39,7 +39,7 @@
 
     private static final String RULE_LIST_TAG = "RL";
     private static final String RULE_TAG = "R";
-    private static final String OPEN_FORMULA_TAG = "OF";
+    private static final String COMPOUND_FORMULA_TAG = "OF";
     private static final String ATOMIC_FORMULA_TAG = "AF";
     private static final String EFFECT_ATTRIBUTE = "E";
     private static final String KEY_ATTRIBUTE = "K";
@@ -78,13 +78,13 @@
     private void serializeRules(List<Rule> rules, XmlSerializer xmlSerializer) throws IOException {
         xmlSerializer.startTag(NAMESPACE, RULE_LIST_TAG);
         for (Rule rule : rules) {
-            serialize(rule, xmlSerializer);
+            serializeRule(rule, xmlSerializer);
         }
         xmlSerializer.endTag(NAMESPACE, RULE_LIST_TAG);
         xmlSerializer.endDocument();
     }
 
-    private void serialize(Rule rule, XmlSerializer xmlSerializer) throws IOException {
+    private void serializeRule(Rule rule, XmlSerializer xmlSerializer) throws IOException {
         if (rule == null) {
             return;
         }
@@ -98,25 +98,25 @@
         if (formula instanceof AtomicFormula) {
             serializeAtomicFormula((AtomicFormula) formula, xmlSerializer);
         } else if (formula instanceof CompoundFormula) {
-            serializeOpenFormula((CompoundFormula) formula, xmlSerializer);
+            serializeCompoundFormula((CompoundFormula) formula, xmlSerializer);
         } else {
             throw new IllegalArgumentException(
                     String.format("Invalid formula type: %s", formula.getClass()));
         }
     }
 
-    private void serializeOpenFormula(CompoundFormula compoundFormula, XmlSerializer xmlSerializer)
-            throws IOException {
+    private void serializeCompoundFormula(
+            CompoundFormula compoundFormula, XmlSerializer xmlSerializer) throws IOException {
         if (compoundFormula == null) {
             return;
         }
-        xmlSerializer.startTag(NAMESPACE, OPEN_FORMULA_TAG);
+        xmlSerializer.startTag(NAMESPACE, COMPOUND_FORMULA_TAG);
         serializeAttributeValue(
                 CONNECTOR_ATTRIBUTE, String.valueOf(compoundFormula.getConnector()), xmlSerializer);
         for (Formula formula : compoundFormula.getFormulas()) {
             serializeFormula(formula, xmlSerializer);
         }
-        xmlSerializer.endTag(NAMESPACE, OPEN_FORMULA_TAG);
+        xmlSerializer.endTag(NAMESPACE, COMPOUND_FORMULA_TAG);
     }
 
     private void serializeAtomicFormula(AtomicFormula atomicFormula, XmlSerializer xmlSerializer)
diff --git a/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleBinarySerializerTest.java b/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleBinarySerializerTest.java
new file mode 100644
index 0000000..a43dbd7
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleBinarySerializerTest.java
@@ -0,0 +1,445 @@
+/*
+ * Copyright (C) 2019 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.server.integrity.serializer;
+
+import static com.android.server.integrity.utils.TestUtils.getBits;
+import static com.android.server.integrity.utils.TestUtils.getBytes;
+import static com.android.server.integrity.utils.TestUtils.getValueBits;
+import static com.android.server.testutils.TestUtils.assertExpectException;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.integrity.AppInstallMetadata;
+import android.content.integrity.AtomicFormula;
+import android.content.integrity.CompoundFormula;
+import android.content.integrity.Formula;
+import android.content.integrity.Rule;
+
+import androidx.annotation.NonNull;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Optional;
+
+@RunWith(JUnit4.class)
+public class RuleBinarySerializerTest {
+
+    private static final String COMPOUND_FORMULA_START =
+            getBits(
+                    RuleBinarySerializer.COMPOUND_FORMULA_START,
+                    RuleBinarySerializer.SEPARATOR_BITS);
+    private static final String COMPOUND_FORMULA_END =
+            getBits(RuleBinarySerializer.COMPOUND_FORMULA_END, RuleBinarySerializer.SEPARATOR_BITS);
+    private static final String ATOMIC_FORMULA_START =
+            getBits(RuleBinarySerializer.ATOMIC_FORMULA_START, RuleBinarySerializer.SEPARATOR_BITS);
+
+    private static final String NOT =
+            getBits(CompoundFormula.NOT, RuleBinarySerializer.CONNECTOR_BITS);
+    private static final String AND =
+            getBits(CompoundFormula.AND, RuleBinarySerializer.CONNECTOR_BITS);
+    private static final String OR =
+            getBits(CompoundFormula.OR, RuleBinarySerializer.CONNECTOR_BITS);
+
+    private static final String PACKAGE_NAME =
+            getBits(AtomicFormula.PACKAGE_NAME, RuleBinarySerializer.KEY_BITS);
+    private static final String APP_CERTIFICATE =
+            getBits(AtomicFormula.APP_CERTIFICATE, RuleBinarySerializer.KEY_BITS);
+    private static final String VERSION_CODE =
+            getBits(AtomicFormula.VERSION_CODE, RuleBinarySerializer.KEY_BITS);
+    private static final String PRE_INSTALLED =
+            getBits(AtomicFormula.PRE_INSTALLED, RuleBinarySerializer.KEY_BITS);
+
+    private static final String EQ = getBits(AtomicFormula.EQ, RuleBinarySerializer.OPERATOR_BITS);
+
+    private static final String IS_NOT_HASHED = "0";
+
+    private static final String DENY = getBits(Rule.DENY, RuleBinarySerializer.EFFECT_BITS);
+
+    private static final String START_BIT = "1";
+    private static final String END_BIT = "1";
+
+    private static final byte[] DEFAULT_FORMAT_VERSION =
+            getBytes(
+                    getBits(
+                            RuleBinarySerializer.DEFAULT_FORMAT_VERSION,
+                            RuleBinarySerializer.FORMAT_VERSION_BITS));
+
+    @Test
+    public void testBinaryString_serializeEmptyRule() throws Exception {
+        Rule rule = null;
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+
+        assertExpectException(
+                RuleSerializeException.class,
+                /* expectedExceptionMessageRegex= */ "Null rule can not be serialized",
+                () ->
+                        binarySerializer.serialize(
+                                Collections.singletonList(rule),
+                                /* formatVersion= */ Optional.empty()));
+    }
+
+    @Test
+    public void testBinaryStream_serializeValidCompoundFormula() throws Exception {
+        String packageName = "com.test.app";
+        Rule rule =
+                new Rule(
+                        new CompoundFormula(
+                                CompoundFormula.NOT,
+                                Collections.singletonList(
+                                        new AtomicFormula.StringAtomicFormula(
+                                                AtomicFormula.PACKAGE_NAME,
+                                                packageName,
+                                                /* isHashedValue= */ false))),
+                        Rule.DENY);
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        String expectedBits =
+                START_BIT
+                        + COMPOUND_FORMULA_START
+                        + NOT
+                        + ATOMIC_FORMULA_START
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), RuleBinarySerializer.VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + COMPOUND_FORMULA_END
+                        + DENY
+                        + END_BIT;
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION);
+        byteArrayOutputStream.write(getBytes(expectedBits));
+        byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+        binarySerializer.serialize(
+                Collections.singletonList(rule),
+                /* formatVersion= */ Optional.empty(),
+                outputStream);
+
+        byte[] actualRules = outputStream.toByteArray();
+        assertThat(actualRules).isEqualTo(expectedRules);
+    }
+
+    @Test
+    public void testBinaryString_serializeValidCompoundFormula_notConnector() throws Exception {
+        String packageName = "com.test.app";
+        Rule rule =
+                new Rule(
+                        new CompoundFormula(
+                                CompoundFormula.NOT,
+                                Collections.singletonList(
+                                        new AtomicFormula.StringAtomicFormula(
+                                                AtomicFormula.PACKAGE_NAME,
+                                                packageName,
+                                                /* isHashedValue= */ false))),
+                        Rule.DENY);
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+        String expectedBits =
+                START_BIT
+                        + COMPOUND_FORMULA_START
+                        + NOT
+                        + ATOMIC_FORMULA_START
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), RuleBinarySerializer.VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + COMPOUND_FORMULA_END
+                        + DENY
+                        + END_BIT;
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION);
+        byteArrayOutputStream.write(getBytes(expectedBits));
+        byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+        byte[] actualRules =
+                binarySerializer.serialize(
+                        Collections.singletonList(rule), /* formatVersion= */ Optional.empty());
+
+        assertThat(actualRules).isEqualTo(expectedRules);
+    }
+
+    @Test
+    public void testBinaryString_serializeValidCompoundFormula_andConnector() throws Exception {
+        String packageName = "com.test.app";
+        String appCertificate = "test_cert";
+        Rule rule =
+                new Rule(
+                        new CompoundFormula(
+                                CompoundFormula.AND,
+                                Arrays.asList(
+                                        new AtomicFormula.StringAtomicFormula(
+                                                AtomicFormula.PACKAGE_NAME,
+                                                packageName,
+                                                /* isHashedValue= */ false),
+                                        new AtomicFormula.StringAtomicFormula(
+                                                AtomicFormula.APP_CERTIFICATE,
+                                                appCertificate,
+                                                /* isHashedValue= */ false))),
+                        Rule.DENY);
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+        String expectedBits =
+                START_BIT
+                        + COMPOUND_FORMULA_START
+                        + AND
+                        + ATOMIC_FORMULA_START
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), RuleBinarySerializer.VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + ATOMIC_FORMULA_START
+                        + APP_CERTIFICATE
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(appCertificate.length(), RuleBinarySerializer.VALUE_SIZE_BITS)
+                        + getValueBits(appCertificate)
+                        + COMPOUND_FORMULA_END
+                        + DENY
+                        + END_BIT;
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION);
+        byteArrayOutputStream.write(getBytes(expectedBits));
+        byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+        byte[] actualRules =
+                binarySerializer.serialize(
+                        Collections.singletonList(rule), /* formatVersion= */ Optional.empty());
+
+        assertThat(actualRules).isEqualTo(expectedRules);
+    }
+
+    @Test
+    public void testBinaryString_serializeValidCompoundFormula_orConnector() throws Exception {
+        String packageName = "com.test.app";
+        String appCertificate = "test_cert";
+        Rule rule =
+                new Rule(
+                        new CompoundFormula(
+                                CompoundFormula.OR,
+                                Arrays.asList(
+                                        new AtomicFormula.StringAtomicFormula(
+                                                AtomicFormula.PACKAGE_NAME,
+                                                packageName,
+                                                /* isHashedValue= */ false),
+                                        new AtomicFormula.StringAtomicFormula(
+                                                AtomicFormula.APP_CERTIFICATE,
+                                                appCertificate,
+                                                /* isHashedValue= */ false))),
+                        Rule.DENY);
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+        String expectedBits =
+                START_BIT
+                        + COMPOUND_FORMULA_START
+                        + OR
+                        + ATOMIC_FORMULA_START
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), RuleBinarySerializer.VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + ATOMIC_FORMULA_START
+                        + APP_CERTIFICATE
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(appCertificate.length(), RuleBinarySerializer.VALUE_SIZE_BITS)
+                        + getValueBits(appCertificate)
+                        + COMPOUND_FORMULA_END
+                        + DENY
+                        + END_BIT;
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION);
+        byteArrayOutputStream.write(getBytes(expectedBits));
+        byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+        byte[] actualRules =
+                binarySerializer.serialize(
+                        Collections.singletonList(rule), /* formatVersion= */ Optional.empty());
+
+        assertThat(actualRules).isEqualTo(expectedRules);
+    }
+
+    @Test
+    public void testBinaryString_serializeValidAtomicFormula_stringValue() throws Exception {
+        String packageName = "com.test.app";
+        Rule rule =
+                new Rule(
+                        new AtomicFormula.StringAtomicFormula(
+                                AtomicFormula.PACKAGE_NAME,
+                                packageName,
+                                /* isHashedValue= */ false),
+                        Rule.DENY);
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+        String expectedBits =
+                START_BIT
+                        + ATOMIC_FORMULA_START
+                        + PACKAGE_NAME
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(packageName.length(), RuleBinarySerializer.VALUE_SIZE_BITS)
+                        + getValueBits(packageName)
+                        + DENY
+                        + END_BIT;
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION);
+        byteArrayOutputStream.write(getBytes(expectedBits));
+        byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+        byte[] actualRules =
+                binarySerializer.serialize(
+                        Collections.singletonList(rule), /* formatVersion= */ Optional.empty());
+
+        assertThat(actualRules).isEqualTo(expectedRules);
+    }
+
+    @Test
+    public void testBinaryString_serializeValidAtomicFormula_integerValue() throws Exception {
+        String versionCode = "1";
+        Rule rule =
+                new Rule(
+                        new AtomicFormula.IntAtomicFormula(
+                                AtomicFormula.VERSION_CODE,
+                                AtomicFormula.EQ,
+                                Integer.parseInt(versionCode)),
+                        Rule.DENY);
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+        String expectedBits =
+                START_BIT
+                        + ATOMIC_FORMULA_START
+                        + VERSION_CODE
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(versionCode.length(), RuleBinarySerializer.VALUE_SIZE_BITS)
+                        + getValueBits(versionCode)
+                        + DENY
+                        + END_BIT;
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION);
+        byteArrayOutputStream.write(getBytes(expectedBits));
+        byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+        byte[] actualRules =
+                binarySerializer.serialize(
+                        Collections.singletonList(rule), /* formatVersion= */ Optional.empty());
+
+        assertThat(actualRules).isEqualTo(expectedRules);
+    }
+
+    @Test
+    public void testBinaryString_serializeValidAtomicFormula_booleanValue() throws Exception {
+        String preInstalled = "1";
+        Rule rule =
+                new Rule(
+                        new AtomicFormula.BooleanAtomicFormula(AtomicFormula.PRE_INSTALLED, true),
+                        Rule.DENY);
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+        String expectedBits =
+                START_BIT
+                        + ATOMIC_FORMULA_START
+                        + PRE_INSTALLED
+                        + EQ
+                        + IS_NOT_HASHED
+                        + getBits(preInstalled.length(), RuleBinarySerializer.VALUE_SIZE_BITS)
+                        + getValueBits(preInstalled)
+                        + DENY
+                        + END_BIT;
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION);
+        byteArrayOutputStream.write(getBytes(expectedBits));
+        byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+        byte[] actualRules =
+                binarySerializer.serialize(
+                        Collections.singletonList(rule), /* formatVersion= */ Optional.empty());
+
+        assertThat(actualRules).isEqualTo(expectedRules);
+    }
+
+    @Test
+    public void testBinaryString_serializeInvalidFormulaType() throws Exception {
+        Formula invalidFormula = getInvalidFormula();
+        Rule rule = new Rule(invalidFormula, Rule.DENY);
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+
+        assertExpectException(
+                RuleSerializeException.class,
+                /* expectedExceptionMessageRegex= */ "Invalid formula type",
+                () ->
+                        binarySerializer.serialize(
+                                Collections.singletonList(rule),
+                                /* formatVersion= */ Optional.empty()));
+    }
+
+    @Test
+    public void testBinaryString_serializeFormatVersion() throws Exception {
+        int formatVersion = 1;
+        RuleSerializer binarySerializer = new RuleBinarySerializer();
+        String expectedBits = getBits(formatVersion, RuleBinarySerializer.FORMAT_VERSION_BITS);
+        byte[] expectedRules = getBytes(expectedBits);
+
+        byte[] actualRules =
+                binarySerializer.serialize(
+                        Collections.emptyList(), /* formatVersion= */ Optional.of(formatVersion));
+
+        assertThat(actualRules).isEqualTo(expectedRules);
+    }
+
+    private static Formula getInvalidFormula() {
+        return new Formula() {
+            @Override
+            public boolean isSatisfied(AppInstallMetadata appInstallMetadata) {
+                return false;
+            }
+
+            @Override
+            public int getTag() {
+                return 0;
+            }
+
+            @Override
+            public int hashCode() {
+                return super.hashCode();
+            }
+
+            @Override
+            public boolean equals(Object obj) {
+                return super.equals(obj);
+            }
+
+            @NonNull
+            @Override
+            protected Object clone() throws CloneNotSupportedException {
+                return super.clone();
+            }
+
+            @Override
+            public String toString() {
+                return super.toString();
+            }
+
+            @Override
+            protected void finalize() throws Throwable {
+                super.finalize();
+            }
+        };
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleXmlSerializerTest.java b/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleXmlSerializerTest.java
index 5903b5a..ad74901 100644
--- a/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleXmlSerializerTest.java
+++ b/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleXmlSerializerTest.java
@@ -91,7 +91,7 @@
     }
 
     @Test
-    public void testXmlStream_serializeValidOpenFormula() throws Exception {
+    public void testXmlStream_serializeValidCompoundFormula() throws Exception {
         Rule rule =
                 new Rule(
                         new CompoundFormula(
@@ -134,7 +134,7 @@
     }
 
     @Test
-    public void testXmlString_serializeValidOpenFormula_notConnector() throws Exception {
+    public void testXmlString_serializeValidCompoundFormula_notConnector() throws Exception {
         Rule rule =
                 new Rule(
                         new CompoundFormula(
@@ -174,7 +174,7 @@
     }
 
     @Test
-    public void testXmlString_serializeValidOpenFormula_andConnector() throws Exception {
+    public void testXmlString_serializeValidCompoundFormula_andConnector() throws Exception {
         Rule rule =
                 new Rule(
                         new CompoundFormula(
@@ -224,7 +224,7 @@
     }
 
     @Test
-    public void testXmlString_serializeValidOpenFormula_orConnector() throws Exception {
+    public void testXmlString_serializeValidCompoundFormula_orConnector() throws Exception {
         Rule rule =
                 new Rule(
                         new CompoundFormula(
diff --git a/services/tests/servicestests/src/com/android/server/integrity/utils/TestUtils.java b/services/tests/servicestests/src/com/android/server/integrity/utils/TestUtils.java
new file mode 100644
index 0000000..e54410b
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/integrity/utils/TestUtils.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2019 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.server.integrity.utils;
+
+public class TestUtils {
+
+    public static String getBits(int component, int numOfBits) {
+        return String.format("%" + numOfBits + "s", Integer.toBinaryString(component))
+                .replace(' ', '0');
+    }
+
+    public static String getValueBits(String value) {
+        StringBuilder stringBuilder = new StringBuilder();
+        for (byte valueByte : value.getBytes()) {
+            stringBuilder.append(getBits(valueByte, /* numOfBits= */ 8));
+        }
+        return stringBuilder.toString();
+    }
+
+    public static byte[] getBytes(String bits) {
+        int bitStringSize = bits.length();
+        int numOfBytes = bitStringSize / 8;
+        if (bitStringSize % 8 != 0) {
+            numOfBytes++;
+        }
+        byte[] bytes = new byte[numOfBytes];
+        for (int i = 0; i < bits.length(); i++) {
+            if (bits.charAt(i) == '1') {
+                bytes[i / 8] |= 1 << (7 - (i % 8));
+            }
+        }
+        return bytes;
+    }
+}