consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
@@ -155,11 +155,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.SourceI
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography", "..\\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj", "{F664A948-E352-5808-E780-77A03F19E93E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Core", "..\\Excititor\__Libraries\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj", "{9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Feedser.BinaryAnalysis", "..\\Feedser\StellaOps.Feedser.BinaryAnalysis\StellaOps.Feedser.BinaryAnalysis.csproj", "{CB296A20-2732-77C1-7F23-27D5BAEDD0C7}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Feedser.Core", "..\\Feedser\StellaOps.Feedser.Core\StellaOps.Feedser.Core.csproj", "{0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Excititor.Core", "..\\Concelier\__Libraries\StellaOps.Excititor.Core\StellaOps.Excititor.Core.csproj", "{9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Infrastructure.Postgres", "..\\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj", "{8C594D82-3463-3367-4F06-900AC707753D}"
|
||||
EndProject
|
||||
@@ -185,26 +181,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Normali
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Models", "..\Concelier\__Libraries\StellaOps.Concelier.Models\StellaOps.Concelier.Models.csproj", "{409A8978-55FB-4CBF-82FE-0BE3192284E1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.RawModels", "..\Concelier\__Libraries\StellaOps.Concelier.RawModels\StellaOps.Concelier.RawModels.csproj", "{3986C8D7-3EB0-4EDF-9E0F-D833AF50B3AD}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Plugin", "..\__Libraries\StellaOps.Plugin\StellaOps.Plugin.csproj", "{C632D90B-673B-4F8E-9287-CA7561B79C48}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.DependencyInjection", "..\__Libraries\StellaOps.DependencyInjection\StellaOps.DependencyInjection.csproj", "{A9F4D7D9-042A-44AE-8201-BBF48DA22661}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Cryptography", "..\__Libraries\StellaOps.Cryptography\StellaOps.Cryptography.csproj", "{DE94C81C-7699-4E92-82AE-D811F77ED7DC}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Core", "..\Concelier\__Libraries\StellaOps.Concelier.Core\StellaOps.Concelier.Core.csproj", "{439BCE02-2B9E-4B00-879B-329F06C987D5}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Ingestion.Telemetry", "..\__Libraries\StellaOps.Ingestion.Telemetry\StellaOps.Ingestion.Telemetry.csproj", "{885E394D-7FC9-4F5E-BE67-3B7C164B2846}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Provenance", "..\__Libraries\StellaOps.Provenance\StellaOps.Provenance.csproj", "{9F1BC667-7A66-4B26-AEC0-11ABFB8015D2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Aoc", "..\Aoc\__Libraries\StellaOps.Aoc\StellaOps.Aoc.csproj", "{4A709A7B-8A79-40BE-93F3-9D8037E4CC3C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Persistence", "..\Concelier\__Libraries\StellaOps.Concelier.Persistence\StellaOps.Concelier.Persistence.csproj", "{40440CD8-2B06-49A5-9F01-89EC02F40885}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Infrastructure.Postgres", "..\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj", "{F030414A-B815-4067-854A-D66E88AA7D91}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Infrastructure.EfCore", "..\__Libraries\StellaOps.Infrastructure.EfCore\StellaOps.Infrastructure.EfCore.csproj", "{0582E2E0-EEC4-43D8-99C7-ADE2F34CED4F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Interest", "..\Concelier\__Libraries\StellaOps.Concelier.Interest\StellaOps.Concelier.Interest.csproj", "{9A09E7B5-58EA-40E0-AD5B-BC75881AFE8B}"
|
||||
@@ -219,26 +205,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.Merge",
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.ProofService", "..\Concelier\__Libraries\StellaOps.Concelier.ProofService\StellaOps.Concelier.ProofService.csproj", "{98FE445B-1C5F-40BB-93C3-494CFD6EB2A9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Concelier.SourceIntel", "..\Concelier\__Libraries\StellaOps.Concelier.SourceIntel\StellaOps.Concelier.SourceIntel.csproj", "{E42F789A-1AE9-4A39-A598-F2372F11231A}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Feedser.Core", "..\Concelier\StellaOps.Feedser.Core\StellaOps.Feedser.Core.csproj", "{5A79046F-D7A9-47D0-B7A7-F608509EB094}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Feedser.Core", "..\Feedser\StellaOps.Feedser.Core\StellaOps.Feedser.Core.csproj", "{5A79046F-D7A9-47D0-B7A7-F608509EB094}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Feedser.BinaryAnalysis", "..\Feedser\StellaOps.Feedser.BinaryAnalysis\StellaOps.Feedser.BinaryAnalysis.csproj", "{A2061AB8-4E75-4D90-8702-B30E9087DC73}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.ProofChain", "..\Attestor\__Libraries\StellaOps.Attestor.ProofChain\StellaOps.Attestor.ProofChain.csproj", "{896F054B-6B0D-458E-9A86-010AE62BD199}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Canonical.Json", "..\__Libraries\StellaOps.Canonical.Json\StellaOps.Canonical.Json.csproj", "{8243922C-3720-49F1-8CBF-C7B5F9F7A143}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Attestor.Envelope", "..\Attestor\StellaOps.Attestor.Envelope\StellaOps.Attestor.Envelope.csproj", "{AF5ECA13-B6FC-4CBF-B38E-7049BC59F0C8}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Feedser.BinaryAnalysis", "..\Concelier\StellaOps.Feedser.BinaryAnalysis\StellaOps.Feedser.BinaryAnalysis.csproj", "{A2061AB8-4E75-4D90-8702-B30E9087DC73}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Provcache", "..\__Libraries\StellaOps.Provcache\StellaOps.Provcache.csproj", "{BF06778E-0C1A-44B3-A608-95C4605FE7FE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Provenance.Attestation", "..\Provenance\StellaOps.Provenance.Attestation\StellaOps.Provenance.Attestation.csproj", "{D7938493-65EE-4A6A-B9E3-904C1587A4DD}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Provenance.Attestation", "..\Attestor\StellaOps.Provenance.Attestation\StellaOps.Provenance.Attestation.csproj", "{D7938493-65EE-4A6A-B9E3-904C1587A4DD}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.VersionComparison", "..\__Libraries\StellaOps.VersionComparison\StellaOps.VersionComparison.csproj", "{DFB96B1D-D5C2-4775-ADEB-A302BAE5A099}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Infrastructure.Postgres.Testing", "..\__Tests\__Libraries\StellaOps.Infrastructure.Postgres.Testing\StellaOps.Infrastructure.Postgres.Testing.csproj", "{15CA713E-DFC3-4A9F-B623-614C46C40ABE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.Contracts.Tests", "__Tests\StellaOps.BinaryIndex.Contracts.Tests\StellaOps.BinaryIndex.Contracts.Tests.csproj", "{D5CA3FC2-CC92-4CB6-A894-7BA83A25E7C6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.Corpus.Tests", "__Tests\StellaOps.BinaryIndex.Corpus.Tests\StellaOps.BinaryIndex.Corpus.Tests.csproj", "{76B3C1EC-565B-4424-B242-DCAB40C7BD21}"
|
||||
@@ -259,8 +235,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.Disas
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.Semantic.Tests", "__Tests\StellaOps.BinaryIndex.Semantic.Tests\StellaOps.BinaryIndex.Semantic.Tests.csproj", "{89CCD547-09D4-4923-9644-17724AF60F1C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.TestKit", "..\__Libraries\StellaOps.TestKit\StellaOps.TestKit.csproj", "{C064F3B6-AF8E-4C92-A2FB-3BEF9FB7CC92}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.Ensemble", "__Libraries\StellaOps.BinaryIndex.Ensemble\StellaOps.BinaryIndex.Ensemble.csproj", "{7612CE73-B27A-4489-A89E-E22FF19981B7}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.Decompiler", "__Libraries\StellaOps.BinaryIndex.Decompiler\StellaOps.BinaryIndex.Decompiler.csproj", "{66EEF897-8006-4C53-B2AB-C55D82BDE6D7}"
|
||||
@@ -281,6 +255,20 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.Groun
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.BinaryIndex.GroundTruth.Reproducible", "__Libraries\StellaOps.BinaryIndex.GroundTruth.Reproducible\StellaOps.BinaryIndex.GroundTruth.Reproducible.csproj", "{C43AEE19-B4E1-41D8-8568-181889EB90E3}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Symbols.Core", "__Libraries\StellaOps.Symbols.Core\StellaOps.Symbols.Core.csproj", "{85B8B27B-51DD-025E-EEED-D44BC0D318B8}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Symbols.Client", "__Libraries\StellaOps.Symbols.Client\StellaOps.Symbols.Client.csproj", "{FFC170B2-A6F0-A1D7-02BD-16D813C8C8C0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Symbols.Infrastructure", "__Libraries\StellaOps.Symbols.Infrastructure\StellaOps.Symbols.Infrastructure.csproj", "{52B06550-8D39-5E07-3718-036FC7B21773}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Symbols.Marketplace", "__Libraries\StellaOps.Symbols.Marketplace\StellaOps.Symbols.Marketplace.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Symbols.Bundle", "__Libraries\StellaOps.Symbols.Bundle\StellaOps.Symbols.Bundle.csproj", "{8E47F8BB-B54F-40C9-6FB0-5F64BF5BE054}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Symbols.Server", "StellaOps.Symbols.Server\StellaOps.Symbols.Server.csproj", "{264AC7DD-45B3-7E71-BC04-F21E2D4E308A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.Symbols.Tests", "__Tests\StellaOps.Symbols.Tests\StellaOps.Symbols.Tests.csproj", "{AADF36CD-36BD-482F-8554-4D06668F2042}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -603,30 +591,6 @@ Global
|
||||
{9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}.Release|x64.Build.0 = Release|Any CPU
|
||||
{9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{9151601C-8784-01A6-C2E7-A5C0FAAB0AEF}.Release|x86.Build.0 = Release|Any CPU
|
||||
{CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Release|x64.Build.0 = Release|Any CPU
|
||||
{CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{CB296A20-2732-77C1-7F23-27D5BAEDD0C7}.Release|x86.Build.0 = Release|Any CPU
|
||||
{0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{8C594D82-3463-3367-4F06-900AC707753D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8C594D82-3463-3367-4F06-900AC707753D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8C594D82-3463-3367-4F06-900AC707753D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
@@ -771,18 +735,6 @@ Global
|
||||
{409A8978-55FB-4CBF-82FE-0BE3192284E1}.Release|x64.Build.0 = Release|Any CPU
|
||||
{409A8978-55FB-4CBF-82FE-0BE3192284E1}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{409A8978-55FB-4CBF-82FE-0BE3192284E1}.Release|x86.Build.0 = Release|Any CPU
|
||||
{3986C8D7-3EB0-4EDF-9E0F-D833AF50B3AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3986C8D7-3EB0-4EDF-9E0F-D833AF50B3AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3986C8D7-3EB0-4EDF-9E0F-D833AF50B3AD}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{3986C8D7-3EB0-4EDF-9E0F-D833AF50B3AD}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{3986C8D7-3EB0-4EDF-9E0F-D833AF50B3AD}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{3986C8D7-3EB0-4EDF-9E0F-D833AF50B3AD}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{3986C8D7-3EB0-4EDF-9E0F-D833AF50B3AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3986C8D7-3EB0-4EDF-9E0F-D833AF50B3AD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3986C8D7-3EB0-4EDF-9E0F-D833AF50B3AD}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{3986C8D7-3EB0-4EDF-9E0F-D833AF50B3AD}.Release|x64.Build.0 = Release|Any CPU
|
||||
{3986C8D7-3EB0-4EDF-9E0F-D833AF50B3AD}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{3986C8D7-3EB0-4EDF-9E0F-D833AF50B3AD}.Release|x86.Build.0 = Release|Any CPU
|
||||
{C632D90B-673B-4F8E-9287-CA7561B79C48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C632D90B-673B-4F8E-9287-CA7561B79C48}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C632D90B-673B-4F8E-9287-CA7561B79C48}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
@@ -807,18 +759,6 @@ Global
|
||||
{A9F4D7D9-042A-44AE-8201-BBF48DA22661}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A9F4D7D9-042A-44AE-8201-BBF48DA22661}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A9F4D7D9-042A-44AE-8201-BBF48DA22661}.Release|x86.Build.0 = Release|Any CPU
|
||||
{DE94C81C-7699-4E92-82AE-D811F77ED7DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DE94C81C-7699-4E92-82AE-D811F77ED7DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DE94C81C-7699-4E92-82AE-D811F77ED7DC}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{DE94C81C-7699-4E92-82AE-D811F77ED7DC}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{DE94C81C-7699-4E92-82AE-D811F77ED7DC}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{DE94C81C-7699-4E92-82AE-D811F77ED7DC}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{DE94C81C-7699-4E92-82AE-D811F77ED7DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DE94C81C-7699-4E92-82AE-D811F77ED7DC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DE94C81C-7699-4E92-82AE-D811F77ED7DC}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{DE94C81C-7699-4E92-82AE-D811F77ED7DC}.Release|x64.Build.0 = Release|Any CPU
|
||||
{DE94C81C-7699-4E92-82AE-D811F77ED7DC}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{DE94C81C-7699-4E92-82AE-D811F77ED7DC}.Release|x86.Build.0 = Release|Any CPU
|
||||
{439BCE02-2B9E-4B00-879B-329F06C987D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{439BCE02-2B9E-4B00-879B-329F06C987D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{439BCE02-2B9E-4B00-879B-329F06C987D5}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
@@ -831,18 +771,6 @@ Global
|
||||
{439BCE02-2B9E-4B00-879B-329F06C987D5}.Release|x64.Build.0 = Release|Any CPU
|
||||
{439BCE02-2B9E-4B00-879B-329F06C987D5}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{439BCE02-2B9E-4B00-879B-329F06C987D5}.Release|x86.Build.0 = Release|Any CPU
|
||||
{885E394D-7FC9-4F5E-BE67-3B7C164B2846}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{885E394D-7FC9-4F5E-BE67-3B7C164B2846}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{885E394D-7FC9-4F5E-BE67-3B7C164B2846}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{885E394D-7FC9-4F5E-BE67-3B7C164B2846}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{885E394D-7FC9-4F5E-BE67-3B7C164B2846}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{885E394D-7FC9-4F5E-BE67-3B7C164B2846}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{885E394D-7FC9-4F5E-BE67-3B7C164B2846}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{885E394D-7FC9-4F5E-BE67-3B7C164B2846}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{885E394D-7FC9-4F5E-BE67-3B7C164B2846}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{885E394D-7FC9-4F5E-BE67-3B7C164B2846}.Release|x64.Build.0 = Release|Any CPU
|
||||
{885E394D-7FC9-4F5E-BE67-3B7C164B2846}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{885E394D-7FC9-4F5E-BE67-3B7C164B2846}.Release|x86.Build.0 = Release|Any CPU
|
||||
{9F1BC667-7A66-4B26-AEC0-11ABFB8015D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9F1BC667-7A66-4B26-AEC0-11ABFB8015D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9F1BC667-7A66-4B26-AEC0-11ABFB8015D2}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
@@ -855,18 +783,6 @@ Global
|
||||
{9F1BC667-7A66-4B26-AEC0-11ABFB8015D2}.Release|x64.Build.0 = Release|Any CPU
|
||||
{9F1BC667-7A66-4B26-AEC0-11ABFB8015D2}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{9F1BC667-7A66-4B26-AEC0-11ABFB8015D2}.Release|x86.Build.0 = Release|Any CPU
|
||||
{4A709A7B-8A79-40BE-93F3-9D8037E4CC3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4A709A7B-8A79-40BE-93F3-9D8037E4CC3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4A709A7B-8A79-40BE-93F3-9D8037E4CC3C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{4A709A7B-8A79-40BE-93F3-9D8037E4CC3C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{4A709A7B-8A79-40BE-93F3-9D8037E4CC3C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{4A709A7B-8A79-40BE-93F3-9D8037E4CC3C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{4A709A7B-8A79-40BE-93F3-9D8037E4CC3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4A709A7B-8A79-40BE-93F3-9D8037E4CC3C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4A709A7B-8A79-40BE-93F3-9D8037E4CC3C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{4A709A7B-8A79-40BE-93F3-9D8037E4CC3C}.Release|x64.Build.0 = Release|Any CPU
|
||||
{4A709A7B-8A79-40BE-93F3-9D8037E4CC3C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{4A709A7B-8A79-40BE-93F3-9D8037E4CC3C}.Release|x86.Build.0 = Release|Any CPU
|
||||
{40440CD8-2B06-49A5-9F01-89EC02F40885}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{40440CD8-2B06-49A5-9F01-89EC02F40885}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{40440CD8-2B06-49A5-9F01-89EC02F40885}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
@@ -879,18 +795,6 @@ Global
|
||||
{40440CD8-2B06-49A5-9F01-89EC02F40885}.Release|x64.Build.0 = Release|Any CPU
|
||||
{40440CD8-2B06-49A5-9F01-89EC02F40885}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{40440CD8-2B06-49A5-9F01-89EC02F40885}.Release|x86.Build.0 = Release|Any CPU
|
||||
{F030414A-B815-4067-854A-D66E88AA7D91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F030414A-B815-4067-854A-D66E88AA7D91}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F030414A-B815-4067-854A-D66E88AA7D91}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{F030414A-B815-4067-854A-D66E88AA7D91}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{F030414A-B815-4067-854A-D66E88AA7D91}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{F030414A-B815-4067-854A-D66E88AA7D91}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{F030414A-B815-4067-854A-D66E88AA7D91}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F030414A-B815-4067-854A-D66E88AA7D91}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F030414A-B815-4067-854A-D66E88AA7D91}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{F030414A-B815-4067-854A-D66E88AA7D91}.Release|x64.Build.0 = Release|Any CPU
|
||||
{F030414A-B815-4067-854A-D66E88AA7D91}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F030414A-B815-4067-854A-D66E88AA7D91}.Release|x86.Build.0 = Release|Any CPU
|
||||
{0582E2E0-EEC4-43D8-99C7-ADE2F34CED4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0582E2E0-EEC4-43D8-99C7-ADE2F34CED4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0582E2E0-EEC4-43D8-99C7-ADE2F34CED4F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
@@ -975,18 +879,6 @@ Global
|
||||
{98FE445B-1C5F-40BB-93C3-494CFD6EB2A9}.Release|x64.Build.0 = Release|Any CPU
|
||||
{98FE445B-1C5F-40BB-93C3-494CFD6EB2A9}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{98FE445B-1C5F-40BB-93C3-494CFD6EB2A9}.Release|x86.Build.0 = Release|Any CPU
|
||||
{E42F789A-1AE9-4A39-A598-F2372F11231A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E42F789A-1AE9-4A39-A598-F2372F11231A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E42F789A-1AE9-4A39-A598-F2372F11231A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{E42F789A-1AE9-4A39-A598-F2372F11231A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{E42F789A-1AE9-4A39-A598-F2372F11231A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{E42F789A-1AE9-4A39-A598-F2372F11231A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{E42F789A-1AE9-4A39-A598-F2372F11231A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E42F789A-1AE9-4A39-A598-F2372F11231A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E42F789A-1AE9-4A39-A598-F2372F11231A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{E42F789A-1AE9-4A39-A598-F2372F11231A}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E42F789A-1AE9-4A39-A598-F2372F11231A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E42F789A-1AE9-4A39-A598-F2372F11231A}.Release|x86.Build.0 = Release|Any CPU
|
||||
{5A79046F-D7A9-47D0-B7A7-F608509EB094}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5A79046F-D7A9-47D0-B7A7-F608509EB094}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5A79046F-D7A9-47D0-B7A7-F608509EB094}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
@@ -1011,42 +903,6 @@ Global
|
||||
{A2061AB8-4E75-4D90-8702-B30E9087DC73}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A2061AB8-4E75-4D90-8702-B30E9087DC73}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A2061AB8-4E75-4D90-8702-B30E9087DC73}.Release|x86.Build.0 = Release|Any CPU
|
||||
{896F054B-6B0D-458E-9A86-010AE62BD199}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{896F054B-6B0D-458E-9A86-010AE62BD199}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{896F054B-6B0D-458E-9A86-010AE62BD199}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{896F054B-6B0D-458E-9A86-010AE62BD199}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{896F054B-6B0D-458E-9A86-010AE62BD199}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{896F054B-6B0D-458E-9A86-010AE62BD199}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{896F054B-6B0D-458E-9A86-010AE62BD199}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{896F054B-6B0D-458E-9A86-010AE62BD199}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{896F054B-6B0D-458E-9A86-010AE62BD199}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{896F054B-6B0D-458E-9A86-010AE62BD199}.Release|x64.Build.0 = Release|Any CPU
|
||||
{896F054B-6B0D-458E-9A86-010AE62BD199}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{896F054B-6B0D-458E-9A86-010AE62BD199}.Release|x86.Build.0 = Release|Any CPU
|
||||
{8243922C-3720-49F1-8CBF-C7B5F9F7A143}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8243922C-3720-49F1-8CBF-C7B5F9F7A143}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8243922C-3720-49F1-8CBF-C7B5F9F7A143}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{8243922C-3720-49F1-8CBF-C7B5F9F7A143}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{8243922C-3720-49F1-8CBF-C7B5F9F7A143}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8243922C-3720-49F1-8CBF-C7B5F9F7A143}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8243922C-3720-49F1-8CBF-C7B5F9F7A143}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8243922C-3720-49F1-8CBF-C7B5F9F7A143}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8243922C-3720-49F1-8CBF-C7B5F9F7A143}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{8243922C-3720-49F1-8CBF-C7B5F9F7A143}.Release|x64.Build.0 = Release|Any CPU
|
||||
{8243922C-3720-49F1-8CBF-C7B5F9F7A143}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8243922C-3720-49F1-8CBF-C7B5F9F7A143}.Release|x86.Build.0 = Release|Any CPU
|
||||
{AF5ECA13-B6FC-4CBF-B38E-7049BC59F0C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AF5ECA13-B6FC-4CBF-B38E-7049BC59F0C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AF5ECA13-B6FC-4CBF-B38E-7049BC59F0C8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{AF5ECA13-B6FC-4CBF-B38E-7049BC59F0C8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{AF5ECA13-B6FC-4CBF-B38E-7049BC59F0C8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{AF5ECA13-B6FC-4CBF-B38E-7049BC59F0C8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{AF5ECA13-B6FC-4CBF-B38E-7049BC59F0C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AF5ECA13-B6FC-4CBF-B38E-7049BC59F0C8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AF5ECA13-B6FC-4CBF-B38E-7049BC59F0C8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{AF5ECA13-B6FC-4CBF-B38E-7049BC59F0C8}.Release|x64.Build.0 = Release|Any CPU
|
||||
{AF5ECA13-B6FC-4CBF-B38E-7049BC59F0C8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{AF5ECA13-B6FC-4CBF-B38E-7049BC59F0C8}.Release|x86.Build.0 = Release|Any CPU
|
||||
{BF06778E-0C1A-44B3-A608-95C4605FE7FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BF06778E-0C1A-44B3-A608-95C4605FE7FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BF06778E-0C1A-44B3-A608-95C4605FE7FE}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
@@ -1083,18 +939,6 @@ Global
|
||||
{DFB96B1D-D5C2-4775-ADEB-A302BAE5A099}.Release|x64.Build.0 = Release|Any CPU
|
||||
{DFB96B1D-D5C2-4775-ADEB-A302BAE5A099}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{DFB96B1D-D5C2-4775-ADEB-A302BAE5A099}.Release|x86.Build.0 = Release|Any CPU
|
||||
{15CA713E-DFC3-4A9F-B623-614C46C40ABE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{15CA713E-DFC3-4A9F-B623-614C46C40ABE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{15CA713E-DFC3-4A9F-B623-614C46C40ABE}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{15CA713E-DFC3-4A9F-B623-614C46C40ABE}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{15CA713E-DFC3-4A9F-B623-614C46C40ABE}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{15CA713E-DFC3-4A9F-B623-614C46C40ABE}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{15CA713E-DFC3-4A9F-B623-614C46C40ABE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{15CA713E-DFC3-4A9F-B623-614C46C40ABE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{15CA713E-DFC3-4A9F-B623-614C46C40ABE}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{15CA713E-DFC3-4A9F-B623-614C46C40ABE}.Release|x64.Build.0 = Release|Any CPU
|
||||
{15CA713E-DFC3-4A9F-B623-614C46C40ABE}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{15CA713E-DFC3-4A9F-B623-614C46C40ABE}.Release|x86.Build.0 = Release|Any CPU
|
||||
{D5CA3FC2-CC92-4CB6-A894-7BA83A25E7C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D5CA3FC2-CC92-4CB6-A894-7BA83A25E7C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D5CA3FC2-CC92-4CB6-A894-7BA83A25E7C6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
@@ -1215,18 +1059,6 @@ Global
|
||||
{89CCD547-09D4-4923-9644-17724AF60F1C}.Release|x64.Build.0 = Release|Any CPU
|
||||
{89CCD547-09D4-4923-9644-17724AF60F1C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{89CCD547-09D4-4923-9644-17724AF60F1C}.Release|x86.Build.0 = Release|Any CPU
|
||||
{C064F3B6-AF8E-4C92-A2FB-3BEF9FB7CC92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C064F3B6-AF8E-4C92-A2FB-3BEF9FB7CC92}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C064F3B6-AF8E-4C92-A2FB-3BEF9FB7CC92}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{C064F3B6-AF8E-4C92-A2FB-3BEF9FB7CC92}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{C064F3B6-AF8E-4C92-A2FB-3BEF9FB7CC92}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C064F3B6-AF8E-4C92-A2FB-3BEF9FB7CC92}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C064F3B6-AF8E-4C92-A2FB-3BEF9FB7CC92}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C064F3B6-AF8E-4C92-A2FB-3BEF9FB7CC92}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C064F3B6-AF8E-4C92-A2FB-3BEF9FB7CC92}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{C064F3B6-AF8E-4C92-A2FB-3BEF9FB7CC92}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C064F3B6-AF8E-4C92-A2FB-3BEF9FB7CC92}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C064F3B6-AF8E-4C92-A2FB-3BEF9FB7CC92}.Release|x86.Build.0 = Release|Any CPU
|
||||
{7612CE73-B27A-4489-A89E-E22FF19981B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7612CE73-B27A-4489-A89E-E22FF19981B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7612CE73-B27A-4489-A89E-E22FF19981B7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
@@ -1350,6 +1182,90 @@ Global
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
{85B8B27B-51DD-025E-EEED-D44BC0D318B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{85B8B27B-51DD-025E-EEED-D44BC0D318B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{85B8B27B-51DD-025E-EEED-D44BC0D318B8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{85B8B27B-51DD-025E-EEED-D44BC0D318B8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{85B8B27B-51DD-025E-EEED-D44BC0D318B8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{85B8B27B-51DD-025E-EEED-D44BC0D318B8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{85B8B27B-51DD-025E-EEED-D44BC0D318B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{85B8B27B-51DD-025E-EEED-D44BC0D318B8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{85B8B27B-51DD-025E-EEED-D44BC0D318B8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{85B8B27B-51DD-025E-EEED-D44BC0D318B8}.Release|x64.Build.0 = Release|Any CPU
|
||||
{85B8B27B-51DD-025E-EEED-D44BC0D318B8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{85B8B27B-51DD-025E-EEED-D44BC0D318B8}.Release|x86.Build.0 = Release|Any CPU
|
||||
{FFC170B2-A6F0-A1D7-02BD-16D813C8C8C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FFC170B2-A6F0-A1D7-02BD-16D813C8C8C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FFC170B2-A6F0-A1D7-02BD-16D813C8C8C0}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{FFC170B2-A6F0-A1D7-02BD-16D813C8C8C0}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{FFC170B2-A6F0-A1D7-02BD-16D813C8C8C0}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{FFC170B2-A6F0-A1D7-02BD-16D813C8C8C0}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{FFC170B2-A6F0-A1D7-02BD-16D813C8C8C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FFC170B2-A6F0-A1D7-02BD-16D813C8C8C0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FFC170B2-A6F0-A1D7-02BD-16D813C8C8C0}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{FFC170B2-A6F0-A1D7-02BD-16D813C8C8C0}.Release|x64.Build.0 = Release|Any CPU
|
||||
{FFC170B2-A6F0-A1D7-02BD-16D813C8C8C0}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{FFC170B2-A6F0-A1D7-02BD-16D813C8C8C0}.Release|x86.Build.0 = Release|Any CPU
|
||||
{52B06550-8D39-5E07-3718-036FC7B21773}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{52B06550-8D39-5E07-3718-036FC7B21773}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{52B06550-8D39-5E07-3718-036FC7B21773}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{52B06550-8D39-5E07-3718-036FC7B21773}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{52B06550-8D39-5E07-3718-036FC7B21773}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{52B06550-8D39-5E07-3718-036FC7B21773}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{52B06550-8D39-5E07-3718-036FC7B21773}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{52B06550-8D39-5E07-3718-036FC7B21773}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{52B06550-8D39-5E07-3718-036FC7B21773}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{52B06550-8D39-5E07-3718-036FC7B21773}.Release|x64.Build.0 = Release|Any CPU
|
||||
{52B06550-8D39-5E07-3718-036FC7B21773}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{52B06550-8D39-5E07-3718-036FC7B21773}.Release|x86.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU
|
||||
{8E47F8BB-B54F-40C9-6FB0-5F64BF5BE054}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8E47F8BB-B54F-40C9-6FB0-5F64BF5BE054}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8E47F8BB-B54F-40C9-6FB0-5F64BF5BE054}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{8E47F8BB-B54F-40C9-6FB0-5F64BF5BE054}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{8E47F8BB-B54F-40C9-6FB0-5F64BF5BE054}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8E47F8BB-B54F-40C9-6FB0-5F64BF5BE054}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8E47F8BB-B54F-40C9-6FB0-5F64BF5BE054}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8E47F8BB-B54F-40C9-6FB0-5F64BF5BE054}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8E47F8BB-B54F-40C9-6FB0-5F64BF5BE054}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{8E47F8BB-B54F-40C9-6FB0-5F64BF5BE054}.Release|x64.Build.0 = Release|Any CPU
|
||||
{8E47F8BB-B54F-40C9-6FB0-5F64BF5BE054}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8E47F8BB-B54F-40C9-6FB0-5F64BF5BE054}.Release|x86.Build.0 = Release|Any CPU
|
||||
{264AC7DD-45B3-7E71-BC04-F21E2D4E308A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{264AC7DD-45B3-7E71-BC04-F21E2D4E308A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{264AC7DD-45B3-7E71-BC04-F21E2D4E308A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{264AC7DD-45B3-7E71-BC04-F21E2D4E308A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{264AC7DD-45B3-7E71-BC04-F21E2D4E308A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{264AC7DD-45B3-7E71-BC04-F21E2D4E308A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{264AC7DD-45B3-7E71-BC04-F21E2D4E308A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{264AC7DD-45B3-7E71-BC04-F21E2D4E308A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{264AC7DD-45B3-7E71-BC04-F21E2D4E308A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{264AC7DD-45B3-7E71-BC04-F21E2D4E308A}.Release|x64.Build.0 = Release|Any CPU
|
||||
{264AC7DD-45B3-7E71-BC04-F21E2D4E308A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{264AC7DD-45B3-7E71-BC04-F21E2D4E308A}.Release|x86.Build.0 = Release|Any CPU
|
||||
{AADF36CD-36BD-482F-8554-4D06668F2042}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AADF36CD-36BD-482F-8554-4D06668F2042}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AADF36CD-36BD-482F-8554-4D06668F2042}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{AADF36CD-36BD-482F-8554-4D06668F2042}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{AADF36CD-36BD-482F-8554-4D06668F2042}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{AADF36CD-36BD-482F-8554-4D06668F2042}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{AADF36CD-36BD-482F-8554-4D06668F2042}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AADF36CD-36BD-482F-8554-4D06668F2042}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AADF36CD-36BD-482F-8554-4D06668F2042}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{AADF36CD-36BD-482F-8554-4D06668F2042}.Release|x64.Build.0 = Release|Any CPU
|
||||
{AADF36CD-36BD-482F-8554-4D06668F2042}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{AADF36CD-36BD-482F-8554-4D06668F2042}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{03DFF14F-7321-1784-D4C7-4E99D4120F48} = {5B52EF8A-3661-DCFF-797D-BC4D6AC60BDA}
|
||||
@@ -1425,8 +1341,6 @@ Global
|
||||
{EB093C48-CDAC-106B-1196-AE34809B34C0} = {F2B58F4E-6F28-A25F-5BFB-CDEBAD6B9A3E}
|
||||
{F664A948-E352-5808-E780-77A03F19E93E} = {66557252-B5C4-664B-D807-07018C627474}
|
||||
{9151601C-8784-01A6-C2E7-A5C0FAAB0AEF} = {6DCAF6F3-717F-27A9-D96C-F2BFA5550347}
|
||||
{CB296A20-2732-77C1-7F23-27D5BAEDD0C7} = {054761F9-16D3-B2F8-6F4D-EFC2248805CD}
|
||||
{0DBEC9BA-FE1D-3898-B2C6-E4357DC23E0F} = {B54CE64C-4167-1DD1-B7D6-2FD7A5AEF715}
|
||||
{8C594D82-3463-3367-4F06-900AC707753D} = {61B23570-4F2D-B060-BE1F-37995682E494}
|
||||
{52F400CD-D473-7A1F-7986-89011CD2A887} = {CEDC2447-F717-3C95-7E08-F214D575A7B7}
|
||||
{9588FBF9-C37E-D16E-2E8F-CFA226EAC01D} = {1182764D-2143-EEF0-9270-3DCE392F5D06}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
using StellaOps.Symbols.Core.Models;
|
||||
|
||||
namespace StellaOps.Symbols.Server.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Request to upload a symbol manifest.
|
||||
/// </summary>
|
||||
public sealed record UploadSymbolManifestRequest(
|
||||
string DebugId,
|
||||
string BinaryName,
|
||||
string? CodeId,
|
||||
string? Platform,
|
||||
BinaryFormat Format,
|
||||
IReadOnlyList<SymbolEntryDto> Symbols,
|
||||
IReadOnlyList<SourceMappingDto>? SourceMappings);
|
||||
|
||||
/// <summary>
|
||||
/// Symbol entry DTO for API.
|
||||
/// </summary>
|
||||
public sealed record SymbolEntryDto(
|
||||
ulong Address,
|
||||
ulong Size,
|
||||
string MangledName,
|
||||
string? DemangledName,
|
||||
SymbolType Type,
|
||||
SymbolBinding Binding,
|
||||
string? SourceFile,
|
||||
int? SourceLine,
|
||||
string? ContentHash);
|
||||
|
||||
/// <summary>
|
||||
/// Source mapping DTO for API.
|
||||
/// </summary>
|
||||
public sealed record SourceMappingDto(
|
||||
string CompiledPath,
|
||||
string SourcePath,
|
||||
string? ContentHash);
|
||||
|
||||
/// <summary>
|
||||
/// Response from manifest upload.
|
||||
/// </summary>
|
||||
public sealed record UploadSymbolManifestResponse(
|
||||
string ManifestId,
|
||||
string DebugId,
|
||||
string BinaryName,
|
||||
string? BlobUri,
|
||||
int SymbolCount,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Request to resolve symbols.
|
||||
/// </summary>
|
||||
public sealed record ResolveSymbolsRequest(
|
||||
string DebugId,
|
||||
IReadOnlyList<ulong> Addresses);
|
||||
|
||||
/// <summary>
|
||||
/// Response from symbol resolution.
|
||||
/// </summary>
|
||||
public sealed record ResolveSymbolsResponse(
|
||||
string DebugId,
|
||||
IReadOnlyList<SymbolResolutionDto> Resolutions);
|
||||
|
||||
/// <summary>
|
||||
/// Symbol resolution DTO.
|
||||
/// </summary>
|
||||
public sealed record SymbolResolutionDto(
|
||||
ulong Address,
|
||||
bool Found,
|
||||
string? MangledName,
|
||||
string? DemangledName,
|
||||
ulong Offset,
|
||||
string? SourceFile,
|
||||
int? SourceLine,
|
||||
double Confidence);
|
||||
|
||||
/// <summary>
|
||||
/// Symbol manifest list response.
|
||||
/// </summary>
|
||||
public sealed record SymbolManifestListResponse(
|
||||
IReadOnlyList<SymbolManifestSummary> Manifests,
|
||||
int TotalCount,
|
||||
int Offset,
|
||||
int Limit);
|
||||
|
||||
/// <summary>
|
||||
/// Summary of a symbol manifest.
|
||||
/// </summary>
|
||||
public sealed record SymbolManifestSummary(
|
||||
string ManifestId,
|
||||
string DebugId,
|
||||
string? CodeId,
|
||||
string BinaryName,
|
||||
string? Platform,
|
||||
BinaryFormat Format,
|
||||
int SymbolCount,
|
||||
bool HasDsse,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Detailed manifest response.
|
||||
/// </summary>
|
||||
public sealed record SymbolManifestDetailResponse(
|
||||
string ManifestId,
|
||||
string DebugId,
|
||||
string? CodeId,
|
||||
string BinaryName,
|
||||
string? Platform,
|
||||
BinaryFormat Format,
|
||||
string TenantId,
|
||||
string? BlobUri,
|
||||
string? DsseDigest,
|
||||
long? RekorLogIndex,
|
||||
int SymbolCount,
|
||||
IReadOnlyList<SymbolEntryDto> Symbols,
|
||||
IReadOnlyList<SourceMappingDto>? SourceMappings,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Health check response.
|
||||
/// </summary>
|
||||
public sealed record SymbolsHealthResponse(
|
||||
string Status,
|
||||
string Version,
|
||||
DateTimeOffset Timestamp,
|
||||
SymbolsHealthMetrics? Metrics);
|
||||
|
||||
/// <summary>
|
||||
/// Health metrics.
|
||||
/// </summary>
|
||||
public sealed record SymbolsHealthMetrics(
|
||||
long TotalManifests,
|
||||
long TotalSymbols,
|
||||
long TotalBlobBytes);
|
||||
@@ -0,0 +1,133 @@
|
||||
using StellaOps.Symbols.Marketplace.Models;
|
||||
using StellaOps.Symbols.Marketplace.Repositories;
|
||||
|
||||
namespace StellaOps.Symbols.Server.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory marketplace catalog repository for development.
|
||||
/// </summary>
|
||||
internal sealed class InMemoryMarketplaceCatalogRepository : IMarketplaceCatalogRepository
|
||||
{
|
||||
private readonly List<SymbolPackCatalogEntry> _catalog =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("b0000000-0000-0000-0000-000000000001"),
|
||||
SourceId = Guid.Parse("a0000000-0000-0000-0000-000000000001"),
|
||||
PackId = "pkg:nuget/System.Runtime@10.0.0",
|
||||
Platform = "any",
|
||||
Components = ["System.Runtime"],
|
||||
DsseDigest = "sha256:aabbccdd",
|
||||
Version = "10.0.0",
|
||||
SizeBytes = 5_200_000,
|
||||
Installed = false,
|
||||
PublishedAt = DateTimeOffset.UtcNow.AddDays(-7),
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("b0000000-0000-0000-0000-000000000002"),
|
||||
SourceId = Guid.Parse("a0000000-0000-0000-0000-000000000002"),
|
||||
PackId = "pkg:deb/ubuntu/libc6-dbg@2.35-0ubuntu3",
|
||||
Platform = "linux/amd64",
|
||||
Components = ["libc6", "ld-linux"],
|
||||
DsseDigest = "sha256:11223344",
|
||||
Version = "2.35-0ubuntu3",
|
||||
SizeBytes = 15_000_000,
|
||||
Installed = true,
|
||||
PublishedAt = DateTimeOffset.UtcNow.AddDays(-14),
|
||||
InstalledAt = DateTimeOffset.UtcNow.AddDays(-3),
|
||||
},
|
||||
];
|
||||
|
||||
private readonly HashSet<string> _installedKeys = new(["default:b0000000-0000-0000-0000-000000000002"]);
|
||||
|
||||
public Task<IReadOnlyList<SymbolPackCatalogEntry>> ListCatalogAsync(
|
||||
Guid? sourceId,
|
||||
string? search,
|
||||
int limit,
|
||||
int offset,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _catalog.AsEnumerable();
|
||||
|
||||
if (sourceId.HasValue)
|
||||
{
|
||||
query = query.Where(e => e.SourceId == sourceId.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
var term = search.Trim();
|
||||
query = query.Where(e =>
|
||||
e.PackId.Contains(term, StringComparison.OrdinalIgnoreCase) ||
|
||||
e.Platform.Contains(term, StringComparison.OrdinalIgnoreCase) ||
|
||||
e.Components.Any(c => c.Contains(term, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
IReadOnlyList<SymbolPackCatalogEntry> result = query
|
||||
.Skip(offset)
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task<SymbolPackCatalogEntry?> GetCatalogEntryAsync(
|
||||
Guid entryId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entry = _catalog.FirstOrDefault(e => e.Id == entryId);
|
||||
return Task.FromResult(entry);
|
||||
}
|
||||
|
||||
public Task InstallPackAsync(
|
||||
Guid entryId,
|
||||
string tenantId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_installedKeys.Add($"{tenantId}:{entryId}");
|
||||
|
||||
var idx = _catalog.FindIndex(e => e.Id == entryId);
|
||||
if (idx >= 0)
|
||||
{
|
||||
_catalog[idx] = _catalog[idx] with
|
||||
{
|
||||
Installed = true,
|
||||
InstalledAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task UninstallPackAsync(
|
||||
Guid entryId,
|
||||
string tenantId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_installedKeys.Remove($"{tenantId}:{entryId}");
|
||||
|
||||
var idx = _catalog.FindIndex(e => e.Id == entryId);
|
||||
if (idx >= 0)
|
||||
{
|
||||
_catalog[idx] = _catalog[idx] with
|
||||
{
|
||||
Installed = false,
|
||||
InstalledAt = null,
|
||||
};
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<SymbolPackCatalogEntry>> ListInstalledAsync(
|
||||
string tenantId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
IReadOnlyList<SymbolPackCatalogEntry> result = _catalog
|
||||
.Where(e => _installedKeys.Contains($"{tenantId}:{e.Id}"))
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using StellaOps.Symbols.Marketplace.Models;
|
||||
using StellaOps.Symbols.Marketplace.Repositories;
|
||||
|
||||
namespace StellaOps.Symbols.Server.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory symbol source read repository for development.
|
||||
/// </summary>
|
||||
internal sealed class InMemorySymbolSourceReadRepository : ISymbolSourceReadRepository
|
||||
{
|
||||
private readonly List<SymbolSourceFreshnessRecord> _sources =
|
||||
[
|
||||
new(
|
||||
SourceId: Guid.Parse("a0000000-0000-0000-0000-000000000001"),
|
||||
SourceKey: "microsoft-symbols",
|
||||
SourceName: "Microsoft Public Symbols",
|
||||
SourceType: "vendor",
|
||||
SourceUrl: "https://msdl.microsoft.com/download/symbols",
|
||||
Priority: 1,
|
||||
Enabled: true,
|
||||
LastSyncAt: DateTimeOffset.UtcNow.AddMinutes(-30),
|
||||
LastSuccessAt: DateTimeOffset.UtcNow.AddMinutes(-30),
|
||||
LastError: null,
|
||||
SyncCount: 120,
|
||||
ErrorCount: 2,
|
||||
FreshnessSlaSeconds: 21600,
|
||||
WarningRatio: 0.80m,
|
||||
FreshnessAgeSeconds: 1800,
|
||||
FreshnessStatus: "healthy",
|
||||
SignatureStatus: "signed",
|
||||
TotalPacks: 450,
|
||||
SignedPacks: 445,
|
||||
UnsignedPacks: 5,
|
||||
SignatureFailureCount: 0),
|
||||
new(
|
||||
SourceId: Guid.Parse("a0000000-0000-0000-0000-000000000002"),
|
||||
SourceKey: "ubuntu-debuginfod",
|
||||
SourceName: "Ubuntu Debuginfod",
|
||||
SourceType: "distro",
|
||||
SourceUrl: "https://debuginfod.ubuntu.com",
|
||||
Priority: 2,
|
||||
Enabled: true,
|
||||
LastSyncAt: DateTimeOffset.UtcNow.AddHours(-2),
|
||||
LastSuccessAt: DateTimeOffset.UtcNow.AddHours(-2),
|
||||
LastError: null,
|
||||
SyncCount: 85,
|
||||
ErrorCount: 5,
|
||||
FreshnessSlaSeconds: 21600,
|
||||
WarningRatio: 0.80m,
|
||||
FreshnessAgeSeconds: 7200,
|
||||
FreshnessStatus: "healthy",
|
||||
SignatureStatus: "signed",
|
||||
TotalPacks: 280,
|
||||
SignedPacks: 260,
|
||||
UnsignedPacks: 20,
|
||||
SignatureFailureCount: 1),
|
||||
];
|
||||
|
||||
public Task<IReadOnlyList<SymbolSourceFreshnessRecord>> ListSourcesAsync(
|
||||
bool includeDisabled,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
IReadOnlyList<SymbolSourceFreshnessRecord> result = includeDisabled
|
||||
? _sources
|
||||
: _sources.Where(s => s.Enabled).ToList();
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public Task<SymbolSourceFreshnessRecord?> GetSourceByIdAsync(
|
||||
Guid sourceId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var source = _sources.FirstOrDefault(s => s.SourceId == sourceId);
|
||||
return Task.FromResult(source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using StellaOps.Symbols.Marketplace.Models;
|
||||
using StellaOps.Symbols.Marketplace.Repositories;
|
||||
using StellaOps.Symbols.Marketplace.Scoring;
|
||||
using StellaOps.Symbols.Server.Security;
|
||||
using StellaOps.Auth.ServerIntegration.Tenancy;
|
||||
|
||||
namespace StellaOps.Symbols.Server.Endpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Symbol source and marketplace catalog endpoints.
|
||||
/// </summary>
|
||||
public static class SymbolSourceEndpoints
|
||||
{
|
||||
private const int DefaultLimit = 50;
|
||||
private const int MaxLimit = 200;
|
||||
|
||||
public static IEndpointRouteBuilder MapSymbolSourceEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
// --- Symbol Sources ---
|
||||
var sources = app.MapGroup("/api/v1/symbols/sources")
|
||||
.WithTags("Symbol Sources")
|
||||
.RequireAuthorization(SymbolsPolicies.Read)
|
||||
.RequireTenant();
|
||||
|
||||
sources.MapGet(string.Empty, async (
|
||||
ISymbolSourceReadRepository repository,
|
||||
[FromQuery] bool includeDisabled,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var items = await repository.ListSourcesAsync(includeDisabled, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
items,
|
||||
totalCount = items.Count,
|
||||
dataAsOf = DateTimeOffset.UtcNow,
|
||||
});
|
||||
})
|
||||
.WithName("ListSymbolSources")
|
||||
.WithSummary("List symbol sources with freshness projections")
|
||||
.WithDescription("Returns all configured symbol pack sources with their freshness projections, sync state, and trust metadata. Optionally includes disabled sources when includeDisabled is true. Requires authentication.");
|
||||
|
||||
sources.MapGet("/summary", async (
|
||||
ISymbolSourceReadRepository repository,
|
||||
ISymbolSourceTrustScorer scorer,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var items = await repository.ListSourcesAsync(false, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var healthy = items.Count(s => s.FreshnessStatus == "healthy");
|
||||
var warning = items.Count(s => s.FreshnessStatus == "warning");
|
||||
var stale = items.Count(s => s.FreshnessStatus == "stale");
|
||||
var unavailable = items.Count(s => s.FreshnessStatus == "unavailable");
|
||||
|
||||
var avgTrust = items.Count > 0
|
||||
? items.Average(s => scorer.CalculateTrust(s).Overall)
|
||||
: 0.0;
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
totalSources = items.Count,
|
||||
healthySources = healthy,
|
||||
warningSources = warning,
|
||||
staleSources = stale,
|
||||
unavailableSources = unavailable,
|
||||
averageTrustScore = Math.Round(avgTrust, 4),
|
||||
dataAsOf = DateTimeOffset.UtcNow,
|
||||
});
|
||||
})
|
||||
.WithName("GetSymbolSourceSummary")
|
||||
.WithSummary("Get symbol source summary cards")
|
||||
.WithDescription("Returns aggregated health summary cards for all enabled symbol sources including counts of healthy, warning, stale, and unavailable sources and the average trust score across the fleet. Requires authentication.");
|
||||
|
||||
sources.MapGet("/{id:guid}", async (
|
||||
Guid id,
|
||||
ISymbolSourceReadRepository repository,
|
||||
ISymbolSourceTrustScorer scorer,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var source = await repository.GetSourceByIdAsync(id, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (source is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "source_not_found", id });
|
||||
}
|
||||
|
||||
var trust = scorer.CalculateTrust(source);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
source,
|
||||
trust,
|
||||
dataAsOf = DateTimeOffset.UtcNow,
|
||||
});
|
||||
})
|
||||
.WithName("GetSymbolSource")
|
||||
.WithSummary("Get symbol source detail with trust score")
|
||||
.WithDescription("Returns the full symbol source record by ID including its sync state, freshness projection, and computed trust score breakdown. Returns 404 if the source is not found. Requires authentication.");
|
||||
|
||||
sources.MapGet("/{id:guid}/freshness", async (
|
||||
Guid id,
|
||||
ISymbolSourceReadRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var source = await repository.GetSourceByIdAsync(id, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (source is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "source_not_found", id });
|
||||
}
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
source.SourceId,
|
||||
source.SourceKey,
|
||||
source.FreshnessStatus,
|
||||
source.FreshnessAgeSeconds,
|
||||
source.FreshnessSlaSeconds,
|
||||
source.LastSyncAt,
|
||||
source.LastSuccessAt,
|
||||
source.LastError,
|
||||
source.SyncCount,
|
||||
source.ErrorCount,
|
||||
dataAsOf = DateTimeOffset.UtcNow,
|
||||
});
|
||||
})
|
||||
.WithName("GetSymbolSourceFreshness")
|
||||
.WithSummary("Get symbol source freshness detail")
|
||||
.WithDescription("Returns freshness detail for a specific symbol source including status, age in seconds, SLA threshold, last sync time, last successful sync, last error, and cumulative sync and error counts. Returns 404 if the source is not found. Requires authentication.");
|
||||
|
||||
sources.MapPost(string.Empty, (SymbolPackSource request) =>
|
||||
{
|
||||
// Placeholder: in production, persist via write repository.
|
||||
var created = request with
|
||||
{
|
||||
Id = request.Id == Guid.Empty ? Guid.NewGuid() : request.Id,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
return Results.Created($"/api/v1/symbols/sources/{created.Id}", created);
|
||||
})
|
||||
.WithName("CreateSymbolSource")
|
||||
.WithSummary("Create a new symbol source")
|
||||
.WithDescription("Creates a new symbol pack source with the provided configuration. Assigns a new ID if not supplied. Returns 201 Created with the created source record. Requires authentication.");
|
||||
|
||||
sources.MapPut("/{id:guid}", (Guid id, SymbolPackSource request) =>
|
||||
{
|
||||
var updated = request with
|
||||
{
|
||||
Id = id,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
return Results.Ok(updated);
|
||||
})
|
||||
.WithName("UpdateSymbolSource")
|
||||
.WithSummary("Update a symbol source")
|
||||
.WithDescription("Replaces the configuration of an existing symbol source by ID, updating its metadata, freshness SLA, and enabled state. Returns 200 with the updated source record. Requires authentication.");
|
||||
|
||||
sources.MapDelete("/{id:guid}", (Guid id) =>
|
||||
{
|
||||
return Results.NoContent();
|
||||
})
|
||||
.WithName("DisableSymbolSource")
|
||||
.WithSummary("Disable (soft-delete) a symbol source")
|
||||
.WithDescription("Soft-deletes (disables) a symbol source by ID, preventing it from appearing in default listings without permanently removing its history. Returns 204 No Content on success. Requires authentication.");
|
||||
|
||||
// --- Marketplace Catalog ---
|
||||
var marketplace = app.MapGroup("/api/v1/symbols/marketplace")
|
||||
.WithTags("Symbol Marketplace")
|
||||
.RequireAuthorization(SymbolsPolicies.Read)
|
||||
.RequireTenant();
|
||||
|
||||
marketplace.MapGet(string.Empty, async (
|
||||
IMarketplaceCatalogRepository repository,
|
||||
[FromQuery] Guid? sourceId,
|
||||
[FromQuery] string? search,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] int? offset,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var normalizedLimit = NormalizeLimit(limit);
|
||||
var normalizedOffset = NormalizeOffset(offset);
|
||||
|
||||
var items = await repository.ListCatalogAsync(
|
||||
sourceId, search, normalizedLimit, normalizedOffset, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
items,
|
||||
totalCount = items.Count,
|
||||
limit = normalizedLimit,
|
||||
offset = normalizedOffset,
|
||||
dataAsOf = DateTimeOffset.UtcNow,
|
||||
});
|
||||
})
|
||||
.WithName("ListMarketplaceCatalog")
|
||||
.WithSummary("List symbol pack catalog entries")
|
||||
.WithDescription("Returns a paginated list of symbol pack catalog entries, optionally filtered by source ID and a free-text search term. Results include pack metadata and are bounded by the configured limit and offset. Requires authentication.");
|
||||
|
||||
marketplace.MapGet("/search", async (
|
||||
IMarketplaceCatalogRepository repository,
|
||||
[FromQuery] string? q,
|
||||
[FromQuery] string? platform,
|
||||
[FromQuery] int? limit,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var normalizedLimit = NormalizeLimit(limit);
|
||||
var searchTerm = string.IsNullOrWhiteSpace(q) ? platform : q;
|
||||
|
||||
var items = await repository.ListCatalogAsync(
|
||||
null, searchTerm, normalizedLimit, 0, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
items,
|
||||
totalCount = items.Count,
|
||||
dataAsOf = DateTimeOffset.UtcNow,
|
||||
});
|
||||
})
|
||||
.WithName("SearchMarketplaceCatalog")
|
||||
.WithSummary("Search catalog by PURL or platform")
|
||||
.WithDescription("Searches the symbol pack marketplace catalog by a free-text query (q) or platform string. Falls back to platform if q is empty. Returns matching catalog entries up to the specified limit. Requires authentication.");
|
||||
|
||||
marketplace.MapGet("/{entryId:guid}", async (
|
||||
Guid entryId,
|
||||
IMarketplaceCatalogRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var entry = await repository.GetCatalogEntryAsync(entryId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (entry is null)
|
||||
{
|
||||
return Results.NotFound(new { error = "catalog_entry_not_found", entryId });
|
||||
}
|
||||
|
||||
return Results.Ok(new { entry, dataAsOf = DateTimeOffset.UtcNow });
|
||||
})
|
||||
.WithName("GetMarketplaceCatalogEntry")
|
||||
.WithSummary("Get catalog entry detail")
|
||||
.WithDescription("Returns the full catalog entry record for a specific marketplace entry ID including pack metadata, publisher, version, and install eligibility. Returns 404 if the entry is not found. Requires authentication.");
|
||||
|
||||
marketplace.MapPost("/{entryId:guid}/install", async (
|
||||
HttpContext httpContext,
|
||||
Guid entryId,
|
||||
IMarketplaceCatalogRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var tenantId = httpContext.Request.Headers["X-Stella-Tenant"].ToString();
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
return Results.BadRequest(new { error = "missing_tenant" });
|
||||
}
|
||||
|
||||
await repository.InstallPackAsync(entryId, tenantId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new { entryId, status = "installed", dataAsOf = DateTimeOffset.UtcNow });
|
||||
})
|
||||
.WithName("InstallMarketplacePack")
|
||||
.WithSummary("Install a symbol pack from the marketplace")
|
||||
.WithDescription("Installs a symbol pack from the marketplace catalog for the requesting tenant, recording the installation against the specified catalog entry ID. Returns 200 with the installation status. Returns 400 if the X-Stella-Tenant header is missing. Requires authentication.");
|
||||
|
||||
marketplace.MapPost("/{entryId:guid}/uninstall", async (
|
||||
HttpContext httpContext,
|
||||
Guid entryId,
|
||||
IMarketplaceCatalogRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var tenantId = httpContext.Request.Headers["X-Stella-Tenant"].ToString();
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
return Results.BadRequest(new { error = "missing_tenant" });
|
||||
}
|
||||
|
||||
await repository.UninstallPackAsync(entryId, tenantId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new { entryId, status = "uninstalled", dataAsOf = DateTimeOffset.UtcNow });
|
||||
})
|
||||
.WithName("UninstallMarketplacePack")
|
||||
.WithSummary("Uninstall a symbol pack")
|
||||
.WithDescription("Removes the installation of a symbol pack for the requesting tenant by catalog entry ID. Returns 200 with the uninstall status. Returns 400 if the X-Stella-Tenant header is missing. Requires authentication.");
|
||||
|
||||
marketplace.MapGet("/installed", async (
|
||||
HttpContext httpContext,
|
||||
IMarketplaceCatalogRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var tenantId = httpContext.Request.Headers["X-Stella-Tenant"].ToString();
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
return Results.BadRequest(new { error = "missing_tenant" });
|
||||
}
|
||||
|
||||
var items = await repository.ListInstalledAsync(tenantId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
items,
|
||||
totalCount = items.Count,
|
||||
dataAsOf = DateTimeOffset.UtcNow,
|
||||
});
|
||||
})
|
||||
.WithName("ListInstalledPacks")
|
||||
.WithSummary("List installed symbol packs")
|
||||
.WithDescription("Returns all symbol packs currently installed for the requesting tenant. Returns 400 if the X-Stella-Tenant header is missing. Requires authentication.");
|
||||
|
||||
marketplace.MapPost("/sync", (HttpContext httpContext) =>
|
||||
{
|
||||
var tenantId = httpContext.Request.Headers["X-Stella-Tenant"].ToString();
|
||||
if (string.IsNullOrWhiteSpace(tenantId))
|
||||
{
|
||||
return Results.BadRequest(new { error = "missing_tenant" });
|
||||
}
|
||||
|
||||
return Results.Accepted(value: new
|
||||
{
|
||||
status = "sync_queued",
|
||||
tenantId,
|
||||
dataAsOf = DateTimeOffset.UtcNow,
|
||||
});
|
||||
})
|
||||
.WithName("TriggerMarketplaceSync")
|
||||
.WithSummary("Trigger marketplace sync from configured sources")
|
||||
.WithDescription("Enqueues a marketplace sync job to refresh the symbol pack catalog from all configured sources for the requesting tenant. Returns 202 Accepted with the queued status. Returns 400 if the X-Stella-Tenant header is missing. Requires authentication.");
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static int NormalizeLimit(int? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
null => DefaultLimit,
|
||||
< 1 => 1,
|
||||
> MaxLimit => MaxLimit,
|
||||
_ => value.Value,
|
||||
};
|
||||
}
|
||||
|
||||
private static int NormalizeOffset(int? value) => value is null or < 0 ? 0 : value.Value;
|
||||
}
|
||||
343
src/BinaryIndex/StellaOps.Symbols.Server/Program.cs
Normal file
343
src/BinaryIndex/StellaOps.Symbols.Server/Program.cs
Normal file
@@ -0,0 +1,343 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Auth.ServerIntegration.Tenancy;
|
||||
using StellaOps.Symbols.Core.Abstractions;
|
||||
using StellaOps.Symbols.Core.Models;
|
||||
using StellaOps.Symbols.Infrastructure;
|
||||
using StellaOps.Symbols.Infrastructure.Hashing;
|
||||
using StellaOps.Symbols.Marketplace.Scoring;
|
||||
using StellaOps.Symbols.Server.Contracts;
|
||||
using StellaOps.Symbols.Server.Endpoints;
|
||||
using StellaOps.Symbols.Server.Security;
|
||||
|
||||
using StellaOps.Router.AspNet;
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Authentication and Authorization
|
||||
builder.Services.AddStellaOpsResourceServerAuthentication(
|
||||
builder.Configuration,
|
||||
configure: options =>
|
||||
{
|
||||
options.RequiredScopes.Clear();
|
||||
});
|
||||
builder.Services.AddStellaOpsTenantServices();
|
||||
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddStellaOpsScopePolicy(SymbolsPolicies.Read, StellaOpsScopes.SymbolsRead);
|
||||
options.AddStellaOpsScopePolicy(SymbolsPolicies.Write, StellaOpsScopes.SymbolsWrite);
|
||||
});
|
||||
|
||||
// Symbols services (in-memory for development)
|
||||
builder.Services.AddSymbolsInMemory();
|
||||
|
||||
// Marketplace services
|
||||
builder.Services.AddSingleton<ISymbolSourceTrustScorer, DefaultSymbolSourceTrustScorer>();
|
||||
builder.Services.AddSingleton<StellaOps.Symbols.Marketplace.Repositories.ISymbolSourceReadRepository, StellaOps.Symbols.Server.Endpoints.InMemorySymbolSourceReadRepository>();
|
||||
builder.Services.AddSingleton<StellaOps.Symbols.Marketplace.Repositories.IMarketplaceCatalogRepository, StellaOps.Symbols.Server.Endpoints.InMemoryMarketplaceCatalogRepository>();
|
||||
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
// Stella Router integration
|
||||
var routerEnabled = builder.Services.AddRouterMicroservice(
|
||||
builder.Configuration,
|
||||
serviceName: "symbols",
|
||||
version: System.Reflection.CustomAttributeExtensions.GetCustomAttribute<System.Reflection.AssemblyInformationalVersionAttribute>(System.Reflection.Assembly.GetExecutingAssembly())?.InformationalVersion ?? "1.0.0",
|
||||
routerOptionsSection: "Router");
|
||||
builder.TryAddStellaOpsLocalBinding("symbols");
|
||||
var app = builder.Build();
|
||||
app.LogStellaOpsLocalHostname("symbols");
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.UseStellaOpsCors();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseStellaOpsTenantMiddleware();
|
||||
app.TryUseStellaRouter(routerEnabled);
|
||||
|
||||
// Health endpoint (anonymous)
|
||||
app.MapGet("/health", () =>
|
||||
{
|
||||
return TypedResults.Ok(new SymbolsHealthResponse(
|
||||
Status: "healthy",
|
||||
Version: "1.0.0",
|
||||
Timestamp: DateTimeOffset.UtcNow,
|
||||
Metrics: null));
|
||||
})
|
||||
.AllowAnonymous()
|
||||
.WithName("GetHealth")
|
||||
.WithSummary("Health check endpoint");
|
||||
|
||||
// Upload symbol manifest
|
||||
app.MapPost("/v1/symbols/manifests", async Task<Results<Created<UploadSymbolManifestResponse>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
UploadSymbolManifestRequest request,
|
||||
ISymbolRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
var symbols = request.Symbols.Select(s => new SymbolEntry
|
||||
{
|
||||
Address = s.Address,
|
||||
Size = s.Size,
|
||||
MangledName = s.MangledName,
|
||||
DemangledName = s.DemangledName,
|
||||
Type = s.Type,
|
||||
Binding = s.Binding,
|
||||
SourceFile = s.SourceFile,
|
||||
SourceLine = s.SourceLine,
|
||||
ContentHash = s.ContentHash
|
||||
}).ToList();
|
||||
|
||||
var sourceMappings = request.SourceMappings?.Select(m => new SourceMapping
|
||||
{
|
||||
CompiledPath = m.CompiledPath,
|
||||
SourcePath = m.SourcePath,
|
||||
ContentHash = m.ContentHash
|
||||
}).ToList();
|
||||
|
||||
var manifestId = ComputeManifestId(request.DebugId, tenantId, symbols);
|
||||
|
||||
var manifest = new SymbolManifest
|
||||
{
|
||||
ManifestId = manifestId,
|
||||
DebugId = request.DebugId,
|
||||
CodeId = request.CodeId,
|
||||
BinaryName = request.BinaryName,
|
||||
Platform = request.Platform,
|
||||
Format = request.Format,
|
||||
Symbols = symbols,
|
||||
SourceMappings = sourceMappings,
|
||||
TenantId = tenantId,
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
await repository.StoreManifestAsync(manifest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = new UploadSymbolManifestResponse(
|
||||
ManifestId: manifestId,
|
||||
DebugId: request.DebugId,
|
||||
BinaryName: request.BinaryName,
|
||||
BlobUri: manifest.BlobUri,
|
||||
SymbolCount: symbols.Count,
|
||||
CreatedAt: manifest.CreatedAt);
|
||||
|
||||
return TypedResults.Created($"/v1/symbols/manifests/{manifestId}", response);
|
||||
})
|
||||
.RequireAuthorization(SymbolsPolicies.Write)
|
||||
.WithName("UploadSymbolManifest")
|
||||
.WithSummary("Upload a symbol manifest")
|
||||
.Produces(StatusCodes.Status201Created)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// Get manifest by ID
|
||||
app.MapGet("/v1/symbols/manifests/{manifestId}", async Task<Results<Ok<SymbolManifestDetailResponse>, NotFound, ProblemHttpResult>> (
|
||||
string manifestId,
|
||||
ISymbolRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var manifest = await repository.GetManifestAsync(manifestId, cancellationToken).ConfigureAwait(false);
|
||||
if (manifest is null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
var response = MapToDetailResponse(manifest);
|
||||
return TypedResults.Ok(response);
|
||||
})
|
||||
.RequireAuthorization(SymbolsPolicies.Read)
|
||||
.WithName("GetSymbolManifest")
|
||||
.WithSummary("Get symbol manifest by ID")
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// Query manifests
|
||||
app.MapGet("/v1/symbols/manifests", async Task<Results<Ok<SymbolManifestListResponse>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
ISymbolRepository repository,
|
||||
string? debugId,
|
||||
string? codeId,
|
||||
string? binaryName,
|
||||
string? platform,
|
||||
int? limit,
|
||||
int? offset,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
var query = new SymbolQuery
|
||||
{
|
||||
TenantId = tenantId,
|
||||
DebugId = debugId,
|
||||
CodeId = codeId,
|
||||
BinaryName = binaryName,
|
||||
Platform = platform,
|
||||
Limit = limit ?? 50,
|
||||
Offset = offset ?? 0
|
||||
};
|
||||
|
||||
var result = await repository.QueryManifestsAsync(query, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var summaries = result.Manifests.Select(m => new SymbolManifestSummary(
|
||||
ManifestId: m.ManifestId,
|
||||
DebugId: m.DebugId,
|
||||
CodeId: m.CodeId,
|
||||
BinaryName: m.BinaryName,
|
||||
Platform: m.Platform,
|
||||
Format: m.Format,
|
||||
SymbolCount: m.Symbols.Count,
|
||||
HasDsse: !string.IsNullOrEmpty(m.DsseDigest),
|
||||
CreatedAt: m.CreatedAt)).ToList();
|
||||
|
||||
return TypedResults.Ok(new SymbolManifestListResponse(
|
||||
Manifests: summaries,
|
||||
TotalCount: result.TotalCount,
|
||||
Offset: result.Offset,
|
||||
Limit: result.Limit));
|
||||
})
|
||||
.RequireAuthorization(SymbolsPolicies.Read)
|
||||
.WithName("QuerySymbolManifests")
|
||||
.WithSummary("Query symbol manifests")
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// Resolve symbols
|
||||
app.MapPost("/v1/symbols/resolve", async Task<Results<Ok<ResolveSymbolsResponse>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
ResolveSymbolsRequest request,
|
||||
ISymbolResolver resolver,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
var resolutions = await resolver.ResolveBatchAsync(
|
||||
request.DebugId,
|
||||
request.Addresses,
|
||||
tenantId,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var dtos = resolutions.Select(r => new SymbolResolutionDto(
|
||||
Address: r.Address,
|
||||
Found: r.Found,
|
||||
MangledName: r.Symbol?.MangledName,
|
||||
DemangledName: r.Symbol?.DemangledName,
|
||||
Offset: r.Offset,
|
||||
SourceFile: r.Symbol?.SourceFile,
|
||||
SourceLine: r.Symbol?.SourceLine,
|
||||
Confidence: r.Confidence)).ToList();
|
||||
|
||||
return TypedResults.Ok(new ResolveSymbolsResponse(
|
||||
DebugId: request.DebugId,
|
||||
Resolutions: dtos));
|
||||
})
|
||||
.RequireAuthorization(SymbolsPolicies.Read)
|
||||
.WithName("ResolveSymbols")
|
||||
.WithSummary("Resolve symbol addresses")
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
// Symbol source and marketplace endpoints
|
||||
app.MapSymbolSourceEndpoints();
|
||||
|
||||
// Get manifests by debug ID
|
||||
app.MapGet("/v1/symbols/by-debug-id/{debugId}", async Task<Results<Ok<SymbolManifestListResponse>, ProblemHttpResult>> (
|
||||
HttpContext httpContext,
|
||||
string debugId,
|
||||
ISymbolRepository repository,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (!TryGetTenant(httpContext, out var tenantProblem, out var tenantId))
|
||||
{
|
||||
return tenantProblem!;
|
||||
}
|
||||
|
||||
var manifests = await repository.GetManifestsByDebugIdAsync(debugId, tenantId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var summaries = manifests.Select(m => new SymbolManifestSummary(
|
||||
ManifestId: m.ManifestId,
|
||||
DebugId: m.DebugId,
|
||||
CodeId: m.CodeId,
|
||||
BinaryName: m.BinaryName,
|
||||
Platform: m.Platform,
|
||||
Format: m.Format,
|
||||
SymbolCount: m.Symbols.Count,
|
||||
HasDsse: !string.IsNullOrEmpty(m.DsseDigest),
|
||||
CreatedAt: m.CreatedAt)).ToList();
|
||||
|
||||
return TypedResults.Ok(new SymbolManifestListResponse(
|
||||
Manifests: summaries,
|
||||
TotalCount: summaries.Count,
|
||||
Offset: 0,
|
||||
Limit: summaries.Count));
|
||||
})
|
||||
.RequireAuthorization(SymbolsPolicies.Read)
|
||||
.WithName("GetManifestsByDebugId")
|
||||
.WithSummary("Get manifests by debug ID")
|
||||
.Produces(StatusCodes.Status200OK)
|
||||
.ProducesProblem(StatusCodes.Status400BadRequest);
|
||||
|
||||
app.TryRefreshStellaRouterEndpoints(routerEnabled);
|
||||
app.Run();
|
||||
|
||||
static bool TryGetTenant(HttpContext httpContext, out ProblemHttpResult? problem, out string tenantId)
|
||||
{
|
||||
tenantId = string.Empty;
|
||||
if (!httpContext.Request.Headers.TryGetValue("X-Stella-Tenant", out var tenantValues) ||
|
||||
string.IsNullOrWhiteSpace(tenantValues))
|
||||
{
|
||||
problem = TypedResults.Problem(statusCode: StatusCodes.Status400BadRequest, title: "missing_tenant");
|
||||
return false;
|
||||
}
|
||||
|
||||
tenantId = tenantValues.ToString();
|
||||
problem = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
static string ComputeManifestId(string debugId, string tenantId, IReadOnlyList<SymbolEntry> symbols)
|
||||
{
|
||||
return SymbolHashing.ComputeManifestId(debugId, tenantId, symbols);
|
||||
}
|
||||
|
||||
static SymbolManifestDetailResponse MapToDetailResponse(SymbolManifest manifest)
|
||||
{
|
||||
return new SymbolManifestDetailResponse(
|
||||
ManifestId: manifest.ManifestId,
|
||||
DebugId: manifest.DebugId,
|
||||
CodeId: manifest.CodeId,
|
||||
BinaryName: manifest.BinaryName,
|
||||
Platform: manifest.Platform,
|
||||
Format: manifest.Format,
|
||||
TenantId: manifest.TenantId,
|
||||
BlobUri: manifest.BlobUri,
|
||||
DsseDigest: manifest.DsseDigest,
|
||||
RekorLogIndex: manifest.RekorLogIndex,
|
||||
SymbolCount: manifest.Symbols.Count,
|
||||
Symbols: manifest.Symbols.Select(s => new SymbolEntryDto(
|
||||
s.Address, s.Size, s.MangledName, s.DemangledName,
|
||||
s.Type, s.Binding, s.SourceFile, s.SourceLine, s.ContentHash)).ToList(),
|
||||
SourceMappings: manifest.SourceMappings?.Select(m => new SourceMappingDto(
|
||||
m.CompiledPath, m.SourcePath, m.ContentHash)).ToList(),
|
||||
CreatedAt: manifest.CreatedAt);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"profiles": {
|
||||
"StellaOps.Symbols.Server": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"STELLAOPS_WEBSERVICES_CORS": "true",
|
||||
"STELLAOPS_WEBSERVICES_CORS_ORIGIN": "https://stella-ops.local,https://stella-ops.local:10000,https://localhost:10000"
|
||||
},
|
||||
"applicationUrl": "https://localhost:10380;http://localhost:10381"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) StellaOps. Licensed under the BUSL-1.1.
|
||||
|
||||
namespace StellaOps.Symbols.Server.Security;
|
||||
|
||||
/// <summary>
|
||||
/// Named authorization policy constants for the Symbols service.
|
||||
/// Policies are registered via AddStellaOpsScopePolicy in Program.cs.
|
||||
/// </summary>
|
||||
internal static class SymbolsPolicies
|
||||
{
|
||||
/// <summary>Policy for querying symbol manifests. Requires symbols:read scope.</summary>
|
||||
public const string Read = "Symbols.Read";
|
||||
|
||||
/// <summary>Policy for uploading symbol manifests and resolving symbols. Requires symbols:write scope.</summary>
|
||||
public const string Write = "Symbols.Write";
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\__Libraries\StellaOps.Symbols.Core\StellaOps.Symbols.Core.csproj" />
|
||||
<ProjectReference Include="..\__Libraries\StellaOps.Symbols.Infrastructure\StellaOps.Symbols.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\__Libraries\StellaOps.Symbols.Marketplace\StellaOps.Symbols.Marketplace.csproj" />
|
||||
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.Abstractions\StellaOps.Auth.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\Authority\StellaOps.Authority\StellaOps.Auth.ServerIntegration\StellaOps.Auth.ServerIntegration.csproj" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="StellaOpsReleaseVersion">
|
||||
<Version>1.0.0-alpha1</Version>
|
||||
<InformationalVersion>1.0.0-alpha1</InformationalVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
8
src/BinaryIndex/StellaOps.Symbols.Server/TASKS.md
Normal file
8
src/BinaryIndex/StellaOps.Symbols.Server/TASKS.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# StellaOps.Symbols.Server Task Board
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Symbols/StellaOps.Symbols.Server/StellaOps.Symbols.Server.md. |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
@@ -16,7 +16,7 @@
|
||||
<ProjectReference Include="..\StellaOps.BinaryIndex.GroundTruth.Abstractions\StellaOps.BinaryIndex.GroundTruth.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.BinaryIndex.Normalization\StellaOps.BinaryIndex.Normalization.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.BinaryIndex.Semantic\StellaOps.BinaryIndex.Semantic.csproj" />
|
||||
<ProjectReference Include="..\..\..\Symbols\StellaOps.Symbols.Core\StellaOps.Symbols.Core.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Symbols.Core\StellaOps.Symbols.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../StellaOps.BinaryIndex.Core/StellaOps.BinaryIndex.Core.csproj" />
|
||||
<ProjectReference Include="../StellaOps.BinaryIndex.FixIndex/StellaOps.BinaryIndex.FixIndex.csproj" />
|
||||
<ProjectReference Include="../../../Excititor/__Libraries/StellaOps.Excititor.Core/StellaOps.Excititor.Core.csproj" />
|
||||
<ProjectReference Include="../../../Concelier/__Libraries/StellaOps.Excititor.Core/StellaOps.Excititor.Core.csproj" />
|
||||
<ProjectReference Include="../../../Attestor/StellaOps.Attestor.Envelope/StellaOps.Attestor.Envelope.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
using StellaOps.Symbols.Bundle.Models;
|
||||
|
||||
namespace StellaOps.Symbols.Bundle.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// SYMS-BUNDLE-401-014: Builds deterministic symbol bundles for air-gapped installations.
|
||||
/// </summary>
|
||||
public interface IBundleBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a symbol bundle from the specified options.
|
||||
/// </summary>
|
||||
Task<BundleBuildResult> BuildAsync(
|
||||
BundleBuildOptions options,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a bundle's integrity and signatures.
|
||||
/// </summary>
|
||||
Task<BundleVerifyResult> VerifyAsync(
|
||||
string bundlePath,
|
||||
BundleVerifyOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts a bundle to target directory.
|
||||
/// </summary>
|
||||
Task<BundleExtractResult> ExtractAsync(
|
||||
string bundlePath,
|
||||
string outputDir,
|
||||
BundleExtractOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Lists contents of a bundle without extracting.
|
||||
/// </summary>
|
||||
Task<BundleManifest?> InspectAsync(
|
||||
string bundlePath,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for building a symbol bundle.
|
||||
/// </summary>
|
||||
public sealed record BundleBuildOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Bundle name.
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Bundle version (SemVer).
|
||||
/// </summary>
|
||||
public required string Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Source directory containing symbol manifests and blobs.
|
||||
/// </summary>
|
||||
public required string SourceDir { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Output directory for the bundle archive.
|
||||
/// </summary>
|
||||
public required string OutputDir { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Platform filter (e.g., "linux-x64"). Null means all platforms.
|
||||
/// </summary>
|
||||
public string? Platform { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tenant ID filter. Null means all tenants.
|
||||
/// </summary>
|
||||
public string? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Sign the bundle with DSSE.
|
||||
/// </summary>
|
||||
public bool Sign { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to signing key (PEM-encoded private key).
|
||||
/// </summary>
|
||||
public string? SigningKeyPath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Key ID for DSSE signature.
|
||||
/// </summary>
|
||||
public string? KeyId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Signing algorithm to use.
|
||||
/// </summary>
|
||||
public string SigningAlgorithm { get; init; } = "ecdsa-p256";
|
||||
|
||||
/// <summary>
|
||||
/// Submit to Rekor transparency log.
|
||||
/// </summary>
|
||||
public bool SubmitRekor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rekor server URL.
|
||||
/// </summary>
|
||||
public string RekorUrl { get; init; } = "https://rekor.sigstore.dev";
|
||||
|
||||
/// <summary>
|
||||
/// Include Rekor log public key for offline verification.
|
||||
/// </summary>
|
||||
public bool IncludeRekorPublicKey { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Include public key in manifest for offline verification.
|
||||
/// </summary>
|
||||
public bool IncludePublicKey { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Bundle format (zip or tar.gz).
|
||||
/// </summary>
|
||||
public BundleFormat Format { get; init; } = BundleFormat.Zip;
|
||||
|
||||
/// <summary>
|
||||
/// Compression level (0-9).
|
||||
/// </summary>
|
||||
public int CompressionLevel { get; init; } = 6;
|
||||
|
||||
/// <summary>
|
||||
/// Additional metadata to include in manifest.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum bundle size in bytes (0 = unlimited).
|
||||
/// </summary>
|
||||
public long MaxSizeBytes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, create multiple bundles if size limit exceeded.
|
||||
/// </summary>
|
||||
public bool AllowSplit { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bundle archive format.
|
||||
/// </summary>
|
||||
public enum BundleFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// ZIP archive format.
|
||||
/// </summary>
|
||||
Zip = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Gzipped TAR archive format.
|
||||
/// </summary>
|
||||
TarGz = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of bundle build operation.
|
||||
/// </summary>
|
||||
public sealed record BundleBuildResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the build succeeded.
|
||||
/// </summary>
|
||||
public required bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to the created bundle archive.
|
||||
/// </summary>
|
||||
public string? BundlePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Path to the manifest JSON file.
|
||||
/// </summary>
|
||||
public string? ManifestPath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The bundle manifest.
|
||||
/// </summary>
|
||||
public BundleManifest? Manifest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message if build failed.
|
||||
/// </summary>
|
||||
public string? Error { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Warnings during build.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Warnings { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Build duration.
|
||||
/// </summary>
|
||||
public TimeSpan Duration { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for verifying a bundle.
|
||||
/// </summary>
|
||||
public sealed record BundleVerifyOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Path to public key for signature verification.
|
||||
/// If null, uses embedded public key.
|
||||
/// </summary>
|
||||
public string? PublicKeyPath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Verify Rekor inclusion proof offline.
|
||||
/// </summary>
|
||||
public bool VerifyRekorOffline { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Path to Rekor public key for offline verification.
|
||||
/// If null, uses embedded key.
|
||||
/// </summary>
|
||||
public string? RekorPublicKeyPath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Verify all blob hashes.
|
||||
/// </summary>
|
||||
public bool VerifyBlobHashes { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Verify manifest hashes.
|
||||
/// </summary>
|
||||
public bool VerifyManifestHashes { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Require a valid DSSE signature for verification to pass.
|
||||
/// </summary>
|
||||
public bool RequireSignature { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Require a Rekor checkpoint and valid inclusion proof.
|
||||
/// </summary>
|
||||
public bool RequireRekorProof { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of bundle verification.
|
||||
/// </summary>
|
||||
public sealed record BundleVerifyResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Overall verification status.
|
||||
/// </summary>
|
||||
public required bool Valid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Signature verification status.
|
||||
/// </summary>
|
||||
public required SignatureStatus SignatureStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rekor verification status.
|
||||
/// </summary>
|
||||
public RekorVerifyStatus? RekorStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash verification status.
|
||||
/// </summary>
|
||||
public required HashVerifyStatus HashStatus { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Verification errors.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Errors { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Verification warnings.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Warnings { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Verified manifest (if valid).
|
||||
/// </summary>
|
||||
public BundleManifest? Manifest { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signature verification status.
|
||||
/// </summary>
|
||||
public enum SignatureStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Bundle is not signed.
|
||||
/// </summary>
|
||||
Unsigned = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Signature is valid.
|
||||
/// </summary>
|
||||
Valid = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Signature verification failed.
|
||||
/// </summary>
|
||||
Invalid = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Could not verify (missing key, etc.).
|
||||
/// </summary>
|
||||
Unknown = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rekor verification status.
|
||||
/// </summary>
|
||||
public enum RekorVerifyStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// No Rekor checkpoint present.
|
||||
/// </summary>
|
||||
NotPresent = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Inclusion proof verified offline.
|
||||
/// </summary>
|
||||
VerifiedOffline = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Verified against live Rekor.
|
||||
/// </summary>
|
||||
VerifiedOnline = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Verification failed.
|
||||
/// </summary>
|
||||
Invalid = 3
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hash verification status.
|
||||
/// </summary>
|
||||
public sealed record HashVerifyStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Bundle hash valid.
|
||||
/// </summary>
|
||||
public required bool BundleHashValid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of entries with valid hashes.
|
||||
/// </summary>
|
||||
public required int ValidEntries { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of entries with invalid hashes.
|
||||
/// </summary>
|
||||
public required int InvalidEntries { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total entries checked.
|
||||
/// </summary>
|
||||
public required int TotalEntries { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Entries with hash mismatches.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> InvalidEntryIds { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for extracting a bundle.
|
||||
/// </summary>
|
||||
public sealed record BundleExtractOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Verify bundle before extracting.
|
||||
/// </summary>
|
||||
public bool VerifyFirst { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Verification options if VerifyFirst is true.
|
||||
/// </summary>
|
||||
public BundleVerifyOptions? VerifyOptions { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Platform filter for extraction.
|
||||
/// </summary>
|
||||
public string? Platform { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Overwrite existing files.
|
||||
/// </summary>
|
||||
public bool Overwrite { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Only extract manifest files (not blobs).
|
||||
/// </summary>
|
||||
public bool ManifestsOnly { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of bundle extraction.
|
||||
/// </summary>
|
||||
public sealed record BundleExtractResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether extraction succeeded.
|
||||
/// </summary>
|
||||
public required bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Verification result (if verification was performed).
|
||||
/// </summary>
|
||||
public BundleVerifyResult? VerifyResult { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of entries extracted.
|
||||
/// </summary>
|
||||
public int ExtractedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of entries skipped.
|
||||
/// </summary>
|
||||
public int SkippedCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total bytes extracted.
|
||||
/// </summary>
|
||||
public long TotalBytesExtracted { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message if failed.
|
||||
/// </summary>
|
||||
public string? Error { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Extraction duration.
|
||||
/// </summary>
|
||||
public TimeSpan Duration { get; init; }
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,331 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Symbols.Bundle.Models;
|
||||
|
||||
/// <summary>
|
||||
/// SYMS-BUNDLE-401-014: Symbol bundle manifest for air-gapped installations.
|
||||
/// Contains deterministic ordering of symbol entries with DSSE signatures
|
||||
/// and Rekor checkpoint references.
|
||||
/// </summary>
|
||||
public sealed record BundleManifest
|
||||
{
|
||||
/// <summary>
|
||||
/// Schema version for bundle manifest format.
|
||||
/// </summary>
|
||||
[JsonPropertyName("schemaVersion")]
|
||||
public string SchemaVersion { get; init; } = "stellaops.symbols.bundle/v1";
|
||||
|
||||
/// <summary>
|
||||
/// Unique bundle identifier (BLAKE3 hash of canonical manifest content).
|
||||
/// </summary>
|
||||
[JsonPropertyName("bundleId")]
|
||||
public required string BundleId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable bundle name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Bundle version (SemVer).
|
||||
/// </summary>
|
||||
[JsonPropertyName("version")]
|
||||
public required string Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Bundle creation timestamp (UTC ISO-8601).
|
||||
/// </summary>
|
||||
[JsonPropertyName("createdAt")]
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Platform/architecture filter for included symbols (e.g., "linux-x64").
|
||||
/// Null means all platforms.
|
||||
/// </summary>
|
||||
[JsonPropertyName("platform")]
|
||||
public string? Platform { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tenant ID for multi-tenant isolation. Null means system-wide bundle.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tenantId")]
|
||||
public string? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol entries included in this bundle (deterministically sorted).
|
||||
/// </summary>
|
||||
[JsonPropertyName("entries")]
|
||||
public required IReadOnlyList<BundleEntry> Entries { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total size of all blob data in bytes.
|
||||
/// </summary>
|
||||
[JsonPropertyName("totalSizeBytes")]
|
||||
public long TotalSizeBytes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// DSSE signature information.
|
||||
/// </summary>
|
||||
[JsonPropertyName("signature")]
|
||||
public BundleSignature? Signature { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rekor transparency log checkpoint.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rekorCheckpoint")]
|
||||
public RekorCheckpoint? RekorCheckpoint { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash algorithm used for all hashes in this manifest.
|
||||
/// </summary>
|
||||
[JsonPropertyName("hashAlgorithm")]
|
||||
public string HashAlgorithm { get; init; } = "blake3";
|
||||
|
||||
/// <summary>
|
||||
/// Additional metadata for offline verification.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public IReadOnlyDictionary<string, string>? Metadata { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Individual entry in a symbol bundle.
|
||||
/// </summary>
|
||||
public sealed record BundleEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Debug ID for symbol lookup.
|
||||
/// </summary>
|
||||
[JsonPropertyName("debugId")]
|
||||
public required string DebugId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Code ID (GNU build-id, PE checksum) if available.
|
||||
/// </summary>
|
||||
[JsonPropertyName("codeId")]
|
||||
public string? CodeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Original binary name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("binaryName")]
|
||||
public required string BinaryName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Platform/architecture (e.g., linux-x64, win-x64).
|
||||
/// </summary>
|
||||
[JsonPropertyName("platform")]
|
||||
public string? Platform { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Binary format (ELF, PE, Mach-O, WASM).
|
||||
/// </summary>
|
||||
[JsonPropertyName("format")]
|
||||
public string Format { get; init; } = "unknown";
|
||||
|
||||
/// <summary>
|
||||
/// BLAKE3 hash of the manifest content.
|
||||
/// </summary>
|
||||
[JsonPropertyName("manifestHash")]
|
||||
public required string ManifestHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// BLAKE3 hash of the symbol blob content.
|
||||
/// </summary>
|
||||
[JsonPropertyName("blobHash")]
|
||||
public required string BlobHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Size of the blob in bytes.
|
||||
/// </summary>
|
||||
[JsonPropertyName("blobSizeBytes")]
|
||||
public long BlobSizeBytes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Relative path within the bundle archive.
|
||||
/// Format: "symbols/{debugId}/{binaryName}.symbols"
|
||||
/// </summary>
|
||||
[JsonPropertyName("archivePath")]
|
||||
public required string ArchivePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of symbols in the manifest.
|
||||
/// </summary>
|
||||
[JsonPropertyName("symbolCount")]
|
||||
public int SymbolCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// DSSE envelope digest for individual manifest signing.
|
||||
/// </summary>
|
||||
[JsonPropertyName("dsseDigest")]
|
||||
public string? DsseDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rekor log index if individually published.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rekorLogIndex")]
|
||||
public long? RekorLogIndex { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DSSE signature information for the bundle.
|
||||
/// </summary>
|
||||
public sealed record BundleSignature
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the bundle is signed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("signed")]
|
||||
public bool Signed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Signing algorithm (e.g., "ecdsa-p256", "ed25519", "rsa-pss-sha256").
|
||||
/// </summary>
|
||||
[JsonPropertyName("algorithm")]
|
||||
public string? Algorithm { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Key ID used for signing.
|
||||
/// </summary>
|
||||
[JsonPropertyName("keyId")]
|
||||
public string? KeyId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// DSSE envelope digest.
|
||||
/// </summary>
|
||||
[JsonPropertyName("dsseDigest")]
|
||||
public string? DsseDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Signing timestamp (UTC ISO-8601).
|
||||
/// </summary>
|
||||
[JsonPropertyName("signedAt")]
|
||||
public DateTimeOffset? SignedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Certificate chain for verification (PEM-encoded).
|
||||
/// </summary>
|
||||
[JsonPropertyName("certificateChain")]
|
||||
public IReadOnlyList<string>? CertificateChain { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Public key for offline verification (PEM-encoded).
|
||||
/// </summary>
|
||||
[JsonPropertyName("publicKey")]
|
||||
public string? PublicKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// DSSE payload type used during signing.
|
||||
/// </summary>
|
||||
[JsonPropertyName("payloadType")]
|
||||
public string? PayloadType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Base64-encoded canonical payload bytes that were signed.
|
||||
/// </summary>
|
||||
[JsonPropertyName("payload")]
|
||||
public string? Payload { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Base64-encoded signature over DSSE pre-authenticated encoding.
|
||||
/// </summary>
|
||||
[JsonPropertyName("signature")]
|
||||
public string? Signature { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rekor transparency log checkpoint for offline verification.
|
||||
/// </summary>
|
||||
public sealed record RekorCheckpoint
|
||||
{
|
||||
/// <summary>
|
||||
/// Rekor server URL where this checkpoint was created.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rekorUrl")]
|
||||
public required string RekorUrl { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Log entry ID (UUID or log index).
|
||||
/// </summary>
|
||||
[JsonPropertyName("logEntryId")]
|
||||
public required string LogEntryId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Log index (monotonic sequence number).
|
||||
/// </summary>
|
||||
[JsonPropertyName("logIndex")]
|
||||
public required long LogIndex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Signed entry timestamp from Rekor.
|
||||
/// </summary>
|
||||
[JsonPropertyName("integratedTime")]
|
||||
public required DateTimeOffset IntegratedTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Root hash of the Merkle tree at time of inclusion.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rootHash")]
|
||||
public required string RootHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tree size at time of inclusion.
|
||||
/// </summary>
|
||||
[JsonPropertyName("treeSize")]
|
||||
public required long TreeSize { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Inclusion proof for offline verification.
|
||||
/// </summary>
|
||||
[JsonPropertyName("inclusionProof")]
|
||||
public InclusionProof? InclusionProof { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Signed checkpoint from the log.
|
||||
/// </summary>
|
||||
[JsonPropertyName("signedCheckpoint")]
|
||||
public string? SignedCheckpoint { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Public key of the Rekor log for verification.
|
||||
/// </summary>
|
||||
[JsonPropertyName("logPublicKey")]
|
||||
public string? LogPublicKey { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merkle tree inclusion proof for offline verification.
|
||||
/// </summary>
|
||||
public sealed record InclusionProof
|
||||
{
|
||||
/// <summary>
|
||||
/// Log index of the entry.
|
||||
/// </summary>
|
||||
[JsonPropertyName("logIndex")]
|
||||
public required long LogIndex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Root hash of the Merkle tree.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rootHash")]
|
||||
public required string RootHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tree size at time of proof.
|
||||
/// </summary>
|
||||
[JsonPropertyName("treeSize")]
|
||||
public required long TreeSize { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Hashes forming the Merkle proof path.
|
||||
/// </summary>
|
||||
[JsonPropertyName("hashes")]
|
||||
public required IReadOnlyList<string> Hashes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Checkpoint signature.
|
||||
/// </summary>
|
||||
[JsonPropertyName("checkpoint")]
|
||||
public string? Checkpoint { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Symbols.Bundle.Abstractions;
|
||||
|
||||
namespace StellaOps.Symbols.Bundle;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for registering Symbol Bundle services.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds symbol bundle services to the service collection.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSymbolBundle(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IBundleBuilder, BundleBuilder>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Description>StellaOps Symbol Bundle - Deterministic symbol bundles for air-gapped installs with DSSE manifests and Rekor checkpoints</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blake3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Symbols.Core\StellaOps.Symbols.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,8 @@
|
||||
# StellaOps.Symbols.Bundle Task Board
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Symbols/StellaOps.Symbols.Bundle/StellaOps.Symbols.Bundle.md. |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
@@ -0,0 +1,322 @@
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Symbols.Client;
|
||||
|
||||
/// <summary>
|
||||
/// LRU disk cache for symbol data with size-based eviction.
|
||||
/// </summary>
|
||||
public sealed class DiskLruCache : IDisposable
|
||||
{
|
||||
private readonly string _cachePath;
|
||||
private readonly long _maxSizeBytes;
|
||||
private readonly ILogger<DiskLruCache>? _logger;
|
||||
private readonly ConcurrentDictionary<string, CacheEntry> _index = new();
|
||||
private readonly SemaphoreSlim _evictionLock = new(1, 1);
|
||||
private long _currentSizeBytes;
|
||||
private bool _disposed;
|
||||
|
||||
private const string IndexFileName = ".cache-index.json";
|
||||
|
||||
public DiskLruCache(string cachePath, long maxSizeBytes, ILogger<DiskLruCache>? logger = null)
|
||||
{
|
||||
_cachePath = cachePath ?? throw new ArgumentNullException(nameof(cachePath));
|
||||
_maxSizeBytes = maxSizeBytes > 0 ? maxSizeBytes : throw new ArgumentOutOfRangeException(nameof(maxSizeBytes));
|
||||
_logger = logger;
|
||||
|
||||
Directory.CreateDirectory(_cachePath);
|
||||
LoadIndex();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a cached item by key.
|
||||
/// </summary>
|
||||
public async Task<byte[]?> GetAsync(string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
var hash = ComputeKeyHash(key);
|
||||
if (!_index.TryGetValue(hash, out var entry))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var filePath = GetFilePath(hash);
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
_index.TryRemove(hash, out _);
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var data = await File.ReadAllBytesAsync(filePath, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Update access time (LRU tracking)
|
||||
entry.LastAccess = DateTimeOffset.UtcNow;
|
||||
_index[hash] = entry;
|
||||
|
||||
_logger?.LogDebug("Cache hit for key {Key}", key);
|
||||
return data;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to read cached file for key {Key}", key);
|
||||
_index.TryRemove(hash, out _);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores an item in the cache.
|
||||
/// </summary>
|
||||
public async Task SetAsync(string key, byte[] data, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
if (data.Length > _maxSizeBytes)
|
||||
{
|
||||
_logger?.LogWarning("Data size {Size} exceeds max cache size {MaxSize}, skipping cache", data.Length, _maxSizeBytes);
|
||||
return;
|
||||
}
|
||||
|
||||
var hash = ComputeKeyHash(key);
|
||||
var filePath = GetFilePath(hash);
|
||||
|
||||
// Ensure enough space
|
||||
await EnsureSpaceAsync(data.Length, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await File.WriteAllBytesAsync(filePath, data, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var entry = new CacheEntry
|
||||
{
|
||||
Key = key,
|
||||
Hash = hash,
|
||||
Size = data.Length,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
LastAccess = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
if (_index.TryGetValue(hash, out var existing))
|
||||
{
|
||||
Interlocked.Add(ref _currentSizeBytes, -existing.Size);
|
||||
}
|
||||
|
||||
_index[hash] = entry;
|
||||
Interlocked.Add(ref _currentSizeBytes, data.Length);
|
||||
|
||||
_logger?.LogDebug("Cached {Size} bytes for key {Key}", data.Length, key);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to cache data for key {Key}", key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes an item from the cache.
|
||||
/// </summary>
|
||||
public Task<bool> RemoveAsync(string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
var hash = ComputeKeyHash(key);
|
||||
if (!_index.TryRemove(hash, out var entry))
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
var filePath = GetFilePath(hash);
|
||||
try
|
||||
{
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
Interlocked.Add(ref _currentSizeBytes, -entry.Size);
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to remove cached file for key {Key}", key);
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all cached items.
|
||||
/// </summary>
|
||||
public Task ClearAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
foreach (var entry in _index.Values)
|
||||
{
|
||||
var filePath = GetFilePath(entry.Hash);
|
||||
try
|
||||
{
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
_index.Clear();
|
||||
Interlocked.Exchange(ref _currentSizeBytes, 0);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets current cache statistics.
|
||||
/// </summary>
|
||||
public CacheStats GetStats()
|
||||
{
|
||||
return new CacheStats
|
||||
{
|
||||
ItemCount = _index.Count,
|
||||
CurrentSizeBytes = Interlocked.Read(ref _currentSizeBytes),
|
||||
MaxSizeBytes = _maxSizeBytes
|
||||
};
|
||||
}
|
||||
|
||||
private async Task EnsureSpaceAsync(long requiredBytes, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Interlocked.Read(ref _currentSizeBytes) + requiredBytes <= _maxSizeBytes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _evictionLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// Evict LRU entries until we have enough space
|
||||
var targetSize = _maxSizeBytes - requiredBytes;
|
||||
var entries = _index.Values
|
||||
.OrderBy(e => e.LastAccess)
|
||||
.ToList();
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (Interlocked.Read(ref _currentSizeBytes) <= targetSize)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var filePath = GetFilePath(entry.Hash);
|
||||
try
|
||||
{
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
_index.TryRemove(entry.Hash, out _);
|
||||
Interlocked.Add(ref _currentSizeBytes, -entry.Size);
|
||||
_logger?.LogDebug("Evicted cache entry {Key} ({Size} bytes)", entry.Key, entry.Size);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to evict cache entry {Key}", entry.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_evictionLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadIndex()
|
||||
{
|
||||
var indexPath = Path.Combine(_cachePath, IndexFileName);
|
||||
if (!File.Exists(indexPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(indexPath);
|
||||
var entries = JsonSerializer.Deserialize<List<CacheEntry>>(json);
|
||||
if (entries is not null)
|
||||
{
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
var filePath = GetFilePath(entry.Hash);
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
_index[entry.Hash] = entry;
|
||||
Interlocked.Add(ref _currentSizeBytes, entry.Size);
|
||||
}
|
||||
}
|
||||
}
|
||||
_logger?.LogDebug("Loaded {Count} cache entries from index", _index.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to load cache index, starting fresh");
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveIndex()
|
||||
{
|
||||
var indexPath = Path.Combine(_cachePath, IndexFileName);
|
||||
try
|
||||
{
|
||||
var entries = _index.Values.ToList();
|
||||
var json = JsonSerializer.Serialize(entries, new JsonSerializerOptions { WriteIndented = false });
|
||||
File.WriteAllText(indexPath, json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Failed to save cache index");
|
||||
}
|
||||
}
|
||||
|
||||
private string GetFilePath(string hash) => Path.Combine(_cachePath, hash);
|
||||
|
||||
private static string ComputeKeyHash(string key)
|
||||
{
|
||||
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(key));
|
||||
return Convert.ToHexString(bytes).ToLowerInvariant();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
SaveIndex();
|
||||
_evictionLock.Dispose();
|
||||
}
|
||||
|
||||
private sealed class CacheEntry
|
||||
{
|
||||
public string Key { get; set; } = string.Empty;
|
||||
public string Hash { get; set; } = string.Empty;
|
||||
public long Size { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
public DateTimeOffset LastAccess { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cache statistics.
|
||||
/// </summary>
|
||||
public sealed record CacheStats
|
||||
{
|
||||
public int ItemCount { get; init; }
|
||||
public long CurrentSizeBytes { get; init; }
|
||||
public long MaxSizeBytes { get; init; }
|
||||
public double UsagePercent => MaxSizeBytes > 0 ? (double)CurrentSizeBytes / MaxSizeBytes * 100 : 0;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
using StellaOps.Symbols.Core.Models;
|
||||
|
||||
namespace StellaOps.Symbols.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Client interface for the Symbols service.
|
||||
/// </summary>
|
||||
public interface ISymbolsClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Uploads a symbol manifest to the server.
|
||||
/// </summary>
|
||||
Task<SymbolManifestUploadResult> UploadManifestAsync(
|
||||
SymbolManifest manifest,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a manifest by ID.
|
||||
/// </summary>
|
||||
Task<SymbolManifest?> GetManifestAsync(
|
||||
string manifestId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets manifests by debug ID.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<SymbolManifest>> GetManifestsByDebugIdAsync(
|
||||
string debugId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves addresses to symbols.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<SymbolResolutionResult>> ResolveAsync(
|
||||
string debugId,
|
||||
IEnumerable<ulong> addresses,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a single address to a symbol.
|
||||
/// </summary>
|
||||
Task<SymbolResolutionResult?> ResolveAddressAsync(
|
||||
string debugId,
|
||||
ulong address,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Queries manifests with filters.
|
||||
/// </summary>
|
||||
Task<SymbolManifestQueryResult> QueryManifestsAsync(
|
||||
SymbolManifestQuery query,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets service health status.
|
||||
/// </summary>
|
||||
Task<SymbolsHealthStatus> GetHealthAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of manifest upload.
|
||||
/// </summary>
|
||||
public sealed record SymbolManifestUploadResult
|
||||
{
|
||||
public required string ManifestId { get; init; }
|
||||
public required string DebugId { get; init; }
|
||||
public required string BinaryName { get; init; }
|
||||
public string? BlobUri { get; init; }
|
||||
public required int SymbolCount { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of symbol resolution.
|
||||
/// </summary>
|
||||
public sealed record SymbolResolutionResult
|
||||
{
|
||||
public required ulong Address { get; init; }
|
||||
public required bool Found { get; init; }
|
||||
public string? MangledName { get; init; }
|
||||
public string? DemangledName { get; init; }
|
||||
public ulong Offset { get; init; }
|
||||
public string? SourceFile { get; init; }
|
||||
public int? SourceLine { get; init; }
|
||||
public double Confidence { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Query parameters for manifest search.
|
||||
/// </summary>
|
||||
public sealed record SymbolManifestQuery
|
||||
{
|
||||
public string? DebugId { get; init; }
|
||||
public string? CodeId { get; init; }
|
||||
public string? BinaryName { get; init; }
|
||||
public string? Platform { get; init; }
|
||||
public BinaryFormat? Format { get; init; }
|
||||
public DateTimeOffset? CreatedAfter { get; init; }
|
||||
public DateTimeOffset? CreatedBefore { get; init; }
|
||||
public bool? HasDsse { get; init; }
|
||||
public int Offset { get; init; }
|
||||
public int Limit { get; init; } = 50;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of manifest query.
|
||||
/// </summary>
|
||||
public sealed record SymbolManifestQueryResult
|
||||
{
|
||||
public required IReadOnlyList<SymbolManifestSummary> Manifests { get; init; }
|
||||
public required int TotalCount { get; init; }
|
||||
public required int Offset { get; init; }
|
||||
public required int Limit { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Summary of a symbol manifest.
|
||||
/// </summary>
|
||||
public sealed record SymbolManifestSummary
|
||||
{
|
||||
public required string ManifestId { get; init; }
|
||||
public required string DebugId { get; init; }
|
||||
public string? CodeId { get; init; }
|
||||
public required string BinaryName { get; init; }
|
||||
public string? Platform { get; init; }
|
||||
public required BinaryFormat Format { get; init; }
|
||||
public required int SymbolCount { get; init; }
|
||||
public required bool HasDsse { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Symbols service health status.
|
||||
/// </summary>
|
||||
public sealed record SymbolsHealthStatus
|
||||
{
|
||||
public required string Status { get; init; }
|
||||
public required string Version { get; init; }
|
||||
public required DateTimeOffset Timestamp { get; init; }
|
||||
public long? TotalManifests { get; init; }
|
||||
public long? TotalSymbols { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Symbols.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Service collection extensions for Symbols client.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the Symbols client with default configuration.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSymbolsClient(this IServiceCollection services)
|
||||
{
|
||||
return services.AddSymbolsClient(_ => { });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the Symbols client with configuration.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSymbolsClient(
|
||||
this IServiceCollection services,
|
||||
Action<SymbolsClientOptions> configure)
|
||||
{
|
||||
services.Configure(configure);
|
||||
|
||||
services.AddHttpClient<ISymbolsClient, SymbolsClient>((sp, client) =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<SymbolsClientOptions>>().Value;
|
||||
client.BaseAddress = new Uri(options.BaseUrl);
|
||||
client.Timeout = TimeSpan.FromSeconds(options.TimeoutSeconds);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the Symbols client with a named HTTP client.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSymbolsClient(
|
||||
this IServiceCollection services,
|
||||
string httpClientName,
|
||||
Action<SymbolsClientOptions> configure)
|
||||
{
|
||||
services.Configure(configure);
|
||||
|
||||
services.AddHttpClient<ISymbolsClient, SymbolsClient>(httpClientName, (sp, client) =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<SymbolsClientOptions>>().Value;
|
||||
client.BaseAddress = new Uri(options.BaseUrl);
|
||||
client.Timeout = TimeSpan.FromSeconds(options.TimeoutSeconds);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Symbols.Core\StellaOps.Symbols.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,435 @@
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Symbols.Core.Models;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace StellaOps.Symbols.Client;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client for the Symbols service.
|
||||
/// </summary>
|
||||
public sealed class SymbolsClient : ISymbolsClient, IDisposable
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly SymbolsClientOptions _options;
|
||||
private readonly DiskLruCache? _cache;
|
||||
private readonly ILogger<SymbolsClient>? _logger;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private bool _disposed;
|
||||
|
||||
private const string TenantHeader = "X-Tenant-Id";
|
||||
|
||||
public SymbolsClient(
|
||||
HttpClient httpClient,
|
||||
IOptions<SymbolsClientOptions> options,
|
||||
ILogger<SymbolsClient>? logger = null,
|
||||
ILoggerFactory? loggerFactory = null)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger;
|
||||
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
if (_options.EnableDiskCache)
|
||||
{
|
||||
var cacheLogger = loggerFactory?.CreateLogger<DiskLruCache>();
|
||||
_cache = new DiskLruCache(_options.CachePath, _options.MaxCacheSizeBytes, cacheLogger);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<SymbolManifestUploadResult> UploadManifestAsync(
|
||||
SymbolManifest manifest,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
var request = new UploadManifestRequest(
|
||||
DebugId: manifest.DebugId,
|
||||
BinaryName: manifest.BinaryName,
|
||||
CodeId: manifest.CodeId,
|
||||
Platform: manifest.Platform,
|
||||
Format: manifest.Format,
|
||||
Symbols: manifest.Symbols.Select(s => new SymbolEntryRequest(
|
||||
Address: s.Address,
|
||||
Size: s.Size,
|
||||
MangledName: s.MangledName,
|
||||
DemangledName: s.DemangledName,
|
||||
Type: s.Type,
|
||||
Binding: s.Binding,
|
||||
SourceFile: s.SourceFile,
|
||||
SourceLine: s.SourceLine,
|
||||
ContentHash: s.ContentHash)).ToList(),
|
||||
SourceMappings: manifest.SourceMappings?.Select(m => new SourceMappingRequest(
|
||||
CompiledPath: m.CompiledPath,
|
||||
SourcePath: m.SourcePath,
|
||||
ContentHash: m.ContentHash)).ToList());
|
||||
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/v1/symbols/manifests");
|
||||
AddTenantHeader(httpRequest);
|
||||
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
|
||||
|
||||
using var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<UploadManifestResponse>(_jsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return new SymbolManifestUploadResult
|
||||
{
|
||||
ManifestId = result!.ManifestId,
|
||||
DebugId = result.DebugId,
|
||||
BinaryName = result.BinaryName,
|
||||
BlobUri = result.BlobUri,
|
||||
SymbolCount = result.SymbolCount,
|
||||
CreatedAt = result.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<SymbolManifest?> GetManifestAsync(
|
||||
string manifestId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"/v1/symbols/manifests/{Uri.EscapeDataString(manifestId)}");
|
||||
AddTenantHeader(request);
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var detail = await response.Content.ReadFromJsonAsync<ManifestDetailResponse>(_jsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return MapToManifest(detail!);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IReadOnlyList<SymbolManifest>> GetManifestsByDebugIdAsync(
|
||||
string debugId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"/v1/symbols/by-debug-id/{Uri.EscapeDataString(debugId)}");
|
||||
AddTenantHeader(request);
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var summaries = await response.Content.ReadFromJsonAsync<List<ManifestSummaryResponse>>(_jsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// Note: This returns summaries, not full manifests. For full manifests, call GetManifestAsync for each.
|
||||
return summaries!.Select(s => new SymbolManifest
|
||||
{
|
||||
ManifestId = s.ManifestId,
|
||||
DebugId = s.DebugId,
|
||||
CodeId = s.CodeId,
|
||||
BinaryName = s.BinaryName,
|
||||
Platform = s.Platform,
|
||||
Format = s.Format,
|
||||
TenantId = _options.TenantId ?? string.Empty,
|
||||
Symbols = [],
|
||||
CreatedAt = s.CreatedAt
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IReadOnlyList<SymbolResolutionResult>> ResolveAsync(
|
||||
string debugId,
|
||||
IEnumerable<ulong> addresses,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
var addressList = addresses.ToList();
|
||||
|
||||
// Check cache first
|
||||
if (_cache is not null)
|
||||
{
|
||||
var cacheKey = $"resolve:{debugId}:{string.Join(",", addressList)}";
|
||||
var cached = await _cache.GetAsync(cacheKey, cancellationToken).ConfigureAwait(false);
|
||||
if (cached is not null)
|
||||
{
|
||||
_logger?.LogDebug("Cache hit for resolution batch");
|
||||
return JsonSerializer.Deserialize<List<SymbolResolutionResult>>(cached, _jsonOptions)!;
|
||||
}
|
||||
}
|
||||
|
||||
var requestBody = new ResolveRequest(debugId, addressList);
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, "/v1/symbols/resolve");
|
||||
AddTenantHeader(request);
|
||||
request.Content = JsonContent.Create(requestBody, options: _jsonOptions);
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var resolveResponse = await response.Content.ReadFromJsonAsync<ResolveResponse>(_jsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var results = resolveResponse!.Resolutions.Select(r => new SymbolResolutionResult
|
||||
{
|
||||
Address = r.Address,
|
||||
Found = r.Found,
|
||||
MangledName = r.MangledName,
|
||||
DemangledName = r.DemangledName,
|
||||
Offset = r.Offset,
|
||||
SourceFile = r.SourceFile,
|
||||
SourceLine = r.SourceLine,
|
||||
Confidence = r.Confidence
|
||||
}).ToList();
|
||||
|
||||
// Cache result
|
||||
if (_cache is not null)
|
||||
{
|
||||
var cacheKey = $"resolve:{debugId}:{string.Join(",", addressList)}";
|
||||
var data = JsonSerializer.SerializeToUtf8Bytes(results, _jsonOptions);
|
||||
await _cache.SetAsync(cacheKey, data, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<SymbolResolutionResult?> ResolveAddressAsync(
|
||||
string debugId,
|
||||
ulong address,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var results = await ResolveAsync(debugId, [address], cancellationToken).ConfigureAwait(false);
|
||||
return results.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<SymbolManifestQueryResult> QueryManifestsAsync(
|
||||
SymbolManifestQuery query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
var queryParams = new List<string>();
|
||||
if (!string.IsNullOrEmpty(query.DebugId)) queryParams.Add($"debugId={Uri.EscapeDataString(query.DebugId)}");
|
||||
if (!string.IsNullOrEmpty(query.CodeId)) queryParams.Add($"codeId={Uri.EscapeDataString(query.CodeId)}");
|
||||
if (!string.IsNullOrEmpty(query.BinaryName)) queryParams.Add($"binaryName={Uri.EscapeDataString(query.BinaryName)}");
|
||||
if (!string.IsNullOrEmpty(query.Platform)) queryParams.Add($"platform={Uri.EscapeDataString(query.Platform)}");
|
||||
if (query.Format.HasValue) queryParams.Add($"format={query.Format.Value}");
|
||||
if (query.CreatedAfter.HasValue) queryParams.Add($"createdAfter={query.CreatedAfter.Value:O}");
|
||||
if (query.CreatedBefore.HasValue) queryParams.Add($"createdBefore={query.CreatedBefore.Value:O}");
|
||||
if (query.HasDsse.HasValue) queryParams.Add($"hasDsse={query.HasDsse.Value}");
|
||||
queryParams.Add($"offset={query.Offset}");
|
||||
queryParams.Add($"limit={query.Limit}");
|
||||
|
||||
var url = "/v1/symbols/manifests?" + string.Join("&", queryParams);
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
AddTenantHeader(request);
|
||||
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var listResponse = await response.Content.ReadFromJsonAsync<ManifestListResponse>(_jsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return new SymbolManifestQueryResult
|
||||
{
|
||||
Manifests = listResponse!.Manifests.Select(m => new SymbolManifestSummary
|
||||
{
|
||||
ManifestId = m.ManifestId,
|
||||
DebugId = m.DebugId,
|
||||
CodeId = m.CodeId,
|
||||
BinaryName = m.BinaryName,
|
||||
Platform = m.Platform,
|
||||
Format = m.Format,
|
||||
SymbolCount = m.SymbolCount,
|
||||
HasDsse = m.HasDsse,
|
||||
CreatedAt = m.CreatedAt
|
||||
}).ToList(),
|
||||
TotalCount = listResponse.TotalCount,
|
||||
Offset = listResponse.Offset,
|
||||
Limit = listResponse.Limit
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<SymbolsHealthStatus> GetHealthAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
using var response = await _httpClient.GetAsync("/health", cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var health = await response.Content.ReadFromJsonAsync<HealthResponse>(_jsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return new SymbolsHealthStatus
|
||||
{
|
||||
Status = health!.Status,
|
||||
Version = health.Version,
|
||||
Timestamp = health.Timestamp,
|
||||
TotalManifests = health.Metrics?.TotalManifests,
|
||||
TotalSymbols = health.Metrics?.TotalSymbols
|
||||
};
|
||||
}
|
||||
|
||||
private void AddTenantHeader(HttpRequestMessage request)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_options.TenantId))
|
||||
{
|
||||
request.Headers.Add(TenantHeader, _options.TenantId);
|
||||
}
|
||||
}
|
||||
|
||||
private static SymbolManifest MapToManifest(ManifestDetailResponse detail)
|
||||
{
|
||||
return new SymbolManifest
|
||||
{
|
||||
ManifestId = detail.ManifestId,
|
||||
DebugId = detail.DebugId,
|
||||
CodeId = detail.CodeId,
|
||||
BinaryName = detail.BinaryName,
|
||||
Platform = detail.Platform,
|
||||
Format = detail.Format,
|
||||
TenantId = detail.TenantId,
|
||||
BlobUri = detail.BlobUri,
|
||||
DsseDigest = detail.DsseDigest,
|
||||
RekorLogIndex = detail.RekorLogIndex,
|
||||
Symbols = detail.Symbols.Select(s => new SymbolEntry
|
||||
{
|
||||
Address = s.Address,
|
||||
Size = s.Size,
|
||||
MangledName = s.MangledName,
|
||||
DemangledName = s.DemangledName,
|
||||
Type = s.Type,
|
||||
Binding = s.Binding,
|
||||
SourceFile = s.SourceFile,
|
||||
SourceLine = s.SourceLine,
|
||||
ContentHash = s.ContentHash
|
||||
}).ToList(),
|
||||
SourceMappings = detail.SourceMappings?.Select(m => new SourceMapping
|
||||
{
|
||||
CompiledPath = m.CompiledPath,
|
||||
SourcePath = m.SourcePath,
|
||||
ContentHash = m.ContentHash
|
||||
}).ToList(),
|
||||
CreatedAt = detail.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_cache?.Dispose();
|
||||
}
|
||||
|
||||
// Request/Response DTOs for serialization
|
||||
private sealed record UploadManifestRequest(
|
||||
string DebugId,
|
||||
string BinaryName,
|
||||
string? CodeId,
|
||||
string? Platform,
|
||||
BinaryFormat Format,
|
||||
IReadOnlyList<SymbolEntryRequest> Symbols,
|
||||
IReadOnlyList<SourceMappingRequest>? SourceMappings);
|
||||
|
||||
private sealed record SymbolEntryRequest(
|
||||
ulong Address,
|
||||
ulong Size,
|
||||
string MangledName,
|
||||
string? DemangledName,
|
||||
SymbolType Type,
|
||||
SymbolBinding Binding,
|
||||
string? SourceFile,
|
||||
int? SourceLine,
|
||||
string? ContentHash);
|
||||
|
||||
private sealed record SourceMappingRequest(
|
||||
string CompiledPath,
|
||||
string SourcePath,
|
||||
string? ContentHash);
|
||||
|
||||
private sealed record UploadManifestResponse(
|
||||
string ManifestId,
|
||||
string DebugId,
|
||||
string BinaryName,
|
||||
string? BlobUri,
|
||||
int SymbolCount,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
private sealed record ManifestDetailResponse(
|
||||
string ManifestId,
|
||||
string DebugId,
|
||||
string? CodeId,
|
||||
string BinaryName,
|
||||
string? Platform,
|
||||
BinaryFormat Format,
|
||||
string TenantId,
|
||||
string? BlobUri,
|
||||
string? DsseDigest,
|
||||
long? RekorLogIndex,
|
||||
int SymbolCount,
|
||||
IReadOnlyList<SymbolEntryRequest> Symbols,
|
||||
IReadOnlyList<SourceMappingRequest>? SourceMappings,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
private sealed record ManifestSummaryResponse(
|
||||
string ManifestId,
|
||||
string DebugId,
|
||||
string? CodeId,
|
||||
string BinaryName,
|
||||
string? Platform,
|
||||
BinaryFormat Format,
|
||||
int SymbolCount,
|
||||
bool HasDsse,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
private sealed record ManifestListResponse(
|
||||
IReadOnlyList<ManifestSummaryResponse> Manifests,
|
||||
int TotalCount,
|
||||
int Offset,
|
||||
int Limit);
|
||||
|
||||
private sealed record ResolveRequest(string DebugId, IReadOnlyList<ulong> Addresses);
|
||||
|
||||
private sealed record ResolveResponse(string DebugId, IReadOnlyList<ResolutionDto> Resolutions);
|
||||
|
||||
private sealed record ResolutionDto(
|
||||
ulong Address,
|
||||
bool Found,
|
||||
string? MangledName,
|
||||
string? DemangledName,
|
||||
ulong Offset,
|
||||
string? SourceFile,
|
||||
int? SourceLine,
|
||||
double Confidence);
|
||||
|
||||
private sealed record HealthResponse(
|
||||
string Status,
|
||||
string Version,
|
||||
DateTimeOffset Timestamp,
|
||||
HealthMetrics? Metrics);
|
||||
|
||||
private sealed record HealthMetrics(
|
||||
long TotalManifests,
|
||||
long TotalSymbols,
|
||||
long TotalBlobBytes);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
namespace StellaOps.Symbols.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for the Symbols client.
|
||||
/// </summary>
|
||||
public sealed class SymbolsClientOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Base URL of the Symbols server.
|
||||
/// </summary>
|
||||
public string BaseUrl { get; set; } = "http://localhost:5270";
|
||||
|
||||
/// <summary>
|
||||
/// Timeout for HTTP requests in seconds.
|
||||
/// </summary>
|
||||
public int TimeoutSeconds { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum retry attempts for transient failures.
|
||||
/// </summary>
|
||||
public int MaxRetries { get; set; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Enable local disk cache for resolved symbols.
|
||||
/// </summary>
|
||||
public bool EnableDiskCache { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Path to the disk cache directory.
|
||||
/// </summary>
|
||||
public string CachePath { get; set; } = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"StellaOps", "SymbolsCache");
|
||||
|
||||
/// <summary>
|
||||
/// Maximum size of disk cache in bytes (default 1GB).
|
||||
/// </summary>
|
||||
public long MaxCacheSizeBytes { get; set; } = 1024 * 1024 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Tenant ID header value for multi-tenant requests.
|
||||
/// </summary>
|
||||
public string? TenantId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# StellaOps.Symbols.Client Task Board
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Symbols/StellaOps.Symbols.Client/StellaOps.Symbols.Client.md. |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
@@ -0,0 +1,86 @@
|
||||
namespace StellaOps.Symbols.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Blob store for symbol files (PDBs, DWARF, etc.).
|
||||
/// </summary>
|
||||
public interface ISymbolBlobStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Uploads a symbol blob and returns its CAS URI.
|
||||
/// </summary>
|
||||
Task<SymbolBlobUploadResult> UploadAsync(
|
||||
Stream content,
|
||||
string tenantId,
|
||||
string debugId,
|
||||
string? fileName = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a symbol blob by CAS URI.
|
||||
/// </summary>
|
||||
Task<Stream?> DownloadAsync(
|
||||
string blobUri,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a blob exists.
|
||||
/// </summary>
|
||||
Task<bool> ExistsAsync(
|
||||
string blobUri,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets blob metadata without downloading content.
|
||||
/// </summary>
|
||||
Task<SymbolBlobMetadata?> GetMetadataAsync(
|
||||
string blobUri,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a blob (requires admin).
|
||||
/// </summary>
|
||||
Task<bool> DeleteAsync(
|
||||
string blobUri,
|
||||
string reason,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of blob upload operation.
|
||||
/// </summary>
|
||||
public sealed record SymbolBlobUploadResult
|
||||
{
|
||||
/// <summary>
|
||||
/// CAS URI for the uploaded blob.
|
||||
/// </summary>
|
||||
public required string BlobUri { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// BLAKE3 hash of the content.
|
||||
/// </summary>
|
||||
public required string ContentHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Size in bytes.
|
||||
/// </summary>
|
||||
public required long Size { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// True if this was a duplicate (already existed).
|
||||
/// </summary>
|
||||
public bool IsDuplicate { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metadata about a stored blob.
|
||||
/// </summary>
|
||||
public sealed record SymbolBlobMetadata
|
||||
{
|
||||
public required string BlobUri { get; init; }
|
||||
public required string ContentHash { get; init; }
|
||||
public required long Size { get; init; }
|
||||
public required string ContentType { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public string? TenantId { get; init; }
|
||||
public string? DebugId { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using StellaOps.Symbols.Core.Models;
|
||||
|
||||
namespace StellaOps.Symbols.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for storing and retrieving symbol manifests.
|
||||
/// </summary>
|
||||
public interface ISymbolRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores a symbol manifest.
|
||||
/// </summary>
|
||||
Task<string> StoreManifestAsync(SymbolManifest manifest, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a manifest by ID.
|
||||
/// </summary>
|
||||
Task<SymbolManifest?> GetManifestAsync(string manifestId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves manifests by debug ID (may return multiple for different platforms).
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<SymbolManifest>> GetManifestsByDebugIdAsync(
|
||||
string debugId,
|
||||
string? tenantId = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves manifests by code ID.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<SymbolManifest>> GetManifestsByCodeIdAsync(
|
||||
string codeId,
|
||||
string? tenantId = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Queries manifests with filters.
|
||||
/// </summary>
|
||||
Task<SymbolQueryResult> QueryManifestsAsync(
|
||||
SymbolQuery query,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a manifest exists.
|
||||
/// </summary>
|
||||
Task<bool> ExistsAsync(string manifestId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a manifest (soft delete with tombstone).
|
||||
/// </summary>
|
||||
Task<bool> DeleteManifestAsync(string manifestId, string reason, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Query parameters for symbol manifests.
|
||||
/// </summary>
|
||||
public sealed record SymbolQuery
|
||||
{
|
||||
public string? TenantId { get; init; }
|
||||
public string? DebugId { get; init; }
|
||||
public string? CodeId { get; init; }
|
||||
public string? BinaryName { get; init; }
|
||||
public string? Platform { get; init; }
|
||||
public BinaryFormat? Format { get; init; }
|
||||
public DateTimeOffset? CreatedAfter { get; init; }
|
||||
public DateTimeOffset? CreatedBefore { get; init; }
|
||||
public bool? HasDsse { get; init; }
|
||||
public int Limit { get; init; } = 50;
|
||||
public int Offset { get; init; } = 0;
|
||||
public string SortBy { get; init; } = "created_at";
|
||||
public bool SortDescending { get; init; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of a symbol query.
|
||||
/// </summary>
|
||||
public sealed record SymbolQueryResult
|
||||
{
|
||||
public required IReadOnlyList<SymbolManifest> Manifests { get; init; }
|
||||
public required int TotalCount { get; init; }
|
||||
public required int Offset { get; init; }
|
||||
public required int Limit { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using StellaOps.Symbols.Core.Models;
|
||||
|
||||
namespace StellaOps.Symbols.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves symbols for addresses in binaries.
|
||||
/// </summary>
|
||||
public interface ISymbolResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves a symbol at the given address.
|
||||
/// </summary>
|
||||
Task<SymbolResolution?> ResolveAsync(
|
||||
string debugId,
|
||||
ulong address,
|
||||
string? tenantId = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Batch resolve multiple addresses.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<SymbolResolution>> ResolveBatchAsync(
|
||||
string debugId,
|
||||
IEnumerable<ulong> addresses,
|
||||
string? tenantId = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all symbols for a binary.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<SymbolEntry>> GetAllSymbolsAsync(
|
||||
string debugId,
|
||||
string? tenantId = null,
|
||||
SymbolType? typeFilter = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of symbol resolution.
|
||||
/// </summary>
|
||||
public sealed record SymbolResolution
|
||||
{
|
||||
/// <summary>
|
||||
/// The requested address.
|
||||
/// </summary>
|
||||
public required ulong Address { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// True if a symbol was found.
|
||||
/// </summary>
|
||||
public required bool Found { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The symbol entry if found.
|
||||
/// </summary>
|
||||
public SymbolEntry? Symbol { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Offset within the symbol (address - symbol.Address).
|
||||
/// </summary>
|
||||
public ulong Offset { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Debug ID used for resolution.
|
||||
/// </summary>
|
||||
public required string DebugId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Manifest ID that provided the symbol.
|
||||
/// </summary>
|
||||
public string? ManifestId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolution confidence (1.0 = exact match).
|
||||
/// </summary>
|
||||
public double Confidence { get; init; } = 1.0;
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
namespace StellaOps.Symbols.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a symbol manifest containing debug symbols for a binary artifact.
|
||||
/// </summary>
|
||||
public sealed record SymbolManifest
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique identifier for this manifest (BLAKE3 hash of content).
|
||||
/// </summary>
|
||||
public required string ManifestId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Debug ID (build-id or PDB GUID) for lookup.
|
||||
/// </summary>
|
||||
public required string DebugId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Code ID for the binary (GNU build-id, PE checksum, etc.).
|
||||
/// </summary>
|
||||
public string? CodeId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Original binary name.
|
||||
/// </summary>
|
||||
public required string BinaryName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Platform/architecture (e.g., linux-x64, win-x64).
|
||||
/// </summary>
|
||||
public string? Platform { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Binary format (ELF, PE, Mach-O).
|
||||
/// </summary>
|
||||
public BinaryFormat Format { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol entries in the manifest.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<SymbolEntry> Symbols { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Source file mappings if available.
|
||||
/// </summary>
|
||||
public IReadOnlyList<SourceMapping>? SourceMappings { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Tenant ID for multi-tenant isolation.
|
||||
/// </summary>
|
||||
public required string TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CAS URI where the symbol blob is stored.
|
||||
/// </summary>
|
||||
public string? BlobUri { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// DSSE envelope digest if signed.
|
||||
/// </summary>
|
||||
public string? DsseDigest { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Rekor log index if published.
|
||||
/// </summary>
|
||||
public long? RekorLogIndex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Created timestamp (UTC).
|
||||
/// </summary>
|
||||
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Hash algorithm used for ManifestId.
|
||||
/// </summary>
|
||||
public string HashAlgorithm { get; init; } = "blake3";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Individual symbol entry in a manifest.
|
||||
/// </summary>
|
||||
public sealed record SymbolEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol address (virtual address or offset).
|
||||
/// </summary>
|
||||
public required ulong Address { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol size in bytes.
|
||||
/// </summary>
|
||||
public ulong Size { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Mangled symbol name.
|
||||
/// </summary>
|
||||
public required string MangledName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Demangled/human-readable name.
|
||||
/// </summary>
|
||||
public string? DemangledName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol type (function, variable, etc.).
|
||||
/// </summary>
|
||||
public SymbolType Type { get; init; } = SymbolType.Function;
|
||||
|
||||
/// <summary>
|
||||
/// Symbol binding (local, global, weak).
|
||||
/// </summary>
|
||||
public SymbolBinding Binding { get; init; } = SymbolBinding.Global;
|
||||
|
||||
/// <summary>
|
||||
/// Source file path if available.
|
||||
/// </summary>
|
||||
public string? SourceFile { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Source line number if available.
|
||||
/// </summary>
|
||||
public int? SourceLine { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// BLAKE3 hash of the symbol content for deduplication.
|
||||
/// </summary>
|
||||
public string? ContentHash { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Source file mapping for source-level debugging.
|
||||
/// </summary>
|
||||
public sealed record SourceMapping
|
||||
{
|
||||
/// <summary>
|
||||
/// Compiled file path in binary.
|
||||
/// </summary>
|
||||
public required string CompiledPath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Original source file path.
|
||||
/// </summary>
|
||||
public required string SourcePath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Source content hash for verification.
|
||||
/// </summary>
|
||||
public string? ContentHash { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binary format types.
|
||||
/// </summary>
|
||||
public enum BinaryFormat
|
||||
{
|
||||
Unknown = 0,
|
||||
Elf = 1,
|
||||
Pe = 2,
|
||||
MachO = 3,
|
||||
Wasm = 4
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Symbol types.
|
||||
/// </summary>
|
||||
public enum SymbolType
|
||||
{
|
||||
Unknown = 0,
|
||||
Function = 1,
|
||||
Variable = 2,
|
||||
Object = 3,
|
||||
Section = 4,
|
||||
File = 5,
|
||||
TlsData = 6
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Symbol binding types.
|
||||
/// </summary>
|
||||
public enum SymbolBinding
|
||||
{
|
||||
Local = 0,
|
||||
Global = 1,
|
||||
Weak = 2
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,8 @@
|
||||
# StellaOps.Symbols.Core Task Board
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Symbols/StellaOps.Symbols.Core/StellaOps.Symbols.Core.md. |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
@@ -0,0 +1,76 @@
|
||||
using Blake3;
|
||||
using StellaOps.Symbols.Core.Models;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Symbols.Infrastructure.Hashing;
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic BLAKE3 hashing helpers for Symbols artifacts and manifests.
|
||||
/// </summary>
|
||||
public static class SymbolHashing
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes a BLAKE3 digest string with algorithm prefix.
|
||||
/// </summary>
|
||||
public static string ComputeHash(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
using var hasher = Hasher.New();
|
||||
hasher.Update(bytes);
|
||||
return $"blake3:{Convert.ToHexStringLower(hasher.Finalize().AsSpan())}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a deterministic manifest identifier from manifest identity inputs.
|
||||
/// </summary>
|
||||
public static string ComputeManifestId(
|
||||
string debugId,
|
||||
string tenantId,
|
||||
IReadOnlyList<SymbolEntry> symbols)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(debugId);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
|
||||
ArgumentNullException.ThrowIfNull(symbols);
|
||||
|
||||
var builder = new StringBuilder(capacity: 256 + (symbols.Count * 96));
|
||||
builder.Append("debug=").Append(debugId.Trim()).Append('\n');
|
||||
builder.Append("tenant=").Append(tenantId.Trim()).Append('\n');
|
||||
|
||||
foreach (var line in symbols
|
||||
.Select(SerializeSymbolEntry)
|
||||
.OrderBy(static value => value, StringComparer.Ordinal))
|
||||
{
|
||||
builder.Append(line).Append('\n');
|
||||
}
|
||||
|
||||
return ComputeHash(Encoding.UTF8.GetBytes(builder.ToString()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts lowercase hexadecimal digest bytes from a prefixed hash value.
|
||||
/// </summary>
|
||||
public static string ExtractHex(string hash)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(hash);
|
||||
|
||||
const string prefix = "blake3:";
|
||||
if (hash.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return hash[prefix.Length..].ToLowerInvariant();
|
||||
}
|
||||
|
||||
return hash.ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string SerializeSymbolEntry(SymbolEntry symbol)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(symbol);
|
||||
|
||||
static string N(string? value) => value?.Trim() ?? string.Empty;
|
||||
static string NInt(int? value) => value?.ToString(CultureInfo.InvariantCulture) ?? string.Empty;
|
||||
|
||||
return string.Create(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"addr={symbol.Address:x16}|size={symbol.Size}|m={N(symbol.MangledName)}|d={N(symbol.DemangledName)}|t={symbol.Type}|b={symbol.Binding}|sf={N(symbol.SourceFile)}|sl={NInt(symbol.SourceLine)}|h={N(symbol.ContentHash)}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
using StellaOps.Symbols.Core.Abstractions;
|
||||
using StellaOps.Symbols.Core.Models;
|
||||
|
||||
namespace StellaOps.Symbols.Infrastructure.Resolution;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation of symbol resolver using the symbol repository.
|
||||
/// </summary>
|
||||
public sealed class DefaultSymbolResolver : ISymbolResolver
|
||||
{
|
||||
private readonly ISymbolRepository _repository;
|
||||
|
||||
public DefaultSymbolResolver(ISymbolRepository repository)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<SymbolResolution?> ResolveAsync(
|
||||
string debugId,
|
||||
ulong address,
|
||||
string? tenantId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var manifests = await _repository.GetManifestsByDebugIdAsync(debugId, tenantId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
foreach (var manifest in manifests)
|
||||
{
|
||||
var symbol = FindSymbolAtAddress(manifest.Symbols, address);
|
||||
if (symbol is not null)
|
||||
{
|
||||
return new SymbolResolution
|
||||
{
|
||||
Address = address,
|
||||
Found = true,
|
||||
Symbol = symbol,
|
||||
Offset = address - symbol.Address,
|
||||
DebugId = debugId,
|
||||
ManifestId = manifest.ManifestId,
|
||||
Confidence = 1.0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return new SymbolResolution
|
||||
{
|
||||
Address = address,
|
||||
Found = false,
|
||||
DebugId = debugId,
|
||||
Confidence = 0.0
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IReadOnlyList<SymbolResolution>> ResolveBatchAsync(
|
||||
string debugId,
|
||||
IEnumerable<ulong> addresses,
|
||||
string? tenantId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var manifests = await _repository.GetManifestsByDebugIdAsync(debugId, tenantId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var results = new List<SymbolResolution>();
|
||||
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
SymbolResolution? resolution = null;
|
||||
|
||||
foreach (var manifest in manifests)
|
||||
{
|
||||
var symbol = FindSymbolAtAddress(manifest.Symbols, address);
|
||||
if (symbol is not null)
|
||||
{
|
||||
resolution = new SymbolResolution
|
||||
{
|
||||
Address = address,
|
||||
Found = true,
|
||||
Symbol = symbol,
|
||||
Offset = address - symbol.Address,
|
||||
DebugId = debugId,
|
||||
ManifestId = manifest.ManifestId,
|
||||
Confidence = 1.0
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
results.Add(resolution ?? new SymbolResolution
|
||||
{
|
||||
Address = address,
|
||||
Found = false,
|
||||
DebugId = debugId,
|
||||
Confidence = 0.0
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IReadOnlyList<SymbolEntry>> GetAllSymbolsAsync(
|
||||
string debugId,
|
||||
string? tenantId = null,
|
||||
SymbolType? typeFilter = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var manifests = await _repository.GetManifestsByDebugIdAsync(debugId, tenantId, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var symbols = manifests
|
||||
.SelectMany(m => m.Symbols)
|
||||
.Where(s => !typeFilter.HasValue || s.Type == typeFilter.Value)
|
||||
.DistinctBy(s => s.Address)
|
||||
.OrderBy(s => s.Address)
|
||||
.ToList();
|
||||
|
||||
return symbols;
|
||||
}
|
||||
|
||||
private static SymbolEntry? FindSymbolAtAddress(IReadOnlyList<SymbolEntry> symbols, ulong address)
|
||||
{
|
||||
// Binary search for the symbol containing the address
|
||||
var left = 0;
|
||||
var right = symbols.Count - 1;
|
||||
SymbolEntry? candidate = null;
|
||||
|
||||
while (left <= right)
|
||||
{
|
||||
var mid = left + (right - left) / 2;
|
||||
var symbol = symbols[mid];
|
||||
|
||||
if (address >= symbol.Address && address < symbol.Address + symbol.Size)
|
||||
{
|
||||
return symbol; // Exact match within symbol bounds
|
||||
}
|
||||
|
||||
if (address >= symbol.Address)
|
||||
{
|
||||
candidate = symbol;
|
||||
left = mid + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
right = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a candidate and address is within reasonable range, return it
|
||||
if (candidate is not null && address >= candidate.Address && address < candidate.Address + Math.Max(candidate.Size, 4096))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Symbols.Core.Abstractions;
|
||||
using StellaOps.Symbols.Infrastructure.Resolution;
|
||||
using StellaOps.Symbols.Infrastructure.Storage;
|
||||
|
||||
namespace StellaOps.Symbols.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Service collection extensions for Symbols infrastructure.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds in-memory symbol services for development and testing.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSymbolsInMemory(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddSingleton<ISymbolRepository, InMemorySymbolRepository>();
|
||||
services.TryAddSingleton<ISymbolBlobStore, InMemorySymbolBlobStore>();
|
||||
services.TryAddSingleton<ISymbolResolver, DefaultSymbolResolver>();
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the default symbol resolver.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSymbolResolver(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddSingleton<ISymbolResolver, DefaultSymbolResolver>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blake3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Symbols.Core\StellaOps.Symbols.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,103 @@
|
||||
|
||||
using StellaOps.Symbols.Core.Abstractions;
|
||||
using StellaOps.Symbols.Infrastructure.Hashing;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Symbols.Infrastructure.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory implementation of symbol blob store for development and testing.
|
||||
/// </summary>
|
||||
public sealed class InMemorySymbolBlobStore : ISymbolBlobStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, BlobEntry> _blobs = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<SymbolBlobUploadResult> UploadAsync(
|
||||
Stream content,
|
||||
string tenantId,
|
||||
string debugId,
|
||||
string? fileName = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false);
|
||||
var data = ms.ToArray();
|
||||
|
||||
var contentHash = SymbolHashing.ComputeHash(data);
|
||||
var blobUri = $"cas://symbols/{tenantId}/{debugId}/{SymbolHashing.ExtractHex(contentHash)}";
|
||||
|
||||
var isDuplicate = _blobs.ContainsKey(blobUri);
|
||||
|
||||
var entry = new BlobEntry(
|
||||
Data: data,
|
||||
ContentHash: contentHash,
|
||||
TenantId: tenantId,
|
||||
DebugId: debugId,
|
||||
FileName: fileName,
|
||||
ContentType: "application/octet-stream",
|
||||
CreatedAt: DateTimeOffset.UtcNow);
|
||||
|
||||
_blobs[blobUri] = entry;
|
||||
|
||||
return new SymbolBlobUploadResult
|
||||
{
|
||||
BlobUri = blobUri,
|
||||
ContentHash = contentHash,
|
||||
Size = data.Length,
|
||||
IsDuplicate = isDuplicate
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<Stream?> DownloadAsync(string blobUri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_blobs.TryGetValue(blobUri, out var entry))
|
||||
{
|
||||
return Task.FromResult<Stream?>(null);
|
||||
}
|
||||
|
||||
return Task.FromResult<Stream?>(new MemoryStream(entry.Data));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<bool> ExistsAsync(string blobUri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(_blobs.ContainsKey(blobUri));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<SymbolBlobMetadata?> GetMetadataAsync(string blobUri, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_blobs.TryGetValue(blobUri, out var entry))
|
||||
{
|
||||
return Task.FromResult<SymbolBlobMetadata?>(null);
|
||||
}
|
||||
|
||||
return Task.FromResult<SymbolBlobMetadata?>(new SymbolBlobMetadata
|
||||
{
|
||||
BlobUri = blobUri,
|
||||
ContentHash = entry.ContentHash,
|
||||
Size = entry.Data.Length,
|
||||
ContentType = entry.ContentType,
|
||||
CreatedAt = entry.CreatedAt,
|
||||
TenantId = entry.TenantId,
|
||||
DebugId = entry.DebugId
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<bool> DeleteAsync(string blobUri, string reason, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(_blobs.TryRemove(blobUri, out _));
|
||||
}
|
||||
|
||||
private sealed record BlobEntry(
|
||||
byte[] Data,
|
||||
string ContentHash,
|
||||
string TenantId,
|
||||
string DebugId,
|
||||
string? FileName,
|
||||
string ContentType,
|
||||
DateTimeOffset CreatedAt);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
|
||||
using StellaOps.Symbols.Core.Abstractions;
|
||||
using StellaOps.Symbols.Core.Models;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace StellaOps.Symbols.Infrastructure.Storage;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory implementation of symbol repository for development and testing.
|
||||
/// </summary>
|
||||
public sealed class InMemorySymbolRepository : ISymbolRepository
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, SymbolManifest> _manifests = new();
|
||||
private readonly ConcurrentDictionary<string, HashSet<string>> _debugIdIndex = new();
|
||||
private readonly ConcurrentDictionary<string, HashSet<string>> _codeIdIndex = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<string> StoreManifestAsync(SymbolManifest manifest, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_manifests[manifest.ManifestId] = manifest;
|
||||
|
||||
// Update debug ID index
|
||||
_debugIdIndex.AddOrUpdate(
|
||||
manifest.DebugId,
|
||||
_ => [manifest.ManifestId],
|
||||
(_, set) => { set.Add(manifest.ManifestId); return set; });
|
||||
|
||||
// Update code ID index if present
|
||||
if (!string.IsNullOrEmpty(manifest.CodeId))
|
||||
{
|
||||
_codeIdIndex.AddOrUpdate(
|
||||
manifest.CodeId,
|
||||
_ => [manifest.ManifestId],
|
||||
(_, set) => { set.Add(manifest.ManifestId); return set; });
|
||||
}
|
||||
|
||||
return Task.FromResult(manifest.ManifestId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<SymbolManifest?> GetManifestAsync(string manifestId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_manifests.TryGetValue(manifestId, out var manifest);
|
||||
return Task.FromResult(manifest);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<IReadOnlyList<SymbolManifest>> GetManifestsByDebugIdAsync(
|
||||
string debugId,
|
||||
string? tenantId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_debugIdIndex.TryGetValue(debugId, out var ids))
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<SymbolManifest>>([]);
|
||||
}
|
||||
|
||||
var manifests = ids
|
||||
.Select(id => _manifests.GetValueOrDefault(id))
|
||||
.Where(m => m is not null && (tenantId is null || m.TenantId == tenantId))
|
||||
.Cast<SymbolManifest>()
|
||||
.OrderByDescending(m => m.CreatedAt)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<SymbolManifest>>(manifests);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<IReadOnlyList<SymbolManifest>> GetManifestsByCodeIdAsync(
|
||||
string codeId,
|
||||
string? tenantId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_codeIdIndex.TryGetValue(codeId, out var ids))
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<SymbolManifest>>([]);
|
||||
}
|
||||
|
||||
var manifests = ids
|
||||
.Select(id => _manifests.GetValueOrDefault(id))
|
||||
.Where(m => m is not null && (tenantId is null || m.TenantId == tenantId))
|
||||
.Cast<SymbolManifest>()
|
||||
.OrderByDescending(m => m.CreatedAt)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult<IReadOnlyList<SymbolManifest>>(manifests);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<SymbolQueryResult> QueryManifestsAsync(SymbolQuery query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var manifests = _manifests.Values.AsEnumerable();
|
||||
|
||||
if (!string.IsNullOrEmpty(query.TenantId))
|
||||
manifests = manifests.Where(m => m.TenantId == query.TenantId);
|
||||
if (!string.IsNullOrEmpty(query.DebugId))
|
||||
manifests = manifests.Where(m => m.DebugId == query.DebugId);
|
||||
if (!string.IsNullOrEmpty(query.CodeId))
|
||||
manifests = manifests.Where(m => m.CodeId == query.CodeId);
|
||||
if (!string.IsNullOrEmpty(query.BinaryName))
|
||||
manifests = manifests.Where(m => m.BinaryName.Contains(query.BinaryName, StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrEmpty(query.Platform))
|
||||
manifests = manifests.Where(m => m.Platform == query.Platform);
|
||||
if (query.Format.HasValue)
|
||||
manifests = manifests.Where(m => m.Format == query.Format.Value);
|
||||
if (query.CreatedAfter.HasValue)
|
||||
manifests = manifests.Where(m => m.CreatedAt >= query.CreatedAfter.Value);
|
||||
if (query.CreatedBefore.HasValue)
|
||||
manifests = manifests.Where(m => m.CreatedAt <= query.CreatedBefore.Value);
|
||||
if (query.HasDsse.HasValue)
|
||||
manifests = manifests.Where(m => !string.IsNullOrEmpty(m.DsseDigest) == query.HasDsse.Value);
|
||||
|
||||
var total = manifests.Count();
|
||||
|
||||
manifests = query.SortDescending
|
||||
? manifests.OrderByDescending(m => m.CreatedAt)
|
||||
: manifests.OrderBy(m => m.CreatedAt);
|
||||
|
||||
var result = manifests
|
||||
.Skip(query.Offset)
|
||||
.Take(query.Limit)
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult(new SymbolQueryResult
|
||||
{
|
||||
Manifests = result,
|
||||
TotalCount = total,
|
||||
Offset = query.Offset,
|
||||
Limit = query.Limit
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<bool> ExistsAsync(string manifestId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.FromResult(_manifests.ContainsKey(manifestId));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<bool> DeleteManifestAsync(string manifestId, string reason, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_manifests.TryRemove(manifestId, out var manifest))
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
// Remove from indexes
|
||||
if (_debugIdIndex.TryGetValue(manifest.DebugId, out var debugSet))
|
||||
{
|
||||
debugSet.Remove(manifestId);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(manifest.CodeId) && _codeIdIndex.TryGetValue(manifest.CodeId, out var codeSet))
|
||||
{
|
||||
codeSet.Remove(manifestId);
|
||||
}
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# StellaOps.Symbols.Infrastructure Task Board
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Symbols/StellaOps.Symbols.Infrastructure/StellaOps.Symbols.Infrastructure.md. |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace StellaOps.Symbols.Marketplace.Models;
|
||||
|
||||
/// <summary>
|
||||
/// A catalog entry representing an installable symbol/debug pack.
|
||||
/// </summary>
|
||||
public sealed record SymbolPackCatalogEntry
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public Guid SourceId { get; init; }
|
||||
public string PackId { get; init; } = string.Empty;
|
||||
public string Platform { get; init; } = string.Empty;
|
||||
public string[] Components { get; init; } = [];
|
||||
public string DsseDigest { get; init; } = string.Empty;
|
||||
public string Version { get; init; } = string.Empty;
|
||||
public long SizeBytes { get; init; }
|
||||
public bool Installed { get; init; }
|
||||
public DateTimeOffset PublishedAt { get; init; }
|
||||
public DateTimeOffset? InstalledAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace StellaOps.Symbols.Marketplace.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Registry of symbol providers (vendor/distro/community/partner).
|
||||
/// </summary>
|
||||
public sealed record SymbolPackSource
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string Key { get; init; } = string.Empty;
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string SourceType { get; init; } = string.Empty;
|
||||
public string? Url { get; init; }
|
||||
public int Priority { get; init; }
|
||||
public bool Enabled { get; init; } = true;
|
||||
public int FreshnessSlaSeconds { get; init; } = 21600;
|
||||
public decimal WarningRatio { get; init; } = 0.80m;
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset? UpdatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace StellaOps.Symbols.Marketplace.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Freshness projection for a symbol source, mirroring the AdvisorySourceFreshnessRecord pattern.
|
||||
/// </summary>
|
||||
public sealed record SymbolSourceFreshnessRecord(
|
||||
Guid SourceId,
|
||||
string SourceKey,
|
||||
string SourceName,
|
||||
string SourceType,
|
||||
string? SourceUrl,
|
||||
int Priority,
|
||||
bool Enabled,
|
||||
DateTimeOffset? LastSyncAt,
|
||||
DateTimeOffset? LastSuccessAt,
|
||||
string? LastError,
|
||||
long SyncCount,
|
||||
int ErrorCount,
|
||||
int FreshnessSlaSeconds,
|
||||
decimal WarningRatio,
|
||||
long FreshnessAgeSeconds,
|
||||
string FreshnessStatus,
|
||||
string SignatureStatus,
|
||||
long TotalPacks,
|
||||
long SignedPacks,
|
||||
long UnsignedPacks,
|
||||
long SignatureFailureCount);
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace StellaOps.Symbols.Marketplace.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Four-dimension trust score for a symbol source.
|
||||
/// Each dimension is 0.0 to 1.0; Overall is a weighted average.
|
||||
/// </summary>
|
||||
public sealed record SymbolSourceTrustScore(
|
||||
double Freshness,
|
||||
double Signature,
|
||||
double Coverage,
|
||||
double SlCompliance,
|
||||
double Overall);
|
||||
@@ -0,0 +1,34 @@
|
||||
using StellaOps.Symbols.Marketplace.Models;
|
||||
|
||||
namespace StellaOps.Symbols.Marketplace.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for marketplace catalog operations (browse, install, uninstall).
|
||||
/// </summary>
|
||||
public interface IMarketplaceCatalogRepository
|
||||
{
|
||||
Task<IReadOnlyList<SymbolPackCatalogEntry>> ListCatalogAsync(
|
||||
Guid? sourceId,
|
||||
string? search,
|
||||
int limit,
|
||||
int offset,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<SymbolPackCatalogEntry?> GetCatalogEntryAsync(
|
||||
Guid entryId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task InstallPackAsync(
|
||||
Guid entryId,
|
||||
string tenantId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task UninstallPackAsync(
|
||||
Guid entryId,
|
||||
string tenantId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<IReadOnlyList<SymbolPackCatalogEntry>> ListInstalledAsync(
|
||||
string tenantId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using StellaOps.Symbols.Marketplace.Models;
|
||||
|
||||
namespace StellaOps.Symbols.Marketplace.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Read repository for symbol sources and their freshness projections.
|
||||
/// </summary>
|
||||
public interface ISymbolSourceReadRepository
|
||||
{
|
||||
Task<IReadOnlyList<SymbolSourceFreshnessRecord>> ListSourcesAsync(
|
||||
bool includeDisabled,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<SymbolSourceFreshnessRecord?> GetSourceByIdAsync(
|
||||
Guid sourceId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using StellaOps.Symbols.Marketplace.Models;
|
||||
|
||||
namespace StellaOps.Symbols.Marketplace.Scoring;
|
||||
|
||||
/// <summary>
|
||||
/// Default trust scorer using weighted dimensions:
|
||||
/// Freshness=0.3, Signature=0.3, Coverage=0.2, SLA=0.2.
|
||||
/// </summary>
|
||||
public sealed class DefaultSymbolSourceTrustScorer : ISymbolSourceTrustScorer
|
||||
{
|
||||
private const double WeightFreshness = 0.3;
|
||||
private const double WeightSignature = 0.3;
|
||||
private const double WeightCoverage = 0.2;
|
||||
private const double WeightSla = 0.2;
|
||||
|
||||
public SymbolSourceTrustScore CalculateTrust(SymbolSourceFreshnessRecord source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
||||
var freshness = ComputeFreshnessDimension(source);
|
||||
var signature = ComputeSignatureDimension(source);
|
||||
var coverage = ComputeCoverageDimension(source);
|
||||
var slCompliance = ComputeSlaDimension(source);
|
||||
|
||||
var overall =
|
||||
(freshness * WeightFreshness) +
|
||||
(signature * WeightSignature) +
|
||||
(coverage * WeightCoverage) +
|
||||
(slCompliance * WeightSla);
|
||||
|
||||
return new SymbolSourceTrustScore(
|
||||
Freshness: Clamp(freshness),
|
||||
Signature: Clamp(signature),
|
||||
Coverage: Clamp(coverage),
|
||||
SlCompliance: Clamp(slCompliance),
|
||||
Overall: Clamp(overall));
|
||||
}
|
||||
|
||||
private static double ComputeFreshnessDimension(SymbolSourceFreshnessRecord source)
|
||||
{
|
||||
if (source.FreshnessSlaSeconds <= 0)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return source.FreshnessStatus switch
|
||||
{
|
||||
"healthy" => 1.0,
|
||||
"warning" => 1.0 - ((double)source.FreshnessAgeSeconds / source.FreshnessSlaSeconds),
|
||||
"stale" => Math.Max(0.0, 0.3 - (0.1 * ((double)source.FreshnessAgeSeconds / source.FreshnessSlaSeconds - 1.0))),
|
||||
_ => 0.0 // unavailable
|
||||
};
|
||||
}
|
||||
|
||||
private static double ComputeSignatureDimension(SymbolSourceFreshnessRecord source)
|
||||
{
|
||||
if (source.TotalPacks <= 0)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return (double)source.SignedPacks / source.TotalPacks;
|
||||
}
|
||||
|
||||
private static double ComputeCoverageDimension(SymbolSourceFreshnessRecord source)
|
||||
{
|
||||
// Coverage is derived from the presence of packs relative to sync activity.
|
||||
// A source with packs and no errors has full coverage potential.
|
||||
if (source.SyncCount <= 0)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
if (source.TotalPacks <= 0)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
var errorRate = source.SyncCount > 0
|
||||
? (double)source.ErrorCount / source.SyncCount
|
||||
: 0.0;
|
||||
|
||||
return Math.Max(0.0, 1.0 - errorRate);
|
||||
}
|
||||
|
||||
private static double ComputeSlaDimension(SymbolSourceFreshnessRecord source)
|
||||
{
|
||||
if (source.FreshnessSlaSeconds <= 0)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// SLA compliance is based on whether the source stays within its freshness window.
|
||||
if (source.FreshnessAgeSeconds <= source.FreshnessSlaSeconds)
|
||||
{
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// Degrade linearly up to 2x the SLA window, then zero.
|
||||
var overage = (double)(source.FreshnessAgeSeconds - source.FreshnessSlaSeconds) / source.FreshnessSlaSeconds;
|
||||
return Math.Max(0.0, 1.0 - overage);
|
||||
}
|
||||
|
||||
private static double Clamp(double value) => Math.Clamp(value, 0.0, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using StellaOps.Symbols.Marketplace.Models;
|
||||
|
||||
namespace StellaOps.Symbols.Marketplace.Scoring;
|
||||
|
||||
/// <summary>
|
||||
/// Computes a trust score for a symbol source based on freshness, signature coverage,
|
||||
/// artifact coverage, and SLA compliance.
|
||||
/// </summary>
|
||||
public interface ISymbolSourceTrustScorer
|
||||
{
|
||||
SymbolSourceTrustScore CalculateTrust(SymbolSourceFreshnessRecord source);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
29
src/BinaryIndex/__Tests/StellaOps.Symbols.Tests/AGENTS.md
Normal file
29
src/BinaryIndex/__Tests/StellaOps.Symbols.Tests/AGENTS.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# AGENTS.md - Symbols Tests
|
||||
|
||||
## Module Overview
|
||||
Tests for the StellaOps Symbols module covering Core models, Bundle operations, Client behavior, and Server components.
|
||||
|
||||
## Roles & Responsibilities
|
||||
- **QA Engineer**: Write and maintain unit tests for all Symbols module components
|
||||
- **Backend Engineer**: Ensure test coverage for new features in Symbols components
|
||||
|
||||
## Test Categories
|
||||
- `Core/` - Tests for SymbolManifest, SymbolEntry, and resolution models
|
||||
- `Bundle/` - Tests for BundleManifest, BundleBuilder, and bundle operations
|
||||
- `Client/` - Tests for SymbolsClient HTTP operations and caching
|
||||
- `Server/` - Tests for server-side symbol storage and resolution (future)
|
||||
|
||||
## Working Agreements
|
||||
1. All model tests should verify required properties and default values
|
||||
2. Client tests should use mock HTTP handlers to avoid network dependencies
|
||||
3. Bundle tests should verify deterministic ordering and hashing
|
||||
4. Tests must be deterministic - no reliance on `DateTime.Now` or `Guid.NewGuid()` in assertions
|
||||
|
||||
## Documentation Prerequisites
|
||||
- `src/Symbols/AGENTS.md` - Module architecture and guidelines
|
||||
- Symbol resolution and bundle format specifications
|
||||
|
||||
## Running Tests
|
||||
```bash
|
||||
dotnet test src/Symbols/__Tests/StellaOps.Symbols.Tests/StellaOps.Symbols.Tests.csproj
|
||||
```
|
||||
@@ -0,0 +1,251 @@
|
||||
using StellaOps.Symbols.Bundle.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Symbols.Tests.Bundle;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for BundleManifest model.
|
||||
/// </summary>
|
||||
public sealed class BundleManifestTests
|
||||
{
|
||||
[Fact]
|
||||
public void BundleManifest_RequiredProperties_MustBeSet()
|
||||
{
|
||||
var manifest = new BundleManifest
|
||||
{
|
||||
BundleId = "blake3:bundle123",
|
||||
Name = "debug-symbols-linux",
|
||||
Version = "1.0.0",
|
||||
CreatedAt = new DateTimeOffset(2026, 1, 13, 10, 0, 0, TimeSpan.Zero),
|
||||
Entries = []
|
||||
};
|
||||
|
||||
Assert.Equal("blake3:bundle123", manifest.BundleId);
|
||||
Assert.Equal("debug-symbols-linux", manifest.Name);
|
||||
Assert.Equal("1.0.0", manifest.Version);
|
||||
Assert.Empty(manifest.Entries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BundleManifest_SchemaVersion_HasDefault()
|
||||
{
|
||||
var manifest = new BundleManifest
|
||||
{
|
||||
BundleId = "blake3:test",
|
||||
Name = "test-bundle",
|
||||
Version = "0.1.0",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Entries = []
|
||||
};
|
||||
|
||||
Assert.Equal("stellaops.symbols.bundle/v1", manifest.SchemaVersion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BundleManifest_HashAlgorithm_DefaultsToBlake3()
|
||||
{
|
||||
var manifest = new BundleManifest
|
||||
{
|
||||
BundleId = "blake3:hash",
|
||||
Name = "hash-test",
|
||||
Version = "1.0.0",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Entries = []
|
||||
};
|
||||
|
||||
Assert.Equal("blake3", manifest.HashAlgorithm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BundleManifest_OptionalProperties_DefaultToNull()
|
||||
{
|
||||
var manifest = new BundleManifest
|
||||
{
|
||||
BundleId = "blake3:optional",
|
||||
Name = "optional-test",
|
||||
Version = "1.0.0",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Entries = []
|
||||
};
|
||||
|
||||
Assert.Null(manifest.Platform);
|
||||
Assert.Null(manifest.TenantId);
|
||||
Assert.Null(manifest.Signature);
|
||||
Assert.Null(manifest.RekorCheckpoint);
|
||||
Assert.Null(manifest.Metadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BundleManifest_WithEntries_ContainsBundleEntries()
|
||||
{
|
||||
var entries = new List<BundleEntry>
|
||||
{
|
||||
new()
|
||||
{
|
||||
DebugId = "DEBUG001",
|
||||
BinaryName = "lib1.so",
|
||||
ManifestHash = "blake3:manifest1",
|
||||
BlobHash = "blake3:blob1",
|
||||
BlobSizeBytes = 1024,
|
||||
ArchivePath = "symbols/DEBUG001/lib1.so.symbols"
|
||||
},
|
||||
new()
|
||||
{
|
||||
DebugId = "DEBUG002",
|
||||
BinaryName = "lib2.so",
|
||||
ManifestHash = "blake3:manifest2",
|
||||
BlobHash = "blake3:blob2",
|
||||
BlobSizeBytes = 2048,
|
||||
ArchivePath = "symbols/DEBUG002/lib2.so.symbols"
|
||||
}
|
||||
};
|
||||
|
||||
var manifest = new BundleManifest
|
||||
{
|
||||
BundleId = "blake3:multi",
|
||||
Name = "multi-entry",
|
||||
Version = "1.0.0",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Entries = entries,
|
||||
TotalSizeBytes = 3072
|
||||
};
|
||||
|
||||
Assert.Equal(2, manifest.Entries.Count);
|
||||
Assert.Equal(3072, manifest.TotalSizeBytes);
|
||||
Assert.Equal("DEBUG001", manifest.Entries[0].DebugId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BundleManifest_WithSignature_ContainsDsseInfo()
|
||||
{
|
||||
var signature = new BundleSignature
|
||||
{
|
||||
Signed = true,
|
||||
Algorithm = "ecdsa-p256",
|
||||
KeyId = "key-001",
|
||||
DsseDigest = "blake3:dsse123",
|
||||
SignedAt = new DateTimeOffset(2026, 1, 13, 12, 0, 0, TimeSpan.Zero)
|
||||
};
|
||||
|
||||
var manifest = new BundleManifest
|
||||
{
|
||||
BundleId = "blake3:signed",
|
||||
Name = "signed-bundle",
|
||||
Version = "1.0.0",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Entries = [],
|
||||
Signature = signature
|
||||
};
|
||||
|
||||
Assert.NotNull(manifest.Signature);
|
||||
Assert.True(manifest.Signature.Signed);
|
||||
Assert.Equal("ecdsa-p256", manifest.Signature.Algorithm);
|
||||
Assert.Equal("key-001", manifest.Signature.KeyId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BundleManifest_WithRekorCheckpoint_ContainsTransparencyInfo()
|
||||
{
|
||||
var checkpoint = new RekorCheckpoint
|
||||
{
|
||||
RekorUrl = "https://rekor.sigstore.dev",
|
||||
LogEntryId = "entry-uuid-123",
|
||||
LogIndex = 12345678,
|
||||
IntegratedTime = new DateTimeOffset(2026, 1, 13, 11, 30, 0, TimeSpan.Zero),
|
||||
RootHash = "blake3:roothash",
|
||||
TreeSize = 99999999
|
||||
};
|
||||
|
||||
var manifest = new BundleManifest
|
||||
{
|
||||
BundleId = "blake3:transparent",
|
||||
Name = "transparent-bundle",
|
||||
Version = "1.0.0",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Entries = [],
|
||||
RekorCheckpoint = checkpoint
|
||||
};
|
||||
|
||||
Assert.NotNull(manifest.RekorCheckpoint);
|
||||
Assert.Equal(12345678, manifest.RekorCheckpoint.LogIndex);
|
||||
Assert.Equal("https://rekor.sigstore.dev", manifest.RekorCheckpoint.RekorUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BundleManifest_WithMetadata_ContainsCustomData()
|
||||
{
|
||||
var metadata = new Dictionary<string, string>
|
||||
{
|
||||
["build_id"] = "CI-20260113-001",
|
||||
["git_commit"] = "abc123def456",
|
||||
["branch"] = "main"
|
||||
};
|
||||
|
||||
var manifest = new BundleManifest
|
||||
{
|
||||
BundleId = "blake3:metadata",
|
||||
Name = "metadata-bundle",
|
||||
Version = "1.0.0",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Entries = [],
|
||||
Metadata = metadata
|
||||
};
|
||||
|
||||
Assert.NotNull(manifest.Metadata);
|
||||
Assert.Equal(3, manifest.Metadata.Count);
|
||||
Assert.Equal("main", manifest.Metadata["branch"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BundleEntry_RequiredProperties_MustBeSet()
|
||||
{
|
||||
var entry = new BundleEntry
|
||||
{
|
||||
DebugId = "DEBUG123",
|
||||
BinaryName = "test.so",
|
||||
ManifestHash = "blake3:manifest",
|
||||
BlobHash = "blake3:blob",
|
||||
ArchivePath = "symbols/DEBUG123/test.so.symbols"
|
||||
};
|
||||
|
||||
Assert.Equal("DEBUG123", entry.DebugId);
|
||||
Assert.Equal("test.so", entry.BinaryName);
|
||||
Assert.Equal("blake3:manifest", entry.ManifestHash);
|
||||
Assert.Equal("blake3:blob", entry.BlobHash);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RekorCheckpoint_RequiredProperties_MustBeSet()
|
||||
{
|
||||
var checkpoint = new RekorCheckpoint
|
||||
{
|
||||
RekorUrl = "https://rekor.example.com",
|
||||
LogEntryId = "log-entry-001",
|
||||
LogIndex = 1000,
|
||||
IntegratedTime = DateTimeOffset.UtcNow,
|
||||
RootHash = "blake3:root",
|
||||
TreeSize = 5000
|
||||
};
|
||||
|
||||
Assert.Equal("https://rekor.example.com", checkpoint.RekorUrl);
|
||||
Assert.Equal("log-entry-001", checkpoint.LogEntryId);
|
||||
Assert.Equal(1000, checkpoint.LogIndex);
|
||||
Assert.Equal(5000, checkpoint.TreeSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InclusionProof_RequiredProperties_MustBeSet()
|
||||
{
|
||||
var proof = new InclusionProof
|
||||
{
|
||||
LogIndex = 12345,
|
||||
RootHash = "blake3:proofroot",
|
||||
TreeSize = 100000,
|
||||
Hashes = ["hash1", "hash2", "hash3"]
|
||||
};
|
||||
|
||||
Assert.Equal(12345, proof.LogIndex);
|
||||
Assert.Equal("blake3:proofroot", proof.RootHash);
|
||||
Assert.Equal(3, proof.Hashes.Count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using StellaOps.Symbols.Client;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Symbols.Tests.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for SymbolsClientOptions.
|
||||
/// </summary>
|
||||
public sealed class SymbolsClientOptionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void SymbolsClientOptions_DefaultValues_AreSet()
|
||||
{
|
||||
var options = new SymbolsClientOptions();
|
||||
|
||||
Assert.Equal("http://localhost:5270", options.BaseUrl);
|
||||
Assert.Equal(30, options.TimeoutSeconds);
|
||||
Assert.Equal(3, options.MaxRetries);
|
||||
Assert.True(options.EnableDiskCache);
|
||||
Assert.Equal(1024L * 1024 * 1024, options.MaxCacheSizeBytes);
|
||||
Assert.Null(options.TenantId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SymbolsClientOptions_CachePath_ContainsStellaOps()
|
||||
{
|
||||
var options = new SymbolsClientOptions();
|
||||
|
||||
Assert.Contains("StellaOps", options.CachePath);
|
||||
Assert.Contains("SymbolsCache", options.CachePath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SymbolsClientOptions_CanBeCustomized()
|
||||
{
|
||||
var options = new SymbolsClientOptions
|
||||
{
|
||||
BaseUrl = "https://symbols.example.com",
|
||||
TimeoutSeconds = 60,
|
||||
MaxRetries = 5,
|
||||
EnableDiskCache = false,
|
||||
MaxCacheSizeBytes = 512 * 1024 * 1024,
|
||||
TenantId = "tenant-test"
|
||||
};
|
||||
|
||||
Assert.Equal("https://symbols.example.com", options.BaseUrl);
|
||||
Assert.Equal(60, options.TimeoutSeconds);
|
||||
Assert.Equal(5, options.MaxRetries);
|
||||
Assert.False(options.EnableDiskCache);
|
||||
Assert.Equal(512L * 1024 * 1024, options.MaxCacheSizeBytes);
|
||||
Assert.Equal("tenant-test", options.TenantId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SymbolsClientOptions_CachePath_CanBeOverridden()
|
||||
{
|
||||
var customPath = "/custom/cache/path";
|
||||
var options = new SymbolsClientOptions
|
||||
{
|
||||
CachePath = customPath
|
||||
};
|
||||
|
||||
Assert.Equal(customPath, options.CachePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Symbols.Client;
|
||||
using StellaOps.Symbols.Core.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Symbols.Tests.Client;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for SymbolsClient with mock HTTP.
|
||||
/// </summary>
|
||||
public sealed class SymbolsClientTests : IDisposable
|
||||
{
|
||||
private readonly MockHttpMessageHandler _mockHandler;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly SymbolsClient _client;
|
||||
|
||||
public SymbolsClientTests()
|
||||
{
|
||||
_mockHandler = new MockHttpMessageHandler();
|
||||
_httpClient = new HttpClient(_mockHandler)
|
||||
{
|
||||
BaseAddress = new Uri("http://localhost:5270")
|
||||
};
|
||||
|
||||
var options = Options.Create(new SymbolsClientOptions
|
||||
{
|
||||
BaseUrl = "http://localhost:5270",
|
||||
TenantId = "test-tenant",
|
||||
EnableDiskCache = false
|
||||
});
|
||||
|
||||
_client = new SymbolsClient(_httpClient, options);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetManifestAsync_NotFound_ReturnsNull()
|
||||
{
|
||||
_mockHandler.SetResponse(System.Net.HttpStatusCode.NotFound, "{}");
|
||||
|
||||
var result = await _client.GetManifestAsync("nonexistent-manifest");
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetManifestAsync_Disposed_ThrowsObjectDisposedException()
|
||||
{
|
||||
_client.Dispose();
|
||||
|
||||
await Assert.ThrowsAsync<ObjectDisposedException>(
|
||||
() => _client.GetManifestAsync("any-manifest"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetManifestsByDebugIdAsync_ReturnsManifests()
|
||||
{
|
||||
var responseJson = """
|
||||
[
|
||||
{
|
||||
"manifestId": "blake3:manifest1",
|
||||
"debugId": "DEBUG123",
|
||||
"binaryName": "test.so",
|
||||
"createdAt": "2026-01-13T10:00:00Z",
|
||||
"format": "elf"
|
||||
}
|
||||
]
|
||||
""";
|
||||
_mockHandler.SetResponse(System.Net.HttpStatusCode.OK, responseJson);
|
||||
|
||||
var result = await _client.GetManifestsByDebugIdAsync("DEBUG123");
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal("blake3:manifest1", result[0].ManifestId);
|
||||
Assert.Equal("DEBUG123", result[0].DebugId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAddressAsync_Found_ReturnsResult()
|
||||
{
|
||||
var responseJson = """
|
||||
{
|
||||
"resolutions": [
|
||||
{
|
||||
"address": 4096,
|
||||
"found": true,
|
||||
"mangledName": "_ZN4Test4funcEv",
|
||||
"demangledName": "Test::func()",
|
||||
"offset": 16,
|
||||
"confidence": 1.0
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
_mockHandler.SetResponse(System.Net.HttpStatusCode.OK, responseJson);
|
||||
|
||||
var result = await _client.ResolveAddressAsync("DEBUG123", 4096);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.Found);
|
||||
Assert.Equal("Test::func()", result.DemangledName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResolveAsync_BatchAddresses_ReturnsResults()
|
||||
{
|
||||
var responseJson = """
|
||||
{
|
||||
"resolutions": [
|
||||
{ "address": 4096, "found": true, "mangledName": "main", "confidence": 1.0 },
|
||||
{ "address": 8192, "found": false, "confidence": 0.0 }
|
||||
]
|
||||
}
|
||||
""";
|
||||
_mockHandler.SetResponse(System.Net.HttpStatusCode.OK, responseJson);
|
||||
|
||||
var result = await _client.ResolveAsync("DEBUG123", [4096UL, 8192UL]);
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.True(result[0].Found);
|
||||
Assert.False(result[1].Found);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_client.Dispose();
|
||||
_httpClient.Dispose();
|
||||
_mockHandler.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple mock HTTP handler for testing.
|
||||
/// </summary>
|
||||
private sealed class MockHttpMessageHandler : HttpMessageHandler
|
||||
{
|
||||
private System.Net.HttpStatusCode _statusCode = System.Net.HttpStatusCode.OK;
|
||||
private string _responseContent = "{}";
|
||||
|
||||
public void SetResponse(System.Net.HttpStatusCode statusCode, string content)
|
||||
{
|
||||
_statusCode = statusCode;
|
||||
_responseContent = content;
|
||||
}
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var response = new HttpResponseMessage(_statusCode)
|
||||
{
|
||||
Content = new StringContent(_responseContent, System.Text.Encoding.UTF8, "application/json")
|
||||
};
|
||||
return Task.FromResult(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using StellaOps.Symbols.Core.Models;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Symbols.Tests.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for SymbolManifest model.
|
||||
/// </summary>
|
||||
public sealed class SymbolManifestTests
|
||||
{
|
||||
[Fact]
|
||||
public void SymbolManifest_RequiredProperties_MustBeSet()
|
||||
{
|
||||
var manifest = new SymbolManifest
|
||||
{
|
||||
ManifestId = "blake3:abc123def456",
|
||||
DebugId = "123456789ABCDEF0",
|
||||
BinaryName = "libtest.so",
|
||||
TenantId = "tenant-001",
|
||||
Symbols = []
|
||||
};
|
||||
|
||||
Assert.Equal("blake3:abc123def456", manifest.ManifestId);
|
||||
Assert.Equal("123456789ABCDEF0", manifest.DebugId);
|
||||
Assert.Equal("libtest.so", manifest.BinaryName);
|
||||
Assert.Equal("tenant-001", manifest.TenantId);
|
||||
Assert.Empty(manifest.Symbols);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SymbolManifest_OptionalProperties_DefaultToNull()
|
||||
{
|
||||
var manifest = new SymbolManifest
|
||||
{
|
||||
ManifestId = "blake3:test",
|
||||
DebugId = "DEADBEEF",
|
||||
BinaryName = "test.exe",
|
||||
TenantId = "tenant",
|
||||
Symbols = []
|
||||
};
|
||||
|
||||
Assert.Null(manifest.CodeId);
|
||||
Assert.Null(manifest.Platform);
|
||||
Assert.Null(manifest.SourceMappings);
|
||||
Assert.Null(manifest.BlobUri);
|
||||
Assert.Null(manifest.DsseDigest);
|
||||
Assert.Null(manifest.RekorLogIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SymbolManifest_HashAlgorithm_DefaultsToBlake3()
|
||||
{
|
||||
var manifest = new SymbolManifest
|
||||
{
|
||||
ManifestId = "blake3:test",
|
||||
DebugId = "DEADBEEF",
|
||||
BinaryName = "test.exe",
|
||||
TenantId = "tenant",
|
||||
Symbols = []
|
||||
};
|
||||
|
||||
Assert.Equal("blake3", manifest.HashAlgorithm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SymbolManifest_WithSymbols_ContainsEntries()
|
||||
{
|
||||
var symbols = new List<SymbolEntry>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Address = 0x1000,
|
||||
Size = 100,
|
||||
MangledName = "_ZN4Test4funcEv",
|
||||
DemangledName = "Test::func()",
|
||||
Type = SymbolType.Function
|
||||
},
|
||||
new()
|
||||
{
|
||||
Address = 0x2000,
|
||||
Size = 8,
|
||||
MangledName = "g_globalVar",
|
||||
Type = SymbolType.Object
|
||||
}
|
||||
};
|
||||
|
||||
var manifest = new SymbolManifest
|
||||
{
|
||||
ManifestId = "blake3:multi",
|
||||
DebugId = "BUILD123",
|
||||
BinaryName = "mylib.so",
|
||||
TenantId = "tenant",
|
||||
Symbols = symbols
|
||||
};
|
||||
|
||||
Assert.Equal(2, manifest.Symbols.Count);
|
||||
Assert.Equal(0x1000UL, manifest.Symbols[0].Address);
|
||||
Assert.Equal("Test::func()", manifest.Symbols[0].DemangledName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SymbolManifest_Format_CanBeSet()
|
||||
{
|
||||
var manifest = new SymbolManifest
|
||||
{
|
||||
ManifestId = "blake3:elf",
|
||||
DebugId = "ELFBUILD",
|
||||
BinaryName = "app.elf",
|
||||
TenantId = "tenant",
|
||||
Symbols = [],
|
||||
Format = BinaryFormat.Elf
|
||||
};
|
||||
|
||||
Assert.Equal(BinaryFormat.Elf, manifest.Format);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SymbolEntry_RequiredProperties_MustBeSet()
|
||||
{
|
||||
var entry = new SymbolEntry
|
||||
{
|
||||
Address = 0x400000,
|
||||
MangledName = "main"
|
||||
};
|
||||
|
||||
Assert.Equal(0x400000UL, entry.Address);
|
||||
Assert.Equal("main", entry.MangledName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SymbolEntry_Size_DefaultsToZero()
|
||||
{
|
||||
var entry = new SymbolEntry
|
||||
{
|
||||
Address = 0x1000,
|
||||
MangledName = "test"
|
||||
};
|
||||
|
||||
Assert.Equal(0UL, entry.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceMapping_RequiredProperties_MustBeSet()
|
||||
{
|
||||
var mapping = new SourceMapping
|
||||
{
|
||||
CompiledPath = "/build/src/main.o",
|
||||
SourcePath = "/home/dev/project/src/main.cpp"
|
||||
};
|
||||
|
||||
Assert.Equal("/build/src/main.o", mapping.CompiledPath);
|
||||
Assert.Equal("/home/dev/project/src/main.cpp", mapping.SourcePath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceMapping_ContentHash_IsOptional()
|
||||
{
|
||||
var mapping = new SourceMapping
|
||||
{
|
||||
CompiledPath = "obj/debug/file.o",
|
||||
SourcePath = "src/file.c",
|
||||
ContentHash = "blake3:sourcehash123"
|
||||
};
|
||||
|
||||
Assert.Equal("blake3:sourcehash123", mapping.ContentHash);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using StellaOps.Symbols.Core.Models;
|
||||
using StellaOps.Symbols.Infrastructure.Hashing;
|
||||
using StellaOps.Symbols.Infrastructure.Storage;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Symbols.Tests.Infrastructure;
|
||||
|
||||
public sealed class SymbolHashingTests
|
||||
{
|
||||
[Fact]
|
||||
public void ComputeHash_UsesBlake3Prefix_AndIsDeterministic()
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes("deterministic-symbol-content");
|
||||
|
||||
var first = SymbolHashing.ComputeHash(bytes);
|
||||
var second = SymbolHashing.ComputeHash(bytes);
|
||||
|
||||
Assert.Equal(first, second);
|
||||
Assert.StartsWith("blake3:", first, StringComparison.Ordinal);
|
||||
Assert.Equal(64, SymbolHashing.ExtractHex(first).Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeManifestId_IsDeterministic_AndOrderIndependent()
|
||||
{
|
||||
var symbolA = new SymbolEntry
|
||||
{
|
||||
Address = 0x10,
|
||||
Size = 16,
|
||||
MangledName = "_Z4funcv",
|
||||
DemangledName = "func()",
|
||||
Type = SymbolType.Function,
|
||||
Binding = SymbolBinding.Global,
|
||||
SourceFile = "a.c",
|
||||
SourceLine = 42,
|
||||
ContentHash = "blake3:aaa"
|
||||
};
|
||||
|
||||
var symbolB = new SymbolEntry
|
||||
{
|
||||
Address = 0x20,
|
||||
Size = 8,
|
||||
MangledName = "_Z4barv",
|
||||
DemangledName = "bar()",
|
||||
Type = SymbolType.Function,
|
||||
Binding = SymbolBinding.Local,
|
||||
SourceFile = "b.c",
|
||||
SourceLine = 12,
|
||||
ContentHash = "blake3:bbb"
|
||||
};
|
||||
|
||||
var first = SymbolHashing.ComputeManifestId("DBG-1", "tenant-a", [symbolA, symbolB]);
|
||||
var second = SymbolHashing.ComputeManifestId("DBG-1", "tenant-a", [symbolB, symbolA]);
|
||||
var third = SymbolHashing.ComputeManifestId("DBG-1", "tenant-a", [symbolA, symbolB]);
|
||||
|
||||
Assert.Equal(first, second);
|
||||
Assert.Equal(first, third);
|
||||
Assert.StartsWith("blake3:", first, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InMemoryBlobStore_UploadsWithBlake3Hash_AndDetectsDuplicates()
|
||||
{
|
||||
var store = new InMemorySymbolBlobStore();
|
||||
var payload = Encoding.UTF8.GetBytes("symbols-blob-deterministic");
|
||||
|
||||
var first = await store.UploadAsync(
|
||||
new MemoryStream(payload),
|
||||
tenantId: "tenant-a",
|
||||
debugId: "dbg-1");
|
||||
|
||||
var second = await store.UploadAsync(
|
||||
new MemoryStream(payload),
|
||||
tenantId: "tenant-a",
|
||||
debugId: "dbg-1");
|
||||
|
||||
Assert.StartsWith("blake3:", first.ContentHash, StringComparison.Ordinal);
|
||||
Assert.Equal(first.ContentHash, second.ContentHash);
|
||||
Assert.Equal(first.BlobUri, second.BlobUri);
|
||||
Assert.False(first.IsDuplicate);
|
||||
Assert.True(second.IsDuplicate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using StellaOps.Symbols.Marketplace.Models;
|
||||
|
||||
namespace StellaOps.Symbols.Tests.Marketplace;
|
||||
|
||||
public class SymbolPackCatalogEntryTests
|
||||
{
|
||||
[Fact]
|
||||
public void Construction_AllFields_RoundTrips()
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
var sourceId = Guid.NewGuid();
|
||||
var published = DateTimeOffset.UtcNow;
|
||||
var installed = DateTimeOffset.UtcNow;
|
||||
|
||||
var entry = new SymbolPackCatalogEntry
|
||||
{
|
||||
Id = id,
|
||||
SourceId = sourceId,
|
||||
PackId = "pkg:deb/ubuntu/libc6-dbg@2.35-0ubuntu3",
|
||||
Platform = "linux/amd64",
|
||||
Components = ["libc6", "ld-linux"],
|
||||
DsseDigest = "sha256:abcdef1234567890",
|
||||
Version = "2.35-0ubuntu3",
|
||||
SizeBytes = 15_000_000,
|
||||
Installed = true,
|
||||
PublishedAt = published,
|
||||
InstalledAt = installed,
|
||||
};
|
||||
|
||||
Assert.Equal(id, entry.Id);
|
||||
Assert.Equal(sourceId, entry.SourceId);
|
||||
Assert.Equal("pkg:deb/ubuntu/libc6-dbg@2.35-0ubuntu3", entry.PackId);
|
||||
Assert.Equal("linux/amd64", entry.Platform);
|
||||
Assert.Equal(2, entry.Components.Length);
|
||||
Assert.Contains("libc6", entry.Components);
|
||||
Assert.Contains("ld-linux", entry.Components);
|
||||
Assert.Equal("sha256:abcdef1234567890", entry.DsseDigest);
|
||||
Assert.Equal("2.35-0ubuntu3", entry.Version);
|
||||
Assert.Equal(15_000_000, entry.SizeBytes);
|
||||
Assert.True(entry.Installed);
|
||||
Assert.Equal(published, entry.PublishedAt);
|
||||
Assert.Equal(installed, entry.InstalledAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Defaults_EmptyStringsAndArrays()
|
||||
{
|
||||
var entry = new SymbolPackCatalogEntry();
|
||||
|
||||
Assert.Equal(Guid.Empty, entry.Id);
|
||||
Assert.Equal(Guid.Empty, entry.SourceId);
|
||||
Assert.Equal(string.Empty, entry.PackId);
|
||||
Assert.Equal(string.Empty, entry.Platform);
|
||||
Assert.Empty(entry.Components);
|
||||
Assert.Equal(string.Empty, entry.DsseDigest);
|
||||
Assert.Equal(string.Empty, entry.Version);
|
||||
Assert.Equal(0, entry.SizeBytes);
|
||||
Assert.False(entry.Installed);
|
||||
Assert.Null(entry.InstalledAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecordEquality_SameValues_AreEqual()
|
||||
{
|
||||
var id = Guid.NewGuid();
|
||||
var sourceId = Guid.NewGuid();
|
||||
var published = new DateTimeOffset(2026, 2, 19, 12, 0, 0, TimeSpan.Zero);
|
||||
|
||||
var entry1 = new SymbolPackCatalogEntry
|
||||
{
|
||||
Id = id,
|
||||
SourceId = sourceId,
|
||||
PackId = "test-pack",
|
||||
Platform = "linux/amd64",
|
||||
Components = ["comp1"],
|
||||
Version = "1.0",
|
||||
PublishedAt = published,
|
||||
};
|
||||
|
||||
var entry2 = new SymbolPackCatalogEntry
|
||||
{
|
||||
Id = id,
|
||||
SourceId = sourceId,
|
||||
PackId = "test-pack",
|
||||
Platform = "linux/amd64",
|
||||
Components = ["comp1"],
|
||||
Version = "1.0",
|
||||
PublishedAt = published,
|
||||
};
|
||||
|
||||
Assert.Equal(entry1.Id, entry2.Id);
|
||||
Assert.Equal(entry1.PackId, entry2.PackId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using StellaOps.Symbols.Marketplace.Models;
|
||||
|
||||
namespace StellaOps.Symbols.Tests.Marketplace;
|
||||
|
||||
public class SymbolSourceFreshnessRecordTests
|
||||
{
|
||||
[Fact]
|
||||
public void Construction_AllFields_RoundTrips()
|
||||
{
|
||||
var sourceId = Guid.NewGuid();
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
var record = new SymbolSourceFreshnessRecord(
|
||||
SourceId: sourceId,
|
||||
SourceKey: "microsoft-symbols",
|
||||
SourceName: "Microsoft Public Symbols",
|
||||
SourceType: "vendor",
|
||||
SourceUrl: "https://msdl.microsoft.com/download/symbols",
|
||||
Priority: 1,
|
||||
Enabled: true,
|
||||
LastSyncAt: now,
|
||||
LastSuccessAt: now,
|
||||
LastError: null,
|
||||
SyncCount: 42,
|
||||
ErrorCount: 1,
|
||||
FreshnessSlaSeconds: 21600,
|
||||
WarningRatio: 0.80m,
|
||||
FreshnessAgeSeconds: 3600,
|
||||
FreshnessStatus: "healthy",
|
||||
SignatureStatus: "signed",
|
||||
TotalPacks: 500,
|
||||
SignedPacks: 490,
|
||||
UnsignedPacks: 10,
|
||||
SignatureFailureCount: 0);
|
||||
|
||||
Assert.Equal(sourceId, record.SourceId);
|
||||
Assert.Equal("microsoft-symbols", record.SourceKey);
|
||||
Assert.Equal("Microsoft Public Symbols", record.SourceName);
|
||||
Assert.Equal("vendor", record.SourceType);
|
||||
Assert.Equal(1, record.Priority);
|
||||
Assert.True(record.Enabled);
|
||||
Assert.Equal(42, record.SyncCount);
|
||||
Assert.Equal(1, record.ErrorCount);
|
||||
Assert.Equal(21600, record.FreshnessSlaSeconds);
|
||||
Assert.Equal(0.80m, record.WarningRatio);
|
||||
Assert.Equal(3600, record.FreshnessAgeSeconds);
|
||||
Assert.Equal("healthy", record.FreshnessStatus);
|
||||
Assert.Equal("signed", record.SignatureStatus);
|
||||
Assert.Equal(500, record.TotalPacks);
|
||||
Assert.Equal(490, record.SignedPacks);
|
||||
Assert.Equal(10, record.UnsignedPacks);
|
||||
Assert.Equal(0, record.SignatureFailureCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Construction_NullOptionalFields_DefaultsCorrectly()
|
||||
{
|
||||
var record = new SymbolSourceFreshnessRecord(
|
||||
SourceId: Guid.Empty,
|
||||
SourceKey: "test",
|
||||
SourceName: "Test",
|
||||
SourceType: "community",
|
||||
SourceUrl: null,
|
||||
Priority: 99,
|
||||
Enabled: false,
|
||||
LastSyncAt: null,
|
||||
LastSuccessAt: null,
|
||||
LastError: "connection refused",
|
||||
SyncCount: 0,
|
||||
ErrorCount: 5,
|
||||
FreshnessSlaSeconds: 3600,
|
||||
WarningRatio: 0.50m,
|
||||
FreshnessAgeSeconds: 7200,
|
||||
FreshnessStatus: "unavailable",
|
||||
SignatureStatus: "unsigned",
|
||||
TotalPacks: 0,
|
||||
SignedPacks: 0,
|
||||
UnsignedPacks: 0,
|
||||
SignatureFailureCount: 3);
|
||||
|
||||
Assert.Null(record.SourceUrl);
|
||||
Assert.Null(record.LastSyncAt);
|
||||
Assert.Null(record.LastSuccessAt);
|
||||
Assert.Equal("connection refused", record.LastError);
|
||||
Assert.Equal("unavailable", record.FreshnessStatus);
|
||||
Assert.False(record.Enabled);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("healthy")]
|
||||
[InlineData("warning")]
|
||||
[InlineData("stale")]
|
||||
[InlineData("unavailable")]
|
||||
public void FreshnessStatus_AcceptsAllValidStates(string status)
|
||||
{
|
||||
var record = new SymbolSourceFreshnessRecord(
|
||||
SourceId: Guid.NewGuid(),
|
||||
SourceKey: "test",
|
||||
SourceName: "Test",
|
||||
SourceType: "distro",
|
||||
SourceUrl: null,
|
||||
Priority: 1,
|
||||
Enabled: true,
|
||||
LastSyncAt: null,
|
||||
LastSuccessAt: null,
|
||||
LastError: null,
|
||||
SyncCount: 0,
|
||||
ErrorCount: 0,
|
||||
FreshnessSlaSeconds: 21600,
|
||||
WarningRatio: 0.80m,
|
||||
FreshnessAgeSeconds: 0,
|
||||
FreshnessStatus: status,
|
||||
SignatureStatus: "signed",
|
||||
TotalPacks: 0,
|
||||
SignedPacks: 0,
|
||||
UnsignedPacks: 0,
|
||||
SignatureFailureCount: 0);
|
||||
|
||||
Assert.Equal(status, record.FreshnessStatus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
using StellaOps.Symbols.Marketplace.Models;
|
||||
using StellaOps.Symbols.Marketplace.Scoring;
|
||||
|
||||
namespace StellaOps.Symbols.Tests.Marketplace;
|
||||
|
||||
public class SymbolSourceTrustScorerTests
|
||||
{
|
||||
private readonly DefaultSymbolSourceTrustScorer _scorer = new();
|
||||
|
||||
private static SymbolSourceFreshnessRecord CreateRecord(
|
||||
string freshnessStatus = "healthy",
|
||||
long freshnessAgeSeconds = 3600,
|
||||
int freshnessSlaSeconds = 21600,
|
||||
long totalPacks = 100,
|
||||
long signedPacks = 90,
|
||||
long unsignedPacks = 10,
|
||||
long syncCount = 50,
|
||||
int errorCount = 2) =>
|
||||
new(
|
||||
SourceId: Guid.NewGuid(),
|
||||
SourceKey: "test-source",
|
||||
SourceName: "Test Source",
|
||||
SourceType: "vendor",
|
||||
SourceUrl: "https://symbols.example.com",
|
||||
Priority: 1,
|
||||
Enabled: true,
|
||||
LastSyncAt: DateTimeOffset.UtcNow,
|
||||
LastSuccessAt: DateTimeOffset.UtcNow,
|
||||
LastError: null,
|
||||
SyncCount: syncCount,
|
||||
ErrorCount: errorCount,
|
||||
FreshnessSlaSeconds: freshnessSlaSeconds,
|
||||
WarningRatio: 0.80m,
|
||||
FreshnessAgeSeconds: freshnessAgeSeconds,
|
||||
FreshnessStatus: freshnessStatus,
|
||||
SignatureStatus: "signed",
|
||||
TotalPacks: totalPacks,
|
||||
SignedPacks: signedPacks,
|
||||
UnsignedPacks: unsignedPacks,
|
||||
SignatureFailureCount: 0);
|
||||
|
||||
[Fact]
|
||||
public void CalculateTrust_HealthySource_HighOverallScore()
|
||||
{
|
||||
var record = CreateRecord(freshnessStatus: "healthy");
|
||||
var score = _scorer.CalculateTrust(record);
|
||||
|
||||
Assert.Equal(1.0, score.Freshness);
|
||||
Assert.Equal(0.9, score.Signature, 2);
|
||||
Assert.True(score.Overall > 0.8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateTrust_StaleSource_LowFreshness()
|
||||
{
|
||||
var record = CreateRecord(
|
||||
freshnessStatus: "stale",
|
||||
freshnessAgeSeconds: 30000,
|
||||
freshnessSlaSeconds: 21600);
|
||||
var score = _scorer.CalculateTrust(record);
|
||||
|
||||
Assert.True(score.Freshness < 0.5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateTrust_UnavailableSource_ZeroFreshness()
|
||||
{
|
||||
var record = CreateRecord(freshnessStatus: "unavailable");
|
||||
var score = _scorer.CalculateTrust(record);
|
||||
|
||||
Assert.Equal(0.0, score.Freshness);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateTrust_NoSignedPacks_ZeroSignature()
|
||||
{
|
||||
var record = CreateRecord(signedPacks: 0, unsignedPacks: 100);
|
||||
var score = _scorer.CalculateTrust(record);
|
||||
|
||||
Assert.Equal(0.0, score.Signature);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateTrust_AllSignedPacks_FullSignature()
|
||||
{
|
||||
var record = CreateRecord(signedPacks: 100, unsignedPacks: 0);
|
||||
var score = _scorer.CalculateTrust(record);
|
||||
|
||||
Assert.Equal(1.0, score.Signature);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateTrust_NoSyncs_ZeroCoverage()
|
||||
{
|
||||
var record = CreateRecord(syncCount: 0, errorCount: 0);
|
||||
var score = _scorer.CalculateTrust(record);
|
||||
|
||||
Assert.Equal(0.0, score.Coverage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateTrust_NoPacks_ZeroSignatureAndCoverage()
|
||||
{
|
||||
var record = CreateRecord(totalPacks: 0, signedPacks: 0, unsignedPacks: 0);
|
||||
var score = _scorer.CalculateTrust(record);
|
||||
|
||||
Assert.Equal(0.0, score.Signature);
|
||||
Assert.Equal(0.0, score.Coverage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateTrust_WithinSla_FullSlCompliance()
|
||||
{
|
||||
var record = CreateRecord(freshnessAgeSeconds: 10000, freshnessSlaSeconds: 21600);
|
||||
var score = _scorer.CalculateTrust(record);
|
||||
|
||||
Assert.Equal(1.0, score.SlCompliance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateTrust_ExceedsSla_DegradedSlCompliance()
|
||||
{
|
||||
var record = CreateRecord(
|
||||
freshnessStatus: "stale",
|
||||
freshnessAgeSeconds: 32400,
|
||||
freshnessSlaSeconds: 21600);
|
||||
var score = _scorer.CalculateTrust(record);
|
||||
|
||||
Assert.True(score.SlCompliance < 1.0);
|
||||
Assert.True(score.SlCompliance >= 0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateTrust_OverallIsWeightedAverage()
|
||||
{
|
||||
var record = CreateRecord(
|
||||
freshnessStatus: "healthy",
|
||||
totalPacks: 100,
|
||||
signedPacks: 100,
|
||||
syncCount: 100,
|
||||
errorCount: 0,
|
||||
freshnessAgeSeconds: 1000,
|
||||
freshnessSlaSeconds: 21600);
|
||||
var score = _scorer.CalculateTrust(record);
|
||||
|
||||
var expectedOverall =
|
||||
(score.Freshness * 0.3) +
|
||||
(score.Signature * 0.3) +
|
||||
(score.Coverage * 0.2) +
|
||||
(score.SlCompliance * 0.2);
|
||||
|
||||
Assert.Equal(expectedOverall, score.Overall, 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateTrust_AllDimensionsClamped_0_To_1()
|
||||
{
|
||||
var record = CreateRecord();
|
||||
var score = _scorer.CalculateTrust(record);
|
||||
|
||||
Assert.InRange(score.Freshness, 0.0, 1.0);
|
||||
Assert.InRange(score.Signature, 0.0, 1.0);
|
||||
Assert.InRange(score.Coverage, 0.0, 1.0);
|
||||
Assert.InRange(score.SlCompliance, 0.0, 1.0);
|
||||
Assert.InRange(score.Overall, 0.0, 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateTrust_NullSource_ThrowsArgumentNull()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => _scorer.CalculateTrust(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CalculateTrust_ZeroSla_AllZero()
|
||||
{
|
||||
var record = CreateRecord(freshnessSlaSeconds: 0);
|
||||
var score = _scorer.CalculateTrust(record);
|
||||
|
||||
Assert.Equal(0.0, score.Freshness);
|
||||
Assert.Equal(0.0, score.SlCompliance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<UseXunitV3>true</UseXunitV3>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Symbols.Core\StellaOps.Symbols.Core.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Symbols.Bundle\StellaOps.Symbols.Bundle.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Symbols.Client\StellaOps.Symbols.Client.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Symbols.Infrastructure\StellaOps.Symbols.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Symbols.Marketplace\StellaOps.Symbols.Marketplace.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
8
src/BinaryIndex/__Tests/StellaOps.Symbols.Tests/TASKS.md
Normal file
8
src/BinaryIndex/__Tests/StellaOps.Symbols.Tests/TASKS.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# StellaOps.Symbols.Tests Task Board
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Symbols/__Tests/StellaOps.Symbols.Tests/StellaOps.Symbols.Tests.md. |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
|
||||
"diagnosticMessages": true,
|
||||
"parallelizeAssembly": true,
|
||||
"parallelizeTestCollections": true,
|
||||
"maxParallelThreads": -1
|
||||
}
|
||||
Reference in New Issue
Block a user