Skip to content

Commit

Permalink
Support ReadOnlySequence as input (#106)
Browse files Browse the repository at this point in the history
* API defined

As extension-method per #61 (comment)

* Tests

* Implementation

* ReadMe and demo updated

* Demo for ASP.NET Core BodyWriter

* Added comment for hacky example

[skip ci]

* Added benchmark

* Update source/gfoidl.Base64/ThrowHelper.cs

Co-Authored-By: Yann Crumeyrolle <[email protected]>

* Whitespace

* xml doc comments
  • Loading branch information
gfoidl authored Nov 17, 2019
1 parent b6b35f0 commit 3ecee89
Show file tree
Hide file tree
Showing 24 changed files with 968 additions and 21 deletions.
24 changes: 23 additions & 1 deletion README.md → ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ finally scalar code processes the rest (including padding).

Basically the entry to encoder / decoder is `Base64.Default` for _base64_, and `Base64.Url` for _base64Url_.

See [demo](./demo/gfoidl.Base64.Demo/Program.cs) for further examples.

### Encoding

```c#
Expand Down Expand Up @@ -99,7 +101,27 @@ status = Base64.Default.Decode(base64.Slice(consumed), decoded.Slice
decoded = decoded.Slice(0, written + written1);
```

See [demo](./demo/gfoidl.Base64.Demo/Program.cs) for further examples.
### ReadOnlySequence / IBufferWriter

Encoding / decoding with `ReadOnlySequence<byte>` and `IBufferWriter<byte>` can be used together with `System.IO.Pipelines`.

```c#
var pipeOptions = PipeOptions.Default;
var pipe = new Pipe(pipeOptions);

var rnd = new Random(42);
var data = new byte[4097];
rnd.NextBytes(data);

pipe.Writer.Write(data);
await pipe.Writer.CompleteAsync();

ReadResult readResult = await pipe.Reader.ReadAsync();

var resultPipe = new Pipe();
Base64.Default.Encode(readResult.Buffer, resultPipe.Writer, out long consumed, out long written);
await resultPipe.Writer.CompleteAsync();
```

## (Functional) Comparison to classes in .NET

Expand Down
52 changes: 42 additions & 10 deletions demo/gfoidl.Base64.Demo/Program.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
using System;
using System.Buffers;
using System.Diagnostics;
using System.IO.Pipelines;
using System.Threading.Tasks;

namespace gfoidl.Base64.Demo
{
class Program
{
static void Main()
static async Task Main()
{
Action[] demos = { RunGuidEncoding, RunGuidDecoding, RunBufferChainEncode, RunDetectEncoding };
Func<Task>[] demos = { RunGuidEncoding, RunGuidDecoding, RunBufferChainEncoding, RunDetectEncoding, RunSequenceEncoding };

foreach (Action demo in demos)
foreach (Func<Task> demo in demos)
{
Console.Write($"{demo.Method.Name}...");
demo();
await demo();
Console.WriteLine("done");
}
}
//---------------------------------------------------------------------
private static void RunGuidEncoding()
private static Task RunGuidEncoding()
{
byte[] guid = Guid.NewGuid().ToByteArray();

Expand All @@ -38,9 +40,11 @@ private static void RunGuidEncoding()
Debug.Assert(status == OperationStatus.Done);
Debug.Assert(consumed == guid.Length);
Debug.Assert(written == guidBase64UrlUTF8.Length);

return Task.CompletedTask;
}
//---------------------------------------------------------------------
private static void RunGuidDecoding()
private static Task RunGuidDecoding()
{
Guid guid = Guid.NewGuid();

Expand Down Expand Up @@ -68,9 +72,11 @@ private static void RunGuidDecoding()
Debug.Assert(status == OperationStatus.Done);
Debug.Assert(consumed == guidBase64Url.Length);
Debug.Assert(written == guidBase64UrlDecodedBuffer.Length);

return Task.CompletedTask;
}
//---------------------------------------------------------------------
private static void RunBufferChainEncode()
private static Task RunBufferChainEncoding()
{
var rnd = new Random();
Span<byte> data = new byte[1000];
Expand All @@ -80,23 +86,25 @@ private static void RunBufferChainEncode()
Span<char> base64 = new char[5000];

OperationStatus status = Base64.Default.Encode(data.Slice(0, 400), base64, out int consumed, out int written, isFinalBlock: false);
Debug.Assert(status == OperationStatus.NeedMoreData);
Debug.Assert(status == OperationStatus.NeedMoreData); // 400 is not a multiple of 3, so more data is needed
status = Base64.Default.Encode(data.Slice(consumed), base64.Slice(written), out consumed, out int written1, isFinalBlock: true);
Debug.Assert(status == OperationStatus.Done);

base64 = base64.Slice(0, written + written1);

Span<byte> decoded = new byte[5000];
status = Base64.Default.Decode(base64.Slice(0, 100), decoded, out consumed, out written, isFinalBlock: false);
Debug.Assert(status == OperationStatus.Done); // 100 encoded bytes can be decoded to 75 bytes, so Done
Debug.Assert(status == OperationStatus.Done); // 100 encoded bytes can be decoded to 75 bytes, so Done
status = Base64.Default.Decode(base64.Slice(consumed), decoded.Slice(written), out consumed, out written1, isFinalBlock: true);
Debug.Assert(status == OperationStatus.Done);

decoded = decoded.Slice(0, written + written1);
Debug.Assert(data.SequenceEqual(decoded));

return Task.CompletedTask;
}
//---------------------------------------------------------------------
private static void RunDetectEncoding()
private static Task RunDetectEncoding()
{
// Let's assume we don't know whether this string is base64 or base64Url
string encodedString = "a-_9";
Expand All @@ -121,6 +129,30 @@ private static void RunDetectEncoding()
data = data.Slice(0, written);

Debug.Assert(data.Length == 3);

return Task.CompletedTask;
}
//---------------------------------------------------------------------
private static async Task RunSequenceEncoding()
{
var pipeOptions = PipeOptions.Default;
var pipe = new Pipe(pipeOptions);

var rnd = new Random(42);
var data = new byte[4097];
rnd.NextBytes(data);

pipe.Writer.Write(data);
await pipe.Writer.CompleteAsync();

ReadResult readResult = await pipe.Reader.ReadAsync();

var resultPipe = new Pipe();
Base64.Default.Encode(readResult.Buffer, resultPipe.Writer, out long consumed, out long written);
await resultPipe.Writer.CompleteAsync();

Debug.Assert(consumed == data.Length);
Debug.Assert(written == (data.Length + 2) / 3 * 4);
}
}
}
4 changes: 4 additions & 0 deletions demo/gfoidl.Base64.Demo/gfoidl.Base64.Demo.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.IO.Pipelines" Version="4.6.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\source\gfoidl.Base64\gfoidl.Base64.csproj" />
</ItemGroup>
Expand Down
18 changes: 18 additions & 0 deletions demo/gfoidl.Base64.WebDemo/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace gfoidl.Base64.WebDemo
{
public static class Program
{
public static Task Main(string[] args) => CreateHostBuilder(args).Build().RunAsync();
//---------------------------------------------------------------------
public static IHostBuilder CreateHostBuilder(string[] args)
=> Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
27 changes: 27 additions & 0 deletions demo/gfoidl.Base64.WebDemo/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication" : false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:61968",
"sslPort" : 44327
}
},
"profiles": {
"IIS Express": {
"commandName" : "IISExpress",
"launchBrowser" : true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"gfoidl.Base64.WebDemo": {
"commandName" : "Project",
"launchBrowser" : true,
"applicationUrl" : "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
47 changes: 47 additions & 0 deletions demo/gfoidl.Base64.WebDemo/Startup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.IO.Pipelines;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace gfoidl.Base64.WebDemo
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
}
//---------------------------------------------------------------------
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseRouting();

app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("POST data");
});
endpoints.MapPost("/", async context =>
{
// Very simple and hacky example
ReadResult readResult = await context.Request.BodyReader.ReadAsync();
await context.Response.WriteAsync("base64:\n");
Base64.Default.Encode(readResult.Buffer, context.Response.BodyWriter, out long _, out long written);
await context.Response.WriteAsync("\nbase64Url:\n");
Base64.Url.Encode(readResult.Buffer, context.Response.BodyWriter, out long _, out written);
});
});
}
}
}
9 changes: 9 additions & 0 deletions demo/gfoidl.Base64.WebDemo/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default" : "Debug",
"System" : "Information",
"Microsoft": "Information"
}
}
}
10 changes: 10 additions & 0 deletions demo/gfoidl.Base64.WebDemo/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default" : "Information",
"Microsoft" : "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
11 changes: 11 additions & 0 deletions demo/gfoidl.Base64.WebDemo/gfoidl.Base64.WebDemo.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\source\gfoidl.Base64\gfoidl.Base64.csproj" />
</ItemGroup>

</Project>
1 change: 0 additions & 1 deletion fuzz/gfoidl.Base64.FuzzTests/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ private static bool ContainsInvalidData(ReadOnlySpan<byte> encoded, Base64 encod
{
ReadOnlySpan<sbyte> decodingMap = default;


if (encoder is Base64Encoder)
{
decodingMap = Base64Encoder.DecodingMap;
Expand Down
9 changes: 9 additions & 0 deletions gfoidl.Base64.sln
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{6920
fuzz\setup.sh = fuzz\setup.sh
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "gfoidl.Base64.WebDemo", "demo\gfoidl.Base64.WebDemo\gfoidl.Base64.WebDemo.csproj", "{66E1E4B6-BD9B-40FF-85A9-070B0D497E59}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -116,6 +118,12 @@ Global
{DC515055-B8E7-4C25-B4CF-703292C4F975}.Fuzz|Any CPU.ActiveCfg = Release|Any CPU
{DC515055-B8E7-4C25-B4CF-703292C4F975}.Fuzz|Any CPU.Build.0 = Release|Any CPU
{DC515055-B8E7-4C25-B4CF-703292C4F975}.Release|Any CPU.ActiveCfg = Release|Any CPU
{66E1E4B6-BD9B-40FF-85A9-070B0D497E59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{66E1E4B6-BD9B-40FF-85A9-070B0D497E59}.Debug|Any CPU.Build.0 = Debug|Any CPU
{66E1E4B6-BD9B-40FF-85A9-070B0D497E59}.Fuzz|Any CPU.ActiveCfg = Debug|Any CPU
{66E1E4B6-BD9B-40FF-85A9-070B0D497E59}.Fuzz|Any CPU.Build.0 = Debug|Any CPU
{66E1E4B6-BD9B-40FF-85A9-070B0D497E59}.Release|Any CPU.ActiveCfg = Release|Any CPU
{66E1E4B6-BD9B-40FF-85A9-070B0D497E59}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -133,6 +141,7 @@ Global
{CD033330-6EDE-4E8E-976C-5CB7C58217E8} = {403238BD-BE8A-4E78-924A-C841CD8C7955}
{3F0FD9C1-1B52-4BC6-9BD8-8FD3B0162E88} = {CD033330-6EDE-4E8E-976C-5CB7C58217E8}
{692006B6-CD1B-4A91-97C5-0A98C25EAD23} = {89ADD037-F9FC-4ADE-99B8-D89307057409}
{66E1E4B6-BD9B-40FF-85A9-070B0D497E59} = {A52BCD9C-4216-42E5-AB37-FD5F6C46A9E7}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A902DC31-2F3E-4397-93D1-5004F325B71C}
Expand Down
Loading

0 comments on commit 3ecee89

Please sign in to comment.