From a8df76c476ee525d68bbd589f65dc2d944ac902d Mon Sep 17 00:00:00 2001 From: Francis Dupont Date: Sat, 2 Sep 2017 14:35:47 +0200 Subject: [PATCH] [5124a] Rebased --- src/bin/dhcp4/dhcp4_lexer.ll | 4 ++ src/bin/dhcp4/dhcp4_parser.yy | 60 ++++++++++++++-- src/bin/dhcp4/parser_context.cc | 19 ++++- src/bin/dhcp4/parser_context.h | 16 +++++ src/bin/dhcp4/tests/config_parser_unittest.cc | 38 +++++----- src/bin/dhcp4/tests/dhcp4_test_utils.h | 4 +- src/bin/dhcp4/tests/parser_unittest.cc | 7 ++ src/bin/dhcp6/dhcp6_lexer.ll | 4 ++ src/bin/dhcp6/dhcp6_parser.yy | 71 +++++++++++++++++-- src/bin/dhcp6/parser_context.cc | 15 ++++ src/bin/dhcp6/parser_context.h | 16 +++++ src/bin/dhcp6/tests/config_parser_unittest.cc | 45 ++++++------ src/bin/dhcp6/tests/dhcp6_test_utils.h | 4 +- src/bin/dhcp6/tests/parser_unittest.cc | 7 ++ 14 files changed, 255 insertions(+), 55 deletions(-) diff --git a/src/bin/dhcp4/dhcp4_lexer.ll b/src/bin/dhcp4/dhcp4_lexer.ll index 1045238a93..9abcc4c7dc 100644 --- a/src/bin/dhcp4/dhcp4_lexer.ll +++ b/src/bin/dhcp4/dhcp4_lexer.ll @@ -119,6 +119,10 @@ ControlCharacterFill [^"\\]|\\{JSONEscapeSequence} return isc::dhcp::Dhcp4Parser::make_SUB_POOL4(driver.loc_); case Parser4Context::PARSER_HOST_RESERVATION: return isc::dhcp::Dhcp4Parser::make_SUB_RESERVATION(driver.loc_); + case Parser4Context::PARSER_OPTION_DEFS: + return isc::dhcp::Dhcp4Parser::make_SUB_OPTION_DEFS(driver.loc_); + case Parser4Context::PARSER_OPTION_DEF: + return isc::dhcp::Dhcp4Parser::make_SUB_OPTION_DEF(driver.loc_); case Parser4Context::PARSER_OPTION_DATA: return isc::dhcp::Dhcp4Parser::make_SUB_OPTION_DATA(driver.loc_); case Parser4Context::PARSER_HOOKS_LIBRARY: diff --git a/src/bin/dhcp4/dhcp4_parser.yy b/src/bin/dhcp4/dhcp4_parser.yy index c5af1f06c1..4af576f59b 100644 --- a/src/bin/dhcp4/dhcp4_parser.yy +++ b/src/bin/dhcp4/dhcp4_parser.yy @@ -195,6 +195,7 @@ using namespace std; SUB_SUBNET4 SUB_POOL4 SUB_RESERVATION + SUB_OPTION_DEFS SUB_OPTION_DEF SUB_OPTION_DATA SUB_HOOKS_LIBRARY @@ -230,6 +231,7 @@ start: TOPLEVEL_JSON { ctx.ctx_ = ctx.NO_KEYWORD; } sub_json | SUB_SUBNET4 { ctx.ctx_ = ctx.SUBNET4; } sub_subnet4 | SUB_POOL4 { ctx.ctx_ = ctx.POOLS; } sub_pool4 | SUB_RESERVATION { ctx.ctx_ = ctx.RESERVATIONS; } sub_reservation + | SUB_OPTION_DEFS { ctx.ctx_ = ctx.DHCP4; } sub_option_def_list | SUB_OPTION_DEF { ctx.ctx_ = ctx.OPTION_DEF; } sub_option_def | SUB_OPTION_DATA { ctx.ctx_ = ctx.OPTION_DATA; } sub_option_data | SUB_HOOKS_LIBRARY { ctx.ctx_ = ctx.HOOKS_LIBRARIES; } sub_hooks_library @@ -351,6 +353,9 @@ syntax_map: LCURLY_BRACKET { // map parsing completed. If we ever want to do any wrap up // (maybe some sanity checking), this would be the best place // for it. + + // Dhcp4 is required + ctx.require("Dhcp4", ctx.loc2pos(@1), ctx.loc2pos(@4)); }; // This represents top-level entries: Control-agent, Dhcp6, Dhcp4, @@ -376,9 +381,7 @@ dhcp4_object: DHCP4 { ctx.stack_.push_back(m); ctx.enter(ctx.DHCP4); } COLON LCURLY_BRACKET global_params RCURLY_BRACKET { - // map parsing completed. If we ever want to do any wrap up - // (maybe some sanity checking), this would be the best place - // for it. + // No global parameter is required ctx.stack_.pop_back(); ctx.leave(); }; @@ -390,6 +393,7 @@ sub_dhcp4: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.push_back(m); } global_params RCURLY_BRACKET { + // No global parameter is required // parsing completed }; @@ -459,6 +463,7 @@ interfaces_config: INTERFACES_CONFIG { ctx.stack_.push_back(i); ctx.enter(ctx.INTERFACES_CONFIG); } COLON LCURLY_BRACKET interfaces_config_params RCURLY_BRACKET { + // No interfaces config param is required ctx.stack_.pop_back(); ctx.leave(); }; @@ -477,6 +482,7 @@ sub_interfaces4: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.push_back(m); } interfaces_config_params RCURLY_BRACKET { + // No interfaces config param is required // parsing completed }; @@ -513,6 +519,8 @@ lease_database: LEASE_DATABASE { ctx.stack_.push_back(i); ctx.enter(ctx.LEASE_DATABASE); } COLON LCURLY_BRACKET database_map_params RCURLY_BRACKET { + // The type parameter is required + ctx.require("type", ctx.loc2pos(@4), ctx.loc2pos(@6)); ctx.stack_.pop_back(); ctx.leave(); }; @@ -523,6 +531,8 @@ hosts_database: HOSTS_DATABASE { ctx.stack_.push_back(i); ctx.enter(ctx.HOSTS_DATABASE); } COLON LCURLY_BRACKET database_map_params RCURLY_BRACKET { + // The type parameter is required + ctx.require("type", ctx.loc2pos(@4), ctx.loc2pos(@6)); ctx.stack_.pop_back(); ctx.leave(); }; @@ -702,6 +712,8 @@ hooks_library: LCURLY_BRACKET { ctx.stack_.back()->add(m); ctx.stack_.push_back(m); } hooks_params RCURLY_BRACKET { + // The library hooks parameter is required + ctx.require("library", ctx.loc2pos(@1), ctx.loc2pos(@4)); ctx.stack_.pop_back(); }; @@ -710,6 +722,8 @@ sub_hooks_library: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.push_back(m); } hooks_params RCURLY_BRACKET { + // The library hooks parameter is required + ctx.require("library", ctx.loc2pos(@1), ctx.loc2pos(@4)); // parsing completed }; @@ -744,6 +758,7 @@ expired_leases_processing: EXPIRED_LEASES_PROCESSING { ctx.stack_.push_back(m); ctx.enter(ctx.EXPIRED_LEASES_PROCESSING); } COLON LCURLY_BRACKET expired_leases_params RCURLY_BRACKET { + // No expired lease parameter is required ctx.stack_.pop_back(); ctx.leave(); }; @@ -838,6 +853,9 @@ subnet4: LCURLY_BRACKET { // ctx.stack_.back()->set("renew-timer", renew); // } // } + + // The subnet subnet4 parameter is required + ctx.require("subnet", ctx.loc2pos(@1), ctx.loc2pos(@4)); ctx.stack_.pop_back(); }; @@ -846,6 +864,8 @@ sub_subnet4: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.push_back(m); } subnet4_params RCURLY_BRACKET { + // The subnet subnet4 parameter is required + ctx.require("subnet", ctx.loc2pos(@1), ctx.loc2pos(@4)); // parsing completed }; @@ -969,6 +989,16 @@ option_def_list: OPTION_DEF { ctx.leave(); }; +// This defines the top level scope when the parser is told to parse +// option definitions. It works as a subset limited to option +// definitions +sub_option_def_list: LCURLY_BRACKET { + ElementPtr m(new MapElement(ctx.loc2pos(@1))); + ctx.stack_.push_back(m); +} option_def_list RCURLY_BRACKET { + // parsing completed +}; + // This defines the content of option-def. It may be empty, // have one entry or multiple entries separated by comma. option_def_list_content: %empty @@ -986,6 +1016,10 @@ option_def_entry: LCURLY_BRACKET { ctx.stack_.back()->add(m); ctx.stack_.push_back(m); } option_def_params RCURLY_BRACKET { + // The name, code and type option def parameters are required. + ctx.require("name", ctx.loc2pos(@1), ctx.loc2pos(@4)); + ctx.require("code", ctx.loc2pos(@1), ctx.loc2pos(@4)); + ctx.require("type", ctx.loc2pos(@1), ctx.loc2pos(@4)); ctx.stack_.pop_back(); }; @@ -997,6 +1031,10 @@ sub_option_def: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.push_back(m); } option_def_params RCURLY_BRACKET { + // The name, code and type option def parameters are required. + ctx.require("name", ctx.loc2pos(@1), ctx.loc2pos(@4)); + ctx.require("code", ctx.loc2pos(@1), ctx.loc2pos(@4)); + ctx.require("type", ctx.loc2pos(@1), ctx.loc2pos(@4)); // parsing completed }; @@ -1101,6 +1139,7 @@ option_data_entry: LCURLY_BRACKET { ctx.stack_.back()->add(m); ctx.stack_.push_back(m); } option_data_params RCURLY_BRACKET { + /// @todo: the code or name parameters are required. ctx.stack_.pop_back(); }; @@ -1112,6 +1151,7 @@ sub_option_data: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.push_back(m); } option_data_params RCURLY_BRACKET { + /// @todo: the code or name parameters are required. // parsing completed }; @@ -1191,6 +1231,8 @@ pool_list_entry: LCURLY_BRACKET { ctx.stack_.back()->add(m); ctx.stack_.push_back(m); } pool_params RCURLY_BRACKET { + // The pool parameter is required. + ctx.require("pool", ctx.loc2pos(@1), ctx.loc2pos(@4)); ctx.stack_.pop_back(); }; @@ -1199,6 +1241,8 @@ sub_pool4: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.push_back(m); } pool_params RCURLY_BRACKET { + // The pool parameter is required. + ctx.require("pool", ctx.loc2pos(@1), ctx.loc2pos(@4)); // parsing completed }; @@ -1253,6 +1297,7 @@ reservation: LCURLY_BRACKET { ctx.stack_.back()->add(m); ctx.stack_.push_back(m); } reservation_params RCURLY_BRACKET { + /// @todo: an identifier parameter is required. ctx.stack_.pop_back(); }; @@ -1261,6 +1306,7 @@ sub_reservation: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.push_back(m); } reservation_params RCURLY_BRACKET { + /// @todo: an identifier parameter is required. // parsing completed }; @@ -1272,7 +1318,7 @@ not_empty_reservation_params: reservation_param | not_empty_reservation_params COMMA reservation_param ; -// @todo probably need to add mac-address as well here +/// @todo probably need to add mac-address as well here reservation_param: duid | reservation_client_classes | client_id_value @@ -1421,6 +1467,8 @@ client_class: LCURLY_BRACKET { ctx.stack_.back()->add(m); ctx.stack_.push_back(m); } client_class_params RCURLY_BRACKET { + // The name client class parameter is required. + ctx.require("name", ctx.loc2pos(@1), ctx.loc2pos(@4)); ctx.stack_.pop_back(); }; @@ -1504,6 +1552,8 @@ dhcp_ddns: DHCP_DDNS { ctx.stack_.push_back(m); ctx.enter(ctx.DHCP_DDNS); } COLON LCURLY_BRACKET dhcp_ddns_params RCURLY_BRACKET { + // The enable updates DHCP DDNS parameter is required. + ctx.require("enable-updates", ctx.loc2pos(@4), ctx.loc2pos(@6)); ctx.stack_.pop_back(); ctx.leave(); }; @@ -1513,6 +1563,8 @@ sub_dhcp_ddns: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.push_back(m); } dhcp_ddns_params RCURLY_BRACKET { + // The enable updates DHCP DDNS parameter is required. + ctx.require("enable-updates", ctx.loc2pos(@1), ctx.loc2pos(@4)); // parsing completed }; diff --git a/src/bin/dhcp4/parser_context.cc b/src/bin/dhcp4/parser_context.cc index fd3689f236..5e469188bc 100644 --- a/src/bin/dhcp4/parser_context.cc +++ b/src/bin/dhcp4/parser_context.cc @@ -74,13 +74,13 @@ Parser4Context::error(const isc::dhcp::location& loc, const std::string& what) } void -Parser4Context::error (const std::string& what) +Parser4Context::error(const std::string& what) { isc_throw(Dhcp4ParseError, what); } void -Parser4Context::fatal (const std::string& what) +Parser4Context::fatal(const std::string& what) { isc_throw(Dhcp4ParseError, what); } @@ -94,6 +94,21 @@ Parser4Context::loc2pos(isc::dhcp::location& loc) return (isc::data::Element::Position(file, line, pos)); } +void +Parser4Context::require(const std::string& name, + isc::data::Element::Position open_loc, + isc::data::Element::Position close_loc) +{ + ConstElementPtr value = stack_.back()->get(name); + if (!value) { + isc_throw(Dhcp4ParseError, + "missing parameter '" << name << "' (" + << stack_.back()->getPosition() << ") [" + << contextName() << " map between " + << open_loc << " and " << close_loc << "]"); + } +} + void Parser4Context::enter(const ParserContext& ctx) { diff --git a/src/bin/dhcp4/parser_context.h b/src/bin/dhcp4/parser_context.h index 7bab72239f..269588ed3a 100644 --- a/src/bin/dhcp4/parser_context.h +++ b/src/bin/dhcp4/parser_context.h @@ -73,6 +73,9 @@ public: /// This will parse the input as host-reservation. PARSER_HOST_RESERVATION, + /// This will parse the input option definitions (for tests). + PARSER_OPTION_DEFS, + /// This will parse the input as option definition. PARSER_OPTION_DEF, @@ -176,6 +179,19 @@ public: /// @return Position in format accepted by Element isc::data::Element::Position loc2pos(isc::dhcp::location& loc); + /// @brief Check if a required parameter is present + /// + /// Check if a required parameter is present in the map at the top + /// of the stack and raise an error when it is not. + /// + /// @param name name of the parameter to check + /// @param open_loc location of the opening curly bracket + /// @param close_loc ocation of the closing curly bracket + /// @throw Dhcp4ParseError + void require(const std::string& name, + isc::data::Element::Position open_loc, + isc::data::Element::Position close_loc); + /// @brief Defines syntactic contexts for lexical tie-ins typedef enum { ///< This one is used in pure JSON mode. diff --git a/src/bin/dhcp4/tests/config_parser_unittest.cc b/src/bin/dhcp4/tests/config_parser_unittest.cc index dd2c1da780..bc75f86e13 100644 --- a/src/bin/dhcp4/tests/config_parser_unittest.cc +++ b/src/bin/dhcp4/tests/config_parser_unittest.cc @@ -1618,7 +1618,7 @@ TEST_F(Dhcp4ParserTest, optionDefIpv4Address) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config, true)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config, true)); extractConfig(config); // Make sure that the particular option definition does not exist. @@ -1659,10 +1659,8 @@ TEST_F(Dhcp4ParserTest, optionDefIpv4Address) { // Let's apply empty configuration. This removes the option definitions // configuration and should result in removal of the option 100 from the - // libdhcp++. - config = "{ }"; - ASSERT_NO_THROW(json = parseOPTION_DEF(config, true)); - + // libdhcp++. Note DHCP4 or OPTION_DEFS parsers do not accept empty maps. + json.reset(new MapElement()); ASSERT_NO_THROW(status = configureDhcp4Server(*srv_, json)); checkResult(status, 0); @@ -1685,7 +1683,7 @@ TEST_F(Dhcp4ParserTest, optionDefRecord) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); extractConfig(config); // Make sure that the particular option definition does not exist. @@ -1740,7 +1738,7 @@ TEST_F(Dhcp4ParserTest, optionDefMultiple) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); extractConfig(config); // Make sure that the option definitions do not exist yet. @@ -1810,7 +1808,7 @@ TEST_F(Dhcp4ParserTest, optionDefDuplicate) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); // Make sure that the option definition does not exist yet. ASSERT_FALSE(CfgMgr::instance().getStagingCfg()-> @@ -1849,7 +1847,7 @@ TEST_F(Dhcp4ParserTest, optionDefArray) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); extractConfig(config); // Make sure that the particular option definition does not exist. @@ -1892,7 +1890,7 @@ TEST_F(Dhcp4ParserTest, optionDefEncapsulate) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); extractConfig(config); // Make sure that the particular option definition does not exist. @@ -1933,7 +1931,7 @@ TEST_F(Dhcp4ParserTest, optionDefInvalidName) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); // Use the configuration string to create new option definition. ConstElementPtr status; @@ -1958,7 +1956,7 @@ TEST_F(Dhcp4ParserTest, optionDefInvalidType) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); // Use the configuration string to create new option definition. ConstElementPtr status; @@ -1984,7 +1982,7 @@ TEST_F(Dhcp4ParserTest, optionDefInvalidRecordType) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); // Use the configuration string to create new option definition. ConstElementPtr status; @@ -2010,7 +2008,7 @@ TEST_F(Dhcp4ParserTest, optionIntegerTypes) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); // Use the configuration string to create new option definition. ConstElementPtr status; @@ -2035,7 +2033,7 @@ TEST_F(Dhcp4ParserTest, optionDefInvalidEncapsulatedSpace) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); // Use the configuration string to create new option definition. ConstElementPtr status; @@ -2064,7 +2062,7 @@ TEST_F(Dhcp4ParserTest, optionDefEncapsulatedSpaceAndArray) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); // Use the configuration string to create new option definition. ConstElementPtr status; @@ -2090,7 +2088,7 @@ TEST_F(Dhcp4ParserTest, optionDefEncapsulateOwnSpace) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); // Use the configuration string to create new option definition. ConstElementPtr status; @@ -2119,7 +2117,7 @@ TEST_F(Dhcp4ParserTest, optionStandardDefOverride) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); extractConfig(config); OptionDefinitionPtr def = CfgMgr::instance().getStagingCfg()-> @@ -2154,7 +2152,7 @@ TEST_F(Dhcp4ParserTest, optionStandardDefOverride) { " \"space\": \"dhcp4\"" " } ]" "}"; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); // Use the configuration string to create new option definition. EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json)); @@ -2176,7 +2174,7 @@ TEST_F(Dhcp4ParserTest, optionStandardDefOverride) { " \"space\": \"dhcp4\"" " } ]" "}"; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); extractConfig(config); // Use the configuration string to create new option definition. diff --git a/src/bin/dhcp4/tests/dhcp4_test_utils.h b/src/bin/dhcp4/tests/dhcp4_test_utils.h index 5ea2b243a4..c35809c4a7 100644 --- a/src/bin/dhcp4/tests/dhcp4_test_utils.h +++ b/src/bin/dhcp4/tests/dhcp4_test_utils.h @@ -536,11 +536,11 @@ parseDHCP4(const std::string& in, bool verbose = false) /// @param verbose display the exception message when it fails /// @return ElementPtr structure representing parsed JSON inline isc::data::ElementPtr -parseOPTION_DEF(const std::string& in, bool verbose = false) +parseOPTION_DEFS(const std::string& in, bool verbose = false) { try { isc::dhcp::Parser4Context ctx; - return (ctx.parseString(in, isc::dhcp::Parser4Context::PARSER_OPTION_DEF)); + return (ctx.parseString(in, isc::dhcp::Parser4Context::PARSER_OPTION_DEFS)); } catch (const std::exception& ex) { if (verbose) { diff --git a/src/bin/dhcp4/tests/parser_unittest.cc b/src/bin/dhcp4/tests/parser_unittest.cc index 503023518d..fa21fbd134 100644 --- a/src/bin/dhcp4/tests/parser_unittest.cc +++ b/src/bin/dhcp4/tests/parser_unittest.cc @@ -501,6 +501,13 @@ TEST(ParserTest, errors) { Parser4Context::PARSER_DHCP4, ":2.2-17: got unexpected keyword " "\"valid_lifetime\" in Dhcp4 map."); + + // missing parameter + testError("{ \"name\": \"foo\",\n" + " \"code\": 123 }\n", + Parser4Context::PARSER_OPTION_DEF, + "missing parameter 'type' (:1:1) " + "[option-def map between :1:1 and :2:15]"); } // Check unicode escapes diff --git a/src/bin/dhcp6/dhcp6_lexer.ll b/src/bin/dhcp6/dhcp6_lexer.ll index 4c7a418a9a..f5161feda4 100644 --- a/src/bin/dhcp6/dhcp6_lexer.ll +++ b/src/bin/dhcp6/dhcp6_lexer.ll @@ -121,6 +121,10 @@ ControlCharacterFill [^"\\]|\\{JSONEscapeSequence} return isc::dhcp::Dhcp6Parser::make_SUB_PD_POOL(driver.loc_); case Parser6Context::PARSER_HOST_RESERVATION: return isc::dhcp::Dhcp6Parser::make_SUB_RESERVATION(driver.loc_); + case Parser6Context::PARSER_OPTION_DEFS: + return isc::dhcp::Dhcp6Parser::make_SUB_OPTION_DEFS(driver.loc_); + case Parser6Context::PARSER_OPTION_DEF: + return isc::dhcp::Dhcp6Parser::make_SUB_OPTION_DEF(driver.loc_); case Parser6Context::PARSER_OPTION_DATA: return isc::dhcp::Dhcp6Parser::make_SUB_OPTION_DATA(driver.loc_); case Parser6Context::PARSER_HOOKS_LIBRARY: diff --git a/src/bin/dhcp6/dhcp6_parser.yy b/src/bin/dhcp6/dhcp6_parser.yy index 6bebc2f920..1cf4e698bd 100644 --- a/src/bin/dhcp6/dhcp6_parser.yy +++ b/src/bin/dhcp6/dhcp6_parser.yy @@ -203,6 +203,7 @@ using namespace std; SUB_POOL6 SUB_PD_POOL SUB_RESERVATION + SUB_OPTION_DEFS SUB_OPTION_DEF SUB_OPTION_DATA SUB_HOOKS_LIBRARY @@ -239,6 +240,7 @@ start: TOPLEVEL_JSON { ctx.ctx_ = ctx.NO_KEYWORD; } sub_json | SUB_POOL6 { ctx.ctx_ = ctx.POOLS; } sub_pool6 | SUB_PD_POOL { ctx.ctx_ = ctx.PD_POOLS; } sub_pd_pool | SUB_RESERVATION { ctx.ctx_ = ctx.RESERVATIONS; } sub_reservation + | SUB_OPTION_DEFS { ctx.ctx_ = ctx.DHCP6; } sub_option_def_list | SUB_OPTION_DEF { ctx.ctx_ = ctx.OPTION_DEF; } sub_option_def | SUB_OPTION_DATA { ctx.ctx_ = ctx.OPTION_DATA; } sub_option_data | SUB_HOOKS_LIBRARY { ctx.ctx_ = ctx.HOOKS_LIBRARIES; } sub_hooks_library @@ -360,6 +362,9 @@ syntax_map: LCURLY_BRACKET { // map parsing completed. If we ever want to do any wrap up // (maybe some sanity checking), this would be the best place // for it. + + // Dhcp6 is required + ctx.require("Dhcp6", ctx.loc2pos(@1), ctx.loc2pos(@4)); }; // This represents top-level entries: Dhcp6, Dhcp4, DhcpDdns, Logging @@ -384,9 +389,7 @@ dhcp6_object: DHCP6 { ctx.stack_.push_back(m); ctx.enter(ctx.DHCP6); } COLON LCURLY_BRACKET global_params RCURLY_BRACKET { - // map parsing completed. If we ever want to do any wrap up - // (maybe some sanity checking), this would be the best place - // for it. + // No global parameter is required ctx.stack_.pop_back(); ctx.leave(); }; @@ -398,6 +401,7 @@ sub_dhcp6: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.push_back(m); } global_params RCURLY_BRACKET { + // No global parameter is required // parsing completed }; @@ -462,6 +466,7 @@ interfaces_config: INTERFACES_CONFIG { ctx.stack_.push_back(i); ctx.enter(ctx.INTERFACES_CONFIG); } COLON LCURLY_BRACKET interfaces_config_params RCURLY_BRACKET { + // No interfaces config param is required ctx.stack_.pop_back(); ctx.leave(); }; @@ -471,6 +476,7 @@ sub_interfaces6: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.push_back(m); } interfaces_config_params RCURLY_BRACKET { + // No interfaces config param is required // parsing completed }; @@ -504,6 +510,8 @@ lease_database: LEASE_DATABASE { ctx.stack_.push_back(i); ctx.enter(ctx.LEASE_DATABASE); } COLON LCURLY_BRACKET database_map_params RCURLY_BRACKET { + // The type parameter is required + ctx.require("type", ctx.loc2pos(@4), ctx.loc2pos(@6)); ctx.stack_.pop_back(); ctx.leave(); }; @@ -514,6 +522,8 @@ hosts_database: HOSTS_DATABASE { ctx.stack_.push_back(i); ctx.enter(ctx.HOSTS_DATABASE); } COLON LCURLY_BRACKET database_map_params RCURLY_BRACKET { + // The type parameter is required + ctx.require("type", ctx.loc2pos(@4), ctx.loc2pos(@6)); ctx.stack_.pop_back(); ctx.leave(); }; @@ -716,6 +726,8 @@ hooks_library: LCURLY_BRACKET { ctx.stack_.back()->add(m); ctx.stack_.push_back(m); } hooks_params RCURLY_BRACKET { + // The library hooks parameter is required + ctx.require("library", ctx.loc2pos(@1), ctx.loc2pos(@4)); ctx.stack_.pop_back(); }; @@ -724,6 +736,8 @@ sub_hooks_library: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.push_back(m); } hooks_params RCURLY_BRACKET { + // The library hooks parameter is required + ctx.require("library", ctx.loc2pos(@1), ctx.loc2pos(@4)); // parsing completed }; @@ -758,6 +772,7 @@ expired_leases_processing: EXPIRED_LEASES_PROCESSING { ctx.stack_.push_back(m); ctx.enter(ctx.EXPIRED_LEASES_PROCESSING); } COLON LCURLY_BRACKET expired_leases_params RCURLY_BRACKET { + // No expired lease parameter is required ctx.stack_.pop_back(); ctx.leave(); }; @@ -852,6 +867,9 @@ subnet6: LCURLY_BRACKET { // ctx.stack_.back()->set("renew-timer", renew); // } // } + + // The subnet subnet6 parameter is required + ctx.require("subnet", ctx.loc2pos(@1), ctx.loc2pos(@4)); ctx.stack_.pop_back(); }; @@ -860,6 +878,8 @@ sub_subnet6: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.push_back(m); } subnet6_params RCURLY_BRACKET { + // The subnet subnet6 parameter is required + ctx.require("subnet", ctx.loc2pos(@1), ctx.loc2pos(@4)); // parsing completed }; @@ -956,6 +976,16 @@ option_def_list: OPTION_DEF { ctx.leave(); }; +// This defines the top level scope when the parser is told to parse +// option definitions. It works as a subset limited to option +// definitions +sub_option_def_list: LCURLY_BRACKET { + ElementPtr m(new MapElement(ctx.loc2pos(@1))); + ctx.stack_.push_back(m); +} option_def_list RCURLY_BRACKET { + // parsing completed +}; + // This defines the content of option-def. It may be empty, // have one entry or multiple entries separated by comma. option_def_list_content: %empty @@ -973,6 +1003,10 @@ option_def_entry: LCURLY_BRACKET { ctx.stack_.back()->add(m); ctx.stack_.push_back(m); } option_def_params RCURLY_BRACKET { + // The name, code and type option def parameters are required. + ctx.require("name", ctx.loc2pos(@1), ctx.loc2pos(@4)); + ctx.require("code", ctx.loc2pos(@1), ctx.loc2pos(@4)); + ctx.require("type", ctx.loc2pos(@1), ctx.loc2pos(@4)); ctx.stack_.pop_back(); }; @@ -984,6 +1018,10 @@ sub_option_def: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.push_back(m); } option_def_params RCURLY_BRACKET { + // The name, code and type option def parameters are required. + ctx.require("name", ctx.loc2pos(@1), ctx.loc2pos(@4)); + ctx.require("code", ctx.loc2pos(@1), ctx.loc2pos(@4)); + ctx.require("type", ctx.loc2pos(@1), ctx.loc2pos(@4)); // parsing completed }; @@ -1088,6 +1126,7 @@ option_data_entry: LCURLY_BRACKET { ctx.stack_.back()->add(m); ctx.stack_.push_back(m); } option_data_params RCURLY_BRACKET { + /// @todo: the code or name parameters are required. ctx.stack_.pop_back(); }; @@ -1099,6 +1138,7 @@ sub_option_data: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.push_back(m); } option_data_params RCURLY_BRACKET { + /// @todo: the code or name parameters are required. // parsing completed }; @@ -1178,6 +1218,8 @@ pool_list_entry: LCURLY_BRACKET { ctx.stack_.back()->add(m); ctx.stack_.push_back(m); } pool_params RCURLY_BRACKET { + // The pool parameter is required. + ctx.require("pool", ctx.loc2pos(@1), ctx.loc2pos(@4)); ctx.stack_.pop_back(); }; @@ -1186,7 +1228,8 @@ sub_pool6: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.push_back(m); } pool_params RCURLY_BRACKET { - // parsing completed + // The pool parameter is required. + ctx.require("pool", ctx.loc2pos(@1), ctx.loc2pos(@4)); }; pool_params: pool_param @@ -1242,6 +1285,10 @@ pd_pool_entry: LCURLY_BRACKET { ctx.stack_.back()->add(m); ctx.stack_.push_back(m); } pd_pool_params RCURLY_BRACKET { + // The prefix, prefix len and delegated len parameters are required. + ctx.require("prefix", ctx.loc2pos(@1), ctx.loc2pos(@4)); + ctx.require("prefix-len", ctx.loc2pos(@1), ctx.loc2pos(@4)); + ctx.require("delegated-len", ctx.loc2pos(@1), ctx.loc2pos(@4)); ctx.stack_.pop_back(); }; @@ -1250,6 +1297,10 @@ sub_pd_pool: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.push_back(m); } pd_pool_params RCURLY_BRACKET { + // The prefix, prefix len and delegated len parameters are required. + ctx.require("prefix", ctx.loc2pos(@1), ctx.loc2pos(@4)); + ctx.require("prefix-len", ctx.loc2pos(@1), ctx.loc2pos(@4)); + ctx.require("delegated-len", ctx.loc2pos(@1), ctx.loc2pos(@4)); // parsing completed }; @@ -1324,6 +1375,7 @@ reservation: LCURLY_BRACKET { ctx.stack_.back()->add(m); ctx.stack_.push_back(m); } reservation_params RCURLY_BRACKET { + /// @todo: an identifier parameter is required. ctx.stack_.pop_back(); }; @@ -1332,6 +1384,7 @@ sub_reservation: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.push_back(m); } reservation_params RCURLY_BRACKET { + /// @todo: an identifier parameter is required. // parsing completed }; @@ -1343,7 +1396,7 @@ not_empty_reservation_params: reservation_param | not_empty_reservation_params COMMA reservation_param ; -// @todo probably need to add mac-address as well here +/// @todo probably need to add mac-address as well here reservation_param: duid | reservation_client_classes | ip_addresses @@ -1460,6 +1513,8 @@ client_class: LCURLY_BRACKET { ctx.stack_.back()->add(m); ctx.stack_.push_back(m); } client_class_params RCURLY_BRACKET { + // The name client class parameter is required. + ctx.require("name", ctx.loc2pos(@1), ctx.loc2pos(@4)); ctx.stack_.pop_back(); }; @@ -1496,6 +1551,8 @@ server_id: SERVER_ID { ctx.stack_.push_back(m); ctx.enter(ctx.SERVER_ID); } COLON LCURLY_BRACKET server_id_params RCURLY_BRACKET { + // The type parameter is required. + ctx.require("type", ctx.loc2pos(@4), ctx.loc2pos(@6)); ctx.stack_.pop_back(); ctx.leave(); }; @@ -1599,6 +1656,8 @@ dhcp_ddns: DHCP_DDNS { ctx.stack_.push_back(m); ctx.enter(ctx.DHCP_DDNS); } COLON LCURLY_BRACKET dhcp_ddns_params RCURLY_BRACKET { + // The enable updates DHCP DDNS parameter is required. + ctx.require("enable-updates", ctx.loc2pos(@4), ctx.loc2pos(@6)); ctx.stack_.pop_back(); ctx.leave(); }; @@ -1608,6 +1667,8 @@ sub_dhcp_ddns: LCURLY_BRACKET { ElementPtr m(new MapElement(ctx.loc2pos(@1))); ctx.stack_.push_back(m); } dhcp_ddns_params RCURLY_BRACKET { + // The enable updates DHCP DDNS parameter is required. + ctx.require("enable-updates", ctx.loc2pos(@1), ctx.loc2pos(@4)); // parsing completed }; diff --git a/src/bin/dhcp6/parser_context.cc b/src/bin/dhcp6/parser_context.cc index b56a9259be..b050c85f5c 100644 --- a/src/bin/dhcp6/parser_context.cc +++ b/src/bin/dhcp6/parser_context.cc @@ -94,6 +94,21 @@ Parser6Context::loc2pos(isc::dhcp::location& loc) return (isc::data::Element::Position(file, line, pos)); } +void +Parser6Context::require(const std::string& name, + isc::data::Element::Position open_loc, + isc::data::Element::Position close_loc) +{ + ConstElementPtr value = stack_.back()->get(name); + if (!value) { + isc_throw(Dhcp6ParseError, + "missing parameter '" << name << "' (" + << stack_.back()->getPosition() << ") [" + << contextName() << " map between " + << open_loc << " and " << close_loc << "]"); + } +} + void Parser6Context::enter(const ParserContext& ctx) { diff --git a/src/bin/dhcp6/parser_context.h b/src/bin/dhcp6/parser_context.h index 1d42567e0e..4f9d3deb11 100644 --- a/src/bin/dhcp6/parser_context.h +++ b/src/bin/dhcp6/parser_context.h @@ -76,6 +76,9 @@ public: /// This will parse the input as host-reservation. PARSER_HOST_RESERVATION, + /// This will parse the input option definitions (for tests). + PARSER_OPTION_DEFS, + /// This will parse the input as option definition. PARSER_OPTION_DEF, @@ -179,6 +182,19 @@ public: /// @return Position in format accepted by Element isc::data::Element::Position loc2pos(isc::dhcp::location& loc); + /// @brief Check if a required parameter is present + /// + /// Check if a required parameter is present in the map at the top + /// of the stack and raise an error when it is not. + /// + /// @param name name of the parameter expected to be present + /// @param open_loc location of the opening curly bracket + /// @param close_loc ocation of the closing curly bracket + /// @throw Dhcp6ParseError + void require(const std::string& name, + isc::data::Element::Position open_loc, + isc::data::Element::Position close_loc); + /// @brief Defines syntactic contexts for lexical tie-ins typedef enum { ///< This one is used in pure JSON mode. diff --git a/src/bin/dhcp6/tests/config_parser_unittest.cc b/src/bin/dhcp6/tests/config_parser_unittest.cc index 9b71b08cca..0ff4fcced1 100644 --- a/src/bin/dhcp6/tests/config_parser_unittest.cc +++ b/src/bin/dhcp6/tests/config_parser_unittest.cc @@ -1998,7 +1998,13 @@ TEST_F(Dhcp6ParserTest, invalidPdPools) { int num_msgs = sizeof(config)/sizeof(char*); for (unsigned int i = 0; i < num_msgs; i++) { // Convert JSON string to Elements. - ASSERT_NO_THROW(json = parseDHCP6(config[i])); + // The 3 first configs should fail to parse. + if (i < 3) { + EXPECT_THROW(parseDHCP6(config[i]), Dhcp6ParseError); + json = parseJSON(config[i]); + } else { + ASSERT_NO_THROW(json = parseDHCP6(config[i])); + } // Configuration processing should fail without a throw. ASSERT_NO_THROW(x = configureDhcp6Server(srv_, json)); @@ -2024,7 +2030,7 @@ TEST_F(Dhcp6ParserTest, optionDefIpv6Address) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); extractConfig(config); // Make sure that the particular option definition does not exist. @@ -2064,9 +2070,8 @@ TEST_F(Dhcp6ParserTest, optionDefIpv6Address) { // Let's apply empty configuration. This removes the option definitions // configuration and should result in removal of the option 100 from the - // libdhcp++. - config = "{ }"; - json = parseOPTION_DEF(config); + // libdhcp++. Note DHCP6 or OPTION_DEFS parsers do not accept empty maps. + json.reset(new MapElement()); ASSERT_NO_THROW(status = configureDhcp6Server(srv_, json)); checkResult(status, 0); @@ -2089,7 +2094,7 @@ TEST_F(Dhcp6ParserTest, optionDefRecord) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); extractConfig(config); // Make sure that the particular option definition does not exist. @@ -2143,7 +2148,7 @@ TEST_F(Dhcp6ParserTest, optionDefMultiple) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); extractConfig(config); // Make sure that the option definitions do not exist yet. @@ -2211,7 +2216,7 @@ TEST_F(Dhcp6ParserTest, optionDefDuplicate) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); // Make sure that the option definition does not exist yet. ASSERT_FALSE(CfgMgr::instance().getStagingCfg()-> @@ -2250,7 +2255,7 @@ TEST_F(Dhcp6ParserTest, optionDefArray) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); extractConfig(config); // Make sure that the particular option definition does not exist. @@ -2291,7 +2296,7 @@ TEST_F(Dhcp6ParserTest, optionDefEncapsulate) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); extractConfig(config); // Make sure that the particular option definition does not exist. @@ -2331,7 +2336,7 @@ TEST_F(Dhcp6ParserTest, optionDefInvalidName) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); // Use the configuration string to create new option definition. ConstElementPtr status; @@ -2356,7 +2361,7 @@ TEST_F(Dhcp6ParserTest, optionDefInvalidType) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); // Use the configuration string to create new option definition. ConstElementPtr status; @@ -2382,7 +2387,7 @@ TEST_F(Dhcp6ParserTest, optionDefInvalidRecordType) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); // Use the configuration string to create new option definition. ConstElementPtr status; @@ -2408,7 +2413,7 @@ TEST_F(Dhcp6ParserTest, optionIntegerTypes) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); // Use the configuration string to create new option definition. ConstElementPtr status; @@ -2434,7 +2439,7 @@ TEST_F(Dhcp6ParserTest, optionDefInvalidEncapsulatedSpace) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); // Use the configuration string to create new option definition. ConstElementPtr status; @@ -2463,7 +2468,7 @@ TEST_F(Dhcp6ParserTest, optionDefEncapsulatedSpaceAndArray) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); // Use the configuration string to create new option definition. ConstElementPtr status; @@ -2489,7 +2494,7 @@ TEST_F(Dhcp6ParserTest, optionDefEncapsulateOwnSpace) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); // Use the configuration string to create new option definition. ConstElementPtr status; @@ -2519,7 +2524,7 @@ TEST_F(Dhcp6ParserTest, optionStandardDefOverride) { " } ]" "}"; ConstElementPtr json; - ASSERT_NO_THROW(json = parseOPTION_DEF(config)); + ASSERT_NO_THROW(json = parseOPTION_DEFS(config)); OptionDefinitionPtr def = CfgMgr::instance().getStagingCfg()-> getCfgOptionDef()->get(DHCP6_OPTION_SPACE, 100); @@ -2553,7 +2558,7 @@ TEST_F(Dhcp6ParserTest, optionStandardDefOverride) { " \"space\": \"dhcp6\"" " } ]" "}"; - json = parseOPTION_DEF(config); + json = parseOPTION_DEFS(config); // Use the configuration string to create new option definition. EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json)); @@ -2575,7 +2580,7 @@ TEST_F(Dhcp6ParserTest, optionStandardDefOverride) { " \"space\": \"dhcp6\"" " } ]" "}"; - json = parseOPTION_DEF(config); + json = parseOPTION_DEFS(config); // Use the configuration string to create new option definition. EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json)); diff --git a/src/bin/dhcp6/tests/dhcp6_test_utils.h b/src/bin/dhcp6/tests/dhcp6_test_utils.h index df99389157..7b99db54b9 100644 --- a/src/bin/dhcp6/tests/dhcp6_test_utils.h +++ b/src/bin/dhcp6/tests/dhcp6_test_utils.h @@ -696,11 +696,11 @@ parseDHCP6(const std::string& in, bool verbose = false) /// @param verbose display the exception message when it fails /// @return ElementPtr structure representing parsed JSON inline isc::data::ElementPtr -parseOPTION_DEF(const std::string& in, bool verbose = false) +parseOPTION_DEFS(const std::string& in, bool verbose = false) { try { isc::dhcp::Parser6Context ctx; - return (ctx.parseString(in, isc::dhcp::Parser6Context::PARSER_OPTION_DEF)); + return (ctx.parseString(in, isc::dhcp::Parser6Context::PARSER_OPTION_DEFS)); } catch (const std::exception& ex) { if (verbose) { diff --git a/src/bin/dhcp6/tests/parser_unittest.cc b/src/bin/dhcp6/tests/parser_unittest.cc index 811f57dc6f..ed1d35f030 100644 --- a/src/bin/dhcp6/tests/parser_unittest.cc +++ b/src/bin/dhcp6/tests/parser_unittest.cc @@ -506,6 +506,13 @@ TEST(ParserTest, errors) { Parser6Context::PARSER_DHCP6, ":2.2-21: got unexpected keyword " "\"preferred_lifetime\" in Dhcp6 map."); + + // missing parameter + testError("{ \"name\": \"foo\",\n" + " \"code\": 123 }\n", + Parser6Context::PARSER_OPTION_DEF, + "missing parameter 'type' (:1:1) " + "[option-def map between :1:1 and :2:15]"); } // Check unicode escapes -- 2.47.2