blob: 544ac96615fa828d4dacba66ec0235fc95871f1a [file] [log] [blame]
/* 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 <algorithm>
#include <string>
#include <vector>
#include <map>
#include <openssl/bio.h>
#include <openssl/conf.h>
#include <gtest/gtest.h>
#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<std::string, std::vector<std::pair<std::string, std::string>>>;
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 &section = 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_SECTION_num_items(conf->sections), model.size());
EXPECT_EQ(lh_CONF_VALUE_num_items(conf->values), total_values);
}
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(BIO_new_mem_buf(t.in.data(), t.in.size()));
ASSERT_TRUE(bio);
bssl::UniquePtr<CONF> 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(BIO_new_mem_buf(t, strlen(t)));
ASSERT_TRUE(bio);
bssl::UniquePtr<CONF> 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<std::string> 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<std::string> result;
auto append_to_vector = [](const char *elem, size_t len, void *arg) -> int {
auto *vec = static_cast<std::vector<std::string> *>(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);
}
}