/* $NetBSD: prop_extern.c,v 1.2 2025/05/14 03:25:46 thorpej Exp $ */
/*-
* Copyright (c) 2006, 2007, 2025 The NetBSD Foundation, Inc.
* All rights reserved.
*
* This code is derived from software contributed to The NetBSD Foundation
* by Jason R. Thorpe.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include "prop_object_impl.h"
#include <prop/prop_object.h>
#if !defined(_KERNEL) && !defined(_STANDALONE)
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <unistd.h>
#endif /* !_KERNEL && !_STANDALONE */
#define BUF_EXPAND 256
#define PLISTTMP "/.plistXXXXXX"
static prop_format_t _prop_format_default = PROP_FORMAT_XML;
/*
* _prop_extern_append_char --
* Append a single character to the externalize buffer.
*/
bool
_prop_extern_append_char(
struct _prop_object_externalize_context *ctx, unsigned char c)
{
_PROP_ASSERT(ctx->poec_capacity != 0);
_PROP_ASSERT(ctx->poec_buf != NULL);
_PROP_ASSERT(ctx->poec_len <= ctx->poec_capacity);
if (ctx->poec_len == ctx->poec_capacity) {
char *cp = _PROP_REALLOC(ctx->poec_buf,
ctx->poec_capacity + BUF_EXPAND,
M_TEMP);
if (cp == NULL) {
return false;
}
ctx->poec_capacity = ctx->poec_capacity + BUF_EXPAND;
ctx->poec_buf = cp;
}
ctx->poec_buf[ctx->poec_len++] = c;
return true;
}
/*
* _prop_extern_append_cstring --
* Append a C string to the externalize buffer.
*/
bool
_prop_extern_append_cstring(
struct _prop_object_externalize_context *ctx, const char *cp)
{
while (*cp != '\0') {
if (_prop_extern_append_char(ctx,
(unsigned char)*cp) == false) {
return false;
}
cp++;
}
return true;
}
/*
* _prop_json_extern_append_encoded_cstring --
* Append a C string to the externalize buffer, JSON-encoded.
*/
static bool
_prop_json_extern_append_escu(
struct _prop_object_externalize_context *ctx, uint16_t val)
{
char tmpstr[sizeof("\\uXXXX")];
snprintf(tmpstr, sizeof(tmpstr), "\\u%04X", val);
return _prop_extern_append_cstring(ctx, tmpstr);
}
static bool
_prop_json_extern_append_encoded_cstring(
struct _prop_object_externalize_context *ctx, const char *cp)
{
bool esc;
unsigned char ch;
for (; (ch = *cp) != '\0'; cp++) {
esc = true;
switch (ch) {
/*
* First, the two explicit exclusions. They must be
* escaped.
*/
case '"': /* U+0022 quotation mark */
goto emit;
case '\\': /* U+005C reverse solidus */
goto emit;
/*
* And some special cases that are explcit in the grammar.
*/
case '/': /* U+002F solidus (XXX this one seems silly) */
goto emit;
case 0x08: /* U+0008 backspace */
ch = 'b';
goto emit;
case 0x0c: /* U+000C form feed */
ch = 'f';
goto emit;
case 0x0a: /* U+000A line feed */
ch = 'n';
goto emit;
case 0x0d: /* U+000D carriage return */
ch = 'r';
goto emit;
case 0x09: /* U+0009 tab */
ch = 't';
goto emit;
default:
/*
* \u-escape all other single-byte ASCII control
* characters, per RFC 8259:
*
* <quote>
* All Unicode characters may be placed within the
* quotation marks, except for the characters that
* MUST be escaped: quotation mark, reverse solidus,
* and the control characters (U+0000 through U+001F).
* </quote>
*/
if (ch < 0x20) {
if (_prop_json_extern_append_escu(ctx,
ch) == false) {
return false;
}
break;
}
/*
* We're going to just treat everything else like
* UTF-8 (we've been handed a C-string, after all)
* and pretend it will be OK.
*/
esc = false;
emit:
if ((esc && _prop_extern_append_char(ctx,
'\\') == false) ||
_prop_extern_append_char(ctx, ch) == false) {
return false;
}
break;
}
}
return true;
}
/*
* _prop_xml_extern_append_encoded_cstring --
* Append a C string to the externalize buffer, XML-encoded.
*/
static bool
_prop_xml_extern_append_encoded_cstring(
struct _prop_object_externalize_context *ctx, const char *cp)
{
bool rv;
unsigned char ch;
for (rv = true; rv && (ch = *cp) != '\0'; cp++) {
switch (ch) {
case '<':
rv = _prop_extern_append_cstring(ctx, "<");
break;
case '>':
rv = _prop_extern_append_cstring(ctx, ">");
break;
case '&':
rv = _prop_extern_append_cstring(ctx, "&");
break;
default:
rv = _prop_extern_append_char(ctx, ch);
break;
}
}
return rv;
}
/*
* _prop_extern_append_encoded_cstring --
* Append a C string to the externalize buffer, encoding it for
* the selected format.
*/
bool
_prop_extern_append_encoded_cstring(
struct _prop_object_externalize_context *ctx, const char *cp)
{
_PROP_ASSERT(ctx->poec_format == PROP_FORMAT_XML ||
ctx->poec_format == PROP_FORMAT_JSON);
switch (ctx->poec_format) {
case PROP_FORMAT_JSON:
return _prop_json_extern_append_encoded_cstring(ctx, cp);
default: /* PROP_FORMAT_XML */
return _prop_xml_extern_append_encoded_cstring(ctx, cp);
}
}
/*
* _prop_extern_start_line --
* Append the start-of-line character sequence.
*/
bool
_prop_extern_start_line(
struct _prop_object_externalize_context *ctx)
{
unsigned int i;
for (i = 0; i < ctx->poec_depth; i++) {
if (_prop_extern_append_char(ctx, '\t') == false) {
return false;
}
}
return true;
}
/*
* _prop_extern_end_line --
* Append the end-of-line character sequence.
*/
bool
_prop_extern_end_line(
struct _prop_object_externalize_context *ctx, const char *trailer)
{
if (trailer != NULL &&
_prop_extern_append_cstring(ctx, trailer) == false) {
return false;
}
return _prop_extern_append_char(ctx, '\n');
}
/*
* _prop_extern_append_start_tag --
* Append an item's start tag to the externalize buffer.
*/
bool
_prop_extern_append_start_tag(
struct _prop_object_externalize_context *ctx,
const struct _prop_object_type_tags *tags,
const char *tagattrs)
{
bool rv;
_PROP_ASSERT(ctx->poec_format == PROP_FORMAT_XML ||
ctx->poec_format == PROP_FORMAT_JSON);
switch (ctx->poec_format) {
case PROP_FORMAT_JSON:
rv = tags->json_open_tag == NULL ||
_prop_extern_append_cstring(ctx, tags->json_open_tag);
break;
default: /* PROP_FORMAT_XML */
rv = _prop_extern_append_char(ctx, '<') &&
_prop_extern_append_cstring(ctx, tags->xml_tag) &&
(tagattrs == NULL ||
(_prop_extern_append_char(ctx, ' ') &&
_prop_extern_append_cstring(ctx, tagattrs))) &&
_prop_extern_append_char(ctx, '>');
break;
}
return rv;
}
/*
* _prop_extern_append_end_tag --
* Append an item's end tag to the externalize buffer.
*/
bool
_prop_extern_append_end_tag(
struct _prop_object_externalize_context *ctx,
const struct _prop_object_type_tags *tags)
{
bool rv;
_PROP_ASSERT(ctx->poec_format == PROP_FORMAT_XML ||
ctx->poec_format == PROP_FORMAT_JSON);
switch (ctx->poec_format) {
case PROP_FORMAT_JSON:
rv = tags->json_close_tag == NULL ||
_prop_extern_append_cstring(ctx, tags->json_close_tag);
break;
default: /* PROP_FORMAT_XML */
rv = _prop_extern_append_char(ctx, '<') &&
_prop_extern_append_char(ctx, '/') &&
_prop_extern_append_cstring(ctx, tags->xml_tag) &&
_prop_extern_append_char(ctx, '>');
break;
}
return rv;
}
/*
* _prop_extern_append_empty_tag --
* Append an item's empty tag to the externalize buffer.
*/
bool
_prop_extern_append_empty_tag(
struct _prop_object_externalize_context *ctx,
const struct _prop_object_type_tags *tags)
{
bool rv;
_PROP_ASSERT(ctx->poec_format == PROP_FORMAT_XML ||
ctx->poec_format == PROP_FORMAT_JSON);
switch (ctx->poec_format) {
case PROP_FORMAT_JSON:
if (tags->json_open_tag == NULL ||
_prop_extern_append_cstring(ctx,
tags->json_open_tag) == false) {
return false;
}
if (tags->json_empty_sep != NULL &&
_prop_extern_append_cstring(ctx,
tags->json_empty_sep) == false) {
return false;
}
if (tags->json_close_tag != NULL) {
rv = _prop_extern_append_cstring(ctx,
tags->json_close_tag);
} else {
rv = true;
}
break;
default: /* PROP_FORMAT_XML */
rv = _prop_extern_append_char(ctx, '<') &&
_prop_extern_append_cstring(ctx, tags->xml_tag) &&
_prop_extern_append_char(ctx, '/') &&
_prop_extern_append_char(ctx, '>');
break;
}
return rv;
}
static const struct _prop_object_type_tags _plist_type_tags = {
.xml_tag = "plist",
};
/*
* _prop_extern_append_header --
* Append the header to the externalize buffer.
*/
static bool
_prop_extern_append_header(struct _prop_object_externalize_context *ctx)
{
static const char _plist_xml_header[] =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"
http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n";
if (ctx->poec_format != PROP_FORMAT_XML) {
return true;
}
if (_prop_extern_append_cstring(ctx, _plist_xml_header) == false ||
_prop_extern_append_start_tag(ctx,
&_plist_type_tags,
"version=\"1.0\"") == false ||
_prop_extern_append_char(ctx, '\n') == false) {
return false;
}
return true;
}
/*
* _prop_extern_append_footer --
* Append the footer to the externalize buffer. This also
* NUL-terminates the buffer.
*/
static bool
_prop_extern_append_footer(struct _prop_object_externalize_context *ctx)
{
if (_prop_extern_end_line(ctx, NULL) == false) {
return false;
}
if (ctx->poec_format == PROP_FORMAT_XML) {
if (_prop_extern_append_end_tag(ctx,
&_plist_type_tags) == false ||
_prop_extern_end_line(ctx, NULL) == false) {
return false;
}
}
return _prop_extern_append_char(ctx, '\0');
}
/*
* _prop_extern_context_alloc --
* Allocate an externalize context.
*/
static struct _prop_object_externalize_context *
_prop_extern_context_alloc(prop_format_t fmt)
{
struct _prop_object_externalize_context *ctx;
ctx = _PROP_MALLOC(sizeof(*ctx), M_TEMP);
if (ctx != NULL) {
ctx->poec_buf = _PROP_MALLOC(BUF_EXPAND, M_TEMP);
if (ctx->poec_buf == NULL) {
_PROP_FREE(ctx, M_TEMP);
return NULL;
}
ctx->poec_len = 0;
ctx->poec_capacity = BUF_EXPAND;
ctx->poec_depth = 0;
ctx->poec_format = fmt;
}
return ctx;
}
/*
* _prop_extern_context_free --
* Free an externalize context.
*/
static void
_prop_extern_context_free(struct _prop_object_externalize_context *ctx)
{
/* Buffer is always freed by the caller. */
_PROP_FREE(ctx, M_TEMP);
}
/*
* _prop_object_externalize --
* Externalize an object, returning a NUL-terminated buffer
* containing the serialized data in either XML or JSON format.
* The buffer is allocated with the M_TEMP memory type.
*/
char *
_prop_object_externalize(struct _prop_object *obj, prop_format_t fmt)
{
struct _prop_object_externalize_context *ctx;
char *cp = NULL;
if (obj == NULL || obj->po_type->pot_extern == NULL) {
return NULL;
}
if (fmt != PROP_FORMAT_XML && fmt != PROP_FORMAT_JSON) {
return NULL;
}
ctx = _prop_extern_context_alloc(fmt);
if (ctx == NULL) {
return NULL;
}
if (_prop_extern_append_header(ctx) == false ||
obj->po_type->pot_extern(ctx, obj) == false ||
_prop_extern_append_footer(ctx) == false) {
/* We are responsible for releasing the buffer. */
_PROP_FREE(ctx->poec_buf, M_TEMP);
goto bad;
}
cp = ctx->poec_buf;
bad:
_prop_extern_context_free(ctx);
return cp;
}
#if !defined(_KERNEL) && !defined(_STANDALONE)
/*
* _prop_extern_file_dirname --
* dirname(3), basically. We have to roll our own because the
* system dirname(3) isn't reentrant.
*/
static void
_prop_extern_file_dirname(const char *path, char *result)
{
const char *lastp;
size_t len;
/*
* If `path' is a NULL pointer or points to an empty string,
* return ".".
*/
if (path == NULL || *path == '\0') {
goto singledot;
}
/* Strip trailing slashes, if any. */
lastp = path + strlen(path) - 1;
while (lastp != path && *lastp == '/') {
lastp--;
}
/* Terminate path at the last occurrence of '/'. */
do {
if (*lastp == '/') {
/* Strip trailing slashes, if any. */
while (lastp != path && *lastp == '/') {
lastp--;
}
/* ...and copy the result into the result buffer. */
len = (lastp - path) + 1 /* last char */;
if (len > (PATH_MAX - 1)) {
len = PATH_MAX - 1;
}
memcpy(result, path, len);
result[len] = '\0';
return;
}
} while (--lastp >= path);
/* No /'s found, return ".". */
singledot:
strcpy(result, ".");
}
/*
* _prop_extern_write_file --
* Write an externalized object to the specified file.
* The file is written atomically from the caller's perspective,
* and the mode set to 0666 modified by the caller's umask.
*/
static bool
_prop_extern_write_file(const char *fname, const char *data, size_t len)
{
char tname_store[PATH_MAX];
char *tname = NULL;
int fd = -1;
int save_errno;
mode_t myumask;
bool rv = false;
if (len > SSIZE_MAX) {
errno = EFBIG;
return false;
}
/*
* Get the directory name where the file is to be written
* and create the temporary file.
*/
_prop_extern_file_dirname(fname, tname_store);
if (strlen(tname_store) + strlen(PLISTTMP) >= sizeof(tname_store)) {
errno = ENAMETOOLONG;
return false;
}
strcat(tname_store, PLISTTMP);
if ((fd = mkstemp(tname_store)) == -1) {
return false;
}
tname = tname_store;
if (write(fd, data, len) != (ssize_t)len) {
goto bad;
}
if (fsync(fd) == -1) {
goto bad;
}
myumask = umask(0);
(void)umask(myumask);
if (fchmod(fd, 0666 & ~myumask) == -1) {
goto bad;
}
if (rename(tname, fname) == -1) {
goto bad;
}
tname = NULL;
rv = true;
bad:
save_errno = errno;
if (fd != -1) {
(void) close(fd);
}
if (tname != NULL) {
(void) unlink(tname);
}
errno = save_errno;
return rv;
}
/*
* _prop_object_externalize_to_file --
* Externalize an object to the specified file.
*/
bool
_prop_object_externalize_to_file(struct _prop_object *obj, const char *fname,
prop_format_t fmt)
{
char *data = _prop_object_externalize(obj, fmt);
if (data == NULL) {
return false;
}
bool rv = _prop_extern_write_file(fname, data, strlen(data));
int save_errno = errno;
_PROP_FREE(data, M_TEMP);
errno = save_errno;
return rv;
}
/*
* prop_object_externalize_to_file --
* Externalize an object to the specifed file in the default format.
*/
_PROP_EXPORT bool
prop_object_externalize_to_file(prop_object_t po, const char *fname)
{
return _prop_object_externalize_to_file((struct _prop_object *)po,
fname, _prop_format_default);
}
/*
* prop_object_externalize_to_file_with_format --
* Externalize an object to the specifed file in the specified format.
*/
_PROP_EXPORT bool
prop_object_externalize_to_file_with_format(prop_object_t po,
const char *fname, prop_format_t fmt)
{
return _prop_object_externalize_to_file((struct _prop_object *)po,
fname, fmt);
}
#endif /* !_KERNEL && !_STANDALONE */
/*
* prop_object_externalize --
* Externalize an object in the default format.
*/
_PROP_EXPORT char *
prop_object_externalize(prop_object_t po)
{
return _prop_object_externalize((struct _prop_object *)po,
_prop_format_default);
}
/*
* prop_object_externalize_with_format --
* Externalize an object in the specified format.
*/
_PROP_EXPORT char *
prop_object_externalize_with_format(prop_object_t po, prop_format_t fmt)
{
return _prop_object_externalize((struct _prop_object *)po, fmt);
}