Custom file format for string files.

Summary:
We were using JSON as the file format for strings dumped as assets. The
resulting `APK` for fb4a is around 400-500 KB bigger in size, which is not
acceptable. This introduces a custom format (explained in javadocs).

In order to further trim the file size, we store resource ids instead of
resource names. In order to achieve this, the strings file generation step is
now delayed, running it after the final `R.java` file is created by `aapt`.
Instead of parsing `R.java`, I also make `aapt` generate `R.txt` which is very
easy to parse.
diff --git a/src/com/facebook/buck/android/AndroidBinaryRule.java b/src/com/facebook/buck/android/AndroidBinaryRule.java
index cbc2f67..74d9fad 100644
--- a/src/com/facebook/buck/android/AndroidBinaryRule.java
+++ b/src/com/facebook/buck/android/AndroidBinaryRule.java
@@ -442,7 +442,7 @@
 
   /**
    * Sets up filtering of resources, images/drawables and strings in particular, based on build
-   * rule parameters {@code resourceFilter} and {@code isStoreStringsAsAssets}.
+   * rule parameters {@link #resourceFilter} and {@link #isStoreStringsAsAssets}.
    *
    * {@link com.facebook.buck.android.FilterResourcesStep.ResourceFilter} {@code resourceFilter}
    * determines which drawables end up in the APK (based on density - mdpi, hdpi etc), and also
@@ -450,13 +450,9 @@
    *
    * {@code isStoreStringsAsAssets} determines whether non-english string resources are packaged
    * separately as assets (and not bundled together into the {@code resources.arsc} file).
-   *
-   * @return The set of resource directories that will eventually contain filtered resources.
    */
   @VisibleForTesting
-  Set<String> getFilteredResourceDirectories(
-      ImmutableList.Builder<Step> commands,
-      Set<String> resourceDirectories) {
+  FilterResourcesStep getFilterResourcesStep(Set<String> resourceDirectories) {
     ImmutableBiMap.Builder<String, String> filteredResourcesDirMapBuilder = ImmutableBiMap.builder();
     String resDestinationBasePath = getBinPath("__filtered__%s__");
     int count = 0;
@@ -474,16 +470,7 @@
       filterResourcesStepBuilder.enableStringsFilter();
     }
 
-    FilterResourcesStep filterResourcesStep = filterResourcesStepBuilder.build();
-    commands.add(filterResourcesStep);
-
-    if (isStoreStringsAsAssets()) {
-      Path tmpStringsDirPath = getPathForTmpStringAssetsDirectory();
-      commands.add(new MakeCleanDirectoryStep(tmpStringsDirPath));
-      commands.add(new CompileStringsStep(filterResourcesStep, tmpStringsDirPath));
-    }
-
-    return resSourceToDestDirMap.values();
+    return filterResourcesStepBuilder.build();
   }
 
   @Override
@@ -505,8 +492,11 @@
     Set<String> resDirectories = transitiveDependencies.resDirectories;
     Set<String> rDotJavaPackages = transitiveDependencies.rDotJavaPackages;
 
+    FilterResourcesStep filterResourcesStep = null;
     if (requiresResourceFilter()) {
-      resDirectories = getFilteredResourceDirectories(commands, resDirectories);
+      filterResourcesStep = getFilterResourcesStep(resDirectories);
+      commands.add(filterResourcesStep);
+      resDirectories = filterResourcesStep.getOutputResourceDirs();
     }
 
     // Extract the resources from third-party jars.
@@ -523,6 +513,15 @@
           rDotJavaPackages,
           getBuildTarget(),
           commands);
+
+      if (isStoreStringsAsAssets()) {
+        Path tmpStringsDirPath = getPathForTmpStringAssetsDirectory();
+        commands.add(new MakeCleanDirectoryStep(tmpStringsDirPath));
+        commands.add(new CompileStringsStep(
+            filterResourcesStep,
+            Paths.get(UberRDotJavaUtil.getPathToGeneratedRDotJavaSrcFiles(getBuildTarget())),
+            tmpStringsDirPath));
+      }
     }
 
     // Execute preprocess_java_classes_binary, if appropriate.
diff --git a/src/com/facebook/buck/android/CompileStringsStep.java b/src/com/facebook/buck/android/CompileStringsStep.java
index 7b57688..2c006df 100644
--- a/src/com/facebook/buck/android/CompileStringsStep.java
+++ b/src/com/facebook/buck/android/CompileStringsStep.java
@@ -20,16 +20,14 @@
 import com.facebook.buck.step.Step;
 import com.facebook.buck.util.ProjectFilesystem;
 import com.facebook.buck.util.XmlDomParser;
-import com.fasterxml.jackson.core.JsonFactory;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
-import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
+import com.google.common.collect.TreeMultimap;
 
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
@@ -41,15 +39,16 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Collection;
-import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.TreeMap;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 /**
  * This {@link Step} takes in a {@link FilterResourcesStep} that provides a list of string resource
- * files (strings.xml), groups them by locales, and for each locale generates a json file with all
- * the string resources for that locale.
+ * files (strings.xml), groups them by locales, and for each locale generates a file with all the
+ * string resources for that locale.
  *
  * <p>A typical strings.xml file looks like:
  * <pre>
@@ -66,7 +65,7 @@
  *       <item>Default</item>
  *       <item>Verbose</item>
  *       <item>Debug</item>
- *     </string-array>
+ *     </stri (which is accepted, hurray!) and bump .buckversionng-array>
  *   </resources>
  *   }
  * </pre></p>
@@ -83,7 +82,7 @@
  *   <li> a map of plurals </li>
  *   <li> a list of strings </li>
  * </ol>
- * and dumps this map into the json file.</p>
+ * and dumps this map into the output file. See {@link StringResources} for the file format.</p>
  */
 public class CompileStringsStep implements Step {
 
@@ -91,21 +90,15 @@
   static final Pattern STRING_FILE_PATTERN = Pattern.compile(
       ".*res/values-([a-z]{2})(?:-r([A-Z]{2}))*/strings.xml");
 
-  private final FilterResourcesStep filterResourcesStep;
-  private final Path destinationDir;
-  private final ObjectMapper objectMapper;
-  private final Map<String, String> regionSpecificToBaseLocaleMap;
-
   @VisibleForTesting
-  CompileStringsStep(
-      FilterResourcesStep filterResourcesStep,
-      Path destinationDir,
-      ObjectMapper mapper) {
-    this.filterResourcesStep = Preconditions.checkNotNull(filterResourcesStep);
-    this.destinationDir = Preconditions.checkNotNull(destinationDir);
-    this.objectMapper = Preconditions.checkNotNull(mapper);
-    regionSpecificToBaseLocaleMap = Maps.newHashMap();
-  }
+  static final Pattern R_DOT_TXT_STRING_RESOURCE_PATTERN = Pattern.compile(
+      "^int (string|plurals|array) (\\w+) 0x([0-9a-f]+)$");
+
+  private final FilterResourcesStep filterResourcesStep;
+  private final Path rDotJavaSrcDir;
+  private final Path destinationDir;
+  private final Map<String, String> regionSpecificToBaseLocaleMap;
+  private final Map<String, Integer> resourceNameToIdMap;
 
   /**
    * Note: The ordering of files in the input list determines which resource value ends up in the
@@ -113,14 +106,31 @@
    * resource name - file that appears first in the list wins.
    *
    * @param filterResourcesStep {@link FilterResourcesStep} that filters non english string files.
+   * @param rDotJavaSrcDir Path to the directory where aapt generates R.txt file along with the
+   *     final R.java files per package.
    * @param destinationDir Output directory for the generated json files.
    */
-  public CompileStringsStep(FilterResourcesStep filterResourcesStep, Path destinationDir) {
-    this(filterResourcesStep, destinationDir, new ObjectMapper(new JsonFactory()));
+  public CompileStringsStep(
+      FilterResourcesStep filterResourcesStep,
+      Path rDotJavaSrcDir,
+      Path destinationDir) {
+    this.filterResourcesStep = Preconditions.checkNotNull(filterResourcesStep);
+    this.rDotJavaSrcDir = Preconditions.checkNotNull(rDotJavaSrcDir);
+    this.destinationDir = Preconditions.checkNotNull(destinationDir);
+    this.regionSpecificToBaseLocaleMap = Maps.newHashMap();
+    this.resourceNameToIdMap = Maps.newHashMap();
   }
 
   @Override
   public int execute(ExecutionContext context) {
+    ProjectFilesystem filesystem = context.getProjectFilesystem();
+    try {
+      buildResourceNameToIdMap(filesystem);
+    } catch (IOException e) {
+      context.logError(e, "Failure parsing R.txt file.");
+      return 1;
+    }
+
     ImmutableSet<String> filteredStringFiles = filterResourcesStep.getNonEnglishStringFiles();
     ImmutableMultimap<String, String> filesByLocale = groupFilesByLocale(filteredStringFiles);
 
@@ -153,13 +163,12 @@
               .getMergedResources(resourcesByLocale.get(baseLocale)));
     }
 
-    ProjectFilesystem filesystem = context.getProjectFilesystem();
     for (String locale : filesByLocale.keySet()) {
       try {
-        File jsonFile = filesystem.getFileForRelativePath(destinationDir.resolve(locale + ".json"));
-        objectMapper.writeValue(jsonFile, resourcesByLocale.get(locale).asMap());
+        filesystem.writeBytesToPath(resourcesByLocale.get(locale).getBinaryFileContent(),
+            destinationDir.resolve(locale + ".fbstr"));
       } catch (IOException e) {
-        context.logError(e, "Error creating json string file for locale: %s", locale);
+        context.logError(e, "Error creating binary file for locale: %s", locale);
         return 1;
       }
     }
@@ -207,12 +216,29 @@
     return localeToFiles.build();
   }
 
+  /**
+   * Parses the R.txt file generated by aapt, looks for resources of type {@code string},
+   * {@code plurals} and {@code array}, and builds a map of resource names to their corresponding
+   * ids.
+   */
+  @VisibleForTesting
+  void buildResourceNameToIdMap(ProjectFilesystem filesystem)
+      throws IOException {
+    List<String> fileLines = filesystem.readLines(rDotJavaSrcDir.resolve("R.txt"));
+    for (String line : fileLines) {
+      Matcher matcher = R_DOT_TXT_STRING_RESOURCE_PATTERN.matcher(line);
+      if (!matcher.matches()) {
+        continue;
+      }
+      resourceNameToIdMap.put(matcher.group(2), Integer.parseInt(matcher.group(3), 16));
+    }
+  }
+
   private StringResources compileStringFiles(Collection<String> filepaths)
       throws IOException {
-
-    Map<String, String> stringsMap = Maps.newHashMap();
-    Map<String, ImmutableMap<String, String>> pluralsMap = Maps.newHashMap();
-    Multimap<String, String> arraysMap = ArrayListMultimap.create();
+    TreeMap<Integer, String> stringsMap = Maps.newTreeMap();
+    TreeMap<Integer, ImmutableMap<String, String>> pluralsMap = Maps.newTreeMap();
+    TreeMultimap<Integer, String> arraysMap = TreeMultimap.create();
 
     for (String stringFilePath : filepaths) {
       File stringFile = (Paths.get(stringFilePath)).toFile();
@@ -240,13 +266,17 @@
    * @param stringsMap Map from string resource name to its value.
    */
   @VisibleForTesting
-  void scrapeStringNodes(NodeList stringNodes, Map<String, String> stringsMap) {
+  void scrapeStringNodes(NodeList stringNodes, Map<Integer, String> stringsMap) {
     for (int i = 0; i < stringNodes.getLength(); ++i) {
       Node node = stringNodes.item(i);
       String resourceName = node.getAttributes().getNamedItem("name").getNodeValue();
+      if (!resourceNameToIdMap.containsKey(resourceName)) {
+        continue;
+      }
+      int resourceId = resourceNameToIdMap.get(resourceName);
       // Ignore a resource if it has already been found.
-      if (!stringsMap.containsKey(resourceName)) {
-        stringsMap.put(resourceName, node.getTextContent());
+      if (!stringsMap.containsKey(resourceId)) {
+        stringsMap.put(resourceId, node.getTextContent());
       }
     }
   }
@@ -257,13 +287,18 @@
   @VisibleForTesting
   void scrapePluralsNodes(
       NodeList pluralNodes,
-      Map<String, ImmutableMap<String, String>> pluralsMap) {
+      Map<Integer, ImmutableMap<String, String>> pluralsMap) {
 
     for (int i = 0; i < pluralNodes.getLength(); ++i) {
       Node node = pluralNodes.item(i);
       String resourceName = node.getAttributes().getNamedItem("name").getNodeValue();
+      if (!resourceNameToIdMap.containsKey(resourceName)) {
+        continue;
+      }
+      int resourceId = resourceNameToIdMap.get(resourceName);
+
       // Ignore a resource if it has already been found.
-      if (pluralsMap.containsKey(resourceName)) {
+      if (pluralsMap.containsKey(resourceId)) {
         continue;
       }
       ImmutableMap.Builder<String, String> quantityToStringBuilder = ImmutableMap.builder();
@@ -274,7 +309,7 @@
         String quantity = itemNode.getAttributes().getNamedItem("quantity").getNodeValue();
         quantityToStringBuilder.put(quantity, itemNode.getTextContent());
       }
-      pluralsMap.put(resourceName, quantityToStringBuilder.build());
+      pluralsMap.put(resourceId, quantityToStringBuilder.build());
     }
   }
 
@@ -282,22 +317,36 @@
    * Similar to {@code scrapeStringNodes}, but for string array nodes.
    */
   @VisibleForTesting
-  void scrapeStringArrayNodes(NodeList arrayNodes, Multimap<String, String> arraysMap) {
+  void scrapeStringArrayNodes(NodeList arrayNodes, Multimap<Integer, String> arraysMap) {
     for (int i = 0; i < arrayNodes.getLength(); ++i) {
       Node node = arrayNodes.item(i);
       String resourceName = node.getAttributes().getNamedItem("name").getNodeValue();
+      // Ignore a resource if R.txt does not contain an entry for it.
+      if (!resourceNameToIdMap.containsKey(resourceName)) {
+        continue;
+      }
+
+      int resourceId = resourceNameToIdMap.get(resourceName);
       // Ignore a resource if it has already been found.
-      if (arraysMap.containsKey(resourceName)) {
+      if (arraysMap.containsKey(resourceId)) {
         continue;
       }
 
       NodeList itemNodes = ((Element)node).getElementsByTagName("item");
       for (int j = 0; j < itemNodes.getLength(); ++j) {
-        arraysMap.put(resourceName, itemNodes.item(j).getTextContent());
+        arraysMap.put(resourceId, itemNodes.item(j).getTextContent());
       }
     }
   }
 
+  /**
+   * Used in unit tests to inject the resource name to id map.
+   */
+  @VisibleForTesting
+  void addResourceNameToIdMap(Map<String, Integer> nameToIdMap) {
+    resourceNameToIdMap.putAll(nameToIdMap);
+  }
+
   @Override
   public String getShortName() {
     return "compile_strings";
@@ -305,42 +354,6 @@
 
   @Override
   public String getDescription(ExecutionContext context) {
-    return "Combine, parse string resource xml files into one json file per locale.";
-  }
-
-  @VisibleForTesting
-  static class StringResources {
-    public final Map<String, String> strings;
-    public final Map<String, ImmutableMap<String, String>> plurals;
-    public final Multimap<String, String> arrays;
-
-    public StringResources(
-        Map<String, String> strings,
-        Map<String, ImmutableMap<String, String>> plurals,
-        Multimap<String, String> arrays) {
-      this.strings = Preconditions.checkNotNull(strings);
-      this.plurals = Preconditions.checkNotNull(plurals);
-      this.arrays = Preconditions.checkNotNull(arrays);
-    }
-
-    public StringResources getMergedResources(StringResources otherResources) {
-      Map<String, String> stringsMap = new HashMap<>(otherResources.strings);
-      Map<String, ImmutableMap<String, String>> pluralsMap = new HashMap<>(otherResources.plurals);
-      Multimap<String, String> arraysMap = ArrayListMultimap.create(otherResources.arrays);
-
-      stringsMap.putAll(strings);
-      pluralsMap.putAll(plurals);
-      arraysMap.putAll(arrays);
-
-      return new StringResources(stringsMap, pluralsMap, arraysMap);
-    }
-
-    public ImmutableMap<String, Object> asMap() {
-      return ImmutableMap.<String, Object>builder()
-          .putAll(strings)
-          .putAll(plurals)
-          .putAll(arrays.asMap())
-          .build();
-    }
+    return "Combine, parse string resource xml files into one binary file per locale.";
   }
 }
diff --git a/src/com/facebook/buck/android/FilterResourcesStep.java b/src/com/facebook/buck/android/FilterResourcesStep.java
index 966c0c2..04957c3 100644
--- a/src/com/facebook/buck/android/FilterResourcesStep.java
+++ b/src/com/facebook/buck/android/FilterResourcesStep.java
@@ -137,6 +137,10 @@
     return nonEnglishStringFilesBuilder.build();
   }
 
+  public ImmutableSet<String> getOutputResourceDirs() {
+    return inResDirToOutResDirMap.values();
+  }
+
   private int doExecute(ExecutionContext context) throws IOException {
     List<Predicate<File>> filePredicates = Lists.newArrayList();
     if (filterDrawables) {
diff --git a/src/com/facebook/buck/android/GenRDotJavaStep.java b/src/com/facebook/buck/android/GenRDotJavaStep.java
index 5ce4796..aa0285f 100644
--- a/src/com/facebook/buck/android/GenRDotJavaStep.java
+++ b/src/com/facebook/buck/android/GenRDotJavaStep.java
@@ -42,6 +42,7 @@
 
   /**
    * Creates a command that will run {@code aapt} for the purpose of generating {@code R.java}.
+   * Additionally, this command will generate the corresponding {@code R.txt} file.
    * @param resDirectories Directories of resource files. Will be specified with {@code -S} to
    *     {@code aapt}
    * @param genDirectoryPath Directory where {@code R.java} and potentially {@code R.txt} will be
@@ -51,13 +52,11 @@
    *     {@code R.java} file. For this class, the client must specify the {@code package} directly
    *     rather than the path to {@code AndroidManifest.xml}. This precludes the need to keep a
    *     number of dummy {@code AndroidManifest.xml} files in the codebase.
-   * @param isTempRDotJava If true, this command is being run solely for the purpose of generating
-   *     {@code R.txt} (though {@code R.java} will still be generated as a side-effect). The values
-   *     of the resource values in the generated {@code R.java} will be meaningless.
+   * @param isTempRDotJava If true, the values of the resource values in the generated
+   *     {@code R.java} will be meaningless.
    *     <p>
    *     If false, this command will produce an {@code R.java} file with resource values designed to
-   *     match those in an .apk that includes the resources. In this case, no {@code R.txt} will be
-   *     generated.
+   *     match those in an .apk that includes the resources.
    * @param extraLibraryPackages
    */
   public GenRDotJavaStep(
@@ -114,8 +113,8 @@
       builder.add("-S").add(res);
     }
 
+    builder.add("--output-text-symbols").add(genDirectoryPath);
     if (isTempRDotJava) {
-      builder.add("--output-text-symbols").add(genDirectoryPath);
       builder.add("--non-constant-id");
     }
 
diff --git a/src/com/facebook/buck/android/StringResources.java b/src/com/facebook/buck/android/StringResources.java
new file mode 100644
index 0000000..073cd89
--- /dev/null
+++ b/src/com/facebook/buck/android/StringResources.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2013-present Facebook, Inc.
+ *
+ * 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.facebook.buck.android;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.TreeMultimap;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Represents string resources of types string, plural and array for a locale. Also responsible
+ * for generating a custom format binary file for the resources.
+ */
+public class StringResources {
+  public final TreeMap<Integer, String> strings;
+  public final TreeMap<Integer, ImmutableMap<String, String>> plurals;
+  public final TreeMultimap<Integer, String> arrays;
+
+  /**
+   * These are the 6 fixed plural categories for string resources in Android. This mapping is not
+   * expected to change over time. We encode them as integers to optimize space.
+   *
+   * <p>For more information, refer to:
+   * <a href="http://developer.android.com/guide/topics/resources/string-resource.html#Plurals">
+   *   String Resources | Android Developers
+   * </a></p>
+   */
+  private static final ImmutableMap<String, Integer> PLURAL_CATEGORY_MAP =
+      ImmutableMap.<String, Integer>builder()
+          .put("zero", 0)
+          .put("one", 1)
+          .put("two", 2)
+          .put("few", 3)
+          .put("many", 4)
+          .put("other", 5)
+          .build();
+
+  private static Charset charset = Charsets.UTF_8;
+
+  public StringResources(
+      TreeMap<Integer, String> strings,
+      TreeMap<Integer, ImmutableMap<String, String>> plurals,
+      TreeMultimap<Integer, String> arrays) {
+    this.strings = Preconditions.checkNotNull(strings);
+    this.plurals = Preconditions.checkNotNull(plurals);
+    this.arrays = Preconditions.checkNotNull(arrays);
+  }
+
+  public StringResources getMergedResources(StringResources otherResources) {
+    TreeMap<Integer, String> stringsMap = Maps.newTreeMap(otherResources.strings);
+    TreeMap<Integer, ImmutableMap<String, String>> pluralsMap =
+        Maps.newTreeMap(otherResources.plurals);
+    TreeMultimap<Integer, String> arraysMap = TreeMultimap.create(otherResources.arrays);
+
+    stringsMap.putAll(strings);
+    pluralsMap.putAll(plurals);
+    arraysMap.putAll(arrays);
+
+    return new StringResources(stringsMap, pluralsMap, arraysMap);
+  }
+
+  /**
+   * Returns a byte array that represents the entire set of strings, plurals and string arrays in
+   * the following binary file format:
+   * <p>
+   * <pre>
+   *   [Short: #strings][Short: #plurals][Short: #arrays]
+   *   [Int: Smallest resource id among strings]
+   *   [Short: resource id offset][Short: length of the string] x #strings
+   *   [Int: Smallest resource id among plurals]
+   *   [Short: resource id offset][Short: length of string representing the plural] x #plurals
+   *   [Int: Smallest resource id among arrays]
+   *   [Short: resource id offset][Short: length of string representing the array] x #arrays
+   *   [Byte array of the string value] x #strings
+   *   [[Byte: #categories][[Byte: category][Short: length of plural][plural]] x #categories] x #plurals
+   *   [[Byte: #elements][[Short: length of element][element]] x #elements] x #arrays
+   * </pre>
+   * </p>
+   */
+  public byte[] getBinaryFileContent() {
+    try (
+      ByteArrayOutputStream bos1 = new ByteArrayOutputStream();
+      DataOutputStream mapOutStream = new DataOutputStream(bos1);
+      ByteArrayOutputStream bos2 = new ByteArrayOutputStream();
+      DataOutputStream dataOutStream = new DataOutputStream(bos2)
+    ) {
+      writeMapsSizes(mapOutStream);
+
+      writeStrings(mapOutStream, dataOutStream);
+      writePlurals(mapOutStream, dataOutStream);
+      writeArrays(mapOutStream, dataOutStream);
+
+      byte[] result = new byte[bos1.size() + bos2.size()];
+      System.arraycopy(bos1.toByteArray(), 0, result, 0, bos1.size());
+      System.arraycopy(bos2.toByteArray(), 0, result, bos1.size(), bos2.size());
+      return result;
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  private void writeMapsSizes(DataOutputStream stream) throws IOException {
+    stream.writeShort(strings.size());
+    stream.writeShort(plurals.size());
+    stream.writeShort(arrays.keySet().size());
+  }
+
+  private void writeStrings(DataOutputStream mapStream, DataOutputStream dataStream)
+      throws IOException {
+    if (strings.isEmpty()) {
+      return;
+    }
+    int smallestResourceId = strings.firstKey();
+    mapStream.writeInt(smallestResourceId);
+    for (Map.Entry<Integer, String> entry : strings.entrySet()) {
+      byte[] resourceBytes = entry.getValue().getBytes(charset);
+      writeMapEntry(mapStream, entry.getKey() - smallestResourceId, resourceBytes.length);
+      dataStream.write(resourceBytes);
+    }
+  }
+
+  private void writePlurals(DataOutputStream mapStream, DataOutputStream dataStream)
+      throws IOException {
+    if (plurals.isEmpty()) {
+      return;
+    }
+    int smallestResourceId = plurals.firstKey();
+    mapStream.writeInt(smallestResourceId);
+    for (Map.Entry<Integer, ImmutableMap<String, String>> entry : plurals.entrySet()) {
+      ImmutableMap<String, String> categoryMap = entry.getValue();
+      dataStream.writeByte(categoryMap.size());
+      int resourceDataLength = 1;
+
+      for (Map.Entry<String, String> cat : categoryMap.entrySet()) {
+        dataStream.writeByte(PLURAL_CATEGORY_MAP.get(cat.getKey()).byteValue());
+        byte[] pluralValue = cat.getValue().getBytes(charset);
+        dataStream.writeShort(pluralValue.length);
+        dataStream.write(pluralValue);
+        resourceDataLength += 3 + pluralValue.length;
+      }
+
+      writeMapEntry(mapStream, entry.getKey() - smallestResourceId, resourceDataLength);
+    }
+  }
+
+  private void writeArrays(DataOutputStream mapStream, DataOutputStream dataStream)
+      throws IOException {
+    if (arrays.keySet().isEmpty()) {
+      return;
+    }
+    boolean first = true;
+    int smallestResourceId = 0;
+    for (int resourceId : arrays.keySet()) {
+      if (first) {
+        first = false;
+        smallestResourceId = resourceId;
+        mapStream.writeInt(smallestResourceId);
+      }
+      Collection<String> arrayValues = arrays.get(resourceId);
+      dataStream.writeByte(arrayValues.size());
+      int resourceDataLength = 1;
+
+      for (String arrayValue : arrayValues) {
+        byte[] byteValue = arrayValue.getBytes(charset);
+        dataStream.writeShort(byteValue.length);
+        dataStream.write(byteValue);
+        resourceDataLength += 2 + byteValue.length;
+      }
+
+      writeMapEntry(mapStream, resourceId - smallestResourceId, resourceDataLength);
+    }
+  }
+
+  private void writeMapEntry(DataOutputStream stream, int resourceOffset, int length)
+      throws IOException {
+    stream.writeShort(resourceOffset);
+    stream.writeShort(length);
+  }
+}
diff --git a/src/com/facebook/buck/android/UberRDotJavaUtil.java b/src/com/facebook/buck/android/UberRDotJavaUtil.java
index 7d82313..ebf81c1 100644
--- a/src/com/facebook/buck/android/UberRDotJavaUtil.java
+++ b/src/com/facebook/buck/android/UberRDotJavaUtil.java
@@ -78,10 +78,7 @@
       BuildTarget buildTarget,
       ImmutableList.Builder<Step> commands) {
     // Create the path where the R.java files will be generated.
-    String rDotJavaSrc = String.format("%s/%s__%s_uber_rdotjava_src__",
-        BuckConstant.BIN_DIR,
-        buildTarget.getBasePathWithSlash(),
-        buildTarget.getShortName());
+    String rDotJavaSrc = getPathToGeneratedRDotJavaSrcFiles(buildTarget);
     commands.add(new MakeCleanDirectoryStep(rDotJavaSrc));
 
     // Generate the R.java files.
@@ -108,6 +105,13 @@
     commands.add(javac);
   }
 
+  public static String getPathToGeneratedRDotJavaSrcFiles(BuildTarget buildTarget) {
+    return String.format("%s/%s__%s_uber_rdotjava_src__",
+        BuckConstant.BIN_DIR,
+        buildTarget.getBasePathWithSlash(),
+        buildTarget.getShortName());
+  }
+
   public static String getPathToCompiledRDotJavaFiles(BuildTarget buildTarget) {
     return String.format("%s/%s__%s_uber_rdotjava_bin__",
         BuckConstant.BIN_DIR,
diff --git a/src/com/facebook/buck/util/ProjectFilesystem.java b/src/com/facebook/buck/util/ProjectFilesystem.java
index 58f4344..b7ba953 100644
--- a/src/com/facebook/buck/util/ProjectFilesystem.java
+++ b/src/com/facebook/buck/util/ProjectFilesystem.java
@@ -238,6 +238,10 @@
     Files.write(contents, getFileForRelativePath(pathRelativeToProjectRoot), Charsets.UTF_8);
   }
 
+  public void writeBytesToPath(byte[] bytes, Path pathRelativeToProjectRoot) throws IOException {
+    Files.write(bytes, getFileForRelativePath(pathRelativeToProjectRoot));
+  }
+
   public Optional<String> readFileIfItExists(Path pathRelativeToProjectRoot) {
     File fileToRead = getFileForRelativePath(pathRelativeToProjectRoot);
     return readFileIfItExists(fileToRead, pathRelativeToProjectRoot.toString());
diff --git a/test/com/facebook/buck/android/AndroidBinaryRuleTest.java b/test/com/facebook/buck/android/AndroidBinaryRuleTest.java
index 12d8cc3..a8550ef 100644
--- a/test/com/facebook/buck/android/AndroidBinaryRuleTest.java
+++ b/test/com/facebook/buck/android/AndroidBinaryRuleTest.java
@@ -55,7 +55,6 @@
 import com.google.common.base.Predicates;
 import com.google.common.base.Strings;
 import com.google.common.collect.FluentIterable;
-import com.google.common.collect.ImmutableBiMap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -579,32 +578,15 @@
         .setResourceCompressionMode("enabled_with_strings_as_assets");
 
     AndroidBinaryRule buildRule = resolver.buildAndAddToIndex(builder);
-    ImmutableList.Builder<Step> commandsBuilder = ImmutableList.builder();
     Set<String> resourceDirectories = ImmutableSet.of("one", "two");
 
-    Set<String> filteredResDirs = buildRule.getFilteredResourceDirectories(
-        commandsBuilder, resourceDirectories);
+    FilterResourcesStep filterResourcesStep = buildRule.getFilterResourcesStep(resourceDirectories);
 
     assertEquals(
         ImmutableSet.of(
             "buck-out/bin/__filtered__target__/0",
             "buck-out/bin/__filtered__target__/1"),
-        filteredResDirs);
-
-    ImmutableList<Step> commands = commandsBuilder.build();
-    assertEquals(3, commands.size());
-
-    FilterResourcesStep resourcesStep = (FilterResourcesStep)commands.get(0);
-    MakeCleanDirectoryStep cleanDirectoryStep = (MakeCleanDirectoryStep)commands.get(1);
-
-    assertTrue(resourcesStep.isFilterStrings());
-    assertEquals("mdpi", resourcesStep.getResourceFilter());
-    assertEquals(ImmutableBiMap.of(
-        "one", "buck-out/bin/__filtered__target__/0",
-        "two", "buck-out/bin/__filtered__target__/1"),
-        resourcesStep.getInResDirToOutResDirMap());
-
-    assertEquals("buck-out/bin/__strings_target__", cleanDirectoryStep.getPath());
+        filterResourcesStep.getOutputResourceDirs());
   }
 
   private void createAndroidBinaryRuleAndTestCopyNativeLibraryCommand(
diff --git a/test/com/facebook/buck/android/CompileStringsStepTest.java b/test/com/facebook/buck/android/CompileStringsStepTest.java
index a930e07..1e4c636 100644
--- a/test/com/facebook/buck/android/CompileStringsStepTest.java
+++ b/test/com/facebook/buck/android/CompileStringsStepTest.java
@@ -16,32 +16,31 @@
 
 package com.facebook.buck.android;
 
-import static org.easymock.EasyMock.anyObject;
 import static org.easymock.EasyMock.expect;
-import static org.easymock.EasyMock.expectLastCall;
-import static org.easymock.EasyMock.getCurrentArguments;
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 
 import com.facebook.buck.step.ExecutionContext;
 import com.facebook.buck.util.ProjectFilesystem;
 import com.facebook.buck.util.XmlDomParser;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
+import com.google.common.io.Files;
 
 import org.easymock.EasyMockSupport;
-import org.easymock.IAnswer;
 import org.junit.Test;
 import org.w3c.dom.NodeList;
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.charset.Charset;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.regex.Matcher;
 
@@ -82,6 +81,36 @@
   }
 
   @Test
+  public void testRDotTxtContentsPattern() {
+    testContentRegex("  int string r_name 0xdeadbeef", false, null, null, null);
+    testContentRegex("int string r_name 0xdeadbeef  ", false, null, null, null);
+    testContentRegex("int string r_name 0xdeadbeef", true, "string", "r_name", "deadbeef");
+    testContentRegex("int string r_name 0x", false, null, null, null);
+    testContentRegex("int array r_name 0xdead", true, "array", "r_name", "dead");
+    testContentRegex("int plurals r_name 0xdead", true, "plurals", "r_name", "dead");
+    testContentRegex("int plural r_name 0xdead", false, null, null, null);
+    testContentRegex("int plurals r name 0xdead", false, null, null, null);
+    testContentRegex("int[] string r_name 0xdead", false, null, null, null);
+  }
+
+  private void testContentRegex(
+      String input,
+      boolean matches,
+      String resourceType,
+      String resourceName,
+      String resourceId) {
+
+    Matcher matcher = CompileStringsStep.R_DOT_TXT_STRING_RESOURCE_PATTERN.matcher(input);
+    assertEquals(matches, matcher.matches());
+    if (!matches) {
+      return;
+    }
+    assertEquals("Resource type does not match.", resourceType, matcher.group(1));
+    assertEquals("Resource name does not match.", resourceName, matcher.group(2));
+    assertEquals("Resource id does not match.", resourceId, matcher.group(3));
+  }
+
+  @Test
   public void testGroupFilesByLocale() {
     ImmutableSet<String> files = ImmutableSet.of(
         "/project/dir/res/values-da/strings.xml",
@@ -106,7 +135,7 @@
           .putAll("es", ImmutableSet.<String>of("/project/foreveralone/res/values-es/strings.xml"))
           .build();
 
-    assertEquals(expectedMap, groupedByLocale);
+    assertEquals("Incorrect grouping of files by locale.", expectedMap, groupedByLocale);
   }
 
   @Test
@@ -121,16 +150,24 @@
     NodeList stringNodes = XmlDomParser.parse(createResourcesXml(xmlInput))
         .getElementsByTagName("string");
 
-    Map<String, String> stringsMap = new HashMap<>();
-    createNonExecutingStep().scrapeStringNodes(stringNodes, stringsMap);
+    Map<Integer, String> stringsMap = Maps.newHashMap();
+    CompileStringsStep step = createNonExecutingStep();
+    step.addResourceNameToIdMap(ImmutableMap.of(
+        "name1", 1,
+        "name2", 2,
+        "name3", 3,
+        "name4", 4,
+        "name5", 5));
+    step.scrapeStringNodes(stringNodes, stringsMap);
 
     assertEquals(
+        "Incorrect map of resource id to string values.",
         ImmutableMap.of(
-            "name1", "Value1",
-            "name2", "Value with space",
-            "name3", "Value with \"quotes\"",
-            "name4", "",
-            "name5", "Value with %1$s"),
+            1, "Value1",
+            2, "Value with space",
+            3, "Value with \"quotes\"",
+            4, "",
+            5, "Value with %1$s"),
         stringsMap
     );
   }
@@ -152,19 +189,25 @@
     NodeList pluralsNodes = XmlDomParser.parse(createResourcesXml(xmlInput))
         .getElementsByTagName("plurals");
 
-    Map<String, ImmutableMap<String, String>> pluralsMap = new HashMap<>();
-    createNonExecutingStep().scrapePluralsNodes(pluralsNodes, pluralsMap);
+    Map<Integer, ImmutableMap<String, String>> pluralsMap = Maps.newHashMap();
+    CompileStringsStep step = createNonExecutingStep();
+    step.addResourceNameToIdMap(ImmutableMap.of(
+        "name1", 1,
+        "name2", 2,
+        "name3", 3));
+    step.scrapePluralsNodes(pluralsNodes, pluralsMap);
 
     assertEquals(
+        "Incorrect map of resource id to plural values.",
         ImmutableMap.of(
-            "name1", ImmutableMap.of(
+            1, ImmutableMap.of(
                 "zero", "%d people saw this",
                 "one", "%d person saw this",
                 "many", "%d people saw this"),
-            "name2", ImmutableMap.of(
+            2, ImmutableMap.of(
                 "zero", "%d people ate this",
                 "many", "%d people ate this"),
-            "name3", ImmutableMap.of()
+            3, ImmutableMap.of()
         ),
         pluralsMap
     );
@@ -188,14 +231,20 @@
     NodeList arrayNodes = XmlDomParser.parse(createResourcesXml(xmlInput))
         .getElementsByTagName("string-array");
 
-    Multimap<String, String> arraysMap = ArrayListMultimap.create();
-    createNonExecutingStep().scrapeStringArrayNodes(arrayNodes, arraysMap);
+    Multimap<Integer, String> arraysMap = ArrayListMultimap.create();
+    CompileStringsStep step = createNonExecutingStep();
+    step.addResourceNameToIdMap(ImmutableMap.of(
+        "name1", 1,
+        "name2", 2,
+        "name3", 3));
+    step.scrapeStringArrayNodes(arrayNodes, arraysMap);
 
     assertEquals(
+        "Incorrect map of resource id to string arrays.",
         ImmutableMultimap.builder()
-            .put("name1", "Value11")
-            .put("name1", "Value12")
-            .put("name2", "Value21")
+            .put(1, "Value11")
+            .put(1, "Value12")
+            .put(2, "Value21")
             .build(),
         arraysMap
     );
@@ -205,7 +254,7 @@
     return new CompileStringsStep(
         createMock(FilterResourcesStep.class),
         createMock(Path.class),
-        createMock(ObjectMapper.class));
+        createMock(Path.class));
   }
 
   private String createResourcesXml(String contents) {
@@ -215,35 +264,11 @@
   @Test
   public void testSuccessfulStepExecution() throws IOException {
     Path destinationDir = Paths.get("");
+    Path rDotJavaSrcDir = Paths.get("");
 
     ExecutionContext context = createMock(ExecutionContext.class);
-    ProjectFilesystem filesystem = createMock(ProjectFilesystem.class);
-    expect(filesystem.getFileForRelativePath(anyObject(Path.class))).andAnswer(new IAnswer<File>() {
-      @Override
-      public File answer() throws Throwable {
-        return ((Path)getCurrentArguments()[0]).toFile();
-      }
-    }).times(3);
-    expect(context.getProjectFilesystem()).andReturn(filesystem);
-
-    ObjectMapper mapper = createMock(ObjectMapper.class);
-    mapper.writeValue(anyObject(File.class), anyObject());
-
-    final ImmutableMap.Builder<String, ImmutableMap<String, Object>> capturedResourcesBuilder =
-        ImmutableMap.builder();
-    expectLastCall().andAnswer(new IAnswer<Object>() {
-      @Override
-      public Object answer() throws Throwable {
-        File file = (File)getCurrentArguments()[0];
-        capturedResourcesBuilder.put(file.getName(), getMapFromArgs());
-        return null;
-      }
-
-      @SuppressWarnings("unchecked")
-      private ImmutableMap<String, Object> getMapFromArgs() {
-        return (ImmutableMap<String, Object>) getCurrentArguments()[1];
-      }
-    }).times(3);
+    FakeProjectFileSystem fileSystem = new FakeProjectFileSystem();
+    expect(context.getProjectFilesystem()).andStubReturn(fileSystem);
 
     FilterResourcesStep filterResourcesStep = createMock(FilterResourcesStep.class);
     expect(filterResourcesStep.getNonEnglishStringFiles()).andReturn(ImmutableSet.of(
@@ -253,27 +278,48 @@
         FOURTH_FILE));
 
     replayAll();
-    CompileStringsStep step = new CompileStringsStep(filterResourcesStep, destinationDir, mapper);
+    CompileStringsStep step = new CompileStringsStep(
+        filterResourcesStep,
+        rDotJavaSrcDir,
+        destinationDir);
     assertEquals(0, step.execute(context));
-    assertEquals(
-        ImmutableMap.of(
-            "es.json", ImmutableMap.of(
-                "name1_1", "Value11",
-                "name1_2", "Value12",
-                "name1_3", "Value13",
-                "name2_1", "Value21",
-                "name2_2", "Value22"),
-            "pt.json", ImmutableMap.of(
-                "name3_1", "Value31",
-                "name3_2", "Value32",
-                "name3_3", "Value33"),
-            "pt_BR.json", ImmutableMap.of(
-                "name3_1", "Value311",
-                "name3_2", "Value32",
-                "name3_3", "Value33")
-        ),
-        capturedResourcesBuilder.build());
+    Map<String, byte[]> fileContentsMap = fileSystem.getFileContents();
+    assertEquals("Incorrect number of string files written.", 3, fileContentsMap.size());
+    for (Map.Entry<String, byte[]> entry : fileContentsMap.entrySet()) {
+      File expectedFile = Paths.get(TESTDATA_DIR + entry.getKey()).toFile();
+      assertArrayEquals(Files.toByteArray(expectedFile), fileContentsMap.get(entry.getKey()));
+    }
 
     verifyAll();
   }
+
+
+  private static class FakeProjectFileSystem extends ProjectFilesystem {
+
+    private ImmutableMap.Builder<String, byte[]> fileContentsMapBuilder = ImmutableMap.builder();
+
+    public FakeProjectFileSystem() {
+      super(new File("."));
+    }
+
+    @Override
+    public File getFileForRelativePath(Path path) {
+      return path.toFile();
+    }
+
+    @Override
+    public List<String> readLines(Path path) throws IOException {
+      Path fullPath = Paths.get(TESTDATA_DIR).resolve(path);
+      return Files.readLines(fullPath.toFile(), Charset.defaultCharset());
+    }
+
+    @Override
+    public void writeBytesToPath(byte[] content, Path path) {
+      fileContentsMapBuilder.put(path.getFileName().toString(), content);
+    }
+
+    public Map<String, byte[]> getFileContents() {
+      return fileContentsMapBuilder.build();
+    }
+  }
 }
diff --git a/test/com/facebook/buck/android/StringResourcesTest.java b/test/com/facebook/buck/android/StringResourcesTest.java
new file mode 100644
index 0000000..e734f51
--- /dev/null
+++ b/test/com/facebook/buck/android/StringResourcesTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2013-present Facebook, Inc.
+ *
+ * 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.facebook.buck.android;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.TreeMultimap;
+
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.TreeMap;
+
+public class StringResourcesTest {
+
+  private static final ImmutableMap<Integer, String> strings = ImmutableMap.of(
+      12345678, "S_one",
+      12345679, "S_two",
+      12345680, "S_three");
+
+  private static final ImmutableMap<String, String> plural1 = ImmutableMap.of(
+      "one", "P1_one",
+      "few", "P1_few",
+      "many", "P1_many");
+
+  private static final ImmutableMap<String, String> plural2 = ImmutableMap.of(
+      "zero", "P2_zero",
+      "two", "P2_two",
+      "other", "P2_other");
+
+  private static final ImmutableMap<Integer, ImmutableMap<String, String>> plurals =
+      ImmutableMap.of(
+          12345689, plural1,
+          12345692, plural2);
+
+  private static final ImmutableMultimap<Integer, String> arrays =
+      ImmutableMultimap.<Integer, String>builder()
+          .putAll(12345694, Arrays.asList("A1_one", "A1_two"))
+          .putAll(12345699, Arrays.asList("A2_one"))
+          .build();
+
+
+  @Test
+  public void testBinaryStream() throws IOException {
+    TreeMap<Integer, String> stringsMap = Maps.newTreeMap();
+    stringsMap.putAll(strings);
+    TreeMap<Integer, ImmutableMap<String, String>> pluralsMap = Maps.newTreeMap();
+    pluralsMap.putAll(plurals);
+    TreeMultimap<Integer, String> arraysMap = TreeMultimap.create();
+    arraysMap.putAll(arrays);
+    byte[] binaryOutput = new StringResources(stringsMap, pluralsMap, arraysMap)
+        .getBinaryFileContent();
+
+    verifyBinaryStream(binaryOutput);
+  }
+
+  private void verifyBinaryStream(byte[] binaryOutput) throws IOException {
+    DataInputStream stream = new DataInputStream(new ByteArrayInputStream(binaryOutput));
+    assertEquals(3, stream.readShort());
+    assertEquals(2, stream.readShort());
+    assertEquals(2, stream.readShort());
+
+    assertEquals(12345678, stream.readInt());
+    assertEquals(0, stream.readShort());
+    assertEquals(5, stream.readShort());
+    assertEquals(1, stream.readShort());
+    assertEquals(5, stream.readShort());
+    assertEquals(2, stream.readShort());
+    assertEquals(7, stream.readShort());
+
+    assertEquals(12345689, stream.readInt());
+    assertEquals(0, stream.readShort());
+    assertEquals(29, stream.readShort());
+    assertEquals(3, stream.readShort());
+    assertEquals(31, stream.readShort());
+
+    assertEquals(12345694, stream.readInt());
+    assertEquals(0, stream.readShort());
+    assertEquals(17, stream.readShort());
+    assertEquals(5, stream.readShort());
+    assertEquals(9, stream.readShort());
+
+    // string values
+    assertEquals("S_one", readStringOfLength(stream, 5));
+    assertEquals("S_two", readStringOfLength(stream, 5));
+    assertEquals("S_three", readStringOfLength(stream, 7));
+
+    // plural values
+    assertEquals(3, stream.readByte()); // number of categories.
+    assertEquals(1, stream.readByte());
+    assertEquals(6, stream.readShort());
+    assertEquals("P1_one", readStringOfLength(stream, 6));
+    assertEquals(3, stream.readByte());
+    assertEquals(6, stream.readShort());
+    assertEquals("P1_few", readStringOfLength(stream, 6));
+    assertEquals(4, stream.readByte());
+    assertEquals(7, stream.readShort());
+    assertEquals("P1_many", readStringOfLength(stream, 7));
+
+    assertEquals(3, stream.readByte()); // number of categories.
+    assertEquals(0, stream.readByte());
+    assertEquals(7, stream.readShort());
+    assertEquals("P2_zero", readStringOfLength(stream, 7));
+    assertEquals(2, stream.readByte());
+    assertEquals(6, stream.readShort());
+    assertEquals("P2_two", readStringOfLength(stream, 6));
+    assertEquals(5, stream.readByte());
+    assertEquals(8, stream.readShort());
+    assertEquals("P2_other", readStringOfLength(stream, 8));
+
+    //array values
+    assertEquals(2, stream.readByte()); // number of array elements.
+    assertEquals(6, stream.readShort());
+    assertEquals("A1_one", readStringOfLength(stream, 6));
+    assertEquals(6, stream.readShort());
+    assertEquals("A1_two", readStringOfLength(stream, 6));
+
+    assertEquals(1, stream.readByte()); // number of array elements.
+    assertEquals(6, stream.readShort());
+    assertEquals("A2_one", readStringOfLength(stream, 6));
+  }
+
+  private String readStringOfLength(DataInputStream stream, int length) throws IOException {
+    byte[] data = new byte[length];
+    assertEquals(length, stream.read(data, 0, length));
+    return new String(data);
+  }
+}
diff --git a/testdata/com/facebook/buck/android/R.txt b/testdata/com/facebook/buck/android/R.txt
new file mode 100644
index 0000000..a075c96
--- /dev/null
+++ b/testdata/com/facebook/buck/android/R.txt
@@ -0,0 +1,8 @@
+int string name1_1 0x7eadbeef
+int string name1_2 0x7eadc0de
+int string name1_3 0x7eadbabe
+int string name2_1 0x7eadfa11
+int string name2_2 0x7eadba11
+int string name3_1 0x7eadbee1
+int string name3_2 0x7eadfade
+int string name3_3 0x7eadf00d
diff --git a/testdata/com/facebook/buck/android/es.fbstr b/testdata/com/facebook/buck/android/es.fbstr
new file mode 100644
index 0000000..dad3769
--- /dev/null
+++ b/testdata/com/facebook/buck/android/es.fbstr
Binary files differ
diff --git a/testdata/com/facebook/buck/android/pt.fbstr b/testdata/com/facebook/buck/android/pt.fbstr
new file mode 100644
index 0000000..495b871
--- /dev/null
+++ b/testdata/com/facebook/buck/android/pt.fbstr
Binary files differ
diff --git a/testdata/com/facebook/buck/android/pt_BR.fbstr b/testdata/com/facebook/buck/android/pt_BR.fbstr
new file mode 100644
index 0000000..7582074
--- /dev/null
+++ b/testdata/com/facebook/buck/android/pt_BR.fbstr
Binary files differ