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.

204 lines
6.8 KiB

using System;
using System.Buffers;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using gaemstone.Utility;
using static flecs_hub.flecs;
using static flecs_hub.flecs.Runtime;
namespace gaemstone.ECS;
public class EntityPath
{
public const int MaxDepth = 32;
public const char Separator = '/';
internal const byte SeparatorAsByte = (byte)Separator;
internal static readonly CString SeparatorAsCString = (CString)Separator.ToString();
private static readonly ThreadLocal<(int Start, int End)[]> PartsCache
= new(() => new (int, int)[MaxDepth]);
private readonly byte[] _bytes;
private readonly (int Start, int End)[] _parts;
public int Depth => _parts.Length - 1;
public bool IsAbsolute => _bytes[0] == SeparatorAsByte;
public bool IsRelative => !IsAbsolute;
public UTF8View this[int index] { get {
if (index < 0 || index > Depth)
throw new ArgumentOutOfRangeException(nameof(index));
var (start, end) = _parts[index];
return new(_bytes.AsSpan()[start..end]);
} }
public unsafe EntityPath(byte* pointer)
: this(ArrayFromPointer(pointer)) { }
private static unsafe byte[] ArrayFromPointer(byte* pointer)
{
var length = 0;
while (true) if (pointer[length++] == 0) break;
return new Span<byte>(pointer, length).ToArray();
}
// TODO: public EntityPath(EntityPath @base, params string[] parts) { }
public EntityPath(params string[] parts)
: this(ConcatParts(false, parts)) { }
public EntityPath(bool absolute, params string[] parts)
: this(ConcatParts(absolute, parts)) { }
private static byte[] ConcatParts(bool absolute, string[] parts)
{
// number of slashes + NUL
var totalBytes = (parts.Length - 1) + 1;
// If absolute, and parts doesn't already start with a slash, increase length by 1.
var prependSlash = absolute && parts[0].Length > 0 && parts[0][0] != Separator;
if (prependSlash) totalBytes++;
foreach (var part in parts)
totalBytes += Encoding.UTF8.GetByteCount(part);
var bytes = new byte[totalBytes];
var index = 0;
foreach (var part in parts) {
if (index > 0 || prependSlash) bytes[index++] = SeparatorAsByte;
index += Encoding.UTF8.GetBytes(part, 0, part.Length, bytes, index);
}
// NUL byte at the end of bytes.
// bytes[index++] = 0;
return bytes;
}
private EntityPath(byte[] bytes)
{
if (bytes.Length <= 1) throw new ArgumentException("Must not be empty");
if (bytes[^1] != 0) throw new ArgumentException("Must end with a NUL character");
_bytes = bytes;
var depth = 0;
var index = 0;
var partStart = 0;
var partsCache = PartsCache.Value!;
// If path starts with separator, it's an absolute path. Skip first byte.
if (_bytes[0] == SeparatorAsByte) index = partStart = 1;
// -1 is used here because we don't want to include the NUL character.
while (index < _bytes.Length - 1) {
if (_bytes[index] == SeparatorAsByte) {
// +1 is used here because one more part will follow after the loop.
if (depth + 1 >= MaxDepth) throw new ArgumentException(
$"Must not exceed maximum depth of {MaxDepth}");
partsCache[depth++] = (Start: partStart, End: index);
ValidatePart(_bytes.AsSpan()[partStart..index]);
partStart = ++index;
} else {
var slice = _bytes.AsSpan()[index..];
if (Rune.DecodeFromUtf8(slice, out var rune, out var consumed) != OperationStatus.Done)
throw new ArgumentException("Contains invalid UTF8");
ValidateRune(rune);
index += consumed;
}
}
partsCache[depth] = (Start: partStart, End: index);
ValidatePart(_bytes.AsSpan()[partStart..^1]);
// Copy parts from the thread local cache - this avoids unnecessary resizing.
_parts = partsCache[..(depth + 1)];
}
private static void ValidatePart(ReadOnlySpan<byte> part)
{
if (part.Length == 0) throw new ArgumentException(
"Must not contain empty parts");
// NOTE: This is a hopefully straightforward way to also prevent "."
// and ".." to be part of paths which may access the file system.
if (part[0] == (byte)'.') throw new ArgumentException(
"Must not contain parts that start with a dot");
}
private static readonly Rune[] _validRunes = { (Rune)'-', (Rune)'.', (Rune)'_' };
private static readonly UnicodeCategory[] _validCategories = {
UnicodeCategory.LowercaseLetter, UnicodeCategory.UppercaseLetter,
UnicodeCategory.OtherLetter, UnicodeCategory.DecimalDigitNumber };
private static void ValidateRune(Rune rune)
{
if (!_validRunes.Contains(rune) && !_validCategories.Contains(Rune.GetUnicodeCategory(rune)))
throw new ArgumentException($"Must not contain {Rune.GetUnicodeCategory(rune)} character");
}
public Enumerator GetEnumerator() => new(this);
public ref struct Enumerator
{
private readonly EntityPath _path;
private int index = -1;
public UTF8View Current => _path[index];
internal Enumerator(EntityPath path) => _path = path;
public bool MoveNext() => (++index >= _path.Depth);
}
public ReadOnlySpan<byte> AsSpan(bool includeNul = false)
=> includeNul ? _bytes.AsSpan() : _bytes.AsSpan()[..^1];
public override string ToString() => Encoding.UTF8.GetString(AsSpan());
public static implicit operator string(EntityPath view) => view.ToString();
public static implicit operator EntityPath(string str) => new(str);
}
public static class EntityPathExtensions
{
public static unsafe EntityPath GetFullPath(this EntityRef entity)
{
var cStr = ecs_get_path_w_sep(entity.Universe, default, entity,
EntityPath.SeparatorAsCString, EntityPath.SeparatorAsCString);
try { return new((byte*)(nint)cStr); }
finally { cStr.FlecsFree(); }
}
public static unsafe Entity Lookup(Universe universe, Entity parent, EntityPath path)
{
using var alloc = TempAllocator.Lock();
return new(ecs_lookup_path_w_sep(universe, parent, alloc.AllocateCString(path.AsSpan(true)),
EntityPath.SeparatorAsCString, EntityPath.SeparatorAsCString, true));
}
}
public readonly ref struct UTF8View
{
private readonly ReadOnlySpan<byte> _bytes;
public UTF8View(ReadOnlySpan<byte> bytes)
=> _bytes = bytes;
public int Length => _bytes.Length;
public byte this[int index] => _bytes[index];
public override string ToString() => Encoding.UTF8.GetString(_bytes);
public static implicit operator ReadOnlySpan<byte>(UTF8View view) => view._bytes;
public static implicit operator string(UTF8View view) => view.ToString();
public Enumerator GetEnumerator() => new(_bytes);
public ref struct Enumerator
{
private readonly ReadOnlySpan<byte> _bytes;
private int index = 0;
private Rune _current = default;
public Rune Current => _current;
internal Enumerator(ReadOnlySpan<byte> bytes)
=> _bytes = bytes;
public bool MoveNext()
{
if (index >= _bytes.Length) return false;
if (Rune.DecodeFromUtf8(_bytes[index..], out _current, out var consumed) != OperationStatus.Done)
throw new InvalidOperationException("Contains invalid UTF8");
index += consumed;
return true;
}
}
}