Skip to content

Commit 0bc8c0c

Browse files
committed
initial code
1 parent 479b6e5 commit 0bc8c0c

16 files changed

+1240
-0
lines changed

Diff for: .gitignore

+407
Large diffs are not rendered by default.

Diff for: Program.cs

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using Sinch.Encoding;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
4+
using Header = System.Collections.Generic.KeyValuePair<string, string>;
5+
6+
var message = new SignalMessage
7+
{
8+
Headers = new Dictionary<string, string>(new Header[] {
9+
new Header("name-1", "value-1"),
10+
new Header("name-2", "value-2"),
11+
new Header("name-3", "value-3"),
12+
}),
13+
Payload = new byte[] { 0b00100011, 0b00011010, 0b00100011 }
14+
};
15+
16+
Console.WriteLine($"This is object before encoding: ${JsonSerializer.Serialize(message, new JsonSerializerOptions { WriteIndented = true })}\n");
17+
18+
var byteEncoder = new ByteBufferBuilder();
19+
var encoder = new SignalMessageCodec(byteEncoder);
20+
21+
try
22+
{
23+
var encodedMessage = encoder.Encode(message);
24+
25+
Console.WriteLine($"This is encoded stream: ${Convert.ToHexString(encodedMessage)}\n");
26+
27+
var decodedMessage = encoder.Decode(encodedMessage);
28+
29+
Console.WriteLine($"This is object after decoding: ${JsonSerializer.Serialize(decodedMessage, new JsonSerializerOptions { WriteIndented = true })}\n");
30+
}
31+
catch (Exception ex)
32+
{
33+
Console.WriteLine($"Something happened: {ex.Message}");
34+
}

Diff for: README.md

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# #1 Simple binary encoding for messages
2+
3+
To properly decode message from binary stream some structured informations is needed to avoid traversing through the streams in search for some special bytes dividing blocks of data.
4+
5+
## #1.1 Assumptions about model:
6+
7+
1. A message can contain a binary payload limited to 256 kB.
8+
2. A message can contain maximal 63 headers as pairs of ASCII-encoded strings.
9+
3. Each header lenght (name + value) is limited to 2046 bytes.
10+
11+
## #1.2 Assumptions about encoding algorithm:
12+
13+
1. Should not be generic rather optimized to specific structure described in #1.1.
14+
2. Optimization for transport (data compression) is not a requirement.
15+
3. Should be simple and easy to maintain and present accepting lower encoding/decoding performance (avoiding unmanaged code).
16+
17+
## #1.3 Therefore simple binary stream structure should contain:
18+
19+
1. Headers count.
20+
2. For each header offset between name part and value part.
21+
3. Optional checksum (hash).
22+
23+
# #2 Binary stream encoding
24+
25+
Message model contains a dictionary of headers and raw byte stream of message payload. For effectivness of decoding I decided to place in encoded stream
26+
metadata information about:
27+
28+
1. payload offest - 4 bytes,
29+
2. number of headers - 2 bytes,
30+
3. for each header a 4 byte prefix - first two bytes stores offset of the value part, second 2 bytes stores length of value part.
31+
32+
| payload offset | 4 bytes
33+
| headers count | 2 bytes
34+
| value1 offset | 2 bytes
35+
| value1 length | 2 bytes
36+
| name1 | 1 kb
37+
| value1 | 1 kb
38+
| value2 offset | 2 bytes
39+
| value2 length | 2 bytes
40+
| name2 | 1 kb
41+
| value2 | 1 kb
42+
| ... | ...
43+
| payload | 256 kb
44+
45+
Metadata are introduced to make decoding efficient and avoid scanning whole streams for special bytes etc.
46+
Payload offset on start tells what is the volume of headers part of the encoded stream.
47+
Value offset simplifies reading name part of heas whereas value length simplifies reading of value part.
48+
49+
# #3 Implementation
50+
51+
To keep codec structure simple and clean I recognized two aspects of the process - encoding sheme layer and binary operations layer (mapping primitives into bytes streams).
52+
Each aspect is encapsulated in separate module - MessageCodec and ByteBufferBuilder respectively. ByteBufferBuilder module is a ependency of MassageCodec module.
53+
54+
I've started from pure unit tests for an algorithm as specification for my coding algorithm. Also included tests for binary streams builder and
55+
two integration tests for decoding.
56+
57+
Accoringly to task description I assume that headers and payload are not obligatory for message (...can contain...) and I assume that
58+
completely empty message (without and headers and no payload) is invalid.

Diff for: Sinch.Encoding.Tests/ByteBufferBuilderSpecs.cs

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
namespace Sinch.Encoding.Tests
2+
{
3+
public class ByteBufferBuilderSpecs
4+
{
5+
private ByteBufferBuilder builder;
6+
public ByteBufferBuilderSpecs()
7+
{
8+
builder = new ByteBufferBuilder();
9+
}
10+
11+
[Fact]
12+
public void Should_join_three_buffers_into_one()
13+
{
14+
// given
15+
byte[] first = System.Text.Encoding.ASCII.GetBytes("heading text");
16+
byte[] second = System.Text.Encoding.ASCII.GetBytes("payload content");
17+
byte[] third = System.Text.Encoding.ASCII.GetBytes("some other textual content");
18+
19+
// when
20+
byte[] result = builder.JoinBuffers(first, second, third);
21+
22+
// then
23+
Assert.Equal(
24+
$"heading textpayload contentsome other textual content",
25+
System.Text.Encoding.Default.GetString(result)
26+
);
27+
}
28+
29+
[Fact]
30+
public void Should_copy_into_stream()
31+
{
32+
// given
33+
var target = new byte[15];
34+
var buffers = new List<byte[]>(new[] {
35+
System.Text.Encoding.ASCII.GetBytes("Apollo"),
36+
System.Text.Encoding.ASCII.GetBytes("Mars"),
37+
System.Text.Encoding.ASCII.GetBytes("Venus")
38+
}).ToArray();
39+
40+
// when
41+
builder.CopyBuffersIntoTarget(target, null, buffers);
42+
43+
// then
44+
Assert.Equal("ApolloMarsVenus", new String(System.Text.Encoding.ASCII.GetChars(target)));
45+
}
46+
47+
[Fact]
48+
public void Should_copy_from_stream()
49+
{
50+
// given
51+
var stream = System.Text.Encoding.ASCII.GetBytes("0123456789");
52+
53+
// when
54+
var result = builder.CopyFromStream(stream, 2, 5);
55+
56+
// then
57+
Assert.Equal("23456", new String(System.Text.Encoding.ASCII.GetChars(result)));
58+
}
59+
}
60+
}

Diff for: Sinch.Encoding.Tests/IngerationDecodingSpecs.cs

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using Header = System.Collections.Generic.KeyValuePair<string, string>;
2+
3+
namespace Sinch.Encoding.Tests
4+
{
5+
public class IngerationDecodingSpecs
6+
{
7+
private Random random;
8+
private SignalMessageCodec _encoder;
9+
public IngerationDecodingSpecs()
10+
{
11+
random = new Random(DateTime.Now.Millisecond);
12+
var builder = new ByteBufferBuilder();
13+
_encoder = new SignalMessageCodec(builder);
14+
}
15+
16+
[Fact]
17+
public void Should_decode_simple_message()
18+
{
19+
// given
20+
var message = new SignalMessage
21+
{
22+
Headers = new Dictionary<string, string>(new Header[]
23+
{
24+
new Header("Alpha", "Beta"),
25+
new Header("Kappa", "Omega")
26+
}),
27+
Payload = System.Text.Encoding.UTF8.GetBytes("Some Text")
28+
};
29+
var encodedMessage = _encoder.Encode(message);
30+
31+
// when
32+
var decodedMessage = _encoder.Decode(encodedMessage);
33+
34+
// then
35+
Assert.Equal(message.Headers.First().Key, decodedMessage.Headers.First().Key);
36+
Assert.Equal(message.Headers.First().Value, decodedMessage.Headers.First().Value);
37+
Assert.Equal(message.Headers.Last().Key, decodedMessage.Headers.Last().Key);
38+
Assert.Equal(message.Headers.Last().Value, decodedMessage.Headers.Last().Value);
39+
Assert.Equal(
40+
new String(System.Text.Encoding.UTF8.GetChars(message.Payload)),
41+
new String(System.Text.Encoding.UTF8.GetChars(decodedMessage.Payload))
42+
);
43+
}
44+
45+
[Fact]
46+
public void Should_decode_large_message()
47+
{
48+
// given
49+
var payloadBuffer = new byte[10240];
50+
random.NextBytes(payloadBuffer);
51+
var headers = Enumerable.Range(0, 48).Select(index => new Header($"Name {index}", $"Value {index}"));
52+
var message = new SignalMessage
53+
{
54+
Headers = new Dictionary<string, string>(headers),
55+
Payload = payloadBuffer
56+
};
57+
var encodedMessage = _encoder.Encode(message);
58+
59+
// when
60+
var decodedMessage = _encoder.Decode(encodedMessage);
61+
62+
// then
63+
Assert.Equal(message.Headers.Count(), decodedMessage.Headers.Count());
64+
Assert.NotStrictEqual(message.Headers, decodedMessage.Headers);
65+
Assert.Equal(10240, decodedMessage.Payload.Length);
66+
}
67+
}
68+
}

Diff for: Sinch.Encoding.Tests/MessageEncodingSpecs.cs

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
using System;
2+
using System.Reflection.PortableExecutable;
3+
using Header = System.Collections.Generic.KeyValuePair<string, string>;
4+
5+
namespace Sinch.Encoding.Tests
6+
{
7+
public class MessageEncodingSpecs
8+
{
9+
private Random random;
10+
private SignalMessageCodec _encoder;
11+
public MessageEncodingSpecs()
12+
{
13+
random = new Random(DateTime.Now.Millisecond);
14+
var builder = new ByteBufferBuilder();
15+
_encoder = new SignalMessageCodec(builder);
16+
}
17+
18+
[Fact]
19+
public void Should_encode_simple_payload_with_headers()
20+
{
21+
// given
22+
var message = new SignalMessage
23+
{
24+
Headers = new Dictionary<string, string>(new Header[]
25+
{
26+
new Header("Alpha", "Beta"),
27+
new Header("Kappa", "Omega")
28+
}),
29+
Payload = System.Text.Encoding.ASCII.GetBytes("Some Text")
30+
};
31+
32+
// when
33+
var encodedMessage = _encoder.Encode(message);
34+
35+
// then
36+
byte[] payloadOffset = new ArraySegment<byte>(encodedMessage, 0, 4).ToArray();
37+
int offset = BitConverter.ToInt32(payloadOffset);
38+
byte[] headPrefix = new ArraySegment<byte>(encodedMessage, 4, 2).ToArray();
39+
short headersCount = BitConverter.ToInt16(headPrefix);
40+
int messageLength = encodedMessage.Length - offset;
41+
byte[] messageStream = new ArraySegment<byte>(encodedMessage, offset, encodedMessage.Length - offset).ToArray();
42+
43+
Assert.Equal(42, encodedMessage.Length);
44+
Assert.Equal(9, messageLength);
45+
Assert.Equal(33, offset);
46+
Assert.Equal(2, headersCount);
47+
Assert.Equal("Some Text", System.Text.Encoding.Default.GetString(messageStream));
48+
}
49+
50+
[Fact]
51+
public void Should_encode_simple_payload_without_headers()
52+
{
53+
// given
54+
var payload = System.Text.Encoding.ASCII.GetBytes("abc");
55+
var message = new SignalMessage
56+
{
57+
Payload = payload
58+
};
59+
60+
// when
61+
var encodedMessage = _encoder.Encode(message);
62+
63+
// then
64+
Assert.Equal(sizeof(int) + payload.Length, encodedMessage.Length);
65+
}
66+
67+
[Fact]
68+
public void Should_encode_simple_message_without_payload()
69+
{
70+
// given
71+
var message = new SignalMessage
72+
{
73+
Headers = new Dictionary<string, string>(new Header[]
74+
{
75+
new Header("N1", "V1"),
76+
new Header("N2", "V2"),
77+
new Header("N3", "V3")
78+
}),
79+
Payload = null
80+
};
81+
82+
// when
83+
var encodedMessage = _encoder.Encode(message);
84+
85+
// then
86+
int expectedLength = sizeof(int) + sizeof(short) + 3 * (2 * sizeof(short) + 4);
87+
Assert.Equal(expectedLength, encodedMessage.Length);
88+
}
89+
90+
[Fact]
91+
public void Should_throw_error_when_encoding_empty_message()
92+
{
93+
// given
94+
var emptyMessage = new SignalMessage
95+
{
96+
Headers = null,
97+
Payload = null
98+
};
99+
100+
// when
101+
var encodingAction = () => _encoder.Encode(emptyMessage);
102+
103+
// then
104+
Assert.Throws<InvalidDataException>(encodingAction);
105+
}
106+
107+
[Fact]
108+
public void Should_throw_error_when_too_long_header_name()
109+
{
110+
// given
111+
var payload = new byte[256];
112+
random.NextBytes(payload);
113+
114+
var veryLongBuffer = new byte[1200];
115+
random.NextBytes(veryLongBuffer);
116+
var tooLongHeaderName = new String(System.Text.Encoding.ASCII.GetChars(veryLongBuffer));
117+
118+
var invalidMessage = new SignalMessage
119+
{
120+
Headers = new Dictionary<string, string>(new Header[]
121+
{
122+
new Header(tooLongHeaderName, "value"),
123+
new Header("valid Name", "valid Value")
124+
}),
125+
Payload = null
126+
};
127+
128+
// when
129+
var encodingAction = () => _encoder.Encode(invalidMessage);
130+
131+
// then
132+
Assert.Throws<InvalidDataException>(encodingAction);
133+
}
134+
135+
[Fact]
136+
public void Should_throw_error_when_too_many_headers()
137+
{
138+
// given
139+
var tooManyHeaders = Enumerable.Range(0, 100).Select(index => new Header($"Name {index}", $"Value {index}"));
140+
var emptyMessage = new SignalMessage
141+
{
142+
Headers = new Dictionary<string, string>(tooManyHeaders),
143+
Payload = null
144+
};
145+
146+
// when
147+
var encodingAction = () => _encoder.Encode(emptyMessage);
148+
149+
// then
150+
Assert.Throws<InvalidDataException>(encodingAction);
151+
}
152+
}
153+
}

0 commit comments

Comments
 (0)