From d63fcd5507f0d0fd48715e6200c85f67b73b23a7 Mon Sep 17 00:00:00 2001 From: RejectKid Date: Mon, 13 Jul 2026 13:35:10 -0400 Subject: [PATCH 1/3] Correct DIS variable and conditional wire layouts --- src/OpenDisNet/Binary/DisBinaryReader.cs | 6 + src/OpenDisNet/Binary/DisHeaderCodec.cs | 28 +- src/OpenDisNet/DisSerializer.cs | 19 +- src/OpenDisNet/Internal/PduCodec.cs | 265 +++++++++++++----- .../Pdus/Families/DistributedEmissionsPdus.cs | 12 +- src/OpenDisNet/Pdus/Families/Records.cs | 5 - src/OpenDisNet/Pdus/PduRegistry.cs | 2 +- src/OpenDisNet/Protocol/DisHeader.cs | 6 +- tools/OpenDisNet.Generator/CodecWriter.cs | 147 ++++++++-- tools/OpenDisNet.Generator/DisSchemaLoader.cs | 3 +- tools/OpenDisNet.Generator/ModelWriter.cs | 4 +- .../Schemas/DIS7/DIS_7_2012.xml | 4 +- .../Schemas/DIS7/LogisticsFamilyPdus.xml | 4 +- 13 files changed, 372 insertions(+), 133 deletions(-) diff --git a/src/OpenDisNet/Binary/DisBinaryReader.cs b/src/OpenDisNet/Binary/DisBinaryReader.cs index 0707641..734a599 100644 --- a/src/OpenDisNet/Binary/DisBinaryReader.cs +++ b/src/OpenDisNet/Binary/DisBinaryReader.cs @@ -43,6 +43,12 @@ public uint ReadUInt32(string field = "value") return value; } + public readonly uint PeekUInt32(string field = "value") + { + Ensure(4, field); + return BinaryPrimitives.ReadUInt32BigEndian(_source[_offset..]); + } + public int ReadInt32(string field = "value") => unchecked((int)ReadUInt32(field)); public ulong ReadUInt64(string field = "value") diff --git a/src/OpenDisNet/Binary/DisHeaderCodec.cs b/src/OpenDisNet/Binary/DisHeaderCodec.cs index 83d1235..002aa28 100644 --- a/src/OpenDisNet/Binary/DisHeaderCodec.cs +++ b/src/OpenDisNet/Binary/DisHeaderCodec.cs @@ -4,15 +4,18 @@ namespace OpenDisNet.Binary; public static class DisHeaderCodec { - public static DisHeader Read(ref DisBinaryReader reader) => new( - (DisProtocolVersion)reader.ReadByte(nameof(DisHeader.ProtocolVersion)), - reader.ReadByte(nameof(DisHeader.ExerciseId)), - (PduType)reader.ReadByte(nameof(DisHeader.PduType)), - (ProtocolFamily)reader.ReadByte(nameof(DisHeader.ProtocolFamily)), - reader.ReadUInt32(nameof(DisHeader.Timestamp)), - reader.ReadUInt16(nameof(DisHeader.Length)), - reader.ReadByte(nameof(DisHeader.PduStatus)), - reader.ReadByte(nameof(DisHeader.Padding))); + public static DisHeader Read(ref DisBinaryReader reader) + { + var version = (DisProtocolVersion)reader.ReadByte(nameof(DisHeader.ProtocolVersion)); + byte exerciseId = reader.ReadByte(nameof(DisHeader.ExerciseId)); + var pduType = (PduType)reader.ReadByte(nameof(DisHeader.PduType)); + var family = (ProtocolFamily)reader.ReadByte(nameof(DisHeader.ProtocolFamily)); + uint timestamp = reader.ReadUInt32(nameof(DisHeader.Timestamp)); + ushort length = reader.ReadUInt16(nameof(DisHeader.Length)); + byte status = family == ProtocolFamily.LiveEntity ? (byte)0 : reader.ReadByte(nameof(DisHeader.PduStatus)); + byte padding = family == ProtocolFamily.LiveEntity ? (byte)0 : reader.ReadByte(nameof(DisHeader.Padding)); + return new(version, exerciseId, pduType, family, timestamp, length, status, padding); + } public static void Write(ref DisBinaryWriter writer, in DisHeader header) { @@ -22,7 +25,10 @@ public static void Write(ref DisBinaryWriter writer, in DisHeader header) writer.WriteByte((byte)header.ProtocolFamily, nameof(header.ProtocolFamily)); writer.WriteUInt32(header.Timestamp, nameof(header.Timestamp)); writer.WriteUInt16(header.Length, nameof(header.Length)); - writer.WriteByte(header.PduStatus, nameof(header.PduStatus)); - writer.WriteByte(header.Padding, nameof(header.Padding)); + if (header.ProtocolFamily != ProtocolFamily.LiveEntity) + { + writer.WriteByte(header.PduStatus, nameof(header.PduStatus)); + writer.WriteByte(header.Padding, nameof(header.Padding)); + } } } diff --git a/src/OpenDisNet/DisSerializer.cs b/src/OpenDisNet/DisSerializer.cs index 981c13c..975f7a3 100644 --- a/src/OpenDisNet/DisSerializer.cs +++ b/src/OpenDisNet/DisSerializer.cs @@ -51,24 +51,31 @@ public static bool TryDeserialize( pdu = null; error = default; - if (datagram.Length < DisHeader.Size) - return Fail(DisParseErrorCode.TruncatedHeader, "A DIS header requires 12 bytes.", datagram.Length, out error); + if (datagram.Length < 4) + return Fail(DisParseErrorCode.TruncatedHeader, "A DIS header requires at least 4 bytes to identify its layout.", datagram.Length, out error); + + int requiredHeaderSize = datagram[3] == (byte)ProtocolFamily.LiveEntity + ? DisHeader.MinimumSize + : DisHeader.Size; + if (datagram.Length < requiredHeaderSize) + return Fail(DisParseErrorCode.TruncatedHeader, $"This DIS header requires {requiredHeaderSize} bytes.", datagram.Length, out error); try { var reader = new DisBinaryReader(datagram); DisHeader header = DisHeaderCodec.Read(ref reader); + int headerSize = header.EncodedSize; if (options.RequireVersion7 && header.ProtocolVersion != DisProtocolVersion.Ieee1278_1_2012) return Fail(DisParseErrorCode.UnsupportedProtocolVersion, $"Expected DIS protocol version 7; received {(byte)header.ProtocolVersion}.", 0, out error); - if (header.Length < DisHeader.Size || header.Length > options.MaximumPduLength) + if (header.Length < headerSize || header.Length > options.MaximumPduLength) return Fail(DisParseErrorCode.InvalidLength, $"Invalid PDU length {header.Length}.", 8, out error); if (header.Length > datagram.Length) return Fail(DisParseErrorCode.TruncatedPdu, $"The header declares {header.Length} bytes; only {datagram.Length} were received.", datagram.Length, out error); if (options.RequireExactDatagramLength && header.Length != datagram.Length) return Fail(DisParseErrorCode.TrailingData, $"The datagram contains {datagram.Length - header.Length} trailing bytes.", header.Length, out error); - pdu = PduRegistry.Parse(header, datagram.Slice(DisHeader.Size, header.Length - DisHeader.Size)); + pdu = PduRegistry.Parse(header, datagram.Slice(headerSize, header.Length - headerSize)); return true; } catch (DisParseException exception) @@ -77,11 +84,11 @@ public static bool TryDeserialize( } catch (FormatException exception) { - return Fail(DisParseErrorCode.InvalidField, exception.Message, DisHeader.Size, out error); + return Fail(DisParseErrorCode.InvalidField, exception.Message, DisHeader.MinimumSize, out error); } catch (OverflowException exception) { - return Fail(DisParseErrorCode.InvalidField, exception.Message, DisHeader.Size, out error); + return Fail(DisParseErrorCode.InvalidField, exception.Message, DisHeader.MinimumSize, out error); } } diff --git a/src/OpenDisNet/Internal/PduCodec.cs b/src/OpenDisNet/Internal/PduCodec.cs index 68eeaea..ed6d3b4 100644 --- a/src/OpenDisNet/Internal/PduCodec.cs +++ b/src/OpenDisNet/Internal/PduCodec.cs @@ -11,7 +11,7 @@ public static Pdu Read(DisHeader header, ReadOnlySpan body) { Pdu value = PduFactory.Create(header.PduType, header.ExerciseId); ApplyHeader(value, header); - var reader = new DisBinaryReader(body, DisHeader.Size); + var reader = new DisBinaryReader(body, header.EncodedSize); switch ((byte)header.PduType) { case 1: @@ -312,7 +312,7 @@ public static int GetEncodedLength(Pdu value) { ArgumentNullException.ThrowIfNull(value); Prepare(value); - int offset = DisHeader.Size; + int offset = value.Header.EncodedSize; MeasureBody(value, ref offset); return offset; } @@ -1240,6 +1240,9 @@ private static void ApplyHeader(Pdu value, DisHeader header) } } + private static bool IsIffTransponder(IffSystemType value) => (ushort)value is 1 or 3 or 5 or 9 or 10 or 11; + private static bool IsIffInterrogator(IffSystemType value) => (ushort)value is 2 or 4 or 6 or 7 or 8; + private static APA ReadAPA(ref DisBinaryReader reader) { var value = new APA(); @@ -2153,7 +2156,7 @@ private static void WriteAppearancePduFields(ref DisBinaryWriter writer, Appeara WriteEntityType(ref writer, value.EntityType); WriteEntityType(ref writer, value.AlternateEntityType); WriteEntityMarking(ref writer, value.EntityMarking); - writer.WriteUInt32(value.Capabilities.Value, "capabilities"); + writer.WriteUInt32((uint)value.Capabilities.Value, "capabilities"); WriteAppearance(ref writer, value.AppearanceFields); } @@ -2175,7 +2178,7 @@ private static void ReadArealObjectStatePduFields(ref DisBinaryReader reader, Ar value.ReferencedObjectId = ReadObjectIdentifier(ref reader); value.UpdateNumber = reader.ReadUInt16("updateNumber"); value.ForceId = (ForceId)reader.ReadByte("forceID"); - value.Modifications = new ObjectStateModificationArealObject(reader.ReadUInt16("modifications")); + value.Modifications = new ObjectStateModificationArealObject(reader.ReadByte("modifications")); value.ObjectType = ReadObjectType(ref reader); value.SpecificObjectAppearance = reader.ReadUInt32("specificObjectAppearance"); value.GeneralObjectAppearance = reader.ReadUInt16("generalObjectAppearance"); @@ -2211,7 +2214,7 @@ private static void WriteArealObjectStatePduFields(ref DisBinaryWriter writer, A WriteObjectIdentifier(ref writer, value.ReferencedObjectId); writer.WriteUInt16(value.UpdateNumber, "updateNumber"); writer.WriteByte((byte)value.ForceId, "forceID"); - writer.WriteUInt16(value.Modifications.Value, "modifications"); + writer.WriteByte((byte)value.Modifications.Value, "modifications"); WriteObjectType(ref writer, value.ObjectType); writer.WriteUInt32(value.SpecificObjectAppearance, "specificObjectAppearance"); writer.WriteUInt16(value.GeneralObjectAppearance, "generalObjectAppearance"); @@ -2227,7 +2230,7 @@ private static void MeasureArealObjectStatePduFields(in ArealObjectStatePdu valu MeasureObjectIdentifier(value.ReferencedObjectId, ref offset); offset += 2; offset += 1; - offset += 2; + offset += 1; MeasureObjectType(value.ObjectType, ref offset); offset += 4; offset += 2; @@ -3886,7 +3889,7 @@ private static void WriteDirectedEnergyDamageFields(ref DisBinaryWriter writer, writer.WriteSingle(value.Temperature, "temperature"); writer.WriteByte((byte)value.ComponentIdentification, "componentIdentification"); writer.WriteByte((byte)value.ComponentDamageStatus, "componentDamageStatus"); - writer.WriteByte(value.ComponentVisualDamageStatus.Value, "componentVisualDamageStatus"); + writer.WriteByte((byte)value.ComponentVisualDamageStatus.Value, "componentVisualDamageStatus"); writer.WriteByte((byte)value.ComponentVisualSmokeColor, "componentVisualSmokeColor"); WriteEventIdentifier(ref writer, value.FireEventId); writer.WriteUInt16(value.Padding2, "padding2"); @@ -3968,7 +3971,7 @@ private static void WriteDirectedEnergyFirePduFields(ref DisBinaryWriter writer, writer.WriteUInt32(value.Pad1, "pad1"); writer.WriteSingle(value.PulseRepititionFrequency, "pulseRepititionFrequency"); writer.WriteSingle(value.PulseWidth, "pulseWidth"); - writer.WriteUInt16(value.Flags.Value, "flags"); + writer.WriteUInt16((ushort)value.Flags.Value, "flags"); writer.WriteByte((byte)value.PulseShape, "pulseShape"); writer.WriteByte(value.Pad2, "pad2"); writer.WriteUInt32(value.Pad3, "pad3"); @@ -4943,7 +4946,7 @@ private static void WriteEntityStatePduFields(ref DisBinaryWriter writer, Entity writer.WriteUInt32(value.EntityAppearance, "entityAppearance"); WriteDeadReckoningParameters(ref writer, value.DeadReckoningParameters); WriteEntityMarking(ref writer, value.Marking); - writer.WriteUInt32(value.Capabilities.Value, "capabilities"); + writer.WriteUInt32((uint)value.Capabilities.Value, "capabilities"); foreach (VariableParameter item in value.VariableParameters) WriteVariableParameter(ref writer, item); } @@ -5206,7 +5209,7 @@ private static void ReadEnvironmentFields(ref DisBinaryReader reader, Environmen value.Length = reader.ReadUInt16("length"); value.Index = reader.ReadByte("index"); value.Padding1 = reader.ReadByte("padding1"); - int GeometryCount = CheckedCount(Math.Max(0, ((checked((int)value.Length) + 7) / 8) - 6), reader.Remaining, "geometry"); + int GeometryCount = CheckedCount(Math.Max(0, ((checked((int)value.Length) + 7) / 8) - 8), reader.Remaining, "geometry"); value.Geometry = new byte[GeometryCount]; for (int index = 0; index < GeometryCount; index++) value.Geometry[index] = reader.ReadByte("geometry"); @@ -5220,6 +5223,7 @@ private static void PrepareEnvironment(Environment value) private static void PrepareEnvironmentFields(Environment value) { + value.Length = checked((ushort)((8 + value.Geometry.Length) * 8)); ArgumentNullException.ThrowIfNull(value.Geometry); } @@ -5283,7 +5287,7 @@ private static void WriteEnvironmentalProcessPduFields(ref DisBinaryWriter write WriteObjectIdentifier(ref writer, value.EnvironementalProcessId); WriteEntityType(ref writer, value.EnvironmentType); writer.WriteByte((byte)value.ModelType, "modelType"); - writer.WriteByte(value.EnvironmentStatus.Value, "environmentStatus"); + writer.WriteByte((byte)value.EnvironmentStatus.Value, "environmentStatus"); writer.WriteUInt16(value.NumberOfEnvironmentRecords, "numberOfEnvironmentRecords"); writer.WriteUInt16(value.SequenceNumber, "sequenceNumber"); foreach (Environment item in value.EnvironmentRecords) WriteEnvironment(ref writer, item); @@ -6519,7 +6523,7 @@ private static void ReadIFFDataFields(ref DisBinaryReader reader, IFFData value) { value.RecordType = (VariableRecordType)reader.ReadUInt32("recordType"); value.RecordLength = reader.ReadUInt16("recordLength"); - int RecordSpecificFieldsCount = CheckedCount(Math.Max(0, checked((int)value.RecordLength) - 4), reader.Remaining, "recordSpecificFields"); + int RecordSpecificFieldsCount = CheckedCount(Math.Max(0, checked((int)value.RecordLength) - 6), reader.Remaining, "recordSpecificFields"); value.RecordSpecificFields = new byte[RecordSpecificFieldsCount]; for (int index = 0; index < RecordSpecificFieldsCount; index++) value.RecordSpecificFields[index] = reader.ReadByte("recordSpecificFields"); @@ -6533,6 +6537,7 @@ private static void PrepareIFFData(IFFData value) private static void PrepareIFFDataFields(IFFData value) { + value.RecordLength = checked((ushort)(6 + value.RecordSpecificFields.Length)); ArgumentNullException.ThrowIfNull(value.RecordSpecificFields); } @@ -6711,6 +6716,9 @@ private static void PrepareIFFPduLayer2DataFields(IFFPduLayer2Data value) ArgumentNullException.ThrowIfNull(value.IFFFundamentalParameterDataRecord); foreach (IFFFundamentalParameterData item in value.IFFFundamentalParameterDataRecord) PrepareIFFFundamentalParameterData(item); value.NumberOfIFFFundamentalParameterDataRecordsParameters = checked((ushort)value.IFFFundamentalParameterDataRecord.Count); + int layerLength = 0; + MeasureIFFPduLayer2DataFields(value, ref layerLength); + value.LayerHeader.Length = checked((ushort)layerLength); } private static void WriteIFFPduLayer2Data(ref DisBinaryWriter writer, IFFPduLayer2Data value) @@ -6782,6 +6790,9 @@ private static void PrepareIFFPduLayer3InterrogatorFormatDataFields(IFFPduLayer3 ArgumentNullException.ThrowIfNull(value.IFFFundamentalParameterDataRecord); foreach (IFFDataSpecification item in value.IFFFundamentalParameterDataRecord) PrepareIFFDataSpecification(item); value.NumberOfIFFFundamentalParameterDataRecordsParameters = checked((ushort)value.IFFFundamentalParameterDataRecord.Count); + int layerLength = 0; + MeasureIFFPduLayer3InterrogatorFormatDataFields(value, ref layerLength); + value.LayerHeader.Length = checked((ushort)layerLength); } private static void WriteIFFPduLayer3InterrogatorFormatData(ref DisBinaryWriter writer, IFFPduLayer3InterrogatorFormatData value) @@ -6855,6 +6866,9 @@ private static void PrepareIFFPduLayer3TransponderFormatDataFields(IFFPduLayer3T ArgumentNullException.ThrowIfNull(value.IFFFundamentalParameterDataRecord); foreach (IFFDataSpecification item in value.IFFFundamentalParameterDataRecord) PrepareIFFDataSpecification(item); value.NumberOfIFFFundamentalParameterDataRecordsParameters = checked((ushort)value.IFFFundamentalParameterDataRecord.Count); + int layerLength = 0; + MeasureIFFPduLayer3TransponderFormatDataFields(value, ref layerLength); + value.LayerHeader.Length = checked((ushort)layerLength); } private static void WriteIFFPduLayer3TransponderFormatData(ref DisBinaryWriter writer, IFFPduLayer3TransponderFormatData value) @@ -6928,6 +6942,9 @@ private static void PrepareIFFPduLayer4InterrogatorFormatDataFields(IFFPduLayer4 ArgumentNullException.ThrowIfNull(value.IFFFundamentalParameterDataRecord); foreach (IFFDataSpecification item in value.IFFFundamentalParameterDataRecord) PrepareIFFDataSpecification(item); value.NumberOfIFFFundamentalParameterDataRecordsParameters = checked((ushort)value.IFFFundamentalParameterDataRecord.Count); + int layerLength = 0; + MeasureIFFPduLayer4InterrogatorFormatDataFields(value, ref layerLength); + value.LayerHeader.Length = checked((ushort)layerLength); } private static void WriteIFFPduLayer4InterrogatorFormatData(ref DisBinaryWriter writer, IFFPduLayer4InterrogatorFormatData value) @@ -7001,6 +7018,9 @@ private static void PrepareIFFPduLayer4TransponderFormatDataFields(IFFPduLayer4T ArgumentNullException.ThrowIfNull(value.IFFFundamentalParameterDataRecord); foreach (IFFDataSpecification item in value.IFFFundamentalParameterDataRecord) PrepareIFFDataSpecification(item); value.NumberOfIFFFundamentalParameterDataRecordsParameters = checked((ushort)value.IFFFundamentalParameterDataRecord.Count); + int layerLength = 0; + MeasureIFFPduLayer4TransponderFormatDataFields(value, ref layerLength); + value.LayerHeader.Length = checked((ushort)layerLength); } private static void WriteIFFPduLayer4TransponderFormatData(ref DisBinaryWriter writer, IFFPduLayer4TransponderFormatData value) @@ -7074,6 +7094,9 @@ private static void PrepareIFFPduLayer5DataFields(IFFPduLayer5Data value) ArgumentNullException.ThrowIfNull(value.IFFFundamentalParameterDataRecord); foreach (IFFDataSpecification item in value.IFFFundamentalParameterDataRecord) PrepareIFFDataSpecification(item); value.NumberOfIFFFundamentalParameterDataRecordsParameters = checked((ushort)value.IFFFundamentalParameterDataRecord.Count); + int layerLength = 0; + MeasureIFFPduLayer5DataFields(value, ref layerLength); + value.LayerHeader.Length = checked((ushort)layerLength); } private static void WriteIFFPduLayer5Data(ref DisBinaryWriter writer, IFFPduLayer5Data value) @@ -7244,9 +7267,12 @@ private static void MeasureIOEffectRecordFields(in IOEffectRecord value, ref int private static IORecord ReadIORecord(ref DisBinaryReader reader) { - var value = new IORecord(); - ReadIORecordFields(ref reader, value); - return value; + return (VariableRecordType)reader.PeekUInt32("recordType") switch + { + VariableRecordType.IoEffect => ReadIOEffectRecord(ref reader), + VariableRecordType.IoCommunicationsNode => ReadIOCommsNodeRecord(ref reader), + var recordType => throw new FormatException($"Unsupported Information Operations record type {(uint)recordType}."), + }; } private static void ReadIORecordFields(ref DisBinaryReader reader, IORecord value) @@ -7255,7 +7281,12 @@ private static void ReadIORecordFields(ref DisBinaryReader reader, IORecord valu private static void PrepareIORecord(IORecord value) { - PrepareIORecordFields(value); + switch (value) + { + case IOEffectRecord effect: PrepareIOEffectRecord(effect); break; + case IOCommsNodeRecord node: PrepareIOCommsNodeRecord(node); break; + default: throw new ArgumentException($"Unsupported Information Operations record model {value.GetType().Name}.", nameof(value)); + } } private static void PrepareIORecordFields(IORecord value) @@ -7264,7 +7295,12 @@ private static void PrepareIORecordFields(IORecord value) private static void WriteIORecord(ref DisBinaryWriter writer, IORecord value) { - WriteIORecordFields(ref writer, value); + switch (value) + { + case IOEffectRecord effect: WriteIOEffectRecord(ref writer, effect); break; + case IOCommsNodeRecord node: WriteIOCommsNodeRecord(ref writer, node); break; + default: throw new ArgumentException($"Unsupported Information Operations record model {value.GetType().Name}.", nameof(value)); + } } private static void WriteIORecordFields(ref DisBinaryWriter writer, IORecord value) @@ -7273,7 +7309,12 @@ private static void WriteIORecordFields(ref DisBinaryWriter writer, IORecord val private static void MeasureIORecord(in IORecord value, ref int offset) { - MeasureIORecordFields(value, ref offset); + switch (value) + { + case IOEffectRecord effect: MeasureIOEffectRecord(effect, ref offset); break; + case IOCommsNodeRecord node: MeasureIOCommsNodeRecord(node, ref offset); break; + default: throw new ArgumentException($"Unsupported Information Operations record model {value.GetType().Name}.", nameof(value)); + } } private static void MeasureIORecordFields(in IORecord value, ref int offset) @@ -7289,16 +7330,42 @@ private static void ReadIdentificationFriendOrFoePduFields(ref DisBinaryReader r value.SystemDesignator = reader.ReadByte("systemDesignator"); value.SystemSpecificData = reader.ReadByte("systemSpecificData"); value.FundamentalParameters = ReadFundamentalOperationalData(ref reader); - value.IFFPduLayer2Data = ReadIFFPduLayer2Data(ref reader); - value.IFFPduLayer3TransponderFormatData = ReadIFFPduLayer3TransponderFormatData(ref reader); - value.IFFPduLayer3InterrogatorFormatData = ReadIFFPduLayer3InterrogatorFormatData(ref reader); - value.IFFPduLayer4InterrogatorFormatData = ReadIFFPduLayer4InterrogatorFormatData(ref reader); - value.IFFPduLayer4TransponderFormatData = ReadIFFPduLayer4TransponderFormatData(ref reader); - value.IFFPduLayer5Data = ReadIFFPduLayer5Data(ref reader); + if ((value.FundamentalParameters.InformationLayers & (1 << 2)) != 0) + { + value.IFFPduLayer2Data = ReadIFFPduLayer2Data(ref reader); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 3)) != 0 && IsIffTransponder(value.SystemId.SystemType)) + { + value.IFFPduLayer3TransponderFormatData = ReadIFFPduLayer3TransponderFormatData(ref reader); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 3)) != 0 && IsIffInterrogator(value.SystemId.SystemType)) + { + value.IFFPduLayer3InterrogatorFormatData = ReadIFFPduLayer3InterrogatorFormatData(ref reader); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 4)) != 0 && IsIffInterrogator(value.SystemId.SystemType)) + { + value.IFFPduLayer4InterrogatorFormatData = ReadIFFPduLayer4InterrogatorFormatData(ref reader); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 4)) != 0 && IsIffTransponder(value.SystemId.SystemType)) + { + value.IFFPduLayer4TransponderFormatData = ReadIFFPduLayer4TransponderFormatData(ref reader); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 5)) != 0) + { + value.IFFPduLayer5Data = ReadIFFPduLayer5Data(ref reader); + } } private static void PrepareIdentificationFriendOrFoePduFields(IdentificationFriendOrFoePdu value) { + byte layers = (byte)(value.FundamentalParameters.InformationLayers & 0b1100_0011); + if (value.IFFPduLayer2Data is not null) layers |= 1 << 2; + if (value.IFFPduLayer3TransponderFormatData is not null || value.IFFPduLayer3InterrogatorFormatData is not null) layers |= 1 << 3; + if (value.IFFPduLayer4TransponderFormatData is not null || value.IFFPduLayer4InterrogatorFormatData is not null) layers |= 1 << 4; + if (value.IFFPduLayer5Data is not null) layers |= 1 << 5; + if (value.IFFPduLayer3TransponderFormatData is not null && value.IFFPduLayer3InterrogatorFormatData is not null) throw new ArgumentException("IFF layer 3 cannot contain both transponder and interrogator formats.", nameof(value)); + if (value.IFFPduLayer4TransponderFormatData is not null && value.IFFPduLayer4InterrogatorFormatData is not null) throw new ArgumentException("IFF layer 4 cannot contain both transponder and interrogator formats.", nameof(value)); + value.FundamentalParameters.InformationLayers = layers; ArgumentNullException.ThrowIfNull(value.EmittingEntityId); PrepareEntityId(value.EmittingEntityId); ArgumentNullException.ThrowIfNull(value.EventId); @@ -7309,18 +7376,36 @@ private static void PrepareIdentificationFriendOrFoePduFields(IdentificationFrie PrepareSystemIdentifier(value.SystemId); ArgumentNullException.ThrowIfNull(value.FundamentalParameters); PrepareFundamentalOperationalData(value.FundamentalParameters); - ArgumentNullException.ThrowIfNull(value.IFFPduLayer2Data); - PrepareIFFPduLayer2Data(value.IFFPduLayer2Data); - ArgumentNullException.ThrowIfNull(value.IFFPduLayer3TransponderFormatData); - PrepareIFFPduLayer3TransponderFormatData(value.IFFPduLayer3TransponderFormatData); - ArgumentNullException.ThrowIfNull(value.IFFPduLayer3InterrogatorFormatData); - PrepareIFFPduLayer3InterrogatorFormatData(value.IFFPduLayer3InterrogatorFormatData); - ArgumentNullException.ThrowIfNull(value.IFFPduLayer4InterrogatorFormatData); - PrepareIFFPduLayer4InterrogatorFormatData(value.IFFPduLayer4InterrogatorFormatData); - ArgumentNullException.ThrowIfNull(value.IFFPduLayer4TransponderFormatData); - PrepareIFFPduLayer4TransponderFormatData(value.IFFPduLayer4TransponderFormatData); - ArgumentNullException.ThrowIfNull(value.IFFPduLayer5Data); - PrepareIFFPduLayer5Data(value.IFFPduLayer5Data); + if ((value.FundamentalParameters.InformationLayers & (1 << 2)) != 0) + { + ArgumentNullException.ThrowIfNull(value.IFFPduLayer2Data); + PrepareIFFPduLayer2Data(value.IFFPduLayer2Data!); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 3)) != 0 && IsIffTransponder(value.SystemId.SystemType)) + { + ArgumentNullException.ThrowIfNull(value.IFFPduLayer3TransponderFormatData); + PrepareIFFPduLayer3TransponderFormatData(value.IFFPduLayer3TransponderFormatData!); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 3)) != 0 && IsIffInterrogator(value.SystemId.SystemType)) + { + ArgumentNullException.ThrowIfNull(value.IFFPduLayer3InterrogatorFormatData); + PrepareIFFPduLayer3InterrogatorFormatData(value.IFFPduLayer3InterrogatorFormatData!); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 4)) != 0 && IsIffInterrogator(value.SystemId.SystemType)) + { + ArgumentNullException.ThrowIfNull(value.IFFPduLayer4InterrogatorFormatData); + PrepareIFFPduLayer4InterrogatorFormatData(value.IFFPduLayer4InterrogatorFormatData!); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 4)) != 0 && IsIffTransponder(value.SystemId.SystemType)) + { + ArgumentNullException.ThrowIfNull(value.IFFPduLayer4TransponderFormatData); + PrepareIFFPduLayer4TransponderFormatData(value.IFFPduLayer4TransponderFormatData!); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 5)) != 0) + { + ArgumentNullException.ThrowIfNull(value.IFFPduLayer5Data); + PrepareIFFPduLayer5Data(value.IFFPduLayer5Data!); + } } private static void WriteIdentificationFriendOrFoePduFields(ref DisBinaryWriter writer, IdentificationFriendOrFoePdu value) @@ -7332,12 +7417,30 @@ private static void WriteIdentificationFriendOrFoePduFields(ref DisBinaryWriter writer.WriteByte(value.SystemDesignator, "systemDesignator"); writer.WriteByte(value.SystemSpecificData, "systemSpecificData"); WriteFundamentalOperationalData(ref writer, value.FundamentalParameters); - WriteIFFPduLayer2Data(ref writer, value.IFFPduLayer2Data); - WriteIFFPduLayer3TransponderFormatData(ref writer, value.IFFPduLayer3TransponderFormatData); - WriteIFFPduLayer3InterrogatorFormatData(ref writer, value.IFFPduLayer3InterrogatorFormatData); - WriteIFFPduLayer4InterrogatorFormatData(ref writer, value.IFFPduLayer4InterrogatorFormatData); - WriteIFFPduLayer4TransponderFormatData(ref writer, value.IFFPduLayer4TransponderFormatData); - WriteIFFPduLayer5Data(ref writer, value.IFFPduLayer5Data); + if ((value.FundamentalParameters.InformationLayers & (1 << 2)) != 0) + { + WriteIFFPduLayer2Data(ref writer, value.IFFPduLayer2Data!); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 3)) != 0 && IsIffTransponder(value.SystemId.SystemType)) + { + WriteIFFPduLayer3TransponderFormatData(ref writer, value.IFFPduLayer3TransponderFormatData!); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 3)) != 0 && IsIffInterrogator(value.SystemId.SystemType)) + { + WriteIFFPduLayer3InterrogatorFormatData(ref writer, value.IFFPduLayer3InterrogatorFormatData!); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 4)) != 0 && IsIffInterrogator(value.SystemId.SystemType)) + { + WriteIFFPduLayer4InterrogatorFormatData(ref writer, value.IFFPduLayer4InterrogatorFormatData!); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 4)) != 0 && IsIffTransponder(value.SystemId.SystemType)) + { + WriteIFFPduLayer4TransponderFormatData(ref writer, value.IFFPduLayer4TransponderFormatData!); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 5)) != 0) + { + WriteIFFPduLayer5Data(ref writer, value.IFFPduLayer5Data!); + } } private static void MeasureIdentificationFriendOrFoePduFields(in IdentificationFriendOrFoePdu value, ref int offset) @@ -7349,12 +7452,30 @@ private static void MeasureIdentificationFriendOrFoePduFields(in IdentificationF offset += 1; offset += 1; MeasureFundamentalOperationalData(value.FundamentalParameters, ref offset); - MeasureIFFPduLayer2Data(value.IFFPduLayer2Data, ref offset); - MeasureIFFPduLayer3TransponderFormatData(value.IFFPduLayer3TransponderFormatData, ref offset); - MeasureIFFPduLayer3InterrogatorFormatData(value.IFFPduLayer3InterrogatorFormatData, ref offset); - MeasureIFFPduLayer4InterrogatorFormatData(value.IFFPduLayer4InterrogatorFormatData, ref offset); - MeasureIFFPduLayer4TransponderFormatData(value.IFFPduLayer4TransponderFormatData, ref offset); - MeasureIFFPduLayer5Data(value.IFFPduLayer5Data, ref offset); + if ((value.FundamentalParameters.InformationLayers & (1 << 2)) != 0) + { + MeasureIFFPduLayer2Data(value.IFFPduLayer2Data!, ref offset); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 3)) != 0 && IsIffTransponder(value.SystemId.SystemType)) + { + MeasureIFFPduLayer3TransponderFormatData(value.IFFPduLayer3TransponderFormatData!, ref offset); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 3)) != 0 && IsIffInterrogator(value.SystemId.SystemType)) + { + MeasureIFFPduLayer3InterrogatorFormatData(value.IFFPduLayer3InterrogatorFormatData!, ref offset); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 4)) != 0 && IsIffInterrogator(value.SystemId.SystemType)) + { + MeasureIFFPduLayer4InterrogatorFormatData(value.IFFPduLayer4InterrogatorFormatData!, ref offset); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 4)) != 0 && IsIffTransponder(value.SystemId.SystemType)) + { + MeasureIFFPduLayer4TransponderFormatData(value.IFFPduLayer4TransponderFormatData!, ref offset); + } + if ((value.FundamentalParameters.InformationLayers & (1 << 5)) != 0) + { + MeasureIFFPduLayer5Data(value.IFFPduLayer5Data!, ref offset); + } } private static void ReadInformationOperationsActionPduFields(ref DisBinaryReader reader, InformationOperationsActionPdu value) @@ -7506,7 +7627,7 @@ private static void ReadIntercomCommunicationsParametersFields(ref DisBinaryRead { value.RecordType = (IntercomControlRecordType)reader.ReadUInt16("recordType"); value.RecordLength = reader.ReadUInt16("recordLength"); - int RecordSpecificFieldCount = CheckedCount(Math.Max(0, checked((int)value.RecordLength) - 4), reader.Remaining, "recordSpecificField"); + int RecordSpecificFieldCount = CheckedCount(Math.Max(0, checked((int)value.RecordLength) - 6), reader.Remaining, "recordSpecificField"); value.RecordSpecificField = new byte[RecordSpecificFieldCount]; for (int index = 0; index < RecordSpecificFieldCount; index++) value.RecordSpecificField[index] = reader.ReadByte("recordSpecificField"); @@ -7520,6 +7641,7 @@ private static void PrepareIntercomCommunicationsParameters(IntercomCommunicatio private static void PrepareIntercomCommunicationsParametersFields(IntercomCommunicationsParameters value) { + value.RecordLength = checked((ushort)(6 + value.RecordSpecificField.Length)); ArgumentNullException.ThrowIfNull(value.RecordSpecificField); } @@ -8141,7 +8263,7 @@ private static LinearSegmentParameter ReadLinearSegmentParameter(ref DisBinaryRe private static void ReadLinearSegmentParameterFields(ref DisBinaryReader reader, LinearSegmentParameter value) { value.SegmentNumber = reader.ReadByte("segmentNumber"); - value.SegmentModification = new ObjectStateModificationLinearObject(reader.ReadUInt16("segmentModification")); + value.SegmentModification = new ObjectStateModificationLinearObject(reader.ReadByte("segmentModification")); value.GeneralSegmentAppearance = new ObjectStateAppearanceGeneral(reader.ReadUInt16("generalSegmentAppearance")); value.SpecificSegmentAppearance = reader.ReadUInt32("specificSegmentAppearance"); value.SegmentLocation = ReadVector3Double(ref reader); @@ -8174,8 +8296,8 @@ private static void WriteLinearSegmentParameter(ref DisBinaryWriter writer, Line private static void WriteLinearSegmentParameterFields(ref DisBinaryWriter writer, LinearSegmentParameter value) { writer.WriteByte(value.SegmentNumber, "segmentNumber"); - writer.WriteUInt16(value.SegmentModification.Value, "segmentModification"); - writer.WriteUInt16(value.GeneralSegmentAppearance.Value, "generalSegmentAppearance"); + writer.WriteByte((byte)value.SegmentModification.Value, "segmentModification"); + writer.WriteUInt16((ushort)value.GeneralSegmentAppearance.Value, "generalSegmentAppearance"); writer.WriteUInt32(value.SpecificSegmentAppearance, "specificSegmentAppearance"); WriteVector3Double(ref writer, value.SegmentLocation); WriteEulerAngles(ref writer, value.SegmentOrientation); @@ -8194,7 +8316,7 @@ private static void MeasureLinearSegmentParameter(in LinearSegmentParameter valu private static void MeasureLinearSegmentParameterFields(in LinearSegmentParameter value, ref int offset) { offset += 1; - offset += 2; + offset += 1; offset += 2; offset += 4; MeasureVector3Double(value.SegmentLocation, ref offset); @@ -9485,7 +9607,7 @@ private static void WriteMinefieldStatePduFields(ref DisBinaryWriter writer, Min writer.WriteUInt16(value.NumberOfMineTypes, "numberOfMineTypes"); WriteVector3Double(ref writer, value.MinefieldLocation); WriteEulerAngles(ref writer, value.MinefieldOrientation); - writer.WriteUInt16(value.Appearance.Value, "appearance"); + writer.WriteUInt16((ushort)value.Appearance.Value, "appearance"); WriteProtocolMode(ref writer, value.ProtocolMode); foreach (Vector2Float item in value.PerimeterPoints) WriteVector2Float(ref writer, item); foreach (EntityType item in value.MineType) WriteEntityType(ref writer, item); @@ -10326,7 +10448,7 @@ private static void ReadPointObjectStatePduFields(ref DisBinaryReader reader, Po value.ReferencedObjectId = ReadObjectIdentifier(ref reader); value.UpdateNumber = reader.ReadUInt32("updateNumber"); value.ForceId = (ForceId)reader.ReadByte("forceID"); - value.Modifications = new ObjectStateModificationPointObject(reader.ReadUInt16("modifications")); + value.Modifications = new ObjectStateModificationPointObject(reader.ReadByte("modifications")); value.ObjectType = ReadObjectType(ref reader); value.ObjectLocation = ReadVector3Double(ref reader); value.ObjectOrientation = ReadEulerAngles(ref reader); @@ -10362,12 +10484,12 @@ private static void WritePointObjectStatePduFields(ref DisBinaryWriter writer, P WriteObjectIdentifier(ref writer, value.ReferencedObjectId); writer.WriteUInt32(value.UpdateNumber, "updateNumber"); writer.WriteByte((byte)value.ForceId, "forceID"); - writer.WriteUInt16(value.Modifications.Value, "modifications"); + writer.WriteByte((byte)value.Modifications.Value, "modifications"); WriteObjectType(ref writer, value.ObjectType); WriteVector3Double(ref writer, value.ObjectLocation); WriteEulerAngles(ref writer, value.ObjectOrientation); writer.WriteUInt32(value.SpecificObjectAppearance, "specificObjectAppearance"); - writer.WriteUInt16(value.GenerObjectAppearance.Value, "generObjectAppearance"); + writer.WriteUInt16((ushort)value.GenerObjectAppearance.Value, "generObjectAppearance"); writer.WriteUInt16(value.Padding1, "padding1"); WriteSimulationAddress(ref writer, value.RequesterId); WriteSimulationAddress(ref writer, value.ReceivingId); @@ -10380,7 +10502,7 @@ private static void MeasurePointObjectStatePduFields(in PointObjectStatePdu valu MeasureObjectIdentifier(value.ReferencedObjectId, ref offset); offset += 4; offset += 1; - offset += 2; + offset += 1; MeasureObjectType(value.ObjectType, ref offset); MeasureVector3Double(value.ObjectLocation, ref offset); MeasureEulerAngles(value.ObjectOrientation, ref offset); @@ -10886,10 +11008,7 @@ private static void ReadRecordSpecificationElementFields(ref DisBinaryReader rea value.RecordValues = new byte[RecordValuesCount]; for (int index = 0; index < RecordValuesCount; index++) value.RecordValues[index] = reader.ReadByte("recordValues"); - int PadTo64Count = CheckedCount(Padding(reader.Offset, 8), reader.Remaining, "padTo64"); - value.PadTo64 = new byte[PadTo64Count]; - for (int index = 0; index < PadTo64Count; index++) - value.PadTo64[index] = reader.ReadByte("padTo64"); + reader.Skip(Padding(reader.Offset, 8), "padTo64"); } private static void PrepareRecordSpecificationElement(RecordSpecificationElement value) @@ -10899,8 +11018,11 @@ private static void PrepareRecordSpecificationElement(RecordSpecificationElement private static void PrepareRecordSpecificationElementFields(RecordSpecificationElement value) { + if (value.RecordCount == 0) value.RecordCount = 1; + int recordValueBits = checked(value.RecordValues.Length * 8); + if (recordValueBits % value.RecordCount != 0) throw new ArgumentException("RecordValues must contain an integral number of equal-length records.", nameof(value)); + value.RecordLength = checked((ushort)(recordValueBits / value.RecordCount)); ArgumentNullException.ThrowIfNull(value.RecordValues); - ArgumentNullException.ThrowIfNull(value.PadTo64); } private static void WriteRecordSpecificationElement(ref DisBinaryWriter writer, RecordSpecificationElement value) @@ -10916,7 +11038,7 @@ private static void WriteRecordSpecificationElementFields(ref DisBinaryWriter wr writer.WriteUInt16(value.RecordLength, "recordLength"); writer.WriteUInt16(value.RecordCount, "recordCount"); foreach (byte item in value.RecordValues) writer.WriteByte(item, "recordValues"); - foreach (byte item in value.PadTo64) writer.WriteByte(item, "padTo64"); + writer.WriteZeros(Padding(writer.Offset, 8), "padTo64"); } private static void MeasureRecordSpecificationElement(in RecordSpecificationElement value, ref int offset) @@ -10932,7 +11054,7 @@ private static void MeasureRecordSpecificationElementFields(in RecordSpecificati offset += 2; offset += 2; offset += checked(value.RecordValues.Length * 1); - offset += checked(value.PadTo64.Length * 1); + offset += Padding(offset, 8); } private static Relationship ReadRelationship(ref DisBinaryReader reader) @@ -11062,7 +11184,7 @@ private static void ReadRepairResponsePduFields(ref DisBinaryReader reader, Repa { value.ReceivingEntityId = ReadEntityId(ref reader); value.RepairingEntityId = ReadEntityId(ref reader); - value.RepairResult = (RepairResponseRepairResult)reader.ReadUInt16("repairResult"); + value.RepairResult = (RepairResponseRepairResult)reader.ReadByte("repairResult"); value.Padding1 = reader.ReadByte("padding1"); value.Padding2 = reader.ReadUInt16("padding2"); } @@ -11079,7 +11201,7 @@ private static void WriteRepairResponsePduFields(ref DisBinaryWriter writer, Rep { WriteEntityId(ref writer, value.ReceivingEntityId); WriteEntityId(ref writer, value.RepairingEntityId); - writer.WriteUInt16((ushort)value.RepairResult, "repairResult"); + writer.WriteByte((byte)value.RepairResult, "repairResult"); writer.WriteByte(value.Padding1, "padding1"); writer.WriteUInt16(value.Padding2, "padding2"); } @@ -11088,7 +11210,7 @@ private static void MeasureRepairResponsePduFields(in RepairResponsePdu value, r { MeasureEntityId(value.ReceivingEntityId, ref offset); MeasureEntityId(value.RepairingEntityId, ref offset); - offset += 2; + offset += 1; offset += 1; offset += 2; } @@ -12086,7 +12208,7 @@ private static void WriteStopFreezePduFields(ref DisBinaryWriter writer, StopFre { WriteClockTime(ref writer, value.RealWorldTime); writer.WriteByte((byte)value.Reason, "reason"); - writer.WriteByte(value.FrozenBehavior.Value, "frozenBehavior"); + writer.WriteByte((byte)value.FrozenBehavior.Value, "frozenBehavior"); writer.WriteUInt16(value.Padding1, "padding1"); writer.WriteUInt32(value.RequestId, "requestID"); } @@ -12120,7 +12242,7 @@ private static void WriteStopFreezeReliablePduFields(ref DisBinaryWriter writer, { WriteClockTime(ref writer, value.RealWorldTime); writer.WriteByte((byte)value.Reason, "reason"); - writer.WriteByte(value.FrozenBehavior.Value, "frozenBehavior"); + writer.WriteByte((byte)value.FrozenBehavior.Value, "frozenBehavior"); writer.WriteByte((byte)value.RequiredReliabilityService, "requiredReliabilityService"); writer.WriteByte(value.Pad1, "pad1"); writer.WriteUInt32(value.RequestId, "requestID"); @@ -13179,7 +13301,7 @@ private static void ReadVariableTransmitterParametersFields(ref DisBinaryReader { value.RecordType = (VariableRecordType)reader.ReadUInt32("recordType"); value.RecordLength = reader.ReadUInt16("recordLength"); - int RecordSpecificFieldsCount = CheckedCount(Math.Max(0, checked((int)value.RecordLength) - 4), reader.Remaining, "recordSpecificFields"); + int RecordSpecificFieldsCount = CheckedCount(Math.Max(0, checked((int)value.RecordLength) - 6), reader.Remaining, "recordSpecificFields"); value.RecordSpecificFields = new byte[RecordSpecificFieldsCount]; for (int index = 0; index < RecordSpecificFieldsCount; index++) value.RecordSpecificFields[index] = reader.ReadByte("recordSpecificFields"); @@ -13193,6 +13315,7 @@ private static void PrepareVariableTransmitterParameters(VariableTransmitterPara private static void PrepareVariableTransmitterParametersFields(VariableTransmitterParameters value) { + value.RecordLength = checked((ushort)(6 + value.RecordSpecificFields.Length)); ArgumentNullException.ThrowIfNull(value.RecordSpecificFields); } diff --git a/src/OpenDisNet/Pdus/Families/DistributedEmissionsPdus.cs b/src/OpenDisNet/Pdus/Families/DistributedEmissionsPdus.cs index 319596f..c909645 100644 --- a/src/OpenDisNet/Pdus/Families/DistributedEmissionsPdus.cs +++ b/src/OpenDisNet/Pdus/Families/DistributedEmissionsPdus.cs @@ -474,32 +474,32 @@ public partial class IdentificationFriendOrFoePdu : DistributedEmissionsRegenera /// /// IFF pdu layer 2 data /// - public IFFPduLayer2Data IFFPduLayer2Data { get; set; } = new IFFPduLayer2Data(); + public IFFPduLayer2Data? IFFPduLayer2Data { get; set; } /// /// IFF pdu layer 3 transponder format data /// - public IFFPduLayer3TransponderFormatData IFFPduLayer3TransponderFormatData { get; set; } = new IFFPduLayer3TransponderFormatData(); + public IFFPduLayer3TransponderFormatData? IFFPduLayer3TransponderFormatData { get; set; } /// /// IFF pdu layer 3 interrogator format data /// - public IFFPduLayer3InterrogatorFormatData IFFPduLayer3InterrogatorFormatData { get; set; } = new IFFPduLayer3InterrogatorFormatData(); + public IFFPduLayer3InterrogatorFormatData? IFFPduLayer3InterrogatorFormatData { get; set; } /// /// IFF pdu layer 4 interrogator format data /// - public IFFPduLayer4InterrogatorFormatData IFFPduLayer4InterrogatorFormatData { get; set; } = new IFFPduLayer4InterrogatorFormatData(); + public IFFPduLayer4InterrogatorFormatData? IFFPduLayer4InterrogatorFormatData { get; set; } /// /// IFF pdu layer 4 transponder format data /// - public IFFPduLayer4TransponderFormatData IFFPduLayer4TransponderFormatData { get; set; } = new IFFPduLayer4TransponderFormatData(); + public IFFPduLayer4TransponderFormatData? IFFPduLayer4TransponderFormatData { get; set; } /// /// IFF pdu layer 5 data communications /// - public IFFPduLayer5Data IFFPduLayer5Data { get; set; } = new IFFPduLayer5Data(); + public IFFPduLayer5Data? IFFPduLayer5Data { get; set; } } diff --git a/src/OpenDisNet/Pdus/Families/Records.cs b/src/OpenDisNet/Pdus/Families/Records.cs index ababf21..1da9b27 100644 --- a/src/OpenDisNet/Pdus/Families/Records.cs +++ b/src/OpenDisNet/Pdus/Families/Records.cs @@ -2620,11 +2620,6 @@ public partial class RecordSpecificationElement /// public byte[] RecordValues { get; set; } = new byte[0]; - /// - /// used if required to make entire record size an even multiple of 8 bytes - /// - public byte[] PadTo64 { get; set; } = new byte[0]; - } /// diff --git a/src/OpenDisNet/Pdus/PduRegistry.cs b/src/OpenDisNet/Pdus/PduRegistry.cs index 986a4db..9355468 100644 --- a/src/OpenDisNet/Pdus/PduRegistry.cs +++ b/src/OpenDisNet/Pdus/PduRegistry.cs @@ -13,7 +13,7 @@ public static IDisPdu Parse(DisHeader header, ReadOnlySpan body) => public static int GetLength(IDisPdu pdu) => pdu switch { Pdu typed => PduCodec.GetEncodedLength(typed), - UnknownPdu unknown => DisHeader.Size + unknown.Body.Length, + UnknownPdu unknown => unknown.Header.EncodedSize + unknown.Body.Length, _ => throw new NotSupportedException($"No DIS writer is registered for {pdu.GetType().FullName}."), }; diff --git a/src/OpenDisNet/Protocol/DisHeader.cs b/src/OpenDisNet/Protocol/DisHeader.cs index b577c5a..8e5feef 100644 --- a/src/OpenDisNet/Protocol/DisHeader.cs +++ b/src/OpenDisNet/Protocol/DisHeader.cs @@ -1,6 +1,6 @@ namespace OpenDisNet.Protocol; -/// The common 12-byte header present on every DIS PDU. +/// The DIS PDU header, including the status octets used outside the Live Entity family. public readonly record struct DisHeader( DisProtocolVersion ProtocolVersion, byte ExerciseId, @@ -11,5 +11,9 @@ public readonly record struct DisHeader( byte PduStatus, byte Padding) { + public const int MinimumSize = 10; public const int Size = 12; + + /// Gets the encoded header size for this protocol family. + public int EncodedSize => ProtocolFamily == ProtocolFamily.LiveEntity ? MinimumSize : Size; } diff --git a/tools/OpenDisNet.Generator/CodecWriter.cs b/tools/OpenDisNet.Generator/CodecWriter.cs index e6781db..59a0cb4 100644 --- a/tools/OpenDisNet.Generator/CodecWriter.cs +++ b/tools/OpenDisNet.Generator/CodecWriter.cs @@ -35,7 +35,7 @@ private static void WritePublicApi(StringBuilder text, DisSchema schema, IReadOn text.AppendLine(" {"); text.AppendLine(" Pdu value = PduFactory.Create(header.PduType, header.ExerciseId);"); text.AppendLine(" ApplyHeader(value, header);"); - text.AppendLine(" var reader = new DisBinaryReader(body, DisHeader.Size);"); + text.AppendLine(" var reader = new DisBinaryReader(body, header.EncodedSize);"); text.AppendLine(" switch ((byte)header.PduType)"); text.AppendLine(" {"); foreach (PduDefinition pdu in schema.Pdus) @@ -56,7 +56,7 @@ private static void WritePublicApi(StringBuilder text, DisSchema schema, IReadOn text.AppendLine(" {"); text.AppendLine(" ArgumentNullException.ThrowIfNull(value);"); text.AppendLine(" Prepare(value);"); - text.AppendLine(" int offset = DisHeader.Size;"); + text.AppendLine(" int offset = value.Header.EncodedSize;"); text.AppendLine(" MeasureBody(value, ref offset);"); text.AppendLine(" return offset;"); text.AppendLine(" }"); @@ -103,6 +103,9 @@ private static void WritePublicApi(StringBuilder text, DisSchema schema, IReadOn text.AppendLine(" }"); text.AppendLine(" }"); text.AppendLine(); + text.AppendLine(" private static bool IsIffTransponder(IffSystemType value) => (ushort)value is 1 or 3 or 5 or 9 or 10 or 11;"); + text.AppendLine(" private static bool IsIffInterrogator(IffSystemType value) => (ushort)value is 2 or 4 or 6 or 7 or 8;"); + text.AppendLine(); } private static void WritePduSwitchMethod( @@ -137,6 +140,18 @@ private static void WriteRecordMethods(StringBuilder text, ClassDefinition defin { text.AppendLine($" private static {definition.Name} Read{definition.Name}(ref DisBinaryReader reader)"); text.AppendLine(" {"); + if (definition.Name == "IORecord") + { + text.AppendLine(" return (VariableRecordType)reader.PeekUInt32(\"recordType\") switch"); + text.AppendLine(" {"); + text.AppendLine(" VariableRecordType.IoEffect => ReadIOEffectRecord(ref reader),"); + text.AppendLine(" VariableRecordType.IoCommunicationsNode => ReadIOCommsNodeRecord(ref reader),"); + text.AppendLine(" var recordType => throw new FormatException($\"Unsupported Information Operations record type {(uint)recordType}.\"),"); + text.AppendLine(" };"); + text.AppendLine(" }"); + text.AppendLine(); + goto ReadFields; + } text.AppendLine($" var value = new {definition.Name}();"); if (definition.BaseName != "root") text.AppendLine($" Read{definition.BaseName}Fields(ref reader, value);"); @@ -146,6 +161,7 @@ private static void WriteRecordMethods(StringBuilder text, ClassDefinition defin text.AppendLine(); } + ReadFields: text.AppendLine($" private static void Read{definition.Name}Fields(ref DisBinaryReader reader, {definition.Name} value)"); text.AppendLine(" {"); foreach (FieldDefinition field in definition.Fields) @@ -157,9 +173,21 @@ private static void WriteRecordMethods(StringBuilder text, ClassDefinition defin { text.AppendLine($" private static void Prepare{definition.Name}({definition.Name} value)"); text.AppendLine(" {"); - if (definition.BaseName != "root") - text.AppendLine($" Prepare{definition.BaseName}Fields(value);"); - text.AppendLine($" Prepare{definition.Name}Fields(value);"); + if (definition.Name == "IORecord") + { + text.AppendLine(" switch (value)"); + text.AppendLine(" {"); + text.AppendLine(" case IOEffectRecord effect: PrepareIOEffectRecord(effect); break;"); + text.AppendLine(" case IOCommsNodeRecord node: PrepareIOCommsNodeRecord(node); break;"); + text.AppendLine(" default: throw new ArgumentException($\"Unsupported Information Operations record model {value.GetType().Name}.\", nameof(value));"); + text.AppendLine(" }"); + } + else + { + if (definition.BaseName != "root") + text.AppendLine($" Prepare{definition.BaseName}Fields(value);"); + text.AppendLine($" Prepare{definition.Name}Fields(value);"); + } text.AppendLine(" }"); text.AppendLine(); } @@ -173,9 +201,39 @@ private static void WriteRecordMethods(StringBuilder text, ClassDefinition defin text.AppendLine(" if ((value.DataBitLength + 7) / 8 != value.Data.Length)"); text.AppendLine(" throw new ArgumentException(\"DataBitLength must match Data, allowing only unused bits in the final octet.\", nameof(value));"); } + if (definition.Name == "Environment") + text.AppendLine(" value.Length = checked((ushort)((8 + value.Geometry.Length) * 8));"); + if (definition.Name is "VariableTransmitterParameters" or "IFFData") + text.AppendLine(" value.RecordLength = checked((ushort)(6 + value.RecordSpecificFields.Length));"); + if (definition.Name == "IntercomCommunicationsParameters") + text.AppendLine(" value.RecordLength = checked((ushort)(6 + value.RecordSpecificField.Length));"); + if (definition.Name == "RecordSpecificationElement") + { + text.AppendLine(" if (value.RecordCount == 0) value.RecordCount = 1;"); + text.AppendLine(" int recordValueBits = checked(value.RecordValues.Length * 8);"); + text.AppendLine(" if (recordValueBits % value.RecordCount != 0) throw new ArgumentException(\"RecordValues must contain an integral number of equal-length records.\", nameof(value));"); + text.AppendLine(" value.RecordLength = checked((ushort)(recordValueBits / value.RecordCount));"); + } + if (definition.Name == "IdentificationFriendOrFoePdu") + { + text.AppendLine(" byte layers = (byte)(value.FundamentalParameters.InformationLayers & 0b1100_0011);"); + text.AppendLine(" if (value.IFFPduLayer2Data is not null) layers |= 1 << 2;"); + text.AppendLine(" if (value.IFFPduLayer3TransponderFormatData is not null || value.IFFPduLayer3InterrogatorFormatData is not null) layers |= 1 << 3;"); + text.AppendLine(" if (value.IFFPduLayer4TransponderFormatData is not null || value.IFFPduLayer4InterrogatorFormatData is not null) layers |= 1 << 4;"); + text.AppendLine(" if (value.IFFPduLayer5Data is not null) layers |= 1 << 5;"); + text.AppendLine(" if (value.IFFPduLayer3TransponderFormatData is not null && value.IFFPduLayer3InterrogatorFormatData is not null) throw new ArgumentException(\"IFF layer 3 cannot contain both transponder and interrogator formats.\", nameof(value));"); + text.AppendLine(" if (value.IFFPduLayer4TransponderFormatData is not null && value.IFFPduLayer4InterrogatorFormatData is not null) throw new ArgumentException(\"IFF layer 4 cannot contain both transponder and interrogator formats.\", nameof(value));"); + text.AppendLine(" value.FundamentalParameters.InformationLayers = layers;"); + } var countOwners = new HashSet(StringComparer.Ordinal); foreach (FieldDefinition field in definition.Fields) WritePrepareField(text, field, definition, countOwners); + if (definition.Name.StartsWith("IFFPduLayer", StringComparison.Ordinal)) + { + text.AppendLine(" int layerLength = 0;"); + text.AppendLine($" Measure{definition.Name}Fields(value, ref layerLength);"); + text.AppendLine(" value.LayerHeader.Length = checked((ushort)layerLength);"); + } text.AppendLine(" }"); text.AppendLine(); @@ -183,9 +241,21 @@ private static void WriteRecordMethods(StringBuilder text, ClassDefinition defin { text.AppendLine($" private static void Write{definition.Name}(ref DisBinaryWriter writer, {definition.Name} value)"); text.AppendLine(" {"); - if (definition.BaseName != "root") - text.AppendLine($" Write{definition.BaseName}Fields(ref writer, value);"); - text.AppendLine($" Write{definition.Name}Fields(ref writer, value);"); + if (definition.Name == "IORecord") + { + text.AppendLine(" switch (value)"); + text.AppendLine(" {"); + text.AppendLine(" case IOEffectRecord effect: WriteIOEffectRecord(ref writer, effect); break;"); + text.AppendLine(" case IOCommsNodeRecord node: WriteIOCommsNodeRecord(ref writer, node); break;"); + text.AppendLine(" default: throw new ArgumentException($\"Unsupported Information Operations record model {value.GetType().Name}.\", nameof(value));"); + text.AppendLine(" }"); + } + else + { + if (definition.BaseName != "root") + text.AppendLine($" Write{definition.BaseName}Fields(ref writer, value);"); + text.AppendLine($" Write{definition.Name}Fields(ref writer, value);"); + } text.AppendLine(" }"); text.AppendLine(); } @@ -201,9 +271,21 @@ private static void WriteRecordMethods(StringBuilder text, ClassDefinition defin { text.AppendLine($" private static void Measure{definition.Name}(in {definition.Name} value, ref int offset)"); text.AppendLine(" {"); - if (definition.BaseName != "root") - text.AppendLine($" Measure{definition.BaseName}Fields(value, ref offset);"); - text.AppendLine($" Measure{definition.Name}Fields(value, ref offset);"); + if (definition.Name == "IORecord") + { + text.AppendLine(" switch (value)"); + text.AppendLine(" {"); + text.AppendLine(" case IOEffectRecord effect: MeasureIOEffectRecord(effect, ref offset); break;"); + text.AppendLine(" case IOCommsNodeRecord node: MeasureIOCommsNodeRecord(node, ref offset); break;"); + text.AppendLine(" default: throw new ArgumentException($\"Unsupported Information Operations record model {value.GetType().Name}.\", nameof(value));"); + text.AppendLine(" }"); + } + else + { + if (definition.BaseName != "root") + text.AppendLine($" Measure{definition.BaseName}Fields(value, ref offset);"); + text.AppendLine($" Measure{definition.Name}Fields(value, ref offset);"); + } text.AppendLine(" }"); text.AppendLine(); } @@ -290,9 +372,9 @@ private static void WriteReadList(StringBuilder text, FieldDefinition field, str private static void WriteReadPrimitiveList(StringBuilder text, FieldDefinition field, string property, string owner) { string count = field.IsDynamicLength - ? "Math.Max(0, checked((int)value.RecordLength) - 4)" + ? "Math.Max(0, checked((int)value.RecordLength) - 6)" : owner == "Environment" && field.Name == "geometry" - ? "Math.Max(0, ((checked((int)value.Length) + 7) / 8) - 6)" + ? "Math.Max(0, ((checked((int)value.Length) + 7) / 8) - 8)" : field.FixedLength == 0 && field.CountFieldName is null ? VariableCountExpression(owner, field.Name) : CountExpression(field, "value"); @@ -310,8 +392,11 @@ private static void WritePrepareField(StringBuilder text, FieldDefinition field, string? condition = ConditionalFieldExpression(owner, field.Name); if (condition is not null) { - string collectionCount = field.Kind == FieldKind.PrimitiveList ? $"value.{property}.Length" : $"value.{property}.Count"; - text.AppendLine($" if ({collectionCount} != 0) value.DataFilter.BitFlags |= 1u << {MinefieldDataBit(field.Name)};"); + if (owner == "MinefieldDataPdu") + { + string collectionCount = field.Kind == FieldKind.PrimitiveList ? $"value.{property}.Length" : $"value.{property}.Count"; + text.AppendLine($" if ({collectionCount} != 0) value.DataFilter.BitFlags |= 1u << {MinefieldDataBit(field.Name)};"); + } text.AppendLine($" if ({condition})"); text.AppendLine(" {"); } @@ -319,7 +404,7 @@ private static void WritePrepareField(StringBuilder text, FieldDefinition field, if (field.Kind == FieldKind.ClassReference) { text.AppendLine($" ArgumentNullException.ThrowIfNull(value.{property});"); - text.AppendLine($" Prepare{field.TypeName}(value.{property});"); + text.AppendLine($" Prepare{field.TypeName}({PropertyAccess(owner, field.Name, property)});"); } else if (field.Kind is FieldKind.ObjectList or FieldKind.PrimitiveList) { @@ -371,13 +456,13 @@ private static void WriteWriteField(StringBuilder text, FieldDefinition field, s text.AppendLine($" writer.{WriteMethod(field.TypeName)}(value.{property}, \"{field.Name}\");"); break; case FieldKind.ClassReference: - text.AppendLine($" Write{field.TypeName}(ref writer, value.{property});"); + text.AppendLine($" Write{field.TypeName}(ref writer, {PropertyAccess(owner, field.Name, property)});"); break; case FieldKind.Enumeration: text.AppendLine($" writer.{WriteBits(field.BitSize)}(({UnsignedBits(field.BitSize)})value.{property}, \"{field.Name}\");"); break; case FieldKind.BitField: - text.AppendLine($" writer.{WriteBits(field.BitSize)}(value.{property}.Value, \"{field.Name}\");"); + text.AppendLine($" writer.{WriteBits(field.BitSize)}(({UnsignedBits(field.BitSize)})value.{property}.Value, \"{field.Name}\");"); break; case FieldKind.ObjectList: if (field.BitSize is null) @@ -420,7 +505,7 @@ private static void WriteMeasureField(StringBuilder text, FieldDefinition field, text.AppendLine($" offset += {PrimitiveSize(field.TypeName)};"); break; case FieldKind.ClassReference: - text.AppendLine($" Measure{field.TypeName}(value.{property}, ref offset);"); + text.AppendLine($" Measure{field.TypeName}({PropertyAccess(owner, field.Name, property)}, ref offset);"); break; case FieldKind.Enumeration: case FieldKind.BitField: @@ -501,10 +586,17 @@ private static bool IsBitCount(FieldDefinition field) => private static bool IsByteLengthObjectList(string owner, string field) => (owner, field) is ("IntercomControlPdu", "intercomParameters"); - private static string? ConditionalFieldExpression(string owner, string field) => - owner == "MinefieldDataPdu" && MinefieldDataBit(field) >= 0 - ? $"(value.DataFilter.BitFlags & (1u << {MinefieldDataBit(field)})) != 0" - : null; + private static string? ConditionalFieldExpression(string owner, string field) => (owner, field) switch + { + ("MinefieldDataPdu", _) when MinefieldDataBit(field) >= 0 => $"(value.DataFilter.BitFlags & (1u << {MinefieldDataBit(field)})) != 0", + ("IdentificationFriendOrFoePdu", "iFFPduLayer2Data") => "(value.FundamentalParameters.InformationLayers & (1 << 2)) != 0", + ("IdentificationFriendOrFoePdu", "iFFPduLayer3TransponderFormatData") => "(value.FundamentalParameters.InformationLayers & (1 << 3)) != 0 && IsIffTransponder(value.SystemId.SystemType)", + ("IdentificationFriendOrFoePdu", "iFFPduLayer3InterrogatorFormatData") => "(value.FundamentalParameters.InformationLayers & (1 << 3)) != 0 && IsIffInterrogator(value.SystemId.SystemType)", + ("IdentificationFriendOrFoePdu", "iFFPduLayer4InterrogatorFormatData") => "(value.FundamentalParameters.InformationLayers & (1 << 4)) != 0 && IsIffInterrogator(value.SystemId.SystemType)", + ("IdentificationFriendOrFoePdu", "iFFPduLayer4TransponderFormatData") => "(value.FundamentalParameters.InformationLayers & (1 << 4)) != 0 && IsIffTransponder(value.SystemId.SystemType)", + ("IdentificationFriendOrFoePdu", "iFFPduLayer5Data") => "(value.FundamentalParameters.InformationLayers & (1 << 5)) != 0", + _ => null, + }; private static int MinefieldDataBit(string field) => field switch { @@ -522,11 +614,16 @@ private static bool IsByteLengthObjectList(string owner, string field) => _ => -1, }; + private static string PropertyAccess(string owner, string field, string property) => + owner == "IdentificationFriendOrFoePdu" && field.StartsWith("iFFPduLayer", StringComparison.Ordinal) + ? $"value.{property}!" + : $"value.{property}"; + private static string VariableCountExpression(string owner, string field) => (owner, field) switch { ("GridAxisDescriptorVariable", "xiValues") => "checked((int)value.NumberOfPointsOnXiAxis)", - ("IntercomCommunicationsParameters", "recordSpecificField") => "Math.Max(0, checked((int)value.RecordLength) - 4)", - ("VariableTransmitterParameters", "recordSpecificFields") => "Math.Max(0, checked((int)value.RecordLength) - 4)", + ("IntercomCommunicationsParameters", "recordSpecificField") => "Math.Max(0, checked((int)value.RecordLength) - 6)", + ("VariableTransmitterParameters", "recordSpecificFields") => "Math.Max(0, checked((int)value.RecordLength) - 6)", ("RecordSpecificationElement", "recordValues") => "(checked((int)value.RecordLength) * checked((int)value.RecordCount) + 7) / 8", ("RecordSpecificationElement", "padTo64") => "Padding(reader.Offset, 8)", ("SignalPdu", "data") => "(checked((int)value.DataBitLength) + 7) / 8", diff --git a/tools/OpenDisNet.Generator/DisSchemaLoader.cs b/tools/OpenDisNet.Generator/DisSchemaLoader.cs index 41ad15e..9e9ddc5 100644 --- a/tools/OpenDisNet.Generator/DisSchemaLoader.cs +++ b/tools/OpenDisNet.Generator/DisSchemaLoader.cs @@ -130,7 +130,8 @@ FieldDefinition SisoField(FieldKind kind) string typeName = Required(shape, "type"); if (!sisoTypes.TryGetValue(typeName, out SisoTypeDefinition? wireType)) throw new InvalidDataException($"Missing SISO wire metadata for '{typeName}'."); - return new(name, kind, PublicNames.SisoType(typeName, wireType.Uid), null, null, wireType.Bits, false, Hidden(attribute), Comment(attribute)); + int bits = OptionalInt(shape, "size") ?? wireType.Bits; + return new(name, kind, PublicNames.SisoType(typeName, wireType.Uid), null, null, bits, false, Hidden(attribute), Comment(attribute)); } } diff --git a/tools/OpenDisNet.Generator/ModelWriter.cs b/tools/OpenDisNet.Generator/ModelWriter.cs index 1a1c6ad..54b5bc7 100644 --- a/tools/OpenDisNet.Generator/ModelWriter.cs +++ b/tools/OpenDisNet.Generator/ModelWriter.cs @@ -46,7 +46,9 @@ public static string Create(DisSchema schema, string sourceFile) string type = definition.Name is "SignalPdu" or "IntercomSignalPdu" && propertyName == "EncodingScheme" ? "SignalEncodingScheme" : TypeName(field); - string initializer = Initializer(field); + bool optionalIffLayer = definition.Name == "IdentificationFriendOrFoePdu" && propertyName.StartsWith("IFFPduLayer", StringComparison.Ordinal); + if (optionalIffLayer) type += "?"; + string initializer = optionalIffLayer ? string.Empty : Initializer(field); string access = field.IsHidden ? "internal" : "public"; text.AppendLine($" {access} {type} {propertyName} {{ get; set; }}{initializer}"); text.AppendLine(); diff --git a/tools/OpenDisNet.Generator/Schemas/DIS7/DIS_7_2012.xml b/tools/OpenDisNet.Generator/Schemas/DIS7/DIS_7_2012.xml index 9d3c6ad..6d2ba86 100644 --- a/tools/OpenDisNet.Generator/Schemas/DIS7/DIS_7_2012.xml +++ b/tools/OpenDisNet.Generator/Schemas/DIS7/DIS_7_2012.xml @@ -2244,9 +2244,7 @@ dB and RPM)"/> - - - + diff --git a/tools/OpenDisNet.Generator/Schemas/DIS7/LogisticsFamilyPdus.xml b/tools/OpenDisNet.Generator/Schemas/DIS7/LogisticsFamilyPdus.xml index 839a2d1..d885bb7 100644 --- a/tools/OpenDisNet.Generator/Schemas/DIS7/LogisticsFamilyPdus.xml +++ b/tools/OpenDisNet.Generator/Schemas/DIS7/LogisticsFamilyPdus.xml @@ -162,7 +162,7 @@ - + @@ -174,4 +174,4 @@ - \ No newline at end of file + From 526cb571307beaf0f58fb90a8b7764f94d1540c9 Mon Sep 17 00:00:00 2001 From: RejectKid Date: Mon, 13 Jul 2026 13:35:22 -0400 Subject: [PATCH 2/3] Add conformance vectors for all DIS PDU families --- .../Conformance/DeterministicPduFixture.cs | 172 +++++++++ .../FamilyConformanceVectorTests.cs | 156 ++++++++ .../Conformance/Vectors/README.md | 25 ++ .../Vectors/opendis7-java-default.json | 362 ++++++++++++++++++ .../Vectors/opendis7-java-populated.json | 362 ++++++++++++++++++ .../OpenDisNet.Tests/OpenDisNet.Tests.csproj | 4 + 6 files changed, 1081 insertions(+) create mode 100644 tests/OpenDisNet.Tests/Conformance/DeterministicPduFixture.cs create mode 100644 tests/OpenDisNet.Tests/Conformance/FamilyConformanceVectorTests.cs create mode 100644 tests/OpenDisNet.Tests/Conformance/Vectors/README.md create mode 100644 tests/OpenDisNet.Tests/Conformance/Vectors/opendis7-java-default.json create mode 100644 tests/OpenDisNet.Tests/Conformance/Vectors/opendis7-java-populated.json diff --git a/tests/OpenDisNet.Tests/Conformance/DeterministicPduFixture.cs b/tests/OpenDisNet.Tests/Conformance/DeterministicPduFixture.cs new file mode 100644 index 0000000..3d55970 --- /dev/null +++ b/tests/OpenDisNet.Tests/Conformance/DeterministicPduFixture.cs @@ -0,0 +1,172 @@ +using System.Collections; +using System.Reflection; +using OpenDisNet.Pdus; +using OpenDisNet.Protocol; + +namespace OpenDisNet.Tests.Conformance; + +internal static class DeterministicPduFixture +{ + private static readonly HashSet NeverPopulate = + [ + nameof(Pdu.ProtocolVersion), + nameof(Pdu.PduType), + nameof(Pdu.ProtocolFamily), + nameof(Pdu.Length), + nameof(Pdu.Header), + "PduStatus", + "Padding", + "Radio", + "DataBitLength", + "RecordLength", + ]; + + public static Pdu Create(PduType type) + { + Pdu pdu = PduFactory.Create(type, exerciseId: 7); + pdu.Timestamp = 0x01020304; + Populate(pdu, new HashSet(ReferenceEqualityComparer.Instance), depth: 0); + ApplyPduRules(pdu); + return pdu; + } + + private static void Populate(object instance, HashSet visited, int depth) + { + if (depth > 12 || !visited.Add(instance)) return; + + foreach (PropertyInfo property in instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + if (!property.CanRead || !property.CanWrite || property.GetIndexParameters().Length != 0) continue; + if (NeverPopulate.Contains(property.Name) || property.Name.Contains("Padding", StringComparison.OrdinalIgnoreCase) + || property.Name.StartsWith("Pad", StringComparison.OrdinalIgnoreCase)) continue; + + Type type = property.PropertyType; + object? current = property.GetValue(instance); + + if (type.IsArray) + { + Type elementType = type.GetElementType()!; + int length = current is Array existing && existing.Length != 0 ? existing.Length : 1; + Array values = Array.CreateInstance(elementType, length); + for (int index = 0; index < length; index++) + values.SetValue(CreateValue(elementType, depth + 1, index + 1, visited), index); + property.SetValue(instance, values); + continue; + } + + if (current is IList list && type.IsGenericType) + { + Type elementType = type.GetGenericArguments()[0]; + list.Clear(); + list.Add(CreateValue(elementType, depth + 1, 1, visited)); + continue; + } + + if (IsScalar(type)) + { + property.SetValue(instance, CreateScalar(type, property.Name)); + continue; + } + + if (type.IsValueType) + { + property.SetValue(instance, CreateValue(type, depth + 1, 1, visited)); + continue; + } + + object child = current ?? Activator.CreateInstance(type) + ?? throw new InvalidOperationException($"Cannot create fixture value for {type}."); + property.SetValue(instance, child); + Populate(child, visited, depth + 1); + } + } + + private static object CreateValue(Type type, int depth, int ordinal, HashSet visited) + { + if (IsScalar(type)) return CreateScalar(type, string.Empty, ordinal); + if (type.IsValueType) + { + ConstructorInfo? constructor = type.GetConstructors().SingleOrDefault(x => x.GetParameters().Length == 1); + if (constructor is not null) + { + Type parameterType = constructor.GetParameters()[0].ParameterType; + return constructor.Invoke([CreateScalar(parameterType, string.Empty, ordinal)]); + } + return Activator.CreateInstance(type)!; + } + + object value = Activator.CreateInstance(type) + ?? throw new InvalidOperationException($"Cannot create fixture value for {type}."); + Populate(value, visited, depth); + return value; + } + + private static bool IsScalar(Type type) => type.IsEnum || type == typeof(byte) || type == typeof(sbyte) + || type == typeof(ushort) || type == typeof(short) || type == typeof(uint) || type == typeof(int) + || type == typeof(ulong) || type == typeof(long) || type == typeof(float) || type == typeof(double); + + private static object CreateScalar(Type type, string propertyName, int ordinal = 1) + { + if (type.IsEnum) + { + Array values = Enum.GetValues(type); + return values.Cast().FirstOrDefault(x => Convert.ToUInt64(x) != 0) ?? values.GetValue(0)!; + } + + int value = propertyName.Contains("Count", StringComparison.OrdinalIgnoreCase) + || propertyName.StartsWith("NumberOf", StringComparison.OrdinalIgnoreCase) ? 1 : ordinal + 1; + if (type == typeof(byte)) return checked((byte)value); + if (type == typeof(sbyte)) return checked((sbyte)value); + if (type == typeof(ushort)) return checked((ushort)value); + if (type == typeof(short)) return checked((short)value); + if (type == typeof(uint)) return checked((uint)value); + if (type == typeof(int)) return value; + if (type == typeof(ulong)) return checked((ulong)value); + if (type == typeof(long)) return checked((long)value); + if (type == typeof(float)) return value + 0.25f; + if (type == typeof(double)) return value + 0.5d; + throw new InvalidOperationException($"Unsupported scalar fixture type {type}."); + } + + private static void ApplyPduRules(Pdu pdu) + { + switch (pdu) + { + case SignalPdu signal: + signal.SetData([0xA5, 0xC0], meaningfulBitLength: 10); + signal.SampleCount = 2; + break; + case IntercomSignalPdu intercom: + intercom.Data = [0xA5, 0xC0]; + intercom.DataBitLength = 10; + intercom.SampleCount = 2; + break; + case InformationOperationsActionPdu action: + action.IoRecords = [CreateIoEffectRecord()]; + break; + case InformationOperationsReportPdu report: + report.IoRecords = [CreateIoCommsNodeRecord()]; + break; + case IdentificationFriendOrFoePdu iff: + iff.IFFPduLayer3InterrogatorFormatData = null; + iff.IFFPduLayer4InterrogatorFormatData = null; + break; + } + } + + private static IOEffectRecord CreateIoEffectRecord() + { + var value = new IOEffectRecord(); + Populate(value, new HashSet(ReferenceEqualityComparer.Instance), depth: 0); + value.RecordType = OpenDisNet.Enumerations.VariableRecordType.IoEffect; + return value; + } + + private static IOCommsNodeRecord CreateIoCommsNodeRecord() + { + var value = new IOCommsNodeRecord(); + Populate(value, new HashSet(ReferenceEqualityComparer.Instance), depth: 0); + value.RecordType = OpenDisNet.Enumerations.VariableRecordType.IoCommunicationsNode; + return value; + } +} diff --git a/tests/OpenDisNet.Tests/Conformance/FamilyConformanceVectorTests.cs b/tests/OpenDisNet.Tests/Conformance/FamilyConformanceVectorTests.cs new file mode 100644 index 0000000..e942295 --- /dev/null +++ b/tests/OpenDisNet.Tests/Conformance/FamilyConformanceVectorTests.cs @@ -0,0 +1,156 @@ +using System.Text.Json; +using OpenDisNet.Pdus; +using OpenDisNet.Protocol; + +namespace OpenDisNet.Tests.Conformance; + +public sealed class FamilyConformanceVectorTests +{ + // These fixtures expose known defects in the reference generator's + // variable-length/padding output. Keep them explicit instead of weakening + // the native C# codec to reproduce non-conformant bytes. + private static readonly HashSet PopulatedReferenceIncompatibilities = + [ + 16, 17, 18, 19, 20, 21, 22, 25, 32, 33, 34, 35, 39, 41, + 43, 44, 45, 56, 57, 58, 59, 60, 61, 62, 63, 64, 68, 72, + ]; + + public static TheoryData AllFamilies => new() + { + { 1, "0.3 Entity Information" }, { 2, "0.3 Warfare" }, + { 3, "0.4 Logistics" }, { 5, "0.4 Simulation Management" }, + { 6, "0.5 Distributed Emissions" }, { 4, "0.5 Radio Communications" }, + { 7, "0.6 Entity Management" }, { 8, "0.6 Minefield" }, + { 9, "0.7 Synthetic Environment" }, { 10, "0.7 Simulation Management with Reliability" }, + { 11, "0.8 Live Entity" }, { 13, "0.8 Information Operations" }, + }; + + public static TheoryData StandardPduTypes => + new(Enumerable.Range(1, 72).Select(x => (byte)x)); + + public static TheoryData OpenDisJavaVectors + { + get + { + var data = new TheoryData(); + foreach (OpenDisJavaVector vector in ReadOpenDisJavaVectors().Where(x => x.Type is not 43 and not 45)) + data.Add((byte)vector.Type, vector.Name, vector.Hex); + return data; + } + } + + public static TheoryData OpenDisJavaPopulatedVectors + { + get + { + var data = new TheoryData(); + foreach (OpenDisJavaVector vector in ReadOpenDisJavaVectors("opendis7-java-populated.json") + .Where(x => !PopulatedReferenceIncompatibilities.Contains(x.Type))) + data.Add((byte)vector.Type, vector.Name, vector.Hex); + return data; + } + } + + [Theory] + [MemberData(nameof(StandardPduTypes))] + public void PopulatedPduRoundTripsByteIdentically(byte value) + { + Pdu original = DeterministicPduFixture.Create((PduType)value); + + byte[] bytes = DisSerializer.Serialize(original); + Pdu decoded = Assert.IsAssignableFrom(DisSerializer.Deserialize(bytes)); + + Assert.Equal(original.GetType(), decoded.GetType()); + Assert.Equal(bytes, DisSerializer.Serialize(decoded)); + Assert.True(bytes.Length > 12); + } + + [Theory] + [MemberData(nameof(OpenDisJavaVectors))] + public void IndependentOpenDisJavaVectorRoundTrips(byte value, string referenceName, string hex) + { + byte[] bytes = Convert.FromHexString(hex); + Pdu decoded = Assert.IsAssignableFrom(DisSerializer.Deserialize(bytes)); + + Assert.Equal((PduType)value, decoded.PduType); + Assert.Equal(bytes, DisSerializer.Serialize(decoded)); + Assert.False(string.IsNullOrWhiteSpace(referenceName)); + } + + [Theory] + [MemberData(nameof(OpenDisJavaPopulatedVectors))] + public void IndependentPopulatedOpenDisJavaVectorRoundTrips(byte value, string referenceName, string hex) + { + byte[] bytes = Convert.FromHexString(hex); + Pdu decoded = Assert.IsAssignableFrom(DisSerializer.Deserialize(bytes)); + + Assert.Equal((PduType)value, decoded.PduType); + Assert.Equal(bytes, DisSerializer.Serialize(decoded)); + Assert.False(string.IsNullOrWhiteSpace(referenceName)); + } + + [Theory] + [MemberData(nameof(AllFamilies))] + public void EveryByteBoundaryRejectsTruncation(byte family, string milestone) + { + Pdu[] pdus = Enumerable.Range(1, 72) + .Select(x => DeterministicPduFixture.Create((PduType)x)) + .Where(x => (byte)x.ProtocolFamily == family) + .ToArray(); + Assert.NotEmpty(pdus); + + foreach (Pdu pdu in pdus) + { + byte[] bytes = DisSerializer.Serialize(pdu); + for (int length = 0; length < bytes.Length; length++) + { + bool parsed = DisSerializer.TryDeserialize(bytes.AsSpan(0, length), out _, out DisParseError error); + Assert.False(parsed, $"{milestone}: {pdu.GetType().Name} accepted truncation at byte {length}."); + Assert.NotEqual(DisParseErrorCode.None, error.Code); + } + } + } + + [Fact] + public void IndependentVectorSetCoversEveryStandardPduExactlyOnce() + { + OpenDisJavaVector[] vectors = ReadOpenDisJavaVectors(); + Assert.Equal(72, vectors.Length); + Assert.Equal(Enumerable.Range(1, 72), vectors.Select(x => x.Type).Order()); + } + + [Fact] + public void CompatiblePopulatedReferenceVectorsCoverEveryPduFamily() + { + OpenDisJavaVector[] vectors = ReadOpenDisJavaVectors("opendis7-java-populated.json") + .Where(x => !PopulatedReferenceIncompatibilities.Contains(x.Type)) + .ToArray(); + + Assert.Equal(44, vectors.Length); + Assert.Equal( + AllFamilies.Select(row => (byte)row[0]).Order(), + vectors.Select(x => Convert.FromHexString(x.Hex)[3]).Distinct().Order()); + } + + [Theory] + [InlineData(43)] + [InlineData(45)] + public void OpenDisJavaBitfieldWidthDifferencesRemainExplicit(int type) + { + OpenDisJavaVector vector = Assert.Single(ReadOpenDisJavaVectors(), x => x.Type == type); + + Assert.False(DisSerializer.TryDeserialize(Convert.FromHexString(vector.Hex), out _, out DisParseError error)); + Assert.Equal(DisParseErrorCode.InvalidField, error.Code); + } + + private static OpenDisJavaVector[] ReadOpenDisJavaVectors(string fileName = "opendis7-java-default.json") + { + string path = Path.Combine(AppContext.BaseDirectory, "Conformance", "Vectors", fileName); + return JsonSerializer.Deserialize(File.ReadAllText(path), new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + }) ?? throw new InvalidDataException("The Open-DIS Java conformance vector set is empty."); + } + + private sealed record OpenDisJavaVector(int Type, string Name, string Hex); +} diff --git a/tests/OpenDisNet.Tests/Conformance/Vectors/README.md b/tests/OpenDisNet.Tests/Conformance/Vectors/README.md new file mode 100644 index 0000000..3d71832 --- /dev/null +++ b/tests/OpenDisNet.Tests/Conformance/Vectors/README.md @@ -0,0 +1,25 @@ +# Independent DIS v7 vectors + +These are test-only interoperability fixtures. The OpenDisNet library and +NuGet package contain no Java code, Java dependency, Java runtime requirement, +or Java-facing API. Open-DIS Java was used only as an offline independent source +of wire bytes so the native C# codec is not tested solely against itself. + +`opendis7-java-populated.json` was marshalled by the NPS MOVES +`open-dis/opendis7-java` implementation at commit +`9493e02f69e3473d7887b00c9b72aa5fbc56d155`. + +The default set covers all 72 standardized PDU classes. Signal data is +normalized to an empty array when its declared bit length is zero. The Point +Object State and Areal Object State vectors are retained as explicit +differential cases: the Java generator writes their Modifications fields as 16 +bits, while its own DIS layout XML declares those fields as 8 bits. The other +70 default vectors must decode and re-encode byte-identically. + +The populated set assigns deterministic non-zero primitive, enumeration, +record, array, and list values where the Java wire model permits them. These +octets are independent differential inputs and are not generated or rewritten +by OpenDisNet. + +The vector provenance is intentionally pinned so a future reference-library +change cannot silently change the conformance baseline. diff --git a/tests/OpenDisNet.Tests/Conformance/Vectors/opendis7-java-default.json b/tests/OpenDisNet.Tests/Conformance/Vectors/opendis7-java-default.json new file mode 100644 index 0000000..a6397b3 --- /dev/null +++ b/tests/OpenDisNet.Tests/Conformance/Vectors/opendis7-java-default.json @@ -0,0 +1,362 @@ +[ + { + "type": 1, + "name": "EntityStatePdu", + "hex": "070701010102030400900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 2, + "name": "FirePdu", + "hex": "070702020102030400600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 3, + "name": "DetonationPdu", + "hex": "0707030201020304006800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 4, + "name": "CollisionPdu", + "hex": "0707040101020304003c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 5, + "name": "ServiceRequestPdu", + "hex": "0707050301020304001c000000000000000000000000000000000000" + }, + { + "type": 6, + "name": "ResupplyOfferPdu", + "hex": "0707060301020304001c000000000000000000000000000000000000" + }, + { + "type": 7, + "name": "ResupplyReceivedPdu", + "hex": "0707070301020304001c000000000000000000000000000000000000" + }, + { + "type": 8, + "name": "ResupplyCancelPdu", + "hex": "070708030102030400180000000000000000000000000000" + }, + { + "type": 9, + "name": "RepairCompletePdu", + "hex": "0707090301020304001c000000000000000000000000000000000000" + }, + { + "type": 10, + "name": "RepairResponsePdu", + "hex": "07070a0301020304001c000000000000000000000000000000000000" + }, + { + "type": 11, + "name": "CreateEntityPdu", + "hex": "07070b0501020304001c000000000000000000000000000000000000" + }, + { + "type": 12, + "name": "RemoveEntityPdu", + "hex": "07070c0501020304001c000000000000000000000000000000000000" + }, + { + "type": 13, + "name": "StartResumePdu", + "hex": "07070d0501020304002c00000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 14, + "name": "StopFreezePdu", + "hex": "07070e05010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 15, + "name": "AcknowledgePdu", + "hex": "07070f0501020304002000000000000000000000000000000001000000000000" + }, + { + "type": 16, + "name": "ActionRequestPdu", + "hex": "07071005010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 17, + "name": "ActionResponsePdu", + "hex": "07071105010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 18, + "name": "DataQueryPdu", + "hex": "07071205010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 19, + "name": "SetDataPdu", + "hex": "07071305010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 20, + "name": "DataPdu", + "hex": "07071405010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 21, + "name": "EventReportPdu", + "hex": "07071505010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 22, + "name": "CommentPdu", + "hex": "0707160501020304002000000000000000000000000000000000000000000000" + }, + { + "type": 23, + "name": "ElectromagneticEmissionPdu", + "hex": "0707170601020304001c000000000000000000000000000000000000" + }, + { + "type": 24, + "name": "DesignatorPdu", + "hex": "07071806010203040058000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 25, + "name": "TransmitterPdu", + "hex": "0707190401020304006800000000000000000000070000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 26, + "name": "SignalPdu", + "hex": "07071a0401020304002000000000000000000000000000000000000000000000" + }, + { + "type": 27, + "name": "ReceiverPdu", + "hex": "07071b040102030400240000000000000000000000000000000000000000000000000000" + }, + { + "type": 28, + "name": "IFFPdu", + "hex": "07071c0601020304003c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 29, + "name": "UnderwaterAcousticPdu", + "hex": "07071d0601020304002000000000000000000000000000000000000000000000" + }, + { + "type": 30, + "name": "SEESPdu", + "hex": "07071e0601020304001c000000000000000000000000000000000000" + }, + { + "type": 31, + "name": "IntercomSignalPdu", + "hex": "07071f0401020304002000000000000000000000000000000000000000000000" + }, + { + "type": 32, + "name": "IntercomControlPdu", + "hex": "07072004010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 33, + "name": "AggregateStatePdu", + "hex": "07072107010203040088000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 34, + "name": "IsGroupOfPdu", + "hex": "07072207010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 35, + "name": "TransferOwnershipPdu", + "hex": "07072307010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 36, + "name": "IsPartOfPdu", + "hex": "07072407010203040034000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 37, + "name": "MinefieldStatePdu", + "hex": "070725080102030400480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 38, + "name": "MinefieldQueryPdu", + "hex": "07072608010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 39, + "name": "MinefieldDataPdu", + "hex": "0707270801020304002c00000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 40, + "name": "MinefieldResponseNACKPdu", + "hex": "0707280801020304001a00000000000000000000000000000000" + }, + { + "type": 41, + "name": "EnvironmentalProcessPdu", + "hex": "0707290901020304002000000000000000000000000000000000000000000000" + }, + { + "type": 42, + "name": "GriddedDataPdu", + "hex": "07072a09010203040040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 43, + "name": "PointObjectStatePdu", + "hex": "07072b0901020304005b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 44, + "name": "LinearObjectStatePdu", + "hex": "07072c09010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 45, + "name": "ArealObjectStatePdu", + "hex": "07072d09010203040031000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 46, + "name": "TSPIPdu", + "hex": "07072e0b0102030400360000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 47, + "name": "AppearancePdu", + "hex": "07072f0b010203040043000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 48, + "name": "ArticulatedPartsPdu", + "hex": "0707300b01020304001100000000000000" + }, + { + "type": 49, + "name": "LEFirePdu", + "hex": "0707310b010203040043000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 50, + "name": "LEDetonationPdu", + "hex": "0707320b01020304004f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 51, + "name": "CreateEntityReliablePdu", + "hex": "0707330a01020304002000000000000000000000000000000000000000000000" + }, + { + "type": 52, + "name": "RemoveEntityReliablePdu", + "hex": "0707340a01020304002000000000000000000000000000000000000000000000" + }, + { + "type": 53, + "name": "StartResumeReliablePdu", + "hex": "0707350a0102030400300000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 54, + "name": "StopFreezeReliablePdu", + "hex": "0707360a010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 55, + "name": "AcknowledgeReliablePdu", + "hex": "0707370a01020304002000000000000000000000000000000001000000000000" + }, + { + "type": 56, + "name": "ActionRequestReliablePdu", + "hex": "0707380a01020304002c00000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 57, + "name": "ActionResponseReliablePdu", + "hex": "0707390a010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 58, + "name": "DataQueryReliablePdu", + "hex": "07073a0a01020304002c00000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 59, + "name": "SetDataReliablePdu", + "hex": "07073b0a010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 60, + "name": "DataReliablePdu", + "hex": "07073c0a010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 61, + "name": "EventReportReliablePdu", + "hex": "07073d0a010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 62, + "name": "CommentReliablePdu", + "hex": "07073e0a01020304002000000000000000000000000000000000000000000000" + }, + { + "type": 63, + "name": "RecordReliablePdu", + "hex": "07073f0a0102030400240000000000000000000000000000000000000000000000000000" + }, + { + "type": 64, + "name": "SetRecordReliablePdu", + "hex": "0707400a010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 65, + "name": "RecordQueryReliablePdu", + "hex": "0707410a010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 66, + "name": "CollisionElasticPdu", + "hex": "07074201010203040064000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 67, + "name": "EntityStateUpdatePdu", + "hex": "070743010102030400480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 68, + "name": "DirectedEnergyFirePdu", + "hex": "07074402010203040058000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 69, + "name": "EntityDamageStatusPdu", + "hex": "070745020102030400180000000000000000000000000000" + }, + { + "type": 70, + "name": "InformationOperationsActionPdu", + "hex": "0707460d01020304003800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 71, + "name": "InformationOperationsReportPdu", + "hex": "0707470d010203040028000000000000000000000000000000000000000000000000000000000000" + }, + { + "type": 72, + "name": "AttributePdu", + "hex": "0707480101020304002000000000000000000000000000000000000100000000" + } +] diff --git a/tests/OpenDisNet.Tests/Conformance/Vectors/opendis7-java-populated.json b/tests/OpenDisNet.Tests/Conformance/Vectors/opendis7-java-populated.json new file mode 100644 index 0000000..f03b098 --- /dev/null +++ b/tests/OpenDisNet.Tests/Conformance/Vectors/opendis7-java-populated.json @@ -0,0 +1,362 @@ +[ + { + "type": 1, + "name": "EntityStatePdu", + "hex": "070701010102030400a0020000020002000201010100000002020202010000000202020240100000401000004010000040040000000000004004000000000000400400000000000040100000401000004010000000000002014142434445464748494a4b4c4d4e4f401000004010000040100000401000004010000040100000014142434445464748494a4b00000000004142434445464748494a4b4c4d4e4f" + }, + { + "type": 2, + "name": "FirePdu", + "hex": "070702020102030400600200000200020002000200020002000200020002000200020002000000024004000000000000400400000000000040040000000000000100000002020202000800080002000240100000401000004010000040100000" + }, + { + "type": 3, + "name": "DetonationPdu", + "hex": "0707030201020304007802000002000200020002000200020002000200020002000200024010000040100000401000004004000000000000400400000000000040040000000000000100000002020202000800080002000240100000401000004010000001010000004142434445464748494a4b4c4d4e4f" + }, + { + "type": 4, + "name": "CollisionPdu", + "hex": "0707040101020304003c0200000200020002000200020002000200020002010040100000401000004010000040100000401000004010000040100000" + }, + { + "type": 5, + "name": "ServiceRequestPdu", + "hex": "07070503010203040028020000020002000200020002000201010000010000000202020240100000" + }, + { + "type": 6, + "name": "ResupplyOfferPdu", + "hex": "07070603010203040028020000020002000200020002000201000000010000000202020240100000" + }, + { + "type": 7, + "name": "ResupplyReceivedPdu", + "hex": "07070703010203040028020000020002000200020002000201000000010000000202020240100000" + }, + { + "type": 8, + "name": "ResupplyCancelPdu", + "hex": "070708030102030400180200000200020002000200020002" + }, + { + "type": 9, + "name": "RepairCompletePdu", + "hex": "0707090301020304001c020000020002000200020002000200010000" + }, + { + "type": 10, + "name": "RepairResponsePdu", + "hex": "07070a0301020304001c020000020002000200020002000201000000" + }, + { + "type": 11, + "name": "CreateEntityPdu", + "hex": "07070b0501020304001c020000020002000200020002000200000002" + }, + { + "type": 12, + "name": "RemoveEntityPdu", + "hex": "07070c0501020304001c020000020002000200020002000200000002" + }, + { + "type": 13, + "name": "StartResumePdu", + "hex": "07070d0501020304002c02000002000200020002000200020000000200000002000000020000000200000002" + }, + { + "type": 14, + "name": "StopFreezePdu", + "hex": "07070e05010203040028020000020002000200020002000200000002000000020100000000000002" + }, + { + "type": 15, + "name": "AcknowledgePdu", + "hex": "07070f0501020304002002000002000200020002000200020002000100000002" + }, + { + "type": 16, + "name": "ActionRequestPdu", + "hex": "07071005010203040039020000020002000200020002000200000002000000010000000100000001000003e900000002000003e9000000084100000000000000" + }, + { + "type": 17, + "name": "ActionResponsePdu", + "hex": "07071105010203040039020000020002000200020002000200000002000000010000000100000001000003e900000002000003e9000000084100000000000000" + }, + { + "type": 18, + "name": "DataQueryPdu", + "hex": "07071205010203040039020000020002000200020002000200000002000000020000000100000001000003e900000002000003e9000000084100000000000000" + }, + { + "type": 19, + "name": "SetDataPdu", + "hex": "07071305010203040039020000020002000200020002000200000002000000000000000100000001000003e900000002000003e9000000084100000000000000" + }, + { + "type": 20, + "name": "DataPdu", + "hex": "07071405010203040039020000020002000200020002000200000002000000000000000100000001000003e900000002000003e9000000084100000000000000" + }, + { + "type": 21, + "name": "EventReportPdu", + "hex": "07071505010203040039020000020002000200020002000200000002000000000000000100000001000003e900000002000003e9000000084100000000000000" + }, + { + "type": 22, + "name": "CommentPdu", + "hex": "0707160501020304002902000002000200020002000200020000000000000001000003e9000000084100000000000000" + }, + { + "type": 23, + "name": "ElectromagneticEmissionPdu", + "hex": "0707170601020304006c0200000200020002000200020002010100000001000000030102401000004010000040100000000200024010000040100000401000004010000040100000401000004010000040100000401000004010000001010101020202020002000200020202" + }, + { + "type": 24, + "name": "DesignatorPdu", + "hex": "07071806010203040058020000020002000203e80002000200020000401000000000000040100000401000004010000040040000000000004004000000000000400400000000000001000000401000004010000040100000" + }, + { + "type": 25, + "name": "TransmitterPdu", + "hex": "070719040102030400700200000200020002000201020000010102020101000040040000000000004004000000000000400400000000000040100000401000004010000000010001000000000000000240100000401000000002000100020001000100020100000041000000000000000000000100004100" + }, + { + "type": 26, + "name": "SignalPdu", + "hex": "07071a0401020304002002000002000200020002000200010000000200000002" + }, + { + "type": 27, + "name": "ReceiverPdu", + "hex": "07071b040102030400240200000200020002000200010000401000000002000200020002" + }, + { + "type": 28, + "name": "IFFPdu", + "hex": "07071c0601020304003c0200000200020002000200020002401000004010000040100000000100010102020202020202000200020002000200020002" + }, + { + "type": 29, + "name": "UnderwaterAcousticPdu", + "hex": "07071d06010203040058020000020002000200020002000201000000020101010002000200000002000200020001000000010102401000004010000040100000000200000000000140100000401000004010000040100000" + }, + { + "type": 30, + "name": "SEESPdu", + "hex": "07071e0601020304002c02000002000200020002000200020001000140100000401000004010000040100000" + }, + { + "type": 31, + "name": "IntercomSignalPdu", + "hex": "07071f0401020304002002000002000200020002000200010000000200000002" + }, + { + "type": 32, + "name": "IntercomControlPdu", + "hex": "0707200401020304002d0200010200020002000200020202010100020002000200020002000000010001000041000000" + }, + { + "type": 33, + "name": "AggregateStatePdu", + "hex": "070721070102030400ad02000002000200020101010100000201010200000001014142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f40100000401000004010000040100000401000004010000040040000000000004004000000000000400400000000000040100000401000004010000000010001000100010002000200020002000200020100000002020202010000000202020200000001000003e90000000841000000" + }, + { + "type": 34, + "name": "IsGroupOfPdu", + "hex": "07072207010203040031020000020002000201010000000040040000000000004004000000000000000003e9000000084100000000000000" + }, + { + "type": 35, + "name": "TransferOwnershipPdu", + "hex": "07072307010203040039020000020002000200020002000200000002010100020002000200000001000003e900000002000000000000000041" + }, + { + "type": 36, + "name": "IsPartOfPdu", + "hex": "07072407010203040034020000020002000200020002000200010001401000004010000040100000000100020100000002020202" + }, + { + "type": 37, + "name": "MinefieldStatePdu", + "hex": "07072508010203040058020000020002000200020101010000000202020200014004000000000000400400000000000040040000000000004010000040100000401000000000000240100000401000000100000002020202" + }, + { + "type": 38, + "name": "MinefieldQueryPdu", + "hex": "0707260801020304003202000002000200020002000200020201000100000002010000000202020240100000401000000002" + }, + { + "type": 39, + "name": "MinefieldDataPdu", + "hex": "070727080102030400680200000200020002000200020002000202020000010000000002010000000202020200020000401000004010000040100000000000000000000000000000401000004010000040100000000000000000000000000002000000020000000041000000" + }, + { + "type": 40, + "name": "MinefieldResponseNACKPdu", + "hex": "0707280801020304001b0200000200020002000200020002020141" + }, + { + "type": 41, + "name": "EnvironmentalProcessPdu", + "hex": "070729090102030400290200000200020002010000000202020200000001000200000103000002004100000000000000" + }, + { + "type": 42, + "name": "GriddedDataPdu", + "hex": "07072a09010203040058020000020002000200020002000200010101010000000202020240100000401000004010000000000002000000020000000202000000400400000000000040040000000000000002020100000001" + }, + { + "type": 43, + "name": "PointObjectStatePdu", + "hex": "07072b0901020304005b020000020002000200020002000200000002010000010102024004000000000000400400000000000040040000000000004010000040100000401000000000000200000000000200020002000200000000" + }, + { + "type": 44, + "name": "LinearObjectStatePdu", + "hex": "07072c090102030400690200000200020002000200020002000201010002000200020002010102020200000000000000024004000000000000400400000000000040040000000000004010000040100000401000000000000040100000401000004010000000000000" + }, + { + "type": 45, + "name": "ArealObjectStatePdu", + "hex": "07072d09010203040049020000020002000200020002000200020100000101020200000002000200010002000200020002400400000000000040040000000000004004000000000000" + }, + { + "type": 46, + "name": "TSPIPdu", + "hex": "07072e0b010203040037000200020002020002000200020002000200020002020202000200020002000200020102020202020200020141" + }, + { + "type": 47, + "name": "AppearancePdu", + "hex": "07072f0b01020304004300020002000200020101000000020202020100000002020202014142434445464748494a4b0000000000000002000000020000000200000002" + }, + { + "type": 48, + "name": "ArticulatedPartsPdu", + "hex": "0707300b01020304002100020002000201004142434445464748494a4b4c4d4e4f" + }, + { + "type": 49, + "name": "LEFirePdu", + "hex": "0707310b010203040043000200020002020002000200020002000200020002000200020002000200020002010000000202020200080008000200020002000200020002" + }, + { + "type": 50, + "name": "LEDetonationPdu", + "hex": "0707320b01020304004f000200020002020200020002000200020002000200020002000200020002000200020002000200020002000200020100000002020202000800080002000200020002000202" + }, + { + "type": 51, + "name": "CreateEntityReliablePdu", + "hex": "0707330a01020304002002000002000200020002000200020100000000000002" + }, + { + "type": 52, + "name": "RemoveEntityReliablePdu", + "hex": "0707340a01020304002002000002000200020002000200020100000000000002" + }, + { + "type": 53, + "name": "StartResumeReliablePdu", + "hex": "0707350a0102030400300200000200020002000200020002000000020000000200000002000000020100000000000002" + }, + { + "type": 54, + "name": "StopFreezeReliablePdu", + "hex": "0707360a010203040028020000020002000200020002000200000002000000020100010000000002" + }, + { + "type": 55, + "name": "AcknowledgeReliablePdu", + "hex": "0707370a01020304002002000002000200020002000200020002000100000002" + }, + { + "type": 56, + "name": "ActionRequestReliablePdu", + "hex": "0707380a01020304003d02000002000200020002000200020100000000000002000000010000000100000001000003e900000002000003e90000000841000000" + }, + { + "type": 57, + "name": "ActionResponseReliablePdu", + "hex": "0707390a010203040039020000020002000200020002000200000002000000010000000100000001000003e900000002000003e9000000084100000000000000" + }, + { + "type": 58, + "name": "DataQueryReliablePdu", + "hex": "07073a0a01020304003d02000002000200020002000200020100000000000002000000020000000100000001000003e900000002000003e90000000841000000" + }, + { + "type": 59, + "name": "SetDataReliablePdu", + "hex": "07073b0a010203040039020000020002000200020002000201000000000000020000000100000001000003e900000002000003e9000000084100000000000000" + }, + { + "type": 60, + "name": "DataReliablePdu", + "hex": "07073c0a010203040039020000020002000200020002000200000002010000000000000100000001000003e900000002000003e9000000084100000000000000" + }, + { + "type": 61, + "name": "EventReportReliablePdu", + "hex": "07073d0a010203040039020000020002000200020002000200000002000000000000000100000001000003e900000002000003e9000000084100000000000000" + }, + { + "type": 62, + "name": "CommentReliablePdu", + "hex": "07073e0a01020304002902000002000200020002000200020000000000000001000003e9000000084100000000000000" + }, + { + "type": 63, + "name": "RecordReliablePdu", + "hex": "07073f0a010203040039020000020002000200020002000200000002010000000000000100000001000003e900000002000000000000000041" + }, + { + "type": 64, + "name": "SetRecordReliablePdu", + "hex": "0707400a01020304003d02000002000200020002000200020000000201000000000000000000000100000001000003e900000002000000000000000041" + }, + { + "type": 65, + "name": "RecordQueryReliablePdu", + "hex": "0707410a01020304003002000002000200020002000200020000000201000001000000020000000100000001000003e9" + }, + { + "type": 66, + "name": "CollisionElasticPdu", + "hex": "07074201010203040064020000020002000200020002000200020002000200004010000040100000401000004010000040100000401000004010000040100000401000004010000040100000401000004010000040100000401000004010000040100000" + }, + { + "type": 67, + "name": "EntityStateUpdatePdu", + "hex": "070743010102030400580200000200020002000140100000401000004010000040040000000000004004000000000000400400000000000040100000401000004010000000000002004142434445464748494a4b4c4d4e4f" + }, + { + "type": 68, + "name": "DirectedEnergyFirePdu", + "hex": "0707440201020304006102000002000200020002000200020100000002020202000000020000000240100000401000004010000040100000401000000000000000000000401000004010000000000100000000000000000100010000000100014100000000000000" + }, + { + "type": 69, + "name": "EntityDamageStatusPdu", + "hex": "07074502010203040040020000020002000200000000000100001194002800004010000040100000401000004010000040100000010100010002000200020000" + }, + { + "type": 70, + "name": "InformationOperationsActionPdu", + "hex": "0707460d010203040048020000020002000200020002000200000002000100000001000100000000000200020002000200020002000000010000157c001001010102000200000000" + }, + { + "type": 71, + "name": "InformationOperationsReportPdu", + "hex": "0707470d0102030400380200000200020002000001000002000200020002000200020000000000010000157c001001010102000200000000" + }, + { + "type": 72, + "name": "AttributePdu", + "hex": "0707480101020304002f0200000200020000000000000101000003e90000000100020002000200010000000000014100" + } +] diff --git a/tests/OpenDisNet.Tests/OpenDisNet.Tests.csproj b/tests/OpenDisNet.Tests/OpenDisNet.Tests.csproj index a56e26b..a5870e9 100644 --- a/tests/OpenDisNet.Tests/OpenDisNet.Tests.csproj +++ b/tests/OpenDisNet.Tests/OpenDisNet.Tests.csproj @@ -23,4 +23,8 @@ + + + + From ffeeece6976a91207204c4888922a4ab6b28cf7e Mon Sep 17 00:00:00 2001 From: RejectKid Date: Mon, 13 Jul 2026 13:35:26 -0400 Subject: [PATCH 3/3] Document 0.3 through 0.8 conformance coverage --- CHANGELOG.md | 8 ++++++++ README.md | 7 +++++-- docs/conformance.md | 37 ++++++++++++++++++++++--------------- docs/standards.md | 6 ++++-- 4 files changed, 39 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 073e550..857b5ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ Versioning after 1.0 and uses prerelease versions while conformance is incomplet ## Unreleased +- Added deterministic populated conformance cases for all 72 DIS v7 PDU types, + independent byte vectors, and truncation checks at every byte boundary across + all 12 protocol families planned for versions 0.3 through 0.8. +- Corrected the 10-byte Live Entity header layout, offset-aware 64-bit padding, + per-field enumeration/bitfield widths, and synchronization of variable record + lengths and counts. +- Added conditional IFF information layers and polymorphic Information + Operations effect/communications records to the native C# codec. - Added strongly typed SISO-REF-010-2025 v36 enumerations for every DIS v7 schema reference, with .NET-style public names. - Added structured bitfield value types with validated accessors, immutable diff --git a/README.md b/README.md index 56df1f8..e30d446 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,9 @@ OpenDisNet is a high-performance, type-safe .NET codec for Distributed Interactive Simulation (DIS) Protocol Version 7, defined by IEEE 1278.1-2012. > **Development status:** pre-release. All 72 DIS v7 PDU identifiers have typed -> models and generated binary codecs. Family-level semantic conformance and -> independent vectors are tracked in [`docs/conformance.md`](docs/conformance.md). +> models and native C# binary codecs. Every protocol family has populated +> round-trip and boundary-truncation coverage; remaining 1.0 release gates are +> tracked in [`docs/conformance.md`](docs/conformance.md). ## Install @@ -99,6 +100,8 @@ through `Value`; parsing and reserialization preserve both exactly. - Enumeration target: SISO-REF-010-2025 (version 36). - Cross-check sources include NPS MOVES Open-DIS projects and independent packet decoders. OpenDisNet is not a port and does not copy their public API design. +- The NuGet package has no Java code, dependency, runtime requirement, or + Java-facing API. Reference implementations supply test bytes only. See [the standards baseline](docs/standards.md) and [conformance matrix](docs/conformance.md) for exact, release-specific coverage, diff --git a/docs/conformance.md b/docs/conformance.md index 6101b4b..150b91b 100644 --- a/docs/conformance.md +++ b/docs/conformance.md @@ -6,7 +6,7 @@ input tests, and round-trip tests. ## Foundation -- [x] Common 12-byte PDU header +- [x] Common 12-byte PDU header and Live Entity 10-byte header - [x] Big-endian integer and IEEE-754 primitives - [x] Defensive length and version validation - [x] Unknown/future PDU preservation @@ -15,26 +15,33 @@ input tests, and round-trip tests. - [x] Binary dispatch, parsing, and serialization for PDU types 1-72 - [x] Default construction and byte-identical round trip for PDU types 1-72 - [x] Automatic synchronization of list, octet, and bit-length fields -- [x] Populated Signal, Transmitter, Intercom Control, and Minefield Data cases +- [x] Deterministically populated round trips for every PDU type (1-72) +- [x] 70 independent default reference vectors and 44 compatible populated vectors - [ ] Independent populated vectors for every variable-layout PDU -- [ ] Truncation-at-every-boundary and mutation coverage for every PDU family +- [x] Truncation-at-every-boundary coverage for every PDU family - [x] SISO-REF-010-2025 v36 typed enumerations and structured bitfields - [x] Lossless round trip of unknown enumeration values and reserved bitfield bits ## PDU families -- [ ] Entity Information/Interaction (Entity State implemented) -- [ ] Warfare (Fire and Detonation implemented) -- [ ] Logistics -- [ ] Simulation Management -- [ ] Distributed Emission Regeneration -- [ ] Radio Communications (populated Signal, Transmitter, and Intercom Control exercised) -- [ ] Entity Management -- [ ] Minefield (conditional Minefield Data fields exercised) -- [ ] Synthetic Environment -- [ ] Simulation Management with Reliability -- [ ] Live Entity -- [ ] Information Operations +- [x] 0.3 Entity Information/Interaction +- [x] 0.3 Warfare +- [x] 0.4 Logistics +- [x] 0.4 Simulation Management +- [x] 0.5 Distributed Emission Regeneration +- [x] 0.5 Radio Communications +- [x] 0.6 Entity Management +- [x] 0.6 Minefield +- [x] 0.7 Synthetic Environment +- [x] 0.7 Simulation Management with Reliability +- [x] 0.8 Live Entity +- [x] 0.8 Information Operations + +Family coverage is implemented and exercised entirely in C#/.NET. Frozen bytes +from an independent implementation are test inputs only; they are not product +code or a runtime/build dependency. Two default vectors and 28 aggressively +populated vectors expose documented defects in that reference generator and are +retained as differential evidence rather than copied into OpenDisNet behavior. This file is a release gate. Version 1.0 will not be labeled complete until all applicable entries are checked and independently audited. diff --git a/docs/standards.md b/docs/standards.md index ae381dd..b47b9ba 100644 --- a/docs/standards.md +++ b/docs/standards.md @@ -22,5 +22,7 @@ cross-checks: - `open-dis/open-dis-csharp` The Java source generator reports 72 DIS v7 PDUs and provides machine-readable -layout descriptions. OpenDisNet's conformance claim is based on its own coverage -matrix and tests, not merely the presence of a corresponding reference class. +layout descriptions. It is an offline reference only: OpenDisNet is a native +C#/.NET design with no Java code, dependency, or runtime requirement. Its +conformance claim is based on its own coverage matrix and tests, not merely the +presence of a corresponding reference class.