1use std::collections::HashMap;
91use std::fmt;
92use std::net::IpAddr;
93use std::str::FromStr;
94
95use derive_builder::Builder;
96
97use crate::exit_policy::MicroExitPolicy;
98use crate::Error;
99
100use super::{compute_digest, Descriptor, DigestEncoding, DigestHash};
101
102#[derive(Debug, Clone, PartialEq, Builder)]
145#[builder(setter(into, strip_option))]
146pub struct Microdescriptor {
147 pub onion_key: String,
152 #[builder(default)]
157 pub ntor_onion_key: Option<String>,
158 pub or_addresses: Vec<(IpAddr, u16, bool)>,
163 pub family: Vec<String>,
168 pub exit_policy: MicroExitPolicy,
173 #[builder(default)]
177 pub exit_policy_v6: Option<MicroExitPolicy>,
178 pub identifiers: HashMap<String, String>,
183 pub protocols: HashMap<String, Vec<u32>>,
188 raw_content: Vec<u8>,
190 annotations: Vec<(String, Option<String>)>,
195 unrecognized_lines: Vec<String>,
197}
198
199impl Microdescriptor {
200 pub fn validate(&self) -> Result<(), Error> {
225 use crate::descriptor::MicrodescriptorError;
226
227 if self.onion_key.is_empty() {
228 return Err(Error::Descriptor(
229 crate::descriptor::DescriptorError::Microdescriptor(
230 MicrodescriptorError::MissingRequiredField("onion-key".to_string()),
231 ),
232 ));
233 }
234
235 for (addr, port, _) in &self.or_addresses {
236 if *port == 0 {
237 return Err(Error::Descriptor(
238 crate::descriptor::DescriptorError::Microdescriptor(
239 MicrodescriptorError::InvalidPortPolicy(format!(
240 "or-address {}:0 has invalid port",
241 addr
242 )),
243 ),
244 ));
245 }
246 }
247
248 for fp in &self.family {
249 let clean_fp = fp.trim_start_matches('$');
250 if !clean_fp.is_empty()
251 && clean_fp.len() == 40
252 && !clean_fp.chars().all(|c| c.is_ascii_hexdigit())
253 {
254 return Err(Error::Descriptor(
255 crate::descriptor::DescriptorError::Microdescriptor(
256 MicrodescriptorError::InvalidRelayFamily(fp.clone()),
257 ),
258 ));
259 }
260 }
261
262 Ok(())
263 }
264
265 pub fn new(onion_key: String) -> Self {
285 Self {
286 onion_key,
287 ntor_onion_key: None,
288 or_addresses: Vec::new(),
289 family: Vec::new(),
290 exit_policy: MicroExitPolicy::parse("reject 1-65535").unwrap(),
291 exit_policy_v6: None,
292 identifiers: HashMap::new(),
293 protocols: HashMap::new(),
294 raw_content: Vec::new(),
295 annotations: Vec::new(),
296 unrecognized_lines: Vec::new(),
297 }
298 }
299
300 pub fn parse_with_annotations(content: &str, annotations: &[&str]) -> Result<Self, Error> {
335 let mut desc = Self::parse(content)?;
336 for ann in annotations {
337 let ann = ann.trim();
338 if ann.is_empty() {
339 continue;
340 }
341 let ann = ann.strip_prefix('@').unwrap_or(ann);
342 if let Some(space_pos) = ann.find(' ') {
343 let key = ann[..space_pos].to_string();
344 let value = ann[space_pos + 1..].trim().to_string();
345 desc.annotations.push((key, Some(value)));
346 } else {
347 desc.annotations.push((ann.to_string(), None));
348 }
349 }
350 Ok(desc)
351 }
352
353 pub fn get_annotations(&self) -> HashMap<String, Option<String>> {
380 self.annotations.iter().cloned().collect()
381 }
382
383 pub fn get_annotation_lines(&self) -> Vec<String> {
408 self.annotations
409 .iter()
410 .map(|(k, v)| match v {
411 Some(val) => format!("@{} {}", k, val),
412 None => format!("@{}", k),
413 })
414 .collect()
415 }
416
417 fn extract_pem_block(lines: &[&str], start_idx: usize) -> (String, usize) {
419 let mut block = String::new();
420 let mut idx = start_idx;
421 while idx < lines.len() {
422 let line = lines[idx];
423 block.push_str(line);
424 block.push('\n');
425 if line.starts_with("-----END ") {
426 break;
427 }
428 idx += 1;
429 }
430 (block.trim_end().to_string(), idx)
431 }
432
433 fn parse_or_address(line: &str) -> Result<(IpAddr, u16, bool), Error> {
435 let line = line.trim();
436 if line.starts_with('[') {
437 if let Some(bracket_end) = line.find(']') {
438 let ipv6_str = &line[1..bracket_end];
439 let port_str = &line[bracket_end + 2..];
440 let addr: IpAddr = ipv6_str.parse().map_err(|_| Error::Parse {
441 location: "a".to_string(),
442 reason: format!("invalid IPv6 address: {}", ipv6_str),
443 })?;
444 let port: u16 = port_str.parse().map_err(|_| Error::Parse {
445 location: "a".to_string(),
446 reason: format!("invalid port: {}", port_str),
447 })?;
448 return Ok((addr, port, true));
449 }
450 }
451 if let Some(colon_pos) = line.rfind(':') {
452 let addr_str = &line[..colon_pos];
453 let port_str = &line[colon_pos + 1..];
454 let addr: IpAddr = addr_str.parse().map_err(|_| Error::Parse {
455 location: "a".to_string(),
456 reason: format!("invalid address: {}", addr_str),
457 })?;
458 let port: u16 = port_str.parse().map_err(|_| Error::Parse {
459 location: "a".to_string(),
460 reason: format!("invalid port: {}", port_str),
461 })?;
462 let is_ipv6 = addr.is_ipv6();
463 return Ok((addr, port, is_ipv6));
464 }
465 Err(Error::Parse {
466 location: "a".to_string(),
467 reason: format!("invalid or-address format: {}", line),
468 })
469 }
470
471 fn parse_protocols(value: &str) -> HashMap<String, Vec<u32>> {
472 let mut protocols = HashMap::new();
473 for entry in value.split_whitespace() {
474 if let Some(eq_pos) = entry.find('=') {
475 let proto_name = &entry[..eq_pos];
476 let versions_str = &entry[eq_pos + 1..];
477 let versions: Vec<u32> = versions_str
478 .split(',')
479 .filter_map(|v| {
480 if let Some(dash) = v.find('-') {
481 let start: u32 = v[..dash].parse().ok()?;
482 let end: u32 = v[dash + 1..].parse().ok()?;
483 Some((start..=end).collect::<Vec<_>>())
484 } else {
485 v.parse().ok().map(|n| vec![n])
486 }
487 })
488 .flatten()
489 .collect();
490 protocols.insert(proto_name.to_string(), versions);
491 }
492 }
493 protocols
494 }
495
496 fn parse_identifiers(
497 value: &str,
498 identifiers: &mut HashMap<String, String>,
499 validate: bool,
500 ) -> Result<(), Error> {
501 let parts: Vec<&str> = value.split_whitespace().collect();
502 if parts.len() < 2 {
503 return Err(Error::Parse {
504 location: "id".to_string(),
505 reason: format!(
506 "'id' lines should contain both key type and digest: {}",
507 value
508 ),
509 });
510 }
511 let key_type = parts[0].to_string();
512 let key_value = parts[1].to_string();
513 if validate && identifiers.contains_key(&key_type) {
514 return Err(Error::Parse {
515 location: "id".to_string(),
516 reason: format!(
517 "There can only be one 'id' line per key type, but '{}' appeared multiple times",
518 key_type
519 ),
520 });
521 }
522 identifiers.insert(key_type, key_value);
523 Ok(())
524 }
525}
526
527impl Descriptor for Microdescriptor {
528 fn parse(content: &str) -> Result<Self, Error> {
529 let raw_content = content.as_bytes().to_vec();
530 let lines: Vec<&str> = content.lines().collect();
531
532 let mut onion_key: Option<String> = None;
533 let mut ntor_onion_key: Option<String> = None;
534 let mut or_addresses: Vec<(IpAddr, u16, bool)> = Vec::new();
535 let mut family: Vec<String> = Vec::new();
536 let mut exit_policy = MicroExitPolicy::parse("reject 1-65535")?;
537 let mut exit_policy_v6: Option<MicroExitPolicy> = None;
538 let mut identifiers: HashMap<String, String> = HashMap::new();
539 let mut protocols: HashMap<String, Vec<u32>> = HashMap::new();
540 let mut unrecognized_lines: Vec<String> = Vec::new();
541 let mut annotations: Vec<(String, Option<String>)> = Vec::new();
542
543 let mut idx = 0;
544 while idx < lines.len() {
545 let line = lines[idx];
546
547 if line.starts_with('@') {
548 let ann = line.strip_prefix('@').unwrap_or(line);
549 if let Some(space_pos) = ann.find(' ') {
550 let key = ann[..space_pos].to_string();
551 let value = ann[space_pos + 1..].trim().to_string();
552 annotations.push((key, Some(value)));
553 } else {
554 annotations.push((ann.to_string(), None));
555 }
556 idx += 1;
557 continue;
558 }
559
560 let (keyword, value) = if let Some(space_pos) = line.find(' ') {
561 (&line[..space_pos], line[space_pos + 1..].trim())
562 } else {
563 (line, "")
564 };
565
566 match keyword {
567 "onion-key" => {
568 let (block, end_idx) = Self::extract_pem_block(&lines, idx + 1);
569 onion_key = Some(block);
570 idx = end_idx;
571 }
572 "ntor-onion-key" => {
573 ntor_onion_key = Some(value.to_string());
574 }
575 "a" => {
576 if let Ok(addr) = Self::parse_or_address(value) {
577 or_addresses.push(addr);
578 }
579 }
580 "family" => {
581 family = value.split_whitespace().map(|s| s.to_string()).collect();
582 }
583 "p" => {
584 exit_policy = MicroExitPolicy::parse(value)?;
585 }
586 "p6" => {
587 exit_policy_v6 = Some(MicroExitPolicy::parse(value)?);
588 }
589 "pr" => {
590 protocols = Self::parse_protocols(value);
591 }
592 "id" => {
593 let _ = Self::parse_identifiers(value, &mut identifiers, false);
594 }
595 _ => {
596 if !line.is_empty() && !line.starts_with("-----") {
597 unrecognized_lines.push(line.to_string());
598 }
599 }
600 }
601 idx += 1;
602 }
603
604 let onion_key = onion_key.ok_or_else(|| Error::Parse {
605 location: "onion-key".to_string(),
606 reason: "Microdescriptor must have a 'onion-key' entry".to_string(),
607 })?;
608
609 Ok(Self {
610 onion_key,
611 ntor_onion_key,
612 or_addresses,
613 family,
614 exit_policy,
615 exit_policy_v6,
616 identifiers,
617 protocols,
618 raw_content,
619 annotations,
620 unrecognized_lines,
621 })
622 }
623
624 fn to_descriptor_string(&self) -> String {
625 let mut result = String::new();
626
627 result.push_str("onion-key\n");
628 result.push_str(&self.onion_key);
629 result.push('\n');
630
631 if let Some(ref ntor_key) = self.ntor_onion_key {
632 result.push_str(&format!("ntor-onion-key {}\n", ntor_key));
633 }
634
635 for (addr, port, is_ipv6) in &self.or_addresses {
636 if *is_ipv6 {
637 result.push_str(&format!("a [{}]:{}\n", addr, port));
638 } else {
639 result.push_str(&format!("a {}:{}\n", addr, port));
640 }
641 }
642
643 if !self.family.is_empty() {
644 result.push_str(&format!("family {}\n", self.family.join(" ")));
645 }
646
647 result.push_str(&format!("p {}\n", self.exit_policy));
648
649 if let Some(ref policy_v6) = self.exit_policy_v6 {
650 result.push_str(&format!("p6 {}\n", policy_v6));
651 }
652
653 if !self.protocols.is_empty() {
654 let proto_str: Vec<String> = self
655 .protocols
656 .iter()
657 .map(|(k, v)| {
658 let versions: Vec<String> = v.iter().map(|n| n.to_string()).collect();
659 format!("{}={}", k, versions.join(","))
660 })
661 .collect();
662 result.push_str(&format!("pr {}\n", proto_str.join(" ")));
663 }
664
665 for (key_type, key_value) in &self.identifiers {
666 result.push_str(&format!("id {} {}\n", key_type, key_value));
667 }
668
669 result
670 }
671
672 fn digest(&self, hash: DigestHash, encoding: DigestEncoding) -> Result<String, Error> {
673 Ok(compute_digest(&self.raw_content, hash, encoding))
674 }
675
676 fn raw_content(&self) -> &[u8] {
677 &self.raw_content
678 }
679
680 fn unrecognized_lines(&self) -> &[String] {
681 &self.unrecognized_lines
682 }
683}
684
685impl FromStr for Microdescriptor {
686 type Err = Error;
687
688 fn from_str(s: &str) -> Result<Self, Self::Err> {
689 Self::parse(s)
690 }
691}
692
693impl fmt::Display for Microdescriptor {
694 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
695 write!(f, "{}", self.to_descriptor_string())
696 }
697}
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702
703 const FIRST_ONION_KEY: &str = "-----BEGIN RSA PUBLIC KEY-----
704MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
705H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
706CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
707-----END RSA PUBLIC KEY-----";
708
709 const SECOND_ONION_KEY: &str = "-----BEGIN RSA PUBLIC KEY-----
710MIGJAoGBALCOxZdpMI2WO496njSQ2M7b4IgAGATqpJmH3So7lXOa25sK6o7JipgP
711qQE83K/t/xsMIpxQ/hHkft3G78HkeXXFc9lVUzH0HmHwYEu0M+PMVULSkG36MfEl
7127WeSZzaG+Tlnh9OySAzVyTsv1ZJsTQFHH9V8wuM0GOMo9X8DFC+NAgMBAAE=
713-----END RSA PUBLIC KEY-----";
714
715 const THIRD_ONION_KEY: &str = "-----BEGIN RSA PUBLIC KEY-----
716MIGJAoGBAOWFQHxO+5kGuhwPUX5jB7wJCrTbSU0fZwolNV1t9UaDdjGDvIjIhdit
717y2sMbyd9K8lbQO7x9rQjNst5ZicuaSOs854XQddSjm++vMdjYbOcVMqnKGSztvpd
718w/1LVWFfhcBnsGi4JMGbmP+KUZG9A8kI9deSyJhfi35jA7UepiHHAgMBAAE=
719-----END RSA PUBLIC KEY-----";
720
721 fn first_microdesc() -> &'static str {
722 "@last-listed 2013-02-24 00:18:36
723onion-key
724-----BEGIN RSA PUBLIC KEY-----
725MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
726H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
727CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
728-----END RSA PUBLIC KEY-----"
729 }
730
731 fn second_microdesc() -> &'static str {
732 "@last-listed 2013-02-24 00:18:37
733onion-key
734-----BEGIN RSA PUBLIC KEY-----
735MIGJAoGBALCOxZdpMI2WO496njSQ2M7b4IgAGATqpJmH3So7lXOa25sK6o7JipgP
736qQE83K/t/xsMIpxQ/hHkft3G78HkeXXFc9lVUzH0HmHwYEu0M+PMVULSkG36MfEl
7377WeSZzaG+Tlnh9OySAzVyTsv1ZJsTQFHH9V8wuM0GOMo9X8DFC+NAgMBAAE=
738-----END RSA PUBLIC KEY-----
739ntor-onion-key r5572HzD+PMPBbXlZwBhsm6YEbxnYgis8vhZ1jmdI2k=
740family $6141629FA0D15A6AEAEF3A1BEB76E64C767B3174"
741 }
742
743 fn third_microdesc() -> &'static str {
744 "@last-listed 2013-02-24 00:18:36
745onion-key
746-----BEGIN RSA PUBLIC KEY-----
747MIGJAoGBAOWFQHxO+5kGuhwPUX5jB7wJCrTbSU0fZwolNV1t9UaDdjGDvIjIhdit
748y2sMbyd9K8lbQO7x9rQjNst5ZicuaSOs854XQddSjm++vMdjYbOcVMqnKGSztvpd
749w/1LVWFfhcBnsGi4JMGbmP+KUZG9A8kI9deSyJhfi35jA7UepiHHAgMBAAE=
750-----END RSA PUBLIC KEY-----
751a [2001:6b0:7:125::242]:9001
752p accept 80,443"
753 }
754
755 #[test]
756 fn test_parse_first_microdesc() {
757 let desc = Microdescriptor::parse(first_microdesc()).unwrap();
758 assert_eq!(desc.onion_key, FIRST_ONION_KEY);
759 assert_eq!(desc.ntor_onion_key, None);
760 assert!(desc.or_addresses.is_empty());
761 assert!(desc.family.is_empty());
762 assert!(!desc.exit_policy.is_accept);
763 assert!(desc.exit_policy.ports.iter().any(|p| p.is_wildcard()));
764 let annotations = desc.get_annotations();
765 assert_eq!(
766 annotations.get("last-listed"),
767 Some(&Some("2013-02-24 00:18:36".to_string()))
768 );
769 }
770
771 #[test]
772 fn test_parse_second_microdesc() {
773 let desc = Microdescriptor::parse(second_microdesc()).unwrap();
774 assert_eq!(desc.onion_key, SECOND_ONION_KEY);
775 assert_eq!(
776 desc.ntor_onion_key,
777 Some("r5572HzD+PMPBbXlZwBhsm6YEbxnYgis8vhZ1jmdI2k=".to_string())
778 );
779 assert!(desc.or_addresses.is_empty());
780 assert_eq!(
781 desc.family,
782 vec!["$6141629FA0D15A6AEAEF3A1BEB76E64C767B3174"]
783 );
784 assert!(!desc.exit_policy.is_accept);
785 assert!(desc.exit_policy.ports.iter().any(|p| p.is_wildcard()));
786 }
787
788 #[test]
789 fn test_parse_third_microdesc() {
790 let desc = Microdescriptor::parse(third_microdesc()).unwrap();
791 assert_eq!(desc.onion_key, THIRD_ONION_KEY);
792 assert_eq!(desc.ntor_onion_key, None);
793 assert_eq!(desc.or_addresses.len(), 1);
794 let (addr, port, is_ipv6) = &desc.or_addresses[0];
795 assert_eq!(addr.to_string(), "2001:6b0:7:125::242");
796 assert_eq!(*port, 9001);
797 assert!(*is_ipv6);
798 assert!(desc.family.is_empty());
799 assert_eq!(desc.exit_policy.to_string(), "accept 80,443");
800 }
801
802 #[test]
803 fn test_minimal_microdescriptor() {
804 let content = "onion-key
805-----BEGIN RSA PUBLIC KEY-----
806MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
807H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
808CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
809-----END RSA PUBLIC KEY-----";
810 let desc = Microdescriptor::parse(content).unwrap();
811 assert_eq!(desc.ntor_onion_key, None);
812 assert!(desc.or_addresses.is_empty());
813 assert!(desc.family.is_empty());
814 assert!(!desc.exit_policy.is_accept);
815 assert!(desc.exit_policy.ports.iter().any(|p| p.is_wildcard()));
816 assert_eq!(desc.exit_policy_v6, None);
817 assert!(desc.identifiers.is_empty());
818 assert!(desc.protocols.is_empty());
819 assert!(desc.unrecognized_lines.is_empty());
820 }
821
822 #[test]
823 fn test_unrecognized_line() {
824 let content = "onion-key
825-----BEGIN RSA PUBLIC KEY-----
826MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
827H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
828CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
829-----END RSA PUBLIC KEY-----
830pepperjack is oh so tasty!";
831 let desc = Microdescriptor::parse(content).unwrap();
832 assert_eq!(desc.unrecognized_lines, vec!["pepperjack is oh so tasty!"]);
833 }
834
835 #[test]
836 fn test_a_line() {
837 let content = "onion-key
838-----BEGIN RSA PUBLIC KEY-----
839MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
840H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
841CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
842-----END RSA PUBLIC KEY-----
843a 10.45.227.253:9001
844a [fd9f:2e19:3bcf::02:9970]:9001";
845 let desc = Microdescriptor::parse(content).unwrap();
846 assert_eq!(desc.or_addresses.len(), 2);
847 let (addr1, port1, is_ipv6_1) = &desc.or_addresses[0];
848 assert_eq!(addr1.to_string(), "10.45.227.253");
849 assert_eq!(*port1, 9001);
850 assert!(!*is_ipv6_1);
851 let (addr2, port2, is_ipv6_2) = &desc.or_addresses[1];
852 assert_eq!(addr2.to_string(), "fd9f:2e19:3bcf::2:9970");
853 assert_eq!(*port2, 9001);
854 assert!(*is_ipv6_2);
855 }
856
857 #[test]
858 fn test_family() {
859 let content = "onion-key
860-----BEGIN RSA PUBLIC KEY-----
861MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
862H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
863CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
864-----END RSA PUBLIC KEY-----
865family Amunet1 Amunet2 Amunet3";
866 let desc = Microdescriptor::parse(content).unwrap();
867 assert_eq!(desc.family, vec!["Amunet1", "Amunet2", "Amunet3"]);
868 }
869
870 #[test]
871 fn test_exit_policy() {
872 let content = "onion-key
873-----BEGIN RSA PUBLIC KEY-----
874MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
875H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
876CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
877-----END RSA PUBLIC KEY-----
878p accept 80,110,143,443";
879 let desc = Microdescriptor::parse(content).unwrap();
880 assert_eq!(desc.exit_policy.to_string(), "accept 80,110,143,443");
881 }
882
883 #[test]
884 fn test_protocols() {
885 let content = "onion-key
886-----BEGIN RSA PUBLIC KEY-----
887MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
888H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
889CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
890-----END RSA PUBLIC KEY-----
891pr Cons=1 Desc=1 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=1-4 LinkAuth=1 Microdesc=1 Relay=1-2";
892 let desc = Microdescriptor::parse(content).unwrap();
893 assert_eq!(desc.protocols.len(), 10);
894 assert_eq!(desc.protocols.get("Link"), Some(&vec![1, 2, 3, 4]));
895 assert_eq!(desc.protocols.get("Relay"), Some(&vec![1, 2]));
896 }
897
898 #[test]
899 fn test_identifier() {
900 let content = "onion-key
901-----BEGIN RSA PUBLIC KEY-----
902MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
903H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
904CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
905-----END RSA PUBLIC KEY-----
906id rsa1024 Cd47okjCHD83YGzThGBDptXs9Z4";
907 let desc = Microdescriptor::parse(content).unwrap();
908 assert_eq!(
909 desc.identifiers.get("rsa1024"),
910 Some(&"Cd47okjCHD83YGzThGBDptXs9Z4".to_string())
911 );
912 }
913
914 #[test]
915 fn test_multiple_identifiers() {
916 let content = "onion-key
917-----BEGIN RSA PUBLIC KEY-----
918MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
919H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
920CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
921-----END RSA PUBLIC KEY-----
922id rsa1024 Cd47okjCHD83YGzThGBDptXs9Z4
923id ed25519 50f6ddbecdc848dcc6b818b14d1";
924 let desc = Microdescriptor::parse(content).unwrap();
925 assert_eq!(
926 desc.identifiers.get("rsa1024"),
927 Some(&"Cd47okjCHD83YGzThGBDptXs9Z4".to_string())
928 );
929 assert_eq!(
930 desc.identifiers.get("ed25519"),
931 Some(&"50f6ddbecdc848dcc6b818b14d1".to_string())
932 );
933 }
934
935 #[test]
936 fn test_digest() {
937 let desc = Microdescriptor::parse(third_microdesc()).unwrap();
938 let digest = desc
939 .digest(DigestHash::Sha256, DigestEncoding::Base64)
940 .unwrap();
941 assert!(!digest.is_empty());
942 }
943
944 #[test]
945 fn test_missing_onion_key() {
946 let content = "ntor-onion-key r5572HzD+PMPBbXlZwBhsm6YEbxnYgis8vhZ1jmdI2k=";
947 let result = Microdescriptor::parse(content);
948 assert!(result.is_err());
949 }
950
951 #[test]
952 fn test_proceeding_line() {
953 let content = "family Amunet1
954onion-key
955-----BEGIN RSA PUBLIC KEY-----
956MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
957H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
958CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
959-----END RSA PUBLIC KEY-----";
960 let desc = Microdescriptor::parse(content).unwrap();
961 assert_eq!(desc.family, vec!["Amunet1"]);
962 }
963
964 #[test]
965 fn test_conflicting_identifiers() {
966 let content = "onion-key
967-----BEGIN RSA PUBLIC KEY-----
968MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
969H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
970CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
971-----END RSA PUBLIC KEY-----
972id rsa1024 Cd47okjCHD83YGzThGBDptXs9Z4
973id rsa1024 50f6ddbecdc848dcc6b818b14d1";
974 let desc = Microdescriptor::parse(content).unwrap();
975 assert_eq!(
976 desc.identifiers.get("rsa1024"),
977 Some(&"50f6ddbecdc848dcc6b818b14d1".to_string())
978 );
979 }
980
981 #[test]
982 fn test_exit_policy_v6() {
983 let content = "onion-key
984-----BEGIN RSA PUBLIC KEY-----
985MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
986H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
987CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
988-----END RSA PUBLIC KEY-----
989p accept 80,443
990p6 accept 80,443";
991 let desc = Microdescriptor::parse(content).unwrap();
992 assert_eq!(desc.exit_policy.to_string(), "accept 80,443");
993 assert!(desc.exit_policy_v6.is_some());
994 assert_eq!(desc.exit_policy_v6.unwrap().to_string(), "accept 80,443");
995 }
996
997 use proptest::prelude::*;
998
999 fn valid_base64_key() -> impl Strategy<Value = String> {
1000 Just("MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM\nH2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF\nCxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=".to_string())
1001 }
1002
1003 fn valid_ntor_key() -> impl Strategy<Value = String> {
1004 Just("r5572HzD+PMPBbXlZwBhsm6YEbxnYgis8vhZ1jmdI2k=".to_string())
1005 }
1006
1007 fn simple_microdescriptor() -> impl Strategy<Value = Microdescriptor> {
1008 (
1009 valid_base64_key(),
1010 proptest::option::of(valid_ntor_key()),
1011 proptest::collection::vec("[A-Za-z0-9]{1,19}", 0..3),
1012 )
1013 .prop_map(|(onion_key, ntor_key, family)| {
1014 let mut desc = Microdescriptor::new(format!(
1015 "-----BEGIN RSA PUBLIC KEY-----\n{}\n-----END RSA PUBLIC KEY-----",
1016 onion_key
1017 ));
1018 desc.ntor_onion_key = ntor_key;
1019 desc.family = family;
1020 desc
1021 })
1022 }
1023
1024 proptest! {
1025 #![proptest_config(ProptestConfig::with_cases(100))]
1026
1027 #[test]
1028 fn prop_microdescriptor_roundtrip(desc in simple_microdescriptor()) {
1029 let serialized = desc.to_descriptor_string();
1030 let parsed = Microdescriptor::parse(&serialized);
1031
1032 prop_assert!(parsed.is_ok(), "Failed to parse serialized microdescriptor: {:?}", parsed.err());
1033
1034 let parsed = parsed.unwrap();
1035
1036 prop_assert_eq!(&desc.onion_key, &parsed.onion_key, "onion_key mismatch");
1037 prop_assert_eq!(&desc.ntor_onion_key, &parsed.ntor_onion_key, "ntor_onion_key mismatch");
1038 prop_assert_eq!(&desc.family, &parsed.family, "family mismatch");
1039 prop_assert_eq!(desc.exit_policy.to_string(), parsed.exit_policy.to_string(), "exit_policy mismatch");
1040 }
1041 }
1042}
1043
1044#[cfg(test)]
1045mod comprehensive_tests {
1046 use super::*;
1047
1048 #[test]
1049 fn test_edge_case_empty_family() {
1050 let content = r#"onion-key
1051-----BEGIN RSA PUBLIC KEY-----
1052MIGJAoGBAKM682Uf3wfWRaYs/y1NBNjPTMUEt7cOBhDsqQeOCCZwyE7fCVXu+Kkp
1053-----END RSA PUBLIC KEY-----
1054family
1055"#;
1056 let desc = Microdescriptor::parse(content).unwrap();
1057 assert_eq!(desc.family.len(), 0);
1058 }
1059
1060 #[test]
1061 fn test_edge_case_multiple_or_addresses_ipv4() {
1062 let content = r#"onion-key
1063-----BEGIN RSA PUBLIC KEY-----
1064MIGJAoGBAKM682Uf3wfWRaYs/y1NBNjPTMUEt7cOBhDsqQeOCCZwyE7fCVXu+Kkp
1065-----END RSA PUBLIC KEY-----
1066a 10.0.0.1:9001
1067a 192.168.1.1:9002
1068a 172.16.0.1:9003
1069"#;
1070 let desc = Microdescriptor::parse(content).unwrap();
1071 assert_eq!(desc.or_addresses.len(), 3);
1072 assert_eq!(desc.or_addresses[0].0.to_string(), "10.0.0.1");
1073 assert_eq!(desc.or_addresses[0].1, 9001);
1074 assert_eq!(desc.or_addresses[1].0.to_string(), "192.168.1.1");
1075 assert_eq!(desc.or_addresses[1].1, 9002);
1076 }
1077
1078 #[test]
1079 fn test_edge_case_multiple_or_addresses_ipv6() {
1080 let content = r#"onion-key
1081-----BEGIN RSA PUBLIC KEY-----
1082MIGJAoGBAKM682Uf3wfWRaYs/y1NBNjPTMUEt7cOBhDsqQeOCCZwyE7fCVXu+Kkp
1083-----END RSA PUBLIC KEY-----
1084a [2001:db8::1]:9001
1085a [2001:db8::2]:9002
1086a [fe80::1]:9003
1087"#;
1088 let desc = Microdescriptor::parse(content).unwrap();
1089 assert_eq!(desc.or_addresses.len(), 3);
1090 assert_eq!(desc.or_addresses[0].0.to_string(), "2001:db8::1");
1091 assert_eq!(desc.or_addresses[0].1, 9001);
1092 assert!(desc.or_addresses[0].2);
1093 assert_eq!(desc.or_addresses[1].0.to_string(), "2001:db8::2");
1094 assert_eq!(desc.or_addresses[1].1, 9002);
1095 assert!(desc.or_addresses[1].2);
1096 }
1097
1098 #[test]
1099 fn test_edge_case_mixed_ipv4_ipv6_addresses() {
1100 let content = r#"onion-key
1101-----BEGIN RSA PUBLIC KEY-----
1102MIGJAoGBAKM682Uf3wfWRaYs/y1NBNjPTMUEt7cOBhDsqQeOCCZwyE7fCVXu+Kkp
1103-----END RSA PUBLIC KEY-----
1104a 10.0.0.1:9001
1105a [2001:db8::1]:9002
1106a 192.168.1.1:9003
1107"#;
1108 let desc = Microdescriptor::parse(content).unwrap();
1109 assert_eq!(desc.or_addresses.len(), 3);
1110 assert!(!desc.or_addresses[0].2);
1111 assert!(desc.or_addresses[1].2);
1112 assert!(!desc.or_addresses[2].2);
1113 }
1114
1115 #[test]
1116 fn test_edge_case_protocol_ranges() {
1117 let content = r#"onion-key
1118-----BEGIN RSA PUBLIC KEY-----
1119MIGJAoGBAKM682Uf3wfWRaYs/y1NBNjPTMUEt7cOBhDsqQeOCCZwyE7fCVXu+Kkp
1120-----END RSA PUBLIC KEY-----
1121pr Cons=1-2 Desc=1-2 Link=1-5 Relay=1-3
1122"#;
1123 let desc = Microdescriptor::parse(content).unwrap();
1124 assert_eq!(desc.protocols.get("Cons"), Some(&vec![1, 2]));
1125 assert_eq!(desc.protocols.get("Link"), Some(&vec![1, 2, 3, 4, 5]));
1126 assert_eq!(desc.protocols.get("Relay"), Some(&vec![1, 2, 3]));
1127 }
1128
1129 #[test]
1130 fn test_edge_case_protocol_mixed_ranges() {
1131 let content = r#"onion-key
1132-----BEGIN RSA PUBLIC KEY-----
1133MIGJAoGBAKM682Uf3wfWRaYs/y1NBNjPTMUEt7cOBhDsqQeOCCZwyE7fCVXu+Kkp
1134-----END RSA PUBLIC KEY-----
1135pr Link=1-3,5,7-9
1136"#;
1137 let desc = Microdescriptor::parse(content).unwrap();
1138 let link_protos = desc.protocols.get("Link").unwrap();
1139 assert!(link_protos.contains(&1));
1140 assert!(link_protos.contains(&2));
1141 assert!(link_protos.contains(&3));
1142 assert!(link_protos.contains(&5));
1143 assert!(link_protos.contains(&7));
1144 assert!(link_protos.contains(&8));
1145 assert!(link_protos.contains(&9));
1146 assert!(!link_protos.contains(&4));
1147 assert!(!link_protos.contains(&6));
1148 }
1149
1150 #[test]
1151 fn test_edge_case_multiple_identifiers() {
1152 let content = r#"onion-key
1153-----BEGIN RSA PUBLIC KEY-----
1154MIGJAoGBAKM682Uf3wfWRaYs/y1NBNjPTMUEt7cOBhDsqQeOCCZwyE7fCVXu+Kkp
1155-----END RSA PUBLIC KEY-----
1156id ed25519 fPCuy/4J0zrIRdTQfWP0YI5PsxPOkTc0xPPvZZKGmkI
1157id rsa1024 r5572HzD+PMPBbXlZwBhsm6YEbxnYgis8vhZ1jmdI2k
1158"#;
1159 let desc = Microdescriptor::parse(content).unwrap();
1160 assert!(desc.identifiers.contains_key("ed25519"));
1161 assert!(desc.identifiers.contains_key("rsa1024"));
1162 }
1163
1164 #[test]
1165 fn test_edge_case_exit_policy_accept_all() {
1166 let content = r#"onion-key
1167-----BEGIN RSA PUBLIC KEY-----
1168MIGJAoGBAKM682Uf3wfWRaYs/y1NBNjPTMUEt7cOBhDsqQeOCCZwyE7fCVXu+Kkp
1169-----END RSA PUBLIC KEY-----
1170p accept 1-65535
1171"#;
1172 let desc = Microdescriptor::parse(content).unwrap();
1173 assert!(desc.exit_policy.can_exit_to(80));
1174 assert!(desc.exit_policy.can_exit_to(443));
1175 assert!(desc.exit_policy.can_exit_to(22));
1176 }
1177
1178 #[test]
1179 fn test_edge_case_exit_policy_reject_all() {
1180 let content = r#"onion-key
1181-----BEGIN RSA PUBLIC KEY-----
1182MIGJAoGBAKM682Uf3wfWRaYs/y1NBNjPTMUEt7cOBhDsqQeOCCZwyE7fCVXu+Kkp
1183-----END RSA PUBLIC KEY-----
1184p reject 1-65535
1185"#;
1186 let desc = Microdescriptor::parse(content).unwrap();
1187 assert!(!desc.exit_policy.can_exit_to(80));
1188 assert!(!desc.exit_policy.can_exit_to(443));
1189 assert!(!desc.exit_policy.can_exit_to(22));
1190 }
1191
1192 #[test]
1193 fn test_edge_case_exit_policy_specific_ports() {
1194 let content = r#"onion-key
1195-----BEGIN RSA PUBLIC KEY-----
1196MIGJAoGBAKM682Uf3wfWRaYs/y1NBNjPTMUEt7cOBhDsqQeOCCZwyE7fCVXu+Kkp
1197-----END RSA PUBLIC KEY-----
1198p accept 80,443,8080
1199"#;
1200 let desc = Microdescriptor::parse(content).unwrap();
1201 assert!(desc.exit_policy.can_exit_to(80));
1202 assert!(desc.exit_policy.can_exit_to(443));
1203 assert!(desc.exit_policy.can_exit_to(8080));
1204 assert!(!desc.exit_policy.can_exit_to(22));
1205 }
1206
1207 #[test]
1208 fn test_edge_case_exit_policy_v6_accept() {
1209 let content = r#"onion-key
1210-----BEGIN RSA PUBLIC KEY-----
1211MIGJAoGBAKM682Uf3wfWRaYs/y1NBNjPTMUEt7cOBhDsqQeOCCZwyE7fCVXu+Kkp
1212-----END RSA PUBLIC KEY-----
1213p6 accept 80,443
1214"#;
1215 let desc = Microdescriptor::parse(content).unwrap();
1216 assert!(desc.exit_policy_v6.is_some());
1217 assert_eq!(
1218 desc.exit_policy_v6.as_ref().unwrap().to_string(),
1219 "accept 80,443"
1220 );
1221 }
1222
1223 #[test]
1224 fn test_edge_case_exit_policy_v6_reject() {
1225 let content = r#"onion-key
1226-----BEGIN RSA PUBLIC KEY-----
1227MIGJAoGBAKM682Uf3wfWRaYs/y1NBNjPTMUEt7cOBhDsqQeOCCZwyE7fCVXu+Kkp
1228-----END RSA PUBLIC KEY-----
1229p6 reject 1-65535
1230"#;
1231 let desc = Microdescriptor::parse(content).unwrap();
1232 assert!(desc.exit_policy_v6.is_some());
1233 assert_eq!(
1235 desc.exit_policy_v6.as_ref().unwrap().to_string(),
1236 "reject *"
1237 );
1238 }
1239
1240 #[test]
1241 fn test_edge_case_unrecognized_lines() {
1242 let content = r#"onion-key
1243-----BEGIN RSA PUBLIC KEY-----
1244MIGJAoGBAKM682Uf3wfWRaYs/y1NBNjPTMUEt7cOBhDsqQeOCCZwyE7fCVXu+Kkp
1245-----END RSA PUBLIC KEY-----
1246unknown-field some value
1247another-unknown another value
1248"#;
1249 let desc = Microdescriptor::parse(content).unwrap();
1250 assert_eq!(desc.unrecognized_lines.len(), 2);
1251 assert!(desc
1252 .unrecognized_lines
1253 .contains(&"unknown-field some value".to_string()));
1254 assert!(desc
1255 .unrecognized_lines
1256 .contains(&"another-unknown another value".to_string()));
1257 }
1258
1259 #[test]
1260 fn test_validation_missing_onion_key() {
1261 let desc = Microdescriptor {
1262 onion_key: String::new(),
1263 ntor_onion_key: Some("test".to_string()),
1264 or_addresses: Vec::new(),
1265 family: Vec::new(),
1266 exit_policy: MicroExitPolicy::parse("reject 1-65535").unwrap(),
1267 exit_policy_v6: None,
1268 identifiers: HashMap::new(),
1269 protocols: HashMap::new(),
1270 raw_content: Vec::new(),
1271 annotations: Vec::new(),
1272 unrecognized_lines: Vec::new(),
1273 };
1274 let result = desc.validate();
1275 assert!(result.is_err());
1276 match result.unwrap_err() {
1277 Error::Descriptor(crate::descriptor::DescriptorError::Microdescriptor(
1278 crate::descriptor::MicrodescriptorError::MissingRequiredField(_),
1279 )) => {}
1280 _ => panic!("Expected MissingRequiredField error"),
1281 }
1282 }
1283
1284 #[test]
1285 fn test_validation_zero_port_in_or_addresses() {
1286 let desc = Microdescriptor {
1287 onion_key: "test".to_string(),
1288 ntor_onion_key: Some("test".to_string()),
1289 or_addresses: vec![("10.0.0.1".parse().unwrap(), 0, false)],
1290 family: Vec::new(),
1291 exit_policy: MicroExitPolicy::parse("reject 1-65535").unwrap(),
1292 exit_policy_v6: None,
1293 identifiers: HashMap::new(),
1294 protocols: HashMap::new(),
1295 raw_content: Vec::new(),
1296 annotations: Vec::new(),
1297 unrecognized_lines: Vec::new(),
1298 };
1299 let result = desc.validate();
1300 assert!(result.is_err());
1301 match result.unwrap_err() {
1302 Error::Descriptor(crate::descriptor::DescriptorError::Microdescriptor(
1303 crate::descriptor::MicrodescriptorError::InvalidPortPolicy(_),
1304 )) => {}
1305 _ => panic!("Expected InvalidPortPolicy error"),
1306 }
1307 }
1308
1309 #[test]
1310 fn test_validation_invalid_family_fingerprint() {
1311 let desc = Microdescriptor {
1312 onion_key: "test".to_string(),
1313 ntor_onion_key: Some("test".to_string()),
1314 or_addresses: Vec::new(),
1315 family: vec!["$ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ".to_string()],
1316 exit_policy: MicroExitPolicy::parse("reject 1-65535").unwrap(),
1317 exit_policy_v6: None,
1318 identifiers: HashMap::new(),
1319 protocols: HashMap::new(),
1320 raw_content: Vec::new(),
1321 annotations: Vec::new(),
1322 unrecognized_lines: Vec::new(),
1323 };
1324 let result = desc.validate();
1325 assert!(result.is_err());
1326 match result.unwrap_err() {
1327 Error::Descriptor(crate::descriptor::DescriptorError::Microdescriptor(
1328 crate::descriptor::MicrodescriptorError::InvalidRelayFamily(_),
1329 )) => {}
1330 _ => panic!("Expected InvalidRelayFamily error"),
1331 }
1332 }
1333
1334 #[test]
1335 fn test_validation_valid_microdescriptor() {
1336 let desc = Microdescriptor {
1337 onion_key: "test".to_string(),
1338 ntor_onion_key: Some("test".to_string()),
1339 or_addresses: vec![("10.0.0.1".parse().unwrap(), 9001, false)],
1340 family: vec!["$A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB".to_string()],
1341 exit_policy: MicroExitPolicy::parse("reject 1-65535").unwrap(),
1342 exit_policy_v6: None,
1343 identifiers: HashMap::new(),
1344 protocols: HashMap::new(),
1345 raw_content: Vec::new(),
1346 annotations: Vec::new(),
1347 unrecognized_lines: Vec::new(),
1348 };
1349 let result = desc.validate();
1350 assert!(result.is_ok());
1351 }
1352
1353 #[test]
1354 fn test_builder_basic() {
1355 let desc = MicrodescriptorBuilder::default()
1356 .onion_key("test_onion_key")
1357 .or_addresses(vec![])
1358 .family(vec![])
1359 .exit_policy(MicroExitPolicy::parse("reject 1-65535").unwrap())
1360 .identifiers(HashMap::new())
1361 .protocols(HashMap::new())
1362 .raw_content(vec![])
1363 .annotations(vec![])
1364 .unrecognized_lines(vec![])
1365 .build()
1366 .expect("Failed to build");
1367
1368 assert_eq!(desc.onion_key, "test_onion_key");
1369 assert_eq!(desc.or_addresses.len(), 0);
1370 }
1371
1372 #[test]
1373 fn test_builder_with_optional_fields() {
1374 let desc = MicrodescriptorBuilder::default()
1375 .onion_key("test_onion_key")
1376 .ntor_onion_key("test_ntor_key")
1377 .or_addresses(vec![])
1378 .family(vec![])
1379 .exit_policy(MicroExitPolicy::parse("reject 1-65535").unwrap())
1380 .exit_policy_v6(MicroExitPolicy::parse("accept 80,443").unwrap())
1381 .identifiers(HashMap::new())
1382 .protocols(HashMap::new())
1383 .raw_content(vec![])
1384 .annotations(vec![])
1385 .unrecognized_lines(vec![])
1386 .build()
1387 .expect("Failed to build");
1388
1389 assert_eq!(desc.ntor_onion_key, Some("test_ntor_key".to_string()));
1390 assert!(desc.exit_policy_v6.is_some());
1391 assert_eq!(
1392 desc.exit_policy_v6.as_ref().unwrap().to_string(),
1393 "accept 80,443"
1394 );
1395 }
1396
1397 #[test]
1398 fn test_round_trip_serialization() {
1399 let content = r#"onion-key
1400-----BEGIN RSA PUBLIC KEY-----
1401MIGJAoGBAKM682Uf3wfWRaYs/y1NBNjPTMUEt7cOBhDsqQeOCCZwyE7fCVXu+Kkp
1402-----END RSA PUBLIC KEY-----
1403ntor-onion-key r5572HzD+PMPBbXlZwBhsm6YEbxnYgis8vhZ1jmdI2k=
1404family $ABC123 $DEF456
1405p accept 80,443
1406"#;
1407 let desc1 = Microdescriptor::parse(content).unwrap();
1408 let serialized = desc1.to_descriptor_string();
1409 let desc2 = Microdescriptor::parse(&serialized).unwrap();
1410
1411 assert_eq!(desc1.onion_key, desc2.onion_key);
1412 assert_eq!(desc1.ntor_onion_key, desc2.ntor_onion_key);
1413 assert_eq!(desc1.family, desc2.family);
1414 assert_eq!(desc1.exit_policy.to_string(), desc2.exit_policy.to_string());
1415 }
1416
1417 #[test]
1418 fn test_display_implementation() {
1419 let desc = Microdescriptor {
1420 onion_key: "test".to_string(),
1421 ntor_onion_key: Some("test_ntor".to_string()),
1422 or_addresses: Vec::new(),
1423 family: Vec::new(),
1424 exit_policy: MicroExitPolicy::parse("reject 1-65535").unwrap(),
1425 exit_policy_v6: None,
1426 identifiers: HashMap::new(),
1427 protocols: HashMap::new(),
1428 raw_content: Vec::new(),
1429 annotations: Vec::new(),
1430 unrecognized_lines: Vec::new(),
1431 };
1432 let display_str = format!("{}", desc);
1433 assert!(display_str.contains("onion-key"));
1434 }
1435
1436 #[test]
1437 fn test_from_str_implementation() {
1438 let content = r#"onion-key
1439-----BEGIN RSA PUBLIC KEY-----
1440MIGJAoGBAKM682Uf3wfWRaYs/y1NBNjPTMUEt7cOBhDsqQeOCCZwyE7fCVXu+Kkp
1441-----END RSA PUBLIC KEY-----
1442"#;
1443 let desc: Microdescriptor = content.parse().unwrap();
1444 assert!(!desc.onion_key.is_empty());
1445 }
1446}