Skip to main content

mdns_proto/wire/record/
mod.rs

1//! Resource records — type-specific parsers + the generic `Ref`
2//! wrapper that pairs them with their owner name, type, class, and TTL.
3
4mod a;
5mod aaaa;
6mod cname;
7mod nsec;
8mod ptr;
9mod srv;
10mod txt;
11
12pub use a::A;
13pub use aaaa::AAAA;
14pub use cname::Cname;
15pub use nsec::Nsec;
16pub use ptr::Ptr;
17pub use srv::Srv;
18#[allow(unused_imports)]
19pub use txt::{Txt, TxtSegments};
20
21cfg_heap! {
22  use crate::backend::{RdataBuf, rdata_from_vec};
23}
24
25use super::{NameRef, ResourceClass, ResourceType};
26use crate::error::{BufferTooShortDetail, ParseError, RdlengthOverrunDetail};
27
28/// Parsed resource record (zero-copy view into a message). Stores the full
29/// message reference so type-specific rdata parsers can resolve compression
30/// pointers inside record data.
31#[derive(Debug, Copy, Clone, Eq, PartialEq)]
32pub struct Ref<'a> {
33  message: &'a [u8],
34  name: NameRef<'a>,
35  rtype: ResourceType,
36  rclass: ResourceClass,
37  cache_flush: bool,
38  ttl: u32,
39  rdata_start: usize,
40  rdata_len: usize,
41}
42
43impl<'a> Ref<'a> {
44  /// Parses a single resource record from `message` at `offset`.
45  /// Returns the record and the next offset to parse from.
46  pub fn try_parse(message: &'a [u8], offset: usize) -> Result<(Self, usize), ParseError> {
47    use super::resource_class::CACHE_FLUSH_BIT;
48    let (name, name_bytes) = NameRef::try_parse(message, offset)?;
49    let after_name = offset.saturating_add(name_bytes);
50
51    // type (2) + class (2) + ttl (4) + rdlength (2) = 10 bytes
52    let hdr = message
53      .get(after_name..after_name.saturating_add(10))
54      .ok_or_else(|| {
55        ParseError::BufferTooShort(BufferTooShortDetail::new(
56          10,
57          after_name,
58          message.len().saturating_sub(after_name),
59        ))
60      })?;
61
62    let rtype_arr: &[u8; 2] = hdr.first_chunk::<2>().ok_or_else(|| {
63      ParseError::BufferTooShort(BufferTooShortDetail::new(2, after_name, hdr.len()))
64    })?;
65    let rtype = ResourceType::from_u16(u16::from_be_bytes(*rtype_arr));
66
67    let rclass_raw_arr: &[u8; 2] = hdr
68      .get(2..4)
69      .and_then(|s| s.first_chunk::<2>())
70      .ok_or_else(|| {
71        ParseError::BufferTooShort(BufferTooShortDetail::new(
72          2,
73          after_name.saturating_add(2),
74          hdr.len(),
75        ))
76      })?;
77    let rclass_raw = u16::from_be_bytes(*rclass_raw_arr);
78    let cache_flush = (rclass_raw & CACHE_FLUSH_BIT) != 0;
79    let rclass = ResourceClass::from_u16(rclass_raw);
80
81    let ttl_arr: &[u8; 4] = hdr
82      .get(4..8)
83      .and_then(|s| s.first_chunk::<4>())
84      .ok_or_else(|| {
85        ParseError::BufferTooShort(BufferTooShortDetail::new(
86          4,
87          after_name.saturating_add(4),
88          hdr.len(),
89        ))
90      })?;
91    let ttl = u32::from_be_bytes(*ttl_arr);
92
93    let rdlen_arr: &[u8; 2] = hdr
94      .get(8..10)
95      .and_then(|s| s.first_chunk::<2>())
96      .ok_or_else(|| {
97        ParseError::BufferTooShort(BufferTooShortDetail::new(
98          2,
99          after_name.saturating_add(8),
100          hdr.len(),
101        ))
102      })?;
103    let rdlen = u16::from_be_bytes(*rdlen_arr);
104
105    let rdata_start = after_name.saturating_add(10);
106    let rdata_end = rdata_start.saturating_add(rdlen as usize);
107    if rdata_end > message.len() {
108      let remaining = message.len().saturating_sub(rdata_start);
109      return Err(ParseError::RdlengthOverrun(RdlengthOverrunDetail::new(
110        rdlen,
111        rdata_start,
112        remaining,
113      )));
114    }
115
116    Ok((
117      Self {
118        message,
119        name,
120        rtype,
121        rclass,
122        cache_flush,
123        ttl,
124        rdata_start,
125        rdata_len: rdlen as usize,
126      },
127      rdata_end,
128    ))
129  }
130
131  /// Returns the owner name of this record.
132  #[inline(always)]
133  pub const fn name(&self) -> &NameRef<'a> {
134    &self.name
135  }
136
137  /// Returns the resource record type.
138  #[inline(always)]
139  pub const fn rtype(&self) -> ResourceType {
140    self.rtype
141  }
142
143  /// Returns the resource record class.
144  #[inline(always)]
145  pub const fn rclass(&self) -> ResourceClass {
146    self.rclass
147  }
148
149  /// Returns `true` if the mDNS cache-flush bit was set on this record.
150  #[inline(always)]
151  pub const fn cache_flush(&self) -> bool {
152    self.cache_flush
153  }
154
155  /// Returns the time-to-live value in seconds.
156  #[inline(always)]
157  pub const fn ttl(&self) -> u32 {
158    self.ttl
159  }
160
161  /// Raw rdata slice borrowed from the message.
162  pub fn rdata(&self) -> &'a [u8] {
163    self
164      .message
165      .get(self.rdata_start..self.rdata_start.saturating_add(self.rdata_len))
166      .unwrap_or(&[])
167  }
168
169  /// Interpret this record's rdata, dispatching by [`Self::rtype`].
170  /// typed parsers now respect `rdata_len` so a malformed RDLENGTH cannot
171  /// let a name (PTR/SRV) consume bytes past its declared boundary, and
172  /// oversize A/AAAA rdata is rejected explicitly.
173  pub fn rdata_view(&self) -> Result<Rdata<'a>, ParseError> {
174    match self.rtype {
175      ResourceType::A => Ok(Rdata::A(A::try_from_rdata(self.rdata())?)),
176      ResourceType::AAAA => Ok(Rdata::AAAA(AAAA::try_from_rdata(self.rdata())?)),
177      ResourceType::Ptr => Ok(Rdata::Ptr(Ptr::try_from_message(
178        self.message,
179        self.rdata_start,
180        self.rdata_len,
181      )?)),
182      ResourceType::Cname => Ok(Rdata::Cname(Cname::try_from_message(
183        self.message,
184        self.rdata_start,
185        self.rdata_len,
186      )?)),
187      ResourceType::Srv => Ok(Rdata::Srv(Srv::try_from_message(
188        self.message,
189        self.rdata_start,
190        self.rdata_len,
191      )?)),
192      ResourceType::Txt => Ok(Rdata::Txt(Txt::from_rdata(self.rdata()))),
193      ResourceType::Nsec => Ok(Rdata::Nsec(Nsec::try_from_message(
194        self.message,
195        self.rdata_start,
196        self.rdata_len,
197      )?)),
198      _ => Ok(Rdata::Other(self.rdata())),
199    }
200  }
201
202  cfg_heap! {
203  /// Copies this record's rdata with internal DNS compression pointers
204  /// EXPANDED to self-contained wire form, PRESERVING name case. PTR/SRV/NSEC
205  /// rdata carries a domain name that responders — and this crate's own builder
206  /// — may compress with a back-pointer into the packet; a raw copy would
207  /// dangle once the source datagram is gone. Case is preserved so a query
208  /// caller can surface the name for display (RFC 6762 §16). A/AAAA/TXT/Other
209  /// carry no name we expand and are copied verbatim. Malformed typed rdata
210  /// (bad RDLENGTH, an over-length name, or a name with a pointer cycle /
211  /// forward pointer) yields `Err` so the caller can drop the record instead of
212  /// storing undecodable bytes.
213  ///
214  /// For record IDENTITY comparison use [`Self::canonical_rdata_folded`], which
215  /// additionally case-folds so two encodings differing only in name case (or
216  /// compression) compare equal.
217  pub(crate) fn canonical_rdata(&self) -> Result<RdataBuf, ParseError> {
218    self.canonical_rdata_inner(false)
219  }
220
221  /// Like [`Self::canonical_rdata`] but case-FOLDS names (ASCII lowercase) —
222  /// the canonical case-insensitive identity form (RFC 6762 §16). Used for the
223  /// passive cache, whose `(name, rtype, rclass, rdata)` dedup / TTL=0 goodbye
224  /// removal / cache-flush sibling matching compare rdata bytewise: without
225  /// folding, a peer announcing then withdrawing the same record with differing
226  /// case would leave a stale entry (and case variants could bloat the bounded
227  /// cache). The cache never surfaces rdata for display, so folding is safe
228  /// there.
229  pub(crate) fn canonical_rdata_folded(&self) -> Result<RdataBuf, ParseError> {
230    self.canonical_rdata_inner(true)
231  }
232
233  fn canonical_rdata_inner(&self, fold_case: bool) -> Result<RdataBuf, ParseError> {
234    match self.rdata_view()? {
235      Rdata::Ptr(p) => {
236        let mut out = std::vec::Vec::new();
237        p.target().write_wire(&mut out, fold_case)?;
238        Ok(rdata_from_vec(out))
239      }
240      Rdata::Cname(c) => {
241        let mut out = std::vec::Vec::new();
242        c.target().write_wire(&mut out, fold_case)?;
243        Ok(rdata_from_vec(out))
244      }
245      Rdata::Srv(s) => {
246        let mut out = std::vec::Vec::new();
247        out.extend_from_slice(&s.priority().to_be_bytes());
248        out.extend_from_slice(&s.weight().to_be_bytes());
249        out.extend_from_slice(&s.port().to_be_bytes());
250        s.target().write_wire(&mut out, fold_case)?;
251        Ok(rdata_from_vec(out))
252      }
253      Rdata::Nsec(n) => {
254        let mut out = std::vec::Vec::new();
255        n.next_name().write_wire(&mut out, fold_case)?;
256        out.extend_from_slice(n.type_bitmap_slice());
257        Ok(rdata_from_vec(out))
258      }
259      // Truly-unknown types are opaque (RFC 3597 §4 forbids name compression in
260      // them) so raw bytes are a stable identity — EXCEPT a well-known
261      // compressible name-bearing type we don't parse (NS/SOA/MX/DNAME), which
262      // MAY arrive compressed/case-varied and can't be canonicalized; it's not
263      // an mDNS/DNS-SD type, so drop it.
264      Rdata::Other(bytes) => {
265        if self.rtype.is_unhandled_compressible_name() {
266          return Err(ParseError::UnsupportedNameBearingType(self.rtype.to_u16()));
267        }
268        Ok(rdata_from_vec(bytes.to_vec()))
269      }
270      Rdata::Txt(t) => {
271        // TXT rdata is a sequence of length-prefixed strings (RFC 6763
272        // §6), NOT opaque bytes. Walk the segments to VALIDATE: a length octet
273        // that overruns the rdata makes `segments()` yield Err, which propagates
274        // so the caller DROPS the record. Without this a malformed TXT (e.g. a
275        // length byte of 5 followed by 2 bytes) passed this canonical-rdata
276        // validity gate and was admitted to the cache / query results. Rebuild
277        // the canonical bytes from the validated segments; an empty TXT
278        // normalizes to a single zero-length string (§6.1) so it matches both
279        // `respond::write_canonical_txt` and a peer's compliant empty TXT.
280        let mut out = std::vec::Vec::new();
281        let mut wrote_any = false;
282        for seg in t.segments() {
283          let seg = seg?;
284          // A parsed segment's length came from a single octet, so it is <= 255.
285          #[allow(clippy::cast_possible_truncation)]
286          out.push(seg.len() as u8);
287          out.extend_from_slice(seg);
288          wrote_any = true;
289        }
290        if !wrote_any {
291          out.push(0);
292        }
293        Ok(rdata_from_vec(out))
294      }
295      // A / AAAA carry no domain name and no internal structure — copy verbatim.
296      // (`_` also satisfies the `#[non_exhaustive]` enum.)
297      _ => Ok(rdata_from_vec(self.rdata().to_vec())),
298    }
299  }
300  }
301}
302
303/// Dispatched rdata view — interprets `rdata` per `rtype`.
304#[derive(
305  Debug, Copy, Clone, derive_more::IsVariant, derive_more::Unwrap, derive_more::TryUnwrap,
306)]
307#[unwrap(ref)]
308#[try_unwrap(ref)]
309#[non_exhaustive]
310// The `AAAA` variant keeps the canonical DNS record-type spelling.
311#[allow(clippy::upper_case_acronyms)]
312pub enum Rdata<'a> {
313  /// Parsed A record (IPv4 address).
314  A(A),
315  /// Parsed AAAA record (IPv6 address).
316  AAAA(AAAA),
317  /// Parsed PTR record (domain name pointer).
318  Ptr(Ptr<'a>),
319  /// Parsed CNAME record (canonical name alias).
320  Cname(Cname<'a>),
321  /// Parsed SRV record (server location).
322  Srv(Srv<'a>),
323  /// Parsed TXT record (key=value text segments).
324  Txt(Txt<'a>),
325  /// Parsed NSEC record (negative-answer hint).
326  Nsec(Nsec<'a>),
327  /// Catch-all for record types this crate does not type-specifically parse
328  /// (or for `Unknown` rtypes).
329  Other(&'a [u8]),
330}
331
332#[cfg(all(test, any(feature = "alloc", feature = "std")))]
333#[allow(
334  clippy::unwrap_used,
335  clippy::expect_used,
336  clippy::panic,
337  clippy::indexing_slicing,
338  clippy::arithmetic_side_effects
339)]
340mod tests;