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}