privacy_sexy/
collection.rs

1use std::{fs::File, io, path::Path};
2
3use regex::{Captures, Regex};
4use reqwest::{blocking::get, IntoUrl};
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    util::{beautify, parse_start_end, piper},
9    OS,
10};
11
12/// Error type emitted during parsing
13#[derive(Debug)]
14pub enum ParseError {
15    /// Emitted when a function is not found, with the name of the [`FunctionData`]
16    Function(String),
17    /// Emitted when a (non-optional) parameter is not provided, with the name of the [`ParameterDefinitionData`]
18    Parameter(String),
19    /// Emitted when neither call or code are not provided, with the name of the [`ScriptData`]
20    CallCode(String),
21}
22
23/**
24### `Collection`
25
26- A collection simply defines:
27  - different categories and their scripts in a tree structure
28  - OS specific details
29- Also allows defining common [function](FunctionData)s to be used throughout the collection if
30  you'd like different scripts to share same code.
31*/
32#[derive(Debug, Serialize, Deserialize)]
33pub struct CollectionData {
34    /// - Operating system that the [Collection](CollectionData) is written for.
35    /// - 📖 See [crate](OS) enum for allowed values.
36    pub os: OS,
37    /// - Defines the scripting language that the code of other action uses.
38    pub scripting: ScriptingDefinitionData,
39    /// - Each [category](CategoryData) is rendered as different cards in card presentation.
40    /// - ❗ A [Collection](CollectionData) must consist of at least one category.
41    pub actions: Vec<CategoryData>,
42    /// - Functions are optionally defined to re-use the same code throughout different scripts.
43    pub functions: Option<Vec<FunctionData>>,
44}
45
46/// Emitted when reading [`CollectionData`] from file fails
47#[derive(Debug)]
48pub enum CollectionError {
49    /// Refer to [`io::Error`]
50    IOError(io::Error),
51    /// Refer to [`serde_yaml::Error`]
52    SerdeError(serde_yaml::Error),
53    /// Refer to [`reqwest::Error`]
54    ReqwestError(reqwest::Error),
55}
56
57impl From<io::Error> for CollectionError {
58    fn from(err: io::Error) -> Self {
59        Self::IOError(err)
60    }
61}
62
63impl From<serde_yaml::Error> for CollectionError {
64    fn from(err: serde_yaml::Error) -> Self {
65        Self::SerdeError(err)
66    }
67}
68
69impl From<reqwest::Error> for CollectionError {
70    fn from(err: reqwest::Error) -> Self {
71        Self::ReqwestError(err)
72    }
73}
74
75impl CollectionData {
76    /**
77    Reads [`CollectionData`] from file at `path`
78
79    # Errors
80
81    Returns [`CollectionError`] if:
82    - file cannot be opened OR
83    - contents cannot be deserialized into [`CollectionData`]
84    */
85    pub fn from_file(path: impl AsRef<Path>) -> Result<CollectionData, CollectionError> {
86        Ok(serde_yaml::from_reader::<File, CollectionData>(File::open(path)?)?)
87    }
88
89    /**
90    Fetches [`CollectionData`] from `url`
91
92    # Errors
93
94    Returns [`CollectionError`] if:
95    - `url` cannot be fetched OR
96    - contents cannot be deserialized into [`CollectionData`]
97    */
98    pub fn from_url(url: impl IntoUrl) -> Result<CollectionData, CollectionError> {
99        Ok(serde_yaml::from_slice::<CollectionData>(&get(url)?.bytes()?)?)
100    }
101
102    /**
103    Parses [`CollectionData`] into String
104
105    # Errors
106
107    Returns [`ParseError`] if the object is not parsable
108    */
109    pub fn parse(
110        &self,
111        names: Option<&Vec<&str>>,
112        revert: bool,
113        recommend: Option<Recommend>,
114    ) -> Result<String, ParseError> {
115        Ok(format!(
116            "{}\n\n\n{}\n\n\n{}",
117            parse_start_end(&self.scripting.start_code),
118            self.actions
119                .iter()
120                .map(|action| action.parse(names, &self.functions, self.os, revert, recommend))
121                .collect::<Result<Vec<String>, ParseError>>()?
122                .into_iter()
123                .filter(|s| !s.is_empty())
124                .collect::<Vec<String>>()
125                .join("\n\n\n"),
126            parse_start_end(&self.scripting.end_code),
127        ))
128    }
129}
130
131/**
132### `Category`
133
134- Category has a parent that has tree-like structure where it can have subcategories or subscripts.
135- It's a logical grouping of different scripts and other categories.
136*/
137#[derive(Debug, Serialize, Deserialize)]
138pub struct CategoryData {
139    /// - ❗ Category must consist of at least one subcategory or script.
140    /// - Children can be combination of scripts and subcategories.
141    pub children: Vec<CategoryOrScriptData>,
142    /// - Name of the category
143    /// - ❗ Must be unique throughout the [Collection](CollectionData)
144    pub category: String,
145    /// - Single documentation URL or list of URLs for those who wants to learn more about the script
146    /// - E.g. `https://docs.microsoft.com/en-us/windows-server/`
147    pub docs: Option<DocumentationUrlsData>,
148}
149
150impl CategoryData {
151    /**
152    Parses [`CategoryData`] into String
153
154    # Errors
155
156    Returns [`ParseError`] if the object is not parsable
157    */
158    fn parse(
159        &self,
160        names: Option<&Vec<&str>>,
161        funcs: &Option<Vec<FunctionData>>,
162        os: OS,
163        revert: bool,
164        recommend: Option<Recommend>,
165    ) -> Result<String, ParseError> {
166        let (names, recommend) = if names.map_or(false, |ns| ns.contains(&self.category.as_str())) {
167            (None, None)
168        } else {
169            (names, recommend)
170        };
171
172        Ok(self
173            .children
174            .iter()
175            .map(|child| child.parse(names, funcs, os, revert, recommend))
176            .collect::<Result<Vec<String>, ParseError>>()?
177            .into_iter()
178            .filter(|s| !s.is_empty())
179            .collect::<Vec<String>>()
180            .join("\n\n\n"))
181    }
182}
183
184/// Enum to hold possible values
185#[derive(Debug, Serialize, Deserialize)]
186#[serde(untagged)]
187pub enum CategoryOrScriptData {
188    /// Refer to [Collection](CategoryData)
189    CategoryData(CategoryData),
190    /// Refer to [Collection](ScriptData)
191    ScriptData(ScriptData),
192}
193
194impl CategoryOrScriptData {
195    /**
196    Parses [`CategoryOrScriptData`] into String
197
198    # Errors
199
200    Returns [`ParseError`] if the object is not parsable
201    */
202    fn parse(
203        &self,
204        names: Option<&Vec<&str>>,
205        funcs: &Option<Vec<FunctionData>>,
206        os: OS,
207        revert: bool,
208        recommend: Option<Recommend>,
209    ) -> Result<String, ParseError> {
210        match self {
211            CategoryOrScriptData::CategoryData(data) => data.parse(names, funcs, os, revert, recommend),
212            CategoryOrScriptData::ScriptData(data) => data.parse(names, funcs, os, revert, recommend),
213        }
214    }
215}
216
217/// - Single documentation URL or list of URLs for those who wants to learn more about the script
218/// - E.g. `https://docs.microsoft.com/en-us/windows-server/`
219#[derive(Debug, Serialize, Deserialize)]
220#[serde(untagged)]
221pub enum DocumentationUrlsData {
222    /// Multiple URLs
223    VecStrings(Vec<String>),
224    /// Single URL
225    String(String),
226}
227
228/**
229### `FunctionParameter`
230
231- Defines a parameter that function requires optionally or mandatory.
232- Its arguments are provided by a [Script](ScriptData) through a [FunctionCall](FunctionCallData).
233*/
234#[derive(Debug, Serialize, Deserialize)]
235pub struct ParameterDefinitionData {
236    /**
237    - Name of the parameters that the function has.
238    - Parameter names must be defined to be used in
239    [expressions (templating)](https://github.com/SubconsciousCompute/privacy-sexy/blob/master/src/README.md#expressions).
240    - ❗ Parameter names must be unique and include alphanumeric characters only.
241    */
242    pub name: String,
243    /**
244    - Specifies whether the caller [Script](ScriptData) must provide any value for the parameter.
245    - If set to `false` i.e. an argument value is not optional then it expects a non-empty value for the variable;
246      - Otherwise it throws.
247    - 💡 Set it to `true` if a parameter is used conditionally;
248      - Or else set it to `false` for verbosity or do not define it as default value is `false` anyway.
249    - 💡 Can be used in conjunction with
250    [`with` expression](https://github.com/SubconsciousCompute/privacy-sexy/blob/master/src/README.md#with).
251    */
252    #[serde(default)]
253    pub optional: bool,
254}
255
256/**
257### `Function`
258
259- Functions allow re-usable code throughout the defined scripts.
260- Functions are templates compiled by privacy.sexy and uses special expression expressions.
261- A function can be of two different types (just like [scripts](ScriptData)):
262  1. Inline function: a function with an inline code.
263     - Must define `code` property and optionally `revertCode` but not `call`.
264  2. Caller function: a function that calls other functions.
265     - Must define `call` property but not `code` or `revertCode`.
266- 👀 Read more on [Templating](https://github.com/SubconsciousCompute/privacy-sexy/blob/master/src/README.md) for function expressions
267    and [example usages](https://github.com/SubconsciousCompute/privacy-sexy/blob/master/src/README.md#parameter-substitution).
268*/
269#[derive(Debug, Serialize, Deserialize)]
270pub struct FunctionData {
271    /**
272    - Name of the function that scripts will use.
273    - Convention is to use camelCase, and be verbs.
274    - E.g. `uninstallStoreApp`
275    - ❗ Function names must be unique
276    */
277    pub name: String,
278    /**
279    - Batch file commands that will be executed
280    - 💡 [Expressions (templating)](https://github.com/SubconsciousCompute/privacy-sexy/blob/master/src/README.md#expressions)
281        can be used in its value
282    - 💡 If defined, best practice to also define `revertCode`
283    - ❗ If not defined `call` must be defined
284    */
285    pub code: Option<String>,
286    /**
287    - Code that'll undo the change done by `code` property.
288    - E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
289      - then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
290    - 💡 [Expressions (templating)](https://github.com/SubconsciousCompute/privacy-sexy/blob/master/src/README.md#expressions)
291        can be used in code
292    */
293    #[serde(rename = "revertCode")]
294    pub revert_code: Option<String>,
295    /**
296    - A shared function or sequence of functions to call (called in order)
297    - The parameter values that are sent can use [expressions (templating)](https://github.com/SubconsciousCompute/privacy-sexy/blob/master/src/README.md#expressions)
298    - ❗ If not defined `code` must be defined
299    */
300    pub call: Option<FunctionCallsData>,
301    /**
302    - List of parameters that function code refers to.
303    - ❗ Must be defined to be able use in [`FunctionCall`](FunctionCallData) or
304        [expressions (templating)](https://github.com/SubconsciousCompute/privacy-sexy/blob/master/src/README.md#expressions)
305    `code`: *`string`* (**required** if `call` is undefined)
306    - Batch file commands that will be executed
307    - 💡 [Expressions (templating)](https://github.com/SubconsciousCompute/privacy-sexy/blob/master/src/README.md#expressions)
308        can be used in its value
309    - 💡 If defined, best practice to also define `revertCode`
310    - ❗ If not defined `call` must be defined
311    */
312    pub parameters: Option<Vec<ParameterDefinitionData>>,
313}
314
315impl FunctionData {
316    /**
317    Parses [`FunctionData`] into String
318
319    # Errors
320
321    Returns [`ParseError`] if the object is not parsable
322    */
323    fn parse(
324        &self,
325        params: &Option<FunctionCallParametersData>,
326        funcs: &Option<Vec<FunctionData>>,
327        os: OS,
328        revert: bool,
329    ) -> Result<String, ParseError> {
330        let mut parsed = {
331            if let Some(fcd) = &self.call {
332                fcd.parse(funcs, os, revert)?
333            } else if let Some(code_string) = if revert { &self.revert_code } else { &self.code } {
334                code_string.to_string()
335            } else {
336                return Err(ParseError::CallCode(self.name.clone()));
337            }
338        };
339
340        if let Some(vec_pdd) = &self.parameters {
341            for pdd in vec_pdd {
342                parsed = match params.as_ref().and_then(|p| p.get(&pdd.name)) {
343                    Some(v) => {
344                        if pdd.optional {
345                            parsed = Regex::new(&format!(
346                                r"(?s)\{{\{{\s*with\s*\${}\s*\}}\}}\s?(.*?)\s?\{{\{{\s*end\s*\}}\}}",
347                                &pdd.name
348                            ))
349                            .unwrap()
350                            .replace_all(&parsed, |c: &Captures| {
351                                c.get(1)
352                                    .map_or("", |m| m.as_str())
353                                    .replace("{{ . ", &format!("{{{{ ${} ", &pdd.name))
354                            })
355                            .to_string();
356                        }
357
358                        Regex::new(format!(r"\{{\{{\s*\${}\s*((\|\s*\w*\s*)*)\}}\}}", &pdd.name).as_str())
359                            .unwrap()
360                            .replace_all(&parsed, |c: &Captures| {
361                                c.get(1)
362                                    .map_or("", |m| m.as_str())
363                                    .split('|')
364                                    .map(str::trim)
365                                    .filter(|p| !p.is_empty())
366                                    .fold(v.as_str().unwrap().to_string(), |v, pipe| piper(pipe.trim(), &v))
367                            })
368                    }
369                    None => {
370                        if pdd.optional {
371                            Regex::new(&format!(
372                                r"(?s)\{{\{{\s*with\s*\${}\s*\}}\}}\s?(.*?)\s?\{{\{{\s*end\s*\}}\}}",
373                                &pdd.name
374                            ))
375                            .unwrap()
376                            .replace_all(&parsed, "")
377                        } else {
378                            return Err(ParseError::Parameter(pdd.name.clone()));
379                        }
380                    }
381                }
382                .to_string();
383            }
384        }
385
386        Ok(parsed)
387    }
388}
389
390/**
391- Defines key value dictionary for each parameter and its value
392- E.g.
393
394  ```yaml
395    parameters:
396      userDefinedParameterName: parameterValue
397      # ...
398      appName: Microsoft.WindowsFeedbackHub
399  ```
400
401- 💡 [Expressions (templating)](https://github.com/SubconsciousCompute/privacy-sexy/blob/master/src/README.md#expressions)
402    can be used as parameter value
403*/
404pub type FunctionCallParametersData = serde_yaml::Value;
405
406/**
407### `FunctionCall`
408
409- Describes a single call to a function by optionally providing values to its parameters.
410- 👀 See [parameter substitution](https://github.com/SubconsciousCompute/privacy-sexy/blob/master/src/README.md#parameter-substitution)
411    for an example usage
412*/
413#[derive(Debug, Serialize, Deserialize)]
414pub struct FunctionCallData {
415    /// - Name of the function to call.
416    /// - ❗ Function with same name must defined in `functions` property of [Collection](CollectionData)
417    pub function: String,
418    /**
419    - Defines key value dictionary for each parameter and its value
420    - E.g.
421
422      ```yaml
423        parameters:
424          userDefinedParameterName: parameterValue
425          # ...
426          appName: Microsoft.WindowsFeedbackHub
427      ```
428
429    - 💡 [Expressions (templating)](https://github.com/SubconsciousCompute/privacy-sexy/blob/master/src/README.md#expressions)
430        can be used as parameter value
431    */
432    pub parameters: Option<FunctionCallParametersData>,
433}
434
435impl FunctionCallData {
436    /**
437    Parses [`FunctionCallData`] into String
438
439    # Errors
440
441    Returns [`ParseError`] if the object is not parsable
442    */
443    fn parse(&self, funcs: &Option<Vec<FunctionData>>, os: OS, revert: bool) -> Result<String, ParseError> {
444        funcs
445            .as_ref()
446            .and_then(|vec_fd| vec_fd.iter().find(|fd| fd.name == self.function))
447            .map_or(Err(ParseError::Function(self.function.clone())), |fd| {
448                fd.parse(&self.parameters, funcs, os, revert)
449            })
450    }
451}
452
453/// Possible parameters of a function call i.e. either one parameter or multiple parameters
454#[derive(Debug, Serialize, Deserialize)]
455#[serde(untagged)]
456pub enum FunctionCallsData {
457    /// Multiple Parameter
458    VecFunctionCallData(Vec<FunctionCallData>),
459    /// Single Parameter
460    FunctionCallData(FunctionCallData),
461}
462
463impl FunctionCallsData {
464    /**
465    Parses [`FunctionCallsData`] into String
466
467    # Errors
468
469    Returns [`ParseError`] if the object is not parsable
470    */
471    fn parse(&self, funcs: &Option<Vec<FunctionData>>, os: OS, revert: bool) -> Result<String, ParseError> {
472        match &self {
473            FunctionCallsData::VecFunctionCallData(vec_fcd) => Ok(vec_fcd
474                .iter()
475                .map(|fcd| fcd.parse(funcs, os, revert))
476                .collect::<Result<Vec<String>, ParseError>>()?
477                .into_iter()
478                .filter(|s| !s.is_empty())
479                .collect::<Vec<String>>()
480                .join("\n\n")),
481            FunctionCallsData::FunctionCallData(fcd) => fcd.parse(funcs, os, revert),
482        }
483    }
484}
485
486/**
487### `Script`
488
489- Script represents a single tweak.
490- A script can be of two different types (just like [functions](FunctionData)):
491  1. Inline script; a script with an inline code
492     - Must define `code` property and optionally `revertCode` but not `call`
493  2. Caller script; a script that calls other functions
494     - Must define `call` property but not `code` or `revertCode`
495- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
496*/
497#[derive(Debug, Serialize, Deserialize)]
498pub struct ScriptData {
499    /// - Name of the script
500    /// - ❗ Must be unique throughout the [Collection](CollectionData)
501    pub name: String,
502    /**
503    - Batch file commands that will be executed
504    - 💡 If defined, best practice to also define `revertCode`
505    - ❗ If not defined `call` must be defined, do not define if `call` is defined.
506    */
507    pub code: Option<String>,
508    /**
509    - Code that'll undo the change done by `code` property.
510    - E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
511      - then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
512    - ❗ Do not define if `call` is defined.
513    */
514    #[serde(rename = "revertCode")]
515    pub revert_code: Option<String>,
516    /// - A shared function or sequence of functions to call (called in order)
517    /// - ❗ If not defined `code` must be defined
518    pub call: Option<FunctionCallsData>,
519    /// - Single documentation URL or list of URLs for those who wants to learn more about the script
520    /// - E.g. `https://docs.microsoft.com/en-us/windows-server/`
521    pub docs: Option<DocumentationUrlsData>,
522    /**
523    - If not defined then the script will not be recommended
524    - If defined it can be either
525      - `standard`: Only non-breaking scripts without limiting OS functionality
526      - `strict`: Scripts that can break certain functionality in favor of privacy and security
527    */
528    pub recommend: Option<Recommend>,
529}
530
531impl ScriptData {
532    /**
533    Parses [`ScriptData`] into String
534
535    # Errors
536
537    Returns [`ParseError`] if the object is not parsable
538    */
539    fn parse(
540        &self,
541        names: Option<&Vec<&str>>,
542        funcs: &Option<Vec<FunctionData>>,
543        os: OS,
544        revert: bool,
545        recommend: Option<Recommend>,
546    ) -> Result<String, ParseError> {
547        if (recommend.is_some() && recommend > self.recommend)
548            || names.map_or(false, |n| !n.contains(&self.name.as_str()))
549        {
550            Ok(String::new())
551        } else if let Some(fcd) = &self.call {
552            Ok(beautify(&fcd.parse(funcs, os, revert)?, &self.name, os, revert))
553        } else if let Some(code_string) = if revert { &self.revert_code } else { &self.code } {
554            Ok(beautify(code_string, &self.name, os, revert))
555        } else {
556            Err(ParseError::CallCode(self.name.clone()))
557        }
558    }
559}
560
561/**
562### `ScriptingDefinition`
563
564- Defines global properties for scripting that's used throughout its parent [Collection](CollectionData).
565*/
566#[derive(Debug, Serialize, Deserialize)]
567pub struct ScriptingDefinitionData {
568    /// Name of the Script
569    pub language: String,
570    /// Optional file extension for the said script
571    #[serde(rename = "fileExtension")]
572    pub file_extension: Option<String>,
573    /**
574    - Code that'll be inserted on top of user created script.
575    - Global variables such as `$homepage`, `$version`, `$date` can be used using
576      [parameter substitution](https://github.com/SubconsciousCompute/privacy-sexy/blob/master/src/README.md#parameter-substitution)
577      code syntax such as `Welcome to {{ $homepage }}!`
578    */
579    #[serde(rename = "startCode")]
580    pub start_code: String,
581    /**
582    - Code that'll be inserted at the end of user created script.
583    - Global variables such as `$homepage`, `$version`, `$date` can be used using
584      [parameter substitution](https://github.com/SubconsciousCompute/privacy-sexy/blob/master/src/README.md#parameter-substitution)
585      code syntax such as `Welcome to {{ $homepage }}!`
586    */
587    #[serde(rename = "endCode")]
588    pub end_code: String,
589}
590
591/**
592- If not defined then the script will not be recommended
593- If defined it can be either
594  - `standard`: Only non-breaking scripts without limiting OS functionality
595  - `strict`: Scripts that can break certain functionality in favor of privacy and security
596*/
597#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, PartialOrd)]
598pub enum Recommend {
599    /// - `strict`: Scripts that can break certain functionality in favor of privacy and security
600    #[serde(rename = "strict")]
601    Strict,
602    /// - `standard`: Only non-breaking scripts without limiting OS functionality
603    #[serde(rename = "standard")]
604    Standard,
605}