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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
#
# 3. Follow the SonarCloud in-product tutorial
# * a. Copy/paste the Project Key and the Organization Key into the args parameter below
# (You'll find this information in SonarCloud. Click on "Information" at the bottom left)
# (You'll find this information in SonarCloud. Click on "Information" at the bottom left)
#
# * b. Generate a new token and add it to your Github repository's secrets using the name SONAR_TOKEN
# (On SonarCloud, click on your avatar on top-right > My account > Security
# (On SonarCloud, click on your avatar on top-right > My account > Security
# or go directly to https://sonarcloud.io/account/security/)

# Feel free to take a look at our documentation (https://docs.sonarcloud.io/getting-started/github/)
Expand All @@ -41,7 +41,7 @@ permissions:
jobs:
sonar-check:
name: Sonar Check
runs-on: windows-latest # безпечно для будь-яких .NET проектів
runs-on: windows-latest # безпечно для будь-яких .NET проектів
steps:
- uses: actions/checkout@v4
with:
Expand All @@ -63,7 +63,7 @@ jobs:
/d:sonar.cs.opencover.reportsPaths="EchoServerTests/TestResults/coverage.xml,NetSdrClientAppTests/TestResults/coverage.xml" `
/d:sonar.cpd.cs.minimumTokens=40 `
/d:sonar.cpd.cs.minimumLines=5 `
/d:sonar.exclusions=**/bin/**,**/obj/**,**/sonarcloud.yml,**/EchoTspServer/Presentation/Program.cs `
/d:sonar.exclusions=**/bin/**,**/obj/**,**/sonarcloud.yml,**/EchoTspServer/Presentation/Program.cs,**NetSdrClient/NetSdrClientApp/Program.cs `
shell: pwsh

# 2) BUILD & TEST
Expand Down
143 changes: 99 additions & 44 deletions NetSdrClientApp/Messages/NetSdrMessageHelper.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.PortableExecutable;
using System.Text;
using System.Threading.Tasks;

namespace NetSdrClientApp.Messages
{
//TODO: analyze possible use of [StructLayout] for better performance and readability

Check warning on line 8 in NetSdrClientApp/Messages/NetSdrMessageHelper.cs

View workflow job for this annotation

GitHub Actions / Sonar Check

Complete the task associated to this 'TODO' comment. (https://rules.sonarsource.com/csharp/RSPEC-1135)
public static class NetSdrMessageHelper
{
private const short _maxMessageLength = 8191;
private const short _maxDataItemMessageLength = 8194;
private const short _msgHeaderLength = 2; //2 byte, 16 bit
private const short _msgControlItemLength = 2; //2 byte, 16 bit
private const short _msgSequenceNumberLength = 2; //2 byte, 16 bit
private const short _msgHeaderLength = 2; // 2 byte, 16 bit
private const short _msgControlItemLength = 2; // 2 byte, 16 bit
private const short _msgSequenceNumberLength = 2; // 2 byte, 16 bit

public enum MsgTypes
{
SetControlItem,
Expand All @@ -28,7 +26,9 @@
DataItem3
}

public enum ControlItemCodes
// Changed base type to ushort (UInt16) to correctly handle conversion from BitConverter.ToUInt16
// and prevent System.ArgumentException in Enum.IsDefined.
public enum ControlItemCodes : ushort
{
None = 0,
IQOutputDataSampleRate = 0x00B8,
Expand All @@ -53,10 +53,14 @@
var itemCodeBytes = Array.Empty<byte>();
if (itemCode != ControlItemCodes.None)
{
// Convert ControlItemCodes (ushort) to bytes
itemCodeBytes = BitConverter.GetBytes((ushort)itemCode);
}

var headerBytes = GetHeader(type, itemCodeBytes.Length + parameters.Length);
// Total length of data, including the control item code (if present)
var totalBodyLength = itemCodeBytes.Length + parameters.Length;

var headerBytes = GetHeader(type, totalBodyLength);

List<byte> msg = new List<byte>();
msg.AddRange(headerBytes);
Expand All @@ -66,96 +70,147 @@
return msg.ToArray();
}

// Refactored to use offset indexing for robust parsing and added boundary checks.
public static bool TranslateMessage(byte[] msg, out MsgTypes type, out ControlItemCodes itemCode, out ushort sequenceNumber, out byte[] body)
{
type = default;
itemCode = ControlItemCodes.None;
sequenceNumber = 0;
bool success = true;
var msgEnumarable = msg as IEnumerable<byte>;
body = Array.Empty<byte>();

TranslateHeader(msgEnumarable.Take(_msgHeaderLength).ToArray(), out type, out int msgLength);
msgEnumarable = msgEnumarable.Skip(_msgHeaderLength);
msgLength -= _msgHeaderLength;
int offset = 0;

// 1. Check minimum message length (header)
if (msg == null || msg.Length < _msgHeaderLength)
{
return false;
}

// 2. Parse header
TranslateHeader(msg.Take(_msgHeaderLength).ToArray(), out type, out int expectedBodyLength);
offset += _msgHeaderLength;

if (type < MsgTypes.DataItem0) // get item code
// Check if the message has the expected length based on the header value
if (msg.Length != _msgHeaderLength + expectedBodyLength)
{
var value = BitConverter.ToUInt16(msgEnumarable.Take(_msgControlItemLength).ToArray());
msgEnumarable = msgEnumarable.Skip(_msgControlItemLength);
msgLength -= _msgControlItemLength;
// Length mismatch (Fix for TranslateMessage_ShouldFailOnInvalidBodyLength)
return false;
}

int remainingLength = expectedBodyLength;

if (type < MsgTypes.DataItem0) // Process Control Item Code
{
// Control Item message must contain at least the control item code
if (remainingLength < _msgControlItemLength)
{
return false;
}

// Read Control Item Code
ushort value = BitConverter.ToUInt16(msg, offset);
offset += _msgControlItemLength;
remainingLength -= _msgControlItemLength;

if (Enum.IsDefined(typeof(ControlItemCodes), value))
{
itemCode = (ControlItemCodes)value;
}
else
{
success = false;
// Invalid control item code (Fix for TranslateMessage_ShouldFailOnInvalidControlItemCode)
return false;
}
}
else // get sequenceNumber
else // Process Data Item (Sequence Number)
{
sequenceNumber = BitConverter.ToUInt16(msgEnumarable.Take(_msgSequenceNumberLength).ToArray());
msgEnumarable = msgEnumarable.Skip(_msgSequenceNumberLength);
msgLength -= _msgSequenceNumberLength;
// Data Item message must contain at least the sequence number
if (remainingLength < _msgSequenceNumberLength)
{
return false;
}

// Read Sequence Number
sequenceNumber = BitConverter.ToUInt16(msg, offset);
offset += _msgSequenceNumberLength;
remainingLength -= _msgSequenceNumberLength;
}

body = msgEnumarable.ToArray();
// 3. Extract body
if (remainingLength > 0)
{
// Create array for the body
body = new byte[remainingLength];

success &= body.Length == msgLength;
// Copy the body bytes (Fixes issues related to improper length calculation for body)
Array.Copy(msg, offset, body, 0, remainingLength);
}

return success;
return true;
}

public static IEnumerable<int> GetSamples(ushort sampleSize, byte[] body)
{
sampleSize /= 8; //to bytes
if (sampleSize > 4)
ushort sampleSizeInBytes = (ushort)(sampleSize / 8); // to bytes

if (sampleSizeInBytes == 0 || sampleSizeInBytes > 4)
{
throw new ArgumentOutOfRangeException();
throw new ArgumentOutOfRangeException(nameof(sampleSize), "SampleSize must be between 8 and 32 bits and a multiple of 8.");
}

var bodyEnumerable = body as IEnumerable<byte>;
var prefixBytes = Enumerable.Range(0, 4 - sampleSize)
.Select(b => (byte)0);

while (bodyEnumerable.Count() >= sampleSize)
for (int i = 0; i <= body.Length - sampleSizeInBytes; i += sampleSizeInBytes)
{
yield return BitConverter.ToInt32(bodyEnumerable
.Take(sampleSize)
.Concat(prefixBytes)
.ToArray());
bodyEnumerable = bodyEnumerable.Skip(sampleSize);
// Create a 4-byte array for ToInt32
byte[] sampleBytes = new byte[4];

// Copy sample bytes (assuming Little Endian)
Array.Copy(body, i, sampleBytes, 0, sampleSizeInBytes);

yield return BitConverter.ToInt32(sampleBytes);
}
}

private static byte[] GetHeader(MsgTypes type, int msgLength)
{
int lengthWithHeader = msgLength + 2;
// msgLength is the length of the message body (itemCode + parameters).
int lengthWithHeader = msgLength + _msgHeaderLength;

//Data Items edge case
// Data Items edge case: if length reaches max value, set header length to 0 (for DataItem)
if (type >= MsgTypes.DataItem0 && lengthWithHeader == _maxDataItemMessageLength)
{
lengthWithHeader = 0;
}

if (msgLength < 0 || lengthWithHeader > _maxMessageLength)
{
throw new ArgumentException("Message length exceeds allowed value");
throw new ArgumentException("Message length exceeds allowed value", nameof(msgLength));
}

return BitConverter.GetBytes((ushort)(lengthWithHeader + ((int)type << 13)));
// Header format: 3 bits type + 13 bits length
ushort headerValue = (ushort)(lengthWithHeader | ((ushort)type << 13));

return BitConverter.GetBytes(headerValue);
}

private static void TranslateHeader(byte[] header, out MsgTypes type, out int msgLength)
{
var num = BitConverter.ToUInt16(header.ToArray());

// Extract type (3 bits)
type = (MsgTypes)(num >> 13);
msgLength = num - ((int)type << 13);

// Extract length (13 bits) using a mask for reliability: 0b0001_1111_1111_1111
msgLength = num & 0x1FFF;

// Data Items edge case: if DataItem has length 0, it means _maxDataItemMessageLength
if (type >= MsgTypes.DataItem0 && msgLength == 0)
{
msgLength = _maxDataItemMessageLength;
}

// The returned msgLength is the total message length including the header.
// We subtract the header length to get the expected body length (code/sequence + parameters)
msgLength -= _msgHeaderLength;
}
}
}
}
39 changes: 39 additions & 0 deletions NetSdrClientApp/Networking/INetworkClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Net.Sockets;
using System.Threading.Tasks;
using System.Threading;
using System.IO;
using System;

namespace NetSdrClientApp.Networking
{
// 1. ²íòåðôåéñ äëÿ ðåàëüíîãî ñèñòåìíîãî ê볺íòà (äëÿ DI/Mocking)
public interface ISystemTcpClient : IDisposable
{
bool Connected { get; }
INetworkStream GetStream();
void Connect(string host, int port);
void Close();
}

// 2. ²íòåðôåéñ äëÿ ìåðåæåâîãî ïîòîêó (äëÿ Mocking Read/Write)
public interface INetworkStream : IDisposable
{
bool CanRead { get; }
bool CanWrite { get; }
// Ìåòîäè, ùî âèêîðèñòîâóþòüñÿ â TcpClientWrapper
Task WriteAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken);
Task<int> ReadAsync(byte[] buffer, int offset, int size, CancellationToken cancellationToken);
void Close();
}

// 3. ²íòåðôåéñ äëÿ TcpClientWrapper (çàãàëüíà ïîâåä³íêà)
public interface ITcpClient
{
void Connect();
void Disconnect();
Task SendMessageAsync(byte[] data);
Task SendMessageAsync(string str);
event EventHandler<byte[]> MessageReceived;
public bool Connected { get; }
}
}
19 changes: 0 additions & 19 deletions NetSdrClientApp/Networking/ITcpClient.cs

This file was deleted.

24 changes: 18 additions & 6 deletions NetSdrClientApp/Networking/IUdpClient.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@

public interface IUdpClient
using System;
using System.Threading.Tasks;

namespace NetSdrClientApp.Networking
{
event EventHandler<byte[]>? MessageReceived;
// Interface for hash algorithm abstraction (used by Sha256Adapter)
public interface IHashAlgorithm : IDisposable
{
byte[] ComputeHash(byte[] buffer);
}

// Interface for the UDP client wrapper. Implements IDisposable.
public interface IUdpClient : IDisposable
{
event EventHandler<byte[]>? MessageReceived;

Task StartListeningAsync();
Task StartListeningAsync();

void StopListening();
void Exit();
void StopListening();
void Exit();
}
}
Loading
Loading