Move non-jellyfin extensions to separate project

This commit is contained in:
Bond_009
2021-06-19 18:02:33 +02:00
parent 0c3dcdf77b
commit 6f8ccab788
105 changed files with 298 additions and 173 deletions

View File

@@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
namespace Jellyfin.Extensions
{
/// <summary>
/// Alphanumeric <see cref="IComparer{T}" />.
/// </summary>
public class AlphanumericComparator : IComparer<string?>
{
/// <summary>
/// Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other.
/// </summary>
/// <param name="s1">The first object to compare.</param>
/// <param name="s2">The second object to compare.</param>
/// <returns>A signed integer that indicates the relative values of <c>x</c> and <c>y</c>.</returns>
public static int CompareValues(string? s1, string? s2)
{
if (s1 == null && s2 == null)
{
return 0;
}
else if (s1 == null)
{
return -1;
}
else if (s2 == null)
{
return 1;
}
int len1 = s1.Length;
int len2 = s2.Length;
// Early return for empty strings
if (len1 == 0 && len2 == 0)
{
return 0;
}
else if (len1 == 0)
{
return -1;
}
else if (len2 == 0)
{
return 1;
}
int pos1 = 0;
int pos2 = 0;
do
{
int start1 = pos1;
int start2 = pos2;
bool isNum1 = char.IsDigit(s1[pos1++]);
bool isNum2 = char.IsDigit(s2[pos2++]);
while (pos1 < len1 && char.IsDigit(s1[pos1]) == isNum1)
{
pos1++;
}
while (pos2 < len2 && char.IsDigit(s2[pos2]) == isNum2)
{
pos2++;
}
var span1 = s1.AsSpan(start1, pos1 - start1);
var span2 = s2.AsSpan(start2, pos2 - start2);
if (isNum1 && isNum2)
{
// Trim leading zeros so we can compare the length
// of the strings to find the largest number
span1 = span1.TrimStart('0');
span2 = span2.TrimStart('0');
var span1Len = span1.Length;
var span2Len = span2.Length;
if (span1Len < span2Len)
{
return -1;
}
else if (span1Len > span2Len)
{
return 1;
}
else if (span1Len >= 20) // Number is probably too big for a ulong
{
// Trim all the first digits that are the same
int i = 0;
while (i < span1Len && span1[i] == span2[i])
{
i++;
}
// If there are no more digits it's the same number
if (i == span1Len)
{
continue;
}
// Only need to compare the most significant digit
span1 = span1.Slice(i, 1);
span2 = span2.Slice(i, 1);
}
if (!ulong.TryParse(span1, out var num1)
|| !ulong.TryParse(span2, out var num2))
{
return 0;
}
else if (num1 < num2)
{
return -1;
}
else if (num1 > num2)
{
return 1;
}
}
else
{
int result = span1.CompareTo(span2, StringComparison.InvariantCulture);
if (result != 0)
{
return result;
}
}
#pragma warning disable SA1500 // TODO remove with StyleCop.Analyzers v1.2.0 https://github.com/DotNetAnalyzers/StyleCopAnalyzers/pull/3196
} while (pos1 < len1 && pos2 < len2);
#pragma warning restore SA1500
return len1 - len2;
}
/// <inheritdoc />
public int Compare(string? x, string? y)
{
return CompareValues(x, y);
}
}
}

View File

@@ -0,0 +1,26 @@
using System.Collections.Generic;
namespace Jellyfin.Extensions
{
/// <summary>
/// Provides <c>CopyTo</c> extensions methods for <see cref="IReadOnlyList{T}" />.
/// </summary>
public static class CopyToExtensions
{
/// <summary>
/// Copies all the elements of the current collection to the specified list
/// starting at the specified destination array index. The index is specified as a 32-bit integer.
/// </summary>
/// <param name="source">The current collection that is the source of the elements.</param>
/// <param name="destination">The list that is the destination of the elements copied from the current collection.</param>
/// <param name="index">A 32-bit integer that represents the index in <c>destination</c> at which copying begins.</param>
/// <typeparam name="T">The type of the array.</typeparam>
public static void CopyTo<T>(this IReadOnlyList<T> source, IList<T> destination, int index = 0)
{
for (int i = 0; i < source.Count; i++)
{
destination[index + i] = source[i];
}
}
}
}

View File

@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
namespace Jellyfin.Extensions
{
/// <summary>
/// Static extensions for the <see cref="IEnumerable{T}"/> interface.
/// </summary>
public static class EnumerableExtensions
{
/// <summary>
/// Determines whether the value is contained in the source collection.
/// </summary>
/// <param name="source">An instance of the <see cref="IEnumerable{String}"/> interface.</param>
/// <param name="value">The value to look for in the collection.</param>
/// <param name="stringComparison">The string comparison.</param>
/// <returns>A value indicating whether the value is contained in the collection.</returns>
/// <exception cref="ArgumentNullException">The source is null.</exception>
public static bool Contains(this IEnumerable<string> source, ReadOnlySpan<char> value, StringComparison stringComparison)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}
if (source is IList<string> list)
{
int len = list.Count;
for (int i = 0; i < len; i++)
{
if (value.Equals(list[i], stringComparison))
{
return true;
}
}
return false;
}
foreach (string element in source)
{
if (value.Equals(element, stringComparison))
{
return true;
}
}
return false;
}
}
}

View File

@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<Compile Include="../../SharedVersion.cs" />
</ItemGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,30 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Converts a number to a boolean.
/// This is needed for HDHomerun.
/// </summary>
public class JsonBoolNumberConverter : JsonConverter<bool>
{
/// <inheritdoc />
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Number)
{
return Convert.ToBoolean(reader.GetInt32());
}
return reader.GetBoolean();
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
writer.WriteBooleanValue(value);
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Convert comma delimited string to array of type.
/// </summary>
/// <typeparam name="T">Type to convert to.</typeparam>
public sealed class JsonCommaDelimitedArrayConverter<T> : JsonDelimitedArrayConverter<T>
{
/// <summary>
/// Initializes a new instance of the <see cref="JsonCommaDelimitedArrayConverter{T}"/> class.
/// </summary>
public JsonCommaDelimitedArrayConverter() : base()
{
}
/// <inheritdoc />
protected override char Delimiter => ',';
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Json comma delimited array converter factory.
/// </summary>
/// <remarks>
/// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow.
/// </remarks>
public class JsonCommaDelimitedArrayConverterFactory : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return true;
}
/// <inheritdoc />
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0];
return (JsonConverter?)Activator.CreateInstance(typeof(JsonCommaDelimitedArrayConverter<>).MakeGenericType(structType));
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Legacy DateTime converter.
/// Milliseconds aren't output if zero by default.
/// </summary>
public class JsonDateTimeConverter : JsonConverter<DateTime>
{
/// <inheritdoc />
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.GetDateTime();
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
if (value.Millisecond == 0)
{
// Remaining ticks value will be 0, manually format.
writer.WriteStringValue(value.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffZ", CultureInfo.InvariantCulture));
}
else
{
writer.WriteStringValue(value);
}
}
}
}

View File

@@ -0,0 +1,81 @@
using System;
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Convert delimited string to array of type.
/// </summary>
/// <typeparam name="T">Type to convert to.</typeparam>
public abstract class JsonDelimitedArrayConverter<T> : JsonConverter<T[]?>
{
private readonly TypeConverter _typeConverter;
/// <summary>
/// Initializes a new instance of the <see cref="JsonDelimitedArrayConverter{T}"/> class.
/// </summary>
protected JsonDelimitedArrayConverter()
{
_typeConverter = TypeDescriptor.GetConverter(typeof(T));
}
/// <summary>
/// Gets the array delimiter.
/// </summary>
protected virtual char Delimiter { get; }
/// <inheritdoc />
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
// GetString can't return null here because we already handled it above
var stringEntries = reader.GetString()?.Split(Delimiter, StringSplitOptions.RemoveEmptyEntries);
if (stringEntries == null || stringEntries.Length == 0)
{
return Array.Empty<T>();
}
var parsedValues = new object[stringEntries.Length];
var convertedCount = 0;
for (var i = 0; i < stringEntries.Length; i++)
{
try
{
parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim());
convertedCount++;
}
catch (FormatException)
{
// TODO log when upgraded to .Net6
// https://github.com/dotnet/runtime/issues/42975
// _logger.LogDebug(e, "Error converting value.");
}
}
var typedValues = new T[convertedCount];
var typedValueIndex = 0;
for (var i = 0; i < stringEntries.Length; i++)
{
if (parsedValues[i] != null)
{
typedValues.SetValue(parsedValues[i], typedValueIndex);
typedValueIndex++;
}
}
return typedValues;
}
return JsonSerializer.Deserialize<T[]>(ref reader, options);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T[]? value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,26 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Converts a GUID object or value to/from JSON.
/// </summary>
public class JsonGuidConverter : JsonConverter<Guid>
{
/// <inheritdoc />
public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var guidStr = reader.GetString();
return guidStr == null ? Guid.Empty : new Guid(guidStr);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString("N", CultureInfo.InvariantCulture));
}
}
}

View File

@@ -0,0 +1,25 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Converts an object to a lowercase string.
/// </summary>
/// <typeparam name="T">The object type.</typeparam>
public class JsonLowerCaseConverter<T> : JsonConverter<T>
{
/// <inheritdoc />
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return JsonSerializer.Deserialize<T>(ref reader, options);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStringValue(value?.ToString()?.ToLowerInvariant());
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Converts a GUID object or value to/from JSON.
/// </summary>
public class JsonNullableGuidConverter : JsonConverter<Guid?>
{
/// <inheritdoc />
public override Guid? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var guidStr = reader.GetString();
return guidStr == null ? null : new Guid(guidStr);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, Guid? value, JsonSerializerOptions options)
{
if (value == null || value == Guid.Empty)
{
writer.WriteNullValue();
}
else
{
writer.WriteStringValue(value.Value.ToString("N", CultureInfo.InvariantCulture));
}
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Converts a nullable struct or value to/from JSON.
/// Required - some clients send an empty string.
/// </summary>
/// <typeparam name="TStruct">The struct type.</typeparam>
public class JsonNullableStructConverter<TStruct> : JsonConverter<TStruct?>
where TStruct : struct
{
/// <inheritdoc />
public override TStruct? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}
// Token is empty string.
if (reader.TokenType == JsonTokenType.String && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty) || reader.ValueSpan.IsEmpty))
{
return null;
}
return JsonSerializer.Deserialize<TStruct>(ref reader, options);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, TStruct? value, JsonSerializerOptions options)
{
if (value.HasValue)
{
JsonSerializer.Serialize(writer, value.Value, options);
}
else
{
writer.WriteNullValue();
}
}
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Json nullable struct converter factory.
/// </summary>
public class JsonNullableStructConverterFactory : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsGenericType
&& typeToConvert.GetGenericTypeDefinition() == typeof(Nullable<>)
&& typeToConvert.GenericTypeArguments[0].IsValueType;
}
/// <inheritdoc />
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var structType = typeToConvert.GenericTypeArguments[0];
return (JsonConverter?)Activator.CreateInstance(typeof(JsonNullableStructConverter<>).MakeGenericType(structType));
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.ComponentModel;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Convert Pipe delimited string to array of type.
/// </summary>
/// <typeparam name="T">Type to convert to.</typeparam>
public sealed class JsonPipeDelimitedArrayConverter<T> : JsonDelimitedArrayConverter<T>
{
/// <summary>
/// Initializes a new instance of the <see cref="JsonPipeDelimitedArrayConverter{T}"/> class.
/// </summary>
public JsonPipeDelimitedArrayConverter() : base()
{
}
/// <inheritdoc />
protected override char Delimiter => '|';
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Json Pipe delimited array converter factory.
/// </summary>
/// <remarks>
/// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow.
/// </remarks>
public class JsonPipeDelimitedArrayConverterFactory : JsonConverterFactory
{
/// <inheritdoc />
public override bool CanConvert(Type typeToConvert)
{
return true;
}
/// <inheritdoc />
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0];
return (JsonConverter?)Activator.CreateInstance(typeof(JsonPipeDelimitedArrayConverter<>).MakeGenericType(structType));
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
using System.Buffers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Converter to allow the serializer to read strings.
/// </summary>
public class JsonStringConverter : JsonConverter<string?>
{
/// <inheritdoc />
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.Null => null,
JsonTokenType.String => reader.GetString(),
_ => GetRawValue(reader)
};
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
{
writer.WriteStringValue(value);
}
private static string GetRawValue(Utf8JsonReader reader)
{
var utf8Bytes = reader.HasValueSequence
? reader.ValueSequence.ToArray()
: reader.ValueSpan;
return Encoding.UTF8.GetString(utf8Bytes);
}
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Jellyfin.Extensions.Json.Converters
{
/// <summary>
/// Converts a Version object or value to/from JSON.
/// </summary>
/// <remarks>
/// Required to send <see cref="Version"/> as a string instead of an object.
/// </remarks>
public class JsonVersionConverter : JsonConverter<Version>
{
/// <inheritdoc />
public override Version Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> new Version(reader.GetString()!); // Will throw ArgumentNullException on null
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString());
}
}

View File

@@ -0,0 +1,90 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Jellyfin.Extensions.Json.Converters;
namespace Jellyfin.Extensions.Json
{
/// <summary>
/// Helper class for having compatible JSON throughout the codebase.
/// </summary>
public static class JsonDefaults
{
/// <summary>
/// Pascal case json profile media type.
/// </summary>
public const string PascalCaseMediaType = "application/json; profile=\"PascalCase\"";
/// <summary>
/// Camel case json profile media type.
/// </summary>
public const string CamelCaseMediaType = "application/json; profile=\"CamelCase\"";
/// <summary>
/// When changing these options, update
/// Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
/// -> AddJellyfinApi
/// -> AddJsonOptions.
/// </summary>
private static readonly JsonSerializerOptions _jsonSerializerOptions = new ()
{
ReadCommentHandling = JsonCommentHandling.Disallow,
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
Converters =
{
new JsonGuidConverter(),
new JsonNullableGuidConverter(),
new JsonVersionConverter(),
new JsonStringEnumConverter(),
new JsonNullableStructConverterFactory(),
new JsonBoolNumberConverter(),
new JsonDateTimeConverter(),
new JsonStringConverter()
}
};
private static readonly JsonSerializerOptions _pascalCaseJsonSerializerOptions = new (_jsonSerializerOptions)
{
PropertyNamingPolicy = null
};
private static readonly JsonSerializerOptions _camelCaseJsonSerializerOptions = new (_jsonSerializerOptions)
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
/// <summary>
/// Gets the default <see cref="JsonSerializerOptions" /> options.
/// </summary>
/// <remarks>
/// The return value must not be modified.
/// If the defaults must be modified the author must use the copy constructor.
/// </remarks>
/// <returns>The default <see cref="JsonSerializerOptions" /> options.</returns>
public static JsonSerializerOptions Options
=> _jsonSerializerOptions;
/// <summary>
/// Gets camelCase json options.
/// </summary>
/// <remarks>
/// The return value must not be modified.
/// If the defaults must be modified the author must use the copy constructor.
/// </remarks>
/// <returns>The camelCase <see cref="JsonSerializerOptions" /> options.</returns>
public static JsonSerializerOptions CamelCaseOptions
=> _camelCaseJsonSerializerOptions;
/// <summary>
/// Gets PascalCase json options.
/// </summary>
/// <remarks>
/// The return value must not be modified.
/// If the defaults must be modified the author must use the copy constructor.
/// </remarks>
/// <returns>The PascalCase <see cref="JsonSerializerOptions" /> options.</returns>
public static JsonSerializerOptions PascalCaseOptions
=> _pascalCaseJsonSerializerOptions;
}
}

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
namespace Jellyfin.Extensions
{
/// <summary>
/// Provides <c>Shuffle</c> extensions methods for <see cref="IList{T}" />.
/// </summary>
public static class ShuffleExtensions
{
private static readonly Random _rng = new Random();
/// <summary>
/// Shuffles the items in a list.
/// </summary>
/// <param name="list">The list that should get shuffled.</param>
/// <typeparam name="T">The type.</typeparam>
public static void Shuffle<T>(this IList<T> list)
{
list.Shuffle(_rng);
}
/// <summary>
/// Shuffles the items in a list.
/// </summary>
/// <param name="list">The list that should get shuffled.</param>
/// <param name="rng">The random number generator to use.</param>
/// <typeparam name="T">The type.</typeparam>
public static void Shuffle<T>(this IList<T> list, Random rng)
{
int n = list.Count;
while (n > 1)
{
int k = rng.Next(n--);
T value = list[k];
list[k] = list[n];
list[n] = value;
}
}
}
}

View File

@@ -0,0 +1,115 @@
/*
MIT License
Copyright (c) 2019 Gérald Barré
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.
*/
// TODO: remove when analyzer is fixed: https://github.com/dotnet/roslyn-analyzers/issues/5158
#pragma warning disable CA1034 // Nested types should not be visible
using System;
using System.Diagnostics.Contracts;
using System.Runtime.InteropServices;
namespace Jellyfin.Extensions
{
/// <summary>
/// Extension class for splitting lines without unnecessary allocations.
/// </summary>
public static class SplitStringExtensions
{
/// <summary>
/// Creates a new string split enumerator.
/// </summary>
/// <param name="str">The string to split.</param>
/// <param name="separator">The separator to split on.</param>
/// <returns>The enumerator struct.</returns>
[Pure]
public static Enumerator SpanSplit(this string str, char separator) => new (str.AsSpan(), separator);
/// <summary>
/// Creates a new span split enumerator.
/// </summary>
/// <param name="str">The span to split.</param>
/// <param name="separator">The separator to split on.</param>
/// <returns>The enumerator struct.</returns>
[Pure]
public static Enumerator Split(this ReadOnlySpan<char> str, char separator) => new (str, separator);
/// <summary>
/// Provides an enumerator for the substrings seperated by the separator.
/// </summary>
[StructLayout(LayoutKind.Auto)]
public ref struct Enumerator
{
private readonly char _separator;
private ReadOnlySpan<char> _str;
/// <summary>
/// Initializes a new instance of the <see cref="Enumerator"/> struct.
/// </summary>
/// <param name="str">The span to split.</param>
/// <param name="separator">The separator to split on.</param>
public Enumerator(ReadOnlySpan<char> str, char separator)
{
_str = str;
_separator = separator;
Current = default;
}
/// <summary>
/// Gets a reference to the item at the current position of the enumerator.
/// </summary>
public ReadOnlySpan<char> Current { get; private set; }
/// <summary>
/// Returns <c>this</c>.
/// </summary>
/// <returns><c>this</c>.</returns>
public readonly Enumerator GetEnumerator() => this;
/// <summary>
/// Advances the enumerator to the next item.
/// </summary>
/// <returns><c>true</c> if there is a next element; otherwise <c>false</c>.</returns>
public bool MoveNext()
{
if (_str.Length == 0)
{
return false;
}
var span = _str;
var index = span.IndexOf(_separator);
if (index == -1)
{
_str = ReadOnlySpan<char>.Empty;
Current = span;
return true;
}
Current = span.Slice(0, index);
_str = span[(index + 1)..];
return true;
}
}
}
}

View File

@@ -0,0 +1,63 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace Jellyfin.Extensions
{
/// <summary>
/// Class BaseExtensions.
/// </summary>
public static class StreamExtensions
{
/// <summary>
/// Reads all lines in the <see cref="Stream" />.
/// </summary>
/// <param name="stream">The <see cref="Stream" /> to read from.</param>
/// <returns>All lines in the stream.</returns>
public static string[] ReadAllLines(this Stream stream)
=> ReadAllLines(stream, Encoding.UTF8);
/// <summary>
/// Reads all lines in the <see cref="Stream" />.
/// </summary>
/// <param name="stream">The <see cref="Stream" /> to read from.</param>
/// <param name="encoding">The character encoding to use.</param>
/// <returns>All lines in the stream.</returns>
public static string[] ReadAllLines(this Stream stream, Encoding encoding)
{
using (StreamReader reader = new StreamReader(stream, encoding))
{
return ReadAllLines(reader).ToArray();
}
}
/// <summary>
/// Reads all lines in the <see cref="TextReader" />.
/// </summary>
/// <param name="reader">The <see cref="TextReader" /> to read from.</param>
/// <returns>All lines in the stream.</returns>
public static IEnumerable<string> ReadAllLines(this TextReader reader)
{
string? line;
while ((line = reader.ReadLine()) != null)
{
yield return line;
}
}
/// <summary>
/// Reads all lines in the <see cref="TextReader" />.
/// </summary>
/// <param name="reader">The <see cref="TextReader" /> to read from.</param>
/// <returns>All lines in the stream.</returns>
public static async IAsyncEnumerable<string> ReadAllLinesAsync(this TextReader reader)
{
string? line;
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
{
yield return line;
}
}
}
}

View File

@@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.Text;
namespace Jellyfin.Extensions
{
/// <summary>
/// Extension methods for the <see cref="StringBuilder"/> class.
/// </summary>
public static class StringBuilderExtensions
{
/// <summary>
/// Concatenates and appends the members of a collection in single quotes using the specified delimiter.
/// </summary>
/// <param name="builder">The string builder.</param>
/// <param name="delimiter">The character delimiter.</param>
/// <param name="values">The collection of strings to concatenate.</param>
/// <returns>The updated string builder.</returns>
public static StringBuilder AppendJoinInSingleQuotes(this StringBuilder builder, char delimiter, IReadOnlyList<string> values)
{
var len = values.Count;
for (var i = 0; i < len; i++)
{
builder.Append('\'')
.Append(values[i])
.Append('\'')
.Append(delimiter);
}
// remove last ,
builder.Length--;
return builder;
}
}
}

View File

@@ -0,0 +1,31 @@
using System;
namespace Jellyfin.Extensions
{
/// <summary>
/// Provides extensions methods for <see cref="string" />.
/// </summary>
public static class StringExtensions
{
/// <summary>
/// Counts the number of occurrences of [needle] in the string.
/// </summary>
/// <param name="value">The haystack to search in.</param>
/// <param name="needle">The character to search for.</param>
/// <returns>The number of occurrences of the [needle] character.</returns>
public static int Count(this ReadOnlySpan<char> value, char needle)
{
var count = 0;
var length = value.Length;
for (var i = 0; i < length; i++)
{
if (value[i] == needle)
{
count++;
}
}
return count;
}
}
}