Browse Source

[1891] Merge branch 'trac1781' into trac1891

JINMEI Tatuya 13 years ago
parent
commit
d68d574b69

+ 118 - 30
src/lib/datasrc/database.cc

@@ -1415,49 +1415,132 @@ DatabaseUpdater::validateAddOrDelete(const char* const op_str,
     }
 }
 
+// This is a helper class used in adding/deleting RRsets to/from a database.
+// The purpose of this class is to provide conversion interface from various
+// parameters of the RRset to corresponding textual representations that the
+// underlying database interface expects.  The necessary parameters and how
+// to convert them depend on several things, such as whether it's NSEC3 related
+// or not, or whether journaling is requested.  In order to avoid unnecessary
+// conversion, this class also performs the conversion in a lazy manner.
+// Also, in order to avoid redundant conversion when the conversion is
+// requested for the same parameter multiple times, it remembers the
+// conversion result first time, and reuses it for subsequent requests
+// (this implicitly assumes copying std::string objects is not very expensive;
+// this is often the case in some common implementations that have
+// copy-on-write semantics for the string class).
+class RRParameterConverter {
+public:
+    RRParameterConverter(const AbstractRRset& rrset) : rrset_(rrset)
+    {}
+    const string& getName() {
+        if (name_.empty()) {
+            name_ = rrset_.getName().toText();
+        }
+        return (name_);
+    }
+    const string& getNSEC3Name() {
+        if (nsec3_name_.empty()) {
+            nsec3_name_ = rrset_.getName().split(0, 1).toText(true);
+        }
+        return (nsec3_name_);
+    }
+    const string& getRevName() {
+        if (revname_.empty()) {
+            revname_ = rrset_.getName().reverse().toText();
+        }
+        return (revname_);
+    }
+    const string& getTTL() {
+        if (ttl_.empty()) {
+            ttl_ = rrset_.getTTL().toText();
+        }
+        return (ttl_);
+    }
+    const string& getType() {
+        if (type_.empty()) {
+            type_ = rrset_.getType().toText();
+        }
+        return (type_);
+    }
+
+private:
+    string name_;
+    string nsec3_name_;
+    string revname_;
+    string ttl_;
+    string type_;
+    const AbstractRRset& rrset_;
+};
+
+namespace {
+// A shared shortcut to detect if the given type of RDATA is NSEC3 or
+// RRSIG covering NSEC3.  RRSIG for NSEC3 should go to the (conceptual)
+// separate namespace, so we need to check the covered type.
+// Note: in principle the type covered should be the same for
+// all RDATA, but the RRset interface doesn't ensure that condition.
+// So we explicitly check that for every RDATA below.
+bool
+isNSEC3KindType(RRType rrtype, const Rdata& rdata) {
+    if (rrtype == RRType::NSEC3()) {
+        return (true);
+    }
+    if (rrtype == RRType::RRSIG() &&
+        dynamic_cast<const generic::RRSIG&>(rdata).typeCovered() ==
+        RRType::NSEC3())
+    {
+        return (true);
+    }
+    return (false);
+}
+}
+
 void
 DatabaseUpdater::addRRset(const AbstractRRset& rrset) {
     validateAddOrDelete("add", rrset, DELETE, ADD);
 
     // It's guaranteed rrset has at least one RDATA at this point.
     RdataIteratorPtr it = rrset.getRdataIterator();
-
-    string columns[Accessor::ADD_COLUMN_COUNT]; // initialized with ""
-    columns[Accessor::ADD_NAME] = rrset.getName().toText();
-    columns[Accessor::ADD_REV_NAME] = rrset.getName().reverse().toText();
-    columns[Accessor::ADD_TTL] = rrset.getTTL().toText();
-    columns[Accessor::ADD_TYPE] = rrset.getType().toText();
-    string journal[Accessor::DIFF_PARAM_COUNT];
     if (journaling_) {
-        journal[Accessor::DIFF_NAME] = columns[Accessor::ADD_NAME];
-        journal[Accessor::DIFF_TYPE] = columns[Accessor::ADD_TYPE];
-        journal[Accessor::DIFF_TTL] = columns[Accessor::ADD_TTL];
         diff_phase_ = ADD;
         if (rrset.getType() == RRType::SOA()) {
-            serial_ =
-                dynamic_cast<const generic::SOA&>(it->getCurrent()).
+            serial_ = dynamic_cast<const generic::SOA&>(it->getCurrent()).
                 getSerial();
         }
     }
+
+    RRParameterConverter cvtr(rrset);
     for (; !it->isLast(); it->next()) {
+        const Rdata& rdata = it->getCurrent();
+        const bool nsec3_type = isNSEC3KindType(rrset.getType(), rdata);
+
+        string sigtype;
         if (rrset.getType() == RRType::RRSIG()) {
             // XXX: the current interface (based on the current sqlite3
             // data source schema) requires a separate "sigtype" column,
             // even though it won't be used in a newer implementation.
             // We should eventually clean up the schema design and simplify
             // the interface, but until then we have to conform to the schema.
-            const generic::RRSIG& rrsig_rdata =
-                dynamic_cast<const generic::RRSIG&>(it->getCurrent());
-            columns[Accessor::ADD_SIGTYPE] =
-                rrsig_rdata.typeCovered().toText();
+            sigtype = dynamic_cast<const generic::RRSIG&>(rdata).
+                typeCovered().toText();
         }
-        columns[Accessor::ADD_RDATA] = it->getCurrent().toText();
+        const string& rdata_txt = rdata.toText();
         if (journaling_) {
-            journal[Accessor::DIFF_RDATA] = columns[Accessor::ADD_RDATA];
+            const string journal[Accessor::DIFF_PARAM_COUNT] =
+                { cvtr.getName(), cvtr.getType(), cvtr.getTTL(), rdata_txt };
             accessor_->addRecordDiff(zone_id_, serial_.getValue(),
                                      Accessor::DIFF_ADD, journal);
         }
-        accessor_->addRecordToZone(columns);
+        if (nsec3_type) {
+            const string nsec3_columns[Accessor::ADD_NSEC3_COLUMN_COUNT] =
+                { cvtr.getNSEC3Name(), cvtr.getTTL(), cvtr.getType(),
+                  rdata_txt };
+            accessor_->addNSEC3RecordToZone(nsec3_columns);
+        } else {
+            const string columns[Accessor::ADD_COLUMN_COUNT] =
+                { cvtr.getName(), cvtr.getRevName(), cvtr.getTTL(),
+                  cvtr.getType(), sigtype, rdata_txt };
+            accessor_->addRecordToZone(columns);
+        }
     }
 }
 
@@ -1472,15 +1555,7 @@ DatabaseUpdater::deleteRRset(const AbstractRRset& rrset) {
     validateAddOrDelete("delete", rrset, ADD, DELETE);
 
     RdataIteratorPtr it = rrset.getRdataIterator();
-
-    string params[Accessor::DEL_PARAM_COUNT]; // initialized with ""
-    params[Accessor::DEL_NAME] = rrset.getName().toText();
-    params[Accessor::DEL_TYPE] = rrset.getType().toText();
-    string journal[Accessor::DIFF_PARAM_COUNT];
     if (journaling_) {
-        journal[Accessor::DIFF_NAME] = params[Accessor::DEL_NAME];
-        journal[Accessor::DIFF_TYPE] = params[Accessor::DEL_TYPE];
-        journal[Accessor::DIFF_TTL] = rrset.getTTL().toText();
         diff_phase_ = DELETE;
         if (rrset.getType() == RRType::SOA()) {
             serial_ =
@@ -1488,14 +1563,27 @@ DatabaseUpdater::deleteRRset(const AbstractRRset& rrset) {
                 getSerial();
         }
     }
+
+    RRParameterConverter cvtr(rrset);
     for (; !it->isLast(); it->next()) {
-        params[Accessor::DEL_RDATA] = it->getCurrent().toText();
+        const Rdata& rdata = it->getCurrent();
+        const bool nsec3_type = isNSEC3KindType(rrset.getType(), rdata);
+        const string& rdata_txt = it->getCurrent().toText();
+
         if (journaling_) {
-            journal[Accessor::DIFF_RDATA] = params[Accessor::DEL_RDATA];
+            const string journal[Accessor::DIFF_PARAM_COUNT] =
+                { cvtr.getName(), cvtr.getType(), cvtr.getTTL(), rdata_txt };
             accessor_->addRecordDiff(zone_id_, serial_.getValue(),
                                      Accessor::DIFF_DELETE, journal);
         }
-        accessor_->deleteRecordInZone(params);
+        const string params[Accessor::DEL_PARAM_COUNT] =
+            { nsec3_type ? cvtr.getNSEC3Name() : cvtr.getName(),
+              cvtr.getType(), rdata_txt };
+        if (nsec3_type) {
+            accessor_->deleteNSEC3RecordInZone(params);
+        } else {
+            accessor_->deleteRecordInZone(params);
+        }
     }
 }
 

+ 88 - 0
src/lib/datasrc/database.h

@@ -95,13 +95,36 @@ public:
         ADD_COLUMN_COUNT = 6 ///< Number of columns
     };
 
+    /// \brief Definitions of the fields to be passed to addNSEC3RecordToZone()
+    ///
+    /// Each derived implementation of addNSEC3RecordToZone() should expect
+    /// the "columns" array to be filled with the values as described in this
+    /// enumeration, in this order.
+    ///
+    /// Note that there is no "reversed name" column.  Since the conceptual
+    /// separate namespace for NSEC3 is very simplified and essentially only
+    /// consists of a single-label names, there is no need for using reversed
+    /// names to identify the "previous hash".
+    enum AddNSEC3RecordColumns {
+        ADD_NSEC3_HASH = 0, ///< The hash (1st) label of the owner name,
+                            ///< excluding the dot character
+        ADD_NSEC3_TTL = 1,  ///< The TTL of the record (in numeric form)
+        ADD_NSEC3_TYPE = 2, ///< The RRType of the record (either NSEC3 or
+                            ///< RRSIG for NSEC3)
+        ADD_NSEC3_RDATA = 3, ///< Full text representation of the record's
+                             ///< RDATA
+        ADD_NSEC3_COLUMN_COUNT = 4 ///< Number of columns
+    };
+
     /// \brief Definitions of the fields to be passed to deleteRecordInZone()
+    /// and deleteNSEC3RecordInZone()
     ///
     /// Each derived implementation of deleteRecordInZone() should expect
     /// the "params" array to be filled with the values as described in this
     /// enumeration, in this order.
     enum DeleteRecordParams {
         DEL_NAME = 0, ///< The owner name of the record (a domain name)
+                      ///< or the hash label for deleteNSEC3RecordInZone()
         DEL_TYPE = 1, ///< The RRType of the record (A/NS/TXT etc.)
         DEL_RDATA = 2, ///< Full text representation of the record's RDATA
         DEL_PARAM_COUNT = 3 ///< Number of parameters
@@ -432,6 +455,46 @@ public:
     virtual void addRecordToZone(
         const std::string (&columns)[ADD_COLUMN_COUNT]) = 0;
 
+    /// \brief Add a single NSEC3-related record to the zone to be updated.
+    ///
+    /// This method is similar to \c addRecordToZone(), but is expected to
+    /// be only used for NSEC3 RRs or RRSIG RRs that cover NSEC3.  In terms
+    /// of the DNS protocol, these types of RRs reside in a separate space
+    /// of the zone.  While this interface does not mandate a specific way
+    /// of implementing the separate namespaces in the underlying database,
+    /// it would be more convenient for the underlying implementation if the
+    /// interfaces are separated; for example, the implementation does not
+    /// have to examine the given data to identify the appropriate namespace.
+    ///
+    /// An implementation may choose to skip providing this interface if the
+    /// zones managed by that data source are known to not support NSEC3.
+    /// In that case the implementation should throw the
+    /// \c isc::NotImplemented exception.
+    ///
+    /// Note that the \c ADD_NSEC3_HASH column of \c columns is expected to
+    /// store only the hash label, not the entire owner name.  This is similar
+    /// to the \c hash parameter of \c getNSEC3Records().
+    ///
+    /// The RRs to be added using this method are expected to be limited to
+    /// NSEC3 or RRSIG RRs that cover NSEC3, but it's generally assumed to
+    /// be the caller's responsibility to ensure that; the implementation
+    /// is not required to check that condition.  The result of adding
+    /// unexpected type of RRs (and the result of subsequent lookups) is
+    /// undefined.
+    ///
+    /// Other general notes for \c addRecordToZone() also apply to this
+    /// method.
+    ///
+    /// \exception DataSourceError Invalid call without starting a transaction,
+    /// or other internal database error.
+    /// \exception isc::NotImplemented in case the database does not support
+    ///     NSEC3
+    ///
+    /// \param columns An array of strings that defines a record to be added
+    /// to the NSEC3 namespace of the zone.
+    virtual void addNSEC3RecordToZone(
+        const std::string (&columns)[ADD_NSEC3_COLUMN_COUNT]) = 0;
+
     /// \brief Delete a single record from the zone to be updated.
     ///
     /// This method provides a simple interface to delete a record
@@ -469,6 +532,31 @@ public:
     virtual void deleteRecordInZone(
         const std::string (&params)[DEL_PARAM_COUNT]) = 0;
 
+    /// \brief Delete a single NSEC3-related record from the zone to be
+    /// updated.
+    ///
+    /// This method is similar to \c deleteRecordInZone(), but is expected to
+    /// be only used for NSEC3 RRs or RRSIG RRs that cover NSEC3.  The
+    /// relationship between these two methods is similar to that between
+    /// \c addRecordToZone() and \c addNSEC3RecordToZone(), and the same
+    /// notes apply to this method.
+    ///
+    /// This method uses the same set of parameters to specify the record
+    /// to be deleted as \c deleteRecordInZone(), but the \c DEL_NAME column
+    /// is expected to only store the hash label of the owner name.
+    /// This is the same as \c ADD_NSEC3_HASH column for
+    /// \c addNSEC3RecordToZone().
+    ///
+    /// \exception DataSourceError Invalid call without starting a transaction,
+    /// or other internal database error.
+    /// \exception isc::NotImplemented in case the database does not support
+    ///     NSEC3
+    ///
+    /// \param params An array of strings that defines a record to be deleted
+    /// from the NSEC3 namespace of the zone.
+    virtual void deleteNSEC3RecordInZone(
+        const std::string (&params)[DEL_PARAM_COUNT]) = 0;
+
     /// \brief Start a general transaction.
     ///
     /// Each derived class version of this method starts a database

+ 14 - 0
src/lib/datasrc/sqlite3_accessor.cc

@@ -1133,6 +1133,13 @@ SQLite3Accessor::addRecordToZone(const string (&columns)[ADD_COLUMN_COUNT]) {
 }
 
 void
+SQLite3Accessor::addNSEC3RecordToZone(
+    const string (&/*columns*/)[ADD_NSEC3_COLUMN_COUNT])
+{
+    isc_throw(NotImplemented, "not yet implemented");
+}
+
+void
 SQLite3Accessor::deleteRecordInZone(const string (&params)[DEL_PARAM_COUNT]) {
     if (!dbparameters_->updating_zone) {
         isc_throw(DataSourceError, "deleting record in SQLite3 "
@@ -1143,6 +1150,13 @@ SQLite3Accessor::deleteRecordInZone(const string (&params)[DEL_PARAM_COUNT]) {
 }
 
 void
+SQLite3Accessor::deleteNSEC3RecordInZone(
+    const string (&/*params*/)[DEL_PARAM_COUNT])
+{
+    isc_throw(NotImplemented, "not yet implemented");
+}
+
+void
 SQLite3Accessor::addRecordDiff(int zone_id, uint32_t serial,
                                DiffOperation operation,
                                const std::string (&params)[DIFF_PARAM_COUNT])

+ 6 - 0
src/lib/datasrc/sqlite3_accessor.h

@@ -214,9 +214,15 @@ public:
     virtual void addRecordToZone(
         const std::string (&columns)[ADD_COLUMN_COUNT]);
 
+    virtual void addNSEC3RecordToZone(
+        const std::string (&columns)[ADD_NSEC3_COLUMN_COUNT]);
+
     virtual void deleteRecordInZone(
         const std::string (&params)[DEL_PARAM_COUNT]);
 
+    virtual void deleteNSEC3RecordInZone(
+        const std::string (&params)[DEL_PARAM_COUNT]);
+
     /// This derived version of the method prepares an SQLite3 statement
     /// for adding the diff first time it's called, and if it fails throws
     // an \c SQLite3Error exception.

+ 228 - 35
src/lib/datasrc/tests/database_unittest.cc

@@ -14,15 +14,9 @@
 
 #include "faked_nsec3.h"
 
-#include <stdlib.h>
-
-#include <boost/shared_ptr.hpp>
-#include <boost/lexical_cast.hpp>
-
-#include <gtest/gtest.h>
-
 #include <exceptions/exceptions.h>
 
+#include <dns/masterload.h>
 #include <dns/name.h>
 #include <dns/rrttl.h>
 #include <dns/rrset.h>
@@ -37,6 +31,13 @@
 
 #include <testutils/dnsmessage_test.h>
 
+#include <gtest/gtest.h>
+
+#include <boost/bind.hpp>
+#include <boost/shared_ptr.hpp>
+#include <boost/lexical_cast.hpp>
+
+#include <cstdlib>
 #include <map>
 #include <vector>
 
@@ -47,6 +48,7 @@ using namespace std;
 using boost::dynamic_pointer_cast;
 using boost::lexical_cast;
 using namespace isc::dns;
+using namespace isc::testutils;
 using namespace isc::datasrc::test;
 
 namespace {
@@ -261,7 +263,10 @@ public:
     virtual void commit() {}
     virtual void rollback() {}
     virtual void addRecordToZone(const string (&)[ADD_COLUMN_COUNT]) {}
+    virtual void addNSEC3RecordToZone(const string (&)[ADD_NSEC3_COLUMN_COUNT])
+    {}
     virtual void deleteRecordInZone(const string (&)[DEL_PARAM_COUNT]) {}
+    virtual void deleteNSEC3RecordInZone(const string (&)[DEL_PARAM_COUNT]) {}
     virtual void addRecordDiff(int, uint32_t, DiffOperation,
                                const std::string (&)[DIFF_PARAM_COUNT]) {}
 
@@ -374,6 +379,8 @@ public:
     MockAccessor() : rollbacked_(false), did_transaction_(false) {
         readonly_records_ = &readonly_records_master_;
         update_records_ = &update_records_master_;
+        nsec3_namespace_ = &nsec3_namespace_master_;
+        update_nsec3_namespace_ = &update_nsec3_namespace_master_;
         empty_records_ = &empty_records_master_;
         journal_entries_ = &journal_entries_master_;
         fillData();
@@ -383,6 +390,9 @@ public:
         boost::shared_ptr<MockAccessor> cloned_accessor(new MockAccessor());
         cloned_accessor->readonly_records_ = &readonly_records_master_;
         cloned_accessor->update_records_ = &update_records_master_;
+        cloned_accessor->nsec3_namespace_ = &nsec3_namespace_master_;
+        cloned_accessor->update_nsec3_namespace_ =
+            &update_nsec3_namespace_master_;
         cloned_accessor->empty_records_ = &empty_records_master_;
         cloned_accessor->journal_entries_ = &journal_entries_master_;
         latest_clone_ = cloned_accessor;
@@ -649,8 +659,8 @@ public:
     virtual IteratorContextPtr getNSEC3Records(const std::string& hash,
                                                int) const
     {
-        Domains::const_iterator it(nsec3_namespace_.find(hash));
-        if (it == nsec3_namespace_.end()) {
+        Domains::const_iterator it(nsec3_namespace_->find(hash));
+        if (it == nsec3_namespace_->end()) {
             return (IteratorContextPtr(new EmptyIteratorContext()));
         } else {
             return (IteratorContextPtr(new DomainIterator(it->second)));
@@ -670,8 +680,10 @@ public:
         // original.
         if (replace) {
             update_records_->clear();
+            update_nsec3_namespace_->clear();
         } else {
             *update_records_ = *readonly_records_;
+            *update_nsec3_namespace_ = nsec3_namespace_master_;
         }
 
         if (zone_name == "bad.example.org.") {
@@ -684,7 +696,9 @@ public:
     }
     virtual void commit() {
         *readonly_records_ = *update_records_;
+        *nsec3_namespace_ = *update_nsec3_namespace_;
     }
+
     virtual void rollback() {
         // Special hook: if something with a name of "throw.example.org"
         // has been added, trigger an imaginary unexpected event with an
@@ -695,27 +709,54 @@ public:
 
         rollbacked_ = true;
     }
-    virtual void addRecordToZone(const string (&columns)[ADD_COLUMN_COUNT]) {
+
+private:
+    // Common subroutine for addRecordToZone and addNSEC3RecordToZone.
+    void addRecord(Domains& domains,
+                   const string (&columns)[ADD_COLUMN_COUNT])
+    {
         // Copy the current value to cur_name.  If it doesn't exist,
         // operator[] will create a new one.
-        cur_name_ = (*update_records_)[columns[DatabaseAccessor::ADD_NAME]];
+        cur_name_ = domains[columns[ADD_NAME]];
 
         vector<string> record_columns;
-        record_columns.push_back(columns[DatabaseAccessor::ADD_TYPE]);
-        record_columns.push_back(columns[DatabaseAccessor::ADD_TTL]);
-        record_columns.push_back(columns[DatabaseAccessor::ADD_SIGTYPE]);
-        record_columns.push_back(columns[DatabaseAccessor::ADD_RDATA]);
-        record_columns.push_back(columns[DatabaseAccessor::ADD_NAME]);
+        record_columns.push_back(columns[ADD_TYPE]);
+        record_columns.push_back(columns[ADD_TTL]);
+        record_columns.push_back(columns[ADD_SIGTYPE]);
+        record_columns.push_back(columns[ADD_RDATA]);
+        record_columns.push_back(columns[ADD_NAME]);
 
         // copy back the added entry
         cur_name_.push_back(record_columns);
-        (*update_records_)[columns[DatabaseAccessor::ADD_NAME]] = cur_name_;
+        domains[columns[DatabaseAccessor::ADD_NAME]] = cur_name_;
 
         // remember this one so that test cases can check it.
         copy(columns, columns + DatabaseAccessor::ADD_COLUMN_COUNT,
              columns_lastadded_);
     }
 
+public:
+    virtual void addRecordToZone(const string (&columns)[ADD_COLUMN_COUNT]) {
+        addRecord(*update_records_, columns);
+    }
+
+    virtual void addNSEC3RecordToZone(
+        const string (&columns)[ADD_NSEC3_COLUMN_COUNT])
+    {
+        // Convert the NSEC3 parameters in the normal (non NSEC3) style so
+        // we can share the merge code, and then update using addRecord().
+        string normal_columns[ADD_COLUMN_COUNT];
+
+        normal_columns[ADD_TYPE] = columns[ADD_NSEC3_TYPE];
+        normal_columns[ADD_TTL] = columns[ADD_NSEC3_TTL];
+        normal_columns[ADD_SIGTYPE] = "";
+        normal_columns[ADD_RDATA] = columns[ADD_NSEC3_RDATA];
+        normal_columns[ADD_NAME] = columns[ADD_NSEC3_HASH];
+
+        addRecord(*update_nsec3_namespace_, normal_columns);
+    }
+
+private:
     // Helper predicate class used in deleteRecordInZone().
     struct deleteMatch {
         deleteMatch(const string& type, const string& rdata) :
@@ -728,19 +769,33 @@ public:
         const string& rdata_;
     };
 
-    virtual void deleteRecordInZone(const string (&params)[DEL_PARAM_COUNT]) {
+    // Common subroutine for deleteRecordinZone and deleteNSEC3RecordInZone.
+    void deleteRecord(Domains& domains,
+                      const string (&params)[DEL_PARAM_COUNT])
+    {
         vector<vector<string> >& records =
-            (*update_records_)[params[DatabaseAccessor::DEL_NAME]];
+            domains[params[DatabaseAccessor::DEL_NAME]];
         records.erase(remove_if(records.begin(), records.end(),
                                 deleteMatch(
                                     params[DatabaseAccessor::DEL_TYPE],
                                     params[DatabaseAccessor::DEL_RDATA])),
                       records.end());
         if (records.empty()) {
-            (*update_records_).erase(params[DatabaseAccessor::DEL_NAME]);
+            domains.erase(params[DatabaseAccessor::DEL_NAME]);
         }
     }
 
+public:
+    virtual void deleteRecordInZone(const string (&params)[DEL_PARAM_COUNT]) {
+        deleteRecord(*update_records_, params);
+    }
+
+    virtual void deleteNSEC3RecordInZone(
+        const string (&params)[DEL_PARAM_COUNT])
+    {
+        deleteRecord(*update_nsec3_namespace_, params);
+    }
+
     //
     // Helper methods to keep track of some update related activities
     //
@@ -799,13 +854,13 @@ public:
     {
         // TODO: Provide some broken data, but it is not known yet how broken
         // they'll have to be.
-        Domains::const_iterator it(nsec3_namespace_.lower_bound(hash));
+        Domains::const_iterator it(nsec3_namespace_->lower_bound(hash));
         // We got just after the one we want
-        if (it == nsec3_namespace_.begin()) {
+        if (it == nsec3_namespace_->begin()) {
             // Hmm, we got something really small. So we wrap around.
             // This is one after the last, so after decreasing it we'll get
             // the biggest.
-            it = nsec3_namespace_.end();
+            it = nsec3_namespace_->end();
         }
         return ((--it)->first);
     }
@@ -889,12 +944,13 @@ private:
     Domains* readonly_records_;
     Domains update_records_master_;
     Domains* update_records_;
+    Domains nsec3_namespace_master_;
+    Domains* nsec3_namespace_;
+    Domains update_nsec3_namespace_master_;
+    Domains* update_nsec3_namespace_;
     const Domains empty_records_master_;
     const Domains* empty_records_;
 
-    // The NSEC3 namespace. The above trick will be added once it is needed.
-    Domains nsec3_namespace_;
-
     // The journal data
     std::vector<JournalEntry> journal_entries_master_;
     std::vector<JournalEntry>* journal_entries_;
@@ -959,13 +1015,13 @@ private:
     // the NSEC3 namespace. You don't provide the full name, only
     // the hash part.
     void addCurHash(const std::string& hash) {
-        ASSERT_EQ(0, nsec3_namespace_.count(hash));
+        ASSERT_EQ(0, nsec3_namespace_->count(hash));
         // Append the name to all of them
         for (std::vector<std::vector<std::string> >::iterator
              i = cur_name_.begin(); i != cur_name_.end(); ++ i) {
             i->push_back(hash);
         }
-        nsec3_namespace_[hash] = cur_name_;
+        (*nsec3_namespace_)[hash] = cur_name_;
         cur_name_.clear();
     }
 
@@ -1207,7 +1263,7 @@ public:
                     rdata::createRdata(expected_rrset->getType(),
                                        expected_rrset->getClass(),
                                        (*it).data_[Accessor::DIFF_RDATA]));
-                isc::testutils::rrsetCheck(expected_rrset, rrset);
+                rrsetCheck(expected_rrset, rrset);
             }
             // We should have examined all entries of both expected and
             // actual data.
@@ -1376,7 +1432,7 @@ checkRRset(isc::dns::ConstRRsetPtr rrset,
             isc::dns::rdata::createRdata(rrtype, rrclass,
                                          rdatas[i]));
     }
-    isc::testutils::rrsetCheck(expected_rrset, rrset);
+    rrsetCheck(expected_rrset, rrset);
 }
 
 // Iterate through a zone, common case
@@ -1512,7 +1568,7 @@ TYPED_TEST(DatabaseClientTest, getSOAFromIterator) {
     }
     ASSERT_TRUE(rrset);
     // It should be identical to the result of getSOA().
-    isc::testutils::rrsetCheck(it->getSOA(), rrset);
+    rrsetCheck(it->getSOA(), rrset);
 }
 
 TYPED_TEST(DatabaseClientTest, noSOAFromIterator) {
@@ -1550,7 +1606,7 @@ TYPED_TEST(DatabaseClientTest, iterateThenUpdate) {
     }
     ASSERT_TRUE(rrset);
     // It should be identical to the result of getSOA().
-    isc::testutils::rrsetCheck(it->getSOA(), rrset);
+    rrsetCheck(it->getSOA(), rrset);
 }
 
 TYPED_TEST(DatabaseClientTest, updateThenIterateThenUpdate) {
@@ -2946,6 +3002,97 @@ TYPED_TEST(DatabaseClientTest, addRRsetToNewZone) {
     this->checkLastAdded(rrset_added);
 }
 
+// Below we define a set of NSEC3 update tests.   Right now this only works
+// for the mock DB, but the plan is to make it a TYPED_TEST to share the case
+// with SQLite3 implementation, too.
+
+// Commonly used data for NSEC3 update tests below.
+const char* const nsec3_hash = "1BB7SO0452U1QHL98UISNDD9218GELR5";
+const char* const nsec3_rdata = "1 1 12 AABBCCDD "
+    "2T7B4G4VSA5SMI47K61MV5BV1A22BOJR NS SOA RRSIG NSEC3PARAM";
+const char* const nsec3_rdata2 = "1 1 12 AABBCCDD "
+    "2T7B4G4VSA5SMI47K61MV5BV1A22BOJR NS SOA RRSIG"; // differ in bitmaps
+const char* const nsec3_sig_rdata = "NSEC3 5 3 3600 20000101000000 "
+    "20000201000000 12345 example.org. FAKEFAKEFAKE";
+const char* const nsec3_sig_rdata2 = "NSEC3 5 3 3600 20000101000000 "
+    "20000201000000 12345 example.org. FAKEFAKE"; // differ in the signature
+
+// Commonly used subroutine that checks if we can get the expected record.
+// According to the API, implementations can skip filling in columns other
+// than those explicitly checked below, so we don't check them.
+void
+nsec3Check(const vector<ConstRRsetPtr>& expected_rrsets,
+           const Name& zone_name, const string& expected_hash,
+           DatabaseAccessor& accessor)
+{
+    const int zone_id = accessor.getZone(zone_name.toText()).second;
+    DatabaseAccessor::IteratorContextPtr itctx =
+        accessor.getNSEC3Records(expected_hash, zone_id);
+    ASSERT_TRUE(itctx);
+
+    // Build a list of matched RRsets and compare the both expected and built
+    // ones as sets.
+    string columns[DatabaseAccessor::COLUMN_COUNT];
+    vector<ConstRRsetPtr> actual_rrsets;
+    while (itctx->getNext(columns)) {
+        actual_rrsets.push_back(
+            textToRRset(expected_hash + "." + zone_name.toText() + " " +
+                        columns[DatabaseAccessor::TTL_COLUMN] + " IN " +
+                        columns[DatabaseAccessor::TYPE_COLUMN] + " " +
+                        columns[DatabaseAccessor::RDATA_COLUMN]));
+    }
+    rrsetsCheck(expected_rrsets.begin(), expected_rrsets.end(),
+                actual_rrsets.begin(), actual_rrsets.end());
+}
+
+TEST_F(MockDatabaseClientTest, addDeleteNSEC3InZone) {
+    // Add one NSEC3 RR to the zone, delete it, and add another one.
+    this->updater_ = this->client_->getUpdater(this->zname_, true);
+    const ConstRRsetPtr nsec3_rrset =
+        textToRRset(string(nsec3_hash) + ".example.org. 3600 IN NSEC3 " +
+                    string(nsec3_rdata));
+    const ConstRRsetPtr nsec3_rrset2 =
+        textToRRset(string(nsec3_hash) + ".example.org. 3600 IN NSEC3 " +
+                    string(nsec3_rdata2));
+    this->updater_->addRRset(*nsec3_rrset);
+    this->updater_->deleteRRset(*nsec3_rrset);
+    this->updater_->addRRset(*nsec3_rrset2);
+    this->updater_->commit();
+
+    // Check if we can get the expected record.
+    vector<ConstRRsetPtr> expected_rrsets;
+    expected_rrsets.push_back(nsec3_rrset2);
+    nsec3Check(expected_rrsets, this->zname_, nsec3_hash,
+               *this->current_accessor_);
+}
+
+TEST_F(MockDatabaseClientTest, addDeleteNSEC3AndRRSIGToZone) {
+    // Add one NSEC3 RR and its RRSIG to the zone, delete the RRSIG and add
+    // a new one.
+    this->updater_ = this->client_->getUpdater(this->zname_, true);
+    const ConstRRsetPtr nsec3_rrset =
+        textToRRset(string(nsec3_hash) + ".example.org. 3600 IN NSEC3 " +
+                    string(nsec3_rdata));
+    const ConstRRsetPtr nsec3_sig_rrset =
+        textToRRset(string(nsec3_hash) + ".example.org. 3600 IN RRSIG " +
+                    string(nsec3_sig_rdata));
+    const ConstRRsetPtr nsec3_sig_rrset2 =
+        textToRRset(string(nsec3_hash) + ".example.org. 3600 IN RRSIG " +
+                    string(nsec3_sig_rdata2));
+    this->updater_->addRRset(*nsec3_rrset);
+    this->updater_->addRRset(*nsec3_sig_rrset);
+    this->updater_->deleteRRset(*nsec3_sig_rrset);
+    this->updater_->addRRset(*nsec3_sig_rrset2);
+    this->updater_->commit();
+
+    // Check if we can get the expected record.
+    vector<ConstRRsetPtr> expected_rrsets;
+    expected_rrsets.push_back(nsec3_rrset);
+    expected_rrsets.push_back(nsec3_sig_rrset2);
+    nsec3Check(expected_rrsets, this->zname_, nsec3_hash,
+               *this->current_accessor_);
+}
+
 TYPED_TEST(DatabaseClientTest, addRRsetToCurrentZone) {
     // Similar to the previous test, but not replacing the existing data.
     boost::shared_ptr<DatabaseClient::Finder> finder(this->getFinder());
@@ -3492,6 +3639,52 @@ TYPED_TEST(DatabaseClientTest, journal) {
     this->checkJournal(expected);
 }
 
+// At the moment this only works for the mock accessor.  Once sqlite3
+// accessor supports updating NSEC3, this should be merged to the previous
+// test
+TEST_F(MockDatabaseClientTest, journalForNSEC3) {
+    // Similar to the previous test, but adding/deleting NSEC3 RRs, just to
+    // confirm that NSEC3 is not special for managing diffs.
+    const ConstRRsetPtr nsec3_rrset =
+        textToRRset(string(nsec3_hash) + ".example.org. 3600 IN NSEC3 " +
+                    string(nsec3_rdata));
+
+    this->updater_ = this->client_->getUpdater(this->zname_, false, true);
+    this->updater_->deleteRRset(*this->soa_);
+    this->updater_->deleteRRset(*nsec3_rrset);
+
+    this->soa_.reset(new RRset(this->zname_, this->qclass_, RRType::SOA(),
+                               this->rrttl_));
+    this->soa_->addRdata(rdata::createRdata(this->soa_->getType(),
+                                            this->soa_->getClass(),
+                                            "ns1.example.org. "
+                                            "admin.example.org. "
+                                            "1235 3600 1800 2419200 7200"));
+    this->updater_->addRRset(*this->soa_);
+    this->updater_->addRRset(*nsec3_rrset);
+    this->updater_->commit();
+    std::vector<JournalEntry> expected;
+    expected.push_back(JournalEntry(WRITABLE_ZONE_ID, 1234,
+                                    DatabaseAccessor::DIFF_DELETE,
+                                    "example.org.", "SOA", "3600",
+                                    "ns1.example.org. admin.example.org. "
+                                    "1234 3600 1800 2419200 7200"));
+    expected.push_back(JournalEntry(WRITABLE_ZONE_ID, 1234,
+                                    DatabaseAccessor::DIFF_DELETE,
+                                    string(nsec3_hash) + ".example.org.",
+                                    "NSEC3", "3600", nsec3_rdata));
+    expected.push_back(JournalEntry(WRITABLE_ZONE_ID, 1235,
+                                    DatabaseAccessor::DIFF_ADD,
+                                    "example.org.", "SOA", "3600",
+                                    "ns1.example.org. admin.example.org. "
+                                    "1235 3600 1800 2419200 7200"));
+    expected.push_back(JournalEntry(WRITABLE_ZONE_ID, 1235,
+                                    DatabaseAccessor::DIFF_ADD,
+                                    string(nsec3_hash) + ".example.org.",
+                                    "NSEC3", "3600", nsec3_rdata));
+    this->checkJournal(expected);
+}
+
 /*
  * Push multiple delete-add sequences. Checks it is allowed and all is
  * saved.
@@ -3673,10 +3866,10 @@ TYPED_TEST(DatabaseClientTest, journalReader) {
     ASSERT_TRUE(jnl_reader);
     ConstRRsetPtr rrset = jnl_reader->getNextDiff();
     ASSERT_TRUE(rrset);
-    isc::testutils::rrsetCheck(this->soa_, rrset);
+    rrsetCheck(this->soa_, rrset);
     rrset = jnl_reader->getNextDiff();
     ASSERT_TRUE(rrset);
-    isc::testutils::rrsetCheck(soa_end, rrset);
+    rrsetCheck(soa_end, rrset);
     rrset = jnl_reader->getNextDiff();
     ASSERT_FALSE(rrset);
 
@@ -3720,7 +3913,7 @@ TYPED_TEST(DatabaseClientTest, readLargeJournal) {
     ConstRRsetPtr actual;
     int i = 0;
     while ((actual = jnl_reader->getNextDiff()) != NULL) {
-        isc::testutils::rrsetCheck(expected.at(i++), actual);
+        rrsetCheck(expected.at(i++), actual);
     }
     EXPECT_EQ(expected.size(), i); // we should have eaten all expected data
 }

+ 65 - 11
src/lib/datasrc/tests/memory_datasrc_unittest.cc

@@ -293,17 +293,71 @@ setRRset(RRsetPtr rrset, vector<RRsetPtr*>::iterator& it) {
     ++it;
 }
 
-RRsetPtr
-textToRRset(const string& text_rrset, const RRClass& rrclass = RRClass::IN(),
-            const Name& origin = Name::ROOT_NAME())
-{
-    stringstream ss(text_rrset);
-    RRsetPtr rrset;
-    vector<RRsetPtr*> rrsets;
-    rrsets.push_back(&rrset);
-    masterLoad(ss, origin, rrclass, boost::bind(setRRset, _1, rrsets.begin()));
-    return (rrset);
-}
+// Some faked NSEC3 hash values commonly used in tests and the faked NSEC3Hash
+// object.
+//
+// For apex (example.org)
+const char* const apex_hash = "0P9MHAVEQVM6T7VBL5LOP2U3T2RP3TOM";
+const char* const apex_hash_lower = "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom";
+// For ns1.example.org
+const char* const ns1_hash = "2T7B4G4VSA5SMI47K61MV5BV1A22BOJR";
+// For w.example.org
+const char* const w_hash = "01UDEMVP1J2F7EG6JEBPS17VP3N8I58H";
+// For x.y.w.example.org (lower-cased)
+const char* const xyw_hash = "2vptu5timamqttgl4luu9kg21e0aor3s";
+// For zzz.example.org.
+const char* const zzz_hash = "R53BQ7CC2UVMUBFU5OCMM6PERS9TK9EN";
+
+// A simple faked NSEC3 hash calculator with a dedicated creator for it.
+//
+// This is used in some NSEC3-related tests below.
+class TestNSEC3HashCreator : public NSEC3HashCreator {
+    class TestNSEC3Hash : public NSEC3Hash {
+    private:
+        typedef map<Name, string> NSEC3HashMap;
+        typedef NSEC3HashMap::value_type NSEC3HashPair;
+        NSEC3HashMap map_;
+    public:
+        TestNSEC3Hash() {
+            // Build pre-defined hash
+            map_[Name("example.org")] = apex_hash;
+            map_[Name("www.example.org")] = "2S9MHAVEQVM6T7VBL5LOP2U3T2RP3TOM";
+            map_[Name("xxx.example.org")] = "Q09MHAVEQVM6T7VBL5LOP2U3T2RP3TOM";
+            map_[Name("yyy.example.org")] = "0A9MHAVEQVM6T7VBL5LOP2U3T2RP3TOM";
+            map_[Name("x.y.w.example.org")] =
+                "2VPTU5TIMAMQTTGL4LUU9KG21E0AOR3S";
+            map_[Name("y.w.example.org")] = "K8UDEMVP1J2F7EG6JEBPS17VP3N8I58H";
+            map_[Name("w.example.org")] = w_hash;
+            map_[Name("zzz.example.org")] = zzz_hash;
+            map_[Name("smallest.example.org")] =
+                "00000000000000000000000000000000";
+            map_[Name("largest.example.org")] =
+                "UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU";
+        }
+        virtual string calculate(const Name& name) const {
+            const NSEC3HashMap::const_iterator found = map_.find(name);
+            if (found != map_.end()) {
+                return (found->second);
+            }
+            isc_throw(isc::Unexpected, "unexpected name for NSEC3 test: "
+                      << name);
+        }
+        virtual bool match(const generic::NSEC3PARAM&) const {
+            return (true);
+        }
+        virtual bool match(const generic::NSEC3&) const {
+            return (true);
+        }
+    };
+
+public:
+    virtual NSEC3Hash* create(const generic::NSEC3PARAM&) const {
+        return (new TestNSEC3Hash);
+    }
+    virtual NSEC3Hash* create(const generic::NSEC3&) const {
+        return (new TestNSEC3Hash);
+    }
+};
 
 /// \brief Test fixture for the InMemoryZoneFinder class
 class InMemoryZoneFinderTest : public ::testing::Test {

+ 26 - 0
src/lib/testutils/dnsmessage_test.cc

@@ -23,6 +23,12 @@
 
 #include <testutils/dnsmessage_test.h>
 
+#include <boost/bind.hpp>
+
+#include <string>
+#include <sstream>
+
+using namespace std;
 using namespace isc::dns;
 
 namespace isc {
@@ -80,6 +86,26 @@ matchRdata(const char*, const char*,
     }
     return (::testing::AssertionSuccess());
 }
+
+// A helper callback of masterLoad() used by textToRRset() below.
+void
+setRRset(RRsetPtr rrset, RRsetPtr* rrsetp) {
+    if (*rrsetp) {
+        isc_throw(isc::Unexpected,
+                  "multiple RRsets are given to textToRRset");
+    }
+    *rrsetp = rrset;
+}
+}
+
+RRsetPtr
+textToRRset(const string& text_rrset, const RRClass& rrclass,
+            const Name& origin)
+{
+    stringstream ss(text_rrset);
+    RRsetPtr rrset;
+    masterLoad(ss, origin, rrclass, boost::bind(setRRset, _1, &rrset));
+    return (rrset);
 }
 
 void

+ 23 - 0
src/lib/testutils/dnsmessage_test.h

@@ -174,6 +174,29 @@ private:
 };
 }
 
+/// \brief A converter from a string to RRset.
+///
+/// This is a convenient shortcut for tests that need to create an RRset
+/// from textual representation with a single call to a function.
+///
+/// An RRset consisting of multiple RRs can be constructed, but only one
+/// RRset is allowed.  If the given string contains mixed types of RRs
+/// it throws an \c isc::Unexpected exception.
+///
+/// \param text_rrset A complete textual representation of an RRset.
+///  It must meets the assumption of the \c dns::masterLoad() function.
+/// \param rrclass The RR class of the RRset.  Note that \c text_rrset should
+/// contain the RR class, but it's needed for \c dns::masterLoad().
+/// \param origin The zone origin where the RR is expected to belong.  This
+/// parameter normally doesn't have to be specified, but for an SOA RR it
+/// must be set to its owner name, due to the internal check of
+/// \c dns::masterLoad().
+isc::dns::RRsetPtr textToRRset(const std::string& text_rrset,
+                               const isc::dns::RRClass& rrclass =
+                               isc::dns::RRClass::IN(),
+                               const isc::dns::Name& origin =
+                               isc::dns::Name::ROOT_NAME());
+
 /// Set of unit tests to check if two sets of RRsets are identical.
 ///
 /// This templated function takes two sets of sequences, each defined by