| // 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_syslog::fx_log_err, |
| fuchsia_uri::pkg_uri::PkgUri, |
| fuchsia_uri_rewrite::{Rule, RuleConfig}, |
| 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, |
| } |
| |
| #[derive(Debug, Fail)] |
| pub enum CommitError { |
| #[fail(display = "the provided rule set is based on an older generation")] |
| TooLate, |
| } |
| |
| impl RewriteManager { |
| /// Rewrite the given [PkgUri] using the first dynamic or static rewrite rule that matches and |
| /// produces a valid [PkgUri]. If no rewrite rules match or all that do produce invalid |
| /// [PkgUri]s, return the original, unmodified [PkgUri]. |
| pub fn rewrite(&self, uri: PkgUri) -> PkgUri { |
| for rule in self.list() { |
| match rule.apply(&uri) { |
| Some(Ok(res)) => { |
| return res; |
| } |
| Some(Err(err)) => { |
| fx_log_err!("re-write rule {:?} produced an invalid URI, ignoring rule", err); |
| } |
| _ => {} |
| } |
| } |
| uri |
| } |
| |
| 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); |
| } |
| Ok(()) |
| } |
| } |
| |
| /// Return an iterator through all rewrite rules in the order they should be applied to |
| /// incoming `fuchsia-pkg://` URIs. |
| pub fn list<'a>(&'a self) -> impl Iterator<Item = &'a Rule> { |
| self.dynamic_rules.iter().chain(self.static_rules.iter()) |
| } |
| } |
| |
| /// [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, |
| } |
| |
| 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>(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(), |
| }; |
| |
| 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 { |
| RewriteManager { |
| static_rules: self.static_rules, |
| dynamic_rules: self.dynamic_rules, |
| generation: 0, |
| dynamic_rules_path: self.dynamic_rules_path, |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| pub(crate) mod tests { |
| use {super::*, failure::Error, serde_json::json}; |
| |
| macro_rules! rule { |
| ($host_match:expr => $host_replacement:expr, |
| $path_prefix_match:expr => $path_prefix_replacement:expr) => { |
| fuchsia_uri_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 config = make_rule_config(vec![]); |
| |
| let manager = RewriteManagerBuilder::new(&config) |
| .unwrap() |
| .static_rules_path(&config) |
| .unwrap() |
| .build(); |
| |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]); |
| } |
| |
| #[test] |
| fn test_load_single_static_rule() { |
| 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(&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 rules = vec![rule!("fuchsia.com" => "fuchsia.com", "/rolldice" => "/rolldice")]; |
| |
| let dynamic_config = make_rule_config(rules.clone()); |
| let manager = RewriteManagerBuilder::new(&dynamic_config).unwrap().build(); |
| |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), rules); |
| } |
| |
| #[test] |
| fn test_rejects_invalid_static_config() { |
| 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(&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 dynamic_config = make_temp_file(|writer| write!(writer, "invalid")); |
| let rule = rule!("test.com" => "test.com", "/a" => "/b"); |
| |
| { |
| let (builder, _) = RewriteManagerBuilder::new(&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 manager = RewriteManagerBuilder::new(&dynamic_config).unwrap().build(); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![rule]); |
| } |
| |
| #[test] |
| 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 dynamic_config = make_rule_config(rules); |
| let manager = RewriteManagerBuilder::new(&dynamic_config).unwrap().build(); |
| |
| let uri: PkgUri = "fuchsia-pkg://fuchsia.com/c".parse().unwrap(); |
| assert_eq!(manager.rewrite(uri.clone()), uri); |
| } |
| |
| #[test] |
| fn test_rewrite_first_rule_wins() { |
| 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(&dynamic_config).unwrap().build(); |
| |
| let uri = "fuchsia-pkg://fuchsia.com/package".parse().unwrap(); |
| assert_eq!(manager.rewrite(uri), "fuchsia-pkg://fuchsia.com/remapped".parse().unwrap()); |
| } |
| |
| #[test] |
| fn test_rewrite_dynamic_rules_override_static_rules() { |
| 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(&dynamic_config) |
| .unwrap() |
| .static_rules_path(&static_config) |
| .unwrap() |
| .build(); |
| |
| let uri = "fuchsia-pkg://fuchsia.com/package".parse().unwrap(); |
| assert_eq!(manager.rewrite(uri), "fuchsia-pkg://fuchsia.com/remapped".parse().unwrap()); |
| } |
| |
| #[test] |
| fn test_rewrite_with_pending_transaction() { |
| 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(&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 uri: PkgUri = "fuchsia-pkg://fuchsia.com/a".parse().unwrap(); |
| assert_eq!(manager.rewrite(uri.clone()), "fuchsia-pkg://fuchsia.com/b".parse().unwrap()); |
| |
| manager.apply(transaction).unwrap(); |
| |
| let uri = "fuchsia-pkg://fuchsia.com/a".parse().unwrap(); |
| assert_eq!(manager.rewrite(uri), "fuchsia-pkg://fuchsia.com/c".parse().unwrap()); |
| } |
| |
| #[test] |
| 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 dynamic_config = make_rule_config(rules.clone()); |
| let mut manager = RewriteManagerBuilder::new(&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 manager = RewriteManagerBuilder::new(&dynamic_config).unwrap().build(); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), new_rules); |
| } |
| |
| #[test] |
| fn test_erase_all_dynamic_rules() { |
| 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(&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 manager = RewriteManagerBuilder::new(&dynamic_config).unwrap().build(); |
| assert_eq!(manager.list().cloned().collect::<Vec<_>>(), vec![]); |
| } |
| } |