• 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.nio.charset.StandardCharsets;
13 import java.util.HashMap;
14 import java.util.Map;
15 
16 /** Utility class for bridging the gap between HTTP flags and the native `base::Feature` framework. */
17 public final class BaseFeature {
18     /** HTTP flags that start with this name will be turned into base::Feature overrides. */
19     @VisibleForTesting public static final String FLAG_PREFIX = "ChromiumBaseFeature_";
20 
21     /**
22      * If this delimiter is found in an HTTP flag name, the HTTP flag is assumed to refer to a
23      * base::Feature param. The part before the delimiter is the base::Feature name, and the part
24      * after the delimiter is the param name.
25      */
26     @VisibleForTesting public static final String PARAM_DELIMITER = "_PARAM_";
27 
BaseFeature()28     private BaseFeature() {}
29 
30     /**
31      * Turns a set of resolved HTTP flags into native {@code base::Feature} overrides.
32      *
33      * <p>Only HTTP flags whose name start with {@link #FLAG_PREFIX} are considered.
34      *
35      * <p>If the flag name does not include {@link #PARAM_DELIMITER}, then the flag is treated as
36      * a state override for a base::Feature named after the HTTP flag (without the
37      * {@link #FLAG_PREFIX} prefix). In that case the flag value is required to be a boolean. The
38      * state is overridden to the "enabled" state if the flag value is true, or to the "disabled"
39      * state if the flag value is false.
40      *
41      * <p>If the flag name does include {@link #PARAM_DELIMITER}, then the flag is treated as a
42      * base::Feature param override. In that case the part after {@link #FLAG_PREFIX} but before
43      * {@link #PARAM_DELIMITER} is the name of the base::Feature, and the part after {@link
44      * #PARAM_DELIMITER} is the name of the param. The param value is the flag value, converted to
45      * string in such a way as to allow base::FeatureParam code to unparse it.
46      *
47      * <p>Examples:
48      * <ul>
49      * <li>An HTTP flag named {@code ChromiumBaseFeature_LogMe} with value {@code true} enables the
50      * {@code LogMe} base::Feature.
51      * <li>An HTTP flag named {@code ChromiumBaseFeature_LogMe_PARAM_marker} with value {@code
52      * "foobar"} sets the {@code marker} param on the {@code LogMe} base::Feature to {@code
53      * "foobar"}.
54      * </ul>
55      *
56      * @throws IllegalArgumentException if the flags are invalid or otherwise can't be parsed
57      *
58      * @see org.chromium.net.impl.CronetLibraryLoader#getBaseFeatureOverrides
59      */
getOverrides(ResolvedFlags flags)60     public static BaseFeatureOverrides getOverrides(ResolvedFlags flags) {
61         Map<String, BaseFeatureOverrides.FeatureState.Builder> featureStateBuilders =
62                 new HashMap<String, BaseFeatureOverrides.FeatureState.Builder>();
63 
64         for (Map.Entry<String, ResolvedFlags.Value> flag : flags.flags().entrySet()) {
65             try {
66                 applyOverride(flag.getKey(), flag.getValue(), featureStateBuilders);
67             } catch (RuntimeException exception) {
68                 throw new IllegalArgumentException(
69                         "Could not parse HTTP flag `"
70                                 + flag.getKey()
71                                 + "` as a base::Feature override",
72                         exception);
73             }
74         }
75 
76         BaseFeatureOverrides.Builder builder = BaseFeatureOverrides.newBuilder();
77         for (Map.Entry<String, BaseFeatureOverrides.FeatureState.Builder> featureStateBuilder :
78                 featureStateBuilders.entrySet()) {
79             builder.putFeatureStates(
80                     featureStateBuilder.getKey(), featureStateBuilder.getValue().build());
81         }
82         return builder.build();
83     }
84 
applyOverride( String flagName, ResolvedFlags.Value flagValue, Map<String, BaseFeatureOverrides.FeatureState.Builder> featureStateBuilders)85     private static void applyOverride(
86             String flagName,
87             ResolvedFlags.Value flagValue,
88             Map<String, BaseFeatureOverrides.FeatureState.Builder> featureStateBuilders) {
89         ParsedFlagName parsedFlagName = parseFlagName(flagName);
90         if (parsedFlagName == null) return;
91 
92         BaseFeatureOverrides.FeatureState.Builder featureStateBuilder =
93                 featureStateBuilders.get(parsedFlagName.featureName);
94         if (featureStateBuilder == null) {
95             featureStateBuilder = BaseFeatureOverrides.FeatureState.newBuilder();
96             featureStateBuilders.put(parsedFlagName.featureName, featureStateBuilder);
97         }
98 
99         if (parsedFlagName.paramName == null) {
100             applyStateOverride(flagValue, featureStateBuilder);
101         } else {
102             applyParamOverride(parsedFlagName.paramName, flagValue, featureStateBuilder);
103         }
104     }
105 
106     private static final class ParsedFlagName {
107         public String featureName;
108         @Nullable public String paramName;
109     }
110 
111     @Nullable
parseFlagName(String flagName)112     private static ParsedFlagName parseFlagName(String flagName) {
113         if (!flagName.startsWith(FLAG_PREFIX)) return null;
114         String flagNameWithoutPrefix = flagName.substring(FLAG_PREFIX.length());
115 
116         ParsedFlagName parsed = new ParsedFlagName();
117 
118         int delimiterIndex = flagNameWithoutPrefix.indexOf(PARAM_DELIMITER);
119         if (delimiterIndex < 0) {
120             parsed.featureName = flagNameWithoutPrefix;
121         } else {
122             parsed.featureName = flagNameWithoutPrefix.substring(0, delimiterIndex);
123             parsed.paramName =
124                     flagNameWithoutPrefix.substring(delimiterIndex + PARAM_DELIMITER.length());
125         }
126         return parsed;
127     }
128 
applyStateOverride( ResolvedFlags.Value value, BaseFeatureOverrides.FeatureState.Builder featureStateBuilder)129     private static void applyStateOverride(
130             ResolvedFlags.Value value,
131             BaseFeatureOverrides.FeatureState.Builder featureStateBuilder) {
132         ResolvedFlags.Value.Type valueType = value.getType();
133         if (valueType != ResolvedFlags.Value.Type.BOOL) {
134             throw new IllegalArgumentException(
135                     "HTTP flag has type "
136                             + valueType
137                             + ", but only boolean flags are supported as base::Feature overrides");
138         }
139         featureStateBuilder.setEnabled(value.getBoolValue());
140     }
141 
applyParamOverride( String paramName, ResolvedFlags.Value value, BaseFeatureOverrides.FeatureState.Builder featureStateBuilder)142     private static void applyParamOverride(
143             String paramName,
144             ResolvedFlags.Value value,
145             BaseFeatureOverrides.FeatureState.Builder featureStateBuilder) {
146         ResolvedFlags.Value.Type valueType = value.getType();
147         ByteString rawValue;
148         switch (valueType) {
149             case BOOL:
150                 rawValue =
151                         ByteString.copyFrom(
152                                 value.getBoolValue() ? "true" : "false", StandardCharsets.UTF_8);
153                 break;
154             case INT:
155                 rawValue =
156                         ByteString.copyFrom(
157                                 Long.toString(value.getIntValue(), /* radix= */ 10),
158                                 StandardCharsets.UTF_8);
159                 break;
160             case FLOAT:
161                 // TODO: if the value is "weird" (e.g. NaN, infinities) this probably won't produce
162                 // something that the Chromium feature param code can parse. As a workaround, the
163                 // user can use a string-valued flag to directly feed the value to be parsed.
164                 rawValue =
165                         ByteString.copyFrom(
166                                 Float.toString(value.getFloatValue()), StandardCharsets.UTF_8);
167                 break;
168             case STRING:
169                 rawValue = ByteString.copyFrom(value.getStringValue(), StandardCharsets.UTF_8);
170                 break;
171             case BYTES:
172                 rawValue = value.getBytesValue();
173                 break;
174             default:
175                 throw new UnsupportedOperationException(
176                         "Unsupported HTTP flag value type for base::Feature param `"
177                                 + paramName
178                                 + "`: "
179                                 + valueType);
180         }
181         featureStateBuilder.putParams(paramName, rawValue);
182     }
183 }
184