domain/rdata/
zonemd.rs

1//! ZONEMD record data.
2//!
3//! The ZONEMD Resource Record conveys the digest data in the zone itself.
4//!
5//! [RFC 8976]: https://tools.ietf.org/html/rfc8976
6
7// Currently a false positive on Zonemd. We cannot apply it there because
8// the allow attribute doesn't get copied to the code generated by serde.
9#![allow(clippy::needless_maybe_sized)]
10
11use crate::base::cmp::CanonicalOrd;
12use crate::base::iana::{Rtype, ZonemdAlgorithm, ZonemdScheme};
13use crate::base::rdata::{ComposeRecordData, RecordData};
14use crate::base::scan::{Scan, Scanner};
15use crate::base::serial::Serial;
16use crate::base::wire::{Composer, ParseError};
17use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt};
18use crate::utils::base16;
19use core::cmp::Ordering;
20use core::{fmt, hash};
21use octseq::octets::{Octets, OctetsFrom, OctetsInto};
22use octseq::parse::Parser;
23
24// section 2.2.4
25const DIGEST_MIN_LEN: usize = 12;
26
27/// The ZONEMD Resource Record conveys the digest data in the zone itself.
28#[derive(Clone)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct Zonemd<Octs: ?Sized> {
31    serial: Serial,
32    scheme: ZonemdScheme,
33    algo: ZonemdAlgorithm,
34    #[cfg_attr(
35        feature = "serde",
36        serde(
37            serialize_with = "octseq::serde::SerializeOctets::serialize_octets",
38            deserialize_with = "octseq::serde::DeserializeOctets::deserialize_octets",
39            bound(
40                serialize = "Octs: octseq::serde::SerializeOctets",
41                deserialize = "Octs: octseq::serde::DeserializeOctets<'de>",
42            )
43        )
44    )]
45    digest: Octs,
46}
47
48impl Zonemd<()> {
49    /// The rtype of this record data type.
50    pub(crate) const RTYPE: Rtype = Rtype::ZONEMD;
51}
52
53impl<Octs> Zonemd<Octs> {
54    /// Create a Zonemd record data from provided parameters.
55    pub fn new(
56        serial: Serial,
57        scheme: ZonemdScheme,
58        algo: ZonemdAlgorithm,
59        digest: Octs,
60    ) -> Self {
61        Self {
62            serial,
63            scheme,
64            algo,
65            digest,
66        }
67    }
68
69    /// Get the serial field.
70    pub fn serial(&self) -> Serial {
71        self.serial
72    }
73
74    /// Get the scheme field.
75    pub fn scheme(&self) -> ZonemdScheme {
76        self.scheme
77    }
78
79    /// Get the hash algorithm field.
80    pub fn algorithm(&self) -> ZonemdAlgorithm {
81        self.algo
82    }
83
84    /// Get the digest field.
85    pub fn digest(&self) -> &Octs {
86        &self.digest
87    }
88
89    /// Parse the record data from wire format.
90    pub fn parse<'a, Src: Octets<Range<'a> = Octs> + ?Sized>(
91        parser: &mut Parser<'a, Src>,
92    ) -> Result<Self, ParseError> {
93        let serial = Serial::parse(parser)?;
94        let scheme = parser.parse_u8()?.into();
95        let algo = parser.parse_u8()?.into();
96        let len = parser.remaining();
97        if len < DIGEST_MIN_LEN {
98            return Err(ParseError::ShortInput);
99        }
100        let digest = parser.parse_octets(len)?;
101        Ok(Self {
102            serial,
103            scheme,
104            algo,
105            digest,
106        })
107    }
108
109    /// Parse the record data from zonefile format.
110    pub fn scan<S: Scanner<Octets = Octs>>(
111        scanner: &mut S,
112    ) -> Result<Self, S::Error> {
113        let serial = Serial::scan(scanner)?;
114        let scheme = u8::scan(scanner)?.into();
115        let algo = u8::scan(scanner)?.into();
116        let digest = scanner.convert_entry(base16::SymbolConverter::new())?;
117
118        Ok(Self {
119            serial,
120            scheme,
121            algo,
122            digest,
123        })
124    }
125
126    pub(super) fn flatten<Target: OctetsFrom<Octs>>(
127        self,
128    ) -> Result<Zonemd<Target>, Target::Error> {
129        self.convert_octets()
130    }
131
132    pub(super) fn convert_octets<Target: OctetsFrom<Octs>>(
133        self,
134    ) -> Result<Zonemd<Target>, Target::Error> {
135        let Zonemd {
136            serial,
137            scheme,
138            algo,
139            digest,
140        } = self;
141
142        Ok(Zonemd {
143            serial,
144            scheme,
145            algo,
146            digest: digest.try_octets_into()?,
147        })
148    }
149}
150
151impl<Octs> RecordData for Zonemd<Octs> {
152    fn rtype(&self) -> Rtype {
153        Zonemd::RTYPE
154    }
155}
156
157impl<Octs: AsRef<[u8]>> ComposeRecordData for Zonemd<Octs> {
158    fn rdlen(&self, _compress: bool) -> Option<u16> {
159        Some(
160            // serial + scheme + algorithm + digest_len
161            u16::try_from(4 + 1 + 1 + self.digest.as_ref().len())
162                .expect("long ZONEMD rdata"),
163        )
164    }
165
166    fn compose_rdata<Target: Composer + ?Sized>(
167        &self,
168        target: &mut Target,
169    ) -> Result<(), Target::AppendError> {
170        target.append_slice(&self.serial.into_int().to_be_bytes())?;
171        target.append_slice(&[self.scheme.into()])?;
172        target.append_slice(&[self.algo.into()])?;
173        target.append_slice(self.digest.as_ref())
174    }
175
176    fn compose_canonical_rdata<Target: Composer + ?Sized>(
177        &self,
178        target: &mut Target,
179    ) -> Result<(), Target::AppendError> {
180        self.compose_rdata(target)
181    }
182}
183
184impl<Octs: AsRef<[u8]>> hash::Hash for Zonemd<Octs> {
185    fn hash<H: hash::Hasher>(&self, state: &mut H) {
186        self.serial.hash(state);
187        self.scheme.hash(state);
188        self.algo.hash(state);
189        self.digest.as_ref().hash(state);
190    }
191}
192
193impl<Octs, Other> PartialEq<Zonemd<Other>> for Zonemd<Octs>
194where
195    Octs: AsRef<[u8]> + ?Sized,
196    Other: AsRef<[u8]> + ?Sized,
197{
198    fn eq(&self, other: &Zonemd<Other>) -> bool {
199        self.serial.eq(&other.serial)
200            && self.scheme.eq(&other.scheme)
201            && self.algo.eq(&other.algo)
202            && self.digest.as_ref().eq(other.digest.as_ref())
203    }
204}
205
206impl<Octs: AsRef<[u8]> + ?Sized> Eq for Zonemd<Octs> {}
207
208// section 2.4
209impl<Octs: AsRef<[u8]>> fmt::Display for Zonemd<Octs> {
210    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
211        write!(
212            f,
213            "{} {} {} ( ",
214            self.serial,
215            u8::from(self.scheme),
216            u8::from(self.algo)
217        )?;
218        base16::display(&self.digest, f)?;
219        write!(f, " )")
220    }
221}
222
223impl<Octs: AsRef<[u8]>> fmt::Debug for Zonemd<Octs> {
224    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
225        f.write_str("Zonemd(")?;
226        fmt::Display::fmt(self, f)?;
227        f.write_str(")")
228    }
229}
230
231impl<Octs: AsRef<[u8]>> ZonefileFmt for Zonemd<Octs> {
232    fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result {
233        p.block(|p| {
234            p.write_token(self.serial)?;
235            p.write_show(self.scheme)?;
236            p.write_show(self.algo)?;
237            p.write_token(base16::encode_display(&self.digest))
238        })
239    }
240}
241
242impl<Octs, Other> PartialOrd<Zonemd<Other>> for Zonemd<Octs>
243where
244    Octs: AsRef<[u8]>,
245    Other: AsRef<[u8]>,
246{
247    fn partial_cmp(&self, other: &Zonemd<Other>) -> Option<Ordering> {
248        match self.serial.partial_cmp(&other.serial) {
249            Some(Ordering::Equal) => {}
250            other => return other,
251        }
252        match self.scheme.partial_cmp(&other.scheme) {
253            Some(Ordering::Equal) => {}
254            other => return other,
255        }
256        match self.algo.partial_cmp(&other.algo) {
257            Some(Ordering::Equal) => {}
258            other => return other,
259        }
260        self.digest.as_ref().partial_cmp(other.digest.as_ref())
261    }
262}
263
264impl<Octs, Other> CanonicalOrd<Zonemd<Other>> for Zonemd<Octs>
265where
266    Octs: AsRef<[u8]>,
267    Other: AsRef<[u8]>,
268{
269    fn canonical_cmp(&self, other: &Zonemd<Other>) -> Ordering {
270        match self.serial.into_int().cmp(&other.serial.into_int()) {
271            Ordering::Equal => {}
272            other => return other,
273        }
274        match self.scheme.cmp(&other.scheme) {
275            Ordering::Equal => {}
276            other => return other,
277        }
278        match self.algo.cmp(&other.algo) {
279            Ordering::Equal => {}
280            other => return other,
281        }
282        self.digest.as_ref().cmp(other.digest.as_ref())
283    }
284}
285
286impl<Octs: AsRef<[u8]>> Ord for Zonemd<Octs> {
287    fn cmp(&self, other: &Self) -> Ordering {
288        match self.serial.into_int().cmp(&other.serial.into_int()) {
289            Ordering::Equal => {}
290            other => return other,
291        }
292        match self.scheme.cmp(&other.scheme) {
293            Ordering::Equal => {}
294            other => return other,
295        }
296        match self.algo.cmp(&other.algo) {
297            Ordering::Equal => {}
298            other => return other,
299        }
300        self.digest.as_ref().cmp(other.digest.as_ref())
301    }
302}
303
304#[cfg(test)]
305#[cfg(all(feature = "std", feature = "bytes"))]
306mod test {
307    use super::*;
308    use crate::base::rdata::test::{
309        test_compose_parse, test_rdlen, test_scan,
310    };
311    use crate::utils::base16::decode;
312    use std::string::ToString;
313    use std::vec::Vec;
314
315    #[test]
316    #[allow(clippy::redundant_closure)] // lifetimes ...
317    fn zonemd_compose_parse_scan() {
318        let serial = 2023092203;
319        let scheme = 1.into();
320        let algo = 241.into();
321        let digest_str = "CDBE0DED9484490493580583BF868A3E95F89FC3515BF26ADBD230A6C23987F36BC6E504EFC83606F9445476D4E57FFB";
322        let digest: Vec<u8> = decode(digest_str).unwrap();
323        let rdata = Zonemd::new(serial.into(), scheme, algo, digest);
324        test_rdlen(&rdata);
325        test_compose_parse(&rdata, |parser| Zonemd::parse(parser));
326        test_scan(
327            &[
328                &serial.to_string(),
329                &u8::from(scheme).to_string(),
330                &u8::from(algo).to_string(),
331                digest_str,
332            ],
333            Zonemd::scan,
334            &rdata,
335        );
336    }
337
338    #[cfg(feature = "zonefile")]
339    #[test]
340    fn zonemd_parse_zonefile() {
341        use crate::base::iana::ZonemdAlgorithm;
342        use crate::base::Name;
343        use crate::rdata::ZoneRecordData;
344        use crate::zonefile::inplace::{Entry, Zonefile};
345
346        // section A.1
347        let content = r#"
348example.      86400  IN  SOA     ns1 admin 2018031900 (
349                                 1800 900 604800 86400 )
350              86400  IN  NS      ns1
351              86400  IN  NS      ns2
352              86400  IN  ZONEMD  2018031900 1 1 (
353                                 c68090d90a7aed71
354                                 6bc459f9340e3d7c
355                                 1370d4d24b7e2fc3
356                                 a1ddc0b9a87153b9
357                                 a9713b3c9ae5cc27
358                                 777f98b8e730044c )
359ns1           3600   IN  A       203.0.113.63
360ns2           3600   IN  AAAA    2001:db8::63
361"#;
362
363        let mut zone = Zonefile::load(&mut content.as_bytes()).unwrap();
364        zone.set_origin(Name::root());
365        while let Some(entry) = zone.next_entry().unwrap() {
366            match entry {
367                Entry::Record(record) => {
368                    if record.rtype() != Rtype::ZONEMD {
369                        continue;
370                    }
371                    match record.into_data() {
372                        ZoneRecordData::Zonemd(rd) => {
373                            assert_eq!(2018031900, rd.serial().into_int());
374                            assert_eq!(ZonemdScheme::SIMPLE, rd.scheme());
375                            assert_eq!(ZonemdAlgorithm::SHA384, rd.algorithm());
376                        }
377                        _ => panic!(),
378                    }
379                }
380                _ => panic!(),
381            }
382        }
383    }
384}