Browse Source

Merge #2377

The isc::dns::MasterLoader class and its basic implementation. Further handling
is going to be added in future branches, this handles just the simplest form of
master files.
Michal 'vorner' Vaner 12 years ago
parent
commit
70f71c5413

+ 15 - 5
src/lib/datasrc/master_loader_callbacks.cc

@@ -48,6 +48,19 @@ logWarning(const isc::dns::Name& name, const isc::dns::RRClass& rrclass,
         arg(name).arg(rrclass).arg(reason);
 }
 
+void
+addRR(const isc::dns::Name& name, const isc::dns::RRClass& rrclass,
+      const isc::dns::RRType& type, const isc::dns::RRTTL& ttl,
+      const isc::dns::rdata::RdataPtr& data, ZoneUpdater* updater)
+{
+    // We get description of one RR. The updater takes RRset, so we
+    // wrap it up and push there. It should collate the RRsets of the
+    // same name and type together, since the addRRset should "merge".
+    isc::dns::BasicRRset rrset(name, rrclass, type, ttl);
+    rrset.addRdata(data);
+    updater->addRRset(rrset);
+}
+
 }
 
 isc::dns::MasterLoaderCallbacks
@@ -61,12 +74,9 @@ createMasterLoaderCallbacks(const isc::dns::Name& name,
                                                         rrclass, _1, _2, _3)));
 }
 
-isc::dns::AddRRsetCallback
+isc::dns::AddRRCallback
 createMasterLoaderAddCallback(ZoneUpdater& updater) {
-    return (boost::bind(&ZoneUpdater::addRRset, &updater,
-                        // The callback provides a shared pointer, we
-                        // need the object. This bind unpacks the object.
-                        boost::bind(&isc::dns::RRsetPtr::operator*, _1)));
+    return (boost::bind(addRR, _1, _2, _3, _4, _5, &updater));
 }
 
 }

+ 1 - 1
src/lib/datasrc/master_loader_callbacks.h

@@ -58,7 +58,7 @@ createMasterLoaderCallbacks(const isc::dns::Name& name,
 /// \param updater The zone updater to use.
 /// \return The callback to be passed to MasterLoader.
 /// \throw std::bad_alloc when allocation fails.
-isc::dns::AddRRsetCallback
+isc::dns::AddRRCallback
 createMasterLoaderAddCallback(ZoneUpdater& updater);
 
 }

+ 31 - 7
src/lib/datasrc/tests/master_loader_callbacks_test.cc

@@ -18,6 +18,9 @@
 #include <dns/rrset.h>
 #include <dns/rrclass.h>
 #include <dns/rrttl.h>
+#include <dns/rdata.h>
+
+#include <testutils/dnsmessage_test.h>
 
 #include <exceptions/exceptions.h>
 
@@ -40,8 +43,21 @@ public:
     // the correct ones, according to a predefined set in a list.
     virtual void addRRset(const isc::dns::AbstractRRset& rrset) {
         ASSERT_FALSE(expected_rrsets_.empty());
-        // In our tests, pointer equality is enough.
-        EXPECT_EQ(expected_rrsets_.front().get(), &rrset);
+
+        // As the rrsetCheck requires a shared pointer, we need to create
+        // a copy.
+        isc::dns::RRsetPtr copy(new isc::dns::BasicRRset(rrset.getName(),
+                                                         rrset.getClass(),
+                                                         rrset.getType(),
+                                                         rrset.getTTL()));
+        EXPECT_FALSE(rrset.getRRsig()) << "Unexpected RRSIG on rrset, not "
+            "copying. Following check will likely fail as a result.";
+        for (isc::dns::RdataIteratorPtr it(rrset.getRdataIterator());
+             !it->isLast(); it->next()) {
+            copy->addRdata(it->getCurrent());
+        }
+
+        isc::testutils::rrsetCheck(expected_rrsets_.front(), copy);
         // And remove this RRset, as it has been used.
         expected_rrsets_.pop_front();
     }
@@ -67,14 +83,22 @@ protected:
                                                isc::dns::RRClass::IN(), &ok_))
     {}
     // Generate a new RRset, put it to the updater and return it.
-    isc::dns::RRsetPtr generateRRset() {
+    void generateRRset(isc::dns::AddRRCallback callback) {
         const isc::dns::RRsetPtr
             result(new isc::dns::RRset(isc::dns::Name("example.org"),
                                        isc::dns::RRClass::IN(),
                                        isc::dns::RRType::A(),
                                        isc::dns::RRTTL(3600)));
+        const isc::dns::rdata::RdataPtr
+            data(isc::dns::rdata::createRdata(isc::dns::RRType::A(),
+                                              isc::dns::RRClass::IN(),
+                                              "192.0.2.1"));
+
+        result->addRdata(data);
         updater_.expected_rrsets_.push_back(result);
-        return (result);
+
+        callback(result->getName(), result->getClass(), result->getType(),
+                 result->getTTL(), data);
     }
     // An updater to be passed to the context
     MockUpdater updater_;
@@ -112,11 +136,11 @@ TEST_F(MasterLoaderCallbackTest, callbacks) {
 
 // Try adding some RRsets.
 TEST_F(MasterLoaderCallbackTest, addRRset) {
-    isc::dns::AddRRsetCallback
+    isc::dns::AddRRCallback
         callback(createMasterLoaderAddCallback(updater_));
     // Put some of them in.
-    EXPECT_NO_THROW(callback(generateRRset()));
-    EXPECT_NO_THROW(callback(generateRRset()));
+    EXPECT_NO_THROW(generateRRset(callback));
+    EXPECT_NO_THROW(generateRRset(callback));
     // They all get pushed there right away, so there are none in the queue
     EXPECT_TRUE(updater_.expected_rrsets_.empty());
 }

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

@@ -102,6 +102,7 @@ libb10_dns___la_SOURCES += labelsequence.h labelsequence.cc
 libb10_dns___la_SOURCES += masterload.h masterload.cc
 libb10_dns___la_SOURCES += master_lexer.h master_lexer.cc
 libb10_dns___la_SOURCES += master_lexer_state.h
+libb10_dns___la_SOURCES += master_loader.h master_loader.cc
 libb10_dns___la_SOURCES += message.h message.cc
 libb10_dns___la_SOURCES += messagerenderer.h messagerenderer.cc
 libb10_dns___la_SOURCES += name.h name.cc

+ 278 - 0
src/lib/dns/master_loader.cc

@@ -0,0 +1,278 @@
+// Copyright (C) 2012  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 <dns/master_loader.h>
+#include <dns/master_lexer.h>
+#include <dns/name.h>
+#include <dns/rrttl.h>
+#include <dns/rrclass.h>
+#include <dns/rrtype.h>
+#include <dns/rdata.h>
+
+#include <string>
+#include <memory>
+
+using std::string;
+using std::auto_ptr;
+
+namespace isc {
+namespace dns {
+
+class MasterLoader::MasterLoaderImpl {
+public:
+    MasterLoaderImpl(const char* master_file,
+                     const Name& zone_origin,
+                     const RRClass& zone_class,
+                     const MasterLoaderCallbacks& callbacks,
+                     const AddRRCallback& add_callback,
+                     MasterLoader::Options options) :
+        lexer_(),
+        zone_origin_(zone_origin),
+        zone_class_(zone_class),
+        callbacks_(callbacks),
+        add_callback_(add_callback),
+        options_(options),
+        master_file_(master_file),
+        initialized_(false),
+        ok_(true),
+        many_errors_((options & MANY_ERRORS) != 0),
+        complete_(false),
+        seen_error_(false)
+    {}
+
+    void reportError(const std::string& filename, size_t line,
+                     const std::string& reason)
+    {
+        seen_error_ = true;
+        callbacks_.error(filename, line, reason);
+        if (!many_errors_) {
+            // In case we don't have the lenient mode, every error is fatal
+            // and we throw
+            ok_ = false;
+            complete_ = true;
+            isc_throw(MasterLoaderError, reason.c_str());
+        }
+    }
+
+    void pushSource(const std::string& filename) {
+        std::string error;
+        if (!lexer_.pushSource(filename.c_str(), &error)) {
+            if (initialized_) {
+                // $INCLUDE file
+                reportError(lexer_.getSourceName(), lexer_.getSourceLine(),
+                            error);
+            } else {
+                // Top-level file
+                reportError("", 0, error);
+                ok_ = false;
+            }
+        }
+        initialized_ = true;
+    }
+
+    void pushStreamSource(std::istream& stream) {
+        lexer_.pushSource(stream);
+        initialized_ = true;
+    }
+
+    // Get a string token. Handle it as error if it is not string.
+    const string getString() {
+        lexer_.getNextToken(MasterToken::STRING).getString(string_token_);
+        return (string_token_);
+    }
+
+    bool loadIncremental(size_t count_limit);
+
+private:
+    MasterLexer lexer_;
+    const Name zone_origin_;
+    const RRClass zone_class_;
+    MasterLoaderCallbacks callbacks_;
+    AddRRCallback add_callback_;
+    const MasterLoader::Options options_;
+    const std::string master_file_;
+    std::string string_token_;
+    bool initialized_;
+    bool ok_;                   // Is it OK to continue loading?
+    const bool many_errors_;    // Are many errors allowed (or should we abort
+                                // on the first)
+public:
+    bool complete_;             // All work done.
+    bool seen_error_;           // Was there at least one error during the
+                                // load?
+};
+
+bool
+MasterLoader::MasterLoaderImpl::loadIncremental(size_t count_limit) {
+    if (count_limit == 0) {
+        isc_throw(isc::InvalidParameter, "Count limit set to 0");
+    }
+    if (complete_) {
+        isc_throw(isc::InvalidOperation,
+                  "Trying to load when already loaded");
+    }
+    if (!initialized_) {
+        pushSource(master_file_);
+    }
+    size_t count = 0;
+    while (ok_ && count < count_limit) {
+        try {
+            // Skip all EOLNs (empty lines) and finish on EOF
+            bool empty = true;
+            do {
+                const MasterToken& empty_token(lexer_.getNextToken());
+                if (empty_token.getType() == MasterToken::END_OF_FILE) {
+                    // TODO: Check if this is the last source, possibly pop
+                    return (true);
+                }
+                empty = empty_token.getType() == MasterToken::END_OF_LINE;
+            } while (empty);
+            // Return the last token, as it was not empty
+            lexer_.ungetToken();
+
+            const MasterToken::StringRegion&
+                name_string(lexer_.getNextToken(MasterToken::QSTRING).
+                            getStringRegion());
+            // TODO $ handling
+            const Name name(name_string.beg, name_string.len,
+                            &zone_origin_);
+            // TODO: Some more flexibility. We don't allow omitting
+            // anything yet
+
+            // The parameters
+            const RRTTL ttl(getString());
+            const RRClass rrclass(getString());
+            const RRType rrtype(getString());
+
+            // TODO: Some more validation?
+            if (rrclass != zone_class_) {
+                // It doesn't really matter much what type of exception
+                // we throw, we catch it just below.
+                isc_throw(isc::BadValue, "Class mismatch: " << rrclass <<
+                          "vs. " << zone_class_);
+            }
+            // TODO: Check if it is SOA, it should be at the origin.
+
+            const rdata::RdataPtr data(rdata::createRdata(rrtype, rrclass,
+                                                          lexer_,
+                                                          &zone_origin_,
+                                                          options_,
+                                                          callbacks_));
+            // In case we get NULL, it means there was error creating
+            // the Rdata. The errors should have been reported by
+            // callbacks_ already. We need to decide if we want to continue
+            // or not.
+            if (data) {
+                add_callback_(name, rrclass, rrtype, ttl, data);
+
+                // Good, we loaded another one
+                ++count;
+            } else {
+                seen_error_ = true;
+                if (!many_errors_) {
+                    ok_ = false;
+                    complete_ = true;
+                    // We don't have the exact error here, but it was reported
+                    // by the error callback.
+                    isc_throw(MasterLoaderError, "Invalid RR data");
+                }
+            }
+        } catch (const MasterLoaderError&) {
+            // This is a hack. We exclude the MasterLoaderError from the
+            // below case. Once we restrict the below to some smaller
+            // exception, we should remove this.
+            throw;
+        } catch (const isc::Exception& e) {
+            // TODO: Once we do #2518, catch only the DNSTextError here,
+            // not isc::Exception. The rest should be just simply
+            // propagated.
+            reportError(lexer_.getSourceName(), lexer_.getSourceLine(),
+                        e.what());
+            // We want to continue. Try to read until the end of line
+            bool end = false;
+            do {
+                const MasterToken& token(lexer_.getNextToken());
+                switch (token.getType()) {
+                    case MasterToken::END_OF_FILE:
+                        callbacks_.warning(lexer_.getSourceName(),
+                                           lexer_.getSourceLine(),
+                                           "File does not end with newline");
+                        // TODO: Try pop in case this is not the only
+                        // source
+                        return (true);
+                    case MasterToken::END_OF_LINE:
+                        end = true;
+                        break;
+                    default:
+                        // Do nothing. This is just to make compiler
+                        // happy
+                        break;
+                }
+            } while (!end);
+        }
+    }
+    // When there was a fatal error and ok is false, we say we are done.
+    return (!ok_);
+}
+
+MasterLoader::MasterLoader(const char* master_file,
+                           const Name& zone_origin,
+                           const RRClass& zone_class,
+                           const MasterLoaderCallbacks& callbacks,
+                           const AddRRCallback& add_callback,
+                           Options options)
+{
+    if (add_callback.empty()) {
+        isc_throw(isc::InvalidParameter, "Empty add RR callback");
+    }
+    impl_ = new MasterLoaderImpl(master_file, zone_origin,
+                                 zone_class, callbacks, add_callback, options);
+}
+
+MasterLoader::MasterLoader(std::istream& stream,
+                           const Name& zone_origin,
+                           const RRClass& zone_class,
+                           const MasterLoaderCallbacks& callbacks,
+                           const AddRRCallback& add_callback,
+                           Options options)
+{
+    if (add_callback.empty()) {
+        isc_throw(isc::InvalidParameter, "Empty add RR callback");
+    }
+    auto_ptr<MasterLoaderImpl> impl(new MasterLoaderImpl("", zone_origin,
+                                                         zone_class, callbacks,
+                                                         add_callback,
+                                                         options));
+    impl->pushStreamSource(stream);
+    impl_ = impl.release();
+}
+
+MasterLoader::~MasterLoader() {
+    delete impl_;
+}
+
+bool
+MasterLoader::loadIncremental(size_t count_limit) {
+    const bool result = impl_->loadIncremental(count_limit);
+    impl_->complete_ = result;
+    return (result);
+}
+
+bool
+MasterLoader::loadedSucessfully() const {
+    return (impl_->complete_ && !impl_->seen_error_);
+}
+
+} // end namespace dns
+} // end namespace isc

+ 126 - 7
src/lib/dns/master_loader.h

@@ -15,20 +15,139 @@
 #ifndef MASTER_LOADER_H
 #define MASTER_LOADER_H
 
+#include <dns/master_loader_callbacks.h>
+
+#include <boost/noncopyable.hpp>
+
 namespace isc {
 namespace dns {
 
-// Placeholder introduced by #2497. The real class should be updated in
-// #2377.
-class MasterLoader {
+class Name;
+class RRClass;
+
+/// \brief Error while loading by MasterLoader without specifying the
+///     MANY_ERRORS option.
+class MasterLoaderError : public isc::Exception {
 public:
+    MasterLoaderError(const char* file, size_t line, const char* what) :
+        Exception(file, line, what)
+    {}
+};
+
+/// \brief A class able to load DNS master files
+///
+/// This class is able to produce a stream of RRs from a master file.
+/// It is able to load all of the master file at once, or by blocks
+/// incrementally.
+///
+/// It reports the loaded RRs and encountered errors by callbacks.
+class MasterLoader : boost::noncopyable {
+public:
+    /// \brief Options how the parsing should work.
     enum Options {
-         MANY_ERRORS, // lenient mode
-         // also eventually some check policies like "check NS name"
+        DEFAULT = 0,       ///< Nothing special.
+        MANY_ERRORS = 1    ///< Lenient mode (see documentation of MasterLoader
+                           ///  constructor).
     };
+
+    /// \brief Constructor
+    ///
+    /// This creates a master loader and provides it with all
+    /// relevant information.
+    ///
+    /// Except for the exceptions listed below, the constructor doesn't
+    /// throw. Most errors (like non-existent master file) are reported
+    /// by the callbacks during load() or loadIncremental().
+    ///
+    /// \param master_file Path to the file to load.
+    /// \param zone_origin The origin of zone to be expected inside
+    ///     the master file. Currently unused, but it is expected to
+    ///     be used for some validation.
+    /// \param zone_class The class of zone to be expected inside the
+    ///     master file.
+    /// \param callbacks The callbacks by which it should report problems.
+    ///     Usually, the callback carries a filename and line number of the
+    ///     input where the problem happens. There's a special case of empty
+    ///     filename and zero line in case the opening of the top-level master
+    ///     file fails.
+    /// \param add_callback The callback which would be called with each
+    ///     loaded RR.
+    /// \param options Options for the parsing, which is bitwise-or of
+    ///     the Options values or DEFAULT. If the MANY_ERRORS option is
+    ///     included, the parser tries to continue past errors. If it
+    ///     is not included, it stops at first encountered error.
+    /// \throw std::bad_alloc when there's not enough memory.
+    /// \throw isc::InvalidParameter if add_callback is empty.
+    MasterLoader(const char* master_file,
+                 const Name& zone_origin,
+                 const RRClass& zone_class,
+                 const MasterLoaderCallbacks& callbacks,
+                 const AddRRCallback& add_callback,
+                 Options options = DEFAULT);
+
+    /// \brief Constructor from a stream
+    ///
+    /// This is a constructor very similar to the previous one. The only
+    /// difference is it doesn't take a filename, but an input stream
+    /// to read the data from. It is expected to be mostly used in tests,
+    /// but it is public as it may possibly be useful for other currently
+    /// unknown purposes.
+    MasterLoader(std::istream& input,
+                 const Name& zone_origin,
+                 const RRClass& zone_class,
+                 const MasterLoaderCallbacks& callbacks,
+                 const AddRRCallback& add_callback,
+                 Options options = DEFAULT);
+
+    /// \brief Destructor
+    ~MasterLoader();
+
+    /// \brief Load some RRs
+    ///
+    /// This method loads at most count_limit RRs and reports them. In case
+    /// an error (either fatal or without MANY_ERRORS) or end of file is
+    /// encountered, they may be less.
+    ///
+    /// \param count_limit Upper limit on the number of RRs loaded.
+    /// \return In case it stops because of the count limit, it returns false.
+    ///     It returns true if the loading is done.
+    /// \throw isc::InvalidOperation when called after loading was done
+    ///     already.
+    /// \throw MasterLoaderError when there's an error in the input master
+    ///     file and the MANY_ERRORS is not specified. It never throws this
+    ///     in case MANY_ERRORS is specified.
+    bool loadIncremental(size_t count_limit);
+
+    /// \brief Load everything
+    ///
+    /// This simply calls loadIncremental until the loading is done.
+    /// \throw isc::InvalidOperation when called after loading was done
+    ///     already.
+    /// \throw MasterLoaderError when there's an error in the input master
+    ///     file and the MANY_ERRORS is not specified. It never throws this
+    ///     in case MANY_ERRORS is specified.
+    void load() {
+        while (!loadIncremental(1000)) { // 1000 = arbitrary largish number
+            // Body intentionally left blank
+        }
+    }
+
+    /// \brief Was the loading successful?
+    ///
+    /// \return true if and only if the loading was complete (after a call of
+    ///     load or after loadIncremental returned true) and there was no
+    ///     error. In other cases, return false.
+    /// \note While this method works even before the loading is complete (by
+    ///     returning false in that case), it is meant to be called only after
+    ///     finishing the load.
+    bool loadedSucessfully() const;
+
+private:
+    class MasterLoaderImpl;
+    MasterLoaderImpl* impl_;
 };
 
-}
-}
+} // end namespace dns
+} // end namespace isc
 
 #endif // MASTER_LOADER_H

+ 19 - 9
src/lib/dns/master_loader_callbacks.h

@@ -23,20 +23,30 @@
 
 namespace isc {
 namespace dns {
+class Name;
+class RRClass;
+class RRType;
+class RRTTL;
+namespace rdata {
+class Rdata;
+typedef boost::shared_ptr<Rdata> RdataPtr;
+}
 
-class AbstractRRset;
-typedef boost::shared_ptr<AbstractRRset> RRsetPtr;
-
-/// \brief Type of callback to add a RRset.
+/// \brief Type of callback to add a RR.
 ///
 /// This type of callback is used by the loader to report another loaded
-/// RRset. The RRset is no longer preserved by the loader and is fully
+/// RR. The Rdata is no longer preserved by the loader and is fully
 /// owned by the callback.
 ///
-/// \param RRset The rrset to add. It does not contain the accompanying
-///     RRSIG (if the zone is signed), they are reported with separate
-///     calls to the callback.
-typedef boost::function<void(const RRsetPtr& rrset)> AddRRsetCallback;
+/// \param name The domain name where the RR belongs.
+/// \param rrclass The class of the RR.
+/// \param rrtype Type of the RR.
+/// \param rrttl Time to live of the RR.
+/// \param rdata The actual carried data of the RR.
+typedef boost::function<void(const Name& name, const RRClass& rrclass,
+                             const RRType& rrtype, const RRTTL& rrttl,
+                             const rdata::RdataPtr& rdata)>
+    AddRRCallback;
 
 /// \brief Set of issue callbacks for a loader.
 ///

+ 0 - 3
src/lib/dns/rrset.h

@@ -770,9 +770,6 @@ public:
     //@{
     /// \brief Return pointer to this RRset's RRSIG RRset
     ///
-    /// \exception NotImplemented Always thrown.  Associated RRSIG RRsets are
-    ///            not supported in this class.
-    ///
     /// \return Null pointer, as this class does not support RRSIG records.
     virtual RRsetPtr getRRsig() const {
         return (RRsetPtr());

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

@@ -27,6 +27,7 @@ run_unittests_SOURCES += labelsequence_unittest.cc
 run_unittests_SOURCES += messagerenderer_unittest.cc
 run_unittests_SOURCES += master_lexer_token_unittest.cc
 run_unittests_SOURCES += master_lexer_unittest.cc
+run_unittests_SOURCES += master_loader_unittest.cc
 run_unittests_SOURCES += master_lexer_state_unittest.cc
 run_unittests_SOURCES += name_unittest.cc
 run_unittests_SOURCES += nsec3hash_unittest.cc

+ 341 - 0
src/lib/dns/tests/master_loader_unittest.cc

@@ -0,0 +1,341 @@
+// Copyright (C) 2012  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 <dns/master_loader_callbacks.h>
+#include <dns/master_loader.h>
+#include <dns/rrtype.h>
+#include <dns/rrset.h>
+#include <dns/rrclass.h>
+#include <dns/name.h>
+#include <dns/rdata.h>
+
+#include <gtest/gtest.h>
+#include <boost/bind.hpp>
+#include <boost/scoped_ptr.hpp>
+
+#include <string>
+#include <vector>
+#include <list>
+#include <sstream>
+
+using namespace isc::dns;
+using std::vector;
+using std::string;
+using std::list;
+using std::stringstream;
+using std::endl;
+
+namespace {
+class MasterLoaderTest : public ::testing::Test {
+public:
+    MasterLoaderTest() :
+        callbacks_(boost::bind(&MasterLoaderTest::callback, this,
+                               &errors_, _1, _2, _3),
+                   boost::bind(&MasterLoaderTest::callback, this,
+                               &warnings_, _1, _2, _3))
+    {}
+
+    /// Concatenate file, line, and reason, and add it to either errors
+    /// or warnings
+    void callback(vector<string>* target, const std::string& file, size_t line,
+                  const std::string& reason)
+    {
+        std::stringstream ss;
+        ss << reason << " [" << file << ":" << line << "]";
+        target->push_back(ss.str());
+    }
+
+    void addRRset(const Name& name, const RRClass& rrclass,
+                  const RRType& rrtype, const RRTTL& rrttl,
+                  const rdata::RdataPtr& data) {
+        const RRsetPtr rrset(new BasicRRset(name, rrclass, rrtype, rrttl));
+        rrset->addRdata(data);
+        rrsets_.push_back(rrset);
+    }
+
+    void setLoader(const char* file, const Name& origin,
+                   const RRClass& rrclass, const MasterLoader::Options options)
+    {
+        loader_.reset(new MasterLoader(file, origin, rrclass, callbacks_,
+                                       boost::bind(&MasterLoaderTest::addRRset,
+                                                   this, _1, _2, _3, _4, _5),
+                                       options));
+    }
+
+    void setLoader(std::istream& stream, const Name& origin,
+                   const RRClass& rrclass, const MasterLoader::Options options)
+    {
+        loader_.reset(new MasterLoader(stream, origin, rrclass, callbacks_,
+                                       boost::bind(&MasterLoaderTest::addRRset,
+                                                   this, _1, _2, _3, _4, _5),
+                                       options));
+    }
+
+    static string prepareZone(const string& line, bool include_last) {
+        string result;
+        result += "example.org. 3600 IN SOA ns1.example.org. "
+            "admin.example.org. 1234 3600 1800 2419200 7200\n";
+        result += line;
+        if (include_last) {
+            result += "\n";
+            result += "correct 3600    IN  A 192.0.2.2\n";
+        }
+        return (result);
+    }
+
+    void clear() {
+        warnings_.clear();
+        errors_.clear();
+        rrsets_.clear();
+    }
+
+    // Check the next RR in the ones produced by the loader
+    // Other than passed arguments are checked to be the default for the tests
+    void checkRR(const string& name, const RRType& type, const string& data) {
+        ASSERT_FALSE(rrsets_.empty());
+        RRsetPtr current = rrsets_.front();
+        rrsets_.pop_front();
+
+        EXPECT_EQ(Name(name), current->getName());
+        EXPECT_EQ(type, current->getType());
+        EXPECT_EQ(RRClass::IN(), current->getClass());
+        ASSERT_EQ(1, current->getRdataCount());
+        EXPECT_EQ(0, isc::dns::rdata::createRdata(type, RRClass::IN(), data)->
+                  compare(current->getRdataIterator()->getCurrent()));
+    }
+
+    MasterLoaderCallbacks callbacks_;
+    boost::scoped_ptr<MasterLoader> loader_;
+    vector<string> errors_;
+    vector<string> warnings_;
+    list<RRsetPtr> rrsets_;
+};
+
+// Test simple loading. The zone file contains no tricky things, and nothing is
+// omitted. No RRset contains more than one RR Also no errors or warnings.
+TEST_F(MasterLoaderTest, basicLoad) {
+    setLoader(TEST_DATA_SRCDIR "/example.org", Name("example.org."),
+              RRClass::IN(), MasterLoader::MANY_ERRORS);
+
+    EXPECT_FALSE(loader_->loadedSucessfully());
+    loader_->load();
+    EXPECT_TRUE(loader_->loadedSucessfully());
+
+    EXPECT_TRUE(errors_.empty());
+    EXPECT_TRUE(warnings_.empty());
+
+    checkRR("example.org", RRType::SOA(),
+            "ns1.example.org. admin.example.org. "
+            "1234 3600 1800 2419200 7200");
+    checkRR("example.org", RRType::NS(), "ns1.example.org.");
+    checkRR("www.example.org", RRType::A(), "192.0.2.1");
+}
+
+// Check it works the same when created based on a stream, not filename
+TEST_F(MasterLoaderTest, streamConstructor) {
+    stringstream zone_stream(prepareZone("", true));
+    setLoader(zone_stream, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    EXPECT_FALSE(loader_->loadedSucessfully());
+    loader_->load();
+    EXPECT_TRUE(loader_->loadedSucessfully());
+
+    EXPECT_TRUE(errors_.empty());
+    EXPECT_TRUE(warnings_.empty());
+    checkRR("example.org", RRType::SOA(), "ns1.example.org. "
+            "admin.example.org. 1234 3600 1800 2419200 7200");
+    checkRR("correct.example.org", RRType::A(), "192.0.2.2");
+}
+
+// Try loading data incrementally.
+TEST_F(MasterLoaderTest, incrementalLoad) {
+    setLoader(TEST_DATA_SRCDIR "/example.org", Name("example.org."),
+              RRClass::IN(), MasterLoader::MANY_ERRORS);
+
+    EXPECT_FALSE(loader_->loadedSucessfully());
+    EXPECT_FALSE(loader_->loadIncremental(2));
+    EXPECT_FALSE(loader_->loadedSucessfully());
+
+    EXPECT_TRUE(errors_.empty());
+    EXPECT_TRUE(warnings_.empty());
+
+    checkRR("example.org", RRType::SOA(),
+            "ns1.example.org. admin.example.org. "
+            "1234 3600 1800 2419200 7200");
+    checkRR("example.org", RRType::NS(), "ns1.example.org.");
+
+    // The third one is not loaded yet
+    EXPECT_TRUE(rrsets_.empty());
+
+    // Load the rest.
+    EXPECT_TRUE(loader_->loadIncremental(20));
+    EXPECT_TRUE(loader_->loadedSucessfully());
+
+    EXPECT_TRUE(errors_.empty());
+    EXPECT_TRUE(warnings_.empty());
+
+    checkRR("www.example.org", RRType::A(), "192.0.2.1");
+}
+
+// Try loading from file that doesn't exist. There should be single error
+// saying so.
+TEST_F(MasterLoaderTest, invalidFile) {
+    setLoader("This file doesn't exist at all",
+              Name("exmaple.org."), RRClass::IN(), MasterLoader::MANY_ERRORS);
+
+    // Nothing yet. The loader is dormant until invoked.
+    // Is it really what we want?
+    EXPECT_TRUE(errors_.empty());
+
+    loader_->load();
+
+    EXPECT_TRUE(warnings_.empty());
+    EXPECT_TRUE(rrsets_.empty());
+    ASSERT_EQ(1, errors_.size());
+    EXPECT_EQ(0, errors_[0].find("Error opening the input source file: ")) <<
+        "Different error: " << errors_[0];
+}
+
+struct ErrorCase {
+    const char* const line;    // The broken line in master file
+    const char* const problem; // Description of the problem for SCOPED_TRACE
+} const error_cases[] = {
+    { "www...   3600    IN  A   192.0.2.1", "Invalid name" },
+    { "www      FORTNIGHT   IN  A   192.0.2.1", "Invalid TTL" },
+    { "www      3600    XX  A   192.0.2.1", "Invalid class" },
+    { "www      3600    IN  A   bad_ip", "Invalid Rdata" },
+    { "www      3600    IN", "Unexpected EOLN" },
+    { "www      3600    CH  TXT nothing", "Class mismatch" },
+    { "www      \"3600\"  IN  A   192.0.2.1", "Quoted TTL" },
+    { "www      3600    \"IN\"  A   192.0.2.1", "Quoted class" },
+    { "www      3600    IN  \"A\"   192.0.2.1", "Quoted type" },
+    { "unbalanced)paren 3600    IN  A   192.0.2.1", "Token error 1" },
+    { "www  3600    unbalanced)paren    A   192.0.2.1", "Token error 2" },
+    { NULL, NULL }
+};
+
+// Test a broken zone is handled properly. We test several problems,
+// both in strict and lenient mode.
+TEST_F(MasterLoaderTest, brokenZone) {
+    for (const ErrorCase* ec = error_cases; ec->line != NULL; ++ec) {
+        SCOPED_TRACE(ec->problem);
+        const string zone(prepareZone(ec->line, true));
+
+        {
+            SCOPED_TRACE("Strict mode");
+            clear();
+            stringstream zone_stream(zone);
+            setLoader(zone_stream, Name("example.org."), RRClass::IN(),
+                      MasterLoader::DEFAULT);
+            EXPECT_FALSE(loader_->loadedSucessfully());
+            EXPECT_THROW(loader_->load(), MasterLoaderError);
+            EXPECT_FALSE(loader_->loadedSucessfully());
+            EXPECT_EQ(1, errors_.size()) << errors_[0];
+            EXPECT_TRUE(warnings_.empty());
+
+            checkRR("example.org", RRType::SOA(), "ns1.example.org. "
+                    "admin.example.org. 1234 3600 1800 2419200 7200");
+            // In the strict mode, it is aborted. The last RR is not
+            // even attempted.
+            EXPECT_TRUE(rrsets_.empty());
+        }
+
+        {
+            SCOPED_TRACE("Lenient mode");
+            clear();
+            stringstream zone_stream(zone);
+            setLoader(zone_stream, Name("example.org."), RRClass::IN(),
+                      MasterLoader::MANY_ERRORS);
+            EXPECT_FALSE(loader_->loadedSucessfully());
+            EXPECT_NO_THROW(loader_->load());
+            EXPECT_FALSE(loader_->loadedSucessfully());
+            EXPECT_EQ(1, errors_.size());
+            EXPECT_TRUE(warnings_.empty());
+            checkRR("example.org", RRType::SOA(), "ns1.example.org. "
+                    "admin.example.org. 1234 3600 1800 2419200 7200");
+            // This one is below the error one.
+            checkRR("correct.example.org", RRType::A(), "192.0.2.2");
+            EXPECT_TRUE(rrsets_.empty());
+        }
+
+        {
+            SCOPED_TRACE("Error at EOF");
+            // This case is interesting only in the lenient mode.
+            const string zoneEOF(prepareZone(ec->line, false));
+            clear();
+            stringstream zone_stream(zoneEOF);
+            setLoader(zone_stream, Name("example.org."), RRClass::IN(),
+                      MasterLoader::MANY_ERRORS);
+            EXPECT_FALSE(loader_->loadedSucessfully());
+            EXPECT_NO_THROW(loader_->load());
+            EXPECT_FALSE(loader_->loadedSucessfully());
+            EXPECT_EQ(1, errors_.size());
+            // The unexpected EOF warning
+            EXPECT_EQ(1, warnings_.size());
+            checkRR("example.org", RRType::SOA(), "ns1.example.org. "
+                    "admin.example.org. 1234 3600 1800 2419200 7200");
+            EXPECT_TRUE(rrsets_.empty());
+        }
+    }
+}
+
+// Test the constructor rejects empty add callback.
+TEST_F(MasterLoaderTest, emptyCallback) {
+    EXPECT_THROW(MasterLoader(TEST_DATA_SRCDIR "/example.org",
+                              Name("example.org"), RRClass::IN(), callbacks_,
+                              AddRRCallback()), isc::InvalidParameter);
+    // And the same with the second constructor
+    stringstream ss("");
+    EXPECT_THROW(MasterLoader(ss, Name("example.org"), RRClass::IN(),
+                              callbacks_, AddRRCallback()),
+                 isc::InvalidParameter);
+}
+
+// Check it throws when we try to load after loading was complete.
+TEST_F(MasterLoaderTest, loadTwice) {
+    setLoader(TEST_DATA_SRCDIR "/example.org", Name("example.org."),
+              RRClass::IN(), MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_THROW(loader_->load(), isc::InvalidOperation);
+}
+
+// Load 0 items should be rejected
+TEST_F(MasterLoaderTest, loadZero) {
+    setLoader(TEST_DATA_SRCDIR "/example.org", Name("example.org."),
+              RRClass::IN(), MasterLoader::MANY_ERRORS);
+    EXPECT_THROW(loader_->loadIncremental(0), isc::InvalidParameter);
+}
+
+// Test there's a warning when the file terminates without end of
+// line.
+TEST_F(MasterLoaderTest, noEOLN) {
+    // No \n at the end
+    const string input("example.org. 3600 IN SOA ns1.example.org. "
+                       "admin.example.org. 1234 3600 1800 2419200 7200");
+    stringstream ss(input);
+    setLoader(ss, Name("example.org."), RRClass::IN(),
+              MasterLoader::MANY_ERRORS);
+
+    loader_->load();
+    EXPECT_TRUE(loader_->loadedSucessfully());
+    EXPECT_TRUE(errors_.empty()) << errors_[0];
+    // There should be one warning about the EOLN
+    EXPECT_EQ(1, warnings_.size());
+    checkRR("example.org", RRType::SOA(), "ns1.example.org. "
+            "admin.example.org. 1234 3600 1800 2419200 7200");
+}
+
+}

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

@@ -170,6 +170,7 @@ EXTRA_DIST += tsig_verify1.spec tsig_verify2.spec tsig_verify3.spec
 EXTRA_DIST += tsig_verify4.spec tsig_verify5.spec tsig_verify6.spec
 EXTRA_DIST += tsig_verify7.spec tsig_verify8.spec tsig_verify9.spec
 EXTRA_DIST += tsig_verify10.spec
+EXTRA_DIST += example.org
 
 .spec.wire:
 	$(PYTHON) $(top_builddir)/src/lib/util/python/gen_wiredata.py -o $@ $<

+ 15 - 0
src/lib/dns/tests/testdata/example.org

@@ -0,0 +1,15 @@
+example.org.        3600    IN  SOA ( ; The SOA, split across lines for testing
+    ns1.example.org.
+    admin.example.org.
+    1234
+    3600
+    1800
+    2419200
+    7200
+    )
+; Check it accepts quoted name too
+"\101xample.org."        3600    IN  NS ns1.example.org.
+
+
+; Some empty lines here. They are to make sure the loader can skip them.
+www                 3600    IN  A 192.0.2.1 ; Test a relative name as well.