diff --git a/Esiur.sln b/Esiur.sln index 600224a..13bfa1c 100644 --- a/Esiur.sln +++ b/Esiur.sln @@ -1,3 +1,4 @@ + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.4.11612.150 @@ -92,96 +93,282 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ConcurrentAttachSweep", "Co EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.ConcurrentAttachSweep", "Tests\Distribution\ConcurrentAttachSweep\Esiur.Tests.ConcurrentAttachSweep.csproj", "{3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Esiur.Tests.Unit", "Tests\Unit\Esiur.Tests.Unit.csproj", "{D1B99C5A-82F7-459D-B56D-F8FD096D3854}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {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|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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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 GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -226,6 +413,7 @@ Global {550A20AB-8E97-BCDD-9F54-27823663120A} = {21D42B96-99F9-4E48-A499-5170A5A9597F} {E713D25F-2602-44C9-AB9E-C9477FB2BA93} = {94C8CFDB-C7C6-40DF-A596-647FEEA3C917} {3FFB2BF4-159E-3073-4BDF-08AE93C7A2C1} = {E713D25F-2602-44C9-AB9E-C9477FB2BA93} + {D1B99C5A-82F7-459D-B56D-F8FD096D3854} = {2769C4C3-2595-413B-B7FE-5903826770C1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C584421D-5EC0-4821-B7D8-2633D8D405F2} diff --git a/Libraries/Esiur/Data/Codec.cs b/Libraries/Esiur/Data/Codec.cs index a9e0dba..0f3da05 100644 --- a/Libraries/Esiur/Data/Codec.cs +++ b/Libraries/Esiur/Data/Codec.cs @@ -51,6 +51,7 @@ public static class Codec DataDeserializer.BooleanFalseParserAsync, DataDeserializer.BooleanTrueParserAsync, DataDeserializer.NotModifiedParserAsync, + DataDeserializer.InfinityParserAsync, }, new AsyncParser[]{ DataDeserializer.UInt8ParserAsync, @@ -115,6 +116,7 @@ public static class Codec DataDeserializer.BooleanFalseParser, DataDeserializer.BooleanTrueParser, DataDeserializer.NotModifiedParser, + DataDeserializer.InfinityParser, }, new SyncParser[]{ DataDeserializer.UInt8Parser, @@ -376,6 +378,13 @@ public static class Codec } + /// + /// Synchronously parses a single value from its IIP wire representation. + /// + /// Buffer containing the encoded value. + /// Zero-based offset of the value within . + /// Warehouse used to resolve typed structures (records, enums, ...). + /// A tuple of (number of bytes consumed, decoded value). public static (uint, object) ParseSync(byte[] data, uint offset, Warehouse warehouse) { var tdu = ParsedTdu.ParseSync(data, offset, (uint)data.Length, warehouse); @@ -612,7 +621,14 @@ public static class Codec /// EpConnection is required to check locality. /// If True, prepend the DataType at the beginning of the output. /// Array of bytes in the network byte order. - public static byte[] Compose(object valueOrSource, Warehouse warehouse, EpConnection connection)//, bool prependType = true) + /// + /// Encodes a value to its self-describing IIP wire representation (a type-prefixed TDU). + /// + /// The value to encode (may be null, which encodes as the Null TDU). + /// Warehouse used to resolve type definitions for typed structures. + /// Connection context, required when the value references remote resources; may be null for plain data. + /// The encoded bytes, including the leading type identifier. + public static byte[] Compose(object valueOrSource, Warehouse warehouse, EpConnection connection) { var tdu = ComposeInternal(valueOrSource, warehouse, connection); return tdu.Composed; diff --git a/Libraries/Esiur/Data/DataConverter.cs b/Libraries/Esiur/Data/DataConverter.cs index 01600ef..0404912 100644 --- a/Libraries/Esiur/Data/DataConverter.cs +++ b/Libraries/Esiur/Data/DataConverter.cs @@ -108,11 +108,10 @@ public static class DC // Data Converter } } - catch (Exception ex) + catch (Exception) { - - throw ex; - return null; + // Preserve the original stack trace with a bare rethrow. + throw; } } } diff --git a/Libraries/Esiur/Data/DataDeserializer.cs b/Libraries/Esiur/Data/DataDeserializer.cs index e35bdfa..c8d14bf 100644 --- a/Libraries/Esiur/Data/DataDeserializer.cs +++ b/Libraries/Esiur/Data/DataDeserializer.cs @@ -59,6 +59,19 @@ public static class DataDeserializer 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) { return tdu.Data[tdu.PayloadOffset]; @@ -1345,7 +1358,6 @@ public static class DataDeserializer var subTypes = subTrus.Select(x => x.RuntimeType).ToArray(); ParsedTdu current; - ParsedTdu? previous = null; var offset = tdu.PayloadOffset; var length = tdu.PayloadLength; @@ -1477,7 +1489,6 @@ public static class DataDeserializer var types = subTrus.Select(x => x.RuntimeType).ToArray(); ParsedTdu current; - ParsedTdu? previous = null; var offset = tdu.PayloadOffset; var length = tdu.PayloadLength; diff --git a/Libraries/Esiur/Data/DataSerializer.cs b/Libraries/Esiur/Data/DataSerializer.cs index 4779bad..2eaf46d 100644 --- a/Libraries/Esiur/Data/DataSerializer.cs +++ b/Libraries/Esiur/Data/DataSerializer.cs @@ -17,7 +17,7 @@ public static class DataSerializer { 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; @@ -29,22 +29,19 @@ public static class DataSerializer { // Fits in 2 bytes var rt = new byte[2]; - fixed (byte* ptr = rt) - *((short*)ptr) = (short)v; - + BinaryPrimitives.WriteInt16LittleEndian(rt, (short)v); return new Tdu(TduIdentifier.Int16, rt, 2, null, null); } else { // Use full 4 bytes var rt = new byte[4]; - fixed (byte* ptr = rt) - *((int*)ptr) = v; + BinaryPrimitives.WriteInt32LittleEndian(rt, v); 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; @@ -57,23 +54,19 @@ public static class DataSerializer { // Fits in 2 bytes var rt = new byte[2]; - fixed (byte* ptr = rt) - *((ushort*)ptr) = (ushort)v; - + BinaryPrimitives.WriteUInt16LittleEndian(rt, (ushort)v); return new Tdu(TduIdentifier.UInt16, rt, 2, null, null); } else { // Use full 4 bytes var rt = new byte[4]; - fixed (byte* ptr = rt) - *((uint*)ptr) = v; - + BinaryPrimitives.WriteUInt32LittleEndian(rt, v); 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; @@ -86,14 +79,12 @@ public static class DataSerializer { // Use full 2 bytes var rt = new byte[2]; - fixed (byte* ptr = rt) - *((short*)ptr) = v; - + BinaryPrimitives.WriteInt16LittleEndian(rt, v); 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; @@ -106,9 +97,7 @@ public static class DataSerializer { // Use full 2 bytes var rt = new byte[2]; - fixed (byte* ptr = rt) - *((ushort*)ptr) = v; - + BinaryPrimitives.WriteUInt16LittleEndian(rt, v); 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); } } - 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; @@ -224,32 +213,26 @@ public static class DataSerializer { // Fits in 2 bytes var rt = new byte[2]; - fixed (byte* ptr = rt) - *((short*)ptr) = (short)v; - + BinaryPrimitives.WriteInt16LittleEndian(rt, (short)v); return new Tdu(TduIdentifier.Int16, rt, 2, null, null); } else if (v >= int.MinValue && v <= int.MaxValue) { // Fits in 4 bytes var rt = new byte[4]; - fixed (byte* ptr = rt) - *((int*)ptr) = (int)v; - + BinaryPrimitives.WriteInt32LittleEndian(rt, (int)v); return new Tdu(TduIdentifier.Int32, rt, 4, null, null); } else { // Use full 8 bytes var rt = new byte[8]; - fixed (byte* ptr = rt) - *((long*)ptr) = v; - + BinaryPrimitives.WriteInt64LittleEndian(rt, v); 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; @@ -262,39 +245,31 @@ public static class DataSerializer { // Fits in 2 bytes var rt = new byte[2]; - fixed (byte* ptr = rt) - *((ushort*)ptr) = (ushort)v; - + BinaryPrimitives.WriteUInt16LittleEndian(rt, (ushort)v); return new Tdu(TduIdentifier.UInt16, rt, 2, null, null); } else if (v <= uint.MaxValue) { // Fits in 4 bytes var rt = new byte[4]; - fixed (byte* ptr = rt) - *((uint*)ptr) = (uint)v; - + BinaryPrimitives.WriteUInt32LittleEndian(rt, (uint)v); return new Tdu(TduIdentifier.UInt32, rt, 4, null, null); } else { // Use full 8 bytes var rt = new byte[8]; - fixed (byte* ptr = rt) - *((ulong*)ptr) = v; - + BinaryPrimitives.WriteUInt64LittleEndian(rt, v); 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 rt = new byte[8]; - fixed (byte* ptr = rt) - *((long*)ptr) = v; - + BinaryPrimitives.WriteInt64LittleEndian(rt, v); return new Tdu(TduIdentifier.DateTime, rt, 8, null, null); } @@ -362,7 +337,7 @@ public static class DataSerializer double d = (double)v; if ((decimal)d == v) { - var rt = new byte[4]; + var rt = new byte[8]; fixed (byte* ptr = rt) *((double*)ptr) = d; @@ -436,15 +411,12 @@ public static class DataSerializer 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 rt = new byte[2]; - fixed (byte* ptr = rt) - *((char*)ptr) = v; - + BinaryPrimitives.WriteUInt16LittleEndian(rt, v); return new Tdu(TduIdentifier.Char16, rt, 2, null, null); - } public static Tdu BoolComposer(object value, Warehouse warehouse, EpConnection connection) @@ -733,7 +705,9 @@ public static class DataSerializer if (value == null) return null; - var rt = new List(); + // Pre-size the buffer from the element count (when known) to avoid repeated + // List reallocations as items are appended. 4 bytes/element is a rough hint. + var rt = new List(value is ICollection collection ? collection.Count * 4 : 16); Tdu? previous = null; @@ -934,7 +908,7 @@ public static class DataSerializer var trus = fields.Select(x => Tru.FromType(x.FieldType, warehouse)).ToArray(); - var rt = new List(); + var rt = new List(fields.Length * 4); for (var i = 0; i < fields.Length; i++) { diff --git a/Libraries/Esiur/Data/Tdu.cs b/Libraries/Esiur/Data/Tdu.cs index c3834f3..4418482 100644 --- a/Libraries/Esiur/Data/Tdu.cs +++ b/Libraries/Esiur/Data/Tdu.cs @@ -65,7 +65,7 @@ public struct Tdu } public Tdu(TduIdentifier identifier, - byte[] data, ulong length, Tru metadata, EpConnection connection) + byte[]? data, ulong length, Tru? metadata, EpConnection? connection) { Identifier = identifier; //Index = (byte)identifier & 0x7; diff --git a/Libraries/Esiur/Data/Tru.cs b/Libraries/Esiur/Data/Tru.cs index 3f4d679..e253705 100644 --- a/Libraries/Esiur/Data/Tru.cs +++ b/Libraries/Esiur/Data/Tru.cs @@ -138,6 +138,14 @@ namespace Esiur.Data 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 flags); public abstract void SetNotNull(byte flag); @@ -299,7 +307,32 @@ namespace Esiur.Data //private static Dictionary cache = new Dictionary(); //private static object cacheLook = new object(); + /// + /// 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. + /// 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) return new TruPrimitive(TruIdentifier.Void, true, typeof(void)); @@ -714,7 +747,7 @@ namespace Esiur.Data offset += pr.Size; } - Type runtimeType = null; + Type? runtimeType = null; if (identifier == TruIdentifier.TypedList) { @@ -852,7 +885,7 @@ namespace Esiur.Data offset += pr.Size; } - Type runtimeType = null; + Type? runtimeType = null; if (identifier == TruIdentifier.TypedList) { diff --git a/Libraries/Esiur/Data/TruComposite.cs b/Libraries/Esiur/Data/TruComposite.cs index aff54f0..e876e1e 100644 --- a/Libraries/Esiur/Data/TruComposite.cs +++ b/Libraries/Esiur/Data/TruComposite.cs @@ -11,11 +11,9 @@ namespace Esiur.Data { public Tru[] SubTypes; - Type _runtimeType; - 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; Nullable = nullable; diff --git a/Libraries/Esiur/Data/Types/ArgumentDef.cs b/Libraries/Esiur/Data/Types/ArgumentDef.cs index 9db51f1..166acac 100644 --- a/Libraries/Esiur/Data/Types/ArgumentDef.cs +++ b/Libraries/Esiur/Data/Types/ArgumentDef.cs @@ -88,7 +88,7 @@ public class ArgumentDef } else { - var exp = Codec.Compose(Annotations, null, null); + var exp = Codec.Compose(Annotations, connection.Instance.Warehouse, connection); return new BinaryList() .AddUInt8((byte)(0x2 | (Optional ? 1 : 0))) diff --git a/Libraries/Esiur/Data/Types/ConstantDef.cs b/Libraries/Esiur/Data/Types/ConstantDef.cs index 3a2a323..af003b1 100644 --- a/Libraries/Esiur/Data/Types/ConstantDef.cs +++ b/Libraries/Esiur/Data/Types/ConstantDef.cs @@ -76,7 +76,7 @@ public class ConstantDef : MemberDef 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; return new BinaryList() .AddUInt8(hdr) diff --git a/Libraries/Esiur/Data/Types/EventDef.cs b/Libraries/Esiur/Data/Types/EventDef.cs index b85dd21..7799765 100644 --- a/Libraries/Esiur/Data/Types/EventDef.cs +++ b/Libraries/Esiur/Data/Types/EventDef.cs @@ -82,7 +82,7 @@ public class EventDef : MemberDef 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; return new BinaryList() .AddUInt8(hdr) diff --git a/Libraries/Esiur/Data/Types/FunctionDef.cs b/Libraries/Esiur/Data/Types/FunctionDef.cs index c24576b..ea61bfe 100644 --- a/Libraries/Esiur/Data/Types/FunctionDef.cs +++ b/Libraries/Esiur/Data/Types/FunctionDef.cs @@ -110,7 +110,7 @@ public class FunctionDef : MemberDef 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.InsertUInt8(0, (byte)((Inherited ? (byte)0x90 : (byte)0x10) | (IsStatic ? 0x4 : 0))); } diff --git a/Libraries/Esiur/Data/Types/LocalTypeDef.cs b/Libraries/Esiur/Data/Types/LocalTypeDef.cs index e123a41..56a59e5 100644 --- a/Libraries/Esiur/Data/Types/LocalTypeDef.cs +++ b/Libraries/Esiur/Data/Types/LocalTypeDef.cs @@ -389,7 +389,7 @@ public class LocalTypeDef:TypeDef //foreach (var ann in Annotations) // 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); diff --git a/Libraries/Esiur/Data/Types/PropertyDef.cs b/Libraries/Esiur/Data/Types/PropertyDef.cs index cfd7cfa..9caba10 100644 --- a/Libraries/Esiur/Data/Types/PropertyDef.cs +++ b/Libraries/Esiur/Data/Types/PropertyDef.cs @@ -176,7 +176,7 @@ public class PropertyDef : MemberDef //} if (Annotations != null) { - var rexp = Codec.Compose(Annotations, null, null); + var rexp = Codec.Compose(Annotations, connection.Instance.Warehouse, connection); return new BinaryList() .AddUInt8((byte)(0x28 | pv)) .AddUInt8((byte)name.Length) diff --git a/Libraries/Esiur/Esiur.csproj b/Libraries/Esiur/Esiur.csproj index 377ad70..5642d37 100644 --- a/Libraries/Esiur/Esiur.csproj +++ b/Libraries/Esiur/Esiur.csproj @@ -16,6 +16,10 @@ Esiur Esiur latest + + annotations netstandard2.0 README.md enable @@ -67,7 +71,6 @@ - diff --git a/Libraries/Esiur/Misc/Global.cs b/Libraries/Esiur/Misc/Global.cs index e0758fe..2edd547 100644 --- a/Libraries/Esiur/Misc/Global.cs +++ b/Libraries/Esiur/Misc/Global.cs @@ -47,7 +47,10 @@ public static class Global { private static KeyList variables = new KeyList(); - 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) { var b = new byte[length]; - rand.NextBytes(b); + secureRng.GetBytes(b); return b; } public static string GenerateCode(int length) { - return GenerateCode(length, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"); + return GenerateCode(length, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"); } public static string GenerateCode(int length, string chars) { - var result = new string( - Enumerable.Repeat(chars, length) - .Select(s => s[rand.Next(s.Length)]) - .ToArray()); - return result; - } + // Draw each character from a CSPRNG using unbiased rejection sampling, so + // codes used as session identifiers are not predictable. The largest + // multiple of chars.Length that fits in a byte is the acceptance bound; + // bytes at or above it are discarded to avoid modulo bias. + 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) diff --git a/Libraries/Esiur/Net/Http/EpOverWebsocket.cs b/Libraries/Esiur/Net/Http/EpOverWebsocket.cs index 8c895ef..a130de5 100644 --- a/Libraries/Esiur/Net/Http/EpOverWebsocket.cs +++ b/Libraries/Esiur/Net/Http/EpOverWebsocket.cs @@ -34,7 +34,7 @@ using Esiur.Protocol; namespace Esiur.Net.Http; -public class EpOvwerWebsocket : HttpFilter +public class EpOverWebsocket : HttpFilter { //[Attribute] public EpServer Server diff --git a/Libraries/Esiur/Net/NetworkConnection.cs b/Libraries/Esiur/Net/NetworkConnection.cs index b617c09..f573874 100644 --- a/Libraries/Esiur/Net/NetworkConnection.cs +++ b/Libraries/Esiur/Net/NetworkConnection.cs @@ -1,5 +1,5 @@ -/* - +/* + Copyright (c) 2017 Ahmed Kh. Zamil Permission is hereby granted, free of charge, to any person obtaining a copy @@ -23,229 +23,109 @@ SOFTWARE. */ using System; -using System.IO; -using System.Net.Sockets; +using System.Net; using System.Text; using System.Threading; -using System.Net; -using System.Collections; -using System.Collections.Generic; using Esiur.Misc; using Esiur.Core; -using Esiur.Data; using Esiur.Net.Sockets; -using Esiur.Resource; namespace Esiur.Net; -public abstract class NetworkConnection : IDestructible, INetworkReceiver// : IResource where TS : NetworkSession -{ - private Sockets.ISocket sock; - // private bool connected; +/// +/// Base class for a logical connection layered on top of an . +/// It owns the socket, forwards inbound buffers to , and +/// exposes send helpers. Derived classes implement the protocol-specific framing. +/// +public abstract class NetworkConnection : IDestructible, INetworkReceiver +{ + private ISocket sock; private DateTime lastAction; - //public delegate void DataReceivedEvent(NetworkConnection sender, NetworkBuffer data); - //public delegate void ConnectionClosedEvent(NetworkConnection sender); + // Re-entrancy guard for NetworkReceive. 0 = idle, 1 = a thread is draining the buffer. + // 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 event NetworkConnectionEvent OnConnect; - //public event DataReceivedEvent OnDataReceived; public event NetworkConnectionEvent OnClose; - - public event DestroyedEvent OnDestroy; - //object receivingLock = new object(); - - //object sendLock = new object(); - - bool processing = false; - - // public INetworkReceiver Receiver { get; set; } public virtual void Destroy() { - // remove references - //sock.OnClose -= Socket_OnClose; - //sock.OnConnect -= Socket_OnConnect; - //sock.OnReceive -= Socket_OnReceive; sock?.Destroy(); - //Receiver = null; Close(); sock = null; OnClose = null; OnConnect = null; - //OnDataReceived = null; OnDestroy?.Invoke(this); OnDestroy = null; } - public ISocket Socket - { - get - { - return sock; - } - } + public ISocket Socket => sock; public virtual void Assign(ISocket socket) { lastAction = DateTime.Now; sock = socket; sock.Receiver = this; - - //socket.OnReceive += Socket_OnReceive; - //socket.OnClose += Socket_OnClose; - //socket.OnConnect += Socket_OnConnect; } - //private void Socket_OnConnect() - //{ - // OnConnect?.Invoke(this); - //} - - //private void Socket_OnClose() - //{ - // ConnectionClosed(); - // OnClose?.Invoke(this); - //} - - //protected virtual void ConnectionClosed() - //{ - - //} - - //private void Socket_OnReceive(NetworkBuffer buffer) - //{ - //} - + /// + /// Detaches the socket from this connection without closing it and returns it, + /// so ownership can be handed to another connection (e.g. a protocol upgrade). + /// public ISocket Unassign() { - 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 + if (sock == null) return null; - } - //protected virtual void DataReceived(NetworkBuffer data) - //{ - // if (OnDataReceived != null) - // { - // try - // { - // OnDataReceived?.Invoke(this, data); - // } - // catch (Exception ex) - // { - // Global.Log("NetworkConenction:DataReceived", LogType.Error, ex.ToString()); - // } - // } - //} + sock.Receiver = null; + + var detached = sock; + sock = null; + return detached; + } public void Close() { - //if (!connected) - // return; - - try { - if (sock != null) - sock.Close(); + sock?.Close(); } catch (Exception ex) { - Global.Log("NetworkConenction: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; + Global.Log("NetworkConnection:Close", LogType.Error, ex.ToString()); } } - public IPEndPoint LocalEndPoint - { - get - { - if (sock != null) - return (IPEndPoint)sock.LocalEndPoint; - else - return null; - } - } + public DateTime LastAction => lastAction; + public IPEndPoint RemoteEndPoint => sock != null ? (IPEndPoint)sock.RemoteEndPoint : null; - public bool IsConnected - { - get - { - return sock == null ? false : sock.State == SocketState.Established; - } - } + public IPEndPoint LocalEndPoint => sock != null ? (IPEndPoint)sock.LocalEndPoint : null; - - /* - public void CloseAndWait() - { - try - { - if (!connected) - return; - - if (sock != null) - sock.Close(); - - while (connected) - { - Thread.Sleep(100); - } - } - finally - { - - } - } - */ + public bool IsConnected => sock != null && sock.State == SocketState.Established; public virtual AsyncReply SendAsync(byte[] message, int offset, int length) { + var socket = sock; + if (socket == null) + return new AsyncReply(false); + try { 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(false); } } @@ -257,9 +137,9 @@ public abstract class NetworkConnection : IDestructible, INetworkReceiver 0 && !buffer.Protected) - { - //Receiver?.NetworkReceive(this, buffer); - DataReceived(buffer); - } - } - catch - { - - } - - processing = false; + while (buffer.Available > 0 && !buffer.Protected) + DataReceived(buffer); + } + finally + { + Interlocked.Exchange(ref receiving, 0); } - } 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(); - //} } diff --git a/Libraries/Esiur/Net/Packets/EpAuthPacket.cs b/Libraries/Esiur/Net/Packets/EpAuthPacket.cs index 3274038..cec726f 100644 --- a/Libraries/Esiur/Net/Packets/EpAuthPacket.cs +++ b/Libraries/Esiur/Net/Packets/EpAuthPacket.cs @@ -187,8 +187,6 @@ public class EpAuthPacket : Packet Tdu = PlainTdu.Parse(data, offset, ends);//, _warehouse); - Console.WriteLine("Auth TDU " + Tdu.Value.PayloadLength); - if (Tdu.Value.Class == TduClass.Invalid) return -(int)Tdu.Value.TotalLength; diff --git a/Libraries/Esiur/Net/Sockets/FrameworkWebSocket.cs b/Libraries/Esiur/Net/Sockets/FrameworkWebSocket.cs index a37bf91..a716ea8 100644 --- a/Libraries/Esiur/Net/Sockets/FrameworkWebSocket.cs +++ b/Libraries/Esiur/Net/Sockets/FrameworkWebSocket.cs @@ -156,12 +156,18 @@ namespace Esiur.Net.Sockets public void Destroy() { - Close(); + var ws = sock; + + Close(); // best-effort graceful close handshake (fire-and-forget) receiveNetworkBuffer = null; Receiver = 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 = null; } diff --git a/Libraries/Esiur/Net/Sockets/SSLSocket.cs b/Libraries/Esiur/Net/Sockets/SSLSocket.cs index 9e28148..5fc77ab 100644 --- a/Libraries/Esiur/Net/Sockets/SSLSocket.cs +++ b/Libraries/Esiur/Net/Sockets/SSLSocket.cs @@ -458,6 +458,14 @@ public class SSLSocket : ISocket public void Destroy() { 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; receiveNetworkBuffer = null; OnDestroy?.Invoke(this); diff --git a/Libraries/Esiur/Protocol/EpConnection.cs b/Libraries/Esiur/Protocol/EpConnection.cs index d29323e..5d17058 100644 --- a/Libraries/Esiur/Protocol/EpConnection.cs +++ b/Libraries/Esiur/Protocol/EpConnection.cs @@ -99,7 +99,7 @@ public partial class EpConnection : NetworkConnection, IStore AsyncReply _openReply; - bool _authenticated, _readyToEstablish; + bool _authenticated; string _hostname; ushort _port; @@ -2025,7 +2025,6 @@ public partial class EpConnection : NetworkConnection, IStore { // clean up _authenticated = false; - _readyToEstablish = false; Status = EpConnectionStatus.Closed; _keepAliveTimer.Stop(); diff --git a/Libraries/Esiur/Protocol/EpConnectionProtocol.cs b/Libraries/Esiur/Protocol/EpConnectionProtocol.cs index 9c2db14..14d4512 100644 --- a/Libraries/Esiur/Protocol/EpConnectionProtocol.cs +++ b/Libraries/Esiur/Protocol/EpConnectionProtocol.cs @@ -108,7 +108,7 @@ partial class EpConnection var bl = new BinaryList(); bl.AddUInt8((byte)(0x60 | (byte)action)) .AddUInt32(c) - .AddUInt8Array(Codec.Compose(args[0], this.Instance.Warehouse, this)); + .AddUInt8Array(Codec.Compose(args[0], this.Instance?.Warehouse ?? _serverWarehouse, this)); Send(bl.ToArray()); } else @@ -116,7 +116,7 @@ partial class EpConnection var bl = new BinaryList(); bl.AddUInt8((byte)(0x60 | (byte)action)) .AddUInt32(c) - .AddUInt8Array(Codec.Compose(args, this.Instance.Warehouse, this)); + .AddUInt8Array(Codec.Compose(args, this.Instance?.Warehouse ?? _serverWarehouse, this)); Send(bl.ToArray()); } @@ -147,7 +147,7 @@ partial class EpConnection { var bl = new BinaryList(); 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()); } else @@ -165,7 +165,7 @@ partial class EpConnection { var bl = new BinaryList(); 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()); } @@ -183,7 +183,9 @@ partial class EpConnection var bl = new BinaryList(); 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()); } else @@ -212,14 +214,14 @@ partial class EpConnection { var bl = new BinaryList(); 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()); } else { var bl = new BinaryList(); 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()); } @@ -243,7 +245,7 @@ partial class EpConnection var bl = new BinaryList(); bl.AddUInt8((byte)(0xA0 | (byte)action)) .AddUInt32(callbackId) - .AddUInt8Array(Codec.Compose(args[0], this.Instance.Warehouse, this)); + .AddUInt8Array(Codec.Compose(args[0], this.Instance?.Warehouse ?? _serverWarehouse, this)); Send(bl.ToArray()); } else @@ -251,7 +253,7 @@ partial class EpConnection var bl = new BinaryList(); bl.AddUInt8((byte)(0xA0 | (byte)action)) .AddUInt32(callbackId) - .AddUInt8Array(Codec.Compose(args, this.Instance.Warehouse, this)); + .AddUInt8Array(Codec.Compose(args, this.Instance?.Warehouse ?? _serverWarehouse, this)); Send(bl.ToArray()); } } @@ -2033,7 +2035,10 @@ partial class EpConnection _attachedResources[id]?.TryGetTarget(out resource); if (resource != null) + { + Global.Counters["EpResourceAttachedCacheHit"]++; return new AsyncReply(resource); + } resource = _neededResources[id]; @@ -2043,16 +2048,19 @@ partial class EpConnection { if (resource != null && (requestSequence?.Contains(id) ?? false)) { + Global.Counters["EpResourceDeadLockSameChain"]++; // dead lock avoidance for loop reference. return new AsyncReply(resource); } else if (resource != null && requestInfo.RequestSequence.Contains(id)) { + Global.Counters["EpResourceDeadLockCrossChain"]++; // dead lock avoidance for dependent reference. return new AsyncReply(resource); } else { + Global.Counters["EpResourcePendingCacheHit"]++; return requestInfo.Reply; } } diff --git a/Libraries/Esiur/Resource/Warehouse.cs b/Libraries/Esiur/Resource/Warehouse.cs index c7bce57..2d2ff38 100644 --- a/Libraries/Esiur/Resource/Warehouse.cs +++ b/Libraries/Esiur/Resource/Warehouse.cs @@ -57,6 +57,12 @@ public class Warehouse ConcurrentDictionary> _resources = new ConcurrentDictionary>(); ConcurrentDictionary>> _stores = new ConcurrentDictionary>>(); + // 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 TypeRepresentationCache + = new ConcurrentDictionary(); + volatile int _resourceCounter = 0; volatile int _typeDefsCounter = 0; @@ -681,14 +687,10 @@ public class Warehouse || baseType == typeof(IRecord)) return null; - TypeDefKind typeDefKind; - if (Codec.ImplementsInterface(type, typeof(IResource))) - typeDefKind = TypeDefKind.Resource; - else if (Codec.ImplementsInterface(type, typeof(IRecord))) - typeDefKind = TypeDefKind.Record; - else if (type.IsEnum) - typeDefKind = TypeDefKind.Enum; - else + // Only resources, records and enums have type definitions; bail out for anything else. + if (!Codec.ImplementsInterface(type, typeof(IResource)) + && !Codec.ImplementsInterface(type, typeof(IRecord)) + && !type.IsEnum) return null; lock (_typeDefsLock) diff --git a/Libraries/Esiur/Security/Authority/Providers/PasswordAuthenticationHandler.cs b/Libraries/Esiur/Security/Authority/Providers/PasswordAuthenticationHandler.cs index 561bb34..585b241 100644 --- a/Libraries/Esiur/Security/Authority/Providers/PasswordAuthenticationHandler.cs +++ b/Libraries/Esiur/Security/Authority/Providers/PasswordAuthenticationHandler.cs @@ -9,10 +9,20 @@ using Esiur.Data.Types; namespace Esiur.Security.Authority.Providers { + /// + /// 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. + /// public class PasswordAuthenticationHandler : IAuthenticationHandler { 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[] _localSalt, _remoteSalt; @@ -33,6 +43,18 @@ namespace Esiur.Security.Authority.Providers 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) { // 1. Initialize the digest (supports 224, 256, 384, 512) @@ -50,7 +72,22 @@ namespace Esiur.Security.Authority.Providers 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 localAuthData = new List(); @@ -97,7 +134,7 @@ namespace Esiur.Security.Authority.Providers var remoteChallenge = (byte[])remoteAuthData[2]; // 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; return new AuthenticationResult(AuthenticationRuling.Failed, null); @@ -112,7 +149,7 @@ namespace Esiur.Security.Authority.Providers .ToArray()); // compare remote challenge - if (!remoteChallenge.SequenceEqual(expectedRemoteChallenge)) + if (!FixedTimeEquals(remoteChallenge, expectedRemoteChallenge)) { _step = -1; return new AuthenticationResult(AuthenticationRuling.Failed, null); @@ -160,7 +197,7 @@ namespace Esiur.Security.Authority.Providers _responderIdentity = (string)remoteAuthData[1]; // 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; return new AuthenticationResult(AuthenticationRuling.Failed, null); @@ -203,7 +240,7 @@ namespace Esiur.Security.Authority.Providers .Concat(_localNonce) .ToArray()); - if (!remoteChallenge.SequenceEqual(expectedRemoteChallenge)) + if (!FixedTimeEquals(remoteChallenge, expectedRemoteChallenge)) { _step = -1; return new AuthenticationResult(AuthenticationRuling.Failed, null); @@ -262,7 +299,7 @@ namespace Esiur.Security.Authority.Providers _remoteSalt = (byte[])remoteAuthData[2]; // 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; return new AuthenticationResult(AuthenticationRuling.Failed, null); @@ -313,7 +350,7 @@ namespace Esiur.Security.Authority.Providers .Concat(_localNonce) .ToArray()); - if (!remoteChallenge.SequenceEqual(expectedRemoteChallenge)) + if (!FixedTimeEquals(remoteChallenge, expectedRemoteChallenge)) { _step = -1; 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. // @TODO: We can change our localNonce then send it - if (_remoteNonce.SequenceEqual(_localNonce)) + if (_remoteNonce == null || _remoteNonce.Length != NonceLength || FixedTimeEquals(_remoteNonce, _localNonce)) { _step = -1; return new AuthenticationResult(AuthenticationRuling.Failed, null); @@ -403,7 +440,7 @@ namespace Esiur.Security.Authority.Providers .Concat(_localNonce) .ToArray()); // compare remote challenge - if (!expectedRemoteChallenge.SequenceEqual(remoteChallenge)) + if (!FixedTimeEquals(expectedRemoteChallenge, remoteChallenge)) { _step = -1; 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. // @TODO: We can change our localNonce then send it - if (_remoteNonce.SequenceEqual(_localNonce)) + if (_remoteNonce == null || _remoteNonce.Length != NonceLength || FixedTimeEquals(_remoteNonce, _localNonce)) { _step = -1; return new AuthenticationResult(AuthenticationRuling.Failed, null); @@ -490,7 +527,7 @@ namespace Esiur.Security.Authority.Providers .ToArray()); // compare remote challenge - if (!expectedRemoteChallenge.SequenceEqual(remoteChallenge)) + if (!FixedTimeEquals(expectedRemoteChallenge, remoteChallenge)) { _step = -1; 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. // @TODO: We can change our localNonce then send it - if (_remoteNonce.SequenceEqual(_localNonce)) + if (_remoteNonce == null || _remoteNonce.Length != NonceLength || FixedTimeEquals(_remoteNonce, _localNonce)) { _step = -1; return new AuthenticationResult(AuthenticationRuling.Failed, null); @@ -597,7 +634,7 @@ namespace Esiur.Security.Authority.Providers .ToArray()); // compare remote challenge - if (!expectedRemoteChallenge.SequenceEqual(remoteChallenge)) + if (!FixedTimeEquals(expectedRemoteChallenge, remoteChallenge)) { _step = -1; return new AuthenticationResult(AuthenticationRuling.Failed, null); @@ -646,7 +683,7 @@ namespace Esiur.Security.Authority.Providers string domain, PasswordAuthenticationProvider provider) { - _localNonce = Global.GenerateBytes(20); + _localNonce = Global.GenerateBytes(NonceLength); this._provider = provider; this._initiatorIdentity = initiatorIdentity; diff --git a/Tests/Unit/AuthHandshakeTests.cs b/Tests/Unit/AuthHandshakeTests.cs new file mode 100644 index 0000000..adffdd6 --- /dev/null +++ b/Tests/Unit/AuthHandshakeTests.cs @@ -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; + +/// +/// Drives a pair of 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). +/// +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 _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)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); + } +} diff --git a/Tests/Unit/Esiur.Tests.Unit.csproj b/Tests/Unit/Esiur.Tests.Unit.csproj new file mode 100644 index 0000000..4246f3a --- /dev/null +++ b/Tests/Unit/Esiur.Tests.Unit.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tests/Unit/SecureRandomTests.cs b/Tests/Unit/SecureRandomTests.cs new file mode 100644 index 0000000..7831353 --- /dev/null +++ b/Tests/Unit/SecureRandomTests.cs @@ -0,0 +1,59 @@ +using System; +using System.Linq; +using Esiur.Misc; + +namespace Esiur.Tests.Unit; + +/// +/// 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. +/// +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)); + } +} diff --git a/Tests/Unit/SerializationRoundTripTests.cs b/Tests/Unit/SerializationRoundTripTests.cs new file mode 100644 index 0000000..57f7e1c --- /dev/null +++ b/Tests/Unit/SerializationRoundTripTests.cs @@ -0,0 +1,215 @@ +using System; +using System.Collections.Generic; +using Esiur.Data; +using Esiur.Resource; + +namespace Esiur.Tests.Unit; + +/// +/// 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 . +/// +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 ToObjectList(object value) + { + var got = new List(); + 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 { ["one"] = 1, ["two"] = 2 }; + var parsed = RoundTrip(value); + Assert.NotNull(parsed); + } +} diff --git a/Tests/Unit/TypedSerializationTests.cs b/Tests/Unit/TypedSerializationTests.cs new file mode 100644 index 0000000..f9c79fd --- /dev/null +++ b/Tests/Unit/TypedSerializationTests.cs @@ -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, +} + +/// +/// 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. +/// +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 { ["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 }, + }); + } +} diff --git a/Tests/Unit/WireFormatGoldenTests.cs b/Tests/Unit/WireFormatGoldenTests.cs new file mode 100644 index 0000000..a6cce33 --- /dev/null +++ b/Tests/Unit/WireFormatGoldenTests.cs @@ -0,0 +1,86 @@ +using System; +using Esiur.Data; +using Esiur.Resource; + +namespace Esiur.Tests.Unit; + +/// +/// 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. +/// +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)); + } +}