lib/swoc/unit_tests/ex_ipspace_properties.cc (377 lines of code) (raw):
// SPDX-License-Identifier: Apache-2.0
// Copyright 2014 Network Geographics
/** @file
Example use of IPSpace for property mapping.
*/
#include "catch.hpp"
#include <memory>
#include <limits>
#include <iostream>
#include "swoc/TextView.h"
#include "swoc/swoc_ip.h"
#include "swoc/bwf_ip.h"
#include "swoc/bwf_std.h"
using namespace std::literals;
using namespace swoc::literals;
using swoc::TextView;
using swoc::IPEndpoint;
using swoc::IP4Addr;
using swoc::IP4Range;
using swoc::IP6Addr;
using swoc::IP6Range;
using swoc::IPAddr;
using swoc::IPRange;
using swoc::IPSpace;
using swoc::MemSpan;
using swoc::MemArena;
using W = swoc::LocalBufferWriter<256>;
namespace {
bool Verbose_p =
#if VERBOSE_EXAMPLE_OUTPUT
true
#else
false
#endif
;
} // namespace
TEST_CASE("IPSpace bitset blending", "[libswoc][ipspace][bitset][blending]") {
// Color each address with a set of bits.
using PAYLOAD = std::bitset<32>;
// Declare the IPSpace.
using Space = swoc::IPSpace<PAYLOAD>;
// Example data type.
using Data = std::tuple<TextView, PAYLOAD>;
// Dump the ranges to stdout.
auto dump = [](Space &space) -> void {
if (Verbose_p) {
std::cout << W().print("{} ranges\n", space.count());
for (auto &&[r, payload] : space) {
std::cout << W().print("{:25} : {}\n", r, payload);
}
}
};
// Convert a list of bit indices into a bitset.
auto make_bits = [](std::initializer_list<unsigned> indices) -> PAYLOAD {
PAYLOAD bits;
for (auto idx : indices) {
bits[idx] = true;
}
return bits;
};
// Bitset blend functor which computes a union of the bitsets.
auto blender = [](PAYLOAD &lhs, PAYLOAD const &rhs) -> bool {
lhs |= rhs;
return true;
};
// Example marking functor.
auto marker = [&](Space &space, swoc::MemSpan<Data> ranges) -> void {
// For each test range, compute the bitset from the list of bit indices.
for (auto &&[text, bits] : ranges) {
space.blend(IPRange{text}, bits, blender);
}
};
// The IPSpace instance.
Space space;
// test ranges 1
std::array<Data, 7> ranges_1 = {
{{"100.0.0.0-100.0.0.255", make_bits({0})},
{"100.0.1.0-100.0.1.255", make_bits({1})},
{"100.0.2.0-100.0.2.255", make_bits({2})},
{"100.0.3.0-100.0.3.255", make_bits({3})},
{"100.0.4.0-100.0.4.255", make_bits({4})},
{"100.0.5.0-100.0.5.255", make_bits({5})},
{"100.0.6.0-100.0.6.255", make_bits({6})}}
};
marker(space, MemSpan<Data>{ranges_1.data(), ranges_1.size()});
dump(space);
// test ranges 2
std::array<Data, 3> ranges_2 = {
{{"100.0.0.0-100.0.0.255", make_bits({31})},
{"100.0.1.0-100.0.1.255", make_bits({30})},
{"100.0.2.128-100.0.3.127", make_bits({29})}}
};
marker(space, MemSpan<Data>{ranges_2.data(), ranges_2.size()});
dump(space);
// test ranges 3
std::array<Data, 1> ranges_3 = {{{"100.0.2.0-100.0.4.255", make_bits({2, 3, 29})}}};
marker(space, MemSpan<Data>{ranges_3.data(), ranges_3.size()});
dump(space);
// reset blend functor
auto resetter = [](PAYLOAD &lhs, PAYLOAD const &rhs) -> bool {
auto mask = rhs;
lhs &= mask.flip();
return lhs != 0;
};
// erase bits
space.blend(IPRange{"0.0.0.0-255.255.255.255"}, make_bits({2, 3, 29}), resetter);
dump(space);
// ragged boundaries
space.blend(IPRange{"100.0.2.19-100.0.5.117"}, make_bits({16, 18, 20}), blender);
dump(space);
// bit list blend functor which computes a union of the bitsets.
auto bit_blender = [](PAYLOAD &lhs, std::initializer_list<unsigned> const &rhs) -> bool {
for (auto idx : rhs)
lhs[idx] = true;
return true;
};
std::initializer_list<unsigned> bit_list = {10, 11};
space.blend(IPRange{"0.0.0.1-255.255.255.254"}, bit_list, bit_blender);
dump(space);
}
// ---
/** A "table" is conceptually a table with the rows labeled by IP address and a set of
* property columns that represent data for each IP address.
*/
class Table {
using self_type = Table; ///< Self reference type.
public:
static constexpr char SEP = ','; /// Value separator for input file.
/** A property is the description of data for an address.
* The table consists of an ordered list of properties, each corresponding to a column.
*/
class Property {
using self_type = Property; ///< Self reference type.
public:
/// A handle to an instance.
using Handle = std::unique_ptr<self_type>;
/** Construct an instance.
*
* @param name Property name.
*/
Property(TextView const &name) : _name(name){};
/// Force virtual destructor.
virtual ~Property() = default;
/** The size of the property in bytes.
*
* @return The amount of data needed for a single instance of the property value.
*/
virtual size_t size() const = 0;
/** The index in the table of the property.
*
* @return The column index.
*/
unsigned
idx() const {
return _idx;
}
/** Token persistence.
*
* @return @c true if the token needs to be preserved, @c false if not.
*
* If the token for the value is consumed, this should be left as is. However, if the token
* itself needs to be persistent for the lifetime of the table, this must be overridden to
* return @c true.
*/
virtual bool
needs_localized_token() const {
return false;
}
/// @return The row data offset in bytes for this property.
size_t
offset() const {
return _offset;
}
/** Parse the @a token.
*
* @param token Value from the input file for this property.
* @param span Row data storage for this property.
* @return @c true if @a token was correctly parse, @c false if not.
*
* The table parses the input file and handles the fields in a line. Each value is passed to
* the corresponding property for parsing via this method. The method should update the data
* pointed at by @a span.
*/
virtual bool parse(TextView token, MemSpan<std::byte> span) = 0;
protected:
friend class Table;
TextView _name; ///< Name of the property.
unsigned _idx = std::numeric_limits<unsigned>::max(); ///< Column index.
size_t _offset = std::numeric_limits<size_t>::max(); ///< Offset into a row.
/** Set the column index.
*
* @param idx Index for this property.
* @return @a this.
*
* This is called from @c Table to indicate the column index.
*/
self_type &
assign_idx(unsigned idx) {
_idx = idx;
return *this;
}
/** Set the row data @a offset.
*
* @param offset Offset in bytes.
* @return @a this
*
* This is called from @c Table to store the row data offset.
*/
self_type &
assign_offset(size_t offset) {
_offset = offset;
return *this;
}
};
/// Construct an empty Table.
Table() = default;
/** Add a property column to the table.
*
* @tparam P Property class.
* @param col Column descriptor.
* @return @a A pointer to the property.
*
* The @c Property instance must be owned by the @c Table because changes are made to it specific
* to this instance of @c Table.
*/
template <typename P> P *add_column(std::unique_ptr<P> &&col);
/// A row in the table.
class Row {
using self_type = Row; ///< Self reference type.
public:
/// Default cconstruct an row with uninitialized data.
Row(MemSpan<std::byte> span) : _data(span) {}
/** Extract property specific data from @a this.
*
* @param prop Property that defines the data.
* @return The range of bytes in the row for @a prop.
*/
MemSpan<std::byte> span_for(Property const &prop) const;
protected:
MemSpan<std::byte> _data; ///< Raw row data.
};
/** Parse input.
*
* @param src The source to parse.
* @return @a true if parsing was successful, @c false if not.
*
* In general, @a src will be the contents of a file.
*
* @see swoc::file::load
*/
bool parse(TextView src);
/** Look up @a addr in the table.
*
* @param addr Address to find.
* @return A @c Row for the address, or @c nullptr if not found.
*/
Row *find(IPAddr const &addr);
/// @return The number of ranges in the container.
size_t
size() const {
return _space.count();
}
/** Property for column @a idx.
*
* @param idx Index.
* @return The property.
*/
Property *
column(unsigned idx) {
return _columns[idx].get();
}
protected:
size_t _size = 0; ///< Size of row data.
/// Defined properties for columns.
std::vector<Property::Handle> _columns;
/// IPSpace type.
using space = IPSpace<Row>;
space _space; ///< IPSpace instance.
MemArena _arena; ///< Arena for storing rows.
/** Extract the next token from the line.
*
* @param line Current line [in,out]
* @return Extracted token.
*/
TextView token(TextView &line);
/** Localize view.
*
* @param src View to localize.
* @return The localized view.
*
* This copies @a src to the internal @c MemArena and returns a view of the copied data.
*/
TextView localize(TextView const &src);
};
template <typename P>
P *
Table::add_column(std::unique_ptr<P> &&col) {
auto prop = col.get();
auto idx = _columns.size();
col->assign_offset(_size);
col->assign_idx(idx);
_size += static_cast<Property *>(prop)->size();
_columns.emplace_back(std::move(col));
return prop;
}
TextView
Table::localize(TextView const &src) {
auto span = _arena.alloc(src.size()).rebind<char>();
memcpy(span, src);
return span;
}
TextView
Table::token(TextView &line) {
TextView::size_type idx = 0;
// Characters of interest.
static char constexpr separators[2] = {'"', SEP};
static TextView sep_list{separators, 2};
bool in_quote_p = false;
while (idx < line.size()) {
// Next character of interest.
idx = line.find_first_of(sep_list, idx);
if (TextView::npos == idx) { // nothing interesting left, consume all of @a line.
break;
} else if ('"' == line[idx]) { // quote, skip it and flip the quote state.
in_quote_p = !in_quote_p;
++idx;
} else if (SEP == line[idx]) { // separator.
if (in_quote_p) { // quoted separator, skip and continue.
++idx;
} else { // found token, finish up.
break;
}
}
}
// clip the token from @a src and trim whitespace, quotes
auto zret = line.take_prefix(idx).trim_if(&isspace).trim('"');
return zret;
}
bool
Table::parse(TextView src) {
unsigned line_no = 0;
while (src) {
auto line = src.take_prefix_at('\n').ltrim_if(&isspace);
++line_no;
// skip blank and comment lines.
if (line.empty() || '#' == *line) {
continue;
}
auto range_token = line.take_prefix_at(',');
IPRange range{range_token};
if (range.empty()) {
std::cout << W().print("{} is not a valid range specification.", range_token);
continue; // This is an error, real code should report it.
}
auto span = _arena.alloc(_size).rebind<std::byte>(); // need this broken out.
Row row{span}; // store the original span to preserve it.
for (auto const &col : _columns) {
auto token = this->token(line);
if (col->needs_localized_token()) {
token = this->localize(token);
}
if (!col->parse(token, span.subspan(0, col->size()))) {
std::cout << W().print("Value \"{}\" at index {} on line {} is invalid.", token, col->idx(), line_no);
}
// drop reference to storage used by this column.
span.remove_prefix(col->size());
}
_space.mark(range, std::move(row));
}
return true;
}
auto
Table::find(IPAddr const &addr) -> Row * {
auto spot = _space.find(addr);
return spot == _space.end() ? nullptr : &(spot->payload());
}
bool
operator==(Table::Row const &, Table::Row const &) {
return false;
}
MemSpan<std::byte>
Table::Row::span_for(Table::Property const &prop) const {
return _data.subspan(prop.offset(), prop.size());
}
// ---
/** A set of keys, each of which represents an independent property.
* The set of keys must be specified at construction, keys not in the list are invalid.
*/
class FlagGroupProperty : public Table::Property {
using self_type = FlagGroupProperty; ///< Self reference type.
using super_type = Table::Property; ///< Parent type.
public:
/** Construct with a @a name and a list of @a tags.
*
* @param name of the property
* @param tags List of valid tags that represent attributes.
*
* Input tokens must consist of lists of tokens, each of which is one of the @a tags.
* This is stored so that the exact set of tags present can be retrieved.
*/
FlagGroupProperty(TextView const &name, std::initializer_list<TextView> tags);
/** Check for a tag being present.
*
* @param idx Tag index, as specified in the constructor tag list.
* @param row Row data from the @c Table.
* @return @c true if the tag was present, @c false if not.
*/
bool is_set(Table::Row const &row, unsigned idx) const;
protected:
size_t size() const override; ///< Storeage required in a row.
/** Parse a token.
*
* @param token Token to parse (list of tags).
* @param span Storage for parsed results.
* @return @c true on a successful parse, @c false if not.
*/
bool parse(TextView token, MemSpan<std::byte> span) override;
/// List of tags.
std::vector<TextView> _tags;
};
/** Enumeration property.
* The tokens for this property are assumed to be from a limited set of tags. Each token, the
* value for that row, must be one of those tags. The tags do not need to be specified, but will be
* accumulated as needed. The property supports a maximum of 255 distinct tags.
*/
class EnumProperty : public Table::Property {
using self_type = EnumProperty; ///< Self reference type.
using super_type = Table::Property; ///< Parent type.
using store_type = __uint8_t; ///< Row storage type.
public:
using super_type::super_type; ///< Inherit super type constructors.
/// @return The enumeration tag for this @a row.
TextView operator()(Table::Row const &row) const;
protected:
std::vector<TextView> _tags; ///< Tags in the enumeration.
/// @a return Size of required storage.
size_t
size() const override {
return sizeof(store_type);
}
/** Parse a token.
*
* @param token Token to parse (an enumeration tag).
* @param span Storage for parsed results.
* @return @c true on a successful parse, @c false if not.
*/
bool parse(TextView token, MemSpan<std::byte> span) override;
};
class StringProperty : public Table::Property {
using self_type = StringProperty;
using super_type = Table::Property;
public:
static constexpr size_t SIZE = sizeof(TextView);
using super_type::super_type;
protected:
size_t
size() const override {
return SIZE;
}
bool parse(TextView token, MemSpan<std::byte> span) override;
bool
needs_localized_token() const override {
return true;
}
};
// ---
bool
StringProperty::parse(TextView token, MemSpan<std::byte> span) {
memcpy(span.data(), &token, sizeof(token));
return true;
}
FlagGroupProperty::FlagGroupProperty(TextView const &name, std::initializer_list<TextView> tags) : super_type(name) {
_tags.reserve(tags.size());
for (auto const &tag : tags) {
_tags.emplace_back(tag);
}
}
bool
FlagGroupProperty::parse(TextView token, MemSpan<std::byte> span) {
if ("-"_tv == token) {
return true;
} // marker for no flags.
memset(span, 0);
while (token) {
auto tag = token.take_prefix_at(';');
unsigned j = 0;
for (auto const &key : _tags) {
if (0 == strcasecmp(key, tag)) {
span[j / 8] |= (std::byte{1} << (j % 8));
break;
}
++j;
}
if (j > _tags.size()) {
std::cout << W().print("Tag \"{}\" is not recognized.", tag);
return false;
}
}
return true;
}
bool
FlagGroupProperty::is_set(Table::Row const &row, unsigned idx) const {
auto sp = row.span_for(*this);
return std::byte{0} != ((sp[idx / 8] >> (idx % 8)) & std::byte{1});
}
size_t
FlagGroupProperty::size() const {
return swoc::Scalar<8>(swoc::round_up(_tags.size())).count();
}
bool
EnumProperty::parse(TextView token, MemSpan<std::byte> span) {
// Already got one?
auto spot = std::find_if(_tags.begin(), _tags.end(), [&](TextView const &tag) { return 0 == strcasecmp(token, tag); });
if (spot == _tags.end()) { // nope, add it to the list.
_tags.push_back(token);
spot = std::prev(_tags.end());
}
span.rebind<uint8_t>()[0] = spot - _tags.begin();
return true;
}
TextView
EnumProperty::operator()(Table::Row const &row) const {
auto idx = row.span_for(*this).rebind<store_type>()[0];
return _tags[idx];
}
// ---
TEST_CASE("IPSpace properties", "[libswoc][ip][ex][properties]") {
Table table;
auto flag_names = {"prod"_tv, "dmz"_tv, "internal"_tv};
auto owner = table.add_column(std::make_unique<EnumProperty>("owner"));
auto colo = table.add_column(std::make_unique<EnumProperty>("colo"));
auto flags = table.add_column(std::make_unique<FlagGroupProperty>("flags"_tv, flag_names));
[[maybe_unused]] auto description = table.add_column(std::make_unique<StringProperty>("Description"));
TextView src = R"(10.1.1.0/24,asf,cmi,prod;internal,"ASF core net"
192.168.28.0/25,asf,ind,prod,"Indy Net"
192.168.28.128/25,asf,abq,dmz;internal,"Albuquerque zone"
)";
REQUIRE(true == table.parse(src));
REQUIRE(3 == table.size());
auto row = table.find(IPAddr{"10.1.1.56"});
REQUIRE(nullptr != row);
CHECK(true == flags->is_set(*row, 0));
CHECK(false == flags->is_set(*row, 1));
CHECK(true == flags->is_set(*row, 2));
CHECK("asf"_tv == (*owner)(*row));
row = table.find(IPAddr{"192.168.28.131"});
REQUIRE(row != nullptr);
CHECK("abq"_tv == (*colo)(*row));
};