1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
|
#include "parser.hpp"
#include <sstream>
#include <components/debug/debuglog.hpp>
#include <components/files/conversion.hpp>
#include <components/misc/strings/algorithm.hpp>
#include <filesystem>
#include <fstream>
#include <Base64.h>
void Settings::SettingsFileParser::loadSettingsFile(
const std::filesystem::path& file, CategorySettingValueMap& settings, bool base64Encoded, bool overrideExisting)
{
mFile = file;
std::ifstream fstream;
fstream.open(file);
auto stream = std::ref<std::istream>(fstream);
std::istringstream decodedStream;
if (base64Encoded)
{
std::string base64String(std::istreambuf_iterator<char>(fstream), {});
std::string decodedString;
auto result = Base64::Base64::Decode(base64String, decodedString);
if (!result.empty())
fail("Could not decode Base64 file: " + result);
// Move won't do anything until C++20, but won't hurt to do it anyway.
decodedStream.str(std::move(decodedString));
stream = std::ref<std::istream>(decodedStream);
}
Log(Debug::Info) << "Loading settings file: " << file;
std::string currentCategory;
mLine = 0;
while (!stream.get().eof() && !stream.get().fail())
{
++mLine;
std::string line;
std::getline(stream.get(), line);
size_t i = 0;
if (!skipWhiteSpace(i, line))
continue;
if (line[i] == '#') // skip comment
continue;
if (line[i] == '[')
{
size_t end = line.find(']', i);
if (end == std::string::npos)
fail("unterminated category");
currentCategory = line.substr(i + 1, end - (i + 1));
Misc::StringUtils::trim(currentCategory);
i = end + 1;
}
if (!skipWhiteSpace(i, line))
continue;
if (currentCategory.empty())
fail("empty category name");
size_t settingEnd = line.find('=', i);
if (settingEnd == std::string::npos)
fail("unterminated setting name");
std::string setting = line.substr(i, (settingEnd - i));
Misc::StringUtils::trim(setting);
size_t valueBegin = settingEnd + 1;
std::string value = line.substr(valueBegin);
Misc::StringUtils::trim(value);
if (overrideExisting)
settings[std::make_pair(currentCategory, setting)] = std::move(value);
else if (settings.insert(std::make_pair(std::make_pair(currentCategory, setting), value)).second == false)
fail(std::string("duplicate setting: [" + currentCategory + "] " + setting));
}
}
void Settings::SettingsFileParser::saveSettingsFile(
const std::filesystem::path& file, const CategorySettingValueMap& settings)
{
using CategorySettingStatusMap = std::map<CategorySetting, bool>;
// No options have been written to the file yet.
CategorySettingStatusMap written;
for (auto it = settings.begin(); it != settings.end(); ++it)
{
written[it->first] = false;
}
// Have we substantively changed the settings file?
bool changed = false;
// Were there any lines at all in the file?
bool existing = false;
// Is an entirely blank line queued to be added?
bool emptyLineQueued = false;
// The category/section we're currently in.
std::string currentCategory;
// Open the existing settings.cfg file to copy comments. This might not be the same file
// as the output file if we're copying the setting from the default settings.cfg for the
// first time. A minor change in API to pass the source file might be in order here.
std::ifstream istream;
istream.open(file);
// Create a new string stream to write the current settings to. It's likely that the
// input file and the output file are the same, so this stream serves as a temporary file
// of sorts. The setting files aren't very large so there's no performance issue.
std::stringstream ostream;
// For every line in the input file...
while (!istream.eof() && !istream.fail())
{
std::string line;
std::getline(istream, line);
// The current character position in the line.
size_t i = 0;
// An empty line was queued.
if (emptyLineQueued)
{
emptyLineQueued = false;
// We're still going through the current category, so we should copy it.
if (currentCategory.empty() || istream.eof() || line[i] != '[')
ostream << std::endl;
}
// Don't add additional newlines at the end of the file otherwise.
if (istream.eof())
continue;
// Queue entirely blank lines to add them if desired.
if (!skipWhiteSpace(i, line))
{
emptyLineQueued = true;
continue;
}
// There were at least some comments in the input file.
existing = true;
// Copy comments.
if (line[i] == '#')
{
ostream << line << std::endl;
continue;
}
// Category heading.
if (line[i] == '[')
{
size_t end = line.find(']', i);
// This should never happen unless the player edited the file while playing.
if (end == std::string::npos)
{
ostream << "# unterminated category: " << line << std::endl;
changed = true;
continue;
}
if (!currentCategory.empty())
{
// Ensure that all options in the current category have been written.
for (CategorySettingStatusMap::iterator mit = written.begin(); mit != written.end(); ++mit)
{
if (mit->second == false && mit->first.first == currentCategory)
{
Log(Debug::Verbose) << "Added new setting: [" << currentCategory << "] " << mit->first.second
<< " = " << settings.at(mit->first);
ostream << mit->first.second << " = " << settings.at(mit->first) << std::endl;
mit->second = true;
changed = true;
}
}
// Add an empty line after the last option in a category.
ostream << std::endl;
}
// Update the current category.
currentCategory = line.substr(i + 1, end - (i + 1));
Misc::StringUtils::trim(currentCategory);
// Write the (new) current category to the file.
ostream << "[" << currentCategory << "]" << std::endl;
// Log(Debug::Verbose) << "Wrote category: " << currentCategory;
// A setting can apparently follow the category on an input line. That's rather
// inconvenient, since it makes it more likely to have duplicative sections,
// which our algorithm doesn't like. Do the best we can.
i = end + 1;
}
// Truncate trailing whitespace, since we're changing the file anayway.
if (!skipWhiteSpace(i, line))
continue;
// If we've found settings before the first category, something's wrong. This
// should never happen unless the player edited the file while playing, since
// the loadSettingsFile() logic rejects it.
if (currentCategory.empty())
{
ostream << "# empty category name: " << line << std::endl;
changed = true;
continue;
}
// Which setting was at this location in the input file?
size_t settingEnd = line.find('=', i);
// This should never happen unless the player edited the file while playing.
if (settingEnd == std::string::npos)
{
ostream << "# unterminated setting name: " << line << std::endl;
changed = true;
continue;
}
std::string setting = line.substr(i, (settingEnd - i));
Misc::StringUtils::trim(setting);
// Get the existing value so we can see if we've changed it.
size_t valueBegin = settingEnd + 1;
std::string value = line.substr(valueBegin);
Misc::StringUtils::trim(value);
// Construct the setting map key to determine whether the setting has already been
// written to the file.
CategorySetting key = std::make_pair(currentCategory, setting);
CategorySettingStatusMap::iterator finder = written.find(key);
// Settings not in the written map are definitely invalid. Currently, this can only
// happen if the player edited the file while playing, because loadSettingsFile()
// will accept anything and pass it along in the map, but in the future, we might
// want to handle invalid settings more gracefully here.
if (finder == written.end())
{
ostream << "# invalid setting: " << line << std::endl;
changed = true;
continue;
}
// Write the current value of the setting to the file. The key must exist in the
// settings map because of how written was initialized and finder != end().
ostream << setting << " = " << settings.at(key) << std::endl;
// Mark that setting as written.
finder->second = true;
// Did we really change it?
if (value != settings.at(key))
{
Log(Debug::Verbose) << "Changed setting: [" << currentCategory << "] " << setting << " = "
<< settings.at(key);
changed = true;
}
// No need to write the current line, because we just emitted a replacement.
// Curiously, it appears that comments at the ends of lines with settings are not
// allowed, and the comment becomes part of the value. Was that intended?
}
// We're done with the input stream file.
istream.close();
// Ensure that all options in the current category have been written. We must complete
// the current category at the end of the file before moving on to any new categories.
for (CategorySettingStatusMap::iterator mit = written.begin(); mit != written.end(); ++mit)
{
if (mit->second == false && mit->first.first == currentCategory)
{
Log(Debug::Verbose) << "Added new setting: [" << mit->first.first << "] " << mit->first.second << " = "
<< settings.at(mit->first);
ostream << mit->first.second << " = " << settings.at(mit->first) << std::endl;
mit->second = true;
changed = true;
}
}
// If there was absolutely nothing in the file (or more likely the file didn't
// exist), start the newly created file with a helpful comment.
if (!existing)
{
const std::string filename = Files::pathToUnicodeString(file.filename());
ostream << "# This is the OpenMW user '" << filename << "' file. This file only contains" << std::endl;
ostream << "# explicitly changed settings. If you would like to revert a setting" << std::endl;
ostream << "# to its default, simply remove it from this file." << std::endl;
if (filename == "settings.cfg")
{
ostream << "# For available settings, see the file 'files/settings-default.cfg' in our source repo or the "
"documentation at:"
<< std::endl;
ostream << "#" << std::endl;
ostream << "# https://openmw.readthedocs.io/en/master/reference/modding/settings/index.html" << std::endl;
}
else if (filename == "openmw-cs.cfg")
{
ostream << "# For available settings, see the file 'apps/opencs/model/prefs/values.hpp' in our source repo."
<< std::endl;
}
}
// We still have one more thing to do before we're completely done writing the file.
// It's possible that there are entirely new categories, or that the input file had
// disappeared completely, so we need ensure that all settings are written to the file
// regardless of those issues.
for (CategorySettingStatusMap::iterator mit = written.begin(); mit != written.end(); ++mit)
{
// If the setting hasn't been written yet.
if (mit->second == false)
{
// If the catgory has changed, write a new category header.
if (mit->first.first != currentCategory)
{
currentCategory = mit->first.first;
Log(Debug::Verbose) << "Created new setting section: " << mit->first.first;
ostream << std::endl;
ostream << "[" << currentCategory << "]" << std::endl;
}
Log(Debug::Verbose) << "Added new setting: [" << mit->first.first << "] " << mit->first.second << " = "
<< settings.at(mit->first);
// Then write the setting. No need to mark it as written because we're done.
ostream << mit->first.second << " = " << settings.at(mit->first) << std::endl;
changed = true;
}
}
// Now install the newly written file in the requested place.
if (changed)
{
Log(Debug::Info) << "Updating settings file: " << file;
std::ofstream ofstream;
ofstream.open(file);
ofstream << ostream.rdbuf();
ofstream.close();
}
}
bool Settings::SettingsFileParser::skipWhiteSpace(size_t& i, std::string& str)
{
while (i < str.size() && std::isspace(str[i], std::locale::classic()))
{
++i;
}
return i < str.size();
}
[[noreturn]] void Settings::SettingsFileParser::fail(const std::string& message)
{
std::stringstream error;
error << "Error on line " << mLine << " in " << Files::pathToUnicodeString(mFile) << ":\n" << message;
throw std::runtime_error(error.str());
}
|