//
// ZipTest.cpp
//
// Copyright (c) 2007, Applied Informatics Software Engineering GmbH.
// and Contributors.
//
// SPDX-License-Identifier: BSL-1.0
//


#include "ZipTest.h"
#include "Poco/Zip/SkipCallback.h"
#include "Poco/Zip/ZipLocalFileHeader.h"
#include "Poco/Zip/ZipException.h"
#include "Poco/Zip/ZipArchive.h"
#include "Poco/Zip/ZipStream.h"
#include "Poco/Zip/Decompress.h"
#include "Poco/Zip/ZipCommon.h"
#include "Poco/StreamCopier.h"
#include "Poco/File.h"
#include "Poco/FileStream.h"
#include "Poco/URI.h"
#include "Poco/Path.h"
#include "Poco/Delegate.h"
#include "Poco/StreamCopier.h"
#include "Poco/Environment.h"
#include "CppUnit/TestCaller.h"
#include "CppUnit/TestSuite.h"
#undef min
#include <algorithm>
#include <iostream>
#include <sstream>


using namespace Poco::Zip;


ZipTest::ZipTest(const std::string& name): CppUnit::TestCase(name)
{
}


ZipTest::~ZipTest()
{
}


void ZipTest::testSkipSingleFile()
{
	std::string testFile = getTestFile("data", "test.zip");
	Poco::FileInputStream inp(testFile);
	assertTrue (inp.good());
	SkipCallback skip;
	ZipLocalFileHeader hdr(inp, false, skip);
	assertTrue (ZipCommon::HS_FAT == hdr.getHostSystem());
	int major = hdr.getMajorVersionNumber();
	[[maybe_unused]] int minor = hdr.getMinorVersionNumber();
	assertTrue (major <= 2);
	std::size_t hdrSize = hdr.getHeaderSize();
	assertTrue (hdrSize > 30);
	[[maybe_unused]] ZipCommon::CompressionMethod cm = hdr.getCompressionMethod();
	assertTrue (!hdr.isEncrypted());
	Poco::DateTime aDate = hdr.lastModifiedAt();
	[[maybe_unused]] Poco::UInt64 cS = hdr.getCompressedSize();
	[[maybe_unused]] Poco::UInt64 uS = hdr.getUncompressedSize();
	[[maybe_unused]] const std::string& fileName = hdr.getFileName();
}


void ZipTest::testCrcAndSizeAfterDataEncapsulated()
{
	// touch empty.txt
	// zip -fd foo.zip empty.txt
	// zip -fd encapsulated.zip foo.zip
	std::string testFile = getTestFile("data", "encapsulated.zip");
	Poco::FileInputStream inp(testFile);
	assertTrue(inp.good());

	ZipArchive arch(inp);
	ZipArchive::FileHeaders::const_iterator it = arch.findHeader("foo.zip");
	assertTrue(it != arch.headerEnd());
	inp.clear(); // inp eof(), should clear

	ZipInputStream zipin(inp, it->second);
	std::ostringstream out(std::ios::binary);
	Poco::StreamCopier::copyStream(zipin, out);

	std::string result = out.str();
	// sub zip
	std::istringstream istr(result);
	ZipArchive subArch(istr);
	it = subArch.findHeader("empty.txt");
	assertTrue(it != subArch.headerEnd());
	assertTrue(it->second.getCompressedSize() == 0);
	assertTrue(it->second.getUncompressedSize() == 0);
}


void ZipTest::testDecompressSingleFile()
{
	std::string testFile = getTestFile("data", "test.zip");
	Poco::FileInputStream inp(testFile);
	assertTrue (inp.good());
	ZipArchive arch(inp);
	ZipArchive::FileHeaders::const_iterator it = arch.findHeader("testfile.txt");
	assertTrue (it != arch.headerEnd());
	ZipInputStream zipin (inp, it->second);
	std::ostringstream out(std::ios::binary);
	Poco::StreamCopier::copyStream(zipin, out);
	assertTrue (!out.str().empty());
}


void ZipTest::testDecompressSingleFileInDir()
{
	std::string testFile = getTestFile("data","test.zip");
	Poco::FileInputStream inp(testFile);
	assertTrue (inp.good());
	ZipArchive arch(inp);
	ZipArchive::FileHeaders::const_iterator it = arch.findHeader("testdir/testfile.txt");
	assertTrue (it != arch.headerEnd());
	ZipInputStream zipin (inp, it->second);
	std::ostringstream out(std::ios::binary);
	Poco::StreamCopier::copyStream(zipin, out);
	assertTrue (!out.str().empty());
}


void ZipTest::testCrcAndSizeAfterData()
{
	std::string testFile = getTestFile("data", "data.zip");
	Poco::FileInputStream inp(testFile);
	assertTrue (inp.good());
	Decompress dec(inp, Poco::Path::temp());
	dec.EError += Poco::Delegate<ZipTest, std::pair<const Poco::Zip::ZipLocalFileHeader, const std::string>>(this, &ZipTest::onDecompressError);
	dec.decompressAllFiles();
	dec.EError -= Poco::Delegate<ZipTest, std::pair<const Poco::Zip::ZipLocalFileHeader, const std::string>>(this, &ZipTest::onDecompressError);
	assertTrue (_errCnt == 0);
	assertTrue (!dec.mapping().empty());
}


void ZipTest::testCrcAndSizeAfterDataWithArchive()
{
	std::string testFile = getTestFile("data", "data.zip");
	Poco::FileInputStream inp(testFile);
	assertTrue (inp.good());
	Poco::Zip::ZipArchive zip(inp);
	inp.clear();
	inp.seekg(0);
	Poco::Zip::ZipArchive::FileHeaders::const_iterator it = zip.headerBegin();
	for ( ; it!=zip.headerEnd(); ++it)
	{
		Poco::Zip::ZipInputStream zipis(inp,it->second);
		Poco::Path path(it->second.getFileName());
		if (path.isFile())
		{
			Poco::FileOutputStream os(Poco::Path::temp() + "test.dat");
			Poco::StreamCopier::copyStream(zipis,os);
		}
	}
}


std::string ZipTest::getTestFile(const std::string& directory, const std::string& file)
{
	std::ostringstream ostr;
	ostr << directory << '/' << file;
	std::string validDir(ostr.str());
	Poco::Path pathPattern(validDir);
	if (Poco::File(pathPattern).exists())
	{
		return validDir;
	}

	ostr.str("");
	ostr << "/Zip/testsuite/" << directory << '/' << file;
	validDir = Poco::Environment::get("POCO_BASE") + ostr.str();
	pathPattern = validDir;

	if (!Poco::File(pathPattern).exists())
	{
		std::cout << "Can't find " << validDir << std::endl;
		throw Poco::NotFoundException("cannot locate directory containing valid Zip test files");
	}
	return validDir;
}


void ZipTest::testDecompress()
{
	std::string testFile = getTestFile("data", "test.zip");
	Poco::FileInputStream inp(testFile);
	assertTrue (inp.good());
	Decompress dec(inp, Poco::Path::temp());
	dec.EError += Poco::Delegate<ZipTest, std::pair<const Poco::Zip::ZipLocalFileHeader, const std::string>>(this, &ZipTest::onDecompressError);
	dec.decompressAllFiles();
	dec.EError -= Poco::Delegate<ZipTest, std::pair<const Poco::Zip::ZipLocalFileHeader, const std::string>>(this, &ZipTest::onDecompressError);
	assertTrue (_errCnt == 0);
	assertTrue (!dec.mapping().empty());
}


void ZipTest::testDecompressFlat()
{
	std::string testFile = getTestFile("data", "test.zip");
	Poco::FileInputStream inp(testFile);
	assertTrue (inp.good());
	Decompress dec(inp, Poco::Path::temp(), true);
	dec.EError += Poco::Delegate<ZipTest, std::pair<const Poco::Zip::ZipLocalFileHeader, const std::string>>(this, &ZipTest::onDecompressError);
	dec.decompressAllFiles();
	dec.EError -= Poco::Delegate<ZipTest, std::pair<const Poco::Zip::ZipLocalFileHeader, const std::string>>(this, &ZipTest::onDecompressError);
	assertTrue (_errCnt == 0);
	assertTrue (!dec.mapping().empty());
}


void ZipTest::testDecompressVuln()
{
	std::string testFile = getTestFile("data", "vuln.zip");
	Poco::FileInputStream inp(testFile);
	assertTrue (inp.good());
	Decompress dec(inp, Poco::Path::temp());
	dec.EError += Poco::Delegate<ZipTest, std::pair<const Poco::Zip::ZipLocalFileHeader, const std::string>>(this, &ZipTest::onDecompressError);
	dec.decompressAllFiles();
	dec.EError -= Poco::Delegate<ZipTest, std::pair<const Poco::Zip::ZipLocalFileHeader, const std::string>>(this, &ZipTest::onDecompressError);
	assertTrue (_errCnt == 1);
	assertTrue (dec.mapping().empty());
}


void ZipTest::testDecompressFlatVuln()
{
	std::string testFile = getTestFile("data", "vuln.zip");
	Poco::FileInputStream inp(testFile);
	assertTrue (inp.good());
	Decompress dec(inp, Poco::Path::temp(), true);
	dec.EError += Poco::Delegate<ZipTest, std::pair<const Poco::Zip::ZipLocalFileHeader, const std::string>>(this, &ZipTest::onDecompressError);
	dec.decompressAllFiles();
	dec.EError -= Poco::Delegate<ZipTest, std::pair<const Poco::Zip::ZipLocalFileHeader, const std::string>>(this, &ZipTest::onDecompressError);
	assertTrue (_errCnt == 0);
	assertTrue (!dec.mapping().empty());
}


void ZipTest::verifyDataFile(const std::string& path, Poco::UInt64 size)
{
	Poco::FileInputStream in(path);
	assertTrue ( ! in.fail() );
	Poco::Buffer<char> buffer1(MB);
	Poco::Buffer<char> buffer2(MB);
	for (int i = 0; size != 0; i++)
	{
		std::memset(buffer1.begin(), i, buffer1.size());
		std::memset(buffer2.begin(), 0, buffer2.size());
		Poco::UInt64 bytesToRead = std::min(size, static_cast<Poco::UInt64>(buffer2.size()));
		in.read(buffer2.begin(), bytesToRead);
		assertTrue (!in.fail() );
		assertTrue (std::memcmp(buffer1.begin(), buffer2.begin(), static_cast<std::size_t>(bytesToRead)) == 0);
		size -= bytesToRead;
	}
	char c;
	in.read(&c, 1);
	assertTrue ( in.eof() );
}


void ZipTest::testDecompressZip64()
{
	std::map<std::string, Poco::UInt64> files;
	files[Poco::Path::temp() + "data1.bin"] = static_cast<Poco::UInt64>(KB)*4096+1;
	files[Poco::Path::temp() + "data2.bin"] = static_cast<Poco::UInt64>(KB)*16;
	files[Poco::Path::temp() + "data3.bin"] = static_cast<Poco::UInt64>(KB)*4096-1;

	for(std::map<std::string, Poco::UInt64>::const_iterator it = files.begin(); it != files.end(); it++)
	{
		Poco::File file(it->first);
		if(file.exists())
			file.remove();
	}
	Poco::FileInputStream in(Poco::Path::temp() + "zip64.zip");
	Decompress c(in, Poco::Path::temp());
	c.decompressAllFiles();
	for(std::map<std::string, Poco::UInt64>::const_iterator it = files.begin(); it != files.end(); it++)
	{
		verifyDataFile(it->first, it->second);
	}
}


void ZipTest::testDecompressConsistency()
{
	// test archives with borked file headers, but correct directory
	const std::string testArchives[] =
	{
		"consistency-crc32.zip",
		"consistency-uncompressed.zip"
	};


	for (const std::string &archive : testArchives)
	{
		//
		// test decompressing all files
		//
		{
			Poco::FileInputStream inputStream(getTestFile("data", archive));
			assertTrue (inputStream.good());

			Decompress dec(inputStream, Poco::Path::temp());

			try {
				dec.decompressAllFiles();
				assertTrue (false);
			}
			catch (const ZipException &e)
			{
			}
		}

		
		//
		// test decompressing all files (ignore consistency check)
		//
		{
			Poco::FileInputStream inputStream(getTestFile("data", archive));
			assertTrue (inputStream.good());

			Decompress dec(inputStream, Poco::Path::temp());

			// should not throw since consistency checks are ignored
			try {
				dec.decompressAllFiles(false);
			}
			catch (const ZipException &e)
			{
				assertTrue (false);
			}
		}


		//
		// test decompressing single file
		//
		{
			Poco::FileInputStream inputStream(getTestFile("data", archive));
			assertTrue (inputStream.good());

			ZipArchive archive(inputStream);

			// File decompression code is skipped since
			// we are not expecting archive to be processed further
			try
			{
				archive.checkConsistency();
				assertTrue (false);
			}
			catch (const ZipException &e)
			{
			}
		}
	};
}

void ZipTest::testValidPath()
{
	assertTrue (ZipCommon::isValidPath("."));
	assertTrue (ZipCommon::isValidPath("file.txt"));
	assertTrue (ZipCommon::isValidPath(".file.txt"));
	assertTrue (ZipCommon::isValidPath("..file.txt"));
	assertTrue (ZipCommon::isValidPath("file.txt.."));
	assertTrue (ZipCommon::isValidPath(".file..txt"));
	assertTrue (ZipCommon::isValidPath("~file..txt"));
	assertTrue (ZipCommon::isValidPath("~file/~"));
	assertTrue (ZipCommon::isValidPath("dir/~"));
	assertTrue (ZipCommon::isValidPath("some"));
	assertTrue (ZipCommon::isValidPath("some/dir"));
	assertTrue (ZipCommon::isValidPath("some/dir/or/another"));
	assertTrue (ZipCommon::isValidPath("some/dir/./another"));
	assertTrue (ZipCommon::isValidPath("some/dir/or/another/file.txt"));
	assertTrue (ZipCommon::isValidPath("s~me\\d.r\\.or..\\an..her\\file.txt"));
	assertTrue (ZipCommon::isValidPath("some\\dir\\or\\another"));
	assertTrue (ZipCommon::isValidPath("some\\dir\\or\\another\\file.txt"));
	assertTrue (ZipCommon::isValidPath("s~me\\d.r/.or..\\an..her\\file.txt"));

	assertTrue (!ZipCommon::isValidPath("/../"));
	assertTrue (!ZipCommon::isValidPath("/"));
	assertTrue (!ZipCommon::isValidPath("\\..\\"));
	assertTrue (!ZipCommon::isValidPath("/..\\"));
	assertTrue (!ZipCommon::isValidPath("\\../"));
	assertTrue (!ZipCommon::isValidPath(".."));
	assertTrue (!ZipCommon::isValidPath("~/"));
	assertTrue (!ZipCommon::isValidPath("~/~"));
	assertTrue (!ZipCommon::isValidPath("/~"));
	assertTrue (!ZipCommon::isValidPath("/file.txt"));
	assertTrue (!ZipCommon::isValidPath("~/file.txt"));
	assertTrue (!ZipCommon::isValidPath("some/dir/or/../another/file.txt"));
	assertTrue (!ZipCommon::isValidPath("C:\\Windows\\system32"));
}


void ZipTest::onDecompressError(const void* pSender, std::pair<const Poco::Zip::ZipLocalFileHeader, const std::string>& info)
{
	++_errCnt;
}


void ZipTest::setUp()
{
	_errCnt = 0;
}


void ZipTest::tearDown()
{
}


CppUnit::Test* ZipTest::suite()
{
	CppUnit::TestSuite* pSuite = new CppUnit::TestSuite("ZipTest");

	CppUnit_addTest(pSuite, ZipTest, testSkipSingleFile);
	CppUnit_addTest(pSuite, ZipTest, testDecompressSingleFile);
	CppUnit_addTest(pSuite, ZipTest, testDecompressSingleFileInDir);
	CppUnit_addTest(pSuite, ZipTest, testDecompress);
	CppUnit_addTest(pSuite, ZipTest, testDecompressFlat);
	CppUnit_addTest(pSuite, ZipTest, testDecompressVuln);
	CppUnit_addTest(pSuite, ZipTest, testDecompressFlatVuln);
	CppUnit_addTest(pSuite, ZipTest, testCrcAndSizeAfterData);
	CppUnit_addTest(pSuite, ZipTest, testCrcAndSizeAfterDataWithArchive);
	CppUnit_addTest(pSuite, ZipTest, testCrcAndSizeAfterDataEncapsulated);
	CppUnit_addTest(pSuite, ZipTest, testDecompressZip64);
	CppUnit_addTest(pSuite, ZipTest, testValidPath);
	CppUnit_addTest(pSuite, ZipTest, testDecompressConsistency);

	return pSuite;
}
