/Users/alexjokela/projects/lattice/src/toml_ops.c
Line | Count | Source |
1 | | #include "toml_ops.h" |
2 | | #include <stdlib.h> |
3 | | #include <string.h> |
4 | | #include <stdio.h> |
5 | | #include <stdarg.h> |
6 | | #include <ctype.h> |
7 | | |
8 | | /* ======================================================================== |
9 | | * Internal: TOML Parser |
10 | | * ======================================================================== */ |
11 | | |
12 | | typedef struct { |
13 | | const char *src; |
14 | | size_t pos; |
15 | | char *err; |
16 | | } TomlParser; |
17 | | |
18 | 90 | static void tp_skip_ws(TomlParser *p) { |
19 | 144 | while (p->src[p->pos] == ' ' || p->src[p->pos] == '\t') |
20 | 54 | p->pos++; |
21 | 90 | } |
22 | | |
23 | 66 | static void tp_skip_ws_and_newlines(TomlParser *p) { |
24 | 72 | while (p->src[p->pos] == ' ' || p->src[p->pos] == '\t' || |
25 | 72 | p->src[p->pos] == '\n' || p->src[p->pos] == '\r') |
26 | 6 | p->pos++; |
27 | 66 | } |
28 | | |
29 | 78 | static void tp_skip_comment(TomlParser *p) { |
30 | 78 | if (p->src[p->pos] == '#') { |
31 | 0 | while (p->src[p->pos] && p->src[p->pos] != '\n') |
32 | 0 | p->pos++; |
33 | 0 | } |
34 | 78 | } |
35 | | |
36 | 30 | static void tp_skip_line_rest(TomlParser *p) { |
37 | 30 | tp_skip_ws(p); |
38 | 30 | tp_skip_comment(p); |
39 | 30 | if (p->src[p->pos] == '\n') p->pos++; |
40 | 12 | else if (p->src[p->pos] == '\r') { |
41 | 0 | p->pos++; |
42 | 0 | if (p->src[p->pos] == '\n') p->pos++; |
43 | 0 | } |
44 | 30 | } |
45 | | |
46 | 0 | static void tp_error(TomlParser *p, const char *msg) { |
47 | 0 | if (!p->err) { |
48 | 0 | size_t len = strlen(msg) + 64; |
49 | 0 | p->err = malloc(len); |
50 | 0 | snprintf(p->err, len, "toml_parse error at position %zu: %s", p->pos, msg); |
51 | 0 | } |
52 | 0 | } |
53 | | |
54 | | /* Forward declarations */ |
55 | | static LatValue tp_parse_value(TomlParser *p); |
56 | | |
57 | | /* ── Parse bare key ── */ |
58 | 30 | static char *tp_parse_bare_key(TomlParser *p) { |
59 | 30 | size_t start = p->pos; |
60 | 171 | while (isalnum((unsigned char)p->src[p->pos]) || p->src[p->pos] == '_' || |
61 | 171 | p->src[p->pos] == '-') |
62 | 141 | p->pos++; |
63 | 30 | if (p->pos == start) { |
64 | 0 | tp_error(p, "expected key"); |
65 | 0 | return NULL; |
66 | 0 | } |
67 | 30 | return strndup(p->src + start, p->pos - start); |
68 | 30 | } |
69 | | |
70 | | /* ── Parse quoted string ── */ |
71 | 9 | static char *tp_parse_quoted_string(TomlParser *p) { |
72 | 9 | char quote = p->src[p->pos]; |
73 | 9 | p->pos++; |
74 | | |
75 | 9 | size_t cap = 64; |
76 | 9 | char *buf = malloc(cap); |
77 | 9 | size_t len = 0; |
78 | | |
79 | 9 | if (quote == '"') { |
80 | 63 | while (p->src[p->pos] && p->src[p->pos] != '"') { |
81 | 54 | if (p->src[p->pos] == '\\') { |
82 | 0 | p->pos++; |
83 | 0 | char c = p->src[p->pos++]; |
84 | 0 | char esc; |
85 | 0 | switch (c) { |
86 | 0 | case 'n': esc = '\n'; break; |
87 | 0 | case 't': esc = '\t'; break; |
88 | 0 | case 'r': esc = '\r'; break; |
89 | 0 | case '\\': esc = '\\'; break; |
90 | 0 | case '"': esc = '"'; break; |
91 | 0 | default: |
92 | 0 | tp_error(p, "invalid escape sequence"); |
93 | 0 | free(buf); |
94 | 0 | return NULL; |
95 | 0 | } |
96 | 0 | if (len + 1 >= cap) { cap *= 2; buf = realloc(buf, cap); } |
97 | 0 | buf[len++] = esc; |
98 | 54 | } else { |
99 | 54 | if (len + 1 >= cap) { cap *= 2; buf = realloc(buf, cap); } |
100 | 54 | buf[len++] = p->src[p->pos++]; |
101 | 54 | } |
102 | 54 | } |
103 | 9 | } else { |
104 | 0 | while (p->src[p->pos] && p->src[p->pos] != '\'') { |
105 | 0 | if (len + 1 >= cap) { cap *= 2; buf = realloc(buf, cap); } |
106 | 0 | buf[len++] = p->src[p->pos++]; |
107 | 0 | } |
108 | 0 | } |
109 | | |
110 | 9 | if (p->src[p->pos] != quote) { |
111 | 0 | tp_error(p, "unterminated string"); |
112 | 0 | free(buf); |
113 | 0 | return NULL; |
114 | 0 | } |
115 | 9 | p->pos++; |
116 | 9 | buf[len] = '\0'; |
117 | 9 | return buf; |
118 | 9 | } |
119 | | |
120 | | /* ── Parse a key (bare or quoted) ── */ |
121 | 30 | static char *tp_parse_key(TomlParser *p) { |
122 | 30 | if (p->src[p->pos] == '"' || p->src[p->pos] == '\'') |
123 | 0 | return tp_parse_quoted_string(p); |
124 | 30 | return tp_parse_bare_key(p); |
125 | 30 | } |
126 | | |
127 | | /* ── Parse string value ── */ |
128 | 9 | static LatValue tp_parse_string_value(TomlParser *p) { |
129 | 9 | char *s = tp_parse_quoted_string(p); |
130 | 9 | if (!s) return value_unit(); |
131 | 9 | LatValue v = value_string(s); |
132 | 9 | free(s); |
133 | 9 | return v; |
134 | 9 | } |
135 | | |
136 | | /* ── Parse number ── */ |
137 | 18 | static LatValue tp_parse_number(TomlParser *p) { |
138 | 18 | size_t start = p->pos; |
139 | 18 | if (p->src[p->pos] == '-' || p->src[p->pos] == '+') |
140 | 0 | p->pos++; |
141 | 18 | bool is_float = false; |
142 | | |
143 | 51 | while (isdigit((unsigned char)p->src[p->pos]) || p->src[p->pos] == '_') |
144 | 33 | p->pos++; |
145 | 18 | if (p->src[p->pos] == '.') { |
146 | 0 | is_float = true; |
147 | 0 | p->pos++; |
148 | 0 | while (isdigit((unsigned char)p->src[p->pos]) || p->src[p->pos] == '_') |
149 | 0 | p->pos++; |
150 | 0 | } |
151 | 18 | if (p->src[p->pos] == 'e' || p->src[p->pos] == 'E') { |
152 | 0 | is_float = true; |
153 | 0 | p->pos++; |
154 | 0 | if (p->src[p->pos] == '+' || p->src[p->pos] == '-') p->pos++; |
155 | 0 | while (isdigit((unsigned char)p->src[p->pos])) p->pos++; |
156 | 0 | } |
157 | | |
158 | 18 | size_t nlen = p->pos - start; |
159 | 18 | char *numstr = malloc(nlen + 1); |
160 | 18 | size_t j = 0; |
161 | 51 | for (size_t i = start; i < p->pos; i++) { |
162 | 33 | if (p->src[i] != '_') numstr[j++] = p->src[i]; |
163 | 33 | } |
164 | 18 | numstr[j] = '\0'; |
165 | | |
166 | 18 | LatValue result; |
167 | 18 | if (is_float) { |
168 | 0 | result = value_float(strtod(numstr, NULL)); |
169 | 18 | } else { |
170 | 18 | result = value_int(strtoll(numstr, NULL, 10)); |
171 | 18 | } |
172 | 18 | free(numstr); |
173 | 18 | return result; |
174 | 18 | } |
175 | | |
176 | | /* ── Array push helper ── */ |
177 | 9 | static void arr_push(LatValue **elems, size_t *len, size_t *cap, LatValue v) { |
178 | 9 | if (*len >= *cap) { |
179 | 0 | *cap = *cap < 4 ? 4 : *cap * 2; |
180 | 0 | *elems = realloc(*elems, *cap * sizeof(LatValue)); |
181 | 0 | } |
182 | 9 | (*elems)[(*len)++] = v; |
183 | 9 | } |
184 | | |
185 | | /* ── Parse inline array ── */ |
186 | 3 | static LatValue tp_parse_array(TomlParser *p) { |
187 | 3 | p->pos++; /* skip [ */ |
188 | 3 | size_t cap = 4, len = 0; |
189 | 3 | LatValue *elems = malloc(cap * sizeof(LatValue)); |
190 | | |
191 | 3 | tp_skip_ws_and_newlines(p); |
192 | 3 | tp_skip_comment(p); |
193 | 3 | tp_skip_ws_and_newlines(p); |
194 | | |
195 | 12 | while (p->src[p->pos] != ']' && p->src[p->pos] != '\0') { |
196 | 9 | if (p->err) { |
197 | 0 | for (size_t i = 0; i < len; i++) value_free(&elems[i]); |
198 | 0 | free(elems); |
199 | 0 | return value_unit(); |
200 | 0 | } |
201 | | |
202 | 9 | LatValue elem = tp_parse_value(p); |
203 | 9 | if (p->err) { |
204 | 0 | value_free(&elem); |
205 | 0 | for (size_t i = 0; i < len; i++) value_free(&elems[i]); |
206 | 0 | free(elems); |
207 | 0 | return value_unit(); |
208 | 0 | } |
209 | | |
210 | 9 | arr_push(&elems, &len, &cap, elem); |
211 | | |
212 | 9 | tp_skip_ws_and_newlines(p); |
213 | 9 | tp_skip_comment(p); |
214 | 9 | tp_skip_ws_and_newlines(p); |
215 | 9 | if (p->src[p->pos] == ',') { |
216 | 6 | p->pos++; |
217 | 6 | tp_skip_ws_and_newlines(p); |
218 | 6 | tp_skip_comment(p); |
219 | 6 | tp_skip_ws_and_newlines(p); |
220 | 6 | } |
221 | 9 | } |
222 | | |
223 | 3 | if (p->src[p->pos] != ']') { |
224 | 0 | tp_error(p, "unterminated array"); |
225 | 0 | for (size_t i = 0; i < len; i++) value_free(&elems[i]); |
226 | 0 | free(elems); |
227 | 0 | return value_unit(); |
228 | 0 | } |
229 | 3 | p->pos++; |
230 | 3 | LatValue arr = value_array(elems, len); |
231 | 3 | free(elems); |
232 | 3 | return arr; |
233 | 3 | } |
234 | | |
235 | | /* ── Parse inline table ── */ |
236 | 0 | static LatValue tp_parse_inline_table(TomlParser *p) { |
237 | 0 | p->pos++; /* skip { */ |
238 | 0 | LatValue map = value_map_new(); |
239 | |
|
240 | 0 | tp_skip_ws(p); |
241 | 0 | while (p->src[p->pos] != '}' && p->src[p->pos] != '\0') { |
242 | 0 | if (p->err) { value_free(&map); return value_unit(); } |
243 | | |
244 | 0 | char *key = tp_parse_key(p); |
245 | 0 | if (!key || p->err) { free(key); value_free(&map); return value_unit(); } |
246 | | |
247 | 0 | tp_skip_ws(p); |
248 | 0 | if (p->src[p->pos] != '=') { |
249 | 0 | tp_error(p, "expected '=' after key"); |
250 | 0 | free(key); value_free(&map); return value_unit(); |
251 | 0 | } |
252 | 0 | p->pos++; |
253 | 0 | tp_skip_ws(p); |
254 | |
|
255 | 0 | LatValue val = tp_parse_value(p); |
256 | 0 | if (p->err) { free(key); value_free(&val); value_free(&map); return value_unit(); } |
257 | | |
258 | 0 | lat_map_set(map.as.map.map, key, &val); |
259 | 0 | free(key); |
260 | |
|
261 | 0 | tp_skip_ws(p); |
262 | 0 | if (p->src[p->pos] == ',') { |
263 | 0 | p->pos++; |
264 | 0 | tp_skip_ws(p); |
265 | 0 | } |
266 | 0 | } |
267 | | |
268 | 0 | if (p->src[p->pos] != '}') { |
269 | 0 | tp_error(p, "unterminated inline table"); |
270 | 0 | value_free(&map); |
271 | 0 | return value_unit(); |
272 | 0 | } |
273 | 0 | p->pos++; |
274 | 0 | return map; |
275 | 0 | } |
276 | | |
277 | | /* ── Parse a value ── */ |
278 | 36 | static LatValue tp_parse_value(TomlParser *p) { |
279 | 36 | char c = p->src[p->pos]; |
280 | 36 | if (c == '"' || c == '\'') return tp_parse_string_value(p); |
281 | 27 | if (c == '[') return tp_parse_array(p); |
282 | 24 | if (c == '{') return tp_parse_inline_table(p); |
283 | 24 | if (c == 't' && strncmp(p->src + p->pos, "true", 4) == 0 && |
284 | 24 | !isalnum((unsigned char)p->src[p->pos + 4])) { |
285 | 3 | p->pos += 4; |
286 | 3 | return value_bool(true); |
287 | 3 | } |
288 | 21 | if (c == 'f' && strncmp(p->src + p->pos, "false", 5) == 0 && |
289 | 21 | !isalnum((unsigned char)p->src[p->pos + 5])) { |
290 | 3 | p->pos += 5; |
291 | 3 | return value_bool(false); |
292 | 3 | } |
293 | 18 | if (isdigit((unsigned char)c) || c == '-' || c == '+') |
294 | 18 | return tp_parse_number(p); |
295 | | |
296 | 0 | tp_error(p, "unexpected character"); |
297 | 0 | return value_unit(); |
298 | 18 | } |
299 | | |
300 | | /* ── Get or create nested map from dotted key path ── */ |
301 | 3 | static LatMap *tp_ensure_table(LatValue *root, char **parts, size_t count) { |
302 | 3 | LatMap *cur = root->as.map.map; |
303 | 6 | for (size_t i = 0; i < count; i++) { |
304 | 3 | LatValue *existing = (LatValue *)lat_map_get(cur, parts[i]); |
305 | 3 | if (existing && existing->type == VAL_MAP) { |
306 | 0 | cur = existing->as.map.map; |
307 | 3 | } else { |
308 | 3 | LatValue sub = value_map_new(); |
309 | 3 | lat_map_set(cur, parts[i], &sub); |
310 | 3 | LatValue *inserted = (LatValue *)lat_map_get(cur, parts[i]); |
311 | 3 | cur = inserted->as.map.map; |
312 | 3 | } |
313 | 3 | } |
314 | 3 | return cur; |
315 | 3 | } |
316 | | |
317 | | /* ── Parse dotted key into parts ── */ |
318 | 30 | static size_t tp_parse_dotted_key(TomlParser *p, char **parts, size_t max_parts) { |
319 | 30 | size_t count = 0; |
320 | 30 | parts[count] = tp_parse_key(p); |
321 | 30 | if (!parts[count] || p->err) return 0; |
322 | 30 | count++; |
323 | | |
324 | 30 | while (p->src[p->pos] == '.' && count < max_parts) { |
325 | 0 | p->pos++; |
326 | 0 | parts[count] = tp_parse_key(p); |
327 | 0 | if (!parts[count] || p->err) { |
328 | 0 | for (size_t i = 0; i < count; i++) free(parts[i]); |
329 | 0 | return 0; |
330 | 0 | } |
331 | 0 | count++; |
332 | 0 | } |
333 | 30 | return count; |
334 | 30 | } |
335 | | |
336 | | /* ── Main TOML parse function ── */ |
337 | 15 | LatValue toml_ops_parse(const char *toml_str, char **err) { |
338 | 15 | TomlParser p = { .src = toml_str, .pos = 0, .err = NULL }; |
339 | 15 | LatValue root = value_map_new(); |
340 | 15 | LatMap *current_table = root.as.map.map; |
341 | | |
342 | 45 | while (p.src[p.pos] != '\0') { |
343 | 30 | tp_skip_ws_and_newlines(&p); |
344 | 30 | tp_skip_comment(&p); |
345 | 30 | if (p.src[p.pos] == '\0') break; |
346 | 30 | if (p.src[p.pos] == '\n' || p.src[p.pos] == '\r') { p.pos++; continue; } |
347 | | |
348 | 30 | if (p.err) break; |
349 | | |
350 | | /* Table header: [key] or [[key]] */ |
351 | 30 | if (p.src[p.pos] == '[') { |
352 | 3 | bool is_array_table = false; |
353 | 3 | p.pos++; |
354 | 3 | if (p.src[p.pos] == '[') { |
355 | 0 | is_array_table = true; |
356 | 0 | p.pos++; |
357 | 0 | } |
358 | 3 | tp_skip_ws(&p); |
359 | | |
360 | 3 | char *parts[32]; |
361 | 3 | size_t count = tp_parse_dotted_key(&p, parts, 32); |
362 | 3 | if (count == 0 || p.err) { |
363 | 0 | value_free(&root); |
364 | 0 | *err = p.err ? p.err : strdup("toml_parse: invalid table header"); |
365 | 0 | return value_unit(); |
366 | 0 | } |
367 | | |
368 | 3 | tp_skip_ws(&p); |
369 | | |
370 | 3 | if (is_array_table) { |
371 | 0 | if (p.src[p.pos] != ']' || p.src[p.pos + 1] != ']') { |
372 | 0 | for (size_t i = 0; i < count; i++) free(parts[i]); |
373 | 0 | tp_error(&p, "expected ']]'"); |
374 | 0 | break; |
375 | 0 | } |
376 | 0 | p.pos += 2; |
377 | | |
378 | | /* Navigate to parent */ |
379 | 0 | LatMap *parent = root.as.map.map; |
380 | 0 | for (size_t i = 0; i + 1 < count; i++) { |
381 | 0 | LatValue *existing = (LatValue *)lat_map_get(parent, parts[i]); |
382 | 0 | if (existing && existing->type == VAL_MAP) { |
383 | 0 | parent = existing->as.map.map; |
384 | 0 | } else { |
385 | 0 | LatValue sub = value_map_new(); |
386 | 0 | lat_map_set(parent, parts[i], &sub); |
387 | 0 | LatValue *ins = (LatValue *)lat_map_get(parent, parts[i]); |
388 | 0 | parent = ins->as.map.map; |
389 | 0 | } |
390 | 0 | } |
391 | 0 | char *arr_key = parts[count - 1]; |
392 | 0 | LatValue *arr_val = (LatValue *)lat_map_get(parent, arr_key); |
393 | 0 | if (!arr_val || arr_val->type != VAL_ARRAY) { |
394 | 0 | LatValue empty_arr = value_array(NULL, 0); |
395 | 0 | lat_map_set(parent, arr_key, &empty_arr); |
396 | 0 | arr_val = (LatValue *)lat_map_get(parent, arr_key); |
397 | 0 | } |
398 | | /* Push a new map entry to this array */ |
399 | 0 | LatValue new_entry = value_map_new(); |
400 | 0 | if (arr_val->as.array.len >= arr_val->as.array.cap) { |
401 | 0 | size_t old_cap = arr_val->as.array.cap; |
402 | 0 | arr_val->as.array.cap = old_cap < 4 ? 4 : old_cap * 2; |
403 | 0 | arr_val->as.array.elems = realloc(arr_val->as.array.elems, |
404 | 0 | arr_val->as.array.cap * sizeof(LatValue)); |
405 | 0 | } |
406 | 0 | arr_val->as.array.elems[arr_val->as.array.len] = new_entry; |
407 | 0 | current_table = arr_val->as.array.elems[arr_val->as.array.len].as.map.map; |
408 | 0 | arr_val->as.array.len++; |
409 | 3 | } else { |
410 | 3 | if (p.src[p.pos] != ']') { |
411 | 0 | for (size_t i = 0; i < count; i++) free(parts[i]); |
412 | 0 | tp_error(&p, "expected ']'"); |
413 | 0 | break; |
414 | 0 | } |
415 | 3 | p.pos++; |
416 | 3 | current_table = tp_ensure_table(&root, parts, count); |
417 | 3 | } |
418 | | |
419 | 6 | for (size_t i = 0; i < count; i++) free(parts[i]); |
420 | 3 | tp_skip_line_rest(&p); |
421 | 3 | continue; |
422 | 3 | } |
423 | | |
424 | | /* Key = Value */ |
425 | 27 | if (isalnum((unsigned char)p.src[p.pos]) || p.src[p.pos] == '"' || |
426 | 27 | p.src[p.pos] == '\'' || p.src[p.pos] == '_' || p.src[p.pos] == '-') { |
427 | | |
428 | 27 | char *parts[32]; |
429 | 27 | size_t count = tp_parse_dotted_key(&p, parts, 32); |
430 | 27 | if (count == 0 || p.err) { |
431 | 0 | value_free(&root); |
432 | 0 | *err = p.err ? p.err : strdup("toml_parse: invalid key"); |
433 | 0 | return value_unit(); |
434 | 0 | } |
435 | | |
436 | 27 | tp_skip_ws(&p); |
437 | 27 | if (p.src[p.pos] != '=') { |
438 | 0 | for (size_t i = 0; i < count; i++) free(parts[i]); |
439 | 0 | tp_error(&p, "expected '='"); |
440 | 0 | break; |
441 | 0 | } |
442 | 27 | p.pos++; |
443 | 27 | tp_skip_ws(&p); |
444 | | |
445 | 27 | LatValue val = tp_parse_value(&p); |
446 | 27 | if (p.err) { |
447 | 0 | for (size_t i = 0; i < count; i++) free(parts[i]); |
448 | 0 | value_free(&val); |
449 | 0 | break; |
450 | 0 | } |
451 | | |
452 | | /* Navigate dotted key path */ |
453 | 27 | LatMap *target = current_table; |
454 | 27 | for (size_t i = 0; i + 1 < count; i++) { |
455 | 0 | LatValue *existing = (LatValue *)lat_map_get(target, parts[i]); |
456 | 0 | if (existing && existing->type == VAL_MAP) { |
457 | 0 | target = existing->as.map.map; |
458 | 0 | } else { |
459 | 0 | LatValue sub = value_map_new(); |
460 | 0 | lat_map_set(target, parts[i], &sub); |
461 | 0 | LatValue *ins = (LatValue *)lat_map_get(target, parts[i]); |
462 | 0 | target = ins->as.map.map; |
463 | 0 | } |
464 | 0 | } |
465 | 27 | lat_map_set(target, parts[count - 1], &val); |
466 | | |
467 | 54 | for (size_t i = 0; i < count; i++) free(parts[i]); |
468 | 27 | tp_skip_line_rest(&p); |
469 | 27 | continue; |
470 | 27 | } |
471 | | |
472 | 0 | if (p.src[p.pos] == '#') { tp_skip_comment(&p); continue; } |
473 | 0 | if (p.src[p.pos] == '\n' || p.src[p.pos] == '\r') { p.pos++; continue; } |
474 | | |
475 | 0 | tp_error(&p, "unexpected character"); |
476 | 0 | break; |
477 | 0 | } |
478 | | |
479 | 15 | if (p.err) { |
480 | 0 | value_free(&root); |
481 | 0 | *err = p.err; |
482 | 0 | return value_unit(); |
483 | 0 | } |
484 | | |
485 | 15 | return root; |
486 | 15 | } |
487 | | |
488 | | /* ======================================================================== |
489 | | * TOML Stringify |
490 | | * ======================================================================== */ |
491 | | |
492 | | typedef struct { |
493 | | char *buf; |
494 | | size_t len; |
495 | | size_t cap; |
496 | | } TomlBuf; |
497 | | |
498 | 3 | static void tb_init(TomlBuf *b) { |
499 | 3 | b->cap = 256; |
500 | 3 | b->buf = malloc(b->cap); |
501 | 3 | b->len = 0; |
502 | 3 | b->buf[0] = '\0'; |
503 | 3 | } |
504 | | |
505 | 42 | static void tb_append(TomlBuf *b, const char *s) { |
506 | 42 | size_t slen = strlen(s); |
507 | 42 | while (b->len + slen + 1 > b->cap) { b->cap *= 2; b->buf = realloc(b->buf, b->cap); } |
508 | 42 | memcpy(b->buf + b->len, s, slen); |
509 | 42 | b->len += slen; |
510 | 42 | b->buf[b->len] = '\0'; |
511 | 42 | } |
512 | | |
513 | | static void tb_appendf(TomlBuf *b, const char *fmt, ...) __attribute__((format(printf, 2, 3))); |
514 | 3 | static void tb_appendf(TomlBuf *b, const char *fmt, ...) { |
515 | 3 | va_list ap; |
516 | 3 | va_start(ap, fmt); |
517 | 3 | char tmp[256]; |
518 | 3 | vsnprintf(tmp, sizeof(tmp), fmt, ap); |
519 | 3 | va_end(ap); |
520 | 3 | tb_append(b, tmp); |
521 | 3 | } |
522 | | |
523 | 3 | static void tb_append_escaped(TomlBuf *b, const char *s) { |
524 | 3 | tb_append(b, "\""); |
525 | 18 | while (*s) { |
526 | 15 | switch (*s) { |
527 | 0 | case '"': tb_append(b, "\\\""); break; |
528 | 0 | case '\\': tb_append(b, "\\\\"); break; |
529 | 0 | case '\n': tb_append(b, "\\n"); break; |
530 | 0 | case '\t': tb_append(b, "\\t"); break; |
531 | 0 | case '\r': tb_append(b, "\\r"); break; |
532 | 15 | default: { |
533 | 15 | char c[2] = { *s, '\0' }; |
534 | 15 | tb_append(b, c); |
535 | 15 | } |
536 | 15 | } |
537 | 15 | s++; |
538 | 15 | } |
539 | 3 | tb_append(b, "\""); |
540 | 3 | } |
541 | | |
542 | | static void toml_stringify_value(TomlBuf *b, const LatValue *val); |
543 | | |
544 | 6 | static void toml_stringify_value(TomlBuf *b, const LatValue *val) { |
545 | 6 | switch (val->type) { |
546 | 3 | case VAL_STR: |
547 | 3 | tb_append_escaped(b, val->as.str_val); |
548 | 3 | break; |
549 | 3 | case VAL_INT: |
550 | 3 | tb_appendf(b, "%lld", (long long)val->as.int_val); |
551 | 3 | break; |
552 | 0 | case VAL_FLOAT: |
553 | 0 | tb_appendf(b, "%g", val->as.float_val); |
554 | 0 | break; |
555 | 0 | case VAL_BOOL: |
556 | 0 | tb_append(b, val->as.bool_val ? "true" : "false"); |
557 | 0 | break; |
558 | 0 | case VAL_ARRAY: { |
559 | 0 | tb_append(b, "["); |
560 | 0 | for (size_t i = 0; i < val->as.array.len; i++) { |
561 | 0 | if (i > 0) tb_append(b, ", "); |
562 | 0 | toml_stringify_value(b, &val->as.array.elems[i]); |
563 | 0 | } |
564 | 0 | tb_append(b, "]"); |
565 | 0 | break; |
566 | 0 | } |
567 | 0 | case VAL_MAP: { |
568 | 0 | tb_append(b, "{"); |
569 | 0 | bool first = true; |
570 | 0 | for (size_t i = 0; i < val->as.map.map->cap; i++) { |
571 | 0 | if (val->as.map.map->entries[i].state != MAP_OCCUPIED) continue; |
572 | 0 | if (!first) tb_append(b, ", "); |
573 | 0 | first = false; |
574 | 0 | tb_append(b, val->as.map.map->entries[i].key); |
575 | 0 | tb_append(b, " = "); |
576 | 0 | toml_stringify_value(b, (LatValue *)val->as.map.map->entries[i].value); |
577 | 0 | } |
578 | 0 | tb_append(b, "}"); |
579 | 0 | break; |
580 | 0 | } |
581 | 0 | default: |
582 | 0 | tb_append(b, "\"\""); |
583 | 0 | break; |
584 | 6 | } |
585 | 6 | } |
586 | | |
587 | 3 | static void toml_stringify_table(TomlBuf *b, const LatValue *val, const char *prefix) { |
588 | 3 | if (val->type != VAL_MAP) return; |
589 | | |
590 | | /* First pass: simple key = value */ |
591 | 51 | for (size_t i = 0; i < val->as.map.map->cap; i++) { |
592 | 48 | if (val->as.map.map->entries[i].state != MAP_OCCUPIED) continue; |
593 | 6 | LatValue *v = (LatValue *)val->as.map.map->entries[i].value; |
594 | 6 | if (v->type == VAL_MAP) continue; |
595 | | /* Skip arrays of all-maps (array of tables) */ |
596 | 6 | if (v->type == VAL_ARRAY && v->as.array.len > 0) { |
597 | 0 | bool all_maps = true; |
598 | 0 | for (size_t j = 0; j < v->as.array.len; j++) { |
599 | 0 | if (v->as.array.elems[j].type != VAL_MAP) { all_maps = false; break; } |
600 | 0 | } |
601 | 0 | if (all_maps) continue; |
602 | 0 | } |
603 | 6 | tb_append(b, val->as.map.map->entries[i].key); |
604 | 6 | tb_append(b, " = "); |
605 | 6 | toml_stringify_value(b, v); |
606 | 6 | tb_append(b, "\n"); |
607 | 6 | } |
608 | | |
609 | | /* Second pass: sub-tables */ |
610 | 51 | for (size_t i = 0; i < val->as.map.map->cap; i++) { |
611 | 48 | if (val->as.map.map->entries[i].state != MAP_OCCUPIED) continue; |
612 | 6 | LatValue *v = (LatValue *)val->as.map.map->entries[i].value; |
613 | 6 | const char *key = val->as.map.map->entries[i].key; |
614 | | |
615 | 6 | if (v->type == VAL_MAP) { |
616 | 0 | char *full_key; |
617 | 0 | if (prefix[0] != '\0') { |
618 | 0 | size_t flen = strlen(prefix) + strlen(key) + 2; |
619 | 0 | full_key = malloc(flen); |
620 | 0 | snprintf(full_key, flen, "%s.%s", prefix, key); |
621 | 0 | } else { |
622 | 0 | full_key = strdup(key); |
623 | 0 | } |
624 | 0 | tb_append(b, "\n["); |
625 | 0 | tb_append(b, full_key); |
626 | 0 | tb_append(b, "]\n"); |
627 | 0 | toml_stringify_table(b, v, full_key); |
628 | 0 | free(full_key); |
629 | 6 | } else if (v->type == VAL_ARRAY && v->as.array.len > 0) { |
630 | 0 | bool all_maps = true; |
631 | 0 | for (size_t j = 0; j < v->as.array.len; j++) { |
632 | 0 | if (v->as.array.elems[j].type != VAL_MAP) { all_maps = false; break; } |
633 | 0 | } |
634 | 0 | if (all_maps) { |
635 | 0 | char *full_key; |
636 | 0 | if (prefix[0] != '\0') { |
637 | 0 | size_t flen = strlen(prefix) + strlen(key) + 2; |
638 | 0 | full_key = malloc(flen); |
639 | 0 | snprintf(full_key, flen, "%s.%s", prefix, key); |
640 | 0 | } else { |
641 | 0 | full_key = strdup(key); |
642 | 0 | } |
643 | 0 | for (size_t j = 0; j < v->as.array.len; j++) { |
644 | 0 | tb_append(b, "\n[["); |
645 | 0 | tb_append(b, full_key); |
646 | 0 | tb_append(b, "]]\n"); |
647 | 0 | toml_stringify_table(b, &v->as.array.elems[j], full_key); |
648 | 0 | } |
649 | 0 | free(full_key); |
650 | 0 | } |
651 | 0 | } |
652 | 6 | } |
653 | 3 | } |
654 | | |
655 | 6 | char *toml_ops_stringify(const LatValue *val, char **err) { |
656 | 6 | if (val->type != VAL_MAP) { |
657 | 3 | *err = strdup("toml_stringify: value must be a Map"); |
658 | 3 | return NULL; |
659 | 3 | } |
660 | 3 | TomlBuf b; |
661 | 3 | tb_init(&b); |
662 | 3 | toml_stringify_table(&b, val, ""); |
663 | 3 | return b.buf; |
664 | 6 | } |