blob: 9c71c1ba3a8c9ae7ce465c096fa9a71984db3c0c [file] [log] [blame] [edit]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#![deny(missing_docs)]
//! ChromeOS featured client.
//!
//! This crate provides a wrapper around the C implementation of the
//! `featured` client library, which is the preferred way for interacting
//! with ChromeOS features from the platform side.
//!
mod bindings;
use crate::bindings::*;
/// Mock implementation and tests for featured rust client.
///
/// This module will provide a mock featured client that mimics
/// the communication using fake C library, and includes tests to
/// validate the bindings between C and Rust.
#[cfg(feature = "fake_backend")]
pub mod fake;
use dbus::channel::MatchingReceiver;
use dbus::message::MatchRule;
use dbus::nonblock::SyncConnection;
use once_cell::sync::OnceCell;
use std::collections::HashMap;
use std::mem::ManuallyDrop;
use std::pin::Pin;
use std::sync::Arc;
use thiserror::Error;
/// Errors which can occur during `Feature` creation and use.
#[derive(Error, Debug)]
pub enum FeatureError {
/// C strings are null-byte terminated, but Rust strings are not.
/// It is invalid for a Rust string to be converted into a C string
/// when the Rust string contains a null byte.
#[error("feature name contains a null byte: {0}")]
InteriorNullByte(std::ffi::NulError),
}
/// Errors that can occur when interacting with the underlying C library.
#[derive(Error, Debug)]
pub enum PlatformError {
/// When creating handles to the underlying C client, it is possible for
/// the call to return an invalid, null handle.
#[error("c library returned a null pointer instead of a handle")]
NullHandle,
/// When querying for data through the C library handle, a result value
/// is returned. Non-zero result values indicate an error and are passed
/// as the argument to this error.
#[error("c library returned an error code: {0}")]
BadResult(i32),
}
/// `CheckFeature` provides methods for requesting the feature status from
/// the underlying `featured` service. These methods are inherently blocking,
/// and should be assumed to be invalidated when Chrome restarts.
pub trait CheckFeature {
/// Checks if a feature is currently enabled.
///
/// Since Chrome may have restarted between separate calls of this function,
/// `get_params_and_enabled` is preferred if there are multiple dependant features
/// that need to be checked together.
fn is_feature_enabled_blocking(&self, feature: &Feature) -> bool;
/// Requests the status of the provided features, including both if it is enabled
/// and the set parameters if enabled.
///
/// Since Chrome may have restarted between separate calls of this function,
/// make sure to request all co-dependent features together to prevent invalid
/// assumptions around feature state.
///
/// # Errors
///
/// If the underlying C calls do not proper fetch the feature status object,
/// an error string will be returned.
fn get_params_and_enabled(
&self,
features: &[&Feature],
) -> Result<GetParamsAndEnabledResponse, PlatformError>;
}
/// A wrapper around the C implementation for `VariationsFeature`.
///
/// This struct contains two major elements, a `name` attribute that
/// specifies the feature name registered in Chrome, and an `enabled_by_default`
/// attribute that specifies if the feature should enabled when it is
/// not explicitly provided.
///
/// The C featured library uses referential equality checks, so it is
/// important to reuse this struct when checking against the same feature
/// multiple times.
#[derive(Debug)]
pub struct Feature {
name: std::ffi::CString,
c_feature: Pin<Box<VariationsFeature>>,
}
impl Feature {
/// Creates a feature to be used for checking enablement status and parameters.
///
/// The `name` argument must be a valid null-terminated UTF-8 string in order to be properly
/// serialized into a `CString`.
///
/// # Errors
///
/// If the provided feature name is not a valid C string, an error will be returned.
pub fn new(name: &str, enabled_by_default: bool) -> Result<Self, FeatureError> {
let name = std::ffi::CString::new(name).map_err(FeatureError::InteriorNullByte)?;
let default_state = if enabled_by_default {
FeatureState_FEATURE_ENABLED_BY_DEFAULT
} else {
FeatureState_FEATURE_DISABLED_BY_DEFAULT
};
let c_feature = Pin::new(Box::new(VariationsFeature {
name: name.as_ptr(),
default_state,
}));
Ok(Feature { name, c_feature })
}
/// The name assigned to this feature.
///
/// This will be the name registered in `featured` and used to enable/disable the feature
/// via Finch or flags.
pub fn name(&self) -> &str {
// The validation check in `Feature::new` means this is guarateed to be `Ok`.
self.name.to_str().expect("Unreachable")
}
/// If the feature is enabled when not specified in `featured`.
pub fn enabled_by_default(&self) -> bool {
self.c_feature.default_state == FeatureState_FEATURE_ENABLED_BY_DEFAULT
}
}
// feature_library is thread-safe.
// A Feature constructed from one thread can be used in another thread.
unsafe impl Send for Feature {}
unsafe impl Sync for Feature {}
/// An enumeration for representing the state of a feature and its parameters.
///
/// Disabled features will never have associated parameters, and an enabled
/// feature will always have a map of parameter name to parameter value, even
/// if that map is empty.
#[derive(Debug)]
enum FeatureStatus {
Disabled,
Enabled(HashMap<String, String>),
}
impl FeatureStatus {
fn is_enabled(&self) -> bool {
match self {
FeatureStatus::Disabled => false,
FeatureStatus::Enabled(_) => true,
}
}
fn get_parameters(&self) -> Option<&HashMap<String, String>> {
match self {
FeatureStatus::Disabled => None,
FeatureStatus::Enabled(parameters) => Some(parameters),
}
}
}
/// A wrapper around the status attributes returned by calls to
/// `CheckFeature::get_params_and_enabled`.
///
/// Features that are not enabled do not have parameters.
/// Features that are enabled will have an associated parameter
/// map, which may be empty.
#[derive(Debug)]
pub struct GetParamsAndEnabledResponse {
status_map: HashMap<String, FeatureStatus>,
}
impl GetParamsAndEnabledResponse {
/// Checks if the provided feature is enabled.
///
/// If it is not found in the original features list that created
/// then the value provided by `Feature::enabled_by_default` is
/// returned.
pub fn is_enabled(&self, feature: &Feature) -> bool {
self.status_map
.get(feature.name())
.map_or_else(|| feature.enabled_by_default(), FeatureStatus::is_enabled)
}
/// Returns the parameters associated with the provided feature.
///
/// If the feature is disabled or is not present in the original
/// feature list, `None` is returned.
pub fn get_params(&self, feature: &Feature) -> Option<&HashMap<String, String>> {
self.status_map
.get(feature.name())
.and_then(FeatureStatus::get_parameters)
}
/// Returns the parameter value associated with the key for the provided feature.
///
/// If the feature is disabled, is not present in the original feature list,
/// or does not contain the provided key, `None` is returned.
pub fn get_param(&self, feature: &Feature, key: &str) -> Option<&String> {
self.get_params(feature).and_then(|params| params.get(key))
}
}
/// An internal wrapper around C library a handle pointer.
///
/// Wrapping the handle with this struct allows us to be certain
/// that the handle was properly created by the C library, and provides
/// us with a way to safely call FFI functions.
struct SafeHandle {
handle: CFeatureLibrary,
/// Indicates whether this safe handle holds handle to a fake backend.
/// This field may not be read outside of the tests.
#[cfg_attr(not(feature = "fake_backend"), allow(dead_code))]
fake: bool,
}
// SAFETY: `PlatformFeatures` stores `SafeHandle` in an Arc pointer, which is thread-safe so we can
// annotate with `Send`. `CFeatureLibrary` is also safe to use in multiple threads simultaneously
// so we can annotate with `Sync`. `SafeHandle` will only get dropped after the program terminates
// and all references to the global `PlatformFeatures` instance go out of scope. This is necessary
// since `PlatformFeatures` is trying to wrap unsafe C code in a safe Rust struct.
unsafe impl Send for SafeHandle {}
unsafe impl Sync for SafeHandle {}
impl SafeHandle {
fn is_feature_enabled_blocking(&self, feature: &Feature) -> bool {
// SAFETY: The C call is guaranteed to return a valid value and does not modify
// the underlying handle or feature.
unsafe { CFeatureLibraryIsEnabledBlocking(self.handle, &*feature.c_feature.as_ref()) != 0 }
}
fn get_params_and_enabled_blocking(
&self,
features: &[&Feature],
) -> Result<GetParamsAndEnabledResponse, PlatformError> {
// Extract the `VariationsFeature`s to pass to the C library.
let feature_ptrs: Vec<_> = features
.iter()
.map(|feature| &*feature.c_feature.as_ref() as *const VariationsFeature)
.collect();
// Allocate an array for the C library to populate.
let responses: Vec<VariationsFeatureGetParamsResponseEntry> =
Vec::with_capacity(feature_ptrs.len());
// We need to tell Rust that we are transferring ownership of this
// memory to the C library, and will later retake ownership.
let mut responses = ManuallyDrop::new(responses);
let response_ptr = responses.as_mut_ptr();
let response_len = feature_ptrs.len();
// Populate the response array and make sure the response is valid.
// SAFETY: The C library will not invalidate the handle pointer, but may
// fail to write the responses. A check is made after returning ownership
// of the response memory back to Rust, verifying that the data is valid.
let result = unsafe {
CFeatureLibraryGetParamsAndEnabledBlocking(
self.handle,
feature_ptrs.as_ptr(),
response_len,
response_ptr,
)
};
// Rebuild the responses array by recreating a vector
// from the C pointer and length.
// SAFETY: The response pointer is guaranteed to be valid and the contents will
// either be written out by the previous C library call or zeroed out from the original
// memory allocation via `Vec::with_capacity`. All of the safety requirements specified by
// `Vec::from_raw_parts` are met since we are rebuilding the `Vec<T>` with the same `T` and
// capacity. Additionally, It is important that this is called immediately after the C
// library call to prevent a memory leak.
let responses = unsafe { Vec::from_raw_parts(response_ptr, response_len, response_len) };
// The C library call may have failed at some point, which would result in the response
// being incomplete or invalid.
// Note: When the C library call fails, it will deallocate any allocated memory, which means
// there is no need to manually call `CFeatureLibraryFreeEntries` before this early return.
if result != 0 {
return Err(PlatformError::BadResult(result));
}
// Convert the response to Rust native structures.
let status_map = responses
.iter()
.filter_map(|raw_entry| {
// SAFETY: The C library is guaranteed to either provide a valid entry or none
// at all. If the entry is valid, then the name field is guaranteed to contain
// a valid, non-null C string. Additionally, we know that this C string will not
// be mutated during this call.
let name = unsafe { parse_cstr(raw_entry.name)? };
let value = parse_params(raw_entry);
Some((name, value))
})
.collect();
// Free the associated C structures since they have been converted to Rust types.
// The calls to `parse_params` and `parse_cstr` create new Rust-owned values, so the
// underlying `VariationsFeatureGetParamsResponseEntry` data are not copied or referenced
// anywhere.
// SAFETY: This call only frees the underlying data allocated by the C library. It
// does not free the memory at the provided pointer, which is currently owned by Rust
// and will be freed at the end of this function call. The response pointer is guaranteed to
// be valid and is never used again.
unsafe { CFeatureLibraryFreeEntries(response_ptr, response_len) }
Ok(GetParamsAndEnabledResponse { status_map })
}
}
/// Register a callback to run whenever it is required to refetch feature
/// state (that is, whenever chrome restarts).
pub async fn listen_for_refetch_needed<T: FnMut() + Send + 'static>(
conn: &SyncConnection,
mut signal_callback: T,
) -> Result<(), dbus::Error> {
let refetch_signal = MatchRule::new_signal("org.chromium.feature_lib", "RefetchFeatureState");
conn.add_match_no_cb(&refetch_signal.match_str()).await?;
conn.start_receive(
refetch_signal,
Box::new(move |_, _| {
signal_callback();
true
}),
);
Ok(())
}
/// A platform specific featured client, used to communicate to featured via the
/// wrapped C library.
pub struct PlatformFeatures {
handle: SafeHandle,
}
static FEATURE_LIBRARY: OnceCell<Arc<PlatformFeatures>> = OnceCell::new();
impl PlatformFeatures {
/// Returns a client handle for requests to featured. Will also initialize the client handle on
/// the first call.
///
/// # Errors
///
/// If the underlying C calls do not return a proper handle to
/// the featured client, an error will be returned.
pub fn get() -> Result<Arc<PlatformFeatures>, PlatformError> {
FEATURE_LIBRARY
.get_or_try_init(|| {
// SAFETY: The C library will initialize the handle to either a valid object pointer
// or a null pointer. A subsequent check is made to ensure the client is initialized
// properly.
let initialize = unsafe { CFeatureLibraryInitialize() };
if !initialize {
return Err(PlatformError::NullHandle);
}
let cpp_handle = unsafe { CFeatureLibraryGet() };
if cpp_handle.is_null() {
return Err(PlatformError::NullHandle);
}
let lib = Arc::new(PlatformFeatures {
handle: SafeHandle {
handle: cpp_handle,
fake: false,
},
});
Ok(lib)
})
.cloned()
}
}
impl CheckFeature for PlatformFeatures {
fn is_feature_enabled_blocking(&self, feature: &Feature) -> bool {
self.handle.is_feature_enabled_blocking(feature)
}
fn get_params_and_enabled(
&self,
features: &[&Feature],
) -> Result<GetParamsAndEnabledResponse, PlatformError> {
self.handle.get_params_and_enabled_blocking(features)
}
}
fn parse_params(entry: &VariationsFeatureGetParamsResponseEntry) -> FeatureStatus {
// Disabled features will never have associated parameters.
if entry.is_enabled == 0 {
return FeatureStatus::Disabled;
}
// Read the underlying C array as a Rust slice.
// SAFETY: The C library ensures that it will always return valid pointers in its responses,
// which means we can safely read those values as a slice.
let params = unsafe { std::slice::from_raw_parts(entry.params, entry.num_params) };
let hash_map = params
.iter()
.filter_map(|param| {
// SAFETY: The C library guarantees that it only returns valid entries, which means
// that both the key and value for this parameter will be valid, non-null C strings.
// Additionally, we know that this C string will not be mutated during this call.
unsafe { Some((parse_cstr(param.key)?, parse_cstr(param.value)?)) }
})
.collect();
FeatureStatus::Enabled(hash_map)
}
// SAFETY: The caller of this function must guarantee that the pointer points to valid,
// initialized data, that the memory ends with a null byte, and that it will not be
// mutated during the duration of this call.
#[inline]
unsafe fn parse_cstr(ptr: *const std::os::raw::c_char) -> Option<String> {
if ptr.is_null() {
return None;
}
std::ffi::CStr::from_ptr(ptr)
.to_str()
.ok()
.map(str::to_string)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_can_create_a_valid_feature() {
let subject = Feature::new("some-valid-feature", true);
assert!(subject.is_ok());
}
#[test]
fn it_rejects_an_invalid_feature() {
let subject = Feature::new("some-bad-feature\0", true);
assert!(subject.is_err());
}
#[test]
fn it_initializes_and_returns_a_valid_library() {
let first_init = PlatformFeatures::get();
assert!(first_init.is_ok());
let second_init = PlatformFeatures::get();
assert!(second_init.is_ok())
}
}