Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,8 @@ jobs:
- name: Build
run: dotnet build --no-restore --configuration Release

- name: Publish AOT
run: dotnet publish src/Tm7.Cli/Tm7.Cli.csproj --configuration Release --no-restore

- name: Test
run: dotnet test --no-build --configuration Release --verbosity normal
6 changes: 6 additions & 0 deletions src/Tm7.Cli/Tm7.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
so non-nullable properties may be uninitialized during deserialization.
These suppressions apply to the Tm7Model serialization classes. -->
<NoWarn>CS8618;CS8625;CS8600;CS8603</NoWarn>
<!-- DCS itself triggers IL3050 warnings inside System.Private.DataContractSerialization
and ilc rolls them up into IL3053. We can't fix those from outside, but we want
them visible (not silently suppressed) so future regressions in our own code remain
catchable; just don't fail the build on them. Our DCS call sites carry targeted
UnconditionalSuppressMessage attributes for IL2026/IL3050. -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);IL3050;IL3053</WarningsNotAsErrors>
<Company>gholliday</Company>
<Product>TM7 CLI</Product>
<Authors>gholliday</Authors>
Expand Down
42 changes: 39 additions & 3 deletions src/Tm7.Cli/Tm7Model/SerializableDrawingSurfaceModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,28 @@ namespace Tm7.Cli.Model;
[DataContract(Name = "DrawingSurfaceModel", IsReference = true, Namespace = "http://schemas.datacontract.org/2004/07/ThreatModeling.Model")]
public class SerializableDrawingSurfaceModel : SerializableTaggable
{
// The public API stays as Dictionary<Guid, object> so consumers (renderer, commands, tests)
// are unaffected. DCS serialization is redirected to private list fields via
// [IgnoreDataMember] + [DataMember] + [OnSerializing]/[OnDeserialized] callbacks.
// This is required because DCS's reflection-based reader on NativeAOT cannot
// instantiate the internal System.Runtime.Serialization.KeyValue<Guid,object>
// closed generic that backs CollectionDataContract for IDictionary<,>.
[IgnoreDataMember]
public Dictionary<Guid, object> Borders { get; set; } = new();

[DataMember(Name = "Borders")]
public Dictionary<Guid, object> Borders { get; private set; }
private SerializableGuidObjectKvpList _bordersList = new();

[DataMember(Name = "Header")]
public string Header { get; private set; }

[IgnoreDataMember]
public Dictionary<Guid, object> Lines { get; set; } = new();

[DataMember(Name = "Lines")]
public Dictionary<Guid, object> Lines { get; private set; }
private SerializableGuidObjectKvpList _linesList = new();

[DataMember(Name = "Zoom", EmitDefaultValue = false)]
[DataMember(Name = "Zoom")]
public double Zoom { get; private set; }

public SerializableDrawingSurfaceModel(Guid guid, string typeId, string genericTypeId,
Expand All @@ -29,4 +41,28 @@ public SerializableDrawingSurfaceModel(Guid guid, string typeId, string genericT
Zoom = zoom;
Header = header;
}

[OnSerializing]
private void OnSerializingDictionaries(StreamingContext context)
{
_bordersList = Borders is null
? new SerializableGuidObjectKvpList()
: new SerializableGuidObjectKvpList(
Borders.Select(kvp => new SerializableGuidObjectKvp { Key = kvp.Key, Value = kvp.Value }));
_linesList = Lines is null
? new SerializableGuidObjectKvpList()
: new SerializableGuidObjectKvpList(
Lines.Select(kvp => new SerializableGuidObjectKvp { Key = kvp.Key, Value = kvp.Value }));
}

[OnDeserialized]
private void OnDeserializedDictionaries(StreamingContext context)
{
Borders = _bordersList is null
? new Dictionary<Guid, object>()
: _bordersList.Where(e => e.Value is not null).ToDictionary(e => e.Key, e => e.Value!);
Lines = _linesList is null
? new Dictionary<Guid, object>()
: _linesList.Where(e => e.Value is not null).ToDictionary(e => e.Key, e => e.Value!);
}
}
4 changes: 2 additions & 2 deletions src/Tm7.Cli/Tm7Model/SerializableElementType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ public class SerializableElementType : SerializableExtendable
[DataMember(Name = "StencilConstraints")]
public List<SerializableStencilConstraint> StencilConstraints { get; private set; }

[DataMember(Name = "StrokeDashArray", EmitDefaultValue = false)]
[DataMember(Name = "StrokeDashArray")]
public string StrokeDashArray { get; private set; }

[DataMember(Name = "StrokeThickness", EmitDefaultValue = false)]
[DataMember(Name = "StrokeThickness")]
public double StrokeThickness { get; private set; }

public SerializableElementType(bool isExtendable, string name, string id, string description,
Expand Down
71 changes: 71 additions & 0 deletions src/Tm7.Cli/Tm7Model/SerializableKvpTypes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Runtime.Serialization;

namespace Tm7.Cli.Model;

// User-defined key/value pair types that replace DataContractSerializer's internal
// System.Runtime.Serialization.KeyValue<K,V> when serializing the dictionary-shaped
// members of the threat model. The internal KeyValue<,> type cannot be statically
// instantiated for arbitrary closed generics under NativeAOT, which causes
// "missing native code or metadata" failures at runtime. By substituting our own
// item types we keep the wire format identical while making the type graph
// fully analyzable by the AOT compiler.

[DataContract(Name = "KeyValueOfguidanyType",
Namespace = "http://schemas.microsoft.com/2003/10/Serialization/Arrays")]
public class SerializableGuidObjectKvp
{
[DataMember(Name = "Key", Order = 0)]
public Guid Key { get; set; }

[DataMember(Name = "Value", Order = 1)]
public object? Value { get; set; }
}

[CollectionDataContract(Name = "ArrayOfKeyValueOfguidanyType",
Namespace = "http://schemas.microsoft.com/2003/10/Serialization/Arrays",
ItemName = "KeyValueOfguidanyType")]
public class SerializableGuidObjectKvpList : List<SerializableGuidObjectKvp>
{
public SerializableGuidObjectKvpList() { }
public SerializableGuidObjectKvpList(IEnumerable<SerializableGuidObjectKvp> items) : base(items) { }
}

[DataContract(Name = "KeyValueOfstringThreatpc_P0_PhOB",
Namespace = "http://schemas.microsoft.com/2003/10/Serialization/Arrays")]
public class SerializableStringThreatKvp
{
[DataMember(Name = "Key", Order = 0)]
public string? Key { get; set; }

[DataMember(Name = "Value", Order = 1)]
public SerializableThreat? Value { get; set; }
}

[CollectionDataContract(Name = "ArrayOfKeyValueOfstringThreatpc_P0_PhOB",
Namespace = "http://schemas.microsoft.com/2003/10/Serialization/Arrays",
ItemName = "KeyValueOfstringThreatpc_P0_PhOB")]
public class SerializableStringThreatKvpList : List<SerializableStringThreatKvp>
{
public SerializableStringThreatKvpList() { }
public SerializableStringThreatKvpList(IEnumerable<SerializableStringThreatKvp> items) : base(items) { }
}

[DataContract(Name = "KeyValueOfstringstring",
Namespace = "http://schemas.microsoft.com/2003/10/Serialization/Arrays")]
public class SerializableStringStringKvp
{
[DataMember(Name = "Key", Order = 0)]
public string? Key { get; set; }

[DataMember(Name = "Value", Order = 1)]
public string? Value { get; set; }
}

[CollectionDataContract(Name = "ArrayOfKeyValueOfstringstring",
Namespace = "http://schemas.microsoft.com/2003/10/Serialization/Arrays",
ItemName = "KeyValueOfstringstring")]
public class SerializableStringStringKvpList : List<SerializableStringStringKvp>
{
public SerializableStringStringKvpList() { }
public SerializableStringStringKvpList(IEnumerable<SerializableStringStringKvp> items) : base(items) { }
}
4 changes: 2 additions & 2 deletions src/Tm7.Cli/Tm7Model/SerializableLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ public abstract class SerializableLine : SerializableTaggable
[DataMember(Name = "SourceY")]
public int Y0 { get; private set; }

[DataMember(Name = "StrokeDashArray", EmitDefaultValue = false)]
[DataMember(Name = "StrokeDashArray")]
public string StrokeDashArray { get; private set; }

[DataMember(Name = "StrokeThickness", EmitDefaultValue = false)]
[DataMember(Name = "StrokeThickness")]
public double StrokeThickness { get; private set; }

[DataMember(Name = "TargetGuid")]
Expand Down
30 changes: 27 additions & 3 deletions src/Tm7.Cli/Tm7Model/SerializableModelData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,16 @@ public class SerializableModelData
[DataMember(Name = "Notes", Order = 4)]
public List<SerializableNote> Notes { get; private set; }

// The public API keeps Dictionary<string, SerializableThreat>; DCS sees the
// private backing list instead so AOT can fully analyze the type graph.
[IgnoreDataMember]
public Dictionary<string, SerializableThreat> AllThreatsDictionary { get; set; }

[DataMember(Name = "ThreatInstances", Order = 5)]
public Dictionary<string, SerializableThreat> AllThreatsDictionary { get; private set; }
private SerializableStringThreatKvpList _threatInstancesList = new();

[DataMember(Name = "ThreatGenerationEnabled", Order = 6)]
public bool? ThreatGenerationEnabled { get; private set; }
public bool ThreatGenerationEnabled { get; private set; }

[DataMember(Name = "Validations", Order = 7)]
public List<SerializableValidation> Validations { get; private set; }
Expand Down Expand Up @@ -47,11 +52,30 @@ public SerializableModelData(
MetaInformation = metaInformation;
Notes = notes.ToList();
AllThreatsDictionary = allThreatsDictionary;
ThreatGenerationEnabled = threatGenerationEnabled;
ThreatGenerationEnabled = threatGenerationEnabled ?? false;
Validations = validations.ToList();
Version = version ?? "4.3";
KnowledgeBase = knowledgeBase;
Profile = profile;
}

[OnSerializing]
private void OnSerializingDictionaries(StreamingContext context)
{
_threatInstancesList = AllThreatsDictionary is null
? new SerializableStringThreatKvpList()
: new SerializableStringThreatKvpList(
AllThreatsDictionary.Select(kvp => new SerializableStringThreatKvp { Key = kvp.Key, Value = kvp.Value }));
}

[OnDeserialized]
private void OnDeserializedDictionaries(StreamingContext context)
{
AllThreatsDictionary = _threatInstancesList is null
? new Dictionary<string, SerializableThreat>()
: _threatInstancesList
.Where(e => e.Key is not null && e.Value is not null)
.ToDictionary(e => e.Key!, e => e.Value!);
}

}
25 changes: 24 additions & 1 deletion src/Tm7.Cli/Tm7Model/SerializableThreat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ public class SerializableThreat
[DataMember(Name = "Priority")]
public string Priority { get; private set; }

[DataMember(Name = "Properties")]
// Public Dictionary preserved; DCS-visible backing list bypasses internal KeyValue<,>.
[IgnoreDataMember]
public Dictionary<string, string> Properties { get; set; }

[DataMember(Name = "Properties")]
private SerializableStringStringKvpList _propertiesList = new();

[DataMember(Name = "SourceGuid")]
public Guid SourceGuid { get; private set; }

Expand Down Expand Up @@ -85,4 +89,23 @@ public SerializableThreat(int id, string typeId, Guid sourceGuid, Guid targetGui
Upgraded = upgraded;
Properties = properties;
}

[OnSerializing]
private void OnSerializingDictionaries(StreamingContext context)
{
_propertiesList = Properties is null
? new SerializableStringStringKvpList()
: new SerializableStringStringKvpList(
Properties.Select(kvp => new SerializableStringStringKvp { Key = kvp.Key, Value = kvp.Value }));
}

[OnDeserialized]
private void OnDeserializedDictionaries(StreamingContext context)
{
Properties = _propertiesList is null
? new Dictionary<string, string>()
: _propertiesList
.Where(e => e.Key is not null && e.Value is not null)
.ToDictionary(e => e.Key!, e => e.Value!);
}
}
30 changes: 30 additions & 0 deletions src/Tm7.Cli/Tm7XmlSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ namespace Tm7.Cli;
/// </summary>
public static class Tm7XmlSerializer
{
// Force AOT to keep concrete instantiations of Dictionary<,> and its EqualityComparer<>
// dependencies that DataContractSerializer's reflection reader will demand via ISerializable.
private static readonly object?[] _aotKeepAlive =
[
EqualityComparer<Guid>.Default,
EqualityComparer<string>.Default,
new Dictionary<Guid, object>(),
new Dictionary<string, SerializableThreat>(),
new Dictionary<string, string>(),
];

private static readonly Type[] KnownTypes =
[
typeof(List<SerializableDrawingSurfaceModel>),
Expand Down Expand Up @@ -94,6 +105,25 @@ public static class Tm7XmlSerializer
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(SerializableAttributeValues))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(SerializableAvailableToBaseModels))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(SerializableKbVersion))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(SerializableGuidObjectKvp))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(SerializableGuidObjectKvpList))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(SerializableStringThreatKvp))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(SerializableStringThreatKvpList))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(SerializableStringStringKvp))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(SerializableStringStringKvpList))]
// The serializable model classes no longer expose Dictionary<,> as [DataMember];
// those properties are [IgnoreDataMember] and backed by [DataMember] List<Kvp> fields
// (see SerializableKvpTypes.cs) so DCS never instantiates the BCL-internal
// System.Runtime.Serialization.KeyValue<K,V> closed generics that AOT cannot generate
// code for. The Dictionary<,> / EqualityComparer<,> preservation below is defensive:
// if a future change re-introduces a [DataMember] Dictionary<,>, DCS will fall back
// to the ISerializable path and require Dictionary<,>.(SerializationInfo,
// StreamingContext) + the relevant EqualityComparer<T> to exist at runtime.
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Dictionary<Guid, object>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Dictionary<string, SerializableThreat>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Dictionary<string, string>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(EqualityComparer<Guid>))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(EqualityComparer<string>))]
private static DataContractSerializer CreateSerializer()
{
return new DataContractSerializer(typeof(SerializableModelData), KnownTypes);
Expand Down
24 changes: 24 additions & 0 deletions src/Tm7.Cli/TrimmerRoots.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@
<type fullname="Tm7.Cli.Model.SerializableAvailableToBaseModels" preserve="all" />
<type fullname="Tm7.Cli.Model.SerializableKbVersion" preserve="all" />

<!-- Custom KVP wrapper types replacing DCS's internal KeyValue<,> for AOT compat -->
<type fullname="Tm7.Cli.Model.SerializableGuidObjectKvp" preserve="all" />
<type fullname="Tm7.Cli.Model.SerializableGuidObjectKvpList" preserve="all" />
<type fullname="Tm7.Cli.Model.SerializableStringThreatKvp" preserve="all" />
<type fullname="Tm7.Cli.Model.SerializableStringThreatKvpList" preserve="all" />
<type fullname="Tm7.Cli.Model.SerializableStringStringKvp" preserve="all" />
<type fullname="Tm7.Cli.Model.SerializableStringStringKvpList" preserve="all" />

<!-- Enums used by DataContract members -->
<type fullname="Tm7.Cli.Model.AttributeType" preserve="all" />
<type fullname="Tm7.Cli.Model.AttributeInheritance" preserve="all" />
Expand All @@ -69,4 +77,20 @@
<type fullname="Tm7.Cli.Model.StencilConnectionPort" preserve="all" />
<type fullname="Tm7.Cli.Model.ElementVisualRepresentation" preserve="all" />
</assembly>

<!--
Defensive preservation of Dictionary<,> and EqualityComparer<,>: the model classes
no longer expose Dictionary<,> as [DataMember] (they use [IgnoreDataMember] + a
List<Kvp> backing field — see SerializableKvpTypes.cs and OnSerializing callbacks),
so DCS will not normally walk these types. Kept as a safety net in case a future
[DataMember] Dictionary<,> regression sends DCS down the ISerializable fallback.
-->
<assembly fullname="System.Private.CoreLib">
<type fullname="System.Collections.Generic.Dictionary`2" preserve="all" />
<type fullname="System.Collections.Generic.EqualityComparer`1" preserve="all" />
<type fullname="System.Collections.Generic.GenericEqualityComparer`1" preserve="all" />
<type fullname="System.Collections.Generic.ObjectEqualityComparer`1" preserve="all" />
<type fullname="System.Collections.Generic.NonRandomizedStringEqualityComparer" preserve="all" />
<type fullname="System.Collections.Generic.KeyValuePair`2" preserve="all" />
</assembly>
</linker>
Loading
Loading