Created
April 6, 2021 16:21
-
-
Save slavanap/57df6897b53d69bfcb24461b554978d3 to your computer and use it in GitHub Desktop.
Simple bencoder & decoder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System.Text; | |
namespace DHT { | |
public class Bencoder { | |
struct Getter { | |
public string Name; | |
public Func<object, object> Delegate; | |
} | |
readonly Stream _stream; | |
readonly static Encoding _enc = Encoding.UTF8; | |
readonly static Dictionary<Type, Getter[]> _cache = new(); | |
public Bencoder(Stream stream) { | |
_stream = stream; | |
} | |
void EncodeLength(long value) { | |
Span<byte> b = stackalloc byte[20]; | |
Span<char> c = stackalloc char[20]; | |
if (!value.TryFormat(c, out int written)) | |
throw new InvalidDataException("Can't convert int to string"); | |
for (int i = 0; i < written; ++i) | |
b[i] = (byte)c[i]; | |
_stream.Write(b.Slice(0, written)); | |
} | |
public void Encode(long value) { | |
_stream.Write(stackalloc[] { (byte)'i' }); | |
EncodeLength(value); | |
_stream.Write(stackalloc[] { (byte)'e' }); | |
} | |
public void Encode(IEnumerable list) { | |
_stream.Write(stackalloc[] { (byte)'l' }); | |
foreach (var item in list) | |
Encode(item); | |
_stream.Write(stackalloc[] { (byte)'e' }); | |
} | |
public void Encode(byte[] b) { | |
EncodeLength(b.Length); | |
_stream.Write(stackalloc[] { (byte)':' }); | |
_stream.Write(b); | |
} | |
public void Encode(ReadOnlySpan<byte> b) { | |
EncodeLength(b.Length); | |
_stream.Write(stackalloc[] { (byte)':' }); | |
_stream.Write(b); | |
} | |
public void Encode(string s) => Encode(_enc.GetBytes(s)); | |
public void Encode(IDictionary dictionary) { | |
_stream.Write(stackalloc[] { (byte)'d' }); | |
IDictionaryEnumerator enumerator = dictionary.GetEnumerator(); | |
while (enumerator.MoveNext()) { | |
Encode((string)enumerator.Key); | |
Encode(enumerator.Value); | |
} | |
_stream.Write(stackalloc[] { (byte)'e' }); | |
} | |
public void Encode(object value) { | |
switch (value) { | |
case int i: | |
Encode(i); | |
break; | |
case uint i: | |
Encode(i); | |
break; | |
case long i: | |
Encode(i); | |
break; | |
case byte[] b: | |
Encode(b); | |
break; | |
case string s: | |
Encode(s); | |
break; | |
case IDictionary d: | |
Encode(d); | |
break; | |
case IEnumerable l: | |
Encode(l); | |
break; | |
default: { | |
Getter[] getters; | |
lock (_cache) { | |
Type t = value.GetType(); | |
if (!_cache.TryGetValue(t, out getters)) { | |
_cache[t] = getters = t.GetProperties() | |
.Where(prop => prop.CanRead) | |
.OrderBy(prop => prop.Name) | |
.Select(prop => new Getter { | |
Name = prop.Name, | |
Delegate = prop.GetValue | |
}) | |
.ToArray(); | |
} | |
} | |
_stream.Write(stackalloc[] { (byte)'d' }); | |
foreach (var getter in getters) { | |
Encode(getter.Name); | |
Encode(getter.Delegate(value)); | |
} | |
_stream.Write(stackalloc[] { (byte)'e' }); | |
break; | |
} | |
} | |
} | |
static void DecodeLength(ref ReadOnlySpan<byte> s, out long value) { | |
bool sign = false; | |
value = 0; | |
if (s[0] == (byte)'-') { | |
sign = true; | |
s = s.Slice(1); | |
} | |
for (int i = 0; i < 21; ++i) { | |
byte b = s[i]; | |
if (b == (byte)'e' || b == (byte)':') { | |
if (sign) | |
value -= value; | |
s = s.Slice(i); | |
return; | |
} | |
if (!char.IsDigit((char)b)) | |
throw new InvalidDataException($"Unexpected character in integer: {b}"); | |
value = value * 10 + (b - (byte)'0'); | |
} | |
throw new InvalidDataException("Too long integer"); | |
} | |
static ReadOnlySpan<byte> DecodeString(ref ReadOnlySpan<byte> s) { | |
if (char.IsDigit((char)s[0])) { | |
DecodeLength(ref s, out var len); | |
if (s[0] != (byte)':') | |
throw new InvalidDataException($"Unexpected end of string length: {s[0]}"); | |
if (len > int.MaxValue || len < 0) | |
throw new InvalidDataException($"The string is too long: {len}"); | |
var ilen = (int)len; | |
var ret = s.Slice(1, ilen); | |
s = s.Slice(ilen + 1); | |
return ret; | |
} | |
throw new InvalidDataException($"Length of string is not a number: {s[0]}"); | |
} | |
public static object Decode(ref ReadOnlySpan<byte> s) { | |
byte b = s[0]; | |
switch (b) { | |
case (byte)'i': | |
s = s.Slice(1); | |
DecodeLength(ref s, out long i); | |
if (s[0] != (byte)'e') | |
throw new InvalidDataException($"Invalid end of integer: {s[0]}"); | |
s = s.Slice(1); | |
return i; | |
case (byte)'l': | |
s = s.Slice(1); | |
var l = new List<object>(); | |
while (s[0] != (byte)'e') | |
l.Add(Decode(ref s)); | |
s = s.Slice(1); | |
return l; | |
case (byte)'d': | |
s = s.Slice(1); | |
var d = new Dictionary<string, object>(); | |
while (s[0] != (byte)'e') { | |
string key = _enc.GetString(DecodeString(ref s)); | |
object value = Decode(ref s); | |
d.Add(key, value); | |
} | |
s = s.Slice(1); | |
return d; | |
default: | |
return DecodeString(ref s).ToArray(); | |
} | |
} | |
public static object DecodeObject(ReadOnlySpan<byte> s) { | |
var result = Decode(ref s); | |
if (s.Length != 0) | |
throw new InvalidDataException($"{s.Length} more bytes in array after reading full object"); | |
return result; | |
} | |
public static object GetValue(object obj, params object[] keys) { | |
foreach (var key in keys) { | |
obj = key switch { | |
int ikey => (obj as List<object>)[ikey], | |
string skey => (obj as Dictionary<string, object>)[skey], | |
_ => throw new ArgumentException($"Invalid navigation key \"{key}\""), | |
}; | |
} | |
return obj; | |
} | |
} | |
} | |
namespace DHT.BencoderExtensions { | |
public static class BencoderExtensions { | |
public static object BenNavigate(this object rawObj, params object[] keys) | |
=> Bencoder.GetValue(rawObj, keys); | |
public static string ToBenString(this object rawString) | |
=> Encoding.UTF8.GetString(rawString.ToBenBytes()); | |
public static List<object> ToBenList(this object rawList) | |
=> (List<object>)rawList ?? throw new ArgumentException($"Invalid list value: {rawList}"); | |
public static IEnumerable<T> ToBenList<T>(this object rawList) | |
=> rawList.ToBenList().Cast<T>(); | |
public static Dictionary<string, object> ToBenDict(this object rawDict) | |
=> (Dictionary<string, object>)rawDict ?? throw new ArgumentException($"Invalid dict value: {rawDict}"); | |
public static byte[] ToBenBytes(this object rawBytes) | |
=> (byte[])rawBytes ?? throw new ArgumentException($"Invalid byte[] value: {rawBytes}"); | |
public static long ToBenInt(this object rawInt) | |
=> (long)rawInt; | |
public static object BenDecode(this byte[] data) | |
=> Bencoder.DecodeObject(data); | |
public static byte[] BenEncode(this object obj, int expectedLength = 0) { | |
using var stream = expectedLength != 0 ? new MemoryStream(expectedLength) : new MemoryStream(); | |
new Bencoder(stream).Encode(obj); | |
return stream.ToArray(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment