rpz

Response policy zone (RPZ) file generator.
git clone https://git.philomathiclife.com/repos/rpz
Log | Files | Refs | README

app.rs (9027B)


      1 use core::convert;
      2 use rpz::dom::{
      3     Adblock, DomainOnly, FirefoxDomainErr, Hosts, ParsedDomain, RpzAction, RpzDomain, Value,
      4     Wildcard,
      5 };
      6 use rpz::file::{AbsFilePath, File, Files, Kind, LocalFiles, Summary};
      7 use std::{
      8     collections::HashMap,
      9     fs,
     10     io::{self, Error, Write},
     11 };
     12 use superset_map::SupersetSet;
     13 /// Helper that returns the `Kind` of a file.
     14 pub trait Helper {
     15     /// Returns the `Kind` of file.
     16     fn kind() -> Kind;
     17 }
     18 impl Helper for Adblock<'_> {
     19     fn kind() -> Kind {
     20         Kind::Adblock
     21     }
     22 }
     23 impl Helper for DomainOnly<'_> {
     24     fn kind() -> Kind {
     25         Kind::DomainOnly
     26     }
     27 }
     28 impl Helper for Hosts<'_> {
     29     fn kind() -> Kind {
     30         Kind::Hosts
     31     }
     32 }
     33 impl Helper for Wildcard<'_> {
     34     fn kind() -> Kind {
     35         Kind::Wildcard
     36     }
     37 }
     38 /// Container of `Domain`s to block and unblock.
     39 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
     40 pub struct Domains<'unblock, 'block> {
     41     /// `RpzDomain`s to not block.
     42     unblock: SupersetSet<RpzDomain<'unblock>>,
     43     /// `RpzDomain`s to block.
     44     block: SupersetSet<RpzDomain<'block>>,
     45 }
     46 impl<'unblock, 'block> Domains<'unblock, 'block> {
     47     /// Returns a reference to the `RpzDomain`s to block.
     48     pub const fn block(&self) -> &SupersetSet<RpzDomain<'block>> {
     49         &self.block
     50     }
     51     /// Returns an empty `Domains`.
     52     pub const fn new() -> Self {
     53         Self {
     54             unblock: SupersetSet::new(),
     55             block: SupersetSet::new(),
     56         }
     57     }
     58     /// Returns `Domains` based on `LocalFiles` along
     59     /// with a `Vec` of `Summary`.
     60     pub fn new_with<'c: 'unblock + 'block>(
     61         local: &'c LocalFiles,
     62     ) -> (Self, Vec<Summary<'c, FirefoxDomainErr>>) {
     63         let mut val = Self {
     64             unblock: SupersetSet::new(),
     65             block: SupersetSet::new(),
     66         };
     67         let summaries = val.add_local_files(local);
     68         (val, summaries)
     69     }
     70     /// Parses each line in `file` as an `RpzDomain` before
     71     /// adding it to `Domains::block` iff `Domains::unblock` does not contain
     72     /// a superset of it.
     73     ///
     74     /// All parsing errors are ignored.
     75     fn add_block_file<
     76         'c: 'block,
     77         T: Into<RpzDomain<'block>> + ParsedDomain<'block, Error = FirefoxDomainErr> + Helper,
     78     >(
     79         &mut self,
     80         file: &'c File,
     81         summaries: &mut Vec<Summary<'c, FirefoxDomainErr>>,
     82     ) {
     83         let mut summary = Summary {
     84             file,
     85             kind: T::kind(),
     86             domain_count: 0,
     87             comment_count: 0,
     88             blank_count: 0,
     89             errors: HashMap::new(),
     90         };
     91         file.data
     92             .lines()
     93             .fold((), |(), line| match T::parse_value(line) {
     94                 Ok(val) => match val {
     95                     Value::Domain(dom) => {
     96                         summary.domain_count = summary.domain_count.saturating_add(1);
     97                         let domain = dom.into();
     98                         if !self.unblock.contains_superset(&domain) {
     99                             self.block.insert(domain);
    100                         }
    101                     }
    102                     Value::Comment(_) => {
    103                         summary.comment_count = summary.comment_count.saturating_add(1);
    104                     }
    105                     Value::Blank => {
    106                         summary.blank_count = summary.blank_count.saturating_add(1);
    107                     }
    108                 },
    109                 Err(err) => {
    110                     let count = summary.errors.entry(err).or_insert(0);
    111                     *count = count.saturating_add(1);
    112                 }
    113             });
    114         summaries.push(summary);
    115     }
    116     /// Parses each line in the files in `files` as an `RpzDomain` before
    117     /// adding it to `Domains::block` iff `Domains::unblock` does not contain
    118     /// a superset of it.
    119     ///
    120     /// All parsing errors are ignored.
    121     pub fn add_block_files<'c: 'block>(
    122         &mut self,
    123         files: &'c Files,
    124         summaries: &mut Vec<Summary<'c, FirefoxDomainErr>>,
    125     ) {
    126         /// Parses each line in the `String`s in `files` as an `RpzDomain` before
    127         /// adding it to `Domains::block` iff `Domains::unblock` does not contain
    128         /// a superset of it.
    129         ///
    130         /// All parsing errors are ignored.
    131         fn add_files<
    132             'unblock,
    133             'block,
    134             'c: 'block,
    135             T: Into<RpzDomain<'block>> + ParsedDomain<'block, Error = FirefoxDomainErr> + Helper,
    136         >(
    137             doms: &mut Domains<'unblock, 'block>,
    138             files: &'c [File],
    139             summaries: &mut Vec<Summary<'c, FirefoxDomainErr>>,
    140         ) {
    141             files
    142                 .iter()
    143                 .fold((), |(), file| doms.add_block_file::<T>(file, summaries));
    144         }
    145         add_files::<Adblock<'_>>(self, &files.adblock, summaries);
    146         add_files::<DomainOnly<'_>>(self, &files.domain, summaries);
    147         add_files::<Hosts<'_>>(self, &files.hosts, summaries);
    148         add_files::<Wildcard<'_>>(self, &files.wildcard, summaries);
    149     }
    150     /// Parses each line in the files in `files` as an `RpzDomain`.
    151     /// For unblock files, the domain is added to `Domains::unblock`;
    152     /// for block files, the domain is added to `Domains::block` iff
    153     /// `Domains::unblock` does not contain a superset of it.
    154     ///
    155     /// All parsing errors are ignored.
    156     fn add_local_files<'c: 'unblock + 'block>(
    157         &mut self,
    158         files: &'c LocalFiles,
    159     ) -> Vec<Summary<'c, FirefoxDomainErr>> {
    160         let mut summaries = files.unblock.add_to_superset(&mut self.unblock);
    161         self.add_block_files(&files.block, &mut summaries);
    162         summaries
    163     }
    164     /// Writes all necessary unblock `RpzDomain`s as `<domain> CNAME rpz-passthru.`
    165     /// followed by all block `RpzDomain`s as `<domain> CNAME .`
    166     ///
    167     /// When subdomains exist, `*.<domain>` is used.
    168     ///
    169     /// When `path` is `None`, `stdout` is written to; otherwise,
    170     /// `path.unwrap().1` is written to before being renamed
    171     /// to `path.unwrap().0`.
    172     ///
    173     /// Returns the quantity of unblock lines and block lines written
    174     /// respectively.
    175     ///
    176     /// Note that by "necessary unblock" we mean that there exists
    177     /// a proper superset of it in the blocked `RpzDomain`s; as if not,
    178     /// there is no need to write it since a lack of a blocked entry
    179     /// is equivalent to an unblock entry.
    180     ///
    181     /// # Errors
    182     ///
    183     /// Returns `Error` iff `writeln` or `fs::rename` do.
    184     pub fn write(
    185         self,
    186         path: Option<(AbsFilePath<false>, AbsFilePath<false>)>,
    187     ) -> Result<(usize, usize), Error> {
    188         /// When `exclude.is_none()`, calls `Rpz::write_rpz_line` with
    189         /// `RpzAction::Nxdomain` for each `RpzDomain` in `doms`;
    190         /// otherwise calls it with `RpzAction::Passthru`
    191         /// so long as `exclude` contains a proper superset of the domain.
    192         ///
    193         /// Returns the total number of lines written.
    194         fn write_domain<W: Write>(
    195             doms: &SupersetSet<RpzDomain<'_>>,
    196             exclude: Option<&SupersetSet<RpzDomain<'_>>>,
    197             mut writer: W,
    198         ) -> Result<usize, Error> {
    199             let action = exclude.map_or(RpzAction::Nxdomain, |_| RpzAction::Passthru);
    200             doms.into_iter().try_fold(0usize, |count, rpz| {
    201                 if exclude.map_or(true, |other| other.contains_proper_superset(rpz)) {
    202                     rpz.write_to_rpz(action, &mut writer)
    203                         .map(|()| count.saturating_add(if rpz.is_subdomains() { 2 } else { 1 }))
    204                 } else {
    205                     Ok(count)
    206                 }
    207             })
    208         }
    209         path.map_or_else(
    210             || {
    211                 let mut std = io::stdout().lock();
    212                 write_domain(&self.unblock, Some(&self.block), &mut std).and_then(|unblock_count| {
    213                     write_domain(&self.block, None, std)
    214                         .map(|block_count| (unblock_count, block_count))
    215                 })
    216             },
    217             |(rpz, rpz_tmp)| {
    218                 fs::File::options()
    219                     .read(false)
    220                     .write(true)
    221                     .create_new(true)
    222                     .open(rpz_tmp.as_path())
    223                     .and_then(|file| {
    224                         write_domain(&self.unblock, Some(&self.block), &file).and_then(
    225                             |unblock_count| {
    226                                 write_domain(&self.block, None, file).and_then(|block_count| {
    227                                     fs::rename(rpz_tmp.as_path(), rpz)
    228                                         .map(|()| (unblock_count, block_count))
    229                                         .map_err(|err| {
    230                                             fs::remove_file(rpz_tmp)
    231                                                 .map_or_else(convert::identity, |()| err)
    232                                         })
    233                                 })
    234                             },
    235                         )
    236                     })
    237             },
    238         )
    239     }
    240 }