1use std::collections::HashMap;
91use std::fmt;
92use std::net::IpAddr;
93use std::str::FromStr;
94
95use crate::exit_policy::MicroExitPolicy;
96use crate::Error;
97
98use super::{compute_digest, Descriptor, DigestEncoding, DigestHash};
99
100#[derive(Debug, Clone, PartialEq)]
143pub struct Microdescriptor {
144 pub onion_key: String,
149 pub ntor_onion_key: Option<String>,
154 pub or_addresses: Vec<(IpAddr, u16, bool)>,
159 pub family: Vec<String>,
164 pub exit_policy: MicroExitPolicy,
169 pub exit_policy_v6: Option<MicroExitPolicy>,
173 pub identifiers: HashMap<String, String>,
178 pub protocols: HashMap<String, Vec<u32>>,
183 raw_content: Vec<u8>,
185 annotations: Vec<(String, Option<String>)>,
190 unrecognized_lines: Vec<String>,
192}
193
194impl Microdescriptor {
195 pub fn new(onion_key: String) -> Self {
215 Self {
216 onion_key,
217 ntor_onion_key: None,
218 or_addresses: Vec::new(),
219 family: Vec::new(),
220 exit_policy: MicroExitPolicy::parse("reject 1-65535").unwrap(),
221 exit_policy_v6: None,
222 identifiers: HashMap::new(),
223 protocols: HashMap::new(),
224 raw_content: Vec::new(),
225 annotations: Vec::new(),
226 unrecognized_lines: Vec::new(),
227 }
228 }
229
230 pub fn parse_with_annotations(content: &str, annotations: &[&str]) -> Result<Self, Error> {
265 let mut desc = Self::parse(content)?;
266 for ann in annotations {
267 let ann = ann.trim();
268 if ann.is_empty() {
269 continue;
270 }
271 let ann = ann.strip_prefix('@').unwrap_or(ann);
272 if let Some(space_pos) = ann.find(' ') {
273 let key = ann[..space_pos].to_string();
274 let value = ann[space_pos + 1..].trim().to_string();
275 desc.annotations.push((key, Some(value)));
276 } else {
277 desc.annotations.push((ann.to_string(), None));
278 }
279 }
280 Ok(desc)
281 }
282
283 pub fn get_annotations(&self) -> HashMap<String, Option<String>> {
310 self.annotations.iter().cloned().collect()
311 }
312
313 pub fn get_annotation_lines(&self) -> Vec<String> {
338 self.annotations
339 .iter()
340 .map(|(k, v)| match v {
341 Some(val) => format!("@{} {}", k, val),
342 None => format!("@{}", k),
343 })
344 .collect()
345 }
346
347 fn extract_pem_block(lines: &[&str], start_idx: usize) -> (String, usize) {
349 let mut block = String::new();
350 let mut idx = start_idx;
351 while idx < lines.len() {
352 let line = lines[idx];
353 block.push_str(line);
354 block.push('\n');
355 if line.starts_with("-----END ") {
356 break;
357 }
358 idx += 1;
359 }
360 (block.trim_end().to_string(), idx)
361 }
362
363 fn parse_or_address(line: &str) -> Result<(IpAddr, u16, bool), Error> {
365 let line = line.trim();
366 if line.starts_with('[') {
367 if let Some(bracket_end) = line.find(']') {
368 let ipv6_str = &line[1..bracket_end];
369 let port_str = &line[bracket_end + 2..];
370 let addr: IpAddr = ipv6_str.parse().map_err(|_| Error::Parse {
371 location: "a".to_string(),
372 reason: format!("invalid IPv6 address: {}", ipv6_str),
373 })?;
374 let port: u16 = port_str.parse().map_err(|_| Error::Parse {
375 location: "a".to_string(),
376 reason: format!("invalid port: {}", port_str),
377 })?;
378 return Ok((addr, port, true));
379 }
380 }
381 if let Some(colon_pos) = line.rfind(':') {
382 let addr_str = &line[..colon_pos];
383 let port_str = &line[colon_pos + 1..];
384 let addr: IpAddr = addr_str.parse().map_err(|_| Error::Parse {
385 location: "a".to_string(),
386 reason: format!("invalid address: {}", addr_str),
387 })?;
388 let port: u16 = port_str.parse().map_err(|_| Error::Parse {
389 location: "a".to_string(),
390 reason: format!("invalid port: {}", port_str),
391 })?;
392 let is_ipv6 = addr.is_ipv6();
393 return Ok((addr, port, is_ipv6));
394 }
395 Err(Error::Parse {
396 location: "a".to_string(),
397 reason: format!("invalid or-address format: {}", line),
398 })
399 }
400
401 fn parse_protocols(value: &str) -> HashMap<String, Vec<u32>> {
402 let mut protocols = HashMap::new();
403 for entry in value.split_whitespace() {
404 if let Some(eq_pos) = entry.find('=') {
405 let proto_name = &entry[..eq_pos];
406 let versions_str = &entry[eq_pos + 1..];
407 let versions: Vec<u32> = versions_str
408 .split(',')
409 .filter_map(|v| {
410 if let Some(dash) = v.find('-') {
411 let start: u32 = v[..dash].parse().ok()?;
412 let end: u32 = v[dash + 1..].parse().ok()?;
413 Some((start..=end).collect::<Vec<_>>())
414 } else {
415 v.parse().ok().map(|n| vec![n])
416 }
417 })
418 .flatten()
419 .collect();
420 protocols.insert(proto_name.to_string(), versions);
421 }
422 }
423 protocols
424 }
425
426 fn parse_identifiers(
427 value: &str,
428 identifiers: &mut HashMap<String, String>,
429 validate: bool,
430 ) -> Result<(), Error> {
431 let parts: Vec<&str> = value.split_whitespace().collect();
432 if parts.len() < 2 {
433 return Err(Error::Parse {
434 location: "id".to_string(),
435 reason: format!(
436 "'id' lines should contain both key type and digest: {}",
437 value
438 ),
439 });
440 }
441 let key_type = parts[0].to_string();
442 let key_value = parts[1].to_string();
443 if validate && identifiers.contains_key(&key_type) {
444 return Err(Error::Parse {
445 location: "id".to_string(),
446 reason: format!(
447 "There can only be one 'id' line per key type, but '{}' appeared multiple times",
448 key_type
449 ),
450 });
451 }
452 identifiers.insert(key_type, key_value);
453 Ok(())
454 }
455}
456
457impl Descriptor for Microdescriptor {
458 fn parse(content: &str) -> Result<Self, Error> {
459 let raw_content = content.as_bytes().to_vec();
460 let lines: Vec<&str> = content.lines().collect();
461
462 let mut onion_key: Option<String> = None;
463 let mut ntor_onion_key: Option<String> = None;
464 let mut or_addresses: Vec<(IpAddr, u16, bool)> = Vec::new();
465 let mut family: Vec<String> = Vec::new();
466 let mut exit_policy = MicroExitPolicy::parse("reject 1-65535")?;
467 let mut exit_policy_v6: Option<MicroExitPolicy> = None;
468 let mut identifiers: HashMap<String, String> = HashMap::new();
469 let mut protocols: HashMap<String, Vec<u32>> = HashMap::new();
470 let mut unrecognized_lines: Vec<String> = Vec::new();
471 let mut annotations: Vec<(String, Option<String>)> = Vec::new();
472
473 let mut idx = 0;
474 while idx < lines.len() {
475 let line = lines[idx];
476
477 if line.starts_with('@') {
478 let ann = line.strip_prefix('@').unwrap_or(line);
479 if let Some(space_pos) = ann.find(' ') {
480 let key = ann[..space_pos].to_string();
481 let value = ann[space_pos + 1..].trim().to_string();
482 annotations.push((key, Some(value)));
483 } else {
484 annotations.push((ann.to_string(), None));
485 }
486 idx += 1;
487 continue;
488 }
489
490 let (keyword, value) = if let Some(space_pos) = line.find(' ') {
491 (&line[..space_pos], line[space_pos + 1..].trim())
492 } else {
493 (line, "")
494 };
495
496 match keyword {
497 "onion-key" => {
498 let (block, end_idx) = Self::extract_pem_block(&lines, idx + 1);
499 onion_key = Some(block);
500 idx = end_idx;
501 }
502 "ntor-onion-key" => {
503 ntor_onion_key = Some(value.to_string());
504 }
505 "a" => {
506 if let Ok(addr) = Self::parse_or_address(value) {
507 or_addresses.push(addr);
508 }
509 }
510 "family" => {
511 family = value.split_whitespace().map(|s| s.to_string()).collect();
512 }
513 "p" => {
514 exit_policy = MicroExitPolicy::parse(value)?;
515 }
516 "p6" => {
517 exit_policy_v6 = Some(MicroExitPolicy::parse(value)?);
518 }
519 "pr" => {
520 protocols = Self::parse_protocols(value);
521 }
522 "id" => {
523 let _ = Self::parse_identifiers(value, &mut identifiers, false);
524 }
525 _ => {
526 if !line.is_empty() && !line.starts_with("-----") {
527 unrecognized_lines.push(line.to_string());
528 }
529 }
530 }
531 idx += 1;
532 }
533
534 let onion_key = onion_key.ok_or_else(|| Error::Parse {
535 location: "onion-key".to_string(),
536 reason: "Microdescriptor must have a 'onion-key' entry".to_string(),
537 })?;
538
539 Ok(Self {
540 onion_key,
541 ntor_onion_key,
542 or_addresses,
543 family,
544 exit_policy,
545 exit_policy_v6,
546 identifiers,
547 protocols,
548 raw_content,
549 annotations,
550 unrecognized_lines,
551 })
552 }
553
554 fn to_descriptor_string(&self) -> String {
555 let mut result = String::new();
556
557 result.push_str("onion-key\n");
558 result.push_str(&self.onion_key);
559 result.push('\n');
560
561 if let Some(ref ntor_key) = self.ntor_onion_key {
562 result.push_str(&format!("ntor-onion-key {}\n", ntor_key));
563 }
564
565 for (addr, port, is_ipv6) in &self.or_addresses {
566 if *is_ipv6 {
567 result.push_str(&format!("a [{}]:{}\n", addr, port));
568 } else {
569 result.push_str(&format!("a {}:{}\n", addr, port));
570 }
571 }
572
573 if !self.family.is_empty() {
574 result.push_str(&format!("family {}\n", self.family.join(" ")));
575 }
576
577 result.push_str(&format!("p {}\n", self.exit_policy));
578
579 if let Some(ref policy_v6) = self.exit_policy_v6 {
580 result.push_str(&format!("p6 {}\n", policy_v6));
581 }
582
583 if !self.protocols.is_empty() {
584 let proto_str: Vec<String> = self
585 .protocols
586 .iter()
587 .map(|(k, v)| {
588 let versions: Vec<String> = v.iter().map(|n| n.to_string()).collect();
589 format!("{}={}", k, versions.join(","))
590 })
591 .collect();
592 result.push_str(&format!("pr {}\n", proto_str.join(" ")));
593 }
594
595 for (key_type, key_value) in &self.identifiers {
596 result.push_str(&format!("id {} {}\n", key_type, key_value));
597 }
598
599 result
600 }
601
602 fn digest(&self, hash: DigestHash, encoding: DigestEncoding) -> Result<String, Error> {
603 Ok(compute_digest(&self.raw_content, hash, encoding))
604 }
605
606 fn raw_content(&self) -> &[u8] {
607 &self.raw_content
608 }
609
610 fn unrecognized_lines(&self) -> &[String] {
611 &self.unrecognized_lines
612 }
613}
614
615impl FromStr for Microdescriptor {
616 type Err = Error;
617
618 fn from_str(s: &str) -> Result<Self, Self::Err> {
619 Self::parse(s)
620 }
621}
622
623impl fmt::Display for Microdescriptor {
624 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
625 write!(f, "{}", self.to_descriptor_string())
626 }
627}
628
629#[cfg(test)]
630mod tests {
631 use super::*;
632
633 const FIRST_ONION_KEY: &str = "-----BEGIN RSA PUBLIC KEY-----
634MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
635H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
636CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
637-----END RSA PUBLIC KEY-----";
638
639 const SECOND_ONION_KEY: &str = "-----BEGIN RSA PUBLIC KEY-----
640MIGJAoGBALCOxZdpMI2WO496njSQ2M7b4IgAGATqpJmH3So7lXOa25sK6o7JipgP
641qQE83K/t/xsMIpxQ/hHkft3G78HkeXXFc9lVUzH0HmHwYEu0M+PMVULSkG36MfEl
6427WeSZzaG+Tlnh9OySAzVyTsv1ZJsTQFHH9V8wuM0GOMo9X8DFC+NAgMBAAE=
643-----END RSA PUBLIC KEY-----";
644
645 const THIRD_ONION_KEY: &str = "-----BEGIN RSA PUBLIC KEY-----
646MIGJAoGBAOWFQHxO+5kGuhwPUX5jB7wJCrTbSU0fZwolNV1t9UaDdjGDvIjIhdit
647y2sMbyd9K8lbQO7x9rQjNst5ZicuaSOs854XQddSjm++vMdjYbOcVMqnKGSztvpd
648w/1LVWFfhcBnsGi4JMGbmP+KUZG9A8kI9deSyJhfi35jA7UepiHHAgMBAAE=
649-----END RSA PUBLIC KEY-----";
650
651 fn first_microdesc() -> &'static str {
652 "@last-listed 2013-02-24 00:18:36
653onion-key
654-----BEGIN RSA PUBLIC KEY-----
655MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
656H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
657CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
658-----END RSA PUBLIC KEY-----"
659 }
660
661 fn second_microdesc() -> &'static str {
662 "@last-listed 2013-02-24 00:18:37
663onion-key
664-----BEGIN RSA PUBLIC KEY-----
665MIGJAoGBALCOxZdpMI2WO496njSQ2M7b4IgAGATqpJmH3So7lXOa25sK6o7JipgP
666qQE83K/t/xsMIpxQ/hHkft3G78HkeXXFc9lVUzH0HmHwYEu0M+PMVULSkG36MfEl
6677WeSZzaG+Tlnh9OySAzVyTsv1ZJsTQFHH9V8wuM0GOMo9X8DFC+NAgMBAAE=
668-----END RSA PUBLIC KEY-----
669ntor-onion-key r5572HzD+PMPBbXlZwBhsm6YEbxnYgis8vhZ1jmdI2k=
670family $6141629FA0D15A6AEAEF3A1BEB76E64C767B3174"
671 }
672
673 fn third_microdesc() -> &'static str {
674 "@last-listed 2013-02-24 00:18:36
675onion-key
676-----BEGIN RSA PUBLIC KEY-----
677MIGJAoGBAOWFQHxO+5kGuhwPUX5jB7wJCrTbSU0fZwolNV1t9UaDdjGDvIjIhdit
678y2sMbyd9K8lbQO7x9rQjNst5ZicuaSOs854XQddSjm++vMdjYbOcVMqnKGSztvpd
679w/1LVWFfhcBnsGi4JMGbmP+KUZG9A8kI9deSyJhfi35jA7UepiHHAgMBAAE=
680-----END RSA PUBLIC KEY-----
681a [2001:6b0:7:125::242]:9001
682p accept 80,443"
683 }
684
685 #[test]
686 fn test_parse_first_microdesc() {
687 let desc = Microdescriptor::parse(first_microdesc()).unwrap();
688 assert_eq!(desc.onion_key, FIRST_ONION_KEY);
689 assert_eq!(desc.ntor_onion_key, None);
690 assert!(desc.or_addresses.is_empty());
691 assert!(desc.family.is_empty());
692 assert!(!desc.exit_policy.is_accept);
693 assert!(desc.exit_policy.ports.iter().any(|p| p.is_wildcard()));
694 let annotations = desc.get_annotations();
695 assert_eq!(
696 annotations.get("last-listed"),
697 Some(&Some("2013-02-24 00:18:36".to_string()))
698 );
699 }
700
701 #[test]
702 fn test_parse_second_microdesc() {
703 let desc = Microdescriptor::parse(second_microdesc()).unwrap();
704 assert_eq!(desc.onion_key, SECOND_ONION_KEY);
705 assert_eq!(
706 desc.ntor_onion_key,
707 Some("r5572HzD+PMPBbXlZwBhsm6YEbxnYgis8vhZ1jmdI2k=".to_string())
708 );
709 assert!(desc.or_addresses.is_empty());
710 assert_eq!(
711 desc.family,
712 vec!["$6141629FA0D15A6AEAEF3A1BEB76E64C767B3174"]
713 );
714 assert!(!desc.exit_policy.is_accept);
715 assert!(desc.exit_policy.ports.iter().any(|p| p.is_wildcard()));
716 }
717
718 #[test]
719 fn test_parse_third_microdesc() {
720 let desc = Microdescriptor::parse(third_microdesc()).unwrap();
721 assert_eq!(desc.onion_key, THIRD_ONION_KEY);
722 assert_eq!(desc.ntor_onion_key, None);
723 assert_eq!(desc.or_addresses.len(), 1);
724 let (addr, port, is_ipv6) = &desc.or_addresses[0];
725 assert_eq!(addr.to_string(), "2001:6b0:7:125::242");
726 assert_eq!(*port, 9001);
727 assert!(*is_ipv6);
728 assert!(desc.family.is_empty());
729 assert_eq!(desc.exit_policy.to_string(), "accept 80,443");
730 }
731
732 #[test]
733 fn test_minimal_microdescriptor() {
734 let content = "onion-key
735-----BEGIN RSA PUBLIC KEY-----
736MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
737H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
738CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
739-----END RSA PUBLIC KEY-----";
740 let desc = Microdescriptor::parse(content).unwrap();
741 assert_eq!(desc.ntor_onion_key, None);
742 assert!(desc.or_addresses.is_empty());
743 assert!(desc.family.is_empty());
744 assert!(!desc.exit_policy.is_accept);
745 assert!(desc.exit_policy.ports.iter().any(|p| p.is_wildcard()));
746 assert_eq!(desc.exit_policy_v6, None);
747 assert!(desc.identifiers.is_empty());
748 assert!(desc.protocols.is_empty());
749 assert!(desc.unrecognized_lines.is_empty());
750 }
751
752 #[test]
753 fn test_unrecognized_line() {
754 let content = "onion-key
755-----BEGIN RSA PUBLIC KEY-----
756MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
757H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
758CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
759-----END RSA PUBLIC KEY-----
760pepperjack is oh so tasty!";
761 let desc = Microdescriptor::parse(content).unwrap();
762 assert_eq!(desc.unrecognized_lines, vec!["pepperjack is oh so tasty!"]);
763 }
764
765 #[test]
766 fn test_a_line() {
767 let content = "onion-key
768-----BEGIN RSA PUBLIC KEY-----
769MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
770H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
771CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
772-----END RSA PUBLIC KEY-----
773a 10.45.227.253:9001
774a [fd9f:2e19:3bcf::02:9970]:9001";
775 let desc = Microdescriptor::parse(content).unwrap();
776 assert_eq!(desc.or_addresses.len(), 2);
777 let (addr1, port1, is_ipv6_1) = &desc.or_addresses[0];
778 assert_eq!(addr1.to_string(), "10.45.227.253");
779 assert_eq!(*port1, 9001);
780 assert!(!*is_ipv6_1);
781 let (addr2, port2, is_ipv6_2) = &desc.or_addresses[1];
782 assert_eq!(addr2.to_string(), "fd9f:2e19:3bcf::2:9970");
783 assert_eq!(*port2, 9001);
784 assert!(*is_ipv6_2);
785 }
786
787 #[test]
788 fn test_family() {
789 let content = "onion-key
790-----BEGIN RSA PUBLIC KEY-----
791MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
792H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
793CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
794-----END RSA PUBLIC KEY-----
795family Amunet1 Amunet2 Amunet3";
796 let desc = Microdescriptor::parse(content).unwrap();
797 assert_eq!(desc.family, vec!["Amunet1", "Amunet2", "Amunet3"]);
798 }
799
800 #[test]
801 fn test_exit_policy() {
802 let content = "onion-key
803-----BEGIN RSA PUBLIC KEY-----
804MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
805H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
806CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
807-----END RSA PUBLIC KEY-----
808p accept 80,110,143,443";
809 let desc = Microdescriptor::parse(content).unwrap();
810 assert_eq!(desc.exit_policy.to_string(), "accept 80,110,143,443");
811 }
812
813 #[test]
814 fn test_protocols() {
815 let content = "onion-key
816-----BEGIN RSA PUBLIC KEY-----
817MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
818H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
819CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
820-----END RSA PUBLIC KEY-----
821pr Cons=1 Desc=1 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=1-4 LinkAuth=1 Microdesc=1 Relay=1-2";
822 let desc = Microdescriptor::parse(content).unwrap();
823 assert_eq!(desc.protocols.len(), 10);
824 assert_eq!(desc.protocols.get("Link"), Some(&vec![1, 2, 3, 4]));
825 assert_eq!(desc.protocols.get("Relay"), Some(&vec![1, 2]));
826 }
827
828 #[test]
829 fn test_identifier() {
830 let content = "onion-key
831-----BEGIN RSA PUBLIC KEY-----
832MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
833H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
834CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
835-----END RSA PUBLIC KEY-----
836id rsa1024 Cd47okjCHD83YGzThGBDptXs9Z4";
837 let desc = Microdescriptor::parse(content).unwrap();
838 assert_eq!(
839 desc.identifiers.get("rsa1024"),
840 Some(&"Cd47okjCHD83YGzThGBDptXs9Z4".to_string())
841 );
842 }
843
844 #[test]
845 fn test_multiple_identifiers() {
846 let content = "onion-key
847-----BEGIN RSA PUBLIC KEY-----
848MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
849H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
850CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
851-----END RSA PUBLIC KEY-----
852id rsa1024 Cd47okjCHD83YGzThGBDptXs9Z4
853id ed25519 50f6ddbecdc848dcc6b818b14d1";
854 let desc = Microdescriptor::parse(content).unwrap();
855 assert_eq!(
856 desc.identifiers.get("rsa1024"),
857 Some(&"Cd47okjCHD83YGzThGBDptXs9Z4".to_string())
858 );
859 assert_eq!(
860 desc.identifiers.get("ed25519"),
861 Some(&"50f6ddbecdc848dcc6b818b14d1".to_string())
862 );
863 }
864
865 #[test]
866 fn test_digest() {
867 let desc = Microdescriptor::parse(third_microdesc()).unwrap();
868 let digest = desc
869 .digest(DigestHash::Sha256, DigestEncoding::Base64)
870 .unwrap();
871 assert!(!digest.is_empty());
872 }
873
874 #[test]
875 fn test_missing_onion_key() {
876 let content = "ntor-onion-key r5572HzD+PMPBbXlZwBhsm6YEbxnYgis8vhZ1jmdI2k=";
877 let result = Microdescriptor::parse(content);
878 assert!(result.is_err());
879 }
880
881 #[test]
882 fn test_proceeding_line() {
883 let content = "family Amunet1
884onion-key
885-----BEGIN RSA PUBLIC KEY-----
886MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
887H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
888CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
889-----END RSA PUBLIC KEY-----";
890 let desc = Microdescriptor::parse(content).unwrap();
891 assert_eq!(desc.family, vec!["Amunet1"]);
892 }
893
894 #[test]
895 fn test_conflicting_identifiers() {
896 let content = "onion-key
897-----BEGIN RSA PUBLIC KEY-----
898MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
899H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
900CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
901-----END RSA PUBLIC KEY-----
902id rsa1024 Cd47okjCHD83YGzThGBDptXs9Z4
903id rsa1024 50f6ddbecdc848dcc6b818b14d1";
904 let desc = Microdescriptor::parse(content).unwrap();
905 assert_eq!(
906 desc.identifiers.get("rsa1024"),
907 Some(&"50f6ddbecdc848dcc6b818b14d1".to_string())
908 );
909 }
910
911 #[test]
912 fn test_exit_policy_v6() {
913 let content = "onion-key
914-----BEGIN RSA PUBLIC KEY-----
915MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
916H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
917CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
918-----END RSA PUBLIC KEY-----
919p accept 80,443
920p6 accept 80,443";
921 let desc = Microdescriptor::parse(content).unwrap();
922 assert_eq!(desc.exit_policy.to_string(), "accept 80,443");
923 assert!(desc.exit_policy_v6.is_some());
924 assert_eq!(desc.exit_policy_v6.unwrap().to_string(), "accept 80,443");
925 }
926
927 use proptest::prelude::*;
928
929 fn valid_base64_key() -> impl Strategy<Value = String> {
930 Just("MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM\nH2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF\nCxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=".to_string())
931 }
932
933 fn valid_ntor_key() -> impl Strategy<Value = String> {
934 Just("r5572HzD+PMPBbXlZwBhsm6YEbxnYgis8vhZ1jmdI2k=".to_string())
935 }
936
937 fn simple_microdescriptor() -> impl Strategy<Value = Microdescriptor> {
938 (
939 valid_base64_key(),
940 proptest::option::of(valid_ntor_key()),
941 proptest::collection::vec("[A-Za-z0-9]{1,19}", 0..3),
942 )
943 .prop_map(|(onion_key, ntor_key, family)| {
944 let mut desc = Microdescriptor::new(format!(
945 "-----BEGIN RSA PUBLIC KEY-----\n{}\n-----END RSA PUBLIC KEY-----",
946 onion_key
947 ));
948 desc.ntor_onion_key = ntor_key;
949 desc.family = family;
950 desc
951 })
952 }
953
954 proptest! {
955 #![proptest_config(ProptestConfig::with_cases(100))]
956
957 #[test]
958 fn prop_microdescriptor_roundtrip(desc in simple_microdescriptor()) {
959 let serialized = desc.to_descriptor_string();
960 let parsed = Microdescriptor::parse(&serialized);
961
962 prop_assert!(parsed.is_ok(), "Failed to parse serialized microdescriptor: {:?}", parsed.err());
963
964 let parsed = parsed.unwrap();
965
966 prop_assert_eq!(&desc.onion_key, &parsed.onion_key, "onion_key mismatch");
967 prop_assert_eq!(&desc.ntor_onion_key, &parsed.ntor_onion_key, "ntor_onion_key mismatch");
968 prop_assert_eq!(&desc.family, &parsed.family, "family mismatch");
969 prop_assert_eq!(desc.exit_policy.to_string(), parsed.exit_policy.to_string(), "exit_policy mismatch");
970 }
971 }
972}