Parcourir la source

[5226] Add traling array in records

Francis Dupont il y a 8 ans
Parent
commit
85d7ae80c8

Fichier diff supprimé car celui-ci est trop grand
+ 25 - 1
doc/guide/dhcp4-srv.xml


+ 25 - 0
doc/guide/dhcp6-srv.xml

@@ -1155,6 +1155,7 @@ temporarily override a list of interface names and listen on all interfaces.
     </para>
 
 <!-- @todo: describe record types -->
+<!-- @todo: describe array in record types -->
 
       <para>
         The <xref linkend="dhcp6-custom-options"/> describes the configuration
@@ -1400,6 +1401,30 @@ temporarily override a list of interface names and listen on all interfaces.
       the "record-types" field of the option definition.
       </para>
 
+      <para>
+      When the <command>array</command> is set to <command>true</command>
+      with a <command>type</command> is set to "record", the last field
+      is an array, i.e., it can contain more than one value as in:
+<screen>
+"Dhcp6": {
+    "option-def": [
+        {
+            <userinput>"name": "bar",
+            "code": 101,
+            "space": "dhcp6",
+            "type": "record",
+            "array": true,
+            "record-types": "ipv6-address, uint16",
+            "encapsulate": ""</userinput>
+        }, ...
+    ],
+    ...
+}
+</screen>
+      The new option content is one IPv6 address followed by one or more 16
+      bit unsigned integers.
+      </para>
+
       <note>
        <para>In the general case, boolean values are specified as <command>true</command> or
        <command>false</command>, without quotes. Some specific boolean parameters may

+ 125 - 159
src/lib/dhcp/option_custom.cc

@@ -132,6 +132,35 @@ OptionCustom::checkIndex(const uint32_t index) const {
 }
 
 void
+OptionCustom::createBuffer(OptionBuffer& buffer,
+                           const OptionDataType data_type) const {
+    // For data types that have a fixed size we can use the
+    // utility function to get the buffer's size.
+    size_t data_size = OptionDataTypeUtil::getDataTypeLen(data_type);
+
+    // For variable data sizes the utility function returns zero.
+    // It is ok for string values because the default string
+    // is 'empty'. However for FQDN the empty value is not valid
+    // so we initialize it to '.'. For prefix there is a prefix
+    // length fixed field.
+    if (data_size == 0) {
+        if (data_type == OPT_FQDN_TYPE) {
+            OptionDataTypeUtil::writeFqdn(".", buffer);
+
+        } else if (data_type == OPT_IPV6_PREFIX_TYPE) {
+            OptionDataTypeUtil::writePrefix(PrefixLen(0),
+                                            IOAddress::IPV6_ZERO_ADDRESS(),
+                                            buffer);
+        }
+    } else {
+        // At this point we can resize the buffer. Note that
+        // for string values we are setting the empty buffer
+        // here.
+        buffer.resize(data_size);
+    }
+}
+
+void
 OptionCustom::createBuffers() {
     definition_.validate();
 
@@ -154,31 +183,7 @@ OptionCustom::createBuffers() {
         for (OptionDefinition::RecordFieldsConstIter field = fields.begin();
              field != fields.end(); ++field) {
             OptionBuffer buf;
-
-            // For data types that have a fixed size we can use the
-            // utility function to get the buffer's size.
-            size_t data_size = OptionDataTypeUtil::getDataTypeLen(*field);
-
-            // For variable data sizes the utility function returns zero.
-            // It is ok for string values because the default string
-            // is 'empty'. However for FQDN the empty value is not valid
-            // so we initialize it to '.'. For prefix there is a prefix
-            // length fixed field.
-            if (data_size == 0) {
-                if (*field == OPT_FQDN_TYPE) {
-                    OptionDataTypeUtil::writeFqdn(".", buf);
-
-                } else if (*field == OPT_IPV6_PREFIX_TYPE) {
-                    OptionDataTypeUtil::writePrefix(PrefixLen(0),
-                                                    IOAddress::IPV6_ZERO_ADDRESS(),
-                                                    buf);
-                }
-            } else {
-                // At this point we can resize the buffer. Note that
-                // for string values we are setting the empty buffer
-                // here.
-                buf.resize(data_size);
-            }
+            createBuffer(buf, *field);
             // We have the buffer with default value prepared so we
             // add it to the set of buffers.
             buffers.push_back(buf);
@@ -193,21 +198,7 @@ OptionCustom::createBuffers() {
         // For non-arrays we have a single value being held by the option
         // so we have to allocate exactly one buffer.
         OptionBuffer buf;
-        size_t data_size = OptionDataTypeUtil::getDataTypeLen(data_type);
-        if (data_size == 0) {
-            if (data_type == OPT_FQDN_TYPE) {
-                OptionDataTypeUtil::writeFqdn(".", buf);
-
-            } else if (data_type == OPT_IPV6_PREFIX_TYPE) {
-                OptionDataTypeUtil::writePrefix(PrefixLen(0),
-                                                IOAddress::IPV6_ZERO_ADDRESS(),
-                                                buf);
-            }
-        } else {
-            // Note that if our option holds a string value then
-            // we are making empty buffer here.
-            buf.resize(data_size);
-        }
+        createBuffer(buf, data_type);
         // Add a buffer that we have created and leave.
         buffers.push_back(buf);
     }
@@ -217,6 +208,73 @@ OptionCustom::createBuffers() {
     std::swap(buffers, buffers_);
 }
 
+size_t
+OptionCustom::bufferLength(const OptionDataType data_type, bool in_array,
+                           OptionBuffer::const_iterator begin,
+                           OptionBuffer::const_iterator end) const {
+    // For fixed-size data type such as boolean, integer, even
+    // IP address we can use the utility function to get the required
+    // buffer size.
+    size_t data_size = OptionDataTypeUtil::getDataTypeLen(data_type);
+
+    // For variable size types (e.g. string) the function above will
+    // return 0 so we need to do a runtime check of the length.
+    if (data_size == 0) {
+        // FQDN is a special data type as it stores variable length data
+        // but the data length is encoded in the buffer. The easiest way
+        // to obtain the length of the data is to read the FQDN. The
+        // utility function will return the size of the buffer on success.
+        if (data_type == OPT_FQDN_TYPE) {
+            std::string fqdn =
+                OptionDataTypeUtil::readFqdn(OptionBuffer(begin, end));
+            // The size of the buffer holding an FQDN is always
+            // 1 byte larger than the size of the string
+            // representation of this FQDN.
+            data_size = fqdn.size() + 1;
+        } else if (!definition_.getArrayType() &&
+                   ((data_type == OPT_BINARY_TYPE) ||
+                    (data_type == OPT_STRING_TYPE))) {
+            // In other case we are dealing with string or binary value
+            // which size can't be determined. Thus we consume the
+            // remaining part of the buffer for it. Note that variable
+            // size data can be laid at the end of the option only and
+            // that the validate() function in OptionDefinition object
+            // should have checked wheter it is a case for this option.
+            data_size = std::distance(begin, end);
+        } else if (data_type == OPT_IPV6_PREFIX_TYPE) {
+            // The size of the IPV6 prefix type is determined as
+            // one byte (which is the size of the prefix in bits)
+            // followed by the prefix bits (right-padded with
+            // zeros to the nearest octet boundary)
+            if ((begin == end) && !in_array)
+                return 0;
+            PrefixTuple prefix =
+                OptionDataTypeUtil::readPrefix(OptionBuffer(begin, end));
+            // Data size comprises 1 byte holding a prefix length and the
+            // prefix length (in bytes) rounded to the nearest byte boundary.
+            data_size = sizeof(uint8_t) + (prefix.first.asUint8() + 7) / 8;
+        } else if (data_type == OPT_TUPLE_TYPE) {
+            OpaqueDataTuple::LengthFieldType lft =
+                getUniverse() == Option::V4 ?
+                OpaqueDataTuple::LENGTH_1_BYTE :
+                OpaqueDataTuple::LENGTH_2_BYTES;
+            std::string value =
+                OptionDataTypeUtil::readTuple(OptionBuffer(begin, end), lft);
+            data_size = value.size();
+            // The size of the buffer holding a tuple is always
+            // 1 or 2 byte larger than the size of the string
+            data_size += getUniverse() == Option::V4 ? 1 : 2;
+        } else {
+            // If we reached the end of buffer we assume that this option is
+            // truncated because there is no remaining data to initialize
+            // an option field.
+            isc_throw(OutOfRange, "option buffer truncated");
+        }
+    }
+
+    return data_size;
+}
+
 void
 OptionCustom::createBuffers(const OptionBuffer& data_buf) {
     // Check that the option definition is correct as we are going
@@ -237,60 +295,8 @@ OptionCustom::createBuffers(const OptionBuffer& data_buf) {
         // Go over all data fields within a record.
         for (OptionDefinition::RecordFieldsConstIter field = fields.begin();
              field != fields.end(); ++field) {
-            // For fixed-size data type such as boolean, integer, even
-            // IP address we can use the utility function to get the required
-            // buffer size.
-            size_t data_size = OptionDataTypeUtil::getDataTypeLen(*field);
-
-            // For variable size types (e.g. string) the function above will
-            // return 0 so we need to do a runtime check of the length.
-            if (data_size == 0) {
-                // FQDN is a special data type as it stores variable length data
-                // but the data length is encoded in the buffer. The easiest way
-                // to obtain the length of the data is to read the FQDN. The
-                // utility function will return the size of the buffer on success.
-                if (*field == OPT_FQDN_TYPE) {
-                    std::string fqdn =
-                        OptionDataTypeUtil::readFqdn(OptionBuffer(data, data_buf.end()));
-                    // The size of the buffer holding an FQDN is always
-                    // 1 byte larger than the size of the string
-                    // representation of this FQDN.
-                    data_size = fqdn.size() + 1;
-                } else if ((*field == OPT_BINARY_TYPE) || (*field == OPT_STRING_TYPE)) {
-                    // In other case we are dealing with string or binary value
-                    // which size can't be determined. Thus we consume the
-                    // remaining part of the buffer for it. Note that variable
-                    // size data can be laid at the end of the option only and
-                    // that the validate() function in OptionDefinition object
-                    // should have checked wheter it is a case for this option.
-                    data_size = std::distance(data, data_buf.end());
-                } else if (*field == OPT_IPV6_PREFIX_TYPE) {
-                    // The size of the IPV6 prefix type is determined as
-                    // one byte (which is the size of the prefix in bits)
-                    // followed by the prefix bits (right-padded with
-                    // zeros to the nearest octet boundary).
-                    if (std::distance(data, data_buf.end()) > 0) {
-                        data_size = static_cast<size_t>(sizeof(uint8_t) + (*data + 7) / 8);
-                    }
-                } else if (*field == OPT_TUPLE_TYPE) {
-                    OpaqueDataTuple::LengthFieldType lft =
-                        getUniverse() == Option::V4 ?
-                        OpaqueDataTuple::LENGTH_1_BYTE :
-                        OpaqueDataTuple::LENGTH_2_BYTES;
-                    std::string value =
-                        OptionDataTypeUtil::readTuple(OptionBuffer(data, data_buf.end()),
-                                                      lft);
-                    data_size = value.size();
-                    // The size of the buffer holding a tuple is always
-                    // 1 or 2 byte larger than the size of the string
-                    data_size += getUniverse() == Option::V4 ? 1 : 2;
-                } else {
-                    // If we reached the end of buffer we assume that this option is
-                    // truncated because there is no remaining data to initialize
-                    // an option field.
-                    isc_throw(OutOfRange, "option buffer truncated");
-                }
-            }
+            size_t data_size = bufferLength(*field, false,
+                                            data, data_buf.end());
 
             // Our data field requires that there is a certain chunk of
             // data left in the buffer. If not, option is truncated.
@@ -304,8 +310,23 @@ OptionCustom::createBuffers(const OptionBuffer& data_buf) {
             data += data_size;
         }
 
+        // Get extra buffers when the last field is an array.
+        if (definition_.getArrayType()) {
+            while (data != data_buf.end()) {
+                // Code copied from the standard array case
+                size_t data_size = bufferLength(fields.back(), true,
+                                                data, data_buf.end());
+                assert(data_size > 0);
+                if (std::distance(data, data_buf.end()) < data_size) {
+                    break;
+                }
+                buffers.push_back(OptionBuffer(data, data + data_size));
+                data += data_size;
+            }
+        }
+
         // Unpack suboptions if any.
-        if (data != data_buf.end() && !getEncapsulatedSpace().empty()) {
+        else if (data != data_buf.end() && !getEncapsulatedSpace().empty()) {
             unpackOptions(OptionBuffer(data, data_buf.end()));
         }
 
@@ -328,38 +349,7 @@ OptionCustom::createBuffers(const OptionBuffer& data_buf) {
         // we have to handle multiple buffers.
         if (definition_.getArrayType()) {
             while (data != data_buf.end()) {
-                // FQDN is a special case because it is of a variable length.
-                // The actual length for a particular FQDN is encoded within
-                // a buffer so we have to actually read the FQDN from a buffer
-                // to get it.
-                if (data_type == OPT_FQDN_TYPE) {
-                    std::string fqdn =
-                        OptionDataTypeUtil::readFqdn(OptionBuffer(data, data_buf.end()));
-                    // The size of the buffer holding an FQDN is always
-                    // 1 byte larger than the size of the string
-                    // representation of this FQDN.
-                    data_size = fqdn.size() + 1;
-
-                } else if (data_type == OPT_IPV6_PREFIX_TYPE) {
-                    PrefixTuple prefix =
-                        OptionDataTypeUtil::readPrefix(OptionBuffer(data, data_buf.end()));
-                    // Data size comprises 1 byte holding a prefix length and the
-                    // prefix length (in bytes) rounded to the nearest byte boundary.
-                    data_size = sizeof(uint8_t) + (prefix.first.asUint8() + 7) / 8;
-                } else if (data_type == OPT_TUPLE_TYPE) {
-                    OpaqueDataTuple::LengthFieldType lft =
-                        getUniverse() == Option::V4 ?
-                        OpaqueDataTuple::LENGTH_1_BYTE :
-                        OpaqueDataTuple::LENGTH_2_BYTES;
-                    std::string value =
-                        OptionDataTypeUtil::readTuple(OptionBuffer(data, data_buf.end()),
-                                                      lft);
-                    data_size = value.size();
-                    // The size of the buffer holding a tuple is always
-                    // 1 or 2 byte larger than the size of the string
-                    data_size += getUniverse() == Option::V4 ? 1 : 2;
-
-                }
+                data_size = bufferLength(data_type, true, data, data_buf.end());
                 // We don't perform other checks for data types that can't be
                 // used together with array indicator such as strings, empty field
                 // etc. This is because OptionDefinition::validate function should
@@ -381,38 +371,7 @@ OptionCustom::createBuffers(const OptionBuffer& data_buf) {
             // For non-arrays the data_size can be zero because
             // getDataTypeLen returns zero for variable size data types
             // such as strings. Simply take whole buffer.
-            if (data_size == 0) {
-                // For FQDN we get the size by actually reading the FQDN.
-                if (data_type == OPT_FQDN_TYPE) {
-                    std::string fqdn =
-                        OptionDataTypeUtil::readFqdn(OptionBuffer(data, data_buf.end()));
-                    // The size of the buffer holding an FQDN is always
-                    // 1 bytes larger than the size of the string
-                    // representation of this FQDN.
-                    data_size = fqdn.size() + 1;
-
-                } else if (data_type == OPT_IPV6_PREFIX_TYPE) {
-                    if (!data_buf.empty()) {
-                        data_size = static_cast<size_t>
-                            (sizeof(uint8_t) + (data_buf[0] + 7) / 8);
-                    }
-                } else if (data_type == OPT_TUPLE_TYPE) {
-                    OpaqueDataTuple::LengthFieldType lft =
-                        getUniverse() == Option::V4 ?
-                        OpaqueDataTuple::LENGTH_1_BYTE :
-                        OpaqueDataTuple::LENGTH_2_BYTES;
-                    std::string value =
-                        OptionDataTypeUtil::readTuple(OptionBuffer(data, data_buf.end()),
-                                                      lft);
-                    data_size = value.size();
-                    // The size of the buffer holding a tuple is always
-                    // 1 or 2 byte larger than the size of the string
-                    data_size += getUniverse() == Option::V4 ? 1 : 2;
-
-                } else {
-                    data_size = std::distance(data, data_buf.end());
-                }
-            }
+            data_size = bufferLength(data_type, false, data, data_buf.end());
             if ((data_size > 0) && (std::distance(data, data_buf.end()) >= data_size)) {
                 buffers.push_back(OptionBuffer(data, data + data_size));
                 data += data_size;
@@ -744,6 +703,13 @@ std::string OptionCustom::toText(int indent) const {
             output << " " << dataFieldToText(*field, std::distance(fields.begin(),
                                                                    field));
         }
+
+        // If the last record field is an array iterate on extra buffers
+        if (definition_.getArrayType()) {
+            for (unsigned int i = fields.size(); i < getDataFieldsNum(); ++i) {
+                output << " " << dataFieldToText(fields.back(), i);
+            }
+        }
     } else {
         // For non-record types we iterate over all buffers
         // and print the data type set globally for an option

+ 40 - 6
src/lib/dhcp/option_custom.h

@@ -102,6 +102,10 @@ public:
     void addArrayDataField(const T value) {
         checkArrayType();
         OptionDataType data_type = definition_.getType();
+        // Handle record last field.
+        if (data_type == OPT_RECORD_TYPE) {
+            data_type = definition_.getRecordFields().back();
+        }
         if (OptionDataTypeTraits<T>::type != data_type) {
             isc_throw(isc::dhcp::InvalidDataType,
                       "specified data type " << data_type << " does not"
@@ -405,9 +409,28 @@ private:
     /// @throw isc::OutOfRange if index is out of range.
     void checkIndex(const uint32_t index) const;
 
+    /// @brief Create a non initialized buffer.
+    ///
+    /// @param buffer buffer to update.
+    /// @param data_type data type of buffer.
+    void createBuffer(OptionBuffer& buffer,
+                      const OptionDataType data_type) const;
+
     /// @brief Create a collection of non initialized buffers.
     void createBuffers();
 
+    /// @brief Return length of a buffer.
+    ///
+    /// @param data_type data type of buffer.
+    /// @param in_array true is called from the array case
+    /// @param begin iterator to first byte of input data.
+    /// @param end iterator to end of input data.
+    ///
+    /// @return size of data to copy to the buffer.
+    size_t bufferLength(const OptionDataType data_type, bool in_array,
+                        OptionBuffer::const_iterator begin,
+                        OptionBuffer::const_iterator end) const;
+
     /// @brief Create collection of buffers representing data field values.
     ///
     /// @param data_buf a buffer to be parsed.
@@ -453,12 +476,23 @@ OptionCustom::checkDataType(const uint32_t index) const {
     if (data_type == OPT_RECORD_TYPE) {
         const OptionDefinition::RecordFieldsCollection& record_fields =
             definition_.getRecordFields();
-        // When we initialized buffers we have already checked that
-        // the number of these buffers is equal to number of option
-        // fields in the record so the condition below should be met.
-        assert(index < record_fields.size());
-        // Get the data type to be returned.
-        data_type = record_fields[index];
+        if (definition_.getArrayType()) {
+            // If the array flag is set the last record field is an array.
+            if (index < record_fields.size()) {
+                // Get the data type to be returned.
+                data_type = record_fields[index];
+            } else {
+                // Get the data type to be returned from the last record field.
+                data_type = record_fields.back();
+            }
+        } else {
+            // When we initialized buffers we have already checked that
+            // the number of these buffers is equal to number of option
+            // fields in the record so the condition below should be met.
+            assert(index < record_fields.size());
+            // Get the data type to be returned.
+            data_type = record_fields[index];
+        }
     }
 
     if (OptionDataTypeTraits<T>::type != data_type) {

+ 38 - 16
src/lib/dhcp/option_definition.cc

@@ -258,6 +258,12 @@ OptionDefinition::optionFactory(Option::Universe u, uint16_t type,
         for (size_t i = 0; i < records.size(); ++i) {
             writeToBuffer(u, util::str::trim(values[i]), records[i], buf);
         }
+        if (array_type_ && (values.size() > records.size())) {
+            for (size_t i = records.size(); i < values.size(); ++i) {
+                writeToBuffer(u, util::str::trim(values[i]),
+                              records.back(), buf);
+            }
+        }
     }
     return (optionFactory(u, type, buf.begin(), buf.end()));
 }
@@ -292,22 +298,6 @@ OptionDefinition::validate() const {
         // Option definition must be of a known type.
         err_str << "option type " << type_ << " not supported.";
 
-    } else if (array_type_) {
-        if (type_ == OPT_STRING_TYPE) {
-            // Array of strings is not allowed because there is no way
-            // to determine the size of a particular string and thus there
-            // it no way to tell when other data fields begin.
-            err_str << "array of strings is not a valid option definition.";
-        } else if (type_ == OPT_BINARY_TYPE) {
-            err_str << "array of binary values is not"
-                    << " a valid option definition.";
-
-        } else if (type_ == OPT_EMPTY_TYPE) {
-            err_str << "array of empty value is not"
-                    << " a valid option definition.";
-
-        }
-
     } else if (type_ == OPT_RECORD_TYPE) {
         // At least two data fields should be added to the record. Otherwise
         // non-record option definition could be used.
@@ -335,6 +325,7 @@ OptionDefinition::validate() const {
                     it < fields.end() - 1) {
                     err_str << "binary data field can't be laid before data"
                             << " fields of other types.";
+                    break;
                 }
                 /// Empty type is not allowed within a record.
                 if (*it == OPT_EMPTY_TYPE) {
@@ -343,8 +334,35 @@ OptionDefinition::validate() const {
                     break;
                 }
             }
+            // If the array flag is set the last field is an array.
+            if (err_str.str().empty() && array_type_) {
+                const OptionDataType& last_type = fields.back();
+                if (last_type == OPT_STRING_TYPE) {
+                    err_str << "array of strings is not"
+                            << "a valid option definition.";
+                } else if (last_type == OPT_BINARY_TYPE) {
+                    err_str << "array of binary values is not"
+                            << " a valid option definition.";
+                }
+                // Empty type was already checked.
+            }
         }
 
+    } else if (array_type_) {
+        if (type_ == OPT_STRING_TYPE) {
+            // Array of strings is not allowed because there is no way
+            // to determine the size of a particular string and thus there
+            // it no way to tell when other data fields begin.
+            err_str << "array of strings is not a valid option definition.";
+        } else if (type_ == OPT_BINARY_TYPE) {
+            err_str << "array of binary values is not"
+                    << " a valid option definition.";
+
+        } else if (type_ == OPT_EMPTY_TYPE) {
+            err_str << "array of empty value is not"
+                    << " a valid option definition.";
+
+        }
     }
 
     // Non-empty error string means that we have hit the error. We throw
@@ -357,6 +375,7 @@ OptionDefinition::validate() const {
 bool
 OptionDefinition::haveIAx6Format(OptionDataType first_type) const {
    return (haveType(OPT_RECORD_TYPE) &&
+           !getArrayType() &&
            record_fields_.size() == 3 &&
            record_fields_[0] == first_type &&
            record_fields_[1] == OPT_UINT32_TYPE &&
@@ -382,6 +401,7 @@ OptionDefinition::haveIAAddr6Format() const {
 bool
 OptionDefinition::haveIAPrefix6Format() const {
     return (haveType(OPT_RECORD_TYPE) &&
+           !getArrayType() &&
             record_fields_.size() == 4 &&
             record_fields_[0] == OPT_UINT32_TYPE &&
             record_fields_[1] == OPT_UINT32_TYPE &&
@@ -392,6 +412,7 @@ OptionDefinition::haveIAPrefix6Format() const {
 bool
 OptionDefinition::haveFqdn4Format() const {
     return (haveType(OPT_RECORD_TYPE) &&
+           !getArrayType() &&
             record_fields_.size() == 4 &&
             record_fields_[0] == OPT_UINT8_TYPE &&
             record_fields_[1] == OPT_UINT8_TYPE &&
@@ -402,6 +423,7 @@ OptionDefinition::haveFqdn4Format() const {
 bool
 OptionDefinition::haveClientFqdnFormat() const {
     return (haveType(OPT_RECORD_TYPE) &&
+           !getArrayType() &&
             (record_fields_.size() == 2) &&
             (record_fields_[0] == OPT_UINT8_TYPE) &&
             (record_fields_[1] == OPT_FQDN_TYPE));

+ 237 - 0
src/lib/dhcp/tests/option_custom_unittest.cc

@@ -1275,6 +1275,97 @@ TEST_F(OptionCustomTest, recordData) {
     EXPECT_EQ("ABCD", value6);
 }
 
+// The purpose of this test is to verify that the option definition comprising
+// a record of various data fields with an array for the last can be used
+// to create an instance of custom option.
+TEST_F(OptionCustomTest, recordArrayData) {
+    // Create the definition of an option which comprises
+    // a record of fields of different types.
+    OptionDefinition opt_def("OPTION_FOO", 1000, "record", true);
+    ASSERT_NO_THROW(opt_def.addRecordField("uint16"));
+    ASSERT_NO_THROW(opt_def.addRecordField("boolean"));
+    ASSERT_NO_THROW(opt_def.addRecordField("fqdn"));
+    ASSERT_NO_THROW(opt_def.addRecordField("ipv4-address"));
+    ASSERT_NO_THROW(opt_def.addRecordField("ipv6-address"));
+    ASSERT_NO_THROW(opt_def.addRecordField("psid"));
+    ASSERT_NO_THROW(opt_def.addRecordField("uint32"));
+
+    const char fqdn_data[] = {
+        8, 109, 121, 100, 111, 109, 97, 105, 110, // "mydomain"
+        7, 101, 120, 97, 109, 112, 108, 101,      // "example"
+        3, 99, 111, 109,                          // "com"
+        0,
+    };
+
+    OptionBuffer buf;
+    // Initialize field 0 to 8712.
+    writeInt<uint16_t>(8712, buf);
+    // Initialize field 1 to 'true'
+    buf.push_back(static_cast<unsigned short>(1));
+    // Initialize field 2 to 'mydomain.example.com'.
+    buf.insert(buf.end(), fqdn_data, fqdn_data + sizeof(fqdn_data));
+    // Initialize field 3 to IPv4 address.
+    writeAddress(IOAddress("192.168.0.1"), buf);
+    // Initialize field 4 to IPv6 address.
+    writeAddress(IOAddress("2001:db8:1::1"), buf);
+    // Initialize PSID len and PSID value.
+    writeInt<uint8_t>(6, buf);
+    writeInt<uint16_t>(0xD400, buf);
+    // Initialize last field 6 to a pair of int 12345678 and 87654321.
+    writeInt<uint32_t>(12345678, buf);
+    writeInt<uint32_t>(87654321, buf);
+
+    boost::scoped_ptr<OptionCustom> option;
+    ASSERT_NO_THROW(
+         option.reset(new OptionCustom(opt_def, Option::V6, buf.begin(), buf.end()));
+    );
+    ASSERT_TRUE(option);
+
+    // We should have 7+1 data fields.
+    ASSERT_EQ(8, option->getDataFieldsNum());
+
+    // Verify value in the field 0.
+    uint16_t value0 = 0;
+    ASSERT_NO_THROW(value0 = option->readInteger<uint16_t>(0));
+    EXPECT_EQ(8712, value0);
+
+    // Verify value in the field 1.
+    bool value1 = false;
+    ASSERT_NO_THROW(value1 = option->readBoolean(1));
+    EXPECT_TRUE(value1);
+
+    // Verify value in the field 2.
+    std::string value2 = "";
+    ASSERT_NO_THROW(value2 = option->readFqdn(2));
+    EXPECT_EQ("mydomain.example.com.", value2);
+
+    // Verify value in the field 3.
+    IOAddress value3("127.0.0.1");
+    ASSERT_NO_THROW(value3 = option->readAddress(3));
+    EXPECT_EQ("192.168.0.1", value3.toText());
+
+    // Verify value in the field 4.
+    IOAddress value4("::1");
+    ASSERT_NO_THROW(value4 = option->readAddress(4));
+    EXPECT_EQ("2001:db8:1::1", value4.toText());
+
+    // Verify value in the field 5.
+    PSIDTuple value5;
+    ASSERT_NO_THROW(value5 = option->readPsid(5));
+    EXPECT_EQ(6, value5.first.asUnsigned());
+    EXPECT_EQ(0x35, value5.second.asUint16());
+
+    // Verify value in the field 6.
+    uint32_t value6;
+    ASSERT_NO_THROW(value6 = option->readInteger<uint32_t>(6));
+    EXPECT_EQ(12345678, value6);
+
+    // Verify value in the extra field 7.
+    uint32_t value7;
+    ASSERT_NO_THROW(value7 = option->readInteger<uint32_t>(7));
+    EXPECT_EQ(87654321, value7);
+}
+
 // The purpose of this test is to verify that truncated buffer
 // can't be used to create an option being a record of value of
 // different types.
@@ -2000,6 +2091,103 @@ TEST_F(OptionCustomTest, setRecordData) {
     EXPECT_EQ(value8, "hello world");
 }
 
+TEST_F(OptionCustomTest, setRecordArrayData) {
+    OptionDefinition opt_def("OPTION_FOO", 1000, "record", true);
+
+    ASSERT_NO_THROW(opt_def.addRecordField("uint16"));
+    ASSERT_NO_THROW(opt_def.addRecordField("boolean"));
+    ASSERT_NO_THROW(opt_def.addRecordField("fqdn"));
+    ASSERT_NO_THROW(opt_def.addRecordField("ipv4-address"));
+    ASSERT_NO_THROW(opt_def.addRecordField("ipv6-address"));
+    ASSERT_NO_THROW(opt_def.addRecordField("psid"));
+    ASSERT_NO_THROW(opt_def.addRecordField("ipv6-prefix"));
+    ASSERT_NO_THROW(opt_def.addRecordField("tuple"));
+    ASSERT_NO_THROW(opt_def.addRecordField("uint32"));
+
+    // Create an option and let the data field be initialized
+    // to default value (do not provide any data buffer).
+    boost::scoped_ptr<OptionCustom> option;
+    ASSERT_NO_THROW(
+        option.reset(new OptionCustom(opt_def, Option::V6));
+    );
+    ASSERT_TRUE(option);
+
+    // The number of elements should be equal to number of elements
+    // in the record.
+    ASSERT_EQ(9, option->getDataFieldsNum());
+
+    // Check that the default values have been correctly set.
+    uint16_t value0;
+    ASSERT_NO_THROW(value0 = option->readInteger<uint16_t>(0));
+    EXPECT_EQ(0, value0);
+    bool value1 = true;
+    ASSERT_NO_THROW(value1 = option->readBoolean(1));
+    EXPECT_FALSE(value1);
+    std::string value2;
+    ASSERT_NO_THROW(value2 = option->readFqdn(2));
+    EXPECT_EQ(".", value2);
+    IOAddress value3("127.0.0.1");
+    ASSERT_NO_THROW(value3 = option->readAddress(3));
+    EXPECT_EQ("0.0.0.0", value3.toText());
+    IOAddress value4("2001:db8:1::1");
+    ASSERT_NO_THROW(value4 = option->readAddress(4));
+    EXPECT_EQ("::", value4.toText());
+    PSIDTuple value5;
+    ASSERT_NO_THROW(value5 = option->readPsid(5));
+    EXPECT_EQ(0, value5.first.asUnsigned());
+    EXPECT_EQ(0, value5.second.asUint16());
+    PrefixTuple value6(ZERO_PREFIX_TUPLE);
+    ASSERT_NO_THROW(value6 = option->readPrefix(6));
+    EXPECT_EQ(0, value6.first.asUnsigned());
+    EXPECT_EQ("::", value6.second.toText());
+    std::string value7 = "abc";
+    // Tuple has no default value
+    EXPECT_THROW(option->readTuple(7), BadDataTypeCast);
+    uint32_t value8;
+    ASSERT_NO_THROW(value8 = option->readInteger<uint32_t>(8));
+    EXPECT_EQ(0, value8);
+
+    // Override each value with a new value.
+    ASSERT_NO_THROW(option->writeInteger<uint16_t>(1234, 0));
+    ASSERT_NO_THROW(option->writeBoolean(true, 1));
+    ASSERT_NO_THROW(option->writeFqdn("example.com", 2));
+    ASSERT_NO_THROW(option->writeAddress(IOAddress("192.168.0.1"), 3));
+    ASSERT_NO_THROW(option->writeAddress(IOAddress("2001:db8:1::100"), 4));
+    ASSERT_NO_THROW(option->writePsid(PSIDLen(4), PSID(8), 5));
+    ASSERT_NO_THROW(option->writePrefix(PrefixLen(48),
+                                        IOAddress("2001:db8:1::"), 6));
+    ASSERT_NO_THROW(option->writeTuple("foobar", 7));
+    ASSERT_NO_THROW(option->writeInteger<uint32_t>(12345678, 8));
+    ASSERT_NO_THROW(option->addArrayDataField<uint32_t>(87654321));
+
+    // Check that the new values have been correctly set.
+    ASSERT_EQ(10, option->getDataFieldsNum());
+
+    ASSERT_NO_THROW(value0 = option->readInteger<uint16_t>(0));
+    EXPECT_EQ(1234, value0);
+    ASSERT_NO_THROW(value1 = option->readBoolean(1));
+    EXPECT_TRUE(value1);
+    ASSERT_NO_THROW(value2 = option->readFqdn(2));
+    EXPECT_EQ("example.com.", value2);
+    ASSERT_NO_THROW(value3 = option->readAddress(3));
+    EXPECT_EQ("192.168.0.1", value3.toText());
+    ASSERT_NO_THROW(value4 = option->readAddress(4));
+    EXPECT_EQ("2001:db8:1::100", value4.toText());
+    ASSERT_NO_THROW(value5 = option->readPsid(5));
+    EXPECT_EQ(4, value5.first.asUnsigned());
+    EXPECT_EQ(8, value5.second.asUint16());
+    ASSERT_NO_THROW(value6 = option->readPrefix(6));
+    EXPECT_EQ(48, value6.first.asUnsigned());
+    EXPECT_EQ("2001:db8:1::", value6.second.toText());
+    ASSERT_NO_THROW(value7 = option->readTuple(7));
+    EXPECT_EQ(value7, "foobar");
+    ASSERT_NO_THROW(value8 = option->readInteger<uint32_t>(8));
+    EXPECT_EQ(12345678, value8);
+    uint32_t value9;
+    ASSERT_NO_THROW(value9 = option->readInteger<uint32_t>(9));
+    EXPECT_EQ(87654321, value9);
+}
+
 // The purpose of this test is to verify that pack function for
 // DHCPv4 custom option works correctly.
 TEST_F(OptionCustomTest, pack4) {
@@ -2130,6 +2318,55 @@ TEST_F(OptionCustomTest, unpack) {
     }
 }
 
+// The purpose of this test is to verify that unpack function works
+// correctly for a custom option with record and trailing array.
+TEST_F(OptionCustomTest, unpackRecordArray) {
+    OptionDefinition opt_def("OPTION_FOO", 231, "record", true);
+
+    ASSERT_NO_THROW(opt_def.addRecordField("uint16"));
+    ASSERT_NO_THROW(opt_def.addRecordField("ipv4-address"));
+
+    // Initialize reference data.
+    OptionBuffer buf;
+    writeInt<uint16_t>(8712, buf);
+
+    std::vector<IOAddress> addresses;
+    addresses.push_back(IOAddress("192.168.0.1"));
+    addresses.push_back(IOAddress("127.0.0.1"));
+    addresses.push_back(IOAddress("10.10.1.2"));
+
+    // Store the collection of IPv4 addresses into the buffer.
+    for (size_t i = 0; i < addresses.size(); ++i) {
+        writeAddress(addresses[i], buf);
+    }
+
+    // Use the input buffer to create custom option.
+    boost::scoped_ptr<OptionCustom> option;
+    ASSERT_NO_THROW(
+        option.reset(new OptionCustom(opt_def, Option::V4, buf.begin(), buf.end()));
+    );
+    ASSERT_TRUE(option);
+
+    // We should have 4 data fields.
+    ASSERT_EQ(4, option->getDataFieldsNum());
+
+    // We expect a 16 bit integer
+    uint16_t value0;
+    ASSERT_NO_THROW(value0 = option->readInteger<uint16_t>(0));
+    EXPECT_EQ(8712, value0);
+
+    // ... and 3 IPv4 addresses being stored in the option.
+    for (int i = 0; i < 3; ++i) {
+        IOAddress address("10.10.10.10");
+        ASSERT_NO_THROW(address = option->readAddress(i + 1));
+        EXPECT_EQ(addresses[i], address);
+    }
+
+    std::string text = option->toText();
+    EXPECT_EQ("type=231, len=014: 8712 (uint16) 192.168.0.1 (ipv4-address) "
+              "127.0.0.1 (ipv4-address) 10.10.1.2 (ipv4-address)", text);
+}
+
 // The purpose of this test is to verify that new data can be set for
 // a custom option.
 TEST_F(OptionCustomTest, initialize) {

+ 10 - 2
src/lib/dhcp/tests/option_definition_unittest.cc

@@ -325,11 +325,19 @@ TEST_F(OptionDefinitionTest, validate) {
                                "record");
     opt_def16.addRecordField("uint8");
     opt_def16.addRecordField("string");
+    EXPECT_NO_THROW(opt_def16.validate());
+
+    // ... at least if it is not an array.
+    OptionDefinition opt_def17("OPTION_STATUS_CODE", D6O_STATUS_CODE,
+                               "record", true);
+    opt_def17.addRecordField("uint8");
+    opt_def17.addRecordField("string");
+    EXPECT_THROW(opt_def17.validate(), MalformedOptionDefinition);
 
     // Check invalid encapsulated option space name.
-    OptionDefinition opt_def17("OPTION_VENDOR_OPTS", D6O_VENDOR_OPTS,
+    OptionDefinition opt_def18("OPTION_VENDOR_OPTS", D6O_VENDOR_OPTS,
                                "uint32", "invalid%space%name");
-    EXPECT_THROW(opt_def17.validate(), MalformedOptionDefinition);
+    EXPECT_THROW(opt_def18.validate(), MalformedOptionDefinition);
 }