CMake Nightly Date Stamp
[kiteware-cmake.git] / Source / cmVisualStudioSlnParser.cxx
blobadfd4c56c4b4e8d9121977c276916dd72174b2b6
1 /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
2 file Copyright.txt or https://cmake.org/licensing for details. */
3 #include "cmVisualStudioSlnParser.h"
5 #include <cassert>
6 #include <memory>
7 #include <stack>
8 #include <utility>
9 #include <vector>
11 #include <cmext/string_view>
13 #include "cmsys/FStream.hxx"
15 #include "cmStringAlgorithms.h"
16 #include "cmSystemTools.h"
17 #include "cmVisualStudioSlnData.h"
19 namespace {
20 enum LineFormat
22 LineMultiValueTag,
23 LineSingleValueTag,
24 LineKeyValuePair,
25 LineVerbatim
29 class cmVisualStudioSlnParser::ParsedLine
31 public:
32 bool IsComment() const;
33 bool IsKeyValuePair() const;
35 const std::string& GetTag() const { return this->Tag; }
36 const std::string& GetArg() const { return this->Arg.first; }
37 std::string GetArgVerbatim() const;
38 size_t GetValueCount() const { return this->Values.size(); }
39 const std::string& GetValue(size_t idxValue) const;
40 std::string GetValueVerbatim(size_t idxValue) const;
42 void SetTag(const std::string& tag) { this->Tag = tag; }
43 void SetArg(const std::string& arg) { this->Arg = StringData(arg, false); }
44 void SetQuotedArg(const std::string& arg)
46 this->Arg = StringData(arg, true);
48 void AddValue(const std::string& value)
50 this->Values.push_back(StringData(value, false));
52 void AddQuotedValue(const std::string& value)
54 this->Values.push_back(StringData(value, true));
57 void CopyVerbatim(const std::string& line) { this->Tag = line; }
59 private:
60 using StringData = std::pair<std::string, bool>;
61 std::string Tag;
62 StringData Arg;
63 std::vector<StringData> Values;
64 static const std::string BadString;
65 static const std::string Quote;
68 const std::string cmVisualStudioSlnParser::ParsedLine::BadString;
69 const std::string cmVisualStudioSlnParser::ParsedLine::Quote("\"");
71 bool cmVisualStudioSlnParser::ParsedLine::IsComment() const
73 assert(!this->Tag.empty());
74 return (this->Tag[0] == '#');
77 bool cmVisualStudioSlnParser::ParsedLine::IsKeyValuePair() const
79 assert(!this->Tag.empty());
80 return this->Arg.first.empty() && this->Values.size() == 1;
83 std::string cmVisualStudioSlnParser::ParsedLine::GetArgVerbatim() const
85 if (this->Arg.second) {
86 return cmStrCat(Quote, this->Arg.first, Quote);
88 return this->Arg.first;
91 const std::string& cmVisualStudioSlnParser::ParsedLine::GetValue(
92 size_t idxValue) const
94 if (idxValue < this->Values.size()) {
95 return this->Values[idxValue].first;
97 return BadString;
100 std::string cmVisualStudioSlnParser::ParsedLine::GetValueVerbatim(
101 size_t idxValue) const
103 if (idxValue < this->Values.size()) {
104 const StringData& data = this->Values[idxValue];
105 if (data.second) {
106 return cmStrCat(Quote, data.first, Quote);
108 return data.first;
110 return BadString;
113 class cmVisualStudioSlnParser::State
115 public:
116 explicit State(DataGroupSet requestedData);
118 size_t GetCurrentLine() const { return this->CurrentLine; }
119 bool ReadLine(std::istream& input, std::string& line);
121 LineFormat NextLineFormat() const;
123 bool Process(const cmVisualStudioSlnParser::ParsedLine& line,
124 cmSlnData& output, cmVisualStudioSlnParser::ResultData& result);
126 bool Finished(cmVisualStudioSlnParser::ResultData& result);
128 private:
129 enum FileState
131 FileStateStart,
132 FileStateTopLevel,
133 FileStateProject,
134 FileStateProjectDependencies,
135 FileStateGlobal,
136 FileStateSolutionConfigurations,
137 FileStateProjectConfigurations,
138 FileStateSolutionFilters,
139 FileStateGlobalSection,
140 FileStateIgnore
142 std::stack<FileState> Stack;
143 std::string EndIgnoreTag;
144 DataGroupSet RequestedData;
145 size_t CurrentLine = 0;
147 void IgnoreUntilTag(const std::string& endTag);
150 cmVisualStudioSlnParser::State::State(DataGroupSet requestedData)
151 : RequestedData(requestedData)
153 if (this->RequestedData.test(DataGroupProjectDependenciesBit)) {
154 this->RequestedData.set(DataGroupProjectsBit);
156 this->Stack.push(FileStateStart);
159 bool cmVisualStudioSlnParser::State::ReadLine(std::istream& input,
160 std::string& line)
162 ++this->CurrentLine;
163 return !std::getline(input, line).fail();
166 LineFormat cmVisualStudioSlnParser::State::NextLineFormat() const
168 switch (this->Stack.top()) {
169 case FileStateStart:
170 return LineVerbatim;
171 case FileStateTopLevel:
172 return LineMultiValueTag;
173 case FileStateProject:
174 case FileStateGlobal:
175 return LineSingleValueTag;
176 case FileStateProjectDependencies:
177 case FileStateSolutionConfigurations:
178 case FileStateProjectConfigurations:
179 case FileStateSolutionFilters:
180 case FileStateGlobalSection:
181 return LineKeyValuePair;
182 case FileStateIgnore:
183 return LineVerbatim;
184 default:
185 assert(false);
186 return LineVerbatim;
190 bool cmVisualStudioSlnParser::State::Process(
191 const cmVisualStudioSlnParser::ParsedLine& line, cmSlnData& output,
192 cmVisualStudioSlnParser::ResultData& result)
194 assert(!line.IsComment());
195 switch (this->Stack.top()) {
196 case FileStateStart:
197 if (!cmHasLiteralPrefix(line.GetTag(),
198 "Microsoft Visual Studio Solution File")) {
199 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
200 return false;
202 this->Stack.pop();
203 this->Stack.push(FileStateTopLevel);
204 break;
205 case FileStateTopLevel:
206 if (line.GetTag() == "Project"_s) {
207 if (line.GetValueCount() != 3) {
208 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
209 return false;
211 if (this->RequestedData.test(DataGroupProjectsBit)) {
212 if (!output.AddProject(line.GetValue(2), line.GetValue(0),
213 line.GetValue(1))) {
214 result.SetError(ResultErrorInputData, this->GetCurrentLine());
215 return false;
217 this->Stack.push(FileStateProject);
218 } else {
219 this->IgnoreUntilTag("EndProject");
221 } else if (line.GetTag() == "Global"_s) {
223 this->Stack.push(FileStateGlobal);
224 } else if (line.GetTag() == "VisualStudioVersion"_s) {
225 output.SetVisualStudioVersion(line.GetValue(0));
226 } else if (line.GetTag() == "MinimumVisualStudioVersion"_s) {
227 output.SetMinimumVisualStudioVersion(line.GetValue(0));
228 } else {
229 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
230 return false;
232 break;
233 case FileStateProject:
234 if (line.GetTag() == "EndProject"_s) {
235 this->Stack.pop();
236 } else if (line.GetTag() == "ProjectSection"_s) {
237 if (line.GetArg() == "ProjectDependencies"_s &&
238 line.GetValue(0) == "postProject"_s) {
239 if (this->RequestedData.test(DataGroupProjectDependenciesBit)) {
240 this->Stack.push(FileStateProjectDependencies);
241 } else {
242 this->IgnoreUntilTag("EndProjectSection");
244 } else {
245 this->IgnoreUntilTag("EndProjectSection");
247 } else {
248 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
249 return false;
251 break;
252 case FileStateProjectDependencies:
253 if (line.GetTag() == "EndProjectSection"_s) {
254 this->Stack.pop();
255 } else if (line.IsKeyValuePair()) {
256 // implement dependency storing here, once needed
258 } else {
259 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
260 return false;
262 break;
263 case FileStateGlobal:
264 if (line.GetTag() == "EndGlobal"_s) {
265 this->Stack.pop();
266 } else if (line.GetTag() == "GlobalSection"_s) {
267 if (line.GetArg() == "SolutionConfigurationPlatforms"_s &&
268 line.GetValue(0) == "preSolution"_s) {
269 if (this->RequestedData.test(DataGroupSolutionConfigurationsBit)) {
270 this->Stack.push(FileStateSolutionConfigurations);
271 } else {
272 this->IgnoreUntilTag("EndGlobalSection");
274 } else if (line.GetArg() == "ProjectConfigurationPlatforms"_s &&
275 line.GetValue(0) == "postSolution"_s) {
276 if (this->RequestedData.test(DataGroupProjectConfigurationsBit)) {
277 this->Stack.push(FileStateProjectConfigurations);
278 } else {
279 this->IgnoreUntilTag("EndGlobalSection");
281 } else if (line.GetArg() == "NestedProjects"_s &&
282 line.GetValue(0) == "preSolution"_s) {
283 if (this->RequestedData.test(DataGroupSolutionFiltersBit)) {
284 this->Stack.push(FileStateSolutionFilters);
285 } else {
286 this->IgnoreUntilTag("EndGlobalSection");
288 } else if (this->RequestedData.test(
289 DataGroupGenericGlobalSectionsBit)) {
290 this->Stack.push(FileStateGlobalSection);
291 } else {
292 this->IgnoreUntilTag("EndGlobalSection");
294 } else {
295 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
296 return false;
298 break;
299 case FileStateSolutionConfigurations:
300 if (line.GetTag() == "EndGlobalSection"_s) {
301 this->Stack.pop();
302 } else if (line.IsKeyValuePair()) {
303 output.AddConfiguration(line.GetValue(0));
304 } else {
305 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
306 return false;
308 break;
309 case FileStateProjectConfigurations:
310 if (line.GetTag() == "EndGlobalSection"_s) {
311 this->Stack.pop();
312 } else if (line.IsKeyValuePair()) {
313 std::vector<std::string> tagElements =
314 cmSystemTools::SplitString(line.GetTag(), '.');
315 if (tagElements.size() != 3 && tagElements.size() != 4) {
316 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
317 return false;
320 std::string guid = tagElements[0];
321 std::string solutionConfiguration = tagElements[1];
322 std::string activeBuild = tagElements[2];
323 cm::optional<cmSlnProjectEntry> projectEntry =
324 output.GetProjectByGUID(guid);
326 if (!projectEntry) {
327 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
328 return false;
331 if (activeBuild == "ActiveCfg"_s) {
332 projectEntry->AddProjectConfiguration(solutionConfiguration,
333 line.GetValue(0));
335 } else {
336 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
337 return false;
339 break;
340 case FileStateSolutionFilters:
341 if (line.GetTag() == "EndGlobalSection"_s) {
342 this->Stack.pop();
343 } else if (line.IsKeyValuePair()) {
344 // implement filter storing here, once needed
346 } else {
347 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
348 return false;
350 break;
351 case FileStateGlobalSection:
352 if (line.GetTag() == "EndGlobalSection"_s) {
353 this->Stack.pop();
354 } else if (line.IsKeyValuePair()) {
355 // implement section storing here, once needed
357 } else {
358 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
359 return false;
361 break;
362 case FileStateIgnore:
363 if (line.GetTag() == this->EndIgnoreTag) {
364 this->Stack.pop();
365 this->EndIgnoreTag.clear();
367 break;
368 default:
369 result.SetError(ResultErrorBadInternalState, this->GetCurrentLine());
370 return false;
372 return true;
375 bool cmVisualStudioSlnParser::State::Finished(
376 cmVisualStudioSlnParser::ResultData& result)
378 if (this->Stack.top() != FileStateTopLevel) {
379 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
380 return false;
382 result.Result = ResultOK;
383 return true;
386 void cmVisualStudioSlnParser::State::IgnoreUntilTag(const std::string& endTag)
388 this->Stack.push(FileStateIgnore);
389 this->EndIgnoreTag = endTag;
392 cmVisualStudioSlnParser::ResultData::ResultData() = default;
394 void cmVisualStudioSlnParser::ResultData::Clear()
396 *this = ResultData();
399 void cmVisualStudioSlnParser::ResultData::SetError(ParseResult error,
400 size_t line)
402 this->Result = error;
403 this->ResultLine = line;
406 const cmVisualStudioSlnParser::DataGroupSet
407 cmVisualStudioSlnParser::DataGroupProjects(
408 1 << cmVisualStudioSlnParser::DataGroupProjectsBit);
410 const cmVisualStudioSlnParser::DataGroupSet
411 cmVisualStudioSlnParser::DataGroupProjectDependencies(
412 1 << cmVisualStudioSlnParser::DataGroupProjectDependenciesBit);
414 const cmVisualStudioSlnParser::DataGroupSet
415 cmVisualStudioSlnParser::DataGroupSolutionConfigurations(
416 1 << cmVisualStudioSlnParser::DataGroupSolutionConfigurationsBit);
418 const cmVisualStudioSlnParser::DataGroupSet
419 cmVisualStudioSlnParser::DataGroupProjectConfigurations(
420 1 << cmVisualStudioSlnParser::DataGroupProjectConfigurationsBit);
422 const cmVisualStudioSlnParser::DataGroupSet
423 cmVisualStudioSlnParser::DataGroupSolutionFilters(
424 1 << cmVisualStudioSlnParser::DataGroupSolutionFiltersBit);
426 const cmVisualStudioSlnParser::DataGroupSet
427 cmVisualStudioSlnParser::DataGroupGenericGlobalSections(
428 1 << cmVisualStudioSlnParser::DataGroupGenericGlobalSectionsBit);
430 const cmVisualStudioSlnParser::DataGroupSet
431 cmVisualStudioSlnParser::DataGroupAll(~0);
433 bool cmVisualStudioSlnParser::Parse(std::istream& input, cmSlnData& output,
434 DataGroupSet dataGroups)
436 this->LastResult.Clear();
437 if (!this->IsDataGroupSetSupported(dataGroups)) {
438 this->LastResult.SetError(ResultErrorUnsupportedDataGroup, 0);
439 return false;
441 State state(dataGroups);
442 return this->ParseImpl(input, output, state);
445 bool cmVisualStudioSlnParser::ParseFile(const std::string& file,
446 cmSlnData& output,
447 DataGroupSet dataGroups)
449 this->LastResult.Clear();
450 if (!this->IsDataGroupSetSupported(dataGroups)) {
451 this->LastResult.SetError(ResultErrorUnsupportedDataGroup, 0);
452 return false;
454 cmsys::ifstream f(file.c_str());
455 if (!f) {
456 this->LastResult.SetError(ResultErrorOpeningInput, 0);
457 return false;
459 State state(dataGroups);
460 return this->ParseImpl(f, output, state);
463 cmVisualStudioSlnParser::ParseResult cmVisualStudioSlnParser::GetParseResult()
464 const
466 return this->LastResult.Result;
469 size_t cmVisualStudioSlnParser::GetParseResultLine() const
471 return this->LastResult.ResultLine;
474 bool cmVisualStudioSlnParser::GetParseHadBOM() const
476 return this->LastResult.HadBOM;
479 bool cmVisualStudioSlnParser::IsDataGroupSetSupported(
480 DataGroupSet dataGroups) const
482 return (dataGroups & DataGroupProjects) != 0;
485 bool cmVisualStudioSlnParser::ParseImpl(std::istream& input, cmSlnData& output,
486 State& state)
488 std::string line;
489 // Does the .sln start with a Byte Order Mark?
490 if (!this->ParseBOM(input, line, state)) {
491 return false;
493 do {
494 line = cmTrimWhitespace(line);
495 if (line.empty()) {
496 continue;
498 ParsedLine parsedLine;
499 switch (state.NextLineFormat()) {
500 case LineMultiValueTag:
501 if (!this->ParseMultiValueTag(line, parsedLine, state)) {
502 return false;
504 break;
505 case LineSingleValueTag:
506 if (!this->ParseSingleValueTag(line, parsedLine, state)) {
507 return false;
509 break;
510 case LineKeyValuePair:
511 if (!this->ParseKeyValuePair(line, parsedLine, state)) {
512 return false;
514 break;
515 case LineVerbatim:
516 parsedLine.CopyVerbatim(line);
517 break;
519 if (parsedLine.IsComment()) {
520 continue;
522 if (!state.Process(parsedLine, output, this->LastResult)) {
523 return false;
525 } while (state.ReadLine(input, line));
526 return state.Finished(this->LastResult);
529 bool cmVisualStudioSlnParser::ParseBOM(std::istream& input, std::string& line,
530 State& state)
532 char bom[4];
533 if (!input.get(bom, 4)) {
534 this->LastResult.SetError(ResultErrorReadingInput, 1);
535 return false;
537 this->LastResult.HadBOM =
538 (bom[0] == char(0xEF) && bom[1] == char(0xBB) && bom[2] == char(0xBF));
539 if (!state.ReadLine(input, line)) {
540 this->LastResult.SetError(ResultErrorReadingInput, 1);
541 return false;
543 if (!this->LastResult.HadBOM) {
544 line = cmStrCat(bom, line); // it wasn't a BOM, prepend it to first line
546 return true;
549 bool cmVisualStudioSlnParser::ParseMultiValueTag(const std::string& line,
550 ParsedLine& parsedLine,
551 State& state)
553 size_t idxEqualSign = line.find('=');
554 auto fullTag = cm::string_view(line).substr(0, idxEqualSign);
555 if (!this->ParseTag(fullTag, parsedLine, state)) {
556 return false;
558 if (idxEqualSign != std::string::npos) {
559 size_t idxFieldStart = idxEqualSign + 1;
560 if (idxFieldStart < line.size()) {
561 size_t idxParsing = idxFieldStart;
562 bool inQuotes = false;
563 for (;;) {
564 idxParsing = line.find_first_of(",\"", idxParsing);
565 bool fieldOver = false;
566 if (idxParsing == std::string::npos) {
567 fieldOver = true;
568 if (inQuotes) {
569 this->LastResult.SetError(ResultErrorInputStructure,
570 state.GetCurrentLine());
571 return false;
573 } else if (line[idxParsing] == ',' && !inQuotes) {
574 fieldOver = true;
575 } else if (line[idxParsing] == '"') {
576 inQuotes = !inQuotes;
578 if (fieldOver) {
579 if (!this->ParseValue(
580 line.substr(idxFieldStart, idxParsing - idxFieldStart),
581 parsedLine)) {
582 return false;
584 if (idxParsing == std::string::npos) {
585 break; // end of last field
587 idxFieldStart = idxParsing + 1;
589 ++idxParsing;
593 return true;
596 bool cmVisualStudioSlnParser::ParseSingleValueTag(const std::string& line,
597 ParsedLine& parsedLine,
598 State& state)
600 size_t idxEqualSign = line.find('=');
601 auto fullTag = cm::string_view(line).substr(0, idxEqualSign);
602 if (!this->ParseTag(fullTag, parsedLine, state)) {
603 return false;
605 if (idxEqualSign != std::string::npos) {
606 if (!this->ParseValue(line.substr(idxEqualSign + 1), parsedLine)) {
607 return false;
610 return true;
613 bool cmVisualStudioSlnParser::ParseKeyValuePair(const std::string& line,
614 ParsedLine& parsedLine,
615 State& /*state*/)
617 size_t idxEqualSign = line.find('=');
618 if (idxEqualSign == std::string::npos) {
619 parsedLine.CopyVerbatim(line);
620 return true;
622 const std::string& key = line.substr(0, idxEqualSign);
623 parsedLine.SetTag(cmTrimWhitespace(key));
624 const std::string& value = line.substr(idxEqualSign + 1);
625 parsedLine.AddValue(cmTrimWhitespace(value));
626 return true;
629 bool cmVisualStudioSlnParser::ParseTag(cm::string_view fullTag,
630 ParsedLine& parsedLine, State& state)
632 size_t idxLeftParen = fullTag.find('(');
633 if (idxLeftParen == cm::string_view::npos) {
634 parsedLine.SetTag(cmTrimWhitespace(fullTag));
635 return true;
637 parsedLine.SetTag(cmTrimWhitespace(fullTag.substr(0, idxLeftParen)));
638 size_t idxRightParen = fullTag.rfind(')');
639 if (idxRightParen == cm::string_view::npos) {
640 this->LastResult.SetError(ResultErrorInputStructure,
641 state.GetCurrentLine());
642 return false;
644 const std::string& arg = cmTrimWhitespace(
645 fullTag.substr(idxLeftParen + 1, idxRightParen - idxLeftParen - 1));
646 if (arg.front() == '"') {
647 if (arg.back() != '"') {
648 this->LastResult.SetError(ResultErrorInputStructure,
649 state.GetCurrentLine());
650 return false;
652 parsedLine.SetQuotedArg(arg.substr(1, arg.size() - 2));
653 } else {
654 parsedLine.SetArg(arg);
656 return true;
659 bool cmVisualStudioSlnParser::ParseValue(const std::string& value,
660 ParsedLine& parsedLine)
662 const std::string& trimmed = cmTrimWhitespace(value);
663 if (trimmed.empty()) {
664 parsedLine.AddValue(trimmed);
665 } else if (trimmed.front() == '"' && trimmed.back() == '"') {
666 parsedLine.AddQuotedValue(trimmed.substr(1, trimmed.size() - 2));
667 } else {
668 parsedLine.AddValue(trimmed);
670 return true;