Utils.java
/*
* Copyright 2023 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 androidx.javascriptengine.common;
import android.content.res.AssetFileDescriptor;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.Closeable;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;
/**
* Utility methods for use in both service and client side of JavaScriptEngine.
*/
public class Utils {
private static final String TAG = "JavaScriptEngineUtils";
private Utils() {
throw new AssertionError();
}
/**
* Utility method to write a byte array into a stream.
*/
public static void writeByteArrayToStream(@NonNull byte[] inputBytes,
@NonNull OutputStream outputStream) {
try {
outputStream.write(inputBytes);
outputStream.flush();
} catch (IOException e) {
Log.e(TAG, "Writing to outputStream failed", e);
} finally {
closeQuietly(outputStream);
}
}
/**
* Close, ignoring exception.
*/
public static void closeQuietly(@Nullable Closeable closeable) {
if (closeable == null) return;
try {
closeable.close();
} catch (IOException ex) {
// Ignore the exception on close.
}
}
/**
* Creates a pipe, writes the given bytes into one end and returns the other end.
*/
@NonNull
public static AssetFileDescriptor writeBytesIntoPipeAsync(@NonNull byte[] inputBytes,
@NonNull ExecutorService executorService) throws IOException {
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
ParcelFileDescriptor readSide = pipe[0];
ParcelFileDescriptor writeSide = pipe[1];
OutputStream outputStream =
new ParcelFileDescriptor.AutoCloseOutputStream(writeSide);
executorService.execute(
() -> Utils.writeByteArrayToStream(inputBytes, outputStream));
return new AssetFileDescriptor(readSide, 0, inputBytes.length);
}
/**
* Checks if the given AssetFileDescriptor passes certain conditions.
*/
public static void checkAssetFileDescriptor(@NonNull AssetFileDescriptor afd,
boolean allowUnknownLength) {
if (afd.getStartOffset() < 0) {
throw new IllegalArgumentException(
"AssetFileDescriptor offset should be >= 0");
}
if (afd.getLength() != AssetFileDescriptor.UNKNOWN_LENGTH && afd.getLength() < 0) {
throw new IllegalArgumentException(
"AssetFileDescriptor should have valid length");
}
if (afd.getDeclaredLength() != AssetFileDescriptor.UNKNOWN_LENGTH
&& afd.getDeclaredLength() < 0) {
throw new IllegalArgumentException(
"AssetFileDescriptor should have valid declared length");
}
if (afd.getLength() == AssetFileDescriptor.UNKNOWN_LENGTH && afd.getStartOffset() != 0) {
throw new UnsupportedOperationException(
"AssetFileDescriptor offset should be 0 for unknown length");
}
if (!allowUnknownLength && afd.getLength() == AssetFileDescriptor.UNKNOWN_LENGTH) {
throw new UnsupportedOperationException(
"AssetFileDescriptor should have known length");
}
}
/**
* Read a given number of bytes from a given stream into a byte array.
* <p>
* This allows us to use
* <a href=https://developer.android.com/reference/java/io/InputStream#readNBytes(byte[],%20int,%20int)">
* this </a>
* functionality added in API 33.
*/
public static int readNBytes(@NonNull InputStream inputStream, @NonNull byte[] b, int off,
int len)
throws IOException {
int n = 0;
while (n < len) {
int count = inputStream.read(b, off + n, len - n);
if (count < 0) {
break;
}
n += count;
}
return n;
}
/**
* Checks whether a given byte is a UTF8 continuation byte. If a byte can be part of valid
* UTF-8 and is not a continuation byte, it must be a starting byte.
*/
public static boolean isUTF8ContinuationByte(byte b) {
final byte maskContinuationByte = (byte) 0b11000000;
final byte targetContinuationByte = (byte) 0b10000000;
// Checks whether it looks like "0b10xxxxxx"
return (b & maskContinuationByte) == targetContinuationByte;
}
/**
* Returns the index of right-most UTF-8 starting byte.
* <p>
* The input must be valid (or truncated) UTF-8 encoded bytes.
* Returns -1 if there is no starting byte.
*/
public static int getLastUTF8StartingByteIndex(@NonNull byte[] bytes) {
for (int index = bytes.length - 1; index >= 0; index--) {
if (!isUTF8ContinuationByte(bytes[index])) {
return index;
}
}
return -1;
}
/**
* Read from a AssetFileDescriptor into a String and closes it in case of both success and
* failure.
*/
@NonNull
public static String readToString(@NonNull AssetFileDescriptor afd, int maxLength,
boolean truncate)
throws IOException, LengthLimitExceededException {
try {
Utils.checkAssetFileDescriptor(afd, /*allowUnknownLength=*/ false);
int lengthToRead = (int) afd.getLength();
if (afd.getLength() > maxLength) {
if (truncate) {
// If truncate is true, read how much ever you are allowed to read.
lengthToRead = maxLength;
} else {
throw new LengthLimitExceededException(
"AssetFileDescriptor.getLength() should be"
+ " <= " + maxLength);
}
}
byte[] bytes = new byte[lengthToRead];
// We can use AssetFileDescriptor.createInputStream() to get the InputStream directly
// but this API is currently broken while fixing another issue regarding multiple
// AssetFileDescriptor pointing to the same file. (b/263325931)
// Using ParcelFileDescriptor to read the file is correct as long as the offset is 0.
try (ParcelFileDescriptor pfd = afd.getParcelFileDescriptor()) {
InputStream inputStream = new FileInputStream(pfd.getFileDescriptor());
if (Utils.readNBytes(inputStream, bytes, 0, lengthToRead) != lengthToRead) {
throw new IOException("Couldn't read " + lengthToRead + " bytes from the "
+ "AssetFileDescriptor");
}
}
int validUtf8PrefixLength = lengthToRead;
if (truncate) {
// Ignoring the last partial/complete codepoint.
validUtf8PrefixLength = getLastUTF8StartingByteIndex(bytes);
}
// This process can be made more memory efficient by converting the UTF-8 encoded
// bytes to String by reading from the pipe in chunks.
return new String(bytes, 0, validUtf8PrefixLength, StandardCharsets.UTF_8);
} finally {
afd.close();
}
}
/**
* Convert an Exception to a RuntimeException if needed, without needlessly wrapping up an
* existing RuntimeException.
* <p>
* Compared to {@code new RuntimeException(e)}, this avoids hiding a specific subclass of
* RuntimeException from catch blocks if one is available.
*
* @param e Exception to potentially wrap.
* @return e if e was a RuntimeException, else e wrapped in a RuntimeException.
*/
@NonNull
public static RuntimeException exceptionToRuntimeException(@NonNull Exception e) {
if (e instanceof RuntimeException) {
// Don't hide the original RuntimeException type by wrapping it in another
// RuntimeException. Just return it directly.
return (RuntimeException) e;
} else {
return new RuntimeException(e);
}
}
}