You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
556 lines
17 KiB
556 lines
17 KiB
/* -*- coding: utf-8 -*-
|
|
* ----------------------------------------------------------------------
|
|
* Copyright © 2010-2013, RedJack, LLC.
|
|
* All rights reserved.
|
|
*
|
|
* Please see the LICENSE.txt file in this distribution for license
|
|
* details.
|
|
* ----------------------------------------------------------------------
|
|
*/
|
|
|
|
#include <libcork/core.h>
|
|
#include <libcork/helpers/errors.h>
|
|
|
|
#include "ipset/bdd/nodes.h"
|
|
#include "ipset/logging.h"
|
|
|
|
|
|
/*-----------------------------------------------------------------------
|
|
* Generic saving logic
|
|
*/
|
|
|
|
/**
|
|
* On disk, we use a different node ID scheme than we do in memory.
|
|
* Terminal node IDs are non-negative, and are equal to the terminal
|
|
* value. Nonterminal node IDs are negative, starting with -1.
|
|
* Nonterminal -1 appears first on disk, then nonterminal -2, and so
|
|
* on.
|
|
*/
|
|
|
|
typedef int serialized_id;
|
|
|
|
|
|
/* forward declaration */
|
|
struct save_data;
|
|
|
|
|
|
/**
|
|
* A callback that outputs any necessary header. Should return an int
|
|
* status code indicating whether the write was successful.
|
|
*/
|
|
|
|
typedef int
|
|
(*write_header_func)(struct save_data *save_data,
|
|
struct ipset_node_cache *cache,
|
|
ipset_node_id root);
|
|
|
|
|
|
/**
|
|
* A callback that outputs any necessary footer. Should return an int
|
|
* status code indicating whether the write was successful.
|
|
*/
|
|
|
|
typedef int
|
|
(*write_footer_func)(struct save_data *save_data,
|
|
struct ipset_node_cache *cache,
|
|
ipset_node_id root);
|
|
|
|
|
|
/**
|
|
* A callback that actually outputs a terminal node to disk. Should
|
|
* return an int status code indicating whether the write was successful.
|
|
*/
|
|
|
|
typedef int
|
|
(*write_terminal_func)(struct save_data *save_data,
|
|
ipset_value terminal_value);
|
|
|
|
|
|
/**
|
|
* A callback that actually outputs a nonterminal node to disk.
|
|
* Should return an int status code indicating whether the write was
|
|
* successful.
|
|
*/
|
|
|
|
typedef int
|
|
(*write_nonterminal_func)(struct save_data *save_data,
|
|
serialized_id serialized_node,
|
|
ipset_variable variable,
|
|
serialized_id serialized_low,
|
|
serialized_id serialized_high);
|
|
|
|
|
|
/**
|
|
* A helper struct containing all of the persistent data items needed
|
|
* during the execution of a save.
|
|
*/
|
|
|
|
struct save_data {
|
|
/* The node cache that we're saving nodes from. */
|
|
struct ipset_node_cache *cache;
|
|
|
|
/* The output stream to save the data to. */
|
|
struct cork_stream_consumer *stream;
|
|
|
|
/* The cache of serialized IDs for any nonterminals that we've
|
|
* encountered so far. */
|
|
struct cork_hash_table *serialized_ids;
|
|
|
|
/* The serialized ID to use for the next nonterminal that we
|
|
* encounter. */
|
|
serialized_id next_serialized_id;
|
|
|
|
/* The callback used to write the file header to the stream. */
|
|
write_header_func write_header;
|
|
|
|
/* The callback used to write the file footer to the stream. */
|
|
write_footer_func write_footer;
|
|
|
|
/* The callback used to write terminals to the stream. */
|
|
write_terminal_func write_terminal;
|
|
|
|
/* The callback used to write nonterminals to the stream. */
|
|
write_nonterminal_func write_nonterminal;
|
|
|
|
/* A pointer to any additional data needed by the callbacks. */
|
|
void *user_data;
|
|
};
|
|
|
|
|
|
/**
|
|
* A helper function for ipset_node_save(). Outputs a nonterminal
|
|
* node in a BDD tree, if we haven't done so already. Ensures that
|
|
* the children of the nonterminal are output before the nonterminal
|
|
* is. Returns the serialized ID of this node.
|
|
*/
|
|
|
|
static int
|
|
save_visit_node(struct save_data *save_data,
|
|
ipset_node_id node_id, serialized_id *dest)
|
|
{
|
|
/* Check whether we've already serialized this node. */
|
|
|
|
struct cork_hash_table_entry *entry;
|
|
bool is_new;
|
|
entry = cork_hash_table_get_or_create
|
|
(save_data->serialized_ids, (void *) (uintptr_t) node_id, &is_new);
|
|
|
|
if (!is_new) {
|
|
*dest = (intptr_t) entry->value;
|
|
return 0;
|
|
} else {
|
|
if (ipset_node_get_type(node_id) == IPSET_TERMINAL_NODE) {
|
|
/* For terminals, there isn't really anything to do — we
|
|
* just output the terminal node and use its value as the
|
|
* serialized ID. */
|
|
|
|
ipset_value value = ipset_terminal_value(node_id);
|
|
|
|
DEBUG("Writing terminal(%d)", value);
|
|
rii_check(save_data->write_terminal(save_data, value));
|
|
entry->value = (void *) (intptr_t) value;
|
|
*dest = value;
|
|
return 0;
|
|
} else {
|
|
/* For nonterminals, we drill down into the node's children
|
|
* first, then output the nonterminal node. */
|
|
|
|
struct ipset_node *node =
|
|
ipset_node_cache_get_nonterminal(save_data->cache, node_id);
|
|
DEBUG("Visiting node %u nonterminal(x%u? %u: %u)",
|
|
node_id, node->variable, node->high, node->low);
|
|
|
|
/* Output the node's nonterminal children before we output
|
|
* the node itself. */
|
|
serialized_id serialized_low;
|
|
serialized_id serialized_high;
|
|
rii_check(save_visit_node(save_data, node->low, &serialized_low));
|
|
rii_check(save_visit_node(save_data, node->high, &serialized_high));
|
|
|
|
/* Output the nonterminal */
|
|
serialized_id result = save_data->next_serialized_id--;
|
|
DEBUG("Writing node %u as serialized node %d = (x%u? %d: %d)",
|
|
node_id, result,
|
|
node->variable, serialized_low, serialized_high);
|
|
|
|
entry->value = (void *) (intptr_t) result;
|
|
*dest = result;
|
|
return save_data->write_nonterminal
|
|
(save_data, result, node->variable,
|
|
serialized_low, serialized_high);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static int
|
|
save_bdd(struct save_data *save_data,
|
|
struct ipset_node_cache *cache, ipset_node_id root)
|
|
{
|
|
/* First, output the file header. */
|
|
|
|
DEBUG("Writing file header");
|
|
rii_check(save_data->write_header(save_data, cache, root));
|
|
|
|
/* The serialized node IDs are different than the in-memory node
|
|
* IDs. This means that, for our nonterminal nodes, we need a
|
|
* mapping from internal node ID to serialized node ID. */
|
|
|
|
DEBUG("Creating file caches");
|
|
save_data->serialized_ids = cork_pointer_hash_table_new(0, 0);
|
|
save_data->next_serialized_id = -1;
|
|
|
|
/* Trace down through the BDD tree, outputting each terminal and
|
|
* nonterminal node as they're encountered. */
|
|
|
|
DEBUG("Writing nodes");
|
|
|
|
serialized_id last_serialized_id;
|
|
ei_check(save_visit_node(save_data, root, &last_serialized_id));
|
|
|
|
/* Finally, output the file footer and cleanup. */
|
|
|
|
DEBUG("Writing file footer");
|
|
ei_check(save_data->write_footer(save_data, cache, root));
|
|
|
|
DEBUG("Freeing file caches");
|
|
cork_hash_table_free(save_data->serialized_ids);
|
|
return 0;
|
|
|
|
error:
|
|
/* If there's an error, clean up the objects that we've created
|
|
* before returning. */
|
|
cork_hash_table_free(save_data->serialized_ids);
|
|
return -1;
|
|
}
|
|
|
|
|
|
/*-----------------------------------------------------------------------
|
|
* Helper functions
|
|
*/
|
|
|
|
/**
|
|
* Write a NUL-terminated string to a stream. If we can't write the
|
|
* string for some reason, return an error.
|
|
*/
|
|
static int
|
|
write_string(struct cork_stream_consumer *stream, const char *str)
|
|
{
|
|
size_t len = strlen(str);
|
|
return cork_stream_consumer_data(stream, str, len, false);
|
|
}
|
|
|
|
|
|
/**
|
|
* Write a big-endian uint8 to a stream. If we can't write the
|
|
* integer for some reason, return an error.
|
|
*/
|
|
static int
|
|
write_uint8(struct cork_stream_consumer *stream, uint8_t val)
|
|
{
|
|
/* for a byte, we don't need to endian-swap */
|
|
return cork_stream_consumer_data(stream, &val, sizeof(uint8_t), false);
|
|
}
|
|
|
|
|
|
/**
|
|
* Write a big-endian uint16 to a stream. If we can't write the
|
|
* integer for some reason, return an error.
|
|
*/
|
|
static int
|
|
write_uint16(struct cork_stream_consumer *stream, uint16_t val)
|
|
{
|
|
CORK_UINT16_HOST_TO_BIG_IN_PLACE(val);
|
|
return cork_stream_consumer_data(stream, &val, sizeof(uint16_t), false);
|
|
}
|
|
|
|
|
|
/**
|
|
* Write a big-endian uint32 to a stream. If we can't write the
|
|
* integer for some reason, return an error.
|
|
*/
|
|
|
|
static int
|
|
write_uint32(struct cork_stream_consumer *stream, uint32_t val)
|
|
{
|
|
CORK_UINT32_HOST_TO_BIG_IN_PLACE(val);
|
|
return cork_stream_consumer_data(stream, &val, sizeof(uint32_t), false);
|
|
}
|
|
|
|
|
|
/**
|
|
* Write a big-endian uint64 to a stream. If we can't write the
|
|
* integer for some reason, return an error.
|
|
*/
|
|
|
|
static int
|
|
write_uint64(struct cork_stream_consumer *stream, uint64_t val)
|
|
{
|
|
CORK_UINT64_HOST_TO_BIG_IN_PLACE(val);
|
|
return cork_stream_consumer_data(stream, &val, sizeof(uint64_t), false);
|
|
}
|
|
|
|
|
|
/*-----------------------------------------------------------------------
|
|
* V1 BDD file
|
|
*/
|
|
|
|
static const char MAGIC_NUMBER[] = "IP set";
|
|
static const size_t MAGIC_NUMBER_LENGTH = sizeof(MAGIC_NUMBER) - 1;
|
|
|
|
|
|
static int
|
|
write_header_v1(struct save_data *save_data,
|
|
struct ipset_node_cache *cache, ipset_node_id root)
|
|
{
|
|
/* Output the magic number for an IP set, and the file format
|
|
* version that we're going to write. */
|
|
rii_check(cork_stream_consumer_data(save_data->stream, NULL, 0, true));
|
|
rii_check(write_string(save_data->stream, MAGIC_NUMBER));
|
|
rii_check(write_uint16(save_data->stream, 0x0001));
|
|
|
|
/* Determine how many reachable nodes there are, to calculate the
|
|
* size of the set. */
|
|
size_t nonterminal_count = ipset_node_reachable_count(cache, root);
|
|
size_t set_size =
|
|
MAGIC_NUMBER_LENGTH + /* magic number */
|
|
sizeof(uint16_t) + /* version number */
|
|
sizeof(uint64_t) + /* length of set */
|
|
sizeof(uint32_t) + /* number of nonterminals */
|
|
(nonterminal_count * /* for each nonterminal: */
|
|
(sizeof(uint8_t) + /* variable number */
|
|
sizeof(uint32_t) + /* low pointer */
|
|
sizeof(uint32_t) /* high pointer */
|
|
));
|
|
|
|
/* If the root is a terminal, we need to add 4 bytes to the set
|
|
* size, for storing the terminal value. */
|
|
if (ipset_node_get_type(root) == IPSET_TERMINAL_NODE) {
|
|
set_size += sizeof(uint32_t);
|
|
}
|
|
|
|
rii_check(write_uint64(save_data->stream, set_size));
|
|
rii_check(write_uint32(save_data->stream, nonterminal_count));
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int
|
|
write_footer_v1(struct save_data *save_data,
|
|
struct ipset_node_cache *cache, ipset_node_id root)
|
|
{
|
|
/* If the root is a terminal node, then we output the terminal value
|
|
* in place of the (nonexistent) list of nonterminal nodes. */
|
|
|
|
if (ipset_node_get_type(root) == IPSET_TERMINAL_NODE) {
|
|
ipset_value value = ipset_terminal_value(root);
|
|
return write_uint32(save_data->stream, value);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int
|
|
write_terminal_v1(struct save_data *save_data, ipset_value terminal_value)
|
|
{
|
|
/* We don't have to write anything out for a terminal in a V1 file,
|
|
* since the terminal's value will be encoded into the node ID
|
|
* wherever it's used. */
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int
|
|
write_nonterminal_v1(struct save_data *save_data,
|
|
serialized_id serialized_node,
|
|
ipset_variable variable,
|
|
serialized_id serialized_low,
|
|
serialized_id serialized_high)
|
|
{
|
|
rii_check(write_uint8(save_data->stream, variable));
|
|
rii_check(write_uint32(save_data->stream, serialized_low));
|
|
rii_check(write_uint32(save_data->stream, serialized_high));
|
|
return 0;
|
|
}
|
|
|
|
|
|
int
|
|
ipset_node_cache_save(struct cork_stream_consumer *stream, struct ipset_node_cache *cache,
|
|
ipset_node_id node)
|
|
{
|
|
struct save_data save_data;
|
|
save_data.cache = cache;
|
|
save_data.stream = stream;
|
|
save_data.write_header = write_header_v1;
|
|
save_data.write_footer = write_footer_v1;
|
|
save_data.write_terminal = write_terminal_v1;
|
|
save_data.write_nonterminal = write_nonterminal_v1;
|
|
return save_bdd(&save_data, cache, node);
|
|
}
|
|
|
|
|
|
/*-----------------------------------------------------------------------
|
|
* GraphViz dot file
|
|
*/
|
|
|
|
static const char *GRAPHVIZ_HEADER =
|
|
"strict digraph bdd {\n";
|
|
|
|
static const char *GRAPHVIZ_FOOTER =
|
|
"}\n";
|
|
|
|
|
|
struct dot_data {
|
|
/* The terminal value to leave out of the dot file. This should be
|
|
* the default value of the set or map. */
|
|
ipset_value default_value;
|
|
|
|
/* A scratch buffer */
|
|
struct cork_buffer scratch;
|
|
};
|
|
|
|
|
|
static int
|
|
write_header_dot(struct save_data *save_data,
|
|
struct ipset_node_cache *cache, ipset_node_id root)
|
|
{
|
|
/* Output the opening clause of the GraphViz script. */
|
|
rii_check(cork_stream_consumer_data(save_data->stream, NULL, 0, true));
|
|
return write_string(save_data->stream, GRAPHVIZ_HEADER);
|
|
}
|
|
|
|
|
|
static int
|
|
write_footer_dot(struct save_data *save_data,
|
|
struct ipset_node_cache *cache, ipset_node_id root)
|
|
{
|
|
/* Output the closing clause of the GraphViz script. */
|
|
return write_string(save_data->stream, GRAPHVIZ_FOOTER);
|
|
}
|
|
|
|
|
|
static int
|
|
write_terminal_dot(struct save_data *save_data, ipset_value terminal_value)
|
|
{
|
|
struct dot_data *dot_data = save_data->user_data;
|
|
|
|
/* If this terminal has the default value, skip it. */
|
|
if (terminal_value == dot_data->default_value) {
|
|
return 0;
|
|
}
|
|
|
|
/* Output a node for the terminal value. */
|
|
cork_buffer_printf
|
|
(&dot_data->scratch,
|
|
" t%d [shape=box, label=%d];\n",
|
|
terminal_value, terminal_value);
|
|
return write_string(save_data->stream, dot_data->scratch.buf);
|
|
}
|
|
|
|
|
|
static int
|
|
write_nonterminal_dot(struct save_data *save_data,
|
|
serialized_id serialized_node,
|
|
ipset_variable variable,
|
|
serialized_id serialized_low,
|
|
serialized_id serialized_high)
|
|
{
|
|
struct dot_data *dot_data = save_data->user_data;
|
|
|
|
/* Include a node for the nonterminal value. */
|
|
cork_buffer_printf
|
|
(&dot_data->scratch,
|
|
" n%d [shape=circle,label=%u];\n",
|
|
(-serialized_node), variable);
|
|
|
|
/* Include an edge for the low pointer. */
|
|
if (serialized_low < 0) {
|
|
/* The low pointer is a nonterminal. */
|
|
cork_buffer_append_printf
|
|
(&dot_data->scratch,
|
|
" n%d -> n%d",
|
|
(-serialized_node), (-serialized_low));
|
|
} else {
|
|
/* The low pointer is a terminal. */
|
|
ipset_value low_value = (ipset_value) serialized_low;
|
|
|
|
if (low_value == dot_data->default_value) {
|
|
/* The terminal is the default value, so instead of a real
|
|
* terminal, connect this pointer to a dummy circle node. */
|
|
cork_buffer_append_printf
|
|
(&dot_data->scratch,
|
|
" low%d [shape=circle,label=\"\"]\n"
|
|
" n%d -> low%d",
|
|
(-serialized_node), (-serialized_node), (-serialized_node));
|
|
} else {
|
|
/* The terminal isn't a default, so go ahead and output it. */
|
|
cork_buffer_append_printf
|
|
(&dot_data->scratch,
|
|
" n%d -> t%d",
|
|
(-serialized_node), serialized_low);
|
|
}
|
|
}
|
|
|
|
cork_buffer_append_printf
|
|
(&dot_data->scratch, " [style=dashed,color=red]\n");
|
|
|
|
/* Include an edge for the high pointer. */
|
|
if (serialized_high < 0) {
|
|
/* The high pointer is a nonterminal. */
|
|
cork_buffer_append_printf
|
|
(&dot_data->scratch,
|
|
" n%d -> n%d",
|
|
(-serialized_node), (-serialized_high));
|
|
} else {
|
|
/* The high pointer is a terminal. */
|
|
ipset_value high_value = (ipset_value) serialized_high;
|
|
|
|
if (high_value == dot_data->default_value) {
|
|
/* The terminal is the default value, so instead of a real
|
|
* terminal, connect this pointer to a dummy circle node. */
|
|
cork_buffer_append_printf
|
|
(&dot_data->scratch,
|
|
" high%d "
|
|
"[shape=circle,"
|
|
"fixedsize=true,"
|
|
"height=0.25,"
|
|
"width=0.25,"
|
|
"label=\"\"]\n"
|
|
" n%d -> high%d",
|
|
(-serialized_node), (-serialized_node), (-serialized_node));
|
|
} else {
|
|
/* The terminal isn't a default, so go ahead and output it. */
|
|
cork_buffer_append_printf
|
|
(&dot_data->scratch,
|
|
" n%d -> t%d",
|
|
(-serialized_node), serialized_high);
|
|
}
|
|
}
|
|
|
|
cork_buffer_append_printf
|
|
(&dot_data->scratch, " [style=solid,color=black]\n");
|
|
|
|
/* Output the clauses to the stream. */
|
|
return write_string(save_data->stream, dot_data->scratch.buf);
|
|
}
|
|
|
|
|
|
int
|
|
ipset_node_cache_save_dot(struct cork_stream_consumer *stream,
|
|
struct ipset_node_cache *cache, ipset_node_id node)
|
|
{
|
|
struct dot_data dot_data = {
|
|
0 /* default value */
|
|
};
|
|
|
|
struct save_data save_data;
|
|
save_data.cache = cache;
|
|
save_data.stream = stream;
|
|
save_data.write_header = write_header_dot;
|
|
save_data.write_footer = write_footer_dot;
|
|
save_data.write_terminal = write_terminal_dot;
|
|
save_data.write_nonterminal = write_nonterminal_dot;
|
|
save_data.user_data = &dot_data;
|
|
return save_bdd(&save_data, cache, node);
|
|
}
|