diff --git a/doc/examples/kea4/advanced.json b/doc/examples/kea4/advanced.json index 4a15f2cda3..d7bb7a432d 100644 --- a/doc/examples/kea4/advanced.json +++ b/doc/examples/kea4/advanced.json @@ -44,6 +44,14 @@ "lfc-interval": 3600 }, + // This defines a control socket. If defined, Kea will open a UNIX socket + // and will listen for incoming commands. See section 15 of the Kea User's + // Guide for list of supported commands. + "control-socket": { + "socket-type": "unix", + "socket-name": "/tmp/kea4-ctrl-socket" + }, + // Addresses will be assigned with a lifetime of 4000 seconds. // The client is told to start renewing after 1000 seconds. If the server // does not respond within 2000 seconds of the lease being granted, client @@ -83,7 +91,14 @@ }, { "pools": [ { "pool": "192.0.4.1 - 192.0.4.254" } ], - "subnet": "192.0.4.0/24" + "subnet": "192.0.4.0/24", + + // Sometimes the relay may use an IPv4 address that's not matching + // the subnet. This is discouraged, but there are valid cases when it + // makes sense. One case is when there is a shared subnet. + "relay": { + "ip-address": "192.168.1.1" + } } ] }, diff --git a/doc/examples/kea6/advanced.json b/doc/examples/kea6/advanced.json index b68877517b..5468a1cad2 100644 --- a/doc/examples/kea6/advanced.json +++ b/doc/examples/kea6/advanced.json @@ -1,77 +1,90 @@ -# This is an example configuration file for DHCPv6 server in Kea. -# It attempts to showcase some of the more advanced features. -# Topology wise, it's a basic scenario with one IPv6 subnet configured. -# It is assumed that one subnet (2001:db8:1::/64) is available directly -# over ethX interface. -# -# The following features are currently showcased here: -# 1. Configuration of MAC/hardware address sources in DHCPv6 +// This is an example configuration file for DHCPv6 server in Kea. +// It attempts to showcase some of the more advanced features. +// Topology wise, it's a basic scenario with one IPv6 subnet configured. +// It is assumed that one subnet (2001:db8:1::/64) is available directly +// over ethX interface. +// +// The following features are currently showcased here: +// 1. Configuration of MAC/hardware address sources in DHCPv6 +// 2. RSOO (Relay supplied options) - Some relays may insert options with the +// intention for the server to insert them into client directed messages. +// 3. Control socket. Kea can open a socket and listen for incoming +// commands. { "Dhcp6": { -# Kea is told to listen on ethX network interface only. + // Kea is told to listen on ethX network interface only. "interfaces-config": { "interfaces": [ "ethX" ] }, -# We need to specify the the database used to store leases. As of -# September 2016, four database backends are supported: MySQL, -# PostgreSQL, Cassandra, and the in-memory database, Memfile. -# We will use memfile because it doesn't require any prior set up. + // We need to specify the the database used to store leases. As of + // September 2016, four database backends are supported: MySQL, + // PostgreSQL, Cassandra, and the in-memory database, Memfile. + // We will use memfile because it doesn't require any prior set up. "lease-database": { "type": "memfile", "lfc-interval": 3600 }, -# Kea 0.9.1 introduced MAC/hardware addresses support in DHCPv6. There is -# no single reliable method of getting MAC address information in DHCPv6. -# Kea supports several methods. Depending on your network set up, some -# methods may be more preferable than others, hence the configuration -# parameter. 'mac-sources' is a list of methods. Allowed parameters are: -# any, raw, duid, ipv6-link-local, client-link-addr-option, rfc6939 (which -# is an alias for client-link-addr-option), remote-id, rfc4649 (which is an -# alias for remote-id, subscriber-id, rfc4580 (which is an alias for -# subscriber-id) and docsis. -# -# Note that the order matters. Methods are attempted one by one in the order -# specified until hardware address is obtained. If you don't care which method -# is used, using 'any' is marginally faster than enumerating them all. -# -# If mac-sources are not specified, a default value of 'any' is used. +// Kea 0.9.1 introduced MAC/hardware addresses support in DHCPv6. There is +// no single reliable method of getting MAC address information in DHCPv6. +// Kea supports several methods. Depending on your network set up, some +// methods may be more preferable than others, hence the configuration +// parameter. 'mac-sources' is a list of methods. Allowed parameters are: +// any, raw, duid, ipv6-link-local, client-link-addr-option, rfc6939 (which +// is an alias for client-link-addr-option), remote-id, rfc4649 (which is an +// alias for remote-id, subscriber-id, rfc4580 (which is an alias for +// subscriber-id) and docsis. +// +// Note that the order matters. Methods are attempted one by one in the order +// specified until hardware address is obtained. If you don't care which method +// is used, using 'any' is marginally faster than enumerating them all. +// +// If mac-sources are not specified, a default value of 'any' is used. "mac-sources": [ "client-link-addr-option", "duid", "ipv6-link-local" ], -# RFC6422 defines a mechanism called relay-supplied options option. The relay -# agent may insert certain options that the server will echo back to the -# client, if certain criteria are met. One condition is that the option must -# be RSOO-enabled (i.e. allowed to be echoed back). IANA maintains a list -# of those options here: -# http://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml#options-relay-supplied -# However, it is possible to allow the server to echo back additional options. -# This entry marks options 110, 120 and 130 as RSOO-enabled. +// RFC6422 defines a mechanism called relay-supplied options option. The relay +// agent may insert certain options that the server will echo back to the +// client, if certain criteria are met. One condition is that the option must +// be RSOO-enabled (i.e. allowed to be echoed back). IANA maintains a list +// of those options here: +// http://www.iana.org/assignments/dhcpv6-parameters/dhcpv6-parameters.xhtml#options-relay-supplied +// However, it is possible to allow the server to echo back additional options. +// This entry marks options 110, 120 and 130 as RSOO-enabled. "relay-supplied-options": [ "110", "120", "130" ], -# Addresses will be assigned with preferred and valid lifetimes -# being 3000 and 4000, respectively. Client is told to start -# renewing after 1000 seconds. If the server does not respond -# after 2000 seconds since the lease was granted, client is supposed -# to start REBIND procedure (emergency renewal that allows switching -# to a different server). + + // This defines a control socket. If defined, Kea will open a UNIX socket + // and will listen for incoming commands. See section 15 of the Kea User's + // Guide for list of supported commands. + "control-socket": { + "socket-type": "unix", + "socket-name": "/tmp/kea6-ctrl-socket" + }, + +// Addresses will be assigned with preferred and valid lifetimes +// being 3000 and 4000, respectively. Client is told to start +// renewing after 1000 seconds. If the server does not respond +// after 2000 seconds since the lease was granted, client is supposed +// to start REBIND procedure (emergency renewal that allows switching +// to a different server). "preferred-lifetime": 3000, "valid-lifetime": 4000, "renew-timer": 1000, "rebind-timer": 2000, -# The following list defines subnets. Each subnet consists of at -# least subnet and pool entries. +// The following list defines subnets. Each subnet consists of at +// least subnet and pool entries. "subnet6": [ { "pools": [ { "pool": "2001:db8:1::/80" } ], -# This defines PD (prefix delegation) pools. In this case -# we have only one pool. That consists of /64 prefixes -# being delegated out of large /48 pool. Each delegated -# prefix will contain an excluded-prefix option. +// This defines PD (prefix delegation) pools. In this case +// we have only one pool. That consists of /64 prefixes +// being delegated out of large /48 pool. Each delegated +// prefix will contain an excluded-prefix option. "pd-pools": [ { "prefix": "2001:db8:abcd::", @@ -82,13 +95,21 @@ } ], "subnet": "2001:db8:1::/64", - "interface": "ethX" + "interface": "ethX", + + // Sometimes the relay may use an odd IPv6 address that's not matching + // the subnet. This is discouraged, but there are valid cases when it + // makes sense. One case is when the relay has only link-local address + // and another is when there is a shared subnet scenario. + "relay": { + "ip-address": "3000::1" + } } ] }, -# The following configures logging. It assumes that messages with at least -# informational level (info, warn, error and fatal) should be logged to stdout. +// The following configures logging. It assumes that messages with at least +// informational level (info, warn, error and fatal) should be logged to stdout. "Logging": { "loggers": [ { diff --git a/doc/guide/dhcp6-srv.xml b/doc/guide/dhcp6-srv.xml index 1c5153ebae..3c8064bc7f 100644 --- a/doc/guide/dhcp6-srv.xml +++ b/doc/guide/dhcp6-srv.xml @@ -3500,7 +3500,8 @@ If not specified, the default value is: When not specified, a special value of "any" is used, which instructs the server to attempt to use all the methods in sequence and use - value returned by the first one that succeeds. + value returned by the first one that succeeds. If specified, it + has to have at least one value. Supported methods are: diff --git a/src/bin/dhcp4/json_config_parser.cc b/src/bin/dhcp4/json_config_parser.cc index 427ecf4fc6..20725b8277 100644 --- a/src/bin/dhcp4/json_config_parser.cc +++ b/src/bin/dhcp4/json_config_parser.cc @@ -189,8 +189,7 @@ protected: parser = new StringParser(config_id, string_values_); } else if (config_id.compare("pools") == 0) { parser = new Pools4ListParser(config_id, pools_); - } else if (config_id.compare("relay") == 0) { - parser = new RelayInfoParser(config_id, relay_info_, Option::V4); + // relay has been converted to SimpleParser already. // option-data has been converted to SimpleParser already. } else if (config_id.compare("match-client-id") == 0) { parser = new BooleanParser(config_id, boolean_values_); @@ -440,8 +439,7 @@ DhcpConfigParser* createGlobalDhcp4ConfigParser(const std::string& config_id, parser = new D2ClientConfigParser(config_id); } else if (config_id.compare("match-client-id") == 0) { parser = new BooleanParser(config_id, globalContext()->boolean_values_); - } else if (config_id.compare("control-socket") == 0) { - parser = new ControlSocketParser(config_id); + // control-socket has been converted to SimpleParser already. } else if (config_id.compare("expired-leases-processing") == 0) { parser = new ExpirationConfigParser(); } else if (config_id.compare("client-classes") == 0) { @@ -596,6 +594,13 @@ configureDhcp4Server(Dhcpv4Srv&, isc::data::ConstElementPtr config_set) { continue; } + if (config_pair.first == "control-socket") { + ControlSocketParser parser; + SrvConfigPtr srv_cfg = CfgMgr::instance().getStagingCfg(); + parser.parse(*srv_cfg, config_pair.second); + continue; + } + ParserPtr parser(createGlobalDhcp4ConfigParser(config_pair.first, config_pair.second)); LOG_DEBUG(dhcp4_logger, DBG_DHCP4_DETAIL, DHCP4_PARSER_CREATED) diff --git a/src/bin/dhcp4/tests/parser_unittest.cc b/src/bin/dhcp4/tests/parser_unittest.cc index d39652e9fd..7edf6d5a15 100644 --- a/src/bin/dhcp4/tests/parser_unittest.cc +++ b/src/bin/dhcp4/tests/parser_unittest.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2016 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -7,14 +7,14 @@ #include #include #include -#include -#include -#include +#include using namespace isc::data; using namespace std; -namespace { +namespace isc { +namespace dhcp { +namespace test { /// @brief compares two JSON trees /// @@ -128,6 +128,8 @@ TEST(ParserTest, keywordDhcp4) { testParser(txt, Parser4Context::PARSER_DHCP4); } +// Tests if bash (#) comments are supported. That's the only comment type that +// was supported by the old parser. TEST(ParserTest, bashComments) { string txt= "{ \"Dhcp4\": { \"interfaces-config\": {" " \"interfaces\": [ \"*\" ]" @@ -146,7 +148,8 @@ TEST(ParserTest, bashComments) { testParser(txt, Parser4Context::PARSER_DHCP4, false); } -TEST(ParserTest, cComments) { +// Tests if C++ (//) comments can start anywhere, not just in the first line. +TEST(ParserTest, cppComments) { string txt= "{ \"Dhcp4\": { \"interfaces-config\": {" " \"interfaces\": [ \"*\" ]" "},\n" @@ -161,6 +164,7 @@ TEST(ParserTest, cComments) { testParser(txt, Parser4Context::PARSER_DHCP4, false); } +// Tests if bash (#) comments can start anywhere, not just in the first line. TEST(ParserTest, bashCommentsInline) { string txt= "{ \"Dhcp4\": { \"interfaces-config\": {" " \"interfaces\": [ \"*\" ]" @@ -176,6 +180,7 @@ TEST(ParserTest, bashCommentsInline) { testParser(txt, Parser4Context::PARSER_DHCP4, false); } +// Tests if multi-line C style comments are handled correctly. TEST(ParserTest, multilineComments) { string txt= "{ \"Dhcp4\": { \"interfaces-config\": {" " \"interfaces\": [ \"*\" ]" @@ -193,89 +198,13 @@ TEST(ParserTest, multilineComments) { testParser(txt, Parser4Context::PARSER_DHCP4, false); } -/// @brief removes comments from a JSON file -/// -/// This is rather naive implementation, but it's probably sufficient for -/// testing. It won't be able to pick any trickier cases, like # or // -/// appearing in strings, nested C++ comments etc. -/// -/// @param input_file file to be stripped of comments -/// @return a new file that has comments stripped from it -std::string decommentJSONfile(const std::string& input_file) { - ifstream f(input_file); - if (!f.is_open()) { - isc_throw(isc::BadValue, "can't open input file for reading: " + input_file); - } - - string outfile; - size_t last_slash = input_file.find_last_of("/"); - if (last_slash != string::npos) { - outfile = input_file.substr(last_slash + 1); - } else { - outfile = input_file; - } - outfile += "-decommented"; - - ofstream out(outfile); - if (!out.is_open()) { - isc_throw(isc::BadValue, "can't open output file for writing: " + input_file); - } - - bool in_comment = false; - string line; - while (std::getline(f, line)) { - // First, let's get rid of the # comments - size_t hash_pos = line.find("#"); - if (hash_pos != string::npos) { - line = line.substr(0, hash_pos); - } - - // Second, let's get rid of the // comments - size_t dblslash_pos = line.find("//"); - if (dblslash_pos != string::npos) { - line = line.substr(0, dblslash_pos); - } - - // Now the tricky part: c comments. - size_t begin_pos = line.find("/*"); - size_t end_pos = line.find("*/"); - if (in_comment && end_pos == string::npos) { - // we continue through multiline comment - line = ""; - } else { - - if (begin_pos != string::npos) { - in_comment = true; - if (end_pos != string::npos) { - // sigle line comment. Let's get rid of the content in between - line = line.replace(begin_pos, end_pos + 2, end_pos + 2 - begin_pos, ' '); - in_comment = false; - } else { - line = line.substr(0, begin_pos); - } - } else { - if (in_comment && end_pos != string::npos) { - line = line.replace(0, end_pos +2 , end_pos + 2, ' '); - in_comment = false; - } - } - } - - // Finally, write the line to the output file. - out << line << endl; - } - f.close(); - out.close(); - - return (outfile); -} /// @brief Loads specified example config file /// /// This test loads specified example file twice: first, using the legacy /// JSON file and then second time using bison parser. Two created Element /// trees are then compared. The input is decommented before it is passed -/// to legacy parser (as its support for comments is very limited). +/// to legacy parser (as legacy support for comments is very limited). /// /// @param fname name of the file to be loaded void testFile(const std::string& fname) { @@ -284,8 +213,7 @@ void testFile(const std::string& fname) { string decommented = decommentJSONfile(fname); - cout << "Attempting to load file " << fname << " (" << decommented - << ")" << endl; + cout << "Parsing file " << fname << " (" << decommented << ")" << endl; EXPECT_NO_THROW(reference_json = Element::fromJSONFile(decommented, true)); @@ -305,6 +233,8 @@ void testFile(const std::string& fname) { ASSERT_TRUE(test_json); compareJSON(reference_json, test_json); + + unlink(decommented); } // This test loads all available existing files. Each config is loaded @@ -329,6 +259,11 @@ TEST(ParserTest, file) { } } +/// @brief Tests error conditions in Dhcp4Parser +/// +/// @param txt text to be parsed +/// @param parser_type type of the parser to be used in the test +/// @param msg expected content of the exception void testError(const std::string& txt, Parser4Context::ParserType parser_type, const std::string& msg) @@ -347,7 +282,7 @@ void testError(const std::string& txt, } } -// Check errors +// Verify that error conditions are handled correctly. TEST(ParserTest, errors) { // no input testError("", Parser4Context::PARSER_JSON, @@ -609,3 +544,5 @@ TEST(ParserTest, unicodeSlash) { } }; +}; +}; diff --git a/src/bin/dhcp6/json_config_parser.cc b/src/bin/dhcp6/json_config_parser.cc index 84b410791d..e9083e0652 100644 --- a/src/bin/dhcp6/json_config_parser.cc +++ b/src/bin/dhcp6/json_config_parser.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2012-2017 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -424,8 +424,7 @@ protected: parser = new StringParser(config_id, string_values_); } else if (config_id.compare("pools") == 0) { parser = new Pools6ListParser(config_id, pools_); - } else if (config_id.compare("relay") == 0) { - parser = new RelayInfoParser(config_id, relay_info_, Option::V6); + // relay has been converted to SimpleParser. } else if (config_id.compare("pd-pools") == 0) { parser = new PdPoolListParser(config_id, pools_); // option-data was here, but it is now converted to SimpleParser @@ -718,13 +717,10 @@ DhcpConfigParser* createGlobal6DhcpConfigParser(const std::string& config_id, parser = new HooksLibrariesParser(config_id); } else if (config_id.compare("dhcp-ddns") == 0) { parser = new D2ClientConfigParser(config_id); - } else if (config_id.compare("mac-sources") == 0) { - parser = new MACSourcesListConfigParser(config_id, - globalContext()); + // mac-source has been converted to SimpleParser. } else if (config_id.compare("relay-supplied-options") == 0) { parser = new RSOOListConfigParser(config_id); - } else if (config_id.compare("control-socket") == 0) { - parser = new ControlSocketParser(config_id); + // control-socket has been converted to SimpleParser. } else if (config_id.compare("expired-leases-processing") == 0) { parser = new ExpirationConfigParser(); } else if (config_id.compare("client-classes") == 0) { @@ -870,6 +866,20 @@ configureDhcp6Server(Dhcpv6Srv&, isc::data::ConstElementPtr config_set) { continue; } + if (config_pair.first == "mac-sources") { + MACSourcesListConfigParser parser; + CfgMACSource& mac_source = CfgMgr::instance().getStagingCfg()->getMACSources(); + parser.parse(mac_source, config_pair.second); + continue; + } + + if (config_pair.first == "control-socket") { + ControlSocketParser parser; + SrvConfigPtr srv_config = CfgMgr::instance().getStagingCfg(); + parser.parse(*srv_config, config_pair.second); + continue; + } + ParserPtr parser(createGlobal6DhcpConfigParser(config_pair.first, config_pair.second)); LOG_DEBUG(dhcp6_logger, DBG_DHCP6_DETAIL, DHCP6_PARSER_CREATED) diff --git a/src/bin/dhcp6/tests/config_parser_unittest.cc b/src/bin/dhcp6/tests/config_parser_unittest.cc index 051486c3c4..11a0651cf0 100644 --- a/src/bin/dhcp6/tests/config_parser_unittest.cc +++ b/src/bin/dhcp6/tests/config_parser_unittest.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2012-2017 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -762,7 +762,7 @@ TEST_F(Dhcp6ParserTest, emptySubnet) { ConstElementPtr status; EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json)); - + // returned value should be 0 (success) checkResult(status, 0); } @@ -4327,11 +4327,9 @@ TEST_F(Dhcp6ParserTest, macSources) { EXPECT_EQ(HWAddr::HWADDR_SOURCE_SUBSCRIBER_ID, mac_sources[5]); } -/// The goal of this test is to verify that MAC sources configuration can be -/// empty. -/// Note the Dhcp6 parser requires the list to NOT be empty?! +/// The goal of this test is to verify that empty MAC sources configuration +/// is rejected. If specified, this has to have at least one value. TEST_F(Dhcp6ParserTest, macSourcesEmpty) { - ConstElementPtr status; EXPECT_NO_THROW(status = configureDhcp6Server(srv_, @@ -4343,11 +4341,9 @@ TEST_F(Dhcp6ParserTest, macSourcesEmpty) { "\"subnet6\": [ ], " "\"valid-lifetime\": 4000 }"))); - // returned value should be 0 (success) - checkResult(status, 0); - - CfgMACSources mac_sources = CfgMgr::instance().getStagingCfg()->getMACSources().get(); - EXPECT_EQ(0, mac_sources.size()); + // returned value should be 1 (failure), because the mac-sources must not + // be empty when specified. + checkResult(status, 1); } /// The goal of this test is to verify that MAC sources configuration can diff --git a/src/bin/dhcp6/tests/parser_unittest.cc b/src/bin/dhcp6/tests/parser_unittest.cc index 75bc5fdbd3..a11ea63100 100644 --- a/src/bin/dhcp6/tests/parser_unittest.cc +++ b/src/bin/dhcp6/tests/parser_unittest.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2016 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -7,23 +7,38 @@ #include #include #include +#include using namespace isc::data; using namespace std; -namespace { +namespace isc { +namespace dhcp { +namespace test { -void compareJSON(ConstElementPtr a, ConstElementPtr b, bool print = true) { +/// @brief compares two JSON trees +/// +/// If differences are discovered, gtest failure is reported (using EXPECT_EQ) +/// +/// @param a first to be compared +/// @param b second to be compared +void compareJSON(ConstElementPtr a, ConstElementPtr b) { ASSERT_TRUE(a); ASSERT_TRUE(b); - if (print) { - // std::cout << "JSON A: -----" << endl << a->str() << std::endl; - // std::cout << "JSON B: -----" << endl << b->str() << std::endl; - // cout << "---------" << endl << endl; - } EXPECT_EQ(a->str(), b->str()); } +/// @brief Tests if the input string can be parsed with specific parser +/// +/// The input text will be passed to bison parser of specified type. +/// Then the same input text is passed to legacy JSON parser and outputs +/// from both parsers are compared. The legacy comparison can be disabled, +/// if the feature tested is not supported by the old parser (e.g. +/// new comment styles) +/// +/// @param txt text to be compared +/// @param parser_type bison parser type to be instantiated +/// @param compare whether to compare the output with legacy JSON parser void testParser(const std::string& txt, Parser6Context::ParserType parser_type) { ElementPtr reference_json; ConstElementPtr test_json; @@ -44,25 +59,6 @@ void testParser(const std::string& txt, Parser6Context::ParserType parser_type) compareJSON(reference_json, test_json); } -void testParser2(const std::string& txt, Parser6Context::ParserType parser_type) { - ConstElementPtr test_json; - - ASSERT_NO_THROW({ - try { - Parser6Context ctx; - test_json = ctx.parseString(txt, parser_type); - } catch (const std::exception &e) { - cout << "EXCEPTION: " << e.what() << endl; - throw; - } - }); - /// @todo: Implement actual validation here. since the original - /// Element::fromJSON does not support several comment types, we don't - /// have anything to compare with. - /// std::cout << "Original text:" << txt << endl; - /// std::cout << "Parsed text :" << test_json->str() << endl; -} - TEST(ParserTest, mapInMap) { string txt = "{ \"xyzzy\": { \"foo\": 123, \"baz\": 456 } }"; testParser(txt, Parser6Context::PARSER_JSON); @@ -125,9 +121,11 @@ TEST(ParserTest, keywordDhcp6) { " \"subnet\": \"2001:db8:1::/48\", " " \"interface\": \"test\" } ],\n" "\"valid-lifetime\": 4000 } }"; - testParser2(txt, Parser6Context::PARSER_DHCP6); + testParser(txt, Parser6Context::PARSER_DHCP6); } +// Tests if bash (#) comments are supported. That's the only comment type that +// was supported by the old parser. TEST(ParserTest, bashComments) { string txt= "{ \"Dhcp6\": { \"interfaces-config\": {" " \"interfaces\": [ \"*\" ]" @@ -144,10 +142,11 @@ TEST(ParserTest, bashComments) { " \"interface\": \"eth0\"" " } ]," "\"valid-lifetime\": 4000 } }"; - testParser2(txt, Parser6Context::PARSER_DHCP6); + testParser(txt, Parser6Context::PARSER_DHCP6); } -TEST(ParserTest, cComments) { +// Tests if C++ (//) comments can start anywhere, not just in the first line. +TEST(ParserTest, cppComments) { string txt= "{ \"Dhcp6\": { \"interfaces-config\": {" " \"interfaces\": [ \"*\" ]" "},\n" @@ -163,6 +162,7 @@ TEST(ParserTest, cComments) { testParser2(txt, Parser6Context::PARSER_DHCP6); } +// Tests if bash (#) comments can start anywhere, not just in the first line. TEST(ParserTest, bashCommentsInline) { string txt= "{ \"Dhcp6\": { \"interfaces-config\": {" " \"interfaces\": [ \"*\" ]" @@ -179,6 +179,7 @@ TEST(ParserTest, bashCommentsInline) { testParser2(txt, Parser6Context::PARSER_DHCP6); } +// Tests if multi-line C style comments are handled correctly. TEST(ParserTest, multilineComments) { string txt= "{ \"Dhcp6\": { \"interfaces-config\": {" " \"interfaces\": [ \"*\" ]" @@ -196,14 +197,23 @@ TEST(ParserTest, multilineComments) { testParser2(txt, Parser6Context::PARSER_DHCP6); } - -void testFile(const std::string& fname, bool print) { +/// @brief Loads specified example config file +/// +/// This test loads specified example file twice: first, using the legacy +/// JSON file and then second time using bison parser. Two created Element +/// trees are then compared. The input is decommented before it is passed +/// to legacy parser (as legacy support for comments is very limited). +/// +/// @param fname name of the file to be loaded +void testFile(const std::string& fname) { ElementPtr reference_json; ConstElementPtr test_json; - cout << "Attempting to load file " << fname << endl; + string decommented = decommentJSONfile(fname); - EXPECT_NO_THROW(reference_json = Element::fromJSONFile(fname, true)); + cout << "Parsing file " << fname << "(" << decommented << ")" << endl; + + EXPECT_NO_THROW(reference_json = Element::fromJSONFile(decommented, true)); EXPECT_NO_THROW( try { @@ -217,9 +227,9 @@ void testFile(const std::string& fname, bool print) { ASSERT_TRUE(reference_json); ASSERT_TRUE(test_json); - compareJSON(reference_json, test_json, print); - + compareJSON(reference_json, test_json); + unlink(decommented.c_str()); } // This test loads all available existing files. Each config is loaded @@ -243,10 +253,15 @@ TEST(ParserTest, file) { configs.push_back("stateless.json"); for (int i = 0; igetType()); EXPECT_EQ(ins, result->stringValue()); } +} +// This test checks that all representations of a slash is recognized properly. +TEST(ParserTest, unicodeSlash) { // check the 4 possible encodings of solidus '/' - json = "\"/\\/\\u002f\\u002F\""; + ConstElementPtr result; + string json = "\"/\\/\\u002f\\u002F\""; ASSERT_NO_THROW( try { Parser6Context ctx; @@ -520,6 +539,8 @@ TEST(ParserTest, unicodeEscapes) { }); ASSERT_EQ(Element::string, result->getType()); EXPECT_EQ("////", result->stringValue()); -} +} }; +}; +}; diff --git a/src/lib/dhcpsrv/parsers/dhcp_parsers.cc b/src/lib/dhcpsrv/parsers/dhcp_parsers.cc index f3d58d5b52..c2e0389d80 100644 --- a/src/lib/dhcpsrv/parsers/dhcp_parsers.cc +++ b/src/lib/dhcpsrv/parsers/dhcp_parsers.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2013-2016 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2013-2017 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -177,62 +177,45 @@ template <> void ValueParser::build(ConstElementPtr value) { // ******************** MACSourcesListConfigParser ************************* -MACSourcesListConfigParser:: -MACSourcesListConfigParser(const std::string& param_name, - ParserContextPtr global_context) - : param_name_(param_name), global_context_(global_context) { - if (param_name_ != "mac-sources") { - isc_throw(BadValue, "Internal error. MAC sources configuration " - "parser called for the wrong parameter: " << param_name); - } -} - void -MACSourcesListConfigParser::build(ConstElementPtr value) { +MACSourcesListConfigParser::parse(CfgMACSource& mac_sources, ConstElementPtr value) { CfgIface cfg_iface; uint32_t source = 0; + size_t cnt = 0; // By default, there's only one source defined: ANY. // If user specified anything, we need to get rid of that default. - CfgMgr::instance().getStagingCfg()->getMACSources().clear(); + mac_sources.clear(); BOOST_FOREACH(ConstElementPtr source_elem, value->listValue()) { std::string source_str = source_elem->stringValue(); try { source = CfgMACSource::MACSourceFromText(source_str); - CfgMgr::instance().getStagingCfg()->getMACSources().add(source); + mac_sources.add(source); + ++cnt; } catch (const std::exception& ex) { isc_throw(DhcpConfigError, "Failed to convert '" << source_str << "' to any recognized MAC source:" << ex.what() << " (" << value->getPosition() << ")"); } } -} -void -MACSourcesListConfigParser::commit() { - // Nothing to do. + if (!cnt) { + isc_throw(DhcpConfigError, "If specified, MAC Sources cannot be empty"); + } } // ******************** ControlSocketParser ************************* -ControlSocketParser::ControlSocketParser(const std::string& param_name) { - if (param_name != "control-socket") { - isc_throw(BadValue, "Internal error. Control socket parser called " - " for wrong parameter:" << param_name); +void ControlSocketParser::parse(SrvConfig& srv_cfg, isc::data::ConstElementPtr value) { + if (!value) { + isc_throw(DhcpConfigError, "Logic error: specified control-socket is null"); } -} -void ControlSocketParser::build(isc::data::ConstElementPtr value) { if (value->getType() != Element::map) { - isc_throw(BadValue, "Specified control-socket is expected to be a map" + isc_throw(DhcpConfigError, "Specified control-socket is expected to be a map" ", i.e. a structure defined within { }"); } - CfgMgr::instance().getStagingCfg()->setControlSocketInfo(value); -} - -/// @brief Does nothing. -void ControlSocketParser::commit() { - // Nothing to do. + srv_cfg.setControlSocketInfo(value); } // ******************** HooksLibrariesParser ************************* @@ -804,66 +787,57 @@ OptionDefListParser::parse(CfgOptionDefPtr storage, ConstElementPtr option_def_l } //****************************** RelayInfoParser ******************************** -RelayInfoParser::RelayInfoParser(const std::string&, - const isc::dhcp::Subnet::RelayInfoPtr& relay_info, - const Option::Universe& family) - :storage_(relay_info), local_(isc::asiolink::IOAddress( - family == Option::V4 ? "0.0.0.0" : "::")), - string_values_(new StringStorage()), family_(family) { - if (!relay_info) { - isc_throw(isc::dhcp::DhcpConfigError, "parser logic error: " - << "relay-info storage may not be NULL"); - } - +RelayInfoParser::RelayInfoParser(const Option::Universe& family) + : family_(family) { }; void -RelayInfoParser::build(ConstElementPtr relay_info) { +RelayInfoParser::parse(const isc::dhcp::Subnet::RelayInfoPtr& cfg, + ConstElementPtr relay_info) { + // Let's start with some sanity checks. + if (!relay_info || !cfg) { + isc_throw(DhcpConfigError, "Logic error: RelayInfoParser::parse() called " + "with at least one NULL parameter."); + } + if (relay_info->getType() != Element::map) { + isc_throw(DhcpConfigError, "Configuration error: RelayInfoParser::parse() " + "called with non-map parameter"); + } + + // Now create the default value. + isc::asiolink::IOAddress ip(family_ == Option::V4 ? "0.0.0.0" : "::"); + + // Now iterate over all parameters. Currently there's only one supported + // parameter, so it should be an easy thing to check. BOOST_FOREACH(ConfigPair param, relay_info->mapValue()) { - ParserPtr parser(createConfigParser(param.first)); - parser->build(param.second); - parser->commit(); + if (param.first == "ip-address") { + try { + ip = asiolink::IOAddress(param.second->stringValue()); + } catch (...) { + isc_throw(DhcpConfigError, "Failed to parse ip-address " + "value: " << param.second + << " (" << param.second->getPosition() << ")"); + } + + // Check if the address family matches. + if ( (ip.isV4() && family_ != Option::V4) || + (ip.isV6() && family_ != Option::V6) ) { + isc_throw(DhcpConfigError, "ip-address field " << ip.toText() + << " does not have IP address of expected family type: " + << (family_ == Option::V4 ? "IPv4" : "IPv6") + << " (" << param.second->getPosition() << ")"); + } + } else { + isc_throw(NotImplemented, + "parser error: RelayInfoParser parameter not supported: " + << param.second); + } } - // Get the IP address - boost::scoped_ptr ip; - try { - ip.reset(new asiolink::IOAddress(string_values_->getParam("ip-address"))); - } catch (...) { - isc_throw(DhcpConfigError, "Failed to parse ip-address " - "value: " << string_values_->getParam("ip-address") - << " (" << string_values_->getPosition("ip-address") << ")"); - } - - if ( (ip->isV4() && family_ != Option::V4) || - (ip->isV6() && family_ != Option::V6) ) { - isc_throw(DhcpConfigError, "ip-address field " << ip->toText() - << " does not have IP address of expected family type: " - << (family_ == Option::V4 ? "IPv4" : "IPv6") - << " (" << string_values_->getPosition("ip-address") << ")"); - } - - local_.addr_ = *ip; -} - -isc::dhcp::ParserPtr -RelayInfoParser::createConfigParser(const std::string& parameter) { - DhcpConfigParser* parser = NULL; - if (parameter.compare("ip-address") == 0) { - parser = new StringParser(parameter, string_values_); - } else { - isc_throw(NotImplemented, - "parser error: RelayInfoParser parameter not supported: " - << parameter); - } - - return (isc::dhcp::ParserPtr(parser)); -} - -void -RelayInfoParser::commit() { - *storage_ = local_; + // Ok, we're done with parsing. Let's store the result in the structure + // we were given as configuration storage. + *cfg = isc::dhcp::Subnet::RelayInfo(ip); } //****************************** PoolsListParser ******************************** @@ -1069,6 +1043,12 @@ SubnetConfigParser::build(ConstElementPtr subnet) { continue; } + if (param.first == "relay") { + RelayInfoParser parser(global_context_->universe_); + parser.parse(relay_info_, param.second); + continue; + } + ParserPtr parser; // When unsupported parameter is specified, the function called // below will thrown an exception. We have to catch this exception diff --git a/src/lib/dhcpsrv/parsers/dhcp_parsers.h b/src/lib/dhcpsrv/parsers/dhcp_parsers.h index a030580c2b..dcd7528acd 100644 --- a/src/lib/dhcpsrv/parsers/dhcp_parsers.h +++ b/src/lib/dhcpsrv/parsers/dhcp_parsers.h @@ -1,4 +1,4 @@ -// Copyright (C) 2013-2016 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2013-2017 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include #include #include @@ -372,56 +374,33 @@ private: /// It contains a list of MAC/hardware acquisition source, i.e. methods how /// MAC address can possibly by obtained in DHCPv6. For a currently supported /// methods, see @ref isc::dhcp::Pkt::getMAC. -class MACSourcesListConfigParser : public DhcpConfigParser { +class MACSourcesListConfigParser : public isc::data::SimpleParser { public: - - /// @brief constructor - /// - /// As this is a dedicated parser, it must be used to parse - /// "mac-sources" parameter only. All other types will throw exception. - /// - /// @param param_name name of the configuration parameter being parsed - /// @param global_context Global parser context. - /// @throw BadValue if supplied parameter name is not "mac-sources" - MACSourcesListConfigParser(const std::string& param_name, - ParserContextPtr global_context); - /// @brief parses parameters value /// /// Parses configuration entry (list of sources) and adds each element /// to the sources list. /// /// @param value pointer to the content of parsed values - virtual void build(isc::data::ConstElementPtr value); - - /// @brief Does nothing. - virtual void commit(); - -private: - - // Parsed parameter name - std::string param_name_; - - /// Global parser context. - ParserContextPtr global_context_; + void parse(CfgMACSource& mac_sources, isc::data::ConstElementPtr value); }; /// @brief Parser for the control-socket structure /// /// It does not parse anything, simply stores the element in /// the staging config. -class ControlSocketParser : public DhcpConfigParser { +class ControlSocketParser : public isc::data::SimpleParser { public: - - ControlSocketParser(const std::string& param_name); - - /// @brief Stores contents of the control-socket map in the staging config. + /// @brief "Parses" control-socket structure /// + /// Since the SrvConfig structure takes the socket definition + /// as ConstElementPtr, there's really nothing to parse here. + /// It only does basic sanity checks and throws DhcpConfigError + /// if the value is null or is not a map. + /// + /// @param srv_cfg parsed values will be stored here /// @param value pointer to the content of parsed values - virtual void build(isc::data::ConstElementPtr value); - - /// @brief Does nothing. - virtual void commit(); + void parse(SrvConfig& srv_cfg, isc::data::ConstElementPtr value); }; /// @brief Parser for hooks library list @@ -849,48 +828,24 @@ protected: /// is expected that the number of parameters will increase over time. /// /// It is useful for parsing Dhcp<4/6>/subnet<4/6>[x]/relay parameters. -class RelayInfoParser : public DhcpConfigParser { +class RelayInfoParser : public isc::data::SimpleParser { public: /// @brief constructor - /// @param unused first argument is ignored, all Parser constructors - /// accept string as first argument. - /// @param relay_info is the storage in which to store the parsed /// @param family specifies protocol family (IPv4 or IPv6) - RelayInfoParser(const std::string& unused, - const isc::dhcp::Subnet::RelayInfoPtr& relay_info, - const isc::dhcp::Option::Universe& family); + RelayInfoParser(const isc::dhcp::Option::Universe& family); /// @brief parses the actual relay parameters - /// @param relay_info JSON structure holding relay parameters to parse - virtual void build(isc::data::ConstElementPtr relay_info); - - /// @brief stores parsed info in relay_info - virtual void commit(); - -protected: - - /// @brief Creates a parser for the given "relay" member element id. /// /// The elements currently supported are: /// -# ip-address /// - /// @param parser is the "item_name" for a specific member element of - /// the "relay" specification. - /// - /// @return returns a pointer to newly created parser. - isc::dhcp::ParserPtr - createConfigParser(const std::string& parser); - - /// Parsed data will be stored there on commit() - isc::dhcp::Subnet::RelayInfoPtr storage_; - - /// Local storage information (for temporary values) - isc::dhcp::Subnet::RelayInfo local_; - - /// Storage for subnet-specific string values. - StringStoragePtr string_values_; + /// @param cfg configuration will be stored here + /// @param relay_info JSON structure holding relay parameters to parse + void parse(const isc::dhcp::Subnet::RelayInfoPtr& cfg, + isc::data::ConstElementPtr relay_info); +protected: /// Protocol family (IPv4 or IPv6) Option::Universe family_; }; diff --git a/src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc b/src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc index 566be20b64..90e89f3958 100644 --- a/src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc +++ b/src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2012-2017 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -226,48 +226,72 @@ TEST_F(DhcpParserTest, uint32ParserTest) { EXPECT_EQ(test_value, actual_value); } -/// @brief Check MACSourcesListConfigParser basic functionality -/// -/// Verifies that the parser: -/// 1. Does not allow empty for storage. -/// 2. Does not allow name other than "mac-sources" -/// 3. Parses list of mac sources and adds them to CfgMgr -TEST_F(DhcpParserTest, MacSourcesListConfigParserTest) { - - const std::string valid_name = "mac-sources"; - const std::string bogus_name = "bogus-name"; - - ParserContextPtr parser_context(new ParserContext(Option::V6)); - - // Verify that parser constructor fails if parameter name isn't "mac-sources" - EXPECT_THROW(MACSourcesListConfigParser(bogus_name, parser_context), - isc::BadValue); +/// Verifies the code that parses mac sources and adds them to CfgMgr +TEST_F(DhcpParserTest, MacSources) { // That's an equivalent of the following snippet: // "mac-sources: [ \"duid\", \"ipv6\" ]"; - ElementPtr config = Element::createList(); - config->add(Element::create("duid")); - config->add(Element::create("ipv6-link-local")); + ElementPtr values = Element::createList(); + values->add(Element::create("duid")); + values->add(Element::create("ipv6-link-local")); - boost::scoped_ptr - parser(new MACSourcesListConfigParser(valid_name, parser_context)); - - // This should parse the configuration and add eth0 and eth1 to the list - // of interfaces that server should listen on. - EXPECT_NO_THROW(parser->build(config)); - EXPECT_NO_THROW(parser->commit()); - - // Use CfgMgr instance to check if eth0 and eth1 was added, and that - // eth2 was not added. + // Let's grab server configuration from CfgMgr SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg(); ASSERT_TRUE(cfg); - CfgMACSources configured_sources = cfg->getMACSources().get(); + CfgMACSource& sources = cfg->getMACSources(); + // This should parse the configuration and check that it doesn't throw. + MACSourcesListConfigParser parser; + EXPECT_NO_THROW(parser.parse(sources, values)); + + // Finally, check the sources that were configured + CfgMACSources configured_sources = cfg->getMACSources().get(); ASSERT_EQ(2, configured_sources.size()); EXPECT_EQ(HWAddr::HWADDR_SOURCE_DUID, configured_sources[0]); EXPECT_EQ(HWAddr::HWADDR_SOURCE_IPV6_LINK_LOCAL, configured_sources[1]); } +/// @brief Check MACSourcesListConfigParser rejecting empty list +/// +/// Verifies that the code rejects an empty mac-sources list. +TEST_F(DhcpParserTest, MacSourcesEmpty) { + + // That's an equivalent of the following snippet: + // "mac-sources: [ \"duid\", \"ipv6\" ]"; + ElementPtr values = Element::createList(); + + // Let's grab server configuration from CfgMgr + SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg(); + ASSERT_TRUE(cfg); + CfgMACSource& sources = cfg->getMACSources(); + + // This should throw, because if specified, at least one MAC source + // has to be specified. + MACSourcesListConfigParser parser; + EXPECT_THROW(parser.parse(sources, values), DhcpConfigError); +} + +/// @brief Check MACSourcesListConfigParser rejecting empty list +/// +/// Verifies that the code rejects fake mac source. +TEST_F(DhcpParserTest, MacSourcesBogus) { + + // That's an equivalent of the following snippet: + // "mac-sources: [ \"duid\", \"ipv6\" ]"; + ElementPtr values = Element::createList(); + values->add(Element::create("from-ebay")); + values->add(Element::create("just-guess-it")); + + // Let's grab server configuration from CfgMgr + SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg(); + ASSERT_TRUE(cfg); + CfgMACSource& sources = cfg->getMACSources(); + + // This should throw, because these are not valid sources. + MACSourcesListConfigParser parser; + EXPECT_THROW(parser.parse(sources, values), DhcpConfigError); +} + /// @brief Test Fixture class which provides basic structure for testing /// configuration parsing. This is essentially the same structure provided /// by dhcp servers. @@ -2433,21 +2457,17 @@ TEST_F(ParseConfigTest, validRelayInfo4) { // We need to set the default ip-address to something. Subnet::RelayInfoPtr result(new Subnet::RelayInfo(asiolink::IOAddress("0.0.0.0"))); - boost::shared_ptr parser; + RelayInfoParser parser(Option::V4); // Subnet4 parser will pass 0.0.0.0 to the RelayInfoParser - EXPECT_NO_THROW(parser.reset(new RelayInfoParser("ignored", result, - Option::V4))); - EXPECT_NO_THROW(parser->build(json)); - EXPECT_NO_THROW(parser->commit()); - + EXPECT_NO_THROW(parser.parse(result, json)); EXPECT_EQ("192.0.2.1", result->addr_.toText()); // Let's check negative scenario (wrong family type) - EXPECT_THROW(parser->build(json_bogus1), DhcpConfigError); + EXPECT_THROW(parser.parse(result, json_bogus1), DhcpConfigError); // Let's check negative scenario (too large byte values in pseudo-IPv4 addr) - EXPECT_THROW(parser->build(json_bogus2), DhcpConfigError); + EXPECT_THROW(parser.parse(result, json_bogus2), DhcpConfigError); } /// @brief Checks that a valid relay info structure for IPv6 can be handled @@ -2477,20 +2497,16 @@ TEST_F(ParseConfigTest, validRelayInfo6) { // We need to set the default ip-address to something. Subnet::RelayInfoPtr result(new Subnet::RelayInfo(asiolink::IOAddress("::"))); - boost::shared_ptr parser; + RelayInfoParser parser(Option::V6); // Subnet4 parser will pass :: to the RelayInfoParser - EXPECT_NO_THROW(parser.reset(new RelayInfoParser("ignored", result, - Option::V6))); - EXPECT_NO_THROW(parser->build(json)); - EXPECT_NO_THROW(parser->commit()); - + EXPECT_NO_THROW(parser.parse(result, json)); EXPECT_EQ("2001:db8::1", result->addr_.toText()); // Let's check negative scenario (wrong family type) - EXPECT_THROW(parser->build(json_bogus1), DhcpConfigError); + EXPECT_THROW(parser.parse(result, json_bogus1), DhcpConfigError); // Unparseable text that looks like IPv6 address, but has too many colons - EXPECT_THROW(parser->build(json_bogus2), DhcpConfigError); + EXPECT_THROW(parser.parse(result, json_bogus2), DhcpConfigError); } // There's no test for ControlSocketParser, as it is tested in the DHCPv4 code diff --git a/src/lib/testutils/io_utils.cc b/src/lib/testutils/io_utils.cc index 54aff4cb6f..4bee06658d 100644 --- a/src/lib/testutils/io_utils.cc +++ b/src/lib/testutils/io_utils.cc @@ -1,9 +1,10 @@ -// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2015,2017 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. +#include #include #include #include @@ -38,7 +39,78 @@ std::string readFile(const std::string& file_path) { return (output.str()); } +std::string decommentJSONfile(const std::string& input_file) { + + using namespace std; + + ifstream f(input_file); + if (!f.is_open()) { + isc_throw(isc::BadValue, "can't open input file for reading: " + input_file); + } + + string outfile; + size_t last_slash = input_file.find_last_of("/"); + if (last_slash != string::npos) { + outfile = input_file.substr(last_slash + 1); + } else { + outfile = input_file; + } + outfile += "-decommented"; + + ofstream out(outfile); + if (!out.is_open()) { + isc_throw(isc::BadValue, "can't open output file for writing: " + input_file); + } + + bool in_comment = false; + string line; + while (std::getline(f, line)) { + // First, let's get rid of the # comments + size_t hash_pos = line.find("#"); + if (hash_pos != string::npos) { + line = line.substr(0, hash_pos); + } + + // Second, let's get rid of the // comments + size_t dblslash_pos = line.find("//"); + if (dblslash_pos != string::npos) { + line = line.substr(0, dblslash_pos); + } + + // Now the tricky part: c comments. + size_t begin_pos = line.find("/*"); + size_t end_pos = line.find("*/"); + if (in_comment && end_pos == string::npos) { + // we continue through multiline comment + line = ""; + } else { + + if (begin_pos != string::npos) { + in_comment = true; + if (end_pos != string::npos) { + // sigle line comment. Let's get rid of the content in between + line = line.replace(begin_pos, end_pos + 2, end_pos + 2 - begin_pos, ' '); + in_comment = false; + } else { + line = line.substr(0, begin_pos); + } + } else { + if (in_comment && end_pos != string::npos) { + line = line.replace(0, end_pos +2 , end_pos + 2, ' '); + in_comment = false; + } + } + } + + // Finally, write the line to the output file. + out << line << endl; + } + f.close(); + out.close(); + + return (outfile); +} + }; // end of isc::dhcp::test namespace }; // end of isc::dhcp namespace }; // end of isc namespace - diff --git a/src/lib/testutils/io_utils.h b/src/lib/testutils/io_utils.h index 8cf1ef6e14..57c5f69511 100644 --- a/src/lib/testutils/io_utils.h +++ b/src/lib/testutils/io_utils.h @@ -1,4 +1,4 @@ -// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2015,2017 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -25,6 +25,20 @@ bool fileExists(const std::string& file_path); /// @return File contents. std::string readFile(const std::string& file_path); +/// @brief Removes comments from a JSON file +/// +/// Removes //, # and /* */ comments from the input file and writes its content +/// to another file. The comments are replaced with spaces, so the original +/// token locations should remain unaffected. This is rather naive +/// implementation, but it's probably sufficient for testing. It won't be able +/// to pick any trickier cases, like # or // appearing in strings, nested C++ +/// comments etc. +/// +/// @param input_file file to be stripped of comments +/// @return filename of a new file that has comments stripped from it +/// @throw BadValue if the input file cannot be opened +std::string decommentJSONfile(const std::string& input_file); + }; // end of isc::dhcp::test namespace }; // end of isc::dhcp namespace }; // end of isc namespace