/*
 * Copyright (C) 2014 Michael Brown <mbrown@fensystems.co.uk>.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or any later version.
 *
 * This program 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
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

FILE_LICENCE ( GPL2_OR_LATER );

/** @file
 *
 * DNS self-tests
 *
 */

/* Forcibly enable assertions */
#undef NDEBUG

#include <string.h>
#include <assert.h>
#include <ipxe/dns.h>
#include <ipxe/test.h>

/** Define inline data */
#define DATA(...) { __VA_ARGS__ }

/** A DNS encoding test */
struct dns_encode_test {
	/** String */
	const char *string;
	/** Encoded string */
	const void *data;
	/** Length of encoded string */
	int len;
};

/**
 * Define a DNS encoding test
 *
 * @v _name		Test name
 * @v _string		Test string
 * @v _data		Expected encoded data
 * @ret test		DNS encoding test
 */
#define DNS_ENCODE( _name, _string, _data )				\
	static const uint8_t _name ## __data[] = _data;			\
	static struct dns_encode_test _name = {				\
		.string = _string,					\
		.data =	_name ## __data,				\
		.len = sizeof ( _name ## __data ),			\
	}

/**
 * Report DNS encoding test result
 *
 * @v test		DNS encoding test
 * @v file		Test code file
 * @v line		Test code line
 */
static void dns_encode_okx ( struct dns_encode_test *test, const char *file,
			     unsigned int line ) {
	uint8_t data[ test->len ];
	struct dns_name name;
	int len;

	/* Check ability to determine length with no buffer */
	memset ( &name, 0, sizeof ( name ) );
	len = dns_encode ( test->string, &name );
	okx ( len >= 0, file, line );
	okx ( len == test->len, file, line );

	/* Check encoded name */
	name.data = data;
	name.len = sizeof ( data );
	len = dns_encode ( test->string, &name );
	okx ( len >= 0, file, line );
	if ( len >= 0 ) {
		okx ( len == test->len, file, line );
		okx ( memcmp ( data, test->data, test->len ) == 0, file, line );
		DBGC ( test, "DNS encoded \"%s\" to:\n", test->string );
		DBGC_HDA ( test, 0, data, len );
	}
}
#define dns_encode_ok( test ) dns_encode_okx ( test, __FILE__, __LINE__ )

/**
 * Report DNS encoding failure test result
 *
 * @v test		DNS encoding test
 * @v file		Test code file
 * @v line		Test code line
 */
static void dns_encode_fail_okx ( struct dns_encode_test *test,
				  const char *file, unsigned int line ) {
	struct dns_name name = { .data = NULL, .len = 0 };
	int len;

	len = dns_encode ( test->string, &name );
	okx ( len < 0, file, line );
}
#define dns_encode_fail_ok( test ) \
	dns_encode_fail_okx ( test, __FILE__, __LINE__ )

/** A DNS decoding test */
struct dns_decode_test {
	/** Name */
	struct dns_name name;
	/** Expected string */
	const char *string;
};

/**
 * Define a DNS decoding test
 *
 * @v _name		Test name
 * @v _data		RFC1035-encoded data
 * @v _offset		Starting offset within encoded data
 * @v _string		Expected decoded string
 * @ret test		DNS decoding test
 */
#define DNS_DECODE( _name, _data, _offset, _string )			\
	static uint8_t _name ## __data[] = _data;			\
	static struct dns_decode_test _name = {				\
		.name = {						\
			.data = _name ## __data,			\
			.offset = _offset,				\
			.len = sizeof ( _name ## __data ),		\
		},							\
		.string = _string,					\
	}

/**
 * Report DNS decoding test result
 *
 * @v test		DNS decoding test
 * @v file		Test code file
 * @v line		Test code line
 */
static void dns_decode_okx ( struct dns_decode_test *test, const char *file,
			     unsigned int line ) {
	char string[ strlen ( test->string ) + 1 /* NUL */ ];
	int len;

	/* Check ability to determine length with no buffer */
	len = dns_decode ( &test->name, NULL, 0 );
	okx ( len >= 0, file, line );
	okx ( len == ( ( int ) strlen ( test->string ) ), file, line );

	/* Check decoded string */
	len = dns_decode ( &test->name, string, sizeof ( string ) );
	okx ( len >= 0, file, line );
	if ( len >= 0 ) {
		okx ( strcmp ( string, test->string ) == 0, file, line );
		DBGC ( test, "DNS decoded \"%s\" from offset %#zx in:\n",
		       string, test->name.offset );
		DBGC_HDA ( test, 0, test->name.data, test->name.len );
	}
}
#define dns_decode_ok( test ) dns_decode_okx ( test, __FILE__, __LINE__ )

/**
 * Report DNS decoding failure test result
 *
 * @v test		DNS decoding test
 * @v file		Test code file
 * @v line		Test code line
 */
static void dns_decode_fail_okx ( struct dns_decode_test *test,
				  const char *file, unsigned int line ) {
	int len;

	len = dns_decode ( &test->name, NULL, 0 );
	okx ( len < 0, file, line );
}
#define dns_decode_fail_ok( test ) \
	dns_decode_fail_okx ( test, __FILE__, __LINE__ )

/** A DNS comparison test */
struct dns_compare_test {
	/** First name */
	struct dns_name first;
	/** Second name */
	struct dns_name second;
};

/**
 * Define a DNS comparison test
 *
 * @v _name		Test name
 * @v _first_data	First RFC1035-encoded data
 * @v _first_offset	Starting offset within first encoded data
 * @v _second_data	Second RFC1035-encoded data
 * @v _second_offset	Starting offset within second encoded data
 * @ret test		DNS comparison test
 */
#define DNS_COMPARE( _name, _first_data, _first_offset, _second_data,	\
		     _second_offset )					\
	static uint8_t _name ## __first_data[] = _first_data;		\
	static uint8_t _name ## __second_data[] = _second_data;		\
	static struct dns_compare_test _name = {			\
		.first = {						\
			.data = _name ## __first_data,			\
			.offset = _first_offset,			\
			.len = sizeof ( _name ## __first_data ),	\
		},							\
		.second = {						\
			.data = _name ## __second_data,			\
			.offset = _second_offset,			\
			.len = sizeof ( _name ## __second_data ),	\
		},							\
	}

/**
 * Report DNS comparison test result
 *
 * @v test		DNS comparison test
 * @v file		Test code file
 * @v line		Test code line
 */
static void dns_compare_okx ( struct dns_compare_test *test, const char *file,
			      unsigned int line ) {

	okx ( dns_compare ( &test->first, &test->second ) == 0, file, line );
}
#define dns_compare_ok( test ) dns_compare_okx ( test, __FILE__, __LINE__ )

/**
 * Report DNS comparison test failure result
 *
 * @v test		DNS comparison test
 * @v file		Test code file
 * @v line		Test code line
 */
static void dns_compare_fail_okx ( struct dns_compare_test *test,
				   const char *file, unsigned int line ) {

	okx ( dns_compare ( &test->first, &test->second ) != 0, file, line );
}
#define dns_compare_fail_ok( test ) \
	dns_compare_fail_okx ( test, __FILE__, __LINE__ )

/** A DNS copying test */
struct dns_copy_test {
	/** Source name */
	struct dns_name src;
	/** Expected copied name */
	struct dns_name dst;
};

/**
 * Define a DNS copying test
 *
 * @v _name		Test name
 * @v _src_data		Source RFC1035-encoded data
 * @v _src_offset	Starting offset within source encoded data
 * @v _dst_data		Expected copied RFC1035-encoded data
 * @v _dst_offset	Starting offset withint copied encoded data
 * @ret test		DNS copying test
 */
#define DNS_COPY( _name, _src_data, _src_offset, _dst_data,		\
		  _dst_offset )						\
	static uint8_t _name ## __src_data[] = _src_data;		\
	static uint8_t _name ## __dst_data[] = _dst_data;		\
	static struct dns_copy_test _name = {				\
		.src = {						\
			.data = _name ## __src_data,			\
			.offset = _src_offset,				\
			.len = sizeof ( _name ## __src_data ),		\
		},							\
		.dst = {						\
			.data = _name ## __dst_data,			\
			.offset = _dst_offset,				\
			.len = sizeof ( _name ## __dst_data ),		\
		},							\
	}

/**
 * Report a DNS copying test result
 *
 * @v test		DNS copying test
 * @v file		Test code file
 * @v line		Test code line
 */
static void dns_copy_okx ( struct dns_copy_test *test,
			   const char *file, unsigned int line ) {
	uint8_t data[ test->dst.len ];
	struct dns_name dst;
	int len;

	/* Check ability to determine length with no buffer */
	memset ( &dst, 0, sizeof ( dst ) );
	len = dns_copy ( &test->src, &dst );
	okx ( len >= 0, file, line );
	okx ( len == ( ( int ) ( test->dst.len - test->dst.offset ) ),
	      file, line );

	/* Check copied name */
	dst.data = data;
	dst.offset = test->dst.offset;
	dst.len = sizeof ( data );
	memcpy ( dst.data, test->dst.data, test->dst.offset );
	len = dns_copy ( &test->src, &dst );
	okx ( len >= 0, file, line );
	okx ( len == ( ( int ) ( test->dst.len - test->dst.offset ) ),
	      file, line );
	okx ( memcmp ( data, test->dst.data, sizeof ( data ) ) == 0,
	      file, line );
	DBGC ( test, "DNS copied:\n" );
	DBGC_HDA ( test, 0, test->src.data, test->src.len );
	DBGC_HDA ( test, 0, data, ( test->dst.offset + len ) );
}
#define dns_copy_ok( test ) dns_copy_okx ( test, __FILE__, __LINE__ )

/**
 * Report a DNS copying failure test result
 *
 * @v test		DNS copying test
 * @v file		Test code file
 * @v line		Test code line
 */
static void dns_copy_fail_okx ( struct dns_copy_test *test,
				const char *file, unsigned int line ) {
	struct dns_name dst;
	int len;

	memset ( &dst, 0, sizeof ( dst ) );
	len = dns_copy ( &test->src, &dst );
	okx ( len < 0, file, line );
}
#define dns_copy_fail_ok( test ) dns_copy_fail_okx ( test, __FILE__, __LINE__ )

/** A DNS search list test */
struct dns_list_test {
	/** Search list */
	struct dns_name list;
	/** Expected decoded search list */
	const char **strings;
	/** Number of expected decoded string */
	unsigned int count;
};

/**
 * Define a DNS search list test
 *
 * @v _name		Test name
 * @v _list		RFC1035-encoded data
 * @v _strings		Expected decoded strings
 * @ret test		DNS search list test
 */
#define DNS_LIST( _name, _list, _strings )				\
	static uint8_t _name ## __list[] = _list;			\
	static const char * _name ## __strings[] = _strings;		\
	static struct dns_list_test _name = {				\
		.list = {						\
			.data = _name ## __list,			\
			.offset = 0,					\
			.len = sizeof ( _name ## __list ),		\
		},							\
		.strings = _name ## __strings,				\
		.count = ( sizeof ( _name ## __strings ) /		\
			   sizeof ( _name ## __strings[0] ) ),		\
	}

/**
 * Report DNS search list test result
 *
 * @v test		DNS search list test
 * @v file		Test code file
 * @v line		Test code line
 */
static void dns_list_okx ( struct dns_list_test *test, const char *file,
			   unsigned int line ) {
	struct dns_name name;
	unsigned int i;

	DBGC ( test, "DNS search list:\n" );
	DBGC_HDA ( test, 0, test->list.data, test->list.len );
	memcpy ( &name, &test->list, sizeof ( name ) );
	for ( i = 0 ; i < test->count ; i++ ) {
		char buf[ strlen ( test->strings[i] ) + 1 /* NUL */ ];
		int len;
		int offset;

		/* Decode this name */
		len = dns_decode ( &name, buf, sizeof ( buf ) );
		okx ( len >= 0, file, line );
		if ( len >= 0 ) {
			okx ( len == ( ( int ) strlen ( test->strings[i] ) ),
			      file, line );
			okx ( strcmp ( buf, test->strings[i] ) == 0,
			      file, line );
			DBGC ( test, "DNS search list found \"%s\" at offset "
			       "%#zx\n", buf, name.offset );
		}

		/* Skip to next name */
		offset = dns_skip ( &name );
		okx ( offset >= 0, file, line );
		name.offset = offset;
	}

	/* Check that we have consumed the whole search list */
	okx ( name.offset == name.len, file, line );
}
#define dns_list_ok( test ) dns_list_okx ( test, __FILE__, __LINE__ )

/* Simple encoding test */
DNS_ENCODE ( encode_simple, "ipxe.org",
	     DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ) );

/* Single-word encoding test */
DNS_ENCODE ( encode_single, "foo", DATA ( 3, 'f', 'o', 'o', 0 ) );

/* Absolute encoding test */
DNS_ENCODE ( encode_absolute, "git.ipxe.org.",
	     DATA ( 3, 'g', 'i', 't', 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g',
		    0 ) );

/* Empty string encoding test */
DNS_ENCODE ( encode_empty, "", DATA ( 0 ) );

/* Root domain encoding test */
DNS_ENCODE ( encode_root, ".", DATA ( 0 ) );

/* Invalid initial dot encoding test */
DNS_ENCODE ( encode_initial_dot, ".foo", DATA() );

/* Invalid double dot encoding test */
DNS_ENCODE ( encode_double_dot, "ipxe..org", DATA() );

/* Invalid solo double dot encoding test */
DNS_ENCODE ( encode_solo_double_dot, "..", DATA() );

/* Invalid trailing double dot encoding test */
DNS_ENCODE ( encode_trailing_double_dot, "ipxe.org..", DATA() );

/* Invalid overlength label encoding test */
DNS_ENCODE ( encode_overlength,
	     "this-label-is-maliciously-long-in-an-attempt-to-overflow-the-"
	     "length-field-and-generate-a-length-which-looks-like-a-"
	     "compression-pointer", DATA() );

/* Simple decoding test */
DNS_DECODE ( decode_simple,
	     DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0,
	     "ipxe.org" );

/* Compression pointer decoding test */
DNS_DECODE ( decode_ptr,
	     DATA ( 3, 'o', 'r', 'g', 0, 3, 'g', 'i', 't', 4, 'i', 'p', 'x',
		    'e', 0xc0, 0x00 ), 5,
	     "git.ipxe.org" );

/* Root decoding test */
DNS_DECODE ( decode_root,
	     DATA ( 0 ), 0, "" );

/* Incomplete name decoding test */
DNS_DECODE ( decode_incomplete_name,
	     DATA ( 4, 'i', 'p', 'x', 'e' ), 0, NULL );

/* Incomplete label decoding test */
DNS_DECODE ( decode_incomplete_label,
	     DATA ( 4, 'i', 'p', 'x' ), 0, NULL );

/* Incomplete compression pointer decoding test */
DNS_DECODE ( decode_incomplete_ptr,
	     DATA ( 3, 'o', 'r', 'g', 0, 4, 'i', 'p', 'x', 'e', 0xc0 ), 5,
	     NULL );

/* Forward reference decoding test */
DNS_DECODE ( decode_forward,
	     DATA ( 0xc0, 0x02, 3, 'f', 'o', 'o', 0 ), 0, NULL );

/* Infinite loop decoding test */
DNS_DECODE ( decode_infinite,
	     DATA ( 4, 'i', 'p', 'x', 'e', 0xc0, 0x00 ), 0, NULL );

/* Empty decoding test */
DNS_DECODE ( decode_empty,
	     DATA (), 0, NULL );

/* Simple comparison test */
DNS_COMPARE ( compare_simple,
	      DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0,
	      DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0 );

/* Compression pointer comparison test */
DNS_COMPARE ( compare_ptr,
	      DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0,
	      DATA ( 3, 'o', 'r', 'g', 0, 4, 'i', 'p', 'x', 'e',
		     0xc0, 0x00 ), 5 );

/* Case insensitive comparison test */
DNS_COMPARE ( compare_case,
	      DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0,
	      DATA ( 4, 'i', 'p', 'x', 'e', 3, 'O', 'R', 'G', 0 ), 0 );

/* Mismatch comparison test */
DNS_COMPARE ( compare_mismatch,
	      DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0,
	      DATA ( 4, 'g', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0 );

/* Infinite loop comparison test */
DNS_COMPARE ( compare_infinite,
	      DATA ( 3, 'f', 'o', 'o', 0xc0, 0x00 ), 0,
	      DATA ( 3, 'f', 'o', 'o', 0xc0, 0x00 ), 0 );

/* Simple copying test */
DNS_COPY ( copy_simple,
	   DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0,
	   DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0 );

/* Simple copying test with offset */
DNS_COPY ( copy_offset,
	   DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0 ), 0,
	   DATA ( 'f', 'o', 'o', 0, 4, 'i', 'p', 'x', 'e',
		  3, 'o', 'r', 'g', 0 ), 4 );

/* Compression pointer copying test */
DNS_COPY ( copy_ptr,
	   DATA ( 3, 'o', 'r', 'g', 0, 3, 'g', 'i', 't', 4, 'i', 'p', 'x', 'e',
		  0xc0, 0x00 ), 5,
	   DATA ( 3, 'g', 'i', 't', 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g',
		  0 ), 0 );

/* Infinite loop copying test */
DNS_COPY ( copy_infinite,
	   DATA ( 4, 'l', 'o', 'o', 'p', 7, 'f', 'o', 'r', 'e', 'v', 'e', 'r',
		  0xc0, 0x05 ), 0,
	   DATA (), 0 );

/* DNS search list test */
DNS_LIST ( search,
	   DATA ( 4, 'i', 'p', 'x', 'e', 3, 'o', 'r', 'g', 0,
		  4, 'b', 'o', 'o', 't', 0xc0, 0x00,
		  3, 'd', 'e', 'v', 0xc0, 0x0a,
		  11, 'n', 'e', 't', 'w', 'o', 'r', 'k', 'b', 'o', 'o', 't',
		  0xc0, 0x05 ),
	   DATA ( "ipxe.org", "boot.ipxe.org", "dev.boot.ipxe.org",
		  "networkboot.org" ) );

/**
 * Perform DNS self-test
 *
 */
static void dns_test_exec ( void ) {

	/* Encoding tests */
	dns_encode_ok ( &encode_simple );
	dns_encode_ok ( &encode_single );
	dns_encode_ok ( &encode_absolute );
	dns_encode_ok ( &encode_empty );
	dns_encode_ok ( &encode_root );
	dns_encode_fail_ok ( &encode_initial_dot );
	dns_encode_fail_ok ( &encode_double_dot );
	dns_encode_fail_ok ( &encode_solo_double_dot );
	dns_encode_fail_ok ( &encode_trailing_double_dot );
	dns_encode_fail_ok ( &encode_overlength );

	/* Decoding tests */
	dns_decode_ok ( &decode_simple );
	dns_decode_ok ( &decode_ptr );
	dns_decode_ok ( &decode_root );
	dns_decode_fail_ok ( &decode_incomplete_name );
	dns_decode_fail_ok ( &decode_incomplete_label );
	dns_decode_fail_ok ( &decode_incomplete_ptr );
	dns_decode_fail_ok ( &decode_forward );
	dns_decode_fail_ok ( &decode_infinite );
	dns_decode_fail_ok ( &decode_empty );

	/* Comparison tests */
	dns_compare_ok ( &compare_simple );
	dns_compare_ok ( &compare_ptr );
	dns_compare_ok ( &compare_case );
	dns_compare_fail_ok ( &compare_mismatch );
	dns_compare_fail_ok ( &compare_infinite );

	/* Copying tests */
	dns_copy_ok ( &copy_simple );
	dns_copy_ok ( &copy_offset );
	dns_copy_ok ( &copy_ptr );
	dns_copy_fail_ok ( &copy_infinite );

	/* Search list tets */
	dns_list_ok ( &search );
}

/** DNS self-test */
struct self_test dns_test __self_test = {
	.name = "dns",
	.exec = dns_test_exec,
};
