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