1use std::collections::BTreeMap;
4use std::fmt::{self, Debug, Display};
5use std::ops::Range;
6use std::result::Result as StdResult;
7use std::str::FromStr;
8
9use derive_more::From;
10use safelog::DisplayRedacted as _;
11use thiserror::Error;
12use tor_error::{Bug, internal, into_internal};
13use tor_hscrypto::pk::{HSID_ONION_SUFFIX, HsId, HsIdParseError};
14use tor_hscrypto::time::TimePeriod;
15use tor_persist::hsnickname::HsNickname;
16use tor_persist::slug::Slug;
17
18use crate::{ArtiPath, ArtiPathSyntaxError};
19
20#[macro_use]
22pub mod derive;
23
24#[derive(Clone, Debug, PartialEq, Eq, Hash, From, derive_more::Display)]
26#[non_exhaustive]
27pub enum KeyPath {
28 Arti(ArtiPath),
30 CTor(CTorPath),
32}
33
34#[derive(Clone, Debug, PartialEq, Eq, Hash, From)]
36pub struct ArtiPathRange(pub(crate) Range<usize>);
37
38impl ArtiPath {
39 pub fn matches(&self, pat: &KeyPathPattern) -> Option<Vec<ArtiPathRange>> {
60 use KeyPathPattern::*;
61
62 let pattern: &str = match pat {
63 Arti(pat) => pat.as_ref(),
64 _ => return None,
65 };
66
67 glob_match::glob_match_with_captures(pattern, self.as_ref())
68 .map(|res| res.into_iter().map(|r| r.into()).collect())
69 }
70}
71
72impl KeyPath {
73 pub fn matches(&self, pat: &KeyPathPattern) -> bool {
90 use KeyPathPattern::*;
91
92 match (self, pat) {
93 (KeyPath::Arti(p), Arti(_)) => p.matches(pat).is_some(),
94 (KeyPath::CTor(p), CTor(pat)) if p == pat => true,
95 _ => false,
96 }
97 }
98
99 pub fn arti(&self) -> Option<&ArtiPath> {
103 match self {
104 KeyPath::Arti(arti) => Some(arti),
105 KeyPath::CTor(_) => None,
106 }
107 }
108
109 pub fn ctor(&self) -> Option<&CTorPath> {
111 match self {
112 KeyPath::Arti(_) => None,
113 KeyPath::CTor(ctor) => Some(ctor),
114 }
115 }
116}
117
118pub trait KeySpecifierPattern {
125 fn new_any() -> Self
127 where
128 Self: Sized;
129
130 fn arti_pattern(&self) -> Result<KeyPathPattern, Bug>;
133}
134
135#[cfg(feature = "experimental-api")]
141pub trait CertSpecifierPattern {
142 type SubjectKeySpecifierPattern: KeySpecifierPattern;
148
149 fn new_any() -> Self
155 where
156 Self: Sized;
157
158 fn arti_pattern(&self) -> Result<KeyPathPattern, Bug>;
161}
162
163#[derive(Debug, Clone, thiserror::Error)]
170#[non_exhaustive]
171pub enum KeyPathError {
172 #[error("{err}")]
174 Arti {
175 path: ArtiPath,
177 err: ArtiPathError,
179 },
180
181 #[error("{err}")]
183 CTor {
184 path: CTorPath,
186 err: CTorPathError,
188 },
189
190 #[error("Internal error")]
192 Bug(#[from] tor_error::Bug),
193}
194
195#[derive(Debug, Clone, thiserror::Error)]
197#[non_exhaustive]
198pub enum ArtiPathError {
199 #[error("Path does not match expected pattern")]
201 PatternNotMatched,
202
203 #[error("ArtiPath is invalid")]
205 InvalidArtiPath(ArtiPathSyntaxError),
206
207 #[error("invalid string value for element of key path")]
215 InvalidKeyPathComponentValue {
216 #[source]
218 error: InvalidKeyPathComponentValue,
219 key: String,
223 value: Slug,
225 },
226
227 #[error("Internal error")]
229 Bug(#[from] tor_error::Bug),
230}
231
232#[derive(Debug, Clone, PartialEq, thiserror::Error)]
235#[non_exhaustive]
236pub enum CTorPathError {
237 #[error("C Tor path cannot be converted to {0}")]
239 KeySpecifierMismatch(String),
240
241 #[error("Key specifier {0} does not have a C Tor path")]
244 MissingCTorPath(String),
245}
246
247#[derive(Error, Clone, Debug)]
253#[non_exhaustive]
254pub enum InvalidKeyPathComponentValue {
255 #[error("{0}")]
264 Slug(String),
265
266 #[error("Internal error")]
271 Bug(#[from] tor_error::Bug),
272}
273
274#[derive(Debug, Clone, PartialEq, derive_builder::Builder, amplify::Getters)]
281pub struct KeyPathInfo {
282 summary: String,
286 role: String,
292 #[builder(default, setter(custom))]
299 extra_info: BTreeMap<String, String>,
300}
301
302impl KeyPathInfo {
303 pub fn builder() -> KeyPathInfoBuilder {
305 KeyPathInfoBuilder::default()
306 }
307}
308
309impl KeyPathInfoBuilder {
310 pub fn set_all_extra_info(
314 &mut self,
315 all_extra_info: impl Iterator<Item = (String, String)>,
316 ) -> &mut Self {
317 self.extra_info = Some(all_extra_info.collect());
318 self
319 }
320
321 pub fn extra_info(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
325 let extra_info = self.extra_info.get_or_insert(Default::default());
326 extra_info.insert(key.into(), value.into());
327 self
328 }
329}
330
331pub trait KeyPathInfoExtractor: Send + Sync {
336 fn describe(&self, path: &KeyPath) -> StdResult<KeyPathInfo, KeyPathError>;
338}
339
340#[macro_export]
342macro_rules! register_key_info_extractor {
343 ($kv:expr) => {{
344 $crate::inventory::submit!(&$kv as &dyn $crate::KeyPathInfoExtractor);
345 }};
346}
347
348#[derive(Clone, Debug, PartialEq, Eq, Hash)]
368#[non_exhaustive]
369pub enum KeyPathPattern {
370 Arti(String),
372 CTor(CTorPath),
374}
375
376#[derive(Clone, Debug, PartialEq, Eq, Hash, derive_more::Display)] #[non_exhaustive]
379pub enum CTorPath {
380 #[display("HsClientDescEncKeypair({})", hs_id.display_unredacted())]
389 HsClientDescEncKeypair {
390 hs_id: HsId,
392 },
393 #[display("hs_ed25519_public_key")]
395 HsIdPublicKey {
396 nickname: HsNickname,
398 },
399 #[display("hs_ed25519_secret_key")]
401 HsIdKeypair {
402 nickname: HsNickname,
404 },
405}
406
407pub trait KeySpecifier {
411 fn arti_path(&self) -> StdResult<ArtiPath, ArtiPathUnavailableError>;
415
416 fn ctor_path(&self) -> Option<CTorPath>;
421
422 fn keypair_specifier(&self) -> Option<Box<dyn KeySpecifier>>;
425}
426
427pub trait KeySpecifierComponent {
440 fn to_slug(&self) -> Result<Slug, Bug>;
442 fn from_slug(s: &Slug) -> StdResult<Self, InvalidKeyPathComponentValue>
444 where
445 Self: Sized;
446 fn fmt_pretty(&self, f: &mut fmt::Formatter) -> fmt::Result;
450}
451
452#[derive(Error, Debug, Clone)]
457#[non_exhaustive]
458pub enum ArtiPathUnavailableError {
459 #[error("Internal error")]
461 Bug(#[from] tor_error::Bug),
462
463 #[error("ArtiPath unavailable")]
468 ArtiPathUnavailable,
469}
470
471impl KeySpecifier for ArtiPath {
472 fn arti_path(&self) -> StdResult<ArtiPath, ArtiPathUnavailableError> {
473 Ok(self.clone())
474 }
475
476 fn ctor_path(&self) -> Option<CTorPath> {
477 None
478 }
479
480 fn keypair_specifier(&self) -> Option<Box<dyn KeySpecifier>> {
481 None
482 }
483}
484
485impl KeySpecifier for CTorPath {
486 fn arti_path(&self) -> StdResult<ArtiPath, ArtiPathUnavailableError> {
487 Err(ArtiPathUnavailableError::ArtiPathUnavailable)
488 }
489
490 fn ctor_path(&self) -> Option<CTorPath> {
491 Some(self.clone())
492 }
493
494 fn keypair_specifier(&self) -> Option<Box<dyn KeySpecifier>> {
495 None
496 }
497}
498
499impl KeySpecifier for KeyPath {
500 fn arti_path(&self) -> StdResult<ArtiPath, ArtiPathUnavailableError> {
501 match self {
502 KeyPath::Arti(p) => p.arti_path(),
503 KeyPath::CTor(p) => p.arti_path(),
504 }
505 }
506
507 fn ctor_path(&self) -> Option<CTorPath> {
508 match self {
509 KeyPath::Arti(p) => p.ctor_path(),
510 KeyPath::CTor(p) => p.ctor_path(),
511 }
512 }
513
514 fn keypair_specifier(&self) -> Option<Box<dyn KeySpecifier>> {
515 None
516 }
517}
518
519impl KeySpecifierComponent for TimePeriod {
520 fn to_slug(&self) -> Result<Slug, Bug> {
521 Slug::new(format!(
522 "{}_{}_{}",
523 self.interval_num(),
524 self.length(),
525 self.epoch_offset_in_sec()
526 ))
527 .map_err(into_internal!("TP formatting went wrong"))
528 }
529
530 fn from_slug(s: &Slug) -> StdResult<Self, InvalidKeyPathComponentValue>
531 where
532 Self: Sized,
533 {
534 use itertools::Itertools;
535
536 let s = s.to_string();
537 #[allow(clippy::redundant_closure)] let err_ctx = |e: &str| InvalidKeyPathComponentValue::Slug(e.to_string());
539 let (interval, len, offset) = s
540 .split('_')
541 .collect_tuple()
542 .ok_or_else(|| err_ctx("invalid number of subcomponents"))?;
543
544 let length = len.parse().map_err(|_| err_ctx("invalid length"))?;
545 let interval_num = interval
546 .parse()
547 .map_err(|_| err_ctx("invalid interval_num"))?;
548 let offset_in_sec = offset
549 .parse()
550 .map_err(|_| err_ctx("invalid offset_in_sec"))?;
551
552 Ok(TimePeriod::from_parts(length, interval_num, offset_in_sec))
553 }
554
555 fn fmt_pretty(&self, f: &mut fmt::Formatter) -> fmt::Result {
556 Display::fmt(&self, f)
557 }
558}
559
560pub trait KeySpecifierComponentViaDisplayFromStr: Display + FromStr {}
568impl<T: KeySpecifierComponentViaDisplayFromStr> KeySpecifierComponent for T {
569 fn to_slug(&self) -> Result<Slug, Bug> {
570 self.to_string()
571 .try_into()
572 .map_err(into_internal!("Display generated bad Slug"))
573 }
574 fn from_slug(s: &Slug) -> Result<Self, InvalidKeyPathComponentValue>
575 where
576 Self: Sized,
577 {
578 s.as_str()
579 .parse()
580 .map_err(|_| internal!("slug cannot be parsed as component").into())
581 }
582 fn fmt_pretty(&self, f: &mut fmt::Formatter) -> fmt::Result {
583 Display::fmt(self, f)
584 }
585}
586
587impl KeySpecifierComponentViaDisplayFromStr for HsNickname {}
588
589impl KeySpecifierComponent for HsId {
590 fn to_slug(&self) -> StdResult<Slug, Bug> {
591 let hsid = self.display_unredacted().to_string();
595 let hsid_slug = hsid
596 .strip_suffix(HSID_ONION_SUFFIX)
597 .ok_or_else(|| internal!("HsId Display impl missing .onion suffix?!"))?;
598 hsid_slug
599 .to_owned()
600 .try_into()
601 .map_err(into_internal!("Display generated bad Slug"))
602 }
603
604 fn from_slug(s: &Slug) -> StdResult<Self, InvalidKeyPathComponentValue>
605 where
606 Self: Sized,
607 {
608 let onion = format!("{}{HSID_ONION_SUFFIX}", s.as_str());
617
618 onion
619 .parse()
620 .map_err(|e: HsIdParseError| InvalidKeyPathComponentValue::Slug(e.to_string()))
621 }
622
623 fn fmt_pretty(&self, f: &mut fmt::Formatter) -> fmt::Result {
624 Display::fmt(&self.display_redacted(), f)
625 }
626}
627
628struct KeySpecifierComponentPrettyHelper<'c>(&'c dyn KeySpecifierComponent);
630
631impl Display for KeySpecifierComponentPrettyHelper<'_> {
632 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
633 KeySpecifierComponent::fmt_pretty(self.0, f)
634 }
635}
636
637pub trait KeyCertificateSpecifier {
656 fn cert_denotators(&self) -> Vec<&dyn KeySpecifierComponent>;
665 fn subject_key_specifier(&self) -> &dyn KeySpecifier;
667}
668
669impl<T: KeyCertificateSpecifier + ?Sized> KeySpecifier for T {
670 fn arti_path(&self) -> StdResult<ArtiPath, ArtiPathUnavailableError> {
671 let subject_key_arti_path = self
672 .subject_key_specifier()
673 .arti_path()
674 .map_err(|_| internal!("subject key does not have an ArtiPath?!"))?;
675
676 let path =
677 ArtiPath::from_path_and_denotators(subject_key_arti_path, &self.cert_denotators())
678 .map_err(into_internal!("invalid certificate specifier"))?;
679
680 Ok(path)
681 }
682
683 fn ctor_path(&self) -> Option<CTorPath> {
684 None
686 }
687
688 fn keypair_specifier(&self) -> Option<Box<dyn KeySpecifier>> {
689 None
690 }
691}
692
693pub trait CTorKeySpecifier: KeySpecifier + Sized {
698 fn ctor_path(&self) -> Option<CTorPath>;
702
703 fn from_ctor_path(path: CTorPath) -> Result<Self, CTorPathError>;
708}
709
710#[cfg(test)]
711mod test {
712 #![allow(clippy::bool_assert_comparison)]
714 #![allow(clippy::clone_on_copy)]
715 #![allow(clippy::dbg_macro)]
716 #![allow(clippy::mixed_attributes_style)]
717 #![allow(clippy::print_stderr)]
718 #![allow(clippy::print_stdout)]
719 #![allow(clippy::single_char_pattern)]
720 #![allow(clippy::unwrap_used)]
721 #![allow(clippy::unchecked_time_subtraction)]
722 #![allow(clippy::useless_vec)]
723 #![allow(clippy::needless_pass_by_value)]
724 use super::*;
726
727 use crate::test_utils::check_key_specifier;
728 use derive_deftly::Deftly;
729 use humantime::parse_rfc3339;
730 use itertools::Itertools;
731 use serde::{Deserialize, Serialize};
732 use std::fmt::Debug;
733 use std::time::Duration;
734
735 impl KeySpecifierComponentViaDisplayFromStr for usize {}
736 impl KeySpecifierComponentViaDisplayFromStr for String {}
737
738 impl KeySpecifierComponentViaDisplayFromStr for bool {}
741
742 fn test_time_period() -> TimePeriod {
743 TimePeriod::new(
744 Duration::from_secs(86400),
745 parse_rfc3339("2020-09-15T00:00:00Z").unwrap(),
746 Duration::from_secs(3600),
747 )
748 .unwrap()
749 }
750
751 #[test]
752 fn pretty_time_period() {
753 let tp = test_time_period();
754 assert_eq!(
755 KeySpecifierComponentPrettyHelper(&tp).to_string(),
756 "#18519 2020-09-14T01:00:00Z..+24:00",
757 );
758 }
759
760 #[test]
761 fn serde() {
762 #[derive(Serialize, Deserialize, Debug)]
766 struct T {
767 n: Slug,
768 }
769 let j = serde_json::from_str(r#"{ "n": "x" }"#).unwrap();
770 let t: T = serde_json::from_value(j).unwrap();
771 assert_eq!(&t.n.to_string(), "x");
772
773 assert_eq!(&serde_json::to_string(&t).unwrap(), r#"{"n":"x"}"#);
774
775 let j = serde_json::from_str(r#"{ "n": "!" }"#).unwrap();
776 let e = serde_json::from_value::<T>(j).unwrap_err();
777 assert!(
778 e.to_string()
779 .contains("character '!' (U+0021) is not allowed"),
780 "wrong msg {e:?}"
781 );
782 }
783
784 #[test]
785 fn define_key_specifier_with_fields_and_denotator() {
786 let tp = test_time_period();
787
788 #[derive(Deftly, Debug, PartialEq)]
789 #[derive_deftly(KeySpecifier)]
790 #[deftly(prefix = "encabulator")]
791 #[deftly(role = "marzlevane")]
792 #[deftly(summary = "test key")]
793 struct TestSpecifier {
794 kind: String,
796 base: String,
797 casing: String,
798 #[deftly(denotator)]
799 count: usize,
800 #[deftly(denotator)]
801 tp: TimePeriod,
802 }
803
804 let key_spec = TestSpecifier {
805 kind: "hydrocoptic".into(),
806 base: "waneshaft".into(),
807 casing: "logarithmic".into(),
808 count: 6,
809 tp,
810 };
811
812 check_key_specifier(
813 &key_spec,
814 "encabulator/hydrocoptic/waneshaft/logarithmic/marzlevane+6+18519_1440_3600",
815 );
816
817 let info = TestSpecifierInfoExtractor
818 .describe(&KeyPath::Arti(key_spec.arti_path().unwrap()))
819 .unwrap();
820
821 assert_eq!(
822 format!("{info:#?}"),
823 r##"
824KeyPathInfo {
825 summary: "test key",
826 role: "marzlevane",
827 extra_info: {
828 "base": "waneshaft",
829 "casing": "logarithmic",
830 "count": "6",
831 "kind": "hydrocoptic",
832 "tp": "#18519 2020-09-14T01:00:00Z..+24:00",
833 },
834}
835 "##
836 .trim()
837 );
838 }
839
840 #[test]
841 fn define_key_specifier_no_fields() {
842 #[derive(Deftly, Debug, PartialEq)]
843 #[derive_deftly(KeySpecifier)]
844 #[deftly(prefix = "encabulator")]
845 #[deftly(role = "marzlevane")]
846 #[deftly(summary = "test key")]
847 struct TestSpecifier {}
848
849 let key_spec = TestSpecifier {};
850
851 check_key_specifier(&key_spec, "encabulator/marzlevane");
852
853 assert_eq!(
854 TestSpecifierPattern {}.arti_pattern().unwrap(),
855 KeyPathPattern::Arti("encabulator/marzlevane".into())
856 );
857 }
858
859 #[test]
860 fn define_key_specifier_with_denotator() {
861 #[derive(Deftly, Debug, PartialEq)]
862 #[derive_deftly(KeySpecifier)]
863 #[deftly(prefix = "encabulator")]
864 #[deftly(role = "marzlevane")]
865 #[deftly(summary = "test key")]
866 struct TestSpecifier {
867 #[deftly(denotator)]
868 count: usize,
869 }
870
871 let key_spec = TestSpecifier { count: 6 };
872
873 check_key_specifier(&key_spec, "encabulator/marzlevane+6");
874
875 assert_eq!(
876 TestSpecifierPattern { count: None }.arti_pattern().unwrap(),
877 KeyPathPattern::Arti("encabulator/marzlevane+*".into())
878 );
879 }
880
881 #[test]
882 fn define_key_specifier_with_fields() {
883 #[derive(Deftly, Debug, PartialEq)]
884 #[derive_deftly(KeySpecifier)]
885 #[deftly(prefix = "encabulator")]
886 #[deftly(role = "fan")]
887 #[deftly(summary = "test key")]
888 struct TestSpecifier {
889 casing: String,
890 bearings: String,
892 }
893
894 let key_spec = TestSpecifier {
895 casing: "logarithmic".into(),
896 bearings: "spurving".into(),
897 };
898
899 check_key_specifier(&key_spec, "encabulator/logarithmic/spurving/fan");
900
901 assert_eq!(
902 TestSpecifierPattern {
903 casing: Some("logarithmic".into()),
904 bearings: Some("prefabulating".into()),
905 }
906 .arti_pattern()
907 .unwrap(),
908 KeyPathPattern::Arti("encabulator/logarithmic/prefabulating/fan".into())
909 );
910
911 let ctor_path = CTorPath::HsIdPublicKey {
912 nickname: HsNickname::from_str("foo").unwrap(),
913 };
914
915 assert_eq!(
916 TestSpecifier::from_ctor_path(ctor_path).unwrap_err(),
917 CTorPathError::MissingCTorPath("TestSpecifier".into()),
918 );
919 }
920
921 #[test]
922 fn define_key_specifier_with_multiple_denotators() {
923 #[derive(Deftly, Debug, PartialEq)]
924 #[derive_deftly(KeySpecifier)]
925 #[deftly(prefix = "encabulator")]
926 #[deftly(role = "fan")]
927 #[deftly(summary = "test key")]
928 struct TestSpecifier {
929 casing: String,
930 bearings: String,
932
933 #[deftly(denotator)]
934 count: usize,
935
936 #[deftly(denotator)]
937 length: usize,
938
939 #[deftly(denotator)]
940 kind: String,
941 }
942
943 let key_spec = TestSpecifier {
944 casing: "logarithmic".into(),
945 bearings: "spurving".into(),
946 count: 8,
947 length: 2000,
948 kind: "lunar".into(),
949 };
950
951 check_key_specifier(
952 &key_spec,
953 "encabulator/logarithmic/spurving/fan+8+2000+lunar",
954 );
955
956 assert_eq!(
957 TestSpecifierPattern {
958 casing: Some("logarithmic".into()),
959 bearings: Some("prefabulating".into()),
960 ..TestSpecifierPattern::new_any()
961 }
962 .arti_pattern()
963 .unwrap(),
964 KeyPathPattern::Arti("encabulator/logarithmic/prefabulating/fan+*+*+*".into())
965 );
966 }
967
968 #[test]
969 fn define_key_specifier_role_field() {
970 #[derive(Deftly, Debug, Eq, PartialEq)]
971 #[derive_deftly(KeySpecifier)]
972 #[deftly(prefix = "prefix")]
973 #[deftly(summary = "test key")]
974 struct TestSpecifier {
975 #[deftly(role)]
976 role: String,
977 i: usize,
978 #[deftly(denotator)]
979 den: bool,
980 }
981
982 check_key_specifier(
983 &TestSpecifier {
984 i: 1,
985 role: "role".to_string(),
986 den: true,
987 },
988 "prefix/1/role+true",
989 );
990 }
991
992 #[test]
993 fn define_key_specifier_ctor_path() {
994 #[derive(Deftly, Debug, Eq, PartialEq)]
995 #[derive_deftly(KeySpecifier)]
996 #[deftly(prefix = "p")]
997 #[deftly(role = "r")]
998 #[deftly(ctor_path = "HsIdPublicKey")]
999 #[deftly(summary = "test key")]
1000 struct TestSpecifier {
1001 nickname: HsNickname,
1002 }
1003
1004 let spec = TestSpecifier {
1005 nickname: HsNickname::from_str("42").unwrap(),
1006 };
1007
1008 check_key_specifier(&spec, "p/42/r");
1009
1010 let ctor_path = KeySpecifier::ctor_path(&spec);
1011
1012 assert_eq!(
1013 ctor_path,
1014 Some(CTorPath::HsIdPublicKey {
1015 nickname: HsNickname::from_str("42").unwrap(),
1016 }),
1017 );
1018
1019 assert_eq!(
1020 TestSpecifier::from_ctor_path(ctor_path.unwrap()).unwrap(),
1021 spec,
1022 );
1023
1024 const HSID: &str = "yc6v7oeksrbech4ctv53di7rfjuikjagkyfrwu3yclzkfyv5haay6mqd.onion";
1026 let wrong_paths = &[
1027 CTorPath::HsClientDescEncKeypair {
1028 hs_id: HsId::from_str(HSID).unwrap(),
1029 },
1030 CTorPath::HsIdKeypair {
1031 nickname: HsNickname::from_str("42").unwrap(),
1032 },
1033 ];
1034
1035 for path in wrong_paths {
1036 assert_eq!(
1037 TestSpecifier::from_ctor_path(path.clone()).unwrap_err(),
1038 CTorPathError::KeySpecifierMismatch("TestSpecifier".into()),
1039 );
1040 }
1041 }
1042
1043 #[test]
1044 fn define_key_specifier_fixed_path_component() {
1045 #[derive(Deftly, Debug, Eq, PartialEq)]
1046 #[derive_deftly(KeySpecifier)]
1047 #[deftly(prefix = "prefix")]
1048 #[deftly(role = "role")]
1049 #[deftly(summary = "test key")]
1050 struct TestSpecifier {
1051 x: usize,
1052 #[deftly(fixed_path_component = "fixed")]
1053 z: bool,
1054 }
1055
1056 check_key_specifier(&TestSpecifier { x: 1, z: true }, "prefix/1/fixed/true/role");
1057 }
1058
1059 #[test]
1060 #[cfg(feature = "experimental-api")]
1061 fn define_cert_specifier_with_multiple_denotators() {
1062 #[derive(Deftly, Debug, PartialEq)]
1063 #[derive_deftly(KeySpecifier)]
1064 #[deftly(prefix = "encabulator")]
1065 #[deftly(role = "fan")]
1066 #[deftly(summary = "test key")]
1067 struct TestKeySpecifier {
1068 casing: String,
1069 bearings: String,
1070 #[deftly(denotator)]
1071 count: usize,
1072 }
1073
1074 #[derive(Deftly, Debug, PartialEq)]
1075 #[derive_deftly(CertSpecifier)]
1076 #[allow(dead_code)]
1077 struct TestCertSpecifier {
1078 #[deftly(subject)]
1079 subject: TestKeySpecifier,
1080 #[deftly(denotator)]
1081 length: usize,
1082 #[deftly(denotator)]
1083 width: usize,
1084 }
1085
1086 let cert_pat = TestCertSpecifierPattern::new_any();
1087 assert_eq!(
1088 cert_pat.arti_pattern().unwrap(),
1089 KeyPathPattern::Arti("encabulator/*/*/fan+*@*+*".into())
1090 );
1091 }
1092
1093 #[test]
1094 fn encode_time_period() {
1095 let period = TimePeriod::from_parts(1, 2, 3);
1096 let encoded_period = period.to_slug().unwrap();
1097
1098 assert_eq!(encoded_period.to_string(), "2_1_3");
1099 assert_eq!(period, TimePeriod::from_slug(&encoded_period).unwrap());
1100
1101 assert!(TimePeriod::from_slug(&Slug::new("invalid_tp".to_string()).unwrap()).is_err());
1102 assert!(TimePeriod::from_slug(&Slug::new("2_1_3_4".to_string()).unwrap()).is_err());
1103 }
1104
1105 #[test]
1106 fn encode_hsid() {
1107 let b32 = "eweiibe6tdjsdprb4px6rqrzzcsi22m4koia44kc5pcjr7nec2rlxyad";
1108 let onion = format!("{b32}.onion");
1109 let hsid = HsId::from_str(&onion).unwrap();
1110 let hsid_slug = hsid.to_slug().unwrap();
1111
1112 assert_eq!(hsid_slug.to_string(), b32);
1113 assert_eq!(hsid, HsId::from_slug(&hsid_slug).unwrap());
1114 }
1115
1116 #[test]
1117 fn key_info_builder() {
1118 macro_rules! assert_extra_info_eq {
1120 ($key_info:expr, [$(($k:expr, $v:expr),)*]) => {{
1121 assert_eq!(
1122 $key_info.extra_info.into_iter().collect_vec(),
1123 vec![
1124 $(($k.into(), $v.into()),)*
1125 ]
1126 );
1127 }}
1128 }
1129 let extra_info = vec![("nickname".into(), "bar".into())];
1130
1131 let key_info = KeyPathInfo::builder()
1132 .summary("test summary".into())
1133 .role("KS_vote".to_string())
1134 .set_all_extra_info(extra_info.clone().into_iter())
1135 .build()
1136 .unwrap();
1137
1138 assert_eq!(key_info.extra_info.into_iter().collect_vec(), extra_info);
1139
1140 let key_info = KeyPathInfo::builder()
1141 .summary("test summary".into())
1142 .role("KS_vote".to_string())
1143 .set_all_extra_info(extra_info.clone().into_iter())
1144 .extra_info("type", "service")
1145 .extra_info("time period", "100")
1146 .build()
1147 .unwrap();
1148
1149 assert_extra_info_eq!(
1150 key_info,
1151 [
1152 ("nickname", "bar"),
1153 ("time period", "100"),
1154 ("type", "service"),
1155 ]
1156 );
1157
1158 let key_info = KeyPathInfo::builder()
1159 .summary("test summary".into())
1160 .role("KS_vote".to_string())
1161 .extra_info("type", "service")
1162 .extra_info("time period", "100")
1163 .set_all_extra_info(extra_info.clone().into_iter())
1164 .build()
1165 .unwrap();
1166
1167 assert_extra_info_eq!(key_info, [("nickname", "bar"),]);
1168
1169 let key_info = KeyPathInfo::builder()
1170 .summary("test summary".into())
1171 .role("KS_vote".to_string())
1172 .extra_info("type", "service")
1173 .extra_info("time period", "100")
1174 .build()
1175 .unwrap();
1176
1177 assert_extra_info_eq!(key_info, [("time period", "100"), ("type", "service"),]);
1178 }
1179}