Parcourir la source

[2429] Merge branch 'trac2377' into trac2429

JINMEI Tatuya il y a 12 ans
Parent
commit
3c779a4236

+ 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.