Add AdvancedIniFile for better preserving certain aspects like comments

This commit is contained in:
Martchus 2020-04-28 16:25:00 +02:00
parent fe5dc5e6a7
commit 43550b6d55
4 changed files with 694 additions and 56 deletions

View File

@ -1,74 +1,78 @@
#include "./inifile.h"
#include "../conversion/stringconversion.h"
#include <iostream>
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, &sectionName, &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 &section : 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 &section : 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

View File

@ -2,9 +2,12 @@
#define IOUTILITIES_INIFILE_H
#include "../global.h"
#include "../misc/flagenumclass.h"
#include <algorithm>
#include <iosfwd>
#include <map>
#include <optional>
#include <string>
#include <vector>
@ -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<Field>;
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<Section>;
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<FieldList::iterator> findField(std::string_view sectionName, std::string_view key);
std::optional<FieldList::const_iterator> 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(), [&sectionName](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<AdvancedIniFile *>(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(), [&sectionName](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<AdvancedIniFile *>(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::FieldList::iterator> 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::FieldList::const_iterator> AdvancedIniFile::findField(std::string_view sectionName, std::string_view key) const
{
return const_cast<AdvancedIniFile *>(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<Section *>(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<Section *>(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

103
testfiles/pacman.conf Normal file
View File

@ -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

View File

@ -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<char>(inputFile)), istreambuf_iterator<char>());
CPPUNIT_ASSERT_EQUAL(originalContents, newFile.str());
}
/*!
* \brief Tests CopyHelper.
*/