diff --git a/Directory.Packages.props b/Directory.Packages.props index b48592a3..6bc60ac5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,6 +19,9 @@ + + + diff --git a/samples/FishyFlip.Firehose/FishyFlip.Firehose.csproj b/samples/FishyFlip.Firehose/FishyFlip.Firehose.csproj index 37128634..b6320f29 100644 --- a/samples/FishyFlip.Firehose/FishyFlip.Firehose.csproj +++ b/samples/FishyFlip.Firehose/FishyFlip.Firehose.csproj @@ -2,9 +2,10 @@ Exe - net7.0 + net462;net8.0 enable enable + true @@ -13,7 +14,6 @@ - all runtime; build; native; contentfiles; analyzers diff --git a/samples/FishyFlip.Firehose/Program.cs b/samples/FishyFlip.Firehose/Program.cs index 866d09c0..e3661054 100644 --- a/samples/FishyFlip.Firehose/Program.cs +++ b/samples/FishyFlip.Firehose/Program.cs @@ -2,7 +2,6 @@ // Copyright (c) Drastic Actions. All rights reserved. // -using Drastic.Tools; using FishyFlip; using FishyFlip.Models; using FishyFlip.Tools; @@ -23,7 +22,7 @@ atWebProtocol.OnSubscribedRepoMessage += (sender, args) => { - Task.Run(() => HandleMessageAsync(args.Message)).FireAndForgetSafeAsync(); + Task.Run(() => HandleMessageAsync(args.Message)).FireAndForget(); }; await atWebProtocol.StartSubscribeReposAsync(); @@ -60,7 +59,7 @@ async Task HandleMessageAsync(SubscribeRepoMessage message) // Commit.Ops are the actions used when creating the message. // In this case, it's a create record for the post. // The path contains the post action and path, we need the path, so we split to get it. - var url = $"https://bsky.app/profile/{did}/post/{message.Commit.Ops![0]!.Path!.Split("/").Last()}"; + var url = $"https://bsky.app/profile/{did}/post/{message.Commit.Ops![0]!.Path!.Split('/').Last()}"; Console.WriteLine($"Post URL: {url}, from {repo.Handle}"); if (post.Reply is not null) @@ -70,4 +69,25 @@ async Task HandleMessageAsync(SubscribeRepoMessage message) } } } +} + +public static class TaskExtensions +{ + public static void FireAndForget(this Task task, Action errorHandler = null) + { + if (task == null) + throw new ArgumentNullException(nameof(task)); + + task.ContinueWith(t => + { + if (errorHandler != null && t.IsFaulted) + errorHandler(t.Exception); + }, TaskContinuationOptions.OnlyOnFaulted); + + // Avoiding warning about not awaiting the fire-and-forget task. + // However, since the method is intended to fire and forget, we don't actually await it. +#pragma warning disable CS4014 + task.ConfigureAwait(false); +#pragma warning restore CS4014 + } } \ No newline at end of file diff --git a/src/FishyFlip.Tests/AuthorizedTests.cs b/src/FishyFlip.Tests/AuthorizedTests.cs index 0a953bc7..3c679808 100644 --- a/src/FishyFlip.Tests/AuthorizedTests.cs +++ b/src/FishyFlip.Tests/AuthorizedTests.cs @@ -4,6 +4,7 @@ using FishyFlip.Models; using Microsoft.Extensions.Logging.Debug; +using System.Net.Http; namespace FishyFlip.Tests; diff --git a/src/FishyFlip.Tests/FishyFlip.Tests.csproj b/src/FishyFlip.Tests/FishyFlip.Tests.csproj index a0eb2fb3..838e6c3e 100644 --- a/src/FishyFlip.Tests/FishyFlip.Tests.csproj +++ b/src/FishyFlip.Tests/FishyFlip.Tests.csproj @@ -1,7 +1,7 @@ - net7.0;net8.0 + net462;net7.0;net8.0 enable enable diff --git a/src/FishyFlip/ATWebSocketProtocol.cs b/src/FishyFlip/ATWebSocketProtocol.cs index 03c870f0..150d870e 100644 --- a/src/FishyFlip/ATWebSocketProtocol.cs +++ b/src/FishyFlip/ATWebSocketProtocol.cs @@ -280,6 +280,17 @@ private async Task ReceiveMessages(ClientWebSocket webSocket, CancellationToken { try { +#if NETSTANDARD + var result = + await webSocket.ReceiveAsync(new ArraySegment(receiveBuffer), token); + if (result is not { MessageType: WebSocketMessageType.Binary, EndOfMessage: true }) + { + continue; + } + + byte[] newArray = new byte[result.Count]; + Array.Copy(receiveBuffer, 0, newArray, 0, result.Count); +#else var result = await webSocket.ReceiveAsync(new Memory(receiveBuffer), token); if (result is not { MessageType: WebSocketMessageType.Binary, EndOfMessage: true }) @@ -289,6 +300,7 @@ private async Task ReceiveMessages(ClientWebSocket webSocket, CancellationToken byte[] newArray = new byte[result.Count]; Array.Copy(receiveBuffer, 0, newArray, 0, result.Count); +#endif Task.Run(() => this.HandleMessage(newArray)).FireAndForgetSafeAsync(this.logger); } diff --git a/src/FishyFlip/FishyFlip.csproj b/src/FishyFlip/FishyFlip.csproj index 4cb0a362..0441646c 100644 --- a/src/FishyFlip/FishyFlip.csproj +++ b/src/FishyFlip/FishyFlip.csproj @@ -1,7 +1,7 @@ - + - net7.0;net8.0 + netstandard2.0;netstandard2.1;net7.0;net8.0 enable enable true @@ -16,4 +16,9 @@ + + + + + diff --git a/src/FishyFlip/Globals.cs b/src/FishyFlip/Globals.cs index 4d6f37c1..8c2d68f9 100644 --- a/src/FishyFlip/Globals.cs +++ b/src/FishyFlip/Globals.cs @@ -22,3 +22,16 @@ global using Microsoft.IdentityModel.Tokens; global using PeterO.Cbor; global using ATCid = System.String; + +#if NETSTANDARD +namespace System.Runtime.CompilerServices +{ +#pragma warning disable SA1600 // Elements should be documented +#pragma warning disable SA1649 // File name should match first type name + internal static class IsExternalInit + { + } +#pragma warning restore SA1649 // File name should match first type name +#pragma warning restore SA1600 // Elements should be documented +} +#endif \ No newline at end of file diff --git a/src/FishyFlip/Tools/ExceptionExtensions.cs b/src/FishyFlip/Tools/ExceptionExtensions.cs index b2eab5d3..8b5eaf44 100644 --- a/src/FishyFlip/Tools/ExceptionExtensions.cs +++ b/src/FishyFlip/Tools/ExceptionExtensions.cs @@ -18,7 +18,11 @@ internal static class ExceptionExtensions /// Thrown when the provided object is null. internal static T ThrowIfNull(this T? t) { - ArgumentNullException.ThrowIfNull(t); + if (t == null) + { + throw new ArgumentNullException(nameof(t)); + } + return t; } } diff --git a/src/FishyFlip/Tools/HandleValidator.cs b/src/FishyFlip/Tools/HandleValidator.cs index 91f5284f..1a412fa2 100644 --- a/src/FishyFlip/Tools/HandleValidator.cs +++ b/src/FishyFlip/Tools/HandleValidator.cs @@ -52,7 +52,7 @@ internal static bool EnsureValidHandle(string handle, ILogger? logger = default) return false; } - if (l.EndsWith('-') || l.StartsWith('-')) + if (l.EndsWith("-") || l.StartsWith("-")) { logger?.LogError("Handle parts can not start or end with hyphens"); return false; diff --git a/src/FishyFlip/Tools/HttpClientExtensions.cs b/src/FishyFlip/Tools/HttpClientExtensions.cs index 00e7921c..0450f016 100644 --- a/src/FishyFlip/Tools/HttpClientExtensions.cs +++ b/src/FishyFlip/Tools/HttpClientExtensions.cs @@ -45,7 +45,11 @@ internal static async Task> Post( return atError!; } +#if NETSTANDARD + string response = await message.Content.ReadAsStringAsync(); +#else string response = await message.Content.ReadAsStringAsync(cancellationToken); +#endif if (response.IsNullOrEmpty() && message.IsSuccessStatusCode) { response = "{ }"; @@ -85,7 +89,11 @@ internal static async Task> Post( return atError!; } +#if NETSTANDARD + string response = await message.Content.ReadAsStringAsync(); +#else string response = await message.Content.ReadAsStringAsync(cancellationToken); +#endif if (response.IsNullOrEmpty() && message.IsSuccessStatusCode) { response = "{ }"; @@ -123,7 +131,11 @@ internal static async Task> Post( return atError!; } +#if NETSTANDARD + string response = await message.Content.ReadAsStringAsync(); +#else string response = await message.Content.ReadAsStringAsync(cancellationToken); +#endif if (response.IsNullOrEmpty() && message.IsSuccessStatusCode) { response = "{ }"; @@ -158,8 +170,13 @@ internal static async Task> Post( return atError!; } +#if NETSTANDARD + var blob = await message.Content.ReadAsByteArrayAsync(); + string response = await message.Content.ReadAsStringAsync(); +#else var blob = await message.Content.ReadAsByteArrayAsync(cancellationToken); string response = await message.Content.ReadAsStringAsync(cancellationToken); +#endif logger?.LogDebug($"GET BLOB {url}: {response}"); return new Blob(blob); @@ -191,7 +208,11 @@ internal static async Task> Post( return atError!; } +#if NETSTANDARD + using var stream = await message.Content.ReadAsStreamAsync(); +#else await using var stream = await message.Content.ReadAsStreamAsync(cancellationToken); +#endif await CarDecoder.DecodeCarAsync(stream, progress); return new Success(); } @@ -226,11 +247,17 @@ internal static async Task> Post( } var fileDownload = Path.Combine(filePath, StringExtensions.GenerateValidFilename(fileName)); - await using (var content = File.Create(fileDownload)) +#if NETSTANDARD + using var content = File.Create(fileDownload); + using var stream = await message.Content.ReadAsStreamAsync(); + await stream.CopyToAsync(content); +#else + await using (FileStream content = File.Create(fileDownload)) { await using var stream = await message.Content.ReadAsStreamAsync(cancellationToken); await stream.CopyToAsync(content, cancellationToken); } +#endif return new Success(); } @@ -262,7 +289,11 @@ internal static async Task> Post( return atError!; } +#if NETSTANDARD + string response = await message.Content.ReadAsStringAsync(); +#else string response = await message.Content.ReadAsStringAsync(cancellationToken); +#endif if (response.IsNullOrEmpty() && message.IsSuccessStatusCode) { response = "{ }"; @@ -274,7 +305,11 @@ internal static async Task> Post( private static async Task CreateError(HttpResponseMessage message, JsonSerializerOptions options, CancellationToken cancellationToken, ILogger? logger = default) { +#if NETSTANDARD + string response = await message.Content.ReadAsStringAsync(); +#else string response = await message.Content.ReadAsStringAsync(cancellationToken); +#endif ATError atError; ErrorDetail? detail = default; if (string.IsNullOrEmpty(response)) diff --git a/src/FishyFlip/Tools/StreamExtensions.cs b/src/FishyFlip/Tools/StreamExtensions.cs new file mode 100644 index 00000000..c5301b3b --- /dev/null +++ b/src/FishyFlip/Tools/StreamExtensions.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FishyFlip.Tools; + +/// +/// Stream Extensions. +/// +public static class StreamExtensions +{ +#if NETSTANDARD + /// + /// Asynchronously reads exactly the specified number of bytes from the stream into the buffer. + /// + /// The stream to read from. + /// The buffer to store the read bytes. + /// The zero-based byte offset in the buffer at which to begin storing the data read from the stream. + /// The maximum number of bytes to read. + /// A task representing the asynchronous operation. + public static async Task ReadExactlyAsync(this Stream stream, byte[] buffer, int offset, int count) + { + int totalRead = 0; + while (totalRead < count) + { + int bytesRead = await stream.ReadAsync(buffer, offset + totalRead, count - totalRead); + if (bytesRead == 0) + { + throw new EndOfStreamException("End of stream reached before fulfilling read request."); + } + + totalRead += bytesRead; + } + } +#endif +} diff --git a/src/FishyFlip/Tools/SystemImplementations.cs b/src/FishyFlip/Tools/SystemImplementations.cs new file mode 100644 index 00000000..03e89ee6 --- /dev/null +++ b/src/FishyFlip/Tools/SystemImplementations.cs @@ -0,0 +1,265 @@ +// +// Copyright (c) Drastic Actions. All rights reserved. +// + +#pragma warning disable SA1403 // File may only contain a single namespace +#pragma warning disable SA1600 // Elements should be documented +#pragma warning disable SA1649 // File name should match first type name +#pragma warning disable SA1309 // Field names should not begin with underscore +#pragma warning disable SA1101 // Prefix local calls with this +#pragma warning disable SA1503 // Braces should not be omitted +#pragma warning disable SA1623 // Property summary documentation should match accessors +#pragma warning disable SA1615 // Element return value should be documented +#pragma warning disable SA1201 // Elements should appear in the correct order +#pragma warning disable SA1611 // Element parameters should be documented +#pragma warning disable SA1204 // Static elements should appear before instance elements + +#if NETSTANDARD2_0 +using System.Runtime.CompilerServices; + +namespace System +{ + internal readonly struct Index : IEquatable + { + private readonly int _value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + private Index(int value) + { + _value = value; + } + + /// Create an Index pointing at first element. + public static Index Start => new Index(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new Index(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + return new Index(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative"); + } + + return new Index(~value); + } + + /// Returns the index value. + public int Value + { + get + { + if (_value < 0) + { + return ~_value; + } + else + { + return _value; + } + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + var offset = _value; + if (IsFromEnd) + { + offset += length + 1; + } + + return offset; + } + + public override bool Equals(object? value) => value is Index && _value == ((Index)value)._value; + + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + return "^" + ((uint)Value).ToString(); + + return ((uint)Value).ToString(); + } + } + + /// Represent a range has start and end indexes. + /// + /// Range is used by the C# compiler to support the range syntax. + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; + /// int[] subArray1 = someArray[0..2]; // { 1, 2 } + /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } + /// + /// + internal readonly struct Range : IEquatable + { + /// Represent the inclusive start index of the Range. + public Index Start { get; } + + /// Represent the exclusive end index of the Range. + public Index End { get; } + + public Range(Index start, Index end) + { + Start = start; + End = end; + } + + /// Indicates whether the current Range object is equal to another object of the same type. + /// An object to compare with this object. + public override bool Equals(object? value) => + value is Range r && + r.Start.Equals(Start) && + r.End.Equals(End); + + /// Indicates whether the current Range object is equal to another Range object. + /// An object to compare with this object. + public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); + + /// Returns the hash code for this instance. + public override int GetHashCode() + { + return (Start.GetHashCode() * 31) + End.GetHashCode(); + } + + /// Converts the value of the current Range object to its equivalent string representation. + public override string ToString() + { + return Start + ".." + End; + } + + /// Create a Range object starting from start index to the end of the collection. + public static Range StartAt(Index start) => new Range(start, Index.End); + + /// Create a Range object starting from first element in the collection to the end Index. + public static Range EndAt(Index end) => new Range(Index.Start, end); + + /// Create a Range object starting from first element to the end. + public static Range All => new Range(Index.Start, Index.End); + + /// Calculate the start offset and length of range object using a collection length. + /// The length of the collection that the range will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter against negative values. + /// It is expected Range will be used with collections which always have non negative length/count. + /// We validate the range is inside the length scope though. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public (int Offset, int Length) GetOffsetAndLength(int length) + { + int start; + var startIndex = Start; + if (startIndex.IsFromEnd) + start = length - startIndex.Value; + else + start = startIndex.Value; + + int end; + var endIndex = End; + if (endIndex.IsFromEnd) + end = length - endIndex.Value; + else + end = endIndex.Value; + + if ((uint)end > (uint)length || (uint)start > (uint)end) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return (start, end - start); + } + } +} + +namespace System.Runtime.CompilerServices +{ + internal static class RuntimeHelpers + { + /// + /// Slices the specified array using the specified range. + /// +#pragma warning disable SA1618 // Generic type parameters should be documented + public static T[] GetSubArray(T[] array, Range range) +#pragma warning restore SA1618 // Generic type parameters should be documented + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + (int offset, int length) = range.GetOffsetAndLength(array.Length); + + if (default(T) != null || typeof(T[]) == array.GetType()) + { + if (length == 0) + { + return Array.Empty(); + } + + var dest = new T[length]; + Array.Copy(array, offset, dest, 0, length); + return dest; + } + else + { + // The array is actually a U[] where U:T. + var dest = (T[])Array.CreateInstance(array.GetType().GetElementType(), length); + Array.Copy(array, offset, dest, 0, length); + return dest; + } + } + } +} +#endif \ No newline at end of file