blob: b389d3f385ad0c4d55ce81423b693c5027d7a820 [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.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
/**
* Holds the effective HTTP flags that apply to a given instance of the Cronet library.
*
* <p>Cronet business logic code is expected to use this class to enquire about the HTTP flag values
* that it should use.
*/
public final class ResolvedFlags {
/**
* Provides type-safe access to the value of a given HTTP flag.
*
* <p>This object can never hold a null flag value.
*/
public static final class Value {
public static enum Type {
BOOL,
INT,
FLOAT,
STRING,
BYTES
}
private final Object mValue;
@Nullable
private static Value resolve(FlagValue flagValue, String appId, int[] cronetVersion) {
for (var constrainedValue : flagValue.getConstrainedValuesList()) {
if ((constrainedValue.hasAppId() && !constrainedValue.getAppId().equals(appId))
|| (constrainedValue.hasMinVersion()
&& !matchesVersion(
cronetVersion,
parseVersionString(constrainedValue.getMinVersion())))) {
continue;
}
return fromConstrainedValue(constrainedValue);
}
return null;
}
private static boolean matchesVersion(int[] cronetVersion, int[] minVersion) {
for (int i = 0; i < Math.max(cronetVersion.length, minVersion.length); i++) {
int cronetComponent = i < cronetVersion.length ? cronetVersion[i] : 0;
int minComponent = i < minVersion.length ? minVersion[i] : 0;
if (cronetComponent > minComponent) {
return true;
} else if (cronetComponent < minComponent) {
return false;
}
}
return true;
}
private static Value fromConstrainedValue(FlagValue.ConstrainedValue constrainedValue) {
FlagValue.ConstrainedValue.ValueCase valueCase = constrainedValue.getValueCase();
switch (valueCase) {
case BOOL_VALUE:
return new Value(constrainedValue.getBoolValue());
case INT_VALUE:
return new Value(constrainedValue.getIntValue());
case FLOAT_VALUE:
return new Value(constrainedValue.getFloatValue());
case STRING_VALUE:
return new Value(constrainedValue.getStringValue());
case BYTES_VALUE:
return new Value(constrainedValue.getBytesValue());
case VALUE_NOT_SET:
return null;
default:
throw new IllegalArgumentException(
"Flag value uses unknown value type " + valueCase);
}
}
@VisibleForTesting
public Value(boolean value) {
mValue = value;
}
@VisibleForTesting
public Value(long value) {
mValue = value;
}
@VisibleForTesting
public Value(float value) {
mValue = value;
}
@VisibleForTesting
public Value(String value) {
mValue = value;
}
@VisibleForTesting
public Value(ByteString value) {
mValue = value;
}
public Type getType() {
if (mValue instanceof Boolean) {
return Type.BOOL;
} else if (mValue instanceof Long) {
return Type.INT;
} else if (mValue instanceof Float) {
return Type.FLOAT;
} else if (mValue instanceof String) {
return Type.STRING;
} else if (mValue instanceof ByteString) {
return Type.BYTES;
} else {
throw new IllegalStateException(
"Unexpected flag value type: " + mValue.getClass().getName());
}
}
private void checkType(Type requestedType) {
Type actualType = getType();
if (requestedType != actualType) {
throw new IllegalStateException(
"Attempted to access flag value as "
+ requestedType
+ ", but actual type is "
+ actualType);
}
}
/** @throws IllegalStateException Iff {@link #getType} is not {@link Type#BOOL} */
public boolean getBoolValue() {
checkType(Type.BOOL);
return (Boolean) mValue;
}
/** @throws IllegalStateException Iff {@link #getType} is not {@link Type#INT} */
public long getIntValue() {
checkType(Type.INT);
return (Long) mValue;
}
/** @throws IllegalStateException Iff {@link #getType} is not {@link Type#FLOAT} */
public float getFloatValue() {
checkType(Type.FLOAT);
return (Float) mValue;
}
/** @throws IllegalStateException Iff {@link #getType} is not {@link Type#STRING} */
public String getStringValue() {
checkType(Type.STRING);
return (String) mValue;
}
/** @throws IllegalStateException Iff {@link #getType} is not {@link Type#BYTES} */
public ByteString getBytesValue() {
checkType(Type.BYTES);
return (ByteString) mValue;
}
}
private final Map<String, Value> mFlags;
/**
* Computes effective flag values based on the contents of a {@link Flags} proto.
*
* <p>This method will resolve {@link FlagValue.ConstrainedValue} filters according to the
* other arguments, producing the final values that should apply to the caller.
*
* <p>Note that a {@link FlagValue} that has no {@link FlagValue.ConstrainedValue} entry, or
* where the matching entry has no value set, will not be mentioned at all in the resulting
* {@link #flags}.
*
* @param flags The {@link Flags} proto to extract the flag values from. This would normally be
* the return value of {@link HttpFlagsLoader#load}.
* @param appId The App ID for resolving the {@link FlagValue.ConstrainedValue#getAppId} field.
* This would normally be the return value of
* {@link android.content.Context#getPackageName}.
* @param cronetVersion The version to use for filtering against the {@link
* FlagValue.ConstrainedValue#getMinVersion} field.
*/
public static ResolvedFlags resolve(Flags flags, String appId, String cronetVersion) {
int[] parsedCronetVersion = parseVersionString(cronetVersion);
Map<String, Value> resolvedFlags = new HashMap<String, Value>();
for (var flag : flags.getFlagsMap().entrySet()) {
try {
Value value = Value.resolve(flag.getValue(), appId, parsedCronetVersion);
if (value == null) continue;
resolvedFlags.put(flag.getKey(), value);
} catch (RuntimeException exception) {
throw new IllegalArgumentException(
"Unable to resolve HTTP flag `" + flag.getKey() + "`", exception);
}
}
return new ResolvedFlags(resolvedFlags);
}
@VisibleForTesting
public ResolvedFlags(Map<String, Value> flags) {
mFlags = flags;
}
/**
* @return The effective HTTP flag values, keyed by flag name. Neither keys nor values can be
* null. Only flags that have actual values are included in the result.
*/
public Map<String, Value> flags() {
return Collections.unmodifiableMap(mFlags);
}
private static int[] parseVersionString(String versionString) {
try {
if (versionString.isEmpty()) {
throw new IllegalArgumentException("Version string is empty");
}
StringTokenizer tokenizer = new StringTokenizer(versionString, ".");
int[] components = new int[tokenizer.countTokens()];
for (int i = 0; i < components.length; i++) {
components[i] = Integer.parseInt(tokenizer.nextToken());
}
return components;
} catch (RuntimeException exception) {
throw new IllegalArgumentException(
"Unable to parse HTTP flags version string: `" + versionString + "`",
exception);
}
}
}