Переглянути джерело

Initial import

I’ve been working on this for a little while, and it’s finally approaching some semblance of stability.
Benjamin Sago 5 роки тому
коміт
30352d15b5

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+/target
+fuzz-*.log

+ 1 - 0
.rustfmt.toml

@@ -0,0 +1 @@
+disable_all_formatting = true

+ 1023 - 0
Cargo.lock

@@ -0,0 +1,1023 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "aho-corasick"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "hermit-abi 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "bitflags"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "byteorder"
+version = "1.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "bytes"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "cc"
+version = "1.0.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "core-foundation"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "ctor"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "datetime"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "iso8601 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "locale 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pad 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "difference"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "dns"
+version = "0.1.0"
+dependencies = [
+ "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "dns-transport"
+version = "0.1.0"
+dependencies = [
+ "async-trait 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)",
+ "derive_more 0.99.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "dns 0.1.0",
+ "hyper 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "hyper-tls 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio 0.2.19 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-tls 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "dog"
+version = "0.1.0-pre"
+dependencies = [
+ "ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
+ "datetime 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "dns 0.1.0",
+ "dns-transport 0.1.0",
+ "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
+ "humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "fuchsia-zircon"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "fuchsia-zircon-sys"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "futures-channel"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "futures-task"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "futures-util"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pin-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "getopts"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "h2"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio 0.2.19 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "http"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "http-body"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "httparse"
+version = "1.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "humantime"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "hyper"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "h2 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pin-project 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio 0.2.19 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tower-service 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "want 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "hyper 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio 0.2.19 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio-tls 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "iovec"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "iso8601"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "kernel32-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "libc"
+version = "0.2.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "locale"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "log"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "memchr"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "mio"
+version = "0.6.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
+ "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "miow"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "openssl 0.10.29 (registry+https://github.com/rust-lang/crates.io-index)",
+ "openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "openssl-sys 0.9.55 (registry+https://github.com/rust-lang/crates.io-index)",
+ "schannel 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)",
+ "security-framework 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "security-framework-sys 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "net2"
+version = "0.2.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "nom"
+version = "4.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "openssl"
+version = "0.10.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "openssl-sys 0.9.55 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cc 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "output_vt100"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "pad"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "pin-project"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "pin-project-internal 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "pretty_assertions"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ctor 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
+ "difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "output_vt100 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "quote"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.1.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "regex"
+version = "1.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)",
+ "thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "schannel"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "security-framework"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "core-foundation 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "security-framework-sys 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "serde_json"
+version = "1.0.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ryu 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "syn"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
+ "remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "time"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "tokio"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "tokio-tls"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio 0.2.19 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "tokio 0.2.19 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "try-lock"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "version_check"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "winapi"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "winapi"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "winapi-build"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "ws2_32-sys"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[metadata]
+"checksum aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada"
+"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+"checksum ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+"checksum async-trait 0.1.30 (registry+https://github.com/rust-lang/crates.io-index)" = "da71fef07bc806586090247e971229289f64c210a278ee5ae419314eb386b31d"
+"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+"checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
+"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
+"checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
+"checksum bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1"
+"checksum cc 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)" = "c3d87b23d6a92cd03af510a5ade527033f6aa6fa92161e2d5863a907d4c5e31d"
+"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+"checksum core-foundation 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171"
+"checksum core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
+"checksum ctor 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "cf6b25ee9ac1995c54d7adb2eff8cfffb7260bc774fb63c601ec65467f43cd9d"
+"checksum datetime 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4ce78cadfc77450e21ff8bd11ca6f8669949b3b0bd8af92f9c1ee4fb453db7b"
+"checksum derive_more 0.99.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e2323f3f47db9a0e77ce7a300605d8d2098597fc451ed1a97bb1f6411bb550a7"
+"checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
+"checksum env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
+"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
+"checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+"checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
+"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
+"checksum futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c77d04ce8edd9cb903932b608268b3fffec4163dc053b3b402bf47eac1f1a8"
+"checksum futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f25592f769825e89b92358db00d26f965761e094951ac44d3663ef25b7ac464a"
+"checksum futures-sink 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3466821b4bc114d95b087b850a724c6f83115e929bc88f1fa98a3304a944c8a6"
+"checksum futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7b0a34e53cf6cdcd0178aa573aed466b646eb3db769570841fda0c7ede375a27"
+"checksum futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "22766cf25d64306bedf0384da004d05c9974ab104fcc4528f1236181c18004c5"
+"checksum getopts 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
+"checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
+"checksum h2 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "377038bf3c89d18d6ca1431e7a5027194fbd724ca10592b9487ede5e8e144f42"
+"checksum hermit-abi 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8a0d737e0f947a1864e93d33fdef4af8445a00d1ed8dc0c8ddb73139ea6abf15"
+"checksum http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9"
+"checksum http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b"
+"checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9"
+"checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
+"checksum hyper 0.13.5 (registry+https://github.com/rust-lang/crates.io-index)" = "96816e1d921eca64d208a85aab4f7798455a8e34229ee5a88c935bdee1b78b14"
+"checksum hyper-tls 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3adcd308402b9553630734e9c36b77a7e48b3821251ca2493e8cd596763aafaa"
+"checksum indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292"
+"checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
+"checksum iso8601 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "43e86914a73535f3f541a765adea0a9fafcf53fa6adb73662c4988fd9233766f"
+"checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
+"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
+"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+"checksum libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)" = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005"
+"checksum locale 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5fdbe492a9c0238da900a1165c42fc5067161ce292678a6fe80921f30fe307fd"
+"checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
+"checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
+"checksum mio 0.6.21 (registry+https://github.com/rust-lang/crates.io-index)" = "302dec22bcf6bae6dfb69c647187f4b4d0fb6f535521f7bc022430ce8e12008f"
+"checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919"
+"checksum native-tls 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d"
+"checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88"
+"checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
+"checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096"
+"checksum openssl 0.10.29 (registry+https://github.com/rust-lang/crates.io-index)" = "cee6d85f4cb4c4f59a6a85d5b68a233d280c82e29e822913b9c8b129fbf20bdd"
+"checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
+"checksum openssl-sys 0.9.55 (registry+https://github.com/rust-lang/crates.io-index)" = "7717097d810a0f2e2323f9e5d11e71608355e24828410b55b9d4f18aa5f9a5d8"
+"checksum output_vt100 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9"
+"checksum pad 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3"
+"checksum pin-project 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)" = "6f6a7f5eee6292c559c793430c55c00aea9d3b3d1905e855806ca4d7253426a2"
+"checksum pin-project-internal 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)" = "8988430ce790d8682672117bc06dda364c0be32d3abd738234f19f3240bad99a"
+"checksum pin-project-lite 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "237844750cfbb86f67afe27eee600dfbbcb6188d734139b534cbfbf4f96792ae"
+"checksum pin-utils 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+"checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677"
+"checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
+"checksum pretty_assertions 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427"
+"checksum proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3"
+"checksum quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+"checksum quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f"
+"checksum rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+"checksum rand_chacha 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+"checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
+"checksum regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692"
+"checksum regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae"
+"checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e"
+"checksum ryu 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ed3d612bc64430efeb3f7ee6ef26d590dce0c43249217bddc62112540c7941e1"
+"checksum schannel 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "039c25b130bd8c1321ee2d7de7fde2659fa9c2744e4bb29711cfc852ea53cd19"
+"checksum security-framework 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3f331b9025654145cd425b9ded0caf8f5ae0df80d418b326e2dc1c3dc5eb0620"
+"checksum security-framework-sys 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405"
+"checksum serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)" = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399"
+"checksum serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)" = "da07b57ee2623368351e9a0488bb0b261322a15a6e0ae53e243cbdc0f4208da9"
+"checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
+"checksum syn 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)" = "410a7488c0a728c7ceb4ad59b9567eb4053d02e8cc7f5c0e0eeeb39518369213"
+"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
+"checksum termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f"
+"checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
+"checksum time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
+"checksum tokio 0.2.19 (registry+https://github.com/rust-lang/crates.io-index)" = "7d9c43f1bb96970e153bcbae39a65e249ccb942bd9d36dbdf086024920417c9c"
+"checksum tokio-tls 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7bde02a3a5291395f59b06ec6945a3077602fac2b07eeeaf0dee2122f3619828"
+"checksum tokio-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499"
+"checksum tower-service 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860"
+"checksum try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382"
+"checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
+"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
+"checksum vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168"
+"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
+"checksum want 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+"checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
+"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
+"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
+"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+"checksum winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"

+ 46 - 0
Cargo.toml

@@ -0,0 +1,46 @@
+[package]
+name = "dog"
+version = "0.1.0-pre"
+authors = ["Benjamin Sago <ogham@bsago.me>"]
+edition = "2018"
+exclude = ["/completions/*", "/man/*", "/xtests/*", "/clippy.toml", "/screenshots.png", "/README.md"]
+
+[[bin]]
+name = "dog"
+path = "src/main.rs"
+
+[workspace]
+members = [
+  "dns",
+  "dns-transport",
+]
+
+
+[dependencies]
+
+# dns stuff
+dns = { path = "./dns" }
+dns-transport = { path = "./dns-transport" }
+
+# command-line
+ansi_term = "0.12"
+atty = "0.2"
+getopts = "0.2"
+
+# transaction ID generation
+rand = "0.7"
+
+# json
+serde = "1.0"
+serde_json = "1.0"
+
+# logging
+env_logger = "0.7"
+log = "0.4"
+
+[build-dependencies]
+datetime = "0.5"
+regex = "1.3"
+
+[dev-dependencies]
+pretty_assertions = "0.6"

+ 45 - 0
Justfile

@@ -0,0 +1,45 @@
+all: build test
+all-release: build-release test-release
+
+export DOG_DEBUG := ""
+
+
+# compiles the dog binary
+@build:
+    cargo build
+
+# compiles the dog binary (in release mode)
+@build-release:
+    cargo build --release --verbose
+
+# runs unit tests
+@test:
+    cargo test --all -- --quiet
+
+# runs unit tests (in release mode)
+@test-release:
+    cargo test --release --all --verbose
+
+# renders the documentation
+@doc args="":
+    cargo doc --no-deps --all {{args}}
+
+# runs fuzzing on the dns crate
+@fuzz:
+    cargo +nightly fuzz --version
+    cd dns; cargo +nightly fuzz run fuzz_parsing -- -jobs=`nproc` -workers=`nproc` -runs=69105
+
+# prints out the data that caused crashes during fuzzing as hexadecimal
+@fuzz-hex:
+	for crash in dns/fuzz/artifacts/fuzz_parsing/crash-*; do echo; echo $crash; hexyl $crash; done
+
+# removes fuzz log files
+@fuzz-clean:
+	rm dns/fuzz/fuzz-*.log
+
+# lints the code
+@clippy:
+    cargo clippy -- -A clippy::module_name_repetitions \
+                    -A clippy::module_inception \
+                    -A clippy::non_ascii_literal \
+                    -A clippy::use_self

+ 85 - 0
README.md

@@ -0,0 +1,85 @@
+# dog
+
+Dogs _can_ look up!
+
+**dog** is a command-line DNS client.
+It has colourful output, supports the DNS-over-TLS and DNS-over-HTTPS protocols, and can emit JSON.
+
+
+## Screenshots
+
+![A screenshot of dog being used](dog-screenshot.png)
+
+
+## Examples
+
+    dog example.net                          Query a domain using default settings
+    dog example.net MX                       ...looking up MX records instead
+    dog example.net MX @1.1.1.1              ...using a specific nameserver instead
+    dog example.net MX @1.1.1.1 -T           ...using TCP rather than UDP
+    dog -q example.net -t MX -n 1.1.1.1 -T   As above, but using explicit arguments
+
+
+## Options
+
+### Query options
+
+    <arguments>              Human-readable host names, nameservers, types, or classes
+    -q, --query=HOST         Host name or IP address to query
+    -t, --type=TYPE          Type of the DNS record being queried (A, MX, NS...)
+    -n, --nameserver=ADDR    Address of the nameserver to send packets to
+    --class=CLASS            Network class of the DNS record being queried (IN, CH, HS)
+
+### Sending options
+
+    --edns=SETTING           Whether to OPT in to EDNS (disable, hide, show)
+    --txid=NUMBER            Set the transaction ID to a specific value
+    -Z=TWEAKS                Uncommon protocol tweaks
+
+### Protocol options
+
+    -U, --udp                Use the DNS protocol over UDP
+    -T, --tcp                Use the DNS protocol over TCP
+    -S, --tls                Use the DNS-over-TLS protocol
+    -H, --https              Use the DNS-over-HTTPS protocol
+
+### Output options
+
+    -1, --short              Short mode: display nothing but the first result
+    -J, --json               Display the output as JSON
+    --color, --colour=WHEN   When to colourise the output (always, automatic, never)
+    --seconds                Do not format durations, display them as seconds
+    --time                   Print how long the response took to arrive
+
+
+## Installation
+
+Installing dog requires building it from source.
+
+
+### Compilation
+
+dog is written in [Rust](https://www.rust-lang.org).
+You will need a Rust toolchain installed in order to compile it.
+To build, download the source code and run:
+
+    cargo build --release
+
+And the binary will be present in `target/release/dog`.
+
+
+### Minimum supported Rust version
+
+Currently, dog is built and tested against the most recent stable Rust version, with no compatibility guarantees for any older versions.
+
+Once dog is more mature and development has settled down, a minimum supported Rust version will be chosen.
+
+
+## Documentation
+
+For documentation on how to use dog, see the website: <https://dns.lookup.dog>
+
+
+## See also
+
+`mutt`, `tail`, `sleep`, `roff`

+ 95 - 0
build.rs

@@ -0,0 +1,95 @@
+//! This build script gets run during every build. Its purpose is to put
+//! together the files used for the `--help` and `--version`, which need to
+//! come in both coloured and non-coloured variants. The main usage text is
+//! contained in `src/usage.txt`; to make it easier to edit, backslashes (\)
+//! are used instead of the beginning of ANSI escape codes.
+//!
+//! The version string is quite complex: we want to show the version,
+//! current Git hash, and compilation date when building *debug*
+//! versions, but just the version for *release* versions.
+//!
+//! This script generates the string from the environment variables
+//! that Cargo adds (http://doc.crates.io/environment-variables.html)
+//! and runs `git` to get the SHA1 hash. It then writes the strings
+//! into files, which we can include during compilation.
+
+use std::env;
+use std::fs::File;
+use std::io::{self, Write};
+use std::path::PathBuf;
+
+use datetime::{LocalDateTime, ISO};
+use regex::Regex;
+
+
+/// The build script entry point.
+fn main() -> io::Result<()> {
+    let usage   = include_str!("src/usage.txt");
+    let tagline = "dog \\1;32m●\\0m command-line DNS client";
+    let url     = "https://dns.lookup.dog/";
+
+    let ver = if is_development_version() {
+            format!("{}\nv{} [{}] built on {} \\1;31m(pre-release!)\\0m\n\\1;4;34m{}\\0m", tagline, cargo_version(), git_hash(), build_date(), url)
+        }
+        else {
+            format!("{}\nv{}\n\\1;4;34m{}\\0m", tagline, cargo_version(), url)
+        };
+
+    // We need to create these files in the Cargo output directory.
+    let out = PathBuf::from(env::var("OUT_DIR").unwrap());
+
+    // The bits .txt files contain ANSI escape codes, ish.
+    let control_code = Regex::new(r##"\\.+?m"##).unwrap();
+
+    // Pretty version text
+    let mut f = File::create(&out.join("version.pretty.txt"))?;
+    write!(f, "{}\n", ver.replace("\\", "\x1B["))?;
+
+    // Bland version text
+    let mut f = File::create(&out.join("version.bland.txt"))?;
+    write!(f, "{}\n", control_code.replace_all(&ver, ""))?;
+
+    // Pretty usage text
+    let mut f = File::create(&out.join("usage.pretty.txt"))?;
+    write!(f, "{}\n\n{}", tagline.replace("\\", "\x1B["), usage.replace("\\", "\x1B["))?;
+
+    // Bland usage text
+    let mut f = File::create(&out.join("usage.bland.txt"))?;
+    write!(f, "{}\n\n{}", control_code.replace_all(tagline, ""), control_code.replace_all(usage, ""))?;
+
+    Ok(())
+}
+
+
+/// Retrieve the project’s current Git hash, as a string.
+fn git_hash() -> String {
+    use std::process::Command;
+
+    String::from_utf8_lossy(
+        &Command::new("git")
+            .args(&["rev-parse", "--short", "HEAD"])
+            .output().unwrap()
+            .stdout).trim().to_string()
+}
+
+
+/// Whether we should show pre-release info in the version string.
+///
+/// Both weekly releases and actual releases are --release releases,
+/// but actual releases will have a proper version number.
+fn is_development_version() -> bool {
+    cargo_version().ends_with("-pre") || env::var("PROFILE").unwrap() == "debug"
+}
+
+
+/// Retrieves the [package] version in Cargo.toml as a string.
+fn cargo_version() -> String {
+    env::var("CARGO_PKG_VERSION").unwrap()
+}
+
+
+/// Formats the current date as an ISO 8601 string.
+fn build_date() -> String {
+    let now = LocalDateTime::now();
+    format!("{}", now.date().iso())
+}

+ 1 - 0
clippy.toml

@@ -0,0 +1 @@
+

+ 23 - 0
dns-transport/Cargo.toml

@@ -0,0 +1,23 @@
+[package]
+name = "dns-transport"
+version = "0.1.0"
+authors = ["Benjamin Sago <ogham@bsago.me>"]
+edition = "2018"
+
+
+[dependencies]
+derive_more = "0.99"
+
+# dns wire protocol
+dns = { path = "../dns" }
+
+# logging
+log = "0.4"
+
+# networking
+async-trait = "0.1"
+hyper = "0.13"
+hyper-tls = "0.4"
+native-tls = "0.2"
+tokio = { version = "0.2", features = ["dns", "tcp", "udp", "io-util"] }  # dns is used to resolve nameservers
+tokio-tls = "0.3"

+ 64 - 0
dns-transport/src/auto.rs

@@ -0,0 +1,64 @@
+use async_trait::async_trait;
+use log::*;
+
+use dns::{Request, Response};
+use super::{Transport, Error, UdpTransport, TcpTransport};
+
+
+/// The **automatic transport**, which uses the UDP transport, then tries
+/// using the TCP transport if the first one fails.
+///
+/// # Examples
+///
+/// ```no_run
+/// use dns_transport::{Transport, AutoTransport};
+/// use dns::{Request, Flags, Query, QClass, qtype, record::NS};
+///
+/// let query = Query {
+///     qname: String::from("dns.lookup.dog"),
+///     qclass: QClass::IN,
+///     qtype: qtype!(NS),
+/// };
+///
+/// let request = Request {
+///     transaction_id: 0xABCD,
+///     flags: Flags::query(),
+///     queries: vec![ query ],
+///     additional: None,
+/// };
+///
+/// let transport = AutoTransport::new("8.8.8.8");
+/// transport.send(&request);
+/// ```
+#[derive(Debug)]
+pub struct AutoTransport {
+    addr: String,
+}
+
+impl AutoTransport {
+
+    /// Creates a new automatic transport that connects to the given host.
+    pub fn new(sa: impl Into<String>) -> Self {
+        let addr = sa.into();
+        Self { addr }
+    }
+}
+
+
+#[async_trait]
+impl Transport for AutoTransport {
+    async fn send(&self, request: &Request) -> Result<Response, Error> {
+        let udp_transport = UdpTransport::new(&self.addr);
+        let udp_response = udp_transport.send(&request).await?;
+
+        if ! udp_response.flags.truncated {
+            return Ok(udp_response);
+        }
+
+        debug!("Truncated flag set, so switching to TCP");
+
+        let tcp_transport = TcpTransport::new(&self.addr);
+        let tcp_response = tcp_transport.send(&request).await?;
+        Ok(tcp_response)
+    }
+}

+ 85 - 0
dns-transport/src/https.rs

@@ -0,0 +1,85 @@
+use async_trait::async_trait;
+use hyper_tls::HttpsConnector;
+use hyper::Body;
+use hyper::body::HttpBody as _;
+use hyper::Client;
+use log::*;
+
+use dns::{Request, Response};
+use super::{Transport, Error};
+
+
+/// The **HTTPS transport**, which uses Hyper.
+///
+/// # Examples
+///
+/// ```no_run
+/// use dns_transport::{Transport, HttpsTransport};
+/// use dns::{Request, Flags, Query, QClass, qtype, record::A};
+///
+/// let query = Query {
+///     qname: String::from("dns.lookup.dog"),
+///     qclass: QClass::IN,
+///     qtype: qtype!(A),
+/// };
+///
+/// let request = Request {
+///     transaction_id: 0xABCD,
+///     flags: Flags::query(),
+///     queries: vec![ query ],
+///     additional: None,
+/// };
+///
+/// let transport = HttpsTransport::new("https://cloudflare-dns.com/dns-query");
+/// transport.send(&request);
+/// ```
+#[derive(Debug)]
+pub struct HttpsTransport {
+    url: String,
+}
+
+impl HttpsTransport {
+
+    /// Creates a new HTTPS transport that connects to the given URL.
+    pub fn new(url: impl Into<String>) -> Self {
+        Self { url: url.into() }
+    }
+}
+
+#[async_trait]
+impl Transport for HttpsTransport {
+    async fn send(&self, request: &Request) -> Result<Response, Error> {
+        let https = HttpsConnector::new();
+        let client = Client::builder().build::<_, hyper::Body>(https);
+
+        let bytes = request.to_bytes().expect("failed to serialise request");
+        info!("Sending {} bytes of data to {:?}", bytes.len(), self.url);
+
+        let request = hyper::Request::builder()
+            .method("POST")
+            .uri(&self.url)
+            .header("Content-Type", "application/dns-message")
+            .header("Accept",       "application/dns-message")
+            .body(Body::from(bytes))
+            .expect("Failed to build request");  // we control the request, so this should never fail
+
+        let mut response = client.request(request).await?;
+        debug!("Response: {}", response.status());
+        debug!("Headers: {:#?}", response.headers());
+
+        if response.status() != 200 {
+            return Err(Error::BadRequest);
+        }
+
+        debug!("Reading body...");
+        let mut buf = Vec::new();
+        while let Some(chunk) = response.body_mut().data().await {
+            buf.extend(&chunk?);
+        }
+
+        info!("Received {} bytes of data", buf.len());
+        let response = Response::from_bytes(&buf)?;
+
+        Ok(response)
+    }
+}

+ 75 - 0
dns-transport/src/lib.rs

@@ -0,0 +1,75 @@
+//! All the DNS transport types.
+
+#![warn(deprecated_in_future)]
+#![warn(future_incompatible)]
+#![warn(missing_copy_implementations)]
+#![warn(missing_docs)]
+#![warn(nonstandard_style)]
+#![warn(rust_2018_compatibility)]
+#![warn(rust_2018_idioms)]
+#![warn(single_use_lifetimes)]
+#![warn(trivial_casts, trivial_numeric_casts)]
+#![warn(unused)]
+
+#![deny(unsafe_code)]
+
+use async_trait::async_trait;
+use derive_more::From;
+
+use dns::{Request, Response};
+
+
+// Re-export the five transport types, as well as the Tokio runtime, so that
+// the dog crate can just use something called “Runtime” without worrying
+// about which runtime it actually is.
+
+mod auto;
+pub use self::auto::AutoTransport;
+
+mod udp;
+pub use self::udp::UdpTransport;
+
+mod tcp;
+pub use self::tcp::TcpTransport;
+
+mod tls;
+pub use self::tls::TlsTransport;
+
+mod https;
+pub use self::https::HttpsTransport;
+
+pub use tokio::runtime::Runtime;
+
+
+
+/// The trait implemented by all four transport types.
+#[async_trait]
+pub trait Transport {
+
+    /// Convert the request to bytes, send it over the network, wait for a
+    /// response, deserialise it from bytes, and return it, asynchronously.
+    async fn send(&self, request: &Request) -> Result<Response, Error>;
+}
+
+/// Something that can go wrong making a DNS request.
+#[derive(Debug, From)]  // can't be PartialEq due to tokio error
+pub enum Error {
+
+    /// There was a problem with the network sending the request or receiving
+    /// a response asynchorously.
+    NetworkError(tokio::io::Error),
+
+    /// There was a problem making an HTTPS request.
+    HttpError(hyper::Error),
+
+    /// There was a problem making a TLS request.
+    TlsError(native_tls::Error),
+
+    /// The data in the response did not parse correctly from the DNS wire
+    /// protocol format.
+    WireError(dns::WireError),
+
+    /// The server specifically indicated that the request we sent it was
+    /// malformed.
+    BadRequest,
+}

+ 120 - 0
dns-transport/src/tcp.rs

@@ -0,0 +1,120 @@
+use async_trait::async_trait;
+use log::*;
+use tokio::net::TcpStream;
+use tokio::io::{AsyncReadExt, AsyncWriteExt};
+
+use dns::{Request, Response};
+use super::{Transport, Error};
+
+
+/// The **TCP transport**, which uses the stdlib.
+///
+/// # Examples
+///
+/// ```no_run
+/// use dns_transport::{Transport, TcpTransport};
+/// use dns::{Request, Flags, Query, QClass, qtype, record::MX};
+///
+/// let query = Query {
+///     qname: String::from("dns.lookup.dog"),
+///     qclass: QClass::IN,
+///     qtype: qtype!(MX),
+/// };
+///
+/// let request = Request {
+///     transaction_id: 0xABCD,
+///     flags: Flags::query(),
+///     queries: vec![ query ],
+///     additional: None,
+/// };
+///
+/// let transport = TcpTransport::new("8.8.8.8");
+/// transport.send(&request);
+/// ```
+///
+/// # Reference
+///
+/// - [RFC 1035 §4.2.2](https://tools.ietf.org/html/rfc1035) — Domain Names, Implementation and Specification (November 1987)
+/// - [RFC 7766](https://tools.ietf.org/html/rfc1035) — DNS Transport over TCP, Implementation Requirements (March 2016)
+#[derive(Debug)]
+pub struct TcpTransport {
+    addr: String,
+}
+
+impl TcpTransport {
+
+    /// Creates a new TCP transport that connects to the given host.
+    pub fn new(sa: impl Into<String>) -> Self {
+        Self { addr: sa.into() }
+    }
+}
+
+
+#[async_trait]
+impl Transport for TcpTransport {
+    async fn send(&self, request: &Request) -> Result<Response, Error> {
+        let mut stream =
+            if self.addr.contains(':') {
+                TcpStream::connect(&*self.addr).await?
+            }
+            else {
+                TcpStream::connect((&*self.addr, 53)).await?
+            };
+        info!("Created stream");
+
+        // The message is prepended with the length when sent over TCP,
+        // so the server knows how long it is (RFC 1035 §4.2.2)
+        let mut bytes = request.to_bytes().expect("failed to serialise request");
+        let len_bytes = (bytes.len() as u16).to_be_bytes();
+        bytes.insert(0, len_bytes[0]);
+        bytes.insert(1, len_bytes[1]);
+
+        info!("Sending {} bytes of data to {} over TCP", bytes.len(), self.addr);
+
+        let written_len = stream.write(&bytes).await?;
+        debug!("Wrote {} bytes", written_len);
+
+        info!("Waiting to receive...");
+        let mut buf = [0; 4096];
+        let mut read_len = stream.read(&mut buf[..]).await?;
+
+        if read_len == 0 {
+            panic!("Received no bytes!");
+        }
+        else if read_len == 1 {
+            info!("Received one byte of data");
+            let second_read_len = stream.read(&mut buf[1..]).await?;
+            if second_read_len == 0 {
+                panic!("Received no bytes the second time!");
+            }
+
+            read_len += second_read_len;
+        }
+        else {
+            info!("Received {} bytes of data", read_len);
+        }
+
+        let total_len = u16::from_be_bytes([buf[0], buf[1]]);
+        if read_len - 2 == usize::from(total_len) {
+            let response = Response::from_bytes(&buf[2 .. read_len])?;
+            return Ok(response);
+        }
+
+        debug!("We need to read {} bytes total", total_len);
+        let mut combined_buffer = buf[2..read_len].to_vec();
+        while combined_buffer.len() < usize::from(total_len) {
+            let mut buf = [0; 4096];
+            let read_len = stream.read(&mut buf[..]).await?;
+            info!("Received further {} bytes of data (of {})", read_len, total_len);
+
+            if read_len == 0 {
+                panic!("Read zero bytes!");
+            }
+
+            combined_buffer.extend(&buf[0 .. read_len]);
+        }
+
+        let response = Response::from_bytes(&combined_buffer)?;
+        Ok(response)
+    }
+}

+ 99 - 0
dns-transport/src/tls.rs

@@ -0,0 +1,99 @@
+use async_trait::async_trait;
+use log::*;
+use native_tls::TlsConnector;
+use tokio::io::{AsyncReadExt, AsyncWriteExt};
+use tokio::net::TcpStream;
+
+use dns::{Request, Response};
+use super::{Transport, Error};
+
+
+/// The **TLS transport**, which uses Tokio.
+///
+/// # Examples
+///
+/// ```no_run
+/// use dns_transport::{Transport, TlsTransport};
+/// use dns::{Request, Flags, Query, QClass, qtype, record::SRV};
+///
+/// let query = Query {
+///     qname: String::from("dns.lookup.dog"),
+///     qclass: QClass::IN,
+///     qtype: qtype!(SRV),
+/// };
+///
+/// let request = Request {
+///     transaction_id: 0xABCD,
+///     flags: Flags::query(),
+///     queries: vec![ query ],
+///     additional: None,
+/// };
+///
+/// let transport = TlsTransport::new("dns.google");
+/// transport.send(&request);
+/// ```
+#[derive(Debug)]
+pub struct TlsTransport {
+    addr: String,
+}
+
+impl TlsTransport {
+
+    /// Creates a new TLS transport that connects to the given host.
+    pub fn new(sa: impl Into<String>) -> Self {
+        let addr = sa.into();
+        Self { addr }
+    }
+}
+
+#[async_trait]
+impl Transport for TlsTransport {
+    async fn send(&self, request: &Request) -> Result<Response, Error> {
+        let connector = TlsConnector::new()?;
+        let connector = tokio_tls::TlsConnector::from(connector);
+
+        info!("Opening TLS socket");
+        let stream =
+            if self.addr.contains(':') {
+                TcpStream::connect(&*self.addr).await?
+            }
+            else {
+                TcpStream::connect((&*self.addr, 853)).await?
+            };
+
+        info!("Connecting");
+        let mut stream = connector.connect(self.sni_domain(), stream).await?;
+
+        // As with TCP, we need to prepend the message with its length.
+        let mut bytes = request.to_bytes().expect("failed to serialise request");
+        let len_bytes = (bytes.len() as u16).to_be_bytes();
+        bytes.insert(0, len_bytes[0]);
+        bytes.insert(1, len_bytes[1]);
+
+        info!("Sending {} bytes of data to {}", bytes.len(), self.addr);
+
+        stream.write_all(&bytes).await?;
+        debug!("Sent");
+
+        info!("Waiting to receive...");
+        let mut buf = [0; 4096];
+        let len = stream.read(&mut buf).await?;
+
+        // Remember to deal with the length again.
+        info!("Received {} bytes of data", buf.len());
+        let response = Response::from_bytes(&buf[2..len])?;
+
+        Ok(response)
+    }
+}
+
+impl TlsTransport {
+    fn sni_domain(&self) -> &str {
+        if let Some(colon_index) = self.addr.find(':') {
+            &self.addr[.. colon_index]
+        }
+        else {
+            &self.addr[..]
+        }
+    }
+}

+ 78 - 0
dns-transport/src/udp.rs

@@ -0,0 +1,78 @@
+use std::net::Ipv4Addr;
+
+use async_trait::async_trait;
+use log::*;
+use tokio::net::UdpSocket;
+
+use dns::{Request, Response};
+use super::{Transport, Error};
+
+
+/// The **UDP transport**, which uses the stdlib.
+///
+/// # Examples
+///
+/// ```no_run
+/// use dns_transport::{Transport, UdpTransport};
+/// use dns::{Request, Flags, Query, QClass, qtype, record::NS};
+///
+/// let query = Query {
+///     qname: String::from("dns.lookup.dog"),
+///     qclass: QClass::IN,
+///     qtype: qtype!(NS),
+/// };
+///
+/// let request = Request {
+///     transaction_id: 0xABCD,
+///     flags: Flags::query(),
+///     queries: vec![ query ],
+///     additional: None,
+/// };
+///
+/// let transport = UdpTransport::new("8.8.8.8");
+/// transport.send(&request);
+/// ```
+#[derive(Debug)]
+pub struct UdpTransport {
+    addr: String,
+}
+
+impl UdpTransport {
+
+    /// Creates a new UDP transport that connects to the given host.
+    pub fn new(sa: impl Into<String>) -> Self {
+        let addr = sa.into();
+        Self { addr }
+    }
+}
+
+
+#[async_trait]
+impl Transport for UdpTransport {
+    async fn send(&self, request: &Request) -> Result<Response, Error> {
+        info!("Opening UDP socket");
+        let mut socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).await?;
+
+        if self.addr.contains(':') {
+            socket.connect(&*self.addr).await?;
+        }
+        else {
+            socket.connect((&*self.addr, 53)).await?;
+        }
+
+        let bytes = request.to_bytes().expect("failed to serialise request");
+        info!("Sending {} bytes of data to {} over UDP", bytes.len(), self.addr);
+
+        let len = socket.send(&bytes).await?;
+        debug!("Sent {} bytes", len);
+
+        info!("Waiting to receive...");
+        let mut buf = vec![0u8; 1024];
+        let len = socket.recv(&mut buf).await?;
+
+        info!("Received {} bytes of data", len);
+        let response = Response::from_bytes(&buf[..len])?;
+
+        Ok(response)
+    }
+}

+ 18 - 0
dns/Cargo.toml

@@ -0,0 +1,18 @@
+[package]
+name = "dns"
+version = "0.1.0"
+authors = ["Benjamin Sago <ogham@bsago.me>"]
+edition = "2018"
+
+
+[dependencies]
+
+# logging
+log = "0.4"
+
+# protocol parsing
+byteorder = "1.3"
+
+# json
+serde = "1.0"
+serde_json = "1.0"

+ 4 - 0
dns/fuzz/.gitignore

@@ -0,0 +1,4 @@
+
+target
+corpus
+artifacts

+ 93 - 0
dns/fuzz/Cargo.lock

@@ -0,0 +1,93 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "arbitrary"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "byteorder"
+version = "1.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "cc"
+version = "1.0.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "dns"
+version = "0.1.0"
+dependencies = [
+ "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "dns-fuzz"
+version = "0.0.1"
+dependencies = [
+ "dns 0.1.0",
+ "libfuzzer-sys 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "libfuzzer-sys"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "arbitrary 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cc 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "log"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "serde"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "serde_json"
+version = "1.0.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ryu 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[metadata]
+"checksum arbitrary 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1148c9b25d393a07c4cc3ef5dd30f82a40a1c261018c4a670611ed8e76cad3ea"
+"checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
+"checksum cc 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)" = "c3d87b23d6a92cd03af510a5ade527033f6aa6fa92161e2d5863a907d4c5e31d"
+"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+"checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
+"checksum libfuzzer-sys 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8d718794b8e23533b9069bd2c4597d69e41cc7ab1c02700a502971aca0cdcf24"
+"checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
+"checksum ryu 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ed3d612bc64430efeb3f7ee6ef26d590dce0c43249217bddc62112540c7941e1"
+"checksum serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)" = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399"
+"checksum serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)" = "da07b57ee2623368351e9a0488bb0b261322a15a6e0ae53e243cbdc0f4208da9"

+ 22 - 0
dns/fuzz/Cargo.toml

@@ -0,0 +1,22 @@
+[package]
+name = "dns-fuzz"
+version = "0.0.1"
+authors = ["Automatically generated"]
+publish = false
+
+[package.metadata]
+cargo-fuzz = true
+
+[dependencies.dns]
+path = ".."
+
+[dependencies.libfuzzer-sys]
+version = "0.3.0"
+
+# Prevent this from interfering with workspaces
+[workspace]
+members = ["."]
+
+[[bin]]
+name = "fuzz_parsing"
+path = "fuzz_targets/fuzz_parsing.rs"

+ 8 - 0
dns/fuzz/fuzz_targets/fuzz_parsing.rs

@@ -0,0 +1,8 @@
+#![no_main]
+#[macro_use] extern crate libfuzzer_sys;
+extern crate dns;
+use dns::Response;
+
+fuzz_target!(|data: &[u8]| {
+    let _ = Response::from_bytes(data);
+});

+ 27 - 0
dns/src/lib.rs

@@ -0,0 +1,27 @@
+#![warn(deprecated_in_future)]
+#![warn(future_incompatible)]
+#![warn(missing_copy_implementations)]
+#![warn(missing_docs)]
+#![warn(nonstandard_style)]
+#![warn(rust_2018_compatibility)]
+#![warn(rust_2018_idioms)]
+#![warn(single_use_lifetimes)]
+#![warn(trivial_casts, trivial_numeric_casts)]
+#![warn(unused)]
+
+#![deny(unsafe_code)]
+
+
+//! The DNS crate is the ‘library’ part of dog. It implements the DNS
+//! protocol: creating and decoding packets from their byte structure.
+
+
+mod types;
+pub use self::types::*;
+
+mod strings;
+
+mod wire;
+pub use self::wire::{Wire, WireError, find_qtype_number};
+
+pub mod record;

+ 72 - 0
dns/src/record/a.rs

@@ -0,0 +1,72 @@
+use std::net::Ipv4Addr;
+
+use crate::wire::*;
+
+
+/// An **A** record type, which contains an `Ipv4Address`.
+///
+/// # References
+///
+/// - [RFC 1035 §3.4.1](https://tools.ietf.org/html/rfc1035) — Domain Names, Implementation and Specification (November 1987)
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub struct A {
+
+    /// The IPv4 address contained in the packet.
+    pub address: Ipv4Addr,
+}
+
+impl Wire for A {
+    const NAME: &'static str = "A";
+    const RR_TYPE: u16 = 1;
+
+    fn read(len: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
+        let mut buf = Vec::new();
+        for _ in 0 .. len {
+            buf.push(c.read_u8()?);
+        }
+
+        if let [a, b, c, d] = *buf {
+            let address = Ipv4Addr::new(a, b, c, d);
+            Ok(A { address })
+        }
+        else {
+            Err(WireError::WrongLength { expected: 4, got: buf.len() as u16 })
+        }
+    }
+}
+
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn parses() {
+        let buf = &[ 127, 0, 0, 1 ];
+
+        assert_eq!(A::read(4, &mut Cursor::new(buf)).unwrap(),
+                   A { address: Ipv4Addr::new(127, 0, 0, 1) });
+    }
+
+    #[test]
+    fn too_short() {
+        let buf = &[ 127, 0, 1 ];
+
+        assert_eq!(A::read(3, &mut Cursor::new(buf)),
+                   Err(WireError::WrongLength { expected: 4, got: 3 }));
+    }
+
+    #[test]
+    fn too_long() {
+        let buf = &[ 127, 0, 0, 0, 1 ];
+
+        assert_eq!(A::read(5, &mut Cursor::new(buf)),
+                   Err(WireError::WrongLength { expected: 4, got: 5 }));
+    }
+
+    #[test]
+    fn empty() {
+        assert_eq!(A::read(0, &mut Cursor::new(&[])),
+                   Err(WireError::WrongLength { expected: 4, got: 0 }));
+    }
+}

+ 81 - 0
dns/src/record/aaaa.rs

@@ -0,0 +1,81 @@
+use std::net::Ipv6Addr;
+
+use crate::wire::*;
+
+
+/// A **AAAA** record, which contains an `Ipv6Address`.
+///
+/// # References
+///
+/// - [RFC 3596](https://tools.ietf.org/html/rfc3596) — DNS Extensions to Support IP Version 6 (October 2003)
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub struct AAAA {
+
+    /// The IPv6 address contained in the packet.
+    pub address: Ipv6Addr,
+}
+
+impl Wire for AAAA {
+    const NAME: &'static str = "AAAA";
+    const RR_TYPE: u16 = 28;
+
+    fn read(len: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
+        let mut buf = Vec::new();
+        for _ in 0 .. len {
+            buf.push(c.read_u8()?);
+        }
+
+        if let [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p] = *buf {
+            let address = Ipv6Addr::from([a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p]);
+            // probably the best two lines of code I have ever written
+            Ok(AAAA { address })
+        }
+        else {
+            Err(WireError::WrongLength { expected: 16, got: buf.len() as u16 })
+        }
+    }
+}
+
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn parses() {
+        let buf = &[ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ];
+
+        assert_eq!(AAAA::read(16, &mut Cursor::new(buf)).unwrap(),
+                   AAAA { address: Ipv6Addr::new(0,0,0,0,0,0,0,0) });
+    }
+
+    #[test]
+    fn too_long() {
+        let buf = &[9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9];
+
+        assert_eq!(AAAA::read(19, &mut Cursor::new(buf)),
+                   Err(WireError::WrongLength { expected: 16, got: 19 }));
+    }
+
+    #[test]
+    fn too_empty() {
+        let buf = &[];
+
+        assert_eq!(AAAA::read(0, &mut Cursor::new(buf)),
+                   Err(WireError::WrongLength { expected: 16, got: 0 }));
+    }
+
+    #[test]
+    fn too_short() {
+        let buf = &[ 5,5,5,5,5 ];
+
+        assert_eq!(AAAA::read(5, &mut Cursor::new(buf)),
+                   Err(WireError::WrongLength { expected: 16, got: 5 }));
+    }
+
+    #[test]
+    fn empty() {
+        assert_eq!(AAAA::read(0, &mut Cursor::new(&[])),
+                   Err(WireError::WrongLength { expected: 16, got: 0 }));
+    }
+}

+ 72 - 0
dns/src/record/caa.rs

@@ -0,0 +1,72 @@
+use crate::wire::*;
+
+
+/// A **CAA** record. These allow domain names to specify which Certificate
+/// Authorities are allowed to issue certificates for the domain.
+///
+/// # References
+///
+/// - [RFC 6844](https://tools.ietf.org/html/rfc6844) — DNS Certification Authority Authorization Resource Record (January 2013s
+#[derive(PartialEq, Debug, Clone)]
+pub struct CAA {
+
+    /// Whether this record is marked as “critical” or not.
+    pub critical: bool,
+
+    /// The “tag” part of the CAA record.
+    pub tag: String,
+
+    /// The “value” part of the CAA record.
+    pub value: String,
+}
+
+impl Wire for CAA {
+    const NAME: &'static str = "CAA";
+    const RR_TYPE: u16 = 257;
+
+    fn read(len: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
+        let flags = c.read_u8()?;
+        let tag_length = c.read_u8()?;
+
+        let mut tag = Vec::new();
+        for _ in 0 .. tag_length {
+            tag.push(c.read_u8()?);
+        }
+
+        let mut value = Vec::new();
+        for _ in 0 .. len.saturating_sub(u16::from(tag_length)).saturating_sub(2) {
+            value.push(c.read_u8()?);
+        }
+
+        Ok(CAA {
+            critical: flags & 0b_1000_0000 == 0b_1000_0000,
+            tag: String::from_utf8_lossy(&tag).to_string(),
+            value: String::from_utf8_lossy(&value).to_string(),
+        })
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn parses() {
+        let buf = &[ 0x00, 0x09, 0x69, 0x73, 0x73, 0x75, 0x65, 0x77, 0x69,
+                     0x6c, 0x64, 0x65, 0x6e, 0x74, 0x72, 0x75, 0x73, 0x74,
+                     0x2e, 0x6e, 0x65, 0x74 ];
+
+        assert_eq!(CAA::read(22, &mut Cursor::new(buf)).unwrap(),
+                   CAA {
+                       critical: false,
+                       tag: String::from("issuewild"),
+                       value: String::from("entrust.net"),
+                   });
+    }
+
+    #[test]
+    fn empty() {
+        assert_eq!(CAA::read(0, &mut Cursor::new(&[])),
+                   Err(WireError::IO));
+    }
+}

+ 48 - 0
dns/src/record/cname.rs

@@ -0,0 +1,48 @@
+use crate::strings::ReadLabels;
+use crate::wire::*;
+
+
+/// A **CNAME** _(canonical name)_ record, which aliases one domain to another.
+///
+/// # References
+///
+/// - [RFC 1035 §3.3.1](https://tools.ietf.org/html/rfc1035) — Domain Names, Implementation and Specification (November 1987)
+#[derive(PartialEq, Debug, Clone)]
+pub struct CNAME {
+
+    /// The domain name that this CNAME record is responding with.
+    pub domain: String,
+}
+
+impl Wire for CNAME {
+    const NAME: &'static str = "CNAME";
+    const RR_TYPE: u16 = 5;
+
+    fn read(_len: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
+        let domain = c.read_labels()?;
+        Ok(CNAME { domain })
+    }
+}
+
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn parses() {
+        let buf = &[ 0x05, 0x62, 0x73, 0x61, 0x67, 0x6f, 0x02, 0x6d, 0x65, 0x00, ];
+
+        assert_eq!(CNAME::read(10, &mut Cursor::new(buf)).unwrap(),
+                   CNAME {
+                       domain: String::from("bsago.me."),
+                   });
+    }
+
+    #[test]
+    fn empty() {
+        assert_eq!(CNAME::read(0, &mut Cursor::new(&[])),
+                   Err(WireError::IO));
+    }
+}
+

+ 87 - 0
dns/src/record/mod.rs

@@ -0,0 +1,87 @@
+//! All the DNS record types, as well as how to parse each type.
+
+
+mod a;
+pub use self::a::A;
+
+mod aaaa;
+pub use self::aaaa::AAAA;
+
+mod caa;
+pub use self::caa::CAA;
+
+mod cname;
+pub use self::cname::CNAME;
+
+mod mx;
+pub use self::mx::MX;
+
+mod ns;
+pub use self::ns::NS;
+
+mod opt;
+pub use self::opt::OPT;
+
+mod ptr;
+pub use self::ptr::PTR;
+
+mod soa;
+pub use self::soa::SOA;
+
+mod srv;
+pub use self::srv::SRV;
+
+mod txt;
+pub use self::txt::TXT;
+
+
+mod others;
+pub use self::others::{UnknownQtype, find_other_qtype_number};
+
+
+/// A record that’s been parsed from a byte buffer.
+#[derive(PartialEq, Debug, Clone)]
+pub enum Record {
+
+    /// An **A** record.
+    A(A),
+
+    /// An **AAAA** record.
+    AAAA(AAAA),
+
+    /// A **CAA** record.
+    CAA(CAA),
+
+    /// A **CNAME** record.
+    CNAME(CNAME),
+
+    /// A **MX** record.
+    MX(MX),
+
+    /// A **NS** record.
+    NS(NS),
+
+    // OPT is not included here.
+
+    /// A **PTR** record.
+    PTR(PTR),
+
+    /// A **SOA** record.
+    SOA(SOA),
+
+    /// A **SRV** record.
+    SRV(SRV),
+
+    /// A **TXT** record.
+    TXT(TXT),
+
+    /// A record with a type that we don’t recognise.
+    Other {
+
+        /// The number that’s meant to represent the record type.
+        type_number: UnknownQtype,
+
+        /// The undecodable bytes that were in this record.
+        bytes: Vec<u8>,
+    },
+}

+ 65 - 0
dns/src/record/mx.rs

@@ -0,0 +1,65 @@
+use crate::strings::ReadLabels;
+use crate::wire::*;
+
+use log::{warn, debug};
+
+
+/// An **MX** _(mail exchange)_ record, which contains the hostnames for mail
+/// servers that handle mail sent to the domain.
+///
+/// # References
+///
+/// - [RFC 1035 §3.3.s](https://tools.ietf.org/html/rfc1035) — Domain Names, Implementation and Specification (November 1987)
+#[derive(PartialEq, Debug, Clone)]
+pub struct MX {
+
+    /// The preference that clients should give to this MX record amongst all
+    /// that get returned.
+    pub preference: u16,
+
+    /// The domain name of the mail exchange server.
+    pub exchange: String,
+}
+
+impl Wire for MX {
+    const NAME: &'static str = "MX";
+    const RR_TYPE: u16 = 15;
+
+    fn read(len: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
+        let preference = c.read_u16::<BigEndian>()?;
+        let exchange = c.read_labels()?;
+
+        if 2 + exchange.len() + 1 != len as usize {
+            warn!("Expected length {} but read {} bytes", len, 2 + exchange.len() + 1);
+        }
+        else {
+            debug!("Length {} is correct", len);
+        }
+
+        Ok(MX { preference, exchange })
+    }
+}
+
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn parses() {
+        let buf = &[ 0x00, 0x0A, 0x05, 0x62, 0x73, 0x61, 0x67, 0x6f, 0x02,
+                     0x6d, 0x65, 0x00 ];
+
+        assert_eq!(MX::read(12, &mut Cursor::new(buf)).unwrap(),
+                   MX {
+                       preference: 10,
+                       exchange: String::from("bsago.me."),
+                   });
+    }
+
+    #[test]
+    fn empty() {
+        assert_eq!(MX::read(0, &mut Cursor::new(&[])),
+                   Err(WireError::IO));
+    }
+}

+ 60 - 0
dns/src/record/ns.rs

@@ -0,0 +1,60 @@
+use crate::strings::ReadLabels;
+use crate::wire::*;
+
+use log::{warn, debug};
+
+
+/// A **NS** _(name server)_ record, which is used to point domains to name
+/// servers.
+///
+/// # References
+///
+/// - [RFC 1035 §3.3.11](https://tools.ietf.org/html/rfc1035) — Domain Names, Implementation and Specification (November 1987)
+#[derive(PartialEq, Debug, Clone)]
+pub struct NS {
+
+    /// The address of a nameserver that provides this DNS response.
+    pub nameserver: String,
+}
+
+impl Wire for NS {
+    const NAME: &'static str = "NS";
+    const RR_TYPE: u16 = 2;
+
+    fn read(len: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
+        let nameserver = c.read_labels()?;
+
+        if nameserver.len() + 1 != len as usize {
+            warn!("Expected length {} but read {} bytes", len, nameserver.len() + 1);
+        }
+        else {
+            debug!("Length {} is correct", nameserver.len() + 1);
+        }
+
+        Ok(NS { nameserver })
+    }
+}
+
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn parses() {
+        let buf = &[ 0x01, 0x61, 0x0c, 0x67,
+                     0x74, 0x6c, 0x64, 0x2d, 0x73, 0x65, 0x72, 0x76,
+                     0x65, 0x72, 0x73, 0x03, 0x6e, 0x65, 0x74, 0x00, ];
+
+        assert_eq!(NS::read(20, &mut Cursor::new(buf)).unwrap(),
+                   NS {
+                       nameserver: String::from("a.gtld-servers.net."),
+                   });
+    }
+
+    #[test]
+    fn empty() {
+        assert_eq!(NS::read(0, &mut Cursor::new(&[])),
+                   Err(WireError::IO));
+    }
+}

+ 120 - 0
dns/src/record/opt.rs

@@ -0,0 +1,120 @@
+use std::io;
+
+use crate::wire::*;
+
+
+/// A **OPT** _(options)_ pseudo-record, which is used to extend the DNS
+/// protocol with additional flags such as DNSSEC stuff.
+///
+/// # Pseudo-record?
+///
+/// Unlike all the other record types, which are used to return data about a
+/// domain name, the OPT record type is used to add more options to the
+/// request, including data about the client or the server. It can exist, with
+/// a payload, as a query or a response, though it’s usually encountered in
+/// the Additional section. Its purpose is to add more room to the DNS wire
+/// format, as backwards compatibility makes it impossible to simply add more
+/// flags to the header.
+///
+/// The fact that this isn’t a standard record type is annoying for a DNS
+/// implementation. It re-purposes the ‘class’ and ‘TTL’ fields of the
+/// `Answer` struct, as they only have meaning when associated with a domain
+/// name. This means that the parser has to treat the OPT type specially,
+/// switching to `Opt::read` as soon as the rtype is detected. It also means
+/// the output has to deal with missing classes and TTLs.
+///
+/// # References
+///
+/// - [RFC 6891](https://tools.ietf.org/html/rfc6891) — Extension Mechanisms for DNS (April 2013)
+#[derive(PartialEq, Debug, Clone)]
+pub struct OPT {
+
+    /// The maximum size of a UDP packet that the client supports.
+    pub udp_payload_size: u16,
+
+    /// The bits that form an extended rcode when non-zero.
+    pub higher_bits: u8,
+
+    /// The version number of the DNS extension mechanism.
+    pub edns0_version: u8,
+
+    /// Sixteen bits worth of flags.
+    pub flags: u16,
+
+    /// The payload of the OPT record.
+    pub data: Vec<u8>,
+}
+
+impl OPT {
+
+    /// The record type number associated with OPT.
+    pub const RR_TYPE: u16 = 41;
+
+    /// Reads from the given cursor to parse an OPT record.
+    ///
+    /// The buffer will have slightly more bytes to read for an OPT record
+    /// than for a typical one: we will not have encountered the ‘class’ or
+    /// ‘ttl’ fields, which have different meanings for this record type.
+    /// See §6.1.3 of the RFC, “OPT Record TTL Field Use”.
+    ///
+    /// Unlike the `Wire::read` function, this does not require a length.
+    pub fn read(c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
+        let udp_payload_size = c.read_u16::<BigEndian>()?;  // replaces the class field
+        let higher_bits = c.read_u8()?;                     // replaces the ttl field...
+        let edns0_version = c.read_u8()?;                   // ...as does this...
+        let flags = c.read_u16::<BigEndian>()?;             // ...as does this
+
+        let data_length = c.read_u16::<BigEndian>()?;
+        let mut data = Vec::new();
+        for _ in 0 .. data_length {
+            data.push(c.read_u8()?);
+        }
+
+        Ok(OPT { udp_payload_size, higher_bits, edns0_version, flags, data })
+    }
+
+    /// Serialises this OPT record into a vector of bytes.
+    ///
+    /// This is necessary for OPT records to be sent in the Additional section
+    /// of requests.
+    pub fn to_bytes(&self) -> io::Result<Vec<u8>> {
+        let mut bytes = Vec::with_capacity(32);
+
+        bytes.write_u16::<BigEndian>(self.udp_payload_size)?;
+        bytes.write_u8(self.higher_bits)?;
+        bytes.write_u8(self.edns0_version)?;
+        bytes.write_u16::<BigEndian>(self.flags)?;
+        bytes.write_u16::<BigEndian>(self.data.len() as u16)?;
+        for b in &self.data {
+            bytes.write_u8(*b)?;
+        }
+
+        Ok(bytes)
+    }
+}
+
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn parses() {
+        let buf = &[ 0x05, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ];
+
+        assert_eq!(OPT::read(&mut Cursor::new(buf)).unwrap(),
+                   OPT {
+                       udp_payload_size: 1452,
+                       higher_bits: 0,
+                       edns0_version: 0,
+                       flags: 0,
+                       data: vec![],
+                   });
+    }
+
+    #[test]
+    fn empty() {
+        assert_eq!(OPT::read(&mut Cursor::new(&[])),
+                   Err(WireError::IO));
+    }
+}

+ 76 - 0
dns/src/record/others.rs

@@ -0,0 +1,76 @@
+use std::fmt;
+
+
+/// A number representing a record type dog can’t deal with.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum UnknownQtype {
+
+    /// An rtype number that dog is aware of, but does not know how to parse.
+    HeardOf(&'static str),
+
+    /// A completely unknown rtype number.
+    UnheardOf(u16),
+}
+
+impl From<u16> for UnknownQtype {
+    fn from(qtype: u16) -> Self {
+        match TYPES.iter().find(|t| t.1 == qtype) {
+            Some(tuple)  => Self::HeardOf(tuple.0),
+            None         => Self::UnheardOf(qtype),
+        }
+    }
+}
+
+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),
+        }
+    }
+}
+
+/// 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)] = &[
+    ("AFSDB",      18),
+    ("ANY",       255),
+    ("APL",        42),
+    ("AXFR",      252),
+    ("CDNSKEY",    60),
+    ("CDS",        59),
+    ("CERT",       37),
+    ("CSYNC",      62),
+    ("DHCID",      49),
+    ("DLV",     32769),
+    ("DNAME",      39),
+    ("DNSKEEYE",   48),
+    ("DS",         43),
+    ("HINFO",      13),
+    ("HIP",        55),
+    ("IPSECKEY",   45),
+    ("IXFR",      251),
+    ("KEY",        25),
+    ("KX",         36),
+    ("LOC",        29),
+    ("NAPTR",      35),
+    ("NSEC",       47),
+    ("NSEC3",      50),
+    ("NSEC3PARAM", 51),
+    ("OPENPGPKEY", 61),
+    ("RRSIG",      46),
+    ("RP",         17),
+    ("SIG",        24),
+    ("SMIMEA",     53),
+    ("SSHFP",      44),
+    ("TA",      32768),
+    ("TKEY",      249),
+    ("TLSA",       52),
+    ("TSIG",      250),
+    ("URI",       256),
+];

+ 54 - 0
dns/src/record/ptr.rs

@@ -0,0 +1,54 @@
+use crate::strings::ReadLabels;
+use crate::wire::*;
+
+
+/// A **PTR** record, which holds a _pointer_ to a canonical name. This is
+/// most often used for reverse DNS lookups.
+///
+/// # Encoding
+///
+/// The text encoding is not specified, but this crate treats it as UTF-8.
+/// Invalid bytes are turned into the replacement character.
+///
+/// # References
+///
+/// - [RFC 1035 §3.3.14](https://tools.ietf.org/html/rfc1035) — Domain Names, Implementation and Specification (November 1987)
+#[derive(PartialEq, Debug, Clone)]
+pub struct PTR {
+
+    /// The CNAME contained in the record.
+    pub cname: String,
+}
+
+impl Wire for PTR {
+    const NAME: &'static str = "PTR";
+    const RR_TYPE: u16 = 12;
+
+    fn read(_len: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
+        let cname = c.read_labels()?;
+        Ok(PTR { cname })
+    }
+}
+
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn parses() {
+        let buf = &[ 0x03, 0x64, 0x6e, 0x73, 0x06, 0x67,
+                     0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x00 ];
+
+        assert_eq!(PTR::read(12, &mut Cursor::new(buf)).unwrap(),
+                   PTR {
+                       cname: String::from("dns.google."),
+                   });
+    }
+
+    #[test]
+    fn empty() {
+        assert_eq!(PTR::read(0, &mut Cursor::new(&[])),
+                   Err(WireError::IO));
+    }
+}

+ 107 - 0
dns/src/record/soa.rs

@@ -0,0 +1,107 @@
+use crate::strings::ReadLabels;
+use crate::wire::*;
+
+use log::{warn, debug};
+
+
+/// A **SOA** _(start of authority)_ record, which contains administrative
+/// information about the zone the domain is in. These are returned when a
+/// server does not have a record for a domain.
+///
+/// # References
+///
+/// - [RFC 1035 §3.3.13](https://tools.ietf.org/html/rfc1035) — Domain Names, Implementation and Specification (November 1987)
+#[derive(PartialEq, Debug, Clone)]
+pub struct SOA {
+
+    /// The primary master name for this server.
+    pub mname: String,
+
+    /// The e-mail address of the administrator responsible for this DNS zone.
+    pub rname: String,
+
+    /// A serial number for this DNS zone.
+    pub serial: u32,
+
+    /// Duration, in seconds, after which secondary nameservers should query
+    /// the master for _its_ SOA record.
+    pub refresh_interval: u32,
+
+    /// Duration, in seconds, after which secondary nameservers should retry
+    /// requesting the serial number from the master if it does not respond.
+    /// It should be less than `refresh`.
+    pub retry_interval: u32,
+
+    /// Duration, in seconds, after which secondary nameservers should stop
+    /// answering requests for this zone if the master does not respond.
+    /// It should be greater than the sum of `refresh` and `retry`.
+    pub expire_limit: u32,
+
+    /// Duration, in seconds, of the minimum time-to-live.
+    pub minimum_ttl: u32,
+}
+
+impl Wire for SOA {
+    const NAME: &'static str = "SOA";
+    const RR_TYPE: u16 = 6;
+
+    fn read(len: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
+        let mname = c.read_labels()?;
+        let rname = c.read_labels()?;
+
+        let serial           = c.read_u32::<BigEndian>()?;
+        let refresh_interval = c.read_u32::<BigEndian>()?;
+        let retry_interval   = c.read_u32::<BigEndian>()?;
+        let expire_limit     = c.read_u32::<BigEndian>()?;
+        let minimum_ttl      = c.read_u32::<BigEndian>()?;
+
+        let got_length = mname.len() + rname.len() + 4 * 5 + 2;
+        if got_length != len as usize {
+            warn!("Expected length {} but got {}", len, got_length);
+        }
+        else {
+            debug!("Length {} is correct", len);
+        }
+
+        Ok(SOA {
+            mname, rname, serial, refresh_interval,
+            retry_interval, expire_limit, minimum_ttl,
+        })
+    }
+}
+
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn parses() {
+        let buf = &[
+            0x05, 0x62, 0x73, 0x61, 0x67, 0x6f, 0x02, 0x6d, 0x65, 0x00,
+            0x05, 0x62, 0x73, 0x61, 0x67, 0x6f, 0x02, 0x6d, 0x65, 0x00,
+            0x5d, 0x3c, 0xef, 0x02,
+            0x00, 0x01, 0x51, 0x80,
+            0x00, 0x00, 0x1c, 0x20,
+            0x00, 0x09, 0x3a, 0x80,
+            0x00, 0x00, 0x01, 0x2c,
+        ];
+
+        assert_eq!(SOA::read(40, &mut Cursor::new(buf)).unwrap(),
+                   SOA {
+                       mname: String::from("bsago.me."),
+                       rname: String::from("bsago.me."),
+                       serial: 1564274434,
+                       refresh_interval: 86400,
+                       retry_interval: 7200,
+                       expire_limit: 604800,
+                       minimum_ttl: 300,
+                   });
+    }
+
+    #[test]
+    fn empty() {
+        assert_eq!(SOA::read(0, &mut Cursor::new(&[])),
+                   Err(WireError::IO));
+    }
+}

+ 79 - 0
dns/src/record/srv.rs

@@ -0,0 +1,79 @@
+use crate::strings::ReadLabels;
+use crate::wire::*;
+
+use log::{debug, warn};
+
+
+/// A **SRV** record, which contains an IP address as well as a port number,
+/// for specifying the location of services more precisely.
+///
+/// # References
+///
+/// - [RFC 2782](https://tools.ietf.org/html/rfc2782) — A DNS RR for specifying the location of services (February 2000)
+#[derive(PartialEq, Debug, Clone)]
+pub struct SRV {
+
+    /// The priority of this host among all that get returned. Lower values
+    /// are higher priority.
+    pub priority: u16,
+
+    /// A weight to choose among results with the same priority. Higher values
+    /// are higher priority.
+    pub weight: u16,
+
+    /// The port the service is serving on.
+    pub port: u16,
+
+    /// The hostname of the machine the service is running on.
+    pub target: String,
+}
+
+impl Wire for SRV {
+    const NAME: &'static str = "SRV";
+    const RR_TYPE: u16 = 33;
+
+    fn read(len: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
+        let priority = c.read_u16::<BigEndian>()?;
+        let weight   = c.read_u16::<BigEndian>()?;
+        let port     = c.read_u16::<BigEndian>()?;
+        let target   = c.read_labels()?;
+
+        let got_length = 3 * 2 + target.len() + 1;
+        if got_length != len as usize {
+            warn!("Expected length {} but got {}", len, got_length);
+        }
+        else {
+            debug!("Length {} is correct", len);
+        }
+
+        Ok(SRV { priority, weight, port, target })
+    }
+}
+
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn parses() {
+        let buf = &[ 0x00, 0x01, 0x00, 0x01, 0x92, 0x7c, 0x03, 0x61, 0x74,
+                     0x61, 0x05, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x04, 0x6e,
+                     0x6f, 0x64, 0x65, 0x03, 0x64, 0x63, 0x31, 0x06, 0x63,
+                     0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x00, ];
+
+        assert_eq!(SRV::read(33, &mut Cursor::new(buf)).unwrap(),
+                   SRV {
+                       priority: 1,
+                       weight: 1,
+                       port: 37500,
+                       target: String::from("ata.local.node.dc1.consul."),
+                   });
+    }
+
+    #[test]
+    fn empty() {
+        assert_eq!(SRV::read(0, &mut Cursor::new(&[])),
+                   Err(WireError::IO));
+    }
+}

+ 79 - 0
dns/src/record/txt.rs

@@ -0,0 +1,79 @@
+use crate::wire::*;
+
+use log::*;
+
+
+/// A **TXT** record, which holds arbitrary descriptive text.
+///
+/// # Encoding
+///
+/// The text encoding is not specified, but this crate treats it as UTF-8.
+/// Invalid bytes are turned into the replacement character.
+///
+/// # References
+///
+/// - [RFC 1035 §3.3.14](https://tools.ietf.org/html/rfc1035) — Domain Names, Implementation and Specification (November 1987)
+#[derive(PartialEq, Debug, Clone)]
+pub struct TXT {
+
+    /// The message contained in the record.
+    pub message: String,
+}
+
+impl Wire for TXT {
+    const NAME: &'static str = "TXT";
+    const RR_TYPE: u16 = 16;
+
+    fn read(len: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
+        let mut buf = Vec::new();
+        let mut total_len = 0_usize;
+
+        loop {
+            let next_len = c.read_u8()?;
+            total_len += next_len as usize + 1;
+
+            for _ in 0 .. next_len {
+                buf.push(c.read_u8()?);
+            }
+
+            if next_len < 255 {
+                break;
+            }
+            else {
+                debug!("Got length 255 so looping");
+            }
+        }
+
+        if total_len == len as usize {
+            debug!("Length matches expected");
+        }
+        else {
+            warn!("Expected length {} but read {} bytes", len, buf.len());
+        }
+
+        let message = String::from_utf8_lossy(&buf).to_string();
+        Ok(TXT { message })
+    }
+}
+
+
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[test]
+    fn parses() {
+        let buf = &[ 0x06, 0x74, 0x78, 0x74, 0x20, 0x6d, 0x65 ];
+
+        assert_eq!(TXT::read(9, &mut Cursor::new(buf)).unwrap(),
+                   TXT {
+                       message: String::from("txt me"),
+                   });
+    }
+
+    #[test]
+    fn empty() {
+        assert_eq!(TXT::read(0, &mut Cursor::new(&[])),
+                   Err(WireError::IO));
+    }
+}

+ 103 - 0
dns/src/strings.rs

@@ -0,0 +1,103 @@
+//! Reading strings from the DNS wire protocol.
+
+use std::io::{self, Write};
+
+use log::debug;
+use byteorder::{ReadBytesExt, WriteBytesExt};
+
+use crate::wire::*;
+
+
+/// An extension for `Cursor` that enables reading compressed domain names
+/// from DNS packets.
+pub(crate) trait ReadLabels {
+
+    /// Read and expand a compressed domain name.
+    fn read_labels(&mut self) -> Result<String, WireError>;
+}
+
+impl ReadLabels for Cursor<&[u8]> {
+    fn read_labels(&mut self) -> Result<String, WireError> {
+        let mut name_buf = Vec::new();
+        read_string_recursive(&mut name_buf, self, &mut Vec::new())?;
+        Ok(String::from_utf8_lossy(&*name_buf).to_string())
+    }
+}
+
+
+/// An extension for `Write` that enables writing domain names.
+pub(crate) trait WriteLabels {
+
+    /// Write a domain name.
+    ///
+    /// The names being queried are written with one byte slice per
+    /// domain segment, preceded by each segment’s length, with the
+    /// whole thing ending with a segment of zero length.
+    ///
+    /// So “dns.lookup.dog” would be encoded as:
+    /// “3, dns, 6, lookup, 3, dog, 0”.
+    fn write_labels(&mut self, input: &str) -> io::Result<()>;
+}
+
+impl<W: Write> WriteLabels for W {
+    fn write_labels(&mut self, input: &str) -> io::Result<()> {
+        for label in input.split('.') {
+            self.write_u8(label.len() as u8)?;
+
+            for b in label.as_bytes() {
+                self.write_u8(*b)?;
+            }
+        }
+
+        self.write_u8(0)?;  // terminate the string
+        Ok(())
+    }
+}
+
+
+const RECURSION_LIMIT: usize = 8;
+
+fn read_string_recursive(name_buf: &mut Vec<u8>, c: &mut Cursor<&[u8]>, recursions: &mut Vec<u16>) -> Result<(), WireError> {
+    loop {
+        let byte = c.read_u8()?;
+
+        if byte == 0 {
+            break;
+        }
+
+        else if byte >= 0b_1100_0000 {
+            if recursions.len() >= RECURSION_LIMIT {
+                return Err(WireError::TooMuchRecursion(recursions.clone()));
+            }
+
+            let name_one = byte - 0b1100_0000;
+            let name_two = c.read_u8()?;
+            let offset = u16::from_be_bytes([name_one, name_two]);
+
+            debug!("Backtracking to offset {}", offset);
+            let new_pos = c.position();
+            c.set_position(u64::from(offset));
+            recursions.push(offset);
+
+            read_string_recursive(name_buf, c, recursions)?;
+
+            debug!("Coming back to {}", new_pos);
+            c.set_position(new_pos);
+            recursions.pop();
+            break;
+        }
+
+        // Otherwise, treat the byte as the length of a label, and read that
+        // many characters.
+        else {
+            for _ in 0 .. byte {
+                let c = c.read_u8()?;
+                name_buf.push(c);
+            }
+
+            name_buf.push(b'.');
+        }
+    }
+
+    Ok(())
+}

+ 193 - 0
dns/src/types.rs

@@ -0,0 +1,193 @@
+//! DNS packets are traditionally implemented with both the request and
+//! response packets at the same type. After all, both follow the same format,
+//! 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};
+
+
+/// A request that gets sent out over a transport.
+#[derive(PartialEq, Debug, Clone)]
+pub struct Request {
+
+    /// The transaction ID of this request. This is used to make sure
+    /// different DNS packets don’t answer each other’s questions.
+    pub transaction_id: u16,
+
+    /// The flags that accompany every DNS packet.
+    pub flags: Flags,
+
+    /// The queries that this request is making.
+    pub queries: Vec<Query>,
+
+    /// An additional record that may be sent as part of the query.
+    pub additional: Option<OPT>,
+}
+
+
+/// A response obtained from a DNS server.
+#[derive(PartialEq, Debug, Clone)]
+pub struct Response {
+
+    /// The transaction ID, which should match the ID of the request.
+    pub transaction_id: u16,
+
+    /// The flags that accompany every DNS packet.
+    pub flags: Flags,
+
+    /// The queries section.
+    pub queries: Vec<Query>,
+
+    /// The answers section.
+    pub answers: Vec<Answer>,
+
+    /// The authoritative nameservers section.
+    pub authorities: Vec<Answer>,
+
+    /// The additional records section.
+    pub additionals: Vec<Answer>,
+}
+
+
+/// A DNS query section.
+#[derive(PartialEq, Debug, Clone)]
+pub struct Query {
+
+    /// The domain name being queried, in human-readable dotted notation.
+    pub qname: String,
+
+    /// The class number.
+    pub qclass: QClass,
+
+    /// The type number.
+    pub qtype: TypeInt,
+}
+
+
+/// A DNS answer section.
+#[derive(PartialEq, Debug, Clone)]
+pub enum Answer {
+
+    /// This is a standard answer with every field.
+    Standard {
+
+        /// The domain name being answered for.
+        qname: String,
+
+        /// This answer’s class.
+        qclass: QClass,
+
+        /// The time-to-live duration, in seconds.
+        ttl: u32,
+
+        /// The record contained in this answer.
+        record: Record,
+    },
+
+    /// This is a pseudo-record answer, so some of the fields (class and TTL)
+    /// have different meaning.
+    Pseudo {
+
+        /// The domain name being answered for.
+        qname: String,
+
+        /// The OPT record contained in this answer.
+        opt: OPT,
+    },
+}
+
+
+/// A DNS record class. Of these, the only one that's in regular use anymore
+/// is the Internet class.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum QClass {
+
+    /// The **Internet** class.
+    IN,
+
+    /// The **Chaosnet** class.
+    CH,
+
+    /// The **Hesiod** class.
+    HS,
+
+    /// A class number that does not map to any known class.
+    Other(u16),
+}
+
+
+/// 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 {
+
+    /// Whether this packet is a response packet.
+    pub response: bool,
+
+    /// Number representing the operation being performed.
+    pub opcode: u8,
+
+    /// In a response, whether the server is providing authoritative DNS responses.
+    pub authoritative: bool,
+
+    /// In a response, whether this message has been truncated by the transport.
+    pub truncated: bool,
+
+    /// In a query, whether the server may query other nameservers recursively.
+    /// It is up to the server whether it will actually do this.
+    pub recursion_desired: bool,
+
+    /// In a response, whether the server allows recursive query support.
+    pub recursion_available: bool,
+
+    /// In a response, whether the server is marking this data as authentic.
+    pub authentic_data: bool,
+
+    /// In a request, whether the server should disable its authenticity
+    /// checking for the request’s queries.
+    pub checking_disabled: bool,
+
+    /// In a response, a code indicating an error if one occurred.
+    pub error_code: Option<ErrorCode>,
+}
+
+
+/// A code indicating an error.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum ErrorCode {
+
+    /// The server was unable to interpret the oquery.
+    FormatError,
+
+    /// There was a problem with the server.
+    ServerFailure,
+
+    /// The domain name referenced in the query does not exist.
+    NXDomain,
+
+    /// The server does not support one of the requested features.
+    NotImplemented,
+
+    /// The server was able to interpret the query, but refused to fulfil it.
+    QueryRefused,
+
+    /// The server did not accept the EDNS version, or failed to verify a
+    /// signature.
+    BadVersion,
+
+    /// An error code we don’t know what it is.
+    Other(u16),
+}
+
+
+impl Answer {
+
+    /// Whether this Answer holds a standard record, not a pseudo record.
+    pub fn is_standard(&self) -> bool {
+        matches!(self, Self::Standard { .. })
+    }
+}

+ 364 - 0
dns/src/wire.rs

@@ -0,0 +1,364 @@
+//! Parsing the DNS wire protocol.
+
+pub(crate) use std::io::Cursor;
+pub(crate) use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
+
+use std::io;
+use log::{error, info, debug};
+
+use crate::record::{Record, OPT};
+use crate::strings::{ReadLabels, WriteLabels};
+use crate::types::*;
+
+
+impl Request {
+
+    /// Converts this request to a vector of bytes.
+    pub fn to_bytes(&self) -> io::Result<Vec<u8>> {
+        let mut bytes = Vec::with_capacity(32);
+
+        bytes.write_u16::<BigEndian>(self.transaction_id)?;
+        bytes.write_u16::<BigEndian>(self.flags.to_u16())?;
+
+        bytes.write_u16::<BigEndian>(self.queries.len() as u16)?;
+        bytes.write_u16::<BigEndian>(0)?;  // usually answers
+        bytes.write_u16::<BigEndian>(0)?;  // usually authority RRs
+        bytes.write_u16::<BigEndian>(if self.additional.is_some() { 1 } else { 0 })?;  // additional RRs
+
+        for query in &self.queries {
+            bytes.write_labels(&query.qname)?;
+            bytes.write_u16::<BigEndian>(query.qtype)?;
+            bytes.write_u16::<BigEndian>(query.qclass.to_u16())?;
+        }
+
+        if let Some(opt) = &self.additional {
+            bytes.write_u8(0)?;  // usually a name
+            bytes.write_u16::<BigEndian>(OPT::RR_TYPE)?;
+            bytes.extend(opt.to_bytes()?);
+        }
+
+        Ok(bytes)
+    }
+
+    /// Returns the OPT record to be sent as part of requests.
+    pub fn additional_record() -> OPT {
+        OPT {
+            udp_payload_size: 512,
+            higher_bits: 0,
+            edns0_version: 0,
+            flags: 0,
+            data: Vec::new(),
+        }
+    }
+}
+
+
+impl Response {
+
+    /// Reads bytes off of the given slice, parsing them into a response.
+    pub fn from_bytes(bytes: &[u8]) -> Result<Self, WireError> {
+        debug!("Parsing bytes -> {:?}", bytes);
+
+        let mut c = Cursor::new(bytes);
+        let transaction_id = c.read_u16::<BigEndian>()?;
+        let flags = Flags::from_u16(c.read_u16::<BigEndian>()?);
+        debug!("Read flags: {:#?}", flags);
+
+        let query_count      = c.read_u16::<BigEndian>()?;
+        let answer_count     = c.read_u16::<BigEndian>()?;
+        let authority_count  = c.read_u16::<BigEndian>()?;
+        let additional_count = c.read_u16::<BigEndian>()?;
+
+        let mut queries = Vec::new();
+        debug!("Reading {}x query from response", query_count);
+        for _ in 0 .. query_count {
+            let qname = c.read_labels()?;
+            queries.push(Query::from_bytes(qname, &mut c)?);
+        }
+
+        let mut answers = Vec::new();
+        debug!("Reading {}x answer from response", answer_count);
+        for _ in 0 .. answer_count {
+            let qname = c.read_labels()?;
+            answers.push(Answer::from_bytes(qname, &mut c)?);
+        }
+
+        let mut authorities = Vec::new();
+        debug!("Reading {}x authority from response", authority_count);
+        for _ in 0 .. authority_count {
+            let qname = c.read_labels()?;
+            authorities.push(Answer::from_bytes(qname, &mut c)?);
+        }
+
+        let mut additionals = Vec::new();
+        debug!("Reading {}x additional answer from response", additional_count);
+        for _ in 0 .. additional_count {
+            let qname = c.read_labels()?;
+            additionals.push(Answer::from_bytes(qname, &mut c)?);
+        }
+
+        Ok(Response { transaction_id, flags, queries, answers, authorities, additionals })
+    }
+}
+
+
+impl Query {
+
+    /// Reads bytes from the given cursor, and parses them into a query with
+    /// the given domain name.
+    fn from_bytes(qname: String, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
+        let qtype = c.read_u16::<BigEndian>()?;
+        let qclass = QClass::from_u16(c.read_u16::<BigEndian>()?);
+
+        Ok(Query { qtype, qclass, qname })
+    }
+}
+
+
+impl Answer {
+
+    /// Reads bytes from the given cursor, and parses them into an answer with
+    /// the given domain name.
+    fn from_bytes(qname: String, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {
+        let qtype = c.read_u16::<BigEndian>()?;
+        if qtype == OPT::RR_TYPE {
+            let opt = OPT::read(c)?;
+            Ok(Answer::Pseudo { qname, opt })
+        }
+        else {
+            let qclass = QClass::from_u16(c.read_u16::<BigEndian>()?);
+            let ttl = c.read_u32::<BigEndian>()?;
+
+            let len = c.read_u16::<BigEndian>()?;
+            let record = Record::from_bytes(qtype, len, c)?;
+
+            Ok(Answer::Standard { qclass, qname, record, ttl })
+        }
+
+    }
+}
+
+
+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.
+    fn from_bytes(qtype: TypeInt, len: u16, c: &mut Cursor<&[u8]>) -> Result<Record, WireError> {
+        use crate::record::*;
+
+        macro_rules! try_record {
+            ($record:tt) => {
+                if $record::RR_TYPE == qtype {
+                    info!("Deciphering {} record (type {}, len {})", $record::NAME, qtype, len);
+                    return Wire::read(len, c).map(Record::$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!(MX);
+        try_record!(NS);
+        // OPT is handled separately
+        try_record!(PTR);
+        try_record!(SOA);
+        try_record!(SRV);
+        try_record!(TXT);
+
+        // 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()?);
+        }
+
+        let type_number = UnknownQtype::from(qtype);
+        Ok(Record::Other { type_number, bytes })
+    }
+}
+
+
+impl QClass {
+    fn from_u16(uu: u16) -> Self {
+        match uu {
+            0x0001 => QClass::IN,
+            0x0003 => QClass::CH,
+            0x0004 => QClass::HS,
+                 _ => QClass::Other(uu),
+        }
+    }
+
+    fn to_u16(self) -> u16 {
+        match self {
+            QClass::IN        => 0x0001,
+            QClass::CH        => 0x0003,
+            QClass::HS        => 0x0004,
+            QClass::Other(uu) => uu,
+        }
+    }
+}
+
+
+/// 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!(MX);
+    try_record!(NS);
+    // OPT is elsewhere
+    try_record!(PTR);
+    try_record!(SOA);
+    try_record!(SRV);
+    try_record!(TXT);
+
+    None
+}
+
+
+impl Flags {
+
+    /// The set of flags that represents a query packet.
+    pub fn query() -> Self {
+        Self::from_u16(0b_0000_0001_0000_0000)
+    }
+
+    /// Converts the flags into a two-byte number.
+    pub fn to_u16(self) -> u16 {                 // 0123 4567 89AB CDEF
+        let mut                          bits  = 0b_0000_0000_0000_0000;
+        if self.response               { bits += 0b_1000_0000_0000_0000; }
+        match self.opcode {
+                                _ =>   { bits += 0b_0000_0000_0000_0000; }
+        }
+        if self.authoritative          { bits += 0b_0000_0100_0000_0000; }
+        if self.truncated              { bits += 0b_0000_0010_0000_0000; }
+        if self.recursion_desired      { bits += 0b_0000_0001_0000_0000; }
+        if self.recursion_available    { bits += 0b_0000_0000_1000_0000; }
+        // (the Z bit is reserved)               0b_0000_0000_0100_0000
+        if self.authentic_data         { bits += 0b_0000_0000_0010_0000; }
+        if self.checking_disabled      { bits += 0b_0000_0000_0001_0000; }
+
+        bits
+    }
+
+    /// Extracts the flags from the given two-byte number.
+    pub fn from_u16(bits: u16) -> Self {
+        let has_bit = |bit| { bits & bit == bit };
+
+        Flags {
+            response:               has_bit(0b_1000_0000_0000_0000),
+            opcode:                 0,
+            authoritative:          has_bit(0b_0000_0100_0000_0000),
+            truncated:              has_bit(0b_0000_0010_0000_0000),
+            recursion_desired:      has_bit(0b_0000_0001_0000_0000),
+            recursion_available:    has_bit(0b_0000_0000_1000_0000),
+            authentic_data:         has_bit(0b_0000_0000_0010_0000),
+            checking_disabled:      has_bit(0b_0000_0000_0001_0000),
+            error_code:             ErrorCode::from_bits(bits & 0b_1111),
+        }
+    }
+}
+
+
+impl ErrorCode {
+
+    /// Extracts the rcode from the last four bits of the flags field.
+    fn from_bits(bits: u16) -> Option<Self> {
+        match bits {
+            0 => None,
+            1 => Some(Self::FormatError),
+            2 => Some(Self::ServerFailure),
+            3 => Some(Self::NXDomain),
+            4 => Some(Self::NotImplemented),
+            5 => Some(Self::QueryRefused),
+           16 => Some(Self::BadVersion),
+            n => Some(Self::Other(n)),
+        }
+    }
+}
+
+
+/// Trait for decoding DNS record structures from bytes read over the wire.
+pub trait Wire: Sized {
+
+    /// This record’s type as a string, such as `"A"` or `"CNAME"`.
+    const NAME: &'static str;
+
+    /// The number signifying that a record is of this type.
+    /// See <https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4>
+    const RR_TYPE: u16;
+
+    /// Read at most `len` bytes from the given `Cursor`. This cursor travels
+    /// throughout the complete data — by this point, we have read the entire
+    /// response into a buffer.
+    fn read(len: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError>;
+}
+
+
+/// 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 {
+
+    /// There was an IO error reading from the cursor.
+    /// Almost all the time, this means that the buffer was too short.
+    IO,
+    // (io::Error is not PartialEq so we don’t propagate it)
+
+    /// When this record expected the data to be a certain size, but it was
+    /// a different one.
+    WrongLength {
+
+        /// The expected size.
+        expected: u16,
+
+        /// The size that was actually received.
+        got: u16,
+    },
+
+    /// When the data contained a string containing a cycle of pointers.
+    /// Contains the vector of indexes that was being checked.
+    TooMuchRecursion(Vec<u16>),
+
+    /// When the data contained a string with a pointer to an index outside of
+    /// the packet. Contains the invalid index.
+    OutOfBounds(u16),
+}
+
+impl From<io::Error> for WireError {
+    fn from(ioe: io::Error) -> Self {
+        error!("IO error -> {:?}", ioe);
+        WireError::IO
+    }
+}

+ 7 - 0
dns/tests/wire_parsing_tests.rs

@@ -0,0 +1,7 @@
+use dns::Response;
+
+
+#[test]
+fn parse_nothing() {
+    assert!(Response::from_bytes(&[]).is_err());
+}

BIN
dog-screenshot.png


+ 63 - 0
src/colours.rs

@@ -0,0 +1,63 @@
+//! Colours, colour schemes, and terminal styling.
+
+use ansi_term::Style;
+use ansi_term::Color::*;
+
+
+/// The **colours** are used to paint the input.
+#[derive(Debug, Default)]
+pub struct Colours {
+    pub qname: Style,
+
+    pub answer: Style,
+    pub authority: Style,
+    pub additional: Style,
+
+    pub a: Style,
+    pub aaaa: Style,
+    pub caa: Style,
+    pub cname: Style,
+    pub mx: Style,
+    pub ns: Style,
+    pub opt: Style,
+    pub ptr: Style,
+    pub soa: Style,
+    pub srv: Style,
+    pub txt: Style,
+    pub unknown: Style,
+}
+
+impl Colours {
+
+    /// Create a new colour palette that has a variety of different styles
+    /// defined. This is used by default.
+    pub fn pretty() -> Self {
+        Self {
+            qname: Blue.bold(),
+
+            answer: Style::default(),
+            authority: Cyan.normal(),
+            additional: Green.normal(),
+
+            a: Green.bold(),
+            aaaa: Green.bold(),
+            caa: Red.normal(),
+            cname: Yellow.normal(),
+            mx: Cyan.normal(),
+            ns: Red.normal(),
+            opt: Purple.normal(),
+            ptr: Red.normal(),
+            soa: Purple.normal(),
+            srv: Cyan.normal(),
+            txt: Yellow.normal(),
+            unknown: White.on(Red),
+        }
+    }
+
+    /// Create a new colour palette where no styles are defined, causing
+    /// output to be rendered as plain text without any formatting.
+    /// This is used when output is not to a terminal.
+    pub fn plain() -> Self {
+        Self::default()
+    }
+}

+ 42 - 0
src/connect.rs

@@ -0,0 +1,42 @@
+use dns_transport::*;
+
+use crate::resolve::Nameserver;
+
+
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum TransportType {
+
+    /// Send packets over UDP or TCP.
+    /// UDP is used by default. If the request packet would be too large, send
+    /// a TCP packet instead; if a UDP _response_ packet is truncated, try
+    /// again with TCP.
+    Automatic,
+
+    /// Send packets over UDP only.
+    /// If the request packet is too large or the response packet is
+    /// truncated, fail with an error.
+    UDP,
+
+    /// Send packets over TCP only.
+    TCP,
+
+    /// Send encrypted DNS-over-TLS packets.
+    TLS,
+
+    /// Send encrypted DNS-over-HTTPS packets.
+    HTTPS,
+}
+
+impl TransportType {
+
+    /// Creates a boxed `Transport` depending on the transport type.
+    pub fn make_transport(self, ns: Nameserver) -> Box<dyn Transport> {
+        match self {
+            Self::Automatic  => Box::new(AutoTransport::new(ns)),
+            Self::UDP        => Box::new(UdpTransport::new(ns)),
+            Self::TCP        => Box::new(TcpTransport::new(ns)),
+            Self::TLS        => Box::new(TlsTransport::new(ns)),
+            Self::HTTPS      => Box::new(HttpsTransport::new(ns)),
+        }
+    }
+}

+ 175 - 0
src/main.rs

@@ -0,0 +1,175 @@
+//! dog, the command-line DNS client.
+
+#![warn(deprecated_in_future)]
+#![warn(future_incompatible)]
+#![warn(missing_copy_implementations)]
+#![warn(missing_docs)]
+#![warn(nonstandard_style)]
+#![warn(rust_2018_compatibility)]
+#![warn(rust_2018_idioms)]
+#![warn(single_use_lifetimes)]
+#![warn(trivial_casts, trivial_numeric_casts)]
+#![warn(unused)]
+
+#![deny(unsafe_code)]
+
+
+use std::env;
+use std::process::exit;
+use std::time::Instant;
+
+use log::*;
+
+mod colours;
+mod connect;
+mod output;
+mod requests;
+mod resolve;
+mod table;
+mod txid;
+
+mod options;
+use self::options::*;
+
+
+/// Configures logging, parses the command-line options, and handles any
+/// errors before passing control over to the Dog type.
+fn main() {
+    configure_logger();
+
+    match Options::getopts(env::args_os().skip(1)) {
+        OptionsResult::Ok(options) => {
+            info!("Running with options -> {:#?}", options);
+            let dog = Dog::init(options);
+            exit(dog.run());
+        }
+
+        OptionsResult::Help(help_reason, use_colours) => {
+            if use_colours.should_use_colours() {
+                print!("{}", include_str!(concat!(env!("OUT_DIR"), "/usage.pretty.txt")));
+            }
+            else {
+                print!("{}", include_str!(concat!(env!("OUT_DIR"), "/usage.bland.txt")));
+            }
+
+            if help_reason == HelpReason::NoDomains {
+                exit(exits::OPTIONS_ERROR);
+            }
+            else {
+                exit(exits::SUCCESS);
+            }
+        }
+
+        OptionsResult::Version(use_colours) => {
+            if use_colours.should_use_colours() {
+                print!("{}", include_str!(concat!(env!("OUT_DIR"), "/version.pretty.txt")));
+            }
+            else {
+                print!("{}", include_str!(concat!(env!("OUT_DIR"), "/version.bland.txt")));
+            }
+
+            exit(exits::SUCCESS);
+        }
+
+        OptionsResult::InvalidOptionsFormat(oe) => {
+            eprintln!("Invalid options: {:?}", oe);
+            exit(exits::OPTIONS_ERROR);
+        }
+
+        OptionsResult::InvalidOptions(why) => {
+            eprintln!("{}", why);
+            exit(exits::OPTIONS_ERROR);
+        }
+    }
+}
+
+
+/// Checks the `DOG_DEBUG` environment variable, enabling debug logging if
+/// it’s non-empty.
+fn configure_logger() {
+    let present = match env::var_os("DOG_DEBUG") {
+        Some(debug)  => debug.len() > 0,
+        None         => false,
+    };
+
+    let mut logs = env_logger::Builder::new();
+    if present {
+        let _ = logs.filter(None, log::LevelFilter::Debug);
+    }
+    else {
+        let _ = logs.filter(None, log::LevelFilter::Off);
+    }
+
+    logs.init()
+}
+
+struct Dog {
+    options: Options,
+}
+
+impl Dog {
+    fn init(options: Options) -> Self {
+        Self { options }
+    }
+
+    fn run(self) -> i32 {
+        let Options { requests, format, measure_time } = self.options;
+        let mut runtime = dns_transport::Runtime::new().expect("Failed to create runtime");
+        let should_show_opt = requests.edns.should_show();
+
+        let mut responses = Vec::new();
+        let timer = if measure_time { Some(Instant::now()) } else { None };
+
+        let mut errored = false;
+        for (request, transport) in requests.generate() {
+            let result = runtime.block_on(async { transport.send(&request).await });
+
+            match result {
+                Ok(mut response) => {
+                    if ! should_show_opt {
+                        response.answers.retain(dns::Answer::is_standard);
+                        response.authorities.retain(dns::Answer::is_standard);
+                        response.additionals.retain(dns::Answer::is_standard);
+                    }
+
+                    responses.push(response);
+                }
+                Err(e) => {
+                    format.print_error(e);
+                    errored = true;
+                }
+            }
+        }
+
+        let duration = timer.map(|t| t.elapsed());
+        if format.print(responses, duration) {
+            if errored {
+                exits::NETWORK_ERROR
+            }
+            else {
+                exits::SUCCESS
+            }
+        }
+        else {
+            exits::NO_SHORT_RESULTS
+        }
+    }
+}
+
+
+mod exits {
+    #![allow(unused)]
+
+    /// Exit code for when everything turns out OK.
+    pub const SUCCESS: i32 = 0;
+
+    /// Exit code for when there was at least one network error during execution.
+    pub const NETWORK_ERROR: i32 = 1;
+
+    /// Exit code for when there is no result from the server when running in
+    /// short mode. This can be any received server error, not just NXDOMAIN.
+    pub const NO_SHORT_RESULTS: i32 = 2;
+
+    /// Exit code for when the command-line options are invalid.
+    pub const OPTIONS_ERROR: i32 = 3;
+}

+ 654 - 0
src/options.rs

@@ -0,0 +1,654 @@
+use std::ffi::OsStr;
+use std::fmt;
+
+use log::*;
+
+use dns::{QClass, find_qtype_number, qtype};
+use dns::record::{A, find_other_qtype_number};
+
+use crate::connect::TransportType;
+use crate::output::{OutputFormat, UseColours, TextFormat};
+use crate::requests::{RequestGenerator, Inputs, ProtocolTweaks, UseEDNS};
+use crate::resolve::Resolver;
+use crate::txid::TxidGenerator;
+
+
+/// The command-line options used when running dog.
+#[derive(PartialEq, Debug)]
+pub struct Options {
+
+    /// The requests to make and how they should be generated.
+    pub requests: RequestGenerator,
+
+    /// Whether to display the time taken after every query.
+    pub measure_time: bool,
+
+    /// How to format the output data.
+    pub format: OutputFormat,
+}
+
+impl Options {
+
+    /// Parses and interprets a set of options from the user’s command-line
+    /// arguments.
+    ///
+    /// This returns an `Ok` set of options if successful and running
+    /// normally, a `Help` or `Version` variant if one of those options is
+    /// specified, or an error variant if there’s an invalid option or
+    /// inconsistency within the options after they were parsed.
+    #[allow(unused_results)]
+    pub fn getopts<C>(args: C) -> OptionsResult
+    where C: IntoIterator,
+          C::Item: AsRef<OsStr>,
+    {
+        let mut opts = getopts::Options::new();
+
+        // Query options
+        opts.optmulti("q", "query",       "Host name or IP address to query", "HOST");
+        opts.optmulti("t", "type",        "Type of the DNS record being queried (A, MX, NS...)", "TYPE");
+        opts.optmulti("n", "nameserver",  "Address of the nameserver to send packets to", "ADDR");
+        opts.optmulti("",  "class",       "Network class of the DNS record being queried (IN, CH, HS)", "CLASS");
+
+        // Sending options
+        opts.optopt ("",  "edns",         "Whether to OPT in to EDNS (disable, hide, show)", "SETTING");
+        opts.optopt ("",  "txid",         "Set the transaction ID to a specific value", "NUMBER");
+        opts.optopt ("Z", "",             "Uncommon protocol tweaks", "TWEAKS");
+
+        // Protocol options
+        opts.optflag("U", "udp",          "Use the DNS protocol over UDP");
+        opts.optflag("T", "tcp",          "Use the DNS protocol over TCP");
+        opts.optflag("S", "tls",          "Use the DNS-over-TLS protocol");
+        opts.optflag("H", "https",        "Use the DNS-over-HTTPS protocol");
+
+        // Output options
+        opts.optopt ("",  "color",        "When to use terminal colors",  "WHEN");
+        opts.optopt ("",  "colour",       "When to use terminal colours", "WHEN");
+        opts.optflag("J", "json",         "Display the output as JSON");
+        opts.optflag("",  "seconds",      "Do not format durations, display them as seconds");
+        opts.optflag("1", "short",        "Short mode: display nothing but the first result");
+        opts.optflag("",  "time",         "Print how long the response took to arrive");
+
+        // Meta options
+        opts.optflag("v", "version",      "Print version information");
+        opts.optflag("?", "help",         "Print list of command-line options");
+
+        let matches = match opts.parse(args) {
+            Ok(m)  => m,
+            Err(e) => return OptionsResult::InvalidOptionsFormat(e),
+        };
+
+        let uc = UseColours::deduce(&matches);
+
+        if matches.opt_present("version") {
+            OptionsResult::Version(uc)
+        }
+        else if matches.opt_present("help") {
+            OptionsResult::Help(HelpReason::Flag, uc)
+        }
+        else {
+            match Self::deduce(matches) {
+                Ok(opts) => {
+                    if opts.requests.inputs.domains.is_empty() {
+                        OptionsResult::Help(HelpReason::NoDomains, uc)
+                    }
+                    else {
+                        OptionsResult::Ok(opts)
+                    }
+                }
+                Err(e) => {
+                    OptionsResult::InvalidOptions(e)
+                }
+            }
+        }
+    }
+
+    fn deduce(matches: getopts::Matches) -> Result<Self, OptionsError> {
+        let measure_time = matches.opt_present("time");
+        let format = OutputFormat::deduce(&matches);
+        let requests = RequestGenerator::deduce(matches)?;
+
+        Ok(Self { requests, measure_time, format })
+    }
+}
+
+
+impl RequestGenerator {
+    fn deduce(matches: getopts::Matches) -> Result<Self, OptionsError> {
+        let edns = UseEDNS::deduce(&matches)?;
+        let txid_generator = TxidGenerator::deduce(&matches)?;
+        let protocol_tweaks = ProtocolTweaks::deduce(&matches)?;
+        let inputs = Inputs::deduce(matches)?;
+
+        Ok(Self { inputs, txid_generator, edns, protocol_tweaks })
+    }
+}
+
+
+impl Inputs {
+    fn deduce(matches: getopts::Matches) -> Result<Self, OptionsError> {
+        let mut inputs = Self::default();
+        inputs.load_transport_types(&matches);
+        inputs.load_named_args(&matches)?;
+        inputs.load_free_args(matches)?;
+        inputs.load_fallbacks();
+        Ok(inputs)
+    }
+
+    fn load_transport_types(&mut self, matches: &getopts::Matches) {
+        if matches.opt_present("https") {
+            self.transport_types.push(TransportType::HTTPS);
+        }
+
+        if matches.opt_present("tls") {
+            self.transport_types.push(TransportType::TLS);
+        }
+
+        if matches.opt_present("tcp") {
+            self.transport_types.push(TransportType::TCP);
+        }
+
+        if matches.opt_present("udp") {
+            self.transport_types.push(TransportType::UDP);
+        }
+    }
+
+    fn load_named_args(&mut self, matches: &getopts::Matches) -> Result<(), OptionsError> {
+        for domain in matches.opt_strs("query") {
+            self.domains.push(domain);
+        }
+
+        for qtype in matches.opt_strs("type") {
+            self.add_type(&qtype)?;
+        }
+
+        for ns in matches.opt_strs("nameserver") {
+            self.add_nameserver(&ns)?;
+        }
+
+        for qclass in matches.opt_strs("class") {
+            self.add_class(&qclass)?;
+        }
+
+        Ok(())
+    }
+
+    fn add_type(&mut self, input: &str) -> Result<(), OptionsError> {
+        if input == "OPT" {
+            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())),
+        }
+    }
+
+    fn add_nameserver(&mut self, input: &str) -> Result<(), OptionsError> {
+        self.resolvers.push(Resolver::Specified(input.into()));
+        Ok(())
+    }
+
+    fn parse_class_name(&self, input: &str) -> Option<QClass> {
+        match input {
+            "IN"  => Some(QClass::IN),
+            "CH"  => Some(QClass::CH),
+            "HS"  => Some(QClass::HS),
+            _     => None,
+        }
+    }
+
+    fn add_class(&mut self, input: &str) -> Result<(), OptionsError> {
+        let qclass = self.parse_class_name(input)
+            .or_else(|| input.parse().ok().map(QClass::Other));
+
+        match qclass {
+            Some(class)  => Ok(self.classes.push(class)),
+            None         => Err(OptionsError::InvalidQueryClass(input.into())),
+        }
+    }
+
+    fn load_free_args(&mut self, matches: getopts::Matches) -> Result<(), OptionsError> {
+        for a in matches.free {
+            if a.starts_with('@') {
+                trace!("Got nameserver -> {:?}", &a[1..]);
+                self.add_nameserver(&a[1..])?;
+            }
+            else if a.chars().all(char::is_uppercase) {
+                if let Some(class) = self.parse_class_name(&a) {
+                    trace!("Got qclass -> {:?}", &a);
+                    self.classes.push(class);
+                }
+                else {
+                    trace!("Got qtype -> {:?}", &a);
+                    self.add_type(&a)?;
+                }
+            }
+            else {
+                trace!("Got domain -> {:?}", &a);
+                self.domains.push(a);
+            }
+        }
+
+        Ok(())
+    }
+
+    fn load_fallbacks(&mut self) {
+        if self.types.is_empty() {
+            self.types.push(qtype!(A));
+        }
+
+        if self.classes.is_empty() {
+            self.classes.push(QClass::IN);
+        }
+
+        if self.resolvers.is_empty() {
+            self.resolvers.push(Resolver::SystemDefault);
+        }
+
+        if self.transport_types.is_empty() {
+            self.transport_types.push(TransportType::Automatic);
+        }
+    }
+}
+
+
+impl TxidGenerator {
+    fn deduce(matches: &getopts::Matches) -> Result<Self, OptionsError> {
+        if let Some(starting_txid) = matches.opt_str("txid") {
+            if let Ok(start) = starting_txid.parse() {
+                Ok(Self::Sequence(start))
+            }
+            else {
+                Err(OptionsError::InvalidTxid(starting_txid))
+            }
+        }
+        else {
+            Ok(Self::Random)
+        }
+    }
+}
+
+
+impl OutputFormat {
+    fn deduce(matches: &getopts::Matches) -> Self {
+        if matches.opt_present("short") {
+            let summary_format = TextFormat::deduce(matches);
+            Self::Short(summary_format)
+        }
+        else if matches.opt_present("json") {
+            Self::JSON
+        }
+        else {
+            let use_colours = UseColours::deduce(matches);
+            let summary_format = TextFormat::deduce(matches);
+            Self::Text(use_colours, summary_format)
+        }
+    }
+}
+
+
+impl UseColours {
+    fn deduce(matches: &getopts::Matches) -> Self {
+        match matches.opt_str("color").or_else(|| matches.opt_str("colour")).unwrap_or_default().as_str() {
+            "automatic" | "auto" | ""  => Self::Automatic,
+            "always"    | "yes"        => Self::Always,
+            "never"     | "no"         => Self::Never,
+            otherwise => {
+                warn!("Unknown colour setting {:?}", otherwise);
+                Self::Automatic
+            },
+        }
+    }
+}
+
+
+impl TextFormat {
+    fn deduce(matches: &getopts::Matches) -> Self {
+        let format_durations = ! matches.opt_present("seconds");
+        Self { format_durations }
+    }
+}
+
+
+impl UseEDNS {
+    fn deduce(matches: &getopts::Matches) -> Result<Self, OptionsError> {
+        if let Some(edns) = matches.opt_str("edns") {
+            match edns.as_str() {
+                "disable" | "off"  => Ok(Self::Disable),
+                "hide"             => Ok(Self::SendAndHide),
+                "show"             => Ok(Self::SendAndShow),
+                oh                 => Err(OptionsError::InvalidEDNS(oh.into())),
+            }
+        }
+        else {
+            Ok(Self::SendAndHide)
+        }
+    }
+}
+
+
+impl ProtocolTweaks {
+    fn deduce(matches: &getopts::Matches) -> Result<Self, OptionsError> {
+        let mut tweaks = Self::default();
+
+        if let Some(tweak_strs) = matches.opt_str("Z") {
+            for tweak_str in tweak_strs.split(',') {
+                match &*tweak_str {
+                    "authentic"  => { tweaks.set_authentic_flag = true; },
+                    otherwise    => return Err(OptionsError::InvalidTweak(otherwise.into())),
+                }
+            }
+        }
+
+        Ok(tweaks)
+    }
+}
+
+
+/// The result of the `Options::getopts` function.
+#[derive(PartialEq, Debug)]
+pub enum OptionsResult {
+
+    /// The options were parsed successfully.
+    Ok(Options),
+
+    /// There was an error (from `getopts`) parsing the arguments.
+    InvalidOptionsFormat(getopts::Fail),
+
+    /// There was an error with the combination of options the user selected.
+    InvalidOptions(OptionsError),
+
+    /// Can’t run any checks because there’s help to display!
+    Help(HelpReason, UseColours),
+
+    /// One of the arguments was `--version`, to display the version number.
+    Version(UseColours),
+}
+
+/// The reason that help is being displayed. If it’s for the `--help` flag,
+/// then we shouldn’t return an error exit status.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum HelpReason {
+
+    /// Help was requested with the `--help` flag.
+    Flag,
+
+    /// There were no domains being queried, so display help instead.
+    /// Unlike `dig`, we don’t implicitly search for the root domain.
+    NoDomains,
+}
+
+/// Something wrong with the combination of options the user has picked.
+#[derive(PartialEq, Debug)]
+pub enum OptionsError {
+    TooManyProtocols,
+    InvalidEDNS(String),
+    InvalidQueryType(String),
+    InvalidQueryClass(String),
+    InvalidTxid(String),
+    InvalidTweak(String),
+    QueryTypeOPT,
+}
+
+impl fmt::Display for OptionsError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::TooManyProtocols       => write!(f, "Too many protocols"),
+            Self::InvalidEDNS(edns)      => write!(f, "Invalid EDNS setting {:?}", edns),
+            Self::InvalidQueryType(qt)   => write!(f, "Invalid query type {:?}", qt),
+            Self::InvalidQueryClass(qc)  => write!(f, "Invalid query class {:?}", qc),
+            Self::InvalidTxid(txid)      => write!(f, "Invalid transaction ID {:?}", txid),
+            Self::InvalidTweak(tweak)    => write!(f, "Invalid protocol tweak {:?}", tweak),
+            Self::QueryTypeOPT           => write!(f, "OPT request is sent by default (see -Z flag)"),
+        }
+    }
+}
+
+
+#[cfg(test)]
+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) ],
+                classes:         vec![ QClass::IN ],
+                resolvers:       vec![ Resolver::SystemDefault ],
+                transport_types: vec![ TransportType::Automatic ],
+            }
+        }
+    }
+
+    impl OptionsResult {
+        fn unwrap(self) -> Options {
+            match self {
+                Self::Ok(o)  => o,
+                _            => panic!("{:?}", self),
+            }
+        }
+    }
+
+    // help tests
+
+    #[test]
+    fn help() {
+        assert_eq!(Options::getopts(&[ "--help" ]),
+                   OptionsResult::Help(HelpReason::Flag, UseColours::Automatic));
+    }
+
+    #[test]
+    fn help_no_colour() {
+        assert_eq!(Options::getopts(&[ "--help", "--colour=never" ]),
+                   OptionsResult::Help(HelpReason::Flag, UseColours::Never));
+    }
+
+    #[test]
+    fn version() {
+        assert_eq!(Options::getopts(&[ "--version" ]),
+                   OptionsResult::Version(UseColours::Automatic));
+    }
+
+    #[test]
+    fn version_yes_color() {
+        assert_eq!(Options::getopts(&[ "--version", "--color", "always" ]),
+                   OptionsResult::Version(UseColours::Always));
+    }
+
+    #[test]
+    fn fail() {
+        assert_eq!(Options::getopts(&[ "--pear" ]),
+                   OptionsResult::InvalidOptionsFormat(getopts::Fail::UnrecognizedOption("pear".into())));
+    }
+
+    #[test]
+    fn empty() {
+        let nothing: Vec<&str> = vec![];
+        assert_eq!(Options::getopts(nothing),
+                   OptionsResult::Help(HelpReason::NoDomains, UseColours::Automatic));
+    }
+
+    #[test]
+    fn an_unrelated_argument() {
+        assert_eq!(Options::getopts(&[ "--time" ]),
+                   OptionsResult::Help(HelpReason::NoDomains, UseColours::Automatic));
+    }
+
+    // query tests
+
+    #[test]
+    fn just_domain() {
+        let options = Options::getopts(&[ "lookup.dog" ]).unwrap();
+        assert_eq!(options.requests.inputs, Inputs {
+            domains:    vec![ String::from("lookup.dog") ],
+            .. Inputs::fallbacks()
+        });
+    }
+
+    #[test]
+    fn just_named_domain() {
+        let options = Options::getopts(&[ "-q", "lookup.dog" ]).unwrap();
+        assert_eq!(options.requests.inputs, Inputs {
+            domains:    vec![ String::from("lookup.dog") ],
+            .. Inputs::fallbacks()
+        });
+    }
+
+    #[test]
+    fn domain_and_type() {
+        let options = Options::getopts(&[ "lookup.dog", "SOA" ]).unwrap();
+        assert_eq!(options.requests.inputs, Inputs {
+            domains:    vec![ String::from("lookup.dog") ],
+            types:      vec![ qtype!(SOA) ],
+            .. Inputs::fallbacks()
+        });
+    }
+
+    #[test]
+    fn domain_and_nameserver() {
+        let options = Options::getopts(&[ "lookup.dog", "@1.1.1.1" ]).unwrap();
+        assert_eq!(options.requests.inputs, Inputs {
+            domains:    vec![ String::from("lookup.dog") ],
+            resolvers:  vec![ Resolver::Specified("1.1.1.1".into()) ],
+            .. Inputs::fallbacks()
+        });
+    }
+
+    #[test]
+    fn domain_and_class() {
+        let options = Options::getopts(&[ "lookup.dog", "CH" ]).unwrap();
+        assert_eq!(options.requests.inputs, Inputs {
+            domains:    vec![ String::from("lookup.dog") ],
+            classes:    vec![ QClass::CH ],
+            .. Inputs::fallbacks()
+        });
+    }
+
+    #[test]
+    fn all_free() {
+        let options = Options::getopts(&[ "lookup.dog", "CH", "NS", "@1.1.1.1" ]).unwrap();
+        assert_eq!(options.requests.inputs, Inputs {
+            domains:    vec![ String::from("lookup.dog") ],
+            classes:    vec![ QClass::CH ],
+            types:      vec![ qtype!(NS) ],
+            resolvers:  vec![ Resolver::Specified("1.1.1.1".into()) ],
+            .. Inputs::fallbacks()
+        });
+    }
+
+    #[test]
+    fn all_parameters() {
+        let options = Options::getopts(&[ "-q", "lookup.dog", "--class", "CH", "--type", "SOA", "--nameserver", "1.1.1.1" ]).unwrap();
+        assert_eq!(options.requests.inputs, Inputs {
+            domains:    vec![ String::from("lookup.dog") ],
+            classes:    vec![ QClass::CH ],
+            types:      vec![ qtype!(SOA) ],
+            resolvers:  vec![ Resolver::Specified("1.1.1.1".into()) ],
+            .. Inputs::fallbacks()
+        });
+    }
+
+    #[test]
+    fn two_types() {
+        let options = Options::getopts(&[ "-q", "lookup.dog", "--type", "SRV", "--type", "AAAA" ]).unwrap();
+        assert_eq!(options.requests.inputs, Inputs {
+            domains:    vec![ String::from("lookup.dog") ],
+            types:      vec![ qtype!(SRV), qtype!(AAAA) ],
+            .. Inputs::fallbacks()
+        });
+    }
+
+    #[test]
+    fn two_classes() {
+        let options = Options::getopts(&[ "-q", "lookup.dog", "--class", "IN", "--class", "CH" ]).unwrap();
+        assert_eq!(options.requests.inputs, Inputs {
+            domains:    vec![ String::from("lookup.dog") ],
+            classes:    vec![ QClass::IN, QClass::CH ],
+            .. Inputs::fallbacks()
+        });
+    }
+
+    #[test]
+    fn all_mixed_1() {
+        let options = Options::getopts(&[ "lookup.dog", "--class", "CH", "SOA", "--nameserver", "1.1.1.1" ]).unwrap();
+        assert_eq!(options.requests.inputs, Inputs {
+            domains:    vec![ String::from("lookup.dog") ],
+            classes:    vec![ QClass::CH ],
+            types:      vec![ qtype!(SOA) ],
+            resolvers:  vec![ Resolver::Specified("1.1.1.1".into()) ],
+            .. Inputs::fallbacks()
+        });
+    }
+
+    #[test]
+    fn all_mixed_2() {
+        let options = Options::getopts(&[ "CH", "SOA", "MX", "IN", "-q", "lookup.dog", "--class", "HS" ]).unwrap();
+        assert_eq!(options.requests.inputs, Inputs {
+            domains:    vec![ String::from("lookup.dog") ],
+            classes:    vec![ QClass::HS, QClass::CH, QClass::IN ],
+            types:      vec![ qtype!(SOA), qtype!(MX) ],
+            .. Inputs::fallbacks()
+        });
+    }
+
+    #[test]
+    fn all_mixed_3() {
+        let options = Options::getopts(&[ "lookup.dog", "--nameserver", "1.1.1.1", "--nameserver", "1.0.0.1" ]).unwrap();
+        assert_eq!(options.requests.inputs, Inputs {
+            domains:    vec![ String::from("lookup.dog") ],
+            resolvers:  vec![ Resolver::Specified("1.1.1.1".into()),
+                              Resolver::Specified("1.0.0.1".into()), ],
+            .. Inputs::fallbacks()
+        });
+    }
+
+    #[test]
+    fn explicit_numerics() {
+        let options = Options::getopts(&[ "11", "--class", "22", "--type", "33" ]).unwrap();
+        assert_eq!(options.requests.inputs, Inputs {
+            domains:    vec![ String::from("11") ],
+            classes:    vec![ QClass::Other(22) ],
+            types:      vec![ 33 ],
+            .. Inputs::fallbacks()
+        });
+    }
+
+    // invalid options tests
+
+    #[test]
+    fn invalid_named_class() {
+        assert_eq!(Options::getopts(&[ "lookup.dog", "--class", "tubes" ]),
+                   OptionsResult::InvalidOptions(OptionsError::InvalidQueryClass("tubes".into())));
+    }
+
+    #[test]
+    fn invalid_named_type() {
+        assert_eq!(Options::getopts(&[ "lookup.dog", "--type", "tubes" ]),
+                   OptionsResult::InvalidOptions(OptionsError::InvalidQueryType("tubes".into())));
+    }
+
+    #[test]
+    fn invalid_capsword() {
+        assert_eq!(Options::getopts(&[ "SMH", "lookup.dog" ]),
+                   OptionsResult::InvalidOptions(OptionsError::InvalidQueryType("SMH".into())));
+    }
+
+    #[test]
+    fn invalid_txid() {
+        assert_eq!(Options::getopts(&[ "lookup.dog", "--txid=0x1234" ]),
+                   OptionsResult::InvalidOptions(OptionsError::InvalidTxid("0x1234".into())));
+    }
+
+    #[test]
+    fn opt() {
+        assert_eq!(Options::getopts(&[ "OPT", "lookup.dog" ]),
+                   OptionsResult::InvalidOptions(OptionsError::QueryTypeOPT));
+    }
+}

+ 345 - 0
src/output.rs

@@ -0,0 +1,345 @@
+//! Text and JSON output.
+
+use std::time::Duration;
+
+use dns::{Response, Query, Answer, ErrorCode, WireError};
+use dns::record::{Record, OPT, UnknownQtype};
+use dns_transport::Error as TransportError;
+use serde_json::{json, Value as JsonValue};
+
+use crate::colours::Colours;
+use crate::table::{Table, Section};
+
+
+/// How to format the output data.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum OutputFormat {
+
+    /// Format the output as plain text, optionally adding ANSI colours.
+    Text(UseColours, TextFormat),
+
+    /// Format the output as one line of plain text.
+    Short(TextFormat),
+
+    /// Format the entries as JSON.
+    JSON,
+}
+
+
+/// When to use colours in the output.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum UseColours {
+
+    /// Always use colours.
+    Always,
+
+    /// Use colours if output is to a terminal; otherwise, do not.
+    Automatic,
+
+    /// Never use colours.
+    Never,
+}
+
+/// Options that govern how text should be rendered in record summaries.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub struct TextFormat {
+
+    /// Whether to format TTLs as hours, minutes, and seconds.
+    pub format_durations: bool,
+}
+
+impl UseColours {
+
+    /// Whether we should use colours or not. This checks whether the user has
+    /// overridden the colour setting, and if not, whether output is to a
+    /// terminal.
+    pub fn should_use_colours(self) -> bool {
+        self == Self::Always || (atty::is(atty::Stream::Stdout) && self != Self::Never)
+    }
+
+    /// Creates a palette of colours depending on the user’s wishes or whether
+    /// output is to a terminal.
+    pub fn palette(self) -> Colours {
+        if self.should_use_colours() {
+            Colours::pretty()
+        }
+        else {
+            Colours::plain()
+        }
+    }
+}
+
+
+impl OutputFormat {
+    pub fn print(self, responses: Vec<Response>, duration: Option<Duration>) -> bool {
+        match self {
+            Self::Short(tf) => {
+                let all_answers = responses.into_iter().flat_map(|r| r.answers).collect::<Vec<_>>();
+
+                if all_answers.is_empty() {
+                    eprintln!("No results");
+                    return false;
+                }
+
+                for answer in all_answers {
+                    match answer {
+                        Answer::Standard { record, .. } => {
+                            println!("{}", tf.record_payload_summary(&record))
+                        }
+                        Answer::Pseudo { opt, .. } => {
+                            println!("{}", tf.pseudo_record_payload_summary(&opt))
+                        }
+                    }
+
+                }
+            }
+            Self::JSON => {
+                let mut rs = Vec::new();
+
+                for response in responses {
+                    let json = json!({
+                        "queries": self.json_queries(&response.queries),
+                        "answers": self.json_answers(&response.answers),
+                        "authorities": self.json_answers(&response.authorities),
+                        "additionals": self.json_answers(&response.additionals),
+                    });
+
+                    rs.push(json);
+                }
+
+                if let Some(duration) = duration {
+                    let object = json!({ "responses": rs, "duration": duration });
+                    println!("{}", object);
+                }
+                else {
+                    let object = json!({ "responses": rs });
+                    println!("{}", object);
+                }
+            }
+            Self::Text(uc, tf) => {
+                let mut table = Table::new(uc.palette(), tf);
+
+                for response in responses {
+                    if let Some(rcode) = response.flags.error_code {
+                        print_error_code(rcode);
+                    }
+
+                    for a in response.answers {
+                        table.add_row(a, Section::Answer);
+                    }
+
+                    for a in response.authorities {
+                        table.add_row(a, Section::Authority);
+                    }
+
+                    for a in response.additionals {
+                        table.add_row(a, Section::Additional);
+                    }
+                }
+
+                table.print(duration);
+            }
+        }
+
+        true
+    }
+
+    pub fn print_error(&self, error: TransportError) {
+    	match self {
+    		Self::Short(..) | Self::Text(..) => {
+    			eprintln!("Error [{}]: {}", erroneous_phase(&error), error_message(error));
+    		}
+
+    		Self::JSON => {
+    			let object = json!({
+    				"error": true,
+    				"error_phase": erroneous_phase(&error),
+    				"error_message": error_message(error),
+    			});
+
+    			eprintln!("{}", object);
+    		}
+    	}
+    }
+}
+
+fn erroneous_phase(error: &TransportError) -> &'static str {
+	match error {
+		TransportError::NetworkError(_)  => "network",
+		TransportError::HttpError(_)     => "http",
+		TransportError::TlsError(_)      => "tls",
+		TransportError::BadRequest       => "http-status",
+		TransportError::WireError(_)     => "protocol",
+	}
+}
+
+fn error_message(error: TransportError) -> String {
+	match error {
+		TransportError::NetworkError(e)  => e.to_string(),
+		TransportError::HttpError(e)     => e.to_string(),
+		TransportError::TlsError(e)      => e.to_string(),
+		TransportError::BadRequest       => "Nameserver returned HTTP 400 Bad Request".into(),
+		TransportError::WireError(e)     => {
+			match e {
+				WireError::IO                             => "Malformed packet: insufficient data".into(),
+				WireError::WrongLength { expected, got }  => format!("Malformed packet: expected length {}, got {}", expected, got),
+				WireError::TooMuchRecursion(indices)      => format!("Malformed packet: too much recursion: {:?}", indices),
+				WireError::OutOfBounds(index)             => format!("Malformed packet: out of bounds ({})", index),
+			}
+		}
+	}
+}
+
+
+impl TextFormat {
+    pub fn record_payload_summary(self, record: &Record) -> String {
+        match *record {
+            Record::A(ref a) => {
+                format!("{}", a.address)
+            }
+            Record::AAAA(ref aaaa) => {
+                format!("{}", aaaa.address)
+            }
+            Record::CAA(ref caa) => {
+                if caa.critical {
+                    format!("{:?} {:?} (critical)", caa.tag, caa.value)
+                }
+                else {
+                    format!("{:?} {:?} (non-critical)", caa.tag, caa.value)
+                }
+            }
+            Record::CNAME(ref cname) => {
+                format!("{:?}", cname.domain)
+            }
+            Record::MX(ref mx) => {
+                format!("{} {:?}", mx.preference, mx.exchange)
+            }
+            Record::NS(ref ns) => {
+                format!("{:?}", ns.nameserver)
+            }
+            Record::PTR(ref ptr) => {
+                format!("{:?}", ptr.cname)
+            }
+            Record::SOA(ref soa) => {
+                format!("{:?} {:?} {} {} {} {} {}",
+                    soa.mname, soa.rname, soa.serial,
+                    self.format_duration(soa.refresh_interval),
+                    self.format_duration(soa.retry_interval),
+                    self.format_duration(soa.expire_limit),
+                    self.format_duration(soa.minimum_ttl),
+                )
+            }
+            Record::SRV(ref srv) => {
+                format!("{} {} {:?}:{}", srv.priority, srv.weight, srv.target, srv.port)
+            }
+            Record::TXT(ref txt) => {
+                format!("{:?}", txt.message)
+            }
+            Record::Other { ref bytes, .. } => {
+                format!("{:?}", bytes)
+            }
+        }
+    }
+
+    pub fn pseudo_record_payload_summary(self, opt: &OPT) -> String {
+        format!("{} {} {} {} {:?}",
+            opt.udp_payload_size,
+            opt.higher_bits,
+            opt.edns0_version,
+            opt.flags,
+            opt.data)
+    }
+
+    pub fn format_duration(self, seconds: u32) -> String {
+        if ! self.format_durations {
+            format!("{}", seconds)
+        }
+        else if seconds < 60 {
+            format!("{}s", seconds)
+        }
+        else if seconds < 60 * 60 {
+            format!("{}m{:02}s", seconds / 60, seconds % 60)
+        }
+        else if seconds < 60 * 60 * 24{
+            format!("{}h{:02}m{:02}s", seconds / 3600, (seconds % 3600) / 60, seconds % 60)
+        }
+        else {
+            format!("{}d{}h{:02}m{:02}s", seconds / 86400, (seconds % 86400) / 3600, (seconds % 3600) / 60, seconds % 60)
+        }
+    }
+}
+
+impl OutputFormat {
+    fn json_queries(&self, queries: &[Query]) -> JsonValue {
+        let queries = queries.iter().map(|q| {
+            json!({
+                "name": q.qname,
+                "class": format!("{:?}", q.qclass),
+                "type": q.qtype,
+            })
+        }).collect::<Vec<_>>();
+
+        json!(queries)
+    }
+
+    fn json_answers(&self, answers: &[Answer]) -> JsonValue {
+        let answers = answers.iter().map(|a| {
+            match a {
+                Answer::Standard { qname, qclass, ttl, record } => {
+                    let mut object = self.json_record(record);
+                    let omut = object.as_object_mut().unwrap();
+                    omut.insert("name".into(), qname.as_str().into());
+                    omut.insert("class".into(), format!("{:?}", qclass).into());
+                    omut.insert("ttl".into(), (*ttl).into());
+                    json!(object)
+                }
+                Answer::Pseudo { qname, opt } => {
+                    let object = json!({
+                        "name": qname,
+                        "type": "OPT",
+                        "version": opt.edns0_version,
+                        "data": opt.data,
+                    });
+
+                    object
+                }
+            }
+        }).collect::<Vec<_>>();
+
+        json!(answers)
+    }
+
+    fn json_record(&self, record: &Record) -> JsonValue {
+        match record {
+            Record::A(rec)      => json!({ "type": "A",     "address": rec.address.to_string() }),
+            Record::AAAA(rec)   => json!({ "type": "AAAA",  "address": rec.address.to_string() }),
+            Record::CAA(rec)    => json!({ "type": "CAA",   "critical": rec.critical, "tag": rec.tag, "value": rec.value }),
+            Record::CNAME(rec)  => json!({ "type": "CNAME", "domain": rec.domain.to_string() }),
+            Record::MX(rec)     => json!({ "type": "MX",    "preference": rec.preference, "exchange": rec.exchange }),
+            Record::NS(rec)     => json!({ "type": "NS",    "nameserver": rec.nameserver }),
+            Record::PTR(rec)    => json!({ "type": "PTR",   "cname": rec.cname }),
+            Record::SOA(rec)    => json!({ "type": "SOA",   "mname": rec.mname }),
+            Record::SRV(rec)    => json!({ "type": "SRV",   "priority": rec.priority, "weight": rec.weight, "port": rec.port, "target": rec.target, }),
+            Record::TXT(rec)    => json!({ "type": "TXT",   "message": rec.message }),
+            Record::Other { type_number, bytes } => {
+                let type_name = match type_number {
+                    UnknownQtype::HeardOf(name) => json!(name),
+                    UnknownQtype::UnheardOf(num) => json!(num),
+                };
+                json!({ "unknown": true, "type": type_name, "bytes": bytes })
+            }
+        }
+    }
+}
+
+pub fn print_error_code(rcode: ErrorCode) {
+    match rcode {
+        ErrorCode::FormatError     => println!("Status: Format Error"),
+        ErrorCode::ServerFailure   => println!("Status: Server Failure"),
+        ErrorCode::NXDomain        => println!("Status: NXDomain"),
+        ErrorCode::NotImplemented  => println!("Status: Not Implemented"),
+        ErrorCode::QueryRefused    => println!("Status: Query Refused"),
+        ErrorCode::BadVersion      => println!("Status: Bad Version"),
+        ErrorCode::Other(num)      => println!("Status: Other Failure ({})", num),
+    }
+}

+ 125 - 0
src/requests.rs

@@ -0,0 +1,125 @@
+use crate::connect::TransportType;
+use crate::resolve::Resolver;
+use crate::txid::TxidGenerator;
+
+
+/// All the information necessary to generate requests for one or more
+/// queries, nameservers, or transport types.
+#[derive(PartialEq, Debug)]
+pub struct RequestGenerator {
+
+    /// The input parameter matrix.
+    pub inputs: Inputs,
+
+    /// How to generate transaction IDs.
+    pub txid_generator: TxidGenerator,
+
+    /// Whether to OPT in to DNS extensions.
+    pub edns: UseEDNS,
+
+    /// Other weird protocol options.
+    pub protocol_tweaks: ProtocolTweaks,
+}
+
+/// Which things the user has specified they want queried.
+#[derive(PartialEq, Debug, Default)]
+pub struct Inputs {
+
+    /// The list of domain names to query.
+    pub domains: Vec<String>,
+
+    /// The list of DNS record types to query for.
+    pub types: Vec<u16>,
+
+    /// The list of DNS classes to query for.
+    pub classes: Vec<dns::QClass>,
+
+    /// The list of resolvers to send queries to.
+    pub resolvers: Vec<Resolver>,
+
+    /// The list of transport types to send queries over.
+    pub transport_types: Vec<TransportType>,
+}
+
+/// Weird protocol options that are allowed by the spec but are not common.
+#[derive(PartialEq, Debug, Default)]
+pub struct ProtocolTweaks {
+
+    /// Set the `AD` flag (Authentic Data) in the header of each request.
+    pub set_authentic_flag: bool,
+}
+
+/// Whether to send or display OPT packets.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum UseEDNS {
+
+    /// Do not send an OPT query in requests, and do not display them.
+    Disable,
+
+    /// Send an OPT query in requests, but hide the result. This is the
+    /// default, because the information is usually not useful to the user.
+    SendAndHide,
+
+    /// Send an OPT query in requests, _and_ display any OPT records in the
+    /// response we receive.
+    SendAndShow,
+}
+
+
+impl RequestGenerator {
+
+    /// Iterate through the inputs matrix, returning pairs of DNS requests and
+    /// the details of the transport to send them down.
+    pub fn generate(self) -> Vec<(dns::Request, Box<dyn dns_transport::Transport>)> {
+        let nameservers = self.inputs.resolvers.into_iter()
+                              .map(|e| e.lookup().expect("Failed to get nameserver").expect("No nameserver found"))
+                              .collect::<Vec<_>>();
+
+        let mut requests = Vec::new();
+        for domain in &self.inputs.domains {
+            for qtype in self.inputs.types.iter().copied() {
+                for qclass in self.inputs.classes.iter().copied() {
+                    for nameserver in &nameservers {
+                        for transport_type in &self.inputs.transport_types {
+
+                            let transaction_id = self.txid_generator.generate();
+                            let mut flags = dns::Flags::query();
+                            if self.protocol_tweaks.set_authentic_flag {
+                                flags.authentic_data = true;
+                            }
+
+                            let mut additional = None;
+                            if self.edns.should_send() {
+                                additional = Some(dns::Request::additional_record());
+                            }
+
+                            let queries = vec![
+                                dns::Query { qname: domain.clone(), qtype, qclass },
+                            ];
+
+                            let request = dns::Request { transaction_id, flags, queries, additional };
+
+                            let transport = transport_type.make_transport(nameserver.clone());
+                            requests.push((request, transport));
+                        }
+                    }
+                }
+            }
+        }
+
+        requests
+    }
+}
+
+impl UseEDNS {
+
+    /// Whether the user wants to send OPT records.
+    pub fn should_send(self) -> bool {
+        self != Self::Disable
+    }
+
+    /// Whether the user wants to display sent OPT records.
+    pub fn should_show(self) -> bool {
+        self == Self::SendAndShow
+    }
+}

+ 54 - 0
src/resolve.rs

@@ -0,0 +1,54 @@
+use std::io;
+
+use log::*;
+
+
+/// A **resolver** is used to obtain the IP address of the server we should
+/// send DNS requests to.
+#[derive(PartialEq, Debug)]
+pub enum Resolver {
+
+    /// Read the list of nameservers from the system, and use that.
+    SystemDefault,
+
+    // Use a resolver specified by the user.
+    Specified(Nameserver),
+}
+
+pub type Nameserver = String;
+
+
+impl Resolver {
+    pub fn lookup(self) -> io::Result<Option<Nameserver>> {
+        match self {
+            Self::Specified(ns) => {
+                Ok(Some(ns))
+            }
+
+            Self::SystemDefault => {
+                use std::io::{BufRead, BufReader};
+                use std::fs::File;
+
+                let f = File::open("/etc/resolv.conf")?;
+                let reader = BufReader::new(f);
+
+                let mut nameservers = Vec::new();
+                for line in reader.lines() {
+                    let line = line?;
+
+                    if line.starts_with("nameserver ") {
+                        let line = &line[11..];
+                        let ip: Result<std::net::Ipv4Addr, _> = line.parse();
+
+                        match ip {
+                            Ok(_ip) => nameservers.push(line.into()),
+                            Err(e)  => warn!("Failed to parse nameserver line {:?}: {}", line, e),
+                        }
+                    }
+                }
+
+                Ok(nameservers.first().cloned())
+            }
+        }
+    }
+}

+ 152 - 0
src/table.rs

@@ -0,0 +1,152 @@
+//! Tables of DNS response results.
+
+use std::time::Duration;
+
+use ansi_term::ANSIString;
+
+use dns::Answer;
+use dns::record::Record;
+
+use crate::colours::Colours;
+use crate::output::TextFormat;
+
+
+/// A **table** is built up from all the response records present in a DNS
+/// packet. It then gets displayed to the user.
+#[derive(Debug)]
+pub struct Table {
+    colours: Colours,
+    text_format: TextFormat,
+    rows: Vec<Row>,
+}
+
+/// A row of the table. This contains all the fields
+#[derive(Debug)]
+pub struct Row {
+    qtype: ANSIString<'static>,
+    qname: String,
+    ttl: Option<String>,
+    section: Section,
+    summary: String,
+}
+
+/// The section of the DNS response that a record was read from.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum Section {
+
+    /// This record was found in the **Answer** section.
+    Answer,
+
+    /// This record was found in the **Authority** section.
+    Authority,
+
+    /// This record was found in the **Additional** section.
+    Additional,
+}
+
+
+impl Table {
+
+    /// Create a new table with no rows.
+    pub fn new(colours: Colours, text_format: TextFormat) -> Self {
+        Self { colours, text_format, rows: Vec::new() }
+    }
+
+    /// Adds a row to the table, containing the data in the given answer in
+    /// the right section.
+    pub fn add_row(&mut self, answer: Answer, section: Section) {
+        match answer {
+            Answer::Standard { record, qname, ttl, .. } => {
+                let qtype = self.coloured_record_type(&record);
+                let summary = self.text_format.record_payload_summary(&record);
+                let ttl = Some(self.text_format.format_duration(ttl));
+                self.rows.push(Row { qtype, qname, ttl, summary, section });
+            }
+            Answer::Pseudo { qname, opt } => {
+                let qtype = self.colours.opt.paint("OPT");
+                let summary = self.text_format.pseudo_record_payload_summary(&opt);
+                self.rows.push(Row { qtype, qname, ttl: None, summary, section });
+            }
+        }
+    }
+
+    /// Prints the formatted table to stdout.
+    pub fn print(self, duration: Option<Duration>) {
+        if ! self.rows.is_empty() {
+            let qtype_len = self.max_qtype_len();
+            let qname_len = self.max_qname_len();
+            let ttl_len   = self.max_ttl_len();
+
+            for r in &self.rows {
+                for _ in 0 .. qtype_len - r.qtype.len() {
+                    print!(" ");
+                }
+
+                print!("{} {} ", r.qtype, self.colours.qname.paint(&r.qname));
+
+                for _ in 0 .. qname_len - r.qname.len() {
+                    print!(" ");
+                }
+
+                if let Some(ttl) = &r.ttl {
+                    for _ in 0 .. ttl_len - ttl.len() {
+                        print!(" ");
+                    }
+
+                    print!("{}", ttl);
+                }
+                else {
+                    for _ in 0 .. ttl_len {
+                        print!(" ");
+                    }
+                }
+
+                println!(" {} {}", self.format_section(r.section), r.summary);
+            }
+        }
+        else {
+            println!("No results");
+        }
+
+        if let Some(dur) = duration {
+            println!("Ran in {}ms", dur.as_millis());
+        }
+    }
+
+    fn coloured_record_type(&self, record: &Record) -> ANSIString<'static> {
+        match *record {
+            Record::A(_)      => self.colours.a.paint("A"),
+            Record::AAAA(_)   => self.colours.aaaa.paint("AAAA"),
+            Record::CAA(_)    => self.colours.caa.paint("CAA"),
+            Record::CNAME(_)  => self.colours.cname.paint("CNAME"),
+            Record::MX(_)     => self.colours.mx.paint("MX"),
+            Record::NS(_)     => self.colours.ns.paint("NS"),
+            Record::PTR(_)    => self.colours.ptr.paint("PTR"),
+            Record::SOA(_)    => self.colours.soa.paint("SOA"),
+            Record::SRV(_)    => self.colours.srv.paint("SRV"),
+            Record::TXT(_)    => self.colours.txt.paint("TXT"),
+
+            Record::Other { ref type_number, .. } => self.colours.unknown.paint(type_number.to_string()),
+        }
+    }
+
+    fn max_qtype_len(&self) -> usize {
+        self.rows.iter().map(|r| r.qtype.len()).max().unwrap()
+    }
+
+    fn max_qname_len(&self) -> usize {
+        self.rows.iter().map(|r| r.qname.len()).max().unwrap()
+    }
+
+    fn max_ttl_len(&self) -> usize {
+        self.rows.iter().map(|r| r.ttl.as_ref().map_or(0, |e| e.len())).max().unwrap()
+    }
+
+    fn format_section(&self, section: Section) -> ANSIString<'static> {
+        match section {
+            Section::Answer      => self.colours.answer.paint(" "),
+            Section::Authority   => self.colours.authority.paint("A"),
+            Section::Additional  => self.colours.additional.paint("+"),
+        }
+    }
+}

+ 21 - 0
src/txid.rs

@@ -0,0 +1,21 @@
+/// A **transaction ID generator** is used to create unique ID numbers to
+/// identify each packet, as part of the DNS protocol.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum TxidGenerator {
+
+    /// Generate random transaction IDs each time.
+    Random,
+
+    /// Generate transaction IDs in a sequence, starting from the given value,
+    /// wrapping around.
+    Sequence(u16),
+}
+
+impl TxidGenerator {
+    pub fn generate(self) -> u16 {
+        match self {
+            Self::Random           => rand::random(),
+            Self::Sequence(start)  => start,   // todo
+        }
+    }
+}

+ 38 - 0
src/usage.txt

@@ -0,0 +1,38 @@
+\4mUsage:\0m
+  \1mdog\0m \1;33m[OPTIONS]\0m [--] \32m<arguments>\0m
+
+\4mExamples:\0m
+  \1mdog\0m \32mexample.net\0m                          Query a domain using default settings
+  \1mdog\0m \32mexample.net MX\0m                       ...looking up MX records instead
+  \1mdog\0m \32mexample.net MX @1.1.1.1\0m              ...using a specific nameserver instead
+  \1mdog\0m \32mexample.net MX @1.1.1.1\0m \1;33m-T\0m           ...using TCP rather than UDP
+  \1mdog\0m \1;33m-q\0m \33mexample.net\0m \1;33m-t\0m \33mMX\0m \1;33m-n\0m \33m1.1.1.1\0m \1;33m-T\0m   As above, but using explicit arguments
+
+\4mQuery options:\0m
+  \32m<arguments>\0m              Human-readable host names, nameservers, types, or classes
+  \1;33m-q\0m, \1;33m--query\0m=\33mHOST\0m         Host name or IP address to query
+  \1;33m-t\0m, \1;33m--type\0m=\33mTYPE\0m          Type of the DNS record being queried (A, MX, NS...)
+  \1;33m-n\0m, \1;33m--nameserver\0m=\33mADDR\0m    Address of the nameserver to send packets to
+  \1;33m--class\0m=\33mCLASS\0m            Network class of the DNS record being queried (IN, CH, HS)
+
+\4mSending options:\0m
+  \1;33m--edns\0m=\33mSETTING\0m           Whether to OPT in to EDNS (disable, hide, show)
+  \1;33m--txid\0m=\33mNUMBER\0m            Set the transaction ID to a specific value
+  \1;33m-Z\0m=\33mTWEAKS\0m                Uncommon protocol tweaks
+
+\4mProtocol options:\0m
+  \1;33m-U\0m, \1;33m--udp\0m                Use the DNS protocol over UDP
+  \1;33m-T\0m, \1;33m--tcp\0m                Use the DNS protocol over TCP
+  \1;33m-S\0m, \1;33m--tls\0m                Use the DNS-over-TLS protocol
+  \1;33m-H\0m, \1;33m--https\0m              Use the DNS-over-HTTPS protocol
+
+\4mOutput options:\0m
+  \1;33m-1\0m, \1;33m--short\0m              Short mode: display nothing but the first result
+  \1;33m-J\0m, \1;33m--json\0m               Display the output as JSON
+  \1;33m--color\0m, \1;33m--colour\0m=\33mWHEN\0m   When to colourise the output (always, automatic, never)
+  \1;33m--seconds\0m                Do not format durations, display them as seconds
+  \1;33m--time\0m                   Print how long the response took to arrive
+
+\4mMeta options:\0m
+  \1;33m-?\0m, \1;33m--help\0m               Print list of command-line options
+  \1;33m-v\0m, \1;33m--version\0m            Print version information