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(']', "e, &found); |
358 event.name = readTokenAndEatTerminator(']', "e, &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(']', "e, &found); |
365 event.tag = readTokenAndEatTerminator(']', "e, &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('=', "e, &found); |
393 INIContentHandler::EntryEvent event; |
|
394 event.lineNumber = lineNumber; |
|
395 event.eventNumber = ++eventNumber; |
|
396 |
|
397 std::string fullKey = readToken(keyValueSeparators, "e, &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', "e, &found); |
408 std::string value = readToken('\n', "e, &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); |