• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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