blob: d7219055d997ed50156452a57c5956b41fc63700 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.net.httpflags;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.protobuf.ByteString;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/** Utility class for bridging the gap between HTTP flags and the native `base::Feature` framework. */
public final class BaseFeature {
/** HTTP flags that start with this name will be turned into base::Feature overrides. */
@VisibleForTesting public static final String FLAG_PREFIX = "ChromiumBaseFeature_";
/**
* If this delimiter is found in an HTTP flag name, the HTTP flag is assumed to refer to a
* base::Feature param. The part before the delimiter is the base::Feature name, and the part
* after the delimiter is the param name.
*/
@VisibleForTesting public static final String PARAM_DELIMITER = "_PARAM_";
private BaseFeature() {}
/**
* Turns a set of resolved HTTP flags into native {@code base::Feature} overrides.
*
* <p>Only HTTP flags whose name start with {@link #FLAG_PREFIX} are considered.
*
* <p>If the flag name does not include {@link #PARAM_DELIMITER}, then the flag is treated as
* a state override for a base::Feature named after the HTTP flag (without the
* {@link #FLAG_PREFIX} prefix). In that case the flag value is required to be a boolean. The
* state is overridden to the "enabled" state if the flag value is true, or to the "disabled"
* state if the flag value is false.
*
* <p>If the flag name does include {@link #PARAM_DELIMITER}, then the flag is treated as a
* base::Feature param override. In that case the part after {@link #FLAG_PREFIX} but before
* {@link #PARAM_DELIMITER} is the name of the base::Feature, and the part after {@link
* #PARAM_DELIMITER} is the name of the param. The param value is the flag value, converted to
* string in such a way as to allow base::FeatureParam code to unparse it.
*
* <p>Examples:
* <ul>
* <li>An HTTP flag named {@code ChromiumBaseFeature_LogMe} with value {@code true} enables the
* {@code LogMe} base::Feature.
* <li>An HTTP flag named {@code ChromiumBaseFeature_LogMe_PARAM_marker} with value {@code
* "foobar"} sets the {@code marker} param on the {@code LogMe} base::Feature to {@code
* "foobar"}.
* </ul>
*
* @throws IllegalArgumentException if the flags are invalid or otherwise can't be parsed
*
* @see org.chromium.net.impl.CronetLibraryLoader#getBaseFeatureOverrides
*/
public static BaseFeatureOverrides getOverrides(ResolvedFlags flags) {
Map<String, BaseFeatureOverrides.FeatureState.Builder> featureStateBuilders =
new HashMap<String, BaseFeatureOverrides.FeatureState.Builder>();
for (Map.Entry<String, ResolvedFlags.Value> flag : flags.flags().entrySet()) {
try {
applyOverride(flag.getKey(), flag.getValue(), featureStateBuilders);
} catch (RuntimeException exception) {
throw new IllegalArgumentException(
"Could not parse HTTP flag `"
+ flag.getKey()
+ "` as a base::Feature override",
exception);
}
}
BaseFeatureOverrides.Builder builder = BaseFeatureOverrides.newBuilder();
for (Map.Entry<String, BaseFeatureOverrides.FeatureState.Builder> featureStateBuilder :
featureStateBuilders.entrySet()) {
builder.putFeatureStates(
featureStateBuilder.getKey(), featureStateBuilder.getValue().build());
}
return builder.build();
}
private static void applyOverride(
String flagName,
ResolvedFlags.Value flagValue,
Map<String, BaseFeatureOverrides.FeatureState.Builder> featureStateBuilders) {
ParsedFlagName parsedFlagName = parseFlagName(flagName);
if (parsedFlagName == null) return;
BaseFeatureOverrides.FeatureState.Builder featureStateBuilder =
featureStateBuilders.get(parsedFlagName.featureName);
if (featureStateBuilder == null) {
featureStateBuilder = BaseFeatureOverrides.FeatureState.newBuilder();
featureStateBuilders.put(parsedFlagName.featureName, featureStateBuilder);
}
if (parsedFlagName.paramName == null) {
applyStateOverride(flagValue, featureStateBuilder);
} else {
applyParamOverride(parsedFlagName.paramName, flagValue, featureStateBuilder);
}
}
private static final class ParsedFlagName {
public String featureName;
@Nullable public String paramName;
}
@Nullable
private static ParsedFlagName parseFlagName(String flagName) {
if (!flagName.startsWith(FLAG_PREFIX)) return null;
String flagNameWithoutPrefix = flagName.substring(FLAG_PREFIX.length());
ParsedFlagName parsed = new ParsedFlagName();
int delimiterIndex = flagNameWithoutPrefix.indexOf(PARAM_DELIMITER);
if (delimiterIndex < 0) {
parsed.featureName = flagNameWithoutPrefix;
} else {
parsed.featureName = flagNameWithoutPrefix.substring(0, delimiterIndex);
parsed.paramName =
flagNameWithoutPrefix.substring(delimiterIndex + PARAM_DELIMITER.length());
}
return parsed;
}
private static void applyStateOverride(
ResolvedFlags.Value value,
BaseFeatureOverrides.FeatureState.Builder featureStateBuilder) {
ResolvedFlags.Value.Type valueType = value.getType();
if (valueType != ResolvedFlags.Value.Type.BOOL) {
throw new IllegalArgumentException(
"HTTP flag has type "
+ valueType
+ ", but only boolean flags are supported as base::Feature overrides");
}
featureStateBuilder.setEnabled(value.getBoolValue());
}
private static void applyParamOverride(
String paramName,
ResolvedFlags.Value value,
BaseFeatureOverrides.FeatureState.Builder featureStateBuilder) {
ResolvedFlags.Value.Type valueType = value.getType();
ByteString rawValue;
switch (valueType) {
case BOOL:
rawValue =
ByteString.copyFrom(
value.getBoolValue() ? "true" : "false", StandardCharsets.UTF_8);
break;
case INT:
rawValue =
ByteString.copyFrom(
Long.toString(value.getIntValue(), /* radix= */ 10),
StandardCharsets.UTF_8);
break;
case FLOAT:
// TODO: if the value is "weird" (e.g. NaN, infinities) this probably won't produce
// something that the Chromium feature param code can parse. As a workaround, the
// user can use a string-valued flag to directly feed the value to be parsed.
rawValue =
ByteString.copyFrom(
Float.toString(value.getFloatValue()), StandardCharsets.UTF_8);
break;
case STRING:
rawValue = ByteString.copyFrom(value.getStringValue(), StandardCharsets.UTF_8);
break;
case BYTES:
rawValue = value.getBytesValue();
break;
default:
throw new UnsupportedOperationException(
"Unsupported HTTP flag value type for base::Feature param `"
+ paramName
+ "`: "
+ valueType);
}
featureStateBuilder.putParams(paramName, rawValue);
}
}