/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying file Copyright.txt or https://cmake.org/licensing for details. */ #include "cmVisualStudioSlnParser.h" #include "cmStringAlgorithms.h" #include "cmSystemTools.h" #include "cmVisualStudioSlnData.h" #include "cmsys/FStream.hxx" #include #include namespace { enum LineFormat { LineMultiValueTag, LineSingleValueTag, LineKeyValuePair, LineVerbatim }; } class cmVisualStudioSlnParser::ParsedLine { public: bool IsComment() const; bool IsKeyValuePair() const; const std::string& GetTag() const { return this->Tag; } const std::string& GetArg() const { return this->Arg.first; } std::string GetArgVerbatim() const; size_t GetValueCount() const { return this->Values.size(); } const std::string& GetValue(size_t idxValue) const; std::string GetValueVerbatim(size_t idxValue) const; void SetTag(const std::string& tag) { this->Tag = tag; } void SetArg(const std::string& arg) { this->Arg = StringData(arg, false); } void SetQuotedArg(const std::string& arg) { this->Arg = StringData(arg, true); } void AddValue(const std::string& value) { this->Values.push_back(StringData(value, false)); } void AddQuotedValue(const std::string& value) { this->Values.push_back(StringData(value, true)); } void CopyVerbatim(const std::string& line) { this->Tag = line; } private: using StringData = std::pair; std::string Tag; StringData Arg; std::vector Values; static const std::string BadString; static const std::string Quote; }; const std::string cmVisualStudioSlnParser::ParsedLine::BadString; const std::string cmVisualStudioSlnParser::ParsedLine::Quote("\""); bool cmVisualStudioSlnParser::ParsedLine::IsComment() const { assert(!this->Tag.empty()); return (this->Tag[0] == '#'); } bool cmVisualStudioSlnParser::ParsedLine::IsKeyValuePair() const { assert(!this->Tag.empty()); return this->Arg.first.empty() && this->Values.size() == 1; } std::string cmVisualStudioSlnParser::ParsedLine::GetArgVerbatim() const { if (this->Arg.second) return Quote + this->Arg.first + Quote; else return this->Arg.first; } const std::string& cmVisualStudioSlnParser::ParsedLine::GetValue( size_t idxValue) const { if (idxValue < this->Values.size()) return this->Values[idxValue].first; else return BadString; } std::string cmVisualStudioSlnParser::ParsedLine::GetValueVerbatim( size_t idxValue) const { if (idxValue < this->Values.size()) { const StringData& data = this->Values[idxValue]; if (data.second) return Quote + data.first + Quote; else return data.first; } else return BadString; } class cmVisualStudioSlnParser::State { public: explicit State(DataGroupSet requestedData); size_t GetCurrentLine() const { return this->CurrentLine; } bool ReadLine(std::istream& input, std::string& line); LineFormat NextLineFormat() const; bool Process(const cmVisualStudioSlnParser::ParsedLine& line, cmSlnData& output, cmVisualStudioSlnParser::ResultData& result); bool Finished(cmVisualStudioSlnParser::ResultData& result); private: enum FileState { FileStateStart, FileStateTopLevel, FileStateProject, FileStateProjectDependencies, FileStateGlobal, FileStateSolutionConfigurations, FileStateProjectConfigurations, FileStateSolutionFilters, FileStateGlobalSection, FileStateIgnore }; std::stack Stack; std::string EndIgnoreTag; DataGroupSet RequestedData; size_t CurrentLine; void IgnoreUntilTag(const std::string& endTag); }; cmVisualStudioSlnParser::State::State(DataGroupSet requestedData) : RequestedData(requestedData) , CurrentLine(0) { if (this->RequestedData.test(DataGroupProjectDependenciesBit)) this->RequestedData.set(DataGroupProjectsBit); this->Stack.push(FileStateStart); } bool cmVisualStudioSlnParser::State::ReadLine(std::istream& input, std::string& line) { ++this->CurrentLine; return !std::getline(input, line).fail(); } LineFormat cmVisualStudioSlnParser::State::NextLineFormat() const { switch (this->Stack.top()) { case FileStateStart: return LineVerbatim; case FileStateTopLevel: return LineMultiValueTag; case FileStateProject: return LineSingleValueTag; case FileStateProjectDependencies: return LineKeyValuePair; case FileStateGlobal: return LineSingleValueTag; case FileStateSolutionConfigurations: return LineKeyValuePair; case FileStateProjectConfigurations: return LineKeyValuePair; case FileStateSolutionFilters: return LineKeyValuePair; case FileStateGlobalSection: return LineKeyValuePair; case FileStateIgnore: return LineVerbatim; default: assert(false); return LineVerbatim; } } bool cmVisualStudioSlnParser::State::Process( const cmVisualStudioSlnParser::ParsedLine& line, cmSlnData& output, cmVisualStudioSlnParser::ResultData& result) { assert(!line.IsComment()); switch (this->Stack.top()) { case FileStateStart: if (!cmHasLiteralPrefix(line.GetTag(), "Microsoft Visual Studio Solution File")) { result.SetError(ResultErrorInputStructure, this->GetCurrentLine()); return false; } this->Stack.pop(); this->Stack.push(FileStateTopLevel); break; case FileStateTopLevel: if (line.GetTag().compare("Project") == 0) { if (line.GetValueCount() != 3) { result.SetError(ResultErrorInputStructure, this->GetCurrentLine()); return false; } if (this->RequestedData.test(DataGroupProjectsBit)) { if (!output.AddProject(line.GetValue(2), line.GetValue(0), line.GetValue(1))) { result.SetError(ResultErrorInputData, this->GetCurrentLine()); return false; } this->Stack.push(FileStateProject); } else this->IgnoreUntilTag("EndProject"); } else if (line.GetTag().compare("Global") == 0) this->Stack.push(FileStateGlobal); else { result.SetError(ResultErrorInputStructure, this->GetCurrentLine()); return false; } break; case FileStateProject: if (line.GetTag().compare("EndProject") == 0) this->Stack.pop(); else if (line.GetTag().compare("ProjectSection") == 0) { if (line.GetArg().compare("ProjectDependencies") == 0 && line.GetValue(0).compare("postProject") == 0) { if (this->RequestedData.test(DataGroupProjectDependenciesBit)) this->Stack.push(FileStateProjectDependencies); else this->IgnoreUntilTag("EndProjectSection"); } else this->IgnoreUntilTag("EndProjectSection"); } else { result.SetError(ResultErrorInputStructure, this->GetCurrentLine()); return false; } break; case FileStateProjectDependencies: if (line.GetTag().compare("EndProjectSection") == 0) this->Stack.pop(); else if (line.IsKeyValuePair()) // implement dependency storing here, once needed ; else { result.SetError(ResultErrorInputStructure, this->GetCurrentLine()); return false; } break; case FileStateGlobal: if (line.GetTag().compare("EndGlobal") == 0) this->Stack.pop(); else if (line.GetTag().compare("GlobalSection") == 0) { if (line.GetArg().compare("SolutionConfigurationPlatforms") == 0 && line.GetValue(0).compare("preSolution") == 0) { if (this->RequestedData.test(DataGroupSolutionConfigurationsBit)) this->Stack.push(FileStateSolutionConfigurations); else this->IgnoreUntilTag("EndGlobalSection"); } else if (line.GetArg().compare("ProjectConfigurationPlatforms") == 0 && line.GetValue(0).compare("postSolution") == 0) { if (this->RequestedData.test(DataGroupProjectConfigurationsBit)) this->Stack.push(FileStateProjectConfigurations); else this->IgnoreUntilTag("EndGlobalSection"); } else if (line.GetArg().compare("NestedProjects") == 0 && line.GetValue(0).compare("preSolution") == 0) { if (this->RequestedData.test(DataGroupSolutionFiltersBit)) this->Stack.push(FileStateSolutionFilters); else this->IgnoreUntilTag("EndGlobalSection"); } else if (this->RequestedData.test(DataGroupGenericGlobalSectionsBit)) this->Stack.push(FileStateGlobalSection); else this->IgnoreUntilTag("EndGlobalSection"); } else { result.SetError(ResultErrorInputStructure, this->GetCurrentLine()); return false; } break; case FileStateSolutionConfigurations: if (line.GetTag().compare("EndGlobalSection") == 0) this->Stack.pop(); else if (line.IsKeyValuePair()) // implement configuration storing here, once needed ; else { result.SetError(ResultErrorInputStructure, this->GetCurrentLine()); return false; } break; case FileStateProjectConfigurations: if (line.GetTag().compare("EndGlobalSection") == 0) this->Stack.pop(); else if (line.IsKeyValuePair()) // implement configuration storing here, once needed ; else { result.SetError(ResultErrorInputStructure, this->GetCurrentLine()); return false; } break; case FileStateSolutionFilters: if (line.GetTag().compare("EndGlobalSection") == 0) this->Stack.pop(); else if (line.IsKeyValuePair()) // implement filter storing here, once needed ; else { result.SetError(ResultErrorInputStructure, this->GetCurrentLine()); return false; } break; case FileStateGlobalSection: if (line.GetTag().compare("EndGlobalSection") == 0) this->Stack.pop(); else if (line.IsKeyValuePair()) // implement section storing here, once needed ; else { result.SetError(ResultErrorInputStructure, this->GetCurrentLine()); return false; } break; case FileStateIgnore: if (line.GetTag() == this->EndIgnoreTag) { this->Stack.pop(); this->EndIgnoreTag.clear(); } break; default: result.SetError(ResultErrorBadInternalState, this->GetCurrentLine()); return false; } return true; } bool cmVisualStudioSlnParser::State::Finished( cmVisualStudioSlnParser::ResultData& result) { if (this->Stack.top() != FileStateTopLevel) { result.SetError(ResultErrorInputStructure, this->GetCurrentLine()); return false; } result.Result = ResultOK; return true; } void cmVisualStudioSlnParser::State::IgnoreUntilTag(const std::string& endTag) { this->Stack.push(FileStateIgnore); this->EndIgnoreTag = endTag; } cmVisualStudioSlnParser::ResultData::ResultData() : Result(ResultOK) , ResultLine(0) { } void cmVisualStudioSlnParser::ResultData::Clear() { *this = ResultData(); } void cmVisualStudioSlnParser::ResultData::SetError(ParseResult error, size_t line) { this->Result = error; this->ResultLine = line; } const cmVisualStudioSlnParser::DataGroupSet cmVisualStudioSlnParser::DataGroupProjects( 1 << cmVisualStudioSlnParser::DataGroupProjectsBit); const cmVisualStudioSlnParser::DataGroupSet cmVisualStudioSlnParser::DataGroupProjectDependencies( 1 << cmVisualStudioSlnParser::DataGroupProjectDependenciesBit); const cmVisualStudioSlnParser::DataGroupSet cmVisualStudioSlnParser::DataGroupSolutionConfigurations( 1 << cmVisualStudioSlnParser::DataGroupSolutionConfigurationsBit); const cmVisualStudioSlnParser::DataGroupSet cmVisualStudioSlnParser::DataGroupProjectConfigurations( 1 << cmVisualStudioSlnParser::DataGroupProjectConfigurationsBit); const cmVisualStudioSlnParser::DataGroupSet cmVisualStudioSlnParser::DataGroupSolutionFilters( 1 << cmVisualStudioSlnParser::DataGroupSolutionFiltersBit); const cmVisualStudioSlnParser::DataGroupSet cmVisualStudioSlnParser::DataGroupGenericGlobalSections( 1 << cmVisualStudioSlnParser::DataGroupGenericGlobalSectionsBit); const cmVisualStudioSlnParser::DataGroupSet cmVisualStudioSlnParser::DataGroupAll(~0); bool cmVisualStudioSlnParser::Parse(std::istream& input, cmSlnData& output, DataGroupSet dataGroups) { this->LastResult.Clear(); if (!this->IsDataGroupSetSupported(dataGroups)) { this->LastResult.SetError(ResultErrorUnsupportedDataGroup, 0); return false; } State state(dataGroups); return this->ParseImpl(input, output, state); } bool cmVisualStudioSlnParser::ParseFile(const std::string& file, cmSlnData& output, DataGroupSet dataGroups) { this->LastResult.Clear(); if (!this->IsDataGroupSetSupported(dataGroups)) { this->LastResult.SetError(ResultErrorUnsupportedDataGroup, 0); return false; } cmsys::ifstream f(file.c_str()); if (!f) { this->LastResult.SetError(ResultErrorOpeningInput, 0); return false; } State state(dataGroups); return this->ParseImpl(f, output, state); } cmVisualStudioSlnParser::ParseResult cmVisualStudioSlnParser::GetParseResult() const { return this->LastResult.Result; } size_t cmVisualStudioSlnParser::GetParseResultLine() const { return this->LastResult.ResultLine; } bool cmVisualStudioSlnParser::GetParseHadBOM() const { return this->LastResult.HadBOM; } bool cmVisualStudioSlnParser::IsDataGroupSetSupported( DataGroupSet dataGroups) const { return (dataGroups & DataGroupProjects) == dataGroups; // only supporting DataGroupProjects for now } bool cmVisualStudioSlnParser::ParseImpl(std::istream& input, cmSlnData& output, State& state) { std::string line; // Does the .sln start with a Byte Order Mark? if (!this->ParseBOM(input, line, state)) return false; do { line = cmTrimWhitespace(line); if (line.empty()) continue; ParsedLine parsedLine; switch (state.NextLineFormat()) { case LineMultiValueTag: if (!this->ParseMultiValueTag(line, parsedLine, state)) return false; break; case LineSingleValueTag: if (!this->ParseSingleValueTag(line, parsedLine, state)) return false; break; case LineKeyValuePair: if (!this->ParseKeyValuePair(line, parsedLine, state)) return false; break; case LineVerbatim: parsedLine.CopyVerbatim(line); break; } if (parsedLine.IsComment()) continue; if (!state.Process(parsedLine, output, this->LastResult)) return false; } while (state.ReadLine(input, line)); return state.Finished(this->LastResult); } bool cmVisualStudioSlnParser::ParseBOM(std::istream& input, std::string& line, State& state) { char bom[4]; if (!input.get(bom, 4)) { this->LastResult.SetError(ResultErrorReadingInput, 1); return false; } this->LastResult.HadBOM = (bom[0] == char(0xEF) && bom[1] == char(0xBB) && bom[2] == char(0xBF)); if (!state.ReadLine(input, line)) { this->LastResult.SetError(ResultErrorReadingInput, 1); return false; } if (!this->LastResult.HadBOM) line = bom + line; // it wasn't a BOM, prepend it to first line return true; } bool cmVisualStudioSlnParser::ParseMultiValueTag(const std::string& line, ParsedLine& parsedLine, State& state) { size_t idxEqualSign = line.find('='); const std::string& fullTag = line.substr(0, idxEqualSign); if (!this->ParseTag(fullTag, parsedLine, state)) return false; if (idxEqualSign != line.npos) { size_t idxFieldStart = idxEqualSign + 1; if (idxFieldStart < line.size()) { size_t idxParsing = idxFieldStart; bool inQuotes = false; for (;;) { idxParsing = line.find_first_of(",\"", idxParsing); bool fieldOver = false; if (idxParsing == line.npos) { fieldOver = true; if (inQuotes) { this->LastResult.SetError(ResultErrorInputStructure, state.GetCurrentLine()); return false; } } else if (line[idxParsing] == ',' && !inQuotes) fieldOver = true; else if (line[idxParsing] == '"') inQuotes = !inQuotes; if (fieldOver) { if (!this->ParseValue( line.substr(idxFieldStart, idxParsing - idxFieldStart), parsedLine)) return false; if (idxParsing == line.npos) break; // end of last field idxFieldStart = idxParsing + 1; } ++idxParsing; } } } return true; } bool cmVisualStudioSlnParser::ParseSingleValueTag(const std::string& line, ParsedLine& parsedLine, State& state) { size_t idxEqualSign = line.find('='); const std::string& fullTag = line.substr(0, idxEqualSign); if (!this->ParseTag(fullTag, parsedLine, state)) return false; if (idxEqualSign != line.npos) { if (!this->ParseValue(line.substr(idxEqualSign + 1), parsedLine)) return false; } return true; } bool cmVisualStudioSlnParser::ParseKeyValuePair(const std::string& line, ParsedLine& parsedLine, State& /*state*/) { size_t idxEqualSign = line.find('='); if (idxEqualSign == line.npos) { parsedLine.CopyVerbatim(line); return true; } const std::string& key = line.substr(0, idxEqualSign); parsedLine.SetTag(cmTrimWhitespace(key)); const std::string& value = line.substr(idxEqualSign + 1); parsedLine.AddValue(cmTrimWhitespace(value)); return true; } bool cmVisualStudioSlnParser::ParseTag(const std::string& fullTag, ParsedLine& parsedLine, State& state) { size_t idxLeftParen = fullTag.find('('); if (idxLeftParen == fullTag.npos) { parsedLine.SetTag(cmTrimWhitespace(fullTag)); return true; } parsedLine.SetTag(cmTrimWhitespace(fullTag.substr(0, idxLeftParen))); size_t idxRightParen = fullTag.rfind(')'); if (idxRightParen == fullTag.npos) { this->LastResult.SetError(ResultErrorInputStructure, state.GetCurrentLine()); return false; } const std::string& arg = cmTrimWhitespace( fullTag.substr(idxLeftParen + 1, idxRightParen - idxLeftParen - 1)); if (arg.front() == '"') { if (arg.back() != '"') { this->LastResult.SetError(ResultErrorInputStructure, state.GetCurrentLine()); return false; } parsedLine.SetQuotedArg(arg.substr(1, arg.size() - 2)); } else parsedLine.SetArg(arg); return true; } bool cmVisualStudioSlnParser::ParseValue(const std::string& value, ParsedLine& parsedLine) { const std::string& trimmed = cmTrimWhitespace(value); if (trimmed.empty()) parsedLine.AddValue(trimmed); else if (trimmed.front() == '"' && trimmed.back() == '"') parsedLine.AddQuotedValue(trimmed.substr(1, trimmed.size() - 2)); else parsedLine.AddValue(trimmed); return true; }