1 /*
2  * Copyright (C) 2018 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.test.protoinputstream;
18 
19 import android.util.proto.ProtoInputStream;
20 import android.util.proto.ProtoStream;
21 import android.util.proto.WireTypeMismatchException;
22 
23 import junit.framework.TestCase;
24 
25 import java.io.ByteArrayInputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 
29 public class ProtoInputStreamObjectTest extends TestCase {
30 
31 
32     class SimpleObject {
33         public char mChar;
34         public char mLargeChar;
35         public String mString;
36         public SimpleObject mNested;
37 
parseProto(ProtoInputStream pi)38         void parseProto(ProtoInputStream pi) throws IOException {
39             final long uintFieldFlags =
40                     ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_UINT32;
41             final long stringFieldFlags =
42                     ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_STRING;
43             final long messageFieldFlags =
44                     ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_MESSAGE;
45             final long charId = uintFieldFlags | ((long) 2 & 0x0ffffffffL);
46             final long largeCharId = uintFieldFlags | ((long) 5000 & 0x0ffffffffL);
47             final long stringId = stringFieldFlags | ((long) 4 & 0x0ffffffffL);
48             final long nestedId = messageFieldFlags | ((long) 5 & 0x0ffffffffL);
49 
50             while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
51                 switch (pi.getFieldNumber()) {
52                     case (int) charId:
53                         mChar = (char) pi.readInt(charId);
54                         break;
55                     case (int) largeCharId:
56                         mLargeChar = (char) pi.readInt(largeCharId);
57                         break;
58                     case (int) stringId:
59                         mString = pi.readString(stringId);
60                         break;
61                     case (int) nestedId:
62                         long token = pi.start(nestedId);
63                         mNested = new SimpleObject();
64                         mNested.parseProto(pi);
65                         pi.end(token);
66                         break;
67                     default:
68                         fail("Unexpected field id " + pi.getFieldNumber());
69                 }
70             }
71         }
72 
73     }
74 
75     /**
76      * Test reading an object with one char in it.
77      */
testObjectOneChar()78     public void testObjectOneChar() throws IOException {
79         testObjectOneChar(0);
80         testObjectOneChar(1);
81         testObjectOneChar(5);
82     }
83 
84     /**
85      * Implementation of testObjectOneChar for a given chunkSize.
86      */
testObjectOneChar(int chunkSize)87     private void testObjectOneChar(int chunkSize) throws IOException {
88         final long messageFieldFlags =
89                 ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_MESSAGE;
90 
91         final long messageId1 = messageFieldFlags | ((long) 1 & 0x0ffffffffL);
92         final long messageId2 = messageFieldFlags | ((long) 2 & 0x0ffffffffL);
93 
94         final byte[] protobuf = new byte[]{
95                 // Message 2 : { char 2 : 'c' }
96                 (byte) 0x12, (byte) 0x02, (byte) 0x10, (byte) 0x63,
97                 // Message 1 : { char 2 : 'b' }
98                 (byte) 0x0a, (byte) 0x02, (byte) 0x10, (byte) 0x62,
99         };
100 
101         InputStream stream = new ByteArrayInputStream(protobuf);
102         final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize);
103 
104         SimpleObject result = null;
105 
106         while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
107             switch (pi.getFieldNumber()) {
108                 case (int) messageId1:
109                     final long token = pi.start(messageId1);
110                     result = new SimpleObject();
111                     result.parseProto(pi);
112                     pi.end(token);
113                     break;
114                 case (int) messageId2:
115                     // Intentionally don't read the data. Parse should continue normally
116                     break;
117                 default:
118                     fail("Unexpected field id " + pi.getFieldNumber());
119             }
120         }
121         stream.close();
122 
123         assertNotNull(result);
124         assertEquals('b', result.mChar);
125     }
126 
127     /**
128      * Test reading an object with one multibyte unicode char in it.
129      */
testObjectOneLargeChar()130     public void testObjectOneLargeChar() throws IOException {
131         testObjectOneLargeChar(0);
132         testObjectOneLargeChar(1);
133         testObjectOneLargeChar(5);
134     }
135 
136     /**
137      * Implementation of testObjectOneLargeChar for a given chunkSize.
138      */
testObjectOneLargeChar(int chunkSize)139     private void testObjectOneLargeChar(int chunkSize) throws IOException {
140         final long messageFieldFlags =
141                 ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_MESSAGE;
142 
143         final long messageId1 = messageFieldFlags | ((long) 1 & 0x0ffffffffL);
144         final long messageId2 = messageFieldFlags | ((long) 2 & 0x0ffffffffL);
145 
146         final byte[] protobuf = new byte[]{
147                 // Message 2 : { char 5000 : '\u3110' }
148                 (byte) 0x12, (byte) 0x05, (byte) 0xc0, (byte) 0xb8,
149                 (byte) 0x02, (byte) 0x90, (byte) 0x62,
150                 // Message 1 : { char 5000 : '\u3110' }
151                 (byte) 0x0a, (byte) 0x05, (byte) 0xc0, (byte) 0xb8,
152                 (byte) 0x02, (byte) 0x90, (byte) 0x62,
153         };
154 
155         InputStream stream = new ByteArrayInputStream(protobuf);
156         final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize);
157 
158         SimpleObject result = null;
159 
160         while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
161             switch (pi.getFieldNumber()) {
162                 case (int) messageId1:
163                     final long token = pi.start(messageId1);
164                     result = new SimpleObject();
165                     result.parseProto(pi);
166                     pi.end(token);
167                     break;
168                 case (int) messageId2:
169                     // Intentionally don't read the data. Parse should continue normally
170                     break;
171                 default:
172                     fail("Unexpected field id " + pi.getFieldNumber());
173             }
174         }
175         stream.close();
176 
177         assertNotNull(result);
178         assertEquals('\u3110', result.mLargeChar);
179     }
180 
181     /**
182      * Test reading a char, then an object, then a char.
183      */
testObjectAndTwoChars()184     public void testObjectAndTwoChars() throws IOException {
185         testObjectAndTwoChars(0);
186         testObjectAndTwoChars(1);
187         testObjectAndTwoChars(5);
188     }
189 
190     /**
191      * Implementation of testObjectAndTwoChars for a given chunkSize.
192      */
testObjectAndTwoChars(int chunkSize)193     private void testObjectAndTwoChars(int chunkSize) throws IOException  {
194         final long uintFieldFlags =
195                 ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_UINT32;
196         final long messageFieldFlags =
197                 ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_MESSAGE;
198 
199         final long charId1 = uintFieldFlags | ((long) 1 & 0x0ffffffffL);
200         final long messageId2 = messageFieldFlags | ((long) 2 & 0x0ffffffffL);
201         final long charId4 = uintFieldFlags | ((long) 4 & 0x0ffffffffL);
202 
203         final byte[] protobuf = new byte[]{
204                 // 1 -> 'a'
205                 (byte) 0x08, (byte) 0x61,
206                 // Message 1 : { char 2 : 'b' }
207                 (byte) 0x12, (byte) 0x02, (byte) 0x10, (byte) 0x62,
208                 // 4 -> 'c'
209                 (byte) 0x20, (byte) 0x63,
210         };
211 
212         InputStream stream = new ByteArrayInputStream(protobuf);
213         final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize);
214 
215         SimpleObject obj = null;
216         char char1 = '\0';
217         char char4 = '\0';
218 
219         while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
220             switch (pi.getFieldNumber()) {
221                 case (int) charId1:
222                     char1 = (char) pi.readInt(charId1);
223                     break;
224                 case (int) messageId2:
225                     final long token = pi.start(messageId2);
226                     obj = new SimpleObject();
227                     obj.parseProto(pi);
228                     pi.end(token);
229                     break;
230                 case (int) charId4:
231                     char4 = (char) pi.readInt(charId4);
232                     break;
233                 default:
234                     fail("Unexpected field id " + pi.getFieldNumber());
235             }
236         }
237         stream.close();
238 
239         assertEquals('a', char1);
240         assertNotNull(obj);
241         assertEquals('b', obj.mChar);
242         assertEquals('c', char4);
243     }
244 
245     /**
246      * Test reading a char, then an object with an int and a string in it, then a char.
247      */
testComplexObject()248     public void testComplexObject() throws IOException {
249         testComplexObject(0);
250         testComplexObject(1);
251         testComplexObject(5);
252     }
253 
254     /**
255      * Implementation of testComplexObject for a given chunkSize.
256      */
testComplexObject(int chunkSize)257     private void testComplexObject(int chunkSize) throws IOException  {
258         final long uintFieldFlags =
259                 ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_UINT32;
260         final long messageFieldFlags =
261                 ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_MESSAGE;
262 
263         final long charId1 = uintFieldFlags | ((long) 1 & 0x0ffffffffL);
264         final long messageId2 = messageFieldFlags | ((long) 2 & 0x0ffffffffL);
265         final long charId4 = uintFieldFlags | ((long) 4 & 0x0ffffffffL);
266 
267         final byte[] protobuf = new byte[]{
268                 // 1 -> 'x'
269                 (byte) 0x08, (byte) 0x78,
270                 // begin object 2
271                 (byte) 0x12, (byte) 0x10,
272                 // 2 -> 'y'
273                 (byte) 0x10, (byte) 0x79,
274                 // 4 -> "abcdefghijkl"
275                 (byte) 0x22, (byte) 0x0c,
276                 (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66,
277                 (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6a, (byte) 0x6b, (byte) 0x6c,
278                 // 4 -> 'z'
279                 (byte) 0x20, (byte) 0x7a,
280         };
281 
282         InputStream stream = new ByteArrayInputStream(protobuf);
283         final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize);
284 
285         SimpleObject obj = null;
286         char char1 = '\0';
287         char char4 = '\0';
288 
289         while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
290             switch (pi.getFieldNumber()) {
291                 case (int) charId1:
292                     char1 = (char) pi.readInt(charId1);
293                     break;
294                 case (int) messageId2:
295                     final long token = pi.start(messageId2);
296                     obj = new SimpleObject();
297                     obj.parseProto(pi);
298                     pi.end(token);
299                     break;
300                 case (int) charId4:
301                     char4 = (char) pi.readInt(charId4);
302                     break;
303                 default:
304                     fail("Unexpected field id " + pi.getFieldNumber());
305             }
306         }
307         stream.close();
308 
309         assertEquals('x', char1);
310         assertNotNull(obj);
311         assertEquals('y', obj.mChar);
312         assertEquals("abcdefghijkl", obj.mString);
313         assertEquals('z', char4);
314     }
315 
316     /**
317      * Test reading 3 levels deep of objects.
318      */
testDeepObjects()319     public void testDeepObjects() throws IOException {
320         testDeepObjects(0);
321         testDeepObjects(1);
322         testDeepObjects(5);
323     }
324 
325     /**
326      * Implementation of testDeepObjects for a given chunkSize.
327      */
testDeepObjects(int chunkSize)328     private void testDeepObjects(int chunkSize) throws IOException  {
329         final long messageFieldFlags =
330                 ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_MESSAGE;
331         final long messageId2 = messageFieldFlags | ((long) 2 & 0x0ffffffffL);
332 
333         final byte[] protobuf = new byte[]{
334                 // begin object id 2
335                 (byte) 0x12, (byte) 0x1a,
336                 // 2 -> 'a'
337                 (byte) 0x10, (byte) 0x61,
338                 // begin nested object id 5
339                 (byte) 0x2a, (byte) 0x15,
340                 // 5000 -> '\u3110'
341                 (byte) 0xc0, (byte) 0xb8,
342                 (byte) 0x02, (byte) 0x90, (byte) 0x62,
343                 // begin nested object id 5
344                 (byte) 0x2a, (byte) 0x0e,
345                 // 4 -> "abcdefghijkl"
346                 (byte) 0x22, (byte) 0x0c,
347                 (byte) 0x61, (byte) 0x62, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66,
348                 (byte) 0x67, (byte) 0x68, (byte) 0x69, (byte) 0x6a, (byte) 0x6b, (byte) 0x6c,
349         };
350 
351         InputStream stream = new ByteArrayInputStream(protobuf);
352         final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize);
353 
354         SimpleObject obj = null;
355 
356         while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
357             switch (pi.getFieldNumber()) {
358                 case (int) messageId2:
359                     final long token = pi.start(messageId2);
360                     obj = new SimpleObject();
361                     obj.parseProto(pi);
362                     pi.end(token);
363                     break;
364                 default:
365                     fail("Unexpected field id " + pi.getFieldNumber());
366             }
367         }
368         stream.close();
369 
370         assertNotNull(obj);
371         assertEquals('a', obj.mChar);
372         assertNotNull(obj.mNested);
373         assertEquals('\u3110', obj.mNested.mLargeChar);
374         assertNotNull(obj.mNested.mNested);
375         assertEquals("abcdefghijkl", obj.mNested.mNested.mString);
376     }
377 
378     /**
379      * Test that using the wrong read method throws an exception
380      */
testBadReadType()381     public void testBadReadType() throws IOException {
382         final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_MESSAGE;
383 
384         final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL);
385 
386         final byte[] protobuf = new byte[]{
387                 // 1 -> {1}
388                 (byte) 0x0a,
389                 (byte) 0x01,
390                 (byte) 0x01,
391         };
392 
393         ProtoInputStream pi = new ProtoInputStream(protobuf);
394         pi.nextField();
395         try {
396             pi.readFloat(fieldId1);
397             fail("Should have thrown IllegalArgumentException");
398         } catch (IllegalArgumentException iae) {
399             // good
400         }
401 
402         pi = new ProtoInputStream(protobuf);
403         pi.nextField();
404         try {
405             pi.readDouble(fieldId1);
406             fail("Should have thrown IllegalArgumentException");
407         } catch (IllegalArgumentException iae) {
408             // good
409         }
410 
411         pi = new ProtoInputStream(protobuf);
412         pi.nextField();
413         try {
414             pi.readInt(fieldId1);
415             fail("Should have thrown IllegalArgumentException");
416         } catch (IllegalArgumentException iae) {
417             // good
418         }
419 
420         pi = new ProtoInputStream(protobuf);
421         pi.nextField();
422         try {
423             pi.readLong(fieldId1);
424             fail("Should have thrown IllegalArgumentException");
425         } catch (IllegalArgumentException iae) {
426             // good
427         }
428 
429         pi = new ProtoInputStream(protobuf);
430         pi.nextField();
431         try {
432             pi.readBoolean(fieldId1);
433             fail("Should have thrown IllegalArgumentException");
434         } catch (IllegalArgumentException iae) {
435             // good
436         }
437 
438         pi = new ProtoInputStream(protobuf);
439         pi.nextField();
440         try {
441             pi.readString(fieldId1);
442             fail("Should have thrown IllegalArgumentException");
443         } catch (IllegalArgumentException iae) {
444             // good
445         }
446     }
447 
448     /**
449      * Test that unexpected wrong wire types will throw an exception
450      */
testBadWireType()451     public void testBadWireType() throws IOException {
452         final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_MESSAGE;
453 
454         final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL);
455         final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL);
456         final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL);
457         final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL);
458 
459         final byte[] protobuf = new byte[]{
460                 // 1 : varint -> 1
461                 (byte) 0x08,
462                 (byte) 0x01,
463                 // 2 : fixed64 -> 0x1
464                 (byte) 0x11,
465                 (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00,
466                 (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
467                 // 3 : length delimited -> { 1 }
468                 (byte) 0x1a,
469                 (byte) 0x01,
470                 (byte) 0x01,
471                 // 6 : fixed32
472                 (byte) 0x35,
473                 (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00,
474         };
475 
476         InputStream stream = new ByteArrayInputStream(protobuf);
477         final ProtoInputStream pi = new ProtoInputStream(stream);
478 
479         while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
480             try {
481                 switch (pi.getFieldNumber()) {
482                     case (int) fieldId1:
483                         pi.readBytes(fieldId1);
484                         fail("Should have thrown a WireTypeMismatchException");
485                         break;
486                     case (int) fieldId2:
487                         pi.readBytes(fieldId2);
488                         fail("Should have thrown a WireTypeMismatchException");
489                         break;
490                     case (int) fieldId3:
491                         pi.readBytes(fieldId3);
492                         // don't fail, length delimited is ok
493                         break;
494                     case (int) fieldId6:
495                         pi.readBytes(fieldId6);
496                         fail("Should have thrown a WireTypeMismatchException");
497                         break;
498                     default:
499                         fail("Unexpected field id " + pi.getFieldNumber());
500                 }
501             } catch (WireTypeMismatchException wtme) {
502                 // good
503             }
504         }
505         stream.close();
506     }
507 }
508