From: Francis Dupont Date: Tue, 11 Jul 2017 00:46:22 +0000 (+0200) Subject: [5226] Add traling array in records X-Git-Tag: trac5381_base~5^2~3 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=85d7ae80c8fcd15fcb8f18771d5102da22a97208;p=thirdparty%2Fkea.git [5226] Add traling array in records --- diff --git a/doc/guide/dhcp4-srv.xml b/doc/guide/dhcp4-srv.xml index ee1664921a..fc078ccead 100644 --- a/doc/guide/dhcp4-srv.xml +++ b/doc/guide/dhcp4-srv.xml @@ -1161,6 +1161,7 @@ temporarily override a list of interface names and listen on all interfaces. clients to obtain the addresses of multiple NTP servers. + The describes the configuration @@ -1352,7 +1353,7 @@ It is merely echoed by the server ipv6-addressIPv6 address in the usual colon notation (e.g. 2001:db8::1) ipv6-prefixIPv6 prefix and prefix length specified using CIDR notation, e.g. 2001:db8:1::/64. This data type is used to represent an 8-bit field conveying a prefix length and the variable length prefix value psidPSID and PSID length separated by a slash, e.g. 3/4 specifies PSID=3 and PSID length=4. In the wire format it is represented by an 8-bit field carrying PSID length (in this case equal to 4) and the 16-bits long PSID value field (in this case equal to "0011000000000000b" using binary notation). Allowed values for a PSID length are 0 to 16. See RFC 7597 for the details about the PSID wire representation - recordStructured data that may comprise any types (except "record" and "empty") + recordStructured data that may comprise any types (except "record" and "empty"). The array flag applies to the last field. stringAny text tupleA length encoded as a 8 (16 for DHCPv6) bit unsigned integer followed by a string of this length uint88 bit unsigned integer with allowed values 0 to 255 @@ -1479,6 +1480,29 @@ It is merely echoed by the server the types set in the record-types field of the option definition. + + When the array is set to true + with a type is set to "record", the last field + is an array, i.e., it can contain more than one value as in: + +"Dhcp4": { + "option-def": [ + { + "name": "bar", + "code": 223, + "space": "dhcp4", + "type": "record", + "array": true, + "record-types": "ipv4-address, uint16", + "encapsulate": "" + }, ... + ], + ... +} + + The new option content is one IPv4 address followed by one or more 16 + bit unsigned integers. + In the general case, boolean values are specified as true or false, without quotes. Some specific boolean parameters may diff --git a/doc/guide/dhcp6-srv.xml b/doc/guide/dhcp6-srv.xml index a43a4f5fcf..5bd5ef702b 100644 --- a/doc/guide/dhcp6-srv.xml +++ b/doc/guide/dhcp6-srv.xml @@ -1155,6 +1155,7 @@ temporarily override a list of interface names and listen on all interfaces. + The 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. + + When the array is set to true + with a type is set to "record", the last field + is an array, i.e., it can contain more than one value as in: + +"Dhcp6": { + "option-def": [ + { + "name": "bar", + "code": 101, + "space": "dhcp6", + "type": "record", + "array": true, + "record-types": "ipv6-address, uint16", + "encapsulate": "" + }, ... + ], + ... +} + + The new option content is one IPv6 address followed by one or more 16 + bit unsigned integers. + + In the general case, boolean values are specified as true or false, without quotes. Some specific boolean parameters may diff --git a/src/lib/dhcp/option_custom.cc b/src/lib/dhcp/option_custom.cc index bb45e3bb03..a8b784bbda 100644 --- a/src/lib/dhcp/option_custom.cc +++ b/src/lib/dhcp/option_custom.cc @@ -131,6 +131,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(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 - (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 diff --git a/src/lib/dhcp/option_custom.h b/src/lib/dhcp/option_custom.h index 14ab22916d..6af94c37f1 100644 --- a/src/lib/dhcp/option_custom.h +++ b/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::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::type != data_type) { diff --git a/src/lib/dhcp/option_definition.cc b/src/lib/dhcp/option_definition.cc index bc26417016..e8d243cfe3 100644 --- a/src/lib/dhcp/option_definition.cc +++ b/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)); diff --git a/src/lib/dhcp/tests/option_custom_unittest.cc b/src/lib/dhcp/tests/option_custom_unittest.cc index 5b03646f3c..ba7bc3f2ea 100644 --- a/src/lib/dhcp/tests/option_custom_unittest.cc +++ b/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(8712, buf); + // Initialize field 1 to 'true' + buf.push_back(static_cast(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(6, buf); + writeInt(0xD400, buf); + // Initialize last field 6 to a pair of int 12345678 and 87654321. + writeInt(12345678, buf); + writeInt(87654321, buf); + + boost::scoped_ptr 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(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(6)); + EXPECT_EQ(12345678, value6); + + // Verify value in the extra field 7. + uint32_t value7; + ASSERT_NO_THROW(value7 = option->readInteger(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 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(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(8)); + EXPECT_EQ(0, value8); + + // Override each value with a new value. + ASSERT_NO_THROW(option->writeInteger(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(12345678, 8)); + ASSERT_NO_THROW(option->addArrayDataField(87654321)); + + // Check that the new values have been correctly set. + ASSERT_EQ(10, option->getDataFieldsNum()); + + ASSERT_NO_THROW(value0 = option->readInteger(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(8)); + EXPECT_EQ(12345678, value8); + uint32_t value9; + ASSERT_NO_THROW(value9 = option->readInteger(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(8712, buf); + + std::vector 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 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(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) { diff --git a/src/lib/dhcp/tests/option_definition_unittest.cc b/src/lib/dhcp/tests/option_definition_unittest.cc index 588cd286db..4de8243f5c 100644 --- a/src/lib/dhcp/tests/option_definition_unittest.cc +++ b/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); }