Browse Source

sync with trunk

git-svn-id: svn://bind10.isc.org/svn/bind10/branches/trac436@3865 e5f2f494-b856-4b98-b285-d166d9295462
JINMEI Tatuya 14 years ago
parent
commit
09b5e89b3f

+ 9 - 2
ChangeLog

@@ -1,6 +1,13 @@
+  129.	[func]		jinmei
+	src/lib/dns: Added new functions masterLoad() for loading master
+	zone files.  The initial implementation can only parse a limited
+	form of master files, but BIND 9's named-compilezone can convert
+	any valid zone file into the acceptable form.
+	(Trac #423, svn r3857)
+
   128.  [build]     vorner
-	Test for query name = '.', type = DS to authoritative nameserver for root
-	zone was added.
+	Test for query name = '.', type = DS to authoritative nameserver
+	for root zone was added.
 	(Trac #85, svn r3836)
 
   127.  [bug]       stephen

+ 1 - 0
src/lib/dns/Makefile.am

@@ -69,6 +69,7 @@ libdns___la_SOURCES += dnssectime.h dnssectime.cc
 libdns___la_SOURCES += edns.h edns.cc
 libdns___la_SOURCES += exceptions.h exceptions.cc
 libdns___la_SOURCES += util/hex.h
+libdns___la_SOURCES += masterload.h masterload.cc
 libdns___la_SOURCES += message.h message.cc
 libdns___la_SOURCES += messagerenderer.h messagerenderer.cc
 libdns___la_SOURCES += name.h name.cc

+ 162 - 0
src/lib/dns/masterload.cc

@@ -0,0 +1,162 @@
+// Copyright (C) 2010  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <istream>
+#include <fstream>
+#include <sstream>
+#include <string>
+#include <cctype>
+
+#include <boost/scoped_ptr.hpp>
+
+#include <exceptions/exceptions.h>
+
+#include <dns/masterload.h>
+#include <dns/name.h>
+#include <dns/rdata.h>
+#include <dns/rrclass.h>
+#include <dns/rrset.h>
+#include <dns/rrttl.h>
+#include <dns/rrtype.h>
+
+using namespace std;
+using namespace boost;
+using namespace isc::dns::rdata;
+
+namespace isc {
+namespace dns {
+void
+masterLoad(const char* const filename, const Name& origin,
+           const RRClass& zone_class, MasterLoadCallback callback)
+{
+    ifstream ifs;
+
+    ifs.open(filename, ios_base::in);
+    if (ifs.fail()) {
+        isc_throw(MasterLoadError, "Failed to open master file: " << filename);
+    }
+    masterLoad(ifs, origin, zone_class, callback);
+    ifs.close();
+}
+
+void
+masterLoad(istream& input, const Name& origin, const RRClass& zone_class,
+           MasterLoadCallback callback)
+{
+    RRsetPtr rrset;
+    string line;
+    unsigned int line_count = 1;
+
+    do {
+        getline(input, line);
+        if (input.bad() || (input.fail() && !input.eof())) {
+            isc_throw(MasterLoadError, "Unexpectedly failed to read a line");
+        }
+
+        // blank/comment lines should be simply skipped.
+        if (line.empty() || line[0] == ';') {
+            continue;
+        }
+
+        // The line shouldn't have leading space (which means omitting the
+        // owner name).
+        if (isspace(line[0])) {
+            isc_throw(MasterLoadError, "Leading space at line " << line_count);
+        }
+
+        // Parse a single RR
+        istringstream iss(line);
+        string owner_txt, ttl_txt, rrclass_txt, rrtype_txt;
+        stringbuf rdatabuf;
+        iss >> owner_txt >> ttl_txt >> rrclass_txt >> rrtype_txt >> &rdatabuf;
+        if (iss.bad() || iss.fail()) {
+            isc_throw(MasterLoadError, "Parse failure for a valid RR at line "
+                      << line_count);
+        }
+
+        // This simple version doesn't support relative owner names with a
+        // separate origin.
+        if (owner_txt.empty() || *(owner_txt.end() - 1) != '.') {
+            isc_throw(MasterLoadError, "Owner name is not absolute at line "
+                      << line_count);
+        }
+
+        // XXX: this part is a bit tricky (and less efficient).  We are going
+        // to validate the text for the RR parameters, and throw an exception
+        // if any of them is invalid by converting an underlying exception
+        // to MasterLoadError.  To do that, we need to define the corresponding
+        // variables used for RRset construction outside the try-catch block,
+        // but we don't like to use a temporary variable with a meaningless
+        // initial value.  So we define pointers outside the try block
+        // and allocate/initialize the actual objects within the block.
+        // To make it exception safe we use Boost.scoped_ptr.
+        scoped_ptr<const Name> owner;
+        scoped_ptr<const RRTTL> ttl;
+        scoped_ptr<const RRClass> rrclass;
+        scoped_ptr<const RRType> rrtype;
+        ConstRdataPtr rdata;
+        try {
+            owner.reset(new Name(owner_txt));
+            ttl.reset(new RRTTL(ttl_txt));
+            rrclass.reset(new RRClass(rrclass_txt));
+            rrtype.reset(new RRType(rrtype_txt));
+            rdata = createRdata(*rrtype, *rrclass, rdatabuf.str());
+        } catch (const Exception& ex) {
+            isc_throw(MasterLoadError, "Invalid RR text at line " << line_count
+                      << ": " << ex.what());
+        }
+
+        // Origin related validation:
+        //  - reject out-of-zone data
+        //  - reject SOA whose owner is not at the top of zone
+        const NameComparisonResult cmp_result = owner->compare(origin);
+        if (cmp_result.getRelation() != NameComparisonResult::EQUAL &&
+            cmp_result.getRelation() != NameComparisonResult::SUBDOMAIN) {
+            isc_throw(MasterLoadError, "Out-of-zone data at line "
+                      << line_count);
+        }
+        if (*rrtype == RRType::SOA() &&
+            cmp_result.getRelation() != NameComparisonResult::EQUAL) {
+            isc_throw(MasterLoadError, "SOA not at top of zone at line "
+                      << line_count);
+        }
+
+        // Reject RR class mismatching
+        if (*rrclass != zone_class) {
+            isc_throw(MasterLoadError, "RR class (" << rrclass_txt
+                      << ") does not match the zone class (" << zone_class
+                      << ") at line " << line_count);
+        }
+
+        // Everything is okay.  Now create/update RRset with the new RR.
+        // If this is the first RR or the RR type/name is new, we are seeing
+        // a new RRset.
+        if (!rrset || rrset->getType() != *rrtype ||
+            rrset->getName() != *owner) {
+            // Commit the previous RRset, if any.
+            if (rrset) {
+                callback(rrset);
+            }
+            rrset = RRsetPtr(new RRset(*owner, *rrclass, *rrtype, *ttl));
+        }
+        rrset->addRdata(rdata);
+    } while (++line_count, !input.eof());
+
+    // Commit the last RRset, if any.
+    if (rrset) {
+        callback(rrset);
+    }
+}
+} // namespace dns
+} // namespace isc

+ 246 - 0
src/lib/dns/masterload.h

@@ -0,0 +1,246 @@
+// Copyright (C) 2010  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef __MASTERLOAD_H
+#define __MASTERLOAD_H 1
+
+#include <iosfwd>
+
+#include <boost/function.hpp>
+
+#include <exceptions/exceptions.h>
+
+#include <dns/rrset.h>
+
+namespace isc {
+namespace dns {
+class Name;
+class RRClass;
+
+/// \brief An exception that is thrown if an error occurs while loading a
+/// master zone data.
+class MasterLoadError : public Exception {
+public:
+    MasterLoadError(const char* file, size_t line, const char* what) :
+        isc::Exception(file, line, what) {}
+};
+
+/// The type of the \c callback parameter of \c masterLoad().
+///
+/// This represents a functor object or a function that takes one parameter
+/// of type \c RRsetPtr and returns nothing.
+typedef boost::function<void(RRsetPtr rrset)> MasterLoadCallback;
+
+///
+/// \name Master zone file loader functions.
+///
+//@{
+/// Master zone file loader from a file.
+///
+/// This function parses a given file as a master DNS zone file for
+/// the given origin name and RR class, constructs a sequence of \c RRset
+/// from the RRs containing in the file, and calls the given \c callback
+/// functor object or function with each \c RRset.
+///
+/// The \c callback parameter is a functor object or a function that
+/// takes one parameter of type \c RRsetPtr and returns nothing,
+/// i.e. \c void (see below for specific examples).
+/// More precisely, it can be anything that this form of boost::function
+/// can represent, but the caller normally doesn't have to care about
+/// that level of details.
+///
+/// The ownership of constructed RRsets is transferred to the callback
+/// and this function never uses it once it is called.
+/// The callback can freely modify the passed \c RRset.
+///
+/// This function performs minimum level of validation on the input:
+/// - Each RR is a valid textual representation per the DNS protocol.
+/// - The class of each RR must be identical to the specified RR class.
+/// - The owner name of each RR must be a subdomain of the origin name
+///   (that can be equal to the origin).
+/// - If an SOA RR is included, its owner name must be the origin name.
+/// If any of these validation checks fails, this function throws an
+/// exception of class \c MasterLoadError.
+///
+/// It does not perform other semantical checks, however.  For example,
+/// it doesn't check if an NS RR of the origin name is included or if
+/// there is more than one SOA RR.  Such further checks are the caller's
+/// (or the callback's) responsibility.
+///
+/// <b>Acceptable Format</b>
+///
+/// The current implementation only supports a restricted form of master files
+/// for simplicity.  One easy way to ensure that a handwritten zone file is
+/// acceptable to this implementation is to preprocess it with BIND 9's
+/// named-compilezone tool with both the input and output formats being
+/// "text".
+/// Here is an example:
+/// \code % named-compilezone -f text -F text -o example.com.norm
+///      example.com example.com.zone
+/// \endcode
+/// where example.com.zone is the original zone file for the "example.com"
+/// zone.  The output file is example.com.norm, which should be acceptable
+/// by this implementation.
+///
+/// Below are specific restrictions that this implementation assumes.
+/// Basically, each RR must consist of exactly one line
+/// (so there shouldn't be a multi-line RR) in the following format:
+/// \code  <owner name> <TTL> <RRCLASS> <RRTYPE> <RDATA (single line)>
+/// \endcode
+/// Here are some more details about the restrictions:
+/// - No special directives such as $TTL are supported.
+/// - The owner name must be absolute, that is, it must end with a period.
+/// - "@" is not recognized as a valid owner name.
+/// - Owner names, TTL and RRCLASS cannot be omitted.
+/// - As a corollary, a non blank line must not begin with a space character.
+/// - The order of the RR parameters is fixed, for example, this is acceptable:
+/// \code example.com. 3600 IN A 192.0.2.1
+/// \endcode
+///  but this is not even though it's valid per RFC1035:
+/// \code example.com. IN 3600 A 192.0.2.1
+/// \endcode
+/// - <TTL>, <RRCLASS>, and <RRTYPE> must be recognizable by the \c RRTTL,
+///   RRClass and RRType class implementations of this library.  In particular,
+///   as of this writing TTL must be a decimal number (a convenient extension
+///   such as "1H" instead of 3600 cannot be used).  Not all standard RR
+///   classes and RR types are supported yet, so the mnemonics of them will
+///   be rejected, too.
+/// - RR TTLs of the same RRset must be the same; even if they are different,
+///   this implementation simply uses the TTL of the first RR.
+///
+/// Blank lines and lines beginning with a semi-colon are allowed, and will
+/// be simply ignored.  Comments cannot coexist with an RR line, however.
+/// For example, this will be rejected:
+/// \code example.com. 3600 IN A 192.0.2.1 ; this is a comment
+/// \endcode
+///
+/// This implementation assumes that RRs of a single RRset are not
+/// interleaved with RRs of a different RRset.
+/// That is, the following sequence shouldn't happen:
+/// \code example.com. 3600 IN A 192.0.2.1
+/// example.com. 3600 IN AAAA 2001:db8::1
+/// example.com. 3600 IN A 192.0.2.2
+/// \endcode
+/// But it does not consider this an error; it will simply regard each RR
+/// as a separate RRset and call the callback with them separately.
+/// It is up to the callback to merge multiple RRsets into one if possible
+/// and necessary.
+///
+/// <b>Exceptions</b>
+///
+/// This function throws an exception of class \c MasterLoadError in the
+/// following cases:
+/// - Any of the validation checks fails (see the class description).
+/// - The input data is not in the acceptable format (see the details of
+///   the format above).
+/// - The specified file cannot be opened for loading.
+/// - An I/O error occurs during the loading.
+///
+/// In addition, this function requires resource allocation for parsing and
+/// constructing RRsets.  If it fails, the corresponding standard exception
+/// will be thrown.
+///
+/// The callback may throw its own function.  This function doesn't catch it
+/// and will simply propagate it towards the caller.
+///
+/// <b>Usage Examples</b>
+///
+/// A simplest example usage of this function would be to parse a zone
+/// file and (after validation) dump the content to the standard output.
+/// This is an example functor object and a call to \c masterLoad
+/// that implements this scenario:
+/// \code struct ZoneDumper {
+///     void operator()(ConstRRsetPtr rrset) const {
+///        std::cout << *rrset;
+///     }
+/// };
+/// ...
+///    masterLoad(zone_file, Name("example.com"), RRClass::IN(), ZoneDumper());
+/// \endcode
+/// Alternatively, you can use a normal function instead of a functor:
+/// \code void zoneDumper(ConstRRsetPtr rrset) {
+///    std::cout << *rrset;
+/// }
+/// ...
+///    masterLoad(zone_file, Name("example.com"), RRClass::IN(), zoneDumper);
+/// \endcode
+/// Or, if you want to use it with a member function of some other class,
+/// wrapping things with \c boost::bind would be handy:
+/// \code class ZoneDumper {
+/// public:
+///    void dump(ConstRRsetPtr rrset) const {
+///        std::cout << *rrset;
+///    }
+/// };
+/// ...
+///    ZoneDumper dumper;
+///    masterLoad(rr_stream, Name("example.com"), RRClass::IN(),
+///               boost::bind(&ZoneDumper::dump, &dumper, _1));
+/// \endcode
+/// You can find a bit more complicated examples in the unit tests code for
+/// this function.
+///
+/// <b>Implementation Notes</b>
+///
+/// The current implementation is in a preliminary level and needs further
+/// extensions.  Some design decisions may also have to be reconsidered as
+/// we gain experiences.  Those include:
+/// - We should be more flexible about the input format.
+/// - We may want to allow optional conditions.  For example, we may want to
+///   be generous about some validation failures and be able to continue
+///   parsing.
+/// - Especially if we allow to be generous, we may also want to support
+///   returning an error code instead of throwing an exception when we
+///   encounter validation failure.
+/// - We may want to support incremental loading.
+/// - If we add these optional features we may want to introduce a class
+///   that encapsulates loading status and options.
+/// - RRSIGs are currently identified as their owner name and RR type (RRSIG).
+///   In practice it should be sufficient, but technically we should also
+///   consider the Type Covered field.
+///
+/// \param filename A path to a master zone file to be loaded.
+/// \param origin The origin name of the zone.
+/// \param zone_class The RR class of the zone.
+/// \param callbck A callback functor or function that is to be called
+/// for each RRset.
+void masterLoad(const char* const filename, const Name& origin,
+                const RRClass& zone_class, MasterLoadCallback callback);
+
+/// Master zone file loader from input stream.
+///
+/// This function is same as the other version
+/// (\c masterLoad(const char* const, const Name&, const RRClass&, MasterLoadCallback))
+/// except that it takes a \c std::istream instead of a file.
+/// It extracts lines from the stream and handles each line just as a line
+/// of a file for the other version of function.
+/// All descriptions of the other version apply to this version except those
+/// specific to file I/O.
+///
+/// \param input An input stream object that is to emit zone's RRs.
+/// \param origin The origin name of the zone.
+/// \param zone_class The RR class of the zone.
+/// \param callbck A callback functor or function that is to be called for
+/// each RRset.
+void masterLoad(std::istream& input, const Name& origin,
+                const RRClass& zone_class, MasterLoadCallback callback);
+}
+//@}
+}
+
+#endif  // __MASTERLOAD_H
+
+// Local Variables:
+// mode: c++
+// End:

+ 1 - 0
src/lib/dns/tests/Makefile.am

@@ -42,6 +42,7 @@ run_unittests_SOURCES += rdata_tsig_unittest.cc
 run_unittests_SOURCES += rrset_unittest.cc rrsetlist_unittest.cc
 run_unittests_SOURCES += question_unittest.cc
 run_unittests_SOURCES += rrparamregistry_unittest.cc
+run_unittests_SOURCES += masterload_unittest.cc
 run_unittests_SOURCES += message_unittest.cc
 run_unittests_SOURCES += base32hex_unittest.cc
 run_unittests_SOURCES += base64_unittest.cc

+ 268 - 0
src/lib/dns/tests/masterload_unittest.cc

@@ -0,0 +1,268 @@
+// Copyright (C) 2010  Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#include <functional>
+#include <ios>
+#include <fstream>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include <boost/bind.hpp>
+
+#include <gtest/gtest.h>
+
+#include <dns/masterload.h>
+#include <dns/name.h>
+#include <dns/rrclass.h>
+#include <dns/rrset.h>
+
+using namespace std;
+using namespace isc::dns;
+
+namespace {
+// A callback functor for masterLoad() commonly used for the following tests.
+class TestCallback : public unary_function<ConstRRsetPtr, void> {
+public:
+    TestCallback(vector<ConstRRsetPtr>& rrsets) : rrsets_(rrsets) {}
+    void operator()(ConstRRsetPtr rrset) {
+        rrsets_.push_back(rrset);
+    }
+private:
+    vector<ConstRRsetPtr>& rrsets_;
+};
+
+// A function version of TestCallback.
+void
+testCallback(ConstRRsetPtr rrset, vector<ConstRRsetPtr>* rrsets) {
+    rrsets->push_back(rrset);
+}
+
+class MasterLoadTest : public ::testing::Test {
+protected:
+    MasterLoadTest() : origin("example.com"), zclass(RRClass::IN()),
+                   callback(results) {}
+public:
+    void rrsetCallback(ConstRRsetPtr rrset) {
+        results.push_back(rrset);
+    }
+protected:
+    Name origin;
+    RRClass zclass;
+    stringstream rr_stream;
+    vector<ConstRRsetPtr> results;
+    TestCallback callback;
+};
+
+// Commonly used test RRs
+const char* const txt_rr = "example.com. 3600 IN TXT \"test data\"\n";
+const char* const a_rr1 = "www.example.com. 60 IN A 192.0.2.1\n";
+const char* const a_rr2 = "www.example.com. 60 IN A 192.0.2.2\n";
+const char* const a_rr3 = "ftp.example.com. 60 IN A 192.0.2.3\n";
+// multi-field RR case
+const char* const soa_rr = "example.com. 7200 IN SOA . . 0 0 0 0 0\n";
+
+TEST_F(MasterLoadTest, loadRRs) {
+    // a simple case: loading 3 RRs, each consists of a single RRset.
+    rr_stream << txt_rr << a_rr1 << soa_rr;
+    masterLoad(rr_stream, origin, zclass, callback);
+    ASSERT_EQ(3, results.size());
+    EXPECT_EQ(txt_rr, results[0]->toText());
+    EXPECT_EQ(a_rr1, results[1]->toText());
+    EXPECT_EQ(soa_rr, results[2]->toText());
+}
+
+TEST_F(MasterLoadTest, loadWithFunctionCallback) {
+    // The same test as loadRRs but using a normal function (not a functor
+    // object)
+    rr_stream << txt_rr << a_rr1 << soa_rr;
+    masterLoad(rr_stream, origin, zclass,
+               bind2nd(ptr_fun(testCallback), &results));
+    ASSERT_EQ(3, results.size());
+    EXPECT_EQ(txt_rr, results[0]->toText());
+    EXPECT_EQ(a_rr1, results[1]->toText());
+    EXPECT_EQ(soa_rr, results[2]->toText());
+}
+
+TEST_F(MasterLoadTest, loadWithMemFunctionCallback) {
+    // The same test as loadRRs but using a class member function (with a
+    // help of Boost.bind)
+    rr_stream << txt_rr << a_rr1 << soa_rr;
+    masterLoad(rr_stream, origin, zclass,
+               boost::bind(&MasterLoadTest::rrsetCallback, this, _1));
+    ASSERT_EQ(3, results.size());
+    EXPECT_EQ(txt_rr, results[0]->toText());
+    EXPECT_EQ(a_rr1, results[1]->toText());
+    EXPECT_EQ(soa_rr, results[2]->toText());
+}
+
+TEST_F(MasterLoadTest, loadComments) {
+    rr_stream << ";; comment line, should be skipped\n"
+              << "\n"           // blank line (should be skipped)
+              << txt_rr;
+    masterLoad(rr_stream, origin, zclass, callback);
+    ASSERT_EQ(1, results.size());
+    EXPECT_EQ(txt_rr, results[0]->toText());
+}
+
+TEST_F(MasterLoadTest, loadRRset) {
+    // load an RRset containing two RRs
+    rr_stream << a_rr1 << a_rr2;
+    masterLoad(rr_stream, origin, zclass, callback);
+    ASSERT_EQ(1, results.size());
+    EXPECT_EQ(string(a_rr1) + string(a_rr2), results[0]->toText());
+}
+
+TEST_F(MasterLoadTest, loadRRsetsOfSameType) {
+    // load two RRsets with the same RR type and different owner names.
+    // the loader must distinguish them as separate RRsets.
+    rr_stream << a_rr1 << a_rr3;
+    masterLoad(rr_stream, origin, zclass, callback);
+    ASSERT_EQ(2, results.size());
+    EXPECT_EQ(a_rr1, results[0]->toText());
+    EXPECT_EQ(a_rr3, results[1]->toText());
+}
+
+TEST_F(MasterLoadTest, loadRRsetsInterleaved) {
+    // two RRs that belongs to the same RRset (rr1 and rr2) are interleaved
+    // by another.  This is an unexpected case for this loader, but it's
+    // not considered an error.  The loader will simply treat them separate
+    // RRsets.
+    rr_stream << a_rr1 << a_rr3 << a_rr2;
+    masterLoad(rr_stream, origin, zclass, callback);
+    ASSERT_EQ(3, results.size());
+    EXPECT_EQ(a_rr1, results[0]->toText());
+    EXPECT_EQ(a_rr3, results[1]->toText());
+    EXPECT_EQ(a_rr2, results[2]->toText());
+}
+
+TEST_F(MasterLoadTest, loadWithNoEOF) {
+    // the input stream doesn't end with a new line (and the following blank
+    // line).  It should be accepted.
+    string rr_string(a_rr1);
+    rr_string.erase(rr_string.end() - 1);
+    rr_stream << rr_string;
+    masterLoad(rr_stream, origin, zclass, callback);
+    ASSERT_EQ(1, results.size());
+    EXPECT_EQ(a_rr1, results[0]->toText());
+}
+
+TEST_F(MasterLoadTest, loadEmpty) {
+    // an unusual case: empty input.  load must succeed with an empty result.
+    masterLoad(rr_stream, origin, zclass, callback);
+    EXPECT_EQ(0, results.size());   
+}
+
+TEST_F(MasterLoadTest, loadWithBeginningSpace) {
+    rr_stream << " " << a_rr1;
+    EXPECT_THROW(masterLoad(rr_stream, origin, zclass, callback),
+                 MasterLoadError);
+}
+
+TEST_F(MasterLoadTest, loadWithBeginningTab) {
+    rr_stream << "\t" << a_rr1;
+    EXPECT_THROW(masterLoad(rr_stream, origin, zclass, callback),
+                 MasterLoadError);
+}
+
+TEST_F(MasterLoadTest, loadInvalidRRClass) {
+    rr_stream << "example.com. 3600 CH TXT \"test text\"";
+    EXPECT_THROW(masterLoad(rr_stream, origin, zclass, callback),
+                 MasterLoadError);
+}
+
+TEST_F(MasterLoadTest, loadOutOfZoneData) {
+    rr_stream << "example.org. 3600 IN A 192.0.2.255";
+    EXPECT_THROW(masterLoad(rr_stream, origin, zclass, callback),
+                 MasterLoadError);
+}
+
+TEST_F(MasterLoadTest, loadNonAtopSOA) {
+    // SOA's owner name must be zone's origin.
+    rr_stream << "soa.example.com. 3600 IN SOA . . 0 0 0 0 0";
+    EXPECT_THROW(masterLoad(rr_stream, origin, zclass, callback),
+                 MasterLoadError);
+}
+
+TEST_F(MasterLoadTest, loadBadRRText) {
+    rr_stream << "example..com. 3600 IN A 192.0.2.1"; // bad owner name
+    EXPECT_THROW(masterLoad(rr_stream, origin, zclass, callback),
+                 MasterLoadError);
+
+    // currently we only support numeric TTLs
+    stringstream rr_stream2("example.com. 1D IN A 192.0.2.1");
+    EXPECT_THROW(masterLoad(rr_stream2, origin, zclass, callback),
+                 MasterLoadError);
+
+    // bad RR class text
+    stringstream rr_stream3("example.com. 3600 BAD A 192.0.2.1");
+    EXPECT_THROW(masterLoad(rr_stream3, origin, zclass, callback),
+                 MasterLoadError);
+
+    // bad RR type text
+    stringstream rr_stream4("example.com. 3600 IN BAD 192.0.2.1");
+    EXPECT_THROW(masterLoad(rr_stream4, origin, zclass, callback),
+                 MasterLoadError);
+
+    // bad RDATA text
+    stringstream rr_stream5("example.com. 3600 IN A 2001:db8::1");
+    EXPECT_THROW(masterLoad(rr_stream5, origin, zclass, callback),
+                 MasterLoadError);
+
+    // incomplete RR text
+    stringstream rr_stream6("example.com. 3600 IN A");
+    EXPECT_THROW(masterLoad(rr_stream6, origin, zclass, callback),
+                 MasterLoadError);
+
+    // owner name is not absolute
+    stringstream rr_stream7("example.com 3600 IN A 192.0.2.1");
+    EXPECT_THROW(masterLoad(rr_stream7, origin, zclass, callback),
+                 MasterLoadError);
+}
+
+// This is a helper callback to test the case the input stream becomes bad
+// in the middle of processing.
+class StreamInvalidator : public unary_function<ConstRRsetPtr, void> {
+public:
+    StreamInvalidator(stringstream& ss) : ss_(ss) {}
+    void operator()(ConstRRsetPtr) {
+        ss_.setstate(ios::badbit);
+    }
+private:
+    stringstream& ss_;
+};
+
+TEST_F(MasterLoadTest, loadBadStream) {
+    rr_stream << txt_rr << a_rr1;
+    StreamInvalidator invalidator(rr_stream);
+    EXPECT_THROW(masterLoad(rr_stream, origin, zclass, invalidator),
+                 MasterLoadError);
+}
+
+TEST_F(MasterLoadTest, loadFromFile) {
+    // The main parser is shared with the stream version, so we simply test
+    // file I/O specific parts.
+    masterLoad(TEST_DATA_SRCDIR "/masterload.txt", origin, zclass, callback);
+    ASSERT_EQ(2, results.size());
+    EXPECT_EQ(txt_rr, results[0]->toText());
+    EXPECT_EQ(string(a_rr1) + string(a_rr2), results[1]->toText());
+
+    // NULL file name.  Should result in exception.
+    EXPECT_THROW(masterLoad(NULL, origin, zclass, callback), MasterLoadError);
+
+    // Non existent file name.  Ditto.
+    EXPECT_THROW(masterLoad(TEST_DATA_BUILDDIR "/notexistent.txt", origin,
+                            zclass, callback), MasterLoadError);
+}
+} // end namespace

+ 1 - 0
src/lib/dns/tests/testdata/Makefile.am

@@ -26,6 +26,7 @@ BUILT_SOURCES += rdata_tsig_toWire5.wire
 EXTRA_DIST = gen-wiredata.py.in
 EXTRA_DIST += edns_toWire1.spec edns_toWire2.spec
 EXTRA_DIST += edns_toWire3.spec edns_toWire4.spec
+EXTRA_DIST += masterload.txt
 EXTRA_DIST += message_fromWire1 message_fromWire2
 EXTRA_DIST += message_fromWire3 message_fromWire4
 EXTRA_DIST += message_fromWire5 message_fromWire6

+ 5 - 0
src/lib/dns/tests/testdata/masterload.txt

@@ -0,0 +1,5 @@
+;; a simple (incomplete) zone file
+
+example.com. 3600 IN TXT "test data"
+www.example.com. 60 IN A 192.0.2.1
+www.example.com. 60 IN A 192.0.2.2