mirror of
https://github.com/vczh-libraries/Release.git
synced 2026-08-17 09:21:41 +08:00
16301 lines
432 KiB
C++
16301 lines
432 KiB
C++
/***********************************************************************
|
|
THIS FILE IS AUTOMATICALLY GENERATED. DO NOT MODIFY
|
|
DEVELOPER: Zihan Chen(vczh)
|
|
***********************************************************************/
|
|
#include "VlppOS.h"
|
|
|
|
/***********************************************************************
|
|
.\FILESYSTEM.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
|
|
namespace vl
|
|
{
|
|
namespace filesystem
|
|
{
|
|
using namespace collections;
|
|
using namespace stream;
|
|
|
|
extern IFileSystemImpl* GetFileSystemImpl();
|
|
|
|
static wchar_t GetInjectedDelimiter()
|
|
{
|
|
return GetFileSystemImpl()->GetPathDelimiter();
|
|
}
|
|
|
|
static WString GetInjectedDelimiterString()
|
|
{
|
|
return WString::FromChar(GetInjectedDelimiter());
|
|
}
|
|
|
|
// ReadDirectoryChangesW
|
|
|
|
/***********************************************************************
|
|
FilePath
|
|
***********************************************************************/
|
|
|
|
void FilePath::NormalizeDelimiters(collections::Array<wchar_t>& buffer)
|
|
{
|
|
auto delimiter = GetInjectedDelimiter();
|
|
auto compatibleDelimiters = GetFileSystemImpl()->GetCompatibleDelimiters();
|
|
for (vint i = 0; i < buffer.Count(); i++)
|
|
{
|
|
for (auto reading = compatibleDelimiters; *reading; reading++)
|
|
{
|
|
if (*reading == buffer[i])
|
|
{
|
|
buffer[i] = delimiter;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FilePath::TrimLastDelimiter(WString& fullPath)
|
|
{
|
|
auto delimiter = GetInjectedDelimiter();
|
|
if (!GetFileSystemImpl()->IsRoot(fullPath) && fullPath.Length() > 0 && fullPath[fullPath.Length() - 1] == delimiter)
|
|
{
|
|
fullPath = fullPath.Left(fullPath.Length() - 1);
|
|
}
|
|
}
|
|
|
|
FilePath::FilePath()
|
|
{
|
|
Initialize();
|
|
}
|
|
|
|
FilePath::FilePath(const WString& _filePath)
|
|
:fullPath(_filePath)
|
|
{
|
|
Initialize();
|
|
}
|
|
|
|
FilePath::FilePath(const wchar_t* _filePath)
|
|
:fullPath(_filePath)
|
|
{
|
|
Initialize();
|
|
}
|
|
|
|
FilePath::FilePath(const FilePath& _filePath)
|
|
:fullPath(_filePath.fullPath)
|
|
{
|
|
}
|
|
|
|
WString FilePath::GetName()const
|
|
{
|
|
auto delimiter = GetInjectedDelimiterString();
|
|
auto index = INVLOC.FindLast(fullPath, delimiter, Locale::None);
|
|
if (index.key == -1) return fullPath;
|
|
return fullPath.Right(fullPath.Length() - index.key - 1);
|
|
}
|
|
|
|
FilePath FilePath::GetFolder()const
|
|
{
|
|
auto delimiter = GetInjectedDelimiterString();
|
|
auto index = INVLOC.FindLast(fullPath, delimiter, Locale::None);
|
|
if (index.key == -1) return FilePath();
|
|
return fullPath.Left(index.key);
|
|
}
|
|
|
|
WString FilePath::GetFullPath()const
|
|
{
|
|
return fullPath;
|
|
}
|
|
|
|
void FilePath::GetPathComponents(WString path, collections::List<WString>& components)
|
|
{
|
|
WString pathRemaining = path;
|
|
auto delimiter = GetInjectedDelimiter();
|
|
auto delimiterString = WString::FromChar(delimiter);
|
|
auto doubleDelimiter = delimiterString + delimiterString;
|
|
|
|
components.Clear();
|
|
|
|
while (true)
|
|
{
|
|
auto index = INVLOC.FindFirst(pathRemaining, delimiterString, Locale::None);
|
|
if (index.key == -1)
|
|
break;
|
|
|
|
if (index.key != 0)
|
|
components.Add(pathRemaining.Left(index.key));
|
|
else
|
|
{
|
|
if (pathRemaining.Length() >= 2 && pathRemaining[1] == delimiter)
|
|
{
|
|
components.Add(doubleDelimiter);
|
|
index.value++;
|
|
}
|
|
else if (GetFileSystemImpl()->IsRoot(delimiterString))
|
|
{
|
|
components.Add(delimiterString);
|
|
}
|
|
}
|
|
|
|
pathRemaining = pathRemaining.Right(pathRemaining.Length() - (index.key + index.value));
|
|
}
|
|
|
|
if (pathRemaining.Length() != 0)
|
|
{
|
|
components.Add(pathRemaining);
|
|
}
|
|
}
|
|
|
|
WString FilePath::ComponentsToPath(const collections::List<WString>& components)
|
|
{
|
|
WString result;
|
|
auto delimiter = GetInjectedDelimiterString();
|
|
auto doubleDelimiter = delimiter + delimiter;
|
|
|
|
vint i = 0;
|
|
|
|
if (components.Count() > 0)
|
|
{
|
|
if (components[0] == doubleDelimiter)
|
|
{
|
|
result += doubleDelimiter;
|
|
i++;
|
|
}
|
|
else if (components[0] == delimiter)
|
|
{
|
|
result += delimiter;
|
|
i++;
|
|
}
|
|
}
|
|
|
|
for (; i < components.Count(); i++)
|
|
{
|
|
if (result.Length() > 0 && result[result.Length() - 1] != GetInjectedDelimiter())
|
|
{
|
|
result += delimiter;
|
|
}
|
|
result += components[i];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/***********************************************************************
|
|
File
|
|
***********************************************************************/
|
|
|
|
File::File(const FilePath& _filePath)
|
|
:filePath(_filePath)
|
|
{
|
|
}
|
|
|
|
const FilePath& File::GetFilePath()const
|
|
{
|
|
return filePath;
|
|
}
|
|
|
|
bool File::ReadAllTextWithEncodingTesting(WString& text, stream::BomEncoder::Encoding& encoding, bool& containsBom)
|
|
{
|
|
Array<unsigned char> buffer;
|
|
{
|
|
FileStream fileStream(filePath.GetFullPath(), FileStream::ReadOnly);
|
|
if (!fileStream.IsAvailable()) return false;
|
|
if (fileStream.Size() == 0)
|
|
{
|
|
text = L"";
|
|
encoding = BomEncoder::Mbcs;
|
|
containsBom = false;
|
|
return true;
|
|
}
|
|
|
|
buffer.Resize((vint)fileStream.Size());
|
|
vint count = fileStream.Read(&buffer[0], buffer.Count());
|
|
CHECK_ERROR(count == buffer.Count(), L"vl::filesystem::File::ReadAllTextWithEncodingTesting(WString&, BomEncoder::Encoding&, bool&)#Failed to read the whole file.");
|
|
}
|
|
TestEncoding(&buffer[0], buffer.Count(), encoding, containsBom);
|
|
|
|
MemoryWrapperStream memoryStream(&buffer[0], buffer.Count());
|
|
if (containsBom)
|
|
{
|
|
BomDecoder decoder;
|
|
DecoderStream decoderStream(memoryStream, decoder);
|
|
StreamReader reader(decoderStream);
|
|
text = reader.ReadToEnd();
|
|
}
|
|
else
|
|
{
|
|
switch (encoding)
|
|
{
|
|
case BomEncoder::Utf8:
|
|
{
|
|
Utf8Decoder decoder;
|
|
DecoderStream decoderStream(memoryStream, decoder);
|
|
StreamReader reader(decoderStream);
|
|
text = reader.ReadToEnd();
|
|
}
|
|
break;
|
|
case BomEncoder::Utf16:
|
|
{
|
|
Utf16Decoder decoder;
|
|
DecoderStream decoderStream(memoryStream, decoder);
|
|
StreamReader reader(decoderStream);
|
|
text = reader.ReadToEnd();
|
|
}
|
|
break;
|
|
case BomEncoder::Utf16BE:
|
|
{
|
|
Utf16BEDecoder decoder;
|
|
DecoderStream decoderStream(memoryStream, decoder);
|
|
StreamReader reader(decoderStream);
|
|
text = reader.ReadToEnd();
|
|
}
|
|
break;
|
|
default:
|
|
{
|
|
MbcsDecoder decoder;
|
|
DecoderStream decoderStream(memoryStream, decoder);
|
|
StreamReader reader(decoderStream);
|
|
text = reader.ReadToEnd();
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
WString File::ReadAllTextByBom()const
|
|
{
|
|
WString text;
|
|
ReadAllTextByBom(text);
|
|
return text;
|
|
}
|
|
|
|
bool File::ReadAllTextByBom(WString& text)const
|
|
{
|
|
FileStream fileStream(filePath.GetFullPath(), FileStream::ReadOnly);
|
|
if (!fileStream.IsAvailable()) return false;
|
|
BomDecoder decoder;
|
|
DecoderStream decoderStream(fileStream, decoder);
|
|
StreamReader reader(decoderStream);
|
|
text = reader.ReadToEnd();
|
|
return true;
|
|
}
|
|
|
|
bool File::ReadAllLinesByBom(collections::List<WString>& lines)const
|
|
{
|
|
FileStream fileStream(filePath.GetFullPath(), FileStream::ReadOnly);
|
|
if (!fileStream.IsAvailable()) return false;
|
|
BomDecoder decoder;
|
|
DecoderStream decoderStream(fileStream, decoder);
|
|
StreamReader reader(decoderStream);
|
|
while (!reader.IsEnd())
|
|
{
|
|
lines.Add(reader.ReadLine());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool File::WriteAllText(const WString& text, bool bom, stream::BomEncoder::Encoding encoding)
|
|
{
|
|
FileStream fileStream(filePath.GetFullPath(), FileStream::WriteOnly);
|
|
if (!fileStream.IsAvailable()) return false;
|
|
|
|
IEncoder* encoder = nullptr;
|
|
if (bom)
|
|
{
|
|
encoder = new BomEncoder(encoding);
|
|
}
|
|
else switch (encoding)
|
|
{
|
|
case BomEncoder::Utf8:
|
|
encoder = new Utf8Encoder;
|
|
break;
|
|
case BomEncoder::Utf16:
|
|
encoder = new Utf16Encoder;
|
|
break;
|
|
case BomEncoder::Utf16BE:
|
|
encoder = new Utf16BEEncoder;
|
|
break;
|
|
default:
|
|
encoder = new MbcsEncoder;
|
|
break;
|
|
}
|
|
|
|
{
|
|
EncoderStream encoderStream(fileStream, *encoder);
|
|
StreamWriter writer(encoderStream);
|
|
writer.WriteString(text);
|
|
}
|
|
delete encoder;
|
|
return true;
|
|
}
|
|
|
|
bool File::WriteAllLines(collections::List<WString>& lines, bool bom, stream::BomEncoder::Encoding encoding)
|
|
{
|
|
FileStream fileStream(filePath.GetFullPath(), FileStream::WriteOnly);
|
|
if (!fileStream.IsAvailable()) return false;
|
|
|
|
IEncoder* encoder = nullptr;
|
|
if (bom)
|
|
{
|
|
encoder = new BomEncoder(encoding);
|
|
}
|
|
else switch (encoding)
|
|
{
|
|
case BomEncoder::Utf8:
|
|
encoder = new Utf8Encoder;
|
|
break;
|
|
case BomEncoder::Utf16:
|
|
encoder = new Utf16Encoder;
|
|
break;
|
|
case BomEncoder::Utf16BE:
|
|
encoder = new Utf16BEEncoder;
|
|
break;
|
|
default:
|
|
encoder = new MbcsEncoder;
|
|
break;
|
|
}
|
|
|
|
{
|
|
EncoderStream encoderStream(fileStream, *encoder);
|
|
StreamWriter writer(encoderStream);
|
|
for (auto line : lines)
|
|
{
|
|
writer.WriteLine(line);
|
|
}
|
|
}
|
|
delete encoder;
|
|
return true;
|
|
}
|
|
|
|
bool File::Exists()const
|
|
{
|
|
return filePath.IsFile();
|
|
}
|
|
|
|
/***********************************************************************
|
|
Folder
|
|
***********************************************************************/
|
|
|
|
Folder::Folder(const FilePath& _filePath)
|
|
:filePath(_filePath)
|
|
{
|
|
}
|
|
|
|
const FilePath& Folder::GetFilePath()const
|
|
{
|
|
return filePath;
|
|
}
|
|
|
|
bool Folder::Exists()const
|
|
{
|
|
return filePath.IsFolder();
|
|
}
|
|
|
|
bool Folder::Create(bool recursively)const
|
|
{
|
|
if (recursively)
|
|
{
|
|
auto folder = filePath.GetFolder();
|
|
if (folder.IsFile()) return false;
|
|
if (folder.IsFolder()) return Create(false);
|
|
return Folder(folder).Create(true) && Create(false);
|
|
}
|
|
else
|
|
{
|
|
return CreateNonRecursively();
|
|
}
|
|
}
|
|
|
|
bool Folder::Delete(bool recursively)const
|
|
{
|
|
if (!Exists()) return false;
|
|
|
|
if (recursively)
|
|
{
|
|
List<Folder> folders;
|
|
GetFolders(folders);
|
|
for (auto folder : folders)
|
|
{
|
|
if (!folder.Delete(true)) return false;
|
|
}
|
|
|
|
List<File> files;
|
|
GetFiles(files);
|
|
for (auto file : files)
|
|
{
|
|
if (!file.Delete()) return false;
|
|
}
|
|
|
|
return Delete(false);
|
|
}
|
|
return DeleteNonRecursively();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\FILESYSTEM.INJECTABLE.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
|
|
namespace vl
|
|
{
|
|
namespace filesystem
|
|
{
|
|
extern IFileSystemImpl* GetOSFileSystemImpl();
|
|
|
|
feature_injection::FeatureInjection<IFileSystemImpl>& GetFileSystemInjection()
|
|
{
|
|
static feature_injection::FeatureInjection<IFileSystemImpl> injection(GetOSFileSystemImpl());
|
|
return injection;
|
|
}
|
|
|
|
void InjectFileSystemImpl(IFileSystemImpl* impl)
|
|
{
|
|
GetFileSystemInjection().Inject(impl);
|
|
}
|
|
|
|
IFileSystemImpl* GetFileSystemImpl()
|
|
{
|
|
return GetFileSystemInjection().Get();
|
|
}
|
|
|
|
void EjectFileSystemImpl(IFileSystemImpl* impl)
|
|
{
|
|
if (impl == nullptr)
|
|
{
|
|
GetFileSystemInjection().EjectAll();
|
|
}
|
|
else
|
|
{
|
|
GetFileSystemInjection().Eject(impl);
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
FilePath
|
|
***********************************************************************/
|
|
|
|
wchar_t FilePath::GetPathDelimiter()
|
|
{
|
|
return GetFileSystemImpl()->GetPathDelimiter();
|
|
}
|
|
|
|
void FilePath::Initialize()
|
|
{
|
|
GetFileSystemImpl()->Initialize(fullPath);
|
|
}
|
|
|
|
FilePath FilePath::operator/(const WString& relativePath) const
|
|
{
|
|
return GetFileSystemImpl()->ConcatPath(fullPath, relativePath);
|
|
}
|
|
|
|
bool FilePath::IsFile() const
|
|
{
|
|
return GetFileSystemImpl()->IsFile(fullPath);
|
|
}
|
|
|
|
bool FilePath::IsFolder() const
|
|
{
|
|
return GetFileSystemImpl()->IsFolder(fullPath);
|
|
}
|
|
|
|
bool FilePath::IsRoot() const
|
|
{
|
|
return GetFileSystemImpl()->IsRoot(fullPath);
|
|
}
|
|
|
|
WString FilePath::GetRelativePathFor(const FilePath& _filePath) const
|
|
{
|
|
return GetFileSystemImpl()->GetRelativePathFor(fullPath, _filePath.GetFullPath());
|
|
}
|
|
|
|
/***********************************************************************
|
|
File
|
|
***********************************************************************/
|
|
|
|
bool File::Delete() const
|
|
{
|
|
return GetFileSystemImpl()->FileDelete(filePath);
|
|
}
|
|
|
|
bool File::Rename(const WString& newName) const
|
|
{
|
|
return GetFileSystemImpl()->FileRename(filePath, newName);
|
|
}
|
|
|
|
/***********************************************************************
|
|
Folder
|
|
***********************************************************************/
|
|
|
|
bool Folder::GetFolders(collections::List<Folder>& folders) const
|
|
{
|
|
return GetFileSystemImpl()->GetFolders(filePath, folders);
|
|
}
|
|
|
|
bool Folder::GetFiles(collections::List<File>& files) const
|
|
{
|
|
return GetFileSystemImpl()->GetFiles(filePath, files);
|
|
}
|
|
|
|
bool Folder::CreateNonRecursively() const
|
|
{
|
|
return GetFileSystemImpl()->CreateFolder(filePath);
|
|
}
|
|
|
|
bool Folder::DeleteNonRecursively() const
|
|
{
|
|
return GetFileSystemImpl()->DeleteFolder(filePath);
|
|
}
|
|
|
|
bool Folder::Rename(const WString& newName) const
|
|
{
|
|
return GetFileSystemImpl()->FolderRename(filePath, newName);
|
|
}
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
.\LOCALE.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
|
|
namespace vl
|
|
{
|
|
using namespace collections;
|
|
|
|
/***********************************************************************
|
|
EnUsLocaleImpl
|
|
***********************************************************************/
|
|
|
|
#ifdef VCZH_GCC
|
|
#define _wcsicmp wcscasecmp
|
|
#define _wcsnicmp wcsncasecmp
|
|
#endif
|
|
|
|
Locale EnUsLocaleImpl::Invariant() const
|
|
{
|
|
return Locale(L"");
|
|
}
|
|
|
|
Locale EnUsLocaleImpl::SystemDefault() const
|
|
{
|
|
return Locale(L"en-US");
|
|
}
|
|
|
|
Locale EnUsLocaleImpl::UserDefault() const
|
|
{
|
|
return Locale(L"en-US");
|
|
}
|
|
|
|
void EnUsLocaleImpl::Enumerate(List<Locale>& locales) const
|
|
{
|
|
locales.Add(Locale(L"en-US"));
|
|
}
|
|
|
|
void EnUsLocaleImpl::GetShortDateFormats(const WString&, List<WString>& formats) const
|
|
{
|
|
formats.Add(L"MM/dd/yyyy");
|
|
formats.Add(L"yyyy-MM-dd");
|
|
}
|
|
|
|
void EnUsLocaleImpl::GetLongDateFormats(const WString&, List<WString>& formats) const
|
|
{
|
|
formats.Add(L"dddd, dd MMMM yyyy");
|
|
}
|
|
|
|
void EnUsLocaleImpl::GetYearMonthDateFormats(const WString&, List<WString>& formats) const
|
|
{
|
|
formats.Add(L"yyyy MMMM");
|
|
}
|
|
|
|
void EnUsLocaleImpl::GetLongTimeFormats(const WString&, List<WString>& formats) const
|
|
{
|
|
formats.Add(L"HH:mm:ss");
|
|
}
|
|
|
|
void EnUsLocaleImpl::GetShortTimeFormats(const WString&, List<WString>& formats) const
|
|
{
|
|
formats.Add(L"HH:mm");
|
|
formats.Add(L"hh:mm tt");
|
|
}
|
|
|
|
WString EnUsLocaleImpl::FormatDate(const WString& localeName, const WString& format, DateTime date) const
|
|
{
|
|
/*
|
|
auto df = L"yyyy,MM,MMM,MMMM,dd,ddd,dddd";
|
|
auto ds = L"2000,01,Jan,January,02,Sun,Sunday";
|
|
auto tf = L"hh,HH,mm,ss,tt";
|
|
auto ts = L"01,13,02,03,PM";
|
|
*/
|
|
|
|
WString result;
|
|
const wchar_t* reading = format.Buffer();
|
|
while (*reading)
|
|
{
|
|
if (wcsncmp(reading, L"yyyy", 4) == 0)
|
|
{
|
|
WString fragment = itow(date.year);
|
|
while (fragment.Length() < 4) fragment = L"0" + fragment;
|
|
result += fragment;
|
|
reading += 4;
|
|
}
|
|
else if (wcsncmp(reading, L"MMMM", 4) == 0)
|
|
{
|
|
result += GetLongMonthName(localeName, date.month);
|
|
reading += 4;
|
|
}
|
|
else if (wcsncmp(reading, L"MMM", 3) == 0)
|
|
{
|
|
result += GetShortMonthName(localeName, date.month);
|
|
reading += 3;
|
|
}
|
|
else if (wcsncmp(reading, L"MM", 2) == 0)
|
|
{
|
|
WString fragment = itow(date.month);
|
|
while (fragment.Length() < 2) fragment = L"0" + fragment;
|
|
result += fragment;
|
|
reading += 2;
|
|
}
|
|
else if (wcsncmp(reading, L"dddd", 4) == 0)
|
|
{
|
|
result += GetLongDayOfWeekName(localeName, date.dayOfWeek);
|
|
reading += 4;
|
|
}
|
|
else if (wcsncmp(reading, L"ddd", 3) == 0)
|
|
{
|
|
result += GetShortDayOfWeekName(localeName, date.dayOfWeek);
|
|
reading += 3;
|
|
}
|
|
else if (wcsncmp(reading, L"dd", 2) == 0)
|
|
{
|
|
WString fragment = itow(date.day);
|
|
while (fragment.Length() < 2) fragment = L"0" + fragment;
|
|
result += fragment;
|
|
reading += 2;
|
|
}
|
|
else if (wcsncmp(reading, L"hh", 2) == 0)
|
|
{
|
|
WString fragment = itow(date.hour > 12 ? date.hour - 12 : date.hour);
|
|
while (fragment.Length() < 2) fragment = L"0" + fragment;
|
|
result += fragment;
|
|
reading += 2;
|
|
}
|
|
else if (wcsncmp(reading, L"HH", 2) == 0)
|
|
{
|
|
WString fragment = itow(date.hour);
|
|
while (fragment.Length() < 2) fragment = L"0" + fragment;
|
|
result += fragment;
|
|
reading += 2;
|
|
}
|
|
else if (wcsncmp(reading, L"mm", 2) == 0)
|
|
{
|
|
WString fragment = itow(date.minute);
|
|
while (fragment.Length() < 2) fragment = L"0" + fragment;
|
|
result += fragment;
|
|
reading += 2;
|
|
}
|
|
else if (wcsncmp(reading, L"ss", 2) == 0)
|
|
{
|
|
WString fragment = itow(date.second);
|
|
while (fragment.Length() < 2) fragment = L"0" + fragment;
|
|
result += fragment;
|
|
reading += 2;
|
|
}
|
|
else if (wcsncmp(reading, L"tt", 2) == 0)
|
|
{
|
|
result += date.hour > 12 ? L"PM" : L"AM";
|
|
reading += 2;
|
|
}
|
|
else
|
|
{
|
|
result += WString::FromChar(*reading);
|
|
reading++;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
WString EnUsLocaleImpl::FormatTime(const WString& localeName, const WString& format, DateTime time) const
|
|
{
|
|
return FormatDate(localeName, format, time);
|
|
}
|
|
|
|
WString EnUsLocaleImpl::FormatNumber(const WString&, const WString& number) const
|
|
{
|
|
return number;
|
|
}
|
|
|
|
WString EnUsLocaleImpl::FormatCurrency(const WString&, const WString& currency) const
|
|
{
|
|
return currency;
|
|
}
|
|
|
|
WString EnUsLocaleImpl::GetShortDayOfWeekName(const WString&, vint dayOfWeek) const
|
|
{
|
|
switch (dayOfWeek)
|
|
{
|
|
case 0: return L"Sun";
|
|
case 1: return L"Mon";
|
|
case 2: return L"Tue";
|
|
case 3: return L"Wed";
|
|
case 4: return L"Thu";
|
|
case 5: return L"Fri";
|
|
case 6: return L"Sat";
|
|
}
|
|
return L"";
|
|
}
|
|
|
|
WString EnUsLocaleImpl::GetLongDayOfWeekName(const WString&, vint dayOfWeek) const
|
|
{
|
|
switch (dayOfWeek)
|
|
{
|
|
case 0: return L"Sunday";
|
|
case 1: return L"Monday";
|
|
case 2: return L"Tuesday";
|
|
case 3: return L"Wednesday";
|
|
case 4: return L"Thursday";
|
|
case 5: return L"Friday";
|
|
case 6: return L"Saturday";
|
|
}
|
|
return L"";
|
|
}
|
|
|
|
WString EnUsLocaleImpl::GetShortMonthName(const WString&, vint month) const
|
|
{
|
|
switch (month)
|
|
{
|
|
case 1: return L"Jan";
|
|
case 2: return L"Feb";
|
|
case 3: return L"Mar";
|
|
case 4: return L"Apr";
|
|
case 5: return L"May";
|
|
case 6: return L"Jun";
|
|
case 7: return L"Jul";
|
|
case 8: return L"Aug";
|
|
case 9: return L"Sep";
|
|
case 10: return L"Oct";
|
|
case 11: return L"Nov";
|
|
case 12: return L"Dec";
|
|
}
|
|
return L"";
|
|
}
|
|
|
|
WString EnUsLocaleImpl::GetLongMonthName(const WString&, vint month) const
|
|
{
|
|
switch (month)
|
|
{
|
|
case 1: return L"January";
|
|
case 2: return L"February";
|
|
case 3: return L"March";
|
|
case 4: return L"April";
|
|
case 5: return L"May";
|
|
case 6: return L"June";
|
|
case 7: return L"July";
|
|
case 8: return L"August";
|
|
case 9: return L"September";
|
|
case 10: return L"October";
|
|
case 11: return L"November";
|
|
case 12: return L"December";
|
|
}
|
|
return L"";
|
|
}
|
|
|
|
WString EnUsLocaleImpl::ToLower(const WString&, const WString& str) const
|
|
{
|
|
return wlower(str);
|
|
}
|
|
|
|
WString EnUsLocaleImpl::ToUpper(const WString&, const WString& str) const
|
|
{
|
|
return wupper(str);
|
|
}
|
|
|
|
WString EnUsLocaleImpl::ToLinguisticLower(const WString&, const WString& str) const
|
|
{
|
|
return wlower(str);
|
|
}
|
|
|
|
WString EnUsLocaleImpl::ToLinguisticUpper(const WString&, const WString& str) const
|
|
{
|
|
return wupper(str);
|
|
}
|
|
|
|
vint EnUsLocaleImpl::Compare(const WString&, const WString& s1, const WString& s2, Locale::Normalization normalization) const
|
|
{
|
|
switch (normalization)
|
|
{
|
|
case Locale::Normalization::None:
|
|
return wcscmp(s1.Buffer(), s2.Buffer());
|
|
case Locale::Normalization::IgnoreCase:
|
|
return _wcsicmp(s1.Buffer(), s2.Buffer());
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
vint EnUsLocaleImpl::CompareOrdinal(const WString& s1, const WString& s2) const
|
|
{
|
|
return wcscmp(s1.Buffer(), s2.Buffer());
|
|
}
|
|
|
|
vint EnUsLocaleImpl::CompareOrdinalIgnoreCase(const WString& s1, const WString& s2) const
|
|
{
|
|
return _wcsicmp(s1.Buffer(), s2.Buffer());
|
|
}
|
|
|
|
Pair<vint, vint> EnUsLocaleImpl::FindFirst(const WString&, const WString& text, const WString& find, Locale::Normalization normalization) const
|
|
{
|
|
if (text.Length() < find.Length() || find.Length() == 0)
|
|
{
|
|
return Pair<vint, vint>(-1, 0);
|
|
}
|
|
const wchar_t* result = nullptr;
|
|
switch (normalization)
|
|
{
|
|
case Locale::Normalization::None:
|
|
{
|
|
const wchar_t* reading = text.Buffer();
|
|
while (*reading)
|
|
{
|
|
if (wcsncmp(reading, find.Buffer(), find.Length()) == 0)
|
|
{
|
|
result = reading;
|
|
break;
|
|
}
|
|
reading++;
|
|
}
|
|
}
|
|
break;
|
|
case Locale::Normalization::IgnoreCase:
|
|
{
|
|
const wchar_t* reading = text.Buffer();
|
|
while (*reading)
|
|
{
|
|
if (_wcsnicmp(reading, find.Buffer(), find.Length()) == 0)
|
|
{
|
|
result = reading;
|
|
break;
|
|
}
|
|
reading++;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
return result == nullptr ? Pair<vint, vint>(-1, 0) : Pair<vint, vint>(result - text.Buffer(), find.Length());
|
|
}
|
|
|
|
Pair<vint, vint> EnUsLocaleImpl::FindLast(const WString&, const WString& text, const WString& find, Locale::Normalization normalization) const
|
|
{
|
|
if (text.Length() < find.Length() || find.Length() == 0)
|
|
{
|
|
return Pair<vint, vint>(-1, 0);
|
|
}
|
|
const wchar_t* result = nullptr;
|
|
switch (normalization)
|
|
{
|
|
case Locale::Normalization::None:
|
|
{
|
|
const wchar_t* reading = text.Buffer();
|
|
while (*reading)
|
|
{
|
|
if (wcsncmp(reading, find.Buffer(), find.Length()) == 0)
|
|
{
|
|
result = reading;
|
|
}
|
|
reading++;
|
|
}
|
|
}
|
|
break;
|
|
case Locale::Normalization::IgnoreCase:
|
|
{
|
|
const wchar_t* reading = text.Buffer();
|
|
while (*reading)
|
|
{
|
|
if (_wcsnicmp(reading, find.Buffer(), find.Length()) == 0)
|
|
{
|
|
result = reading;
|
|
}
|
|
reading++;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
return result == nullptr ? Pair<vint, vint>(-1, 0) : Pair<vint, vint>(result - text.Buffer(), find.Length());
|
|
}
|
|
|
|
bool EnUsLocaleImpl::StartsWith(const WString&, const WString& text, const WString& find, Locale::Normalization normalization) const
|
|
{
|
|
if (text.Length() < find.Length() || find.Length() == 0)
|
|
{
|
|
return false;
|
|
}
|
|
switch (normalization)
|
|
{
|
|
case Locale::Normalization::None:
|
|
return wcsncmp(text.Buffer(), find.Buffer(), find.Length()) == 0;
|
|
case Locale::Normalization::IgnoreCase:
|
|
return _wcsnicmp(text.Buffer(), find.Buffer(), find.Length()) == 0;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool EnUsLocaleImpl::EndsWith(const WString&, const WString& text, const WString& find, Locale::Normalization normalization) const
|
|
{
|
|
if (text.Length() < find.Length() || find.Length() == 0)
|
|
{
|
|
return false;
|
|
}
|
|
switch (normalization)
|
|
{
|
|
case Locale::Normalization::None:
|
|
return wcsncmp(text.Buffer() + text.Length() - find.Length(), find.Buffer(), find.Length()) == 0;
|
|
case Locale::Normalization::IgnoreCase:
|
|
return _wcsnicmp(text.Buffer() + text.Length() - find.Length(), find.Buffer(), find.Length()) == 0;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#ifdef VCZH_GCC
|
|
#undef _wcsicmp
|
|
#undef _wcsnicmp
|
|
#endif
|
|
|
|
/***********************************************************************
|
|
InjectLocaleImpl
|
|
***********************************************************************/
|
|
|
|
extern ILocaleImpl* GetOSLocaleImpl();
|
|
|
|
feature_injection::FeatureInjection<ILocaleImpl>& GetLocaleInjection()
|
|
{
|
|
static feature_injection::FeatureInjection<ILocaleImpl> injection(GetOSLocaleImpl());
|
|
return injection;
|
|
}
|
|
|
|
void InjectLocaleImpl(ILocaleImpl* impl)
|
|
{
|
|
GetLocaleInjection().Inject(impl);
|
|
}
|
|
|
|
void EjectLocaleImpl(ILocaleImpl* impl)
|
|
{
|
|
if (impl == nullptr)
|
|
{
|
|
GetLocaleInjection().EjectAll();
|
|
}
|
|
else
|
|
{
|
|
GetLocaleInjection().Eject(impl);
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
Locale
|
|
***********************************************************************/
|
|
|
|
Locale::Locale(const WString& _localeName)
|
|
:localeName(_localeName)
|
|
{
|
|
}
|
|
|
|
const WString& Locale::GetName()const
|
|
{
|
|
return localeName;
|
|
}
|
|
|
|
/***********************************************************************
|
|
Locale (static)
|
|
***********************************************************************/
|
|
|
|
Locale Locale::Invariant()
|
|
{
|
|
return GetLocaleInjection().Get()->Invariant();
|
|
}
|
|
|
|
Locale Locale::SystemDefault()
|
|
{
|
|
return GetLocaleInjection().Get()->SystemDefault();
|
|
}
|
|
|
|
Locale Locale::UserDefault()
|
|
{
|
|
return GetLocaleInjection().Get()->UserDefault();
|
|
}
|
|
|
|
void Locale::Enumerate(collections::List<Locale>& locales)
|
|
{
|
|
GetLocaleInjection().Get()->Enumerate(locales);
|
|
}
|
|
|
|
/***********************************************************************
|
|
Locale (ILocaleImpl redirections)
|
|
***********************************************************************/
|
|
|
|
void Locale::GetShortDateFormats(collections::List<WString>& formats)const
|
|
{
|
|
GetLocaleInjection().Get()->GetShortDateFormats(localeName, formats);
|
|
}
|
|
|
|
void Locale::GetLongDateFormats(collections::List<WString>& formats)const
|
|
{
|
|
GetLocaleInjection().Get()->GetLongDateFormats(localeName, formats);
|
|
}
|
|
|
|
void Locale::GetYearMonthDateFormats(collections::List<WString>& formats)const
|
|
{
|
|
GetLocaleInjection().Get()->GetYearMonthDateFormats(localeName, formats);
|
|
}
|
|
|
|
void Locale::GetLongTimeFormats(collections::List<WString>& formats)const
|
|
{
|
|
GetLocaleInjection().Get()->GetLongTimeFormats(localeName, formats);
|
|
}
|
|
|
|
void Locale::GetShortTimeFormats(collections::List<WString>& formats)const
|
|
{
|
|
GetLocaleInjection().Get()->GetShortTimeFormats(localeName, formats);
|
|
}
|
|
|
|
WString Locale::FormatDate(const WString& format, DateTime date)const
|
|
{
|
|
return GetLocaleInjection().Get()->FormatDate(localeName, format, date);
|
|
}
|
|
|
|
WString Locale::FormatTime(const WString& format, DateTime time)const
|
|
{
|
|
return GetLocaleInjection().Get()->FormatTime(localeName, format, time);
|
|
}
|
|
|
|
WString Locale::FormatNumber(const WString& number)const
|
|
{
|
|
return GetLocaleInjection().Get()->FormatNumber(localeName, number);
|
|
}
|
|
|
|
WString Locale::FormatCurrency(const WString& currency)const
|
|
{
|
|
return GetLocaleInjection().Get()->FormatCurrency(localeName, currency);
|
|
}
|
|
|
|
WString Locale::GetShortDayOfWeekName(vint dayOfWeek)const
|
|
{
|
|
return GetLocaleInjection().Get()->GetShortDayOfWeekName(localeName, dayOfWeek);
|
|
}
|
|
|
|
WString Locale::GetLongDayOfWeekName(vint dayOfWeek)const
|
|
{
|
|
return GetLocaleInjection().Get()->GetLongDayOfWeekName(localeName, dayOfWeek);
|
|
}
|
|
|
|
WString Locale::GetShortMonthName(vint month)const
|
|
{
|
|
return GetLocaleInjection().Get()->GetShortMonthName(localeName, month);
|
|
}
|
|
|
|
WString Locale::GetLongMonthName(vint month)const
|
|
{
|
|
return GetLocaleInjection().Get()->GetLongMonthName(localeName, month);
|
|
}
|
|
|
|
WString Locale::ToLower(const WString& str)const
|
|
{
|
|
return GetLocaleInjection().Get()->ToLower(localeName, str);
|
|
}
|
|
|
|
WString Locale::ToUpper(const WString& str)const
|
|
{
|
|
return GetLocaleInjection().Get()->ToUpper(localeName, str);
|
|
}
|
|
|
|
WString Locale::ToLinguisticLower(const WString& str)const
|
|
{
|
|
return GetLocaleInjection().Get()->ToLinguisticLower(localeName, str);
|
|
}
|
|
|
|
WString Locale::ToLinguisticUpper(const WString& str)const
|
|
{
|
|
return GetLocaleInjection().Get()->ToLinguisticUpper(localeName, str);
|
|
}
|
|
|
|
vint Locale::Compare(const WString& s1, const WString& s2, Normalization normalization)const
|
|
{
|
|
return GetLocaleInjection().Get()->Compare(localeName, s1, s2, normalization);
|
|
}
|
|
|
|
vint Locale::CompareOrdinal(const WString& s1, const WString& s2)const
|
|
{
|
|
return GetLocaleInjection().Get()->CompareOrdinal(s1, s2);
|
|
}
|
|
|
|
vint Locale::CompareOrdinalIgnoreCase(const WString& s1, const WString& s2)const
|
|
{
|
|
return GetLocaleInjection().Get()->CompareOrdinalIgnoreCase(s1, s2);
|
|
}
|
|
|
|
collections::Pair<vint, vint> Locale::FindFirst(const WString& text, const WString& find, Normalization normalization)const
|
|
{
|
|
return GetLocaleInjection().Get()->FindFirst(localeName, text, find, normalization);
|
|
}
|
|
|
|
collections::Pair<vint, vint> Locale::FindLast(const WString& text, const WString& find, Normalization normalization)const
|
|
{
|
|
return GetLocaleInjection().Get()->FindLast(localeName, text, find, normalization);
|
|
}
|
|
|
|
bool Locale::StartsWith(const WString& text, const WString& find, Normalization normalization)const
|
|
{
|
|
return GetLocaleInjection().Get()->StartsWith(localeName, text, find, normalization);
|
|
}
|
|
|
|
bool Locale::EndsWith(const WString& text, const WString& find, Normalization normalization)const
|
|
{
|
|
return GetLocaleInjection().Get()->EndsWith(localeName, text, find, normalization);
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\THREADING.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
|
|
#if defined VCZH_ARM
|
|
#include <arm_acle.h>
|
|
#elif defined VCZH_MSVC || defined VCZH_GCC
|
|
#include <emmintrin.h>
|
|
#endif
|
|
|
|
namespace vl
|
|
{
|
|
|
|
/***********************************************************************
|
|
SpinLock
|
|
***********************************************************************/
|
|
|
|
SpinLock::Scope::Scope(SpinLock& _spinLock)
|
|
:spinLock(&_spinLock)
|
|
{
|
|
spinLock->Enter();
|
|
}
|
|
|
|
SpinLock::Scope::~Scope()
|
|
{
|
|
spinLock->Leave();
|
|
}
|
|
|
|
bool SpinLock::TryEnter()
|
|
{
|
|
return token.exchange(1) == 0;
|
|
}
|
|
|
|
void SpinLock::Enter()
|
|
{
|
|
while (true)
|
|
{
|
|
vint expected = 0;
|
|
if (token.compare_exchange_strong(expected, 1))
|
|
{
|
|
return;
|
|
}
|
|
while (token != 0)
|
|
{
|
|
#ifdef VCZH_ARM
|
|
__yield();
|
|
#else
|
|
_mm_pause();
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
void SpinLock::Leave()
|
|
{
|
|
token.exchange(0);
|
|
}
|
|
|
|
/***********************************************************************
|
|
ThreadLocalStorage
|
|
***********************************************************************/
|
|
|
|
void ThreadLocalStorage::Clear()
|
|
{
|
|
CHECK_ERROR(!disposed, L"vl::ThreadLocalStorage::Clear()#Cannot access a disposed ThreadLocalStorage.");
|
|
if(destructor)
|
|
{
|
|
if (auto data = Get())
|
|
{
|
|
destructor(data);
|
|
}
|
|
}
|
|
Set(nullptr);
|
|
}
|
|
|
|
void ThreadLocalStorage::Dispose()
|
|
{
|
|
CHECK_ERROR(!disposed, L"vl::ThreadLocalStorage::Dispose()#Cannot access a disposed ThreadLocalStorage.");
|
|
Clear();
|
|
disposed = true;
|
|
}
|
|
|
|
struct TlsStorageLink
|
|
{
|
|
ThreadLocalStorage* storage = nullptr;
|
|
TlsStorageLink* next = nullptr;
|
|
};
|
|
|
|
volatile bool tlsFixed = false;
|
|
TlsStorageLink* tlsHead = nullptr;
|
|
TlsStorageLink** tlsTail = &tlsHead;
|
|
|
|
void ThreadLocalStorage::PushStorage(ThreadLocalStorage* storage)
|
|
{
|
|
CHECK_ERROR(!tlsFixed, L"vl::ThreadLocalStorage::PushStorage(ThreadLocalStorage*)#Cannot create new ThreadLocalStorage instance after calling ThreadLocalStorage::FixStorages().");
|
|
auto link = new TlsStorageLink;
|
|
link->storage = storage;
|
|
*tlsTail = link;
|
|
tlsTail = &link->next;
|
|
}
|
|
|
|
void ThreadLocalStorage::FixStorages()
|
|
{
|
|
tlsFixed = true;
|
|
}
|
|
|
|
void ThreadLocalStorage::ClearStorages()
|
|
{
|
|
FixStorages();
|
|
auto current = tlsHead;
|
|
while (current)
|
|
{
|
|
current->storage->Clear();
|
|
current = current->next;
|
|
}
|
|
}
|
|
|
|
void ThreadLocalStorage::DisposeStorages()
|
|
{
|
|
FixStorages();
|
|
auto current = tlsHead;
|
|
tlsHead = nullptr;
|
|
tlsTail = nullptr;
|
|
while (current)
|
|
{
|
|
current->storage->Dispose();
|
|
|
|
auto temp = current;
|
|
current = current->next;
|
|
delete temp;
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
TaskQueue
|
|
***********************************************************************/
|
|
|
|
TaskQueue::TaskQueue()
|
|
{
|
|
CHECK_ERROR(semaphoreTasks.Create(0, 65536), L"vl::TaskQueue::TaskQueue()#Failed to create the task semaphore.");
|
|
}
|
|
|
|
TaskQueue::~TaskQueue()
|
|
{
|
|
}
|
|
|
|
void TaskQueue::QueueTask(Func<void()> task)
|
|
{
|
|
SPIN_LOCK(lockTasks)
|
|
{
|
|
tasks.Add(task);
|
|
}
|
|
semaphoreTasks.Release();
|
|
}
|
|
|
|
void TaskQueue::QueueExitTask()
|
|
{
|
|
SPIN_LOCK(lockTasks)
|
|
{
|
|
exitTaskQueued = true;
|
|
}
|
|
semaphoreTasks.Release();
|
|
}
|
|
|
|
void TaskQueue::RunTaskQueue()
|
|
{
|
|
while (true)
|
|
{
|
|
Func<void()> task;
|
|
bool hasTask = false;
|
|
bool shouldExit = false;
|
|
SPIN_LOCK(lockTasks)
|
|
{
|
|
if (tasks.Count() > 0)
|
|
{
|
|
task = tasks[0];
|
|
tasks.RemoveAt(0);
|
|
hasTask = true;
|
|
}
|
|
else
|
|
{
|
|
shouldExit = exitTaskQueued;
|
|
}
|
|
}
|
|
|
|
if (shouldExit)
|
|
{
|
|
break;
|
|
}
|
|
if (!hasTask)
|
|
{
|
|
semaphoreTasks.Wait();
|
|
continue;
|
|
}
|
|
task();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\ENCODING\BASE64ENCODING.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
|
|
namespace vl
|
|
{
|
|
namespace stream
|
|
{
|
|
const char8_t Utf8Base64Codes[] = u8"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
|
|
/***********************************************************************
|
|
Utf8Base64Encoder
|
|
***********************************************************************/
|
|
|
|
void Utf8Base64Encoder::WriteBytesToCharArray(uint8_t* fromBytes, char8_t(&toChars)[Base64CycleChars], vint bytes)
|
|
{
|
|
switch (bytes)
|
|
{
|
|
case 1:
|
|
{
|
|
toChars[0] = Utf8Base64Codes[fromBytes[0] >> 2];
|
|
toChars[1] = Utf8Base64Codes[(fromBytes[0] % (1 << 2)) << 4];
|
|
toChars[2] = u8'=';
|
|
toChars[3] = u8'=';
|
|
}
|
|
break;
|
|
case 2:
|
|
{
|
|
toChars[0] = Utf8Base64Codes[fromBytes[0] >> 2];
|
|
toChars[1] = Utf8Base64Codes[((fromBytes[0] % (1 << 2)) << 4) | (fromBytes[1] >> 4)];
|
|
toChars[2] = Utf8Base64Codes[(fromBytes[1] % (1 << 4)) << 2];
|
|
toChars[3] = u8'=';
|
|
}
|
|
break;
|
|
case 3:
|
|
{
|
|
toChars[0] = Utf8Base64Codes[fromBytes[0] >> 2];
|
|
toChars[1] = Utf8Base64Codes[((fromBytes[0] % (1 << 2)) << 4) | (fromBytes[1] >> 4)];
|
|
toChars[2] = Utf8Base64Codes[((fromBytes[1] % (1 << 4)) << 2) | (fromBytes[2] >> 6)];
|
|
toChars[3] = Utf8Base64Codes[fromBytes[2] % (1 << 6)];
|
|
}
|
|
break;
|
|
default:
|
|
CHECK_FAIL(L"vl::stream::Utf8Base64Encoder::WriteBytesToCharArray(uint8_t*, char8_t(&)[Base64CycleChars], vint)#Parameter bytes should be 1, 2 or 3.");
|
|
}
|
|
}
|
|
|
|
bool Utf8Base64Encoder::WriteCycle(uint8_t*& reading, vint& _size)
|
|
{
|
|
if (_size <= 0) return false;
|
|
vint bytes = _size < Base64CycleBytes ? _size : Base64CycleBytes;
|
|
|
|
char8_t chars[Base64CycleChars];
|
|
WriteBytesToCharArray(reading, chars, bytes);
|
|
vint writtenBytes = stream->Write(chars, Base64CycleChars);
|
|
CHECK_ERROR(writtenBytes == Base64CycleChars, L"vl::stream::Utf8Base64Encoder::WriteCycle(uint8_t*&, vint&)#The underlying stream failed to accept enough base64 characters.");
|
|
|
|
reading += bytes;
|
|
_size -= bytes;
|
|
return true;
|
|
}
|
|
|
|
bool Utf8Base64Encoder::WriteCache(uint8_t*& reading, vint& _size)
|
|
{
|
|
if (cacheSize > 0 || _size < Base64CycleBytes)
|
|
{
|
|
vint copiedBytes = Base64CycleBytes - cacheSize;
|
|
if (copiedBytes > _size) copiedBytes = _size;
|
|
if (copiedBytes > 0)
|
|
{
|
|
memcpy(cache + cacheSize, reading, copiedBytes);
|
|
reading += copiedBytes;
|
|
_size -= copiedBytes;
|
|
cacheSize += copiedBytes;
|
|
}
|
|
}
|
|
|
|
if (cacheSize == 0) return _size > 0;
|
|
if (cacheSize == Base64CycleBytes)
|
|
{
|
|
uint8_t* cacheReading = cache;
|
|
return WriteCycle(cacheReading, cacheSize);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
vint Utf8Base64Encoder::Write(void* _buffer, vint _size)
|
|
{
|
|
uint8_t* reading = (uint8_t*)_buffer;
|
|
|
|
// flush cache if any
|
|
if (!WriteCache(reading, _size)) goto FINISHED_WRITING;
|
|
|
|
// run Base64 encoding
|
|
while (_size >= Base64CycleBytes)
|
|
{
|
|
if (!WriteCycle(reading, _size)) goto FINISHED_WRITING;
|
|
}
|
|
|
|
// run the last Base64 encoding cycle and wrote a postfix to cache
|
|
WriteCache(reading, _size);
|
|
|
|
FINISHED_WRITING:
|
|
return reading - (uint8_t*)_buffer;
|
|
}
|
|
|
|
void Utf8Base64Encoder::Close()
|
|
{
|
|
if (cacheSize > 0)
|
|
{
|
|
char8_t chars[Base64CycleChars];
|
|
WriteBytesToCharArray(cache, chars, cacheSize);
|
|
vint writtenBytes = stream->Write(chars, Base64CycleChars);
|
|
CHECK_ERROR(writtenBytes == Base64CycleChars, L"vl::stream::Utf8Base64Encoder::Close()#The underlying stream failed to accept enough base64 characters.");
|
|
|
|
cacheSize = 0;
|
|
}
|
|
EncoderBase::Close();
|
|
}
|
|
|
|
/***********************************************************************
|
|
Utf8Base64Decoder
|
|
***********************************************************************/
|
|
|
|
vint Utf8Base64Decoder::ReadBytesFromCharArray(char8_t(&fromChars)[Base64CycleChars], uint8_t* toBytes)
|
|
{
|
|
uint8_t nums[Base64CycleChars];
|
|
for (vint i = 0; i < Base64CycleChars; i++)
|
|
{
|
|
char8_t c = fromChars[i];
|
|
if (u8'A' <= c && c <= u8'Z') nums[i] = c - u8'A';
|
|
else if (u8'a' <= c && c <= u8'z') nums[i] = c - u8'a' + 26;
|
|
else if (u8'0' <= c && c <= u8'9') nums[i] = c - u8'0' + 52;
|
|
else switch (c)
|
|
{
|
|
case '+':nums[i] = 62; break;
|
|
case '/':nums[i] = 63; break;
|
|
case '=':nums[i] = 0; break;
|
|
default:
|
|
CHECK_FAIL(L"vl::stream::Utf8Base64Decoder::ReadBytesFromCharArray(char(&)[Base64CycleChars], uint8_t*)#Illegal Base64 character.");
|
|
}
|
|
}
|
|
|
|
toBytes[0] = (nums[0] << 2) | (nums[1] >> 4);
|
|
if (fromChars[2] == u8'=') return 1;
|
|
toBytes[1] = ((nums[1] % (1 << 4)) << 4) | (nums[2] >> 2);
|
|
if (fromChars[3] == u8'=') return 2;
|
|
toBytes[2] = ((nums[2] % (1 << 2)) << 6) | nums[3];
|
|
return 3;
|
|
}
|
|
|
|
vint Utf8Base64Decoder::ReadCycle(uint8_t*& writing, vint& _size)
|
|
{
|
|
char8_t chars[Base64CycleChars];
|
|
vint readChars = stream->Read((void*)chars, Base64CycleChars);
|
|
if (readChars == 0) return 0;
|
|
CHECK_ERROR(readChars == Base64CycleChars, L"vl::stream::Utf8Base64Decoder::ReadCycle(uint8_t*&, vint&)#The underlying stream failed to provide enough base64 characters.");
|
|
|
|
vint readBytes = ReadBytesFromCharArray(chars, writing);
|
|
writing += readBytes;
|
|
_size -= readBytes;
|
|
return readBytes;
|
|
}
|
|
|
|
void Utf8Base64Decoder::ReadCache(uint8_t*& writing, vint& _size)
|
|
{
|
|
if (cacheSize > 0)
|
|
{
|
|
vint copiedBytes = cacheSize;
|
|
if (copiedBytes > _size) copiedBytes = _size;
|
|
if (copiedBytes > 0)
|
|
{
|
|
memcpy(writing, cache, copiedBytes);
|
|
writing += copiedBytes;
|
|
_size -= copiedBytes;
|
|
cacheSize -= copiedBytes;
|
|
if (cacheSize > 0)
|
|
{
|
|
memmove(cache, cache + copiedBytes, cacheSize);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
vint Utf8Base64Decoder::Read(void* _buffer, vint _size)
|
|
{
|
|
uint8_t* writing = (uint8_t*)_buffer;
|
|
|
|
// write cache to buffer if any
|
|
ReadCache(writing, _size);
|
|
|
|
// run Base64 decoding
|
|
while (_size >= Base64CycleBytes)
|
|
{
|
|
if (ReadCycle(writing, _size) == 0) goto FINISHED_READING;
|
|
}
|
|
|
|
// run the last Base64 decoding cycle and write a prefix to buffer
|
|
if (_size > 0)
|
|
{
|
|
uint8_t* cacheWriting = cache;
|
|
vint temp = 0;
|
|
if ((cacheSize = ReadCycle(cacheWriting, temp)) == 0) goto FINISHED_READING;
|
|
ReadCache(writing, _size);
|
|
}
|
|
FINISHED_READING:
|
|
return writing - (uint8_t*)_buffer;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\ENCODING\ENCODING.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
|
|
namespace vl
|
|
{
|
|
namespace stream
|
|
{
|
|
/***********************************************************************
|
|
EncoderBase
|
|
***********************************************************************/
|
|
|
|
void EncoderBase::Setup(IStream* _stream)
|
|
{
|
|
stream = _stream;
|
|
}
|
|
|
|
void EncoderBase::Close()
|
|
{
|
|
}
|
|
|
|
/***********************************************************************
|
|
DecoderBase
|
|
***********************************************************************/
|
|
|
|
void DecoderBase::Setup(IStream* _stream)
|
|
{
|
|
stream = _stream;
|
|
}
|
|
|
|
void DecoderBase::Close()
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\ENCODING\LZWENCODING.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
|
|
namespace vl
|
|
{
|
|
namespace stream
|
|
{
|
|
using namespace collections;
|
|
using namespace lzw;
|
|
|
|
/***********************************************************************
|
|
LzwBase
|
|
***********************************************************************/
|
|
|
|
void LzwBase::UpdateIndexBits()
|
|
{
|
|
if (nextIndex >=2 && (nextIndex & (nextIndex - 1)) == 0)
|
|
{
|
|
indexBits++;
|
|
}
|
|
}
|
|
|
|
lzw::Code* LzwBase::CreateCode(lzw::Code* prefix, vuint8_t byte)
|
|
{
|
|
if (nextIndex < MaxDictionarySize)
|
|
{
|
|
Code* code = codeAllocator.Create();
|
|
code->byte = byte;
|
|
code->code = nextIndex;
|
|
code->parent = prefix;
|
|
code->size = prefix->size + 1;
|
|
prefix->children.Set(byte, code, mapAllocator);
|
|
nextIndex++;
|
|
|
|
return code;
|
|
}
|
|
else
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
LzwBase::LzwBase()
|
|
:codeAllocator(65536)
|
|
, mapAllocator(1048576)
|
|
{
|
|
root = codeAllocator.Create();
|
|
|
|
for (vint i = 0; i < 256; i++)
|
|
{
|
|
UpdateIndexBits();
|
|
CreateCode(root, (vuint8_t)i);
|
|
}
|
|
}
|
|
|
|
LzwBase::LzwBase(bool (&existingBytes)[256])
|
|
{
|
|
root = codeAllocator.Create();
|
|
for (vint i = 0; i < 256; i++)
|
|
{
|
|
if (existingBytes[i])
|
|
{
|
|
UpdateIndexBits();
|
|
CreateCode(root, (vuint8_t)i);
|
|
}
|
|
}
|
|
|
|
if (indexBits < 8)
|
|
{
|
|
eofIndex = nextIndex++;
|
|
}
|
|
}
|
|
|
|
LzwBase::~LzwBase()
|
|
{
|
|
}
|
|
|
|
/***********************************************************************
|
|
LzwEncoder
|
|
***********************************************************************/
|
|
|
|
void LzwEncoder::Flush()
|
|
{
|
|
vint written = 0;
|
|
vint bufferUsedSize = bufferUsedBits / 8;
|
|
if (bufferUsedBits % 8 != 0)
|
|
{
|
|
bufferUsedSize++;
|
|
}
|
|
while (written < bufferUsedSize)
|
|
{
|
|
vint size = stream->Write(buffer + written, bufferUsedSize - written);
|
|
CHECK_ERROR(size != 0, L"LzwEncoder::Flush()#Failed to flush the lzw buffer.");
|
|
written += size;
|
|
}
|
|
bufferUsedBits = 0;
|
|
}
|
|
|
|
vuint8_t highMarks[9] = { 0x00, 0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE, 0xFF };
|
|
vuint8_t lowMarks[9] = { 0x00, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF };
|
|
|
|
void LzwEncoder::WriteNumber(vint number, vint bitSize)
|
|
{
|
|
vint bitStart = 0;
|
|
vint bitStep = 8 - bufferUsedBits % 8;
|
|
if (bitStep > bitSize)
|
|
{
|
|
bitStep = bitSize;
|
|
}
|
|
while (bitStart < bitSize)
|
|
{
|
|
if(bufferUsedBits == BufferSize * 8)
|
|
{
|
|
Flush();
|
|
}
|
|
|
|
vint writeStart = bufferUsedBits % 8;
|
|
vint byteIndex = bufferUsedBits / 8;
|
|
vuint8_t byte = buffer[byteIndex];
|
|
byte &= highMarks[writeStart];
|
|
|
|
vuint8_t content = (vuint8_t)((number >> bitStart)&lowMarks[bitStep]) << (8 - writeStart - bitStep);
|
|
byte |= content;
|
|
|
|
buffer[byteIndex] = byte;
|
|
bufferUsedBits += bitStep;
|
|
|
|
bitStart += bitStep;
|
|
vint remain = bitSize - bitStart;
|
|
bitStep = remain < 8 ? remain : 8;
|
|
}
|
|
}
|
|
|
|
LzwEncoder::LzwEncoder()
|
|
{
|
|
prefix = root;
|
|
}
|
|
|
|
LzwEncoder::LzwEncoder(bool (&existingBytes)[256])
|
|
:LzwBase(existingBytes)
|
|
{
|
|
prefix = root;
|
|
}
|
|
|
|
LzwEncoder::~LzwEncoder()
|
|
{
|
|
}
|
|
|
|
void LzwEncoder::Close()
|
|
{
|
|
if (prefix != root)
|
|
{
|
|
WriteNumber(prefix->code, indexBits);
|
|
prefix = root;
|
|
}
|
|
|
|
vint remain = 8 - bufferUsedBits % 8;
|
|
if (remain != 8 && remain >= indexBits)
|
|
{
|
|
CHECK_ERROR(eofIndex != -1, L"LzwEncoder::Close()#Internal error.");
|
|
WriteNumber(eofIndex, indexBits);
|
|
}
|
|
Flush();
|
|
EncoderBase::Close();
|
|
}
|
|
|
|
vint LzwEncoder::Write(void* _buffer, vint _size)
|
|
{
|
|
vuint8_t* bytes = (vuint8_t*)_buffer;
|
|
for (vint i = 0; i < _size; i++)
|
|
{
|
|
vuint8_t byte = bytes[i];
|
|
Code* next = prefix->children.Get(byte);
|
|
if (next)
|
|
{
|
|
prefix = next;
|
|
}
|
|
else
|
|
{
|
|
WriteNumber(prefix->code, indexBits);
|
|
|
|
if (nextIndex < MaxDictionarySize)
|
|
{
|
|
UpdateIndexBits();
|
|
CreateCode(prefix, byte);
|
|
}
|
|
prefix = root->children.Get(byte);
|
|
}
|
|
}
|
|
return _size;
|
|
}
|
|
|
|
/***********************************************************************
|
|
LzwDecoder
|
|
***********************************************************************/
|
|
|
|
bool LzwDecoder::ReadNumber(vint& number, vint bitSize)
|
|
{
|
|
number = 0;
|
|
if (inputBufferSize == -1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
vint remainBits = inputBufferSize * 8 - inputBufferUsedBits;
|
|
vint writtenBits = 0;
|
|
vint bitStep = 8 - inputBufferUsedBits % 8;
|
|
if (bitStep > bitSize)
|
|
{
|
|
bitStep = bitSize;
|
|
}
|
|
while (writtenBits < bitSize)
|
|
{
|
|
if (remainBits == 0)
|
|
{
|
|
inputBufferSize = stream->Read(inputBuffer, BufferSize);
|
|
if (inputBufferSize == 0)
|
|
{
|
|
inputBufferSize = -1;
|
|
return false;
|
|
}
|
|
remainBits = inputBufferSize * 8;
|
|
inputBufferUsedBits = 0;
|
|
}
|
|
|
|
vuint8_t byte = inputBuffer[inputBufferUsedBits / 8];
|
|
byte >>= (8 - inputBufferUsedBits % 8 - bitStep);
|
|
byte &= lowMarks[bitStep];
|
|
number |= byte << writtenBits;
|
|
|
|
inputBufferUsedBits += bitStep;
|
|
remainBits -= bitStep;
|
|
writtenBits += bitStep;
|
|
vint remain = bitSize - writtenBits;
|
|
bitStep = remain < 8 ? remain : 8;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void LzwDecoder::PrepareOutputBuffer(vint size)
|
|
{
|
|
if (outputBuffer.Count() < size)
|
|
{
|
|
outputBuffer.Resize(size);
|
|
}
|
|
outputBufferSize = size;
|
|
}
|
|
|
|
void LzwDecoder::ExpandCodeToOutputBuffer(lzw::Code* code)
|
|
{
|
|
vuint8_t* outputByte = &outputBuffer[0] + code->size;
|
|
Code* current = code;
|
|
while (current != root)
|
|
{
|
|
*(--outputByte) = current->byte;
|
|
current = current->parent;
|
|
}
|
|
outputBufferUsedBytes = 0;
|
|
}
|
|
|
|
LzwDecoder::LzwDecoder()
|
|
{
|
|
for (vint i = 0; i < 256; i++)
|
|
{
|
|
dictionary.Add(root->children.Get((vuint8_t)i));
|
|
}
|
|
}
|
|
|
|
LzwDecoder::LzwDecoder(bool (&existingBytes)[256])
|
|
:LzwBase(existingBytes)
|
|
{
|
|
for (vint i = 0; i < 256; i++)
|
|
{
|
|
if (existingBytes[i])
|
|
{
|
|
dictionary.Add(root->children.Get((vuint8_t)i));
|
|
}
|
|
}
|
|
if (eofIndex != -1)
|
|
{
|
|
dictionary.Add(nullptr);
|
|
}
|
|
}
|
|
|
|
LzwDecoder::~LzwDecoder()
|
|
{
|
|
}
|
|
|
|
vint LzwDecoder::Read(void* _buffer, vint _size)
|
|
{
|
|
vint written = 0;
|
|
vuint8_t* bytes = (vuint8_t*)_buffer;
|
|
while (written < _size)
|
|
{
|
|
vint expect = _size - written;
|
|
vint remain = outputBufferSize - outputBufferUsedBytes;
|
|
if (remain == 0)
|
|
{
|
|
vint index = 0;
|
|
if (!ReadNumber(index, indexBits) || index == eofIndex)
|
|
{
|
|
break;
|
|
}
|
|
|
|
Code* prefix = 0;
|
|
if (index == dictionary.Count())
|
|
{
|
|
prefix = lastCode;
|
|
PrepareOutputBuffer(prefix->size + 1);
|
|
ExpandCodeToOutputBuffer(prefix);
|
|
outputBuffer[outputBufferSize - 1] = outputBuffer[0];
|
|
}
|
|
else
|
|
{
|
|
prefix = dictionary[index];
|
|
PrepareOutputBuffer(prefix->size);
|
|
ExpandCodeToOutputBuffer(prefix);
|
|
}
|
|
|
|
if (nextIndex < MaxDictionarySize)
|
|
{
|
|
if (lastCode)
|
|
{
|
|
dictionary.Add(CreateCode(lastCode, outputBuffer[0]));
|
|
}
|
|
UpdateIndexBits();
|
|
}
|
|
lastCode = dictionary[index];
|
|
}
|
|
else
|
|
{
|
|
if (remain > expect)
|
|
{
|
|
remain = expect;
|
|
}
|
|
memcpy(bytes + written, &outputBuffer[outputBufferUsedBytes], remain);
|
|
|
|
outputBufferUsedBytes += remain;
|
|
written += remain;
|
|
}
|
|
}
|
|
return written;
|
|
}
|
|
|
|
/***********************************************************************
|
|
Helper Functions
|
|
***********************************************************************/
|
|
|
|
vint CopyStream(stream::IStream& inputStream, stream::IStream& outputStream)
|
|
{
|
|
vint totalSize = 0;
|
|
while (true)
|
|
{
|
|
char buffer[1024];
|
|
vint copied = inputStream.Read(buffer, (vint)sizeof(buffer));
|
|
if (copied == 0)
|
|
{
|
|
break;
|
|
}
|
|
totalSize += outputStream.Write(buffer, copied);
|
|
}
|
|
return totalSize;
|
|
}
|
|
|
|
const vint CompressionFragmentSize = 1048576;
|
|
|
|
void CompressStream(stream::IStream& inputStream, stream::IStream& outputStream)
|
|
{
|
|
Array<char> buffer(CompressionFragmentSize);
|
|
while (true)
|
|
{
|
|
vint size = inputStream.Read(&buffer[0], buffer.Count());
|
|
if (size == 0) break;
|
|
|
|
MemoryStream compressedStream;
|
|
{
|
|
LzwEncoder encoder;
|
|
EncoderStream encoderStream(compressedStream, encoder);
|
|
encoderStream.Write(&buffer[0], size);
|
|
}
|
|
|
|
compressedStream.SeekFromBegin(0);
|
|
{
|
|
{
|
|
vint32_t bufferSize = (vint32_t)size;
|
|
outputStream.Write(&bufferSize, (vint)sizeof(bufferSize));
|
|
}
|
|
{
|
|
vint32_t compressedSize = (vint32_t)compressedStream.Size();
|
|
outputStream.Write(&compressedSize, (vint)sizeof(compressedSize));
|
|
}
|
|
CopyStream(compressedStream, outputStream);
|
|
}
|
|
}
|
|
}
|
|
|
|
void DecompressStream(stream::IStream& inputStream, stream::IStream& outputStream)
|
|
{
|
|
vint totalSize = 0;
|
|
vint totalWritten = 0;
|
|
while (true)
|
|
{
|
|
vint32_t bufferSize = 0;
|
|
if (inputStream.Read(&bufferSize, (vint)sizeof(bufferSize)) != sizeof(bufferSize))
|
|
{
|
|
break;
|
|
}
|
|
|
|
vint32_t compressedSize = 0;
|
|
CHECK_ERROR(inputStream.Read(&compressedSize, (vint)sizeof(compressedSize)) == sizeof(compressedSize), L"vl::stream::DecompressStream(MemoryStream&, MemoryStream&)#Incomplete input");
|
|
|
|
Array<char> buffer(compressedSize);
|
|
CHECK_ERROR(inputStream.Read(&buffer[0], compressedSize) == compressedSize, L"vl::stream::DecompressStream(MemoryStream&, MemoryStream&)#Incomplete input");
|
|
|
|
MemoryWrapperStream compressedStream(&buffer[0], compressedSize);
|
|
LzwDecoder decoder;
|
|
DecoderStream decoderStream(compressedStream, decoder);
|
|
totalWritten += CopyStream(decoderStream, outputStream);
|
|
totalSize += bufferSize;
|
|
}
|
|
CHECK_ERROR(outputStream.Size() == totalSize, L"vl::stream::DecompressStream(MemoryStream&, MemoryStream&)#Incomplete input");
|
|
}
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
.\ENCODING\CHARFORMAT\BOMENCODING.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
|
|
namespace vl
|
|
{
|
|
namespace stream
|
|
{
|
|
/***********************************************************************
|
|
BomEncoder
|
|
***********************************************************************/
|
|
|
|
BomEncoder::BomEncoder(Encoding _encoding)
|
|
:encoding(_encoding)
|
|
,encoder(0)
|
|
{
|
|
switch(encoding)
|
|
{
|
|
case Mbcs:
|
|
encoder=new MbcsEncoder;
|
|
break;
|
|
case Utf8:
|
|
encoder=new Utf8Encoder;
|
|
break;
|
|
case Utf16:
|
|
encoder=new Utf16Encoder;
|
|
break;
|
|
case Utf16BE:
|
|
encoder=new Utf16BEEncoder;
|
|
break;
|
|
}
|
|
}
|
|
|
|
BomEncoder::~BomEncoder()
|
|
{
|
|
Close();
|
|
}
|
|
|
|
void BomEncoder::Setup(IStream* _stream)
|
|
{
|
|
switch(encoding)
|
|
{
|
|
case Mbcs:
|
|
break;
|
|
case Utf8:
|
|
_stream->Write((void*)"\xEF\xBB\xBF", 3);
|
|
break;
|
|
case Utf16:
|
|
_stream->Write((void*)"\xFF\xFE", 2);
|
|
break;
|
|
case Utf16BE:
|
|
_stream->Write((void*)"\xFE\xFF", 2);
|
|
break;
|
|
}
|
|
encoder->Setup(_stream);
|
|
}
|
|
|
|
void BomEncoder::Close()
|
|
{
|
|
if(encoder)
|
|
{
|
|
encoder->Close();
|
|
delete encoder;
|
|
encoder=0;
|
|
}
|
|
}
|
|
|
|
vint BomEncoder::Write(void* _buffer, vint _size)
|
|
{
|
|
return encoder->Write(_buffer, _size);
|
|
}
|
|
|
|
/***********************************************************************
|
|
BomDecoder
|
|
***********************************************************************/
|
|
|
|
BomDecoder::BomStream::BomStream(IStream* _stream, char* _bom, vint _bomLength)
|
|
:stream(_stream)
|
|
,bomPosition(0)
|
|
,bomLength(_bomLength)
|
|
{
|
|
memcpy(bom, _bom, bomLength);
|
|
}
|
|
|
|
bool BomDecoder::BomStream::CanRead()const
|
|
{
|
|
return IsAvailable();
|
|
}
|
|
|
|
bool BomDecoder::BomStream::CanWrite()const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool BomDecoder::BomStream::CanSeek()const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool BomDecoder::BomStream::CanPeek()const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool BomDecoder::BomStream::IsLimited()const
|
|
{
|
|
return stream!=0 && stream->IsLimited();
|
|
}
|
|
|
|
bool BomDecoder::BomStream::IsAvailable()const
|
|
{
|
|
return stream!=0 && stream->IsAvailable();
|
|
}
|
|
|
|
void BomDecoder::BomStream::Close()
|
|
{
|
|
stream=0;
|
|
}
|
|
|
|
pos_t BomDecoder::BomStream::Position()const
|
|
{
|
|
return IsAvailable()?bomPosition+stream->Position():-1;
|
|
}
|
|
|
|
pos_t BomDecoder::BomStream::Size()const
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
void BomDecoder::BomStream::Seek(pos_t _size)
|
|
{
|
|
CHECK_FAIL(L"BomDecoder::BomStream::Seek(pos_t)#Operation not supported.");
|
|
}
|
|
|
|
void BomDecoder::BomStream::SeekFromBegin(pos_t _size)
|
|
{
|
|
CHECK_FAIL(L"BomDecoder::BomStream::SeekFromBegin(pos_t)#Operation not supported.");
|
|
}
|
|
|
|
void BomDecoder::BomStream::SeekFromEnd(pos_t _size)
|
|
{
|
|
CHECK_FAIL(L"BomDecoder::BomStream::SeekFromEnd(pos_t)#Operation not supported.");
|
|
}
|
|
|
|
vint BomDecoder::BomStream::Read(void* _buffer, vint _size)
|
|
{
|
|
vint result=0;
|
|
unsigned char* buffer=(unsigned char*)_buffer;
|
|
if(bomPosition<bomLength)
|
|
{
|
|
vint remain=bomLength-bomPosition;
|
|
result=remain<_size?remain:_size;
|
|
memcpy(buffer, bom+bomPosition, result);
|
|
buffer+=result;
|
|
bomPosition+=result;
|
|
_size-=result;
|
|
}
|
|
if(_size)
|
|
{
|
|
result+=stream->Read(buffer, _size);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
vint BomDecoder::BomStream::Write(void* _buffer, vint _size)
|
|
{
|
|
CHECK_FAIL(L"BomDecoder::BomStream::Write(void*, vint)#Operation not supported.");
|
|
}
|
|
|
|
vint BomDecoder::BomStream::Peek(void* _buffer, vint _size)
|
|
{
|
|
CHECK_FAIL(L"BomDecoder::BomStream::Peek(void*, vint)#Operation not supported.");
|
|
}
|
|
|
|
BomDecoder::BomDecoder()
|
|
:decoder(0)
|
|
{
|
|
}
|
|
|
|
BomDecoder::~BomDecoder()
|
|
{
|
|
Close();
|
|
}
|
|
|
|
void BomDecoder::Setup(IStream* _stream)
|
|
{
|
|
char bom[3]={0};
|
|
vint length=_stream->Read(bom, sizeof(bom));
|
|
if(strncmp(bom, "\xEF\xBB\xBF", 3)==0)
|
|
{
|
|
decoder=new Utf8Decoder;
|
|
stream=new BomStream(_stream, bom+3, 0);
|
|
}
|
|
else if(strncmp(bom, "\xFF\xFE", 2)==0)
|
|
{
|
|
decoder=new Utf16Decoder;
|
|
stream=new BomStream(_stream, bom+2, 1);
|
|
}
|
|
else if(strncmp(bom, "\xFE\xFF", 2)==0)
|
|
{
|
|
decoder=new Utf16BEDecoder;
|
|
stream=new BomStream(_stream, bom+2, 1);
|
|
}
|
|
else
|
|
{
|
|
decoder=new MbcsDecoder;
|
|
stream=new BomStream(_stream, bom, 3);
|
|
}
|
|
decoder->Setup(stream);
|
|
}
|
|
|
|
void BomDecoder::Close()
|
|
{
|
|
if(decoder)
|
|
{
|
|
decoder->Close();
|
|
delete decoder;
|
|
decoder=0;
|
|
stream->Close();
|
|
delete stream;
|
|
stream=0;
|
|
}
|
|
}
|
|
|
|
vint BomDecoder::Read(void* _buffer, vint _size)
|
|
{
|
|
return decoder->Read(_buffer, _size);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\ENCODING\CHARFORMAT\CHARFORMAT.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
|
|
namespace vl
|
|
{
|
|
namespace stream
|
|
{
|
|
/***********************************************************************
|
|
Helper Functions
|
|
***********************************************************************/
|
|
|
|
bool CanBeMbcs(unsigned char* buffer, vint size)
|
|
{
|
|
for(vint i=0;i<size;i++)
|
|
{
|
|
if(buffer[i]==0) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CanBeUtf8(unsigned char* buffer, vint size)
|
|
{
|
|
for(vint i=0;i<size;i++)
|
|
{
|
|
unsigned char c=(unsigned char)buffer[i];
|
|
if(c==0)
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
vint count10xxxxxx=0;
|
|
if((c&0x80)==0x00) /* 0x0xxxxxxx */ count10xxxxxx=0;
|
|
else if((c&0xE0)==0xC0) /* 0x110xxxxx */ count10xxxxxx=1;
|
|
else if((c&0xF0)==0xE0) /* 0x1110xxxx */ count10xxxxxx=2;
|
|
else if((c&0xF8)==0xF0) /* 0x11110xxx */ count10xxxxxx=3;
|
|
else if((c&0xFC)==0xF8) /* 0x111110xx */ count10xxxxxx=4;
|
|
else if((c&0xFE)==0xFC) /* 0x1111110x */ count10xxxxxx=5;
|
|
|
|
if(size<=i+count10xxxxxx)
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
for(vint j=0;j<count10xxxxxx;j++)
|
|
{
|
|
c=(unsigned char)buffer[i+j+1];
|
|
if((c&0xC0)!=0x80) /* 0x10xxxxxx */ return false;
|
|
}
|
|
}
|
|
i+=count10xxxxxx;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CanBeUtf16(unsigned char* buffer, vint size, bool& hitSurrogatePairs)
|
|
{
|
|
hitSurrogatePairs = false;
|
|
if (size % 2 != 0) return false;
|
|
bool needTrail = false;
|
|
for (vint i = 0; i < size; i += 2)
|
|
{
|
|
vuint16_t c = buffer[i] + (buffer[i + 1] << 8);
|
|
if (c == 0) return false;
|
|
vint type = 0;
|
|
if (0xD800 <= c && c <= 0xDBFF) type = 1;
|
|
else if (0xDC00 <= c && c <= 0xDFFF) type = 2;
|
|
if (needTrail)
|
|
{
|
|
if (type == 2)
|
|
{
|
|
needTrail = false;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (type == 1)
|
|
{
|
|
needTrail = true;
|
|
hitSurrogatePairs = true;
|
|
}
|
|
else if (type != 0)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return !needTrail;
|
|
}
|
|
|
|
bool CanBeUtf16BE(unsigned char* buffer, vint size, bool& hitSurrogatePairs)
|
|
{
|
|
hitSurrogatePairs = false;
|
|
if (size % 2 != 0) return false;
|
|
bool needTrail = false;
|
|
for (vint i = 0; i < size; i += 2)
|
|
{
|
|
vuint16_t c = buffer[i + 1] + (buffer[i] << 8);
|
|
if (c == 0) return false;
|
|
vint type = 0;
|
|
if (0xD800 <= c && c <= 0xDBFF) type = 1;
|
|
else if (0xDC00 <= c && c <= 0xDFFF) type = 2;
|
|
if (needTrail)
|
|
{
|
|
if (type == 2)
|
|
{
|
|
needTrail = false;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (type == 1)
|
|
{
|
|
needTrail = true;
|
|
hitSurrogatePairs = true;
|
|
}
|
|
else if (type != 0)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return !needTrail;
|
|
}
|
|
|
|
/***********************************************************************
|
|
TestEncoding
|
|
***********************************************************************/
|
|
|
|
extern void TestEncodingInternal(
|
|
unsigned char* buffer,
|
|
vint size,
|
|
BomEncoder::Encoding& encoding,
|
|
bool containsBom,
|
|
bool utf16HitSurrogatePairs,
|
|
bool utf16BEHitSurrogatePairs,
|
|
bool roughMbcs,
|
|
bool roughUtf8,
|
|
bool roughUtf16,
|
|
bool roughUtf16BE
|
|
);
|
|
|
|
void TestEncoding(unsigned char* buffer, vint size, BomEncoder::Encoding& encoding, bool& containsBom)
|
|
{
|
|
if (size >= 3 && strncmp((char*)buffer, "\xEF\xBB\xBF", 3) == 0)
|
|
{
|
|
encoding = BomEncoder::Utf8;
|
|
containsBom = true;
|
|
}
|
|
else if (size >= 2 && strncmp((char*)buffer, "\xFF\xFE", 2) == 0)
|
|
{
|
|
encoding = BomEncoder::Utf16;
|
|
containsBom = true;
|
|
}
|
|
else if (size >= 2 && strncmp((char*)buffer, "\xFE\xFF", 2) == 0)
|
|
{
|
|
encoding = BomEncoder::Utf16BE;
|
|
containsBom = true;
|
|
}
|
|
else
|
|
{
|
|
encoding = BomEncoder::Mbcs;
|
|
containsBom = false;
|
|
|
|
bool utf16HitSurrogatePairs = false;
|
|
bool utf16BEHitSurrogatePairs = false;
|
|
bool roughMbcs = CanBeMbcs(buffer, size);
|
|
bool roughUtf8 = CanBeUtf8(buffer, size);
|
|
bool roughUtf16 = CanBeUtf16(buffer, size, utf16HitSurrogatePairs);
|
|
bool roughUtf16BE = CanBeUtf16BE(buffer, size, utf16BEHitSurrogatePairs);
|
|
|
|
vint roughCount = (roughMbcs ? 1 : 0) + (roughUtf8 ? 1 : 0) + (roughUtf16 ? 1 : 0) + (roughUtf16BE ? 1 : 0);
|
|
if (roughCount == 1)
|
|
{
|
|
if (roughUtf8) encoding = BomEncoder::Utf8;
|
|
else if (roughUtf16) encoding = BomEncoder::Utf16;
|
|
else if (roughUtf16BE) encoding = BomEncoder::Utf16BE;
|
|
}
|
|
else if (roughCount > 1)
|
|
{
|
|
TestEncodingInternal(buffer, size, encoding, containsBom, utf16HitSurrogatePairs, utf16BEHitSurrogatePairs, roughMbcs, roughUtf8, roughUtf16, roughUtf16BE);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\ENCODING\CHARFORMAT\MBCSENCODING.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
|
|
namespace vl
|
|
{
|
|
namespace stream
|
|
{
|
|
/***********************************************************************
|
|
MbcsDecoder::ReadString
|
|
***********************************************************************/
|
|
|
|
extern bool IsMbcsLeadByte(char c);
|
|
extern void MbcsToWChar(wchar_t* wideBuffer, vint wideChars, vint wideReaded, char* mbcsBuffer, vint mbcsChars);
|
|
|
|
vint MbcsDecoder::ReadString(wchar_t* _buffer, vint chars)
|
|
{
|
|
char* source = new char[chars * 2];
|
|
char* reading = source;
|
|
vint readed = 0;
|
|
while (readed < chars)
|
|
{
|
|
if (stream->Read(reading, 1) != 1)
|
|
{
|
|
break;
|
|
}
|
|
if (IsMbcsLeadByte(*reading))
|
|
{
|
|
if (stream->Read(reading + 1, 1) != 1)
|
|
{
|
|
break;
|
|
}
|
|
reading += 2;
|
|
}
|
|
else
|
|
{
|
|
reading++;
|
|
}
|
|
readed++;
|
|
}
|
|
|
|
MbcsToWChar(_buffer, chars, readed, source, (vint)(reading - source));
|
|
delete[] source;
|
|
return readed;
|
|
}
|
|
|
|
/***********************************************************************
|
|
MbcsEncoder
|
|
***********************************************************************/
|
|
|
|
vint MbcsEncoder::Write(void* _buffer, vint _size)
|
|
{
|
|
// prepare a buffer for input
|
|
vint availableChars = (cacheSize + _size) / sizeof(wchar_t);
|
|
vint availableBytes = availableChars * sizeof(wchar_t);
|
|
bool needToFree = false;
|
|
vuint8_t* unicode = nullptr;
|
|
if (cacheSize > 0)
|
|
{
|
|
unicode = new vuint8_t[cacheSize + _size];
|
|
memcpy(unicode, cacheBuffer, cacheSize);
|
|
memcpy((vuint8_t*)unicode + cacheSize, _buffer, _size);
|
|
needToFree = true;
|
|
}
|
|
else
|
|
{
|
|
unicode = (vuint8_t*)_buffer;
|
|
}
|
|
|
|
#if defined VCZH_WCHAR_UTF16
|
|
if (availableChars > 0)
|
|
{
|
|
// a surrogate pair must be written as a whole thing
|
|
vuint16_t c = (vuint16_t)((wchar_t*)unicode)[availableChars - 1];
|
|
if ((c & 0xFC00U) == 0xD800U)
|
|
{
|
|
availableChars -= 1;
|
|
availableBytes -= sizeof(wchar_t);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// write the buffer
|
|
if (availableChars > 0)
|
|
{
|
|
vint written = WriteString((wchar_t*)unicode, availableChars) * sizeof(wchar_t);
|
|
CHECK_ERROR(written == availableBytes, L"MbcsEncoder::Write(void*, vint)#Failed to write a complete string.");
|
|
}
|
|
|
|
// cache the remaining
|
|
cacheSize = cacheSize + _size - availableBytes;
|
|
if (cacheSize > 0)
|
|
{
|
|
CHECK_ERROR(cacheSize <= sizeof(char32_t), L"MbcsEncoder::Write(void*, vint)#Unwritten text is too large to cache.");
|
|
memcpy(cacheBuffer, unicode + availableBytes, cacheSize);
|
|
}
|
|
|
|
if (needToFree) delete[] unicode;
|
|
return _size;
|
|
}
|
|
|
|
/***********************************************************************
|
|
MbcsDecoder::WriteString
|
|
***********************************************************************/
|
|
|
|
// implemented in platform dependent files
|
|
|
|
/***********************************************************************
|
|
MbcsDecoder
|
|
***********************************************************************/
|
|
|
|
vint MbcsDecoder::Read(void* _buffer, vint _size)
|
|
{
|
|
vuint8_t* writing = (vuint8_t*)_buffer;
|
|
vint filledBytes = 0;
|
|
|
|
// feed the cache first
|
|
if (cacheSize > 0)
|
|
{
|
|
filledBytes = cacheSize < _size ? cacheSize : _size;
|
|
memcpy(writing, cacheBuffer, cacheSize);
|
|
_size -= filledBytes;
|
|
writing += filledBytes;
|
|
|
|
// adjust the cache if it is not fully consumed
|
|
cacheSize -= filledBytes;
|
|
if (cacheSize > 0)
|
|
{
|
|
memcpy(cacheBuffer, cacheBuffer + filledBytes, cacheSize);
|
|
}
|
|
|
|
if (_size == 0)
|
|
{
|
|
return filledBytes;
|
|
}
|
|
}
|
|
|
|
// fill the buffer as many as possible
|
|
while (_size >= sizeof(wchar_t))
|
|
{
|
|
vint availableChars = _size / sizeof(wchar_t);
|
|
vint readBytes = ReadString((wchar_t*)writing, availableChars) * sizeof(wchar_t);
|
|
if (readBytes == 0) break;
|
|
filledBytes += readBytes;
|
|
_size -= readBytes;
|
|
writing += readBytes;
|
|
}
|
|
|
|
// cache the remaining wchar_t
|
|
if (_size < sizeof(wchar_t))
|
|
{
|
|
wchar_t c;
|
|
vint readChars = ReadString(&c, 1) * sizeof(wchar_t);
|
|
if (readChars == sizeof(wchar_t))
|
|
{
|
|
vuint8_t* reading = (vuint8_t*)&c;
|
|
memcpy(writing, reading, _size);
|
|
filledBytes += _size;
|
|
cacheSize = sizeof(wchar_t) - _size;
|
|
memcpy(cacheBuffer, reading + _size, cacheSize);
|
|
}
|
|
}
|
|
|
|
return filledBytes;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\ENCODING\CHARFORMAT\UTFENCODING.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
|
|
namespace vl
|
|
{
|
|
namespace stream
|
|
{
|
|
/***********************************************************************
|
|
UtfGeneralEncoder
|
|
***********************************************************************/
|
|
|
|
template<typename TNative, typename TExpect>
|
|
vint UtfGeneralEncoder<TNative, TExpect>::Write(void* _buffer, vint _size)
|
|
{
|
|
// prepare a buffer for input
|
|
vint availableChars = (cacheSize + _size) / sizeof(TExpect);
|
|
vint availableBytes = availableChars * sizeof(TExpect);
|
|
bool needToFree = false;
|
|
vuint8_t* unicode = nullptr;
|
|
if (cacheSize > 0)
|
|
{
|
|
unicode = new vuint8_t[cacheSize + _size];
|
|
memcpy(unicode, cacheBuffer, cacheSize);
|
|
memcpy((vuint8_t*)unicode + cacheSize, _buffer, _size);
|
|
needToFree = true;
|
|
}
|
|
else
|
|
{
|
|
unicode = (vuint8_t*)_buffer;
|
|
}
|
|
|
|
// write the buffer
|
|
if (availableChars > 0)
|
|
{
|
|
TStringRangeReader reader((TExpect*)unicode, availableChars);
|
|
while (TNative c = reader.Read())
|
|
{
|
|
vint written = stream->Write(&c, sizeof(c));
|
|
if (written != sizeof(c))
|
|
{
|
|
Close();
|
|
CHECK_FAIL(L"UtfGeneralEncoder<T>::Write(void*, vint)#Failed to write a complete string.");
|
|
}
|
|
}
|
|
auto cluster = reader.SourceCluster();
|
|
availableChars = cluster.index + cluster.size;
|
|
availableBytes = availableChars * sizeof(TExpect);
|
|
}
|
|
|
|
// cache the remaining
|
|
cacheSize = cacheSize + _size - availableBytes;
|
|
if (cacheSize > 0)
|
|
{
|
|
CHECK_ERROR(cacheSize <= sizeof(cacheBuffer), L"UtfGeneralEncoder<T>::Write(void*, vint)#Unwritten text is too large to cache.");
|
|
memcpy(cacheBuffer, unicode + availableBytes, cacheSize);
|
|
}
|
|
|
|
if (needToFree) delete[] unicode;
|
|
return _size;
|
|
}
|
|
|
|
/***********************************************************************
|
|
UtfGeneralDecoder
|
|
***********************************************************************/
|
|
|
|
template<typename TNative, typename TExpect>
|
|
void UtfGeneralDecoder<TNative, TExpect>::Setup(IStream* _stream)
|
|
{
|
|
DecoderBase::Setup(_stream);
|
|
reader.Setup(_stream);
|
|
}
|
|
|
|
template<typename TNative, typename TExpect>
|
|
vint UtfGeneralDecoder<TNative, TExpect>::Read(void* _buffer, vint _size)
|
|
{
|
|
vuint8_t* writing = (vuint8_t*)_buffer;
|
|
vint filledBytes = 0;
|
|
|
|
// feed the cache first
|
|
if (cacheSize > 0)
|
|
{
|
|
filledBytes = cacheSize < _size ? cacheSize : _size;
|
|
memcpy(writing, cacheBuffer, cacheSize);
|
|
_size -= filledBytes;
|
|
writing += filledBytes;
|
|
|
|
// adjust the cache if it is not fully consumed
|
|
cacheSize -= filledBytes;
|
|
if (cacheSize > 0)
|
|
{
|
|
memcpy(cacheBuffer, cacheBuffer + filledBytes, cacheSize);
|
|
}
|
|
|
|
if (_size == 0)
|
|
{
|
|
return filledBytes;
|
|
}
|
|
}
|
|
|
|
// fill the buffer as many as possible
|
|
while (_size >= sizeof(TExpect))
|
|
{
|
|
vint availableChars = _size / sizeof(TExpect);
|
|
vint readBytes = 0;
|
|
for (vint i = 0; i < availableChars; i++)
|
|
{
|
|
TExpect c = reader.Read();
|
|
if (!c) break;
|
|
*((TExpect*)writing) = c;
|
|
writing += sizeof(TExpect);
|
|
readBytes += sizeof(TExpect);
|
|
}
|
|
if (readBytes == 0) break;
|
|
filledBytes += readBytes;
|
|
_size -= readBytes;
|
|
}
|
|
|
|
// cache the remaining TExpect
|
|
if (_size < sizeof(TExpect))
|
|
{
|
|
if (TExpect c = reader.Read())
|
|
{
|
|
vuint8_t* reading = (vuint8_t*)&c;
|
|
memcpy(writing, reading, _size);
|
|
filledBytes += _size;
|
|
cacheSize = sizeof(TExpect) - _size;
|
|
memcpy(cacheBuffer, reading + _size, cacheSize);
|
|
}
|
|
}
|
|
|
|
return filledBytes;
|
|
}
|
|
|
|
/***********************************************************************
|
|
UtfGeneralEncoder<T, T>
|
|
***********************************************************************/
|
|
|
|
template<typename T>
|
|
vint UtfGeneralEncoder<T, T>::Write(void* _buffer, vint _size)
|
|
{
|
|
return stream->Write(_buffer, _size);
|
|
}
|
|
|
|
/***********************************************************************
|
|
UtfGeneralDecoder<T, T>
|
|
***********************************************************************/
|
|
|
|
template<typename T>
|
|
vint UtfGeneralDecoder<T, T>::Read(void* _buffer, vint _size)
|
|
{
|
|
return stream->Read(_buffer, _size);
|
|
}
|
|
|
|
/***********************************************************************
|
|
Unicode General (extern templates)
|
|
***********************************************************************/
|
|
|
|
template class UtfGeneralEncoder<wchar_t, wchar_t>;
|
|
template class UtfGeneralEncoder<wchar_t, char8_t>;
|
|
template class UtfGeneralEncoder<wchar_t, char16_t>;
|
|
template class UtfGeneralEncoder<wchar_t, char16be_t>;
|
|
template class UtfGeneralEncoder<wchar_t, char32_t>;
|
|
|
|
template class UtfGeneralEncoder<char8_t, wchar_t>;
|
|
template class UtfGeneralEncoder<char8_t, char8_t>;
|
|
template class UtfGeneralEncoder<char8_t, char16_t>;
|
|
template class UtfGeneralEncoder<char8_t, char16be_t>;
|
|
template class UtfGeneralEncoder<char8_t, char32_t>;
|
|
|
|
template class UtfGeneralEncoder<char16_t, wchar_t>;
|
|
template class UtfGeneralEncoder<char16_t, char8_t>;
|
|
template class UtfGeneralEncoder<char16_t, char16_t>;
|
|
template class UtfGeneralEncoder<char16_t, char16be_t>;
|
|
template class UtfGeneralEncoder<char16_t, char32_t>;
|
|
|
|
template class UtfGeneralEncoder<char16be_t, wchar_t>;
|
|
template class UtfGeneralEncoder<char16be_t, char8_t>;
|
|
template class UtfGeneralEncoder<char16be_t, char16_t>;
|
|
template class UtfGeneralEncoder<char16be_t, char16be_t>;
|
|
template class UtfGeneralEncoder<char16be_t, char32_t>;
|
|
|
|
template class UtfGeneralEncoder<char32_t, wchar_t>;
|
|
template class UtfGeneralEncoder<char32_t, char8_t>;
|
|
template class UtfGeneralEncoder<char32_t, char16_t>;
|
|
template class UtfGeneralEncoder<char32_t, char16be_t>;
|
|
template class UtfGeneralEncoder<char32_t, char32_t>;
|
|
|
|
template class UtfGeneralDecoder<wchar_t, wchar_t>;
|
|
template class UtfGeneralDecoder<wchar_t, char8_t>;
|
|
template class UtfGeneralDecoder<wchar_t, char16_t>;
|
|
template class UtfGeneralDecoder<wchar_t, char16be_t>;
|
|
template class UtfGeneralDecoder<wchar_t, char32_t>;
|
|
|
|
template class UtfGeneralDecoder<char8_t, wchar_t>;
|
|
template class UtfGeneralDecoder<char8_t, char8_t>;
|
|
template class UtfGeneralDecoder<char8_t, char16_t>;
|
|
template class UtfGeneralDecoder<char8_t, char16be_t>;
|
|
template class UtfGeneralDecoder<char8_t, char32_t>;
|
|
|
|
template class UtfGeneralDecoder<char16_t, wchar_t>;
|
|
template class UtfGeneralDecoder<char16_t, char8_t>;
|
|
template class UtfGeneralDecoder<char16_t, char16_t>;
|
|
template class UtfGeneralDecoder<char16_t, char16be_t>;
|
|
template class UtfGeneralDecoder<char16_t, char32_t>;
|
|
|
|
template class UtfGeneralDecoder<char16be_t, wchar_t>;
|
|
template class UtfGeneralDecoder<char16be_t, char8_t>;
|
|
template class UtfGeneralDecoder<char16be_t, char16_t>;
|
|
template class UtfGeneralDecoder<char16be_t, char16be_t>;
|
|
template class UtfGeneralDecoder<char16be_t, char32_t>;
|
|
|
|
template class UtfGeneralDecoder<char32_t, wchar_t>;
|
|
template class UtfGeneralDecoder<char32_t, char8_t>;
|
|
template class UtfGeneralDecoder<char32_t, char16_t>;
|
|
template class UtfGeneralDecoder<char32_t, char16be_t>;
|
|
template class UtfGeneralDecoder<char32_t, char32_t>;
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\INTERPROCESS\CHANNELIMPLS\CHANNELPACKAGE.CPP
|
|
***********************************************************************/
|
|
|
|
namespace vl::inter_process
|
|
{
|
|
NetworkPackage NetworkPackage::Create(Nullable<vint> _clientId, const WString& _channelName, const WString& _messageBody)
|
|
{
|
|
NetworkPackage package;
|
|
package.clientId = std::move(_clientId);
|
|
package.channelName = _channelName;
|
|
package.messageBody = _messageBody;
|
|
return package;
|
|
}
|
|
|
|
NetworkPackage NetworkPackage::Create(Nullable<vint> _clientId, const ClientIdList& _extraClientIds, const WString& _channelName, const WString& _messageBody)
|
|
{
|
|
auto package = Create(std::move(_clientId), _channelName, _messageBody);
|
|
if (_extraClientIds.Count() > 0)
|
|
{
|
|
ClientIdList extraClientIds;
|
|
for (auto clientId : _extraClientIds)
|
|
{
|
|
extraClientIds.Add(clientId);
|
|
}
|
|
package.extraClientIds = std::move(extraClientIds);
|
|
}
|
|
return package;
|
|
}
|
|
|
|
WString NetworkPackage::ToString(const NetworkPackage& package)
|
|
{
|
|
auto clientIds = package.clientId ? itow(package.clientId.Value()) : WString::Empty;
|
|
if (package.extraClientIds)
|
|
{
|
|
for (auto clientId : package.extraClientIds.Value())
|
|
{
|
|
clientIds += L",";
|
|
clientIds += itow(clientId);
|
|
}
|
|
}
|
|
|
|
return clientIds
|
|
+ L";" + package.channelName
|
|
+ L";" + package.messageBody
|
|
;
|
|
}
|
|
|
|
void NetworkPackage::Parse(const WString& str, NetworkPackage& package)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::inter_process::NetworkPackage::Parse(const WString&, NetworkPackage&)#"
|
|
const wchar_t* reading = str.Buffer();
|
|
|
|
const wchar_t* afterClientId = wcschr(reading, L';');
|
|
CHECK_ERROR(afterClientId != nullptr, ERROR_MESSAGE_PREFIX L"Invalid package format.");
|
|
package.clientId.Reset();
|
|
package.extraClientIds.Reset();
|
|
const wchar_t* firstExtraClientId = wcschr(reading, L',');
|
|
if (firstExtraClientId && firstExtraClientId > afterClientId)
|
|
{
|
|
firstExtraClientId = nullptr;
|
|
}
|
|
auto clientIdLength = firstExtraClientId ? (vint)(firstExtraClientId - reading) : (vint)(afterClientId - reading);
|
|
if (clientIdLength > 0)
|
|
{
|
|
package.clientId = wtoi(str.Left(clientIdLength));
|
|
}
|
|
|
|
if (firstExtraClientId)
|
|
{
|
|
ClientIdList extraClientIds;
|
|
auto readingExtraClientId = firstExtraClientId + 1;
|
|
while (readingExtraClientId < afterClientId)
|
|
{
|
|
auto delimiter = wcschr(readingExtraClientId, L',');
|
|
if (delimiter && delimiter > afterClientId)
|
|
{
|
|
delimiter = nullptr;
|
|
}
|
|
auto endingExtraClientId = delimiter ? delimiter : afterClientId;
|
|
CHECK_ERROR(endingExtraClientId > readingExtraClientId, ERROR_MESSAGE_PREFIX L"Invalid extra client id format.");
|
|
extraClientIds.Add(wtoi(WString::CopyFrom(readingExtraClientId, (vint)(endingExtraClientId - readingExtraClientId))));
|
|
readingExtraClientId = endingExtraClientId + (delimiter ? 1 : 0);
|
|
}
|
|
if (extraClientIds.Count() > 0)
|
|
{
|
|
package.extraClientIds = std::move(extraClientIds);
|
|
}
|
|
}
|
|
|
|
const wchar_t* afterChannelName = wcschr(afterClientId + 1, L';');
|
|
CHECK_ERROR(afterChannelName != nullptr, ERROR_MESSAGE_PREFIX L"Invalid package format.");
|
|
package.channelName = str.Sub((vint)(afterClientId - reading + 1), (vint)(afterChannelName - afterClientId - 1));
|
|
package.messageBody = str.Right(str.Length() - (vint)(afterChannelName - reading + 1));
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\STREAM\ACCESSOR.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
#include <string.h>
|
|
|
|
namespace vl
|
|
{
|
|
namespace stream
|
|
{
|
|
using namespace collections;
|
|
|
|
template<typename T>
|
|
struct VCRLF_ {};
|
|
|
|
template<> struct VCRLF_<wchar_t> { static constexpr const wchar_t* Value = L"\r\n"; };
|
|
template<> struct VCRLF_<char8_t> { static constexpr const char8_t* Value = u8"\r\n"; };
|
|
template<> struct VCRLF_<char16_t> { static constexpr const char16_t* Value = u"\r\n"; };
|
|
template<> struct VCRLF_<char32_t> { static constexpr const char32_t* Value = U"\r\n"; };
|
|
|
|
template<typename T>
|
|
constexpr const T* VCRLF = VCRLF_<T>::Value;
|
|
|
|
template<typename T>
|
|
struct VEMPTYSTR_ {};
|
|
|
|
template<> struct VEMPTYSTR_<wchar_t> { static constexpr const wchar_t* Value = L""; };
|
|
template<> struct VEMPTYSTR_<char8_t> { static constexpr const char8_t* Value = u8""; };
|
|
template<> struct VEMPTYSTR_<char16_t> { static constexpr const char16_t* Value = u""; };
|
|
template<> struct VEMPTYSTR_<char32_t> { static constexpr const char32_t* Value = U""; };
|
|
|
|
template<typename T>
|
|
constexpr const T* VEMPTYSTR = VEMPTYSTR_<T>::Value;
|
|
|
|
/***********************************************************************
|
|
TextReader_<T>
|
|
***********************************************************************/
|
|
|
|
template<typename T>
|
|
ObjectString<T> TextReader_<T>::ReadString(vint length)
|
|
{
|
|
T* buffer = new T[length + 1];
|
|
vint i = 0;
|
|
for (; i < length; i++)
|
|
{
|
|
if ((buffer[i] = ReadChar()) == 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
buffer[i] = 0;
|
|
ObjectString<T> result(buffer);
|
|
delete[] buffer;
|
|
return result;
|
|
}
|
|
|
|
template<typename T>
|
|
ObjectString<T> TextReader_<T>::ReadLine()
|
|
{
|
|
ObjectString<T> result;
|
|
auto buffer = new T[65537];
|
|
buffer[0] = 0;
|
|
vint i = 0;
|
|
while (true)
|
|
{
|
|
T c = ReadChar();
|
|
if (c == L'\n' || c == 0)
|
|
{
|
|
buffer[i] = 0;
|
|
result += buffer;
|
|
buffer[0] = 0;
|
|
i = 0;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
if (i == 65536)
|
|
{
|
|
buffer[i] = 0;
|
|
result += buffer;
|
|
buffer[0] = 0;
|
|
i = 0;
|
|
}
|
|
buffer[i++] = c;
|
|
}
|
|
}
|
|
result += buffer;
|
|
delete[] buffer;
|
|
if (result.Length() > 0 && result[result.Length() - 1] == L'\r')
|
|
{
|
|
return result.Left(result.Length() - 1);
|
|
}
|
|
else
|
|
{
|
|
return result;
|
|
}
|
|
}
|
|
|
|
template<typename T>
|
|
ObjectString<T> TextReader_<T>::ReadToEnd()
|
|
{
|
|
ObjectString<T> result;
|
|
auto buffer = new T[65537];
|
|
buffer[0] = 0;
|
|
vint i = 0;
|
|
while (true)
|
|
{
|
|
T c = ReadChar();
|
|
if (c == 0)
|
|
{
|
|
buffer[i] = 0;
|
|
result += buffer;
|
|
buffer[0] = 0;
|
|
i = 0;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
if (i == 65536)
|
|
{
|
|
buffer[i] = 0;
|
|
result += buffer;
|
|
buffer[0] = 0;
|
|
i = 0;
|
|
}
|
|
buffer[i++] = c;
|
|
}
|
|
}
|
|
result += buffer;
|
|
delete[] buffer;
|
|
return result;
|
|
}
|
|
|
|
/***********************************************************************
|
|
TextWriter_<T>
|
|
***********************************************************************/
|
|
|
|
template<typename T>
|
|
void TextWriter_<T>::WriteString(const T* string, vint charCount)
|
|
{
|
|
while (*string)
|
|
{
|
|
WriteChar(*string++);
|
|
}
|
|
}
|
|
|
|
template<typename T>
|
|
void TextWriter_<T>::WriteString(const T* string)
|
|
{
|
|
vint len = 0;
|
|
if constexpr (std::is_same_v<T, char>)
|
|
{
|
|
len = strlen(string);
|
|
}
|
|
else if constexpr (std::is_same_v<T, char8_t>)
|
|
{
|
|
len = strlen((const char*)string);
|
|
}
|
|
else if constexpr (std::is_same_v<T, wchar_t>)
|
|
{
|
|
len = wcslen(string);
|
|
}
|
|
#if defined VCZH_WCHAR_UTF16
|
|
else if constexpr (std::is_same_v<T, char16_t>)
|
|
{
|
|
len = wcslen((const wchar_t*)string);
|
|
}
|
|
#elif defined VCZH_WCHAR_UTF32
|
|
else if constexpr (std::is_same_v<T, char32_t>)
|
|
{
|
|
len = wcslen((const wchar_t*)string);
|
|
}
|
|
#endif
|
|
else
|
|
{
|
|
len = ObjectString<T>::Unmanaged(string).Length();
|
|
}
|
|
WriteString(string, len);
|
|
}
|
|
|
|
template<typename T>
|
|
void TextWriter_<T>::WriteString(const ObjectString<T>& string)
|
|
{
|
|
if (string.Length())
|
|
{
|
|
WriteString(string.Buffer(), string.Length());
|
|
}
|
|
}
|
|
|
|
template<typename T>
|
|
void TextWriter_<T>::WriteLine(const T* string, vint charCount)
|
|
{
|
|
WriteString(string, charCount);
|
|
WriteString(VCRLF<T>, 2);
|
|
}
|
|
|
|
template<typename T>
|
|
void TextWriter_<T>::WriteLine(const T* string)
|
|
{
|
|
WriteString(string);
|
|
WriteString(VCRLF<T>, 2);
|
|
}
|
|
|
|
template<typename T>
|
|
void TextWriter_<T>::WriteLine(const ObjectString<T>& string)
|
|
{
|
|
WriteString(string);
|
|
WriteString(VCRLF<T>, 2);
|
|
}
|
|
|
|
/***********************************************************************
|
|
StringReader_<T>
|
|
***********************************************************************/
|
|
|
|
template<typename T>
|
|
void StringReader_<T>::PrepareIfLastCallIsReadLine()
|
|
{
|
|
if (lastCallIsReadLine)
|
|
{
|
|
lastCallIsReadLine = false;
|
|
if (current < string.Length() && string[current] == L'\r') current++;
|
|
if (current < string.Length() && string[current] == L'\n') current++;
|
|
}
|
|
}
|
|
|
|
template<typename T>
|
|
StringReader_<T>::StringReader_(const ObjectString<T>& _string)
|
|
: string(_string)
|
|
, current(0)
|
|
, lastCallIsReadLine(false)
|
|
{
|
|
}
|
|
|
|
template<typename T>
|
|
bool StringReader_<T>::IsEnd()
|
|
{
|
|
return current == string.Length();
|
|
}
|
|
|
|
template<typename T>
|
|
T StringReader_<T>::ReadChar()
|
|
{
|
|
PrepareIfLastCallIsReadLine();
|
|
if (IsEnd())
|
|
{
|
|
return 0;
|
|
}
|
|
else
|
|
{
|
|
return string[current++];
|
|
}
|
|
}
|
|
|
|
template<typename T>
|
|
ObjectString<T> StringReader_<T>::ReadString(vint length)
|
|
{
|
|
PrepareIfLastCallIsReadLine();
|
|
if (IsEnd())
|
|
{
|
|
return VEMPTYSTR<T>;
|
|
}
|
|
else
|
|
{
|
|
vint remain = string.Length() - current;
|
|
if (length > remain) length = remain;
|
|
ObjectString<T> result = string.Sub(current, length);
|
|
current += length;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
template<typename T>
|
|
ObjectString<T> StringReader_<T>::ReadLine()
|
|
{
|
|
PrepareIfLastCallIsReadLine();
|
|
if (IsEnd())
|
|
{
|
|
return VEMPTYSTR<T>;
|
|
}
|
|
else
|
|
{
|
|
vint lineEnd = current;
|
|
while (lineEnd < string.Length())
|
|
{
|
|
T c = string[lineEnd];
|
|
if (c == L'\r' || c == L'\n') break;
|
|
lineEnd++;
|
|
}
|
|
ObjectString<T> result = string.Sub(current, lineEnd - current);
|
|
current = lineEnd;
|
|
lastCallIsReadLine = true;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
template<typename T>
|
|
ObjectString<T> StringReader_<T>::ReadToEnd()
|
|
{
|
|
return ReadString(string.Length() - current);
|
|
}
|
|
|
|
/***********************************************************************
|
|
StreamReader_<T>
|
|
***********************************************************************/
|
|
|
|
template<typename T>
|
|
StreamReader_<T>::StreamReader_(IStream& _stream)
|
|
: stream(&_stream)
|
|
{
|
|
}
|
|
|
|
template<typename T>
|
|
bool StreamReader_<T>::IsEnd()
|
|
{
|
|
return stream == nullptr;
|
|
}
|
|
|
|
template<typename T>
|
|
T StreamReader_<T>::ReadChar()
|
|
{
|
|
if (stream)
|
|
{
|
|
T buffer = 0;
|
|
if (stream->Read(&buffer, sizeof(buffer)) == 0)
|
|
{
|
|
stream = nullptr;
|
|
return 0;
|
|
}
|
|
else
|
|
{
|
|
return buffer;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
StreamWriter_<T>
|
|
***********************************************************************/
|
|
|
|
template<typename T>
|
|
StreamWriter_<T>::StreamWriter_(IStream& _stream)
|
|
:stream(&_stream)
|
|
{
|
|
}
|
|
|
|
template<typename T>
|
|
void StreamWriter_<T>::WriteChar(T c)
|
|
{
|
|
stream->Write(&c, sizeof(c));
|
|
}
|
|
|
|
template<typename T>
|
|
void StreamWriter_<T>::WriteString(const T* string, vint charCount)
|
|
{
|
|
stream->Write((void*)string, charCount * sizeof(*string));
|
|
}
|
|
|
|
/***********************************************************************
|
|
Extern Templates
|
|
***********************************************************************/
|
|
|
|
template class TextReader_<wchar_t>;
|
|
template class TextReader_<char8_t>;
|
|
template class TextReader_<char16_t>;
|
|
template class TextReader_<char32_t>;
|
|
|
|
template class TextWriter_<wchar_t>;
|
|
template class TextWriter_<char8_t>;
|
|
template class TextWriter_<char16_t>;
|
|
template class TextWriter_<char32_t>;
|
|
|
|
template class StringReader_<wchar_t>;
|
|
template class StringReader_<char8_t>;
|
|
template class StringReader_<char16_t>;
|
|
template class StringReader_<char32_t>;
|
|
|
|
template class StreamReader_<wchar_t>;
|
|
template class StreamReader_<char8_t>;
|
|
template class StreamReader_<char16_t>;
|
|
template class StreamReader_<char32_t>;
|
|
|
|
template class StreamWriter_<wchar_t>;
|
|
template class StreamWriter_<char8_t>;
|
|
template class StreamWriter_<char16_t>;
|
|
template class StreamWriter_<char32_t>;
|
|
|
|
/***********************************************************************
|
|
Extern Templates
|
|
***********************************************************************/
|
|
|
|
namespace monospace_tabling
|
|
{
|
|
void WriteBorderLine(TextWriter& writer, Array<vint>& columnWidths, vint columns)
|
|
{
|
|
writer.WriteChar(L'+');
|
|
for (vint i = 0; i < columns; i++)
|
|
{
|
|
vint c = columnWidths[i];
|
|
for (vint j = 0; j < c; j++)
|
|
{
|
|
writer.WriteChar(L'-');
|
|
}
|
|
writer.WriteChar(L'+');
|
|
}
|
|
writer.WriteLine(L"");
|
|
}
|
|
|
|
void WriteContentLine(TextWriter& writer, Array<vint>& columnWidths, vint rowHeight, vint columns, Array<WString>& tableByRow, vint startRow)
|
|
{
|
|
vint cellStart = startRow * columns;
|
|
for (vint r = 0; r < rowHeight; r++)
|
|
{
|
|
writer.WriteChar(L'|');
|
|
for (vint c = 0; c < columns; c++)
|
|
{
|
|
const wchar_t* cell = tableByRow[cellStart + c].Buffer();
|
|
for (vint i = 0; i < r; i++)
|
|
{
|
|
if (cell) cell = ::wcsstr(cell, L"\r\n");
|
|
if (cell) cell += 2;
|
|
}
|
|
|
|
writer.WriteChar(L' ');
|
|
vint length = 0;
|
|
if (cell)
|
|
{
|
|
const wchar_t* end = ::wcsstr(cell, L"\r\n");
|
|
length = end ? end - cell : (vint)wcslen(cell);
|
|
writer.WriteString(cell, length);
|
|
}
|
|
|
|
for (vint i = columnWidths[c] - 2; i >= length; i--)
|
|
{
|
|
writer.WriteChar(L' ');
|
|
}
|
|
writer.WriteChar(L'|');
|
|
}
|
|
writer.WriteLine(L"");
|
|
}
|
|
}
|
|
}
|
|
|
|
void WriteMonospacedEnglishTable(TextWriter& writer, collections::Array<WString>& tableByRow, vint rows, vint columns)
|
|
{
|
|
Array<vint> rowHeights(rows);
|
|
Array<vint> columnWidths(columns);
|
|
for (vint i = 0; i < rows; i++) rowHeights[i] = 0;
|
|
for (vint j = 0; j < columns; j++) columnWidths[j] = 0;
|
|
|
|
for (vint i = 0; i < rows; i++)
|
|
{
|
|
for (vint j = 0; j < columns; j++)
|
|
{
|
|
WString text = tableByRow[i * columns + j];
|
|
const wchar_t* reading = text.Buffer();
|
|
vint width = 0;
|
|
vint height = 0;
|
|
|
|
while (reading)
|
|
{
|
|
height++;
|
|
const wchar_t* crlf = ::wcsstr(reading, L"\r\n");
|
|
if (crlf)
|
|
{
|
|
vint length = crlf - reading + 2;
|
|
if (width < length) width = length;
|
|
reading = crlf + 2;
|
|
}
|
|
else
|
|
{
|
|
vint length = ::wcslen(reading) + 2;
|
|
if (width < length) width = length;
|
|
reading = 0;
|
|
}
|
|
}
|
|
|
|
if (rowHeights[i] < height) rowHeights[i] = height;
|
|
if (columnWidths[j] < width) columnWidths[j] = width;
|
|
}
|
|
}
|
|
|
|
monospace_tabling::WriteBorderLine(writer, columnWidths, columns);
|
|
for (vint i = 0; i < rows; i++)
|
|
{
|
|
monospace_tabling::WriteContentLine(writer, columnWidths, rowHeights[i], columns, tableByRow, i);
|
|
monospace_tabling::WriteBorderLine(writer, columnWidths, columns);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\STREAM\BROADCASTSTREAM.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
|
|
namespace vl
|
|
{
|
|
namespace stream
|
|
{
|
|
/***********************************************************************
|
|
BroadcastStream
|
|
***********************************************************************/
|
|
|
|
BroadcastStream::BroadcastStream()
|
|
:closed(false)
|
|
,position(0)
|
|
{
|
|
}
|
|
|
|
BroadcastStream::~BroadcastStream()
|
|
{
|
|
}
|
|
|
|
BroadcastStream::StreamList& BroadcastStream::Targets()
|
|
{
|
|
return streams;
|
|
}
|
|
|
|
bool BroadcastStream::CanRead()const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool BroadcastStream::CanWrite()const
|
|
{
|
|
return !closed;
|
|
}
|
|
|
|
bool BroadcastStream::CanSeek()const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool BroadcastStream::CanPeek()const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool BroadcastStream::IsLimited()const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool BroadcastStream::IsAvailable()const
|
|
{
|
|
return !closed;
|
|
}
|
|
|
|
void BroadcastStream::Close()
|
|
{
|
|
closed=true;
|
|
position=-1;
|
|
}
|
|
|
|
pos_t BroadcastStream::Position()const
|
|
{
|
|
return position;
|
|
}
|
|
|
|
pos_t BroadcastStream::Size()const
|
|
{
|
|
return position;
|
|
}
|
|
|
|
void BroadcastStream::Seek(pos_t _size)
|
|
{
|
|
CHECK_FAIL(L"BroadcastStream::Seek(pos_t)#Operation not supported.");
|
|
}
|
|
|
|
void BroadcastStream::SeekFromBegin(pos_t _size)
|
|
{
|
|
CHECK_FAIL(L"BroadcastStream::SeekFromBegin(pos_t)#Operation not supported.");
|
|
}
|
|
|
|
void BroadcastStream::SeekFromEnd(pos_t _size)
|
|
{
|
|
CHECK_FAIL(L"BroadcastStream::SeekFromEnd(pos_t)#Operation not supported.");
|
|
}
|
|
|
|
vint BroadcastStream::Read(void* _buffer, vint _size)
|
|
{
|
|
CHECK_FAIL(L"BroadcastStream::Read(void*, vint)#Operation not supported.");
|
|
}
|
|
|
|
vint BroadcastStream::Write(void* _buffer, vint _size)
|
|
{
|
|
// TODO: (enumerable) foreach
|
|
for(vint i=0;i<streams.Count();i++)
|
|
{
|
|
vint written = streams[i]->Write(_buffer, _size);
|
|
CHECK_ERROR(written == _size, L"BroadcastStream::Write(void*, vint)#Failed to copy data to the output stream.");
|
|
}
|
|
position+=_size;
|
|
return _size;
|
|
}
|
|
|
|
vint BroadcastStream::Peek(void* _buffer, vint _size)
|
|
{
|
|
CHECK_FAIL(L"BroadcastStream::Peek(void*, vint)#Operation not supported.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
.\STREAM\CACHESTREAM.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
|
|
namespace vl
|
|
{
|
|
namespace stream
|
|
{
|
|
/***********************************************************************
|
|
CacheStream
|
|
***********************************************************************/
|
|
|
|
void CacheStream::Flush()
|
|
{
|
|
if(dirtyLength>0)
|
|
{
|
|
if(target->Position()!=start+dirtyStart)
|
|
{
|
|
target->SeekFromBegin(start+dirtyStart);
|
|
}
|
|
target->Write(buffer+dirtyStart, dirtyLength);
|
|
}
|
|
dirtyStart=0;
|
|
dirtyLength=0;
|
|
availableLength=0;
|
|
}
|
|
|
|
void CacheStream::Load(pos_t _position)
|
|
{
|
|
if(target->Position()!=_position)
|
|
{
|
|
target->SeekFromBegin(_position);
|
|
}
|
|
start=_position;
|
|
if(target->CanRead())
|
|
{
|
|
availableLength=target->Read(buffer, block);
|
|
}
|
|
}
|
|
|
|
vint CacheStream::InternalRead(void* _buffer, vint _size)
|
|
{
|
|
vint readed=0;
|
|
if(position>=start && position<start+availableLength)
|
|
{
|
|
vint bufferMax=(vint)(start+availableLength-position);
|
|
vint min=bufferMax<_size?bufferMax:_size;
|
|
memcpy(_buffer, buffer+(position-start), min);
|
|
readed+=min;
|
|
_buffer=(char*)_buffer+min;
|
|
}
|
|
|
|
if(_size>readed)
|
|
{
|
|
Flush();
|
|
if(_size-readed>=block)
|
|
{
|
|
if(CanSeek())
|
|
{
|
|
target->SeekFromBegin(position+readed);
|
|
}
|
|
vint additional=target->Read(_buffer, _size-readed);
|
|
if(additional!=-1)
|
|
{
|
|
readed+=additional;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Load(position+readed);
|
|
vint remain=_size-readed;
|
|
vint min=availableLength<remain?availableLength:remain;
|
|
memcpy(_buffer, buffer, min);
|
|
readed+=min;
|
|
}
|
|
}
|
|
return readed;
|
|
}
|
|
|
|
vint CacheStream::InternalWrite(void* _buffer, vint _size)
|
|
{
|
|
vint written=0;
|
|
if(position>=start && position<start+block)
|
|
{
|
|
vint bufferMax=(vint)(start+block-position);
|
|
vint writeLength=bufferMax<_size?bufferMax:_size;
|
|
vint writeStart=(vint)(position-start);
|
|
|
|
memcpy(buffer+writeStart, _buffer, writeLength);
|
|
written+=writeLength;
|
|
_buffer=(char*)_buffer+writeLength;
|
|
|
|
if(dirtyLength==0)
|
|
{
|
|
dirtyStart=writeStart;
|
|
dirtyLength=writeLength;
|
|
}
|
|
else
|
|
{
|
|
dirtyLength=writeStart+writeLength-dirtyStart;
|
|
}
|
|
|
|
vint availableOffset=writeStart+writeLength-availableLength;
|
|
if(availableOffset>0)
|
|
{
|
|
availableLength+=availableOffset;
|
|
}
|
|
}
|
|
|
|
if(_size>written)
|
|
{
|
|
Flush();
|
|
if(_size-written>=block)
|
|
{
|
|
if(CanSeek())
|
|
{
|
|
target->SeekFromBegin(position+written);
|
|
}
|
|
vint additional=target->Write(_buffer, _size-written);
|
|
if(additional!=-1)
|
|
{
|
|
written+=additional;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Load(position+written);
|
|
dirtyLength=_size-written;
|
|
memcpy(buffer, _buffer, dirtyLength);
|
|
written+=dirtyLength;
|
|
}
|
|
}
|
|
return written;
|
|
}
|
|
|
|
CacheStream::CacheStream(IStream& _target, vint _block)
|
|
:target(&_target)
|
|
,block(_block)
|
|
,start(0)
|
|
,position(0)
|
|
,dirtyStart(0)
|
|
,dirtyLength(0)
|
|
,availableLength(0)
|
|
,operatedSize(0)
|
|
{
|
|
if(block<=0)
|
|
{
|
|
block=65536;
|
|
}
|
|
buffer=new char[block];
|
|
}
|
|
|
|
CacheStream::~CacheStream()
|
|
{
|
|
Close();
|
|
}
|
|
|
|
bool CacheStream::CanRead()const
|
|
{
|
|
return target!=0 && target->CanRead();
|
|
}
|
|
|
|
bool CacheStream::CanWrite()const
|
|
{
|
|
return target!=0 && target->CanWrite();
|
|
}
|
|
|
|
bool CacheStream::CanSeek()const
|
|
{
|
|
return target!=0 && target->CanSeek();
|
|
}
|
|
|
|
bool CacheStream::CanPeek()const
|
|
{
|
|
return target!=0 && target->CanPeek();
|
|
}
|
|
|
|
bool CacheStream::IsLimited()const
|
|
{
|
|
return target!=0 && target->IsLimited();
|
|
}
|
|
|
|
bool CacheStream::IsAvailable()const
|
|
{
|
|
return target!=0 && target->IsAvailable();
|
|
}
|
|
|
|
void CacheStream::Close()
|
|
{
|
|
Flush();
|
|
target=0;
|
|
delete[] buffer;
|
|
buffer=0;
|
|
position=-1;
|
|
dirtyStart=0;
|
|
dirtyLength=0;
|
|
availableLength=0;
|
|
operatedSize=-1;
|
|
}
|
|
|
|
pos_t CacheStream::Position()const
|
|
{
|
|
return position;
|
|
}
|
|
|
|
pos_t CacheStream::Size()const
|
|
{
|
|
if(target!=0)
|
|
{
|
|
if(IsLimited())
|
|
{
|
|
return target->Size();
|
|
}
|
|
else
|
|
{
|
|
return operatedSize;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
void CacheStream::Seek(pos_t _size)
|
|
{
|
|
SeekFromBegin(position+_size);
|
|
}
|
|
|
|
void CacheStream::SeekFromBegin(pos_t _size)
|
|
{
|
|
if(CanSeek())
|
|
{
|
|
if(_size<0)
|
|
{
|
|
position=0;
|
|
}
|
|
else if(_size>Size())
|
|
{
|
|
position=Size();
|
|
}
|
|
else
|
|
{
|
|
position=_size;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CacheStream::SeekFromEnd(pos_t _size)
|
|
{
|
|
SeekFromBegin(Size()-_size);
|
|
}
|
|
|
|
vint CacheStream::Read(void* _buffer, vint _size)
|
|
{
|
|
CHECK_ERROR(CanRead(), L"CacheStream::Read(void*, vint)#Stream is closed or operation not supported.");
|
|
CHECK_ERROR(_size>=0, L"CacheStream::Read(void*, vint)#Argument size cannot be negative.");
|
|
|
|
_size=InternalRead(_buffer, _size);
|
|
position+=_size;
|
|
if(operatedSize<position)
|
|
{
|
|
operatedSize=position;
|
|
}
|
|
return _size;
|
|
}
|
|
|
|
vint CacheStream::Write(void* _buffer, vint _size)
|
|
{
|
|
CHECK_ERROR(CanWrite(), L"CacheStream::Write(void*, vint)#Stream is closed or operation not supported.");
|
|
CHECK_ERROR(_size>=0, L"CacheStream::Read(void*, vint)#Argument size cannot be negative.");
|
|
|
|
if(IsLimited())
|
|
{
|
|
pos_t size=Size();
|
|
if(size!=-1)
|
|
{
|
|
vint remain=(vint)(size-(position+_size));
|
|
if(remain<0)
|
|
{
|
|
_size-=remain;
|
|
}
|
|
}
|
|
}
|
|
|
|
_size=InternalWrite(_buffer, _size);
|
|
position+=_size;
|
|
if(operatedSize<position)
|
|
{
|
|
operatedSize=position;
|
|
}
|
|
return _size;
|
|
}
|
|
|
|
vint CacheStream::Peek(void* _buffer, vint _size)
|
|
{
|
|
CHECK_ERROR(CanPeek(), L"CacheStream::Peek(void*, vint)#Stream is closed or operation not supported.");
|
|
CHECK_ERROR(_size>=0, L"CacheStream::Read(void*, vint)#Argument size cannot be negative.");
|
|
|
|
return InternalRead(_buffer, _size);
|
|
}
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
.\STREAM\ENCODINGSTREAM.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
|
|
namespace vl
|
|
{
|
|
namespace stream
|
|
{
|
|
/***********************************************************************
|
|
EncoderStream
|
|
***********************************************************************/
|
|
|
|
EncoderStream::EncoderStream(IStream& _stream, IEncoder& _encoder)
|
|
:stream(&_stream)
|
|
,encoder(&_encoder)
|
|
,position(0)
|
|
{
|
|
encoder->Setup(stream);
|
|
}
|
|
|
|
EncoderStream::~EncoderStream()
|
|
{
|
|
Close();
|
|
}
|
|
|
|
bool EncoderStream::CanRead()const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool EncoderStream::CanWrite()const
|
|
{
|
|
return IsAvailable();
|
|
}
|
|
|
|
bool EncoderStream::CanSeek()const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool EncoderStream::CanPeek()const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool EncoderStream::IsLimited()const
|
|
{
|
|
return stream!=0 && stream->IsLimited();
|
|
}
|
|
|
|
bool EncoderStream::IsAvailable()const
|
|
{
|
|
return stream!=0 && stream->IsAvailable();
|
|
}
|
|
|
|
void EncoderStream::Close()
|
|
{
|
|
encoder->Close();
|
|
stream=0;
|
|
}
|
|
|
|
pos_t EncoderStream::Position()const
|
|
{
|
|
return IsAvailable()?position:-1;
|
|
}
|
|
|
|
pos_t EncoderStream::Size()const
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
void EncoderStream::Seek(pos_t _size)
|
|
{
|
|
CHECK_FAIL(L"EncoderStream::Seek(pos_t)#Operation not supported.");
|
|
}
|
|
|
|
void EncoderStream::SeekFromBegin(pos_t _size)
|
|
{
|
|
CHECK_FAIL(L"EncoderStream::SeekFromBegin(pos_t)#Operation not supported.");
|
|
}
|
|
|
|
void EncoderStream::SeekFromEnd(pos_t _size)
|
|
{
|
|
CHECK_FAIL(L"EncoderStream::SeekFromEnd(pos_t)#Operation not supported.");
|
|
}
|
|
|
|
vint EncoderStream::Read(void* _buffer, vint _size)
|
|
{
|
|
CHECK_FAIL(L"EncoderStream::Read(void*, vint)#Operation not supported.");
|
|
}
|
|
|
|
vint EncoderStream::Write(void* _buffer, vint _size)
|
|
{
|
|
vint result=encoder->Write(_buffer, _size);
|
|
if(result>=0)
|
|
{
|
|
position+=result;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
vint EncoderStream::Peek(void* _buffer, vint _size)
|
|
{
|
|
CHECK_FAIL(L"EncoderStream::Peek(void*, vint)#Operation not supported.");
|
|
}
|
|
|
|
/***********************************************************************
|
|
DecoderStream
|
|
***********************************************************************/
|
|
|
|
DecoderStream::DecoderStream(IStream& _stream, IDecoder& _decoder)
|
|
:stream(&_stream)
|
|
,decoder(&_decoder)
|
|
,position(0)
|
|
{
|
|
decoder->Setup(stream);
|
|
}
|
|
|
|
DecoderStream::~DecoderStream()
|
|
{
|
|
Close();
|
|
}
|
|
|
|
bool DecoderStream::CanRead()const
|
|
{
|
|
return IsAvailable();
|
|
}
|
|
|
|
bool DecoderStream::CanWrite()const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool DecoderStream::CanSeek()const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool DecoderStream::CanPeek()const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool DecoderStream::IsLimited()const
|
|
{
|
|
return stream!=0 && stream->IsLimited();
|
|
}
|
|
|
|
bool DecoderStream::IsAvailable()const
|
|
{
|
|
return stream!=0 && stream->IsAvailable();
|
|
}
|
|
|
|
void DecoderStream::Close()
|
|
{
|
|
decoder->Close();
|
|
stream=0;
|
|
}
|
|
|
|
pos_t DecoderStream::Position()const
|
|
{
|
|
return IsAvailable()?position:-1;
|
|
}
|
|
|
|
pos_t DecoderStream::Size()const
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
void DecoderStream::Seek(pos_t _size)
|
|
{
|
|
CHECK_FAIL(L"DecoderStream::Seek(pos_t)#Operation not supported.");
|
|
}
|
|
|
|
void DecoderStream::SeekFromBegin(pos_t _size)
|
|
{
|
|
CHECK_FAIL(L"DecoderStream::SeekFromBegin(pos_t)#Operation not supported.");
|
|
}
|
|
|
|
void DecoderStream::SeekFromEnd(pos_t _size)
|
|
{
|
|
CHECK_FAIL(L"DecoderStream::SeekFromEnd(pos_t)#Operation not supported.");
|
|
}
|
|
|
|
vint DecoderStream::Read(void* _buffer, vint _size)
|
|
{
|
|
vint result=decoder->Read(_buffer, _size);
|
|
if(result>=0)
|
|
{
|
|
position+=result;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
vint DecoderStream::Write(void* _buffer, vint _size)
|
|
{
|
|
CHECK_FAIL(L"DecoderStream::Write(void*, vint)#Operation not supported.");
|
|
}
|
|
|
|
vint DecoderStream::Peek(void* _buffer, vint _size)
|
|
{
|
|
CHECK_FAIL(L"DecoderStream::Peek(void*, vint)#Operation not supported.");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\STREAM\FILESTREAM.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
#if defined VCZH_GCC
|
|
#endif
|
|
|
|
namespace vl
|
|
{
|
|
namespace filesystem
|
|
{
|
|
extern IFileSystemImpl* GetFileSystemImpl();
|
|
}
|
|
|
|
namespace stream
|
|
{
|
|
#if defined VCZH_GCC
|
|
void _fseeki64(FILE* file, pos_t offset, int origin)
|
|
{
|
|
fseek(file, (long)offset, origin);
|
|
}
|
|
#endif
|
|
|
|
/***********************************************************************
|
|
OSFileStreamImpl
|
|
***********************************************************************/
|
|
|
|
class OSFileStreamImpl : public Object, public virtual IFileStreamImpl
|
|
{
|
|
private:
|
|
WString fileName;
|
|
FileStream::AccessRight accessRight;
|
|
FILE* file;
|
|
|
|
public:
|
|
OSFileStreamImpl(const WString& _fileName, FileStream::AccessRight _accessRight)
|
|
: fileName(_fileName), accessRight(_accessRight), file(nullptr)
|
|
{
|
|
}
|
|
|
|
~OSFileStreamImpl()
|
|
{
|
|
Close();
|
|
}
|
|
|
|
bool Open() override
|
|
{
|
|
const wchar_t* mode = L"rb";
|
|
switch(accessRight)
|
|
{
|
|
case FileStream::ReadOnly:
|
|
mode = L"rb";
|
|
break;
|
|
case FileStream::WriteOnly:
|
|
mode = L"wb";
|
|
break;
|
|
case FileStream::ReadWrite:
|
|
mode = L"w+b";
|
|
break;
|
|
}
|
|
|
|
#if defined VCZH_MSVC
|
|
if(_wfopen_s(&file, fileName.Buffer(), mode) != 0)
|
|
{
|
|
file = nullptr;
|
|
return false;
|
|
}
|
|
#elif defined VCZH_GCC
|
|
AString fileNameA = wtoa(fileName);
|
|
AString modeA = wtoa(mode);
|
|
file = fopen(fileNameA.Buffer(), modeA.Buffer());
|
|
if(file == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
void Close() override
|
|
{
|
|
if(file != nullptr)
|
|
{
|
|
fclose(file);
|
|
file = nullptr;
|
|
}
|
|
}
|
|
|
|
pos_t Position() const override
|
|
{
|
|
if(file != nullptr)
|
|
{
|
|
#if defined VCZH_MSVC
|
|
fpos_t position = 0;
|
|
if(fgetpos(file, &position) == 0)
|
|
{
|
|
return position;
|
|
}
|
|
#elif defined VCZH_GCC
|
|
return (pos_t)ftell(file);
|
|
#endif
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
pos_t Size() const override
|
|
{
|
|
if(file != nullptr)
|
|
{
|
|
#if defined VCZH_MSVC
|
|
fpos_t position = 0;
|
|
if(fgetpos(file, &position) == 0)
|
|
{
|
|
if(fseek(file, 0, SEEK_END) == 0)
|
|
{
|
|
pos_t size = Position();
|
|
if(fsetpos(file, &position) == 0)
|
|
{
|
|
return size;
|
|
}
|
|
}
|
|
}
|
|
#elif defined VCZH_GCC
|
|
long position = ftell(file);
|
|
fseek(file, 0, SEEK_END);
|
|
long size = ftell(file);
|
|
fseek(file, position, SEEK_SET);
|
|
return (pos_t)size;
|
|
#endif
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void Seek(pos_t _size) override
|
|
{
|
|
if(Position() + _size > Size())
|
|
{
|
|
_fseeki64(file, 0, SEEK_END);
|
|
}
|
|
else if(Position() + _size < 0)
|
|
{
|
|
_fseeki64(file, 0, SEEK_SET);
|
|
}
|
|
else
|
|
{
|
|
_fseeki64(file, _size, SEEK_CUR);
|
|
}
|
|
}
|
|
|
|
void SeekFromBegin(pos_t _size) override
|
|
{
|
|
if(_size > Size())
|
|
{
|
|
_fseeki64(file, 0, SEEK_END);
|
|
}
|
|
else if(_size < 0)
|
|
{
|
|
_fseeki64(file, 0, SEEK_SET);
|
|
}
|
|
else
|
|
{
|
|
_fseeki64(file, _size, SEEK_SET);
|
|
}
|
|
}
|
|
|
|
void SeekFromEnd(pos_t _size) override
|
|
{
|
|
if(_size < 0)
|
|
{
|
|
_fseeki64(file, 0, SEEK_END);
|
|
}
|
|
else if(_size > Size())
|
|
{
|
|
_fseeki64(file, 0, SEEK_SET);
|
|
}
|
|
else
|
|
{
|
|
_fseeki64(file, -_size, SEEK_END);
|
|
}
|
|
}
|
|
|
|
vint Read(void* _buffer, vint _size) override
|
|
{
|
|
CHECK_ERROR(file != nullptr, L"FileStream::Read(pos_t)#Stream is closed, cannot perform this operation.");
|
|
CHECK_ERROR(_size >= 0, L"FileStream::Read(void*, vint)#Argument size cannot be negative.");
|
|
return fread(_buffer, 1, _size, file);
|
|
}
|
|
|
|
vint Write(void* _buffer, vint _size) override
|
|
{
|
|
CHECK_ERROR(file != nullptr, L"FileStream::Write(pos_t)#Stream is closed, cannot perform this operation.");
|
|
CHECK_ERROR(_size >= 0, L"FileStream::Write(void*, vint)#Argument size cannot be negative.");
|
|
return fwrite(_buffer, 1, _size, file);
|
|
}
|
|
|
|
vint Peek(void* _buffer, vint _size) override
|
|
{
|
|
CHECK_ERROR(file != nullptr, L"FileStream::Peek(pos_t)#Stream is closed, cannot perform this operation.");
|
|
CHECK_ERROR(_size >= 0, L"FileStream::Peek(void*, vint)#Argument size cannot be negative.");
|
|
#if defined VCZH_MSVC
|
|
fpos_t position = 0;
|
|
if(fgetpos(file, &position) == 0)
|
|
{
|
|
size_t count = fread(_buffer, 1, _size, file);
|
|
if(fsetpos(file, &position) == 0)
|
|
{
|
|
return count;
|
|
}
|
|
}
|
|
return -1;
|
|
#elif defined VCZH_GCC
|
|
long position = ftell(file);
|
|
size_t count = fread(_buffer, 1, _size, file);
|
|
fseek(file, position, SEEK_SET);
|
|
return count;
|
|
#endif
|
|
}
|
|
};
|
|
|
|
/***********************************************************************
|
|
CreateOSFileStreamImpl
|
|
***********************************************************************/
|
|
|
|
Ptr<IFileStreamImpl> CreateOSFileStreamImpl(const WString& fileName, FileStream::AccessRight accessRight)
|
|
{
|
|
return Ptr(new OSFileStreamImpl(fileName, accessRight));
|
|
}
|
|
|
|
/***********************************************************************
|
|
FileStream
|
|
***********************************************************************/
|
|
|
|
FileStream::FileStream(const WString& fileName, AccessRight _accessRight)
|
|
: accessRight(_accessRight)
|
|
{
|
|
impl = filesystem::GetFileSystemImpl()->GetFileStreamImpl(fileName, _accessRight);
|
|
if(!impl->Open())
|
|
{
|
|
impl = nullptr;
|
|
}
|
|
}
|
|
|
|
FileStream::~FileStream()
|
|
{
|
|
Close();
|
|
}
|
|
|
|
bool FileStream::CanRead() const
|
|
{
|
|
return impl != nullptr && (accessRight == ReadOnly || accessRight == ReadWrite);
|
|
}
|
|
|
|
bool FileStream::CanWrite() const
|
|
{
|
|
return impl != nullptr && (accessRight == WriteOnly || accessRight == ReadWrite);
|
|
}
|
|
|
|
bool FileStream::CanSeek() const
|
|
{
|
|
return impl != nullptr;
|
|
}
|
|
|
|
bool FileStream::CanPeek() const
|
|
{
|
|
return impl != nullptr && (accessRight == ReadOnly || accessRight == ReadWrite);
|
|
}
|
|
|
|
bool FileStream::IsLimited() const
|
|
{
|
|
return impl != nullptr && accessRight == ReadOnly;
|
|
}
|
|
|
|
bool FileStream::IsAvailable() const
|
|
{
|
|
return impl != nullptr;
|
|
}
|
|
|
|
void FileStream::Close()
|
|
{
|
|
if(impl != nullptr)
|
|
{
|
|
impl->Close();
|
|
impl = nullptr;
|
|
}
|
|
}
|
|
|
|
pos_t FileStream::Position() const
|
|
{
|
|
return impl != nullptr ? impl->Position() : -1;
|
|
}
|
|
|
|
pos_t FileStream::Size() const
|
|
{
|
|
return impl != nullptr ? impl->Size() : -1;
|
|
}
|
|
|
|
void FileStream::Seek(pos_t _size)
|
|
{
|
|
CHECK_ERROR(impl != nullptr, L"FileStream::Seek(pos_t)#Stream is closed, cannot perform this operation.");
|
|
impl->Seek(_size);
|
|
}
|
|
|
|
void FileStream::SeekFromBegin(pos_t _size)
|
|
{
|
|
CHECK_ERROR(impl != nullptr, L"FileStream::SeekFromBegin(pos_t)#Stream is closed, cannot perform this operation.");
|
|
impl->SeekFromBegin(_size);
|
|
}
|
|
|
|
void FileStream::SeekFromEnd(pos_t _size)
|
|
{
|
|
CHECK_ERROR(impl != nullptr, L"FileStream::SeekFromEnd(pos_t)#Stream is closed, cannot perform this operation.");
|
|
impl->SeekFromEnd(_size);
|
|
}
|
|
|
|
vint FileStream::Read(void* _buffer, vint _size)
|
|
{
|
|
CHECK_ERROR(impl != nullptr, L"FileStream::Read(pos_t)#Stream is closed, cannot perform this operation.");
|
|
return impl->Read(_buffer, _size);
|
|
}
|
|
|
|
vint FileStream::Write(void* _buffer, vint _size)
|
|
{
|
|
CHECK_ERROR(impl != nullptr, L"FileStream::Write(pos_t)#Stream is closed, cannot perform this operation.");
|
|
return impl->Write(_buffer, _size);
|
|
}
|
|
|
|
vint FileStream::Peek(void* _buffer, vint _size)
|
|
{
|
|
CHECK_ERROR(impl != nullptr, L"FileStream::Peek(pos_t)#Stream is closed, cannot perform this operation.");
|
|
return impl->Peek(_buffer, _size);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\STREAM\MEMORYSTREAM.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
|
|
namespace vl
|
|
{
|
|
namespace stream
|
|
{
|
|
/***********************************************************************
|
|
MemoryStream
|
|
***********************************************************************/
|
|
|
|
void MemoryStream::PrepareSpace(vint totalSpace)
|
|
{
|
|
if(totalSpace>capacity)
|
|
{
|
|
totalSpace=(totalSpace/block+1)*block;
|
|
char* newBuffer=new char[totalSpace];
|
|
if(buffer)
|
|
{
|
|
memcpy(newBuffer, buffer, size);
|
|
delete[] buffer;
|
|
}
|
|
buffer=newBuffer;
|
|
capacity=totalSpace;
|
|
}
|
|
}
|
|
|
|
MemoryStream::MemoryStream(vint _block)
|
|
:block(_block)
|
|
,buffer(0)
|
|
,size(0)
|
|
,position(0)
|
|
,capacity(0)
|
|
{
|
|
if(block<=0)
|
|
{
|
|
block=65536;
|
|
}
|
|
}
|
|
|
|
MemoryStream::~MemoryStream()
|
|
{
|
|
Close();
|
|
}
|
|
|
|
bool MemoryStream::CanRead()const
|
|
{
|
|
return block!=0;
|
|
}
|
|
|
|
bool MemoryStream::CanWrite()const
|
|
{
|
|
return block!=0;
|
|
}
|
|
|
|
bool MemoryStream::CanSeek()const
|
|
{
|
|
return block!=0;
|
|
}
|
|
|
|
bool MemoryStream::CanPeek()const
|
|
{
|
|
return block!=0;
|
|
}
|
|
|
|
bool MemoryStream::IsLimited()const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool MemoryStream::IsAvailable()const
|
|
{
|
|
return block!=0;
|
|
}
|
|
|
|
void MemoryStream::Close()
|
|
{
|
|
if(buffer)
|
|
{
|
|
delete[] buffer;
|
|
}
|
|
block=0;
|
|
buffer=0;
|
|
size=-1;
|
|
position=-1;
|
|
capacity=0;
|
|
}
|
|
|
|
pos_t MemoryStream::Position()const
|
|
{
|
|
return position;
|
|
}
|
|
|
|
pos_t MemoryStream::Size()const
|
|
{
|
|
return size;
|
|
}
|
|
|
|
void MemoryStream::Seek(pos_t _size)
|
|
{
|
|
SeekFromBegin(position+_size);
|
|
}
|
|
|
|
void MemoryStream::SeekFromBegin(pos_t _size)
|
|
{
|
|
CHECK_ERROR(block!=0, L"MemoryStream::SeekFromBegin(pos_t)#Stream is closed, cannot perform this operation.");
|
|
vint expected=(vint)_size;
|
|
if(expected<0)
|
|
{
|
|
position=0;
|
|
}
|
|
else if(expected>=size)
|
|
{
|
|
position=size;
|
|
}
|
|
else
|
|
{
|
|
position=expected;
|
|
}
|
|
}
|
|
|
|
void MemoryStream::SeekFromEnd(pos_t _size)
|
|
{
|
|
SeekFromBegin(size-_size);
|
|
}
|
|
|
|
vint MemoryStream::Read(void* _buffer, vint _size)
|
|
{
|
|
CHECK_ERROR(block!=0, L"MemoryStream::Read(pos_t)#Stream is closed, cannot perform this operation.");
|
|
CHECK_ERROR(_size>=0, L"MemoryStream::Read(void*, vint)#Argument size cannot be negative.");
|
|
vint max=size-position;
|
|
if(_size>max)
|
|
{
|
|
_size=max;
|
|
}
|
|
memmove(_buffer, buffer+position, _size);
|
|
position+=_size;
|
|
return _size;
|
|
}
|
|
|
|
vint MemoryStream::Write(void* _buffer, vint _size)
|
|
{
|
|
CHECK_ERROR(block!=0, L"MemoryStream::Write(pos_t)#Stream is closed, cannot perform this operation.");
|
|
CHECK_ERROR(_size>=0, L"MemoryStream::Write(void*, vint)#Argument size cannot be negative.");
|
|
PrepareSpace(size+_size);
|
|
memmove(buffer+position, _buffer, _size);
|
|
position+=_size;
|
|
if(size<position)
|
|
{
|
|
size=position;
|
|
}
|
|
return _size;
|
|
}
|
|
|
|
vint MemoryStream::Peek(void* _buffer, vint _size)
|
|
{
|
|
CHECK_ERROR(block!=0, L"MemoryStream::Peek(pos_t)#Stream is closed, cannot perform this operation.");
|
|
CHECK_ERROR(_size>=0, L"MemoryStream::Peek(void*, vint)#Argument size cannot be negative.");
|
|
vint max=size-position;
|
|
if(_size>max)
|
|
{
|
|
_size=max;
|
|
}
|
|
memmove(_buffer, buffer+position, _size);
|
|
return _size;
|
|
}
|
|
|
|
void* MemoryStream::GetInternalBuffer()
|
|
{
|
|
return buffer;
|
|
}
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
.\STREAM\MEMORYWRAPPERSTREAM.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
|
|
namespace vl
|
|
{
|
|
namespace stream
|
|
{
|
|
/***********************************************************************
|
|
MemoryWrapperStream
|
|
***********************************************************************/
|
|
|
|
MemoryWrapperStream::MemoryWrapperStream(void* _buffer, vint _size)
|
|
:buffer((char*)_buffer)
|
|
,size(_size)
|
|
,position(0)
|
|
{
|
|
if(size<=0)
|
|
{
|
|
buffer=0;
|
|
size=0;
|
|
}
|
|
}
|
|
|
|
MemoryWrapperStream::~MemoryWrapperStream()
|
|
{
|
|
}
|
|
|
|
bool MemoryWrapperStream::CanRead()const
|
|
{
|
|
return buffer!=0;
|
|
}
|
|
|
|
bool MemoryWrapperStream::CanWrite()const
|
|
{
|
|
return buffer!=0;
|
|
}
|
|
|
|
bool MemoryWrapperStream::CanSeek()const
|
|
{
|
|
return buffer!=0;
|
|
}
|
|
|
|
bool MemoryWrapperStream::CanPeek()const
|
|
{
|
|
return buffer!=0;
|
|
}
|
|
|
|
bool MemoryWrapperStream::IsLimited()const
|
|
{
|
|
return buffer!=0;
|
|
}
|
|
|
|
bool MemoryWrapperStream::IsAvailable()const
|
|
{
|
|
return buffer!=0;
|
|
}
|
|
|
|
void MemoryWrapperStream::Close()
|
|
{
|
|
buffer=0;
|
|
size=-1;
|
|
position=-1;
|
|
}
|
|
|
|
pos_t MemoryWrapperStream::Position()const
|
|
{
|
|
return position;
|
|
}
|
|
|
|
pos_t MemoryWrapperStream::Size()const
|
|
{
|
|
return size;
|
|
}
|
|
|
|
void MemoryWrapperStream::Seek(pos_t _size)
|
|
{
|
|
SeekFromBegin(position+_size);
|
|
}
|
|
|
|
void MemoryWrapperStream::SeekFromBegin(pos_t _size)
|
|
{
|
|
CHECK_ERROR(buffer!=0, L"MemoryWrapperStream::SeekFromBegin(pos_t)#Stream is closed, cannot perform this operation.");
|
|
vint expected=(vint)_size;
|
|
if(expected<0)
|
|
{
|
|
position=0;
|
|
}
|
|
else if(expected>=size)
|
|
{
|
|
position=size;
|
|
}
|
|
else
|
|
{
|
|
position=expected;
|
|
}
|
|
}
|
|
|
|
void MemoryWrapperStream::SeekFromEnd(pos_t _size)
|
|
{
|
|
SeekFromBegin(size-_size);
|
|
}
|
|
|
|
vint MemoryWrapperStream::Read(void* _buffer, vint _size)
|
|
{
|
|
CHECK_ERROR(buffer!=0, L"MemoryWrapperStream::Read(pos_t)#Stream is closed, cannot perform this operation.");
|
|
CHECK_ERROR(_size>=0, L"MemoryWrapperStream::Read(void*, vint)#Argument size cannot be negative.");
|
|
vint max=size-position;
|
|
if(_size>max)
|
|
{
|
|
_size=max;
|
|
}
|
|
memmove(_buffer, buffer+position, _size);
|
|
position+=_size;
|
|
return _size;
|
|
}
|
|
|
|
vint MemoryWrapperStream::Write(void* _buffer, vint _size)
|
|
{
|
|
CHECK_ERROR(buffer!=0, L"MemoryWrapperStream::Write(pos_t)#Stream is closed, cannot perform this operation.");
|
|
CHECK_ERROR(_size>=0, L"MemoryWrapperStream::Write(void*, vint)#Argument size cannot be negative.");
|
|
vint max=size-position;
|
|
if(_size>max)
|
|
{
|
|
_size=max;
|
|
}
|
|
memmove(buffer+position, _buffer, _size);
|
|
position+=_size;
|
|
return _size;
|
|
}
|
|
|
|
vint MemoryWrapperStream::Peek(void* _buffer, vint _size)
|
|
{
|
|
CHECK_ERROR(buffer!=0, L"MemoryWrapperStream::Peek(pos_t)#Stream is closed, cannot perform this operation.");
|
|
CHECK_ERROR(_size>=0, L"MemoryWrapperStream::Peek(void*, vint)#Argument size cannot be negative.");
|
|
vint max=size-position;
|
|
if(_size>max)
|
|
{
|
|
_size=max;
|
|
}
|
|
memmove(_buffer, buffer+position, _size);
|
|
return _size;
|
|
}
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
.\STREAM\RECORDERSTREAM.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
|
|
namespace vl
|
|
{
|
|
namespace stream
|
|
{
|
|
/***********************************************************************
|
|
RecorderStream
|
|
***********************************************************************/
|
|
|
|
RecorderStream::RecorderStream(IStream& _in, IStream& _out)
|
|
:in(&_in)
|
|
, out(&_out)
|
|
{
|
|
}
|
|
|
|
RecorderStream::~RecorderStream()
|
|
{
|
|
}
|
|
|
|
bool RecorderStream::CanRead()const
|
|
{
|
|
return IsAvailable() && in->CanRead();
|
|
}
|
|
|
|
bool RecorderStream::CanWrite()const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool RecorderStream::CanSeek()const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool RecorderStream::CanPeek()const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool RecorderStream::IsLimited()const
|
|
{
|
|
return IsAvailable() && in->IsLimited();
|
|
}
|
|
|
|
bool RecorderStream::IsAvailable()const
|
|
{
|
|
return in != 0 && out != 0 && in->IsAvailable() && out->IsAvailable();
|
|
}
|
|
|
|
void RecorderStream::Close()
|
|
{
|
|
in = nullptr;
|
|
out = nullptr;
|
|
}
|
|
|
|
pos_t RecorderStream::Position()const
|
|
{
|
|
return IsAvailable() ? in->Position() : -1;
|
|
}
|
|
|
|
pos_t RecorderStream::Size()const
|
|
{
|
|
return IsAvailable() ? in->Size() : -1;
|
|
}
|
|
|
|
void RecorderStream::Seek(pos_t _size)
|
|
{
|
|
CHECK_FAIL(L"RecorderStream::Seek(pos_t)#Operation not supported.");
|
|
}
|
|
|
|
void RecorderStream::SeekFromBegin(pos_t _size)
|
|
{
|
|
CHECK_FAIL(L"RecorderStream::SeekFromBegin(pos_t)#Operation not supported.");
|
|
}
|
|
|
|
void RecorderStream::SeekFromEnd(pos_t _size)
|
|
{
|
|
CHECK_FAIL(L"RecorderStream::SeekFromEnd(pos_t)#Operation not supported.");
|
|
}
|
|
|
|
vint RecorderStream::Read(void* _buffer, vint _size)
|
|
{
|
|
_size = in->Read(_buffer, _size);
|
|
vint written = out->Write(_buffer, _size);
|
|
CHECK_ERROR(written == _size, L"RecorderStream::Read(void*, vint)#Failed to copy data to the output stream.");
|
|
return _size;
|
|
}
|
|
|
|
vint RecorderStream::Write(void* _buffer, vint _size)
|
|
{
|
|
CHECK_FAIL(L"RecorderStream::Write(void*, vint)#Operation not supported.");
|
|
}
|
|
|
|
vint RecorderStream::Peek(void* _buffer, vint _size)
|
|
{
|
|
CHECK_FAIL(L"RecorderStream::Peek(void*, vint)#Operation not supported.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET.CPP
|
|
***********************************************************************/
|
|
#include <chrono>
|
|
#include <cstring>
|
|
#include <limits>
|
|
|
|
namespace vl::inter_process::async_tcp_socket
|
|
{
|
|
thread_local NetworkProtocolCallbackDomain::CallbackFrame* NetworkProtocolCallbackDomain::currentCallbackFrame = nullptr;
|
|
thread_local NetworkProtocolConnection::CallbackFrame* NetworkProtocolConnection::currentCallbackFrame = nullptr;
|
|
thread_local NetworkProtocolConnection::SocketCallbackFrame*
|
|
NetworkProtocolConnection::currentSocketCallbackFrame = nullptr;
|
|
|
|
/***********************************************************************
|
|
IAsyncSocketCallback
|
|
***********************************************************************/
|
|
|
|
void IAsyncSocketCallback::OnWriteCompleted(Ptr<AsyncSocketBuffer>)
|
|
{
|
|
}
|
|
|
|
void IAsyncSocketCallback::OnError(const WString&, bool)
|
|
{
|
|
}
|
|
|
|
void IAsyncSocketCallback::OnConnected()
|
|
{
|
|
}
|
|
|
|
void IAsyncSocketCallback::OnDisconnected()
|
|
{
|
|
}
|
|
|
|
AsyncSocketServerStartException::AsyncSocketServerStartException(AsyncSocketServerStartFailure _failure, const WString& message)
|
|
: Exception(message)
|
|
, failure(_failure)
|
|
{
|
|
}
|
|
|
|
AsyncSocketServerStartFailure AsyncSocketServerStartException::GetFailure()const
|
|
{
|
|
return failure;
|
|
}
|
|
|
|
void IAsyncSocketServerCallback::OnServerStopped()
|
|
{
|
|
}
|
|
|
|
/***********************************************************************
|
|
NetworkProtocolCallbackDomain::CallbackFrame
|
|
***********************************************************************/
|
|
|
|
NetworkProtocolCallbackDomain::CallbackFrame::CallbackFrame(Ptr<NetworkProtocolCallbackDomain> _domain)
|
|
: domain(_domain)
|
|
{
|
|
if (domain)
|
|
{
|
|
previous = currentCallbackFrame;
|
|
currentCallbackFrame = this;
|
|
CS_LOCK(domain->lockState)
|
|
{
|
|
domain->activeCallbacks++;
|
|
}
|
|
}
|
|
}
|
|
|
|
NetworkProtocolCallbackDomain::CallbackFrame::~CallbackFrame()
|
|
{
|
|
if (domain)
|
|
{
|
|
currentCallbackFrame = previous;
|
|
CS_LOCK(domain->lockState)
|
|
{
|
|
domain->activeCallbacks--;
|
|
domain->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
NetworkProtocolCallbackDomain
|
|
***********************************************************************/
|
|
|
|
vint NetworkProtocolCallbackDomain::CurrentCallbackDepth()
|
|
{
|
|
vint depth = 0;
|
|
for (auto frame = currentCallbackFrame; frame; frame = frame->previous)
|
|
{
|
|
if (frame->domain.Obj() == this)
|
|
{
|
|
depth++;
|
|
}
|
|
}
|
|
return depth;
|
|
}
|
|
|
|
void NetworkProtocolCallbackDomain::WaitForCallbacks(vint callbackDepth)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
while (activeCallbacks > callbackDepth)
|
|
{
|
|
cvState.SleepWith(lockState);
|
|
}
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
NetworkProtocolConnectionLifecycle
|
|
***********************************************************************/
|
|
|
|
void NetworkProtocolConnectionLifecycle::TakeRetainedAdapterIfDrained(Ptr<Object>& releasing)
|
|
{
|
|
if (stopFinished && disconnectFinished && activeCallbacks == 0 && activeSocketCallbacks == 0 && activeSocketCalls == 0)
|
|
{
|
|
releasing = std::move(retainedAdapter);
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
NetworkProtocolConnection::CallbackFrame
|
|
***********************************************************************/
|
|
|
|
NetworkProtocolConnection::CallbackFrame::CallbackFrame(Ptr<Lifecycle> _state)
|
|
: state(_state)
|
|
, previous(currentCallbackFrame)
|
|
, domainFrame(state->callbackDomain)
|
|
{
|
|
currentCallbackFrame = this;
|
|
}
|
|
|
|
NetworkProtocolConnection::CallbackFrame::~CallbackFrame()
|
|
{
|
|
currentCallbackFrame = previous;
|
|
Ptr<Object> releasing;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->activeCallbacks--;
|
|
state->TakeRetainedAdapterIfDrained(releasing);
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
NetworkProtocolConnection::SocketCallbackFrame
|
|
***********************************************************************/
|
|
|
|
NetworkProtocolConnection::SocketCallbackFrame::SocketCallbackFrame(Ptr<Lifecycle> _state)
|
|
: state(_state)
|
|
, previous(currentSocketCallbackFrame)
|
|
{
|
|
currentSocketCallbackFrame = this;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->activeSocketCallbacks++;
|
|
}
|
|
}
|
|
|
|
NetworkProtocolConnection::SocketCallbackFrame::~SocketCallbackFrame()
|
|
{
|
|
currentSocketCallbackFrame = previous;
|
|
Ptr<Object> releasing;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->activeSocketCallbacks--;
|
|
state->TakeRetainedAdapterIfDrained(releasing);
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
NetworkProtocolConnection
|
|
***********************************************************************/
|
|
|
|
vint NetworkProtocolConnection::CurrentCallbackDepth(Ptr<Lifecycle> state)
|
|
{
|
|
vint depth = 0;
|
|
for (auto frame = currentCallbackFrame; frame; frame = frame->previous)
|
|
{
|
|
if (frame->state.Obj() == state.Obj())
|
|
{
|
|
depth++;
|
|
}
|
|
}
|
|
return depth;
|
|
}
|
|
|
|
vint NetworkProtocolConnection::CurrentSocketCallbackDepth(Ptr<Lifecycle> state)
|
|
{
|
|
vint depth = 0;
|
|
for (auto frame = currentSocketCallbackFrame; frame; frame = frame->previous)
|
|
{
|
|
if (frame->state.Obj() == state.Obj())
|
|
{
|
|
depth++;
|
|
}
|
|
}
|
|
return depth;
|
|
}
|
|
|
|
void NetworkProtocolConnection::FinishSocketCall(Ptr<Lifecycle> state)
|
|
{
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->activeSocketCalls--;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
|
|
template<typename TCallback>
|
|
void NetworkProtocolConnection::InvokeProtocolCallback(Ptr<Lifecycle> state, bool allowTerminal, TCallback&& invoke)
|
|
{
|
|
INetworkProtocolCallback* installed = nullptr;
|
|
auto callbackDepth = CurrentCallbackDepth(state);
|
|
state->lockState.Enter();
|
|
while (state->callbackInstalling && callbackDepth == 0 && state->callback)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
if (state->callback && (allowTerminal || (!state->stopStarted && !state->terminal)))
|
|
{
|
|
installed = state->callback;
|
|
state->activeCallbacks++;
|
|
}
|
|
state->lockState.Leave();
|
|
|
|
if (installed)
|
|
{
|
|
CallbackFrame frame(state);
|
|
invoke(installed);
|
|
}
|
|
}
|
|
|
|
void NetworkProtocolConnection::SubmitWrite(Ptr<Lifecycle> state, IAsyncSocketConnection* connection, Ptr<AsyncSocketBuffer> buffer)
|
|
{
|
|
try
|
|
{
|
|
connection->WriteAsync(buffer);
|
|
}
|
|
catch (...)
|
|
{
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->queuedWrites.Clear();
|
|
state->writePending = false;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
FinishSocketCall(state);
|
|
throw;
|
|
}
|
|
FinishSocketCall(state);
|
|
}
|
|
|
|
void NetworkProtocolConnection::NotifyProtocolDisconnected(Ptr<Lifecycle> state)
|
|
{
|
|
auto callbackDepth = CurrentCallbackDepth(state);
|
|
state->lockState.Enter();
|
|
if (!state->disconnectedNotified)
|
|
{
|
|
state->disconnectedNotified = true;
|
|
state->terminal = true;
|
|
state->queuedWrites.Clear();
|
|
state->writePending = false;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
if (state->disconnectFinished)
|
|
{
|
|
state->lockState.Leave();
|
|
return;
|
|
}
|
|
|
|
if (state->disconnectDelivering)
|
|
{
|
|
if (callbackDepth == 0)
|
|
{
|
|
while (!state->disconnectFinished)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
}
|
|
state->lockState.Leave();
|
|
return;
|
|
}
|
|
|
|
if (callbackDepth == 0)
|
|
{
|
|
while (state->activeCallbacks > 0 && !state->disconnectDelivering && !state->disconnectFinished)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
if (state->disconnectFinished)
|
|
{
|
|
state->lockState.Leave();
|
|
return;
|
|
}
|
|
if (state->disconnectDelivering)
|
|
{
|
|
while (!state->disconnectFinished)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
state->lockState.Leave();
|
|
return;
|
|
}
|
|
}
|
|
|
|
state->disconnectDelivering = true;
|
|
while (state->activeCallbacks > callbackDepth)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
state->lockState.Leave();
|
|
|
|
try
|
|
{
|
|
InvokeProtocolCallback(state, true, [](INetworkProtocolCallback* installed)
|
|
{
|
|
installed->OnDisconnected();
|
|
});
|
|
}
|
|
catch (...)
|
|
{
|
|
Ptr<Object> releasing;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->callback = nullptr;
|
|
while (state->activeCallbacks > callbackDepth)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
state->disconnectFinished = true;
|
|
state->TakeRetainedAdapterIfDrained(releasing);
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
throw;
|
|
}
|
|
|
|
Ptr<Object> releasing;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->callback = nullptr;
|
|
while (state->activeCallbacks > callbackDepth)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
state->disconnectFinished = true;
|
|
state->TakeRetainedAdapterIfDrained(releasing);
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
|
|
void NetworkProtocolConnection::DetachSocketCallback(Ptr<Lifecycle> state, IAsyncSocketConnection* connection)
|
|
{
|
|
if (connection)
|
|
{
|
|
connection->InstallCallback(nullptr);
|
|
}
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (state->socketConnection == connection)
|
|
{
|
|
state->socketConnection = nullptr;
|
|
}
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
|
|
void NetworkProtocolConnection::StopConnection(Ptr<Lifecycle> state, Ptr<Object> retainedAdapter)
|
|
{
|
|
auto callbackDepth = CurrentCallbackDepth(state);
|
|
auto socketCallbackDepth = CurrentSocketCallbackDepth(state);
|
|
auto nestedCallback = callbackDepth > 0 || socketCallbackDepth > 0;
|
|
IAsyncSocketConnection* connection = nullptr;
|
|
bool executeStop = false;
|
|
bool nestedFollower = false;
|
|
Ptr<Object> releasing;
|
|
|
|
state->lockState.Enter();
|
|
if (retainedAdapter)
|
|
{
|
|
state->retainedAdapter = retainedAdapter;
|
|
}
|
|
if (!state->stopStarted)
|
|
{
|
|
state->stopStarted = true;
|
|
if (!nestedCallback && !state->terminal && state->queuedWrites.Count() > 0)
|
|
{
|
|
state->drainWrites = true;
|
|
auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(WriteDrainTimeout);
|
|
while (state->queuedWrites.Count() > 0 && !state->terminal)
|
|
{
|
|
auto now = std::chrono::steady_clock::now();
|
|
if (now >= deadline)
|
|
{
|
|
break;
|
|
}
|
|
auto remaining = std::chrono::ceil<std::chrono::milliseconds>(deadline - now).count();
|
|
state->cvState.SleepWithForTime(state->lockState, (vint)remaining);
|
|
}
|
|
state->drainWrites = false;
|
|
}
|
|
state->queuedWrites.Clear();
|
|
state->writePending = false;
|
|
while (state->activeSocketCalls > 0)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
connection = state->socketConnection;
|
|
executeStop = true;
|
|
}
|
|
else if (nestedCallback)
|
|
{
|
|
connection = state->socketConnection;
|
|
nestedFollower = true;
|
|
}
|
|
else
|
|
{
|
|
while (!state->stopFinished)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
while (state->activeCallbacks > 0 || state->activeSocketCallbacks > 0 || state->activeSocketCalls > 0)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
state->TakeRetainedAdapterIfDrained(releasing);
|
|
state->lockState.Leave();
|
|
return;
|
|
}
|
|
state->lockState.Leave();
|
|
|
|
if (nestedFollower)
|
|
{
|
|
if (connection && socketCallbackDepth > 0)
|
|
{
|
|
connection->Stop();
|
|
}
|
|
NotifyProtocolDisconnected(state);
|
|
return;
|
|
}
|
|
|
|
if (executeStop && connection)
|
|
{
|
|
connection->Stop();
|
|
}
|
|
NotifyProtocolDisconnected(state);
|
|
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
while (state->activeCallbacks > callbackDepth || state->activeSocketCallbacks > socketCallbackDepth)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
state->stopFinished = true;
|
|
state->TakeRetainedAdapterIfDrained(releasing);
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
|
|
void NetworkProtocolConnection::ReportFatalError(Ptr<Lifecycle> state, const WString& error)
|
|
{
|
|
bool report = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (!state->terminal && !state->stopStarted)
|
|
{
|
|
state->terminal = true;
|
|
state->queuedWrites.Clear();
|
|
state->writePending = false;
|
|
state->cvState.WakeAllPendings();
|
|
report = true;
|
|
}
|
|
}
|
|
if (report)
|
|
{
|
|
try
|
|
{
|
|
InvokeProtocolCallback(state, true, [&](INetworkProtocolCallback* installed)
|
|
{
|
|
installed->OnLocalError(error, true);
|
|
});
|
|
}
|
|
catch (...)
|
|
{
|
|
StopConnection(state);
|
|
throw;
|
|
}
|
|
StopConnection(state);
|
|
}
|
|
}
|
|
|
|
void NetworkProtocolConnection::StopWithRetainedAdapter(Ptr<NetworkProtocolConnection> retainedAdapter)
|
|
{
|
|
StopConnection(lifecycle, retainedAdapter);
|
|
}
|
|
|
|
NetworkProtocolConnection::NetworkProtocolConnection(IAsyncSocketConnection* connection, Ptr<NetworkProtocolCallbackDomain> callbackDomain)
|
|
: lifecycle(Ptr(new Lifecycle))
|
|
{
|
|
CHECK_ERROR(connection, L"NetworkProtocolConnection requires a valid async socket connection.");
|
|
lifecycle->socketConnection = connection;
|
|
lifecycle->callbackDomain = callbackDomain;
|
|
connection->InstallCallback(this);
|
|
}
|
|
|
|
NetworkProtocolConnection::~NetworkProtocolConnection()
|
|
{
|
|
StopConnection(lifecycle);
|
|
}
|
|
|
|
void NetworkProtocolConnection::InstallCallback(INetworkProtocolCallback* value)
|
|
{
|
|
auto state = lifecycle;
|
|
if (!value)
|
|
{
|
|
auto callbackDepth = CurrentCallbackDepth(state);
|
|
bool uninstallOwner = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
uninstallOwner = state->callback != nullptr;
|
|
state->callback = nullptr;
|
|
while ((callbackDepth == 0 || uninstallOwner) && state->activeCallbacks > callbackDepth)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
bool canInstall = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (!state->callback && !state->callbackInstalling && !state->stopStarted && !state->terminal)
|
|
{
|
|
state->callback = value;
|
|
state->callbackInstalling = true;
|
|
state->activeCallbacks++;
|
|
canInstall = true;
|
|
}
|
|
}
|
|
CHECK_ERROR(canInstall, L"NetworkProtocolConnection::InstallCallback cannot replace a callback or install one on a stopped connection.");
|
|
|
|
CallbackFrame frame(state);
|
|
try
|
|
{
|
|
value->OnInstalled(this);
|
|
}
|
|
catch (...)
|
|
{
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (state->callback == value)
|
|
{
|
|
state->callback = nullptr;
|
|
}
|
|
state->callbackInstalling = false;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
throw;
|
|
}
|
|
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->callbackInstalling = false;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
|
|
void NetworkProtocolConnection::BeginReadingLoopUnsafe()
|
|
{
|
|
auto state = lifecycle;
|
|
IAsyncSocketConnection* connection = nullptr;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (!state->stopStarted && !state->terminal && state->socketConnection)
|
|
{
|
|
connection = state->socketConnection;
|
|
state->activeSocketCalls++;
|
|
}
|
|
}
|
|
if (!connection)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
connection->BeginReadingLoopUnsafe();
|
|
}
|
|
catch (...)
|
|
{
|
|
FinishSocketCall(state);
|
|
throw;
|
|
}
|
|
FinishSocketCall(state);
|
|
}
|
|
|
|
void NetworkProtocolConnection::SendString(const WString& str)
|
|
{
|
|
auto state = lifecycle;
|
|
auto length = str.Length();
|
|
CHECK_ERROR(length >= 0 && length <= (std::numeric_limits<vint32_t>::max)(), L"NetworkProtocolConnection::SendString cannot encode a string longer than vint32_t.");
|
|
CHECK_ERROR((size_t)length <= ((size_t)(std::numeric_limits<vint>::max)() - sizeof(vint32_t)) / sizeof(wchar_t), L"NetworkProtocolConnection::SendString frame size overflow.");
|
|
|
|
auto buffer = Ptr(new AsyncSocketBuffer);
|
|
auto characterBytes = (size_t)length * sizeof(wchar_t);
|
|
auto frameBytes = sizeof(vint32_t) + characterBytes;
|
|
buffer->data.Resize((vint)frameBytes);
|
|
auto encodedLength = (vint32_t)length;
|
|
std::memcpy(&buffer->data[0], &encodedLength, sizeof(encodedLength));
|
|
if (characterBytes > 0)
|
|
{
|
|
std::memcpy(&buffer->data[sizeof(encodedLength)], str.Buffer(), characterBytes);
|
|
}
|
|
|
|
IAsyncSocketConnection* connection = nullptr;
|
|
Ptr<AsyncSocketBuffer> submitting;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (!state->stopStarted && !state->terminal && state->socketConnection)
|
|
{
|
|
state->queuedWrites.Add(buffer);
|
|
if (!state->writePending)
|
|
{
|
|
state->writePending = true;
|
|
connection = state->socketConnection;
|
|
submitting = state->queuedWrites[0];
|
|
state->activeSocketCalls++;
|
|
}
|
|
}
|
|
}
|
|
if (submitting)
|
|
{
|
|
SubmitWrite(state, connection, submitting);
|
|
}
|
|
}
|
|
|
|
void NetworkProtocolConnection::Stop()
|
|
{
|
|
StopConnection(lifecycle);
|
|
}
|
|
|
|
void NetworkProtocolConnection::OnRead(const vuint8_t* buffer, vint size)
|
|
{
|
|
auto state = lifecycle;
|
|
SocketCallbackFrame socketCallbackFrame(state);
|
|
if (!buffer || size <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
collections::List<WString> completedStrings;
|
|
bool malformed = false;
|
|
CS_LOCK(state->lockParser)
|
|
{
|
|
if (state->parserFailed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto reading = buffer;
|
|
auto available = size;
|
|
while (available > 0)
|
|
{
|
|
if (state->expectedCharacters == -1)
|
|
{
|
|
auto required = (vint)sizeof(vint32_t) - state->lengthBytesReceived;
|
|
auto copied = required < available ? required : available;
|
|
std::memcpy(state->lengthBytes + state->lengthBytesReceived, reading, (size_t)copied);
|
|
state->lengthBytesReceived += copied;
|
|
reading += copied;
|
|
available -= copied;
|
|
if (state->lengthBytesReceived < (vint)sizeof(vint32_t))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
std::memcpy(&state->expectedCharacters, state->lengthBytes, sizeof(state->expectedCharacters));
|
|
state->lengthBytesReceived = 0;
|
|
if (state->expectedCharacters < 0 || (size_t)state->expectedCharacters > ((size_t)(std::numeric_limits<vint>::max)() - sizeof(vint32_t)) / sizeof(wchar_t))
|
|
{
|
|
state->parserFailed = true;
|
|
malformed = true;
|
|
break;
|
|
}
|
|
|
|
state->characterBuffer.Resize((vint)state->expectedCharacters);
|
|
state->characterBytesReceived = 0;
|
|
if (state->expectedCharacters == 0)
|
|
{
|
|
completedStrings.Add(WString());
|
|
state->expectedCharacters = -1;
|
|
}
|
|
}
|
|
|
|
if (state->expectedCharacters >= 0)
|
|
{
|
|
auto characterBytes = (vint)((size_t)state->expectedCharacters * sizeof(wchar_t));
|
|
auto required = characterBytes - state->characterBytesReceived;
|
|
auto copied = required < available ? required : available;
|
|
std::memcpy((vuint8_t*)&state->characterBuffer[0] + state->characterBytesReceived, reading, (size_t)copied);
|
|
state->characterBytesReceived += copied;
|
|
reading += copied;
|
|
available -= copied;
|
|
if (state->characterBytesReceived == characterBytes)
|
|
{
|
|
completedStrings.Add(WString::CopyFrom(&state->characterBuffer[0], state->expectedCharacters));
|
|
state->characterBuffer.Resize(0);
|
|
state->characterBytesReceived = 0;
|
|
state->expectedCharacters = -1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (auto&& str : completedStrings)
|
|
{
|
|
InvokeProtocolCallback(state, false, [&](INetworkProtocolCallback* installed)
|
|
{
|
|
installed->OnReadString(str);
|
|
});
|
|
}
|
|
if (malformed)
|
|
{
|
|
ReportFatalError(state, L"The async socket protocol received an invalid string length.");
|
|
}
|
|
}
|
|
|
|
void NetworkProtocolConnection::OnWriteCompleted(Ptr<AsyncSocketBuffer> buffer)
|
|
{
|
|
auto state = lifecycle;
|
|
SocketCallbackFrame socketCallbackFrame(state);
|
|
IAsyncSocketConnection* connection = nullptr;
|
|
Ptr<AsyncSocketBuffer> submitting;
|
|
bool mismatched = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (state->queuedWrites.Count() == 0)
|
|
{
|
|
return;
|
|
}
|
|
if (state->queuedWrites[0].Obj() != buffer.Obj())
|
|
{
|
|
state->queuedWrites.Clear();
|
|
state->writePending = false;
|
|
mismatched = true;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
else
|
|
{
|
|
state->queuedWrites.RemoveAt(0);
|
|
if ((!state->stopStarted || state->drainWrites) && !state->terminal && state->socketConnection && state->queuedWrites.Count() > 0)
|
|
{
|
|
connection = state->socketConnection;
|
|
submitting = state->queuedWrites[0];
|
|
state->activeSocketCalls++;
|
|
}
|
|
else
|
|
{
|
|
state->writePending = false;
|
|
}
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
CHECK_ERROR(!mismatched, L"NetworkProtocolConnection received a completion for an unexpected async socket buffer.");
|
|
if (submitting)
|
|
{
|
|
SubmitWrite(state, connection, submitting);
|
|
}
|
|
}
|
|
|
|
void NetworkProtocolConnection::OnError(const WString& error, bool fatal)
|
|
{
|
|
auto state = lifecycle;
|
|
SocketCallbackFrame socketCallbackFrame(state);
|
|
if (fatal)
|
|
{
|
|
ReportFatalError(state, error);
|
|
}
|
|
else
|
|
{
|
|
InvokeProtocolCallback(state, false, [&](INetworkProtocolCallback* installed)
|
|
{
|
|
installed->OnLocalError(error, false);
|
|
});
|
|
}
|
|
}
|
|
|
|
void NetworkProtocolConnection::OnConnected()
|
|
{
|
|
auto state = lifecycle;
|
|
SocketCallbackFrame socketCallbackFrame(state);
|
|
InvokeProtocolCallback(state, false, [](INetworkProtocolCallback* installed)
|
|
{
|
|
installed->OnConnected();
|
|
});
|
|
}
|
|
|
|
void NetworkProtocolConnection::OnDisconnected()
|
|
{
|
|
auto state = lifecycle;
|
|
SocketCallbackFrame socketCallbackFrame(state);
|
|
IAsyncSocketConnection* connection = nullptr;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
while (state->activeSocketCalls > 0)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
connection = state->socketConnection;
|
|
}
|
|
|
|
try
|
|
{
|
|
DetachSocketCallback(state, connection);
|
|
}
|
|
catch (...)
|
|
{
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (state->socketConnection == connection)
|
|
{
|
|
state->socketConnection = nullptr;
|
|
}
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
NotifyProtocolDisconnected(state);
|
|
throw;
|
|
}
|
|
NotifyProtocolDisconnected(state);
|
|
}
|
|
|
|
void NetworkProtocolConnection::OnInstalled(IAsyncSocketConnection* connection)
|
|
{
|
|
auto state = lifecycle;
|
|
SocketCallbackFrame socketCallbackFrame(state);
|
|
CHECK_ERROR(connection == state->socketConnection, L"NetworkProtocolConnection was installed on an unexpected async socket connection.");
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPCLIENT.CPP
|
|
***********************************************************************/
|
|
|
|
|
|
namespace vl::inter_process::async_tcp_socket
|
|
{
|
|
using namespace vl::collections;
|
|
|
|
namespace
|
|
{
|
|
constexpr vint HttpRequestMaxAttempts = 3;
|
|
constexpr vint SendDrainTimeout = 1000;
|
|
|
|
class SameEndpointClientContractException : public Exception
|
|
{
|
|
public:
|
|
SameEndpointClientContractException(const WString& message)
|
|
: Exception(message)
|
|
{
|
|
}
|
|
};
|
|
|
|
wchar_t FoldServerCharacter(wchar_t c)
|
|
{
|
|
return L'A' <= c && c <= L'Z' ? c - L'A' + L'a' : c;
|
|
}
|
|
|
|
bool ValidateLoopbackServer(const WString& server)
|
|
{
|
|
if (server == L"127.0.0.1") return true;
|
|
const wchar_t localhost[] = L"localhost";
|
|
if (server.Length() != 9) return false;
|
|
for (vint i = 0; i < 9; i++)
|
|
{
|
|
if (FoldServerCharacter(server[i]) != localhost[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
WString NormalizeUrlPrefix(const WString& urlPrefix)
|
|
{
|
|
auto normalizedUrlPrefix = urlPrefix;
|
|
while (normalizedUrlPrefix.Length() > 0 && normalizedUrlPrefix[normalizedUrlPrefix.Length() - 1] == L'/')
|
|
{
|
|
normalizedUrlPrefix = normalizedUrlPrefix.Left(normalizedUrlPrefix.Length() - 1);
|
|
}
|
|
return normalizedUrlPrefix;
|
|
}
|
|
|
|
WString DescribeHttpError(const wchar_t* operation, const windows_http::HttpError& error)
|
|
{
|
|
return WString::Unmanaged(operation) + L" failed: " + error.message;
|
|
}
|
|
|
|
bool IsResponseNotFoundError(const windows_http::HttpError& error)
|
|
{
|
|
return error.errorCode == (vuint32_t)SocketHttpClientErrorCode::ResponseNotFound;
|
|
}
|
|
|
|
bool DecodeSuccessfulResponse(
|
|
const windows_http::HttpResponse& response,
|
|
const wchar_t* operation,
|
|
WString& body,
|
|
WString& error
|
|
)
|
|
{
|
|
if (response.statusCode != 200)
|
|
{
|
|
error = WString::Unmanaged(operation) + L" returned status code " + itow(response.statusCode) + L".";
|
|
return false;
|
|
}
|
|
if (response.contentType != HttpNetworkProtocolContentType)
|
|
{
|
|
error = WString::Unmanaged(operation) + L" did not return the required content type.";
|
|
return false;
|
|
}
|
|
if (!response.TryGetBodyUtf8(body) || (body.Length() > 0 && !IsValidHttpNetworkProtocolMessage(body)))
|
|
{
|
|
error = WString::Unmanaged(operation) + L" returned malformed UTF-8 or an embedded NUL.";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
BEGIN_GLOBAL_STORAGE_CLASS(SocketHttpClientTestHooks)
|
|
SpinLock lock;
|
|
Func<void()> receiveSubmitted;
|
|
Func<void()> fatalReserved;
|
|
Func<void()> stopStarted;
|
|
INITIALIZE_GLOBAL_STORAGE_CLASS
|
|
FINALIZE_GLOBAL_STORAGE_CLASS
|
|
SPIN_LOCK(lock)
|
|
{
|
|
receiveSubmitted = {};
|
|
fatalReserved = {};
|
|
stopStarted = {};
|
|
}
|
|
END_GLOBAL_STORAGE_CLASS(SocketHttpClientTestHooks)
|
|
|
|
void InvokeReceiveSubmittedForTesting()
|
|
{
|
|
Func<void()> callback;
|
|
auto& hooks = GetSocketHttpClientTestHooks();
|
|
SPIN_LOCK(hooks.lock)
|
|
{
|
|
callback = hooks.receiveSubmitted;
|
|
}
|
|
if (callback)
|
|
{
|
|
try { callback(); }
|
|
catch (...) {}
|
|
}
|
|
}
|
|
|
|
void InvokeFatalReservedForTesting()
|
|
{
|
|
Func<void()> callback;
|
|
auto& hooks = GetSocketHttpClientTestHooks();
|
|
SPIN_LOCK(hooks.lock)
|
|
{
|
|
callback = hooks.fatalReserved;
|
|
}
|
|
if (callback)
|
|
{
|
|
try { callback(); }
|
|
catch (...) {}
|
|
}
|
|
}
|
|
|
|
void InvokeStopStartedForTesting()
|
|
{
|
|
Func<void()> callback;
|
|
auto& hooks = GetSocketHttpClientTestHooks();
|
|
SPIN_LOCK(hooks.lock)
|
|
{
|
|
callback = hooks.stopStarted;
|
|
}
|
|
if (callback)
|
|
{
|
|
try { callback(); }
|
|
catch (...) {}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void SetSocketHttpClientReceiveSubmittedCallbackForTesting(const Func<void()>& callback)
|
|
{
|
|
auto& hooks = GetSocketHttpClientTestHooks();
|
|
SPIN_LOCK(hooks.lock)
|
|
{
|
|
hooks.receiveSubmitted = callback;
|
|
}
|
|
}
|
|
|
|
void ResetSocketHttpClientReceiveSubmittedCallbackForTesting()
|
|
{
|
|
SetSocketHttpClientReceiveSubmittedCallbackForTesting({});
|
|
}
|
|
|
|
void SetSocketHttpClientFatalStopCallbacksForTesting(
|
|
const Func<void()>& fatalReserved,
|
|
const Func<void()>& stopStarted
|
|
)
|
|
{
|
|
auto& hooks = GetSocketHttpClientTestHooks();
|
|
SPIN_LOCK(hooks.lock)
|
|
{
|
|
hooks.fatalReserved = fatalReserved;
|
|
hooks.stopStarted = stopStarted;
|
|
}
|
|
}
|
|
|
|
void ResetSocketHttpClientFatalStopCallbacksForTesting()
|
|
{
|
|
SetSocketHttpClientFatalStopCallbacksForTesting({}, {});
|
|
}
|
|
|
|
/***********************************************************************
|
|
SocketHttpClient::Impl
|
|
***********************************************************************/
|
|
|
|
class SocketHttpClient::Impl : public Object
|
|
{
|
|
using QueryResult = Variant<windows_http::HttpResponse, windows_http::HttpError>;
|
|
|
|
enum class State
|
|
{
|
|
Ready,
|
|
WaitingForServer,
|
|
Connected,
|
|
Stopping,
|
|
};
|
|
|
|
class SendItem : public Object
|
|
{
|
|
public:
|
|
Array<char> body;
|
|
vint attempt = 1;
|
|
};
|
|
|
|
class QueryWaiter : public Object
|
|
{
|
|
private:
|
|
CriticalSection lock;
|
|
ConditionVariable cv;
|
|
Ptr<QueryResult> result;
|
|
|
|
public:
|
|
void Complete(QueryResult value)
|
|
{
|
|
CS_LOCK(lock)
|
|
{
|
|
if (!result)
|
|
{
|
|
result = Ptr(new QueryResult(std::move(value)));
|
|
cv.WakeAllPendings();
|
|
}
|
|
}
|
|
}
|
|
|
|
Ptr<QueryResult> Wait()
|
|
{
|
|
CS_LOCK(lock)
|
|
{
|
|
while (!result) cv.SleepWith(lock);
|
|
return result;
|
|
}
|
|
return nullptr;
|
|
}
|
|
};
|
|
|
|
struct CallbackFrame
|
|
{
|
|
Ptr<Impl> state;
|
|
CallbackFrame* previous = nullptr;
|
|
NetworkProtocolCallbackDomain::CallbackFrame
|
|
domainFrame;
|
|
|
|
CallbackFrame(Ptr<Impl> _state)
|
|
: state(_state)
|
|
, previous(currentCallbackFrame)
|
|
, domainFrame(state->callbackDomain)
|
|
{
|
|
currentCallbackFrame = this;
|
|
}
|
|
|
|
~CallbackFrame()
|
|
{
|
|
currentCallbackFrame = previous;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->activeCallbacks--;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
};
|
|
|
|
struct WorkerFrame
|
|
{
|
|
Ptr<Impl> state;
|
|
WorkerFrame* previous = nullptr;
|
|
|
|
WorkerFrame(Ptr<Impl> _state)
|
|
: state(_state)
|
|
, previous(currentWorkerFrame)
|
|
{
|
|
currentWorkerFrame = this;
|
|
}
|
|
|
|
~WorkerFrame()
|
|
{
|
|
currentWorkerFrame = previous;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->activeWorkers--;
|
|
if (state->hardStopping)
|
|
{
|
|
state->sendReconnecting = false;
|
|
state->receiveReconnecting = false;
|
|
}
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
};
|
|
|
|
struct WaitFrame
|
|
{
|
|
Ptr<Impl> state;
|
|
WaitFrame* previous = nullptr;
|
|
|
|
WaitFrame(Ptr<Impl> _state)
|
|
: state(_state)
|
|
, previous(currentWaitFrame)
|
|
{
|
|
currentWaitFrame = this;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->activeWaits++;
|
|
}
|
|
}
|
|
|
|
~WaitFrame()
|
|
{
|
|
currentWaitFrame = previous;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->activeWaits--;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
};
|
|
|
|
static thread_local CallbackFrame* currentCallbackFrame;
|
|
static thread_local WorkerFrame* currentWorkerFrame;
|
|
static thread_local WaitFrame* currentWaitFrame;
|
|
|
|
SocketHttpClient* owner = nullptr;
|
|
WString server;
|
|
vint port = 0;
|
|
Ptr<IAsyncSocketClient> clientSource;
|
|
Ptr<IAsyncSocketClient> initialClient;
|
|
WString urlPrefix;
|
|
WString urlConnect;
|
|
WString urlRequest;
|
|
WString urlResponse;
|
|
Ptr<Impl> selfReference;
|
|
Ptr<NetworkProtocolCallbackDomain> callbackDomain = Ptr(new NetworkProtocolCallbackDomain);
|
|
|
|
CriticalSection lockState;
|
|
CriticalSection lockClientCreation;
|
|
ConditionVariable cvState;
|
|
State state = State::Ready;
|
|
INetworkProtocolCallback* callback = nullptr;
|
|
bool callbackInstalling = false;
|
|
vint activeCallbacks = 0;
|
|
vint activeWorkers = 0;
|
|
vint activeWaits = 0;
|
|
bool readingStarted = false;
|
|
bool receivePollActive = false;
|
|
bool receiveReconnecting = false;
|
|
bool sendActive = false;
|
|
bool sendReconnecting = false;
|
|
bool stopStarted = false;
|
|
bool drainSends = false;
|
|
bool hardStopping = false;
|
|
bool stopFinished = false;
|
|
bool fatalStarted = false;
|
|
bool disconnectedNotified = false;
|
|
bool disconnectDelivering = false;
|
|
bool disconnectFinished = false;
|
|
Ptr<SocketHttpClientApi> sendApi;
|
|
Ptr<SocketHttpClientApi> receiveApi;
|
|
List<Ptr<SendItem>> sendQueue;
|
|
|
|
Ptr<Impl> RetainSelf()
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
return selfReference;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
vint CurrentCallbackDepth()
|
|
{
|
|
vint depth = 0;
|
|
for (auto frame = currentCallbackFrame; frame; frame = frame->previous)
|
|
{
|
|
if (frame->state.Obj() == this) depth++;
|
|
}
|
|
return depth;
|
|
}
|
|
|
|
vint CurrentWorkerDepth()
|
|
{
|
|
vint depth = 0;
|
|
for (auto frame = currentWorkerFrame; frame; frame = frame->previous)
|
|
{
|
|
if (frame->state.Obj() == this) depth++;
|
|
}
|
|
return depth;
|
|
}
|
|
|
|
vint CurrentWaitDepth()
|
|
{
|
|
vint depth = 0;
|
|
for (auto frame = currentWaitFrame; frame; frame = frame->previous)
|
|
{
|
|
if (frame->state.Obj() == this) depth++;
|
|
}
|
|
return depth;
|
|
}
|
|
|
|
template<typename TCallback>
|
|
void InvokeProtocolCallback(bool allowStopping, TCallback&& invoke)
|
|
{
|
|
INetworkProtocolCallback* installed = nullptr;
|
|
auto callbackDepth = CurrentCallbackDepth();
|
|
lockState.Enter();
|
|
while (callbackInstalling && callbackDepth == 0 && callback)
|
|
{
|
|
cvState.SleepWith(lockState);
|
|
}
|
|
if (callback && (allowStopping || !stopStarted))
|
|
{
|
|
installed = callback;
|
|
activeCallbacks++;
|
|
}
|
|
lockState.Leave();
|
|
|
|
if (installed)
|
|
{
|
|
CallbackFrame frame(RetainSelf());
|
|
invoke(installed);
|
|
}
|
|
}
|
|
|
|
bool IsStopped()
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
return stopStarted;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CanReceiveUnsafe()
|
|
{
|
|
return state == State::Connected && readingStarted && !stopStarted && !hardStopping;
|
|
}
|
|
|
|
bool CanSendUnsafe()
|
|
{
|
|
return !hardStopping && (state == State::Connected || drainSends);
|
|
}
|
|
|
|
void StopApiNoThrow(Ptr<SocketHttpClientApi> api)
|
|
{
|
|
if (!api) return;
|
|
try
|
|
{
|
|
api->Stop();
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
}
|
|
|
|
Ptr<SocketHttpClientApi> CreateApi()
|
|
{
|
|
Ptr<IAsyncSocketClient> nativeClient;
|
|
bool sameEndpointClient = false;
|
|
try
|
|
{
|
|
CS_LOCK(lockClientCreation)
|
|
{
|
|
nativeClient = initialClient;
|
|
initialClient = nullptr;
|
|
if (!nativeClient)
|
|
{
|
|
sameEndpointClient = true;
|
|
nativeClient = clientSource->CreateSameEndpointClient();
|
|
}
|
|
}
|
|
if (!nativeClient)
|
|
{
|
|
throw SameEndpointClientContractException(L"IAsyncSocketClient::CreateSameEndpointClient returned null.");
|
|
}
|
|
if (sameEndpointClient && nativeClient.Obj() == clientSource.Obj())
|
|
{
|
|
throw SameEndpointClientContractException(L"IAsyncSocketClient::CreateSameEndpointClient returned the source client instead of a fresh client.");
|
|
}
|
|
if (nativeClient->GetPort() != port)
|
|
{
|
|
throw SameEndpointClientContractException(L"IAsyncSocketClient::CreateSameEndpointClient returned a client for a different port.");
|
|
}
|
|
if (nativeClient->GetStatus() != ClientStatus::Ready)
|
|
{
|
|
throw SameEndpointClientContractException(L"IAsyncSocketClient::CreateSameEndpointClient returned a client that is not ready.");
|
|
}
|
|
return Ptr(new SocketHttpClientApi(nativeClient, server));
|
|
}
|
|
catch (const SameEndpointClientContractException&)
|
|
{
|
|
throw;
|
|
}
|
|
catch (const Exception& exception)
|
|
{
|
|
throw SameEndpointClientContractException(L"IAsyncSocketClient::CreateSameEndpointClient failed: " + exception.Message());
|
|
}
|
|
catch (...)
|
|
{
|
|
throw SameEndpointClientContractException(L"IAsyncSocketClient::CreateSameEndpointClient failed.");
|
|
}
|
|
}
|
|
|
|
bool WaitApiForServer(Ptr<SocketHttpClientApi> api)
|
|
{
|
|
api->WaitForServer();
|
|
return api->GetStatus() == ClientStatus::Connected;
|
|
}
|
|
|
|
void ReportLocalError(const WString& error, bool fatal)
|
|
{
|
|
InvokeProtocolCallback(fatal, [&](INetworkProtocolCallback* installed)
|
|
{
|
|
installed->OnLocalError(error, fatal);
|
|
});
|
|
}
|
|
|
|
void NotifyDisconnected()
|
|
{
|
|
auto callbackDepth = CurrentCallbackDepth();
|
|
lockState.Enter();
|
|
if (!disconnectedNotified)
|
|
{
|
|
disconnectedNotified = true;
|
|
cvState.WakeAllPendings();
|
|
}
|
|
if (disconnectFinished)
|
|
{
|
|
lockState.Leave();
|
|
return;
|
|
}
|
|
if (disconnectDelivering)
|
|
{
|
|
if (callbackDepth == 0)
|
|
{
|
|
while (!disconnectFinished) cvState.SleepWith(lockState);
|
|
}
|
|
lockState.Leave();
|
|
return;
|
|
}
|
|
if (callbackDepth == 0)
|
|
{
|
|
while (activeCallbacks > 0 && !disconnectDelivering && !disconnectFinished)
|
|
{
|
|
cvState.SleepWith(lockState);
|
|
}
|
|
if (disconnectFinished)
|
|
{
|
|
lockState.Leave();
|
|
return;
|
|
}
|
|
}
|
|
disconnectDelivering = true;
|
|
while (activeCallbacks > callbackDepth) cvState.SleepWith(lockState);
|
|
lockState.Leave();
|
|
|
|
try
|
|
{
|
|
InvokeProtocolCallback(true, [](INetworkProtocolCallback* installed)
|
|
{
|
|
installed->OnDisconnected();
|
|
});
|
|
}
|
|
catch (...)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
callback = nullptr;
|
|
disconnectFinished = true;
|
|
cvState.WakeAllPendings();
|
|
}
|
|
throw;
|
|
}
|
|
|
|
CS_LOCK(lockState)
|
|
{
|
|
callback = nullptr;
|
|
disconnectFinished = true;
|
|
cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
|
|
void ReportFatalError(const WString& error)
|
|
{
|
|
INetworkProtocolCallback* installed = nullptr;
|
|
auto callbackDepth = CurrentCallbackDepth();
|
|
bool report = false;
|
|
lockState.Enter();
|
|
while (callbackInstalling && callbackDepth == 0 && callback && !stopStarted)
|
|
{
|
|
cvState.SleepWith(lockState);
|
|
}
|
|
if (!fatalStarted && !stopStarted)
|
|
{
|
|
fatalStarted = true;
|
|
report = true;
|
|
if (callback)
|
|
{
|
|
installed = callback;
|
|
activeCallbacks++;
|
|
}
|
|
}
|
|
lockState.Leave();
|
|
if (!report) return;
|
|
InvokeFatalReservedForTesting();
|
|
|
|
try
|
|
{
|
|
if (installed)
|
|
{
|
|
CallbackFrame frame(RetainSelf());
|
|
installed->OnLocalError(error, true);
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
Stop(true);
|
|
throw;
|
|
}
|
|
Stop(true);
|
|
}
|
|
|
|
bool HandleConnectFailure(WString error, vint& attempt)
|
|
{
|
|
if (IsStopped()) return false;
|
|
if (attempt >= HttpRequestMaxAttempts)
|
|
{
|
|
ReportFatalError(error);
|
|
return false;
|
|
}
|
|
ReportLocalError(error, false);
|
|
attempt++;
|
|
return !IsStopped();
|
|
}
|
|
|
|
Ptr<QueryResult> QueryConnect(Ptr<SocketHttpClientApi> api)
|
|
{
|
|
auto request = CreateHttpNetworkProtocolConnectRequest(urlConnect);
|
|
|
|
auto waiter = Ptr(new QueryWaiter);
|
|
api->HttpQuery(request, [waiter](QueryResult result)
|
|
{
|
|
waiter->Complete(std::move(result));
|
|
});
|
|
return waiter->Wait();
|
|
}
|
|
|
|
bool ValidateConnectResponse(
|
|
const windows_http::HttpResponse& response,
|
|
WString& requestUrl,
|
|
WString& responseUrl,
|
|
WString& error
|
|
)
|
|
{
|
|
WString body;
|
|
if (!DecodeSuccessfulResponse(response, L"/Connect", body, error)) return false;
|
|
if (body.Length() == 0)
|
|
{
|
|
error = L"/Connect returned an empty body.";
|
|
return false;
|
|
}
|
|
|
|
WString requestPath;
|
|
WString responsePath;
|
|
if (!ParseHttpNetworkProtocolConnectBody(body, requestPath, responsePath))
|
|
{
|
|
error = L"/Connect did not return exactly two paths.";
|
|
return false;
|
|
}
|
|
if (!ValidateHttpNetworkProtocolEndpointPath(requestPath) || !ValidateHttpNetworkProtocolEndpointPath(responsePath))
|
|
{
|
|
error = L"/Connect returned an illegal path.";
|
|
return false;
|
|
}
|
|
|
|
auto requestTarget = urlPrefix + requestPath;
|
|
auto responseTarget = urlPrefix + responsePath;
|
|
if (
|
|
ValidateHttpRequestLine(L"POST", requestTarget) != HttpRequestLineValidationResult::Succeeded ||
|
|
ValidateHttpRequestLine(L"POST", responseTarget) != HttpRequestLineValidationResult::Succeeded
|
|
)
|
|
{
|
|
error = L"/Connect returned a path exceeding the HTTP request-target contract.";
|
|
return false;
|
|
}
|
|
requestUrl = std::move(requestTarget);
|
|
responseUrl = std::move(responseTarget);
|
|
return true;
|
|
}
|
|
|
|
bool PublishApi(Ptr<SocketHttpClientApi> api, bool receive)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (stopStarted || hardStopping) return false;
|
|
if (receive)
|
|
{
|
|
receiveApi = api;
|
|
}
|
|
else
|
|
{
|
|
sendApi = api;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void ClearApi(Ptr<SocketHttpClientApi> api, bool receive)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (receive)
|
|
{
|
|
if (receiveApi == api) receiveApi = nullptr;
|
|
}
|
|
else
|
|
{
|
|
if (sendApi == api) sendApi = nullptr;
|
|
}
|
|
cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
|
|
void SubmitReceivePoll(Ptr<SocketHttpClientApi> api)
|
|
{
|
|
auto self = RetainSelf();
|
|
if (!self) return;
|
|
auto request = CreateHttpNetworkProtocolReceiveRequest(urlRequest);
|
|
request.receiveTimeout = 0;
|
|
|
|
try
|
|
{
|
|
api->HttpQuery(request, [self, api](QueryResult result)
|
|
{
|
|
self->OnReceiveCompleted(api, std::move(result));
|
|
});
|
|
InvokeReceiveSubmittedForTesting();
|
|
}
|
|
catch (...)
|
|
{
|
|
HandleReceiveTransportFailure(api);
|
|
}
|
|
}
|
|
|
|
bool ReserveReceivePoll(Ptr<SocketHttpClientApi> api)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!CanReceiveUnsafe() || receiveReconnecting || receivePollActive || receiveApi != api) return false;
|
|
receivePollActive = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void HandleReceiveTransportFailure(Ptr<SocketHttpClientApi> api)
|
|
{
|
|
bool schedule = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (receiveApi == api && CanReceiveUnsafe() && !receiveReconnecting)
|
|
{
|
|
receivePollActive = false;
|
|
receiveReconnecting = true;
|
|
activeWorkers++;
|
|
schedule = true;
|
|
}
|
|
}
|
|
if (!schedule) return;
|
|
|
|
auto self = RetainSelf();
|
|
bool queued = false;
|
|
try
|
|
{
|
|
auto worker = Func<void()>([self, api]()
|
|
{
|
|
WorkerFrame frame(self);
|
|
try
|
|
{
|
|
self->RunReceiveReplacement(api);
|
|
}
|
|
catch (...)
|
|
{
|
|
try
|
|
{
|
|
self->ReportFatalError(L"/Request replacement worker failed unexpectedly.");
|
|
}
|
|
catch (...)
|
|
{
|
|
self->Stop();
|
|
}
|
|
}
|
|
});
|
|
if (self)
|
|
{
|
|
try
|
|
{
|
|
queued = ThreadPoolLite::Queue(worker);
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
if (!queued)
|
|
{
|
|
try
|
|
{
|
|
queued = Thread::CreateAndStart(worker) != nullptr;
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
if (!queued)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
activeWorkers--;
|
|
receiveReconnecting = false;
|
|
cvState.WakeAllPendings();
|
|
}
|
|
ReportFatalError(L"/Request could not queue its replacement worker.");
|
|
}
|
|
}
|
|
|
|
void RunReceiveReplacement(Ptr<SocketHttpClientApi> deadApi)
|
|
{
|
|
StopApiNoThrow(deadApi);
|
|
ClearApi(deadApi, true);
|
|
|
|
while (true)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!CanReceiveUnsafe() || !receiveReconnecting) return;
|
|
}
|
|
|
|
Ptr<SocketHttpClientApi> api;
|
|
try
|
|
{
|
|
api = CreateApi();
|
|
}
|
|
catch (const SameEndpointClientContractException& exception)
|
|
{
|
|
ReportFatalError(exception.Message());
|
|
return;
|
|
}
|
|
catch (...)
|
|
{
|
|
ReportFatalError(L"IAsyncSocketClient::CreateSameEndpointClient failed.");
|
|
return;
|
|
}
|
|
if (!PublishApi(api, true))
|
|
{
|
|
StopApiNoThrow(api);
|
|
return;
|
|
}
|
|
|
|
bool connected = false;
|
|
try
|
|
{
|
|
connected = WaitApiForServer(api);
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
if (!connected)
|
|
{
|
|
StopApiNoThrow(api);
|
|
ClearApi(api, true);
|
|
continue;
|
|
}
|
|
|
|
bool submit = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (receiveApi == api && CanReceiveUnsafe() && receiveReconnecting)
|
|
{
|
|
receiveReconnecting = false;
|
|
receivePollActive = true;
|
|
submit = true;
|
|
}
|
|
}
|
|
if (!submit)
|
|
{
|
|
StopApiNoThrow(api);
|
|
return;
|
|
}
|
|
SubmitReceivePoll(api);
|
|
return;
|
|
}
|
|
}
|
|
|
|
void OnReceiveCompleted(Ptr<SocketHttpClientApi> api, QueryResult result)
|
|
{
|
|
bool current = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (receiveApi == api && receivePollActive)
|
|
{
|
|
receivePollActive = false;
|
|
current = true;
|
|
}
|
|
}
|
|
if (!current) return;
|
|
|
|
if (auto httpError = result.TryGet<windows_http::HttpError>())
|
|
{
|
|
if (IsResponseNotFoundError(*httpError))
|
|
{
|
|
ReportFatalError(DescribeHttpError(L"/Request", *httpError));
|
|
return;
|
|
}
|
|
HandleReceiveTransportFailure(api);
|
|
return;
|
|
}
|
|
|
|
WString body;
|
|
WString error;
|
|
auto valid = DecodeSuccessfulResponse(result.Get<windows_http::HttpResponse>(), L"/Request", body, error);
|
|
if (ReserveReceivePoll(api))
|
|
{
|
|
// SocketHttpClientApi starts this replacement from inside its response
|
|
// callback before any user callback below can create a receive gap.
|
|
SubmitReceivePoll(api);
|
|
}
|
|
if (valid && body.Length() > 0)
|
|
{
|
|
InvokeProtocolCallback(false, [&](INetworkProtocolCallback* installed)
|
|
{
|
|
installed->OnReadString(body);
|
|
});
|
|
}
|
|
}
|
|
|
|
bool IsCurrentSendUnsafe(Ptr<SocketHttpClientApi> api, Ptr<SendItem> item)
|
|
{
|
|
return sendQueue.Count() > 0 && sendQueue[0] == item && sendApi == api;
|
|
}
|
|
|
|
void SubmitSend(Ptr<SocketHttpClientApi> api, Ptr<SendItem> item)
|
|
{
|
|
bool submit = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
submit = CanSendUnsafe() && sendActive && IsCurrentSendUnsafe(api, item);
|
|
}
|
|
if (!submit) return;
|
|
|
|
auto request = CreateHttpNetworkProtocolSendRequest(urlResponse, item->body);
|
|
|
|
auto self = RetainSelf();
|
|
try
|
|
{
|
|
api->HttpQuery(request, [self, api, item](QueryResult result)
|
|
{
|
|
self->OnSendCompleted(api, item, std::move(result));
|
|
});
|
|
}
|
|
catch (...)
|
|
{
|
|
HandleSendFailure(api, item, L"/Response could not submit the HTTP exchange.", true);
|
|
}
|
|
}
|
|
|
|
void QueueSendReplacement(Ptr<SocketHttpClientApi> deadApi, Ptr<SendItem> item)
|
|
{
|
|
bool schedule = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (CanSendUnsafe() && sendReconnecting && IsCurrentSendUnsafe(deadApi, item))
|
|
{
|
|
activeWorkers++;
|
|
schedule = true;
|
|
}
|
|
else
|
|
{
|
|
sendReconnecting = false;
|
|
}
|
|
}
|
|
if (!schedule) return;
|
|
|
|
auto self = RetainSelf();
|
|
bool queued = false;
|
|
try
|
|
{
|
|
auto worker = Func<void()>([self, deadApi, item]()
|
|
{
|
|
WorkerFrame frame(self);
|
|
try
|
|
{
|
|
self->RunSendReplacement(deadApi, item);
|
|
}
|
|
catch (...)
|
|
{
|
|
try
|
|
{
|
|
self->ReportFatalError(L"/Response replacement worker failed unexpectedly.");
|
|
}
|
|
catch (...)
|
|
{
|
|
self->Stop();
|
|
}
|
|
}
|
|
});
|
|
if (self)
|
|
{
|
|
try
|
|
{
|
|
queued = ThreadPoolLite::Queue(worker);
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
if (!queued)
|
|
{
|
|
try
|
|
{
|
|
queued = Thread::CreateAndStart(worker) != nullptr;
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
if (!queued)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
activeWorkers--;
|
|
sendReconnecting = false;
|
|
cvState.WakeAllPendings();
|
|
}
|
|
ReportFatalError(L"/Response could not queue its replacement worker.");
|
|
}
|
|
}
|
|
|
|
bool HandleSendPreparationFailure(Ptr<SendItem> item, const WString& error)
|
|
{
|
|
bool fatal = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!CanSendUnsafe() || !sendReconnecting || sendQueue.Count() == 0 || sendQueue[0] != item) return false;
|
|
fatal = item->attempt >= HttpRequestMaxAttempts;
|
|
if (!fatal) item->attempt++;
|
|
}
|
|
if (fatal)
|
|
{
|
|
ReportFatalError(error);
|
|
return false;
|
|
}
|
|
ReportLocalError(error, false);
|
|
CS_LOCK(lockState)
|
|
{
|
|
return CanSendUnsafe() && sendReconnecting && sendQueue.Count() > 0 && sendQueue[0] == item;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void RunSendReplacement(Ptr<SocketHttpClientApi> deadApi, Ptr<SendItem> item)
|
|
{
|
|
StopApiNoThrow(deadApi);
|
|
ClearApi(deadApi, false);
|
|
|
|
while (true)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!CanSendUnsafe() || !sendReconnecting || sendQueue.Count() == 0 || sendQueue[0] != item) return;
|
|
}
|
|
|
|
Ptr<SocketHttpClientApi> api;
|
|
try
|
|
{
|
|
api = CreateApi();
|
|
}
|
|
catch (const SameEndpointClientContractException& exception)
|
|
{
|
|
ReportFatalError(exception.Message());
|
|
return;
|
|
}
|
|
catch (...)
|
|
{
|
|
ReportFatalError(L"IAsyncSocketClient::CreateSameEndpointClient failed.");
|
|
return;
|
|
}
|
|
if (!PublishApi(api, false))
|
|
{
|
|
StopApiNoThrow(api);
|
|
return;
|
|
}
|
|
|
|
bool connected = false;
|
|
try
|
|
{
|
|
connected = WaitApiForServer(api);
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
if (!connected)
|
|
{
|
|
StopApiNoThrow(api);
|
|
ClearApi(api, false);
|
|
if (!HandleSendPreparationFailure(item, L"/Response replacement failed to connect.")) return;
|
|
continue;
|
|
}
|
|
|
|
bool submit = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (sendApi == api && CanSendUnsafe() && sendReconnecting && sendQueue.Count() > 0 && sendQueue[0] == item)
|
|
{
|
|
sendReconnecting = false;
|
|
sendActive = true;
|
|
submit = true;
|
|
}
|
|
}
|
|
if (!submit)
|
|
{
|
|
StopApiNoThrow(api);
|
|
return;
|
|
}
|
|
SubmitSend(api, item);
|
|
return;
|
|
}
|
|
}
|
|
|
|
void HandleSendFailure(Ptr<SocketHttpClientApi> api, Ptr<SendItem> item, WString error, bool transportFailure)
|
|
{
|
|
bool fatal = false;
|
|
bool retryHealthy = false;
|
|
bool replace = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!sendActive || !CanSendUnsafe() || !IsCurrentSendUnsafe(api, item)) return;
|
|
sendActive = false;
|
|
fatal = item->attempt >= HttpRequestMaxAttempts;
|
|
if (!fatal)
|
|
{
|
|
item->attempt++;
|
|
if (transportFailure)
|
|
{
|
|
sendReconnecting = true;
|
|
replace = true;
|
|
}
|
|
else
|
|
{
|
|
sendActive = true;
|
|
retryHealthy = true;
|
|
}
|
|
}
|
|
cvState.WakeAllPendings();
|
|
}
|
|
|
|
if (fatal)
|
|
{
|
|
ReportFatalError(error);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
ReportLocalError(error, false);
|
|
}
|
|
catch (...)
|
|
{
|
|
if (replace) QueueSendReplacement(api, item);
|
|
if (retryHealthy) SubmitSend(api, item);
|
|
throw;
|
|
}
|
|
if (replace) QueueSendReplacement(api, item);
|
|
if (retryHealthy) SubmitSend(api, item);
|
|
}
|
|
|
|
void OnSendCompleted(Ptr<SocketHttpClientApi> api, Ptr<SendItem> item, QueryResult result)
|
|
{
|
|
if (auto httpError = result.TryGet<windows_http::HttpError>())
|
|
{
|
|
if (IsResponseNotFoundError(*httpError))
|
|
{
|
|
ReportFatalError(DescribeHttpError(L"/Response", *httpError));
|
|
return;
|
|
}
|
|
HandleSendFailure(api, item, DescribeHttpError(L"/Response", *httpError), true);
|
|
return;
|
|
}
|
|
|
|
WString body;
|
|
WString error;
|
|
if (!DecodeSuccessfulResponse(result.Get<windows_http::HttpResponse>(), L"/Response", body, error))
|
|
{
|
|
HandleSendFailure(api, item, error, false);
|
|
return;
|
|
}
|
|
|
|
Ptr<SendItem> next;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!sendActive || !IsCurrentSendUnsafe(api, item)) return;
|
|
sendActive = false;
|
|
sendQueue.RemoveAt(0);
|
|
if (CanSendUnsafe() && sendQueue.Count() > 0)
|
|
{
|
|
next = sendQueue[0];
|
|
sendActive = true;
|
|
}
|
|
cvState.WakeAllPendings();
|
|
}
|
|
|
|
// Preserve FIFO ownership by submitting the next accepted send before
|
|
// delivering a piggybacked message to callback-reentrant application code.
|
|
if (next) SubmitSend(api, next);
|
|
if (body.Length() > 0)
|
|
{
|
|
InvokeProtocolCallback(false, [&](INetworkProtocolCallback* installed)
|
|
{
|
|
installed->OnReadString(body);
|
|
});
|
|
}
|
|
}
|
|
|
|
public:
|
|
Impl(
|
|
SocketHttpClient* _owner,
|
|
Ptr<IAsyncSocketClient> _initialClient,
|
|
const WString& _server,
|
|
const WString& _urlPrefix
|
|
)
|
|
: owner(_owner)
|
|
, server(_server)
|
|
, clientSource(_initialClient)
|
|
, initialClient(_initialClient)
|
|
, urlPrefix(NormalizeUrlPrefix(_urlPrefix))
|
|
, urlConnect(urlPrefix + HttpServerUrl_Connect)
|
|
{
|
|
CHECK_ERROR(owner, L"SocketHttpClient requires an owning adapter.");
|
|
CHECK_ERROR(initialClient, L"SocketHttpClient requires an initial native client.");
|
|
CHECK_ERROR(ValidateLoopbackServer(server), L"SocketHttpClient requires an explicit loopback server.");
|
|
port = initialClient->GetPort();
|
|
CHECK_ERROR(1 <= port && port <= 65535, L"SocketHttpClient requires the initial client port to be in 1..65535.");
|
|
CHECK_ERROR(initialClient->GetStatus() == ClientStatus::Ready, L"SocketHttpClient requires an initial client in the ready state.");
|
|
CHECK_ERROR(ValidateHttpNetworkProtocolBaseUrl(urlPrefix), L"SocketHttpClient requires an empty or legal origin-form URL prefix.");
|
|
CHECK_ERROR(ValidateHttpRequestLine(L"GET", urlConnect) == HttpRequestLineValidationResult::Succeeded, L"SocketHttpClient URL prefix makes /Connect exceed the HTTP request-line limit.");
|
|
}
|
|
|
|
void Initialize(Ptr<Impl> self)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
selfReference = self;
|
|
}
|
|
}
|
|
|
|
void ReleaseSelf()
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
owner = nullptr;
|
|
selfReference = nullptr;
|
|
}
|
|
}
|
|
|
|
INetworkProtocolConnection* GetConnection()
|
|
{
|
|
return owner;
|
|
}
|
|
|
|
void WaitForServer()
|
|
{
|
|
auto self = RetainSelf();
|
|
CS_LOCK(lockState)
|
|
{
|
|
CHECK_ERROR(state == State::Ready && !stopStarted, L"SocketHttpClient::WaitForServer can only be called once.");
|
|
state = State::WaitingForServer;
|
|
}
|
|
WaitFrame waitFrame(self);
|
|
|
|
vint attempt = 1;
|
|
Ptr<SocketHttpClientApi> api;
|
|
while (!IsStopped())
|
|
{
|
|
if (!api)
|
|
{
|
|
try
|
|
{
|
|
api = CreateApi();
|
|
}
|
|
catch (const SameEndpointClientContractException& exception)
|
|
{
|
|
ReportFatalError(exception.Message());
|
|
return;
|
|
}
|
|
catch (...)
|
|
{
|
|
ReportFatalError(L"IAsyncSocketClient::CreateSameEndpointClient failed.");
|
|
return;
|
|
}
|
|
if (!PublishApi(api, false))
|
|
{
|
|
StopApiNoThrow(api);
|
|
return;
|
|
}
|
|
bool connected = false;
|
|
try
|
|
{
|
|
connected = WaitApiForServer(api);
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
if (!connected)
|
|
{
|
|
StopApiNoThrow(api);
|
|
ClearApi(api, false);
|
|
api = nullptr;
|
|
if (!HandleConnectFailure(L"/Connect native connection failed.", attempt)) return;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
Ptr<QueryResult> result;
|
|
try
|
|
{
|
|
result = QueryConnect(api);
|
|
}
|
|
catch (...)
|
|
{
|
|
StopApiNoThrow(api);
|
|
ClearApi(api, false);
|
|
api = nullptr;
|
|
if (!HandleConnectFailure(L"/Connect could not submit its HTTP exchange.", attempt)) return;
|
|
continue;
|
|
}
|
|
if (IsStopped()) return;
|
|
|
|
if (auto httpError = result->TryGet<windows_http::HttpError>())
|
|
{
|
|
auto error = DescribeHttpError(L"/Connect", *httpError);
|
|
if (IsResponseNotFoundError(*httpError))
|
|
{
|
|
ReportFatalError(error);
|
|
return;
|
|
}
|
|
StopApiNoThrow(api);
|
|
ClearApi(api, false);
|
|
api = nullptr;
|
|
if (!HandleConnectFailure(error, attempt)) return;
|
|
continue;
|
|
}
|
|
|
|
WString requestUrl;
|
|
WString responseUrl;
|
|
WString error;
|
|
if (!ValidateConnectResponse(result->Get<windows_http::HttpResponse>(), requestUrl, responseUrl, error))
|
|
{
|
|
if (!HandleConnectFailure(error, attempt)) return;
|
|
continue;
|
|
}
|
|
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (stopStarted) return;
|
|
urlRequest = requestUrl;
|
|
urlResponse = responseUrl;
|
|
}
|
|
break;
|
|
}
|
|
if (IsStopped()) return;
|
|
|
|
// The logical token is already fixed. Failures in this second physical
|
|
// bootstrap are silent and never repeat /Connect.
|
|
while (!IsStopped())
|
|
{
|
|
Ptr<SocketHttpClientApi> apiReceive;
|
|
try
|
|
{
|
|
apiReceive = CreateApi();
|
|
}
|
|
catch (const SameEndpointClientContractException& exception)
|
|
{
|
|
ReportFatalError(exception.Message());
|
|
return;
|
|
}
|
|
catch (...)
|
|
{
|
|
ReportFatalError(L"IAsyncSocketClient::CreateSameEndpointClient failed.");
|
|
return;
|
|
}
|
|
if (!PublishApi(apiReceive, true))
|
|
{
|
|
StopApiNoThrow(apiReceive);
|
|
return;
|
|
}
|
|
bool connected = false;
|
|
try
|
|
{
|
|
connected = WaitApiForServer(apiReceive);
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
if (!connected)
|
|
{
|
|
StopApiNoThrow(apiReceive);
|
|
ClearApi(apiReceive, true);
|
|
continue;
|
|
}
|
|
|
|
bool publishConnected = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!stopStarted && receiveApi == apiReceive)
|
|
{
|
|
state = State::Connected;
|
|
publishConnected = true;
|
|
}
|
|
}
|
|
if (!publishConnected)
|
|
{
|
|
StopApiNoThrow(apiReceive);
|
|
return;
|
|
}
|
|
InvokeProtocolCallback(false, [](INetworkProtocolCallback* installed)
|
|
{
|
|
installed->OnConnected();
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
ClientStatus GetStatus()
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
switch (state)
|
|
{
|
|
case State::Ready:
|
|
return ClientStatus::Ready;
|
|
case State::WaitingForServer:
|
|
return ClientStatus::WaitingForServer;
|
|
case State::Connected:
|
|
return ClientStatus::Connected;
|
|
default:
|
|
return ClientStatus::Disconnected;
|
|
}
|
|
}
|
|
return ClientStatus::Disconnected;
|
|
}
|
|
|
|
void InstallCallback(INetworkProtocolCallback* value)
|
|
{
|
|
if (!value)
|
|
{
|
|
auto callbackDepth = CurrentCallbackDepth();
|
|
bool uninstallOwner = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
uninstallOwner = callback != nullptr;
|
|
callback = nullptr;
|
|
while ((callbackDepth == 0 || uninstallOwner) && activeCallbacks > callbackDepth)
|
|
{
|
|
cvState.SleepWith(lockState);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
bool canInstall = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!callback && !callbackInstalling && !stopStarted)
|
|
{
|
|
callback = value;
|
|
callbackInstalling = true;
|
|
activeCallbacks++;
|
|
canInstall = true;
|
|
}
|
|
}
|
|
CHECK_ERROR(canInstall, L"SocketHttpClient::InstallCallback cannot replace a callback or install one on a stopped client.");
|
|
|
|
CallbackFrame frame(RetainSelf());
|
|
try
|
|
{
|
|
value->OnInstalled(owner);
|
|
}
|
|
catch (...)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (callback == value) callback = nullptr;
|
|
callbackInstalling = false;
|
|
cvState.WakeAllPendings();
|
|
}
|
|
throw;
|
|
}
|
|
CS_LOCK(lockState)
|
|
{
|
|
callbackInstalling = false;
|
|
cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
|
|
void BeginReadingLoopUnsafe()
|
|
{
|
|
Ptr<SocketHttpClientApi> api;
|
|
CS_LOCK(lockState)
|
|
{
|
|
CHECK_ERROR(state == State::Connected && !stopStarted, L"SocketHttpClient::BeginReadingLoopUnsafe requires a connected client.");
|
|
CHECK_ERROR(!readingStarted, L"SocketHttpClient::BeginReadingLoopUnsafe can only be called once.");
|
|
readingStarted = true;
|
|
api = receiveApi;
|
|
CHECK_ERROR(api, L"SocketHttpClient has no receive API after connecting.");
|
|
receivePollActive = true;
|
|
}
|
|
SubmitReceivePoll(api);
|
|
}
|
|
|
|
void SendString(const WString& str)
|
|
{
|
|
CHECK_ERROR(str.Length() > 0, L"SocketHttpClient::SendString does not accept an empty string.");
|
|
CHECK_ERROR(IsValidHttpNetworkProtocolMessage(str), L"SocketHttpClient::SendString requires valid Unicode without embedded NUL characters.");
|
|
Array<vuint8_t> validated;
|
|
CHECK_ERROR(EncodeStrictUtf8(str, validated), L"SocketHttpClient::SendString requires valid Unicode without embedded NUL characters.");
|
|
CHECK_ERROR(validated.Count() <= HttpBodySizeLimit, L"SocketHttpClient::SendString exceeds the HTTP body size limit.");
|
|
|
|
auto item = Ptr(new SendItem);
|
|
item->body.Resize(validated.Count());
|
|
for (vint i = 0; i < validated.Count(); i++)
|
|
{
|
|
item->body[i] = (char)validated[i];
|
|
}
|
|
Ptr<SocketHttpClientApi> api;
|
|
bool submit = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
CHECK_ERROR(state == State::Connected && !stopStarted, L"SocketHttpClient::SendString requires a connected client.");
|
|
sendQueue.Add(item);
|
|
if (!sendActive && !sendReconnecting)
|
|
{
|
|
api = sendApi;
|
|
CHECK_ERROR(api, L"SocketHttpClient has no send API after connecting.");
|
|
sendActive = true;
|
|
submit = true;
|
|
}
|
|
}
|
|
if (submit) SubmitSend(api, item);
|
|
}
|
|
|
|
void Stop(bool internalFollower = false)
|
|
{
|
|
auto self = RetainSelf();
|
|
auto callbackDepth = CurrentCallbackDepth();
|
|
auto workerDepth = CurrentWorkerDepth();
|
|
auto waitDepth = CurrentWaitDepth();
|
|
auto nested = internalFollower || callbackDepth > 0 || workerDepth > 0 || waitDepth > 0;
|
|
bool executeStop = false;
|
|
Ptr<SocketHttpClientApi> cancellingReceive;
|
|
|
|
lockState.Enter();
|
|
if (stopFinished)
|
|
{
|
|
while (activeCallbacks > callbackDepth || activeWorkers > workerDepth || activeWaits > waitDepth)
|
|
{
|
|
cvState.SleepWith(lockState);
|
|
}
|
|
lockState.Leave();
|
|
return;
|
|
}
|
|
if (!stopStarted)
|
|
{
|
|
stopStarted = true;
|
|
state = State::Stopping;
|
|
drainSends = !fatalStarted && sendQueue.Count() > 0;
|
|
cancellingReceive = receiveApi;
|
|
executeStop = true;
|
|
}
|
|
else if (nested)
|
|
{
|
|
lockState.Leave();
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
while (!stopFinished) cvState.SleepWith(lockState);
|
|
while (activeCallbacks > 0 || activeWorkers > 0 || activeWaits > 0) cvState.SleepWith(lockState);
|
|
lockState.Leave();
|
|
return;
|
|
}
|
|
lockState.Leave();
|
|
|
|
if (!executeStop) return;
|
|
InvokeStopStartedForTesting();
|
|
// The infinite receive exchange is always cancelled before the bounded
|
|
// opportunity given to already accepted send-lane messages.
|
|
StopApiNoThrow(cancellingReceive);
|
|
|
|
lockState.Enter();
|
|
if (drainSends)
|
|
{
|
|
auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(SendDrainTimeout);
|
|
while (sendQueue.Count() > 0)
|
|
{
|
|
auto now = std::chrono::steady_clock::now();
|
|
if (now >= deadline) break;
|
|
auto remaining = std::chrono::ceil<std::chrono::milliseconds>(deadline - now).count();
|
|
cvState.SleepWithForTime(lockState, (vint)remaining);
|
|
}
|
|
}
|
|
drainSends = false;
|
|
hardStopping = true;
|
|
sendQueue.Clear();
|
|
sendActive = false;
|
|
receivePollActive = false;
|
|
auto stoppingSend = sendApi;
|
|
auto stoppingReceive = receiveApi;
|
|
lockState.Leave();
|
|
|
|
StopApiNoThrow(stoppingReceive);
|
|
StopApiNoThrow(stoppingSend);
|
|
|
|
lockState.Enter();
|
|
while (activeWorkers > workerDepth || activeWaits > waitDepth)
|
|
{
|
|
cvState.SleepWith(lockState);
|
|
}
|
|
lockState.Leave();
|
|
|
|
try
|
|
{
|
|
NotifyDisconnected();
|
|
}
|
|
catch (...)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
while (activeCallbacks > callbackDepth) cvState.SleepWith(lockState);
|
|
stopFinished = true;
|
|
cvState.WakeAllPendings();
|
|
}
|
|
throw;
|
|
}
|
|
CS_LOCK(lockState)
|
|
{
|
|
while (activeCallbacks > callbackDepth) cvState.SleepWith(lockState);
|
|
stopFinished = true;
|
|
cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
};
|
|
|
|
thread_local SocketHttpClient::Impl::CallbackFrame* SocketHttpClient::Impl::currentCallbackFrame = nullptr;
|
|
thread_local SocketHttpClient::Impl::WorkerFrame* SocketHttpClient::Impl::currentWorkerFrame = nullptr;
|
|
thread_local SocketHttpClient::Impl::WaitFrame* SocketHttpClient::Impl::currentWaitFrame = nullptr;
|
|
|
|
/***********************************************************************
|
|
SocketHttpClient
|
|
***********************************************************************/
|
|
|
|
SocketHttpClient::SocketHttpClient(
|
|
Ptr<IAsyncSocketClient> client,
|
|
const WString& server,
|
|
const WString& urlPrefix
|
|
)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::SocketHttpClient::SocketHttpClient(Ptr<IAsyncSocketClient>, const WString&, const WString&)#"
|
|
CHECK_ERROR(client, ERROR_MESSAGE_PREFIX L"An initial native client is required.");
|
|
auto created = Ptr(new Impl(this, client, server, urlPrefix));
|
|
created->Initialize(created);
|
|
impl = created;
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
SocketHttpClient::~SocketHttpClient()
|
|
{
|
|
try
|
|
{
|
|
impl->Stop();
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
impl->ReleaseSelf();
|
|
}
|
|
|
|
INetworkProtocolConnection* SocketHttpClient::GetConnection()
|
|
{
|
|
return impl->GetConnection();
|
|
}
|
|
|
|
void SocketHttpClient::WaitForServer()
|
|
{
|
|
impl->WaitForServer();
|
|
}
|
|
|
|
ClientStatus SocketHttpClient::GetStatus()
|
|
{
|
|
return impl->GetStatus();
|
|
}
|
|
|
|
void SocketHttpClient::InstallCallback(INetworkProtocolCallback* callback)
|
|
{
|
|
impl->InstallCallback(callback);
|
|
}
|
|
|
|
void SocketHttpClient::BeginReadingLoopUnsafe()
|
|
{
|
|
impl->BeginReadingLoopUnsafe();
|
|
}
|
|
|
|
void SocketHttpClient::SendString(const WString& str)
|
|
{
|
|
impl->SendString(str);
|
|
}
|
|
|
|
void SocketHttpClient::Stop()
|
|
{
|
|
impl->Stop();
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPCLIENTAPI.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Vczh Library++ 3.0
|
|
Developer: Zihan Chen(vczh)
|
|
|
|
Interfaces:
|
|
SocketHttpClientApi
|
|
|
|
***********************************************************************/
|
|
|
|
|
|
namespace vl::inter_process::async_tcp_socket
|
|
{
|
|
using namespace vl::collections;
|
|
|
|
namespace
|
|
{
|
|
wchar_t FoldAscii(wchar_t c)
|
|
{
|
|
return L'A' <= c && c <= L'Z' ? c - L'A' + L'a' : c;
|
|
}
|
|
|
|
bool AsciiEqualsIgnoreCase(const WString& a, const WString& b)
|
|
{
|
|
if (a.Length() != b.Length()) return false;
|
|
for (vint i = 0; i < a.Length(); i++)
|
|
{
|
|
if (FoldAscii(a[i]) != FoldAscii(b[i])) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
WString FoldAsciiFieldName(const WString& name)
|
|
{
|
|
Array<wchar_t> characters(name.Length());
|
|
for (vint i = 0; i < name.Length(); i++)
|
|
{
|
|
characters[i] = FoldAscii(name[i]);
|
|
}
|
|
return characters.Count() == 0 ? WString() : WString::CopyFrom(&characters[0], characters.Count());
|
|
}
|
|
|
|
WString TrimHttpWhitespace(const WString& value)
|
|
{
|
|
vint begin = 0;
|
|
vint end = value.Length();
|
|
while (begin < end && (value[begin] == L' ' || value[begin] == L'\t')) begin++;
|
|
while (begin < end && (value[end - 1] == L' ' || value[end - 1] == L'\t')) end--;
|
|
return value.Sub(begin, end - begin);
|
|
}
|
|
|
|
bool ContainsOnlyIdentityCoding(const WString& value)
|
|
{
|
|
vint begin = 0;
|
|
bool found = false;
|
|
while (begin <= value.Length())
|
|
{
|
|
vint end = begin;
|
|
while (end < value.Length() && value[end] != L',') end++;
|
|
auto coding = TrimHttpWhitespace(value.Sub(begin, end - begin));
|
|
if (!AsciiEqualsIgnoreCase(coding, L"identity")) return false;
|
|
found = true;
|
|
if (end == value.Length()) break;
|
|
begin = end + 1;
|
|
}
|
|
return found;
|
|
}
|
|
|
|
bool ValidateServer(const WString& server)
|
|
{
|
|
return
|
|
AsciiEqualsIgnoreCase(server, L"localhost") ||
|
|
server == L"127.0.0.1";
|
|
}
|
|
|
|
HttpField CreateField(const WString& name, const WString& value)
|
|
{
|
|
HttpField field;
|
|
field.name = FoldAsciiFieldName(name);
|
|
auto utf8 = wtou8(value);
|
|
field.value.Resize(utf8.Length());
|
|
if (utf8.Length() > 0)
|
|
{
|
|
memcpy(&field.value[0], utf8.Buffer(), utf8.Length());
|
|
}
|
|
return field;
|
|
}
|
|
|
|
WString DecodeFieldValue(const Array<vuint8_t>& value)
|
|
{
|
|
if (value.Count() == 0) return WString::Empty;
|
|
Array<char8_t> utf8(value.Count());
|
|
for (vint i = 0; i < value.Count(); i++)
|
|
{
|
|
utf8[i] = (char8_t)value[i];
|
|
}
|
|
return u8tow(U8String::CopyFrom(&utf8[0], utf8.Count()));
|
|
}
|
|
|
|
windows_http::HttpError MakeError(const WString& operation, const WString& message, SocketHttpClientErrorCode code)
|
|
{
|
|
windows_http::HttpError error;
|
|
error.operation = operation;
|
|
error.errorCode = (vuint32_t)code;
|
|
error.message = message;
|
|
return error;
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
SocketHttpClientApi::Impl
|
|
***********************************************************************/
|
|
|
|
class SocketHttpClientApi::Impl : public Object, public virtual IHttpRequestCallback
|
|
{
|
|
using QueryResult = Variant<windows_http::HttpResponse, windows_http::HttpError>;
|
|
using QueryCallback = Func<void(QueryResult)>;
|
|
|
|
class Query : public Object
|
|
{
|
|
public:
|
|
Ptr<HttpRequest> request;
|
|
vint responseTimeout = 0;
|
|
QueryCallback callback;
|
|
bool completed = false;
|
|
};
|
|
|
|
struct CallbackFrame
|
|
{
|
|
Impl* owner = nullptr;
|
|
Ptr<Impl> self;
|
|
CallbackFrame* previous = nullptr;
|
|
|
|
CallbackFrame(Impl* _owner, Ptr<Impl> _self)
|
|
: owner(_owner)
|
|
, self(_self)
|
|
, previous(currentCallbackFrame)
|
|
{
|
|
currentCallbackFrame = this;
|
|
}
|
|
|
|
~CallbackFrame()
|
|
{
|
|
currentCallbackFrame = previous;
|
|
CS_LOCK(owner->lockState)
|
|
{
|
|
owner->activeCallbacks--;
|
|
owner->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
};
|
|
|
|
struct ResponseFrame
|
|
{
|
|
Impl* owner = nullptr;
|
|
Ptr<Impl> self;
|
|
ResponseFrame* previous = nullptr;
|
|
|
|
ResponseFrame(Impl* _owner, Ptr<Impl> _self)
|
|
: owner(_owner)
|
|
, self(_self)
|
|
, previous(currentResponseFrame)
|
|
{
|
|
currentResponseFrame = this;
|
|
}
|
|
|
|
~ResponseFrame()
|
|
{
|
|
currentResponseFrame = previous;
|
|
}
|
|
};
|
|
|
|
static thread_local CallbackFrame* currentCallbackFrame;
|
|
static thread_local ResponseFrame* currentResponseFrame;
|
|
|
|
Ptr<HttpRequestClient> client;
|
|
IHttpRequestConnection* connection = nullptr;
|
|
WString authority;
|
|
Ptr<Impl> selfReference;
|
|
|
|
CriticalSection lockState;
|
|
ConditionVariable cvState;
|
|
Ptr<Query> activeQuery;
|
|
List<Ptr<Query>> queuedQueries;
|
|
List<Ptr<Query>> pendingQueries;
|
|
vint activeCallbacks = 0;
|
|
bool waitStarted = false;
|
|
bool readingStarted = false;
|
|
bool responseDispatching = false;
|
|
bool terminal = false;
|
|
bool stopStarted = false;
|
|
bool stopFinished = false;
|
|
|
|
Ptr<Impl> RetainSelf()
|
|
{
|
|
Ptr<Impl> self;
|
|
CS_LOCK(lockState)
|
|
{
|
|
self = selfReference;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
vint CurrentCallbackDepth()
|
|
{
|
|
vint depth = 0;
|
|
for (auto frame = currentCallbackFrame; frame; frame = frame->previous)
|
|
{
|
|
if (frame->owner == this) depth++;
|
|
}
|
|
return depth;
|
|
}
|
|
|
|
bool IsInsideResponseCallback()
|
|
{
|
|
for (auto frame = currentResponseFrame; frame; frame = frame->previous)
|
|
{
|
|
if (frame->owner == this) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Ptr<Query> TakeFirstQueuedUnsafe()
|
|
{
|
|
auto query = queuedQueries[0];
|
|
queuedQueries.RemoveAt(0);
|
|
return query;
|
|
}
|
|
|
|
void TakeAllQueriesUnsafe(List<Ptr<Query>>& queries)
|
|
{
|
|
if (activeQuery)
|
|
{
|
|
queries.Add(activeQuery);
|
|
activeQuery = nullptr;
|
|
}
|
|
for (auto query : queuedQueries)
|
|
{
|
|
queries.Add(query);
|
|
}
|
|
queuedQueries.Clear();
|
|
for (auto query : pendingQueries)
|
|
{
|
|
queries.Add(query);
|
|
}
|
|
pendingQueries.Clear();
|
|
}
|
|
|
|
void MoveAllQueriesToPendingUnsafe()
|
|
{
|
|
if (activeQuery)
|
|
{
|
|
pendingQueries.Add(activeQuery);
|
|
activeQuery = nullptr;
|
|
}
|
|
for (auto query : queuedQueries)
|
|
{
|
|
pendingQueries.Add(query);
|
|
}
|
|
queuedQueries.Clear();
|
|
}
|
|
|
|
bool ReserveCallbackUnsafe(Ptr<Query> query, QueryCallback& callback)
|
|
{
|
|
if (!query || query->completed) return false;
|
|
query->completed = true;
|
|
callback = query->callback;
|
|
if (callback) activeCallbacks++;
|
|
return true;
|
|
}
|
|
|
|
void InvokeReserved(QueryCallback callback, QueryResult result)
|
|
{
|
|
if (!callback) return;
|
|
CallbackFrame frame(this, RetainSelf());
|
|
try
|
|
{
|
|
callback(std::move(result));
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
}
|
|
|
|
void CompleteWithError(Ptr<Query> query, const windows_http::HttpError& error)
|
|
{
|
|
QueryCallback callback;
|
|
bool reserved = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
reserved = ReserveCallbackUnsafe(query, callback);
|
|
}
|
|
if (reserved)
|
|
{
|
|
InvokeReserved(callback, QueryResult(error));
|
|
}
|
|
}
|
|
|
|
void CompleteAllWithError(List<Ptr<Query>>& queries, const windows_http::HttpError& error)
|
|
{
|
|
for (auto query : queries)
|
|
{
|
|
CompleteWithError(query, error);
|
|
}
|
|
}
|
|
|
|
void CompletePendingWithError(const windows_http::HttpError& error)
|
|
{
|
|
while (true)
|
|
{
|
|
QueryCallback callback;
|
|
bool reserved = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!stopStarted && pendingQueries.Count() > 0)
|
|
{
|
|
auto query = pendingQueries[0];
|
|
pendingQueries.RemoveAt(0);
|
|
reserved = ReserveCallbackUnsafe(query, callback);
|
|
}
|
|
}
|
|
if (!reserved) return;
|
|
InvokeReserved(callback, QueryResult(error));
|
|
}
|
|
}
|
|
|
|
void InvokeDirect(QueryCallback callback, const windows_http::HttpError& error)
|
|
{
|
|
if (!callback) return;
|
|
CS_LOCK(lockState)
|
|
{
|
|
activeCallbacks++;
|
|
}
|
|
InvokeReserved(callback, QueryResult(error));
|
|
}
|
|
|
|
Ptr<Query> CreateQuery(const windows_http::HttpRequest& request, windows_http::HttpError& error)
|
|
{
|
|
if (request.secure)
|
|
{
|
|
error = MakeError(L"SocketHttpClientApi::HttpQuery", L"TLS is not supported by SocketHttpClientApi.", SocketHttpClientErrorCode::InvalidRequest);
|
|
return nullptr;
|
|
}
|
|
if (request.username != WString::Empty || request.password != WString::Empty)
|
|
{
|
|
error = MakeError(L"SocketHttpClientApi::HttpQuery", L"Credentials are not supported by SocketHttpClientApi.", SocketHttpClientErrorCode::InvalidRequest);
|
|
return nullptr;
|
|
}
|
|
if (request.keepAliveOnStop)
|
|
{
|
|
error = MakeError(L"SocketHttpClientApi::HttpQuery", L"keepAliveOnStop is not supported by SocketHttpClientApi.", SocketHttpClientErrorCode::InvalidRequest);
|
|
return nullptr;
|
|
}
|
|
|
|
auto query = Ptr(new Query);
|
|
query->request = Ptr(new HttpRequest);
|
|
query->responseTimeout = request.receiveTimeout;
|
|
query->callback = {};
|
|
query->request->method = request.method == WString::Empty ? WString::Unmanaged(L"GET") : request.method;
|
|
query->request->requestTarget = request.query == WString::Empty ? WString::Unmanaged(L"/") : request.query;
|
|
|
|
query->request->headers.Add(CreateAsciiHttpField(L"Host", authority));
|
|
query->request->headers.Add(CreateAsciiHttpField(L"Accept-Encoding", L"identity"));
|
|
for (vint i = 0; i < request.acceptTypes.Count(); i++)
|
|
{
|
|
query->request->headers.Add(CreateField(L"Accept", request.acceptTypes.Get(i)));
|
|
}
|
|
if (request.contentType != WString::Empty)
|
|
{
|
|
query->request->headers.Add(CreateField(L"Content-Type", request.contentType));
|
|
}
|
|
if (request.cookie != WString::Empty)
|
|
{
|
|
query->request->headers.Add(CreateField(L"Cookie", request.cookie));
|
|
}
|
|
|
|
for (vint i = 0; i < request.extraHeaders.Count(); i++)
|
|
{
|
|
auto name = request.extraHeaders.Keys()[i];
|
|
auto value = request.extraHeaders.Values()[i];
|
|
if (AsciiEqualsIgnoreCase(name, L"Host"))
|
|
{
|
|
if (!AsciiEqualsIgnoreCase(TrimHttpWhitespace(value), authority))
|
|
{
|
|
error = MakeError(L"SocketHttpClientApi::HttpQuery", L"A caller-supplied Host field conflicts with the configured server and injected client port.", SocketHttpClientErrorCode::InvalidRequest);
|
|
return nullptr;
|
|
}
|
|
continue;
|
|
}
|
|
if (AsciiEqualsIgnoreCase(name, L"Accept-Encoding"))
|
|
{
|
|
if (!ContainsOnlyIdentityCoding(value))
|
|
{
|
|
error = MakeError(L"SocketHttpClientApi::HttpQuery", L"SocketHttpClientApi only supports Accept-Encoding: identity.", SocketHttpClientErrorCode::InvalidRequest);
|
|
return nullptr;
|
|
}
|
|
continue;
|
|
}
|
|
query->request->headers.Add(CreateField(name, value));
|
|
}
|
|
|
|
if (request.body.Count() > 0)
|
|
{
|
|
HttpBodyChunk chunk;
|
|
chunk.data.Resize(request.body.Count());
|
|
memcpy(&chunk.data[0], &request.body.Get(0), request.body.Count());
|
|
query->request->body.chunks.Add(std::move(chunk));
|
|
}
|
|
return query;
|
|
}
|
|
|
|
bool ConvertResponse(Ptr<HttpResponse> response, windows_http::HttpResponse& output, windows_http::HttpError& error)
|
|
{
|
|
if (!response)
|
|
{
|
|
error = MakeError(L"SocketHttpClientApi::OnReadResponse", L"The HTTP request layer returned an empty response.", SocketHttpClientErrorCode::Transport);
|
|
return false;
|
|
}
|
|
|
|
output.statusCode = response->statusCode;
|
|
bool contentTypeAssigned = false;
|
|
bool cookieAssigned = false;
|
|
auto processField = [&](const HttpField& field)
|
|
{
|
|
if (AsciiEqualsIgnoreCase(field.name, L"Content-Encoding"))
|
|
{
|
|
if (!ContainsOnlyIdentityCoding(DecodeFieldValue(field.value)))
|
|
{
|
|
error = MakeError(L"SocketHttpClientApi::OnReadResponse", L"The server returned an unsupported Content-Encoding.", SocketHttpClientErrorCode::UnsupportedCoding);
|
|
return false;
|
|
}
|
|
}
|
|
else if (!contentTypeAssigned && AsciiEqualsIgnoreCase(field.name, L"Content-Type"))
|
|
{
|
|
output.contentType = DecodeFieldValue(field.value);
|
|
contentTypeAssigned = true;
|
|
}
|
|
else if (!cookieAssigned && AsciiEqualsIgnoreCase(field.name, L"Set-Cookie"))
|
|
{
|
|
output.cookie = DecodeFieldValue(field.value);
|
|
cookieAssigned = true;
|
|
}
|
|
return true;
|
|
};
|
|
for (auto&& field : response->headers)
|
|
{
|
|
if (!processField(field)) return false;
|
|
}
|
|
for (auto&& field : response->body.trailers)
|
|
{
|
|
if (!processField(field)) return false;
|
|
}
|
|
|
|
Array<vuint8_t> body;
|
|
if (!FlattenHttpBody(response->body, body))
|
|
{
|
|
error = MakeError(L"SocketHttpClientApi::OnReadResponse", L"The response body is too large to flatten.", SocketHttpClientErrorCode::Transport);
|
|
return false;
|
|
}
|
|
output.body.Resize(body.Count());
|
|
for (vint i = 0; i < body.Count(); i++)
|
|
{
|
|
output.body[i] = (char)body[i];
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool TrySend(Ptr<Query> query, windows_http::HttpError& error)
|
|
{
|
|
try
|
|
{
|
|
connection->SendRequest(query->request, query->responseTimeout);
|
|
return true;
|
|
}
|
|
catch (...)
|
|
{
|
|
error = MakeError(L"SocketHttpClientApi::HttpQuery", L"The HTTP request layer rejected the exchange.", SocketHttpClientErrorCode::Transport);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void StopConnectionNoThrow()
|
|
{
|
|
try
|
|
{
|
|
connection->Stop();
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
}
|
|
|
|
void HandleSendFailure(Ptr<Query> query, const windows_http::HttpError& error)
|
|
{
|
|
bool stopConnection = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!query->completed && !stopStarted && !terminal)
|
|
{
|
|
terminal = true;
|
|
responseDispatching = false;
|
|
MoveAllQueriesToPendingUnsafe();
|
|
stopConnection = true;
|
|
}
|
|
}
|
|
if (stopConnection)
|
|
{
|
|
StopConnectionNoThrow();
|
|
CompletePendingWithError(error);
|
|
}
|
|
}
|
|
|
|
void HandleTerminalError(const windows_http::HttpError& error, bool stopConnectionImmediately)
|
|
{
|
|
bool shouldStop = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!stopStarted && !terminal)
|
|
{
|
|
terminal = true;
|
|
responseDispatching = false;
|
|
MoveAllQueriesToPendingUnsafe();
|
|
shouldStop = stopConnectionImmediately;
|
|
}
|
|
}
|
|
if (shouldStop)
|
|
{
|
|
StopConnectionNoThrow();
|
|
}
|
|
CompletePendingWithError(error);
|
|
}
|
|
|
|
public:
|
|
Impl(Ptr<IAsyncSocketClient> socketClient, const WString& _authority)
|
|
: client(new HttpRequestClient(socketClient))
|
|
, authority(_authority)
|
|
{
|
|
connection = client->GetConnection();
|
|
}
|
|
|
|
void Initialize(Ptr<Impl> self)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
selfReference = self;
|
|
}
|
|
try
|
|
{
|
|
connection->InstallCallback(this);
|
|
}
|
|
catch (...)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
selfReference = nullptr;
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
void ReleaseSelf()
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
selfReference = nullptr;
|
|
}
|
|
}
|
|
|
|
void WaitForServer()
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
CHECK_ERROR(!waitStarted && !stopStarted && !terminal, L"SocketHttpClientApi::WaitForServer can only be called once on an active client.");
|
|
waitStarted = true;
|
|
}
|
|
|
|
try
|
|
{
|
|
client->WaitForServer();
|
|
}
|
|
catch (...)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
terminal = true;
|
|
}
|
|
throw;
|
|
}
|
|
|
|
bool beginReading = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
beginReading = !stopStarted && !terminal;
|
|
}
|
|
if (!beginReading) return;
|
|
|
|
try
|
|
{
|
|
connection->BeginReadingLoopUnsafe();
|
|
}
|
|
catch (...)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
terminal = true;
|
|
}
|
|
throw;
|
|
}
|
|
CS_LOCK(lockState)
|
|
{
|
|
readingStarted = !stopStarted && !terminal;
|
|
}
|
|
}
|
|
|
|
ClientStatus GetStatus()
|
|
{
|
|
return client->GetStatus();
|
|
}
|
|
|
|
void HttpQuery(const windows_http::HttpRequest& request, QueryCallback callback)
|
|
{
|
|
auto self = RetainSelf();
|
|
windows_http::HttpError error;
|
|
Ptr<Query> query;
|
|
try
|
|
{
|
|
query = CreateQuery(request, error);
|
|
}
|
|
catch (...)
|
|
{
|
|
error = MakeError(L"SocketHttpClientApi::HttpQuery", L"The request could not be translated to the socket HTTP representation.", SocketHttpClientErrorCode::InvalidRequest);
|
|
}
|
|
if (!query)
|
|
{
|
|
InvokeDirect(callback, error);
|
|
return;
|
|
}
|
|
query->callback = callback;
|
|
|
|
bool start = false;
|
|
bool reject = false;
|
|
bool notReady = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (stopStarted || terminal)
|
|
{
|
|
reject = true;
|
|
}
|
|
else if (!readingStarted)
|
|
{
|
|
notReady = true;
|
|
}
|
|
else if (!activeQuery && queuedQueries.Count() == 0 && (!responseDispatching || IsInsideResponseCallback()))
|
|
{
|
|
activeQuery = query;
|
|
start = true;
|
|
}
|
|
else
|
|
{
|
|
queuedQueries.Add(query);
|
|
}
|
|
}
|
|
|
|
if (reject)
|
|
{
|
|
CompleteWithError(query, MakeError(L"SocketHttpClientApi::HttpQuery", L"SocketHttpClientApi has stopped accepting work.", SocketHttpClientErrorCode::Stopped));
|
|
}
|
|
else if (notReady)
|
|
{
|
|
CompleteWithError(query, MakeError(L"SocketHttpClientApi::HttpQuery", L"WaitForServer must complete before sending an HTTP query.", SocketHttpClientErrorCode::InvalidRequest));
|
|
}
|
|
else if (start)
|
|
{
|
|
if (!TrySend(query, error))
|
|
{
|
|
HandleSendFailure(query, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Stop()
|
|
{
|
|
auto self = RetainSelf();
|
|
List<Ptr<Query>> cancelledQueries;
|
|
auto callbackDepth = CurrentCallbackDepth();
|
|
bool executeStop = false;
|
|
lockState.Enter();
|
|
if (stopFinished)
|
|
{
|
|
while (activeCallbacks > callbackDepth) cvState.SleepWith(lockState);
|
|
lockState.Leave();
|
|
return;
|
|
}
|
|
if (!stopStarted)
|
|
{
|
|
stopStarted = true;
|
|
terminal = true;
|
|
responseDispatching = false;
|
|
TakeAllQueriesUnsafe(cancelledQueries);
|
|
executeStop = true;
|
|
}
|
|
else if (callbackDepth > 0)
|
|
{
|
|
lockState.Leave();
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
while (!stopFinished) cvState.SleepWith(lockState);
|
|
while (activeCallbacks > 0) cvState.SleepWith(lockState);
|
|
lockState.Leave();
|
|
return;
|
|
}
|
|
lockState.Leave();
|
|
|
|
if (executeStop)
|
|
{
|
|
StopConnectionNoThrow();
|
|
CompleteAllWithError(
|
|
cancelledQueries,
|
|
MakeError(L"SocketHttpClientApi::Stop", L"The HTTP query was cancelled because the client stopped.", SocketHttpClientErrorCode::Stopped)
|
|
);
|
|
}
|
|
|
|
CS_LOCK(lockState)
|
|
{
|
|
while (activeCallbacks > callbackDepth) cvState.SleepWith(lockState);
|
|
stopFinished = true;
|
|
cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
|
|
void OnReadRequest(Ptr<HttpRequest>) override
|
|
{
|
|
auto self = RetainSelf();
|
|
if (!self) return;
|
|
HandleTerminalError(
|
|
MakeError(L"SocketHttpClientApi::OnReadRequest", L"The client connection received a request instead of a response.", SocketHttpClientErrorCode::Transport),
|
|
true
|
|
);
|
|
}
|
|
|
|
void OnReadRequestFailure(HttpRequestFailure) override
|
|
{
|
|
auto self = RetainSelf();
|
|
if (!self) return;
|
|
HandleTerminalError(
|
|
MakeError(L"SocketHttpClientApi::OnReadRequestFailure", L"The client connection reported a request parsing failure.", SocketHttpClientErrorCode::Transport),
|
|
true
|
|
);
|
|
}
|
|
|
|
void OnReadResponse(Ptr<HttpResponse> response) override
|
|
{
|
|
auto self = RetainSelf();
|
|
if (!self) return;
|
|
ResponseFrame responseFrame(this, self);
|
|
|
|
windows_http::HttpResponse convertedResponse;
|
|
windows_http::HttpError responseError;
|
|
auto converted = ConvertResponse(response, convertedResponse, responseError);
|
|
Ptr<Query> completedQuery;
|
|
Ptr<Query> nextQuery;
|
|
QueryCallback completedCallback;
|
|
bool reserved = false;
|
|
bool stopForResponse = false;
|
|
windows_http::HttpError terminalError;
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (activeQuery && !activeQuery->completed)
|
|
{
|
|
responseDispatching = true;
|
|
completedQuery = activeQuery;
|
|
activeQuery = nullptr;
|
|
reserved = ReserveCallbackUnsafe(completedQuery, completedCallback);
|
|
if (!converted)
|
|
{
|
|
terminal = true;
|
|
MoveAllQueriesToPendingUnsafe();
|
|
stopForResponse = true;
|
|
terminalError = responseError;
|
|
}
|
|
else if (!stopStarted && !terminal && queuedQueries.Count() > 0)
|
|
{
|
|
nextQuery = TakeFirstQueuedUnsafe();
|
|
activeQuery = nextQuery;
|
|
}
|
|
}
|
|
}
|
|
if (!reserved) return;
|
|
|
|
windows_http::HttpError sendError;
|
|
bool sendFailed = nextQuery && !TrySend(nextQuery, sendError);
|
|
if (sendFailed)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!nextQuery->completed && !stopStarted && !terminal)
|
|
{
|
|
terminal = true;
|
|
MoveAllQueriesToPendingUnsafe();
|
|
stopForResponse = true;
|
|
terminalError = sendError;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (converted)
|
|
{
|
|
InvokeReserved(completedCallback, QueryResult(std::move(convertedResponse)));
|
|
}
|
|
else
|
|
{
|
|
InvokeReserved(completedCallback, QueryResult(responseError));
|
|
}
|
|
|
|
Ptr<Query> lateQuery;
|
|
if (!stopForResponse)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!stopStarted && !terminal && !activeQuery && queuedQueries.Count() > 0)
|
|
{
|
|
lateQuery = TakeFirstQueuedUnsafe();
|
|
activeQuery = lateQuery;
|
|
}
|
|
responseDispatching = false;
|
|
}
|
|
if (lateQuery && !TrySend(lateQuery, sendError))
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!lateQuery->completed && !stopStarted && !terminal)
|
|
{
|
|
terminal = true;
|
|
MoveAllQueriesToPendingUnsafe();
|
|
stopForResponse = true;
|
|
terminalError = sendError;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
responseDispatching = false;
|
|
}
|
|
}
|
|
|
|
if (stopForResponse)
|
|
{
|
|
StopConnectionNoThrow();
|
|
CompletePendingWithError(terminalError);
|
|
}
|
|
}
|
|
|
|
void OnReadResponseFailure(HttpResponseFailure failure) override
|
|
{
|
|
auto self = RetainSelf();
|
|
if (!self) return;
|
|
#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::SocketHttpClientApi::Impl::OnReadResponseFailure(HttpResponseFailure)#"
|
|
CHECK_ERROR(failure == HttpResponseFailure::NotFound, ERROR_MESSAGE_PREFIX L"Received an unsupported response failure.");
|
|
HandleTerminalError(
|
|
MakeError(L"SocketHttpClientApi::OnReadResponseFailure", L"The server returned 404 Not Found.", SocketHttpClientErrorCode::ResponseNotFound),
|
|
false
|
|
);
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void OnWriteCompleted() override
|
|
{
|
|
}
|
|
|
|
void OnError(const WString& error, bool fatal) override
|
|
{
|
|
auto self = RetainSelf();
|
|
if (!self) return;
|
|
// The raw layer stops after delivering a fatal error. A nonfatal error
|
|
// needs an explicit stop after this wrapper terminalizes its queue.
|
|
auto stopConnectionImmediately = !fatal;
|
|
HandleTerminalError(
|
|
MakeError(L"SocketHttpClientApi::OnError", error, SocketHttpClientErrorCode::Transport),
|
|
stopConnectionImmediately
|
|
);
|
|
}
|
|
|
|
void OnConnected() override
|
|
{
|
|
}
|
|
|
|
void OnDisconnected() override
|
|
{
|
|
auto self = RetainSelf();
|
|
if (!self) return;
|
|
HandleTerminalError(
|
|
MakeError(L"SocketHttpClientApi::OnDisconnected", L"The HTTP connection was disconnected.", SocketHttpClientErrorCode::Transport),
|
|
false
|
|
);
|
|
}
|
|
|
|
void OnInstalled(IHttpRequestConnection* installedConnection) override
|
|
{
|
|
CHECK_ERROR(installedConnection == connection, L"SocketHttpClientApi was installed on an unexpected HTTP connection.");
|
|
}
|
|
};
|
|
|
|
thread_local SocketHttpClientApi::Impl::CallbackFrame* SocketHttpClientApi::Impl::currentCallbackFrame = nullptr;
|
|
thread_local SocketHttpClientApi::Impl::ResponseFrame* SocketHttpClientApi::Impl::currentResponseFrame = nullptr;
|
|
|
|
/***********************************************************************
|
|
SocketHttpClientApi
|
|
***********************************************************************/
|
|
|
|
SocketHttpClientApi::SocketHttpClientApi(Ptr<IAsyncSocketClient> client, const WString& server)
|
|
{
|
|
CHECK_ERROR(client, L"SocketHttpClientApi requires an asynchronous socket client.");
|
|
CHECK_ERROR(ValidateServer(server), L"SocketHttpClientApi requires an explicit loopback server.");
|
|
auto port = client->GetPort();
|
|
CHECK_ERROR(1 <= port && port <= 65535, L"SocketHttpClientApi requires the injected client port to be in 1..65535.");
|
|
auto authority = server + WString::Unmanaged(L":") + itow(port);
|
|
auto created = Ptr(new Impl(client, authority));
|
|
created->Initialize(created);
|
|
impl = created;
|
|
}
|
|
|
|
SocketHttpClientApi::~SocketHttpClientApi()
|
|
{
|
|
impl->Stop();
|
|
impl->ReleaseSelf();
|
|
}
|
|
|
|
void SocketHttpClientApi::WaitForServer()
|
|
{
|
|
impl->WaitForServer();
|
|
}
|
|
|
|
ClientStatus SocketHttpClientApi::GetStatus()
|
|
{
|
|
return impl->GetStatus();
|
|
}
|
|
|
|
void SocketHttpClientApi::HttpQuery(
|
|
const windows_http::HttpRequest& request,
|
|
Func<void(Variant<windows_http::HttpResponse, windows_http::HttpError>)> callback
|
|
)
|
|
{
|
|
impl->HttpQuery(request, callback);
|
|
}
|
|
|
|
void SocketHttpClientApi::Stop()
|
|
{
|
|
impl->Stop();
|
|
}
|
|
|
|
WString SocketHttpClientApi::UrlEncodeQuery(const WString& query)
|
|
{
|
|
return HttpUrlEncodeQuery(query);
|
|
}
|
|
|
|
WString SocketHttpClientApi::UrlDecodeQuery(const WString& query)
|
|
{
|
|
return HttpUrlDecodeQuery(query);
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPREQUEST.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Vczh Library++ 3.0
|
|
Developer: Zihan Chen(vczh)
|
|
|
|
Async Socket HTTP/1.1 Connection
|
|
|
|
***********************************************************************/
|
|
|
|
|
|
|
|
namespace vl::inter_process::async_tcp_socket
|
|
{
|
|
using namespace collections;
|
|
|
|
namespace
|
|
{
|
|
constexpr vint HttpWireMessageSizeLimit = 128 * 1024 * 1024;
|
|
constexpr vint HttpChunkCountLimit = 64 * 1024;
|
|
|
|
bool IsOws(vuint8_t c)
|
|
{
|
|
return c == ' ' || c == '\t';
|
|
}
|
|
|
|
bool IsDigit(vuint8_t c)
|
|
{
|
|
return c >= '0' && c <= '9';
|
|
}
|
|
|
|
bool IsHexDigit(vuint8_t c)
|
|
{
|
|
return IsDigit(c) || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
|
|
}
|
|
|
|
vuint8_t HexDigitValue(vuint8_t c)
|
|
{
|
|
if (IsDigit(c)) return c - '0';
|
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
return c - 'a' + 10;
|
|
}
|
|
|
|
bool IsTokenCharacter(vuint8_t c)
|
|
{
|
|
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))
|
|
{
|
|
return true;
|
|
}
|
|
switch (c)
|
|
{
|
|
case '!': case '#': case '$': case '%': case '&': case '\'': case '*': case '+':
|
|
case '-': case '.': case '^': case '_': case '`': case '|': case '~':
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool IsFieldValueCharacter(vuint8_t c)
|
|
{
|
|
return c == '\t' || (c >= 0x20 && c != 0x7F);
|
|
}
|
|
|
|
bool IsQuotedTextCharacter(vuint8_t c)
|
|
{
|
|
return c == '\t' || c == ' ' || c == '!' || (c >= '#' && c <= '[') || (c >= ']' && c != 0x7F);
|
|
}
|
|
|
|
vint FindCrlf(const vuint8_t* buffer, vint begin, vint end)
|
|
{
|
|
for (vint i = begin; i + 1 < end; i++)
|
|
{
|
|
if (buffer[i] == '\r' && buffer[i + 1] == '\n')
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
WString CopyAscii(const vuint8_t* buffer, vint begin, vint end, bool lowercase)
|
|
{
|
|
if (begin == end)
|
|
{
|
|
return L"";
|
|
}
|
|
Array<wchar_t> text(end - begin);
|
|
for (vint i = begin; i < end; i++)
|
|
{
|
|
auto c = buffer[i];
|
|
if (lowercase && c >= 'A' && c <= 'Z')
|
|
{
|
|
c += 'a' - 'A';
|
|
}
|
|
text[i - begin] = (wchar_t)c;
|
|
}
|
|
return WString::CopyFrom(&text[0], text.Count());
|
|
}
|
|
|
|
bool IsAsciiEqual(const WString& text, const wchar_t* expected)
|
|
{
|
|
return text == expected;
|
|
}
|
|
|
|
bool ParseQuotedString(const vuint8_t* buffer, vint& reading, vint end)
|
|
{
|
|
if (reading >= end || buffer[reading] != '"')
|
|
{
|
|
return false;
|
|
}
|
|
reading++;
|
|
while (reading < end)
|
|
{
|
|
auto c = buffer[reading++];
|
|
if (c == '"')
|
|
{
|
|
return true;
|
|
}
|
|
if (c == '\\')
|
|
{
|
|
if (reading >= end || !IsFieldValueCharacter(buffer[reading]))
|
|
{
|
|
return false;
|
|
}
|
|
reading++;
|
|
}
|
|
else if (!IsQuotedTextCharacter(c))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ParseToken(const vuint8_t* buffer, vint& reading, vint end, WString* output = nullptr)
|
|
{
|
|
vint begin = reading;
|
|
while (reading < end && IsTokenCharacter(buffer[reading]))
|
|
{
|
|
reading++;
|
|
}
|
|
if (reading == begin)
|
|
{
|
|
return false;
|
|
}
|
|
if (output)
|
|
{
|
|
*output = CopyAscii(buffer, begin, reading, true);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ParseSemicolonParameters(const vuint8_t* buffer, vint& reading, vint end)
|
|
{
|
|
while (true)
|
|
{
|
|
while (reading < end && IsOws(buffer[reading])) reading++;
|
|
if (reading == end)
|
|
{
|
|
return true;
|
|
}
|
|
if (buffer[reading++] != ';')
|
|
{
|
|
return false;
|
|
}
|
|
while (reading < end && IsOws(buffer[reading])) reading++;
|
|
if (!ParseToken(buffer, reading, end))
|
|
{
|
|
return false;
|
|
}
|
|
while (reading < end && IsOws(buffer[reading])) reading++;
|
|
if (reading < end && buffer[reading] == '=')
|
|
{
|
|
reading++;
|
|
while (reading < end && IsOws(buffer[reading])) reading++;
|
|
if (reading < end && buffer[reading] == '"')
|
|
{
|
|
if (!ParseQuotedString(buffer, reading, end)) return false;
|
|
}
|
|
else if (!ParseToken(buffer, reading, end))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool ValidateChunkSizeLinePrefix(const vuint8_t* buffer, vint begin, vint end)
|
|
{
|
|
vint reading = begin;
|
|
if (reading == end) return true;
|
|
if (!IsHexDigit(buffer[reading])) return false;
|
|
vuint64_t chunkSize = 0;
|
|
while (reading < end && IsHexDigit(buffer[reading]))
|
|
{
|
|
auto digit = (vuint64_t)HexDigitValue(buffer[reading++]);
|
|
if (chunkSize > ((std::numeric_limits<vuint64_t>::max)() - digit) / 16) return false;
|
|
chunkSize = chunkSize * 16 + digit;
|
|
if (chunkSize > (vuint64_t)HttpBodySizeLimit) return false;
|
|
}
|
|
while (true)
|
|
{
|
|
while (reading < end && IsOws(buffer[reading])) reading++;
|
|
if (reading == end) return true;
|
|
if (buffer[reading++] != ';') return false;
|
|
while (reading < end && IsOws(buffer[reading])) reading++;
|
|
if (reading == end) return true;
|
|
vint nameBegin = reading;
|
|
while (reading < end && IsTokenCharacter(buffer[reading])) reading++;
|
|
if (reading == nameBegin) return false;
|
|
while (reading < end && IsOws(buffer[reading])) reading++;
|
|
if (reading == end) return true;
|
|
if (buffer[reading] != '=') continue;
|
|
reading++;
|
|
while (reading < end && IsOws(buffer[reading])) reading++;
|
|
if (reading == end) return true;
|
|
if (buffer[reading] == '"')
|
|
{
|
|
reading++;
|
|
bool closed = false;
|
|
while (reading < end)
|
|
{
|
|
auto c = buffer[reading++];
|
|
if (c == '"')
|
|
{
|
|
closed = true;
|
|
break;
|
|
}
|
|
if (c == '\\')
|
|
{
|
|
if (reading == end) return true;
|
|
if (!IsFieldValueCharacter(buffer[reading++])) return false;
|
|
}
|
|
else if (!IsQuotedTextCharacter(c))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
if (!closed) return true;
|
|
}
|
|
else
|
|
{
|
|
vint valueBegin = reading;
|
|
while (reading < end && IsTokenCharacter(buffer[reading])) reading++;
|
|
if (reading == valueBegin) return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool ValidateCompleteChunkSizeLine(const vuint8_t* buffer, vint begin, vint end)
|
|
{
|
|
vint reading = begin;
|
|
vuint64_t chunkSize = 0;
|
|
while (reading < end && IsHexDigit(buffer[reading]))
|
|
{
|
|
auto digit = (vuint64_t)HexDigitValue(buffer[reading++]);
|
|
if (chunkSize > ((std::numeric_limits<vuint64_t>::max)() - digit) / 16) return false;
|
|
chunkSize = chunkSize * 16 + digit;
|
|
}
|
|
if (reading == begin || chunkSize > (vuint64_t)HttpBodySizeLimit) return false;
|
|
if (reading < end)
|
|
{
|
|
vint firstExtension = reading;
|
|
while (firstExtension < end && IsOws(buffer[firstExtension])) firstExtension++;
|
|
if (firstExtension == end || buffer[firstExtension] != ';') return false;
|
|
}
|
|
return ParseSemicolonParameters(buffer, reading, end);
|
|
}
|
|
|
|
bool ParseFieldLine(const vuint8_t* buffer, vint begin, vint end, HttpField& field)
|
|
{
|
|
vint colon = -1;
|
|
for (vint i = begin; i < end; i++)
|
|
{
|
|
if (buffer[i] == ':')
|
|
{
|
|
colon = i;
|
|
break;
|
|
}
|
|
if (!IsTokenCharacter(buffer[i]))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
if (colon == begin || colon == -1)
|
|
{
|
|
return false;
|
|
}
|
|
for (vint i = begin; i < colon; i++)
|
|
{
|
|
if (!IsTokenCharacter(buffer[i])) return false;
|
|
}
|
|
|
|
vint valueBegin = colon + 1;
|
|
vint valueEnd = end;
|
|
while (valueBegin < valueEnd && IsOws(buffer[valueBegin])) valueBegin++;
|
|
while (valueBegin < valueEnd && IsOws(buffer[valueEnd - 1])) valueEnd--;
|
|
for (vint i = valueBegin; i < valueEnd; i++)
|
|
{
|
|
if (!IsFieldValueCharacter(buffer[i])) return false;
|
|
}
|
|
|
|
field.name = CopyAscii(buffer, begin, colon, true);
|
|
field.value.Resize(valueEnd - valueBegin);
|
|
if (valueEnd > valueBegin)
|
|
{
|
|
std::memcpy(&field.value[0], buffer + valueBegin, (size_t)(valueEnd - valueBegin));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ParseUnsignedDecimal(const Array<vuint8_t>& value, vint begin, vint end, vuint64_t& number)
|
|
{
|
|
if (begin == end) return false;
|
|
number = 0;
|
|
for (vint i = begin; i < end; i++)
|
|
{
|
|
if (!IsDigit(value[i])) return false;
|
|
auto digit = (vuint64_t)(value[i] - '0');
|
|
if (number > ((std::numeric_limits<vuint64_t>::max)() - digit) / 10)
|
|
{
|
|
return false;
|
|
}
|
|
number = number * 10 + digit;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ParseContentLength(const Array<vuint8_t>& value, bool& initialized, vuint64_t& contentLength, vint& valueCount)
|
|
{
|
|
vint reading = 0;
|
|
while (true)
|
|
{
|
|
while (reading < value.Count() && IsOws(value[reading])) reading++;
|
|
vint begin = reading;
|
|
while (reading < value.Count() && IsDigit(value[reading])) reading++;
|
|
vint end = reading;
|
|
while (reading < value.Count() && IsOws(value[reading])) reading++;
|
|
vuint64_t number = 0;
|
|
if (!ParseUnsignedDecimal(value, begin, end, number))
|
|
{
|
|
return false;
|
|
}
|
|
valueCount++;
|
|
if (!initialized)
|
|
{
|
|
initialized = true;
|
|
contentLength = number;
|
|
}
|
|
else if (contentLength != number)
|
|
{
|
|
return false;
|
|
}
|
|
if (reading == value.Count()) return true;
|
|
if (value[reading++] != ',') return false;
|
|
if (reading == value.Count()) return false;
|
|
}
|
|
}
|
|
|
|
bool ParseTransferCodings(const Array<vuint8_t>& value, List<WString>& codings, bool& hasParameters)
|
|
{
|
|
vint reading = 0;
|
|
while (true)
|
|
{
|
|
while (reading < value.Count() && IsOws(value[reading])) reading++;
|
|
WString coding;
|
|
if (!ParseToken(value.Count() == 0 ? nullptr : &value[0], reading, value.Count(), &coding))
|
|
{
|
|
return false;
|
|
}
|
|
codings.Add(coding);
|
|
while (reading < value.Count() && IsOws(value[reading])) reading++;
|
|
while (reading < value.Count() && value[reading] == ';')
|
|
{
|
|
hasParameters = true;
|
|
reading++;
|
|
while (reading < value.Count() && IsOws(value[reading])) reading++;
|
|
if (!ParseToken(&value[0], reading, value.Count())) return false;
|
|
while (reading < value.Count() && IsOws(value[reading])) reading++;
|
|
if (reading >= value.Count() || value[reading] != '=')
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
reading++;
|
|
while (reading < value.Count() && IsOws(value[reading])) reading++;
|
|
if (reading < value.Count() && value[reading] == '"')
|
|
{
|
|
if (!ParseQuotedString(&value[0], reading, value.Count())) return false;
|
|
}
|
|
else if (!ParseToken(&value[0], reading, value.Count()))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
while (reading < value.Count() && IsOws(value[reading])) reading++;
|
|
}
|
|
if (reading == value.Count()) return true;
|
|
if (value[reading++] != ',' || reading == value.Count()) return false;
|
|
}
|
|
}
|
|
|
|
bool HasConnectionClose(const Array<vuint8_t>& value)
|
|
{
|
|
vint reading = 0;
|
|
while (reading < value.Count())
|
|
{
|
|
while (reading < value.Count() && IsOws(value[reading])) reading++;
|
|
vint begin = reading;
|
|
while (reading < value.Count() && value[reading] != ',') reading++;
|
|
vint end = reading;
|
|
while (begin < end && IsOws(value[begin])) begin++;
|
|
while (begin < end && IsOws(value[end - 1])) end--;
|
|
if (end - begin == 5)
|
|
{
|
|
const wchar_t* close = L"close";
|
|
bool matched = true;
|
|
for (vint i = 0; i < 5; i++)
|
|
{
|
|
auto c = value[begin + i];
|
|
if (c >= 'A' && c <= 'Z') c += 'a' - 'A';
|
|
if (c != close[i]) matched = false;
|
|
}
|
|
if (matched) return true;
|
|
}
|
|
if (reading < value.Count()) reading++;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ParseHttpVersion(const vuint8_t* buffer, vint begin, vint end, HttpVersion& version)
|
|
{
|
|
const wchar_t* prefix = L"HTTP/";
|
|
if (end - begin < 8) return false;
|
|
for (vint i = 0; i < 5; i++)
|
|
{
|
|
if (buffer[begin + i] != prefix[i]) return false;
|
|
}
|
|
vint reading = begin + 5;
|
|
vuint64_t major = 0;
|
|
vuint64_t minor = 0;
|
|
vint majorBegin = reading;
|
|
while (reading < end && IsDigit(buffer[reading]))
|
|
{
|
|
auto digit = (vuint64_t)(buffer[reading++] - '0');
|
|
if (major > ((std::numeric_limits<vuint64_t>::max)() - digit) / 10) return false;
|
|
major = major * 10 + digit;
|
|
}
|
|
if (reading == majorBegin || reading >= end || buffer[reading++] != '.') return false;
|
|
vint minorBegin = reading;
|
|
while (reading < end && IsDigit(buffer[reading]))
|
|
{
|
|
auto digit = (vuint64_t)(buffer[reading++] - '0');
|
|
if (minor > ((std::numeric_limits<vuint64_t>::max)() - digit) / 10) return false;
|
|
minor = minor * 10 + digit;
|
|
}
|
|
if (reading != end || reading == minorBegin || major > (vuint64_t)(std::numeric_limits<vint>::max)() || minor > (vuint64_t)(std::numeric_limits<vint>::max)())
|
|
{
|
|
return false;
|
|
}
|
|
version.major = (vint)major;
|
|
version.minor = (vint)minor;
|
|
return true;
|
|
}
|
|
|
|
bool ParseRequestLine(const vuint8_t* buffer, vint end, HttpVersion& version, WString& method, WString& target)
|
|
{
|
|
vint firstSpace = -1;
|
|
vint secondSpace = -1;
|
|
for (vint i = 0; i < end; i++)
|
|
{
|
|
if (buffer[i] == ' ')
|
|
{
|
|
if (firstSpace == -1) firstSpace = i;
|
|
else if (secondSpace == -1) secondSpace = i;
|
|
else return false;
|
|
}
|
|
}
|
|
if (firstSpace <= 0 || secondSpace <= firstSpace + 1 || secondSpace + 1 >= end) return false;
|
|
for (vint i = 0; i < firstSpace; i++) if (!IsTokenCharacter(buffer[i])) return false;
|
|
for (vint i = firstSpace + 1; i < secondSpace; i++)
|
|
{
|
|
if (buffer[i] < 0x21 || buffer[i] > 0x7E) return false;
|
|
}
|
|
method = CopyAscii(buffer, 0, firstSpace, false);
|
|
target = CopyAscii(buffer, firstSpace + 1, secondSpace, false);
|
|
return ParseHttpVersion(buffer, secondSpace + 1, end, version);
|
|
}
|
|
|
|
bool ParseStatusLine(const vuint8_t* buffer, vint end, HttpVersion& version, vint& statusCode, WString& reason)
|
|
{
|
|
vint firstSpace = -1;
|
|
for (vint i = 0; i < end; i++)
|
|
{
|
|
if (buffer[i] == ' ')
|
|
{
|
|
firstSpace = i;
|
|
break;
|
|
}
|
|
}
|
|
if (firstSpace == -1 || !ParseHttpVersion(buffer, 0, firstSpace, version)) return false;
|
|
if (firstSpace + 4 >= end || !IsDigit(buffer[firstSpace + 1]) || !IsDigit(buffer[firstSpace + 2]) || !IsDigit(buffer[firstSpace + 3])) return false;
|
|
if (buffer[firstSpace + 4] != ' ') return false;
|
|
statusCode = (buffer[firstSpace + 1] - '0') * 100 + (buffer[firstSpace + 2] - '0') * 10 + buffer[firstSpace + 3] - '0';
|
|
if (statusCode < 200 || statusCode > 599) return false;
|
|
vint reasonBegin = firstSpace + 5;
|
|
for (vint i = reasonBegin; i < end; i++)
|
|
{
|
|
if (buffer[i] < 0x20 || buffer[i] > 0x7E) return false;
|
|
}
|
|
reason = CopyAscii(buffer, reasonBegin, end, false);
|
|
return true;
|
|
}
|
|
|
|
enum class HttpBodyDetailedParsingResult
|
|
{
|
|
Succeeded,
|
|
Incomplete,
|
|
BadRequest,
|
|
PayloadTooLarge,
|
|
TrailerFieldsTooLarge,
|
|
};
|
|
|
|
HttpBodyDetailedParsingResult ParseHttpRequestBodyToChunksDetailed(
|
|
const vuint8_t* buffer,
|
|
vint availableBytes,
|
|
HttpBody& output,
|
|
vint& consumedBytes
|
|
);
|
|
|
|
enum class HttpMessageParsingResult
|
|
{
|
|
Succeeded,
|
|
Incomplete,
|
|
BadRequest,
|
|
PayloadTooLarge,
|
|
UriTooLong,
|
|
ExpectationFailed,
|
|
RequestHeaderFieldsTooLarge,
|
|
NotImplemented,
|
|
HttpVersionNotSupported,
|
|
};
|
|
|
|
HttpRequestFailure GetHttpRequestFailure(HttpMessageParsingResult result)
|
|
{
|
|
switch (result)
|
|
{
|
|
case HttpMessageParsingResult::PayloadTooLarge:
|
|
return HttpRequestFailure::PayloadTooLarge;
|
|
case HttpMessageParsingResult::UriTooLong:
|
|
return HttpRequestFailure::UriTooLong;
|
|
case HttpMessageParsingResult::ExpectationFailed:
|
|
return HttpRequestFailure::ExpectationFailed;
|
|
case HttpMessageParsingResult::RequestHeaderFieldsTooLarge:
|
|
return HttpRequestFailure::RequestHeaderFieldsTooLarge;
|
|
case HttpMessageParsingResult::NotImplemented:
|
|
return HttpRequestFailure::NotImplemented;
|
|
case HttpMessageParsingResult::HttpVersionNotSupported:
|
|
return HttpRequestFailure::HttpVersionNotSupported;
|
|
default:
|
|
return HttpRequestFailure::BadRequest;
|
|
}
|
|
}
|
|
|
|
bool HasHeader(const List<HttpField>& fields, const wchar_t* name)
|
|
{
|
|
for (auto&& field : fields)
|
|
{
|
|
if (IsAsciiEqual(field.name, name)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool TryGetHttpRequestLineSize(const WString& method, const WString& requestTarget, vint& size)
|
|
{
|
|
if (
|
|
method.Length() > HttpRequestLineSizeLimit - 10 ||
|
|
requestTarget.Length() > HttpRequestLineSizeLimit - 10 - method.Length()
|
|
)
|
|
{
|
|
return false;
|
|
}
|
|
size = 10 + method.Length() + requestTarget.Length();
|
|
return true;
|
|
}
|
|
|
|
HttpMessageParsingResult ParseHttpMessage(
|
|
const vuint8_t* buffer,
|
|
vint availableBytes,
|
|
bool requestMessage,
|
|
const WString& responseToMethod,
|
|
WString& parsedRequestMethod,
|
|
Ptr<HttpRequest>& request,
|
|
Ptr<HttpResponse>& response,
|
|
vint& consumedBytes,
|
|
bool& connectionClose
|
|
)
|
|
{
|
|
consumedBytes = 0;
|
|
connectionClose = false;
|
|
parsedRequestMethod = L"";
|
|
vint startLineEnd = FindCrlf(buffer, 0, availableBytes);
|
|
if (startLineEnd == -1)
|
|
{
|
|
auto possibleLineBytes = availableBytes > 0 && buffer[availableBytes - 1] == '\r' ? availableBytes - 1 : availableBytes;
|
|
return possibleLineBytes > HttpRequestLineSizeLimit
|
|
? (requestMessage ? HttpMessageParsingResult::UriTooLong : HttpMessageParsingResult::BadRequest)
|
|
: HttpMessageParsingResult::Incomplete;
|
|
}
|
|
if (startLineEnd > HttpRequestLineSizeLimit)
|
|
{
|
|
return requestMessage ? HttpMessageParsingResult::UriTooLong : HttpMessageParsingResult::BadRequest;
|
|
}
|
|
|
|
HttpVersion version;
|
|
WString method;
|
|
WString target;
|
|
vint statusCode = 0;
|
|
WString reason;
|
|
if (requestMessage)
|
|
{
|
|
if (!ParseRequestLine(buffer, startLineEnd, version, method, target)) return HttpMessageParsingResult::BadRequest;
|
|
parsedRequestMethod = method;
|
|
}
|
|
else
|
|
{
|
|
if (!ParseStatusLine(buffer, startLineEnd, version, statusCode, reason)) return HttpMessageParsingResult::BadRequest;
|
|
}
|
|
if (version.major != 1 || version.minor != 1)
|
|
{
|
|
return HttpMessageParsingResult::HttpVersionNotSupported;
|
|
}
|
|
|
|
List<HttpField> headers;
|
|
vint headersBegin = startLineEnd + 2;
|
|
vint reading = headersBegin;
|
|
vint bodyBegin = -1;
|
|
while (true)
|
|
{
|
|
vint lineEnd = FindCrlf(buffer, reading, availableBytes);
|
|
if (lineEnd == -1)
|
|
{
|
|
return availableBytes - headersBegin >= HttpHeaderBlockSizeLimit
|
|
? (requestMessage ? HttpMessageParsingResult::RequestHeaderFieldsTooLarge : HttpMessageParsingResult::BadRequest)
|
|
: HttpMessageParsingResult::Incomplete;
|
|
}
|
|
if (lineEnd + 2 - headersBegin > HttpHeaderBlockSizeLimit)
|
|
{
|
|
return requestMessage ? HttpMessageParsingResult::RequestHeaderFieldsTooLarge : HttpMessageParsingResult::BadRequest;
|
|
}
|
|
if (lineEnd == reading)
|
|
{
|
|
bodyBegin = lineEnd + 2;
|
|
break;
|
|
}
|
|
HttpField field;
|
|
if (!ParseFieldLine(buffer, reading, lineEnd, field)) return HttpMessageParsingResult::BadRequest;
|
|
headers.Add(std::move(field));
|
|
reading = lineEnd + 2;
|
|
}
|
|
|
|
HttpFraming framing;
|
|
auto framingResult = AnalyzeHttpFraming(headers, framing);
|
|
if (framingResult == HttpFramingAnalysisResult::Invalid) return HttpMessageParsingResult::BadRequest;
|
|
if (framingResult == HttpFramingAnalysisResult::UnsupportedTransferCoding) return HttpMessageParsingResult::NotImplemented;
|
|
if (requestMessage && HasHeader(headers, L"expect")) return HttpMessageParsingResult::ExpectationFailed;
|
|
auto hasContentLength = framing.kind == HttpFramingKind::ContentLength;
|
|
auto hasTransferEncoding = framing.kind == HttpFramingKind::Chunked;
|
|
|
|
auto headResponse = !requestMessage && responseToMethod == L"HEAD";
|
|
auto noContentResponse = !requestMessage && statusCode == 204;
|
|
auto notModifiedResponse = !requestMessage && statusCode == 304;
|
|
auto responseWithoutBody = headResponse || noContentResponse || notModifiedResponse;
|
|
if (noContentResponse && (hasContentLength || hasTransferEncoding)) return HttpMessageParsingResult::BadRequest;
|
|
if (notModifiedResponse && hasTransferEncoding) return HttpMessageParsingResult::BadRequest;
|
|
if (!requestMessage && !responseWithoutBody && !hasContentLength && !hasTransferEncoding) return HttpMessageParsingResult::BadRequest;
|
|
|
|
HttpBody body;
|
|
vint bodyBytes = 0;
|
|
if (!responseWithoutBody && hasTransferEncoding)
|
|
{
|
|
auto result = ParseHttpRequestBodyToChunksDetailed(buffer + bodyBegin, availableBytes - bodyBegin, body, bodyBytes);
|
|
if (result == HttpBodyDetailedParsingResult::Incomplete) return HttpMessageParsingResult::Incomplete;
|
|
if (result == HttpBodyDetailedParsingResult::PayloadTooLarge) return HttpMessageParsingResult::PayloadTooLarge;
|
|
if (result == HttpBodyDetailedParsingResult::TrailerFieldsTooLarge) return HttpMessageParsingResult::RequestHeaderFieldsTooLarge;
|
|
if (result == HttpBodyDetailedParsingResult::BadRequest) return HttpMessageParsingResult::BadRequest;
|
|
}
|
|
else if (!responseWithoutBody && hasContentLength)
|
|
{
|
|
if (framing.contentLength > (vuint64_t)HttpBodySizeLimit) return HttpMessageParsingResult::PayloadTooLarge;
|
|
bodyBytes = (vint)framing.contentLength;
|
|
if (availableBytes - bodyBegin < bodyBytes) return HttpMessageParsingResult::Incomplete;
|
|
if (bodyBytes > 0)
|
|
{
|
|
HttpBodyChunk chunk;
|
|
chunk.data.Resize(bodyBytes);
|
|
std::memcpy(&chunk.data[0], buffer + bodyBegin, (size_t)bodyBytes);
|
|
body.chunks.Add(std::move(chunk));
|
|
}
|
|
}
|
|
|
|
consumedBytes = bodyBegin + bodyBytes;
|
|
connectionClose = framing.connectionClose;
|
|
if (requestMessage)
|
|
{
|
|
request = Ptr(new HttpRequest);
|
|
request->version = version;
|
|
request->method = method;
|
|
request->requestTarget = target;
|
|
request->headers = std::move(headers);
|
|
request->body = std::move(body);
|
|
}
|
|
else
|
|
{
|
|
response = Ptr(new HttpResponse);
|
|
response->version = version;
|
|
response->statusCode = statusCode;
|
|
response->reason = reason;
|
|
response->headers = std::move(headers);
|
|
response->body = std::move(body);
|
|
}
|
|
return HttpMessageParsingResult::Succeeded;
|
|
}
|
|
|
|
bool ValidateField(const HttpField& field, bool trailer)
|
|
{
|
|
if (field.name.Length() == 0) return false;
|
|
for (vint i = 0; i < field.name.Length(); i++)
|
|
{
|
|
auto c = field.name[i];
|
|
if ((vuint32_t)c > 0x7F || !IsTokenCharacter((vuint8_t)c) || (c >= L'A' && c <= L'Z')) return false;
|
|
}
|
|
for (auto c : field.value)
|
|
{
|
|
if (!IsFieldValueCharacter(c)) return false;
|
|
}
|
|
if (trailer && (field.name == L"content-length" || field.name == L"transfer-encoding")) return false;
|
|
return true;
|
|
}
|
|
|
|
void AppendAscii(List<vuint8_t>& bytes, const wchar_t* text)
|
|
{
|
|
while (*text)
|
|
{
|
|
CHECK_ERROR(*text <= 0x7F, L"HTTP serialization requires ASCII protocol text.");
|
|
bytes.Add((vuint8_t)*text++);
|
|
}
|
|
}
|
|
|
|
void AppendAscii(List<vuint8_t>& bytes, const WString& text)
|
|
{
|
|
for (vint i = 0; i < text.Length(); i++)
|
|
{
|
|
CHECK_ERROR(text[i] <= 0x7F, L"HTTP serialization requires ASCII protocol text.");
|
|
bytes.Add((vuint8_t)text[i]);
|
|
}
|
|
}
|
|
|
|
void AppendDecimal(List<vuint8_t>& bytes, vint number)
|
|
{
|
|
CHECK_ERROR(number >= 0, L"HTTP serialization requires a non-negative decimal number.");
|
|
vuint8_t digits[32];
|
|
auto count = 0;
|
|
auto value = (vuint64_t)number;
|
|
do
|
|
{
|
|
digits[count++] = (vuint8_t)('0' + value % 10);
|
|
value /= 10;
|
|
} while (value > 0);
|
|
for (vint i = count - 1; i >= 0; i--) bytes.Add(digits[i]);
|
|
}
|
|
|
|
void AppendHexadecimal(List<vuint8_t>& bytes, vint number)
|
|
{
|
|
CHECK_ERROR(number > 0, L"HTTP chunk serialization requires a positive chunk size.");
|
|
vuint8_t digits[32];
|
|
auto count = 0;
|
|
auto value = (vuint64_t)number;
|
|
const wchar_t* hex = L"0123456789abcdef";
|
|
while (value > 0)
|
|
{
|
|
digits[count++] = (vuint8_t)hex[value % 16];
|
|
value /= 16;
|
|
}
|
|
for (vint i = count - 1; i >= 0; i--) bytes.Add(digits[i]);
|
|
}
|
|
|
|
void AppendCrlf(List<vuint8_t>& bytes)
|
|
{
|
|
bytes.Add('\r');
|
|
bytes.Add('\n');
|
|
}
|
|
|
|
void AppendField(List<vuint8_t>& bytes, const HttpField& field)
|
|
{
|
|
AppendAscii(bytes, field.name);
|
|
bytes.Add(':');
|
|
bytes.Add(' ');
|
|
for (auto c : field.value) bytes.Add(c);
|
|
AppendCrlf(bytes);
|
|
}
|
|
|
|
vint DecimalDigitCount(vint number)
|
|
{
|
|
vint count = 1;
|
|
while (number >= 10)
|
|
{
|
|
number /= 10;
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
vint HexadecimalDigitCount(vint number)
|
|
{
|
|
vint count = 1;
|
|
while (number >= 16)
|
|
{
|
|
number /= 16;
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
vint AsciiLength(const wchar_t* text)
|
|
{
|
|
vint count = 0;
|
|
while (text[count]) count++;
|
|
return count;
|
|
}
|
|
|
|
void AddBoundedSize(vint& total, vint adding, vint limit, const wchar_t* error)
|
|
{
|
|
CHECK_ERROR(adding >= 0 && total <= limit - adding, error);
|
|
total += adding;
|
|
}
|
|
|
|
vint ValidateBody(const HttpBody& body)
|
|
{
|
|
CHECK_ERROR(body.chunks.Count() <= HttpChunkCountLimit, L"HTTP body contains too many chunks.");
|
|
vint total = 0;
|
|
for (auto&& chunk : body.chunks)
|
|
{
|
|
CHECK_ERROR(chunk.data.Count() > 0, L"HTTP body chunks must contain at least one octet.");
|
|
CHECK_ERROR(total <= HttpBodySizeLimit - chunk.data.Count(), L"HTTP body exceeds the configured size limit.");
|
|
total += chunk.data.Count();
|
|
}
|
|
for (auto&& trailer : body.trailers)
|
|
{
|
|
CHECK_ERROR(ValidateField(trailer, true), L"HTTP body contains an invalid trailer field.");
|
|
}
|
|
return total;
|
|
}
|
|
|
|
Ptr<AsyncSocketBuffer> SerializeHttpMessage(HttpRequest* request, HttpResponse* response, const WString& responseToMethod, bool& connectionClose)
|
|
{
|
|
auto requestMessage = request != nullptr;
|
|
CHECK_ERROR(requestMessage != (response != nullptr), L"HTTP serialization requires exactly one message.");
|
|
auto&& version = requestMessage ? request->version : response->version;
|
|
auto&& headers = requestMessage ? request->headers : response->headers;
|
|
auto&& body = requestMessage ? request->body : response->body;
|
|
CHECK_ERROR(version.major == 1 && version.minor == 1, L"HTTP serialization only supports HTTP/1.1.");
|
|
|
|
vint startLineSize = 0;
|
|
if (requestMessage)
|
|
{
|
|
switch (ValidateHttpRequestLine(request->method, request->requestTarget))
|
|
{
|
|
case HttpRequestLineValidationResult::InvalidMethod:
|
|
CHECK_FAIL(L"HTTP request serialization received an invalid method.");
|
|
case HttpRequestLineValidationResult::InvalidRequestTarget:
|
|
CHECK_FAIL(L"HTTP request serialization received an invalid request target.");
|
|
case HttpRequestLineValidationResult::TooLong:
|
|
CHECK_FAIL(L"HTTP start line exceeds the configured size limit.");
|
|
default:
|
|
break;
|
|
}
|
|
CHECK_ERROR(TryGetHttpRequestLineSize(request->method, request->requestTarget, startLineSize), L"HTTP start line exceeds the configured size limit.");
|
|
}
|
|
else
|
|
{
|
|
CHECK_ERROR(response->statusCode >= 200 && response->statusCode <= 599, L"HTTP response serialization only supports final status codes from 200 through 599.");
|
|
for (vint i = 0; i < response->reason.Length(); i++)
|
|
{
|
|
CHECK_ERROR(response->reason[i] >= 0x20 && response->reason[i] <= 0x7E, L"HTTP response serialization received an invalid reason phrase.");
|
|
}
|
|
startLineSize = 13;
|
|
AddBoundedSize(startLineSize, response->reason.Length(), HttpRequestLineSizeLimit, L"HTTP start line exceeds the configured size limit.");
|
|
}
|
|
|
|
vint headerBlockSize = 2;
|
|
for (auto&& field : headers)
|
|
{
|
|
CHECK_ERROR(ValidateField(field, false), L"HTTP serialization received an invalid header field.");
|
|
AddBoundedSize(headerBlockSize, field.name.Length(), HttpHeaderBlockSizeLimit, L"HTTP header block exceeds the configured size limit.");
|
|
AddBoundedSize(headerBlockSize, 2, HttpHeaderBlockSizeLimit, L"HTTP header block exceeds the configured size limit.");
|
|
AddBoundedSize(headerBlockSize, field.value.Count(), HttpHeaderBlockSizeLimit, L"HTTP header block exceeds the configured size limit.");
|
|
AddBoundedSize(headerBlockSize, 2, HttpHeaderBlockSizeLimit, L"HTTP header block exceeds the configured size limit.");
|
|
}
|
|
|
|
HttpFraming framing;
|
|
CHECK_ERROR(AnalyzeHttpFraming(headers, framing) == HttpFramingAnalysisResult::Succeeded, L"HTTP serialization received invalid, ambiguous, or unsupported body framing.");
|
|
auto bodySize = ValidateBody(body);
|
|
auto headResponse = !requestMessage && responseToMethod == L"HEAD";
|
|
auto noContentResponse = !requestMessage && response->statusCode == 204;
|
|
auto notModifiedResponse = !requestMessage && response->statusCode == 304;
|
|
auto suppressBody = headResponse || noContentResponse || notModifiedResponse;
|
|
auto hasContentLength = framing.kind == HttpFramingKind::ContentLength;
|
|
auto hasTransferEncoding = framing.kind == HttpFramingKind::Chunked;
|
|
auto chunked = hasTransferEncoding;
|
|
auto generateContentLength = false;
|
|
auto generateTransferEncoding = false;
|
|
if (noContentResponse)
|
|
{
|
|
CHECK_ERROR(body.chunks.Count() == 0 && body.trailers.Count() == 0, L"An HTTP 204 response cannot contain a body.");
|
|
CHECK_ERROR(!hasContentLength && !hasTransferEncoding, L"An HTTP 204 response cannot contain body framing.");
|
|
chunked = false;
|
|
}
|
|
else if (notModifiedResponse)
|
|
{
|
|
CHECK_ERROR(body.chunks.Count() == 0 && body.trailers.Count() == 0, L"An HTTP 304 response cannot contain a body.");
|
|
CHECK_ERROR(!hasTransferEncoding, L"An HTTP 304 response cannot contain Transfer-Encoding.");
|
|
chunked = false;
|
|
}
|
|
else if (!hasContentLength && !hasTransferEncoding)
|
|
{
|
|
chunked = body.chunks.Count() > 1 || body.trailers.Count() > 0;
|
|
generateTransferEncoding = chunked;
|
|
generateContentLength = !chunked && (!requestMessage || body.chunks.Count() > 0);
|
|
}
|
|
if (!noContentResponse && !notModifiedResponse && !chunked)
|
|
{
|
|
CHECK_ERROR(body.trailers.Count() == 0 && body.chunks.Count() <= 1, L"A fixed HTTP body cannot contain multiple chunks or trailers.");
|
|
if (hasContentLength)
|
|
{
|
|
if (!headResponse || body.chunks.Count() > 0)
|
|
{
|
|
CHECK_ERROR(framing.contentLength == (vuint64_t)bodySize, L"HTTP Content-Length does not match the supplied body.");
|
|
}
|
|
}
|
|
else if (!generateContentLength && !headResponse)
|
|
{
|
|
CHECK_ERROR(requestMessage && bodySize == 0, L"An HTTP response requires explicit body framing.");
|
|
}
|
|
}
|
|
if (generateContentLength)
|
|
{
|
|
AddBoundedSize(headerBlockSize, AsciiLength(L"content-length: ") + DecimalDigitCount(bodySize) + 2, HttpHeaderBlockSizeLimit, L"HTTP header block exceeds the configured size limit.");
|
|
}
|
|
if (generateTransferEncoding)
|
|
{
|
|
AddBoundedSize(headerBlockSize, AsciiLength(L"transfer-encoding: chunked\r\n"), HttpHeaderBlockSizeLimit, L"HTTP header block exceeds the configured size limit.");
|
|
}
|
|
|
|
vint trailerBlockSize = 2;
|
|
for (auto&& trailer : body.trailers)
|
|
{
|
|
AddBoundedSize(trailerBlockSize, trailer.name.Length(), HttpTrailerBlockSizeLimit, L"HTTP trailer block exceeds the configured size limit.");
|
|
AddBoundedSize(trailerBlockSize, 2, HttpTrailerBlockSizeLimit, L"HTTP trailer block exceeds the configured size limit.");
|
|
AddBoundedSize(trailerBlockSize, trailer.value.Count(), HttpTrailerBlockSizeLimit, L"HTTP trailer block exceeds the configured size limit.");
|
|
AddBoundedSize(trailerBlockSize, 2, HttpTrailerBlockSizeLimit, L"HTTP trailer block exceeds the configured size limit.");
|
|
}
|
|
vint wireSize = startLineSize;
|
|
AddBoundedSize(wireSize, 2, HttpWireMessageSizeLimit, L"HTTP wire message exceeds the configured size limit.");
|
|
AddBoundedSize(wireSize, headerBlockSize, HttpWireMessageSizeLimit, L"HTTP wire message exceeds the configured size limit.");
|
|
if (!suppressBody && chunked)
|
|
{
|
|
for (auto&& chunk : body.chunks)
|
|
{
|
|
AddBoundedSize(wireSize, HexadecimalDigitCount(chunk.data.Count()) + 4, HttpWireMessageSizeLimit, L"HTTP wire message exceeds the configured size limit.");
|
|
AddBoundedSize(wireSize, chunk.data.Count(), HttpWireMessageSizeLimit, L"HTTP wire message exceeds the configured size limit.");
|
|
}
|
|
AddBoundedSize(wireSize, 3, HttpWireMessageSizeLimit, L"HTTP wire message exceeds the configured size limit.");
|
|
AddBoundedSize(wireSize, trailerBlockSize, HttpWireMessageSizeLimit, L"HTTP wire message exceeds the configured size limit.");
|
|
}
|
|
else if (!suppressBody)
|
|
{
|
|
AddBoundedSize(wireSize, bodySize, HttpWireMessageSizeLimit, L"HTTP wire message exceeds the configured size limit.");
|
|
}
|
|
|
|
List<vuint8_t> bytes;
|
|
if (requestMessage)
|
|
{
|
|
AppendAscii(bytes, request->method);
|
|
bytes.Add(' ');
|
|
AppendAscii(bytes, request->requestTarget);
|
|
bytes.Add(' ');
|
|
}
|
|
AppendAscii(bytes, L"HTTP/");
|
|
AppendDecimal(bytes, version.major);
|
|
bytes.Add('.');
|
|
AppendDecimal(bytes, version.minor);
|
|
if (!requestMessage)
|
|
{
|
|
bytes.Add(' ');
|
|
AppendDecimal(bytes, response->statusCode);
|
|
bytes.Add(' ');
|
|
AppendAscii(bytes, response->reason);
|
|
}
|
|
CHECK_ERROR(bytes.Count() <= HttpRequestLineSizeLimit, L"HTTP start line exceeds the configured size limit.");
|
|
AppendCrlf(bytes);
|
|
|
|
for (auto&& field : headers) AppendField(bytes, field);
|
|
if (generateContentLength)
|
|
{
|
|
AppendAscii(bytes, L"content-length: ");
|
|
AppendDecimal(bytes, bodySize);
|
|
AppendCrlf(bytes);
|
|
}
|
|
if (generateTransferEncoding)
|
|
{
|
|
AppendAscii(bytes, L"transfer-encoding: chunked\r\n");
|
|
}
|
|
AppendCrlf(bytes);
|
|
|
|
if (!suppressBody && chunked)
|
|
{
|
|
for (auto&& chunk : body.chunks)
|
|
{
|
|
AppendHexadecimal(bytes, chunk.data.Count());
|
|
AppendCrlf(bytes);
|
|
for (auto c : chunk.data) bytes.Add(c);
|
|
AppendCrlf(bytes);
|
|
}
|
|
AppendAscii(bytes, L"0\r\n");
|
|
for (auto&& trailer : body.trailers) AppendField(bytes, trailer);
|
|
AppendCrlf(bytes);
|
|
}
|
|
else if (!suppressBody && body.chunks.Count() == 1)
|
|
{
|
|
for (auto c : body.chunks[0].data) bytes.Add(c);
|
|
}
|
|
CHECK_ERROR(bytes.Count() <= HttpWireMessageSizeLimit, L"HTTP wire message exceeds the configured size limit.");
|
|
|
|
auto buffer = Ptr(new AsyncSocketBuffer);
|
|
buffer->data.Resize(bytes.Count());
|
|
if (bytes.Count() > 0)
|
|
{
|
|
std::memcpy(&buffer->data[0], &bytes[0], (size_t)bytes.Count());
|
|
}
|
|
connectionClose = framing.connectionClose;
|
|
return buffer;
|
|
}
|
|
}
|
|
|
|
HttpFramingAnalysisResult AnalyzeHttpFraming(const List<HttpField>& fields, HttpFraming& framing)
|
|
{
|
|
framing = HttpFraming();
|
|
bool contentLengthInitialized = false;
|
|
bool hasTransferEncoding = false;
|
|
List<WString> transferCodings;
|
|
bool transferCodingParameters = false;
|
|
for (auto&& field : fields)
|
|
{
|
|
if (field.name == L"content-length")
|
|
{
|
|
framing.contentLengthFieldCount++;
|
|
if (field.value.Count() == 0)
|
|
{
|
|
framing.contentLengthValuesPlainDecimal = false;
|
|
}
|
|
else
|
|
{
|
|
for (auto c : field.value)
|
|
{
|
|
if (!IsDigit(c))
|
|
{
|
|
framing.contentLengthValuesPlainDecimal = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!ParseContentLength(field.value, contentLengthInitialized, framing.contentLength, framing.contentLengthValueCount))
|
|
{
|
|
return HttpFramingAnalysisResult::Invalid;
|
|
}
|
|
}
|
|
else if (field.name == L"transfer-encoding")
|
|
{
|
|
hasTransferEncoding = true;
|
|
if (!ParseTransferCodings(field.value, transferCodings, transferCodingParameters))
|
|
{
|
|
return HttpFramingAnalysisResult::Invalid;
|
|
}
|
|
}
|
|
else if (field.name == L"connection")
|
|
{
|
|
framing.connectionClose |= HasConnectionClose(field.value);
|
|
}
|
|
}
|
|
if (hasTransferEncoding && contentLengthInitialized)
|
|
{
|
|
return HttpFramingAnalysisResult::Invalid;
|
|
}
|
|
if (hasTransferEncoding)
|
|
{
|
|
if (
|
|
transferCodings.Count() != 1 ||
|
|
transferCodings[0] != L"chunked" ||
|
|
transferCodingParameters
|
|
)
|
|
{
|
|
return HttpFramingAnalysisResult::UnsupportedTransferCoding;
|
|
}
|
|
framing.kind = HttpFramingKind::Chunked;
|
|
}
|
|
else if (contentLengthInitialized)
|
|
{
|
|
framing.kind = HttpFramingKind::ContentLength;
|
|
}
|
|
return HttpFramingAnalysisResult::Succeeded;
|
|
}
|
|
|
|
const HttpField* FindHttpField(const List<HttpField>& fields, const WString& normalizedName)
|
|
{
|
|
for (auto&& field : fields)
|
|
{
|
|
if (field.name == normalizedName)
|
|
{
|
|
return &field;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
vint CountHttpFields(const List<HttpField>& fields, const WString& normalizedName)
|
|
{
|
|
vint count = 0;
|
|
for (auto&& field : fields)
|
|
{
|
|
if (field.name == normalizedName)
|
|
{
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
HttpField CreateAsciiHttpField(const WString& name, const WString& value)
|
|
{
|
|
CHECK_ERROR(name.Length() > 0, L"An HTTP field name cannot be empty.");
|
|
HttpField field;
|
|
Array<wchar_t> normalizedName(name.Length());
|
|
for (vint i = 0; i < name.Length(); i++)
|
|
{
|
|
auto c = name[i];
|
|
CHECK_ERROR((vuint32_t)c <= 0x7F && IsTokenCharacter((vuint8_t)c), L"An HTTP field name must contain only ASCII token characters.");
|
|
if (L'A' <= c && c <= L'Z') c += L'a' - L'A';
|
|
normalizedName[i] = c;
|
|
}
|
|
field.name = WString::CopyFrom(&normalizedName[0], normalizedName.Count());
|
|
field.value.Resize(value.Length());
|
|
for (vint i = 0; i < value.Length(); i++)
|
|
{
|
|
auto c = value[i];
|
|
CHECK_ERROR((vuint32_t)c <= 0x7F && IsFieldValueCharacter((vuint8_t)c), L"An HTTP field value must contain only valid ASCII field characters.");
|
|
field.value[i] = (vuint8_t)c;
|
|
}
|
|
return field;
|
|
}
|
|
|
|
bool DecodeAsciiHttpFieldValue(const Array<vuint8_t>& value, WString& text)
|
|
{
|
|
for (auto c : value)
|
|
{
|
|
if (c > 0x7F) return false;
|
|
}
|
|
if (value.Count() == 0)
|
|
{
|
|
text = WString::Empty;
|
|
return true;
|
|
}
|
|
Array<wchar_t> characters(value.Count());
|
|
for (vint i = 0; i < value.Count(); i++)
|
|
{
|
|
characters[i] = (wchar_t)value[i];
|
|
}
|
|
text = WString::CopyFrom(&characters[0], characters.Count());
|
|
return true;
|
|
}
|
|
|
|
bool HttpFieldValueEqualsAscii(const Array<vuint8_t>& value, const WString& expected)
|
|
{
|
|
if (value.Count() != expected.Length()) return false;
|
|
for (vint i = 0; i < value.Count(); i++)
|
|
{
|
|
if ((vuint32_t)expected[i] > 0x7F || value[i] != (vuint8_t)expected[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool TryGetHttpBodySize(const HttpBody& body, vint& size)
|
|
{
|
|
vint total = 0;
|
|
for (auto&& chunk : body.chunks)
|
|
{
|
|
if (chunk.data.Count() > HttpBodySizeLimit - total) return false;
|
|
total += chunk.data.Count();
|
|
}
|
|
size = total;
|
|
return true;
|
|
}
|
|
|
|
bool FlattenHttpBody(const HttpBody& body, Array<vuint8_t>& bytes)
|
|
{
|
|
vint size = 0;
|
|
if (!TryGetHttpBodySize(body, size)) return false;
|
|
Array<vuint8_t> flattened(size);
|
|
vint offset = 0;
|
|
for (auto&& chunk : body.chunks)
|
|
{
|
|
if (chunk.data.Count() > 0)
|
|
{
|
|
std::memcpy(&flattened[offset], &chunk.data[0], (size_t)chunk.data.Count());
|
|
offset += chunk.data.Count();
|
|
}
|
|
}
|
|
bytes = std::move(flattened);
|
|
return true;
|
|
}
|
|
|
|
void SetHttpBodyBytes(HttpBody& body, Array<vuint8_t>&& bytes)
|
|
{
|
|
CHECK_ERROR(bytes.Count() <= HttpBodySizeLimit, L"HTTP body exceeds the configured size limit.");
|
|
Array<vuint8_t> replacement = std::move(bytes);
|
|
body.chunks.Clear();
|
|
body.trailers.Clear();
|
|
if (replacement.Count() > 0)
|
|
{
|
|
HttpBodyChunk chunk;
|
|
chunk.data = std::move(replacement);
|
|
body.chunks.Add(std::move(chunk));
|
|
}
|
|
}
|
|
|
|
bool EncodeStrictUtf8(const WString& text, Array<vuint8_t>& bytes)
|
|
{
|
|
List<vuint8_t> encoded;
|
|
for (vint i = 0; i < text.Length(); i++)
|
|
{
|
|
auto code = (vuint32_t)text[i];
|
|
if constexpr (sizeof(wchar_t) == 2)
|
|
{
|
|
if (0xD800 <= code && code <= 0xDBFF)
|
|
{
|
|
if (++i == text.Length()) return false;
|
|
auto low = (vuint32_t)text[i];
|
|
if (low < 0xDC00 || low > 0xDFFF) return false;
|
|
code = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00);
|
|
}
|
|
else if (0xDC00 <= code && code <= 0xDFFF)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else if (code > 0x10FFFF || (0xD800 <= code && code <= 0xDFFF))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
vint adding = code < 0x80 ? 1 : code < 0x800 ? 2 : code < 0x10000 ? 3 : 4;
|
|
if (encoded.Count() > (std::numeric_limits<vint>::max)() - adding) return false;
|
|
if (adding == 1)
|
|
{
|
|
encoded.Add((vuint8_t)code);
|
|
}
|
|
else
|
|
{
|
|
static const vuint8_t prefixes[] = { 0, 0xC0, 0xE0, 0xF0 };
|
|
vuint8_t output[4];
|
|
for (vint j = adding - 1; j > 0; j--)
|
|
{
|
|
output[j] = 0x80 | (code & 0x3F);
|
|
code >>= 6;
|
|
}
|
|
output[0] = prefixes[adding - 1] | (vuint8_t)code;
|
|
for (vint j = 0; j < adding; j++) encoded.Add(output[j]);
|
|
}
|
|
}
|
|
Array<vuint8_t> result(encoded.Count());
|
|
for (vint i = 0; i < encoded.Count(); i++) result[i] = encoded[i];
|
|
bytes = std::move(result);
|
|
return true;
|
|
}
|
|
|
|
bool DecodeStrictUtf8(const vuint8_t* bytes, vint count, WString& text)
|
|
{
|
|
if (count < 0 || (!bytes && count > 0)) return false;
|
|
List<wchar_t> characters;
|
|
for (vint i = 0; i < count;)
|
|
{
|
|
auto first = bytes[i++];
|
|
vuint32_t code = 0;
|
|
vint following = 0;
|
|
if (first < 0x80)
|
|
{
|
|
code = first;
|
|
}
|
|
else if (0xC2 <= first && first <= 0xDF)
|
|
{
|
|
code = first & 0x1F;
|
|
following = 1;
|
|
}
|
|
else if (0xE0 <= first && first <= 0xEF)
|
|
{
|
|
code = first & 0x0F;
|
|
following = 2;
|
|
}
|
|
else if (0xF0 <= first && first <= 0xF4)
|
|
{
|
|
code = first & 0x07;
|
|
following = 3;
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
if (following > count - i) return false;
|
|
for (vint j = 0; j < following; j++)
|
|
{
|
|
auto next = bytes[i++];
|
|
if ((next & 0xC0) != 0x80) return false;
|
|
code = (code << 6) | (next & 0x3F);
|
|
}
|
|
if (
|
|
(following == 1 && code < 0x80) ||
|
|
(following == 2 && code < 0x800) ||
|
|
(following == 3 && code < 0x10000) ||
|
|
code > 0x10FFFF ||
|
|
(0xD800 <= code && code <= 0xDFFF)
|
|
)
|
|
{
|
|
return false;
|
|
}
|
|
if constexpr (sizeof(wchar_t) == 2)
|
|
{
|
|
if (code <= 0xFFFF)
|
|
{
|
|
characters.Add((wchar_t)code);
|
|
}
|
|
else
|
|
{
|
|
if (characters.Count() > (std::numeric_limits<vint>::max)() - 2) return false;
|
|
code -= 0x10000;
|
|
characters.Add((wchar_t)(0xD800 + (code >> 10)));
|
|
characters.Add((wchar_t)(0xDC00 + (code & 0x3FF)));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
characters.Add((wchar_t)code);
|
|
}
|
|
}
|
|
text = characters.Count() == 0 ? WString::Empty : WString::CopyFrom(&characters[0], characters.Count());
|
|
return true;
|
|
}
|
|
|
|
HttpRequestLineValidationResult ValidateHttpRequestLine(const WString& method, const WString& requestTarget)
|
|
{
|
|
if (method.Length() == 0) return HttpRequestLineValidationResult::InvalidMethod;
|
|
for (vint i = 0; i < method.Length(); i++)
|
|
{
|
|
auto c = method[i];
|
|
if ((vuint32_t)c > 0x7F || !IsTokenCharacter((vuint8_t)c)) return HttpRequestLineValidationResult::InvalidMethod;
|
|
}
|
|
if (requestTarget.Length() == 0) return HttpRequestLineValidationResult::InvalidRequestTarget;
|
|
for (vint i = 0; i < requestTarget.Length(); i++)
|
|
{
|
|
auto c = requestTarget[i];
|
|
if (c < 0x21 || c > 0x7E) return HttpRequestLineValidationResult::InvalidRequestTarget;
|
|
}
|
|
vint requestLineSize = 0;
|
|
if (!TryGetHttpRequestLineSize(method, requestTarget, requestLineSize)) return HttpRequestLineValidationResult::TooLong;
|
|
return HttpRequestLineValidationResult::Succeeded;
|
|
}
|
|
|
|
/***********************************************************************
|
|
HttpRequestBody
|
|
***********************************************************************/
|
|
|
|
namespace
|
|
{
|
|
HttpBodyDetailedParsingResult ParseHttpRequestBodyToChunksDetailed(
|
|
const vuint8_t* buffer,
|
|
vint availableBytes,
|
|
HttpBody& output,
|
|
vint& consumedBytes
|
|
)
|
|
{
|
|
consumedBytes = 0;
|
|
if (availableBytes < 0 || (!buffer && availableBytes > 0)) return HttpBodyDetailedParsingResult::BadRequest;
|
|
HttpBody parsed;
|
|
vint reading = 0;
|
|
vint decodedBytes = 0;
|
|
while (true)
|
|
{
|
|
vint lineEnd = FindCrlf(buffer, reading, availableBytes);
|
|
if (lineEnd == -1)
|
|
{
|
|
auto trailingCrlfPrefix = availableBytes > reading && buffer[availableBytes - 1] == '\r';
|
|
vint prefixEnd = trailingCrlfPrefix ? availableBytes - 1 : availableBytes;
|
|
auto validPrefix = trailingCrlfPrefix
|
|
? ValidateCompleteChunkSizeLine(buffer, reading, prefixEnd)
|
|
: ValidateChunkSizeLinePrefix(buffer, reading, prefixEnd);
|
|
if (prefixEnd - reading > HttpChunkSizeLineLimit || !validPrefix)
|
|
{
|
|
return HttpBodyDetailedParsingResult::BadRequest;
|
|
}
|
|
return HttpBodyDetailedParsingResult::Incomplete;
|
|
}
|
|
if (lineEnd - reading > HttpChunkSizeLineLimit) return HttpBodyDetailedParsingResult::BadRequest;
|
|
if (lineEnd + 2 > HttpWireMessageSizeLimit) return HttpBodyDetailedParsingResult::PayloadTooLarge;
|
|
vint numberEnd = reading;
|
|
vuint64_t chunkSize = 0;
|
|
while (numberEnd < lineEnd && IsHexDigit(buffer[numberEnd]))
|
|
{
|
|
auto digit = (vuint64_t)HexDigitValue(buffer[numberEnd++]);
|
|
if (chunkSize > ((std::numeric_limits<vuint64_t>::max)() - digit) / 16) return HttpBodyDetailedParsingResult::PayloadTooLarge;
|
|
chunkSize = chunkSize * 16 + digit;
|
|
}
|
|
if (numberEnd == reading) return HttpBodyDetailedParsingResult::BadRequest;
|
|
if (chunkSize > (vuint64_t)HttpBodySizeLimit) return HttpBodyDetailedParsingResult::PayloadTooLarge;
|
|
if (numberEnd < lineEnd)
|
|
{
|
|
vint firstExtension = numberEnd;
|
|
while (firstExtension < lineEnd && IsOws(buffer[firstExtension])) firstExtension++;
|
|
if (firstExtension == lineEnd || buffer[firstExtension] != ';') return HttpBodyDetailedParsingResult::BadRequest;
|
|
}
|
|
vint extensionReading = numberEnd;
|
|
if (!ParseSemicolonParameters(buffer, extensionReading, lineEnd)) return HttpBodyDetailedParsingResult::BadRequest;
|
|
reading = lineEnd + 2;
|
|
|
|
if (chunkSize == 0)
|
|
{
|
|
vint trailerBegin = reading;
|
|
while (true)
|
|
{
|
|
vint trailerEnd = FindCrlf(buffer, reading, availableBytes);
|
|
if (trailerEnd == -1)
|
|
{
|
|
return availableBytes - trailerBegin >= HttpTrailerBlockSizeLimit
|
|
? HttpBodyDetailedParsingResult::TrailerFieldsTooLarge
|
|
: HttpBodyDetailedParsingResult::Incomplete;
|
|
}
|
|
if (trailerEnd + 2 - trailerBegin > HttpTrailerBlockSizeLimit) return HttpBodyDetailedParsingResult::TrailerFieldsTooLarge;
|
|
if (trailerEnd + 2 > HttpWireMessageSizeLimit) return HttpBodyDetailedParsingResult::PayloadTooLarge;
|
|
if (trailerEnd == reading)
|
|
{
|
|
consumedBytes = trailerEnd + 2;
|
|
output = std::move(parsed);
|
|
return HttpBodyDetailedParsingResult::Succeeded;
|
|
}
|
|
HttpField trailer;
|
|
if (!ParseFieldLine(buffer, reading, trailerEnd, trailer) || trailer.name == L"content-length" || trailer.name == L"transfer-encoding")
|
|
{
|
|
return HttpBodyDetailedParsingResult::BadRequest;
|
|
}
|
|
parsed.trailers.Add(std::move(trailer));
|
|
reading = trailerEnd + 2;
|
|
}
|
|
}
|
|
|
|
if (chunkSize > (vuint64_t)(HttpBodySizeLimit - decodedBytes)) return HttpBodyDetailedParsingResult::PayloadTooLarge;
|
|
vint chunkBytes = (vint)chunkSize;
|
|
if (availableBytes - reading < chunkBytes) return HttpBodyDetailedParsingResult::Incomplete;
|
|
auto terminatorBytes = availableBytes - reading - chunkBytes;
|
|
if (terminatorBytes == 0) return HttpBodyDetailedParsingResult::Incomplete;
|
|
if (buffer[reading + chunkBytes] != '\r') return HttpBodyDetailedParsingResult::BadRequest;
|
|
if (terminatorBytes == 1) return HttpBodyDetailedParsingResult::Incomplete;
|
|
if (buffer[reading + chunkBytes + 1] != '\n') return HttpBodyDetailedParsingResult::BadRequest;
|
|
if (reading > HttpWireMessageSizeLimit - chunkBytes - 2) return HttpBodyDetailedParsingResult::PayloadTooLarge;
|
|
if (parsed.chunks.Count() >= HttpChunkCountLimit) return HttpBodyDetailedParsingResult::PayloadTooLarge;
|
|
HttpBodyChunk chunk;
|
|
chunk.data.Resize(chunkBytes);
|
|
std::memcpy(&chunk.data[0], buffer + reading, (size_t)chunkBytes);
|
|
parsed.chunks.Add(std::move(chunk));
|
|
decodedBytes += chunkBytes;
|
|
reading += chunkBytes + 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
HttpRequestBodyParsingResult ParseHttpRequestBodyToChunks(
|
|
const vuint8_t* buffer,
|
|
vint availableBytes,
|
|
HttpBody& output,
|
|
vint& consumedBytes
|
|
)
|
|
{
|
|
auto result = ParseHttpRequestBodyToChunksDetailed(buffer, availableBytes, output, consumedBytes);
|
|
switch (result)
|
|
{
|
|
case HttpBodyDetailedParsingResult::Succeeded:
|
|
return HttpRequestBodyParsingResult::Succeeded;
|
|
case HttpBodyDetailedParsingResult::Incomplete:
|
|
return HttpRequestBodyParsingResult::Incomplete;
|
|
default:
|
|
return HttpRequestBodyParsingResult::Invalid;
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
IHttpRequestCallback
|
|
***********************************************************************/
|
|
|
|
void IHttpRequestCallback::OnReadRequest(Ptr<HttpRequest>)
|
|
{
|
|
}
|
|
|
|
void IHttpRequestCallback::OnReadRequestFailure(HttpRequestFailure)
|
|
{
|
|
}
|
|
|
|
void IHttpRequestCallback::OnReadResponse(Ptr<HttpResponse>)
|
|
{
|
|
}
|
|
|
|
void IHttpRequestCallback::OnReadResponseFailure(HttpResponseFailure)
|
|
{
|
|
OnError(L"The HTTP client received a 404 Not Found response.", true);
|
|
}
|
|
|
|
void IHttpRequestCallback::OnWriteCompleted()
|
|
{
|
|
}
|
|
|
|
void IHttpRequestCallback::OnError(const WString&, bool)
|
|
{
|
|
}
|
|
|
|
void IHttpRequestCallback::OnConnected()
|
|
{
|
|
}
|
|
|
|
void IHttpRequestCallback::OnDisconnected()
|
|
{
|
|
}
|
|
|
|
/***********************************************************************
|
|
HttpRequestTimeoutController
|
|
***********************************************************************/
|
|
|
|
namespace
|
|
{
|
|
class HttpRequestTimeoutController : public Object, public virtual IHttpRequestTimeoutController
|
|
{
|
|
private:
|
|
class State : public Object
|
|
{
|
|
public:
|
|
CriticalSection lock;
|
|
ConditionVariable cv;
|
|
Func<void()> callback;
|
|
std::chrono::steady_clock::time_point
|
|
deadline;
|
|
vint duration = 0;
|
|
bool armed = false;
|
|
bool workerRunning = false;
|
|
vint activeCallbacks = 0;
|
|
};
|
|
|
|
static thread_local State* currentCallbackState;
|
|
Ptr<State> state = Ptr(new State);
|
|
|
|
static void Run(Ptr<State> state)
|
|
{
|
|
while (true)
|
|
{
|
|
Func<void()> callback;
|
|
state->lock.Enter();
|
|
while (state->armed)
|
|
{
|
|
auto now = std::chrono::steady_clock::now();
|
|
if (now >= state->deadline)
|
|
{
|
|
callback = state->callback;
|
|
state->callback = {};
|
|
state->armed = false;
|
|
state->activeCallbacks++;
|
|
break;
|
|
}
|
|
auto remaining = std::chrono::ceil<std::chrono::milliseconds>(state->deadline - now).count();
|
|
auto wait = remaining > (std::numeric_limits<vint>::max)()
|
|
? (std::numeric_limits<vint>::max)()
|
|
: (vint)remaining;
|
|
state->cv.SleepWithForTime(state->lock, wait);
|
|
}
|
|
if (!callback)
|
|
{
|
|
state->workerRunning = false;
|
|
state->cv.WakeAllPendings();
|
|
state->lock.Leave();
|
|
return;
|
|
}
|
|
state->lock.Leave();
|
|
|
|
auto previous = currentCallbackState;
|
|
currentCallbackState = state.Obj();
|
|
try
|
|
{
|
|
callback();
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
currentCallbackState = previous;
|
|
|
|
CS_LOCK(state->lock)
|
|
{
|
|
state->activeCallbacks--;
|
|
state->cv.WakeAllPendings();
|
|
}
|
|
}
|
|
}
|
|
|
|
public:
|
|
~HttpRequestTimeoutController()
|
|
{
|
|
CancelAndWait();
|
|
}
|
|
|
|
void Arm(vint milliseconds, const Func<void()>& callback) override
|
|
{
|
|
CHECK_ERROR(milliseconds > 0, L"The HTTP timeout controller requires a positive duration.");
|
|
bool queueWorker = false;
|
|
CS_LOCK(state->lock)
|
|
{
|
|
CHECK_ERROR(!state->armed, L"The HTTP timeout controller is already armed.");
|
|
state->callback = callback;
|
|
state->duration = milliseconds;
|
|
state->deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(milliseconds);
|
|
state->armed = true;
|
|
if (!state->workerRunning)
|
|
{
|
|
state->workerRunning = true;
|
|
queueWorker = true;
|
|
}
|
|
state->cv.WakeAllPendings();
|
|
}
|
|
if (queueWorker)
|
|
{
|
|
auto captured = state;
|
|
auto queued = ThreadPoolLite::Queue(Func<void()>([captured]()
|
|
{
|
|
Run(captured);
|
|
}));
|
|
if (!queued)
|
|
{
|
|
CS_LOCK(state->lock)
|
|
{
|
|
state->callback = {};
|
|
state->armed = false;
|
|
state->workerRunning = false;
|
|
state->cv.WakeAllPendings();
|
|
}
|
|
CHECK_ERROR(false, L"The HTTP timeout controller could not queue its deadline worker.");
|
|
}
|
|
}
|
|
}
|
|
|
|
void Refresh() override
|
|
{
|
|
CS_LOCK(state->lock)
|
|
{
|
|
if (state->armed)
|
|
{
|
|
state->deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(state->duration);
|
|
state->cv.WakeAllPendings();
|
|
}
|
|
}
|
|
}
|
|
|
|
void CancelAndWait() override
|
|
{
|
|
auto nestedCallback = currentCallbackState == state.Obj();
|
|
state->lock.Enter();
|
|
state->armed = false;
|
|
state->callback = {};
|
|
state->cv.WakeAllPendings();
|
|
if (!nestedCallback)
|
|
{
|
|
while (state->workerRunning || state->activeCallbacks > 0)
|
|
{
|
|
state->cv.SleepWith(state->lock);
|
|
}
|
|
}
|
|
state->lock.Leave();
|
|
}
|
|
};
|
|
|
|
thread_local HttpRequestTimeoutController::State* HttpRequestTimeoutController::currentCallbackState = nullptr;
|
|
}
|
|
|
|
Ptr<IHttpRequestTimeoutController> CreateHttpRequestTimeoutController()
|
|
{
|
|
return Ptr(new HttpRequestTimeoutController);
|
|
}
|
|
|
|
/***********************************************************************
|
|
HttpRequestCallbackDomain
|
|
***********************************************************************/
|
|
|
|
thread_local HttpRequestCallbackDomain::CallbackFrame* HttpRequestCallbackDomain::currentCallbackFrame = nullptr;
|
|
|
|
HttpRequestCallbackDomain::CallbackFrame::CallbackFrame(Ptr<HttpRequestCallbackDomain> _domain)
|
|
: domain(_domain)
|
|
, previous(currentCallbackFrame)
|
|
{
|
|
currentCallbackFrame = this;
|
|
CS_LOCK(domain->lockState)
|
|
{
|
|
domain->activeCallbacks++;
|
|
}
|
|
}
|
|
|
|
HttpRequestCallbackDomain::CallbackFrame::~CallbackFrame()
|
|
{
|
|
currentCallbackFrame = previous;
|
|
CS_LOCK(domain->lockState)
|
|
{
|
|
domain->activeCallbacks--;
|
|
domain->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
|
|
vint HttpRequestCallbackDomain::CurrentCallbackDepth()
|
|
{
|
|
vint depth = 0;
|
|
for (auto frame = currentCallbackFrame; frame; frame = frame->previous)
|
|
{
|
|
if (frame->domain.Obj() == this) depth++;
|
|
}
|
|
return depth;
|
|
}
|
|
|
|
void HttpRequestCallbackDomain::WaitForCallbacks(vint callbackDepth)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
while (activeCallbacks > callbackDepth)
|
|
{
|
|
cvState.SleepWith(lockState);
|
|
}
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
HttpRequestConnectionLifecycle
|
|
***********************************************************************/
|
|
|
|
class HttpRequestConnectionLifecycle : public Object
|
|
{
|
|
public:
|
|
class RetainedAdapterRelease
|
|
{
|
|
public:
|
|
Ptr<Object> adapter;
|
|
Func<void()> drainedCallback;
|
|
|
|
~RetainedAdapterRelease()
|
|
{
|
|
if (drainedCallback)
|
|
{
|
|
try
|
|
{
|
|
drainedCallback();
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
IAsyncSocketConnection* socketConnection = nullptr;
|
|
HttpRequestConnectionDirection direction = HttpRequestConnectionDirection::Server;
|
|
bool responseNotFoundIsFatal = false;
|
|
Ptr<HttpRequestCallbackDomain> callbackDomain;
|
|
Ptr<IHttpRequestTimeoutController> timeoutController;
|
|
Ptr<Object> retainedAdapter;
|
|
Func<void()> drainedCallback;
|
|
|
|
CriticalSection lockState;
|
|
ConditionVariable cvState;
|
|
IHttpRequestCallback* callback = nullptr;
|
|
bool callbackInstalling = false;
|
|
vint activeCallbacks = 0;
|
|
vint activeSocketCallbacks = 0;
|
|
vint activeSocketCalls = 0;
|
|
bool stopStarted = false;
|
|
bool stopFinished = false;
|
|
bool terminal = false;
|
|
bool disconnectedNotified = false;
|
|
bool disconnectDelivering = false;
|
|
bool disconnectFinished = false;
|
|
bool readingStarted = false;
|
|
bool parserFailed = false;
|
|
bool peerDisconnected = false;
|
|
|
|
List<vuint8_t> receiveBuffer;
|
|
bool timeoutArmed = false;
|
|
bool awaitingResponse = false;
|
|
bool exchangeActive = false;
|
|
bool closeAfterExchange = false;
|
|
WString activeRequestMethod;
|
|
vint activeResponseTimeout = HttpIncompleteMessageTimeout;
|
|
Ptr<HttpResponse> heldResponse;
|
|
bool fatalAfterResponse = false;
|
|
bool responseDelivering = false;
|
|
bool responseFinalizing = false;
|
|
Ptr<AsyncSocketBuffer> deferredRequestWrite;
|
|
bool deferredRequestClose = false;
|
|
WString deferredRequestMethod;
|
|
vint deferredResponseTimeout = HttpIncompleteMessageTimeout;
|
|
Ptr<AsyncSocketBuffer> pendingWrite;
|
|
bool writePending = false;
|
|
|
|
void TakeRetainedAdapterIfDrained(RetainedAdapterRelease& releasing)
|
|
{
|
|
if (stopFinished && disconnectFinished && activeCallbacks == 0 && activeSocketCallbacks == 0 && activeSocketCalls == 0)
|
|
{
|
|
releasing.adapter = std::move(retainedAdapter);
|
|
releasing.drainedCallback = drainedCallback;
|
|
drainedCallback = {};
|
|
}
|
|
}
|
|
};
|
|
|
|
/***********************************************************************
|
|
HttpRequestConnection callback frames
|
|
***********************************************************************/
|
|
|
|
struct HttpRequestConnection::CallbackFrame
|
|
{
|
|
Ptr<Lifecycle> state;
|
|
CallbackFrame* previous = nullptr;
|
|
HttpRequestCallbackDomain::CallbackFrame domainFrame;
|
|
|
|
CallbackFrame(Ptr<Lifecycle> _state)
|
|
: state(_state)
|
|
, previous(currentCallbackFrame)
|
|
, domainFrame(state->callbackDomain)
|
|
{
|
|
currentCallbackFrame = this;
|
|
}
|
|
|
|
~CallbackFrame()
|
|
{
|
|
currentCallbackFrame = previous;
|
|
Lifecycle::RetainedAdapterRelease releasing;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->activeCallbacks--;
|
|
state->TakeRetainedAdapterIfDrained(releasing);
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
};
|
|
|
|
struct HttpRequestConnection::SocketCallbackFrame
|
|
{
|
|
Ptr<Lifecycle> state;
|
|
SocketCallbackFrame* previous = nullptr;
|
|
|
|
SocketCallbackFrame(Ptr<Lifecycle> _state)
|
|
: state(_state)
|
|
, previous(currentSocketCallbackFrame)
|
|
{
|
|
currentSocketCallbackFrame = this;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->activeSocketCallbacks++;
|
|
}
|
|
}
|
|
|
|
~SocketCallbackFrame()
|
|
{
|
|
currentSocketCallbackFrame = previous;
|
|
Lifecycle::RetainedAdapterRelease releasing;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->activeSocketCallbacks--;
|
|
state->TakeRetainedAdapterIfDrained(releasing);
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
};
|
|
|
|
struct HttpRequestConnection::TimeoutCallbackFrame
|
|
{
|
|
Ptr<Lifecycle> state;
|
|
TimeoutCallbackFrame* previous = nullptr;
|
|
|
|
TimeoutCallbackFrame(Ptr<Lifecycle> _state)
|
|
: state(_state)
|
|
, previous(currentTimeoutCallbackFrame)
|
|
{
|
|
currentTimeoutCallbackFrame = this;
|
|
}
|
|
|
|
~TimeoutCallbackFrame()
|
|
{
|
|
currentTimeoutCallbackFrame = previous;
|
|
}
|
|
};
|
|
|
|
thread_local HttpRequestConnection::CallbackFrame* HttpRequestConnection::currentCallbackFrame = nullptr;
|
|
thread_local HttpRequestConnection::SocketCallbackFrame* HttpRequestConnection::currentSocketCallbackFrame = nullptr;
|
|
thread_local HttpRequestConnection::TimeoutCallbackFrame* HttpRequestConnection::currentTimeoutCallbackFrame = nullptr;
|
|
|
|
/***********************************************************************
|
|
HttpRequestConnection helpers
|
|
***********************************************************************/
|
|
|
|
vint HttpRequestConnection::CurrentCallbackDepth(Ptr<Lifecycle> state)
|
|
{
|
|
vint depth = 0;
|
|
for (auto frame = currentCallbackFrame; frame; frame = frame->previous)
|
|
{
|
|
if (frame->state.Obj() == state.Obj()) depth++;
|
|
}
|
|
return depth;
|
|
}
|
|
|
|
vint HttpRequestConnection::CurrentSocketCallbackDepth(Ptr<Lifecycle> state)
|
|
{
|
|
vint depth = 0;
|
|
for (auto frame = currentSocketCallbackFrame; frame; frame = frame->previous)
|
|
{
|
|
if (frame->state.Obj() == state.Obj()) depth++;
|
|
}
|
|
return depth;
|
|
}
|
|
|
|
vint HttpRequestConnection::CurrentTimeoutCallbackDepth(Ptr<Lifecycle> state)
|
|
{
|
|
vint depth = 0;
|
|
for (auto frame = currentTimeoutCallbackFrame; frame; frame = frame->previous)
|
|
{
|
|
if (frame->state.Obj() == state.Obj()) depth++;
|
|
}
|
|
return depth;
|
|
}
|
|
|
|
void HttpRequestConnection::FinishSocketCall(Ptr<Lifecycle> state)
|
|
{
|
|
bool completePeerStop = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->activeSocketCalls--;
|
|
completePeerStop = state->activeSocketCalls == 0 && state->peerDisconnected && !state->stopFinished;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
if (completePeerStop)
|
|
{
|
|
StopConnection(state);
|
|
}
|
|
|
|
Lifecycle::RetainedAdapterRelease releasing;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->TakeRetainedAdapterIfDrained(releasing);
|
|
}
|
|
}
|
|
|
|
template<typename TCallback>
|
|
void HttpRequestConnection::InvokeHttpCallback(Ptr<Lifecycle> state, bool allowTerminal, TCallback&& invoke)
|
|
{
|
|
IHttpRequestCallback* installed = nullptr;
|
|
auto callbackDepth = CurrentCallbackDepth(state);
|
|
state->lockState.Enter();
|
|
while (state->callbackInstalling && callbackDepth == 0 && state->callback)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
if (state->callback && (allowTerminal || (!state->stopStarted && !state->terminal)))
|
|
{
|
|
installed = state->callback;
|
|
state->activeCallbacks++;
|
|
}
|
|
state->lockState.Leave();
|
|
if (installed)
|
|
{
|
|
CallbackFrame frame(state);
|
|
invoke(installed);
|
|
}
|
|
}
|
|
|
|
void HttpRequestConnection::SubmitWrite(Ptr<Lifecycle> state, IAsyncSocketConnection* connection, Ptr<AsyncSocketBuffer> buffer)
|
|
{
|
|
try
|
|
{
|
|
connection->WriteAsync(buffer);
|
|
}
|
|
catch (...)
|
|
{
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (state->pendingWrite.Obj() == buffer.Obj())
|
|
{
|
|
state->pendingWrite = nullptr;
|
|
state->writePending = false;
|
|
if (state->direction == HttpRequestConnectionDirection::Client)
|
|
{
|
|
state->exchangeActive = false;
|
|
state->closeAfterExchange = false;
|
|
}
|
|
}
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
FinishSocketCall(state);
|
|
throw;
|
|
}
|
|
FinishSocketCall(state);
|
|
}
|
|
|
|
void HttpRequestConnection::ReportRequestFailure(Ptr<Lifecycle> state, HttpRequestFailure failure, bool timeoutOnly, bool reserved)
|
|
{
|
|
bool report = false;
|
|
bool cancelTimeout = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (
|
|
reserved &&
|
|
state->direction == HttpRequestConnectionDirection::Server &&
|
|
!state->stopStarted &&
|
|
!state->terminal &&
|
|
state->parserFailed &&
|
|
state->awaitingResponse
|
|
)
|
|
{
|
|
cancelTimeout = state->timeoutArmed;
|
|
state->timeoutArmed = false;
|
|
report = true;
|
|
}
|
|
else if (
|
|
state->direction == HttpRequestConnectionDirection::Server &&
|
|
!state->stopStarted &&
|
|
!state->terminal &&
|
|
!state->parserFailed &&
|
|
!state->awaitingResponse &&
|
|
(!timeoutOnly || state->timeoutArmed)
|
|
)
|
|
{
|
|
state->parserFailed = true;
|
|
state->awaitingResponse = true;
|
|
state->closeAfterExchange = true;
|
|
state->receiveBuffer.Clear();
|
|
cancelTimeout = state->timeoutArmed;
|
|
state->timeoutArmed = false;
|
|
report = true;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
if (!report)
|
|
{
|
|
return;
|
|
}
|
|
if (cancelTimeout)
|
|
{
|
|
state->timeoutController->CancelAndWait();
|
|
}
|
|
|
|
try
|
|
{
|
|
InvokeHttpCallback(state, false, [&](IHttpRequestCallback* installed)
|
|
{
|
|
installed->OnReadRequestFailure(failure);
|
|
});
|
|
}
|
|
catch (...)
|
|
{
|
|
StopConnection(state);
|
|
throw;
|
|
}
|
|
|
|
bool closeWithoutResponse = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
closeWithoutResponse = !state->stopStarted && !state->terminal && !state->peerDisconnected && !state->writePending;
|
|
}
|
|
if (closeWithoutResponse)
|
|
{
|
|
StopConnection(state);
|
|
}
|
|
}
|
|
|
|
void HttpRequestConnection::InstallTimeout(Ptr<Lifecycle> state, vint milliseconds, const WString& error)
|
|
{
|
|
CHECK_ERROR(milliseconds > 0, L"HttpRequestConnection requires a positive timeout when arming a deadline.");
|
|
auto captured = state;
|
|
auto capturedError = error;
|
|
try
|
|
{
|
|
state->timeoutController->Arm(milliseconds, Func<void()>([captured, capturedError]()
|
|
{
|
|
TimeoutCallbackFrame timeoutCallbackFrame(captured);
|
|
if (captured->direction == HttpRequestConnectionDirection::Server)
|
|
{
|
|
ReportRequestFailure(captured, HttpRequestFailure::RequestTimeout, true);
|
|
return;
|
|
}
|
|
|
|
bool expired = false;
|
|
CS_LOCK(captured->lockState)
|
|
{
|
|
if (captured->timeoutArmed && !captured->stopStarted && !captured->terminal && !captured->parserFailed)
|
|
{
|
|
captured->timeoutArmed = false;
|
|
captured->parserFailed = true;
|
|
expired = true;
|
|
}
|
|
}
|
|
if (expired)
|
|
{
|
|
ReportFatalError(captured, capturedError);
|
|
}
|
|
}));
|
|
bool cancelStaleTimeout = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
cancelStaleTimeout = !state->timeoutArmed || state->stopStarted || state->terminal;
|
|
}
|
|
if (cancelStaleTimeout)
|
|
{
|
|
state->timeoutController->CancelAndWait();
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->timeoutArmed = false;
|
|
}
|
|
ReportFatalError(state, L"The HTTP timeout controller failed while arming a message timeout.");
|
|
}
|
|
}
|
|
|
|
void HttpRequestConnection::RefreshTimeout(Ptr<Lifecycle> state)
|
|
{
|
|
try
|
|
{
|
|
state->timeoutController->Refresh();
|
|
}
|
|
catch (...)
|
|
{
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->timeoutArmed = false;
|
|
}
|
|
ReportFatalError(state, L"The HTTP timeout controller failed while refreshing a message timeout.");
|
|
}
|
|
}
|
|
|
|
void HttpRequestConnection::ReportResponseFailure(Ptr<Lifecycle> state, HttpResponseFailure failure)
|
|
{
|
|
bool report = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (!state->terminal && !state->stopStarted)
|
|
{
|
|
state->terminal = true;
|
|
state->parserFailed = true;
|
|
state->timeoutArmed = false;
|
|
state->pendingWrite = nullptr;
|
|
state->writePending = false;
|
|
state->heldResponse = nullptr;
|
|
state->fatalAfterResponse = false;
|
|
state->responseDelivering = false;
|
|
state->deferredRequestWrite = nullptr;
|
|
state->deferredRequestClose = false;
|
|
state->deferredRequestMethod = L"";
|
|
report = true;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
if (report)
|
|
{
|
|
state->timeoutController->CancelAndWait();
|
|
try
|
|
{
|
|
InvokeHttpCallback(state, true, [&](IHttpRequestCallback* installed)
|
|
{
|
|
installed->OnReadResponseFailure(failure);
|
|
});
|
|
}
|
|
catch (...)
|
|
{
|
|
StopConnection(state);
|
|
throw;
|
|
}
|
|
StopConnection(state);
|
|
}
|
|
}
|
|
|
|
void HttpRequestConnection::DeliverResponse(Ptr<Lifecycle> state, Ptr<HttpResponse> response, bool closeAfterDelivery)
|
|
{
|
|
if (state->responseNotFoundIsFatal && response->statusCode == (vint)HttpResponseFailure::NotFound)
|
|
{
|
|
ReportResponseFailure(state, HttpResponseFailure::NotFound);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
InvokeHttpCallback(state, false, [&](IHttpRequestCallback* installed)
|
|
{
|
|
installed->OnReadResponse(response);
|
|
});
|
|
}
|
|
catch (...)
|
|
{
|
|
bool notifyDisconnected = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->responseDelivering = false;
|
|
state->deferredRequestWrite = nullptr;
|
|
state->deferredRequestClose = false;
|
|
state->deferredRequestMethod = L"";
|
|
notifyDisconnected = state->peerDisconnected;
|
|
state->responseFinalizing = notifyDisconnected;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
if (notifyDisconnected)
|
|
{
|
|
StopConnection(state);
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->responseFinalizing = false;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
throw;
|
|
}
|
|
|
|
bool fatalAfterDelivery = false;
|
|
bool notifyDisconnected = false;
|
|
Ptr<AsyncSocketBuffer> deferredRequestWrite;
|
|
IAsyncSocketConnection* deferredRequestConnection = nullptr;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
fatalAfterDelivery = state->fatalAfterResponse;
|
|
state->fatalAfterResponse = false;
|
|
notifyDisconnected = state->peerDisconnected;
|
|
state->responseDelivering = false;
|
|
state->responseFinalizing = fatalAfterDelivery || closeAfterDelivery || notifyDisconnected;
|
|
if (state->responseFinalizing)
|
|
{
|
|
state->deferredRequestWrite = nullptr;
|
|
state->deferredRequestClose = false;
|
|
state->deferredRequestMethod = L"";
|
|
}
|
|
else if (state->deferredRequestWrite && !state->stopStarted && !state->terminal && state->socketConnection)
|
|
{
|
|
deferredRequestWrite = std::move(state->deferredRequestWrite);
|
|
state->exchangeActive = true;
|
|
state->closeAfterExchange = state->deferredRequestClose;
|
|
state->deferredRequestClose = false;
|
|
state->activeRequestMethod = std::move(state->deferredRequestMethod);
|
|
state->activeResponseTimeout = state->deferredResponseTimeout;
|
|
state->pendingWrite = deferredRequestWrite;
|
|
state->writePending = true;
|
|
deferredRequestConnection = state->socketConnection;
|
|
state->activeSocketCalls++;
|
|
}
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
|
|
try
|
|
{
|
|
if (fatalAfterDelivery)
|
|
{
|
|
ReportFatalError(state, L"The HTTP client received an unsolicited response after its exchange completed.");
|
|
}
|
|
else if (notifyDisconnected)
|
|
{
|
|
StopConnection(state);
|
|
}
|
|
else if (closeAfterDelivery)
|
|
{
|
|
StopConnection(state);
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->responseFinalizing = false;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
throw;
|
|
}
|
|
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->responseFinalizing = false;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
if (!fatalAfterDelivery && !closeAfterDelivery && !notifyDisconnected)
|
|
{
|
|
if (deferredRequestWrite)
|
|
{
|
|
try
|
|
{
|
|
SubmitWrite(state, deferredRequestConnection, deferredRequestWrite);
|
|
}
|
|
catch (...)
|
|
{
|
|
ReportFatalError(state, L"The HTTP client failed to submit a deferred request write.");
|
|
return;
|
|
}
|
|
}
|
|
ProcessBufferedInput(state);
|
|
}
|
|
}
|
|
|
|
void HttpRequestConnection::ProcessBufferedInput(Ptr<Lifecycle> state)
|
|
{
|
|
Ptr<HttpRequest> request;
|
|
Ptr<HttpResponse> response;
|
|
bool cancelTimeout = false;
|
|
bool deliverRequest = false;
|
|
bool deliverResponse = false;
|
|
bool closeAfterDelivery = false;
|
|
bool requestFailure = false;
|
|
bool clientFailure = false;
|
|
HttpRequestFailure failure = HttpRequestFailure::BadRequest;
|
|
WString parsedRequestMethod;
|
|
|
|
state->lockState.Enter();
|
|
if (state->stopStarted || state->terminal || state->parserFailed || state->peerDisconnected)
|
|
{
|
|
state->lockState.Leave();
|
|
return;
|
|
}
|
|
auto parseEnabled = state->direction == HttpRequestConnectionDirection::Server
|
|
? !state->awaitingResponse
|
|
: state->exchangeActive && !state->heldResponse && !state->responseDelivering && !state->responseFinalizing;
|
|
if (!parseEnabled)
|
|
{
|
|
state->lockState.Leave();
|
|
return;
|
|
}
|
|
if (state->receiveBuffer.Count() == 0)
|
|
{
|
|
state->lockState.Leave();
|
|
return;
|
|
}
|
|
|
|
vint consumedBytes = 0;
|
|
bool messageClose = false;
|
|
auto result = ParseHttpMessage(
|
|
&state->receiveBuffer[0],
|
|
state->receiveBuffer.Count(),
|
|
state->direction == HttpRequestConnectionDirection::Server,
|
|
state->activeRequestMethod,
|
|
parsedRequestMethod,
|
|
request,
|
|
response,
|
|
consumedBytes,
|
|
messageClose
|
|
);
|
|
if (result == HttpMessageParsingResult::Incomplete)
|
|
{
|
|
bool armTimeout = false;
|
|
bool refreshTimeout = false;
|
|
auto serverSide = state->direction == HttpRequestConnectionDirection::Server;
|
|
if (serverSide && parsedRequestMethod.Length() > 0)
|
|
{
|
|
state->activeRequestMethod = parsedRequestMethod;
|
|
}
|
|
auto timeout = serverSide ? HttpIncompleteMessageTimeout : state->activeResponseTimeout;
|
|
auto canArm = timeout > 0 && (serverSide || !state->writePending);
|
|
if (canArm && !state->timeoutArmed)
|
|
{
|
|
state->timeoutArmed = true;
|
|
armTimeout = true;
|
|
}
|
|
else if (canArm && serverSide)
|
|
{
|
|
refreshTimeout = true;
|
|
}
|
|
state->lockState.Leave();
|
|
if (armTimeout)
|
|
{
|
|
InstallTimeout(state, timeout, L"The HTTP peer timed out while sending an incomplete message.");
|
|
}
|
|
else if (refreshTimeout)
|
|
{
|
|
RefreshTimeout(state);
|
|
}
|
|
return;
|
|
}
|
|
if (result != HttpMessageParsingResult::Succeeded)
|
|
{
|
|
if (state->direction == HttpRequestConnectionDirection::Server)
|
|
{
|
|
state->activeRequestMethod = parsedRequestMethod;
|
|
state->parserFailed = true;
|
|
state->awaitingResponse = true;
|
|
state->closeAfterExchange = true;
|
|
state->receiveBuffer.Clear();
|
|
failure = GetHttpRequestFailure(result);
|
|
requestFailure = true;
|
|
}
|
|
else
|
|
{
|
|
clientFailure = true;
|
|
}
|
|
state->lockState.Leave();
|
|
}
|
|
else
|
|
{
|
|
state->receiveBuffer.RemoveRange(0, consumedBytes);
|
|
if (state->timeoutArmed)
|
|
{
|
|
state->timeoutArmed = false;
|
|
cancelTimeout = true;
|
|
}
|
|
state->closeAfterExchange |= messageClose;
|
|
if (state->direction == HttpRequestConnectionDirection::Server)
|
|
{
|
|
state->activeRequestMethod = request->method;
|
|
state->awaitingResponse = true;
|
|
deliverRequest = true;
|
|
}
|
|
else if (state->writePending)
|
|
{
|
|
state->heldResponse = response;
|
|
if (state->receiveBuffer.Count() > 0)
|
|
{
|
|
state->fatalAfterResponse = true;
|
|
state->receiveBuffer.Clear();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
state->exchangeActive = false;
|
|
state->responseDelivering = true;
|
|
deliverResponse = true;
|
|
closeAfterDelivery = state->closeAfterExchange;
|
|
if (state->receiveBuffer.Count() > 0)
|
|
{
|
|
state->fatalAfterResponse = true;
|
|
state->receiveBuffer.Clear();
|
|
}
|
|
}
|
|
state->lockState.Leave();
|
|
}
|
|
|
|
if (requestFailure)
|
|
{
|
|
ReportRequestFailure(state, failure, false, true);
|
|
return;
|
|
}
|
|
if (clientFailure)
|
|
{
|
|
ReportFatalError(state, L"The HTTP peer sent malformed or unsafe HTTP/1.1 framing.");
|
|
return;
|
|
}
|
|
if (cancelTimeout)
|
|
{
|
|
state->timeoutController->CancelAndWait();
|
|
}
|
|
if (deliverRequest)
|
|
{
|
|
InvokeHttpCallback(state, false, [&](IHttpRequestCallback* installed)
|
|
{
|
|
installed->OnReadRequest(request);
|
|
});
|
|
}
|
|
else if (deliverResponse)
|
|
{
|
|
DeliverResponse(state, response, closeAfterDelivery);
|
|
}
|
|
}
|
|
|
|
void HttpRequestConnection::NotifyDisconnected(Ptr<Lifecycle> state)
|
|
{
|
|
auto callbackDepth = CurrentCallbackDepth(state);
|
|
state->lockState.Enter();
|
|
if (!state->disconnectedNotified)
|
|
{
|
|
state->disconnectedNotified = true;
|
|
state->terminal = true;
|
|
state->pendingWrite = nullptr;
|
|
state->writePending = false;
|
|
state->heldResponse = nullptr;
|
|
state->fatalAfterResponse = false;
|
|
state->deferredRequestWrite = nullptr;
|
|
state->deferredRequestClose = false;
|
|
state->deferredRequestMethod = L"";
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
if (state->disconnectFinished)
|
|
{
|
|
state->lockState.Leave();
|
|
return;
|
|
}
|
|
if (state->disconnectDelivering)
|
|
{
|
|
if (callbackDepth == 0)
|
|
{
|
|
while (!state->disconnectFinished) state->cvState.SleepWith(state->lockState);
|
|
}
|
|
state->lockState.Leave();
|
|
return;
|
|
}
|
|
if (callbackDepth == 0)
|
|
{
|
|
while (state->activeCallbacks > 0 && !state->disconnectDelivering && !state->disconnectFinished)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
if (state->disconnectFinished)
|
|
{
|
|
state->lockState.Leave();
|
|
return;
|
|
}
|
|
}
|
|
state->disconnectDelivering = true;
|
|
while (state->activeCallbacks > callbackDepth)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
state->lockState.Leave();
|
|
|
|
try
|
|
{
|
|
InvokeHttpCallback(state, true, [](IHttpRequestCallback* installed)
|
|
{
|
|
installed->OnDisconnected();
|
|
});
|
|
}
|
|
catch (...)
|
|
{
|
|
Lifecycle::RetainedAdapterRelease releasing;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->callback = nullptr;
|
|
state->disconnectFinished = true;
|
|
state->TakeRetainedAdapterIfDrained(releasing);
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
throw;
|
|
}
|
|
|
|
Lifecycle::RetainedAdapterRelease releasing;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->callback = nullptr;
|
|
while (state->activeCallbacks > callbackDepth)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
state->disconnectFinished = true;
|
|
state->TakeRetainedAdapterIfDrained(releasing);
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
|
|
void HttpRequestConnection::StopConnection(Ptr<Lifecycle> state, Ptr<Object> retainedAdapter)
|
|
{
|
|
auto callbackDepth = CurrentCallbackDepth(state);
|
|
auto socketCallbackDepth = CurrentSocketCallbackDepth(state);
|
|
auto timeoutCallbackDepth = CurrentTimeoutCallbackDepth(state);
|
|
auto nestedCallback = callbackDepth > 0 || socketCallbackDepth > 0;
|
|
IAsyncSocketConnection* connection = nullptr;
|
|
bool executeStop = false;
|
|
bool nestedFollower = false;
|
|
bool timeoutFollower = false;
|
|
Lifecycle::RetainedAdapterRelease releasing;
|
|
|
|
state->lockState.Enter();
|
|
if (retainedAdapter) state->retainedAdapter = retainedAdapter;
|
|
if (!state->stopStarted)
|
|
{
|
|
state->stopStarted = true;
|
|
state->timeoutArmed = false;
|
|
state->pendingWrite = nullptr;
|
|
state->writePending = false;
|
|
state->heldResponse = nullptr;
|
|
state->fatalAfterResponse = false;
|
|
state->deferredRequestWrite = nullptr;
|
|
state->deferredRequestClose = false;
|
|
state->deferredRequestMethod = L"";
|
|
if (!nestedCallback)
|
|
{
|
|
while (state->activeSocketCalls > 0) state->cvState.SleepWith(state->lockState);
|
|
}
|
|
connection = state->peerDisconnected ? nullptr : state->socketConnection;
|
|
executeStop = true;
|
|
}
|
|
else if (nestedCallback)
|
|
{
|
|
connection = state->socketConnection;
|
|
nestedFollower = true;
|
|
}
|
|
else if (timeoutCallbackDepth > 0)
|
|
{
|
|
timeoutFollower = true;
|
|
}
|
|
else
|
|
{
|
|
while (!state->stopFinished) state->cvState.SleepWith(state->lockState);
|
|
while (state->activeCallbacks > 0 || state->activeSocketCallbacks > 0 || state->activeSocketCalls > 0)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
state->TakeRetainedAdapterIfDrained(releasing);
|
|
state->lockState.Leave();
|
|
return;
|
|
}
|
|
state->lockState.Leave();
|
|
|
|
if (nestedFollower)
|
|
{
|
|
if (connection && socketCallbackDepth > 0) connection->Stop();
|
|
NotifyDisconnected(state);
|
|
return;
|
|
}
|
|
if (timeoutFollower)
|
|
{
|
|
return;
|
|
}
|
|
state->timeoutController->CancelAndWait();
|
|
if (executeStop && connection) connection->Stop();
|
|
NotifyDisconnected(state);
|
|
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
while (state->activeCallbacks > callbackDepth || state->activeSocketCallbacks > socketCallbackDepth)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
state->stopFinished = true;
|
|
state->TakeRetainedAdapterIfDrained(releasing);
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
|
|
void HttpRequestConnection::ReportFatalError(Ptr<Lifecycle> state, const WString& error)
|
|
{
|
|
bool report = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (!state->terminal && !state->stopStarted)
|
|
{
|
|
state->terminal = true;
|
|
state->parserFailed = true;
|
|
state->timeoutArmed = false;
|
|
state->pendingWrite = nullptr;
|
|
state->writePending = false;
|
|
state->heldResponse = nullptr;
|
|
state->fatalAfterResponse = false;
|
|
state->deferredRequestWrite = nullptr;
|
|
state->deferredRequestClose = false;
|
|
state->deferredRequestMethod = L"";
|
|
report = true;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
if (report)
|
|
{
|
|
state->timeoutController->CancelAndWait();
|
|
try
|
|
{
|
|
InvokeHttpCallback(state, true, [&](IHttpRequestCallback* installed)
|
|
{
|
|
installed->OnError(error, true);
|
|
});
|
|
}
|
|
catch (...)
|
|
{
|
|
StopConnection(state);
|
|
throw;
|
|
}
|
|
StopConnection(state);
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
HttpRequestConnection
|
|
***********************************************************************/
|
|
|
|
HttpRequestConnection::HttpRequestConnection(
|
|
IAsyncSocketConnection* connection,
|
|
HttpRequestConnectionDirection direction,
|
|
Ptr<HttpRequestCallbackDomain> callbackDomain,
|
|
Ptr<IHttpRequestTimeoutController> timeoutController,
|
|
bool responseNotFoundIsFatal
|
|
)
|
|
: lifecycle(Ptr(new Lifecycle))
|
|
{
|
|
CHECK_ERROR(connection, L"HttpRequestConnection requires a valid async socket connection.");
|
|
lifecycle->socketConnection = connection;
|
|
lifecycle->direction = direction;
|
|
lifecycle->responseNotFoundIsFatal = responseNotFoundIsFatal;
|
|
lifecycle->callbackDomain = callbackDomain ? callbackDomain : Ptr(new HttpRequestCallbackDomain);
|
|
lifecycle->timeoutController = timeoutController ? timeoutController : CreateHttpRequestTimeoutController();
|
|
connection->InstallCallback(this);
|
|
}
|
|
|
|
HttpRequestConnection::~HttpRequestConnection()
|
|
{
|
|
StopConnection(lifecycle);
|
|
}
|
|
|
|
void HttpRequestConnection::RetainUntilStopped(Ptr<HttpRequestConnection> retainedAdapter, const Func<void()>& drainedCallback)
|
|
{
|
|
CHECK_ERROR(retainedAdapter.Obj() == this, L"HttpRequestConnection::RetainUntilStopped requires this connection as its retained adapter.");
|
|
CHECK_ERROR(drainedCallback, L"HttpRequestConnection::RetainUntilStopped requires a drained callback.");
|
|
bool canRetain = false;
|
|
CS_LOCK(lifecycle->lockState)
|
|
{
|
|
if (!lifecycle->retainedAdapter && !lifecycle->drainedCallback && !lifecycle->stopStarted)
|
|
{
|
|
lifecycle->retainedAdapter = retainedAdapter;
|
|
lifecycle->drainedCallback = drainedCallback;
|
|
canRetain = true;
|
|
}
|
|
}
|
|
CHECK_ERROR(canRetain, L"HttpRequestConnection::RetainUntilStopped can only be called once before stopping.");
|
|
}
|
|
|
|
void HttpRequestConnection::StopWithRetainedAdapter(Ptr<HttpRequestConnection> retainedAdapter)
|
|
{
|
|
StopConnection(lifecycle, retainedAdapter);
|
|
}
|
|
|
|
bool HttpRequestConnection::IsInsideCallback()
|
|
{
|
|
return CurrentCallbackDepth(lifecycle) > 0 || CurrentSocketCallbackDepth(lifecycle) > 0;
|
|
}
|
|
|
|
void HttpRequestConnection::InstallCallback(IHttpRequestCallback* value)
|
|
{
|
|
auto state = lifecycle;
|
|
if (!value)
|
|
{
|
|
auto callbackDepth = CurrentCallbackDepth(state);
|
|
bool uninstallOwner = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
uninstallOwner = state->callback != nullptr;
|
|
state->callback = nullptr;
|
|
while ((callbackDepth == 0 || uninstallOwner) && state->activeCallbacks > callbackDepth)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
bool canInstall = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (!state->callback && !state->callbackInstalling && !state->stopStarted && !state->terminal)
|
|
{
|
|
state->callback = value;
|
|
state->callbackInstalling = true;
|
|
state->activeCallbacks++;
|
|
canInstall = true;
|
|
}
|
|
}
|
|
CHECK_ERROR(canInstall, L"HttpRequestConnection::InstallCallback cannot replace a callback or install one on a stopped connection.");
|
|
|
|
CallbackFrame frame(state);
|
|
try
|
|
{
|
|
value->OnInstalled(this);
|
|
}
|
|
catch (...)
|
|
{
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (state->callback == value) state->callback = nullptr;
|
|
state->callbackInstalling = false;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
throw;
|
|
}
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->callbackInstalling = false;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
|
|
void HttpRequestConnection::BeginReadingLoopUnsafe()
|
|
{
|
|
auto state = lifecycle;
|
|
IAsyncSocketConnection* connection = nullptr;
|
|
Ptr<Object> callRetainer;
|
|
bool canBegin = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (!state->readingStarted && !state->stopStarted && !state->terminal && state->socketConnection)
|
|
{
|
|
state->readingStarted = true;
|
|
connection = state->socketConnection;
|
|
callRetainer = state->retainedAdapter;
|
|
state->activeSocketCalls++;
|
|
canBegin = true;
|
|
}
|
|
}
|
|
CHECK_ERROR(canBegin, L"HttpRequestConnection::BeginReadingLoopUnsafe can only be called once on an active connection.");
|
|
try
|
|
{
|
|
connection->BeginReadingLoopUnsafe();
|
|
}
|
|
catch (...)
|
|
{
|
|
FinishSocketCall(state);
|
|
throw;
|
|
}
|
|
FinishSocketCall(state);
|
|
}
|
|
|
|
void HttpRequestConnection::SendRequest(Ptr<HttpRequest> request, vint responseTimeout)
|
|
{
|
|
auto state = lifecycle;
|
|
CHECK_ERROR(state->direction == HttpRequestConnectionDirection::Client, L"HttpRequestConnection::SendRequest is only available on a client connection.");
|
|
CHECK_ERROR(request, L"HttpRequestConnection::SendRequest requires a request.");
|
|
bool requestClose = false;
|
|
auto buffer = SerializeHttpMessage(request.Obj(), nullptr, L"", requestClose);
|
|
IAsyncSocketConnection* connection = nullptr;
|
|
Ptr<Object> callRetainer;
|
|
bool canSend = false;
|
|
bool deferSend = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
auto commonStateAvailable = !state->stopStarted && !state->terminal && !state->peerDisconnected
|
|
&& !state->exchangeActive && !state->writePending && !state->deferredRequestWrite
|
|
&& !state->responseFinalizing && !state->fatalAfterResponse && !state->closeAfterExchange && state->socketConnection;
|
|
if (commonStateAvailable && state->responseDelivering)
|
|
{
|
|
state->deferredRequestWrite = buffer;
|
|
state->deferredRequestClose = requestClose;
|
|
state->deferredRequestMethod = request->method;
|
|
state->deferredResponseTimeout = responseTimeout;
|
|
deferSend = true;
|
|
canSend = true;
|
|
}
|
|
else if (commonStateAvailable && !state->responseDelivering)
|
|
{
|
|
state->exchangeActive = true;
|
|
state->closeAfterExchange = requestClose;
|
|
state->activeRequestMethod = request->method;
|
|
state->activeResponseTimeout = responseTimeout;
|
|
state->pendingWrite = buffer;
|
|
state->writePending = true;
|
|
connection = state->socketConnection;
|
|
callRetainer = state->retainedAdapter;
|
|
state->activeSocketCalls++;
|
|
canSend = true;
|
|
}
|
|
}
|
|
CHECK_ERROR(canSend, L"HttpRequestConnection::SendRequest requires an idle active client connection.");
|
|
if (deferSend) return;
|
|
SubmitWrite(state, connection, buffer);
|
|
ProcessBufferedInput(state);
|
|
}
|
|
|
|
void HttpRequestConnection::SendResponse(Ptr<HttpResponse> response)
|
|
{
|
|
auto state = lifecycle;
|
|
CHECK_ERROR(state->direction == HttpRequestConnectionDirection::Server, L"HttpRequestConnection::SendResponse is only available on a server connection.");
|
|
CHECK_ERROR(response, L"HttpRequestConnection::SendResponse requires a response.");
|
|
WString responseToMethod;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
responseToMethod = state->activeRequestMethod;
|
|
}
|
|
bool responseClose = false;
|
|
auto buffer = SerializeHttpMessage(nullptr, response.Obj(), responseToMethod, responseClose);
|
|
IAsyncSocketConnection* connection = nullptr;
|
|
Ptr<Object> callRetainer;
|
|
bool canSend = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (!state->stopStarted && !state->terminal && state->awaitingResponse && !state->writePending && state->socketConnection)
|
|
{
|
|
state->closeAfterExchange |= responseClose;
|
|
state->pendingWrite = buffer;
|
|
state->writePending = true;
|
|
connection = state->socketConnection;
|
|
callRetainer = state->retainedAdapter;
|
|
state->activeSocketCalls++;
|
|
canSend = true;
|
|
}
|
|
}
|
|
CHECK_ERROR(canSend, L"HttpRequestConnection::SendResponse requires one delivered request without a response in progress.");
|
|
SubmitWrite(state, connection, buffer);
|
|
}
|
|
|
|
void HttpRequestConnection::Stop()
|
|
{
|
|
StopConnection(lifecycle);
|
|
}
|
|
|
|
void HttpRequestConnection::OnRead(const vuint8_t* buffer, vint size)
|
|
{
|
|
auto state = lifecycle;
|
|
SocketCallbackFrame frame(state);
|
|
if (!buffer || size <= 0) return;
|
|
bool tooLarge = false;
|
|
bool requestTooLarge = false;
|
|
bool unsolicited = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (state->stopStarted || state->terminal || state->parserFailed || state->peerDisconnected) return;
|
|
if (state->direction == HttpRequestConnectionDirection::Client && state->heldResponse)
|
|
{
|
|
state->fatalAfterResponse = true;
|
|
}
|
|
else if (state->direction == HttpRequestConnectionDirection::Client && (state->responseDelivering || state->responseFinalizing) && !state->exchangeActive)
|
|
{
|
|
state->fatalAfterResponse = true;
|
|
}
|
|
else if (state->direction == HttpRequestConnectionDirection::Client && !state->exchangeActive)
|
|
{
|
|
state->parserFailed = true;
|
|
unsolicited = true;
|
|
}
|
|
else if (size > HttpWireMessageSizeLimit - state->receiveBuffer.Count())
|
|
{
|
|
if (state->direction == HttpRequestConnectionDirection::Server)
|
|
{
|
|
state->parserFailed = true;
|
|
state->awaitingResponse = true;
|
|
state->closeAfterExchange = true;
|
|
state->receiveBuffer.Clear();
|
|
requestTooLarge = true;
|
|
}
|
|
else
|
|
{
|
|
state->parserFailed = true;
|
|
tooLarge = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (vint i = 0; i < size; i++) state->receiveBuffer.Add(buffer[i]);
|
|
}
|
|
}
|
|
if (unsolicited)
|
|
{
|
|
ReportFatalError(state, L"The HTTP client received bytes without an active request exchange.");
|
|
return;
|
|
}
|
|
if (tooLarge)
|
|
{
|
|
ReportFatalError(state, L"The HTTP peer exceeded the configured wire-message size limit.");
|
|
return;
|
|
}
|
|
if (requestTooLarge)
|
|
{
|
|
ReportRequestFailure(state, HttpRequestFailure::PayloadTooLarge, false, true);
|
|
return;
|
|
}
|
|
ProcessBufferedInput(state);
|
|
}
|
|
|
|
void HttpRequestConnection::OnWriteCompleted(Ptr<AsyncSocketBuffer> buffer)
|
|
{
|
|
auto state = lifecycle;
|
|
SocketCallbackFrame frame(state);
|
|
bool mismatched = false;
|
|
bool ignored = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (!state->writePending || state->pendingWrite.Obj() != buffer.Obj())
|
|
{
|
|
if (state->stopStarted || state->terminal || state->peerDisconnected)
|
|
{
|
|
ignored = true;
|
|
}
|
|
else
|
|
{
|
|
mismatched = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
state->pendingWrite = nullptr;
|
|
}
|
|
}
|
|
if (ignored) return;
|
|
CHECK_ERROR(!mismatched, L"HttpRequestConnection received a completion for an unexpected async socket buffer.");
|
|
|
|
InvokeHttpCallback(state, false, [](IHttpRequestCallback* installed)
|
|
{
|
|
installed->OnWriteCompleted();
|
|
});
|
|
|
|
Ptr<HttpResponse> response;
|
|
bool serverSide = false;
|
|
bool closeAfterDelivery = false;
|
|
bool armTimeout = false;
|
|
vint responseTimeout = 0;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (state->stopStarted || state->terminal) return;
|
|
state->writePending = false;
|
|
serverSide = state->direction == HttpRequestConnectionDirection::Server;
|
|
if (serverSide)
|
|
{
|
|
state->awaitingResponse = false;
|
|
state->activeRequestMethod = L"";
|
|
closeAfterDelivery = state->closeAfterExchange;
|
|
}
|
|
else if (state->heldResponse)
|
|
{
|
|
response = std::move(state->heldResponse);
|
|
state->exchangeActive = false;
|
|
state->responseDelivering = true;
|
|
closeAfterDelivery = state->closeAfterExchange;
|
|
}
|
|
else if (!state->peerDisconnected && !state->timeoutArmed && state->activeResponseTimeout > 0)
|
|
{
|
|
state->timeoutArmed = true;
|
|
responseTimeout = state->activeResponseTimeout;
|
|
armTimeout = true;
|
|
}
|
|
}
|
|
|
|
if (response)
|
|
{
|
|
DeliverResponse(state, response, closeAfterDelivery);
|
|
}
|
|
else if (closeAfterDelivery)
|
|
{
|
|
StopConnection(state);
|
|
}
|
|
else if (serverSide)
|
|
{
|
|
ProcessBufferedInput(state);
|
|
}
|
|
else if (armTimeout)
|
|
{
|
|
InstallTimeout(state, responseTimeout, L"The HTTP peer timed out before sending a response header.");
|
|
}
|
|
}
|
|
|
|
void HttpRequestConnection::OnError(const WString& error, bool fatal)
|
|
{
|
|
auto state = lifecycle;
|
|
SocketCallbackFrame frame(state);
|
|
if (fatal)
|
|
{
|
|
ReportFatalError(state, error);
|
|
}
|
|
else
|
|
{
|
|
InvokeHttpCallback(state, false, [&](IHttpRequestCallback* installed)
|
|
{
|
|
installed->OnError(error, false);
|
|
});
|
|
}
|
|
}
|
|
|
|
void HttpRequestConnection::OnConnected()
|
|
{
|
|
auto state = lifecycle;
|
|
SocketCallbackFrame frame(state);
|
|
InvokeHttpCallback(state, false, [](IHttpRequestCallback* installed)
|
|
{
|
|
installed->OnConnected();
|
|
});
|
|
}
|
|
|
|
void HttpRequestConnection::OnDisconnected()
|
|
{
|
|
auto state = lifecycle;
|
|
SocketCallbackFrame frame(state);
|
|
IAsyncSocketConnection* connection = nullptr;
|
|
bool incomplete = false;
|
|
bool surplusAfterHeldResponse = false;
|
|
bool deferDisconnected = false;
|
|
bool cancelTimeout = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->peerDisconnected = true;
|
|
state->timeoutArmed = false;
|
|
cancelTimeout = !state->stopStarted;
|
|
connection = state->socketConnection;
|
|
if (!state->terminal && !state->stopStarted && state->direction == HttpRequestConnectionDirection::Client && state->heldResponse)
|
|
{
|
|
surplusAfterHeldResponse = state->fatalAfterResponse;
|
|
incomplete = !surplusAfterHeldResponse;
|
|
state->terminal = true;
|
|
state->pendingWrite = nullptr;
|
|
state->writePending = false;
|
|
}
|
|
else if (state->direction == HttpRequestConnectionDirection::Client && (state->responseDelivering || state->responseFinalizing))
|
|
{
|
|
deferDisconnected = true;
|
|
}
|
|
else
|
|
{
|
|
incomplete = !state->terminal && !state->stopStarted && (
|
|
state->direction == HttpRequestConnectionDirection::Client
|
|
? state->exchangeActive
|
|
: !state->awaitingResponse && state->receiveBuffer.Count() > 0
|
|
);
|
|
state->terminal = true;
|
|
}
|
|
}
|
|
if (connection)
|
|
{
|
|
connection->InstallCallback(nullptr);
|
|
}
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (state->socketConnection == connection) state->socketConnection = nullptr;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
if (cancelTimeout)
|
|
{
|
|
state->timeoutController->CancelAndWait();
|
|
}
|
|
if (deferDisconnected)
|
|
{
|
|
return;
|
|
}
|
|
if (surplusAfterHeldResponse)
|
|
{
|
|
InvokeHttpCallback(state, true, [](IHttpRequestCallback* installed)
|
|
{
|
|
installed->OnError(L"The HTTP client received an unsolicited response after its exchange completed.", true);
|
|
});
|
|
}
|
|
else if (incomplete)
|
|
{
|
|
InvokeHttpCallback(state, true, [](IHttpRequestCallback* installed)
|
|
{
|
|
installed->OnError(L"The HTTP peer disconnected while a message was incomplete.", true);
|
|
});
|
|
}
|
|
NotifyDisconnected(state);
|
|
StopConnection(state);
|
|
}
|
|
|
|
void HttpRequestConnection::OnInstalled(IAsyncSocketConnection* connection)
|
|
{
|
|
auto state = lifecycle;
|
|
SocketCallbackFrame frame(state);
|
|
CHECK_ERROR(connection == state->socketConnection, L"HttpRequestConnection was installed on an unexpected async socket connection.");
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPREQUESTCLIENT.CPP
|
|
***********************************************************************/
|
|
|
|
namespace vl::inter_process::async_tcp_socket
|
|
{
|
|
/***********************************************************************
|
|
HttpRequestClient::Impl
|
|
***********************************************************************/
|
|
|
|
class HttpRequestClient::Impl : public Object
|
|
{
|
|
private:
|
|
class DeferredClientRelease : public Object
|
|
{
|
|
private:
|
|
CriticalSection lockState;
|
|
Ptr<DeferredClientRelease> selfReference;
|
|
Ptr<IAsyncSocketClient> retainedClient;
|
|
|
|
public:
|
|
DeferredClientRelease(Ptr<IAsyncSocketClient> client)
|
|
: retainedClient(client)
|
|
{
|
|
}
|
|
|
|
void InitializeSelf(Ptr<DeferredClientRelease> self)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
selfReference = self;
|
|
}
|
|
}
|
|
|
|
void Run()
|
|
{
|
|
Ptr<IAsyncSocketClient> client;
|
|
CS_LOCK(lockState)
|
|
{
|
|
client = retainedClient;
|
|
}
|
|
try
|
|
{
|
|
client->GetConnection()->Stop();
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
CS_LOCK(lockState)
|
|
{
|
|
retainedClient = nullptr;
|
|
selfReference = nullptr;
|
|
}
|
|
}
|
|
};
|
|
|
|
Ptr<IAsyncSocketClient> client;
|
|
Ptr<HttpRequestConnection> connection;
|
|
|
|
static void QueueDeferredRelease(Ptr<DeferredClientRelease> deferredRelease)
|
|
{
|
|
auto finalize = Func<void()>([deferredRelease]()
|
|
{
|
|
deferredRelease->Run();
|
|
});
|
|
if (!ThreadPoolLite::Queue(finalize))
|
|
{
|
|
// The holder self-reference keeps the native client alive if the
|
|
// operating system cannot start either asynchronous cleanup path.
|
|
if (!Thread::CreateAndStart(finalize)) return;
|
|
}
|
|
}
|
|
|
|
public:
|
|
Impl(Ptr<IAsyncSocketClient> _client)
|
|
: client(_client)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::HttpRequestClient::HttpRequestClient(Ptr<IAsyncSocketClient>)#"
|
|
CHECK_ERROR(client, ERROR_MESSAGE_PREFIX L"Requires a client.");
|
|
connection = Ptr(new HttpRequestConnection(
|
|
client->GetConnection(),
|
|
HttpRequestConnectionDirection::Client,
|
|
nullptr,
|
|
nullptr,
|
|
true
|
|
));
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
~Impl()
|
|
{
|
|
auto deferFinalization = connection->IsInsideCallback();
|
|
Ptr<DeferredClientRelease> deferredRelease;
|
|
if (deferFinalization)
|
|
{
|
|
deferredRelease = Ptr(new DeferredClientRelease(client));
|
|
deferredRelease->InitializeSelf(deferredRelease);
|
|
}
|
|
connection->StopWithRetainedAdapter(connection);
|
|
if (deferFinalization)
|
|
{
|
|
QueueDeferredRelease(deferredRelease);
|
|
}
|
|
}
|
|
|
|
IHttpRequestConnection* GetConnection()
|
|
{
|
|
return connection.Obj();
|
|
}
|
|
|
|
void WaitForServer()
|
|
{
|
|
client->WaitForServer();
|
|
}
|
|
|
|
ClientStatus GetStatus()
|
|
{
|
|
return client->GetStatus();
|
|
}
|
|
};
|
|
|
|
/***********************************************************************
|
|
HttpRequestClient
|
|
***********************************************************************/
|
|
|
|
HttpRequestClient::HttpRequestClient(Ptr<IAsyncSocketClient> client)
|
|
: impl(Ptr(new Impl(client)))
|
|
{
|
|
}
|
|
|
|
HttpRequestClient::~HttpRequestClient()
|
|
{
|
|
}
|
|
|
|
IHttpRequestConnection* HttpRequestClient::GetConnection()
|
|
{
|
|
return impl->GetConnection();
|
|
}
|
|
|
|
void HttpRequestClient::WaitForServer()
|
|
{
|
|
impl->WaitForServer();
|
|
}
|
|
|
|
ClientStatus HttpRequestClient::GetStatus()
|
|
{
|
|
return impl->GetStatus();
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPREQUESTSERVER.CPP
|
|
***********************************************************************/
|
|
|
|
namespace vl::inter_process::async_tcp_socket
|
|
{
|
|
/***********************************************************************
|
|
HttpRequestServer::Impl
|
|
***********************************************************************/
|
|
|
|
class HttpRequestServer::Impl : public Object
|
|
{
|
|
private:
|
|
class Lifecycle : public Object
|
|
{
|
|
public:
|
|
HttpRequestServer* owner = nullptr;
|
|
Ptr<IAsyncSocketServer> server;
|
|
Ptr<HttpRequestCallbackDomain> callbackDomain = Ptr(new HttpRequestCallbackDomain);
|
|
Func<Ptr<IHttpRequestTimeoutController>()>
|
|
timeoutControllerFactory;
|
|
|
|
// covers owner, connections, and all lifecycle flags below
|
|
CriticalSection lockState;
|
|
ConditionVariable cvState;
|
|
collections::List<Ptr<HttpRequestConnection>>
|
|
connections;
|
|
bool startCalled = false;
|
|
bool stopStarted = false;
|
|
bool unexpectedStopNotified = false;
|
|
bool stopFinished = false;
|
|
bool nativeStopCalling = false;
|
|
bool destroyStarted = false;
|
|
bool destroyAdaptersRetained = false;
|
|
|
|
Lifecycle(
|
|
HttpRequestServer* _owner,
|
|
Ptr<IAsyncSocketServer> _server,
|
|
const Func<Ptr<IHttpRequestTimeoutController>()>& _timeoutControllerFactory
|
|
)
|
|
: owner(_owner)
|
|
, server(_server)
|
|
, timeoutControllerFactory(_timeoutControllerFactory)
|
|
{
|
|
}
|
|
};
|
|
|
|
class SocketServerCallback
|
|
: public Object
|
|
, public virtual IAsyncSocketServerCallback
|
|
{
|
|
private:
|
|
Ptr<Lifecycle> lifecycle;
|
|
CriticalSection lockSelf;
|
|
Ptr<SocketServerCallback> selfReference;
|
|
|
|
public:
|
|
SocketServerCallback(Ptr<Lifecycle> _lifecycle)
|
|
: lifecycle(_lifecycle)
|
|
{
|
|
}
|
|
|
|
void InitializeSelf(Ptr<SocketServerCallback> self)
|
|
{
|
|
CS_LOCK(lockSelf)
|
|
{
|
|
selfReference = self;
|
|
}
|
|
}
|
|
|
|
void ReleaseSelfReference()
|
|
{
|
|
CS_LOCK(lockSelf)
|
|
{
|
|
selfReference = nullptr;
|
|
}
|
|
}
|
|
|
|
WaitForClientResult OnClientConnected(IAsyncSocketConnection* connection) override
|
|
{
|
|
Ptr<SocketServerCallback> self;
|
|
CS_LOCK(lockSelf)
|
|
{
|
|
self = selfReference;
|
|
}
|
|
return self
|
|
? Impl::OnSocketClientConnected(lifecycle, connection)
|
|
: WaitForClientResult::Reject;
|
|
}
|
|
|
|
void OnServerStopped() override
|
|
{
|
|
Ptr<SocketServerCallback> self;
|
|
CS_LOCK(lockSelf)
|
|
{
|
|
self = selfReference;
|
|
}
|
|
if (self)
|
|
{
|
|
Impl::OnSocketServerStopped(lifecycle);
|
|
}
|
|
}
|
|
};
|
|
|
|
Ptr<Lifecycle> lifecycle;
|
|
Ptr<SocketServerCallback> callback;
|
|
|
|
static void StopConnections(Ptr<Lifecycle> state, bool retainAdapters = false)
|
|
{
|
|
collections::List<Ptr<HttpRequestConnection>> stoppingConnections;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
for (auto connection : state->connections)
|
|
{
|
|
stoppingConnections.Add(connection);
|
|
}
|
|
}
|
|
for (auto connection : stoppingConnections)
|
|
{
|
|
if (retainAdapters)
|
|
{
|
|
connection->StopWithRetainedAdapter(connection);
|
|
}
|
|
else
|
|
{
|
|
connection->Stop();
|
|
}
|
|
}
|
|
}
|
|
|
|
static void FinalizeDestroyedOwner(Ptr<Lifecycle> state)
|
|
{
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (state->destroyStarted && state->destroyAdaptersRetained)
|
|
{
|
|
state->owner = nullptr;
|
|
state->connections.Clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
static void QueueDeferredStop(Ptr<Lifecycle> state, Ptr<SocketServerCallback> retainedCallback)
|
|
{
|
|
auto finalize = Func<void()>([state, retainedCallback]()
|
|
{
|
|
state->lockState.Enter();
|
|
while (!state->stopFinished || state->nativeStopCalling)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
state->nativeStopCalling = true;
|
|
state->lockState.Leave();
|
|
|
|
try
|
|
{
|
|
state->server->Stop();
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
try
|
|
{
|
|
StopConnections(state);
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
state->callbackDomain->WaitForCallbacks(0);
|
|
FinalizeDestroyedOwner(state);
|
|
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->nativeStopCalling = false;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
retainedCallback->ReleaseSelfReference();
|
|
});
|
|
if (!ThreadPoolLite::Queue(finalize))
|
|
{
|
|
// The callback self-reference keeps all deferred state alive if the
|
|
// operating system cannot start either asynchronous cleanup path.
|
|
if (!Thread::CreateAndStart(finalize)) return;
|
|
}
|
|
}
|
|
|
|
static WaitForClientResult OnSocketClientConnected(Ptr<Lifecycle> state, IAsyncSocketConnection* connection)
|
|
{
|
|
HttpRequestCallbackDomain::CallbackFrame callbackFrame(state->callbackDomain);
|
|
HttpRequestServer* owner = nullptr;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (!state->stopStarted)
|
|
{
|
|
owner = state->owner;
|
|
}
|
|
}
|
|
if (!owner)
|
|
{
|
|
return WaitForClientResult::Reject;
|
|
}
|
|
|
|
Ptr<IHttpRequestTimeoutController> timeoutController;
|
|
try
|
|
{
|
|
if (state->timeoutControllerFactory)
|
|
{
|
|
timeoutController = state->timeoutControllerFactory();
|
|
CHECK_ERROR(timeoutController, L"The HTTP request timeout controller factory returned null.");
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
return WaitForClientResult::Reject;
|
|
}
|
|
|
|
auto httpConnection = Ptr(new HttpRequestConnection(
|
|
connection,
|
|
HttpRequestConnectionDirection::Server,
|
|
state->callbackDomain,
|
|
timeoutController
|
|
));
|
|
auto connectionObject = httpConnection.Obj();
|
|
httpConnection->RetainUntilStopped(httpConnection, Func<void()>([state, connectionObject]()
|
|
{
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->connections.Remove(connectionObject);
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}));
|
|
bool invoke = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (!state->stopStarted && state->owner == owner)
|
|
{
|
|
state->connections.Add(httpConnection);
|
|
invoke = true;
|
|
}
|
|
}
|
|
if (!invoke)
|
|
{
|
|
httpConnection->StopWithRetainedAdapter(httpConnection);
|
|
return WaitForClientResult::Reject;
|
|
}
|
|
|
|
auto result = WaitForClientResult::Reject;
|
|
try
|
|
{
|
|
result = owner->OnClientConnected(httpConnection.Obj());
|
|
}
|
|
catch (...)
|
|
{
|
|
result = WaitForClientResult::Reject;
|
|
}
|
|
|
|
bool accepted = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
accepted = result == WaitForClientResult::Accept && !state->stopStarted;
|
|
if (!accepted)
|
|
{
|
|
state->connections.Remove(httpConnection.Obj());
|
|
}
|
|
}
|
|
if (!accepted)
|
|
{
|
|
httpConnection->StopWithRetainedAdapter(httpConnection);
|
|
}
|
|
return accepted ? WaitForClientResult::Accept : WaitForClientResult::Reject;
|
|
}
|
|
|
|
static void OnSocketServerStopped(Ptr<Lifecycle> state)
|
|
{
|
|
HttpRequestCallbackDomain::CallbackFrame callbackFrame(state->callbackDomain);
|
|
HttpRequestServer* owner = nullptr;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (!state->stopStarted && !state->unexpectedStopNotified)
|
|
{
|
|
state->unexpectedStopNotified = true;
|
|
owner = state->owner;
|
|
}
|
|
}
|
|
if (owner)
|
|
{
|
|
try
|
|
{
|
|
owner->OnServerStopped();
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
public:
|
|
Impl(
|
|
HttpRequestServer* owner,
|
|
Ptr<IAsyncSocketServer> server,
|
|
const Func<Ptr<IHttpRequestTimeoutController>()>& timeoutControllerFactory
|
|
)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::HttpRequestServer::HttpRequestServer(Ptr<IAsyncSocketServer>)#"
|
|
CHECK_ERROR(server, ERROR_MESSAGE_PREFIX L"Requires a server.");
|
|
lifecycle = Ptr(new Lifecycle(owner, server, timeoutControllerFactory));
|
|
callback = Ptr(new SocketServerCallback(lifecycle));
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
~Impl()
|
|
{
|
|
Destroy();
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::HttpRequestServer::Start()#"
|
|
auto state = lifecycle;
|
|
bool canStart = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (!state->startCalled && !state->stopStarted)
|
|
{
|
|
state->startCalled = true;
|
|
canStart = true;
|
|
}
|
|
}
|
|
CHECK_ERROR(canStart, ERROR_MESSAGE_PREFIX L"Can only be called once before stopping.");
|
|
|
|
callback->InitializeSelf(callback);
|
|
try
|
|
{
|
|
state->server->Start(callback.Obj());
|
|
}
|
|
catch (...)
|
|
{
|
|
try
|
|
{
|
|
Stop();
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
throw;
|
|
}
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
void Stop()
|
|
{
|
|
auto state = lifecycle;
|
|
auto retainedCallback = callback;
|
|
auto callbackDepth = state->callbackDomain->CurrentCallbackDepth();
|
|
bool firstStop = false;
|
|
bool nestedFollower = false;
|
|
bool deferFinalization = false;
|
|
state->lockState.Enter();
|
|
if (!state->stopStarted)
|
|
{
|
|
state->stopStarted = true;
|
|
state->nativeStopCalling = true;
|
|
firstStop = true;
|
|
}
|
|
else if (callbackDepth > 0)
|
|
{
|
|
nestedFollower = true;
|
|
}
|
|
else
|
|
{
|
|
while (!state->stopFinished || state->nativeStopCalling)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
state->nativeStopCalling = true;
|
|
}
|
|
state->lockState.Leave();
|
|
if (nestedFollower)
|
|
{
|
|
StopConnections(state);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
state->server->Stop();
|
|
StopConnections(state);
|
|
state->callbackDomain->WaitForCallbacks(firstStop ? callbackDepth : 0);
|
|
deferFinalization = firstStop && callbackDepth > 0;
|
|
if (!deferFinalization)
|
|
{
|
|
FinalizeDestroyedOwner(state);
|
|
retainedCallback->ReleaseSelfReference();
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
if (!firstStop || callbackDepth == 0)
|
|
{
|
|
retainedCallback->ReleaseSelfReference();
|
|
}
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->nativeStopCalling = false;
|
|
if (firstStop)
|
|
{
|
|
state->stopFinished = true;
|
|
}
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
throw;
|
|
}
|
|
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->nativeStopCalling = false;
|
|
if (firstStop)
|
|
{
|
|
state->stopFinished = true;
|
|
}
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
if (deferFinalization)
|
|
{
|
|
QueueDeferredStop(state, retainedCallback);
|
|
}
|
|
}
|
|
|
|
void Destroy()
|
|
{
|
|
auto state = lifecycle;
|
|
bool execute = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (!state->destroyStarted)
|
|
{
|
|
state->destroyStarted = true;
|
|
execute = true;
|
|
}
|
|
}
|
|
if (!execute)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Stop();
|
|
StopConnections(state, true);
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->destroyAdaptersRetained = true;
|
|
}
|
|
if (state->callbackDomain->CurrentCallbackDepth() == 0)
|
|
{
|
|
state->callbackDomain->WaitForCallbacks(0);
|
|
FinalizeDestroyedOwner(state);
|
|
}
|
|
}
|
|
|
|
bool IsStopped()
|
|
{
|
|
return lifecycle->server->IsStopped();
|
|
}
|
|
};
|
|
|
|
/***********************************************************************
|
|
HttpRequestServer
|
|
***********************************************************************/
|
|
|
|
HttpRequestServer::HttpRequestServer(Ptr<IAsyncSocketServer> server)
|
|
: HttpRequestServer(server, {})
|
|
{
|
|
}
|
|
|
|
HttpRequestServer::HttpRequestServer(
|
|
Ptr<IAsyncSocketServer> server,
|
|
const Func<Ptr<IHttpRequestTimeoutController>()>& timeoutControllerFactory
|
|
)
|
|
: impl(Ptr(new Impl(this, server, timeoutControllerFactory)))
|
|
{
|
|
}
|
|
|
|
HttpRequestServer::~HttpRequestServer()
|
|
{
|
|
impl->Destroy();
|
|
}
|
|
|
|
WaitForClientResult HttpRequestServer::OnClientConnected(IHttpRequestConnection*)
|
|
{
|
|
return WaitForClientResult::Accept;
|
|
}
|
|
|
|
void HttpRequestServer::OnServerStopped()
|
|
{
|
|
Stop();
|
|
}
|
|
|
|
void HttpRequestServer::Start()
|
|
{
|
|
impl->Start();
|
|
}
|
|
|
|
void HttpRequestServer::Stop()
|
|
{
|
|
impl->Stop();
|
|
}
|
|
|
|
bool HttpRequestServer::IsStopped()
|
|
{
|
|
return impl->IsStopped();
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPSERVER.CPP
|
|
***********************************************************************/
|
|
|
|
#include <random>
|
|
|
|
namespace vl::inter_process::async_tcp_socket
|
|
{
|
|
using namespace collections;
|
|
|
|
namespace
|
|
{
|
|
constexpr vint GeneratedTokenLength = 36;
|
|
|
|
WString ValidateServerUrlPrefix(const WString& urlPrefix)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::SocketHttpServer::SocketHttpServer(Ptr<IAsyncSocketServer>, const WString&)#"
|
|
auto normalizedUrlPrefix = urlPrefix;
|
|
while (normalizedUrlPrefix.Length() > 0 && normalizedUrlPrefix[normalizedUrlPrefix.Length() - 1] == L'/')
|
|
{
|
|
normalizedUrlPrefix = normalizedUrlPrefix.Left(normalizedUrlPrefix.Length() - 1);
|
|
}
|
|
CHECK_ERROR(ValidateHttpNetworkProtocolBaseUrl(normalizedUrlPrefix), ERROR_MESSAGE_PREFIX L"urlPrefix must be empty or a legal ASCII origin-form path prefix.");
|
|
CHECK_ERROR(ValidateHttpRequestLine(L"GET", normalizedUrlPrefix + HttpServerUrl_Connect) == HttpRequestLineValidationResult::Succeeded, ERROR_MESSAGE_PREFIX L"The /Connect target exceeds the HTTP request-line limit.");
|
|
const WString tokenPlaceholder = L"000000000000000000000000000000000000";
|
|
CHECK_ERROR(ValidateHttpRequestLine(L"POST", normalizedUrlPrefix + HttpServerUrl_Request + L"/" + tokenPlaceholder) == HttpRequestLineValidationResult::Succeeded, ERROR_MESSAGE_PREFIX L"The /Request target plus a generated token exceeds the HTTP request-line limit.");
|
|
CHECK_ERROR(ValidateHttpRequestLine(L"POST", normalizedUrlPrefix + HttpServerUrl_Response + L"/" + tokenPlaceholder) == HttpRequestLineValidationResult::Succeeded, ERROR_MESSAGE_PREFIX L"The /Response target plus a generated token exceeds the HTTP request-line limit.");
|
|
return normalizedUrlPrefix;
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
}
|
|
|
|
bool HasEmptyBody(Ptr<HttpRequest> request)
|
|
{
|
|
if (!request || request->body.chunks.Count() != 0 || request->body.trailers.Count() != 0) return false;
|
|
HttpFraming framing;
|
|
if (AnalyzeHttpFraming(request->headers, framing) != HttpFramingAnalysisResult::Succeeded) return false;
|
|
if (framing.kind == HttpFramingKind::None) return true;
|
|
return
|
|
framing.kind == HttpFramingKind::ContentLength &&
|
|
framing.contentLength == 0 &&
|
|
framing.contentLengthFieldCount == 1 &&
|
|
framing.contentLengthValueCount == 1 &&
|
|
framing.contentLengthValuesPlainDecimal;
|
|
}
|
|
|
|
bool DecodeSubmittedMessage(Ptr<SocketHttpRequestContext> context, Ptr<HttpRequest> request, WString& message)
|
|
{
|
|
if (!request || request->body.trailers.Count() != 0) return false;
|
|
HttpFraming framing;
|
|
if (AnalyzeHttpFraming(request->headers, framing) != HttpFramingAnalysisResult::Succeeded) return false;
|
|
if (
|
|
framing.kind != HttpFramingKind::ContentLength ||
|
|
framing.contentLength == 0 ||
|
|
framing.contentLengthFieldCount != 1 ||
|
|
framing.contentLengthValueCount != 1 ||
|
|
!framing.contentLengthValuesPlainDecimal ||
|
|
CountHttpFields(request->headers, L"content-type") != 1
|
|
)
|
|
{
|
|
return false;
|
|
}
|
|
auto contentType = FindHttpField(request->headers, L"content-type");
|
|
return
|
|
HttpFieldValueEqualsAscii(contentType->value, HttpNetworkProtocolContentType) &&
|
|
context->TryGetBodyUtf8(message) &&
|
|
IsValidHttpNetworkProtocolMessage(message);
|
|
}
|
|
|
|
bool ExtractToken(const WString& path, const wchar_t* route, WString& token)
|
|
{
|
|
auto prefix = WString::Unmanaged(route) + L"/";
|
|
if (path.Length() <= prefix.Length() || path.Left(prefix.Length()) != prefix) return false;
|
|
token = path.Right(path.Length() - prefix.Length());
|
|
return token.Length() > 0;
|
|
}
|
|
|
|
WString GenerateToken()
|
|
{
|
|
vuint8_t bytes[16];
|
|
std::random_device random;
|
|
for (vint i = 0; i < 16; i++) bytes[i] = (vuint8_t)random();
|
|
bytes[6] = (bytes[6] & 0x0F) | 0x40;
|
|
bytes[8] = (bytes[8] & 0x3F) | 0x80;
|
|
|
|
const wchar_t* hex = L"0123456789abcdef";
|
|
wchar_t text[GeneratedTokenLength];
|
|
vint writing = 0;
|
|
for (vint i = 0; i < 16; i++)
|
|
{
|
|
if (i == 4 || i == 6 || i == 8 || i == 10) text[writing++] = L'-';
|
|
text[writing++] = hex[bytes[i] >> 4];
|
|
text[writing++] = hex[bytes[i] & 0x0F];
|
|
}
|
|
return WString::CopyFrom(text, GeneratedTokenLength);
|
|
}
|
|
|
|
BEGIN_GLOBAL_STORAGE_CLASS(SocketHttpServerTestHooks)
|
|
SpinLock lock;
|
|
Func<void(const WString&)> claimed;
|
|
Func<void(const WString&, bool)> completed;
|
|
Func<void(const WString&)> registered;
|
|
Func<bool(const WString&)> cancelBeforeResponse;
|
|
INITIALIZE_GLOBAL_STORAGE_CLASS
|
|
FINALIZE_GLOBAL_STORAGE_CLASS
|
|
SPIN_LOCK(lock)
|
|
{
|
|
claimed = {};
|
|
completed = {};
|
|
registered = {};
|
|
cancelBeforeResponse = {};
|
|
}
|
|
END_GLOBAL_STORAGE_CLASS(SocketHttpServerTestHooks)
|
|
|
|
void InvokePollClaimed(const WString& token)
|
|
{
|
|
Func<void(const WString&)> callback;
|
|
auto& hooks = GetSocketHttpServerTestHooks();
|
|
SPIN_LOCK(hooks.lock) { callback = hooks.claimed; }
|
|
if (callback) try { callback(token); } catch (...) {}
|
|
}
|
|
|
|
void InvokePollCompleted(const WString& token, bool succeeded)
|
|
{
|
|
Func<void(const WString&, bool)> callback;
|
|
auto& hooks = GetSocketHttpServerTestHooks();
|
|
SPIN_LOCK(hooks.lock) { callback = hooks.completed; }
|
|
if (callback) try { callback(token, succeeded); } catch (...) {}
|
|
}
|
|
|
|
void InvokePollRegistered(const WString& token)
|
|
{
|
|
Func<void(const WString&)> callback;
|
|
auto& hooks = GetSocketHttpServerTestHooks();
|
|
SPIN_LOCK(hooks.lock) { callback = hooks.registered; }
|
|
if (callback) try { callback(token); } catch (...) {}
|
|
}
|
|
|
|
bool ShouldCancelPollBeforeResponse(const WString& token)
|
|
{
|
|
Func<bool(const WString&)> callback;
|
|
auto& hooks = GetSocketHttpServerTestHooks();
|
|
SPIN_LOCK(hooks.lock) { callback = hooks.cancelBeforeResponse; }
|
|
if (callback) try { return callback(token); } catch (...) {}
|
|
return false;
|
|
}
|
|
|
|
class SocketHttpServerConnection;
|
|
class SocketHttpServerLifecycle;
|
|
class SocketHttpServerOutboundMessage : public Object
|
|
{
|
|
public:
|
|
Array<vuint8_t> body;
|
|
};
|
|
|
|
class SocketHttpServerConnectionLifecycle : public Object
|
|
{
|
|
public:
|
|
CriticalSection lockState;
|
|
ConditionVariable cvState;
|
|
SocketHttpServerConnection* owner = nullptr;
|
|
Ptr<SocketHttpServerLifecycle> server;
|
|
WString token;
|
|
INetworkProtocolCallback* callback = nullptr;
|
|
List<WString> queuedInbound;
|
|
List<Ptr<SocketHttpServerOutboundMessage>>
|
|
queuedOutbound;
|
|
List<Ptr<SocketHttpRequestContext>>
|
|
queuedPollRegistrations;
|
|
Ptr<SocketHttpRequestContext> pendingPoll;
|
|
Ptr<SocketHttpRequestContext> inFlightPoll;
|
|
Ptr<SocketHttpServerOutboundMessage>
|
|
inFlightMessage;
|
|
vint activeCallbacks = 0;
|
|
bool callbackInstalling = false;
|
|
bool pollRegistrationProcessing = false;
|
|
bool accepted = false;
|
|
bool stopStarted = false;
|
|
bool stopCancellationFinished = false;
|
|
bool stopFinished = false;
|
|
bool stopAssistProcessing = false;
|
|
bool disconnectDelivering = false;
|
|
bool disconnectFinished = false;
|
|
};
|
|
|
|
class SocketHttpServerConnection
|
|
: public Object
|
|
, public virtual INetworkProtocolConnection
|
|
{
|
|
struct CallbackFrame
|
|
{
|
|
Ptr<SocketHttpServerConnectionLifecycle> state;
|
|
CallbackFrame* previous = nullptr;
|
|
|
|
CallbackFrame(Ptr<SocketHttpServerConnectionLifecycle> _state);
|
|
~CallbackFrame();
|
|
};
|
|
|
|
struct InboundFrame
|
|
{
|
|
Ptr<SocketHttpServerConnectionLifecycle> state;
|
|
InboundFrame* previous = nullptr;
|
|
List<Ptr<SocketHttpServerOutboundMessage>>
|
|
generated;
|
|
|
|
InboundFrame(Ptr<SocketHttpServerConnectionLifecycle> _state);
|
|
~InboundFrame();
|
|
};
|
|
|
|
struct PollWork
|
|
{
|
|
Ptr<SocketHttpRequestContext> context;
|
|
Ptr<SocketHttpServerOutboundMessage>
|
|
message;
|
|
|
|
operator bool() const { return context != nullptr; }
|
|
};
|
|
|
|
static thread_local CallbackFrame* currentCallbackFrame;
|
|
static thread_local InboundFrame* currentInboundFrame;
|
|
Ptr<SocketHttpServerConnectionLifecycle>
|
|
lifecycle;
|
|
|
|
static vint CurrentCallbackDepth(Ptr<SocketHttpServerConnectionLifecycle> state);
|
|
static bool ClaimPollUnsafe(Ptr<SocketHttpServerConnectionLifecycle> state, PollWork& work);
|
|
static void StartPollResponse(Ptr<SocketHttpServerConnectionLifecycle> state, PollWork work);
|
|
static void FinishPollResponse(Ptr<SocketHttpServerConnectionLifecycle> state, Ptr<SocketHttpRequestContext> context, bool succeeded);
|
|
static void ProcessPollRegistrations(Ptr<SocketHttpServerConnectionLifecycle> state);
|
|
void StopCore(bool removeFromServer, bool waitForPoll);
|
|
|
|
public:
|
|
SocketHttpServerConnection(Ptr<SocketHttpServerLifecycle> server, const WString& token);
|
|
|
|
void DetachServer(SocketHttpServerLifecycle* server);
|
|
bool MarkAccepted();
|
|
bool IsAccepted();
|
|
bool HasCurrentCallback();
|
|
WString GetToken();
|
|
WaitForClientResult InvokeClientConnected(SocketHttpServer* server);
|
|
bool RegisterPoll(Ptr<SocketHttpRequestContext> context);
|
|
bool DispatchInbound(const WString& message, Ptr<SocketHttpServerOutboundMessage>& response);
|
|
void StopFromServer();
|
|
void WaitForPollCompletion();
|
|
|
|
void InstallCallback(INetworkProtocolCallback* callback) override;
|
|
void BeginReadingLoopUnsafe() override;
|
|
void SendString(const WString& str) override;
|
|
void Stop() override;
|
|
};
|
|
|
|
class SocketHttpServerLifecycle : public Object
|
|
{
|
|
public:
|
|
SocketHttpServer* owner = nullptr;
|
|
CriticalSection lockState;
|
|
ConditionVariable cvState;
|
|
Dictionary<WString, Ptr<SocketHttpServerConnection>>
|
|
connections;
|
|
List<Ptr<SocketHttpServerConnection>>
|
|
stoppingConnections;
|
|
bool startCalled = false;
|
|
bool started = false;
|
|
bool stopStarted = false;
|
|
bool stopProcessing = false;
|
|
bool stopProcessed = false;
|
|
bool stopAssistProcessing = false;
|
|
|
|
SocketHttpServerLifecycle(SocketHttpServer* _owner)
|
|
: owner(_owner)
|
|
{
|
|
}
|
|
|
|
void PrepareStart()
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
CHECK_ERROR(!startCalled && !stopStarted, L"SocketHttpServer::Start can only be called once before stopping.");
|
|
startCalled = true;
|
|
started = true;
|
|
}
|
|
}
|
|
|
|
Ptr<SocketHttpServerConnection> CreateConnection(Ptr<SocketHttpServerLifecycle> retainedSelf)
|
|
{
|
|
while (true)
|
|
{
|
|
auto token = GenerateToken();
|
|
auto connection = Ptr(new SocketHttpServerConnection(retainedSelf, token));
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!started || stopStarted) return nullptr;
|
|
if (!connections.Keys().Contains(token))
|
|
{
|
|
connections.Add(token, connection);
|
|
return connection;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ptr<SocketHttpServerConnection> FindConnection(const WString& token)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (stopStarted) return nullptr;
|
|
auto index = connections.Keys().IndexOf(token);
|
|
return index == -1 ? nullptr : connections.Values()[index];
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bool TryAccept(const WString& token, SocketHttpServerConnection* connection)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (stopStarted) return false;
|
|
auto index = connections.Keys().IndexOf(token);
|
|
if (index == -1 || connections.Values()[index].Obj() != connection) return false;
|
|
return connection->MarkAccepted();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool IsStopped()
|
|
{
|
|
CS_LOCK(lockState) { return stopStarted; }
|
|
return true;
|
|
}
|
|
|
|
void RemoveConnection(const WString& token, SocketHttpServerConnection* connection)
|
|
{
|
|
bool removed = false;
|
|
bool retained = false;
|
|
CS_LOCK(lockState)
|
|
{
|
|
auto index = connections.Keys().IndexOf(token);
|
|
if (index != -1 && connections.Values()[index].Obj() == connection)
|
|
{
|
|
if (connection->IsAccepted())
|
|
{
|
|
stoppingConnections.Add(connections.Values()[index]);
|
|
retained = true;
|
|
}
|
|
connections.Remove(token);
|
|
removed = true;
|
|
}
|
|
}
|
|
if (removed && !retained) connection->DetachServer(this);
|
|
}
|
|
|
|
void PrepareStop(List<Ptr<SocketHttpServerConnection>>& stopping)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!stopStarted)
|
|
{
|
|
stopStarted = true;
|
|
started = false;
|
|
for (auto connection : connections.Values()) stoppingConnections.Add(connection);
|
|
connections.Clear();
|
|
}
|
|
for (auto connection : stoppingConnections) stopping.Add(connection);
|
|
}
|
|
}
|
|
|
|
void PrepareStopProcessing(bool callbackNested, bool inheritsAssist, bool& execute, bool& assist)
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
if (!stopProcessing && !stopProcessed)
|
|
{
|
|
stopProcessing = true;
|
|
execute = true;
|
|
if (callbackNested)
|
|
{
|
|
stopAssistProcessing = true;
|
|
assist = true;
|
|
}
|
|
}
|
|
else if (callbackNested && !inheritsAssist && !stopProcessed && !stopAssistProcessing)
|
|
{
|
|
stopAssistProcessing = true;
|
|
assist = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void FinishStop(bool ownsAssist)
|
|
{
|
|
lockState.Enter();
|
|
if (ownsAssist)
|
|
{
|
|
stopAssistProcessing = false;
|
|
}
|
|
else
|
|
{
|
|
while (stopAssistProcessing) cvState.SleepWith(lockState);
|
|
}
|
|
stopProcessing = false;
|
|
stopProcessed = true;
|
|
cvState.WakeAllPendings();
|
|
lockState.Leave();
|
|
}
|
|
|
|
void FinishStopAssist()
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
stopAssistProcessing = false;
|
|
cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
|
|
void WaitForStop()
|
|
{
|
|
CS_LOCK(lockState)
|
|
{
|
|
while (!stopProcessed) cvState.SleepWith(lockState);
|
|
}
|
|
}
|
|
|
|
Ptr<SocketHttpServerConnection> ReleaseStoppedConnection(SocketHttpServerConnection* connection)
|
|
{
|
|
Ptr<SocketHttpServerConnection> releasing;
|
|
CS_LOCK(lockState)
|
|
{
|
|
for (vint i = 0; i < stoppingConnections.Count(); i++)
|
|
{
|
|
if (stoppingConnections[i].Obj() == connection)
|
|
{
|
|
releasing = stoppingConnections[i];
|
|
stoppingConnections.RemoveAt(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (releasing) connection->DetachServer(this);
|
|
return releasing;
|
|
}
|
|
};
|
|
|
|
struct SocketHttpServerStopFrame
|
|
{
|
|
SocketHttpServerLifecycle* lifecycle = nullptr;
|
|
SocketHttpServerStopFrame* previous = nullptr;
|
|
bool ownsAssist = false;
|
|
};
|
|
|
|
thread_local SocketHttpServerStopFrame* currentSocketHttpServerStopFrame = nullptr;
|
|
|
|
SocketHttpServerStopFrame* FindSocketHttpServerStopFrame(SocketHttpServerLifecycle* lifecycle)
|
|
{
|
|
for (auto frame = currentSocketHttpServerStopFrame; frame; frame = frame->previous)
|
|
{
|
|
if (frame->lifecycle == lifecycle) return frame;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
struct SocketHttpServerStopScope
|
|
{
|
|
SocketHttpServerStopFrame frame;
|
|
|
|
SocketHttpServerStopScope(SocketHttpServerLifecycle* lifecycle, bool ownsAssist)
|
|
{
|
|
frame.lifecycle = lifecycle;
|
|
frame.previous = currentSocketHttpServerStopFrame;
|
|
frame.ownsAssist = ownsAssist;
|
|
currentSocketHttpServerStopFrame = &frame;
|
|
}
|
|
|
|
~SocketHttpServerStopScope()
|
|
{
|
|
currentSocketHttpServerStopFrame = frame.previous;
|
|
}
|
|
};
|
|
|
|
thread_local SocketHttpServerConnection::CallbackFrame* SocketHttpServerConnection::currentCallbackFrame = nullptr;
|
|
thread_local SocketHttpServerConnection::InboundFrame* SocketHttpServerConnection::currentInboundFrame = nullptr;
|
|
|
|
SocketHttpServerConnection::CallbackFrame::CallbackFrame(Ptr<SocketHttpServerConnectionLifecycle> _state)
|
|
: state(_state)
|
|
, previous(currentCallbackFrame)
|
|
{
|
|
currentCallbackFrame = this;
|
|
}
|
|
|
|
SocketHttpServerConnection::CallbackFrame::~CallbackFrame()
|
|
{
|
|
currentCallbackFrame = previous;
|
|
Ptr<SocketHttpServerLifecycle> server;
|
|
SocketHttpServerConnection* owner = nullptr;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->activeCallbacks--;
|
|
if (state->activeCallbacks == 0 && state->stopFinished && state->server)
|
|
{
|
|
server = state->server;
|
|
owner = state->owner;
|
|
}
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
Ptr<SocketHttpServerConnection> releasing;
|
|
if (server && owner) releasing = server->ReleaseStoppedConnection(owner);
|
|
}
|
|
|
|
SocketHttpServerConnection::InboundFrame::InboundFrame(Ptr<SocketHttpServerConnectionLifecycle> _state)
|
|
: state(_state)
|
|
, previous(currentInboundFrame)
|
|
{
|
|
currentInboundFrame = this;
|
|
}
|
|
|
|
SocketHttpServerConnection::InboundFrame::~InboundFrame()
|
|
{
|
|
currentInboundFrame = previous;
|
|
}
|
|
|
|
vint SocketHttpServerConnection::CurrentCallbackDepth(Ptr<SocketHttpServerConnectionLifecycle> state)
|
|
{
|
|
vint depth = 0;
|
|
for (auto frame = currentCallbackFrame; frame; frame = frame->previous)
|
|
{
|
|
if (frame->state == state) depth++;
|
|
}
|
|
return depth;
|
|
}
|
|
|
|
bool SocketHttpServerConnection::ClaimPollUnsafe(Ptr<SocketHttpServerConnectionLifecycle> state, PollWork& work)
|
|
{
|
|
if (
|
|
state->stopStarted ||
|
|
!state->accepted ||
|
|
state->inFlightPoll ||
|
|
!state->pendingPoll ||
|
|
state->queuedOutbound.Count() == 0
|
|
)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
state->inFlightPoll = state->pendingPoll;
|
|
state->pendingPoll = nullptr;
|
|
state->inFlightMessage = state->queuedOutbound[0];
|
|
state->queuedOutbound.RemoveAt(0);
|
|
work.context = state->inFlightPoll;
|
|
work.message = state->inFlightMessage;
|
|
return true;
|
|
}
|
|
|
|
void SocketHttpServerConnection::StartPollResponse(Ptr<SocketHttpServerConnectionLifecycle> state, PollWork work)
|
|
{
|
|
if (!work) return;
|
|
InvokePollClaimed(state->token);
|
|
if (ShouldCancelPollBeforeResponse(state->token)) work.context->Cancel();
|
|
bool submitted = false;
|
|
try
|
|
{
|
|
submitted = work.context->RespondBytes(
|
|
200,
|
|
L"OK",
|
|
HttpNetworkProtocolContentType,
|
|
work.message->body,
|
|
Func<void(bool)>([state, context = work.context](bool succeeded)
|
|
{
|
|
FinishPollResponse(state, context, succeeded);
|
|
})
|
|
);
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
if (!submitted) FinishPollResponse(state, work.context, false);
|
|
}
|
|
|
|
void SocketHttpServerConnection::FinishPollResponse(Ptr<SocketHttpServerConnectionLifecycle> state, Ptr<SocketHttpRequestContext> context, bool succeeded)
|
|
{
|
|
PollWork next;
|
|
bool completed = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (state->inFlightPoll == context)
|
|
{
|
|
if (!succeeded && !state->stopStarted)
|
|
{
|
|
state->queuedOutbound.Insert(0, state->inFlightMessage);
|
|
}
|
|
state->inFlightPoll = nullptr;
|
|
state->inFlightMessage = nullptr;
|
|
ClaimPollUnsafe(state, next);
|
|
state->cvState.WakeAllPendings();
|
|
completed = true;
|
|
}
|
|
}
|
|
if (!completed) return;
|
|
InvokePollCompleted(state->token, succeeded);
|
|
StartPollResponse(state, next);
|
|
}
|
|
|
|
void SocketHttpServerConnection::ProcessPollRegistrations(Ptr<SocketHttpServerConnectionLifecycle> state)
|
|
{
|
|
while (true)
|
|
{
|
|
Ptr<SocketHttpRequestContext> context;
|
|
Ptr<SocketHttpRequestContext> replaced;
|
|
PollWork work;
|
|
bool cancel = false;
|
|
state->lockState.Enter();
|
|
if (state->queuedPollRegistrations.Count() == 0)
|
|
{
|
|
auto registered = state->pendingPoll && !state->stopStarted && state->accepted;
|
|
state->pollRegistrationProcessing = false;
|
|
state->cvState.WakeAllPendings();
|
|
state->lockState.Leave();
|
|
if (registered) InvokePollRegistered(state->token);
|
|
return;
|
|
}
|
|
context = state->queuedPollRegistrations[0];
|
|
state->queuedPollRegistrations.RemoveAt(0);
|
|
replaced = state->pendingPoll;
|
|
state->pendingPoll = nullptr;
|
|
state->lockState.Leave();
|
|
|
|
if (replaced) replaced->Cancel();
|
|
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (state->stopStarted || !state->accepted)
|
|
{
|
|
cancel = true;
|
|
}
|
|
else
|
|
{
|
|
state->pendingPoll = context;
|
|
ClaimPollUnsafe(state, work);
|
|
}
|
|
}
|
|
if (cancel) context->Cancel();
|
|
StartPollResponse(state, work);
|
|
}
|
|
}
|
|
|
|
SocketHttpServerConnection::SocketHttpServerConnection(Ptr<SocketHttpServerLifecycle> server, const WString& token)
|
|
: lifecycle(Ptr(new SocketHttpServerConnectionLifecycle))
|
|
{
|
|
lifecycle->owner = this;
|
|
lifecycle->server = server;
|
|
lifecycle->token = token;
|
|
}
|
|
|
|
void SocketHttpServerConnection::DetachServer(SocketHttpServerLifecycle* server)
|
|
{
|
|
CS_LOCK(lifecycle->lockState)
|
|
{
|
|
if (lifecycle->server.Obj() == server) lifecycle->server = nullptr;
|
|
}
|
|
}
|
|
|
|
bool SocketHttpServerConnection::MarkAccepted()
|
|
{
|
|
CS_LOCK(lifecycle->lockState)
|
|
{
|
|
if (!lifecycle->stopStarted)
|
|
{
|
|
lifecycle->accepted = true;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool SocketHttpServerConnection::IsAccepted()
|
|
{
|
|
CS_LOCK(lifecycle->lockState) { return lifecycle->accepted; }
|
|
return false;
|
|
}
|
|
|
|
bool SocketHttpServerConnection::HasCurrentCallback()
|
|
{
|
|
return CurrentCallbackDepth(lifecycle) > 0;
|
|
}
|
|
|
|
WString SocketHttpServerConnection::GetToken()
|
|
{
|
|
return lifecycle->token;
|
|
}
|
|
|
|
WaitForClientResult SocketHttpServerConnection::InvokeClientConnected(SocketHttpServer* server)
|
|
{
|
|
auto state = lifecycle;
|
|
bool invoke = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (!state->stopStarted)
|
|
{
|
|
state->activeCallbacks++;
|
|
invoke = true;
|
|
}
|
|
}
|
|
if (!invoke) return WaitForClientResult::Reject;
|
|
|
|
WaitForClientResult result;
|
|
{
|
|
CallbackFrame frame(state);
|
|
result = server->OnClientConnected(this);
|
|
}
|
|
if (result != WaitForClientResult::Accept) return WaitForClientResult::Reject;
|
|
|
|
Ptr<SocketHttpServerLifecycle> retainedServer;
|
|
CS_LOCK(state->lockState) { retainedServer = state->server; }
|
|
return retainedServer && retainedServer->TryAccept(state->token, this)
|
|
? WaitForClientResult::Accept
|
|
: WaitForClientResult::Reject;
|
|
}
|
|
|
|
bool SocketHttpServerConnection::RegisterPoll(Ptr<SocketHttpRequestContext> context)
|
|
{
|
|
auto state = lifecycle;
|
|
bool process = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (state->stopStarted || !state->accepted) return false;
|
|
state->queuedPollRegistrations.Add(context);
|
|
if (!state->pollRegistrationProcessing)
|
|
{
|
|
state->pollRegistrationProcessing = true;
|
|
process = true;
|
|
}
|
|
}
|
|
if (process) ProcessPollRegistrations(state);
|
|
return true;
|
|
}
|
|
|
|
bool SocketHttpServerConnection::DispatchInbound(const WString& message, Ptr<SocketHttpServerOutboundMessage>& response)
|
|
{
|
|
auto state = lifecycle;
|
|
INetworkProtocolCallback* installed = nullptr;
|
|
PollWork work;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (state->stopStarted || !state->accepted) return false;
|
|
if (state->callback && !state->callbackInstalling)
|
|
{
|
|
installed = state->callback;
|
|
state->activeCallbacks++;
|
|
}
|
|
else
|
|
{
|
|
state->queuedInbound.Add(message);
|
|
if (state->queuedOutbound.Count() > 0)
|
|
{
|
|
response = state->queuedOutbound[0];
|
|
state->queuedOutbound.RemoveAt(0);
|
|
}
|
|
ClaimPollUnsafe(state, work);
|
|
}
|
|
}
|
|
|
|
if (!installed)
|
|
{
|
|
StartPollResponse(state, work);
|
|
return true;
|
|
}
|
|
|
|
List<Ptr<SocketHttpServerOutboundMessage>> generated;
|
|
{
|
|
CallbackFrame callbackFrame(state);
|
|
InboundFrame inboundFrame(state);
|
|
installed->OnReadString(message);
|
|
generated = std::move(inboundFrame.generated);
|
|
}
|
|
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (state->stopStarted) return false;
|
|
if (generated.Count() > 0)
|
|
{
|
|
response = generated[0];
|
|
for (vint i = 1; i < generated.Count(); i++) state->queuedOutbound.Add(generated[i]);
|
|
}
|
|
else if (state->queuedOutbound.Count() > 0)
|
|
{
|
|
response = state->queuedOutbound[0];
|
|
state->queuedOutbound.RemoveAt(0);
|
|
}
|
|
ClaimPollUnsafe(state, work);
|
|
}
|
|
StartPollResponse(state, work);
|
|
return true;
|
|
}
|
|
|
|
void SocketHttpServerConnection::StopCore(bool removeFromServer, bool waitForPoll)
|
|
{
|
|
auto state = lifecycle;
|
|
if (removeFromServer)
|
|
{
|
|
Ptr<SocketHttpServerLifecycle> server;
|
|
CS_LOCK(state->lockState) { server = state->server; }
|
|
if (server) server->RemoveConnection(state->token, this);
|
|
}
|
|
|
|
auto callbackDepth = CurrentCallbackDepth(state);
|
|
List<Ptr<SocketHttpRequestContext>> cancelling;
|
|
bool first = false;
|
|
bool ownsAssist = false;
|
|
state->lockState.Enter();
|
|
if (!state->stopStarted)
|
|
{
|
|
first = true;
|
|
state->stopStarted = true;
|
|
if (callbackDepth > 0)
|
|
{
|
|
state->stopAssistProcessing = true;
|
|
ownsAssist = true;
|
|
}
|
|
if (state->pendingPoll) cancelling.Add(state->pendingPoll);
|
|
state->pendingPoll = nullptr;
|
|
for (auto context : state->queuedPollRegistrations) cancelling.Add(context);
|
|
state->queuedPollRegistrations.Clear();
|
|
state->queuedInbound.Clear();
|
|
state->queuedOutbound.Clear();
|
|
}
|
|
else if (callbackDepth > 0)
|
|
{
|
|
if (
|
|
state->stopFinished ||
|
|
state->disconnectDelivering ||
|
|
state->disconnectFinished ||
|
|
state->stopAssistProcessing
|
|
)
|
|
{
|
|
state->lockState.Leave();
|
|
return;
|
|
}
|
|
state->stopAssistProcessing = true;
|
|
ownsAssist = true;
|
|
state->lockState.Leave();
|
|
}
|
|
else
|
|
{
|
|
while (!state->stopFinished) state->cvState.SleepWith(state->lockState);
|
|
while (state->activeCallbacks > 0 || (waitForPoll && (state->inFlightPoll || state->pollRegistrationProcessing)))
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
state->lockState.Leave();
|
|
return;
|
|
}
|
|
if (first) state->lockState.Leave();
|
|
|
|
if (first)
|
|
{
|
|
for (auto context : cancelling)
|
|
{
|
|
try { context->Cancel(); } catch (...) {}
|
|
}
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->stopCancellationFinished = true;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
|
|
INetworkProtocolCallback* disconnected = nullptr;
|
|
state->lockState.Enter();
|
|
while (!state->stopCancellationFinished || state->pollRegistrationProcessing)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
if (first && !ownsAssist)
|
|
{
|
|
while (state->stopAssistProcessing) state->cvState.SleepWith(state->lockState);
|
|
}
|
|
while (state->activeCallbacks > callbackDepth)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
if (state->accepted && state->callback && !state->disconnectDelivering && !state->disconnectFinished)
|
|
{
|
|
disconnected = state->callback;
|
|
state->disconnectDelivering = true;
|
|
state->activeCallbacks++;
|
|
}
|
|
state->lockState.Leave();
|
|
|
|
if (disconnected)
|
|
{
|
|
try
|
|
{
|
|
CallbackFrame frame(state);
|
|
disconnected->OnDisconnected();
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
}
|
|
|
|
Ptr<SocketHttpServerLifecycle> releasingServer;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->callback = nullptr;
|
|
state->callbackInstalling = false;
|
|
state->disconnectDelivering = false;
|
|
state->disconnectFinished = true;
|
|
if (ownsAssist) state->stopAssistProcessing = false;
|
|
if (first)
|
|
{
|
|
state->stopFinished = true;
|
|
if (state->activeCallbacks == 0) releasingServer = state->server;
|
|
}
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
Ptr<SocketHttpServerConnection> releasing;
|
|
if (releasingServer) releasing = releasingServer->ReleaseStoppedConnection(this);
|
|
if (first && waitForPoll)
|
|
{
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
while (state->inFlightPoll) state->cvState.SleepWith(state->lockState);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SocketHttpServerConnection::StopFromServer()
|
|
{
|
|
StopCore(false, false);
|
|
}
|
|
|
|
void SocketHttpServerConnection::WaitForPollCompletion()
|
|
{
|
|
CS_LOCK(lifecycle->lockState)
|
|
{
|
|
while (lifecycle->inFlightPoll || lifecycle->pollRegistrationProcessing)
|
|
{
|
|
lifecycle->cvState.SleepWith(lifecycle->lockState);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SocketHttpServerConnection::InstallCallback(INetworkProtocolCallback* callback)
|
|
{
|
|
auto state = lifecycle;
|
|
if (!callback)
|
|
{
|
|
auto callbackDepth = CurrentCallbackDepth(state);
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
state->callback = nullptr;
|
|
while (state->activeCallbacks > callbackDepth)
|
|
{
|
|
state->cvState.SleepWith(state->lockState);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
bool canInstall = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (!state->callback && !state->callbackInstalling && !state->stopStarted)
|
|
{
|
|
state->callback = callback;
|
|
state->callbackInstalling = true;
|
|
state->activeCallbacks++;
|
|
canInstall = true;
|
|
}
|
|
}
|
|
CHECK_ERROR(canInstall, L"SocketHttpServerConnection::InstallCallback cannot replace a callback or install one on a stopped connection.");
|
|
|
|
try
|
|
{
|
|
CallbackFrame frame(state);
|
|
callback->OnInstalled(this);
|
|
while (true)
|
|
{
|
|
WString message;
|
|
bool replay = false;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (!state->stopStarted && state->callback == callback && state->queuedInbound.Count() > 0)
|
|
{
|
|
message = state->queuedInbound[0];
|
|
state->queuedInbound.RemoveAt(0);
|
|
replay = true;
|
|
}
|
|
else
|
|
{
|
|
state->callbackInstalling = false;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
}
|
|
if (!replay) break;
|
|
callback->OnReadString(message);
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
if (state->callback == callback) state->callback = nullptr;
|
|
state->callbackInstalling = false;
|
|
state->cvState.WakeAllPendings();
|
|
}
|
|
throw;
|
|
}
|
|
}
|
|
|
|
void SocketHttpServerConnection::BeginReadingLoopUnsafe()
|
|
{
|
|
}
|
|
|
|
void SocketHttpServerConnection::SendString(const WString& str)
|
|
{
|
|
#define ERROR_MESSAGE_PREFIX L"vl::inter_process::async_tcp_socket::SocketHttpServerConnection::SendString(const WString&)#"
|
|
Array<vuint8_t> validated;
|
|
CHECK_ERROR(str.Length() > 0, ERROR_MESSAGE_PREFIX L"A logical HTTP message cannot be empty.");
|
|
CHECK_ERROR(IsValidHttpNetworkProtocolMessage(str), ERROR_MESSAGE_PREFIX L"A logical HTTP message must contain valid Unicode without NUL.");
|
|
CHECK_ERROR(EncodeStrictUtf8(str, validated), ERROR_MESSAGE_PREFIX L"A logical HTTP message must contain valid Unicode without NUL.");
|
|
CHECK_ERROR(validated.Count() <= HttpBodySizeLimit, ERROR_MESSAGE_PREFIX L"The UTF-8 message exceeds HttpBodySizeLimit.");
|
|
#undef ERROR_MESSAGE_PREFIX
|
|
auto message = Ptr(new SocketHttpServerOutboundMessage);
|
|
message->body = std::move(validated);
|
|
auto state = lifecycle;
|
|
PollWork work;
|
|
CS_LOCK(state->lockState)
|
|
{
|
|
CHECK_ERROR(!state->stopStarted, L"SocketHttpServerConnection::SendString cannot send on a stopped connection.");
|
|
if (currentInboundFrame && currentInboundFrame->state == state)
|
|
{
|
|
currentInboundFrame->generated.Add(message);
|
|
return;
|
|
}
|
|
state->queuedOutbound.Add(message);
|
|
ClaimPollUnsafe(state, work);
|
|
}
|
|
StartPollResponse(state, work);
|
|
}
|
|
|
|
void SocketHttpServerConnection::Stop()
|
|
{
|
|
StopCore(true, true);
|
|
}
|
|
}
|
|
|
|
class SocketHttpServer::Impl : public Object
|
|
{
|
|
public:
|
|
enum class BeginStopResult
|
|
{
|
|
Continue,
|
|
ReturnFollower,
|
|
};
|
|
|
|
Ptr<SocketHttpServerLifecycle> lifecycle;
|
|
|
|
Impl(SocketHttpServer* owner)
|
|
: lifecycle(Ptr(new SocketHttpServerLifecycle(owner)))
|
|
{
|
|
}
|
|
|
|
static bool HasCurrentCallback(List<Ptr<SocketHttpServerConnection>>& stopping)
|
|
{
|
|
for (auto connection : stopping)
|
|
{
|
|
if (connection->HasCurrentCallback()) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void DrainConnections(List<Ptr<SocketHttpServerConnection>>& stopping)
|
|
{
|
|
for (auto connection : stopping)
|
|
{
|
|
if (!connection->HasCurrentCallback()) connection->StopFromServer();
|
|
}
|
|
for (auto connection : stopping)
|
|
{
|
|
if (connection->HasCurrentCallback()) connection->StopFromServer();
|
|
}
|
|
}
|
|
|
|
void ExecuteAssistant(List<Ptr<SocketHttpServerConnection>>& stopping)
|
|
{
|
|
try
|
|
{
|
|
SocketHttpServerStopScope scope(lifecycle.Obj(), true);
|
|
DrainConnections(stopping);
|
|
}
|
|
catch (...)
|
|
{
|
|
lifecycle->FinishStopAssist();
|
|
throw;
|
|
}
|
|
lifecycle->FinishStopAssist();
|
|
}
|
|
|
|
BeginStopResult BeginStop(List<Ptr<SocketHttpServerConnection>>& stopping)
|
|
{
|
|
lifecycle->PrepareStop(stopping);
|
|
auto existingFrame = FindSocketHttpServerStopFrame(lifecycle.Obj());
|
|
auto callbackNested = HasCurrentCallback(stopping);
|
|
bool execute = false;
|
|
bool assist = false;
|
|
lifecycle->PrepareStopProcessing(
|
|
callbackNested,
|
|
existingFrame && existingFrame->ownsAssist,
|
|
execute,
|
|
assist
|
|
);
|
|
|
|
if (execute)
|
|
{
|
|
try
|
|
{
|
|
SocketHttpServerStopScope scope(lifecycle.Obj(), assist);
|
|
DrainConnections(stopping);
|
|
}
|
|
catch (...)
|
|
{
|
|
lifecycle->FinishStop(assist);
|
|
throw;
|
|
}
|
|
lifecycle->FinishStop(assist);
|
|
return BeginStopResult::Continue;
|
|
}
|
|
|
|
if (existingFrame)
|
|
{
|
|
if (existingFrame->ownsAssist)
|
|
{
|
|
DrainConnections(stopping);
|
|
}
|
|
else if (assist)
|
|
{
|
|
ExecuteAssistant(stopping);
|
|
}
|
|
return BeginStopResult::ReturnFollower;
|
|
}
|
|
|
|
if (callbackNested)
|
|
{
|
|
if (assist) ExecuteAssistant(stopping);
|
|
return BeginStopResult::ReturnFollower;
|
|
}
|
|
|
|
lifecycle->WaitForStop();
|
|
DrainConnections(stopping);
|
|
return BeginStopResult::Continue;
|
|
}
|
|
|
|
void OnRequest(SocketHttpServer* owner, Ptr<SocketHttpRequestContext> context)
|
|
{
|
|
auto request = context->GetRequest();
|
|
auto path = context->GetRelativePath();
|
|
if (!request || context->GetQuery() != WString::Empty)
|
|
{
|
|
context->RespondStatus(404, L"Route not found");
|
|
return;
|
|
}
|
|
|
|
if (request->method == L"GET" && path == HttpServerUrl_Connect && HasEmptyBody(request))
|
|
{
|
|
auto connection = lifecycle->CreateConnection(lifecycle);
|
|
if (!connection)
|
|
{
|
|
context->RespondStatus(404, L"Connection rejected");
|
|
return;
|
|
}
|
|
|
|
WaitForClientResult result = WaitForClientResult::Reject;
|
|
try { result = connection->InvokeClientConnected(owner); }
|
|
catch (...) { result = WaitForClientResult::Reject; }
|
|
if (result != WaitForClientResult::Accept)
|
|
{
|
|
connection->Stop();
|
|
context->RespondStatus(404, L"Connection rejected");
|
|
return;
|
|
}
|
|
|
|
auto token = connection->GetToken();
|
|
auto body = CreateHttpNetworkProtocolConnectBody(
|
|
WString::Unmanaged(HttpServerUrl_Request) + L"/" + token,
|
|
WString::Unmanaged(HttpServerUrl_Response) + L"/" + token
|
|
);
|
|
context->RespondUtf8(200, L"OK", HttpNetworkProtocolContentType, body);
|
|
return;
|
|
}
|
|
|
|
WString token;
|
|
if (request->method == L"POST" && ExtractToken(path, HttpServerUrl_Request, token) && HasEmptyBody(request))
|
|
{
|
|
auto connection = lifecycle->FindConnection(token);
|
|
if (connection && connection->RegisterPoll(context)) return;
|
|
context->RespondStatus(404, L"Connection not found");
|
|
return;
|
|
}
|
|
|
|
WString message;
|
|
if (request->method == L"POST" && ExtractToken(path, HttpServerUrl_Response, token) && DecodeSubmittedMessage(context, request, message))
|
|
{
|
|
auto connection = lifecycle->FindConnection(token);
|
|
Ptr<SocketHttpServerOutboundMessage> response;
|
|
if (connection && connection->DispatchInbound(message, response))
|
|
{
|
|
Array<vuint8_t> empty;
|
|
context->RespondBytes(200, L"OK", HttpNetworkProtocolContentType, response ? response->body : empty);
|
|
return;
|
|
}
|
|
}
|
|
|
|
context->RespondStatus(404, L"Route not found");
|
|
}
|
|
};
|
|
|
|
void SetSocketHttpServerPollCallbacksForTesting(
|
|
const Func<void(const WString&)>& claimed,
|
|
const Func<void(const WString&, bool)>& completed,
|
|
const Func<void(const WString&)>& registered,
|
|
const Func<bool(const WString&)>& cancelBeforeResponse
|
|
)
|
|
{
|
|
auto& hooks = GetSocketHttpServerTestHooks();
|
|
SPIN_LOCK(hooks.lock)
|
|
{
|
|
hooks.claimed = claimed;
|
|
hooks.completed = completed;
|
|
hooks.registered = registered;
|
|
hooks.cancelBeforeResponse = cancelBeforeResponse;
|
|
}
|
|
}
|
|
|
|
void ResetSocketHttpServerPollCallbacksForTesting()
|
|
{
|
|
SetSocketHttpServerPollCallbacksForTesting({}, {}, {}, {});
|
|
}
|
|
|
|
SocketHttpServer::SocketHttpServer(Ptr<IAsyncSocketServer> server, const WString& urlPrefix)
|
|
: SocketHttpServerApi(server, ValidateServerUrlPrefix(urlPrefix))
|
|
, impl(Ptr(new Impl(this)))
|
|
{
|
|
}
|
|
|
|
SocketHttpServer::~SocketHttpServer()
|
|
{
|
|
try { Stop(); } catch (...) {}
|
|
CS_LOCK(impl->lifecycle->lockState) { impl->lifecycle->owner = nullptr; }
|
|
}
|
|
|
|
WaitForClientResult SocketHttpServer::OnClientConnected(INetworkProtocolConnection*)
|
|
{
|
|
return WaitForClientResult::Accept;
|
|
}
|
|
|
|
void SocketHttpServer::OnHttpRequestReceived(Ptr<SocketHttpRequestContext> context)
|
|
{
|
|
impl->OnRequest(this, context);
|
|
}
|
|
|
|
void SocketHttpServer::OnHttpServerStopping()
|
|
{
|
|
List<Ptr<SocketHttpServerConnection>> stopping;
|
|
impl->BeginStop(stopping);
|
|
}
|
|
|
|
void SocketHttpServer::Start()
|
|
{
|
|
impl->lifecycle->PrepareStart();
|
|
try
|
|
{
|
|
SocketHttpServerApi::Start();
|
|
}
|
|
catch (...)
|
|
{
|
|
List<Ptr<SocketHttpServerConnection>> stopping;
|
|
impl->BeginStop(stopping);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
void SocketHttpServer::Stop()
|
|
{
|
|
List<Ptr<SocketHttpServerConnection>> stopping;
|
|
if (impl->BeginStop(stopping) == Impl::BeginStopResult::ReturnFollower) return;
|
|
SocketHttpServerApi::Stop();
|
|
for (auto connection : stopping) connection->WaitForPollCompletion();
|
|
}
|
|
|
|
bool SocketHttpServer::IsStopped()
|
|
{
|
|
return impl->lifecycle->IsStopped() || SocketHttpServerApi::IsStopped();
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\INTERPROCESS\ASYNCSOCKET\ASYNCSOCKET_HTTPSERVERAPI.CPP
|
|
***********************************************************************/
|
|
|
|
|
|
namespace vl::inter_process::async_tcp_socket
|
|
{
|
|
using namespace collections;
|
|
|
|
namespace
|
|
{
|
|
class ApiRegistration;
|
|
class RegistryEntry;
|
|
class SharedServer;
|
|
class ConnectionState;
|
|
|
|
struct PrefixInfo
|
|
{
|
|
WString normalizedUrl;
|
|
WString decodedPath;
|
|
};
|
|
|
|
wchar_t Lower(wchar_t c)
|
|
{
|
|
return c >= L'A' && c <= L'Z' ? c - L'A' + L'a' : c;
|
|
}
|
|
|
|
WString Lower(const WString& text)
|
|
{
|
|
if (text.Length() == 0) return {};
|
|
auto buffer = new wchar_t[text.Length() + 1];
|
|
for (vint i = 0; i < text.Length(); i++) buffer[i] = Lower(text[i]);
|
|
buffer[text.Length()] = 0;
|
|
return WString::TakeOver(buffer, text.Length());
|
|
}
|
|
|
|
bool Token(wchar_t c)
|
|
{
|
|
if ((c >= L'a' && c <= L'z') || (c >= L'A' && c <= L'Z') || (c >= L'0' && c <= L'9')) return true;
|
|
switch (c)
|
|
{
|
|
case L'!': case L'#': case L'$': case L'%': case L'&': case L'\'': case L'*':
|
|
case L'+': case L'-': case L'.': case L'^': case L'_': case L'`': case L'|': case L'~':
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
vint Hex(wchar_t c)
|
|
{
|
|
if (c >= L'0' && c <= L'9') return c - L'0';
|
|
if (c >= L'a' && c <= L'f') return c - L'a' + 10;
|
|
if (c >= L'A' && c <= L'F') return c - L'A' + 10;
|
|
return -1;
|
|
}
|
|
|
|
bool DecodePath(const WString& raw, WString& decoded)
|
|
{
|
|
List<vuint8_t> bytes;
|
|
for (vint i = 0; i < raw.Length(); i++)
|
|
{
|
|
auto c = raw[i];
|
|
if (c == L'%')
|
|
{
|
|
if (i + 2 >= raw.Length()) return false;
|
|
auto h1 = Hex(raw[i + 1]);
|
|
auto h2 = Hex(raw[i + 2]);
|
|
if (h1 < 0 || h2 < 0) return false;
|
|
auto b = (vuint8_t)(h1 * 16 + h2);
|
|
if (b == 0 || b == '/' || b == '\\') return false;
|
|
bytes.Add(b);
|
|
i += 2;
|
|
}
|
|
else
|
|
{
|
|
if (c == 0 || c == L'\\' || c > 0x7F) return false;
|
|
bytes.Add((vuint8_t)c);
|
|
}
|
|
}
|
|
|
|
Array<vuint8_t> encoded(bytes.Count());
|
|
for (vint i = 0; i < bytes.Count(); i++) encoded[i] = bytes[i];
|
|
return ::vl::inter_process::async_tcp_socket::DecodeStrictUtf8(
|
|
encoded.Count() == 0 ? nullptr : &encoded[0],
|
|
encoded.Count(),
|
|
decoded
|
|
);
|
|
}
|
|
|
|
bool ParseAuthority(const WString& text, vint& port)
|
|
{
|
|
vint colon = -1;
|
|
for (vint i = 0; i < text.Length(); i++)
|
|
{
|
|
if (text[i] == L':')
|
|
{
|
|
if (colon != -1) return false;
|
|
colon = i;
|
|
}
|
|
else if (text[i] == L'@' || text[i] == L'/' || text[i] == L'?' || text[i] == L'#') return false;
|
|
}
|
|
if (colon <= 0 || colon + 1 >= text.Length()) return false;
|
|
auto host = Lower(text.Left(colon));
|
|
if (host != L"localhost" && host != L"127.0.0.1") return false;
|
|
vuint64_t number = 0;
|
|
for (vint i = colon + 1; i < text.Length(); i++)
|
|
{
|
|
if (text[i] < L'0' || text[i] > L'9') return false;
|
|
number = number * 10 + text[i] - L'0';
|
|
if (number > 65535) return false;
|
|
}
|
|
if (number == 0) return false;
|
|
port = (vint)number;
|
|
return true;
|
|
}
|
|
|
|
PrefixInfo ParsePrefix(vint port, const WString& urlPrefix)
|
|
{
|
|
#define ERROR_PREFIX L"vl::inter_process::async_tcp_socket::SocketHttpServerApi::SocketHttpServerApi(Ptr<IAsyncSocketServer>, const WString&, bool)#"
|
|
CHECK_ERROR(1 <= port && port <= 65535, ERROR_PREFIX L"The injected server port must be in 1..65535.");
|
|
CHECK_ERROR(urlPrefix.Length() == 0 || urlPrefix[0] == L'/', ERROR_PREFIX L"The URL prefix must be empty or begin with /.");
|
|
CHECK_ERROR(urlPrefix.IndexOf(L'?') == -1 && urlPrefix.IndexOf(L'#') == -1, ERROR_PREFIX L"Query and fragment components are not supported.");
|
|
auto rawPath = urlPrefix;
|
|
PrefixInfo info;
|
|
while (rawPath.Length() > 0 && rawPath[rawPath.Length() - 1] == L'/') rawPath = rawPath.Left(rawPath.Length() - 1);
|
|
CHECK_ERROR(DecodePath(rawPath, info.decodedPath), ERROR_PREFIX L"The path contains invalid escaping, UTF-8, NUL, or an encoded separator.");
|
|
info.normalizedUrl = L"http://localhost:" + itow(port) + rawPath;
|
|
return info;
|
|
#undef ERROR_PREFIX
|
|
}
|
|
|
|
WString TrimOws(const WString& text)
|
|
{
|
|
vint begin = 0, end = text.Length();
|
|
while (begin < end && (text[begin] == L' ' || text[begin] == L'\t')) begin++;
|
|
while (begin < end && (text[end - 1] == L' ' || text[end - 1] == L'\t')) end--;
|
|
return text.Sub(begin, end - begin);
|
|
}
|
|
|
|
bool AnalyzeCacheControl(const Array<vuint8_t>& bytes, bool& noStore)
|
|
{
|
|
WString text;
|
|
if (!DecodeAsciiHttpFieldValue(bytes, text)) return false;
|
|
noStore = false;
|
|
vint begin = 0;
|
|
bool quoted = false, escaped = false;
|
|
for (vint i = 0; i <= text.Length(); i++)
|
|
{
|
|
if (i < text.Length())
|
|
{
|
|
auto c = text[i];
|
|
if (escaped) { escaped = false; continue; }
|
|
if (quoted)
|
|
{
|
|
if (c == L'\\') escaped = true;
|
|
else if (c == L'"') quoted = false;
|
|
continue;
|
|
}
|
|
if (c == L'"') { quoted = true; continue; }
|
|
if (c != L',') continue;
|
|
}
|
|
else if (quoted || escaped) return false;
|
|
|
|
auto item = TrimOws(text.Sub(begin, i - begin));
|
|
if (item.Length() == 0) return false;
|
|
auto equals = item.IndexOf(L'=');
|
|
auto name = TrimOws(equals == -1 ? item : item.Left(equals));
|
|
if (name.Length() == 0) return false;
|
|
for (vint j = 0; j < name.Length(); j++) if (!Token(name[j])) return false;
|
|
if (equals != -1 && TrimOws(item.Right(item.Length() - equals - 1)).Length() == 0) return false;
|
|
if (Lower(name) == L"no-store")
|
|
{
|
|
if (equals != -1) return false;
|
|
noStore = true;
|
|
}
|
|
begin = i + 1;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ValidHttpDate(const Array<vuint8_t>& bytes)
|
|
{
|
|
WString text;
|
|
if (!DecodeAsciiHttpFieldValue(bytes, text)) return false;
|
|
text = TrimOws(text);
|
|
if (text.Length() != 29
|
|
|| text[3] != L',' || text[4] != L' ' || text[7] != L' ' || text[11] != L' '
|
|
|| text[16] != L' ' || text[19] != L':' || text[22] != L':' || text[25] != L' '
|
|
|| text.Sub(26, 3) != L"GMT") return false;
|
|
auto dayName = text.Sub(0, 3);
|
|
if (dayName != L"Sun" && dayName != L"Mon" && dayName != L"Tue" && dayName != L"Wed" && dayName != L"Thu" && dayName != L"Fri" && dayName != L"Sat") return false;
|
|
vint digitIndexes[] = { 5, 6, 12, 13, 14, 15, 17, 18, 20, 21, 23, 24 };
|
|
for (auto index : digitIndexes)
|
|
{
|
|
if (text[index] < L'0' || text[index] > L'9') return false;
|
|
}
|
|
auto monthName = text.Sub(8, 3);
|
|
vint month =
|
|
monthName == L"Jan" ? 1 : monthName == L"Feb" ? 2 : monthName == L"Mar" ? 3 :
|
|
monthName == L"Apr" ? 4 : monthName == L"May" ? 5 : monthName == L"Jun" ? 6 :
|
|
monthName == L"Jul" ? 7 : monthName == L"Aug" ? 8 : monthName == L"Sep" ? 9 :
|
|
monthName == L"Oct" ? 10 : monthName == L"Nov" ? 11 : monthName == L"Dec" ? 12 : 0;
|
|
if (month == 0) return false;
|
|
auto day = (text[5] - L'0') * 10 + text[6] - L'0';
|
|
auto year = (text[12] - L'0') * 1000 + (text[13] - L'0') * 100 + (text[14] - L'0') * 10 + text[15] - L'0';
|
|
auto hour = (text[17] - L'0') * 10 + text[18] - L'0';
|
|
auto minute = (text[20] - L'0') * 10 + text[21] - L'0';
|
|
auto second = (text[23] - L'0') * 10 + text[24] - L'0';
|
|
if (year == 0 || hour > 23 || minute > 59 || second > 60) return false;
|
|
vint monthDays[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
|
|
if (month == 2 && (year % 400 == 0 || (year % 4 == 0 && year % 100 != 0))) monthDays[1] = 29;
|
|
return day >= 1 && day <= monthDays[month - 1];
|
|
}
|
|
|
|
const wchar_t* Reason(vint code)
|
|
{
|
|
switch (code)
|
|
{
|
|
case 200: return L"OK"; case 204: return L"No Content"; case 304: return L"Not Modified";
|
|
case 400: return L"Bad Request"; case 404: return L"Not Found"; case 405: return L"Method Not Allowed";
|
|
case 408: return L"Request Timeout"; case 413: return L"Payload Too Large"; case 414: return L"URI Too Long";
|
|
case 415: return L"Unsupported Media Type"; case 417: return L"Expectation Failed";
|
|
case 431: return L"Request Header Fields Too Large"; case 500: return L"Internal Server Error";
|
|
case 501: return L"Not Implemented"; case 505: return L"HTTP Version Not Supported";
|
|
default: return L"Response";
|
|
}
|
|
}
|
|
|
|
void ValidateConvenienceResponseArguments(vint statusCode, const WString& reason, const WString& contentType, vint bodySize)
|
|
{
|
|
CHECK_ERROR(statusCode >= 200 && statusCode <= 599, L"Invalid HTTP response status.");
|
|
for (vint i = 0; i < reason.Length(); i++)
|
|
{
|
|
CHECK_ERROR((vuint32_t)reason[i] >= 0x20 && (vuint32_t)reason[i] <= 0x7E, L"Invalid HTTP response reason phrase.");
|
|
}
|
|
if (contentType != WString::Empty)
|
|
{
|
|
CreateAsciiHttpField(L"content-type", contentType);
|
|
}
|
|
CHECK_ERROR(bodySize <= HttpBodySizeLimit, L"The response body is too large.");
|
|
}
|
|
|
|
Ptr<HttpResponse> CreateConvenienceResponse(vint statusCode, const WString& reason, const WString& contentType, Array<vuint8_t>&& body)
|
|
{
|
|
auto response = Ptr(new HttpResponse);
|
|
response->statusCode = statusCode;
|
|
response->reason = reason;
|
|
if (contentType != WString::Empty)
|
|
{
|
|
response->headers.Add(CreateAsciiHttpField(L"content-type", contentType));
|
|
}
|
|
SetHttpBodyBytes(response->body, std::move(body));
|
|
return response;
|
|
}
|
|
|
|
WString Two(vint value) { return value < 10 ? L"0" + itow(value) : itow(value); }
|
|
|
|
WString DateHeader()
|
|
{
|
|
const wchar_t* days[] = { L"Sun", L"Mon", L"Tue", L"Wed", L"Thu", L"Fri", L"Sat" };
|
|
const wchar_t* months[] = { L"Jan", L"Feb", L"Mar", L"Apr", L"May", L"Jun", L"Jul", L"Aug", L"Sep", L"Oct", L"Nov", L"Dec" };
|
|
auto now = DateTime::UtcTime();
|
|
return WString::Unmanaged(days[now.dayOfWeek]) + L", " + Two(now.day) + L" " + months[now.month - 1] + L" " + itow(now.year) + L" " + Two(now.hour) + L":" + Two(now.minute) + L":" + Two(now.second) + L" GMT";
|
|
}
|
|
|
|
Ptr<HttpResponse> Normalize(Ptr<HttpResponse> input, const WString& method)
|
|
{
|
|
CHECK_ERROR(input, L"SocketHttpRequestContext::Respond requires a response.");
|
|
CHECK_ERROR(input->statusCode >= 200 && input->statusCode <= 599, L"Invalid HTTP response status.");
|
|
CHECK_ERROR(input->body.trailers.Count() == 0, L"Response trailers are not supported.");
|
|
Array<vuint8_t> body;
|
|
CHECK_ERROR(FlattenHttpBody(input->body, body), L"The response body is too large.");
|
|
auto bodySize = body.Count();
|
|
auto bodyAllowed = method != L"HEAD" && input->statusCode != 204 && input->statusCode != 304;
|
|
auto length = bodyAllowed || method == L"HEAD" ? bodySize : 0;
|
|
auto output = Ptr(new HttpResponse);
|
|
output->statusCode = input->statusCode;
|
|
output->reason = input->reason == WString::Empty ? Reason(input->statusCode) : input->reason;
|
|
for (vint i = 0; i < output->reason.Length(); i++)
|
|
{
|
|
CHECK_ERROR(output->reason[i] >= 0x20 && output->reason[i] <= 0x7E, L"Invalid HTTP response reason phrase.");
|
|
}
|
|
|
|
List<HttpField> normalizedHeaders;
|
|
for (auto&& source : input->headers)
|
|
{
|
|
auto copy = CreateAsciiHttpField(source.name, WString::Empty);
|
|
for (auto c : source.value) CHECK_ERROR(c == '\t' || (c >= 0x20 && c != 0x7F), L"Invalid response field value.");
|
|
copy.value.Resize(source.value.Count());
|
|
for (vint i = 0; i < source.value.Count(); i++) copy.value[i] = source.value[i];
|
|
normalizedHeaders.Add(std::move(copy));
|
|
}
|
|
|
|
HttpFraming framing;
|
|
CHECK_ERROR(AnalyzeHttpFraming(normalizedHeaders, framing) == HttpFramingAnalysisResult::Succeeded, L"Invalid or unsupported HTTP response framing.");
|
|
CHECK_ERROR(framing.kind != HttpFramingKind::Chunked, L"Response Transfer-Encoding is not supported.");
|
|
CHECK_ERROR(framing.contentLengthValuesPlainDecimal, L"Application Content-Length values must be plain decimal numbers.");
|
|
CHECK_ERROR(framing.contentLength <= (vuint64_t)std::numeric_limits<vint>::max(), L"The application Content-Length is too large.");
|
|
|
|
bool date = false, noStore = false, cors = false;
|
|
for (auto&& source : normalizedHeaders)
|
|
{
|
|
if (source.name == L"content-length") continue;
|
|
if (source.name == L"date")
|
|
{
|
|
CHECK_ERROR(!date && ValidHttpDate(source.value), L"A response requires at most one valid IMF-fixdate Date field.");
|
|
date = true;
|
|
}
|
|
else if (source.name == L"cache-control")
|
|
{
|
|
bool sourceNoStore = false;
|
|
CHECK_ERROR(AnalyzeCacheControl(source.value, sourceNoStore), L"Invalid Cache-Control response field.");
|
|
noStore |= sourceNoStore;
|
|
}
|
|
else if (source.name == L"access-control-allow-origin")
|
|
{
|
|
WString value;
|
|
CHECK_ERROR(!cors && DecodeAsciiHttpFieldValue(source.value, value) && TrimOws(value) == L"*", L"The loopback CORS policy requires exactly one Access-Control-Allow-Origin: * field.");
|
|
cors = true;
|
|
}
|
|
HttpField copy;
|
|
copy.name = source.name;
|
|
copy.value.Resize(source.value.Count());
|
|
if (source.value.Count() > 0)
|
|
{
|
|
memcpy(©.value[0], &source.value[0], source.value.Count());
|
|
}
|
|
output->headers.Add(std::move(copy));
|
|
}
|
|
auto contentLength = framing.kind == HttpFramingKind::ContentLength;
|
|
auto suppliedLength = (vint)framing.contentLength;
|
|
auto outputLength = length;
|
|
if (contentLength && input->statusCode == 304 && method != L"HEAD") outputLength = suppliedLength;
|
|
else CHECK_ERROR(!contentLength || suppliedLength == length, L"Contradictory response Content-Length.");
|
|
if (!date) output->headers.Add(CreateAsciiHttpField(L"date", DateHeader()));
|
|
if (!noStore) output->headers.Add(CreateAsciiHttpField(L"cache-control", L"no-store"));
|
|
if (!cors) output->headers.Add(CreateAsciiHttpField(L"access-control-allow-origin", L"*"));
|
|
if (input->statusCode != 204) output->headers.Add(CreateAsciiHttpField(L"content-length", itow(outputLength)));
|
|
if (bodyAllowed && bodySize > 0)
|
|
{
|
|
SetHttpBodyBytes(output->body, std::move(body));
|
|
}
|
|
return output;
|
|
}
|
|
|
|
Ptr<HttpResponse> Automatic(vint code, bool close, bool preflight = false)
|
|
{
|
|
auto response = Ptr(new HttpResponse);
|
|
response->statusCode = code;
|
|
response->reason = Reason(code);
|
|
if (close) response->headers.Add(CreateAsciiHttpField(L"connection", L"close"));
|
|
if (preflight)
|
|
{
|
|
response->headers.Add(CreateAsciiHttpField(L"access-control-allow-methods", L"GET, HEAD, POST, OPTIONS"));
|
|
response->headers.Add(CreateAsciiHttpField(L"access-control-allow-headers", L"Accept, Content-Type"));
|
|
response->headers.Add(CreateAsciiHttpField(L"allow", L"GET, HEAD, POST, OPTIONS"));
|
|
}
|
|
return response;
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace vl::inter_process::async_tcp_socket
|
|
{
|
|
using namespace collections;
|
|
|
|
namespace
|
|
{
|
|
class ApiRegistration : public Object
|
|
{
|
|
public:
|
|
struct Frame
|
|
{
|
|
ApiRegistration* registration;
|
|
Frame* previous;
|
|
Frame(ApiRegistration* value);
|
|
~Frame();
|
|
};
|
|
static thread_local Frame* currentFrame;
|
|
|
|
PrefixInfo prefix;
|
|
bool respondToOptions;
|
|
Ptr<RegistryEntry> entry;
|
|
CriticalSection lock;
|
|
ConditionVariable cv;
|
|
List<Ptr<SocketHttpRequestContext>> contexts;
|
|
List<Func<void()>> contextCancellations;
|
|
Func<void(Ptr<SocketHttpRequestContext>)> requestCallback;
|
|
Func<void()> stoppingCallback;
|
|
vint activeCallbacks = 0;
|
|
bool stopping = false;
|
|
bool stopped = false;
|
|
|
|
ApiRegistration(const PrefixInfo& _prefix, bool _respondToOptions)
|
|
: prefix(_prefix), respondToOptions(_respondToOptions)
|
|
{
|
|
}
|
|
|
|
static vint Depth(ApiRegistration* registration)
|
|
{
|
|
vint depth = 0;
|
|
for (auto frame = currentFrame; frame; frame = frame->previous) if (frame->registration == registration) depth++;
|
|
return depth;
|
|
}
|
|
|
|
bool AddContext(Ptr<SocketHttpRequestContext> context, const Func<void()>& cancellation)
|
|
{
|
|
bool added = false;
|
|
CS_LOCK(lock)
|
|
{
|
|
if (!stopping)
|
|
{
|
|
contexts.Add(context);
|
|
contextCancellations.Add(cancellation);
|
|
added = true;
|
|
}
|
|
}
|
|
return added;
|
|
}
|
|
|
|
void RemoveContext(SocketHttpRequestContext* context)
|
|
{
|
|
CS_LOCK(lock)
|
|
{
|
|
vint index = -1;
|
|
for (vint i = 0; i < contexts.Count(); i++)
|
|
{
|
|
if (contexts[i].Obj() == context) { index = i; break; }
|
|
}
|
|
if (index != -1)
|
|
{
|
|
contexts.RemoveAt(index);
|
|
contextCancellations.RemoveAt(index);
|
|
}
|
|
cv.WakeAllPendings();
|
|
}
|
|
}
|
|
|
|
bool InvokeRequest(Ptr<SocketHttpRequestContext> context)
|
|
{
|
|
Func<void(Ptr<SocketHttpRequestContext>)> callback;
|
|
CS_LOCK(lock)
|
|
{
|
|
if (!stopping && requestCallback)
|
|
{
|
|
callback = requestCallback;
|
|
activeCallbacks++;
|
|
}
|
|
}
|
|
if (!callback) return false;
|
|
Frame frame(this);
|
|
try
|
|
{
|
|
callback(context);
|
|
}
|
|
catch (...)
|
|
{
|
|
CS_LOCK(lock) { activeCallbacks--; cv.WakeAllPendings(); }
|
|
throw;
|
|
}
|
|
CS_LOCK(lock) { activeCallbacks--; cv.WakeAllPendings(); }
|
|
return true;
|
|
}
|
|
|
|
bool ReserveCompletion(const Func<void(bool)>& callback)
|
|
{
|
|
if (!callback) return false;
|
|
bool reserved = false;
|
|
CS_LOCK(lock)
|
|
{
|
|
if (!stopped) { activeCallbacks++; reserved = true; }
|
|
}
|
|
return reserved;
|
|
}
|
|
|
|
void InvokeReservedCompletion(const Func<void(bool)>& callback, bool succeeded)
|
|
{
|
|
Frame frame(this);
|
|
try { callback(succeeded); } catch (...) {}
|
|
CS_LOCK(lock) { activeCallbacks--; cv.WakeAllPendings(); }
|
|
}
|
|
|
|
void Stop(bool notify)
|
|
{
|
|
List<Func<void()>> cancelling;
|
|
Func<void()> callback;
|
|
auto depth = Depth(this);
|
|
lock.Enter();
|
|
if (stopped) { lock.Leave(); return; }
|
|
if (stopping)
|
|
{
|
|
if (depth > 0) { lock.Leave(); return; }
|
|
while (!stopped) cv.SleepWith(lock);
|
|
lock.Leave();
|
|
return;
|
|
}
|
|
stopping = true;
|
|
if (notify) callback = stoppingCallback;
|
|
for (auto cancellation : contextCancellations) cancelling.Add(cancellation);
|
|
lock.Leave();
|
|
|
|
if (callback)
|
|
{
|
|
CS_LOCK(lock) { activeCallbacks++; }
|
|
Frame frame(this);
|
|
try { callback(); } catch (...) {}
|
|
CS_LOCK(lock) { activeCallbacks--; cv.WakeAllPendings(); }
|
|
}
|
|
for (auto cancellation : cancelling) cancellation();
|
|
lock.Enter();
|
|
while (contexts.Count() > 0 || activeCallbacks > depth) cv.SleepWith(lock);
|
|
requestCallback = {};
|
|
stoppingCallback = {};
|
|
stopped = true;
|
|
cv.WakeAllPendings();
|
|
lock.Leave();
|
|
}
|
|
|
|
bool IsStopped()
|
|
{
|
|
bool result = false;
|
|
CS_LOCK(lock) { result = stopping; }
|
|
return result;
|
|
}
|
|
};
|
|
|
|
thread_local ApiRegistration::Frame* ApiRegistration::currentFrame = nullptr;
|
|
ApiRegistration::Frame::Frame(ApiRegistration* value) : registration(value), previous(currentFrame) { currentFrame = this; }
|
|
ApiRegistration::Frame::~Frame() { currentFrame = previous; }
|
|
|
|
class RegistryEntry : public Object
|
|
{
|
|
public:
|
|
Ptr<IAsyncSocketServer> socketServer;
|
|
EventObject readyEvent;
|
|
List<Ptr<ApiRegistration>> registrations;
|
|
Ptr<HttpRequestServer> server;
|
|
WString failureMessage;
|
|
AsyncSocketServerStartFailure failure = AsyncSocketServerStartFailure::Other;
|
|
bool succeeded = false, terminal = false;
|
|
|
|
RegistryEntry(Ptr<IAsyncSocketServer> _socketServer)
|
|
: socketServer(_socketServer)
|
|
{
|
|
CHECK_ERROR(readyEvent.CreateManualUnsignal(false), L"Failed to create a socket HTTP registry event.");
|
|
}
|
|
};
|
|
|
|
class ConnectionState : public Object
|
|
{
|
|
public:
|
|
CriticalSection lock;
|
|
Ptr<RegistryEntry> entry;
|
|
IHttpRequestConnection* connection = nullptr;
|
|
Ptr<SocketHttpRequestContext> context;
|
|
bool automaticWrite = false;
|
|
bool closeAfterWrite = false;
|
|
bool terminal = false;
|
|
|
|
ConnectionState(Ptr<RegistryEntry> _entry) : entry(_entry) {}
|
|
};
|
|
}
|
|
|
|
class SocketHttpRequestContext::Impl : public Object
|
|
{
|
|
public:
|
|
enum class State { Pending, Sending, Completed, Cancelled };
|
|
CriticalSection lock;
|
|
Ptr<ConnectionState> connectionState;
|
|
Ptr<ApiRegistration> registration;
|
|
Ptr<HttpRequest> request;
|
|
WString relativePath, query;
|
|
SocketHttpRequestContext* owner = nullptr;
|
|
Func<void(bool)> completion;
|
|
State state = State::Pending;
|
|
|
|
Impl(Ptr<ConnectionState> cs, Ptr<ApiRegistration> reg, Ptr<HttpRequest> req, const WString& relative, const WString& _query)
|
|
: connectionState(cs), registration(reg), request(req), relativePath(relative), query(_query)
|
|
{
|
|
}
|
|
|
|
bool Finish(bool succeeded, bool includePending, bool includeSending, bool stopConnection)
|
|
{
|
|
Func<void(bool)> callback;
|
|
bool won = false;
|
|
CS_LOCK(lock)
|
|
{
|
|
if ((includeSending && state == State::Sending) || (includePending && state == State::Pending))
|
|
{
|
|
if (state == State::Sending)
|
|
{
|
|
callback = completion;
|
|
completion = {};
|
|
}
|
|
state = succeeded ? State::Completed : State::Cancelled;
|
|
won = true;
|
|
}
|
|
}
|
|
if (!won) return false;
|
|
auto completionReserved = registration->ReserveCompletion(callback);
|
|
IHttpRequestConnection* connection = nullptr;
|
|
CS_LOCK(connectionState->lock)
|
|
{
|
|
if (connectionState->context.Obj() == owner) connectionState->context = nullptr;
|
|
if (stopConnection) { connectionState->terminal = true; connection = connectionState->connection; }
|
|
}
|
|
registration->RemoveContext(owner);
|
|
if (connection) try { connection->Stop(); } catch (...) {}
|
|
if (completionReserved) registration->InvokeReservedCompletion(callback, succeeded);
|
|
return true;
|
|
}
|
|
};
|
|
|
|
class SocketHttpServerApiDispatcher : public Object, public virtual IHttpRequestCallback
|
|
{
|
|
private:
|
|
CriticalSection lockSelf;
|
|
Ptr<SocketHttpServerApiDispatcher> selfReference;
|
|
Ptr<ConnectionState> state;
|
|
Ptr<SocketHttpServerApiDispatcher> Retain();
|
|
void SendAutomatic(vint code, bool close, bool preflight = false);
|
|
void Process(Ptr<HttpRequest> request);
|
|
|
|
public:
|
|
SocketHttpServerApiDispatcher(Ptr<RegistryEntry> entry) : state(Ptr(new ConnectionState(entry))) {}
|
|
void InitializeSelf(Ptr<SocketHttpServerApiDispatcher> self) { CS_LOCK(lockSelf) { selfReference = self; } }
|
|
void OnReadRequest(Ptr<HttpRequest> request) override;
|
|
void OnReadRequestFailure(HttpRequestFailure failure) override;
|
|
void OnWriteCompleted() override;
|
|
void OnError(const WString& error, bool fatal) override;
|
|
void OnDisconnected() override;
|
|
void OnInstalled(IHttpRequestConnection* connection) override;
|
|
};
|
|
|
|
namespace
|
|
{
|
|
void UnexpectedStop(Ptr<RegistryEntry> entry);
|
|
|
|
class SharedServer : public HttpRequestServer
|
|
{
|
|
private:
|
|
CriticalSection lock;
|
|
Ptr<SharedServer> selfReference;
|
|
Ptr<RegistryEntry> entry;
|
|
protected:
|
|
void OnServerStopped() override;
|
|
public:
|
|
SharedServer(
|
|
Ptr<IAsyncSocketServer> server,
|
|
Ptr<RegistryEntry> _entry,
|
|
const Func<Ptr<IHttpRequestTimeoutController>()>& timeoutControllerFactory
|
|
)
|
|
: HttpRequestServer(server, timeoutControllerFactory)
|
|
, entry(_entry)
|
|
{
|
|
}
|
|
void InitializeSelf(Ptr<SharedServer> self) { CS_LOCK(lock) { selfReference = self; } }
|
|
WaitForClientResult OnClientConnected(IHttpRequestConnection* connection) override;
|
|
void StopAndRelease();
|
|
};
|
|
|
|
BEGIN_GLOBAL_STORAGE_CLASS(SocketHttpRegistry)
|
|
SpinLock lock;
|
|
Dictionary<IAsyncSocketServer*, Ptr<RegistryEntry>> entries;
|
|
Func<Ptr<IHttpRequestTimeoutController>()>
|
|
timeoutControllerFactory;
|
|
INITIALIZE_GLOBAL_STORAGE_CLASS
|
|
FINALIZE_GLOBAL_STORAGE_CLASS
|
|
List<Ptr<HttpRequestServer>> servers;
|
|
List<Ptr<RegistryEntry>> retainedEntries;
|
|
SPIN_LOCK(lock)
|
|
{
|
|
for (auto entry : entries.Values())
|
|
{
|
|
retainedEntries.Add(entry);
|
|
if (entry->server) servers.Add(entry->server);
|
|
}
|
|
entries.Clear();
|
|
timeoutControllerFactory = {};
|
|
}
|
|
for (auto server : servers)
|
|
{
|
|
auto shared = server.Cast<SharedServer>();
|
|
if (shared) shared->StopAndRelease(); else server->Stop();
|
|
}
|
|
END_GLOBAL_STORAGE_CLASS(SocketHttpRegistry)
|
|
}
|
|
}
|
|
|
|
namespace vl::inter_process::async_tcp_socket
|
|
{
|
|
using namespace collections;
|
|
|
|
namespace
|
|
{
|
|
Func<Ptr<IHttpRequestTimeoutController>()> GetTimeoutControllerFactory()
|
|
{
|
|
Func<Ptr<IHttpRequestTimeoutController>()> factory;
|
|
auto& registry = GetSocketHttpRegistry();
|
|
SPIN_LOCK(registry.lock) { factory = registry.timeoutControllerFactory; }
|
|
return factory;
|
|
}
|
|
|
|
bool CurrentEntry(SocketHttpRegistry& registry, Ptr<RegistryEntry> entry)
|
|
{
|
|
auto index = registry.entries.Keys().IndexOf(entry->socketServer.Obj());
|
|
return index != -1 && registry.entries.Values()[index] == entry;
|
|
}
|
|
|
|
bool Duplicate(Ptr<RegistryEntry> entry, Ptr<ApiRegistration> registration)
|
|
{
|
|
for (auto existing : entry->registrations)
|
|
{
|
|
if (existing != registration && existing->prefix.decodedPath == registration->prefix.decodedPath) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void RegisterApi(Ptr<IAsyncSocketServer> socketServer, Ptr<ApiRegistration> registration)
|
|
{
|
|
auto& registry = GetSocketHttpRegistry();
|
|
Ptr<RegistryEntry> entry;
|
|
bool creator = false;
|
|
SPIN_LOCK(registry.lock)
|
|
{
|
|
auto index = registry.entries.Keys().IndexOf(socketServer.Obj());
|
|
if (index != -1) entry = registry.entries.Values()[index];
|
|
}
|
|
if (!entry)
|
|
{
|
|
auto candidate = Ptr(new RegistryEntry(socketServer));
|
|
SPIN_LOCK(registry.lock)
|
|
{
|
|
auto index = registry.entries.Keys().IndexOf(socketServer.Obj());
|
|
if (index == -1)
|
|
{
|
|
entry = candidate;
|
|
entry->registrations.Add(registration);
|
|
registration->entry = entry;
|
|
registry.entries.Add(socketServer.Obj(), entry);
|
|
creator = true;
|
|
}
|
|
else entry = registry.entries.Values()[index];
|
|
}
|
|
}
|
|
if (!creator)
|
|
{
|
|
CHECK_ERROR(entry->readyEvent.Wait(), L"Failed to wait for a shared socket HTTP listener.");
|
|
bool joined = false, duplicate = false;
|
|
AsyncSocketServerStartFailure failure = AsyncSocketServerStartFailure::Other;
|
|
WString message;
|
|
SPIN_LOCK(registry.lock)
|
|
{
|
|
if (entry->succeeded && !entry->terminal && CurrentEntry(registry, entry))
|
|
{
|
|
duplicate = Duplicate(entry, registration);
|
|
if (!duplicate)
|
|
{
|
|
entry->registrations.Add(registration);
|
|
registration->entry = entry;
|
|
joined = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
failure = entry->failure;
|
|
message = entry->failureMessage;
|
|
}
|
|
}
|
|
CHECK_ERROR(!duplicate, L"A SocketHttpServerApi with the same normalized prefix has already started.");
|
|
if (joined) return;
|
|
if (message == WString::Empty) message = L"The injected async socket server stopped before the API could join it.";
|
|
throw AsyncSocketServerStartException(failure, message);
|
|
}
|
|
|
|
Ptr<SharedServer> server;
|
|
AsyncSocketServerStartFailure failure = AsyncSocketServerStartFailure::Other;
|
|
WString message;
|
|
bool started = false;
|
|
try
|
|
{
|
|
server = Ptr(new SharedServer(socketServer, entry, GetTimeoutControllerFactory()));
|
|
server->InitializeSelf(server);
|
|
server->Start();
|
|
started = true;
|
|
}
|
|
catch (const AsyncSocketServerStartException& exception) { failure = exception.GetFailure(); message = exception.Message(); }
|
|
catch (const Exception& exception) { message = exception.Message(); }
|
|
catch (...) { message = L"The socket HTTP listener failed to start."; }
|
|
|
|
bool published = false;
|
|
SPIN_LOCK(registry.lock)
|
|
{
|
|
if (started && !entry->terminal && CurrentEntry(registry, entry))
|
|
{
|
|
entry->server = server;
|
|
entry->succeeded = true;
|
|
published = true;
|
|
}
|
|
else
|
|
{
|
|
if (started)
|
|
{
|
|
failure = AsyncSocketServerStartFailure::Other;
|
|
message = L"The listener stopped while starting.";
|
|
}
|
|
else if (message == WString::Empty)
|
|
{
|
|
message = L"The socket HTTP listener failed to start.";
|
|
}
|
|
if (CurrentEntry(registry, entry)) registry.entries.Remove(entry->socketServer.Obj());
|
|
entry->registrations.Remove(registration.Obj());
|
|
registration->entry = nullptr;
|
|
entry->failure = failure;
|
|
entry->failureMessage = message;
|
|
entry->terminal = true;
|
|
}
|
|
}
|
|
entry->readyEvent.Signal();
|
|
if (published) return;
|
|
if (server) server->StopAndRelease();
|
|
throw AsyncSocketServerStartException(failure, message);
|
|
}
|
|
|
|
Ptr<HttpRequestServer> UnregisterApi(Ptr<ApiRegistration> registration)
|
|
{
|
|
Ptr<HttpRequestServer> server;
|
|
Ptr<RegistryEntry> entry;
|
|
auto& registry = GetSocketHttpRegistry();
|
|
SPIN_LOCK(registry.lock)
|
|
{
|
|
entry = registration->entry;
|
|
if (entry)
|
|
{
|
|
entry->registrations.Remove(registration.Obj());
|
|
registration->entry = nullptr;
|
|
if (entry->registrations.Count() == 0)
|
|
{
|
|
if (CurrentEntry(registry, entry)) registry.entries.Remove(entry->socketServer.Obj());
|
|
entry->terminal = true;
|
|
server = entry->server;
|
|
entry->server = nullptr;
|
|
}
|
|
}
|
|
}
|
|
return server;
|
|
}
|
|
|
|
List<Ptr<ApiRegistration>> Registrations(Ptr<RegistryEntry> entry)
|
|
{
|
|
List<Ptr<ApiRegistration>> result;
|
|
auto& registry = GetSocketHttpRegistry();
|
|
SPIN_LOCK(registry.lock)
|
|
{
|
|
if (!entry->terminal) for (auto item : entry->registrations) result.Add(item);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void UnexpectedStop(Ptr<RegistryEntry> entry)
|
|
{
|
|
List<Ptr<ApiRegistration>> stopping;
|
|
auto& registry = GetSocketHttpRegistry();
|
|
SPIN_LOCK(registry.lock)
|
|
{
|
|
if (!entry->terminal)
|
|
{
|
|
entry->terminal = true;
|
|
if (CurrentEntry(registry, entry)) registry.entries.Remove(entry->socketServer.Obj());
|
|
for (auto registration : entry->registrations) { registration->entry = nullptr; stopping.Add(registration); }
|
|
entry->registrations.Clear();
|
|
entry->server = nullptr;
|
|
}
|
|
}
|
|
for (auto registration : stopping) registration->Stop(true);
|
|
}
|
|
|
|
bool Match(const WString& prefix, const WString& path)
|
|
{
|
|
return prefix.Length() == 0 || path == prefix || (path.Length() > prefix.Length() && path.Left(prefix.Length()) == prefix && path[prefix.Length()] == L'/');
|
|
}
|
|
|
|
bool SingleHeader(Ptr<HttpRequest> request, const WString& name, WString& value, bool& exists)
|
|
{
|
|
auto count = CountHttpFields(request->headers, name);
|
|
if (count > 1) return false;
|
|
exists = count == 1;
|
|
if (!exists) return true;
|
|
return DecodeAsciiHttpFieldValue(FindHttpField(request->headers, name)->value, value);
|
|
}
|
|
|
|
WString Trim(const WString& text)
|
|
{
|
|
vint begin = 0, end = text.Length();
|
|
while (begin < end && (text[begin] == L' ' || text[begin] == L'\t')) begin++;
|
|
while (begin < end && (text[end - 1] == L' ' || text[end - 1] == L'\t')) end--;
|
|
return text.Sub(begin, end - begin);
|
|
}
|
|
|
|
bool Preflight(Ptr<HttpRequest> request, bool& present, bool& supportedMethod)
|
|
{
|
|
present = false;
|
|
supportedMethod = false;
|
|
WString method;
|
|
bool exists = false;
|
|
if (!SingleHeader(request, L"access-control-request-method", method, exists)) return false;
|
|
if (!exists) return true;
|
|
present = true;
|
|
method = Trim(method);
|
|
supportedMethod = method == L"GET" || method == L"HEAD" || method == L"POST" || method == L"OPTIONS";
|
|
if (method.Length() == 0) return false;
|
|
for (auto&& field : request->headers)
|
|
{
|
|
if (field.name != L"access-control-request-headers") continue;
|
|
WString headers;
|
|
if (!DecodeAsciiHttpFieldValue(field.value, headers)) return false;
|
|
vint reading = 0;
|
|
while (reading <= headers.Length())
|
|
{
|
|
vint comma = reading;
|
|
while (comma < headers.Length() && headers[comma] != L',') comma++;
|
|
auto item = Lower(Trim(headers.Sub(reading, comma - reading)));
|
|
if (item.Length() == 0 || (item != L"accept" && item != L"content-type")) return false;
|
|
if (comma == headers.Length()) break;
|
|
reading = comma + 1;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void SetSocketHttpServerTimeoutControllerFactoryForTesting(const Func<Ptr<IHttpRequestTimeoutController>()>& factory)
|
|
{
|
|
auto& registry = GetSocketHttpRegistry();
|
|
SPIN_LOCK(registry.lock)
|
|
{
|
|
CHECK_ERROR(registry.entries.Count() == 0, L"The test timeout controller factory can only change while no API is started.");
|
|
registry.timeoutControllerFactory = factory;
|
|
}
|
|
}
|
|
|
|
void ResetSocketHttpServerTimeoutControllerFactoryForTesting() { SetSocketHttpServerTimeoutControllerFactoryForTesting({}); }
|
|
|
|
WaitForClientResult SharedServer::OnClientConnected(IHttpRequestConnection* connection)
|
|
{
|
|
Ptr<RegistryEntry> retained;
|
|
CS_LOCK(lock) { retained = entry; }
|
|
if (!retained) return WaitForClientResult::Reject;
|
|
try
|
|
{
|
|
auto dispatcher = Ptr(new SocketHttpServerApiDispatcher(retained));
|
|
dispatcher->InitializeSelf(dispatcher);
|
|
connection->InstallCallback(dispatcher.Obj());
|
|
return WaitForClientResult::Accept;
|
|
}
|
|
catch (...) { return WaitForClientResult::Reject; }
|
|
}
|
|
|
|
void SharedServer::StopAndRelease()
|
|
{
|
|
Ptr<SharedServer> retained;
|
|
CS_LOCK(lock) { retained = selfReference; }
|
|
try { HttpRequestServer::Stop(); } catch (...) {}
|
|
CS_LOCK(lock) { entry = nullptr; selfReference = nullptr; }
|
|
}
|
|
|
|
void SharedServer::OnServerStopped()
|
|
{
|
|
Ptr<SharedServer> retained;
|
|
Ptr<RegistryEntry> retainedEntry;
|
|
CS_LOCK(lock) { retained = selfReference; retainedEntry = entry; }
|
|
if (retainedEntry) UnexpectedStop(retainedEntry);
|
|
try { HttpRequestServer::OnServerStopped(); } catch (...) {}
|
|
CS_LOCK(lock) { entry = nullptr; selfReference = nullptr; }
|
|
}
|
|
}
|
|
|
|
namespace vl::inter_process::async_tcp_socket
|
|
{
|
|
using namespace collections;
|
|
|
|
Ptr<SocketHttpServerApiDispatcher> SocketHttpServerApiDispatcher::Retain()
|
|
{
|
|
Ptr<SocketHttpServerApiDispatcher> retained;
|
|
CS_LOCK(lockSelf) { retained = selfReference; }
|
|
return retained;
|
|
}
|
|
|
|
void SocketHttpServerApiDispatcher::SendAutomatic(vint code, bool close, bool preflight)
|
|
{
|
|
auto response = Normalize(Automatic(code, close, preflight), L"GET");
|
|
IHttpRequestConnection* connection = nullptr;
|
|
CS_LOCK(state->lock)
|
|
{
|
|
if (state->terminal || state->automaticWrite || state->context) return;
|
|
state->automaticWrite = true;
|
|
state->closeAfterWrite = close;
|
|
connection = state->connection;
|
|
}
|
|
if (!connection) return;
|
|
try
|
|
{
|
|
connection->SendResponse(response);
|
|
}
|
|
catch (...)
|
|
{
|
|
CS_LOCK(state->lock)
|
|
{
|
|
state->automaticWrite = false;
|
|
state->terminal = true;
|
|
}
|
|
try { connection->Stop(); } catch (...) {}
|
|
}
|
|
}
|
|
|
|
void SocketHttpServerApiDispatcher::Process(Ptr<HttpRequest> request)
|
|
{
|
|
if (!request || request->version.major != 1 || request->version.minor != 1)
|
|
{
|
|
SendAutomatic(505, true);
|
|
return;
|
|
}
|
|
|
|
WString host;
|
|
bool hostExists = false;
|
|
if (!SingleHeader(request, L"host", host, hostExists) || !hostExists)
|
|
{
|
|
SendAutomatic(400, true);
|
|
return;
|
|
}
|
|
vint port = 0;
|
|
if (!ParseAuthority(host, port))
|
|
{
|
|
SendAutomatic(400, true);
|
|
return;
|
|
}
|
|
|
|
auto supportedMethod = request->method == L"GET" || request->method == L"HEAD" || request->method == L"POST" || request->method == L"OPTIONS";
|
|
if (!supportedMethod)
|
|
{
|
|
SendAutomatic(501, false);
|
|
return;
|
|
}
|
|
|
|
auto registrations = Registrations(state->entry);
|
|
auto matchesPort = state->entry->socketServer->GetPort() == port;
|
|
if (request->requestTarget == L"*")
|
|
{
|
|
if (request->method != L"OPTIONS")
|
|
{
|
|
SendAutomatic(400, true);
|
|
return;
|
|
}
|
|
bool automaticOptions = false;
|
|
for (auto registration : registrations)
|
|
{
|
|
if (matchesPort && registration->respondToOptions)
|
|
{
|
|
automaticOptions = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!automaticOptions)
|
|
{
|
|
SendAutomatic(501, false);
|
|
return;
|
|
}
|
|
bool present = false, methodSupported = false;
|
|
if (!Preflight(request, present, methodSupported))
|
|
{
|
|
SendAutomatic(400, false, true);
|
|
return;
|
|
}
|
|
SendAutomatic(present && !methodSupported ? 405 : 200, false, true);
|
|
return;
|
|
}
|
|
|
|
auto target = request->requestTarget;
|
|
if (target.Length() == 0 || target[0] != L'/' || target.IndexOf(L'#') != -1)
|
|
{
|
|
SendAutomatic(400, true);
|
|
return;
|
|
}
|
|
vint question = target.IndexOf(L'?');
|
|
auto rawPath = question == -1 ? target : target.Left(question);
|
|
auto query = question == -1 ? WString::Empty : target.Right(target.Length() - question - 1);
|
|
WString path;
|
|
if (!DecodePath(rawPath, path))
|
|
{
|
|
SendAutomatic(400, true);
|
|
return;
|
|
}
|
|
|
|
Ptr<ApiRegistration> selected;
|
|
for (auto registration : registrations)
|
|
{
|
|
if (matchesPort && Match(registration->prefix.decodedPath, path))
|
|
{
|
|
if (!selected || registration->prefix.decodedPath.Length() > selected->prefix.decodedPath.Length()) selected = registration;
|
|
}
|
|
}
|
|
if (!selected)
|
|
{
|
|
SendAutomatic(404, false);
|
|
return;
|
|
}
|
|
|
|
if (request->method == L"OPTIONS" && selected->respondToOptions)
|
|
{
|
|
bool present = false, methodSupported = false;
|
|
if (!Preflight(request, present, methodSupported))
|
|
{
|
|
SendAutomatic(400, false, true);
|
|
return;
|
|
}
|
|
if (present)
|
|
{
|
|
SendAutomatic(methodSupported ? 200 : 405, false, true);
|
|
return;
|
|
}
|
|
}
|
|
|
|
WString relative;
|
|
if (selected->prefix.decodedPath.Length() == 0) relative = path;
|
|
else if (path == selected->prefix.decodedPath) relative = L"/";
|
|
else relative = path.Right(path.Length() - selected->prefix.decodedPath.Length());
|
|
if (relative.Length() == 0) relative = L"/";
|
|
|
|
auto contextImpl = Ptr(new SocketHttpRequestContext::Impl(state, selected, request, relative, query));
|
|
auto context = Ptr(new SocketHttpRequestContext(contextImpl));
|
|
contextImpl->owner = context.Obj();
|
|
auto cancelForStop = Func<void()>([contextImpl]()
|
|
{
|
|
contextImpl->Finish(false, true, true, true);
|
|
});
|
|
bool installed = false;
|
|
CS_LOCK(state->lock)
|
|
{
|
|
if (!state->terminal && !state->context && !state->automaticWrite)
|
|
{
|
|
state->context = context;
|
|
installed = true;
|
|
}
|
|
}
|
|
if (!installed || !selected->AddContext(context, cancelForStop))
|
|
{
|
|
CS_LOCK(state->lock) { if (state->context == context) state->context = nullptr; }
|
|
SendAutomatic(404, false);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (!selected->InvokeRequest(context)) context->Cancel();
|
|
}
|
|
catch (...)
|
|
{
|
|
context->Respond(Automatic(500, false));
|
|
}
|
|
}
|
|
|
|
void SocketHttpServerApiDispatcher::OnReadRequest(Ptr<HttpRequest> request)
|
|
{
|
|
auto retained = Retain();
|
|
if (!retained) return;
|
|
try { Process(request); } catch (...) { SendAutomatic(500, true); }
|
|
}
|
|
|
|
void SocketHttpServerApiDispatcher::OnReadRequestFailure(HttpRequestFailure failure)
|
|
{
|
|
auto retained = Retain();
|
|
if (!retained) return;
|
|
SendAutomatic((vint)failure, true);
|
|
}
|
|
|
|
void SocketHttpServerApiDispatcher::OnWriteCompleted()
|
|
{
|
|
auto retained = Retain();
|
|
if (!retained) return;
|
|
Ptr<SocketHttpRequestContext> context;
|
|
bool automatic = false, close = false;
|
|
CS_LOCK(state->lock)
|
|
{
|
|
context = state->context;
|
|
if (!context && state->automaticWrite)
|
|
{
|
|
automatic = true;
|
|
close = state->closeAfterWrite;
|
|
state->automaticWrite = false;
|
|
state->closeAfterWrite = false;
|
|
}
|
|
}
|
|
if (context) context->impl->Finish(true, false, true, false);
|
|
if (!context && !automatic) return;
|
|
|
|
if (close)
|
|
{
|
|
IHttpRequestConnection* stopping = nullptr;
|
|
CS_LOCK(state->lock)
|
|
{
|
|
state->terminal = true;
|
|
stopping = state->connection;
|
|
}
|
|
if (stopping) try { stopping->Stop(); } catch (...) {}
|
|
}
|
|
}
|
|
|
|
void SocketHttpServerApiDispatcher::OnError(const WString&, bool fatal)
|
|
{
|
|
auto retained = Retain();
|
|
if (!retained) return;
|
|
Ptr<SocketHttpRequestContext> context;
|
|
IHttpRequestConnection* connection = nullptr;
|
|
CS_LOCK(state->lock)
|
|
{
|
|
context = state->context;
|
|
if (fatal || context || state->automaticWrite)
|
|
{
|
|
state->terminal = true;
|
|
connection = state->connection;
|
|
}
|
|
}
|
|
if (context) context->impl->Finish(false, true, true, true);
|
|
else if (connection) try { connection->Stop(); } catch (...) {}
|
|
}
|
|
|
|
void SocketHttpServerApiDispatcher::OnDisconnected()
|
|
{
|
|
auto retained = Retain();
|
|
if (!retained) return;
|
|
Ptr<SocketHttpRequestContext> context;
|
|
CS_LOCK(state->lock)
|
|
{
|
|
state->terminal = true;
|
|
state->connection = nullptr;
|
|
state->automaticWrite = false;
|
|
context = state->context;
|
|
}
|
|
if (context) context->impl->Finish(false, true, true, false);
|
|
CS_LOCK(lockSelf) { selfReference = nullptr; }
|
|
}
|
|
|
|
void SocketHttpServerApiDispatcher::OnInstalled(IHttpRequestConnection* connection)
|
|
{
|
|
CS_LOCK(state->lock) { state->connection = connection; }
|
|
connection->BeginReadingLoopUnsafe();
|
|
}
|
|
|
|
SocketHttpRequestContext::SocketHttpRequestContext(Ptr<Impl> _impl) : impl(_impl) {}
|
|
SocketHttpRequestContext::~SocketHttpRequestContext() {}
|
|
Ptr<HttpRequest> SocketHttpRequestContext::GetRequest() { return impl->request; }
|
|
WString SocketHttpRequestContext::GetRelativePath() { return impl->relativePath; }
|
|
WString SocketHttpRequestContext::GetQuery() { return impl->query; }
|
|
bool SocketHttpRequestContext::TryGetBodyUtf8(WString& body)
|
|
{
|
|
Array<vuint8_t> bytes;
|
|
if (!FlattenHttpBody(impl->request->body, bytes)) return false;
|
|
return ::vl::inter_process::async_tcp_socket::DecodeStrictUtf8(
|
|
bytes.Count() == 0 ? nullptr : &bytes[0],
|
|
bytes.Count(),
|
|
body
|
|
);
|
|
}
|
|
|
|
bool SocketHttpRequestContext::Respond(Ptr<HttpResponse> response, Func<void(bool)> completion)
|
|
{
|
|
Ptr<HttpResponse> normalized;
|
|
IHttpRequestConnection* connection = nullptr;
|
|
CS_LOCK(impl->lock)
|
|
{
|
|
if (impl->state != Impl::State::Pending) return false;
|
|
normalized = Normalize(response, impl->request->method);
|
|
impl->state = Impl::State::Sending;
|
|
impl->completion = completion;
|
|
}
|
|
CS_LOCK(impl->connectionState->lock)
|
|
{
|
|
if (!impl->connectionState->terminal && impl->connectionState->context.Obj() == this) connection = impl->connectionState->connection;
|
|
}
|
|
if (!connection)
|
|
{
|
|
impl->Finish(false, false, true, false);
|
|
return true;
|
|
}
|
|
try { connection->SendResponse(normalized); }
|
|
catch (...) { impl->Finish(false, false, true, true); }
|
|
return true;
|
|
}
|
|
|
|
bool SocketHttpRequestContext::RespondStatus(vint statusCode, const WString& reason, Func<void(bool)> completion)
|
|
{
|
|
ValidateConvenienceResponseArguments(statusCode, reason, WString::Empty, 0);
|
|
Array<vuint8_t> body;
|
|
return Respond(CreateConvenienceResponse(statusCode, reason, WString::Empty, std::move(body)), completion);
|
|
}
|
|
|
|
bool SocketHttpRequestContext::RespondBytes(vint statusCode, const WString& reason, const WString& contentType, const Array<vuint8_t>& body, Func<void(bool)> completion)
|
|
{
|
|
ValidateConvenienceResponseArguments(statusCode, reason, contentType, body.Count());
|
|
Array<vuint8_t> copy(body.Count());
|
|
if (body.Count() > 0) memcpy(©[0], &body[0], body.Count());
|
|
return Respond(CreateConvenienceResponse(statusCode, reason, contentType, std::move(copy)), completion);
|
|
}
|
|
|
|
bool SocketHttpRequestContext::RespondUtf8(vint statusCode, const WString& reason, const WString& contentType, const WString& body, Func<void(bool)> completion)
|
|
{
|
|
ValidateConvenienceResponseArguments(statusCode, reason, contentType, 0);
|
|
Array<vuint8_t> encoded;
|
|
CHECK_ERROR(EncodeStrictUtf8(body, encoded), L"The response body contains invalid Unicode.");
|
|
CHECK_ERROR(encoded.Count() <= HttpBodySizeLimit, L"The response body is too large.");
|
|
return Respond(CreateConvenienceResponse(statusCode, reason, contentType, std::move(encoded)), completion);
|
|
}
|
|
|
|
bool SocketHttpRequestContext::Cancel()
|
|
{
|
|
return impl->Finish(false, true, false, true);
|
|
}
|
|
}
|
|
|
|
namespace vl::inter_process::async_tcp_socket
|
|
{
|
|
class SocketHttpServerApi::Impl : public Object
|
|
{
|
|
private:
|
|
SocketHttpServerApi* owner;
|
|
Ptr<IAsyncSocketServer> socketServer;
|
|
Ptr<ApiRegistration> registration;
|
|
CriticalSection lock;
|
|
ConditionVariable cv;
|
|
bool startCalled = false;
|
|
bool startInProgress = false;
|
|
bool startSucceeded = false;
|
|
bool stopRequested = false;
|
|
bool stopRequestedNotify = false;
|
|
bool stopStarted = false;
|
|
bool stopFinished = false;
|
|
|
|
void Request(Ptr<SocketHttpRequestContext> context)
|
|
{
|
|
SocketHttpServerApi* target = nullptr;
|
|
CS_LOCK(lock) { target = owner; }
|
|
if (target) target->OnHttpRequestReceived(context);
|
|
else context->Cancel();
|
|
}
|
|
|
|
void Stopping()
|
|
{
|
|
SocketHttpServerApi* target = nullptr;
|
|
CS_LOCK(lock) { target = owner; }
|
|
if (target) target->OnHttpServerStopping();
|
|
}
|
|
|
|
void StopInternal(bool notify)
|
|
{
|
|
bool first = false;
|
|
bool notifyRegistration = false;
|
|
auto callbackDepth = ApiRegistration::Depth(registration.Obj());
|
|
lock.Enter();
|
|
if (startInProgress && callbackDepth > 0)
|
|
{
|
|
stopRequested = true;
|
|
stopRequestedNotify |= notify;
|
|
lock.Leave();
|
|
return;
|
|
}
|
|
while (startInProgress) cv.SleepWith(lock);
|
|
if (!stopStarted)
|
|
{
|
|
stopStarted = true;
|
|
first = true;
|
|
notifyRegistration = notify && startSucceeded;
|
|
}
|
|
else if (callbackDepth > 0)
|
|
{
|
|
lock.Leave();
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
while (!stopFinished) cv.SleepWith(lock);
|
|
}
|
|
lock.Leave();
|
|
if (!first) return;
|
|
|
|
auto server = UnregisterApi(registration);
|
|
registration->Stop(notifyRegistration);
|
|
if (server)
|
|
{
|
|
auto shared = server.Cast<SharedServer>();
|
|
if (shared) shared->StopAndRelease();
|
|
else server->Stop();
|
|
}
|
|
CS_LOCK(lock)
|
|
{
|
|
stopFinished = true;
|
|
cv.WakeAllPendings();
|
|
}
|
|
}
|
|
|
|
public:
|
|
Impl(SocketHttpServerApi* _owner, Ptr<IAsyncSocketServer> _socketServer, const WString& urlPrefix, bool respondToOptions)
|
|
: owner(_owner)
|
|
, socketServer(_socketServer)
|
|
{
|
|
#define ERROR_PREFIX L"vl::inter_process::async_tcp_socket::SocketHttpServerApi::SocketHttpServerApi(Ptr<IAsyncSocketServer>, const WString&, bool)#"
|
|
CHECK_ERROR(socketServer, ERROR_PREFIX L"Requires a server.");
|
|
registration = Ptr(new ApiRegistration(ParsePrefix(socketServer->GetPort(), urlPrefix), respondToOptions));
|
|
registration->requestCallback = Func<void(Ptr<SocketHttpRequestContext>)>([this](Ptr<SocketHttpRequestContext> context) { Request(context); });
|
|
registration->stoppingCallback = Func<void()>([this]() { Stopping(); });
|
|
#undef ERROR_PREFIX
|
|
}
|
|
|
|
void Start()
|
|
{
|
|
#define ERROR_PREFIX L"vl::inter_process::async_tcp_socket::SocketHttpServerApi::Start()#"
|
|
CS_LOCK(lock)
|
|
{
|
|
CHECK_ERROR(!startCalled && !stopStarted, ERROR_PREFIX L"Can only be called once before stopping.");
|
|
startCalled = true;
|
|
startInProgress = true;
|
|
}
|
|
try
|
|
{
|
|
RegisterApi(socketServer, registration);
|
|
}
|
|
catch (...)
|
|
{
|
|
bool stopAfterStart = false, notifyAfterStart = false;
|
|
CS_LOCK(lock)
|
|
{
|
|
startInProgress = false;
|
|
stopAfterStart = stopRequested;
|
|
notifyAfterStart = stopRequestedNotify;
|
|
stopRequested = false;
|
|
stopRequestedNotify = false;
|
|
cv.WakeAllPendings();
|
|
}
|
|
if (stopAfterStart)
|
|
{
|
|
try { StopInternal(notifyAfterStart); } catch (...) {}
|
|
}
|
|
throw;
|
|
}
|
|
bool stopAfterStart = false, notifyAfterStart = false;
|
|
CS_LOCK(lock)
|
|
{
|
|
startSucceeded = true;
|
|
startInProgress = false;
|
|
stopAfterStart = stopRequested;
|
|
notifyAfterStart = stopRequestedNotify;
|
|
stopRequested = false;
|
|
stopRequestedNotify = false;
|
|
cv.WakeAllPendings();
|
|
}
|
|
if (stopAfterStart) StopInternal(notifyAfterStart);
|
|
#undef ERROR_PREFIX
|
|
}
|
|
|
|
void Stop() { StopInternal(true); }
|
|
|
|
void Destroy()
|
|
{
|
|
CS_LOCK(lock) { owner = nullptr; }
|
|
StopInternal(false);
|
|
}
|
|
|
|
bool IsStopped()
|
|
{
|
|
CS_LOCK(lock)
|
|
{
|
|
if (stopStarted) return true;
|
|
}
|
|
return registration->IsStopped();
|
|
}
|
|
|
|
WString GetUrlPrefix() { return registration->prefix.normalizedUrl; }
|
|
};
|
|
|
|
SocketHttpServerApi::SocketHttpServerApi(Ptr<IAsyncSocketServer> server, const WString& urlPrefix, bool respondToOptions)
|
|
: impl(Ptr(new Impl(this, server, urlPrefix, respondToOptions)))
|
|
{
|
|
}
|
|
|
|
SocketHttpServerApi::~SocketHttpServerApi() { impl->Destroy(); }
|
|
void SocketHttpServerApi::OnHttpServerStopping() {}
|
|
void SocketHttpServerApi::Start() { impl->Start(); }
|
|
void SocketHttpServerApi::Stop() { impl->Stop(); }
|
|
bool SocketHttpServerApi::IsStopped() { return impl->IsStopped(); }
|
|
WString SocketHttpServerApi::GetUrlPrefix() { return impl->GetUrlPrefix(); }
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\INTERPROCESS\NETWORKPROTOCOLHTTP.CPP
|
|
***********************************************************************/
|
|
|
|
namespace vl::inter_process
|
|
{
|
|
using namespace vl::collections;
|
|
|
|
namespace
|
|
{
|
|
void EncodeUtf8(const WString& text, Array<char>& utf8)
|
|
{
|
|
stream::MemoryStream memoryStream;
|
|
{
|
|
stream::Utf8Encoder encoder;
|
|
stream::EncoderStream encoderStream(memoryStream, encoder);
|
|
stream::StreamWriter writer(encoderStream);
|
|
writer.WriteString(text);
|
|
}
|
|
|
|
utf8.Resize((vint)memoryStream.Size());
|
|
if (utf8.Count() > 0)
|
|
{
|
|
memoryStream.SeekFromBegin(0);
|
|
memoryStream.Read(&utf8[0], utf8.Count());
|
|
}
|
|
}
|
|
|
|
WString DecodeUtf8(const void* buffer, vint size)
|
|
{
|
|
if (size == 0)
|
|
{
|
|
return WString::Empty;
|
|
}
|
|
|
|
stream::MemoryWrapperStream memoryStream((void*)buffer, size);
|
|
stream::Utf8Decoder decoder;
|
|
stream::DecoderStream decoderStream(memoryStream, decoder);
|
|
stream::StreamReader reader(decoderStream);
|
|
return reader.ReadToEnd();
|
|
}
|
|
|
|
vint HexValue(char c)
|
|
{
|
|
if ('0' <= c && c <= '9') return c - '0';
|
|
if ('a' <= c && c <= 'f') return c - 'a' + 10;
|
|
if ('A' <= c && c <= 'F') return c - 'A' + 10;
|
|
return -1;
|
|
}
|
|
|
|
bool IsHttpNetworkProtocolPathCharacter(wchar_t c)
|
|
{
|
|
if (L'a' <= c && c <= L'z') return true;
|
|
if (L'A' <= c && c <= L'Z') return true;
|
|
if (L'0' <= c && c <= L'9') return true;
|
|
switch (c)
|
|
{
|
|
case L'-': case L'.': case L'_': case L'~':
|
|
case L'!': case L'$': case L'&': case L'\'': case L'(':
|
|
case L')': case L'*': case L'+': case L',': case L';': case L'=':
|
|
case L':': case L'@': case L'/':
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool ValidateHttpNetworkProtocolPath(const WString& path, bool allowEmpty, bool rejectTrailingSlash)
|
|
{
|
|
if (path.Length() == 0) return allowEmpty;
|
|
if (path[0] != L'/') return false;
|
|
if (rejectTrailingSlash && path[path.Length() - 1] == L'/') return false;
|
|
|
|
List<vuint8_t> decoded;
|
|
for (vint i = 0; i < path.Length(); i++)
|
|
{
|
|
wchar_t c = path[i];
|
|
if (c == L'%')
|
|
{
|
|
if (i + 2 >= path.Length()) return false;
|
|
wchar_t highChar = path[i + 1];
|
|
wchar_t lowChar = path[i + 2];
|
|
if (highChar > 0x7F || lowChar > 0x7F) return false;
|
|
vint high = HexValue((char)highChar);
|
|
vint low = HexValue((char)lowChar);
|
|
if (high == -1 || low == -1) return false;
|
|
vuint8_t byte = (vuint8_t)(high * 16 + low);
|
|
if (byte == 0 || byte == '/' || byte == '\\') return false;
|
|
decoded.Add(byte);
|
|
i += 2;
|
|
}
|
|
else
|
|
{
|
|
if (c > 0x7F || !IsHttpNetworkProtocolPathCharacter(c)) return false;
|
|
decoded.Add((vuint8_t)c);
|
|
}
|
|
}
|
|
|
|
WString ignored;
|
|
return async_tcp_socket::DecodeStrictUtf8(
|
|
decoded.Count() == 0 ? nullptr : &decoded[0],
|
|
decoded.Count(),
|
|
ignored
|
|
);
|
|
}
|
|
}
|
|
|
|
WString CreateHttpNetworkProtocolConnectBody(const WString& requestPath, const WString& responsePath)
|
|
{
|
|
CHECK_ERROR(requestPath.Length() > 0, L"CreateHttpNetworkProtocolConnectBody(const WString&, const WString&): The request path cannot be empty.");
|
|
CHECK_ERROR(responsePath.Length() > 0, L"CreateHttpNetworkProtocolConnectBody(const WString&, const WString&): The response path cannot be empty.");
|
|
CHECK_ERROR(requestPath.IndexOf(L';') == -1, L"CreateHttpNetworkProtocolConnectBody(const WString&, const WString&): The request path cannot contain a semicolon.");
|
|
CHECK_ERROR(responsePath.IndexOf(L';') == -1, L"CreateHttpNetworkProtocolConnectBody(const WString&, const WString&): The response path cannot contain a semicolon.");
|
|
return requestPath + L";" + responsePath;
|
|
}
|
|
|
|
bool ParseHttpNetworkProtocolConnectBody(const WString& body, WString& requestPath, WString& responsePath)
|
|
{
|
|
vint delimiter = body.IndexOf(L';');
|
|
if (delimiter <= 0 || delimiter == body.Length() - 1) return false;
|
|
if (body.Right(body.Length() - delimiter - 1).IndexOf(L';') != -1) return false;
|
|
|
|
WString parsedRequestPath = body.Left(delimiter);
|
|
WString parsedResponsePath = body.Right(body.Length() - delimiter - 1);
|
|
requestPath = parsedRequestPath;
|
|
responsePath = parsedResponsePath;
|
|
return true;
|
|
}
|
|
|
|
bool ValidateHttpNetworkProtocolBaseUrl(const WString& baseUrl)
|
|
{
|
|
return ValidateHttpNetworkProtocolPath(baseUrl, true, true);
|
|
}
|
|
|
|
bool ValidateHttpNetworkProtocolEndpointPath(const WString& path)
|
|
{
|
|
return ValidateHttpNetworkProtocolPath(path, false, false);
|
|
}
|
|
|
|
bool IsValidHttpNetworkProtocolMessage(const WString& message)
|
|
{
|
|
if (message.Length() == 0) return false;
|
|
for (vint i = 0; i < message.Length(); i++)
|
|
{
|
|
if (message[i] == 0) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
WString HttpUrlEncodeQuery(const WString& query)
|
|
{
|
|
Array<char> utf8;
|
|
EncodeUtf8(query, utf8);
|
|
|
|
Array<wchar_t> encoded(utf8.Count() * 3 + 1);
|
|
wchar_t* writing = &encoded[0];
|
|
for (vint i = 0; i < utf8.Count(); i++)
|
|
{
|
|
vuint8_t x = (vuint8_t)utf8[i];
|
|
if ((L'a' <= x && x <= L'z') || (L'A' <= x && x <= L'Z') || (L'0' <= x && x <= L'9'))
|
|
{
|
|
*writing++ = (wchar_t)x;
|
|
}
|
|
else
|
|
{
|
|
*writing++ = L'%';
|
|
*writing++ = L"0123456789ABCDEF"[x / 16];
|
|
*writing++ = L"0123456789ABCDEF"[x % 16];
|
|
}
|
|
}
|
|
*writing = 0;
|
|
return &encoded[0];
|
|
}
|
|
|
|
WString HttpUrlDecodeQuery(const WString& query)
|
|
{
|
|
Array<char> encoded;
|
|
EncodeUtf8(query, encoded);
|
|
|
|
List<char> utf8;
|
|
for (vint i = 0; i < encoded.Count(); i++)
|
|
{
|
|
char c = encoded[i];
|
|
if (c == '%' && i + 2 < encoded.Count())
|
|
{
|
|
vint high = HexValue(encoded[i + 1]);
|
|
vint low = HexValue(encoded[i + 2]);
|
|
if (high != -1 && low != -1)
|
|
{
|
|
utf8.Add((char)(high * 16 + low));
|
|
i += 2;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
utf8.Add(c == '+' ? ' ' : c);
|
|
}
|
|
|
|
return utf8.Count() == 0
|
|
? WString::Empty
|
|
: DecodeUtf8(&utf8[0], utf8.Count());
|
|
}
|
|
}
|
|
|
|
namespace vl::inter_process::windows_http
|
|
{
|
|
void HttpRequest::SetBodyUtf8(const WString& bodyString)
|
|
{
|
|
EncodeUtf8(bodyString, body);
|
|
}
|
|
|
|
bool HttpResponse::TryGetBodyUtf8(WString& bodyString) const
|
|
{
|
|
return ::vl::inter_process::async_tcp_socket::DecodeStrictUtf8(
|
|
body.Count() == 0 ? nullptr : reinterpret_cast<const vuint8_t*>(&body[0]),
|
|
body.Count(),
|
|
bodyString
|
|
);
|
|
}
|
|
|
|
WString HttpResponse::GetBodyUtf8() const
|
|
{
|
|
return body.Count() == 0
|
|
? WString::Empty
|
|
: DecodeUtf8(&body[0], body.Count());
|
|
}
|
|
}
|
|
|
|
namespace vl::inter_process
|
|
{
|
|
windows_http::HttpRequest CreateHttpNetworkProtocolConnectRequest(const WString& target)
|
|
{
|
|
windows_http::HttpRequest request;
|
|
request.method = L"GET";
|
|
request.query = target;
|
|
request.acceptTypes.Add(HttpNetworkProtocolContentType);
|
|
return request;
|
|
}
|
|
|
|
windows_http::HttpRequest CreateHttpNetworkProtocolReceiveRequest(const WString& target)
|
|
{
|
|
windows_http::HttpRequest request;
|
|
request.method = L"POST";
|
|
request.query = target;
|
|
request.acceptTypes.Add(HttpNetworkProtocolContentType);
|
|
request.extraHeaders.Add(L"Content-Length", L"0");
|
|
return request;
|
|
}
|
|
|
|
windows_http::HttpRequest CreateHttpNetworkProtocolSendRequest(const WString& target, const Array<char>& body)
|
|
{
|
|
windows_http::HttpRequest request;
|
|
request.method = L"POST";
|
|
request.query = target;
|
|
request.acceptTypes.Add(HttpNetworkProtocolContentType);
|
|
request.contentType = HttpNetworkProtocolContentType;
|
|
request.body.Resize(body.Count());
|
|
for (vint i = 0; i < body.Count(); i++)
|
|
{
|
|
request.body[i] = body[i];
|
|
}
|
|
return request;
|
|
}
|
|
}
|
|
|
|
|
|
/***********************************************************************
|
|
.\TUI\TUI.CPP
|
|
***********************************************************************/
|
|
/***********************************************************************
|
|
Author: Zihan Chen (vczh)
|
|
Licensed under https://github.com/vczh-libraries/License
|
|
***********************************************************************/
|
|
|
|
#include <exception>
|
|
|
|
using namespace vl;
|
|
using namespace vl::collections;
|
|
|
|
namespace vl
|
|
{
|
|
namespace console
|
|
{
|
|
namespace tui_internal
|
|
{
|
|
struct ListenerEntry
|
|
{
|
|
ITuiCallback* listener = nullptr;
|
|
vuint64_t generation = 0;
|
|
};
|
|
}
|
|
|
|
class TUI::Impl
|
|
{
|
|
public:
|
|
bool active = false;
|
|
bool stopRequested = false;
|
|
bool shuttingDown = false;
|
|
bool backendStarted = false;
|
|
bool consoleDisabled = false;
|
|
vint ownerThreadId = -1;
|
|
vint width = 0;
|
|
vint height = 0;
|
|
vint timerPeriod = 0;
|
|
vuint64_t nextTimer = 0;
|
|
TuiColorMode colorMode = TuiColorMode::Auto;
|
|
Array<TuiPixel> buffer;
|
|
List<unittest::TuiBackendEvent> eventQueue;
|
|
Ptr<unittest::ITuiBackend> backend;
|
|
std::exception_ptr callbackException;
|
|
std::exception_ptr* cleanupException = nullptr;
|
|
|
|
~Impl()
|
|
{
|
|
if (backendStarted)
|
|
{
|
|
try
|
|
{
|
|
backend->Stop();
|
|
}
|
|
catch (...)
|
|
{
|
|
if (cleanupException && !*cleanupException)
|
|
{
|
|
*cleanupException = std::current_exception();
|
|
}
|
|
}
|
|
}
|
|
if (consoleDisabled)
|
|
{
|
|
Console::Enable();
|
|
}
|
|
}
|
|
};
|
|
|
|
class TUI::ListenerStorage
|
|
{
|
|
public:
|
|
vuint64_t nextGeneration = 0;
|
|
List<tui_internal::ListenerEntry> listeners;
|
|
};
|
|
|
|
TUI::Impl* TUI::impl = nullptr;
|
|
TUI::ListenerStorage* TUI::listenerStorage = nullptr;
|
|
Ptr<unittest::ITuiBackend>* TUI::injectedBackend = nullptr;
|
|
|
|
namespace tui_internal
|
|
{
|
|
constexpr vuint8_t PackState(vuint8_t up, vuint8_t down, vuint8_t left, vuint8_t right)
|
|
{
|
|
return (vuint8_t)((up << 6) | (down << 4) | (left << 2) | right);
|
|
}
|
|
|
|
char32_t GetMergeableChar(const TuiMergeablePixel& pixel)
|
|
{
|
|
auto up = (vuint8_t)pixel.up;
|
|
auto down = (vuint8_t)pixel.down;
|
|
auto left = (vuint8_t)pixel.left;
|
|
auto right = (vuint8_t)pixel.right;
|
|
if (up > 3 || down > 3 || left > 3 || right > 3) return 0;
|
|
|
|
switch (PackState(up, down, left, right))
|
|
{
|
|
case PackState(0, 0, 0, 0): return 0;
|
|
case PackState(0, 0, 1, 1): return U'\u2500';
|
|
case PackState(0, 0, 2, 2): return U'\u2501';
|
|
case PackState(1, 1, 0, 0): return U'\u2502';
|
|
case PackState(2, 2, 0, 0): return U'\u2503';
|
|
case PackState(0, 1, 0, 1): return U'\u250C';
|
|
case PackState(0, 1, 0, 2): return U'\u250D';
|
|
case PackState(0, 2, 0, 1): return U'\u250E';
|
|
case PackState(0, 2, 0, 2): return U'\u250F';
|
|
case PackState(0, 1, 1, 0): return U'\u2510';
|
|
case PackState(0, 1, 2, 0): return U'\u2511';
|
|
case PackState(0, 2, 1, 0): return U'\u2512';
|
|
case PackState(0, 2, 2, 0): return U'\u2513';
|
|
case PackState(1, 0, 0, 1): return U'\u2514';
|
|
case PackState(1, 0, 0, 2): return U'\u2515';
|
|
case PackState(2, 0, 0, 1): return U'\u2516';
|
|
case PackState(2, 0, 0, 2): return U'\u2517';
|
|
case PackState(1, 0, 1, 0): return U'\u2518';
|
|
case PackState(1, 0, 2, 0): return U'\u2519';
|
|
case PackState(2, 0, 1, 0): return U'\u251A';
|
|
case PackState(2, 0, 2, 0): return U'\u251B';
|
|
case PackState(1, 1, 0, 1): return U'\u251C';
|
|
case PackState(1, 1, 0, 2): return U'\u251D';
|
|
case PackState(2, 1, 0, 1): return U'\u251E';
|
|
case PackState(1, 2, 0, 1): return U'\u251F';
|
|
case PackState(2, 2, 0, 1): return U'\u2520';
|
|
case PackState(2, 1, 0, 2): return U'\u2521';
|
|
case PackState(1, 2, 0, 2): return U'\u2522';
|
|
case PackState(2, 2, 0, 2): return U'\u2523';
|
|
case PackState(1, 1, 1, 0): return U'\u2524';
|
|
case PackState(1, 1, 2, 0): return U'\u2525';
|
|
case PackState(2, 1, 1, 0): return U'\u2526';
|
|
case PackState(1, 2, 1, 0): return U'\u2527';
|
|
case PackState(2, 2, 1, 0): return U'\u2528';
|
|
case PackState(2, 1, 2, 0): return U'\u2529';
|
|
case PackState(1, 2, 2, 0): return U'\u252A';
|
|
case PackState(2, 2, 2, 0): return U'\u252B';
|
|
case PackState(0, 1, 1, 1): return U'\u252C';
|
|
case PackState(0, 1, 2, 1): return U'\u252D';
|
|
case PackState(0, 1, 1, 2): return U'\u252E';
|
|
case PackState(0, 1, 2, 2): return U'\u252F';
|
|
case PackState(0, 2, 1, 1): return U'\u2530';
|
|
case PackState(0, 2, 2, 1): return U'\u2531';
|
|
case PackState(0, 2, 1, 2): return U'\u2532';
|
|
case PackState(0, 2, 2, 2): return U'\u2533';
|
|
case PackState(1, 0, 1, 1): return U'\u2534';
|
|
case PackState(1, 0, 2, 1): return U'\u2535';
|
|
case PackState(1, 0, 1, 2): return U'\u2536';
|
|
case PackState(1, 0, 2, 2): return U'\u2537';
|
|
case PackState(2, 0, 1, 1): return U'\u2538';
|
|
case PackState(2, 0, 2, 1): return U'\u2539';
|
|
case PackState(2, 0, 1, 2): return U'\u253A';
|
|
case PackState(2, 0, 2, 2): return U'\u253B';
|
|
case PackState(1, 1, 1, 1): return U'\u253C';
|
|
case PackState(1, 1, 2, 1): return U'\u253D';
|
|
case PackState(1, 1, 1, 2): return U'\u253E';
|
|
case PackState(1, 1, 2, 2): return U'\u253F';
|
|
case PackState(2, 1, 1, 1): return U'\u2540';
|
|
case PackState(1, 2, 1, 1): return U'\u2541';
|
|
case PackState(2, 2, 1, 1): return U'\u2542';
|
|
case PackState(2, 1, 2, 1): return U'\u2543';
|
|
case PackState(2, 1, 1, 2): return U'\u2544';
|
|
case PackState(1, 2, 2, 1): return U'\u2545';
|
|
case PackState(1, 2, 1, 2): return U'\u2546';
|
|
case PackState(2, 1, 2, 2): return U'\u2547';
|
|
case PackState(1, 2, 2, 2): return U'\u2548';
|
|
case PackState(2, 2, 2, 1): return U'\u2549';
|
|
case PackState(2, 2, 1, 2): return U'\u254A';
|
|
case PackState(2, 2, 2, 2): return U'\u254B';
|
|
case PackState(0, 0, 3, 3): return U'\u2550';
|
|
case PackState(3, 3, 0, 0): return U'\u2551';
|
|
case PackState(0, 1, 0, 3): return U'\u2552';
|
|
case PackState(0, 3, 0, 1): return U'\u2553';
|
|
case PackState(0, 3, 0, 3): return U'\u2554';
|
|
case PackState(0, 1, 3, 0): return U'\u2555';
|
|
case PackState(0, 3, 1, 0): return U'\u2556';
|
|
case PackState(0, 3, 3, 0): return U'\u2557';
|
|
case PackState(1, 0, 0, 3): return U'\u2558';
|
|
case PackState(3, 0, 0, 1): return U'\u2559';
|
|
case PackState(3, 0, 0, 3): return U'\u255A';
|
|
case PackState(1, 0, 3, 0): return U'\u255B';
|
|
case PackState(3, 0, 1, 0): return U'\u255C';
|
|
case PackState(3, 0, 3, 0): return U'\u255D';
|
|
case PackState(1, 1, 0, 3): return U'\u255E';
|
|
case PackState(3, 3, 0, 1): return U'\u255F';
|
|
case PackState(3, 3, 0, 3): return U'\u2560';
|
|
case PackState(1, 1, 3, 0): return U'\u2561';
|
|
case PackState(3, 3, 1, 0): return U'\u2562';
|
|
case PackState(3, 3, 3, 0): return U'\u2563';
|
|
case PackState(0, 1, 3, 3): return U'\u2564';
|
|
case PackState(0, 3, 1, 1): return U'\u2565';
|
|
case PackState(0, 3, 3, 3): return U'\u2566';
|
|
case PackState(1, 0, 3, 3): return U'\u2567';
|
|
case PackState(3, 0, 1, 1): return U'\u2568';
|
|
case PackState(3, 0, 3, 3): return U'\u2569';
|
|
case PackState(1, 1, 3, 3): return U'\u256A';
|
|
case PackState(3, 3, 1, 1): return U'\u256B';
|
|
case PackState(3, 3, 3, 3): return U'\u256C';
|
|
case PackState(0, 0, 1, 0): return U'\u2574';
|
|
case PackState(1, 0, 0, 0): return U'\u2575';
|
|
case PackState(0, 0, 0, 1): return U'\u2576';
|
|
case PackState(0, 1, 0, 0): return U'\u2577';
|
|
case PackState(0, 0, 2, 0): return U'\u2578';
|
|
case PackState(2, 0, 0, 0): return U'\u2579';
|
|
case PackState(0, 0, 0, 2): return U'\u257A';
|
|
case PackState(0, 2, 0, 0): return U'\u257B';
|
|
case PackState(0, 0, 1, 2): return U'\u257C';
|
|
case PackState(1, 2, 0, 0): return U'\u257D';
|
|
case PackState(0, 0, 2, 1): return U'\u257E';
|
|
case PackState(2, 1, 0, 0): return U'\u257F';
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
bool IsEmptyMergeable(const TuiMergeablePixel& pixel)
|
|
{
|
|
return
|
|
pixel.up == TuiMergeableGlyph::None &&
|
|
pixel.down == TuiMergeableGlyph::None &&
|
|
pixel.left == TuiMergeableGlyph::None &&
|
|
pixel.right == TuiMergeableGlyph::None;
|
|
}
|
|
|
|
bool IsLineGlyph(TuiMergeableGlyph glyph)
|
|
{
|
|
return
|
|
glyph == TuiMergeableGlyph::ThinLine ||
|
|
glyph == TuiMergeableGlyph::ThickLine ||
|
|
glyph == TuiMergeableGlyph::DoubleLine;
|
|
}
|
|
|
|
bool IsColorMode(TuiColorMode colorMode, bool allowAuto)
|
|
{
|
|
return
|
|
(allowAuto && colorMode == TuiColorMode::Auto) ||
|
|
colorMode == TuiColorMode::TrueColor ||
|
|
colorMode == TuiColorMode::Color256 ||
|
|
colorMode == TuiColorMode::Color16;
|
|
}
|
|
|
|
char32_t GetUnmergeableChar(const TuiUnmergeablePixel& pixel)
|
|
{
|
|
if (pixel.glyph != TuiUnmergeableGlyph::RoundCorner) return 0;
|
|
switch (pixel.direction)
|
|
{
|
|
case TuiUnmergeableDirection::LeftTop: return U'\u256D';
|
|
case TuiUnmergeableDirection::RightTop: return U'\u256E';
|
|
case TuiUnmergeableDirection::LeftBottom: return U'\u2570';
|
|
case TuiUnmergeableDirection::RightBottom: return U'\u256F';
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
bool IsScalar(char32_t code)
|
|
{
|
|
return code <= 0x10FFFF && !(code >= 0xD800 && code <= 0xDFFF);
|
|
}
|
|
|
|
TuiPixel EmptyPixel(TuiColor background = { 0, 0, 0 })
|
|
{
|
|
TuiPixel pixel;
|
|
pixel.backgroundColor = background;
|
|
return pixel;
|
|
}
|
|
|
|
void CheckBuffer(TuiPixel* buffer, vint width, vint height)
|
|
{
|
|
CHECK_ERROR(buffer != nullptr, L"vl::console::TUI drawing helper requires a non-null buffer.");
|
|
CHECK_ERROR(width >= 0 && height >= 0, L"vl::console::TUI drawing helper requires non-negative dimensions.");
|
|
}
|
|
|
|
void RepairWide(TuiPixel* buffer, vint width, vint height, vint x, vint y)
|
|
{
|
|
if (x < 0 || x >= width || y < 0 || y >= height) return;
|
|
auto index = y * width + x;
|
|
auto& pixel = buffer[index];
|
|
if (pixel.glyph == TuiPixelGlyph::WideCharContinuation)
|
|
{
|
|
pixel = EmptyPixel(pixel.backgroundColor);
|
|
if (x > 0)
|
|
{
|
|
auto& leading = buffer[index - 1];
|
|
if (leading.glyph == TuiPixelGlyph::Char && TUI::MeasureChar(leading.c) == 2)
|
|
{
|
|
leading = EmptyPixel(leading.backgroundColor);
|
|
}
|
|
}
|
|
}
|
|
else if (pixel.glyph == TuiPixelGlyph::Char && TUI::MeasureChar(pixel.c) == 2)
|
|
{
|
|
pixel = EmptyPixel(pixel.backgroundColor);
|
|
if (x + 1 < width && buffer[index + 1].glyph == TuiPixelGlyph::WideCharContinuation)
|
|
{
|
|
auto background = buffer[index + 1].backgroundColor;
|
|
buffer[index + 1] = EmptyPixel(background);
|
|
}
|
|
}
|
|
}
|
|
|
|
void PlaceMergeable(TuiPixel* buffer, vint width, vint height, vint x, vint y, const TuiMergeablePixel& drawing, TuiColor foreground, Nullable<TuiColor> background)
|
|
{
|
|
if (x < 0 || x >= width || y < 0 || y >= height) return;
|
|
RepairWide(buffer, width, height, x, y);
|
|
auto& pixel = buffer[y * width + x];
|
|
auto candidate = drawing;
|
|
if (pixel.glyph == TuiPixelGlyph::Mergeable)
|
|
{
|
|
candidate = pixel.mergeable;
|
|
if (drawing.up != TuiMergeableGlyph::None) candidate.up = drawing.up;
|
|
if (drawing.down != TuiMergeableGlyph::None) candidate.down = drawing.down;
|
|
if (drawing.left != TuiMergeableGlyph::None) candidate.left = drawing.left;
|
|
if (drawing.right != TuiMergeableGlyph::None) candidate.right = drawing.right;
|
|
if (GetMergeableChar(candidate) == 0)
|
|
{
|
|
candidate = drawing;
|
|
}
|
|
}
|
|
pixel.glyph = TuiPixelGlyph::Mergeable;
|
|
pixel.mergeable = candidate;
|
|
pixel.foregroundColor = foreground;
|
|
if (background) pixel.backgroundColor = background.Value();
|
|
}
|
|
|
|
void PlaceUnmergeable(TuiPixel* buffer, vint width, vint height, vint x, vint y, TuiUnmergeableDirection direction, TuiColor foreground, Nullable<TuiColor> background)
|
|
{
|
|
if (x < 0 || x >= width || y < 0 || y >= height) return;
|
|
RepairWide(buffer, width, height, x, y);
|
|
auto& pixel = buffer[y * width + x];
|
|
pixel.glyph = TuiPixelGlyph::Unmergeable;
|
|
pixel.unmergeable = { TuiUnmergeableGlyph::RoundCorner, direction };
|
|
pixel.foregroundColor = foreground;
|
|
if (background) pixel.backgroundColor = background.Value();
|
|
}
|
|
|
|
void CheckOwner(auto& storage)
|
|
{
|
|
CHECK_ERROR(storage.active, L"vl::console::TUI operation requires an active TUI.");
|
|
CHECK_ERROR(storage.ownerThreadId == Thread::GetCurrentThreadId(), L"vl::console::TUI operation must run on the owner thread.");
|
|
}
|
|
|
|
vint FindListener(auto* storage, ITuiCallback* listener, vuint64_t generation = 0)
|
|
{
|
|
if (!storage) return -1;
|
|
for (vint i = 0; i < storage->listeners.Count(); i++)
|
|
{
|
|
auto entry = storage->listeners[i];
|
|
if (entry.listener == listener && (generation == 0 || entry.generation == generation)) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
template<typename TCallback>
|
|
void InvokeListeners(auto& storage, auto* listenerStorage, TCallback&& callback, bool stopping = false)
|
|
{
|
|
if (!listenerStorage) return;
|
|
List<ListenerEntry> snapshot;
|
|
for (auto entry : listenerStorage->listeners) snapshot.Add(entry);
|
|
for (auto entry : snapshot)
|
|
{
|
|
if (!stopping && storage.stopRequested) break;
|
|
if (FindListener(listenerStorage, entry.listener, entry.generation) == -1) continue;
|
|
try
|
|
{
|
|
callback(entry.listener);
|
|
}
|
|
catch (...)
|
|
{
|
|
if (!storage.callbackException) storage.callbackException = std::current_exception();
|
|
storage.stopRequested = true;
|
|
throw;
|
|
}
|
|
if (storage.callbackException) std::rethrow_exception(storage.callbackException);
|
|
}
|
|
}
|
|
|
|
void ResizeBuffer(auto& storage, vint width, vint height)
|
|
{
|
|
CHECK_ERROR(width > 0 && height > 0, L"vl::console::TUI backend returned an invalid terminal size.");
|
|
Array<TuiPixel> newBuffer(width * height);
|
|
auto copyWidth = width < storage.width ? width : storage.width;
|
|
auto copyHeight = height < storage.height ? height : storage.height;
|
|
for (vint y = 0; y < copyHeight; y++)
|
|
{
|
|
for (vint x = 0; x < copyWidth; x++)
|
|
{
|
|
newBuffer[y * width + x] = storage.buffer[y * storage.width + x];
|
|
}
|
|
}
|
|
for (vint y = 0; y < height; y++)
|
|
{
|
|
for (vint x = 0; x < width; x++)
|
|
{
|
|
auto index = y * width + x;
|
|
auto& pixel = newBuffer[index];
|
|
if (pixel.glyph == TuiPixelGlyph::WideCharContinuation)
|
|
{
|
|
if (x == 0 || newBuffer[index - 1].glyph != TuiPixelGlyph::Char || TUI::MeasureChar(newBuffer[index - 1].c) != 2)
|
|
{
|
|
pixel = EmptyPixel(pixel.backgroundColor);
|
|
}
|
|
}
|
|
else if (pixel.glyph == TuiPixelGlyph::Char && TUI::MeasureChar(pixel.c) == 2)
|
|
{
|
|
if (x + 1 >= width || newBuffer[index + 1].glyph != TuiPixelGlyph::WideCharContinuation)
|
|
{
|
|
pixel = EmptyPixel(pixel.backgroundColor);
|
|
if (x + 1 < width && newBuffer[index + 1].glyph == TuiPixelGlyph::WideCharContinuation)
|
|
{
|
|
auto background = newBuffer[index + 1].backgroundColor;
|
|
newBuffer[index + 1] = EmptyPixel(background);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
storage.buffer = std::move(newBuffer);
|
|
storage.width = width;
|
|
storage.height = height;
|
|
}
|
|
|
|
void DispatchEvent(auto& storage, auto* listenerStorage, const unittest::TuiBackendEvent& event)
|
|
{
|
|
if (storage.stopRequested) return;
|
|
switch (event.type)
|
|
{
|
|
case unittest::TuiBackendEventType::Resize:
|
|
if (event.width != storage.width || event.height != storage.height)
|
|
{
|
|
ResizeBuffer(storage, event.width, event.height);
|
|
InvokeListeners(storage, listenerStorage, [](ITuiCallback* listener) { listener->BufferSizeChanged(); });
|
|
}
|
|
break;
|
|
case unittest::TuiBackendEventType::MouseMove:
|
|
InvokeListeners(storage, listenerStorage, [&](ITuiCallback* listener) { listener->MouseMove(event.mouseInfo); });
|
|
break;
|
|
case unittest::TuiBackendEventType::MouseDown:
|
|
InvokeListeners(storage, listenerStorage, [&](ITuiCallback* listener) { listener->MouseDown(event.mouseButton, event.mouseInfo); });
|
|
break;
|
|
case unittest::TuiBackendEventType::MouseUp:
|
|
InvokeListeners(storage, listenerStorage, [&](ITuiCallback* listener) { listener->MouseUp(event.mouseButton, event.mouseInfo); });
|
|
break;
|
|
case unittest::TuiBackendEventType::MouseDoubleClick:
|
|
InvokeListeners(storage, listenerStorage, [&](ITuiCallback* listener) { listener->MouseDoubleClick(event.mouseButton, event.mouseInfo); });
|
|
break;
|
|
case unittest::TuiBackendEventType::MouseVerticalWheel:
|
|
InvokeListeners(storage, listenerStorage, [&](ITuiCallback* listener) { listener->MouseVerticalWheel(event.mouseInfo); });
|
|
break;
|
|
case unittest::TuiBackendEventType::MouseHorizontalWheel:
|
|
InvokeListeners(storage, listenerStorage, [&](ITuiCallback* listener) { listener->MouseHorizontalWheel(event.mouseInfo); });
|
|
break;
|
|
case unittest::TuiBackendEventType::KeyDown:
|
|
InvokeListeners(storage, listenerStorage, [&](ITuiCallback* listener) { listener->KeyDown(event.keyInfo); });
|
|
break;
|
|
case unittest::TuiBackendEventType::KeyUp:
|
|
InvokeListeners(storage, listenerStorage, [&](ITuiCallback* listener) { listener->KeyUp(event.keyInfo); });
|
|
break;
|
|
case unittest::TuiBackendEventType::Char:
|
|
InvokeListeners(storage, listenerStorage, [&](ITuiCallback* listener) { listener->Char(event.charInfo); });
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
const TuiColor canonicalColor16[] =
|
|
{
|
|
{ 0x00, 0x00, 0x00 }, { 0x80, 0x00, 0x00 }, { 0x00, 0x80, 0x00 }, { 0x80, 0x80, 0x00 },
|
|
{ 0x00, 0x00, 0x80 }, { 0x80, 0x00, 0x80 }, { 0x00, 0x80, 0x80 }, { 0xC0, 0xC0, 0xC0 },
|
|
{ 0x80, 0x80, 0x80 }, { 0xFF, 0x00, 0x00 }, { 0x00, 0xFF, 0x00 }, { 0xFF, 0xFF, 0x00 },
|
|
{ 0x00, 0x00, 0xFF }, { 0xFF, 0x00, 0xFF }, { 0x00, 0xFF, 0xFF }, { 0xFF, 0xFF, 0xFF },
|
|
};
|
|
|
|
TuiColor GetCanonicalColor(vint index)
|
|
{
|
|
CHECK_ERROR(index >= 0 && index < 256, L"vl::console::tui_internal::GetCanonicalColor(vint)#Index out of range.");
|
|
if (index < 16) return canonicalColor16[index];
|
|
if (index < 232)
|
|
{
|
|
auto value = index - 16;
|
|
vuint8_t levels[] = { 0, 95, 135, 175, 215, 255 };
|
|
return { levels[value / 36], levels[(value / 6) % 6], levels[value % 6] };
|
|
}
|
|
auto level = (vuint8_t)(8 + 10 * (index - 232));
|
|
return { level, level, level };
|
|
}
|
|
|
|
vint QuantizeColor(TuiColor color, TuiColorMode colorMode, const TuiColor* customColor16)
|
|
{
|
|
auto count = colorMode == TuiColorMode::Color16 ? 16 : 256;
|
|
vint best = 0;
|
|
vint bestDistance = -1;
|
|
for (vint i = 0; i < count; i++)
|
|
{
|
|
auto candidate = customColor16 && i < 16 ? customColor16[i] : GetCanonicalColor(i);
|
|
auto dr = (vint)color.r - candidate.r;
|
|
auto dg = (vint)color.g - candidate.g;
|
|
auto db = (vint)color.b - candidate.b;
|
|
auto distance = dr * dr + dg * dg + db * db;
|
|
if (bestDistance == -1 || distance < bestDistance)
|
|
{
|
|
best = i;
|
|
bestDistance = distance;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
}
|
|
|
|
using namespace tui_internal;
|
|
|
|
/***********************************************************************
|
|
TuiPixel
|
|
***********************************************************************/
|
|
|
|
char32_t TuiPixel::GetChar32() const
|
|
{
|
|
switch (glyph)
|
|
{
|
|
case TuiPixelGlyph::Char:
|
|
return c;
|
|
case TuiPixelGlyph::Mergeable:
|
|
return GetMergeableChar(mergeable);
|
|
case TuiPixelGlyph::Unmergeable:
|
|
return GetUnmergeableChar(unmergeable);
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
wchar_t TuiPixel::GetWChar() const
|
|
{
|
|
auto code = GetChar32();
|
|
if (!IsScalar(code) || code == 0) return 0;
|
|
if constexpr (sizeof(wchar_t) == 2)
|
|
{
|
|
return code <= 0xFFFF ? (wchar_t)code : 0;
|
|
}
|
|
else
|
|
{
|
|
return (wchar_t)code;
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
ITuiCallback
|
|
***********************************************************************/
|
|
|
|
void ITuiCallback::Starting() {}
|
|
void ITuiCallback::Stopping() {}
|
|
void ITuiCallback::BufferSizeChanged() {}
|
|
void ITuiCallback::MouseMove(const TuiMouseInfo&) {}
|
|
void ITuiCallback::MouseDown(TuiMouseButton, const TuiMouseInfo&) {}
|
|
void ITuiCallback::MouseUp(TuiMouseButton, const TuiMouseInfo&) {}
|
|
void ITuiCallback::MouseDoubleClick(TuiMouseButton, const TuiMouseInfo&) {}
|
|
void ITuiCallback::MouseVerticalWheel(const TuiMouseInfo&) {}
|
|
void ITuiCallback::MouseHorizontalWheel(const TuiMouseInfo&) {}
|
|
void ITuiCallback::KeyDown(const TuiKeyInfo&) {}
|
|
void ITuiCallback::KeyUp(const TuiKeyInfo&) {}
|
|
void ITuiCallback::Char(const TuiCharInfo&) {}
|
|
void ITuiCallback::Timer() {}
|
|
|
|
/***********************************************************************
|
|
TUI
|
|
***********************************************************************/
|
|
|
|
TUI::Impl& TUI::GetImpl()
|
|
{
|
|
CHECK_ERROR(impl, L"vl::console::TUI operation requires an active TUI.");
|
|
CheckOwner(*impl);
|
|
return *impl;
|
|
}
|
|
|
|
bool TUI::TryGetConsoleSize(vint& width, vint& height)
|
|
{
|
|
if (impl && impl->active)
|
|
{
|
|
CheckOwner(*impl);
|
|
return impl->backend->TryGetConsoleSize(width, height);
|
|
}
|
|
if (injectedBackend)
|
|
{
|
|
return (*injectedBackend)->TryGetConsoleSize(width, height);
|
|
}
|
|
auto backend = CreateTuiBackend();
|
|
return backend->TryGetConsoleSize(width, height);
|
|
}
|
|
|
|
void TUI::Start(const TuiStartOptions& options)
|
|
{
|
|
if (impl)
|
|
{
|
|
if (impl->active) CheckOwner(*impl);
|
|
return;
|
|
}
|
|
CHECK_ERROR(IsColorMode(options.colorMode, true), L"vl::console::TUI::Start(const TuiStartOptions&)#The requested color mode is invalid.");
|
|
CHECK_ERROR(Console::IsEnabled(), L"vl::console::TUI::Start(const TuiStartOptions&)#Console must be enabled before TUI starts.");
|
|
|
|
auto storage = new Impl;
|
|
impl = storage;
|
|
std::exception_ptr thrown;
|
|
try
|
|
{
|
|
storage->backend = injectedBackend ? *injectedBackend : CreateTuiBackend();
|
|
storage->colorMode = storage->backend->Start(options);
|
|
storage->backendStarted = true;
|
|
CHECK_ERROR(IsColorMode(storage->colorMode, false), L"vl::console::TUI::Start(const TuiStartOptions&)#The backend selected an invalid color mode.");
|
|
|
|
vint width = 0;
|
|
vint height = 0;
|
|
CHECK_ERROR(storage->backend->TryGetConsoleSize(width, height), L"vl::console::TUI::Start(const TuiStartOptions&)#Failed to query the terminal size.");
|
|
Console::Disable();
|
|
storage->consoleDisabled = true;
|
|
ResizeBuffer(*storage, width, height);
|
|
storage->ownerThreadId = Thread::GetCurrentThreadId();
|
|
storage->active = true;
|
|
|
|
InvokeListeners(*storage, listenerStorage, [](ITuiCallback* listener) { listener->Starting(); });
|
|
if (!storage->stopRequested)
|
|
{
|
|
InvokeListeners(*storage, listenerStorage, [](ITuiCallback* listener) { listener->BufferSizeChanged(); });
|
|
}
|
|
while (!storage->stopRequested)
|
|
{
|
|
if (!RunOneCycle()) break;
|
|
}
|
|
if (!storage->callbackException)
|
|
{
|
|
storage->shuttingDown = true;
|
|
InvokeListeners(*storage, listenerStorage, [](ITuiCallback* listener) { listener->Stopping(); }, true);
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
thrown = std::current_exception();
|
|
}
|
|
storage->cleanupException = &thrown;
|
|
impl = nullptr;
|
|
delete storage;
|
|
if (listenerStorage && listenerStorage->listeners.Count() == 0)
|
|
{
|
|
delete listenerStorage;
|
|
listenerStorage = nullptr;
|
|
}
|
|
if (thrown) std::rethrow_exception(thrown);
|
|
}
|
|
|
|
bool TUI::RunOneCycle()
|
|
{
|
|
auto& storage = GetImpl();
|
|
if (storage.callbackException) std::rethrow_exception(storage.callbackException);
|
|
if (storage.stopRequested) return false;
|
|
|
|
if (storage.eventQueue.Count() == 0)
|
|
{
|
|
auto now = storage.backend->GetMonotonicTime();
|
|
vint timeout = -1;
|
|
if (storage.timerPeriod > 0)
|
|
{
|
|
if (now >= storage.nextTimer)
|
|
{
|
|
storage.nextTimer += storage.timerPeriod;
|
|
InvokeListeners(storage, listenerStorage, [](ITuiCallback* listener) { listener->Timer(); });
|
|
return !storage.stopRequested;
|
|
}
|
|
auto remaining = storage.nextTimer - now;
|
|
timeout = remaining > (vuint64_t)0x7FFFFFFF ? 0x7FFFFFFF : (vint)remaining;
|
|
}
|
|
unittest::TuiBackendEvent event;
|
|
if (storage.backend->ReadEvent(timeout, event))
|
|
{
|
|
storage.eventQueue.Add(event);
|
|
}
|
|
else if (storage.timerPeriod > 0 && storage.backend->GetMonotonicTime() >= storage.nextTimer)
|
|
{
|
|
storage.nextTimer += storage.timerPeriod;
|
|
InvokeListeners(storage, listenerStorage, [](ITuiCallback* listener) { listener->Timer(); });
|
|
return !storage.stopRequested;
|
|
}
|
|
}
|
|
|
|
if (storage.eventQueue.Count() > 0)
|
|
{
|
|
auto event = storage.eventQueue[0];
|
|
storage.eventQueue.RemoveAt(0);
|
|
DispatchEvent(storage, listenerStorage, event);
|
|
}
|
|
if (storage.callbackException) std::rethrow_exception(storage.callbackException);
|
|
return !storage.stopRequested;
|
|
}
|
|
|
|
void TUI::Stop()
|
|
{
|
|
if (!impl || !impl->active) return;
|
|
auto& storage = *impl;
|
|
CheckOwner(storage);
|
|
if (storage.shuttingDown) return;
|
|
storage.stopRequested = true;
|
|
}
|
|
|
|
bool TUI::IsInUse()
|
|
{
|
|
if (!impl || !impl->active) return false;
|
|
CheckOwner(*impl);
|
|
return true;
|
|
}
|
|
|
|
bool TUI::IsStopRequested()
|
|
{
|
|
if (!impl || !impl->active) return false;
|
|
auto& storage = *impl;
|
|
CheckOwner(storage);
|
|
return storage.stopRequested;
|
|
}
|
|
|
|
TuiColorMode TUI::GetColorMode()
|
|
{
|
|
auto& storage = GetImpl();
|
|
return storage.colorMode;
|
|
}
|
|
|
|
bool TUI::InstallListener(ITuiCallback* listener)
|
|
{
|
|
if (impl && impl->active) CheckOwner(*impl);
|
|
if (!listener || FindListener(listenerStorage, listener) != -1) return false;
|
|
if (!listenerStorage) listenerStorage = new ListenerStorage;
|
|
listenerStorage->listeners.Add({ listener, ++listenerStorage->nextGeneration });
|
|
return true;
|
|
}
|
|
|
|
bool TUI::UninstallListener(ITuiCallback* listener)
|
|
{
|
|
if (impl && impl->active) CheckOwner(*impl);
|
|
if (!listener) return false;
|
|
auto index = FindListener(listenerStorage, listener);
|
|
if (index == -1) return false;
|
|
listenerStorage->listeners.RemoveAt(index);
|
|
if (!impl && listenerStorage->listeners.Count() == 0)
|
|
{
|
|
delete listenerStorage;
|
|
listenerStorage = nullptr;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void TUI::StartTimer(vint milliseconds)
|
|
{
|
|
auto& storage = GetImpl();
|
|
CHECK_ERROR(milliseconds > 0, L"vl::console::TUI::StartTimer(vint)#The timer period must be positive.");
|
|
storage.timerPeriod = milliseconds;
|
|
storage.nextTimer = storage.backend->GetMonotonicTime() + milliseconds;
|
|
}
|
|
|
|
void TUI::StopTimer()
|
|
{
|
|
auto& storage = GetImpl();
|
|
storage.timerPeriod = 0;
|
|
storage.nextTimer = 0;
|
|
}
|
|
|
|
TuiPixel* TUI::GetBuffer()
|
|
{
|
|
auto& storage = GetImpl();
|
|
return storage.buffer.Count() == 0 ? nullptr : &storage.buffer[0];
|
|
}
|
|
|
|
vint TUI::GetBufferWidth()
|
|
{
|
|
auto& storage = GetImpl();
|
|
return storage.width;
|
|
}
|
|
|
|
vint TUI::GetBufferHeight()
|
|
{
|
|
auto& storage = GetImpl();
|
|
return storage.height;
|
|
}
|
|
|
|
void TUI::RenderBuffer()
|
|
{
|
|
auto& storage = GetImpl();
|
|
for (vint y = 0; y < storage.height; y++)
|
|
{
|
|
for (vint x = 0; x < storage.width; x++)
|
|
{
|
|
auto index = y * storage.width + x;
|
|
auto& pixel = storage.buffer[index];
|
|
switch (pixel.glyph)
|
|
{
|
|
case TuiPixelGlyph::Char:
|
|
if (pixel.c != 0)
|
|
{
|
|
CHECK_ERROR(IsScalar(pixel.c), L"vl::console::TUI::RenderBuffer()#A Char cell contains an invalid Unicode scalar.");
|
|
auto width = MeasureChar(pixel.c);
|
|
CHECK_ERROR(width == 1 || width == 2, L"vl::console::TUI::RenderBuffer()#A Char cell contains a zero-width or non-printable scalar.");
|
|
if (width == 2)
|
|
{
|
|
CHECK_ERROR(x + 1 < storage.width && storage.buffer[index + 1].glyph == TuiPixelGlyph::WideCharContinuation, L"vl::console::TUI::RenderBuffer()#A width-two Char cell has no continuation.");
|
|
CHECK_ERROR(storage.buffer[index + 1].foregroundColor == pixel.foregroundColor && storage.buffer[index + 1].backgroundColor == pixel.backgroundColor, L"vl::console::TUI::RenderBuffer()#A width-two continuation has different colors.");
|
|
}
|
|
}
|
|
break;
|
|
case TuiPixelGlyph::Mergeable:
|
|
CHECK_ERROR(GetMergeableChar(pixel.mergeable) != 0 || IsEmptyMergeable(pixel.mergeable), L"vl::console::TUI::RenderBuffer()#A Mergeable cell contains an unsupported arm state.");
|
|
break;
|
|
case TuiPixelGlyph::Unmergeable:
|
|
CHECK_ERROR(GetUnmergeableChar(pixel.unmergeable) != 0, L"vl::console::TUI::RenderBuffer()#An Unmergeable cell contains an invalid glyph.");
|
|
break;
|
|
case TuiPixelGlyph::WideCharContinuation:
|
|
CHECK_ERROR(x > 0 && storage.buffer[index - 1].glyph == TuiPixelGlyph::Char && MeasureChar(storage.buffer[index - 1].c) == 2, L"vl::console::TUI::RenderBuffer()#A continuation cell has no width-two leading cell.");
|
|
break;
|
|
default:
|
|
CHECK_FAIL(L"vl::console::TUI::RenderBuffer()#A cell contains an invalid glyph type.");
|
|
}
|
|
}
|
|
}
|
|
storage.backend->Render(&storage.buffer[0], storage.width, storage.height, storage.colorMode);
|
|
}
|
|
|
|
void TUI::PrintChar(const TuiPrintOptions& options, char32_t code, vint x, vint y)
|
|
{
|
|
PrintChar(GetBuffer(), GetBufferWidth(), GetBufferHeight(), options, code, x, y);
|
|
}
|
|
|
|
void TUI::DrawLineV(const TuiLineOptions& options, vint x, vint y1, vint y2)
|
|
{
|
|
DrawLineV(GetBuffer(), GetBufferWidth(), GetBufferHeight(), options, x, y1, y2);
|
|
}
|
|
|
|
void TUI::DrawLineH(const TuiLineOptions& options, vint x1, vint x2, vint y)
|
|
{
|
|
DrawLineH(GetBuffer(), GetBufferWidth(), GetBufferHeight(), options, x1, x2, y);
|
|
}
|
|
|
|
void TUI::DrawRect(const TuiRectOptions& options, vint x1, vint y1, vint x2, vint y2)
|
|
{
|
|
DrawRect(GetBuffer(), GetBufferWidth(), GetBufferHeight(), options, x1, y1, x2, y2);
|
|
}
|
|
|
|
void TUI::Clear(TuiColor backgroundColor, vint x1, vint y1, vint x2, vint y2)
|
|
{
|
|
Clear(GetBuffer(), GetBufferWidth(), GetBufferHeight(), backgroundColor, x1, y1, x2, y2);
|
|
}
|
|
|
|
void TUI::PrintChar(TuiPixel* buffer, vint width, vint height, const TuiPrintOptions& options, char32_t code, vint x, vint y)
|
|
{
|
|
CheckBuffer(buffer, width, height);
|
|
CHECK_ERROR(IsScalar(code), L"vl::console::TUI::PrintChar(...)#The character must be a Unicode scalar.");
|
|
auto charWidth = MeasureChar(code);
|
|
if (charWidth == 0 || x < 0 || x >= width || y < 0 || y >= height) return;
|
|
if (charWidth == 2 && x + 1 >= width) return;
|
|
RepairWide(buffer, width, height, x, y);
|
|
if (charWidth == 2) RepairWide(buffer, width, height, x + 1, y);
|
|
auto& leading = buffer[y * width + x];
|
|
leading.glyph = TuiPixelGlyph::Char;
|
|
leading.c = code;
|
|
leading.foregroundColor = options.foregroundColor;
|
|
leading.backgroundColor = options.backgroundColor;
|
|
if (charWidth == 2)
|
|
{
|
|
auto& continuation = buffer[y * width + x + 1];
|
|
continuation.glyph = TuiPixelGlyph::WideCharContinuation;
|
|
continuation.c = 0;
|
|
continuation.foregroundColor = options.foregroundColor;
|
|
continuation.backgroundColor = options.backgroundColor;
|
|
}
|
|
}
|
|
|
|
void TUI::DrawLineV(TuiPixel* buffer, vint width, vint height, const TuiLineOptions& options, vint x, vint y1, vint y2)
|
|
{
|
|
CheckBuffer(buffer, width, height);
|
|
CHECK_ERROR(y1 <= y2, L"vl::console::TUI::DrawLineV(...)#The ordered range is invalid.");
|
|
CHECK_ERROR(IsLineGlyph(options.glyph), L"vl::console::TUI::DrawLineV(...)#The line style is invalid.");
|
|
if (x < 0 || x >= width || y2 < 0 || y1 >= height) return;
|
|
auto begin = y1 < 0 ? 0 : y1;
|
|
auto end = y2 >= height ? height - 1 : y2;
|
|
TuiMergeablePixel drawing = { options.glyph, options.glyph, TuiMergeableGlyph::None, TuiMergeableGlyph::None };
|
|
for (vint y = begin; y <= end; y++) PlaceMergeable(buffer, width, height, x, y, drawing, options.foregroundColor, options.backgroundColor);
|
|
}
|
|
|
|
void TUI::DrawLineH(TuiPixel* buffer, vint width, vint height, const TuiLineOptions& options, vint x1, vint x2, vint y)
|
|
{
|
|
CheckBuffer(buffer, width, height);
|
|
CHECK_ERROR(x1 <= x2, L"vl::console::TUI::DrawLineH(...)#The ordered range is invalid.");
|
|
CHECK_ERROR(IsLineGlyph(options.glyph), L"vl::console::TUI::DrawLineH(...)#The line style is invalid.");
|
|
if (y < 0 || y >= height || x2 < 0 || x1 >= width) return;
|
|
auto begin = x1 < 0 ? 0 : x1;
|
|
auto end = x2 >= width ? width - 1 : x2;
|
|
TuiMergeablePixel drawing = { TuiMergeableGlyph::None, TuiMergeableGlyph::None, options.glyph, options.glyph };
|
|
for (vint x = begin; x <= end; x++) PlaceMergeable(buffer, width, height, x, y, drawing, options.foregroundColor, options.backgroundColor);
|
|
}
|
|
|
|
void TUI::DrawRect(TuiPixel* buffer, vint width, vint height, const TuiRectOptions& options, vint x1, vint y1, vint x2, vint y2)
|
|
{
|
|
CheckBuffer(buffer, width, height);
|
|
CHECK_ERROR(x1 < x2 && y1 < y2, L"vl::console::TUI::DrawRect(...)#A rectangle must have distinct corners.");
|
|
CHECK_ERROR(IsLineGlyph(options.glyph), L"vl::console::TUI::DrawRect(...)#The line style is invalid.");
|
|
CHECK_ERROR(options.corner == TuiRectCorner::Sharp || options.corner == TuiRectCorner::Round, L"vl::console::TUI::DrawRect(...)#The corner style is invalid.");
|
|
CHECK_ERROR(options.corner == TuiRectCorner::Sharp || options.glyph == TuiMergeableGlyph::ThinLine, L"vl::console::TUI::DrawRect(...)#Rounded corners require a thin line.");
|
|
if (x2 < 0 || y2 < 0 || x1 >= width || y1 >= height) return;
|
|
|
|
TuiMergeablePixel horizontal = { TuiMergeableGlyph::None, TuiMergeableGlyph::None, options.glyph, options.glyph };
|
|
TuiMergeablePixel vertical = { options.glyph, options.glyph, TuiMergeableGlyph::None, TuiMergeableGlyph::None };
|
|
auto left = x1 + 1 < 0 ? 0 : x1 + 1;
|
|
auto right = x2 - 1 >= width ? width - 1 : x2 - 1;
|
|
for (vint x = left; x <= right; x++)
|
|
{
|
|
PlaceMergeable(buffer, width, height, x, y1, horizontal, options.foregroundColor, options.backgroundColor);
|
|
PlaceMergeable(buffer, width, height, x, y2, horizontal, options.foregroundColor, options.backgroundColor);
|
|
}
|
|
auto top = y1 + 1 < 0 ? 0 : y1 + 1;
|
|
auto bottom = y2 - 1 >= height ? height - 1 : y2 - 1;
|
|
for (vint y = top; y <= bottom; y++)
|
|
{
|
|
PlaceMergeable(buffer, width, height, x1, y, vertical, options.foregroundColor, options.backgroundColor);
|
|
PlaceMergeable(buffer, width, height, x2, y, vertical, options.foregroundColor, options.backgroundColor);
|
|
}
|
|
|
|
if (options.corner == TuiRectCorner::Round)
|
|
{
|
|
PlaceUnmergeable(buffer, width, height, x1, y1, TuiUnmergeableDirection::LeftTop, options.foregroundColor, options.backgroundColor);
|
|
PlaceUnmergeable(buffer, width, height, x2, y1, TuiUnmergeableDirection::RightTop, options.foregroundColor, options.backgroundColor);
|
|
PlaceUnmergeable(buffer, width, height, x1, y2, TuiUnmergeableDirection::LeftBottom, options.foregroundColor, options.backgroundColor);
|
|
PlaceUnmergeable(buffer, width, height, x2, y2, TuiUnmergeableDirection::RightBottom, options.foregroundColor, options.backgroundColor);
|
|
}
|
|
else
|
|
{
|
|
PlaceMergeable(buffer, width, height, x1, y1, { TuiMergeableGlyph::None, options.glyph, TuiMergeableGlyph::None, options.glyph }, options.foregroundColor, options.backgroundColor);
|
|
PlaceMergeable(buffer, width, height, x2, y1, { TuiMergeableGlyph::None, options.glyph, options.glyph, TuiMergeableGlyph::None }, options.foregroundColor, options.backgroundColor);
|
|
PlaceMergeable(buffer, width, height, x1, y2, { options.glyph, TuiMergeableGlyph::None, TuiMergeableGlyph::None, options.glyph }, options.foregroundColor, options.backgroundColor);
|
|
PlaceMergeable(buffer, width, height, x2, y2, { options.glyph, TuiMergeableGlyph::None, options.glyph, TuiMergeableGlyph::None }, options.foregroundColor, options.backgroundColor);
|
|
}
|
|
}
|
|
|
|
void TUI::Clear(TuiPixel* buffer, vint width, vint height, TuiColor backgroundColor, vint x1, vint y1, vint x2, vint y2)
|
|
{
|
|
CheckBuffer(buffer, width, height);
|
|
CHECK_ERROR(x1 <= x2 && y1 <= y2, L"vl::console::TUI::Clear(...)#The ordered rectangle is invalid.");
|
|
if (x2 < 0 || y2 < 0 || x1 >= width || y1 >= height) return;
|
|
auto left = x1 < 0 ? 0 : x1;
|
|
auto top = y1 < 0 ? 0 : y1;
|
|
auto right = x2 >= width ? width - 1 : x2;
|
|
auto bottom = y2 >= height ? height - 1 : y2;
|
|
for (vint y = top; y <= bottom; y++)
|
|
{
|
|
for (vint x = left; x <= right; x++)
|
|
{
|
|
RepairWide(buffer, width, height, x, y);
|
|
buffer[y * width + x] = EmptyPixel(backgroundColor);
|
|
}
|
|
}
|
|
}
|
|
|
|
/***********************************************************************
|
|
ScopedTuiBackend
|
|
***********************************************************************/
|
|
|
|
namespace unittest
|
|
{
|
|
ScopedTuiBackend::ScopedTuiBackend(Ptr<ITuiBackend> backend)
|
|
{
|
|
CHECK_ERROR(!TUI::impl, L"vl::console::unittest::ScopedTuiBackend::ScopedTuiBackend(...)#Cannot replace the backend while TUI is active.");
|
|
CHECK_ERROR(backend, L"vl::console::unittest::ScopedTuiBackend::ScopedTuiBackend(...)#The backend cannot be null.");
|
|
previous = TUI::injectedBackend;
|
|
current = backend;
|
|
TUI::injectedBackend = ¤t;
|
|
}
|
|
|
|
ScopedTuiBackend::~ScopedTuiBackend() noexcept(false)
|
|
{
|
|
CHECK_ERROR(!TUI::impl, L"vl::console::unittest::ScopedTuiBackend::~ScopedTuiBackend()#Cannot restore the backend while TUI is active.");
|
|
TUI::injectedBackend = previous;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|