src/lib/INIReader.cpp
branchv_0
changeset 28 0e7c57d48d1e
parent 26 80e129ec3408
child 29 06aaad12c207
equal deleted inserted replaced
27:fd669e73d39a 28:0e7c57d48d1e
    30 class INIReaderImpl : public INIReader {
    30 class INIReaderImpl : public INIReader {
    31 private:
    31 private:
    32 	std::istream& input;
    32 	std::istream& input;
    33 	std::vector<INIContentHandler*> handlers;
    33 	std::vector<INIContentHandler*> handlers;
    34 
    34 
       
    35 	class ConfiguredUnescapingProcessor {
       
    36 	public:
       
    37 		std::shared_ptr<UnescapingProcessor> processor;
       
    38 		const std::string uri;
       
    39 		bool enbaled;
       
    40 
       
    41 		ConfiguredUnescapingProcessor(std::shared_ptr<UnescapingProcessor> processor, const std::string uri, bool enbaled) : processor(processor), uri(uri), enbaled(enbaled) {
       
    42 		}
       
    43 
       
    44 	};
       
    45 
       
    46 	std::vector<ConfiguredUnescapingProcessor> unescapingProcessors;
       
    47 
    35 	/** 
    48 	/** 
    36 	 * This might be configurable.
       
    37 	 * 
       
    38 	 * By default, we ignore all leading whitespace on continuing lines.
    49 	 * By default, we ignore all leading whitespace on continuing lines.
    39 	 * If there should be some spaces or tabs, they should be placed on the previous line before the „\“.
    50 	 * If there should be some spaces or tabs, they should be placed on the previous line before the „\“.
    40 	 * If a line break is desired, it should be written as \n (escaped) or the value should be quoted in " or '.
    51 	 * If a line break is desired, it should be written as \n (escaped) or the value should be quoted in " or '.
    41 	 * 
    52 	 * 
       
    53 	 * TODO: several options:
       
    54 	 *  - enabled, disabled
       
    55 	 *  - if disabled, then: keep backslash, trim backslash, escape backslash
       
    56 	 *    (keep requires support in some further unescaping phase, or it will cause an error)
       
    57 	 *  - keep or trim the line end
       
    58 	 *  - keep or trim the leading spaces
       
    59 	 *  - allow comments interleaved with continuing lines (the freaky systemd syntax)
       
    60 	 * 
    42 	 * Related specifications:
    61 	 * Related specifications:
    43 	 *  - https://docs.oracle.com/javase/8/docs/api/index.html?java/util/Properties.html
    62 	 *  - https://docs.oracle.com/javase/8/docs/api/index.html?java/util/Properties.html
    44 	 */
    63 	 *  - https://www.freedesktop.org/software/systemd/man/systemd.syntax.html
    45 	bool consumeLeadingSpacesOnContinuingLines = true;
    64 	 */
    46 
    65 	bool trimLeadingSpacesOnContinuingLines = true;
    47 	/**
    66 
    48 	 * This might be configurable.
    67 
    49 	 * 
    68 	/**
       
    69 	 * Some dialects or configuration files in general does not support sections.
       
    70 	 * Then a line, that looks like an INI section, should be interpreted as a key
       
    71 	 * (or error, if does not have a proper key-value separator).
       
    72 	 */
       
    73 	bool allowSections = true;
       
    74 
       
    75 	/**
    50 	 * KDE uses some weird INI dialect that allows [section][x] syntax where „x“ is kind of „tag“ that signalizes some properties of given section.
    76 	 * KDE uses some weird INI dialect that allows [section][x] syntax where „x“ is kind of „tag“ that signalizes some properties of given section.
    51 	 * Line „[section_1][$i]“ means that the „section_1“ is „locked“.
    77 	 * Line „[section_1][$i]“ means that the „section_1“ is „locked“.
    52 	 * We may emit this information somehow later, but for now, it is just ignored.
    78 	 * We may emit this information somehow later, but for now, it is just ignored.
    53 	 * 
    79 	 * 
    54 	 * TODO: Is „section tag“ right name?
    80 	 * TODO: Is „section tag“ right name?
    57 	 *  - https://userbase.kde.org/KDE_System_Administration/Configuration_Files#Lock_Down
    83 	 *  - https://userbase.kde.org/KDE_System_Administration/Configuration_Files#Lock_Down
    58 	 */
    84 	 */
    59 	bool allowSectionTags = true;
    85 	bool allowSectionTags = true;
    60 
    86 
    61 	/**
    87 	/**
    62 	 * This might be configurable.
       
    63 	 * 
       
    64 	 * If whole key is „aaa[bbb]“ then „aaa“ is considered to be the key and „bbb“ the sub-key.
    88 	 * If whole key is „aaa[bbb]“ then „aaa“ is considered to be the key and „bbb“ the sub-key.
    65 	 * No \[ escaping is currently supported, so the key might not contain the bracket character.
    89 	 * No \[ escaping is currently supported, so the key might not contain the bracket character.
    66 	 * 
    90 	 * 
    67 	 * Related specifications:
    91 	 * Related specifications:
    68 	 *  - https://userbase.kde.org/KDE_System_Administration/Configuration_Files#Shell_Expansion
    92 	 *  - https://userbase.kde.org/KDE_System_Administration/Configuration_Files#Shell_Expansion
    69 	 *  - https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s05.html
    93 	 *  - https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s05.html
    70 	 */
    94 	 */
    71 	bool allowSubKeys = true;
    95 	bool allowSubKeys = true;
       
    96 
       
    97 	/**
       
    98 	 * Classic INI uses „key=value“ syntax.
       
    99 	 * But some other formats/dialects might use key:value.
       
   100 	 * 
       
   101 	 * Only single character separators are supported.
       
   102 	 * If multiple separators should be recognized (e.g. both „=“ and „:“), this string will contain all of them,
       
   103 	 * i.e. „:=“ does not mean that the „key:=value“ syntax, but „key=value“ or „key:value“.
       
   104 	 */
       
   105 	std::string keyValueSeparators = "=";
       
   106 
       
   107 	/**
       
   108 	 * Classic INI uses „; comment“ syntax.
       
   109 	 * But many existing files contain „# comment“ lines.
       
   110 	 * 
       
   111 	 * Only single character separators are supported (works same as keyValueSeparators).
       
   112 	 */
       
   113 	std::string commentSeparators = ";#";
       
   114 
       
   115 	/**
       
   116 	 * INI often support both "quotes" and 'apostrophes' styles.
       
   117 	 * But some dialects may support only one of them or not support quoting at all.
       
   118 	 * 
       
   119 	 * In such case e.g. „key="some value"“ would mean that the value is „"value"“ (including the quotes).
       
   120 	 * Thus it is important to allow disabling quote recognizing (which is done by setting this parameter to empty string).
       
   121 	 * 
       
   122 	 * Only single character quotes are supported (works same as keyValueSeparators).
       
   123 	 */
       
   124 	std::string quotes = "\"'";
    72 
   125 
    73 	int lineNumber = 1;
   126 	int lineNumber = 1;
    74 	int eventNumber = 0;
   127 	int eventNumber = 0;
    75 
   128 
    76 	/**
   129 	/**
   109 		for (char ch = peek(); input.good() && (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'); ch = peek()) result.put(get());
   162 		for (char ch = peek(); input.good() && (ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'); ch = peek()) result.put(get());
   110 		return result.str();
   163 		return result.str();
   111 	}
   164 	}
   112 
   165 
   113 	void processContinuingLine(std::stringstream& result) {
   166 	void processContinuingLine(std::stringstream& result) {
   114 		if (consumeLeadingSpacesOnContinuingLines) readSpacesAndTabs();
   167 		if (trimLeadingSpacesOnContinuingLines) readSpacesAndTabs();
   115 		else result.put('\n');
   168 		else result.put('\n');
   116 	}
   169 	}
   117 
   170 
   118 	std::string readUntil(char until, bool* found = nullptr) {
   171 	std::string readUntil(const char until, bool* found = nullptr) {
       
   172 		return readUntil(std::string(1, until), found);
       
   173 	}
       
   174 
       
   175 	std::string readUntil(const std::string& until, bool* found = nullptr) {
   119 		std::stringstream result;
   176 		std::stringstream result;
   120 
   177 
   121 		for (char ch = peek(); input.good() && ch != until; ch = peek()) {
   178 		for (char ch = peek(); input.good() && !oneOf(ch, until); ch = peek()) {
   122 			if (ch == '\\') {
   179 			if (ch == '\\') {
   123 				get();
   180 				get();
   124 				ch = get();
   181 				ch = get();
   125 				if (ch == until && ch == '\n') processContinuingLine(result);
   182 				if (oneOf(ch, until) && ch == '\n') processContinuingLine(result);
   126 				else if (ch == until) result.put(ch);
   183 				else if (oneOf(ch, until)) result.put(ch);
   127 				else if (ch == std::istream::traits_type::eof()) break;
   184 				else if (ch == std::istream::traits_type::eof()) break;
   128 				else result.put('\\').put(ch);
   185 				else result.put('\\').put(ch);
   129 				// TODO: two-stage and modular unescaping: here unescape only \+LF or more genereally: unescape only the until character and rest leave untouched
   186 				// unescaping is done in two phases:
   130 				// second escaping stage move to separate class/wrapper (similar to hierarchical wrappers)
   187 				// here we unescape just the \n (LF)
       
   188 				// other escape sequences are leaved untouched and will be processed in later phases, see see UnescapingINIHandler
   131 			} else {
   189 			} else {
   132 				ch = get();
   190 				ch = get();
   133 				result.put(ch);
   191 				result.put(ch);
   134 			}
   192 			}
   135 		}
   193 		}
   136 
   194 
   137 		if (peek() == until) {
   195 		if (oneOf(peek(), until)) {
   138 			get();
   196 			get();
   139 			if (found) *found = true;
   197 			if (found) *found = true;
   140 		} else {
   198 		} else {
   141 			if (found) *found = false;
   199 			if (found) *found = false;
   142 		}
   200 		}
   143 
   201 
   144 		return result.str();
   202 		return result.str();
   145 	}
   203 	}
   146 
   204 
   147 	std::string readToken(char until, char* quote = nullptr, bool* found = nullptr) {
   205 	std::string readToken(const char until, char* quote = nullptr, bool* found = nullptr) {
       
   206 		return readToken(std::string(1, until), quote, found);
       
   207 	}
       
   208 
       
   209 	std::string readToken(const std::string& until, char* quote = nullptr, bool* found = nullptr) {
   148 		std::string result;
   210 		std::string result;
   149 
   211 
   150 		char ch = peek();
   212 		char ch = peek();
   151 		if (isQuote(ch)) {
   213 		if (isQuote(ch)) {
   152 			if (quote) *quote = ch;
   214 			if (quote) *quote = ch;
   153 			result = readUntil(get(), found);
   215 			result = readUntil(std::string(1, get()), found);
   154 		} else {
   216 		} else {
   155 			if (quote) *quote = 0;
   217 			if (quote) *quote = 0;
   156 			result = readUntil(until, found);
   218 			result = readUntil(until, found);
   157 		}
   219 		}
   158 
   220 
   159 		return result;
   221 		return result;
   160 	}
   222 	}
   161 
   223 
   162 	std::string readTokenAndEatTerminator(char until, char* quote = nullptr, bool* found = nullptr) {
   224 	std::string readTokenAndEatTerminator(char until, char* quote = nullptr, bool* found = nullptr) {
       
   225 		return readTokenAndEatTerminator(std::string(1, until), quote, found);
       
   226 	}
       
   227 
       
   228 	std::string readTokenAndEatTerminator(const std::string& until, char* quote = nullptr, bool* found = nullptr) {
   163 		std::string result = readToken(until, quote, found);
   229 		std::string result = readToken(until, quote, found);
   164 		if (*quote) {
   230 		if (*quote) {
   165 			readAllWhitespace();
   231 			readAllWhitespace();
   166 			if (get() != until) throw std::logic_error(std::string("missing „") + std::string(1, until) + "“ after quoted section name");
   232 			if (!oneOf(get(), until)) throw std::logic_error(std::string("missing „") + until + "“ after quoted section name");
   167 		}
   233 		}
   168 		return result;
   234 		return result;
   169 	}
   235 	}
   170 
   236 
       
   237 	std::string unescape(const std::string& value, UnescapingProcessor::TextType type) {
       
   238 		std::string result = value;
       
   239 		for (ConfiguredUnescapingProcessor p : unescapingProcessors) if (p.enbaled) result = p.processor->unescape(result, type);
       
   240 		return result;
       
   241 	}
       
   242 
   171 	bool isComment(char ch) {
   243 	bool isComment(char ch) {
   172 		return ch == '#' || ch == ';';
   244 		return oneOf(ch, commentSeparators);
   173 	}
   245 	}
   174 
   246 
   175 	bool isQuote(char ch) {
   247 	bool isQuote(char ch) {
   176 		return ch == '"' || ch == '\'';
   248 		return oneOf(ch, quotes);
       
   249 	}
       
   250 
       
   251 	/**
       
   252 	 * @param ch character to be evaluated
       
   253 	 * @param options list of options (characters)
       
   254 	 * @return whether ch is one of options
       
   255 	 */
       
   256 	bool oneOf(char ch, const std::string& options) {
       
   257 		return options.find(ch) != std::string::npos;
   177 	}
   258 	}
   178 
   259 
   179 	std::string trim(std::string s) {
   260 	std::string trim(std::string s) {
   180 		return std::regex_replace(s, std::regex("^\\s+|\\s+$"), "");
   261 		return std::regex_replace(s, std::regex("^\\s+|\\s+$"), "");
   181 	}
   262 	}
   182 
   263 
       
   264 	/**
       
   265 	 * TODO: use a common method
       
   266 	 */
       
   267 	bool parseBoolean(const std::string& value) {
       
   268 		if (value == "true") return true;
       
   269 		else if (value == "false") return false;
       
   270 		else throw std::invalid_argument(std::string("Unable to parse boolean value: ") + value + " (expecting true or false)");
       
   271 	}
       
   272 
       
   273 	void setDialect(const std::string& name) {
       
   274 		if (name == "default-ini") {
       
   275 			// already set
       
   276 		} else if (name == "java-properties") {
       
   277 			trimLeadingSpacesOnContinuingLines = true;
       
   278 			allowSections = false;
       
   279 			allowSectionTags = false;
       
   280 			allowSubKeys = false;
       
   281 			commentSeparators = "#";
       
   282 			keyValueSeparators = "=:";
       
   283 			quotes = "";
       
   284 			// TODO: enable unicode unescaping
       
   285 		} else {
       
   286 			throw std::invalid_argument(std::string("Unsupported INI dialect: ") + name);
       
   287 		}
       
   288 	}
       
   289 
       
   290 	bool setUnescaping(const std::string& uri, const std::string& value) {
       
   291 		for (ConfiguredUnescapingProcessor& p : unescapingProcessors) {
       
   292 			if (p.uri == uri) {
       
   293 				p.enbaled = parseBoolean(value);
       
   294 				return true;
       
   295 			}
       
   296 		}
       
   297 		return false;
       
   298 	}
       
   299 
   183 public:
   300 public:
   184 
   301 
   185 	INIReaderImpl(std::istream& input) : input(input) {
   302 	INIReaderImpl(std::istream& input) : input(input) {
       
   303 	}
       
   304 
       
   305 	void setOption(const std::string& uri, const std::string& value) override {
       
   306 		if (uri == "trim-continuing-lines") trimLeadingSpacesOnContinuingLines = parseBoolean(value); // TODO: continuing lines modes (enum), not just boolean
       
   307 		else if (uri == "allow-sections") allowSections = parseBoolean(value);
       
   308 		else if (uri == "allow-section-tags") allowSectionTags = parseBoolean(value);
       
   309 		else if (uri == "allow-sub-keys") allowSubKeys = parseBoolean(value);
       
   310 		else if (uri == "comment-separators") commentSeparators = value;
       
   311 		else if (uri == "key-value-separators") keyValueSeparators = value;
       
   312 		else if (uri == "quotes") quotes = value;
       
   313 		else if (uri == "dialect") setDialect(value);
       
   314 		else if (setUnescaping(uri, value));
       
   315 		else throw std::invalid_argument(std::string("Invalid parser option: „") + uri + "“ with value: „" + value + "“");
   186 	}
   316 	}
   187 
   317 
   188 	void addHandler(INIContentHandler* handler) override {
   318 	void addHandler(INIContentHandler* handler) override {
   189 		handlers.push_back(handler);
   319 		handlers.push_back(handler);
   190 	}
   320 	}
   191 
   321 
       
   322 	void addUnescapingProcessor(std::shared_ptr<UnescapingProcessor> processor, const std::string uri, bool enabledByDefault) override {
       
   323 		unescapingProcessors.push_back({processor, uri, enabledByDefault});
       
   324 	}
       
   325 
   192 	void process() override {
   326 	void process() override {
   193 		for (INIContentHandler* handler : handlers) handler->startDocument();
   327 		for (INIContentHandler* handler : handlers) handler->startDocument();
   194 
   328 
   195 		bool inSection = false;
   329 		bool inSection = false;
   196 
   330 
   197 		while (input.good()) { // TODO: condition
   331 		while (input.good()) { // TODO: condition
   198 			{
   332 			{
       
   333 				INIContentHandler::WhitespaceEvent event;
       
   334 				event.lineNumber = lineNumber;
   199 				std::string whitespace = readAllWhitespace();
   335 				std::string whitespace = readAllWhitespace();
   200 				if (whitespace.size()) {
   336 				if (whitespace.size()) {
   201 					INIContentHandler::WhitespaceEvent event;
       
   202 					event.lineNumber = lineNumber;
       
   203 					event.eventNumber = ++eventNumber;
   337 					event.eventNumber = ++eventNumber;
   204 					event.whitespace = whitespace;
   338 					event.whitespace = whitespace;
   205 					for (INIContentHandler* handler : handlers) handler->whitespace(event);
   339 					for (INIContentHandler* handler : handlers) handler->whitespace(event);
   206 				}
   340 				}
   207 			}
   341 			}
   211 
   345 
   212 			char ch = peek();
   346 			char ch = peek();
   213 
   347 
   214 			if (ch == std::istream::traits_type::eof()) {
   348 			if (ch == std::istream::traits_type::eof()) {
   215 				break;
   349 				break;
   216 			} else if (ch == '[') {
   350 			} else if (ch == '[' && allowSections) {
   217 				if (inSection) for (INIContentHandler* handler : handlers) handler->endSection();
   351 				if (inSection) for (INIContentHandler* handler : handlers) handler->endSection();
   218 				inSection = true;
   352 				inSection = true;
   219 				get();
       
   220 				readAllWhitespace();
       
   221 				INIContentHandler::SectionStartEvent event;
   353 				INIContentHandler::SectionStartEvent event;
   222 				event.lineNumber = lineNumber;
   354 				event.lineNumber = lineNumber;
   223 				event.eventNumber = ++eventNumber;
   355 				event.eventNumber = ++eventNumber;
       
   356 				get();
       
   357 				readAllWhitespace();
   224 				event.name = readTokenAndEatTerminator(']', &quote, &found);
   358 				event.name = readTokenAndEatTerminator(']', &quote, &found);
       
   359 				if (!quote) event.name = trim(event.name);
       
   360 				event.name = unescape(event.name, UnescapingProcessor::TextType::SectionName);
   225 
   361 
   226 				readSpacesAndTabs();
   362 				readSpacesAndTabs();
   227 				if (allowSectionTags && peek() == '[') {
   363 				if (allowSectionTags && peek() == '[') {
   228 					get();
   364 					get();
   229 					event.tag = readTokenAndEatTerminator(']', &quote, &found);
   365 					event.tag = readTokenAndEatTerminator(']', &quote, &found);
       
   366 					event.tag = unescape(event.tag, UnescapingProcessor::TextType::SectionTag);
   230 				}
   367 				}
   231 
   368 
   232 				readSpacesAndTabs();
   369 				readSpacesAndTabs();
   233 				ch = peek();
   370 				ch = peek();
   234 				if (isComment(ch)) {
   371 				if (isComment(ch)) {
   235 					get();
   372 					get();
   236 					readSpacesAndTabs();
   373 					readSpacesAndTabs();
   237 					event.comment = readUntil('\n', &found);
   374 					event.comment = readUntil('\n', &found);
       
   375 					event.comment = unescape(event.comment, UnescapingProcessor::TextType::SectionComment);
   238 				} else if (ch == '\n') {
   376 				} else if (ch == '\n') {
   239 					get();
   377 					get();
   240 				} else {
   378 				} else {
   241 					throw std::logic_error(std::string("unexpected content after the section: '") + event.name + "'");
   379 					throw std::logic_error(std::string("unexpected content after the section: '") + event.name + "'");
   242 				}
   380 				}
   243 
   381 
   244 				for (INIContentHandler* handler : handlers) handler->startSection(event);
   382 				for (INIContentHandler* handler : handlers) handler->startSection(event);
   245 			} else if (isComment(ch)) {
   383 			} else if (isComment(ch)) {
   246 				get();
       
   247 				readSpacesAndTabs();
       
   248 				INIContentHandler::CommentEvent event;
   384 				INIContentHandler::CommentEvent event;
   249 				event.lineNumber = lineNumber;
   385 				event.lineNumber = lineNumber;
   250 				event.eventNumber = ++eventNumber;
   386 				event.eventNumber = ++eventNumber;
       
   387 				get();
       
   388 				readSpacesAndTabs();
   251 				event.comment = readUntil('\n', &found);
   389 				event.comment = readUntil('\n', &found);
       
   390 				event.comment = unescape(event.comment, UnescapingProcessor::TextType::Comment);
   252 				for (INIContentHandler* handler : handlers) handler->comment(event);
   391 				for (INIContentHandler* handler : handlers) handler->comment(event);
   253 			} else {
   392 			} else {
   254 				std::string fullKey = readToken('=', &quote, &found);
   393 				INIContentHandler::EntryEvent event;
       
   394 				event.lineNumber = lineNumber;
       
   395 				event.eventNumber = ++eventNumber;
       
   396 
       
   397 				std::string fullKey = readToken(keyValueSeparators, &quote, &found);
   255 				if (!found) throw std::logic_error(std::string("missing = after key: '") + fullKey + "'");
   398 				if (!found) throw std::logic_error(std::string("missing = after key: '") + fullKey + "'");
   256 				if (!quote) fullKey = trim(fullKey);
   399 				if (!quote) fullKey = trim(fullKey);
   257 				readSpacesAndTabs();
   400 				readSpacesAndTabs();
   258 
   401 
   259 				if (quote) {
   402 				if (quote) {
   260 					ch = get();
   403 					ch = get();
   261 					if (ch == '=') readSpacesAndTabs();
   404 					if (oneOf(ch, keyValueSeparators)) readSpacesAndTabs();
   262 					else throw std::logic_error(std::string("missing = after quoted key: '") + fullKey + "'");
   405 					else throw std::logic_error(std::string("missing = after quoted key: '") + fullKey + "'");
   263 				}
   406 				}
   264 
   407 
   265 				std::string value = readToken('\n', &quote, &found);
   408 				std::string value = readToken('\n', &quote, &found);
   266 				if (!quote) value = trim(value);
   409 				if (!quote) value = trim(value);
   267 
   410 
   268 				INIContentHandler::EntryEvent event;
       
   269 				event.lineNumber = lineNumber;
       
   270 				event.eventNumber = ++eventNumber;
       
   271 				event.key = fullKey;
   411 				event.key = fullKey;
   272 				event.fullKey = fullKey;
   412 				event.fullKey = fullKey;
   273 				event.value = value;
   413 				event.value = value;
   274 
   414 
   275 				if (allowSubKeys) {
   415 				if (allowSubKeys) {
   276 					std::smatch match;
   416 					std::smatch match;
   277 					if (std::regex_match(fullKey, match, std::regex("([^\\[]+)\\[([^\\[]+)\\]"))) {
   417 					if (std::regex_match(fullKey, match, std::regex("([^\\[]+)\\[([^\\[]+)\\]"))) {
   278 						event.key = match[1];
   418 						event.key = match[1];
   279 						event.subKey = match[2];
   419 						event.subKey = match[2];
   280 						event.fullKey = fullKey;
   420 						event.fullKey = fullKey;
       
   421 						event.subKey = unescape(event.subKey, UnescapingProcessor::TextType::EntryKey);
   281 					}
   422 					}
   282 				}
   423 				}
       
   424 
       
   425 				event.key = unescape(event.key, UnescapingProcessor::TextType::EntryKey);
       
   426 				event.fullKey = unescape(event.fullKey, UnescapingProcessor::TextType::EntryKey);
       
   427 				event.value = unescape(event.value, UnescapingProcessor::TextType::EntryValue);
   283 
   428 
   284 				if (quote) {
   429 				if (quote) {
   285 					readSpacesAndTabs();
   430 					readSpacesAndTabs();
   286 					ch = peek();
   431 					ch = peek();
   287 					if (isComment(ch)) {
   432 					if (isComment(ch)) {
   288 						get();
   433 						get();
   289 						readSpacesAndTabs();
   434 						readSpacesAndTabs();
   290 						event.comment = readUntil('\n', &found);
   435 						event.comment = readUntil('\n', &found);
       
   436 						event.comment = unescape(event.comment, UnescapingProcessor::TextType::EntryComment);
   291 					} else if (ch == '\n') {
   437 					} else if (ch == '\n') {
   292 						get();
   438 						get();
   293 					} else {
   439 					} else {
       
   440 						// TODO: optional support for multiple tokens in a single entry?
       
   441 						// modes: array, concatenate
       
   442 						// some-array-1 = "item 1" "item 2" 'item 3' item 4
       
   443 						// some-array-2 = "item 1" "item 2" 'item 3' item_4 item_5
       
   444 						// some-bash-style-string-value = "this "will' be' concatenated → this will be concatenated
   294 						throw std::logic_error(std::string("unexpected content after the quoted value: key='") + fullKey + "' value='" + event.value + "'");
   445 						throw std::logic_error(std::string("unexpected content after the quoted value: key='") + fullKey + "' value='" + event.value + "'");
   295 					}
   446 					}
   296 				}
   447 				}
   297 
   448 
   298 				for (INIContentHandler* handler : handlers) handler->entry(event);
   449 				for (INIContentHandler* handler : handlers) handler->entry(event);