/* radare - LGPL - Copyright 2022 - pancake */
/*
 * bplist.c
 * Binary plist implementation
 *
 * Copyright (c) 2011-2017 Nikias Bassen, All Rights Reserved.
 * Copyright (c) 2008-2010 Jonathan Beck, All Rights Reserved.
 * Copyright (c) 2022      pancake, No Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <r_util.h>
#include <r_util/bplist.h>

/* Magic marker and size. */
#define BPLIST_MAGIC            ((ut8*)"bplist")
#define BPLIST_MAGIC_SIZE       6

#define BPLIST_VERSION          ((ut8*)"00")
#define BPLIST_VERSION_SIZE     2

R_PACKED (typedef struct {
	ut8 unused[6];
	ut8 offset_size;
	ut8 ref_size;
	ut64 num_objects;
	ut64 root_object_index;
	ut64 offset_table_offset;
}) BPlistTrailer;

enum {
	BPLIST_NULL = 0x00,
	BPLIST_FALSE = 0x08,
	BPLIST_TRUE = 0x09,
	BPLIST_FILL = 0x0F,			/* will be used for length grabbing */
	BPLIST_UINT = 0x10,
	BPLIST_REAL = 0x20,
	BPLIST_DATE = 0x30,
	BPLIST_DATA = 0x40,
	BPLIST_STRING = 0x50,
	BPLIST_UNICODE = 0x60,
	BPLIST_UNK_0x70 = 0x70,
	BPLIST_UID = 0x80,
	BPLIST_ARRAY = 0xA0,
	BPLIST_SET = 0xC0,
	BPLIST_DICT = 0xD0,
	BPLIST_MASK = 0xF0
};

#ifndef bswap32
#define bswap32(x)   ((((x) & 0xFF000000) >> 24) \
		| (((x) & 0x00FF0000) >>  8) \
		| (((x) & 0x0000FF00) <<  8) \
		| (((x) & 0x000000FF) << 24))
#endif

#ifndef bswap64
#define bswap64(x)   ((((x) & 0xFF00000000000000ull) >> 56) \
		| (((x) & 0x00FF000000000000ull) >> 40) \
		| (((x) & 0x0000FF0000000000ull) >> 24) \
		| (((x) & 0x000000FF00000000ull) >>  8) \
		| (((x) & 0x00000000FF000000ull) <<  8) \
		| (((x) & 0x0000000000FF0000ull) << 24) \
		| (((x) & 0x000000000000FF00ull) << 40) \
		| (((x) & 0x00000000000000FFull) << 56))
#endif

#ifndef be64toh
#ifdef __BIG_ENDIAN__
#define be64toh(x) (x)
#else
#define be64toh(x) bswap64 (x)
#endif
#endif

static ut64 UINT_TO_HOST(const char *data, int n) {
	switch (n) {
	case 1:
		return r_read_be8 (data);
	case 2:
		return r_read_be16 (data);
	case 4:
		return r_read_be32 (data);
	default:
		return r_read_be64 (data);
	}
	return 0;
}

#if (defined(__LITTLE_ENDIAN__) && !defined(__FLOAT_WORD_ORDER__)) \
	|| (defined(__FLOAT_WORD_ORDER__) && __FLOAT_WORD_ORDER__ == __ORDER_LITTLE_ENDIAN__)
#define float_bswap64(x) bswap64(x)
#define float_bswap32(x) bswap32(x)
#else
#define float_bswap64(x) (x)
#define float_bswap32(x) (x)
#endif

static bool parse_bin_node_at_index(RBPlist *bplist, ut32 node_index);

static bool parse_uint_node(RBPlist *bplist, const char **bnode, ut8 size) {
	size = 1 << size; // make length less misleading
#if 1
	switch (size) {
	case sizeof (ut8):
	case sizeof (ut16):
	case sizeof (ut32):
	case sizeof (ut64):
		// data.length = sizeof (ut64);
		break;
	case 16:
		// data.length = size;
		break;
	default:
		// free (data);
		R_LOG_ERROR ("Invalid byte size for integer node");
		return false;
	};
#endif
	ut64 intval = UINT_TO_HOST (*bnode, size);
	(*bnode) += size;
	pj_i (bplist->pj, intval);
	// printf (" %lld", intval);
	return true;
}

static double parse_real(const char **bnode, ut8 size) {
	double realval = 0.0;
	ut64 data = 0;
	ut8 buf[8] = {0};
	memcpy (&data, *bnode, sizeof (buf));
	switch (1 << size) {
	case sizeof (ut32):
		*(ut32*)buf = float_bswap32 (data);
		realval = *(float *) buf;
		break;
	case sizeof (ut64):
		*(ut64*)buf = float_bswap64 (data);
		realval = *(double *) buf;
		break;
	default:
		R_LOG_ERROR ("Invalid byte size for real node");
		return false;
	}
	return realval;
}

static bool parse_date_node(RBPlist *bplist, const char **bnode, ut8 size) {
	double realval = parse_real (bnode, size);
	// printf ("date(%d)", (int)(size_t)realval / 1000000);
	// realval = (double)sec + (double)usec / 1000000;
	pj_n (bplist->pj, (int)(size_t)realval / 1000000);
	return true;
}

static bool parse_string_node(RBPlist *bplist, const char **bnode, ut64 size) {
	char *s = r_str_ndup (*bnode, size);
	pj_s (bplist->pj, s);
	//printf (" \"%s\"", s);
	free (s);
	return true;
}

static bool parse_unicode_node(RBPlist *bplist, const char **bnode, ut64 size) {
	ut8 *dst = malloc (size + 1);
	if (!dst) {
		return false;
	}
	const ut8 *src = (const ut8*)*bnode;
 	if (!r_str_utf16_to_utf8 (dst, size, src, size, false)) {
		free (dst);
		return false;
	}
	//char *tmpstr = plist_utf16be_to_utf8 ((ut16*)(*bnode), size, &items_read, &items_written);
	pj_s (bplist->pj, (const char *)dst);
	//printf (" unicode(%s)", dst);
	free (dst);
	return true;
}

static bool parse_data_node(RBPlist *bplist, const char **bnode, ut64 size) {
	pj_r (bplist->pj, (const ut8*)(*bnode), size);
	return true;
}

static bool parse_dict_node(RBPlist *bplist, const char** bnode, ut64 size) {
	ut64 j;
	bool res = true;

	pj_o (bplist->pj);
	for (j = 0; j < size; j++) {
		ut64 str_i = j * bplist->ref_size;
		ut64 str_j = (j + size) * bplist->ref_size;
		const char *index1_ptr = (*bnode) + str_i;
		const char *index2_ptr = (*bnode) + str_j;

		if ((index1_ptr < bplist->data || index1_ptr + bplist->ref_size > bplist->offset_table) ||
				(index2_ptr < bplist->data || index2_ptr + bplist->ref_size > bplist->offset_table)) {
			R_LOG_ERROR ("dict entry %" PRIu64 " is outside of valid range", j);
			return false;
		}

		ut64 index1 = UINT_TO_HOST(index1_ptr, bplist->ref_size);
		ut64 index2 = UINT_TO_HOST(index2_ptr, bplist->ref_size);

		if (index1 >= bplist->num_objects) {
			R_LOG_ERROR ("dict entry %" PRIu64 ": key index (%" PRIu64 ") must be smaller than the number of objects (%" PRIu64 ")", j, index1, bplist->num_objects);
			return false;
		}
		if (index2 >= bplist->num_objects) {
			R_LOG_ERROR ("dict entry %" PRIu64 ": value index (%" PRIu64 ") must be smaller than the number of objects (%" PRIu64 ")", j, index1, bplist->num_objects);
			return false;
		}
		/* key */
		if (!parse_bin_node_at_index (bplist, index1)) {
			R_LOG_ERROR ("cannot find key");
			return false;
		}
		pj_kraw (bplist->pj);
		/* value */
		if (!parse_bin_node_at_index (bplist, index2)) {
			res = false;
			break;
		}
	}
	pj_end (bplist->pj);
	return res;
}

static bool parse_array_node(RBPlist *bplist, const char** bnode, const ut64 size) {
	bool res = true;
	ut64 j;
	ut64 index1;

	pj_a (bplist->pj);
	for (j = 0; j < size; j++) {
		ut64 str_j = j * bplist->ref_size;
		const char *index1_ptr = (*bnode) + str_j;

		if (index1_ptr < bplist->data || index1_ptr + bplist->ref_size > bplist->offset_table) {
			R_LOG_ERROR ("array item %" PRIu64 " is outside of valid range", j);
			return false;
		}

		index1 = UINT_TO_HOST(index1_ptr, bplist->ref_size);

		if (index1 >= bplist->num_objects) {
			R_LOG_ERROR ("array item %" PRIu64 " object index (%" PRIu64 ") must be smaller than the number of objects (%" PRIu64 ")", j, index1, bplist->num_objects);
			return false;
		}

		/* process value node */
		if (!parse_bin_node_at_index (bplist, index1)) {
			res = false;
			break;
		}
	}
	pj_end (bplist->pj);
	return res;
}

static bool parse_uid_node(RBPlist *bplist, const char **bnode, ut8 size) {
	size++;
	long long intval = UINT_TO_HOST (*bnode, size);
	if (intval > UINT32_MAX) {
		R_LOG_ERROR ("value %" PRIu64 " too large for UID node (must be <= %u)", (ut64)intval, UINT32_MAX);
		return false;
	}

	(*bnode) += size;
	pj_n (bplist->pj, (int)intval);
	return true;
}

static bool parse_bin_node(RBPlist *bplist, const char** object) {
	PJ *pj = bplist->pj;
	ut64 poffset_table = (uint64_t)(uintptr_t)bplist->offset_table;

	if (!object) {
		return false;
	}

	ut16 type = (**object) & BPLIST_MASK;
	ut64 size = (**object) & BPLIST_FILL;
	(*object)++;

	if (size == BPLIST_FILL) {
		switch (type) {
		case BPLIST_DATA:
		case BPLIST_STRING:
		case BPLIST_UNICODE:
		case BPLIST_ARRAY:
		case BPLIST_SET:
		case BPLIST_DICT:
			{
				ut16 next_size = **object & BPLIST_FILL;
				if ((**object & BPLIST_MASK) != BPLIST_UINT) {
					R_LOG_ERROR ("invalid size node type for node type 0x%02x: found 0x%02x, expected 0x%02x", type, **object & BPLIST_MASK, BPLIST_UINT);
					return false;
				}
				(*object)++;
				next_size = 1 << next_size;
				if (*object + next_size > bplist->offset_table) {
					R_LOG_ERROR ("size node data bytes for node type 0x%02x point outside of valid range", type);
					return false;
				}
				size = UINT_TO_HOST(*object, next_size);
				(*object) += next_size;
				break;
			}
		default:
			break;
		}
	}

	ut64 pobject = (uint64_t)(uintptr_t)*object;

	switch (type) {
	case BPLIST_NULL:
		switch (size) {
		case BPLIST_TRUE:
			pj_b (pj, true);
			return true;
		case BPLIST_FALSE:
			pj_b (pj, true);
			return true;
		case BPLIST_NULL:
			pj_null (pj);
			return true;
		default:
			return false;
		}
	case BPLIST_UINT:
		if (pobject + (ut64)(1 << size) > poffset_table) {
			R_LOG_ERROR ("BPLIST_UINT data bytes point outside of valid range");
			return false;
		}
		return parse_uint_node (bplist, object, size);
	case BPLIST_REAL:
		if (pobject + (ut64)(1 << size) > poffset_table) {
			R_LOG_ERROR ("BPLIST_REAL data bytes point outside of valid range");
			return false;
		}
		pj_d (bplist->pj, parse_real (object, size));
		return true;
	case BPLIST_DATE:
		if (3 != size) {
			R_LOG_ERROR ("invalid data size for BPLIST_DATE node");
			return false;
		}
		if (pobject + (ut64)(1 << size) > poffset_table) {
			R_LOG_ERROR ("BPLIST_DATE data bytes point outside of valid range");
			return false;
		}
		return parse_date_node (bplist, object, size);
	case BPLIST_DATA:
		if (pobject + size < pobject || pobject + size > poffset_table) {
			R_LOG_ERROR ("BPLIST_DATA data bytes point outside of valid range");
			return false;
		}
		return parse_data_node (bplist, object, size);
	case BPLIST_STRING:
		if (pobject + size < pobject || pobject + size > poffset_table) {
			R_LOG_ERROR ("BPLIST_STRING data bytes point outside of valid range");
			return false;
		}
		return parse_string_node (bplist, object, size);
	case BPLIST_UNICODE:
		if (size * 2 < size) {
			R_LOG_ERROR ("Integer overflow when calculating BPLIST_UNICODE data size");
			return false;
		}
		if (pobject + size*2 < pobject || pobject + size*2 > poffset_table) {
			R_LOG_ERROR ("BPLIST_UNICODE data bytes point outside of valid range");
			return false;
		}
		return parse_unicode_node (bplist, object, size);
	case BPLIST_SET:
	case BPLIST_ARRAY:
		if (pobject + size < pobject || pobject + size > poffset_table) {
			R_LOG_ERROR ("BPLIST_ARRAY data bytes point outside of valid range");
			return false;
		}
		return parse_array_node(bplist, object, size);
	case BPLIST_UID:
		if (pobject + size + 1 > poffset_table) {
			R_LOG_ERROR ("BPLIST_UID data bytes point outside of valid range");
			return false;
		}
		return parse_uid_node (bplist, object, size);
	case BPLIST_DICT:
		if (pobject + size < pobject || pobject + size > poffset_table) {
			R_LOG_ERROR ("BPLIST_DICT data bytes point outside of valid range");
			return false;
		}
		return parse_dict_node (bplist, object, size);
	default:
		R_LOG_ERROR ("unexpected node type 0x%02x", type);
		return false;
	}
	return true;
}

static bool parse_bin_node_at_index(RBPlist *bplist, ut32 node_index) {
	if (node_index >= bplist->num_objects) {
		R_LOG_ERROR ("node index (%u) must be smaller than the number of objects (%" PRIu64 ")", node_index, bplist->num_objects);
		return false;
	}
	const char *idx_ptr = bplist->offset_table + node_index * bplist->offset_size;
	if (idx_ptr < bplist->offset_table ||
			idx_ptr >= bplist->offset_table + bplist->num_objects * bplist->offset_size) {
		R_LOG_ERROR ("node index %u points outside of valid range", node_index);
		return false;
	}

	const char* ptr = bplist->data + UINT_TO_HOST(idx_ptr, bplist->offset_size);
	/* make sure the node offset is in a sane range */
	if ((ptr < bplist->data) || (ptr >= bplist->offset_table)) {
		R_LOG_ERROR ("offset for node index %u points outside of valid range", node_index);
		return false;
	}
	/* finally parse node */
	return parse_bin_node (bplist, &ptr);
}

static bool r_bplist_check(const ut8 *plist_data, ut32 length) {
	return length > 7 && !memcmp (plist_data, "bplist00", 8);
}

R_API bool r_bplist_parse(PJ *pj, const ut8 *data, size_t data_len) {
	R_RETURN_VAL_IF_FAIL (data, false);
	if (!data_len) {
		return false;
	}
	if (!r_bplist_check (data, data_len)) {
		return false;
	}

	if (data_len < BPLIST_MAGIC_SIZE + BPLIST_VERSION_SIZE + sizeof (BPlistTrailer)) {
		R_LOG_ERROR ("plist data is to small to hold a binary plist");
		return false;
	}
	// check version
	if (memcmp (data + BPLIST_MAGIC_SIZE, BPLIST_VERSION, BPLIST_VERSION_SIZE)) {
		R_LOG_ERROR ("unsupported binary plist version '%.2s", data+BPLIST_MAGIC_SIZE);
		return false;
	}

	const ut8 *start_data = data + BPLIST_MAGIC_SIZE + BPLIST_VERSION_SIZE;
	const ut8 *end_data = data + data_len - sizeof (BPlistTrailer);

	// now parse trailer
	BPlistTrailer *trailer = (BPlistTrailer*)end_data;
	ut8 offset_size = trailer->offset_size;
	ut8 ref_size = trailer->ref_size;
	ut64 num_objects = be64toh (trailer->num_objects);
	ut64 root_object = be64toh (trailer->root_object_index);
	const ut8 *offset_table = (ut8 *)(data + be64toh (trailer->offset_table_offset));

	if (num_objects == 0) {
		R_LOG_ERROR ("number of objects must be larger than 0");
		return false;
	}
	if (offset_size == 0) {
		R_LOG_ERROR ("offset size in trailer must be larger than 0");
		return false;
	}
	if (ref_size == 0) {
		R_LOG_ERROR ("object reference size in trailer must be larger than 0");
		return false;
	}
	if (root_object >= num_objects) {
		R_LOG_ERROR ("root object index (%" PRIu64 ") must be smaller than number of objects (%" PRIu64 ")", root_object, num_objects);
		return false;
	}
	if (offset_table < start_data || offset_table >= end_data) {
		R_LOG_ERROR ("offset table offset points outside of valid range");
		return false;
	}
	ut64 offset_table_size;
	if (r_mul_overflow (num_objects, offset_size, &offset_table_size)) {
		R_LOG_ERROR ("integer overflow when calculating offset table size");
		return false;
	}
	if (offset_table_size > (ut64)(end_data - offset_table)) {
		R_LOG_ERROR ("offset table points outside of valid range");
		return false;
	}

	RBPlist bplist = {
		.data = (const char *)data,
		.size = data_len,
		.num_objects = num_objects,
		.ref_size = ref_size,
		.offset_size = offset_size,
		.offset_table = (const char *)offset_table,
		.pj = pj,
	};
	return parse_bin_node_at_index (&bplist, root_object);
}
