1 /*
2  * Copyright (C) 2019 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 com.android.test.protoinputstream.nano.Test;
24 
25 import com.google.protobuf.nano.MessageNano;
26 
27 import junit.framework.TestCase;
28 
29 import java.io.ByteArrayInputStream;
30 import java.io.IOException;
31 import java.io.InputStream;
32 
33 public class ProtoInputStreamEnumTest extends TestCase {
34 
testRead()35     public void testRead() throws IOException {
36         testRead(0);
37         testRead(1);
38         testRead(5);
39     }
40 
testRead(int chunkSize)41     private void testRead(int chunkSize) throws IOException {
42         final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_ENUM;
43 
44         final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL);
45         final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL);
46         final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL);
47         final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL);
48         final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL);
49         final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL);
50 
51         final byte[] protobuf = new byte[]{
52                 // 1 -> 0 - default value, not written
53                 // 2 -> 1
54                 (byte) 0x10,
55                 (byte) 0x01,
56                 // 6 -> MAX_VALUE
57                 (byte) 0x30,
58                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07,
59                 // 3 -> -1
60                 (byte) 0x18,
61                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
62                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01,
63                 // 4 -> MIN_VALUE
64                 (byte) 0x20,
65                 (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8,
66                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01,
67                 // 5 -> MAX_VALUE
68                 (byte) 0x28,
69                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07,
70         };
71 
72         InputStream stream = new ByteArrayInputStream(protobuf);
73         final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize);
74         int[] results = new int[5];
75         while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
76             switch (pi.getFieldNumber()) {
77                 case (int) fieldId1:
78                     fail("Should never reach this");
79                     break;
80                 case (int) fieldId2:
81                     results[1] = pi.readInt(fieldId2);
82                     break;
83                 case (int) fieldId3:
84                     results[2] = pi.readInt(fieldId3);
85                     break;
86                 case (int) fieldId4:
87                     results[3] = pi.readInt(fieldId4);
88                     break;
89                 case (int) fieldId5:
90                     results[4] = pi.readInt(fieldId5);
91                     break;
92                 case (int) fieldId6:
93                     // Intentionally don't read the data. Parse should continue normally
94                     break;
95                 default:
96                     fail("Unexpected field id " + pi.getFieldNumber());
97             }
98         }
99         stream.close();
100 
101         assertEquals(0, results[0]);
102         assertEquals(1, results[1]);
103         assertEquals(-1, results[2]);
104         assertEquals(Integer.MIN_VALUE, results[3]);
105         assertEquals(Integer.MAX_VALUE, results[4]);
106     }
107 
108     /**
109      * Test that reading with ProtoInputStream matches, and can read the output of standard proto.
110      */
testReadCompat()111     public void testReadCompat() throws Exception {
112         testReadCompat(0);
113         testReadCompat(1);
114         testReadCompat(-1);
115         testReadCompat(Integer.MIN_VALUE);
116         testReadCompat(Integer.MAX_VALUE);
117     }
118 
119     /**
120      * Implementation of testReadCompat with a given value.
121      */
testReadCompat(int val)122     private void testReadCompat(int val) throws Exception {
123         final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_ENUM;
124         final long fieldId = fieldFlags | ((long) 160 & 0x0ffffffffL);
125 
126         final Test.All all = new Test.All();
127         all.outsideField = val;
128 
129         final byte[] proto = MessageNano.toByteArray(all);
130 
131         final ProtoInputStream pi = new ProtoInputStream(proto);
132 
133         int result = 0; // start off with default value
134         while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
135             switch (pi.getFieldNumber()) {
136                 case (int) fieldId:
137                     result = pi.readInt(fieldId);
138                     break;
139                 default:
140                     fail("Unexpected field id " + pi.getFieldNumber());
141             }
142         }
143 
144         // Nano proto drops values that are outside the range, so compare against val
145         assertEquals(val, result);
146     }
147 
testRepeated()148     public void testRepeated() throws IOException {
149         testRepeated(0);
150         testRepeated(1);
151         testRepeated(5);
152     }
153 
testRepeated(int chunkSize)154     private void testRepeated(int chunkSize) throws IOException {
155         final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_ENUM;
156 
157         final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL);
158         final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL);
159         final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL);
160         final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL);
161         final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL);
162         final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL);
163 
164         final byte[] protobuf = new byte[]{
165                 // 1 -> 0 - default value, written when repeated
166                 (byte) 0x08,
167                 (byte) 0x00,
168                 // 2 -> 1
169                 (byte) 0x10,
170                 (byte) 0x01,
171                 // 3 -> -1
172                 (byte) 0x18,
173                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
174                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01,
175                 // 4 -> MIN_VALUE
176                 (byte) 0x20,
177                 (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8,
178                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01,
179                 // 5 -> MAX_VALUE
180                 (byte) 0x28,
181                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07,
182 
183                 // 6 -> MAX_VALUE
184                 (byte) 0x30,
185                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07,
186 
187                 // 1 -> 0 - default value, written when repeated
188                 (byte) 0x08,
189                 (byte) 0x00,
190                 // 2 -> 1
191                 (byte) 0x10,
192                 (byte) 0x01,
193                 // 3 -> -1
194                 (byte) 0x18,
195                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
196                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01,
197                 // 4 -> MIN_VALUE
198                 (byte) 0x20,
199                 (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8,
200                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01,
201                 // 5 -> MAX_VALUE
202                 (byte) 0x28,
203                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07,
204         };
205 
206         InputStream stream = new ByteArrayInputStream(protobuf);
207         final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize);
208         int[][] results = new int[5][2];
209         int[] indices = new int[5];
210         while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
211 
212             switch (pi.getFieldNumber()) {
213                 case (int) fieldId1:
214                     results[0][indices[0]++] = pi.readInt(fieldId1);
215                     break;
216                 case (int) fieldId2:
217                     results[1][indices[1]++] = pi.readInt(fieldId2);
218                     break;
219                 case (int) fieldId3:
220                     results[2][indices[2]++] = pi.readInt(fieldId3);
221                     break;
222                 case (int) fieldId4:
223                     results[3][indices[3]++] = pi.readInt(fieldId4);
224                     break;
225                 case (int) fieldId5:
226                     results[4][indices[4]++] = pi.readInt(fieldId5);
227                     break;
228                 case (int) fieldId6:
229                     // Intentionally don't read the data. Parse should continue normally
230                     break;
231                 default:
232                     fail("Unexpected field id " + pi.getFieldNumber());
233             }
234         }
235         stream.close();
236 
237 
238         assertEquals(0, results[0][0]);
239         assertEquals(0, results[0][1]);
240         assertEquals(1, results[1][0]);
241         assertEquals(1, results[1][1]);
242         assertEquals(-1, results[2][0]);
243         assertEquals(-1, results[2][1]);
244         assertEquals(Integer.MIN_VALUE, results[3][0]);
245         assertEquals(Integer.MIN_VALUE, results[3][1]);
246         assertEquals(Integer.MAX_VALUE, results[4][0]);
247         assertEquals(Integer.MAX_VALUE, results[4][1]);
248     }
249 
250 
251     /**
252      * Test that reading with ProtoInputStream matches, and can read the output of standard proto.
253      */
testRepeatedCompat()254     public void testRepeatedCompat() throws Exception {
255         testRepeatedCompat(new int[]{});
256         testRepeatedCompat(new int[]{0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE});
257     }
258 
259     /**
260      * Implementation of testRepeatedCompat with a given value.
261      */
testRepeatedCompat(int[] val)262     private void testRepeatedCompat(int[] val) throws Exception {
263         final long fieldFlags = ProtoStream.FIELD_COUNT_REPEATED | ProtoStream.FIELD_TYPE_ENUM;
264         final long fieldId = fieldFlags | ((long) 161 & 0x0ffffffffL);
265 
266         final Test.All all = new Test.All();
267         all.outsideFieldRepeated = val;
268 
269         final byte[] proto = MessageNano.toByteArray(all);
270 
271         final ProtoInputStream pi = new ProtoInputStream(proto);
272 
273         int[] result = new int[val.length];
274         int index = 0;
275         while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
276             switch (pi.getFieldNumber()) {
277                 case (int) fieldId:
278                     result[index++] = pi.readInt(fieldId);
279                     break;
280                 default:
281                     fail("Unexpected field id " + pi.getFieldNumber());
282             }
283         }
284 
285         // Nano proto drops values that are outside the range, so compare against val
286         assertEquals(val.length, result.length);
287         for (int i = 0; i < result.length; i++) {
288             assertEquals(val[i], result[i]);
289         }
290     }
291 
testPacked()292     public void testPacked() throws IOException {
293         testPacked(0);
294         testPacked(1);
295         testPacked(5);
296     }
297 
testPacked(int chunkSize)298     private void testPacked(int chunkSize) throws IOException {
299         final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_ENUM;
300 
301         final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL);
302         final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL);
303         final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL);
304         final long fieldId4 = fieldFlags | ((long) 4 & 0x0ffffffffL);
305         final long fieldId5 = fieldFlags | ((long) 5 & 0x0ffffffffL);
306         final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL);
307 
308         final byte[] protobuf = new byte[]{
309                 // 1 -> 0 - default value, written when repeated
310                 (byte) 0x0a,
311                 (byte) 0x02,
312                 (byte) 0x00,
313                 (byte) 0x00,
314                 // 2 -> 1
315                 (byte) 0x12,
316                 (byte) 0x02,
317                 (byte) 0x01,
318                 (byte) 0x01,
319 
320                 // 6 -> MAX_VALUE
321                 (byte) 0x32,
322                 (byte) 0x0a,
323                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07,
324 
325                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07,
326 
327                 // 3 -> -1
328                 (byte) 0x1a,
329                 (byte) 0x14,
330                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
331                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01,
332 
333                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
334                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01,
335 
336                 // 4 -> MIN_VALUE
337                 (byte) 0x22,
338                 (byte) 0x14,
339                 (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8,
340                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01,
341 
342                 (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0xf8,
343                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01,
344 
345                 // 5 -> MAX_VALUE
346                 (byte) 0x2a,
347                 (byte) 0x0a,
348                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07,
349 
350                 (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x07,
351         };
352 
353         InputStream stream = new ByteArrayInputStream(protobuf);
354         final ProtoInputStream pi = new ProtoInputStream(stream, chunkSize);
355         int[][] results = new int[5][2];
356         int[] indices = new int[5];
357         while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
358 
359             switch (pi.getFieldNumber()) {
360                 case (int) fieldId1:
361                     results[0][indices[0]++] = pi.readInt(fieldId1);
362                     break;
363                 case (int) fieldId2:
364                     results[1][indices[1]++] = pi.readInt(fieldId2);
365                     break;
366                 case (int) fieldId3:
367                     results[2][indices[2]++] = pi.readInt(fieldId3);
368                     break;
369                 case (int) fieldId4:
370                     results[3][indices[3]++] = pi.readInt(fieldId4);
371                     break;
372                 case (int) fieldId5:
373                     results[4][indices[4]++] = pi.readInt(fieldId5);
374                     break;
375                 case (int) fieldId6:
376                     // Intentionally don't read the data. Parse should continue normally
377                     break;
378                 default:
379                     fail("Unexpected field id " + pi.getFieldNumber());
380             }
381         }
382         stream.close();
383 
384 
385         assertEquals(0, results[0][0]);
386         assertEquals(0, results[0][1]);
387         assertEquals(1, results[1][0]);
388         assertEquals(1, results[1][1]);
389         assertEquals(-1, results[2][0]);
390         assertEquals(-1, results[2][1]);
391         assertEquals(Integer.MIN_VALUE, results[3][0]);
392         assertEquals(Integer.MIN_VALUE, results[3][1]);
393         assertEquals(Integer.MAX_VALUE, results[4][0]);
394         assertEquals(Integer.MAX_VALUE, results[4][1]);
395     }
396 
397     /**
398      * Test that reading with ProtoInputStream matches, and can read the output of standard proto.
399      */
testPackedCompat()400     public void testPackedCompat() throws Exception {
401         testPackedCompat(new int[]{});
402         testPackedCompat(new int[]{0, 1});
403 
404         // Nano proto has a bug.  It gets the size with computeInt32SizeNoTag (correctly)
405         // but incorrectly uses writeRawVarint32 to write the value for negative numbers.
406         //testPackedCompat(new int[] { 0, 1, -1, Integer.MIN_VALUE, Integer.MAX_VALUE });
407     }
408 
409     /**
410      * Implementation of testRepeatedCompat with a given value.
411      */
testPackedCompat(int[] val)412     private void testPackedCompat(int[] val) throws Exception {
413         final long fieldFlags = ProtoStream.FIELD_COUNT_PACKED | ProtoStream.FIELD_TYPE_ENUM;
414         final long fieldId = fieldFlags | ((long) 162 & 0x0ffffffffL);
415 
416         final Test.All all = new Test.All();
417         all.outsideFieldPacked = val;
418 
419         final byte[] proto = MessageNano.toByteArray(all);
420 
421         final ProtoInputStream pi = new ProtoInputStream(proto);
422 
423         int[] result = new int[val.length];
424         int index = 0;
425         while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
426             switch (pi.getFieldNumber()) {
427                 case (int) fieldId:
428                     result[index++] = pi.readInt(fieldId);
429                     break;
430                 default:
431                     fail("Unexpected field id " + pi.getFieldNumber());
432             }
433         }
434 
435         // Nano proto drops values that are outside the range, so compare against val
436         assertEquals(val.length, result.length);
437         for (int i = 0; i < result.length; i++) {
438             assertEquals(val[i], result[i]);
439         }
440     }
441 
442     /**
443      * Test that using the wrong read method throws an exception
444      */
testBadReadType()445     public void testBadReadType() throws IOException {
446         final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_ENUM;
447 
448         final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL);
449 
450         final byte[] protobuf = new byte[]{
451                 // 1 -> 1
452                 (byte) 0x08,
453                 (byte) 0x01,
454         };
455 
456         ProtoInputStream pi = new ProtoInputStream(protobuf);
457         pi.nextField();
458         try {
459             pi.readFloat(fieldId1);
460             fail("Should have thrown IllegalArgumentException");
461         } catch (IllegalArgumentException iae) {
462             // good
463         }
464 
465         pi = new ProtoInputStream(protobuf);
466         pi.nextField();
467         try {
468             pi.readDouble(fieldId1);
469             fail("Should have thrown IllegalArgumentException");
470         } catch (IllegalArgumentException iae) {
471             // good
472         }
473 
474         pi = new ProtoInputStream(protobuf);
475         pi.nextField();
476         try {
477             pi.readBoolean(fieldId1);
478             fail("Should have thrown IllegalArgumentException");
479         } catch (IllegalArgumentException iae) {
480             // good
481         }
482 
483         pi = new ProtoInputStream(protobuf);
484         pi.nextField();
485         try {
486             pi.readLong(fieldId1);
487             fail("Should have thrown IllegalArgumentException");
488         } catch (IllegalArgumentException iae) {
489             // good
490         }
491 
492         pi = new ProtoInputStream(protobuf);
493         pi.nextField();
494         try {
495             pi.readBytes(fieldId1);
496             fail("Should have thrown IllegalArgumentException");
497         } catch (IllegalArgumentException iae) {
498             // good
499         }
500 
501         pi = new ProtoInputStream(protobuf);
502         pi.nextField();
503         try {
504             pi.readString(fieldId1);
505             fail("Should have thrown IllegalArgumentException");
506         } catch (IllegalArgumentException iae) {
507             // good
508         }
509     }
510 
511     /**
512      * Test that unexpected wrong wire types will throw an exception
513      */
testBadWireType()514     public void testBadWireType() throws IOException {
515         final long fieldFlags = ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_ENUM;
516 
517         final long fieldId1 = fieldFlags | ((long) 1 & 0x0ffffffffL);
518         final long fieldId2 = fieldFlags | ((long) 2 & 0x0ffffffffL);
519         final long fieldId3 = fieldFlags | ((long) 3 & 0x0ffffffffL);
520         final long fieldId6 = fieldFlags | ((long) 6 & 0x0ffffffffL);
521 
522         final byte[] protobuf = new byte[]{
523                 // 1 : varint -> 1
524                 (byte) 0x08,
525                 (byte) 0x01,
526                 // 2 : fixed64 -> 0x1
527                 (byte) 0x11,
528                 (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00,
529                 (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
530                 // 3 : length delimited -> { 1 }
531                 (byte) 0x1a,
532                 (byte) 0x01,
533                 (byte) 0x01,
534                 // 6 : fixed32
535                 (byte) 0x35,
536                 (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00,
537         };
538 
539         InputStream stream = new ByteArrayInputStream(protobuf);
540         final ProtoInputStream pi = new ProtoInputStream(stream);
541 
542         while (pi.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
543             try {
544                 switch (pi.getFieldNumber()) {
545                     case (int) fieldId1:
546                         pi.readInt(fieldId1);
547                         // don't fail, varint is ok
548                         break;
549                     case (int) fieldId2:
550                         pi.readInt(fieldId2);
551                         fail("Should have thrown a WireTypeMismatchException");
552                         break;
553                     case (int) fieldId3:
554                         pi.readInt(fieldId3);
555                         // don't fail, length delimited is ok (represents packed enums)
556                         break;
557                     case (int) fieldId6:
558                         pi.readInt(fieldId6);
559                         fail("Should have thrown a WireTypeMismatchException");
560                         break;
561                     default:
562                         fail("Unexpected field id " + pi.getFieldNumber());
563                 }
564             } catch (WireTypeMismatchException wtme) {
565                 // good
566             }
567         }
568         stream.close();
569     }
570 }
571