1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.modules.utils; 18 19 import android.annotation.NonNull; 20 21 import dalvik.system.VMRuntime; 22 23 import java.io.BufferedInputStream; 24 import java.io.Closeable; 25 import java.io.DataInput; 26 import java.io.DataInputStream; 27 import java.io.EOFException; 28 import java.io.IOException; 29 import java.io.InputStream; 30 import java.util.Arrays; 31 import java.util.Objects; 32 33 /** 34 * Optimized implementation of {@link DataInput} which buffers data in memory 35 * from the underlying {@link InputStream}. 36 * <p> 37 * Benchmarks have demonstrated this class is 3x more efficient than using a 38 * {@link DataInputStream} with a {@link BufferedInputStream}. 39 */ 40 public class FastDataInput implements DataInput, Closeable { 41 protected static final int MAX_UNSIGNED_SHORT = 65_535; 42 43 protected static final int DEFAULT_BUFFER_SIZE = 32_768; 44 45 protected final VMRuntime mRuntime; 46 47 protected final byte[] mBuffer; 48 protected final int mBufferCap; 49 50 private InputStream mIn; 51 protected int mBufferPos; 52 protected int mBufferLim; 53 54 /** 55 * Values that have been "interned" by {@link #readInternedUTF()}. 56 */ 57 private int mStringRefCount = 0; 58 private String[] mStringRefs = new String[32]; 59 FastDataInput(@onNull InputStream in, int bufferSize)60 public FastDataInput(@NonNull InputStream in, int bufferSize) { 61 mRuntime = VMRuntime.getRuntime(); 62 mIn = Objects.requireNonNull(in); 63 if (bufferSize < 8) { 64 throw new IllegalArgumentException(); 65 } 66 67 mBuffer = (byte[]) mRuntime.newNonMovableArray(byte.class, bufferSize); 68 mBufferCap = mBuffer.length; 69 } 70 71 /** 72 * Obtain a {@link FastDataInput} configured with the given 73 * {@link InputStream} and which encodes large code-points using 3-byte 74 * sequences. 75 * <p> 76 * This <em>is</em> compatible with the {@link DataInput} API contract, 77 * which specifies that large code-points must be encoded with 3-byte 78 * sequences. 79 */ obtain(@onNull InputStream in)80 public static FastDataInput obtain(@NonNull InputStream in) { 81 return new FastDataInput(in, DEFAULT_BUFFER_SIZE); 82 } 83 84 /** 85 * Release a {@link FastDataInput} to potentially be recycled. You must not 86 * interact with the object after releasing it. 87 */ release()88 public void release() { 89 mIn = null; 90 mBufferPos = 0; 91 mBufferLim = 0; 92 mStringRefCount = 0; 93 } 94 95 /** 96 * Re-initializes the object for the new input. 97 */ setInput(@onNull InputStream in)98 protected void setInput(@NonNull InputStream in) { 99 if (mIn != null) { 100 throw new IllegalStateException("setInput() called before calling release()"); 101 } 102 103 mIn = Objects.requireNonNull(in); 104 mBufferPos = 0; 105 mBufferLim = 0; 106 mStringRefCount = 0; 107 } 108 fill(int need)109 protected void fill(int need) throws IOException { 110 final int remain = mBufferLim - mBufferPos; 111 System.arraycopy(mBuffer, mBufferPos, mBuffer, 0, remain); 112 mBufferPos = 0; 113 mBufferLim = remain; 114 need -= remain; 115 116 while (need > 0) { 117 int c = mIn.read(mBuffer, mBufferLim, mBufferCap - mBufferLim); 118 if (c == -1) { 119 throw new EOFException(); 120 } else { 121 mBufferLim += c; 122 need -= c; 123 } 124 } 125 } 126 127 @Override close()128 public void close() throws IOException { 129 mIn.close(); 130 release(); 131 } 132 133 @Override readFully(byte[] b)134 public void readFully(byte[] b) throws IOException { 135 readFully(b, 0, b.length); 136 } 137 138 @Override readFully(byte[] b, int off, int len)139 public void readFully(byte[] b, int off, int len) throws IOException { 140 // Attempt to read directly from buffer space if there's enough room, 141 // otherwise fall back to chunking into place 142 if (mBufferCap >= len) { 143 if (mBufferLim - mBufferPos < len) fill(len); 144 System.arraycopy(mBuffer, mBufferPos, b, off, len); 145 mBufferPos += len; 146 } else { 147 final int remain = mBufferLim - mBufferPos; 148 System.arraycopy(mBuffer, mBufferPos, b, off, remain); 149 mBufferPos += remain; 150 off += remain; 151 len -= remain; 152 153 while (len > 0) { 154 int c = mIn.read(b, off, len); 155 if (c == -1) { 156 throw new EOFException(); 157 } else { 158 off += c; 159 len -= c; 160 } 161 } 162 } 163 } 164 165 @Override readUTF()166 public String readUTF() throws IOException { 167 // Attempt to read directly from buffer space if there's enough room, 168 // otherwise fall back to chunking into place 169 final int len = readUnsignedShort(); 170 if (mBufferCap > len) { 171 if (mBufferLim - mBufferPos < len) fill(len); 172 final String res = ModifiedUtf8.decode(mBuffer, new char[len], mBufferPos, len); 173 mBufferPos += len; 174 return res; 175 } else { 176 final byte[] tmp = (byte[]) mRuntime.newNonMovableArray(byte.class, len + 1); 177 readFully(tmp, 0, len); 178 return ModifiedUtf8.decode(tmp, new char[len], 0, len); 179 } 180 } 181 182 /** 183 * Read a {@link String} value with the additional signal that the given 184 * value is a candidate for being canonicalized, similar to 185 * {@link String#intern()}. 186 * <p> 187 * Canonicalization is implemented by writing each unique string value once 188 * the first time it appears, and then writing a lightweight {@code short} 189 * reference when that string is written again in the future. 190 * 191 * @see FastDataOutput#writeInternedUTF(String) 192 */ readInternedUTF()193 public @NonNull String readInternedUTF() throws IOException { 194 final int ref = readUnsignedShort(); 195 if (ref == MAX_UNSIGNED_SHORT) { 196 final String s = readUTF(); 197 198 // We can only safely intern when we have remaining values; if we're 199 // full we at least sent the string value above 200 if (mStringRefCount < MAX_UNSIGNED_SHORT) { 201 if (mStringRefCount == mStringRefs.length) { 202 mStringRefs = Arrays.copyOf(mStringRefs, 203 mStringRefCount + (mStringRefCount >> 1)); 204 } 205 mStringRefs[mStringRefCount++] = s; 206 } 207 208 return s; 209 } else { 210 if (ref >= mStringRefs.length) { 211 throw new IOException("Invalid interned string reference " + ref + " for " 212 + mStringRefs.length + " interned strings"); 213 } 214 return mStringRefs[ref]; 215 } 216 } 217 218 @Override readBoolean()219 public boolean readBoolean() throws IOException { 220 return readByte() != 0; 221 } 222 223 /** 224 * Returns the same decoded value as {@link #readByte()} but without 225 * actually consuming the underlying data. 226 */ peekByte()227 public byte peekByte() throws IOException { 228 if (mBufferLim - mBufferPos < 1) fill(1); 229 return mBuffer[mBufferPos]; 230 } 231 232 @Override readByte()233 public byte readByte() throws IOException { 234 if (mBufferLim - mBufferPos < 1) fill(1); 235 return mBuffer[mBufferPos++]; 236 } 237 238 @Override readUnsignedByte()239 public int readUnsignedByte() throws IOException { 240 return Byte.toUnsignedInt(readByte()); 241 } 242 243 @Override readShort()244 public short readShort() throws IOException { 245 if (mBufferLim - mBufferPos < 2) fill(2); 246 return (short) (((mBuffer[mBufferPos++] & 0xff) << 8) | 247 ((mBuffer[mBufferPos++] & 0xff) << 0)); 248 } 249 250 @Override readUnsignedShort()251 public int readUnsignedShort() throws IOException { 252 return Short.toUnsignedInt((short) readShort()); 253 } 254 255 @Override readChar()256 public char readChar() throws IOException { 257 return (char) readShort(); 258 } 259 260 @Override readInt()261 public int readInt() throws IOException { 262 if (mBufferLim - mBufferPos < 4) fill(4); 263 return (((mBuffer[mBufferPos++] & 0xff) << 24) | 264 ((mBuffer[mBufferPos++] & 0xff) << 16) | 265 ((mBuffer[mBufferPos++] & 0xff) << 8) | 266 ((mBuffer[mBufferPos++] & 0xff) << 0)); 267 } 268 269 @Override readLong()270 public long readLong() throws IOException { 271 if (mBufferLim - mBufferPos < 8) fill(8); 272 int h = ((mBuffer[mBufferPos++] & 0xff) << 24) | 273 ((mBuffer[mBufferPos++] & 0xff) << 16) | 274 ((mBuffer[mBufferPos++] & 0xff) << 8) | 275 ((mBuffer[mBufferPos++] & 0xff) << 0); 276 int l = ((mBuffer[mBufferPos++] & 0xff) << 24) | 277 ((mBuffer[mBufferPos++] & 0xff) << 16) | 278 ((mBuffer[mBufferPos++] & 0xff) << 8) | 279 ((mBuffer[mBufferPos++] & 0xff) << 0); 280 return (((long) h) << 32L) | ((long) l) & 0xffffffffL; 281 } 282 283 @Override readFloat()284 public float readFloat() throws IOException { 285 return Float.intBitsToFloat(readInt()); 286 } 287 288 @Override readDouble()289 public double readDouble() throws IOException { 290 return Double.longBitsToDouble(readLong()); 291 } 292 293 @Override skipBytes(int n)294 public int skipBytes(int n) throws IOException { 295 // Callers should read data piecemeal 296 throw new UnsupportedOperationException(); 297 } 298 299 @Override readLine()300 public String readLine() throws IOException { 301 // Callers should read data piecemeal 302 throw new UnsupportedOperationException(); 303 } 304 } 305