[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: ogham\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Report a crash, runtime error, or invalid output in dog\n---\n\nIf dog does something unexpected, or displays an error on the screen, or if it outright crashes, then please include the following information in your report:\n\n- The version of dog being used (`dog --version`)\n- The command-line arguments you are using\n- Your operating system and hardware platform\n\nIf it’s a crash, please include the full text of the crash that gets printed to the screen. If you’re seeing unexpected behaviour, a screenshot of the issue will help a lot.\n\n---\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/compilation_error.md",
    "content": "---\nname: Compilation error\nabout: Report a problem compiling dog\n---\n\nIf dog fails to compile, or if there is a problem during the build process, then please include the following information in your report:\n\n- The exact dog commit you are building (`git rev-parse --short HEAD`)\n- The version of rustc you are compiling it with (`rustc --version`)\n- Your operating system and hardware platform\n- The Rust build target (the _exact_ output of `rustc --print cfg`)\n\nIf you are seeing compilation errors, please include the output of the build process.\n\n---\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Request a feature or enhancement to dog\n---\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/question.md",
    "content": "---\nname: Question\nabout: Ask a question about dog\n---\n"
  },
  {
    "path": ".gitignore",
    "content": "/target\n/tarpaulin-report.html\nfuzz-*.log\n/cargo-timing*.html\n"
  },
  {
    "path": ".rustfmt.toml",
    "content": "disable_all_formatting = true\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: rust\nrust:\n  - 1.45.0\n  - stable\n  - beta\n  - nightly\n\nscript:\n  - cargo build --verbose --workspace\n  - cargo test --verbose --workspace --no-run\n  - cargo test --verbose --workspace\n\nos:\n  - windows\n  - linux\n  - osx\n\njobs:\n  fast_finish: true\n  allow_failures:\n    - rust: nightly\n\n  include:\n    - name: 'Rust: lint with Clippy'\n      rust: stable\n      install:\n        - rustup component add clippy\n      script:\n        - cargo clippy\n\n    - name: 'Rust: mutation testing'\n      rust: nightly\n      install:\n        - git clone https://github.com/llogiq/mutagen.git\n        - cd mutagen/mutagen-runner\n        - cargo install --path .\n        - cd ../..\n      script:\n        - cargo test    --package dns --features=dns/with_mutagen -- --quiet\n        - cargo mutagen --package dns --features=dns/with_mutagen\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"dog\"\ndescription = \"A command-line DNS client\"\n\nauthors = [\"Benjamin Sago <ogham@bsago.me>\"]\ncategories = [\"command-line-utilities\"]\nedition = \"2018\"\nexclude = [\n    \"/completions/*\", \"/man/*\", \"/xtests/*\",\n    \"/dog-screenshot.png\", \"/Justfile\", \"/README.md\", \"/.rustfmt.toml\", \"/.travis.yml\",\n]\nhomepage = \"https://dns.lookup.dog/\"\nlicense = \"EUPL-1.2\"\nversion = \"0.2.0-pre\"\n\n\n[[bin]]\nname = \"dog\"\npath = \"src/main.rs\"\ndoctest = false\n\n\n[workspace]\nmembers = [\n  \"dns\",\n  \"dns-transport\",\n]\n\n\n# make dev builds faster by excluding debug symbols\n[profile.dev]\ndebug = false\n\n# use LTO for smaller binaries (that take longer to build)\n[profile.release]\nlto = true\noverflow-checks = true\npanic = \"abort\"\n\n\n[dependencies]\n\n# dns stuff\ndns = { path = \"./dns\" }\ndns-transport = { path = \"./dns-transport\" }\n\n# command-line\nansi_term = \"0.12\"\natty = \"0.2\"\ngetopts = \"0.2\"\n\n# transaction ID generation\nrand = \"0.8\"\n\n# json output\njson = \"0.12\"\n\n# logging\nlog = \"0.4\"\n\n# windows default nameserver determination\n[target.'cfg(windows)'.dependencies]\nipconfig = { version = \"0.2\" }\n\n[build-dependencies]\ndatetime = { version = \"0.5.1\", default_features = false }\n\n[dev-dependencies]\npretty_assertions = \"0.7\"\n\n[features]\ndefault = [\"with_idna\", \"with_tls\", \"with_https\", \"with_nativetls\"]\nwith_idna = [\"dns/with_idna\"]\n\nwith_tls = [\"dns-transport/with_tls\"]\nwith_https = [\"dns-transport/with_https\"]\n\nwith_nativetls = [\"dns-transport/with_nativetls\"]\nwith_nativetls_vendored = [\"with_nativetls\", \"dns-transport/with_nativetls\", \"dns-transport/with_nativetls_vendored\"]\nwith_rustls = [\"dns-transport/with_rustls\"]\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM rust as build\n\nWORKDIR /build\nCOPY /src /build/src\nCOPY /dns /build/dns\nCOPY /dns-transport /build/dns-transport\nCOPY /man /build/man\nCOPY build.rs Cargo.toml /build/\n\nRUN cargo build --release\n\nFROM debian:buster-slim\n\nRUN apt update && apt install -y libssl1.1 ca-certificates && apt clean all\n\nCOPY --from=build /build/target/release/dog /dog\n\nENTRYPOINT [\"/dog\"]\n"
  },
  {
    "path": "Justfile",
    "content": "all: build test xtests\nall-release: build-release test-release xtests-release\nall-quick: build-quick test-quick xtests-quick\n\nexport DOG_DEBUG := \"\"\n\n\n#----------#\n# building #\n#----------#\n\n# compile the dog binary\n@build:\n    cargo build\n\n# compile the dog binary (in release mode)\n@build-release:\n    cargo build --release --verbose\n    strip \"${CARGO_TARGET_DIR:-target}/release/dog\"\n\n# produce an HTML chart of compilation timings\n@build-time:\n    cargo +nightly clean\n    cargo +nightly build -Z timings\n\n# compile the dog binary (without some features)\n@build-quick:\n    cargo build --no-default-features\n\n# check that the dog binary can compile\n@check:\n    cargo check\n\n\n#---------------#\n# running tests #\n#---------------#\n\n# run unit tests\n@test:\n    cargo test --workspace -- --quiet\n\n# run unit tests (in release mode)\n@test-release:\n    cargo test --workspace --release --verbose\n\n# run unit tests (without some features)\n@test-quick:\n    cargo test --workspace --no-default-features -- --quiet\n\n# run mutation tests\n@test-mutation:\n    cargo +nightly test    --package dns --features=dns/with_mutagen -- --quiet\n    cargo +nightly mutagen --package dns --features=dns/with_mutagen\n\n\n#------------------------#\n# running extended tests #\n#------------------------#\n\n# run extended tests\n@xtests *args:\n    specsheet xtests/{options,live,madns}/*.toml -shide {{args}} \\\n        -O cmd.target.dog=\"${CARGO_TARGET_DIR:-../../target}/debug/dog\"\n\n# run extended tests (in release mode)\n@xtests-release *args:\n    specsheet xtests/{options,live,madns}/*.toml {{args}} \\\n        -O cmd.target.dog=\"${CARGO_TARGET_DIR:-../../target}/release/dog\"\n\n# run extended tests (omitting certain feature tests)\n@xtests-quick *args:\n    specsheet xtests/options/*.toml xtests/live/{basics,tcp}.toml -shide {{args}} \\\n        -O cmd.target.dog=\"${CARGO_TARGET_DIR:-../../target}/debug/dog\"\n\n# run extended tests against a local madns instance\n@xtests-madns-local *args:\n    env MADNS_ARGS=\"@localhost:5301 --tcp\" \\\n        specsheet xtests/madns/*.toml -shide {{args}} \\\n            -O cmd.target.dog=\"${CARGO_TARGET_DIR:-../../target}/debug/dog\"\n\n# display the number of extended tests that get run\n@count-xtests:\n    grep -F '[[cmd]]' -R xtests | wc -l\n\n\n#---------#\n# fuzzing #\n#---------#\n\n# run fuzzing on the dns crate\n@fuzz:\n    cargo +nightly fuzz --version\n    cd dns; cargo +nightly fuzz run fuzz_parsing -- -jobs=`nproc` -workers=`nproc` -runs=69105\n\n# print out the data that caused crashes during fuzzing as hexadecimal\n@fuzz-hex:\n    for crash in dns/fuzz/artifacts/fuzz_parsing/crash-*; do echo; echo $crash; hexyl $crash; done\n\n# remove fuzz log files\n@fuzz-clean:\n    rm dns/fuzz/fuzz-*.log\n\n\n#-----------------------#\n# code quality and misc #\n#-----------------------#\n\n# lint the code\n@clippy:\n    touch dns/src/lib.rs\n    cargo clippy\n\n# generate a code coverage report using tarpaulin via docker\n@coverage-docker:\n    docker run --security-opt seccomp=unconfined -v \"${PWD}:/volume\" xd009642/tarpaulin cargo tarpaulin --all --out Html\n\n# update dependency versions, and check for outdated ones\n@update-deps:\n    cargo update\n    command -v cargo-outdated >/dev/null || (echo \"cargo-outdated not installed\" && exit 1)\n    cargo outdated\n\n# list unused dependencies\n@unused-deps:\n    command -v cargo-udeps >/dev/null || (echo \"cargo-udeps not installed\" && exit 1)\n    cargo +nightly udeps\n\n# builds dog and runs extended tests with features disabled\n@feature-checks *args:\n    cargo build --no-default-features\n    specsheet xtests/features/none.toml -shide {{args}} \\\n        -O cmd.target.dog=\"${CARGO_TARGET_DIR:-../../target}/debug/dog\"\n\n# print versions of the necessary build tools\n@versions:\n    rustc --version\n    cargo --version\n\n\n#---------------#\n# documentation #\n#---------------#\n\n# render the documentation\n@doc:\n    cargo doc --no-deps --workspace\n\n# build the man pages\n@man:\n    mkdir -p \"${CARGO_TARGET_DIR:-target}/man\"\n    pandoc --standalone -f markdown -t man man/dog.1.md > \"${CARGO_TARGET_DIR:-target}/man/dog.1\"\n\n# build and preview the man page\n@man-preview: man\n    man \"${CARGO_TARGET_DIR:-target}/man/dog.1\"\n\n\n#-----------#\n# packaging #\n#-----------#\n\n# create a distributable package\nzip desc exe=\"dog\":\n    #!/usr/bin/env perl\n    use Archive::Zip;\n    -e 'target/release/{{ exe }}' || die 'Binary not built!';\n    -e 'target/man/dog.1' || die 'Man page not built!';\n    my $zip = Archive::Zip->new();\n    $zip->addFile('completions/dog.bash');\n    $zip->addFile('completions/dog.zsh');\n    $zip->addFile('completions/dog.fish');\n    $zip->addFile('target/man/dog.1', 'man/dog.1');\n    $zip->addFile('target/release/{{ exe }}', 'bin/{{ exe }}');\n    $zip->writeToFileNamed('dog-{{ desc }}.zip') == AZ_OK || die 'Zip write error!';\n    system 'unzip -l \"dog-{{ desc }}\".zip'\n"
  },
  {
    "path": "LICENCE",
    "content": "                      EUROPEAN UNION PUBLIC LICENCE v. 1.2\n                      EUPL © the European Union 2007, 2016\n\nThis European Union Public Licence (the ‘EUPL’) applies to the Work (as defined\nbelow) which is provided under the terms of this Licence. Any use of the Work,\nother than as authorised under this Licence is prohibited (to the extent such\nuse is covered by a right of the copyright holder of the Work).\n\nThe Work is provided under the terms of this Licence when the Licensor (as\ndefined below) has placed the following notice immediately following the\ncopyright notice for the Work:\n\n        Licensed under the EUPL\n\nor has expressed by any other means his willingness to license under the EUPL.\n\n1. Definitions\n\nIn this Licence, the following terms have the following meaning:\n\n- ‘The Licence’: this Licence.\n\n- ‘The Original Work’: the work or software distributed or communicated by the\n  Licensor under this Licence, available as Source Code and also as Executable\n  Code as the case may be.\n\n- ‘Derivative Works’: the works or software that could be created by the\n  Licensee, based upon the Original Work or modifications thereof. This Licence\n  does not define the extent of modification or dependence on the Original Work\n  required in order to classify a work as a Derivative Work; this extent is\n  determined by copyright law applicable in the country mentioned in Article 15.\n\n- ‘The Work’: the Original Work or its Derivative Works.\n\n- ‘The Source Code’: the human-readable form of the Work which is the most\n  convenient for people to study and modify.\n\n- ‘The Executable Code’: any code which has generally been compiled and which is\n  meant to be interpreted by a computer as a program.\n\n- ‘The Licensor’: the natural or legal person that distributes or communicates\n  the Work under the Licence.\n\n- ‘Contributor(s)’: any natural or legal person who modifies the Work under the\n  Licence, or otherwise contributes to the creation of a Derivative Work.\n\n- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of\n  the Work under the terms of the Licence.\n\n- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending,\n  renting, distributing, communicating, transmitting, or otherwise making\n  available, online or offline, copies of the Work or providing access to its\n  essential functionalities at the disposal of any other natural or legal\n  person.\n\n2. Scope of the rights granted by the Licence\n\nThe Licensor hereby grants You a worldwide, royalty-free, non-exclusive,\nsublicensable licence to do the following, for the duration of copyright vested\nin the Original Work:\n\n- use the Work in any circumstance and for all usage,\n- reproduce the Work,\n- modify the Work, and make Derivative Works based upon the Work,\n- communicate to the public, including the right to make available or display\n  the Work or copies thereof to the public and perform publicly, as the case may\n  be, the Work,\n- distribute the Work or copies thereof,\n- lend and rent the Work or copies thereof,\n- sublicense rights in the Work or copies thereof.\n\nThose rights can be exercised on any media, supports and formats, whether now\nknown or later invented, as far as the applicable law permits so.\n\nIn the countries where moral rights apply, the Licensor waives his right to\nexercise his moral right to the extent allowed by law in order to make effective\nthe licence of the economic rights here above listed.\n\nThe Licensor grants to the Licensee royalty-free, non-exclusive usage rights to\nany patents held by the Licensor, to the extent necessary to make use of the\nrights granted on the Work under this Licence.\n\n3. Communication of the Source Code\n\nThe Licensor may provide the Work either in its Source Code form, or as\nExecutable Code. If the Work is provided as Executable Code, the Licensor\nprovides in addition a machine-readable copy of the Source Code of the Work\nalong with each copy of the Work that the Licensor distributes or indicates, in\na notice following the copyright notice attached to the Work, a repository where\nthe Source Code is easily and freely accessible for as long as the Licensor\ncontinues to distribute or communicate the Work.\n\n4. Limitations on copyright\n\nNothing in this Licence is intended to deprive the Licensee of the benefits from\nany exception or limitation to the exclusive rights of the rights owners in the\nWork, of the exhaustion of those rights or of other applicable limitations\nthereto.\n\n5. Obligations of the Licensee\n\nThe grant of the rights mentioned above is subject to some restrictions and\nobligations imposed on the Licensee. Those obligations are the following:\n\nAttribution right: The Licensee shall keep intact all copyright, patent or\ntrademarks notices and all notices that refer to the Licence and to the\ndisclaimer of warranties. The Licensee must include a copy of such notices and a\ncopy of the Licence with every copy of the Work he/she distributes or\ncommunicates. The Licensee must cause any Derivative Work to carry prominent\nnotices stating that the Work has been modified and the date of modification.\n\nCopyleft clause: If the Licensee distributes or communicates copies of the\nOriginal Works or Derivative Works, this Distribution or Communication will be\ndone under the terms of this Licence or of a later version of this Licence\nunless the Original Work is expressly distributed only under this version of the\nLicence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee\n(becoming Licensor) cannot offer or impose any additional terms or conditions on\nthe Work or Derivative Work that alter or restrict the terms of the Licence.\n\nCompatibility clause: If the Licensee Distributes or Communicates Derivative\nWorks or copies thereof based upon both the Work and another work licensed under\na Compatible Licence, this Distribution or Communication can be done under the\nterms of this Compatible Licence. For the sake of this clause, ‘Compatible\nLicence’ refers to the licences listed in the appendix attached to this Licence.\nShould the Licensee's obligations under the Compatible Licence conflict with\nhis/her obligations under this Licence, the obligations of the Compatible\nLicence shall prevail.\n\nProvision of Source Code: When distributing or communicating copies of the Work,\nthe Licensee will provide a machine-readable copy of the Source Code or indicate\na repository where this Source will be easily and freely available for as long\nas the Licensee continues to distribute or communicate the Work.\n\nLegal Protection: This Licence does not grant permission to use the trade names,\ntrademarks, service marks, or names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the copyright notice.\n\n6. Chain of Authorship\n\nThe original Licensor warrants that the copyright in the Original Work granted\nhereunder is owned by him/her or licensed to him/her and that he/she has the\npower and authority to grant the Licence.\n\nEach Contributor warrants that the copyright in the modifications he/she brings\nto the Work are owned by him/her or licensed to him/her and that he/she has the\npower and authority to grant the Licence.\n\nEach time You accept the Licence, the original Licensor and subsequent\nContributors grant You a licence to their contributions to the Work, under the\nterms of this Licence.\n\n7. Disclaimer of Warranty\n\nThe Work is a work in progress, which is continuously improved by numerous\nContributors. It is not a finished work and may therefore contain defects or\n‘bugs’ inherent to this type of development.\n\nFor the above reason, the Work is provided under the Licence on an ‘as is’ basis\nand without warranties of any kind concerning the Work, including without\nlimitation merchantability, fitness for a particular purpose, absence of defects\nor errors, accuracy, non-infringement of intellectual property rights other than\ncopyright as stated in Article 6 of this Licence.\n\nThis disclaimer of warranty is an essential part of the Licence and a condition\nfor the grant of any rights to the Work.\n\n8. Disclaimer of Liability\n\nExcept in the cases of wilful misconduct or damages directly caused to natural\npersons, the Licensor will in no event be liable for any direct or indirect,\nmaterial or moral, damages of any kind, arising out of the Licence or of the use\nof the Work, including without limitation, damages for loss of goodwill, work\nstoppage, computer failure or malfunction, loss of data or any commercial\ndamage, even if the Licensor has been advised of the possibility of such damage.\nHowever, the Licensor will be liable under statutory product liability laws as\nfar such laws apply to the Work.\n\n9. Additional agreements\n\nWhile distributing the Work, You may choose to conclude an additional agreement,\ndefining obligations or services consistent with this Licence. However, if\naccepting obligations, You may act only on your own behalf and on your sole\nresponsibility, not on behalf of the original Licensor or any other Contributor,\nand only if You agree to indemnify, defend, and hold each Contributor harmless\nfor any liability incurred by, or claims asserted against such Contributor by\nthe fact You have accepted any warranty or additional liability.\n\n10. Acceptance of the Licence\n\nThe provisions of this Licence can be accepted by clicking on an icon ‘I agree’\nplaced under the bottom of a window displaying the text of this Licence or by\naffirming consent in any other similar way, in accordance with the rules of\napplicable law. Clicking on that icon indicates your clear and irrevocable\nacceptance of this Licence and all of its terms and conditions.\n\nSimilarly, you irrevocably accept this Licence and all of its terms and\nconditions by exercising any rights granted to You by Article 2 of this Licence,\nsuch as the use of the Work, the creation by You of a Derivative Work or the\nDistribution or Communication by You of the Work or copies thereof.\n\n11. Information to the public\n\nIn case of any Distribution or Communication of the Work by means of electronic\ncommunication by You (for example, by offering to download the Work from a\nremote location) the distribution channel or media (for example, a website) must\nat least provide to the public the information requested by the applicable law\nregarding the Licensor, the Licence and the way it may be accessible, concluded,\nstored and reproduced by the Licensee.\n\n12. Termination of the Licence\n\nThe Licence and the rights granted hereunder will terminate automatically upon\nany breach by the Licensee of the terms of the Licence.\n\nSuch a termination will not terminate the licences of any person who has\nreceived the Work from the Licensee under the Licence, provided such persons\nremain in full compliance with the Licence.\n\n13. Miscellaneous\n\nWithout prejudice of Article 9 above, the Licence represents the complete\nagreement between the Parties as to the Work.\n\nIf any provision of the Licence is invalid or unenforceable under applicable\nlaw, this will not affect the validity or enforceability of the Licence as a\nwhole. Such provision will be construed or reformed so as necessary to make it\nvalid and enforceable.\n\nThe European Commission may publish other linguistic versions or new versions of\nthis Licence or updated versions of the Appendix, so far this is required and\nreasonable, without reducing the scope of the rights granted by the Licence. New\nversions of the Licence will be published with a unique version number.\n\nAll linguistic versions of this Licence, approved by the European Commission,\nhave identical value. Parties can take advantage of the linguistic version of\ntheir choice.\n\n14. Jurisdiction\n\nWithout prejudice to specific agreement between parties,\n\n- any litigation resulting from the interpretation of this License, arising\n  between the European Union institutions, bodies, offices or agencies, as a\n  Licensor, and any Licensee, will be subject to the jurisdiction of the Court\n  of Justice of the European Union, as laid down in article 272 of the Treaty on\n  the Functioning of the European Union,\n\n- any litigation arising between other parties and resulting from the\n  interpretation of this License, will be subject to the exclusive jurisdiction\n  of the competent court where the Licensor resides or conducts its primary\n  business.\n\n15. Applicable Law\n\nWithout prejudice to specific agreement between parties,\n\n- this Licence shall be governed by the law of the European Union Member State\n  where the Licensor has his seat, resides or has his registered office,\n\n- this licence shall be governed by Belgian law if the Licensor has no seat,\n  residence or registered office inside a European Union Member State.\n\nAppendix\n\n‘Compatible Licences’ according to Article 5 EUPL are:\n\n- GNU General Public License (GPL) v. 2, v. 3\n- GNU Affero General Public License (AGPL) v. 3\n- Open Software License (OSL) v. 2.1, v. 3.0\n- Eclipse Public License (EPL) v. 1.0\n- CeCILL v. 2.0, v. 2.1\n- Mozilla Public Licence (MPL) v. 2\n- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3\n- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for\n  works other than software\n- European Union Public Licence (EUPL) v. 1.1, v. 1.2\n- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong\n  Reciprocity (LiLiQ-R+).\n\nThe European Commission may update this Appendix to later versions of the above\nlicences without producing a new version of the EUPL, as long as they provide\nthe rights granted in Article 2 of this Licence and protect the covered Source\nCode from exclusive appropriation.\n\nAll other changes or additions to this Appendix require the production of a new\nEUPL version.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n<h1>dog</h1>\n\n[dog](https://dns.lookup.dog/) is a command-line DNS client.\n\n<a href=\"https://travis-ci.org/github/ogham/dog\">\n    <img src=\"https://travis-ci.org/ogham/dog.svg?branch=master\" alt=\"Build status\" />\n</a>\n\n<a href=\"https://saythanks.io/to/ogham%40bsago.me\">\n    <img src=\"https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg\" alt=\"Say thanks!\" />\n</a>\n</div>\n\n![A screenshot of dog making a DNS request](dog-screenshot.png)\n\n---\n\nDogs _can_ look up!\n\n**dog** is a command-line DNS client, like `dig`.\nIt has colourful output, understands normal command-line argument syntax, supports the DNS-over-TLS and DNS-over-HTTPS protocols, and can emit JSON.\n\n## Examples\n\n    dog example.net                          Query a domain using default settings\n    dog example.net MX                       ...looking up MX records instead\n    dog example.net MX @1.1.1.1              ...using a specific nameserver instead\n    dog example.net MX @1.1.1.1 -T           ...using TCP rather than UDP\n    dog -q example.net -t MX -n 1.1.1.1 -T   As above, but using explicit arguments\n\n---\n\n## Command-line options\n\n### Query options\n\n    <arguments>              Human-readable host names, nameservers, types, or classes\n    -q, --query=HOST         Host name or domain name to query\n    -t, --type=TYPE          Type of the DNS record being queried (A, MX, NS...)\n    -n, --nameserver=ADDR    Address of the nameserver to send packets to\n    --class=CLASS            Network class of the DNS record being queried (IN, CH, HS)\n\n### Sending options\n\n    --edns=SETTING           Whether to OPT in to EDNS (disable, hide, show)\n    --txid=NUMBER            Set the transaction ID to a specific value\n    -Z=TWEAKS                Set uncommon protocol-level tweaks\n\n### Protocol options\n\n    -U, --udp                Use the DNS protocol over UDP\n    -T, --tcp                Use the DNS protocol over TCP\n    -S, --tls                Use the DNS-over-TLS protocol\n    -H, --https              Use the DNS-over-HTTPS protocol\n\n### Output options\n\n    -1, --short              Short mode: display nothing but the first result\n    -J, --json               Display the output as JSON\n    --color, --colour=WHEN   When to colourise the output (always, automatic, never)\n    --seconds                Do not format durations, display them as seconds\n    --time                   Print how long the response took to arrive\n\n\n---\n\n## Installation\n\nTo install dog, you can download a pre-compiled binary, or you can compile it from source. You _may_ be able to install dog using your OS’s package manager, depending on your platform.\n\n\n### Packages\n\n- For Arch Linux, install the [`dog`](https://www.archlinux.org/packages/community/x86_64/dog/) package.\n- For Homebrew on macOS, install the [`dog`](https://formulae.brew.sh/formula/dog) formula.\n- For NixOS, install the [`dogdns`](https://search.nixos.org/packages?channel=unstable&show=dogdns&query=dogdns) package.\n\n\n### Downloads\n\nBinary downloads of dog are available from [the releases section on GitHub](https://github.com/ogham/dog/releases/) for 64-bit Windows, macOS, and Linux targets. They contain the compiled executable, the manual page, and shell completions.\n\n\n### Compilation\n\ndog is written in [Rust](https://www.rust-lang.org).\nYou will need rustc version [1.45.0](https://blog.rust-lang.org/2020/07/16/Rust-1.45.0.html) or higher.\nThe recommended way to install Rust for development is from the [official download page](https://www.rust-lang.org/tools/install), using rustup.\n\nTo build, download the source code and run:\n\n    $ cargo build\n    $ cargo test\n\n- The [just](https://github.com/casey/just) command runner can be used to run some helpful development commands, in a manner similar to `make`.\nRun `just --list` to get an overview of what’s available.\n\n- If you are compiling a copy for yourself, be sure to run `cargo build --release` or `just build-release` to benefit from release-mode optimisations.\nCopy the resulting binary, which will be in the `target/release` directory, into a folder in your `$PATH`.\n`/usr/local/bin` is usually a good choice.\n\n- To compile and install the manual pages, you will need [pandoc](https://pandoc.org/).\nThe `just man` command will compile the Markdown into manual pages, which it will place in the `target/man` directory.\nTo use them, copy them into a directory that `man` will read.\n`/usr/local/share/man` is usually a good choice.\n\n\n### Container image\n\nTo build the container image of dog, you can use Docker or Kaniko. Here an example using Docker:\n\n    $ docker build -t dog .\n\nYou can then run it using the following command:\n\n    $ docker run -it --rm dog\n\nTo run dog directly, you can then define the following alias:\n\n    $ alias dog=\"docker run -it --rm dog\"\n\n\n### End-to-end testing\n\ndog has an integration test suite written as [Specsheet](https://specsheet.software/) check documents.\nIf you have a copy installed, you can run:\n\n    $ just xtests\n\nSpecsheet will test the compiled binary by making DNS requests over the network, checking that dog returns the correct results and does not crash.\nNote that this will expose your IP address.\nFor more information, read [the xtests README](xtests/README.md).\n\n\n### Feature toggles\n\ndog has three Cargo features that can be switched off to remove functionality.\nWhile doing so makes dog less useful, it results in a smaller binary that takes less time to build.\n\nThere are three feature toggles available, all of which are active by default:\n\n- `with_idna`, which enables [IDNA](https://en.wikipedia.org/wiki/Internationalized_domain_name) processing\n- `with_tls`, which enables DNS-over-TLS\n- `with_https`, which enables DNS-over-HTTPS (requires `with_tls`)\n\nUse `cargo` to build a binary that uses feature toggles. For example, to disable TLS and HTTPS support but keep IDNA support enabled, you can run:\n\n    $ cargo build --no-default-features --features=with_idna\n\nThe list of features that have been disabled can be checked at runtime as part of the `--version` string.\n\n\n---\n\n## Documentation\n\nFor documentation on how to use dog, see the website: <https://dns.lookup.dog/>\n\n\n## See also\n\n`mutt`, `tail`, `sleep`, `roff`\n\n\n## Licence\n\ndog’s source code is licenced under the [European Union Public Licence](https://choosealicense.com/licenses/eupl-1.2/).\n"
  },
  {
    "path": "build.rs",
    "content": "//! This build script gets run during every build. Its purpose is to put\n//! together the files used for the `--help` and `--version`, which need to\n//! come in both coloured and non-coloured variants. The main usage text is\n//! contained in `src/usage.txt`; to make it easier to edit, backslashes (\\)\n//! are used instead of the beginning of ANSI escape codes.\n//!\n//! The version string is quite complex: we want to show the version,\n//! current Git hash, and compilation date when building *debug*\n//! versions, but just the version for *release* versions.\n//!\n//! This script generates the string from the environment variables\n//! that Cargo adds (http://doc.crates.io/environment-variables.html)\n//! and runs `git` to get the SHA1 hash. It then writes the strings\n//! into files, which we can include during compilation.\n\nuse std::env;\nuse std::fs::File;\nuse std::io::{self, Write};\nuse std::path::PathBuf;\n\nuse datetime::{LocalDateTime, ISO};\n\n\n/// The build script entry point.\nfn main() -> io::Result<()> {\n    #![allow(clippy::write_with_newline)]\n\n    let usage   = include_str!(\"src/usage.txt\");\n    let tagline = \"dog \\\\1;32m●\\\\0m command-line DNS client\";\n    let url     = \"https://dns.lookup.dog/\";\n\n    let ver =\n        if is_debug_build() {\n            format!(\"{}\\nv{} \\\\1;31m(pre-release debug build!)\\\\0m\\n\\\\1;4;34m{}\\\\0m\", tagline, version_string(), url)\n        }\n        else if is_development_version() {\n            format!(\"{}\\nv{} [{}] built on {} \\\\1;31m(pre-release!)\\\\0m\\n\\\\1;4;34m{}\\\\0m\", tagline, version_string(), git_hash(), build_date(), url)\n        }\n        else {\n            format!(\"{}\\nv{}\\n\\\\1;4;34m{}\\\\0m\", tagline, version_string(), url)\n        };\n\n    // We need to create these files in the Cargo output directory.\n    let out = PathBuf::from(env::var(\"OUT_DIR\").unwrap());\n\n    // Pretty version text\n    let mut f = File::create(&out.join(\"version.pretty.txt\"))?;\n    writeln!(f, \"{}\", convert_codes(&ver))?;\n\n    // Bland version text\n    let mut f = File::create(&out.join(\"version.bland.txt\"))?;\n    writeln!(f, \"{}\", strip_codes(&ver))?;\n\n    // Pretty usage text\n    let mut f = File::create(&out.join(\"usage.pretty.txt\"))?;\n    writeln!(f, \"{}\", convert_codes(&tagline))?;\n    writeln!(f)?;\n    write!(f, \"{}\", convert_codes(&usage))?;\n\n    // Bland usage text\n    let mut f = File::create(&out.join(\"usage.bland.txt\"))?;\n    writeln!(f, \"{}\", strip_codes(&tagline))?;\n    writeln!(f)?;\n    write!(f, \"{}\", strip_codes(&usage))?;\n\n    Ok(())\n}\n\n/// Converts the escape codes in ‘usage.txt’ to ANSI escape codes.\nfn convert_codes(input: &str) -> String {\n    input.replace(\"\\\\\", \"\\x1B[\")\n}\n\n/// Removes escape codes from ‘usage.txt’.\nfn strip_codes(input: &str) -> String {\n    input.replace(\"\\\\0m\", \"\")\n         .replace(\"\\\\1m\", \"\")\n         .replace(\"\\\\4m\", \"\")\n         .replace(\"\\\\32m\", \"\")\n         .replace(\"\\\\33m\", \"\")\n         .replace(\"\\\\1;31m\", \"\")\n         .replace(\"\\\\1;32m\", \"\")\n         .replace(\"\\\\1;33m\", \"\")\n         .replace(\"\\\\1;4;34\", \"\")\n}\n\n/// Retrieve the project’s current Git hash, as a string.\nfn git_hash() -> String {\n    use std::process::Command;\n\n    String::from_utf8_lossy(\n        &Command::new(\"git\")\n            .args(&[\"rev-parse\", \"--short\", \"HEAD\"])\n            .output().unwrap()\n            .stdout).trim().to_string()\n}\n\n/// Whether we should show pre-release info in the version string.\n///\n/// Both weekly releases and actual releases are --release releases,\n/// but actual releases will have a proper version number.\nfn is_development_version() -> bool {\n    cargo_version().ends_with(\"-pre\") || env::var(\"PROFILE\").unwrap() == \"debug\"\n}\n\n/// Whether we are building in debug mode.\nfn is_debug_build() -> bool {\n    env::var(\"PROFILE\").unwrap() == \"debug\"\n}\n\n/// Retrieves the [package] version in Cargo.toml as a string.\nfn cargo_version() -> String {\n    env::var(\"CARGO_PKG_VERSION\").unwrap()\n}\n\n/// Returns the version and build parameters string.\nfn version_string() -> String {\n    let mut ver = cargo_version();\n\n    let feats = nonstandard_features_string();\n    if ! feats.is_empty() {\n        ver.push_str(&format!(\" [{}]\", &feats));\n    }\n\n    ver\n}\n\n/// Finds whether a feature is enabled by examining the Cargo variable.\nfn feature_enabled(name: &str) -> bool {\n    env::var(&format!(\"CARGO_FEATURE_{}\", name))\n        .map(|e| ! e.is_empty())\n        .unwrap_or(false)\n}\n\n/// A comma-separated list of non-standard feature choices.\nfn nonstandard_features_string() -> String {\n    let mut s = Vec::new();\n\n    if ! feature_enabled(\"WITH_IDNA\") {\n        s.push(\"-idna\");\n    }\n\n    if ! feature_enabled(\"WITH_TLS\") {\n        s.push(\"-tls\");\n    }\n\n    if ! feature_enabled(\"WITH_HTTPS\") {\n        s.push(\"-https\");\n    }\n\n    s.join(\", \")\n}\n\n\n/// Formats the current date as an ISO 8601 string.\nfn build_date() -> String {\n    let now = LocalDateTime::now();\n    format!(\"{}\", now.date().iso())\n}\n"
  },
  {
    "path": "completions/dog.bash",
    "content": "_dog()\n{\n    cur=${COMP_WORDS[COMP_CWORD]}\n    prev=${COMP_WORDS[COMP_CWORD-1]}\n\n    case \"$prev\" in\n        -'?'|--help|-v|--version)\n            return\n            ;;\n\n        -t|--type)\n            COMPREPLY=( $( compgen -W 'A AAAA CAA CNAME HINFO MX NS PTR SOA SRV TXT' -- \"$cur\" ) )\n            return\n            ;;\n\n        --edns)\n            COMPREPLY=( $( compgen -W 'disable hide show' -- \"$cur\" ) )\n            return\n            ;;\n\n        -Z)\n            COMPREPLY=( $( compgen -W 'aa ad bufsize= cd' -- \"$cur\" ) )\n            return\n            ;;\n\n        --class)\n            COMPREPLY=( $( compgen -W 'IN CH HS' -- \"$cur\" ) )\n            return\n            ;;\n\n        --color|--colour)\n            COMPREPLY=( $( compgen -W 'always automatic never' -- $cur ) )\n            return\n            ;;\n    esac\n\n    case \"$cur\" in\n        -*)\n            COMPREPLY=( $( compgen -W '$( _parse_help \"$1\" )' -- \"$cur\" ) )\n            return\n            ;;\n\n        *)\n            COMPREPLY=( $( compgen -W 'A AAAA CAA CNAME HINFO MX NS PTR SOA SRV TXT' -- \"$cur\" ) )\n            ;;\n    esac\n} &&\ncomplete -o bashdefault -F _dog dog\n"
  },
  {
    "path": "completions/dog.fish",
    "content": "# Meta options\ncomplete -c dog -s 'v' -l 'version' -d \"Show version of dog\"\ncomplete -c dog -s '?' -l 'help'    -d \"Show list of command-line options\"\n\n# Query options\ncomplete -c dog -x -a \"(__fish_print_hostnames) A AAAA CAA CNAME HINFO MX NS PTR SOA SRV TXT IN CH HS\"\ncomplete -c dog -s 'q' -l 'query'      -d \"Host name or domain name to query\" -x -a \"(__fish_print_hostnames)\"\ncomplete -c dog -s 't' -l 'type'       -d \"Type of the DNS record being queried\" -x -a \"A AAAA CAA CNAME HINFO MX NS PTR SOA SRV TXT\"\ncomplete -c dog -s 'n' -l 'nameserver' -d \"Address of the nameserver to send packets to\" -x -a \"(__fish_print_hostnames)\"\ncomplete -c dog        -l 'class'      -d \"Network class of the DNS record being queried\" -x -a \"IN CH HS\"\n\n# Sending options\ncomplete -c dog        -l 'edns'       -d \"Whether to OPT in to EDNS\" -x -a \"\n    disable\\t'Do not send an OPT query'\n    hide\\t'Send an OPT query, but hide the result'\n    show\\t'Send an OPT query, and show the result'\n\"\ncomplete -c dog        -l 'txid'       -d \"Set the transaction ID to a specific value\" -x\ncomplete -c dog -s 'Z'                 -d \"Configure uncommon protocol-level tweaks\" -x -a \"\n    aa\\t'Set the AA (Authoritative Answers) query bit'\n    ad\\t'Set the AD (Authentic Data) query bit'\n    bufsize=\\t'Set the UDP payload size'\n    cd\\t'Set the CD (Checking Disabled) query bit'\n\"\n\n# Protocol options\ncomplete -c dog -s 'U' -l 'udp'        -d \"Use the DNS protocol over UDP\"\ncomplete -c dog -s 'T' -l 'tcp'        -d \"Use the DNS protocol over TCP\"\ncomplete -c dog -s 'S' -l 'tls'        -d \"Use the DNS-over-TLS protocol\"\ncomplete -c dog -s 'H' -l 'https'      -d \"Use the DNS-over-HTTPS protocol\"\n\n# Output options\ncomplete -c dog -s '1' -l 'short'      -d \"Display nothing but the first result\"\ncomplete -c dog -s 'J' -l 'json'       -d \"Display the output as JSON\"\ncomplete -c dog        -l 'color'      -d \"When to colorise the output\" -x -a \"\n    always\\t'Always use colors'\n    automatic\\t'Use colors when printing to a terminal'\n    never\\t'Never use colors'\n\"\ncomplete -c dog        -l 'colour'     -d \"When to colourise the output\" -x -a \"\n    always\\t'Always use colours'\n    automatic\\t'Use colours when printing to a terminal'\n    never\\t'Never use colours'\n\"\ncomplete -c dog        -l 'seconds'    -d \"Do not format durations, display them as seconds\"\ncomplete -c dog        -l 'time'       -d \"Print how long the response took to arrive\"\n"
  },
  {
    "path": "completions/dog.ps1",
    "content": "# Note: This works for both Windows PowerShell 5.1 and also PowerShell 7 (Core).\n# But beware that in Windows PowerShell 5.1, it has issues with completing args if they start with '-'.\n# For more information about the bug, see: https://github.com/PowerShell/PowerShell/issues/2912\n# In PowerShell 7+, it should work correctly.\nRegister-ArgumentCompleter -Native -CommandName 'dog' -ScriptBlock {\n    param($wordToComplete, $commandAst, $cursorPosition)\n\n    [string]$argsString = $commandAst.ToString()\n\n    # skip the \"dog\", split the args afterwards as array\n    [string[]]$argsArray = $argsString.Split([char[]]@(' ', '=')) | Select-Object -Skip 1\n    if ($argsArray -eq $null) { $argsArray = @() }\n\n    # detect if starting a new arg (aka ending with space and asking for a completion)\n    [bool]$isNewArg = $cursorPosition -gt $argsString.Length\n    if ($isNewArg) {\n        # if writing a new arg, add empty arg so that current and previous would be shifted\n        $argsArray += ''\n    }\n\n    # get current arg (empty if starting new)\n    [string]$currentArg = $argsArray[-1]\n    if ([string]::IsNullOrEmpty($currentArg)) {\n        $currentArg = ''\n    }\n\n    # get previous arg\n    [string]$previousArg = $argsArray[-2]\n    if ([string]::IsNullOrEmpty($previousArg)) {\n        $previousArg = ''\n    }\n\n    [string[]]$dnsTypeValues = @('A', 'AAAA', 'CAA', 'CNAME', 'HINFO', 'MX', 'NS', 'PTR', 'SOA', 'SRV', 'TXT')\n\n    [string[]]$completions = @()\n    [bool]$isOptionValue = $argsString.EndsWith('=')\n\n    # complete option value\n    switch -Regex ($previousArg) {\n        '^(-q|--query)'       { $isOptionValue = $true }\n        '^(-t|--type)'        { $isOptionValue = $true; $completions += $dnsTypeValues }\n        '^(-n|--nameserver)'  { $isOptionValue = $true }\n        '^(--class)'          { $isOptionValue = $true; $completions += @('IN', 'CH', 'HS') }\n        '^(--edns)'           { $isOptionValue = $true; $completions += @('disable', 'hide', 'show') }\n        '^(--txid)'           { $isOptionValue = $true }\n        '^(-Z)'               { $isOptionValue = $true; $completions += @('aa', 'ad', 'bufsize=', 'cd') }\n        '^(--color|--colour)' { $isOptionValue = $true; $completions += @('always', 'automatic', 'never') }\n    }\n\n    # detect whether to complete option value\n    if ($isOptionValue) {\n        if (!$isNewArg) {\n            # if using =, complete including the option name and =\n            $completions = $completions | ForEach-Object { \"$previousArg=$_\" }\n        }\n    } \n    else {\n        # if not completing option value, offer DNS type values first\n        $completions += $dnsTypeValues\n\n        # complete option name\n        [string[]]$allOptions = @(\n            '-q', '--query',\n            '-t', '--type',\n            '-n', '--nameserver',\n            '--class',\n            '--edns',\n            '--txid',\n            '-Z',\n            '-U', '--udp',\n            '-T', '--tcp',\n            '-S', '--tls',\n            '-H', '--https',\n            '-1', '--short',\n            '-J', '--json',\n            '--color', '--colour',\n            '--seconds',\n            '--time',\n            '-?', '--help',\n            '-v', '--version'\n        ) | Sort-Object\n\n        $completions += $allOptions\n    }\n\n    if ($completions.Count -gt 0) {\n        # narrow down completions by like* matching\n        return $completions -like \"$currentArg*\"\n    }\n}\n"
  },
  {
    "path": "completions/dog.zsh",
    "content": "#compdef dog\n\n__dog() {\n    _arguments \\\n        \"(- 1 *)\"{-v,--version}\"[Show version of dog]\" \\\n        \"(- 1 *)\"{-\\?,--help}\"[Show list of command-line options]\" \\\n        {-q,--query}\"[Host name or domain name to query]::_hosts\" \\\n        {-t,--type}\"[Type of the DNS record being queried]:(record type):(A AAAA CAA CNAME HINFO MX NS PTR SOA SRV TXT)\" \\\n        {-n,--nameserver}\"[Address of the nameserver to send packets to]::_hosts;\" \\\n        --class\"[Network class of the DNS record being queried]:(network class):(IN CH HS)\" \\\n        --edns\"[Whether to OPT in to EDNS]:(edns setting):(disable hide show)\" \\\n        --txid\"[Set the transaction ID to a specific value]\" \\\n        -Z\"[Configure uncommon protocol-level tweaks]:(protocol tweak):(aa ad bufsize= cd)\" \\\n        {-U,--udp}\"[Use the DNS protocol over UDP]\" \\\n        {-T,--tcp}\"[Use the DNS protocol over TCP]\" \\\n        {-S,--tls}\"[Use the DNS-over-TLS protocol]\" \\\n        {-H,--https}\"[Use the DNS-over-HTTPS protocol]\" \\\n        {-1,--short}\"[Display nothing but the finst result]\" \\\n        {-J,--json}\"[Display the output as JSON]\" \\\n        {--color,--colour}\"[When to use terminal colours]:(setting):(always automatic never)\" \\\n        --seconds\"[Do not format durations, display them as seconds]\" \\\n        --time\"[Print how long the response took to arrive\"] \\\n        '*:filename:_hosts'\n}\n\n__dog\n"
  },
  {
    "path": "dns/Cargo.toml",
    "content": "[package]\nname = \"dns\"\nversion = \"0.2.0-pre\"\nauthors = [\"Benjamin Sago <ogham@bsago.me>\"]\nedition = \"2018\"\n\n[lib]\ndoctest = false\n\n\n[dependencies]\n\n# logging\nlog = \"0.4\"\n\n# protocol parsing helper\nbyteorder = \"1.3\"\n\n# printing of certain packets\nbase64 = \"0.13\"\n\n# idna encoding\nunic-idna = { version = \"0.9.0\", optional = true }\n\n# mutation testing\nmutagen = { git = \"https://github.com/llogiq/mutagen\", optional = true }\n\n[dev-dependencies]\npretty_assertions = \"0.7\"\n\n[features]\ndefault = []  # idna is enabled in the main dog crate\nwith_idna = [\"unic-idna\"]\nwith_mutagen = [\"mutagen\"]  # needs nightly\n"
  },
  {
    "path": "dns/fuzz/.gitignore",
    "content": "\ntarget\ncorpus\nartifacts\n"
  },
  {
    "path": "dns/fuzz/Cargo.toml",
    "content": "[package]\nname = \"dns-fuzz\"\nversion = \"0.0.1\"\nauthors = [\"Automatically generated\"]\npublish = false\n\n[package.metadata]\ncargo-fuzz = true\n\n[dependencies.dns]\npath = \"..\"\n\n[dependencies.libfuzzer-sys]\nversion = \"0.3.0\"\n\n# Prevent this from interfering with workspaces\n[workspace]\nmembers = [\".\"]\n\n[[bin]]\nname = \"fuzz_parsing\"\npath = \"fuzz_targets/fuzz_parsing.rs\"\n"
  },
  {
    "path": "dns/fuzz/fuzz_targets/fuzz_parsing.rs",
    "content": "#![no_main]\n#[macro_use] extern crate libfuzzer_sys;\nextern crate dns;\nuse dns::Response;\n\nfuzz_target!(|data: &[u8]| {\n    let _ = Response::from_bytes(data);\n});\n"
  },
  {
    "path": "dns/src/lib.rs",
    "content": "#![warn(deprecated_in_future)]\n#![warn(future_incompatible)]\n#![warn(missing_copy_implementations)]\n#![warn(missing_docs)]\n#![warn(nonstandard_style)]\n#![warn(rust_2018_compatibility)]\n#![warn(rust_2018_idioms)]\n#![warn(single_use_lifetimes)]\n#![warn(trivial_casts, trivial_numeric_casts)]\n#![warn(unused)]\n\n#![warn(clippy::all, clippy::pedantic)]\n#![allow(clippy::doc_markdown)]\n#![allow(clippy::len_without_is_empty)]\n#![allow(clippy::missing_errors_doc)]\n#![allow(clippy::module_name_repetitions)]\n#![allow(clippy::must_use_candidate)]\n#![allow(clippy::non_ascii_literal)]\n#![allow(clippy::redundant_else)]\n#![allow(clippy::struct_excessive_bools)]\n#![allow(clippy::upper_case_acronyms)]\n#![allow(clippy::wildcard_imports)]\n\n#![deny(clippy::cast_possible_truncation)]\n#![deny(clippy::cast_lossless)]\n#![deny(clippy::cast_possible_wrap)]\n#![deny(clippy::cast_sign_loss)]\n#![deny(unsafe_code)]\n\n\n//! The DNS crate is the ‘library’ part of dog. It implements the DNS\n//! protocol: creating and decoding packets from their byte structure.\n\n\nmod types;\npub use self::types::*;\n\nmod strings;\npub use self::strings::Labels;\n\nmod wire;\npub use self::wire::{Wire, WireError, MandatedLength};\n\npub mod record;\n"
  },
  {
    "path": "dns/src/record/a.rs",
    "content": "use std::net::Ipv4Addr;\n\nuse log::*;\n\nuse crate::wire::*;\n\n\n/// An **A** record type, which contains an `Ipv4Address`.\n///\n/// # References\n///\n/// - [RFC 1035 §3.4.1](https://tools.ietf.org/html/rfc1035) — Domain Names,\n///   Implementation and Specification (November 1987)\n#[derive(PartialEq, Debug, Copy, Clone)]\npub struct A {\n\n    /// The IPv4 address contained in the packet.\n    pub address: Ipv4Addr,\n}\n\nimpl Wire for A {\n    const NAME: &'static str = \"A\";\n    const RR_TYPE: u16 = 1;\n\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        if stated_length != 4 {\n            warn!(\"Length is incorrect (record length {:?}, but should be four)\", stated_length);\n            let mandated_length = MandatedLength::Exactly(4);\n            return Err(WireError::WrongRecordLength { stated_length, mandated_length });\n        }\n\n        let mut buf = [0_u8; 4];\n        c.read_exact(&mut buf)?;\n\n        let address = Ipv4Addr::from(buf);\n        trace!(\"Parsed IPv4 address -> {:?}\", address);\n\n        Ok(Self { address })\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn parses() {\n        let buf = &[\n            0x7F, 0x00, 0x00, 0x01,  // IPv4 address\n        ];\n\n        assert_eq!(A::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   A { address: Ipv4Addr::new(127, 0, 0, 1) });\n    }\n\n    #[test]\n    fn record_too_short() {\n        let buf = &[\n            0x7F, 0x00, 0x00,  // Too short IPv4 address\n        ];\n\n        assert_eq!(A::read(buf.len() as _, &mut Cursor::new(buf)),\n                   Err(WireError::WrongRecordLength { stated_length: 3, mandated_length: MandatedLength::Exactly(4) }));\n    }\n\n    #[test]\n    fn record_too_long() {\n        let buf = &[\n            0x7F, 0x00, 0x00, 0x00,  // IPv4 address\n            0x01,  // Unexpected extra byte\n        ];\n\n        assert_eq!(A::read(buf.len() as _, &mut Cursor::new(buf)),\n                   Err(WireError::WrongRecordLength { stated_length: 5, mandated_length: MandatedLength::Exactly(4) }));\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(A::read(0, &mut Cursor::new(&[])),\n                   Err(WireError::WrongRecordLength { stated_length: 0, mandated_length: MandatedLength::Exactly(4) }));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x7F, 0x00,  // Half an IPv4 address\n        ];\n\n        assert_eq!(A::read(4, &mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n}\n"
  },
  {
    "path": "dns/src/record/aaaa.rs",
    "content": "use std::net::Ipv6Addr;\n\nuse log::*;\n\nuse crate::wire::*;\n\n\n/// A **AAAA** record, which contains an `Ipv6Address`.\n///\n/// # References\n///\n/// - [RFC 3596](https://tools.ietf.org/html/rfc3596) — DNS Extensions to\n///   Support IP Version 6 (October 2003)\n#[derive(PartialEq, Debug, Copy, Clone)]\npub struct AAAA {\n\n    /// The IPv6 address contained in the packet.\n    pub address: Ipv6Addr,\n}\n\nimpl Wire for AAAA {\n    const NAME: &'static str = \"AAAA\";\n    const RR_TYPE: u16 = 28;\n\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        if stated_length != 16 {\n            warn!(\"Length is incorrect (stated length {:?}, but should be sixteen)\", stated_length);\n            let mandated_length = MandatedLength::Exactly(16);\n            return Err(WireError::WrongRecordLength { stated_length, mandated_length });\n        }\n\n        let mut buf = [0_u8; 16];\n        c.read_exact(&mut buf)?;\n\n        let address = Ipv6Addr::from(buf);\n        trace!(\"Parsed IPv6 address -> {:#x?}\", address);\n\n        Ok(Self { address })\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn parses() {\n        let buf = &[\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // IPv6 address\n        ];\n\n        assert_eq!(AAAA::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   AAAA { address: Ipv6Addr::new(0,0,0,0,0,0,0,0) });\n    }\n\n    #[test]\n    fn record_too_long() {\n        let buf = &[\n            0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,\n            0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09,  // IPv6 address\n            0x09,  // Unexpected extra byte\n        ];\n\n        assert_eq!(AAAA::read(buf.len() as _, &mut Cursor::new(buf)),\n                   Err(WireError::WrongRecordLength { stated_length: 17, mandated_length: MandatedLength::Exactly(16) }));\n    }\n\n    #[test]\n    fn record_too_short() {\n        let buf = &[\n            0x05, 0x05, 0x05, 0x05, 0x05,  // Five arbitrary bytes\n        ];\n\n        assert_eq!(AAAA::read(buf.len() as _, &mut Cursor::new(buf)),\n                   Err(WireError::WrongRecordLength { stated_length: 5, mandated_length: MandatedLength::Exactly(16) }));\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(AAAA::read(0, &mut Cursor::new(&[])),\n                   Err(WireError::WrongRecordLength { stated_length: 0, mandated_length: MandatedLength::Exactly(16) }));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x05, 0x05, 0x05, 0x05, 0x05,  // Five arbitrary bytes\n        ];\n\n        assert_eq!(AAAA::read(16, &mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n}\n"
  },
  {
    "path": "dns/src/record/caa.rs",
    "content": "use log::*;\n\nuse crate::wire::*;\n\n\n/// A **CAA** _(certification authority authorization)_ record. These allow\n/// domain names to specify which Certificate Authorities are allowed to issue\n/// certificates for the domain.\n///\n/// # References\n///\n/// - [RFC 6844](https://tools.ietf.org/html/rfc6844) — DNS Certification\n///   Authority Authorization Resource Record (January 2013)\n#[derive(PartialEq, Debug)]\npub struct CAA {\n\n    /// Whether this record is marked as “critical” or not.\n    pub critical: bool,\n\n    /// The “tag” part of the CAA record.\n    pub tag: Box<[u8]>,\n\n    /// The “value” part of the CAA record.\n    pub value: Box<[u8]>,\n}\n\nimpl Wire for CAA {\n    const NAME: &'static str = \"CAA\";\n    const RR_TYPE: u16 = 257;\n\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n\n        // flags\n        let flags = c.read_u8()?;\n        trace!(\"Parsed flags -> {:#08b}\", flags);\n\n        let has_bit = |bit| { flags & bit == bit };\n        let critical = has_bit(0b_1000_0000);\n        trace!(\"Parsed critical flag -> {:?}\", critical);\n\n        // tag\n        let tag_length = c.read_u8()?;\n        trace!(\"Parsed tag length -> {:?}\", tag_length);\n\n        let mut tag = vec![0_u8; usize::from(tag_length)].into_boxed_slice();\n        c.read_exact(&mut tag)?;\n        trace!(\"Parsed tag -> {:?}\", String::from_utf8_lossy(&tag));\n\n        // value\n        let remaining_length = stated_length.saturating_sub(u16::from(tag_length)).saturating_sub(2);\n        trace!(\"Remaining length -> {:?}\", remaining_length);\n\n        let mut value = vec![0_u8; usize::from(remaining_length)].into_boxed_slice();\n        c.read_exact(&mut value)?;\n        trace!(\"Parsed value -> {:?}\", String::from_utf8_lossy(&value));\n\n        Ok(Self { critical, tag, value })\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn parses_non_critical() {\n        let buf = &[\n            0x00,  // flags (all unset)\n            0x09,  // tag length\n            0x69, 0x73, 0x73, 0x75, 0x65, 0x77, 0x69, 0x6c, 0x64,  // tag\n            0x65, 0x6e, 0x74, 0x72, 0x75, 0x73, 0x74, 0x2e, 0x6e, 0x65, 0x74,  // value\n        ];\n\n        assert_eq!(CAA::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   CAA {\n                       critical: false,\n                       tag: Box::new(*b\"issuewild\"),\n                       value: Box::new(*b\"entrust.net\"),\n                   });\n    }\n\n    #[test]\n    fn parses_critical() {\n        let buf = &[\n            0x80,  // flags (critical bit set)\n            0x09,  // tag length\n            0x69, 0x73, 0x73, 0x75, 0x65, 0x77, 0x69, 0x6c, 0x64,  // tag\n            0x65, 0x6e, 0x74, 0x72, 0x75, 0x73, 0x74, 0x2e, 0x6e, 0x65, 0x74,  // value\n        ];\n\n        assert_eq!(CAA::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   CAA {\n                       critical: true,\n                       tag: Box::new(*b\"issuewild\"),\n                       value: Box::new(*b\"entrust.net\"),\n                   });\n    }\n\n    #[test]\n    fn ignores_other_flags() {\n        let buf = &[\n            0x7F,  // flags (all except critical bit set)\n            0x01,  // tag length\n            0x65,  // tag\n            0x45,  // value\n        ];\n\n        assert_eq!(CAA::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   CAA {\n                       critical: false,\n                       tag: Box::new(*b\"e\"),\n                       value: Box::new(*b\"E\"),\n                   });\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(CAA::read(0, &mut Cursor::new(&[])),\n                   Err(WireError::IO));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x00,  // flags\n        ];\n\n        assert_eq!(CAA::read(23, &mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n}\n"
  },
  {
    "path": "dns/src/record/cname.rs",
    "content": "use log::*;\n\nuse crate::strings::{Labels, ReadLabels};\nuse crate::wire::*;\n\n\n/// A **CNAME** _(canonical name)_ record, which aliases one domain to another.\n///\n/// # References\n///\n/// - [RFC 1035 §3.3.1](https://tools.ietf.org/html/rfc1035) — Domain Names,\n///   Implementation and Specification (November 1987)\n#[derive(PartialEq, Debug)]\npub struct CNAME {\n\n    /// The domain name that this CNAME record is responding with.\n    pub domain: Labels,\n}\n\nimpl Wire for CNAME {\n    const NAME: &'static str = \"CNAME\";\n    const RR_TYPE: u16 = 5;\n\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        let (domain, domain_length) = c.read_labels()?;\n        trace!(\"Parsed domain -> {:?}\", domain);\n\n        if stated_length == domain_length {\n            trace!(\"Length is correct\");\n            Ok(Self { domain })\n        }\n        else {\n            warn!(\"Length is incorrect (stated length {:?}, domain length {:?})\", stated_length, domain_length);\n            Err(WireError::WrongLabelLength { stated_length, length_after_labels: domain_length })\n        }\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn parses() {\n        let buf = &[\n            0x05, 0x62, 0x73, 0x61, 0x67, 0x6f, 0x02, 0x6d, 0x65,  // domain\n            0x00,  // domain terminator\n        ];\n\n        assert_eq!(CNAME::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   CNAME {\n                       domain: Labels::encode(\"bsago.me\").unwrap(),\n                   });\n    }\n\n    #[test]\n    fn incorrect_record_length() {\n        let buf = &[\n            0x03, 0x65, 0x66, 0x67,  // domain\n            0x00,  // domain terminator\n        ];\n\n        assert_eq!(CNAME::read(6, &mut Cursor::new(buf)),\n                   Err(WireError::WrongLabelLength { stated_length: 6, length_after_labels: 5 }));\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(CNAME::read(0, &mut Cursor::new(&[])),\n                   Err(WireError::IO));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x05, 0x62, 0x73,  // the stard of a string\n        ];\n\n        assert_eq!(CNAME::read(23, &mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n}\n\n"
  },
  {
    "path": "dns/src/record/eui48.rs",
    "content": "use log::*;\n\nuse crate::wire::*;\n\n\n/// A **EUI48** record, which holds a six-octet (48-bit) Extended Unique\n/// Identifier. These identifiers can be used as MAC addresses.\n///\n/// # References\n///\n/// - [RFC 7043](https://tools.ietf.org/html/rfc7043) — Resource Records for\n///   EUI-48 and EUI-64 Addresses in the DNS (October 2013)\n#[derive(PartialEq, Debug, Copy, Clone)]\npub struct EUI48 {\n\n    /// The six octets that make up the identifier.\n    pub octets: [u8; 6],\n}\n\nimpl Wire for EUI48 {\n    const NAME: &'static str = \"EUI48\";\n    const RR_TYPE: u16 = 108;\n\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        if stated_length != 6 {\n            warn!(\"Length is incorrect (record length {:?}, but should be six)\", stated_length);\n            let mandated_length = MandatedLength::Exactly(6);\n            return Err(WireError::WrongRecordLength { stated_length, mandated_length });\n        }\n\n        let mut octets = [0_u8; 6];\n        c.read_exact(&mut octets)?;\n        trace!(\"Parsed 6-byte address -> {:#x?}\", octets);\n\n        Ok(Self { octets })\n    }\n}\n\n\nimpl EUI48 {\n\n    /// Returns this EUI as hexadecimal numbers, separated by dashes.\n    pub fn formatted_address(self) -> String {\n        format!(\"{:02x}-{:02x}-{:02x}-{:02x}-{:02x}-{:02x}\",\n                self.octets[0], self.octets[1], self.octets[2],\n                self.octets[3], self.octets[4], self.octets[5])\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn parses() {\n        let buf = &[\n            0x00, 0x7F, 0x23, 0x12, 0x34, 0x56,  // identifier\n        ];\n\n        assert_eq!(EUI48::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   EUI48 { octets: [ 0x00, 0x7F, 0x23, 0x12, 0x34, 0x56 ] });\n    }\n\n    #[test]\n    fn record_too_short() {\n        let buf = &[\n            0x00, 0x7F, 0x23,  // a mere OUI\n        ];\n\n        assert_eq!(EUI48::read(buf.len() as _, &mut Cursor::new(buf)),\n                   Err(WireError::WrongRecordLength { stated_length: 3, mandated_length: MandatedLength::Exactly(6) }));\n    }\n\n    #[test]\n    fn record_too_long() {\n        let buf = &[\n            0x00, 0x7F, 0x23, 0x12, 0x34, 0x56,  // identifier\n            0x01,  // an unexpected extra byte\n        ];\n\n        assert_eq!(EUI48::read(buf.len() as _, &mut Cursor::new(buf)),\n                   Err(WireError::WrongRecordLength { stated_length: 7, mandated_length: MandatedLength::Exactly(6) }));\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(EUI48::read(0, &mut Cursor::new(&[])),\n                   Err(WireError::WrongRecordLength { stated_length: 0, mandated_length: MandatedLength::Exactly(6) }));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x00, 0x7F, 0x23,  // a mere OUI\n        ];\n\n        assert_eq!(EUI48::read(6, &mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n\n    #[test]\n    fn hex_rep() {\n        let record = EUI48 { octets: [ 0x00, 0x7F, 0x23, 0x12, 0x34, 0x56 ] };\n\n        assert_eq!(record.formatted_address(),\n                   \"00-7f-23-12-34-56\");\n    }\n}\n"
  },
  {
    "path": "dns/src/record/eui64.rs",
    "content": "use log::*;\n\nuse crate::wire::*;\n\n\n/// A **EUI64** record, which holds an eight-octet (64-bit) Extended Unique\n/// Identifier.\n///\n/// # References\n///\n/// - [RFC 7043](https://tools.ietf.org/html/rfc7043) — Resource Records for\n///   EUI-48 and EUI-64 Addresses in the DNS (October 2013)\n#[derive(PartialEq, Debug, Copy, Clone)]\npub struct EUI64 {\n\n    /// The eight octets that make up the identifier.\n    pub octets: [u8; 8],\n}\n\nimpl Wire for EUI64 {\n    const NAME: &'static str = \"EUI64\";\n    const RR_TYPE: u16 = 109;\n\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        if stated_length != 8 {\n            warn!(\"Length is incorrect (record length {:?}, but should be eight)\", stated_length);\n            let mandated_length = MandatedLength::Exactly(8);\n            return Err(WireError::WrongRecordLength { stated_length, mandated_length });\n        }\n\n        let mut octets = [0_u8; 8];\n        c.read_exact(&mut octets)?;\n        trace!(\"Parsed 8-byte address -> {:#x?}\", octets);\n\n        Ok(Self { octets })\n    }\n}\n\n\nimpl EUI64 {\n\n    /// Returns this EUI as hexadecimal numbers, separated by dashes.\n    pub fn formatted_address(self) -> String {\n        format!(\"{:02x}-{:02x}-{:02x}-{:02x}-{:02x}-{:02x}-{:02x}-{:02x}\",\n                self.octets[0], self.octets[1], self.octets[2], self.octets[3],\n                self.octets[4], self.octets[5], self.octets[6], self.octets[7])\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn parses() {\n        let buf = &[\n            0x00, 0x7F, 0x23, 0x12, 0x34, 0x56, 0x78, 0x90,  // identifier\n        ];\n\n        assert_eq!(EUI64::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   EUI64 { octets: [ 0x00, 0x7F, 0x23, 0x12, 0x34, 0x56, 0x78, 0x90 ] });\n    }\n\n    #[test]\n    fn record_too_short() {\n        let buf = &[\n            0x00, 0x7F, 0x23,  // a mere OUI\n        ];\n\n        assert_eq!(EUI64::read(buf.len() as _, &mut Cursor::new(buf)),\n                   Err(WireError::WrongRecordLength { stated_length: 3, mandated_length: MandatedLength::Exactly(8) }));\n    }\n\n    #[test]\n    fn record_too_long() {\n        let buf = &[\n            0x00, 0x7F, 0x23, 0x12, 0x34, 0x56, 0x78, 0x90,  // identifier\n            0x01,  // an unexpected extra byte\n        ];\n\n        assert_eq!(EUI64::read(buf.len() as _, &mut Cursor::new(buf)),\n                   Err(WireError::WrongRecordLength { stated_length: 9, mandated_length: MandatedLength::Exactly(8) }));\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(EUI64::read(0, &mut Cursor::new(&[])),\n                   Err(WireError::WrongRecordLength { stated_length: 0, mandated_length: MandatedLength::Exactly(8) }));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x00, 0x7F, 0x23,  // a mere OUI\n        ];\n\n        assert_eq!(EUI64::read(8, &mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n\n    #[test]\n    fn hex_rep() {\n        let record = EUI64 { octets: [ 0x00, 0x7F, 0x23, 0x12, 0x34, 0x56, 0x78, 0x90 ] };\n\n        assert_eq!(record.formatted_address(),\n                   \"00-7f-23-12-34-56-78-90\");\n    }\n}\n"
  },
  {
    "path": "dns/src/record/hinfo.rs",
    "content": "use log::*;\n\nuse crate::wire::*;\n\n\n/// A (an?) **HINFO** _(host information)_ record, which contains the CPU and\n/// OS information about a host.\n///\n/// It also gets used as the response for an `ANY` query, if it is blocked.\n///\n/// # References\n///\n/// - [RFC 1035 §3.3.2](https://tools.ietf.org/html/rfc1035) — Domain Names,\n///   Implementation and Specification (November 1987)\n/// - [RFC 8482 §6](https://tools.ietf.org/html/rfc8482#section-6) — Providing\n///   Minimal-Sized Responses to DNS Queries That Have QTYPE=ANY (January 2019)\n#[derive(PartialEq, Debug)]\npub struct HINFO {\n\n    /// The CPU field, specifying the CPU type.\n    pub cpu: Box<[u8]>,\n\n    /// The OS field, specifying the operating system.\n    pub os: Box<[u8]>,\n}\n\nimpl Wire for HINFO {\n    const NAME: &'static str = \"HINFO\";\n    const RR_TYPE: u16 = 13;\n\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n\n        let cpu_length = c.read_u8()?;\n        trace!(\"Parsed CPU length -> {:?}\", cpu_length);\n\n        let mut cpu = vec![0_u8; usize::from(cpu_length)].into_boxed_slice();\n        c.read_exact(&mut cpu)?;\n        trace!(\"Parsed CPU -> {:?}\", String::from_utf8_lossy(&cpu));\n\n        let os_length = c.read_u8()?;\n        trace!(\"Parsed OS length -> {:?}\", os_length);\n\n        let mut os = vec![0_u8; usize::from(os_length)].into_boxed_slice();\n        c.read_exact(&mut os)?;\n        trace!(\"Parsed OS -> {:?}\", String::from_utf8_lossy(&os));\n\n        let length_after_labels = 1 + u16::from(cpu_length) + 1 + u16::from(os_length);\n        if stated_length == length_after_labels {\n            trace!(\"Length is correct\");\n            Ok(Self { cpu, os })\n        }\n        else {\n            warn!(\"Length is incorrect (stated length {:?}, cpu plus length {:?}\", stated_length, length_after_labels);\n            Err(WireError::WrongLabelLength { stated_length, length_after_labels })\n        }\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn parses() {\n        let buf = &[\n            0x0e,  // cpu length\n            0x73, 0x6f, 0x6d, 0x65, 0x2d, 0x6b, 0x69, 0x6e, 0x64, 0x61, 0x2d,\n            0x63, 0x70, 0x75,  // cpu\n            0x0d,  // os length\n            0x73, 0x6f, 0x6d, 0x65, 0x2d, 0x6b, 0x69, 0x6e, 0x64, 0x61, 0x2d,\n            0x6f, 0x73,  // os\n        ];\n\n        assert_eq!(HINFO::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   HINFO {\n                       cpu: Box::new(*b\"some-kinda-cpu\"),\n                       os: Box::new(*b\"some-kinda-os\"),\n                   });\n    }\n\n    #[test]\n    fn incorrect_record_length() {\n        let buf = &[\n            0x03,  // cpu length\n            0x65, 0x66, 0x67,  // cpu\n            0x03,  // os length\n            0x68, 0x69, 0x70,  // os\n        ];\n\n        assert_eq!(HINFO::read(6, &mut Cursor::new(buf)),\n                   Err(WireError::WrongLabelLength { stated_length: 6, length_after_labels: 8 }));\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(HINFO::read(0, &mut Cursor::new(&[])),\n                   Err(WireError::IO));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x14, 0x0A, 0x0B, 0x0C,  // 32-bit CPU\n        ];\n\n        assert_eq!(HINFO::read(23, &mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n}\n"
  },
  {
    "path": "dns/src/record/loc.rs",
    "content": "use std::fmt;\n\nuse log::*;\n\nuse crate::wire::*;\n\n\n/// A **LOC** _(location)_ record, which points to a location on Earth using\n/// its latitude, longitude, and altitude.\n///\n/// # References\n///\n/// - [RFC 1876](https://tools.ietf.org/html/rfc1876) — A Means for Expressing\n///   Location Information in the Domain Name System (January 1996)\n#[derive(PartialEq, Debug, Copy, Clone)]\npub struct LOC {\n\n    /// The diameter of a sphere enclosing the entity at the location, as a\n    /// measure of its size, measured in centimetres.\n    pub size: Size,\n\n    /// The diameter of the “circle of error” that this location could be in,\n    /// measured in centimetres.\n    pub horizontal_precision: u8,\n\n    /// The amount of vertical space that this location could be in, measured\n    /// in centimetres.\n    pub vertical_precision: u8,\n\n    /// The latitude of the centre of the sphere. If `None`, the packet\n    /// parses, but the position is out of range.\n    pub latitude: Option<Position>,\n\n    /// The longitude of the centre of the sphere. If `None`, the packet\n    /// parses, but the position is out of range.\n    pub longitude: Option<Position>,\n\n    /// The altitude of the centre of the sphere, measured in centimetres\n    /// above a base of 100,000 metres below the GPS reference spheroid.\n    pub altitude: Altitude,\n}\n\n/// A measure of size, in centimetres, represented by a base and an exponent.\n#[derive(PartialEq, Debug, Copy, Clone)]\npub struct Size {\n    base: u8,\n    power_of_ten: u8,\n}\n\n/// A position on one of the world’s axes.\n#[derive(PartialEq, Debug, Copy, Clone)]\npub struct Position {\n    degrees: u32,\n    arcminutes: u32,\n    arcseconds: u32,\n    milliarcseconds: u32,\n    direction: Direction,\n}\n\n/// A position on the vertical axis.\n#[derive(PartialEq, Debug, Copy, Clone)]\npub struct Altitude {\n    metres: i64,\n    centimetres: i64,\n}\n\n/// One of the directions a position could be in, relative to the equator or\n/// prime meridian.\n#[derive(PartialEq, Debug, Copy, Clone)]\npub enum Direction {\n    North,\n    East,\n    South,\n    West,\n}\n\nimpl Wire for LOC {\n    const NAME: &'static str = \"LOC\";\n    const RR_TYPE: u16 = 29;\n\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        let version = c.read_u8()?;\n        trace!(\"Parsed version -> {:?}\", version);\n\n        if version != 0 {\n            return Err(WireError::WrongVersion {\n                stated_version: version,\n                maximum_supported_version: 0,\n            });\n        }\n\n        if stated_length != 16 {\n            let mandated_length = MandatedLength::Exactly(16);\n            return Err(WireError::WrongRecordLength { stated_length, mandated_length });\n        }\n\n        let size_bits = c.read_u8()?;\n        let size = Size::from_u8(size_bits);\n        trace!(\"Parsed size -> {:#08b} ({})\", size_bits, size);\n\n        let horizontal_precision = c.read_u8()?;\n        trace!(\"Parsed horizontal precision -> {:?}\", horizontal_precision);\n\n        let vertical_precision = c.read_u8()?;\n        trace!(\"Parsed vertical precision -> {:?}\", vertical_precision);\n\n        let latitude_num = c.read_u32::<BigEndian>()?;\n        let latitude = Position::from_u32(latitude_num, true);\n        trace!(\"Parsed latitude -> {:?} ({:?})\", latitude_num, latitude);\n\n        let longitude_num = c.read_u32::<BigEndian>()?;\n        let longitude = Position::from_u32(longitude_num, false);\n        trace!(\"Parsed longitude -> {:?} ({:?})\", longitude_num, longitude);\n\n        let altitude_num = c.read_u32::<BigEndian>()?;\n        let altitude = Altitude::from_u32(altitude_num);\n        trace!(\"Parsed altitude -> {:?} ({:})\", altitude_num, altitude);\n\n        Ok(Self {\n            size, horizontal_precision, vertical_precision, latitude, longitude, altitude,\n        })\n    }\n}\n\nimpl Size {\n\n    /// Converts a number into the size it represents. To allow both small and\n    /// large sizes, the input octet is split into two four-bit sizes, one the\n    /// base, and one the power of ten exponent.\n    fn from_u8(input: u8) -> Self {\n        let base = input >> 4;\n        let power_of_ten = input & 0b_0000_1111;\n        Self { base, power_of_ten }\n    }\n}\n\nimpl Position {\n\n    /// Converts a number into the position it represents. The input number is\n    /// measured in thousandths of an arcsecond (milliarcseconds), with 2^31\n    /// as the equator or prime meridian.\n    ///\n    /// Returns `None` if the input is out of range, meaning it would wrap\n    /// around to another half of the Earth once or more.\n    fn from_u32(mut input: u32, vertical: bool) -> Option<Self> {\n        let max_for_direction = if vertical { 90 } else { 180 };\n        let limit = 1000 * 60 * 60 * max_for_direction;\n\n        if input < (0x_8000_0000 - limit) || input > (0x_8000_0000 + limit) {\n            // Input is out of range\n            None\n        }\n        else if input >= 0x_8000_0000 {\n            // Input is north or east, so de-relativise it and divide into segments\n            input -= 0x_8000_0000;\n            let milliarcseconds = input % 1000;\n            let total_arcseconds = input / 1000;\n\n            let arcseconds = total_arcseconds % 60;\n            let total_arcminutes = total_arcseconds / 60;\n\n            let arcminutes = total_arcminutes % 60;\n            let degrees = total_arcminutes / 60;\n\n            let direction = if vertical { Direction::North }\n                                   else { Direction::East };\n\n            Some(Self { degrees, arcminutes, arcseconds, milliarcseconds, direction })\n        }\n        else {\n            // Input is south or west, so do the calculations for\n            let mut pos = Self::from_u32(input + (0x_8000_0000_u32 - input) * 2, vertical)?;\n\n            pos.direction = if vertical { Direction::South }\n                                   else { Direction::West };\n            Some(pos)\n        }\n    }\n}\n\nimpl Altitude {\n    fn from_u32(input: u32) -> Self {\n        let mut input = i64::from(input);\n        input -= 10_000_000;  // 100,000m\n        let metres = input / 100;\n        let centimetres = input % 100;\n        Self { metres, centimetres }\n    }\n}\n\n\nimpl fmt::Display for Size {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"{}e{}\", self.base, self.power_of_ten)\n    }\n}\n\nimpl fmt::Display for Position {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"{}°{}′{}\",\n            self.degrees,\n            self.arcminutes,\n            self.arcseconds,\n        )?;\n\n        if self.milliarcseconds != 0 {\n            write!(f, \".{:03}\", self.milliarcseconds)?;\n        }\n\n        write!(f, \"″ {}\", self.direction)\n    }\n}\n\nimpl fmt::Display for Direction {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::North  => write!(f, \"N\"),\n            Self::East   => write!(f, \"E\"),\n            Self::South  => write!(f, \"S\"),\n            Self::West   => write!(f, \"W\"),\n        }\n    }\n}\n\nimpl fmt::Display for Altitude {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        // Usually there’s a space between the number and the unit, but\n        // spaces are already used to delimit segments in the record summary\n        if self.centimetres == 0 {\n            write!(f, \"{}m\", self.metres)\n        }\n        else {\n            write!(f, \"{}.{:02}m\", self.metres, self.centimetres)\n        }\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn parses() {\n        let buf = &[\n            0x00,  // version\n            0x32,  // size,\n            0x00,  // horizontal precision\n            0x00,  // vertical precision\n            0x8b, 0x0d, 0x2c, 0x8c,  // latitude\n            0x7f, 0xf8, 0xfc, 0xa5,  // longitude\n            0x00, 0x98, 0x96, 0x80,  // altitude\n        ];\n\n        assert_eq!(LOC::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   LOC {\n                       size: Size { base: 3, power_of_ten: 2 },\n                       horizontal_precision: 0,\n                       vertical_precision: 0,\n                       latitude:  Position::from_u32(0x_8b_0d_2c_8c, true),\n                       longitude: Position::from_u32(0x_7f_f8_fc_a5, false),\n                       altitude:  Altitude::from_u32(0x_00_98_96_80),\n                   });\n    }\n\n    #[test]\n    fn record_too_short() {\n        let buf = &[\n            0x00,  // version\n            0x00,  // size\n        ];\n\n        assert_eq!(LOC::read(buf.len() as _, &mut Cursor::new(buf)),\n                   Err(WireError::WrongRecordLength { stated_length: 2, mandated_length: MandatedLength::Exactly(16) }));\n    }\n\n    #[test]\n    fn record_too_long() {\n        let buf = &[\n            0x00,  // version\n            0x32,  // size,\n            0x00,  // horizontal precision\n            0x00,  // vertical precision\n            0x8b, 0x0d, 0x2c, 0x8c,  // latitude\n            0x7f, 0xf8, 0xfc, 0xa5,  // longitude\n            0x00, 0x98, 0x96, 0x80,  // altitude\n            0x12, 0x34, 0x56,  // some other stuff\n        ];\n\n        assert_eq!(LOC::read(buf.len() as _, &mut Cursor::new(buf)),\n                   Err(WireError::WrongRecordLength { stated_length: 19, mandated_length: MandatedLength::Exactly(16) }));\n    }\n\n    #[test]\n    fn more_recent_version() {\n        let buf = &[\n            0x80,  // version\n            0x12, 0x34, 0x56,  // some data in an unknown format\n        ];\n\n        assert_eq!(LOC::read(buf.len() as _, &mut Cursor::new(buf)),\n                   Err(WireError::WrongVersion { stated_version: 128, maximum_supported_version: 0 }));\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(LOC::read(0, &mut Cursor::new(&[])),\n                   Err(WireError::IO));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x00,  // version\n        ];\n\n        assert_eq!(LOC::read(16, &mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n}\n\n\n#[cfg(test)]\nmod size_test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn zeroes() {\n        assert_eq!(Size::from_u8(0b_0000_0000).to_string(),\n                   String::from(\"0e0\"));\n    }\n\n    #[test]\n    fn ones() {\n        assert_eq!(Size::from_u8(0b_0001_0001).to_string(),\n                   String::from(\"1e1\"));\n    }\n\n    #[test]\n    fn schfourteen_teen() {\n        assert_eq!(Size::from_u8(0b_1110_0011).to_string(),\n                   String::from(\"14e3\"));\n    }\n\n    #[test]\n    fn ones_but_bits_this_time() {\n        assert_eq!(Size::from_u8(0b_1111_1111).to_string(),\n                   String::from(\"15e15\"));\n    }\n}\n\n\n#[cfg(test)]\nmod position_test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    // centre line tests\n\n    #[test]\n    fn meridian() {\n        assert_eq!(Position::from_u32(0x_8000_0000, false).unwrap().to_string(),\n                   String::from(\"0°0′0″ E\"));\n    }\n\n    #[test]\n    fn meridian_plus_one() {\n        assert_eq!(Position::from_u32(0x_8000_0000 + 1, false).unwrap().to_string(),\n                   String::from(\"0°0′0.001″ E\"));\n    }\n\n    #[test]\n    fn meridian_minus_one() {\n        assert_eq!(Position::from_u32(0x_8000_0000 - 1, false).unwrap().to_string(),\n                   String::from(\"0°0′0.001″ W\"));\n    }\n\n    #[test]\n    fn equator() {\n        assert_eq!(Position::from_u32(0x_8000_0000, true).unwrap().to_string(),\n                   String::from(\"0°0′0″ N\"));\n    }\n\n    #[test]\n    fn equator_plus_one() {\n        assert_eq!(Position::from_u32(0x_8000_0000 + 1, true).unwrap().to_string(),\n                   String::from(\"0°0′0.001″ N\"));\n    }\n\n    #[test]\n    fn equator_minus_one() {\n        assert_eq!(Position::from_u32(0x_8000_0000 - 1, true).unwrap().to_string(),\n                   String::from(\"0°0′0.001″ S\"));\n    }\n\n    // arbitrary value tests\n\n    #[test]\n    fn some_latitude() {\n        assert_eq!(Position::from_u32(2332896396, true).unwrap().to_string(),\n                   String::from(\"51°30′12.748″ N\"));\n    }\n\n    #[test]\n    fn some_longitude() {\n        assert_eq!(Position::from_u32(2147024037, false).unwrap().to_string(),\n                   String::from(\"0°7′39.611″ W\"));\n    }\n\n    // limit tests\n\n    #[test]\n    fn the_north_pole() {\n        assert_eq!(Position::from_u32(0x8000_0000 + (1000 * 60 * 60 * 90), true).unwrap().to_string(),\n                   String::from(\"90°0′0″ N\"));\n    }\n\n    #[test]\n    fn the_north_pole_plus_one() {\n        assert_eq!(Position::from_u32(0x8000_0000 + (1000 * 60 * 60 * 90) + 1, true),\n                   None);\n    }\n\n    #[test]\n    fn the_south_pole() {\n        assert_eq!(Position::from_u32(0x8000_0000 - (1000 * 60 * 60 * 90), true).unwrap().to_string(),\n                   String::from(\"90°0′0″ S\"));\n    }\n\n    #[test]\n    fn the_south_pole_minus_one() {\n        assert_eq!(Position::from_u32(0x8000_0000 - (1000 * 60 * 60 * 90) - 1, true),\n                   None);\n    }\n\n    #[test]\n    fn the_far_east() {\n        assert_eq!(Position::from_u32(0x8000_0000 + (1000 * 60 * 60 * 180), false).unwrap().to_string(),\n                   String::from(\"180°0′0″ E\"));\n    }\n\n    #[test]\n    fn the_far_east_plus_one() {\n        assert_eq!(Position::from_u32(0x8000_0000 + (1000 * 60 * 60 * 180) + 1, false),\n                   None);\n    }\n\n    #[test]\n    fn the_far_west() {\n        assert_eq!(Position::from_u32(0x8000_0000 - (1000 * 60 * 60 * 180), false).unwrap().to_string(),\n                   String::from(\"180°0′0″ W\"));\n    }\n\n    #[test]\n    fn the_far_west_minus_one() {\n        assert_eq!(Position::from_u32(0x8000_0000 - (1000 * 60 * 60 * 180) - 1, false),\n                   None);\n    }\n}\n\n\n#[cfg(test)]\nmod altitude_test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn base_level() {\n        assert_eq!(Altitude::from_u32(10000000).to_string(),\n                   String::from(\"0m\"));\n    }\n\n    #[test]\n    fn up_high() {\n        assert_eq!(Altitude::from_u32(20000000).to_string(),\n                   String::from(\"100000m\"));\n    }\n\n    #[test]\n    fn down_low() {\n        assert_eq!(Altitude::from_u32(0).to_string(),\n                   String::from(\"-100000m\"));\n    }\n\n    #[test]\n    fn with_decimal() {\n        assert_eq!(Altitude::from_u32(50505050).to_string(),\n                   String::from(\"405050.50m\"));\n    }\n}\n"
  },
  {
    "path": "dns/src/record/mod.rs",
    "content": "//! All the DNS record types, as well as how to parse each type.\n\nuse crate::wire::*;\n\n\nmod a;\npub use self::a::A;\n\nmod aaaa;\npub use self::aaaa::AAAA;\n\nmod caa;\npub use self::caa::CAA;\n\nmod cname;\npub use self::cname::CNAME;\n\nmod eui48;\npub use self::eui48::EUI48;\n\nmod eui64;\npub use self::eui64::EUI64;\n\nmod hinfo;\npub use self::hinfo::HINFO;\n\nmod loc;\npub use self::loc::LOC;\n\nmod mx;\npub use self::mx::MX;\n\nmod naptr;\npub use self::naptr::NAPTR;\n\nmod ns;\npub use self::ns::NS;\n\nmod openpgpkey;\npub use self::openpgpkey::OPENPGPKEY;\n\nmod opt;\npub use self::opt::OPT;\n\nmod ptr;\npub use self::ptr::PTR;\n\nmod sshfp;\npub use self::sshfp::SSHFP;\n\nmod soa;\npub use self::soa::SOA;\n\nmod srv;\npub use self::srv::SRV;\n\nmod tlsa;\npub use self::tlsa::TLSA;\n\nmod txt;\npub use self::txt::TXT;\n\nmod uri;\npub use self::uri::URI;\n\n\nmod others;\npub use self::others::UnknownQtype;\n\n\n/// A record that’s been parsed from a byte buffer.\n#[derive(PartialEq, Debug)]\n#[allow(missing_docs)]\npub enum Record {\n    A(A),\n    AAAA(AAAA),\n    CAA(CAA),\n    CNAME(CNAME),\n    EUI48(EUI48),\n    EUI64(EUI64),\n    HINFO(HINFO),\n    LOC(LOC),\n    MX(MX),\n    NAPTR(NAPTR),\n    NS(NS),\n    OPENPGPKEY(OPENPGPKEY),\n    // OPT is not included here.\n    PTR(PTR),\n    SSHFP(SSHFP),\n    SOA(SOA),\n    SRV(SRV),\n    TLSA(TLSA),\n    TXT(TXT),\n    URI(URI),\n\n    /// A record with a type that we don’t recognise.\n    Other {\n\n        /// The number that’s meant to represent the record type.\n        type_number: UnknownQtype,\n\n        /// The undecodable bytes that were in this record.\n        bytes: Vec<u8>,\n    },\n}\n\n\n/// The type of a record that may or may not be one of the known ones. Has no\n/// data associated with it other than what type of record it is.\n#[derive(PartialEq, Debug, Copy, Clone)]\n#[allow(missing_docs)]\npub enum RecordType {\n    A,\n    AAAA,\n    CAA,\n    CNAME,\n    EUI48,\n    EUI64,\n    HINFO,\n    LOC,\n    MX,\n    NAPTR,\n    NS,\n    OPENPGPKEY,\n    PTR,\n    SSHFP,\n    SOA,\n    SRV,\n    TLSA,\n    TXT,\n    URI,\n\n    /// A record type we don’t recognise.\n    Other(UnknownQtype),\n}\n\nimpl From<u16> for RecordType {\n    fn from(type_number: u16) -> Self {\n        macro_rules! try_record {\n            ($record:tt) => {\n                if $record::RR_TYPE == type_number {\n                    return RecordType::$record;\n                }\n            }\n        }\n\n        try_record!(A);\n        try_record!(AAAA);\n        try_record!(CAA);\n        try_record!(CNAME);\n        try_record!(EUI48);\n        try_record!(EUI64);\n        try_record!(HINFO);\n        try_record!(LOC);\n        try_record!(MX);\n        try_record!(NAPTR);\n        try_record!(NS);\n        try_record!(OPENPGPKEY);\n        // OPT is handled separately\n        try_record!(PTR);\n        try_record!(SSHFP);\n        try_record!(SOA);\n        try_record!(SRV);\n        try_record!(TLSA);\n        try_record!(TXT);\n        try_record!(URI);\n\n        RecordType::Other(UnknownQtype::from(type_number))\n    }\n}\n\n\nimpl RecordType {\n\n    /// Determines the record type with a given name, or `None` if none is\n    /// known. Matches names case-insensitively.\n    pub fn from_type_name(type_name: &str) -> Option<Self> {\n        macro_rules! try_record {\n            ($record:tt) => {\n                if $record::NAME.eq_ignore_ascii_case(type_name) {\n                    return Some(Self::$record);\n                }\n            }\n        }\n\n        try_record!(A);\n        try_record!(AAAA);\n        try_record!(CAA);\n        try_record!(CNAME);\n        try_record!(EUI48);\n        try_record!(EUI64);\n        try_record!(HINFO);\n        try_record!(LOC);\n        try_record!(MX);\n        try_record!(NAPTR);\n        try_record!(NS);\n        try_record!(OPENPGPKEY);\n        // OPT is elsewhere\n        try_record!(PTR);\n        try_record!(SSHFP);\n        try_record!(SOA);\n        try_record!(SRV);\n        try_record!(TLSA);\n        try_record!(TXT);\n        try_record!(URI);\n\n        UnknownQtype::from_type_name(type_name).map(Self::Other)\n    }\n\n    /// Returns the record type number associated with this record type.\n    pub fn type_number(self) -> u16 {\n        match self {\n            Self::A           => A::RR_TYPE,\n            Self::AAAA        => AAAA::RR_TYPE,\n            Self::CAA         => CAA::RR_TYPE,\n            Self::CNAME       => CNAME::RR_TYPE,\n            Self::EUI48       => EUI48::RR_TYPE,\n            Self::EUI64       => EUI64::RR_TYPE,\n            Self::HINFO       => HINFO::RR_TYPE,\n            Self::LOC         => LOC::RR_TYPE,\n            Self::MX          => MX::RR_TYPE,\n            Self::NAPTR       => NAPTR::RR_TYPE,\n            Self::NS          => NS::RR_TYPE,\n            Self::OPENPGPKEY  => OPENPGPKEY::RR_TYPE,\n            // Wherefore art thou, OPT\n            Self::PTR         => PTR::RR_TYPE,\n            Self::SSHFP       => SSHFP::RR_TYPE,\n            Self::SOA         => SOA::RR_TYPE,\n            Self::SRV         => SRV::RR_TYPE,\n            Self::TLSA        => TLSA::RR_TYPE,\n            Self::TXT         => TXT::RR_TYPE,\n            Self::URI         => URI::RR_TYPE,\n            Self::Other(o)    => o.type_number(),\n        }\n    }\n}\n\n// This code is really repetitive, I know, I know\n"
  },
  {
    "path": "dns/src/record/mx.rs",
    "content": "use log::*;\n\nuse crate::strings::{Labels, ReadLabels};\nuse crate::wire::*;\n\n\n/// An **MX** _(mail exchange)_ record, which contains the hostnames for mail\n/// servers that handle mail sent to the domain.\n///\n/// # References\n///\n/// - [RFC 1035 §3.3.9](https://tools.ietf.org/html/rfc1035) — Domain Names,\n///   Implementation and Specification (November 1987)\n#[derive(PartialEq, Debug)]\npub struct MX {\n\n    /// The preference that clients should give to this MX record amongst all\n    /// that get returned.\n    pub preference: u16,\n\n    /// The domain name of the mail exchange server.\n    pub exchange: Labels,\n}\n\nimpl Wire for MX {\n    const NAME: &'static str = \"MX\";\n    const RR_TYPE: u16 = 15;\n\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        let preference = c.read_u16::<BigEndian>()?;\n        trace!(\"Parsed preference -> {:?}\", preference);\n\n        let (exchange, exchange_length) = c.read_labels()?;\n        trace!(\"Parsed exchange -> {:?}\", exchange);\n\n        let length_after_labels = 2 + exchange_length;\n        if stated_length == length_after_labels {\n            trace!(\"Length is correct\");\n            Ok(Self { preference, exchange })\n        }\n        else {\n            warn!(\"Length is incorrect (stated length {:?}, preference plus exchange length {:?}\", stated_length, length_after_labels);\n            Err(WireError::WrongLabelLength { stated_length, length_after_labels })\n        }\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn parses() {\n        let buf = &[\n            0x00, 0x0A,  // preference\n            0x05, 0x62, 0x73, 0x61, 0x67, 0x6f, 0x02, 0x6d, 0x65,  // exchange\n            0x00,  // exchange terminator\n        ];\n\n        assert_eq!(MX::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   MX {\n                       preference: 10,\n                       exchange: Labels::encode(\"bsago.me\").unwrap(),\n                   });\n    }\n\n    #[test]\n    fn incorrect_record_length() {\n        let buf = &[\n            0x00, 0x0A,  // preference\n            0x03, 0x65, 0x66, 0x67,  // domain\n            0x00,  // domain terminator\n        ];\n\n        assert_eq!(MX::read(6, &mut Cursor::new(buf)),\n                   Err(WireError::WrongLabelLength { stated_length: 6, length_after_labels: 7 }));\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(MX::read(0, &mut Cursor::new(&[])),\n                   Err(WireError::IO));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x00, 0x0A,  // half a preference\n        ];\n\n        assert_eq!(MX::read(23, &mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n}\n"
  },
  {
    "path": "dns/src/record/naptr.rs",
    "content": "use log::*;\n\nuse crate::strings::{Labels, ReadLabels};\nuse crate::wire::*;\n\n\n/// A **NAPTR** _(naming authority pointer)_ record, which holds a rule for\n/// the Dynamic Delegation Discovery System.\n///\n/// # References\n///\n/// - [RFC 3403](https://tools.ietf.org/html/rfc3403) — Dynamic Delegation\n///   Discovery System (DDDS) Part Three: The Domain Name System (DNS) Database\n///   (October 2002)\n#[derive(PartialEq, Debug)]\npub struct NAPTR {\n\n    /// The order in which NAPTR records must be processed.\n    pub order: u16,\n\n    /// The DDDS priority.\n    pub preference: u16,\n\n    /// A set of characters that control the rewriting and interpretation of\n    /// the other fields.\n    pub flags: Box<[u8]>,\n\n    /// The service parameters applicable to this delegation path.\n    pub service: Box<[u8]>,\n\n    /// A regular expression that gets applied to a string in order to\n    /// construct the next domain name to look up using the DDDS algorithm.\n    pub regex: Box<[u8]>,\n\n    /// The replacement domain name as part of the DDDS algorithm.\n    pub replacement: Labels,\n}\n\nimpl Wire for NAPTR {\n    const NAME: &'static str = \"NAPTR\";\n    const RR_TYPE: u16 = 35;\n\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        let order = c.read_u16::<BigEndian>()?;\n        trace!(\"Parsed order -> {:?}\", order);\n\n        // preference\n        let preference = c.read_u16::<BigEndian>()?;\n        trace!(\"Parsed preference -> {:?}\", preference);\n\n        // flags\n        let flags_length = c.read_u8()?;\n        trace!(\"Parsed flags length -> {:?}\", flags_length);\n\n        let mut flags = vec![0_u8; usize::from(flags_length)].into_boxed_slice();\n        c.read_exact(&mut flags)?;\n        trace!(\"Parsed flags -> {:?}\", String::from_utf8_lossy(&flags));\n\n        // service\n        let service_length = c.read_u8()?;\n        trace!(\"Parsed service length -> {:?}\", service_length);\n\n        let mut service = vec![0_u8; usize::from(service_length)].into_boxed_slice();\n        c.read_exact(&mut service)?;\n        trace!(\"Parsed service -> {:?}\", String::from_utf8_lossy(&service));\n\n        // regex\n        let regex_length = c.read_u8()?;\n        trace!(\"Parsed regex length -> {:?}\", regex_length);\n\n        let mut regex = vec![0_u8; usize::from(regex_length)].into_boxed_slice();\n        c.read_exact(&mut regex)?;\n        trace!(\"Parsed regex -> {:?}\", String::from_utf8_lossy(&regex));\n\n        // replacement\n        let (replacement, replacement_length) = c.read_labels()?;\n        trace!(\"Parsed replacement -> {:?}\", replacement);\n\n        let length_after_labels = 2 + 2 +\n            1 + u16::from(flags_length) + 1 + u16::from(service_length) +\n            1 + u16::from(regex_length) + replacement_length;\n\n        if stated_length == length_after_labels {\n            Ok(Self { order, preference, flags, service, regex, replacement })\n        }\n        else {\n            Err(WireError::WrongLabelLength { stated_length, length_after_labels })\n        }\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn parses() {\n        let buf = &[\n            0x00, 0x05,  // order\n            0x00, 0x0a,  // preference\n            0x01,  // flags length\n            0x73,  // flags\n            0x03,  // service length\n            0x53, 0x52, 0x56,  // service\n            0x0e,  // regex length\n            0x5c, 0x64, 0x5c, 0x64, 0x3a, 0x5c, 0x64, 0x5c, 0x64, 0x3a, 0x5c,\n            0x64, 0x5c, 0x64,  // regex\n            0x0b, 0x73, 0x72, 0x76, 0x2d, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c,\n            0x65, 0x06, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x03, 0x64, 0x6f,\n            0x67, 0x00,  // replacement\n        ];\n\n        assert_eq!(NAPTR::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   NAPTR {\n                       order: 5,\n                       preference: 10,\n                       flags: Box::new(*b\"s\"),\n                       service: Box::new(*b\"SRV\"),\n                       regex: Box::new(*b\"\\\\d\\\\d:\\\\d\\\\d:\\\\d\\\\d\"),\n                       replacement: Labels::encode(\"srv-example.lookup.dog\").unwrap(),\n                   });\n    }\n\n    #[test]\n    fn incorrect_length() {\n        let buf = &[\n            0x00, 0x05,  // order\n            0x00, 0x0a,  // preference\n            0x01,  // flags length\n            0x73,  // flags\n            0x03,  // service length\n            0x53, 0x52, 0x56,  // service\n            0x01,  // regex length\n            0x64,  // regex,\n            0x00,  // replacement\n        ];\n\n        assert_eq!(NAPTR::read(11, &mut Cursor::new(buf)),\n                   Err(WireError::WrongLabelLength { stated_length: 11, length_after_labels: 13 }));\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(NAPTR::read(0, &mut Cursor::new(&[])),\n                   Err(WireError::IO));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x00, 0x0A,  // order\n        ];\n\n        assert_eq!(NAPTR::read(23, &mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n}\n"
  },
  {
    "path": "dns/src/record/ns.rs",
    "content": "use log::*;\n\nuse crate::strings::{Labels, ReadLabels};\nuse crate::wire::*;\n\n\n/// A **NS** _(name server)_ record, which is used to point domains to name\n/// servers.\n///\n/// # References\n///\n/// - [RFC 1035 §3.3.11](https://tools.ietf.org/html/rfc1035) — Domain Names,\n///   Implementation and Specification (November 1987)\n#[derive(PartialEq, Debug)]\npub struct NS {\n\n    /// The address of a nameserver that provides this DNS response.\n    pub nameserver: Labels,\n}\n\nimpl Wire for NS {\n    const NAME: &'static str = \"NS\";\n    const RR_TYPE: u16 = 2;\n\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        let (nameserver, nameserver_length) = c.read_labels()?;\n        trace!(\"Parsed nameserver -> {:?}\", nameserver);\n\n        if stated_length == nameserver_length {\n            trace!(\"Length is correct\");\n            Ok(Self { nameserver })\n        }\n        else {\n            warn!(\"Length is incorrect (stated length {:?}, nameserver length {:?}\", stated_length, nameserver_length);\n            Err(WireError::WrongLabelLength { stated_length, length_after_labels: nameserver_length })\n        }\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn parses() {\n        let buf = &[\n            0x01, 0x61, 0x0c, 0x67, 0x74, 0x6c, 0x64, 0x2d, 0x73, 0x65, 0x72,\n            0x76, 0x65, 0x72, 0x73, 0x03, 0x6e, 0x65, 0x74,  // nameserver\n            0x00,  // nameserver terminator\n        ];\n\n        assert_eq!(NS::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   NS {\n                       nameserver: Labels::encode(\"a.gtld-servers.net\").unwrap(),\n                   });\n    }\n\n    #[test]\n    fn incorrect_record_length() {\n        let buf = &[\n            0x03, 0x65, 0x66, 0x67,  // nameserver\n            0x00,  // nameserver terminator\n        ];\n\n        assert_eq!(NS::read(66, &mut Cursor::new(buf)),\n                   Err(WireError::WrongLabelLength { stated_length: 66, length_after_labels: 5 }));\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(NS::read(0, &mut Cursor::new(&[])),\n                   Err(WireError::IO));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x01,  // the first byte of a string\n        ];\n\n        assert_eq!(NS::read(23, &mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n}\n"
  },
  {
    "path": "dns/src/record/openpgpkey.rs",
    "content": "use log::*;\n\nuse crate::wire::*;\n\n\n/// A **OPENPGPKEY** record, which holds a PGP key.\n///\n/// # References\n///\n/// - [RFC 1035 §3.3.14](https://tools.ietf.org/html/rfc7929) — DNS-Based\n///   Authentication of Named Entities Bindings for OpenPGP (August 2016)\n#[derive(PartialEq, Debug)]\npub struct OPENPGPKEY {\n\n    /// The PGP key, as unencoded bytes.\n    pub key: Vec<u8>,\n}\n\nimpl Wire for OPENPGPKEY {\n    const NAME: &'static str = \"OPENPGPKEY\";\n    const RR_TYPE: u16 = 61;\n\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        if stated_length == 0 {\n            let mandated_length = MandatedLength::AtLeast(1);\n            return Err(WireError::WrongRecordLength { stated_length, mandated_length });\n        }\n\n        let mut key = vec![0_u8; usize::from(stated_length)];\n        c.read_exact(&mut key)?;\n        trace!(\"Parsed key -> {:#x?}\", key);\n\n        Ok(Self { key })\n    }\n}\n\nimpl OPENPGPKEY {\n\n    /// The base64-encoded PGP key.\n    pub fn base64_key(&self) -> String {\n        base64::encode(&self.key)\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn parses() {\n        let buf = &[\n            0x12, 0x34, 0x56, 0x78,  // key\n        ];\n\n        assert_eq!(OPENPGPKEY::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   OPENPGPKEY {\n                       key: vec![ 0x12, 0x34, 0x56, 0x78 ],\n                   });\n    }\n\n    #[test]\n    fn one_byte_of_uri() {\n        let buf = &[\n            0x2b,  // one byte of key\n        ];\n\n        assert_eq!(OPENPGPKEY::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   OPENPGPKEY {\n                       key: vec![ 0x2b ],\n                   });\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(OPENPGPKEY::read(0, &mut Cursor::new(&[])),\n                   Err(WireError::WrongRecordLength { stated_length: 0, mandated_length: MandatedLength::AtLeast(1) }));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x12, 0x34,  // the beginning of a key\n        ];\n\n        assert_eq!(OPENPGPKEY::read(23, &mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n}\n"
  },
  {
    "path": "dns/src/record/opt.rs",
    "content": "use std::convert::TryFrom;\nuse std::io;\n\nuse log::*;\n\nuse crate::wire::*;\n\n\n/// A **OPT** _(options)_ pseudo-record, which is used to extend the DNS\n/// protocol with additional flags such as DNSSEC stuff.\n///\n/// # Pseudo-record?\n///\n/// Unlike all the other record types, which are used to return data about a\n/// domain name, the OPT record type is used to add more options to the\n/// request, including data about the client or the server. It can exist, with\n/// a payload, as a query or a response, though it’s usually encountered in\n/// the Additional section. Its purpose is to add more room to the DNS wire\n/// format, as backwards compatibility makes it impossible to simply add more\n/// flags to the header.\n///\n/// The fact that this isn’t a standard record type is annoying for a DNS\n/// implementation. It re-purposes the ‘class’ and ‘TTL’ fields of the\n/// `Answer` struct, as they only have meaning when associated with a domain\n/// name. This means that the parser has to treat the OPT type specially,\n/// switching to `Opt::read` as soon as the rtype is detected. It also means\n/// the output has to deal with missing classes and TTLs.\n///\n/// # References\n///\n/// - [RFC 6891](https://tools.ietf.org/html/rfc6891) — Extension Mechanisms\n///   for DNS (April 2013)\n#[derive(PartialEq, Debug, Clone)]\npub struct OPT {\n\n    /// The maximum size of a UDP packet that the client supports.\n    pub udp_payload_size: u16,\n\n    /// The bits that form an extended rcode when non-zero.\n    pub higher_bits: u8,\n\n    /// The version number of the DNS extension mechanism.\n    pub edns0_version: u8,\n\n    /// Sixteen bits worth of flags.\n    pub flags: u16,\n\n    /// The payload of the OPT record.\n    pub data: Vec<u8>,\n}\n\nimpl OPT {\n\n    /// The record type number associated with OPT.\n    pub const RR_TYPE: u16 = 41;\n\n    /// Reads from the given cursor to parse an OPT record.\n    ///\n    /// The buffer will have slightly more bytes to read for an OPT record\n    /// than for a typical one: we will not have encountered the ‘class’ or\n    /// ‘ttl’ fields, which have different meanings for this record type.\n    /// See §6.1.3 of the RFC, “OPT Record TTL Field Use”.\n    ///\n    /// Unlike the `Wire::read` function, this does not require a length.\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    pub fn read(c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        let udp_payload_size = c.read_u16::<BigEndian>()?;  // replaces the class field\n        trace!(\"Parsed UDP payload size -> {:?}\", udp_payload_size);\n\n        let higher_bits = c.read_u8()?;  // replaces the ttl field...\n        trace!(\"Parsed higher bits -> {:#08b}\", higher_bits);\n\n        let edns0_version = c.read_u8()?;  // ...as does this...\n        trace!(\"Parsed EDNS(0) version -> {:?}\", edns0_version);\n\n        let flags = c.read_u16::<BigEndian>()?;  // ...as does this\n        trace!(\"Parsed flags -> {:#08b}\", flags);\n\n        let data_length = c.read_u16::<BigEndian>()?;\n        trace!(\"Parsed data length -> {:?}\", data_length);\n\n        let mut data = vec![0_u8; usize::from(data_length)];\n        c.read_exact(&mut data)?;\n        trace!(\"Parsed data -> {:#x?}\", data);\n\n        Ok(Self { udp_payload_size, higher_bits, edns0_version, flags, data })\n    }\n\n    /// Serialises this OPT record into a vector of bytes.\n    ///\n    /// This is necessary for OPT records to be sent in the Additional section\n    /// of requests.\n    pub fn to_bytes(&self) -> io::Result<Vec<u8>> {\n        let mut bytes = Vec::with_capacity(32);\n\n        bytes.write_u16::<BigEndian>(self.udp_payload_size)?;\n        bytes.write_u8(self.higher_bits)?;\n        bytes.write_u8(self.edns0_version)?;\n        bytes.write_u16::<BigEndian>(self.flags)?;\n\n        // We should not be sending any data at all in the request, really,\n        // so sending too much data is downright nonsensical\n        let data_len = u16::try_from(self.data.len()).expect(\"Sending too much data\");\n        bytes.write_u16::<BigEndian>(data_len)?;\n\n        for b in &self.data {\n            bytes.write_u8(*b)?;\n        }\n\n        Ok(bytes)\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn parses_no_data() {\n        let buf = &[\n            0x05, 0xAC,  // UDP payload size\n            0x00,        // higher bits\n            0x00, 0x00,  // EDNS(0) version\n            0x00, 0x00,  // flags\n            0x00,        // data length (followed by no data)\n        ];\n\n        assert_eq!(OPT::read(&mut Cursor::new(buf)).unwrap(),\n                   OPT {\n                       udp_payload_size: 1452,\n                       higher_bits: 0,\n                       edns0_version: 0,\n                       flags: 0,\n                       data: vec![],\n                   });\n    }\n\n    #[test]\n    fn parses_with_data() {\n        let buf = &[\n            0x05, 0xAC,  // UDP payload size\n            0x00,        // higher bits\n            0x00, 0x00,  // EDNS(0) version\n            0x00, 0x00,  // flags\n            0x04,        // data length\n            0x01, 0x02, 0x03, 0x04,  // data\n        ];\n\n        assert_eq!(OPT::read(&mut Cursor::new(buf)).unwrap(),\n                   OPT {\n                       udp_payload_size: 1452,\n                       higher_bits: 0,\n                       edns0_version: 0,\n                       flags: 0,\n                       data: vec![1, 2, 3, 4],\n                   });\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(OPT::read(&mut Cursor::new(&[])),\n                   Err(WireError::IO));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x05,  // half a UDP payload size\n        ];\n\n        assert_eq!(OPT::read(&mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n}\n"
  },
  {
    "path": "dns/src/record/others.rs",
    "content": "use std::fmt;\n\n\n/// A number representing a record type dog can’t deal with.\n#[derive(PartialEq, Debug, Copy, Clone)]\npub enum UnknownQtype {\n\n    /// An rtype number that dog is aware of, but does not know how to parse.\n    HeardOf(&'static str, u16),\n\n    /// A completely unknown rtype number.\n    UnheardOf(u16),\n}\n\nimpl UnknownQtype {\n\n    /// Searches the list for an unknown type with the given name, returning a\n    /// `HeardOf` variant if one is found, and `None` otherwise.\n    pub fn from_type_name(type_name: &str) -> Option<Self> {\n        let (name, num) = TYPES.iter().find(|t| t.0.eq_ignore_ascii_case(type_name))?;\n        Some(Self::HeardOf(name, *num))\n    }\n\n    /// Returns the type number behind this unknown type.\n    pub fn type_number(self) -> u16 {\n        match self {\n            Self::HeardOf(_, num) |\n            Self::UnheardOf(num)  => num,\n        }\n    }\n}\n\nimpl From<u16> for UnknownQtype {\n    fn from(qtype: u16) -> Self {\n        match TYPES.iter().find(|t| t.1 == qtype) {\n            Some(tuple)  => Self::HeardOf(tuple.0, qtype),\n            None         => Self::UnheardOf(qtype),\n        }\n    }\n}\n\nimpl fmt::Display for UnknownQtype {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::HeardOf(name, _)  => write!(f, \"{}\", name),\n            Self::UnheardOf(num)    => write!(f, \"{}\", num),\n        }\n    }\n}\n\n\n/// Mapping of record type names to their assigned numbers.\nstatic TYPES: &[(&str, u16)] = &[\n    (\"AFSDB\",      18),\n    (\"ANY\",       255),\n    (\"APL\",        42),\n    (\"AXFR\",      252),\n    (\"CDNSKEY\",    60),\n    (\"CDS\",        59),\n    (\"CERT\",       37),\n    (\"CSYNC\",      62),\n    (\"DHCID\",      49),\n    (\"DLV\",     32769),\n    (\"DNAME\",      39),\n    (\"DNSKEEYE\",   48),\n    (\"DS\",         43),\n    (\"HIP\",        55),\n    (\"IPSECKEY\",   45),\n    (\"IXFR\",      251),\n    (\"KEY\",        25),\n    (\"KX\",         36),\n    (\"NSEC\",       47),\n    (\"NSEC3\",      50),\n    (\"NSEC3PARAM\", 51),\n    (\"OPENPGPKEY\", 61),\n    (\"RRSIG\",      46),\n    (\"RP\",         17),\n    (\"SIG\",        24),\n    (\"SMIMEA\",     53),\n    (\"TA\",      32768),\n    (\"TKEY\",      249),\n    (\"TSIG\",      250),\n    (\"URI\",       256),\n];\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[test]\n    fn known() {\n        assert_eq!(UnknownQtype::from(46).to_string(),\n                   String::from(\"RRSIG\"));\n    }\n\n    #[test]\n    fn unknown() {\n        assert_eq!(UnknownQtype::from(4444).to_string(),\n                   String::from(\"4444\"));\n    }\n}\n"
  },
  {
    "path": "dns/src/record/ptr.rs",
    "content": "use log::*;\n\nuse crate::strings::{Labels, ReadLabels};\nuse crate::wire::*;\n\n\n/// A **PTR** record, which holds a _pointer_ to a canonical name. This is\n/// most often used for reverse DNS lookups.\n///\n/// # Encoding\n///\n/// The text encoding is not specified, but this crate treats it as UTF-8.\n/// Invalid bytes are turned into the replacement character.\n///\n/// # References\n///\n/// - [RFC 1035 §3.3.14](https://tools.ietf.org/html/rfc1035) — Domain Names,\n///   Implementation and Specification (November 1987)\n#[derive(PartialEq, Debug)]\npub struct PTR {\n\n    /// The CNAME contained in the record.\n    pub cname: Labels,\n}\n\nimpl Wire for PTR {\n    const NAME: &'static str = \"PTR\";\n    const RR_TYPE: u16 = 12;\n\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        let (cname, cname_length) = c.read_labels()?;\n        trace!(\"Parsed cname -> {:?}\", cname);\n\n        if stated_length == cname_length {\n            trace!(\"Length is correct\");\n            Ok(Self { cname })\n        }\n        else {\n            warn!(\"Length is incorrect (stated length {:?}, cname length {:?}\", stated_length, cname_length);\n            Err(WireError::WrongLabelLength { stated_length, length_after_labels: cname_length })\n        }\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn parses() {\n        let buf = &[\n            0x03, 0x64, 0x6e, 0x73, 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,  // cname\n            0x00,  // cname terminator\n        ];\n\n        assert_eq!(PTR::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   PTR {\n                       cname: Labels::encode(\"dns.google\").unwrap(),\n                   });\n    }\n\n    #[test]\n    fn incorrect_record_length() {\n        let buf = &[\n            0x03, 0x65, 0x66, 0x67,  // cname\n            0x00,  // cname terminator\n        ];\n\n        assert_eq!(PTR::read(6, &mut Cursor::new(buf)),\n                   Err(WireError::WrongLabelLength { stated_length: 6, length_after_labels: 5 }));\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(PTR::read(0, &mut Cursor::new(&[])),\n                   Err(WireError::IO));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x03, 0x64,  // the start of a cname\n        ];\n\n        assert_eq!(PTR::read(23, &mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n}\n"
  },
  {
    "path": "dns/src/record/soa.rs",
    "content": "use log::*;\n\nuse crate::strings::{Labels, ReadLabels};\nuse crate::wire::*;\n\n\n/// A **SOA** _(start of authority)_ record, which contains administrative\n/// information about the zone the domain is in. These are returned when a\n/// server does not have a record for a domain.\n///\n/// # References\n///\n/// - [RFC 1035 §3.3.13](https://tools.ietf.org/html/rfc1035) — Domain Names,\n///   Implementation and Specification (November 1987)\n#[derive(PartialEq, Debug)]\npub struct SOA {\n\n    /// The primary master name for this server.\n    pub mname: Labels,\n\n    /// The e-mail address of the administrator responsible for this DNS zone.\n    pub rname: Labels,\n\n    /// A serial number for this DNS zone.\n    pub serial: u32,\n\n    /// Duration, in seconds, after which secondary nameservers should query\n    /// the master for _its_ SOA record.\n    pub refresh_interval: u32,\n\n    /// Duration, in seconds, after which secondary nameservers should retry\n    /// requesting the serial number from the master if it does not respond.\n    /// It should be less than `refresh`.\n    pub retry_interval: u32,\n\n    /// Duration, in seconds, after which secondary nameservers should stop\n    /// answering requests for this zone if the master does not respond.\n    /// It should be greater than the sum of `refresh` and `retry`.\n    pub expire_limit: u32,\n\n    /// Duration, in seconds, of the minimum time-to-live.\n    pub minimum_ttl: u32,\n}\n\nimpl Wire for SOA {\n    const NAME: &'static str = \"SOA\";\n    const RR_TYPE: u16 = 6;\n\n    #[allow(clippy::similar_names)]\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        let (mname, mname_length) = c.read_labels()?;\n        trace!(\"Parsed mname -> {:?}\", mname);\n\n        let (rname, rname_length) = c.read_labels()?;\n        trace!(\"Parsed rname -> {:?}\", rname);\n\n        let serial = c.read_u32::<BigEndian>()?;\n        trace!(\"Parsed serial -> {:?}\", serial);\n\n        let refresh_interval = c.read_u32::<BigEndian>()?;\n        trace!(\"Parsed refresh interval -> {:?}\", refresh_interval);\n\n        let retry_interval = c.read_u32::<BigEndian>()?;\n        trace!(\"Parsed retry interval -> {:?}\", retry_interval);\n\n        let expire_limit = c.read_u32::<BigEndian>()?;\n        trace!(\"Parsed expire limit -> {:?}\", expire_limit);\n\n        let minimum_ttl = c.read_u32::<BigEndian>()?;\n        trace!(\"Parsed minimum TTL -> {:?}\", minimum_ttl);\n\n        let length_after_labels = 4 * 5 + mname_length + rname_length;\n        if stated_length == length_after_labels {\n            trace!(\"Length is correct\");\n            Ok(Self {\n                mname, rname, serial, refresh_interval,\n                retry_interval, expire_limit, minimum_ttl,\n            })\n        }\n        else {\n            warn!(\"Length is incorrect (stated length {:?}, mname plus rname plus fields length {:?})\", stated_length, length_after_labels);\n            Err(WireError::WrongLabelLength { stated_length, length_after_labels })\n        }\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn parses() {\n        let buf = &[\n            0x05, 0x62, 0x73, 0x61, 0x67, 0x6f, 0x02, 0x6d, 0x65,  // mname\n            0x00,  // mname terminator\n            0x05, 0x62, 0x73, 0x61, 0x67, 0x6f, 0x02, 0x6d, 0x65,  // rname\n            0x00,  // rname terminator\n            0x5d, 0x3c, 0xef, 0x02,  // Serial\n            0x00, 0x01, 0x51, 0x80,  // Refresh interval\n            0x00, 0x00, 0x1c, 0x20,  // Retry interval\n            0x00, 0x09, 0x3a, 0x80,  // Expire limit\n            0x00, 0x00, 0x01, 0x2c,  // Minimum TTL\n        ];\n\n        assert_eq!(SOA::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   SOA {\n                       mname: Labels::encode(\"bsago.me\").unwrap(),\n                       rname: Labels::encode(\"bsago.me\").unwrap(),\n                       serial: 1564274434,\n                       refresh_interval: 86400,\n                       retry_interval: 7200,\n                       expire_limit: 604800,\n                       minimum_ttl: 300,\n                   });\n    }\n\n    #[test]\n    fn incorrect_record_length() {\n        let buf = &[\n            0x03, 0x65, 0x66, 0x67,  // mname\n            0x00,  // mname terminator\n            0x03, 0x65, 0x66, 0x67,  // rname\n            0x00,  // rname terminator\n            0x5d, 0x3c, 0xef, 0x02,  // Serial\n            0x00, 0x01, 0x51, 0x80,  // Refresh interval\n            0x00, 0x00, 0x1c, 0x20,  // Retry interval\n            0x00, 0x09, 0x3a, 0x80,  // Expire limit\n            0x00, 0x00, 0x01, 0x2c,  // Minimum TTL\n        ];\n\n        assert_eq!(SOA::read(89, &mut Cursor::new(buf)),\n                   Err(WireError::WrongLabelLength { stated_length: 89, length_after_labels: 30 }));\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(SOA::read(0, &mut Cursor::new(&[])),\n                   Err(WireError::IO));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x05, 0x62,  // the start of an mname\n        ];\n\n        assert_eq!(SOA::read(23, &mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n}\n"
  },
  {
    "path": "dns/src/record/srv.rs",
    "content": "use log::*;\n\nuse crate::strings::{Labels, ReadLabels};\nuse crate::wire::*;\n\n\n/// A **SRV** record, which contains an IP address as well as a port number,\n/// for specifying the location of services more precisely.\n///\n/// # References\n///\n/// - [RFC 2782](https://tools.ietf.org/html/rfc2782) — A DNS RR for\n///   specifying the location of services (February 2000)\n#[derive(PartialEq, Debug)]\npub struct SRV {\n\n    /// The priority of this host among all that get returned. Lower values\n    /// are higher priority.\n    pub priority: u16,\n\n    /// A weight to choose among results with the same priority. Higher values\n    /// are higher priority.\n    pub weight: u16,\n\n    /// The port the service is serving on.\n    pub port: u16,\n\n    /// The hostname of the machine the service is running on.\n    pub target: Labels,\n}\n\nimpl Wire for SRV {\n    const NAME: &'static str = \"SRV\";\n    const RR_TYPE: u16 = 33;\n\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        let priority = c.read_u16::<BigEndian>()?;\n        trace!(\"Parsed priority -> {:?}\", priority);\n\n        let weight = c.read_u16::<BigEndian>()?;\n        trace!(\"Parsed weight -> {:?}\", weight);\n\n        let port = c.read_u16::<BigEndian>()?;\n        trace!(\"Parsed port -> {:?}\", port);\n\n        let (target, target_length) = c.read_labels()?;\n        trace!(\"Parsed target -> {:?}\", target);\n\n        let length_after_labels = 3 * 2 + target_length;\n        if stated_length == length_after_labels {\n            trace!(\"Length is correct\");\n            Ok(Self { priority, weight, port, target })\n        }\n        else {\n            warn!(\"Length is incorrect (stated length {:?}, fields plus target length {:?})\", stated_length, length_after_labels);\n            Err(WireError::WrongLabelLength { stated_length, length_after_labels })\n        }\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn parses() {\n        let buf = &[\n            0x00, 0x01,  // priority\n            0x00, 0x01,  // weight\n            0x92, 0x7c,  // port\n            0x03, 0x61, 0x74, 0x61, 0x05, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x04,\n            0x6e, 0x6f, 0x64, 0x65, 0x03, 0x64, 0x63, 0x31, 0x06, 0x63, 0x6f,\n            0x6e, 0x73, 0x75, 0x6c,  // target\n            0x00,  // target terminator\n        ];\n\n        assert_eq!(SRV::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   SRV {\n                       priority: 1,\n                       weight: 1,\n                       port: 37500,\n                       target: Labels::encode(\"ata.local.node.dc1.consul\").unwrap(),\n                   });\n    }\n\n    #[test]\n    fn incorrect_record_length() {\n        let buf = &[\n            0x00, 0x01,  // priority\n            0x00, 0x01,  // weight\n            0x92, 0x7c,  // port\n            0x03, 0x61, 0x74, 0x61,  // target\n            0x00,  // target terminator\n        ];\n\n        assert_eq!(SRV::read(16, &mut Cursor::new(buf)),\n                   Err(WireError::WrongLabelLength { stated_length: 16, length_after_labels: 11 }));\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(SRV::read(0, &mut Cursor::new(&[])),\n                   Err(WireError::IO));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x00,  // half a priority\n        ];\n\n        assert_eq!(SRV::read(23, &mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n}\n"
  },
  {
    "path": "dns/src/record/sshfp.rs",
    "content": "use log::*;\n\nuse crate::wire::*;\n\n\n/// A **SSHFP** _(secure shell fingerprint)_ record, which contains the\n/// fingerprint of an SSH public key.\n///\n/// # References\n///\n/// - [RFC 4255](https://tools.ietf.org/html/rfc4255) — Using DNS to Securely\n///   Publish Secure Shell (SSH) Key Fingerprints (January 2006)\n#[derive(PartialEq, Debug)]\npub struct SSHFP {\n\n    /// The algorithm of the public key. This is a number with several defined\n    /// mappings.\n    pub algorithm: u8,\n\n    /// The type of the fingerprint, which specifies the hashing algorithm\n    /// used to derive the fingerprint. This is a number with several defined\n    /// mappings.\n    pub fingerprint_type: u8,\n\n    /// The fingerprint of the public key.\n    pub fingerprint: Vec<u8>,\n}\n\nimpl Wire for SSHFP {\n    const NAME: &'static str = \"SSHFP\";\n    const RR_TYPE: u16 = 44;\n\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        let algorithm = c.read_u8()?;\n        trace!(\"Parsed algorithm -> {:?}\", algorithm);\n\n        let fingerprint_type = c.read_u8()?;\n        trace!(\"Parsed fingerprint type -> {:?}\", fingerprint_type);\n\n        if stated_length <= 2 {\n            let mandated_length = MandatedLength::AtLeast(3);\n            return Err(WireError::WrongRecordLength { stated_length, mandated_length });\n        }\n\n        let fingerprint_length = stated_length - 1 - 1;\n        let mut fingerprint = vec![0_u8; usize::from(fingerprint_length)];\n        c.read_exact(&mut fingerprint)?;\n        trace!(\"Parsed fingerprint -> {:#x?}\", fingerprint);\n\n        Ok(Self { algorithm, fingerprint_type, fingerprint })\n    }\n}\n\nimpl SSHFP {\n\n    /// Returns the hexadecimal representation of the fingerprint.\n    pub fn hex_fingerprint(&self) -> String {\n        self.fingerprint.iter()\n            .map(|byte| format!(\"{:02x}\", byte))\n            .collect()\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[test]\n    fn parses() {\n        let buf = &[\n            0x01,  // algorithm\n            0x01,  // fingerprint type\n            0x21, 0x22, 0x23, 0x24, 0x25, 0x26,  // a short fingerprint\n        ];\n\n        assert_eq!(SSHFP::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   SSHFP {\n                       algorithm: 1,\n                       fingerprint_type: 1,\n                       fingerprint: vec![ 0x21, 0x22, 0x23, 0x24, 0x25, 0x26 ],\n                   });\n    }\n\n    #[test]\n    fn one_byte_fingerprint() {\n        let buf = &[\n            0x01,  // algorithm\n            0x01,  // fingerprint type\n            0x21,  // an extremely short fingerprint\n        ];\n\n        assert_eq!(SSHFP::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   SSHFP {\n                       algorithm: 1,\n                       fingerprint_type: 1,\n                       fingerprint: vec![ 0x21 ],\n                   });\n    }\n\n    #[test]\n    fn record_too_short() {\n        let buf = &[\n            0x01,  // algorithm\n            0x01,  // fingerprint type\n        ];\n\n        assert_eq!(SSHFP::read(buf.len() as _, &mut Cursor::new(buf)),\n                   Err(WireError::WrongRecordLength { stated_length: 2, mandated_length: MandatedLength::AtLeast(3) }));\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(SSHFP::read(0, &mut Cursor::new(&[])),\n                   Err(WireError::IO));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x01,  // algorithm\n        ];\n\n        assert_eq!(SSHFP::read(6, &mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n\n    #[test]\n    fn hex_rep() {\n        let sshfp = SSHFP {\n            algorithm: 1,\n            fingerprint_type: 1,\n            fingerprint: vec![ 0xf3, 0x48, 0xcd, 0xc9 ],\n        };\n\n        assert_eq!(sshfp.hex_fingerprint(),\n                   String::from(\"f348cdc9\"));\n    }\n}\n"
  },
  {
    "path": "dns/src/record/tlsa.rs",
    "content": "use log::*;\n\nuse crate::wire::*;\n\n\n/// A **TLSA** _(TLS authentication)_ record, which contains a TLS certificate\n/// (or a public key, or its hash), associating it with a domain.\n///\n/// # References\n///\n/// - [RFC 6698](https://tools.ietf.org/html/rfc6698) — The DNS-Based\n///   Authentication of Named Entities (DANE) Transport Layer Security\n///   Protocol: TLSA (August 2012)\n#[derive(PartialEq, Debug)]\npub struct TLSA {\n\n    /// A number representing the purpose of the certificate.\n    pub certificate_usage: u8,\n\n    /// A number representing which part of the certificate is returned in the\n    /// data. This could be the full certificate, or just the public key.\n    pub selector: u8,\n\n    /// A number representing whether a certificate should be associated with\n    /// the exact data, or with a hash of it.\n    pub matching_type: u8,\n\n    /// A series of bytes representing the certificate.\n    pub certificate_data: Vec<u8>,\n}\n\n\nimpl Wire for TLSA {\n    const NAME: &'static str = \"TLSA\";\n    const RR_TYPE: u16 = 52;\n\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n\n        let certificate_usage = c.read_u8()?;\n        trace!(\"Parsed certificate_usage -> {:?}\", certificate_usage);\n\n        let selector = c.read_u8()?;\n        trace!(\"Parsed selector -> {:?}\", selector);\n\n        let matching_type = c.read_u8()?;\n        trace!(\"Parsed matching type -> {:?}\", matching_type);\n\n        if stated_length <= 3 {\n            let mandated_length = MandatedLength::AtLeast(4);\n            return Err(WireError::WrongRecordLength { stated_length, mandated_length });\n        }\n\n        let certificate_data_length = stated_length - 1 - 1 - 1;\n        let mut certificate_data = vec![0_u8; usize::from(certificate_data_length)];\n        c.read_exact(&mut certificate_data)?;\n        trace!(\"Parsed fingerprint -> {:#x?}\", certificate_data);\n\n        Ok(Self { certificate_usage, selector, matching_type, certificate_data })\n    }\n}\n\nimpl TLSA {\n\n    /// Returns the hexadecimal representation of the fingerprint.\n    pub fn hex_certificate_data(&self) -> String {\n        self.certificate_data.iter()\n            .map(|byte| format!(\"{:02x}\", byte))\n            .collect()\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[test]\n    fn parses() {\n        let buf = &[\n            0x03,  // certificate usage\n            0x01,  // selector\n            0x01,  // matching type\n            0x05, 0x95, 0x98, 0x11, 0x22, 0x33 // data\n        ];\n\n        assert_eq!(TLSA::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   TLSA {\n                       certificate_usage: 3,\n                       selector: 1,\n                       matching_type: 1,\n                       certificate_data: vec![ 0x05, 0x95, 0x98, 0x11, 0x22, 0x33 ],\n                   });\n    }\n\n    #[test]\n    fn one_byte_certificate() {\n        let buf = &[\n            0x03,  // certificate usage\n            0x01,  // selector\n            0x01,  // matching type\n            0x05,  // one byte of data\n        ];\n\n        assert_eq!(TLSA::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   TLSA {\n                       certificate_usage: 3,\n                       selector: 1,\n                       matching_type: 1,\n                       certificate_data: vec![ 0x05 ],\n                   });\n    }\n\n    #[test]\n    fn record_too_short() {\n        let buf = &[\n            0x03,  // certificate usage\n            0x01,  // selector\n            0x01,  // matching type\n        ];\n\n        assert_eq!(TLSA::read(buf.len() as _, &mut Cursor::new(buf)),\n                   Err(WireError::WrongRecordLength { stated_length: 3, mandated_length: MandatedLength::AtLeast(4) }));\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(TLSA::read(0, &mut Cursor::new(&[])),\n                   Err(WireError::IO));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x01,  // certificate_usage\n        ];\n\n        assert_eq!(TLSA::read(6, &mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n}\n\n"
  },
  {
    "path": "dns/src/record/txt.rs",
    "content": "use log::*;\n\nuse crate::wire::*;\n\n\n/// A **TXT** record, which holds arbitrary descriptive text.\n///\n/// # Encoding\n///\n/// The text encoding is not specified, but this crate treats it as UTF-8.\n/// Invalid bytes are turned into the replacement character.\n///\n/// # References\n///\n/// - [RFC 1035 §3.3.14](https://tools.ietf.org/html/rfc1035) — Domain Names,\n///   Implementation and Specification (November 1987)\n#[derive(PartialEq, Debug)]\npub struct TXT {\n\n    /// The messages contained in the record.\n    pub messages: Vec<Box<[u8]>>,\n}\n\nimpl Wire for TXT {\n    const NAME: &'static str = \"TXT\";\n    const RR_TYPE: u16 = 16;\n\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        let mut messages = Vec::new();\n        let mut total_length = 0_u16;\n\n        loop {\n            let mut buf = Vec::new();\n\n            loop {\n                let next_length = c.read_u8()?;\n                total_length += u16::from(next_length) + 1;\n                trace!(\"Parsed slice length -> {:?} (total so far {:?})\", next_length, total_length);\n\n                for _ in 0 .. next_length {\n                    buf.push(c.read_u8()?);\n                }\n\n                if next_length < 255 {\n                    break;\n                }\n                else {\n                    trace!(\"Got length 255, so looping\");\n                }\n            }\n\n            let message = buf.into_boxed_slice();\n            trace!(\"Parsed message -> {:?}\", String::from_utf8_lossy(&message));\n            messages.push(message);\n\n            if total_length >= stated_length {\n                break;\n            }\n        }\n\n        if stated_length == total_length {\n            trace!(\"Length is correct\");\n            Ok(Self { messages })\n        }\n        else {\n            warn!(\"Length is incorrect (stated length {:?}, messages length {:?})\", stated_length, total_length);\n            Err(WireError::WrongLabelLength { stated_length, length_after_labels: total_length })\n        }\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn parses_one_iteration() {\n        let buf = &[\n            0x06,  // message chunk length\n            0x74, 0x78, 0x74, 0x20, 0x6d, 0x65,  // message chunk\n        ];\n\n        assert_eq!(TXT::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   TXT {\n                       messages: vec![ Box::new(*b\"txt me\") ],\n                   });\n    }\n\n    #[test]\n    fn parses_two_iterations() {\n        let buf = &[\n            0xFF,  // message chunk length\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41,\n            0x41, 0x41,  // exactly two hundred and fifty five ‘A’s (screaming)\n            0x04,  // message chunk length\n            0x41, 0x41, 0x41, 0x41,  // four more ‘A’s (the scream abruptly stops)\n        ];\n\n        assert_eq!(TXT::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   TXT {\n                       messages: vec![\n                           Box::new(*b\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\\n                                       AAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\\n                                       AAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\\n                                       AAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\\n                                       AAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\\n                                       AAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\\n                                       AAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\\n                                       AAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\\n                                       AAAAAAAAAAAAAAAAAAAAAAAAAAA\"),\n                       ],\n                   });\n        // did you know you can just _write_ code like this, and nobody will stop you?\n    }\n\n    #[test]\n    fn right_at_the_limit() {\n        let buf = &[\n            0xFE,  // message chunk length\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,\n            0x42,  // exactly two hundred and fifty four ‘B’s (a hive)\n        ];\n\n        assert_eq!(TXT::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   TXT {\n                       messages: vec![\n                           Box::new(*b\"BBBBBBBBBBBBBBBBBBBBBBBBBBBBB\\\n                                       BBBBBBBBBBBBBBBBBBBBBBBBBBBBB\\\n                                       BBBBBBBBBBBBBBBBBBBBBBBBBBBBB\\\n                                       BBBBBBBBBBBBBBBBBBBBBBBBBBBBB\\\n                                       BBBBBBBBBBBBBBBBBBBBBBBBBBBBB\\\n                                       BBBBBBBBBBBBBBBBBBBBBBBBBBBBB\\\n                                       BBBBBBBBBBBBBBBBBBBBBBBBBBBBB\\\n                                       BBBBBBBBBBBBBBBBBBBBBBBBBBBBB\\\n                                       BBBBBBBBBBBBBBBBBBBBBB\"),\n                       ],\n                   });\n    }\n\n    #[test]\n    fn another_message() {\n        let buf = &[\n            0x06,  // message chunk length\n            0x74, 0x78, 0x74, 0x20, 0x6d, 0x65,  // message chunk\n            0x06,  // message chunk length\n            0x79, 0x61, 0x20, 0x62, 0x65, 0x62,  // message chunk\n        ];\n\n        assert_eq!(TXT::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   TXT {\n                       messages: vec![\n                           Box::new(*b\"txt me\"),\n                           Box::new(*b\"ya beb\"),\n                       ],\n                   });\n    }\n\n    #[test]\n    fn length_too_short() {\n        let buf = &[\n            0x06,  // message chunk length\n            0x74, 0x78, 0x74, 0x20, 0x6d, 0x65,  // message chunk\n        ];\n\n        assert_eq!(TXT::read(2, &mut Cursor::new(buf)),\n                   Err(WireError::WrongLabelLength { stated_length: 2, length_after_labels: 7 }));\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(TXT::read(0, &mut Cursor::new(&[])),\n                   Err(WireError::IO));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x06, 0x74,  // the start of a message\n        ];\n\n        assert_eq!(TXT::read(23, &mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n}\n"
  },
  {
    "path": "dns/src/record/uri.rs",
    "content": "use log::*;\n\nuse crate::wire::*;\n\n\n/// A **URI** record, which holds a URI along with weight and priority values\n/// to balance between several records.\n///\n/// # References\n///\n/// - [RFC 7553](https://tools.ietf.org/html/rfc7553) — The Uniform Resource\n///   Identifier (URI) DNS Resource Record (June 2015)\n/// - [RFC 3986](https://tools.ietf.org/html/rfc3986) — Uniform Resource\n///   Identifier (URI): Generic Syntax (January 2005)\n#[derive(PartialEq, Debug)]\npub struct URI {\n\n    /// The priority of the URI. Clients are supposed to contact the URI with\n    /// the lowest priority out of all the ones it can reach.\n    pub priority: u16,\n\n    /// The weight of the URI, which specifies a relative weight for entries\n    /// with the same priority.\n    pub weight: u16,\n\n    /// The URI contained in the record. Since all we are doing is displaying\n    /// it to the user, we do not need to parse it for accuracy.\n    pub target: Box<[u8]>,\n}\n\nimpl Wire for URI {\n    const NAME: &'static str = \"URI\";\n    const RR_TYPE: u16 = 256;\n\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        let priority = c.read_u16::<BigEndian>()?;\n        trace!(\"Parsed priority -> {:?}\", priority);\n\n        let weight = c.read_u16::<BigEndian>()?;\n        trace!(\"Parsed weight -> {:?}\", weight);\n\n        // The target must not be empty.\n        if stated_length <= 4 {\n            let mandated_length = MandatedLength::AtLeast(5);\n            return Err(WireError::WrongRecordLength { stated_length, mandated_length });\n        }\n\n        let remaining_length = stated_length - 4;\n        let mut target = vec![0_u8; usize::from(remaining_length)].into_boxed_slice();\n        c.read_exact(&mut target)?;\n        trace!(\"Parsed target -> {:?}\", String::from_utf8_lossy(&target));\n\n        Ok(Self { priority, weight, target })\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    #[test]\n    fn parses() {\n        let buf = &[\n            0x00, 0x0A,  // priority\n            0x00, 0x10,  // weight\n            0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x72, 0x66, 0x63,\n            0x73, 0x2e, 0x69, 0x6f, 0x2f,  // uri\n        ];\n\n        assert_eq!(URI::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   URI {\n                       priority: 10,\n                       weight: 16,\n                       target: Box::new(*b\"https://rfcs.io/\"),\n                   });\n    }\n\n    #[test]\n    fn one_byte_of_uri() {\n        let buf = &[\n            0x00, 0x0A,  // priority\n            0x00, 0x10,  // weight\n            0x2f,  // one byte of uri (invalid but still a legitimate DNS record)\n        ];\n\n        assert_eq!(URI::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(),\n                   URI {\n                       priority: 10,\n                       weight: 16,\n                       target: Box::new(*b\"/\"),\n                   });\n    }\n\n    #[test]\n    fn missing_any_data() {\n        let buf = &[\n            0x00, 0x0A,  // priority\n            0x00, 0x10,  // weight\n        ];\n\n        assert_eq!(URI::read(buf.len() as _, &mut Cursor::new(buf)),\n                   Err(WireError::WrongRecordLength { stated_length: 4, mandated_length: MandatedLength::AtLeast(5) }));\n    }\n\n    #[test]\n    fn record_empty() {\n        assert_eq!(URI::read(0, &mut Cursor::new(&[])),\n                   Err(WireError::IO));\n    }\n\n    #[test]\n    fn buffer_ends_abruptly() {\n        let buf = &[\n            0x00, 0x0A,  // half a priority\n        ];\n\n        assert_eq!(URI::read(23, &mut Cursor::new(buf)),\n                   Err(WireError::IO));\n    }\n}\n"
  },
  {
    "path": "dns/src/strings.rs",
    "content": "//! Reading strings from the DNS wire protocol.\n\nuse std::convert::TryFrom;\nuse std::fmt;\nuse std::io::{self, Write};\n\nuse byteorder::{ReadBytesExt, WriteBytesExt};\nuse log::*;\n\nuse crate::wire::*;\n\n\n/// Domain names in the DNS protocol are encoded as **Labels**, which are\n/// segments of ASCII characters prefixed by their length. When written out,\n/// each segment is followed by a dot.\n///\n/// The maximum length of a segment is 255 characters.\n#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)]\npub struct Labels {\n    segments: Vec<(u8, String)>,\n}\n\n#[cfg(feature = \"with_idna\")]\nfn label_to_ascii(label: &str) -> Result<String, unic_idna::Errors> {\n    let flags = unic_idna::Flags{use_std3_ascii_rules: false, transitional_processing: false, verify_dns_length: true};\n    unic_idna::to_ascii(label, flags)\n}\n\n#[cfg(not(feature = \"with_idna\"))]\nfn label_to_ascii(label: &str) -> Result<String, ()> {\n    Ok(label.to_owned())\n}\n\nimpl Labels {\n\n    /// Creates a new empty set of labels, which represent the root of the DNS\n    /// as a domain with no name.\n    pub fn root() -> Self {\n        Self { segments: Vec::new() }\n    }\n\n    /// Encodes the given input string as labels. If any segment is too long,\n    /// returns that segment as an error.\n    pub fn encode(input: &str) -> Result<Self, &str> {\n        let mut segments = Vec::new();\n\n        for label in input.split('.') {\n            if label.is_empty() {\n                continue;\n            }\n\n            let label_idn = label_to_ascii(label)\n                    .map_err(|e| {\n                        warn!(\"Could not encode label {:?}: {:?}\", label, e);\n                        label\n                    })?;\n\n            match u8::try_from(label_idn.len()) {\n                Ok(length) => {\n                    segments.push((length, label_idn));\n                }\n                Err(e) => {\n                    warn!(\"Could not encode label {:?}: {}\", label, e);\n                    return Err(label);\n                }\n            }\n        }\n\n        Ok(Self { segments })\n    }\n\n    /// Returns the number of segments.\n    pub fn len(&self) -> usize {\n        self.segments.len()\n    }\n\n    /// Returns a new set of labels concatenating two names.\n    pub fn extend(&self, other: &Self) -> Self {\n        let mut segments = self.segments.clone();\n        segments.extend_from_slice(&other.segments);\n        Self { segments }\n    }\n}\n\nimpl fmt::Display for Labels {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        for (_, segment) in &self.segments {\n            write!(f, \"{}.\", segment)?;\n        }\n\n        Ok(())\n    }\n}\n\n/// An extension for `Cursor` that enables reading compressed domain names\n/// from DNS packets.\npub(crate) trait ReadLabels {\n\n    /// Read and expand a compressed domain name.\n    fn read_labels(&mut self) -> Result<(Labels, u16), WireError>;\n}\n\nimpl ReadLabels for Cursor<&[u8]> {\n    fn read_labels(&mut self) -> Result<(Labels, u16), WireError> {\n        let mut labels = Labels { segments: Vec::new() };\n        let bytes_read = read_string_recursive(&mut labels, self, &mut Vec::new())?;\n        Ok((labels, bytes_read))\n    }\n}\n\n\n/// An extension for `Write` that enables writing domain names.\npub(crate) trait WriteLabels {\n\n    /// Write a domain name.\n    ///\n    /// The names being queried are written with one byte slice per\n    /// domain segment, preceded by each segment’s length, with the\n    /// whole thing ending with a segment of zero length.\n    ///\n    /// So “dns.lookup.dog” would be encoded as:\n    /// “3, dns, 6, lookup, 3, dog, 0”.\n    fn write_labels(&mut self, input: &Labels) -> io::Result<()>;\n}\n\nimpl<W: Write> WriteLabels for W {\n    fn write_labels(&mut self, input: &Labels) -> io::Result<()> {\n        for (length, label) in &input.segments {\n            self.write_u8(*length)?;\n\n            for b in label.as_bytes() {\n                self.write_u8(*b)?;\n            }\n        }\n\n        self.write_u8(0)?;  // terminate the string\n        Ok(())\n    }\n}\n\n\nconst RECURSION_LIMIT: usize = 8;\n\n/// Reads bytes from the given cursor into the given buffer, using the list of\n/// recursions to track backtracking positions. Returns the count of bytes\n/// that had to be read to produce the string, including the bytes to signify\n/// backtracking, but not including the bytes read _during_ backtracking.\n#[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\nfn read_string_recursive(labels: &mut Labels, c: &mut Cursor<&[u8]>, recursions: &mut Vec<u16>) -> Result<u16, WireError> {\n    let mut bytes_read = 0;\n\n    loop {\n        let byte = c.read_u8()?;\n        bytes_read += 1;\n\n        if byte == 0 {\n            break;\n        }\n\n        else if byte >= 0b_1100_0000 {\n            let name_one = byte - 0b1100_0000;\n            let name_two = c.read_u8()?;\n            bytes_read += 1;\n            let offset = u16::from_be_bytes([name_one, name_two]);\n\n            if recursions.contains(&offset) {\n                warn!(\"Hit previous offset ({}) decoding string\", offset);\n                return Err(WireError::TooMuchRecursion(recursions.clone().into_boxed_slice()));\n            }\n\n            recursions.push(offset);\n\n            if recursions.len() >= RECURSION_LIMIT {\n                warn!(\"Hit recursion limit ({}) decoding string\", RECURSION_LIMIT);\n                return Err(WireError::TooMuchRecursion(recursions.clone().into_boxed_slice()));\n            }\n\n            trace!(\"Backtracking to offset {}\", offset);\n            let new_pos = c.position();\n            c.set_position(u64::from(offset));\n\n            read_string_recursive(labels, c, recursions)?;\n\n            trace!(\"Coming back to {}\", new_pos);\n            c.set_position(new_pos);\n            break;\n        }\n\n        // Otherwise, treat the byte as the length of a label, and read that\n        // many characters.\n        else {\n            let mut name_buf = Vec::new();\n\n            for _ in 0 .. byte {\n                let c = c.read_u8()?;\n                bytes_read += 1;\n                name_buf.push(c);\n            }\n\n            let string = String::from_utf8_lossy(&*name_buf).to_string();\n            labels.segments.push((byte, string));\n        }\n    }\n\n    Ok(bytes_read)\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n\n    // The buffers used in these tests contain nothing but the labels we’re\n    // decoding. In DNS packets found in the wild, the cursor will be able to\n    // reach all the bytes of the packet, so the Answer section can reference\n    // strings in the Query section.\n\n    #[test]\n    fn nothing() {\n        let buf: &[u8] = &[\n            0x00,  // end reading\n        ];\n\n        assert_eq!(Cursor::new(buf).read_labels(),\n                   Ok((Labels::root(), 1)));\n    }\n\n    #[test]\n    fn one_label() {\n        let buf: &[u8] = &[\n            0x03,  // label of length 3\n            b'o', b'n', b'e',  // label\n            0x00,  // end reading\n        ];\n\n        assert_eq!(Cursor::new(buf).read_labels(),\n                   Ok((Labels::encode(\"one.\").unwrap(), 5)));\n    }\n\n    #[test]\n    fn two_labels() {\n        let buf: &[u8] = &[\n            0x03,  // label of length 3\n            b'o', b'n', b'e',  // label\n            0x03,  // label of length 3\n            b't', b'w', b'o',  // label\n            0x00,  // end reading\n        ];\n\n        assert_eq!(Cursor::new(buf).read_labels(),\n                   Ok((Labels::encode(\"one.two.\").unwrap(), 9)));\n    }\n\n    #[test]\n    fn label_followed_by_backtrack() {\n        let buf: &[u8] = &[\n            0x03,  // label of length 3\n            b'o', b'n', b'e',  // label\n            0xc0, 0x06,  // skip to position 6 (the next byte)\n\n            0x03,  // label of length 3\n            b't', b'w', b'o',  // label\n            0x00,  // end reading\n        ];\n\n        assert_eq!(Cursor::new(buf).read_labels(),\n                   Ok((Labels::encode(\"one.two.\").unwrap(), 6)));\n    }\n\n    #[test]\n    fn extremely_long_label() {\n        let mut buf: Vec<u8> = vec![\n            0xbf,  // label of length 191\n        ];\n\n        buf.extend(vec![0x65; 191]);  // the rest of the label\n        buf.push(0x00);  // end reading\n\n        assert_eq!(Cursor::new(&*buf).read_labels().unwrap().1, 193);\n    }\n\n    #[test]\n    fn immediate_recursion() {\n        let buf: &[u8] = &[\n            0xc0, 0x00,  // skip to position 0\n        ];\n\n        assert_eq!(Cursor::new(buf).read_labels(),\n                   Err(WireError::TooMuchRecursion(Box::new([ 0 ]))));\n    }\n\n    #[test]\n    fn mutual_recursion() {\n        let buf: &[u8] = &[\n            0xc0, 0x02,  // skip to position 2\n            0xc0, 0x00,  // skip to position 0\n        ];\n\n        let mut cursor = Cursor::new(buf);\n\n        assert_eq!(cursor.read_labels(),\n                   Err(WireError::TooMuchRecursion(Box::new([ 2, 0 ]))));\n    }\n\n    #[test]\n    fn too_much_recursion() {\n        let buf: &[u8] = &[\n            0xc0, 0x02,  // skip to position 2\n            0xc0, 0x04,  // skip to position 4\n            0xc0, 0x06,  // skip to position 6\n            0xc0, 0x08,  // skip to position 8\n            0xc0, 0x0A,  // skip to position 10\n            0xc0, 0x0C,  // skip to position 12\n            0xc0, 0x0E,  // skip to position 14\n            0xc0, 0x10,  // skip to position 16\n            0x00,        // no label\n        ];\n\n        let mut cursor = Cursor::new(buf);\n\n        assert_eq!(cursor.read_labels(),\n                   Err(WireError::TooMuchRecursion(Box::new([ 2, 4, 6, 8, 10, 12, 14, 16 ]))));\n    }\n}\n"
  },
  {
    "path": "dns/src/types.rs",
    "content": "//! DNS packets are traditionally implemented with both the request and\n//! response packets at the same type. After all, both follow the same format,\n//! with the request packet having zero answer fields, and the response packet\n//! having at least one record in its answer fields.\n\nuse crate::record::{Record, RecordType, OPT};\nuse crate::strings::Labels;\n\n\n/// A request that gets sent out over a transport.\n#[derive(PartialEq, Debug)]\npub struct Request {\n\n    /// The transaction ID of this request. This is used to make sure\n    /// different DNS packets don’t answer each other’s questions.\n    pub transaction_id: u16,\n\n    /// The flags that accompany every DNS packet.\n    pub flags: Flags,\n\n    /// The query that this request is making. Only one query is allowed per\n    /// request, as traditionally, DNS servers only respond to the first query\n    /// in a packet.\n    pub query: Query,\n\n    /// An additional record that may be sent as part of the query.\n    pub additional: Option<OPT>,\n}\n\n\n/// A response obtained from a DNS server.\n#[derive(PartialEq, Debug)]\npub struct Response {\n\n    /// The transaction ID, which should match the ID of the request.\n    pub transaction_id: u16,\n\n    /// The flags that accompany every DNS packet.\n    pub flags: Flags,\n\n    /// The queries section.\n    pub queries: Vec<Query>,\n\n    /// The answers section.\n    pub answers: Vec<Answer>,\n\n    /// The authoritative nameservers section.\n    pub authorities: Vec<Answer>,\n\n    /// The additional records section.\n    pub additionals: Vec<Answer>,\n}\n\n\n/// A DNS query section.\n#[derive(PartialEq, Debug)]\npub struct Query {\n\n    /// The domain name being queried, in human-readable dotted notation.\n    pub qname: Labels,\n\n    /// The class number.\n    pub qclass: QClass,\n\n    /// The type number.\n    pub qtype: RecordType,\n}\n\n\n/// A DNS answer section.\n#[derive(PartialEq, Debug)]\npub enum Answer {\n\n    /// This is a standard answer with every field.\n    Standard {\n\n        /// The domain name being answered for.\n        qname: Labels,\n\n        /// This answer’s class.\n        qclass: QClass,\n\n        /// The time-to-live duration, in seconds.\n        ttl: u32,\n\n        /// The record contained in this answer.\n        record: Record,\n    },\n\n    /// This is a pseudo-record answer, so some of the fields (class and TTL)\n    /// have different meaning.\n    Pseudo {\n\n        /// The domain name being answered for.\n        qname: Labels,\n\n        /// The OPT record contained in this answer.\n        opt: OPT,\n    },\n}\n\n\n/// A DNS record class. Of these, the only one that’s in regular use anymore\n/// is the Internet class.\n#[derive(PartialEq, Debug, Copy, Clone)]\npub enum QClass {\n\n    /// The **Internet** class.\n    IN,\n\n    /// The **Chaosnet** class.\n    CH,\n\n    /// The **Hesiod** class.\n    HS,\n\n    /// A class number that does not map to any known class.\n    Other(u16),\n}\n\n\n/// The flags that accompany every DNS packet.\n#[derive(PartialEq, Debug, Copy, Clone)]\npub struct Flags {\n\n    /// Whether this packet is a response packet.\n    pub response: bool,\n\n    /// The operation being performed.\n    pub opcode: Opcode,\n\n    /// In a response, whether the server is providing authoritative DNS responses.\n    pub authoritative: bool,\n\n    /// In a response, whether this message has been truncated by the transport.\n    pub truncated: bool,\n\n    /// In a query, whether the server may query other nameservers recursively.\n    /// It is up to the server whether it will actually do this.\n    pub recursion_desired: bool,\n\n    /// In a response, whether the server allows recursive query support.\n    pub recursion_available: bool,\n\n    /// In a response, whether the server is marking this data as authentic.\n    pub authentic_data: bool,\n\n    /// In a request, whether the server should disable its authenticity\n    /// checking for the request’s queries.\n    pub checking_disabled: bool,\n\n    /// In a response, a code indicating an error if one occurred.\n    pub error_code: Option<ErrorCode>,\n}\n\n\n/// A number representing the operation being performed.\n#[derive(PartialEq, Debug, Copy, Clone)]\npub enum Opcode {\n\n    /// This request is a standard query, or this response is answering a\n    /// standard query.\n    Query,\n\n    /// Any other opcode. This can be from 1 to 15, as the opcode field is\n    /// four bits wide, and 0 is taken.\n    Other(u8),\n}\n\n\n/// A code indicating an error.\n///\n/// # References\n///\n/// - [RFC 6895 §2.3](https://tools.ietf.org/html/rfc6895#section-2.3) — Domain\n///   Name System (DNS) IANA Considerations (April 2013)\n#[derive(PartialEq, Debug, Copy, Clone)]\npub enum ErrorCode {\n\n    /// `FormErr` — The server was unable to interpret the query.\n    FormatError,\n\n    /// `ServFail` — There was a problem with the server.\n    ServerFailure,\n\n    /// `NXDomain` — The domain name referenced in the query does not exist.\n    NXDomain,\n\n    /// `NotImp` — The server does not support one of the requested features.\n    NotImplemented,\n\n    /// `Refused` — The server was able to interpret the query, but refused to\n    /// fulfil it.\n    QueryRefused,\n\n    /// `BADVERS` and `BADSIG` — The server did not accept the EDNS version,\n    /// or failed to verify a signature. The same code is used for both.\n    BadVersion,\n\n    /// An error code with no currently-defined meaning.\n    Other(u16),\n\n    /// An error code within the ‘Reserved for Private Use’ range.\n    Private(u16)\n}\n\n\nimpl Answer {\n\n    /// Whether this Answer holds a standard record, not a pseudo record.\n    pub fn is_standard(&self) -> bool {\n        matches!(self, Self::Standard { .. })\n    }\n}\n"
  },
  {
    "path": "dns/src/wire.rs",
    "content": "//! Parsing the DNS wire protocol.\n\npub(crate) use std::io::{Cursor, Read};\npub(crate) use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};\n\nuse std::io;\nuse log::*;\n\nuse crate::record::{Record, RecordType, OPT};\nuse crate::strings::{Labels, ReadLabels, WriteLabels};\nuse crate::types::*;\n\n\nimpl Request {\n\n    /// Converts this request to a vector of bytes.\n    pub fn to_bytes(&self) -> io::Result<Vec<u8>> {\n        let mut bytes = Vec::with_capacity(32);\n\n        bytes.write_u16::<BigEndian>(self.transaction_id)?;\n        bytes.write_u16::<BigEndian>(self.flags.to_u16())?;\n\n        bytes.write_u16::<BigEndian>(1)?;  // query count\n        bytes.write_u16::<BigEndian>(0)?;  // answer count\n        bytes.write_u16::<BigEndian>(0)?;  // authority RR count\n        bytes.write_u16::<BigEndian>(if self.additional.is_some() { 1 } else { 0 })?;  // additional RR count\n\n        bytes.write_labels(&self.query.qname)?;\n        bytes.write_u16::<BigEndian>(self.query.qtype.type_number())?;\n        bytes.write_u16::<BigEndian>(self.query.qclass.to_u16())?;\n\n        if let Some(opt) = &self.additional {\n            bytes.write_u8(0)?;  // usually a name\n            bytes.write_u16::<BigEndian>(OPT::RR_TYPE)?;\n            bytes.extend(opt.to_bytes()?);\n        }\n\n        Ok(bytes)\n    }\n\n    /// Returns the OPT record to be sent as part of requests.\n    pub fn additional_record() -> OPT {\n        OPT {\n            udp_payload_size: 512,\n            higher_bits: 0,\n            edns0_version: 0,\n            flags: 0,\n            data: Vec::new(),\n        }\n    }\n}\n\n\nimpl Response {\n\n    /// Reads bytes off of the given slice, parsing them into a response.\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    pub fn from_bytes(bytes: &[u8]) -> Result<Self, WireError> {\n        info!(\"Parsing response\");\n        trace!(\"Bytes -> {:?}\", bytes);\n        let mut c = Cursor::new(bytes);\n\n        let transaction_id = c.read_u16::<BigEndian>()?;\n        trace!(\"Read txid -> {:?}\", transaction_id);\n\n        let flags = Flags::from_u16(c.read_u16::<BigEndian>()?);\n        trace!(\"Read flags -> {:#?}\", flags);\n\n        let query_count      = c.read_u16::<BigEndian>()?;\n        let answer_count     = c.read_u16::<BigEndian>()?;\n        let authority_count  = c.read_u16::<BigEndian>()?;\n        let additional_count = c.read_u16::<BigEndian>()?;\n\n        // We can pre-allocate these vectors by giving them an initial\n        // capacity based on the count fields. But because the count fields\n        // are user-controlled (with a maximum of 2^16 - 1) we cannot trust\n        // them _entirely_, so cap the pre-allocation if the count looks\n        // arbitrarily large (9 seems about right).\n\n        let mut queries = Vec::with_capacity(usize::from(query_count.min(9)));\n        debug!(\"Reading {}x query from response\", query_count);\n        for _ in 0 .. query_count {\n            let (qname, _) = c.read_labels()?;\n            queries.push(Query::from_bytes(qname, &mut c)?);\n        }\n\n        let mut answers = Vec::with_capacity(usize::from(answer_count.min(9)));\n        debug!(\"Reading {}x answer from response\", answer_count);\n        for _ in 0 .. answer_count {\n            let (qname, _) = c.read_labels()?;\n            answers.push(Answer::from_bytes(qname, &mut c)?);\n        }\n\n        let mut authorities = Vec::with_capacity(usize::from(authority_count.min(9)));\n        debug!(\"Reading {}x authority from response\", authority_count);\n        for _ in 0 .. authority_count {\n            let (qname, _) = c.read_labels()?;\n            authorities.push(Answer::from_bytes(qname, &mut c)?);\n        }\n\n        let mut additionals = Vec::with_capacity(usize::from(additional_count.min(9)));\n        debug!(\"Reading {}x additional answer from response\", additional_count);\n        for _ in 0 .. additional_count {\n            let (qname, _) = c.read_labels()?;\n            additionals.push(Answer::from_bytes(qname, &mut c)?);\n        }\n\n        Ok(Self { transaction_id, flags, queries, answers, authorities, additionals })\n    }\n}\n\n\nimpl Query {\n\n    /// Reads bytes from the given cursor, and parses them into a query with\n    /// the given domain name.\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn from_bytes(qname: Labels, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        let qtype_number = c.read_u16::<BigEndian>()?;\n        trace!(\"Read qtype number -> {:?}\", qtype_number );\n\n        let qtype = RecordType::from(qtype_number);\n        trace!(\"Found qtype -> {:?}\", qtype );\n\n        let qclass = QClass::from_u16(c.read_u16::<BigEndian>()?);\n        trace!(\"Read qclass -> {:?}\", qtype);\n\n        Ok(Self { qtype, qclass, qname })\n    }\n}\n\n\nimpl Answer {\n\n    /// Reads bytes from the given cursor, and parses them into an answer with\n    /// the given domain name.\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn from_bytes(qname: Labels, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        let qtype_number = c.read_u16::<BigEndian>()?;\n        trace!(\"Read qtype number -> {:?}\", qtype_number );\n\n        if qtype_number == OPT::RR_TYPE {\n            let opt = OPT::read(c)?;\n            Ok(Self::Pseudo { qname, opt })\n        }\n        else {\n            let qtype = RecordType::from(qtype_number);\n            trace!(\"Found qtype -> {:?}\", qtype );\n\n            let qclass = QClass::from_u16(c.read_u16::<BigEndian>()?);\n            trace!(\"Read qclass -> {:?}\", qtype);\n\n            let ttl = c.read_u32::<BigEndian>()?;\n            trace!(\"Read TTL -> {:?}\", ttl);\n\n            let record_length = c.read_u16::<BigEndian>()?;\n            trace!(\"Read record length -> {:?}\", record_length);\n\n            let record = Record::from_bytes(qtype, record_length, c)?;\n            Ok(Self::Standard { qclass, qname, record, ttl })\n        }\n    }\n}\n\n\nimpl Record {\n\n    /// Reads at most `len` bytes from the given curser, and parses them into\n    /// a record structure depending on the type number, which has already been read.\n    #[cfg_attr(feature = \"with_mutagen\", ::mutagen::mutate)]\n    fn from_bytes(record_type: RecordType, len: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError> {\n        if cfg!(feature = \"with_mutagen\") {\n            warn!(\"Mutation is enabled!\");\n        }\n\n        macro_rules! read_record {\n            ($record:tt) => { {\n                info!(\"Parsing {} record (type {}, len {})\", crate::record::$record::NAME, record_type.type_number(), len);\n                Wire::read(len, c).map(Self::$record)\n            } }\n        }\n\n        match record_type {\n            RecordType::A           => read_record!(A),\n            RecordType::AAAA        => read_record!(AAAA),\n            RecordType::CAA         => read_record!(CAA),\n            RecordType::CNAME       => read_record!(CNAME),\n            RecordType::EUI48       => read_record!(EUI48),\n            RecordType::EUI64       => read_record!(EUI64),\n            RecordType::HINFO       => read_record!(HINFO),\n            RecordType::LOC         => read_record!(LOC),\n            RecordType::MX          => read_record!(MX),\n            RecordType::NAPTR       => read_record!(NAPTR),\n            RecordType::NS          => read_record!(NS),\n            RecordType::OPENPGPKEY  => read_record!(OPENPGPKEY),\n            RecordType::PTR         => read_record!(PTR),\n            RecordType::SSHFP       => read_record!(SSHFP),\n            RecordType::SOA         => read_record!(SOA),\n            RecordType::SRV         => read_record!(SRV),\n            RecordType::TLSA        => read_record!(TLSA),\n            RecordType::TXT         => read_record!(TXT),\n            RecordType::URI         => read_record!(URI),\n\n            RecordType::Other(type_number) => {\n                let mut bytes = Vec::new();\n                for _ in 0 .. len {\n                    bytes.push(c.read_u8()?);\n                }\n\n                Ok(Self::Other { type_number, bytes })\n            }\n        }\n    }\n}\n\n\nimpl QClass {\n    fn from_u16(uu: u16) -> Self {\n        match uu {\n            0x0001 => Self::IN,\n            0x0003 => Self::CH,\n            0x0004 => Self::HS,\n                 _ => Self::Other(uu),\n        }\n    }\n\n    fn to_u16(self) -> u16 {\n        match self {\n            Self::IN        => 0x0001,\n            Self::CH        => 0x0003,\n            Self::HS        => 0x0004,\n            Self::Other(uu) => uu,\n        }\n    }\n}\n\n\nimpl Flags {\n\n    /// The set of flags that represents a query packet.\n    pub fn query() -> Self {\n        Self::from_u16(0b_0000_0001_0000_0000)\n    }\n\n    /// The set of flags that represents a successful response.\n    pub fn standard_response() -> Self {\n        Self::from_u16(0b_1000_0001_1000_0000)\n    }\n\n    /// Converts the flags into a two-byte number.\n    pub fn to_u16(self) -> u16 {                 // 0123 4567 89AB CDEF\n        let mut                          bits  = 0b_0000_0000_0000_0000;\n        if self.response               { bits |= 0b_1000_0000_0000_0000; }\n        match self.opcode {\n            Opcode::Query     =>       { bits |= 0b_0000_0000_0000_0000; }\n            Opcode::Other(_)  =>       { unimplemented!(); }\n        }\n        if self.authoritative          { bits |= 0b_0000_0100_0000_0000; }\n        if self.truncated              { bits |= 0b_0000_0010_0000_0000; }\n        if self.recursion_desired      { bits |= 0b_0000_0001_0000_0000; }\n        if self.recursion_available    { bits |= 0b_0000_0000_1000_0000; }\n        // (the Z bit is reserved)               0b_0000_0000_0100_0000\n        if self.authentic_data         { bits |= 0b_0000_0000_0010_0000; }\n        if self.checking_disabled      { bits |= 0b_0000_0000_0001_0000; }\n\n        bits\n    }\n\n    /// Extracts the flags from the given two-byte number.\n    pub fn from_u16(bits: u16) -> Self {\n        let has_bit = |bit| { bits & bit == bit };\n\n        Self {\n            response:               has_bit(0b_1000_0000_0000_0000),\n            opcode:                 Opcode::from_bits((bits.to_be_bytes()[0] & 0b_0111_1000) >> 3),\n            authoritative:          has_bit(0b_0000_0100_0000_0000),\n            truncated:              has_bit(0b_0000_0010_0000_0000),\n            recursion_desired:      has_bit(0b_0000_0001_0000_0000),\n            recursion_available:    has_bit(0b_0000_0000_1000_0000),\n            authentic_data:         has_bit(0b_0000_0000_0010_0000),\n            checking_disabled:      has_bit(0b_0000_0000_0001_0000),\n            error_code:             ErrorCode::from_bits(bits & 0b_1111),\n        }\n    }\n}\n\n\nimpl Opcode {\n\n    /// Extracts the opcode from this four-bit number, which should have been\n    /// extracted from the packet and shifted to be in the range 0–15.\n    fn from_bits(bits: u8) -> Self {\n        if bits == 0 {\n            Self::Query\n        }\n        else {\n            assert!(bits <= 15, \"bits {:#08b} out of range\", bits);\n            Self::Other(bits)\n        }\n    }\n}\n\n\nimpl ErrorCode {\n\n    /// Extracts the rcode from the last four bits of the flags field.\n    fn from_bits(bits: u16) -> Option<Self> {\n        if (0x0F01 .. 0x0FFF).contains(&bits) {\n            return Some(Self::Private(bits));\n        }\n\n        match bits {\n            0 => None,\n            1 => Some(Self::FormatError),\n            2 => Some(Self::ServerFailure),\n            3 => Some(Self::NXDomain),\n            4 => Some(Self::NotImplemented),\n            5 => Some(Self::QueryRefused),\n           16 => Some(Self::BadVersion),\n            n => Some(Self::Other(n)),\n        }\n    }\n}\n\n\n/// Trait for decoding DNS record structures from bytes read over the wire.\npub trait Wire: Sized {\n\n    /// This record’s type as a string, such as `\"A\"` or `\"CNAME\"`.\n    const NAME: &'static str;\n\n    /// The number signifying that a record is of this type.\n    /// See <https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4>\n    const RR_TYPE: u16;\n\n    /// Read at most `len` bytes from the given `Cursor`. This cursor travels\n    /// throughout the complete data — by this point, we have read the entire\n    /// response into a buffer.\n    fn read(len: u16, c: &mut Cursor<&[u8]>) -> Result<Self, WireError>;\n}\n\n\n/// Something that can go wrong deciphering a record.\n#[derive(PartialEq, Debug)]\npub enum WireError {\n\n    /// There was an IO error reading from the cursor.\n    /// Almost all the time, this means that the buffer was too short.\n    IO,\n    // (io::Error is not PartialEq so we don’t propagate it)\n\n    /// When the DNS standard requires records of this type to have a certain\n    /// fixed length, but the response specified a different length.\n    ///\n    /// This error should be returned regardless of the _content_ of the\n    /// record, whatever it is.\n    WrongRecordLength {\n\n        /// The length of the record’s data, as specified in the packet.\n        stated_length: u16,\n\n        /// The length of the record that the DNS specification mandates.\n        mandated_length: MandatedLength,\n    },\n\n    /// When the length of this record as specified in the packet differs from\n    /// the computed length, as determined by reading labels.\n    ///\n    /// There are two ways, in general, to read arbitrary-length data from a\n    /// stream of bytes: length-prefixed (read the length, then read that many\n    /// bytes) or sentinel-terminated (keep reading bytes until you read a\n    /// certain value, usually zero). The DNS protocol uses both: each\n    /// record’s size is specified up-front in the packet, but inside the\n    /// record, there exist arbitrary-length strings that must be read until a\n    /// zero is read, indicating there is no more string.\n    ///\n    /// Consider the case of a packet, with a specified length, containing a\n    /// string of arbitrary length (such as the CNAME or TXT records). A DNS\n    /// client has to deal with this in one of two ways:\n    ///\n    /// 1. Read exactly the specified length of bytes from the record, raising\n    ///    an error if the contents are too short or a string keeps going past\n    ///    the length (assume the length is correct but the contents are wrong).\n    ///\n    /// 2. Read as many bytes from the record as the string requests, raising\n    ///    an error if the number of bytes read at the end differs from the\n    ///    expected length of the record (assume the length is wrong but the\n    ///    contents are correct).\n    ///\n    /// Note that no matter which way is picked, the record will still be\n    /// incorrect — it only impacts the parsing of records that occur after it\n    /// in the packet. Knowing which method should be used requires knowing\n    /// what caused the DNS packet to be erroneous, which we cannot know.\n    ///\n    /// dog picks the second way. If a record ends up reading more or fewer\n    /// bytes than it is ‘supposed’ to, it will raise this error, but _after_\n    /// having read a different number of bytes than the specified length.\n    WrongLabelLength {\n\n        /// The length of the record’s data, as specified in the packet.\n        stated_length: u16,\n\n        /// The computed length of the record’s data, based on the number of\n        /// bytes consumed by reading labels from the packet.\n        length_after_labels: u16,\n    },\n\n    /// When the data contained a string containing a cycle of pointers.\n    /// Contains the vector of indexes that was being checked.\n    TooMuchRecursion(Box<[u16]>),\n\n    /// When the data contained a string with a pointer to an index outside of\n    /// the packet. Contains the invalid index.\n    OutOfBounds(u16),\n\n    /// When a record in the packet contained a version field that specifies\n    /// the format of its remaining fields, but this version is too recent to\n    /// be supported, so we cannot parse it.\n    WrongVersion {\n\n        /// The version of the record layout, as specified in the packet\n        stated_version: u8,\n\n        /// The maximum version that this version of dog supports.\n        maximum_supported_version: u8,\n    }\n}\n\n/// The rule for how long a record in a packet should be.\n#[derive(PartialEq, Debug, Copy, Clone)]\npub enum MandatedLength {\n\n    /// The record should be exactly this many bytes in length.\n    Exactly(u16),\n\n    /// The record should be _at least_ this many bytes in length.\n    AtLeast(u16),\n}\n\nimpl From<io::Error> for WireError {\n    fn from(ioe: io::Error) -> Self {\n        error!(\"IO error -> {:?}\", ioe);\n        Self::IO\n    }\n}\n"
  },
  {
    "path": "dns/tests/wire_building_tests.rs",
    "content": "use dns::{Request, Flags, Query, Labels, QClass};\nuse dns::record::RecordType;\n\nuse pretty_assertions::assert_eq;\n\n\n#[test]\nfn build_request() {\n    let request = Request {\n        transaction_id: 0xceac,\n        flags: Flags::query(),\n        query: Query {\n            qname: Labels::encode(\"rfcs.io\").unwrap(),\n            qclass: QClass::Other(0x42),\n            qtype: RecordType::from(0x1234),\n        },\n        additional: Some(Request::additional_record()),\n    };\n\n    let result = vec![\n        0xce, 0xac,  // transaction ID\n        0x01, 0x00,  // flags (standard query)\n        0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,  // counts (1, 0, 0, 1)\n\n        // query:\n        0x04, 0x72, 0x66, 0x63, 0x73, 0x02, 0x69, 0x6f, 0x00,  // qname\n        0x12, 0x34,  // type\n        0x00, 0x42,  // class\n\n        // OPT record:\n        0x00,  // name\n        0x00, 0x29,  // type OPT\n        0x02, 0x00,  // UDP payload size\n        0x00,  // higher bits\n        0x00,  // EDNS(0) version\n        0x00, 0x00,  // more flags\n        0x00, 0x00,  // no data\n    ];\n\n    assert_eq!(request.to_bytes().unwrap(), result);\n}\n"
  },
  {
    "path": "dns/tests/wire_parsing_tests.rs",
    "content": "use std::net::Ipv4Addr;\n\nuse dns::{Response, Query, Answer, Labels, Flags, Opcode, QClass};\nuse dns::record::{Record, A, CNAME, OPT, SOA, UnknownQtype, RecordType};\n\nuse pretty_assertions::assert_eq;\n\n\n#[test]\nfn parse_nothing() {\n    assert!(Response::from_bytes(&[]).is_err());\n}\n\n\n#[test]\nfn parse_response_standard() {\n    let buf = &[\n        0x0d, 0xcd,  // transaction ID\n        0x81, 0x80,  // flags (standard query, response, no error)\n        0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,  // counts (1, 1, 0, 1)\n\n        // the query:\n        0x03, 0x64, 0x6e, 0x73, 0x06, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x03,\n        0x64, 0x6f, 0x67, 0x00,  // \"dns.lookup.dog.\"\n        0x00, 0x01,  // type A\n        0x00, 0x01,  // class IN\n\n        // the answer:\n        0xc0, 0x0c,  // to find the name, backtrack to position 0x0c (12)\n        0x00, 0x01,  // type A\n        0x00, 0x01,  // class IN\n        0x00, 0x00, 0x03, 0xa5,  // TTL (933 seconds)\n        0x00, 0x04,  // record data length 4\n        0x8a, 0x44, 0x75, 0x5e,  // record date (138.68.117.94)\n\n        // the additional:\n        0x00,        // no name\n        0x00, 0x29,  // type OPT\n        0x02, 0x00,  // UDP payload size (512)\n        0x00, 0x00,  // higher bits (all 0)\n        0x00,        // EDNS version\n        0x00, 0x00,  // extra bits (DO bit unset)\n        0x00,        // data length 0\n    ];\n\n    let response = Response {\n        transaction_id: 0x0dcd,\n        flags: Flags {\n            response: true,\n            opcode: Opcode::Query,\n            authoritative: false,\n            truncated: false,\n            recursion_desired: true,\n            recursion_available: true,\n            authentic_data: false,\n            checking_disabled: false,\n            error_code: None,\n        },\n        queries: vec![\n            Query {\n                qname: Labels::encode(\"dns.lookup.dog\").unwrap(),\n                qclass: QClass::IN,\n                qtype: RecordType::A,\n            },\n        ],\n        answers: vec![\n            Answer::Standard {\n                qname: Labels::encode(\"dns.lookup.dog\").unwrap(),\n                qclass: QClass::IN,\n                ttl: 933,\n                record: Record::A(A {\n                    address: Ipv4Addr::new(138, 68, 117, 94),\n                }),\n            }\n        ],\n        authorities: vec![],\n        additionals: vec![\n            Answer::Pseudo {\n                qname: Labels::root(),\n                opt: OPT {\n                    udp_payload_size: 512,\n                    higher_bits: 0,\n                    edns0_version: 0,\n                    flags: 0,\n                    data: vec![],\n                },\n            },\n        ],\n    };\n\n    assert_eq!(Response::from_bytes(buf), Ok(response));\n}\n\n\n#[test]\nfn parse_response_with_mixed_string() {\n    let buf = &[\n        0x06, 0x9f,  // transaction ID\n        0x81, 0x80,  // flags (standard query, response, no error)\n        0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,  // counts (1, 1, 0, 0)\n\n        // the query:\n        0x0d, 0x63, 0x6e, 0x61, 0x6d, 0x65, 0x2d, 0x65, 0x78, 0x61, 0x6d, 0x70,\n        0x6c, 0x65, 0x06, 0x6c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x03, 0x64, 0x6f,\n        0x67, 0x00,  // \"cname-example.lookup.dog\"\n        0x00, 0x05,  // type CNAME\n        0x00, 0x01,  // class IN\n\n        // the answer:\n        0xc0, 0x0c,  // to find the name, backtrack to position 0x0c (12)\n        0x00, 0x05,  // type CNAME\n        0x00, 0x01,  // class IN\n        0x00, 0x00, 0x03, 0x69,  // TTL (873 seconds)\n        0x00, 0x06,  // record data length 6\n        0x03, 0x64, 0x6e, 0x73, 0xc0, 0x1a,\n        // \"dns.lookup.dog.\", which is \"dns.\" + backtrack to position 0x1a (28)\n    ];\n\n    let response = Response {\n        transaction_id: 0x069f,\n        flags: Flags {\n            response: true,\n            opcode: Opcode::Query,\n            authoritative: false,\n            truncated: false,\n            recursion_desired: true,\n            recursion_available: true,\n            authentic_data: false,\n            checking_disabled: false,\n            error_code: None,\n        },\n        queries: vec![\n            Query {\n                qname: Labels::encode(\"cname-example.lookup.dog\").unwrap(),\n                qclass: QClass::IN,\n                qtype: RecordType::CNAME,\n            },\n        ],\n        answers: vec![\n            Answer::Standard {\n                qname: Labels::encode(\"cname-example.lookup.dog\").unwrap(),\n                qclass: QClass::IN,\n                ttl: 873,\n                record: Record::CNAME(CNAME {\n                    domain: Labels::encode(\"dns.lookup.dog\").unwrap(),\n                }),\n            }\n        ],\n        authorities: vec![],\n        additionals: vec![],\n    };\n\n    assert_eq!(Response::from_bytes(buf), Ok(response));\n}\n\n\n#[test]\nfn parse_response_with_multiple_additionals() {\n\n    // This is an artifical amalgam of DNS, not a real-world response!\n    let buf = &[\n        0xce, 0xac,  // transaction ID\n        0x81, 0x80,  // flags (standard query, response, no error)\n        0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x02,  // counts (1, 1, 1, 2)\n\n        // query:\n        0x05, 0x62, 0x73, 0x61, 0x67, 0x6f, 0x02, 0x6d, 0x65, 0x00,  // name\n        0x00, 0x01,  // type A\n        0x00, 0x01,  // class IN\n\n        // answer:\n        0xc0, 0x0c,  // name (backreference)\n        0x00, 0x01,  // type A\n        0x00, 0x01,  // class IN\n        0x00, 0x00, 0x03, 0x77,  // TTL\n        0x00, 0x04,  // data length 4\n        0x8a, 0x44, 0x75, 0x5e,  // IP address\n\n        // authoritative:\n        0x00,  // name\n        0x00, 0x06,  // type SOA\n        0x00, 0x01,  // class IN\n        0xFF, 0xFF, 0xFF, 0xFF,  // TTL (maximum possible!)\n        0x00, 0x1B,  // data length\n        0x01, 0x61, 0x00,  // primary name server (\"a\")\n        0x02, 0x6d, 0x78, 0x00,  // mailbox (\"mx\")\n        0x78, 0x68, 0x52, 0x2c,  // serial number\n        0x00, 0x00, 0x07, 0x08,  // refresh interval\n        0x00, 0x00, 0x03, 0x84,  // retry interval\n        0x00, 0x09, 0x3a, 0x80,  // expire limit\n        0x00, 0x01, 0x51, 0x80,  // minimum TTL\n\n        // additional 1:\n        0x00,  // name\n        0x00, 0x99,  // unknown type\n        0x00, 0x99,  // unknown class\n        0x12, 0x34, 0x56, 0x78,  // TTL\n        0x00, 0x04,  // data length 4\n        0x12, 0x34, 0x56, 0x78,  // data\n\n        // additional 2:\n        0x00,  // name\n        0x00, 0x29,  // type OPT\n        0x02, 0x00,  // UDP payload size\n        0x00,  // higher bits\n        0x00,  // EDNS(0) version\n        0x00, 0x00,  // more flags\n        0x00, 0x00,  // no data\n    ];\n\n    let response = Response {\n        transaction_id: 0xceac,\n        flags: Flags::standard_response(),\n        queries: vec![\n            Query {\n                qname: Labels::encode(\"bsago.me\").unwrap(),\n                qclass: QClass::IN,\n                qtype: RecordType::A,\n            },\n        ],\n        answers: vec![\n            Answer::Standard {\n                qname: Labels::encode(\"bsago.me\").unwrap(),\n                qclass: QClass::IN,\n                ttl: 887,\n                record: Record::A(A {\n                    address: Ipv4Addr::new(138, 68, 117, 94),\n                }),\n            }\n        ],\n        authorities: vec![\n            Answer::Standard {\n                qname: Labels::root(),\n                qclass: QClass::IN,\n                ttl: 4294967295,\n                record: Record::SOA(SOA {\n                    mname: Labels::encode(\"a\").unwrap(),\n                    rname: Labels::encode(\"mx\").unwrap(),\n                    serial: 2020102700,\n                    refresh_interval: 1800,\n                    retry_interval: 900,\n                    expire_limit: 604800,\n                    minimum_ttl: 86400,\n                }),\n            }\n        ],\n        additionals: vec![\n            Answer::Standard {\n                qname: Labels::root(),\n                qclass: QClass::Other(153),\n                ttl: 305419896,\n                record: Record::Other {\n                    type_number: UnknownQtype::UnheardOf(153),\n                    bytes: vec![ 0x12, 0x34, 0x56, 0x78 ],\n                },\n            },\n            Answer::Pseudo {\n                qname: Labels::root(),\n                opt: OPT {\n                    udp_payload_size: 512,\n                    higher_bits: 0,\n                    edns0_version: 0,\n                    flags: 0,\n                    data: vec![],\n                },\n            },\n        ],\n    };\n\n    assert_eq!(Response::from_bytes(buf), Ok(response));\n}\n"
  },
  {
    "path": "dns-transport/Cargo.toml",
    "content": "[package]\nname = \"dns-transport\"\nversion = \"0.2.0-pre\"\nauthors = [\"Benjamin Sago <ogham@bsago.me>\"]\nedition = \"2018\"\n\n[lib]\ndoctest = false\ntest = false\n\n\n[dependencies]\n\n# dns wire protocol\ndns = { path = \"../dns\" }\n\n# logging\nlog = \"0.4\"\n\n# tls networking\nnative-tls = { version = \"0.2\", optional = true }\n\n# http response parsing\nhttparse = { version = \"1.3\", optional = true }\n\nrustls = { version = \"0.19\", optional = true }\n\nwebpki = { version = \"0.21.0\", optional = true }\n\nwebpki-roots = { version = \"0.21.0\", optional = true }\n\ncfg-if = \"1\"\n\n[features]\ndefault = []  # these are enabled in the main dog crate\n\nwith_tls   = []\nwith_https = [\"httparse\"]\n\nwith_nativetls = [\"native-tls\"]\nwith_nativetls_vendored = [\"native-tls\", \"native-tls/vendored\"]\nwith_rustls = [\"rustls\", \"webpki-roots\", \"webpki\"]\n"
  },
  {
    "path": "dns-transport/src/auto.rs",
    "content": "use log::*;\n\nuse dns::{Request, Response};\nuse super::{Transport, Error, UdpTransport, TcpTransport};\n\n\n/// The **automatic transport**, which sends DNS wire data using the UDP\n/// transport, then tries using the TCP transport if the first one fails\n/// because the response wouldn’t fit in a single UDP packet.\n///\n/// This is the default behaviour for many DNS clients.\npub struct AutoTransport {\n    addr: String,\n}\n\nimpl AutoTransport {\n\n    /// Creates a new automatic transport that connects to the given host.\n    pub fn new(addr: String) -> Self {\n        Self { addr }\n    }\n}\n\n\nimpl Transport for AutoTransport {\n    fn send(&self, request: &Request) -> Result<Response, Error> {\n        let udp_transport = UdpTransport::new(self.addr.clone());\n        let udp_response = udp_transport.send(&request)?;\n\n        if ! udp_response.flags.truncated {\n            return Ok(udp_response);\n        }\n\n        debug!(\"Truncated flag set, so switching to TCP\");\n\n        let tcp_transport = TcpTransport::new(self.addr.clone());\n        let tcp_response = tcp_transport.send(&request)?;\n        Ok(tcp_response)\n    }\n}\n"
  },
  {
    "path": "dns-transport/src/error.rs",
    "content": "/// Something that can go wrong making a DNS request.\n#[derive(Debug)]\npub enum Error {\n\n    /// The data in the response did not parse correctly from the DNS wire\n    /// protocol format.\n    WireError(dns::WireError),\n\n    /// There was a problem with the network making a TCP or UDP request.\n    NetworkError(std::io::Error),\n\n    /// Not enough information was received from the server before a `read`\n    /// call returned zero bytes.\n    TruncatedResponse,\n\n    /// There was a problem making a TLS request.\n    #[cfg(feature = \"with_nativetls\")]\n    TlsError(native_tls::Error),\n\n    /// There was a problem _establishing_ a TLS request.\n    #[cfg(feature = \"with_nativetls\")]\n    TlsHandshakeError(native_tls::HandshakeError<std::net::TcpStream>),\n\n    /// Provided dns name is not valid\n    #[cfg(feature = \"with_rustls\")]\n    RustlsInvalidDnsNameError(webpki::InvalidDNSNameError),\n\n    /// There was a problem decoding the response HTTP headers or body.\n    #[cfg(feature = \"with_https\")]\n    HttpError(httparse::Error),\n\n    /// The HTTP response code was something other than 200 OK, along with the\n    /// response code text, if present.\n    #[cfg(feature = \"with_https\")]\n    WrongHttpStatus(u16, Option<String>),\n}\n\n\n// From impls\n\nimpl From<dns::WireError> for Error {\n    fn from(inner: dns::WireError) -> Self {\n        Self::WireError(inner)\n    }\n}\n\nimpl From<std::io::Error> for Error {\n    fn from(inner: std::io::Error) -> Self {\n        Self::NetworkError(inner)\n    }\n}\n\n#[cfg(feature = \"with_nativetls\")]\nimpl From<native_tls::Error> for Error {\n    fn from(inner: native_tls::Error) -> Self {\n        Self::TlsError(inner)\n    }\n}\n\n#[cfg(feature = \"with_nativetls\")]\nimpl From<native_tls::HandshakeError<std::net::TcpStream>> for Error {\n    fn from(inner: native_tls::HandshakeError<std::net::TcpStream>) -> Self {\n        Self::TlsHandshakeError(inner)\n    }\n}\n\n#[cfg(feature = \"with_rustls\")]\nimpl From<webpki::InvalidDNSNameError> for Error {\n    fn from(inner: webpki::InvalidDNSNameError) -> Self {\n        Self::RustlsInvalidDnsNameError(inner)\n    }\n}\n\n#[cfg(feature = \"with_https\")]\nimpl From<httparse::Error> for Error {\n    fn from(inner: httparse::Error) -> Self {\n        Self::HttpError(inner)\n    }\n}\n"
  },
  {
    "path": "dns-transport/src/https.rs",
    "content": "#![cfg_attr(not(feature = \"https\"), allow(unused))]\n\nuse std::io::{Read, Write};\nuse std::net::TcpStream;\n\nuse log::*;\n\nuse dns::{Request, Response, WireError};\nuse super::{Transport, Error};\n\nuse super::tls_stream;\n\n/// The **HTTPS transport**, which sends DNS wire data inside HTTP packets\n/// encrypted with TLS, using TCP.\npub struct HttpsTransport {\n    url: String,\n}\n\nimpl HttpsTransport {\n\n    /// Creates a new HTTPS transport that connects to the given URL.\n    pub fn new(url: String) -> Self {\n        Self { url }\n    }\n}\n\nfn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {\n    haystack.windows(needle.len()).position(|window| window == needle)\n}\n\nfn contains_header(buf: &[u8]) -> bool {\n    let header_end: [u8; 4] = [ 13, 10, 13, 10 ];\n    find_subsequence(buf, &header_end).is_some()\n}\n\nuse tls_stream::TlsStream;\n\nimpl Transport for HttpsTransport {\n\n    #[cfg(any(feature = \"with_https\"))]\n    fn send(&self, request: &Request) -> Result<Response, Error> {\n        let (domain, path) = self.split_domain().expect(\"Invalid HTTPS nameserver\");\n\n        info!(\"Opening TLS socket to {:?}\", domain);\n        let mut stream = Self::stream(&domain, 443)?;\n\n        debug!(\"Connected\");\n\n        let request_bytes = request.to_bytes().expect(\"failed to serialise request\");\n        let mut bytes_to_send = format!(\"\\\n            POST {} HTTP/1.1\\r\\n\\\n            Host: {}\\r\\n\\\n            Content-Type: application/dns-message\\r\\n\\\n            Accept: application/dns-message\\r\\n\\\n            User-Agent: {}\\r\\n\\\n            Content-Length: {}\\r\\n\\r\\n\",\n            path, domain, USER_AGENT, request_bytes.len()).into_bytes();\n        bytes_to_send.extend(request_bytes);\n\n        info!(\"Sending {} bytes of data to {:?} over HTTPS\", bytes_to_send.len(), self.url);\n        stream.write_all(&bytes_to_send)?;\n        debug!(\"Wrote all bytes\");\n\n        info!(\"Waiting to receive...\");\n        let mut buf = [0; 4096];\n        let mut read_len = stream.read(&mut buf)?;\n        while !contains_header(&buf[0..read_len]) {\n            if read_len == buf.len() {\n                return Err(Error::WireError(WireError::IO));\n            }\n            read_len += stream.read(&mut buf[read_len..])?;\n        }\n        let mut expected_len = read_len;\n        info!(\"Received {} bytes of data\", read_len);\n\n        let mut headers = [httparse::EMPTY_HEADER; 16];\n        let mut response = httparse::Response::new(&mut headers);\n        let index: usize = response.parse(&buf)?.unwrap();\n\n        if response.code != Some(200) {\n            let reason = response.reason.map(str::to_owned);\n            return Err(Error::WrongHttpStatus(response.code.unwrap(), reason));\n        }\n\n        for header in response.headers {\n            let str_value = String::from_utf8_lossy(header.value);\n            debug!(\"Header {:?} -> {:?}\", header.name, str_value);\n            if header.name == \"Content-Length\" {\n                let content_length: usize = str_value.parse().unwrap();\n                expected_len = index + content_length;\n            }\n        }\n\n        while read_len < expected_len {\n            if read_len == buf.len() {\n                return Err(Error::WireError(WireError::IO));\n            }\n            read_len += stream.read(&mut buf[read_len..])?;\n        }\n\n        let body = &buf[index .. read_len];\n        debug!(\"HTTP body has {} bytes\", body.len());\n        let response = Response::from_bytes(&body)?;\n        Ok(response)\n    }\n\n    #[cfg(not(feature = \"with_https\"))]\n    fn send(&self, request: &Request) -> Result<Response, Error> {\n        unreachable!(\"HTTPS feature disabled\")\n    }\n}\n\nimpl HttpsTransport {\n    fn split_domain(&self) -> Option<(&str, &str)> {\n        if let Some(sp) = self.url.strip_prefix(\"https://\") {\n            if let Some(colon_index) = sp.find('/') {\n                return Some((&sp[.. colon_index], &sp[colon_index ..]));\n            }\n        }\n\n        None\n    }\n}\n\n/// The User-Agent header sent with HTTPS requests.\nstatic USER_AGENT: &str = concat!(\"dog/\", env!(\"CARGO_PKG_VERSION\"));\n\n"
  },
  {
    "path": "dns-transport/src/lib.rs",
    "content": "//! All the DNS transport types.\n\n#![warn(deprecated_in_future)]\n#![warn(future_incompatible)]\n#![warn(missing_copy_implementations)]\n#![warn(missing_docs)]\n#![warn(nonstandard_style)]\n#![warn(rust_2018_compatibility)]\n#![warn(rust_2018_idioms)]\n#![warn(single_use_lifetimes)]\n#![warn(trivial_casts, trivial_numeric_casts)]\n#![warn(unused)]\n\n#![warn(clippy::all, clippy::pedantic)]\n#![allow(clippy::module_name_repetitions)]\n#![allow(clippy::must_use_candidate)]\n#![allow(clippy::option_if_let_else)]\n#![allow(clippy::pub_enum_variant_names)]\n#![allow(clippy::wildcard_imports)]\n\n#![deny(clippy::cast_possible_truncation)]\n#![deny(clippy::cast_lossless)]\n#![deny(clippy::cast_possible_wrap)]\n#![deny(clippy::cast_sign_loss)]\n#![deny(unsafe_code)]\n\n\nmod auto;\npub use self::auto::AutoTransport;\n\nmod udp;\npub use self::udp::UdpTransport;\n\nmod tcp;\npub use self::tcp::TcpTransport;\n\nmod tls;\npub use self::tls::TlsTransport;\n\nmod https;\npub use self::https::HttpsTransport;\n\nmod error;\n\nmod tls_stream;\n\npub use self::error::Error;\n\n/// The trait implemented by all transport types.\npub trait Transport {\n\n    /// Convert the request to bytes, send it over the network, wait for a\n    /// response, deserialise it from bytes, and return it, asynchronously.\n    ///\n    /// # Errors\n    ///\n    /// Returns an `Error` error if there’s an I/O error sending or\n    /// receiving data, or the DNS packet in the response contained invalid\n    /// bytes and failed to parse, or if there was a protocol-level error for\n    /// the TLS and HTTPS transports.\n    fn send(&self, request: &dns::Request) -> Result<dns::Response, Error>;\n}\n"
  },
  {
    "path": "dns-transport/src/tcp.rs",
    "content": "use std::convert::TryFrom;\nuse std::net::TcpStream;\nuse std::io::{Read, Write};\n\nuse log::*;\n\nuse dns::{Request, Response};\nuse super::{Transport, Error};\n\n\n/// The **TCP transport**, which sends DNS wire data over a TCP stream.\n///\n/// # References\n///\n/// - [RFC 1035 §4.2.2](https://tools.ietf.org/html/rfc1035) — Domain Names,\n///   Implementation and Specification (November 1987)\n/// - [RFC 7766](https://tools.ietf.org/html/rfc1035) — DNS Transport over\n///   TCP, Implementation Requirements (March 2016)\npub struct TcpTransport {\n    addr: String,\n}\n\nimpl TcpTransport {\n\n    /// Creates a new TCP transport that connects to the given host.\n    pub fn new(addr: String) -> Self {\n        Self { addr }\n    }\n}\n\n\nimpl Transport for TcpTransport {\n    fn send(&self, request: &Request) -> Result<Response, Error> {\n        info!(\"Opening TCP stream\");\n        let mut stream =\n            if self.addr.contains(':') {\n                TcpStream::connect(&*self.addr)?\n            }\n            else {\n                TcpStream::connect((&*self.addr, 53))?\n            };\n        debug!(\"Opened\");\n\n        // The message is prepended with the length when sent over TCP,\n        // so the server knows how long it is (RFC 1035 §4.2.2)\n        let mut bytes_to_send = request.to_bytes().expect(\"failed to serialise request\");\n        Self::prefix_with_length(&mut bytes_to_send);\n\n        info!(\"Sending {} bytes of data to {:?} over TCP\", bytes_to_send.len(), self.addr);\n        let written_len = stream.write(&bytes_to_send)?;\n        debug!(\"Wrote {} bytes\", written_len);\n\n        let read_bytes = Self::length_prefixed_read(&mut stream)?;\n        let response = Response::from_bytes(&read_bytes)?;\n        Ok(response)\n    }\n}\n\nimpl TcpTransport {\n\n    /// Mutate the given byte buffer, prefixing it with its own length as a\n    /// big-endian `u16`.\n    pub(crate) fn prefix_with_length(bytes: &mut Vec<u8>) {\n        let len_bytes = u16::try_from(bytes.len())\n            .expect(\"request too long\")\n            .to_be_bytes();\n\n        bytes.insert(0, len_bytes[0]);\n        bytes.insert(1, len_bytes[1]);\n    }\n\n    /// Reads from the given I/O source as many times as necessary to read a\n    /// length-prefixed stream of bytes. The first two bytes are taken as a\n    /// big-endian `u16` to determine the length. Then, that many bytes are\n    /// read from the source.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if there’s a network error during reading, or not\n    /// enough bytes have been sent.\n    pub(crate) fn length_prefixed_read(stream: &mut impl Read) -> Result<Vec<u8>, Error> {\n        info!(\"Waiting to receive...\");\n\n        let mut buf = [0; 4096];\n        let mut read_len = stream.read(&mut buf[..])?;\n\n        if read_len == 0 {\n            warn!(\"Received no bytes!\");\n            return Err(Error::TruncatedResponse);\n        }\n        else if read_len == 1 {\n            info!(\"Received one byte of data\");\n            let second_read_len = stream.read(&mut buf[1..])?;\n            if second_read_len == 0 {\n                warn!(\"Received no bytes the second time!\");\n                return Err(Error::TruncatedResponse);\n            }\n\n            read_len += second_read_len;\n        }\n        else {\n            info!(\"Received {} bytes of data\", read_len);\n        }\n\n        let total_len = u16::from_be_bytes([buf[0], buf[1]]);\n        if read_len - 2 == usize::from(total_len) {\n            debug!(\"We have enough bytes\");\n            return Ok(buf[2..read_len].to_vec());\n        }\n\n        debug!(\"We need to read {} bytes total\", total_len);\n        let mut combined_buffer = buf[2..read_len].to_vec();\n        while combined_buffer.len() < usize::from(total_len) {\n            let mut extend_buf = [0; 4096];\n            let extend_len = stream.read(&mut extend_buf[..])?;\n            info!(\"Received further {} bytes of data (of {})\", extend_len, total_len);\n\n            if read_len == 0 {\n                warn!(\"Read zero bytes!\");\n                return Err(Error::TruncatedResponse);\n            }\n\n            combined_buffer.extend(&extend_buf[0 .. extend_len]);\n        }\n\n        Ok(combined_buffer)\n    }\n}\n"
  },
  {
    "path": "dns-transport/src/tls.rs",
    "content": "#![cfg_attr(not(feature = \"tls\"), allow(unused))]\n\nuse std::net::TcpStream;\nuse std::io::Write;\n\nuse log::*;\n\nuse dns::{Request, Response};\nuse super::{Transport, Error, TcpTransport};\nuse super::tls_stream::TlsStream;\n\n\n/// The **TLS transport**, which sends DNS wire data using TCP through an\n/// encrypted TLS connection.\npub struct TlsTransport {\n    addr: String,\n}\n\nimpl TlsTransport {\n\n    /// Creates a new TLS transport that connects to the given host.\n    pub fn new(addr: String) -> Self {\n        Self { addr }\n    }\n}\n\n\n\nimpl Transport for TlsTransport {\n\n    #[cfg(feature = \"with_tls\")]\n    fn send(&self, request: &Request) -> Result<Response, Error> {\n        info!(\"Opening TLS socket\");\n\n        let domain = self.sni_domain();\n        info!(\"Connecting using domain {:?}\", domain);\n        let mut stream =\n            if self.addr.contains(':') {\n                let mut parts = self.addr.split(\":\");\n                let domain = parts.nth(0).unwrap();\n                let port = parts.last().unwrap().parse::<u16>().expect(\"Invalid port number\");\n\n                Self::stream(domain, port)?\n            }\n            else {\n                Self::stream(&*self.addr, 853)?\n            };\n\n\n        debug!(\"Connected\");\n\n        // The message is prepended with the length when sent over TCP,\n        // so the server knows how long it is (RFC 1035 §4.2.2)\n        let mut bytes_to_send = request.to_bytes().expect(\"failed to serialise request\");\n        TcpTransport::prefix_with_length(&mut bytes_to_send);\n\n        info!(\"Sending {} bytes of data to {} over TLS\", bytes_to_send.len(), self.addr);\n        stream.write_all(&bytes_to_send)?;\n        debug!(\"Wrote all bytes\");\n\n        let read_bytes = TcpTransport::length_prefixed_read(&mut stream)?;\n        let response = Response::from_bytes(&read_bytes)?;\n        Ok(response)\n    }\n\n    #[cfg(not(feature = \"with_tls\"))]\n    fn send(&self, request: &Request) -> Result<Response, Error> {\n        unreachable!(\"TLS feature disabled\")\n    }\n}\n\nimpl TlsTransport {\n    fn sni_domain(&self) -> &str {\n        if let Some(colon_index) = self.addr.find(':') {\n            &self.addr[.. colon_index]\n        }\n        else {\n            &self.addr[..]\n        }\n    }\n}\n"
  },
  {
    "path": "dns-transport/src/tls_stream.rs",
    "content": "use std::net::TcpStream;\nuse super::Error;\nuse super::HttpsTransport;\nuse super::TlsTransport;\n\n#[cfg(any(feature = \"with_nativetls\", feature = \"with_nativetls_vendored\"))]\nfn stream_nativetls(domain: &str, port: u16) -> Result<native_tls::TlsStream<TcpStream>, Error> {\n    let connector = native_tls::TlsConnector::new()?;\n    let stream = TcpStream::connect((domain, port))?;\n    Ok(connector.connect(domain, stream)?)\n}\n\n#[cfg(feature = \"with_rustls\")]\nfn stream_rustls(domain: &str, port: u16) -> Result<rustls::StreamOwned<rustls::ClientSession,TcpStream>, Error> {\n    use std::sync::Arc;\n\n    let mut config = rustls::ClientConfig::new();\n\n    config.root_store.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);\n\n    let dns_name = webpki::DNSNameRef::try_from_ascii_str(domain)?;\n\n    let conn = rustls::ClientSession::new(&Arc::new(config), dns_name);\n\n    let sock = TcpStream::connect((domain, port))?;\n    let tls = rustls::StreamOwned::new(conn, sock);\n\n    Ok(tls)\n}\n\npub trait TlsStream<S: std::io::Read + std::io::Write> {\n    fn stream(domain: &str, port: u16) -> Result<S, Error>;\n}\n\n#[cfg(any(feature = \"with_tls\", feature = \"with_https\"))]\ncfg_if::cfg_if! {\n    if #[cfg(any(feature = \"with_nativetls\", feature = \"with_nativetls_vendored\"))] {\n\n        impl TlsStream<native_tls::TlsStream<TcpStream>> for HttpsTransport {\n            fn stream(domain: &str, port: u16) -> Result<native_tls::TlsStream<TcpStream>, Error> {\n                stream_nativetls(domain, port)\n            }\n        }\n\n        impl TlsStream<native_tls::TlsStream<TcpStream>> for TlsTransport {\n            fn stream(domain: &str, port: u16) -> Result<native_tls::TlsStream<TcpStream>, Error> {\n                stream_nativetls(domain, port)\n            }\n        }\n\n    } else if #[cfg(feature = \"with_rustls\")] {\n\n        impl TlsStream<rustls::StreamOwned<rustls::ClientSession,TcpStream>> for HttpsTransport {\n            fn stream(domain: &str, port: u16) -> Result<rustls::StreamOwned<rustls::ClientSession,TcpStream>, Error> {\n                stream_rustls(domain, port)\n            }\n        }\n\n        impl TlsStream<rustls::StreamOwned<rustls::ClientSession,TcpStream>> for TlsTransport {\n            fn stream(domain: &str, port: u16) -> Result<rustls::StreamOwned<rustls::ClientSession,TcpStream>, Error> {\n                stream_rustls(domain, port)\n            }\n        }\n\n    } else {\n        unreachable!(\"tls/https enabled but no tls implementation provided\")\n    }\n}\n\n"
  },
  {
    "path": "dns-transport/src/udp.rs",
    "content": "use std::net::{Ipv4Addr, UdpSocket};\n\nuse log::*;\n\nuse dns::{Request, Response};\nuse super::{Transport, Error};\n\n\n/// The **UDP transport**, which sends DNS wire data inside a UDP datagram.\n///\n/// # References\n///\n/// - [RFC 1035 §4.2.1](https://tools.ietf.org/html/rfc1035) — Domain Names,\n///   Implementation and Specification (November 1987)\npub struct UdpTransport {\n    addr: String,\n}\n\nimpl UdpTransport {\n\n    /// Creates a new UDP transport that connects to the given host.\n    pub fn new(addr: String) -> Self {\n        Self { addr }\n    }\n}\n\n\nimpl Transport for UdpTransport {\n    fn send(&self, request: &Request) -> Result<Response, Error> {\n        info!(\"Opening UDP socket\");\n        // TODO: This will need to be changed for IPv6 support.\n        let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0))?;\n\n        if self.addr.contains(':') {\n            socket.connect(&*self.addr)?;\n        }\n        else {\n            socket.connect((&*self.addr, 53))?;\n        }\n        debug!(\"Opened\");\n\n        let bytes_to_send = request.to_bytes().expect(\"failed to serialise request\");\n\n        info!(\"Sending {} bytes of data to {} over UDP\", bytes_to_send.len(), self.addr);\n        let written_len = socket.send(&bytes_to_send)?;\n        debug!(\"Wrote {} bytes\", written_len);\n\n        info!(\"Waiting to receive...\");\n        let mut buf = vec![0; 4096];\n        let received_len = socket.recv(&mut buf)?;\n\n        info!(\"Received {} bytes of data\", received_len);\n        let response = Response::from_bytes(&buf[.. received_len])?;\n        Ok(response)\n    }\n}\n"
  },
  {
    "path": "man/dog.1.md",
    "content": "% dog(1) v0.1.0\n\n<!-- This is the dog(1) man page, written in Markdown. -->\n<!-- To generate the roff version, run `just man`, -->\n<!-- and the man page will appear in the ‘target’ directory. -->\n\n\nNAME\n====\n\ndog — a command-line DNS client\n\n\nSYNOPSIS\n========\n\n`dog [options] [domains...]`\n\n**dog** is a command-line DNS client.\nIt has colourful output, supports the DNS-over-TLS and DNS-over-HTTPS protocols, and can emit JSON.\n\n\nEXAMPLES\n========\n\n`dog example.net`\n: Query the `A` record of a domain using default settings\n\n`dog example.net MX`\n: ...looking up `MX` records instead\n\n`dog example.net MX @1.1.1.1`\n: ...using a specific nameserver instead\n\n`dog example.net MX @1.1.1.1 -T`\n: ...using TCP rather than UDP\n\n`dog -q example.net -t MX -n 1.1.1.1 -T`\n: As above, but using explicit arguments\n\n\nQUERY OPTIONS\n=============\n\n`-q`, `--query=HOST`\n: Host name or domain name to query.\n\n`-t`, `--type=TYPE`\n: Type of the DNS record being queried (`A`, `MX`, `NS`...)\n\n`-n`, `--nameserver=ADDR`\n: Address of the nameserver to send packets to.\n\n`--class=CLASS`\n: Network class of the DNS record being queried (`IN`, `CH`, `HS`)\n\nBy default, dog will request A records using the system default resolver. At least one domain name must be passed — dog will not automatically query the root nameservers.\n\nQuery options passed in using a command-line option, such as ‘`--query lookup.dog`’ or ‘`--type MX`’, or as plain arguments, such as ‘`lookup.dog`’ or ‘`MX`’. dog will make an intelligent guess as to what plain arguments mean (`MX` is quite clearly a type), which makes it easier to compose ad-hoc queries quickly. If precision is desired, use the long-form options.\n\nIf more than one domain, type, nameserver, or class is specified, dog will perform one query for each combination, and display the combined results in a table. For example, passing three type arguments and two domain name arguments will send six requests.\n\nDNS traditionally uses port 53 for both TCP and UDP. To use a resolver with a different port, include the port number after a colon (`:`) in the nameserver address.\n\n\nSENDING OPTIONS\n===============\n\n`--edns=SETTING`\n: Whether to opt in to DNS. This can be ‘`disable`’, ‘`hide`’, or ‘`show`’.\n\n`--txid=NUMBER`\n: Set the transaction ID to a specific value.\n\n`-Z=TWEAKS`\n: Set uncommon protocol-level tweaks.\n\n\nTRANSPORT OPTIONS\n=================\n\n`-U`, `--udp`\n: Use the DNS protocol over UDP.\n\n`-T`, `--tcp`\n: Use the DNS protocol over TCP.\n\n`-S`, `--tls`\n: Use the DNS-over-TLS protocol.\n\n`-H`, `--https`\n: Use the DNS-over-HTTPS protocol.\n\nBy default, dog will use the UDP protocol, automatically re-sending the request using TCP if the response indicates that the message is too large for UDP. Passing `--udp` will only use UDP and will fail in this case; passing `--tcp` will use TCP by default.\n\nThe DNS-over-TLS (DoT) and DNS-over-HTTPS (DoH) protocols are available with the `--tls` and `--https` options. Bear in mind that the system default resolver is unlikely to respond to requests using these protocols.\n\nNote that if a hostname or domain name is given as a nameserver, rather than an IP address, the resolution of that host is performed by the operating system, _not_ by dog.\n\nUnlike the others, the HTTPS transport type requires an entire URL, complete with protocol, domain name, and path.\n\n\nOUTPUT OPTIONS\n==============\n\n`-1`, `--short`\n: Short mode: display nothing but the first result.\n\n`-J`, `--json`\n: Display the output as JSON.\n\n`--color`, `--colour=WHEN`\n: When to colourise the output. This can be ‘`always`’, ‘`automatic`’, or ‘`never`’.\n\n`--seconds`\n: Do not format durations as hours and minutes; instead, display them as seconds.\n\n`--time`\n: Print how long the response took to arrive.\n\n\nMETA OPTIONS\n============\n\n`--help`\n: Displays an overview of the command-line options.\n\n`--version`\n: Displays the version of dog being invoked.\n\n\nENVIRONMENT VARIABLES\n=====================\n\ndog responds to the following environment variables:\n\n## `DOG_DEBUG`\n\nSet this to any non-empty value to have dog emit debugging information to standard error. For more in-depth output, set this to the exact string ‘`trace`’.\n\n\nRECORD TYPES\n============\n\ndog understands and can interpret the following record types:\n\n`A`\n: IPv4 addresses\n\n`AAAA`\n: IPv6 addresses\n\n`CAA`\n: permitted certificate authorities\n\n`CNAME`\n: canonical domain aliases\n\n`HINFO`\n: system information and, sometimes, forbidden request explanations\n\n`LOC`\n: location information\n\n`MX`\n: e-mail server addresses\n\n`NAPTR`\n: DDDS rules\n\n`NS`\n: domain name servers\n\n`OPT`\n: extensions to the DNS protocol\n\n`PTR`\n: pointers to canonical names, usually for reverse lookups\n\n`SOA`\n: administrative information about zones\n\n`SRV`\n: IP addresses with port numbers\n\n`SSHFP`\n: SSH key fingerprints\n\n`TLSA`\n: TLS certificates, public keys, and hashes\n\n`TXT`\n: arbitrary textual information\n\nWhen a response DNS packet contains a record of one of these known types, dog will display it in a table containing the type name and a human-readable summary of its contents.\n\nRecords with a type number that does not map to any known record type will still be displayed. As they cannot be interpreted, their contents will be displayed as a series of numbers instead.\n\ndog also contains a list of record type names that it knows the type number of, but is not able to interpret, such as `IXFR` or `ANY` or `AFSDB`. These are acceptable as command-line arguments, meaning you can send an AFSDB request with ‘`dog AFSDB`’. However, their response contents will still be displayed as numbers. They may be supported in future versions of dog.\n\n\nPROTOCOL TWEAKS\n===============\n\nThe `-Z` command-line argument can be used one or more times to set some protocol-level options in the DNS queries that get sent. It accepts the following values:\n\n`aa`\n: Sets the `AA` (Authoritative Answers) bit in the query.\n\n`ad`\n: Sets the `AD` (Authentic Data) bit in the query.\n\n`bufsize=NUM`\n: Sets the UDP payload size field in the OPT field in the query. This has no effect if EDNS is diabled.\n\n`cd`\n: Sets the `CD` (Checking Disabled) bit in the query.\n\n\nEXIT STATUSES\n=============\n\n0\n: If everything goes OK.\n\n1\n: If there was a network, I/O, or TLS error during operation.\n\n2\n: If there is no result from the server when running in short mode. This can be any received server error, not just NXDOMAIN.\n\n3\n: If there was a problem with the command-line arguments.\n\n4\n: If there was a problem obtaining the system nameserver information.\n\n\nAUTHOR\n======\n\ndog is maintained by Benjamin ‘ogham’ Sago.\n\n**Website:** `https://dns.lookup.dog/` \\\n**Source code:** `https://github.com/ogham/dog`\n"
  },
  {
    "path": "src/colours.rs",
    "content": "//! Colours, colour schemes, and terminal styling.\n\nuse ansi_term::Style;\nuse ansi_term::Color::*;\n\n\n/// The **colours** are used to paint the input.\n#[derive(Debug, Default)]\npub struct Colours {\n    pub qname: Style,\n\n    pub answer: Style,\n    pub authority: Style,\n    pub additional: Style,\n\n    pub a: Style,\n    pub aaaa: Style,\n    pub caa: Style,\n    pub cname: Style,\n    pub eui48: Style,\n    pub eui64: Style,\n    pub hinfo: Style,\n    pub loc: Style,\n    pub mx: Style,\n    pub ns: Style,\n    pub naptr: Style,\n    pub openpgpkey: Style,\n    pub opt: Style,\n    pub ptr: Style,\n    pub sshfp: Style,\n    pub soa: Style,\n    pub srv: Style,\n    pub tlsa: Style,\n    pub txt: Style,\n    pub uri: Style,\n    pub unknown: Style,\n}\n\nimpl Colours {\n\n    /// Create a new colour palette that has a variety of different styles\n    /// defined. This is used by default.\n    pub fn pretty() -> Self {\n        Self {\n            qname: Blue.bold(),\n\n            answer: Style::default(),\n            authority: Cyan.normal(),\n            additional: Green.normal(),\n\n            a: Green.bold(),\n            aaaa: Green.bold(),\n            caa: Red.normal(),\n            cname: Yellow.normal(),\n            eui48: Yellow.normal(),\n            eui64: Yellow.bold(),\n            hinfo: Yellow.normal(),\n            loc: Yellow.normal(),\n            mx: Cyan.normal(),\n            naptr: Green.normal(),\n            ns: Red.normal(),\n            openpgpkey: Cyan.normal(),\n            opt: Purple.normal(),\n            ptr: Red.normal(),\n            sshfp: Cyan.normal(),\n            soa: Purple.normal(),\n            srv: Cyan.normal(),\n            tlsa: Yellow.normal(),\n            txt: Yellow.normal(),\n            uri: Yellow.normal(),\n            unknown: White.on(Red),\n        }\n    }\n\n    /// Create a new colour palette where no styles are defined, causing\n    /// output to be rendered as plain text without any formatting.\n    /// This is used when output is not to a terminal.\n    pub fn plain() -> Self {\n        Self::default()\n    }\n}\n"
  },
  {
    "path": "src/connect.rs",
    "content": "//! Creating DNS transports based on the user’s input arguments.\n\nuse dns_transport::*;\n\n\n/// A **transport type** creates a `Transport` that determines which protocols\n/// should be used to send and receive DNS wire data over the network.\n#[derive(PartialEq, Debug, Copy, Clone)]\npub enum TransportType {\n\n    /// Send packets over UDP or TCP.\n    /// UDP is used by default. If the request packet would be too large, send\n    /// a TCP packet instead; if a UDP _response_ packet is truncated, try\n    /// again with TCP.\n    Automatic,\n\n    /// Send packets over UDP only.\n    /// If the request packet is too large or the response packet is\n    /// truncated, fail with an error.\n    UDP,\n\n    /// Send packets over TCP only.\n    TCP,\n\n    /// Send encrypted DNS-over-TLS packets.\n    TLS,\n\n    /// Send encrypted DNS-over-HTTPS packets.\n    HTTPS,\n}\n\nimpl TransportType {\n\n    /// Creates a boxed `Transport` depending on the transport type. The\n    /// parameter will be a URL for the HTTPS transport type, and a\n    /// stringified address for the others.\n    pub fn make_transport(self, param: String) -> Box<dyn Transport> {\n        match self {\n            Self::Automatic  => Box::new(AutoTransport::new(param)),\n            Self::UDP        => Box::new(UdpTransport::new(param)),\n            Self::TCP        => Box::new(TcpTransport::new(param)),\n            Self::TLS        => Box::new(TlsTransport::new(param)),\n            Self::HTTPS      => Box::new(HttpsTransport::new(param)),\n        }\n    }\n}\n"
  },
  {
    "path": "src/hints.rs",
    "content": "//! Hints to the user made before a query is sent, in case the answer that\n//! comes back isn’t what they expect.\n\nuse std::collections::BTreeSet;\nuse std::fs::File;\nuse std::io;\n\nuse log::*;\n\n\n/// The set of hostnames that are configured to point to a specific host in\n/// the hosts file on the local machine. This gets queried before a request is\n/// made: because the running OS will consult the hosts file before looking up\n/// a hostname, but dog will not, it’s possible for dog to output one address\n/// while the OS is using another. dog displays a warning when this is the\n/// case, to prevent confusion.\n#[derive(Default)]\npub struct LocalHosts {\n    hostnames: BTreeSet<dns::Labels>,\n}\n\nimpl LocalHosts {\n\n    /// Loads the set of hostnames from the hosts file path on Unix.\n    #[cfg(unix)]\n    pub fn load() -> io::Result<Self> {\n        debug!(\"Reading hints from /etc/hosts\");\n        Self::load_from_file(File::open(\"/etc/hosts\")?)\n    }\n\n    /// Loads the set of hostnames from the hosts file path on Windows.\n    #[cfg(windows)]\n    pub fn load() -> io::Result<Self> {\n        debug!(\"Reading hints from /etc/hosts equivalent\");\n        Self::load_from_file(File::open(\"C:\\\\Windows\\\\system32\\\\drivers\\\\etc\\\\hosts\")?)\n    }\n\n    /// On other machines, load an empty set of hostnames that match nothing.\n    #[cfg(all(not(windows), not(unix)))]\n    pub fn load() -> io::Result<Self> {\n        Ok(Self::default())\n    }\n\n    /// Reads hostnames from the given file and returns them as a `LocalHosts`\n    /// struct, or an I/O error if one occurs. The file should be in the\n    /// standard `/etc/hosts` format, with one entry per line, separated by\n    /// whitespace, where the first field is the address and the remaining\n    /// fields are hostname aliases, and `#` signifies a comment.\n    fn load_from_file(file: File) -> io::Result<Self> {\n        use std::io::{BufRead, BufReader};\n\n        if cfg!(test) {\n            panic!(\"load_from_file() called from test code\");\n        }\n\n        let reader = BufReader::new(file);\n\n        let mut hostnames = BTreeSet::new();\n        for line in reader.lines() {\n            let mut line = line?;\n\n            if let Some(hash_index) = line.find('#') {\n                line.truncate(hash_index);\n            }\n\n            for hostname in line.split_ascii_whitespace().skip(1) {\n                match dns::Labels::encode(hostname) {\n                    Ok(hn) => {\n                        hostnames.insert(hn);\n                    }\n                    Err(e) => {\n                        warn!(\"Failed to encode local host hint {:?}: {}\", hostname, e);\n                    }\n                }\n            }\n        }\n\n        trace!(\"{} hostname hints loaded OK.\", hostnames.len());\n        Ok(Self { hostnames })\n    }\n\n    /// Queries this set of hostnames to see if the given name, which is about\n    /// to be queried for, exists within the file.\n    pub fn contains(&self, hostname_in_query: &dns::Labels) -> bool {\n        self.hostnames.contains(hostname_in_query)\n    }\n}\n"
  },
  {
    "path": "src/logger.rs",
    "content": "//! Debug error logging.\n\nuse std::ffi::OsStr;\n\nuse ansi_term::{Colour, ANSIString};\n\n\n/// Sets the internal logger, changing the log level based on the value of an\n/// environment variable.\npub fn configure<T: AsRef<OsStr>>(ev: Option<T>) {\n    let ev = match ev {\n        Some(v)  => v,\n        None     => return,\n    };\n\n    let env_var = ev.as_ref();\n    if env_var.is_empty() {\n        return;\n    }\n\n    if env_var == \"trace\" {\n        log::set_max_level(log::LevelFilter::Trace);\n    }\n    else {\n        log::set_max_level(log::LevelFilter::Debug);\n    }\n\n    let result = log::set_logger(GLOBAL_LOGGER);\n    if let Err(e) = result {\n        eprintln!(\"Failed to initialise logger: {}\", e);\n    }\n}\n\n\n#[derive(Debug)]\nstruct Logger;\n\nconst GLOBAL_LOGGER: &Logger = &Logger;\n\nimpl log::Log for Logger {\n    fn enabled(&self, _: &log::Metadata<'_>) -> bool {\n        true  // no need to filter after using ‘set_max_level’.\n    }\n\n    fn log(&self, record: &log::Record<'_>) {\n        let open = Colour::Fixed(243).paint(\"[\");\n        let level = level(record.level());\n        let close = Colour::Fixed(243).paint(\"]\");\n\n        eprintln!(\"{}{} {}{} {}\", open, level, record.target(), close, record.args());\n    }\n\n    fn flush(&self) {\n        // no need to flush with ‘eprintln!’.\n    }\n}\n\nfn level(level: log::Level) -> ANSIString<'static> {\n    match level {\n        log::Level::Error => Colour::Red.paint(\"ERROR\"),\n        log::Level::Warn  => Colour::Yellow.paint(\"WARN\"),\n        log::Level::Info  => Colour::Cyan.paint(\"INFO\"),\n        log::Level::Debug => Colour::Blue.paint(\"DEBUG\"),\n        log::Level::Trace => Colour::Fixed(245).paint(\"TRACE\"),\n    }\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "//! dog, the command-line DNS client.\n\n#![warn(deprecated_in_future)]\n#![warn(future_incompatible)]\n#![warn(missing_copy_implementations)]\n#![warn(missing_docs)]\n#![warn(nonstandard_style)]\n#![warn(rust_2018_compatibility)]\n#![warn(rust_2018_idioms)]\n#![warn(single_use_lifetimes)]\n#![warn(trivial_casts, trivial_numeric_casts)]\n#![warn(unused)]\n\n#![warn(clippy::all, clippy::pedantic)]\n#![allow(clippy::enum_glob_use)]\n#![allow(clippy::module_name_repetitions)]\n#![allow(clippy::option_if_let_else)]\n#![allow(clippy::too_many_lines)]\n#![allow(clippy::upper_case_acronyms)]\n#![allow(clippy::wildcard_imports)]\n\n#![deny(unsafe_code)]\n\nuse log::*;\n\nmod colours;\nmod connect;\nmod hints;\nmod logger;\nmod output;\nmod requests;\nmod resolve;\nmod table;\nmod txid;\n\nmod options;\nuse self::options::*;\n\n\n/// Configures logging, parses the command-line options, and handles any\n/// errors before passing control over to the Dog type.\nfn main() {\n    use std::env;\n    use std::process::exit;\n\n    logger::configure(env::var_os(\"DOG_DEBUG\"));\n\n    #[cfg(windows)]\n    if let Err(e) = ansi_term::enable_ansi_support() {\n        warn!(\"Failed to enable ANSI support: {}\", e);\n    }\n\n    match Options::getopts(env::args_os().skip(1)) {\n        OptionsResult::Ok(options) => {\n            info!(\"Running with options -> {:#?}\", options);\n            disabled_feature_check(&options);\n            exit(run(options));\n        }\n\n        OptionsResult::Help(help_reason, use_colours) => {\n            if use_colours.should_use_colours() {\n                print!(\"{}\", include_str!(concat!(env!(\"OUT_DIR\"), \"/usage.pretty.txt\")));\n            }\n            else {\n                print!(\"{}\", include_str!(concat!(env!(\"OUT_DIR\"), \"/usage.bland.txt\")));\n            }\n\n            if help_reason == HelpReason::NoDomains {\n                exit(exits::OPTIONS_ERROR);\n            }\n            else {\n                exit(exits::SUCCESS);\n            }\n        }\n\n        OptionsResult::Version(use_colours) => {\n            if use_colours.should_use_colours() {\n                print!(\"{}\", include_str!(concat!(env!(\"OUT_DIR\"), \"/version.pretty.txt\")));\n            }\n            else {\n                print!(\"{}\", include_str!(concat!(env!(\"OUT_DIR\"), \"/version.bland.txt\")));\n            }\n\n            exit(exits::SUCCESS);\n        }\n\n        OptionsResult::InvalidOptionsFormat(oe) => {\n            eprintln!(\"dog: Invalid options: {}\", oe);\n            exit(exits::OPTIONS_ERROR);\n        }\n\n        OptionsResult::InvalidOptions(why) => {\n            eprintln!(\"dog: Invalid options: {}\", why);\n            exit(exits::OPTIONS_ERROR);\n        }\n    }\n}\n\n\n/// Runs dog with some options, returning the status to exit with.\nfn run(Options { requests, format, measure_time }: Options) -> i32 {\n    use std::time::Instant;\n\n    let should_show_opt = requests.edns.should_show();\n\n    let mut responses = Vec::new();\n    let timer = if measure_time { Some(Instant::now()) } else { None };\n\n    let mut errored = false;\n\n    let local_host_hints = match hints::LocalHosts::load() {\n        Ok(lh) => lh,\n        Err(e) => {\n            warn!(\"Error loading local host hints: {}\", e);\n            hints::LocalHosts::default()\n        }\n    };\n\n    for hostname_in_query in &requests.inputs.domains {\n        if local_host_hints.contains(hostname_in_query) {\n            eprintln!(\"warning: domain '{}' also exists in hosts file\", hostname_in_query);\n        }\n    }\n\n    let request_tuples = match requests.generate() {\n        Ok(rt) => rt,\n        Err(e) => {\n            eprintln!(\"Unable to obtain resolver: {}\", e);\n            return exits::SYSTEM_ERROR;\n        }\n    };\n\n    for (transport, request_list) in request_tuples {\n        let request_list_len = request_list.len();\n        for (i, request) in request_list.into_iter().enumerate() {\n            let result = transport.send(&request);\n\n            match result {\n                Ok(mut response) => {\n                    if response.flags.error_code.is_some() && i != request_list_len - 1 {\n                        continue;\n                    }\n\n                    if ! should_show_opt {\n                        response.answers.retain(dns::Answer::is_standard);\n                        response.authorities.retain(dns::Answer::is_standard);\n                        response.additionals.retain(dns::Answer::is_standard);\n                    }\n\n                    responses.push(response);\n                    break;\n                }\n                Err(e) => {\n                    format.print_error(e);\n                    errored = true;\n                    break;\n                }\n            }\n        }\n    }\n\n    let duration = timer.map(|t| t.elapsed());\n    if format.print(responses, duration) {\n        if errored {\n            exits::NETWORK_ERROR\n        }\n        else {\n            exits::SUCCESS\n        }\n    }\n    else {\n        exits::NO_SHORT_RESULTS\n    }\n}\n\n\n/// Checks whether the options contain parameters that will cause dog to fail\n/// because the feature is disabled by exiting if so.\n#[allow(unused)]\nfn disabled_feature_check(options: &Options) {\n    use std::process::exit;\n    use crate::connect::TransportType;\n\n    #[cfg(all(not(feature = \"with_tls\"), not(feature = \"with_rustls_tls\")))]\n    if options.requests.inputs.transport_types.contains(&TransportType::TLS) {\n        eprintln!(\"dog: Cannot use '--tls': This version of dog has been compiled without TLS support\");\n        exit(exits::OPTIONS_ERROR);\n    }\n\n    #[cfg(all(not(feature = \"with_https\"), not(feature = \"with_rustls_https\")))]\n    if options.requests.inputs.transport_types.contains(&TransportType::HTTPS) {\n        eprintln!(\"dog: Cannot use '--https': This version of dog has been compiled without HTTPS support\");\n        exit(exits::OPTIONS_ERROR);\n    }\n}\n\n\n/// The possible status numbers dog can exit with.\nmod exits {\n\n    /// Exit code for when everything turns out OK.\n    pub const SUCCESS: i32 = 0;\n\n    /// Exit code for when there was at least one network error during execution.\n    pub const NETWORK_ERROR: i32 = 1;\n\n    /// Exit code for when there is no result from the server when running in\n    /// short mode. This can be any received server error, not just `NXDOMAIN`.\n    pub const NO_SHORT_RESULTS: i32 = 2;\n\n    /// Exit code for when the command-line options are invalid.\n    pub const OPTIONS_ERROR: i32 = 3;\n\n    /// Exit code for when the system network configuration could not be determined.\n    pub const SYSTEM_ERROR: i32 = 4;\n}\n"
  },
  {
    "path": "src/options.rs",
    "content": "//! Command-line option parsing.\n\nuse std::ffi::OsStr;\nuse std::fmt;\n\nuse log::*;\n\nuse dns::{QClass, Labels};\nuse dns::record::RecordType;\n\nuse crate::connect::TransportType;\nuse crate::output::{OutputFormat, UseColours, TextFormat};\nuse crate::requests::{RequestGenerator, Inputs, ProtocolTweaks, UseEDNS};\nuse crate::resolve::ResolverType;\nuse crate::txid::TxidGenerator;\n\n\n/// The command-line options used when running dog.\n#[derive(PartialEq, Debug)]\npub struct Options {\n\n    /// The requests to make and how they should be generated.\n    pub requests: RequestGenerator,\n\n    /// Whether to display the time taken after every query.\n    pub measure_time: bool,\n\n    /// How to format the output data.\n    pub format: OutputFormat,\n}\n\nimpl Options {\n\n    /// Parses and interprets a set of options from the user’s command-line\n    /// arguments.\n    ///\n    /// This returns an `Ok` set of options if successful and running\n    /// normally, a `Help` or `Version` variant if one of those options is\n    /// specified, or an error variant if there’s an invalid option or\n    /// inconsistency within the options after they were parsed.\n    #[allow(unused_results)]\n    pub fn getopts<C>(args: C) -> OptionsResult\n    where C: IntoIterator,\n          C::Item: AsRef<OsStr>,\n    {\n        let mut opts = getopts::Options::new();\n\n        // Query options\n        opts.optmulti(\"q\", \"query\",       \"Host name or domain name to query\", \"HOST\");\n        opts.optmulti(\"t\", \"type\",        \"Type of the DNS record being queried (A, MX, NS...)\", \"TYPE\");\n        opts.optmulti(\"n\", \"nameserver\",  \"Address of the nameserver to send packets to\", \"ADDR\");\n        opts.optmulti(\"\",  \"class\",       \"Network class of the DNS record being queried (IN, CH, HS)\", \"CLASS\");\n\n        // Sending options\n        opts.optopt  (\"\",  \"edns\",         \"Whether to OPT in to EDNS (disable, hide, show)\", \"SETTING\");\n        opts.optopt  (\"\",  \"txid\",         \"Set the transaction ID to a specific value\", \"NUMBER\");\n        opts.optmulti(\"Z\", \"\",             \"Set uncommon protocol tweaks\", \"TWEAKS\");\n\n        // Protocol options\n        opts.optflag (\"U\", \"udp\",          \"Use the DNS protocol over UDP\");\n        opts.optflag (\"T\", \"tcp\",          \"Use the DNS protocol over TCP\");\n        opts.optflag (\"S\", \"tls\",          \"Use the DNS-over-TLS protocol\");\n        opts.optflag (\"H\", \"https\",        \"Use the DNS-over-HTTPS protocol\");\n\n        // Output options\n        opts.optopt  (\"\",  \"color\",        \"When to use terminal colors\",  \"WHEN\");\n        opts.optopt  (\"\",  \"colour\",       \"When to use terminal colours\", \"WHEN\");\n        opts.optflag (\"J\", \"json\",         \"Display the output as JSON\");\n        opts.optflag (\"\",  \"seconds\",      \"Do not format durations, display them as seconds\");\n        opts.optflag (\"1\", \"short\",        \"Short mode: display nothing but the first result\");\n        opts.optflag (\"\",  \"time\",         \"Print how long the response took to arrive\");\n\n        // Meta options\n        opts.optflag (\"v\", \"version\",      \"Print version information\");\n        opts.optflag (\"?\", \"help\",         \"Print list of command-line options\");\n\n        let matches = match opts.parse(args) {\n            Ok(m)  => m,\n            Err(e) => return OptionsResult::InvalidOptionsFormat(e),\n        };\n\n        let uc = UseColours::deduce(&matches);\n\n        if matches.opt_present(\"version\") {\n            OptionsResult::Version(uc)\n        }\n        else if matches.opt_present(\"help\") {\n            OptionsResult::Help(HelpReason::Flag, uc)\n        }\n        else {\n            match Self::deduce(matches) {\n                Ok(opts) => {\n                    if opts.requests.inputs.domains.is_empty() {\n                        OptionsResult::Help(HelpReason::NoDomains, uc)\n                    }\n                    else {\n                        OptionsResult::Ok(opts)\n                    }\n                }\n                Err(e) => {\n                    OptionsResult::InvalidOptions(e)\n                }\n            }\n        }\n    }\n\n    fn deduce(matches: getopts::Matches) -> Result<Self, OptionsError> {\n        let measure_time = matches.opt_present(\"time\");\n        let format = OutputFormat::deduce(&matches);\n        let requests = RequestGenerator::deduce(matches)?;\n\n        Ok(Self { requests, measure_time, format })\n    }\n}\n\n\nimpl RequestGenerator {\n    fn deduce(matches: getopts::Matches) -> Result<Self, OptionsError> {\n        let edns = UseEDNS::deduce(&matches)?;\n        let txid_generator = TxidGenerator::deduce(&matches)?;\n        let protocol_tweaks = ProtocolTweaks::deduce(&matches)?;\n        let inputs = Inputs::deduce(matches)?;\n\n        Ok(Self { inputs, txid_generator, edns, protocol_tweaks })\n    }\n}\n\n\nimpl Inputs {\n    fn deduce(matches: getopts::Matches) -> Result<Self, OptionsError> {\n        let mut inputs = Self::default();\n        inputs.load_transport_types(&matches);\n        inputs.load_named_args(&matches)?;\n        inputs.load_free_args(matches)?;\n        inputs.check_for_missing_nameserver()?;\n        inputs.load_fallbacks();\n        Ok(inputs)\n    }\n\n    fn load_transport_types(&mut self, matches: &getopts::Matches) {\n        if matches.opt_present(\"https\") {\n            self.transport_types.push(TransportType::HTTPS);\n        }\n\n        if matches.opt_present(\"tls\") {\n            self.transport_types.push(TransportType::TLS);\n        }\n\n        if matches.opt_present(\"tcp\") {\n            self.transport_types.push(TransportType::TCP);\n        }\n\n        if matches.opt_present(\"udp\") {\n            self.transport_types.push(TransportType::UDP);\n        }\n    }\n\n    fn load_named_args(&mut self, matches: &getopts::Matches) -> Result<(), OptionsError> {\n        for domain in matches.opt_strs(\"query\") {\n            self.add_domain(&domain)?;\n        }\n\n        for record_name in matches.opt_strs(\"type\") {\n            if record_name.eq_ignore_ascii_case(\"OPT\") {\n                return Err(OptionsError::QueryTypeOPT);\n            }\n            else if let Some(record_type) = RecordType::from_type_name(&record_name) {\n                self.add_type(record_type);\n            }\n            else if let Ok(type_number) = record_name.parse::<u16>() {\n                self.record_types.push(RecordType::from(type_number));\n            }\n            else {\n                return Err(OptionsError::InvalidQueryType(record_name));\n            }\n        }\n\n        for ns in matches.opt_strs(\"nameserver\") {\n            self.add_nameserver(&ns);\n        }\n\n        for class_name in matches.opt_strs(\"class\") {\n            if let Some(class) = parse_class_name(&class_name) {\n                self.add_class(class);\n            }\n            else if let Ok(class_number) = class_name.parse() {\n                self.add_class(QClass::Other(class_number));\n            }\n            else {\n                return Err(OptionsError::InvalidQueryClass(class_name));\n            }\n        }\n\n        Ok(())\n    }\n\n    fn load_free_args(&mut self, matches: getopts::Matches) -> Result<(), OptionsError> {\n        for argument in matches.free {\n            if let Some(nameserver) = argument.strip_prefix('@') {\n                trace!(\"Got nameserver -> {:?}\", nameserver);\n                self.add_nameserver(nameserver);\n            }\n            else if is_constant_name(&argument) {\n                if argument.eq_ignore_ascii_case(\"OPT\") {\n                    return Err(OptionsError::QueryTypeOPT);\n                }\n                else if let Some(class) = parse_class_name(&argument) {\n                    trace!(\"Got qclass -> {:?}\", &argument);\n                    self.add_class(class);\n                }\n                else if let Some(record_type) = RecordType::from_type_name(&argument) {\n                    trace!(\"Got qtype -> {:?}\", &argument);\n                    self.add_type(record_type);\n                }\n                else {\n                    trace!(\"Got single-word domain -> {:?}\", &argument);\n                    self.add_domain(&argument)?;\n                }\n            }\n            else {\n                trace!(\"Got domain -> {:?}\", &argument);\n                self.add_domain(&argument)?;\n            }\n        }\n\n        Ok(())\n    }\n\n    fn check_for_missing_nameserver(&self) -> Result<(), OptionsError> {\n        if self.resolver_types.is_empty() && self.transport_types == [TransportType::HTTPS] {\n            Err(OptionsError::MissingHttpsUrl)\n        }\n        else {\n            Ok(())\n        }\n    }\n\n    fn load_fallbacks(&mut self) {\n        if self.record_types.is_empty() {\n            self.record_types.push(RecordType::A);\n        }\n\n        if self.classes.is_empty() {\n            self.classes.push(QClass::IN);\n        }\n\n        if self.resolver_types.is_empty() {\n            self.resolver_types.push(ResolverType::SystemDefault);\n        }\n\n        if self.transport_types.is_empty() {\n            self.transport_types.push(TransportType::Automatic);\n        }\n    }\n\n    fn add_domain(&mut self, input: &str) -> Result<(), OptionsError> {\n        if let Ok(domain) = Labels::encode(input) {\n            self.domains.push(domain);\n            Ok(())\n        }\n        else {\n            Err(OptionsError::InvalidDomain(input.into()))\n        }\n    }\n\n    fn add_type(&mut self, rt: RecordType) {\n        self.record_types.push(rt);\n    }\n\n    fn add_nameserver(&mut self, input: &str) {\n        self.resolver_types.push(ResolverType::Specific(input.into()));\n    }\n\n    fn add_class(&mut self, class: QClass) {\n        self.classes.push(class);\n    }\n}\n\nfn is_constant_name(argument: &str) -> bool {\n    let first_char = match argument.chars().next() {\n        Some(c)  => c,\n        None     => return false,\n    };\n\n    if ! first_char.is_ascii_alphabetic() {\n        return false;\n    }\n\n    argument.chars().all(|c| c.is_ascii_alphanumeric())\n}\n\nfn parse_class_name(input: &str) -> Option<QClass> {\n    if input.eq_ignore_ascii_case(\"IN\") {\n        Some(QClass::IN)\n    }\n    else if input.eq_ignore_ascii_case(\"CH\") {\n        Some(QClass::CH)\n    }\n    else if input.eq_ignore_ascii_case(\"HS\") {\n        Some(QClass::HS)\n    }\n    else {\n        None\n    }\n}\n\n\nimpl TxidGenerator {\n    fn deduce(matches: &getopts::Matches) -> Result<Self, OptionsError> {\n        if let Some(starting_txid) = matches.opt_str(\"txid\") {\n            if let Some(start) = parse_dec_or_hex(&starting_txid) {\n                Ok(Self::Sequence(start))\n            }\n            else {\n                Err(OptionsError::InvalidTxid(starting_txid))\n            }\n        }\n        else {\n            Ok(Self::Random)\n        }\n    }\n}\n\nfn parse_dec_or_hex(input: &str) -> Option<u16> {\n    if let Some(hex_str) = input.strip_prefix(\"0x\") {\n        match u16::from_str_radix(hex_str, 16) {\n            Ok(num) => {\n                Some(num)\n            }\n            Err(e) => {\n                warn!(\"Error parsing hex number: {}\", e);\n                None\n            }\n        }\n    }\n    else {\n        match input.parse() {\n            Ok(num) => {\n                Some(num)\n            }\n            Err(e) => {\n                warn!(\"Error parsing number: {}\", e);\n                None\n            }\n        }\n    }\n}\n\n\nimpl OutputFormat {\n    fn deduce(matches: &getopts::Matches) -> Self {\n        if matches.opt_present(\"short\") {\n            let summary_format = TextFormat::deduce(matches);\n            Self::Short(summary_format)\n        }\n        else if matches.opt_present(\"json\") {\n            Self::JSON\n        }\n        else {\n            let use_colours = UseColours::deduce(matches);\n            let summary_format = TextFormat::deduce(matches);\n            Self::Text(use_colours, summary_format)\n        }\n    }\n}\n\n\nimpl UseColours {\n    fn deduce(matches: &getopts::Matches) -> Self {\n        match matches.opt_str(\"color\").or_else(|| matches.opt_str(\"colour\")).unwrap_or_default().as_str() {\n            \"automatic\" | \"auto\" | \"\"  => Self::Automatic,\n            \"always\"    | \"yes\"        => Self::Always,\n            \"never\"     | \"no\"         => Self::Never,\n            otherwise => {\n                warn!(\"Unknown colour setting {:?}\", otherwise);\n                Self::Automatic\n            },\n        }\n    }\n}\n\n\nimpl TextFormat {\n    fn deduce(matches: &getopts::Matches) -> Self {\n        let format_durations = ! matches.opt_present(\"seconds\");\n        Self { format_durations }\n    }\n}\n\n\nimpl UseEDNS {\n    fn deduce(matches: &getopts::Matches) -> Result<Self, OptionsError> {\n        if let Some(edns) = matches.opt_str(\"edns\") {\n            match edns.as_str() {\n                \"disable\" | \"off\"  => Ok(Self::Disable),\n                \"hide\"             => Ok(Self::SendAndHide),\n                \"show\"             => Ok(Self::SendAndShow),\n                oh                 => Err(OptionsError::InvalidEDNS(oh.into())),\n            }\n        }\n        else {\n            Ok(Self::SendAndHide)\n        }\n    }\n}\n\n\nimpl ProtocolTweaks {\n    fn deduce(matches: &getopts::Matches) -> Result<Self, OptionsError> {\n        let mut tweaks = Self::default();\n\n        for tweak_str in matches.opt_strs(\"Z\") {\n            match &*tweak_str {\n                \"aa\" | \"authoritative\" => {\n                    tweaks.set_authoritative_flag = true;\n                }\n                \"ad\" | \"authentic\" => {\n                    tweaks.set_authentic_flag = true;\n                }\n                \"cd\" | \"checking-disabled\" => {\n                    tweaks.set_checking_disabled_flag = true;\n                }\n                otherwise => {\n                    if let Some(remaining_num) = tweak_str.strip_prefix(\"bufsize=\") {\n                        match remaining_num.parse() {\n                            Ok(parsed_bufsize) => {\n                                tweaks.udp_payload_size = Some(parsed_bufsize);\n                                continue;\n                            }\n                            Err(e) => {\n                                warn!(\"Failed to parse buffer size: {}\", e);\n                            }\n                        }\n                    }\n\n                    return Err(OptionsError::InvalidTweak(otherwise.into()));\n                }\n            }\n        }\n\n        Ok(tweaks)\n    }\n}\n\n\n/// The result of the `Options::getopts` function.\n#[derive(PartialEq, Debug)]\npub enum OptionsResult {\n\n    /// The options were parsed successfully.\n    Ok(Options),\n\n    /// There was an error (from `getopts`) parsing the arguments.\n    InvalidOptionsFormat(getopts::Fail),\n\n    /// There was an error with the combination of options the user selected.\n    InvalidOptions(OptionsError),\n\n    /// Can’t run any checks because there’s help to display!\n    Help(HelpReason, UseColours),\n\n    /// One of the arguments was `--version`, to display the version number.\n    Version(UseColours),\n}\n\n/// The reason that help is being displayed. If it’s for the `--help` flag,\n/// then we shouldn’t return an error exit status.\n#[derive(PartialEq, Debug, Copy, Clone)]\npub enum HelpReason {\n\n    /// Help was requested with the `--help` flag.\n    Flag,\n\n    /// There were no domains being queried, so display help instead.\n    /// Unlike `dig`, we don’t implicitly search for the root domain.\n    NoDomains,\n}\n\n/// Something wrong with the combination of options the user has picked.\n#[derive(PartialEq, Debug)]\npub enum OptionsError {\n    InvalidDomain(String),\n    InvalidEDNS(String),\n    InvalidQueryType(String),\n    InvalidQueryClass(String),\n    InvalidTxid(String),\n    InvalidTweak(String),\n    QueryTypeOPT,\n    MissingHttpsUrl,\n}\n\nimpl fmt::Display for OptionsError {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::InvalidDomain(domain)  => write!(f, \"Invalid domain {:?}\", domain),\n            Self::InvalidEDNS(edns)      => write!(f, \"Invalid EDNS setting {:?}\", edns),\n            Self::InvalidQueryType(qt)   => write!(f, \"Invalid query type {:?}\", qt),\n            Self::InvalidQueryClass(qc)  => write!(f, \"Invalid query class {:?}\", qc),\n            Self::InvalidTxid(txid)      => write!(f, \"Invalid transaction ID {:?}\", txid),\n            Self::InvalidTweak(tweak)    => write!(f, \"Invalid protocol tweak {:?}\", tweak),\n            Self::QueryTypeOPT           => write!(f, \"OPT request is sent by default (see -Z flag)\"),\n            Self::MissingHttpsUrl        => write!(f, \"You must pass a URL as a nameserver when using --https\"),\n        }\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n    use pretty_assertions::assert_eq;\n    use dns::record::UnknownQtype;\n\n    impl Inputs {\n        fn fallbacks() -> Self {\n            Inputs {\n                domains:         vec![ /* No domains by default */ ],\n                record_types:    vec![ RecordType::A ],\n                classes:         vec![ QClass::IN ],\n                resolver_types:  vec![ ResolverType::SystemDefault ],\n                transport_types: vec![ TransportType::Automatic ],\n            }\n        }\n    }\n\n    impl OptionsResult {\n        fn unwrap(self) -> Options {\n            match self {\n                Self::Ok(o)  => o,\n                _            => panic!(\"{:?}\", self),\n            }\n        }\n    }\n\n    // help tests\n\n    #[test]\n    fn help() {\n        assert_eq!(Options::getopts(&[ \"--help\" ]),\n                   OptionsResult::Help(HelpReason::Flag, UseColours::Automatic));\n    }\n\n    #[test]\n    fn help_no_colour() {\n        assert_eq!(Options::getopts(&[ \"--help\", \"--colour=never\" ]),\n                   OptionsResult::Help(HelpReason::Flag, UseColours::Never));\n    }\n\n    #[test]\n    fn version() {\n        assert_eq!(Options::getopts(&[ \"--version\" ]),\n                   OptionsResult::Version(UseColours::Automatic));\n    }\n\n    #[test]\n    fn version_yes_color() {\n        assert_eq!(Options::getopts(&[ \"--version\", \"--color\", \"always\" ]),\n                   OptionsResult::Version(UseColours::Always));\n    }\n\n    #[test]\n    fn fail() {\n        assert_eq!(Options::getopts(&[ \"--pear\" ]),\n                   OptionsResult::InvalidOptionsFormat(getopts::Fail::UnrecognizedOption(\"pear\".into())));\n    }\n\n    #[test]\n    fn empty() {\n        let nothing: Vec<&str> = vec![];\n        assert_eq!(Options::getopts(nothing),\n                   OptionsResult::Help(HelpReason::NoDomains, UseColours::Automatic));\n    }\n\n    #[test]\n    fn an_unrelated_argument() {\n        assert_eq!(Options::getopts(&[ \"--time\" ]),\n                   OptionsResult::Help(HelpReason::NoDomains, UseColours::Automatic));\n    }\n\n    // query tests\n\n    #[test]\n    fn just_domain() {\n        let options = Options::getopts(&[ \"lookup.dog\" ]).unwrap();\n        assert_eq!(options.requests.inputs, Inputs {\n            domains: vec![ Labels::encode(\"lookup.dog\").unwrap() ],\n            .. Inputs::fallbacks()\n        });\n    }\n\n    #[test]\n    fn just_named_domain() {\n        let options = Options::getopts(&[ \"-q\", \"lookup.dog\" ]).unwrap();\n        assert_eq!(options.requests.inputs, Inputs {\n            domains: vec![ Labels::encode(\"lookup.dog\").unwrap() ],\n            .. Inputs::fallbacks()\n        });\n    }\n\n    #[test]\n    fn domain_and_type() {\n        let options = Options::getopts(&[ \"lookup.dog\", \"SOA\" ]).unwrap();\n        assert_eq!(options.requests.inputs, Inputs {\n            domains:      vec![ Labels::encode(\"lookup.dog\").unwrap() ],\n            record_types: vec![ RecordType::SOA ],\n            .. Inputs::fallbacks()\n        });\n    }\n\n    #[test]\n    fn domain_and_type_lowercase() {\n        let options = Options::getopts(&[ \"lookup.dog\", \"soa\" ]).unwrap();\n        assert_eq!(options.requests.inputs, Inputs {\n            domains:      vec![ Labels::encode(\"lookup.dog\").unwrap() ],\n            record_types: vec![ RecordType::SOA ],\n            .. Inputs::fallbacks()\n        });\n    }\n\n    #[test]\n    fn domain_and_other_type() {\n        let options = Options::getopts(&[ \"lookup.dog\", \"any\" ]).unwrap();\n        assert_eq!(options.requests.inputs, Inputs {\n            domains:      vec![ Labels::encode(\"lookup.dog\").unwrap() ],\n            record_types: vec![ RecordType::Other(UnknownQtype::from_type_name(\"ANY\").unwrap()) ],\n            .. Inputs::fallbacks()\n        });\n    }\n\n    #[test]\n    fn domain_and_single_domain() {\n        let options = Options::getopts(&[ \"lookup.dog\", \"mixes\" ]).unwrap();\n        assert_eq!(options.requests.inputs, Inputs {\n            domains:      vec![ Labels::encode(\"lookup.dog\").unwrap(),\n                                Labels::encode(\"mixes\").unwrap() ],\n            .. Inputs::fallbacks()\n        });\n    }\n\n    #[test]\n    fn domain_and_nameserver() {\n        let options = Options::getopts(&[ \"lookup.dog\", \"@1.1.1.1\" ]).unwrap();\n        assert_eq!(options.requests.inputs, Inputs {\n            domains:        vec![ Labels::encode(\"lookup.dog\").unwrap() ],\n            resolver_types: vec![ ResolverType::Specific(\"1.1.1.1\".into()) ],\n            .. Inputs::fallbacks()\n        });\n    }\n\n    #[test]\n    fn domain_and_class() {\n        let options = Options::getopts(&[ \"lookup.dog\", \"CH\" ]).unwrap();\n        assert_eq!(options.requests.inputs, Inputs {\n            domains: vec![ Labels::encode(\"lookup.dog\").unwrap() ],\n            classes: vec![ QClass::CH ],\n            .. Inputs::fallbacks()\n        });\n    }\n\n    #[test]\n    fn domain_and_class_lowercase() {\n        let options = Options::getopts(&[ \"lookup.dog\", \"ch\" ]).unwrap();\n        assert_eq!(options.requests.inputs, Inputs {\n            domains: vec![ Labels::encode(\"lookup.dog\").unwrap() ],\n            classes: vec![ QClass::CH ],\n            .. Inputs::fallbacks()\n        });\n    }\n\n    #[test]\n    fn all_free() {\n        let options = Options::getopts(&[ \"lookup.dog\", \"CH\", \"NS\", \"@1.1.1.1\" ]).unwrap();\n        assert_eq!(options.requests.inputs, Inputs {\n            domains:        vec![ Labels::encode(\"lookup.dog\").unwrap() ],\n            classes:        vec![ QClass::CH ],\n            record_types:   vec![ RecordType::NS ],\n            resolver_types: vec![ ResolverType::Specific(\"1.1.1.1\".into()) ],\n            .. Inputs::fallbacks()\n        });\n    }\n\n    #[test]\n    fn all_parameters() {\n        let options = Options::getopts(&[ \"-q\", \"lookup.dog\", \"--class\", \"CH\", \"--type\", \"SOA\", \"--nameserver\", \"1.1.1.1\" ]).unwrap();\n        assert_eq!(options.requests.inputs, Inputs {\n            domains:        vec![ Labels::encode(\"lookup.dog\").unwrap() ],\n            classes:        vec![ QClass::CH ],\n            record_types:   vec![ RecordType::SOA ],\n            resolver_types: vec![ ResolverType::Specific(\"1.1.1.1\".into()) ],\n            .. Inputs::fallbacks()\n        });\n    }\n\n    #[test]\n    fn all_parameters_lowercase() {\n        let options = Options::getopts(&[ \"-q\", \"lookup.dog\", \"--class\", \"ch\", \"--type\", \"soa\", \"--nameserver\", \"1.1.1.1\" ]).unwrap();\n        assert_eq!(options.requests.inputs, Inputs {\n            domains:        vec![ Labels::encode(\"lookup.dog\").unwrap() ],\n            classes:        vec![ QClass::CH ],\n            record_types:   vec![ RecordType::SOA ],\n            resolver_types: vec![ ResolverType::Specific(\"1.1.1.1\".into()) ],\n            .. Inputs::fallbacks()\n        });\n    }\n\n    #[test]\n    fn two_types() {\n        let options = Options::getopts(&[ \"-q\", \"lookup.dog\", \"--type\", \"SRV\", \"--type\", \"AAAA\" ]).unwrap();\n        assert_eq!(options.requests.inputs, Inputs {\n            domains:      vec![ Labels::encode(\"lookup.dog\").unwrap() ],\n            record_types: vec![ RecordType::SRV, RecordType::AAAA ],\n            .. Inputs::fallbacks()\n        });\n    }\n\n    #[test]\n    fn two_classes() {\n        let options = Options::getopts(&[ \"-q\", \"lookup.dog\", \"--class\", \"IN\", \"--class\", \"CH\" ]).unwrap();\n        assert_eq!(options.requests.inputs, Inputs {\n            domains: vec![ Labels::encode(\"lookup.dog\").unwrap() ],\n            classes: vec![ QClass::IN, QClass::CH ],\n            .. Inputs::fallbacks()\n        });\n    }\n\n    #[test]\n    fn all_mixed_1() {\n        let options = Options::getopts(&[ \"lookup.dog\", \"--class\", \"CH\", \"SOA\", \"--nameserver\", \"1.1.1.1\" ]).unwrap();\n        assert_eq!(options.requests.inputs, Inputs {\n            domains:        vec![ Labels::encode(\"lookup.dog\").unwrap() ],\n            classes:        vec![ QClass::CH ],\n            record_types:   vec![ RecordType::SOA ],\n            resolver_types: vec![ ResolverType::Specific(\"1.1.1.1\".into()) ],\n            .. Inputs::fallbacks()\n        });\n    }\n\n    #[test]\n    fn all_mixed_2() {\n        let options = Options::getopts(&[ \"CH\", \"SOA\", \"MX\", \"IN\", \"-q\", \"lookup.dog\", \"--class\", \"HS\" ]).unwrap();\n        assert_eq!(options.requests.inputs, Inputs {\n            domains:      vec![ Labels::encode(\"lookup.dog\").unwrap() ],\n            classes:      vec![ QClass::HS, QClass::CH, QClass::IN ],\n            record_types: vec![ RecordType::SOA, RecordType::MX ],\n            .. Inputs::fallbacks()\n        });\n    }\n\n    #[test]\n    fn all_mixed_3() {\n        let options = Options::getopts(&[ \"lookup.dog\", \"--nameserver\", \"1.1.1.1\", \"--nameserver\", \"1.0.0.1\" ]).unwrap();\n        assert_eq!(options.requests.inputs, Inputs {\n            domains:        vec![ Labels::encode(\"lookup.dog\").unwrap() ],\n            resolver_types: vec![ ResolverType::Specific(\"1.1.1.1\".into()),\n                                  ResolverType::Specific(\"1.0.0.1\".into()), ],\n            .. Inputs::fallbacks()\n        });\n    }\n\n    #[test]\n    fn explicit_numerics() {\n        let options = Options::getopts(&[ \"11\", \"--class\", \"22\", \"--type\", \"33\" ]).unwrap();\n        assert_eq!(options.requests.inputs, Inputs {\n            domains:      vec![ Labels::encode(\"11\").unwrap() ],\n            classes:      vec![ QClass::Other(22) ],\n            record_types: vec![ RecordType::from(33) ],\n            .. Inputs::fallbacks()\n        });\n    }\n\n    #[test]\n    fn edns_and_tweaks() {\n        let options = Options::getopts(&[ \"dom.ain\", \"--edns\", \"show\", \"-Z\", \"authentic\" ]).unwrap();\n        assert_eq!(options.requests.edns, UseEDNS::SendAndShow);\n        assert_eq!(options.requests.protocol_tweaks.set_authentic_flag, true);\n    }\n\n    #[test]\n    fn two_more_tweaks() {\n        let options = Options::getopts(&[ \"dom.ain\", \"-Z\", \"aa\", \"-Z\", \"cd\" ]).unwrap();\n        assert_eq!(options.requests.protocol_tweaks.set_authoritative_flag, true);\n        assert_eq!(options.requests.protocol_tweaks.set_checking_disabled_flag, true);\n    }\n\n    #[test]\n    fn udp_size() {\n        let options = Options::getopts(&[ \"dom.ain\", \"-Z\", \"bufsize=4096\" ]).unwrap();\n        assert_eq!(options.requests.protocol_tweaks.udp_payload_size, Some(4096));\n    }\n\n    #[test]\n    fn short_mode() {\n        let tf = TextFormat { format_durations: true };\n        let options = Options::getopts(&[ \"dom.ain\", \"--short\" ]).unwrap();\n        assert_eq!(options.format, OutputFormat::Short(tf));\n    }\n\n    #[test]\n    fn short_mode_seconds() {\n        let tf = TextFormat { format_durations: false };\n        let options = Options::getopts(&[ \"dom.ain\", \"--short\", \"--seconds\" ]).unwrap();\n        assert_eq!(options.format, OutputFormat::Short(tf));\n    }\n\n    #[test]\n    fn json_output() {\n        let options = Options::getopts(&[ \"dom.ain\", \"--json\" ]).unwrap();\n        assert_eq!(options.format, OutputFormat::JSON);\n    }\n\n    #[test]\n    fn specific_txid() {\n        let options = Options::getopts(&[ \"dom.ain\", \"--txid\", \"1234\" ]).unwrap();\n        assert_eq!(options.requests.txid_generator,\n                   TxidGenerator::Sequence(1234));\n    }\n\n    #[test]\n    fn all_transport_types() {\n        use crate::connect::TransportType::*;\n\n        let options = Options::getopts(&[ \"dom.ain\", \"--https\", \"--tls\", \"--tcp\", \"--udp\" ]).unwrap();\n        assert_eq!(options.requests.inputs.transport_types,\n                   vec![ HTTPS, TLS, TCP, UDP ]);\n    }\n\n    // invalid options tests\n\n    #[test]\n    fn invalid_named_class() {\n        assert_eq!(Options::getopts(&[ \"lookup.dog\", \"--class\", \"tubes\" ]),\n                   OptionsResult::InvalidOptions(OptionsError::InvalidQueryClass(\"tubes\".into())));\n    }\n\n    #[test]\n    fn invalid_named_class_too_big() {\n        assert_eq!(Options::getopts(&[ \"lookup.dog\", \"--class\", \"999999\" ]),\n                   OptionsResult::InvalidOptions(OptionsError::InvalidQueryClass(\"999999\".into())));\n    }\n\n    #[test]\n    fn invalid_named_type() {\n        assert_eq!(Options::getopts(&[ \"lookup.dog\", \"--type\", \"tubes\" ]),\n                   OptionsResult::InvalidOptions(OptionsError::InvalidQueryType(\"tubes\".into())));\n    }\n\n    #[test]\n    fn invalid_named_type_too_big() {\n        assert_eq!(Options::getopts(&[ \"lookup.dog\", \"--type\", \"999999\" ]),\n                   OptionsResult::InvalidOptions(OptionsError::InvalidQueryType(\"999999\".into())));\n    }\n\n    #[test]\n    fn invalid_txid() {\n        assert_eq!(Options::getopts(&[ \"lookup.dog\", \"--txid=0x10000\" ]),\n                   OptionsResult::InvalidOptions(OptionsError::InvalidTxid(\"0x10000\".into())));\n    }\n\n    #[test]\n    fn invalid_edns() {\n        assert_eq!(Options::getopts(&[ \"--edns=yep\" ]),\n                   OptionsResult::InvalidOptions(OptionsError::InvalidEDNS(\"yep\".into())));\n    }\n\n    #[test]\n    fn invalid_tweaks() {\n        assert_eq!(Options::getopts(&[ \"-Zsleep\" ]),\n                   OptionsResult::InvalidOptions(OptionsError::InvalidTweak(\"sleep\".into())));\n    }\n\n    #[test]\n    fn invalid_udp_size() {\n        assert_eq!(Options::getopts(&[ \"-Z\", \"bufsize=null\" ]),\n                   OptionsResult::InvalidOptions(OptionsError::InvalidTweak(\"bufsize=null\".into())));\n    }\n\n    #[test]\n    fn invalid_udp_size_size() {\n        assert_eq!(Options::getopts(&[ \"-Z\", \"bufsize=999999999\" ]),\n                   OptionsResult::InvalidOptions(OptionsError::InvalidTweak(\"bufsize=999999999\".into())));\n    }\n\n    #[test]\n    fn invalid_udp_size_missing() {\n        assert_eq!(Options::getopts(&[ \"-Z\", \"bufsize=\" ]),\n                   OptionsResult::InvalidOptions(OptionsError::InvalidTweak(\"bufsize=\".into())));\n    }\n\n    #[test]\n    fn missing_https_url() {\n        assert_eq!(Options::getopts(&[ \"--https\", \"lookup.dog\" ]),\n                   OptionsResult::InvalidOptions(OptionsError::MissingHttpsUrl));\n    }\n\n    // opt tests\n\n    #[test]\n    fn opt() {\n        assert_eq!(Options::getopts(&[ \"OPT\", \"lookup.dog\" ]),\n                   OptionsResult::InvalidOptions(OptionsError::QueryTypeOPT));\n    }\n\n    #[test]\n    fn opt_lowercase() {\n        assert_eq!(Options::getopts(&[ \"opt\", \"lookup.dog\" ]),\n                   OptionsResult::InvalidOptions(OptionsError::QueryTypeOPT));\n    }\n\n    #[test]\n    fn opt_arg() {\n        assert_eq!(Options::getopts(&[ \"-t\", \"OPT\", \"lookup.dog\" ]),\n                   OptionsResult::InvalidOptions(OptionsError::QueryTypeOPT));\n    }\n\n    #[test]\n    fn opt_arg_lowercase() {\n        assert_eq!(Options::getopts(&[ \"-t\", \"opt\", \"lookup.dog\" ]),\n                   OptionsResult::InvalidOptions(OptionsError::QueryTypeOPT));\n    }\n\n    // txid tests\n\n    #[test]\n    fn number_parsing() {\n        assert_eq!(parse_dec_or_hex(\"1234\"),    Some(1234));\n        assert_eq!(parse_dec_or_hex(\"0x1234\"),  Some(0x1234));\n        assert_eq!(parse_dec_or_hex(\"0xABcd\"),  Some(0xABcd));\n\n        assert_eq!(parse_dec_or_hex(\"65536\"),   None);\n        assert_eq!(parse_dec_or_hex(\"0x65536\"), None);\n\n        assert_eq!(parse_dec_or_hex(\"\"),        None);\n        assert_eq!(parse_dec_or_hex(\"0x\"),      None);\n    }\n}\n"
  },
  {
    "path": "src/output.rs",
    "content": "//! Text and JSON output.\n\nuse std::fmt;\nuse std::time::Duration;\nuse std::env;\n\nuse dns::{Response, Query, Answer, QClass, ErrorCode, WireError, MandatedLength};\nuse dns::record::{Record, RecordType, UnknownQtype, OPT};\nuse dns_transport::Error as TransportError;\nuse json::{object, JsonValue};\n\nuse crate::colours::Colours;\nuse crate::table::{Table, Section};\n\n\n/// How to format the output data.\n#[derive(PartialEq, Debug, Copy, Clone)]\npub enum OutputFormat {\n\n    /// Format the output as plain text, optionally adding ANSI colours.\n    Text(UseColours, TextFormat),\n\n    /// Format the output as one line of plain text.\n    Short(TextFormat),\n\n    /// Format the entries as JSON.\n    JSON,\n}\n\n\n/// When to use colours in the output.\n#[derive(PartialEq, Debug, Copy, Clone)]\npub enum UseColours {\n\n    /// Always use colours.\n    Always,\n\n    /// Use colours if output is to a terminal; otherwise, do not.\n    Automatic,\n\n    /// Never use colours.\n    Never,\n}\n\n/// Options that govern how text should be rendered in record summaries.\n#[derive(PartialEq, Debug, Copy, Clone)]\npub struct TextFormat {\n\n    /// Whether to format TTLs as hours, minutes, and seconds.\n    pub format_durations: bool,\n}\n\nimpl UseColours {\n\n    /// Whether we should use colours or not. This checks whether the user has\n    /// overridden the colour setting, and if not, whether output is to a\n    /// terminal.\n    pub fn should_use_colours(self) -> bool {\n        self == Self::Always || (atty::is(atty::Stream::Stdout) && env::var(\"NO_COLOR\").is_err() && self != Self::Never)\n    }\n\n    /// Creates a palette of colours depending on the user’s wishes or whether\n    /// output is to a terminal.\n    pub fn palette(self) -> Colours {\n        if self.should_use_colours() {\n            Colours::pretty()\n        }\n        else {\n            Colours::plain()\n        }\n    }\n}\n\n\nimpl OutputFormat {\n\n    /// Prints the entirety of the output, formatted according to the\n    /// settings. If the duration has been measured, it should also be\n    /// printed. Returns `false` if there were no results to print, and `true`\n    /// otherwise.\n    pub fn print(self, responses: Vec<Response>, duration: Option<Duration>) -> bool {\n        match self {\n            Self::Short(tf) => {\n                let all_answers = responses.into_iter().flat_map(|r| r.answers).collect::<Vec<_>>();\n\n                if all_answers.is_empty() {\n                    eprintln!(\"No results\");\n                    return false;\n                }\n\n                for answer in all_answers {\n                    match answer {\n                        Answer::Standard { record, .. } => {\n                            println!(\"{}\", tf.record_payload_summary(record))\n                        }\n                        Answer::Pseudo { opt, .. } => {\n                            println!(\"{}\", tf.pseudo_record_payload_summary(opt))\n                        }\n                    }\n\n                }\n            }\n            Self::JSON => {\n                let mut rs = Vec::new();\n\n                for response in responses {\n                    let json = object! {\n                        \"queries\": json_queries(response.queries),\n                        \"answers\": json_answers(response.answers),\n                        \"authorities\": json_answers(response.authorities),\n                        \"additionals\": json_answers(response.additionals),\n                    };\n\n                    rs.push(json);\n                }\n\n                if let Some(duration) = duration {\n                    let object = object! {\n                        \"responses\": rs,\n                        \"duration\": {\n                            \"secs\": duration.as_secs(),\n                            \"millis\": duration.subsec_millis(),\n                        },\n                    };\n\n                    println!(\"{}\", object);\n                }\n                else {\n                    let object = object! {\n                        \"responses\": rs,\n                    };\n\n                    println!(\"{}\", object);\n                }\n            }\n            Self::Text(uc, tf) => {\n                let mut table = Table::new(uc.palette(), tf);\n\n                for response in responses {\n                    if let Some(rcode) = response.flags.error_code {\n                        print_error_code(rcode);\n                    }\n\n                    for a in response.answers {\n                        table.add_row(a, Section::Answer);\n                    }\n\n                    for a in response.authorities {\n                        table.add_row(a, Section::Authority);\n                    }\n\n                    for a in response.additionals {\n                        table.add_row(a, Section::Additional);\n                    }\n                }\n\n                table.print(duration);\n            }\n        }\n\n        true\n    }\n\n    /// Print an error that’s ocurred while sending or receiving DNS packets\n    /// to standard error.\n    pub fn print_error(self, error: TransportError) {\n        match self {\n            Self::Short(..) | Self::Text(..) => {\n                eprintln!(\"Error [{}]: {}\", erroneous_phase(&error), error_message(error));\n            }\n\n            Self::JSON => {\n                let object = object! {\n                    \"error\": true,\n                    \"error_phase\": erroneous_phase(&error),\n                    \"error_message\": error_message(error),\n                };\n\n                eprintln!(\"{}\", object);\n            }\n        }\n    }\n}\n\nimpl TextFormat {\n\n    /// Formats a summary of a record in a received DNS response. Each record\n    /// type contains wildly different data, so the format of the summary\n    /// depends on what record it’s for.\n    pub fn record_payload_summary(self, record: Record) -> String {\n        match record {\n            Record::A(a) => {\n                format!(\"{}\", a.address)\n            }\n            Record::AAAA(aaaa) => {\n                format!(\"{}\", aaaa.address)\n            }\n            Record::CAA(caa) => {\n                if caa.critical {\n                    format!(\"{} {} (critical)\", Ascii(&caa.tag), Ascii(&caa.value))\n                }\n                else {\n                    format!(\"{} {} (non-critical)\", Ascii(&caa.tag), Ascii(&caa.value))\n                }\n            }\n            Record::CNAME(cname) => {\n                format!(\"{:?}\", cname.domain.to_string())\n            }\n            Record::EUI48(eui48) => {\n                format!(\"{:?}\", eui48.formatted_address())\n            }\n            Record::EUI64(eui64) => {\n                format!(\"{:?}\", eui64.formatted_address())\n            }\n            Record::HINFO(hinfo) => {\n                format!(\"{} {}\", Ascii(&hinfo.cpu), Ascii(&hinfo.os))\n            }\n            Record::LOC(loc) => {\n                format!(\"{} ({}, {}) ({}, {}, {})\",\n                    loc.size,\n                    loc.horizontal_precision,\n                    loc.vertical_precision,\n                    loc.latitude .map_or_else(|| \"Out of range\".into(), |e| e.to_string()),\n                    loc.longitude.map_or_else(|| \"Out of range\".into(), |e| e.to_string()),\n                    loc.altitude,\n                )\n            }\n            Record::MX(mx) => {\n                format!(\"{} {:?}\", mx.preference, mx.exchange.to_string())\n            }\n            Record::NAPTR(naptr) => {\n                format!(\"{} {} {} {} {} {:?}\",\n                    naptr.order,\n                    naptr.preference,\n                    Ascii(&naptr.flags),\n                    Ascii(&naptr.service),\n                    Ascii(&naptr.regex),\n                    naptr.replacement.to_string(),\n                )\n            }\n            Record::NS(ns) => {\n                format!(\"{:?}\", ns.nameserver.to_string())\n            }\n            Record::OPENPGPKEY(opgp) => {\n                format!(\"{:?}\", opgp.base64_key())\n            }\n            Record::PTR(ptr) => {\n                format!(\"{:?}\", ptr.cname.to_string())\n            }\n            Record::SSHFP(sshfp) => {\n                format!(\"{} {} {}\",\n                    sshfp.algorithm,\n                    sshfp.fingerprint_type,\n                    sshfp.hex_fingerprint(),\n                )\n            }\n            Record::SOA(soa) => {\n                format!(\"{:?} {:?} {} {} {} {} {}\",\n                    soa.mname.to_string(),\n                    soa.rname.to_string(),\n                    soa.serial,\n                    self.format_duration(soa.refresh_interval),\n                    self.format_duration(soa.retry_interval),\n                    self.format_duration(soa.expire_limit),\n                    self.format_duration(soa.minimum_ttl),\n                )\n            }\n            Record::SRV(srv) => {\n                format!(\"{} {} {:?}:{}\", srv.priority, srv.weight, srv.target.to_string(), srv.port)\n            }\n            Record::TLSA(tlsa) => {\n                format!(\"{} {} {} {:?}\",\n                    tlsa.certificate_usage,\n                    tlsa.selector,\n                    tlsa.matching_type,\n                    tlsa.hex_certificate_data(),\n                )\n            }\n            Record::TXT(txt) => {\n                let messages = txt.messages.iter().map(|t| Ascii(t).to_string()).collect::<Vec<_>>();\n                messages.join(\", \")\n            }\n            Record::URI(uri) => {\n                format!(\"{} {} {}\", uri.priority, uri.weight, Ascii(&uri.target))\n            }\n            Record::Other { bytes, .. } => {\n                format!(\"{:?}\", bytes)\n            }\n        }\n    }\n\n    /// Formats a summary of an OPT pseudo-record. Pseudo-records have a different\n    /// structure than standard ones.\n    pub fn pseudo_record_payload_summary(self, opt: OPT) -> String {\n        format!(\"{} {} {} {} {:?}\",\n            opt.udp_payload_size,\n            opt.higher_bits,\n            opt.edns0_version,\n            opt.flags,\n            opt.data)\n    }\n\n    /// Formats a duration depending on whether it should be displayed as\n    /// seconds, or as computed units.\n    pub fn format_duration(self, seconds: u32) -> String {\n        if self.format_durations {\n            format_duration_hms(seconds)\n        }\n        else {\n            format!(\"{}\", seconds)\n        }\n    }\n}\n\n/// Formats a duration as days, hours, minutes, and seconds, skipping leading\n/// zero units.\nfn format_duration_hms(seconds: u32) -> String {\n    if seconds < 60 {\n        format!(\"{}s\", seconds)\n    }\n    else if seconds < 60 * 60 {\n        format!(\"{}m{:02}s\",\n            seconds / 60,\n            seconds % 60)\n    }\n    else if seconds < 60 * 60 * 24 {\n        format!(\"{}h{:02}m{:02}s\",\n            seconds / 3600,\n            (seconds % 3600) / 60,\n            seconds % 60)\n    }\n    else {\n        format!(\"{}d{}h{:02}m{:02}s\",\n            seconds / 86400,\n            (seconds % 86400) / 3600,\n            (seconds % 3600) / 60,\n            seconds % 60)\n    }\n}\n\n/// Serialises multiple DNS queries as a JSON value.\nfn json_queries(queries: Vec<Query>) -> JsonValue {\n    let queries = queries.iter().map(|q| {\n        object! {\n            \"name\": q.qname.to_string(),\n            \"class\": json_class(q.qclass),\n            \"type\": json_record_type_name(q.qtype),\n        }\n    }).collect::<Vec<_>>();\n\n    queries.into()\n}\n\n/// Serialises multiple received DNS answers as a JSON value.\nfn json_answers(answers: Vec<Answer>) -> JsonValue {\n    let answers = answers.into_iter().map(|a| {\n        match a {\n            Answer::Standard { qname, qclass, ttl, record } => {\n                object! {\n                    \"name\": qname.to_string(),\n                    \"class\": json_class(qclass),\n                    \"ttl\": ttl,\n                    \"type\": json_record_name(&record),\n                    \"data\": json_record_data(record),\n                }\n            }\n            Answer::Pseudo { qname, opt } => {\n                object! {\n                    \"name\": qname.to_string(),\n                    \"type\": \"OPT\",\n                    \"data\": {\n                        \"version\": opt.edns0_version,\n                        \"data\": opt.data,\n                    },\n                }\n            }\n        }\n    }).collect::<Vec<_>>();\n\n    answers.into()\n}\n\n\nfn json_class(class: QClass) -> JsonValue {\n    match class {\n        QClass::IN        => \"IN\".into(),\n        QClass::CH        => \"CH\".into(),\n        QClass::HS        => \"HS\".into(),\n        QClass::Other(n)  => n.into(),\n    }\n}\n\n\n/// Serialises a DNS record type name.\nfn json_record_type_name(record: RecordType) -> JsonValue {\n    match record {\n        RecordType::A           => \"A\".into(),\n        RecordType::AAAA        => \"AAAA\".into(),\n        RecordType::CAA         => \"CAA\".into(),\n        RecordType::CNAME       => \"CNAME\".into(),\n        RecordType::EUI48       => \"EUI48\".into(),\n        RecordType::EUI64       => \"EUI64\".into(),\n        RecordType::HINFO       => \"HINFO\".into(),\n        RecordType::LOC         => \"LOC\".into(),\n        RecordType::MX          => \"MX\".into(),\n        RecordType::NAPTR       => \"NAPTR\".into(),\n        RecordType::NS          => \"NS\".into(),\n        RecordType::OPENPGPKEY  => \"OPENPGPKEY\".into(),\n        RecordType::PTR         => \"PTR\".into(),\n        RecordType::SOA         => \"SOA\".into(),\n        RecordType::SRV         => \"SRV\".into(),\n        RecordType::SSHFP       => \"SSHFP\".into(),\n        RecordType::TLSA        => \"TLSA\".into(),\n        RecordType::TXT         => \"TXT\".into(),\n        RecordType::URI         => \"URI\".into(),\n        RecordType::Other(unknown) => {\n            match unknown {\n                UnknownQtype::HeardOf(name, _)  => (*name).into(),\n                UnknownQtype::UnheardOf(num)    => (num).into(),\n            }\n        }\n    }\n}\n\n/// Serialises a DNS record type name.\nfn json_record_name(record: &Record) -> JsonValue {\n    match record {\n        Record::A(_)           => \"A\".into(),\n        Record::AAAA(_)        => \"AAAA\".into(),\n        Record::CAA(_)         => \"CAA\".into(),\n        Record::CNAME(_)       => \"CNAME\".into(),\n        Record::EUI48(_)       => \"EUI48\".into(),\n        Record::EUI64(_)       => \"EUI64\".into(),\n        Record::HINFO(_)       => \"HINFO\".into(),\n        Record::LOC(_)         => \"LOC\".into(),\n        Record::MX(_)          => \"MX\".into(),\n        Record::NAPTR(_)       => \"NAPTR\".into(),\n        Record::NS(_)          => \"NS\".into(),\n        Record::OPENPGPKEY(_)  => \"OPENPGPKEY\".into(),\n        Record::PTR(_)         => \"PTR\".into(),\n        Record::SOA(_)         => \"SOA\".into(),\n        Record::SRV(_)         => \"SRV\".into(),\n        Record::SSHFP(_)       => \"SSHFP\".into(),\n        Record::TLSA(_)        => \"TLSA\".into(),\n        Record::TXT(_)         => \"TXT\".into(),\n        Record::URI(_)         => \"URI\".into(),\n        Record::Other { type_number, .. } => {\n            match type_number {\n                UnknownQtype::HeardOf(name, _)  => (*name).into(),\n                UnknownQtype::UnheardOf(num)    => (*num).into(),\n            }\n        }\n    }\n}\n\n\n/// Serialises a received DNS record as a JSON value.\n\n/// Even though DNS doesn’t specify a character encoding, strings are still\n/// converted from UTF-8, because JSON specifies UTF-8.\nfn json_record_data(record: Record) -> JsonValue {\n    match record {\n        Record::A(a) => {\n            object! {\n                \"address\": a.address.to_string(),\n            }\n        }\n        Record::AAAA(aaaa) => {\n            object! {\n                \"address\": aaaa.address.to_string(),\n            }\n        }\n        Record::CAA(caa) => {\n            object! {\n                \"critical\": caa.critical,\n                \"tag\": String::from_utf8_lossy(&caa.tag).to_string(),\n                \"value\": String::from_utf8_lossy(&caa.value).to_string(),\n            }\n        }\n        Record::CNAME(cname) => {\n            object! {\n                \"domain\": cname.domain.to_string(),\n            }\n        }\n        Record::EUI48(eui48) => {\n            object! {\n                \"identifier\": eui48.formatted_address(),\n            }\n        }\n        Record::EUI64(eui64) => {\n            object! {\n                \"identifier\": eui64.formatted_address(),\n            }\n        }\n        Record::HINFO(hinfo) => {\n            object! {\n                \"cpu\": String::from_utf8_lossy(&hinfo.cpu).to_string(),\n                \"os\": String::from_utf8_lossy(&hinfo.os).to_string(),\n            }\n        }\n        Record::LOC(loc) => {\n            object! {\n                \"size\": loc.size.to_string(),\n                \"precision\": {\n                    \"horizontal\": loc.horizontal_precision,\n                    \"vertical\": loc.vertical_precision,\n                },\n                \"point\": {\n                    \"latitude\": loc.latitude.map(|e| e.to_string()),\n                    \"longitude\": loc.longitude.map(|e| e.to_string()),\n                    \"altitude\": loc.altitude.to_string(),\n                },\n            }\n        }\n        Record::MX(mx) => {\n            object! {\n                \"preference\": mx.preference,\n                \"exchange\": mx.exchange.to_string(),\n            }\n        }\n        Record::NAPTR(naptr) => {\n            object! {\n                \"order\": naptr.order,\n                \"flags\": String::from_utf8_lossy(&naptr.flags).to_string(),\n                \"service\": String::from_utf8_lossy(&naptr.service).to_string(),\n                \"regex\": String::from_utf8_lossy(&naptr.regex).to_string(),\n                \"replacement\": naptr.replacement.to_string(),\n            }\n        }\n        Record::NS(ns) => {\n            object! {\n                \"nameserver\": ns.nameserver.to_string(),\n            }\n        }\n        Record::OPENPGPKEY(opgp) => {\n            object! {\n                \"key\": opgp.base64_key(),\n            }\n        }\n        Record::PTR(ptr) => {\n            object! {\n                \"cname\": ptr.cname.to_string(),\n            }\n        }\n        Record::SSHFP(sshfp) => {\n            object! {\n                \"algorithm\": sshfp.algorithm,\n                \"fingerprint_type\": sshfp.fingerprint_type,\n                \"fingerprint\": sshfp.hex_fingerprint(),\n            }\n        }\n        Record::SOA(soa) => {\n            object! {\n                \"mname\": soa.mname.to_string(),\n            }\n        }\n        Record::SRV(srv) => {\n            object! {\n                \"priority\": srv.priority,\n                \"weight\": srv.weight,\n                \"port\": srv.port,\n                \"target\": srv.target.to_string(),\n            }\n        }\n        Record::TLSA(tlsa) => {\n            object! {\n                \"certificate_usage\": tlsa.certificate_usage,\n                \"selector\": tlsa.selector,\n                \"matching_type\": tlsa.matching_type,\n                \"certificate_data\": tlsa.hex_certificate_data(),\n            }\n        }\n        Record::TXT(txt) => {\n            let ms = txt.messages.into_iter()\n                        .map(|txt| String::from_utf8_lossy(&txt).to_string())\n                        .collect::<Vec<_>>();\n            object! {\n                \"messages\": ms,\n            }\n        }\n        Record::URI(uri) => {\n            object! {\n                \"priority\": uri.priority,\n                \"weight\": uri.weight,\n                \"target\": String::from_utf8_lossy(&uri.target).to_string(),\n            }\n        }\n        Record::Other { bytes, .. } => {\n            object! {\n                \"bytes\": bytes,\n            }\n        }\n    }\n}\n\n\n/// A wrapper around displaying characters that escapes quotes and\n/// backslashes, and writes control and upper-bit bytes as their number rather\n/// than their character. This is needed because even though such characters\n/// are not allowed in domain names, packets can contain anything, and we need\n/// a way to display the response, whatever it is.\nstruct Ascii<'a>(&'a [u8]);\n\nimpl fmt::Display for Ascii<'_> {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"\\\"\")?;\n\n        for byte in self.0.iter().copied() {\n            if byte < 32 || byte >= 128 {\n                write!(f, \"\\\\{}\", byte)?;\n            }\n            else if byte == b'\"' {\n                write!(f, \"\\\\\\\"\")?;\n            }\n            else if byte == b'\\\\' {\n                write!(f, \"\\\\\\\\\")?;\n            }\n            else {\n                write!(f, \"{}\", byte as char)?;\n            }\n        }\n\n        write!(f, \"\\\"\")\n    }\n}\n\n\n/// Prints a message describing the “error code” field of a DNS packet. This\n/// happens when the packet was received correctly, but the server indicated\n/// an error.\npub fn print_error_code(rcode: ErrorCode) {\n    match rcode {\n        ErrorCode::FormatError     => println!(\"Status: Format Error\"),\n        ErrorCode::ServerFailure   => println!(\"Status: Server Failure\"),\n        ErrorCode::NXDomain        => println!(\"Status: NXDomain\"),\n        ErrorCode::NotImplemented  => println!(\"Status: Not Implemented\"),\n        ErrorCode::QueryRefused    => println!(\"Status: Query Refused\"),\n        ErrorCode::BadVersion      => println!(\"Status: Bad Version\"),\n        ErrorCode::Private(num)    => println!(\"Status: Private Reason ({})\", num),\n        ErrorCode::Other(num)      => println!(\"Status: Other Failure ({})\", num),\n    }\n}\n\n/// Returns the “phase” of operation where an error occurred. This gets shown\n/// to the user so they can debug what went wrong.\nfn erroneous_phase(error: &TransportError) -> &'static str {\n    match error {\n        TransportError::WireError(_)          => \"protocol\",\n        TransportError::TruncatedResponse     |\n        TransportError::NetworkError(_)       => \"network\",\n        #[cfg(feature = \"with_nativetls\")]\n        TransportError::TlsError(_)           |\n        TransportError::TlsHandshakeError(_)  => \"tls\",\n        #[cfg(feature = \"with_rustls\")]\n        TransportError::RustlsInvalidDnsNameError(_) => \"tls\", // TODO: Actually wrong, could be https\n        #[cfg(feature = \"with_https\")]\n        TransportError::HttpError(_)          |\n        TransportError::WrongHttpStatus(_,_)  => \"http\",\n    }\n}\n\n/// Formats an error into its human-readable message.\nfn error_message(error: TransportError) -> String {\n    match error {\n        TransportError::WireError(e)          => wire_error_message(e),\n        TransportError::TruncatedResponse     => \"Truncated response\".into(),\n        TransportError::NetworkError(e)       => e.to_string(),\n        #[cfg(feature = \"with_nativetls\")]\n        TransportError::TlsError(e)           => e.to_string(),\n        #[cfg(feature = \"with_nativetls\")]\n        TransportError::TlsHandshakeError(e)  => e.to_string(),\n        #[cfg(any(feature = \"with_rustls\"))]\n        TransportError::RustlsInvalidDnsNameError(e) => e.to_string(),\n        #[cfg(feature = \"with_https\")]\n        TransportError::HttpError(e)          => e.to_string(),\n        #[cfg(feature = \"with_https\")]\n        TransportError::WrongHttpStatus(t,r)  => format!(\"Nameserver returned HTTP {} ({})\", t, r.unwrap_or_else(|| \"No reason\".into()))\n    }\n}\n\n/// Formats a wire error into its human-readable message, describing what was\n/// wrong with the packet we received.\nfn wire_error_message(error: WireError) -> String {\n    match error {\n        WireError::IO => {\n            \"Malformed packet: insufficient data\".into()\n        }\n        WireError::WrongRecordLength { stated_length, mandated_length: MandatedLength::Exactly(len) } => {\n            format!(\"Malformed packet: record length should be {}, got {}\", len, stated_length )\n        }\n        WireError::WrongRecordLength { stated_length, mandated_length: MandatedLength::AtLeast(len) } => {\n            format!(\"Malformed packet: record length should be at least {}, got {}\", len, stated_length )\n        }\n        WireError::WrongLabelLength { stated_length, length_after_labels } => {\n            format!(\"Malformed packet: length {} was specified, but read {} bytes\", stated_length, length_after_labels)\n        }\n        WireError::TooMuchRecursion(indices) => {\n            format!(\"Malformed packet: too much recursion: {:?}\", indices)\n        }\n        WireError::OutOfBounds(index) => {\n            format!(\"Malformed packet: out of bounds ({})\", index)\n        }\n        WireError::WrongVersion { stated_version, maximum_supported_version } => {\n            format!(\"Malformed packet: record specifies version {}, expected up to {}\", stated_version, maximum_supported_version)\n        }\n    }\n}\n\n\n#[cfg(test)]\nmod test {\n    use super::*;\n\n    #[test]\n    fn escape_quotes() {\n        assert_eq!(Ascii(b\"Mallard \\\"The Duck\\\" Fillmore\").to_string(),\n                   \"\\\"Mallard \\\\\\\"The Duck\\\\\\\" Fillmore\\\"\");\n    }\n\n    #[test]\n    fn escape_backslashes() {\n        assert_eq!(Ascii(b\"\\\\\").to_string(),\n                   \"\\\"\\\\\\\\\\\"\");\n    }\n\n    #[test]\n    fn escape_lows() {\n        assert_eq!(Ascii(b\"\\n\\r\\t\").to_string(),\n                   \"\\\"\\\\10\\\\13\\\\9\\\"\");\n    }\n\n    #[test]\n    fn escape_highs() {\n        assert_eq!(Ascii(\"pâté\".as_bytes()).to_string(),\n                   \"\\\"p\\\\195\\\\162t\\\\195\\\\169\\\"\");\n    }\n}\n"
  },
  {
    "path": "src/requests.rs",
    "content": "//! Request generation based on the user’s input arguments.\n\nuse crate::connect::TransportType;\nuse crate::resolve::{ResolverType, ResolverLookupError};\nuse crate::txid::TxidGenerator;\n\n\n/// All the information necessary to generate requests for one or more\n/// queries, nameservers, or transport types.\n#[derive(PartialEq, Debug)]\npub struct RequestGenerator {\n\n    /// The input parameter matrix.\n    pub inputs: Inputs,\n\n    /// How to generate transaction IDs.\n    pub txid_generator: TxidGenerator,\n\n    /// Whether to OPT in to DNS extensions.\n    pub edns: UseEDNS,\n\n    /// Other weird protocol options.\n    pub protocol_tweaks: ProtocolTweaks,\n}\n\n/// Which things the user has specified they want queried.\n#[derive(PartialEq, Debug, Default)]\npub struct Inputs {\n\n    /// The list of domain names to query.\n    pub domains: Vec<dns::Labels>,\n\n    /// The list of DNS record types to query for.\n    pub record_types: Vec<dns::record::RecordType>,\n\n    /// The list of DNS classes to query for.\n    pub classes: Vec<dns::QClass>,\n\n    /// The list of resolvers to send queries to.\n    pub resolver_types: Vec<ResolverType>,\n\n    /// The list of transport types to send queries over.\n    pub transport_types: Vec<TransportType>,\n}\n\n/// Weird protocol options that are allowed by the spec but are not common.\n#[derive(PartialEq, Debug, Default, Copy, Clone)]\npub struct ProtocolTweaks {\n\n    /// Set the `AA` (Authoritative Answer) flag in the header of each request.\n    pub set_authoritative_flag: bool,\n\n    /// Set the `AD` (Authentic Data) flag in the header of each request.\n    pub set_authentic_flag: bool,\n\n    /// Set the `CD` (Checking Disabled) flag in the header of each request.\n    pub set_checking_disabled_flag: bool,\n\n    /// Set the buffer size field in the OPT record of each request.\n    pub udp_payload_size: Option<u16>,\n}\n\n/// Whether to send or display OPT packets.\n#[derive(PartialEq, Debug, Copy, Clone)]\npub enum UseEDNS {\n\n    /// Do not send an OPT query in requests, and do not display them.\n    Disable,\n\n    /// Send an OPT query in requests, but hide the result. This is the\n    /// default, because the information is usually not useful to the user.\n    SendAndHide,\n\n    /// Send an OPT query in requests, _and_ display any OPT records in the\n    /// response we receive.\n    SendAndShow,\n}\n\n\n/// The entry type for `RequestGenerator`: a transport to send a request, and\n/// a list of one or more DNS queries to send over it, as determined by the\n/// search path in the resolver.\npub type RequestSet = (Box<dyn dns_transport::Transport>, Vec<dns::Request>);\n\nimpl RequestGenerator {\n\n    /// Iterate through the inputs matrix, returning pairs of DNS request list\n    /// and the details of the transport to send them down.\n    pub fn generate(self) -> Result<Vec<RequestSet>, ResolverLookupError> {\n        let mut requests = Vec::new();\n\n        let resolvers = self.inputs.resolver_types.into_iter()\n            .map(ResolverType::obtain)\n            .collect::<Result<Vec<_>, _>>()?;\n\n        for domain in &self.inputs.domains {\n            for qtype in self.inputs.record_types.iter().copied() {\n                for qclass in self.inputs.classes.iter().copied() {\n                    for resolver in &resolvers {\n                        for transport_type in &self.inputs.transport_types {\n\n                            let mut flags = dns::Flags::query();\n                            self.protocol_tweaks.set_request_flags(&mut flags);\n\n                            let mut additional = None;\n                            if self.edns.should_send() {\n                                let mut opt = dns::Request::additional_record();\n                                self.protocol_tweaks.set_request_opt_fields(&mut opt);\n                                additional = Some(opt);\n                            }\n\n                            let nameserver = resolver.nameserver();\n                            let transport = transport_type.make_transport(nameserver);\n\n                            let mut request_list = Vec::new();\n                            for qname in resolver.name_list(domain) {\n                                let transaction_id = self.txid_generator.generate();\n                                let query = dns::Query { qname, qtype, qclass };\n                                let request = dns::Request { transaction_id, flags, query, additional: additional.clone() };\n                                request_list.push(request);\n                            }\n                            requests.push((transport, request_list));\n                        }\n                    }\n                }\n            }\n        }\n\n        Ok(requests)\n    }\n}\n\nimpl UseEDNS {\n\n    /// Whether the user wants to send OPT records.\n    pub fn should_send(self) -> bool {\n        self != Self::Disable\n    }\n\n    /// Whether the user wants to display sent OPT records.\n    pub fn should_show(self) -> bool {\n        self == Self::SendAndShow\n    }\n}\n\nimpl ProtocolTweaks {\n\n    /// Sets fields in the DNS flags based on the user’s requested tweaks.\n    pub fn set_request_flags(self, flags: &mut dns::Flags) {\n        if self.set_authoritative_flag {\n            flags.authoritative = true;\n        }\n\n        if self.set_authentic_flag {\n            flags.authentic_data = true;\n        }\n\n        if self.set_checking_disabled_flag {\n            flags.checking_disabled = true;\n        }\n    }\n\n    /// Set the payload size field in the outgoing OPT record, if the user has\n    /// requested to do so.\n    pub fn set_request_opt_fields(self, opt: &mut dns::record::OPT) {\n        if let Some(bufsize) = self.udp_payload_size {\n            opt.udp_payload_size = bufsize;\n        }\n    }\n}\n"
  },
  {
    "path": "src/resolve.rs",
    "content": "//! Specifying the address of the DNS server to send requests to.\n\nuse std::fmt;\nuse std::io;\n\nuse log::*;\n\nuse dns::Labels;\n\n\n/// A **resolver type** is the source of a `Resolver`.\n#[derive(PartialEq, Debug)]\npub enum ResolverType {\n\n    /// Obtain a resolver by consulting the system in order to find a\n    /// nameserver and a search list.\n    SystemDefault,\n\n    /// Obtain a resolver by using the given user-submitted string.\n    Specific(String),\n}\n\nimpl ResolverType {\n\n    /// Obtains a resolver by the means specified in this type. Returns an\n    /// error if there was a problem looking up system information, or if\n    /// there is no suitable nameserver available.\n    pub fn obtain(self) -> Result<Resolver, ResolverLookupError> {\n        match self {\n            Self::SystemDefault => {\n                system_nameservers()\n            }\n            Self::Specific(nameserver) => {\n                let search_list = Vec::new();\n                Ok(Resolver { nameserver, search_list })\n            }\n        }\n    }\n}\n\n\n/// A **resolver** knows the address of the server we should\n/// send DNS requests to, and the search list for name lookup.\n#[derive(Debug)]\npub struct Resolver {\n\n    /// The address of the nameserver.\n    pub nameserver: String,\n\n    /// The search list for name lookup.\n    pub search_list: Vec<String>,\n}\n\nimpl Resolver {\n\n    /// Returns a nameserver that queries should be sent to.\n    pub fn nameserver(&self) -> String {\n        self.nameserver.clone()\n    }\n\n    /// Returns a sequence of names to be queried, taking into account\n    /// the search list.\n    pub fn name_list(&self, name: &Labels) -> Vec<Labels> {\n        let mut list = Vec::new();\n\n        if name.len() > 1 {\n            list.push(name.clone());\n            return list;\n        }\n\n        for search in &self.search_list {\n            match Labels::encode(search) {\n                Ok(suffix)  => list.push(name.extend(&suffix)),\n                Err(_)      => warn!(\"Invalid search list: {}\", search),\n            }\n        }\n\n        list.push(name.clone());\n        list\n    }\n}\n\n\n/// Looks up the system default nameserver on Unix, by querying\n/// `/etc/resolv.conf` and using the first line that specifies one.\n/// Returns an error if there’s a problem reading the file, or `None` if no\n/// nameserver is specified in the file.\n#[cfg(unix)]\nfn system_nameservers() -> Result<Resolver, ResolverLookupError> {\n    use std::fs::File;\n    use std::io::{BufRead, BufReader};\n\n    if cfg!(test) {\n        panic!(\"system_nameservers() called from test code\");\n    }\n\n    let f = File::open(\"/etc/resolv.conf\")?;\n    let reader = BufReader::new(f);\n\n    let mut nameservers = Vec::new();\n    let mut search_list = Vec::new();\n    for line in reader.lines() {\n        let line = line?;\n\n        if let Some(nameserver_str) = line.strip_prefix(\"nameserver \") {\n            let ip: Result<std::net::Ipv4Addr, _> = nameserver_str.parse();\n            // TODO: This will need to be changed for IPv6 support.\n\n            match ip {\n                Ok(_ip) => nameservers.push(nameserver_str.into()),\n                Err(e)  => warn!(\"Failed to parse nameserver line {:?}: {}\", line, e),\n            }\n        }\n\n        if let Some(search_str) = line.strip_prefix(\"search \") {\n            search_list.clear();\n            search_list.extend(search_str.split_ascii_whitespace().map(|s| s.into()));\n        }\n    }\n\n    if let Some(nameserver) = nameservers.into_iter().next() {\n        Ok(Resolver { nameserver, search_list })\n    }\n    else {\n        Err(ResolverLookupError::NoNameserver)\n    }\n}\n\n\n/// Looks up the system default nameserver on Windows, by iterating through\n/// the list of network adapters and returning the first nameserver it finds.\n#[cfg(windows)]\n#[allow(unused)]  // todo: Remove this when the time is right\nfn system_nameservers() -> Result<Resolver, ResolverLookupError> {\n    use std::net::{IpAddr, UdpSocket};\n\n    if cfg!(test) {\n        panic!(\"system_nameservers() called from test code\");\n    }\n\n    // According to the specification, prefer ipv6 by default.\n    // TODO: add control flag to select an ip family.\n    #[derive(Debug, PartialEq)]\n    enum ForceIPFamily {\n        V4,\n        V6,\n        None,\n    }\n\n    // get the IP of the Network adapter that is used to access the Internet\n    // https://stackoverflow.com/questions/24661022/getting-ip-adress-associated-to-real-hardware-ethernet-controller-in-windows-c\n    fn get_ipv4() -> io::Result<IpAddr> {\n        let s = UdpSocket::bind(\"0.0.0.0:0\")?;\n        s.connect(\"8.8.8.8:53\")?;\n        let addr = s.local_addr()?;\n        Ok(addr.ip())\n    }\n\n    fn get_ipv6() -> io::Result<IpAddr> {\n        let s = UdpSocket::bind(\"[::1]:0\")?;\n        s.connect(\"[2001:4860:4860::8888]:53\")?;\n        let addr = s.local_addr()?;\n        Ok(addr.ip())\n    }\n\n    let force_ip_family: ForceIPFamily = ForceIPFamily::None;\n    let ip = match force_ip_family {\n        ForceIPFamily::V4 => get_ipv4().ok(),\n        ForceIPFamily::V6 => get_ipv6().ok(),\n        ForceIPFamily::None => get_ipv6().or(get_ipv4()).ok(),\n    };\n\n    let search_list = Vec::new();  // todo: implement this\n\n    let adapters = ipconfig::get_adapters()?;\n    let active_adapters = adapters.iter().filter(|a| {\n        a.oper_status() == ipconfig::OperStatus::IfOperStatusUp && !a.gateways().is_empty()\n    });\n\n    if let Some(dns_server) = active_adapters\n        .clone()\n        .find(|a| ip.map(|ip| a.ip_addresses().contains(&ip)).unwrap_or(false))\n        .map(|a| a.dns_servers().first())\n        .flatten()\n    {\n        debug!(\"Found first nameserver {:?}\", dns_server);\n        let nameserver = dns_server.to_string();\n        Ok(Resolver { nameserver, search_list })\n    }\n\n    // Fallback\n    else if let Some(dns_server) = active_adapters\n        .flat_map(|a| a.dns_servers())\n        .find(|d| (d.is_ipv4() && force_ip_family != ForceIPFamily::V6) || d.is_ipv6())\n    {\n        debug!(\"Found first fallback nameserver {:?}\", dns_server);\n        let nameserver = dns_server.to_string();\n        Ok(Resolver { nameserver, search_list })\n    }\n\n    else {\n        Err(ResolverLookupError::NoNameserver)\n    }\n}\n\n\n/// The fall-back system default nameserver determinator that is not very\n/// determined as it returns nothing without actually checking anything.\n#[cfg(all(not(unix), not(windows)))]\nfn system_nameservers() -> Result<Resolver, ResolverLookupError> {\n    warn!(\"Unable to fetch default nameservers on this platform.\");\n    Err(ResolverLookupError::UnsupportedPlatform)\n}\n\n\n/// Something that can go wrong while obtaining a `Resolver`.\npub enum ResolverLookupError {\n\n    /// The system information was successfully read, but there was no adapter\n    /// suitable to use.\n    NoNameserver,\n\n    /// There was an error accessing the network configuration.\n    IO(io::Error),\n\n    /// There was an error accessing the network configuration (extra errors\n    /// that can only happen on Windows).\n    #[cfg(windows)]\n    Windows(ipconfig::error::Error),\n\n    /// dog is running on a platform where it doesn’t know how to get the\n    /// network configuration, so the user must supply one instead.\n    #[cfg(all(not(unix), not(windows)))]\n    UnsupportedPlatform,\n}\n\nimpl From<io::Error> for ResolverLookupError {\n    fn from(error: io::Error) -> ResolverLookupError {\n        Self::IO(error)\n    }\n}\n\n#[cfg(windows)]\nimpl From<ipconfig::error::Error> for ResolverLookupError {\n    fn from(error: ipconfig::error::Error) -> ResolverLookupError {\n        Self::Windows(error)\n    }\n}\n\nimpl fmt::Display for ResolverLookupError {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        match self {\n            Self::NoNameserver => {\n                write!(f, \"No nameserver found\")\n            }\n            Self::IO(ioe) => {\n                write!(f, \"Error reading network configuration: {}\", ioe)\n            }\n            #[cfg(windows)]\n            Self::Windows(ipe) => {\n                write!(f, \"Error reading network configuration: {}\", ipe)\n            }\n            #[cfg(all(not(unix), not(windows)))]\n            Self::UnsupportedPlatform => {\n                write!(f, \"dog cannot automatically detect nameservers on this platform; you will have to provide one explicitly\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/table.rs",
    "content": "//! Rendering tables of DNS response results.\n\nuse std::time::Duration;\n\nuse ansi_term::ANSIString;\n\nuse dns::Answer;\nuse dns::record::Record;\n\nuse crate::colours::Colours;\nuse crate::output::TextFormat;\n\n\n/// A **table** is built up from all the response records present in a DNS\n/// packet. It then gets displayed to the user.\n#[derive(Debug)]\npub struct Table {\n    colours: Colours,\n    text_format: TextFormat,\n    rows: Vec<Row>,\n}\n\n/// A row of the table. This contains all the fields\n#[derive(Debug)]\npub struct Row {\n    qtype: ANSIString<'static>,\n    qname: String,\n    ttl: Option<String>,\n    section: Section,\n    summary: String,\n}\n\n/// The section of the DNS response that a record was read from.\n#[derive(PartialEq, Debug, Copy, Clone)]\npub enum Section {\n\n    /// This record was found in the **Answer** section.\n    Answer,\n\n    /// This record was found in the **Authority** section.\n    Authority,\n\n    /// This record was found in the **Additional** section.\n    Additional,\n}\n\n\nimpl Table {\n\n    /// Create a new table with no rows.\n    pub fn new(colours: Colours, text_format: TextFormat) -> Self {\n        Self { colours, text_format, rows: Vec::new() }\n    }\n\n    /// Adds a row to the table, containing the data in the given answer in\n    /// the right section.\n    pub fn add_row(&mut self, answer: Answer, section: Section) {\n        match answer {\n            Answer::Standard { record, qname, ttl, .. } => {\n                let qtype = self.coloured_record_type(&record);\n                let qname = qname.to_string();\n                let summary = self.text_format.record_payload_summary(record);\n                let ttl = Some(self.text_format.format_duration(ttl));\n                self.rows.push(Row { qtype, qname, ttl, summary, section });\n            }\n            Answer::Pseudo { qname, opt } => {\n                let qtype = self.colours.opt.paint(\"OPT\");\n                let qname = qname.to_string();\n                let summary = self.text_format.pseudo_record_payload_summary(opt);\n                self.rows.push(Row { qtype, qname, ttl: None, summary, section });\n            }\n        }\n    }\n\n    /// Prints the formatted table to stdout.\n    pub fn print(self, duration: Option<Duration>) {\n        if ! self.rows.is_empty() {\n            let qtype_len = self.max_qtype_len();\n            let qname_len = self.max_qname_len();\n            let ttl_len   = self.max_ttl_len();\n\n            for r in &self.rows {\n                for _ in 0 .. qtype_len - r.qtype.len() {\n                    print!(\" \");\n                }\n\n                print!(\"{} {} \", r.qtype, self.colours.qname.paint(&r.qname));\n\n                for _ in 0 .. qname_len - r.qname.len() {\n                    print!(\" \");\n                }\n\n                if let Some(ttl) = &r.ttl {\n                    for _ in 0 .. ttl_len - ttl.len() {\n                        print!(\" \");\n                    }\n\n                    print!(\"{}\", ttl);\n                }\n                else {\n                    for _ in 0 .. ttl_len {\n                        print!(\" \");\n                    }\n                }\n\n                println!(\" {} {}\", self.format_section(r.section), r.summary);\n            }\n        }\n\n        if let Some(dur) = duration {\n            println!(\"Ran in {}ms\", dur.as_millis());\n        }\n    }\n\n    fn coloured_record_type(&self, record: &Record) -> ANSIString<'static> {\n        match *record {\n            Record::A(_)           => self.colours.a.paint(\"A\"),\n            Record::AAAA(_)        => self.colours.aaaa.paint(\"AAAA\"),\n            Record::CAA(_)         => self.colours.caa.paint(\"CAA\"),\n            Record::CNAME(_)       => self.colours.cname.paint(\"CNAME\"),\n            Record::EUI48(_)       => self.colours.eui48.paint(\"EUI48\"),\n            Record::EUI64(_)       => self.colours.eui64.paint(\"EUI64\"),\n            Record::HINFO(_)       => self.colours.hinfo.paint(\"HINFO\"),\n            Record::LOC(_)         => self.colours.loc.paint(\"LOC\"),\n            Record::MX(_)          => self.colours.mx.paint(\"MX\"),\n            Record::NAPTR(_)       => self.colours.ns.paint(\"NAPTR\"),\n            Record::NS(_)          => self.colours.ns.paint(\"NS\"),\n            Record::OPENPGPKEY(_)  => self.colours.openpgpkey.paint(\"OPENPGPKEY\"),\n            Record::PTR(_)         => self.colours.ptr.paint(\"PTR\"),\n            Record::SSHFP(_)       => self.colours.sshfp.paint(\"SSHFP\"),\n            Record::SOA(_)         => self.colours.soa.paint(\"SOA\"),\n            Record::SRV(_)         => self.colours.srv.paint(\"SRV\"),\n            Record::TLSA(_)        => self.colours.tlsa.paint(\"TLSA\"),\n            Record::TXT(_)         => self.colours.txt.paint(\"TXT\"),\n            Record::URI(_)         => self.colours.uri.paint(\"URI\"),\n\n            Record::Other { ref type_number, .. } => self.colours.unknown.paint(type_number.to_string()),\n        }\n    }\n\n    fn max_qtype_len(&self) -> usize {\n        self.rows.iter().map(|r| r.qtype.len()).max().unwrap()\n    }\n\n    fn max_qname_len(&self) -> usize {\n        self.rows.iter().map(|r| r.qname.len()).max().unwrap()\n    }\n\n    fn max_ttl_len(&self) -> usize {\n        self.rows.iter().map(|r| r.ttl.as_ref().map_or(0, String::len)).max().unwrap()\n    }\n\n    fn format_section(&self, section: Section) -> ANSIString<'static> {\n        match section {\n            Section::Answer      => self.colours.answer.paint(\" \"),\n            Section::Authority   => self.colours.authority.paint(\"A\"),\n            Section::Additional  => self.colours.additional.paint(\"+\"),\n        }\n    }\n}\n"
  },
  {
    "path": "src/txid.rs",
    "content": "//! Transaction ID generation.\n\n\n/// A **transaction ID generator** is used to create unique ID numbers to\n/// identify each packet, as part of the DNS protocol.\n#[derive(PartialEq, Debug, Copy, Clone)]\npub enum TxidGenerator {\n\n    /// Generate random transaction IDs each time.\n    Random,\n\n    /// Generate transaction IDs in a sequence, starting from the given value,\n    /// wrapping around.\n    Sequence(u16),\n}\n\nimpl TxidGenerator {\n    pub fn generate(self) -> u16 {\n        match self {\n            Self::Random           => rand::random(),\n            Self::Sequence(start)  => start,   // todo\n        }\n    }\n}\n"
  },
  {
    "path": "src/usage.txt",
    "content": "\\4mUsage:\\0m\n  \\1mdog\\0m \\1;33m[OPTIONS]\\0m [--] \\32m<arguments>\\0m\n\n\\4mExamples:\\0m\n  \\1mdog\\0m \\32mexample.net\\0m                          Query a domain using default settings\n  \\1mdog\\0m \\32mexample.net MX\\0m                       ...looking up MX records instead\n  \\1mdog\\0m \\32mexample.net MX @1.1.1.1\\0m              ...using a specific nameserver instead\n  \\1mdog\\0m \\32mexample.net MX @1.1.1.1\\0m \\1;33m-T\\0m           ...using TCP rather than UDP\n  \\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\n\n\\4mQuery options:\\0m\n  \\32m<arguments>\\0m              Human-readable host names, nameservers, types, or classes\n  \\1;33m-q\\0m, \\1;33m--query\\0m=\\33mHOST\\0m         Host name or domain name to query\n  \\1;33m-t\\0m, \\1;33m--type\\0m=\\33mTYPE\\0m          Type of the DNS record being queried (A, MX, NS...)\n  \\1;33m-n\\0m, \\1;33m--nameserver\\0m=\\33mADDR\\0m    Address of the nameserver to send packets to\n  \\1;33m--class\\0m=\\33mCLASS\\0m            Network class of the DNS record being queried (IN, CH, HS)\n\n\\4mSending options:\\0m\n  \\1;33m--edns\\0m=\\33mSETTING\\0m           Whether to OPT in to EDNS (disable, hide, show)\n  \\1;33m--txid\\0m=\\33mNUMBER\\0m            Set the transaction ID to a specific value\n  \\1;33m-Z\\0m=\\33mTWEAKS\\0m                Set uncommon protocol-level tweaks\n\n\\4mProtocol options:\\0m\n  \\1;33m-U\\0m, \\1;33m--udp\\0m                Use the DNS protocol over UDP\n  \\1;33m-T\\0m, \\1;33m--tcp\\0m                Use the DNS protocol over TCP\n  \\1;33m-S\\0m, \\1;33m--tls\\0m                Use the DNS-over-TLS protocol\n  \\1;33m-H\\0m, \\1;33m--https\\0m              Use the DNS-over-HTTPS protocol\n\n\\4mOutput options:\\0m\n  \\1;33m-1\\0m, \\1;33m--short\\0m              Short mode: display nothing but the first result\n  \\1;33m-J\\0m, \\1;33m--json\\0m               Display the output as JSON\n  \\1;33m--color\\0m, \\1;33m--colour\\0m=\\33mWHEN\\0m   When to colourise the output (always, automatic, never)\n  \\1;33m--seconds\\0m                Do not format durations, display them as seconds\n  \\1;33m--time\\0m                   Print how long the response took to arrive\n\n\\4mMeta options:\\0m\n  \\1;33m-?\\0m, \\1;33m--help\\0m               Print list of command-line options\n  \\1;33m-v\\0m, \\1;33m--version\\0m            Print version information\n"
  },
  {
    "path": "xtests/README.md",
    "content": "# dog › xtests\n\nThis is dog’s extended test suite. The checks herein form a complete end-to-end set of tests, covering things like network connections, DNS protocol parsing, command-line options, error handling, and edge case behaviour.\n\nThe checks are written as [Specsheet] documents, which you’ll need to have installed. For the JSON tests, you’ll also need [jq].\n\nBecause these tests make connections over the network, the outcome of the test suite will depend on your own machine‘s Internet connection! It also means that your own IP address will be recorded as making the requests.\n\n\n### Test layout\n\nThe tests have been divided into four sections:\n\n1. **live**, which uses both your computer’s default resolver and the [public Cloudflare DNS resolver] to access records that have been created using a public-facing DNS host. This checks that dog works using whatever software is between you and those nameservers on the Internet right now. Because these are _live_ records, the output will vary as things like the TTL vary, so we cannot assert on the _exact_ output; nevertheless, it’s a good check to see if the basic functionality is working.\n\n2. **madns**, which sends requests to the [madns resolver]. This resolver has been pre-programmed with deliberately incorrect responses to see how dog handles edge cases in the DNS specification. These are not live records, so things like the TTLs of the responses are fixed, meaning the output should never change over time; however, it does not mean dog will hold up against the network infrastructure of the real world.\n\n3. **options**, which runs dog using various command-line options and checks that the correct output is returned. These tests should not make network requests when behaving correctly.\n\n4. **features**, which checks dog does the right thing when certain features have been enabled or disabled at compile-time. These tests also should not make network requests when behaving correctly.\n\nAll four categories of check are needed to ensure dog is working correctly.\n\n\n### Tags\n\nTo run a subset of the checks, you can filter with the following tags:\n\n- `cloudflare`: Tests that use the [public Cloudflare DNS resolver].\n- `isp`: Tests that use your computer’s default resolver.\n- `madns`: Tests that use the [madns resolver].\n- `options`: Tests that check the command-line options.\n\nYou can also use a DNS record type as a tag to only run the checks for that particular type.\n\n[Specsheet]: https://specsheet.software/\n[jq]: https://stedolan.github.io/jq/\n[public Cloudflare DNS resolver]: https://developers.cloudflare.com/1.1.1.1/\n[madns resolver]: https://madns.binarystar.systems/\n"
  },
  {
    "path": "xtests/features/none.toml",
    "content": "# These tests are meant to be run against a dog binary compiled with\n# `--no-default-features`. They will fail otherwise.\n\n\n[[cmd]]\nname = \"The missing features are documented in the version\"\nshell = \"dog --version\"\nstdout = { string = \"[-idna, -tls, -https]\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ 'features' ]\n\n[[cmd]]\nname = \"The ‘--tls’ option is not accepted when the feature is disabled\"\nshell = \"dog --tls a.b.c.d\"\nstdout = { empty = true }\nstderr = { file = \"outputs/disabled_tls.txt\" }\nstatus = 3\ntags = [ 'features' ]\n\n[[cmd]]\nname = \"The ‘--https option is not accepted when the feature is disabled\"\nshell = \"dog --https a.b.c.d @name.server\"\nstdout = { empty = true }\nstderr = { file = \"outputs/disabled_https.txt\" }\nstatus = 3\ntags = [ 'features' ]\n"
  },
  {
    "path": "xtests/features/outputs/disabled_https.txt",
    "content": "dog: Cannot use '--https': This version of dog has been compiled without HTTPS support\n"
  },
  {
    "path": "xtests/features/outputs/disabled_tls.txt",
    "content": "dog: Cannot use '--tls': This version of dog has been compiled without TLS support\n"
  },
  {
    "path": "xtests/live/badssl.toml",
    "content": "# Untrusted certificates\n\n[[cmd]]\nname = \"Using a DNS-over-HTTPS server with an expired certificate\"\nshell = \"dog --https @https://expired.badssl.com/ lookup.dog\"\nstdout = { empty = true }\nstderr = { string = \"Error [tls]: The certificate was not trusted.\" }\nstatus = 1\ntags = [ 'live', 'badssl', 'https' ]\n\n[[cmd]]\nname = \"Using a DNS-over-HTTPS server with the wrong host in the certificate\"\nshell = \"dog --https @https://wrong.host.badssl.com/ lookup.dog\"\nstdout = { empty = true }\nstderr = { string = \"Error [tls]: The certificate was not trusted.\" }\nstatus = 1\ntags = [ 'live', 'badssl', 'https' ]\n\n[[cmd]]\nname = \"Using a DNS-over-HTTPS server with a self-signed certificate\"\nshell = \"dog --https @https://self-signed.badssl.com/ lookup.dog\"\nstdout = { empty = true }\nstderr = { string = \"Error [tls]: The certificate was not trusted.\" }\nstatus = 1\ntags = [ 'live', 'badssl', 'https' ]\n\n[[cmd]]\nname = \"Using a DNS-over-HTTPS server with an untrusted root certificate\"\nshell = \"dog --https @https://untrusted-root.badssl.com/ lookup.dog\"\nstdout = { empty = true }\nstderr = { string = \"Error [tls]: The certificate was not trusted.\" }\nstatus = 1\ntags = [ 'live', 'badssl', 'https' ]\n\n[[cmd]]\nname = \"Using a DNS-over-HTTPS server with a revoked certificate\"\nshell = \"dog --https @https://revoked.badssl.com/ lookup.dog\"\nstdout = { empty = true }\nstderr = { string = \"Error [tls]: The certificate was not trusted.\" }\nstatus = 1\ntags = [ 'live', 'badssl', 'https' ]\n\n[[cmd]]\nname = \"Using a DNS-over-HTTPS server with a known bad certificate\"\nshell = \"dog --https @https://superfish.badssl.com/ lookup.dog\"\nstdout = { empty = true }\nstderr = { string = \"Error [tls]: The certificate was not trusted.\" }\nstatus = 1\ntags = [ 'live', 'badssl', 'https' ]\n\n\n# Handshake failures\n\n[[cmd]]\nname = \"Using a DNS-over-HTTPS server that accepts the null cipher\"\nshell = \"dog --https @https://null.badssl.com/ lookup.dog\"\nstdout = { empty = true }\nstderr = { string = \"Error [tls]: handshake failure\" }\nstatus = 1\ntags = [ 'live', 'badssl', 'https' ]\n\n[[cmd]]\nname = \"Using a DNS-over-HTTPS server that accepts the rc4-md5 cipher\"\nshell = \"dog --https @https://rc4-md5.badssl.com/ lookup.dog\"\nstdout = { empty = true }\nstderr = { string = \"Error [tls]: handshake failure\" }\nstatus = 1\ntags = [ 'live', 'badssl', 'https' ]\n"
  },
  {
    "path": "xtests/live/basics.toml",
    "content": "# Colour output\n\n[[cmd]]\nname = \"Running dog with ‘--colour=always’ produces colourful output\"\nshell = \"dog dns.google --colour=always\"\nstdout = { string = \"\\u001B[1;32mA\\u001B[0m \\u001B[1;34mdns.google.\\u001B[0m\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"isp\" ]\n\n[[cmd]]\nname = \"Running dog produces an A record by default\"\nshell = \"dog dns.google\"\nstdout = { string = \"A dns.google.\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"isp\" ]\n\n[[cmd]]\nname = \"Running dog with ‘--colour=never’ produces plain output\"\nshell = \"dog dns.google --colour=never\"\nstdout = { string = \"A dns.google.\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"isp\" ]\n\n\n# Default record type and transport\n\n[[cmd]]\nname = \"Running dog with ‘-U’ produces no errors\"\nshell = \"dog dns.google -U\"\nstdout = { string = \"A dns.google.\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"isp\" ]\n\n[[cmd]]\nname = \"Running dog with ‘A’ produces no errors\"\nshell = \"dog A dns.google\"\nstdout = { string = \"A dns.google.\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"isp\" ]\n\n[[cmd]]\nname = \"Running dog with ‘--time’ outputs a duration\"\nshell = \"dog A dns.google --time\"\nstdout = { string = \"Ran in\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"isp\" ]\n\n\n# Network errors\n\n[[cmd]]\nname = \"Using a DNS server that does not exist on the network\"\nshell = \"dog A dns.google @non.exist --time\"\nstdout = { string = \"Ran in\" }\nstderr = { string = \"Error [network]\" }\nstatus = 1\ntags = [ \"live\", \"isp\" ]\n"
  },
  {
    "path": "xtests/live/bins.toml",
    "content": "# HTTPS\n\n[[cmd]]\nname = \"Using a DNS-over-HTTPS server that returns status 500\"\nshell = \"dog --https @https://eu.httpbin.org/status/500 lookup.dog\"\nstdout = { empty = true }\nstderr = { string = \"Error [http]: Nameserver returned HTTP 500 (INTERNAL SERVER ERROR)\" }\nstatus = 1\ntags = [ 'live', 'httpbin', 'https' ]\n\n[[cmd]]\nname = \"Using a DNS-over-HTTPS server that returns no content\"\nshell = \"dog --https @https://eu.httpbin.org/status/200 lookup.dog\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ 'live', 'httpbin', 'https' ]\n\n\n# TCP\n\n# [[cmd]]\n# name = \"Using a TCP server that returns an empty message\"\n# shell = \"dog --tcp @52.20.16.20:30000 lookup.dog\"\n# stdout = { empty = true }\n# stderr = { string = \"Error [network]: Truncated response\" }\n# status = 1\n# tags = [ 'live', 'tcpbin', 'tcp' ]\n\n# The above test is flaky. It works correctly the first time, but produces a\n# different error message on subsequent runs.\n#\n# The ‘other’ tcpbin can be used to test the truncated response error\n# handling, but it requires waiting 60 seconds for their server to give up and\n# send us a FIN:\n#\n# - dog --tcp bsago.me @tcpbin.com:4242\n# - dog --tls bsago.me @tcpbin.com:4243\n"
  },
  {
    "path": "xtests/live/https.toml",
    "content": "# A records\n\n[[cmd]]\nname = \"Look up an existing A record using HTTPS\"\nshell = \"dog a-example.lookup.dog @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { string = '10.20.30.40' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"https\", \"a\" ]\n\n[[cmd]]\nname = \"Look up a missing A record using HTTPS\"\nshell = \"dog non.existent @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"https\", \"a\" ]\n\n\n# AAAA records\n\n[[cmd]]\nname = \"Look up an existing AAAA record using HTTPS\"\nshell = \"dog AAAA aaaa-example.lookup.dog @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { string = '::1' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"https\", \"aaaa\" ]\n\n[[cmd]]\nname = \"Look up a missing AAAA record using HTTPS\"\nshell = \"dog AAAA non.existent @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"https\", \"aaaa\" ]\n\n\n# CAA records\n\n[[cmd]]\nname = \"Look up an existing CAA record using HTTPS\"\nshell = \"dog CAA caa-example.lookup.dog @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { string = '\"issue\" \"some.certificate.authority\" (non-critical)' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"https\", \"caa\" ]\n\n[[cmd]]\nname = \"Look up a missing CAA record using HTTPS\"\nshell = \"dog CAA non.existent @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"https\", \"caa\" ]\n\n\n# CNAME records\n\n[[cmd]]\nname = \"Look up an existing CNAME record using HTTPS\"\nshell = \"dog CNAME cname-example.lookup.dog @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { string = '\"dns.lookup.dog.\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"https\", \"cname\" ]\n\n[[cmd]]\nname = \"Look up a missing CNAME record using HTTPS\"\nshell = \"dog CNAME non.existent @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"https\", \"cname\" ]\n\n\n# HINFO records\n\n[[cmd]]\nname = \"Look up an existing HINFO record using HTTPS\"\nshell = \"dog HINFO hinfo-example.lookup.dog @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { string = '\"some-kinda-os\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"https\", \"hinfo\" ]\n\n[[cmd]]\nname = \"Look up a missing HINFO record using HTTPS\"\nshell = \"dog HINFO non.existent @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"https\", \"hinfo\" ]\n\n\n# MX records\n\n[[cmd]]\nname = \"Look up an existing MX record using HTTPS\"\nshell = \"dog MX mx-example.lookup.dog @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { string = '10 \"some.mail.server.\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"https\", \"mx\" ]\n\n[[cmd]]\nname = \"Look up a missing MX record using HTTPS\"\nshell = \"dog MX non.existent @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"https\", \"mx\" ]\n\n\n# NS records\n\n[[cmd]]\nname = \"Look up an existing NS record using HTTPS\"\nshell = \"dog NS lookup.dog @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { string = 'ns1' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"https\", \"ns\" ]\n\n[[cmd]]\nname = \"Look up a missing NS record using HTTPS\"\nshell = \"dog NS non.existent @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"https\", \"ns\" ]\n\n\n# SOA records\n\n[[cmd]]\nname = \"Look up an existing SOA record using HTTPS\"\nshell = \"dog SOA lookup.dog @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { string = 'ns1' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"https\", \"soa\" ]\n\n[[cmd]]\nname = \"Look up a missing SOA record using HTTPS\"\nshell = \"dog MX non.existent @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"https\", \"soa\" ]\n\n\n# SRV records\n\n[[cmd]]\nname = \"Look up an existing SRV record using HTTPS\"\nshell = \"dog SRV srv-example.lookup.dog @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { string = '20 \"dns.lookup.dog.\":5000' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"https\", \"srv\" ]\n\n[[cmd]]\nname = \"Look up a missing SRV record using HTTPS\"\nshell = \"dog SRV non.existent @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"https\", \"srv\" ]\n\n\n# TXT records\n\n[[cmd]]\nname = \"Look up an existing TXT record using HTTPS\"\nshell = \"dog TXT txt-example.lookup.dog @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { string = '\"Cache Invalidation and Naming Things\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"https\", \"txt\" ]\n\n[[cmd]]\nname = \"Look up a missing TXT record using HTTPS\"\nshell = \"dog TXT non.existent @https://cloudflare-dns.com/dns-query --short --https\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"https\", \"txt\" ]\n"
  },
  {
    "path": "xtests/live/json.toml",
    "content": "# A records\n\n[[cmd]]\nname = \"Look up an existing A record formatted as JSON\"\nshell = \"dog a-example.lookup.dog @1.1.1.1 --json\"\nstdout = { string = '10.20.30.40' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"a\" ]\n\n[[cmd]]\nname = \"Look up a missing A record formatted as JSON\"\nshell = \"dog non.existent @1.1.1.1 --json\"\nstdout = { string = '[]' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"a\" ]\n\n\n# AAAA records\n\n[[cmd]]\nname = \"Look up an existing AAAA record formatted as JSON\"\nshell = \"dog AAAA aaaa-example.lookup.dog @1.1.1.1 --json\"\nstdout = { string = '::1' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"aaaa\" ]\n\n[[cmd]]\nname = \"Look up a missing AAAA record formatted as JSON\"\nshell = \"dog AAAA non.existent @1.1.1.1 --json\"\nstdout = { string = '[]' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"aaaa\" ]\n\n\n# CAA records\n\n[[cmd]]\nname = \"Look up an existing CAA record formatted as JSON\"\nshell = \"dog CAA caa-example.lookup.dog @1.1.1.1 --json\"\nstdout = { string = '\"some.certificate.authority\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"caa\" ]\n\n[[cmd]]\nname = \"Look up a missing CAA record formatted as JSON\"\nshell = \"dog CAA non.existent @1.1.1.1 --json\"\nstdout = { string = '[]' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"caa\" ]\n\n\n# CNAME records\n\n[[cmd]]\nname = \"Look up an existing CNAME record formatted as JSON\"\nshell = \"dog CNAME cname-example.lookup.dog @1.1.1.1 --json\"\nstdout = { string = '\"dns.lookup.dog.\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"cname\" ]\n\n[[cmd]]\nname = \"Look up a missing CNAME record formatted as JSON\"\nshell = \"dog CNAME non.existent @1.1.1.1 --json\"\nstdout = { string = '[]' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"cname\" ]\n\n\n# HINFO records\n\n[[cmd]]\nname = \"Look up an existing HINFO record formatted as JSON\"\nshell = \"dog HINFO hinfo-example.lookup.dog @1.1.1.1 --json\"\nstdout = { string = '\"some-kinda-os\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"hinfo\" ]\n\n[[cmd]]\nname = \"Look up a missing HINFO record formatted as JSON\"\nshell = \"dog HINFO non.existent @1.1.1.1 --json\"\nstdout = { string = '[]' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"hinfo\" ]\n\n\n# MX records\n\n[[cmd]]\nname = \"Look up an existing MX record formatted as JSON\"\nshell = \"dog MX mx-example.lookup.dog @1.1.1.1 --json\"\nstdout = { string = 'some.mail.server.' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"mx\" ]\n\n[[cmd]]\nname = \"Look up a missing MX record formatted as JSON\"\nshell = \"dog MX non.existent @1.1.1.1 --json\"\nstdout = { string = '[]' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"mx\" ]\n\n\n# NS records\n\n[[cmd]]\nname = \"Look up an existing NS record formatted as JSON\"\nshell = \"dog NS lookup.dog @1.1.1.1 --json\"\nstdout = { string = 'ns1' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"ns\" ]\n\n[[cmd]]\nname = \"Look up a missing NS record formatted as JSON\"\nshell = \"dog NS non.existent @1.1.1.1 --json\"\nstdout = { string = '[]' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"ns\" ]\n\n\n# SOA records\n\n[[cmd]]\nname = \"Look up an existing SOA record formatted as JSON\"\nshell = \"dog SOA lookup.dog @1.1.1.1 --json\"\nstdout = { string = 'ns1' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"soa\" ]\n\n[[cmd]]\nname = \"Look up a missing SOA record formatted as JSON\"\nshell = \"dog MX non.existent @1.1.1.1 --json\"\nstdout = { string = '[]' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"soa\" ]\n\n\n# SRV records\n\n[[cmd]]\nname = \"Look up an existing SRV record formatted as JSON\"\nshell = \"dog SRV srv-example.lookup.dog @1.1.1.1 --json\"\nstdout = { string = 'dns.lookup.dog.' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"srv\" ]\n\n[[cmd]]\nname = \"Look up a missing SRV record formatted as JSON\"\nshell = \"dog SRV non.existent @1.1.1.1 --json\"\nstdout = { string = '[]' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"srv\" ]\n\n\n# TXT records\n\n[[cmd]]\nname = \"Look up an existing TXT record formatted as JSON\"\nshell = \"dog TXT txt-example.lookup.dog @1.1.1.1 --json\"\nstdout = { string = '\"Cache Invalidation and Naming Things\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"txt\" ]\n\n[[cmd]]\nname = \"Look up a missing TXT record formatted as JSON\"\nshell = \"dog TXT non.existent @1.1.1.1 --json\"\nstdout = { string = '[]' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"json\", \"txt\" ]\n"
  },
  {
    "path": "xtests/live/tcp.toml",
    "content": "# A records\n\n[[cmd]]\nname = \"Look up an existing A record using TCP\"\nshell = \"dog a-example.lookup.dog @1.1.1.1 --short --tcp\"\nstdout = { string = '10.20.30.40' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"a\" ]\n\n[[cmd]]\nname = \"Look up a missing A record using TCP\"\nshell = \"dog non.existent @1.1.1.1 --short --tcp\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"a\" ]\n\n\n# AAAA records\n\n[[cmd]]\nname = \"Look up an existing AAAA record using TCP\"\nshell = \"dog AAAA aaaa-example.lookup.dog @1.1.1.1 --short --tcp\"\nstdout = { string = '::1' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"aaaa\" ]\n\n[[cmd]]\nname = \"Look up a missing AAAA record using TCP\"\nshell = \"dog AAAA non.existent @1.1.1.1 --short --tcp\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"aaaa\" ]\n\n\n# CAA records\n\n[[cmd]]\nname = \"Look up an existing CAA record using TCP\"\nshell = \"dog CAA caa-example.lookup.dog @1.1.1.1 --short --tcp\"\nstdout = { string = '\"issue\" \"some.certificate.authority\" (non-critical)' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"caa\" ]\n\n[[cmd]]\nname = \"Look up a missing CAA record using TCP\"\nshell = \"dog CAA non.existent @1.1.1.1 --short --tcp\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"caa\" ]\n\n\n# CNAME records\n\n[[cmd]]\nname = \"Look up an existing CNAME record using TCP\"\nshell = \"dog CNAME cname-example.lookup.dog @1.1.1.1 --short --tcp\"\nstdout = { string = '\"dns.lookup.dog.\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"cname\" ]\n\n[[cmd]]\nname = \"Look up a missing CNAME record using TCP\"\nshell = \"dog CNAME non.existent @1.1.1.1 --short --tcp\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"cname\" ]\n\n\n# HINFO records\n\n[[cmd]]\nname = \"Look up an existing HINFO record using TCP\"\nshell = \"dog HINFO hinfo-example.lookup.dog @1.1.1.1 --short --tcp\"\nstdout = { string = '\"some-kinda-cpu\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"hinfo\" ]\n\n[[cmd]]\nname = \"Look up a missing HINFO record using TCP\"\nshell = \"dog HINFO non.existent @1.1.1.1 --short --tcp\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"hinfo\" ]\n\n\n# MX records\n\n[[cmd]]\nname = \"Look up an existing MX record using TCP\"\nshell = \"dog MX mx-example.lookup.dog @1.1.1.1 --short --tcp\"\nstdout = { string = '10 \"some.mail.server.\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"mx\" ]\n\n[[cmd]]\nname = \"Look up a missing MX record using TCP\"\nshell = \"dog MX non.existent @1.1.1.1 --short --tcp\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"mx\" ]\n\n\n# NS records\n\n[[cmd]]\nname = \"Look up an existing NS record using TCP\"\nshell = \"dog NS lookup.dog @1.1.1.1 --short --tcp\"\nstdout = { string = 'ns1' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"ns\" ]\n\n[[cmd]]\nname = \"Look up a missing NS record using TCP\"\nshell = \"dog NS non.existent @1.1.1.1 --short --tcp\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"ns\" ]\n\n\n# SOA records\n\n[[cmd]]\nname = \"Look up an existing SOA record using TCP\"\nshell = \"dog SOA lookup.dog @1.1.1.1 --short --tcp\"\nstdout = { string = 'ns1' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"soa\" ]\n\n[[cmd]]\nname = \"Look up a missing SOA record using TCP\"\nshell = \"dog MX non.existent @1.1.1.1 --short --tcp\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"soa\" ]\n\n\n# SRV records\n\n[[cmd]]\nname = \"Look up an existing SRV record using TCP\"\nshell = \"dog SRV srv-example.lookup.dog @1.1.1.1 --short --tcp\"\nstdout = { string = '20 \"dns.lookup.dog.\":5000' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"srv\" ]\n\n[[cmd]]\nname = \"Look up a missing SRV record using TCP\"\nshell = \"dog SRV non.existent @1.1.1.1 --short --tcp\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"srv\" ]\n\n\n# TXT records\n\n[[cmd]]\nname = \"Look up an existing TXT record using TCP\"\nshell = \"dog TXT txt-example.lookup.dog @1.1.1.1 --short --tcp\"\nstdout = { string = '\"Cache Invalidation and Naming Things\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"txt\" ]\n\n[[cmd]]\nname = \"Look up a missing TXT record using TCP\"\nshell = \"dog TXT non.existent @1.1.1.1 --short --tcp\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tcp\", \"txt\" ]\n"
  },
  {
    "path": "xtests/live/tls.toml",
    "content": "# A records\n\n[[cmd]]\nname = \"Look up an existing A record using TLS\"\nshell = \"dog a-example.lookup.dog @1.1.1.1 --short --tls\"\nstdout = { string = '10.20.30.40' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tls\", \"a\" ]\n\n[[cmd]]\nname = \"Look up a missing A record using TLS\"\nshell = \"dog non.existent @1.1.1.1 --short --tls\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tls\", \"a\" ]\n\n\n# AAAA records\n\n[[cmd]]\nname = \"Look up an existing AAAA record using TLS\"\nshell = \"dog AAAA aaaa-example.lookup.dog @1.1.1.1 --short --tls\"\nstdout = { string = '::1' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tls\", \"aaaa\" ]\n\n[[cmd]]\nname = \"Look up a missing AAAA record using TLS\"\nshell = \"dog AAAA non.existent @1.1.1.1 --short --tls\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tls\", \"aaaa\" ]\n\n\n# CAA records\n\n[[cmd]]\nname = \"Look up an existing CAA record using TLS\"\nshell = \"dog CAA caa-example.lookup.dog @1.1.1.1 --short --tls\"\nstdout = { string = '\"issue\" \"some.certificate.authority\" (non-critical)' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tls\", \"caa\" ]\n\n[[cmd]]\nname = \"Look up a missing CAA record using TLS\"\nshell = \"dog CAA non.existent @1.1.1.1 --short --tls\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tls\", \"caa\" ]\n\n\n# CNAME records\n\n[[cmd]]\nname = \"Look up an existing CNAME record using TLS\"\nshell = \"dog CNAME cname-example.lookup.dog @1.1.1.1 --short --tls\"\nstdout = { string = '\"dns.lookup.dog.\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tls\", \"cname\" ]\n\n[[cmd]]\nname = \"Look up a missing CNAME record using TLS\"\nshell = \"dog CNAME non.existent @1.1.1.1 --short --tls\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tls\", \"cname\" ]\n\n\n# CNAME records\n\n[[cmd]]\nname = \"Look up an existing HINFO record using TLS\"\nshell = \"dog HINFO hinfo-example.lookup.dog @1.1.1.1 --short --tls\"\nstdout = { string = '\"some-kinda-os\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tls\", \"hinfo\" ]\n\n[[cmd]]\nname = \"Look up a missing HINFO record using TLS\"\nshell = \"dog HINFO non.existent @1.1.1.1 --short --tls\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tls\", \"hinfo\" ]\n\n\n# MX records\n\n[[cmd]]\nname = \"Look up an existing MX record using TLS\"\nshell = \"dog MX mx-example.lookup.dog @1.1.1.1 --short --tls\"\nstdout = { string = '10 \"some.mail.server.\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tls\", \"mx\" ]\n\n[[cmd]]\nname = \"Look up a missing MX record using TLS\"\nshell = \"dog MX non.existent @1.1.1.1 --short --tls\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tls\", \"mx\" ]\n\n\n# NS records\n\n[[cmd]]\nname = \"Look up an existing NS record using TLS\"\nshell = \"dog NS lookup.dog @1.1.1.1 --short --tls\"\nstdout = { string = 'ns1' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tls\", \"ns\" ]\n\n[[cmd]]\nname = \"Look up a missing NS record using TLS\"\nshell = \"dog NS non.existent @1.1.1.1 --short --tls\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tls\", \"ns\" ]\n\n\n# SOA records\n\n[[cmd]]\nname = \"Look up an existing SOA record using TLS\"\nshell = \"dog SOA lookup.dog @1.1.1.1 --short --tls\"\nstdout = { string = 'ns1' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tls\", \"soa\" ]\n\n[[cmd]]\nname = \"Look up a missing SOA record using TLS\"\nshell = \"dog MX non.existent @1.1.1.1 --short --tls\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tls\", \"soa\" ]\n\n\n# SRV records\n\n[[cmd]]\nname = \"Look up an existing SRV record using TLS\"\nshell = \"dog SRV srv-example.lookup.dog @1.1.1.1 --short --tls\"\nstdout = { string = '20 \"dns.lookup.dog.\":5000' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tls\", \"srv\" ]\n\n[[cmd]]\nname = \"Look up a missing SRV record using TLS\"\nshell = \"dog SRV non.existent @1.1.1.1 --short --tls\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tls\", \"srv\" ]\n\n\n# TXT records\n\n[[cmd]]\nname = \"Look up an existing TXT record using TLS\"\nshell = \"dog TXT txt-example.lookup.dog @1.1.1.1 --short --tls\"\nstdout = { string = '\"Cache Invalidation and Naming Things\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"tls\", \"txt\" ]\n\n[[cmd]]\nname = \"Look up a missing TXT record using TLS\"\nshell = \"dog TXT non.existent @1.1.1.1 --short --tls\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"tls\", \"txt\" ]\n"
  },
  {
    "path": "xtests/live/udp.toml",
    "content": "# A records\n\n[[cmd]]\nname = \"Look up an existing A record using UDP\"\nshell = \"dog a-example.lookup.dog @1.1.1.1 --short\"\nstdout = { string = '10.20.30.40' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"udp\", \"a\" ]\n\n[[cmd]]\nname = \"Look up a missing A record using UDP\"\nshell = \"dog non.existent @1.1.1.1 --short\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"udp\", \"a\" ]\n\n\n# AAAA records\n\n[[cmd]]\nname = \"Look up an existing AAAA record using UDP\"\nshell = \"dog AAAA aaaa-example.lookup.dog @1.1.1.1 --short\"\nstdout = { string = '::1' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"udp\", \"aaaa\" ]\n\n[[cmd]]\nname = \"Look up a missing AAAA record using UDP\"\nshell = \"dog AAAA non.existent @1.1.1.1 --short\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"udp\", \"aaaa\" ]\n\n\n# CAA records\n\n[[cmd]]\nname = \"Look up an existing CAA record using UDP\"\nshell = \"dog CAA caa-example.lookup.dog @1.1.1.1 --short\"\nstdout = { string = '\"issue\" \"some.certificate.authority\" (non-critical)' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"udp\", \"caa\" ]\n\n[[cmd]]\nname = \"Look up a missing CAA record using UDP\"\nshell = \"dog CAA non.existent @1.1.1.1 --short\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"udp\", \"caa\" ]\n\n\n# CNAME records\n\n[[cmd]]\nname = \"Look up an existing CNAME record using UDP\"\nshell = \"dog CNAME cname-example.lookup.dog @1.1.1.1 --short\"\nstdout = { string = '\"dns.lookup.dog.\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"udp\", \"cname\" ]\n\n[[cmd]]\nname = \"Look up a missing CNAME record using UDP\"\nshell = \"dog CNAME non.existent @1.1.1.1 --short\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"udp\", \"cname\" ]\n\n\n# CNAME records\n\n[[cmd]]\nname = \"Look up an existing HINFO record using UDP\"\nshell = \"dog HINFO hinfo-example.lookup.dog @1.1.1.1 --short\"\nstdout = { string = '\"some-kinda-cpu\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"udp\", \"hinfo\" ]\n\n[[cmd]]\nname = \"Look up a missing HINFO record using UDP\"\nshell = \"dog HINFO non.existent @1.1.1.1 --short\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"udp\", \"hinfo\" ]\n\n\n# MX records\n\n[[cmd]]\nname = \"Look up an existing MX record using UDP\"\nshell = \"dog MX mx-example.lookup.dog @1.1.1.1 --short\"\nstdout = { string = '10 \"some.mail.server.\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"udp\", \"mx\" ]\n\n[[cmd]]\nname = \"Look up a missing MX record using UDP\"\nshell = \"dog MX non.existent @1.1.1.1 --short\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"udp\", \"mx\" ]\n\n\n# NS records\n\n[[cmd]]\nname = \"Look up an existing NS record using UDP\"\nshell = \"dog NS lookup.dog @1.1.1.1 --short\"\nstdout = { string = 'ns1' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"udp\", \"ns\" ]\n\n[[cmd]]\nname = \"Look up a missing NS record using UDP\"\nshell = \"dog NS non.existent @1.1.1.1 --short\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"udp\", \"ns\" ]\n\n\n# SOA records\n\n[[cmd]]\nname = \"Look up an existing SOA record using UDP\"\nshell = \"dog SOA lookup.dog @1.1.1.1 --short\"\nstdout = { string = 'ns1' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"udp\", \"soa\" ]\n\n[[cmd]]\nname = \"Look up a missing SOA record using UDP\"\nshell = \"dog MX non.existent @1.1.1.1 --short\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"udp\", \"soa\" ]\n\n\n# SRV records\n\n[[cmd]]\nname = \"Look up an existing SRV record using UDP\"\nshell = \"dog SRV srv-example.lookup.dog @1.1.1.1 --short\"\nstdout = { string = '20 \"dns.lookup.dog.\":5000' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"udp\", \"srv\" ]\n\n[[cmd]]\nname = \"Look up a missing SRV record using UDP\"\nshell = \"dog SRV non.existent @1.1.1.1 --short\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"udp\", \"srv\" ]\n\n\n# TXT records\n\n[[cmd]]\nname = \"Look up an existing TXT record using UDP\"\nshell = \"dog TXT txt-example.lookup.dog @1.1.1.1 --short\"\nstdout = { string = '\"Cache Invalidation and Naming Things\"' }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"live\", \"cloudflare\", \"udp\", \"txt\" ]\n\n[[cmd]]\nname = \"Look up a missing TXT record using UDP\"\nshell = \"dog TXT non.existent @1.1.1.1 --short\"\nstdout = { empty = true }\nstderr = { string = \"No results\" }\nstatus = 2\ntags = [ \"live\", \"cloudflare\", \"udp\", \"txt\" ]\n"
  },
  {
    "path": "xtests/madns/a-records.toml",
    "content": "# A record successes\n\n[[cmd]]\nname = \"Running with ‘a.example’ prints the correct A record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A a.example\"\nstdout = { file = \"outputs/a.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"a\", \"madns\" ]\n\n\n# A record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘a.example --json’ prints the correct A record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A a.example --json | jq\"\nstdout = { file = \"outputs/a.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"a\", \"madns\", \"json\" ]\n\n\n# A record invalid packets\n\n[[cmd]]\nname = \"Running with ‘too-long.a.invalid’ displays a record length error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A too-long.a.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: record length should be 4, got 5\" }\nstatus = 1\ntags = [ \"a\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘too-short.a.invalid’ displays a record length error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A too-short.a.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: record length should be 4, got 3\" }\nstatus = 1\ntags = [ \"a\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘empty.a.invalid’ displays a record length error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A empty.a.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: record length should be 4, got 0\" }\nstatus = 1\ntags = [ \"a\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘incomplete.a.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A incomplete.a.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"a\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/aaaa-records.toml",
    "content": "# AAAA record successes\n\n[[cmd]]\nname = \"Running with ‘aaaa.example’ prints the correct AAAA record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} AAAA aaaa.example\"\nstdout = { file = \"outputs/aaaa.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"aaaa\", \"madns\" ]\n\n\n# AAAA record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘aaaa.example --json’ prints the correct AAAA record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} AAAA aaaa.example --json | jq\"\nstdout = { file = \"outputs/aaaa.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"aaaa\", \"madns\", \"json\" ]\n\n\n# AAAA record invalid packets\n\n[[cmd]]\nname = \"Running with ‘too-long.aaaa.invalid’ displays a record length error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} AAAA too-long.aaaa.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: record length should be 16, got 17\" }\nstatus = 1\ntags = [ \"aaaa\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘too-short.aaaa.invalid’ displays a record length error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} AAAA too-short.aaaa.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: record length should be 16, got 8\" }\nstatus = 1\ntags = [ \"aaaa\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘empty.aaaa.invalid’ displays a record length error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} AAAA empty.aaaa.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: record length should be 16, got 0\" }\nstatus = 1\ntags = [ \"aaaa\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘incomplete.aaaa.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} AAAA incomplete.aaaa.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"aaaa\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/caa-records.toml",
    "content": "# CAA record successes\n\n[[cmd]]\nname = \"Running with ‘caa.example’ prints the correct CAA record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA caa.example\"\nstdout = { file = \"outputs/caa.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"caa\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘critical.caa.example’ prints the correct CAA record with the flag\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA critical.caa.example\"\nstdout = { file = \"outputs/critical.caa.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"caa\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘others.caa.example’ prints the correct CAA record and ignores the flags\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA others.caa.example\"\nstdout = { file = \"outputs/others.caa.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"caa\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘utf8.caa.example’ escapes characters in the fields\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA utf8.caa.example\"\nstdout = { file = \"outputs/utf8.caa.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"caa\", \"madns\", \"chars\" ]\n\n[[cmd]]\nname = \"Running with ‘bad-utf8.caa.example’ escapes characters in the fields and does not crash\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA bad-utf8.caa.example\"\nstdout = { file = \"outputs/bad-utf8.caa.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"caa\", \"madns\", \"chars\" ]\n\n\n# CAA record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘caa.example --json’ prints the correct CAA record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA caa.example --json | jq\"\nstdout = { file = \"outputs/caa.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"caa\", \"madns\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘critical.caa.example --json’ prints the correct CAA record structurewith the flag\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA critical.caa.example --json | jq\"\nstdout = { file = \"outputs/critical.caa.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"caa\", \"madns\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘others.caa.example --json’ prints the correct CAA record structure and ignores the flags\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA others.caa.example --json | jq\"\nstdout = { file = \"outputs/others.caa.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"caa\", \"madns\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘utf8.caa.example --json’ interprets the response as UTF-8\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA utf8.caa.example --json | jq\"\nstdout = { file = \"outputs/utf8.caa.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"caa\", \"madns\", \"chars\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘bad-utf8.caa.example --json’ uses UTF-8 replacement characters\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA bad-utf8.caa.example --json | jq\"\nstdout = { file = \"outputs/bad-utf8.caa.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"caa\", \"madns\", \"chars\", \"json\" ]\n\n\n# CAA record invalid packets\n\n[[cmd]]\nname = \"Running with ‘empty.caa.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA empty.caa.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"caa\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘incomplete.caa.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CAA incomplete.caa.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"caa\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/cname-records.toml",
    "content": "# CNAME record successes\n\n[[cmd]]\nname = \"Running with ‘cname.example’ prints the correct CNAME record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME cname.example\"\nstdout = { file = \"outputs/cname.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"cname\", \"madns\" ]\n\n\n# CNAME record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘cname.example --json’ prints the correct CNAME record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME cname.example --json | jq\"\nstdout = { file = \"outputs/cname.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"cname\", \"madns\", \"json\" ]\n\n\n# CNAME record invalid packets\n\n[[cmd]]\nname = \"Running with ‘empty.cname.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME empty.cname.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"cname\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘incomplete.cname.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME incomplete.cname.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"cname\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/eui48-records.toml",
    "content": "# EUI48 record successes\n\n[[cmd]]\nname = \"Running with ‘eui48.example’ prints the correct EUI48 record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} EUI48 eui48.example\"\nstdout = { file = \"outputs/eui48.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"eui48\", \"madns\" ]\n\n\n# EUI48 record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘eui48.example --json’ prints the correct EUI48 record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} EUI48 eui48.example --json | jq\"\nstdout = { file = \"outputs/eui48.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"eui48\", \"madns\", \"json\" ]\n\n\n# EUI48 record invalid packets\n\n[[cmd]]\nname = \"Running with ‘too-long.eui48.invalid’ displays a record length error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} EUI48 too-long.eui48.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: record length should be 6, got 7\" }\nstatus = 1\ntags = [ \"eui48\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘too-short.eui48.invalid’ displays a record length error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} EUI48 too-short.eui48.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: record length should be 6, got 5\" }\nstatus = 1\ntags = [ \"eui48\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘empty.eui48.invalid’ displays a record length error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} EUI48 empty.eui48.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: record length should be 6, got 0\" }\nstatus = 1\ntags = [ \"eui48\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘incomplete.eui48.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} EUI48 incomplete.eui48.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"eui48\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/eui64-records.toml",
    "content": "# EUI64 record successes\n\n[[cmd]]\nname = \"Running with ‘eui64.example’ prints the correct EUI64 record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} EUI64 eui64.example\"\nstdout = { file = \"outputs/eui64.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"eui64\", \"madns\" ]\n\n\n# EUI64 record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘eui64.example --json’ prints the correct EUI64 record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} EUI64 eui64.example --json | jq\"\nstdout = { file = \"outputs/eui64.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"eui64\", \"madns\", \"json\" ]\n\n\n# EUI64 record invalid packets\n\n[[cmd]]\nname = \"Running with ‘too-long.eui64.invalid’ displays a record length error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} EUI64 too-long.eui64.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: record length should be 8, got 9\" }\nstatus = 1\ntags = [ \"eui64\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘too-short.eui64.invalid’ displays a record length error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} EUI64 too-short.eui64.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: record length should be 8, got 7\" }\nstatus = 1\ntags = [ \"eui64\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘empty.eui64.invalid’ displays a record length error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} EUI64 empty.eui64.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: record length should be 8, got 0\" }\nstatus = 1\ntags = [ \"eui64\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘incomplete.eui64.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} EUI64 incomplete.eui64.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"eui64\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/hinfo-records.toml",
    "content": "# HINFO record successes\n\n[[cmd]]\nname = \"Running with ‘hinfo.example’ prints the correct HINFO record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} HINFO hinfo.example\"\nstdout = { file = \"outputs/hinfo.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"hinfo\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘utf8.hinfo.example’ escapes characters in the fields\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} HINFO utf8.hinfo.example\"\nstdout = { file = \"outputs/utf8.hinfo.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"hinfo\", \"madns\", \"chars\" ]\n\n[[cmd]]\nname = \"Running with ‘bad-utf8.hinfo.example’ escapes characters in the fields and does not crash\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} HINFO bad-utf8.hinfo.example\"\nstdout = { file = \"outputs/bad-utf8.hinfo.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"hinfo\", \"madns\", \"chars\" ]\n\n\n# HINFO record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘hinfo.example --json’ prints the correct HINFO record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} HINFO hinfo.example --json | jq\"\nstdout = { file = \"outputs/hinfo.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"hinfo\", \"madns\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘utf8.hinfo.example --json’ interprets the response as UTF-8\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} HINFO utf8.hinfo.example --json | jq\"\nstdout = { file = \"outputs/utf8.hinfo.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"hinfo\", \"madns\", \"chars\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘bad-utf8.hinfo.example --json’ uses UTF-8 replacement characters\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} HINFO bad-utf8.hinfo.example --json | jq\"\nstdout = { file = \"outputs/bad-utf8.hinfo.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"hinfo\", \"madns\", \"chars\", \"json\" ]\n\n# HINFO record invalid packets\n\n[[cmd]]\nname = \"Running with ‘empty.hinfo.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} HINFO empty.hinfo.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"hinfo\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘incomplete.hinfo.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} HINFO incomplete.hinfo.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"hinfo\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/loc-records.toml",
    "content": "# LOC record successes\n\n[[cmd]]\nname = \"Running with ‘loc.example’ prints the correct LOC record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC loc.example\"\nstdout = { file = \"outputs/loc.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"loc\", \"madns\" ]\n\n\n# LOC record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘loc.example --json’ prints the correct LOC record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC loc.example --json | jq\"\nstdout = { file = \"outputs/loc.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"loc\", \"madns\", \"json\" ]\n\n\n# LOC record out-of-range positions\n\n[[cmd]]\nname = \"Running with ‘far-negative-longitude.loc.invalid’ displays a record with an out-of-range field\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC far-negative-longitude.loc.invalid\"\nstdout = { file = \"outputs/far-negative-longitude.loc.invalid.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"loc\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘far-positive-longitude.loc.invalid’ displays a record with an out-of-range field\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC far-positive-longitude.loc.invalid\"\nstdout = { file = \"outputs/far-positive-longitude.loc.invalid.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"loc\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘far-negative-latitude.loc.invalid’ displays a record with an out-of-range field\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC far-negative-latitude.loc.invalid\"\nstdout = { file = \"outputs/far-negative-latitude.loc.invalid.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"loc\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘far-positive-latitude.loc.invalid’ displays a record with an out-of-range field\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC far-positive-latitude.loc.invalid\"\nstdout = { file = \"outputs/far-positive-latitude.loc.invalid.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"loc\", \"madns\" ]\n\n\n# LOC record out-of-range positions (JSON)\n\n[[cmd]]\nname = \"Running with ‘far-negative-longitude.loc.invalid’ displays a record structure with an out-of-range field\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC far-negative-longitude.loc.invalid --json | jq\"\nstdout = { file = \"outputs/far-negative-longitude.loc.invalid.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"loc\", \"madns\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘far-positive-longitude.loc.invalid’ displays a record structure with an out-of-range field\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC far-positive-longitude.loc.invalid --json | jq\"\nstdout = { file = \"outputs/far-positive-longitude.loc.invalid.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"loc\", \"madns\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘far-negative-latitude.loc.invalid’ displays a record structure with an out-of-range field\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC far-negative-latitude.loc.invalid --json | jq\"\nstdout = { file = \"outputs/far-negative-latitude.loc.invalid.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"loc\", \"madns\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘far-positive-latitude.loc.invalid’ displays a record structure with an out-of-range field\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC far-positive-latitude.loc.invalid --json | jq\"\nstdout = { file = \"outputs/far-positive-latitude.loc.invalid.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"loc\", \"madns\", \"json\" ]\n\n\n# LOC record version 1\n\n[[cmd]]\nname = \"Running with ‘v1-conform.loc.invalid’ displays a version error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC v1-conform.loc.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: record specifies version 1, expected up to 0\" }\nstatus = 1\ntags = [ \"loc\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘v1-nonconform.loc.invalid’ displays a version error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC v1-nonconform.loc.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: record specifies version 1, expected up to 0\" }\nstatus = 1\ntags = [ \"loc\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘v1-empty.loc.invalid’ displays a version error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC v1-empty.loc.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: record specifies version 1, expected up to 0\" }\nstatus = 1\ntags = [ \"loc\", \"madns\" ]\n\n\n# LOC record invalid packets\n\n[[cmd]]\nname = \"Running with ‘empty.loc.invalid’ displays a record length error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC empty.loc.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"loc\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘incomplete.loc.invalid’ displays a record length error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} LOC incomplete.loc.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"loc\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/mx-records.toml",
    "content": "# MX record successes\n\n[[cmd]]\nname = \"Running with ‘mx.example’ prints the correct MX record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} MX mx.example\"\nstdout = { file = \"outputs/mx.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"mx\", \"madns\" ]\n\n\n# MX record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘mx.example --json’ prints the correct MX record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} MX mx.example --json | jq\"\nstdout = { file = \"outputs/mx.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"mx\", \"madns\", \"json\" ]\n\n\n# MX record invalid packets\n\n[[cmd]]\nname = \"Running with ‘empty.mx.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} MX empty.mx.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"mx\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘incomplete.mx.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} MX incomplete.mx.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"mx\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/naptr-records.toml",
    "content": "# NAPTR record successes\n\n[[cmd]]\nname = \"Running with ‘naptr.example’ prints the correct NAPTR record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NAPTR naptr.example\"\nstdout = { file = \"outputs/naptr.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"naptr\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘bad-regex.naptr.example’ still prints the correct NAPTR record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NAPTR bad-regex.naptr.example\"\nstdout = { file = \"outputs/bad-regex.naptr.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"naptr\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘utf8.naptr.example’ escapes characters in the NAPTR\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NAPTR utf8.naptr.invalid\"\nstdout = { file = \"outputs/utf8.naptr.invalid.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"naptr\", \"madns\", \"chars\" ]\n\n[[cmd]]\nname = \"Running with ‘bad-utf8.naptr.example’ escapes characters in the NAPTR and does not crash\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NAPTR bad-utf8.naptr.invalid\"\nstdout = { file = \"outputs/bad-utf8.naptr.invalid.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"naptr\", \"madns\", \"chars\" ]\n\n\n# NAPTR record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘naptr.example --json’ prints the correct NAPTR record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NAPTR naptr.example --json | jq\"\nstdout = { file = \"outputs/naptr.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"naptr\", \"madns\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘utf8.naptr.example --json’ interprets the response as UTF-8\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NAPTR utf8.naptr.invalid --json | jq\"\nstdout = { file = \"outputs/utf8.naptr.invalid.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"naptr\", \"madns\", \"chars\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘bad-utf8.naptr.example --json’ uses UTF-8 replacement characters\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NAPTR bad-utf8.naptr.invalid --json | jq\"\nstdout = { file = \"outputs/bad-utf8.naptr.invalid.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"naptr\", \"madns\", \"chars\", \"json\" ]\n\n\n# NAPTR record invalid packets\n\n[[cmd]]\nname = \"Running with ‘empty.naptr.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NAPTR empty.naptr.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"naptr\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘incomplete.naptr.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NAPTR incomplete.naptr.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"naptr\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/ns-records.toml",
    "content": "# NS record successes\n\n[[cmd]]\nname = \"Running with ‘ns.example’ prints the correct NS record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NS ns.example\"\nstdout = { file = \"outputs/ns.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"ns\", \"madns\" ]\n\n\n# NS record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘ns.example --json’ prints the correct NS record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NS ns.example --json | jq\"\nstdout = { file = \"outputs/ns.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"ns\", \"madns\", \"json\" ]\n\n\n# NS record invalid packets\n\n[[cmd]]\nname = \"Running with ‘empty.ns.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NS empty.ns.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"ns\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘incomplete.ns.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} NS incomplete.ns.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"ns\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/openpgpkey-records.toml",
    "content": "# OPENPGPKEY record successes\n\n[[cmd]]\nname = \"Running with ‘openpgpkey.example’ prints the correct OPENPGPKEY record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} OPENPGPKEY openpgpkey.example\"\nstdout = { file = \"outputs/openpgpkey.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"openpgpkey\", \"madns\" ]\n\n\n# OPENPGPKEY record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘openpgpkey.example --json’ prints the correct OPENPGPKEY record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} OPENPGPKEY openpgpkey.example --json | jq\"\nstdout = { file = \"outputs/openpgpkey.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"openpgpkey\", \"madns\", \"json\" ]\n\n\n# OPENPGPKEY record invalid packets\n\n[[cmd]]\nname = \"Running with ‘empty.openpgpkey.invalid’ displays a record length error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} OPENPGPKEY empty.openpgpkey.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: record length should be at least 1, got 0\" }\nstatus = 1\ntags = [ \"openpgpkey\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘incomplete.openpgpkey.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} OPENPGPKEY incomplete.openpgpkey.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"openpgpkey\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/opt-records.toml",
    "content": "# OPT record successes\n\n[[cmd]]\nname = \"Running with ‘opt.example’ prints the correct OPT record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A opt.example --edns=show\"\nstdout = { file = \"outputs/opt.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"opt\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘do-flag.opt.example’ prints the correct OPT record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A do-flag.opt.example --edns=show\"\nstdout = { file = \"outputs/do-flag.opt.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"opt\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘other-flags.opt.example’ prints the correct OPT record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A other-flags.opt.example --edns=show\"\nstdout = { file = \"outputs/other-flags.opt.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"opt\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘named.opt.invalid’ prints the correct OPT record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A named.opt.invalid --edns=show\"\nstdout = { file = \"outputs/named.opt.invalid.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"opt\", \"madns\" ]\n\n\n# OPT record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘opt.example --json’ prints the correct OPT record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A opt.example --edns=show --json | jq\"\nstdout = { file = \"outputs/opt.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"opt\", \"madns\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘do-flag.opt.example --json’ prints the correct OPT record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A do-flag.opt.example --edns=show --json | jq\"\nstdout = { file = \"outputs/do-flag.opt.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"opt\", \"madns\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘other-flags.opt.example --json’ prints the correct OPT record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A other-flags.opt.example --edns=show --json | jq\"\nstdout = { file = \"outputs/other-flags.opt.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"opt\", \"madns\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘named.opt.invalid --json’ prints the correct OPT record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A named.opt.invalid --edns=show --json | jq\"\nstdout = { file = \"outputs/named.opt.invalid.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"opt\", \"madns\", \"json\" ]\n\n\n# OPT record invalid packets\n\n[[cmd]]\nname = \"Running with ‘incomplete.opt.invalid’ displays a record length error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A incomplete.opt.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"opt\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/outputs/a.example.ansitxt",
    "content": "\u001b[1;32mA\u001b[0m \u001b[1;34ma.example.\u001b[0m 10m00s   127.0.0.1\n"
  },
  {
    "path": "xtests/madns/outputs/a.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"a.example.\",\n          \"class\": \"IN\",\n          \"type\": \"A\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"a.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"A\",\n          \"data\": {\n            \"address\": \"127.0.0.1\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/aaaa.example.ansitxt",
    "content": "\u001b[1;32mAAAA\u001b[0m \u001b[1;34maaaa.example.\u001b[0m 10m00s   ::1\n"
  },
  {
    "path": "xtests/madns/outputs/aaaa.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"aaaa.example.\",\n          \"class\": \"IN\",\n          \"type\": \"AAAA\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"aaaa.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"AAAA\",\n          \"data\": {\n            \"address\": \"::1\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/ansi.str.example.ansitxt",
    "content": "\u001b[33mCNAME\u001b[0m \u001b[1;34mansi.str.example.\u001b[0m 10m00s   \"\\u{1b}[32mgreen.\\u{1b}[34mblue.\\u{1b}[31mred.\\u{1b}[0m.\"\n"
  },
  {
    "path": "xtests/madns/outputs/ansi.str.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"ansi.str.example.\",\n          \"class\": \"IN\",\n          \"type\": \"CNAME\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"ansi.str.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"CNAME\",\n          \"data\": {\n            \"domain\": \"\\u001b[32mgreen.\\u001b[34mblue.\\u001b[31mred.\\u001b[0m.\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/bad-regex.naptr.example.ansitxt",
    "content": "\u001b[31mNAPTR\u001b[0m \u001b[1;34mbad-regex.naptr.example.\u001b[0m 10m00s   5 10 \"s\" \"SRV\" \"(((((((((((((((((((((((((\" \"srv.example.\"\n"
  },
  {
    "path": "xtests/madns/outputs/bad-utf8.caa.example.ansitxt",
    "content": "\u001b[31mCAA\u001b[0m \u001b[1;34mbad-utf8.caa.example.\u001b[0m 10m00s   \"issuewild\" \"\\208\\208\\160\\255\" (non-critical)\n"
  },
  {
    "path": "xtests/madns/outputs/bad-utf8.caa.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"bad-utf8.caa.example.\",\n          \"class\": \"IN\",\n          \"type\": \"CAA\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"bad-utf8.caa.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"CAA\",\n          \"data\": {\n            \"critical\": false,\n            \"tag\": \"issuewild\",\n            \"value\": \"�Р�\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/bad-utf8.hinfo.example.ansitxt",
    "content": "\u001b[33mHINFO\u001b[0m \u001b[1;34mbad-utf8.hinfo.example.\u001b[0m 10m00s   \"\\208\\208\\160\\255\" \"\\208\\208\\160\\255\"\n"
  },
  {
    "path": "xtests/madns/outputs/bad-utf8.hinfo.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"bad-utf8.hinfo.example.\",\n          \"class\": \"IN\",\n          \"type\": \"HINFO\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"bad-utf8.hinfo.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"HINFO\",\n          \"data\": {\n            \"cpu\": \"�Р�\",\n            \"os\": \"�Р�\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/bad-utf8.naptr.invalid.ansitxt",
    "content": "\u001b[31mNAPTR\u001b[0m \u001b[1;34mbad-utf8.naptr.invalid.\u001b[0m 10m00s   5 10 \"\\208\\208\\160\\255\" \"\\208\\208\\160\\255\" \"\\208\\208\\160\\255\" \"�Р�.\"\n"
  },
  {
    "path": "xtests/madns/outputs/bad-utf8.naptr.invalid.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"bad-utf8.naptr.invalid.\",\n          \"class\": \"IN\",\n          \"type\": \"NAPTR\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"bad-utf8.naptr.invalid.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"NAPTR\",\n          \"data\": {\n            \"order\": 5,\n            \"flags\": \"�Р�\",\n            \"service\": \"�Р�\",\n            \"regex\": \"�Р�\",\n            \"replacement\": \"�Р�.\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/bad-utf8.txt.example.ansitxt",
    "content": "\u001b[33mTXT\u001b[0m \u001b[1;34mbad-utf8.txt.example.\u001b[0m 10m00s   \"\\208\\208\\160\\255\"\n"
  },
  {
    "path": "xtests/madns/outputs/bad-utf8.txt.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"bad-utf8.txt.example.\",\n          \"class\": \"IN\",\n          \"type\": \"TXT\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"bad-utf8.txt.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"TXT\",\n          \"data\": {\n            \"messages\": [\n              \"�Р�\"\n            ]\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/bad-utf8.uri.example.ansitxt",
    "content": "\u001b[33mURI\u001b[0m \u001b[1;34mbad-utf8.uri.example.\u001b[0m 10m00s   10 16 \"\\208\\208\\160\\255\"\n"
  },
  {
    "path": "xtests/madns/outputs/bad-utf8.uri.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"bad-utf8.uri.example.\",\n          \"class\": \"IN\",\n          \"type\": \"URI\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"bad-utf8.uri.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"URI\",\n          \"data\": {\n            \"priority\": 10,\n            \"weight\": 16,\n            \"target\": \"�Р�\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/caa.example.ansitxt",
    "content": "\u001b[31mCAA\u001b[0m \u001b[1;34mcaa.example.\u001b[0m 10m00s   \"issuewild\" \"trustworthy.example\" (non-critical)\n"
  },
  {
    "path": "xtests/madns/outputs/caa.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"caa.example.\",\n          \"class\": \"IN\",\n          \"type\": \"CAA\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"caa.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"CAA\",\n          \"data\": {\n            \"critical\": false,\n            \"tag\": \"issuewild\",\n            \"value\": \"trustworthy.example\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/cname.example.ansitxt",
    "content": "\u001b[33mCNAME\u001b[0m \u001b[1;34mcname.example.\u001b[0m 10m00s   \"dns.lookup.dog.\"\n"
  },
  {
    "path": "xtests/madns/outputs/cname.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"cname.example.\",\n          \"class\": \"IN\",\n          \"type\": \"CNAME\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"cname.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"CNAME\",\n          \"data\": {\n            \"domain\": \"dns.lookup.dog.\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/critical.caa.example.ansitxt",
    "content": "\u001b[31mCAA\u001b[0m \u001b[1;34mcritical.caa.example.\u001b[0m 10m00s   \"issuewild\" \"trustworthy.example\" (critical)\n"
  },
  {
    "path": "xtests/madns/outputs/critical.caa.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"critical.caa.example.\",\n          \"class\": \"IN\",\n          \"type\": \"CAA\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"critical.caa.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"CAA\",\n          \"data\": {\n            \"critical\": true,\n            \"tag\": \"issuewild\",\n            \"value\": \"trustworthy.example\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/do-flag.opt.example.ansitxt",
    "content": "  \u001b[1;32mA\u001b[0m \u001b[1;34mdo-flag.opt.example.\u001b[0m 10m00s   127.0.0.1\n\u001b[35mOPT\u001b[0m \u001b[1;34m\u001b[0m                            \u001b[32m+\u001b[0m 1452 0 0 32768 []\n"
  },
  {
    "path": "xtests/madns/outputs/do-flag.opt.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"do-flag.opt.example.\",\n          \"class\": \"IN\",\n          \"type\": \"A\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"do-flag.opt.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"A\",\n          \"data\": {\n            \"address\": \"127.0.0.1\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": [\n        {\n          \"name\": \"\",\n          \"type\": \"OPT\",\n          \"data\": {\n            \"version\": 0,\n            \"data\": []\n          }\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/eui48.example.ansitxt",
    "content": "\u001b[33mEUI48\u001b[0m \u001b[1;34meui48.example.\u001b[0m 10m00s   \"12-34-56-78-90-ab\"\n"
  },
  {
    "path": "xtests/madns/outputs/eui48.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"eui48.example.\",\n          \"class\": \"IN\",\n          \"type\": \"EUI48\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"eui48.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"EUI48\",\n          \"data\": {\n            \"identifier\": \"12-34-56-78-90-ab\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/eui64.example.ansitxt",
    "content": "\u001b[1;33mEUI64\u001b[0m \u001b[1;34meui64.example.\u001b[0m 10m00s   \"12-34-56-ff-fe-78-90-ab\"\n"
  },
  {
    "path": "xtests/madns/outputs/eui64.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"eui64.example.\",\n          \"class\": \"IN\",\n          \"type\": \"EUI64\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"eui64.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"EUI64\",\n          \"data\": {\n            \"identifier\": \"12-34-56-ff-fe-78-90-ab\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/far-negative-latitude.loc.invalid.ansitxt",
    "content": "\u001b[33mLOC\u001b[0m \u001b[1;34mfar-negative-latitude.loc.invalid.\u001b[0m 10m00s   3e2 (0, 0) (Out of range, 0°0′0″ E, 0m)\n"
  },
  {
    "path": "xtests/madns/outputs/far-negative-latitude.loc.invalid.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"far-negative-latitude.loc.invalid.\",\n          \"class\": \"IN\",\n          \"type\": \"LOC\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"far-negative-latitude.loc.invalid.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"LOC\",\n          \"data\": {\n            \"size\": \"3e2\",\n            \"precision\": {\n              \"horizontal\": 0,\n              \"vertical\": 0\n            },\n            \"point\": {\n              \"latitude\": null,\n              \"longitude\": \"0°0′0″ E\",\n              \"altitude\": \"0m\"\n            }\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/far-negative-longitude.loc.invalid.ansitxt",
    "content": "\u001b[33mLOC\u001b[0m \u001b[1;34mfar-negative-longitude.loc.invalid.\u001b[0m 10m00s   3e2 (0, 0) (0°0′0″ N, Out of range, 0m)\n"
  },
  {
    "path": "xtests/madns/outputs/far-negative-longitude.loc.invalid.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"far-negative-longitude.loc.invalid.\",\n          \"class\": \"IN\",\n          \"type\": \"LOC\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"far-negative-longitude.loc.invalid.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"LOC\",\n          \"data\": {\n            \"size\": \"3e2\",\n            \"precision\": {\n              \"horizontal\": 0,\n              \"vertical\": 0\n            },\n            \"point\": {\n              \"latitude\": \"0°0′0″ N\",\n              \"longitude\": null,\n              \"altitude\": \"0m\"\n            }\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/far-positive-latitude.loc.invalid.ansitxt",
    "content": "\u001b[33mLOC\u001b[0m \u001b[1;34mfar-positive-latitude.loc.invalid.\u001b[0m 10m00s   3e2 (0, 0) (Out of range, 0°0′0″ E, 0m)\n"
  },
  {
    "path": "xtests/madns/outputs/far-positive-latitude.loc.invalid.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"far-positive-latitude.loc.invalid.\",\n          \"class\": \"IN\",\n          \"type\": \"LOC\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"far-positive-latitude.loc.invalid.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"LOC\",\n          \"data\": {\n            \"size\": \"3e2\",\n            \"precision\": {\n              \"horizontal\": 0,\n              \"vertical\": 0\n            },\n            \"point\": {\n              \"latitude\": null,\n              \"longitude\": \"0°0′0″ E\",\n              \"altitude\": \"0m\"\n            }\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/far-positive-longitude.loc.invalid.ansitxt",
    "content": "\u001b[33mLOC\u001b[0m \u001b[1;34mfar-positive-longitude.loc.invalid.\u001b[0m 10m00s   3e2 (0, 0) (0°0′0″ N, Out of range, 0m)\n"
  },
  {
    "path": "xtests/madns/outputs/far-positive-longitude.loc.invalid.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"far-positive-longitude.loc.invalid.\",\n          \"class\": \"IN\",\n          \"type\": \"LOC\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"far-positive-longitude.loc.invalid.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"LOC\",\n          \"data\": {\n            \"size\": \"3e2\",\n            \"precision\": {\n              \"horizontal\": 0,\n              \"vertical\": 0\n            },\n            \"point\": {\n              \"latitude\": \"0°0′0″ N\",\n              \"longitude\": null,\n              \"altitude\": \"0m\"\n            }\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/hinfo.example.ansitxt",
    "content": "\u001b[33mHINFO\u001b[0m \u001b[1;34mhinfo.example.\u001b[0m 10m00s   \"some-kinda-cpu\" \"some-kinda-os\"\n"
  },
  {
    "path": "xtests/madns/outputs/hinfo.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"hinfo.example.\",\n          \"class\": \"IN\",\n          \"type\": \"HINFO\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"hinfo.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"HINFO\",\n          \"data\": {\n            \"cpu\": \"some-kinda-cpu\",\n            \"os\": \"some-kinda-os\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/loc.example.ansitxt",
    "content": "\u001b[33mLOC\u001b[0m \u001b[1;34mloc.example.\u001b[0m 10m00s   3e2 (0, 0) (51°30′12.748″ N, 0°7′39.611″ W, 0m)\n"
  },
  {
    "path": "xtests/madns/outputs/loc.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"loc.example.\",\n          \"class\": \"IN\",\n          \"type\": \"LOC\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"loc.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"LOC\",\n          \"data\": {\n            \"size\": \"3e2\",\n            \"precision\": {\n              \"horizontal\": 0,\n              \"vertical\": 0\n            },\n            \"point\": {\n              \"latitude\": \"51°30′12.748″ N\",\n              \"longitude\": \"0°7′39.611″ W\",\n              \"altitude\": \"0m\"\n            }\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/mx.example.ansitxt",
    "content": "\u001b[36mMX\u001b[0m \u001b[1;34mmx.example.\u001b[0m 10m00s   10 \"exchange.example.\"\n"
  },
  {
    "path": "xtests/madns/outputs/mx.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"mx.example.\",\n          \"class\": \"IN\",\n          \"type\": \"MX\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"mx.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"MX\",\n          \"data\": {\n            \"preference\": 10,\n            \"exchange\": \"exchange.example.\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/named.opt.invalid.ansitxt",
    "content": "  \u001b[1;32mA\u001b[0m \u001b[1;34mnamed.opt.invalid.\u001b[0m           10m00s   127.0.0.1\n\u001b[35mOPT\u001b[0m \u001b[1;34mbingle.bongle.dingle.dangle.\u001b[0m        \u001b[32m+\u001b[0m 1452 0 0 0 []\n"
  },
  {
    "path": "xtests/madns/outputs/named.opt.invalid.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"named.opt.invalid.\",\n          \"class\": \"IN\",\n          \"type\": \"A\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"named.opt.invalid.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"A\",\n          \"data\": {\n            \"address\": \"127.0.0.1\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": [\n        {\n          \"name\": \"bingle.bongle.dingle.dangle.\",\n          \"type\": \"OPT\",\n          \"data\": {\n            \"version\": 0,\n            \"data\": []\n          }\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/naptr.example.ansitxt",
    "content": "\u001b[31mNAPTR\u001b[0m \u001b[1;34mnaptr.example.\u001b[0m 10m00s   5 10 \"s\" \"SRV\" \"\\\\d\\\\d:\\\\d\\\\d:\\\\d\\\\d\" \"srv.example.\"\n"
  },
  {
    "path": "xtests/madns/outputs/naptr.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"naptr.example.\",\n          \"class\": \"IN\",\n          \"type\": \"NAPTR\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"naptr.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"NAPTR\",\n          \"data\": {\n            \"order\": 5,\n            \"flags\": \"s\",\n            \"service\": \"SRV\",\n            \"regex\": \"\\\\d\\\\d:\\\\d\\\\d:\\\\d\\\\d\",\n            \"replacement\": \"srv.example.\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/newline.str.example.ansitxt",
    "content": "\u001b[33mCNAME\u001b[0m \u001b[1;34mnewline.str.example.\u001b[0m 10m00s   \"some\\nnew\\r\\nlines\\n.example.\"\n"
  },
  {
    "path": "xtests/madns/outputs/newline.str.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"newline.str.example.\",\n          \"class\": \"IN\",\n          \"type\": \"CNAME\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"newline.str.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"CNAME\",\n          \"data\": {\n            \"domain\": \"some\\nnew\\r\\nlines\\n.example.\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/ns.example.ansitxt",
    "content": "\u001b[31mNS\u001b[0m \u001b[1;34mns.example.\u001b[0m 10m00s   \"a.gtld-servers.net.\"\n"
  },
  {
    "path": "xtests/madns/outputs/ns.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"ns.example.\",\n          \"class\": \"IN\",\n          \"type\": \"NS\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"ns.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"NS\",\n          \"data\": {\n            \"nameserver\": \"a.gtld-servers.net.\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/null.str.example.ansitxt",
    "content": "\u001b[33mCNAME\u001b[0m \u001b[1;34mnull.str.example.\u001b[0m 10m00s   \"some\\u{0}null\\u{0}\\u{0}chars\\u{0}.example.\"\n"
  },
  {
    "path": "xtests/madns/outputs/null.str.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"null.str.example.\",\n          \"class\": \"IN\",\n          \"type\": \"CNAME\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"null.str.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"CNAME\",\n          \"data\": {\n            \"domain\": \"some\\u0000null\\u0000\\u0000chars\\u0000.example.\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/openpgpkey.example.ansitxt",
    "content": "\u001b[36mOPENPGPKEY\u001b[0m \u001b[1;34mopenpgpkey.example.\u001b[0m 10m00s   \"EjRWeA==\"\n"
  },
  {
    "path": "xtests/madns/outputs/openpgpkey.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"openpgpkey.example.\",\n          \"class\": \"IN\",\n          \"type\": \"OPENPGPKEY\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"openpgpkey.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"OPENPGPKEY\",\n          \"data\": {\n            \"key\": \"EjRWeA==\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/opt.example.ansitxt",
    "content": "  \u001b[1;32mA\u001b[0m \u001b[1;34mopt.example.\u001b[0m 10m00s   127.0.0.1\n\u001b[35mOPT\u001b[0m \u001b[1;34m\u001b[0m                    \u001b[32m+\u001b[0m 1452 0 0 0 []\n"
  },
  {
    "path": "xtests/madns/outputs/opt.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"opt.example.\",\n          \"class\": \"IN\",\n          \"type\": \"A\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"opt.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"A\",\n          \"data\": {\n            \"address\": \"127.0.0.1\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": [\n        {\n          \"name\": \"\",\n          \"type\": \"OPT\",\n          \"data\": {\n            \"version\": 0,\n            \"data\": []\n          }\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/other-flags.opt.example.ansitxt",
    "content": "  \u001b[1;32mA\u001b[0m \u001b[1;34mother-flags.opt.example.\u001b[0m 10m00s   127.0.0.1\n\u001b[35mOPT\u001b[0m \u001b[1;34m\u001b[0m                                \u001b[32m+\u001b[0m 1452 0 0 32767 []\n"
  },
  {
    "path": "xtests/madns/outputs/other-flags.opt.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"other-flags.opt.example.\",\n          \"class\": \"IN\",\n          \"type\": \"A\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"other-flags.opt.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"A\",\n          \"data\": {\n            \"address\": \"127.0.0.1\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": [\n        {\n          \"name\": \"\",\n          \"type\": \"OPT\",\n          \"data\": {\n            \"version\": 0,\n            \"data\": []\n          }\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/others.caa.example.ansitxt",
    "content": "\u001b[31mCAA\u001b[0m \u001b[1;34mcaa.example.\u001b[0m 10m00s   \"issuewild\" \"trustworthy.example\" (non-critical)\n"
  },
  {
    "path": "xtests/madns/outputs/others.caa.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"caa.example.\",\n          \"class\": \"IN\",\n          \"type\": \"CAA\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"caa.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"CAA\",\n          \"data\": {\n            \"critical\": false,\n            \"tag\": \"issuewild\",\n            \"value\": \"trustworthy.example\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/ptr.example.ansitxt",
    "content": "\u001b[31mPTR\u001b[0m \u001b[1;34mptr.example.\u001b[0m 10m00s   \"dns.example.\"\n"
  },
  {
    "path": "xtests/madns/outputs/ptr.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"ptr.example.\",\n          \"class\": \"IN\",\n          \"type\": \"PTR\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"ptr.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"PTR\",\n          \"data\": {\n            \"cname\": \"dns.example.\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/slash.uri.example.ansitxt",
    "content": "\u001b[33mURI\u001b[0m \u001b[1;34mslash.uri.example.\u001b[0m 10m00s   10 1 \"/\"\n"
  },
  {
    "path": "xtests/madns/outputs/soa.example.ansitxt",
    "content": "\u001b[35mSOA\u001b[0m \u001b[1;34msoa.example.\u001b[0m 10m00s   \"mname.example.\" \"rname.example.\" 1564274434 1d0h00m00s 2h00m00s 7d0h00m00s 5m00s\n"
  },
  {
    "path": "xtests/madns/outputs/soa.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"soa.example.\",\n          \"class\": \"IN\",\n          \"type\": \"SOA\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"soa.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"SOA\",\n          \"data\": {\n            \"mname\": \"mname.example.\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/srv.example.ansitxt",
    "content": "\u001b[36mSRV\u001b[0m \u001b[1;34msrv.example.\u001b[0m 10m00s   1 1 \"service.example.\":37500\n"
  },
  {
    "path": "xtests/madns/outputs/srv.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"srv.example.\",\n          \"class\": \"IN\",\n          \"type\": \"SRV\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"srv.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"SRV\",\n          \"data\": {\n            \"priority\": 1,\n            \"weight\": 1,\n            \"port\": 37500,\n            \"target\": \"service.example.\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/sshfp.example.ansitxt",
    "content": "\u001b[36mSSHFP\u001b[0m \u001b[1;34msshfp.example.\u001b[0m 10m00s   1 1 212223242526\n"
  },
  {
    "path": "xtests/madns/outputs/sshfp.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"sshfp.example.\",\n          \"class\": \"IN\",\n          \"type\": \"SSHFP\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"sshfp.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"SSHFP\",\n          \"data\": {\n            \"algorithm\": 1,\n            \"fingerprint_type\": 1,\n            \"fingerprint\": \"212223242526\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/tab.str.example.ansitxt",
    "content": "\u001b[33mCNAME\u001b[0m \u001b[1;34mtab.str.example.\u001b[0m 10m00s   \"some\\ttab\\t\\tchars\\t.example.\"\n"
  },
  {
    "path": "xtests/madns/outputs/tab.str.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"tab.str.example.\",\n          \"class\": \"IN\",\n          \"type\": \"CNAME\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"tab.str.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"CNAME\",\n          \"data\": {\n            \"domain\": \"some\\ttab\\t\\tchars\\t.example.\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/tlsa.example.ansitxt",
    "content": "\u001b[33mTLSA\u001b[0m \u001b[1;34mtlsa.example.\u001b[0m 10m00s   3 1 1 \"112233445566\"\n"
  },
  {
    "path": "xtests/madns/outputs/tlsa.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"tlsa.example.\",\n          \"class\": \"IN\",\n          \"type\": \"TLSA\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"tlsa.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"TLSA\",\n          \"data\": {\n            \"certificate_usage\": 3,\n            \"selector\": 1,\n            \"matching_type\": 1,\n            \"certificate_data\": \"112233445566\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/txt.example.ansitxt",
    "content": "\u001b[33mTXT\u001b[0m \u001b[1;34mtxt.example.\u001b[0m 10m00s   \"Cache Invalidation and Naming Things\"\n"
  },
  {
    "path": "xtests/madns/outputs/txt.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"txt.example.\",\n          \"class\": \"IN\",\n          \"type\": \"TXT\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"txt.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"TXT\",\n          \"data\": {\n            \"messages\": [\n              \"Cache Invalidation and Naming Things\"\n            ]\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/upperbit.str.example.ansitxt",
    "content": "\u001b[33mCNAME\u001b[0m \u001b[1;34mupperbit.str.example.\u001b[0m 10m00s   \"\\u{7f}�����.example.\"\n"
  },
  {
    "path": "xtests/madns/outputs/upperbit.str.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"upperbit.str.example.\",\n          \"class\": \"IN\",\n          \"type\": \"CNAME\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"upperbit.str.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"CNAME\",\n          \"data\": {\n            \"domain\": \"\\u007f�����.example.\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/uri.example.ansitxt",
    "content": "\u001b[33mURI\u001b[0m \u001b[1;34muri.example.\u001b[0m 10m00s   10 16 \"https://rfcs.io/\"\n"
  },
  {
    "path": "xtests/madns/outputs/uri.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"uri.example.\",\n          \"class\": \"IN\",\n          \"type\": \"URI\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"uri.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"URI\",\n          \"data\": {\n            \"priority\": 10,\n            \"weight\": 16,\n            \"target\": \"https://rfcs.io/\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/utf8.caa.example.ansitxt",
    "content": "\u001b[31mCAA\u001b[0m \u001b[1;34mutf8.caa.example.\u001b[0m 10m00s   \"issuewild\" \"trustworthy\\240\\159\\140\\180example\" (non-critical)\n"
  },
  {
    "path": "xtests/madns/outputs/utf8.caa.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"utf8.caa.example.\",\n          \"class\": \"IN\",\n          \"type\": \"CAA\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"utf8.caa.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"CAA\",\n          \"data\": {\n            \"critical\": false,\n            \"tag\": \"issuewild\",\n            \"value\": \"trustworthy🌴example\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/utf8.hinfo.example.ansitxt",
    "content": "\u001b[33mHINFO\u001b[0m \u001b[1;34mutf8.hinfo.example.\u001b[0m 10m00s   \"some\\240\\159\\140\\180kinda\\240\\159\\140\\180cpu\" \"some\\240\\159\\140\\180kinda\\240\\159\\140\\180os\"\n"
  },
  {
    "path": "xtests/madns/outputs/utf8.hinfo.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"utf8.hinfo.example.\",\n          \"class\": \"IN\",\n          \"type\": \"HINFO\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"utf8.hinfo.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"HINFO\",\n          \"data\": {\n            \"cpu\": \"some🌴kinda🌴cpu\",\n            \"os\": \"some🌴kinda🌴os\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/utf8.naptr.invalid.ansitxt",
    "content": "\u001b[31mNAPTR\u001b[0m \u001b[1;34mutf8.naptr.invalid.\u001b[0m 10m00s   5 10 \"\\240\\159\\140\\180\" \"\\240\\159\\140\\180\" \"\\240\\159\\140\\180\" \"🌴.\"\n"
  },
  {
    "path": "xtests/madns/outputs/utf8.naptr.invalid.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"utf8.naptr.invalid.\",\n          \"class\": \"IN\",\n          \"type\": \"NAPTR\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"utf8.naptr.invalid.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"NAPTR\",\n          \"data\": {\n            \"order\": 5,\n            \"flags\": \"🌴\",\n            \"service\": \"🌴\",\n            \"regex\": \"🌴\",\n            \"replacement\": \"🌴.\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/utf8.txt.example.ansitxt",
    "content": "\u001b[33mTXT\u001b[0m \u001b[1;34mutf8.txt.example.\u001b[0m 10m00s   \"\\240\\159\\146\\176Cache \\240\\159\\153\\133\\226\\128\\141\\239\\184\\143Invalidation \\226\\133\\139and \\240\\159\\147\\155Naming \\240\\159\\142\\179Things\"\n"
  },
  {
    "path": "xtests/madns/outputs/utf8.txt.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"utf8.txt.example.\",\n          \"class\": \"IN\",\n          \"type\": \"TXT\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"utf8.txt.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"TXT\",\n          \"data\": {\n            \"messages\": [\n              \"💰Cache 🙅‍️Invalidation ⅋and 📛Naming 🎳Things\"\n            ]\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/outputs/utf8.uri.example.ansitxt",
    "content": "\u001b[33mURI\u001b[0m \u001b[1;34mutf8.uri.example.\u001b[0m 10m00s   10 16 \"https://\\240\\159\\146\\169.la/\"\n"
  },
  {
    "path": "xtests/madns/outputs/utf8.uri.example.json",
    "content": "{\n  \"responses\": [\n    {\n      \"queries\": [\n        {\n          \"name\": \"utf8.uri.example.\",\n          \"class\": \"IN\",\n          \"type\": \"URI\"\n        }\n      ],\n      \"answers\": [\n        {\n          \"name\": \"utf8.uri.example.\",\n          \"class\": \"IN\",\n          \"ttl\": 600,\n          \"type\": \"URI\",\n          \"data\": {\n            \"priority\": 10,\n            \"weight\": 16,\n            \"target\": \"https://💩.la/\"\n          }\n        }\n      ],\n      \"authorities\": [],\n      \"additionals\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "xtests/madns/protocol-chars.toml",
    "content": "# Character escaping\n\n[[cmd]]\nname = \"Running with ‘ansi.str.example’ properly escapes the codes\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME ansi.str.example\"\nstdout = { file = \"outputs/ansi.str.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"protocol\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘newline.str.example’ properly escapes the newlines\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME newline.str.example\"\nstdout = { file = \"outputs/newline.str.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"protocol\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘null.str.example’ properly handles the null bytes\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME null.str.example\"\nstdout = { file = \"outputs/null.str.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"protocol\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘tab.str.example’ properly escapes the tabs\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME tab.str.example\"\nstdout = { file = \"outputs/tab.str.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"protocol\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘upperbit.str.example’ properly escapes the upper-bit characters\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME upperbit.str.example\"\nstdout = { file = \"outputs/upperbit.str.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"protocol\", \"madns\" ]\n\n\n# Character escaping (JSON)\n\n[[cmd]]\nname = \"Running with ‘ansi.str.example --json’ properly escapes the codes in the JSON string\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME ansi.str.example --json | jq\"\nstdout = { file = \"outputs/ansi.str.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"protocol\", \"madns\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘newline.str.example --json’ properly escapes the newlines in the JSON string\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME newline.str.example --json | jq\"\nstdout = { file = \"outputs/newline.str.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"protocol\", \"madns\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘null.str.example --json’ properly handles the null bytes in the JSON string\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME null.str.example --json | jq\"\nstdout = { file = \"outputs/null.str.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"protocol\", \"madns\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘tab.str.example --json’ properly escapes the tabs in the JSON string\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME tab.str.example --json | jq\"\nstdout = { file = \"outputs/tab.str.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"protocol\", \"madns\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘upperbit.str.example --json’ properly escapes the upper-bit characters in the JSON string\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} CNAME upperbit.str.example --json | jq\"\nstdout = { file = \"outputs/upperbit.str.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"protocol\", \"madns\", \"json\" ]\n"
  },
  {
    "path": "xtests/madns/protocol-compression.toml",
    "content": "[[cmd]]\nname = \"Running with ‘out-of-range.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A out-of-range.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"protocol\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘recursive-1.invalid’ displays a recursion error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A recursive-1.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: too much recursion: [37]\" }\nstatus = 1\ntags = [ \"protocol\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘recursive-2.invalid’ displays a recursion error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A recursive-2.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: too much recursion: [53, 37]\" }\nstatus = 1\ntags = [ \"protocol\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/protocol-error-codes.toml",
    "content": "[[cmd]]\nname = \"Running with ‘formerr.invalid’ displays the error code\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A formerr.invalid\"\nstdout = { string = \"Status: Format Error\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"protocol\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘servfail.invalid’ displays the error code\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A servfail.invalid\"\nstdout = { string = \"Status: Server Failure\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"protocol\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘nxdomain.invalid’ displays the error code\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A nxdomain.invalid\"\nstdout = { string = \"Status: NXDomain\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"protocol\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘notimp.invalid’ displays the error code\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A notimp.invalid\"\nstdout = { string = \"Status: Not Implemented\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"protocol\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘refused.invalid’ displays the error code\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} A refused.invalid\"\nstdout = { string = \"Status: Query Refused\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"protocol\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/ptr-records.toml",
    "content": "# PTR record successes\n\n[[cmd]]\nname = \"Running with ‘ptr.example’ prints the correct PTR record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} PTR ptr.example\"\nstdout = { file = \"outputs/ptr.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"ptr\", \"madns\" ]\n\n\n# PTR record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘ptr.example --json’ prints the correct PTR record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} PTR ptr.example --json | jq\"\nstdout = { file = \"outputs/ptr.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"ptr\", \"madns\", \"json\" ]\n\n\n# PTR record invalid packets\n\n[[cmd]]\nname = \"Running with ‘empty.ptr.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} PTR empty.ptr.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"ptr\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘incomplete.ptr.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} PTR incomplete.ptr.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"ptr\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/soa-records.toml",
    "content": "# SOA record successes\n\n[[cmd]]\nname = \"Running with ‘soa.example’ prints the correct SOA record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SOA soa.example\"\nstdout = { file = \"outputs/soa.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"soa\", \"madns\" ]\n\n\n# SOA record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘soa.example --json’ prints the correct SOA record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SOA soa.example --json | jq\"\nstdout = { file = \"outputs/soa.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"soa\", \"madns\", \"json\" ]\n\n\n# SOA record invalid packets\n\n[[cmd]]\nname = \"Running with ‘empty.soa.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SOA empty.soa.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"soa\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘incomplete.soa.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SOA incomplete.soa.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"soa\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/srv-records.toml",
    "content": "# SRV record successes\n\n[[cmd]]\nname = \"Running with ‘srv.example’ prints the correct SRV record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SRV srv.example\"\nstdout = { file = \"outputs/srv.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"soa\", \"madns\" ]\n\n\n# SRV record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘srv.example --json’ prints the correct SRV record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SRV srv.example --json | jq\"\nstdout = { file = \"outputs/srv.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"soa\", \"madns\", \"json\" ]\n\n\n# SRV record invalid packets\n\n[[cmd]]\nname = \"Running with ‘empty.srv.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SRV empty.srv.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"soa\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘incomplete.srv.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SRV incomplete.srv.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"soa\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/sshfp-records.toml",
    "content": "# SSHFP record successes\n\n[[cmd]]\nname = \"Running with ‘sshfp.example’ prints the correct SSHFP record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SSHFP sshfp.example\"\nstdout = { file = \"outputs/sshfp.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"sshfp\", \"madns\" ]\n\n\n# SSHFP record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘sshfp.example --json’ prints the correct SSHFP record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SSHFP sshfp.example --json | jq\"\nstdout = { file = \"outputs/sshfp.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"sshfp\", \"madns\", \"json\" ]\n\n\n# SSHFP record invalid packets\n\n[[cmd]]\nname = \"Running with ‘empty.sshfp.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SSHFP empty.sshfp.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"sshfp\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘incomplete.sshfp.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} SSHFP incomplete.sshfp.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"sshfp\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/tlsa-records.toml",
    "content": "# TLSA record successes\n\n[[cmd]]\nname = \"Running with ‘tlsa.example’ prints the correct TLSA record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TLSA tlsa.example\"\nstdout = { file = \"outputs/tlsa.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"tlsa\", \"madns\" ]\n\n\n# TLSA record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘tlsa.example --json’ prints the correct TLSA record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TLSA tlsa.example --json | jq\"\nstdout = { file = \"outputs/tlsa.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"tlsa\", \"madns\", \"json\" ]\n\n\n# TLSA record invalid packets\n\n[[cmd]]\nname = \"Running with ‘empty.tlsa.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TLSA empty.tlsa.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"tlsa\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘incomplete.tlsa.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TLSA incomplete.tlsa.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"tlsa\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/txt-records.toml",
    "content": "# TXT record successes\n\n[[cmd]]\nname = \"Running with ‘txt.example’ prints the correct TXT record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TXT txt.example\"\nstdout = { file = \"outputs/txt.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"txt\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘utf8.txt.example’ escapes characters in the message\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TXT utf8.txt.example\"\nstdout = { file = \"outputs/utf8.txt.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"txt\", \"madns\", \"chars\" ]\n\n[[cmd]]\nname = \"Running with ‘bad-utf8.txt.example’ escapes characters in the message and does not crash\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TXT bad-utf8.txt.example\"\nstdout = { file = \"outputs/bad-utf8.txt.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"txt\", \"madns\", \"chars\" ]\n\n\n# TXT record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘txt.example --json’ prints the correct TXT record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TXT txt.example --json | jq\"\nstdout = { file = \"outputs/txt.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"txt\", \"madns\", \"json\" ]\n\n\n[[cmd]]\nname = \"Running with ‘utf8.txt.example --json’ interprets the response as UTF-8\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TXT utf8.txt.example --json | jq\"\nstdout = { file = \"outputs/utf8.txt.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"txt\", \"madns\", \"chars\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘bad-utf8.txt.example --json’ uses UTF-8 replacement characters\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TXT bad-utf8.txt.example --json | jq\"\nstdout = { file = \"outputs/bad-utf8.txt.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"txt\", \"madns\", \"chars\", \"json\" ]\n\n\n# TXT record invalid packets\n\n[[cmd]]\nname = \"Running with ‘empty.txt.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TXT empty.txt.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"txt\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘incomplete.txt.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} TXT incomplete.txt.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"txt\", \"madns\" ]\n"
  },
  {
    "path": "xtests/madns/uri-records.toml",
    "content": "# URI record successes\n\n[[cmd]]\nname = \"Running with ‘uri.example’ prints the correct URI record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI uri.example\"\nstdout = { file = \"outputs/uri.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"uri\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘slash.uri.example’ still prints the correct URI record\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI slash.uri.example\"\nstdout = { file = \"outputs/slash.uri.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"uri\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘utf8.uri.example’ escapes characters in the URI\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI utf8.uri.example\"\nstdout = { file = \"outputs/utf8.uri.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"uri\", \"madns\", \"chars\" ]\n\n[[cmd]]\nname = \"Running with ‘bad-utf8.uri.example’ escapes characters in the URI and does not crash\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI bad-utf8.uri.example\"\nstdout = { file = \"outputs/bad-utf8.uri.example.ansitxt\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"uri\", \"madns\", \"chars\" ]\n\n\n# URI record successes (JSON)\n\n[[cmd]]\nname = \"Running with ‘uri.example --json’ prints the correct URI record structure\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI uri.example --json | jq\"\nstdout = { file = \"outputs/uri.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"uri\", \"madns\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘utf8.uri.example --json’ interprets the response as UTF-8\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI utf8.uri.example --json | jq\"\nstdout = { file = \"outputs/utf8.uri.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"uri\", \"madns\", \"chars\", \"json\" ]\n\n[[cmd]]\nname = \"Running with ‘bad-utf8.uri.example --json’ uses UTF-8 replacement characters\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI bad-utf8.uri.example --json | jq\"\nstdout = { file = \"outputs/bad-utf8.uri.example.json\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ \"uri\", \"madns\", \"chars\", \"json\" ]\n\n\n# URI record invalid packets\n\n[[cmd]]\nname = \"Running with ‘missing-data.uri.invalid’ displays a packet length error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI missing-data.uri.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: record length should be at least 5, got 4\" }\nstatus = 1\ntags = [ \"uri\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘empty.uri.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI empty.uri.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"uri\", \"madns\" ]\n\n[[cmd]]\nname = \"Running with ‘incomplete.uri.invalid’ displays a protocol error\"\nshell = \"dog --colour=always ${MADNS_ARGS:-@madns.binarystar.systems:5301 --tcp} URI incomplete.uri.invalid\"\nstdout = { empty = true }\nstderr = { string = \"Error [protocol]: Malformed packet: insufficient data\" }\nstatus = 1\ntags = [ \"uri\", \"madns\" ]\n"
  },
  {
    "path": "xtests/options/errors.toml",
    "content": "[[cmd]]\nname = \"Running dog with ‘--wibble’ warns about the invalid argument\"\nshell = \"dog --wibble\"\nstdout = { empty = true }\nstderr = { file = \"outputs/invalid-argument.txt\" }\nstatus = 3\ntags = [ 'options' ]\n\n[[cmd]]\nname = \"Running dog with ‘--class’ warns about the missing argument parameter\"\nshell = \"dog --class\"\nstdout = { empty = true }\nstderr = { file = \"outputs/missing-parameter.txt\" }\nstatus = 3\ntags = [ 'options' ]\n\n[[cmd]]\nname = \"Running dog with ‘--type XYZZY’ warns about the invalid record type\"\nshell = \"dog --type XYZZY dns.google\"\nstdout = { empty = true }\nstderr = { file = \"outputs/invalid-query-type.txt\" }\nstatus = 3\ntags = [ 'options' ]\n\n[[cmd]]\nname = \"Running dog with ‘--class XYZZY’ warns about the invalid class\"\nshell = \"dog --class XYZZY dns.google\"\nstdout = { empty = true }\nstderr = { file = \"outputs/invalid-query-class.txt\" }\nstatus = 3\ntags = [ 'options' ]\n\n[[cmd]]\nname = \"Running dog with ‘-Z aoeu’ warns about the invalid protocol tweak\"\nshell = \"dog -Z aoeu dns.google\"\nstdout = { empty = true }\nstderr = { file = \"outputs/invalid-protocol-tweak.txt\" }\nstatus = 3\ntags = [ 'options' ]\n\n[[cmd]]\nname = \"Running dog with ‘OPT’ warns that OPT requests are sent by default\"\nshell = \"dog OPT dns.google\"\nstdout = { empty = true }\nstderr = { file = \"outputs/opt-query.txt\" }\nstatus = 3\ntags = [ 'options' ]\n\n[[cmd]]\nname = \"Running dog with ‘opt’ also warns that OPT requests are sent by default\"\nshell = \"dog opt dns.google\"\nstdout = { empty = true }\nstderr = { file = \"outputs/opt-query.txt\" }\nstatus = 3\ntags = [ 'options' ]\n\n[[cmd]]\nname = \"Running dog with ‘--type OPT’ warns that OPT requests are sent by default\"\nshell = \"dog --type OPT dns.google\"\nstdout = { empty = true }\nstderr = { file = \"outputs/opt-query.txt\" }\nstatus = 3\ntags = [ 'options' ]\n\n[[cmd]]\nname = \"Running dog with ‘--type opt’ also warns that OPT requests are sent by default\"\nshell = \"dog --type opt dns.google\"\nstdout = { empty = true }\nstderr = { file = \"outputs/opt-query.txt\" }\nstatus = 3\ntags = [ 'options' ]\n\n[[cmd]]\nname = \"Running dog with a domain longer than 255 bytes warns about it being too long\"\nshell = \"dog 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890\"\nstdout = { empty = true }\nstderr = { file = \"outputs/huge-domain.txt\" }\nstatus = 3\ntags = [ 'options' ]\n\n[[cmd]]\nname = \"Running dog with ‘--https’ and no nameserver warns that one is missing\"\nshell = \"dog --https dns.google\"\nstdout = { empty = true }\nstderr = { file = \"outputs/missing-nameserver.txt\" }\nstatus = 3\ntags = [ 'options' ]\n"
  },
  {
    "path": "xtests/options/help.toml",
    "content": "# help\n\n[[cmd]]\nname = \"Running ‘dog --help’ shows help\"\nshell = \"dog --help\"\nstdout = { string = \"dog ●\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ 'options' ]\n\n[[cmd]]\nname = \"Running ‘dog --help --color=automatic’ not to a terminal shows help without colour\"\nshell = \"dog --help --color=automatic\"\nstdout = { string = \"dog ●\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ 'options' ]\n\n[[cmd]]\nname = \"Running ‘dog --help --colour=always’ shows help with colour\"\nshell = \"dog --help --colour=always\"\nstdout = { string = \"dog \\u001B[1;32m●\\u001B[0m\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ 'options' ]\n\n[[cmd]]\nname = \"Running ‘dog --help --color=never’ shows without colour\"\nshell = \"dog --help --color=never\"\nstdout = { string = \"dog ●\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ 'options' ]\n\n[[cmd]]\nname = \"Running ‘dog’ with no arguments shows help\"\nshell = \"dog\"\nstdout = { string = \"dog ●\" }\nstderr = { empty = true }\nstatus = 3\ntags = [ 'options' ]\n\n\n# versions\n\n[[cmd]]\nname = \"Running ‘dog --version’ shows version information\"\nshell = \"dog --version\"\nstdout = { string = \"dog ●\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ 'options' ]\n\n[[cmd]]\nname = \"Running ‘dog --version --colour=automatic’ not to a terminal shows version information without colour\"\nshell = \"dog --version --colour=automatic\"\nstdout = { string = \"dog ●\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ 'options' ]\n\n[[cmd]]\nname = \"Running ‘dog --version --color=always’ shows version information with colour\"\nshell = \"dog --version --color=always\"\nstdout = { string = \"dog \\u001B[1;32m●\\u001B[0m\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ 'options' ]\n\n[[cmd]]\nname = \"Running ‘dog --version --colour=never’ shows version information without colour\"\nshell = \"dog --version --colour=never\"\nstdout = { string = \"dog ●\" }\nstderr = { empty = true }\nstatus = 0\ntags = [ 'options' ]\n"
  },
  {
    "path": "xtests/options/outputs/huge-domain.txt",
    "content": "dog: Invalid options: Invalid domain \"12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890\"\n"
  },
  {
    "path": "xtests/options/outputs/invalid-argument.txt",
    "content": "dog: Invalid options: Unrecognized option: 'wibble'\n"
  },
  {
    "path": "xtests/options/outputs/invalid-protocol-tweak.txt",
    "content": "dog: Invalid options: Invalid protocol tweak \"aoeu\"\n"
  },
  {
    "path": "xtests/options/outputs/invalid-query-class.txt",
    "content": "dog: Invalid options: Invalid query class \"XYZZY\"\n"
  },
  {
    "path": "xtests/options/outputs/invalid-query-type.txt",
    "content": "dog: Invalid options: Invalid query type \"XYZZY\"\n"
  },
  {
    "path": "xtests/options/outputs/missing-nameserver.txt",
    "content": "dog: Invalid options: You must pass a URL as a nameserver when using --https\n"
  },
  {
    "path": "xtests/options/outputs/missing-parameter.txt",
    "content": "dog: Invalid options: Argument to option 'class' missing\n"
  },
  {
    "path": "xtests/options/outputs/opt-query.txt",
    "content": "dog: Invalid options: OPT request is sent by default (see -Z flag)\n"
  }
]