/* Copyright (c) 2021, Google Inc. * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include #include #include #include #include "internal.h" // A |CONF| is an unordered list of sections, where each section contains an // ordered list of (name, value) pairs. using ConfModel = std::map>>; static void ExpectConfEquals(const CONF *conf, const ConfModel &model) { // There is always a default section, even if empty. This is an easy mistake // to make in test data, so test for it. EXPECT_NE(model.find("default"), model.end()) << "Model does not have a default section"; size_t total_values = 0; for (const auto &pair : model) { const std::string §ion = pair.first; SCOPED_TRACE(section); const STACK_OF(CONF_VALUE) *values = NCONF_get_section(conf, section.c_str()); ASSERT_TRUE(values); total_values += pair.second.size(); EXPECT_EQ(sk_CONF_VALUE_num(values), pair.second.size()); // If the lengths do not match, still compare up to the smaller of the two, // to aid debugging. size_t min_len = std::min(sk_CONF_VALUE_num(values), pair.second.size()); for (size_t i = 0; i < min_len; i++) { SCOPED_TRACE(i); const std::string &name = pair.second[i].first; const std::string &value = pair.second[i].second; const CONF_VALUE *v = sk_CONF_VALUE_value(values, i); EXPECT_EQ(v->section, section); EXPECT_EQ(v->name, name); EXPECT_EQ(v->value, value); const char *str = NCONF_get_string(conf, section.c_str(), name.c_str()); ASSERT_NE(str, nullptr); EXPECT_EQ(str, value); if (section == "default") { // nullptr is interpreted as the default section. str = NCONF_get_string(conf, nullptr, name.c_str()); ASSERT_NE(str, nullptr); EXPECT_EQ(str, value); } } } // Unrecognized sections must return nullptr. EXPECT_EQ(NCONF_get_section(conf, "must_not_appear_in_tests"), nullptr); EXPECT_EQ(NCONF_get_string(conf, "must_not_appear_in_tests", "must_not_appear_in_tests"), nullptr); if (!model.empty()) { // Valid section, invalid name. EXPECT_EQ(NCONF_get_string(conf, model.begin()->first.c_str(), "must_not_appear_in_tests"), nullptr); if (!model.begin()->second.empty()) { // Invalid section, valid name. EXPECT_EQ(NCONF_get_string(conf, "must_not_appear_in_tests", model.begin()->second.front().first.c_str()), nullptr); } } // There should not be any other values in |conf|. |conf| currently stores // both sections and values in the same map. EXPECT_EQ(lh_CONF_VALUE_num_items(conf->data), total_values + model.size()); } TEST(ConfTest, Parse) { const struct { std::string in; ConfModel model; } kTests[] = { // Test basic parsing. { R"(# Comment key=value [section_name] key=value2 )", { {"default", {{"key", "value"}}}, {"section_name", {{"key", "value2"}}}, }, }, // If a section is listed multiple times, keys add to the existing one. { R"(key1 = value1 [section1] key2 = value2 [section2] key3 = value3 [default] key4 = value4 [section1] key5 = value5 )", { {"default", {{"key1", "value1"}, {"key4", "value4"}}}, {"section1", {{"key2", "value2"}, {"key5", "value5"}}}, {"section2", {{"key3", "value3"}}}, }, }, // Although the CONF parser internally uses a buffer size of 512 bytes to // read one line, it detects truncation and is able to parse long lines. { std::string(1000, 'a') + " = " + std::string(1000, 'b') + "\n", { {"default", {{std::string(1000, 'a'), std::string(1000, 'b')}}}, }, }, // Trailing backslashes are line continations. { "key=\\\nvalue\nkey2=foo\\\nbar=baz", { {"default", {{"key", "value"}, {"key2", "foobar=baz"}}}, }, }, // To be a line continuation, it must be at the end of the line. { "key=\\\nvalue\nkey2=foo\\ \nbar=baz", { {"default", {{"key", "value"}, {"key2", "foo"}, {"bar", "baz"}}}, }, }, // A line continuation without any following line is ignored. { "key=value\\", { {"default", {{"key", "value"}}}, }, }, // Values may have embedded whitespace, but leading and trailing // whitespace is dropped. { "key = \t foo \t\t\tbar \t ", { {"default", {{"key", "foo \t\t\tbar"}}}, }, }, // Empty sections still end up in the file. { "[section1]\n[section2]\n[section3]\n", { {"default", {}}, {"section1", {}}, {"section2", {}}, {"section3", {}}, }, }, // Section names can contain spaces and punctuation. { "[This! Is. A? Section;]\nkey = value", { {"default", {}}, {"This! Is. A? Section;", {{"key", "value"}}}, }, }, // Trailing data after a section line is ignored. { "[section] key = value\nkey2 = value2\n", { {"default", {}}, {"section", {{"key2", "value2"}}}, }, }, // Comments may appear within a line. Escapes and quotes, however, // suppress the comment character. { R"( key1 = # comment key2 = "# not a comment" key3 = '# not a comment' key4 = `# not a comment` key5 = \# not a comment )", { {"default", { {"key1", ""}, {"key2", "# not a comment"}, {"key3", "# not a comment"}, {"key4", "# not a comment"}, {"key5", "# not a comment"}, }}, }, }, // Quotes may appear in the middle of a string. Inside quotes, escape // sequences like \n are not evaluated. \X always evaluates to X. { R"( key1 = mix "of" 'different' `quotes` key2 = "`'" key3 = "\r\n\b\t\"" key4 = '\r\n\b\t\'' key5 = `\r\n\b\t\`` )", { {"default", { {"key1", "mix of different quotes"}, {"key2", "`'"}, {"key3", "rnbt\""}, {"key4", "rnbt'"}, {"key5", "rnbt`"}, }}, }, }, // Outside quotes, escape sequences like \n are evaluated. Unknown escapes // turn into the character. { R"( key = \r\n\b\t\"\'\`\z )", { {"default", { {"key", "\r\n\b\t\"'`z"}, }}, }, }, // Escapes (but not quoting) work inside section names. { "[section\\ name]\nkey = value\n", { {"default", {}}, {"section name", {{"key", "value"}}}, }, }, // Escapes (but not quoting) are skipped over in key names, but they are // left unevaluated. This is probably a bug. { "key\\ name = value\n", { {"default", {{"key\\ name", "value"}}}, }, }, // Keys can specify sections explicitly with ::. { R"( [section1] default::key1 = value1 section1::key2 = value2 section2::key3 = value3 section1::key4 = value4 section2::key5 = value5 default::key6 = value6 key7 = value7 # section1 )", { {"default", {{"key1", "value1"}, {"key6", "value6"}}}, {"section1", {{"key2", "value2"}, {"key4", "value4"}, {"key7", "value7"}}}, {"section2", {{"key3", "value3"}, {"key5", "value5"}}}, }, }, // Punctuation is allowed in key names. { "key.1 = value\n", { {"default", {{"key.1", "value"}}}, }, }, }; for (const auto &t : kTests) { SCOPED_TRACE(t.in); bssl::UniquePtr bio(BIO_new_mem_buf(t.in.data(), t.in.size())); ASSERT_TRUE(bio); bssl::UniquePtr conf(NCONF_new(nullptr)); ASSERT_TRUE(conf); ASSERT_TRUE(NCONF_load_bio(conf.get(), bio.get(), nullptr)); ExpectConfEquals(conf.get(), t.model); } const char *kInvalidTests[] = { // Missing equals sign. "key", // Unterminated section heading. "[section", // Section names can only contain alphanumeric characters, punctuation, // and escapes. Quotes are not punctuation. "[\"section\"]", // Keys can only contain alphanumeric characters, punctuaion, and escapes. "key name = value", "\"key\" = value", // Variable references have been removed. "key1 = value1\nkey2 = $key1", }; for (const auto &t : kInvalidTests) { SCOPED_TRACE(t); bssl::UniquePtr bio(BIO_new_mem_buf(t, strlen(t))); ASSERT_TRUE(bio); bssl::UniquePtr conf(NCONF_new(nullptr)); ASSERT_TRUE(conf); EXPECT_FALSE(NCONF_load_bio(conf.get(), bio.get(), nullptr)); } } TEST(ConfTest, ParseList) { const struct { const char *list; char sep; bool remove_whitespace; std::vector expected; } kTests[] = { {"", ',', /*remove_whitespace=*/0, {""}}, {"", ',', /*remove_whitespace=*/1, {""}}, {" ", ',', /*remove_whitespace=*/0, {" "}}, {" ", ',', /*remove_whitespace=*/1, {""}}, {"hello world", ',', /*remove_whitespace=*/0, {"hello world"}}, {"hello world", ',', /*remove_whitespace=*/1, {"hello world"}}, {" hello world ", ',', /*remove_whitespace=*/0, {" hello world "}}, {" hello world ", ',', /*remove_whitespace=*/1, {"hello world"}}, {"hello,world", ',', /*remove_whitespace=*/0, {"hello", "world"}}, {"hello,world", ',', /*remove_whitespace=*/1, {"hello", "world"}}, {"hello,,world", ',', /*remove_whitespace=*/0, {"hello", "", "world"}}, {"hello,,world", ',', /*remove_whitespace=*/1, {"hello", "", "world"}}, {"\tab cd , , ef gh ", ',', /*remove_whitespace=*/0, {"\tab cd ", " ", " ef gh "}}, {"\tab cd , , ef gh ", ',', /*remove_whitespace=*/1, {"ab cd", "", "ef gh"}}, }; for (const auto& t : kTests) { SCOPED_TRACE(t.list); SCOPED_TRACE(t.sep); SCOPED_TRACE(t.remove_whitespace); std::vector result; auto append_to_vector = [](const char *elem, size_t len, void *arg) -> int { auto *vec = static_cast *>(arg); vec->push_back(std::string(elem, len)); return 1; }; ASSERT_TRUE(CONF_parse_list(t.list, t.sep, t.remove_whitespace, append_to_vector, &result)); EXPECT_EQ(result, t.expected); } }