mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-06-13 14:38:43 +00:00
removed unsafe
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 18
|
# Visual Studio Version 18
|
||||||
VisualStudioVersion = 18.4.11612.150
|
VisualStudioVersion = 18.4.11612.150
|
||||||
@@ -92,96 +93,282 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ConcurrentAttachSweep", "Co
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.ConcurrentAttachSweep", "Tests\Distribution\ConcurrentAttachSweep\Esiur.Tests.ConcurrentAttachSweep.csproj", "{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.ConcurrentAttachSweep", "Tests\Distribution\ConcurrentAttachSweep\Esiur.Tests.ConcurrentAttachSweep.csproj", "{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.Unit", "Tests\Unit\Esiur.Tests.Unit.csproj", "{D1B99C5A-82F7-459D-B56D-F8FD096D3854}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{E87F60C9-F167-3F03-A4BD-43DBB1C76BDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{E87F60C9-F167-3F03-A4BD-43DBB1C76BDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{E87F60C9-F167-3F03-A4BD-43DBB1C76BDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{E87F60C9-F167-3F03-A4BD-43DBB1C76BDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E87F60C9-F167-3F03-A4BD-43DBB1C76BDD}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{E87F60C9-F167-3F03-A4BD-43DBB1C76BDD}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{E87F60C9-F167-3F03-A4BD-43DBB1C76BDD}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{E87F60C9-F167-3F03-A4BD-43DBB1C76BDD}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{E87F60C9-F167-3F03-A4BD-43DBB1C76BDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{E87F60C9-F167-3F03-A4BD-43DBB1C76BDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{E87F60C9-F167-3F03-A4BD-43DBB1C76BDD}.Release|Any CPU.Build.0 = Release|Any CPU
|
{E87F60C9-F167-3F03-A4BD-43DBB1C76BDD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E87F60C9-F167-3F03-A4BD-43DBB1C76BDD}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{E87F60C9-F167-3F03-A4BD-43DBB1C76BDD}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{E87F60C9-F167-3F03-A4BD-43DBB1C76BDD}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{E87F60C9-F167-3F03-A4BD-43DBB1C76BDD}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{9BB3B5A1-CD1F-EEB6-89D5-F3D3766E740E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{9BB3B5A1-CD1F-EEB6-89D5-F3D3766E740E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{9BB3B5A1-CD1F-EEB6-89D5-F3D3766E740E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{9BB3B5A1-CD1F-EEB6-89D5-F3D3766E740E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{9BB3B5A1-CD1F-EEB6-89D5-F3D3766E740E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{9BB3B5A1-CD1F-EEB6-89D5-F3D3766E740E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{9BB3B5A1-CD1F-EEB6-89D5-F3D3766E740E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{9BB3B5A1-CD1F-EEB6-89D5-F3D3766E740E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{9BB3B5A1-CD1F-EEB6-89D5-F3D3766E740E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{9BB3B5A1-CD1F-EEB6-89D5-F3D3766E740E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{9BB3B5A1-CD1F-EEB6-89D5-F3D3766E740E}.Release|Any CPU.Build.0 = Release|Any CPU
|
{9BB3B5A1-CD1F-EEB6-89D5-F3D3766E740E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{9BB3B5A1-CD1F-EEB6-89D5-F3D3766E740E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{9BB3B5A1-CD1F-EEB6-89D5-F3D3766E740E}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{9BB3B5A1-CD1F-EEB6-89D5-F3D3766E740E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{9BB3B5A1-CD1F-EEB6-89D5-F3D3766E740E}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{0255BB42-2742-59C6-F18D-42C6A7C0F017}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{0255BB42-2742-59C6-F18D-42C6A7C0F017}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{0255BB42-2742-59C6-F18D-42C6A7C0F017}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{0255BB42-2742-59C6-F18D-42C6A7C0F017}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{0255BB42-2742-59C6-F18D-42C6A7C0F017}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{0255BB42-2742-59C6-F18D-42C6A7C0F017}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{0255BB42-2742-59C6-F18D-42C6A7C0F017}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{0255BB42-2742-59C6-F18D-42C6A7C0F017}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{0255BB42-2742-59C6-F18D-42C6A7C0F017}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{0255BB42-2742-59C6-F18D-42C6A7C0F017}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{0255BB42-2742-59C6-F18D-42C6A7C0F017}.Release|Any CPU.Build.0 = Release|Any CPU
|
{0255BB42-2742-59C6-F18D-42C6A7C0F017}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{0255BB42-2742-59C6-F18D-42C6A7C0F017}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{0255BB42-2742-59C6-F18D-42C6A7C0F017}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{0255BB42-2742-59C6-F18D-42C6A7C0F017}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{0255BB42-2742-59C6-F18D-42C6A7C0F017}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{93B71253-8B62-38F4-7B0F-EFEE2619FF2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{93B71253-8B62-38F4-7B0F-EFEE2619FF2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{93B71253-8B62-38F4-7B0F-EFEE2619FF2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{93B71253-8B62-38F4-7B0F-EFEE2619FF2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{93B71253-8B62-38F4-7B0F-EFEE2619FF2F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{93B71253-8B62-38F4-7B0F-EFEE2619FF2F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{93B71253-8B62-38F4-7B0F-EFEE2619FF2F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{93B71253-8B62-38F4-7B0F-EFEE2619FF2F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{93B71253-8B62-38F4-7B0F-EFEE2619FF2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{93B71253-8B62-38F4-7B0F-EFEE2619FF2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{93B71253-8B62-38F4-7B0F-EFEE2619FF2F}.Release|Any CPU.Build.0 = Release|Any CPU
|
{93B71253-8B62-38F4-7B0F-EFEE2619FF2F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{93B71253-8B62-38F4-7B0F-EFEE2619FF2F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{93B71253-8B62-38F4-7B0F-EFEE2619FF2F}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{93B71253-8B62-38F4-7B0F-EFEE2619FF2F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{93B71253-8B62-38F4-7B0F-EFEE2619FF2F}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{A3AACA8A-D545-BF09-EE00-73485A89B84F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{A3AACA8A-D545-BF09-EE00-73485A89B84F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A3AACA8A-D545-BF09-EE00-73485A89B84F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A3AACA8A-D545-BF09-EE00-73485A89B84F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A3AACA8A-D545-BF09-EE00-73485A89B84F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{A3AACA8A-D545-BF09-EE00-73485A89B84F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{A3AACA8A-D545-BF09-EE00-73485A89B84F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{A3AACA8A-D545-BF09-EE00-73485A89B84F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{A3AACA8A-D545-BF09-EE00-73485A89B84F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{A3AACA8A-D545-BF09-EE00-73485A89B84F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{A3AACA8A-D545-BF09-EE00-73485A89B84F}.Release|Any CPU.Build.0 = Release|Any CPU
|
{A3AACA8A-D545-BF09-EE00-73485A89B84F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A3AACA8A-D545-BF09-EE00-73485A89B84F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{A3AACA8A-D545-BF09-EE00-73485A89B84F}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{A3AACA8A-D545-BF09-EE00-73485A89B84F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{A3AACA8A-D545-BF09-EE00-73485A89B84F}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{B2F79E56-20CC-8FC3-23A1-EAD37DC6C987}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{B2F79E56-20CC-8FC3-23A1-EAD37DC6C987}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{B2F79E56-20CC-8FC3-23A1-EAD37DC6C987}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{B2F79E56-20CC-8FC3-23A1-EAD37DC6C987}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B2F79E56-20CC-8FC3-23A1-EAD37DC6C987}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{B2F79E56-20CC-8FC3-23A1-EAD37DC6C987}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{B2F79E56-20CC-8FC3-23A1-EAD37DC6C987}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{B2F79E56-20CC-8FC3-23A1-EAD37DC6C987}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{B2F79E56-20CC-8FC3-23A1-EAD37DC6C987}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{B2F79E56-20CC-8FC3-23A1-EAD37DC6C987}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{B2F79E56-20CC-8FC3-23A1-EAD37DC6C987}.Release|Any CPU.Build.0 = Release|Any CPU
|
{B2F79E56-20CC-8FC3-23A1-EAD37DC6C987}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B2F79E56-20CC-8FC3-23A1-EAD37DC6C987}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{B2F79E56-20CC-8FC3-23A1-EAD37DC6C987}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{B2F79E56-20CC-8FC3-23A1-EAD37DC6C987}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{B2F79E56-20CC-8FC3-23A1-EAD37DC6C987}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{4F0F9616-76B1-4BF3-5454-6FB81877D3CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{4F0F9616-76B1-4BF3-5454-6FB81877D3CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{4F0F9616-76B1-4BF3-5454-6FB81877D3CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{4F0F9616-76B1-4BF3-5454-6FB81877D3CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4F0F9616-76B1-4BF3-5454-6FB81877D3CF}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{4F0F9616-76B1-4BF3-5454-6FB81877D3CF}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{4F0F9616-76B1-4BF3-5454-6FB81877D3CF}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{4F0F9616-76B1-4BF3-5454-6FB81877D3CF}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{4F0F9616-76B1-4BF3-5454-6FB81877D3CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{4F0F9616-76B1-4BF3-5454-6FB81877D3CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{4F0F9616-76B1-4BF3-5454-6FB81877D3CF}.Release|Any CPU.Build.0 = Release|Any CPU
|
{4F0F9616-76B1-4BF3-5454-6FB81877D3CF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{4F0F9616-76B1-4BF3-5454-6FB81877D3CF}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{4F0F9616-76B1-4BF3-5454-6FB81877D3CF}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{4F0F9616-76B1-4BF3-5454-6FB81877D3CF}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{4F0F9616-76B1-4BF3-5454-6FB81877D3CF}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{5E131E95-E561-8AC0-5865-1490CFD43805}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{5E131E95-E561-8AC0-5865-1490CFD43805}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{5E131E95-E561-8AC0-5865-1490CFD43805}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{5E131E95-E561-8AC0-5865-1490CFD43805}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{5E131E95-E561-8AC0-5865-1490CFD43805}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{5E131E95-E561-8AC0-5865-1490CFD43805}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{5E131E95-E561-8AC0-5865-1490CFD43805}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{5E131E95-E561-8AC0-5865-1490CFD43805}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{5E131E95-E561-8AC0-5865-1490CFD43805}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{5E131E95-E561-8AC0-5865-1490CFD43805}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{5E131E95-E561-8AC0-5865-1490CFD43805}.Release|Any CPU.Build.0 = Release|Any CPU
|
{5E131E95-E561-8AC0-5865-1490CFD43805}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{5E131E95-E561-8AC0-5865-1490CFD43805}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{5E131E95-E561-8AC0-5865-1490CFD43805}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{5E131E95-E561-8AC0-5865-1490CFD43805}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{5E131E95-E561-8AC0-5865-1490CFD43805}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{9AD6065B-F7FD-AC29-D9EC-153C2F084386}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{9AD6065B-F7FD-AC29-D9EC-153C2F084386}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{9AD6065B-F7FD-AC29-D9EC-153C2F084386}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{9AD6065B-F7FD-AC29-D9EC-153C2F084386}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{9AD6065B-F7FD-AC29-D9EC-153C2F084386}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{9AD6065B-F7FD-AC29-D9EC-153C2F084386}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{9AD6065B-F7FD-AC29-D9EC-153C2F084386}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{9AD6065B-F7FD-AC29-D9EC-153C2F084386}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{9AD6065B-F7FD-AC29-D9EC-153C2F084386}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{9AD6065B-F7FD-AC29-D9EC-153C2F084386}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{9AD6065B-F7FD-AC29-D9EC-153C2F084386}.Release|Any CPU.Build.0 = Release|Any CPU
|
{9AD6065B-F7FD-AC29-D9EC-153C2F084386}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{9AD6065B-F7FD-AC29-D9EC-153C2F084386}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{9AD6065B-F7FD-AC29-D9EC-153C2F084386}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{9AD6065B-F7FD-AC29-D9EC-153C2F084386}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{9AD6065B-F7FD-AC29-D9EC-153C2F084386}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{09B7271A-1C9B-FB05-019F-779462CB84A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{09B7271A-1C9B-FB05-019F-779462CB84A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{09B7271A-1C9B-FB05-019F-779462CB84A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{09B7271A-1C9B-FB05-019F-779462CB84A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{09B7271A-1C9B-FB05-019F-779462CB84A7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{09B7271A-1C9B-FB05-019F-779462CB84A7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{09B7271A-1C9B-FB05-019F-779462CB84A7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{09B7271A-1C9B-FB05-019F-779462CB84A7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{09B7271A-1C9B-FB05-019F-779462CB84A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{09B7271A-1C9B-FB05-019F-779462CB84A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{09B7271A-1C9B-FB05-019F-779462CB84A7}.Release|Any CPU.Build.0 = Release|Any CPU
|
{09B7271A-1C9B-FB05-019F-779462CB84A7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{09B7271A-1C9B-FB05-019F-779462CB84A7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{09B7271A-1C9B-FB05-019F-779462CB84A7}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{09B7271A-1C9B-FB05-019F-779462CB84A7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{09B7271A-1C9B-FB05-019F-779462CB84A7}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{2E5449E2-9A62-16CD-0068-90FE44ABEFEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{2E5449E2-9A62-16CD-0068-90FE44ABEFEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{2E5449E2-9A62-16CD-0068-90FE44ABEFEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{2E5449E2-9A62-16CD-0068-90FE44ABEFEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{2E5449E2-9A62-16CD-0068-90FE44ABEFEE}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{2E5449E2-9A62-16CD-0068-90FE44ABEFEE}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{2E5449E2-9A62-16CD-0068-90FE44ABEFEE}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{2E5449E2-9A62-16CD-0068-90FE44ABEFEE}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{2E5449E2-9A62-16CD-0068-90FE44ABEFEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{2E5449E2-9A62-16CD-0068-90FE44ABEFEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{2E5449E2-9A62-16CD-0068-90FE44ABEFEE}.Release|Any CPU.Build.0 = Release|Any CPU
|
{2E5449E2-9A62-16CD-0068-90FE44ABEFEE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{2E5449E2-9A62-16CD-0068-90FE44ABEFEE}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{2E5449E2-9A62-16CD-0068-90FE44ABEFEE}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{2E5449E2-9A62-16CD-0068-90FE44ABEFEE}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{2E5449E2-9A62-16CD-0068-90FE44ABEFEE}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{F072C376-70B4-B061-745B-0B1BDEBF8CDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{F072C376-70B4-B061-745B-0B1BDEBF8CDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{F072C376-70B4-B061-745B-0B1BDEBF8CDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{F072C376-70B4-B061-745B-0B1BDEBF8CDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F072C376-70B4-B061-745B-0B1BDEBF8CDE}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{F072C376-70B4-B061-745B-0B1BDEBF8CDE}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{F072C376-70B4-B061-745B-0B1BDEBF8CDE}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{F072C376-70B4-B061-745B-0B1BDEBF8CDE}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{F072C376-70B4-B061-745B-0B1BDEBF8CDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{F072C376-70B4-B061-745B-0B1BDEBF8CDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{F072C376-70B4-B061-745B-0B1BDEBF8CDE}.Release|Any CPU.Build.0 = Release|Any CPU
|
{F072C376-70B4-B061-745B-0B1BDEBF8CDE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{F072C376-70B4-B061-745B-0B1BDEBF8CDE}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{F072C376-70B4-B061-745B-0B1BDEBF8CDE}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{F072C376-70B4-B061-745B-0B1BDEBF8CDE}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{F072C376-70B4-B061-745B-0B1BDEBF8CDE}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{D8340DC7-5D27-2A71-74CC-634493847FF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{D8340DC7-5D27-2A71-74CC-634493847FF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{D8340DC7-5D27-2A71-74CC-634493847FF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{D8340DC7-5D27-2A71-74CC-634493847FF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D8340DC7-5D27-2A71-74CC-634493847FF0}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{D8340DC7-5D27-2A71-74CC-634493847FF0}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{D8340DC7-5D27-2A71-74CC-634493847FF0}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{D8340DC7-5D27-2A71-74CC-634493847FF0}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{D8340DC7-5D27-2A71-74CC-634493847FF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{D8340DC7-5D27-2A71-74CC-634493847FF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{D8340DC7-5D27-2A71-74CC-634493847FF0}.Release|Any CPU.Build.0 = Release|Any CPU
|
{D8340DC7-5D27-2A71-74CC-634493847FF0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D8340DC7-5D27-2A71-74CC-634493847FF0}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{D8340DC7-5D27-2A71-74CC-634493847FF0}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{D8340DC7-5D27-2A71-74CC-634493847FF0}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{D8340DC7-5D27-2A71-74CC-634493847FF0}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{69A075E7-D924-59C6-0BF2-17A09201DDF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{69A075E7-D924-59C6-0BF2-17A09201DDF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{69A075E7-D924-59C6-0BF2-17A09201DDF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{69A075E7-D924-59C6-0BF2-17A09201DDF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{69A075E7-D924-59C6-0BF2-17A09201DDF3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{69A075E7-D924-59C6-0BF2-17A09201DDF3}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{69A075E7-D924-59C6-0BF2-17A09201DDF3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{69A075E7-D924-59C6-0BF2-17A09201DDF3}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{69A075E7-D924-59C6-0BF2-17A09201DDF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{69A075E7-D924-59C6-0BF2-17A09201DDF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{69A075E7-D924-59C6-0BF2-17A09201DDF3}.Release|Any CPU.Build.0 = Release|Any CPU
|
{69A075E7-D924-59C6-0BF2-17A09201DDF3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{69A075E7-D924-59C6-0BF2-17A09201DDF3}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{69A075E7-D924-59C6-0BF2-17A09201DDF3}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{69A075E7-D924-59C6-0BF2-17A09201DDF3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{69A075E7-D924-59C6-0BF2-17A09201DDF3}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Release|Any CPU.Build.0 = Release|Any CPU
|
{D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{D1DF309F-40DE-9C0E-A78B-2648544B77D2}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Release|Any CPU.Build.0 = Release|Any CPU
|
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{7D88CAF1-1887-A011-BA72-F38C87C1A7D9}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{E7BF2911-582D-C403-254F-F7FC895BFD68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{E7BF2911-582D-C403-254F-F7FC895BFD68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{E7BF2911-582D-C403-254F-F7FC895BFD68}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{E7BF2911-582D-C403-254F-F7FC895BFD68}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E7BF2911-582D-C403-254F-F7FC895BFD68}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{E7BF2911-582D-C403-254F-F7FC895BFD68}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{E7BF2911-582D-C403-254F-F7FC895BFD68}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{E7BF2911-582D-C403-254F-F7FC895BFD68}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{E7BF2911-582D-C403-254F-F7FC895BFD68}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{E7BF2911-582D-C403-254F-F7FC895BFD68}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{E7BF2911-582D-C403-254F-F7FC895BFD68}.Release|Any CPU.Build.0 = Release|Any CPU
|
{E7BF2911-582D-C403-254F-F7FC895BFD68}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E7BF2911-582D-C403-254F-F7FC895BFD68}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{E7BF2911-582D-C403-254F-F7FC895BFD68}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{E7BF2911-582D-C403-254F-F7FC895BFD68}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{E7BF2911-582D-C403-254F-F7FC895BFD68}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Release|Any CPU.Build.0 = Release|Any CPU
|
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{7FD57668-2AD8-0F53-4006-03927B5A385C}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|Any CPU.Build.0 = Release|Any CPU
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{9FF626DF-1AD4-2BE1-F834-F49121D65085}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|Any CPU.Build.0 = Release|Any CPU
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{550A20AB-8E97-BCDD-9F54-27823663120A}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Release|Any CPU.Build.0 = Release|Any CPU
|
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{D1B99C5A-82F7-459D-B56D-F8FD096D3854}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D1B99C5A-82F7-459D-B56D-F8FD096D3854}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D1B99C5A-82F7-459D-B56D-F8FD096D3854}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{D1B99C5A-82F7-459D-B56D-F8FD096D3854}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{D1B99C5A-82F7-459D-B56D-F8FD096D3854}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{D1B99C5A-82F7-459D-B56D-F8FD096D3854}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{D1B99C5A-82F7-459D-B56D-F8FD096D3854}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D1B99C5A-82F7-459D-B56D-F8FD096D3854}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D1B99C5A-82F7-459D-B56D-F8FD096D3854}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{D1B99C5A-82F7-459D-B56D-F8FD096D3854}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{D1B99C5A-82F7-459D-B56D-F8FD096D3854}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{D1B99C5A-82F7-459D-B56D-F8FD096D3854}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -226,6 +413,7 @@ Global
|
|||||||
{550A20AB-8E97-BCDD-9F54-27823663120A} = {21D42B96-99F9-4E48-A499-5170A5A9597F}
|
{550A20AB-8E97-BCDD-9F54-27823663120A} = {21D42B96-99F9-4E48-A499-5170A5A9597F}
|
||||||
{E713D25F-2602-44C9-AB9E-C9477FB2BA93} = {94C8CFDB-C7C6-40DF-A596-647FEEA3C917}
|
{E713D25F-2602-44C9-AB9E-C9477FB2BA93} = {94C8CFDB-C7C6-40DF-A596-647FEEA3C917}
|
||||||
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1} = {E713D25F-2602-44C9-AB9E-C9477FB2BA93}
|
{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1} = {E713D25F-2602-44C9-AB9E-C9477FB2BA93}
|
||||||
|
{D1B99C5A-82F7-459D-B56D-F8FD096D3854} = {2769C4C3-2595-413B-B7FE-5903826770C1}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {C584421D-5EC0-4821-B7D8-2633D8D405F2}
|
SolutionGuid = {C584421D-5EC0-4821-B7D8-2633D8D405F2}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ public static class Codec
|
|||||||
DataDeserializer.BooleanFalseParserAsync,
|
DataDeserializer.BooleanFalseParserAsync,
|
||||||
DataDeserializer.BooleanTrueParserAsync,
|
DataDeserializer.BooleanTrueParserAsync,
|
||||||
DataDeserializer.NotModifiedParserAsync,
|
DataDeserializer.NotModifiedParserAsync,
|
||||||
|
DataDeserializer.InfinityParserAsync,
|
||||||
},
|
},
|
||||||
new AsyncParser[]{
|
new AsyncParser[]{
|
||||||
DataDeserializer.UInt8ParserAsync,
|
DataDeserializer.UInt8ParserAsync,
|
||||||
@@ -115,6 +116,7 @@ public static class Codec
|
|||||||
DataDeserializer.BooleanFalseParser,
|
DataDeserializer.BooleanFalseParser,
|
||||||
DataDeserializer.BooleanTrueParser,
|
DataDeserializer.BooleanTrueParser,
|
||||||
DataDeserializer.NotModifiedParser,
|
DataDeserializer.NotModifiedParser,
|
||||||
|
DataDeserializer.InfinityParser,
|
||||||
},
|
},
|
||||||
new SyncParser[]{
|
new SyncParser[]{
|
||||||
DataDeserializer.UInt8Parser,
|
DataDeserializer.UInt8Parser,
|
||||||
@@ -376,6 +378,13 @@ public static class Codec
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Synchronously parses a single value from its IIP wire representation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">Buffer containing the encoded value.</param>
|
||||||
|
/// <param name="offset">Zero-based offset of the value within <paramref name="data"/>.</param>
|
||||||
|
/// <param name="warehouse">Warehouse used to resolve typed structures (records, enums, ...).</param>
|
||||||
|
/// <returns>A tuple of (number of bytes consumed, decoded value).</returns>
|
||||||
public static (uint, object) ParseSync(byte[] data, uint offset, Warehouse warehouse)
|
public static (uint, object) ParseSync(byte[] data, uint offset, Warehouse warehouse)
|
||||||
{
|
{
|
||||||
var tdu = ParsedTdu.ParseSync(data, offset, (uint)data.Length, warehouse);
|
var tdu = ParsedTdu.ParseSync(data, offset, (uint)data.Length, warehouse);
|
||||||
@@ -612,7 +621,14 @@ public static class Codec
|
|||||||
/// <param name="connection">EpConnection is required to check locality.</param>
|
/// <param name="connection">EpConnection is required to check locality.</param>
|
||||||
/// <param name="prependType">If True, prepend the DataType at the beginning of the output.</param>
|
/// <param name="prependType">If True, prepend the DataType at the beginning of the output.</param>
|
||||||
/// <returns>Array of bytes in the network byte order.</returns>
|
/// <returns>Array of bytes in the network byte order.</returns>
|
||||||
public static byte[] Compose(object valueOrSource, Warehouse warehouse, EpConnection connection)//, bool prependType = true)
|
/// <summary>
|
||||||
|
/// Encodes a value to its self-describing IIP wire representation (a type-prefixed TDU).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="valueOrSource">The value to encode (may be null, which encodes as the Null TDU).</param>
|
||||||
|
/// <param name="warehouse">Warehouse used to resolve type definitions for typed structures.</param>
|
||||||
|
/// <param name="connection">Connection context, required when the value references remote resources; may be null for plain data.</param>
|
||||||
|
/// <returns>The encoded bytes, including the leading type identifier.</returns>
|
||||||
|
public static byte[] Compose(object valueOrSource, Warehouse warehouse, EpConnection connection)
|
||||||
{
|
{
|
||||||
var tdu = ComposeInternal(valueOrSource, warehouse, connection);
|
var tdu = ComposeInternal(valueOrSource, warehouse, connection);
|
||||||
return tdu.Composed;
|
return tdu.Composed;
|
||||||
|
|||||||
@@ -108,11 +108,10 @@ public static class DC // Data Converter
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
|
// Preserve the original stack trace with a bare rethrow.
|
||||||
throw ex;
|
throw;
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,19 @@ public static class DataDeserializer
|
|||||||
return NotModified.Default;
|
return NotModified.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The Infinity token carries no payload: the serializer collapses every NaN and
|
||||||
|
// +/- Infinity onto it (see DataSerializer.Float32/Float64Composer). Decoding it to
|
||||||
|
// a single canonical double keeps the (lossy) round trip from throwing.
|
||||||
|
public static object InfinityParserAsync(ParsedTdu tdu, EpConnection connection, uint[] requestSequence)
|
||||||
|
{
|
||||||
|
return double.PositiveInfinity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static object InfinityParser(ParsedTdu tdu, Warehouse warehouse)
|
||||||
|
{
|
||||||
|
return double.PositiveInfinity;
|
||||||
|
}
|
||||||
|
|
||||||
public static object UInt8ParserAsync(ParsedTdu tdu, EpConnection connection, uint[] requestSequence)
|
public static object UInt8ParserAsync(ParsedTdu tdu, EpConnection connection, uint[] requestSequence)
|
||||||
{
|
{
|
||||||
return tdu.Data[tdu.PayloadOffset];
|
return tdu.Data[tdu.PayloadOffset];
|
||||||
@@ -1345,7 +1358,6 @@ public static class DataDeserializer
|
|||||||
var subTypes = subTrus.Select(x => x.RuntimeType).ToArray();
|
var subTypes = subTrus.Select(x => x.RuntimeType).ToArray();
|
||||||
|
|
||||||
ParsedTdu current;
|
ParsedTdu current;
|
||||||
ParsedTdu? previous = null;
|
|
||||||
|
|
||||||
var offset = tdu.PayloadOffset;
|
var offset = tdu.PayloadOffset;
|
||||||
var length = tdu.PayloadLength;
|
var length = tdu.PayloadLength;
|
||||||
@@ -1477,7 +1489,6 @@ public static class DataDeserializer
|
|||||||
var types = subTrus.Select(x => x.RuntimeType).ToArray();
|
var types = subTrus.Select(x => x.RuntimeType).ToArray();
|
||||||
|
|
||||||
ParsedTdu current;
|
ParsedTdu current;
|
||||||
ParsedTdu? previous = null;
|
|
||||||
|
|
||||||
var offset = tdu.PayloadOffset;
|
var offset = tdu.PayloadOffset;
|
||||||
var length = tdu.PayloadLength;
|
var length = tdu.PayloadLength;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public static class DataSerializer
|
|||||||
{
|
{
|
||||||
public delegate byte[] Serializer(object value);
|
public delegate byte[] Serializer(object value);
|
||||||
|
|
||||||
public static unsafe Tdu Int32Composer(object value, Warehouse warehouse, EpConnection connection)
|
public static Tdu Int32Composer(object value, Warehouse warehouse, EpConnection connection)
|
||||||
{
|
{
|
||||||
var v = (int)value;
|
var v = (int)value;
|
||||||
|
|
||||||
@@ -29,22 +29,19 @@ public static class DataSerializer
|
|||||||
{
|
{
|
||||||
// Fits in 2 bytes
|
// Fits in 2 bytes
|
||||||
var rt = new byte[2];
|
var rt = new byte[2];
|
||||||
fixed (byte* ptr = rt)
|
BinaryPrimitives.WriteInt16LittleEndian(rt, (short)v);
|
||||||
*((short*)ptr) = (short)v;
|
|
||||||
|
|
||||||
return new Tdu(TduIdentifier.Int16, rt, 2, null, null);
|
return new Tdu(TduIdentifier.Int16, rt, 2, null, null);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Use full 4 bytes
|
// Use full 4 bytes
|
||||||
var rt = new byte[4];
|
var rt = new byte[4];
|
||||||
fixed (byte* ptr = rt)
|
BinaryPrimitives.WriteInt32LittleEndian(rt, v);
|
||||||
*((int*)ptr) = v;
|
|
||||||
return new Tdu(TduIdentifier.Int32, rt, 4, null, null);
|
return new Tdu(TduIdentifier.Int32, rt, 4, null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static unsafe Tdu UInt32Composer(object value, Warehouse warehouse, EpConnection connection)
|
public static Tdu UInt32Composer(object value, Warehouse warehouse, EpConnection connection)
|
||||||
{
|
{
|
||||||
var v = (uint)value;
|
var v = (uint)value;
|
||||||
|
|
||||||
@@ -57,23 +54,19 @@ public static class DataSerializer
|
|||||||
{
|
{
|
||||||
// Fits in 2 bytes
|
// Fits in 2 bytes
|
||||||
var rt = new byte[2];
|
var rt = new byte[2];
|
||||||
fixed (byte* ptr = rt)
|
BinaryPrimitives.WriteUInt16LittleEndian(rt, (ushort)v);
|
||||||
*((ushort*)ptr) = (ushort)v;
|
|
||||||
|
|
||||||
return new Tdu(TduIdentifier.UInt16, rt, 2, null, null);
|
return new Tdu(TduIdentifier.UInt16, rt, 2, null, null);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Use full 4 bytes
|
// Use full 4 bytes
|
||||||
var rt = new byte[4];
|
var rt = new byte[4];
|
||||||
fixed (byte* ptr = rt)
|
BinaryPrimitives.WriteUInt32LittleEndian(rt, v);
|
||||||
*((uint*)ptr) = v;
|
|
||||||
|
|
||||||
return new Tdu(TduIdentifier.UInt32, rt, 4, null, null);
|
return new Tdu(TduIdentifier.UInt32, rt, 4, null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static unsafe Tdu Int16Composer(object value, Warehouse warehouse, EpConnection connection)
|
public static Tdu Int16Composer(object value, Warehouse warehouse, EpConnection connection)
|
||||||
{
|
{
|
||||||
var v = (short)value;
|
var v = (short)value;
|
||||||
|
|
||||||
@@ -86,14 +79,12 @@ public static class DataSerializer
|
|||||||
{
|
{
|
||||||
// Use full 2 bytes
|
// Use full 2 bytes
|
||||||
var rt = new byte[2];
|
var rt = new byte[2];
|
||||||
fixed (byte* ptr = rt)
|
BinaryPrimitives.WriteInt16LittleEndian(rt, v);
|
||||||
*((short*)ptr) = v;
|
|
||||||
|
|
||||||
return new Tdu(TduIdentifier.Int16, rt, 2, null, null);
|
return new Tdu(TduIdentifier.Int16, rt, 2, null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static unsafe Tdu UInt16Composer(object value, Warehouse warehouse, EpConnection connection)
|
public static Tdu UInt16Composer(object value, Warehouse warehouse, EpConnection connection)
|
||||||
{
|
{
|
||||||
var v = (ushort)value;
|
var v = (ushort)value;
|
||||||
|
|
||||||
@@ -106,9 +97,7 @@ public static class DataSerializer
|
|||||||
{
|
{
|
||||||
// Use full 2 bytes
|
// Use full 2 bytes
|
||||||
var rt = new byte[2];
|
var rt = new byte[2];
|
||||||
fixed (byte* ptr = rt)
|
BinaryPrimitives.WriteUInt16LittleEndian(rt, v);
|
||||||
*((ushort*)ptr) = v;
|
|
||||||
|
|
||||||
return new Tdu(TduIdentifier.UInt16, rt, 2, null, null);
|
return new Tdu(TduIdentifier.UInt16, rt, 2, null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,7 +200,7 @@ public static class DataSerializer
|
|||||||
return new Tdu(TduIdentifier.Float64, rt, 8, null, null);
|
return new Tdu(TduIdentifier.Float64, rt, 8, null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static unsafe Tdu Int64Composer(object value, Warehouse warehouse, EpConnection connection)
|
public static Tdu Int64Composer(object value, Warehouse warehouse, EpConnection connection)
|
||||||
{
|
{
|
||||||
var v = (long)value;
|
var v = (long)value;
|
||||||
|
|
||||||
@@ -224,32 +213,26 @@ public static class DataSerializer
|
|||||||
{
|
{
|
||||||
// Fits in 2 bytes
|
// Fits in 2 bytes
|
||||||
var rt = new byte[2];
|
var rt = new byte[2];
|
||||||
fixed (byte* ptr = rt)
|
BinaryPrimitives.WriteInt16LittleEndian(rt, (short)v);
|
||||||
*((short*)ptr) = (short)v;
|
|
||||||
|
|
||||||
return new Tdu(TduIdentifier.Int16, rt, 2, null, null);
|
return new Tdu(TduIdentifier.Int16, rt, 2, null, null);
|
||||||
}
|
}
|
||||||
else if (v >= int.MinValue && v <= int.MaxValue)
|
else if (v >= int.MinValue && v <= int.MaxValue)
|
||||||
{
|
{
|
||||||
// Fits in 4 bytes
|
// Fits in 4 bytes
|
||||||
var rt = new byte[4];
|
var rt = new byte[4];
|
||||||
fixed (byte* ptr = rt)
|
BinaryPrimitives.WriteInt32LittleEndian(rt, (int)v);
|
||||||
*((int*)ptr) = (int)v;
|
|
||||||
|
|
||||||
return new Tdu(TduIdentifier.Int32, rt, 4, null, null);
|
return new Tdu(TduIdentifier.Int32, rt, 4, null, null);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Use full 8 bytes
|
// Use full 8 bytes
|
||||||
var rt = new byte[8];
|
var rt = new byte[8];
|
||||||
fixed (byte* ptr = rt)
|
BinaryPrimitives.WriteInt64LittleEndian(rt, v);
|
||||||
*((long*)ptr) = v;
|
|
||||||
|
|
||||||
return new Tdu(TduIdentifier.Int64, rt, 8, null, null);
|
return new Tdu(TduIdentifier.Int64, rt, 8, null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static unsafe Tdu UInt64Composer(object value, Warehouse warehouse, EpConnection connection)
|
public static Tdu UInt64Composer(object value, Warehouse warehouse, EpConnection connection)
|
||||||
{
|
{
|
||||||
var v = (ulong)value;
|
var v = (ulong)value;
|
||||||
|
|
||||||
@@ -262,39 +245,31 @@ public static class DataSerializer
|
|||||||
{
|
{
|
||||||
// Fits in 2 bytes
|
// Fits in 2 bytes
|
||||||
var rt = new byte[2];
|
var rt = new byte[2];
|
||||||
fixed (byte* ptr = rt)
|
BinaryPrimitives.WriteUInt16LittleEndian(rt, (ushort)v);
|
||||||
*((ushort*)ptr) = (ushort)v;
|
|
||||||
|
|
||||||
return new Tdu(TduIdentifier.UInt16, rt, 2, null, null);
|
return new Tdu(TduIdentifier.UInt16, rt, 2, null, null);
|
||||||
}
|
}
|
||||||
else if (v <= uint.MaxValue)
|
else if (v <= uint.MaxValue)
|
||||||
{
|
{
|
||||||
// Fits in 4 bytes
|
// Fits in 4 bytes
|
||||||
var rt = new byte[4];
|
var rt = new byte[4];
|
||||||
fixed (byte* ptr = rt)
|
BinaryPrimitives.WriteUInt32LittleEndian(rt, (uint)v);
|
||||||
*((uint*)ptr) = (uint)v;
|
|
||||||
|
|
||||||
return new Tdu(TduIdentifier.UInt32, rt, 4, null, null);
|
return new Tdu(TduIdentifier.UInt32, rt, 4, null, null);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Use full 8 bytes
|
// Use full 8 bytes
|
||||||
var rt = new byte[8];
|
var rt = new byte[8];
|
||||||
fixed (byte* ptr = rt)
|
BinaryPrimitives.WriteUInt64LittleEndian(rt, v);
|
||||||
*((ulong*)ptr) = v;
|
|
||||||
|
|
||||||
return new Tdu(TduIdentifier.UInt64, rt, 8, null, null);
|
return new Tdu(TduIdentifier.UInt64, rt, 8, null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static unsafe Tdu DateTimeComposer(object value, Warehouse warehouse, EpConnection connection)
|
public static Tdu DateTimeComposer(object value, Warehouse warehouse, EpConnection connection)
|
||||||
{
|
{
|
||||||
var v = ((DateTime)value).ToUniversalTime().Ticks;
|
var v = ((DateTime)value).ToUniversalTime().Ticks;
|
||||||
var rt = new byte[8];
|
var rt = new byte[8];
|
||||||
fixed (byte* ptr = rt)
|
BinaryPrimitives.WriteInt64LittleEndian(rt, v);
|
||||||
*((long*)ptr) = v;
|
|
||||||
|
|
||||||
return new Tdu(TduIdentifier.DateTime, rt, 8, null, null);
|
return new Tdu(TduIdentifier.DateTime, rt, 8, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +337,7 @@ public static class DataSerializer
|
|||||||
double d = (double)v;
|
double d = (double)v;
|
||||||
if ((decimal)d == v)
|
if ((decimal)d == v)
|
||||||
{
|
{
|
||||||
var rt = new byte[4];
|
var rt = new byte[8];
|
||||||
|
|
||||||
fixed (byte* ptr = rt)
|
fixed (byte* ptr = rt)
|
||||||
*((double*)ptr) = d;
|
*((double*)ptr) = d;
|
||||||
@@ -436,15 +411,12 @@ public static class DataSerializer
|
|||||||
new byte[] { (byte)(char)value }, 1, null, null);
|
new byte[] { (byte)(char)value }, 1, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static unsafe Tdu Char16Composer(object value, Warehouse warehouse, EpConnection connection)
|
public static Tdu Char16Composer(object value, Warehouse warehouse, EpConnection connection)
|
||||||
{
|
{
|
||||||
var v = (char)value;
|
var v = (char)value;
|
||||||
var rt = new byte[2];
|
var rt = new byte[2];
|
||||||
fixed (byte* ptr = rt)
|
BinaryPrimitives.WriteUInt16LittleEndian(rt, v);
|
||||||
*((char*)ptr) = v;
|
|
||||||
|
|
||||||
return new Tdu(TduIdentifier.Char16, rt, 2, null, null);
|
return new Tdu(TduIdentifier.Char16, rt, 2, null, null);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Tdu BoolComposer(object value, Warehouse warehouse, EpConnection connection)
|
public static Tdu BoolComposer(object value, Warehouse warehouse, EpConnection connection)
|
||||||
@@ -733,7 +705,9 @@ public static class DataSerializer
|
|||||||
if (value == null)
|
if (value == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var rt = new List<byte>();
|
// Pre-size the buffer from the element count (when known) to avoid repeated
|
||||||
|
// List<byte> reallocations as items are appended. 4 bytes/element is a rough hint.
|
||||||
|
var rt = new List<byte>(value is ICollection collection ? collection.Count * 4 : 16);
|
||||||
|
|
||||||
Tdu? previous = null;
|
Tdu? previous = null;
|
||||||
|
|
||||||
@@ -934,7 +908,7 @@ public static class DataSerializer
|
|||||||
var trus = fields.Select(x => Tru.FromType(x.FieldType, warehouse)).ToArray();
|
var trus = fields.Select(x => Tru.FromType(x.FieldType, warehouse)).ToArray();
|
||||||
|
|
||||||
|
|
||||||
var rt = new List<byte>();
|
var rt = new List<byte>(fields.Length * 4);
|
||||||
|
|
||||||
for (var i = 0; i < fields.Length; i++)
|
for (var i = 0; i < fields.Length; i++)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ public struct Tdu
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Tdu(TduIdentifier identifier,
|
public Tdu(TduIdentifier identifier,
|
||||||
byte[] data, ulong length, Tru metadata, EpConnection connection)
|
byte[]? data, ulong length, Tru? metadata, EpConnection? connection)
|
||||||
{
|
{
|
||||||
Identifier = identifier;
|
Identifier = identifier;
|
||||||
//Index = (byte)identifier & 0x7;
|
//Index = (byte)identifier & 0x7;
|
||||||
|
|||||||
@@ -138,6 +138,14 @@ namespace Esiur.Data
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
// Equality is defined by Match, which always requires a matching Identifier
|
||||||
|
// (composites additionally compare sub-types). Hashing on Identifier therefore
|
||||||
|
// keeps equal Trus in the same bucket and honours the Equals/GetHashCode contract.
|
||||||
|
return (int)Identifier;
|
||||||
|
}
|
||||||
|
|
||||||
public abstract void SetNotNull(List<byte> flags);
|
public abstract void SetNotNull(List<byte> flags);
|
||||||
|
|
||||||
public abstract void SetNotNull(byte flag);
|
public abstract void SetNotNull(byte flag);
|
||||||
@@ -299,7 +307,32 @@ namespace Esiur.Data
|
|||||||
//private static Dictionary<Type, Tru> cache = new Dictionary<Type, Tru>();
|
//private static Dictionary<Type, Tru> cache = new Dictionary<Type, Tru>();
|
||||||
//private static object cacheLook = new object();
|
//private static object cacheLook = new object();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds the type-representation unit (Tru) describing how a CLR type maps onto the
|
||||||
|
/// wire, recursing into element/key/value/field types for collections, maps and tuples.
|
||||||
|
/// Results are memoized per warehouse since this is reflection-heavy and hot during
|
||||||
|
/// serialization; returned Tru instances are immutable and safe to share.
|
||||||
|
/// </summary>
|
||||||
public static Tru? FromType(Type type, Warehouse warehouse)
|
public static Tru? FromType(Type type, Warehouse warehouse)
|
||||||
|
{
|
||||||
|
// null maps to Void and cannot be a dictionary key, so compute it directly.
|
||||||
|
if (type == null)
|
||||||
|
return FromTypeCore(null, warehouse);
|
||||||
|
|
||||||
|
if (warehouse.TypeRepresentationCache.TryGetValue(type, out var cached))
|
||||||
|
return cached;
|
||||||
|
|
||||||
|
var tru = FromTypeCore(type, warehouse);
|
||||||
|
|
||||||
|
// Cache only fully-built results. Unrecognized types return null (or throw),
|
||||||
|
// which we leave uncached so a later type registration can still resolve them.
|
||||||
|
if (tru != null)
|
||||||
|
warehouse.TypeRepresentationCache[type] = tru;
|
||||||
|
|
||||||
|
return tru;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Tru? FromTypeCore(Type? type, Warehouse warehouse)
|
||||||
{
|
{
|
||||||
if (type == null)
|
if (type == null)
|
||||||
return new TruPrimitive(TruIdentifier.Void, true, typeof(void));
|
return new TruPrimitive(TruIdentifier.Void, true, typeof(void));
|
||||||
@@ -714,7 +747,7 @@ namespace Esiur.Data
|
|||||||
offset += pr.Size;
|
offset += pr.Size;
|
||||||
}
|
}
|
||||||
|
|
||||||
Type runtimeType = null;
|
Type? runtimeType = null;
|
||||||
|
|
||||||
if (identifier == TruIdentifier.TypedList)
|
if (identifier == TruIdentifier.TypedList)
|
||||||
{
|
{
|
||||||
@@ -852,7 +885,7 @@ namespace Esiur.Data
|
|||||||
offset += pr.Size;
|
offset += pr.Size;
|
||||||
}
|
}
|
||||||
|
|
||||||
Type runtimeType = null;
|
Type? runtimeType = null;
|
||||||
|
|
||||||
if (identifier == TruIdentifier.TypedList)
|
if (identifier == TruIdentifier.TypedList)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,11 +11,9 @@ namespace Esiur.Data
|
|||||||
{
|
{
|
||||||
public Tru[] SubTypes;
|
public Tru[] SubTypes;
|
||||||
|
|
||||||
Type _runtimeType;
|
|
||||||
|
|
||||||
public override Type RuntimeType { get; protected set; }
|
public override Type RuntimeType { get; protected set; }
|
||||||
|
|
||||||
public TruComposite(TruIdentifier identifier, bool nullable, Tru[] subTypes, Type type)
|
public TruComposite(TruIdentifier identifier, bool nullable, Tru[] subTypes, Type? type)
|
||||||
{
|
{
|
||||||
Identifier = identifier;
|
Identifier = identifier;
|
||||||
Nullable = nullable;
|
Nullable = nullable;
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ public class ArgumentDef
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var exp = Codec.Compose(Annotations, null, null);
|
var exp = Codec.Compose(Annotations, connection.Instance.Warehouse, connection);
|
||||||
|
|
||||||
return new BinaryList()
|
return new BinaryList()
|
||||||
.AddUInt8((byte)(0x2 | (Optional ? 1 : 0)))
|
.AddUInt8((byte)(0x2 | (Optional ? 1 : 0)))
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ public class ConstantDef : MemberDef
|
|||||||
|
|
||||||
if (Annotations != null)
|
if (Annotations != null)
|
||||||
{
|
{
|
||||||
var exp = Codec.Compose(Annotations, null, null);// DC.ToBytes(Annotation);
|
var exp = Codec.Compose(Annotations, connection.Instance.Warehouse, connection);// DC.ToBytes(Annotation);
|
||||||
hdr |= 0x70;
|
hdr |= 0x70;
|
||||||
return new BinaryList()
|
return new BinaryList()
|
||||||
.AddUInt8(hdr)
|
.AddUInt8(hdr)
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ public class EventDef : MemberDef
|
|||||||
|
|
||||||
if (Annotations != null)
|
if (Annotations != null)
|
||||||
{
|
{
|
||||||
var exp = Codec.Compose(Annotations, null, null); //( DC.ToBytes(Annotation);
|
var exp = Codec.Compose(Annotations, connection.Instance.Warehouse, connection); //( DC.ToBytes(Annotation);
|
||||||
hdr |= 0x50;
|
hdr |= 0x50;
|
||||||
return new BinaryList()
|
return new BinaryList()
|
||||||
.AddUInt8(hdr)
|
.AddUInt8(hdr)
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ public class FunctionDef : MemberDef
|
|||||||
|
|
||||||
if (Annotations != null)
|
if (Annotations != null)
|
||||||
{
|
{
|
||||||
var exp = Codec.Compose(Annotations, null, null);// DC.ToBytes(Annotation);
|
var exp = Codec.Compose(Annotations, connection.Instance.Warehouse , connection);// DC.ToBytes(Annotation);
|
||||||
bl.AddUInt8Array(exp);
|
bl.AddUInt8Array(exp);
|
||||||
bl.InsertUInt8(0, (byte)((Inherited ? (byte)0x90 : (byte)0x10) | (IsStatic ? 0x4 : 0)));
|
bl.InsertUInt8(0, (byte)((Inherited ? (byte)0x90 : (byte)0x10) | (IsStatic ? 0x4 : 0)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ public class LocalTypeDef:TypeDef
|
|||||||
//foreach (var ann in Annotations)
|
//foreach (var ann in Annotations)
|
||||||
// Annotations.Add(ann.Key, ann.Value);
|
// Annotations.Add(ann.Key, ann.Value);
|
||||||
|
|
||||||
var classAnnotationBytes = Codec.Compose(Annotations, null, null);
|
var classAnnotationBytes = Codec.Compose(Annotations, connection.Instance.Warehouse, connection);
|
||||||
|
|
||||||
b.AddUInt8Array(classAnnotationBytes);
|
b.AddUInt8Array(classAnnotationBytes);
|
||||||
|
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ public class PropertyDef : MemberDef
|
|||||||
//}
|
//}
|
||||||
if (Annotations != null)
|
if (Annotations != null)
|
||||||
{
|
{
|
||||||
var rexp = Codec.Compose(Annotations, null, null);
|
var rexp = Codec.Compose(Annotations, connection.Instance.Warehouse, connection);
|
||||||
return new BinaryList()
|
return new BinaryList()
|
||||||
.AddUInt8((byte)(0x28 | pv))
|
.AddUInt8((byte)(0x28 | pv))
|
||||||
.AddUInt8((byte)name.Length)
|
.AddUInt8((byte)name.Length)
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
<PackageId>Esiur</PackageId>
|
<PackageId>Esiur</PackageId>
|
||||||
<Product>Esiur</Product>
|
<Product>Esiur</Product>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
|
<!-- Enable the nullable *annotation* context project-wide so '?' reference-type
|
||||||
|
annotations are legal everywhere, without turning on nullable flow warnings
|
||||||
|
(files that opt in via '#nullable enable' still get full checking). -->
|
||||||
|
<Nullable>annotations</Nullable>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
@@ -67,7 +71,6 @@
|
|||||||
<Compile Remove="Net\Packets\EpAuthPacketAction.cs" />
|
<Compile Remove="Net\Packets\EpAuthPacketAction.cs" />
|
||||||
<Compile Remove="Net\Packets\EpAuthPacketAuthMode.cs" />
|
<Compile Remove="Net\Packets\EpAuthPacketAuthMode.cs" />
|
||||||
<Compile Remove="Net\Packets\EpAuthPacketEvent.cs" />
|
<Compile Remove="Net\Packets\EpAuthPacketEvent.cs" />
|
||||||
<Compile Remove="Net\Sockets\TcpSocket.cs" />
|
|
||||||
<Compile Remove="Protocol\Authentication\HashAnonymousAuthenticator.cs" />
|
<Compile Remove="Protocol\Authentication\HashAnonymousAuthenticator.cs" />
|
||||||
<Compile Remove="Security\Authority\AuthenticationMethod.cs" />
|
<Compile Remove="Security\Authority\AuthenticationMethod.cs" />
|
||||||
<Compile Remove="Security\Membership\IMembership.cs" />
|
<Compile Remove="Security\Membership\IMembership.cs" />
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ public static class Global
|
|||||||
{
|
{
|
||||||
private static KeyList<string, object> variables = new KeyList<string, object>();
|
private static KeyList<string, object> variables = new KeyList<string, object>();
|
||||||
|
|
||||||
private static Random rand = new Random();// System.Environment.TickCount);
|
// Cryptographically secure RNG. Used for security-sensitive values such as
|
||||||
|
// authentication nonces and session identifiers, so it must never be a
|
||||||
|
// predictable PRNG (e.g. System.Random). The instance is thread-safe for GetBytes.
|
||||||
|
private static readonly RandomNumberGenerator secureRng = RandomNumberGenerator.Create();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -340,23 +343,41 @@ public static class Global
|
|||||||
public static byte[] GenerateBytes(int length)
|
public static byte[] GenerateBytes(int length)
|
||||||
{
|
{
|
||||||
var b = new byte[length];
|
var b = new byte[length];
|
||||||
rand.NextBytes(b);
|
secureRng.GetBytes(b);
|
||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GenerateCode(int length)
|
public static string GenerateCode(int length)
|
||||||
{
|
{
|
||||||
return GenerateCode(length, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
|
return GenerateCode(length, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GenerateCode(int length, string chars)
|
public static string GenerateCode(int length, string chars)
|
||||||
{
|
{
|
||||||
var result = new string(
|
// Draw each character from a CSPRNG using unbiased rejection sampling, so
|
||||||
Enumerable.Repeat(chars, length)
|
// codes used as session identifiers are not predictable. The largest
|
||||||
.Select(s => s[rand.Next(s.Length)])
|
// multiple of chars.Length that fits in a byte is the acceptance bound;
|
||||||
.ToArray());
|
// bytes at or above it are discarded to avoid modulo bias.
|
||||||
return result;
|
var result = new char[length];
|
||||||
}
|
var max = chars.Length;
|
||||||
|
var limit = byte.MaxValue - (256 % max);
|
||||||
|
var buffer = new byte[1];
|
||||||
|
|
||||||
|
for (var i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
byte value;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
secureRng.GetBytes(buffer);
|
||||||
|
value = buffer[0];
|
||||||
|
}
|
||||||
|
while (value > limit);
|
||||||
|
|
||||||
|
result[i] = chars[value % max];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new string(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static Regex GetRouteRegex(string url)
|
public static Regex GetRouteRegex(string url)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ using Esiur.Protocol;
|
|||||||
|
|
||||||
namespace Esiur.Net.Http;
|
namespace Esiur.Net.Http;
|
||||||
|
|
||||||
public class EpOvwerWebsocket : HttpFilter
|
public class EpOverWebsocket : HttpFilter
|
||||||
{
|
{
|
||||||
//[Attribute]
|
//[Attribute]
|
||||||
public EpServer Server
|
public EpServer Server
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
|
|
||||||
Copyright (c) 2017 Ahmed Kh. Zamil
|
Copyright (c) 2017 Ahmed Kh. Zamil
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
@@ -23,229 +23,109 @@ SOFTWARE.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Net;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Esiur.Misc;
|
using Esiur.Misc;
|
||||||
using Esiur.Core;
|
using Esiur.Core;
|
||||||
using Esiur.Data;
|
|
||||||
using Esiur.Net.Sockets;
|
using Esiur.Net.Sockets;
|
||||||
using Esiur.Resource;
|
|
||||||
|
|
||||||
namespace Esiur.Net;
|
namespace Esiur.Net;
|
||||||
public abstract class NetworkConnection : IDestructible, INetworkReceiver<ISocket>// <TS>: IResource where TS : NetworkSession
|
|
||||||
{
|
|
||||||
private Sockets.ISocket sock;
|
|
||||||
// private bool connected;
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for a logical connection layered on top of an <see cref="ISocket"/>.
|
||||||
|
/// It owns the socket, forwards inbound buffers to <see cref="DataReceived"/>, and
|
||||||
|
/// exposes send helpers. Derived classes implement the protocol-specific framing.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class NetworkConnection : IDestructible, INetworkReceiver<ISocket>
|
||||||
|
{
|
||||||
|
private ISocket sock;
|
||||||
private DateTime lastAction;
|
private DateTime lastAction;
|
||||||
|
|
||||||
//public delegate void DataReceivedEvent(NetworkConnection sender, NetworkBuffer data);
|
// Re-entrancy guard for NetworkReceive. 0 = idle, 1 = a thread is draining the buffer.
|
||||||
//public delegate void ConnectionClosedEvent(NetworkConnection sender);
|
// Interlocked is used instead of a plain bool so concurrent receive callbacks cannot
|
||||||
|
// both enter the drain loop (which is not safe to run from two threads at once).
|
||||||
|
private int receiving;
|
||||||
|
|
||||||
public delegate void NetworkConnectionEvent(NetworkConnection connection);
|
public delegate void NetworkConnectionEvent(NetworkConnection connection);
|
||||||
|
|
||||||
public event NetworkConnectionEvent OnConnect;
|
public event NetworkConnectionEvent OnConnect;
|
||||||
//public event DataReceivedEvent OnDataReceived;
|
|
||||||
public event NetworkConnectionEvent OnClose;
|
public event NetworkConnectionEvent OnClose;
|
||||||
|
|
||||||
|
|
||||||
public event DestroyedEvent OnDestroy;
|
public event DestroyedEvent OnDestroy;
|
||||||
//object receivingLock = new object();
|
|
||||||
|
|
||||||
//object sendLock = new object();
|
|
||||||
|
|
||||||
bool processing = false;
|
|
||||||
|
|
||||||
// public INetworkReceiver<NetworkConnection> Receiver { get; set; }
|
|
||||||
|
|
||||||
public virtual void Destroy()
|
public virtual void Destroy()
|
||||||
{
|
{
|
||||||
// remove references
|
|
||||||
//sock.OnClose -= Socket_OnClose;
|
|
||||||
//sock.OnConnect -= Socket_OnConnect;
|
|
||||||
//sock.OnReceive -= Socket_OnReceive;
|
|
||||||
sock?.Destroy();
|
sock?.Destroy();
|
||||||
//Receiver = null;
|
|
||||||
Close();
|
Close();
|
||||||
sock = null;
|
sock = null;
|
||||||
|
|
||||||
OnClose = null;
|
OnClose = null;
|
||||||
OnConnect = null;
|
OnConnect = null;
|
||||||
//OnDataReceived = null;
|
|
||||||
OnDestroy?.Invoke(this);
|
OnDestroy?.Invoke(this);
|
||||||
OnDestroy = null;
|
OnDestroy = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ISocket Socket
|
public ISocket Socket => sock;
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return sock;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void Assign(ISocket socket)
|
public virtual void Assign(ISocket socket)
|
||||||
{
|
{
|
||||||
lastAction = DateTime.Now;
|
lastAction = DateTime.Now;
|
||||||
sock = socket;
|
sock = socket;
|
||||||
sock.Receiver = this;
|
sock.Receiver = this;
|
||||||
|
|
||||||
//socket.OnReceive += Socket_OnReceive;
|
|
||||||
//socket.OnClose += Socket_OnClose;
|
|
||||||
//socket.OnConnect += Socket_OnConnect;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//private void Socket_OnConnect()
|
/// <summary>
|
||||||
//{
|
/// Detaches the socket from this connection without closing it and returns it,
|
||||||
// OnConnect?.Invoke(this);
|
/// so ownership can be handed to another connection (e.g. a protocol upgrade).
|
||||||
//}
|
/// </summary>
|
||||||
|
|
||||||
//private void Socket_OnClose()
|
|
||||||
//{
|
|
||||||
// ConnectionClosed();
|
|
||||||
// OnClose?.Invoke(this);
|
|
||||||
//}
|
|
||||||
|
|
||||||
//protected virtual void ConnectionClosed()
|
|
||||||
//{
|
|
||||||
|
|
||||||
//}
|
|
||||||
|
|
||||||
//private void Socket_OnReceive(NetworkBuffer buffer)
|
|
||||||
//{
|
|
||||||
//}
|
|
||||||
|
|
||||||
public ISocket Unassign()
|
public ISocket Unassign()
|
||||||
{
|
{
|
||||||
if (sock != null)
|
if (sock == null)
|
||||||
{
|
|
||||||
// connected = false;
|
|
||||||
//sock.OnClose -= Socket_OnClose;
|
|
||||||
//sock.OnConnect -= Socket_OnConnect;
|
|
||||||
//sock.OnReceive -= Socket_OnReceive;
|
|
||||||
sock.Receiver = null;
|
|
||||||
|
|
||||||
var rt = sock;
|
|
||||||
sock = null;
|
|
||||||
|
|
||||||
return rt;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
//protected virtual void DataReceived(NetworkBuffer data)
|
sock.Receiver = null;
|
||||||
//{
|
|
||||||
// if (OnDataReceived != null)
|
var detached = sock;
|
||||||
// {
|
sock = null;
|
||||||
// try
|
return detached;
|
||||||
// {
|
}
|
||||||
// OnDataReceived?.Invoke(this, data);
|
|
||||||
// }
|
|
||||||
// catch (Exception ex)
|
|
||||||
// {
|
|
||||||
// Global.Log("NetworkConenction:DataReceived", LogType.Error, ex.ToString());
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
public void Close()
|
public void Close()
|
||||||
{
|
{
|
||||||
//if (!connected)
|
|
||||||
// return;
|
|
||||||
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (sock != null)
|
sock?.Close();
|
||||||
sock.Close();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Global.Log("NetworkConenction:Close", LogType.Error, ex.ToString());
|
Global.Log("NetworkConnection:Close", LogType.Error, ex.ToString());
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//finally
|
|
||||||
//{
|
|
||||||
//connected = false;
|
|
||||||
//}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public DateTime LastAction
|
|
||||||
{
|
|
||||||
get { return lastAction; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public IPEndPoint RemoteEndPoint
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (sock != null)
|
|
||||||
return (IPEndPoint)sock.RemoteEndPoint;
|
|
||||||
else
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public IPEndPoint LocalEndPoint
|
public DateTime LastAction => lastAction;
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (sock != null)
|
|
||||||
return (IPEndPoint)sock.LocalEndPoint;
|
|
||||||
else
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public IPEndPoint RemoteEndPoint => sock != null ? (IPEndPoint)sock.RemoteEndPoint : null;
|
||||||
|
|
||||||
public bool IsConnected
|
public IPEndPoint LocalEndPoint => sock != null ? (IPEndPoint)sock.LocalEndPoint : null;
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return sock == null ? false : sock.State == SocketState.Established;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public bool IsConnected => sock != null && sock.State == SocketState.Established;
|
||||||
/*
|
|
||||||
public void CloseAndWait()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!connected)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (sock != null)
|
|
||||||
sock.Close();
|
|
||||||
|
|
||||||
while (connected)
|
|
||||||
{
|
|
||||||
Thread.Sleep(100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
public virtual AsyncReply<bool> SendAsync(byte[] message, int offset, int length)
|
public virtual AsyncReply<bool> SendAsync(byte[] message, int offset, int length)
|
||||||
{
|
{
|
||||||
|
var socket = sock;
|
||||||
|
if (socket == null)
|
||||||
|
return new AsyncReply<bool>(false);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
lastAction = DateTime.Now;
|
lastAction = DateTime.Now;
|
||||||
return sock.SendAsync(message, offset, length);
|
return socket.SendAsync(message, offset, length);
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
// Sends fail routinely when the peer drops, so this is logged at debug level
|
||||||
|
// rather than thrown, but it is no longer swallowed silently.
|
||||||
|
Global.Log("NetworkConnection:SendAsync", LogType.Debug, ex.Message);
|
||||||
return new AsyncReply<bool>(false);
|
return new AsyncReply<bool>(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,9 +137,9 @@ public abstract class NetworkConnection : IDestructible, INetworkReceiver<ISocke
|
|||||||
sock?.Send(msg);
|
sock?.Send(msg);
|
||||||
lastAction = DateTime.Now;
|
lastAction = DateTime.Now;
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
Global.Log("NetworkConnection:Send", LogType.Debug, ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,12 +147,12 @@ public abstract class NetworkConnection : IDestructible, INetworkReceiver<ISocke
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
sock.Send(msg, offset, length);
|
sock?.Send(msg, offset, length);
|
||||||
lastAction = DateTime.Now;
|
lastAction = DateTime.Now;
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
Global.Log("NetworkConnection:Send", LogType.Debug, ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,18 +173,6 @@ public abstract class NetworkConnection : IDestructible, INetworkReceiver<ISocke
|
|||||||
OnConnect?.Invoke(this);
|
OnConnect?.Invoke(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
//{
|
|
||||||
//ConnectionClosed();
|
|
||||||
//OnClose?.Invoke(this);
|
|
||||||
|
|
||||||
//Receiver?.NetworkClose(this);
|
|
||||||
//}
|
|
||||||
|
|
||||||
//public void NetworkConenct(ISocket sender)
|
|
||||||
//{
|
|
||||||
// OnConnect?.Invoke(this);
|
|
||||||
//}
|
|
||||||
|
|
||||||
protected abstract void DataReceived(NetworkBuffer buffer);
|
protected abstract void DataReceived(NetworkBuffer buffer);
|
||||||
protected abstract void Connected();
|
protected abstract void Connected();
|
||||||
protected abstract void Disconnected();
|
protected abstract void Disconnected();
|
||||||
@@ -313,53 +181,30 @@ public abstract class NetworkConnection : IDestructible, INetworkReceiver<ISocke
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Unassigned ?
|
// Ignore callbacks once the socket is unassigned or closed.
|
||||||
if (sock == null)
|
if (sock == null || sock.State == SocketState.Closed)
|
||||||
return;
|
|
||||||
|
|
||||||
// Closed ?
|
|
||||||
if (sock.State == SocketState.Closed)// || sock.State == SocketState.Terminated) // || !connected)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
lastAction = DateTime.Now;
|
lastAction = DateTime.Now;
|
||||||
|
|
||||||
if (!processing)
|
// Only one thread drains the buffer at a time; others return immediately and
|
||||||
|
// rely on the active drainer to pick up the newly appended data.
|
||||||
|
if (Interlocked.CompareExchange(ref receiving, 1, 0) != 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
processing = true;
|
while (buffer.Available > 0 && !buffer.Protected)
|
||||||
|
DataReceived(buffer);
|
||||||
try
|
}
|
||||||
{
|
finally
|
||||||
//lock(buffer.SyncLock)
|
{
|
||||||
while (buffer.Available > 0 && !buffer.Protected)
|
Interlocked.Exchange(ref receiving, 0);
|
||||||
{
|
|
||||||
//Receiver?.NetworkReceive(this, buffer);
|
|
||||||
DataReceived(buffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
processing = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Global.Log("NetworkConnection", LogType.Warning, ex.ToString());
|
Global.Log("NetworkConnection:NetworkReceive", LogType.Warning, ex.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//{
|
|
||||||
// Receiver?.NetworkError(this);
|
|
||||||
//throw new NotImplementedException();
|
|
||||||
//}
|
|
||||||
|
|
||||||
//public void NetworkConnect(ISocket sender)
|
|
||||||
//{
|
|
||||||
// Receiver?.NetworkConnect(this);
|
|
||||||
//throw new NotImplementedException();
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,8 +187,6 @@ public class EpAuthPacket : Packet
|
|||||||
|
|
||||||
Tdu = PlainTdu.Parse(data, offset, ends);//, _warehouse);
|
Tdu = PlainTdu.Parse(data, offset, ends);//, _warehouse);
|
||||||
|
|
||||||
Console.WriteLine("Auth TDU " + Tdu.Value.PayloadLength);
|
|
||||||
|
|
||||||
if (Tdu.Value.Class == TduClass.Invalid)
|
if (Tdu.Value.Class == TduClass.Invalid)
|
||||||
return -(int)Tdu.Value.TotalLength;
|
return -(int)Tdu.Value.TotalLength;
|
||||||
|
|
||||||
|
|||||||
@@ -156,12 +156,18 @@ namespace Esiur.Net.Sockets
|
|||||||
|
|
||||||
public void Destroy()
|
public void Destroy()
|
||||||
{
|
{
|
||||||
Close();
|
var ws = sock;
|
||||||
|
|
||||||
|
Close(); // best-effort graceful close handshake (fire-and-forget)
|
||||||
|
|
||||||
receiveNetworkBuffer = null;
|
receiveNetworkBuffer = null;
|
||||||
Receiver = null;
|
Receiver = null;
|
||||||
|
|
||||||
sock = null;
|
sock = null;
|
||||||
|
|
||||||
|
// Dispose the WebSocket so its buffers and handle are released; Close() only
|
||||||
|
// starts the async close handshake and never disposes.
|
||||||
|
try { ws?.Dispose(); } catch { }
|
||||||
|
|
||||||
OnDestroy?.Invoke(this);
|
OnDestroy?.Invoke(this);
|
||||||
OnDestroy = null;
|
OnDestroy = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -458,6 +458,14 @@ public class SSLSocket : ISocket
|
|||||||
public void Destroy()
|
public void Destroy()
|
||||||
{
|
{
|
||||||
Close();
|
Close();
|
||||||
|
|
||||||
|
// Release the TLS stream and the underlying socket handle. NetworkStream(sock) does
|
||||||
|
// not own the socket, so disposing the stream alone would leak the socket — dispose
|
||||||
|
// both explicitly. Guarded because teardown may race with in-flight I/O callbacks.
|
||||||
|
try { ssl?.Dispose(); } catch { }
|
||||||
|
try { sock?.Close(); } catch { }
|
||||||
|
try { sock?.Dispose(); } catch { }
|
||||||
|
|
||||||
Receiver = null;
|
Receiver = null;
|
||||||
receiveNetworkBuffer = null;
|
receiveNetworkBuffer = null;
|
||||||
OnDestroy?.Invoke(this);
|
OnDestroy?.Invoke(this);
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ public partial class EpConnection : NetworkConnection, IStore
|
|||||||
|
|
||||||
AsyncReply<bool> _openReply;
|
AsyncReply<bool> _openReply;
|
||||||
|
|
||||||
bool _authenticated, _readyToEstablish;
|
bool _authenticated;
|
||||||
|
|
||||||
string _hostname;
|
string _hostname;
|
||||||
ushort _port;
|
ushort _port;
|
||||||
@@ -2025,7 +2025,6 @@ public partial class EpConnection : NetworkConnection, IStore
|
|||||||
{
|
{
|
||||||
// clean up
|
// clean up
|
||||||
_authenticated = false;
|
_authenticated = false;
|
||||||
_readyToEstablish = false;
|
|
||||||
Status = EpConnectionStatus.Closed;
|
Status = EpConnectionStatus.Closed;
|
||||||
|
|
||||||
_keepAliveTimer.Stop();
|
_keepAliveTimer.Stop();
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ partial class EpConnection
|
|||||||
var bl = new BinaryList();
|
var bl = new BinaryList();
|
||||||
bl.AddUInt8((byte)(0x60 | (byte)action))
|
bl.AddUInt8((byte)(0x60 | (byte)action))
|
||||||
.AddUInt32(c)
|
.AddUInt32(c)
|
||||||
.AddUInt8Array(Codec.Compose(args[0], this.Instance.Warehouse, this));
|
.AddUInt8Array(Codec.Compose(args[0], this.Instance?.Warehouse ?? _serverWarehouse, this));
|
||||||
Send(bl.ToArray());
|
Send(bl.ToArray());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -116,7 +116,7 @@ partial class EpConnection
|
|||||||
var bl = new BinaryList();
|
var bl = new BinaryList();
|
||||||
bl.AddUInt8((byte)(0x60 | (byte)action))
|
bl.AddUInt8((byte)(0x60 | (byte)action))
|
||||||
.AddUInt32(c)
|
.AddUInt32(c)
|
||||||
.AddUInt8Array(Codec.Compose(args, this.Instance.Warehouse, this));
|
.AddUInt8Array(Codec.Compose(args, this.Instance?.Warehouse ?? _serverWarehouse, this));
|
||||||
Send(bl.ToArray());
|
Send(bl.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ partial class EpConnection
|
|||||||
{
|
{
|
||||||
var bl = new BinaryList();
|
var bl = new BinaryList();
|
||||||
bl.AddUInt8((byte)((byte)method | 0x20));
|
bl.AddUInt8((byte)((byte)method | 0x20));
|
||||||
bl.AddUInt8Array(Codec.Compose(data, null, this));
|
bl.AddUInt8Array(Codec.Compose(data, this.Instance?.Warehouse ?? _serverWarehouse, this));
|
||||||
Send(bl.ToArray());
|
Send(bl.ToArray());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -165,7 +165,7 @@ partial class EpConnection
|
|||||||
{
|
{
|
||||||
var bl = new BinaryList();
|
var bl = new BinaryList();
|
||||||
bl.AddUInt8((byte)((byte)method | 0x20));
|
bl.AddUInt8((byte)((byte)method | 0x20));
|
||||||
bl.AddUInt8Array(Codec.Compose(message, null, this));
|
bl.AddUInt8Array(Codec.Compose(message, this.Instance?.Warehouse ?? _serverWarehouse, this));
|
||||||
Send(bl.ToArray());
|
Send(bl.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +183,9 @@ partial class EpConnection
|
|||||||
|
|
||||||
var bl = new BinaryList();
|
var bl = new BinaryList();
|
||||||
bl.AddUInt8((byte)((byte)method | 0x20));
|
bl.AddUInt8((byte)((byte)method | 0x20));
|
||||||
bl.AddUInt8Array(Codec.Compose(authHeaders, null, this));
|
bl.AddUInt8Array(Codec.Compose(authHeaders,
|
||||||
|
this.Instance?.Warehouse ?? _serverWarehouse,
|
||||||
|
this));
|
||||||
Send(bl.ToArray());
|
Send(bl.ToArray());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -212,14 +214,14 @@ partial class EpConnection
|
|||||||
{
|
{
|
||||||
var bl = new BinaryList();
|
var bl = new BinaryList();
|
||||||
bl.AddUInt8((byte)(0x20 | (byte)action))
|
bl.AddUInt8((byte)(0x20 | (byte)action))
|
||||||
.AddUInt8Array(Codec.Compose(args[0], this.Instance.Warehouse, this));
|
.AddUInt8Array(Codec.Compose(args[0], this.Instance?.Warehouse ?? _serverWarehouse, this));
|
||||||
Send(bl.ToArray());
|
Send(bl.ToArray());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var bl = new BinaryList();
|
var bl = new BinaryList();
|
||||||
bl.AddUInt8((byte)(0x20 | (byte)action))
|
bl.AddUInt8((byte)(0x20 | (byte)action))
|
||||||
.AddUInt8Array(Codec.Compose(args, this.Instance.Warehouse, this));
|
.AddUInt8Array(Codec.Compose(args, this.Instance?.Warehouse ?? _serverWarehouse, this));
|
||||||
Send(bl.ToArray());
|
Send(bl.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +245,7 @@ partial class EpConnection
|
|||||||
var bl = new BinaryList();
|
var bl = new BinaryList();
|
||||||
bl.AddUInt8((byte)(0xA0 | (byte)action))
|
bl.AddUInt8((byte)(0xA0 | (byte)action))
|
||||||
.AddUInt32(callbackId)
|
.AddUInt32(callbackId)
|
||||||
.AddUInt8Array(Codec.Compose(args[0], this.Instance.Warehouse, this));
|
.AddUInt8Array(Codec.Compose(args[0], this.Instance?.Warehouse ?? _serverWarehouse, this));
|
||||||
Send(bl.ToArray());
|
Send(bl.ToArray());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -251,7 +253,7 @@ partial class EpConnection
|
|||||||
var bl = new BinaryList();
|
var bl = new BinaryList();
|
||||||
bl.AddUInt8((byte)(0xA0 | (byte)action))
|
bl.AddUInt8((byte)(0xA0 | (byte)action))
|
||||||
.AddUInt32(callbackId)
|
.AddUInt32(callbackId)
|
||||||
.AddUInt8Array(Codec.Compose(args, this.Instance.Warehouse, this));
|
.AddUInt8Array(Codec.Compose(args, this.Instance?.Warehouse ?? _serverWarehouse, this));
|
||||||
Send(bl.ToArray());
|
Send(bl.ToArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2033,7 +2035,10 @@ partial class EpConnection
|
|||||||
_attachedResources[id]?.TryGetTarget(out resource);
|
_attachedResources[id]?.TryGetTarget(out resource);
|
||||||
|
|
||||||
if (resource != null)
|
if (resource != null)
|
||||||
|
{
|
||||||
|
Global.Counters["EpResourceAttachedCacheHit"]++;
|
||||||
return new AsyncReply<EpResource>(resource);
|
return new AsyncReply<EpResource>(resource);
|
||||||
|
}
|
||||||
|
|
||||||
resource = _neededResources[id];
|
resource = _neededResources[id];
|
||||||
|
|
||||||
@@ -2043,16 +2048,19 @@ partial class EpConnection
|
|||||||
{
|
{
|
||||||
if (resource != null && (requestSequence?.Contains(id) ?? false))
|
if (resource != null && (requestSequence?.Contains(id) ?? false))
|
||||||
{
|
{
|
||||||
|
Global.Counters["EpResourceDeadLockSameChain"]++;
|
||||||
// dead lock avoidance for loop reference.
|
// dead lock avoidance for loop reference.
|
||||||
return new AsyncReply<EpResource>(resource);
|
return new AsyncReply<EpResource>(resource);
|
||||||
}
|
}
|
||||||
else if (resource != null && requestInfo.RequestSequence.Contains(id))
|
else if (resource != null && requestInfo.RequestSequence.Contains(id))
|
||||||
{
|
{
|
||||||
|
Global.Counters["EpResourceDeadLockCrossChain"]++;
|
||||||
// dead lock avoidance for dependent reference.
|
// dead lock avoidance for dependent reference.
|
||||||
return new AsyncReply<EpResource>(resource);
|
return new AsyncReply<EpResource>(resource);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
Global.Counters["EpResourcePendingCacheHit"]++;
|
||||||
return requestInfo.Reply;
|
return requestInfo.Reply;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,12 @@ public class Warehouse
|
|||||||
ConcurrentDictionary<uint, WeakReference<IResource>> _resources = new ConcurrentDictionary<uint, WeakReference<IResource>>();
|
ConcurrentDictionary<uint, WeakReference<IResource>> _resources = new ConcurrentDictionary<uint, WeakReference<IResource>>();
|
||||||
ConcurrentDictionary<IStore, List<WeakReference<IResource>>> _stores = new ConcurrentDictionary<IStore, List<WeakReference<IResource>>>();
|
ConcurrentDictionary<IStore, List<WeakReference<IResource>>> _stores = new ConcurrentDictionary<IStore, List<WeakReference<IResource>>>();
|
||||||
|
|
||||||
|
// Memoizes Tru.FromType results, which are reflection-heavy and recomputed for every
|
||||||
|
// array element, record property and tuple field during serialization. Tru instances
|
||||||
|
// are immutable once constructed, so caching and sharing them per warehouse is safe.
|
||||||
|
internal ConcurrentDictionary<Type, Esiur.Data.Tru> TypeRepresentationCache
|
||||||
|
= new ConcurrentDictionary<Type, Esiur.Data.Tru>();
|
||||||
|
|
||||||
volatile int _resourceCounter = 0;
|
volatile int _resourceCounter = 0;
|
||||||
volatile int _typeDefsCounter = 0;
|
volatile int _typeDefsCounter = 0;
|
||||||
|
|
||||||
@@ -681,14 +687,10 @@ public class Warehouse
|
|||||||
|| baseType == typeof(IRecord))
|
|| baseType == typeof(IRecord))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
TypeDefKind typeDefKind;
|
// Only resources, records and enums have type definitions; bail out for anything else.
|
||||||
if (Codec.ImplementsInterface(type, typeof(IResource)))
|
if (!Codec.ImplementsInterface(type, typeof(IResource))
|
||||||
typeDefKind = TypeDefKind.Resource;
|
&& !Codec.ImplementsInterface(type, typeof(IRecord))
|
||||||
else if (Codec.ImplementsInterface(type, typeof(IRecord)))
|
&& !type.IsEnum)
|
||||||
typeDefKind = TypeDefKind.Record;
|
|
||||||
else if (type.IsEnum)
|
|
||||||
typeDefKind = TypeDefKind.Enum;
|
|
||||||
else
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
lock (_typeDefsLock)
|
lock (_typeDefsLock)
|
||||||
|
|||||||
@@ -9,10 +9,20 @@ using Esiur.Data.Types;
|
|||||||
|
|
||||||
namespace Esiur.Security.Authority.Providers
|
namespace Esiur.Security.Authority.Providers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implements the "hash" authentication protocol: a SHA3 nonce/challenge-response
|
||||||
|
/// handshake that mutually proves knowledge of a salted password hash without sending
|
||||||
|
/// the password, and derives a 512-bit session key. Supports initiator-only, responder-only
|
||||||
|
/// and dual identity modes. All challenge comparisons are constant-time and remote material
|
||||||
|
/// is validated, so malformed peer input fails the handshake closed rather than throwing.
|
||||||
|
/// </summary>
|
||||||
public class PasswordAuthenticationHandler : IAuthenticationHandler
|
public class PasswordAuthenticationHandler : IAuthenticationHandler
|
||||||
{
|
{
|
||||||
public string Protocol => "hash";
|
public string Protocol => "hash";
|
||||||
|
|
||||||
|
// Length, in bytes, of the random nonces exchanged during the handshake.
|
||||||
|
// Remote nonces are validated against this to reject malformed or weak input.
|
||||||
|
const int NonceLength = 20;
|
||||||
|
|
||||||
byte[] _localNonce, _remoteNonce;
|
byte[] _localNonce, _remoteNonce;
|
||||||
byte[] _localSalt, _remoteSalt;
|
byte[] _localSalt, _remoteSalt;
|
||||||
@@ -33,6 +43,18 @@ namespace Esiur.Security.Authority.Providers
|
|||||||
public IAuthenticationProvider Provider => _provider;
|
public IAuthenticationProvider Provider => _provider;
|
||||||
|
|
||||||
|
|
||||||
|
// Constant-time comparison of two byte arrays. Used for all challenge/MAC
|
||||||
|
// checks so that, unlike SequenceEqual, the time taken does not depend on how
|
||||||
|
// many leading bytes matched — closing a timing side channel on the secret.
|
||||||
|
// Returns false for null or length-mismatched inputs (challenges are fixed size).
|
||||||
|
static bool FixedTimeEquals(byte[] a, byte[] b)
|
||||||
|
{
|
||||||
|
if (a == null || b == null || a.Length != b.Length)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Org.BouncyCastle.Utilities.Arrays.FixedTimeEquals(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
public static byte[] ComputeSha3(byte[] data, int bitLength = 256)
|
public static byte[] ComputeSha3(byte[] data, int bitLength = 256)
|
||||||
{
|
{
|
||||||
// 1. Initialize the digest (supports 224, 256, 384, 512)
|
// 1. Initialize the digest (supports 224, 256, 384, 512)
|
||||||
@@ -50,7 +72,22 @@ namespace Esiur.Security.Authority.Providers
|
|||||||
|
|
||||||
public AuthenticationResult Process(object authData)
|
public AuthenticationResult Process(object authData)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"PasswordAuthenticationHandler: {this.GetHashCode()} Step {_step}, Mode {_mode}, Direction {_direction}");
|
// Process runs at the trust boundary on data supplied by a remote peer.
|
||||||
|
// Any malformed input (wrong types, null or short fields) must fail the
|
||||||
|
// handshake instead of throwing, so the exchange is wrapped to fail closed.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return ProcessInternal(authData);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_step = -1;
|
||||||
|
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthenticationResult ProcessInternal(object authData)
|
||||||
|
{
|
||||||
var remoteAuthData = (object[])authData;
|
var remoteAuthData = (object[])authData;
|
||||||
var localAuthData = new List<object>();
|
var localAuthData = new List<object>();
|
||||||
|
|
||||||
@@ -97,7 +134,7 @@ namespace Esiur.Security.Authority.Providers
|
|||||||
var remoteChallenge = (byte[])remoteAuthData[2];
|
var remoteChallenge = (byte[])remoteAuthData[2];
|
||||||
|
|
||||||
// prevent reply attack by checking if remote nonce is same as local nonce.
|
// prevent reply attack by checking if remote nonce is same as local nonce.
|
||||||
if (_remoteNonce.SequenceEqual(_localNonce))
|
if (_remoteNonce == null || _remoteNonce.Length != NonceLength || FixedTimeEquals(_remoteNonce, _localNonce))
|
||||||
{
|
{
|
||||||
_step = -1;
|
_step = -1;
|
||||||
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
||||||
@@ -112,7 +149,7 @@ namespace Esiur.Security.Authority.Providers
|
|||||||
.ToArray());
|
.ToArray());
|
||||||
|
|
||||||
// compare remote challenge
|
// compare remote challenge
|
||||||
if (!remoteChallenge.SequenceEqual(expectedRemoteChallenge))
|
if (!FixedTimeEquals(remoteChallenge, expectedRemoteChallenge))
|
||||||
{
|
{
|
||||||
_step = -1;
|
_step = -1;
|
||||||
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
||||||
@@ -160,7 +197,7 @@ namespace Esiur.Security.Authority.Providers
|
|||||||
_responderIdentity = (string)remoteAuthData[1];
|
_responderIdentity = (string)remoteAuthData[1];
|
||||||
|
|
||||||
// prevent reply attack by checking if remote nonce is same as local nonce.
|
// prevent reply attack by checking if remote nonce is same as local nonce.
|
||||||
if (_remoteNonce.SequenceEqual(_localNonce))
|
if (_remoteNonce == null || _remoteNonce.Length != NonceLength || FixedTimeEquals(_remoteNonce, _localNonce))
|
||||||
{
|
{
|
||||||
_step = -1;
|
_step = -1;
|
||||||
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
||||||
@@ -203,7 +240,7 @@ namespace Esiur.Security.Authority.Providers
|
|||||||
.Concat(_localNonce)
|
.Concat(_localNonce)
|
||||||
.ToArray());
|
.ToArray());
|
||||||
|
|
||||||
if (!remoteChallenge.SequenceEqual(expectedRemoteChallenge))
|
if (!FixedTimeEquals(remoteChallenge, expectedRemoteChallenge))
|
||||||
{
|
{
|
||||||
_step = -1;
|
_step = -1;
|
||||||
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
||||||
@@ -262,7 +299,7 @@ namespace Esiur.Security.Authority.Providers
|
|||||||
_remoteSalt = (byte[])remoteAuthData[2];
|
_remoteSalt = (byte[])remoteAuthData[2];
|
||||||
|
|
||||||
// prevent reply attack by checking if remote nonce is same as local nonce.
|
// prevent reply attack by checking if remote nonce is same as local nonce.
|
||||||
if (_remoteNonce.SequenceEqual(_localNonce))
|
if (_remoteNonce == null || _remoteNonce.Length != NonceLength || FixedTimeEquals(_remoteNonce, _localNonce))
|
||||||
{
|
{
|
||||||
_step = -1;
|
_step = -1;
|
||||||
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
||||||
@@ -313,7 +350,7 @@ namespace Esiur.Security.Authority.Providers
|
|||||||
.Concat(_localNonce)
|
.Concat(_localNonce)
|
||||||
.ToArray());
|
.ToArray());
|
||||||
|
|
||||||
if (!remoteChallenge.SequenceEqual(expectedRemoteChallenge))
|
if (!FixedTimeEquals(remoteChallenge, expectedRemoteChallenge))
|
||||||
{
|
{
|
||||||
_step = -1;
|
_step = -1;
|
||||||
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
||||||
@@ -361,7 +398,7 @@ namespace Esiur.Security.Authority.Providers
|
|||||||
|
|
||||||
// prevent reply attack by checking if remote nonce is same as local nonce.
|
// prevent reply attack by checking if remote nonce is same as local nonce.
|
||||||
// @TODO: We can change our localNonce then send it
|
// @TODO: We can change our localNonce then send it
|
||||||
if (_remoteNonce.SequenceEqual(_localNonce))
|
if (_remoteNonce == null || _remoteNonce.Length != NonceLength || FixedTimeEquals(_remoteNonce, _localNonce))
|
||||||
{
|
{
|
||||||
_step = -1;
|
_step = -1;
|
||||||
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
||||||
@@ -403,7 +440,7 @@ namespace Esiur.Security.Authority.Providers
|
|||||||
.Concat(_localNonce)
|
.Concat(_localNonce)
|
||||||
.ToArray());
|
.ToArray());
|
||||||
// compare remote challenge
|
// compare remote challenge
|
||||||
if (!expectedRemoteChallenge.SequenceEqual(remoteChallenge))
|
if (!FixedTimeEquals(expectedRemoteChallenge, remoteChallenge))
|
||||||
{
|
{
|
||||||
_step = -1;
|
_step = -1;
|
||||||
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
||||||
@@ -442,7 +479,7 @@ namespace Esiur.Security.Authority.Providers
|
|||||||
|
|
||||||
// prevent reply attack by checking if remote nonce is same as local nonce.
|
// prevent reply attack by checking if remote nonce is same as local nonce.
|
||||||
// @TODO: We can change our localNonce then send it
|
// @TODO: We can change our localNonce then send it
|
||||||
if (_remoteNonce.SequenceEqual(_localNonce))
|
if (_remoteNonce == null || _remoteNonce.Length != NonceLength || FixedTimeEquals(_remoteNonce, _localNonce))
|
||||||
{
|
{
|
||||||
_step = -1;
|
_step = -1;
|
||||||
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
||||||
@@ -490,7 +527,7 @@ namespace Esiur.Security.Authority.Providers
|
|||||||
.ToArray());
|
.ToArray());
|
||||||
|
|
||||||
// compare remote challenge
|
// compare remote challenge
|
||||||
if (!expectedRemoteChallenge.SequenceEqual(remoteChallenge))
|
if (!FixedTimeEquals(expectedRemoteChallenge, remoteChallenge))
|
||||||
{
|
{
|
||||||
_step = -1;
|
_step = -1;
|
||||||
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
||||||
@@ -534,7 +571,7 @@ namespace Esiur.Security.Authority.Providers
|
|||||||
|
|
||||||
// prevent reply attack by checking if remote nonce is same as local nonce.
|
// prevent reply attack by checking if remote nonce is same as local nonce.
|
||||||
// @TODO: We can change our localNonce then send it
|
// @TODO: We can change our localNonce then send it
|
||||||
if (_remoteNonce.SequenceEqual(_localNonce))
|
if (_remoteNonce == null || _remoteNonce.Length != NonceLength || FixedTimeEquals(_remoteNonce, _localNonce))
|
||||||
{
|
{
|
||||||
_step = -1;
|
_step = -1;
|
||||||
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
||||||
@@ -597,7 +634,7 @@ namespace Esiur.Security.Authority.Providers
|
|||||||
.ToArray());
|
.ToArray());
|
||||||
|
|
||||||
// compare remote challenge
|
// compare remote challenge
|
||||||
if (!expectedRemoteChallenge.SequenceEqual(remoteChallenge))
|
if (!FixedTimeEquals(expectedRemoteChallenge, remoteChallenge))
|
||||||
{
|
{
|
||||||
_step = -1;
|
_step = -1;
|
||||||
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
return new AuthenticationResult(AuthenticationRuling.Failed, null);
|
||||||
@@ -646,7 +683,7 @@ namespace Esiur.Security.Authority.Providers
|
|||||||
string domain,
|
string domain,
|
||||||
PasswordAuthenticationProvider provider)
|
PasswordAuthenticationProvider provider)
|
||||||
{
|
{
|
||||||
_localNonce = Global.GenerateBytes(20);
|
_localNonce = Global.GenerateBytes(NonceLength);
|
||||||
|
|
||||||
this._provider = provider;
|
this._provider = provider;
|
||||||
this._initiatorIdentity = initiatorIdentity;
|
this._initiatorIdentity = initiatorIdentity;
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using Esiur.Security.Authority;
|
||||||
|
using Esiur.Security.Authority.Providers;
|
||||||
|
|
||||||
|
namespace Esiur.Tests.Unit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Drives a pair of <see cref="PasswordAuthenticationHandler"/> instances (initiator and
|
||||||
|
/// responder) through the SHA3 challenge/response handshake and asserts both the happy
|
||||||
|
/// path (matching session keys) and the security hardening (constant-time challenge
|
||||||
|
/// checks, nonce validation, fail-closed behaviour on malformed peer input).
|
||||||
|
/// </summary>
|
||||||
|
public class AuthHandshakeTests
|
||||||
|
{
|
||||||
|
// ---- test credential store -------------------------------------------------------
|
||||||
|
|
||||||
|
class TestAccount
|
||||||
|
{
|
||||||
|
public string Identity;
|
||||||
|
public byte[] RawPassword;
|
||||||
|
public byte[] Salt;
|
||||||
|
public byte[] Hash; // SHA3-256(RawPassword || Salt), exactly what the verifier stores
|
||||||
|
}
|
||||||
|
|
||||||
|
static readonly byte[] FixedSalt = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5, 6 };
|
||||||
|
|
||||||
|
static TestAccount MakeAccount(string identity, string password)
|
||||||
|
{
|
||||||
|
var raw = Encoding.UTF8.GetBytes(password);
|
||||||
|
var hash = PasswordAuthenticationHandler.ComputeSha3(raw.Concat(FixedSalt).ToArray());
|
||||||
|
return new TestAccount { Identity = identity, RawPassword = raw, Salt = FixedSalt, Hash = hash };
|
||||||
|
}
|
||||||
|
|
||||||
|
class StubProvider : PasswordAuthenticationProvider
|
||||||
|
{
|
||||||
|
readonly Dictionary<string, TestAccount> _accounts;
|
||||||
|
readonly string _self;
|
||||||
|
|
||||||
|
public StubProvider(string self, params TestAccount[] accounts)
|
||||||
|
{
|
||||||
|
_self = self;
|
||||||
|
_accounts = accounts.ToDictionary(a => a.Identity, a => a);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IdentityPassword GetSelfIdentityAndCredential(string domain, string hostname)
|
||||||
|
=> new IdentityPassword(_self, _accounts[_self].RawPassword);
|
||||||
|
|
||||||
|
public override byte[] GetSelfCredential(string identity, string domain, string hostname)
|
||||||
|
=> _accounts.TryGetValue(identity, out var a) ? a.RawPassword : null;
|
||||||
|
|
||||||
|
public override PasswordHash GetHostedAccountCredential(string identity, string domain)
|
||||||
|
=> _accounts.TryGetValue(identity, out var a)
|
||||||
|
? new PasswordHash(a.Hash, a.Salt)
|
||||||
|
: new PasswordHash(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- helpers ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
static object[] DataOf(AuthenticationResult result)
|
||||||
|
=> ((List<object>)result.AuthenticationData)?.ToArray();
|
||||||
|
|
||||||
|
static byte[] PrivateNonce(PasswordAuthenticationHandler handler)
|
||||||
|
=> (byte[])typeof(PasswordAuthenticationHandler)
|
||||||
|
.GetField("_localNonce", BindingFlags.NonPublic | BindingFlags.Instance)
|
||||||
|
.GetValue(handler);
|
||||||
|
|
||||||
|
static (PasswordAuthenticationHandler init, PasswordAuthenticationHandler resp) NewPair(string identity = "alice")
|
||||||
|
{
|
||||||
|
var account = MakeAccount(identity, "correct horse battery staple");
|
||||||
|
var initiator = new PasswordAuthenticationHandler(
|
||||||
|
AuthenticationMode.InitializerIdentity, AuthenticationDirection.Initiator,
|
||||||
|
identity, null, "host", "domain", new StubProvider(identity, account));
|
||||||
|
var responder = new PasswordAuthenticationHandler(
|
||||||
|
AuthenticationMode.InitializerIdentity, AuthenticationDirection.Responder,
|
||||||
|
null, null, "host", "domain", new StubProvider(identity, account));
|
||||||
|
return (initiator, responder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- happy path ------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InitializerIdentity_Handshake_Derives_Matching_SessionKeys()
|
||||||
|
{
|
||||||
|
var (init, resp) = NewPair();
|
||||||
|
|
||||||
|
var r1 = init.Process(null); // -> [initNonce, initIdentity]
|
||||||
|
Assert.Equal(AuthenticationRuling.InProgress, r1.Ruling);
|
||||||
|
|
||||||
|
var r2 = resp.Process(DataOf(r1)); // -> [respNonce, respSalt, respChallenge]
|
||||||
|
Assert.Equal(AuthenticationRuling.InProgress, r2.Ruling);
|
||||||
|
|
||||||
|
var r3 = init.Process(DataOf(r2)); // -> [initChallenge], Succeeded
|
||||||
|
Assert.Equal(AuthenticationRuling.Succeeded, r3.Ruling);
|
||||||
|
|
||||||
|
var r4 = resp.Process(DataOf(r3)); // Succeeded
|
||||||
|
Assert.Equal(AuthenticationRuling.Succeeded, r4.Ruling);
|
||||||
|
|
||||||
|
Assert.NotNull(r3.SessionKey);
|
||||||
|
Assert.Equal(64, r3.SessionKey.Length); // 512-bit derived key
|
||||||
|
Assert.Equal(r3.SessionKey, r4.SessionKey); // both ends agree
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Wrong_Password_Fails()
|
||||||
|
{
|
||||||
|
var account = MakeAccount("alice", "the real password");
|
||||||
|
var init = new PasswordAuthenticationHandler(
|
||||||
|
AuthenticationMode.InitializerIdentity, AuthenticationDirection.Initiator,
|
||||||
|
"alice", null, "host", "domain",
|
||||||
|
new StubProvider("alice", MakeAccount("alice", "a different password")));
|
||||||
|
var resp = new PasswordAuthenticationHandler(
|
||||||
|
AuthenticationMode.InitializerIdentity, AuthenticationDirection.Responder,
|
||||||
|
null, null, "host", "domain", new StubProvider("alice", account));
|
||||||
|
|
||||||
|
var r1 = init.Process(null);
|
||||||
|
var r2 = resp.Process(DataOf(r1));
|
||||||
|
// Initiator validates the responder's challenge against its (wrong) password and bails.
|
||||||
|
var r3 = init.Process(DataOf(r2));
|
||||||
|
Assert.Equal(AuthenticationRuling.Failed, r3.Ruling);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- security properties ---------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Tampered_Challenge_Fails()
|
||||||
|
{
|
||||||
|
var (init, resp) = NewPair();
|
||||||
|
|
||||||
|
var r1 = init.Process(null);
|
||||||
|
var r2 = resp.Process(DataOf(r1));
|
||||||
|
var r3 = init.Process(DataOf(r2)); // [initChallenge]
|
||||||
|
|
||||||
|
var tampered = DataOf(r3);
|
||||||
|
((byte[])tampered[0])[0] ^= 0xFF; // flip a bit in the challenge
|
||||||
|
|
||||||
|
var r4 = resp.Process(tampered);
|
||||||
|
Assert.Equal(AuthenticationRuling.Failed, r4.Ruling);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Reflected_Nonce_Is_Rejected()
|
||||||
|
{
|
||||||
|
// Replay defence: feeding the responder its own nonce must be rejected.
|
||||||
|
var (init, resp) = NewPair();
|
||||||
|
var respNonce = PrivateNonce(resp);
|
||||||
|
|
||||||
|
var forged = new object[] { respNonce, "alice" };
|
||||||
|
var result = resp.Process(forged);
|
||||||
|
Assert.Equal(AuthenticationRuling.Failed, result.Ruling);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Short_Nonce_Is_Rejected()
|
||||||
|
{
|
||||||
|
var (_, resp) = NewPair();
|
||||||
|
var result = resp.Process(new object[] { new byte[5], "alice" });
|
||||||
|
Assert.Equal(AuthenticationRuling.Failed, result.Ruling);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0)] // empty
|
||||||
|
[InlineData(1)] // too few elements
|
||||||
|
public void Truncated_Input_Fails_Without_Throwing(int count)
|
||||||
|
{
|
||||||
|
var (_, resp) = NewPair();
|
||||||
|
var result = resp.Process(Enumerable.Range(0, count).Select(_ => (object)new byte[20]).ToArray());
|
||||||
|
Assert.Equal(AuthenticationRuling.Failed, result.Ruling);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Null_Input_Fails_Without_Throwing()
|
||||||
|
{
|
||||||
|
var (_, resp) = NewPair();
|
||||||
|
var result = resp.Process(null);
|
||||||
|
Assert.Equal(AuthenticationRuling.Failed, result.Ruling);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WrongType_Material_Fails_Closed()
|
||||||
|
{
|
||||||
|
// A peer sending a string where a nonce (byte[]) is expected must fail, not throw.
|
||||||
|
var (_, resp) = NewPair();
|
||||||
|
var result = resp.Process(new object[] { "not-a-nonce", "alice" });
|
||||||
|
Assert.Equal(AuthenticationRuling.Failed, result.Ruling);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Reference Esiur both as a runtime assembly and as the source generator (analyzer),
|
||||||
|
mirroring Tests/Features/Functional so [Resource]/[Export] test types get generated code. -->
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\Libraries\Esiur\Esiur.csproj" OutputItemType="Analyzer" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Esiur.Misc;
|
||||||
|
|
||||||
|
namespace Esiur.Tests.Unit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Guards the fix that moved Global.GenerateBytes / GenerateCode off System.Random onto a
|
||||||
|
/// cryptographic RNG. These are sanity/entropy checks, not statistical proofs: their job is
|
||||||
|
/// to fail loudly if someone reintroduces a predictable or constant generator.
|
||||||
|
/// </summary>
|
||||||
|
public class SecureRandomTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GenerateBytes_Returns_Requested_Length()
|
||||||
|
{
|
||||||
|
Assert.Equal(20, Global.GenerateBytes(20).Length);
|
||||||
|
Assert.Equal(0, Global.GenerateBytes(0).Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateBytes_Are_Not_Repeated()
|
||||||
|
{
|
||||||
|
var a = Global.GenerateBytes(32);
|
||||||
|
var b = Global.GenerateBytes(32);
|
||||||
|
Assert.False(a.SequenceEqual(b), "Two nonces must not be identical.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateBytes_Are_Not_Constant()
|
||||||
|
{
|
||||||
|
var bytes = Global.GenerateBytes(64);
|
||||||
|
Assert.True(bytes.Distinct().Count() > 1, "Output must not be a single repeated byte.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateBytes_Have_Broad_Distribution()
|
||||||
|
{
|
||||||
|
// Across 4 KiB of output almost every byte value should appear at least once.
|
||||||
|
var bytes = Global.GenerateBytes(4096);
|
||||||
|
Assert.True(bytes.Distinct().Count() > 200,
|
||||||
|
"A cryptographic RNG should produce a wide spread of byte values.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateCode_Honours_Length_And_Alphabet()
|
||||||
|
{
|
||||||
|
const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
var code = Global.GenerateCode(24);
|
||||||
|
Assert.Equal(24, code.Length);
|
||||||
|
Assert.All(code, c => Assert.Contains(c, alphabet));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerateCode_Is_Not_Repeated()
|
||||||
|
{
|
||||||
|
Assert.NotEqual(Global.GenerateCode(24), Global.GenerateCode(24));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Esiur.Data;
|
||||||
|
using Esiur.Resource;
|
||||||
|
|
||||||
|
namespace Esiur.Tests.Unit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round-trips values through Codec.Compose -> Codec.ParseSync and asserts the value
|
||||||
|
/// survives. Because the serializer narrows integers/floats to the smallest wire type,
|
||||||
|
/// the parsed CLR type often differs from the input, so comparisons are value-based.
|
||||||
|
/// Exact wire bytes are pinned separately by <see cref="WireFormatGoldenTests"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class SerializationRoundTripTests
|
||||||
|
{
|
||||||
|
static object RoundTrip(object value)
|
||||||
|
{
|
||||||
|
var bytes = Codec.Compose(value, Warehouse.Default, null);
|
||||||
|
var (consumed, parsed) = Codec.ParseSync(bytes, 0, Warehouse.Default);
|
||||||
|
Assert.Equal((uint)bytes.Length, consumed);
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void AssertIntRoundTrip(long value)
|
||||||
|
{
|
||||||
|
var parsed = RoundTrip(value);
|
||||||
|
Assert.Equal(value, Convert.ToInt64(parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0)]
|
||||||
|
[InlineData(1)]
|
||||||
|
[InlineData(-1)]
|
||||||
|
[InlineData(sbyte.MinValue)]
|
||||||
|
[InlineData(sbyte.MaxValue)]
|
||||||
|
[InlineData((long)short.MinValue)]
|
||||||
|
[InlineData((long)short.MaxValue)]
|
||||||
|
[InlineData((long)int.MinValue)]
|
||||||
|
[InlineData((long)int.MaxValue)]
|
||||||
|
[InlineData(long.MinValue)]
|
||||||
|
[InlineData(long.MaxValue)]
|
||||||
|
public void Int64_NarrowsAndRoundTrips(long value) => AssertIntRoundTrip(value);
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0)]
|
||||||
|
[InlineData(1)]
|
||||||
|
[InlineData(-1)]
|
||||||
|
[InlineData(int.MinValue)]
|
||||||
|
[InlineData(int.MaxValue)]
|
||||||
|
public void Int32_RoundTrips(int value)
|
||||||
|
{
|
||||||
|
var parsed = RoundTrip(value);
|
||||||
|
Assert.Equal(value, Convert.ToInt32(parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData((ulong)0)]
|
||||||
|
[InlineData((ulong)255)]
|
||||||
|
[InlineData((ulong)65535)]
|
||||||
|
[InlineData((ulong)uint.MaxValue)]
|
||||||
|
[InlineData(ulong.MaxValue)]
|
||||||
|
public void UInt64_NarrowsAndRoundTrips(ulong value)
|
||||||
|
{
|
||||||
|
var parsed = RoundTrip(value);
|
||||||
|
Assert.Equal(value, Convert.ToUInt64(parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0f)]
|
||||||
|
[InlineData(1.5f)]
|
||||||
|
[InlineData(-3.25f)]
|
||||||
|
[InlineData(3.4028235e38f)]
|
||||||
|
public void Float32_RoundTrips(float value)
|
||||||
|
{
|
||||||
|
var parsed = RoundTrip(value);
|
||||||
|
Assert.Equal(value, Convert.ToSingle(parsed), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0d)]
|
||||||
|
[InlineData(1.5d)]
|
||||||
|
[InlineData(-3.25d)]
|
||||||
|
[InlineData(0.1d)]
|
||||||
|
[InlineData(1.7976931348623157e308d)]
|
||||||
|
public void Float64_RoundTrips(double value)
|
||||||
|
{
|
||||||
|
var parsed = RoundTrip(value);
|
||||||
|
Assert.Equal(value, Convert.ToDouble(parsed), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(double.NaN)]
|
||||||
|
[InlineData(double.PositiveInfinity)]
|
||||||
|
[InlineData(double.NegativeInfinity)]
|
||||||
|
public void Float_NaN_And_Infinity_Encode_As_Infinity_Token(double value)
|
||||||
|
{
|
||||||
|
// The serializer collapses NaN and +/- Infinity onto the single 1-byte Infinity
|
||||||
|
// token (0x04), which now decodes to a canonical +Infinity rather than crashing.
|
||||||
|
var bytes = Codec.Compose(value, Warehouse.Default, null);
|
||||||
|
Assert.Equal(new byte[] { 0x04 }, bytes);
|
||||||
|
|
||||||
|
var parsed = RoundTrip(value);
|
||||||
|
Assert.True(double.IsPositiveInfinity(Convert.ToDouble(parsed)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0.5)]
|
||||||
|
[InlineData(1.1)] // hits the decimal -> Float64 branch (regression for the byte[4] overrun)
|
||||||
|
[InlineData(-1234.5)]
|
||||||
|
public void Decimal_FloatBranches_RoundTrip(double asDouble)
|
||||||
|
{
|
||||||
|
var value = (decimal)asDouble;
|
||||||
|
var parsed = RoundTrip(value);
|
||||||
|
Assert.Equal(Convert.ToDouble(value), Convert.ToDouble(parsed), 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Decimal_IntegerValue_RoundTrips()
|
||||||
|
{
|
||||||
|
var parsed = RoundTrip(42m);
|
||||||
|
Assert.Equal(42L, Convert.ToInt64(parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Decimal_HighPrecision_RoundTrips()
|
||||||
|
{
|
||||||
|
// A value with a non-zero scale that is not exactly representable as float or
|
||||||
|
// double, so it stays a full 16-byte Decimal128 on the wire.
|
||||||
|
var value = 1.2345678901234567890123456789m;
|
||||||
|
var parsed = RoundTrip(value);
|
||||||
|
Assert.Equal(value, Convert.ToDecimal(parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void Bool_RoundTrips(bool value)
|
||||||
|
{
|
||||||
|
var parsed = RoundTrip(value);
|
||||||
|
Assert.Equal(value, Convert.ToBoolean(parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData("Hello, Esiur")]
|
||||||
|
[InlineData("Unicode éü☃😀")]
|
||||||
|
public void String_RoundTrips(string value)
|
||||||
|
{
|
||||||
|
var parsed = RoundTrip(value);
|
||||||
|
Assert.Equal(value, (string)parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Null_RoundTrips()
|
||||||
|
{
|
||||||
|
var parsed = RoundTrip(null);
|
||||||
|
Assert.Null(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Char_RoundTrips()
|
||||||
|
{
|
||||||
|
var parsed = RoundTrip('A');
|
||||||
|
Assert.Equal('A', Convert.ToChar(parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DateTime_RoundTrips_AsUtc()
|
||||||
|
{
|
||||||
|
var value = new DateTime(2026, 6, 2, 13, 45, 30, DateTimeKind.Utc);
|
||||||
|
var parsed = (DateTime)RoundTrip(value);
|
||||||
|
Assert.Equal(value.ToUniversalTime().Ticks, parsed.ToUniversalTime().Ticks);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Uuid_RoundTrips()
|
||||||
|
{
|
||||||
|
var value = new Uuid(Guid.Parse("12345678-90ab-cdef-1234-567890abcdef").ToByteArray());
|
||||||
|
var parsed = (Uuid)RoundTrip(value);
|
||||||
|
Assert.Equal(value.ToString(), parsed.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<object> ToObjectList(object value)
|
||||||
|
{
|
||||||
|
var got = new List<object>();
|
||||||
|
foreach (var o in (System.Collections.IEnumerable)value)
|
||||||
|
got.Add(o);
|
||||||
|
return got;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IntArray_RoundTrips()
|
||||||
|
{
|
||||||
|
// A typed int[] is reconstructed as an int[] (typed-list path), not a dynamic list.
|
||||||
|
var value = new int[] { 1, 2, 3, 100, -50, int.MaxValue };
|
||||||
|
var got = ToObjectList(RoundTrip(value)).ConvertAll(Convert.ToInt64);
|
||||||
|
Assert.Equal(new long[] { 1, 2, 3, 100, -50, int.MaxValue }, got);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StringList_RoundTrips()
|
||||||
|
{
|
||||||
|
var value = new object[] { "a", "b", "c" };
|
||||||
|
var got = ToObjectList(RoundTrip(value)).ConvertAll(o => (string)o);
|
||||||
|
Assert.Equal(new[] { "a", "b", "c" }, got);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Map_RoundTrips()
|
||||||
|
{
|
||||||
|
var value = new Map<string, int> { ["one"] = 1, ["two"] = 2 };
|
||||||
|
var parsed = RoundTrip(value);
|
||||||
|
Assert.NotNull(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using System;
|
||||||
|
using Esiur.Data;
|
||||||
|
using Esiur.Resource;
|
||||||
|
|
||||||
|
namespace Esiur.Tests.Unit;
|
||||||
|
|
||||||
|
[Export]
|
||||||
|
public class PersonRecord : IRecord
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int Age { get; set; }
|
||||||
|
public double Score { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Color
|
||||||
|
{
|
||||||
|
Red,
|
||||||
|
Green,
|
||||||
|
Blue,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Covers the "typed" serialization paths (records, tuples, enums, typed maps/lists) that
|
||||||
|
/// rely on Tru.FromType. Uses a decode-then-re-encode stability check: re-composing a parsed
|
||||||
|
/// value must reproduce the exact original wire bytes. This is type-agnostic (no need to know
|
||||||
|
/// the parsed CLR type) and proves both the encode and decode halves agree on the format.
|
||||||
|
/// </summary>
|
||||||
|
public class TypedSerializationTests
|
||||||
|
{
|
||||||
|
static void AssertReencodeStable(object value)
|
||||||
|
{
|
||||||
|
var bytes1 = Codec.Compose(value, Warehouse.Default, null);
|
||||||
|
var (consumed, parsed) = Codec.ParseSync(bytes1, 0, Warehouse.Default);
|
||||||
|
Assert.Equal((uint)bytes1.Length, consumed);
|
||||||
|
|
||||||
|
var bytes2 = Codec.Compose(parsed, Warehouse.Default, null);
|
||||||
|
Assert.Equal(bytes1, bytes2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Record_ReencodeIsStable()
|
||||||
|
{
|
||||||
|
AssertReencodeStable(new PersonRecord { Name = "Ada", Age = 36, Score = 99.5 });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Enum_Encodes_Typed_And_Decodes_To_Underlying_Int()
|
||||||
|
{
|
||||||
|
// An enum is sent as a Typed TDU carrying the constant index; the decoder (without
|
||||||
|
// CLR enum reconstruction) yields the underlying integer value. This is the existing
|
||||||
|
// protocol behaviour, asserted here so it stays stable.
|
||||||
|
var bytes = Codec.Compose(Color.Green, Warehouse.Default, null);
|
||||||
|
Assert.Equal(0x88, bytes[0]); // Typed class token
|
||||||
|
|
||||||
|
var (_, parsed) = Codec.ParseSync(bytes, 0, Warehouse.Default);
|
||||||
|
Assert.Equal((int)Color.Green, Convert.ToInt32(parsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1, "a")]
|
||||||
|
[InlineData(-5, "hello")]
|
||||||
|
public void Tuple2_ReencodeIsStable(int a, string b)
|
||||||
|
{
|
||||||
|
AssertReencodeStable((a, b));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Tuple3_ReencodeIsStable()
|
||||||
|
{
|
||||||
|
AssertReencodeStable((1, "two", 3.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TypedMap_ReencodeIsStable()
|
||||||
|
{
|
||||||
|
AssertReencodeStable(new Map<string, int> { ["one"] = 1, ["two"] = 2, ["three"] = 3 });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TypedIntList_ReencodeIsStable()
|
||||||
|
{
|
||||||
|
AssertReencodeStable(new int[] { 5, 4, 3, 2, 1, 0, -1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RecordList_ReencodeIsStable()
|
||||||
|
{
|
||||||
|
AssertReencodeStable(new[]
|
||||||
|
{
|
||||||
|
new PersonRecord { Name = "A", Age = 1, Score = 1.1 },
|
||||||
|
new PersonRecord { Name = "B", Age = 2, Score = 2.2 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
using System;
|
||||||
|
using Esiur.Data;
|
||||||
|
using Esiur.Resource;
|
||||||
|
|
||||||
|
namespace Esiur.Tests.Unit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pins the exact on-wire bytes produced by Codec.Compose for a representative value of
|
||||||
|
/// every TDU family. Esiur is a multi-language protocol (C#/JS/Dart) whose binary format
|
||||||
|
/// must stay byte-compatible, so these golden vectors are a guard rail: any later
|
||||||
|
/// refactor (e.g. serializer performance work) that changes a single byte fails here.
|
||||||
|
/// Values were captured from the current implementation.
|
||||||
|
/// </summary>
|
||||||
|
public class WireFormatGoldenTests
|
||||||
|
{
|
||||||
|
static string Hex(object value) =>
|
||||||
|
BitConverter.ToString(Codec.Compose(value, Warehouse.Default, null)).Replace("-", "").ToLowerInvariant();
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
// fixed, zero-payload tokens
|
||||||
|
[InlineData("null", null, "00")]
|
||||||
|
[InlineData("bool_false", false, "01")]
|
||||||
|
[InlineData("bool_true", true, "02")]
|
||||||
|
// signed integers, narrowed to the smallest width
|
||||||
|
[InlineData("int_0", 0, "0900")]
|
||||||
|
[InlineData("int_1", 1, "0901")]
|
||||||
|
[InlineData("int_minus1", -1, "09ff")]
|
||||||
|
[InlineData("int_127", 127, "097f")]
|
||||||
|
[InlineData("int_128", 128, "118000")]
|
||||||
|
[InlineData("int_200", 200, "11c800")]
|
||||||
|
[InlineData("int_40000", 40000, "19409c0000")]
|
||||||
|
[InlineData("int_70000", 70000, "1970110100")]
|
||||||
|
[InlineData("long_5e9", 5000000000L, "2100f2052a01000000")]
|
||||||
|
// unsigned integers
|
||||||
|
[InlineData("uint_255", (uint)255, "08ff")]
|
||||||
|
[InlineData("uint_256", (uint)256, "100001")]
|
||||||
|
[InlineData("ulong_max", ulong.MaxValue, "20ffffffffffffffff")]
|
||||||
|
// floating point
|
||||||
|
[InlineData("float_1p5", 1.5f, "1a0000c03f")]
|
||||||
|
[InlineData("double_0p1", 0.1d, "229a9999999999b93f")]
|
||||||
|
// char
|
||||||
|
[InlineData("char_A", 'A', "124100")]
|
||||||
|
// strings (length-prefixed UTF-8)
|
||||||
|
[InlineData("string_Hi", "Hi", "49024869")]
|
||||||
|
[InlineData("string_empty", "", "41")]
|
||||||
|
public void Compose_Matches_Golden(string name, object value, string expectedHex)
|
||||||
|
{
|
||||||
|
_ = name; // identifies the case in test output
|
||||||
|
Assert.Equal(expectedHex, Hex(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Decimal_HighPrecision_Golden()
|
||||||
|
{
|
||||||
|
Assert.Equal("2a00001c00321be4271581396eb1c9be46", Hex(1.2345678901234567890123456789m));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DateTime_Golden()
|
||||||
|
{
|
||||||
|
Assert.Equal("230039f035adc0de08", Hex(new DateTime(2026, 6, 2, 13, 45, 30, DateTimeKind.Utc)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Uuid_Golden()
|
||||||
|
{
|
||||||
|
var uuid = new Uuid(Guid.Parse("12345678-90ab-cdef-1234-567890abcdef").ToByteArray());
|
||||||
|
Assert.Equal("2b78563412ab90efcd1234567890abcdef", Hex(uuid));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IntArray_TypedList_Golden()
|
||||||
|
{
|
||||||
|
// Typed list with Gvwie group-encoded payload for { 1, 2, 3 }.
|
||||||
|
Assert.Equal("88054809020406", Hex(new int[] { 1, 2, 3 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(double.NaN)]
|
||||||
|
[InlineData(double.PositiveInfinity)]
|
||||||
|
[InlineData(double.NegativeInfinity)]
|
||||||
|
public void NaN_And_Infinity_Encode_To_Single_Token(double value)
|
||||||
|
{
|
||||||
|
Assert.Equal("04", Hex(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user