diff --git a/io/inifile.cpp b/io/inifile.cpp index bac5877..3b51266 100644 --- a/io/inifile.cpp +++ b/io/inifile.cpp @@ -1,74 +1,78 @@ #include "./inifile.h" +#include "../conversion/stringconversion.h" + #include using namespace std; namespace CppUtilities { +/// \cond +static void addChar(char c, std::string &to, std::size_t &padding) +{ + if (c == ' ') { + ++padding; + return; + } + if (!to.empty()) { + while (padding) { + to += ' '; + --padding; + } + } else { + padding = 0; + } + to += c; +}; +/// \endcond + /*! * \class IniFile - * \brief The IniFile class parses and makes INI files. - * \todo - * - Preserve difference between absence of equal sign and empty value after equal sign. - * - Preserve order within section. - * - Preserve comments. + * \brief The IniFile class allows parsing and writing INI files. + * \sa See AdvancedIniFile for a more advanced version which preserves more particularities of the original file. */ /*! * \brief Parses all data from the specified \a inputStream. + * \throws Throws an std::ios_base::failure when an IO error (other than end-of-file) occurs. */ void IniFile::parse(std::istream &inputStream) { - // define variable for state machine - enum State { Init, Comment, ScopeName, Key, Value } state = Init; - // current character - char c; - // number of postponed whitespaces - unsigned int whitespace = 0; - // current scope, key and value - string scope, key, value; - scope.reserve(16); + inputStream.exceptions(ios_base::failbit | ios_base::badbit); + + // define variables for state machine + enum State { Init, Comment, SectionName, Key, Value } state = Init; + char currentCharacter; + + // keep track of current scope, key and value and number of postponed whitespaces + std::size_t whitespace = 0; + string sectionName, key, value; + sectionName.reserve(16); key.reserve(16); value.reserve(256); - // define actions for state machine - // called when key/value pair is complete - const auto finishKeyValue = [&state, &scope, &key, &value, &whitespace, this] { + + // define function to add a key/value pair + const auto finishKeyValue = [&state, §ionName, &key, &value, &whitespace, this] { if (key.empty() && value.empty() && state != Value) { return; } - if (m_data.empty() || m_data.back().first != scope) { - m_data.emplace_back(make_pair(scope, decltype(m_data)::value_type::second_type())); + if (m_data.empty() || m_data.back().first != sectionName) { + m_data.emplace_back(make_pair(sectionName, decltype(m_data)::value_type::second_type())); } m_data.back().second.insert(make_pair(key, value)); key.clear(); value.clear(); whitespace = 0; }; - // called to add current character to current key or value - const auto addChar = [&whitespace, &c](string &to) { - if (c == ' ') { - ++whitespace; - } else { - if (!to.empty()) { - while (whitespace) { - to += ' '; - --whitespace; - } - } else { - whitespace = 0; - } - to += c; - } - }; - // thorw an exception when an IO error occurs - inputStream.exceptions(ios_base::failbit | ios_base::badbit); + // parse the file char by char try { - while (inputStream.get(c)) { + while (inputStream.get(currentCharacter)) { + // handle next character switch (state) { case Init: - switch (c) { + switch (currentCharacter) { case '\n': break; case '#': @@ -79,16 +83,16 @@ void IniFile::parse(std::istream &inputStream) state = Value; break; case '[': - scope.clear(); - state = ScopeName; + sectionName.clear(); + state = SectionName; break; default: - addChar(key); + addChar(currentCharacter, key, whitespace); state = Key; } break; case Key: - switch (c) { + switch (currentCharacter) { case '\n': finishKeyValue(); state = Init; @@ -102,28 +106,28 @@ void IniFile::parse(std::istream &inputStream) state = Value; break; default: - addChar(key); + addChar(currentCharacter, key, whitespace); } break; case Comment: - switch (c) { + switch (currentCharacter) { case '\n': state = Init; break; default:; } break; - case ScopeName: - switch (c) { + case SectionName: + switch (currentCharacter) { case ']': state = Init; break; default: - scope += c; + sectionName += currentCharacter; } break; case Value: - switch (c) { + switch (currentCharacter) { case '\n': finishKeyValue(); state = Init; @@ -133,7 +137,7 @@ void IniFile::parse(std::istream &inputStream) state = Comment; break; default: - addChar(value); + addChar(currentCharacter, value, whitespace); } break; } @@ -142,26 +146,300 @@ void IniFile::parse(std::istream &inputStream) if (!inputStream.eof()) { throw; } - // we just reached the end of the file - // don't forget to save the last key/value pair + + // handle eof finishKeyValue(); } } /*! * \brief Write the current data to the specified \a outputStream. + * \throws Throws an std::ios_base::failure when an IO error occurs. */ void IniFile::make(ostream &outputStream) { - // thorw an exception when an IO error occurs outputStream.exceptions(ios_base::failbit | ios_base::badbit); - for (const auto &scope : m_data) { - outputStream << '[' << scope.first << ']' << '\n'; - for (const auto &field : scope.second) { + for (const auto §ion : m_data) { + outputStream << '[' << section.first << ']' << '\n'; + for (const auto &field : section.second) { outputStream << field.first << '=' << field.second << '\n'; } outputStream << '\n'; } } +/*! + * \class AdvancedIniFile + * \brief The AdvancedIniFile class allows parsing and writing INI files. + * + * In contrast to IniFile this class preserves + * - the difference between absence of an equal sign and an empty value after equal sign. + * - the order of the fields within a section. + * - alignment of equal signs via extra spaces between key and equal sign. + * - comments. + * + * In the following description the word "element" is used to refer to a section or a field. + * + * The parsed data or data to be written is directly exposed via the "sections" member variable of this class. There are also + * convenience functions to find a particular element. + * + * Since the preceding comment of an element usually belongs to it each element has a "precedingCommentBlock" member variable. + * If a file ends with a comment an implicit section is added at the end which contains that comment as "precedingCommentBlock". + * Comments are not stripped from newline and '#' characters. When altering/adding comments be sure to use newline and '#' + * characters as needed. + * + * Comments appearing in the same line as an element are stored using the "followingInlineComment" member variable. + * + * \remarks + * The AdvancedIniFile class is still experimental. It might be modified in an incompatible way or even removed + * in the next minor or patch release. + */ + +/*! + * \class AdvancedIniFile::Section + * \brief The AdvancedIniFile::Section class represents a section within an INI file. + */ + +/*! + * \class AdvancedIniFile::Field + * \brief The AdvancedIniFile::Field class represents a field within an INI file. + */ + +/*! + * \brief Parses all data from the specified \a inputStream. + * \remarks + * Does *not* strip newline and '#' characters from comments. So far there is no option (or separate function) to help with that. + * \throws Throws an std::ios_base::failure when an IO error (other than end-of-file) occurs. + */ +void AdvancedIniFile::parse(std::istream &inputStream, IniFileParseOptions) +{ + inputStream.exceptions(ios_base::failbit | ios_base::badbit); + + // define variables for state machine + enum State { Init, CommentBlock, InlineComment, SectionInlineComment, SectionName, SectionEnd, Key, Value } state = Init; + char currentCharacter; + + // keep track of current comment, section, key and value + std::string commentBlock, inlineComment, sectionName, key, value; + std::size_t keyPadding = 0, valuePadding = 0; + commentBlock.reserve(256); + inlineComment.reserve(256); + sectionName.reserve(16); + key.reserve(16); + value.reserve(256); + + // define function to add entry + const auto finishKeyValue = [&, this] { + if (key.empty() && value.empty() && state != Value) { + return; + } + if (sections.empty()) { + sections.emplace_back(Section{ .flags = IniFileSectionFlags::Implicit }); + } + sections.back().fields.emplace_back(Field{ .key = key, + .value = value, + .precedingCommentBlock = commentBlock, + .followingInlineComment = inlineComment, + .paddedKeyLength = key.size() + keyPadding, + .flags = (!value.empty() || state == Value ? IniFileFieldFlags::HasValue : IniFileFieldFlags::None) }); + key.clear(); + value.clear(); + commentBlock.clear(); + inlineComment.clear(); + keyPadding = valuePadding = 0; + }; + + // parse the file char by char + try { + while (inputStream.get(currentCharacter)) { + // handle next character + switch (state) { + case Init: + switch (currentCharacter) { + case '\n': + commentBlock += currentCharacter; + break; + case '#': + commentBlock += currentCharacter; + state = CommentBlock; + break; + case '=': + keyPadding = valuePadding = 0; + state = Value; + break; + case '[': + sectionName.clear(); + state = SectionName; + break; + default: + addChar(currentCharacter, key, keyPadding); + state = Key; + } + break; + case Key: + switch (currentCharacter) { + case '\n': + finishKeyValue(); + state = Init; + break; + case '#': + state = InlineComment; + inlineComment += currentCharacter; + break; + case '=': + valuePadding = 0; + state = Value; + break; + default: + addChar(currentCharacter, key, keyPadding); + } + break; + case CommentBlock: + switch (currentCharacter) { + case '\n': + state = Init; + [[fallthrough]]; + default: + commentBlock += currentCharacter; + } + break; + case InlineComment: + case SectionInlineComment: + switch (currentCharacter) { + case '\n': + switch (state) { + case InlineComment: + finishKeyValue(); + break; + case SectionInlineComment: + sections.back().followingInlineComment = inlineComment; + inlineComment.clear(); + break; + default:; + } + state = Init; + break; + default: + inlineComment += currentCharacter; + } + break; + case SectionName: + switch (currentCharacter) { + case ']': + state = SectionEnd; + sections.emplace_back(Section{ .name = sectionName }); + sections.back().precedingCommentBlock = commentBlock; + sectionName.clear(); + commentBlock.clear(); + break; + default: + sectionName += currentCharacter; + } + break; + case SectionEnd: + switch (currentCharacter) { + case '\n': + state = Init; + break; + case '#': + state = SectionInlineComment; + inlineComment += currentCharacter; + break; + case '=': + keyPadding = valuePadding = 0; + state = Value; + break; + case ' ': + break; + default: + state = Key; + addChar(currentCharacter, key, keyPadding); + } + break; + case Value: + switch (currentCharacter) { + case '\n': + finishKeyValue(); + state = Init; + break; + case '#': + state = InlineComment; + inlineComment += currentCharacter; + break; + default: + addChar(currentCharacter, value, valuePadding); + } + break; + } + } + } catch (const std::ios_base::failure &) { + if (!inputStream.eof()) { + throw; + } + + // handle eof + switch (state) { + case Init: + case CommentBlock: + sections.emplace_back(Section{ .precedingCommentBlock = commentBlock, .flags = IniFileSectionFlags::Implicit }); + break; + case SectionName: + sections.emplace_back(Section{ .name = sectionName, .precedingCommentBlock = commentBlock, .flags = IniFileSectionFlags::Implicit }); + break; + case SectionEnd: + case SectionInlineComment: + sections.emplace_back(Section{ .name = sectionName, .precedingCommentBlock = commentBlock, .followingInlineComment = inlineComment }); + break; + case Key: + case Value: + case InlineComment: + finishKeyValue(); + break; + } + } +} + +/*! + * \brief Write the current data to the specified \a outputStream. + * \throws Throws an std::ios_base::failure when an IO error occurs. + * \remarks + * Might write garbage if comments do not contain newline and '#' characters as needed. So far there is no option to insert these characters + * automatically as needed. + */ +void AdvancedIniFile::make(ostream &outputStream, IniFileMakeOptions) +{ + outputStream.exceptions(ios_base::failbit | ios_base::badbit); + for (const auto §ion : sections) { + if (!section.precedingCommentBlock.empty()) { + outputStream << section.precedingCommentBlock; + } + if (!(section.flags & IniFileSectionFlags::Implicit)) { + outputStream << '[' << section.name << ']'; + if (!section.followingInlineComment.empty()) { + outputStream << ' ' << section.followingInlineComment; + } + outputStream << '\n'; + } + for (const auto &field : section.fields) { + if (!field.precedingCommentBlock.empty()) { + outputStream << field.precedingCommentBlock; + } + outputStream << field.key; + for (auto charsWritten = field.key.size(); charsWritten < field.paddedKeyLength; ++charsWritten) { + outputStream << ' '; + } + if (field.flags & IniFileFieldFlags::HasValue) { + outputStream << '=' << ' ' << field.value; + } + if (!field.followingInlineComment.empty()) { + if (field.flags & IniFileFieldFlags::HasValue) { + outputStream << ' '; + } + outputStream << field.followingInlineComment; + } + outputStream << '\n'; + } + } +} + } // namespace CppUtilities diff --git a/io/inifile.h b/io/inifile.h index f8a7ee7..997fd07 100644 --- a/io/inifile.h +++ b/io/inifile.h @@ -2,9 +2,12 @@ #define IOUTILITIES_INIFILE_H #include "../global.h" +#include "../misc/flagenumclass.h" +#include #include #include +#include #include #include @@ -54,6 +57,190 @@ inline const IniFile::ScopeList &IniFile::data() const return m_data; } +enum class IniFileParseOptions { + None = 0, +}; + +enum class IniFileMakeOptions { + None = 0, +}; + +enum class IniFileFieldFlags { + None = 0, + HasValue = (1 << 0), +}; + +enum class IniFileSectionFlags { + None = 0, + Implicit = (1 << 0), + Truncated = (1 << 1), +}; + +struct CPP_UTILITIES_EXPORT AdvancedIniFile { + struct Field { + std::string key; + std::string value; + std::string precedingCommentBlock; + std::string followingInlineComment; + std::size_t paddedKeyLength = 0; + IniFileFieldFlags flags = IniFileFieldFlags::None; + }; + using FieldList = std::vector; + struct Section { + FieldList::iterator findField(std::string_view key); + FieldList::const_iterator findField(std::string_view key) const; + FieldList::iterator findField(FieldList::iterator after, std::string_view key); + FieldList::const_iterator findField(FieldList::iterator after, std::string_view key) const; + FieldList::iterator fieldEnd(); + FieldList::const_iterator fieldEnd() const; + + std::string name; + FieldList fields; + std::string precedingCommentBlock; + std::string followingInlineComment; + IniFileSectionFlags flags = IniFileSectionFlags::None; + }; + using SectionList = std::vector
; + + SectionList::iterator findSection(std::string_view sectionName); + SectionList::const_iterator findSection(std::string_view sectionName) const; + SectionList::iterator findSection(SectionList::iterator after, std::string_view sectionName); + SectionList::const_iterator findSection(SectionList::iterator after, std::string_view sectionName) const; + SectionList::iterator sectionEnd(); + SectionList::const_iterator sectionEnd() const; + std::optional findField(std::string_view sectionName, std::string_view key); + std::optional findField(std::string_view sectionName, std::string_view key) const; + void parse(std::istream &inputStream, IniFileParseOptions options = IniFileParseOptions::None); + void make(std::ostream &outputStream, IniFileMakeOptions options = IniFileMakeOptions::None); + + SectionList sections; +}; + +/*! + * \brief Returns an iterator to the first section with the name \a sectionName. + */ +inline AdvancedIniFile::SectionList::iterator AdvancedIniFile::findSection(std::string_view sectionName) +{ + return std::find_if(sections.begin(), sections.end(), [§ionName](const auto &scope) { return scope.name == sectionName; }); +} + +/*! + * \brief Returns an iterator to the first section with the name \a sectionName. + */ +inline AdvancedIniFile::SectionList::const_iterator AdvancedIniFile::findSection(std::string_view sectionName) const +{ + return const_cast(this)->findSection(sectionName); +} + +/*! + * \brief Returns an iterator to the first section with the name \a sectionName which comes after \a after. + */ +inline AdvancedIniFile::SectionList::iterator AdvancedIniFile::findSection(SectionList::iterator after, std::string_view sectionName) +{ + return std::find_if(after + 1, sections.end(), [§ionName](const auto &scope) { return scope.name == sectionName; }); +} + +/*! + * \brief Returns an iterator to the first section with the name \a sectionName which comes after \a after. + */ +inline AdvancedIniFile::SectionList::const_iterator AdvancedIniFile::findSection(SectionList::iterator after, std::string_view sectionName) const +{ + return const_cast(this)->findSection(after, sectionName); +} + +/*! + * \brief Returns an iterator that points one past the last section. + */ +inline AdvancedIniFile::SectionList::iterator AdvancedIniFile::sectionEnd() +{ + return sections.end(); +} + +/*! + * \brief Returns an iterator that points one past the last section. + */ +inline AdvancedIniFile::SectionList::const_iterator AdvancedIniFile::sectionEnd() const +{ + return sections.end(); +} + +/*! + * \brief Returns an iterator to the first field within the first section with matching \a sectionName and \a key. + */ +inline std::optional AdvancedIniFile::findField(std::string_view sectionName, std::string_view key) +{ + const SectionList::iterator scope = findSection(sectionName); + if (scope == sectionEnd()) { + return std::nullopt; + } + const FieldList::iterator field = scope->findField(key); + if (field == scope->fieldEnd()) { + return std::nullopt; + } + return field; +} + +/*! + * \brief Returns an iterator to the first field within the first section with matching \a sectionName and \a key. + */ +inline std::optional AdvancedIniFile::findField(std::string_view sectionName, std::string_view key) const +{ + return const_cast(this)->findField(sectionName, key); +} + +/*! + * \brief Returns an iterator to the first field with the key \a key. + */ +inline AdvancedIniFile::FieldList::iterator AdvancedIniFile::Section::findField(std::string_view key) +{ + return std::find_if(fields.begin(), fields.end(), [&key](const auto &field) { return field.key == key; }); +} + +/*! + * \brief Returns an iterator to the first field with the key \a key. + */ +inline AdvancedIniFile::FieldList::const_iterator AdvancedIniFile::Section::findField(std::string_view key) const +{ + return const_cast
(this)->findField(key); +} + +/*! + * \brief Returns an iterator to the first field with the key \a key which comes after \a after. + */ +inline AdvancedIniFile::FieldList::iterator AdvancedIniFile::Section::findField(FieldList::iterator after, std::string_view key) +{ + return std::find_if(after + 1, fields.end(), [&key](const auto &field) { return field.key == key; }); +} + +/*! + * \brief Returns an iterator to the first field with the key \a key which comes after \a after. + */ +inline AdvancedIniFile::FieldList::const_iterator AdvancedIniFile::Section::findField(FieldList::iterator after, std::string_view key) const +{ + return const_cast
(this)->findField(after, key); +} + +/*! + * \brief Returns an iterator that points one past the last field. + */ +inline AdvancedIniFile::FieldList::iterator AdvancedIniFile::Section::fieldEnd() +{ + return fields.end(); +} + +/*! + * \brief Returns an iterator that points one past the last field. + */ +inline AdvancedIniFile::FieldList::const_iterator AdvancedIniFile::Section::fieldEnd() const +{ + return fields.end(); +} + } // namespace CppUtilities +CPP_UTILITIES_MARK_FLAG_ENUM_CLASS(CppUtilities, IniFileParseOptions); +CPP_UTILITIES_MARK_FLAG_ENUM_CLASS(CppUtilities, IniFileMakeOptions); +CPP_UTILITIES_MARK_FLAG_ENUM_CLASS(CppUtilities, IniFileFieldFlags); +CPP_UTILITIES_MARK_FLAG_ENUM_CLASS(CppUtilities, IniFileSectionFlags); + #endif // IOUTILITIES_INIFILE_H diff --git a/testfiles/pacman.conf b/testfiles/pacman.conf new file mode 100644 index 0000000..f180fb0 --- /dev/null +++ b/testfiles/pacman.conf @@ -0,0 +1,103 @@ +# Based on config file from https://git.archlinux.org/svntogit/packages.git/?h=packages/pacman +# Used here as an example to test the advanced INI (de)serializer +# +# /etc/pacman.conf +# +# See the pacman.conf(5) manpage for option and repository directives + +# +# GENERAL OPTIONS +# +[options] +# The following paths are commented out with their default values listed. +# If you wish to use different paths, uncomment and update the paths. +#RootDir = / +#DBPath = /var/lib/pacman/ +#CacheDir = /var/cache/pacman/pkg/ +#LogFile = /var/log/pacman.log +#GPGDir = /etc/pacman.d/gnupg/ +#HookDir = /etc/pacman.d/hooks/ +HoldPkg = pacman glibc +#XferCommand = /usr/bin/curl -L -C - -f -o %o %u +#XferCommand = /usr/bin/wget --passive-ftp -c -O %o %u +#CleanMethod = KeepInstalled +Foo = bar # inline comment +Architecture = auto + +# Pacman won't upgrade packages listed in IgnorePkg and members of IgnoreGroup +#IgnorePkg = +#IgnoreGroup = + +#NoUpgrade = +#NoExtract = + +# Misc options +#UseSyslog +#Color +#TotalDownload +CheckSpace +Bar # another inline comment +#VerbosePkgLists + +# By default, pacman accepts packages signed by keys that its local keyring +# trusts (see pacman-key and its man page), as well as unsigned packages. +SigLevel = Required DatabaseOptional +LocalFileSigLevel = Optional +#RemoteFileSigLevel = Required + +# NOTE: You must run `pacman-key --init` before first using pacman; the local +# keyring can then be populated with the keys of all official Arch Linux +# packagers with `pacman-key --populate archlinux`. + +# +# REPOSITORIES +# - can be defined here or included from another file +# - pacman will search repositories in the order defined here +# - local/custom mirrors can be added here or in separate files +# - repositories listed first will take precedence when packages +# have identical names, regardless of version number +# - URLs will have $repo replaced by the name of the current repo +# - URLs will have $arch replaced by the name of the architecture +# +# Repository entries are of the format: +# [repo-name] +# Server = ServerName +# Include = IncludePath +# +# The header [repo-name] is crucial - it must be present and +# uncommented to enable the repo. +# + +# The testing repositories are disabled by default. To enable, uncomment the +# repo name header and Include lines. You can add preferred servers immediately +# after the header, and they will be used before the default mirrors. + +#[testing] +#Include = /etc/pacman.d/mirrorlist + +[core] +Include = /etc/pacman.d/mirrorlist + +[extra] # an inline comment after a scope name +Include = /etc/pacman.d/mirrorlist + +#[community-testing] +#Include = /etc/pacman.d/mirrorlist + +[community] +Include = /etc/pacman.d/mirrorlist + +# If you want to run 32 bit applications on your x86_64 system, +# enable the multilib repositories as required here. + +#[multilib-testing] +#Include = /etc/pacman.d/mirrorlist + +#[multilib] +#Include = /etc/pacman.d/mirrorlist + +# An example of a custom package repository. See the pacman manpage for +# tips on creating your own repositories. +#[custom] +#SigLevel = Optional TrustAll +#Server = file:///home/custompkgs diff --git a/tests/iotests.cpp b/tests/iotests.cpp index c1f0d85..11e8764 100644 --- a/tests/iotests.cpp +++ b/tests/iotests.cpp @@ -45,6 +45,7 @@ class IoTests : public TestFixture { CPPUNIT_TEST(testBitReader); CPPUNIT_TEST(testPathUtilities); CPPUNIT_TEST(testIniFile); + CPPUNIT_TEST(testAdvancedIniFile); CPPUNIT_TEST(testCopy); CPPUNIT_TEST(testReadFile); CPPUNIT_TEST(testWriteFile); @@ -63,6 +64,7 @@ public: void testBitReader(); void testPathUtilities(); void testIniFile(); + void testAdvancedIniFile(); void testCopy(); void testReadFile(); void testWriteFile(); @@ -316,6 +318,74 @@ void IoTests::testIniFile() CPPUNIT_ASSERT(ini.data() == ini2.data()); } +/*! + * \brief Tests AdvancedIniFile. + */ +void IoTests::testAdvancedIniFile() +{ + // prepare reading test file + fstream inputFile; + inputFile.exceptions(ios_base::failbit | ios_base::badbit); + inputFile.open(testFilePath("pacman.conf"), ios_base::in); + + // parse the test file + AdvancedIniFile ini; + ini.parse(inputFile); + + // check whether scope data is as expected + CPPUNIT_ASSERT_EQUAL_MESSAGE("5 scopes (taking implicit empty section at the end into account)", 5_st, ini.sections.size()); + auto options = ini.findSection("options"); + CPPUNIT_ASSERT(options != ini.sectionEnd()); + TESTUTILS_ASSERT_LIKE_FLAGS( + "comment block before section", "# Based on.*\n.*# GENERAL OPTIONS\n#\n"s, std::regex::extended, options->precedingCommentBlock); + CPPUNIT_ASSERT_EQUAL(7_st, options->fields.size()); + CPPUNIT_ASSERT_EQUAL("HoldPkg"s, options->fields[0].key); + CPPUNIT_ASSERT_EQUAL("pacman glibc"s, options->fields[0].value); + CPPUNIT_ASSERT_MESSAGE("value present", options->fields[0].flags & IniFileFieldFlags::HasValue); + TESTUTILS_ASSERT_LIKE_FLAGS("comment block between section header and first field", + "# The following paths are.*\n.*#HookDir = /etc/pacman\\.d/hooks/\n"s, std::regex::extended, options->fields[0].precedingCommentBlock); + CPPUNIT_ASSERT_EQUAL(""s, options->fields[0].followingInlineComment); + CPPUNIT_ASSERT_EQUAL("Foo"s, options->fields[1].key); + CPPUNIT_ASSERT_EQUAL("bar"s, options->fields[1].value); + CPPUNIT_ASSERT_MESSAGE("value present", options->fields[1].flags & IniFileFieldFlags::HasValue); + TESTUTILS_ASSERT_LIKE_FLAGS("comment block between fields", "#XferCommand.*\n.*#CleanMethod = KeepInstalled\n"s, std::regex::extended, + options->fields[1].precedingCommentBlock); + CPPUNIT_ASSERT_EQUAL("# inline comment"s, options->fields[1].followingInlineComment); + CPPUNIT_ASSERT_EQUAL("CheckSpace"s, options->fields[3].key); + CPPUNIT_ASSERT_EQUAL(""s, options->fields[3].value); + CPPUNIT_ASSERT_MESSAGE("no value present", !(options->fields[3].flags & IniFileFieldFlags::HasValue)); + TESTUTILS_ASSERT_LIKE_FLAGS("empty lines in comments preserved", "\n# Pacman.*\n.*\n\n#NoUpgrade =\n.*#TotalDownload\n"s, std::regex::extended, + options->fields[3].precedingCommentBlock); + CPPUNIT_ASSERT_EQUAL(""s, options->fields[3].followingInlineComment); + auto extraScope = ini.findSection(options, "extra"); + CPPUNIT_ASSERT(extraScope != ini.sectionEnd()); + CPPUNIT_ASSERT_EQUAL_MESSAGE("comment block which is only an empty line", "\n"s, extraScope->precedingCommentBlock); + CPPUNIT_ASSERT_EQUAL_MESSAGE("inline comment after scope", "# an inline comment after a scope name"s, extraScope->followingInlineComment); + CPPUNIT_ASSERT_EQUAL(1_st, extraScope->fields.size()); + CPPUNIT_ASSERT(ini.sections.back().flags & IniFileSectionFlags::Implicit); + TESTUTILS_ASSERT_LIKE_FLAGS("comment block after last field present in implicitly added last scope", "\n# If you.*\n.*custompkgs\n"s, + std::regex::extended, ini.sections.back().precedingCommentBlock); + + // test finding a field from file level and const access + const auto *const constIniFile = &ini; + auto includeField = constIniFile->findField("extra", "Include"); + CPPUNIT_ASSERT(includeField.has_value()); + CPPUNIT_ASSERT_EQUAL("Include"s, includeField.value()->key); + CPPUNIT_ASSERT_EQUAL("/etc/pacman.d/mirrorlist"s, includeField.value()->value); + CPPUNIT_ASSERT_MESSAGE("field not present", !constIniFile->findField("extra", "Includ").has_value()); + CPPUNIT_ASSERT_MESSAGE("scope not present", !constIniFile->findField("extr", "Includ").has_value()); + + // write values again; there shouldn't be a difference as the parser and the writer are supposed to + // preserve the order of all elements and comments + std::stringstream newFile; + ini.make(newFile); + std::string originalContents; + inputFile.clear(); + inputFile.seekg(std::ios_base::beg); + originalContents.assign((istreambuf_iterator(inputFile)), istreambuf_iterator()); + CPPUNIT_ASSERT_EQUAL(originalContents, newFile.str()); +} + /*! * \brief Tests CopyHelper. */