/*
 * ARM64 kexec.
 */

#define _GNU_SOURCE

#include <assert.h>
#include <errno.h>
#include <getopt.h>
#include <inttypes.h>
#include <libfdt.h>
#include <limits.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <linux/elf-em.h>
#include <elf.h>

#include "kexec.h"
#include "kexec-arm64.h"
#include "crashdump.h"
#include "crashdump-arm64.h"
#include "dt-ops.h"
#include "fs2dt.h"
#include "iomem.h"
#include "kexec-syscall.h"
#include "arch/options.h"

#define ROOT_NODE_ADDR_CELLS_DEFAULT 1
#define ROOT_NODE_SIZE_CELLS_DEFAULT 1

#define PROP_ADDR_CELLS "#address-cells"
#define PROP_SIZE_CELLS "#size-cells"
#define PROP_ELFCOREHDR "linux,elfcorehdr"
#define PROP_USABLE_MEM_RANGE "linux,usable-memory-range"

/* Global varables the core kexec routines expect. */

unsigned char reuse_initrd;

off_t initrd_base;
off_t initrd_size;

const struct arch_map_entry arches[] = {
	{ "aarch64", KEXEC_ARCH_ARM64 },
	{ "aarch64_be", KEXEC_ARCH_ARM64 },
	{ NULL, 0 },
};

struct file_type file_type[] = {
	{"vmlinux", elf_arm64_probe, elf_arm64_load, elf_arm64_usage},
	{"Image", image_arm64_probe, image_arm64_load, image_arm64_usage},
	{"uImage", uImage_arm64_probe, uImage_arm64_load, uImage_arm64_usage},
};

int file_types = sizeof(file_type) / sizeof(file_type[0]);

/* arm64 global varables. */

struct arm64_opts arm64_opts;
struct arm64_mem arm64_mem = {
	.phys_offset = arm64_mem_ngv,
	.vp_offset = arm64_mem_ngv,
};

uint64_t get_phys_offset(void)
{
	assert(arm64_mem.phys_offset != arm64_mem_ngv);
	return arm64_mem.phys_offset;
}

uint64_t get_vp_offset(void)
{
	assert(arm64_mem.vp_offset != arm64_mem_ngv);
	return arm64_mem.vp_offset;
}

/**
 * arm64_process_image_header - Process the arm64 image header.
 *
 * Make a guess that KERNEL_IMAGE_SIZE will be enough for older kernels.
 */

int arm64_process_image_header(const struct arm64_image_header *h)
{
#if !defined(KERNEL_IMAGE_SIZE)
# define KERNEL_IMAGE_SIZE MiB(16)
#endif

	if (!arm64_header_check_magic(h))
		return EFAILED;

	if (h->image_size) {
		arm64_mem.text_offset = arm64_header_text_offset(h);
		arm64_mem.image_size = arm64_header_image_size(h);
	} else {
		/* For 3.16 and older kernels. */
		arm64_mem.text_offset = 0x80000;
		arm64_mem.image_size = KERNEL_IMAGE_SIZE;
		fprintf(stderr,
			"kexec: %s: Warning: Kernel image size set to %lu MiB.\n"
			"  Please verify compatability with lodaed kernel.\n",
			__func__, KERNEL_IMAGE_SIZE / 1024UL / 1024UL);
	}

	return 0;
}

void arch_usage(void)
{
	printf(arm64_opts_usage);
}

int arch_process_options(int argc, char **argv)
{
	static const char short_options[] = KEXEC_OPT_STR "";
	static const struct option options[] = {
		KEXEC_ARCH_OPTIONS
		{ 0 }
	};
	int opt;
	char *cmdline = NULL;
	const char *append = NULL;

	for (opt = 0; opt != -1; ) {
		opt = getopt_long(argc, argv, short_options, options, 0);

		switch (opt) {
		case OPT_APPEND:
			append = optarg;
			break;
		case OPT_REUSE_CMDLINE:
			cmdline = get_command_line();
			break;
		case OPT_DTB:
			arm64_opts.dtb = optarg;
			break;
		case OPT_INITRD:
			arm64_opts.initrd = optarg;
			break;
		default:
			break; /* Ignore core and unknown options. */
		}
	}

	arm64_opts.command_line = concat_cmdline(cmdline, append);

	dbgprintf("%s:%d: command_line: %s\n", __func__, __LINE__,
		arm64_opts.command_line);
	dbgprintf("%s:%d: initrd: %s\n", __func__, __LINE__,
		arm64_opts.initrd);
	dbgprintf("%s:%d: dtb: %s\n", __func__, __LINE__, arm64_opts.dtb);

	return 0;
}

/**
 * struct dtb - Info about a binary device tree.
 *
 * @buf: Device tree data.
 * @size: Device tree data size.
 * @name: Shorthand name of this dtb for messages.
 * @path: Filesystem path.
 */

struct dtb {
	char *buf;
	off_t size;
	const char *name;
	const char *path;
};

/**
 * dump_reservemap - Dump the dtb's reservemap.
 */

static void dump_reservemap(const struct dtb *dtb)
{
	int i;

	for (i = 0; ; i++) {
		uint64_t address;
		uint64_t size;

		fdt_get_mem_rsv(dtb->buf, i, &address, &size);

		if (!size)
			break;

		dbgprintf("%s: %s {%" PRIx64 ", %" PRIx64 "}\n", __func__,
			dtb->name, address, size);
	}
}

/**
 * set_bootargs - Set the dtb's bootargs.
 */

static int set_bootargs(struct dtb *dtb, const char *command_line)
{
	int result;

	if (!command_line || !command_line[0])
		return 0;

	result = dtb_set_bootargs(&dtb->buf, &dtb->size, command_line);

	if (result) {
		fprintf(stderr,
			"kexec: Set device tree bootargs failed.\n");
		return EFAILED;
	}

	return 0;
}

/**
 * read_proc_dtb - Read /proc/device-tree.
 */

static int read_proc_dtb(struct dtb *dtb)
{
	int result;
	struct stat s;
	static const char path[] = "/proc/device-tree";

	result = stat(path, &s);

	if (result) {
		dbgprintf("%s: %s\n", __func__, strerror(errno));
		return EFAILED;
	}

	dtb->path = path;
	create_flatten_tree((char **)&dtb->buf, &dtb->size, NULL);

	return 0;
}

/**
 * read_sys_dtb - Read /sys/firmware/fdt.
 */

static int read_sys_dtb(struct dtb *dtb)
{
	int result;
	struct stat s;
	static const char path[] = "/sys/firmware/fdt";

	result = stat(path, &s);

	if (result) {
		dbgprintf("%s: %s\n", __func__, strerror(errno));
		return EFAILED;
	}

	dtb->path = path;
	dtb->buf = slurp_file(path, &dtb->size);

	return 0;
}

/**
 * read_1st_dtb - Read the 1st stage kernel's dtb.
 */

static int read_1st_dtb(struct dtb *dtb)
{
	int result;

	dtb->name = "dtb_sys";
	result = read_sys_dtb(dtb);

	if (!result)
		goto on_success;

	dtb->name = "dtb_proc";
	result = read_proc_dtb(dtb);

	if (!result)
		goto on_success;

	dbgprintf("%s: not found\n", __func__);
	return EFAILED;

on_success:
	dbgprintf("%s: found %s\n", __func__, dtb->path);
	return 0;
}

static int get_cells_size(void *fdt, uint32_t *address_cells,
						uint32_t *size_cells)
{
	int nodeoffset;
	const uint32_t *prop = NULL;
	int prop_len;

	/* default values */
	*address_cells = ROOT_NODE_ADDR_CELLS_DEFAULT;
	*size_cells = ROOT_NODE_SIZE_CELLS_DEFAULT;

	/* under root node */
	nodeoffset = fdt_path_offset(fdt, "/");
	if (nodeoffset < 0)
		goto on_error;

	prop = fdt_getprop(fdt, nodeoffset, PROP_ADDR_CELLS, &prop_len);
	if (prop) {
		if (prop_len == sizeof(*prop))
			*address_cells = fdt32_to_cpu(*prop);
		else
			goto on_error;
	}

	prop = fdt_getprop(fdt, nodeoffset, PROP_SIZE_CELLS, &prop_len);
	if (prop) {
		if (prop_len == sizeof(*prop))
			*size_cells = fdt32_to_cpu(*prop);
		else
			goto on_error;
	}

	dbgprintf("%s: #address-cells:%d #size-cells:%d\n", __func__,
			*address_cells, *size_cells);
	return 0;

on_error:
	return EFAILED;
}

static bool cells_size_fitted(uint32_t address_cells, uint32_t size_cells,
						struct memory_range *range)
{
	dbgprintf("%s: %llx-%llx\n", __func__, range->start, range->end);

	/* if *_cells >= 2, cells can hold 64-bit values anyway */
	if ((address_cells == 1) && (range->start >= (1ULL << 32)))
		return false;

	if ((size_cells == 1) &&
			((range->end - range->start + 1) >= (1ULL << 32)))
		return false;

	return true;
}

static void fill_property(void *buf, uint64_t val, uint32_t cells)
{
	uint32_t val32;
	int i;

	if (cells == 1) {
		val32 = cpu_to_fdt32((uint32_t)val);
		memcpy(buf, &val32, sizeof(uint32_t));
	} else {
		for (i = 0;
		     i < (cells * sizeof(uint32_t) - sizeof(uint64_t)); i++)
			*(char *)buf++ = 0;

		val = cpu_to_fdt64(val);
		memcpy(buf, &val, sizeof(uint64_t));
	}
}

static int fdt_setprop_range(void *fdt, int nodeoffset,
				const char *name, struct memory_range *range,
				uint32_t address_cells, uint32_t size_cells)
{
	void *buf, *prop;
	size_t buf_size;
	int result;

	buf_size = (address_cells + size_cells) * sizeof(uint32_t);
	prop = buf = xmalloc(buf_size);

	fill_property(prop, range->start, address_cells);
	prop += address_cells * sizeof(uint32_t);

	fill_property(prop, range->end - range->start + 1, size_cells);
	prop += size_cells * sizeof(uint32_t);

	result = fdt_setprop(fdt, nodeoffset, name, buf, buf_size);

	free(buf);

	return result;
}

/**
 * setup_2nd_dtb - Setup the 2nd stage kernel's dtb.
 */

static int setup_2nd_dtb(struct dtb *dtb, char *command_line, int on_crash)
{
	uint32_t address_cells, size_cells;
	int range_len;
	int nodeoffset;
	char *new_buf = NULL;
	int new_size;
	int result;

	result = fdt_check_header(dtb->buf);

	if (result) {
		fprintf(stderr, "kexec: Invalid 2nd device tree.\n");
		return EFAILED;
	}

	result = set_bootargs(dtb, command_line);

	if (on_crash) {
		/* determine #address-cells and #size-cells */
		result = get_cells_size(dtb->buf, &address_cells, &size_cells);
		if (result) {
			fprintf(stderr,
				"kexec: cannot determine cells-size.\n");
			result = -EINVAL;
			goto on_error;
		}

		if (!cells_size_fitted(address_cells, size_cells,
					&elfcorehdr_mem)) {
			fprintf(stderr,
				"kexec: elfcorehdr doesn't fit cells-size.\n");
			result = -EINVAL;
			goto on_error;
		}

		if (!cells_size_fitted(address_cells, size_cells,
					&crash_reserved_mem)) {
			fprintf(stderr,
				"kexec: usable memory range doesn't fit cells-size.\n");
			result = -EINVAL;
			goto on_error;
		}

		/* duplicate dt blob */
		range_len = sizeof(uint32_t) * (address_cells + size_cells);
		new_size = fdt_totalsize(dtb->buf)
			+ fdt_prop_len(PROP_ELFCOREHDR, range_len)
			+ fdt_prop_len(PROP_USABLE_MEM_RANGE, range_len);

		new_buf = xmalloc(new_size);
		result = fdt_open_into(dtb->buf, new_buf, new_size);
		if (result) {
			dbgprintf("%s: fdt_open_into failed: %s\n", __func__,
				fdt_strerror(result));
			result = -ENOSPC;
			goto on_error;
		}

		/* add linux,elfcorehdr */
		nodeoffset = fdt_path_offset(new_buf, "/chosen");
		result = fdt_setprop_range(new_buf, nodeoffset,
				PROP_ELFCOREHDR, &elfcorehdr_mem,
				address_cells, size_cells);
		if (result) {
			dbgprintf("%s: fdt_setprop failed: %s\n", __func__,
				fdt_strerror(result));
			result = -EINVAL;
			goto on_error;
		}

		/* add linux,usable-memory-range */
		nodeoffset = fdt_path_offset(new_buf, "/chosen");
		result = fdt_setprop_range(new_buf, nodeoffset,
				PROP_USABLE_MEM_RANGE, &crash_reserved_mem,
				address_cells, size_cells);
		if (result) {
			dbgprintf("%s: fdt_setprop failed: %s\n", __func__,
				fdt_strerror(result));
			result = -EINVAL;
			goto on_error;
		}

		fdt_pack(new_buf);
		dtb->buf = new_buf;
		dtb->size = fdt_totalsize(new_buf);
	}

	dump_reservemap(dtb);


	return result;

on_error:
	fprintf(stderr, "kexec: %s failed.\n", __func__);
	if (new_buf)
		free(new_buf);

	return result;
}

unsigned long arm64_locate_kernel_segment(struct kexec_info *info)
{
	unsigned long hole;

	if (info->kexec_flags & KEXEC_ON_CRASH) {
		unsigned long hole_end;

		hole = (crash_reserved_mem.start < mem_min ?
				mem_min : crash_reserved_mem.start);
		hole = _ALIGN_UP(hole, MiB(2));
		hole_end = hole + arm64_mem.text_offset + arm64_mem.image_size;

		if ((hole_end > mem_max) ||
		    (hole_end > crash_reserved_mem.end)) {
			dbgprintf("%s: Crash kernel out of range\n", __func__);
			hole = ULONG_MAX;
		}
	} else {
		hole = locate_hole(info,
			arm64_mem.text_offset + arm64_mem.image_size,
			MiB(2), 0, ULONG_MAX, 1);

		if (hole == ULONG_MAX)
			dbgprintf("%s: locate_hole failed\n", __func__);
	}

	return hole;
}

/**
 * arm64_load_other_segments - Prepare the dtb, initrd and purgatory segments.
 */

int arm64_load_other_segments(struct kexec_info *info,
	unsigned long image_base)
{
	int result;
	unsigned long dtb_base;
	unsigned long hole_min;
	unsigned long hole_max;
	unsigned long initrd_end;
	char *initrd_buf = NULL;
	struct dtb dtb;
	char command_line[COMMAND_LINE_SIZE] = "";

	if (arm64_opts.command_line) {
		strncpy(command_line, arm64_opts.command_line,
			sizeof(command_line));
		command_line[sizeof(command_line) - 1] = 0;
	}

	if (arm64_opts.dtb) {
		dtb.name = "dtb_user";
		dtb.buf = slurp_file(arm64_opts.dtb, &dtb.size);
	} else {
		result = read_1st_dtb(&dtb);

		if (result) {
			fprintf(stderr,
				"kexec: Error: No device tree available.\n");
			return EFAILED;
		}
	}

	result = setup_2nd_dtb(&dtb, command_line,
			info->kexec_flags & KEXEC_ON_CRASH);

	if (result)
		return EFAILED;

	/* Put the other segments after the image. */

	hole_min = image_base + arm64_mem.image_size;
	if (info->kexec_flags & KEXEC_ON_CRASH)
		hole_max = crash_reserved_mem.end;
	else
		hole_max = ULONG_MAX;

	if (arm64_opts.initrd) {
		initrd_buf = slurp_file(arm64_opts.initrd, &initrd_size);

		if (!initrd_buf)
			fprintf(stderr, "kexec: Empty ramdisk file.\n");
		else {
			/* Put the initrd after the kernel. */

			initrd_base = add_buffer_phys_virt(info, initrd_buf,
				initrd_size, initrd_size, 0,
				hole_min, hole_max, 1, 0);

			initrd_end = initrd_base + initrd_size;

			/* Check limits as specified in booting.txt.
			 * The kernel may have as little as 32 GB of address space to map
			 * system memory and both kernel and initrd must be 1GB aligend.
			 */

			if (_ALIGN_UP(initrd_end, GiB(1)) - _ALIGN_DOWN(image_base, GiB(1)) > GiB(32)) {
				fprintf(stderr, "kexec: Error: image + initrd too big.\n");
				return EFAILED;
			}

			dbgprintf("initrd: base %lx, size %lxh (%ld)\n",
				initrd_base, initrd_size, initrd_size);

			result = dtb_set_initrd((char **)&dtb.buf,
				&dtb.size, initrd_base,
				initrd_base + initrd_size);

			if (result)
				return EFAILED;
		}
	}

	/* Check size limit as specified in booting.txt. */

	if (dtb.size > MiB(2)) {
		fprintf(stderr, "kexec: Error: dtb too big.\n");
		return EFAILED;
	}

	dtb_base = add_buffer_phys_virt(info, dtb.buf, dtb.size, dtb.size,
		0, hole_min, hole_max, 1, 0);

	/* dtb_base is valid if we got here. */

	dbgprintf("dtb:    base %lx, size %lxh (%ld)\n", dtb_base, dtb.size,
		dtb.size);

	elf_rel_build_load(info, &info->rhdr, purgatory, purgatory_size,
		hole_min, hole_max, 1, 0);

	info->entry = (void *)elf_rel_get_addr(&info->rhdr, "purgatory_start");

	elf_rel_set_symbol(&info->rhdr, "arm64_kernel_entry", &image_base,
		sizeof(image_base));

	elf_rel_set_symbol(&info->rhdr, "arm64_dtb_addr", &dtb_base,
		sizeof(dtb_base));

	return 0;
}

/**
 * virt_to_phys - For processing elf file values.
 */

unsigned long virt_to_phys(unsigned long v)
{
	unsigned long p;

	p = v - get_vp_offset() + get_phys_offset();

	return p;
}

/**
 * phys_to_virt - For crashdump setup.
 */

unsigned long phys_to_virt(struct crash_elf_info *elf_info,
	unsigned long long p)
{
	unsigned long v;

	v = p - get_phys_offset() + elf_info->page_offset;

	return v;
}

/**
 * add_segment - Use virt_to_phys when loading elf files.
 */

void add_segment(struct kexec_info *info, const void *buf, size_t bufsz,
	unsigned long base, size_t memsz)
{
	add_segment_phys_virt(info, buf, bufsz, base, memsz, 1);
}

/**
 * get_memory_ranges_iomem_cb - Helper for get_memory_ranges_iomem.
 */

static int get_memory_ranges_iomem_cb(void *data, int nr, char *str,
	unsigned long long base, unsigned long long length)
{
	struct memory_range *r;

	if (nr >= KEXEC_SEGMENT_MAX)
		return -1;

	r = (struct memory_range *)data + nr;

	if (!strncmp(str, SYSTEM_RAM, strlen(SYSTEM_RAM)))
		r->type = RANGE_RAM;
	else if (!strncmp(str, IOMEM_RESERVED, strlen(IOMEM_RESERVED)))
		r->type = RANGE_RESERVED;
	else
		return 1;

	r->start = base;
	r->end = base + length - 1;

	set_phys_offset(r->start);

	dbgprintf("%s: %016llx - %016llx : %s", __func__, r->start,
		r->end, str);

	return 0;
}

/**
 * get_memory_ranges_iomem - Try to get the memory ranges from /proc/iomem.
 */

static int get_memory_ranges_iomem(struct memory_range *array,
	unsigned int *count)
{
	*count = kexec_iomem_for_each_line(NULL,
		get_memory_ranges_iomem_cb, array);

	if (!*count) {
		dbgprintf("%s: failed: No RAM found.\n", __func__);
		return EFAILED;
	}

	return 0;
}

/**
 * get_memory_ranges - Try to get the memory ranges some how.
 */

int get_memory_ranges(struct memory_range **range, int *ranges,
	unsigned long kexec_flags)
{
	static struct memory_range array[KEXEC_SEGMENT_MAX];
	unsigned int count;
	int result;

	result = get_memory_ranges_iomem(array, &count);

	*range = result ? NULL : array;
	*ranges = result ? 0 : count;

	return result;
}

int arch_compat_trampoline(struct kexec_info *info)
{
	return 0;
}

int machine_verify_elf_rel(struct mem_ehdr *ehdr)
{
	return (ehdr->e_machine == EM_AARCH64);
}

void machine_apply_elf_rel(struct mem_ehdr *ehdr, struct mem_sym *UNUSED(sym),
	unsigned long r_type, void *ptr, unsigned long address,
	unsigned long value)
{
#if !defined(R_AARCH64_ABS64)
# define R_AARCH64_ABS64 257
#endif

#if !defined(R_AARCH64_PREL32)
# define R_AARCH64_PREL32 261
#endif

#if !defined(R_AARCH64_LD_PREL_LO19)
# define R_AARCH64_LD_PREL_LO19 273
#endif

#if !defined(R_AARCH64_ADR_PREL_LO21)
# define R_AARCH64_ADR_PREL_LO21 274
#endif

#if !defined(R_AARCH64_ADR_PREL_PG_HI21)
# define R_AARCH64_ADR_PREL_PG_HI21 275
#endif

#if !defined(R_AARCH64_ADD_ABS_LO12_NC)
# define R_AARCH64_ADD_ABS_LO12_NC 277
#endif

#if !defined(R_AARCH64_JUMP26)
# define R_AARCH64_JUMP26 282
#endif

#if !defined(R_AARCH64_CALL26)
# define R_AARCH64_CALL26 283
#endif

#if !defined(R_AARCH64_LDST64_ABS_LO12_NC)
# define R_AARCH64_LDST64_ABS_LO12_NC 286
#endif

	uint64_t *loc64;
	uint32_t *loc32;
	uint64_t *location = (uint64_t *)ptr;
	uint64_t data = *location;
	uint64_t imm;
	const char *type = NULL;

	switch(r_type) {
	case R_AARCH64_ABS64:
		type = "ABS64";
		loc64 = ptr;
		*loc64 = cpu_to_elf64(ehdr, elf64_to_cpu(ehdr, *loc64) + value);
		break;
	case R_AARCH64_PREL32:
		type = "PREL32";
		loc32 = ptr;
		*loc32 = cpu_to_elf32(ehdr,
			elf32_to_cpu(ehdr, *loc32) + value - address);
		break;
	case R_AARCH64_LD_PREL_LO19:
		type = "LD_PREL_LO19";
		loc32 = ptr;
		*loc32 = cpu_to_le32(le32_to_cpu(*loc32)
			+ (((value - address) << 3) & 0xffffe0));
		break;
	case R_AARCH64_ADR_PREL_LO21:
		if (value & 3)
			die("%s: ERROR Unaligned value: %lx\n", __func__,
				value);
		type = "ADR_PREL_LO21";
		loc32 = ptr;
		*loc32 = cpu_to_le32(le32_to_cpu(*loc32)
			+ (((value - address) << 3) & 0xffffe0));
		break;
	case R_AARCH64_ADR_PREL_PG_HI21:
		type = "ADR_PREL_PG_HI21";
		imm = ((value & ~0xfff) - (address & ~0xfff)) >> 12;
		loc32 = ptr;
		*loc32 = cpu_to_le32(le32_to_cpu(*loc32)
			+ ((imm & 3) << 29) + ((imm & 0x1ffffc) << (5 - 2)));
		break;
	case R_AARCH64_ADD_ABS_LO12_NC:
		type = "ADD_ABS_LO12_NC";
		loc32 = ptr;
		*loc32 = cpu_to_le32(le32_to_cpu(*loc32)
			+ ((value & 0xfff) << 10));
		break;
	case R_AARCH64_JUMP26:
		type = "JUMP26";
		loc32 = ptr;
		*loc32 = cpu_to_le32(le32_to_cpu(*loc32)
			+ (((value - address) >> 2) & 0x3ffffff));
		break;
	case R_AARCH64_CALL26:
		type = "CALL26";
		loc32 = ptr;
		*loc32 = cpu_to_le32(le32_to_cpu(*loc32)
			+ (((value - address) >> 2) & 0x3ffffff));
		break;
	case R_AARCH64_LDST64_ABS_LO12_NC:
		if (value & 7)
			die("%s: ERROR Unaligned value: %lx\n", __func__,
				value);
		type = "LDST64_ABS_LO12_NC";
		loc32 = ptr;
		*loc32 = cpu_to_le32(le32_to_cpu(*loc32)
			+ ((value & 0xff8) << (10 - 3)));
		break;
	default:
		die("%s: ERROR Unknown type: %lu\n", __func__, r_type);
		break;
	}

	dbgprintf("%s: %s %016lx->%016lx\n", __func__, type, data, *location);
}

void arch_reuse_initrd(void)
{
	reuse_initrd = 1;
}

void arch_update_purgatory(struct kexec_info *UNUSED(info))
{
}
