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