Skip to content

Commit

Permalink
Add the IEnumerable, array and List parsers.
Browse files Browse the repository at this point in the history
The length prefix based one is not prefixed because it is the intended
one to be used for the common case and the sentinel one should only be
used for supporting already existing formats.
  • Loading branch information
GGG-KILLER committed Aug 17, 2022
1 parent 7ffa55d commit 81fa68f
Show file tree
Hide file tree
Showing 2 changed files with 386 additions and 0 deletions.
178 changes: 178 additions & 0 deletions Tsu.BinaryParser/src/Parsers/ArrayBinaryParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Copyright © 2021 GGG KILLER <[email protected]>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
// and associated documentation files (the “Software”), to deal in the Software without
// restriction, including without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom
// the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tsu.BinaryParser.Parsers;

namespace Tsu.BinaryParser.Parsers
{
/// <summary>
/// The builtin length-prefixed array parser.
/// </summary>
/// <typeparam name="T"></typeparam>
public sealed class ArrayBinaryParser<T> : IBinaryParser<List<T>>, IBinaryParser<T[]>, IBinaryParser<IEnumerable<T>>
{
private readonly Int32BinaryParser _lengthParser;
private readonly IBinaryParser<T> _wrappedParser;
private readonly bool _acceptEofAsEnd;

/// <summary>
/// Initializes the builtin parser for nullable types.
/// </summary>
/// <param name="wrappedParser">
/// The parser that will be responsible for parsing the actual type.
/// </param>
/// <param name="acceptEofAsEnd">
/// Whether we should accept an end of stream as the sentinel value.
/// </param>
public ArrayBinaryParser(
IBinaryParser<T> wrappedParser,
bool acceptEofAsEnd)
{
_lengthParser = new Int32BinaryParser(false);
_wrappedParser = wrappedParser ?? throw new ArgumentNullException(nameof(wrappedParser));
_acceptEofAsEnd = acceptEofAsEnd;
}

/// <remarks>
/// This always returns 4 which is the amount of bytes used to represent the 0 length of the array.
/// </remarks>
public Option<int> MinimumByteCount => 4;
/// <inheritdoc/>
/// <remarks>
/// This always returns None because there is no limit to the size of an array other than <see cref="int.MaxValue"/>.
/// </remarks>
public Option<int> MaxmiumByteCount => Option.None<int>();
/// <inheritdoc/>
public bool IsFixedSize => false;

/// <inheritdoc/>
public long CalculateSize(IEnumerable<T> values)
{
long size = 0;
var count = 0;
foreach (var value in values)
{
size += _wrappedParser.CalculateSize(value);
count++;
}
size += _lengthParser.CalculateSize(count);
return size;
}
long IBinaryParser<T[]>.CalculateSize(T[] values) => CalculateSize(values);
long IBinaryParser<List<T>>.CalculateSize(List<T> values) => CalculateSize(values);

/// <inheritdoc/>
public List<T> Deserialize(IBinaryReader reader, IBinaryParsingContext context)
{
var values = new List<T>();
var count = _lengthParser.Deserialize(reader, context);
while (!reader.EndOfStream && values.Count < count)
{
var value = _wrappedParser.Deserialize(reader, context);
values.Add(value);
}

if (reader.EndOfStream && !_acceptEofAsEnd)
throw new FormatException("Hit EOF before reading all values for the array.");

return values;
}
T[] IBinaryParser<T[]>.Deserialize(IBinaryReader reader, IBinaryParsingContext context) =>
Deserialize(reader, context).ToArray();
IEnumerable<T> IBinaryParser<IEnumerable<T>>.Deserialize(IBinaryReader reader, IBinaryParsingContext context) =>
Deserialize(reader, context);

/// <inheritdoc/>
public async ValueTask<List<T>> DeserializeAsync(IBinaryReader reader, IBinaryParsingContext context, CancellationToken cancellationToken = default)
{
var values = new List<T>();
var count = await _lengthParser.DeserializeAsync(reader, context, cancellationToken);
while (!reader.EndOfStream && values.Count < count)
{
var value = await _wrappedParser.DeserializeAsync(reader, context, cancellationToken);
values.Add(value);
}

if (reader.EndOfStream && !_acceptEofAsEnd)
throw new FormatException("Hit EOF before reading all values for the array.");

return values;
}
async ValueTask<T[]> IBinaryParser<T[]>.DeserializeAsync(IBinaryReader reader, IBinaryParsingContext context, CancellationToken cancellationToken) =>
(await DeserializeAsync(reader, context, cancellationToken)).ToArray();
async ValueTask<IEnumerable<T>> IBinaryParser<IEnumerable<T>>.DeserializeAsync(IBinaryReader reader, IBinaryParsingContext context, CancellationToken cancellationToken) =>
await DeserializeAsync(reader, context, cancellationToken);

/// <inheritdoc/>
public void Serialize(Stream stream, IBinaryParsingContext context, IEnumerable<T> values)
{
var count = values.Count();
_lengthParser.Serialize(stream, context, count);
foreach (var value in values)
{
_wrappedParser.Serialize(stream, context, value);
}
}
void IBinaryParser<List<T>>.Serialize(Stream stream, IBinaryParsingContext context, List<T> values) => Serialize(stream, context, values);
void IBinaryParser<T[]>.Serialize(Stream stream, IBinaryParsingContext context, T[] values) => Serialize(stream, context, values);

/// <inheritdoc/>
public async ValueTask SerializeAsync(Stream stream, IBinaryParsingContext context, IEnumerable<T> values, CancellationToken cancellationToken = default)
{
var count = values.Count();
await _lengthParser.SerializeAsync(stream, context, count, cancellationToken);
foreach (var value in values)
{
await _wrappedParser.SerializeAsync(stream, context, value, cancellationToken);
}
}
ValueTask IBinaryParser<List<T>>.SerializeAsync(Stream stream, IBinaryParsingContext context, List<T> values, CancellationToken cancellationToken) =>
SerializeAsync(stream, context, values, cancellationToken);
ValueTask IBinaryParser<T[]>.SerializeAsync(Stream stream, IBinaryParsingContext context, T[] values, CancellationToken cancellationToken) =>
SerializeAsync(stream, context, values, cancellationToken);
}
}

namespace Tsu.BinaryParser
{
public static partial class BinaryParserExtensions
{
/// <summary>
/// Wraps the parser in an <see cref="ArrayBinaryParser{T}"/> parser.
/// </summary>
/// <typeparam name="T">
/// The type which is optional
/// </typeparam>
/// <param name="wrappedParser">
/// <inheritdoc cref="ArrayBinaryParser{T}.ArrayBinaryParser(IBinaryParser{T}, bool)" path="/param[@name='wrappedParser']"/>
/// </param>
/// <param name="acceptEofAsEnd">
/// <inheritdoc cref="ArrayBinaryParser{T}.ArrayBinaryParser(IBinaryParser{T}, bool)" path="/param[@name='acceptEofAsEnd']"/>
/// </param>
/// <returns></returns>
public static ArrayBinaryParser<T> ArrayOf<T>(
this IBinaryParser<T> wrappedParser,
bool acceptEofAsEnd = false) =>
new ArrayBinaryParser<T>(wrappedParser, acceptEofAsEnd);
}
}
208 changes: 208 additions & 0 deletions Tsu.BinaryParser/src/Parsers/SentinelArrayBinaryParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// Copyright © 2021 GGG KILLER <[email protected]>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
// and associated documentation files (the “Software”), to deal in the Software without
// restriction, including without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom
// the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tsu.BinaryParser.Parsers;

namespace Tsu.BinaryParser.Parsers
{
/// <summary>
/// The builtin sentinel-based array parser.
/// <para>This is <b>NOT RECOMMENDED</b> for creating your own file formats, only use it to parse existing file formats.</para>
/// <para>Always prefer the length-prefixed array parser for creating your own file formats instead.</para>
/// </summary>
/// <typeparam name="T"></typeparam>
public sealed class SentinelArrayBinaryParser<T> : IBinaryParser<List<T>>, IBinaryParser<T[]>, IBinaryParser<IEnumerable<T>>
{
private readonly IBinaryParser<T> _wrappedParser;
private readonly T _sentinelValue;
private readonly bool _includeSentinelInTheArray, _acceptEofAsSentinel;

/// <summary>
/// Initializes the builtin parser for nullable types.
/// </summary>
/// <param name="wrappedParser">
/// The parser that will be responsible for parsing the actual type.
/// </param>
/// <param name="sentinelValue">
/// The sentinel value we should stop reading at.
/// </param>
/// <param name="includeSentinelInTheArray">
/// Whether the sentinel value should be included in the parsed array.
/// <param name="acceptEofAsSentinel">
/// </param>
/// Whether we should accept an end of stream as the sentinel value.
/// </param>
public SentinelArrayBinaryParser(
IBinaryParser<T> wrappedParser,
T sentinelValue,
bool includeSentinelInTheArray,
bool acceptEofAsSentinel)
{
_wrappedParser = wrappedParser ?? throw new ArgumentNullException(nameof(wrappedParser));
_sentinelValue = sentinelValue;
_includeSentinelInTheArray = includeSentinelInTheArray;
_acceptEofAsSentinel = acceptEofAsSentinel;
}

/// <inheritdoc/>
public Option<int> MinimumByteCount => _wrappedParser.MinimumByteCount;
/// <inheritdoc/>
/// <remarks>
/// This always returns None because there is no sane limit to the size of an array other than <see cref="int.MaxValue"/>.
/// </remarks>
public Option<int> MaxmiumByteCount => Option.None<int>();
/// <inheritdoc/>
public bool IsFixedSize => false;

/// <inheritdoc/>
public long CalculateSize(IEnumerable<T> values)
{
var length = values.Sum(_wrappedParser.CalculateSize);
if (!EqualityComparer<T>.Default.Equals(_sentinelValue, values.Last()))
length += _wrappedParser.CalculateSize(_sentinelValue);
return length;
}
long IBinaryParser<T[]>.CalculateSize(T[] values) => CalculateSize(values);
long IBinaryParser<List<T>>.CalculateSize(List<T> values) => CalculateSize(values);

/// <inheritdoc/>
public List<T> Deserialize(IBinaryReader reader, IBinaryParsingContext context)
{
var values = new List<T>();
var foundSentinel = false;
while (!reader.EndOfStream)
{
var value = _wrappedParser.Deserialize(reader, context);
if (EqualityComparer<T>.Default.Equals(_sentinelValue, value))
{
foundSentinel = true;
if (_includeSentinelInTheArray)
values.Add(value);
break;
}
values.Add(value);
}

if (!foundSentinel && reader.EndOfStream && !_acceptEofAsSentinel)
throw new FormatException("Hit EOF before finding end of array value.");

return values;
}
T[] IBinaryParser<T[]>.Deserialize(IBinaryReader reader, IBinaryParsingContext context) =>
Deserialize(reader, context).ToArray();
IEnumerable<T> IBinaryParser<IEnumerable<T>>.Deserialize(IBinaryReader reader, IBinaryParsingContext context) =>
Deserialize(reader, context);

/// <inheritdoc/>
public async ValueTask<List<T>> DeserializeAsync(IBinaryReader reader, IBinaryParsingContext context, CancellationToken cancellationToken = default)
{
var values = new List<T>();
var foundSentinel = false;
while (!reader.EndOfStream)
{
var value = await _wrappedParser.DeserializeAsync(reader, context, cancellationToken);
if (EqualityComparer<T>.Default.Equals(_sentinelValue, value))
{
foundSentinel = true;
if (_includeSentinelInTheArray)
values.Add(value);
break;
}
values.Add(value);
}

if (!foundSentinel && reader.EndOfStream && !_acceptEofAsSentinel)
throw new FormatException("Hit EOF before finding end of array value.");

return values;
}
async ValueTask<T[]> IBinaryParser<T[]>.DeserializeAsync(IBinaryReader reader, IBinaryParsingContext context, CancellationToken cancellationToken) =>
(await DeserializeAsync(reader, context, cancellationToken)).ToArray();
async ValueTask<IEnumerable<T>> IBinaryParser<IEnumerable<T>>.DeserializeAsync(IBinaryReader reader, IBinaryParsingContext context, CancellationToken cancellationToken) =>
await DeserializeAsync(reader, context, cancellationToken);

/// <inheritdoc/>
public void Serialize(Stream stream, IBinaryParsingContext context, IEnumerable<T> values)
{
Option<T> last = Option.None<T>();
foreach (var value in values)
{
_wrappedParser.Serialize(stream, context, value);
last = value;
}
if (last.IsNone || !EqualityComparer<T>.Default.Equals(last.Value, _sentinelValue))
_wrappedParser.Serialize(stream, context, _sentinelValue);
}
void IBinaryParser<List<T>>.Serialize(Stream stream, IBinaryParsingContext context, List<T> values) => Serialize(stream, context, values);
void IBinaryParser<T[]>.Serialize(Stream stream, IBinaryParsingContext context, T[] values) => Serialize(stream, context, values);

/// <inheritdoc/>
public async ValueTask SerializeAsync(Stream stream, IBinaryParsingContext context, IEnumerable<T> values, CancellationToken cancellationToken = default)
{
Option<T> last = Option.None<T>();
foreach (var value in values)
{
await _wrappedParser.SerializeAsync(stream, context, value, cancellationToken);
last = value;
}
if (last.IsNone || !EqualityComparer<T>.Default.Equals(last.Value, _sentinelValue))
await _wrappedParser.SerializeAsync(stream, context, _sentinelValue, cancellationToken);
}
ValueTask IBinaryParser<List<T>>.SerializeAsync(Stream stream, IBinaryParsingContext context, List<T> values, CancellationToken cancellationToken) =>
SerializeAsync(stream, context, values, cancellationToken);
ValueTask IBinaryParser<T[]>.SerializeAsync(Stream stream, IBinaryParsingContext context, T[] values, CancellationToken cancellationToken) =>
SerializeAsync(stream, context, values, cancellationToken);
}
}

namespace Tsu.BinaryParser
{
public static partial class BinaryParserExtensions
{
/// <summary>
/// Wraps the parser in a <see cref="SentinelArrayBinaryParser{T}"/> parser.
/// </summary>
/// <typeparam name="T">
/// The type which is optional
/// </typeparam>
/// <param name="wrappedParser">
/// <inheritdoc cref="SentinelArrayBinaryParser{T}.SentinelArrayBinaryParser(IBinaryParser{T}, T, bool, bool)" path="/param[@name='wrappedParser']"/>
/// </param>
/// <param name="sentinelValue">
/// <inheritdoc cref="SentinelArrayBinaryParser{T}.SentinelArrayBinaryParser(IBinaryParser{T}, T, bool, bool)" path="/param[@name='sentinelValue']"/>
/// </param>
/// <param name="includeSentinelInTheArray">
/// <inheritdoc cref="SentinelArrayBinaryParser{T}.SentinelArrayBinaryParser(IBinaryParser{T}, T, bool, bool)" path="/param[@name='includeSentinelInTheArray']"/>
/// </param>
/// <param name="acceptEofAsSentinel">
/// <inheritdoc cref="SentinelArrayBinaryParser{T}.SentinelArrayBinaryParser(IBinaryParser{T}, T, bool, bool)" path="/param[@name='acceptEofAsSentinel']"/>
/// </param>
/// <returns></returns>
public static SentinelArrayBinaryParser<T> SentinelArrayOf<T>(
this IBinaryParser<T> wrappedParser,
T sentinelValue,
bool includeSentinelInTheArray = false,
bool acceptEofAsSentinel = true) =>
new SentinelArrayBinaryParser<T>(wrappedParser, sentinelValue, includeSentinelInTheArray, acceptEofAsSentinel);
}
}

0 comments on commit 81fa68f

Please sign in to comment.