Commit 85d7ae80 authored by Francis Dupont's avatar Francis Dupont
Browse files

[5226] Add traling array in records

parent 7118f9a9
......@@ -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.
</para>
<!-- @todo: describe record types -->
<!-- @todo: describe array in record types -->
<para>
The <xref linkend="dhcp4-custom-options"/> describes the configuration
......@@ -1352,7 +1353,7 @@ It is merely echoed by the server
<row><entry>ipv6-address</entry><entry>IPv6 address in the usual colon notation (e.g. 2001:db8::1)</entry></row>
<row><entry>ipv6-prefix</entry><entry>IPv6 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</entry></row>
<row><entry>psid</entry><entry>PSID 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 <ulink url="http://tools.ietf.org/html/rfc7597">RFC 7597</ulink> for the details about the PSID wire representation</entry></row>
<row><entry>record</entry><entry>Structured data that may comprise any types (except "record" and "empty")</entry></row>
<row><entry>record</entry><entry>Structured data that may comprise any types (except "record" and "empty"). The array flag applies to the last field.</entry></row>
<row><entry>string</entry><entry>Any text</entry></row>
<row><entry>tuple</entry><entry>A length encoded as a 8 (16 for DHCPv6) bit unsigned integer followed by a string of this length</entry></row>
<row><entry>uint8</entry><entry>8 bit unsigned integer with allowed values 0 to 255</entry></row>
......@@ -1479,6 +1480,29 @@ It is merely echoed by the server
the types set in the <command>record-types</command> 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>
"Dhcp4": {
"option-def": [
{
<userinput>"name": "bar",
"code": 223,
"space": "dhcp4",
"type": "record",
"array": true,
"record-types": "ipv4-address, uint16",
"encapsulate": ""</userinput>
}, ...
],
...
}
</screen>
The new option content is one IPv4 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
......
......@@ -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
......
......@@ -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<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
......
......@@ -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) {
......
......@@ -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));
......
......@@ -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