| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | /* |
| 4 | * Netdata YAML Parser/Generator Module |
| 5 | * |
| 6 | * This module provides YAML parsing and generation using libyaml, with conversion |
| 7 | * to/from json-c objects. It supports the YAML subset that is 100% compatible with JSON. |
| 8 | * |
| 9 | * KNOWN LIMITATIONS (due to libyaml): |
| 10 | * 1. Octal escape sequences (\101) are not supported - use hex (\x41) or unicode (\u0041) |
| 11 | * 2. Single-quoted strings with literal newlines have them converted to spaces |
| 12 | * 3. Null bytes in strings may cause issues |
| 13 | * 4. Complex block scalar indentation may not be preserved exactly |
| 14 | * 5. Some invalid YAML syntax may be accepted without error |
| 15 | * |
| 16 | * The module handles these limitations gracefully and provides consistent behavior |
| 17 | * for round-trip conversion where possible. |
| 18 | */ |
| 19 | |
| 20 | #include "yaml.h" |
| 21 | |
| 22 | #define YAML_MAX_NESTING_DEPTH 256 |
| 23 | |
| 24 | static struct json_object *yaml_node_to_json(yaml_document_t *document, yaml_node_t *node, BUFFER *error, YAML2JSON_FLAGS flags, int depth); |
| 25 | |
| 26 | static struct json_object *yaml_sequence_to_json(yaml_document_t *document, yaml_node_t *node, BUFFER *error, YAML2JSON_FLAGS flags, int depth) { |
| 27 | struct json_object *array = json_object_new_array(); |
| 28 | if (!array) { |
| 29 | buffer_strcat(error, "Failed to create JSON array"); |
| 30 | return NULL; |
| 31 | } |
| 32 | |
| 33 | yaml_node_item_t *item; |
| 34 | for (item = node->data.sequence.items.start; item < node->data.sequence.items.top; item++) { |
| 35 | yaml_node_t *child = yaml_document_get_node(document, *item); |
| 36 | if (!child) { |
| 37 | buffer_sprintf(error, "Invalid sequence item reference"); |
| 38 | json_object_put(array); |
| 39 | return NULL; |
| 40 | } |
| 41 | |
| 42 | size_t error_len = buffer_strlen(error); |
| 43 | struct json_object *child_obj = yaml_node_to_json(document, child, error, flags, depth); |
| 44 | |
| 45 | // Check for error (error buffer grew) |
| 46 | if (buffer_strlen(error) > error_len) { |
| 47 | if (child_obj) json_object_put(child_obj); |
| 48 | json_object_put(array); |
| 49 | return NULL; |
| 50 | } |
| 51 | |
| 52 | if (json_object_array_add(array, child_obj) != 0) { |
| 53 | buffer_strcat(error, "Failed to add item to JSON array"); |
| 54 | json_object_put(child_obj); |
| 55 | json_object_put(array); |
| 56 | return NULL; |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | return array; |
| 61 | } |
| 62 | |
| 63 | static struct json_object *yaml_mapping_to_json(yaml_document_t *document, yaml_node_t *node, BUFFER *error, YAML2JSON_FLAGS flags, int depth) { |
| 64 | struct json_object *object = json_object_new_object(); |
| 65 | if (!object) { |
| 66 | buffer_strcat(error, "Failed to create JSON object"); |
| 67 | return NULL; |
| 68 | } |
| 69 | |
| 70 | yaml_node_pair_t *pair; |
| 71 | for (pair = node->data.mapping.pairs.start; pair < node->data.mapping.pairs.top; pair++) { |
| 72 | yaml_node_t *key_node = yaml_document_get_node(document, pair->key); |
| 73 | yaml_node_t *value_node = yaml_document_get_node(document, pair->value); |
| 74 | |
| 75 | if (!key_node || !value_node) { |
| 76 | buffer_sprintf(error, "Invalid mapping pair reference"); |
| 77 | json_object_put(object); |
| 78 | return NULL; |
| 79 | } |
| 80 | |
| 81 | if (key_node->type != YAML_SCALAR_NODE) { |
| 82 | buffer_sprintf(error, "Mapping key must be a scalar"); |
| 83 | json_object_put(object); |
| 84 | return NULL; |
| 85 | } |
| 86 | |
| 87 | const char *key = (const char *)key_node->data.scalar.value; |
| 88 | size_t error_len = buffer_strlen(error); |
| 89 | struct json_object *value_obj = yaml_node_to_json(document, value_node, error, flags, depth); |
| 90 | |
| 91 | // Check for error (error buffer grew) |
| 92 | if (buffer_strlen(error) > error_len) { |
| 93 | if (value_obj) json_object_put(value_obj); |
| 94 | json_object_put(object); |
| 95 | return NULL; |
| 96 | } |
| 97 | |
| 98 | // json_object_object_add handles NULL values correctly |
| 99 | if (json_object_object_add(object, key, value_obj) != 0) { |
| 100 | buffer_sprintf(error, "Failed to add property to JSON object"); |
| 101 | if (value_obj) json_object_put(value_obj); |
| 102 | json_object_put(object); |
| 103 | return NULL; |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | return object; |
| 108 | } |
| 109 | |
| 110 | // Remove underscores from a numeric string. Returns cleaned length, or 0 if too long. |
| 111 | static size_t remove_underscores(const char *str, size_t len, char *cleaned, size_t cleaned_size) { |
| 112 | size_t j = 0; |
| 113 | for (size_t i = 0; i < len; i++) { |
| 114 | if (str[i] != '_') { |
| 115 | if (j >= cleaned_size - 1) |
| 116 | return 0; // too long to be a number |
| 117 | cleaned[j++] = str[i]; |
| 118 | } |
| 119 | } |
| 120 | cleaned[j] = '\0'; |
| 121 | return j; |
| 122 | } |
| 123 | |
| 124 | // Helper function to parse numbers with underscores |
| 125 | static bool parse_number_with_underscores(const char *str, size_t len, long long *int_result, double *double_result, bool *is_double) { |
| 126 | char cleaned[256]; |
| 127 | if (!remove_underscores(str, len, cleaned, sizeof(cleaned))) |
| 128 | return false; // too long, treat as string |
| 129 | |
| 130 | char *endptr; |
| 131 | errno = 0; |
| 132 | |
| 133 | // Try as integer first |
| 134 | *int_result = strtoll(cleaned, &endptr, 10); |
| 135 | if (errno == 0 && *endptr == '\0') { |
| 136 | *is_double = false; |
| 137 | return true; |
| 138 | } |
| 139 | |
| 140 | // Try as double |
| 141 | errno = 0; |
| 142 | *double_result = strtod(cleaned, &endptr); |
| 143 | if (errno == 0 && *endptr == '\0') { |
| 144 | *is_double = true; |
| 145 | return true; |
| 146 | } |
| 147 | |
| 148 | return false; |
| 149 | } |
| 150 | |
| 151 | static struct json_object *yaml_scalar_to_json(yaml_node_t *node, YAML2JSON_FLAGS flags) { |
| 152 | const char *value = (const char *)node->data.scalar.value; |
| 153 | size_t length = node->data.scalar.length; |
| 154 | |
| 155 | // If YAML2JSON_ALL_VALUES_AS_STRINGS flag is set, always return as string |
| 156 | if (flags & YAML2JSON_ALL_VALUES_AS_STRINGS) { |
| 157 | return json_object_new_string_len(value, (int)length); |
| 158 | } |
| 159 | |
| 160 | // Only plain scalars get implicit type conversion (null/bool/number). |
| 161 | // Quoted, literal block (|), and folded block (>) scalars are always strings. |
| 162 | if (node->data.scalar.style != YAML_PLAIN_SCALAR_STYLE) { |
| 163 | return json_object_new_string_len(value, (int)length); |
| 164 | } |
| 165 | |
| 166 | // Handle null and tilde (case-insensitive for null) |
| 167 | if ((length == 4 && strcasecmp(value, "null") == 0) || |
| 168 | (length == 1 && strncmp(value, "~", 1) == 0)) { |
| 169 | // In json-c, NULL represents JSON null values |
| 170 | return NULL; |
| 171 | } |
| 172 | |
| 173 | // Handle booleans (case-insensitive) |
| 174 | if ((length == 4 && strcasecmp(value, "true") == 0) || |
| 175 | (length == 3 && strcasecmp(value, "yes") == 0) || |
| 176 | (length == 2 && strcasecmp(value, "on") == 0)) { |
| 177 | return json_object_new_boolean(1); |
| 178 | } |
| 179 | |
| 180 | if ((length == 5 && strcasecmp(value, "false") == 0) || |
| 181 | (length == 2 && strcasecmp(value, "no") == 0) || |
| 182 | (length == 3 && strcasecmp(value, "off") == 0)) { |
| 183 | return json_object_new_boolean(0); |
| 184 | } |
| 185 | |
| 186 | // Try to parse as number |
| 187 | char *endptr; |
| 188 | errno = 0; |
| 189 | |
| 190 | // Check for hex (0x or 0X), octal (0o or 0O), or binary (0b or 0B) prefix |
| 191 | if (length > 2 && value[0] == '0') { |
| 192 | int base = 0; |
| 193 | if (value[1] == 'x' || value[1] == 'X') base = 16; |
| 194 | else if (value[1] == 'o' || value[1] == 'O') base = 8; |
| 195 | else if (value[1] == 'b' || value[1] == 'B') base = 2; |
| 196 | |
| 197 | if (base) { |
| 198 | // Check if it contains underscores |
| 199 | bool has_underscore = false; |
| 200 | for (size_t i = 2; i < length; i++) { |
| 201 | if (value[i] == '_') { has_underscore = true; break; } |
| 202 | } |
| 203 | |
| 204 | if (has_underscore) { |
| 205 | char cleaned[256]; |
| 206 | size_t cleaned_len = remove_underscores(value, length, cleaned, sizeof(cleaned)); |
| 207 | // need at least one digit after the prefix (e.g., "0x_" → "0x" has no digits) |
| 208 | if (cleaned_len > 2) { |
| 209 | long long int_val = strtoll(cleaned + 2, &endptr, base); |
| 210 | if (errno == 0 && *endptr == '\0') |
| 211 | return json_object_new_int64(int_val); |
| 212 | } |
| 213 | } else { |
| 214 | long long int_val = strtoll(value + 2, &endptr, base); |
| 215 | if (errno == 0 && endptr == value + length) |
| 216 | return json_object_new_int64(int_val); |
| 217 | } |
| 218 | } |
| 219 | } |
| 220 | |
| 221 | // Check if number contains underscores |
| 222 | bool has_underscore = false; |
| 223 | for (size_t i = 0; i < length; i++) { |
| 224 | if (value[i] == '_') { |
| 225 | has_underscore = true; |
| 226 | break; |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | if (has_underscore) { |
| 231 | long long int_result; |
| 232 | double double_result; |
| 233 | bool is_double; |
| 234 | |
| 235 | if (parse_number_with_underscores(value, length, &int_result, &double_result, &is_double)) { |
| 236 | if (is_double) { |
| 237 | return json_object_new_double(double_result); |
| 238 | } else { |
| 239 | return json_object_new_int64(int_result); |
| 240 | } |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | // Try integer |
| 245 | long long int_val = strtoll(value, &endptr, 10); |
| 246 | if (errno == 0 && endptr == value + length && *value != '\0') { |
| 247 | return json_object_new_int64(int_val); |
| 248 | } |
| 249 | |
| 250 | // Try double |
| 251 | errno = 0; |
| 252 | double double_val = strtod(value, &endptr); |
| 253 | if (errno == 0 && endptr == value + length && *value != '\0') { |
| 254 | return json_object_new_double(double_val); |
| 255 | } |
| 256 | |
| 257 | // Default to string |
| 258 | return json_object_new_string_len(value, (int)length); |
| 259 | } |
| 260 | |
| 261 | static struct json_object *yaml_node_to_json(yaml_document_t *document, yaml_node_t *node, BUFFER *error, YAML2JSON_FLAGS flags, int depth) { |
| 262 | if (!node) { |
| 263 | buffer_strcat(error, "NULL node"); |
| 264 | return NULL; |
| 265 | } |
| 266 | |
| 267 | if (depth > YAML_MAX_NESTING_DEPTH) { |
| 268 | buffer_sprintf(error, "YAML nesting too deep (max %d levels)", YAML_MAX_NESTING_DEPTH); |
| 269 | return NULL; |
| 270 | } |
| 271 | |
| 272 | switch (node->type) { |
| 273 | case YAML_SCALAR_NODE: |
| 274 | return yaml_scalar_to_json(node, flags); |
| 275 | |
| 276 | case YAML_SEQUENCE_NODE: |
| 277 | return yaml_sequence_to_json(document, node, error, flags, depth + 1); |
| 278 | |
| 279 | case YAML_MAPPING_NODE: |
| 280 | return yaml_mapping_to_json(document, node, error, flags, depth + 1); |
| 281 | |
| 282 | default: |
| 283 | buffer_sprintf(error, "Unsupported YAML node type: %u", (unsigned int)node->type); |
| 284 | return NULL; |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | static struct json_object *yaml_document_to_json_with_flags(yaml_document_t *document, BUFFER *error, YAML2JSON_FLAGS flags) { |
| 289 | yaml_node_t *root = yaml_document_get_root_node(document); |
| 290 | if (!root) { |
| 291 | // Empty document - this is valid YAML but we return NULL |
| 292 | return NULL; |
| 293 | } |
| 294 | |
| 295 | return yaml_node_to_json(document, root, error, flags, 0); |
| 296 | } |
| 297 | |
| 298 | static struct json_object *yaml_parse_common(yaml_parser_t *parser, BUFFER *error, YAML2JSON_FLAGS flags) { |
| 299 | yaml_document_t document; |
| 300 | struct json_object *result = NULL; |
| 301 | |
| 302 | if (!yaml_parser_load(parser, &document)) { |
| 303 | if (parser->error == YAML_NO_ERROR) { |
| 304 | buffer_strcat(error, "No YAML document found (empty input)"); |
| 305 | } else { |
| 306 | buffer_sprintf(error, "YAML parse error: %s at line %zu, column %zu", |
| 307 | parser->problem ? parser->problem : "unknown error", |
| 308 | parser->problem_mark.line + 1, |
| 309 | parser->problem_mark.column + 1); |
| 310 | } |
| 311 | goto cleanup; |
| 312 | } |
| 313 | |
| 314 | |
| 315 | result = yaml_document_to_json_with_flags(&document, error, flags); |
| 316 | yaml_document_delete(&document); |
| 317 | |
| 318 | cleanup: |
| 319 | yaml_parser_delete(parser); |
| 320 | return result; |
| 321 | } |
| 322 | |
| 323 | struct json_object *yaml_parse_string(const char *yaml_string, BUFFER *error, YAML2JSON_FLAGS flags) { |
| 324 | if (!yaml_string) { |
| 325 | buffer_strcat(error, "NULL YAML string"); |
| 326 | return NULL; |
| 327 | } |
| 328 | |
| 329 | yaml_parser_t parser; |
| 330 | if (!yaml_parser_initialize(&parser)) { |
| 331 | buffer_strcat(error, "Failed to initialize YAML parser"); |
| 332 | return NULL; |
| 333 | } |
| 334 | |
| 335 | yaml_parser_set_input_string(&parser, (const unsigned char *)yaml_string, strlen(yaml_string)); |
| 336 | return yaml_parse_common(&parser, error, flags); |
| 337 | } |
| 338 | |
| 339 | struct json_object *yaml_parse_filename(const char *filename, BUFFER *error, YAML2JSON_FLAGS flags) { |
| 340 | if (!filename) { |
| 341 | buffer_strcat(error, "NULL filename"); |
| 342 | return NULL; |
| 343 | } |
| 344 | |
| 345 | FILE *file = fopen(filename, "r"); |
| 346 | if (!file) { |
| 347 | buffer_sprintf(error, "Failed to open file '%s': %s", filename, strerror(errno)); |
| 348 | return NULL; |
| 349 | } |
| 350 | |
| 351 | yaml_parser_t parser; |
| 352 | if (!yaml_parser_initialize(&parser)) { |
| 353 | buffer_strcat(error, "Failed to initialize YAML parser"); |
| 354 | fclose(file); |
| 355 | return NULL; |
| 356 | } |
| 357 | |
| 358 | yaml_parser_set_input_file(&parser, file); |
| 359 | struct json_object *result = yaml_parse_common(&parser, error, flags); |
| 360 | fclose(file); |
| 361 | return result; |
| 362 | } |
| 363 | |
| 364 | struct json_object *yaml_parse_fd(int fd, BUFFER *error, YAML2JSON_FLAGS flags) { |
| 365 | if (fd < 0) { |
| 366 | buffer_strcat(error, "Invalid file descriptor"); |
| 367 | return NULL; |
| 368 | } |
| 369 | |
| 370 | int duped = dup(fd); |
| 371 | if (duped < 0) { |
| 372 | buffer_sprintf(error, "Failed to dup file descriptor: %s", strerror(errno)); |
| 373 | return NULL; |
| 374 | } |
| 375 | |
| 376 | FILE *file = fdopen(duped, "r"); |
| 377 | if (!file) { |
| 378 | close(duped); |
| 379 | buffer_sprintf(error, "Failed to open file descriptor: %s", strerror(errno)); |
| 380 | return NULL; |
| 381 | } |
| 382 | |
| 383 | yaml_parser_t parser; |
| 384 | if (!yaml_parser_initialize(&parser)) { |
| 385 | buffer_strcat(error, "Failed to initialize YAML parser"); |
| 386 | fclose(file); |
| 387 | return NULL; |
| 388 | } |
| 389 | |
| 390 | yaml_parser_set_input_file(&parser, file); |
| 391 | struct json_object *result = yaml_parse_common(&parser, error, flags); |
| 392 | fclose(file); |
| 393 | return result; |
| 394 | } |
| 395 | |
| 396 | // YAML generation functions |
| 397 | |
| 398 | // Determine the scalar style needed to preserve round-trip fidelity for a string. |
| 399 | // Strings that would be misinterpreted as null/bool/number by YAML parsers get quoted. |
| 400 | static yaml_scalar_style_t yaml_string_scalar_style(const char *str, size_t len) { |
| 401 | if (len == 0) |
| 402 | return YAML_DOUBLE_QUOTED_SCALAR_STYLE; |
| 403 | |
| 404 | if (strchr(str, '\n') || strchr(str, '\r') || |
| 405 | str[0] == ' ' || str[len - 1] == ' ' || str[0] == '\t') |
| 406 | return YAML_DOUBLE_QUOTED_SCALAR_STYLE; |
| 407 | |
| 408 | // YAML null/tilde and booleans (case-insensitive) |
| 409 | if ((len == 4 && strcasecmp(str, "null") == 0) || |
| 410 | (len == 1 && str[0] == '~') || |
| 411 | (len == 4 && strcasecmp(str, "true") == 0) || |
| 412 | (len == 5 && strcasecmp(str, "false") == 0) || |
| 413 | (len == 3 && strcasecmp(str, "yes") == 0) || |
| 414 | (len == 2 && strcasecmp(str, "no") == 0) || |
| 415 | (len == 2 && strcasecmp(str, "on") == 0) || |
| 416 | (len == 3 && strcasecmp(str, "off") == 0)) |
| 417 | return YAML_DOUBLE_QUOTED_SCALAR_STYLE; |
| 418 | |
| 419 | // Numeric-looking strings |
| 420 | char c = str[0]; |
| 421 | if (c == '+' || c == '-' || c == '.' || (c >= '0' && c <= '9')) |
| 422 | return YAML_DOUBLE_QUOTED_SCALAR_STYLE; |
| 423 | |
| 424 | return YAML_PLAIN_SCALAR_STYLE; |
| 425 | } |
| 426 | |
| 427 | static int yaml_add_json_to_document(yaml_document_t *document, struct json_object *json, BUFFER *error, int depth); |
| 428 | |
| 429 | static int yaml_add_array_to_document(yaml_document_t *document, struct json_object *array, BUFFER *error, int depth) { |
| 430 | int sequence = yaml_document_add_sequence(document, NULL, YAML_BLOCK_SEQUENCE_STYLE); |
| 431 | if (!sequence) { |
| 432 | buffer_strcat(error, "Failed to add sequence to YAML document"); |
| 433 | return 0; |
| 434 | } |
| 435 | |
| 436 | size_t len = json_object_array_length(array); |
| 437 | for (size_t i = 0; i < len; i++) { |
| 438 | struct json_object *item = json_object_array_get_idx(array, i); |
| 439 | int item_node = yaml_add_json_to_document(document, item, error, depth); |
| 440 | if (!item_node) { |
| 441 | return 0; |
| 442 | } |
| 443 | |
| 444 | if (!yaml_document_append_sequence_item(document, sequence, item_node)) { |
| 445 | buffer_strcat(error, "Failed to append item to YAML sequence"); |
| 446 | return 0; |
| 447 | } |
| 448 | } |
| 449 | |
| 450 | return sequence; |
| 451 | } |
| 452 | |
| 453 | static int yaml_add_object_to_document(yaml_document_t *document, struct json_object *object, BUFFER *error, int depth) { |
| 454 | int mapping = yaml_document_add_mapping(document, NULL, YAML_BLOCK_MAPPING_STYLE); |
| 455 | if (!mapping) { |
| 456 | buffer_strcat(error, "Failed to add mapping to YAML document"); |
| 457 | return 0; |
| 458 | } |
| 459 | |
| 460 | json_object_object_foreach(object, key, value) { |
| 461 | size_t key_len = strlen(key); |
| 462 | yaml_scalar_style_t key_style = yaml_string_scalar_style(key, key_len); |
| 463 | int key_node = yaml_document_add_scalar(document, NULL, |
| 464 | (yaml_char_t *)key, |
| 465 | key_len, |
| 466 | key_style); |
| 467 | if (!key_node) { |
| 468 | buffer_sprintf(error, "Failed to add key '%s' to YAML document", key); |
| 469 | return 0; |
| 470 | } |
| 471 | |
| 472 | int value_node = yaml_add_json_to_document(document, value, error, depth); |
| 473 | if (!value_node) { |
| 474 | return 0; |
| 475 | } |
| 476 | |
| 477 | if (!yaml_document_append_mapping_pair(document, mapping, key_node, value_node)) { |
| 478 | buffer_sprintf(error, "Failed to add mapping pair for key '%s'", key); |
| 479 | return 0; |
| 480 | } |
| 481 | } |
| 482 | |
| 483 | return mapping; |
| 484 | } |
| 485 | |
| 486 | static int yaml_add_json_to_document(yaml_document_t *document, struct json_object *json, BUFFER *error, int depth) { |
| 487 | if (depth > YAML_MAX_NESTING_DEPTH) { |
| 488 | buffer_sprintf(error, "JSON nesting too deep for YAML generation (max %d levels)", YAML_MAX_NESTING_DEPTH); |
| 489 | return 0; |
| 490 | } |
| 491 | |
| 492 | if (!json) { |
| 493 | return yaml_document_add_scalar(document, NULL, |
| 494 | (yaml_char_t *)"null", |
| 495 | 4, |
| 496 | YAML_PLAIN_SCALAR_STYLE); |
| 497 | } |
| 498 | |
| 499 | enum json_type type = json_object_get_type(json); |
| 500 | |
| 501 | switch (type) { |
| 502 | case json_type_null: |
| 503 | return yaml_document_add_scalar(document, NULL, |
| 504 | (yaml_char_t *)"null", |
| 505 | 4, |
| 506 | YAML_PLAIN_SCALAR_STYLE); |
| 507 | |
| 508 | case json_type_boolean: { |
| 509 | const char *bool_str = json_object_get_boolean(json) ? "true" : "false"; |
| 510 | return yaml_document_add_scalar(document, NULL, |
| 511 | (yaml_char_t *)bool_str, |
| 512 | strlen(bool_str), |
| 513 | YAML_PLAIN_SCALAR_STYLE); |
| 514 | } |
| 515 | |
| 516 | case json_type_int: { |
| 517 | char buf[32]; |
| 518 | snprintf(buf, sizeof(buf), "%" PRId64, json_object_get_int64(json)); |
| 519 | return yaml_document_add_scalar(document, NULL, |
| 520 | (yaml_char_t *)buf, |
| 521 | strlen(buf), |
| 522 | YAML_PLAIN_SCALAR_STYLE); |
| 523 | } |
| 524 | |
| 525 | case json_type_double: { |
| 526 | char buf[64]; |
| 527 | double val = json_object_get_double(json); |
| 528 | |
| 529 | // Handle special case of -0.0 |
| 530 | if (val == 0.0 && signbit(val)) { |
| 531 | strcpy(buf, "0.0"); |
| 532 | } else { |
| 533 | // Use json-c's formatting but preserve .0 for whole numbers |
| 534 | const char *json_str = json_object_to_json_string(json); |
| 535 | strncpy(buf, json_str, sizeof(buf) - 1); |
| 536 | buf[sizeof(buf) - 1] = '\0'; |
| 537 | |
| 538 | // Check if this is a whole number that needs .0 suffix |
| 539 | double int_part; |
| 540 | if (modf(val, &int_part) == 0.0 && !strchr(buf, '.') && !strchr(buf, 'e') && !strchr(buf, 'E')) { |
| 541 | size_t len = strlen(buf); |
| 542 | if (len < sizeof(buf) - 3) { |
| 543 | strcat(buf, ".0"); |
| 544 | } |
| 545 | } |
| 546 | } |
| 547 | |
| 548 | return yaml_document_add_scalar(document, NULL, |
| 549 | (yaml_char_t *)buf, |
| 550 | strlen(buf), |
| 551 | YAML_PLAIN_SCALAR_STYLE); |
| 552 | } |
| 553 | |
| 554 | case json_type_string: { |
| 555 | const char *str = json_object_get_string(json); |
| 556 | size_t len = json_object_get_string_len(json); |
| 557 | yaml_scalar_style_t style = yaml_string_scalar_style(str, len); |
| 558 | |
| 559 | return yaml_document_add_scalar(document, NULL, |
| 560 | (yaml_char_t *)str, |
| 561 | len, |
| 562 | style); |
| 563 | } |
| 564 | |
| 565 | case json_type_array: |
| 566 | return yaml_add_array_to_document(document, json, error, depth + 1); |
| 567 | |
| 568 | case json_type_object: |
| 569 | return yaml_add_object_to_document(document, json, error, depth + 1); |
| 570 | |
| 571 | default: |
| 572 | buffer_sprintf(error, "Unknown JSON type: %u", (unsigned int)type); |
| 573 | return 0; |
| 574 | } |
| 575 | } |
| 576 | |
| 577 | static bool json_to_yaml_document(struct json_object *json, yaml_document_t *document, BUFFER *error) { |
| 578 | // Initialize with implicit_start=1 and implicit_end=1 to avoid "---" and "..." |
| 579 | if (!yaml_document_initialize(document, NULL, NULL, NULL, 1, 1)) { |
| 580 | buffer_strcat(error, "Failed to initialize YAML document"); |
| 581 | return false; |
| 582 | } |
| 583 | |
| 584 | int root = yaml_add_json_to_document(document, json, error, 0); |
| 585 | if (!root) { |
| 586 | yaml_document_delete(document); |
| 587 | return false; |
| 588 | } |
| 589 | |
| 590 | return true; |
| 591 | } |
| 592 | |
| 593 | static bool yaml_generate_common(yaml_emitter_t *emitter, struct json_object *json, BUFFER *error) { |
| 594 | yaml_document_t document; |
| 595 | bool success = false; |
| 596 | |
| 597 | if (!json_to_yaml_document(json, &document, error)) { |
| 598 | goto cleanup; |
| 599 | } |
| 600 | |
| 601 | if (!yaml_emitter_open(emitter)) { |
| 602 | buffer_strcat(error, "Failed to open YAML emitter"); |
| 603 | yaml_document_delete(&document); |
| 604 | goto cleanup; |
| 605 | } |
| 606 | |
| 607 | // yaml_emitter_dump() takes ownership of the document and destroys it |
| 608 | // regardless of success or failure — do NOT call yaml_document_delete after this |
| 609 | if (!yaml_emitter_dump(emitter, &document)) { |
| 610 | buffer_sprintf(error, "YAML emit error: %s", |
| 611 | emitter->problem ? emitter->problem : "unknown error"); |
| 612 | goto cleanup; |
| 613 | } |
| 614 | |
| 615 | if (!yaml_emitter_close(emitter)) { |
| 616 | buffer_strcat(error, "Failed to close YAML emitter"); |
| 617 | goto cleanup; |
| 618 | } |
| 619 | |
| 620 | success = true; |
| 621 | |
| 622 | cleanup: |
| 623 | yaml_emitter_delete(emitter); |
| 624 | return success; |
| 625 | } |
| 626 | |
| 627 | static int yaml_write_handler_buffer(void *data, unsigned char *buffer, size_t size) { |
| 628 | BUFFER *buf = (BUFFER *)data; |
| 629 | buffer_memcat(buf, buffer, size); |
| 630 | return 1; |
| 631 | } |
| 632 | |
| 633 | bool yaml_generate_to_buffer(BUFFER *dst, struct json_object *json, BUFFER *error) { |
| 634 | if (!dst) { |
| 635 | buffer_strcat(error, "NULL destination buffer"); |
| 636 | return false; |
| 637 | } |
| 638 | |
| 639 | yaml_emitter_t emitter; |
| 640 | if (!yaml_emitter_initialize(&emitter)) { |
| 641 | buffer_strcat(error, "Failed to initialize YAML emitter"); |
| 642 | return false; |
| 643 | } |
| 644 | |
| 645 | yaml_emitter_set_output(&emitter, yaml_write_handler_buffer, dst); |
| 646 | yaml_emitter_set_unicode(&emitter, 1); |
| 647 | return yaml_generate_common(&emitter, json, error); |
| 648 | } |
| 649 | |
| 650 | bool yaml_generate_to_filename(const char *filename, struct json_object *json, BUFFER *error) { |
| 651 | if (!filename) { |
| 652 | buffer_strcat(error, "NULL filename"); |
| 653 | return false; |
| 654 | } |
| 655 | |
| 656 | FILE *file = fopen(filename, "w"); |
| 657 | if (!file) { |
| 658 | buffer_sprintf(error, "Failed to open file '%s': %s", filename, strerror(errno)); |
| 659 | return false; |
| 660 | } |
| 661 | |
| 662 | yaml_emitter_t emitter; |
| 663 | if (!yaml_emitter_initialize(&emitter)) { |
| 664 | buffer_strcat(error, "Failed to initialize YAML emitter"); |
| 665 | fclose(file); |
| 666 | return false; |
| 667 | } |
| 668 | |
| 669 | yaml_emitter_set_output_file(&emitter, file); |
| 670 | yaml_emitter_set_unicode(&emitter, 1); |
| 671 | bool result = yaml_generate_common(&emitter, json, error); |
| 672 | fclose(file); |
| 673 | return result; |
| 674 | } |
| 675 | |
| 676 | bool yaml_generate_to_fd(int fd, struct json_object *json, BUFFER *error) { |
| 677 | if (fd < 0) { |
| 678 | buffer_strcat(error, "Invalid file descriptor"); |
| 679 | return false; |
| 680 | } |
| 681 | |
| 682 | int duped = dup(fd); |
| 683 | if (duped < 0) { |
| 684 | buffer_sprintf(error, "Failed to dup file descriptor: %s", strerror(errno)); |
| 685 | return false; |
| 686 | } |
| 687 | |
| 688 | FILE *file = fdopen(duped, "w"); |
| 689 | if (!file) { |
| 690 | close(duped); |
| 691 | buffer_sprintf(error, "Failed to open file descriptor: %s", strerror(errno)); |
| 692 | return false; |
| 693 | } |
| 694 | |
| 695 | yaml_emitter_t emitter; |
| 696 | if (!yaml_emitter_initialize(&emitter)) { |
| 697 | buffer_strcat(error, "Failed to initialize YAML emitter"); |
| 698 | fclose(file); |
| 699 | return false; |
| 700 | } |
| 701 | |
| 702 | yaml_emitter_set_output_file(&emitter, file); |
| 703 | yaml_emitter_set_unicode(&emitter, 1); |
| 704 | bool result = yaml_generate_common(&emitter, json, error); |
| 705 | fclose(file); |
| 706 | return result; |
| 707 | } |