Skip to content

Instantly share code, notes, and snippets.

@slavanap
Created April 6, 2021 16:21
Show Gist options
  • Save slavanap/57df6897b53d69bfcb24461b554978d3 to your computer and use it in GitHub Desktop.
Save slavanap/57df6897b53d69bfcb24461b554978d3 to your computer and use it in GitHub Desktop.
Simple bencoder & decoder
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