Skip to content

Commit

Permalink
Use span based metadata parser on Core 3.1 or newer
Browse files Browse the repository at this point in the history
  • Loading branch information
smatsson committed Sep 28, 2021
1 parent 997aaff commit c5b79aa
Show file tree
Hide file tree
Showing 12 changed files with 279 additions and 91 deletions.
24 changes: 24 additions & 0 deletions Source/tusdotnet.benchmark/Benchmarks/MetadataParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using BenchmarkDotNet.Attributes;
using tusdotnet.Parsers;
using tusdotnet.Parsers.MetadataParserHelpers;

namespace tusdotnet.benchmark.Benchmarks
{
[MemoryDiagnoser]
public class MetadataParser
{
private const string UploadMetadata = "filename d29ybGRfZG9taW5hdGlvbl9wbGFuLnBkZg==,othermeta c29tZSBvdGhlciBkYXRh,utf8key wrbDgMSaxafMsw==,emptyKey,loremipsum TG9yZW0gSXBzdW0gaXMgc2ltcGx5IGR1bW15IHRleHQgb2YgdGhlIHByaW50aW5nIGFuZCB0eXBlc2V0dGluZyBpbmR1c3RyeS4gTG9yZW0gSXBzdW0gaGFzIGJlZW4gdGhlIGluZHVzdHJ5J3Mgc3RhbmRhcmQgZHVtbXkgdGV4dCBldmVyIHNpbmNlIHRoZSAxNTAwcywgd2hlbiBhbiB1bmtub3duIHByaW50ZXIgdG9vayBhIGdhbGxleSBvZiB0eXBlIGFuZCBzY3JhbWJsZWQgaXQgdG8gbWFrZSBhIHR5cGUgc3BlY2ltZW4gYm9vay4gSXQgaGFzIHN1cnZpdmVkIG5vdCBvbmx5IGZpdmUgY2VudHVyaWVzLCBidXQgYWxzbyB0aGUgbGVhcCBpbnRvIGVsZWN0cm9uaWMgdHlwZXNldHRpbmcsIHJlbWFpbmluZyBlc3NlbnRpYWxseSB1bmNoYW5nZWQuIEl0IHdhcyBwb3B1bGFyaXNlZCBpbiB0aGUgMTk2MHMgd2l0aCB0aGUgcmVsZWFzZSBvZiBMZXRyYXNldCBzaGVldHMgY29udGFpbmluZyBMb3JlbSBJcHN1bSBwYXNzYWdlcywgYW5kIG1vcmUgcmVjZW50bHkgd2l0aCBkZXNrdG9wIHB1Ymxpc2hpbmcgc29mdHdhcmUgbGlrZSBBbGR1cyBQYWdlTWFrZXIgaW5jbHVkaW5nIHZlcnNpb25zIG9mIExvcmVtIElwc3VtLg==";

[Benchmark(Baseline = true)]
public MetadataParserResult MetadataParserStringBased_Test()
{
return MetadataParserStringBased.ParseAndValidate(Models.MetadataParsingStrategy.AllowEmptyValues, UploadMetadata);
}

[Benchmark]
public MetadataParserResult MetadataParserSpanBased_Test()
{
return MetadataParserSpanBased.ParseAndValidate(UploadMetadata);
}
}
}
4 changes: 4 additions & 0 deletions Source/tusdotnet.benchmark/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using BenchmarkDotNet.Running;
using tusdotnet.benchmark.Benchmarks;
//using tusdotnet.benchmark.Benchmarks;

namespace tusdotnet.benchmark
Expand All @@ -13,6 +14,9 @@ public static void Main()
//var summary = BenchmarkRunner.Run<NoTusResumableHeader>();
//var summary = BenchmarkRunner.Run<RequestIsNotForTusEndpoint>();

var summary = BenchmarkRunner.Run<MetadataParser>();

//new MetadataParser().TestNewWithTextRead();
//Test().GetAwaiter().GetResult();
}

Expand Down
6 changes: 1 addition & 5 deletions Source/tusdotnet.benchmark/tusdotnet.benchmark.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,11 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\tusdotnet\tusdotnet.csproj" />
</ItemGroup>

<ItemGroup>
<Folder Include="Benchmarks\" />
</ItemGroup>

</Project>
27 changes: 27 additions & 0 deletions Source/tusdotnet/Extensions/Internal/ReadOnlySpanExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#if NETCOREAPP3_1_OR_GREATER

using System;
using System.Runtime.CompilerServices;

namespace tusdotnet.Extensions.Internal
{
internal static class ReadOnlySpanExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int Count(this ReadOnlySpan<char> span, char characterToFind)
{
var numberOfCharacters = 0;
for (int i = 0; i < span.Length; i++)
{
if (span[i] == characterToFind)
{
numberOfCharacters++;
}
}

return numberOfCharacters;
}
}
}

#endif
41 changes: 7 additions & 34 deletions Source/tusdotnet/Parsers/MetadataParser.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using tusdotnet.Models;
using tusdotnet.Models;
using tusdotnet.Parsers.MetadataParserHelpers;

namespace tusdotnet.Parsers
{
Expand Down Expand Up @@ -33,42 +32,16 @@ public static MetadataParserResult ParseAndValidate(MetadataParsingStrategy stra
* to help with compatibility with clients that are less than ideal.
* */

var parser = GetParser(strategy);
#if NETCOREAPP3_1_OR_GREATER

if (string.IsNullOrWhiteSpace(uploadMetadataHeaderValue))
if (strategy == MetadataParsingStrategy.AllowEmptyValues)
{
return parser.GetResultForEmptyHeader();
return MetadataParserSpanBased.ParseAndValidate(uploadMetadataHeaderValue);
}

var splitMetadataHeader = uploadMetadataHeaderValue.Split(',');
var parsedMetadata = new Dictionary<string, Metadata>(splitMetadataHeader.Length);
#endif

foreach (var pair in splitMetadataHeader)
{
var singleItemParseResult = parser.ParseSingleItem(pair, parsedMetadata.Keys);

if (singleItemParseResult.Success)
{
var parsedKeyAndValue = singleItemParseResult.Metadata.First();
parsedMetadata.Add(parsedKeyAndValue.Key, parsedKeyAndValue.Value);
}
else
{
return singleItemParseResult;
}
}

return MetadataParserResult.FromResult(parsedMetadata);
}

private static IInternalMetadataParser GetParser(MetadataParsingStrategy strategy)
{
if (strategy == MetadataParsingStrategy.Original)
{
return new OriginalMetadataParser();
}

return new AllowEmptyValuesMetadataParser();
return MetadataParserStringBased.ParseAndValidate(strategy, uploadMetadataHeaderValue);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
using System;
using System.Collections.Generic;
using tusdotnet.Constants;
using tusdotnet.Models;

namespace tusdotnet.Parsers
namespace tusdotnet.Parsers.MetadataParserHelpers
{
internal class AllowEmptyValuesMetadataParser : IInternalMetadataParser
{
internal static AllowEmptyValuesMetadataParser Instance { get; } = new AllowEmptyValuesMetadataParser();

private AllowEmptyValuesMetadataParser()
{
}

public MetadataParserResult GetResultForEmptyHeader()
{
return MetadataParserResult.FromResult(new Dictionary<string, Metadata>());
Expand All @@ -18,18 +23,18 @@ public MetadataParserResult ParseSingleItem(string metadataItem, ICollection<str

if (pairParts.Length < 1 || pairParts.Length > 2)
{
return MetadataParserResult.FromError($"Header {HeaderConstants.UploadMetadata}: The Upload-Metadata request and response header MUST consist of one or more comma - separated key - value pairs. The key and value MUST be separated by a space.The key MUST NOT contain spaces and commas and MUST NOT be empty. The key SHOULD be ASCII encoded and the value MUST be Base64 encoded. All keys MUST be unique. The value MAY be empty. In these cases, the space, which would normally separate the key and the value, MAY be left out.");
return MetadataParserResult.FromError(MetadataParserErrorTexts.INVALID_FORMAT_ALLOW_EMPTY_VALUES);
}

var key = pairParts[0];
if (string.IsNullOrEmpty(key))
{
return MetadataParserResult.FromError($"Header {HeaderConstants.UploadMetadata}: Key must not be empty");
return MetadataParserResult.FromError(MetadataParserErrorTexts.KEY_EMPTY);
}

if (existingKeys.Contains(key))
{
return MetadataParserResult.FromError($"Header {HeaderConstants.UploadMetadata}: Duplicate keys are not allowed");
return MetadataParserResult.FromError(MetadataParserErrorTexts.DUPLICATE_KEY_FOUND);
}

try
Expand All @@ -38,7 +43,7 @@ public MetadataParserResult ParseSingleItem(string metadataItem, ICollection<str
}
catch (FormatException)
{
return MetadataParserResult.FromError($"Header {HeaderConstants.UploadMetadata}: Value for {key} is not properly encoded using base64");
return MetadataParserResult.FromError(MetadataParserErrorTexts.InvalidBase64Value(key));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Collections.Generic;

namespace tusdotnet.Parsers
namespace tusdotnet.Parsers.MetadataParserHelpers
{
internal interface IInternalMetadataParser
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Runtime.CompilerServices;
using tusdotnet.Constants;

namespace tusdotnet.Parsers.MetadataParserHelpers
{
internal class MetadataParserErrorTexts
{
internal const string INVALID_FORMAT_ALLOW_EMPTY_VALUES = $"Header {HeaderConstants.UploadMetadata}: The Upload-Metadata request and response header MUST consist of one or more comma - separated key - value pairs. The key and value MUST be separated by a space.The key MUST NOT contain spaces and commas and MUST NOT be empty. The key SHOULD be ASCII encoded and the value MUST be Base64 encoded. All keys MUST be unique. The value MAY be empty. In these cases, the space, which would normally separate the key and the value, MAY be left out.";
internal const string INVALID_FORMAT_ORIGINAL = $"Header {HeaderConstants.UploadMetadata}: The Upload-Metadata request and response header MUST consist of one or more comma - separated key - value pairs. The key and value MUST be separated by a space.The key MUST NOT contain spaces and commas and MUST NOT be empty. The key SHOULD be ASCII encoded and the value MUST be Base64 encoded. All keys MUST be unique.";
internal const string KEY_EMPTY = $"Header {HeaderConstants.UploadMetadata}: Key must not be empty";
internal const string DUPLICATE_KEY_FOUND = $"Header {HeaderConstants.UploadMetadata}: Duplicate keys are not allowed";

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static string InvalidBase64Value(string metadataKey) => $"Header {HeaderConstants.UploadMetadata}: Value for {metadataKey} is not properly encoded using base64";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#if NETCOREAPP3_1_OR_GREATER
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using tusdotnet.Extensions.Internal;
using tusdotnet.Models;

namespace tusdotnet.Parsers.MetadataParserHelpers
{
internal class MetadataParserSpanBased
{
internal static MetadataParserResult ParseAndValidate(string uploadMetadataHeaderValue)
{
if (string.IsNullOrEmpty(uploadMetadataHeaderValue))
return MetadataParserResult.FromResult(new Dictionary<string, Metadata>());

var span = uploadMetadataHeaderValue.AsSpan();
var result = new Dictionary<string, Metadata>();

int indexOfNextPair = -1;
while (!span.IsEmpty)
{
indexOfNextPair = span.IndexOf(',');
var pair = indexOfNextPair == -1 ? span : span[0..indexOfNextPair].TrimEnd();

var indexOfSpaceInPair = pair.IndexOf(' ');

ReadOnlySpan<char> key;
ReadOnlySpan<char> value;

if (indexOfSpaceInPair == -1)
{
key = pair;
value = ReadOnlySpan<char>.Empty;
}
else
{
key = pair[0..indexOfSpaceInPair].TrimEnd();
value = pair[(indexOfSpaceInPair + 1)..].TrimEnd();
}

if (value.IndexOf(' ') > -1)
{
return MetadataParserResult.FromError(MetadataParserErrorTexts.INVALID_FORMAT_ALLOW_EMPTY_VALUES);
}

if (key.IsEmpty)
{
return MetadataParserResult.FromError(MetadataParserErrorTexts.KEY_EMPTY);
}

var keyString = key.ToString();

if (result.ContainsKey(keyString))
{
return MetadataParserResult.FromError(MetadataParserErrorTexts.DUPLICATE_KEY_FOUND);
}

byte[] decodedValue = null;
if (!value.IsEmpty)
{
var validBase64 = false;
(validBase64, decodedValue) = IsBase64String(value);
if (!validBase64)
{
return MetadataParserResult.FromError(MetadataParserErrorTexts.InvalidBase64Value(keyString));
}
}

result.Add(keyString, Metadata.FromBytes(decodedValue));

span = indexOfNextPair == -1 ? ReadOnlySpan<char>.Empty : span[(indexOfNextPair + 1)..];
}

return MetadataParserResult.FromResult(result);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static (bool, byte[]) IsBase64String(ReadOnlySpan<char> value)
{
var byteLength = (3 * (value.Length / 4)) - value.Count('=');
var bytes = new byte[byteLength];
return (Convert.TryFromBase64Chars(value, bytes, out var written), bytes);
}
}
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Collections.Generic;
using System.Linq;
using tusdotnet.Models;

namespace tusdotnet.Parsers.MetadataParserHelpers
{
internal class MetadataParserStringBased
{
internal static MetadataParserResult ParseAndValidate(MetadataParsingStrategy strategy, string uploadMetadataHeaderValue)
{
var parser = GetParser(strategy);

if (string.IsNullOrWhiteSpace(uploadMetadataHeaderValue))
{
return parser.GetResultForEmptyHeader();
}

var splitMetadataHeader = uploadMetadataHeaderValue.Split(',');
var parsedMetadata = new Dictionary<string, Metadata>(splitMetadataHeader.Length);

foreach (var pair in splitMetadataHeader)
{
var singleItemParseResult = parser.ParseSingleItem(pair, parsedMetadata.Keys);

if (singleItemParseResult.Success)
{
var parsedKeyAndValue = singleItemParseResult.Metadata.First();
parsedMetadata.Add(parsedKeyAndValue.Key, parsedKeyAndValue.Value);
}
else
{
return singleItemParseResult;
}
}

return MetadataParserResult.FromResult(parsedMetadata);
}

private static IInternalMetadataParser GetParser(MetadataParsingStrategy strategy)
{
return strategy == MetadataParsingStrategy.Original
? OriginalMetadataParser.Instance
: AllowEmptyValuesMetadataParser.Instance;
}
}
}
Loading

0 comments on commit c5b79aa

Please sign in to comment.