1 /*
2  * Copyright (C) 2023 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.adservices.service.signals.updateprocessors;
18 
19 import org.json.JSONArray;
20 import org.json.JSONObject;
21 
22 import java.nio.ByteBuffer;
23 import java.nio.charset.StandardCharsets;
24 import java.util.Base64;
25 import java.util.Set;
26 
27 /** Collection of common utilities for implementers of the UpdateProcessor interface. */
28 public class UpdateProcessorUtils {
29 
30     private static final int KEY_SIZE_BYTES = 4;
31     private static final int VALUE_MAX_SIZE_BYTES = 100;
32 
33     /**
34      * Casts the given update object to a JSONArray throwing an appropriate error if the input is
35      * not a JSONArray.
36      *
37      * @param commandName The name of the command running this method (needed for constructing the
38      *     error message in the event of a failure).
39      * @param updates The JSONArray to be cast.
40      * @return The post-cast JSONArray.
41      */
castToJSONArray(String commandName, Object updates)42     public static JSONArray castToJSONArray(String commandName, Object updates) {
43         if (!(updates instanceof JSONArray)) {
44             throw new IllegalArgumentException(
45                     String.format("Value for \"%s\" must be a JSON array", commandName));
46         }
47         return (JSONArray) updates;
48     }
49 
50     /**
51      * Casts the given update object to a JSONObject throwing an appropriate error if the input is
52      * not a JSONObject.
53      *
54      * @param commandName The name of the command running this method (needed for constructing the
55      *     error message in the event of a failure).
56      * @param updates The JSONObject to be cast.
57      * @return The post-cast JSONObject.
58      */
castToJSONObject(String commandName, Object updates)59     public static JSONObject castToJSONObject(String commandName, Object updates) {
60         if (!(updates instanceof JSONObject)) {
61             throw new IllegalArgumentException(
62                     String.format("Value for \"%s\" must be a JSON object", commandName));
63         }
64         return (JSONObject) updates;
65     }
66 
67     /**
68      * Adds a key to the set of touched keys, throwing an exception if the key is already in the
69      * set.
70      *
71      * @param key The key to add.
72      * @param keysTouched The set of keys already touched.
73      */
touchKey(ByteBuffer key, Set<ByteBuffer> keysTouched)74     public static void touchKey(ByteBuffer key, Set<ByteBuffer> keysTouched) {
75         if (!keysTouched.add(key)) {
76             throw new IllegalArgumentException("Keys must only appear once per update JSON");
77         }
78     }
79 
80     /**
81      * Converts a key from base 64 to a ByteBuffer wrapping a size 4 byte array. Throws an error if
82      * the input is not valid base 64 or does not fit in 4 bytes.
83      *
84      * @param commandName The name of the command calling this method (needed for constructing the
85      *     error message in the event of a failure).
86      * @param key The base 64 key to convert.
87      * @return A byte buffer of the decoded bytes.
88      */
decodeKey(String commandName, String key)89     public static ByteBuffer decodeKey(String commandName, String key) {
90         byte[] toReturn = new byte[4];
91         try {
92             Base64.getDecoder().decode(key.getBytes(StandardCharsets.ISO_8859_1), toReturn);
93         } catch (IllegalArgumentException e) {
94             throw new IllegalArgumentException(
95                     String.format(
96                             "Keys in \"%s\" must be valid base 64 and under %d bytes.",
97                             commandName, KEY_SIZE_BYTES));
98         }
99         return ByteBuffer.wrap(toReturn);
100     }
101 
102     /**
103      * Converts a value from a base 64 string to a byte array. Throws an error if the input is not
104      * valid base 64 or does not fit in VALUE_MAX_SIZE_BYTES bytes.
105      *
106      * @param commandName The name of the command calling this method (needed for constructing the
107      *     error message in the event of a failure).
108      * @param value The base 64 value to convert.
109      * @return A byte array of the decoded bytes.
110      */
decodeValue(String commandName, String value)111     public static byte[] decodeValue(String commandName, String value) {
112         byte[] toReturn;
113         try {
114             toReturn = Base64.getDecoder().decode(value);
115         } catch (IllegalArgumentException e) {
116             throw new IllegalArgumentException(
117                     String.format("Values in \"%s\" must be valid base 64", commandName));
118         }
119         if (toReturn.length > VALUE_MAX_SIZE_BYTES) {
120             throw new IllegalArgumentException(
121                     String.format(
122                             "Values in \"%s\" must be under %d bytes",
123                             commandName, VALUE_MAX_SIZE_BYTES));
124         }
125         return toReturn;
126     }
127 
128     /**
129      * Takes a byte buffer and returns the underlying array, while making sure the ByteBuffer is
130      * properly wrapping an array with no offset
131      *
132      * @param buffer The buffer the convert.
133      * @return The underlying byte[].
134      */
getByteArrayFromBuffer(ByteBuffer buffer)135     public static byte[] getByteArrayFromBuffer(ByteBuffer buffer) {
136         if (buffer.arrayOffset() != 0) {
137             throw new IllegalStateException("Improperly created ByteBuffer");
138         }
139         return buffer.array();
140     }
141 }
142