Skip to main content

walnut_dns/rr/
rset.rs

1use hickory_proto::rr::{DNSClass, LowerName, RData, RecordData, RecordType, RrKey};
2
3use super::{AsHickory, SerialNumber, record::Record, ttl::TimeToLive};
4use hickory_proto::rr::Name;
5
6/// A collection of like resource records.
7///
8/// The collection consists of a list of resource records of the same type, and
9/// a separate list of the singing records which secure these resource recrods.
10///
11/// The entire record set has a common TTL, and tracks the SOA Serial Number when
12/// it was last updated.
13#[derive(Debug, Clone, PartialEq)]
14pub struct RecordSet {
15    name: Name,
16    record_type: RecordType,
17    dns_class: DNSClass,
18    ttl: TimeToLive,
19    records: Vec<Record>,
20    rrsigs: Vec<Record>,
21    serial: SerialNumber,
22}
23
24impl RecordSet {
25    /// Create a new empty record set
26    ///
27    /// Creates a new record set for the specified name and record type.
28    /// Records can be added later using the insert method.
29    ///
30    /// # Arguments
31    ///
32    /// * `name` - The DNS name for this record set
33    /// * `record_type` - The DNS record type (A, AAAA, CNAME, etc.)
34    ///
35    /// # Returns
36    ///
37    /// A new empty record set
38    pub fn new(name: Name, record_type: RecordType) -> Self {
39        Self {
40            name,
41            record_type,
42            dns_class: DNSClass::IN,
43            ttl: TimeToLive::ZERO,
44            records: Vec::new(),
45            rrsigs: Vec::new(),
46            serial: SerialNumber::ZERO,
47        }
48    }
49
50    /// Create a record set from a single record
51    ///
52    /// Creates a new record set containing the specified record.
53    ///
54    /// # Arguments
55    ///
56    /// * `name` - The DNS name for this record set
57    /// * `record` - The initial record to include
58    ///
59    /// # Returns
60    ///
61    /// A new record set containing the specified record
62    pub fn from_record<R: RecordData>(name: Name, record: Record<R>) -> Self {
63        let record = record.into_record_rdata();
64        let mut rrset = Self {
65            name,
66            record_type: record.record_type(),
67            dns_class: DNSClass::IN,
68            ttl: record.ttl(),
69            records: Vec::new(),
70            rrsigs: Vec::new(),
71            serial: SerialNumber::ZERO,
72        };
73        // This is a single record, so it is safe to skip the deduplication checks, updates, etc.
74        rrset.records.push(record);
75        rrset
76    }
77
78    /// Get the DNS name for this record set
79    ///
80    /// Returns the fully qualified domain name that all records in this set apply to.
81    ///
82    /// # Returns
83    ///
84    /// A reference to the DNS name
85    pub fn name(&self) -> &Name {
86        &self.name
87    }
88
89    /// Get the record type for this record set
90    ///
91    /// Returns the DNS record type (A, AAAA, CNAME, etc.) that all records
92    /// in this set share.
93    ///
94    /// # Returns
95    ///
96    /// The DNS record type
97    pub fn record_type(&self) -> RecordType {
98        self.record_type
99    }
100
101    /// Get the DNS class for this record set
102    ///
103    /// Returns the DNS class (typically IN for Internet) that all records
104    /// in this set share.
105    ///
106    /// # Returns
107    ///
108    /// The DNS class
109    pub fn dns_class(&self) -> DNSClass {
110        self.dns_class
111    }
112
113    /// Set the DNS class for this record set
114    ///
115    /// Updates the DNS class for all records in this set.
116    ///
117    /// # Arguments
118    ///
119    /// * `dns_class` - The new DNS class
120    ///
121    /// # Returns
122    ///
123    /// A mutable reference to this record set for method chaining
124    pub fn set_dns_class(&mut self, dns_class: DNSClass) -> &mut Self {
125        self.dns_class = dns_class;
126        self
127    }
128
129    /// Set the Time To Live for all records in this set
130    ///
131    /// Updates the TTL value for all records in this set, which determines
132    /// how long DNS resolvers should cache these records.
133    ///
134    /// # Arguments
135    ///
136    /// * `ttl` - The new TTL value
137    ///
138    /// # Returns
139    ///
140    /// A mutable reference to this record set for method chaining
141    pub fn set_ttl(&mut self, ttl: TimeToLive) -> &mut Self {
142        self.ttl = ttl;
143        for r in &mut self.records {
144            r.set_ttl(ttl);
145        }
146        self
147    }
148
149    /// Create a copy of this record set with a different name
150    ///
151    /// Creates a new record set with the same records but a different DNS name.
152    /// RRSIG signatures are cleared since they would be invalid for the new name.
153    ///
154    /// # Arguments
155    ///
156    /// * `name` - The new DNS name for the copied record set
157    ///
158    /// # Returns
159    ///
160    /// A new record set with the specified name
161    pub fn with_name(&self, name: Name) -> Self {
162        RecordSet {
163            name: name.clone(),
164            record_type: self.record_type,
165            dns_class: self.dns_class,
166            ttl: self.ttl,
167            serial: self.serial,
168            records: self.records.clone(),
169            rrsigs: Default::default(),
170        }
171    }
172
173    /// Get the Time To Live for this record set
174    ///
175    /// Returns the TTL value which determines how long DNS resolvers
176    /// should cache records from this set.
177    ///
178    /// # Returns
179    ///
180    /// The TTL value
181    pub fn ttl(&self) -> TimeToLive {
182        self.ttl
183    }
184
185    /// Get the serial number for this record set
186    ///
187    /// Returns the serial number that tracks the last update to this record set.
188    ///
189    /// # Returns
190    ///
191    /// The serial number
192    pub fn serial(&self) -> SerialNumber {
193        self.serial
194    }
195}
196
197impl RecordSet {
198    /// Record Lookup Key for this Record Set
199    pub fn rrkey(&self) -> RrKey {
200        RrKey::new(self.name().into(), self.record_type())
201    }
202
203    /// Get an iterator over all records in this set
204    ///
205    /// Returns an iterator that yields all DNS records in the set,
206    /// excluding any RRSIG signatures.
207    ///
208    /// # Returns
209    ///
210    /// An iterator over the records
211    pub fn records(&self) -> impl Iterator<Item = &Record> {
212        self.records.iter()
213    }
214
215    pub(crate) fn records_mut(&mut self) -> impl Iterator<Item = &mut Record> {
216        self.records.iter_mut()
217    }
218
219    /// Convert this record set into an iterator over its records
220    ///
221    /// Consumes the record set and returns an iterator over all DNS records,
222    /// excluding any RRSIG signatures.
223    ///
224    /// # Returns
225    ///
226    /// An iterator that takes ownership of the records
227    pub fn into_records(self) -> impl Iterator<Item = Record> {
228        self.records.into_iter()
229    }
230
231    /// Convert this record set into an iterator over all records and signatures
232    ///
233    /// Consumes the record set and returns an iterator over all DNS records
234    /// and their RRSIG signatures.
235    ///
236    /// # Returns
237    ///
238    /// An iterator that takes ownership of all records and signatures
239    pub fn into_signed_records(self) -> impl Iterator<Item = Record> {
240        self.records.into_iter().chain(self.rrsigs)
241    }
242
243    /// Get an iterator over all records and signatures in this set
244    ///
245    /// Returns an iterator that yields all DNS records in the set along
246    /// with any RRSIG signatures.
247    ///
248    /// # Returns
249    ///
250    /// An iterator over all records and signatures
251    pub fn signed_records(&self) -> impl Iterator<Item = &Record> {
252        self.records.iter().chain(self.rrsigs.iter())
253    }
254
255    /// Check if this record set is empty
256    ///
257    /// Returns true if the record set contains no DNS records.
258    /// RRSIG signatures are not counted.
259    ///
260    /// # Returns
261    ///
262    /// `true` if the record set contains no records
263    pub fn is_empty(&self) -> bool {
264        self.records.is_empty()
265    }
266
267    /// Get the number of records in this set
268    ///
269    /// Returns the count of DNS records in the set.
270    /// RRSIG signatures are not counted.
271    ///
272    /// # Returns
273    ///
274    /// The number of records
275    pub fn len(&self) -> usize {
276        self.records.len()
277    }
278
279    /// Get all RRSIG signatures for this record set
280    ///
281    /// Returns a slice containing all RRSIG records that provide
282    /// DNSSEC signatures for the records in this set.
283    ///
284    /// # Returns
285    ///
286    /// A slice of RRSIG records
287    pub fn rrsigs(&self) -> &[Record] {
288        &self.rrsigs
289    }
290
291    #[allow(dead_code)]
292    pub(crate) fn rrsigs_mut(&mut self) -> &mut Vec<Record> {
293        &mut self.rrsigs
294    }
295
296    /// Insert an RRSIG signature for this record set
297    ///
298    /// Adds a DNSSEC signature (RRSIG) to this record set. The signature must
299    /// cover the correct record type. Multiple signatures can be added.
300    ///
301    /// # Arguments
302    ///
303    /// * `rrsig` - The RRSIG record to add
304    ///
305    /// # Returns
306    ///
307    /// `Ok(())` on success, or an error if the signature is invalid
308    ///
309    /// # Errors
310    ///
311    /// Returns an error if the RRSIG doesn't cover the correct record type
312    pub fn insert_rrsig(&mut self, rrsig: Record) -> Result<(), Mismatch> {
313        if rrsig
314            .rdata()
315            .as_dnssec()
316            .and_then(|dnssecdata| dnssecdata.as_rrsig())
317            .map(|rrsig| rrsig.type_covered())
318            != Some(self.record_type)
319        {
320            return Err(Mismatch("RRSIG Covered type"));
321        }
322
323        self.rrsigs.push(rrsig);
324        Ok(())
325    }
326
327    /// Clear all RRSIG signatures from this record set
328    ///
329    /// Removes all DNSSEC signatures. This is useful when the record set
330    /// is updated or when cryptographic keys are rotated.
331    pub fn clear_rrsigs(&mut self) {
332        self.rrsigs.clear()
333    }
334
335    /// Add a new record to this record set
336    ///
337    /// Creates a new record with the specified resource data and adds it
338    /// to this record set. The record will use the set's name and TTL.
339    ///
340    /// # Arguments
341    ///
342    /// * `rdata` - The resource data for the new record
343    ///
344    /// # Returns
345    ///
346    /// `Ok(true)` if the record was added, `Ok(false)` if it was a duplicate,
347    /// or an error if the record is incompatible with this set
348    ///
349    /// # Errors
350    ///
351    /// Returns an error if the record type doesn't match this set
352    pub fn push(&mut self, rdata: RData) -> Result<bool, Mismatch> {
353        let record = Record::from_rdata(self.name.clone(), self.ttl, rdata);
354        self.insert(record, SerialNumber::ZERO)
355    }
356
357    fn updated(&mut self, serial: SerialNumber) {
358        self.serial = serial;
359        self.rrsigs.clear(); // on updates, the rrsigs are invalid
360    }
361
362    /// Insert a record into this record set
363    ///
364    /// Adds a record to this set, handling deduplication and special rules
365    /// for different record types (SOA, CNAME, etc.). Updates the serial number
366    /// if the record set is modified.
367    ///
368    /// # Arguments
369    ///
370    /// * `record` - The record to insert
371    /// * `serial` - The serial number for this update
372    ///
373    /// # Returns
374    ///
375    /// `Ok(true)` if the record set was modified, `Ok(false)` if no change occurred,
376    /// or an error if the record is incompatible
377    ///
378    /// # Errors
379    ///
380    /// Returns an error if the record's name or type doesn't match this set
381    pub fn insert(&mut self, record: Record, serial: SerialNumber) -> Result<bool, Mismatch> {
382        if record.name() != self.name() {
383            return Err(Mismatch("name"));
384        }
385        if record.record_type() != self.record_type() {
386            return Err(Mismatch("type"));
387        }
388
389        //TODO: Why only discard inserts when the SOA serials differ? The serial
390        // argument is otherwise just used to track the last update?
391
392        // 1.1.5. The following RR types cannot be appended to an RRset.  If the
393        //  following comparison rules are met, then an attempt to add the new RR
394        //  will result in the replacement of the previous RR:
395        match record.record_type() {
396            RecordType::SOA => {
397                assert!(self.records.len() <= 1);
398
399                // SOA    compare only NAME, CLASS and TYPE -- it is not possible to
400                //         have more than one SOA per zone, even if any of the data
401                //         fields differ.
402                if let Some(soa_record) = self.records.first() {
403                    match soa_record.rdata() {
404                        RData::SOA(existing_soa) => {
405                            if let RData::SOA(new_soa) = record.rdata() {
406                                if SerialNumber::from(new_soa.serial())
407                                    <= SerialNumber::from(existing_soa.serial())
408                                {
409                                    tracing::debug!(
410                                        "update SOA ignored serial out of date: {:?} <= {:?}",
411                                        new_soa,
412                                        existing_soa
413                                    );
414                                    return Ok(false);
415                                }
416                            }
417                        }
418                        _ => unreachable!("Wrong rdata, expected SOA rdata"),
419                    }
420                }
421                self.records.clear();
422            }
423            // CNAME  compare only NAME, CLASS, and TYPE -- it is not possible
424            //         to have more than one CNAME RR, even if their data fields
425            //         differ.
426            RecordType::CNAME | RecordType::ANAME => {
427                assert!(self.records.len() <= 1);
428                self.records.clear();
429            }
430            _ => (),
431        };
432
433        let mut ttl_modified = false;
434        let mut found = false;
435
436        for target in &mut self.records {
437            if target.rdata() == record.rdata() {
438                found = true;
439                if target.ttl() != record.ttl() {
440                    // Something is different, but RData is the same
441                    ttl_modified = true
442                }
443                if target.expires() != record.expires() {
444                    // The expiration date has updated. Since this isn't a canonical RR field,
445                    // we don't trigger the serial number etc. in this case.
446                    if let Some(expires) = record.expires() {
447                        target.set_expires(expires);
448                    } else {
449                        target.clear_expires();
450                    }
451                }
452            }
453        }
454
455        if ttl_modified {
456            self.set_ttl(record.ttl());
457            self.updated(serial);
458            return Ok(true);
459        }
460
461        if !found {
462            self.set_ttl(record.ttl());
463            self.records.push(record);
464            self.updated(serial);
465            return Ok(true);
466        }
467
468        Ok(false)
469    }
470
471    /// Remove a record from this record set
472    ///
473    /// Removes records matching the specified record's resource data.
474    /// Some record types (SOA, last NS) have special protection against removal.
475    ///
476    /// # Arguments
477    ///
478    /// * `record` - The record to remove (matched by resource data)
479    /// * `serial` - The serial number for this update
480    ///
481    /// # Returns
482    ///
483    /// `Ok(true)` if the record set was modified, `Ok(false)` if no change occurred,
484    /// or an error if the record is incompatible
485    ///
486    /// # Errors
487    ///
488    /// Returns an error if the record's name or type doesn't match this set
489    pub fn remove(&mut self, record: &Record, serial: SerialNumber) -> Result<bool, Mismatch> {
490        if record.name() != self.name() {
491            return Err(Mismatch("name"));
492        }
493        if record.record_type() != self.record_type() {
494            return Err(Mismatch("type"));
495        }
496
497        match record.record_type() {
498            // never delete the last NS record
499            RecordType::NS => {
500                if self.records.len() <= 1 {
501                    tracing::warn!("ignoring delete of last NS record: {:?}", record);
502                    return Ok(false);
503                }
504            }
505            // never delete SOA
506            RecordType::SOA => {
507                tracing::warn!("ignored delete of SOA");
508                return Ok(false);
509            }
510            _ => (), // move on to the delete
511        }
512
513        let before = self.records.len();
514        self.records
515            .retain(|target| target.rdata() != record.rdata());
516        if before != self.records.len() {
517            self.updated(serial);
518            Ok(true)
519        } else {
520            Ok(false)
521        }
522    }
523
524    /// Give a query type, if this record set points to a new name (e.g. via CNAME, ANAME, or another record which must be resolved)
525    /// return the new name to resolve and the record type we are searching for now.
526    pub(crate) fn next_lookup_name(
527        &self,
528        query_type: RecordType,
529    ) -> Option<(LowerName, RecordType)> {
530        match (self.record_type(), query_type) {
531            (t @ RecordType::ANAME, RecordType::A)
532            | (t @ RecordType::ANAME, RecordType::AAAA)
533            | (t @ RecordType::ANAME, RecordType::ANAME) => {
534                self.records().next().and_then(|record| {
535                    record
536                        .rdata()
537                        .as_aname()
538                        .map(|aname| (LowerName::new(&aname.0), t))
539                        .inspect(|(name, record_type)| {
540                            tracing::trace!("Next name: {}, record type: {:?}", name, record_type);
541                        })
542                })
543            }
544            (t @ RecordType::NS, RecordType::NS) => self
545                .records()
546                .next()
547                .and_then(|record| record.rdata().as_ns().map(|ns| (LowerName::from(&ns.0), t)))
548                .inspect(|(name, record_type)| {
549                    tracing::trace!("Next name: {}, record type: {:?}", name, record_type);
550                }),
551            (t @ RecordType::CNAME, _) => self.records().next().and_then(|record| {
552                record
553                    .rdata()
554                    .as_cname()
555                    .map(|cname| (LowerName::from(&cname.0), t))
556                    .inspect(|(name, record_type)| {
557                        tracing::trace!("Next name: {}, record type: {:?}", name, record_type);
558                    })
559            }),
560            (t @ RecordType::MX, RecordType::MX) => self.records().next().and_then(|record| {
561                record
562                    .rdata()
563                    .as_mx()
564                    .map(|mx| (LowerName::from(mx.exchange()), t))
565                    .inspect(|(name, record_type)| {
566                        tracing::trace!("Next name: {}, record type: {:?}", name, record_type);
567                    })
568            }),
569            (t @ RecordType::SRV, RecordType::SRV) => self.records().next().and_then(|record| {
570                record
571                    .rdata()
572                    .as_srv()
573                    .map(|srv| (LowerName::from(srv.target()), t))
574                    .inspect(|(name, record_type)| {
575                        tracing::trace!("Next name: {}, record type: {:?}", name, record_type);
576                    })
577            }),
578            _ => None,
579        }
580    }
581}
582
583impl From<Record> for RecordSet {
584    fn from(record: Record) -> Self {
585        let mut rrset = RecordSet::new(record.name().clone(), record.record_type());
586        rrset.insert(record, SerialNumber::ZERO).unwrap();
587        rrset
588    }
589}
590
591impl From<hickory_proto::rr::RecordSet> for RecordSet {
592    fn from(value: hickory_proto::rr::RecordSet) -> Self {
593        let mut rrset = RecordSet::new(value.name().clone(), value.record_type());
594        let parts = value.into_parts();
595
596        for record in parts.records {
597            rrset.insert(record.into(), parts.serial.into()).unwrap();
598        }
599        for record in parts.rrsigs {
600            rrset.insert_rrsig(record.into()).unwrap();
601        }
602
603        rrset
604    }
605}
606
607impl AsHickory for RecordSet {
608    type Hickory = hickory_proto::rr::RecordSet;
609
610    fn as_hickory(&self) -> Self::Hickory {
611        let mut rset = hickory_proto::rr::RecordSet::new(
612            self.name().clone(),
613            self.record_type(),
614            self.serial().get(),
615        );
616        for record in self.records() {
617            rset.insert(
618                record.as_hickory().into_record_of_rdata(),
619                self.serial().get(),
620            );
621        }
622
623        for record in self.rrsigs() {
624            rset.insert_rrsig(record.as_hickory().into_record_of_rdata());
625        }
626
627        rset
628    }
629}
630
631/// Mismatch between record and recordset properties
632///
633/// An error that indicates a record is incompatible with an existing
634/// record set.
635#[derive(Debug, Clone, Copy, thiserror::Error)]
636#[error("Mismatched {0} between new record and record set")]
637pub struct Mismatch(pub(super) &'static str);
638
639#[cfg(test)]
640mod tests {
641    use std::net::Ipv4Addr;
642
643    use super::*;
644    use hickory_proto::rr::{RecordType, rdata::A};
645
646    fn create_test_name() -> Name {
647        Name::from_utf8("test.example.com").unwrap()
648    }
649
650    fn create_test_a_record() -> Record {
651        let name = create_test_name();
652        let ttl = TimeToLive::from(300);
653        let rdata = A::new(192, 168, 1, 1);
654        Record::from_rdata(name, ttl, rdata).into_record_rdata()
655    }
656
657    #[test]
658    fn test_recordset_new() {
659        let name = create_test_name();
660        let rrset = RecordSet::new(name.clone(), RecordType::A);
661
662        assert_eq!(rrset.name(), &name);
663        assert_eq!(rrset.record_type(), RecordType::A);
664        assert_eq!(rrset.dns_class(), DNSClass::IN);
665        assert_eq!(rrset.ttl(), TimeToLive::ZERO);
666        assert!(rrset.is_empty());
667        assert_eq!(rrset.len(), 0);
668        assert_eq!(rrset.serial(), SerialNumber::ZERO);
669    }
670
671    #[test]
672    fn test_recordset_from_record() {
673        let name = create_test_name();
674        let record = create_test_a_record();
675        let ttl = record.ttl();
676
677        let rrset = RecordSet::from_record(name.clone(), record);
678
679        assert_eq!(rrset.name(), &name);
680        assert_eq!(rrset.record_type(), RecordType::A);
681        assert_eq!(rrset.dns_class(), DNSClass::IN);
682        assert_eq!(rrset.ttl(), ttl);
683        assert!(!rrset.is_empty());
684        assert_eq!(rrset.len(), 1);
685    }
686
687    #[test]
688    fn test_recordset_with_name() {
689        let name1 = create_test_name();
690        let name2 = Name::from_utf8("other.example.com").unwrap();
691        let record = create_test_a_record();
692
693        let rrset1 = RecordSet::from_record(name1, record);
694        let rrset2 = rrset1.with_name(name2.clone());
695
696        assert_eq!(rrset2.name(), &name2);
697        assert_eq!(rrset2.record_type(), RecordType::A);
698        assert_eq!(rrset2.len(), 1);
699        assert!(rrset2.rrsigs().is_empty()); // RRSIGs should be cleared
700    }
701
702    #[test]
703    fn test_recordset_push() {
704        let name = create_test_name();
705        let mut rrset = RecordSet::new(name, RecordType::A);
706        let rdata = RData::A(A::new(192, 168, 1, 1));
707
708        let result = rrset.push(rdata);
709        assert!(result.is_ok());
710        assert!(result.unwrap());
711        assert_eq!(rrset.len(), 1);
712        assert!(!rrset.is_empty());
713    }
714
715    #[test]
716    fn test_recordset_set_ttl() {
717        let name = create_test_name();
718        let record = create_test_a_record();
719        let mut rrset = RecordSet::from_record(name, record);
720
721        let new_ttl = TimeToLive::from(600);
722        rrset.set_ttl(new_ttl);
723
724        assert_eq!(rrset.ttl(), new_ttl);
725        for record in rrset.records() {
726            assert_eq!(record.ttl(), new_ttl);
727        }
728    }
729
730    #[test]
731    fn test_recordset_set_dns_class() {
732        let name = create_test_name();
733        let mut rrset = RecordSet::new(name, RecordType::A);
734
735        rrset.set_dns_class(DNSClass::CH);
736        assert_eq!(rrset.dns_class(), DNSClass::CH);
737    }
738
739    #[test]
740    fn test_recordset_insert_duplicate() {
741        let name = create_test_name();
742        let mut rrset = RecordSet::new(name.clone(), RecordType::A);
743        let rdata = RData::A(A::new(192, 168, 1, 1));
744
745        // Insert first record
746        let record1 = Record::from_rdata(name.clone(), TimeToLive::from(300), rdata.clone())
747            .into_record_rdata();
748        let result1 = rrset.insert(record1, SerialNumber::from(1));
749        assert!(result1.is_ok());
750        assert!(result1.unwrap());
751        assert_eq!(rrset.len(), 1);
752
753        // Insert duplicate record
754        let record2 = Record::from_rdata(name, TimeToLive::from(300), rdata).into_record_rdata();
755        let result2 = rrset.insert(record2, SerialNumber::from(2));
756        assert!(result2.is_ok());
757        assert!(!result2.unwrap()); // Should return false for duplicate
758        assert_eq!(rrset.len(), 1); // Length should not change
759    }
760
761    #[test]
762    fn test_recordset_insert_wrong_name() {
763        let name1 = create_test_name();
764        let name2 = Name::from_utf8("other.example.com").unwrap();
765        let mut rrset = RecordSet::new(name1, RecordType::A);
766
767        let record = Record::from_rdata(
768            name2,
769            TimeToLive::from(300),
770            RData::A(A::new(192, 168, 1, 1)),
771        )
772        .into_record_rdata();
773        let result = rrset.insert(record, SerialNumber::from(1));
774
775        assert!(result.is_err());
776        if let Err(Mismatch(msg)) = result {
777            assert_eq!(msg, "name");
778        }
779    }
780
781    #[test]
782    fn test_recordset_insert_wrong_type() {
783        let name = create_test_name();
784        let mut rrset = RecordSet::new(name.clone(), RecordType::A);
785
786        let record = Record::from_rdata(
787            name,
788            TimeToLive::from(300),
789            RData::CNAME(hickory_proto::rr::rdata::CNAME(
790                Name::from_utf8("target.example.com").unwrap(),
791            )),
792        )
793        .into_record_rdata();
794        let result = rrset.insert(record, SerialNumber::from(1));
795
796        assert!(result.is_err());
797        if let Err(Mismatch(msg)) = result {
798            assert_eq!(msg, "type");
799        }
800    }
801
802    #[test]
803    fn test_recordset_remove() {
804        let name = create_test_name();
805        let mut rrset = RecordSet::new(name.clone(), RecordType::A);
806        let rdata = RData::A(A::new(192, 168, 1, 1));
807        let record = Record::from_rdata(name, TimeToLive::from(300), rdata).into_record_rdata();
808
809        // Insert record
810        rrset.insert(record.clone(), SerialNumber::from(1)).unwrap();
811        assert_eq!(rrset.len(), 1);
812
813        // Remove record
814        let result = rrset.remove(&record, SerialNumber::from(2));
815        assert!(result.is_ok());
816        assert!(result.unwrap());
817        assert_eq!(rrset.len(), 0);
818        assert!(rrset.is_empty());
819    }
820
821    #[test]
822    fn test_recordset_rrkey() {
823        let name = create_test_name();
824        let rrset = RecordSet::new(name.clone(), RecordType::A);
825        let rrkey = rrset.rrkey();
826
827        assert_eq!(rrkey.name(), &LowerName::from(name));
828        assert_eq!(rrkey.record_type, RecordType::A);
829    }
830
831    #[test]
832    fn test_recordset_iterators() {
833        let name = create_test_name();
834        let record = create_test_a_record();
835        let rrset = RecordSet::from_record(name, record);
836
837        // Test records iterator
838        assert_eq!(rrset.records().count(), 1);
839
840        // Test into_records
841        let records: Vec<_> = rrset.clone().into_records().collect();
842        assert_eq!(records.len(), 1);
843
844        // Test signed_records (no signatures)
845        assert_eq!(rrset.signed_records().count(), 1);
846
847        // Test into_signed_records
848        let signed_records: Vec<_> = rrset.into_signed_records().collect();
849        assert_eq!(signed_records.len(), 1);
850    }
851
852    #[test]
853    fn test_recordset_rrsigs() {
854        let name = create_test_name();
855        let mut rrset = RecordSet::new(name, RecordType::A);
856
857        assert!(rrset.rrsigs().is_empty());
858
859        // Clear RRSIGs (should be no-op when empty)
860        rrset.clear_rrsigs();
861        assert!(rrset.rrsigs().is_empty());
862    }
863
864    #[test]
865    fn test_recordset_as_hickory() {
866        let name = create_test_name();
867        let record = create_test_a_record();
868        let rrset = RecordSet::from_record(name.clone(), record);
869
870        let hickory_rrset = rrset.as_hickory();
871        assert_eq!(hickory_rrset.name(), &name);
872        assert_eq!(hickory_rrset.record_type(), RecordType::A);
873    }
874
875    #[test]
876    fn test_mismatch_error() {
877        let mismatch = Mismatch("test");
878        assert_eq!(
879            format!("{mismatch}"),
880            "Mismatched test between new record and record set"
881        );
882    }
883
884    #[test]
885    fn test_recordset_clone() {
886        let name = create_test_name();
887        let record = create_test_a_record();
888        let rrset1 = RecordSet::from_record(name, record);
889        let rrset2 = rrset1.clone();
890
891        assert_eq!(rrset1, rrset2);
892        assert_eq!(rrset1.name(), rrset2.name());
893        assert_eq!(rrset1.len(), rrset2.len());
894    }
895
896    #[test]
897    fn test_recordset_soa_serial_number_handling() {
898        use hickory_proto::rr::rdata::SOA;
899
900        let name = create_test_name();
901        let mut rrset = RecordSet::new(name.clone(), RecordType::SOA);
902
903        // Create first SOA with serial 100
904        let soa1 = SOA::new(
905            name.clone(),
906            Name::from_utf8("admin.example.com").unwrap(),
907            100, // serial
908            3600,
909            1800,
910            604800,
911            86400,
912        );
913        let record1 =
914            Record::from_rdata(name.clone(), TimeToLive::from(3600), soa1).into_record_rdata();
915
916        // Insert first SOA
917        let result = rrset.insert(record1, SerialNumber::from(1));
918        assert!(result.is_ok());
919        assert!(result.unwrap()); // Should succeed
920        assert_eq!(rrset.len(), 1);
921
922        // Try to insert SOA with older serial (50) - should be rejected
923        let soa2 = SOA::new(
924            name.clone(),
925            Name::from_utf8("admin.example.com").unwrap(),
926            50, // older serial
927            3600,
928            1800,
929            604800,
930            86400,
931        );
932        let record2 =
933            Record::from_rdata(name.clone(), TimeToLive::from(3600), soa2).into_record_rdata();
934
935        let result = rrset.insert(record2, SerialNumber::from(2));
936        assert!(result.is_ok());
937        assert!(!result.unwrap()); // Should be rejected due to old serial
938        assert_eq!(rrset.len(), 1); // Length should not change
939
940        // Try to insert SOA with newer serial (200) - should succeed
941        let soa3 = SOA::new(
942            name.clone(),
943            Name::from_utf8("admin.example.com").unwrap(),
944            200, // newer serial
945            3600,
946            1800,
947            604800,
948            86400,
949        );
950        let record3 = Record::from_rdata(name, TimeToLive::from(3600), soa3).into_record_rdata();
951
952        let result = rrset.insert(record3, SerialNumber::from(3));
953        assert!(result.is_ok());
954        assert!(result.unwrap()); // Should succeed
955        assert_eq!(rrset.len(), 1); // Still only one SOA (replaced)
956
957        // Verify the SOA has the newer serial
958        let soa_record = rrset.records().next().unwrap();
959        if let RData::SOA(soa) = soa_record.rdata() {
960            assert_eq!(soa.serial(), 200);
961        } else {
962            panic!("Expected SOA record");
963        }
964    }
965
966    #[test]
967    fn test_recordset_ns_last_record_protection() {
968        use hickory_proto::rr::rdata::NS;
969
970        let name = create_test_name();
971        let mut rrset = RecordSet::new(name.clone(), RecordType::NS);
972
973        // Add first NS record
974        let ns1 = NS(Name::from_utf8("ns1.example.com").unwrap());
975        let record1 = Record::from_rdata(name.clone(), TimeToLive::from(86400), ns1.clone())
976            .into_record_rdata();
977        rrset
978            .insert(record1.clone(), SerialNumber::from(1))
979            .unwrap();
980        assert_eq!(rrset.len(), 1);
981
982        // Add second NS record
983        let ns2 = NS(Name::from_utf8("ns2.example.com").unwrap());
984        let record2 =
985            Record::from_rdata(name.clone(), TimeToLive::from(86400), ns2).into_record_rdata();
986        rrset
987            .insert(record2.clone(), SerialNumber::from(2))
988            .unwrap();
989        assert_eq!(rrset.len(), 2);
990
991        // Remove first NS record - should succeed
992        let result = rrset.remove(&record1, SerialNumber::from(3));
993        assert!(result.is_ok());
994        assert!(result.unwrap()); // Should succeed
995        assert_eq!(rrset.len(), 1);
996
997        // Try to remove the last NS record - should be rejected
998        let result = rrset.remove(&record2, SerialNumber::from(4));
999        assert!(result.is_ok());
1000        assert!(!result.unwrap()); // Should be rejected
1001        assert_eq!(rrset.len(), 1); // Length should not change
1002    }
1003
1004    #[test]
1005    fn test_recordset_soa_deletion_protection() {
1006        use hickory_proto::rr::rdata::SOA;
1007
1008        let name = create_test_name();
1009        let mut rrset = RecordSet::new(name.clone(), RecordType::SOA);
1010
1011        // Add SOA record
1012        let soa = SOA::new(
1013            name.clone(),
1014            Name::from_utf8("admin.example.com").unwrap(),
1015            1,
1016            3600,
1017            1800,
1018            604800,
1019            86400,
1020        );
1021        let record = Record::from_rdata(name, TimeToLive::from(3600), soa).into_record_rdata();
1022        rrset.insert(record.clone(), SerialNumber::from(1)).unwrap();
1023        assert_eq!(rrset.len(), 1);
1024
1025        // Try to remove SOA record - should be rejected
1026        let result = rrset.remove(&record, SerialNumber::from(2));
1027        assert!(result.is_ok());
1028        assert!(!result.unwrap()); // Should be rejected
1029        assert_eq!(rrset.len(), 1); // Length should not change
1030    }
1031
1032    #[test]
1033    fn test_recordset_cname_replacement() {
1034        use hickory_proto::rr::rdata::CNAME;
1035
1036        let name = create_test_name();
1037        let mut rrset = RecordSet::new(name.clone(), RecordType::CNAME);
1038
1039        // Add first CNAME record
1040        let cname1 = CNAME(Name::from_utf8("target1.example.com").unwrap());
1041        let record1 =
1042            Record::from_rdata(name.clone(), TimeToLive::from(300), cname1).into_record_rdata();
1043        rrset.insert(record1, SerialNumber::from(1)).unwrap();
1044        assert_eq!(rrset.len(), 1);
1045
1046        // Add second CNAME record - should replace the first
1047        let cname2 = CNAME(Name::from_utf8("target2.example.com").unwrap());
1048        let record2 = Record::from_rdata(name, TimeToLive::from(300), cname2).into_record_rdata();
1049        rrset.insert(record2, SerialNumber::from(2)).unwrap();
1050        assert_eq!(rrset.len(), 1); // Still only one CNAME
1051
1052        // Verify the CNAME points to the new target
1053        let cname_record = rrset.records().next().unwrap();
1054        if let RData::CNAME(cname) = cname_record.rdata() {
1055            assert!(cname.0.to_utf8().starts_with("target2.example.com"));
1056        } else {
1057            panic!("Expected CNAME record");
1058        }
1059    }
1060
1061    #[test]
1062    fn test_recordset_aname_replacement() {
1063        use hickory_proto::rr::rdata::ANAME;
1064
1065        let name = create_test_name();
1066        let mut rrset = RecordSet::new(name.clone(), RecordType::ANAME);
1067
1068        // Add first ANAME record
1069        let aname1 = ANAME(Name::from_utf8("target1.example.com").unwrap());
1070        let record1 =
1071            Record::from_rdata(name.clone(), TimeToLive::from(300), aname1).into_record_rdata();
1072        rrset.insert(record1, SerialNumber::from(1)).unwrap();
1073        assert_eq!(rrset.len(), 1);
1074
1075        // Add second ANAME record - should replace the first
1076        let aname2 = ANAME(Name::from_utf8("target2.example.com").unwrap());
1077        let record2 = Record::from_rdata(name, TimeToLive::from(300), aname2).into_record_rdata();
1078        rrset.insert(record2, SerialNumber::from(2)).unwrap();
1079        assert_eq!(rrset.len(), 1); // Still only one ANAME
1080
1081        // Verify the ANAME points to the new target
1082        let aname_record = rrset.records().next().unwrap();
1083        if let RData::ANAME(aname) = aname_record.rdata() {
1084            assert!(aname.0.to_utf8().starts_with("target2.example.com"));
1085        } else {
1086            panic!("Expected ANAME record");
1087        }
1088    }
1089
1090    #[test]
1091    #[allow(clippy::blocks_in_conditions)]
1092    fn test_get_filter() {
1093        use hickory_proto::dnssec::{
1094            Algorithm,
1095            rdata::{DNSSECRData, RRSIG},
1096        };
1097
1098        let name = Name::root();
1099        let rsasha256 = RRSIG::new(
1100            RecordType::A,
1101            Algorithm::RSASHA256,
1102            0,
1103            0,
1104            0,
1105            0,
1106            0,
1107            Name::root(),
1108            vec![],
1109        );
1110        let ecp256 = RRSIG::new(
1111            RecordType::A,
1112            Algorithm::ECDSAP256SHA256,
1113            0,
1114            0,
1115            0,
1116            0,
1117            0,
1118            Name::root(),
1119            vec![],
1120        );
1121        let ecp384 = RRSIG::new(
1122            RecordType::A,
1123            Algorithm::ECDSAP384SHA384,
1124            0,
1125            0,
1126            0,
1127            0,
1128            0,
1129            Name::root(),
1130            vec![],
1131        );
1132        let ed25519 = RRSIG::new(
1133            RecordType::A,
1134            Algorithm::ED25519,
1135            0,
1136            0,
1137            0,
1138            0,
1139            0,
1140            Name::root(),
1141            vec![],
1142        );
1143
1144        let rrsig_rsa = Record::from_rdata(
1145            name.clone(),
1146            3600.into(),
1147            RData::DNSSEC(DNSSECRData::RRSIG(rsasha256)),
1148        )
1149        .set_dns_class(DNSClass::IN)
1150        .clone();
1151        let rrsig_ecp256 = Record::from_rdata(
1152            name.clone(),
1153            3600.into(),
1154            RData::DNSSEC(DNSSECRData::RRSIG(ecp256)),
1155        )
1156        .set_dns_class(DNSClass::IN)
1157        .clone();
1158        let rrsig_ecp384 = Record::from_rdata(
1159            name.clone(),
1160            3600.into(),
1161            RData::DNSSEC(DNSSECRData::RRSIG(ecp384)),
1162        )
1163        .set_dns_class(DNSClass::IN)
1164        .clone();
1165        let rrsig_ed25519 = Record::from_rdata(
1166            name.clone(),
1167            3600.into(),
1168            RData::DNSSEC(DNSSECRData::RRSIG(ed25519)),
1169        )
1170        .set_dns_class(DNSClass::IN)
1171        .clone();
1172
1173        let a = Record::from_rdata(
1174            name,
1175            3600.into(),
1176            RData::A(Ipv4Addr::new(93, 184, 216, 24).into()),
1177        )
1178        .set_dns_class(DNSClass::IN)
1179        .clone();
1180
1181        let mut rrset = RecordSet::from(a);
1182        rrset.insert_rrsig(rrsig_rsa).unwrap();
1183        rrset.insert_rrsig(rrsig_ecp256).unwrap();
1184        rrset.insert_rrsig(rrsig_ecp384).unwrap();
1185        rrset.insert_rrsig(rrsig_ed25519).unwrap();
1186
1187        assert!(rrset.signed_records().any(|r| {
1188            if let RData::DNSSEC(DNSSECRData::RRSIG(sig)) = r.rdata() {
1189                sig.algorithm() == Algorithm::ED25519
1190            } else {
1191                false
1192            }
1193        },));
1194
1195        assert!(rrset.signed_records().any(|r| {
1196            if let RData::DNSSEC(DNSSECRData::RRSIG(sig)) = r.rdata() {
1197                sig.algorithm() == Algorithm::ECDSAP384SHA384
1198            } else {
1199                false
1200            }
1201        }));
1202
1203        assert!(rrset.signed_records().any(|r| {
1204            if let RData::DNSSEC(DNSSECRData::RRSIG(sig)) = r.rdata() {
1205                sig.algorithm() == Algorithm::ED25519
1206            } else {
1207                false
1208            }
1209        }));
1210    }
1211}