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#[derive(Debug, Clone, thiserror::Error)]
142#[non_exhaustive]
143pub enum KeyPathError {
144 #[error("{err}")]
146 Arti {
147 path: ArtiPath,
149 err: ArtiPathError,
151 },
152
153 #[error("{err}")]
155 CTor {
156 path: CTorPath,
158 err: CTorPathError,
160 },
161
162 #[error("Internal error")]
164 Bug(#[from] tor_error::Bug),
165}
166
167#[derive(Debug, Clone, thiserror::Error)]
169#[non_exhaustive]
170pub enum ArtiPathError {
171 #[error("Path does not match expected pattern")]
173 PatternNotMatched,
174
175 #[error("ArtiPath is invalid")]
177 InvalidArtiPath(ArtiPathSyntaxError),
178
179 #[error("invalid string value for element of key path")]
187 InvalidKeyPathComponentValue {
188 #[source]
190 error: InvalidKeyPathComponentValue,
191 key: String,
195 value: Slug,
197 },
198
199 #[error("Internal error")]
201 Bug(#[from] tor_error::Bug),
202}
203
204#[derive(Debug, Clone, PartialEq, thiserror::Error)]
207#[non_exhaustive]
208pub enum CTorPathError {
209 #[error("C Tor path cannot be converted to {0}")]
211 KeySpecifierMismatch(String),
212
213 #[error("Key specifier {0} does not have a C Tor path")]
216 MissingCTorPath(String),
217}
218
219#[derive(Error, Clone, Debug)]
225#[non_exhaustive]
226pub enum InvalidKeyPathComponentValue {
227 #[error("{0}")]
236 Slug(String),
237
238 #[error("Internal error")]
243 Bug(#[from] tor_error::Bug),
244}
245
246#[derive(Debug, Clone, PartialEq, derive_builder::Builder, amplify::Getters)]
253pub struct KeyPathInfo {
254 summary: String,
258 role: String,
264 #[builder(default, setter(custom))]
271 extra_info: BTreeMap<String, String>,
272}
273
274impl KeyPathInfo {
275 pub fn builder() -> KeyPathInfoBuilder {
277 KeyPathInfoBuilder::default()
278 }
279}
280
281impl KeyPathInfoBuilder {
282 pub fn set_all_extra_info(
286 &mut self,
287 all_extra_info: impl Iterator<Item = (String, String)>,
288 ) -> &mut Self {
289 self.extra_info = Some(all_extra_info.collect());
290 self
291 }
292
293 pub fn extra_info(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
297 let extra_info = self.extra_info.get_or_insert(Default::default());
298 extra_info.insert(key.into(), value.into());
299 self
300 }
301}
302
303pub trait KeyPathInfoExtractor: Send + Sync {
308 fn describe(&self, path: &KeyPath) -> StdResult<KeyPathInfo, KeyPathError>;
310}
311
312#[macro_export]
314macro_rules! register_key_info_extractor {
315 ($kv:expr) => {{
316 $crate::inventory::submit!(&$kv as &dyn $crate::KeyPathInfoExtractor);
317 }};
318}
319
320#[derive(Clone, Debug, PartialEq, Eq, Hash)]
340#[non_exhaustive]
341pub enum KeyPathPattern {
342 Arti(String),
344 CTor(CTorPath),
346}
347
348#[derive(Clone, Debug, PartialEq, Eq, Hash, derive_more::Display)] #[non_exhaustive]
351pub enum CTorPath {
352 #[display("HsClientDescEncKeypair({})", hs_id.display_unredacted())]
361 HsClientDescEncKeypair {
362 hs_id: HsId,
364 },
365 #[display("hs_ed25519_public_key")]
367 HsIdPublicKey {
368 nickname: HsNickname,
370 },
371 #[display("hs_ed25519_secret_key")]
373 HsIdKeypair {
374 nickname: HsNickname,
376 },
377}
378
379pub trait KeySpecifier {
383 fn arti_path(&self) -> StdResult<ArtiPath, ArtiPathUnavailableError>;
387
388 fn ctor_path(&self) -> Option<CTorPath>;
393
394 fn keypair_specifier(&self) -> Option<Box<dyn KeySpecifier>>;
397}
398
399pub trait KeySpecifierComponent {
412 fn to_slug(&self) -> Result<Slug, Bug>;
414 fn from_slug(s: &Slug) -> StdResult<Self, InvalidKeyPathComponentValue>
416 where
417 Self: Sized;
418 fn fmt_pretty(&self, f: &mut fmt::Formatter) -> fmt::Result;
422}
423
424#[derive(Error, Debug, Clone)]
429#[non_exhaustive]
430pub enum ArtiPathUnavailableError {
431 #[error("Internal error")]
433 Bug(#[from] tor_error::Bug),
434
435 #[error("ArtiPath unavailable")]
440 ArtiPathUnavailable,
441}
442
443impl KeySpecifier for ArtiPath {
444 fn arti_path(&self) -> StdResult<ArtiPath, ArtiPathUnavailableError> {
445 Ok(self.clone())
446 }
447
448 fn ctor_path(&self) -> Option<CTorPath> {
449 None
450 }
451
452 fn keypair_specifier(&self) -> Option<Box<dyn KeySpecifier>> {
453 None
454 }
455}
456
457impl KeySpecifier for CTorPath {
458 fn arti_path(&self) -> StdResult<ArtiPath, ArtiPathUnavailableError> {
459 Err(ArtiPathUnavailableError::ArtiPathUnavailable)
460 }
461
462 fn ctor_path(&self) -> Option<CTorPath> {
463 Some(self.clone())
464 }
465
466 fn keypair_specifier(&self) -> Option<Box<dyn KeySpecifier>> {
467 None
468 }
469}
470
471impl KeySpecifier for KeyPath {
472 fn arti_path(&self) -> StdResult<ArtiPath, ArtiPathUnavailableError> {
473 match self {
474 KeyPath::Arti(p) => p.arti_path(),
475 KeyPath::CTor(p) => p.arti_path(),
476 }
477 }
478
479 fn ctor_path(&self) -> Option<CTorPath> {
480 match self {
481 KeyPath::Arti(p) => p.ctor_path(),
482 KeyPath::CTor(p) => p.ctor_path(),
483 }
484 }
485
486 fn keypair_specifier(&self) -> Option<Box<dyn KeySpecifier>> {
487 None
488 }
489}
490
491impl KeySpecifierComponent for TimePeriod {
492 fn to_slug(&self) -> Result<Slug, Bug> {
493 Slug::new(format!(
494 "{}_{}_{}",
495 self.interval_num(),
496 self.length(),
497 self.epoch_offset_in_sec()
498 ))
499 .map_err(into_internal!("TP formatting went wrong"))
500 }
501
502 fn from_slug(s: &Slug) -> StdResult<Self, InvalidKeyPathComponentValue>
503 where
504 Self: Sized,
505 {
506 use itertools::Itertools;
507
508 let s = s.to_string();
509 #[allow(clippy::redundant_closure)] let err_ctx = |e: &str| InvalidKeyPathComponentValue::Slug(e.to_string());
511 let (interval, len, offset) = s
512 .split('_')
513 .collect_tuple()
514 .ok_or_else(|| err_ctx("invalid number of subcomponents"))?;
515
516 let length = len.parse().map_err(|_| err_ctx("invalid length"))?;
517 let interval_num = interval
518 .parse()
519 .map_err(|_| err_ctx("invalid interval_num"))?;
520 let offset_in_sec = offset
521 .parse()
522 .map_err(|_| err_ctx("invalid offset_in_sec"))?;
523
524 Ok(TimePeriod::from_parts(length, interval_num, offset_in_sec))
525 }
526
527 fn fmt_pretty(&self, f: &mut fmt::Formatter) -> fmt::Result {
528 Display::fmt(&self, f)
529 }
530}
531
532pub trait KeySpecifierComponentViaDisplayFromStr: Display + FromStr {}
540impl<T: KeySpecifierComponentViaDisplayFromStr> KeySpecifierComponent for T {
541 fn to_slug(&self) -> Result<Slug, Bug> {
542 self.to_string()
543 .try_into()
544 .map_err(into_internal!("Display generated bad Slug"))
545 }
546 fn from_slug(s: &Slug) -> Result<Self, InvalidKeyPathComponentValue>
547 where
548 Self: Sized,
549 {
550 s.as_str()
551 .parse()
552 .map_err(|_| internal!("slug cannot be parsed as component").into())
553 }
554 fn fmt_pretty(&self, f: &mut fmt::Formatter) -> fmt::Result {
555 Display::fmt(self, f)
556 }
557}
558
559impl KeySpecifierComponentViaDisplayFromStr for HsNickname {}
560
561impl KeySpecifierComponent for HsId {
562 fn to_slug(&self) -> StdResult<Slug, Bug> {
563 let hsid = self.display_unredacted().to_string();
567 let hsid_slug = hsid
568 .strip_suffix(HSID_ONION_SUFFIX)
569 .ok_or_else(|| internal!("HsId Display impl missing .onion suffix?!"))?;
570 hsid_slug
571 .to_owned()
572 .try_into()
573 .map_err(into_internal!("Display generated bad Slug"))
574 }
575
576 fn from_slug(s: &Slug) -> StdResult<Self, InvalidKeyPathComponentValue>
577 where
578 Self: Sized,
579 {
580 let onion = format!("{}{HSID_ONION_SUFFIX}", s.as_str());
589
590 onion
591 .parse()
592 .map_err(|e: HsIdParseError| InvalidKeyPathComponentValue::Slug(e.to_string()))
593 }
594
595 fn fmt_pretty(&self, f: &mut fmt::Formatter) -> fmt::Result {
596 Display::fmt(&self.display_redacted(), f)
597 }
598}
599
600struct KeySpecifierComponentPrettyHelper<'c>(&'c dyn KeySpecifierComponent);
602
603impl Display for KeySpecifierComponentPrettyHelper<'_> {
604 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
605 KeySpecifierComponent::fmt_pretty(self.0, f)
606 }
607}
608
609pub trait KeyCertificateSpecifier {
615 fn cert_denotators(&self) -> Vec<&dyn KeySpecifierComponent>;
624 fn signing_key_specifier(&self) -> Option<&dyn KeySpecifier>;
632 fn subject_key_specifier(&self) -> &dyn KeySpecifier;
634}
635
636pub trait CTorKeySpecifier: KeySpecifier + Sized {
641 fn ctor_path(&self) -> Option<CTorPath>;
645
646 fn from_ctor_path(path: CTorPath) -> Result<Self, CTorPathError>;
651}
652
653#[cfg(test)]
654mod test {
655 #![allow(clippy::bool_assert_comparison)]
657 #![allow(clippy::clone_on_copy)]
658 #![allow(clippy::dbg_macro)]
659 #![allow(clippy::mixed_attributes_style)]
660 #![allow(clippy::print_stderr)]
661 #![allow(clippy::print_stdout)]
662 #![allow(clippy::single_char_pattern)]
663 #![allow(clippy::unwrap_used)]
664 #![allow(clippy::unchecked_time_subtraction)]
665 #![allow(clippy::useless_vec)]
666 #![allow(clippy::needless_pass_by_value)]
667 use super::*;
669
670 use crate::test_utils::check_key_specifier;
671 use derive_deftly::Deftly;
672 use humantime::parse_rfc3339;
673 use itertools::Itertools;
674 use serde::{Deserialize, Serialize};
675 use std::fmt::Debug;
676 use std::time::Duration;
677
678 impl KeySpecifierComponentViaDisplayFromStr for usize {}
679 impl KeySpecifierComponentViaDisplayFromStr for String {}
680
681 impl KeySpecifierComponentViaDisplayFromStr for bool {}
684
685 fn test_time_period() -> TimePeriod {
686 TimePeriod::new(
687 Duration::from_secs(86400),
688 parse_rfc3339("2020-09-15T00:00:00Z").unwrap(),
689 Duration::from_secs(3600),
690 )
691 .unwrap()
692 }
693
694 #[test]
695 fn pretty_time_period() {
696 let tp = test_time_period();
697 assert_eq!(
698 KeySpecifierComponentPrettyHelper(&tp).to_string(),
699 "#18519 2020-09-14T01:00:00Z..+24:00",
700 );
701 }
702
703 #[test]
704 fn serde() {
705 #[derive(Serialize, Deserialize, Debug)]
709 struct T {
710 n: Slug,
711 }
712 let j = serde_json::from_str(r#"{ "n": "x" }"#).unwrap();
713 let t: T = serde_json::from_value(j).unwrap();
714 assert_eq!(&t.n.to_string(), "x");
715
716 assert_eq!(&serde_json::to_string(&t).unwrap(), r#"{"n":"x"}"#);
717
718 let j = serde_json::from_str(r#"{ "n": "!" }"#).unwrap();
719 let e = serde_json::from_value::<T>(j).unwrap_err();
720 assert!(
721 e.to_string()
722 .contains("character '!' (U+0021) is not allowed"),
723 "wrong msg {e:?}"
724 );
725 }
726
727 #[test]
728 fn define_key_specifier_with_fields_and_denotator() {
729 let tp = test_time_period();
730
731 #[derive(Deftly, Debug, PartialEq)]
732 #[derive_deftly(KeySpecifier)]
733 #[deftly(prefix = "encabulator")]
734 #[deftly(role = "marzlevane")]
735 #[deftly(summary = "test key")]
736 struct TestSpecifier {
737 kind: String,
739 base: String,
740 casing: String,
741 #[deftly(denotator)]
742 count: usize,
743 #[deftly(denotator)]
744 tp: TimePeriod,
745 }
746
747 let key_spec = TestSpecifier {
748 kind: "hydrocoptic".into(),
749 base: "waneshaft".into(),
750 casing: "logarithmic".into(),
751 count: 6,
752 tp,
753 };
754
755 check_key_specifier(
756 &key_spec,
757 "encabulator/hydrocoptic/waneshaft/logarithmic/marzlevane+6+18519_1440_3600",
758 );
759
760 let info = TestSpecifierInfoExtractor
761 .describe(&KeyPath::Arti(key_spec.arti_path().unwrap()))
762 .unwrap();
763
764 assert_eq!(
765 format!("{info:#?}"),
766 r##"
767KeyPathInfo {
768 summary: "test key",
769 role: "marzlevane",
770 extra_info: {
771 "base": "waneshaft",
772 "casing": "logarithmic",
773 "count": "6",
774 "kind": "hydrocoptic",
775 "tp": "#18519 2020-09-14T01:00:00Z..+24:00",
776 },
777}
778 "##
779 .trim()
780 );
781 }
782
783 #[test]
784 fn define_key_specifier_no_fields() {
785 #[derive(Deftly, Debug, PartialEq)]
786 #[derive_deftly(KeySpecifier)]
787 #[deftly(prefix = "encabulator")]
788 #[deftly(role = "marzlevane")]
789 #[deftly(summary = "test key")]
790 struct TestSpecifier {}
791
792 let key_spec = TestSpecifier {};
793
794 check_key_specifier(&key_spec, "encabulator/marzlevane");
795
796 assert_eq!(
797 TestSpecifierPattern {}.arti_pattern().unwrap(),
798 KeyPathPattern::Arti("encabulator/marzlevane".into())
799 );
800 }
801
802 #[test]
803 fn define_key_specifier_with_denotator() {
804 #[derive(Deftly, Debug, PartialEq)]
805 #[derive_deftly(KeySpecifier)]
806 #[deftly(prefix = "encabulator")]
807 #[deftly(role = "marzlevane")]
808 #[deftly(summary = "test key")]
809 struct TestSpecifier {
810 #[deftly(denotator)]
811 count: usize,
812 }
813
814 let key_spec = TestSpecifier { count: 6 };
815
816 check_key_specifier(&key_spec, "encabulator/marzlevane+6");
817
818 assert_eq!(
819 TestSpecifierPattern { count: None }.arti_pattern().unwrap(),
820 KeyPathPattern::Arti("encabulator/marzlevane+*".into())
821 );
822 }
823
824 #[test]
825 fn define_key_specifier_with_fields() {
826 #[derive(Deftly, Debug, PartialEq)]
827 #[derive_deftly(KeySpecifier)]
828 #[deftly(prefix = "encabulator")]
829 #[deftly(role = "fan")]
830 #[deftly(summary = "test key")]
831 struct TestSpecifier {
832 casing: String,
833 bearings: String,
835 }
836
837 let key_spec = TestSpecifier {
838 casing: "logarithmic".into(),
839 bearings: "spurving".into(),
840 };
841
842 check_key_specifier(&key_spec, "encabulator/logarithmic/spurving/fan");
843
844 assert_eq!(
845 TestSpecifierPattern {
846 casing: Some("logarithmic".into()),
847 bearings: Some("prefabulating".into()),
848 }
849 .arti_pattern()
850 .unwrap(),
851 KeyPathPattern::Arti("encabulator/logarithmic/prefabulating/fan".into())
852 );
853
854 let ctor_path = CTorPath::HsIdPublicKey {
855 nickname: HsNickname::from_str("foo").unwrap(),
856 };
857
858 assert_eq!(
859 TestSpecifier::from_ctor_path(ctor_path).unwrap_err(),
860 CTorPathError::MissingCTorPath("TestSpecifier".into()),
861 );
862 }
863
864 #[test]
865 fn define_key_specifier_with_multiple_denotators() {
866 #[derive(Deftly, Debug, PartialEq)]
867 #[derive_deftly(KeySpecifier)]
868 #[deftly(prefix = "encabulator")]
869 #[deftly(role = "fan")]
870 #[deftly(summary = "test key")]
871 struct TestSpecifier {
872 casing: String,
873 bearings: String,
875
876 #[deftly(denotator)]
877 count: usize,
878
879 #[deftly(denotator)]
880 length: usize,
881
882 #[deftly(denotator)]
883 kind: String,
884 }
885
886 let key_spec = TestSpecifier {
887 casing: "logarithmic".into(),
888 bearings: "spurving".into(),
889 count: 8,
890 length: 2000,
891 kind: "lunar".into(),
892 };
893
894 check_key_specifier(
895 &key_spec,
896 "encabulator/logarithmic/spurving/fan+8+2000+lunar",
897 );
898
899 assert_eq!(
900 TestSpecifierPattern {
901 casing: Some("logarithmic".into()),
902 bearings: Some("prefabulating".into()),
903 ..TestSpecifierPattern::new_any()
904 }
905 .arti_pattern()
906 .unwrap(),
907 KeyPathPattern::Arti("encabulator/logarithmic/prefabulating/fan+*+*+*".into())
908 );
909 }
910
911 #[test]
912 fn define_key_specifier_role_field() {
913 #[derive(Deftly, Debug, Eq, PartialEq)]
914 #[derive_deftly(KeySpecifier)]
915 #[deftly(prefix = "prefix")]
916 #[deftly(summary = "test key")]
917 struct TestSpecifier {
918 #[deftly(role)]
919 role: String,
920 i: usize,
921 #[deftly(denotator)]
922 den: bool,
923 }
924
925 check_key_specifier(
926 &TestSpecifier {
927 i: 1,
928 role: "role".to_string(),
929 den: true,
930 },
931 "prefix/1/role+true",
932 );
933 }
934
935 #[test]
936 fn define_key_specifier_ctor_path() {
937 #[derive(Deftly, Debug, Eq, PartialEq)]
938 #[derive_deftly(KeySpecifier)]
939 #[deftly(prefix = "p")]
940 #[deftly(role = "r")]
941 #[deftly(ctor_path = "HsIdPublicKey")]
942 #[deftly(summary = "test key")]
943 struct TestSpecifier {
944 nickname: HsNickname,
945 }
946
947 let spec = TestSpecifier {
948 nickname: HsNickname::from_str("42").unwrap(),
949 };
950
951 check_key_specifier(&spec, "p/42/r");
952
953 let ctor_path = KeySpecifier::ctor_path(&spec);
954
955 assert_eq!(
956 ctor_path,
957 Some(CTorPath::HsIdPublicKey {
958 nickname: HsNickname::from_str("42").unwrap(),
959 }),
960 );
961
962 assert_eq!(
963 TestSpecifier::from_ctor_path(ctor_path.unwrap()).unwrap(),
964 spec,
965 );
966
967 const HSID: &str = "yc6v7oeksrbech4ctv53di7rfjuikjagkyfrwu3yclzkfyv5haay6mqd.onion";
969 let wrong_paths = &[
970 CTorPath::HsClientDescEncKeypair {
971 hs_id: HsId::from_str(HSID).unwrap(),
972 },
973 CTorPath::HsIdKeypair {
974 nickname: HsNickname::from_str("42").unwrap(),
975 },
976 ];
977
978 for path in wrong_paths {
979 assert_eq!(
980 TestSpecifier::from_ctor_path(path.clone()).unwrap_err(),
981 CTorPathError::KeySpecifierMismatch("TestSpecifier".into()),
982 );
983 }
984 }
985
986 #[test]
987 fn define_key_specifier_fixed_path_component() {
988 #[derive(Deftly, Debug, Eq, PartialEq)]
989 #[derive_deftly(KeySpecifier)]
990 #[deftly(prefix = "prefix")]
991 #[deftly(role = "role")]
992 #[deftly(summary = "test key")]
993 struct TestSpecifier {
994 x: usize,
995 #[deftly(fixed_path_component = "fixed")]
996 z: bool,
997 }
998
999 check_key_specifier(&TestSpecifier { x: 1, z: true }, "prefix/1/fixed/true/role");
1000 }
1001
1002 #[test]
1003 fn encode_time_period() {
1004 let period = TimePeriod::from_parts(1, 2, 3);
1005 let encoded_period = period.to_slug().unwrap();
1006
1007 assert_eq!(encoded_period.to_string(), "2_1_3");
1008 assert_eq!(period, TimePeriod::from_slug(&encoded_period).unwrap());
1009
1010 assert!(TimePeriod::from_slug(&Slug::new("invalid_tp".to_string()).unwrap()).is_err());
1011 assert!(TimePeriod::from_slug(&Slug::new("2_1_3_4".to_string()).unwrap()).is_err());
1012 }
1013
1014 #[test]
1015 fn encode_hsid() {
1016 let b32 = "eweiibe6tdjsdprb4px6rqrzzcsi22m4koia44kc5pcjr7nec2rlxyad";
1017 let onion = format!("{b32}.onion");
1018 let hsid = HsId::from_str(&onion).unwrap();
1019 let hsid_slug = hsid.to_slug().unwrap();
1020
1021 assert_eq!(hsid_slug.to_string(), b32);
1022 assert_eq!(hsid, HsId::from_slug(&hsid_slug).unwrap());
1023 }
1024
1025 #[test]
1026 fn key_info_builder() {
1027 macro_rules! assert_extra_info_eq {
1029 ($key_info:expr, [$(($k:expr, $v:expr),)*]) => {{
1030 assert_eq!(
1031 $key_info.extra_info.into_iter().collect_vec(),
1032 vec![
1033 $(($k.into(), $v.into()),)*
1034 ]
1035 );
1036 }}
1037 }
1038 let extra_info = vec![("nickname".into(), "bar".into())];
1039
1040 let key_info = KeyPathInfo::builder()
1041 .summary("test summary".into())
1042 .role("KS_vote".to_string())
1043 .set_all_extra_info(extra_info.clone().into_iter())
1044 .build()
1045 .unwrap();
1046
1047 assert_eq!(key_info.extra_info.into_iter().collect_vec(), extra_info);
1048
1049 let key_info = KeyPathInfo::builder()
1050 .summary("test summary".into())
1051 .role("KS_vote".to_string())
1052 .set_all_extra_info(extra_info.clone().into_iter())
1053 .extra_info("type", "service")
1054 .extra_info("time period", "100")
1055 .build()
1056 .unwrap();
1057
1058 assert_extra_info_eq!(
1059 key_info,
1060 [
1061 ("nickname", "bar"),
1062 ("time period", "100"),
1063 ("type", "service"),
1064 ]
1065 );
1066
1067 let key_info = KeyPathInfo::builder()
1068 .summary("test summary".into())
1069 .role("KS_vote".to_string())
1070 .extra_info("type", "service")
1071 .extra_info("time period", "100")
1072 .set_all_extra_info(extra_info.clone().into_iter())
1073 .build()
1074 .unwrap();
1075
1076 assert_extra_info_eq!(key_info, [("nickname", "bar"),]);
1077
1078 let key_info = KeyPathInfo::builder()
1079 .summary("test summary".into())
1080 .role("KS_vote".to_string())
1081 .extra_info("type", "service")
1082 .extra_info("time period", "100")
1083 .build()
1084 .unwrap();
1085
1086 assert_extra_info_eq!(key_info, [("time period", "100"), ("type", "service"),]);
1087 }
1088}