blob: 523ae37ff9fa7db29448f42c830acb8af363e1ae [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 {
failure::Fail,
fuchsia_inspect::{self as inspect, Property},
fuchsia_syslog::fx_log_err,
fuchsia_url::pkg_url::PkgUrl,
fuchsia_url_rewrite::{Rule, RuleConfig, RuleInspectState},
std::{
collections::VecDeque,
fs::{self, File},
io,
path::{Path, PathBuf},
},
};
/// [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 {
static_rules: Vec<Rule>,
dynamic_rules: Vec<Rule>,
generation: u32,
dynamic_rules_path: PathBuf,
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,
dynamic_rules_path_property: fuchsia_inspect::StringProperty,
node: inspect::Node,
}
#[derive(Debug, Fail)]
pub enum CommitError {
#[fail(display = "the provided rule set is based on an older generation")]
TooLate,
}
impl RewriteManager {
/// Rewrite the given [PkgUrl] using the first dynamic or static rewrite rule that matches and
/// produces a valid [PkgUrl]. If no rewrite rules match or all that do produce invalid
/// [PkgUrl]s, return the original, unmodified [PkgUrl].
pub fn rewrite(&self, url: PkgUrl) -> PkgUrl {
for rule in self.list() {
match rule.apply(&url) {
Some(Ok(res)) => {
return res;
}
Some(Err(err)) => {
fx_log_err!("re-write rule {:?} produced an invalid URL, ignoring rule", err);
}
_ => {}
}
}
url
}
fn save(&mut self) -> io::Result<()> {
let config = RuleConfig::Version1(std::mem::replace(&mut self.dynamic_rules, vec![]));
let result = (|| {
let mut temp_path = self.dynamic_rules_path.clone().into_os_string();
temp_path.push(".new");
let temp_path = PathBuf::from(temp_path);
{
let f = File::create(&temp_path)?;
serde_json::to_writer(f, &config)?;
};
fs::rename(temp_path, &self.dynamic_rules_path)
})();
let RuleConfig::Version1(rules) = config;
self.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] iff no other
/// [RewriteRuleStates] have been applied since `transaction` was cloned from this
/// [RewriteManager].
pub fn apply(&mut self, transaction: Transaction) -> Result<(), CommitError> {
if self.generation != transaction.generation {
Err(CommitError::TooLate)
} else {
self.dynamic_rules = transaction.dynamic_rules.into();
self.generation += 1;
// FIXME(kevinwells) synchronous I/O in an async context
if let Err(err) = self.save() {
fx_log_err!("error while saving dynamic rewrite rules: {}", err);
}
self.update_inspect_objects();
Ok(())
}
}
/// Finds the first rule, if any, that remaps all of `fuchsia.com`.
pub fn amber_source_name(&self) -> Option<String> {
self.list().filter_map(|rule| rule.fuchsia_replacement()).next()
}
/// Return an iterator through all rewrite rules in the order they should be applied to
/// incoming `fuchsia-pkg://` URLs.
pub fn list<'a>(&'a self) -> impl Iterator<Item = &'a Rule> {
self.dynamic_rules.iter().chain(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(rule.create_inspect_state(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,
);
}
}
/// [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);
}
#[cfg(test)]
fn list_dynamic<'a>(&'a self) -> impl Iterator<Item = &'a Rule> {
self.dynamic_rules.iter()
}
}
/// [RewriteManagerBuilder] constructs a [RewriteManager], optionally initializing it with [Rule]s
/// passed in directly or loaded out of the filesystem.
#[derive(Debug, PartialEq, Eq)]
pub struct RewriteManagerBuilder {
static_rules: Vec<Rule>,
dynamic_rules: Vec<Rule>,
dynamic_rules_path: PathBuf,
inspect_node: inspect::Node,
}
impl RewriteManagerBuilder {
/// 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 fn new<T>(
inspect_node: inspect::Node,
dynamic_rules_path: T,
) -> Result<Self, (Self, io::Error)>
where
T: Into<PathBuf>,
{
let mut builder = RewriteManagerBuilder {
static_rules: vec![],
dynamic_rules: vec![],
dynamic_rules_path: dynamic_rules_path.into(),
inspect_node,
};
match Self::load_rules(&builder.dynamic_rules_path) {
Ok(rules) => {
builder.dynamic_rules = rules;
Ok(builder)
}
Err(err) => Err((builder, err)),
}
}
/// 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 fn static_rules_path<T>(mut self, path: T) -> Result<Self, (Self, io::Error)>
where
T: AsRef<Path>,
{
match Self::load_rules(path) {
Ok(rules) => {
self.static_rules = rules;
Ok(self)
}
Err(err) => Err((self, err)),
}
}
fn load_rules<T>(path: T) -> Result<Vec<Rule>, io::Error>
where
T: AsRef<Path>,
{
let f = File::open(path.as_ref())?;
let RuleConfig::Version1(rules) = serde_json::from_reader(f)?;
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
}
/// 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",
&self.dynamic_rules_path.display().to_string(),
),
node: self.inspect_node,
};
let mut rw = RewriteManager {
static_rules: self.static_rules,
dynamic_rules: self.dynamic_rules,
generation: 0,
dynamic_rules_path: self.dynamic_rules_path,
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::*, failure::Error, fuchsia_inspect::assert_inspect_tree, serde_json::json};
macro_rules! rule {
($host_match:expr => $host_replacement:expr,
$path_prefix_match:expr => $path_prefix_replacement:expr) => {
fuchsia_url_rewrite::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 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))
}
#[test]
fn test_empty_configs() {
let inspector = fuchsia_inspect::Inspector::new();
let node = inspector.root().create_child("top-level-node");
let config = make_rule_config(vec![]);
let manager = RewriteManagerBuilder::new(node, &config)
.unwrap()
.static_rules_path(&config)
.unwrap()
.build();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]);
}
#[test]
fn test_load_single_static_rule() {
let inspector = fuchsia_inspect::Inspector::new();
let node = inspector.root().create_child("top-level-node");
let rules = vec![rule!("fuchsia.com" => "fuchsia.com", "/rolldice" => "/rolldice")];
let dynamic_config = make_rule_config(vec![]);
let static_config = make_rule_config(rules.clone());
let manager = RewriteManagerBuilder::new(node, &dynamic_config)
.unwrap()
.static_rules_path(&static_config)
.unwrap()
.build();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules);
}
#[test]
fn test_load_single_dynamic_rule() {
let inspector = fuchsia_inspect::Inspector::new();
let node = inspector.root().create_child("top-level-node");
let rules = vec![rule!("fuchsia.com" => "fuchsia.com", "/rolldice" => "/rolldice")];
let dynamic_config = make_rule_config(rules.clone());
let manager = RewriteManagerBuilder::new(node, &dynamic_config).unwrap().build();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules);
}
#[test]
fn test_rejects_invalid_static_config() {
let inspector = fuchsia_inspect::Inspector::new();
let node = inspector.root().create_child("top-level-node");
let rules = vec![rule!("fuchsia.com" => "fuchsia.com", "/a" => "/b")];
let dynamic_config = make_rule_config(rules.clone());
let static_config = make_temp_file(|writer| {
write!(
writer,
"{}",
json!({
"version": "1",
"content": {} // should be an array
})
)
});
let (builder, _) = RewriteManagerBuilder::new(node, &dynamic_config)
.unwrap()
.static_rules_path(&static_config)
.unwrap_err();
let manager = builder.build();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules);
}
#[test]
fn test_recovers_from_invalid_dynamic_config() {
let inspector = fuchsia_inspect::Inspector::new();
let node = inspector.root().create_child("top-level-node");
let dynamic_config = make_temp_file(|writer| write!(writer, "invalid"));
let rule = rule!("test.com" => "test.com", "/a" => "/b");
{
let (builder, _) = RewriteManagerBuilder::new(node, &dynamic_config).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).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 node = inspector.root().create_child("top-level-node");
let manager = RewriteManagerBuilder::new(node, &dynamic_config).unwrap().build();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![rule]);
}
#[test]
fn test_rewrite_identity_if_no_rules_match() {
let inspector = fuchsia_inspect::Inspector::new();
let node = inspector.root().create_child("top-level-node");
let rules = vec![
rule!("fuchsia.com" => "fuchsia.com", "/a" => "/aa"),
rule!("fuchsia.com" => "fuchsia.com", "/b" => "/bb"),
];
let dynamic_config = make_rule_config(rules);
let manager = RewriteManagerBuilder::new(node, &dynamic_config).unwrap().build();
let url: PkgUrl = "fuchsia-pkg://fuchsia.com/c".parse().unwrap();
assert_eq!(manager.rewrite(url.clone()), url);
}
#[test]
fn test_rewrite_first_rule_wins() {
let inspector = fuchsia_inspect::Inspector::new();
let node = inspector.root().create_child("top-level-node");
let rules = vec![
rule!("fuchsia.com" => "fuchsia.com", "/package" => "/remapped"),
rule!("fuchsia.com" => "fuchsia.com", "/package" => "/incorrect"),
];
let dynamic_config = make_rule_config(rules);
let manager = RewriteManagerBuilder::new(node, &dynamic_config).unwrap().build();
let url = "fuchsia-pkg://fuchsia.com/package".parse().unwrap();
assert_eq!(manager.rewrite(url), "fuchsia-pkg://fuchsia.com/remapped".parse().unwrap());
}
#[test]
fn test_rewrite_dynamic_rules_override_static_rules() {
let inspector = fuchsia_inspect::Inspector::new();
let node = inspector.root().create_child("top-level-node");
let dynamic_config = make_rule_config(vec![
rule!("fuchsia.com" => "fuchsia.com", "/package" => "/remapped"),
]);
let static_config = make_rule_config(vec![
rule!("fuchsia.com" => "fuchsia.com", "/package" => "/incorrect"),
]);
let manager = RewriteManagerBuilder::new(node, &dynamic_config)
.unwrap()
.static_rules_path(&static_config)
.unwrap()
.build();
let url = "fuchsia-pkg://fuchsia.com/package".parse().unwrap();
assert_eq!(manager.rewrite(url), "fuchsia-pkg://fuchsia.com/remapped".parse().unwrap());
}
#[test]
fn test_rewrite_with_pending_transaction() {
let inspector = fuchsia_inspect::Inspector::new();
let node = inspector.root().create_child("top-level-node");
let override_rule = rule!("fuchsia.com" => "fuchsia.com", "/a" => "/c");
let dynamic_config =
make_rule_config(vec![rule!("fuchsia.com" => "fuchsia.com", "/a" => "/b")]);
let mut manager = RewriteManagerBuilder::new(node, &dynamic_config).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: PkgUrl = "fuchsia-pkg://fuchsia.com/a".parse().unwrap();
assert_eq!(manager.rewrite(url.clone()), "fuchsia-pkg://fuchsia.com/b".parse().unwrap());
manager.apply(transaction).unwrap();
let url = "fuchsia-pkg://fuchsia.com/a".parse().unwrap();
assert_eq!(manager.rewrite(url), "fuchsia-pkg://fuchsia.com/c".parse().unwrap());
}
#[test]
fn test_commit_additional_rule() {
let inspector = fuchsia_inspect::Inspector::new();
let node = inspector.root().create_child("top-level-node");
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 dynamic_config = make_rule_config(rules.clone());
let mut manager = RewriteManagerBuilder::new(node, &dynamic_config).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
assert_eq!(manager.apply(transaction).unwrap(), ());
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), new_rules);
// Ensure new rules are persisted to the dynamic config file
let node = inspector.root().create_child("top-level-node");
let manager = RewriteManagerBuilder::new(node, &dynamic_config).unwrap().build();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), new_rules);
}
#[test]
fn test_erase_all_dynamic_rules() {
let inspector = fuchsia_inspect::Inspector::new();
let node = inspector.root().create_child("top-level-node");
let rules = vec![
rule!("fuchsia.com" => "fuchsia.com", "/rolldice" => "/rolldice"),
rule!("fuchsia.com" => "fuchsia.com", "/rolldice/" => "/rolldice/"),
];
let dynamic_config = make_rule_config(rules.clone());
let mut manager = RewriteManagerBuilder::new(node, &dynamic_config).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![]);
assert_eq!(manager.apply(transaction).unwrap(), ());
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]);
let node = inspector.root().create_child("top-level-node");
let manager = RewriteManagerBuilder::new(node, &dynamic_config).unwrap().build();
assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]);
}
#[test]
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_config = make_rule_config(dynamic_rules.clone());
let static_rules =
vec![rule!("example.com" => "example.org", "/this_throwdice" => "/that_throwdice")];
let static_config = make_rule_config(static_rules.clone());
let _manager = RewriteManagerBuilder::new(node, &dynamic_config)
.unwrap()
.static_rules_path(&static_config)
.unwrap()
.build();
assert_inspect_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: dynamic_config.to_str().unwrap().to_string(),
static_rules: {
"0": {
host_match: "example.com",
host_replacement: "example.org",
path_prefix_match: "/this_throwdice",
path_prefix_replacement: "/that_throwdice",
},
},
generation: 0u64,
}
}
);
}
#[test]
fn test_transaction_updates_inspect() {
let inspector = fuchsia_inspect::Inspector::new();
let node = inspector.root().create_child("rewrite_manager");
let dynamic_config = make_rule_config(vec![]);
let mut manager = RewriteManagerBuilder::new(node, &dynamic_config).unwrap().build();
assert_inspect_tree!(
inspector,
root: {
rewrite_manager: {
dynamic_rules: {},
dynamic_rules_path: dynamic_config.to_str().unwrap().to_string(),
static_rules: {},
generation: 0u64,
}
}
);
let mut transaction = manager.transaction();
transaction
.add(rule!("example.com" => "example.org", "/this_rolldice/" => "/that_rolldice/"));
manager.apply(transaction).unwrap();
assert_inspect_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: dynamic_config.to_str().unwrap().to_string(),
static_rules: {},
generation: 1u64,
}
}
);
}
}