// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (C) 2024 Google LLC
 */

#define _GNU_SOURCE
#include <inttypes.h>
#include <stdio.h>
#include <zlib.h>

#include "gendwarfksyms.h"

static struct cache expansion_cache;

/*
 * A simple linked list of shared or owned strings to avoid copying strings
 * around when not necessary.
 */
struct type_list_entry {
	const char *str;
	void *owned;
	struct list_head list;
};

static void type_list_free(struct list_head *list)
{
	struct type_list_entry *entry;
	struct type_list_entry *tmp;

	list_for_each_entry_safe(entry, tmp, list, list) {
		if (entry->owned)
			free(entry->owned);
		free(entry);
	}

	INIT_LIST_HEAD(list);
}

static int type_list_append(struct list_head *list, const char *s, void *owned)
{
	struct type_list_entry *entry;

	if (!s)
		return 0;

	entry = xmalloc(sizeof(struct type_list_entry));
	entry->str = s;
	entry->owned = owned;
	list_add_tail(&entry->list, list);

	return strlen(entry->str);
}

static void type_list_write(struct list_head *list, FILE *file)
{
	struct type_list_entry *entry;

	list_for_each_entry(entry, list, list) {
		if (entry->str)
			checkp(fputs(entry->str, file));
	}
}

/*
 * An expanded type string in symtypes format.
 */
struct type_expansion {
	char *name;
	size_t len;
	struct list_head expanded;
	struct hlist_node hash;
};

static void type_expansion_init(struct type_expansion *type)
{
	type->name = NULL;
	type->len = 0;
	INIT_LIST_HEAD(&type->expanded);
}

static inline void type_expansion_free(struct type_expansion *type)
{
	free(type->name);
	type->name = NULL;
	type->len = 0;
	type_list_free(&type->expanded);
}

static void type_expansion_append(struct type_expansion *type, const char *s,
				  void *owned)
{
	type->len += type_list_append(&type->expanded, s, owned);
}

/*
 * type_map -- the longest expansions for each type.
 *
 * const char *name -> struct type_expansion *
 */
#define TYPE_HASH_BITS 12
static HASHTABLE_DEFINE(type_map, 1 << TYPE_HASH_BITS);

static int type_map_get(const char *name, struct type_expansion **res)
{
	struct type_expansion *e;

	hash_for_each_possible(type_map, e, hash, hash_str(name)) {
		if (!strcmp(name, e->name)) {
			*res = e;
			return 0;
		}
	}

	return -1;
}

static void type_map_add(const char *name, struct type_expansion *type)
{
	struct type_expansion *e;

	if (type_map_get(name, &e)) {
		e = xmalloc(sizeof(struct type_expansion));
		type_expansion_init(e);
		e->name = xstrdup(name);

		hash_add(type_map, &e->hash, hash_str(e->name));

		if (dump_types)
			debug("adding %s", e->name);
	} else {
		/* Use the longest available expansion */
		if (type->len <= e->len)
			return;

		type_list_free(&e->expanded);

		if (dump_types)
			debug("replacing %s", e->name);
	}

	/* Take ownership of type->expanded */
	list_replace_init(&type->expanded, &e->expanded);
	e->len = type->len;

	if (dump_types) {
		checkp(fputs(e->name, stderr));
		checkp(fputs(" ", stderr));
		type_list_write(&e->expanded, stderr);
		checkp(fputs("\n", stderr));
	}
}

static void type_map_write(FILE *file)
{
	struct type_expansion *e;
	struct hlist_node *tmp;

	if (!file)
		return;

	hash_for_each_safe(type_map, e, tmp, hash) {
		checkp(fputs(e->name, file));
		checkp(fputs(" ", file));
		type_list_write(&e->expanded, file);
		checkp(fputs("\n", file));
	}
}

static void type_map_free(void)
{
	struct type_expansion *e;
	struct hlist_node *tmp;

	hash_for_each_safe(type_map, e, tmp, hash) {
		type_expansion_free(e);
		free(e);
	}

	hash_init(type_map);
}

/*
 * CRC for a type, with an optional fully expanded type string for
 * debugging.
 */
struct version {
	struct type_expansion type;
	unsigned long crc;
};

static void version_init(struct version *version)
{
	version->crc = crc32(0, NULL, 0);
	type_expansion_init(&version->type);
}

static void version_free(struct version *version)
{
	type_expansion_free(&version->type);
}

static void version_add(struct version *version, const char *s)
{
	version->crc = crc32(version->crc, (void *)s, strlen(s));
	if (dump_versions)
		type_expansion_append(&version->type, s, NULL);
}

/*
 * Type reference format: <prefix>#<name>, where prefix:
 * 	s -> structure
 * 	u -> union
 * 	e -> enum
 * 	t -> typedef
 *
 * Names with spaces are additionally wrapped in single quotes.
 */
static inline bool is_type_prefix(const char *s)
{
	return (s[0] == 's' || s[0] == 'u' || s[0] == 'e' || s[0] == 't') &&
	       s[1] == '#';
}

static char get_type_prefix(int tag)
{
	switch (tag) {
	case DW_TAG_class_type:
	case DW_TAG_structure_type:
		return 's';
	case DW_TAG_union_type:
		return 'u';
	case DW_TAG_enumeration_type:
		return 'e';
	case DW_TAG_typedef_type:
		return 't';
	default:
		return 0;
	}
}

static char *get_type_name(struct die *cache)
{
	const char *quote;
	char prefix;
	char *name;

	if (cache->state == DIE_INCOMPLETE) {
		warn("found incomplete cache entry: %p", cache);
		return NULL;
	}
	if (cache->state == DIE_SYMBOL || cache->state == DIE_FQN)
		return NULL;
	if (!cache->fqn || !*cache->fqn)
		return NULL;

	prefix = get_type_prefix(cache->tag);
	if (!prefix)
		return NULL;

	/* Wrap names with spaces in single quotes */
	quote = strstr(cache->fqn, " ") ? "'" : "";

	/* <prefix>#<type_name>\0 */
	if (asprintf(&name, "%c#%s%s%s", prefix, quote, cache->fqn, quote) < 0)
		error("asprintf failed for '%s'", cache->fqn);

	return name;
}

static void __calculate_version(struct version *version, struct list_head *list)
{
	struct type_list_entry *entry;
	struct type_expansion *e;

	/* Calculate a CRC over an expanded type string */
	list_for_each_entry(entry, list, list) {
		if (is_type_prefix(entry->str)) {
			check(type_map_get(entry->str, &e));

			/*
			 * It's sufficient to expand each type reference just
			 * once to detect changes.
			 */
			if (cache_was_expanded(&expansion_cache, e)) {
				version_add(version, entry->str);
			} else {
				cache_mark_expanded(&expansion_cache, e);
				__calculate_version(version, &e->expanded);
			}
		} else {
			version_add(version, entry->str);
		}
	}
}

static void calculate_version(struct version *version, struct list_head *list)
{
	version_init(version);
	__calculate_version(version, list);
	cache_free(&expansion_cache);
}

static void __type_expand(struct die *cache, struct type_expansion *type,
			  bool recursive);

static void type_expand_child(struct die *cache, struct type_expansion *type,
			      bool recursive)
{
	struct type_expansion child;
	char *name;

	name = get_type_name(cache);
	if (!name) {
		__type_expand(cache, type, recursive);
		return;
	}

	if (recursive && !__cache_was_expanded(&expansion_cache, cache->addr)) {
		__cache_mark_expanded(&expansion_cache, cache->addr);
		type_expansion_init(&child);
		__type_expand(cache, &child, true);
		type_map_add(name, &child);
		type_expansion_free(&child);
	}

	type_expansion_append(type, name, name);
}

static void __type_expand(struct die *cache, struct type_expansion *type,
			  bool recursive)
{
	struct die_fragment *df;
	struct die *child;

	list_for_each_entry(df, &cache->fragments, list) {
		switch (df->type) {
		case FRAGMENT_STRING:
			type_expansion_append(type, df->data.str, NULL);
			break;
		case FRAGMENT_DIE:
			/* Use a complete die_map expansion if available */
			if (__die_map_get(df->data.addr, DIE_COMPLETE,
					  &child) &&
			    __die_map_get(df->data.addr, DIE_UNEXPANDED,
					  &child))
				error("unknown child: %" PRIxPTR,
				      df->data.addr);

			type_expand_child(child, type, recursive);
			break;
		case FRAGMENT_LINEBREAK:
			/*
			 * Keep whitespace in the symtypes format, but avoid
			 * repeated spaces.
			 */
			if (list_is_last(&df->list, &cache->fragments) ||
			    list_next_entry(df, list)->type !=
				    FRAGMENT_LINEBREAK)
				type_expansion_append(type, " ", NULL);
			break;
		default:
			error("empty die_fragment in %p", cache);
		}
	}
}

static void type_expand(struct die *cache, struct type_expansion *type,
			bool recursive)
{
	type_expansion_init(type);
	__type_expand(cache, type, recursive);
	cache_free(&expansion_cache);
}

static void expand_type(struct die *cache, void *arg)
{
	struct type_expansion type;
	char *name;

	if (cache->mapped)
		return;

	cache->mapped = true;

	/*
	 * Skip unexpanded die_map entries if there's a complete
	 * expansion available for this DIE.
	 */
	if (cache->state == DIE_UNEXPANDED &&
	    !__die_map_get(cache->addr, DIE_COMPLETE, &cache)) {
		if (cache->mapped)
			return;

		cache->mapped = true;
	}

	name = get_type_name(cache);
	if (!name)
		return;

	debug("%s", name);
	type_expand(cache, &type, true);
	type_map_add(name, &type);

	type_expansion_free(&type);
	free(name);
}

static void expand_symbol(struct symbol *sym, void *arg)
{
	struct type_expansion type;
	struct version version;
	struct die *cache;

	/*
	 * No need to expand again unless we want a symtypes file entry
	 * for the symbol. Note that this means `sym` has the same address
	 * as another symbol that was already processed.
	 */
	if (!symtypes && sym->state == SYMBOL_PROCESSED)
		return;

	if (__die_map_get(sym->die_addr, DIE_SYMBOL, &cache))
		return; /* We'll warn about missing CRCs later. */

	type_expand(cache, &type, false);

	/* If the symbol already has a version, don't calculate it again. */
	if (sym->state != SYMBOL_PROCESSED) {
		calculate_version(&version, &type.expanded);
		symbol_set_crc(sym, version.crc);
		debug("%s = %lx", sym->name, version.crc);

		if (dump_versions) {
			checkp(fputs(sym->name, stderr));
			checkp(fputs(" ", stderr));
			type_list_write(&version.type.expanded, stderr);
			checkp(fputs("\n", stderr));
		}

		version_free(&version);
	}

	/* These aren't needed in type_map unless we want a symtypes file. */
	if (symtypes)
		type_map_add(sym->name, &type);

	type_expansion_free(&type);
}

void generate_symtypes_and_versions(FILE *file)
{
	cache_init(&expansion_cache);

	/*
	 * die_map processing:
	 *
	 *   1. die_map contains all types referenced in exported symbol
	 *      signatures, but can contain duplicates just like the original
	 *      DWARF, and some references may not be fully expanded depending
	 *      on how far we processed the DIE tree for that specific symbol.
	 *
	 *      For each die_map entry, find the longest available expansion,
	 *      and add it to type_map.
	 */
	die_map_for_each(expand_type, NULL);

	/*
	 *   2. For each exported symbol, expand the die_map type, and use
	 *      type_map expansions to calculate a symbol version from the
	 *      fully expanded type string.
	 */
	symbol_for_each(expand_symbol, NULL);

	/*
	 *   3. If a symtypes file is requested, write type_map contents to
	 *      the file.
	 */
	type_map_write(file);
	type_map_free();
}
