Commit:
37fbe34Parent:
cebe3e9Idiomatic .NET DLT reader
Assisted-by: GitHub Copilot: claude-opus-5
README.md
+87
-0
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5cd6d6f
@@ -0,0 +1,87 @@
# SharpDlt
A pure, dependency free .NET library to read AUTOSAR DLT (Diagnostic Log and Trace) data.
The parser is modelled after the [PyDLT](https://github.com/mikiepure/pydlt) reader, but exposes an idiomatic C# API:
the entry point is a function that turns a stream into a lazily evaluated sequence of DLT messages.
## Usage
```csharp
using SharpDlt;
// From a file (the file is opened and closed by the reader)
foreach (var message in DltReader.ReadMessages("trace.dlt"))
{
Console.WriteLine(message);
}
// From any stream (the caller keeps ownership of the stream)
using var stream = File.OpenRead("trace.dlt");
foreach (var message in DltReader.ReadMessages(stream))
{
if (message.VerbosePayload is { } payload)
{
foreach (var argument in payload.Arguments)
{
Console.WriteLine(argument);
}
}
}
// Asynchronously
await foreach (var message in DltReader.ReadMessagesAsync("trace.dlt"))
{
Console.WriteLine(message.StandardHeader.Timestamp);
}
```
Options are passed via `DltReaderOptions`:
```csharp
var options = new DltReaderOptions
{
HasStorageHeader = false, // messages without a Storage Header, e.g. from a socket
StringEncoding = Encoding.Latin1, // encoding of 8 bit coded string arguments (default: ASCII)
};
var messages = DltReader.ReadMessages(stream, options);
```
A single message can also be parsed from a span of bytes:
```csharp
var message = DltMessage.Parse(bytes, options);
```
## Object model
| Type | Description |
|---------------------|--------------------------------------------------------------------------------------------------------------------------|
| `DltMessage` | Storage Header, Standard Header, Extended Header and payload of one message |
| `StorageHeader` | Timestamp and ECU ID of a message stored in a DLT file |
| `StandardHeader` | Header type, message counter, length and the optional ECU ID, session ID and timestamp |
| `ExtendedHeader` | Verbose flag, message type and type info, argument count, application and context ID |
| `NonVerbosePayload` | Message ID and non-static data |
| `VerbosePayload` | List of `DltArgument` values |
| `DltArgument` | `DltBoolArgument`, `DltSignedArgument`, `DltUnsignedArgument`, `DltFloatArgument`, `DltStringArgument`, `DltRawArgument` |
All of these are records, so they can be compared, deconstructed and pattern matched.
`DltMessage.ToString()` renders the same overview line as the reference implementation.
## Behavior
* Reading stops silently as soon as the remaining data does not contain a complete message.
* Malformed data throws a `DltParseException`.
* If a numeric argument at the end of a payload has fewer bytes than its declared type, it is
reduced to the next smaller type, which mirrors DLT Viewer and the reference implementation.
* Not supported (as in the reference implementation): arrays, structs, trace info, variable info,
fixed point and 128 bit types.
## Development
```powershell
dotnet build
dotnet test
dotnet csharpier format src tests
```
src/Class1.cs
+0
-6
diff --git a/src/Class1.cs b/src/Class1.cs
deleted file mode 100644
index 53821ad..0000000
@@ -1,6 +0,0 @@
namespace SharpDlt;
public class Class1
{
}
src/DltMessage.cs
+43
-0
diff --git a/src/DltMessage.cs b/src/DltMessage.cs
new file mode 100644
index 0000000..e04da5d
@@ -0,0 +1,43 @@
using SharpDlt.Internal;
namespace SharpDlt;
/// <summary>
/// A DLT message consisting of its headers and its payload.
/// </summary>
/// <param name="StorageHeader">Storage Header of the message, if the message was stored in a file.</param>
/// <param name="StandardHeader">Standard Header of the message.</param>
/// <param name="ExtendedHeader">Extended Header of the message, if it is transmitted.</param>
/// <param name="Payload">Payload of the message, if it carries one.</param>
public sealed record DltMessage(
StorageHeader? StorageHeader,
StandardHeader StandardHeader,
ExtendedHeader? ExtendedHeader,
DltPayload? Payload
)
{
/// <summary>Gets a value indicating whether the message is in verbose mode.</summary>
public bool IsVerbose => ExtendedHeader?.Verbose ?? false;
/// <summary>Gets the payload as a verbose payload, or <see langword="null"/> if it is not verbose.</summary>
public VerbosePayload? VerbosePayload => Payload as VerbosePayload;
/// <summary>Gets the payload as a non-verbose payload, or <see langword="null"/> if it is verbose.</summary>
public NonVerbosePayload? NonVerbosePayload => Payload as NonVerbosePayload;
/// <summary>Parses a single DLT message from the beginning of <paramref name="data"/>.</summary>
/// <param name="data">The data of one complete DLT message.</param>
/// <param name="options">Options controlling the parsing, <see cref="DltReaderOptions.Default"/> if omitted.</param>
/// <exception cref="DltParseException">The data is malformed.</exception>
public static DltMessage Parse(ReadOnlySpan<byte> data, DltReaderOptions? options = null)
{
options ??= DltReaderOptions.Default;
return DltMessageParser.Parse(data, options.HasStorageHeader, options.StringEncoding);
}
/// <summary>
/// Gets an overview of the message in the format
/// <c>[time ][timestamp ]counter[ ecuid][ apid][ ctid][ session][ type][ subtype] mode[ args][ payload]</c>.
/// </summary>
public override string ToString() => DltMessageParser.Describe(this);
}
src/DltParseException.cs
+15
-0
diff --git a/src/DltParseException.cs b/src/DltParseException.cs
new file mode 100644
index 0000000..50c4567
@@ -0,0 +1,15 @@
namespace SharpDlt;
/// <summary>
/// The exception that is thrown when DLT data cannot be parsed.
/// </summary>
public sealed class DltParseException : FormatException
{
/// <summary>Initializes a new instance of the <see cref="DltParseException"/> class.</summary>
public DltParseException(string message)
: base(message) { }
/// <summary>Initializes a new instance of the <see cref="DltParseException"/> class.</summary>
public DltParseException(string message, Exception innerException)
: base(message, innerException) { }
}
src/DltReader.cs
+196
-0
diff --git a/src/DltReader.cs b/src/DltReader.cs
new file mode 100644
index 0000000..8974060
@@ -0,0 +1,196 @@
using System.Runtime.CompilerServices;
namespace SharpDlt;
/// <summary>
/// Reads DLT messages from a stream or a DLT file.
/// </summary>
/// <example>
/// <code>
/// foreach (var message in DltReader.ReadMessages("trace.dlt"))
/// {
/// Console.WriteLine(message);
/// }
/// </code>
/// </example>
public static class DltReader
{
/// <summary>
/// Reads all DLT messages from <paramref name="stream"/>.
/// </summary>
/// <param name="stream">The stream to read from. The caller keeps ownership of the stream.</param>
/// <param name="options">Options controlling the parsing, <see cref="DltReaderOptions.Default"/> if omitted.</param>
/// <returns>A lazily evaluated sequence of DLT messages.</returns>
/// <remarks>
/// Reading stops silently as soon as the remaining data no longer contains a complete message.
/// </remarks>
/// <exception cref="DltParseException">The data of a message is malformed.</exception>
public static IEnumerable<DltMessage> ReadMessages(Stream stream, DltReaderOptions? options = null)
{
ArgumentNullException.ThrowIfNull(stream);
return Iterate(stream, options ?? DltReaderOptions.Default);
static IEnumerable<DltMessage> Iterate(Stream stream, DltReaderOptions options)
{
var prefixLength = GetPrefixLength(options);
var prefix = new byte[prefixLength];
while (true)
{
if (stream.ReadAtLeast(prefix, prefixLength, throwOnEndOfStream: false) < prefixLength)
{
yield break;
}
var buffer = CreateMessageBuffer(prefix, options, out var remaining);
if (
remaining > 0
&& stream.ReadAtLeast(buffer.AsSpan(prefixLength), remaining, throwOnEndOfStream: false) < remaining
)
{
yield break;
}
yield return DltMessage.Parse(buffer, options);
}
}
}
/// <summary>
/// Opens the DLT file at <paramref name="path"/> and reads all DLT messages from it.
/// </summary>
/// <param name="path">Path of the DLT file.</param>
/// <param name="options">Options controlling the parsing, <see cref="DltReaderOptions.Default"/> if omitted.</param>
/// <returns>A lazily evaluated sequence of DLT messages. The file is closed when enumeration ends.</returns>
/// <exception cref="DltParseException">The data of a message is malformed.</exception>
public static IEnumerable<DltMessage> ReadMessages(string path, DltReaderOptions? options = null)
{
ArgumentException.ThrowIfNullOrEmpty(path);
return Iterate(path, options);
static IEnumerable<DltMessage> Iterate(string path, DltReaderOptions? options)
{
using var stream = OpenFile(path);
foreach (var message in ReadMessages(stream, options))
{
yield return message;
}
}
}
/// <summary>
/// Asynchronously reads all DLT messages from <paramref name="stream"/>.
/// </summary>
/// <param name="stream">The stream to read from. The caller keeps ownership of the stream.</param>
/// <param name="options">Options controlling the parsing, <see cref="DltReaderOptions.Default"/> if omitted.</param>
/// <param name="cancellationToken">Token to cancel the enumeration.</param>
/// <returns>A lazily evaluated asynchronous sequence of DLT messages.</returns>
/// <exception cref="DltParseException">The data of a message is malformed.</exception>
public static IAsyncEnumerable<DltMessage> ReadMessagesAsync(
Stream stream,
DltReaderOptions? options = null,
CancellationToken cancellationToken = default
)
{
ArgumentNullException.ThrowIfNull(stream);
return Iterate(stream, options ?? DltReaderOptions.Default, cancellationToken);
static async IAsyncEnumerable<DltMessage> Iterate(
Stream stream,
DltReaderOptions options,
[EnumeratorCancellation] CancellationToken cancellationToken
)
{
var prefixLength = GetPrefixLength(options);
var prefix = new byte[prefixLength];
while (true)
{
var read = await stream
.ReadAtLeastAsync(prefix, prefixLength, false, cancellationToken)
.ConfigureAwait(false);
if (read < prefixLength)
{
yield break;
}
var buffer = CreateMessageBuffer(prefix, options, out var remaining);
if (remaining > 0)
{
read = await stream
.ReadAtLeastAsync(buffer.AsMemory(prefixLength), remaining, false, cancellationToken)
.ConfigureAwait(false);
if (read < remaining)
{
yield break;
}
}
yield return DltMessage.Parse(buffer, options);
}
}
}
/// <summary>
/// Opens the DLT file at <paramref name="path"/> and asynchronously reads all DLT messages from it.
/// </summary>
/// <param name="path">Path of the DLT file.</param>
/// <param name="options">Options controlling the parsing, <see cref="DltReaderOptions.Default"/> if omitted.</param>
/// <param name="cancellationToken">Token to cancel the enumeration.</param>
/// <returns>A lazily evaluated asynchronous sequence of DLT messages. The file is closed when enumeration ends.</returns>
/// <exception cref="DltParseException">The data of a message is malformed.</exception>
public static IAsyncEnumerable<DltMessage> ReadMessagesAsync(
string path,
DltReaderOptions? options = null,
CancellationToken cancellationToken = default
)
{
ArgumentException.ThrowIfNullOrEmpty(path);
return Iterate(path, options, cancellationToken);
static async IAsyncEnumerable<DltMessage> Iterate(
string path,
DltReaderOptions? options,
[EnumeratorCancellation] CancellationToken cancellationToken
)
{
await using var stream = OpenFile(path, useAsync: true);
await foreach (var message in ReadMessagesAsync(stream, options, cancellationToken).ConfigureAwait(false))
{
yield return message;
}
}
}
private static FileStream OpenFile(string path, bool useAsync = false) =>
new(
path,
new FileStreamOptions
{
Mode = FileMode.Open,
Access = FileAccess.Read,
Share = FileShare.Read,
Options = FileOptions.SequentialScan | (useAsync ? FileOptions.Asynchronous : 0),
}
);
private static int GetPrefixLength(DltReaderOptions options) =>
(options.HasStorageHeader ? StorageHeader.Length : 0) + StandardHeader.MinimumLength;
private static byte[] CreateMessageBuffer(byte[] prefix, DltReaderOptions options, out int remaining)
{
var storageHeaderLength = options.HasStorageHeader ? StorageHeader.Length : 0;
var length = StandardHeader.ReadMessageLength(prefix.AsSpan(storageHeaderLength));
if (length < StandardHeader.MinimumLength)
{
throw new DltParseException(
$"Invalid length of a DLT message: {length} / it must be " + $"{StandardHeader.MinimumLength} or more."
);
}
var buffer = new byte[storageHeaderLength + length];
prefix.CopyTo(buffer.AsSpan());
remaining = buffer.Length - prefix.Length;
return buffer;
}
}
src/DltReaderOptions.cs
+25
-0
diff --git a/src/DltReaderOptions.cs b/src/DltReaderOptions.cs
new file mode 100644
index 0000000..40e292f
@@ -0,0 +1,25 @@
using System.Text;
namespace SharpDlt;
/// <summary>Options controlling how DLT data is parsed.</summary>
public sealed record DltReaderOptions
{
/// <summary>Gets the default options.</summary>
public static DltReaderOptions Default { get; } = new();
/// <summary>
/// Gets a value indicating whether every message is preceded by a Storage Header,
/// which is the case for messages stored in a DLT file. Defaults to <see langword="true"/>.
/// </summary>
public bool HasStorageHeader { get; init; } = true;
/// <summary>
/// Gets the encoding used to decode 8 bit coded string arguments. Defaults to ASCII.
/// </summary>
/// <remarks>
/// The DLT specification only supports ASCII and UTF-8 explicitly. Some implementations
/// however store strings in a local 8 bit encoding such as Latin-1.
/// </remarks>
public Encoding? StringEncoding { get; init; }
}
src/Headers/ExtendedHeader.cs
+77
-0
diff --git a/src/Headers/ExtendedHeader.cs b/src/Headers/ExtendedHeader.cs
new file mode 100644
index 0000000..ae857cd
@@ -0,0 +1,77 @@
using SharpDlt.Internal;
namespace SharpDlt;
/// <summary>
/// The Extended Header of a DLT message.
/// </summary>
/// <param name="Verbose">If set, the payload carries a description of the transmitted data.</param>
/// <param name="MessageType">Type of the transmitted DLT message.</param>
/// <param name="MessageTypeInfo">Raw Message Type Info, its meaning depends on <paramref name="MessageType"/>.</param>
/// <param name="NumberOfArguments">Number of arguments in the message payload.</param>
/// <param name="ApplicationId">ID of the application.</param>
/// <param name="ContextId">Unique ID of the logging / tracing context.</param>
public sealed record ExtendedHeader(
bool Verbose,
DltMessageType MessageType,
byte MessageTypeInfo,
byte NumberOfArguments,
string ApplicationId,
string ContextId
)
{
/// <summary>Length of the Extended Header in bytes.</summary>
public const int Length = 10;
private const byte VerboseMask = 0b0000_0001;
private const byte MessageTypeMask = 0b0000_1110;
private const byte MessageTypeInfoMask = 0b1111_0000;
private const int MessageTypeShift = 1;
private const int MessageTypeInfoShift = 4;
/// <summary>Gets the Message Type Info interpreted as log info.</summary>
public DltLogInfo LogInfo => (DltLogInfo)MessageTypeInfo;
/// <summary>Gets the Message Type Info interpreted as application trace info.</summary>
public DltTraceInfo TraceInfo => (DltTraceInfo)MessageTypeInfo;
/// <summary>Gets the Message Type Info interpreted as network trace (bus) info.</summary>
public DltBusInfo BusInfo => (DltBusInfo)MessageTypeInfo;
/// <summary>Gets the Message Type Info interpreted as control info.</summary>
public DltControlInfo ControlInfo => (DltControlInfo)MessageTypeInfo;
/// <summary>Gets the Message Info (MSIN) byte.</summary>
public byte MessageInfo
{
get
{
var msin = Verbose ? VerboseMask : 0;
msin |= ((int)MessageType << MessageTypeShift) & MessageTypeMask;
msin |= (MessageTypeInfo << MessageTypeInfoShift) & MessageTypeInfoMask;
return (byte)msin;
}
}
/// <summary>Parses an Extended Header from the beginning of <paramref name="data"/>.</summary>
/// <exception cref="DltParseException">The data is too short.</exception>
public static ExtendedHeader Parse(ReadOnlySpan<byte> data)
{
if (data.Length < Length)
{
throw new DltParseException(
$"Unexpected length of the data: {data.Length} / Extended Header must be {Length} or more."
);
}
var messageInfo = data[0];
var verbose = (messageInfo & VerboseMask) != 0;
var messageType = (DltMessageType)((messageInfo & MessageTypeMask) >> MessageTypeShift);
var messageTypeInfo = (byte)((messageInfo & MessageTypeInfoMask) >> MessageTypeInfoShift);
var numberOfArguments = data[1];
var applicationId = AsciiText.Decode(data.Slice(2, 4));
var contextId = AsciiText.Decode(data.Slice(6, 4));
return new ExtendedHeader(verbose, messageType, messageTypeInfo, numberOfArguments, applicationId, contextId);
}
}
src/Headers/MessageEnums.cs
+87
-0
diff --git a/src/Headers/MessageEnums.cs b/src/Headers/MessageEnums.cs
new file mode 100644
index 0000000..80d4340
@@ -0,0 +1,87 @@
namespace SharpDlt;
/// <summary>Message Type (MSTP) of the DLT Extended Header.</summary>
public enum DltMessageType
{
/// <summary>DLT log message.</summary>
Log = 0x0,
/// <summary>DLT application trace message.</summary>
AppTrace = 0x1,
/// <summary>DLT network trace message.</summary>
NetworkTrace = 0x2,
/// <summary>DLT control message.</summary>
Control = 0x3,
}
/// <summary>Message Log Info (MTIN) of a log message.</summary>
public enum DltLogInfo
{
/// <summary>Fatal error.</summary>
Fatal = 0x1,
/// <summary>Error with impact on the correct functionality.</summary>
Error = 0x2,
/// <summary>Warning, correct behavior cannot be ensured.</summary>
Warn = 0x3,
/// <summary>Informational message.</summary>
Info = 0x4,
/// <summary>Debug message.</summary>
Debug = 0x5,
/// <summary>Verbose message of lowest priority.</summary>
Verbose = 0x6,
}
/// <summary>Message Trace Info (MTIN) of an application trace message.</summary>
public enum DltTraceInfo
{
/// <summary>Value of a variable.</summary>
Variable = 0x1,
/// <summary>Call of a function.</summary>
FunctionIn = 0x2,
/// <summary>Return of a function.</summary>
FunctionOut = 0x3,
/// <summary>State of a state machine.</summary>
State = 0x4,
/// <summary>RTE events.</summary>
Vfb = 0x5,
}
/// <summary>Message Bus Info (MTIN) of a network trace message.</summary>
public enum DltBusInfo
{
/// <summary>Inter-process communication.</summary>
Ipc = 0x1,
/// <summary>CAN communications bus.</summary>
Can = 0x2,
/// <summary>FlexRay communications bus.</summary>
FlexRay = 0x3,
/// <summary>Most communications bus.</summary>
Most = 0x4,
}
/// <summary>Message Control Info (MTIN) of a control message.</summary>
public enum DltControlInfo
{
/// <summary>Request control message.</summary>
Request = 0x1,
/// <summary>Response control message.</summary>
Response = 0x2,
/// <summary>Time control message.</summary>
Time = 0x3,
}
src/Headers/StandardHeader.cs
+167
-0
diff --git a/src/Headers/StandardHeader.cs b/src/Headers/StandardHeader.cs
new file mode 100644
index 0000000..e9d2432
@@ -0,0 +1,167 @@
using System.Buffers.Binary;
using SharpDlt.Internal;
namespace SharpDlt;
/// <summary>
/// The Standard Header of a DLT message.
/// </summary>
/// <param name="UseExtendedHeader">If set, the Extended Header is transmitted.</param>
/// <param name="MsbFirst">If set, the payload data is big endian, otherwise little endian.</param>
/// <param name="VersionNumber">Version number of the DLT data protocol.</param>
/// <param name="MessageCounter">Continuous number of the message.</param>
/// <param name="Length">Length of the complete message (without Storage Header) in bytes.</param>
/// <param name="EcuId">Unique address of the sender, if transmitted.</param>
/// <param name="SessionId">Session number, if transmitted.</param>
/// <param name="Timestamp">Ticks of the ECU at the moment the message was sent, if transmitted.</param>
public sealed record StandardHeader(
bool UseExtendedHeader,
bool MsbFirst,
int VersionNumber,
byte MessageCounter,
ushort Length,
string? EcuId = null,
uint? SessionId = null,
uint? Timestamp = null
)
{
/// <summary>Minimum length of the Standard Header in bytes.</summary>
public const int MinimumLength = 4;
private const byte UseExtendedHeaderMask = 0b0000_0001;
private const byte MsbFirstMask = 0b0000_0010;
private const byte WithEcuIdMask = 0b0000_0100;
private const byte WithSessionIdMask = 0b0000_1000;
private const byte WithTimestampMask = 0b0001_0000;
private const byte VersionNumberMask = 0b1110_0000;
private const int VersionNumberShift = 5;
/// <summary>Gets a value indicating whether the header carries an ECU ID.</summary>
public bool WithEcuId => EcuId is not null;
/// <summary>Gets a value indicating whether the header carries a session ID.</summary>
public bool WithSessionId => SessionId is not null;
/// <summary>Gets a value indicating whether the header carries a timestamp.</summary>
public bool WithTimestamp => Timestamp is not null;
/// <summary>Gets the length of this header in bytes.</summary>
public int ByteLength => MinimumLength + (WithEcuId ? 4 : 0) + (WithSessionId ? 4 : 0) + (WithTimestamp ? 4 : 0);
/// <summary>Gets the Header Type (HTYP) byte.</summary>
public byte HeaderType
{
get
{
var htyp = (VersionNumber << VersionNumberShift) & VersionNumberMask;
if (UseExtendedHeader)
{
htyp |= UseExtendedHeaderMask;
}
if (MsbFirst)
{
htyp |= MsbFirstMask;
}
if (WithEcuId)
{
htyp |= WithEcuIdMask;
}
if (WithSessionId)
{
htyp |= WithSessionIdMask;
}
if (WithTimestamp)
{
htyp |= WithTimestampMask;
}
return (byte)htyp;
}
}
/// <summary>
/// Reads the message length field of a Standard Header which starts at the beginning of
/// <paramref name="data"/>.
/// </summary>
/// <exception cref="DltParseException">The data is shorter than the minimum header length.</exception>
public static ushort ReadMessageLength(ReadOnlySpan<byte> data)
{
if (data.Length < MinimumLength)
{
throw new DltParseException(
$"Unexpected length of the data: {data.Length} / Standard Header must be {MinimumLength} or more."
);
}
return BinaryPrimitives.ReadUInt16BigEndian(data[2..]);
}
/// <summary>Parses a Standard Header from the beginning of <paramref name="data"/>.</summary>
/// <exception cref="DltParseException">The data is too short for the encoded header type.</exception>
public static StandardHeader Parse(ReadOnlySpan<byte> data)
{
if (data.Length < MinimumLength)
{
throw new DltParseException(
$"Unexpected length of the data: {data.Length} / Standard Header must be {MinimumLength} or more."
);
}
var headerType = data[0];
var useExtendedHeader = (headerType & UseExtendedHeaderMask) != 0;
var msbFirst = (headerType & MsbFirstMask) != 0;
var withEcuId = (headerType & WithEcuIdMask) != 0;
var withSessionId = (headerType & WithSessionIdMask) != 0;
var withTimestamp = (headerType & WithTimestampMask) != 0;
var versionNumber = (headerType & VersionNumberMask) >> VersionNumberShift;
var expectedLength = MinimumLength + (withEcuId ? 4 : 0) + (withSessionId ? 4 : 0) + (withTimestamp ? 4 : 0);
if (data.Length < expectedLength)
{
throw new DltParseException(
$"Unexpected length of the data: {data.Length} / Standard Header with Header Type: "
+ $"WEID={withEcuId} WSID={withSessionId} WTMS={withTimestamp} "
+ $"must be {expectedLength} or more."
);
}
var messageCounter = data[1];
var length = BinaryPrimitives.ReadUInt16BigEndian(data[2..]);
var offset = MinimumLength;
string? ecuId = null;
if (withEcuId)
{
ecuId = AsciiText.Decode(data.Slice(offset, 4));
offset += 4;
}
uint? sessionId = null;
if (withSessionId)
{
sessionId = BinaryPrimitives.ReadUInt32BigEndian(data[offset..]);
offset += 4;
}
uint? timestamp = null;
if (withTimestamp)
{
timestamp = BinaryPrimitives.ReadUInt32BigEndian(data[offset..]);
}
return new StandardHeader(
useExtendedHeader,
msbFirst,
versionNumber,
messageCounter,
length,
ecuId,
sessionId,
timestamp
);
}
}
src/Headers/StorageHeader.cs
+47
-0
diff --git a/src/Headers/StorageHeader.cs b/src/Headers/StorageHeader.cs
new file mode 100644
index 0000000..9772679
@@ -0,0 +1,47 @@
using System.Buffers.Binary;
using SharpDlt.Internal;
namespace SharpDlt;
/// <summary>
/// The Storage Header which precedes every DLT message stored in a DLT file.
/// </summary>
/// <param name="Seconds">Seconds since 01.01.1970 (unix time).</param>
/// <param name="Microseconds">Microseconds of the second.</param>
/// <param name="EcuId">The ECU ID.</param>
public sealed record StorageHeader(uint Seconds, int Microseconds, string EcuId)
{
/// <summary>Length of the Storage Header in bytes.</summary>
public const int Length = 16;
/// <summary>The DLT pattern ("DLT" + 0x01) which starts every Storage Header.</summary>
public static ReadOnlySpan<byte> DltPattern => "DLT\u0001"u8;
/// <summary>Gets the point in time at which the message was stored.</summary>
public DateTimeOffset Timestamp =>
DateTimeOffset.FromUnixTimeSeconds(Seconds)
+ TimeSpan.FromTicks(Microseconds * (TimeSpan.TicksPerMillisecond / 1000));
/// <summary>Parses a Storage Header from the beginning of <paramref name="data"/>.</summary>
/// <exception cref="DltParseException">The data is too short or does not start with the DLT pattern.</exception>
public static StorageHeader Parse(ReadOnlySpan<byte> data)
{
if (data.Length < Length)
{
throw new DltParseException(
$"Unexpected length of the data: {data.Length} / Storage Header must be {Length} or more."
);
}
if (!data[..4].SequenceEqual(DltPattern))
{
throw new DltParseException($"DLT pattern is not found in the data: {Convert.ToHexString(data[..4])}.");
}
var seconds = BinaryPrimitives.ReadUInt32LittleEndian(data[4..]);
var microseconds = BinaryPrimitives.ReadInt32LittleEndian(data[8..]);
var ecuId = AsciiText.Decode(data[12..16]);
return new StorageHeader(seconds, microseconds, ecuId);
}
}
src/Internal/AsciiText.cs
+13
-0
diff --git a/src/Internal/AsciiText.cs b/src/Internal/AsciiText.cs
new file mode 100644
index 0000000..1b02354
@@ -0,0 +1,13 @@
using System.Text;
namespace SharpDlt.Internal;
/// <summary>Helpers to decode the fixed size ASCII fields of the DLT protocol.</summary>
internal static class AsciiText
{
/// <summary>
/// Decodes a fixed size ASCII field, replacing invalid bytes and stripping NUL padding.
/// </summary>
public static string Decode(ReadOnlySpan<byte> data) =>
Encoding.ASCII.GetString(data).Replace("\0", string.Empty, StringComparison.Ordinal);
}
src/Internal/DltMessageParser.cs
+170
-0
diff --git a/src/Internal/DltMessageParser.cs b/src/Internal/DltMessageParser.cs
new file mode 100644
index 0000000..a618203
@@ -0,0 +1,170 @@
using System.Globalization;
using System.Text;
namespace SharpDlt.Internal;
/// <summary>Composes a <see cref="DltMessage"/> out of the parts of a DLT message.</summary>
internal static class DltMessageParser
{
public static DltMessage Parse(ReadOnlySpan<byte> data, bool withStorageHeader, Encoding? encoding)
{
var offset = 0;
StorageHeader? storageHeader = null;
if (withStorageHeader)
{
storageHeader = StorageHeader.Parse(data);
offset += StorageHeader.Length;
}
var standardHeader = StandardHeader.Parse(data[offset..]);
offset += standardHeader.ByteLength;
ExtendedHeader? extendedHeader = null;
var extendedHeaderLength = 0;
if (standardHeader.UseExtendedHeader)
{
extendedHeader = ExtendedHeader.Parse(data[offset..]);
extendedHeaderLength = ExtendedHeader.Length;
offset += extendedHeaderLength;
}
DltPayload? payload = null;
if (standardHeader.Length > standardHeader.ByteLength + extendedHeaderLength)
{
var storageHeaderLength = withStorageHeader ? StorageHeader.Length : 0;
var end = Math.Min(storageHeaderLength + standardHeader.Length, data.Length);
if (end < offset)
{
throw new DltParseException(
$"Unexpected length of the data: {data.Length} / "
+ $"the message declares a length of {standardHeader.Length}."
);
}
var payloadData = data[offset..end];
payload = extendedHeader is { Verbose: true }
? VerbosePayload.Parse(payloadData, standardHeader.MsbFirst, extendedHeader.NumberOfArguments, encoding)
: NonVerbosePayload.Parse(payloadData, standardHeader.MsbFirst);
}
return new DltMessage(storageHeader, standardHeader, extendedHeader, payload);
}
public static string Describe(DltMessage message)
{
var parts = new List<string>(12);
var storageHeader = message.StorageHeader;
var standardHeader = message.StandardHeader;
var extendedHeader = message.ExtendedHeader;
if (storageHeader is not null)
{
parts.Add(
storageHeader.Timestamp.UtcDateTime.ToString("yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture)
+ "."
+ storageHeader.Microseconds.ToString("D6", CultureInfo.InvariantCulture)
);
}
if (standardHeader.Timestamp is { } timestamp)
{
parts.Add(
(timestamp / 10000).ToString(CultureInfo.InvariantCulture)
+ "."
+ (timestamp % 10000).ToString("D4", CultureInfo.InvariantCulture)
);
}
parts.Add(standardHeader.MessageCounter.ToString(CultureInfo.InvariantCulture));
if (standardHeader.EcuId is not null)
{
parts.Add(standardHeader.EcuId);
}
else if (storageHeader is not null)
{
parts.Add(storageHeader.EcuId);
}
if (extendedHeader is not null)
{
parts.Add(extendedHeader.ApplicationId);
parts.Add(extendedHeader.ContextId);
}
if (standardHeader.SessionId is { } sessionId)
{
parts.Add(sessionId.ToString(CultureInfo.InvariantCulture));
}
if (extendedHeader is not null)
{
parts.Add(DescribeMessageType(extendedHeader.MessageType));
parts.Add(DescribeMessageTypeInfo(extendedHeader));
}
parts.Add(extendedHeader is { Verbose: true } ? "verbose" : "non-verbose");
if (extendedHeader is not null)
{
parts.Add(extendedHeader.NumberOfArguments.ToString(CultureInfo.InvariantCulture));
}
if (message.Payload is not null)
{
parts.Add(message.Payload.ToString());
}
return string.Join(' ', parts);
}
private static string DescribeMessageType(DltMessageType messageType) =>
messageType switch
{
DltMessageType.Log => "log",
DltMessageType.AppTrace => "app_trace",
DltMessageType.NetworkTrace => "nw_trace",
DltMessageType.Control => "control",
_ => "unknown",
};
private static string DescribeMessageTypeInfo(ExtendedHeader header) =>
header.MessageType switch
{
DltMessageType.Log => header.LogInfo switch
{
DltLogInfo.Fatal => "fatal",
DltLogInfo.Error => "error",
DltLogInfo.Warn => "warn",
DltLogInfo.Info => "info",
DltLogInfo.Debug => "debug",
DltLogInfo.Verbose => "verbose",
_ => "unknown",
},
DltMessageType.AppTrace => header.TraceInfo switch
{
DltTraceInfo.Variable => "variable",
DltTraceInfo.FunctionIn => "func_in",
DltTraceInfo.FunctionOut => "func_out",
DltTraceInfo.State => "state",
DltTraceInfo.Vfb => "vfb",
_ => "unknown",
},
DltMessageType.NetworkTrace => header.BusInfo switch
{
DltBusInfo.Ipc => "ipc",
DltBusInfo.Can => "can",
DltBusInfo.FlexRay => "flexray",
DltBusInfo.Most => "most",
_ => "unknown",
},
DltMessageType.Control => header.ControlInfo switch
{
DltControlInfo.Request => "request",
DltControlInfo.Response => "response",
DltControlInfo.Time => "time",
_ => "unknown",
},
_ => "unknown",
};
}
src/Internal/SpanReader.cs
+31
-0
diff --git a/src/Internal/SpanReader.cs b/src/Internal/SpanReader.cs
new file mode 100644
index 0000000..68d1079
@@ -0,0 +1,31 @@
using System.Buffers.Binary;
namespace SharpDlt.Internal;
/// <summary>Endian aware primitive readers used by the DLT payload parser.</summary>
internal static class SpanReader
{
public static ushort ReadUInt16(ReadOnlySpan<byte> data, bool msbFirst) =>
msbFirst ? BinaryPrimitives.ReadUInt16BigEndian(data) : BinaryPrimitives.ReadUInt16LittleEndian(data);
public static uint ReadUInt32(ReadOnlySpan<byte> data, bool msbFirst) =>
msbFirst ? BinaryPrimitives.ReadUInt32BigEndian(data) : BinaryPrimitives.ReadUInt32LittleEndian(data);
public static ulong ReadUInt64(ReadOnlySpan<byte> data, bool msbFirst) =>
msbFirst ? BinaryPrimitives.ReadUInt64BigEndian(data) : BinaryPrimitives.ReadUInt64LittleEndian(data);
public static short ReadInt16(ReadOnlySpan<byte> data, bool msbFirst) =>
msbFirst ? BinaryPrimitives.ReadInt16BigEndian(data) : BinaryPrimitives.ReadInt16LittleEndian(data);
public static int ReadInt32(ReadOnlySpan<byte> data, bool msbFirst) =>
msbFirst ? BinaryPrimitives.ReadInt32BigEndian(data) : BinaryPrimitives.ReadInt32LittleEndian(data);
public static long ReadInt64(ReadOnlySpan<byte> data, bool msbFirst) =>
msbFirst ? BinaryPrimitives.ReadInt64BigEndian(data) : BinaryPrimitives.ReadInt64LittleEndian(data);
public static float ReadSingle(ReadOnlySpan<byte> data, bool msbFirst) =>
msbFirst ? BinaryPrimitives.ReadSingleBigEndian(data) : BinaryPrimitives.ReadSingleLittleEndian(data);
public static double ReadDouble(ReadOnlySpan<byte> data, bool msbFirst) =>
msbFirst ? BinaryPrimitives.ReadDoubleBigEndian(data) : BinaryPrimitives.ReadDoubleLittleEndian(data);
}
src/Payloads/ArgumentEnums.cs
+37
-0
diff --git a/src/Payloads/ArgumentEnums.cs b/src/Payloads/ArgumentEnums.cs
new file mode 100644
index 0000000..c7140cb
@@ -0,0 +1,37 @@
namespace SharpDlt;
/// <summary>Width of an integer argument. The numeric value is the size in bytes.</summary>
public enum DltIntegerWidth
{
/// <summary>8 bit integer.</summary>
Bits8 = 1,
/// <summary>16 bit integer.</summary>
Bits16 = 2,
/// <summary>32 bit integer.</summary>
Bits32 = 4,
/// <summary>64 bit integer.</summary>
Bits64 = 8,
}
/// <summary>Width of a floating point argument. The numeric value is the size in bytes.</summary>
public enum DltFloatWidth
{
/// <summary>32 bit (single precision) float.</summary>
Bits32 = 4,
/// <summary>64 bit (double precision) float.</summary>
Bits64 = 8,
}
/// <summary>Coding (SCOD) of a string argument.</summary>
public enum DltStringCoding
{
/// <summary>8 bit coding, ASCII by default.</summary>
Ascii = 0,
/// <summary>UTF-8 coding.</summary>
Utf8 = 1,
}
src/Payloads/Arguments.cs
+73
-0
diff --git a/src/Payloads/Arguments.cs b/src/Payloads/Arguments.cs
new file mode 100644
index 0000000..7e9d681
@@ -0,0 +1,73 @@
using System.Globalization;
namespace SharpDlt;
/// <summary>A single argument of a verbose mode payload.</summary>
public abstract partial record DltArgument
{
/// <summary>Gets a human readable representation of the argument value.</summary>
public abstract override string ToString();
}
/// <summary>An argument of type bool (BOOL).</summary>
/// <param name="Value">The value of the argument.</param>
public sealed record DltBoolArgument(bool Value) : DltArgument
{
/// <inheritdoc />
public override string ToString() => Value ? "True" : "False";
}
/// <summary>An argument of type signed integer (SINT).</summary>
/// <param name="Value">The value of the argument.</param>
/// <param name="Width">The width of the value as it was encoded.</param>
public sealed record DltSignedArgument(long Value, DltIntegerWidth Width) : DltArgument
{
/// <inheritdoc />
public override string ToString() => Value.ToString(CultureInfo.InvariantCulture);
}
/// <summary>An argument of type unsigned integer (UINT).</summary>
/// <param name="Value">The value of the argument.</param>
/// <param name="Width">The width of the value as it was encoded.</param>
public sealed record DltUnsignedArgument(ulong Value, DltIntegerWidth Width) : DltArgument
{
/// <inheritdoc />
public override string ToString() => Value.ToString(CultureInfo.InvariantCulture);
}
/// <summary>An argument of type float (FLOA).</summary>
/// <param name="Value">The value of the argument.</param>
/// <param name="Width">The width of the value as it was encoded.</param>
public sealed record DltFloatArgument(double Value, DltFloatWidth Width) : DltArgument
{
/// <inheritdoc />
public override string ToString() => Value.ToString(CultureInfo.InvariantCulture);
}
/// <summary>An argument of type string (STRG).</summary>
/// <param name="Value">The value of the argument, without its NUL terminator.</param>
/// <param name="Coding">The coding of the string on the wire.</param>
public sealed record DltStringArgument(string Value, DltStringCoding Coding) : DltArgument
{
/// <inheritdoc />
public override string ToString() => Value;
}
/// <summary>An argument of type raw (RAWD).</summary>
/// <param name="Value">The raw bytes of the argument.</param>
public sealed record DltRawArgument(ReadOnlyMemory<byte> Value) : DltArgument
{
/// <inheritdoc />
public override string ToString() => Convert.ToHexString(Value.Span).ToLowerInvariant();
/// <summary>Determines whether the argument carries the same bytes as <paramref name="other"/>.</summary>
public bool Equals(DltRawArgument? other) => other is not null && Value.Span.SequenceEqual(other.Value.Span);
/// <inheritdoc />
public override int GetHashCode()
{
var hash = new HashCode();
hash.AddBytes(Value.Span);
return hash.ToHashCode();
}
}
src/Payloads/DltArgument.Parsing.cs
+218
-0
diff --git a/src/Payloads/DltArgument.Parsing.cs b/src/Payloads/DltArgument.Parsing.cs
new file mode 100644
index 0000000..448c072
@@ -0,0 +1,218 @@
using System.Text;
using SharpDlt.Internal;
namespace SharpDlt;
public abstract partial record DltArgument
{
/// <summary>Length of the Type Info field in bytes.</summary>
internal const int TypeInfoLength = 4;
private const uint MaskBaseType = 0b0000_0000_0110_0111_1111_0000;
private const uint MaskTypeLength = 0b1111;
private const uint MaskStringCoding = 0b0011_1000_0000_0000_0000;
private const uint TypeBool = 0x0000_0010;
private const uint TypeSigned = 0x0000_0020;
private const uint TypeUnsigned = 0x0000_0040;
private const uint TypeFloat = 0x0000_0080;
private const uint TypeString = 0x0000_0200;
private const uint TypeRaw = 0x0000_0400;
private const uint StringCodingAscii = 0x0000_0000;
private const uint StringCodingUtf8 = 0x0000_8000;
private const int LengthFieldSize = 2;
/// <summary>Parses a single argument from the beginning of <paramref name="data"/>.</summary>
/// <param name="data">Data starting with the Type Info of the argument.</param>
/// <param name="msbFirst">If set, the payload data is big endian, otherwise little endian.</param>
/// <param name="encoding">Encoding used for 8 bit coded strings. Defaults to ASCII.</param>
/// <param name="bytesRead">Number of bytes consumed by the argument.</param>
/// <exception cref="DltParseException">The data is malformed or uses an unsupported type.</exception>
public static DltArgument Parse(ReadOnlySpan<byte> data, bool msbFirst, Encoding? encoding, out int bytesRead)
{
if (data.Length < TypeInfoLength)
{
throw new DltParseException(
$"Unexpected length of the data: {data.Length} / an argument must be {TypeInfoLength} or more."
);
}
var typeInfo = SpanReader.ReadUInt32(data, msbFirst);
var payload = data[TypeInfoLength..];
return (typeInfo & MaskBaseType) switch
{
TypeBool => ParseBool(payload, typeInfo, out bytesRead),
TypeSigned => ParseSigned(payload, msbFirst, typeInfo, out bytesRead),
TypeUnsigned => ParseUnsigned(payload, msbFirst, typeInfo, out bytesRead),
TypeFloat => ParseFloat(payload, msbFirst, typeInfo, out bytesRead),
TypeString => ParseString(payload, msbFirst, typeInfo, encoding, out bytesRead),
TypeRaw => ParseRaw(payload, msbFirst, typeInfo, out bytesRead),
_ => throw UnsupportedTypeInfo(typeInfo),
};
}
private static DltArgument ParseBool(ReadOnlySpan<byte> payload, uint typeInfo, out int bytesRead)
{
if (payload.IsEmpty)
{
throw new DltParseException(
$"There is no data for a bool argument. Type Info: {FormatTypeInfo(typeInfo)}."
);
}
bytesRead = TypeInfoLength + 1;
return new DltBoolArgument(payload[0] != 0);
}
private static DltArgument ParseSigned(ReadOnlySpan<byte> payload, bool msbFirst, uint typeInfo, out int bytesRead)
{
var width = ResolveIntegerWidth(payload.Length, typeInfo, "signed integer");
bytesRead = TypeInfoLength + (int)width;
long value = width switch
{
DltIntegerWidth.Bits8 => (sbyte)payload[0],
DltIntegerWidth.Bits16 => SpanReader.ReadInt16(payload, msbFirst),
DltIntegerWidth.Bits32 => SpanReader.ReadInt32(payload, msbFirst),
_ => SpanReader.ReadInt64(payload, msbFirst),
};
return new DltSignedArgument(value, width);
}
private static DltArgument ParseUnsigned(
ReadOnlySpan<byte> payload,
bool msbFirst,
uint typeInfo,
out int bytesRead
)
{
var width = ResolveIntegerWidth(payload.Length, typeInfo, "unsigned integer");
bytesRead = TypeInfoLength + (int)width;
ulong value = width switch
{
DltIntegerWidth.Bits8 => payload[0],
DltIntegerWidth.Bits16 => SpanReader.ReadUInt16(payload, msbFirst),
DltIntegerWidth.Bits32 => SpanReader.ReadUInt32(payload, msbFirst),
_ => SpanReader.ReadUInt64(payload, msbFirst),
};
return new DltUnsignedArgument(value, width);
}
private static DltArgument ParseFloat(ReadOnlySpan<byte> payload, bool msbFirst, uint typeInfo, out int bytesRead)
{
var width = (typeInfo & MaskTypeLength) switch
{
3 => DltFloatWidth.Bits32,
4 => DltFloatWidth.Bits64,
_ => throw UnsupportedTypeInfo(typeInfo),
};
if (payload.Length < (int)width)
{
throw new DltParseException(
$"There is no data for a {(int)width * 8} bit float. " + $"Type Info: {FormatTypeInfo(typeInfo)}."
);
}
bytesRead = TypeInfoLength + (int)width;
var value =
width == DltFloatWidth.Bits32
? SpanReader.ReadSingle(payload, msbFirst)
: SpanReader.ReadDouble(payload, msbFirst);
return new DltFloatArgument(value, width);
}
private static DltArgument ParseString(
ReadOnlySpan<byte> payload,
bool msbFirst,
uint typeInfo,
Encoding? encoding,
out int bytesRead
)
{
var coding = (typeInfo & MaskStringCoding) switch
{
StringCodingAscii => DltStringCoding.Ascii,
StringCodingUtf8 => DltStringCoding.Utf8,
_ => throw UnsupportedTypeInfo(typeInfo),
};
var text = ReadLengthPrefixed(payload, msbFirst, typeInfo, out bytesRead, out var complete);
// The string on the wire is NUL terminated; the terminator is not part of the value.
if (complete && !text.IsEmpty)
{
text = text[..^1];
}
var effectiveEncoding = coding == DltStringCoding.Utf8 ? Encoding.UTF8 : encoding ?? Encoding.ASCII;
return new DltStringArgument(effectiveEncoding.GetString(text), coding);
}
private static DltArgument ParseRaw(ReadOnlySpan<byte> payload, bool msbFirst, uint typeInfo, out int bytesRead)
{
var raw = ReadLengthPrefixed(payload, msbFirst, typeInfo, out bytesRead, out _);
return new DltRawArgument(raw.ToArray());
}
private static ReadOnlySpan<byte> ReadLengthPrefixed(
ReadOnlySpan<byte> payload,
bool msbFirst,
uint typeInfo,
out int bytesRead,
out bool complete
)
{
if (payload.Length < LengthFieldSize)
{
throw new DltParseException(
$"There is no length field for the argument. Type Info: {FormatTypeInfo(typeInfo)}."
);
}
int length = SpanReader.ReadUInt16(payload, msbFirst);
bytesRead = TypeInfoLength + LengthFieldSize + length;
var available = payload[LengthFieldSize..];
complete = length <= available.Length;
return complete ? available[..length] : available;
}
/// <summary>
/// Resolves the width of an integer argument. If the remaining data is shorter than the
/// declared width, the next smaller width is used, mirroring the behavior of DLT Viewer.
/// </summary>
private static DltIntegerWidth ResolveIntegerWidth(int available, uint typeInfo, string typeName)
{
var declared = (typeInfo & MaskTypeLength) switch
{
1 => DltIntegerWidth.Bits8,
2 => DltIntegerWidth.Bits16,
3 => DltIntegerWidth.Bits32,
4 => DltIntegerWidth.Bits64,
_ => throw UnsupportedTypeInfo(typeInfo),
};
foreach (
var width in (ReadOnlySpan<DltIntegerWidth>)
[DltIntegerWidth.Bits64, DltIntegerWidth.Bits32, DltIntegerWidth.Bits16, DltIntegerWidth.Bits8]
)
{
if (width <= declared && (int)width <= available)
{
return width;
}
}
throw new DltParseException(
$"There is no data for a {(int)declared * 8} bit {typeName}. " + $"Type Info: {FormatTypeInfo(typeInfo)}."
);
}
private static DltParseException UnsupportedTypeInfo(uint typeInfo) =>
new($"Unsupported Type Info: {FormatTypeInfo(typeInfo)}.");
private static string FormatTypeInfo(uint typeInfo) => $"0x{typeInfo:X8}";
}
src/Payloads/DltPayload.cs
+109
-0
diff --git a/src/Payloads/DltPayload.cs b/src/Payloads/DltPayload.cs
new file mode 100644
index 0000000..ca89103
@@ -0,0 +1,109 @@
using System.Text;
namespace SharpDlt;
/// <summary>The payload of a DLT message.</summary>
public abstract record DltPayload
{
/// <summary>Gets a human readable representation of the payload.</summary>
public abstract override string ToString();
}
/// <summary>The payload of a DLT message in non-verbose mode.</summary>
/// <param name="MessageId">Unique ID of a specific DLT message.</param>
/// <param name="NonStaticData">All non-static information of the DLT message.</param>
public sealed record NonVerbosePayload(uint MessageId, ReadOnlyMemory<byte> NonStaticData) : DltPayload
{
/// <summary>Minimum length of a non-verbose payload in bytes.</summary>
public const int MessageIdLength = 4;
/// <inheritdoc />
public override string ToString() => $"[{MessageId}] {Convert.ToHexString(NonStaticData.Span).ToLowerInvariant()}";
/// <summary>Determines whether the payload equals <paramref name="other"/> by value.</summary>
public bool Equals(NonVerbosePayload? other) =>
other is not null && MessageId == other.MessageId && NonStaticData.Span.SequenceEqual(other.NonStaticData.Span);
/// <inheritdoc />
public override int GetHashCode()
{
var hash = new HashCode();
hash.Add(MessageId);
hash.AddBytes(NonStaticData.Span);
return hash.ToHashCode();
}
/// <summary>Parses a non-verbose payload from <paramref name="data"/>.</summary>
/// <param name="data">The complete payload data.</param>
/// <param name="msbFirst">If set, the payload data is big endian, otherwise little endian.</param>
/// <exception cref="DltParseException">The data is shorter than the message ID.</exception>
public static NonVerbosePayload Parse(ReadOnlySpan<byte> data, bool msbFirst)
{
if (data.Length < MessageIdLength)
{
throw new DltParseException(
$"Unexpected length of the data: {data.Length} / "
+ $"payload of non-verbose mode must not be < {MessageIdLength}."
);
}
var messageId = Internal.SpanReader.ReadUInt32(data, msbFirst);
return new NonVerbosePayload(messageId, data[MessageIdLength..].ToArray());
}
}
/// <summary>The payload of a DLT message in verbose mode.</summary>
/// <param name="Arguments">The arguments of the payload.</param>
public sealed record VerbosePayload(IReadOnlyList<DltArgument> Arguments) : DltPayload
{
/// <inheritdoc />
public override string ToString() => string.Join(' ', Arguments);
/// <summary>Determines whether the payload equals <paramref name="other"/> by value.</summary>
public bool Equals(VerbosePayload? other) => other is not null && Arguments.SequenceEqual(other.Arguments);
/// <inheritdoc />
public override int GetHashCode()
{
var hash = new HashCode();
foreach (var argument in Arguments)
{
hash.Add(argument);
}
return hash.ToHashCode();
}
/// <summary>Parses a verbose payload from <paramref name="data"/>.</summary>
/// <param name="data">The complete payload data.</param>
/// <param name="msbFirst">If set, the payload data is big endian, otherwise little endian.</param>
/// <param name="numberOfArguments">Number of arguments within the payload.</param>
/// <param name="encoding">Encoding used for 8 bit coded strings. Defaults to ASCII.</param>
/// <exception cref="DltParseException">The data is malformed.</exception>
public static VerbosePayload Parse(
ReadOnlySpan<byte> data,
bool msbFirst,
int numberOfArguments,
Encoding? encoding = null
)
{
ArgumentOutOfRangeException.ThrowIfNegative(numberOfArguments);
var arguments = new List<DltArgument>(numberOfArguments);
var offset = 0;
for (var i = 0; i < numberOfArguments; i++)
{
if (offset > data.Length)
{
throw new DltParseException(
$"Unexpected end of the payload while reading argument {i + 1} of {numberOfArguments}."
);
}
arguments.Add(DltArgument.Parse(data[offset..], msbFirst, encoding, out var bytesRead));
offset += bytesRead;
}
return new VerbosePayload(arguments);
}
}
src/SharpDlt.csproj
+1
-2
diff --git a/src/SharpDlt.csproj b/src/SharpDlt.csproj
index b760144..5789f76 100644
@@ -1,9 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>
tests/AsyncEnumerableTestExtensions.cs
+18
-0
diff --git a/tests/AsyncEnumerableTestExtensions.cs b/tests/AsyncEnumerableTestExtensions.cs
new file mode 100644
index 0000000..234046a
@@ -0,0 +1,18 @@
namespace SharpDlt.Tests;
internal static class AsyncEnumerableTestExtensions
{
public static async Task<List<T>> ToListAsync<T>(
this IAsyncEnumerable<T> source,
CancellationToken cancellationToken = default
)
{
var items = new List<T>();
await foreach (var item in source.WithCancellation(cancellationToken))
{
items.Add(item);
}
return items;
}
}
tests/Data/issue24.dlt
+0
-0
diff --git a/tests/Data/issue24.dlt b/tests/Data/issue24.dlt
new file mode 100644
index 0000000..43b3b66
Binary files /dev/null and b/tests/Data/issue24.dlt differ
tests/DltArgumentTests.cs
+227
-0
diff --git a/tests/DltArgumentTests.cs b/tests/DltArgumentTests.cs
new file mode 100644
index 0000000..8e3c5b8
@@ -0,0 +1,227 @@
using System.Buffers.Binary;
using System.Text;
using Shouldly;
namespace SharpDlt.Tests;
public class DltArgumentTests
{
[Theory]
[InlineData(0, false)]
[InlineData(1, true)]
[InlineData(255, true)]
public void Parse_Bool(byte raw, bool expected)
{
var data = TestDlt.Argument(TestDlt.TypeInfoBool, [raw]);
var argument = DltArgument.Parse(data, msbFirst: false, encoding: null, out var bytesRead);
argument.ShouldBe(new DltBoolArgument(expected));
bytesRead.ShouldBe(5);
}
[Fact]
public void Parse_UnsignedIntegers()
{
AssertUnsigned(TestDlt.TypeInfoUInt8, [0xFF], 255, DltIntegerWidth.Bits8);
AssertUnsigned(TestDlt.TypeInfoUInt16, [0x00, 0x01], 256, DltIntegerWidth.Bits16);
AssertUnsigned(TestDlt.TypeInfoUInt32, [0x00, 0x00, 0x01, 0x00], 65536, DltIntegerWidth.Bits32);
AssertUnsigned(
TestDlt.TypeInfoUInt64,
[0, 0, 0, 0, 0, 0, 0, 0x80],
0x8000_0000_0000_0000,
DltIntegerWidth.Bits64
);
static void AssertUnsigned(uint typeInfo, byte[] payload, ulong expected, DltIntegerWidth width)
{
var data = TestDlt.Argument(typeInfo, payload);
var argument = DltArgument.Parse(data, false, null, out var bytesRead);
argument.ShouldBe(new DltUnsignedArgument(expected, width));
bytesRead.ShouldBe(4 + payload.Length);
}
}
[Fact]
public void Parse_SignedIntegers()
{
AssertSigned(TestDlt.TypeInfoSInt8, [0xFF], -1, DltIntegerWidth.Bits8);
AssertSigned(TestDlt.TypeInfoSInt16, [0xFF, 0xFF], -1, DltIntegerWidth.Bits16);
AssertSigned(TestDlt.TypeInfoSInt32, [0xFE, 0xFF, 0xFF, 0xFF], -2, DltIntegerWidth.Bits32);
AssertSigned(
TestDlt.TypeInfoSInt64,
[0xFD, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
-3,
DltIntegerWidth.Bits64
);
static void AssertSigned(uint typeInfo, byte[] payload, long expected, DltIntegerWidth width)
{
var data = TestDlt.Argument(typeInfo, payload);
var argument = DltArgument.Parse(data, false, null, out var bytesRead);
argument.ShouldBe(new DltSignedArgument(expected, width));
bytesRead.ShouldBe(4 + payload.Length);
}
}
[Fact]
public void Parse_BigEndianInteger()
{
var data = TestDlt.Argument(TestDlt.TypeInfoUInt32, [0x00, 0x01, 0x00, 0x00], msbFirst: true);
var argument = DltArgument.Parse(data, msbFirst: true, encoding: null, out _);
argument.ShouldBe(new DltUnsignedArgument(65536, DltIntegerWidth.Bits32));
}
[Fact]
public void Parse_Float32()
{
var payload = new byte[4];
BinaryPrimitives.WriteSingleLittleEndian(payload, 1.5f);
var data = TestDlt.Argument(TestDlt.TypeInfoFloat32, payload);
var argument = DltArgument.Parse(data, false, null, out var bytesRead);
argument.ShouldBe(new DltFloatArgument(1.5, DltFloatWidth.Bits32));
bytesRead.ShouldBe(8);
}
[Fact]
public void Parse_Float64()
{
var payload = new byte[8];
BinaryPrimitives.WriteDoubleBigEndian(payload, -2.25);
var data = TestDlt.Argument(TestDlt.TypeInfoFloat64, payload, msbFirst: true);
var argument = DltArgument.Parse(data, true, null, out var bytesRead);
argument.ShouldBe(new DltFloatArgument(-2.25, DltFloatWidth.Bits64));
bytesRead.ShouldBe(12);
}
[Fact]
public void Parse_AsciiString()
{
var data = TestDlt.StringArgument("hello");
var argument = DltArgument.Parse(data, false, null, out var bytesRead);
argument.ShouldBe(new DltStringArgument("hello", DltStringCoding.Ascii));
bytesRead.ShouldBe(data.Length);
}
[Fact]
public void Parse_Utf8String()
{
var data = TestDlt.StringArgument("hällö", utf8: true);
var argument = DltArgument.Parse(data, false, null, out var bytesRead);
argument.ShouldBe(new DltStringArgument("hällö", DltStringCoding.Utf8));
bytesRead.ShouldBe(data.Length);
}
[Fact]
public void Parse_EmptyString()
{
var data = TestDlt.StringArgument(string.Empty);
DltArgument
.Parse(data, false, null, out _)
.ShouldBe(new DltStringArgument(string.Empty, DltStringCoding.Ascii));
}
[Fact]
public void Parse_EightBitStringUsesConfiguredEncoding()
{
var latin1 = Encoding.Latin1;
var encoded = latin1.GetBytes("Grüße");
var payload = new byte[2 + encoded.Length + 1];
BinaryPrimitives.WriteUInt16LittleEndian(payload, (ushort)(encoded.Length + 1));
encoded.CopyTo(payload.AsSpan(2));
var data = TestDlt.Argument(TestDlt.TypeInfoStringAscii, payload);
DltArgument.Parse(data, false, latin1, out _).ShouldBe(new DltStringArgument("Grüße", DltStringCoding.Ascii));
}
[Fact]
public void Parse_Raw()
{
var data = TestDlt.RawArgument([0xDE, 0xAD, 0xBE, 0xEF]);
var argument = DltArgument.Parse(data, false, null, out var bytesRead);
argument.ShouldBe(new DltRawArgument(new byte[] { 0xDE, 0xAD, 0xBE, 0xEF }));
argument.ToString().ShouldBe("deadbeef");
bytesRead.ShouldBe(10);
}
[Fact]
public void Parse_TruncatedInteger_FallsBackToSmallerWidth()
{
// A 64 bit unsigned integer with only 4 bytes of data is read as a 32 bit integer,
// which mirrors the behavior of DLT Viewer.
var data = TestDlt.Argument(TestDlt.TypeInfoUInt64, [0x00, 0x00, 0x01, 0x00]);
var argument = DltArgument.Parse(data, false, null, out var bytesRead);
argument.ShouldBe(new DltUnsignedArgument(65536, DltIntegerWidth.Bits32));
bytesRead.ShouldBe(8);
}
[Fact]
public void Parse_IntegerWithoutAnyData_Throws()
{
var data = TestDlt.Argument(TestDlt.TypeInfoUInt32, []);
Should.Throw<DltParseException>(() => DltArgument.Parse(data, false, null, out _));
}
[Fact]
public void Parse_TruncatedFloat_Throws()
{
var data = TestDlt.Argument(TestDlt.TypeInfoFloat64, [0x00, 0x00]);
Should.Throw<DltParseException>(() => DltArgument.Parse(data, false, null, out _));
}
[Fact]
public void Parse_TooShortForTypeInfo_Throws()
{
Should.Throw<DltParseException>(() => DltArgument.Parse(new byte[3], false, null, out _));
}
[Fact]
public void Parse_UnsupportedType_Throws()
{
// TYPE_ARRAY is not supported.
var data = TestDlt.Argument(0x00000100, [0x00, 0x00, 0x00, 0x00]);
Should.Throw<DltParseException>(() => DltArgument.Parse(data, false, null, out _));
}
[Fact]
public void ToString_UsesInvariantFormatting()
{
new DltBoolArgument(true).ToString().ShouldBe("True");
new DltSignedArgument(-5, DltIntegerWidth.Bits32).ToString().ShouldBe("-5");
new DltUnsignedArgument(7, DltIntegerWidth.Bits8).ToString().ShouldBe("7");
new DltFloatArgument(1.5, DltFloatWidth.Bits32).ToString().ShouldBe("1.5");
new DltStringArgument("text", DltStringCoding.Ascii).ToString().ShouldBe("text");
}
[Fact]
public void RawArgument_EqualityComparesContent()
{
new DltRawArgument(new byte[] { 1, 2 }).ShouldBe(new DltRawArgument(new byte[] { 1, 2 }));
new DltRawArgument(new byte[] { 1, 2 })
.GetHashCode()
.ShouldBe(new DltRawArgument(new byte[] { 1, 2 }).GetHashCode());
new DltRawArgument(new byte[] { 1, 2 }).ShouldNotBe(new DltRawArgument(new byte[] { 1 }));
}
}
tests/DltFileTests.cs
+33
-0
diff --git a/tests/DltFileTests.cs b/tests/DltFileTests.cs
new file mode 100644
index 0000000..9ab650c
@@ -0,0 +1,33 @@
using Shouldly;
namespace SharpDlt.Tests;
/// <summary>Parses a real world DLT file recorded by a target.</summary>
public class DltFileTests
{
private static string DataPath(string fileName) => Path.Combine(AppContext.BaseDirectory, "Data", fileName);
[Fact]
public void ReadMessages_Issue24File_ReducesTruncatedNumericArgument()
{
// The last numeric argument of the payload does not have enough bytes for its
// declared type, so it is reduced to the next smaller type, like DLT Viewer does.
var messages = DltReader.ReadMessages(DataPath("issue24.dlt")).ToList();
var message = messages.ShouldHaveSingleItem();
message.IsVerbose.ShouldBeTrue();
var arguments = message.VerbosePayload.ShouldNotBeNull().Arguments;
arguments.Count.ShouldBe(2);
arguments[0].ShouldBe(new DltStringArgument("HIST: SCU_RSTSTAT:", DltStringCoding.Ascii));
arguments[1].ShouldBe(new DltUnsignedArgument(65536, DltIntegerWidth.Bits32));
}
[Fact]
public async Task ReadMessagesAsync_Issue24File_YieldsTheSameMessages()
{
var sync = DltReader.ReadMessages(DataPath("issue24.dlt")).ToList();
var async = await DltReader.ReadMessagesAsync(DataPath("issue24.dlt")).ToListAsync();
async.ShouldBe(sync);
}
}
tests/DltMessageTests.cs
+152
-0
diff --git a/tests/DltMessageTests.cs b/tests/DltMessageTests.cs
new file mode 100644
index 0000000..e397e2c
@@ -0,0 +1,152 @@
using Shouldly;
namespace SharpDlt.Tests;
public class DltMessageTests
{
[Fact]
public void Parse_VerboseMessageWithStorageHeader()
{
var data = TestDlt.VerboseMessage([TestDlt.StringArgument("hello")], timestamp: 12_3456);
var message = DltMessage.Parse(data);
message.StorageHeader.ShouldNotBeNull().EcuId.ShouldBe("ECU1");
message.StandardHeader.UseExtendedHeader.ShouldBeTrue();
message.StandardHeader.Timestamp.ShouldBe(123_456u);
message.ExtendedHeader.ShouldNotBeNull().ApplicationId.ShouldBe("APP1");
message.IsVerbose.ShouldBeTrue();
message
.VerbosePayload.ShouldNotBeNull()
.Arguments.ShouldHaveSingleItem()
.ShouldBe(new DltStringArgument("hello", DltStringCoding.Ascii));
message.NonVerbosePayload.ShouldBeNull();
}
[Fact]
public void Parse_WithoutStorageHeader()
{
var data = TestDlt.VerboseMessage([TestDlt.StringArgument("x")], withStorageHeader: false);
var message = DltMessage.Parse(data, new DltReaderOptions { HasStorageHeader = false });
message.StorageHeader.ShouldBeNull();
message.VerbosePayload.ShouldNotBeNull().Arguments.Count.ShouldBe(1);
}
[Fact]
public void Parse_NonVerboseMessage()
{
var data = TestDlt.Message(
TestDlt.StorageHeader(),
TestDlt.StandardHeader(useExtendedHeader: true),
TestDlt.ExtendedHeader(
verbose: false,
messageType: DltMessageType.Control,
messageTypeInfo: (byte)DltControlInfo.Response,
numberOfArguments: 0
),
[.. TestDlt.LittleEndian(42), 0xAB]
);
var message = DltMessage.Parse(data);
message.IsVerbose.ShouldBeFalse();
var payload = message.NonVerbosePayload.ShouldNotBeNull();
payload.MessageId.ShouldBe(42u);
payload.NonStaticData.ToArray().ShouldBe(new byte[] { 0xAB });
}
[Fact]
public void Parse_MessageWithoutExtendedHeaderAndWithoutPayload()
{
var data = TestDlt.Message(TestDlt.StorageHeader(), TestDlt.StandardHeader());
var message = DltMessage.Parse(data);
message.ExtendedHeader.ShouldBeNull();
message.Payload.ShouldBeNull();
message.IsVerbose.ShouldBeFalse();
}
[Fact]
public void Parse_BigEndianPayload()
{
var data = TestDlt.VerboseMessage(
[TestDlt.Argument(TestDlt.TypeInfoUInt32, [0x00, 0x01, 0x00, 0x00], msbFirst: true)],
msbFirst: true
);
var message = DltMessage.Parse(data);
message.StandardHeader.MsbFirst.ShouldBeTrue();
message
.VerbosePayload.ShouldNotBeNull()
.Arguments[0]
.ShouldBe(new DltUnsignedArgument(65536, DltIntegerWidth.Bits32));
}
[Fact]
public void Parse_IgnoresDataBeyondTheDeclaredMessageLength()
{
var data = TestDlt.VerboseMessage([TestDlt.StringArgument("hi")]).Concat<byte>([0xFF, 0xFF, 0xFF]).ToArray();
var message = DltMessage.Parse(data);
message
.VerbosePayload.ShouldNotBeNull()
.Arguments.ShouldHaveSingleItem()
.ShouldBe(new DltStringArgument("hi", DltStringCoding.Ascii));
}
[Fact]
public void ToString_DescribesTheMessage()
{
var data = TestDlt.Message(
TestDlt.StorageHeader(seconds: 1_600_000_000, microseconds: 123, ecuId: "ECU1"),
TestDlt.StandardHeader(useExtendedHeader: true, messageCounter: 9, timestamp: 12_345),
TestDlt.ExtendedHeader(messageType: DltMessageType.Log, messageTypeInfo: (byte)DltLogInfo.Warn),
TestDlt.StringArgument("hello")
);
DltMessage
.Parse(data)
.ToString()
.ShouldBe("2020/09/13 12:26:40.000123 1.2345 9 ECU1 APP1 CTX1 log warn verbose 1 hello");
}
[Fact]
public void ToString_NonVerboseWithoutStorageHeader()
{
var data = TestDlt.Message(
null,
TestDlt.StandardHeader(useExtendedHeader: true, messageCounter: 3, sessionId: 7),
TestDlt.ExtendedHeader(
verbose: false,
messageType: DltMessageType.Control,
messageTypeInfo: (byte)DltControlInfo.Request,
numberOfArguments: 0
),
TestDlt.LittleEndian(5)
);
DltMessage
.Parse(data, new DltReaderOptions { HasStorageHeader = false })
.ToString()
.ShouldBe("3 APP1 CTX1 7 control request non-verbose 0 [5] ");
}
[Fact]
public void Parse_MalformedData_Throws()
{
Should.Throw<DltParseException>(() => DltMessage.Parse(new byte[10]));
}
[Fact]
public void Equality_IsByValue()
{
var data = TestDlt.VerboseMessage([TestDlt.StringArgument("hello")]);
DltMessage.Parse(data).ShouldBe(DltMessage.Parse(data));
}
}
tests/DltPayloadTests.cs
+87
-0
diff --git a/tests/DltPayloadTests.cs b/tests/DltPayloadTests.cs
new file mode 100644
index 0000000..fe4745c
@@ -0,0 +1,87 @@
using Shouldly;
namespace SharpDlt.Tests;
public class DltPayloadTests
{
[Fact]
public void NonVerbosePayload_ParsesMessageIdAndData()
{
var data = TestDlt.LittleEndian(0x12345678).Concat<byte>([1, 2, 3]).ToArray();
var payload = NonVerbosePayload.Parse(data, msbFirst: false);
payload.MessageId.ShouldBe(0x12345678u);
payload.NonStaticData.ToArray().ShouldBe(new byte[] { 1, 2, 3 });
payload.ToString().ShouldBe("[305419896] 010203");
}
[Fact]
public void NonVerbosePayload_ParsesBigEndianMessageId()
{
var data = TestDlt.BigEndian(0x12345678u);
var payload = NonVerbosePayload.Parse(data, msbFirst: true);
payload.MessageId.ShouldBe(0x12345678u);
payload.NonStaticData.Length.ShouldBe(0);
}
[Fact]
public void NonVerbosePayload_TooShort_Throws()
{
Should.Throw<DltParseException>(() => NonVerbosePayload.Parse(new byte[3], false));
}
[Fact]
public void NonVerbosePayload_EqualityComparesContent()
{
new NonVerbosePayload(1, new byte[] { 1, 2 }).ShouldBe(new NonVerbosePayload(1, new byte[] { 1, 2 }));
new NonVerbosePayload(1, new byte[] { 1, 2 }).ShouldNotBe(new NonVerbosePayload(2, new byte[] { 1, 2 }));
}
[Fact]
public void VerbosePayload_ParsesEveryArgumentInOrder()
{
var data = TestDlt
.StringArgument("value")
.Concat(TestDlt.Argument(TestDlt.TypeInfoUInt16, [0x2A, 0x00]))
.Concat(TestDlt.Argument(TestDlt.TypeInfoBool, [1]))
.ToArray();
var payload = VerbosePayload.Parse(data, msbFirst: false, numberOfArguments: 3);
payload.Arguments.ShouldBe([
new DltStringArgument("value", DltStringCoding.Ascii),
new DltUnsignedArgument(42, DltIntegerWidth.Bits16),
new DltBoolArgument(true),
]);
payload.ToString().ShouldBe("value 42 True");
}
[Fact]
public void VerbosePayload_WithoutArguments_IsEmpty()
{
var payload = VerbosePayload.Parse([], msbFirst: false, numberOfArguments: 0);
payload.Arguments.ShouldBeEmpty();
payload.ToString().ShouldBeEmpty();
}
[Fact]
public void VerbosePayload_MoreArgumentsThanData_Throws()
{
var data = TestDlt.StringArgument("value");
Should.Throw<DltParseException>(() => VerbosePayload.Parse(data, false, 2));
}
[Fact]
public void VerbosePayload_EqualityComparesArguments()
{
var payload = new VerbosePayload([new DltBoolArgument(true)]);
payload.ShouldBe(new VerbosePayload([new DltBoolArgument(true)]));
payload.ShouldNotBe(new VerbosePayload([new DltBoolArgument(false)]));
}
}
tests/DltReaderTests.cs
+181
-0
diff --git a/tests/DltReaderTests.cs b/tests/DltReaderTests.cs
new file mode 100644
index 0000000..e7f1c0a
@@ -0,0 +1,181 @@
using Shouldly;
namespace SharpDlt.Tests;
public class DltReaderTests
{
private static byte[] TwoMessages() =>
[
.. TestDlt.VerboseMessage([TestDlt.StringArgument("first")], messageCounter: 1),
.. TestDlt.VerboseMessage([TestDlt.StringArgument("second")], messageCounter: 2),
];
[Fact]
public void ReadMessages_FromStream_ReadsEveryMessage()
{
using var stream = new MemoryStream(TwoMessages());
var messages = DltReader.ReadMessages(stream).ToList();
messages.Count.ShouldBe(2);
messages[0].StandardHeader.MessageCounter.ShouldBe((byte)1);
messages[0].ToString().ShouldEndWith("first");
messages[1].StandardHeader.MessageCounter.ShouldBe((byte)2);
messages[1].ToString().ShouldEndWith("second");
}
[Fact]
public void ReadMessages_FromEmptyStream_ReturnsNothing()
{
using var stream = new MemoryStream();
DltReader.ReadMessages(stream).ShouldBeEmpty();
}
[Fact]
public void ReadMessages_IsLazy()
{
using var stream = new MemoryStream(TwoMessages());
var messages = DltReader.ReadMessages(stream);
stream.Position.ShouldBe(0);
messages.First();
stream.Position.ShouldBeGreaterThan(0);
stream.Position.ShouldBeLessThan(stream.Length);
}
[Fact]
public void ReadMessages_TruncatedTrailingMessage_IsIgnored()
{
var data = TwoMessages();
using var stream = new MemoryStream(data[..^3]);
DltReader.ReadMessages(stream).Count().ShouldBe(1);
}
[Fact]
public void ReadMessages_TrailingBytesShorterThanAHeader_AreIgnored()
{
var data = TwoMessages().Concat<byte>([0x44, 0x4C]).ToArray();
using var stream = new MemoryStream(data);
DltReader.ReadMessages(stream).Count().ShouldBe(2);
}
[Fact]
public void ReadMessages_WithoutStorageHeader()
{
var data = TestDlt.VerboseMessage([TestDlt.StringArgument("no storage")], withStorageHeader: false);
using var stream = new MemoryStream(data);
var messages = DltReader.ReadMessages(stream, new DltReaderOptions { HasStorageHeader = false }).ToList();
messages.ShouldHaveSingleItem().StorageHeader.ShouldBeNull();
}
[Fact]
public void ReadMessages_InvalidLengthField_Throws()
{
var data = TestDlt.Message(TestDlt.StorageHeader(), TestDlt.StandardHeader());
data[18] = 0;
data[19] = 3;
using var stream = new MemoryStream(data);
Should.Throw<DltParseException>(() => DltReader.ReadMessages(stream).ToList());
}
[Fact]
public void ReadMessages_InvalidStorageHeader_Throws()
{
var data = TwoMessages();
data[0] = (byte)'X';
using var stream = new MemoryStream(data);
Should.Throw<DltParseException>(() => DltReader.ReadMessages(stream).ToList());
}
[Fact]
public void ReadMessages_NullStream_Throws()
{
Should.Throw<ArgumentNullException>(() => DltReader.ReadMessages((Stream)null!));
}
[Fact]
public void ReadMessages_EmptyPath_Throws()
{
Should.Throw<ArgumentException>(() => DltReader.ReadMessages(string.Empty));
}
[Fact]
public void ReadMessages_FromFile_ReadsEveryMessageAndClosesTheFile()
{
var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.dlt");
File.WriteAllBytes(path, TwoMessages());
try
{
var messages = DltReader.ReadMessages(path).ToList();
messages.Count.ShouldBe(2);
// The file is closed again, so it can be deleted.
File.Delete(path);
File.Exists(path).ShouldBeFalse();
}
finally
{
if (File.Exists(path))
{
File.Delete(path);
}
}
}
[Fact]
public void ReadMessages_MissingFile_Throws()
{
var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.dlt");
Should.Throw<FileNotFoundException>(() => DltReader.ReadMessages(path).ToList());
}
[Fact]
public async Task ReadMessagesAsync_FromStream_ReadsEveryMessage()
{
using var stream = new MemoryStream(TwoMessages());
var messages = await DltReader.ReadMessagesAsync(stream).ToListAsync();
messages.Count.ShouldBe(2);
messages[1].ToString().ShouldEndWith("second");
}
[Fact]
public async Task ReadMessagesAsync_FromFile_ReadsEveryMessage()
{
var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.dlt");
await File.WriteAllBytesAsync(path, TwoMessages());
try
{
var messages = await DltReader.ReadMessagesAsync(path).ToListAsync();
messages.Count.ShouldBe(2);
}
finally
{
File.Delete(path);
}
}
[Fact]
public async Task ReadMessagesAsync_Cancellation_Throws()
{
using var stream = new MemoryStream(TwoMessages());
using var cancellation = new CancellationTokenSource();
await cancellation.CancelAsync();
await Should.ThrowAsync<OperationCanceledException>(async () =>
await DltReader.ReadMessagesAsync(stream, cancellationToken: cancellation.Token).ToListAsync()
);
}
}
tests/ExtendedHeaderTests.cs
+74
-0
diff --git a/tests/ExtendedHeaderTests.cs b/tests/ExtendedHeaderTests.cs
new file mode 100644
index 0000000..87f5cd8
@@ -0,0 +1,74 @@
using Shouldly;
namespace SharpDlt.Tests;
public class ExtendedHeaderTests
{
[Fact]
public void Parse_ReadsAllFields()
{
var data = TestDlt.ExtendedHeader(
verbose: true,
messageType: DltMessageType.Log,
messageTypeInfo: (byte)DltLogInfo.Warn,
numberOfArguments: 3,
applicationId: "APP1",
contextId: "CTX1"
);
var header = ExtendedHeader.Parse(data);
header.Verbose.ShouldBeTrue();
header.MessageType.ShouldBe(DltMessageType.Log);
header.MessageTypeInfo.ShouldBe((byte)DltLogInfo.Warn);
header.LogInfo.ShouldBe(DltLogInfo.Warn);
header.NumberOfArguments.ShouldBe((byte)3);
header.ApplicationId.ShouldBe("APP1");
header.ContextId.ShouldBe("CTX1");
header.MessageInfo.ShouldBe(data[0]);
}
[Theory]
[InlineData(DltMessageType.Log, (byte)DltLogInfo.Fatal)]
[InlineData(DltMessageType.AppTrace, (byte)DltTraceInfo.State)]
[InlineData(DltMessageType.NetworkTrace, (byte)DltBusInfo.Can)]
[InlineData(DltMessageType.Control, (byte)DltControlInfo.Response)]
public void Parse_DecodesMessageTypeAndTypeInfo(DltMessageType messageType, byte typeInfo)
{
var data = TestDlt.ExtendedHeader(verbose: false, messageType: messageType, messageTypeInfo: typeInfo);
var header = ExtendedHeader.Parse(data);
header.Verbose.ShouldBeFalse();
header.MessageType.ShouldBe(messageType);
header.MessageTypeInfo.ShouldBe(typeInfo);
}
[Fact]
public void TypedTypeInfoAccessors_ReinterpretTheSameValue()
{
var header = ExtendedHeader.Parse(TestDlt.ExtendedHeader(messageTypeInfo: (byte)DltTraceInfo.FunctionIn));
header.TraceInfo.ShouldBe(DltTraceInfo.FunctionIn);
header.BusInfo.ShouldBe(DltBusInfo.Can);
header.ControlInfo.ShouldBe(DltControlInfo.Response);
header.LogInfo.ShouldBe(DltLogInfo.Error);
}
[Fact]
public void Parse_StripsNulPaddingOfIds()
{
var data = TestDlt.ExtendedHeader(applicationId: "AP", contextId: "C");
var header = ExtendedHeader.Parse(data);
header.ApplicationId.ShouldBe("AP");
header.ContextId.ShouldBe("C");
}
[Fact]
public void Parse_TooShortData_Throws()
{
Should.Throw<DltParseException>(() => ExtendedHeader.Parse(new byte[9]));
}
}
tests/SharpDlt.Tests.csproj
+3
-1
diff --git a/tests/SharpDlt.Tests.csproj b/tests/SharpDlt.Tests.csproj
index c2bd3b2..be91b33 100644
@@ -1,5 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
@@ -28,4 +27,7 @@
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<None Include="Data\**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
tests/StandardHeaderTests.cs
+111
-0
diff --git a/tests/StandardHeaderTests.cs b/tests/StandardHeaderTests.cs
new file mode 100644
index 0000000..5a79663
@@ -0,0 +1,111 @@
using Shouldly;
namespace SharpDlt.Tests;
public class StandardHeaderTests
{
[Fact]
public void Parse_MinimalHeader()
{
var data = TestDlt.StandardHeader(length: 4, messageCounter: 7);
var header = StandardHeader.Parse(data);
header.UseExtendedHeader.ShouldBeFalse();
header.MsbFirst.ShouldBeFalse();
header.VersionNumber.ShouldBe(1);
header.MessageCounter.ShouldBe((byte)7);
header.Length.ShouldBe((ushort)4);
header.EcuId.ShouldBeNull();
header.SessionId.ShouldBeNull();
header.Timestamp.ShouldBeNull();
header.ByteLength.ShouldBe(4);
}
[Fact]
public void Parse_AllOptionalFields()
{
var data = TestDlt.StandardHeader(
useExtendedHeader: true,
msbFirst: true,
versionNumber: 1,
messageCounter: 42,
length: 16,
ecuId: "ECU1",
sessionId: 0x11223344,
timestamp: 0x55667788
);
var header = StandardHeader.Parse(data);
header.UseExtendedHeader.ShouldBeTrue();
header.MsbFirst.ShouldBeTrue();
header.EcuId.ShouldBe("ECU1");
header.SessionId.ShouldBe(0x11223344u);
header.Timestamp.ShouldBe(0x55667788u);
header.ByteLength.ShouldBe(16);
header.WithEcuId.ShouldBeTrue();
header.WithSessionId.ShouldBeTrue();
header.WithTimestamp.ShouldBeTrue();
}
[Theory]
[InlineData(false, false, null, null, null, 4)]
[InlineData(false, false, "ECU1", null, null, 8)]
[InlineData(true, true, "ECU1", 1u, null, 12)]
[InlineData(true, true, "ECU1", 1u, 2u, 16)]
public void Parse_RoundTripsHeaderType(
bool useExtendedHeader,
bool msbFirst,
string? ecuId,
uint? sessionId,
uint? timestamp,
int expectedByteLength
)
{
var data = TestDlt.StandardHeader(
useExtendedHeader,
msbFirst,
ecuId: ecuId,
sessionId: sessionId,
timestamp: timestamp
);
var header = StandardHeader.Parse(data);
header.ByteLength.ShouldBe(expectedByteLength);
header.HeaderType.ShouldBe(data[0]);
}
[Fact]
public void Parse_LengthIsBigEndian()
{
var data = TestDlt.StandardHeader(length: 0x0102);
StandardHeader.Parse(data).Length.ShouldBe((ushort)0x0102);
data[2].ShouldBe((byte)0x01);
data[3].ShouldBe((byte)0x02);
}
[Fact]
public void ReadMessageLength_ReadsLengthWithoutParsingTheHeader()
{
var data = TestDlt.StandardHeader(length: 0x0123, ecuId: "ECU1");
StandardHeader.ReadMessageLength(data).ShouldBe((ushort)0x0123);
}
[Fact]
public void Parse_TooShortData_Throws()
{
Should.Throw<DltParseException>(() => StandardHeader.Parse(new byte[3]));
}
[Fact]
public void Parse_OptionalFieldsAnnouncedButMissing_Throws()
{
var data = TestDlt.StandardHeader(ecuId: "ECU1", timestamp: 1)[..8];
Should.Throw<DltParseException>(() => StandardHeader.Parse(data));
}
}
tests/StorageHeaderTests.cs
+66
-0
diff --git a/tests/StorageHeaderTests.cs b/tests/StorageHeaderTests.cs
new file mode 100644
index 0000000..69496ff
@@ -0,0 +1,66 @@
using Shouldly;
namespace SharpDlt.Tests;
public class StorageHeaderTests
{
[Fact]
public void Parse_ReadsAllFields()
{
var data = TestDlt.StorageHeader(seconds: 1_600_000_000, microseconds: 123_456, ecuId: "ECU");
var header = StorageHeader.Parse(data);
header.Seconds.ShouldBe(1_600_000_000u);
header.Microseconds.ShouldBe(123_456);
header.EcuId.ShouldBe("ECU");
}
[Fact]
public void Parse_StripsNulPaddingOfEcuId()
{
var data = TestDlt.StorageHeader(ecuId: "AB");
StorageHeader.Parse(data).EcuId.ShouldBe("AB");
}
[Fact]
public void Parse_IgnoresTrailingData()
{
var data = TestDlt.StorageHeader().Concat<byte>([1, 2, 3]).ToArray();
StorageHeader.Parse(data).EcuId.ShouldBe("ECU1");
}
[Fact]
public void Timestamp_CombinesSecondsAndMicroseconds()
{
var header = new StorageHeader(1_600_000_000, 500_000, "ECU1");
header.Timestamp.ShouldBe(DateTimeOffset.FromUnixTimeSeconds(1_600_000_000).AddMilliseconds(500));
}
[Fact]
public void Parse_TooShortData_Throws()
{
var data = TestDlt.StorageHeader()[..15];
Should.Throw<DltParseException>(() => StorageHeader.Parse(data));
}
[Fact]
public void Parse_WithoutDltPattern_Throws()
{
var data = TestDlt.StorageHeader();
data[0] = (byte)'X';
Should.Throw<DltParseException>(() => StorageHeader.Parse(data));
}
[Fact]
public void Equality_IsByValue()
{
new StorageHeader(1, 2, "ECU1").ShouldBe(new StorageHeader(1, 2, "ECU1"));
new StorageHeader(1, 2, "ECU1").ShouldNotBe(new StorageHeader(1, 3, "ECU1"));
}
}
tests/TestDlt.cs
+223
-0
diff --git a/tests/TestDlt.cs b/tests/TestDlt.cs
new file mode 100644
index 0000000..8227437
@@ -0,0 +1,223 @@
using System.Buffers.Binary;
using System.Text;
namespace SharpDlt.Tests;
/// <summary>Builds raw DLT byte sequences to feed the parser under test.</summary>
internal static class TestDlt
{
public const uint TypeInfoBool = 0x00000011;
public const uint TypeInfoUInt8 = 0x00000041;
public const uint TypeInfoUInt16 = 0x00000042;
public const uint TypeInfoUInt32 = 0x00000043;
public const uint TypeInfoUInt64 = 0x00000044;
public const uint TypeInfoSInt8 = 0x00000021;
public const uint TypeInfoSInt16 = 0x00000022;
public const uint TypeInfoSInt32 = 0x00000023;
public const uint TypeInfoSInt64 = 0x00000024;
public const uint TypeInfoFloat32 = 0x00000083;
public const uint TypeInfoFloat64 = 0x00000084;
public const uint TypeInfoStringAscii = 0x00000200;
public const uint TypeInfoStringUtf8 = 0x00008200;
public const uint TypeInfoRaw = 0x00000400;
public static byte[] StorageHeader(uint seconds = 1_000_000, int microseconds = 123_456, string ecuId = "ECU1")
{
var data = new byte[16];
"DLT\u0001"u8.CopyTo(data);
BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(4), seconds);
BinaryPrimitives.WriteInt32LittleEndian(data.AsSpan(8), microseconds);
WriteAsciiField(data.AsSpan(12, 4), ecuId);
return data;
}
public static byte[] StandardHeader(
bool useExtendedHeader = false,
bool msbFirst = false,
int versionNumber = 1,
byte messageCounter = 0,
ushort length = 4,
string? ecuId = null,
uint? sessionId = null,
uint? timestamp = null
)
{
var headerType = (versionNumber << 5) & 0b1110_0000;
if (useExtendedHeader)
{
headerType |= 0b0000_0001;
}
if (msbFirst)
{
headerType |= 0b0000_0010;
}
if (ecuId is not null)
{
headerType |= 0b0000_0100;
}
if (sessionId is not null)
{
headerType |= 0b0000_1000;
}
if (timestamp is not null)
{
headerType |= 0b0001_0000;
}
var data = new List<byte> { (byte)headerType, messageCounter };
data.AddRange(BigEndian(length));
if (ecuId is not null)
{
var field = new byte[4];
WriteAsciiField(field, ecuId);
data.AddRange(field);
}
if (sessionId is { } session)
{
data.AddRange(BigEndian(session));
}
if (timestamp is { } stamp)
{
data.AddRange(BigEndian(stamp));
}
return [.. data];
}
public static byte[] ExtendedHeader(
bool verbose = true,
DltMessageType messageType = DltMessageType.Log,
byte messageTypeInfo = (byte)DltLogInfo.Info,
byte numberOfArguments = 1,
string applicationId = "APP1",
string contextId = "CTX1"
)
{
var messageInfo = verbose ? 0b0000_0001 : 0;
messageInfo |= ((int)messageType << 1) & 0b0000_1110;
messageInfo |= (messageTypeInfo << 4) & 0b1111_0000;
var data = new byte[10];
data[0] = (byte)messageInfo;
data[1] = numberOfArguments;
WriteAsciiField(data.AsSpan(2, 4), applicationId);
WriteAsciiField(data.AsSpan(6, 4), contextId);
return data;
}
public static byte[] Argument(uint typeInfo, ReadOnlySpan<byte> payload, bool msbFirst = false)
{
var data = new byte[4 + payload.Length];
if (msbFirst)
{
BinaryPrimitives.WriteUInt32BigEndian(data, typeInfo);
}
else
{
BinaryPrimitives.WriteUInt32LittleEndian(data, typeInfo);
}
payload.CopyTo(data.AsSpan(4));
return data;
}
public static byte[] StringArgument(string value, bool utf8 = false, bool msbFirst = false)
{
var encoded = (utf8 ? Encoding.UTF8 : Encoding.ASCII).GetBytes(value);
var payload = new byte[2 + encoded.Length + 1];
WriteUInt16(payload, (ushort)(encoded.Length + 1), msbFirst);
encoded.CopyTo(payload.AsSpan(2));
return Argument(utf8 ? TypeInfoStringUtf8 : TypeInfoStringAscii, payload, msbFirst);
}
public static byte[] RawArgument(ReadOnlySpan<byte> value, bool msbFirst = false)
{
var payload = new byte[2 + value.Length];
WriteUInt16(payload, (ushort)value.Length, msbFirst);
value.CopyTo(payload.AsSpan(2));
return Argument(TypeInfoRaw, payload, msbFirst);
}
/// <summary>Composes a complete message and fills in the length field of the Standard Header.</summary>
public static byte[] Message(
byte[]? storageHeader,
byte[] standardHeader,
byte[]? extendedHeader = null,
byte[]? payload = null
)
{
var length = standardHeader.Length + (extendedHeader?.Length ?? 0) + (payload?.Length ?? 0);
var standard = (byte[])standardHeader.Clone();
BinaryPrimitives.WriteUInt16BigEndian(standard.AsSpan(2), (ushort)length);
return [.. storageHeader ?? [], .. standard, .. extendedHeader ?? [], .. payload ?? []];
}
/// <summary>Composes a verbose log message with the given arguments.</summary>
public static byte[] VerboseMessage(
IEnumerable<byte[]> arguments,
bool msbFirst = false,
byte messageCounter = 0,
uint? timestamp = null,
bool withStorageHeader = true
)
{
var argumentList = arguments.ToArray();
return Message(
withStorageHeader ? StorageHeader() : null,
StandardHeader(
useExtendedHeader: true,
msbFirst: msbFirst,
messageCounter: messageCounter,
timestamp: timestamp
),
ExtendedHeader(numberOfArguments: (byte)argumentList.Length),
[.. argumentList.SelectMany(argument => argument)]
);
}
public static byte[] BigEndian(ushort value)
{
var data = new byte[2];
BinaryPrimitives.WriteUInt16BigEndian(data, value);
return data;
}
public static byte[] BigEndian(uint value)
{
var data = new byte[4];
BinaryPrimitives.WriteUInt32BigEndian(data, value);
return data;
}
public static byte[] LittleEndian(uint value)
{
var data = new byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(data, value);
return data;
}
private static void WriteUInt16(Span<byte> destination, ushort value, bool msbFirst)
{
if (msbFirst)
{
BinaryPrimitives.WriteUInt16BigEndian(destination, value);
}
else
{
BinaryPrimitives.WriteUInt16LittleEndian(destination, value);
}
}
private static void WriteAsciiField(Span<byte> destination, string value)
{
destination.Clear();
Encoding.ASCII.GetBytes(value.AsSpan(0, Math.Min(value.Length, destination.Length)), destination);
}
}
tests/UnitTest1.cs
+0
-10
diff --git a/tests/UnitTest1.cs b/tests/UnitTest1.cs
deleted file mode 100644
index 6dc3ecc..0000000
@@ -1,10 +0,0 @@
namespace SharpDlt.Tests;
public class Tests
{
[Fact]
public void Test1()
{
// Pass
}
}