Skip to content
Draft
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
7 changes: 7 additions & 0 deletions Common/CommonJsonContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using System.Text.Json.Serialization;

namespace Common;

[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.Unspecified)]
[JsonSerializable(typeof(VersionCheckBody))]
internal partial class CommonJsonContext : JsonSerializerContext { }
3 changes: 3 additions & 0 deletions Common/JsonFile.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;

Expand All @@ -14,6 +15,7 @@ public static class JsonFile
AllowTrailingCommas = true
};

[RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")]
public static async Task SerializeToFileAsync<T>(T obj, string filePath, JsonSerializerOptions? options = null)
{
await using (var fileStream = new FileStream(
Expand All @@ -28,6 +30,7 @@ public static async Task SerializeToFileAsync<T>(T obj, string filePath, JsonSer
}
}

[RequiresUnreferencedCode("JSON serialization and deserialization might require types that cannot be statically analyzed.")]
public static async Task<T?> DeserializeFromFileAsync<T>(string filePath, JsonSerializerOptions? options = null)
{
await using (var fileStream = new FileStream(
Expand Down
2 changes: 2 additions & 0 deletions Common/Logging/Logger.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

namespace Common.Logging;

Expand All @@ -10,6 +11,7 @@ public class Logger : ILogger

public event EventHandler<LogAddedEventArgs>? LogAdded;

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "StackFrame.GetMethod() is used only for diagnostic logging of the caller's class name and is safe to call even if metadata is trimmed.")]
public void Log(LogLevel level, string message, string callerMemberName = "", string sourceFilePath = "", int sourceLineNumber = 0)
{
// Get the class name using reflection
Expand Down
3 changes: 3 additions & 0 deletions Common/Logging/ReflectionLogger.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text;

namespace Common.Logging;

public static class ReflectionLogger
{
[RequiresUnreferencedCode("ReflectionLogger uses reflection to enumerate public properties and fields of objects, which may be trimmed.")]
public static string ToString<T>(T obj)
{
var sb = new StringBuilder();
return ToString(obj, sb).ToString();
}

[RequiresUnreferencedCode("ReflectionLogger uses reflection to enumerate public properties and fields of objects, which may be trimmed.")]
static StringBuilder ToString<T>(T obj, StringBuilder sb)
{
if (obj == null)
Expand Down
2 changes: 1 addition & 1 deletion Common/VersionHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public static SemanticVersion GetLatestAppVersion(string productName, SemanticVe
if (response.IsSuccessStatusCode)
{
var jsonResponse = response.Content.ReadAsStringAsync().Result;
var body = JsonSerializer.Deserialize<VersionCheckBody>(jsonResponse);
var body = JsonSerializer.Deserialize(jsonResponse, CommonJsonContext.Default.VersionCheckBody);
var versionText = body?.TagName;
return GetVersionFromText(versionText);
}
Expand Down
7 changes: 4 additions & 3 deletions Dat/FileParsing/AttributeHelper.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;

namespace Dat.FileParsing;
Expand All @@ -16,15 +17,15 @@ public static class AttributeHelper
return attributes.Length == 1 ? attributes[0] as T : null;
}

public static T? Get<T>(Type t) where T : Attribute
public static T? Get<T>([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type t) where T : Attribute
=> t.GetCustomAttribute<T>(inherit: false);

public static bool Has<T>(PropertyInfo p) where T : Attribute
=> p.GetCustomAttribute<T>(inherit: false) is not null;

public static bool Has<T>(Type t) where T : Attribute
public static bool Has<T>([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type t) where T : Attribute
=> t.GetCustomAttribute<T>(inherit: false) is not null;

public static IEnumerable<PropertyInfo> GetAllPropertiesWithAttribute<T>(Type t) where T : Attribute
public static IEnumerable<PropertyInfo> GetAllPropertiesWithAttribute<T>([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type t) where T : Attribute
=> t.GetProperties().Where(Has<T>);
}
3 changes: 2 additions & 1 deletion Dat/FileParsing/ByteHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;

namespace Dat.FileParsing;

public static class ByteHelpers
{
public static int GetObjectSize(Type type)
public static int GetObjectSize([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type)
{
var size = 0;
if (type == typeof(byte) || type == typeof(sbyte))
Expand Down
12 changes: 9 additions & 3 deletions Dat/FileParsing/ByteReader.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using Dat.Types;
using Definitions.ObjectModels;
using System.Diagnostics.CodeAnalysis;

namespace Dat.FileParsing;

public static class ByteReader
{
public static object ReadT(ReadOnlySpan<byte> data, Type t, int offset, int arrLength = 0, bool isVariableLoad = false)
[RequiresUnreferencedCode("ReadT uses reflection-based type handling and may call ReadLocoStruct which requires unreferenced code.")]
public static object ReadT(ReadOnlySpan<byte> data, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type t, int offset, int arrLength = 0, bool isVariableLoad = false)
{
if (t == typeof(uint8_t))
{
Expand Down Expand Up @@ -178,10 +180,12 @@ static object ReadEnum(ReadOnlySpan<byte> data, Type t, int offset)
}
}

[RequiresUnreferencedCode("ReadLocoStruct uses reflection to read public properties and invoke constructors of the target type, which may be trimmed.")]
public static T ReadLocoStruct<T>(ReadOnlySpan<byte> data) where T : class
=> (T)ReadLocoStruct(data, typeof(T));

public static ILocoStruct ReadLocoStruct(ReadOnlySpan<byte> data, Type t)
[RequiresUnreferencedCode("ReadLocoStruct uses reflection to read public properties and invoke constructors of the target type, which may be trimmed.")]
public static ILocoStruct ReadLocoStruct(ReadOnlySpan<byte> data, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicConstructors)] Type t)
{
var properties = t.GetProperties();
var args = new List<object>();
Expand Down Expand Up @@ -258,7 +262,8 @@ public static ILocoStruct ReadLocoStruct(ReadOnlySpan<byte> data, Type t)
return (ILocoStruct?)Activator.CreateInstance(t, [.. args]) ?? throw new InvalidDataException("couldn't parse");
}

public static IList<ILocoStruct> ReadLocoStructArray(ReadOnlySpan<byte> data, Type t, int count, int structSize) // could get struct size from attribute, but easier just to pass in
[RequiresUnreferencedCode("ReadLocoStructArray uses reflection to read public properties and invoke constructors of the target type, which may be trimmed.")]
public static IList<ILocoStruct> ReadLocoStructArray(ReadOnlySpan<byte> data, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicConstructors)] Type t, int count, int structSize) // could get struct size from attribute, but easier just to pass in
{
// cannot use ReadOnlySpan with yield return :|
var list = new List<ILocoStruct>();
Expand All @@ -285,6 +290,7 @@ public static IList<ILocoStruct> ReadLocoStructArray(ReadOnlySpan<byte> data, Ty
// return list;
//}

[RequiresUnreferencedCode("ReadLocoStructArray uses reflection to read public properties and invoke constructors of the target type, which may be trimmed.")]
public static IList<T> ReadLocoStructArray<T>(ReadOnlySpan<byte> data, int count, int structSize) where T : class
{
// cannot use ReadOnlySpan with yield return :|
Expand Down
5 changes: 4 additions & 1 deletion Dat/FileParsing/ByteWriter.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using Definitions.ObjectModels;
using System.Diagnostics.CodeAnalysis;

namespace Dat.FileParsing;

public static class ByteWriter
{
public static void WriteT(Span<byte> data, Type t, int offset, object val)
[RequiresUnreferencedCode("WriteT uses reflection-based type handling and may call WriteLocoStruct which requires unreferenced code.")]
public static void WriteT(Span<byte> data, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type t, int offset, object val)
{
if (t == typeof(uint8_t))
{
Expand Down Expand Up @@ -76,6 +78,7 @@ public static void WriteT(Span<byte> data, Type t, int offset, object val)
}
}

[RequiresUnreferencedCode("WriteLocoStruct uses reflection to enumerate public properties of the object type, which may be trimmed.")]
public static ReadOnlySpan<byte> WriteLocoStruct(ILocoStruct obj)
{
ArgumentNullException.ThrowIfNull(obj);
Expand Down
3 changes: 2 additions & 1 deletion Dat/FileParsing/ObjectAttributes.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using Dat.Data;
using Definitions.ObjectModels;
using System.Diagnostics.CodeAnalysis;

namespace Dat.FileParsing;

public static class ObjectAttributes
{
public static int StructSize<T>() where T : ILocoStruct
public static int StructSize<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() where T : ILocoStruct
=> AttributeHelper.Get<LocoStructSizeAttribute>(typeof(T))!.Size;

//public static ObjectType ObjectType<T>() // where T : ILocoStruct
Expand Down
7 changes: 7 additions & 0 deletions Dat/FileParsing/SawyerStreamReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Definitions.ObjectModels.Graphics;
using Definitions.ObjectModels.Objects.Sound;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Text;

namespace Dat.FileParsing;
Expand Down Expand Up @@ -94,18 +95,21 @@ public static bool TryGetHeadersFromBytes(Stream stream, out (S5Header S5, Objec
return true;
}

[RequiresUnreferencedCode("LoadFullObject calls ValidateLocoStruct which uses DataAnnotations validation requiring reflection.")]
public static (DatHeaderInfo DatFileInfo, LocoObject? LocoObject) LoadFullObject(string filename, ILogger logger, bool loadExtra = true)
{
using var fs = new FileStream(filename, FileMode.Open);
return LoadFullObject(fs, logger, filename, loadExtra);
}

[RequiresUnreferencedCode("LoadFullObject calls ValidateLocoStruct which uses DataAnnotations validation requiring reflection.")]
public static (DatHeaderInfo DatFileInfo, LocoObject? LocoObject) LoadFullObject(byte[] data, ILogger logger, string filename = "<in-memory>", bool loadExtra = true)
{
using var ms = new MemoryStream(data);
return LoadFullObject(ms, logger, filename: filename, loadExtra: loadExtra);
}

[RequiresUnreferencedCode("LoadFullObject calls ValidateLocoStruct which uses DataAnnotations validation requiring reflection.")]
public static (DatHeaderInfo DatFileInfo, LocoObject? LocoObject) LoadFullObject(Stream stream, ILogger logger, string filename, bool loadExtra = true)
{
logger.Info($"Full-loading \"{filename}\" with loadExtra={loadExtra}");
Expand Down Expand Up @@ -135,6 +139,7 @@ public static (DatHeaderInfo DatFileInfo, LocoObject? LocoObject) LoadFullObject
}
}

[RequiresUnreferencedCode("ValidateLocoStruct uses DataAnnotations validation which requires reflection and may not work correctly when trimming application code.")]
static void ValidateLocoStruct(S5Header s5Header, ILocoStruct locoStruct, ILogger? logger)
{
var warnings = new List<string>();
Expand Down Expand Up @@ -240,6 +245,7 @@ public static StringTable ReadStringTableStream(Stream stream, string[] stringNa
}
}

[RequiresUnreferencedCode("ReadImageTable calls ReadLocoStruct which uses reflection-based parsing requiring unreferenced code.")]
public static (G1Header Header, List<GraphicsElement> Table) ReadImageTable(LocoBinaryReader br)
{
if (br.BaseStream.Length - br.BaseStream.Position < ObjectAttributes.StructSize<G1Header>())
Expand Down Expand Up @@ -416,6 +422,7 @@ public static byte[] Decode(SawyerEncoding encoding, ReadOnlySpan<byte> data, in
_ => throw new InvalidDataException("Unknown chunk encoding scheme"),
};

[RequiresUnreferencedCode("ReadChunk calls ReadLocoStruct which uses reflection-based parsing requiring unreferenced code.")]
public static T ReadChunk<T>(ref ReadOnlySpan<byte> data) where T : class
=> ByteReader.ReadLocoStruct<T>(ReadChunkCore(ref data));

Expand Down
5 changes: 5 additions & 0 deletions Dat/FileParsing/SawyerStreamWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Definitions.ObjectModels.Objects.Sound;
using Definitions.ObjectModels.Types;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Text;

namespace Dat.FileParsing;
Expand Down Expand Up @@ -81,6 +82,7 @@ public static byte[] SaveSoundEffectsToCSS(List<(SoundEffectWaveFormat locoWaveH
}
}

[RequiresUnreferencedCode("SaveMusicToDat calls WriteLocoStruct which uses reflection-based serialization requiring unreferenced code.")]
public static byte[] SaveMusicToDat(DatMusicWaveFormat header, byte[] data)
{
using (var ms = new MemoryStream())
Expand All @@ -96,6 +98,7 @@ public static byte[] SaveMusicToDat(DatMusicWaveFormat header, byte[] data)
}
}

[RequiresUnreferencedCode("Save calls WriteLocoObject which uses DataAnnotations validation and WriteLocoStruct requiring reflection.")]
public static void Save(string filename, string objName, ObjectSource objectSource, SawyerEncoding encoding, LocoObject locoObject, ILogger logger, bool allowWritingAsVanilla)
{
ArgumentNullException.ThrowIfNull(locoObject);
Expand Down Expand Up @@ -369,6 +372,7 @@ public static byte[] EncodeRLEImageData(DatG1ElementFlags flags, byte[] imageDat
return ms.ToArray();
}

[RequiresUnreferencedCode("WriteChunk calls WriteLocoStruct which uses reflection-based serialization requiring unreferenced code.")]
public static ReadOnlySpan<byte> WriteChunk(ILocoStruct str, SawyerEncoding encoding)
=> WriteChunkCore(ByteWriter.WriteLocoStruct(str), encoding);

Expand Down Expand Up @@ -490,6 +494,7 @@ public static void WriteLocoObject(Stream stream, LocoObject obj)
}
}

[RequiresUnreferencedCode("WriteLocoObject uses DataAnnotations validation (Validator.TryValidateObject) which requires reflection and may not work correctly when trimming application code.")]
public static MemoryStream WriteLocoObject(string objName, ObjectType objectType, ObjectSource objectSource, SawyerEncoding encoding, ILogger logger, LocoObject obj, bool allowWritingAsVanilla)
{
var validationResults = new List<ValidationResult>();
Expand Down
3 changes: 3 additions & 0 deletions Dat/Loaders/SoundObjectLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Definitions.ObjectModels.Types;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;

namespace Dat.Loaders;

Expand Down Expand Up @@ -160,6 +161,7 @@ internal record DatSoundObject(
uint pcmDataLength;
byte[] unkData;

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "LoadVariable calls ReadLocoStruct which uses reflection. The required types (DatSoundObjectData) are always statically referenced and preserved.")]
public ReadOnlySpan<byte> LoadVariable(ReadOnlySpan<byte> remainingData)
{
// unknown structs
Expand All @@ -183,6 +185,7 @@ public ReadOnlySpan<byte> LoadVariable(ReadOnlySpan<byte> remainingData)
return remainingData[remainingData.Length..];
}

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "SaveVariable calls WriteLocoStruct which uses reflection. The required types (DatSoundObjectData) are always statically referenced and preserved.")]
public ReadOnlySpan<byte> SaveVariable()
{
using (var ms = new MemoryStream())
Expand Down
2 changes: 2 additions & 0 deletions Definitions/ObjectModels/Objects/Building/BuildingObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ public static class Constants

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
#pragma warning disable IL2026 // ValidationContext constructor uses reflection; this usage is safe as the target types are always present at runtime.
var bcValidationContext = new ValidationContext(BuildingComponents);
#pragma warning restore IL2026
foreach (var result in BuildingComponents.Validate(bcValidationContext))
{
yield return result;
Expand Down
3 changes: 3 additions & 0 deletions Definitions/ObjectModels/Objects/Common/BuildingComponents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
using Definitions.ObjectModels.Validation;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;

namespace Definitions.ObjectModels.Objects.Common;

[TypeConverter(typeof(ExpandableObjectConverter))]
public class BuildingComponents : ILocoStruct
{
#pragma warning disable IL2026 // LengthAttribute constructor uses reflection to get 'Count' on non-ICollection types; our properties use List<T> which implements ICollection so Count is preserved.
[Length(1, BuildingObject.Constants.MaxAnimationsCount)]
[CountEqualTo(nameof(BuildingAnimations))]
public List<uint8_t> BuildingHeights { get; set; } = [];
Expand All @@ -18,6 +20,7 @@ public class BuildingComponents : ILocoStruct

[Length(1, BuildingObject.Constants.MaxVariationsCount)]
public List<List<uint8_t>> BuildingVariations { get; set; } = [];
#pragma warning restore IL2026

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
Expand Down
6 changes: 6 additions & 0 deletions Definitions/ObjectModels/Objects/Industry/IndustryObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ public class IndustryObject : ILocoStruct, IHasBuildingComponents
{
public uint32_t FarmImagesPerGrowthStage { get; set; }
public BuildingComponents BuildingComponents { get; set; } = new();
#pragma warning disable IL2026 // LengthAttribute constructor uses reflection to get 'Count' on non-ICollection types; our properties use List<T> which implements ICollection so Count is preserved.
[Length(4, 4)]
public List<List<uint8_t>> AnimationSequences { get; set; } = []; // Access with getAnimationSequence helper method
#pragma warning restore IL2026
public List<IndustryObjectUnk38> var_38 { get; set; } = []; // Access with getUnk38 helper method
public uint8_t MinNumBuildings { get; set; }
public uint8_t MaxNumBuildings { get; set; }
Expand All @@ -25,6 +27,7 @@ public class IndustryObject : ILocoStruct, IHasBuildingComponents
public uint8_t ScaffoldingSegmentType { get; set; }
public Colour ScaffoldingColour { get; set; }

#pragma warning disable IL2026 // LengthAttribute constructor uses reflection to get 'Count' on non-ICollection types; our properties use List<T> which implements ICollection so Count is preserved.
[Length(2, 2)]
public List<IndustryObjectProductionRateRange> InitialProductionRate { get; set; } = []; // 2 entries, min and max production rate

Expand All @@ -33,6 +36,7 @@ public class IndustryObject : ILocoStruct, IHasBuildingComponents

[Length(3, 3)]
public List<ObjectModelHeader> RequiredCargo { get; set; } = []; // Cargo required by this industry
#pragma warning restore IL2026

public Colour MapColour { get; set; }
public IndustryObjectFlags Flags { get; set; }
Expand All @@ -50,7 +54,9 @@ public class IndustryObject : ILocoStruct, IHasBuildingComponents

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
#pragma warning disable IL2026 // ValidationContext constructor uses reflection; this usage is safe as the target types are always present at runtime.
var bcValidationContext = new ValidationContext(BuildingComponents);
#pragma warning restore IL2026
foreach (var result in BuildingComponents.Validate(bcValidationContext))
{
yield return result;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;

namespace Definitions.ObjectModels.Objects.Scaffolding;

public class ScaffoldingObject : ILocoStruct
{
#pragma warning disable IL2026 // LengthAttribute constructor uses reflection to get 'Count' on non-ICollection types; our properties use List<T> which implements ICollection so Count is preserved.
[Length(3, 3)]
public List<uint16_t> SegmentHeights { get; set; } = [];

[Length(3, 3)]
public List<uint16_t> RoofHeights { get; set; } = [];
#pragma warning restore IL2026

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
=> [];
Expand Down
Loading