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