• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2023 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.net.httpflags;
6 
7 import androidx.annotation.Nullable;
8 import androidx.annotation.VisibleForTesting;
9 
10 import com.google.protobuf.ByteString;
11 
12 import java.util.Collections;
13 import java.util.HashMap;
14 import java.util.Map;
15 import java.util.StringTokenizer;
16 
17 /**
18  * Holds the effective HTTP flags that apply to a given instance of the Cronet library.
19  *
20  * <p>Cronet business logic code is expected to use this class to enquire about the HTTP flag values
21  * that it should use.
22  */
23 public final class ResolvedFlags {
24     /**
25      * Provides type-safe access to the value of a given HTTP flag.
26      *
27      * <p>This object can never hold a null flag value.
28      */
29     public static final class Value {
30         public static enum Type {
31             BOOL,
32             INT,
33             FLOAT,
34             STRING,
35             BYTES
36         }
37 
38         private final Object mValue;
39 
40         @Nullable
resolve(FlagValue flagValue, String appId, int[] cronetVersion)41         private static Value resolve(FlagValue flagValue, String appId, int[] cronetVersion) {
42             for (var constrainedValue : flagValue.getConstrainedValuesList()) {
43                 if ((constrainedValue.hasAppId() && !constrainedValue.getAppId().equals(appId))
44                         || (constrainedValue.hasMinVersion()
45                                 && !matchesVersion(
46                                         cronetVersion,
47                                         parseVersionString(constrainedValue.getMinVersion())))) {
48                     continue;
49                 }
50                 return fromConstrainedValue(constrainedValue);
51             }
52             return null;
53         }
54 
matchesVersion(int[] cronetVersion, int[] minVersion)55         private static boolean matchesVersion(int[] cronetVersion, int[] minVersion) {
56             for (int i = 0; i < Math.max(cronetVersion.length, minVersion.length); i++) {
57                 int cronetComponent = i < cronetVersion.length ? cronetVersion[i] : 0;
58                 int minComponent = i < minVersion.length ? minVersion[i] : 0;
59                 if (cronetComponent > minComponent) {
60                     return true;
61                 } else if (cronetComponent < minComponent) {
62                     return false;
63                 }
64             }
65             return true;
66         }
67 
68         private static Value fromConstrainedValue(FlagValue.ConstrainedValue constrainedValue) {
69             FlagValue.ConstrainedValue.ValueCase valueCase = constrainedValue.getValueCase();
70             switch (valueCase) {
71                 case BOOL_VALUE:
72                     return new Value(constrainedValue.getBoolValue());
73                 case INT_VALUE:
74                     return new Value(constrainedValue.getIntValue());
75                 case FLOAT_VALUE:
76                     return new Value(constrainedValue.getFloatValue());
77                 case STRING_VALUE:
78                     return new Value(constrainedValue.getStringValue());
79                 case BYTES_VALUE:
80                     return new Value(constrainedValue.getBytesValue());
81                 case VALUE_NOT_SET:
82                     return null;
83                 default:
84                     throw new IllegalArgumentException(
85                             "Flag value uses unknown value type " + valueCase);
86             }
87         }
88 
89         @VisibleForTesting
90         public Value(boolean value) {
91             mValue = value;
92         }
93 
94         @VisibleForTesting
95         public Value(long value) {
96             mValue = value;
97         }
98 
99         @VisibleForTesting
100         public Value(float value) {
101             mValue = value;
102         }
103 
104         @VisibleForTesting
105         public Value(String value) {
106             mValue = value;
107         }
108 
109         @VisibleForTesting
110         public Value(ByteString value) {
111             mValue = value;
112         }
113 
114         public Type getType() {
115             if (mValue instanceof Boolean) {
116                 return Type.BOOL;
117             } else if (mValue instanceof Long) {
118                 return Type.INT;
119             } else if (mValue instanceof Float) {
120                 return Type.FLOAT;
121             } else if (mValue instanceof String) {
122                 return Type.STRING;
123             } else if (mValue instanceof ByteString) {
124                 return Type.BYTES;
125             } else {
126                 throw new IllegalStateException(
127                         "Unexpected flag value type: " + mValue.getClass().getName());
128             }
129         }
130 
131         private void checkType(Type requestedType) {
132             Type actualType = getType();
133             if (requestedType != actualType) {
134                 throw new IllegalStateException(
135                         "Attempted to access flag value as "
136                                 + requestedType
137                                 + ", but actual type is "
138                                 + actualType);
139             }
140         }
141 
142         /** @throws IllegalStateException Iff {@link #getType} is not {@link Type#BOOL} */
143         public boolean getBoolValue() {
144             checkType(Type.BOOL);
145             return (Boolean) mValue;
146         }
147 
148         /** @throws IllegalStateException Iff {@link #getType} is not {@link Type#INT} */
149         public long getIntValue() {
150             checkType(Type.INT);
151             return (Long) mValue;
152         }
153 
154         /** @throws IllegalStateException Iff {@link #getType} is not {@link Type#FLOAT} */
155         public float getFloatValue() {
156             checkType(Type.FLOAT);
157             return (Float) mValue;
158         }
159 
160         /** @throws IllegalStateException Iff {@link #getType} is not {@link Type#STRING} */
161         public String getStringValue() {
162             checkType(Type.STRING);
163             return (String) mValue;
164         }
165 
166         /** @throws IllegalStateException Iff {@link #getType} is not {@link Type#BYTES} */
167         public ByteString getBytesValue() {
168             checkType(Type.BYTES);
169             return (ByteString) mValue;
170         }
171     }
172 
173     private final Map<String, Value> mFlags;
174 
175     /**
176      * Computes effective flag values based on the contents of a {@link Flags} proto.
177      *
178      * <p>This method will resolve {@link FlagValue.ConstrainedValue} filters according to the
179      * other arguments, producing the final values that should apply to the caller.
180      *
181      * <p>Note that a {@link FlagValue} that has no {@link FlagValue.ConstrainedValue} entry, or
182      * where the matching entry has no value set, will not be mentioned at all in the resulting
183      * {@link #flags}.
184      *
185      * @param flags The {@link Flags} proto to extract the flag values from. This would normally be
186      *              the return value of {@link HttpFlagsLoader#load}.
187      * @param appId The App ID for resolving the {@link FlagValue.ConstrainedValue#getAppId} field.
188      *              This would normally be the return value of
189      *              {@link android.content.Context#getPackageName}.
190      * @param cronetVersion The version to use for filtering against the {@link
191      *                      FlagValue.ConstrainedValue#getMinVersion} field.
192      */
193     public static ResolvedFlags resolve(Flags flags, String appId, String cronetVersion) {
194         int[] parsedCronetVersion = parseVersionString(cronetVersion);
195         Map<String, Value> resolvedFlags = new HashMap<String, Value>();
196         for (var flag : flags.getFlagsMap().entrySet()) {
197             try {
198                 Value value = Value.resolve(flag.getValue(), appId, parsedCronetVersion);
199                 if (value == null) continue;
200                 resolvedFlags.put(flag.getKey(), value);
201             } catch (RuntimeException exception) {
202                 throw new IllegalArgumentException(
203                         "Unable to resolve HTTP flag `" + flag.getKey() + "`", exception);
204             }
205         }
206         return new ResolvedFlags(resolvedFlags);
207     }
208 
209     @VisibleForTesting
210     public ResolvedFlags(Map<String, Value> flags) {
211         mFlags = flags;
212     }
213 
214     /**
215      * @return The effective HTTP flag values, keyed by flag name. Neither keys nor values can be
216      * null. Only flags that have actual values are included in the result.
217      */
218     public Map<String, Value> flags() {
219         return Collections.unmodifiableMap(mFlags);
220     }
221 
222     private static int[] parseVersionString(String versionString) {
223         try {
224             if (versionString.isEmpty()) {
225                 throw new IllegalArgumentException("Version string is empty");
226             }
227             StringTokenizer tokenizer = new StringTokenizer(versionString, ".");
228             int[] components = new int[tokenizer.countTokens()];
229             for (int i = 0; i < components.length; i++) {
230                 components[i] = Integer.parseInt(tokenizer.nextToken());
231             }
232             return components;
233         } catch (RuntimeException exception) {
234             throw new IllegalArgumentException(
235                     "Unable to parse HTTP flags version string: `" + versionString + "`",
236                     exception);
237         }
238     }
239 }
240