Merge changes from topic "nov7"

* changes:
  Custom binary XML wire protocol.
  Progress towards efficient XML serialization.
  More efficient alternatives to ByteBuffer.
  CharsetUtils alternatives that avoid allocations.
This commit is contained in:
Jeff Sharkey
2020-11-10 23:35:51 +00:00
committed by Android (Google) Code Review
21 changed files with 4235 additions and 1 deletions

View File

@@ -0,0 +1,91 @@
/*
* Copyright (C) 2020 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 android.util;
import android.perftests.utils.BenchmarkState;
import android.perftests.utils.PerfStatusReporter;
import androidx.test.filters.LargeTest;
import dalvik.system.VMRuntime;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
@LargeTest
@RunWith(Parameterized.class)
public class CharsetUtilsPerfTest {
@Rule
public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
@Parameterized.Parameter(0)
public String mName;
@Parameterized.Parameter(1)
public String mValue;
@Parameterized.Parameters(name = "{0}")
public static Collection<Object[]> getParameters() {
return Arrays.asList(new Object[][] {
{ "simple", "com.example.typical_package_name" },
{ "complex", "從不喜歡孤單一個 - 蘇永康/吳雨霏" },
});
}
@Test
public void timeUpstream() {
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
mValue.getBytes(StandardCharsets.UTF_8);
}
}
/**
* Measure performance of writing into a small buffer where bounds checking
* requires careful measurement of encoded size.
*/
@Test
public void timeLocal_SmallBuffer() {
final byte[] dest = (byte[]) VMRuntime.getRuntime().newNonMovableArray(byte.class, 64);
final long destPtr = VMRuntime.getRuntime().addressOf(dest);
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
CharsetUtils.toUtf8Bytes(mValue, destPtr, 0, dest.length);
}
}
/**
* Measure performance of writing into a large buffer where bounds checking
* only needs a simple worst-case 4-bytes-per-char check.
*/
@Test
public void timeLocal_LargeBuffer() {
final byte[] dest = (byte[]) VMRuntime.getRuntime().newNonMovableArray(byte.class, 1024);
final long destPtr = VMRuntime.getRuntime().addressOf(dest);
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
CharsetUtils.toUtf8Bytes(mValue, destPtr, 0, dest.length);
}
}
}

View File

@@ -0,0 +1,292 @@
/*
* Copyright (C) 2020 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 android.util;
import static org.junit.Assert.assertEquals;
import android.os.Bundle;
import android.os.Debug;
import android.perftests.utils.BenchmarkState;
import android.perftests.utils.PerfStatusReporter;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.HexDump;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.xmlpull.v1.XmlPullParser;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.function.Supplier;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class XmlPerfTest {
/**
* Since allocation measurement adds overhead, it's disabled by default for
* performance runs. It can be manually enabled to compare GC behavior.
*/
private static final boolean MEASURE_ALLOC = false;
@Rule
public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
@Test
public void timeWrite_Fast() throws Exception {
doWrite(() -> Xml.newFastSerializer());
}
@Test
public void timeWrite_Binary() throws Exception {
doWrite(() -> Xml.newBinarySerializer());
}
private void doWrite(Supplier<TypedXmlSerializer> outFactory) throws Exception {
if (MEASURE_ALLOC) {
Debug.startAllocCounting();
}
int iterations = 0;
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
iterations++;
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
final TypedXmlSerializer out = outFactory.get();
out.setOutput(os, StandardCharsets.UTF_8.name());
write(out);
}
}
if (MEASURE_ALLOC) {
Debug.stopAllocCounting();
final Bundle results = new Bundle();
results.putLong("threadAllocCount_mean", Debug.getThreadAllocCount() / iterations);
results.putLong("threadAllocSize_mean", Debug.getThreadAllocSize() / iterations);
InstrumentationRegistry.getInstrumentation().sendStatus(0, results);
}
}
@Test
public void timeRead_Fast() throws Exception {
doRead(() -> Xml.newFastSerializer(), () -> Xml.newFastPullParser());
}
@Test
public void timeRead_Binary() throws Exception {
doRead(() -> Xml.newBinarySerializer(), () -> Xml.newBinaryPullParser());
}
private void doRead(Supplier<TypedXmlSerializer> outFactory,
Supplier<TypedXmlPullParser> inFactory) throws Exception {
final byte[] raw;
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
TypedXmlSerializer out = outFactory.get();
out.setOutput(os, StandardCharsets.UTF_8.name());
write(out);
raw = os.toByteArray();
}
if (MEASURE_ALLOC) {
Debug.startAllocCounting();
}
int iterations = 0;
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
iterations++;
try (ByteArrayInputStream is = new ByteArrayInputStream(raw)) {
TypedXmlPullParser xml = inFactory.get();
xml.setInput(is, StandardCharsets.UTF_8.name());
read(xml);
}
}
if (MEASURE_ALLOC) {
Debug.stopAllocCounting();
final Bundle results = new Bundle();
results.putLong("sizeBytes", raw.length);
results.putLong("threadAllocCount_mean", Debug.getThreadAllocCount() / iterations);
results.putLong("threadAllocSize_mean", Debug.getThreadAllocSize() / iterations);
InstrumentationRegistry.getInstrumentation().sendStatus(0, results);
} else {
final Bundle results = new Bundle();
results.putLong("sizeBytes", raw.length);
InstrumentationRegistry.getInstrumentation().sendStatus(0, results);
}
}
/**
* Not even joking, this is a typical public key blob stored in
* {@code packages.xml}.
*/
private static final byte[] KEY_BLOB = HexDump.hexStringToByteArray(""
+ "308204a830820390a003020102020900a1573d0f45bea193300d06092a864886f70d010105050030819"
+ "4310b3009060355040613025553311330110603550408130a43616c69666f726e696131163014060355"
+ "0407130d4d6f756e7461696e20566965773110300e060355040a1307416e64726f69643110300e06035"
+ "5040b1307416e64726f69643110300e06035504031307416e64726f69643122302006092a864886f70d"
+ "0109011613616e64726f696440616e64726f69642e636f6d301e170d3131303931393138343232355a1"
+ "70d3339303230343138343232355a308194310b3009060355040613025553311330110603550408130a"
+ "43616c69666f726e6961311630140603550407130d4d6f756e7461696e20566965773110300e0603550"
+ "40a1307416e64726f69643110300e060355040b1307416e64726f69643110300e06035504031307416e"
+ "64726f69643122302006092a864886f70d0109011613616e64726f696440616e64726f69642e636f6d3"
+ "0820120300d06092a864886f70d01010105000382010d00308201080282010100de1b51336afc909d8b"
+ "cca5920fcdc8940578ec5c253898930e985481cfdea75ba6fc54b1f7bb492a03d98db471ab4200103a8"
+ "314e60ee25fef6c8b83bc1b2b45b084874cffef148fa2001bb25c672b6beba50b7ac026b546da762ea2"
+ "23829a22b80ef286131f059d2c9b4ca71d54e515a8a3fd6bf5f12a2493dfc2619b337b032a7cf8bbd34"
+ "b833f2b93aeab3d325549a93272093943bb59dfc0197ae4861ff514e019b73f5cf10023ad1a032adb4b"
+ "9bbaeb4debecb4941d6a02381f1165e1ac884c1fca9525c5854dce2ad8ec839b8ce78442c16367efc07"
+ "778a337d3ca2cdf9792ac722b95d67c345f1c00976ec372f02bfcbef0262cc512a6845e71cfea0d0201"
+ "03a381fc3081f9301d0603551d0e0416041478a0fc4517fb70ff52210df33c8d32290a44b2bb3081c90"
+ "603551d230481c13081be801478a0fc4517fb70ff52210df33c8d32290a44b2bba1819aa48197308194"
+ "310b3009060355040613025553311330110603550408130a43616c69666f726e6961311630140603550"
+ "407130d4d6f756e7461696e20566965773110300e060355040a1307416e64726f69643110300e060355"
+ "040b1307416e64726f69643110300e06035504031307416e64726f69643122302006092a864886f70d0"
+ "109011613616e64726f696440616e64726f69642e636f6d820900a1573d0f45bea193300c0603551d13"
+ "040530030101ff300d06092a864886f70d01010505000382010100977302dfbf668d7c61841c9c78d25"
+ "63bcda1b199e95e6275a799939981416909722713531157f3cdcfea94eea7bb79ca3ca972bd8058a36a"
+ "d1919291df42d7190678d4ea47a4b9552c9dfb260e6d0d9129b44615cd641c1080580e8a990dd768c6a"
+ "b500c3b964e185874e4105109d94c5bd8c405deb3cf0f7960a563bfab58169a956372167a7e2674a04c"
+ "4f80015d8f7869a7a4139aecbbdca2abc294144ee01e4109f0e47a518363cf6e9bf41f7560e94bdd4a5"
+ "d085234796b05c7a1389adfd489feec2a107955129d7991daa49afb3d327dc0dc4fe959789372b093a8"
+ "9c8dbfa41554f771c18015a6cb242a17e04d19d55d3b4664eae12caf2a11cd2b836e");
/**
* Typical list of permissions referenced in {@code packages.xml}.
*/
private static final String[] PERMS = new String[] {
"android.permission.ACCESS_CACHE_FILESYSTEM",
"android.permission.WRITE_SETTINGS",
"android.permission.MANAGE_EXTERNAL_STORAGE",
"android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS",
"android.permission.FOREGROUND_SERVICE",
"android.permission.RECEIVE_BOOT_COMPLETED",
"android.permission.WRITE_MEDIA_STORAGE",
"android.permission.INTERNET",
"android.permission.UPDATE_DEVICE_STATS",
"android.permission.RECEIVE_DEVICE_CUSTOMIZATION_READY",
"android.permission.MANAGE_USB",
"android.permission.ACCESS_ALL_DOWNLOADS",
"android.permission.ACCESS_DOWNLOAD_MANAGER",
"android.permission.MANAGE_USERS",
"android.permission.ACCESS_NETWORK_STATE",
"android.permission.ACCESS_MTP",
"android.permission.INTERACT_ACROSS_USERS",
"android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS",
"android.permission.CLEAR_APP_CACHE",
"android.permission.CONNECTIVITY_INTERNAL",
"android.permission.START_ACTIVITIES_FROM_BACKGROUND",
"android.permission.QUERY_ALL_PACKAGES",
"android.permission.WAKE_LOCK",
"android.permission.UPDATE_APP_OPS_STATS",
};
/**
* Write a typical {@code packages.xml} file containing 100 applications,
* each of which defines signing key and permission information.
*/
private static void write(TypedXmlSerializer out) throws IOException {
out.startDocument(null, true);
out.startTag(null, "packages");
for (int i = 0; i < 100; i++) {
out.startTag(null, "package");
out.attribute(null, "name", "com.android.providers.media");
out.attribute(null, "codePath", "/system/priv-app/MediaProviderLegacy");
out.attribute(null, "nativeLibraryPath", "/system/priv-app/MediaProviderLegacy/lib");
out.attributeLong(null, "publicFlags", 944258629L);
out.attributeLong(null, "privateFlags", -1946152952L);
out.attributeLong(null, "ft", 1603899064000L);
out.attributeLong(null, "it", 1603899064000L);
out.attributeLong(null, "ut", 1603899064000L);
out.attributeInt(null, "version", 1024);
out.attributeInt(null, "sharedUserId", 10100);
out.attributeBoolean(null, "isOrphaned", true);
out.startTag(null, "sigs");
out.startTag(null, "cert");
out.attributeInt(null, "index", 10);
out.attributeBytesHex(null, "key", KEY_BLOB);
out.endTag(null, "cert");
out.endTag(null, "sigs");
out.startTag(null, "perms");
for (String perm : PERMS) {
out.startTag(null, "item");
out.attributeInterned(null, "name", perm);
out.attributeBoolean(null, "granted", true);
out.attributeInt(null, "flags", 0);
out.endTag(null, "item");
}
out.endTag(null, "perms");
out.endTag(null, "package");
}
out.endTag(null, "packages");
out.endDocument();
}
/**
* Read a typical {@code packages.xml} file containing 100 applications, and
* verify that data passes smell test.
*/
private static void read(TypedXmlPullParser xml) throws Exception {
int type;
int packages = 0;
int certs = 0;
int perms = 0;
while ((type = xml.next()) != XmlPullParser.END_DOCUMENT) {
final String tag = xml.getName();
if (type == XmlPullParser.START_TAG) {
if ("package".equals(tag)) {
xml.getAttributeValue(null, "name");
xml.getAttributeValue(null, "codePath");
xml.getAttributeValue(null, "nativeLibraryPath");
xml.getAttributeLong(null, "publicFlags");
assertEquals(-1946152952L, xml.getAttributeLong(null, "privateFlags"));
xml.getAttributeLong(null, "ft");
xml.getAttributeLong(null, "it");
xml.getAttributeLong(null, "ut");
xml.getAttributeInt(null, "version");
xml.getAttributeInt(null, "sharedUserId");
xml.getAttributeBoolean(null, "isOrphaned");
packages++;
} else if ("cert".equals(tag)) {
xml.getAttributeInt(null, "index");
xml.getAttributeBytesHex(null, "key");
certs++;
} else if ("item".equals(tag)) {
xml.getAttributeValue(null, "name");
xml.getAttributeBoolean(null, "granted");
xml.getAttributeInt(null, "flags");
perms++;
}
} else if (type == XmlPullParser.TEXT) {
xml.getText();
}
}
assertEquals(100, packages);
assertEquals(packages * 1, certs);
assertEquals(packages * PERMS.length, perms);
}
}

View File

@@ -0,0 +1,132 @@
/*
* Copyright (C) 2020 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.internal.util;
import android.perftests.utils.BenchmarkState;
import android.perftests.utils.PerfStatusReporter;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.IOException;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class FastDataPerfTest {
@Rule
public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
private static final int OUTPUT_SIZE = 64000;
private static final int BUFFER_SIZE = 4096;
@Test
public void timeWrite_Upstream() throws IOException {
final ByteArrayOutputStream os = new ByteArrayOutputStream(OUTPUT_SIZE);
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
os.reset();
final BufferedOutputStream bos = new BufferedOutputStream(os, BUFFER_SIZE);
final DataOutput out = new DataOutputStream(bos);
doWrite(out);
bos.flush();
}
}
@Test
public void timeWrite_Local() throws IOException {
final ByteArrayOutputStream os = new ByteArrayOutputStream(OUTPUT_SIZE);
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
os.reset();
final FastDataOutput out = new FastDataOutput(os, BUFFER_SIZE);
doWrite(out);
out.flush();
}
}
@Test
public void timeRead_Upstream() throws Exception {
final ByteArrayInputStream is = new ByteArrayInputStream(doWrite());
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
is.reset();
final BufferedInputStream bis = new BufferedInputStream(is, BUFFER_SIZE);
final DataInput in = new DataInputStream(bis);
doRead(in);
}
}
@Test
public void timeRead_Local() throws Exception {
final ByteArrayInputStream is = new ByteArrayInputStream(doWrite());
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
is.reset();
final DataInput in = new FastDataInput(is, BUFFER_SIZE);
doRead(in);
}
}
/**
* Since each iteration is around 64 bytes, we need to iterate many times to
* exercise the buffer logic.
*/
private static final int REPEATS = 1000;
private static byte[] doWrite() throws IOException {
final ByteArrayOutputStream os = new ByteArrayOutputStream(OUTPUT_SIZE);
final DataOutput out = new DataOutputStream(os);
doWrite(out);
return os.toByteArray();
}
private static void doWrite(DataOutput out) throws IOException {
for (int i = 0; i < REPEATS; i++) {
out.writeByte(Byte.MAX_VALUE);
out.writeShort(Short.MAX_VALUE);
out.writeInt(Integer.MAX_VALUE);
out.writeLong(Long.MAX_VALUE);
out.writeFloat(Float.MAX_VALUE);
out.writeDouble(Double.MAX_VALUE);
out.writeUTF("com.example.typical_package_name");
}
}
private static void doRead(DataInput in) throws IOException {
for (int i = 0; i < REPEATS; i++) {
in.readByte();
in.readShort();
in.readInt();
in.readLong();
in.readFloat();
in.readDouble();
in.readUTF();
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) 2020 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 android.util;
import android.annotation.NonNull;
import dalvik.annotation.optimization.FastNative;
/**
* Specializations of {@code libcore.util.CharsetUtils} which enable efficient
* in-place encoding without making any new allocations.
* <p>
* These methods purposefully accept only non-movable byte array addresses to
* avoid extra JNI overhead.
*
* @hide
*/
public class CharsetUtils {
/**
* Attempt to encode the given string as UTF-8 into the destination byte
* array without making any new allocations.
*
* @param src string value to be encoded
* @param dest destination byte array to encode into
* @param destOff offset into destination where encoding should begin
* @param destLen length of destination
* @return the number of bytes written to the destination when encoded
* successfully, otherwise {@code -1} if not large enough
*/
public static int toUtf8Bytes(@NonNull String src,
long dest, int destOff, int destLen) {
return toUtf8Bytes(src, src.length(), dest, destOff, destLen);
}
/**
* Attempt to encode the given string as UTF-8 into the destination byte
* array without making any new allocations.
*
* @param src string value to be encoded
* @param srcLen exact length of string to be encoded
* @param dest destination byte array to encode into
* @param destOff offset into destination where encoding should begin
* @param destLen length of destination
* @return the number of bytes written to the destination when encoded
* successfully, otherwise {@code -1} if not large enough
*/
@FastNative
private static native int toUtf8Bytes(@NonNull String src, int srcLen,
long dest, int destOff, int destLen);
}

View File

@@ -0,0 +1,186 @@
/*
* Copyright (C) 2020 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 android.util;
import android.annotation.NonNull;
import android.annotation.Nullable;
import org.xmlpull.v1.XmlPullParser;
import java.io.IOException;
/**
* Specialization of {@link XmlPullParser} which adds explicit methods to
* support consistent and efficient conversion of primitive data types.
*
* @hide
*/
public interface TypedXmlPullParser extends XmlPullParser {
/**
* @return decoded strongly-typed {@link #getAttributeValue}, or
* {@code null} if malformed or undefined
*/
@Nullable byte[] getAttributeBytesHex(@Nullable String namespace, @NonNull String name)
throws IOException;
/**
* @return decoded strongly-typed {@link #getAttributeValue}, or
* {@code null} if malformed or undefined
*/
@Nullable byte[] getAttributeBytesBase64(@Nullable String namespace, @NonNull String name)
throws IOException;
/**
* @return decoded strongly-typed {@link #getAttributeValue}
* @throws IOException if the value is malformed or undefined
*/
int getAttributeInt(@Nullable String namespace, @NonNull String name)
throws IOException;
/**
* @return decoded strongly-typed {@link #getAttributeValue}
* @throws IOException if the value is malformed or undefined
*/
int getAttributeIntHex(@Nullable String namespace, @NonNull String name)
throws IOException;
/**
* @return decoded strongly-typed {@link #getAttributeValue}
* @throws IOException if the value is malformed or undefined
*/
long getAttributeLong(@Nullable String namespace, @NonNull String name)
throws IOException;
/**
* @return decoded strongly-typed {@link #getAttributeValue}
* @throws IOException if the value is malformed or undefined
*/
long getAttributeLongHex(@Nullable String namespace, @NonNull String name)
throws IOException;
/**
* @return decoded strongly-typed {@link #getAttributeValue}
* @throws IOException if the value is malformed or undefined
*/
float getAttributeFloat(@Nullable String namespace, @NonNull String name)
throws IOException;
/**
* @return decoded strongly-typed {@link #getAttributeValue}
* @throws IOException if the value is malformed or undefined
*/
double getAttributeDouble(@Nullable String namespace, @NonNull String name)
throws IOException;
/**
* @return decoded strongly-typed {@link #getAttributeValue}
* @throws IOException if the value is malformed or undefined
*/
boolean getAttributeBoolean(@Nullable String namespace, @NonNull String name)
throws IOException;
/**
* @return decoded strongly-typed {@link #getAttributeValue}, otherwise
* default value if the value is malformed or undefined
*/
default int getAttributeInt(@Nullable String namespace, @NonNull String name,
int defaultValue) {
try {
return getAttributeInt(namespace, name);
} catch (Exception ignored) {
return defaultValue;
}
}
/**
* @return decoded strongly-typed {@link #getAttributeValue}, otherwise
* default value if the value is malformed or undefined
*/
default int getAttributeIntHex(@Nullable String namespace, @NonNull String name,
int defaultValue) {
try {
return getAttributeIntHex(namespace, name);
} catch (Exception ignored) {
return defaultValue;
}
}
/**
* @return decoded strongly-typed {@link #getAttributeValue}, otherwise
* default value if the value is malformed or undefined
*/
default long getAttributeLong(@Nullable String namespace, @NonNull String name,
long defaultValue) {
try {
return getAttributeLong(namespace, name);
} catch (Exception ignored) {
return defaultValue;
}
}
/**
* @return decoded strongly-typed {@link #getAttributeValue}, otherwise
* default value if the value is malformed or undefined
*/
default long getAttributeLongHex(@Nullable String namespace, @NonNull String name,
long defaultValue) {
try {
return getAttributeLongHex(namespace, name);
} catch (Exception ignored) {
return defaultValue;
}
}
/**
* @return decoded strongly-typed {@link #getAttributeValue}, otherwise
* default value if the value is malformed or undefined
*/
default float getAttributeFloat(@Nullable String namespace, @NonNull String name,
float defaultValue) {
try {
return getAttributeFloat(namespace, name);
} catch (Exception ignored) {
return defaultValue;
}
}
/**
* @return decoded strongly-typed {@link #getAttributeValue}, otherwise
* default value if the value is malformed or undefined
*/
default double getAttributeDouble(@Nullable String namespace, @NonNull String name,
double defaultValue) {
try {
return getAttributeDouble(namespace, name);
} catch (Exception ignored) {
return defaultValue;
}
}
/**
* @return decoded strongly-typed {@link #getAttributeValue}, otherwise
* default value if the value is malformed or undefined
*/
default boolean getAttributeBoolean(@Nullable String namespace, @NonNull String name,
boolean defaultValue) {
try {
return getAttributeBoolean(namespace, name);
} catch (Exception ignored) {
return defaultValue;
}
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright (C) 2020 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 android.util;
import android.annotation.NonNull;
import android.annotation.Nullable;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
/**
* Specialization of {@link XmlSerializer} which adds explicit methods to
* support consistent and efficient conversion of primitive data types.
*
* @hide
*/
public interface TypedXmlSerializer extends XmlSerializer {
/**
* Functionally equivalent to {@link #attribute(String, String, String)} but
* with the additional signal that the given value is a candidate for being
* canonicalized, similar to {@link String#intern()}.
*/
@NonNull XmlSerializer attributeInterned(@Nullable String namespace, @NonNull String name,
@Nullable String value) throws IOException;
/**
* Encode the given strongly-typed value and serialize using
* {@link #attribute(String, String, String)}.
*/
@NonNull XmlSerializer attributeBytesHex(@Nullable String namespace, @NonNull String name,
byte[] value) throws IOException;
/**
* Encode the given strongly-typed value and serialize using
* {@link #attribute(String, String, String)}.
*/
@NonNull XmlSerializer attributeBytesBase64(@Nullable String namespace, @NonNull String name,
byte[] value) throws IOException;
/**
* Encode the given strongly-typed value and serialize using
* {@link #attribute(String, String, String)}.
*/
@NonNull XmlSerializer attributeInt(@Nullable String namespace, @NonNull String name,
int value) throws IOException;
/**
* Encode the given strongly-typed value and serialize using
* {@link #attribute(String, String, String)}.
*/
@NonNull XmlSerializer attributeIntHex(@Nullable String namespace, @NonNull String name,
int value) throws IOException;
/**
* Encode the given strongly-typed value and serialize using
* {@link #attribute(String, String, String)}.
*/
@NonNull XmlSerializer attributeLong(@Nullable String namespace, @NonNull String name,
long value) throws IOException;
/**
* Encode the given strongly-typed value and serialize using
* {@link #attribute(String, String, String)}.
*/
@NonNull XmlSerializer attributeLongHex(@Nullable String namespace, @NonNull String name,
long value) throws IOException;
/**
* Encode the given strongly-typed value and serialize using
* {@link #attribute(String, String, String)}.
*/
@NonNull XmlSerializer attributeFloat(@Nullable String namespace, @NonNull String name,
float value) throws IOException;
/**
* Encode the given strongly-typed value and serialize using
* {@link #attribute(String, String, String)}.
*/
@NonNull XmlSerializer attributeDouble(@Nullable String namespace, @NonNull String name,
double value) throws IOException;
/**
* Encode the given strongly-typed value and serialize using
* {@link #attribute(String, String, String)}.
*/
@NonNull XmlSerializer attributeBoolean(@Nullable String namespace, @NonNull String name,
boolean value) throws IOException;
}

View File

@@ -16,6 +16,14 @@
package android.util;
import android.annotation.NonNull;
import android.annotation.Nullable;
import com.android.internal.util.BinaryXmlPullParser;
import com.android.internal.util.BinaryXmlSerializer;
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.XmlUtils;
import libcore.util.XmlObjectFactory;
import org.xml.sax.ContentHandler;
@@ -26,11 +34,15 @@ import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
/**
* XML utility methods.
@@ -98,6 +110,57 @@ public class Xml {
}
}
/**
* Creates a new {@link TypedXmlPullParser} which is optimized for use
* inside the system, typically by supporting only a basic set of features.
* <p>
* In particular, the returned parser does not support namespaces, prefixes,
* properties, or options.
*
* @hide
*/
public static @NonNull TypedXmlPullParser newFastPullParser() {
return XmlUtils.makeTyped(newPullParser());
}
/**
* Creates a new {@link XmlPullParser} that reads XML documents using a
* custom binary wire protocol which benchmarking has shown to be 8.5x
* faster than {@code Xml.newFastPullParser()} for a typical
* {@code packages.xml}.
*
* @hide
*/
public static @NonNull TypedXmlPullParser newBinaryPullParser() {
return new BinaryXmlPullParser();
}
/**
* Creates a new {@link XmlPullParser} which is optimized for use inside the
* system, typically by supporting only a basic set of features.
* <p>
* This returned instance may be configured to read using an efficient
* binary format instead of a human-readable text format, depending on
* device feature flags.
* <p>
* To ensure that both formats are detected and transparently handled
* correctly, you must shift to using both {@link #resolveSerializer} and
* {@link #resolvePullParser}.
*
* @hide
*/
public static @NonNull TypedXmlPullParser resolvePullParser(@NonNull InputStream in)
throws IOException {
// TODO: add support for binary format
final TypedXmlPullParser xml = newFastPullParser();
try {
xml.setInput(in, StandardCharsets.UTF_8.name());
} catch (XmlPullParserException e) {
throw new IOException(e);
}
return xml;
}
/**
* Creates a new xml serializer.
*/
@@ -105,6 +168,129 @@ public class Xml {
return XmlObjectFactory.newXmlSerializer();
}
/**
* Creates a new {@link XmlSerializer} which is optimized for use inside the
* system, typically by supporting only a basic set of features.
* <p>
* In particular, the returned parser does not support namespaces, prefixes,
* properties, or options.
*
* @hide
*/
public static @NonNull TypedXmlSerializer newFastSerializer() {
return XmlUtils.makeTyped(new FastXmlSerializer());
}
/**
* Creates a new {@link XmlSerializer} that writes XML documents using a
* custom binary wire protocol which benchmarking has shown to be 4.4x
* faster and use 2.8x less disk space than {@code Xml.newFastSerializer()}
* for a typical {@code packages.xml}.
*
* @hide
*/
public static @NonNull TypedXmlSerializer newBinarySerializer() {
return new BinaryXmlSerializer();
}
/**
* Creates a new {@link XmlSerializer} which is optimized for use inside the
* system, typically by supporting only a basic set of features.
* <p>
* This returned instance may be configured to write using an efficient
* binary format instead of a human-readable text format, depending on
* device feature flags.
* <p>
* To ensure that both formats are detected and transparently handled
* correctly, you must shift to using both {@link #resolveSerializer} and
* {@link #resolvePullParser}.
*
* @hide
*/
public static @NonNull TypedXmlSerializer resolveSerializer(@NonNull OutputStream out)
throws IOException {
// TODO: add support for binary format
final TypedXmlSerializer xml = newFastSerializer();
xml.setOutput(out, StandardCharsets.UTF_8.name());
return xml;
}
/**
* Copy the first XML document into the second document.
* <p>
* Implemented by reading all events from the given {@link XmlPullParser}
* and writing them directly to the given {@link XmlSerializer}. This can be
* useful for transparently converting between underlying wire protocols.
*
* @hide
*/
public static void copy(@NonNull XmlPullParser in, @NonNull XmlSerializer out)
throws XmlPullParserException, IOException {
// Some parsers may have already consumed the event that starts the
// document, so we manually emit that event here for consistency
if (in.getEventType() == XmlPullParser.START_DOCUMENT) {
out.startDocument(in.getInputEncoding(), true);
}
while (true) {
final int token = in.nextToken();
switch (token) {
case XmlPullParser.START_DOCUMENT:
out.startDocument(in.getInputEncoding(), true);
break;
case XmlPullParser.END_DOCUMENT:
out.endDocument();
return;
case XmlPullParser.START_TAG:
out.startTag(normalizeNamespace(in.getNamespace()), in.getName());
for (int i = 0; i < in.getAttributeCount(); i++) {
out.attribute(normalizeNamespace(in.getAttributeNamespace(i)),
in.getAttributeName(i), in.getAttributeValue(i));
}
break;
case XmlPullParser.END_TAG:
out.endTag(normalizeNamespace(in.getNamespace()), in.getName());
break;
case XmlPullParser.TEXT:
out.text(in.getText());
break;
case XmlPullParser.CDSECT:
out.cdsect(in.getText());
break;
case XmlPullParser.ENTITY_REF:
out.entityRef(in.getName());
break;
case XmlPullParser.IGNORABLE_WHITESPACE:
out.ignorableWhitespace(in.getText());
break;
case XmlPullParser.PROCESSING_INSTRUCTION:
out.processingInstruction(in.getText());
break;
case XmlPullParser.COMMENT:
out.comment(in.getText());
break;
case XmlPullParser.DOCDECL:
out.docdecl(in.getText());
break;
default:
throw new IllegalStateException("Unknown token " + token);
}
}
}
/**
* Some parsers may return an empty string {@code ""} when a namespace in
* unsupported, which can confuse serializers. This method normalizes empty
* strings to be {@code null}.
*/
private static @Nullable String normalizeNamespace(@Nullable String namespace) {
if (namespace == null || namespace.isEmpty()) {
return null;
} else {
return namespace;
}
}
/**
* Supported character encodings.
*/

View File

@@ -0,0 +1,899 @@
/*
* Copyright (C) 2020 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.internal.util;
import static com.android.internal.util.BinaryXmlSerializer.ATTRIBUTE;
import static com.android.internal.util.BinaryXmlSerializer.PROTOCOL_MAGIC_VERSION_0;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_BOOLEAN_FALSE;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_BOOLEAN_TRUE;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_BYTES_BASE64;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_BYTES_HEX;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_DOUBLE;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_FLOAT;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_INT;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_INT_HEX;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_LONG;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_LONG_HEX;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_NULL;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_STRING;
import static com.android.internal.util.BinaryXmlSerializer.TYPE_STRING_INTERNED;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.text.TextUtils;
import android.util.Base64;
import android.util.TypedXmlPullParser;
import android.util.TypedXmlSerializer;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
/**
* Parser that reads XML documents using a custom binary wire protocol which
* benchmarking has shown to be 8.5x faster than {@link Xml.newFastPullParser()}
* for a typical {@code packages.xml}.
* <p>
* The high-level design of the wire protocol is to directly serialize the event
* stream, while efficiently and compactly writing strongly-typed primitives
* delivered through the {@link TypedXmlSerializer} interface.
* <p>
* Each serialized event is a single byte where the lower half is a normal
* {@link XmlPullParser} token and the upper half is an optional data type
* signal, such as {@link #TYPE_INT}.
* <p>
* This parser has some specific limitations:
* <ul>
* <li>Only the UTF-8 encoding is supported.
* <li>Variable length values, such as {@code byte[]} or {@link String}, are
* limited to 65,535 bytes in length. Note that {@link String} values are stored
* as UTF-8 on the wire.
* <li>Namespaces, prefixes, properties, and options are unsupported.
* </ul>
*/
public final class BinaryXmlPullParser implements TypedXmlPullParser {
/**
* Default buffer size, which matches {@code FastXmlSerializer}. This should
* be kept in sync with {@link BinaryXmlPullParser}.
*/
private static final int BUFFER_SIZE = 32_768;
private FastDataInput mIn;
private int mCurrentToken = START_DOCUMENT;
private int mCurrentDepth = 0;
private String mCurrentName;
private String mCurrentText;
/**
* Pool of attributes parsed for the currently tag. All interactions should
* be done via {@link #obtainAttribute()}, {@link #findAttribute(String)},
* and {@link #resetAttributes()}.
*/
private int mAttributeCount = 0;
private Attribute[] mAttributes;
@Override
public void setInput(InputStream is, String inputEncoding) throws XmlPullParserException {
if (inputEncoding != null && !StandardCharsets.UTF_8.name().equals(inputEncoding)) {
throw new UnsupportedOperationException();
}
mIn = new FastDataInput(is, BUFFER_SIZE);
mCurrentToken = START_DOCUMENT;
mCurrentDepth = 0;
mCurrentName = null;
mCurrentText = null;
mAttributeCount = 0;
mAttributes = new Attribute[8];
for (int i = 0; i < mAttributes.length; i++) {
mAttributes[i] = new Attribute();
}
try {
final byte[] magic = new byte[4];
mIn.readFully(magic);
if (!Arrays.equals(magic, PROTOCOL_MAGIC_VERSION_0)) {
throw new IOException("Unexpected magic " + bytesToHexString(magic));
}
// We're willing to immediately consume a START_DOCUMENT if present,
// but we're okay if it's missing
if (peekNextExternalToken() == START_DOCUMENT) {
consumeToken();
}
} catch (IOException e) {
throw new XmlPullParserException(e.toString());
}
}
@Override
public void setInput(Reader in) throws XmlPullParserException {
throw new UnsupportedOperationException();
}
@Override
public int next() throws XmlPullParserException, IOException {
while (true) {
final int token = nextToken();
switch (token) {
case START_TAG:
case END_TAG:
case END_DOCUMENT:
return token;
case TEXT:
consumeAdditionalText();
// Per interface docs, empty text regions are skipped
if (mCurrentText == null || mCurrentText.length() == 0) {
continue;
} else {
return TEXT;
}
}
}
}
@Override
public int nextToken() throws XmlPullParserException, IOException {
if (mCurrentToken == XmlPullParser.END_TAG) {
mCurrentDepth--;
}
int token;
try {
token = peekNextExternalToken();
consumeToken();
} catch (EOFException e) {
token = END_DOCUMENT;
}
switch (token) {
case XmlPullParser.START_TAG:
// We need to peek forward to find the next external token so
// that we parse all pending INTERNAL_ATTRIBUTE tokens
peekNextExternalToken();
mCurrentDepth++;
break;
}
mCurrentToken = token;
return token;
}
/**
* Peek at the next "external" token without consuming it.
* <p>
* External tokens, such as {@link #START_TAG}, are expected by typical
* {@link XmlPullParser} clients. In contrast, internal tokens, such as
* {@link #ATTRIBUTE}, are not expected by typical clients.
* <p>
* This method consumes any internal events until it reaches the next
* external event.
*/
private int peekNextExternalToken() throws IOException, XmlPullParserException {
while (true) {
final int token = peekNextToken();
switch (token) {
case ATTRIBUTE:
consumeToken();
continue;
default:
return token;
}
}
}
/**
* Peek at the next token in the underlying stream without consuming it.
*/
private int peekNextToken() throws IOException {
return mIn.peekByte() & 0x0f;
}
/**
* Parse and consume the next token in the underlying stream.
*/
private void consumeToken() throws IOException, XmlPullParserException {
final int event = mIn.readByte();
final int token = event & 0x0f;
final int type = event & 0xf0;
switch (token) {
case ATTRIBUTE: {
final Attribute attr = obtainAttribute();
attr.name = mIn.readInternedUTF();
attr.type = type;
switch (type) {
case TYPE_NULL:
case TYPE_BOOLEAN_TRUE:
case TYPE_BOOLEAN_FALSE:
// Nothing extra to fill in
break;
case TYPE_STRING:
attr.valueString = mIn.readUTF();
break;
case TYPE_STRING_INTERNED:
attr.valueString = mIn.readInternedUTF();
break;
case TYPE_BYTES_HEX:
case TYPE_BYTES_BASE64:
final int len = mIn.readUnsignedShort();
final byte[] res = new byte[len];
mIn.readFully(res);
attr.valueBytes = res;
break;
case TYPE_INT:
case TYPE_INT_HEX:
attr.valueInt = mIn.readInt();
break;
case TYPE_LONG:
case TYPE_LONG_HEX:
attr.valueLong = mIn.readLong();
break;
case TYPE_FLOAT:
attr.valueFloat = mIn.readFloat();
break;
case TYPE_DOUBLE:
attr.valueDouble = mIn.readDouble();
break;
default:
throw new IOException("Unexpected data type " + type);
}
break;
}
case XmlPullParser.START_DOCUMENT: {
break;
}
case XmlPullParser.END_DOCUMENT: {
break;
}
case XmlPullParser.START_TAG: {
mCurrentName = mIn.readInternedUTF();
resetAttributes();
break;
}
case XmlPullParser.END_TAG: {
mCurrentName = mIn.readInternedUTF();
resetAttributes();
break;
}
case XmlPullParser.TEXT:
case XmlPullParser.CDSECT:
case XmlPullParser.PROCESSING_INSTRUCTION:
case XmlPullParser.COMMENT:
case XmlPullParser.DOCDECL:
case XmlPullParser.IGNORABLE_WHITESPACE: {
mCurrentText = mIn.readUTF();
break;
}
case XmlPullParser.ENTITY_REF: {
mCurrentName = mIn.readUTF();
mCurrentText = resolveEntity(mCurrentName);
break;
}
default: {
throw new IOException("Unknown token " + token + " with type " + type);
}
}
}
/**
* When the current tag is {@link #TEXT}, consume all subsequent "text"
* events, as described by {@link #next}. When finished, the current event
* will still be {@link #TEXT}.
*/
private void consumeAdditionalText() throws IOException, XmlPullParserException {
String combinedText = mCurrentText;
while (true) {
final int token = peekNextExternalToken();
switch (token) {
case COMMENT:
case PROCESSING_INSTRUCTION:
// Quietly consumed
consumeToken();
break;
case TEXT:
case CDSECT:
case ENTITY_REF:
// Additional text regions collected
consumeToken();
combinedText += mCurrentText;
break;
default:
// Next token is something non-text, so wrap things up
mCurrentToken = TEXT;
mCurrentName = null;
mCurrentText = combinedText;
return;
}
}
}
static @NonNull String resolveEntity(@NonNull String entity)
throws XmlPullParserException {
switch (entity) {
case "lt": return "<";
case "gt": return ">";
case "amp": return "&";
case "apos": return "'";
case "quot": return "\"";
}
if (entity.length() > 1 && entity.charAt(0) == '#') {
final char c = (char) Integer.parseInt(entity.substring(1));
return new String(new char[] { c });
}
throw new XmlPullParserException("Unknown entity " + entity);
}
@Override
public void require(int type, String namespace, String name)
throws XmlPullParserException, IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
if (mCurrentToken != type || !Objects.equals(mCurrentName, name)) {
throw new XmlPullParserException(getPositionDescription());
}
}
@Override
public String nextText() throws XmlPullParserException, IOException {
if (getEventType() != START_TAG) {
throw new XmlPullParserException(getPositionDescription());
}
int eventType = next();
if (eventType == TEXT) {
String result = getText();
eventType = next();
if (eventType != END_TAG) {
throw new XmlPullParserException(getPositionDescription());
}
return result;
} else if (eventType == END_TAG) {
return "";
} else {
throw new XmlPullParserException(getPositionDescription());
}
}
@Override
public int nextTag() throws XmlPullParserException, IOException {
int eventType = next();
if (eventType == TEXT && isWhitespace()) {
eventType = next();
}
if (eventType != START_TAG && eventType != END_TAG) {
throw new XmlPullParserException(getPositionDescription());
}
return eventType;
}
/**
* Allocate and return a new {@link Attribute} associated with the tag being
* currently processed. This will automatically grow the internal pool as
* needed.
*/
private @NonNull Attribute obtainAttribute() {
if (mAttributeCount == mAttributes.length) {
final int before = mAttributes.length;
final int after = before + (before >> 1);
mAttributes = Arrays.copyOf(mAttributes, after);
for (int i = before; i < after; i++) {
mAttributes[i] = new Attribute();
}
}
return mAttributes[mAttributeCount++];
}
/**
* Clear any {@link Attribute} instances that have been allocated by
* {@link #obtainAttribute()}, returning them into the pool for recycling.
*/
private void resetAttributes() {
for (int i = 0; i < mAttributeCount; i++) {
mAttributes[i].reset();
}
mAttributeCount = 0;
}
/**
* Search through the pool of currently allocated {@link Attribute}
* instances for one that matches the given name.
*/
private @NonNull Attribute findAttribute(@NonNull String name) throws IOException {
for (int i = 0; i < mAttributeCount; i++) {
if (Objects.equals(mAttributes[i].name, name)) {
return mAttributes[i];
}
}
throw new IOException("Missing attribute " + name);
}
@Override
public String getAttributeValue(String namespace, String name) {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
try {
return findAttribute(name).getValueString();
} catch (IOException e) {
// Missing attributes default to null
return null;
}
}
@Override
public String getAttributeValue(int index) {
return mAttributes[index].getValueString();
}
@Override
public byte[] getAttributeBytesHex(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
return findAttribute(name).getValueBytesHex();
}
@Override
public byte[] getAttributeBytesBase64(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
return findAttribute(name).getValueBytesBase64();
}
@Override
public int getAttributeInt(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
return findAttribute(name).getValueInt();
}
@Override
public int getAttributeIntHex(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
return findAttribute(name).getValueIntHex();
}
@Override
public long getAttributeLong(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
return findAttribute(name).getValueLong();
}
@Override
public long getAttributeLongHex(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
return findAttribute(name).getValueLongHex();
}
@Override
public float getAttributeFloat(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
return findAttribute(name).getValueFloat();
}
@Override
public double getAttributeDouble(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
return findAttribute(name).getValueDouble();
}
@Override
public boolean getAttributeBoolean(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
return findAttribute(name).getValueBoolean();
}
@Override
public String getText() {
return mCurrentText;
}
@Override
public char[] getTextCharacters(int[] holderForStartAndLength) {
final char[] chars = mCurrentText.toCharArray();
holderForStartAndLength[0] = 0;
holderForStartAndLength[1] = chars.length;
return chars;
}
@Override
public String getInputEncoding() {
return StandardCharsets.UTF_8.name();
}
@Override
public int getDepth() {
return mCurrentDepth;
}
@Override
public String getPositionDescription() {
// Not very helpful, but it's the best information we have
return "Token " + mCurrentToken + " at depth " + mCurrentDepth;
}
@Override
public int getLineNumber() {
return -1;
}
@Override
public int getColumnNumber() {
return -1;
}
@Override
public boolean isWhitespace() throws XmlPullParserException {
switch (mCurrentToken) {
case IGNORABLE_WHITESPACE:
return true;
case TEXT:
case CDSECT:
return !TextUtils.isGraphic(mCurrentText);
default:
throw new XmlPullParserException("Not applicable for token " + mCurrentToken);
}
}
@Override
public String getNamespace() {
switch (mCurrentToken) {
case START_TAG:
case END_TAG:
// Namespaces are unsupported
return NO_NAMESPACE;
default:
return null;
}
}
@Override
public String getName() {
return mCurrentName;
}
@Override
public String getPrefix() {
// Prefixes are not supported
return null;
}
@Override
public boolean isEmptyElementTag() throws XmlPullParserException {
switch (mCurrentToken) {
case START_TAG:
try {
return (peekNextExternalToken() == END_TAG);
} catch (IOException e) {
throw new XmlPullParserException(e.toString());
}
default:
throw new XmlPullParserException("Not at START_TAG");
}
}
@Override
public int getAttributeCount() {
return mAttributeCount;
}
@Override
public String getAttributeNamespace(int index) {
// Namespaces are unsupported
return NO_NAMESPACE;
}
@Override
public String getAttributeName(int index) {
return mAttributes[index].name;
}
@Override
public String getAttributePrefix(int index) {
// Prefixes are not supported
return null;
}
@Override
public String getAttributeType(int index) {
// Validation is not supported
return "CDATA";
}
@Override
public boolean isAttributeDefault(int index) {
// Validation is not supported
return false;
}
@Override
public int getEventType() throws XmlPullParserException {
return mCurrentToken;
}
@Override
public int getNamespaceCount(int depth) throws XmlPullParserException {
// Namespaces are unsupported
return 0;
}
@Override
public String getNamespacePrefix(int pos) throws XmlPullParserException {
// Namespaces are unsupported
throw new UnsupportedOperationException();
}
@Override
public String getNamespaceUri(int pos) throws XmlPullParserException {
// Namespaces are unsupported
throw new UnsupportedOperationException();
}
@Override
public String getNamespace(String prefix) {
// Namespaces are unsupported
throw new UnsupportedOperationException();
}
@Override
public void defineEntityReplacementText(String entityName, String replacementText)
throws XmlPullParserException {
// Custom entities are not supported
throw new UnsupportedOperationException();
}
@Override
public void setFeature(String name, boolean state) throws XmlPullParserException {
// Features are not supported
throw new UnsupportedOperationException();
}
@Override
public boolean getFeature(String name) {
// Features are not supported
throw new UnsupportedOperationException();
}
@Override
public void setProperty(String name, Object value) throws XmlPullParserException {
// Properties are not supported
throw new UnsupportedOperationException();
}
@Override
public Object getProperty(String name) {
// Properties are not supported
throw new UnsupportedOperationException();
}
private static IllegalArgumentException illegalNamespace() {
throw new IllegalArgumentException("Namespaces are not supported");
}
/**
* Holder representing a single attribute. This design enables object
* recycling without resorting to autoboxing.
* <p>
* To support conversion between human-readable XML and binary XML, the
* various accessor methods will transparently convert from/to
* human-readable values when needed.
*/
private static class Attribute {
public String name;
public int type;
public String valueString;
public byte[] valueBytes;
public int valueInt;
public long valueLong;
public float valueFloat;
public double valueDouble;
public void reset() {
name = null;
valueString = null;
valueBytes = null;
}
public @Nullable String getValueString() {
switch (type) {
case TYPE_NULL:
return null;
case TYPE_STRING:
case TYPE_STRING_INTERNED:
return valueString;
case TYPE_BYTES_HEX:
return bytesToHexString(valueBytes);
case TYPE_BYTES_BASE64:
return Base64.encodeToString(valueBytes, Base64.NO_WRAP);
case TYPE_INT:
return Integer.toString(valueInt);
case TYPE_INT_HEX:
return Integer.toString(valueInt, 16);
case TYPE_LONG:
return Long.toString(valueLong);
case TYPE_LONG_HEX:
return Long.toString(valueLong, 16);
case TYPE_FLOAT:
return Float.toString(valueFloat);
case TYPE_DOUBLE:
return Double.toString(valueDouble);
case TYPE_BOOLEAN_TRUE:
return "true";
case TYPE_BOOLEAN_FALSE:
return "false";
default:
// Unknown data type; null is the best we can offer
return null;
}
}
public @Nullable byte[] getValueBytesHex() throws IOException {
switch (type) {
case TYPE_NULL:
return null;
case TYPE_BYTES_HEX:
case TYPE_BYTES_BASE64:
return valueBytes;
case TYPE_STRING:
return hexStringToBytes(valueString);
default:
throw new IOException("Invalid conversion from " + type);
}
}
public @Nullable byte[] getValueBytesBase64() throws IOException {
switch (type) {
case TYPE_NULL:
return null;
case TYPE_BYTES_HEX:
case TYPE_BYTES_BASE64:
return valueBytes;
case TYPE_STRING:
return Base64.decode(valueString, Base64.NO_WRAP);
default:
throw new IOException("Invalid conversion from " + type);
}
}
public int getValueInt() throws IOException {
switch (type) {
case TYPE_INT:
case TYPE_INT_HEX:
return valueInt;
case TYPE_STRING:
return Integer.parseInt(valueString);
default:
throw new IOException("Invalid conversion from " + type);
}
}
public int getValueIntHex() throws IOException {
switch (type) {
case TYPE_INT:
case TYPE_INT_HEX:
return valueInt;
case TYPE_STRING:
return Integer.parseInt(valueString, 16);
default:
throw new IOException("Invalid conversion from " + type);
}
}
public long getValueLong() throws IOException {
switch (type) {
case TYPE_LONG:
case TYPE_LONG_HEX:
return valueLong;
case TYPE_STRING:
return Long.parseLong(valueString);
default:
throw new IOException("Invalid conversion from " + type);
}
}
public long getValueLongHex() throws IOException {
switch (type) {
case TYPE_LONG:
case TYPE_LONG_HEX:
return valueLong;
case TYPE_STRING:
return Long.parseLong(valueString, 16);
default:
throw new IOException("Invalid conversion from " + type);
}
}
public float getValueFloat() throws IOException {
switch (type) {
case TYPE_FLOAT:
return valueFloat;
case TYPE_STRING:
return Float.parseFloat(valueString);
default:
throw new IOException("Invalid conversion from " + type);
}
}
public double getValueDouble() throws IOException {
switch (type) {
case TYPE_DOUBLE:
return valueDouble;
case TYPE_STRING:
return Double.parseDouble(valueString);
default:
throw new IOException("Invalid conversion from " + type);
}
}
public boolean getValueBoolean() throws IOException {
switch (type) {
case TYPE_BOOLEAN_TRUE:
return true;
case TYPE_BOOLEAN_FALSE:
return false;
case TYPE_STRING:
if ("true".equalsIgnoreCase(valueString)) {
return true;
} else if ("false".equalsIgnoreCase(valueString)) {
return false;
} else {
throw new IOException("Invalid boolean: " + valueString);
}
default:
throw new IOException("Invalid conversion from " + type);
}
}
}
// NOTE: To support unbundled clients, we include an inlined copy
// of hex conversion logic from HexDump below
private final static char[] HEX_DIGITS =
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
private static int toByte(char c) throws IOException {
if (c >= '0' && c <= '9') return (c - '0');
if (c >= 'A' && c <= 'F') return (c - 'A' + 10);
if (c >= 'a' && c <= 'f') return (c - 'a' + 10);
throw new IOException("Invalid hex char '" + c + "'");
}
static String bytesToHexString(byte[] value) {
final int length = value.length;
final char[] buf = new char[length * 2];
int bufIndex = 0;
for (int i = 0; i < length; i++) {
byte b = value[i];
buf[bufIndex++] = HEX_DIGITS[(b >>> 4) & 0x0F];
buf[bufIndex++] = HEX_DIGITS[b & 0x0F];
}
return new String(buf);
}
static byte[] hexStringToBytes(String value) throws IOException {
final int length = value.length();
if (length % 2 != 0) {
throw new IOException("Invalid hex length " + length);
}
byte[] buffer = new byte[length / 2];
for (int i = 0; i < length; i += 2) {
buffer[i / 2] = (byte) ((toByte(value.charAt(i)) << 4)
| toByte(value.charAt(i + 1)));
}
return buffer;
}
}

View File

@@ -0,0 +1,396 @@
/*
* Copyright (C) 2020 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.internal.util;
import static org.xmlpull.v1.XmlPullParser.CDSECT;
import static org.xmlpull.v1.XmlPullParser.COMMENT;
import static org.xmlpull.v1.XmlPullParser.DOCDECL;
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.END_TAG;
import static org.xmlpull.v1.XmlPullParser.ENTITY_REF;
import static org.xmlpull.v1.XmlPullParser.IGNORABLE_WHITESPACE;
import static org.xmlpull.v1.XmlPullParser.PROCESSING_INSTRUCTION;
import static org.xmlpull.v1.XmlPullParser.START_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import static org.xmlpull.v1.XmlPullParser.TEXT;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.TypedXmlSerializer;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
/**
* Serializer that writes XML documents using a custom binary wire protocol
* which benchmarking has shown to be 4.3x faster and use 2.4x less disk space
* than {@code Xml.newFastSerializer()} for a typical {@code packages.xml}.
* <p>
* The high-level design of the wire protocol is to directly serialize the event
* stream, while efficiently and compactly writing strongly-typed primitives
* delivered through the {@link TypedXmlSerializer} interface.
* <p>
* Each serialized event is a single byte where the lower half is a normal
* {@link XmlPullParser} token and the upper half is an optional data type
* signal, such as {@link #TYPE_INT}.
* <p>
* This serializer has some specific limitations:
* <ul>
* <li>Only the UTF-8 encoding is supported.
* <li>Variable length values, such as {@code byte[]} or {@link String}, are
* limited to 65,535 bytes in length. Note that {@link String} values are stored
* as UTF-8 on the wire.
* <li>Namespaces, prefixes, properties, and options are unsupported.
* </ul>
*/
public final class BinaryXmlSerializer implements TypedXmlSerializer {
/**
* The wire protocol always begins with a well-known magic value of
* {@code ABX_}, representing "Android Binary XML." The final byte is a
* version number which may be incremented as the protocol changes.
*/
static final byte[] PROTOCOL_MAGIC_VERSION_0 = new byte[] { 0x41, 0x42, 0x58, 0x00 };
/**
* Internal token which represents an attribute associated with the most
* recent {@link #START_TAG} token.
*/
static final int ATTRIBUTE = 15;
static final int TYPE_NULL = 1 << 4;
static final int TYPE_STRING = 2 << 4;
static final int TYPE_STRING_INTERNED = 3 << 4;
static final int TYPE_BYTES_HEX = 4 << 4;
static final int TYPE_BYTES_BASE64 = 5 << 4;
static final int TYPE_INT = 6 << 4;
static final int TYPE_INT_HEX = 7 << 4;
static final int TYPE_LONG = 8 << 4;
static final int TYPE_LONG_HEX = 9 << 4;
static final int TYPE_FLOAT = 10 << 4;
static final int TYPE_DOUBLE = 11 << 4;
static final int TYPE_BOOLEAN_TRUE = 12 << 4;
static final int TYPE_BOOLEAN_FALSE = 13 << 4;
/**
* Default buffer size, which matches {@code FastXmlSerializer}. This should
* be kept in sync with {@link BinaryXmlPullParser}.
*/
private static final int BUFFER_SIZE = 32_768;
private FastDataOutput mOut;
/**
* Stack of tags which are currently active via {@link #startTag} and which
* haven't been terminated via {@link #endTag}.
*/
private int mTagCount = 0;
private String[] mTagNames;
/**
* Write the given token and optional {@link String} into our buffer.
*/
private void writeToken(int token, @Nullable String text) throws IOException {
if (text != null) {
mOut.writeByte(token | TYPE_STRING);
mOut.writeUTF(text);
} else {
mOut.writeByte(token | TYPE_NULL);
}
}
@Override
public void setOutput(@NonNull OutputStream os, @Nullable String encoding) throws IOException {
if (encoding != null && !StandardCharsets.UTF_8.name().equals(encoding)) {
throw new UnsupportedOperationException();
}
mOut = new FastDataOutput(os, BUFFER_SIZE);
mOut.write(PROTOCOL_MAGIC_VERSION_0);
mTagCount = 0;
mTagNames = new String[8];
}
@Override
public void setOutput(Writer writer) {
throw new UnsupportedOperationException();
}
@Override
public void flush() throws IOException {
mOut.flush();
}
@Override
public void startDocument(@Nullable String encoding, @Nullable Boolean standalone)
throws IOException {
if (encoding != null && !StandardCharsets.UTF_8.name().equals(encoding)) {
throw new UnsupportedOperationException();
}
mOut.writeByte(START_DOCUMENT | TYPE_NULL);
}
@Override
public void endDocument() throws IOException {
mOut.writeByte(END_DOCUMENT | TYPE_NULL);
flush();
}
@Override
public int getDepth() {
return mTagCount;
}
@Override
public String getNamespace() {
// Namespaces are unsupported
return XmlPullParser.NO_NAMESPACE;
}
@Override
public String getName() {
return mTagNames[mTagCount - 1];
}
@Override
public XmlSerializer startTag(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
if (mTagCount == mTagNames.length) {
mTagNames = Arrays.copyOf(mTagNames, mTagCount + (mTagCount >> 1));
}
mTagNames[mTagCount++] = name;
mOut.writeByte(START_TAG | TYPE_STRING_INTERNED);
mOut.writeInternedUTF(name);
return this;
}
@Override
public XmlSerializer endTag(String namespace, String name) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mTagCount--;
mOut.writeByte(END_TAG | TYPE_STRING_INTERNED);
mOut.writeInternedUTF(name);
return this;
}
@Override
public XmlSerializer attribute(String namespace, String name, String value) throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_STRING);
mOut.writeInternedUTF(name);
mOut.writeUTF(value);
return this;
}
@Override
public XmlSerializer attributeInterned(String namespace, String name, String value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_STRING_INTERNED);
mOut.writeInternedUTF(name);
mOut.writeInternedUTF(value);
return this;
}
@Override
public XmlSerializer attributeBytesHex(String namespace, String name, byte[] value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_BYTES_HEX);
mOut.writeInternedUTF(name);
mOut.writeShort(value.length);
mOut.write(value);
return this;
}
@Override
public XmlSerializer attributeBytesBase64(String namespace, String name, byte[] value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_BYTES_BASE64);
mOut.writeInternedUTF(name);
mOut.writeShort(value.length);
mOut.write(value);
return this;
}
@Override
public XmlSerializer attributeInt(String namespace, String name, int value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_INT);
mOut.writeInternedUTF(name);
mOut.writeInt(value);
return this;
}
@Override
public XmlSerializer attributeIntHex(String namespace, String name, int value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_INT_HEX);
mOut.writeInternedUTF(name);
mOut.writeInt(value);
return this;
}
@Override
public XmlSerializer attributeLong(String namespace, String name, long value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_LONG);
mOut.writeInternedUTF(name);
mOut.writeLong(value);
return this;
}
@Override
public XmlSerializer attributeLongHex(String namespace, String name, long value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_LONG_HEX);
mOut.writeInternedUTF(name);
mOut.writeLong(value);
return this;
}
@Override
public XmlSerializer attributeFloat(String namespace, String name, float value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_FLOAT);
mOut.writeInternedUTF(name);
mOut.writeFloat(value);
return this;
}
@Override
public XmlSerializer attributeDouble(String namespace, String name, double value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
mOut.writeByte(ATTRIBUTE | TYPE_DOUBLE);
mOut.writeInternedUTF(name);
mOut.writeDouble(value);
return this;
}
@Override
public XmlSerializer attributeBoolean(String namespace, String name, boolean value)
throws IOException {
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
if (value) {
mOut.writeByte(ATTRIBUTE | TYPE_BOOLEAN_TRUE);
mOut.writeInternedUTF(name);
} else {
mOut.writeByte(ATTRIBUTE | TYPE_BOOLEAN_FALSE);
mOut.writeInternedUTF(name);
}
return this;
}
@Override
public XmlSerializer text(char[] buf, int start, int len) throws IOException {
writeToken(TEXT, new String(buf, start, len));
return this;
}
@Override
public XmlSerializer text(String text) throws IOException {
writeToken(TEXT, text);
return this;
}
@Override
public void cdsect(String text) throws IOException {
writeToken(CDSECT, text);
}
@Override
public void entityRef(String text) throws IOException {
writeToken(ENTITY_REF, text);
}
@Override
public void processingInstruction(String text) throws IOException {
writeToken(PROCESSING_INSTRUCTION, text);
}
@Override
public void comment(String text) throws IOException {
writeToken(COMMENT, text);
}
@Override
public void docdecl(String text) throws IOException {
writeToken(DOCDECL, text);
}
@Override
public void ignorableWhitespace(String text) throws IOException {
writeToken(IGNORABLE_WHITESPACE, text);
}
@Override
public void setFeature(String name, boolean state) {
// Quietly handle no-op features
if ("http://xmlpull.org/v1/doc/features.html#indent-output".equals(name)) {
return;
}
// Features are not supported
throw new UnsupportedOperationException();
}
@Override
public boolean getFeature(String name) {
// Features are not supported
throw new UnsupportedOperationException();
}
@Override
public void setProperty(String name, Object value) {
// Properties are not supported
throw new UnsupportedOperationException();
}
@Override
public Object getProperty(String name) {
// Properties are not supported
throw new UnsupportedOperationException();
}
@Override
public void setPrefix(String prefix, String namespace) {
// Prefixes are not supported
throw new UnsupportedOperationException();
}
@Override
public String getPrefix(String namespace, boolean generatePrefix) {
// Prefixes are not supported
throw new UnsupportedOperationException();
}
private static IllegalArgumentException illegalNamespace() {
throw new IllegalArgumentException("Namespaces are not supported");
}
}

View File

@@ -0,0 +1,256 @@
/*
* Copyright (C) 2020 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.internal.util;
import android.annotation.NonNull;
import java.io.BufferedInputStream;
import java.io.Closeable;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
/**
* Optimized implementation of {@link DataInput} which buffers data in memory
* from the underlying {@link InputStream}.
* <p>
* Benchmarks have demonstrated this class is 3x more efficient than using a
* {@link DataInputStream} with a {@link BufferedInputStream}.
*/
public class FastDataInput implements DataInput, Closeable {
private static final int MAX_UNSIGNED_SHORT = 65_535;
private final InputStream mIn;
private final byte[] mBuffer;
private final int mBufferCap;
private int mBufferPos;
private int mBufferLim;
/**
* Values that have been "interned" by {@link #readInternedUTF()}.
*/
private int mStringRefCount = 0;
private String[] mStringRefs = new String[32];
public FastDataInput(@NonNull InputStream in, int bufferSize) {
mIn = Objects.requireNonNull(in);
if (bufferSize < 8) {
throw new IllegalArgumentException();
}
mBuffer = new byte[bufferSize];
mBufferCap = mBuffer.length;
}
private void fill(int need) throws IOException {
final int remain = mBufferLim - mBufferPos;
System.arraycopy(mBuffer, mBufferPos, mBuffer, 0, remain);
mBufferPos = 0;
mBufferLim = remain;
need -= remain;
while (need > 0) {
int c = mIn.read(mBuffer, mBufferLim, mBufferCap - mBufferLim);
if (c == -1) {
throw new EOFException();
} else {
mBufferLim += c;
need -= c;
}
}
}
@Override
public void close() throws IOException {
mIn.close();
}
@Override
public void readFully(byte[] b) throws IOException {
readFully(b, 0, b.length);
}
@Override
public void readFully(byte[] b, int off, int len) throws IOException {
// Attempt to read directly from buffer space if there's enough room,
// otherwise fall back to chunking into place
if (mBufferCap >= len) {
if (mBufferLim - mBufferPos < len) fill(len);
System.arraycopy(mBuffer, mBufferPos, b, off, len);
mBufferPos += len;
} else {
final int remain = mBufferLim - mBufferPos;
System.arraycopy(mBuffer, mBufferPos, b, off, remain);
mBufferPos += remain;
off += remain;
len -= remain;
while (len > 0) {
int c = mIn.read(b, off, len);
if (c == -1) {
throw new EOFException();
} else {
off += c;
len -= c;
}
}
}
}
@Override
public String readUTF() throws IOException {
// Attempt to read directly from buffer space if there's enough room,
// otherwise fall back to chunking into place
final int len = readUnsignedShort();
if (mBufferCap >= len) {
if (mBufferLim - mBufferPos < len) fill(len);
final String res = new String(mBuffer, mBufferPos, len, StandardCharsets.UTF_8);
mBufferPos += len;
return res;
} else {
final byte[] tmp = new byte[len];
readFully(tmp, 0, tmp.length);
return new String(tmp, StandardCharsets.UTF_8);
}
}
/**
* Read a {@link String} value with the additional signal that the given
* value is a candidate for being canonicalized, similar to
* {@link String#intern()}.
* <p>
* Canonicalization is implemented by writing each unique string value once
* the first time it appears, and then writing a lightweight {@code short}
* reference when that string is written again in the future.
*
* @see FastDataOutput#writeInternedUTF(String)
*/
public @NonNull String readInternedUTF() throws IOException {
final int ref = readUnsignedShort();
if (ref == MAX_UNSIGNED_SHORT) {
final String s = readUTF();
// We can only safely intern when we have remaining values; if we're
// full we at least sent the string value above
if (mStringRefCount < MAX_UNSIGNED_SHORT) {
if (mStringRefCount == mStringRefs.length) {
mStringRefs = Arrays.copyOf(mStringRefs,
mStringRefCount + (mStringRefCount >> 1));
}
mStringRefs[mStringRefCount++] = s;
}
return s;
} else {
return mStringRefs[ref];
}
}
@Override
public boolean readBoolean() throws IOException {
return readByte() != 0;
}
/**
* Returns the same decoded value as {@link #readByte()} but without
* actually consuming the underlying data.
*/
public byte peekByte() throws IOException {
if (mBufferLim - mBufferPos < 1) fill(1);
return mBuffer[mBufferPos];
}
@Override
public byte readByte() throws IOException {
if (mBufferLim - mBufferPos < 1) fill(1);
return mBuffer[mBufferPos++];
}
@Override
public int readUnsignedByte() throws IOException {
return Byte.toUnsignedInt(readByte());
}
@Override
public short readShort() throws IOException {
if (mBufferLim - mBufferPos < 2) fill(2);
return (short) (((mBuffer[mBufferPos++] & 0xff) << 8) |
((mBuffer[mBufferPos++] & 0xff) << 0));
}
@Override
public int readUnsignedShort() throws IOException {
return Short.toUnsignedInt((short) readShort());
}
@Override
public char readChar() throws IOException {
return (char) readShort();
}
@Override
public int readInt() throws IOException {
if (mBufferLim - mBufferPos < 4) fill(4);
return (((mBuffer[mBufferPos++] & 0xff) << 24) |
((mBuffer[mBufferPos++] & 0xff) << 16) |
((mBuffer[mBufferPos++] & 0xff) << 8) |
((mBuffer[mBufferPos++] & 0xff) << 0));
}
@Override
public long readLong() throws IOException {
if (mBufferLim - mBufferPos < 8) fill(8);
int h = ((mBuffer[mBufferPos++] & 0xff) << 24) |
((mBuffer[mBufferPos++] & 0xff) << 16) |
((mBuffer[mBufferPos++] & 0xff) << 8) |
((mBuffer[mBufferPos++] & 0xff) << 0);
int l = ((mBuffer[mBufferPos++] & 0xff) << 24) |
((mBuffer[mBufferPos++] & 0xff) << 16) |
((mBuffer[mBufferPos++] & 0xff) << 8) |
((mBuffer[mBufferPos++] & 0xff) << 0);
return (((long) h) << 32L) | ((long) l) & 0xffffffffL;
}
@Override
public float readFloat() throws IOException {
return Float.intBitsToFloat(readInt());
}
@Override
public double readDouble() throws IOException {
return Double.longBitsToDouble(readLong());
}
@Override
public int skipBytes(int n) throws IOException {
// Callers should read data piecemeal
throw new UnsupportedOperationException();
}
@Override
public String readLine() throws IOException {
// Callers should read data piecemeal
throw new UnsupportedOperationException();
}
}

View File

@@ -0,0 +1,228 @@
/*
* Copyright (C) 2020 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.internal.util;
import android.annotation.NonNull;
import android.util.CharsetUtils;
import dalvik.system.VMRuntime;
import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.Flushable;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Objects;
/**
* Optimized implementation of {@link DataOutput} which buffers data in memory
* before flushing to the underlying {@link OutputStream}.
* <p>
* Benchmarks have demonstrated this class is 2x more efficient than using a
* {@link DataOutputStream} with a {@link BufferedOutputStream}.
*/
public class FastDataOutput implements DataOutput, Flushable, Closeable {
private static final int MAX_UNSIGNED_SHORT = 65_535;
private final OutputStream mOut;
private final byte[] mBuffer;
private final long mBufferPtr;
private final int mBufferCap;
private int mBufferPos;
/**
* Values that have been "interned" by {@link #writeInternedUTF(String)}.
*/
private HashMap<String, Short> mStringRefs = new HashMap<>();
public FastDataOutput(@NonNull OutputStream out, int bufferSize) {
mOut = Objects.requireNonNull(out);
if (bufferSize < 8) {
throw new IllegalArgumentException();
}
mBuffer = (byte[]) VMRuntime.getRuntime().newNonMovableArray(byte.class, bufferSize);
mBufferPtr = VMRuntime.getRuntime().addressOf(mBuffer);
mBufferCap = mBuffer.length;
}
private void drain() throws IOException {
if (mBufferPos > 0) {
mOut.write(mBuffer, 0, mBufferPos);
mBufferPos = 0;
}
}
@Override
public void flush() throws IOException {
drain();
mOut.flush();
}
@Override
public void close() throws IOException {
mOut.close();
}
@Override
public void write(int b) throws IOException {
writeByte(b);
}
@Override
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
if (mBufferCap < len) {
drain();
mOut.write(b, off, len);
} else {
if (mBufferCap - mBufferPos < len) drain();
System.arraycopy(b, off, mBuffer, mBufferPos, len);
mBufferPos += len;
}
}
@Override
public void writeUTF(String s) throws IOException {
// Attempt to write directly to buffer space if there's enough room,
// otherwise fall back to chunking into place
if (mBufferCap - mBufferPos < 2 + s.length()) drain();
final int res = CharsetUtils.toUtf8Bytes(s, mBufferPtr, mBufferPos + 2,
mBufferCap - mBufferPos - 2);
if (res >= 0) {
if (res > MAX_UNSIGNED_SHORT) {
throw new IOException("UTF-8 length too large: " + res);
}
writeShort(res);
mBufferPos += res;
} else {
final byte[] tmp = s.getBytes(StandardCharsets.UTF_8);
if (tmp.length > MAX_UNSIGNED_SHORT) {
throw new IOException("UTF-8 length too large: " + res);
}
writeShort(tmp.length);
write(tmp, 0, tmp.length);
}
}
/**
* Write a {@link String} value with the additional signal that the given
* value is a candidate for being canonicalized, similar to
* {@link String#intern()}.
* <p>
* Canonicalization is implemented by writing each unique string value once
* the first time it appears, and then writing a lightweight {@code short}
* reference when that string is written again in the future.
*
* @see FastDataInput#readInternedUTF()
*/
public void writeInternedUTF(@NonNull String s) throws IOException {
Short ref = mStringRefs.get(s);
if (ref != null) {
writeShort(ref);
} else {
writeShort(MAX_UNSIGNED_SHORT);
writeUTF(s);
// We can only safely intern when we have remaining values; if we're
// full we at least sent the string value above
ref = (short) mStringRefs.size();
if (ref < MAX_UNSIGNED_SHORT) {
mStringRefs.put(s, ref);
}
}
}
@Override
public void writeBoolean(boolean v) throws IOException {
writeByte(v ? 1 : 0);
}
@Override
public void writeByte(int v) throws IOException {
if (mBufferCap - mBufferPos < 1) drain();
mBuffer[mBufferPos++] = (byte) ((v >> 0) & 0xff);
}
@Override
public void writeShort(int v) throws IOException {
if (mBufferCap - mBufferPos < 2) drain();
mBuffer[mBufferPos++] = (byte) ((v >> 8) & 0xff);
mBuffer[mBufferPos++] = (byte) ((v >> 0) & 0xff);
}
@Override
public void writeChar(int v) throws IOException {
writeShort((short) v);
}
@Override
public void writeInt(int v) throws IOException {
if (mBufferCap - mBufferPos < 4) drain();
mBuffer[mBufferPos++] = (byte) ((v >> 24) & 0xff);
mBuffer[mBufferPos++] = (byte) ((v >> 16) & 0xff);
mBuffer[mBufferPos++] = (byte) ((v >> 8) & 0xff);
mBuffer[mBufferPos++] = (byte) ((v >> 0) & 0xff);
}
@Override
public void writeLong(long v) throws IOException {
if (mBufferCap - mBufferPos < 8) drain();
int i = (int) (v >> 32);
mBuffer[mBufferPos++] = (byte) ((i >> 24) & 0xff);
mBuffer[mBufferPos++] = (byte) ((i >> 16) & 0xff);
mBuffer[mBufferPos++] = (byte) ((i >> 8) & 0xff);
mBuffer[mBufferPos++] = (byte) ((i >> 0) & 0xff);
i = (int) v;
mBuffer[mBufferPos++] = (byte) ((i >> 24) & 0xff);
mBuffer[mBufferPos++] = (byte) ((i >> 16) & 0xff);
mBuffer[mBufferPos++] = (byte) ((i >> 8) & 0xff);
mBuffer[mBufferPos++] = (byte) ((i >> 0) & 0xff);
}
@Override
public void writeFloat(float v) throws IOException {
writeInt(Float.floatToIntBits(v));
}
@Override
public void writeDouble(double v) throws IOException {
writeLong(Double.doubleToLongBits(v));
}
@Override
public void writeBytes(String s) throws IOException {
// Callers should use writeUTF()
throw new UnsupportedOperationException();
}
@Override
public void writeChars(String s) throws IOException {
// Callers should use writeUTF()
throw new UnsupportedOperationException();
}
}

View File

@@ -0,0 +1,189 @@
/*
* Copyright (C) 2020 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.internal.util;
import android.annotation.NonNull;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.Objects;
/**
* Wrapper which delegates all calls through to the given {@link XmlPullParser}.
*/
public class XmlPullParserWrapper implements XmlPullParser {
private final XmlPullParser mWrapped;
public XmlPullParserWrapper(@NonNull XmlPullParser wrapped) {
mWrapped = Objects.requireNonNull(wrapped);
}
public void setFeature(String name, boolean state) throws XmlPullParserException {
mWrapped.setFeature(name, state);
}
public boolean getFeature(String name) {
return mWrapped.getFeature(name);
}
public void setProperty(String name, Object value) throws XmlPullParserException {
mWrapped.setProperty(name, value);
}
public Object getProperty(String name) {
return mWrapped.getProperty(name);
}
public void setInput(Reader in) throws XmlPullParserException {
mWrapped.setInput(in);
}
public void setInput(InputStream inputStream, String inputEncoding)
throws XmlPullParserException {
mWrapped.setInput(inputStream, inputEncoding);
}
public String getInputEncoding() {
return mWrapped.getInputEncoding();
}
public void defineEntityReplacementText(String entityName, String replacementText)
throws XmlPullParserException {
mWrapped.defineEntityReplacementText(entityName, replacementText);
}
public int getNamespaceCount(int depth) throws XmlPullParserException {
return mWrapped.getNamespaceCount(depth);
}
public String getNamespacePrefix(int pos) throws XmlPullParserException {
return mWrapped.getNamespacePrefix(pos);
}
public String getNamespaceUri(int pos) throws XmlPullParserException {
return mWrapped.getNamespaceUri(pos);
}
public String getNamespace(String prefix) {
return mWrapped.getNamespace(prefix);
}
public int getDepth() {
return mWrapped.getDepth();
}
public String getPositionDescription() {
return mWrapped.getPositionDescription();
}
public int getLineNumber() {
return mWrapped.getLineNumber();
}
public int getColumnNumber() {
return mWrapped.getColumnNumber();
}
public boolean isWhitespace() throws XmlPullParserException {
return mWrapped.isWhitespace();
}
public String getText() {
return mWrapped.getText();
}
public char[] getTextCharacters(int[] holderForStartAndLength) {
return mWrapped.getTextCharacters(holderForStartAndLength);
}
public String getNamespace() {
return mWrapped.getNamespace();
}
public String getName() {
return mWrapped.getName();
}
public String getPrefix() {
return mWrapped.getPrefix();
}
public boolean isEmptyElementTag() throws XmlPullParserException {
return mWrapped.isEmptyElementTag();
}
public int getAttributeCount() {
return mWrapped.getAttributeCount();
}
public String getAttributeNamespace(int index) {
return mWrapped.getAttributeNamespace(index);
}
public String getAttributeName(int index) {
return mWrapped.getAttributeName(index);
}
public String getAttributePrefix(int index) {
return mWrapped.getAttributePrefix(index);
}
public String getAttributeType(int index) {
return mWrapped.getAttributeType(index);
}
public boolean isAttributeDefault(int index) {
return mWrapped.isAttributeDefault(index);
}
public String getAttributeValue(int index) {
return mWrapped.getAttributeValue(index);
}
public String getAttributeValue(String namespace, String name) {
return mWrapped.getAttributeValue(namespace, name);
}
public int getEventType() throws XmlPullParserException {
return mWrapped.getEventType();
}
public int next() throws XmlPullParserException, IOException {
return mWrapped.next();
}
public int nextToken() throws XmlPullParserException, IOException {
return mWrapped.nextToken();
}
public void require(int type, String namespace, String name)
throws XmlPullParserException, IOException {
mWrapped.require(type, namespace, name);
}
public String nextText() throws XmlPullParserException, IOException {
return mWrapped.nextText();
}
public int nextTag() throws XmlPullParserException, IOException {
return mWrapped.nextTag();
}
}

View File

@@ -0,0 +1,140 @@
/*
* Copyright (C) 2020 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.internal.util;
import android.annotation.NonNull;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.util.Objects;
/**
* Wrapper which delegates all calls through to the given {@link XmlSerializer}.
*/
public class XmlSerializerWrapper {
private final XmlSerializer mWrapped;
public XmlSerializerWrapper(@NonNull XmlSerializer wrapped) {
mWrapped = Objects.requireNonNull(wrapped);
}
public void setFeature(String name, boolean state) {
mWrapped.setFeature(name, state);
}
public boolean getFeature(String name) {
return mWrapped.getFeature(name);
}
public void setProperty(String name, Object value) {
mWrapped.setProperty(name, value);
}
public Object getProperty(String name) {
return mWrapped.getProperty(name);
}
public void setOutput(OutputStream os, String encoding) throws IOException {
mWrapped.setOutput(os, encoding);
}
public void setOutput(Writer writer)
throws IOException, IllegalArgumentException, IllegalStateException {
mWrapped.setOutput(writer);
}
public void startDocument(String encoding, Boolean standalone) throws IOException {
mWrapped.startDocument(encoding, standalone);
}
public void endDocument() throws IOException {
mWrapped.endDocument();
}
public void setPrefix(String prefix, String namespace) throws IOException {
mWrapped.setPrefix(prefix, namespace);
}
public String getPrefix(String namespace, boolean generatePrefix) {
return mWrapped.getPrefix(namespace, generatePrefix);
}
public int getDepth() {
return mWrapped.getDepth();
}
public String getNamespace() {
return mWrapped.getNamespace();
}
public String getName() {
return mWrapped.getName();
}
public XmlSerializer startTag(String namespace, String name) throws IOException {
return mWrapped.startTag(namespace, name);
}
public XmlSerializer attribute(String namespace, String name, String value)
throws IOException {
return mWrapped.attribute(namespace, name, value);
}
public XmlSerializer endTag(String namespace, String name) throws IOException {
return mWrapped.endTag(namespace, name);
}
public XmlSerializer text(String text) throws IOException{
return mWrapped.text(text);
}
public XmlSerializer text(char[] buf, int start, int len) throws IOException {
return mWrapped.text(buf, start, len);
}
public void cdsect(String text)
throws IOException, IllegalArgumentException, IllegalStateException {
mWrapped.cdsect(text);
}
public void entityRef(String text) throws IOException {
mWrapped.entityRef(text);
}
public void processingInstruction(String text) throws IOException {
mWrapped.processingInstruction(text);
}
public void comment(String text) throws IOException {
mWrapped.comment(text);
}
public void docdecl(String text) throws IOException {
mWrapped.docdecl(text);
}
public void ignorableWhitespace(String text) throws IOException {
mWrapped.ignorableWhitespace(text);
}
public void flush() throws IOException {
mWrapped.flush();
}
}

View File

@@ -16,6 +16,7 @@
package com.android.internal.util;
import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
@@ -24,6 +25,8 @@ import android.net.Uri;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Base64;
import android.util.TypedXmlPullParser;
import android.util.TypedXmlSerializer;
import android.util.Xml;
import libcore.util.HexEncoding;
@@ -48,9 +51,193 @@ import java.util.Set;
/** {@hide} */
public class XmlUtils {
private static final String STRING_ARRAY_SEPARATOR = ":";
private static class ForcedTypedXmlSerializer extends XmlSerializerWrapper
implements TypedXmlSerializer {
public ForcedTypedXmlSerializer(XmlSerializer wrapped) {
super(wrapped);
}
@Override
public XmlSerializer attributeInterned(String namespace, String name, String value)
throws IOException {
return attribute(namespace, name, value);
}
@Override
public XmlSerializer attributeBytesHex(String namespace, String name, byte[] value)
throws IOException {
return attribute(namespace, name, HexDump.toHexString(value));
}
@Override
public XmlSerializer attributeBytesBase64(String namespace, String name, byte[] value)
throws IOException {
return attribute(namespace, name, Base64.encodeToString(value, Base64.NO_WRAP));
}
@Override
public XmlSerializer attributeInt(String namespace, String name, int value)
throws IOException {
return attribute(namespace, name, Integer.toString(value));
}
@Override
public XmlSerializer attributeIntHex(String namespace, String name, int value)
throws IOException {
return attribute(namespace, name, Integer.toString(value, 16));
}
@Override
public XmlSerializer attributeLong(String namespace, String name, long value)
throws IOException {
return attribute(namespace, name, Long.toString(value));
}
@Override
public XmlSerializer attributeLongHex(String namespace, String name, long value)
throws IOException {
return attribute(namespace, name, Long.toString(value, 16));
}
@Override
public XmlSerializer attributeFloat(String namespace, String name, float value)
throws IOException {
return attribute(namespace, name, Float.toString(value));
}
@Override
public XmlSerializer attributeDouble(String namespace, String name, double value)
throws IOException {
return attribute(namespace, name, Double.toString(value));
}
@Override
public XmlSerializer attributeBoolean(String namespace, String name, boolean value)
throws IOException {
return attribute(namespace, name, Boolean.toString(value));
}
}
/**
* Return a specialization of the given {@link XmlSerializer} which has
* explicit methods to support consistent and efficient conversion of
* primitive data types.
*/
public static @NonNull TypedXmlSerializer makeTyped(@NonNull XmlSerializer xml) {
if (xml instanceof TypedXmlSerializer) {
return (TypedXmlSerializer) xml;
} else {
return new ForcedTypedXmlSerializer(xml);
}
}
private static class ForcedTypedXmlPullParser extends XmlPullParserWrapper
implements TypedXmlPullParser {
public ForcedTypedXmlPullParser(XmlPullParser wrapped) {
super(wrapped);
}
@Override
public byte[] getAttributeBytesHex(String namespace, String name) throws IOException {
try {
return HexDump.hexStringToByteArray(getAttributeValue(namespace, name));
} catch (Exception e) {
throw new IOException("Invalid attribute " + name, e);
}
}
@Override
public byte[] getAttributeBytesBase64(String namespace, String name) throws IOException {
try {
return Base64.decode(getAttributeValue(namespace, name), Base64.NO_WRAP);
} catch (Exception e) {
throw new IOException("Invalid attribute " + name, e);
}
}
@Override
public int getAttributeInt(String namespace, String name) throws IOException {
try {
return Integer.parseInt(getAttributeValue(namespace, name));
} catch (NumberFormatException e) {
throw new IOException("Invalid attribute " + name, e);
}
}
@Override
public int getAttributeIntHex(String namespace, String name) throws IOException {
try {
return Integer.parseInt(getAttributeValue(namespace, name), 16);
} catch (NumberFormatException e) {
throw new IOException("Invalid attribute " + name, e);
}
}
@Override
public long getAttributeLong(String namespace, String name) throws IOException {
try {
return Long.parseLong(getAttributeValue(namespace, name));
} catch (NumberFormatException e) {
throw new IOException("Invalid attribute " + name, e);
}
}
@Override
public long getAttributeLongHex(String namespace, String name) throws IOException {
try {
return Long.parseLong(getAttributeValue(namespace, name), 16);
} catch (NumberFormatException e) {
throw new IOException("Invalid attribute " + name, e);
}
}
@Override
public float getAttributeFloat(String namespace, String name) throws IOException {
try {
return Float.parseFloat(getAttributeValue(namespace, name));
} catch (NumberFormatException e) {
throw new IOException("Invalid attribute " + name, e);
}
}
@Override
public double getAttributeDouble(String namespace, String name) throws IOException {
try {
return Double.parseDouble(getAttributeValue(namespace, name));
} catch (NumberFormatException e) {
throw new IOException("Invalid attribute " + name, e);
}
}
@Override
public boolean getAttributeBoolean(String namespace, String name) throws IOException {
final String value = getAttributeValue(namespace, name);
if ("true".equalsIgnoreCase(value)) {
return true;
} else if ("false".equalsIgnoreCase(value)) {
return false;
} else {
throw new IOException("Invalid attribute " + name,
new IllegalArgumentException("For input string: \"" + value + "\""));
}
}
}
/**
* Return a specialization of the given {@link XmlPullParser} which has
* explicit methods to support consistent and efficient conversion of
* primitive data types.
*/
public static @NonNull TypedXmlPullParser makeTyped(@NonNull XmlPullParser xml) {
if (xml instanceof TypedXmlPullParser) {
return (TypedXmlPullParser) xml;
} else {
return new ForcedTypedXmlPullParser(xml);
}
}
@UnsupportedAppUsage
public static void skipCurrentTag(XmlPullParser parser)
throws XmlPullParserException, IOException {

View File

@@ -134,6 +134,7 @@ cc_library_shared {
"android_service_DataLoaderService.cpp",
"android_util_AssetManager.cpp",
"android_util_Binder.cpp",
"android_util_CharsetUtils.cpp",
"android_util_MemoryIntArray.cpp",
"android_util_Process.cpp",
"android_media_AudioDeviceAttributes.cpp",

View File

@@ -105,6 +105,7 @@ namespace android {
*/
extern int register_android_app_admin_SecurityLog(JNIEnv* env);
extern int register_android_content_AssetManager(JNIEnv* env);
extern int register_android_util_CharsetUtils(JNIEnv* env);
extern int register_android_util_EventLog(JNIEnv* env);
extern int register_android_util_Log(JNIEnv* env);
extern int register_android_util_MemoryIntArray(JNIEnv* env);
@@ -1449,6 +1450,7 @@ static const RegJNIRec gRegJNI[] = {
REG_JNI(register_com_android_internal_os_RuntimeInit),
REG_JNI(register_com_android_internal_os_ZygoteInit_nativeZygoteInit),
REG_JNI(register_android_os_SystemClock),
REG_JNI(register_android_util_CharsetUtils),
REG_JNI(register_android_util_EventLog),
REG_JNI(register_android_util_Log),
REG_JNI(register_android_util_MemoryIntArray),

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2020 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.
*/
#include "core_jni_helpers.h"
#include "nativehelper/scoped_primitive_array.h"
namespace android {
static jint android_util_CharsetUtils_toUtf8Bytes(JNIEnv *env, jobject clazz,
jstring src, jint srcLen, jlong dest, jint destOff, jint destLen) {
char *destPtr = reinterpret_cast<char*>(dest);
// Quickly check if destination has plenty of room for worst-case
// 4-bytes-per-char encoded size
if (destOff >= 0 && destOff + (srcLen * 4) < destLen) {
env->GetStringUTFRegion(src, 0, srcLen, destPtr + destOff);
return strlen(destPtr + destOff + srcLen) + srcLen;
}
// String still might fit in destination, but we need to measure
// its actual encoded size to be sure
const size_t encodedLen = env->GetStringUTFLength(src);
if (destOff >= 0 && destOff + encodedLen < destLen) {
env->GetStringUTFRegion(src, 0, srcLen, destPtr + destOff);
return encodedLen;
}
return -1;
}
static const JNINativeMethod methods[] = {
// @FastNative
{"toUtf8Bytes", "(Ljava/lang/String;IJII)I",
(void*)android_util_CharsetUtils_toUtf8Bytes},
};
int register_android_util_CharsetUtils(JNIEnv *env) {
return RegisterMethodsOrDie(env, "android/util/CharsetUtils", methods, NELEM(methods));
}
}

View File

@@ -0,0 +1,99 @@
/*
* Copyright (C) 2020 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 android.util;
import static android.util.XmlTest.assertNext;
import static android.util.XmlTest.buildPersistableBundle;
import static android.util.XmlTest.doPersistableBundleRead;
import static android.util.XmlTest.doPersistableBundleWrite;
import static org.junit.Assert.assertEquals;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import android.os.PersistableBundle;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
@RunWith(AndroidJUnit4.class)
public class BinaryXmlTest {
/**
* Verify that we can write and read large numbers of interned
* {@link String} values.
*/
@Test
public void testLargeInterned_Binary() throws Exception {
// We're okay with the tag itself being interned
final int count = (1 << 16) - 2;
final TypedXmlSerializer out = Xml.newBinarySerializer();
final ByteArrayOutputStream os = new ByteArrayOutputStream();
out.setOutput(os, StandardCharsets.UTF_8.name());
out.startTag(null, "tag");
for (int i = 0; i < count; i++) {
out.attribute(null, "name" + i, "value");
}
out.endTag(null, "tag");
out.flush();
final TypedXmlPullParser in = Xml.newBinaryPullParser();
final ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
in.setInput(is, StandardCharsets.UTF_8.name());
assertNext(in, START_TAG, "tag", 1);
assertEquals(count, in.getAttributeCount());
}
@Test
public void testTranscode_FastToBinary() throws Exception {
doTranscode(Xml.newFastSerializer(), Xml.newFastPullParser(),
Xml.newBinarySerializer(), Xml.newBinaryPullParser());
}
@Test
public void testTranscode_BinaryToFast() throws Exception {
doTranscode(Xml.newBinarySerializer(), Xml.newBinaryPullParser(),
Xml.newFastSerializer(), Xml.newFastPullParser());
}
/**
* Verify that a complex {@link PersistableBundle} can be transcoded using
* the two given formats with the original structure intact.
*/
private static void doTranscode(TypedXmlSerializer firstOut, TypedXmlPullParser firstIn,
TypedXmlSerializer secondOut, TypedXmlPullParser secondIn) throws Exception {
final PersistableBundle expected = buildPersistableBundle();
final byte[] firstRaw = doPersistableBundleWrite(firstOut, expected);
// Perform actual transcoding between the two formats
final ByteArrayInputStream is = new ByteArrayInputStream(firstRaw);
firstIn.setInput(is, StandardCharsets.UTF_8.name());
final ByteArrayOutputStream os = new ByteArrayOutputStream();
secondOut.setOutput(os, StandardCharsets.UTF_8.name());
Xml.copy(firstIn, secondOut);
// Yes, this string-based check is fragile, but kindofEquals() is broken
// when working with nested objects and arrays
final PersistableBundle actual = doPersistableBundleRead(secondIn, os.toByteArray());
assertEquals(expected.toString(), actual.toString());
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2020 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 android.util;
import static org.junit.Assert.assertEquals;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.HexDump;
import dalvik.system.VMRuntime;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class CharsetUtilsTest {
private byte[] dest;
private long destPtr;
@Before
public void setUp() {
dest = (byte[]) VMRuntime.getRuntime().newNonMovableArray(byte.class, 8);
destPtr = VMRuntime.getRuntime().addressOf(dest);
}
@Test
public void testUtf8_Empty() {
assertEquals(0, CharsetUtils.toUtf8Bytes("", destPtr, 0, dest.length));
assertEquals("0000000000000000", HexDump.toHexString(dest));
}
@Test
public void testUtf8_Simple() {
assertEquals(7, CharsetUtils.toUtf8Bytes("example", destPtr, 0, dest.length));
assertEquals("6578616D706C6500", HexDump.toHexString(dest));
}
@Test
public void testUtf8_Complex() {
assertEquals(3, CharsetUtils.toUtf8Bytes("", destPtr, 4, dest.length));
assertEquals("00000000E2988300", HexDump.toHexString(dest));
}
@Test
public void testUtf8_Bounds() {
assertEquals(-1, CharsetUtils.toUtf8Bytes("foo", destPtr, 0, 0));
assertEquals(-1, CharsetUtils.toUtf8Bytes("foo", destPtr, 0, 2));
assertEquals(-1, CharsetUtils.toUtf8Bytes("foo", destPtr, -2, 8));
assertEquals(-1, CharsetUtils.toUtf8Bytes("foo", destPtr, 6, 8));
assertEquals(-1, CharsetUtils.toUtf8Bytes("foo", destPtr, 10, 8));
}
@Test
public void testUtf8_Overwrite() {
assertEquals(5, CharsetUtils.toUtf8Bytes("!!!!!", destPtr, 0, dest.length));
assertEquals(3, CharsetUtils.toUtf8Bytes("...", destPtr, 0, dest.length));
assertEquals(1, CharsetUtils.toUtf8Bytes("?", destPtr, 0, dest.length));
assertEquals("3F002E0021000000", HexDump.toHexString(dest));
}
}

View File

@@ -0,0 +1,305 @@
/*
* Copyright (C) 2020 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 android.util;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.END_TAG;
import static org.xmlpull.v1.XmlPullParser.START_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import static org.xmlpull.v1.XmlPullParser.TEXT;
import android.os.PersistableBundle;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.util.XmlUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
@RunWith(AndroidJUnit4.class)
public class XmlTest {
@Test
public void testLargeValues_Normal() throws Exception {
doLargeValues(XmlUtils.makeTyped(Xml.newSerializer()),
XmlUtils.makeTyped(Xml.newPullParser()));
}
@Test
public void testLargeValues_Fast() throws Exception {
doLargeValues(Xml.newFastSerializer(),
Xml.newFastPullParser());
}
@Test
public void testLargeValues_Binary() throws Exception {
doLargeValues(Xml.newBinarySerializer(),
Xml.newBinaryPullParser());
}
/**
* Verify that we can write and read large {@link String} and {@code byte[]}
* without issues.
*/
private static void doLargeValues(TypedXmlSerializer out, TypedXmlPullParser in)
throws Exception {
final char[] chars = new char[(1 << 16) - 1];
Arrays.fill(chars, '!');
final String string = new String(chars);
final byte[] bytes = string.getBytes();
assertEquals(chars.length, bytes.length);
final ByteArrayOutputStream os = new ByteArrayOutputStream();
out.setOutput(os, StandardCharsets.UTF_8.name());
out.startTag(null, "tag");
out.attribute(null, "string", string);
out.attributeBytesBase64(null, "bytes", bytes);
out.endTag(null, "tag");
out.flush();
final ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
in.setInput(is, StandardCharsets.UTF_8.name());
assertNext(in, START_TAG, "tag", 1);
assertEquals(2, in.getAttributeCount());
assertEquals(string, in.getAttributeValue(null, "string"));
assertArrayEquals(bytes, in.getAttributeBytesBase64(null, "bytes"));
}
@Test
public void testPersistableBundle_Normal() throws Exception {
doPersistableBundle(XmlUtils.makeTyped(Xml.newSerializer()),
XmlUtils.makeTyped(Xml.newPullParser()));
}
@Test
public void testPersistableBundle_Fast() throws Exception {
doPersistableBundle(Xml.newFastSerializer(),
Xml.newFastPullParser());
}
@Test
public void testPersistableBundle_Binary() throws Exception {
doPersistableBundle(Xml.newBinarySerializer(),
Xml.newBinaryPullParser());
}
/**
* Verify that a complex {@link PersistableBundle} can be serialized out and
* then parsed in with the original structure intact.
*/
private static void doPersistableBundle(TypedXmlSerializer out, TypedXmlPullParser in)
throws Exception {
final PersistableBundle expected = buildPersistableBundle();
final byte[] raw = doPersistableBundleWrite(out, expected);
// Yes, this string-based check is fragile, but kindofEquals() is broken
// when working with nested objects and arrays
final PersistableBundle actual = doPersistableBundleRead(in, raw);
assertEquals(expected.toString(), actual.toString());
}
static PersistableBundle buildPersistableBundle() {
final PersistableBundle outer = new PersistableBundle();
outer.putBoolean("boolean", true);
outer.putInt("int", 42);
outer.putLong("long", 43L);
outer.putDouble("double", 44d);
outer.putString("string", "com.example <and></and> &amp; more");
outer.putBooleanArray("boolean[]", new boolean[] { true, false, true });
outer.putIntArray("int[]", new int[] { 42, 43, 44 });
outer.putLongArray("long[]", new long[] { 43L, 44L, 45L });
outer.putDoubleArray("double[]", new double[] { 43d, 44d, 45d });
outer.putStringArray("string[]", new String[] { "foo", "bar", "baz" });
final PersistableBundle nested = new PersistableBundle();
nested.putString("nested_key", "nested_value");
outer.putPersistableBundle("nested", nested);
return outer;
}
static byte[] doPersistableBundleWrite(TypedXmlSerializer out, PersistableBundle bundle)
throws Exception {
// We purposefully omit START/END_DOCUMENT events here to verify correct
// behavior of what PersistableBundle does internally
final ByteArrayOutputStream os = new ByteArrayOutputStream();
out.setOutput(os, StandardCharsets.UTF_8.name());
out.startTag(null, "bundle");
bundle.saveToXml(out);
out.endTag(null, "bundle");
out.flush();
return os.toByteArray();
}
static PersistableBundle doPersistableBundleRead(TypedXmlPullParser in, byte[] raw)
throws Exception {
final ByteArrayInputStream is = new ByteArrayInputStream(raw);
in.setInput(is, StandardCharsets.UTF_8.name());
in.next();
return PersistableBundle.restoreFromXml(in);
}
@Test
public void testVerify_Normal() throws Exception {
doVerify(XmlUtils.makeTyped(Xml.newSerializer()),
XmlUtils.makeTyped(Xml.newPullParser()));
}
@Test
public void testVerify_Fast() throws Exception {
doVerify(Xml.newFastSerializer(),
Xml.newFastPullParser());
}
@Test
public void testVerify_Binary() throws Exception {
doVerify(Xml.newBinarySerializer(),
Xml.newBinaryPullParser());
}
/**
* Verify that example test data is correctly serialized and parsed
* end-to-end using the given objects.
*/
private static void doVerify(TypedXmlSerializer out, TypedXmlPullParser in) throws Exception {
final ByteArrayOutputStream os = new ByteArrayOutputStream();
out.setOutput(os, StandardCharsets.UTF_8.name());
doVerifyWrite(out);
out.flush();
final ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
in.setInput(is, StandardCharsets.UTF_8.name());
doVerifyRead(in);
}
private static final String TEST_STRING = "com.example";
private static final byte[] TEST_BYTES = new byte[] { 0, 1, 2, 3, 4, 3, 2, 1, 0 };
private static void doVerifyWrite(TypedXmlSerializer out) throws Exception {
out.startDocument(StandardCharsets.UTF_8.name(), true);
out.startTag(null, "one");
{
out.startTag(null, "two");
{
out.attribute(null, "string", TEST_STRING);
out.attribute(null, "stringNumber", "49");
out.attributeBytesHex(null, "bytesHex", TEST_BYTES);
out.attributeBytesBase64(null, "bytesBase64", TEST_BYTES);
out.attributeInt(null, "int", 43);
out.attributeIntHex(null, "intHex", 44);
out.attributeLong(null, "long", 45L);
out.attributeLongHex(null, "longHex", 46L);
out.attributeFloat(null, "float", 47f);
out.attributeDouble(null, "double", 48d);
out.attributeBoolean(null, "boolean", true);
}
out.endTag(null, "two");
out.startTag(null, "three");
{
out.text("foo");
out.startTag(null, "four");
{
}
out.endTag(null, "four");
out.text("bar");
out.text("baz");
}
out.endTag(null, "three");
}
out.endTag(null, "one");
out.endDocument();
}
private static void doVerifyRead(TypedXmlPullParser in) throws Exception {
assertEquals(START_DOCUMENT, in.getEventType());
assertNext(in, START_TAG, "one", 1);
{
assertNext(in, START_TAG, "two", 2);
{
assertEquals(11, in.getAttributeCount());
assertEquals(TEST_STRING, in.getAttributeValue(null, "string"));
assertArrayEquals(TEST_BYTES, in.getAttributeBytesHex(null, "bytesHex"));
assertArrayEquals(TEST_BYTES, in.getAttributeBytesBase64(null, "bytesBase64"));
assertEquals(43, in.getAttributeInt(null, "int"));
assertEquals(44, in.getAttributeIntHex(null, "intHex"));
assertEquals(45L, in.getAttributeLong(null, "long"));
assertEquals(46L, in.getAttributeLongHex(null, "longHex"));
assertEquals(47f, in.getAttributeFloat(null, "float"), 0.01);
assertEquals(48d, in.getAttributeDouble(null, "double"), 0.01);
assertEquals(true, in.getAttributeBoolean(null, "boolean"));
// Also verify that typed values are available as strings
assertEquals("000102030403020100", in.getAttributeValue(null, "bytesHex"));
assertEquals("AAECAwQDAgEA", in.getAttributeValue(null, "bytesBase64"));
assertEquals("43", in.getAttributeValue(null, "int"));
assertEquals("2c", in.getAttributeValue(null, "intHex"));
assertEquals("45", in.getAttributeValue(null, "long"));
assertEquals("2e", in.getAttributeValue(null, "longHex"));
assertEquals("true", in.getAttributeValue(null, "boolean"));
// And that raw strings can be parsed too
assertEquals("49", in.getAttributeValue(null, "stringNumber"));
assertEquals(49, in.getAttributeInt(null, "stringNumber"));
}
assertNext(in, END_TAG, "two", 2);
assertNext(in, START_TAG, "three", 2);
{
assertNext(in, TEXT);
assertEquals("foo", in.getText().trim());
assertNext(in, START_TAG, "four", 3);
{
assertEquals(0, in.getAttributeCount());
}
assertNext(in, END_TAG, "four", 3);
assertNext(in, TEXT);
assertEquals("barbaz", in.getText().trim());
}
assertNext(in, END_TAG, "three", 2);
}
assertNext(in, END_TAG, "one", 1);
assertNext(in, END_DOCUMENT);
}
static void assertNext(TypedXmlPullParser in, int token) throws Exception {
// We're willing to skip over empty text regions, which some
// serializers emit transparently
int event;
while ((event = in.next()) == TEXT && in.getText().trim().length() == 0) {
}
assertEquals("next", token, event);
assertEquals("getEventType", token, in.getEventType());
}
static void assertNext(TypedXmlPullParser in, int token, String name, int depth)
throws Exception {
assertNext(in, token);
assertEquals("getName", name, in.getName());
assertEquals("getDepth", depth, in.getDepth());
}
}

View File

@@ -0,0 +1,348 @@
/*
* Copyright (C) 2020 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.internal.util;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import android.annotation.NonNull;
import android.util.ExceptionUtils;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.function.Consumer;
@RunWith(AndroidJUnit4.class)
public class FastDataTest {
private static final String TEST_SHORT_STRING = "a";
private static final String TEST_LONG_STRING = "com☃example☃typical☃package☃name";
private static final byte[] TEST_BYTES = TEST_LONG_STRING.getBytes(StandardCharsets.UTF_16LE);
@Test
public void testEndOfFile_Int() throws Exception {
try (FastDataInput in = new FastDataInput(new ByteArrayInputStream(
new byte[] { 1 }), 1000)) {
assertThrows(EOFException.class, () -> in.readInt());
}
try (FastDataInput in = new FastDataInput(new ByteArrayInputStream(
new byte[] { 1, 1, 1, 1 }), 1000)) {
assertEquals(1, in.readByte());
assertThrows(EOFException.class, () -> in.readInt());
}
}
@Test
public void testEndOfFile_String() throws Exception {
try (FastDataInput in = new FastDataInput(new ByteArrayInputStream(
new byte[] { 1 }), 1000)) {
assertThrows(EOFException.class, () -> in.readUTF());
}
try (FastDataInput in = new FastDataInput(new ByteArrayInputStream(
new byte[] { 1, 1, 1, 1 }), 1000)) {
assertThrows(EOFException.class, () -> in.readUTF());
}
}
@Test
public void testEndOfFile_Bytes_Small() throws Exception {
try (FastDataInput in = new FastDataInput(new ByteArrayInputStream(
new byte[] { 1, 1, 1, 1 }), 1000)) {
final byte[] tmp = new byte[10];
assertThrows(EOFException.class, () -> in.readFully(tmp));
}
try (FastDataInput in = new FastDataInput(new ByteArrayInputStream(
new byte[] { 1, 1, 1, 1 }), 1000)) {
final byte[] tmp = new byte[10_000];
assertThrows(EOFException.class, () -> in.readFully(tmp));
}
}
@Test
public void testUTF_Bounds() throws Exception {
final char[] buf = new char[65_535];
try (FastDataOutput out = new FastDataOutput(new ByteArrayOutputStream(), BOUNCE_SIZE)) {
// Writing simple string will fit fine
Arrays.fill(buf, '!');
final String simple = new String(buf);
out.writeUTF(simple);
out.writeInternedUTF(simple);
// Just one complex char will cause it to overflow
buf[0] = '☃';
final String complex = new String(buf);
assertThrows(IOException.class, () -> out.writeUTF(complex));
assertThrows(IOException.class, () -> out.writeInternedUTF(complex));
}
}
@Test
public void testBounce_Char() throws Exception {
doBounce((out) -> {
out.writeChar('\0');
out.writeChar('☃');
}, (in) -> {
assertEquals('\0', in.readChar());
assertEquals('☃', in.readChar());
});
}
@Test
public void testBounce_Short() throws Exception {
doBounce((out) -> {
out.writeShort(0);
out.writeShort((short) 0x0f0f);
out.writeShort((short) 0xf0f0);
out.writeShort(Short.MIN_VALUE);
out.writeShort(Short.MAX_VALUE);
}, (in) -> {
assertEquals(0, in.readShort());
assertEquals((short) 0x0f0f, in.readShort());
assertEquals((short) 0xf0f0, in.readShort());
assertEquals(Short.MIN_VALUE, in.readShort());
assertEquals(Short.MAX_VALUE, in.readShort());
});
}
@Test
public void testBounce_Int() throws Exception {
doBounce((out) -> {
out.writeInt(0);
out.writeInt(0x0f0f0f0f);
out.writeInt(0xf0f0f0f0);
out.writeInt(Integer.MIN_VALUE);
out.writeInt(Integer.MAX_VALUE);
}, (in) -> {
assertEquals(0, in.readInt());
assertEquals(0x0f0f0f0f, in.readInt());
assertEquals(0xf0f0f0f0, in.readInt());
assertEquals(Integer.MIN_VALUE, in.readInt());
assertEquals(Integer.MAX_VALUE, in.readInt());
});
}
@Test
public void testBounce_Long() throws Exception {
doBounce((out) -> {
out.writeLong(0);
out.writeLong(0x0f0f0f0f0f0f0f0fL);
out.writeLong(0xf0f0f0f0f0f0f0f0L);
out.writeLong(Long.MIN_VALUE);
out.writeLong(Long.MAX_VALUE);
}, (in) -> {
assertEquals(0, in.readLong());
assertEquals(0x0f0f0f0f0f0f0f0fL, in.readLong());
assertEquals(0xf0f0f0f0f0f0f0f0L, in.readLong());
assertEquals(Long.MIN_VALUE, in.readLong());
assertEquals(Long.MAX_VALUE, in.readLong());
});
}
@Test
public void testBounce_UTF() throws Exception {
doBounce((out) -> {
out.writeUTF("");
out.writeUTF("");
out.writeUTF("example");
}, (in) -> {
assertEquals("", in.readUTF());
assertEquals("", in.readUTF());
assertEquals("example", in.readUTF());
});
}
@Test
public void testBounce_UTF_Exact() throws Exception {
final char[] expectedBuf = new char[BOUNCE_SIZE];
Arrays.fill(expectedBuf, '!');
final String expected = new String(expectedBuf);
doBounce((out) -> {
out.writeUTF(expected);
}, (in) -> {
final String actual = in.readUTF();
assertEquals(expected.length(), actual.length());
assertEquals(expected, actual);
});
}
@Test
public void testBounce_UTF_Maximum() throws Exception {
final char[] expectedBuf = new char[65_535];
Arrays.fill(expectedBuf, '!');
final String expected = new String(expectedBuf);
doBounce((out) -> {
out.writeUTF(expected);
}, (in) -> {
final String actual = in.readUTF();
assertEquals(expected.length(), actual.length());
assertEquals(expected, actual);
}, 1);
}
@Test
public void testBounce_InternedUTF() throws Exception {
doBounce((out) -> {
out.writeInternedUTF("foo");
out.writeInternedUTF("bar");
out.writeInternedUTF("baz");
out.writeInternedUTF("bar");
out.writeInternedUTF("foo");
}, (in) -> {
assertEquals("foo", in.readInternedUTF());
assertEquals("bar", in.readInternedUTF());
assertEquals("baz", in.readInternedUTF());
assertEquals("bar", in.readInternedUTF());
assertEquals("foo", in.readInternedUTF());
});
}
/**
* Verify that when we overflow the maximum number of interned string
* references, we still transport the raw string values successfully.
*/
@Test
public void testBounce_InternedUTF_Maximum() throws Exception {
final int num = 70_000;
doBounce((out) -> {
for (int i = 0; i < num; i++) {
out.writeInternedUTF("foo" + i);
}
}, (in) -> {
for (int i = 0; i < num; i++) {
assertEquals("foo" + i, in.readInternedUTF());
}
}, 1);
}
@Test
public void testBounce_Bytes() throws Exception {
doBounce((out) -> {
out.write(TEST_BYTES, 8, 32);
out.writeInt(64);
}, (in) -> {
final byte[] tmp = new byte[128];
in.readFully(tmp, 8, 32);
assertArrayEquals(Arrays.copyOfRange(TEST_BYTES, 8, 8 + 32),
Arrays.copyOfRange(tmp, 8, 8 + 32));
assertEquals(64, in.readInt());
});
}
@Test
public void testBounce_Mixed() throws Exception {
doBounce((out) -> {
out.writeBoolean(true);
out.writeBoolean(false);
out.writeByte(1);
out.writeShort(2);
out.writeInt(4);
out.writeUTF(TEST_SHORT_STRING);
out.writeUTF(TEST_LONG_STRING);
out.writeLong(8L);
out.writeFloat(16f);
out.writeDouble(32d);
}, (in) -> {
assertEquals(true, in.readBoolean());
assertEquals(false, in.readBoolean());
assertEquals(1, in.readByte());
assertEquals(2, in.readShort());
assertEquals(4, in.readInt());
assertEquals(TEST_SHORT_STRING, in.readUTF());
assertEquals(TEST_LONG_STRING, in.readUTF());
assertEquals(8L, in.readLong());
assertEquals(16f, in.readFloat(), 0.01);
assertEquals(32d, in.readDouble(), 0.01);
});
}
/**
* Buffer size to use for {@link #doBounce}; purposefully chosen to be a
* small prime number to help uncover edge cases.
*/
private static final int BOUNCE_SIZE = 11;
/**
* Number of times to repeat message when bouncing; repeating is used to
* help uncover edge cases.
*/
private static final int BOUNCE_REPEAT = 1_000;
/**
* Verify that some common data can be written and read back, effectively
* "bouncing" it through a serialized representation.
*/
private static void doBounce(@NonNull ThrowingConsumer<FastDataOutput> out,
@NonNull ThrowingConsumer<FastDataInput> in) throws Exception {
doBounce(out, in, BOUNCE_REPEAT);
}
private static void doBounce(@NonNull ThrowingConsumer<FastDataOutput> out,
@NonNull ThrowingConsumer<FastDataInput> in, int count) throws Exception {
final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
final FastDataOutput outData = new FastDataOutput(outStream, BOUNCE_SIZE);
for (int i = 0; i < count; i++) {
out.accept(outData);
}
outData.flush();
final ByteArrayInputStream inStream = new ByteArrayInputStream(outStream.toByteArray());
final FastDataInput inData = new FastDataInput(inStream, BOUNCE_SIZE);
for (int i = 0; i < count; i++) {
in.accept(inData);
}
}
private static <T extends Exception> void assertThrows(Class<T> clazz, ThrowingRunnable r)
throws Exception {
try {
r.run();
fail("Expected " + clazz + " to be thrown");
} catch (Exception e) {
if (!clazz.isAssignableFrom(e.getClass())) {
throw e;
}
}
}
public interface ThrowingRunnable {
void run() throws Exception;
}
public interface ThrowingConsumer<T> extends Consumer<T> {
void acceptOrThrow(T t) throws Exception;
@Override
default void accept(T t) {
try {
acceptOrThrow(t);
} catch (Exception ex) {
throw ExceptionUtils.propagate(ex);
}
}
}
}