/**
 * Copyright 2020 German Tischler-Höhle
 *
 * 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 COPYRIGHT HOLDERS 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 COPYRIGHT
 * HOLDER 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 <libsecrecy/GCMInputStream.hpp>

#include <iostream>
#include <cstdlib>
#include <ctime>

std::streamsize randNum(std::streamsize const s)
{
	if ( ! s )
		return 0;

	while ( true )
	{
		std::streamsize v = 0;

		for ( unsigned int i = 0; i < sizeof(std::streamsize); ++i )
		{
			v <<= CHAR_BIT;
			v |= random()&0xFF;
		}

		if ( v >= 0 )
			return v % (s+1);
	}
}

int main(int argc, char * argv[])
{
	try
	{
		if ( !(2 < argc) )
		{
			std::cerr << "usage: " << argv[0] << " <file.plain> <file.crypt>" << std::endl;
			return EXIT_FAILURE;
		}

		::srandom(::time(nullptr));

		std::ifstream plainistr(argv[1],std::ios::binary);
		if ( ! plainistr.is_open() )
		{
			std::cerr << "Failed to open file " << argv[1] << std::endl;
			return EXIT_FAILURE;
		}

		std::ifstream rawcryptostr(argv[2],std::ios::binary);
		std::size_t const pushbacksize = 16;
		libsecrecy::GCMInputStream GIS(rawcryptostr,pushbacksize);

		std::size_t const readblocksize = 64*1024;
		std::shared_ptr<char[]> A(new char[readblocksize]);
		std::shared_ptr<char[]> B(new char[readblocksize]);

		std::streamsize s = 0;

		while ( true )
		{
			bool const eofGIS = (GIS.peek() == std::istream::traits_type::eof());
			bool const eofplain = (plainistr.peek() == std::istream::traits_type::eof());

			if ( eofGIS && eofplain )
				break;
			else if ( eofGIS )
				throw std::runtime_error("EOF on encrypted file but not on plain file");
			else if ( eofplain )
				throw std::runtime_error("EOF on plain file but not on encrypted file");

			GIS.read(A.get(),readblocksize);
			plainistr.read(B.get(),readblocksize);

			if ( GIS.gcount() != plainistr.gcount() )
				throw std::runtime_error("Unable to read same number of bytes from streams.");

			std::streamsize const r = GIS.gcount();

			if ( ! std::equal(A.get(),A.get()+r,B.get()) )
				throw std::runtime_error("Content mismatch.");

			s += r;
		}

		std::cerr << "Succesfully compared files of size " << s << std::endl;

		unsigned int const num = 64*1024;

		for ( unsigned int j = 0; j < num; ++j )
		{
			std::streamsize const p = randNum(s);

			plainistr.clear();
			plainistr.seekg(p);
			GIS.clear();
			GIS.seekg(p);

			plainistr.read(A.get(),readblocksize);
			GIS.read(B.get(),readblocksize);

			if ( GIS.gcount() != plainistr.gcount() )
				throw std::runtime_error("Unable to read same number of bytes from streams.");

			std::streamsize const r = GIS.gcount();

			if ( ! std::equal(A.get(),A.get()+r,B.get()) )
				throw std::runtime_error("Content mismatch.");
		}

		std::cerr << "Succesfully performed " << num << " reads of size " << readblocksize << " after random absolute seeks" << std::endl;

		for ( unsigned int j = 0; j < num; ++j )
		{
			std::streamsize const p = randNum(s);

			plainistr.clear();
			plainistr.seekg(p,std::ios::beg);
			GIS.clear();
			GIS.seekg(p,std::ios::beg);

			plainistr.read(A.get(),readblocksize);
			GIS.read(B.get(),readblocksize);

			if ( GIS.gcount() != plainistr.gcount() )
				throw std::runtime_error("Unable to read same number of bytes from streams.");

			std::streamsize const r = GIS.gcount();

			if ( ! std::equal(A.get(),A.get()+r,B.get()) )
				throw std::runtime_error("Content mismatch.");
		}

		std::cerr << "Succesfully performed " << num << " reads of size " << readblocksize << " after random relative seeks from start of file" << std::endl;

		for ( unsigned int j = 0; j < num; ++j )
		{
			std::streamsize const p = -randNum(s);

			plainistr.clear();
			plainistr.seekg(p,std::ios::end);
			GIS.clear();
			GIS.seekg(p,std::ios::end);

			plainistr.read(A.get(),readblocksize);
			GIS.read(B.get(),readblocksize);

			if ( GIS.gcount() != plainistr.gcount() )
				throw std::runtime_error("Unable to read same number of bytes from streams.");

			std::streamsize const r = GIS.gcount();

			if ( ! std::equal(A.get(),A.get()+r,B.get()) )
				throw std::runtime_error("Content mismatch.");
		}

		std::cerr << "Succesfully performed " << num << " reads of size " << readblocksize << " after random relative seeks from end of file" << std::endl;

		for ( unsigned int j = 0; j < num; ++j )
		{
			plainistr.clear();
			std::streampos const pplain = plainistr.tellg();
			GIS.clear();
			std::streampos const pGIS = GIS.tellg();

			if ( pplain != pGIS )
				throw std::runtime_error("Position mismatch between plain and crypto stream.");

			std::streamsize const p =
				randNum(1)
				?
				-randNum(pplain)
				:
				randNum(s-pplain);

			assert ( pplain + p >= 0 && pplain+p <= s );

			plainistr.clear();
			plainistr.seekg(p,std::ios::cur);
			GIS.clear();
			GIS.seekg(p,std::ios::cur);

			plainistr.read(A.get(),readblocksize);
			GIS.read(B.get(),readblocksize);

			if ( GIS.gcount() != plainistr.gcount() )
				throw std::runtime_error("Unable to read same number of bytes from streams.");

			std::streamsize const r = GIS.gcount();

			if ( ! std::equal(A.get(),A.get()+r,B.get()) )
				throw std::runtime_error("Content mismatch.");
		}

		std::cerr << "Succesfully performed " << num << " reads of size " << readblocksize << " after random relative seeks from current position in file" << std::endl;

		return EXIT_SUCCESS;
	}
	catch(std::exception const & ex)
	{
		std::cerr << ex.what() << std::endl;
		return EXIT_FAILURE;
	}
}
