

//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
//_/_/
//_/_/  This file is part of CMSDK version 2.1.1.260726 (26 July 2026)
//_/_/  
//_/_/  developed by
//_/_/  
//_/_/  Communicative Machines Ltd. 
//_/_/    http://cmlabs.com/
//_/_/  Copyright (c) 2026
//_/_/  
//_/_/  in collaboration with 
//_/_/  
//_/_/  Icelandic Institute for Intelligent Machines ses
//_/_/    Menntavegur 1
//_/_/    101 Reykjavik
//_/_/    ICELAND
//_/_/    http://www.iiim.is/
//_/_/
//_/_/  and is released under the following
//_/_/  
//_/_/  EXTENDED BSD License
//_/_/
//_/_/  Redistribution and use in source and binary forms, with or without
//_/_/  modification, is permitted provided that the following conditions 
//_/_/  are met:
//_/_/
//_/_/  - Redistributions of source code must retain the above copyright 
//_/_/    and collaboration notice, this list of conditions and the following 
//_/_/    disclaimer.
//_/_/
//_/_/  - 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.
//_/_/
//_/_/  - Neither the name of its copyright holders nor the names of its 
//_/_/    contributors may be used to endorse or promote products derived from 
//_/_/    this software without specific prior written permission.
//_/_/
//_/_/  - CADIA Clause: The license granted in and to the software under this 
//_/_/    agreement is a limited-use license. The software may not be used in 
//_/_/    furtherance of: (i) intentionally causing bodily injury or severe 
//_/_/    emotional harm to any person; (ii) invading the personal privacy or 
//_/_/    violating the human rights of any person; or (iii) committing or 
//_/_/    preparing for any act of war. 
//_/_/
//_/_/  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 OWNER 
//_/_/  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.
//_/_/
//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
/*! @file HTML.cpp
	@brief Implementation of the HTML/URL helpers: entity encode/decode
	tables, extension-to-MIME-type mapping and URL component extraction.
	@ingroup Networking */
#include "HTML.h"
#include "PsyTime.h"
#include "UnitTestFramework.h"

namespace cmlabs {
namespace html {

std::string DecodeHTML(std::string str) {

	size_t p = 0, c;
	while ((p = str.find("+", p)) != std::string::npos) {
		str.replace(p, 1, 1, ' ');
	}

	char ch;
	p = 0;
	while ((p = str.find("&", p)) != std::string::npos) {
		if ( (c = str.find(";", p)) != std::string::npos ) {
			// Check string between p - c
			if (str[p+1] == '#') {
				if (str[p+2] == 'x')
					ch = (char) strtol(str.substr(p+3, c-p-3).c_str(), NULL, 16);
				else
					ch = (char) strtol(str.substr(p+2, c-p-2).c_str(), NULL, 10);
				if (ch == 0)
					ch = ' ';
				str.replace(p, c-p+1, 1, ch);
			}
			else {
				str.replace(p, c-p+1, 1, (char)html::HTML2Char(str.substr(p+1, c-p-1).c_str()));
			}
		}
		p++;
	}

	p = 0;
	while ((p = str.find("%", p)) != std::string::npos) {
		ch = (char) strtol(str.substr(p+1, 2).c_str(), NULL, 16);
		if (ch == 0)
			ch = ' ';
		str.replace(p, 3, 1, ch);
	}

	return str;
}

std::string EncodeHTML(std::string str) {
	if (str.size() == 0)
		return str;
	std::string result = str;
	char c;
	char* tmp = (char*)malloc(20);

	for (uint32 n=0; n<result.size(); n++) {
		switch(c = result[n]) {
			case '\n':
				result.replace(n, 1, "<br>\n");
				n+=4;
				break;
			case '\r':
				result.replace(n, 1, "");
				n-=1;
				break;
			case '\t':
				result.replace(n, 1, "&nbsp;&nbsp;&nbsp;");
				n+=17;
				break;
			default:
				if ( (c < 32) || (c > 122) ||
					( (c > 90) && (c < 97) ) ||
					( (c > 57) && (c < 65) ) ||
					( (c > 33) && (c < 48) ) ) {
					snprintf(tmp, 20, "&#x%x;", (int) c & 0xFF);
					result.replace(n, 1, tmp);
					n+=(uint32)strlen(tmp)-1;
				}
				break;
		}
	}

	free((char*)tmp);

	return result;
}

unsigned char HTML2Char(const char* str) {
	if (strcmp(str, "amp") == 0)
		return '&';
	else if (strcmp(str, "lt") == 0)
		return '<';
	else if (strcmp(str, "gt") == 0)
		return '>';
	else if (strcmp(str, "quot") == 0)
		return '"';
	else if (strcmp(str, "apos") == 0)
		return '\'';
	else if (strcmp(str, "lsquo") == 0)
		return (unsigned char)0x91;
	else if (strcmp(str, "rsquo") == 0)
		return (unsigned char)0x92;
	else if (strcmp(str, "sbquo") == 0)
		return (unsigned char)0x82;
	else if (strcmp(str, "ldquo") == 0)
		return (unsigned char)0x93;
	else if (strcmp(str, "rdquo") == 0)
		return (unsigned char)0x94;
	else if (strcmp(str, "bdquo") == 0)
		return (unsigned char)0x84;
	else if (strcmp(str, "dagger") == 0)
		return (unsigned char)0x86;
	else if (strcmp(str, "Dagger") == 0)
		return (unsigned char)0x87;
	else if (strcmp(str, "permil") == 0)
		return (unsigned char)0x89;
	else if (strcmp(str, "lsaquo") == 0)
		return (unsigned char)0x8B;
	else if (strcmp(str, "rsaquo") == 0)
		return (unsigned char)0x9B;
	else if (strcmp(str, "trade") == 0)
		return (unsigned char)0x99;
	else
		return ' ';
}


bool IsMimeBinary(const char* type) {
	if (utils::stristr(type, "text") || utils::stristr(type, "json"))
		return false;
	else
		return true;
}

bool GetMimeType(char* dest, const char* filename, uint32 maxsize) {
	bool isBinary;
	return GetMimeType(dest, filename, maxsize, isBinary);
}

bool GetMimeType(char* dest, const char* filename, uint32 maxsize, bool &isBinary) {

	isBinary = true;
	if (maxsize < 5) {
		utils::strcpyavail(dest, "", maxsize, false);
		return false;
	}
	else if (!filename) {
		return (utils::strcpyavail(dest, "application/octet-stream", maxsize, false) != 0);
	}

	if (utils::TextEndsWith(filename, ".bmp"))
		return (utils::strcpyavail(dest, "image/bmp", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".gif"))
		return (utils::strcpyavail(dest, "image/gif", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".jpg"))
		return (utils::strcpyavail(dest, "image/jpeg", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".png"))
		return (utils::strcpyavail(dest, "image/png", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".svg"))
		return (utils::strcpyavail(dest, "image/svg+xml", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".tif"))
		return (utils::strcpyavail(dest, "image/tiff", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".ico"))
		return (utils::strcpyavail(dest, "image/x-icon", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".apk"))
		return (utils::strcpyavail(dest, "application/vnd.android.package-archive", maxsize, false) != 0);

	else if ( (utils::TextEndsWith(filename, ".html")) || (utils::TextEndsWith(filename, ".htm")) ) {
		isBinary = false;
		return (utils::strcpyavail(dest, "text/html", maxsize, false) != 0);
	}
	else if (utils::TextEndsWith(filename, ".css")) {
		isBinary = false;
		return (utils::strcpyavail(dest, "text/css", maxsize, false) != 0);
	}
	else if (utils::TextEndsWith(filename, ".js")) {
		isBinary = false;
		return (utils::strcpyavail(dest, "text/javascript", maxsize, false) != 0);
	}
	else if (utils::TextEndsWith(filename, ".json")) { // should ideally be 'application/json', but IE <= 8 doesn't work :-)
		isBinary = false;
		return (utils::strcpyavail(dest, "application/json", maxsize, false) != 0);
		//return (utils::strcpyavail(dest, "text/html", maxsize, false) != 0);
	}
	else if (utils::TextEndsWith(filename, ".xml")) {
		isBinary = false;
		return (utils::strcpyavail(dest, "text/xml", maxsize, false) != 0);
	}
	else if (utils::TextEndsWith(filename, ".txt")) {
		isBinary = false;
		return (utils::strcpyavail(dest, "text/plain", maxsize, false) != 0);
	}
	else if (utils::TextEndsWith(filename, ".log")) {
		isBinary = false;
		return (utils::strcpyavail(dest, "text/plain", maxsize, false) != 0);
	}
	else if (utils::TextEndsWith(filename, ".bin"))
		return (utils::strcpyavail(dest, "application/octet-stream", maxsize, false) != 0);

	else if ( (utils::TextEndsWith(filename, ".mp3"))
		|| (utils::TextEndsWith(filename, ".mpg")) 
		|| (utils::TextEndsWith(filename, ".mpa")) )
		return (utils::strcpyavail(dest, "audio/mpeg", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".wav"))
		return (utils::strcpyavail(dest, "audio/x-wav", maxsize, false) != 0);
	else if ( (utils::TextEndsWith(filename, ".mpeg"))
		|| (utils::TextEndsWith(filename, ".mpe")) 
		|| (utils::TextEndsWith(filename, ".mp2")) 
		|| (utils::TextEndsWith(filename, ".mpv2")) )
		return (utils::strcpyavail(dest, "video/mpeg", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".mp4"))
		return (utils::strcpyavail(dest, "video/mp4", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".avi"))
		return (utils::strcpyavail(dest, "video/msvideo", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".h263"))
		return (utils::strcpyavail(dest, "video/h263", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".h264"))
		return (utils::strcpyavail(dest, "video/h264", maxsize, false) != 0);
	else if ( (utils::TextEndsWith(filename, ".qt")) || (utils::TextEndsWith(filename, ".mov")) )
		return (utils::strcpyavail(dest, "video/quicktime", maxsize, false) != 0);

	else if (utils::TextEndsWith(filename, ".zip"))
		return (utils::strcpyavail(dest, "application/zip", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".gz"))
		return (utils::strcpyavail(dest, "application/x-gzip", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".tar"))
		return (utils::strcpyavail(dest, "application/x-tar", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".pdf"))
		return (utils::strcpyavail(dest, "application/pdf", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".doc"))
		return (utils::strcpyavail(dest, "application/msword", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".xls"))
		return (utils::strcpyavail(dest, "application/vnd.ms-excel", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".ppt"))
		return (utils::strcpyavail(dest, "application/vnd.ms-powerpoint", maxsize, false) != 0);

	else if (utils::TextEndsWith(filename, ".eot"))
		return (utils::strcpyavail(dest, "application/vnd.ms-fontobject", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".woff"))
		return (utils::strcpyavail(dest, "application/font-woff", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".woff2"))
		return (utils::strcpyavail(dest, "application/font-woff2", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".ttf"))
		return (utils::strcpyavail(dest, "application/x-font-truetype", maxsize, false) != 0);
	else if (utils::TextEndsWith(filename, ".otf"))
		return (utils::strcpyavail(dest, "application/x-font-opentype", maxsize, false) != 0);

	else
		return (utils::strcpyavail(dest, "application/octet-stream", maxsize, false) != 0);
}


std::string GetUsernameFromURL(std::string url) {
	std::string auth = GetAuthenticationFromURL(url);
	if (!auth.size())
		return "";

	size_t colon = url.find(':');
	if (colon == std::string::npos)
		return auth;
	else
		return url.substr(0, colon);
}

std::string GetPasswordFromURL(std::string url) {
	std::string auth = GetAuthenticationFromURL(url);
	if (!auth.size())
		return "";

	size_t colon = url.find(':');
	if (colon == std::string::npos)
		return "";
	else
		return url.substr(colon+1);
}

std::string GetAuthenticationFromURL(std::string url) {
	size_t start = url.find("//");
	if (start == std::string::npos)
		return "";

	size_t offset = url.find("@");
	if (offset == std::string::npos)
		return "";

	return url.substr(start, offset-start);
}

std::string GetHostFromURL(std::string url) {
	size_t offset = url.find("@");
	if (offset == std::string::npos) {
		offset = url.find("//");
		if (offset == std::string::npos)
			return "";
		else
			offset += 2;
	}
	else
		offset++;

	size_t end = url.find(':', offset);
	if (end != std::string::npos)
		return url.substr(offset, end-offset);

	end = url.find('/', offset);
	if (end != std::string::npos)
		return url.substr(offset, end-offset);

	return url.substr(offset);
}

uint16 GetPortFromURL(std::string url) {
	size_t offset = url.find("@");
	if (offset == std::string::npos) {
		offset = url.find("//");
		if (offset == std::string::npos)
			return 0;
		else
			offset += 2;
	}
	else
		offset++;

	size_t colon = url.find(':', offset);
	if (colon == std::string::npos)
		return 0;

	return (uint16) utils::Ascii2Uint32(url.substr(colon+1).c_str());
}

std::string GetProtocolFromURL(std::string url) {
	if (url.find("http:") == 0)
		return "http";
	else if (url.find("https:") == 0)
		return "https";
	else if (url.find("ftp:") == 0)
		return "ftp";

	size_t offset = url.find("//");
	if (offset == std::string::npos)
		return "";

	return url.substr(0, offset);
}

std::string GetURIFromURL(std::string url) {
	size_t offset = url.find("//");
	if (offset == std::string::npos)
		return "";

	offset += 2;

	size_t end = url.find('/', offset);
	if (end != std::string::npos)
		return url.substr(end);
	else
		return "/";
}

bool HTML_UnitTest() {

	// 1. HTML entity decoding of named entities via HTML2Char
	unittest::progress(5, "named entity decoding");
	{
		if (HTML2Char("amp") != '&') { unittest::fail("HTML test: HTML2Char amp mismatch\n"); return false; }
		if (HTML2Char("lt") != '<') { unittest::fail("HTML test: HTML2Char lt mismatch\n"); return false; }
		if (HTML2Char("gt") != '>') { unittest::fail("HTML test: HTML2Char gt mismatch\n"); return false; }
		if (HTML2Char("quot") != '"') { unittest::fail("HTML test: HTML2Char quot mismatch\n"); return false; }
		if (HTML2Char("apos") != '\'') { unittest::fail("HTML test: HTML2Char apos mismatch\n"); return false; }
		// Unknown entity falls back to space
		if (HTML2Char("notanentity") != ' ') { unittest::fail("HTML test: HTML2Char unknown not space\n"); return false; }
	}

	// 2. EncodeHTML escaping of special characters
	unittest::progress(20, "encode escaping");
	{
		// Plain alphanumerics are left untouched
		if (EncodeHTML("Hello123") != "Hello123") { unittest::fail("HTML test: EncodeHTML altered plain text\n"); return false; }
		// Empty string stays empty
		if (EncodeHTML("") != "") { unittest::fail("HTML test: EncodeHTML empty not empty\n"); return false; }
		// Special chars are escaped to numeric hex entities
		if (EncodeHTML("<") != "&#x3c;") { unittest::fail("HTML test: EncodeHTML '<' mismatch\n"); return false; }
		if (EncodeHTML(">") != "&#x3e;") { unittest::fail("HTML test: EncodeHTML '>' mismatch\n"); return false; }
		if (EncodeHTML("&") != "&#x26;") { unittest::fail("HTML test: EncodeHTML '&' mismatch\n"); return false; }
		// Newline becomes a line break, carriage return is stripped, tab becomes spaces
		if (EncodeHTML("\n") != "<br>\n") { unittest::fail("HTML test: EncodeHTML newline mismatch\n"); return false; }
		if (EncodeHTML("\r") != "") { unittest::fail("HTML test: EncodeHTML carriage return not stripped\n"); return false; }
		if (EncodeHTML("\t") != "&nbsp;&nbsp;&nbsp;") { unittest::fail("HTML test: EncodeHTML tab mismatch\n"); return false; }
		unittest::detail("EncodeHTML('a<b>c') = '%s'", EncodeHTML("a<b>c").c_str());
	}

	// 3. DecodeHTML of named, decimal, hex and percent encodings
	unittest::progress(40, "decode decoding");
	{
		// Named entities
		if (DecodeHTML("a&amp;b") != "a&b") { unittest::fail("HTML test: DecodeHTML &amp; mismatch\n"); return false; }
		if (DecodeHTML("&lt;tag&gt;") != "<tag>") { unittest::fail("HTML test: DecodeHTML lt/gt mismatch\n"); return false; }
		// Decimal numeric entity (65 == 'A')
		if (DecodeHTML("&#65;") != "A") { unittest::fail("HTML test: DecodeHTML decimal mismatch\n"); return false; }
		// Hex numeric entity (0x42 == 'B')
		if (DecodeHTML("&#x42;") != "B") { unittest::fail("HTML test: DecodeHTML hex mismatch\n"); return false; }
		// Plus signs decode to spaces
		if (DecodeHTML("hello+world") != "hello world") { unittest::fail("HTML test: DecodeHTML plus mismatch\n"); return false; }
		// Percent encoding (%20 == space)
		if (DecodeHTML("a%20b") != "a b") { unittest::fail("HTML test: DecodeHTML percent mismatch\n"); return false; }
		// Round-trip of encoded special characters back through decode
		if (DecodeHTML(EncodeHTML("<>&")) != "<>&") { unittest::fail("HTML test: encode/decode round-trip mismatch\n"); return false; }
	}

	// 4. MIME type lookup by file extension
	unittest::progress(55, "mime type lookup");
	{
		char mime[128];
		bool isBinary = true;
		if (!GetMimeType(mime, "page.html", sizeof(mime), isBinary) || strcmp(mime, "text/html") != 0) {
			unittest::fail("HTML test: GetMimeType html mismatch\n"); return false;
		}
		if (isBinary) { unittest::fail("HTML test: GetMimeType html flagged binary\n"); return false; }
		if (!GetMimeType(mime, "image.png", sizeof(mime), isBinary) || strcmp(mime, "image/png") != 0) {
			unittest::fail("HTML test: GetMimeType png mismatch\n"); return false;
		}
		if (!isBinary) { unittest::fail("HTML test: GetMimeType png not flagged binary\n"); return false; }
		if (!GetMimeType(mime, "data.json", sizeof(mime), isBinary) || strcmp(mime, "application/json") != 0) {
			unittest::fail("HTML test: GetMimeType json mismatch\n"); return false;
		}
		// Unknown extension falls back to octet-stream
		if (!GetMimeType(mime, "file.unknownext", sizeof(mime), isBinary) || strcmp(mime, "application/octet-stream") != 0) {
			unittest::fail("HTML test: GetMimeType unknown fallback mismatch\n"); return false;
		}
		// Three-arg overload should agree with the four-arg form
		if (!GetMimeType(mime, "doc.txt", sizeof(mime)) || strcmp(mime, "text/plain") != 0) {
			unittest::fail("HTML test: GetMimeType 3-arg txt mismatch\n"); return false;
		}
		// Binary classification of MIME strings
		if (IsMimeBinary("text/html")) { unittest::fail("HTML test: IsMimeBinary text/html true\n"); return false; }
		if (IsMimeBinary("application/json")) { unittest::fail("HTML test: IsMimeBinary json true\n"); return false; }
		if (!IsMimeBinary("image/png")) { unittest::fail("HTML test: IsMimeBinary image/png false\n"); return false; }
	}

	// 5. URL component parsing
	unittest::progress(70, "URL parsing");
	{
		std::string url = "http://user:pass@example.com:8080/path/to/page";
		if (GetProtocolFromURL(url) != "http") { unittest::fail("HTML test: GetProtocolFromURL mismatch\n"); return false; }
		if (GetHostFromURL(url) != "example.com") { unittest::fail("HTML test: GetHostFromURL mismatch\n"); return false; }
		if (GetPortFromURL(url) != 8080) { unittest::fail("HTML test: GetPortFromURL mismatch\n"); return false; }
		if (GetURIFromURL(url) != "/path/to/page") { unittest::fail("HTML test: GetURIFromURL mismatch\n"); return false; }

		// https protocol detection
		if (GetProtocolFromURL("https://secure.example.com/") != "https") { unittest::fail("HTML test: GetProtocolFromURL https mismatch\n"); return false; }

		// Host without credentials or port
		std::string simple = "http://host.example.org/";
		if (GetHostFromURL(simple) != "host.example.org") { unittest::fail("HTML test: GetHostFromURL simple mismatch\n"); return false; }
		if (GetPortFromURL(simple) != 0) { unittest::fail("HTML test: GetPortFromURL simple not zero\n"); return false; }

		// URL with no path returns root URI
		if (GetURIFromURL("http://host.example.org") != "/") { unittest::fail("HTML test: GetURIFromURL root mismatch\n"); return false; }
		unittest::detail("parsed host=%s port=%u", GetHostFromURL(url).c_str(), (unsigned)GetPortFromURL(url));
	}

	// 6. Throughput metric for encode/decode
	unittest::progress(85, "throughput");
	{
		const int N = 20000;
		std::string sample = "Tom & Jerry <said> \"hi\"\nwith\ttabs";
		uint64 t0 = GetTimeNow();
		std::string enc;
		for (int i = 0; i < N; i++)
			enc = EncodeHTML(sample);
		double encUs = (double)(GetTimeNow() - t0);
		if (enc.empty()) { unittest::fail("HTML test: EncodeHTML produced empty result\n"); return false; }

		t0 = GetTimeNow();
		std::string dec;
		for (int i = 0; i < N; i++)
			dec = DecodeHTML(enc);
		double decUs = (double)(GetTimeNow() - t0);
		if (dec.empty()) { unittest::fail("HTML test: DecodeHTML produced empty result\n"); return false; }

		if (encUs > 0)
			unittest::metric("encode_throughput", (double)N / encUs * 1e6, "ops/s", true);
		if (decUs > 0)
			unittest::metric("decode_throughput", (double)N / decUs * 1e6, "ops/s", true);
	}

	unittest::progress(100, "done");
	return true;
}

} // namespace html

void Register_HTML_Tests() {
	UnitTestRunner::instance().registerTest("html", html::HTML_UnitTest,
		"HTML entity encode/decode, MIME type lookup, URL parsing", "util");
}

} // namespace cmlabs


