Skip to content

Commit

Permalink
more strict parsing (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
TenCoKaciStromy committed Jul 6, 2023
1 parent ad9e520 commit 08ebc48
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 63 deletions.
17 changes: 11 additions & 6 deletions src/dotnet-typeid-tests/TypeId_Test.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Headers;
using SimpleBase;
using TcKs.TypeId;
using UUIDNext;
using Xunit;

namespace dotnet_typeid_tests;
Expand All @@ -19,7 +18,7 @@ public class TypeId_Test {

[Fact]
public void ConstructorWorks() {
var id = Guid.NewGuid();
var id = Uuid.NewSequential();
var value = new TypeId("user", id);

Assert.Equal("user", value.Type);
Expand All @@ -29,16 +28,22 @@ public class TypeId_Test {

[Fact]
public void CanFormatAndParse() {
var id = Guid.NewGuid();

var id = Uuid.NewDatabaseFriendly(Database.PostgreSql);
var value0 = new TypeId("user", id);
var text0 = value0.ToString();

var value1 = TypeId.Parse(text0);
Assert.Equal(value0.Type, value1.Type);
Assert.Equal(value0.Id, value1.Id);
}

[Fact]
public void ParsingWillFailWhen128BitOverflow() {
Assert.False(TypeId.TryParse("prefix_80041061050r3gg28a1c60t3gg", out _));
Assert.False(TypeId.TryParse("prefix_8zzzzzzzzzzzzzzzzzzzzzzzzz", out _));
}

[Fact]
[SuppressMessage("ReSharper", "EqualExpressionComparison")]
public void EqualsWorks() {
Expand Down
40 changes: 40 additions & 0 deletions src/dotnet-typeid/Base32.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using System.Runtime.CompilerServices;

namespace TcKs.TypeId;

/// <summary>
/// Reimplemented Base32 encoding/decoding from original go code: https://github.com/jetpack-io/typeid-go/blob/main/base32/base32.go
/// </summary>
internal static partial class Base32 {
private const string Alphabet = "0123456789abcdefghjkmnpqrstvwxyz";

private static readonly byte[] DecBytes = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01,
0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0A, 0x0B, 0x0C,
0x0D, 0x0E, 0x0F, 0x10, 0x11, 0xFF, 0x12, 0x13, 0xFF, 0x14,
0x15, 0xFF, 0x16, 0x17, 0x18, 0x19, 0x1A, 0xFF, 0x1B, 0x1C,
0x1D, 0x1E, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};
}
102 changes: 102 additions & 0 deletions src/dotnet-typeid/Base32/Decoding.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System;
using System.Runtime.CompilerServices;

namespace TcKs.TypeId;

partial class Base32 {
public static byte[] Decode(string input)
=> TryDecode(input, out var output) is true && output is not null
? output
: throw new FormatException("Input can not be decoded.");

public static bool TryDecode(string input, out byte[]? output)
=> TryDecode(input.AsSpan(), out output);

public static byte[] Decode(ReadOnlyMemory<char> input)
=> TryDecode(input, out var output) is true && output is not null
? output
: throw new FormatException("Input can not be decoded.");

public static bool TryDecode(ReadOnlyMemory<char> input, out byte[]? output)
=> TryDecode(input.Span, out output);

public static byte[] Decode(ReadOnlySpan<char> input)
=> TryDecode(input, out var output) is true && output is not null
? output
: throw new FormatException("Input can not be decoded.");

public static bool TryDecode(ReadOnlySpan<char> input, out byte[]? output) {
if (input.Length != 26)
return Fail(out output);

var inputBytes = new byte[26];
if (System.Text.Encoding.UTF8.GetBytes(input, inputBytes.AsSpan()) != 26)
return Fail(out output);

if (!AllInputBytesAreOk(inputBytes))
return Fail(out output);

output = new byte[16];

var v = inputBytes;
var dec = DecBytes;

// 6 bytes timestamp (48 bits)
output[0] = (byte)((dec[v[0]] << 5) | dec[v[1]]);
output[1] = (byte)((dec[v[2]] << 3) | (dec[v[3]] >> 2));
output[2] = (byte)((dec[v[3]] << 6) | (dec[v[4]] << 1) | (dec[v[5]] >> 4));
output[3] = (byte)((dec[v[5]] << 4) | (dec[v[6]] >> 1));
output[4] = (byte)((dec[v[6]] << 7) | (dec[v[7]] << 2) | (dec[v[8]] >> 3));
output[5] = (byte)((dec[v[8]] << 5) | dec[v[9]]);

// 10 bytes of entropy (80 bits)
output[6] = (byte)((dec[v[10]] << 3) | (dec[v[11]] >> 2)); // First 4 bits are the version
output[7] = (byte)((dec[v[11]] << 6) | (dec[v[12]] << 1) | (dec[v[13]] >> 4));
output[8] = (byte)((dec[v[13]] << 4) | (dec[v[14]] >> 1)); // First 2 bits are the variant
output[9] = (byte)((dec[v[14]] << 7) | (dec[v[15]] << 2) | (dec[v[16]] >> 3));
output[10] = (byte)((dec[v[16]] << 5) | dec[v[17]]);
output[11] = (byte)((dec[v[18]] << 3) | dec[v[19]] >> 2);
output[12] = (byte)((dec[v[19]] << 6) | (dec[v[20]] << 1) | (dec[v[21]] >> 4));
output[13] = (byte)((dec[v[21]] << 4) | (dec[v[22]] >> 1));
output[14] = (byte)((dec[v[22]] << 7) | (dec[v[23]] << 2) | (dec[v[24]] >> 3));
output[15] = (byte)((dec[v[24]] << 5) | dec[v[25]]);

return true;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool AllInputBytesAreOk(byte[] inputBytes)
=> DecBytes is { } decBytes
&& decBytes[inputBytes[0]] != 0xFF
&& decBytes[inputBytes[1]] != 0xFF
&& decBytes[inputBytes[2]] != 0xFF
&& decBytes[inputBytes[3]] != 0xFF
&& decBytes[inputBytes[4]] != 0xFF
&& decBytes[inputBytes[5]] != 0xFF
&& decBytes[inputBytes[6]] != 0xFF
&& decBytes[inputBytes[7]] != 0xFF
&& decBytes[inputBytes[8]] != 0xFF
&& decBytes[inputBytes[9]] != 0xFF
&& decBytes[inputBytes[10]] != 0xFF
&& decBytes[inputBytes[11]] != 0xFF
&& decBytes[inputBytes[12]] != 0xFF
&& decBytes[inputBytes[13]] != 0xFF
&& decBytes[inputBytes[14]] != 0xFF
&& decBytes[inputBytes[15]] != 0xFF
&& decBytes[inputBytes[16]] != 0xFF
&& decBytes[inputBytes[17]] != 0xFF
&& decBytes[inputBytes[18]] != 0xFF
&& decBytes[inputBytes[19]] != 0xFF
&& decBytes[inputBytes[20]] != 0xFF
&& decBytes[inputBytes[21]] != 0xFF
&& decBytes[inputBytes[22]] != 0xFF
&& decBytes[inputBytes[23]] != 0xFF
&& decBytes[inputBytes[24]] != 0xFF
&& decBytes[inputBytes[25]] != 0xFF;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool Fail(out byte[]? output) {
output = default!;
return false;
}
}
}
75 changes: 75 additions & 0 deletions src/dotnet-typeid/Base32/Encoding.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;
using System.Runtime.CompilerServices;

namespace TcKs.TypeId;

partial class Base32 {
public static string Encode(byte[] input)
=> TryEncode(input, out var output) is true && output is not null
? output
: throw new FormatException("Input was can not be encoded.");

public static bool TryEncode(byte[] input, out string? output)
=> TryEncode(input.AsSpan(), out output);

public static string Encode(ReadOnlyMemory<byte> input)
=> TryEncode(input, out var output) is true && output is not null
? output
: throw new FormatException("Input was can not be encoded.");

public static bool TryEncode(ReadOnlyMemory<byte> input, out string? output)
=> TryEncode(input.Span, out output);

public static string Encode(ReadOnlySpan<byte> input)
=> TryEncode(input, out var output) is true && output is not null
? output
: throw new FormatException("Input was can not be encoded.");

public static bool TryEncode(ReadOnlySpan<byte> input, out string? output) {
if (input.Length != 16)
return Fail(out output);

var alphabet = Alphabet;
var src = input;
var dst = new char[26];

// 10 byte timestamp
dst[0] = alphabet[(src[0] & 224) >> 5];
dst[1] = alphabet[src[0] & 31];
dst[2] = alphabet[(src[1] & 248) >> 3];
dst[3] = alphabet[((src[1] & 7) << 2) | ((src[2] & 192) >> 6)];
dst[4] = alphabet[(src[2] & 62) >> 1];
dst[5] = alphabet[((src[2] & 1) << 4) | ((src[3] & 240) >> 4)];
dst[6] = alphabet[((src[3] & 15) << 1) | ((src[4] & 128) >> 7)];
dst[7] = alphabet[(src[4] & 124) >> 2];
dst[8] = alphabet[((src[4] & 3) << 3) | ((src[5] & 224) >> 5)];
dst[9] = alphabet[src[5] & 31];

// 16 bytes of entropy
dst[10] = alphabet[(src[6] & 248) >> 3];
dst[11] = alphabet[((src[6] & 7) << 2) | ((src[7] & 192) >> 6)];
dst[12] = alphabet[(src[7] & 62) >> 1];
dst[13] = alphabet[((src[7] & 1) << 4) | ((src[8] & 240) >> 4)];
dst[14] = alphabet[((src[8] & 15) << 1) | ((src[9] & 128) >> 7)];
dst[15] = alphabet[(src[9] & 124) >> 2];
dst[16] = alphabet[((src[9] & 3) << 3) | ((src[10] & 224) >> 5)];
dst[17] = alphabet[src[10] & 31];
dst[18] = alphabet[(src[11] & 248) >> 3];
dst[19] = alphabet[((src[11] & 7) << 2) | ((src[12] & 192) >> 6)];
dst[20] = alphabet[(src[12] & 62) >> 1];
dst[21] = alphabet[((src[12] & 1) << 4) | ((src[13] & 240) >> 4)];
dst[22] = alphabet[((src[13] & 15) << 1) | ((src[14] & 128) >> 7)];
dst[23] = alphabet[(src[14] & 124) >> 2];
dst[24] = alphabet[((src[14] & 3) << 3) | ((src[15] & 224) >> 5)];
dst[25] = alphabet[src[15] & 31];

output = new(dst);
return true;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool Fail(out string? output) {
output = default!;
return false;
}
}
}
93 changes: 49 additions & 44 deletions src/dotnet-typeid/TcKs.TypeId.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 08ebc48

Please sign in to comment.