Update the strings asset file format for easier parsing.
Summary:
The new file format makes it so that parsing sections of the file one at a time
becomes easier.
Also, switch some of the `byte` and `short` to `int` to make it more flexible.
Intentionally left the `byte` data type in plurals since the number of plural
categories is guaranteed to be <= 6.
To make it easier to maintain the unit test, I changed the file format to
something easy to understand/edit.
Test Plan: ant test
diff --git a/src/com/facebook/buck/android/StringResources.java b/src/com/facebook/buck/android/StringResources.java
index 073cd89..77b8aba 100644
--- a/src/com/facebook/buck/android/StringResources.java
+++ b/src/com/facebook/buck/android/StringResources.java
@@ -35,6 +35,14 @@
* for generating a custom format binary file for the resources.
*/
public class StringResources {
+
+ /**
+ * Bump this whenever there's a change in the file format. The parser can decide to abort parsing
+ * if the version it finds in the file does not match it's own version, thereby avoiding
+ * potential data corruption issues.
+ */
+ private static final int FORMAT_VERSION = 1;
+
public final TreeMap<Integer, String> strings;
public final TreeMap<Integer, ImmutableMap<String, String>> plurals;
public final TreeMultimap<Integer, String> arrays;
@@ -87,116 +95,118 @@
* the following binary file format:
* <p>
* <pre>
- * [Short: #strings][Short: #plurals][Short: #arrays]
+ * [Int: Version]
+ * [Int: # of strings]
* [Int: Smallest resource id among strings]
- * [Short: resource id offset][Short: length of the string] x #strings
+ * [Short: resource id delta][Short: length of the string] x # of strings
+ * [Byte array of the string value] x # of strings
+ * [Int: # of plurals]
* [Int: Smallest resource id among plurals]
- * [Short: resource id offset][Short: length of string representing the plural] x #plurals
+ * [[Short: resource id delta][Byte: #categories][[Byte: category][Short: length of plural
+ * value]] x #categories] x # of plurals
+ * [Byte array of plural value] x Summation of plural categories over # of plurals
+ * [Int: # of arrays]
* [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
+ * [[Short: resource id delta][Int: #elements][Short: length of element] x #elements] x # of
+ * arrays
+ * [Byte array of string value] x Summation of array elements over # of 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)
+ ByteArrayOutputStream bytesStream = new ByteArrayOutputStream();
+ DataOutputStream outputStream = new DataOutputStream(bytesStream)
) {
- writeMapsSizes(mapOutStream);
+ outputStream.writeInt(FORMAT_VERSION);
- writeStrings(mapOutStream, dataOutStream);
- writePlurals(mapOutStream, dataOutStream);
- writeArrays(mapOutStream, dataOutStream);
+ writeStrings(outputStream);
+ writePlurals(outputStream);
+ writeArrays(outputStream);
- 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;
+ return bytesStream.toByteArray();
} 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 {
+ private void writeStrings(DataOutputStream outputStream) throws IOException {
+ outputStream.writeInt(strings.size());
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);
+ int previousResourceId = strings.firstKey();
+ outputStream.writeInt(previousResourceId);
+
+ try (ByteArrayOutputStream dataStream = new ByteArrayOutputStream()) {
+ for (Map.Entry<Integer, String> entry : strings.entrySet()) {
+ byte[] resourceBytes = entry.getValue().getBytes(charset);
+ writeShort(outputStream, entry.getKey() - previousResourceId);
+ writeShort(outputStream, resourceBytes.length);
+ dataStream.write(resourceBytes, 0, resourceBytes.length);
+
+ previousResourceId = entry.getKey();
+ }
+ outputStream.write(dataStream.toByteArray());
}
}
- private void writePlurals(DataOutputStream mapStream, DataOutputStream dataStream)
- throws IOException {
+ private void writePlurals(DataOutputStream outputStream) throws IOException {
+ outputStream.writeInt(plurals.size());
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;
+ int previousResourceId = plurals.firstKey();
+ outputStream.writeInt(previousResourceId);
- 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;
+ try (ByteArrayOutputStream dataStream = new ByteArrayOutputStream()) {
+ for (Map.Entry<Integer, ImmutableMap<String, String>> entry : plurals.entrySet()) {
+ writeShort(outputStream, entry.getKey() - previousResourceId);
+ ImmutableMap<String, String> categoryMap = entry.getValue();
+ outputStream.writeByte(categoryMap.size());
+
+ for (Map.Entry<String, String> cat : categoryMap.entrySet()) {
+ outputStream.writeByte(PLURAL_CATEGORY_MAP.get(cat.getKey()).byteValue());
+ byte[] pluralValue = cat.getValue().getBytes(charset);
+ writeShort(outputStream, pluralValue.length);
+ dataStream.write(pluralValue);
+ }
+
+ previousResourceId = entry.getKey();
}
- writeMapEntry(mapStream, entry.getKey() - smallestResourceId, resourceDataLength);
+ outputStream.write(dataStream.toByteArray());
}
}
- private void writeArrays(DataOutputStream mapStream, DataOutputStream dataStream)
- throws IOException {
+ private void writeArrays(DataOutputStream outputStream) throws IOException {
+ outputStream.writeInt(arrays.keySet().size());
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;
+ int previousResourceId = arrays.keySet().first();
+ outputStream.writeInt(previousResourceId);
+ try (ByteArrayOutputStream dataStream = new ByteArrayOutputStream()) {
+ for (int resourceId : arrays.keySet()) {
+ writeShort(outputStream, resourceId - previousResourceId);
+ Collection<String> arrayValues = arrays.get(resourceId);
+ outputStream.writeInt(arrayValues.size());
- for (String arrayValue : arrayValues) {
- byte[] byteValue = arrayValue.getBytes(charset);
- dataStream.writeShort(byteValue.length);
- dataStream.write(byteValue);
- resourceDataLength += 2 + byteValue.length;
- }
+ for (String arrayValue : arrayValues) {
+ byte[] byteValue = arrayValue.getBytes(charset);
+ writeShort(outputStream, byteValue.length);
+ dataStream.write(byteValue);
+ }
- writeMapEntry(mapStream, resourceId - smallestResourceId, resourceDataLength);
+ previousResourceId = resourceId;
+ }
+ outputStream.write(dataStream.toByteArray());
}
}
- private void writeMapEntry(DataOutputStream stream, int resourceOffset, int length)
- throws IOException {
- stream.writeShort(resourceOffset);
- stream.writeShort(length);
+ private void writeShort(DataOutputStream stream, int number) throws IOException {
+ Preconditions.checkState(number <= Short.MAX_VALUE,
+ "Error attempting to compact a numeral to short: " + number);
+ stream.writeShort(number);
}
}
diff --git a/test/com/facebook/buck/android/CompileStringsStepTest.java b/test/com/facebook/buck/android/CompileStringsStepTest.java
index 1e4c636..3aa4cfc 100644
--- a/test/com/facebook/buck/android/CompileStringsStepTest.java
+++ b/test/com/facebook/buck/android/CompileStringsStepTest.java
@@ -23,6 +23,7 @@
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.util.ProjectFilesystem;
import com.facebook.buck.util.XmlDomParser;
+import com.google.common.base.Splitter;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
@@ -35,6 +36,8 @@
import org.junit.Test;
import org.w3c.dom.NodeList;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
@@ -287,12 +290,44 @@
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()));
+ assertArrayEquals(createBinaryStream(expectedFile), fileContentsMap.get(entry.getKey()));
}
verifyAll();
}
+ private byte[] createBinaryStream(File expectedFile) throws IOException {
+ try (
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ DataOutputStream stream = new DataOutputStream(bos)
+ ) {
+ for (String line : Files.readLines(expectedFile, Charset.defaultCharset())) {
+ for (String token : Splitter.on('|').split(line)) {
+ char dataType = token.charAt(0);
+ String value = token.substring(2);
+ switch (dataType) {
+ case 'i':
+ stream.writeInt(Integer.parseInt(value));
+ break;
+ case 's':
+ stream.writeShort(Integer.parseInt(value));
+ break;
+ case 'b':
+ stream.writeByte(Integer.parseInt(value));
+ break;
+ case 't':
+ stream.write(value.getBytes());
+ break;
+ default:
+ throw new RuntimeException("Unexpected data type in .fbstr file: " + dataType);
+ }
+ }
+ }
+
+ return bos.toByteArray();
+ }
+ }
+
private static class FakeProjectFileSystem extends ProjectFilesystem {
diff --git a/test/com/facebook/buck/android/StringResourcesTest.java b/test/com/facebook/buck/android/StringResourcesTest.java
index e734f51..da08b8b 100644
--- a/test/com/facebook/buck/android/StringResourcesTest.java
+++ b/test/com/facebook/buck/android/StringResourcesTest.java
@@ -76,67 +76,68 @@
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());
+ // Version
+ assertEquals(1, stream.readInt());
+
+ // Strings
+ assertEquals(3, stream.readInt());
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(1, 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
+ // Plurals
+ assertEquals(2, stream.readInt());
+ assertEquals(12345689, stream.readInt());
+ assertEquals(0, stream.readShort());
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.readShort());
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());
+
+ // plural strings
+ assertEquals("P1_one", readStringOfLength(stream, 6));
+ assertEquals("P1_few", readStringOfLength(stream, 6));
+ assertEquals("P1_many", readStringOfLength(stream, 7));
+ assertEquals("P2_zero", readStringOfLength(stream, 7));
+ assertEquals("P2_two", readStringOfLength(stream, 6));
assertEquals("P2_other", readStringOfLength(stream, 8));
- //array values
- assertEquals(2, stream.readByte()); // number of array elements.
+ // Arrays
+ assertEquals(2, stream.readInt());
+ assertEquals(12345694, stream.readInt());
+ assertEquals(0, stream.readShort());
+ assertEquals(2, stream.readInt()); // 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(5, stream.readShort());
+ assertEquals(1, stream.readInt()); // number of array elements.
+ assertEquals(6, stream.readShort());
- assertEquals(1, stream.readByte()); // number of array elements.
- assertEquals(6, stream.readShort());
+ // array strings
+ assertEquals("A1_one", readStringOfLength(stream, 6));
+ assertEquals("A1_two", readStringOfLength(stream, 6));
assertEquals("A2_one", readStringOfLength(stream, 6));
}
diff --git a/testdata/com/facebook/buck/android/R.txt b/testdata/com/facebook/buck/android/R.txt
index a075c96..b189911 100644
--- a/testdata/com/facebook/buck/android/R.txt
+++ b/testdata/com/facebook/buck/android/R.txt
@@ -1,8 +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
+int string name1_1 0x7eadbee8
+int string name1_2 0x7eadbee9
+int string name1_3 0x7eadbeea
+int string name2_1 0x7eadbeeb
+int string name2_2 0x7eadbeec
+int string name3_1 0x7eadbeed
+int string name3_2 0x7eadbeee
+int string name3_3 0x7eadbeef
diff --git a/testdata/com/facebook/buck/android/es.fbstr b/testdata/com/facebook/buck/android/es.fbstr
index dad3769..58e5e23 100644
--- a/testdata/com/facebook/buck/android/es.fbstr
+++ 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
index 495b871..7e99138 100644
--- a/testdata/com/facebook/buck/android/pt.fbstr
+++ 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
index 7582074..30288a9 100644
--- a/testdata/com/facebook/buck/android/pt_BR.fbstr
+++ b/testdata/com/facebook/buck/android/pt_BR.fbstr
Binary files differ