Browse Source

Fix JSON output and introduce RecordType type

This commit fixes some bugs in the JSON output:

• The query type in the question structure is now the type name, like A, instead of a number like 1
• The record-specific data (like an A record's IP address) has been moved into a "data" field, for easier parsing
• The NAPTR regex field was incorrectly the service field
• The duration now has its structure specified, rather than relying on the internal representation
• Unknown classes now use their number, rather than a string of Rust Option

In order to turn types into names consistently, a `RecordType` type has been introduced to replace `TypeInt`. It's basically a `Record` without the assocatiated data.

Previously, even though a Query held a Labels field for a domain and a QClass field for a class, it still just held a u16 field for the record type. This was why it was showing up in the JSON output. It also showed up in the log output. Now, thanks to RecordType, its true name is known in both situations.

It also means we can get rid of some "weird stuff" like the qtype macro or the enum imports.

It also also means that, because RecordType is an enum, it will be a compile error if a new record type is added without the corresponding code in the wire module handling the new case.
Benjamin Sago 4 years ago
parent
commit
a1d78ff7e1
10 changed files with 355 additions and 245 deletions
  1. 1 1
      dns/src/lib.rs
  2. 136 40
      dns/src/record/mod.rs
  3. 22 9
      dns/src/record/others.rs
  4. 2 7
      dns/src/types.rs
  5. 48 103
      dns/src/wire.rs
  6. 2 1
      dns/tests/wire_building_tests.rs
  7. 5 5
      dns/tests/wire_parsing_tests.rs
  8. 21 19
      src/options.rs
  9. 116 56
      src/output.rs
  10. 2 4
      src/requests.rs

+ 1 - 1
dns/src/lib.rs

@@ -39,6 +39,6 @@ mod strings;
 pub use self::strings::Labels;
 
 mod wire;
-pub use self::wire::{Wire, WireError, MandatedLength, find_qtype_number};
+pub use self::wire::{Wire, WireError, MandatedLength};
 
 pub mod record;

+ 136 - 40
dns/src/record/mod.rs

@@ -1,5 +1,7 @@
 //! All the DNS record types, as well as how to parse each type.
 
+use crate::wire::*;
+
 
 mod a;
 pub use self::a::A;
@@ -63,70 +65,32 @@ pub use self::uri::URI;
 
 
 mod others;
-pub use self::others::{UnknownQtype, find_other_qtype_number};
+pub use self::others::UnknownQtype;
 
 
 /// A record that’s been parsed from a byte buffer.
 #[derive(PartialEq, Debug)]
+#[allow(missing_docs)]
 pub enum Record {
-
-    /// An **A** record.
     A(A),
-
-    /// An **AAAA** record.
     AAAA(AAAA),
-
-    /// A **CAA** record.
     CAA(CAA),
-
-    /// A **CNAME** record.
     CNAME(CNAME),
-
-    /// An **EUI48** record.
     EUI48(EUI48),
-
-    /// An **EUI64** record.
     EUI64(EUI64),
-
-    /// A **HINFO** record.
     HINFO(HINFO),
-
-    /// A **LOC** record.
     LOC(LOC),
-
-    /// A **MX** record.
     MX(MX),
-
-    /// A **NAPTR** record.
     NAPTR(NAPTR),
-
-    /// A **NS** record.
     NS(NS),
-
-    /// An **OPENPGPKEY** record.
     OPENPGPKEY(OPENPGPKEY),
-
     // OPT is not included here.
-
-    /// A **PTR** record.
     PTR(PTR),
-
-    /// A **SSHFP** record.
     SSHFP(SSHFP),
-
-    /// A **SOA** record.
     SOA(SOA),
-
-    /// A **SRV** record.
     SRV(SRV),
-
-    /// A **TLSA** record.
     TLSA(TLSA),
-
-    /// A **TXT** record.
     TXT(TXT),
-
-    /// A **URI** record.
     URI(URI),
 
     /// A record with a type that we don’t recognise.
@@ -139,3 +103,135 @@ pub enum Record {
         bytes: Vec<u8>,
     },
 }
+
+
+/// The type of a record that may or may not be one of the known ones. Has no
+/// data associated with it other than what type of record it is.
+#[derive(PartialEq, Debug, Copy, Clone)]
+#[allow(missing_docs)]
+pub enum RecordType {
+    A,
+    AAAA,
+    CAA,
+    CNAME,
+    EUI48,
+    EUI64,
+    HINFO,
+    LOC,
+    MX,
+    NAPTR,
+    NS,
+    OPENPGPKEY,
+    PTR,
+    SSHFP,
+    SOA,
+    SRV,
+    TLSA,
+    TXT,
+    URI,
+
+    /// A record type we don’t recognise.
+    Other(UnknownQtype),
+}
+
+impl From<u16> for RecordType {
+    fn from(type_number: u16) -> Self {
+        macro_rules! try_record {
+            ($record:tt) => {
+                if $record::RR_TYPE == type_number {
+                    return RecordType::$record;
+                }
+            }
+        }
+
+        try_record!(A);
+        try_record!(AAAA);
+        try_record!(CAA);
+        try_record!(CNAME);
+        try_record!(EUI48);
+        try_record!(EUI64);
+        try_record!(HINFO);
+        try_record!(LOC);
+        try_record!(MX);
+        try_record!(NAPTR);
+        try_record!(NS);
+        try_record!(OPENPGPKEY);
+        // OPT is handled separately
+        try_record!(PTR);
+        try_record!(SSHFP);
+        try_record!(SOA);
+        try_record!(SRV);
+        try_record!(TLSA);
+        try_record!(TXT);
+        try_record!(URI);
+
+        RecordType::Other(UnknownQtype::from(type_number))
+    }
+}
+
+
+impl RecordType {
+
+    /// Determines the record type with a given name, or `None` if none is known.
+    pub fn from_type_name(type_name: &str) -> Option<Self> {
+        macro_rules! try_record {
+            ($record:tt) => {
+                if $record::NAME == type_name {
+                    return Some(Self::$record);
+                }
+            }
+        }
+
+        try_record!(A);
+        try_record!(AAAA);
+        try_record!(CAA);
+        try_record!(CNAME);
+        try_record!(EUI48);
+        try_record!(EUI64);
+        try_record!(HINFO);
+        try_record!(LOC);
+        try_record!(MX);
+        try_record!(NAPTR);
+        try_record!(NS);
+        try_record!(OPENPGPKEY);
+        // OPT is elsewhere
+        try_record!(PTR);
+        try_record!(SSHFP);
+        try_record!(SOA);
+        try_record!(SRV);
+        try_record!(TLSA);
+        try_record!(TXT);
+        try_record!(URI);
+
+        UnknownQtype::from_type_name(type_name).map(Self::Other)
+    }
+
+    /// Returns the record type number associated with this record type.
+    pub fn type_number(self) -> u16 {
+        match self {
+            Self::A           => A::RR_TYPE,
+            Self::AAAA        => AAAA::RR_TYPE,
+            Self::CAA         => CAA::RR_TYPE,
+            Self::CNAME       => CNAME::RR_TYPE,
+            Self::EUI48       => EUI48::RR_TYPE,
+            Self::EUI64       => EUI64::RR_TYPE,
+            Self::HINFO       => HINFO::RR_TYPE,
+            Self::LOC         => LOC::RR_TYPE,
+            Self::MX          => MX::RR_TYPE,
+            Self::NAPTR       => NAPTR::RR_TYPE,
+            Self::NS          => NS::RR_TYPE,
+            Self::OPENPGPKEY  => OPENPGPKEY::RR_TYPE,
+            // Wherefore art thou, OPT
+            Self::PTR         => PTR::RR_TYPE,
+            Self::SSHFP       => SSHFP::RR_TYPE,
+            Self::SOA         => SOA::RR_TYPE,
+            Self::SRV         => SRV::RR_TYPE,
+            Self::TLSA        => TLSA::RR_TYPE,
+            Self::TXT         => TXT::RR_TYPE,
+            Self::URI         => URI::RR_TYPE,
+            Self::Other(o)    => o.type_number(),
+        }
+    }
+}
+
+// This code is really repetitive, I know, I know

+ 22 - 9
dns/src/record/others.rs

@@ -6,16 +6,34 @@ use std::fmt;
 pub enum UnknownQtype {
 
     /// An rtype number that dog is aware of, but does not know how to parse.
-    HeardOf(&'static str),
+    HeardOf(&'static str, u16),
 
     /// A completely unknown rtype number.
     UnheardOf(u16),
 }
 
+impl UnknownQtype {
+
+    /// Searches the list for an unknown type with the given name, returning a
+    /// `HeardOf` variant if one is found, and `None` otherwise.
+    pub fn from_type_name(type_name: &str) -> Option<Self> {
+        let (name, num) = TYPES.iter().find(|t| t.0 == type_name)?;
+        Some(Self::HeardOf(name, *num))
+    }
+
+    /// Returns the type number behind this unknown type.
+    pub fn type_number(self) -> u16 {
+        match self {
+            Self::HeardOf(_, num)  => num,
+            Self::UnheardOf(num)   => num,
+        }
+    }
+}
+
 impl From<u16> for UnknownQtype {
     fn from(qtype: u16) -> Self {
         match TYPES.iter().find(|t| t.1 == qtype) {
-            Some(tuple)  => Self::HeardOf(tuple.0),
+            Some(tuple)  => Self::HeardOf(tuple.0, qtype),
             None         => Self::UnheardOf(qtype),
         }
     }
@@ -24,17 +42,12 @@ impl From<u16> for UnknownQtype {
 impl fmt::Display for UnknownQtype {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
-            Self::HeardOf(name)   => write!(f, "{}", name),
-            Self::UnheardOf(num)  => write!(f, "{}", num),
+            Self::HeardOf(name, _)  => write!(f, "{}", name),
+            Self::UnheardOf(num)    => write!(f, "{}", num),
         }
     }
 }
 
-/// Looks up a record type for a name dog knows about, but still doesn’t know
-/// how to parse.
-pub fn find_other_qtype_number(name: &str) -> Option<u16> {
-    TYPES.iter().find(|t| t.0 == name).map(|t| t.1)
-}
 
 /// Mapping of record type names to their assigned numbers.
 static TYPES: &[(&str, u16)] = &[

+ 2 - 7
dns/src/types.rs

@@ -3,7 +3,7 @@
 //! with the request packet having zero answer fields, and the response packet
 //! having at least one record in its answer fields.
 
-use crate::record::{Record, OPT};
+use crate::record::{Record, RecordType, OPT};
 use crate::strings::Labels;
 
 
@@ -63,7 +63,7 @@ pub struct Query {
     pub qclass: QClass,
 
     /// The type number.
-    pub qtype: TypeInt,
+    pub qtype: RecordType,
 }
 
 
@@ -119,11 +119,6 @@ pub enum QClass {
 }
 
 
-/// The number representing a record type, such as `1` for an **A** record, or
-/// `15` for an **MX** record.
-pub type TypeInt = u16;
-
-
 /// The flags that accompany every DNS packet.
 #[derive(PartialEq, Debug, Copy, Clone)]
 pub struct Flags {

+ 48 - 103
dns/src/wire.rs

@@ -6,7 +6,7 @@ pub(crate) use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
 use std::io;
 use log::*;
 
-use crate::record::{Record, OPT};
+use crate::record::{Record, RecordType, OPT};
 use crate::strings::{Labels, ReadLabels, WriteLabels};
 use crate::types::*;
 
@@ -26,7 +26,7 @@ impl Request {
         bytes.write_u16::<BigEndian>(if self.additional.is_some() { 1 } else { 0 })?;  // additional RR count
 
         bytes.write_labels(&self.query.qname)?;
-        bytes.write_u16::<BigEndian>(self.query.qtype)?;
+        bytes.write_u16::<BigEndian>(self.query.qtype.type_number())?;
         bytes.write_u16::<BigEndian>(self.query.qclass.to_u16())?;
 
         if let Some(opt) = &self.additional {
@@ -110,8 +110,11 @@ impl Query {
     /// the given domain name.
     #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn from_bytes(qname: Labels, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
-        let qtype = c.read_u16::<BigEndian>()?;
-        trace!("Read qtype -> {:?}", qtype);
+        let qtype_number = c.read_u16::<BigEndian>()?;
+        trace!("Read qtype number -> {:?}", qtype_number );
+
+        let qtype = RecordType::from(qtype_number);
+        trace!("Found qtype -> {:?}", qtype );
 
         let qclass = QClass::from_u16(c.read_u16::<BigEndian>()?);
         trace!("Read qclass -> {:?}", qtype);
@@ -127,14 +130,17 @@ impl Answer {
     /// the given domain name.
     #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
     fn from_bytes(qname: Labels, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
-        let qtype = c.read_u16::<BigEndian>()?;
-        trace!("Read qtype -> {:?}", qtype);
+        let qtype_number = c.read_u16::<BigEndian>()?;
+        trace!("Read qtype number -> {:?}", qtype_number );
 
-        if qtype == OPT::RR_TYPE {
+        if qtype_number == OPT::RR_TYPE {
             let opt = OPT::read(c)?;
             Ok(Self::Pseudo { qname, opt })
         }
         else {
+            let qtype = RecordType::from(qtype_number);
+            trace!("Found qtype -> {:?}", qtype );
+
             let qclass = QClass::from_u16(c.read_u16::<BigEndian>()?);
             trace!("Read qclass -> {:?}", qtype);
 
@@ -147,7 +153,6 @@ impl Answer {
             let record = Record::from_bytes(qtype, record_length, c)?;
             Ok(Self::Standard { qclass, qname, record, ttl })
         }
-
     }
 }
 
@@ -157,54 +162,48 @@ impl Record {
     /// Reads at most `len` bytes from the given curser, and parses them into
     /// a record structure depending on the type number, which has already been read.
     #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)]
-    fn from_bytes(qtype: TypeInt, len: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
-        use crate::record::*;
-
+    fn from_bytes(record_type: RecordType, len: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
         if cfg!(feature = "with_mutagen") {
             warn!("Mutation is enabled!");
         }
 
-        macro_rules! try_record {
-            ($record:tt) => {
-                if $record::RR_TYPE == qtype {
-                    info!("Parsing {} record (type {}, len {})", $record::NAME, qtype, len);
-                    return Wire::read(len, c).map(Self::$record)
-                }
-            }
+        macro_rules! read_record {
+            ($record:tt) => { {
+                info!("Parsing {} record (type {}, len {})", crate::record::$record::NAME, record_type.type_number(), len);
+                Wire::read(len, c).map(Self::$record)
+            } }
         }
 
-        // Try all the records, one type at a time, returning early if the
-        // type number matches.
-        try_record!(A);
-        try_record!(AAAA);
-        try_record!(CAA);
-        try_record!(CNAME);
-        try_record!(EUI48);
-        try_record!(EUI64);
-        try_record!(HINFO);
-        try_record!(LOC);
-        try_record!(MX);
-        try_record!(NAPTR);
-        try_record!(NS);
-        try_record!(OPENPGPKEY);
-        // OPT is handled separately
-        try_record!(PTR);
-        try_record!(SSHFP);
-        try_record!(SOA);
-        try_record!(SRV);
-        try_record!(TLSA);
-        try_record!(TXT);
-        try_record!(URI);
-
-        // Otherwise, collect the bytes into a vector and return an unknown
-        // record type.
-        let mut bytes = Vec::new();
-        for _ in 0 .. len {
-            bytes.push(c.read_u8()?);
-        }
+        match record_type {
+            RecordType::A           => read_record!(A),
+            RecordType::AAAA        => read_record!(AAAA),
+            RecordType::CAA         => read_record!(CAA),
+            RecordType::CNAME       => read_record!(CNAME),
+            RecordType::EUI48       => read_record!(EUI48),
+            RecordType::EUI64       => read_record!(EUI64),
+            RecordType::HINFO       => read_record!(HINFO),
+            RecordType::LOC         => read_record!(LOC),
+            RecordType::MX          => read_record!(MX),
+            RecordType::NAPTR       => read_record!(NAPTR),
+            RecordType::NS          => read_record!(NS),
+            RecordType::OPENPGPKEY  => read_record!(OPENPGPKEY),
+            RecordType::PTR         => read_record!(PTR),
+            RecordType::SSHFP       => read_record!(SSHFP),
+            RecordType::SOA         => read_record!(SOA),
+            RecordType::SRV         => read_record!(SRV),
+            RecordType::TLSA        => read_record!(TLSA),
+            RecordType::TXT         => read_record!(TXT),
+            RecordType::URI         => read_record!(URI),
+
+            RecordType::Other(type_number) => {
+                let mut bytes = Vec::new();
+                for _ in 0 .. len {
+                    bytes.push(c.read_u8()?);
+                }
 
-        let type_number = UnknownQtype::from(qtype);
-        Ok(Self::Other { type_number, bytes })
+                Ok(Self::Other { type_number, bytes })
+            }
+        }
     }
 }
 
@@ -230,43 +229,6 @@ impl QClass {
 }
 
 
-/// Determines the record type number to signify a record with the given name.
-pub fn find_qtype_number(record_type: &str) -> Option<TypeInt> {
-    use crate::record::*;
-
-    macro_rules! try_record {
-        ($record:tt) => {
-            if $record::NAME == record_type {
-                return Some($record::RR_TYPE);
-            }
-        }
-    }
-
-    try_record!(A);
-    try_record!(AAAA);
-    try_record!(CAA);
-    try_record!(CNAME);
-    try_record!(EUI48);
-    try_record!(EUI64);
-    try_record!(HINFO);
-    try_record!(LOC);
-    try_record!(MX);
-    try_record!(NAPTR);
-    try_record!(NS);
-    try_record!(OPENPGPKEY);
-    // OPT is elsewhere
-    try_record!(PTR);
-    try_record!(SSHFP);
-    try_record!(SOA);
-    try_record!(SRV);
-    try_record!(TLSA);
-    try_record!(TXT);
-    try_record!(URI);
-
-    None
-}
-
-
 impl Flags {
 
     /// The set of flags that represents a query packet.
@@ -372,23 +334,6 @@ pub trait Wire: Sized {
 }
 
 
-/// Helper macro to get the qtype number of a record type at compile-time.
-///
-/// # Examples
-///
-/// ```
-/// use dns::{qtype, record::MX};
-///
-/// assert_eq!(15, qtype!(MX));
-/// ```
-#[macro_export]
-macro_rules! qtype {
-    ($type:ty) => {
-        <$type as $crate::Wire>::RR_TYPE
-    }
-}
-
-
 /// Something that can go wrong deciphering a record.
 #[derive(PartialEq, Debug)]
 pub enum WireError {

+ 2 - 1
dns/tests/wire_building_tests.rs

@@ -1,4 +1,5 @@
 use dns::{Request, Flags, Query, Labels, QClass};
+use dns::record::RecordType;
 
 use pretty_assertions::assert_eq;
 
@@ -11,7 +12,7 @@ fn build_request() {
         query: Query {
             qname: Labels::encode("rfcs.io").unwrap(),
             qclass: QClass::Other(0x42),
-            qtype: 0x1234,
+            qtype: RecordType::from(0x1234),
         },
         additional: Some(Request::additional_record()),
     };

+ 5 - 5
dns/tests/wire_parsing_tests.rs

@@ -1,7 +1,7 @@
 use std::net::Ipv4Addr;
 
-use dns::{Response, Query, Answer, Labels, Flags, Opcode, QClass, qtype};
-use dns::record::{Record, A, CNAME, OPT, SOA, UnknownQtype};
+use dns::{Response, Query, Answer, Labels, Flags, Opcode, QClass};
+use dns::record::{Record, A, CNAME, OPT, SOA, UnknownQtype, RecordType};
 
 use pretty_assertions::assert_eq;
 
@@ -60,7 +60,7 @@ fn parse_response_standard() {
             Query {
                 qname: Labels::encode("dns.lookup.dog").unwrap(),
                 qclass: QClass::IN,
-                qtype: qtype!(A),
+                qtype: RecordType::A,
             },
         ],
         answers: vec![
@@ -133,7 +133,7 @@ fn parse_response_with_mixed_string() {
             Query {
                 qname: Labels::encode("cname-example.lookup.dog").unwrap(),
                 qclass: QClass::IN,
-                qtype: qtype!(CNAME),
+                qtype: RecordType::CNAME,
             },
         ],
         answers: vec![
@@ -215,7 +215,7 @@ fn parse_response_with_multiple_additionals() {
             Query {
                 qname: Labels::encode("bsago.me").unwrap(),
                 qclass: QClass::IN,
-                qtype: qtype!(A),
+                qtype: RecordType::A,
             },
         ],
         answers: vec![

+ 21 - 19
src/options.rs

@@ -5,8 +5,8 @@ use std::fmt;
 
 use log::*;
 
-use dns::{QClass, Labels, find_qtype_number, qtype};
-use dns::record::{A, find_other_qtype_number};
+use dns::{QClass, Labels};
+use dns::record::RecordType;
 
 use crate::connect::TransportType;
 use crate::output::{OutputFormat, UseColours, TextFormat};
@@ -183,13 +183,16 @@ impl Inputs {
             return Err(OptionsError::QueryTypeOPT);
         }
 
-        let type_number = find_qtype_number(input)
-            .or_else(|| find_other_qtype_number(input))
-            .or_else(|| input.parse().ok());
-
-        match type_number {
-            Some(qtype)  => Ok(self.types.push(qtype)),
-            None         => Err(OptionsError::InvalidQueryType(input.into())),
+        if let Some(rt) = RecordType::from_type_name(input) {
+            self.types.push(rt);
+            Ok(())
+        }
+        else if let Ok(type_number) = input.parse::<u16>() {
+            self.types.push(RecordType::from(type_number));
+            Ok(())
+        }
+        else {
+            Err(OptionsError::InvalidQueryType(input.into()))
         }
     }
 
@@ -251,7 +254,7 @@ impl Inputs {
 
     fn load_fallbacks(&mut self) {
         if self.types.is_empty() {
-            self.types.push(qtype!(A));
+            self.types.push(RecordType::A);
         }
 
         if self.classes.is_empty() {
@@ -484,13 +487,12 @@ impl fmt::Display for OptionsError {
 mod test {
     use super::*;
     use pretty_assertions::assert_eq;
-    use dns::record::*;
 
     impl Inputs {
         fn fallbacks() -> Self {
             Inputs {
                 domains:         vec![ /* No domains by default */ ],
-                types:           vec![ qtype!(A) ],
+                types:           vec![ RecordType::A ],
                 classes:         vec![ QClass::IN ],
                 resolvers:       vec![ Resolver::system_default() ],
                 transport_types: vec![ TransportType::Automatic ],
@@ -577,7 +579,7 @@ mod test {
         let options = Options::getopts(&[ "lookup.dog", "SOA" ]).unwrap();
         assert_eq!(options.requests.inputs, Inputs {
             domains:    vec![ Labels::encode("lookup.dog").unwrap() ],
-            types:      vec![ qtype!(SOA) ],
+            types:      vec![ RecordType::SOA ],
             .. Inputs::fallbacks()
         });
     }
@@ -608,7 +610,7 @@ mod test {
         assert_eq!(options.requests.inputs, Inputs {
             domains:    vec![ Labels::encode("lookup.dog").unwrap() ],
             classes:    vec![ QClass::CH ],
-            types:      vec![ qtype!(NS) ],
+            types:      vec![ RecordType::NS ],
             resolvers:  vec![ Resolver::specified("1.1.1.1".into()) ],
             .. Inputs::fallbacks()
         });
@@ -620,7 +622,7 @@ mod test {
         assert_eq!(options.requests.inputs, Inputs {
             domains:    vec![ Labels::encode("lookup.dog").unwrap() ],
             classes:    vec![ QClass::CH ],
-            types:      vec![ qtype!(SOA) ],
+            types:      vec![ RecordType::SOA ],
             resolvers:  vec![ Resolver::specified("1.1.1.1".into()) ],
             .. Inputs::fallbacks()
         });
@@ -631,7 +633,7 @@ mod test {
         let options = Options::getopts(&[ "-q", "lookup.dog", "--type", "SRV", "--type", "AAAA" ]).unwrap();
         assert_eq!(options.requests.inputs, Inputs {
             domains:    vec![ Labels::encode("lookup.dog").unwrap() ],
-            types:      vec![ qtype!(SRV), qtype!(AAAA) ],
+            types:      vec![ RecordType::SRV, RecordType::AAAA ],
             .. Inputs::fallbacks()
         });
     }
@@ -652,7 +654,7 @@ mod test {
         assert_eq!(options.requests.inputs, Inputs {
             domains:    vec![ Labels::encode("lookup.dog").unwrap() ],
             classes:    vec![ QClass::CH ],
-            types:      vec![ qtype!(SOA) ],
+            types:      vec![ RecordType::SOA ],
             resolvers:  vec![ Resolver::specified("1.1.1.1".into()) ],
             .. Inputs::fallbacks()
         });
@@ -664,7 +666,7 @@ mod test {
         assert_eq!(options.requests.inputs, Inputs {
             domains:    vec![ Labels::encode("lookup.dog").unwrap() ],
             classes:    vec![ QClass::HS, QClass::CH, QClass::IN ],
-            types:      vec![ qtype!(SOA), qtype!(MX) ],
+            types:      vec![ RecordType::SOA, RecordType::MX ],
             .. Inputs::fallbacks()
         });
     }
@@ -686,7 +688,7 @@ mod test {
         assert_eq!(options.requests.inputs, Inputs {
             domains:    vec![ Labels::encode("11").unwrap() ],
             classes:    vec![ QClass::Other(22) ],
-            types:      vec![ 33 ],
+            types:      vec![ RecordType::from(33) ],
             .. Inputs::fallbacks()
         });
     }

+ 116 - 56
src/output.rs

@@ -2,8 +2,8 @@
 
 use std::time::Duration;
 
-use dns::{Response, Query, Answer, ErrorCode, WireError, MandatedLength};
-use dns::record::{Record, OPT, UnknownQtype};
+use dns::{Response, Query, Answer, QClass, ErrorCode, WireError, MandatedLength};
+use dns::record::{Record, RecordType, UnknownQtype, OPT};
 use dns_transport::Error as TransportError;
 use serde_json::{json, Value as JsonValue};
 
@@ -103,21 +103,31 @@ impl OutputFormat {
 
                 for response in responses {
                     let json = json!({
-                        "queries": json_queries(&response.queries),
-                        "answers": json_answers(&response.answers),
-                        "authorities": json_answers(&response.authorities),
-                        "additionals": json_answers(&response.additionals),
+                        "queries": json_queries(response.queries),
+                        "answers": json_answers(response.answers),
+                        "authorities": json_answers(response.authorities),
+                        "additionals": json_answers(response.additionals),
                     });
 
                     rs.push(json);
                 }
 
                 if let Some(duration) = duration {
-                    let object = json!({ "responses": rs, "duration": duration });
+                    let object = json!({
+                        "responses": rs,
+                        "duration": {
+                            "secs": duration.as_secs(),
+                            "millis": duration.subsec_millis(),
+                        },
+                    });
+
                     println!("{}", object);
                 }
                 else {
-                    let object = json!({ "responses": rs });
+                    let object = json!({
+                        "responses": rs,
+                    });
+
                     println!("{}", object);
                 }
             }
@@ -327,64 +337,136 @@ fn format_duration_hms(seconds: u32) -> String {
 }
 
 /// Serialises multiple DNS queries as a JSON value.
-fn json_queries(queries: &[Query]) -> JsonValue {
+fn json_queries(queries: Vec<Query>) -> JsonValue {
     let queries = queries.iter().map(|q| {
         json!({
             "name": q.qname.to_string(),
-            "class": format!("{:?}", q.qclass),
-            "type": q.qtype,
+            "class": json_class(q.qclass),
+            "type": json_record_type_name(q.qtype),
         })
     }).collect::<Vec<_>>();
 
-    json!(queries)
+    queries.into()
 }
 
 /// Serialises multiple received DNS answers as a JSON value.
-fn json_answers(answers: &[Answer]) -> JsonValue {
-    let answers = answers.iter().map(|a| {
+fn json_answers(answers: Vec<Answer>) -> JsonValue {
+    let answers = answers.into_iter().map(|a| {
         match a {
             Answer::Standard { qname, qclass, ttl, record } => {
-                let mut object = json_record(record);
-                let omut = object.as_object_mut().unwrap();
-                omut.insert("name".into(), qname.to_string().into());
-                omut.insert("class".into(), format!("{:?}", qclass).into());
-                omut.insert("ttl".into(), (*ttl).into());
-                json!(object)
+                json!({
+                    "name": qname.to_string(),
+                    "class": json_class(qclass),
+                    "ttl": ttl,
+                    "type": json_record_name(&record),
+                    "data": json_record_data(record),
+                })
             }
             Answer::Pseudo { qname, opt } => {
-                let object = json!({
+                json!({
                     "name": qname.to_string(),
                     "type": "OPT",
-                    "version": opt.edns0_version,
-                    "data": opt.data,
-                });
-
-                object
+                    "data": {
+                        "version": opt.edns0_version,
+                        "data": opt.data,
+                    },
+                })
             }
         }
     }).collect::<Vec<_>>();
 
-    json!(answers)
+    answers.into()
+}
+
+
+fn json_class(class: QClass) -> JsonValue {
+    match class {
+        QClass::IN        => "IN".into(),
+        QClass::CH        => "CH".into(),
+        QClass::HS        => "HS".into(),
+        QClass::Other(n)  => n.into(),
+    }
+}
+
+
+/// Serialises a DNS record type name.
+fn json_record_type_name(record: RecordType) -> JsonValue {
+    match record {
+        RecordType::A           => "A".into(),
+        RecordType::AAAA        => "AAAA".into(),
+        RecordType::CAA         => "CAA".into(),
+        RecordType::CNAME       => "CNAME".into(),
+        RecordType::EUI48       => "EUI48".into(),
+        RecordType::EUI64       => "EUI64".into(),
+        RecordType::HINFO       => "HINFO".into(),
+        RecordType::LOC         => "LOC".into(),
+        RecordType::MX          => "MX".into(),
+        RecordType::NAPTR       => "NAPTR".into(),
+        RecordType::NS          => "NS".into(),
+        RecordType::OPENPGPKEY  => "OPENPGPKEY".into(),
+        RecordType::PTR         => "PTR".into(),
+        RecordType::SOA         => "SOA".into(),
+        RecordType::SRV         => "SRV".into(),
+        RecordType::SSHFP       => "SSHFP".into(),
+        RecordType::TLSA        => "TLSA".into(),
+        RecordType::TXT         => "TXT".into(),
+        RecordType::URI         => "URI".into(),
+        RecordType::Other(unknown) => {
+            match unknown {
+                UnknownQtype::HeardOf(name, _)  => (*name).into(),
+                UnknownQtype::UnheardOf(num)    => (num).into(),
+            }
+        }
+    }
+}
+
+/// Serialises a DNS record type name.
+fn json_record_name(record: &Record) -> JsonValue {
+    match record {
+        Record::A(_)           => "A".into(),
+        Record::AAAA(_)        => "AAAA".into(),
+        Record::CAA(_)         => "CAA".into(),
+        Record::CNAME(_)       => "CNAME".into(),
+        Record::EUI48(_)       => "EUI48".into(),
+        Record::EUI64(_)       => "EUI64".into(),
+        Record::HINFO(_)       => "HINFO".into(),
+        Record::LOC(_)         => "LOC".into(),
+        Record::MX(_)          => "MX".into(),
+        Record::NAPTR(_)       => "NAPTR".into(),
+        Record::NS(_)          => "NS".into(),
+        Record::OPENPGPKEY(_)  => "OPENPGPKEY".into(),
+        Record::PTR(_)         => "PTR".into(),
+        Record::SOA(_)         => "SOA".into(),
+        Record::SRV(_)         => "SRV".into(),
+        Record::SSHFP(_)       => "SSHFP".into(),
+        Record::TLSA(_)        => "TLSA".into(),
+        Record::TXT(_)         => "TXT".into(),
+        Record::URI(_)         => "URI".into(),
+        Record::Other { type_number, .. } => {
+            match type_number {
+                UnknownQtype::HeardOf(name, _)  => (*name).into(),
+                UnknownQtype::UnheardOf(num)    => (*num).into(),
+            }
+        }
+    }
 }
 
+
 /// Serialises a received DNS record as a JSON value.
-fn json_record(record: &Record) -> JsonValue {
+fn json_record_data(record: Record) -> JsonValue {
     match record {
         Record::A(a) => {
             json!({
-                "type": "A",
                 "address": a.address.to_string(),
             })
         }
         Record::AAAA(aaaa) => {
             json!({
-                "type": "AAAA",
                 "address": aaaa.address.to_string(),
             })
         }
         Record::CAA(caa) => {
             json!({
-                "type": "CAA",
                 "critical": caa.critical,
                 "tag": caa.tag,
                 "value": caa.value,
@@ -392,32 +474,27 @@ fn json_record(record: &Record) -> JsonValue {
         }
         Record::CNAME(cname) => {
             json!({
-                "type": "CNAME",
                 "domain": cname.domain.to_string(),
             })
         }
         Record::EUI48(eui48) => {
             json!({
-                "type": "EUI48",
                 "identifier": eui48.formatted_address(),
             })
         }
         Record::EUI64(eui64) => {
             json!({
-                "type": "EUI64",
                 "identifier": eui64.formatted_address(),
             })
         }
         Record::HINFO(hinfo) => {
             json!({
-                "type": "HINFO",
                 "cpu": hinfo.cpu,
                 "os": hinfo.os,
             })
         }
         Record::LOC(loc) => {
             json!({
-                "type": "LOC",
                 "size": loc.size.to_string(),
                 "precision": {
                     "horizontal": loc.horizontal_precision,
@@ -432,42 +509,36 @@ fn json_record(record: &Record) -> JsonValue {
         }
         Record::MX(mx) => {
             json!({
-                "type": "MX",
                 "preference": mx.preference,
                 "exchange": mx.exchange.to_string(),
             })
         }
         Record::NAPTR(naptr) => {
             json!({
-                "type": "NAPTR",
                 "order": naptr.order,
                 "flags": naptr.flags,
                 "service": naptr.service,
-                "regex": naptr.service,
+                "regex": naptr.regex,
                 "replacement": naptr.replacement.to_string(),
             })
         }
         Record::NS(ns) => {
             json!({
-                "type": "NS",
                 "nameserver": ns.nameserver.to_string(),
             })
         }
         Record::OPENPGPKEY(opgp) => {
             json!({
-                "type": "OPENPGPKEY",
                 "key": opgp.base64_key(),
             })
         }
         Record::PTR(ptr) => {
             json!({
-                "type": "PTR",
                 "cname": ptr.cname.to_string(),
             })
         }
         Record::SSHFP(sshfp) => {
             json!({
-                "type": "SSHFP",
                 "algorithm": sshfp.algorithm,
                 "fingerprint_type": sshfp.fingerprint_type,
                 "fingerprint": sshfp.hex_fingerprint(),
@@ -475,13 +546,11 @@ fn json_record(record: &Record) -> JsonValue {
         }
         Record::SOA(soa) => {
             json!({
-                "type": "SOA",
                 "mname": soa.mname.to_string(),
             })
         }
         Record::SRV(srv) => {
             json!({
-                "type": "SRV",
                 "priority": srv.priority,
                 "weight": srv.weight,
                 "port": srv.port,
@@ -490,7 +559,6 @@ fn json_record(record: &Record) -> JsonValue {
         }
         Record::TLSA(tlsa) => {
             json!({
-                "type": "TLSA",
                 "certificate_usage": tlsa.certificate_usage,
                 "selector": tlsa.selector,
                 "matching_type": tlsa.matching_type,
@@ -499,33 +567,25 @@ fn json_record(record: &Record) -> JsonValue {
         }
         Record::TXT(txt) => {
             json!({
-                "type": "TXT",
                 "messages": txt.messages,
             })
         }
         Record::URI(uri) => {
             json!({
-                "type": "URI",
                 "priority": uri.priority,
                 "weight": uri.weight,
                 "target": uri.target,
             })
         }
-        Record::Other { type_number, bytes } => {
-            let type_name = match type_number {
-                UnknownQtype::HeardOf(name) => json!(name),
-                UnknownQtype::UnheardOf(num) => json!(num),
-            };
-
+        Record::Other { bytes, .. } => {
             json!({
-                "unknown": true,
-                "type": type_name,
                 "bytes": bytes,
             })
         }
     }
 }
 
+
 /// Prints a message describing the “error code” field of a DNS packet. This
 /// happens when the packet was received correctly, but the server indicated
 /// an error.

+ 2 - 4
src/requests.rs

@@ -1,7 +1,5 @@
 //! Request generation based on the user’s input arguments.
 
-use dns::Labels;
-
 use crate::connect::TransportType;
 use crate::resolve::Resolver;
 use crate::txid::TxidGenerator;
@@ -30,10 +28,10 @@ pub struct RequestGenerator {
 pub struct Inputs {
 
     /// The list of domain names to query.
-    pub domains: Vec<Labels>,
+    pub domains: Vec<dns::Labels>,
 
     /// The list of DNS record types to query for.
-    pub types: Vec<u16>,
+    pub types: Vec<dns::record::RecordType>,
 
     /// The list of DNS classes to query for.
     pub classes: Vec<dns::QClass>,