73 lines
2.3 KiB
C#
73 lines
2.3 KiB
C#
using System.Globalization;
|
|
using System.Text;
|
|
|
|
namespace Nexus.Api.Services;
|
|
|
|
internal readonly record struct TaskBoardCursorPosition(
|
|
DateTimeOffset UpdatedAt,
|
|
Guid Id);
|
|
|
|
internal static class TaskBoardCursorCodec
|
|
{
|
|
private const string Version = "v1";
|
|
private const int MaximumEncodedLength = 128;
|
|
|
|
public static string Encode(DateTimeOffset updatedAt, Guid id)
|
|
{
|
|
var payload = string.Create(
|
|
CultureInfo.InvariantCulture,
|
|
$"{Version}|{updatedAt.UtcDateTime.Ticks}|{id:N}");
|
|
|
|
return Convert.ToBase64String(Encoding.UTF8.GetBytes(payload))
|
|
.TrimEnd('=')
|
|
.Replace('+', '-')
|
|
.Replace('/', '_');
|
|
}
|
|
|
|
public static bool TryDecode(string? cursor, out TaskBoardCursorPosition position)
|
|
{
|
|
position = default;
|
|
if (string.IsNullOrWhiteSpace(cursor) || cursor.Length > MaximumEncodedLength)
|
|
return false;
|
|
|
|
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 = Encoding.UTF8.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;
|
|
}
|
|
|
|
var updatedAt = new DateTimeOffset(utcTicks, TimeSpan.Zero);
|
|
position = new TaskBoardCursorPosition(updatedAt, id);
|
|
return true;
|
|
}
|
|
catch (Exception exception) when (
|
|
exception is FormatException
|
|
or ArgumentOutOfRangeException
|
|
or DecoderFallbackException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
public sealed class InvalidTaskBoardCursorException()
|
|
: FormatException("The Done cursor is invalid or unsupported.");
|