blob: 2c7ffdb2d52d59e1c7f2cf2a1a2bb4b37c1c4e51 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
anyhow::{anyhow, Context as _},
fidl_fuchsia_io as fio,
fidl_fuchsia_pkg_rewrite_ext::{Rule, RuleConfig},
fuchsia_inspect::{self as inspect, Property},
fuchsia_syslog::fx_log_err,
fuchsia_url::AbsolutePackageUrl,
std::collections::VecDeque,
thiserror::Error,
};
/// [RewriteManager] controls access to all static and dynamic rewrite rules used by the package
/// resolver.
///
/// No two instances of [RewriteManager] should be configured to use the same `dynamic_rules_path`,
/// or concurrent saves could corrupt the config file or lose edits. Instead, use the provided
/// [RewriteManager::transaction] API to safely manage concurrent edits.
#[derive(Debug)]
pub struct RewriteManager {
enable_dynamic_configuration: bool,
static_rules: Vec<Rule>,
dynamic_rules: Vec<Rule>,
generation: u32,
data_proxy: Option<fio::DirectoryProxy>,
dynamic_rules_path: Option<String>,
inspect: RewriteManagerInspectState,
}
#[derive(Debug)]
struct RewriteManagerInspectState {
static_rules_node: inspect::Node,
static_rules_states: Vec<RuleInspectState>,
dynamic_rules_node: inspect::Node,
dynamic_rules_states: Vec<RuleInspectState>,
generation_property: fuchsia_inspect::UintProperty,
// TODO(fxbug.dev/84729)
#[allow(unused)]
dynamic_rules_path_property: fuchsia_inspect::StringProperty,
// TODO(fxbug.dev/84729)
#[allow(unused)]
node: inspect::Node,
}
#[derive(Debug, Error)]
pub enum CommitError {
#[error("the provided rule set is based on an older generation")]
TooLate,
#[error("editing rewrite rules is permanently disabled")]
DynamicConfigurationDisabled,
}
impl RewriteManager {
/// Rewrite the given [AbsolutePackageUrl] using the first dynamic or static rewrite rule that
/// matches and produces a valid [AbsolutePackageUrl]. If no rewrite rules match or all that do
/// produce invalid [AbsolutePackageUrl]s, return the original, unmodified [AbsolutePackageUrl].
pub fn rewrite(&self, url: &AbsolutePackageUrl) -> AbsolutePackageUrl {
for rule in self.list() {
match rule.apply(&url) {
Some(Ok(res)) => {
return res;
}
Some(Err(err)) => {
fx_log_err!(
"ignoring rewrite rule {:?} that produced an invalid URL: {:#}",
rule,
anyhow!(err)
);
}
_ => {}
}
}
url.clone()
}
async fn save(
dynamic_rules: &mut Vec<Rule>,
data_proxy: &fio::DirectoryProxy,
dynamic_rules_path: &str,
) -> Result<(), anyhow::Error> {
let config = RuleConfig::Version1(std::mem::replace(dynamic_rules, vec![]));
let result = async {
// TODO(fxbug.dev/83342): We need to reopen because `resolve_succeeds_with_broken_minfs`
// expects it, this should be removed once the test is fixed.
let data_proxy = fuchsia_fs::directory::open_directory(
&data_proxy,
".",
fio::OpenFlags::RIGHT_WRITABLE,
)
.await
.context("opening data-proxy directory")?;
let temp_filename = &format!("{dynamic_rules_path}.new");
let data = serde_json::to_vec(&config).context("encoding config")?;
crate::util::do_with_atomic_file(
&data_proxy,
temp_filename,
&dynamic_rules_path,
|proxy| async move {
fuchsia_fs::file::write(&proxy, &data)
.await
.with_context(|| format!("writing file: {}", temp_filename))
},
)
.await
}
.await;
let RuleConfig::Version1(rules) = config;
*dynamic_rules = rules;
result
}
/// Construct a new [Transaction] containing the dynamic config rules from this
/// [RewriteManager].
pub fn transaction(&self) -> Transaction {
Transaction {
dynamic_rules: self.dynamic_rules.clone().into(),
generation: self.generation,
}
}
/// Apply the given [Transaction] object to this [RewriteManager] if and only if:
/// * dynamic configuration is enabled, and
/// * no other [RewriteRuleStates] have been applied since `transaction` was
/// cloned from this [RewriteManager].
pub async fn apply(&mut self, transaction: Transaction) -> Result<(), CommitError> {
if !self.enable_dynamic_configuration {
return Err(CommitError::DynamicConfigurationDisabled);
}
if self.generation != transaction.generation {
Err(CommitError::TooLate)
} else {
self.dynamic_rules = transaction.dynamic_rules.into();
self.generation += 1;
if let (Some(ref data_proxy), Some(ref dynamic_rules_path)) =
(self.data_proxy.as_ref(), self.dynamic_rules_path.as_ref())
{
if let Err(err) =
Self::save(&mut self.dynamic_rules, data_proxy, dynamic_rules_path).await
{
fx_log_err!("error while saving dynamic rewrite rules: {:#}", anyhow!(err));
}
}
self.update_inspect_objects();
Ok(())
}
}
/// Return an iterator through all rewrite rules in the order they should be applied to
/// incoming `fuchsia-pkg://` URLs.
pub fn list(&self) -> impl Iterator<Item = &Rule> {
self.dynamic_rules.iter().chain(self.list_static())
}
/// Return an iterator through all static rewrite rules in the order they should be applied to
/// incoming `fuchsia-pkg://` URLs, after all dynamic rules have been considered.
pub fn list_static(&self) -> impl Iterator<Item = &Rule> {
self.static_rules.iter()
}
fn update_rule_inspect_states(
node: &inspect::Node,
rules: &[Rule],
states: &mut Vec<RuleInspectState>,
) {
states.clear();
for (i, rule) in rules.iter().enumerate() {
let rule_node = node.create_child(&i.to_string());
states.push(create_rule_inspect_state(rule, rule_node));
}
}
fn update_inspect_objects(&mut self) {
self.inspect.generation_property.set(self.generation.into());
RewriteManager::update_rule_inspect_states(
&self.inspect.dynamic_rules_node,
&self.dynamic_rules,
&mut self.inspect.dynamic_rules_states,
);
}
}
#[allow(missing_docs)]
#[derive(Debug, PartialEq, Eq)]
pub struct RuleInspectState {
_host_match_property: inspect::StringProperty,
_host_replacement_property: inspect::StringProperty,
_path_prefix_match_property: inspect::StringProperty,
_path_prefix_replacement_property: inspect::StringProperty,
_node: inspect::Node,
}
fn create_rule_inspect_state(rule: &Rule, node: inspect::Node) -> RuleInspectState {
RuleInspectState {
_host_match_property: node.create_string("host_match", &rule.host_match()),
_host_replacement_property: node
.create_string("host_replacement", &rule.host_replacement()),
_path_prefix_match_property: node
.create_string("path_prefix_match", &rule.path_prefix_match()),
_path_prefix_replacement_property: node
.create_string("path_prefix_replacement", &rule.path_prefix_replacement()),
_node: node,
}
}
/// [Transaction] tracks an edit transaction to a set of dynamic rewrite rules.
#[derive(Debug, PartialEq, Eq)]
pub struct Transaction {
dynamic_rules: VecDeque<Rule>,
generation: u32,
}
impl Transaction {
#[cfg(test)]
pub fn new(dynamic_rules: Vec<Rule>, generation: u32) -> Self {
Self { dynamic_rules: dynamic_rules.into(), generation }
}
/// Remove all dynamic rules from this [Transaction].
pub fn reset_all(&mut self) {
self.dynamic_rules.clear();
}
/// Add the given [Rule] to this [Transaction] with the highest match priority.
pub fn add(&mut self, rule: Rule) {
self.dynamic_rules.push_front(rule);
}
/// Return an iterator through all dynamic rewrite rules in the order they should be applied to
/// incoming `fuchsia-pkg://` URLs.
pub fn list_dynamic(&self) -> impl Iterator<Item = &Rule> {
self.dynamic_rules.iter()
}
}
#[derive(Debug)]
pub struct UnsetInspectNode;
/// [RewriteManagerBuilder] constructs a [RewriteManager], optionally initializing it with [Rule]s
/// passed in directly or loaded out of the filesystem.
#[derive(Debug)]
pub struct RewriteManagerBuilder<N> {
static_rules: Vec<Rule>,
dynamic_rules: Vec<Rule>,
data_proxy: Option<fio::DirectoryProxy>,
dynamic_rules_path: Option<String>,
enable_dynamic_configuration: bool,
inspect_node: N,
}
impl RewriteManagerBuilder<UnsetInspectNode> {
/// Create a new [RewriteManagerBuilder] and initialize it with the dynamic [Rule]s from the
/// provided path. If the provided dynamic rule config file does not exist or is corrupt, this
/// method returns an [RewriteManagerBuilder] initialized with no rules and configured with the
/// given dynamic config path.
pub async fn new<P>(
data_proxy: Option<fio::DirectoryProxy>,
dynamic_rules_path: Option<P>,
) -> Result<Self, (Self, LoadRulesError)>
where
P: Into<String>,
{
let enable_dynamic_configuration = dynamic_rules_path.is_some();
let mut builder = RewriteManagerBuilder {
static_rules: vec![],
dynamic_rules: vec![],
data_proxy: data_proxy.clone(),
dynamic_rules_path: dynamic_rules_path.map(|p| p.into()),
inspect_node: UnsetInspectNode,
enable_dynamic_configuration,
};
if let Some(ref dynamic_rules_path) = builder.dynamic_rules_path {
match Self::load_rules(&data_proxy, dynamic_rules_path).await {
Ok(rules) => {
builder.dynamic_rules = rules;
Ok(builder)
}
Err(err) => Err((builder, err)),
}
} else {
Ok(builder)
}
}
/// Use the given inspect_node in the [RewriteManager].
pub fn inspect_node(self, inspect_node: inspect::Node) -> RewriteManagerBuilder<inspect::Node> {
RewriteManagerBuilder {
static_rules: self.static_rules,
dynamic_rules: self.dynamic_rules,
data_proxy: self.data_proxy,
dynamic_rules_path: self.dynamic_rules_path,
enable_dynamic_configuration: self.enable_dynamic_configuration,
inspect_node,
}
}
}
#[derive(Debug, Error)]
pub enum LoadRulesError {
#[error("directory open")]
DirOpen(#[source] anyhow::Error),
#[error("file open")]
FileOpen(#[from] fuchsia_fs::node::OpenError),
#[error("read file")]
ReadFile(#[source] anyhow::Error),
#[error("parse")]
Parse(#[from] serde_json::Error),
}
impl<N> RewriteManagerBuilder<N> {
/// Load [Rule]s from the provided path and register them as static rewrite rules. On error,
/// return this [RewriteManagerBuilder] unmodified along with the encountered error.
pub async fn static_rules_path(
mut self,
config_proxy: Option<fio::DirectoryProxy>,
static_rules_path: &str,
) -> Result<Self, (Self, LoadRulesError)> {
match Self::load_rules(&config_proxy, static_rules_path).await {
Ok(rules) => {
self.static_rules = rules;
Ok(self)
}
Err(err) => Err((self, err)),
}
}
async fn load_rules(
dir_proxy: &Option<fio::DirectoryProxy>,
path: &str,
) -> Result<Vec<Rule>, LoadRulesError> {
let dir_proxy = dir_proxy
.as_ref()
.ok_or_else(|| LoadRulesError::DirOpen(anyhow!("failed to open config directory")))?;
let file_proxy =
fuchsia_fs::directory::open_file(&dir_proxy, &path, fio::OpenFlags::RIGHT_READABLE)
.await?;
let contents =
fuchsia_fs::read_file(&file_proxy).await.map_err(LoadRulesError::ReadFile)?;
let RuleConfig::Version1(rules) = serde_json::from_str(&contents)?;
Ok(rules)
}
/// Append the given [Rule]s to the static rewrite rules.
#[cfg(test)]
pub fn static_rules<T>(mut self, iter: T) -> Self
where
T: IntoIterator<Item = Rule>,
{
self.static_rules.extend(iter);
self
}
/// Replace the dynamic rules with the given [Rule]s.
pub fn replace_dynamic_rules<T>(mut self, iter: T) -> Self
where
T: IntoIterator<Item = Rule>,
{
self.dynamic_rules.clear();
self.dynamic_rules.extend(iter);
self
}
}
#[cfg(test)]
impl RewriteManagerBuilder<UnsetInspectNode> {
/// In test configurations, allow building the [RewriteManager] without a configured inspect
/// node.
pub fn build(self) -> RewriteManager {
let node = inspect::Inspector::new().root().create_child("test");
self.inspect_node(node).build()
}
}
impl RewriteManagerBuilder<inspect::Node> {
/// Build the [RewriteManager].
pub fn build(self) -> RewriteManager {
let inspect = RewriteManagerInspectState {
static_rules_node: self.inspect_node.create_child("static_rules"),
static_rules_states: vec![],
dynamic_rules_node: self.inspect_node.create_child("dynamic_rules"),
dynamic_rules_states: vec![],
generation_property: self.inspect_node.create_uint("generation", 0),
dynamic_rules_path_property: self
.inspect_node
.create_string("dynamic_rules_path", &format!("{:?}", self.dynamic_rules_path)),
node: self.inspect_node,
};
let mut rw = RewriteManager {
static_rules: self.static_rules,
dynamic_rules: self.dynamic_rules,
generation: 0,
data_proxy: self.data_proxy,
dynamic_rules_path: self.dynamic_rules_path,
enable_dynamic_configuration: self.enable_dynamic_configuration,
inspect,
};
RewriteManager::update_rule_inspect_states(
&rw.inspect.static_rules_node,
&rw.static_rules,
&mut rw.inspect.static_rules_states,
);
rw.update_inspect_objects();
rw
}
}
#[cfg(test)]
pub(crate) mod tests {
use {
super::*,
anyhow::Error,
assert_matches::assert_matches,
fuchsia_async as fasync,
fuchsia_inspect::assert_data_tree,
serde_json::json,
std::{io, path::Path},
};
macro_rules! rule {
($host_match:expr => $host_replacement:expr,
$path_prefix_match:expr => $path_prefix_replacement:expr) => {
fidl_fuchsia_pkg_rewrite_ext::Rule::new(
$host_match.to_owned(),
$host_replacement.to_owned(),
$path_prefix_match.to_owned(),
$path_prefix_replacement.to_owned(),
)
.unwrap()
};
}
pub(crate) fn make_temp_file<CB, E>(writer: CB) -> tempfile::TempPath
where
CB: FnOnce(&mut dyn io::Write) -> Result<(), E>,
E: Into<Error>,
{
let mut f = tempfile::NamedTempFile::new().unwrap();
writer(f.as_file_mut()).map_err(|err| err.into()).unwrap();
f.into_temp_path()
}
pub(crate) fn make_rule_config(rules: Vec<Rule>) -> tempfile::TempPath {
let config = RuleConfig::Version1(rules);
make_temp_file(|writer| serde_json::to_writer(writer, &config))
}
pub(crate) fn temp_path_into_proxy_and_path(
path: &tempfile::TempPath,
) -> (Option<fio::DirectoryProxy>, Option<String>) {
let filename = Some(path.file_name().unwrap().to_str().unwrap().to_string());
let dir = path.parent().unwrap().to_str().unwrap().to_string();
let proxy = fuchsia_fs::directory::open_in_namespace(
&dir,
fuchsia_fs::OpenFlags::RIGHT_READABLE | fuchsia_fs::OpenFlags::RIGHT_WRITABLE,
)
.ok();
(proxy, filename)
}
#[fasync::run_singlethreaded(test)]
async fn test_empty_configs() {
let path = make_rule_config(vec![]);
let (config_dir, config_file) = temp_path_into_proxy_and_path(&path);
let manager = RewriteManagerBuilder::new(config_dir.clone(), config_file.clone())
.await
.unwrap()
.static_rules_path(config_dir, &config_file.unwrap())
.await
.unwrap()
.build();
assert_eq!(manager.list_static().cloned().collect::<Vec<_>>(), vec![]);
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]);
}
#[fasync::run_singlethreaded(test)]
async fn test_load_single_static_rule() {
let rules = vec![rule!("fuchsia.com" => "fuchsia.com", "/rolldice" => "/rolldice")];
let dynamic_path = make_rule_config(vec![]);
let (dynamic_config_dir, dynamic_config_file) =
temp_path_into_proxy_and_path(&dynamic_path);
let path = make_rule_config(rules.clone());
let (static_config_dir, static_config_file) = temp_path_into_proxy_and_path(&path);
let manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.static_rules_path(static_config_dir, &static_config_file.unwrap())
.await
.unwrap()
.build();
assert_eq!(manager.list_static().cloned().collect::<Vec<_>>(), rules);
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules);
}
#[fasync::run_singlethreaded(test)]
async fn test_load_single_dynamic_rule() {
let rules = vec![rule!("fuchsia.com" => "fuchsia.com", "/rolldice" => "/rolldice")];
let path = make_rule_config(rules.clone());
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.build();
assert_eq!(manager.list_static().cloned().collect::<Vec<_>>(), vec![]);
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules);
}
#[fasync::run_singlethreaded(test)]
async fn test_rejects_invalid_static_config() {
let rules = vec![rule!("fuchsia.com" => "fuchsia.com", "/a" => "/b")];
let dynamic_path = make_rule_config(rules.clone());
let (dynamic_config_dir, dynamic_config_file) =
temp_path_into_proxy_and_path(&dynamic_path);
let path = make_temp_file(|writer| {
write!(
writer,
"{}",
json!({
"version": "1",
"content": {} // should be an array
})
)
});
let (static_config_dir, static_config_file) = temp_path_into_proxy_and_path(&path);
let (builder, _) = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.static_rules_path(static_config_dir, &static_config_file.unwrap())
.await
.unwrap_err();
let manager = builder.build();
assert_eq!(manager.list_static().cloned().collect::<Vec<_>>(), vec![]);
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules);
}
#[fasync::run_singlethreaded(test)]
async fn test_recovers_from_invalid_dynamic_config() {
let path = make_temp_file(|writer| write!(writer, "invalid"));
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let rule = rule!("test.com" => "test.com", "/a" => "/b");
{
let (builder, _) =
RewriteManagerBuilder::new(dynamic_config_dir.clone(), dynamic_config_file.clone())
.await
.unwrap_err();
let mut manager = builder.build();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]);
let mut transaction = manager.transaction();
transaction.add(rule.clone());
manager.apply(transaction).await.unwrap();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![rule.clone()]);
}
// Verify the dynamic config file is no longer corrupt and contains the newly added rule.
let manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.build();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![rule]);
}
#[fasync::run_singlethreaded(test)]
async fn test_rewrite_identity_if_no_rules_match() {
let rules = vec![
rule!("fuchsia.com" => "fuchsia.com", "/a" => "/aa"),
rule!("fuchsia.com" => "fuchsia.com", "/b" => "/bb"),
];
let path = make_rule_config(rules);
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.build();
let url: AbsolutePackageUrl = "fuchsia-pkg://fuchsia.com/c".parse().unwrap();
assert_eq!(manager.rewrite(&url), url);
}
#[fasync::run_singlethreaded(test)]
async fn test_rewrite_first_rule_wins() {
let rules = vec![
rule!("fuchsia.com" => "fuchsia.com", "/package" => "/remapped"),
rule!("fuchsia.com" => "fuchsia.com", "/package" => "/incorrect"),
];
let path = make_rule_config(rules);
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.build();
let url = "fuchsia-pkg://fuchsia.com/package".parse().unwrap();
assert_eq!(manager.rewrite(&url), "fuchsia-pkg://fuchsia.com/remapped".parse().unwrap());
}
#[fasync::run_singlethreaded(test)]
async fn test_rewrite_dynamic_rules_override_static_rules() {
let dynamic_path = make_rule_config(vec![
rule!("fuchsia.com" => "fuchsia.com", "/package" => "/remapped"),
]);
let (dynamic_config_dir, dynamic_config_file) =
temp_path_into_proxy_and_path(&dynamic_path);
let path = make_rule_config(vec![
rule!("fuchsia.com" => "fuchsia.com", "/package" => "/incorrect"),
]);
let (static_config_dir, static_config_file) = temp_path_into_proxy_and_path(&path);
let manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.static_rules_path(static_config_dir, &static_config_file.unwrap())
.await
.unwrap()
.build();
let url = "fuchsia-pkg://fuchsia.com/package".parse().unwrap();
assert_eq!(manager.rewrite(&url), "fuchsia-pkg://fuchsia.com/remapped".parse().unwrap());
}
#[fasync::run_singlethreaded(test)]
async fn test_rewrite_replace_dynamic_configs() {
let static_rules = vec![rule!("fuchsia.com" => "foo.com", "/" => "/")];
let dynamic_rules = vec![rule!("fuchsia.com" => "bar.com", "/" => "/")];
let new_dynamic_rules = vec![rule!("fuchsia.com" => "baz.com", "/" => "/")];
let static_path = make_rule_config(static_rules);
let (static_config_dir, static_config_file) = temp_path_into_proxy_and_path(&static_path);
let path = make_rule_config(dynamic_rules);
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.static_rules_path(static_config_dir, &static_config_file.unwrap())
.await
.unwrap()
.replace_dynamic_rules(new_dynamic_rules.clone())
.build();
let url = "fuchsia-pkg://fuchsia.com/package".parse().unwrap();
assert_eq!(manager.rewrite(&url), "fuchsia-pkg://baz.com/package".parse().unwrap());
}
#[fasync::run_singlethreaded(test)]
async fn test_rewrite_with_pending_transaction() {
let override_rule = rule!("fuchsia.com" => "fuchsia.com", "/a" => "/c");
let path = make_rule_config(vec![rule!("fuchsia.com" => "fuchsia.com", "/a" => "/b")]);
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let mut manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.build();
let mut transaction = manager.transaction();
transaction.add(override_rule.clone());
// new rule is not yet committed and should not be used yet
let url: AbsolutePackageUrl = "fuchsia-pkg://fuchsia.com/a".parse().unwrap();
assert_eq!(manager.rewrite(&url), "fuchsia-pkg://fuchsia.com/b".parse().unwrap());
manager.apply(transaction).await.unwrap();
let url = "fuchsia-pkg://fuchsia.com/a".parse().unwrap();
assert_eq!(manager.rewrite(&url), "fuchsia-pkg://fuchsia.com/c".parse().unwrap());
}
#[fasync::run_singlethreaded(test)]
async fn test_commit_additional_rule() {
let existing_rule = rule!("fuchsia.com" => "fuchsia.com", "/rolldice" => "/rolldice");
let new_rule = rule!("fuchsia.com" => "fuchsia.com", "/rolldice/" => "/rolldice/");
let rules = vec![existing_rule.clone()];
let path = make_rule_config(rules.clone());
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let mut manager =
RewriteManagerBuilder::new(dynamic_config_dir.clone(), dynamic_config_file.clone())
.await
.unwrap()
.build();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules);
// Fork the existing state, add a rule, and verify both instances are distinct
let new_rules = vec![new_rule.clone(), existing_rule.clone()];
let mut transaction = manager.transaction();
transaction.add(new_rule);
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules);
assert_eq!(transaction.list_dynamic().cloned().collect::<Vec<_>>(), new_rules);
// Commit the new rule set
let () = manager.apply(transaction).await.unwrap();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), new_rules);
// Ensure new rules are persisted to the dynamic config file
let manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.build();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), new_rules);
}
#[fasync::run_singlethreaded(test)]
async fn test_erase_all_dynamic_rules() {
let rules = vec![
rule!("fuchsia.com" => "fuchsia.com", "/rolldice" => "/rolldice"),
rule!("fuchsia.com" => "fuchsia.com", "/rolldice/" => "/rolldice/"),
];
let path = make_rule_config(rules.clone());
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let mut manager =
RewriteManagerBuilder::new(dynamic_config_dir.clone(), dynamic_config_file.clone())
.await
.unwrap()
.build();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules);
let mut transaction = manager.transaction();
transaction.reset_all();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules);
assert_eq!(transaction.list_dynamic().cloned().collect::<Vec<_>>(), vec![]);
let () = manager.apply(transaction).await.unwrap();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]);
let manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.build();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]);
}
#[fasync::run_singlethreaded(test)]
async fn test_building_rewrite_manager_populates_inspect() {
let inspector = fuchsia_inspect::Inspector::new();
let node = inspector.root().create_child("rewrite_manager");
let dynamic_rules = vec![
rule!("this.example.com" => "that.example.com", "/this_rolldice" => "/that_rolldice"),
];
let dynamic_path = make_rule_config(dynamic_rules.clone());
let (dynamic_config_dir, dynamic_config_file) =
temp_path_into_proxy_and_path(&dynamic_path);
let static_rules =
vec![rule!("example.com" => "example.org", "/this_throwdice" => "/that_throwdice")];
let path = make_rule_config(static_rules.clone());
let (static_config_dir, static_config_file) = temp_path_into_proxy_and_path(&path);
let _manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file.clone())
.await
.unwrap()
.static_rules_path(static_config_dir, &static_config_file.unwrap())
.await
.unwrap()
.inspect_node(node)
.build();
assert_data_tree!(
inspector,
root: {
rewrite_manager: {
dynamic_rules: {
"0": {
host_match: "this.example.com",
host_replacement: "that.example.com",
path_prefix_match: "/this_rolldice",
path_prefix_replacement: "/that_rolldice",
},
},
dynamic_rules_path: format!("{:?}", dynamic_config_file),
static_rules: {
"0": {
host_match: "example.com",
host_replacement: "example.org",
path_prefix_match: "/this_throwdice",
path_prefix_replacement: "/that_throwdice",
},
},
generation: 0u64,
}
}
);
}
#[fasync::run_singlethreaded(test)]
async fn test_inspect_rewrite_manager_no_dynamic_rules_path() {
let inspector = fuchsia_inspect::Inspector::new();
let node = inspector.root().create_child("rewrite_manager");
let _manager = RewriteManagerBuilder::new(None, Option::<&str>::None)
.await
.unwrap()
.inspect_node(node)
.build();
assert_data_tree!(
inspector,
root: {
rewrite_manager: {
dynamic_rules: {},
dynamic_rules_path: format!("{:?}", Option::<&Path>::None),
static_rules: {},
generation: 0u64,
}
}
);
}
#[fasync::run_singlethreaded(test)]
async fn test_transaction_updates_inspect() {
let inspector = fuchsia_inspect::Inspector::new();
let node = inspector.root().create_child("rewrite_manager");
let path = make_rule_config(vec![]);
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let mut manager =
RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file.clone())
.await
.unwrap()
.inspect_node(node)
.build();
assert_data_tree!(
inspector,
root: {
rewrite_manager: {
dynamic_rules: {},
dynamic_rules_path: format!("{:?}", dynamic_config_file.clone()),
static_rules: {},
generation: 0u64,
}
}
);
let mut transaction = manager.transaction();
transaction
.add(rule!("example.com" => "example.org", "/this_rolldice/" => "/that_rolldice/"));
manager.apply(transaction).await.unwrap();
assert_data_tree!(
inspector,
root: {
rewrite_manager: {
dynamic_rules: {
"0": {
host_match: "example.com",
host_replacement: "example.org",
path_prefix_match: "/this_rolldice/",
path_prefix_replacement: "/that_rolldice/",
},
},
dynamic_rules_path: format!("{:?}", dynamic_config_file),
static_rules: {},
generation: 1u64,
}
}
);
}
#[fasync::run_singlethreaded(test)]
async fn test_no_dynamic_rules_if_no_dynamic_rules_path() {
let manager = RewriteManagerBuilder::new(None, Option::<&str>::None).await.unwrap().build();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]);
}
#[fasync::run_singlethreaded(test)]
async fn test_same_dynamic_rules_if_apply_fails() {
let path = make_rule_config(vec![]);
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
let rule0 = rule!("test0.com" => "test0.com", "/a" => "/b");
let rule1 = rule!("test1.com" => "test1.com", "/a" => "/b");
let mut manager = RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file)
.await
.unwrap()
.build();
assert_eq!(manager.list().collect::<Vec<_>>(), Vec::<&Rule>::new());
// transaction0 adds a dynamic rule
let mut transaction0 = manager.transaction();
let mut transaction1 = manager.transaction();
transaction0.add(rule0.clone());
manager.apply(transaction0).await.unwrap();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![rule0.clone()]);
// transaction1 fails to apply b/c it was created before transaction0 was applied
// the dynamic rewrite rules should be unchanged
transaction1.add(rule1.clone());
assert_matches!(manager.apply(transaction1).await, Err(CommitError::TooLate));
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![rule0.clone()]);
// transaction2 applies the rule from transaction1
let mut transaction2 = manager.transaction();
transaction2.add(rule1.clone());
manager.apply(transaction2).await.unwrap();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![rule1.clone(), rule0.clone()]);
}
#[fasync::run_singlethreaded(test)]
async fn test_apply_fails_with_no_change_if_no_dynamic_rules_path() {
let mut manager =
RewriteManagerBuilder::new(None, Option::<&str>::None).await.unwrap().build();
let mut transaction = manager.transaction();
transaction.add(rule!("test0.com" => "test0.com", "/a" => "/b"));
assert_matches!(
manager.apply(transaction).await,
Err(CommitError::DynamicConfigurationDisabled)
);
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]);
}
#[fasync::run_singlethreaded(test)]
async fn test_rewrite_works_when_data_inaccessible() {
let rule0 = rule!("test0.com" => "test0.com", "/a" => "/b");
let (builder, _) =
RewriteManagerBuilder::new(None, Some("nonemptyfilename")).await.unwrap_err();
let mut manager = builder.build();
// transaction0 adds a dynamic rule
let mut transaction0 = manager.transaction();
transaction0.add(rule0.clone());
manager.apply(transaction0).await.unwrap();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![rule0.clone()]);
}
#[fasync::run_singlethreaded(test)]
async fn test_constructor_returns_not_found_if_file_missing() {
let path = make_rule_config(vec![]);
let (dynamic_config_dir, dynamic_config_file) = temp_path_into_proxy_and_path(&path);
// Delete the config file.
path.close().unwrap();
assert_matches!(
RewriteManagerBuilder::new(dynamic_config_dir, dynamic_config_file).await,
Err((
_,
LoadRulesError::FileOpen(fuchsia_fs::node::OpenError::OpenError(
fuchsia_zircon::Status::NOT_FOUND
))
))
);
}
#[fasync::run_singlethreaded(test)]
async fn test_static_rules_return_not_found_if_file_missing() {
let path = make_rule_config(vec![]);
let (config_dir, config_file) = temp_path_into_proxy_and_path(&path);
// Delete the config file.
path.close().unwrap();
let builder = RewriteManagerBuilder::new(None, Option::<&str>::None).await.unwrap();
assert_matches!(
builder.static_rules_path(config_dir, &config_file.unwrap()).await,
Err((
_,
LoadRulesError::FileOpen(fuchsia_fs::node::OpenError::OpenError(
fuchsia_zircon::Status::NOT_FOUND
))
))
);
}
#[test]
fn test_create_rule_inspect_state_passes_through_fields() {
let inspector = inspect::Inspector::new();
let node = inspector.root().create_child("rule_node");
let rule = rule!("fuchsia.com" => "example.com", "/foo" => "/bar");
let _state = create_rule_inspect_state(&rule, node);
assert_data_tree!(
inspector,
root: {
rule_node: {
host_match: rule.host_match().to_string(),
host_replacement: rule.host_replacement().to_string(),
path_prefix_match: rule.path_prefix_match().to_string(),
path_prefix_replacement: rule.path_prefix_replacement().to_string(),
}
}
);
}
}