1 /* 2 * Copyright 2022 Google LLC 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 package com.google.android.libraries.mobiledatadownload.internal.util; 17 18 import android.text.TextUtils; 19 import android.util.Log; 20 import androidx.annotation.Nullable; 21 import com.google.protobuf.CodedOutputStream; 22 import com.google.protobuf.ExtensionRegistryLite; 23 import com.google.protobuf.InvalidProtocolBufferException; 24 import com.google.protobuf.MessageLite; 25 import com.google.protobuf.Parser; 26 import java.io.IOException; 27 import java.nio.BufferOverflowException; 28 import java.nio.BufferUnderflowException; 29 import java.nio.ByteBuffer; 30 import java.nio.charset.Charset; 31 import java.util.ArrayList; 32 import java.util.List; 33 import java.util.Locale; 34 import java.util.zip.CRC32; 35 36 /** Utils for moving Protobuf messages in and out of ByteBuffers. */ 37 // LINT.IfChange 38 public class ProtoLiteUtil { 39 public static final String TAG = "ProtoLiteUtil"; 40 41 /* 42 * File format (with tail crc): 43 * ( 44 * int: number of bytes in message; 45 * byte[number of bytes in message]: bytes of message; 46 * )...: message blocks; 47 * long: CRC of the above data 48 * 49 * File format (without tail crc): 50 * ( 51 * int: number of bytes in message; 52 * byte[number of bytes in message]: bytes of message; 53 * long: the CRC of the bytes of the message above 54 * )...: message blocks; 55 */ 56 57 private static final byte INT_BYTE_SIZE = Integer.SIZE / Byte.SIZE; 58 private static final byte LONG_BYTE_SIZE = Long.SIZE / Byte.SIZE; 59 private static final byte CRC_LEN = LONG_BYTE_SIZE; 60 61 // Used to help guess a good initial capacity for the ArrayList 62 private static final int EXPECTED_MESS_SIZE = 1000; 63 64 /** 65 * @param buf MUST be a 0 based array buffer (aka, buf.arrayOffset() must return 0) and be mutable 66 * (aka, buf.isReadOnly() must return false) 67 * @param messageType The type of the proto 68 * @param tailCrc True if there is a single CRC at the end of the file for the whole content, 69 * false if there is a CRC after every record. 70 * @return A list of proto messages read from the buffer, or null on failure. 71 */ 72 @Nullable readFromBuffer( ByteBuffer buf, Class<T> messageType, Parser<T> messageParser, boolean tailCrc)73 public static <T extends MessageLite> List<T> readFromBuffer( 74 ByteBuffer buf, Class<T> messageType, Parser<T> messageParser, boolean tailCrc) { 75 // assert buf.arrayOffset() == 0; 76 // annoyingly, ByteBuffer#array() throws an exception if the ByteBuffer was readonly 77 // assert !buf.isReadOnly(); 78 String typename = messageType.toString(); 79 80 if (tailCrc) { 81 // Validate the tail CRC before reading any messages. 82 int crcPos = buf.limit() - CRC_LEN; 83 if (crcPos < 0) { // Equivalently, buf.limit() < CRC_LEN 84 Log.e(TAG, "Protobuf data too short to be valid"); 85 return null; 86 } 87 // First off, check the crc 88 long crc = buf.getLong(crcPos); 89 // Position should still be at the beginning; 90 // the read and write operations that take an index explicitly do not touch the 91 // position 92 if (!validateCRC(buf.array(), buf.arrayOffset(), crcPos, crc)) { 93 Log.e(TAG, "Ignoring corrupt protobuf data"); 94 return null; 95 } 96 if (crcPos == 0) { // If the only thing in there was the CRC, then there are no messages 97 return new ArrayList<T>(0); 98 } 99 } 100 101 int end = tailCrc ? buf.limit() - CRC_LEN : buf.limit(); 102 103 List<T> toReturn = new ArrayList<T>((buf.limit() / EXPECTED_MESS_SIZE) + 1); 104 while (buf.position() < end) { 105 T dest; 106 int bytesInMessage; 107 try { 108 bytesInMessage = buf.getInt(); 109 } catch (BufferUnderflowException ex) { 110 handleBufferUnderflow(ex, typename); 111 return null; 112 } 113 if (bytesInMessage < 0) { 114 // This actually can happen even if the CRC check passed, 115 // if the user gave the wrong MessageLite type. 116 // Same goes for all of the other exceptions that can be thrown. 117 Log.e( 118 TAG, 119 String.format( 120 "Invalid message size: %d. May have given the wrong message type: %s", 121 bytesInMessage, typename)); 122 return null; 123 } 124 125 if (!tailCrc) { 126 // May have read a garbage size. Read carefully. 127 if (end < buf.position() + bytesInMessage + CRC_LEN) { 128 Log.e( 129 TAG, 130 String.format("Invalid message size: %d (buffer end is %d)", bytesInMessage, end)); 131 return toReturn; 132 } 133 long crc = buf.getLong(buf.position() + bytesInMessage); 134 if (!validateCRC(buf.array(), buf.arrayOffset() + buf.position(), bytesInMessage, crc)) { 135 // Return the valid messages we have read so far. 136 return toReturn; 137 } 138 } 139 140 // According to ByteBuffer#array()'s spec, this should not copy the backing array 141 dest = 142 tryCreate( 143 buf.array(), 144 buf.arrayOffset() + buf.position(), 145 bytesInMessage, 146 messageType, 147 messageParser); 148 if (dest == null) { 149 // Something is seriously hosed at this point, return nothing. 150 return null; 151 } 152 toReturn.add(dest); 153 // Advance the buffer manually, since we read from it "raw" from the array above 154 buf.position(buf.position() + bytesInMessage + (tailCrc ? 0 : CRC_LEN)); 155 } 156 return toReturn; 157 } 158 159 @Nullable tryCreate( byte[] arr, int pos, int len, Class<T> type, Parser<T> parser)160 private static <T extends MessageLite> T tryCreate( 161 byte[] arr, int pos, int len, Class<T> type, Parser<T> parser) { 162 try { 163 // Cannot use generated registry here, because it may cause NPE to clients. 164 // For more detail, see b/140135059. 165 return parser.parseFrom(arr, pos, len, ExtensionRegistryLite.getEmptyRegistry()); 166 } catch (InvalidProtocolBufferException ex) { 167 Log.e(TAG, "Cannot deserialize message of type " + type, ex); 168 return null; 169 } 170 } 171 172 /** 173 * Serializes the given MessageLite messages into a ByteBuffer, with either a CRC of the whole 174 * content at the end of the buffer (tail CRC) or a CRC of every message at the end of the 175 * message. 176 * 177 * @param coll The messages to write. 178 * @param tailCrc true to use a tail CRC, false to put a CRC after every message. 179 * @return A ByteBuffer containing the serialized messages. 180 */ 181 @Nullable dumpIntoBuffer( Iterable<T> coll, boolean tailCrc)182 public static <T extends MessageLite> ByteBuffer dumpIntoBuffer( 183 Iterable<T> coll, boolean tailCrc) { 184 int count = 0; 185 long toWriteOut = tailCrc ? CRC_LEN : 0; 186 187 final int extraBytesPerMessage = tailCrc ? INT_BYTE_SIZE : INT_BYTE_SIZE + CRC_LEN; 188 // First, get the size of how much will be written out 189 // TODO find out if there is a adder util thingy I can use (could be parallel) 190 for (MessageLite mess : coll) { 191 toWriteOut += extraBytesPerMessage + mess.getSerializedSize(); 192 ++count; 193 } 194 if (count == 0) { 195 // If there are no counters to write, don't even bother with the checksum. 196 return ByteBuffer.allocate(0); 197 } 198 // Now we got this, make a ByteBuffer to hold all that we need to 199 ByteBuffer buff = null; 200 try { 201 buff = ByteBuffer.allocate((int) toWriteOut); 202 } catch (IllegalArgumentException ex) { 203 Log.e(TAG, String.format("Too big to serialize, %s", prettyPrintBytes(toWriteOut)), ex); 204 return null; 205 } 206 207 // According to ByteBuffer#array()'s spec, this should not copy the backing array 208 byte[] arr = buff.array(); 209 // Also conveniently is where we need to write next 210 int writtenSoFar = 0; 211 // Now add in the serialized forms 212 for (MessageLite mess : coll) { 213 // As we called getSerializedSize above, this is assured to give us a non-bogus answer 214 int bytesInMessage = mess.getSerializedSize(); 215 try { 216 buff.putInt(bytesInMessage); 217 } catch (BufferOverflowException ex) { 218 handleBufferOverflow(ex); 219 return null; 220 } 221 writtenSoFar += INT_BYTE_SIZE; 222 // We are writing past the end of where buff is currently "looking at", 223 // So reusing the backing array here should be fine. 224 try { 225 mess.writeTo(CodedOutputStream.newInstance(arr, writtenSoFar, bytesInMessage)); 226 } catch (IOException e) { 227 Log.e(TAG, "Exception while writing to buffer.", e); 228 } 229 230 // Same as above, but reading past the end this time. 231 try { 232 buff.put(arr, writtenSoFar, bytesInMessage); 233 } catch (BufferOverflowException ex) { 234 handleBufferOverflow(ex); 235 return null; 236 } 237 writtenSoFar += bytesInMessage; 238 if (!tailCrc) { 239 appendCRC(buff, arr, writtenSoFar - bytesInMessage, bytesInMessage); 240 writtenSoFar += CRC_LEN; 241 } 242 } 243 if (tailCrc) { 244 try { 245 appendCRC(buff, arr, 0, writtenSoFar); 246 } catch (BufferOverflowException ex) { 247 handleBufferOverflow(ex); 248 return null; 249 } 250 } 251 buff.rewind(); 252 return buff; 253 } 254 255 /** Return string from proto bytes when we know bytes are UTF-8. */ getDataString(byte[] data)256 public static String getDataString(byte[] data) { 257 return new String(data, Charset.forName("UTF-8")); 258 } 259 260 /** Return null if input is empty (or null). */ 261 @Nullable nullIfEmpty(String input)262 public static String nullIfEmpty(String input) { 263 return TextUtils.isEmpty(input) ? null : input; 264 } 265 266 /** Return null if input array is empty (or null). */ 267 @Nullable nullIfEmpty(T[] input)268 public static <T> T[] nullIfEmpty(T[] input) { 269 return input == null || input.length == 0 ? null : input; 270 } 271 272 /** Similar to Objects.equal but available pre-kitkat. */ safeEqual(Object a, Object b)273 public static boolean safeEqual(Object a, Object b) { 274 return a == null ? b == null : a.equals(b); 275 } 276 277 /** Wraps MessageLite.toByteArray to check for null and return null if that's the case. */ 278 @Nullable safeToByteArray(MessageLite msg)279 public static final byte[] safeToByteArray(MessageLite msg) { 280 return msg == null ? null : msg.toByteArray(); 281 } 282 handleBufferUnderflow(BufferUnderflowException ex, String typename)283 private static void handleBufferUnderflow(BufferUnderflowException ex, String typename) { 284 Log.e( 285 TAG, 286 String.format("Buffer underflow. May have given the wrong message type: %s", typename), 287 ex); 288 } 289 handleBufferOverflow(BufferOverflowException ex)290 private static void handleBufferOverflow(BufferOverflowException ex) { 291 Log.e( 292 TAG, 293 "Buffer underflow. A message may have an invalid serialized form" 294 + " or has been concurrently modified.", 295 ex); 296 } 297 298 /** 299 * Reads the bytes given in an array and appends the CRC32 checksum to the ByteBuffer. The 300 * location the CRC32 checksum will be written is the {@link ByteBuffer#position() current 301 * position} in the ByteBuffer. The given ByteBuffer must have must have enough room (starting at 302 * its position) to fit an additonal {@link #CRC_LEN} bytes. 303 * 304 * @param dest where to write the CRC32 checksum; must have enough room to fit an additonal {@link 305 * #CRC_LEN} bytes 306 * @param src the array of bytes containing the data to checksum 307 * @param off offset of where to start reading the array 308 * @param len number of bytes to read in the array 309 */ appendCRC(ByteBuffer dest, byte[] src, int off, int len)310 private static void appendCRC(ByteBuffer dest, byte[] src, int off, int len) { 311 CRC32 crc = new CRC32(); 312 crc.update(src, off, len); 313 dest.putLong(crc.getValue()); 314 } 315 validateCRC(byte[] arr, int off, int len, long expectedCRC)316 private static boolean validateCRC(byte[] arr, int off, int len, long expectedCRC) { 317 CRC32 crc = new CRC32(); 318 crc.update(arr, off, len); 319 long computedCRC = crc.getValue(); 320 boolean matched = computedCRC == expectedCRC; 321 if (!matched) { 322 Log.e( 323 TAG, 324 String.format( 325 "Corrupt protobuf data, expected CRC: %d computed CRC: %d", 326 expectedCRC, computedCRC)); 327 } 328 return matched; 329 } 330 ProtoLiteUtil()331 private ProtoLiteUtil() { 332 // No instantiation. 333 } 334 prettyPrintBytes(long bytes)335 private static String prettyPrintBytes(long bytes) { 336 if (bytes > 1024L * 1024 * 1024) { 337 return String.format(Locale.US, "%.2fGB", (double) bytes / (1024L * 1024 * 1024)); 338 } else if (bytes > 1024 * 1024) { 339 return String.format(Locale.US, "%.2fMB", (double) bytes / (1024 * 1024)); 340 } else if (bytes > 1024) { 341 return String.format(Locale.US, "%.2fKB", (double) bytes / 1024); 342 } 343 return String.format(Locale.US, "%d Bytes", bytes); 344 } 345 } 346 // LINT.ThenChange(<internal>) 347