/*	$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, "&lt;");
			break;
		case '>':
			rv = _prop_extern_append_cstring(ctx, "&gt;");
			break;
		case '&':
			rv = _prop_extern_append_cstring(ctx, "&amp;");
			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);
}