master
cpp 867 lines 22.9 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft Corporation. All rights reserved
4
5 Parses .gitconfig-style properties files. This consists of key-value pairs
6 divided into sections.
7
8 For example:
9
10 [section1]
11 key1 = value
12 key2 = " value with leading and trailing spaces "
13 key3 = value with \"embedded quotes\"
14 boolkey = true
15
16 # Comments start with hash
17 [section2]
18 intkey = 37
19 intkey2 = 0x3000 # integers can be in hex
20 intkey3 = 0644 # octal is OK too
21
22 [section3]
23 key = this key has a line continuation \
24 so that it can wrap to the next line
25
26 key2 = this key has an \n embedded newline
27 key3 = "this key uses quotes to # include a comment prefix"
28
29 --*/
30
31 #if defined(_MSC_VER)
32
33 #define strcasecmp _stricmp
34 #define strncasecmp _strnicmp
35 #define strdup _strdup
36 #define _WINSOCKAPI_
37
38 #include "precomp.h"
39
40 using wsl::shared::string::MacAddress;
41
42 #else
43
44 #include <cassert>
45 #include <csignal>
46
47 #endif
48
49 #include "configfile.h"
50 #include <ctype.h>
51 #include <limits.h>
52 #include <stdio.h>
53 #include <stdlib.h>
54 #include <string.h>
55 #include <algorithm>
56 #include <format>
57 #include "stringshared.h"
58 #include "Localization.h"
59
60 using wsl::shared::Localization;
61
62 bool ConfigKey::ParseImpl(const char* name, const char* value, const wchar_t* filePath, unsigned long fileLine, bool& result)
63 {
64 const auto parsed = wsl::shared::string::ParseBool(value);
65 if (!parsed.has_value())
66 {
67 EMIT_USER_WARNING(Localization::MessageConfigInvalidBoolean(value, name, filePath, fileLine));
68 return false;
69 }
70
71 result = parsed.value();
72 return true;
73 }
74
75 bool ConfigKey::ParseImpl(const char* name, const char* value, const wchar_t* filePath, unsigned long fileLine, int& result)
76 {
77 char* end{};
78 const long number = strtol(value, &end, 0);
79 if (*value == '\0' || *end != '\0' || number < INT_MIN || number > INT_MAX)
80 {
81 EMIT_USER_WARNING(Localization::MessageConfigInvalidInteger(value, name, filePath, fileLine));
82 return false;
83 }
84
85 result = number;
86 return true;
87 }
88
89 bool ConfigKey::ParseImpl(const char* name, const char* value, const wchar_t* filePath, unsigned long fileLine, std::string& result)
90 {
91 result = value;
92 return true;
93 }
94
95 bool ConfigKey::ParseImpl(const char* name, const char* value, const wchar_t* filePath, unsigned long fileLine, MemoryString result)
96 {
97 const auto memory = wsl::shared::string::ParseMemorySize(value);
98 if (!memory.has_value())
99 {
100 EMIT_USER_WARNING(wsl::shared::Localization::MessageInvalidNumberString(value, name, filePath, fileLine));
101 return false;
102 }
103
104 result.m_value = memory.value();
105 return true;
106 }
107
108 bool ConfigKey::ParseImpl(const char* name, const char* value, const wchar_t* filePath, unsigned long fileLine, std::wstring& result)
109 {
110 result = wsl::shared::string::MultiByteToWide(value);
111 return true;
112 }
113
114 #ifdef WIN32
115
116 bool ConfigKey::ParseImpl(const char* name, const char* value, const wchar_t* filePath, unsigned long fileLine, MacAddress& outValue)
117 {
118 if (auto parsed = wsl::shared::string::ParseMacAddressNoThrow<char>(value))
119 {
120 outValue = std::move(parsed.value());
121 }
122 else
123 {
124 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageConfigMacAddress(value, name, filePath, fileLine));
125 }
126
127 return true;
128 }
129
130 #endif
131
132 bool ConfigKey::ParseImpl(const char* name, const char* value, const wchar_t* filePath, unsigned long fileLine, std::filesystem::path& result)
133 {
134 result = wsl::shared::string::MultiByteToWide(value);
135 return true;
136 }
137
138 std::wstring ConfigKey::GetValueImpl(bool result)
139 {
140 return result ? L"true" : L"false";
141 }
142
143 std::wstring ConfigKey::GetValueImpl(int result)
144 {
145 return std::to_wstring(result);
146 }
147
148 std::wstring ConfigKey::GetValueImpl(const std::string& result)
149 {
150 return wsl::shared::string::MultiByteToWide(result);
151 }
152
153 std::wstring ConfigKey::GetValueImpl(const std::optional<std::string>& result)
154 {
155 return result.has_value() ? wsl::shared::string::MultiByteToWide(result.value()) : L"";
156 }
157
158 std::wstring ConfigKey::GetValueImpl(const MemoryString& result)
159 {
160 return std::to_wstring(result.m_value);
161 }
162
163 std::wstring ConfigKey::GetValueImpl(const std::wstring& result)
164 {
165 return result;
166 }
167
168 #ifdef WIN32
169
170 std::wstring ConfigKey::GetValueImpl(const MacAddress& result)
171 {
172 return wsl::shared::string::FormatMacAddress(result, L':');
173 }
174
175 #endif
176
177 bool ConfigKey::Matches(const char* name) const
178 {
179 return std::any_of(m_names.begin(), m_names.end(), [&](const auto& e) { return strcasecmp(e, name) == 0; });
180 }
181
182 bool ConfigKey::Matches(const char* name, size_t length) const
183 {
184 return std::any_of(m_names.begin(), m_names.end(), [&](const auto& e) { return strncasecmp(e, name, length) == 0; });
185 }
186
187 void ConfigKey::Parse(const char* name, const char* value, const wchar_t* fileName, unsigned long line)
188 {
189 if (m_parseResult.has_value())
190 {
191 EMIT_USER_WARNING(
192 Localization::MessageConfigKeyDuplicated(name, fileName, line, m_parseResult->first, fileName, m_parseResult->second));
193 return;
194 }
195
196 m_parse(name, value, fileName, line);
197 m_parseResult.emplace(name, line);
198 }
199
200 const std::vector<const char*>& ConfigKey::GetNames() const
201 {
202 return m_names;
203 }
204
205 std::wstring ConfigKey::GetValue() const
206 {
207 return m_getValue();
208 }
209
210 // Updates the configuration with the given value.
211 static void SetConfig(std::vector<ConfigKey>& keys, const char* keyName, const char* value, bool debug, const wchar_t* filePath, unsigned long fileLine)
212 {
213 const auto key = std::find_if(keys.begin(), keys.end(), [keyName](const auto& e) { return e.Matches(keyName); });
214 if (key == keys.end())
215 {
216 EMIT_USER_WARNING(Localization::MessageConfigUnknownKey(keyName, filePath, fileLine));
217 return;
218 }
219
220 key->Parse(keyName, value, filePath, fileLine);
221 }
222
223 // Returns whether a character is a horizontal space (' ' or '\t').
224 static bool IsHSpace(wint_t ch)
225 {
226 return ch == ' ' || ch == '\t';
227 }
228
229 // Parses a configuration file. If file is NULL, then just set the configuration
230 // to the default values.
231 int ParseConfigFile(std::vector<ConfigKey>& keys, FILE* file, int flags, const wchar_t* filePath)
232 {
233 std::wstring emptyStr;
234 return ParseConfigFile(keys, file, flags, filePath, emptyStr);
235 }
236
237 // Parses a configuration file. If file is NULL, then just set the configuration
238 // to the default values.
239 int ParseConfigFile(std::vector<ConfigKey>& keys, FILE* file, int flags, const wchar_t* filePath, std::wstring& configFileOutput, std::optional<ConfigKey> outputKey, bool removeKey)
240 {
241 wint_t ch = 0;
242 unsigned long line = 0;
243 bool trailingComment = false;
244 bool inQuote = false;
245 size_t trimmedLength = 0;
246 int result;
247 size_t sectionLength = 0;
248 std::string key = {0};
249 std::string value = {0};
250
251 // Function default is parse mode (updateConfigFile = false).
252 // Otherwise, update mode (though parsing logic is still used).
253 bool updateConfigFile = false;
254 bool outputKeyValueUpdated = false;
255 bool matchedKey = false;
256 bool firstMatchedKey = false;
257
258 if (outputKey.has_value())
259 {
260 updateConfigFile = true;
261 }
262
263 if (file == NULL)
264 {
265 result = 0;
266 if (updateConfigFile && !outputKeyValueUpdated && !removeKey)
267 {
268 goto WriteNewKeyValue;
269 }
270 else
271 {
272 goto Done;
273 }
274 }
275
276 NewLine:
277 if (!trailingComment)
278 {
279 line++;
280 }
281
282 // parse [section], key = value, or empty line
283 for (;;)
284 {
285 if (updateConfigFile && ch != 0 && ch != WEOF)
286 {
287 if (trailingComment && matchedKey)
288 {
289 // If we're removing a key and have a trailing comment,
290 // the comment will be preserved. The newline char will have
291 // been removed from the output stream (due to key removal),
292 // so insert it back here.
293 configFileOutput += L'\n';
294 }
295
296 // Write the current character to output now, since, in
297 // addition to writing the characters read in this loop,
298 // future parsing may jump back to the NewLine label
299 // and we assume the 'ch' has yet to be written.
300 configFileOutput += ch;
301 }
302
303 // Skip any pending comment.
304 if (ch == '#')
305 {
306 do
307 {
308 ch = fgetwc(file);
309
310 if (updateConfigFile && ch != WEOF)
311 {
312 // Write out the rest of the comment line.
313 configFileOutput += ch;
314 }
315
316 if (ch == '\r')
317 {
318 ch = fgetwc(file);
319 }
320
321 if (ch == '\n')
322 {
323 line++;
324 }
325
326 } while (ch != '\n' && ch != WEOF);
327
328 if (trailingComment)
329 {
330 trailingComment = false;
331 }
332 }
333
334 if (feof(file))
335 {
336 result = 0;
337 if (updateConfigFile && !outputKeyValueUpdated && !removeKey)
338 {
339 goto WriteNewKeyValue;
340 }
341 else
342 {
343 goto Done;
344 }
345 }
346
347 if (ferror(file))
348 {
349 result = -1;
350 goto Done;
351 }
352
353 // Skip leading spaces.
354 while (IsHSpace(ch = fgetwc(file)))
355 {
356 if (updateConfigFile)
357 {
358 configFileOutput += ch;
359 }
360 }
361
362 switch (ch)
363 {
364 case WEOF:
365 break;
366
367 case '\r':
368 {
369 auto nextc = fgetwc(file);
370 if (nextc == '\n')
371 {
372 line++;
373 }
374 else
375 {
376 ungetwc(nextc, file);
377 }
378
379 break;
380 }
381
382 case '\n':
383 line++;
384 break;
385
386 case '#':
387 break;
388
389 case '[':
390 // We're about to parse a new section. If we have an unwritten key-value
391 // and the current section matches, write it now before moving to the new section.
392 if (updateConfigFile && !outputKeyValueUpdated && !removeKey && sectionLength > 0)
393 {
394 const auto& outputConfigKey = outputKey.value();
395 if (outputConfigKey.Matches(key.c_str(), sectionLength))
396 {
397 const auto& keyNames = outputConfigKey.GetNames();
398 // Config key without name.
399 FAIL_FAST_IF(keyNames.empty());
400 const auto keyNameUtf8 = keyNames.front();
401 const auto keyName = wsl::shared::string::MultiByteToWide(keyNameUtf8);
402 const auto sectionKeySeparatorPos = keyName.find('.');
403 // Config key without separated section/key name
404 FAIL_FAST_IF(sectionKeySeparatorPos == std::string_view::npos);
405 // Config key without section name
406 FAIL_FAST_IF(sectionKeySeparatorPos == 0);
407 // Config key without key name
408 FAIL_FAST_IF(sectionKeySeparatorPos == (keyName.length() - 1));
409
410 // Remove any trailing newlines before inserting the new key-value
411 while (!configFileOutput.empty() && configFileOutput.back() == L'\n')
412 {
413 configFileOutput.pop_back();
414 }
415
416 auto keyValue = std::format(L"\n{}={}\n\n", keyName.substr(sectionKeySeparatorPos + 1), outputKey.value().GetValue());
417 configFileOutput += keyValue;
418 outputKeyValueUpdated = true;
419 }
420 }
421 goto ParseSection;
422
423 default:
424 if (!isalpha(ch))
425 {
426 if (flags & CFG_DEBUG)
427 {
428 fputs("expected a-z\n", stderr);
429 }
430
431 EMIT_USER_WARNING(Localization::MessageConfigInvalidKey(filePath, line));
432
433 if (updateConfigFile)
434 {
435 // Always write out the invalid character
436 // prior to jumping to the InvalidLine label.
437 configFileOutput += ch;
438 ch = 0;
439 }
440
441 goto InvalidLine;
442 }
443
444 goto ParseKeyValue;
445 }
446 }
447
448 ParseSection:
449 // parse [section] ([ is already parsed)
450 if (updateConfigFile)
451 {
452 // Write the '[' character to the output.
453 configFileOutput += ch;
454 }
455
456 ch = fgetwc(file);
457
458 if (!isalpha(ch))
459 {
460 if (flags & CFG_DEBUG)
461 {
462 fputs("expected a-z\n", stderr);
463 }
464
465 EMIT_USER_WARNING(Localization::MessageConfigInvalidSection(filePath, line));
466
467 if (updateConfigFile)
468 {
469 // Always write out the invalid character
470 // prior to jumping to the InvalidLine label.
471 configFileOutput += ch;
472 ch = 0;
473 }
474
475 goto InvalidLine;
476 }
477
478 key.clear();
479
480 do
481 {
482 if (updateConfigFile)
483 {
484 // Write the first alpha character of the section
485 // name followed by the rest of the section name.
486 configFileOutput += ch;
487 }
488
489 key += static_cast<char>(ch);
490
491 ch = fgetwc(file);
492 } while (isalnum(ch));
493
494 if (ch != ']')
495 {
496 if (flags & CFG_DEBUG)
497 {
498 fputs("expected ]\n", stderr);
499 }
500
501 EMIT_USER_WARNING(Localization::MessageConfigExpected("']'", filePath, line));
502
503 if (updateConfigFile)
504 {
505 // Always write out the invalid character
506 // prior to jumping to the InvalidLine label.
507 configFileOutput += ch;
508 ch = 0;
509 }
510
511 goto InvalidLine;
512 }
513
514 if (updateConfigFile)
515 {
516 // Write the ']' character to the output.
517 configFileOutput += ch;
518 }
519
520 // Skip trailing space.
521 while (IsHSpace(ch = fgetwc(file)))
522 {
523 if (updateConfigFile)
524 {
525 configFileOutput += ch;
526 }
527 }
528
529 switch (ch)
530 {
531 case WEOF:
532 case '\n':
533 case '\r':
534 break;
535
536 case '#':
537 trailingComment = true;
538 break;
539
540 default:
541 if (flags & CFG_DEBUG)
542 {
543 fputs("expected space or EOL\n", stderr);
544 }
545
546 EMIT_USER_WARNING(Localization::MessageConfigExpected("' ' or '\\n'", filePath, line));
547
548 if (updateConfigFile)
549 {
550 // Always write out the invalid character
551 // prior to jumping to the InvalidLine label.
552 configFileOutput += ch;
553 ch = 0;
554 }
555
556 goto InvalidLine;
557 }
558
559 sectionLength = key.size();
560
561 goto NewLine;
562
563 ParseKeyValue:
564 // parse key = value. The first character of the key is in ch.
565 key.resize(sectionLength);
566 if (key.size() > 0)
567 {
568 key += '.';
569 }
570
571 do
572 {
573 if (updateConfigFile)
574 {
575 // Write out the first character of the key to the
576 // output followed by the rest of the key name.
577 configFileOutput += ch;
578 }
579
580 key += static_cast<char>(ch);
581
582 ch = fgetwc(file);
583 } while (isalnum(ch));
584
585 // Skip leading space.
586 while (IsHSpace(ch))
587 {
588 if (updateConfigFile)
589 {
590 configFileOutput += ch;
591 }
592
593 ch = fgetwc(file);
594 }
595
596 if (ch != '=')
597 {
598 if (flags & CFG_DEBUG)
599 {
600 fputs("expected =\n", stderr);
601 }
602
603 EMIT_USER_WARNING(Localization::MessageConfigExpected("'='", filePath, line));
604
605 if (updateConfigFile)
606 {
607 // Always write out the invalid character
608 // prior to jumping to the InvalidLine label.
609 configFileOutput += ch;
610 ch = 0;
611 }
612
613 goto InvalidLine;
614 }
615
616 if (updateConfigFile)
617 {
618 // Write the '=' character to the output.
619 configFileOutput += ch;
620 }
621
622 // Skip trailing space.
623 while (IsHSpace(ch = fgetwc(file)))
624 {
625 if (updateConfigFile)
626 {
627 configFileOutput += ch;
628 }
629 }
630
631 // Only match the first instance of the key in the input file.
632 // In other words, if we've already updated the matched key value,
633 // then ignore updating any other keys that match.
634 // This is consistent with the behavior of the parsing logic.
635 firstMatchedKey = false;
636 if (updateConfigFile && !outputKeyValueUpdated && !removeKey)
637 {
638 firstMatchedKey = outputKey.value().Matches(key.c_str());
639 }
640
641 // There may be multiple instances of the same key in the input file,
642 // so we need to find and remove all instances of the key.
643 matchedKey = false;
644 if (updateConfigFile && removeKey)
645 {
646 matchedKey = outputKey.value().Matches(key.c_str());
647 if (matchedKey)
648 {
649 auto previousNewLine = configFileOutput.rfind(L'\n');
650 if (previousNewLine != std::wstring::npos)
651 {
652 configFileOutput = configFileOutput.substr(0, previousNewLine);
653 }
654 }
655 }
656
657 // Parse the value by removing unescaped quotes, handling escaped n, t, \,
658 // ", and NewLine (line continuation). End parsing on a NewLine, EOF, or
659 // comment (#).
660 value.clear();
661 trimmedLength = 0;
662 inQuote = false;
663 while (ch != WEOF && ch != '\n' && ch != '\r')
664 {
665 if (updateConfigFile && !firstMatchedKey && !matchedKey && ch != '#')
666 {
667 // Write out the first character of the value to
668 // the output followed by the rest of the value.
669 // Don't write the '#' as it will be written by the
670 // NewLine label after the ValueDone label. This is
671 // done to ensure consistency with the writing logic.
672 configFileOutput += ch;
673 }
674
675 switch (ch)
676 {
677 case '"':
678 inQuote = !inQuote;
679 break;
680
681 case '\\':
682 {
683 auto ch2 = fgetwc(file);
684
685 if (updateConfigFile && !firstMatchedKey && !matchedKey && ch2 != WEOF)
686 {
687 // Write out the escaped character to the output, also,
688 // handling the case where ch2 is an invalid character.
689 configFileOutput += ch2;
690 }
691
692 switch (ch2)
693 {
694 case '\\':
695 case '"':
696 value += static_cast<char>(ch2);
697
698 break;
699
700 case 'b':
701 value += '\b';
702 break;
703
704 case 'n':
705 value += '\n';
706 break;
707
708 case 't':
709 value += '\t';
710 break;
711
712 case '\r':
713 break;
714
715 case '\n':
716 // Line continuation. Skip both characters.
717 line++;
718 break;
719
720 default:
721 if (flags & CFG_DEBUG)
722 {
723 fprintf(stderr, "unexpected escaped character %lc\n", ch2);
724 }
725
726 EMIT_USER_WARNING(Localization::MessageConfigInvalidEscape(static_cast<wchar_t>(ch2), filePath, line));
727
728 if (firstMatchedKey)
729 {
730 // This key value will be overwritten, so we can ignore any malformed values,
731 // since none of the value should/will have been written to the output file.
732 // However, we can still inform the user of the issue per the above warning.
733 break;
734 }
735
736 goto InvalidLine;
737 }
738 }
739
740 break;
741
742 case '#':
743 if (!inQuote)
744 {
745 trailingComment = true;
746 goto ValueDone;
747 }
748 default:
749 value += static_cast<char>(ch);
750
751 break;
752 }
753
754 // Track the length without trailing space.
755 if (!IsHSpace(ch))
756 {
757 trimmedLength = value.size();
758 }
759 ch = fgetwc(file);
760 }
761
762 ValueDone:
763 // If we overwrote an existing key value, where the value is malformed, we can ignore it.
764 if (inQuote)
765 {
766 if (flags & CFG_DEBUG)
767 {
768 fprintf(stderr, "expected \"\n");
769 }
770
771 EMIT_USER_WARNING(Localization::MessageConfigExpected("\"", filePath, line));
772
773 // This key value will be overwritten, so we can ignore any malformed values.
774 // However, we can still inform the user of the issue per warning above.
775 if (!firstMatchedKey && !matchedKey)
776 {
777 goto InvalidLine;
778 }
779 }
780
781 if (firstMatchedKey)
782 {
783 for (auto outValueCh : outputKey.value().GetValue())
784 {
785 configFileOutput += outValueCh;
786 }
787
788 // Preserve any spacing in the parsed value.
789 // Invalid values are still parsed in the case
790 // of trailing comments.
791 for (size_t spaceIdx = trimmedLength; spaceIdx < value.size(); spaceIdx++)
792 {
793 configFileOutput += value[spaceIdx];
794 }
795
796 outputKeyValueUpdated = true;
797 }
798 else if (!matchedKey)
799 {
800 // Trim any trailing space.
801 value.resize(trimmedLength);
802 SetConfig(keys, key.c_str(), value.c_str(), flags & CFG_DEBUG, filePath, line);
803 }
804
805 goto NewLine;
806
807 InvalidLine:
808 if (!(flags & CFG_SKIP_INVALID_LINES))
809 {
810 result = -1;
811 goto Done;
812 }
813
814 while (ch != WEOF && ch != '\n')
815 {
816 ch = fgetwc(file);
817
818 if (updateConfigFile && ch != WEOF && ch != '\n' && ch != '\r')
819 {
820 // Write out the rest of the remaining
821 // invalid line. WEOF and '\n' will be
822 // handled/written by the NewLine label.
823 configFileOutput += ch;
824 }
825 }
826
827 goto NewLine;
828
829 WriteNewKeyValue:
830 {
831 const auto& outputConfigKey = outputKey.value();
832 const auto& keyNames = outputConfigKey.GetNames();
833 // Config key without name.
834 FAIL_FAST_IF(keyNames.empty());
835 const auto keyNameUtf8 = keyNames.front();
836 const auto keyName = wsl::shared::string::MultiByteToWide(keyNameUtf8);
837 const auto sectionKeySeparatorPos = keyName.find('.');
838 // Config key without separated section/key name
839 FAIL_FAST_IF(sectionKeySeparatorPos == std::string_view::npos);
840 // Config key without section name
841 FAIL_FAST_IF(sectionKeySeparatorPos == 0);
842 // Config key without key name
843 FAIL_FAST_IF(sectionKeySeparatorPos == (keyName.length() - 1));
844
845 // This is a new key/value pair not present in the input file, so write it out.
846 // No need for newline if this is the first key/value pair.
847 if (file != NULL)
848 {
849 configFileOutput += L'\n';
850 }
851
852 // Check if we currently parsed a key and the key matches the section name.
853 // In this case, we don't need to write the section name again.
854 if (!(sectionLength > 0 && outputConfigKey.Matches(key.c_str(), sectionLength)))
855 {
856 configFileOutput += std::format(L"[{}]\n", keyName.substr(0, sectionKeySeparatorPos));
857 }
858
859 configFileOutput += std::format(L"{}={}", keyName.substr(sectionKeySeparatorPos + 1), outputKey.value().GetValue());
860
861 outputKeyValueUpdated = true;
862 goto Done;
863 }
864
865 Done:
866 return result;
867 }