1 /*
2  * ConfigProfile.cpp
3  *
4  * Copyright (C) 2021 by RStudio, PBC
5  *
6  * Unless you have received this program directly from RStudio pursuant
7  * to the terms of a commercial license agreement with RStudio, then
8  * this program is licensed to you under the terms of version 3 of the
9  * GNU Affero General Public License. This program is distributed WITHOUT
10  * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11  * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12  * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13  *
14  */
15 
16 #include <boost/algorithm/string.hpp>
17 #include <boost/property_tree/ini_parser.hpp>
18 #include <boost/property_tree/ptree.hpp>
19 
20 #include <core/FileSerializer.hpp>
21 
22 #include <core/ConfigProfile.hpp>
23 
24 namespace rstudio {
25 namespace core {
26 
27 using namespace boost::property_tree;
28 
validateParam(const std::string & paramName,const std::string & paramValue) const29 Error ConfigProfile::validateParam(const std::string& paramName,
30                                    const std::string& paramValue) const
31 {
32    ValidatorMap::const_iterator iter = validators_.find(paramName);
33 
34    if (iter == validators_.end())
35       return Success();
36 
37    const ValidatorFunc& validator = iter->second;
38 
39    std::string errorMessage;
40    if (!validator(paramValue, &errorMessage))
41    {
42       return systemError(boost::system::errc::protocol_error,
43                          "Param " + paramName + " is not valid: " + errorMessage,
44                          ERROR_LOCATION);
45    }
46 
47    return Success();
48 }
49 
load(const FilePath & filePath)50 Error ConfigProfile::load(const FilePath& filePath)
51 {
52    std::string contents;
53 
54    Error error = readStringFromFile(filePath, &contents);
55    if (error)
56       return error;
57 
58    error = parseString(contents);
59    if (error)
60    {
61       std::string description = error.getProperty("description");
62       description += " in file " + filePath.getAbsolutePath();
63       error.addOrUpdateProperty("description", description);
64    }
65 
66    return error;
67 }
68 
parseString(const std::string & profileStr)69 Error ConfigProfile::parseString(const std::string& profileStr)
70 {
71    std::istringstream stream{profileStr};
72 
73    // parse the profile (ini syntax)
74    ptree profileTree;
75    try
76    {
77       ini_parser::read_ini(stream, profileTree);
78    }
79    catch (const ini_parser_error& error)
80    {
81       return systemError(boost::system::errc::protocol_error,
82                          "Could not parse config profile: " + error.message(),
83                          ERROR_LOCATION);
84    }
85 
86    // build section overrides
87    std::vector<LevelValues> levels;
88    for (const ptree::value_type& child : profileTree)
89    {
90       boost::optional<Level> matchingLevel;
91       for (Level level : sections_)
92       {
93          // if the section level starts with the defined section name
94          // from a prior call to addSections, we have a matching section level
95          if (boost::algorithm::starts_with(child.first, level.second))
96          {
97             matchingLevel = level;
98             break;
99          }
100       }
101 
102       if (!matchingLevel)
103       {
104          // no matching section level - it was not specified and is thus
105          // an erroneous section
106          return systemError(boost::system::errc::protocol_error,
107                             "Invalid config section " + child.first,
108                             ERROR_LOCATION);
109       }
110 
111       std::map<std::string, std::string> values;
112       for (const ptree::value_type& val : child.second)
113       {
114          // check to see if the parameter within the section has been registered
115          const std::string& paramName = val.first;
116 
117          DefaultParamValuesMap::const_iterator defaultIter = defaultValues_.find(paramName);
118          if (defaultIter == defaultValues_.end())
119          {
120             // we require every param to have a default value / be registered so error out here
121             return systemError(boost::system::errc::protocol_error,
122                                "Unknown param " + paramName + " specified",
123                                ERROR_LOCATION);
124          }
125 
126          std::string paramValue = val.second.get_value<std::string>();
127 
128          Error error = validateParam(paramName, paramValue);
129          if (error)
130             return error;
131 
132          values[paramName] = paramValue;
133       }
134 
135       std::string levelValue = child.first.substr(matchingLevel.get().second.size());
136       levels.push_back({{matchingLevel.get().first, levelValue}, values});
137    }
138 
139    // stable sort the levels in ascending level number
140    // this will preserve the order of same-level section overrides
141    std::stable_sort(
142             levels.begin(),
143             levels.end(),
144             [](const LevelValues& a, const LevelValues& b) { return a.first.first < b.first.first; });
145 
146    // assign the temporary variable levels to the class member
147    // this assures that we can safely call this method multiple times, preserving the last
148    // good configuration in case we error out above due to invalid configuration
149    levels_ = levels;
150 
151    return Success();
152 }
153 
getLevelNames(uint32_t level) const154 std::vector<std::string> ConfigProfile::getLevelNames(uint32_t level) const
155 {
156    std::vector<std::string> levelNames;
157 
158    for (const LevelValues& value : levels_)
159    {
160       if (value.first.first == level)
161          levelNames.push_back(value.first.second);
162    }
163 
164    return levelNames;
165 }
166 
167 } // core
168 } // namespace rstudio
169