You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
278 lines
12 KiB
278 lines
12 KiB
using System.Runtime.InteropServices; |
|
using System.Text; |
|
using System.Text.Json.Nodes; |
|
|
|
namespace Res2tf; |
|
|
|
using List = List<BsonValue>; |
|
using Dict = Dictionary<string, BsonValue>; |
|
|
|
public enum ElementType : byte |
|
{ |
|
Double = 1, |
|
String, |
|
Object, |
|
Array, |
|
Binary, |
|
Undefined, // deprecated |
|
ObjectId, // unsupported |
|
Boolean, |
|
DateTime, |
|
Null, |
|
RegExp, // unsupported |
|
DbPointer, // deprecated |
|
JsCode, // unsupported |
|
Symbol, // deprecated |
|
JsCodeScope, // deprecated |
|
Int32, |
|
Timestamp, // unsupported |
|
Int64, |
|
Decimal128, // unsupported |
|
}; |
|
|
|
public class BsonValue : IEquatable<BsonValue> |
|
{ |
|
public static readonly BsonValue Null = new(ElementType.Null, null); |
|
|
|
public ElementType ElementType { get; } |
|
public object? Value { get; } |
|
|
|
public int Count => AsCollection().Count; |
|
public BsonValue this[int index] { get => AsList()[index]; set => AsList()[index] = value; } |
|
public BsonValue this[string key] { get => AsDict()[key ]; set => AsDict()[key ] = value; } |
|
public Dict.KeyCollection Keys => AsDict().Keys; |
|
public Dict.ValueCollection Values => AsDict().Values; |
|
|
|
private BsonValue(ElementType type, object? value) { ElementType = type; Value = value; } |
|
|
|
public BsonValue(double value) : this(ElementType.Double , value) { } |
|
public BsonValue(string value) : this(ElementType.String , value) { ArgumentNullException.ThrowIfNull(value); } |
|
public BsonValue(byte[] value) : this(ElementType.Binary , value) { ArgumentNullException.ThrowIfNull(value); } |
|
public BsonValue(bool value) : this(ElementType.Boolean , value) { } |
|
public BsonValue(DateTime value) : this(ElementType.DateTime, value) { } |
|
public BsonValue(int value) : this(ElementType.Int32 , value) { } |
|
public BsonValue(long value) : this(ElementType.Int64 , value) { } |
|
|
|
public static BsonValue Array() => new(ElementType.Array, new List()); |
|
public static BsonValue Array(params IEnumerable<BsonValue> values) => new(ElementType.Array, values.ToList()); |
|
|
|
public static BsonValue Object() => new(ElementType.Object, new Dict()); |
|
public static BsonValue Object(IDictionary<string, BsonValue> entries) => new(ElementType.Object, entries.ToDictionary()); |
|
public static BsonValue Object(params IEnumerable<(string, BsonValue)> entries) => new(ElementType.Object, entries.ToDictionary()); |
|
|
|
public List AsList() => (Value as List) ?? throw new InvalidOperationException($"Attempting to access {ElementType} as Array"); |
|
public Dict AsDict() => (Value as Dict) ?? throw new InvalidOperationException($"Attempting to access {ElementType} as Object"); |
|
protected ICollection<BsonValue> AsCollection() => Value switch { List list => list, Dict dict => dict.Values, _ => throw new NotSupportedException($"Attempting to access {ElementType} as Array or Object"), }; |
|
|
|
public JsonNode? ToJson() |
|
=> Value switch { |
|
Dict v => new JsonObject(AsDict().Select(e => new KeyValuePair<string, JsonNode?>(e.Key, e.Value.ToJson()))), |
|
List v => new JsonArray (AsList().Select(e => e.ToJson()).ToArray()), |
|
double v => JsonValue.Create(v), |
|
string v => JsonValue.Create(v)!, |
|
byte[] v => JsonValue.Create(v)!, |
|
bool v => JsonValue.Create(v), |
|
DateTime v => JsonValue.Create(v), |
|
int v => JsonValue.Create(v), |
|
long v => JsonValue.Create(v), |
|
null => null, |
|
_ => throw new NotImplementedException(), |
|
}; |
|
|
|
public T[] ToArray<T>() |
|
{ |
|
if (typeof(T) == typeof(BsonValue)) return (T[])(object)AsList().ToArray(); |
|
if (typeof(T) == typeof(byte) && ElementType == ElementType.Binary) return (T[])Value!; |
|
if (typeof(T) == typeof(float)) return AsList().Select(v => (T)(object)(float)(double)v.Value!).ToArray(); |
|
if (ElementTypeOf<T>() == null) throw new NotSupportedException($"{typeof(T)} is not a valid ElementType"); |
|
return AsList().Select(v => (T)v.Value!).ToArray(); |
|
} |
|
|
|
public Dictionary<string, T> ToDictionary<T>() |
|
{ |
|
if (typeof(T) == typeof(BsonValue)) return (Dictionary<string, T>)(object)AsDict().ToDictionary(); |
|
if (ElementTypeOf<T>() == null) throw new NotSupportedException($"{typeof(T)} is not a valid ElementType"); |
|
return AsDict().ToDictionary(e => e.Key, e => (T)e.Value.Value!); |
|
} |
|
|
|
public BsonValue? GetOrNull(string key) => AsDict().GetValueOrDefault(key); |
|
|
|
public void Add(BsonValue value) => AsList().Add(value); |
|
public void Add(string key, BsonValue value) => AsDict().Add(key, value); |
|
|
|
public bool Contains(BsonValue value) => AsList().Contains(value); |
|
public bool ContainsKey(string key) => AsDict().ContainsKey(key); |
|
|
|
public void RemoveAt(int index) => AsList().RemoveAt(index); |
|
public bool Remove(string key) => AsDict().Remove(key); |
|
|
|
public void Clear() |
|
{ |
|
switch (Value) { |
|
case List list: list.Clear(); break; |
|
case Dict dict: dict.Clear(); break; |
|
default: throw new NotSupportedException($"Attempting to access {ElementType} as Array or Object"); |
|
}; |
|
} |
|
|
|
public override bool Equals(object? obj) |
|
=> Equals(obj as BsonValue); |
|
public bool Equals(BsonValue? other) |
|
=> (other is not null) |
|
&& (ElementType == other.ElementType) |
|
&& ElementType switch { |
|
ElementType.Array => Enumerable.SequenceEqual(AsList(), other.AsList()), |
|
ElementType.Object => DictionaryEquals(AsDict(), other.AsDict()), |
|
_ => Equals(Value, other.Value), |
|
}; |
|
public override int GetHashCode() |
|
=> HashCode.Combine(ElementType, Value); |
|
|
|
public static bool operator ==(BsonValue? left, BsonValue? right) |
|
=> ReferenceEquals(left, right) || (left?.Equals(right) ?? false); |
|
public static bool operator !=(BsonValue? left, BsonValue? right) |
|
=> !(left == right); |
|
|
|
public static implicit operator BsonValue(double value) => new(value); |
|
public static implicit operator BsonValue(string value) => new(value); |
|
public static implicit operator BsonValue(byte[] value) => new(value); |
|
public static implicit operator BsonValue(bool value) => new(value); |
|
public static implicit operator BsonValue(DateTime value) => new(value); |
|
public static implicit operator BsonValue(int value) => new(value); |
|
public static implicit operator BsonValue(long value) => new(value); |
|
|
|
public static explicit operator double (BsonValue value) => (double )value.Value!; |
|
public static explicit operator string (BsonValue value) => (string )value.Value!; |
|
public static explicit operator byte[] (BsonValue value) => (byte[] )value.Value!; |
|
public static explicit operator bool (BsonValue value) => (bool )value.Value!; |
|
public static explicit operator DateTime(BsonValue value) => (DateTime)value.Value!; |
|
public static explicit operator int (BsonValue value) => (int )value.Value!; |
|
public static explicit operator long (BsonValue value) => (long )value.Value!; |
|
|
|
public static explicit operator float(BsonValue value) => (float)(double)value; |
|
|
|
private static ElementType? ElementTypeOf<T>() |
|
{ |
|
if (typeof(T) == typeof(double )) return ElementType.Double; |
|
if (typeof(T) == typeof(string )) return ElementType.String; |
|
if (typeof(T) == typeof(Dict )) return ElementType.Object; |
|
if (typeof(T) == typeof(List )) return ElementType.Array; |
|
if (typeof(T) == typeof(byte[] )) return ElementType.Binary; |
|
if (typeof(T) == typeof(bool )) return ElementType.Boolean; |
|
if (typeof(T) == typeof(DateTime)) return ElementType.DateTime; |
|
if (typeof(T) == typeof(int )) return ElementType.Int32; |
|
if (typeof(T) == typeof(long )) return ElementType.Int64; |
|
return null; |
|
} |
|
|
|
private static bool DictionaryEquals(Dict first, Dict second) |
|
=> (first.Count == second.Count) && !first.Except(second).Any(); |
|
} |
|
|
|
public class BsonReader : IDisposable |
|
{ |
|
private readonly BinaryReader _reader; |
|
private readonly List<byte> _cStringBuf = []; |
|
|
|
public BsonReader(Stream stream, bool leaveOpen = false) |
|
{ |
|
if (!stream.CanRead) throw new ArgumentException("stream is not readable"); |
|
_reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen); |
|
} |
|
|
|
public void Dispose() |
|
{ |
|
_reader.Dispose(); |
|
GC.SuppressFinalize(this); |
|
} |
|
|
|
public static BsonValue Load(Stream stream, bool leaveOpen = false) |
|
{ |
|
using var reader = new BsonReader(stream, leaveOpen); |
|
return reader.ReadObject(); |
|
} |
|
public static BsonValue Load(byte[] bytes) |
|
{ |
|
using var stream = new MemoryStream(bytes); |
|
return Load(stream); |
|
} |
|
|
|
public BsonValue ReadObject() |
|
{ |
|
// A "document" is prefixed with the number of bytes it takes up, but |
|
// we can just ignore it and read elements until we detect a NUL byte. |
|
// Thankfully we don't need to write a BsonWriter ... |
|
_reader.ReadInt32(); // length, unused |
|
|
|
var result = BsonValue.Object(); |
|
while (ReadElement() is (string name, BsonValue value)) |
|
result.Add(name, value); |
|
return result; |
|
} |
|
|
|
public BsonValue ReadArray() |
|
{ |
|
// An "array" is just an object with stringified integer keys ... |
|
_reader.ReadInt32(); // length, unused |
|
|
|
var result = BsonValue.Array(); |
|
while (ReadElement() is (string name, BsonValue value)) |
|
{ |
|
if (!int.TryParse(name, out var index)) throw new Exception("Couldn't parse array index as integer"); |
|
if (index != result.Count) throw new Exception("Non-sequential array index"); |
|
result.Add(value); |
|
} |
|
return result; |
|
} |
|
|
|
public BsonValue ReadBinary() |
|
{ |
|
var length = _reader.ReadInt32(); |
|
_reader.ReadByte(); // subtype, unused |
|
return _reader.ReadBytes(length); |
|
} |
|
|
|
public (string Name, BsonValue Value)? ReadElement() |
|
{ |
|
var type = (ElementType)_reader.ReadByte(); |
|
if (type == 0) return null; |
|
|
|
var name = ReadCString(); |
|
return (name, type switch |
|
{ |
|
ElementType.Double => _reader.ReadDouble(), |
|
ElementType.String => ReadLengthPrefixedString(), |
|
ElementType.Object => ReadObject(), |
|
ElementType.Array => ReadArray(), |
|
ElementType.Binary => ReadBinary(), |
|
ElementType.Boolean => _reader.ReadByte() != 0, |
|
ElementType.DateTime => DateTime.UnixEpoch + TimeSpan.FromMilliseconds(_reader.ReadInt64()), |
|
ElementType.Null => BsonValue.Null, |
|
ElementType.Int32 => _reader.ReadInt32(), |
|
ElementType.Int64 => _reader.ReadInt64(), |
|
|
|
ElementType.Undefined | ElementType.ObjectId | ElementType.RegExp |
|
| ElementType.DbPointer | ElementType.JsCode | ElementType.Symbol |
|
| ElementType.JsCodeScope | ElementType.Timestamp | ElementType.Decimal128 |
|
=> throw new NotSupportedException($"Reading {type} is not supported"), |
|
|
|
_ => throw new Exception($"Unknown ElementType {type}"), |
|
}); |
|
} |
|
|
|
private string ReadLengthPrefixedString() |
|
{ |
|
var length = _reader.ReadInt32(); |
|
var bytes = _reader.ReadBytes(length); |
|
if (bytes[^1] != 0) throw new Exception("Trailing byte in string must be NUL"); |
|
return Encoding.UTF8.GetString(bytes.AsSpan()[0..^1]); |
|
} |
|
|
|
private string ReadCString() |
|
{ |
|
_cStringBuf.Clear(); |
|
byte b; while ((b = _reader.ReadByte()) != 0) _cStringBuf.Add(b); |
|
var bytes = CollectionsMarshal.AsSpan(_cStringBuf); |
|
return Encoding.UTF8.GetString(bytes); |
|
} |
|
}
|
|
|