1use std::{
2 collections::HashMap,
3 io::{Read, Seek},
4};
5
6use content_inspector::inspect;
7use handlebars::Handlebars;
8use serde::Serialize;
9
10use crate::{
11 config::DTConfig,
12 error::{Error as AppError, Result},
13};
14
15#[allow(unused_variables)]
16pub trait Register
19where
20 Self: Sized,
21{
22 fn register_helpers(self) -> Result<Self> {
26 unimplemented!()
27 }
28 fn load(self, config: &DTConfig) -> Result<Self> {
34 unimplemented!()
35 }
36 fn update<S: Serialize>(&mut self, name: &str, ctx: &S) -> Result<()> {
51 unimplemented!()
52 }
53 fn get(&self, name: &str) -> Result<Vec<u8>> {
55 unimplemented!()
56 }
57}
58
59#[derive(Debug, Default)]
61pub struct Registry<'reg> {
62 pub env: Handlebars<'reg>,
64 pub content: HashMap<String, Vec<u8>>,
66}
67
68impl Register for Registry<'_> {
69 fn register_helpers(self) -> Result<Self> {
70 let mut render_env = self.env;
71
72 render_env.register_helper("get_mine", Box::new(helpers::get_mine));
73 render_env.register_helper("if_user", Box::new(helpers::if_user));
74 render_env.register_helper("if_uid", Box::new(helpers::if_uid));
75 render_env.register_helper("if_host", Box::new(helpers::if_host));
76 render_env.register_helper("unless_user", Box::new(helpers::unless_user));
77 render_env.register_helper("unless_uid", Box::new(helpers::unless_uid));
78 render_env.register_helper("unless_host", Box::new(helpers::unless_host));
79 render_env.register_helper("if_os", Box::new(helpers::if_os));
80 render_env.register_helper("unless_os", Box::new(helpers::unless_os));
81
82 Ok(Self {
83 env: render_env,
84 ..self
85 })
86 }
87
88 fn load(self, config: &DTConfig) -> Result<Self> {
89 let mut registry = self;
90 for group in &config.local {
91 for s in &group.sources {
92 let name = s.to_string_lossy();
93
94 if group.is_renderable() {
95 registry.update(&name, &config.context)?;
96 } else {
97 log::trace!(
98 "'{}' is from an unrenderable group '{}'",
99 s.display(),
100 group.name,
101 );
102 }
103 }
104 }
105 Ok(registry)
106 }
107
108 fn update<S: Serialize>(&mut self, name: &str, ctx: &S) -> Result<()> {
109 let mut f = std::fs::File::open(name)?;
110 f.seek(std::io::SeekFrom::Start(0))?;
111 let mut indicator = vec![0; std::cmp::min(1024, f.metadata()?.len() as usize)];
112 f.read_exact(&mut indicator)?;
113 if inspect(&indicator).is_text() {
114 self.env
115 .register_template_string(name, std::fs::read_to_string(name)?)?;
116 self.content
117 .insert(name.to_owned(), self.env.render(name, ctx)?.into());
118 } else {
119 log::trace!("'{}' has binary contents, skipping rendering", name);
120 self.content.insert(name.to_owned(), std::fs::read(name)?);
121 }
122 Ok(())
123 }
124
125 fn get(&self, name: &str) -> Result<Vec<u8>> {
126 match self.content.get(name) {
127 Some(content) => Ok(content.to_owned()),
128 None => Err(AppError::TemplatingError(format!(
129 "The template specified by '{}' is not known",
130 name,
131 ))),
132 }
133 }
134}
135
136pub mod helpers {
140 #[cfg(not(test))]
141 use {
142 gethostname::gethostname,
143 sys_info::linux_os_release,
144 users::{get_current_uid, get_current_username},
145 };
146
147 #[cfg(test)]
148 use crate::utils::testing::{
149 get_current_uid, get_current_username, gethostname, linux_os_release,
150 };
151
152 use handlebars::{
153 Context, Handlebars, Helper, HelperResult, JsonRender, Output, RenderContext, RenderError,
154 Renderable,
155 };
156
157 pub fn get_mine(
170 h: &Helper,
171 _: &Handlebars,
172 _: &Context,
173 _rc: &mut RenderContext,
174 out: &mut dyn Output,
175 ) -> HelperResult {
176 let docmsg = format!(
177 r#"
178Inline helper `{0}`:
179 expected 0 or 2 arguments, 1 found
180
181 Usage:
182 1. {{{{ {0} }}}}
183 Renders current machine's hostname
184
185 2. {{{{ {0} <map> <default-value> }}}}
186 Gets value of <map>.$CURRENT_HOSTNAME, falls back to <default-value>"#,
187 h.name(),
188 );
189
190 let map = match h.param(0) {
191 Some(map) => map.value(),
192 None => {
193 out.write(&gethostname().to_string_lossy().to_string())?;
194 return Ok(());
195 }
196 };
197 let default_content = match h.param(1) {
198 Some(content) => content.value(),
199 None => {
200 return Err(RenderError::new(docmsg));
201 }
202 };
203
204 let content = match map.get(gethostname().to_string_lossy().to_string()) {
205 Some(content) => content.render(),
206 None => default_content.render(),
207 };
208
209 out.write(&content)?;
210
211 Ok(())
212 }
213
214 pub fn if_user<'reg, 'rc>(
236 h: &Helper<'reg, 'rc>,
237 r: &'reg Handlebars<'reg>,
238 ctx: &'rc Context,
239 rc: &mut RenderContext<'reg, 'rc>,
240 out: &mut dyn Output,
241 ) -> HelperResult {
242 let docmsg = format!(
243 r#"
244Block helper `#{0}`:
245 expected exactly 1 argument, {1} found
246
247 Usage:
248 1. {{{{#{0} "foo,bar"}}}}..baz..{{{{/{0}}}}}
249 Renders `..baz..` only if current user's username is either "foo"
250 or "bar"
251
252 2. {{{{#{0} "foo"}}}}..baz..{{{{else}}}}..qux..{{{{/{0}}}}}
253 Renders `..baz..` only if current user's username is "foo", renders
254 `..qux..` only if current user's username is NOT "foo"
255
256 3. {{{{#{0} some.array}}}}..foo..{{{{/{0}}}}}
257 Renders `..foo..` only if current user's username is exactly one of
258 the values from the templating variable `some.array` (defined in
259 the config file's `[context]` section)"#,
260 h.name(),
261 h.params().len(),
262 );
263
264 if h.params().len() > 1 {
265 return Err(RenderError::new(docmsg));
266 }
267
268 let allowed_usernames: Vec<String> = match h.param(0) {
269 Some(v) => {
270 if v.value().is_array() {
271 v.value()
272 .as_array()
273 .unwrap()
274 .iter()
275 .map(|elem| elem.render())
276 .collect()
277 } else {
278 v.value()
279 .render()
280 .split(',')
281 .map(|u| u.trim().to_owned())
282 .collect()
283 }
284 }
285 None => {
286 return Err(RenderError::new(docmsg));
287 }
288 };
289
290 let current_username = get_current_username()
291 .unwrap()
292 .to_string_lossy()
293 .to_string();
294 if !allowed_usernames.is_empty() {
295 if allowed_usernames.contains(¤t_username) {
296 log::debug!(
297 "Current username '{}' matches allowed usernames '{:?}'",
298 current_username,
299 allowed_usernames,
300 );
301 h.template().map(|t| t.render(r, ctx, rc, out));
302 } else {
303 log::debug!(
304 "Current username '{}' does not match allowed usernames {:?}",
305 current_username,
306 allowed_usernames,
307 );
308 h.inverse().map(|t| t.render(r, ctx, rc, out));
309 }
310 } else {
311 return Err(RenderError::new(format!(
312 "no username(s) supplied for matching in helper {}",
313 h.name(),
314 )));
315 }
316 Ok(())
317 }
318
319 pub fn unless_user<'reg, 'rc>(
343 h: &Helper<'reg, 'rc>,
344 r: &'reg Handlebars<'reg>,
345 ctx: &'rc Context,
346 rc: &mut RenderContext<'reg, 'rc>,
347 out: &mut dyn Output,
348 ) -> HelperResult {
349 let docmsg = format!(
350 r#"
351Block helper `#{0}`:
352 expected exactly 1 argument, {1} found
353
354 Usage:
355 1. {{{{#{0} "foo,bar"}}}}..baz..{{{{/{0}}}}}
356 Renders `..baz..` only if current user's username is neither "foo"
357 nor "bar"
358
359 2. {{{{#{0} "foo"}}}}..baz..{{{{else}}}}..qux..{{{{/{0}}}}}
360 Renders `..baz..` only if current user's username is NOT "foo",
361 renders `..qux..` only if current user's username is "foo"
362
363 3. {{{{#{0} some.array}}}}..foo..{{{{/{0}}}}}
364 Renders `..foo..` only if current user's username is none of the
365 values from the templating variable `some.array` (defined in the
366 config file's `[context]` section)"#,
367 h.name(),
368 h.params().len(),
369 );
370
371 if h.params().len() > 1 {
372 return Err(RenderError::new(docmsg));
373 }
374
375 let disallowed_usernames: Vec<String> = match h.param(0) {
376 Some(v) => {
377 if v.value().is_array() {
378 v.value()
379 .as_array()
380 .unwrap()
381 .iter()
382 .map(|elem| elem.render())
383 .collect()
384 } else {
385 v.value()
386 .render()
387 .split(',')
388 .map(|u| u.trim().to_owned())
389 .collect()
390 }
391 }
392 None => {
393 return Err(RenderError::new(docmsg));
394 }
395 };
396
397 let current_username: String = get_current_username()
398 .unwrap()
399 .to_string_lossy()
400 .to_string();
401 if !disallowed_usernames.is_empty() {
402 if disallowed_usernames.contains(¤t_username) {
403 log::debug!(
404 "Current username '{}' matches disallowed usernames '{:?}'",
405 current_username,
406 disallowed_usernames,
407 );
408 h.inverse().map(|t| t.render(r, ctx, rc, out));
409 } else {
410 log::debug!(
411 "Current username '{}' does not match disallowed usernames {:?}",
412 current_username,
413 disallowed_usernames,
414 );
415 h.template().map(|t| t.render(r, ctx, rc, out));
416 }
417 } else {
418 return Err(RenderError::new(format!(
419 "no username(s) supplied for matching in helper {}",
420 h.name(),
421 )));
422 }
423 Ok(())
424 }
425
426 pub fn if_uid<'reg, 'rc>(
447 h: &Helper<'reg, 'rc>,
448 r: &'reg Handlebars<'reg>,
449 ctx: &'rc Context,
450 rc: &mut RenderContext<'reg, 'rc>,
451 out: &mut dyn Output,
452 ) -> HelperResult {
453 let docmsg = format!(
454 r#"
455Block helper `#{0}`:
456 expected exactly 1 argument, {1} found
457
458 Usage:
459 1. {{{{#{0} "1000,1001"}}}}..foo..{{{{/{0}}}}}
460 Renders `..foo..` only if current user's effective uid is either
461 `1000` or `1001`"
462
463 2. {{{{#{0} 0}}}}..foo..{{{{else}}}}..bar..{{{{/{0}}}}}
464 Renders `..foo..` only if current user's effective uid is `0`,
465 renders `..bar..` if current user's effective uid is not `0`
466
467 3. {{{{#{0} some.array}}}}..foo..{{{{/{0}}}}}
468 Renders `..foo..` only if current user's effective uid is exactly
469 one of the values from the templating variable `some.array`
470 (defined in the config file's `[context]` section)"#,
471 h.name(),
472 h.params().len(),
473 );
474
475 if h.params().len() > 1 {
476 return Err(RenderError::new(docmsg));
477 }
478
479 let allowed_uids: Vec<u32> = match h.param(0) {
480 Some(v) => {
481 if v.value().is_array() {
482 v.value()
483 .as_array()
484 .unwrap()
485 .iter()
486 .map(|elem| elem.render().parse())
487 .collect::<Result<Vec<_>, _>>()?
488 } else {
489 v.value()
490 .render()
491 .split(',')
492 .map(|uid| uid.parse())
493 .collect::<Result<Vec<_>, _>>()?
494 }
495 }
496 None => {
497 return Err(RenderError::new(docmsg));
498 }
499 };
500
501 let current_uid = get_current_uid();
502 if !allowed_uids.is_empty() {
503 if allowed_uids.contains(¤t_uid) {
504 log::debug!(
505 "Current uid '{}' matches allowed uids '{:?}'",
506 current_uid,
507 allowed_uids,
508 );
509 h.template().map(|t| t.render(r, ctx, rc, out));
510 } else {
511 log::debug!(
512 "Current uid '{}' does not match allowed uids {:?}",
513 current_uid,
514 allowed_uids,
515 );
516 h.inverse().map(|t| t.render(r, ctx, rc, out));
517 }
518 } else {
519 return Err(RenderError::new(format!(
520 "no uid(s) supplied for matching in helper {}",
521 h.name(),
522 )));
523 }
524 Ok(())
525 }
526
527 pub fn unless_uid<'reg, 'rc>(
549 h: &Helper<'reg, 'rc>,
550 r: &'reg Handlebars<'reg>,
551 ctx: &'rc Context,
552 rc: &mut RenderContext<'reg, 'rc>,
553 out: &mut dyn Output,
554 ) -> HelperResult {
555 let docmsg = format!(
556 r#"
557Block helper `#{0}`:
558 expected exactly 1 argument, {1} found
559
560 Usage:
561 1. {{{{#{0} "1000,1001"}}}}..foo..{{{{/{0}}}}}
562 Renders `..foo..` only if current user's effective uid is neither
563 `1000` nor `1001`"
564
565 2. {{{{#{0} 0}}}}..foo..{{{{else}}}}..bar..{{{{/{0}}}}}
566 Renders `..foo..` only if current user's effective uid is NOT `0`,
567 renders `..bar..` if current user's effective uid is `0`
568
569 3. {{{{#{0} some.array}}}}..foo..{{{{/{0}}}}}`
570 Renders `..foo..` only if current user's effective uid is none of
571 the values from the templating variable `some.array` (defined in
572 the config file's `[context]` section)"#,
573 h.name(),
574 h.params().len(),
575 );
576
577 if h.params().len() > 1 {
578 return Err(RenderError::new(docmsg));
579 }
580
581 let disallowed_uids: Vec<u32> = match h.param(0) {
582 Some(v) => {
583 if v.value().is_array() {
584 v.value()
585 .as_array()
586 .unwrap()
587 .iter()
588 .map(|elem| elem.render().parse())
589 .collect::<Result<Vec<_>, _>>()?
590 } else {
591 v.value()
592 .render()
593 .split(',')
594 .map(|uid| uid.parse())
595 .collect::<Result<Vec<_>, _>>()?
596 }
597 }
598 None => {
599 return Err(RenderError::new(docmsg));
600 }
601 };
602
603 let current_uid = get_current_uid();
604 if !disallowed_uids.is_empty() {
605 if disallowed_uids.contains(¤t_uid) {
606 log::debug!(
607 "Current uid '{}' matches disallowed uids '{:?}'",
608 current_uid,
609 disallowed_uids,
610 );
611 h.inverse().map(|t| t.render(r, ctx, rc, out));
612 } else {
613 log::debug!(
614 "Current uid '{}' does not match disallowed uids '{:?}'",
615 current_uid,
616 disallowed_uids,
617 );
618 h.template().map(|t| t.render(r, ctx, rc, out));
619 }
620 } else {
621 return Err(RenderError::new(format!(
622 "no uid(s) supplied for matching in helper {}",
623 h.name(),
624 )));
625 }
626 Ok(())
627 }
628
629 pub fn if_host<'reg, 'rc>(
650 h: &Helper<'reg, 'rc>,
651 r: &'reg Handlebars<'reg>,
652 ctx: &'rc Context,
653 rc: &mut RenderContext<'reg, 'rc>,
654 out: &mut dyn Output,
655 ) -> HelperResult {
656 let docmsg = format!(
657 r#"
658Block helper `#{0}`:
659 expected exactly 1 argument, {1} found
660
661 Usage:
662 1. {{{{#{0} "foo,bar"}}}}..bar..{{{{/{0}}}}}
663 Renders `..bar..` only if current machine's hostname is either
664 "foo" or "bar"
665
666 2. {{{{#{0} "foo"}}}}..baz..{{{{else}}}}..qux..{{{{/{0}}}}}
667 Renders `..baz..` only if current machine's hostname is "foo",
668 renders `..qux..` only if current user's username is NOT "foo"
669
670 3. {{{{#{0} some.array}}}}..foo..{{{{/{0}}}}}
671 Renders `..foo..` only if current machine's hostname is exactly one
672 of the values from the templating variable `some.array` (defined in
673 the config file's `[context]` section)"#,
674 h.name(),
675 h.params().len(),
676 );
677
678 if h.params().len() > 1 {
679 return Err(RenderError::new(docmsg));
680 }
681
682 let allowed_hostnames: Vec<String> = match h.param(0) {
683 Some(v) => {
684 if v.value().is_array() {
685 v.value()
686 .as_array()
687 .unwrap()
688 .iter()
689 .map(|elem| elem.render())
690 .collect::<Vec<_>>()
691 } else {
692 v.value()
693 .render()
694 .split(',')
695 .map(|h| h.trim().to_owned())
696 .collect()
697 }
698 }
699 None => {
700 return Err(RenderError::new(docmsg));
701 }
702 };
703
704 let current_hostname = gethostname().to_string_lossy().to_string();
705 if !allowed_hostnames.is_empty() {
706 if allowed_hostnames.contains(¤t_hostname) {
707 log::debug!(
708 "Current hostname '{}' matches allowed hostnames '{:?}'",
709 current_hostname,
710 allowed_hostnames,
711 );
712 h.template().map(|t| t.render(r, ctx, rc, out));
713 } else {
714 log::debug!(
715 "Current hostname '{}' does not match allowed hostnames '{:?}'",
716 current_hostname,
717 allowed_hostnames,
718 );
719 h.inverse().map(|t| t.render(r, ctx, rc, out));
720 }
721 } else {
722 return Err(RenderError::new(format!(
723 "no hostname(s) supplied for matching in helper {}",
724 h.name(),
725 )));
726 }
727 Ok(())
728 }
729
730 pub fn unless_host<'reg, 'rc>(
752 h: &Helper<'reg, 'rc>,
753 r: &'reg Handlebars<'reg>,
754 ctx: &'rc Context,
755 rc: &mut RenderContext<'reg, 'rc>,
756 out: &mut dyn Output,
757 ) -> HelperResult {
758 let docmsg = format!(
759 r#"
760Block helper `#{0}`:
761 expected exactly 1 argument, {1} found
762
763 Usage:
764 1. {{{{#{0} "foo,bar"}}}}..bar..{{{{/{0}}}}}
765 Renders `..bar..` only if current machine's hostname is neither
766 "foo" nor "bar"
767
768 2. {{{{#{0} "foo"}}}}..baz..{{{{else}}}}..qux..{{{{/{0}}}}}
769 Renders `..baz..` only if current machine's hostname is NOT "foo",
770 renders `..qux..` only if current user's username is "foo"
771
772 3. {{{{#{0} some.array}}}}..foo..{{{{/{0}}}}}
773 Renders `..foo..` only if current machine's hostname is none of the
774 values from the templating variable `some.array` (defined in the
775 config file's `[context]` section)"#,
776 h.name(),
777 h.params().len(),
778 );
779
780 if h.params().len() != 1 {
781 return Err(RenderError::new(docmsg));
782 }
783
784 let disallowed_hostnames: Vec<String> = match h.param(0) {
785 Some(v) => {
786 if v.value().is_array() {
787 v.value()
788 .as_array()
789 .unwrap()
790 .iter()
791 .map(|elem| elem.render())
792 .collect::<Vec<_>>()
793 } else {
794 v.value()
795 .render()
796 .split(',')
797 .map(|h| h.trim().to_owned())
798 .collect()
799 }
800 }
801 None => {
802 return Err(RenderError::new(docmsg));
803 }
804 };
805
806 let current_hostname = gethostname().to_string_lossy().to_string();
807 if !disallowed_hostnames.is_empty() {
808 if disallowed_hostnames.contains(¤t_hostname) {
809 log::debug!(
810 "Current hostname '{}' matches disallowed hostnames '{:?}'",
811 current_hostname,
812 disallowed_hostnames,
813 );
814 h.inverse().map(|t| t.render(r, ctx, rc, out));
815 } else {
816 log::debug!(
817 "Current hostname '{}' does not match disallowed hostnames '{:?}'",
818 current_hostname,
819 disallowed_hostnames,
820 );
821 h.template().map(|t| t.render(r, ctx, rc, out));
822 }
823 } else {
824 return Err(RenderError::new(format!(
825 "no hostname(s) supplied for matching in helper {}",
826 h.name(),
827 )));
828 }
829 Ok(())
830 }
831
832 pub fn if_os<'reg, 'rc>(
853 h: &Helper<'reg, 'rc>,
854 r: &'reg Handlebars<'reg>,
855 ctx: &'rc Context,
856 rc: &mut RenderContext<'reg, 'rc>,
857 out: &mut dyn Output,
858 ) -> HelperResult {
859 let docmsg = format!(
860 r#"
861Block helper `#{0}`:
862 expected exactly 2 arguments, {1} found
863
864 Usage:
865 1. {{{{#{0} "PRETTY_NAME" "foo,bar"}}}}..baz..{{{{/{0}}}}}
866 Renders `..baz..` only if current machine's PRETTY_NAME is either
867 "foo" or "bar"
868
869 2. {{{{#{0} "id" "foo"}}}}..baz..{{{{else}}}}..qux..{{{{/{0}}}}}
870 Renders `..baz..` only if current machine's ID is "foo", renders
871 `..qux..` only if current user's ID is NOT "foo"
872
873 3. {{{{#{0} "build_id" some.array}}}}..foo..{{{{/{0}}}}}
874 Renders `..foo..` only if current machine's BUILD_ID is exactly one
875 of the values from the templating variable `some.array` (defined in
876 the config file's `[context]` section)"#,
877 h.name(),
878 h.params().len(),
879 );
880
881 if h.params().len() != 2 {
882 return Err(RenderError::new(docmsg));
883 }
884
885 if let Some(key) = h.param(0) {
886 let os_rel_info = match linux_os_release() {
887 Ok(info) => info,
888 Err(msg) => return Err(RenderError::new(msg.to_string())),
889 };
890 let query: &str = &key.value().render().to_uppercase();
891 let value = match query {
892 "ID" => os_rel_info.id,
894 "ID_LIKE" => os_rel_info.id_like,
895 "NAME" => os_rel_info.name,
896 "PRETTY_NAME" => os_rel_info.pretty_name,
897 "VERSION" => os_rel_info.version,
898 "VERSION_ID" => os_rel_info.version_id,
899 "VERSION_CODENAME" => os_rel_info.version_codename,
900 "ANSI_COLOR" => os_rel_info.ansi_color,
901 "LOGO" => os_rel_info.logo,
902 "CPE_NAME" => os_rel_info.cpe_name,
903 "BUILD_ID" => os_rel_info.build_id,
904 "VARIANT" => os_rel_info.variant,
905 "VARIANT_ID" => os_rel_info.variant_id,
906 "HOME_URL" => os_rel_info.home_url,
907 "DOCUMENTATION_URL" => os_rel_info.documentation_url,
908 "SUPPORT_URL" => os_rel_info.support_url,
909 "BUG_REPORT_URL" => os_rel_info.bug_report_url,
910 "PRIVACY_POLICY_URL" => os_rel_info.privacy_policy_url,
911 _ => None,
912 };
913 if value == None {
914 log::warn!(
915 "/etc/os-release does not seem to provide '{}', see man:os-release(5) for more information",
916 query,
917 );
918 h.inverse().map(|t| t.render(r, ctx, rc, out));
919 return Ok(());
920 }
921 let value = value.unwrap();
922
923 let allowed_values: Vec<String> = match h.param(1) {
924 Some(v) => {
925 if v.value().is_array() {
926 v.value()
927 .as_array()
928 .unwrap()
929 .iter()
930 .map(|elem| elem.render())
931 .collect::<Vec<_>>()
932 } else {
933 v.value()
934 .render()
935 .split(',')
936 .map(|h| h.trim().to_owned())
937 .collect()
938 }
939 }
940 None => {
941 return Err(RenderError::new(docmsg));
942 }
943 };
944
945 if !allowed_values.is_empty() {
946 if allowed_values.contains(&value) {
947 log::debug!(
948 "Query result '{}' matches allowed value '{:?}'",
949 value,
950 allowed_values,
951 );
952 h.template().map(|t| t.render(r, ctx, rc, out));
953 } else {
954 log::debug!(
955 "Query result '{}' does not match allowed value '{:?}'",
956 value,
957 allowed_values,
958 );
959 h.inverse().map(|t| t.render(r, ctx, rc, out));
960 }
961 } else {
962 return Err(RenderError::new(format!(
963 "no value(s) supplied for matching in helper {}",
964 h.name(),
965 )));
966 }
967 } else {
968 return Err(RenderError::new(docmsg));
969 }
970
971 Ok(())
972 }
973
974 pub fn unless_os<'reg, 'rc>(
996 h: &Helper<'reg, 'rc>,
997 r: &'reg Handlebars<'reg>,
998 ctx: &'rc Context,
999 rc: &mut RenderContext<'reg, 'rc>,
1000 out: &mut dyn Output,
1001 ) -> HelperResult {
1002 let docmsg = format!(
1003 r#"
1004Block helper `#{0}`:
1005 expected exactly 2 arguments, {1} found
1006
1007 Usage:
1008 1. {{{{#{0} "PRETTY_NAME" "foo,bar"}}}}..baz..{{{{/{0}}}}}
1009 Renders `..baz..` only if current machine's PRETTY_NAME is neither
1010 "foo" nor "bar"
1011
1012 2. {{{{#{0} "id" "foo"}}}}..baz..{{{{else}}}}..qux..{{{{/{0}}}}}
1013 Renders `..baz..` only if current machine's ID is NOT "foo",
1014 renders `..qux..` only if current user's ID is "foo"
1015
1016 3. {{{{#{0} "build_id" some.array}}}}..foo..{{{{/{0}}}}}
1017 Renders `..foo..` only if current machine's BUILD_ID is none of the
1018 values from the templating variable `some.array` (defined in the
1019 config file's `[context]` section)"#,
1020 h.name(),
1021 h.params().len(),
1022 );
1023
1024 if h.params().len() != 2 {
1025 return Err(RenderError::new(docmsg));
1026 }
1027
1028 if let Some(key) = h.param(0) {
1029 let os_rel_info = match linux_os_release() {
1030 Ok(info) => info,
1031 Err(msg) => return Err(RenderError::new(msg.to_string())),
1032 };
1033 let query: &str = &key.value().render().to_uppercase();
1034 let value = match query {
1035 "ID" => os_rel_info.id,
1037 "ID_LIKE" => os_rel_info.id_like,
1038 "NAME" => os_rel_info.name,
1039 "PRETTY_NAME" => os_rel_info.pretty_name,
1040 "VERSION" => os_rel_info.version,
1041 "VERSION_ID" => os_rel_info.version_id,
1042 "VERSION_CODENAME" => os_rel_info.version_codename,
1043 "ANSI_COLOR" => os_rel_info.ansi_color,
1044 "LOGO" => os_rel_info.logo,
1045 "CPE_NAME" => os_rel_info.cpe_name,
1046 "BUILD_ID" => os_rel_info.build_id,
1047 "VARIANT" => os_rel_info.variant,
1048 "VARIANT_ID" => os_rel_info.variant_id,
1049 "HOME_URL" => os_rel_info.home_url,
1050 "DOCUMENTATION_URL" => os_rel_info.documentation_url,
1051 "SUPPORT_URL" => os_rel_info.support_url,
1052 "BUG_REPORT_URL" => os_rel_info.bug_report_url,
1053 "PRIVACY_POLICY_URL" => os_rel_info.privacy_policy_url,
1054 _ => None,
1055 };
1056 if value == None {
1057 log::warn!(
1058 "/etc/os-release does not seem to provide '{}', see man:os-release(5) for more information",
1059 query,
1060 );
1061 h.inverse().map(|t| t.render(r, ctx, rc, out));
1062 return Ok(());
1063 }
1064 let value = value.unwrap();
1065
1066 let disallowed_values: Vec<String> = match h.param(1) {
1067 Some(v) => {
1068 if v.value().is_array() {
1069 v.value()
1070 .as_array()
1071 .unwrap()
1072 .iter()
1073 .map(|elem| elem.render())
1074 .collect::<Vec<_>>()
1075 } else {
1076 v.value()
1077 .render()
1078 .split(',')
1079 .map(|h| h.trim().to_owned())
1080 .collect()
1081 }
1082 }
1083 None => {
1084 return Err(RenderError::new(docmsg));
1085 }
1086 };
1087
1088 if !disallowed_values.is_empty() {
1089 if disallowed_values.contains(&value) {
1090 log::debug!(
1091 "Query result '{}' matches disallowed value '{:?}'",
1092 value,
1093 disallowed_values,
1094 );
1095 h.inverse().map(|t| t.render(r, ctx, rc, out));
1096 } else {
1097 log::debug!(
1098 "Query result '{}' does not match disallowed value '{:?}'",
1099 value,
1100 disallowed_values,
1101 );
1102 h.template().map(|t| t.render(r, ctx, rc, out));
1103 }
1104 } else {
1105 return Err(RenderError::new(format!(
1106 "no value(s) supplied for matching in helper {}",
1107 h.name(),
1108 )));
1109 }
1110 } else {
1111 return Err(RenderError::new(docmsg));
1112 }
1113
1114 Ok(())
1115 }
1116}
1117
1118