Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
CypherPotato committed Jul 15, 2024
1 parent cbf7009 commit c7605d7
Show file tree
Hide file tree
Showing 18 changed files with 464 additions and 75 deletions.
12 changes: 12 additions & 0 deletions src/Entity/HttpHeaderCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

using Sisk.Core.Http;
using System.Collections.Specialized;
using System.Text;
using Header = Sisk.Core.Internal.HttpKnownHeaderNames;

namespace Sisk.Core.Entity;
Expand All @@ -34,6 +35,17 @@ public HttpHeaderCollection(NameValueCollection headers) : base(headers)
{
}

/// <inheritdoc/>
public override string ToString()
{
StringBuilder sb = new StringBuilder();
foreach (string key in this.Keys)
{
sb.AppendLine($"{key}: {this[key]}");
}
return sb.ToString();
}

/// <summary>
/// Gets or sets the value of the HTTP Accept header.
/// </summary>
Expand Down
67 changes: 64 additions & 3 deletions src/Entity/MultipartFormCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@

using System.Collections;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Text;

namespace Sisk.Core.Entity;

/// <summary>
/// Represents an class which hosts an multipart form data contents.
/// </summary>
public sealed class MultipartFormCollection : IEnumerable<MultipartObject>, IReadOnlyList<MultipartObject>, IReadOnlyCollection<MultipartObject>
public sealed class MultipartFormCollection : IReadOnlyList<MultipartObject>, IReadOnlyDictionary<string, MultipartObject>
{
private readonly IList<MultipartObject> _items;

Expand Down Expand Up @@ -61,23 +62,83 @@ public StringValue GetStringValue(string name)
public MultipartObject this[int index] => ((IReadOnlyList<MultipartObject>)_items)[index];

/// <exclude/>
/// <inheritdoc/>
public MultipartObject this[string name] => GetItem(name, false) ?? throw new KeyNotFoundException();

/// <inheritdoc/>
public int Count => ((IReadOnlyCollection<MultipartObject>)_items).Count;

/// <exclude/>

/// <inheritdoc/>
public IEnumerable<string> Keys
{
get
{
for (int i = 0; i < _items.Count; i++)
{
MultipartObject? item = _items[i];
yield return item.Name;
}
}
}


/// <inheritdoc/>
public IEnumerable<MultipartObject> Values
{
get
{
for (int i = 0; i < _items.Count; i++)
{
MultipartObject? item = _items[i];
yield return item;
}
}
}

/// <inheritdoc/>
public IEnumerator<MultipartObject> GetEnumerator()
{
return ((IEnumerable<MultipartObject>)_items).GetEnumerator();
}

/// <exclude/>
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)_items).GetEnumerator();
}

/// <inheritdoc/>
public bool ContainsKey(string key)
{
return _items.Any(i => i.Name.CompareTo(key) == 0);
}

/// <inheritdoc/>
public bool TryGetValue(string key, [MaybeNullWhen(false)] out MultipartObject value)
{
var i = _items.FirstOrDefault(item => item.Name.CompareTo(key) == 0);
if (i is null)
{
value = default;
return false;
}
else
{
value = i;
return true;
}
}

IEnumerator<KeyValuePair<string, MultipartObject>> IEnumerable<KeyValuePair<string, MultipartObject>>.GetEnumerator()
{
for (int i = 0; i < _items.Count; i++)
{
MultipartObject? item = _items[i];
yield return new KeyValuePair<string, MultipartObject>(item.Name, item);
}
}

/// <exclude/>
/// <inheritdoc/>
public static implicit operator MultipartObject[](MultipartFormCollection t)
Expand Down
203 changes: 203 additions & 0 deletions src/Entity/MultipartFormReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// The Sisk Framework source code
// Copyright (c) 2023 PROJECT PRINCIPIUM
//
// The code below is licensed under the MIT license as
// of the date of its publication, available at
//
// File name: MultipartFormReader.cs
// Repository: https://github.com/sisk-http/core

using Sisk.Core.Internal;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Sisk.Core.Entity;

internal sealed class MultipartFormReader
{
byte[] boundaryBytes;
byte[] bytes;
byte[] nlbytes;
int position = 0;
Encoding encoder;
bool debugEnabled;

public MultipartFormReader(byte[] inputBytes, byte[] boundaryBytes, Encoding baseEncoding, bool debugEnabled)
{
this.boundaryBytes = boundaryBytes;
this.encoder = baseEncoding;
this.bytes = inputBytes;
this.position = 0;
this.nlbytes = baseEncoding.GetBytes("\r\n");
this.debugEnabled = debugEnabled;
}

void ThrowDataException(string message)
{
if (debugEnabled)
{
throw new InvalidDataException(SR.Format(SR.MultipartFormReader_InvalidData, position, message));
}
}

bool CanRead { get => position < bytes.Length; }

int ReadByte()
{
if (CanRead)
return bytes[position++];

return -1;
}

void ReadNewLine()
{
position += nlbytes.Length;
}

int Read(Span<byte> buffer)
{
int read = 0;
for (int i = 0; i < buffer.Length; i++)
{
if (ReadByte() is > 0 and int b)
{
buffer[read++] = (byte)b;
}
else break;
}
return read;
}

public MultipartObject[] Read()
{
List<MultipartObject> objects = new List<MultipartObject>();
while (this.CanRead)
{
ReadNextBoundary();
NameValueCollection headers = ReadHeaders();

if (!CanRead)
break;

byte[] content = ReadContent().ToArray();

ReadNewLine();

string? contentDisposition = headers[HttpKnownHeaderNames.ContentDisposition];
if (contentDisposition is null)
{
ThrowDataException("The Content-Disposition header is empty or missing.");
continue;
}

NameValueCollection cdispositionValues = CookieParser.ParseCookieString(contentDisposition);
string? formItemName = CookieParser.RemoveValueQuotes(cdispositionValues["name"]);
string? formFilename = CookieParser.RemoveValueQuotes(cdispositionValues["filename"]);

if (string.IsNullOrEmpty(formItemName))
{
ThrowDataException("The Content-Disposition \"name\" parameter is empty or missing.");
continue;
}

MultipartObject resultObj = new MultipartObject(headers, formFilename, formItemName, content, encoder);

objects.Add(resultObj);
}

return objects.ToArray();
}

string ReadLine()
{
Span<byte> line = stackalloc byte[2048];
int read,
n = 0,
lnbytelen = nlbytes.Length;

while ((read = ReadByte()) > 0)
{
if (n == line.Length)
{
ThrowDataException($"Header line was too long (> {line.Length} bytes allocated).");
break;
}

line[n++] = (byte)read;

if (n >= lnbytelen)
{
if (line[(n - lnbytelen)..n].SequenceEqual(nlbytes))
{
break;
}
}
}

return encoder.GetString(line[0..n]);
}

Span<byte> ReadContent()
{
int boundaryLen = boundaryBytes.Length;
int istart = position;

while (CanRead)
{
position++;

if ((position - istart) > boundaryLen)
{
if (bytes[(position - boundaryLen)..position].SequenceEqual(boundaryBytes))
{
break;
}
}
}

position -= boundaryLen + nlbytes.Length + 2 /* the boundary "--" construct */;

return bytes[istart..position];
}

NameValueCollection ReadHeaders()
{
NameValueCollection headers = new NameValueCollection();
string? line;
while (!string.IsNullOrEmpty(line = ReadLine()))
{
int sepIndex = line.IndexOf(':');
if (sepIndex == -1)
break;

string hname = line.Substring(0, sepIndex);
string hvalue = line.Substring(sepIndex + 1).Trim();

headers.Add(hname, hvalue);
}

return headers;
}

unsafe void ReadNextBoundary()
{
Span<byte> boundaryBlock = stackalloc byte[boundaryBytes.Length + 2];
int nextLine = Read(boundaryBlock);

ReadNewLine();

if (nextLine != boundaryBlock.Length)
{
ThrowDataException($"Boundary expected at byte {position}.");
}
if (!boundaryBlock[2..].SequenceEqual(boundaryBytes))
{
ThrowDataException($"The provided boundary string does not match the request boundary string.");
}
}
}
Loading

0 comments on commit c7605d7

Please sign in to comment.