106 lines
3.1 KiB
C#
106 lines
3.1 KiB
C#
using System.Globalization;
|
|
using System.Text;
|
|
|
|
namespace Nexus.Api.Repositories;
|
|
|
|
internal readonly record struct OpenClawRunCursorPosition(
|
|
DateTimeOffset CreatedAt,
|
|
Guid? Id);
|
|
|
|
internal static class OpenClawRunCursorCodec
|
|
{
|
|
private const string Version = "v1";
|
|
private const int MaximumEncodedLength = 128;
|
|
|
|
public static string Encode(DateTimeOffset createdAt, Guid id)
|
|
{
|
|
var payload = string.Create(
|
|
CultureInfo.InvariantCulture,
|
|
$"{Version}|{createdAt.UtcTicks}|{id:N}");
|
|
|
|
return Convert.ToBase64String(Encoding.UTF8.GetBytes(payload))
|
|
.TrimEnd('=')
|
|
.Replace('+', '-')
|
|
.Replace('/', '_');
|
|
}
|
|
|
|
public static bool TryDecode(
|
|
string? cursor,
|
|
out OpenClawRunCursorPosition position)
|
|
{
|
|
position = default;
|
|
if (string.IsNullOrWhiteSpace(cursor) || cursor.Length > MaximumEncodedLength)
|
|
return false;
|
|
|
|
// Compatibility with the original run cursor, which was an unversioned
|
|
// UTC tick value and therefore cannot express the Id tie-breaker.
|
|
if (long.TryParse(
|
|
cursor,
|
|
NumberStyles.None,
|
|
CultureInfo.InvariantCulture,
|
|
out var legacyTicks))
|
|
{
|
|
return TryCreatePosition(legacyTicks, null, out position);
|
|
}
|
|
|
|
try
|
|
{
|
|
var normalized = cursor
|
|
.Replace('-', '+')
|
|
.Replace('_', '/');
|
|
|
|
normalized = (normalized.Length % 4) switch
|
|
{
|
|
0 => normalized,
|
|
2 => normalized + "==",
|
|
3 => normalized + "=",
|
|
_ => throw new FormatException("Invalid Base64Url length.")
|
|
};
|
|
|
|
var payload = new UTF8Encoding(
|
|
encoderShouldEmitUTF8Identifier: false,
|
|
throwOnInvalidBytes: true)
|
|
.GetString(Convert.FromBase64String(normalized));
|
|
var parts = payload.Split('|');
|
|
if (parts.Length != 3
|
|
|| !string.Equals(parts[0], Version, StringComparison.Ordinal)
|
|
|| !long.TryParse(
|
|
parts[1],
|
|
NumberStyles.None,
|
|
CultureInfo.InvariantCulture,
|
|
out var utcTicks)
|
|
|| !Guid.TryParseExact(parts[2], "N", out var id))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return TryCreatePosition(utcTicks, id, out position);
|
|
}
|
|
catch (Exception exception) when (
|
|
exception is FormatException
|
|
or DecoderFallbackException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool TryCreatePosition(
|
|
long utcTicks,
|
|
Guid? id,
|
|
out OpenClawRunCursorPosition position)
|
|
{
|
|
position = default;
|
|
try
|
|
{
|
|
position = new OpenClawRunCursorPosition(
|
|
new DateTimeOffset(utcTicks, TimeSpan.Zero),
|
|
id);
|
|
return true;
|
|
}
|
|
catch (ArgumentOutOfRangeException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|