ini.h (18620B)
1/* 2 * The MIT License (MIT) 3 * Copyright (c) 2018 Danijel Durakovic 4 * 5 * Permission is hereby granted, free of charge, to any person obtaining a copy of 6 * this software and associated documentation files (the "Software"), to deal in 7 * the Software without restriction, including without limitation the rights to 8 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 * of the Software, and to permit persons to whom the Software is furnished to do 10 * so, subject to the following conditions: 11 * 12 * The above copyright notice and this permission notice shall be included in all 13 * copies or substantial portions of the Software. 14 * 15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 * 22 */ 23 24/////////////////////////////////////////////////////////////////////////////// 25// 26// /mINI/ v0.9.10 27// An INI file reader and writer for the modern age. 28// 29/////////////////////////////////////////////////////////////////////////////// 30// 31// A tiny utility library for manipulating INI files with a straightforward 32// API and a minimal footprint. It conforms to the (somewhat) standard INI 33// format - sections and keys are case insensitive and all leading and 34// trailing whitespace is ignored. Comments are lines that begin with a 35// semicolon. Trailing comments are allowed on section lines. 36// 37// Files are read on demand, upon which data is kept in memory and the file 38// is closed. This utility supports lazy writing, which only writes changes 39// and updates to a file and preserves custom formatting and comments. A lazy 40// write invoked by a write() call will read the output file, find what 41// changes have been made and update the file accordingly. If you only need to 42// generate files, use generate() instead. Section and key order is preserved 43// on read, write and insert. 44// 45/////////////////////////////////////////////////////////////////////////////// 46// 47// /* BASIC USAGE EXAMPLE: */ 48// 49// /* read from file */ 50// mINI::INIFile file("myfile.ini"); 51// mINI::INIStructure ini; 52// file.read(ini); 53// 54// /* read value; gets a reference to actual value in the structure. 55// if key or section don't exist, a new empty value will be created */ 56// std::string& value = ini["section"]["key"]; 57// 58// /* read value safely; gets a copy of value in the structure. 59// does not alter the structure */ 60// std::string value = ini.get("section").get("key"); 61// 62// /* set or update values */ 63// ini["section"]["key"] = "value"; 64// 65// /* set multiple values */ 66// ini["section2"].set({ 67// {"key1", "value1"}, 68// {"key2", "value2"} 69// }); 70// 71// /* write updates back to file, preserving comments and formatting */ 72// file.write(ini); 73// 74// /* or generate a file (overwrites the original) */ 75// file.generate(ini); 76// 77/////////////////////////////////////////////////////////////////////////////// 78// 79// Long live the INI file!!! 80// 81/////////////////////////////////////////////////////////////////////////////// 82 83#ifndef MINI_INI_H_ 84#define MINI_INI_H_ 85 86#include <string> 87#include <sstream> 88#include <algorithm> 89#include <utility> 90#include <unordered_map> 91#include <vector> 92#include <memory> 93#include <fstream> 94#include <sys/stat.h> 95#include <cctype> 96 97namespace mINI 98{ 99 namespace INIStringUtil 100 { 101 const char* const whitespaceDelimiters = " \t\n\r\f\v"; 102 inline void trim(std::string& str) 103 { 104 str.erase(str.find_last_not_of(whitespaceDelimiters) + 1); 105 str.erase(0, str.find_first_not_of(whitespaceDelimiters)); 106 } 107#ifndef MINI_CASE_SENSITIVE 108 inline void toLower(std::string& str) 109 { 110 std::transform(str.begin(), str.end(), str.begin(), [](const char c) { 111 return static_cast<const char>(std::tolower(c)); 112 }); 113 } 114#endif 115 inline void replace(std::string& str, std::string const& a, std::string const& b) 116 { 117 if (!a.empty()) 118 { 119 std::size_t pos = 0; 120 while ((pos = str.find(a, pos)) != std::string::npos) 121 { 122 str.replace(pos, a.size(), b); 123 pos += b.size(); 124 } 125 } 126 } 127#ifdef _WIN32 128 const char* const endl = "\r\n"; 129#else 130 const char* const endl = "\n"; 131#endif 132 }; 133 134 template<typename T> 135 class INIMap 136 { 137 private: 138 using T_DataIndexMap = std::unordered_map<std::string, std::size_t>; 139 using T_DataItem = std::pair<std::string, T>; 140 using T_DataContainer = std::vector<T_DataItem>; 141 using T_MultiArgs = typename std::vector<std::pair<std::string, T>>; 142 143 T_DataIndexMap dataIndexMap; 144 T_DataContainer data; 145 146 inline std::size_t setEmpty(std::string& key) 147 { 148 std::size_t index = data.size(); 149 dataIndexMap[key] = index; 150 data.emplace_back(key, T()); 151 return index; 152 } 153 154 public: 155 using const_iterator = typename T_DataContainer::const_iterator; 156 157 INIMap() { } 158 159 INIMap(INIMap const& other) 160 { 161 std::size_t data_size = other.data.size(); 162 for (std::size_t i = 0; i < data_size; ++i) 163 { 164 auto const& key = other.data[i].first; 165 auto const& obj = other.data[i].second; 166 data.emplace_back(key, obj); 167 } 168 dataIndexMap = T_DataIndexMap(other.dataIndexMap); 169 } 170 171 T& operator[](std::string key) 172 { 173 INIStringUtil::trim(key); 174#ifndef MINI_CASE_SENSITIVE 175 INIStringUtil::toLower(key); 176#endif 177 auto it = dataIndexMap.find(key); 178 bool hasIt = (it != dataIndexMap.end()); 179 std::size_t index = (hasIt) ? it->second : setEmpty(key); 180 return data[index].second; 181 } 182 T get(std::string key) const 183 { 184 INIStringUtil::trim(key); 185#ifndef MINI_CASE_SENSITIVE 186 INIStringUtil::toLower(key); 187#endif 188 auto it = dataIndexMap.find(key); 189 if (it == dataIndexMap.end()) 190 { 191 return T(); 192 } 193 return T(data[it->second].second); 194 } 195 bool has(std::string key) const 196 { 197 INIStringUtil::trim(key); 198#ifndef MINI_CASE_SENSITIVE 199 INIStringUtil::toLower(key); 200#endif 201 return (dataIndexMap.count(key) == 1); 202 } 203 void set(std::string key, T obj) 204 { 205 INIStringUtil::trim(key); 206#ifndef MINI_CASE_SENSITIVE 207 INIStringUtil::toLower(key); 208#endif 209 auto it = dataIndexMap.find(key); 210 if (it != dataIndexMap.end()) 211 { 212 data[it->second].second = obj; 213 } 214 else 215 { 216 dataIndexMap[key] = data.size(); 217 data.emplace_back(key, obj); 218 } 219 } 220 void set(T_MultiArgs const& multiArgs) 221 { 222 for (auto const& it : multiArgs) 223 { 224 auto const& key = it.first; 225 auto const& obj = it.second; 226 set(key, obj); 227 } 228 } 229 bool remove(std::string key) 230 { 231 INIStringUtil::trim(key); 232#ifndef MINI_CASE_SENSITIVE 233 INIStringUtil::toLower(key); 234#endif 235 auto it = dataIndexMap.find(key); 236 if (it != dataIndexMap.end()) 237 { 238 std::size_t index = it->second; 239 data.erase(data.begin() + index); 240 dataIndexMap.erase(it); 241 for (auto& it2 : dataIndexMap) 242 { 243 auto& vi = it2.second; 244 if (vi > index) 245 { 246 vi--; 247 } 248 } 249 return true; 250 } 251 return false; 252 } 253 void clear() 254 { 255 data.clear(); 256 dataIndexMap.clear(); 257 } 258 std::size_t size() const 259 { 260 return data.size(); 261 } 262 const_iterator begin() const { return data.begin(); } 263 const_iterator end() const { return data.end(); } 264 }; 265 266 using INIStructure = INIMap<INIMap<std::string>>; 267 268 namespace INIParser 269 { 270 using T_ParseValues = std::pair<std::string, std::string>; 271 272 enum class PDataType : char 273 { 274 PDATA_NONE, 275 PDATA_COMMENT, 276 PDATA_SECTION, 277 PDATA_KEYVALUE, 278 PDATA_UNKNOWN 279 }; 280 281 inline PDataType parseLine(std::string line, T_ParseValues& parseData) 282 { 283 parseData.first.clear(); 284 parseData.second.clear(); 285 INIStringUtil::trim(line); 286 if (line.empty()) 287 { 288 return PDataType::PDATA_NONE; 289 } 290 char firstCharacter = line[0]; 291 if (firstCharacter == ';') 292 { 293 return PDataType::PDATA_COMMENT; 294 } 295 if (firstCharacter == '[') 296 { 297 auto commentAt = line.find_first_of(';'); 298 if (commentAt != std::string::npos) 299 { 300 line = line.substr(0, commentAt); 301 } 302 auto closingBracketAt = line.find_last_of(']'); 303 if (closingBracketAt != std::string::npos) 304 { 305 auto section = line.substr(1, closingBracketAt - 1); 306 INIStringUtil::trim(section); 307 parseData.first = section; 308 return PDataType::PDATA_SECTION; 309 } 310 } 311 auto lineNorm = line; 312 INIStringUtil::replace(lineNorm, "\\=", " "); 313 auto equalsAt = lineNorm.find_first_of('='); 314 if (equalsAt != std::string::npos) 315 { 316 auto key = line.substr(0, equalsAt); 317 INIStringUtil::trim(key); 318 INIStringUtil::replace(key, "\\=", "="); 319 auto value = line.substr(equalsAt + 1); 320 INIStringUtil::trim(value); 321 parseData.first = key; 322 parseData.second = value; 323 return PDataType::PDATA_KEYVALUE; 324 } 325 return PDataType::PDATA_UNKNOWN; 326 } 327 }; 328 329 class INIReader 330 { 331 public: 332 using T_LineData = std::vector<std::string>; 333 using T_LineDataPtr = std::shared_ptr<T_LineData>; 334 335 private: 336 std::ifstream fileReadStream; 337 T_LineDataPtr lineData; 338 339 T_LineData readFile() 340 { 341 std::string fileContents; 342 fileReadStream.seekg(0, std::ios::end); 343 fileContents.resize(fileReadStream.tellg()); 344 fileReadStream.seekg(0, std::ios::beg); 345 std::size_t fileSize = fileContents.size(); 346 fileReadStream.read(&fileContents[0], fileSize); 347 fileReadStream.close(); 348 T_LineData output; 349 if (fileSize == 0) 350 { 351 return output; 352 } 353 std::string buffer; 354 buffer.reserve(50); 355 for (std::size_t i = 0; i < fileSize; ++i) 356 { 357 char& c = fileContents[i]; 358 if (c == '\n') 359 { 360 output.emplace_back(buffer); 361 buffer.clear(); 362 continue; 363 } 364 if (c != '\0' && c != '\r') 365 { 366 buffer += c; 367 } 368 } 369 output.emplace_back(buffer); 370 return output; 371 } 372 373 public: 374 INIReader(std::string const& filename, bool keepLineData = false) 375 { 376 fileReadStream.open(filename, std::ios::in | std::ios::binary); 377 if (keepLineData) 378 { 379 lineData = std::make_shared<T_LineData>(); 380 } 381 } 382 ~INIReader() { } 383 384 bool operator>>(INIStructure& data) 385 { 386 if (!fileReadStream.is_open()) 387 { 388 return false; 389 } 390 T_LineData fileLines = readFile(); 391 std::string section; 392 bool inSection = false; 393 INIParser::T_ParseValues parseData; 394 for (auto const& line : fileLines) 395 { 396 auto parseResult = INIParser::parseLine(line, parseData); 397 if (parseResult == INIParser::PDataType::PDATA_SECTION) 398 { 399 inSection = true; 400 data[section = parseData.first]; 401 } 402 else if (inSection && parseResult == INIParser::PDataType::PDATA_KEYVALUE) 403 { 404 auto const& key = parseData.first; 405 auto const& value = parseData.second; 406 data[section][key] = value; 407 } 408 if (lineData && parseResult != INIParser::PDataType::PDATA_UNKNOWN) 409 { 410 if (parseResult == INIParser::PDataType::PDATA_KEYVALUE && !inSection) 411 { 412 continue; 413 } 414 lineData->emplace_back(line); 415 } 416 } 417 return true; 418 } 419 T_LineDataPtr getLines() 420 { 421 return lineData; 422 } 423 }; 424 425 class INIGenerator 426 { 427 private: 428 std::ofstream fileWriteStream; 429 430 public: 431 bool prettyPrint = false; 432 433 INIGenerator(std::string const& filename) 434 { 435 fileWriteStream.open(filename, std::ios::out | std::ios::binary); 436 } 437 ~INIGenerator() { } 438 439 bool operator<<(INIStructure const& data) 440 { 441 if (!fileWriteStream.is_open()) 442 { 443 return false; 444 } 445 if (!data.size()) 446 { 447 return true; 448 } 449 auto it = data.begin(); 450 for (;;) 451 { 452 auto const& section = it->first; 453 auto const& collection = it->second; 454 fileWriteStream 455 << "[" 456 << section 457 << "]"; 458 if (collection.size()) 459 { 460 fileWriteStream << INIStringUtil::endl; 461 auto it2 = collection.begin(); 462 for (;;) 463 { 464 auto key = it2->first; 465 INIStringUtil::replace(key, "=", "\\="); 466 auto value = it2->second; 467 INIStringUtil::trim(value); 468 fileWriteStream 469 << key 470 << ((prettyPrint) ? " = " : "=") 471 << value; 472 if (++it2 == collection.end()) 473 { 474 break; 475 } 476 fileWriteStream << INIStringUtil::endl; 477 } 478 } 479 if (++it == data.end()) 480 { 481 break; 482 } 483 fileWriteStream << INIStringUtil::endl; 484 if (prettyPrint) 485 { 486 fileWriteStream << INIStringUtil::endl; 487 } 488 } 489 return true; 490 } 491 }; 492 493 class INIWriter 494 { 495 private: 496 using T_LineData = std::vector<std::string>; 497 using T_LineDataPtr = std::shared_ptr<T_LineData>; 498 499 std::string filename; 500 501 T_LineData getLazyOutput(T_LineDataPtr const& lineData, INIStructure& data, INIStructure& original) 502 { 503 T_LineData output; 504 INIParser::T_ParseValues parseData; 505 std::string sectionCurrent; 506 bool parsingSection = false; 507 bool continueToNextSection = false; 508 bool discardNextEmpty = false; 509 bool writeNewKeys = false; 510 std::size_t lastKeyLine = 0; 511 for (auto line = lineData->begin(); line != lineData->end(); ++line) 512 { 513 if (!writeNewKeys) 514 { 515 auto parseResult = INIParser::parseLine(*line, parseData); 516 if (parseResult == INIParser::PDataType::PDATA_SECTION) 517 { 518 if (parsingSection) 519 { 520 writeNewKeys = true; 521 parsingSection = false; 522 --line; 523 continue; 524 } 525 sectionCurrent = parseData.first; 526 if (data.has(sectionCurrent)) 527 { 528 parsingSection = true; 529 continueToNextSection = false; 530 discardNextEmpty = false; 531 output.emplace_back(*line); 532 lastKeyLine = output.size(); 533 } 534 else 535 { 536 continueToNextSection = true; 537 discardNextEmpty = true; 538 continue; 539 } 540 } 541 else if (parseResult == INIParser::PDataType::PDATA_KEYVALUE) 542 { 543 if (continueToNextSection) 544 { 545 continue; 546 } 547 if (data.has(sectionCurrent)) 548 { 549 auto& collection = data[sectionCurrent]; 550 auto const& key = parseData.first; 551 auto const& value = parseData.second; 552 if (collection.has(key)) 553 { 554 auto outputValue = collection[key]; 555 if (value == outputValue) 556 { 557 output.emplace_back(*line); 558 } 559 else 560 { 561 INIStringUtil::trim(outputValue); 562 auto lineNorm = *line; 563 INIStringUtil::replace(lineNorm, "\\=", " "); 564 auto equalsAt = lineNorm.find_first_of('='); 565 auto valueAt = lineNorm.find_first_not_of( 566 INIStringUtil::whitespaceDelimiters, 567 equalsAt + 1 568 ); 569 std::string outputLine = line->substr(0, valueAt); 570 if (prettyPrint && equalsAt + 1 == valueAt) 571 { 572 outputLine += " "; 573 } 574 outputLine += outputValue; 575 output.emplace_back(outputLine); 576 } 577 lastKeyLine = output.size(); 578 } 579 } 580 } 581 else 582 { 583 if (discardNextEmpty && line->empty()) 584 { 585 discardNextEmpty = false; 586 } 587 else if (parseResult != INIParser::PDataType::PDATA_UNKNOWN) 588 { 589 output.emplace_back(*line); 590 } 591 } 592 } 593 if (writeNewKeys || std::next(line) == lineData->end()) 594 { 595 T_LineData linesToAdd; 596 if (data.has(sectionCurrent) && original.has(sectionCurrent)) 597 { 598 auto const& collection = data[sectionCurrent]; 599 auto const& collectionOriginal = original[sectionCurrent]; 600 for (auto const& it : collection) 601 { 602 auto key = it.first; 603 if (collectionOriginal.has(key)) 604 { 605 continue; 606 } 607 auto value = it.second; 608 INIStringUtil::replace(key, "=", "\\="); 609 INIStringUtil::trim(value); 610 linesToAdd.emplace_back( 611 key + ((prettyPrint) ? " = " : "=") + value 612 ); 613 } 614 } 615 if (!linesToAdd.empty()) 616 { 617 output.insert( 618 output.begin() + lastKeyLine, 619 linesToAdd.begin(), 620 linesToAdd.end() 621 ); 622 } 623 if (writeNewKeys) 624 { 625 writeNewKeys = false; 626 --line; 627 } 628 } 629 } 630 for (auto const& it : data) 631 { 632 auto const& section = it.first; 633 if (original.has(section)) 634 { 635 continue; 636 } 637 if (prettyPrint && output.size() > 0 && !output.back().empty()) 638 { 639 output.emplace_back(); 640 } 641 output.emplace_back("[" + section + "]"); 642 auto const& collection = it.second; 643 for (auto const& it2 : collection) 644 { 645 auto key = it2.first; 646 auto value = it2.second; 647 INIStringUtil::replace(key, "=", "\\="); 648 INIStringUtil::trim(value); 649 output.emplace_back( 650 key + ((prettyPrint) ? " = " : "=") + value 651 ); 652 } 653 } 654 return output; 655 } 656 657 public: 658 bool prettyPrint = false; 659 660 INIWriter(std::string const& filename) 661 : filename(filename) 662 { 663 } 664 ~INIWriter() { } 665 666 bool operator<<(INIStructure& data) 667 { 668 struct stat buf; 669 bool fileExists = (stat(filename.c_str(), &buf) == 0); 670 if (!fileExists) 671 { 672 INIGenerator generator(filename); 673 generator.prettyPrint = prettyPrint; 674 return generator << data; 675 } 676 INIStructure originalData; 677 T_LineDataPtr lineData; 678 bool readSuccess = false; 679 { 680 INIReader reader(filename, true); 681 if ((readSuccess = reader >> originalData)) 682 { 683 lineData = reader.getLines(); 684 } 685 } 686 if (!readSuccess) 687 { 688 return false; 689 } 690 T_LineData output = getLazyOutput(lineData, data, originalData); 691 std::ofstream fileWriteStream(filename, std::ios::out | std::ios::binary); 692 if (fileWriteStream.is_open()) 693 { 694 if (output.size()) 695 { 696 auto line = output.begin(); 697 for (;;) 698 { 699 fileWriteStream << *line; 700 if (++line == output.end()) 701 { 702 break; 703 } 704 fileWriteStream << INIStringUtil::endl; 705 } 706 } 707 return true; 708 } 709 return false; 710 } 711 }; 712 713 class INIFile 714 { 715 private: 716 std::string filename; 717 718 public: 719 INIFile(std::string const& filename) 720 : filename(filename) 721 { } 722 723 ~INIFile() { } 724 725 bool read(INIStructure& data) const 726 { 727 if (data.size()) 728 { 729 data.clear(); 730 } 731 if (filename.empty()) 732 { 733 return false; 734 } 735 INIReader reader(filename); 736 return reader >> data; 737 } 738 bool generate(INIStructure const& data, bool pretty = false) const 739 { 740 if (filename.empty()) 741 { 742 return false; 743 } 744 INIGenerator generator(filename); 745 generator.prettyPrint = pretty; 746 return generator << data; 747 } 748 bool write(INIStructure& data, bool pretty = false) const 749 { 750 if (filename.empty()) 751 { 752 return false; 753 } 754 INIWriter writer(filename); 755 writer.prettyPrint = pretty; 756 return writer << data; 757 } 758 }; 759} 760 761#endif // MINI_INI_H_