Line data Source code
1 : /**
2 : * @file
3 : *
4 : * @brief Write key sets using yaml-cpp
5 : *
6 : * @copyright BSD License (see LICENSE.md or https://www.libelektra.org)
7 : */
8 :
9 : #include "write.hpp"
10 : #include "log.hpp"
11 : #include "yaml-cpp/yaml.h"
12 :
13 : #include <kdbassert.h>
14 : #include <kdbease.h>
15 : #include <kdblogger.h>
16 : #include <kdbplugin.h>
17 :
18 : #include <fstream>
19 :
20 : using namespace std;
21 : using namespace kdb;
22 :
23 : namespace
24 : {
25 :
26 : using KeySetPair = pair<KeySet, KeySet>;
27 :
28 : /**
29 : * @brief This function checks if `element` is an array element of `parent`.
30 : *
31 : * @pre The key `child` must be below `parent`.
32 : *
33 : * @param parent This parameter specifies a parent key.
34 : * @param keys This variable stores a direct or indirect child of `parent`.
35 : *
36 : * @retval true If `element` is an array element
37 : * @retval false Otherwise
38 : */
39 593 : bool isArrayElementOf (Key const & parent, Key const & child)
40 : {
41 1186 : char const * relative = elektraKeyGetRelativeName (*child, *parent);
42 593 : auto offsetIndex = ckdb::elektraArrayValidateBaseNameString (relative);
43 593 : if (offsetIndex <= 0) return false;
44 : // Skip `#`, underscores and digits
45 588 : relative += 2 * offsetIndex;
46 : // The next character has to be the separation char (`/`) or end of string
47 588 : if (relative[0] != '\0' && relative[0] != '/') return false;
48 :
49 588 : return true;
50 : }
51 :
52 : /**
53 : * @brief This function determines if the given key is an array parent.
54 : *
55 : * @param parent This parameter specifies a possible array parent.
56 : * @param keys This variable stores the key set of `parent`.
57 : *
58 : * @retval true If `parent` is the parent key of an array
59 : * @retval false Otherwise
60 : */
61 111 : bool isArrayParent (Key const & parent, KeySet const & keys)
62 : {
63 2522 : for (auto const & key : keys)
64 : {
65 1097 : if (!key.isBelow (parent)) continue;
66 598 : if (!isArrayElementOf (parent, key)) return false;
67 : }
68 :
69 106 : return true;
70 : }
71 :
72 : /**
73 : * @brief This function returns all array parents for a given key set.
74 : *
75 : * @note This function also adds empty parent keys for arrays, if they did not exist beforehand. For example for the key set that **only**
76 : * contains the keys:
77 : *
78 : * - `user/array/#0`, and
79 : * - `user/array/#1`
80 : *
81 : * the function will add the array parent `user/array` to the returned key set.
82 : *
83 : * @param keys This parameter contains the key set this function searches for array parents.
84 : *
85 : * @return A key sets that contains all array parents stored in `keys`
86 : */
87 68 : KeySet splitArrayParents (KeySet const & keys)
88 : {
89 68 : KeySet arrayParents;
90 :
91 68 : keys.rewind ();
92 68 : Key previous;
93 1902 : for (; keys.next (); previous = keys.current ())
94 : {
95 1698 : if (keys.current ().hasMeta ("array"))
96 : {
97 3 : arrayParents.append (keys.current ());
98 1 : continue;
99 : }
100 :
101 1410 : if (keys.current ().getBaseName ()[0] == '#')
102 : {
103 333 : if (!keys.current ().isDirectBelow (previous))
104 : {
105 276 : Key directParent{ keys.current ().getName (), KEY_END };
106 69 : ckdb::keySetBaseName (*directParent, NULL);
107 207 : if (isArrayParent (*directParent, keys)) arrayParents.append (directParent);
108 : }
109 126 : else if (isArrayParent (*previous, keys))
110 : {
111 : arrayParents.append (previous);
112 : }
113 : }
114 : }
115 :
116 : #ifdef HAVE_LOGGER
117 : ELEKTRA_LOG_DEBUG ("Array parents:");
118 : logKeySet (arrayParents);
119 : #endif
120 :
121 68 : return arrayParents;
122 : }
123 :
124 : /**
125 : * @brief This function splits `keys` into two key sets, one for array parents and elements, and the other one for all other keys.
126 : *
127 : * @param arrayParents This key set contains a (copy) of all array parents of `keys`.
128 : * @param keys This parameter contains the key set this function splits.
129 : *
130 : * @return A pair of key sets, where the first key set contains all array parents and elements,
131 : * and the second key set contains all other keys
132 : */
133 68 : KeySetPair splitArrayOther (KeySet const & arrayParents, KeySet const & keys)
134 : {
135 204 : KeySet others = keys.dup ();
136 136 : KeySet arrays;
137 :
138 333 : for (auto const & parent : arrayParents)
139 : {
140 215 : arrays.append (others.cut (parent));
141 : }
142 :
143 136 : return make_pair (arrays, others);
144 : }
145 :
146 : /**
147 : * @brief This function removes all **non-essential** array metadata from a given key set.
148 : *
149 : * @param keys This parameter contains the key set this function modifies.
150 : *
151 : * @return A copy of `keys` that only contains array metadata for empty arrays
152 : */
153 68 : KeySet removeArrayMetaData (KeySet const & keys)
154 : {
155 68 : KeySet result;
156 966 : for (auto const & key : keys)
157 : {
158 1016 : result.append (key.dup ());
159 : }
160 :
161 136 : Key previous;
162 : result.rewind ();
163 1220 : while (result.next ())
164 : {
165 1222 : if (result.current ().isBelow (previous)) previous.delMeta ("array");
166 762 : previous = result.current ();
167 : }
168 :
169 136 : ELEKTRA_ASSERT (keys.size () == result.size (), "Size of input and output keys set is different (%zu ≠ %zu)", keys.size (),
170 68 : result.size ());
171 :
172 68 : return result;
173 : }
174 : /**
175 : * @brief This function determines all keys “missing” from the given keyset.
176 : *
177 : * The term “missing” refers to keys that are not part of the hierarchy. For example in a key set with the parent key
178 : *
179 : * - `user/parent`
180 : *
181 : * that contains the keys
182 : *
183 : * - `user/parent/level1/level2`, and
184 : * - `user/parent/level1/level2/level3/level4`
185 : *
186 : * , the keys
187 : *
188 : * - `user/parent/level1`, and
189 : * - user/parent/level1/level2/level3
190 : *
191 : * are missing.
192 : *
193 : * @param keys This parameter contains the key set for which this function determines missing keys.
194 : * @param parent This value stores the parent key of `keys`.
195 : *
196 : * @return A key set that contains all keys missing from `keys`
197 : */
198 68 : KeySet missingKeys (KeySet const & keys, Key const & parent)
199 : {
200 68 : KeySet missing;
201 :
202 68 : keys.rewind ();
203 204 : Key previous{ parent.getName (), KEY_BINARY, KEY_END };
204 1728 : for (; keys.next (); previous = keys.current ())
205 : {
206 1746 : if (keys.current ().isDirectBelow (previous) || !keys.current ().isBelow (previous)) continue;
207 :
208 76 : Key current{ keys.current ().getName (), KEY_BINARY, KEY_END };
209 48 : while (!current.isDirectBelow (previous))
210 : {
211 29 : ckdb::keySetBaseName (*current, NULL);
212 29 : missing.append (current);
213 29 : current = current.dup ();
214 : }
215 : }
216 :
217 68 : return missing;
218 : }
219 :
220 : /**
221 : * @brief This function returns a `NameIterator` starting at the first level that is not part of `parent`.
222 : *
223 : * @pre The parameter `key` must be a child of `parent`.
224 : *
225 : * @param key This is the key for which this function returns a relative iterator.
226 : * @param parent This key specifies the part of the name iterator that will not be part of the return value of this function.
227 : *
228 : * @returns A relative iterator that starts with the first part of the name of `key` not contained in `parent`.
229 : */
230 283 : NameIterator relativeKeyIterator (Key const & key, Key const & parent)
231 : {
232 283 : auto parentIterator = parent.begin ();
233 : auto keyIterator = key.begin ();
234 5961 : while (parentIterator != parent.end () && keyIterator != key.end ())
235 : {
236 1704 : parentIterator++;
237 1704 : keyIterator++;
238 : }
239 283 : return keyIterator;
240 : }
241 :
242 : /**
243 : * @brief This function checks if a key name specifies an array key.
244 : *
245 : * If the key name contains a valid array index that is smaller than `unsigned long long`, then the function will also return this index.
246 : *
247 : * @param nameIterator This iterator specifies the name of the key.
248 : *
249 : * @retval (true, arrayIndex) if `name` specifies an array key, where `arrayIndex` specifies the index stored in the array key.
250 : * @retval (false, 0) otherwise
251 : */
252 294 : std::pair<bool, unsigned long long> isArrayIndex (NameIterator const & nameIterator)
253 : {
254 588 : string const name = *nameIterator;
255 294 : auto const offsetIndex = ckdb::elektraArrayValidateBaseNameString (name.c_str ());
256 294 : auto const isArrayElement = offsetIndex >= 1;
257 1303 : return { isArrayElement, isArrayElement ? stoull (name.substr (offsetIndex)) : 0 };
258 : }
259 :
260 : /**
261 : * @brief This function creates a YAML node representing a key value.
262 : *
263 : * @param key This key specifies the data that should be saved in the YAML node returned by this function.
264 : *
265 : * @note Since YAML does not support non-empty binary data directly this function replaces data stored in binary keys with the string
266 : * `Unsupported binary value!`. If you need support for binary data, please load the Base64 plugin before you use YAML CPP.
267 : *
268 : * @returns A new YAML node containing the data specified in `key`
269 : */
270 283 : YAML::Node createMetaDataNode (Key const & key)
271 : {
272 1132 : if (key.hasMeta ("array"))
273 : {
274 1 : return YAML::Node (YAML::NodeType::Sequence);
275 : }
276 282 : if (key.getBinarySize () == 0)
277 : {
278 92 : return YAML::Node (YAML::NodeType::Null);
279 : }
280 190 : if (key.isBinary ())
281 : {
282 0 : return YAML::Node ("Unsupported binary value!");
283 : }
284 :
285 190 : auto value = key.get<string> ();
286 379 : if (value == "0" || value == "1")
287 : {
288 4 : return YAML::Node (key.get<bool> ());
289 : }
290 186 : return YAML::Node (value);
291 : }
292 :
293 : /**
294 : * @brief This function creates a YAML Node containing a key value and optionally metadata.
295 : *
296 : * @param key This key specifies the data that should be saved in the YAML node returned by this function.
297 : *
298 : * @note Since YAML does not support non-empty binary data directly this function replaces data stored in binary keys with the string
299 : * `Unsupported binary value!`. If you need support for binary data, please load the Base64 before you use YAML CPP.
300 : *
301 : * @returns A new YAML node containing the data and metadata specified in `key`
302 : */
303 283 : YAML::Node createLeafNode (Key & key)
304 : {
305 :
306 566 : YAML::Node metaNode{ YAML::Node (YAML::NodeType::Map) };
307 566 : YAML::Node dataNode = createMetaDataNode (key);
308 :
309 : key.rewindMeta ();
310 805 : while (Key meta = key.nextMeta ())
311 : {
312 647 : if (meta.getName () == "array" || meta.getName () == "binary") continue;
313 64 : if (meta.getName () == "type" && meta.getString () == "binary")
314 : {
315 4 : dataNode.SetTag ("tag:yaml.org,2002:binary");
316 1 : continue;
317 : }
318 68 : metaNode[meta.getName ()] = meta.getString ();
319 : ELEKTRA_LOG_DEBUG ("Add metakey “%s: %s”", meta.getName ().c_str (), meta.getString ().c_str ());
320 : }
321 :
322 566 : if (metaNode.size () <= 0)
323 : {
324 : ELEKTRA_LOG_DEBUG ("Return leaf node with value “%s”",
325 : dataNode.IsNull () ? "~" : dataNode.IsSequence () ? "[]" : dataNode.as<string> ().c_str ());
326 : return dataNode;
327 : }
328 :
329 16 : YAML::Node node{ YAML::Node (YAML::NodeType::Sequence) };
330 64 : node.SetTag ("!elektra/meta");
331 16 : node.push_back (dataNode);
332 16 : node.push_back (metaNode);
333 :
334 : #ifdef HAVE_LOGGER
335 : ostringstream data;
336 : data << node;
337 : ELEKTRA_LOG_DEBUG ("Return meta leaf node with value “%s”", data.str ().c_str ());
338 : #endif
339 :
340 16 : return node;
341 : }
342 :
343 : /**
344 : * @brief This function adds `null` elements to the given YAML collection.
345 : *
346 : * @param sequence This node stores the collection to which this function adds `numberOfElements` empty elements.
347 : * @param numberOfElements This parameter specifies the number of empty element this function adds to `sequence`.
348 : */
349 106 : void addEmptyArrayElements (YAML::Node & sequence, unsigned long long const numberOfElements)
350 : {
351 : ELEKTRA_LOG_DEBUG ("Add %lld empty array elements", numberOfElements);
352 112 : for (auto missingFields = numberOfElements; missingFields > 0; missingFields--)
353 : {
354 12 : sequence.push_back ({});
355 : }
356 106 : }
357 :
358 : /**
359 : * @brief This function adds a key that is not part of any array to a YAML node.
360 : *
361 : * @param data This node stores the data specified via `keyIterator`.
362 : * @param keyIterator This iterator specifies the current part of the key name this function adds to `data`.
363 : * @param key This parameter specifies the key that should be added to `data`.
364 : */
365 215 : void addKeyNoArray (YAML::Node & data, NameIterator & keyIterator, Key & key)
366 : {
367 218 : if (data.IsScalar ()) data = YAML::Node (YAML::NodeType::Undefined);
368 :
369 : #ifdef HAVE_LOGGER
370 : ostringstream output;
371 : output << data;
372 : ELEKTRA_LOG_DEBUG ("Add key part “%s”", (*keyIterator).c_str ());
373 : #endif
374 :
375 430 : if (keyIterator == key.end ())
376 : {
377 : ELEKTRA_LOG_DEBUG ("Create leaf node for key “%s”", key.getName ().c_str ());
378 6 : data = createLeafNode (key);
379 134 : return;
380 : }
381 424 : if (keyIterator == --key.end ())
382 : {
383 640 : data[*keyIterator] = createLeafNode (key);
384 128 : return;
385 : }
386 :
387 168 : YAML::Node node;
388 :
389 992 : node = (data[*keyIterator] && !data[*keyIterator].IsScalar ()) ? data[*keyIterator] : YAML::Node ();
390 252 : data[*keyIterator] = node;
391 84 : addKeyNoArray (node, ++keyIterator, key);
392 : }
393 :
394 : /**
395 : * @brief This function adds a key that is either, element of an array, or an array parent to a YAML node.
396 : *
397 : * @param data This node stores the data specified via `keyIterator`.
398 : * @param keyIterator This iterator specifies the current part of the key name this function adds to `data`.
399 : * @param key This parameter specifies the key that should be added to `data`.
400 : */
401 294 : void addKeyArray (YAML::Node & data, NameIterator & keyIterator, Key & key)
402 : {
403 294 : auto const isArrayAndIndex = isArrayIndex (keyIterator);
404 294 : auto const isArrayElement = isArrayAndIndex.first;
405 294 : auto const arrayIndex = isArrayAndIndex.second;
406 :
407 296 : if (data.IsScalar ()) data = YAML::Node (YAML::NodeType::Undefined);
408 :
409 : #ifdef HAVE_LOGGER
410 : ostringstream output;
411 : output << data;
412 : ELEKTRA_LOG_DEBUG ("Add key part “%s”", (*keyIterator).c_str ());
413 : #endif
414 :
415 588 : if (keyIterator == key.end ())
416 : {
417 : ELEKTRA_LOG_DEBUG ("Create leaf node for key “%s”", key.getName ().c_str ());
418 8 : data = createLeafNode (key);
419 156 : return;
420 : }
421 580 : if (keyIterator == --key.end ())
422 : {
423 148 : if (isArrayElement)
424 : {
425 106 : addEmptyArrayElements (data, arrayIndex - data.size ());
426 212 : data.push_back (createLeafNode (key));
427 : }
428 : else
429 : {
430 210 : data[*keyIterator] = createLeafNode (key);
431 : }
432 :
433 : return;
434 : }
435 :
436 284 : YAML::Node node;
437 :
438 142 : if (isArrayElement)
439 : {
440 131 : node = (data[arrayIndex] && !data[arrayIndex].IsScalar ()) ? data[arrayIndex] : YAML::Node ();
441 42 : data[arrayIndex] = node;
442 : }
443 : else
444 : {
445 1543 : node = (data[*keyIterator] && !data[*keyIterator].IsScalar ()) ? data[*keyIterator] : YAML::Node ();
446 363 : data[*keyIterator] = node;
447 : }
448 142 : addKeyArray (node, ++keyIterator, key);
449 : }
450 :
451 : /**
452 : * @brief This function adds a key set to a YAML node.
453 : *
454 : * @param data This node stores the data specified via `mappings`.
455 : * @param mappings This keyset specifies all keys and values this function adds to `data`.
456 : * @param parent This key is the root of all keys stored in `mappings`.
457 : * @param isArray This value specifies if the keys inside `keys` are all part of an array (either element or parent), or if none of them is
458 : * part of an array.
459 : */
460 136 : void addKeys (YAML::Node & data, KeySet const & mappings, Key const & parent, bool const isArray = false)
461 : {
462 1257 : for (auto key : mappings)
463 : {
464 : ELEKTRA_LOG_DEBUG ("Convert key “%s”: “%s”", key.getName ().c_str (),
465 : key.getBinarySize () == 0 ? "NULL" : key.isString () ? key.getString ().c_str () : "binary value!");
466 283 : NameIterator keyIterator = relativeKeyIterator (key, parent);
467 :
468 283 : if (isArray)
469 : {
470 152 : addKeyArray (data, keyIterator, key);
471 : }
472 : else
473 : {
474 131 : addKeyNoArray (data, keyIterator, key);
475 : }
476 :
477 : #ifdef HAVE_LOGGER
478 : ostringstream output;
479 : output << data;
480 :
481 : ELEKTRA_LOG_DEBUG ("Converted Data:");
482 : ELEKTRA_LOG_DEBUG ("——————————");
483 :
484 : istringstream stream (output.str ());
485 : for (string line; std::getline (stream, line);)
486 : {
487 : ELEKTRA_LOG_DEBUG ("%s", line.c_str ());
488 : }
489 :
490 : ELEKTRA_LOG_DEBUG ("——————————");
491 : #endif
492 : }
493 136 : }
494 :
495 : } // end namespace
496 :
497 : /**
498 : * @brief This function saves the key-value pairs stored in `mappings` as YAML data in the location specified via `parent`.
499 : *
500 : * @param mappings This key set stores the mappings that should be saved as YAML data.
501 : * @param parent This key specifies the path to the YAML data file that should be written.
502 : */
503 68 : void yamlcpp::yamlWrite (KeySet const & mappings, Key const & parent)
504 : {
505 136 : auto keys = removeArrayMetaData (mappings);
506 136 : auto missing = missingKeys (keys, parent);
507 68 : keys.append (missing);
508 :
509 136 : KeySet arrayParents;
510 136 : KeySet arrays;
511 136 : KeySet nonArrays;
512 :
513 204 : arrayParents = splitArrayParents (keys);
514 272 : tie (arrays, nonArrays) = splitArrayOther (arrayParents, keys);
515 :
516 136 : auto data = YAML::Node ();
517 68 : addKeys (data, nonArrays, parent);
518 68 : addKeys (data, arrays, parent, true);
519 :
520 : #ifdef HAVE_LOGGER
521 : ELEKTRA_LOG_DEBUG ("Write Data:");
522 : ELEKTRA_LOG_DEBUG ("——————————");
523 :
524 : ostringstream outputString;
525 : outputString << data;
526 : istringstream stream (outputString.str ());
527 : for (string line; std::getline (stream, line);)
528 : {
529 : ELEKTRA_LOG_DEBUG ("%s", line.c_str ());
530 : }
531 :
532 : ELEKTRA_LOG_DEBUG ("——————————");
533 : #endif
534 :
535 204 : ofstream output (parent.getString ());
536 68 : output << data;
537 68 : }
|