config_parser: Rewrite file parsing logic

This grealty improves the correctness of the config parser to
better match the way systemd parses config files. It also allows us to
provide errors / warnings when the file format doesn't match
expectations.

Tested: On an existing BMC system to verify NTP / DHCP settings were
still parsed as expected.

Change-Id: I1f0cb631f680f5957a29accaa749d491e6f68faf
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/src/config_parser.cpp b/src/config_parser.cpp
index a6b7e36..26226d4 100644
--- a/src/config_parser.cpp
+++ b/src/config_parser.cpp
@@ -1,8 +1,9 @@
 #include "config_parser.hpp"
 
-#include <fstream>
-#include <regex>
-#include <string>
+#include <stdplus/exception.hpp>
+#include <stdplus/fd/create.hpp>
+#include <stdplus/fd/line.hpp>
+#include <utility>
 
 namespace phosphor
 {
@@ -35,57 +36,112 @@
     return kit->second;
 }
 
-void Parser::setValue(const std::string& section, const std::string& key,
-                      const std::string& value)
+inline bool isspace(char c) noexcept
 {
-    auto sit = sections.find(section);
-    if (sit == sections.end())
-    {
-        std::tie(sit, std::ignore) = sections.emplace(section, KeyValuesMap{});
-    }
-    auto kit = sit->second.find(key);
-    if (kit == sit->second.end())
-    {
-        std::tie(kit, std::ignore) = sit->second.emplace(key, ValueList{});
-    }
-    kit->second.push_back(value);
+    return c == ' ' || c == '\t';
 }
 
+inline bool iscomment(char c) noexcept
+{
+    return c == '#' || c == ';';
+}
+
+static void removePadding(std::string_view& str) noexcept
+{
+    size_t idx = str.size();
+    for (; idx > 0 && isspace(str[idx - 1]); idx--)
+        ;
+    str.remove_suffix(str.size() - idx);
+
+    idx = 0;
+    for (; idx < str.size() && isspace(str[idx]); idx++)
+        ;
+    str.remove_prefix(idx);
+}
+
+struct Parse
+{
+    SectionMap sections;
+    KeyValuesMap* section = nullptr;
+
+    void pumpSection(std::string_view line)
+    {
+        auto cpos = line.find(']');
+        auto s = line.substr(0, cpos);
+        auto it = sections.find(s);
+        if (it == sections.end())
+        {
+            std::tie(it, std::ignore) =
+                sections.emplace(Section(s), KeyValuesMap{});
+        }
+        section = &it->second;
+    }
+
+    void pumpKV(std::string_view line)
+    {
+        auto epos = line.find('=');
+        if (epos == line.npos)
+        {
+            return;
+        }
+        if (section == nullptr)
+        {
+            return;
+        }
+        auto k = line.substr(0, epos);
+        removePadding(k);
+        auto v = line.substr(epos + 1);
+        removePadding(v);
+
+        auto it = section->find(k);
+        if (it == section->end())
+        {
+            std::tie(it, std::ignore) = section->emplace(Key(k), ValueList{});
+        }
+        it->second.emplace_back(v);
+    }
+
+    void pump(std::string_view line)
+    {
+        for (size_t i = 0; i < line.size(); ++i)
+        {
+            auto c = line[i];
+            if (iscomment(c))
+            {
+                return;
+            }
+            else if (c == '[')
+            {
+                return pumpSection(line.substr(i + 1));
+            }
+            else if (!isspace(c))
+            {
+                return pumpKV(line.substr(i));
+            }
+        }
+    }
+};
+
 void Parser::setFile(const fs::path& filename)
 {
-    std::fstream stream(filename, std::fstream::in);
-    if (!stream.is_open())
+    Parse parse;
+
+    try
     {
-        return;
-    }
-    // clear all the section data.
-    sections.clear();
-    static const std::regex commentRegex{R"x(\s*[;#])x"};
-    static const std::regex sectionRegex{R"x(\s*\[([^\]]+)\])x"};
-    static const std::regex valueRegex{R"x(\s*(\S[^ \t=]*)\s*=\s*(\S+)\s*$)x"};
-    std::string section;
-    std::smatch pieces;
-    for (std::string line; std::getline(stream, line);)
-    {
-        if (line.empty() || std::regex_match(line, pieces, commentRegex))
+        auto fd = stdplus::fd::open(filename.c_str(),
+                                    stdplus::fd::OpenAccess::ReadOnly);
+        stdplus::fd::LineReader reader(fd);
+        while (true)
         {
-            // skip comment lines and blank lines
-        }
-        else if (std::regex_match(line, pieces, sectionRegex))
-        {
-            if (pieces.size() == 2)
-            {
-                section = pieces[1].str();
-            }
-        }
-        else if (std::regex_match(line, pieces, valueRegex))
-        {
-            if (pieces.size() == 3)
-            {
-                setValue(section, pieces[1].str(), pieces[2].str());
-            }
+            parse.pump(*reader.readLine());
         }
     }
+    catch (...)
+    {
+        // TODO: Pass exceptions once callers can handle them
+    }
+
+    this->sections = std::move(parse.sections);
 }
 
 } // namespace config
diff --git a/src/config_parser.hpp b/src/config_parser.hpp
index 3065162..9a30833 100644
--- a/src/config_parser.hpp
+++ b/src/config_parser.hpp
@@ -48,14 +48,6 @@
     const ValueList& getValues(std::string_view section,
                                std::string_view key) const noexcept;
 
-    /** @brief Set the value of the given key and section.
-     *  @param[in] section - section name.
-     *  @param[in] key - key name.
-     *  @param[in] value - value.
-     */
-    void setValue(const std::string& section, const std::string& key,
-                  const std::string& value);
-
     /** @brief Set the file name and parse it.
      *  @param[in] filename - Absolute path of the file.
      */
diff --git a/test/test_config_parser.cpp b/test/test_config_parser.cpp
index ec90a5f..a49b23e 100644
--- a/test/test_config_parser.cpp
+++ b/test/test_config_parser.cpp
@@ -51,6 +51,9 @@
     EXPECT_THAT(parser.getValues("Match", "Name"), ElementsAre("eth0"));
     EXPECT_THAT(parser.getValues("DHCP", "ClientIdentifier"),
                 ElementsAre("mac"));
+    EXPECT_THAT(parser.getValues("Network", "DHCP"),
+                ElementsAre("true", "false #hi", "yes"));
+    EXPECT_THAT(parser.getValues(" SEC ", "'DHCP#'"), ElementsAre("\"#hi\""));
     EXPECT_THAT(parser.getValues("Blah", "nil"), ElementsAre());
     EXPECT_THAT(parser.getValues("Network", "nil"), ElementsAre());
 }