1use super::{parse::TxtRecordParser, verify::DomainKey};
8use crate::Instant;
9use crate::{
10 DnssecStatus, Error, IpLookupStrategy, MX, MessageAuthenticator, RecordSet, ResolverCache, Txt,
11 dkim::{Atps, DomainKeyReport},
12 dmarc::Dmarc,
13 mta_sts::{MtaSts, TlsRpt},
14 spf::{Macro, Spf},
15};
16#[cfg(not(feature = "dns-doh"))]
17use hickory_resolver::{
18 TokioResolver,
19 config::{CLOUDFLARE, GOOGLE, QUAD9, ResolverConfig, ResolverOpts},
20 net::{DnsError, NetError, runtime::TokioRuntimeProvider},
21 proto::{
22 ProtoError,
23 rr::{Name, RData},
24 },
25 system_conf::read_system_conf,
26};
27use std::borrow::Cow;
28use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
29use std::sync::Arc;
30
31pub struct DnsEntry<T> {
32 pub entry: T,
33 pub expires: Instant,
34}
35
36#[cfg(not(feature = "dns-doh"))]
37impl MessageAuthenticator {
38 #[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
39 pub fn new_cloudflare_tls() -> Result<Self, NetError> {
40 Self::new(ResolverConfig::tls(&CLOUDFLARE), ResolverOpts::default())
41 }
42
43 pub fn new_cloudflare() -> Result<Self, NetError> {
44 Self::new(
45 ResolverConfig::udp_and_tcp(&CLOUDFLARE),
46 ResolverOpts::default(),
47 )
48 }
49
50 pub fn new_google() -> Result<Self, NetError> {
51 Self::new(
52 ResolverConfig::udp_and_tcp(&GOOGLE),
53 ResolverOpts::default(),
54 )
55 }
56
57 pub fn new_quad9() -> Result<Self, NetError> {
58 Self::new(ResolverConfig::udp_and_tcp(&QUAD9), ResolverOpts::default())
59 }
60
61 #[cfg(any(feature = "ring", feature = "aws-lc-rs"))]
62 pub fn new_quad9_tls() -> Result<Self, NetError> {
63 Self::new(ResolverConfig::tls(&QUAD9), ResolverOpts::default())
64 }
65
66 pub fn new_system_conf() -> Result<Self, NetError> {
67 let (config, options) = read_system_conf()?;
68 Self::new(config, options)
69 }
70
71 pub fn new(config: ResolverConfig, options: ResolverOpts) -> Result<Self, NetError> {
72 Ok(MessageAuthenticator(
73 TokioResolver::builder_with_config(config, TokioRuntimeProvider::default())
74 .with_options(options)
75 .build()?,
76 ))
77 }
78
79 pub fn resolver(&self) -> &TokioResolver {
80 &self.0
81 }
82}
83
84impl MessageAuthenticator {
85 pub async fn txt_raw_lookup(&self, key: impl ToFqdn) -> crate::Result<Vec<u8>> {
86 let key = key.to_fqdn();
87
88 #[cfg(not(feature = "dns-doh"))]
89 let records = {
90 let lookup = self
91 .0
92 .txt_lookup(Name::from_str_relaxed::<&str>(key.as_ref())?)
93 .await?;
94 let mut records: Vec<Vec<u8>> = Vec::new();
95 for record in lookup.answers() {
96 if let RData::TXT(txt) = &record.data {
97 let mut entry = Vec::new();
98 for item in &txt.txt_data {
99 entry.extend_from_slice(item);
100 }
101 records.push(entry);
102 }
103 }
104 records
105 };
106
107 #[cfg(feature = "dns-doh")]
108 let records = self.doh_txt(key.as_ref()).await?.entry;
109
110 Ok(records.into_iter().flatten().collect())
111 }
112
113 pub async fn txt_lookup<T: TxtRecordParser + Into<Txt> + UnwrapTxtRecord>(
114 &self,
115 key: impl ToFqdn,
116 cache: Option<&impl ResolverCache<Box<str>, Txt>>,
117 ) -> crate::Result<Arc<T>> {
118 let key = key.to_fqdn();
119 if let Some(value) = cache.as_ref().and_then(|c| c.get::<str>(key.as_ref())) {
120 return T::unwrap_txt(value);
121 }
122
123 #[cfg(any(test, feature = "test"))]
124 if true {
125 return mock_resolve(key.as_ref());
126 }
127
128 #[cfg(not(feature = "dns-doh"))]
129 let (records, expires) = {
130 let lookup = self
131 .0
132 .txt_lookup(Name::from_str_relaxed::<&str>(key.as_ref())?)
133 .await?;
134 let expires = lookup.valid_until();
135 let mut records: Vec<Vec<u8>> = Vec::new();
136 for record in lookup.answers() {
137 let RData::TXT(txt) = &record.data else {
138 continue;
139 };
140 match txt.txt_data.len() {
141 0 => {}
142 1 => records.push(txt.txt_data[0].to_vec()),
143 _ => {
144 let mut entry = Vec::with_capacity(255 * txt.txt_data.len());
145 for data in txt.txt_data.iter() {
146 entry.extend_from_slice(data);
147 }
148 records.push(entry);
149 }
150 }
151 }
152 (records, expires)
153 };
154
155 #[cfg(feature = "dns-doh")]
156 let (records, expires) = {
157 let raw = self.doh_txt(key.as_ref()).await?;
158 (raw.entry, raw.expires)
159 };
160
161 let mut result = Err(Error::Dns(crate::DnsError::InvalidRecordType));
162 for record in &records {
163 result = T::parse(record);
164 if result.is_ok() {
165 break;
166 }
167 }
168
169 let result: Txt = result.into();
170
171 if let Some(cache) = cache {
172 cache.insert(key.into_owned().into_boxed_str(), result.clone(), expires);
173 }
174
175 T::unwrap_txt(result)
176 }
177
178 pub async fn mx_lookup(
179 &self,
180 key: impl ToFqdn,
181 cache: Option<&impl ResolverCache<Box<str>, RecordSet<MX>>>,
182 ) -> crate::Result<RecordSet<MX>> {
183 let key = key.to_fqdn();
184 if let Some(value) = cache.as_ref().and_then(|c| c.get::<str>(key.as_ref())) {
185 return Ok(value);
186 }
187
188 #[cfg(any(test, feature = "test"))]
189 if true {
190 return mock_resolve(key.as_ref());
191 }
192
193 #[cfg(not(feature = "dns-doh"))]
194 let (mx_records, expires): (Vec<(u16, Box<str>)>, Instant) = {
195 let lookup = self
196 .0
197 .mx_lookup(Name::from_str_relaxed::<&str>(key.as_ref())?)
198 .await?;
199 let expires = lookup.valid_until();
200 let mx_records = lookup
201 .answers()
202 .iter()
203 .filter_map(|r| {
204 let RData::MX(mx) = &r.data else {
205 return None;
206 };
207 Some((
208 mx.preference,
209 mx.exchange.to_lowercase().to_ascii().into_boxed_str(),
210 ))
211 })
212 .collect();
213 (mx_records, expires)
214 };
215
216 #[cfg(feature = "dns-doh")]
217 let (mx_records, expires): (Vec<(u16, Box<str>)>, Instant) = {
218 let raw = self.doh_mx(key.as_ref()).await?;
219 (raw.entry, raw.expires)
220 };
221
222 let mut records: Vec<(u16, Vec<Box<str>>)> = Vec::with_capacity(mx_records.len());
223 for (preference, exchange) in mx_records {
224 if let Some(record) = records.iter_mut().find(|r| r.0 == preference) {
225 record.1.push(exchange);
226 } else {
227 records.push((preference, vec![exchange]));
228 }
229 }
230
231 records.sort_unstable_by_key(|a| a.0);
232 let records: Arc<[MX]> = records
233 .into_iter()
234 .map(|(preference, exchanges)| MX {
235 preference,
236 exchanges: exchanges.into_boxed_slice(),
237 })
238 .collect::<Arc<[MX]>>();
239 let records = RecordSet {
240 rrset: records,
241 dnssec_status: DnssecStatus::Indeterminate,
242 };
243
244 if let Some(cache) = cache {
245 cache.insert(key.into_owned().into_boxed_str(), records.clone(), expires);
246 }
247
248 Ok(records)
249 }
250
251 pub async fn ipv4_lookup(
252 &self,
253 key: impl ToFqdn,
254 cache: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv4Addr>>>,
255 ) -> crate::Result<RecordSet<Ipv4Addr>> {
256 let key = key.to_fqdn();
257 if let Some(value) = cache.as_ref().and_then(|c| c.get::<str>(key.as_ref())) {
258 return Ok(value);
259 }
260
261 let ipv4_lookup = self.ipv4_lookup_raw(key.as_ref()).await?;
262 let records = RecordSet {
263 rrset: ipv4_lookup.entry,
264 dnssec_status: DnssecStatus::Indeterminate,
265 };
266
267 if let Some(cache) = cache {
268 cache.insert(
269 key.into_owned().into_boxed_str(),
270 records.clone(),
271 ipv4_lookup.expires,
272 );
273 }
274
275 Ok(records)
276 }
277
278 pub async fn ipv4_lookup_raw(&self, key: &str) -> crate::Result<DnsEntry<Arc<[Ipv4Addr]>>> {
279 #[cfg(any(test, feature = "test"))]
280 if true {
281 return mock_resolve(key);
282 }
283
284 #[cfg(not(feature = "dns-doh"))]
285 {
286 let lookup = self
287 .0
288 .ipv4_lookup(Name::from_str_relaxed::<&str>(key)?)
289 .await?;
290 let expires = lookup.valid_until();
291 let entry: Arc<[Ipv4Addr]> = lookup
292 .answers()
293 .iter()
294 .filter_map(|r| {
295 if let RData::A(a) = &r.data {
296 Some(a.0)
297 } else {
298 None
299 }
300 })
301 .collect::<Vec<Ipv4Addr>>()
302 .into();
303 Ok(DnsEntry { entry, expires })
304 }
305
306 #[cfg(feature = "dns-doh")]
307 self.doh_ipv4(key).await
308 }
309
310 pub async fn ipv6_lookup(
311 &self,
312 key: impl ToFqdn,
313 cache: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv6Addr>>>,
314 ) -> crate::Result<RecordSet<Ipv6Addr>> {
315 let key = key.to_fqdn();
316 if let Some(value) = cache.as_ref().and_then(|c| c.get::<str>(key.as_ref())) {
317 return Ok(value);
318 }
319
320 let ipv6_lookup = self.ipv6_lookup_raw(key.as_ref()).await?;
321 let records = RecordSet {
322 rrset: ipv6_lookup.entry,
323 dnssec_status: DnssecStatus::Indeterminate,
324 };
325
326 if let Some(cache) = cache {
327 cache.insert(
328 key.into_owned().into_boxed_str(),
329 records.clone(),
330 ipv6_lookup.expires,
331 );
332 }
333
334 Ok(records)
335 }
336
337 pub async fn ipv6_lookup_raw(&self, key: &str) -> crate::Result<DnsEntry<Arc<[Ipv6Addr]>>> {
338 #[cfg(any(test, feature = "test"))]
339 if true {
340 return mock_resolve(key);
341 }
342
343 #[cfg(not(feature = "dns-doh"))]
344 {
345 let lookup = self
346 .0
347 .ipv6_lookup(Name::from_str_relaxed::<&str>(key)?)
348 .await?;
349 let expires = lookup.valid_until();
350 let entry: Arc<[Ipv6Addr]> = lookup
351 .answers()
352 .iter()
353 .filter_map(|r| {
354 if let RData::AAAA(aaaa) = &r.data {
355 Some(aaaa.0)
356 } else {
357 None
358 }
359 })
360 .collect::<Vec<Ipv6Addr>>()
361 .into();
362 Ok(DnsEntry { entry, expires })
363 }
364
365 #[cfg(feature = "dns-doh")]
366 self.doh_ipv6(key).await
367 }
368
369 pub async fn ip_lookup(
370 &self,
371 key: &str,
372 mut strategy: IpLookupStrategy,
373 max_results: usize,
374 cache_ipv4: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv4Addr>>>,
375 cache_ipv6: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv6Addr>>>,
376 ) -> crate::Result<Vec<IpAddr>> {
377 loop {
378 match strategy {
379 IpLookupStrategy::Ipv4Only | IpLookupStrategy::Ipv4thenIpv6 => {
380 match (self.ipv4_lookup(key, cache_ipv4).await, strategy) {
381 (Ok(result), _) => {
382 return Ok(result
383 .rrset
384 .iter()
385 .take(max_results)
386 .copied()
387 .map(IpAddr::from)
388 .collect());
389 }
390 (Err(err), IpLookupStrategy::Ipv4Only) => return Err(err),
391 _ => {
392 strategy = IpLookupStrategy::Ipv6Only;
393 }
394 }
395 }
396 IpLookupStrategy::Ipv6Only | IpLookupStrategy::Ipv6thenIpv4 => {
397 match (self.ipv6_lookup(key, cache_ipv6).await, strategy) {
398 (Ok(result), _) => {
399 return Ok(result
400 .rrset
401 .iter()
402 .take(max_results)
403 .copied()
404 .map(IpAddr::from)
405 .collect());
406 }
407 (Err(err), IpLookupStrategy::Ipv6Only) => return Err(err),
408 _ => {
409 strategy = IpLookupStrategy::Ipv4Only;
410 }
411 }
412 }
413 }
414 }
415 }
416
417 pub async fn ptr_lookup(
418 &self,
419 addr: IpAddr,
420 cache: Option<&impl ResolverCache<IpAddr, RecordSet<Box<str>>>>,
421 ) -> crate::Result<RecordSet<Box<str>>> {
422 if let Some(value) = cache.as_ref().and_then(|c| c.get(&addr)) {
423 return Ok(value);
424 }
425
426 #[cfg(any(test, feature = "test"))]
427 if true {
428 return mock_resolve(&addr.to_string());
429 }
430
431 #[cfg(not(feature = "dns-doh"))]
432 let (entry, expires): (Arc<[Box<str>]>, Instant) = {
433 let lookup = self.0.reverse_lookup(addr).await?;
434 let expires = lookup.valid_until();
435 let entry = lookup
436 .answers()
437 .iter()
438 .filter_map(|r| {
439 let RData::PTR(ptr) = &r.data else {
440 return None;
441 };
442 if !ptr.is_empty() {
443 Some(ptr.to_lowercase().to_ascii().into_boxed_str())
444 } else {
445 None
446 }
447 })
448 .collect::<Arc<[Box<str>]>>();
449 (entry, expires)
450 };
451
452 #[cfg(feature = "dns-doh")]
453 let (entry, expires): (Arc<[Box<str>]>, Instant) = {
454 let raw = self.doh_ptr(addr).await?;
455 (raw.entry, raw.expires)
456 };
457
458 let ptr = RecordSet {
459 rrset: entry,
460 dnssec_status: DnssecStatus::Indeterminate,
461 };
462
463 if let Some(cache) = cache {
464 cache.insert(addr, ptr.clone(), expires);
465 }
466
467 Ok(ptr)
468 }
469
470 #[cfg(any(test, feature = "test"))]
471 pub async fn exists(
472 &self,
473 key: impl ToFqdn,
474 cache_ipv4: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv4Addr>>>,
475 cache_ipv6: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv6Addr>>>,
476 ) -> crate::Result<bool> {
477 let key = key.to_fqdn();
478 match self.ipv4_lookup(key.as_ref(), cache_ipv4).await {
479 Ok(_) => Ok(true),
480 Err(Error::Dns(crate::DnsError::RecordNotFound(_))) => {
481 match self.ipv6_lookup(key.as_ref(), cache_ipv6).await {
482 Ok(_) => Ok(true),
483 Err(Error::Dns(crate::DnsError::RecordNotFound(_))) => Ok(false),
484 Err(err) => Err(err),
485 }
486 }
487 Err(err) => Err(err),
488 }
489 }
490
491 #[cfg(not(any(test, feature = "test")))]
492 pub async fn exists(
493 &self,
494 key: impl ToFqdn,
495 cache_ipv4: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv4Addr>>>,
496 cache_ipv6: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv6Addr>>>,
497 ) -> crate::Result<bool> {
498 let key = key.to_fqdn();
499
500 if cache_ipv4.is_some_and(|c| c.get::<str>(key.as_ref()).is_some())
501 || cache_ipv6.is_some_and(|c| c.get::<str>(key.as_ref()).is_some())
502 {
503 return Ok(true);
504 }
505
506 #[cfg(not(feature = "dns-doh"))]
507 {
508 match self
509 .0
510 .lookup_ip(Name::from_str_relaxed::<&str>(key.as_ref())?)
511 .await
512 {
513 Ok(result) => Ok(result.as_lookup().answers().iter().any(|r| {
514 matches!(
515 &r.data.record_type(),
516 hickory_resolver::proto::rr::RecordType::A
517 | hickory_resolver::proto::rr::RecordType::AAAA
518 )
519 })),
520 Err(err) if err.is_no_records_found() => Ok(false),
521 Err(err) => Err(err.into()),
522 }
523 }
524
525 #[cfg(feature = "dns-doh")]
526 self.doh_exists(key.as_ref()).await
527 }
528}
529
530#[cfg(not(feature = "dns-doh"))]
531impl From<ProtoError> for Error {
532 fn from(err: ProtoError) -> Self {
533 Error::Dns(crate::DnsError::Resolver(err.to_string()))
534 }
535}
536
537#[cfg(not(feature = "dns-doh"))]
538impl From<NetError> for Error {
539 fn from(err: NetError) -> Self {
540 match &err {
541 NetError::Dns(DnsError::NoRecordsFound(no_records)) => {
542 Error::Dns(crate::DnsError::RecordNotFound(no_records.response_code))
543 }
544 _ => Error::Dns(crate::DnsError::Resolver(err.to_string())),
545 }
546 }
547}
548
549impl From<DomainKey> for Txt {
550 fn from(v: DomainKey) -> Self {
551 Txt::DomainKey(v.into())
552 }
553}
554
555impl From<DomainKeyReport> for Txt {
556 fn from(v: DomainKeyReport) -> Self {
557 Txt::DomainKeyReport(v.into())
558 }
559}
560
561impl From<Atps> for Txt {
562 fn from(v: Atps) -> Self {
563 Txt::Atps(v.into())
564 }
565}
566
567impl From<Spf> for Txt {
568 fn from(v: Spf) -> Self {
569 Txt::Spf(v.into())
570 }
571}
572
573impl From<Macro> for Txt {
574 fn from(v: Macro) -> Self {
575 Txt::SpfMacro(v.into())
576 }
577}
578
579impl From<Dmarc> for Txt {
580 fn from(v: Dmarc) -> Self {
581 Txt::Dmarc(v.into())
582 }
583}
584
585impl From<MtaSts> for Txt {
586 fn from(v: MtaSts) -> Self {
587 Txt::MtaSts(v.into())
588 }
589}
590
591impl From<TlsRpt> for Txt {
592 fn from(v: TlsRpt) -> Self {
593 Txt::TlsRpt(v.into())
594 }
595}
596
597impl<T: Into<Txt>> From<crate::Result<T>> for Txt {
598 fn from(v: crate::Result<T>) -> Self {
599 match v {
600 Ok(v) => v.into(),
601 Err(err) => Txt::Error(err),
602 }
603 }
604}
605
606pub trait UnwrapTxtRecord: Sized {
607 fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>>;
608}
609
610impl UnwrapTxtRecord for DomainKey {
611 fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
612 match txt {
613 Txt::DomainKey(a) => Ok(a),
614 Txt::Error(err) => Err(err),
615 _ => Err(Error::Io("Invalid record type".to_string())),
616 }
617 }
618}
619
620impl UnwrapTxtRecord for DomainKeyReport {
621 fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
622 match txt {
623 Txt::DomainKeyReport(a) => Ok(a),
624 Txt::Error(err) => Err(err),
625 _ => Err(Error::Io("Invalid record type".to_string())),
626 }
627 }
628}
629
630impl UnwrapTxtRecord for Atps {
631 fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
632 match txt {
633 Txt::Atps(a) => Ok(a),
634 Txt::Error(err) => Err(err),
635 _ => Err(Error::Io("Invalid record type".to_string())),
636 }
637 }
638}
639
640impl UnwrapTxtRecord for Spf {
641 fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
642 match txt {
643 Txt::Spf(a) => Ok(a),
644 Txt::Error(err) => Err(err),
645 _ => Err(Error::Io("Invalid record type".to_string())),
646 }
647 }
648}
649
650impl UnwrapTxtRecord for Macro {
651 fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
652 match txt {
653 Txt::SpfMacro(a) => Ok(a),
654 Txt::Error(err) => Err(err),
655 _ => Err(Error::Io("Invalid record type".to_string())),
656 }
657 }
658}
659
660impl UnwrapTxtRecord for Dmarc {
661 fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
662 match txt {
663 Txt::Dmarc(a) => Ok(a),
664 Txt::Error(err) => Err(err),
665 _ => Err(Error::Io("Invalid record type".to_string())),
666 }
667 }
668}
669
670impl UnwrapTxtRecord for MtaSts {
671 fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
672 match txt {
673 Txt::MtaSts(a) => Ok(a),
674 Txt::Error(err) => Err(err),
675 _ => Err(Error::Io("Invalid record type".to_string())),
676 }
677 }
678}
679
680impl UnwrapTxtRecord for TlsRpt {
681 fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
682 match txt {
683 Txt::TlsRpt(a) => Ok(a),
684 Txt::Error(err) => Err(err),
685 _ => Err(Error::Io("Invalid record type".to_string())),
686 }
687 }
688}
689
690pub trait ToFqdn {
691 fn to_fqdn(&self) -> Cow<'_, str>;
692}
693
694impl<T: AsRef<str>> ToFqdn for T {
695 fn to_fqdn(&self) -> Cow<'_, str> {
696 let value = self.as_ref();
697 let bytes = value.as_bytes();
698 if matches!(bytes.last(), Some(b'.'))
699 && !bytes
700 .iter()
701 .any(|byte| byte.is_ascii_uppercase() || !byte.is_ascii())
702 {
703 Cow::Borrowed(value)
704 } else if value.is_ascii() {
705 let mut fqdn = String::with_capacity(value.len() + 1);
706 fqdn.push_str(value);
707 fqdn.make_ascii_lowercase();
708 if !matches!(bytes.last(), Some(b'.')) {
709 fqdn.push('.');
710 }
711 Cow::Owned(fqdn)
712 } else {
713 let mut fqdn = value.to_lowercase();
714 if !value.ends_with('.') {
715 fqdn.push('.');
716 }
717 Cow::Owned(fqdn)
718 }
719 }
720}
721
722pub trait ToReverseName {
723 fn to_reverse_name(&self) -> String;
724}
725
726impl ToReverseName for IpAddr {
727 fn to_reverse_name(&self) -> String {
728 match self {
729 IpAddr::V4(ip) => {
730 let mut segments = String::with_capacity(15);
731 let mut buf = [0u8; 3];
732 for octet in ip.octets().iter().rev() {
733 if !segments.is_empty() {
734 segments.push('.');
735 }
736 for &digit in decimal_u8(*octet, &mut buf) {
737 segments.push(char::from(digit));
738 }
739 }
740 segments
741 }
742 IpAddr::V6(ip) => {
743 let mut segments = String::with_capacity(63);
744 for segment in ip.segments().iter().rev() {
745 for shift in [0u32, 4, 8, 12] {
746 if !segments.is_empty() {
747 segments.push('.');
748 }
749 segments.push(char::from(hex_nibble((segment >> shift) as u8)));
750 }
751 }
752 segments
753 }
754 }
755 }
756}
757
758#[inline(always)]
759pub(crate) fn hex_nibble(value: u8) -> u8 {
760 b"0123456789abcdef"[(value & 0x0f) as usize]
761}
762
763#[inline(always)]
764pub(crate) fn decimal_u8(value: u8, buf: &mut [u8; 3]) -> &[u8] {
765 buf[0] = b'0' + value / 100;
766 buf[1] = b'0' + (value / 10) % 10;
767 buf[2] = b'0' + value % 10;
768 let start = if value >= 100 {
769 0
770 } else if value >= 10 {
771 1
772 } else {
773 2
774 };
775 &buf[start..]
776}
777
778#[cfg(any(test, feature = "test"))]
779pub fn mock_resolve<T>(domain: &str) -> crate::Result<T> {
780 Err(if domain.contains("_parse_error.") {
781 Error::ParseError
782 } else if domain.contains("_invalid_record.") {
783 Error::Dns(crate::DnsError::InvalidRecordType)
784 } else if domain.contains("_dns_error.") {
785 Error::Dns(crate::DnsError::Resolver("".to_string()))
786 } else {
787 Error::Dns(crate::DnsError::RecordNotFound(crate::DNS_RCODE_NXDOMAIN))
788 })
789}
790
791#[cfg(test)]
792mod test {
793 use std::net::IpAddr;
794
795 use crate::common::resolver::ToReverseName;
796
797 #[test]
798 fn reverse_lookup_addr() {
799 for (addr, expected) in [
800 ("1.2.3.4", "4.3.2.1"),
801 (
802 "2001:db8::cb01",
803 "1.0.b.c.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2",
804 ),
805 (
806 "2a01:4f9:c011:b43c::1",
807 "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.c.3.4.b.1.1.0.c.9.f.4.0.1.0.a.2",
808 ),
809 ] {
810 assert_eq!(addr.parse::<IpAddr>().unwrap().to_reverse_name(), expected);
811 }
812 }
813}