rpz

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

app.rs (9050B)


      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(crate) 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(crate) 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(crate) const fn block(&self) -> &SupersetSet<RpzDomain<'block>> {
     49         &self.block
     50     }
     51     /// Returns an empty `Domains`.
     52     pub(crate) 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(crate) 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(crate) 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             'block,
    133             'c: 'block,
    134             T: Into<RpzDomain<'block>> + ParsedDomain<'block, Error = FirefoxDomainErr> + Helper,
    135         >(
    136             doms: &mut Domains<'_, 'block>,
    137             files: &'c [File],
    138             summaries: &mut Vec<Summary<'c, FirefoxDomainErr>>,
    139         ) {
    140             files
    141                 .iter()
    142                 .fold((), |(), file| doms.add_block_file::<T>(file, summaries));
    143         }
    144         add_files::<Adblock<'_>>(self, &files.adblock, summaries);
    145         add_files::<DomainOnly<'_>>(self, &files.domain, summaries);
    146         add_files::<Hosts<'_>>(self, &files.hosts, summaries);
    147         add_files::<Wildcard<'_>>(self, &files.wildcard, summaries);
    148     }
    149     /// Parses each line in the files in `files` as an `RpzDomain`.
    150     /// For unblock files, the domain is added to `Domains::unblock`;
    151     /// for block files, the domain is added to `Domains::block` iff
    152     /// `Domains::unblock` does not contain a superset of it.
    153     ///
    154     /// All parsing errors are ignored.
    155     fn add_local_files<'c: 'unblock + 'block>(
    156         &mut self,
    157         files: &'c LocalFiles,
    158     ) -> Vec<Summary<'c, FirefoxDomainErr>> {
    159         let mut summaries = files.unblock.add_to_superset(&mut self.unblock);
    160         self.add_block_files(&files.block, &mut summaries);
    161         summaries
    162     }
    163     /// Writes all necessary unblock `RpzDomain`s as `<domain> CNAME rpz-passthru.`
    164     /// followed by all block `RpzDomain`s as `<domain> CNAME .`
    165     ///
    166     /// When subdomains exist, `*.<domain>` is used.
    167     ///
    168     /// When `path` is `None`, `stdout` is written to; otherwise,
    169     /// `path.unwrap().1` is written to before being renamed
    170     /// to `path.unwrap().0`.
    171     ///
    172     /// Returns the quantity of unblock lines and block lines written
    173     /// respectively.
    174     ///
    175     /// Note that by "necessary unblock" we mean that there exists
    176     /// a proper superset of it in the blocked `RpzDomain`s; as if not,
    177     /// there is no need to write it since a lack of a blocked entry
    178     /// is equivalent to an unblock entry.
    179     ///
    180     /// # Errors
    181     ///
    182     /// Returns `Error` iff `writeln` or `fs::rename` do.
    183     pub(crate) fn write(
    184         self,
    185         path: Option<(AbsFilePath<false>, AbsFilePath<false>)>,
    186     ) -> Result<(usize, usize), Error> {
    187         /// When `exclude.is_none()`, calls `Rpz::write_rpz_line` with
    188         /// `RpzAction::Nxdomain` for each `RpzDomain` in `doms`;
    189         /// otherwise calls it with `RpzAction::Passthru`
    190         /// so long as `exclude` contains a proper superset of the domain.
    191         ///
    192         /// Returns the total number of lines written.
    193         fn write_domain<W: Write>(
    194             doms: &SupersetSet<RpzDomain<'_>>,
    195             exclude: Option<&SupersetSet<RpzDomain<'_>>>,
    196             mut writer: W,
    197         ) -> Result<usize, Error> {
    198             let action = exclude.map_or(RpzAction::Nxdomain, |_| RpzAction::Passthru);
    199             doms.into_iter().try_fold(0usize, |count, rpz| {
    200                 if exclude.is_none_or(|other| other.contains_proper_superset(rpz)) {
    201                     rpz.write_to_rpz(action, &mut writer)
    202                         .map(|()| count.saturating_add(if rpz.is_subdomains() { 2 } else { 1 }))
    203                 } else {
    204                     Ok(count)
    205                 }
    206             })
    207         }
    208         path.map_or_else(
    209             || {
    210                 let mut std = io::stdout().lock();
    211                 write_domain(&self.unblock, Some(&self.block), &mut std).and_then(|unblock_count| {
    212                     write_domain(&self.block, None, std)
    213                         .map(|block_count| (unblock_count, block_count))
    214                 })
    215             },
    216             |(rpz, rpz_tmp)| {
    217                 fs::File::options()
    218                     .read(false)
    219                     .write(true)
    220                     .create_new(true)
    221                     .open(rpz_tmp.as_path())
    222                     .and_then(|file| {
    223                         write_domain(&self.unblock, Some(&self.block), &file).and_then(
    224                             |unblock_count| {
    225                                 write_domain(&self.block, None, file).and_then(|block_count| {
    226                                     fs::rename(rpz_tmp.as_path(), rpz)
    227                                         .map(|()| (unblock_count, block_count))
    228                                         .map_err(|err| {
    229                                             fs::remove_file(rpz_tmp)
    230                                                 .map_or_else(convert::identity, |()| err)
    231                                         })
    232                                 })
    233                             },
    234                         )
    235                     })
    236             },
    237         )
    238     }
    239 }