prop_extern.c revision 1.1 1 /* $NetBSD: prop_extern.c,v 1.1 2025/05/13 13:26:12 thorpej Exp $ */
2
3 /*-
4 * Copyright (c) 2006, 2007, 2025 The NetBSD Foundation, Inc.
5 * All rights reserved.
6 *
7 * This code is derived from software contributed to The NetBSD Foundation
8 * by Jason R. Thorpe.
9 *
10 * Redistribution and use in source and binary forms, with or without
11 * modification, are permitted provided that the following conditions
12 * are met:
13 * 1. Redistributions of source code must retain the above copyright
14 * notice, this list of conditions and the following disclaimer.
15 * 2. Redistributions in binary form must reproduce the above copyright
16 * notice, this list of conditions and the following disclaimer in the
17 * documentation and/or other materials provided with the distribution.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
20 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
21 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
22 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
23 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 * POSSIBILITY OF SUCH DAMAGE.
30 */
31
32 #if !defined(_KERNEL) && !defined(_STANDALONE)
33 #include <sys/stat.h>
34 #include <errno.h>
35 #include <fcntl.h>
36 #include <limits.h>
37 #include <unistd.h>
38 #endif /* !_KERNEL && !_STANDALONE */
39
40 #include "prop_object_impl.h"
41 #include <prop/prop_object.h>
42
43 #define BUF_EXPAND 256
44 #define PLISTTMP "/.plistXXXXXX"
45
46 static prop_format_t _prop_format_default = PROP_FORMAT_XML;
47
48 /*
49 * _prop_extern_append_char --
50 * Append a single character to the externalize buffer.
51 */
52 bool
53 _prop_extern_append_char(
54 struct _prop_object_externalize_context *ctx, unsigned char c)
55 {
56
57 _PROP_ASSERT(ctx->poec_capacity != 0);
58 _PROP_ASSERT(ctx->poec_buf != NULL);
59 _PROP_ASSERT(ctx->poec_len <= ctx->poec_capacity);
60
61 if (ctx->poec_len == ctx->poec_capacity) {
62 char *cp = _PROP_REALLOC(ctx->poec_buf,
63 ctx->poec_capacity + BUF_EXPAND,
64 M_TEMP);
65 if (cp == NULL) {
66 return false;
67 }
68 ctx->poec_capacity = ctx->poec_capacity + BUF_EXPAND;
69 ctx->poec_buf = cp;
70 }
71
72 ctx->poec_buf[ctx->poec_len++] = c;
73
74 return true;
75 }
76
77 /*
78 * _prop_extern_append_cstring --
79 * Append a C string to the externalize buffer.
80 */
81 bool
82 _prop_extern_append_cstring(
83 struct _prop_object_externalize_context *ctx, const char *cp)
84 {
85
86 while (*cp != '\0') {
87 if (_prop_extern_append_char(ctx,
88 (unsigned char)*cp) == false) {
89 return false;
90 }
91 cp++;
92 }
93 return true;
94 }
95
96 /*
97 * _prop_json_extern_append_encoded_cstring --
98 * Append a C string to the externalize buffer, JSON-encoded.
99 */
100 static bool
101 _prop_json_extern_append_escu(
102 struct _prop_object_externalize_context *ctx, uint16_t val)
103 {
104 char tmpstr[sizeof("\\uXXXX")];
105
106 snprintf(tmpstr, sizeof(tmpstr), "\\u%04X", val);
107 return _prop_extern_append_cstring(ctx, tmpstr);
108 }
109
110 static bool
111 _prop_json_extern_append_encoded_cstring(
112 struct _prop_object_externalize_context *ctx, const char *cp)
113 {
114 bool esc;
115 unsigned char ch;
116
117 for (; (ch = *cp) != '\0'; cp++) {
118 esc = true;
119 switch (ch) {
120 /*
121 * First, the two explicit exclusions. They must be
122 * escaped.
123 */
124 case '"': /* U+0022 quotation mark */
125 goto emit;
126
127 case '\\': /* U+005C reverse solidus */
128 goto emit;
129
130 /*
131 * And some special cases that are explcit in the grammar.
132 */
133 case '/': /* U+002F solidus (XXX this one seems silly) */
134 goto emit;
135
136 case 0x08: /* U+0008 backspace */
137 ch = 'b';
138 goto emit;
139
140 case 0x0c: /* U+000C form feed */
141 ch = 'f';
142 goto emit;
143
144 case 0x0a: /* U+000A line feed */
145 ch = 'n';
146 goto emit;
147
148 case 0x0d: /* U+000D carriage return */
149 ch = 'r';
150 goto emit;
151
152 case 0x09: /* U+0009 tab */
153 ch = 't';
154 goto emit;
155
156 default:
157 /*
158 * \u-escape all other single-byte ASCII control
159 * characters, per RFC 8259:
160 *
161 * <quote>
162 * All Unicode characters may be placed within the
163 * quotation marks, except for the characters that
164 * MUST be escaped: quotation mark, reverse solidus,
165 * and the control characters (U+0000 through U+001F).
166 * </quote>
167 */
168 if (ch < 0x20) {
169 if (_prop_json_extern_append_escu(ctx,
170 ch) == false) {
171 return false;
172 }
173 break;
174 }
175 /*
176 * We're going to just treat everything else like
177 * UTF-8 (we've been handed a C-string, after all)
178 * and pretend it will be OK.
179 */
180 esc = false;
181 emit:
182 if ((esc && _prop_extern_append_char(ctx,
183 '\\') == false) ||
184 _prop_extern_append_char(ctx, ch) == false) {
185 return false;
186 }
187 break;
188 }
189 }
190
191 return true;
192 }
193
194 /*
195 * _prop_xml_extern_append_encoded_cstring --
196 * Append a C string to the externalize buffer, XML-encoded.
197 */
198 static bool
199 _prop_xml_extern_append_encoded_cstring(
200 struct _prop_object_externalize_context *ctx, const char *cp)
201 {
202 bool rv;
203 unsigned char ch;
204
205 for (rv = true; rv && (ch = *cp) != '\0'; cp++) {
206 switch (ch) {
207 case '<':
208 rv = _prop_extern_append_cstring(ctx, "<");
209 break;
210 case '>':
211 rv = _prop_extern_append_cstring(ctx, ">");
212 break;
213 case '&':
214 rv = _prop_extern_append_cstring(ctx, "&");
215 break;
216 default:
217 rv = _prop_extern_append_char(ctx, ch);
218 break;
219 }
220 }
221
222 return rv;
223 }
224
225 /*
226 * _prop_extern_append_encoded_cstring --
227 * Append a C string to the externalize buffer, encoding it for
228 * the selected format.
229 */
230 bool
231 _prop_extern_append_encoded_cstring(
232 struct _prop_object_externalize_context *ctx, const char *cp)
233 {
234 _PROP_ASSERT(ctx->poec_format == PROP_FORMAT_XML ||
235 ctx->poec_format == PROP_FORMAT_JSON);
236
237 switch (ctx->poec_format) {
238 case PROP_FORMAT_JSON:
239 return _prop_json_extern_append_encoded_cstring(ctx, cp);
240
241 default: /* PROP_FORMAT_XML */
242 return _prop_xml_extern_append_encoded_cstring(ctx, cp);
243 }
244 }
245
246 /*
247 * _prop_extern_start_line --
248 * Append the start-of-line character sequence.
249 */
250 bool
251 _prop_extern_start_line(
252 struct _prop_object_externalize_context *ctx)
253 {
254 unsigned int i;
255
256 for (i = 0; i < ctx->poec_depth; i++) {
257 if (_prop_extern_append_char(ctx, '\t') == false) {
258 return false;
259 }
260 }
261 return true;
262 }
263
264 /*
265 * _prop_extern_end_line --
266 * Append the end-of-line character sequence.
267 */
268 bool
269 _prop_extern_end_line(
270 struct _prop_object_externalize_context *ctx, const char *trailer)
271 {
272 if (trailer != NULL &&
273 _prop_extern_append_cstring(ctx, trailer) == false) {
274 return false;
275 }
276 return _prop_extern_append_char(ctx, '\n');
277 }
278
279 /*
280 * _prop_extern_append_start_tag --
281 * Append an item's start tag to the externalize buffer.
282 */
283 bool
284 _prop_extern_append_start_tag(
285 struct _prop_object_externalize_context *ctx,
286 const struct _prop_object_type_tags *tags,
287 const char *tagattrs)
288 {
289 bool rv;
290
291 _PROP_ASSERT(ctx->poec_format == PROP_FORMAT_XML ||
292 ctx->poec_format == PROP_FORMAT_JSON);
293
294 switch (ctx->poec_format) {
295 case PROP_FORMAT_JSON:
296 rv = tags->json_open_tag == NULL ||
297 _prop_extern_append_cstring(ctx, tags->json_open_tag);
298 break;
299
300 default: /* PROP_FORMAT_XML */
301 rv = _prop_extern_append_char(ctx, '<') &&
302 _prop_extern_append_cstring(ctx, tags->xml_tag) &&
303 (tagattrs == NULL ||
304 (_prop_extern_append_char(ctx, ' ') &&
305 _prop_extern_append_cstring(ctx, tagattrs))) &&
306 _prop_extern_append_char(ctx, '>');
307 break;
308 }
309
310 return rv;
311 }
312
313 /*
314 * _prop_extern_append_end_tag --
315 * Append an item's end tag to the externalize buffer.
316 */
317 bool
318 _prop_extern_append_end_tag(
319 struct _prop_object_externalize_context *ctx,
320 const struct _prop_object_type_tags *tags)
321 {
322 bool rv;
323
324 _PROP_ASSERT(ctx->poec_format == PROP_FORMAT_XML ||
325 ctx->poec_format == PROP_FORMAT_JSON);
326
327 switch (ctx->poec_format) {
328 case PROP_FORMAT_JSON:
329 rv = tags->json_close_tag == NULL ||
330 _prop_extern_append_cstring(ctx, tags->json_close_tag);
331 break;
332
333 default: /* PROP_FORMAT_XML */
334 rv = _prop_extern_append_char(ctx, '<') &&
335 _prop_extern_append_char(ctx, '/') &&
336 _prop_extern_append_cstring(ctx, tags->xml_tag) &&
337 _prop_extern_append_char(ctx, '>');
338 break;
339 }
340
341 return rv;
342 }
343
344 /*
345 * _prop_extern_append_empty_tag --
346 * Append an item's empty tag to the externalize buffer.
347 */
348 bool
349 _prop_extern_append_empty_tag(
350 struct _prop_object_externalize_context *ctx,
351 const struct _prop_object_type_tags *tags)
352 {
353 bool rv;
354
355 _PROP_ASSERT(ctx->poec_format == PROP_FORMAT_XML ||
356 ctx->poec_format == PROP_FORMAT_JSON);
357
358 switch (ctx->poec_format) {
359 case PROP_FORMAT_JSON:
360 if (tags->json_open_tag == NULL ||
361 _prop_extern_append_cstring(ctx,
362 tags->json_open_tag) == false) {
363 return false;
364 }
365 if (tags->json_empty_sep != NULL &&
366 _prop_extern_append_cstring(ctx,
367 tags->json_empty_sep) == false) {
368 return false;
369 }
370 if (tags->json_close_tag != NULL) {
371 rv = _prop_extern_append_cstring(ctx,
372 tags->json_close_tag);
373 } else {
374 rv = true;
375 }
376 break;
377
378 default: /* PROP_FORMAT_XML */
379 rv = _prop_extern_append_char(ctx, '<') &&
380 _prop_extern_append_cstring(ctx, tags->xml_tag) &&
381 _prop_extern_append_char(ctx, '/') &&
382 _prop_extern_append_char(ctx, '>');
383 break;
384 }
385
386 return rv;
387 }
388
389 static const struct _prop_object_type_tags _plist_type_tags = {
390 .xml_tag = "plist",
391 };
392
393 /*
394 * _prop_extern_append_header --
395 * Append the header to the externalize buffer.
396 */
397 static bool
398 _prop_extern_append_header(struct _prop_object_externalize_context *ctx)
399 {
400 static const char _plist_xml_header[] =
401 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
402 "<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n";
403
404 if (ctx->poec_format != PROP_FORMAT_XML) {
405 return true;
406 }
407
408 if (_prop_extern_append_cstring(ctx, _plist_xml_header) == false ||
409 _prop_extern_append_start_tag(ctx,
410 &_plist_type_tags,
411 "version=\"1.0\"") == false ||
412 _prop_extern_append_char(ctx, '\n') == false) {
413 return false;
414 }
415
416 return true;
417 }
418
419 /*
420 * _prop_extern_append_footer --
421 * Append the footer to the externalize buffer. This also
422 * NUL-terminates the buffer.
423 */
424 static bool
425 _prop_extern_append_footer(struct _prop_object_externalize_context *ctx)
426 {
427 if (_prop_extern_end_line(ctx, NULL) == false) {
428 return false;
429 }
430
431 if (ctx->poec_format == PROP_FORMAT_XML) {
432 if (_prop_extern_append_end_tag(ctx,
433 &_plist_type_tags) == false ||
434 _prop_extern_end_line(ctx, NULL) == false) {
435 return false;
436 }
437 }
438
439 return _prop_extern_append_char(ctx, '\0');
440 }
441
442 /*
443 * _prop_extern_context_alloc --
444 * Allocate an externalize context.
445 */
446 static struct _prop_object_externalize_context *
447 _prop_extern_context_alloc(prop_format_t fmt)
448 {
449 struct _prop_object_externalize_context *ctx;
450
451 ctx = _PROP_MALLOC(sizeof(*ctx), M_TEMP);
452 if (ctx != NULL) {
453 ctx->poec_buf = _PROP_MALLOC(BUF_EXPAND, M_TEMP);
454 if (ctx->poec_buf == NULL) {
455 _PROP_FREE(ctx, M_TEMP);
456 return NULL;
457 }
458 ctx->poec_len = 0;
459 ctx->poec_capacity = BUF_EXPAND;
460 ctx->poec_depth = 0;
461 ctx->poec_format = fmt;
462 }
463 return ctx;
464 }
465
466 /*
467 * _prop_extern_context_free --
468 * Free an externalize context.
469 */
470 static void
471 _prop_extern_context_free(struct _prop_object_externalize_context *ctx)
472 {
473 /* Buffer is always freed by the caller. */
474 _PROP_FREE(ctx, M_TEMP);
475 }
476
477 /*
478 * _prop_object_externalize --
479 * Externalize an object, returning a NUL-terminated buffer
480 * containing the serialized data in either XML or JSON format.
481 * The buffer is allocated with the M_TEMP memory type.
482 */
483 char *
484 _prop_object_externalize(struct _prop_object *obj, prop_format_t fmt)
485 {
486 struct _prop_object_externalize_context *ctx;
487 char *cp = NULL;
488
489 if (obj == NULL || obj->po_type->pot_extern == NULL) {
490 return NULL;
491 }
492 if (fmt != PROP_FORMAT_XML && fmt != PROP_FORMAT_JSON) {
493 return NULL;
494 }
495
496 ctx = _prop_extern_context_alloc(fmt);
497 if (ctx == NULL) {
498 return NULL;
499 }
500
501 if (_prop_extern_append_header(ctx) == false ||
502 obj->po_type->pot_extern(ctx, obj) == false ||
503 _prop_extern_append_footer(ctx) == false) {
504 /* We are responsible for releasing the buffer. */
505 _PROP_FREE(ctx->poec_buf, M_TEMP);
506 goto bad;
507 }
508
509 cp = ctx->poec_buf;
510 bad:
511 _prop_extern_context_free(ctx);
512 return cp;
513 }
514
515 #if !defined(_KERNEL) && !defined(_STANDALONE)
516 /*
517 * _prop_extern_file_dirname --
518 * dirname(3), basically. We have to roll our own because the
519 * system dirname(3) isn't reentrant.
520 */
521 static void
522 _prop_extern_file_dirname(const char *path, char *result)
523 {
524 const char *lastp;
525 size_t len;
526
527 /*
528 * If `path' is a NULL pointer or points to an empty string,
529 * return ".".
530 */
531 if (path == NULL || *path == '\0') {
532 goto singledot;
533 }
534
535 /* Strip trailing slashes, if any. */
536 lastp = path + strlen(path) - 1;
537 while (lastp != path && *lastp == '/') {
538 lastp--;
539 }
540
541 /* Terminate path at the last occurrence of '/'. */
542 do {
543 if (*lastp == '/') {
544 /* Strip trailing slashes, if any. */
545 while (lastp != path && *lastp == '/') {
546 lastp--;
547 }
548
549 /* ...and copy the result into the result buffer. */
550 len = (lastp - path) + 1 /* last char */;
551 if (len > (PATH_MAX - 1)) {
552 len = PATH_MAX - 1;
553 }
554
555 memcpy(result, path, len);
556 result[len] = '\0';
557 return;
558 }
559 } while (--lastp >= path);
560
561 /* No /'s found, return ".". */
562 singledot:
563 strcpy(result, ".");
564 }
565
566 /*
567 * _prop_extern_write_file --
568 * Write an externalized object to the specified file.
569 * The file is written atomically from the caller's perspective,
570 * and the mode set to 0666 modified by the caller's umask.
571 */
572 static bool
573 _prop_extern_write_file(const char *fname, const char *data, size_t len)
574 {
575 char tname_store[PATH_MAX];
576 char *tname = NULL;
577 int fd = -1;
578 int save_errno;
579 mode_t myumask;
580 bool rv = false;
581
582 if (len > SSIZE_MAX) {
583 errno = EFBIG;
584 return false;
585 }
586
587 /*
588 * Get the directory name where the file is to be written
589 * and create the temporary file.
590 */
591 _prop_extern_file_dirname(fname, tname_store);
592 if (strlen(tname_store) + strlen(PLISTTMP) >= sizeof(tname_store)) {
593 errno = ENAMETOOLONG;
594 return false;
595 }
596 strcat(tname_store, PLISTTMP);
597
598 if ((fd = mkstemp(tname_store)) == -1) {
599 return false;
600 }
601 tname = tname_store;
602
603 if (write(fd, data, len) != (ssize_t)len) {
604 goto bad;
605 }
606
607 if (fsync(fd) == -1) {
608 goto bad;
609 }
610
611 myumask = umask(0);
612 (void)umask(myumask);
613 if (fchmod(fd, 0666 & ~myumask) == -1) {
614 goto bad;
615 }
616
617 if (rename(tname, fname) == -1) {
618 goto bad;
619 }
620 tname = NULL;
621
622 rv = true;
623
624 bad:
625 save_errno = errno;
626 if (fd != -1) {
627 (void) close(fd);
628 }
629 if (tname != NULL) {
630 (void) unlink(tname);
631 }
632 errno = save_errno;
633 return rv;
634 }
635
636 /*
637 * _prop_object_externalize_to_file --
638 * Externalize an object to the specified file.
639 */
640 bool
641 _prop_object_externalize_to_file(struct _prop_object *obj, const char *fname,
642 prop_format_t fmt)
643 {
644 char *data = _prop_object_externalize(obj, fmt);
645 if (data == NULL) {
646 return false;
647 }
648 bool rv = _prop_extern_write_file(fname, data, strlen(data));
649 int save_errno = errno;
650 _PROP_FREE(data, M_TEMP);
651 errno = save_errno;
652
653 return rv;
654 }
655
656 /*
657 * prop_object_externalize_to_file --
658 * Externalize an object to the specifed file in the default format.
659 */
660 _PROP_EXPORT bool
661 prop_object_externalize_to_file(prop_object_t po, const char *fname)
662 {
663 return _prop_object_externalize_to_file((struct _prop_object *)po,
664 fname, _prop_format_default);
665 }
666
667 /*
668 * prop_object_externalize_to_file_with_format --
669 * Externalize an object to the specifed file in the specified format.
670 */
671 _PROP_EXPORT bool
672 prop_object_externalize_to_file_with_format(prop_object_t po,
673 const char *fname, prop_format_t fmt)
674 {
675 return _prop_object_externalize_to_file((struct _prop_object *)po,
676 fname, fmt);
677 }
678 #endif /* !_KERNEL && !_STANDALONE */
679
680 /*
681 * prop_object_externalize --
682 * Externalize an object in the default format.
683 */
684 _PROP_EXPORT char *
685 prop_object_externalize(prop_object_t po)
686 {
687 return _prop_object_externalize((struct _prop_object *)po,
688 _prop_format_default);
689 }
690
691 /*
692 * prop_object_externalize_with_format --
693 * Externalize an object in the specified format.
694 */
695 _PROP_EXPORT char *
696 prop_object_externalize_with_format(prop_object_t po, prop_format_t fmt)
697 {
698 return _prop_object_externalize((struct _prop_object *)po, fmt);
699 }
700