Skip to content

Commit

Permalink
Improve content id serializer (#1068)
Browse files Browse the repository at this point in the history
* Improve content id serializer

* Fix serializer

* Fix serializer again.
  • Loading branch information
SebastianStehle authored Jan 24, 2024
1 parent c0c6904 commit 105bcdb
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 113 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================

using System.Text;
using Squidex.Infrastructure;

namespace Squidex.Domain.Apps.Entities.MongoDb;

#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
#pragma warning disable RECS0082 // Parameter has the same name as a member and hides it

public partial class BsonUniqueContentIdSerializer
{
private readonly record struct IdInfo(int Length, bool IsGuid, Guid AsGuid, string Source)
{
public const byte GuidLength = 16;
public const byte GuidIndicator = byte.MaxValue;
public const byte LongIdIndicator = byte.MaxValue - 1;
public const byte SizeOfInt = 4;
public const byte SizeOfByte = 1;

public bool IsEmpty => IsGuid && AsGuid == default;

public static IdInfo Create(DomainId id)
{
var source = id.ToString();

if (Guid.TryParse(source, out var guid))
{
return new IdInfo(GuidLength, true, guid, source);
}

return new IdInfo(Encoding.UTF8.GetByteCount(source), false, default, source);
}

public int Size(bool writeEmpty)
{
if (IsEmpty && !writeEmpty)
{
return 0;
}

if (Length >= LongIdIndicator)
{
return SizeOfByte + SizeOfInt + Length;
}

return SizeOfByte + Length;
}

public int Write(Span<byte> buffer)
{
if (Length >= LongIdIndicator)
{
buffer[0] = LongIdIndicator;

Write(buffer[1..], WriteLengthAsInt);
}
else
{
Write(buffer, WriteLengthAsByte);
}

return Size(false);
}

private int Write(Span<byte> buffer, WriteLength writeLength)
{
int lengthSize;
if (IsGuid)
{
// Special length indicator for all guids.
lengthSize = writeLength(buffer, GuidIndicator);

AsGuid.TryWriteBytes(buffer[lengthSize..]);
}
else
{
// We assume that we use relatively small IDs, not longer than 253 bytes.
lengthSize = writeLength(buffer, Length);

Encoding.UTF8.GetBytes(Source, buffer[lengthSize..]);
}

return lengthSize + Length;
}

public static (DomainId Id, int Length) Read(ReadOnlySpan<byte> buffer)
{
if (buffer.Length == 0)
{
return default;
}

if (buffer[0] == LongIdIndicator)
{
var (id, read) = Read(buffer[1..], ReadLengthAsInt);

return (id, read + 1);
}
else
{
return Read(buffer, ReadLengthAsByte);
}
}

private static (DomainId Id, int Length) Read(ReadOnlySpan<byte> buffer, ReadLength readLength)
{
var (length, offset) = readLength(buffer);

if (length == GuidIndicator)
{
// For guids the size is just an indicator and we use a hardcoded size.
buffer = buffer.Slice(offset, GuidLength);

return (DomainId.Create(new Guid(buffer)), offset + GuidLength);
}
else
{
// For strings the size is correct.
buffer = buffer.Slice(offset, length);

return (DomainId.Create(Encoding.UTF8.GetString(buffer)), offset + length);
}
}

private static int WriteLengthAsByte(Span<byte> buffer, int length)
{
buffer[0] = (byte)length;

return SizeOfByte;
}

private static int WriteLengthAsInt(Span<byte> buffer, int length)
{
BitConverter.TryWriteBytes(buffer, length);

return SizeOfInt;
}

private static (int, int) ReadLengthAsByte(ReadOnlySpan<byte> buffer)
{
return (buffer[0], SizeOfByte);
}

private static (int, int) ReadLengthAsInt(ReadOnlySpan<byte> buffer)
{
return (BitConverter.ToInt32(buffer), SizeOfInt);
}

private delegate int WriteLength(Span<byte> buffer, int length);

private delegate (int, int) ReadLength(ReadOnlySpan<byte> buffer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,15 @@

namespace Squidex.Domain.Apps.Entities.MongoDb;

public sealed class BsonUniqueContentIdSerializer : SerializerBase<UniqueContentId>
public partial class BsonUniqueContentIdSerializer : SerializerBase<UniqueContentId>
{
private const byte GuidLength = 16;
private static readonly BsonUniqueContentIdSerializer Instance = new BsonUniqueContentIdSerializer();

public static void Register()
{
BsonSerializer.TryRegisterSerializer(Instance);
}

private BsonUniqueContentIdSerializer()
{
}

public static UniqueContentId NextAppId(DomainId appId)
{
static void IncrementByteArray(byte[] bytes)
Expand Down Expand Up @@ -63,121 +58,37 @@ static void IncrementByteArray(byte[] bytes)

public override UniqueContentId Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var buffer = context.Reader.ReadBytes()!;
var offset = 0;

static DomainId ReadId(byte[] buffer, ref int offset)
{
DomainId id;

// If we have reached the end of the buffer then
if (offset >= buffer.Length)
{
return default;
}

var length = buffer[offset++];
// Special length indicator for all guids.
if (length == 0xFF)
{
id = DomainId.Create(new Guid(buffer.AsSpan(offset, GuidLength)));
offset += GuidLength;
}
else
{
id = DomainId.Create(Encoding.UTF8.GetString(buffer.AsSpan(offset, length)));
offset += length;
}
var buffer = context.Reader.ReadBytes()!.AsSpan();

return id;
}
var (appId, read) = IdInfo.Read(buffer);

return new UniqueContentId(ReadId(buffer, ref offset), ReadId(buffer, ref offset));
return new UniqueContentId(appId, IdInfo.Read(buffer[read..]).Id);
}

public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, UniqueContentId value)
{
var appId = CheckId(value.AppId);

var contentId = CheckId(value.ContentId);
var appId = IdInfo.Create(value.AppId);

var isEmptyContentId =
contentId.IsGuid &&
contentId.Guid == default;

// Do not write empty Ids to the buffer to allow prefix searches.
var contentLength = !isEmptyContentId ? contentId.Length + 1 : 0;

var bufferLength = appId.Length + 1 + contentLength;
var bufferArray = new byte[bufferLength];

var offset = Write(bufferArray, 0,
appId.IsGuid,
appId.Guid,
appId.Source,
appId.Length);

if (!isEmptyContentId)
if (appId.Length >= IdInfo.LongIdIndicator)
{
// Do not write the empty content id, so we can search for app as well.
Write(bufferArray, offset,
contentId.IsGuid,
contentId.Guid,
contentId.Source,
contentId.Length);
ThrowHelper.InvalidOperationException("App ID cannot be longer than 253 bytes.");
}

static int Write(byte[] buffer, int offset, bool isGuid, Guid guid, string id, int idLength)
{
if (isGuid)
{
// Special length indicator for all guids.
buffer[offset++] = 0xFF;
WriteGuid(buffer.AsSpan(offset), guid);

return offset + GuidLength;
}
else
{
// We assume that we use relatively small IDs, not longer than 254 bytes.
buffer[offset++] = (byte)idLength;
WriteString(buffer.AsSpan(offset), id);

return offset + idLength;
}
}
var contentId = IdInfo.Create(value.ContentId);

context.Writer.WriteBytes(bufferArray);
}
var size = appId.Size(true) + contentId.Size(false);

private static (int Length, bool IsGuid, Guid Guid, string Source) CheckId(DomainId id)
{
var source = id.ToString();
var bufferArray = new byte[size];
var bufferSpan = bufferArray.AsSpan();

var idIsGuid = Guid.TryParse(source, out var idGuid);
var idLength = GuidLength;
var written = appId.Write(bufferSpan);

if (!idIsGuid)
if (!contentId.IsEmpty)
{
idLength = (byte)Encoding.UTF8.GetByteCount(source);

// We only use a single byte to write the length, therefore we do not allow large strings.
if (idLength > 254)
{
ThrowHelper.InvalidOperationException("Cannot write long IDs.");
}
// Do not write empty Ids to the buffer to allow prefix searches.
contentId.Write(bufferSpan[written..]);
}

return (idLength, idIsGuid, idGuid, source);
}

private static void WriteString(Span<byte> span, string id)
{
Encoding.UTF8.GetBytes(id, span);
}

private static void WriteGuid(Span<byte> span, Guid guid)
{
guid.TryWriteBytes(span);
context.Writer.WriteBytes(bufferArray);
}
}
Loading

0 comments on commit 105bcdb

Please sign in to comment.