stem_rs/descriptor/
micro.rs

1//! Microdescriptor parsing for Tor relay microdescriptors.
2//!
3//! Microdescriptors are compact relay descriptors used by Tor clients
4//! to reduce bandwidth usage. They contain a subset of server descriptor
5//! information and are referenced by their SHA-256 digest.
6//!
7//! # Overview
8//!
9//! Microdescriptors were introduced to reduce the bandwidth required for
10//! clients to learn about the Tor network. Instead of downloading full
11//! server descriptors, clients download:
12//!
13//! 1. A consensus document containing microdescriptor hashes
14//! 2. The microdescriptors themselves (much smaller than server descriptors)
15//!
16//! Microdescriptors contain only the information clients need for circuit
17//! building:
18//!
19//! - **Onion keys**: For circuit creation handshakes
20//! - **Exit policy summary**: Compact representation of allowed ports
21//! - **Protocol versions**: Supported protocol versions
22//! - **Family**: Related relays
23//!
24//! # Descriptor Format
25//!
26//! Microdescriptors use a compact text format:
27//!
28//! ```text
29//! onion-key
30//! -----BEGIN RSA PUBLIC KEY-----
31//! <base64 encoded key>
32//! -----END RSA PUBLIC KEY-----
33//! ntor-onion-key <base64 curve25519 key>
34//! a [<ipv6>]:<port>
35//! family <fingerprint> <fingerprint> ...
36//! p accept|reject <port-list>
37//! p6 accept|reject <port-list>
38//! pr <protocol>=<versions> ...
39//! id <type> <digest>
40//! ```
41//!
42//! # Annotations
43//!
44//! Microdescriptors from CollecTor archives may include annotations
45//! (lines starting with `@`) that provide metadata like when the
46//! descriptor was last seen:
47//!
48//! ```text
49//! @last-listed 2023-01-01 00:00:00
50//! onion-key
51//! ...
52//! ```
53//!
54//! # Example
55//!
56//! ```rust
57//! use stem_rs::descriptor::{Microdescriptor, Descriptor, DigestHash, DigestEncoding};
58//!
59//! let content = r#"onion-key
60//! -----BEGIN RSA PUBLIC KEY-----
61//! MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
62//! H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
63//! CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
64//! -----END RSA PUBLIC KEY-----
65//! ntor-onion-key r5572HzD+PMPBbXlZwBhsm6YEbxnYgis8vhZ1jmdI2k=
66//! p accept 80,443
67//! "#;
68//!
69//! let desc = Microdescriptor::parse(content).unwrap();
70//! println!("Has ntor key: {}", desc.ntor_onion_key.is_some());
71//! println!("Exit policy: {}", desc.exit_policy);
72//!
73//! // Compute SHA-256 digest (used for identification)
74//! let digest = desc.digest(DigestHash::Sha256, DigestEncoding::Base64).unwrap();
75//! println!("Digest: {}", digest);
76//! ```
77//!
78//! # Digest Computation
79//!
80//! Unlike server descriptors which use SHA-1, microdescriptors are
81//! identified by their SHA-256 digest. The digest is computed over
82//! the entire microdescriptor content (excluding annotations).
83//!
84//! # See Also
85//!
86//! - [`ServerDescriptor`](super::ServerDescriptor) - Full relay descriptors
87//! - [`NetworkStatusDocument`](super::NetworkStatusDocument) - Contains microdescriptor hashes
88//! - [Python Stem Microdescriptor](https://stem.torproject.org/api/descriptor/microdescriptor.html)
89
90use 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/// A microdescriptor containing compact relay information for clients.
103///
104/// Microdescriptors are designed to minimize bandwidth for Tor clients.
105/// They contain only the information needed for circuit building, omitting
106/// details like contact info, platform, and full exit policies.
107///
108/// # Fields Overview
109///
110/// | Field | Description |
111/// |-------|-------------|
112/// | `onion_key` | RSA key for TAP handshake (legacy) |
113/// | `ntor_onion_key` | Curve25519 key for ntor handshake |
114/// | `exit_policy` | Compact exit policy (ports only) |
115/// | `family` | Related relay fingerprints |
116/// | `protocols` | Supported protocol versions |
117///
118/// # Invariants
119///
120/// - `onion_key` is always present (required field)
121/// - `exit_policy` defaults to "reject 1-65535" if not specified
122///
123/// # Example
124///
125/// ```rust
126/// use stem_rs::descriptor::{Microdescriptor, Descriptor};
127///
128/// let content = r#"onion-key
129/// -----BEGIN RSA PUBLIC KEY-----
130/// MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
131/// H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
132/// CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
133/// -----END RSA PUBLIC KEY-----
134/// p accept 80,443
135/// "#;
136///
137/// let desc = Microdescriptor::parse(content).unwrap();
138/// assert!(desc.exit_policy.is_accept);
139/// ```
140///
141/// # Thread Safety
142///
143/// `Microdescriptor` is `Send` and `Sync` as it contains only owned data.
144#[derive(Debug, Clone, PartialEq, Builder)]
145#[builder(setter(into, strip_option))]
146pub struct Microdescriptor {
147    /// RSA onion key for TAP circuit handshake (PEM format).
148    ///
149    /// This is the legacy key used for the original Tor handshake.
150    /// Modern clients prefer the ntor handshake using `ntor_onion_key`.
151    pub onion_key: String,
152    /// Curve25519 onion key for ntor circuit handshake (base64).
153    ///
154    /// This is the modern key used for the ntor handshake, which provides
155    /// better security properties than the TAP handshake.
156    #[builder(default)]
157    pub ntor_onion_key: Option<String>,
158    /// Additional addresses (IPv4 or IPv6) the relay listens on.
159    ///
160    /// Each tuple is (address, port, is_ipv6). The `a` lines in the
161    /// microdescriptor provide these additional addresses.
162    pub or_addresses: Vec<(IpAddr, u16, bool)>,
163    /// Fingerprints of related relays (same operator).
164    ///
165    /// These are typically prefixed with `$` and contain the full
166    /// 40-character hex fingerprint.
167    pub family: Vec<String>,
168    /// Compact IPv4 exit policy.
169    ///
170    /// Unlike full exit policies, microdescriptor policies only specify
171    /// which ports are accepted or rejected, not addresses.
172    pub exit_policy: MicroExitPolicy,
173    /// Compact IPv6 exit policy.
174    ///
175    /// Separate policy for IPv6 traffic, if different from IPv4.
176    #[builder(default)]
177    pub exit_policy_v6: Option<MicroExitPolicy>,
178    /// Identity key digests by type.
179    ///
180    /// Maps key type (e.g., "rsa1024", "ed25519") to the base64-encoded
181    /// digest of that key.
182    pub identifiers: HashMap<String, String>,
183    /// Supported protocol versions.
184    ///
185    /// Maps protocol name to list of supported versions.
186    /// Common protocols include "Link", "Relay", "HSDir", etc.
187    pub protocols: HashMap<String, Vec<u32>>,
188    /// Raw bytes of the original descriptor content.
189    raw_content: Vec<u8>,
190    /// Annotations from CollecTor archives.
191    ///
192    /// Each annotation is a (key, optional_value) pair from lines
193    /// starting with `@`.
194    annotations: Vec<(String, Option<String>)>,
195    /// Lines that were not recognized during parsing.
196    unrecognized_lines: Vec<String>,
197}
198
199impl Microdescriptor {
200    /// Validates the microdescriptor for correctness and consistency.
201    ///
202    /// Performs comprehensive validation including:
203    /// - Onion key presence (required field)
204    /// - Port validity in or-addresses
205    /// - Family fingerprint format
206    ///
207    /// # Returns
208    ///
209    /// `Ok(())` if validation passes, otherwise returns a descriptive error.
210    ///
211    /// # Example
212    ///
213    /// ```rust,no_run
214    /// use stem_rs::descriptor::{Microdescriptor, Descriptor};
215    ///
216    /// let content = std::fs::read_to_string("microdescriptor").unwrap();
217    /// let microdesc = Microdescriptor::parse(&content).unwrap();
218    ///
219    /// match microdesc.validate() {
220    ///     Ok(()) => println!("Microdescriptor is valid"),
221    ///     Err(e) => eprintln!("Validation failed: {}", e),
222    /// }
223    /// ```
224    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    /// Creates a new microdescriptor with the given onion key.
266    ///
267    /// This creates a descriptor with default values for optional fields.
268    /// The exit policy defaults to rejecting all ports.
269    ///
270    /// # Arguments
271    ///
272    /// * `onion_key` - The RSA onion key in PEM format
273    ///
274    /// # Example
275    ///
276    /// ```rust
277    /// use stem_rs::descriptor::Microdescriptor;
278    ///
279    /// let key = "-----BEGIN RSA PUBLIC KEY-----\n...\n-----END RSA PUBLIC KEY-----";
280    /// let desc = Microdescriptor::new(key.to_string());
281    /// assert!(desc.ntor_onion_key.is_none());
282    /// assert!(desc.family.is_empty());
283    /// ```
284    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    /// Parses a microdescriptor with additional annotations.
301    ///
302    /// This is useful when annotations are provided separately from
303    /// the descriptor content (e.g., when reading from a cache file
304    /// where annotations precede the descriptor).
305    ///
306    /// # Arguments
307    ///
308    /// * `content` - The microdescriptor content
309    /// * `annotations` - Additional annotation lines (with or without `@` prefix)
310    ///
311    /// # Errors
312    ///
313    /// Returns [`Error::Parse`] if the content is malformed.
314    ///
315    /// # Example
316    ///
317    /// ```rust
318    /// use stem_rs::descriptor::Microdescriptor;
319    ///
320    /// let content = r#"onion-key
321    /// -----BEGIN RSA PUBLIC KEY-----
322    /// MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
323    /// H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
324    /// CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
325    /// -----END RSA PUBLIC KEY-----
326    /// "#;
327    ///
328    /// let annotations = &["@last-listed 2023-01-01 00:00:00"];
329    /// let desc = Microdescriptor::parse_with_annotations(content, annotations).unwrap();
330    ///
331    /// let anns = desc.get_annotations();
332    /// assert!(anns.contains_key("last-listed"));
333    /// ```
334    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    /// Returns all annotations as a map.
354    ///
355    /// Annotations are metadata lines starting with `@` that may be
356    /// present in CollecTor archives.
357    ///
358    /// # Example
359    ///
360    /// ```rust
361    /// use stem_rs::descriptor::{Microdescriptor, Descriptor};
362    ///
363    /// let content = r#"@last-listed 2023-01-01 00:00:00
364    /// onion-key
365    /// -----BEGIN RSA PUBLIC KEY-----
366    /// MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
367    /// H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
368    /// CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
369    /// -----END RSA PUBLIC KEY-----
370    /// "#;
371    ///
372    /// let desc = Microdescriptor::parse(content).unwrap();
373    /// let annotations = desc.get_annotations();
374    ///
375    /// if let Some(Some(date)) = annotations.get("last-listed") {
376    ///     println!("Last listed: {}", date);
377    /// }
378    /// ```
379    pub fn get_annotations(&self) -> HashMap<String, Option<String>> {
380        self.annotations.iter().cloned().collect()
381    }
382
383    /// Returns annotations formatted as lines with `@` prefix.
384    ///
385    /// This is useful for serializing annotations back to their
386    /// original format.
387    ///
388    /// # Example
389    ///
390    /// ```rust
391    /// use stem_rs::descriptor::{Microdescriptor, Descriptor};
392    ///
393    /// let content = r#"@last-listed 2023-01-01 00:00:00
394    /// onion-key
395    /// -----BEGIN RSA PUBLIC KEY-----
396    /// MIGJAoGBAMhPQtZPaxP3ukybV5LfofKQr20/ljpRk0e9IlGWWMSTkfVvBcHsa6IM
397    /// H2KE6s4uuPHp7FqhakXAzJbODobnPHY8l1E4efyrqMQZXEQk2IMhgSNtG6YqUrVF
398    /// CxdSKSSy0mmcBe2TOyQsahlGZ9Pudxfnrey7KcfqnArEOqNH09RpAgMBAAE=
399    /// -----END RSA PUBLIC KEY-----
400    /// "#;
401    ///
402    /// let desc = Microdescriptor::parse(content).unwrap();
403    /// for line in desc.get_annotation_lines() {
404    ///     println!("{}", line);
405    /// }
406    /// ```
407    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    /// Extracts a PEM block from lines starting at the given index.
418    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    /// Parses an `a` (or-address) line.
434    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        // MicroExitPolicy normalizes "reject 1-65535" to "reject *"
1234        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}