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"
4
5 #include <cassert>
6 #include <stack>
7
8 #include "cmsys/FStream.hxx"
9
10 #include "cmStringAlgorithms.h"
11 #include "cmSystemTools.h"
12 #include "cmVisualStudioSlnData.h"
13
14 namespace {
15 enum LineFormat
16 {
17 LineMultiValueTag,
18 LineSingleValueTag,
19 LineKeyValuePair,
20 LineVerbatim
21 };
22 }
23
24 class cmVisualStudioSlnParser::ParsedLine
25 {
26 public:
27 bool IsComment() const;
28 bool IsKeyValuePair() const;
29
GetTag() const30 const std::string& GetTag() const { return this->Tag; }
GetArg() const31 const std::string& GetArg() const { return this->Arg.first; }
32 std::string GetArgVerbatim() const;
GetValueCount() const33 size_t GetValueCount() const { return this->Values.size(); }
34 const std::string& GetValue(size_t idxValue) const;
35 std::string GetValueVerbatim(size_t idxValue) const;
36
SetTag(const std::string & tag)37 void SetTag(const std::string& tag) { this->Tag = tag; }
SetArg(const std::string & arg)38 void SetArg(const std::string& arg) { this->Arg = StringData(arg, false); }
SetQuotedArg(const std::string & arg)39 void SetQuotedArg(const std::string& arg)
40 {
41 this->Arg = StringData(arg, true);
42 }
AddValue(const std::string & value)43 void AddValue(const std::string& value)
44 {
45 this->Values.push_back(StringData(value, false));
46 }
AddQuotedValue(const std::string & value)47 void AddQuotedValue(const std::string& value)
48 {
49 this->Values.push_back(StringData(value, true));
50 }
51
CopyVerbatim(const std::string & line)52 void CopyVerbatim(const std::string& line) { this->Tag = line; }
53
54 private:
55 using StringData = std::pair<std::string, bool>;
56 std::string Tag;
57 StringData Arg;
58 std::vector<StringData> Values;
59 static const std::string BadString;
60 static const std::string Quote;
61 };
62
63 const std::string cmVisualStudioSlnParser::ParsedLine::BadString;
64 const std::string cmVisualStudioSlnParser::ParsedLine::Quote("\"");
65
IsComment() const66 bool cmVisualStudioSlnParser::ParsedLine::IsComment() const
67 {
68 assert(!this->Tag.empty());
69 return (this->Tag[0] == '#');
70 }
71
IsKeyValuePair() const72 bool cmVisualStudioSlnParser::ParsedLine::IsKeyValuePair() const
73 {
74 assert(!this->Tag.empty());
75 return this->Arg.first.empty() && this->Values.size() == 1;
76 }
77
GetArgVerbatim() const78 std::string cmVisualStudioSlnParser::ParsedLine::GetArgVerbatim() const
79 {
80 if (this->Arg.second)
81 return Quote + this->Arg.first + Quote;
82 else
83 return this->Arg.first;
84 }
85
GetValue(size_t idxValue) const86 const std::string& cmVisualStudioSlnParser::ParsedLine::GetValue(
87 size_t idxValue) const
88 {
89 if (idxValue < this->Values.size())
90 return this->Values[idxValue].first;
91 else
92 return BadString;
93 }
94
GetValueVerbatim(size_t idxValue) const95 std::string cmVisualStudioSlnParser::ParsedLine::GetValueVerbatim(
96 size_t idxValue) const
97 {
98 if (idxValue < this->Values.size()) {
99 const StringData& data = this->Values[idxValue];
100 if (data.second)
101 return Quote + data.first + Quote;
102 else
103 return data.first;
104 } else
105 return BadString;
106 }
107
108 class cmVisualStudioSlnParser::State
109 {
110 public:
111 explicit State(DataGroupSet requestedData);
112
GetCurrentLine() const113 size_t GetCurrentLine() const { return this->CurrentLine; }
114 bool ReadLine(std::istream& input, std::string& line);
115
116 LineFormat NextLineFormat() const;
117
118 bool Process(const cmVisualStudioSlnParser::ParsedLine& line,
119 cmSlnData& output, cmVisualStudioSlnParser::ResultData& result);
120
121 bool Finished(cmVisualStudioSlnParser::ResultData& result);
122
123 private:
124 enum FileState
125 {
126 FileStateStart,
127 FileStateTopLevel,
128 FileStateProject,
129 FileStateProjectDependencies,
130 FileStateGlobal,
131 FileStateSolutionConfigurations,
132 FileStateProjectConfigurations,
133 FileStateSolutionFilters,
134 FileStateGlobalSection,
135 FileStateIgnore
136 };
137 std::stack<FileState> Stack;
138 std::string EndIgnoreTag;
139 DataGroupSet RequestedData;
140 size_t CurrentLine;
141
142 void IgnoreUntilTag(const std::string& endTag);
143 };
144
State(DataGroupSet requestedData)145 cmVisualStudioSlnParser::State::State(DataGroupSet requestedData)
146 : RequestedData(requestedData)
147 , CurrentLine(0)
148 {
149 if (this->RequestedData.test(DataGroupProjectDependenciesBit))
150 this->RequestedData.set(DataGroupProjectsBit);
151 this->Stack.push(FileStateStart);
152 }
153
ReadLine(std::istream & input,std::string & line)154 bool cmVisualStudioSlnParser::State::ReadLine(std::istream& input,
155 std::string& line)
156 {
157 ++this->CurrentLine;
158 return !std::getline(input, line).fail();
159 }
160
NextLineFormat() const161 LineFormat cmVisualStudioSlnParser::State::NextLineFormat() const
162 {
163 switch (this->Stack.top()) {
164 case FileStateStart:
165 return LineVerbatim;
166 case FileStateTopLevel:
167 return LineMultiValueTag;
168 case FileStateProject:
169 return LineSingleValueTag;
170 case FileStateProjectDependencies:
171 return LineKeyValuePair;
172 case FileStateGlobal:
173 return LineSingleValueTag;
174 case FileStateSolutionConfigurations:
175 return LineKeyValuePair;
176 case FileStateProjectConfigurations:
177 return LineKeyValuePair;
178 case FileStateSolutionFilters:
179 return LineKeyValuePair;
180 case FileStateGlobalSection:
181 return LineKeyValuePair;
182 case FileStateIgnore:
183 return LineVerbatim;
184 default:
185 assert(false);
186 return LineVerbatim;
187 }
188 }
189
Process(const cmVisualStudioSlnParser::ParsedLine & line,cmSlnData & output,cmVisualStudioSlnParser::ResultData & result)190 bool cmVisualStudioSlnParser::State::Process(
191 const cmVisualStudioSlnParser::ParsedLine& line, cmSlnData& output,
192 cmVisualStudioSlnParser::ResultData& result)
193 {
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;
201 }
202 this->Stack.pop();
203 this->Stack.push(FileStateTopLevel);
204 break;
205 case FileStateTopLevel:
206 if (line.GetTag().compare("Project") == 0) {
207 if (line.GetValueCount() != 3) {
208 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
209 return false;
210 }
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;
216 }
217 this->Stack.push(FileStateProject);
218 } else
219 this->IgnoreUntilTag("EndProject");
220 } else if (line.GetTag().compare("Global") == 0)
221 this->Stack.push(FileStateGlobal);
222 else {
223 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
224 return false;
225 }
226 break;
227 case FileStateProject:
228 if (line.GetTag().compare("EndProject") == 0)
229 this->Stack.pop();
230 else if (line.GetTag().compare("ProjectSection") == 0) {
231 if (line.GetArg().compare("ProjectDependencies") == 0 &&
232 line.GetValue(0).compare("postProject") == 0) {
233 if (this->RequestedData.test(DataGroupProjectDependenciesBit))
234 this->Stack.push(FileStateProjectDependencies);
235 else
236 this->IgnoreUntilTag("EndProjectSection");
237 } else
238 this->IgnoreUntilTag("EndProjectSection");
239 } else {
240 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
241 return false;
242 }
243 break;
244 case FileStateProjectDependencies:
245 if (line.GetTag().compare("EndProjectSection") == 0)
246 this->Stack.pop();
247 else if (line.IsKeyValuePair())
248 // implement dependency storing here, once needed
249 ;
250 else {
251 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
252 return false;
253 }
254 break;
255 case FileStateGlobal:
256 if (line.GetTag().compare("EndGlobal") == 0)
257 this->Stack.pop();
258 else if (line.GetTag().compare("GlobalSection") == 0) {
259 if (line.GetArg().compare("SolutionConfigurationPlatforms") == 0 &&
260 line.GetValue(0).compare("preSolution") == 0) {
261 if (this->RequestedData.test(DataGroupSolutionConfigurationsBit))
262 this->Stack.push(FileStateSolutionConfigurations);
263 else
264 this->IgnoreUntilTag("EndGlobalSection");
265 } else if (line.GetArg().compare("ProjectConfigurationPlatforms") ==
266 0 &&
267 line.GetValue(0).compare("postSolution") == 0) {
268 if (this->RequestedData.test(DataGroupProjectConfigurationsBit))
269 this->Stack.push(FileStateProjectConfigurations);
270 else
271 this->IgnoreUntilTag("EndGlobalSection");
272 } else if (line.GetArg().compare("NestedProjects") == 0 &&
273 line.GetValue(0).compare("preSolution") == 0) {
274 if (this->RequestedData.test(DataGroupSolutionFiltersBit))
275 this->Stack.push(FileStateSolutionFilters);
276 else
277 this->IgnoreUntilTag("EndGlobalSection");
278 } else if (this->RequestedData.test(DataGroupGenericGlobalSectionsBit))
279 this->Stack.push(FileStateGlobalSection);
280 else
281 this->IgnoreUntilTag("EndGlobalSection");
282 } else {
283 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
284 return false;
285 }
286 break;
287 case FileStateSolutionConfigurations:
288 if (line.GetTag().compare("EndGlobalSection") == 0)
289 this->Stack.pop();
290 else if (line.IsKeyValuePair())
291 // implement configuration storing here, once needed
292 ;
293 else {
294 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
295 return false;
296 }
297 break;
298 case FileStateProjectConfigurations:
299 if (line.GetTag().compare("EndGlobalSection") == 0)
300 this->Stack.pop();
301 else if (line.IsKeyValuePair())
302 // implement configuration storing here, once needed
303 ;
304 else {
305 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
306 return false;
307 }
308 break;
309 case FileStateSolutionFilters:
310 if (line.GetTag().compare("EndGlobalSection") == 0)
311 this->Stack.pop();
312 else if (line.IsKeyValuePair())
313 // implement filter storing here, once needed
314 ;
315 else {
316 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
317 return false;
318 }
319 break;
320 case FileStateGlobalSection:
321 if (line.GetTag().compare("EndGlobalSection") == 0)
322 this->Stack.pop();
323 else if (line.IsKeyValuePair())
324 // implement section storing here, once needed
325 ;
326 else {
327 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
328 return false;
329 }
330 break;
331 case FileStateIgnore:
332 if (line.GetTag() == this->EndIgnoreTag) {
333 this->Stack.pop();
334 this->EndIgnoreTag.clear();
335 }
336 break;
337 default:
338 result.SetError(ResultErrorBadInternalState, this->GetCurrentLine());
339 return false;
340 }
341 return true;
342 }
343
Finished(cmVisualStudioSlnParser::ResultData & result)344 bool cmVisualStudioSlnParser::State::Finished(
345 cmVisualStudioSlnParser::ResultData& result)
346 {
347 if (this->Stack.top() != FileStateTopLevel) {
348 result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
349 return false;
350 }
351 result.Result = ResultOK;
352 return true;
353 }
354
IgnoreUntilTag(const std::string & endTag)355 void cmVisualStudioSlnParser::State::IgnoreUntilTag(const std::string& endTag)
356 {
357 this->Stack.push(FileStateIgnore);
358 this->EndIgnoreTag = endTag;
359 }
360
ResultData()361 cmVisualStudioSlnParser::ResultData::ResultData()
362 : Result(ResultOK)
363 , ResultLine(0)
364 {
365 }
366
Clear()367 void cmVisualStudioSlnParser::ResultData::Clear()
368 {
369 *this = ResultData();
370 }
371
SetError(ParseResult error,size_t line)372 void cmVisualStudioSlnParser::ResultData::SetError(ParseResult error,
373 size_t line)
374 {
375 this->Result = error;
376 this->ResultLine = line;
377 }
378
379 const cmVisualStudioSlnParser::DataGroupSet
380 cmVisualStudioSlnParser::DataGroupProjects(
381 1 << cmVisualStudioSlnParser::DataGroupProjectsBit);
382
383 const cmVisualStudioSlnParser::DataGroupSet
384 cmVisualStudioSlnParser::DataGroupProjectDependencies(
385 1 << cmVisualStudioSlnParser::DataGroupProjectDependenciesBit);
386
387 const cmVisualStudioSlnParser::DataGroupSet
388 cmVisualStudioSlnParser::DataGroupSolutionConfigurations(
389 1 << cmVisualStudioSlnParser::DataGroupSolutionConfigurationsBit);
390
391 const cmVisualStudioSlnParser::DataGroupSet
392 cmVisualStudioSlnParser::DataGroupProjectConfigurations(
393 1 << cmVisualStudioSlnParser::DataGroupProjectConfigurationsBit);
394
395 const cmVisualStudioSlnParser::DataGroupSet
396 cmVisualStudioSlnParser::DataGroupSolutionFilters(
397 1 << cmVisualStudioSlnParser::DataGroupSolutionFiltersBit);
398
399 const cmVisualStudioSlnParser::DataGroupSet
400 cmVisualStudioSlnParser::DataGroupGenericGlobalSections(
401 1 << cmVisualStudioSlnParser::DataGroupGenericGlobalSectionsBit);
402
403 const cmVisualStudioSlnParser::DataGroupSet
404 cmVisualStudioSlnParser::DataGroupAll(~0);
405
Parse(std::istream & input,cmSlnData & output,DataGroupSet dataGroups)406 bool cmVisualStudioSlnParser::Parse(std::istream& input, cmSlnData& output,
407 DataGroupSet dataGroups)
408 {
409 this->LastResult.Clear();
410 if (!this->IsDataGroupSetSupported(dataGroups)) {
411 this->LastResult.SetError(ResultErrorUnsupportedDataGroup, 0);
412 return false;
413 }
414 State state(dataGroups);
415 return this->ParseImpl(input, output, state);
416 }
417
ParseFile(const std::string & file,cmSlnData & output,DataGroupSet dataGroups)418 bool cmVisualStudioSlnParser::ParseFile(const std::string& file,
419 cmSlnData& output,
420 DataGroupSet dataGroups)
421 {
422 this->LastResult.Clear();
423 if (!this->IsDataGroupSetSupported(dataGroups)) {
424 this->LastResult.SetError(ResultErrorUnsupportedDataGroup, 0);
425 return false;
426 }
427 cmsys::ifstream f(file.c_str());
428 if (!f) {
429 this->LastResult.SetError(ResultErrorOpeningInput, 0);
430 return false;
431 }
432 State state(dataGroups);
433 return this->ParseImpl(f, output, state);
434 }
435
GetParseResult() const436 cmVisualStudioSlnParser::ParseResult cmVisualStudioSlnParser::GetParseResult()
437 const
438 {
439 return this->LastResult.Result;
440 }
441
GetParseResultLine() const442 size_t cmVisualStudioSlnParser::GetParseResultLine() const
443 {
444 return this->LastResult.ResultLine;
445 }
446
GetParseHadBOM() const447 bool cmVisualStudioSlnParser::GetParseHadBOM() const
448 {
449 return this->LastResult.HadBOM;
450 }
451
IsDataGroupSetSupported(DataGroupSet dataGroups) const452 bool cmVisualStudioSlnParser::IsDataGroupSetSupported(
453 DataGroupSet dataGroups) const
454 {
455 return (dataGroups & DataGroupProjects) == dataGroups;
456 // only supporting DataGroupProjects for now
457 }
458
ParseImpl(std::istream & input,cmSlnData & output,State & state)459 bool cmVisualStudioSlnParser::ParseImpl(std::istream& input, cmSlnData& output,
460 State& state)
461 {
462 std::string line;
463 // Does the .sln start with a Byte Order Mark?
464 if (!this->ParseBOM(input, line, state))
465 return false;
466 do {
467 line = cmTrimWhitespace(line);
468 if (line.empty())
469 continue;
470 ParsedLine parsedLine;
471 switch (state.NextLineFormat()) {
472 case LineMultiValueTag:
473 if (!this->ParseMultiValueTag(line, parsedLine, state))
474 return false;
475 break;
476 case LineSingleValueTag:
477 if (!this->ParseSingleValueTag(line, parsedLine, state))
478 return false;
479 break;
480 case LineKeyValuePair:
481 if (!this->ParseKeyValuePair(line, parsedLine, state))
482 return false;
483 break;
484 case LineVerbatim:
485 parsedLine.CopyVerbatim(line);
486 break;
487 }
488 if (parsedLine.IsComment())
489 continue;
490 if (!state.Process(parsedLine, output, this->LastResult))
491 return false;
492 } while (state.ReadLine(input, line));
493 return state.Finished(this->LastResult);
494 }
495
ParseBOM(std::istream & input,std::string & line,State & state)496 bool cmVisualStudioSlnParser::ParseBOM(std::istream& input, std::string& line,
497 State& state)
498 {
499 char bom[4];
500 if (!input.get(bom, 4)) {
501 this->LastResult.SetError(ResultErrorReadingInput, 1);
502 return false;
503 }
504 this->LastResult.HadBOM =
505 (bom[0] == char(0xEF) && bom[1] == char(0xBB) && bom[2] == char(0xBF));
506 if (!state.ReadLine(input, line)) {
507 this->LastResult.SetError(ResultErrorReadingInput, 1);
508 return false;
509 }
510 if (!this->LastResult.HadBOM)
511 line = bom + line; // it wasn't a BOM, prepend it to first line
512 return true;
513 }
514
ParseMultiValueTag(const std::string & line,ParsedLine & parsedLine,State & state)515 bool cmVisualStudioSlnParser::ParseMultiValueTag(const std::string& line,
516 ParsedLine& parsedLine,
517 State& state)
518 {
519 size_t idxEqualSign = line.find('=');
520 auto fullTag = cm::string_view(line).substr(0, idxEqualSign);
521 if (!this->ParseTag(fullTag, parsedLine, state))
522 return false;
523 if (idxEqualSign != line.npos) {
524 size_t idxFieldStart = idxEqualSign + 1;
525 if (idxFieldStart < line.size()) {
526 size_t idxParsing = idxFieldStart;
527 bool inQuotes = false;
528 for (;;) {
529 idxParsing = line.find_first_of(",\"", idxParsing);
530 bool fieldOver = false;
531 if (idxParsing == line.npos) {
532 fieldOver = true;
533 if (inQuotes) {
534 this->LastResult.SetError(ResultErrorInputStructure,
535 state.GetCurrentLine());
536 return false;
537 }
538 } else if (line[idxParsing] == ',' && !inQuotes)
539 fieldOver = true;
540 else if (line[idxParsing] == '"')
541 inQuotes = !inQuotes;
542 if (fieldOver) {
543 if (!this->ParseValue(
544 line.substr(idxFieldStart, idxParsing - idxFieldStart),
545 parsedLine))
546 return false;
547 if (idxParsing == line.npos)
548 break; // end of last field
549 idxFieldStart = idxParsing + 1;
550 }
551 ++idxParsing;
552 }
553 }
554 }
555 return true;
556 }
557
ParseSingleValueTag(const std::string & line,ParsedLine & parsedLine,State & state)558 bool cmVisualStudioSlnParser::ParseSingleValueTag(const std::string& line,
559 ParsedLine& parsedLine,
560 State& state)
561 {
562 size_t idxEqualSign = line.find('=');
563 auto fullTag = cm::string_view(line).substr(0, idxEqualSign);
564 if (!this->ParseTag(fullTag, parsedLine, state))
565 return false;
566 if (idxEqualSign != line.npos) {
567 if (!this->ParseValue(line.substr(idxEqualSign + 1), parsedLine))
568 return false;
569 }
570 return true;
571 }
572
ParseKeyValuePair(const std::string & line,ParsedLine & parsedLine,State &)573 bool cmVisualStudioSlnParser::ParseKeyValuePair(const std::string& line,
574 ParsedLine& parsedLine,
575 State& /*state*/)
576 {
577 size_t idxEqualSign = line.find('=');
578 if (idxEqualSign == line.npos) {
579 parsedLine.CopyVerbatim(line);
580 return true;
581 }
582 const std::string& key = line.substr(0, idxEqualSign);
583 parsedLine.SetTag(cmTrimWhitespace(key));
584 const std::string& value = line.substr(idxEqualSign + 1);
585 parsedLine.AddValue(cmTrimWhitespace(value));
586 return true;
587 }
588
ParseTag(cm::string_view fullTag,ParsedLine & parsedLine,State & state)589 bool cmVisualStudioSlnParser::ParseTag(cm::string_view fullTag,
590 ParsedLine& parsedLine, State& state)
591 {
592 size_t idxLeftParen = fullTag.find('(');
593 if (idxLeftParen == cm::string_view::npos) {
594 parsedLine.SetTag(cmTrimWhitespace(fullTag));
595 return true;
596 }
597 parsedLine.SetTag(cmTrimWhitespace(fullTag.substr(0, idxLeftParen)));
598 size_t idxRightParen = fullTag.rfind(')');
599 if (idxRightParen == cm::string_view::npos) {
600 this->LastResult.SetError(ResultErrorInputStructure,
601 state.GetCurrentLine());
602 return false;
603 }
604 const std::string& arg = cmTrimWhitespace(
605 fullTag.substr(idxLeftParen + 1, idxRightParen - idxLeftParen - 1));
606 if (arg.front() == '"') {
607 if (arg.back() != '"') {
608 this->LastResult.SetError(ResultErrorInputStructure,
609 state.GetCurrentLine());
610 return false;
611 }
612 parsedLine.SetQuotedArg(arg.substr(1, arg.size() - 2));
613 } else
614 parsedLine.SetArg(arg);
615 return true;
616 }
617
ParseValue(const std::string & value,ParsedLine & parsedLine)618 bool cmVisualStudioSlnParser::ParseValue(const std::string& value,
619 ParsedLine& parsedLine)
620 {
621 const std::string& trimmed = cmTrimWhitespace(value);
622 if (trimmed.empty())
623 parsedLine.AddValue(trimmed);
624 else if (trimmed.front() == '"' && trimmed.back() == '"')
625 parsedLine.AddQuotedValue(trimmed.substr(1, trimmed.size() - 2));
626 else
627 parsedLine.AddValue(trimmed);
628 return true;
629 }
630