Allow '.' in setting names (for "waits-for.d").
[oweals/dinit.git] / src / includes / load-service.h
1 #include <iostream>
2 #include <list>
3
4 // Exception while loading a service
5 class service_load_exc
6 {
7     public:
8     std::string serviceName;
9     std::string excDescription;
10
11     protected:
12     service_load_exc(const std::string &serviceName, std::string &&desc) noexcept
13         : serviceName(serviceName), excDescription(std::move(desc))
14     {
15     }
16 };
17
18 class service_not_found : public service_load_exc
19 {
20     public:
21     service_not_found(const std::string &serviceName) noexcept
22         : service_load_exc(serviceName, "Service description not found.")
23     {
24     }
25 };
26
27 class service_cyclic_dependency : public service_load_exc
28 {
29     public:
30     service_cyclic_dependency(const std::string &serviceName) noexcept
31         : service_load_exc(serviceName, "Has cyclic dependency.")
32     {
33     }
34 };
35
36 class service_description_exc : public service_load_exc
37 {
38     public:
39     service_description_exc(const std::string &serviceName, std::string &&extraInfo) noexcept
40         : service_load_exc(serviceName, std::move(extraInfo))
41     {
42     }
43 };
44
45 namespace dinit_load {
46
47 using string = std::string;
48 using string_iterator = std::string::iterator;
49
50 // exception thrown when encountering a syntax issue when reading a setting value
51 class setting_exception
52 {
53     std::string info;
54
55     public:
56     setting_exception(const std::string &&exc_info) : info(std::move(exc_info))
57     {
58     }
59
60     std::string &get_info()
61     {
62         return info;
63     }
64 };
65
66
67 // Utility function to skip white space. Returns an iterator at the
68 // first non-white-space position (or at end).
69 inline string_iterator skipws(string_iterator i, string_iterator end)
70 {
71     using std::locale;
72     using std::isspace;
73
74     while (i != end) {
75       if (! isspace(*i, locale::classic())) {
76         break;
77       }
78       ++i;
79     }
80     return i;
81 }
82
83 // Read a setting name.
84 inline string read_setting_name(string_iterator & i, string_iterator end)
85 {
86     using std::locale;
87     using std::ctype;
88     using std::use_facet;
89
90     const ctype<char> & facet = use_facet<ctype<char> >(locale::classic());
91
92     string rval;
93     // Allow alphabetical characters, and dash (-) in setting name
94     while (i != end && (*i == '-' || *i == '.' || facet.is(ctype<char>::alpha, *i))) {
95         rval += *i;
96         ++i;
97     }
98     return rval;
99 }
100
101 // Read a setting value
102 //
103 // In general a setting value is a single-line string. It may contain multiple parts
104 // separated by white space (which is normally collapsed). A hash mark - # - denotes
105 // the end of the value and the beginning of a comment (it should be preceded by
106 // whitespace).
107 //
108 // Part of a value may be quoted using double quote marks, which prevents collapse
109 // of whitespace and interpretation of most special characters (the quote marks will
110 // not be considered part of the value). A backslash can precede a character (such
111 // as '#' or '"' or another backslash) to remove its special meaning. Newline
112 // characters are not allowed in values and cannot be quoted.
113 //
114 // This function expects the string to be in an ASCII-compatible, single byte
115 // encoding (the "classic" locale).
116 //
117 // Params:
118 //    service_name - the name of the service to which the setting applies
119 //    i  -  reference to string iterator through the line
120 //    end -   iterator at end of line
121 //    part_positions -  list of <int,int> to which the position of each setting value
122 //                      part will be added as [start,end). May be null.
123 inline string read_setting_value(string_iterator & i, string_iterator end,
124         std::list<std::pair<unsigned,unsigned>> * part_positions = nullptr)
125 {
126     using std::locale;
127     using std::isspace;
128
129     i = skipws(i, end);
130
131     string rval;
132     bool new_part = true;
133     int part_start;
134
135     while (i != end) {
136         char c = *i;
137         if (c == '\"') {
138             if (new_part) {
139                 part_start = rval.length();
140                 new_part = false;
141             }
142             // quoted string
143             ++i;
144             while (i != end) {
145                 c = *i;
146                 if (c == '\"') break;
147                 if (c == '\n') {
148                     throw setting_exception("Line end inside quoted string");
149                 }
150                 else if (c == '\\') {
151                     // A backslash escapes the following character.
152                     ++i;
153                     if (i != end) {
154                         c = *i;
155                         if (c == '\n') {
156                             throw setting_exception("Line end follows backslash escape character (`\\')");
157                         }
158                         rval += c;
159                     }
160                 }
161                 else {
162                     rval += c;
163                 }
164                 ++i;
165             }
166             if (i == end) {
167                 // String wasn't terminated
168                 throw setting_exception("Unterminated quoted string");
169             }
170         }
171         else if (c == '\\') {
172             if (new_part) {
173                 part_start = rval.length();
174                 new_part = false;
175             }
176             // A backslash escapes the next character
177             ++i;
178             if (i != end) {
179                 rval += *i;
180             }
181             else {
182                 throw setting_exception("Backslash escape (`\\') not followed by character");
183             }
184         }
185         else if (isspace(c, locale::classic())) {
186             if (! new_part && part_positions != nullptr) {
187                 part_positions->emplace_back(part_start, rval.length());
188                 new_part = true;
189             }
190             i = skipws(i, end);
191             if (i == end) break;
192             if (*i == '#') break; // comment
193             rval += ' ';  // collapse ws to a single space
194             continue;
195         }
196         else if (c == '#') {
197             // Possibly intended a comment; we require leading whitespace to reduce occurrence of accidental
198             // comments in setting values.
199             throw setting_exception("hashmark (`#') comment must be separated from setting value by whitespace");
200         }
201         else {
202             if (new_part) {
203                 part_start = rval.length();
204                 new_part = false;
205             }
206             rval += c;
207         }
208         ++i;
209     }
210
211     // Got to end:
212     if (part_positions != nullptr) {
213         part_positions->emplace_back(part_start, rval.length());
214     }
215
216     return rval;
217 }
218
219 // Process an opened service file, line by line.
220 //    name - the service name
221 //    service_file - the service file input stream
222 //    func - a function of the form:
223 //             void(string &line, string &setting, string_iterator i, string_iterator end)
224 //           Called with:
225 //               line - the complete line
226 //               setting - the setting name, from the beginning of the line
227 //               i - iterator at the beginning of the setting value
228 //               end - iterator marking the end of the line
229 //
230 // May throw service load exceptions or I/O exceptions if enabled on stream.
231 template <typename T>
232 void process_service_file(string name, std::istream &service_file, T func)
233 {
234     string line;
235
236     while (getline(service_file, line)) {
237         string::iterator i = line.begin();
238         string::iterator end = line.end();
239
240         i = skipws(i, end);
241         if (i != end) {
242             if (*i == '#') {
243                 continue;  // comment line
244             }
245             string setting = read_setting_name(i, end);
246             i = skipws(i, end);
247             if (i == end || (*i != '=' && *i != ':')) {
248                 throw service_description_exc(name, "Badly formed line.");
249             }
250             i = skipws(++i, end);
251
252             func(line, setting, i, end);
253         }
254     }
255 }
256
257 } // namespace dinit_load
258
259 using dinit_load::process_service_file;