Initial commit
This commit is contained in:
commit
fe3aa4a77e
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
* text=auto
|
||||
*.sh text eol=lf
|
303
.gitignore
vendored
Normal file
303
.gitignore
vendored
Normal file
@ -0,0 +1,303 @@
|
||||
*.d
|
||||
*.slo
|
||||
*.lo
|
||||
*.o
|
||||
*.obj
|
||||
*.gch
|
||||
*.pch
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
*.mod
|
||||
*.smod
|
||||
*.lai
|
||||
*.la
|
||||
*.a
|
||||
*.lib
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
.idea/**/aws.xml
|
||||
.idea/**/contentModel.xml
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
cmake-build-*/
|
||||
.idea/**/mongoSettings.xml
|
||||
*.iws
|
||||
out/
|
||||
.idea_modules/
|
||||
atlassian-ide-plugin.xml
|
||||
.idea/replstate.xml
|
||||
.idea/sonarlint/
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
.idea/httpRequests
|
||||
.idea/caches/build_file_checksums.ser
|
||||
*~
|
||||
.fuse_hidden*
|
||||
.directory
|
||||
.Trash-*
|
||||
.nfs*
|
||||
CMakeLists.txt.user
|
||||
CMakeCache.txt
|
||||
CMakeFiles
|
||||
CMakeScripts
|
||||
Testing
|
||||
Makefile
|
||||
cmake_install.cmake
|
||||
install_manifest.txt
|
||||
compile_commands.json
|
||||
CTestTestfile.cmake
|
||||
_deps
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
*.userprefs
|
||||
mono_crash.*
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
.vs/
|
||||
Generated\ Files/
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
BenchmarkDotNet.Artifacts/
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
ScaffoldingReadMe.txt
|
||||
StyleCopReport.xml
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.iobj
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
_Chutzpah*
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
*.e2e
|
||||
$tf/
|
||||
*.gpState
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
_TeamCity*
|
||||
*.dotCover
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
.sass-cache/
|
||||
[Ee]xpress/
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
publish/
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
PublishScripts/
|
||||
*.nupkg
|
||||
*.snupkg
|
||||
**/[Pp]ackages/*
|
||||
!**/[Pp]ackages/build/
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
csx/
|
||||
*.build.csdef
|
||||
ecf/
|
||||
rcf/
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
*.[Cc]ache
|
||||
!?*.[Cc]ache/
|
||||
ClientBin/
|
||||
~$*
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
Generated_Code/
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
FakesAssemblies/
|
||||
*.GhostDoc.xml
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
*.plg
|
||||
*.opt
|
||||
*.vbw
|
||||
*.vbp
|
||||
*.dsw
|
||||
*.dsp
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
.fake/
|
||||
.cr/personal
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.tss
|
||||
*.jmconfig
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
OpenCover/
|
||||
ASALocalRun/
|
||||
*.binlog
|
||||
*.nvuser
|
||||
.mfractor/
|
||||
.localhistory/
|
||||
.vshistory/
|
||||
healthchecksdb
|
||||
MigrationBackup/
|
||||
.ionide/
|
||||
FodyWeavers.xsd
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
.history/
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
*.sln.iml
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
*.stackdump
|
||||
[Dd]esktop.ini
|
||||
$RECYCLE.BIN/
|
||||
*.lnk
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
Icon
|
||||
._*
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
.runenv/
|
||||
vcpkg_installed/
|
||||
git2
|
43
CMakeLists.txt
Normal file
43
CMakeLists.txt
Normal file
@ -0,0 +1,43 @@
|
||||
cmake_minimum_required(VERSION 3.30)
|
||||
project(mmo)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 26)
|
||||
if (MSVC)
|
||||
add_compile_options(/W4)
|
||||
add_compile_options(/WX)
|
||||
add_compile_options(/external:anglebrackets)
|
||||
add_compile_options(/external:W0)
|
||||
add_compile_options(/wd4100)
|
||||
add_compile_options(/wd5050)
|
||||
add_definitions(-DWIN32_LEAN_AND_MEAN -DVC_EXTRALEAN)
|
||||
add_compile_definitions(WIN32_LEAN_AND_MEAN NOMINMAX)
|
||||
else ()
|
||||
add_compile_options(-Wall)
|
||||
add_compile_options(-Wextra)
|
||||
add_compile_options(-Wpedantic)
|
||||
add_compile_options(-Werror)
|
||||
endif ()
|
||||
|
||||
# function add santizers
|
||||
function(add_sanitizers target)
|
||||
if (MSVC)
|
||||
target_compile_options(${target} PRIVATE /fsanitize=address /fsanitize=fuzzer /Zi)
|
||||
target_link_options(${target} PRIVATE /fsanitize=address /fsanitize=fuzzer /Zi)
|
||||
else ()
|
||||
target_compile_options(${target} PRIVATE -fsanitize=address -fsanitize=fuzzer)
|
||||
target_link_options(${target} PRIVATE -fsanitize=address -fsanitize=fuzzer)
|
||||
endif ()
|
||||
endfunction()
|
||||
|
||||
if (DEFINED VCPKG_INSTALLED_DIR)
|
||||
elseif (DEFINED ENV{VCPKG_ROOT})
|
||||
include($ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake)
|
||||
else ()
|
||||
message(FATAL_ERROR "VCPKG is not loaded, set VCPKG_ROOT to automatically load it or specify the cmake toolchain")
|
||||
endif ()
|
||||
|
||||
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||
|
||||
add_subdirectory(common)
|
||||
#add_subdirectory(client)
|
||||
#add_subdirectory(server)
|
38
README.md
Normal file
38
README.md
Normal file
@ -0,0 +1,38 @@
|
||||
Add `-DCMAKE_TOOLCHAIN_FILE=K:\Dev\Tools\vcpkg\scripts\buildsystems\vcpkg.cmake` to cmake command line to use vcpkg
|
||||
toolchain file.
|
||||
|
||||
This is only necessary in debug mode:
|
||||
|
||||
Modify the VCPKG port of crypto++ and cli11 add the following to the
|
||||
`<vcpkg install dir>\ports\<cryptopp|cli11>\portfile.cmake` file before the configure
|
||||
command:
|
||||
|
||||
```cmake
|
||||
if (MSVC)
|
||||
set(VCPKG_CXX_FLAGS_DEBUG "/fsanitize=address /fsanitize=fuzzer")
|
||||
set(VCPKG_C_FLAGS_DEBUG "/fsanitize=address /fsanitize=fuzzer")
|
||||
set(VCPKG_LINKER_FLAGS_DEBUG "/fsanitize=address /fsanitize=fuzzer")
|
||||
else ()
|
||||
set(VCPKG_CXX_FLAGS_DEBUG -fsanitize=address)
|
||||
set(VCPKG_C_FLAGS_DEBUG -fsanitize=address)
|
||||
set(VCPKG_LINKER_FLAGS_DEBUG -fsanitize=address)
|
||||
endif ()
|
||||
```
|
||||
|
||||
An overlay can be created but I don't want to maintain adding the overlay to the vcpkg toolchain file and keeping it up
|
||||
to date with the vcpkg toolchain file.
|
||||
|
||||
Generate a certificate using the following command (for testing purposes only):
|
||||
|
||||
```bash
|
||||
openssl req -new -newkey rsa:2048 -days 1 -nodes -x509 -keyout key.pem -out cert.pem -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com"
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
Someone decided to migrate MSQuic's vcpkg to schannel instead of using OpenSSL (which is exclusive to windows).
|
||||
This means that certificates are not supported, which is a problem. Which is why the 0-RTT feature is enabled by in
|
||||
vcpkg.json, not because we need or want it.
|
||||
|
||||
Initially, Glaze was intended for parsing session info, but it caused issues with the MSVC compiler.
|
||||
Instead, we're using simdjson.
|
3
common/CMakeLists.txt
Normal file
3
common/CMakeLists.txt
Normal file
@ -0,0 +1,3 @@
|
||||
add_subdirectory(logging)
|
||||
add_subdirectory(connection)
|
||||
add_subdirectory(packet)
|
62
common/connection/CMakeLists.txt
Normal file
62
common/connection/CMakeLists.txt
Normal file
@ -0,0 +1,62 @@
|
||||
project(connection)
|
||||
|
||||
|
||||
set(${PROJECT_NAME}_src
|
||||
src/DataConnection.cppm
|
||||
src/MSQuicConnection.cppm
|
||||
src/MSQuicServer.cppm
|
||||
src/MSQuicGlobal.cppm
|
||||
src/MSQuicError.cppm)
|
||||
# this is the "object library" target: compiles the sources only once
|
||||
add_library(${PROJECT_NAME}_lib STATIC ${${PROJECT_NAME}_src})
|
||||
target_include_directories(${PROJECT_NAME}_lib PUBLIC ./src)
|
||||
|
||||
find_package(msquic CONFIG REQUIRED)
|
||||
target_link_libraries(${PROJECT_NAME}_lib PRIVATE msquic)
|
||||
target_link_libraries(${PROJECT_NAME}_lib PUBLIC logging_lib)
|
||||
if(WIN32)
|
||||
target_link_libraries(${PROJECT_NAME}_lib PUBLIC ntdll)
|
||||
endif()
|
||||
|
||||
# Setup tests
|
||||
enable_testing()
|
||||
|
||||
find_package(GTest CONFIG REQUIRED)
|
||||
include(GoogleTest)
|
||||
|
||||
file(GLOB_RECURSE tests_${PROJECT_NAME}_src CONFIGURE_DEPENDS tests/*.cppm tests/*/*.cppm)
|
||||
|
||||
add_executable(tests_${PROJECT_NAME} ${tests_${PROJECT_NAME}_src}
|
||||
tests/sanitizers.cpp)
|
||||
target_link_libraries(tests_${PROJECT_NAME} PRIVATE GTest::gtest GTest::gtest_main GTest::gmock GTest::gmock_main ${PROJECT_NAME}_lib)
|
||||
|
||||
add_test(AllTestsIn${PROJECT_NAME} tests_${PROJECT_NAME})
|
||||
|
||||
gtest_discover_tests(tests_${PROJECT_NAME})
|
||||
|
||||
find_program(OPENSSL_EXECUTABLE openssl)
|
||||
|
||||
set(OPENSSL_EXECUTABLE_CFG "" CACHE FILEPATH "Path to openssl.cnf")
|
||||
if (NOT OPENSSL_EXECUTABLE)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
message(STATUS "OpenSSL found: ${OPENSSL_FOUND} ${OPENSSL_VERSION} ${OPENSSL_INCLUDE_DIR} ${OPENSSL_LIBRARIES}")
|
||||
|
||||
set(OPENSSL_EXECUTABLE ${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/tools/openssl/openssl)
|
||||
endif ()
|
||||
if (NOT OPENSSL_EXECUTABLE)
|
||||
message(FATAL_ERROR "OpenSSL not found")
|
||||
endif ()
|
||||
|
||||
if (OPENSSL_EXECUTABLE MATCHES ^${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/tools/openssl/openssl)
|
||||
set(OPENSSL_EXECUTABLE_CFG ${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/tools/openssl/openssl.cnf)
|
||||
endif ()
|
||||
|
||||
add_custom_command(TARGET tests_${PROJECT_NAME} POST_BUILD
|
||||
COMMAND "${OPENSSL_EXECUTABLE}" req -new -newkey rsa:2048 -days 1 -nodes -x509 -keyout key.pem -out cert.pem -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" $<$<BOOL:${OPENSSL_EXECUTABLE_CFG}>:-config> $<$<BOOL:${OPENSSL_EXECUTABLE_CFG}>:${OPENSSL_EXECUTABLE_CFG}>
|
||||
WORKING_DIRECTORY ${WORKING_DIRECTORY}
|
||||
BYPRODUCTS ${WORKING_DIRECTORY}/key.pem ${WORKING_DIRECTORY}/cert.pem
|
||||
COMMENT "Generating self-signed certificate using ${OPENSSL_EXECUTABLE} ${OPENSSL_EXECUTABLE_CFG}"
|
||||
VERBATIM
|
||||
COMMAND_EXPAND_LISTS
|
||||
)
|
26
common/connection/src/DataConnection.cppm
Normal file
26
common/connection/src/DataConnection.cppm
Normal file
@ -0,0 +1,26 @@
|
||||
export module MMO.DataConnection;
|
||||
import std;
|
||||
import :MSQuicConnection;
|
||||
import :MSQuicServer;
|
||||
|
||||
namespace MMO::Networking {
|
||||
export using NetworkingDataLane = MSQUIC::MSQuicStream;
|
||||
export using NetworkingConnection = MSQUIC::MSQuicConnection;
|
||||
export using NetworkingServer = MSQUIC::MSQuicServer;
|
||||
|
||||
export using NetworkingError = MSQUIC::MSQuicError;
|
||||
|
||||
export using NetworkSettings = QUIC_SETTINGS;
|
||||
export using NetworkCertificate = QUIC_CERTIFICATE_FILE;
|
||||
export using NetworkCertificateProtected = QUIC_CERTIFICATE_FILE_PROTECTED;
|
||||
export using NetworkCredentials = QUIC_CREDENTIAL_CONFIG;
|
||||
|
||||
export namespace Additional {
|
||||
using CertificateHash = QUIC_CERTIFICATE_HASH;
|
||||
using CertificateHashStore = QUIC_CERTIFICATE_HASH_STORE;
|
||||
using CertificateHashStoreFlags = QUIC_CERTIFICATE_HASH_STORE_FLAGS;
|
||||
|
||||
using CredentialFlags = QUIC_CREDENTIAL_FLAGS;
|
||||
using CredentialType = QUIC_CREDENTIAL_TYPE;
|
||||
}
|
||||
}
|
850
common/connection/src/MSQuicConnection.cppm
Normal file
850
common/connection/src/MSQuicConnection.cppm
Normal file
@ -0,0 +1,850 @@
|
||||
export module MMO.DataConnection:MSQuicConnection;
|
||||
|
||||
import <msquic.h>;
|
||||
import std;
|
||||
import :MSQuicGlobal;
|
||||
import :MSQuicError;
|
||||
import MMO.Logging;
|
||||
|
||||
namespace MMO::Networking::MSQUIC {
|
||||
export template<class T>
|
||||
concept DataStorage = requires(T a) {
|
||||
sizeof(typename std::remove_reference_t<decltype(a)>::value_type) == 1;
|
||||
{ reinterpret_cast<uint8_t*>(std::data(a)) } -> std::same_as<uint8_t*>;
|
||||
{ static_cast<uint32_t>(std::size(a)) } -> std::same_as<uint32_t>;
|
||||
};
|
||||
|
||||
|
||||
// Forward declaration because we need to be able to ref MSQuicServer
|
||||
QUIC_STATUS msquicClientConnectionCallbackProxy(HQUIC connection, void* context, QUIC_CONNECTION_EVENT* event);
|
||||
QUIC_STATUS msquicClientStreamCallbackProxy(HQUIC connection, void* context, QUIC_STREAM_EVENT* event);
|
||||
|
||||
// Forward declaration because we need to be able to ref MSQuicConnection
|
||||
QUIC_STATUS msquicServerListeningCallbackProxy(HQUIC connection, void* context, QUIC_LISTENER_EVENT* event);
|
||||
QUIC_STATUS msquicServerConnectionCallbackProxy(HQUIC connection, void* context, QUIC_CONNECTION_EVENT* event);
|
||||
QUIC_STATUS msquicServerStreamCallbackProxy(HQUIC connection, void* context, QUIC_STREAM_EVENT* event);
|
||||
|
||||
export class MSQuicServer;
|
||||
export class MSQuicConnection;
|
||||
|
||||
export class MSQuicStream : public std::enable_shared_from_this<MSQuicStream> {
|
||||
friend MSQuicConnection;
|
||||
std::shared_ptr<MSQuicStream> backReference = nullptr;
|
||||
std::shared_ptr<HQUIC> connectionStream = nullptr;
|
||||
std::shared_ptr<void> streamAutoCloser = nullptr;
|
||||
QUIC_STATUS status = QUIC_STATUS_NOT_FOUND;
|
||||
|
||||
std::vector<uint8_t> incomingData = { };
|
||||
|
||||
std::weak_ptr<MSQuicConnection> origin;
|
||||
|
||||
MSQuicStream(
|
||||
std::shared_ptr<HQUIC> connectionStream,
|
||||
std::shared_ptr<void> streamAutoCloser,
|
||||
std::weak_ptr<MSQuicConnection> origin,
|
||||
const QUIC_STATUS status
|
||||
) :
|
||||
connectionStream(std::move(connectionStream)), streamAutoCloser(std::move(streamAutoCloser)),
|
||||
status(status),
|
||||
origin(std::move(origin)) { }
|
||||
|
||||
public:
|
||||
[[nodiscard]]
|
||||
std::vector<uint8_t> retrieveAndResetData() {
|
||||
std::vector<uint8_t> outgoingData;
|
||||
outgoingData.swap(incomingData);
|
||||
return outgoingData;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
bool hasData() const {
|
||||
return !incomingData.empty();
|
||||
}
|
||||
|
||||
std::weak_ptr<MSQuicConnection> getOrigin() {
|
||||
return origin;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
bool isConnected() const {
|
||||
return status == QUIC_STATUS_SUCCESS;
|
||||
}
|
||||
};
|
||||
|
||||
struct Payload : public QUIC_BUFFER, std::enable_shared_from_this<Payload> {
|
||||
// We use this persistent buffer to ensure that the data is not deallocated
|
||||
std::any data;
|
||||
|
||||
template<DataStorage T>
|
||||
explicit Payload(T& data) : data(data) {
|
||||
this->Length = static_cast<uint32_t>(std::size(std::any_cast<T&>(this->data)));
|
||||
this->Buffer = const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(
|
||||
std::data(std::any_cast<T&>(this->data))));
|
||||
}
|
||||
};
|
||||
|
||||
export class MSQuicConnection : public std::enable_shared_from_this<MSQuicConnection> {
|
||||
public:
|
||||
std::shared_ptr<Logging::Logger> logger = MMO::Logging::DEFAULT_LOGGER
|
||||
? MMO::Logging::DEFAULT_LOGGER
|
||||
: std::make_shared<Logging::NullLogger>();
|
||||
|
||||
protected:
|
||||
std::shared_ptr<HQUIC> configuration = nullptr;
|
||||
std::shared_ptr<HQUIC> connectionHandle = nullptr;
|
||||
std::vector<std::shared_ptr<HQUIC>> connectionStreams = { };
|
||||
std::unordered_map<HQUIC, std::weak_ptr<MSQuicStream>> connectionStreamMap = { };
|
||||
|
||||
std::unordered_map<void*, std::shared_ptr<Payload>> queuedData = { };
|
||||
|
||||
|
||||
std::vector<std::weak_ptr<std::function<void(
|
||||
std::shared_ptr<MSQuicStream>,
|
||||
std::shared_ptr<MSQuicConnection>
|
||||
)>>> onRemoteStreamCallbacks;
|
||||
|
||||
QUIC_STATUS connectionStatus = QUIC_STATUS_NOT_FOUND;
|
||||
|
||||
public:
|
||||
const std::string remoteAddress;
|
||||
const uint16_t remotePort;
|
||||
|
||||
|
||||
void addRemoteStreamCallbacks(
|
||||
const std::weak_ptr<std::function<void(
|
||||
std::shared_ptr<MSQuicStream>,
|
||||
std::shared_ptr<MSQuicConnection>
|
||||
)>>& callback
|
||||
) {
|
||||
onRemoteStreamCallbacks.emplace_back(callback);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
bool isConnected() const {
|
||||
return connectionStatus == QUIC_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
bool isTerminated() const {
|
||||
return connectionStatus != QUIC_STATUS_PENDING && connectionStatus != QUIC_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
bool isConnecting() const {
|
||||
return connectionStatus == QUIC_STATUS_PENDING;
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]] explicit
|
||||
MSQuicConnection(
|
||||
std::shared_ptr<HQUIC> configuration,
|
||||
std::string remoteAddress,
|
||||
const uint16_t remotePort
|
||||
) :
|
||||
configuration(std::move(configuration)), remoteAddress(std::move(remoteAddress)), remotePort(remotePort) { }
|
||||
|
||||
friend MSQuicServer;
|
||||
|
||||
public:
|
||||
static void defaultSettings(QUIC_SETTINGS& settings, QUIC_CREDENTIAL_CONFIG& credConfig) {
|
||||
settings.IdleTimeoutMs = 5000;
|
||||
settings.IsSet.IdleTimeoutMs = TRUE;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
static std::expected<std::shared_ptr<MSQuicConnection>, MSQuicError> connectTo(
|
||||
const QUIC_SETTINGS& settings,
|
||||
const QUIC_CREDENTIAL_CONFIG& credConfig,
|
||||
const std::string& remoteAddress,
|
||||
const uint16_t remotePort
|
||||
) {
|
||||
auto MSQGR = MSQuicGlobal::get();
|
||||
|
||||
if (!MSQGR.has_value()) {
|
||||
return std::unexpected(MSQGR.error());
|
||||
}
|
||||
|
||||
const auto& MSQG = MSQGR.value().get();
|
||||
const QUIC_API_TABLE* MsQuic = *MSQG.msQuicApiTable;
|
||||
|
||||
std::shared_ptr<HQUIC> configuration(
|
||||
new HQUIC(nullptr), [](const HQUIC* configuration) {
|
||||
auto msquic = MSQuicGlobal::get();
|
||||
if (msquic.has_value() && *configuration != nullptr) {
|
||||
(*msquic.value().get().msQuicApiTable)->ConfigurationClose(*configuration);
|
||||
}
|
||||
|
||||
delete configuration;
|
||||
}
|
||||
);
|
||||
if (QUIC_STATUS status = MsQuic->ConfigurationOpen(
|
||||
*MSQG.registration,
|
||||
&MSQG.applicationLayerProtocolNegotiationName, 1,
|
||||
&settings, sizeof(settings), nullptr,
|
||||
configuration.get()
|
||||
);
|
||||
QUIC_FAILED(status)) {
|
||||
return std::unexpected(MSQuicError(status));
|
||||
}
|
||||
|
||||
if (QUIC_STATUS status = MsQuic->ConfigurationLoadCredential(*configuration.get(), &credConfig);
|
||||
QUIC_FAILED(status)) {
|
||||
return std::unexpected(MSQuicError(status));
|
||||
}
|
||||
|
||||
return std::shared_ptr<MSQuicConnection>(
|
||||
new MSQuicConnection(configuration, remoteAddress, remotePort),
|
||||
[](const MSQuicConnection* client) {
|
||||
auto msquic = MSQuicGlobal::get();
|
||||
if (msquic.has_value()) {
|
||||
auto MSQG2 = msquic.value().get();
|
||||
for (auto& stream : client->connectionStreams) {
|
||||
(*MSQG2.msQuicApiTable)->StreamShutdown(
|
||||
*stream,
|
||||
QUIC_STREAM_SHUTDOWN_FLAG_ABORT | QUIC_STREAM_SHUTDOWN_FLAG_IMMEDIATE,
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
delete client;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
static std::expected<std::shared_ptr<MSQuicConnection>, MSQuicError>
|
||||
connectTo(const std::string& remoteAddress, const uint16_t remotePort) {
|
||||
QUIC_SETTINGS settings{ 0 };
|
||||
QUIC_CREDENTIAL_CONFIG credentialConfig{
|
||||
.Type = QUIC_CREDENTIAL_TYPE_NONE,
|
||||
.Flags = QUIC_CREDENTIAL_FLAG_CLIENT | QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION
|
||||
};
|
||||
defaultSettings(settings, credentialConfig);
|
||||
return connectTo(settings, credentialConfig, remoteAddress, remotePort);
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]]
|
||||
std::optional<MSQuicError> establishConnection(const std::span<std::uint8_t>& resumptionTicket = { }) {
|
||||
if (!isConnecting() && !isConnected()) {
|
||||
connectionStatus = QUIC_STATUS_PENDING;
|
||||
auto MSQGR = MSQuicGlobal::get();
|
||||
|
||||
if (!MSQGR.has_value()) {
|
||||
connectionStatus = MSQGR.error().status;
|
||||
return MSQGR.error();
|
||||
}
|
||||
|
||||
const auto& MSQG = MSQGR.value().get();
|
||||
|
||||
connectionHandle.reset(
|
||||
new HQUIC(nullptr), [](const HQUIC* connectionHandle) {
|
||||
auto msquic = MSQuicGlobal::get();
|
||||
if (msquic.has_value() && *connectionHandle != nullptr) {
|
||||
(*msquic.value().get().msQuicApiTable)->ConnectionClose(*connectionHandle);
|
||||
}
|
||||
|
||||
delete connectionHandle;
|
||||
}
|
||||
);
|
||||
|
||||
if (QUIC_STATUS status = (*MSQG.msQuicApiTable)->ConnectionOpen(
|
||||
*MSQG.registration, msquicClientConnectionCallbackProxy, this, connectionHandle.get()
|
||||
);
|
||||
QUIC_FAILED(status)) {
|
||||
connectionStatus = status;
|
||||
connectionHandle.reset();
|
||||
return MSQuicError(status);
|
||||
}
|
||||
|
||||
if (!resumptionTicket.empty()) {
|
||||
if (QUIC_STATUS status = (*MSQG.msQuicApiTable)->SetParam(
|
||||
*connectionHandle, QUIC_PARAM_CONN_RESUMPTION_TICKET,
|
||||
static_cast<uint32_t>(resumptionTicket.size()),
|
||||
resumptionTicket.data()
|
||||
);
|
||||
QUIC_FAILED(status)) {
|
||||
connectionStatus = status;
|
||||
connectionHandle.reset();
|
||||
return MSQuicError(status);
|
||||
}
|
||||
}
|
||||
|
||||
if (QUIC_STATUS status = (*MSQG.msQuicApiTable)->ConnectionStart(
|
||||
*connectionHandle, *configuration, QUIC_ADDRESS_FAMILY_UNSPEC,
|
||||
remoteAddress.c_str(), remotePort
|
||||
);
|
||||
QUIC_FAILED(status)) {
|
||||
connectionStatus = status;
|
||||
connectionHandle.reset();
|
||||
return MSQuicError(status);
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::expected<std::shared_ptr<MSQuicStream>, MSQuicError> createStream(
|
||||
QUIC_STREAM_OPEN_FLAGS openFlags = QUIC_STREAM_OPEN_FLAG_NONE,
|
||||
QUIC_STREAM_START_FLAGS startFlags = QUIC_STREAM_START_FLAG_NONE
|
||||
) {
|
||||
auto MSQGR = MSQuicGlobal::get();
|
||||
|
||||
if (!MSQGR.has_value()) {
|
||||
return std::unexpected(MSQGR.error());
|
||||
}
|
||||
|
||||
const auto& MSQG = MSQGR.value().get();
|
||||
|
||||
std::shared_ptr<HQUIC> stream;
|
||||
stream.reset(
|
||||
new HQUIC(nullptr), [&](const HQUIC* connectionStream) {
|
||||
auto msquic = MSQuicGlobal::get();
|
||||
if (msquic.has_value() && *connectionStream != nullptr) {
|
||||
(*msquic.value().get().msQuicApiTable)->StreamClose(*connectionStream);
|
||||
}
|
||||
|
||||
delete connectionStream;
|
||||
}
|
||||
);
|
||||
if (QUIC_STATUS status = (*MSQG.msQuicApiTable)->StreamOpen(
|
||||
*connectionHandle, openFlags,
|
||||
msquicClientStreamCallbackProxy, this,
|
||||
stream.get()
|
||||
);
|
||||
QUIC_FAILED(status)) {
|
||||
return std::unexpected(MSQuicError(status));
|
||||
}
|
||||
|
||||
if (QUIC_STATUS status = (*MSQG.msQuicApiTable)->StreamStart(*stream, startFlags);
|
||||
QUIC_FAILED(status)) {
|
||||
return std::unexpected(MSQuicError(status));
|
||||
}
|
||||
|
||||
connectionStreams.emplace_back(stream);
|
||||
|
||||
// We store these separately, since connectionStreams should be closed based on the relevant events but
|
||||
// a close can also be requested by losing the shared_ptr preventing a memory/stream leak
|
||||
std::shared_ptr<MSQuicStream> streamPtr(
|
||||
new MSQuicStream(
|
||||
stream,
|
||||
std::shared_ptr<void>(
|
||||
nullptr, [&, stream](void*) {
|
||||
auto msquic = MSQuicGlobal::get();
|
||||
if (msquic.has_value() && *stream != nullptr) {
|
||||
(*msquic.value().get().msQuicApiTable)->StreamShutdown(
|
||||
*stream, QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL, 0
|
||||
);
|
||||
} else { std::erase(connectionStreams, stream); }
|
||||
}
|
||||
),
|
||||
shared_from_this(),
|
||||
QUIC_STATUS_SUCCESS
|
||||
), [](const MSQuicStream* ptr) {
|
||||
delete ptr;
|
||||
}
|
||||
);
|
||||
|
||||
connectionStreamMap.emplace(*stream, streamPtr);
|
||||
|
||||
return streamPtr;
|
||||
}
|
||||
|
||||
template<DataStorage T>
|
||||
[[nodiscard]]
|
||||
std::optional<MSQuicError> send(
|
||||
T& data,
|
||||
std::shared_ptr<MSQuicStream>& stream,
|
||||
QUIC_SEND_FLAGS flags = QUIC_SEND_FLAG_NONE
|
||||
) {
|
||||
if (!isConnected() || (stream && !stream->isConnected())) {
|
||||
return MSQuicError(QUIC_STATUS_INVALID_STATE);
|
||||
}
|
||||
|
||||
auto MSQGR = MSQuicGlobal::get();
|
||||
|
||||
if (!MSQGR.has_value()) {
|
||||
return MSQGR.error();
|
||||
}
|
||||
|
||||
const auto& MSQG = MSQGR.value().get();
|
||||
|
||||
if ((!stream || !stream->connectionStream) && !(flags & QUIC_SEND_FLAG_START)) {
|
||||
return MSQuicError(QUIC_STATUS_INVALID_STATE);
|
||||
}
|
||||
|
||||
std::shared_ptr<HQUIC> dataStream = nullptr;
|
||||
if (stream && stream->connectionStream) {
|
||||
dataStream = stream->connectionStream;
|
||||
} else if (flags & QUIC_SEND_FLAG_FIN) {
|
||||
dataStream.reset(
|
||||
new HQUIC(nullptr), [](const HQUIC* connectionStream) {
|
||||
auto msquic = MSQuicGlobal::get();
|
||||
if (msquic.has_value() && *connectionStream != nullptr) {
|
||||
(*msquic.value().get().msQuicApiTable)->StreamClose(*connectionStream);
|
||||
}
|
||||
delete connectionStream;
|
||||
}
|
||||
);
|
||||
|
||||
if (QUIC_STATUS status = (*MSQG.msQuicApiTable)->StreamOpen(
|
||||
*connectionHandle, QUIC_STREAM_OPEN_FLAG_NONE, //QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL,
|
||||
msquicClientStreamCallbackProxy, this,
|
||||
dataStream.get()
|
||||
);
|
||||
QUIC_FAILED(status)) {
|
||||
return MSQuicError(status);
|
||||
}
|
||||
|
||||
connectionStreams.emplace_back(dataStream);
|
||||
} else if (auto strim = createStream(); strim.has_value()) {
|
||||
stream = strim.value();
|
||||
dataStream = stream->connectionStream;
|
||||
} else {
|
||||
return strim.error();
|
||||
}
|
||||
|
||||
if (!dataStream) {
|
||||
return MSQuicError(QUIC_STATUS_INVALID_STATE);
|
||||
}
|
||||
|
||||
|
||||
std::shared_ptr<Payload> sendData = std::make_shared<Payload>(data);
|
||||
|
||||
queuedData.emplace(sendData.get(), sendData);
|
||||
|
||||
// QUIC_BUFFER* buffer = new QUIC_BUFFER{
|
||||
// static_cast<uint32_t>(std::size(data)), reinterpret_cast<uint8_t*>(std::data(data))
|
||||
// };
|
||||
if (QUIC_STATUS status = (*MSQG.msQuicApiTable)->StreamSend(
|
||||
*dataStream.get(), sendData.get(), 1, flags, sendData.get()
|
||||
);
|
||||
QUIC_FAILED(status)) {
|
||||
(*MSQG.msQuicApiTable)->StreamShutdown(*dataStream.get(), QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 1);
|
||||
return MSQuicError(status);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template<DataStorage T>
|
||||
[[nodiscard]]
|
||||
constexpr std::optional<MSQuicError> send(
|
||||
T& data,
|
||||
QUIC_SEND_FLAGS flags = QUIC_SEND_FLAG_START | QUIC_SEND_FLAG_FIN
|
||||
) {
|
||||
std::shared_ptr<MSQuicStream> stream;
|
||||
return send(data, stream, flags | QUIC_SEND_FLAG_START | QUIC_SEND_FLAG_FIN);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::optional<MSQuicError> shutdown() const {
|
||||
if (!isConnected()) {
|
||||
return MSQuicError(QUIC_STATUS_INVALID_STATE);
|
||||
}
|
||||
|
||||
auto MSQGR = MSQuicGlobal::get();
|
||||
|
||||
if (!MSQGR.has_value()) {
|
||||
return MSQGR.error();
|
||||
}
|
||||
|
||||
const auto& MSQG = MSQGR.value().get();
|
||||
|
||||
(*MSQG.msQuicApiTable)->ConnectionShutdown(
|
||||
*connectionHandle, QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, 0
|
||||
);
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
private:
|
||||
friend QUIC_STATUS msquicClientConnectionCallbackProxy(
|
||||
HQUIC connection,
|
||||
void* context,
|
||||
QUIC_CONNECTION_EVENT* event
|
||||
);
|
||||
friend QUIC_STATUS msquicClientStreamCallbackProxy(
|
||||
HQUIC stream,
|
||||
void* context,
|
||||
QUIC_STREAM_EVENT* event
|
||||
);
|
||||
friend QUIC_STATUS msquicServerConnectionCallbackProxy(
|
||||
HQUIC connection,
|
||||
void* context,
|
||||
QUIC_CONNECTION_EVENT* event
|
||||
);
|
||||
friend QUIC_STATUS msquicServerStreamCallbackProxy(HQUIC connection, void* context, QUIC_STREAM_EVENT* event);
|
||||
|
||||
QUIC_STATUS connectionCallback(HQUIC connection, const QUIC_CONNECTION_EVENT* event) {
|
||||
auto clientId = reinterpret_cast<std::uintptr_t>(connection);
|
||||
switch (event->Type) {
|
||||
case QUIC_CONNECTION_EVENT_CONNECTED:
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG,
|
||||
"[client][{:X}] Connected", clientId
|
||||
);
|
||||
|
||||
connectionStatus = QUIC_STATUS_SUCCESS;
|
||||
break;
|
||||
|
||||
case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT:
|
||||
|
||||
/*
|
||||
* The transport has shut down the connection.
|
||||
* Generally, this is the expected way for the connection to shut down with this
|
||||
* protocol, since we let idle timeout kill the connection.
|
||||
*/
|
||||
if (event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status == QUIC_STATUS_CONNECTION_IDLE) {
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG, "[client][{:X}] Successfully shutdown on idle", clientId
|
||||
);
|
||||
} else {
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG,
|
||||
"[client][{:X}] Shut down by transport, 0x{:X}", clientId,
|
||||
static_cast<std::uint64_t>(event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status)
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER:
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG,
|
||||
"[client][{:X}] Shut down by peer 0x{:X}", clientId,
|
||||
event->SHUTDOWN_INITIATED_BY_PEER.ErrorCode
|
||||
);
|
||||
|
||||
break;
|
||||
case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE:
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG, "[client][{:X}] Connection shutdown complete", clientId
|
||||
);
|
||||
|
||||
|
||||
connectionStatus = QUIC_STATUS_NOT_FOUND;
|
||||
if (!event->SHUTDOWN_COMPLETE.AppCloseInProgress) {
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG, "[client][{:X}] Connection closed", clientId
|
||||
);
|
||||
connectionHandle.reset();
|
||||
}
|
||||
break;
|
||||
case QUIC_CONNECTION_EVENT_RESUMPTION_TICKET_RECEIVED:
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG,
|
||||
"[client][{:X}] Resumption ticket received ({} bytes):\n{}", clientId,
|
||||
event->RESUMPTION_TICKET_RECEIVED.ResumptionTicketLength, std::ranges::to<std::string>(
|
||||
std::views::join_with(
|
||||
std::ranges::transform_view(
|
||||
std::ranges::subrange(
|
||||
event->RESUMPTION_TICKET_RECEIVED.ResumptionTicket,
|
||||
event->RESUMPTION_TICKET_RECEIVED.ResumptionTicket + event->
|
||||
RESUMPTION_TICKET_RECEIVED.ResumptionTicketLength
|
||||
),
|
||||
[](std::uint8_t byte) {
|
||||
return std::format("{:0>2X}", byte);
|
||||
}
|
||||
),
|
||||
std::string("")
|
||||
)
|
||||
)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG, "[client][{:X}] Connection event [{}]", clientId, std::to_underlying(event->Type));
|
||||
|
||||
break;
|
||||
}
|
||||
return QUIC_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
void cleanupStreams() const {
|
||||
auto MSQGR = MSQuicGlobal::get();
|
||||
if (MSQGR.has_value()) {
|
||||
auto MSQG = MSQGR.value().get();
|
||||
for (auto& stream : connectionStreams) {
|
||||
if (*stream != nullptr) {
|
||||
(*MSQG.msQuicApiTable)->StreamShutdown(*stream, QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QUIC_STATUS commonStreamCallback(HQUIC stream, const QUIC_STREAM_EVENT* event, const bool isClient) {
|
||||
auto streamId = reinterpret_cast<std::uintptr_t>(stream);
|
||||
auto side = isClient ? "client" : "server";
|
||||
switch (event->Type) {
|
||||
case QUIC_STREAM_EVENT_SEND_COMPLETE:
|
||||
{
|
||||
//
|
||||
// A previous StreamSend call has completed, and the context is being
|
||||
// returned to the app.
|
||||
//
|
||||
auto dataId = reinterpret_cast<std::uintptr_t>(event->SEND_COMPLETE.ClientContext);
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG,
|
||||
"[{}][stream][{:X}] Data sent {} bytes [{:X} {}]", side, streamId,
|
||||
static_cast<Payload*>(event->SEND_COMPLETE.ClientContext)->Length,
|
||||
dataId,
|
||||
queuedData.contains(event->SEND_COMPLETE.ClientContext)
|
||||
? "CACHE HIT"
|
||||
: "CACHE MISS"
|
||||
);
|
||||
std::flush(std::cout);
|
||||
|
||||
queuedData.erase(event->SEND_COMPLETE.ClientContext);
|
||||
}
|
||||
break;
|
||||
case QUIC_STREAM_EVENT_RECEIVE:
|
||||
//
|
||||
// Data was received from the peer on the stream.
|
||||
//
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG,
|
||||
"[{}][stream][{:X}] Data received ({} bytes)", side, streamId,
|
||||
event->RECEIVE.TotalBufferLength
|
||||
);
|
||||
std::flush(std::cout);
|
||||
|
||||
if (connectionStreamMap.contains(stream)) {
|
||||
if (const auto ds = connectionStreamMap.at(stream).lock(); ds) {
|
||||
for (uint32_t i = 0; i < event->RECEIVE.BufferCount; i++) {
|
||||
const auto& [Length, Buffer] = event->RECEIVE.Buffers[i];
|
||||
ds->incomingData.insert(
|
||||
std::end(ds->incomingData),
|
||||
Buffer, Buffer + Length
|
||||
);
|
||||
}
|
||||
} else {
|
||||
connectionStreamMap.erase(stream);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case QUIC_STREAM_EVENT_PEER_SEND_ABORTED:
|
||||
{
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG,
|
||||
"[{}][stream][{:X}] Peer aborted reason: {}", side, streamId,
|
||||
event->PEER_SEND_ABORTED.ErrorCode
|
||||
);
|
||||
|
||||
|
||||
auto MSQGR = MSQuicGlobal::get();
|
||||
|
||||
if (!MSQGR.has_value()) {
|
||||
connectionStatus = MSQGR.error().status;
|
||||
return MSQGR.error().status;
|
||||
}
|
||||
|
||||
const auto& MSQG = MSQGR.value().get();
|
||||
(*MSQG.msQuicApiTable)->StreamShutdown(stream, QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 0);
|
||||
}
|
||||
break;
|
||||
case QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN:
|
||||
{
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG, "[{}][stream][{:X}] Peer shut down", side, streamId);
|
||||
|
||||
auto MSQGR = MSQuicGlobal::get();
|
||||
|
||||
if (!MSQGR.has_value()) {
|
||||
connectionStatus = MSQGR.error().status;
|
||||
return MSQGR.error().status;
|
||||
}
|
||||
|
||||
const auto& MSQG = MSQGR.value().get();
|
||||
(*MSQG.msQuicApiTable)->StreamShutdown(stream, QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL, 0);
|
||||
}
|
||||
break;
|
||||
case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE:
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG, "[{}][stream][{:X}] Shutdown done", side, streamId);
|
||||
|
||||
if (connectionStreamMap.contains(stream)) {
|
||||
if (auto ds = connectionStreamMap.at(stream).lock(); ds) {
|
||||
ds->status = QUIC_STATUS_ABORTED;
|
||||
}
|
||||
connectionStreamMap.erase(stream);
|
||||
}
|
||||
|
||||
std::erase_if(
|
||||
connectionStreams,
|
||||
[&](const std::shared_ptr<HQUIC>& strm) {
|
||||
return *strm.get() == stream;
|
||||
}
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return QUIC_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
QUIC_STATUS streamCallback(HQUIC stream, const QUIC_STREAM_EVENT* event) {
|
||||
return commonStreamCallback(stream, event, true);
|
||||
}
|
||||
|
||||
QUIC_STATUS serverConnectionCallback(HQUIC connection, const QUIC_CONNECTION_EVENT* event) {
|
||||
auto connectionId = reinterpret_cast<std::uintptr_t>(connection);
|
||||
switch (event->Type) {
|
||||
case QUIC_CONNECTION_EVENT_CONNECTED:
|
||||
{
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG, "[server][{:X}] Connected", connectionId);
|
||||
|
||||
auto MSQGR = MSQuicGlobal::get();
|
||||
|
||||
if (!MSQGR.has_value()) {
|
||||
connectionStatus = MSQGR.error().status;
|
||||
return MSQGR.error().status;
|
||||
}
|
||||
|
||||
const auto& MSQG = MSQGR.value().get();
|
||||
|
||||
(*MSQG.msQuicApiTable)->ConnectionSendResumptionTicket(
|
||||
connection, QUIC_SEND_RESUMPTION_FLAG_NONE, 0,
|
||||
nullptr
|
||||
);
|
||||
|
||||
connectionStatus = QUIC_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
break;
|
||||
case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT:
|
||||
if (event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status == QUIC_STATUS_CONNECTION_IDLE) {
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG, "[server][{:X}] Successfully shutdown on idle", connectionId
|
||||
);
|
||||
} else {
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG,
|
||||
"[server][{:X}] Shut down by transport, 0x{:X}", connectionId,
|
||||
static_cast<uint64_t>(event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status)
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER:
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG,
|
||||
"[server][{:X}] Shut down by peer 0x{:X}", connectionId,
|
||||
event->SHUTDOWN_INITIATED_BY_PEER.ErrorCode
|
||||
);
|
||||
|
||||
break;
|
||||
case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE:
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG,"[server][{:X}] Shutdown complete", connectionId);
|
||||
|
||||
|
||||
connectionStatus = QUIC_STATUS_NOT_FOUND;
|
||||
connectionHandle.reset();
|
||||
break;
|
||||
case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED:
|
||||
{
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG,
|
||||
"[server][{:X}] Peer stream started ({:X})", connectionId,
|
||||
reinterpret_cast<std::uintptr_t>(event->PEER_STREAM_STARTED.Stream)
|
||||
);
|
||||
|
||||
auto MSQGR = MSQuicGlobal::get();
|
||||
|
||||
if (!MSQGR.has_value()) {
|
||||
connectionStatus = MSQGR.error().status;
|
||||
return MSQGR.error().status;
|
||||
}
|
||||
|
||||
const auto& MSQG = MSQGR.value().get();
|
||||
|
||||
std::shared_ptr<HQUIC> newStream(
|
||||
new HQUIC(event->PEER_STREAM_STARTED.Stream),
|
||||
[&](const HQUIC* connectionStream) {
|
||||
auto msquic = MSQuicGlobal::get();
|
||||
if (msquic.has_value() && *connectionStream != nullptr) {
|
||||
auto table = *MSQG.msQuicApiTable;
|
||||
table->SetCallbackHandler(
|
||||
*connectionStream, nullptr, this
|
||||
);
|
||||
table->StreamClose(*connectionStream);
|
||||
}
|
||||
|
||||
delete connectionStream;
|
||||
}
|
||||
);
|
||||
|
||||
connectionStreams.emplace_back(newStream);
|
||||
|
||||
if (!onRemoteStreamCallbacks.empty()) {
|
||||
std::shared_ptr<MSQuicStream> client(
|
||||
new MSQuicStream(
|
||||
newStream, std::shared_ptr<void>(
|
||||
nullptr, [&, newStream](void*) {
|
||||
auto msquic = MSQuicGlobal::get();
|
||||
if (msquic.has_value() && *newStream != nullptr) {
|
||||
(*msquic.value().get().msQuicApiTable)->StreamShutdown(
|
||||
*newStream, QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL, 0
|
||||
);
|
||||
} else { std::erase(connectionStreams, newStream); }
|
||||
}
|
||||
), shared_from_this(), QUIC_STATUS_SUCCESS
|
||||
), [](const MSQuicStream* ptr) {
|
||||
delete ptr;
|
||||
}
|
||||
);
|
||||
|
||||
connectionStreamMap.emplace(*newStream, client);
|
||||
for (auto& callback : onRemoteStreamCallbacks) {
|
||||
if (const auto cb = callback.lock(); cb) {
|
||||
cb->operator()(client, shared_from_this());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(*MSQG.msQuicApiTable)->SetCallbackHandler(
|
||||
event->PEER_STREAM_STARTED.Stream,
|
||||
msquicServerStreamCallbackProxy, this
|
||||
);
|
||||
}
|
||||
break;
|
||||
case QUIC_CONNECTION_EVENT_RESUMED:
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG, "[server][{:X}] Connection resumed!", connectionId
|
||||
);
|
||||
|
||||
|
||||
break;
|
||||
default:
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG, "[server][{:X}] Connection event [{}]", connectionId, std::to_underlying(event->Type));
|
||||
|
||||
break;
|
||||
}
|
||||
return QUIC_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
QUIC_STATUS serverStreamCallback(HQUIC stream, const QUIC_STREAM_EVENT* event) {
|
||||
return commonStreamCallback(stream, event, false);
|
||||
}
|
||||
};
|
||||
|
||||
QUIC_STATUS msquicClientConnectionCallbackProxy(HQUIC connection, void* context, QUIC_CONNECTION_EVENT* event) {
|
||||
return static_cast<MSQuicConnection*>(context)->connectionCallback(connection, event);
|
||||
}
|
||||
|
||||
QUIC_STATUS msquicClientStreamCallbackProxy(HQUIC stream, void* context, QUIC_STREAM_EVENT* event) {
|
||||
return static_cast<MSQuicConnection*>(context)->streamCallback(stream, event);
|
||||
}
|
||||
|
||||
QUIC_STATUS msquicServerConnectionCallbackProxy(HQUIC connection, void* context, QUIC_CONNECTION_EVENT* event) {
|
||||
return static_cast<MSQuicConnection*>(context)->serverConnectionCallback(connection, event);
|
||||
}
|
||||
|
||||
QUIC_STATUS msquicServerStreamCallbackProxy(HQUIC connection, void* context, QUIC_STREAM_EVENT* event) {
|
||||
return static_cast<MSQuicConnection*>(context)->serverStreamCallback(connection, event);
|
||||
}
|
||||
|
||||
}
|
17
common/connection/src/MSQuicError.cppm
Normal file
17
common/connection/src/MSQuicError.cppm
Normal file
@ -0,0 +1,17 @@
|
||||
export module MMO.DataConnection:MSQuicError;
|
||||
|
||||
import <msquic.h>;
|
||||
import std;
|
||||
namespace MMO::Networking::MSQUIC {
|
||||
|
||||
export class MSQuicError : public std::runtime_error {
|
||||
public:
|
||||
QUIC_STATUS status = QUIC_STATUS_SUCCESS;
|
||||
|
||||
[[nodiscard]] explicit
|
||||
MSQuicError(QUIC_STATUS status) : status(status),
|
||||
std::runtime_error(
|
||||
std::format("MSQuic error: 0x{:X}", static_cast<uint64_t>(status))
|
||||
) { }
|
||||
};
|
||||
}
|
62
common/connection/src/MSQuicGlobal.cppm
Normal file
62
common/connection/src/MSQuicGlobal.cppm
Normal file
@ -0,0 +1,62 @@
|
||||
export module MMO.DataConnection:MSQuicGlobal;
|
||||
|
||||
import <msquic.h>;
|
||||
import std;
|
||||
import :MSQuicError;
|
||||
|
||||
namespace MMO::Networking::MSQUIC {
|
||||
export class MSQuicGlobal {
|
||||
public:
|
||||
std::shared_ptr<const QUIC_API_TABLE*> msQuicApiTable = nullptr;
|
||||
std::shared_ptr<HQUIC> registration;
|
||||
|
||||
const QUIC_BUFFER applicationLayerProtocolNegotiationName = { sizeof("sample") - 1, (uint8_t*) "sample" };
|
||||
|
||||
void reset() {
|
||||
std::println("Cleaning up MSQuic...");
|
||||
|
||||
|
||||
registration.reset();
|
||||
msQuicApiTable.reset();
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
static std::expected<std::reference_wrapper<MSQuicGlobal>, MSQuicError> get() {
|
||||
static MSQuicGlobal instance;
|
||||
if (!instance.msQuicApiTable) {
|
||||
instance.msQuicApiTable = std::shared_ptr<const QUIC_API_TABLE*>(
|
||||
new const QUIC_API_TABLE*(nullptr), [](const QUIC_API_TABLE** table) {
|
||||
std::println("Cleaning up MSQuic...");
|
||||
if (*table != nullptr) {
|
||||
MsQuicClose(*table);
|
||||
}
|
||||
delete table;
|
||||
}
|
||||
);
|
||||
if (QUIC_STATUS status = MsQuicOpen2(instance.msQuicApiTable.get()); QUIC_FAILED(status)) {
|
||||
return std::unexpected(MSQuicError(status));
|
||||
}
|
||||
|
||||
|
||||
instance.registration.reset(
|
||||
new HQUIC(nullptr),
|
||||
[apiTable = instance.msQuicApiTable](const HQUIC* registration) {
|
||||
if (*registration != nullptr) {
|
||||
(*apiTable.get())->RegistrationClose(*registration);
|
||||
}
|
||||
|
||||
delete registration;
|
||||
}
|
||||
);
|
||||
if (QUIC_STATUS status = (*instance.msQuicApiTable.get())->RegistrationOpen(
|
||||
nullptr, instance.registration.get()
|
||||
);
|
||||
QUIC_FAILED(status)) {
|
||||
return std::unexpected(MSQuicError(status));
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
};
|
||||
}
|
316
common/connection/src/MSQuicServer.cppm
Normal file
316
common/connection/src/MSQuicServer.cppm
Normal file
@ -0,0 +1,316 @@
|
||||
export module MMO.DataConnection:MSQuicServer;
|
||||
|
||||
import <msquic.h>;
|
||||
import std;
|
||||
import :MSQuicConnection;
|
||||
import :MSQuicGlobal;
|
||||
import :MSQuicError;
|
||||
import MMO.Logging;
|
||||
|
||||
namespace MMO::Networking::MSQUIC {
|
||||
export class MSQuicServer : public std::enable_shared_from_this<MSQuicServer> {
|
||||
public:
|
||||
std::shared_ptr<Logging::Logger> logger = MMO::Logging::DEFAULT_LOGGER
|
||||
? MMO::Logging::DEFAULT_LOGGER
|
||||
: std::make_shared<Logging::NullLogger>();
|
||||
std::shared_ptr<HQUIC> configuration = nullptr;
|
||||
std::shared_ptr<HQUIC> listenerHandle = nullptr;
|
||||
const std::string hostAddress;
|
||||
const uint16_t hostPort;
|
||||
|
||||
std::vector<std::weak_ptr<std::function<void(std::shared_ptr<MSQuicConnection>)>>> onConnectionCallbacks;
|
||||
|
||||
bool starting = false;
|
||||
bool listening = false;
|
||||
bool stopping = false;
|
||||
|
||||
public:
|
||||
[[nodiscard]]
|
||||
bool isStarting() const {
|
||||
return starting;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
bool isListening() const {
|
||||
return listening;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
bool isStopping() const {
|
||||
return stopping;
|
||||
}
|
||||
|
||||
private :
|
||||
MSQuicServer(std::shared_ptr<HQUIC> configuration, std::string hostAddress, const uint16_t hostPort) :
|
||||
configuration(std::move(configuration)), hostAddress(std::move(hostAddress)), hostPort(hostPort) { }
|
||||
|
||||
public:
|
||||
static void defaultSettings(QUIC_SETTINGS& settings, QUIC_CREDENTIAL_CONFIG& credConfig) {
|
||||
settings.IdleTimeoutMs = 5000;
|
||||
settings.IsSet.IdleTimeoutMs = TRUE;
|
||||
|
||||
settings.ServerResumptionLevel = QUIC_SERVER_RESUME_AND_ZERORTT;
|
||||
settings.IsSet.ServerResumptionLevel = TRUE;
|
||||
|
||||
settings.PeerBidiStreamCount = 1;
|
||||
settings.IsSet.PeerBidiStreamCount = TRUE;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]]
|
||||
static std::expected<std::shared_ptr<MSQuicServer>, MSQuicError> hostFrom(
|
||||
QUIC_SETTINGS& settings,
|
||||
QUIC_CREDENTIAL_CONFIG& credConfig,
|
||||
const uint16_t hostPort,
|
||||
const std::string& hostAddress = "0.0.0.0"
|
||||
) {
|
||||
auto MSQGR = MSQuicGlobal::get();
|
||||
|
||||
if (!MSQGR.has_value()) {
|
||||
return std::unexpected(MSQGR.error());
|
||||
}
|
||||
|
||||
const auto& MSQG = MSQGR.value().get();
|
||||
const QUIC_API_TABLE* MsQuic = *MSQG.msQuicApiTable;
|
||||
|
||||
std::shared_ptr<HQUIC> configuration(
|
||||
new HQUIC(nullptr), [](const HQUIC* configuration) {
|
||||
auto msquic = MSQuicGlobal::get();
|
||||
if (msquic.has_value() && *configuration != nullptr) {
|
||||
(*msquic.value().get().msQuicApiTable)->ConfigurationClose(*configuration);
|
||||
}
|
||||
|
||||
delete configuration;
|
||||
}
|
||||
);
|
||||
if (QUIC_STATUS status = MsQuic->ConfigurationOpen(
|
||||
*MSQG.registration,
|
||||
&MSQG.applicationLayerProtocolNegotiationName, 1,
|
||||
&settings, sizeof(settings), nullptr,
|
||||
configuration.get()
|
||||
);
|
||||
QUIC_FAILED(status)) {
|
||||
return std::unexpected(MSQuicError(status));
|
||||
}
|
||||
|
||||
if (QUIC_STATUS status = MsQuic->ConfigurationLoadCredential(*configuration.get(), &credConfig);
|
||||
QUIC_FAILED(status)) {
|
||||
return std::unexpected(MSQuicError(status));
|
||||
}
|
||||
|
||||
return std::shared_ptr<MSQuicServer>(
|
||||
new MSQuicServer(configuration, hostAddress, hostPort), [](const auto* v) {
|
||||
delete v;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
static std::expected<std::shared_ptr<MSQuicServer>, MSQuicError> hostFrom(
|
||||
QUIC_CREDENTIAL_CONFIG& credentialConfig,
|
||||
const uint16_t hostPort,
|
||||
const std::string& hostAddress = "0.0.0.0"
|
||||
) {
|
||||
QUIC_SETTINGS settings{ 0 };
|
||||
defaultSettings(settings, credentialConfig);
|
||||
return hostFrom(settings, credentialConfig, hostPort, hostAddress);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::optional<MSQuicError> startListening() {
|
||||
if (!listening && !starting) {
|
||||
starting = true;
|
||||
|
||||
auto MSQGR = MSQuicGlobal::get();
|
||||
|
||||
if (!MSQGR.has_value()) {
|
||||
starting = false;
|
||||
return MSQGR.error();
|
||||
}
|
||||
|
||||
const auto& MSQG = MSQGR.value().get();
|
||||
|
||||
|
||||
listenerHandle.reset(
|
||||
new HQUIC(nullptr), [](const HQUIC* listenerHandle) {
|
||||
auto msquic = MSQuicGlobal::get();
|
||||
if (msquic.has_value() && *listenerHandle != nullptr) {
|
||||
(*msquic.value().get().msQuicApiTable)->ListenerClose(*listenerHandle);
|
||||
}
|
||||
|
||||
delete listenerHandle;
|
||||
}
|
||||
);
|
||||
|
||||
if (const QUIC_STATUS status = (*MSQG.msQuicApiTable)->ListenerOpen(
|
||||
*MSQG.registration,
|
||||
msquicServerListeningCallbackProxy, this,
|
||||
listenerHandle.get()
|
||||
);
|
||||
QUIC_FAILED(status)) {
|
||||
starting = false;
|
||||
return MSQuicError(status);
|
||||
}
|
||||
|
||||
QUIC_ADDR address = { 0 };
|
||||
QuicAddrSetFamily(&address, QUIC_ADDRESS_FAMILY_UNSPEC);
|
||||
QuicAddrFromString(hostAddress.c_str(), hostPort, &address);
|
||||
|
||||
if (const QUIC_STATUS status = (*MSQG.msQuicApiTable)->ListenerStart(
|
||||
*listenerHandle.get(), &MSQG.applicationLayerProtocolNegotiationName, 1, &address
|
||||
);
|
||||
QUIC_FAILED(status)) {
|
||||
starting = false;
|
||||
return MSQuicError(status);
|
||||
}
|
||||
|
||||
listening = true;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
std::optional<MSQuicError> stopListening() {
|
||||
if (listening && !stopping) {
|
||||
stopping = true;
|
||||
|
||||
auto MSQGR = MSQuicGlobal::get();
|
||||
|
||||
if (!MSQGR.has_value()) {
|
||||
return MSQGR.error();
|
||||
}
|
||||
|
||||
const auto& MSQG = MSQGR.value().get();
|
||||
|
||||
(*MSQG.msQuicApiTable)->ListenerStop(*listenerHandle.get());
|
||||
|
||||
listenerHandle.reset();
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void addReceiveCallback(const std::weak_ptr<std::function<void(std::shared_ptr<MSQuicConnection>)>>& callback) {
|
||||
onConnectionCallbacks.emplace_back(callback);
|
||||
}
|
||||
|
||||
private:
|
||||
friend QUIC_STATUS msquicServerListeningCallbackProxy(
|
||||
HQUIC connection,
|
||||
void* context,
|
||||
QUIC_LISTENER_EVENT* event
|
||||
);
|
||||
friend QUIC_STATUS msquicServerConnectionCallbackProxy(
|
||||
HQUIC connection,
|
||||
void* context,
|
||||
QUIC_CONNECTION_EVENT* event
|
||||
);
|
||||
friend QUIC_STATUS msquicServerStreamCallbackProxy(HQUIC connection, void* context, QUIC_STREAM_EVENT* event);
|
||||
|
||||
QUIC_STATUS serverListenerCallback(HQUIC connection, QUIC_LISTENER_EVENT* event) {
|
||||
auto connectionId = reinterpret_cast<std::uintptr_t>(connection);
|
||||
|
||||
QUIC_STATUS Status = QUIC_STATUS_NOT_SUPPORTED;
|
||||
switch (event->Type) {
|
||||
case QUIC_LISTENER_EVENT_NEW_CONNECTION:
|
||||
{
|
||||
QUIC_ADDR_STR address = { 0 };
|
||||
QuicAddrToString(event->NEW_CONNECTION.Info->RemoteAddress, &address);
|
||||
|
||||
const std::string_view remoteAddress = address.Address;
|
||||
const auto indexSplit = remoteAddress.find_last_of(':');
|
||||
auto remoteAddressPort = static_cast<uint16_t>(std::strtol(
|
||||
remoteAddress.substr(indexSplit + 1).data(), nullptr, 10
|
||||
));
|
||||
std::string remoteAddressIp(remoteAddress.substr(0, indexSplit));
|
||||
|
||||
logger->log(
|
||||
Logging::LEVEL_DEBUG,
|
||||
"[server][{:X}][{}] New connection from {}:{}", connectionId,
|
||||
reinterpret_cast<std::uintptr_t>(event->NEW_CONNECTION.Connection),
|
||||
remoteAddressIp, remoteAddressPort
|
||||
);
|
||||
|
||||
|
||||
auto MSQGR = MSQuicGlobal::get();
|
||||
|
||||
if (!MSQGR.has_value()) {
|
||||
listening = false;
|
||||
return MSQGR.error().status;
|
||||
}
|
||||
|
||||
const auto& MSQG = MSQGR.value().get();
|
||||
|
||||
const std::shared_ptr<MSQuicConnection> client(
|
||||
new MSQuicConnection{
|
||||
configuration,
|
||||
remoteAddressIp,
|
||||
remoteAddressPort
|
||||
}, [](MSQuicConnection* c) {
|
||||
auto msquic = MSQuicGlobal::get();
|
||||
if (msquic.has_value()) {
|
||||
auto MSQG2 = msquic->get();
|
||||
for (auto& stream : c->connectionStreams) {
|
||||
(*MSQG2.msQuicApiTable)->StreamShutdown(
|
||||
*stream,
|
||||
QUIC_STREAM_SHUTDOWN_FLAG_ABORT | QUIC_STREAM_SHUTDOWN_FLAG_IMMEDIATE, 0
|
||||
);
|
||||
}
|
||||
if (c->connectionHandle) {
|
||||
const auto handle = *c->connectionHandle.get();
|
||||
c->connectionHandle.reset();
|
||||
(*MSQG2.msQuicApiTable)->SetCallbackHandler(handle, nullptr, nullptr);
|
||||
}
|
||||
}
|
||||
delete c;
|
||||
}
|
||||
);
|
||||
|
||||
client->connectionHandle.reset(
|
||||
new HQUIC(event->NEW_CONNECTION.Connection),
|
||||
[](const HQUIC* connectionHandle) {
|
||||
auto msquic = MSQuicGlobal::get();
|
||||
if (msquic.has_value() && *connectionHandle != nullptr) {
|
||||
(*msquic.value().get().msQuicApiTable)->ConnectionClose(
|
||||
*connectionHandle
|
||||
);
|
||||
}
|
||||
|
||||
delete connectionHandle;
|
||||
}
|
||||
);
|
||||
|
||||
(*MSQG.msQuicApiTable)->SetCallbackHandler(
|
||||
event->NEW_CONNECTION.Connection,
|
||||
msquicServerConnectionCallbackProxy, client.get()
|
||||
);
|
||||
Status = (*MSQG.msQuicApiTable)->ConnectionSetConfiguration(
|
||||
event->NEW_CONNECTION.Connection, *configuration.get()
|
||||
);
|
||||
|
||||
for (auto& callback : onConnectionCallbacks) {
|
||||
if (const auto cb = callback.lock(); cb) {
|
||||
cb->operator()(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case QUIC_LISTENER_EVENT_STOP_COMPLETE:
|
||||
{
|
||||
listenerHandle.reset();
|
||||
listening = false;
|
||||
stopping = false;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return Status;
|
||||
}
|
||||
};
|
||||
|
||||
QUIC_STATUS msquicServerListeningCallbackProxy(HQUIC connection, void* context, QUIC_LISTENER_EVENT* event) {
|
||||
return static_cast<MSQuicServer*>(context)->serverListenerCallback(connection, event);
|
||||
}
|
||||
|
||||
}
|
266
common/connection/tests/DataConnection_test.cppm
Normal file
266
common/connection/tests/DataConnection_test.cppm
Normal file
@ -0,0 +1,266 @@
|
||||
module;
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
export module DataConnection_test;
|
||||
import MMO.DataConnection;
|
||||
import std;
|
||||
|
||||
// class DataConnection : public ::testing::Test {
|
||||
// void TearDown() override {
|
||||
// MMO::Networking::MSQUIC::MSQuicGlobal::get()->get().reset();
|
||||
// }
|
||||
// };
|
||||
|
||||
TEST(DataConnection, ClientSettings) {
|
||||
MMO::Networking::NetworkSettings settings{0};
|
||||
MMO::Networking::NetworkCredentials credConfig{
|
||||
.Type = MMO::Networking::Additional::CredentialType::QUIC_CREDENTIAL_TYPE_NONE,
|
||||
.Flags = MMO::Networking::Additional::CredentialFlags::QUIC_CREDENTIAL_FLAG_CLIENT | MMO::Networking::Additional::CredentialFlags::QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION
|
||||
};
|
||||
|
||||
MMO::Networking::NetworkingConnection::defaultSettings(settings, credConfig);
|
||||
auto res = MMO::Networking::NetworkingConnection::connectTo(settings, credConfig, "127.0.0.1", 2524);
|
||||
EXPECT_TRUE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(DataConnection, ServerSettings) {
|
||||
MMO::Networking::NetworkSettings settings{0};
|
||||
MMO::Networking::NetworkCertificate certFile{
|
||||
.PrivateKeyFile = "../key.pem",
|
||||
.CertificateFile = "../cert.pem",
|
||||
};
|
||||
MMO::Networking::NetworkCredentials credConfig{
|
||||
.Type = MMO::Networking::Additional::CredentialType::QUIC_CREDENTIAL_TYPE_CERTIFICATE_FILE,
|
||||
.CertificateFile = &certFile
|
||||
};
|
||||
|
||||
MMO::Networking::NetworkingServer::defaultSettings(settings, credConfig);
|
||||
auto res = MMO::Networking::NetworkingServer::hostFrom(settings, credConfig, 2524, "127.0.0.1");
|
||||
EXPECT_TRUE(res.has_value());
|
||||
}
|
||||
|
||||
TEST(DataConnection, ConnectToRemote) {
|
||||
MMO::Networking::NetworkSettings settings{0};
|
||||
MMO::Networking::NetworkCredentials credConfig{
|
||||
.Type = MMO::Networking::Additional::CredentialType::QUIC_CREDENTIAL_TYPE_NONE,
|
||||
.Flags = MMO::Networking::Additional::CredentialFlags::QUIC_CREDENTIAL_FLAG_CLIENT | MMO::Networking::Additional::CredentialFlags::QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION
|
||||
};
|
||||
MMO::Networking::NetworkingConnection::defaultSettings(settings, credConfig);
|
||||
auto res = MMO::Networking::NetworkingConnection::connectTo(settings, credConfig, "127.0.0.1", 2524);
|
||||
EXPECT_TRUE(res.has_value());
|
||||
|
||||
auto connection = res.value();
|
||||
|
||||
auto result = connection->establishConnection();
|
||||
|
||||
EXPECT_FALSE(result.has_value());
|
||||
}
|
||||
|
||||
TEST(DataConnection, ListenForConnections) {
|
||||
MMO::Networking::NetworkSettings settings{0};
|
||||
MMO::Networking::NetworkCertificate certFile{
|
||||
.PrivateKeyFile = "../key.pem",
|
||||
.CertificateFile = "../cert.pem",
|
||||
};
|
||||
MMO::Networking::NetworkCredentials credConfig{
|
||||
.Type = MMO::Networking::Additional::CredentialType::QUIC_CREDENTIAL_TYPE_CERTIFICATE_FILE,
|
||||
.CertificateFile = &certFile
|
||||
};
|
||||
MMO::Networking::NetworkingServer::defaultSettings(settings, credConfig);
|
||||
auto res = MMO::Networking::NetworkingServer::hostFrom(settings, credConfig, 2524, "127.0.0.1");
|
||||
EXPECT_TRUE(res.has_value());
|
||||
|
||||
auto server = res.value();
|
||||
|
||||
EXPECT_THAT(server->startListening(), testing::Eq(std::nullopt));
|
||||
}
|
||||
|
||||
|
||||
TEST(DataConnection, EstablishConnection) {
|
||||
auto timeout = std::chrono::seconds(5);
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
|
||||
MMO::Networking::NetworkSettings serverSettings{0};
|
||||
MMO::Networking::NetworkCertificate serverCertFile{
|
||||
.PrivateKeyFile = "../key.pem",
|
||||
.CertificateFile = "../cert.pem",
|
||||
};
|
||||
MMO::Networking::NetworkCredentials serverCredConfig{
|
||||
.Type = MMO::Networking::Additional::CredentialType::QUIC_CREDENTIAL_TYPE_CERTIFICATE_FILE,
|
||||
.CertificateFile = &serverCertFile
|
||||
};
|
||||
MMO::Networking::NetworkingServer::defaultSettings(serverSettings, serverCredConfig);
|
||||
auto serverRes = MMO::Networking::NetworkingServer::hostFrom(serverSettings, serverCredConfig, 2524,
|
||||
"0.0.0.0");
|
||||
EXPECT_TRUE(serverRes.has_value());
|
||||
|
||||
auto server = serverRes.value();
|
||||
|
||||
EXPECT_THAT(server->startListening(), testing::Eq(std::nullopt));
|
||||
|
||||
std::println("Binding listener...");
|
||||
std::fflush(stdout);
|
||||
|
||||
std::shared_ptr<MMO::Networking::NetworkingConnection> serverConnection;
|
||||
|
||||
auto serverCB = std::make_shared<std::function<void(std::shared_ptr<MMO::Networking::NetworkingConnection>)>>(
|
||||
[&serverConnection](auto data) {
|
||||
serverConnection = std::move(data);
|
||||
});
|
||||
|
||||
server->addReceiveCallback(serverCB);
|
||||
|
||||
std::println("Creating client->..");
|
||||
std::fflush(stdout);
|
||||
|
||||
MMO::Networking::NetworkSettings clientSettings{0};
|
||||
MMO::Networking::NetworkCredentials clientCredConfig{
|
||||
.Type = MMO::Networking::Additional::CredentialType::QUIC_CREDENTIAL_TYPE_NONE,
|
||||
.Flags = MMO::Networking::Additional::CredentialFlags::QUIC_CREDENTIAL_FLAG_CLIENT | MMO::Networking::Additional::CredentialFlags::QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION
|
||||
};
|
||||
MMO::Networking::NetworkingConnection::defaultSettings(clientSettings, clientCredConfig);
|
||||
auto clientRes = MMO::Networking::NetworkingConnection::connectTo(clientSettings, clientCredConfig, "127.0.0.1",
|
||||
2524);
|
||||
EXPECT_TRUE(clientRes.has_value());
|
||||
|
||||
auto client = clientRes.value();
|
||||
|
||||
std::println("Connecting to server...");
|
||||
std::fflush(stdout);
|
||||
EXPECT_FALSE(client->establishConnection().has_value());
|
||||
|
||||
while (!client->isConnected()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
if (std::chrono::high_resolution_clock::now() - start > timeout) {
|
||||
FAIL() << "Connection timed out";
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(client->isConnected());
|
||||
EXPECT_TRUE(serverConnection != nullptr);
|
||||
|
||||
|
||||
while (!serverConnection->isConnected()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
if (std::chrono::high_resolution_clock::now() - start > timeout) {
|
||||
FAIL() << "Connection timed out";
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(serverConnection->isConnected());
|
||||
|
||||
}
|
||||
|
||||
|
||||
TEST(DataConnection, SendData) {
|
||||
auto timeout = std::chrono::seconds(5);
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
MMO::Networking::NetworkSettings serverSettings{0};
|
||||
MMO::Networking::NetworkCertificate serverCertFile{
|
||||
.PrivateKeyFile = "../key.pem",
|
||||
.CertificateFile = "../cert.pem",
|
||||
};
|
||||
MMO::Networking::NetworkCredentials serverCredConfig{
|
||||
.Type = MMO::Networking::Additional::CredentialType::QUIC_CREDENTIAL_TYPE_CERTIFICATE_FILE,
|
||||
.CertificateFile = &serverCertFile
|
||||
};
|
||||
MMO::Networking::NetworkingServer::defaultSettings(serverSettings, serverCredConfig);
|
||||
auto serverRes = MMO::Networking::NetworkingServer::hostFrom(serverSettings, serverCredConfig, 2524,
|
||||
"0.0.0.0");
|
||||
EXPECT_TRUE(serverRes.has_value());
|
||||
|
||||
auto server = serverRes.value();
|
||||
|
||||
EXPECT_THAT(server->startListening(), testing::Eq(std::nullopt));
|
||||
|
||||
std::println("Binding listener...");
|
||||
std::fflush(stdout);
|
||||
|
||||
std::shared_ptr<MMO::Networking::NetworkingConnection> serverConnection;
|
||||
|
||||
auto serverCB = std::make_shared<std::function<void(std::shared_ptr<MMO::Networking::NetworkingConnection>)>>(
|
||||
[&serverConnection](auto data) {
|
||||
serverConnection = std::move(data);
|
||||
});
|
||||
|
||||
server->addReceiveCallback(serverCB);
|
||||
|
||||
|
||||
std::println("Creating client->..");
|
||||
std::fflush(stdout);
|
||||
|
||||
MMO::Networking::NetworkSettings clientSettings{0};
|
||||
MMO::Networking::NetworkCredentials clientCredConfig{
|
||||
.Type = MMO::Networking::Additional::CredentialType::QUIC_CREDENTIAL_TYPE_NONE,
|
||||
.Flags = MMO::Networking::Additional::CredentialFlags::QUIC_CREDENTIAL_FLAG_CLIENT | MMO::Networking::Additional::CredentialFlags::QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION
|
||||
};
|
||||
MMO::Networking::NetworkingConnection::defaultSettings(clientSettings, clientCredConfig);
|
||||
auto clientRes = MMO::Networking::NetworkingConnection::connectTo(clientSettings, clientCredConfig, "127.0.0.1",
|
||||
2524);
|
||||
EXPECT_TRUE(clientRes.has_value());
|
||||
|
||||
auto client = clientRes.value();
|
||||
|
||||
std::println("Connecting to server...");
|
||||
std::fflush(stdout);
|
||||
EXPECT_FALSE(client->establishConnection().has_value());
|
||||
|
||||
while (!client->isConnected()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
if (std::chrono::high_resolution_clock::now() - start > timeout) {
|
||||
FAIL() << "Connection timed out";
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(client->isConnected());
|
||||
EXPECT_TRUE(serverConnection != nullptr);
|
||||
|
||||
|
||||
while (!serverConnection->isConnected()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
if (std::chrono::high_resolution_clock::now() - start > timeout) {
|
||||
FAIL() << "Connection timed out";
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(serverConnection->isConnected());
|
||||
|
||||
std::println("Sending data...");
|
||||
std::fflush(stdout);
|
||||
|
||||
std::shared_ptr<MMO::Networking::NetworkingDataLane> serverStream;
|
||||
|
||||
auto serverStreamCB = std::make_shared<std::function<void(std::shared_ptr<MMO::Networking::NetworkingDataLane>, std::shared_ptr<MMO::Networking::NetworkingConnection>)>>(
|
||||
[&serverStream](const auto& dataLane, const auto&) {
|
||||
serverStream = dataLane;
|
||||
});
|
||||
|
||||
serverConnection->addRemoteStreamCallbacks(serverStreamCB);
|
||||
|
||||
// auto streamRes = client->createStream();
|
||||
// EXPECT_TRUE(streamRes.has_value());
|
||||
// auto stream = streamRes.value();
|
||||
std::vector<uint8_t> testData = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
|
||||
auto sendResult = client->send(testData);
|
||||
EXPECT_FALSE(sendResult.has_value());
|
||||
|
||||
std::println("Waiting for data to be received...");
|
||||
std::fflush(stdout);
|
||||
while (!serverStream || !serverStream->hasData()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
if (std::chrono::high_resolution_clock::now() - start > timeout) {
|
||||
FAIL() << "No data received";
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(serverStream->hasData());
|
||||
|
||||
auto receivedData = serverStream->retrieveAndResetData();
|
||||
|
||||
EXPECT_THAT(receivedData, testing::ContainerEq(testData));
|
||||
|
||||
}
|
14
common/connection/tests/sanitizers.cpp
Normal file
14
common/connection/tests/sanitizers.cpp
Normal file
@ -0,0 +1,14 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
extern "C" {
|
||||
void __ubsan_on_report() {
|
||||
FAIL() << "Encountered an undefined behavior sanitizer error";
|
||||
}
|
||||
void __asan_on_error() {
|
||||
FAIL() << "Encountered an address sanitizer error";
|
||||
}
|
||||
void __tsan_on_report() {
|
||||
FAIL() << "Encountered a thread sanitizer error";
|
||||
}
|
||||
}
|
26
common/logging/CMakeLists.txt
Normal file
26
common/logging/CMakeLists.txt
Normal file
@ -0,0 +1,26 @@
|
||||
project(logging)
|
||||
|
||||
|
||||
set(${PROJECT_NAME}_src
|
||||
src/logging.cppm)
|
||||
# this is the "object library" target: compiles the sources only once
|
||||
add_library(${PROJECT_NAME}_lib STATIC ${${PROJECT_NAME}_src})
|
||||
target_include_directories(${PROJECT_NAME}_lib PUBLIC ./src)
|
||||
|
||||
find_package(spdlog CONFIG REQUIRED)
|
||||
target_link_libraries(${PROJECT_NAME}_lib PUBLIC spdlog::spdlog)
|
||||
# Setup tests
|
||||
enable_testing()
|
||||
|
||||
find_package(GTest CONFIG REQUIRED)
|
||||
include(GoogleTest)
|
||||
|
||||
file(GLOB_RECURSE tests_${PROJECT_NAME}_src CONFIGURE_DEPENDS tests/*.cppm tests/*/*.cppm)
|
||||
|
||||
add_executable(tests_${PROJECT_NAME} ${tests_${PROJECT_NAME}_src}
|
||||
tests/sanitizers.cpp)
|
||||
target_link_libraries(tests_${PROJECT_NAME} PRIVATE GTest::gtest GTest::gtest_main GTest::gmock GTest::gmock_main ${PROJECT_NAME}_lib)
|
||||
add_test(AllTestsIn${PROJECT_NAME} tests_${PROJECT_NAME})
|
||||
|
||||
gtest_discover_tests(tests_${PROJECT_NAME})
|
||||
|
105
common/logging/src/logging.cppm
Normal file
105
common/logging/src/logging.cppm
Normal file
@ -0,0 +1,105 @@
|
||||
export module MMO.Logging;
|
||||
|
||||
import <spdlog/spdlog.h>;
|
||||
import std;
|
||||
|
||||
namespace MMO::Logging {
|
||||
export class Logger {
|
||||
public:
|
||||
virtual ~Logger() = default;
|
||||
|
||||
|
||||
virtual void log(uint8_t level, const std::string& message) = 0;
|
||||
|
||||
template<class... _Types>
|
||||
void log(uint8_t level, const std::format_string<_Types...> _Fmt, _Types&&... _Args) {
|
||||
log(level, std::format(_Fmt, std::forward<_Types>(_Args)...));
|
||||
}
|
||||
};
|
||||
|
||||
export constexpr uint8_t LEVEL_OFF = 0;
|
||||
export constexpr uint8_t LEVEL_TRACE = 1;
|
||||
export constexpr uint8_t LEVEL_DEBUG = 2;
|
||||
export constexpr uint8_t LEVEL_INFO = 3;
|
||||
export constexpr uint8_t LEVEL_WARN = 4;
|
||||
export constexpr uint8_t LEVEL_ERROR = 5;
|
||||
export constexpr uint8_t LEVEL_FATAL = 6;
|
||||
|
||||
export template<class T>
|
||||
concept LoggerConcept = requires(T a, uint8_t level, const std::string& message) {
|
||||
{ a.log(level, message) } -> std::same_as<void>;
|
||||
};
|
||||
|
||||
export class NullLogger : public Logger {
|
||||
public:
|
||||
void log(const uint8_t level, const std::string& message) override { }
|
||||
};
|
||||
|
||||
export class ConsoleLogger : public Logger {
|
||||
public:
|
||||
uint8_t activeLevel = LEVEL_INFO;
|
||||
|
||||
explicit ConsoleLogger() = default;
|
||||
explicit ConsoleLogger(const uint8_t level) : activeLevel(level) { }
|
||||
|
||||
void log(const uint8_t level, const std::string& message) override {
|
||||
if (level < activeLevel) {
|
||||
return;
|
||||
}
|
||||
switch (level) {
|
||||
case LEVEL_TRACE:
|
||||
std::println("[TRACE] {}", message);
|
||||
break;
|
||||
case LEVEL_DEBUG:
|
||||
std::println("[DEBUG] {}", message);
|
||||
break;
|
||||
case LEVEL_INFO:
|
||||
std::println("[INFO] {}", message);
|
||||
break;
|
||||
case LEVEL_WARN:
|
||||
std::println("[WARN] {}", message);
|
||||
break;
|
||||
case LEVEL_ERROR:
|
||||
std::println("[ERROR] {}", message);
|
||||
break;
|
||||
case LEVEL_FATAL:
|
||||
std::println("[FATAL] {}", message);
|
||||
break;
|
||||
default:
|
||||
std::println("[UNKNOWN ({})] {}", level, message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export class SPDLogger : public Logger {
|
||||
public:
|
||||
void log(const uint8_t level, const std::string& message) override {
|
||||
switch (level) {
|
||||
case LEVEL_TRACE:
|
||||
spdlog::trace(message);
|
||||
break;
|
||||
case LEVEL_DEBUG:
|
||||
spdlog::debug(message);
|
||||
break;
|
||||
case LEVEL_INFO:
|
||||
spdlog::info(message);
|
||||
break;
|
||||
case LEVEL_WARN:
|
||||
spdlog::warn(message);
|
||||
break;
|
||||
case LEVEL_ERROR:
|
||||
spdlog::error(message);
|
||||
break;
|
||||
case LEVEL_FATAL:
|
||||
spdlog::critical(message);
|
||||
break;
|
||||
default:
|
||||
spdlog::error("Unknown log level: {}", level);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export std::shared_ptr<Logger> DEFAULT_LOGGER = std::make_shared<NullLogger>();
|
||||
}
|
10
common/logging/tests/logging_test.cppm
Normal file
10
common/logging/tests/logging_test.cppm
Normal file
@ -0,0 +1,10 @@
|
||||
module;
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
export module Packet_test;
|
||||
import MMO.Packet;
|
||||
import std;
|
14
common/logging/tests/sanitizers.cpp
Normal file
14
common/logging/tests/sanitizers.cpp
Normal file
@ -0,0 +1,14 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
extern "C" {
|
||||
void __ubsan_on_report() {
|
||||
FAIL() << "Encountered an undefined behavior sanitizer error";
|
||||
}
|
||||
void __asan_on_error() {
|
||||
FAIL() << "Encountered an address sanitizer error";
|
||||
}
|
||||
void __tsan_on_report() {
|
||||
FAIL() << "Encountered a thread sanitizer error";
|
||||
}
|
||||
}
|
26
common/packet/CMakeLists.txt
Normal file
26
common/packet/CMakeLists.txt
Normal file
@ -0,0 +1,26 @@
|
||||
project(packet)
|
||||
|
||||
|
||||
set(${PROJECT_NAME}_src
|
||||
src/Packet.cppm)
|
||||
# this is the "object library" target: compiles the sources only once
|
||||
add_library(${PROJECT_NAME}_lib STATIC ${${PROJECT_NAME}_src})
|
||||
target_include_directories(${PROJECT_NAME}_lib PUBLIC ./src)
|
||||
|
||||
find_package(cryptopp CONFIG REQUIRED)
|
||||
target_link_libraries(${PROJECT_NAME}_lib PRIVATE cryptopp::cryptopp)
|
||||
# Setup tests
|
||||
enable_testing()
|
||||
|
||||
find_package(GTest CONFIG REQUIRED)
|
||||
include(GoogleTest)
|
||||
|
||||
file(GLOB_RECURSE tests_${PROJECT_NAME}_src CONFIGURE_DEPENDS tests/*.cppm tests/*/*.cppm)
|
||||
|
||||
add_executable(tests_${PROJECT_NAME} ${tests_${PROJECT_NAME}_src}
|
||||
tests/sanitizers.cpp)
|
||||
target_link_libraries(tests_${PROJECT_NAME} PRIVATE GTest::gtest GTest::gtest_main GTest::gmock GTest::gmock_main ${PROJECT_NAME}_lib)
|
||||
add_test(AllTestsIn${PROJECT_NAME} tests_${PROJECT_NAME})
|
||||
|
||||
gtest_discover_tests(tests_${PROJECT_NAME})
|
||||
|
629
common/packet/src/Packet.cppm
Normal file
629
common/packet/src/Packet.cppm
Normal file
@ -0,0 +1,629 @@
|
||||
module;
|
||||
|
||||
#include <cryptopp/crc.h>
|
||||
|
||||
export module MMO.Packet;
|
||||
import std;
|
||||
import <cassert>;
|
||||
|
||||
namespace MMO::Networking {
|
||||
inline namespace v1 {
|
||||
template<std::integral T>
|
||||
constexpr T enforceLittleEndian(T value) {
|
||||
if constexpr (std::endian::native == std::endian::big) {
|
||||
return std::byteswap(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Container concept
|
||||
*
|
||||
* A type that meets the requirements of the Container concept must have an
|
||||
* emplace() function that matches a vector's emplace() function.
|
||||
* The type must also have a size() function that returns the number of elements in the container.
|
||||
* The type must also have a begin() function that returns an iterator to the beginning of the container.
|
||||
* The type must also have an end() function that returns an iterator to the end of the container.
|
||||
*/
|
||||
export template<typename T>
|
||||
concept Container = requires(T t) {
|
||||
{ t.emplace(t.begin(), std::declval<std::uint8_t>()) };
|
||||
{ std::size(t) } -> std::convertible_to<std::size_t>;
|
||||
{ std::begin(t) };
|
||||
{ std::end(t) };
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Encode a value
|
||||
*
|
||||
* Encodes the given value into a sequence of bytes using variable length
|
||||
* encoding, and stores the resulting bytes in the given container.
|
||||
*
|
||||
* @param value The value to encode
|
||||
* @param encoded The container to store the encoded bytes in
|
||||
* @param offset The offset to start encoding at (-1 to start at the end of the container)
|
||||
* @return The number of bytes added to the container
|
||||
*/
|
||||
|
||||
// export template<typename T, Container C>
|
||||
// size_t encodeValue(T value, C& encoded, std::int32_t offset = -1) = delete;
|
||||
|
||||
/**
|
||||
* @brief Variable length encode an integral value
|
||||
*
|
||||
* Encodes the given integral value into a sequence of bytes using
|
||||
* variable length encoding, and stores the resulting bytes in the given
|
||||
* container.
|
||||
*
|
||||
* @param value The value to encode
|
||||
* @param encoded The container to store the encoded bytes in
|
||||
* @return The number of bytes added to the container
|
||||
*/
|
||||
export template<std::integral T, Container C>
|
||||
size_t encodeValue(const T valueO, C& encoded, std::int32_t offset = -1) {
|
||||
T value = enforceLittleEndian(valueO);
|
||||
|
||||
auto insertPos = offset < 0 ? std::size(encoded) + offset + 1 : offset;
|
||||
|
||||
size_t count = 0;
|
||||
|
||||
T comparator = T(value < 0);
|
||||
|
||||
std::uint8_t data;
|
||||
|
||||
if constexpr (std::signed_integral<T>) {
|
||||
data = value & 0b00111111u;
|
||||
if (value < 0) {
|
||||
data |= 0b01000000u;
|
||||
}
|
||||
value >>= 6;
|
||||
} else {
|
||||
data = value & 0b01111111u;
|
||||
value >>= 7;
|
||||
}
|
||||
|
||||
while (value + comparator) {
|
||||
auto writeLocation = std::begin(encoded);
|
||||
std::advance(writeLocation, insertPos + count);
|
||||
encoded.emplace(writeLocation, data);
|
||||
|
||||
|
||||
++count;
|
||||
|
||||
data = value & 0b01111111u;
|
||||
value >>= 7;
|
||||
}
|
||||
|
||||
auto writeLocation = std::begin(encoded);
|
||||
std::advance(writeLocation, insertPos + count);
|
||||
++count;
|
||||
encoded.emplace(writeLocation, static_cast<uint8_t>(data | 0b10000000u));
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
export template<Container C>
|
||||
size_t encodeValue(const float value, C& encoded, std::int32_t offset = -1) {
|
||||
// std::numeric_limits<float>::digits is 24
|
||||
// std::numeric_limits<float>::max_exponent is 128 (we convert this to 127)
|
||||
// std::numeric_limits<float>::min_exponent is -125 (we convert this to -126)
|
||||
// `exp` is -128 (IEEE 754 this would be -1) is NaN when `mantisa` is 2
|
||||
// `exp` is -128 (IEEE 754 this would be -1) is Inf when `mantisa` is 0 or 1 (uses signbit in IEEE 754 but this would bloat the encoding since it would be part of `sign`)
|
||||
// The final exp needs to be adjusted by adding 1 and subtracting 24 to get the correct exponent
|
||||
size_t count = 0;
|
||||
|
||||
auto insertPos = offset < 0 ? std::size(encoded) + offset + 1 : offset;
|
||||
|
||||
if (std::isnan(value) || std::isinf(value)) {
|
||||
count += encodeValue(
|
||||
static_cast<std::int8_t>(-128), encoded,
|
||||
static_cast<std::int32_t>(insertPos + count)
|
||||
);
|
||||
}
|
||||
|
||||
if (std::isnan(value)) {
|
||||
count += encodeValue(
|
||||
2, encoded,
|
||||
static_cast<std::int32_t>(insertPos + count)
|
||||
);
|
||||
return count;
|
||||
}
|
||||
if (std::isinf(value)) {
|
||||
count += encodeValue(
|
||||
value < 0 ? 0 : 1,
|
||||
encoded,
|
||||
static_cast<std::int32_t>(insertPos + count)
|
||||
);
|
||||
return count;
|
||||
}
|
||||
|
||||
int exp = 0;
|
||||
const auto frac = std::frexp(value, &exp);
|
||||
|
||||
// This adjustment is necessary to get the correct exponent. `exp` will be between 128 to -125,
|
||||
// so this adjustment will force it back into being a valid int8_t
|
||||
|
||||
const int adjExp = exp - 1;
|
||||
count = encodeValue(
|
||||
static_cast<std::int8_t>(adjExp), encoded,
|
||||
static_cast<std::int32_t>(insertPos + count)
|
||||
);
|
||||
|
||||
// If the exponent is 0, then the mantissa is 0, so we can return early
|
||||
if (exp == 0) {
|
||||
return count;
|
||||
}
|
||||
|
||||
const auto sign = std::scalbn(frac, std::numeric_limits<float>::digits);
|
||||
|
||||
count += encodeValue(
|
||||
static_cast<std::int32_t>(sign), encoded,
|
||||
static_cast<std::int32_t>(insertPos + count)
|
||||
);
|
||||
return count;
|
||||
}
|
||||
|
||||
export template<Container C>
|
||||
size_t encodeValue(const double value, C& encoded, std::int32_t offset = -1) {
|
||||
// std::numeric_limits<double>::digits is 53
|
||||
// std::numeric_limits<double>::max_exponent is 1024
|
||||
// std::numeric_limits<double>::min_exponent is -1021
|
||||
// `exp` is -2048 (IEEE 754 this would be -1) is NaN when `mantisa` is 2
|
||||
// `exp` is -2048 (IEEE 754 this would be -1) is Inf when `mantisa` is 0 or 1 (uses signbit in IEEE 754 but this would bloat the encoding since it would be part of `sign`)
|
||||
// The final exp needs to be adjusted by adding 1 and subtracting 24 to get the correct exponent
|
||||
size_t count = 0;
|
||||
|
||||
auto insertPos = offset < 0 ? std::size(encoded) + offset + 1 : offset;
|
||||
|
||||
if (std::isnan(value) || std::isinf(value)) {
|
||||
count += encodeValue(
|
||||
static_cast<std::int16_t>(-2048), encoded,
|
||||
static_cast<std::int32_t>(insertPos + count)
|
||||
);
|
||||
}
|
||||
|
||||
if (std::isnan(value)) {
|
||||
count += encodeValue(
|
||||
static_cast<std::int64_t>(2), encoded,
|
||||
static_cast<std::int32_t>(insertPos + count)
|
||||
);
|
||||
return count;
|
||||
} else if (std::isinf(value)) {
|
||||
if (value < 0) {
|
||||
count += encodeValue(
|
||||
static_cast<std::int64_t>(0), encoded,
|
||||
static_cast<std::int32_t>(insertPos + count)
|
||||
);
|
||||
} else {
|
||||
count += encodeValue(
|
||||
static_cast<std::int64_t>(1), encoded,
|
||||
static_cast<std::int32_t>(insertPos + count)
|
||||
);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int exp = 0;
|
||||
const auto frac = std::frexp(value, &exp);
|
||||
|
||||
// This adjustment is necessary to get the correct exponent. `exp` will be between 128 to -125,
|
||||
// so this adjustment will force it back into being a valid int8_t
|
||||
|
||||
count = encodeValue(exp, encoded, static_cast<std::int32_t>(insertPos + count));
|
||||
|
||||
// If the exponent is 0, then the mantissa is 0, so we can return early
|
||||
if (exp == 0) {
|
||||
return count;
|
||||
}
|
||||
|
||||
const auto sign = std::scalbn(frac, std::numeric_limits<double>::digits);
|
||||
|
||||
count += encodeValue(
|
||||
static_cast<std::int64_t>(sign), encoded,
|
||||
static_cast<std::int32_t>(insertPos + count)
|
||||
);
|
||||
return count;
|
||||
}
|
||||
|
||||
export template<std::ranges::random_access_range Box, Container C>
|
||||
size_t encodeValue(const Box& value, C& encoded, std::int32_t offset = -1) {
|
||||
size_t count = encodeValue(static_cast<std::uint32_t>(std::size(value)), encoded, offset);
|
||||
|
||||
for (const auto& data : value) {
|
||||
auto adjustedOffset = offset < 0
|
||||
? std::min(offset + static_cast<std::int32_t>(count), -1)
|
||||
: offset + static_cast<std::int32_t>(count);
|
||||
count += encodeValue(data, encoded, adjustedOffset);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
export class ParseError : public std::runtime_error {
|
||||
public:
|
||||
static constexpr uint16_t NOT_ENOUGH_DATA = 1;
|
||||
static constexpr uint16_t INVALID_ENCODING = 2;
|
||||
const uint16_t reasonCode;
|
||||
|
||||
ParseError(const std::string& what, uint16_t reasonCode)
|
||||
: std::runtime_error(what), reasonCode(reasonCode) { }
|
||||
|
||||
ParseError(const char* what, uint16_t reasonCode)
|
||||
: std::runtime_error(what), reasonCode(reasonCode) { }
|
||||
};
|
||||
|
||||
// export template<typename T, std::integral Counter>
|
||||
// std::expected<T, ParseError> decodeValue(const std::span<std::uint8_t>& data, Counter& read) = delete;
|
||||
|
||||
|
||||
export template<std::integral T, std::integral Counter>
|
||||
std::expected<T, ParseError> decodeValue(const std::span<std::uint8_t>& data, Counter& read) {
|
||||
if (data.size() == 0) {
|
||||
return std::unexpected(ParseError("Not enough data to decode", ParseError::NOT_ENOUGH_DATA));
|
||||
}
|
||||
|
||||
++read;
|
||||
if (data[0] & 0b10000000) {
|
||||
if constexpr (std::signed_integral<T>) {
|
||||
if (data[0] & 0b01000000) {
|
||||
return T((~data[0] & 0b00111111) ^ -1);
|
||||
}
|
||||
}
|
||||
return T(data[0] & 0b01111111);
|
||||
}
|
||||
|
||||
T value = 0;
|
||||
std::uint8_t shift = 0;
|
||||
|
||||
if constexpr (std::signed_integral<T>) {
|
||||
value |= data[0] & 0b00111111;
|
||||
shift = 6;
|
||||
} else {
|
||||
value = data[0] & 0b01111111;
|
||||
shift = 7;
|
||||
}
|
||||
|
||||
const std::uint8_t maxShifts = sizeof(T) * 8;
|
||||
|
||||
for (size_t i = 1; i < data.size(); ++i) {
|
||||
++read;
|
||||
value |= (static_cast<T>(data[i]) & 0b01111111) << shift;
|
||||
|
||||
shift += 7;
|
||||
|
||||
if (data[i] & 0b10000000) {
|
||||
if constexpr (std::signed_integral<T>) {
|
||||
if (data[0] & 0b01000000) {
|
||||
// Fill the leftover bits with ones since it's a negative number
|
||||
value |= T(-1) << std::min<std::uint8_t>(shift, maxShifts - 1);
|
||||
}
|
||||
}
|
||||
return enforceLittleEndian(value);
|
||||
}
|
||||
|
||||
if (shift >= maxShifts) {
|
||||
return std::unexpected(ParseError("Value too large to decode", ParseError::INVALID_ENCODING));
|
||||
}
|
||||
}
|
||||
|
||||
return std::unexpected(ParseError("Not enough data to decode", ParseError::NOT_ENOUGH_DATA));
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
concept IsFloat = std::same_as<float, T>;
|
||||
|
||||
export template<IsFloat T, std::integral Counter>
|
||||
std::expected<T, ParseError> decodeValue(const std::span<std::uint8_t>& data, Counter& read) {
|
||||
if (data.size() < 1) {
|
||||
return std::unexpected(ParseError("Not enough data to decode", ParseError::NOT_ENOUGH_DATA));
|
||||
}
|
||||
|
||||
// 0b11111111 is -1
|
||||
if (data[0] == 0b11111111) {
|
||||
++read;
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
if (data.size() < 2) {
|
||||
return std::unexpected(ParseError("Not enough data to decode", ParseError::NOT_ENOUGH_DATA));
|
||||
}
|
||||
|
||||
// Even though the encoded size is int8_t, we need to use int16_t to handle the additional adjustments if it is valid
|
||||
// The encoding supports this cast/conversion
|
||||
std::expected<std::int16_t, ParseError> exp = decodeValue<std::int16_t>(data, read);
|
||||
if (!exp) {
|
||||
return std::unexpected(exp.error());
|
||||
}
|
||||
|
||||
if (*exp == -128) {
|
||||
std::expected<std::int32_t, ParseError> mantissa = decodeValue<std::int32_t>(data.subspan(read), read);
|
||||
if (!mantissa) {
|
||||
return std::unexpected(mantissa.error());
|
||||
}
|
||||
|
||||
if (*mantissa == 0) {
|
||||
return -std::numeric_limits<T>::infinity();
|
||||
} else if (*mantissa == 1) {
|
||||
return std::numeric_limits<T>::infinity();
|
||||
} else if (*mantissa == 2) {
|
||||
return std::numeric_limits<T>::quiet_NaN();
|
||||
} else {
|
||||
return std::unexpected(ParseError("Invalid NaN/Inf encoding", ParseError::INVALID_ENCODING));
|
||||
}
|
||||
} else if (*exp < -126 && *exp > 127) {
|
||||
return std::unexpected(ParseError("Invalid encoding", ParseError::INVALID_ENCODING));
|
||||
}
|
||||
|
||||
std::expected<std::int32_t, ParseError> mantissa = decodeValue<std::int32_t>(data.subspan(read), read);
|
||||
if (!mantissa) {
|
||||
return std::unexpected(mantissa.error());
|
||||
}
|
||||
|
||||
return std::scalbn(static_cast<T>(*mantissa), *exp + 1 - std::numeric_limits<T>::digits);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
concept IsDouble = std::same_as<double, T>;
|
||||
|
||||
export template<IsDouble T, std::integral Counter>
|
||||
std::expected<T, ParseError> decodeValue(const std::span<std::uint8_t>& data, Counter& read) {
|
||||
if (data.size() == 0) {
|
||||
return std::unexpected(ParseError("Not enough data to decode", ParseError::NOT_ENOUGH_DATA));
|
||||
}
|
||||
|
||||
// 128 is 0
|
||||
if (data[0] == static_cast<std::uint8_t>(0b10000000)) {
|
||||
++read;
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
if (data.size() < 2) {
|
||||
return std::unexpected(ParseError("Not enough data to decode", ParseError::NOT_ENOUGH_DATA));
|
||||
}
|
||||
|
||||
std::expected<std::int16_t, ParseError> exp = decodeValue<std::int16_t>(data, read);
|
||||
if (!exp) {
|
||||
return std::unexpected(exp.error());
|
||||
}
|
||||
|
||||
if (*exp == -2048) {
|
||||
std::expected<std::int64_t, ParseError> mantissa = decodeValue<std::int64_t>(data.subspan(read), read);
|
||||
if (!mantissa) {
|
||||
return std::unexpected(mantissa.error());
|
||||
}
|
||||
|
||||
if (*mantissa == 0) {
|
||||
return -std::numeric_limits<T>::infinity();
|
||||
} else if (*mantissa == 1) {
|
||||
return std::numeric_limits<T>::infinity();
|
||||
} else if (*mantissa == 2) {
|
||||
return std::numeric_limits<T>::quiet_NaN();
|
||||
} else {
|
||||
return std::unexpected(ParseError("Invalid NaN/Inf encoding", ParseError::INVALID_ENCODING));
|
||||
}
|
||||
} else if (*exp < -1021 && *exp > 1024) {
|
||||
return std::unexpected(ParseError("Invalid encoding", ParseError::INVALID_ENCODING));
|
||||
}
|
||||
|
||||
std::expected<std::int64_t, ParseError> mantissa = decodeValue<std::int64_t>(data.subspan(read), read);
|
||||
if (!mantissa) {
|
||||
return std::unexpected(mantissa.error());
|
||||
}
|
||||
|
||||
return std::scalbn(static_cast<T>(*mantissa), *exp - std::numeric_limits<T>::digits);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
concept IsCollection = requires(T t) {
|
||||
typename T::value_type;
|
||||
typename T::size_type;
|
||||
{ t.reserve(std::declval<typename T::size_type>()) };
|
||||
{ t.emplace_back(std::declval<typename T::value_type>()) };
|
||||
};
|
||||
|
||||
export template<IsCollection Box, std::integral Counter>
|
||||
std::expected<Box, ParseError> decodeValue(const std::span<std::uint8_t>& data, Counter& read) {
|
||||
if (data.size() == 0) {
|
||||
return std::unexpected(ParseError("Not enough data to decode", ParseError::NOT_ENOUGH_DATA));
|
||||
}
|
||||
|
||||
if (data[0] == static_cast<std::uint8_t>(0b10000000)) {
|
||||
return Box{ };
|
||||
}
|
||||
|
||||
std::expected<std::uint32_t, ParseError> size = decodeValue<std::uint32_t>(data, read);
|
||||
if (!size) {
|
||||
return std::unexpected(size.error());
|
||||
}
|
||||
|
||||
Box box;
|
||||
box.reserve(static_cast<typename Box::size_type>(*size));
|
||||
|
||||
for (std::uint32_t i = 0; i < *size; ++i) {
|
||||
std::expected<typename Box::value_type, ParseError> value = decodeValue<typename Box::value_type>(
|
||||
data.subspan(read), read
|
||||
);
|
||||
if (!value) {
|
||||
return std::unexpected(value.error());
|
||||
}
|
||||
|
||||
box.emplace_back(*value);
|
||||
}
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
export template<typename T>
|
||||
constexpr std::expected<T, ParseError> decodeValue(const std::span<std::uint8_t>& data) {
|
||||
int read = 0;
|
||||
return decodeValue<T>(data, read);
|
||||
}
|
||||
|
||||
// Aim for ~350 byte packets
|
||||
export class Packet {
|
||||
public:
|
||||
static constexpr std::uint32_t VERSION = 0;
|
||||
|
||||
private:
|
||||
std::uint32_t packetVersion = VERSION;
|
||||
std::uint32_t crcHash = 0;
|
||||
std::uint32_t size = 0;
|
||||
|
||||
std::vector<std::uint8_t> data = { };
|
||||
|
||||
std::int32_t readHead = 0;
|
||||
std::int32_t writeHead = 0;
|
||||
|
||||
public:
|
||||
Packet() = default;
|
||||
|
||||
|
||||
template<typename T>
|
||||
constexpr size_t write(const T& value) {
|
||||
const auto written = encodeValue(value, data, writeHead);
|
||||
size += static_cast<std::uint32_t>(written);
|
||||
writeHead += static_cast<std::int32_t>(written);
|
||||
return written;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
constexpr std::expected<T, ParseError> read() {
|
||||
return decodeValue<T>(std::span(data).subspan(readHead), readHead);;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
data.clear();
|
||||
readHead = 0;
|
||||
writeHead = 0;
|
||||
size = 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::span<std::uint8_t> bytes() {
|
||||
return std::span(data);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint32_t packetSize() const {
|
||||
return size;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint32_t crc() const {
|
||||
return crcHash;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::uint32_t version() const {
|
||||
return packetVersion;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::int32_t readPosition() const {
|
||||
return readHead;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::int32_t writePosition() const {
|
||||
return writeHead;
|
||||
}
|
||||
|
||||
void setReadPosition(std::int32_t position) {
|
||||
readHead = position;
|
||||
}
|
||||
|
||||
void setWritePosition(std::int32_t position) {
|
||||
writeHead = position;
|
||||
}
|
||||
|
||||
std::expected<void, ParseError> initializePacket(const std::span<std::uint8_t>& encodedData, int& read) {
|
||||
if (encodedData.size() < 6) {
|
||||
return std::unexpected(
|
||||
ParseError("Not enough data to initialize packet", ParseError::NOT_ENOUGH_DATA)
|
||||
);
|
||||
}
|
||||
|
||||
auto pVesion = decodeValue<std::uint8_t>(encodedData, read);
|
||||
if (!pVesion) {
|
||||
return std::unexpected(pVesion.error());
|
||||
}
|
||||
|
||||
|
||||
auto crc = decodeValue<std::uint32_t>(encodedData.subspan(read), read);
|
||||
if (!crc) {
|
||||
return std::unexpected(crc.error());
|
||||
}
|
||||
|
||||
|
||||
const auto crcHead = read;
|
||||
|
||||
auto pSize = decodeValue<std::uint32_t>(encodedData.subspan(read), read);
|
||||
if (!pSize) {
|
||||
return std::unexpected(pSize.error());
|
||||
}
|
||||
|
||||
|
||||
if (encodedData.size() < *pSize + read) {
|
||||
return std::unexpected(ParseError("Not enough data to initialize packet", ParseError::NOT_ENOUGH_DATA));
|
||||
}
|
||||
|
||||
const auto dataOffset = read - crcHead;
|
||||
|
||||
const auto tempCRC = *crc;
|
||||
|
||||
if (CryptoPP::CRC32C crc32; !crc32.VerifyDigest(
|
||||
reinterpret_cast<const CryptoPP::byte*>(&tempCRC),
|
||||
encodedData.subspan(crcHead).data(), *pSize + dataOffset
|
||||
)) {
|
||||
return std::unexpected(ParseError("Invalid CRC", ParseError::INVALID_ENCODING));
|
||||
}
|
||||
|
||||
packetVersion = *pVesion;
|
||||
crcHash = tempCRC;
|
||||
size = *pSize;
|
||||
|
||||
auto payload = encodedData.subspan(read, *pSize);
|
||||
data = std::vector(payload.begin(), payload.end());
|
||||
|
||||
assert(data.size() == size);
|
||||
|
||||
writeHead = static_cast<std::int32_t>(size);
|
||||
|
||||
return { };
|
||||
}
|
||||
|
||||
std::expected<void, ParseError> initializePacket(const std::span<std::uint8_t>& encodedData) {
|
||||
int read = 0;
|
||||
return initializePacket(encodedData, read);
|
||||
}
|
||||
|
||||
|
||||
static std::expected<Packet, ParseError> createPacket(const std::span<std::uint8_t>& encodedData, int& read) {
|
||||
Packet packet;
|
||||
if (auto result = packet.initializePacket(encodedData, read)) {
|
||||
return packet;
|
||||
} else {
|
||||
return std::unexpected(result.error());
|
||||
}
|
||||
}
|
||||
|
||||
static std::expected<Packet, ParseError> createPacket(const std::span<std::uint8_t>& encodedData) {
|
||||
int read = 0;
|
||||
return createPacket(encodedData, read);
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> finalizePacket() {
|
||||
assert(size == data.size());
|
||||
|
||||
std::vector<std::uint8_t> encodedData;
|
||||
encodeValue(size, encodedData);
|
||||
encodedData.insert(encodedData.end(), std::begin(data), std::end(data));
|
||||
|
||||
CryptoPP::CRC32C crc32;
|
||||
crc32.Update(encodedData.data(), encodedData.size());
|
||||
crc32.Final(reinterpret_cast<CryptoPP::byte*>(&crcHash));
|
||||
|
||||
encodeValue(crcHash, encodedData, 0);
|
||||
encodeValue(packetVersion, encodedData, 0);
|
||||
|
||||
|
||||
return encodedData;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
991
common/packet/tests/Packet_test.cppm
Normal file
991
common/packet/tests/Packet_test.cppm
Normal file
@ -0,0 +1,991 @@
|
||||
module;
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
export module Packet_test;
|
||||
import MMO.Packet;
|
||||
import std;
|
||||
|
||||
// Encodes
|
||||
TEST(DataEncoding, uint8Min) {
|
||||
std::vector<uint8_t> encoded;
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<uint8_t>::min(), encoded), 1);
|
||||
EXPECT_EQ(encoded, std::vector<uint8_t>{0b10000000});
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, uint8Max) {
|
||||
std::vector<uint8_t> encoded;
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<uint8_t>::max(), encoded), 2);
|
||||
EXPECT_EQ(encoded, std::vector<uint8_t>({ 0b01111111, 0b10000001}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, int8Min) {
|
||||
std::vector<uint8_t> encoded;
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<int8_t>::min(), encoded), 2);
|
||||
EXPECT_EQ(encoded, std::vector<uint8_t>({ 0b01000000, 0b11111110}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, int8Max) {
|
||||
std::vector<uint8_t> encoded;
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<int8_t>::max(), encoded), 2);
|
||||
EXPECT_EQ(encoded, std::vector<uint8_t>({ 0b00111111,0b10000001,}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, uint16Min) {
|
||||
std::vector<uint8_t> encoded;
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<uint16_t>::min(), encoded), 1);
|
||||
EXPECT_EQ(encoded, std::vector<uint8_t>{0b10000000});
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, uint16Max) {
|
||||
std::vector<uint8_t> encoded;
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<uint16_t>::max(), encoded), 3);
|
||||
EXPECT_EQ(encoded, std::vector<uint8_t>({ 0b01111111, 0b01111111, 0b10000011}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, int16Min) {
|
||||
std::vector<uint8_t> encoded;
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<int16_t>::min(), encoded), 3);
|
||||
EXPECT_EQ(encoded, std::vector<uint8_t>({ 0b01000000, 0, 0b11111100}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, int16Max) {
|
||||
std::vector<uint8_t> encoded;
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<int16_t>::max(), encoded), 3);
|
||||
EXPECT_EQ(encoded, std::vector<uint8_t>({ 0b00111111, 0b01111111, 0b10000011}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, uint32Min) {
|
||||
std::vector<uint8_t> encoded;
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<uint32_t>::min(), encoded), 1);
|
||||
EXPECT_EQ(encoded, std::vector<uint8_t>{0b10000000});
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, uint32Max) {
|
||||
std::vector<uint8_t> encoded;
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<uint32_t>::max(), encoded), 5);
|
||||
EXPECT_EQ(encoded, std::vector<uint8_t>({ 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b10001111}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, int32Min) {
|
||||
std::vector<uint8_t> encoded;
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<int32_t>::min(), encoded), 5);
|
||||
EXPECT_EQ(encoded, std::vector<uint8_t>({ 0b01000000, 0, 0, 0, 0b11110000}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, int32Max) {
|
||||
std::vector<uint8_t> encoded;
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<int32_t>::max(), encoded), 5);
|
||||
EXPECT_EQ(encoded, std::vector<uint8_t>({ 0b00111111, 0b01111111, 0b01111111, 0b01111111, 0b10001111}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, uint64Min) {
|
||||
std::vector<uint8_t> encoded;
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<uint64_t>::min(), encoded), 1);
|
||||
EXPECT_EQ(encoded, std::vector<uint8_t>{0b10000000});
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, uint64Max) {
|
||||
std::vector<uint8_t> encoded;
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<uint64_t>::max(), encoded), 10);
|
||||
EXPECT_EQ(encoded,
|
||||
std::vector<uint8_t>({ 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111,
|
||||
0b01111111, 0b01111111, 0b10000001}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, int64Min) {
|
||||
std::vector<uint8_t> encoded;
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<int64_t>::min(), encoded), 10);
|
||||
EXPECT_EQ(encoded,
|
||||
std::vector<uint8_t>({0b01000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000, 0b11111110}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, int64Max) {
|
||||
std::vector<uint8_t> encoded;
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<int64_t>::max(), encoded), 10);
|
||||
EXPECT_EQ(encoded,
|
||||
std::vector<uint8_t>({ 0b00111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111,
|
||||
0b01111111,
|
||||
0b01111111, 0b10000001}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEncoding, float0) {
|
||||
const auto value = 0.0f;
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 1);
|
||||
EXPECT_EQ(encoded,
|
||||
std::vector<uint8_t>({ 0b11111111}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, floatMin) {
|
||||
const auto value = std::numeric_limits<float>::min();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 6);
|
||||
EXPECT_EQ(encoded,
|
||||
std::vector<uint8_t>({ 0b01000010, 0b11111110, 0, 0, 0, 0b10001000}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, floatLowest) {
|
||||
const auto value = std::numeric_limits<float>::lowest();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 6);
|
||||
EXPECT_EQ(encoded,
|
||||
std::vector<uint8_t>({ 0b00111111, 0b10000001, 0b01000001, 0 ,0, 0b11110000}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, floatMax) {
|
||||
const auto value = std::numeric_limits<float>::max();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 6);
|
||||
EXPECT_EQ(encoded,
|
||||
std::vector<uint8_t>({ 0b00111111, 0b10000001, 0b00111111, 0b01111111, 0b01111111, 0b10001111}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEncoding, floatNan) {
|
||||
const auto value = std::numeric_limits<float>::quiet_NaN();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 3);
|
||||
EXPECT_EQ(encoded,
|
||||
std::vector<uint8_t>({ 0b01000000, 0b11111110, 0b10000010}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEncoding, floatInfPositive) {
|
||||
const auto value = std::numeric_limits<float>::infinity();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 3);
|
||||
EXPECT_EQ(encoded,
|
||||
std::vector<uint8_t>({ 0b01000000, 0b11111110, 0b10000001}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEncoding, floatInfNegative) {
|
||||
const auto value = -std::numeric_limits<float>::infinity();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 3);
|
||||
EXPECT_EQ(encoded,
|
||||
std::vector<uint8_t>({ 0b01000000, 0b11111110, 0b10000000}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEncoding, double0) {
|
||||
const double value = 0.0;
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 1);
|
||||
EXPECT_EQ(encoded,
|
||||
std::vector<uint8_t>({ 0b10000000 }));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, doubleMin) {
|
||||
const auto value = std::numeric_limits<double>::min();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 10);
|
||||
EXPECT_EQ(encoded,
|
||||
std::vector<uint8_t>({ 0b01000011, 0b11110000, 0, 0, 0, 0,0,0,0,0b10010000}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, doubleLowest) {
|
||||
const auto value = std::numeric_limits<double>::lowest();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 10);
|
||||
EXPECT_EQ(encoded,
|
||||
std::vector<uint8_t>({ 0, 0b10010000, 0b01000001,0, 0 ,0,0,0,0, 0b11100000}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
TEST(DataEncoding, doubleMax) {
|
||||
const auto value = std::numeric_limits<double>::max();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 10);
|
||||
EXPECT_EQ(encoded,
|
||||
std::vector<uint8_t>({ 0, 0b10010000, 0b00111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111,
|
||||
0b01111111, 0b01111111, 0b10011111}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEncoding, doubleNan) {
|
||||
const auto value = std::numeric_limits<double>::quiet_NaN();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 3);
|
||||
EXPECT_EQ(encoded,
|
||||
std::vector<uint8_t>({ 0b01000000, 0b11100000, 0b10000010}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEncoding, doubleInfPositive) {
|
||||
const auto value = std::numeric_limits<double>::infinity();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 3);
|
||||
EXPECT_EQ(encoded,
|
||||
std::vector<uint8_t>({ 0b01000000, 0b11100000, 0b10000001}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEncoding, doubleInfNegative) {
|
||||
const auto value = -std::numeric_limits<double>::infinity();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 3);
|
||||
EXPECT_EQ(encoded,
|
||||
std::vector<uint8_t>({ 0b01000000, 0b11100000, 0b10000000}));
|
||||
encoded.clear();
|
||||
}
|
||||
|
||||
|
||||
// Decodes
|
||||
TEST(DataDecoding, uint8Min) {
|
||||
std::vector<uint8_t> encoded = { 0b10000000 };
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<uint8_t>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<uint8_t>::min());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, uint8Max) {
|
||||
std::vector<uint8_t> encoded = { 0b01111111, 0b10000001 };
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<uint8_t>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<uint8_t>::max());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, int8Min) {
|
||||
std::vector<uint8_t> encoded = { 0b01000000, 0b11111110 };
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<int8_t>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<int8_t>::min());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, int8Max) {
|
||||
std::vector<uint8_t> encoded = { 0b00111111, 0b10000001, };
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<int8_t>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<int8_t>::max());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, uint16Min) {
|
||||
std::vector<uint8_t> encoded = { 0b10000000 };
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<uint16_t>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<uint16_t>::min());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, uint16Max) {
|
||||
std::vector<uint8_t> encoded = { 0b01111111, 0b01111111, 0b10000011 };
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<uint16_t>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<uint16_t>::max());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, int16Min) {
|
||||
std::vector<uint8_t> encoded = { 0b01000000, 0, 0b11111100 };
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<int16_t>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<int16_t>::min());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, int16Max) {
|
||||
std::vector<uint8_t> encoded = { 0b00111111, 0b01111111, 0b10000011 };
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<int16_t>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<int16_t>::max());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, uint32Min) {
|
||||
std::vector<uint8_t> encoded = { 0b10000000 };
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<uint32_t>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<uint32_t>::min());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, uint32Max) {
|
||||
std::vector<uint8_t> encoded = { 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b10001111 };
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<uint32_t>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<uint32_t>::max());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, int32Min) {
|
||||
std::vector<uint8_t> encoded = { 0b01000000, 0, 0, 0, 0b11110000 };
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<int32_t, int>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<int32_t>::min());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, int32Max) {
|
||||
std::vector<uint8_t> encoded = { 0b00111111, 0b01111111, 0b01111111, 0b01111111, 0b10001111 };
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<int32_t, int>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<int32_t>::max());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, uint64Min) {
|
||||
std::vector<uint8_t> encoded = { 0b10000000 };
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<uint64_t>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<uint64_t>::min());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, uint64Max) {
|
||||
std::vector<uint8_t> encoded = {
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b10000001
|
||||
};
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<uint64_t>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<uint64_t>::max());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, int64Min) {
|
||||
std::vector<uint8_t> encoded = {
|
||||
0b01000000,
|
||||
0b00000000,
|
||||
0b00000000,
|
||||
0b00000000,
|
||||
0b00000000,
|
||||
0b00000000,
|
||||
0b00000000,
|
||||
0b00000000,
|
||||
0b00000000,
|
||||
0b11111110
|
||||
};
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<int64_t>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<int64_t>::min());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, int64Max) {
|
||||
std::vector<uint8_t> encoded = {
|
||||
0b00111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b10000001
|
||||
};
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<int64_t>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<int64_t>::max());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, float0) {
|
||||
std::vector<uint8_t> encoded = { 0b11111111 };
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<float>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), 0.0f);
|
||||
}
|
||||
|
||||
TEST(DataDecoding, floatMin) {
|
||||
std::vector<uint8_t> encoded = {
|
||||
0b01000010,
|
||||
0b11111110,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0b10001000
|
||||
};
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<float>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<float>::min());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, floatLowest) {
|
||||
std::vector<uint8_t> encoded = {
|
||||
0b00111111,
|
||||
0b10000001,
|
||||
0b01000001,
|
||||
0,
|
||||
0,
|
||||
0b11110000
|
||||
};
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<float>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<float>::lowest());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, floatMax) {
|
||||
std::vector<uint8_t> encoded = {
|
||||
0b00111111,
|
||||
0b10000001,
|
||||
0b00111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b10001111
|
||||
};
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<float>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<float>::max());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, floatNan) {
|
||||
std::vector<uint8_t> encoded = {
|
||||
0b01000000,
|
||||
0b11111110,
|
||||
0b10000010
|
||||
};
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<float>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_TRUE(std::isnan(value.value()));
|
||||
}
|
||||
|
||||
TEST(DataDecoding, floatInfPositive) {
|
||||
std::vector<uint8_t> encoded = {
|
||||
0b01000000,
|
||||
0b11111110,
|
||||
0b10000001
|
||||
};
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<float>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<float>::infinity());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, floatInfNegative) {
|
||||
std::vector<uint8_t> encoded = {
|
||||
0b01000000,
|
||||
0b11111110,
|
||||
0b10000000
|
||||
};
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<float>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), -std::numeric_limits<float>::infinity());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, double0) {
|
||||
std::vector<uint8_t> encoded = { 0b10000000 };
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<double>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), 0.0f);
|
||||
}
|
||||
|
||||
TEST(DataDecoding, doubleMin) {
|
||||
std::vector<uint8_t> encoded = { 0b01000011, 0b11110000, 0, 0, 0, 0, 0, 0, 0, 0b10010000 };
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<double>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<double>::min());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, doubleLowest) {
|
||||
std::vector<uint8_t> encoded = { 0, 0b10010000, 0b01000001, 0, 0, 0, 0, 0, 0, 0b11100000 };
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<double>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<double>::lowest());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, doubleMax) {
|
||||
std::vector<uint8_t> encoded = {
|
||||
0,
|
||||
0b10010000,
|
||||
0b00111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b01111111,
|
||||
0b10011111
|
||||
};
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<double>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<double>::max());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, doubleNan) {
|
||||
std::vector<uint8_t> encoded = {
|
||||
0b01000000,
|
||||
0b11100000,
|
||||
0b10000010
|
||||
};
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<double>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_TRUE(std::isnan(value.value()));
|
||||
}
|
||||
|
||||
TEST(DataDecoding, doubleInfPositive) {
|
||||
std::vector<uint8_t> encoded = {
|
||||
0b01000000,
|
||||
0b11100000,
|
||||
0b10000001
|
||||
};
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<double>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), std::numeric_limits<double>::infinity());
|
||||
}
|
||||
|
||||
TEST(DataDecoding, doubleInfNegative) {
|
||||
std::vector<uint8_t> encoded = {
|
||||
0b01000000,
|
||||
0b11100000,
|
||||
0b10000000
|
||||
};
|
||||
int read = 0;
|
||||
auto value = MMO::Networking::decodeValue<double>(encoded, read);
|
||||
EXPECT_TRUE(value.has_value());
|
||||
EXPECT_EQ(read, encoded.size());
|
||||
EXPECT_EQ(value.value(), -std::numeric_limits<double>::infinity());
|
||||
}
|
||||
|
||||
template<std::integral T>
|
||||
T randomValue() {
|
||||
std::random_device rd;
|
||||
std::mt19937_64 gen(rd());
|
||||
|
||||
std::uniform_int_distribution<std::conditional_t<std::signed_integral<T>, int64_t, uint64_t>> dis(
|
||||
std::numeric_limits<T>::min(), std::numeric_limits<T>::max());
|
||||
return static_cast<T>(dis(gen));
|
||||
}
|
||||
|
||||
const auto valueAttempts = 100;
|
||||
|
||||
TEST(DataEnDecoding, uint8) {
|
||||
for (int i = 0; i < valueAttempts; ++i) {
|
||||
const auto value = randomValue<uint8_t>();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<uint8_t>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DataEnDecoding, int8) {
|
||||
for (int i = 0; i < valueAttempts; ++i) {
|
||||
const auto value = randomValue<int8_t>();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<int8_t>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DataEnDecoding, uint16) {
|
||||
for (int i = 0; i < valueAttempts; ++i) {
|
||||
const auto value = randomValue<uint16_t>();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<uint16_t>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DataEnDecoding, int16) {
|
||||
for (int i = 0; i < valueAttempts; ++i) {
|
||||
const auto value = randomValue<int16_t>();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<int16_t>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DataEnDecoding, uint32) {
|
||||
for (int i = 0; i < valueAttempts; ++i) {
|
||||
const auto value = randomValue<uint32_t>();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<uint32_t>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DataEnDecoding, int32) {
|
||||
for (int i = 0; i < valueAttempts; ++i) {
|
||||
const auto value = randomValue<int32_t>();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<int32_t>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DataEnDecoding, uint64) {
|
||||
for (int i = 0; i < valueAttempts; ++i) {
|
||||
const auto value = randomValue<uint64_t>();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<uint64_t>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DataEnDecoding, int64) {
|
||||
for (int i = 0; i < valueAttempts; ++i) {
|
||||
const auto value = randomValue<int64_t>();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<int64_t>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DataEnDecoding, floats) {
|
||||
for (int i = 0; i < valueAttempts; ++i) {
|
||||
const auto value = static_cast<float>(randomValue<int64_t>()) / (static_cast<float>(randomValue<uint32_t>()) +
|
||||
1);
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<float>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEnDecoding, float0) {
|
||||
const auto value = 0.0f;
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<float>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEnDecoding, floatMin) {
|
||||
const auto value = std::numeric_limits<float>::min();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<float>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEnDecoding, floatLowest) {
|
||||
const auto value = std::numeric_limits<float>::lowest();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<float>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
|
||||
TEST(DataEnDecoding, floatMax) {
|
||||
const auto value = std::numeric_limits<float>::min();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<float>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEnDecoding, floatNan) {
|
||||
const auto value = std::numeric_limits<float>::quiet_NaN();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<float>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_TRUE(std::isnan(decoded.value()));
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEnDecoding, floatInfPositive) {
|
||||
const auto value = std::numeric_limits<float>::infinity();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<float>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEnDecoding, floatInfNegative) {
|
||||
const auto value = -std::numeric_limits<float>::infinity();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<float>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
|
||||
TEST(DataEnDecoding, doubles) {
|
||||
for (int i = 0; i < valueAttempts; ++i) {
|
||||
const auto value = static_cast<double>(randomValue<int64_t>()) / (static_cast<double>(randomValue<uint32_t>()) +
|
||||
1);
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<double>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEnDecoding, double0) {
|
||||
const auto value = 0.0;
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<double>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEnDecoding, doubleMin) {
|
||||
const auto value = std::numeric_limits<double>::min();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<double>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEnDecoding, doubleLowest) {
|
||||
const auto value = std::numeric_limits<double>::lowest();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<double>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
|
||||
TEST(DataEnDecoding, doubleMax) {
|
||||
const auto value = std::numeric_limits<double>::min();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<double>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEnDecoding, doubleNan) {
|
||||
const auto value = std::numeric_limits<double>::quiet_NaN();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<double>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_TRUE(std::isnan(decoded.value()));
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEnDecoding, doubleInfPositive) {
|
||||
const auto value = std::numeric_limits<double>::infinity();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<double>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEnDecoding, doubleInfNegative) {
|
||||
const auto value = -std::numeric_limits<double>::infinity();
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<double>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
|
||||
|
||||
TEST(DataEnDecoding, arrayOfValue) {
|
||||
const std::vector<int32_t> value{
|
||||
randomValue<int32_t>(),
|
||||
randomValue<int32_t>(),
|
||||
randomValue<int32_t>(),
|
||||
randomValue<int32_t>(),
|
||||
randomValue<int32_t>(),
|
||||
randomValue<int32_t>(),
|
||||
};
|
||||
std::vector<uint8_t> encoded;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
MMO::Networking::encodeValue(value, encoded);
|
||||
auto decoded = MMO::Networking::decodeValue<std::vector<int32_t>>(encoded);
|
||||
EXPECT_TRUE(decoded.has_value());
|
||||
EXPECT_EQ(decoded.value(), value);
|
||||
}
|
||||
|
||||
TEST(MultiWriteTest, outOfOrderIsInOrder) {
|
||||
const std::vector<int32_t> value{
|
||||
randomValue<int32_t>(),
|
||||
randomValue<int32_t>(),
|
||||
randomValue<int32_t>(),
|
||||
randomValue<int32_t>(),
|
||||
randomValue<int32_t>(),
|
||||
randomValue<int32_t>(),
|
||||
};
|
||||
std::vector<uint8_t> encodedInOrder;
|
||||
std::vector<uint8_t> encodedOutOrder;
|
||||
// Length is variable so we can't predict it (easily, there is math for it but I've lost it)
|
||||
for (const auto& v : value) {
|
||||
MMO::Networking::encodeValue(v, encodedInOrder);
|
||||
}
|
||||
for (int i = static_cast<int>(value.size()) - 1; i >= 0; --i) {
|
||||
MMO::Networking::encodeValue(value[i], encodedOutOrder, 0);
|
||||
}
|
||||
EXPECT_EQ(encodedInOrder, encodedOutOrder);
|
||||
}
|
||||
|
||||
|
||||
TEST(Packet, CreateFrom) {
|
||||
MMO::Networking::Packet packet;
|
||||
|
||||
packet.write<uint32_t>(0xDEADBEEF);
|
||||
packet.write<int32_t>(-1);
|
||||
|
||||
|
||||
auto packetData = packet.finalizePacket();
|
||||
|
||||
auto packet2Exp = MMO::Networking::Packet::createPacket(packetData);
|
||||
|
||||
EXPECT_TRUE(packet2Exp.has_value());
|
||||
auto packet2 = packet2Exp.value();
|
||||
{
|
||||
auto readValue = packet2.read<uint32_t>();
|
||||
EXPECT_TRUE(readValue.has_value());
|
||||
EXPECT_EQ(*readValue, 0xDEADBEEF);
|
||||
}
|
||||
{
|
||||
auto readValue = packet2.read<int32_t>();
|
||||
EXPECT_TRUE(readValue.has_value());
|
||||
EXPECT_EQ(*readValue, -1);
|
||||
}
|
||||
}
|
14
common/packet/tests/sanitizers.cpp
Normal file
14
common/packet/tests/sanitizers.cpp
Normal file
@ -0,0 +1,14 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
extern "C" {
|
||||
void __ubsan_on_report() {
|
||||
FAIL() << "Encountered an undefined behavior sanitizer error";
|
||||
}
|
||||
void __asan_on_error() {
|
||||
FAIL() << "Encountered an address sanitizer error";
|
||||
}
|
||||
void __tsan_on_report() {
|
||||
FAIL() << "Encountered a thread sanitizer error";
|
||||
}
|
||||
}
|
32
vcpkg.json
Normal file
32
vcpkg.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "mmo",
|
||||
"version": "1.0.0",
|
||||
"description": "MMOServers",
|
||||
"builtin-baseline": "4a9af5e5efcb24d0c8fcdf82dacf3b2b780b99f8",
|
||||
"dependencies": [
|
||||
{
|
||||
"name": "msquic",
|
||||
"features": [
|
||||
"0-rtt"
|
||||
]
|
||||
},
|
||||
"gtest",
|
||||
{
|
||||
"name": "openssl",
|
||||
"features": [
|
||||
"tools"
|
||||
]
|
||||
},
|
||||
"cryptopp",
|
||||
"cli11",
|
||||
{
|
||||
"name": "redis-plus-plus",
|
||||
"features": [
|
||||
"cxx17",
|
||||
"tls"
|
||||
]
|
||||
},
|
||||
"yyjson",
|
||||
"spdlog"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user