< Summary - Jellyfin

Information
Class: Jellyfin.Extensions.StreamExtensions
Assembly: Jellyfin.Extensions
File(s): /srv/git/jellyfin/src/Jellyfin.Extensions/StreamExtensions.cs
Line coverage
97%
Covered lines: 73
Uncovered lines: 2
Coverable lines: 75
Total lines: 236
Line coverage: 97.3%
Branch coverage
97%
Covered branches: 45
Total branches: 46
Branch coverage: 97.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/5/2026 - 12:13:57 AM Line coverage: 100% (4/4) Total lines: 644/19/2026 - 12:14:27 AM Line coverage: 100% (10/10) Branch coverage: 100% (4/4) Total lines: 646/1/2026 - 12:16:05 AM Line coverage: 97.3% (73/75) Branch coverage: 97.8% (45/46) Total lines: 236 4/19/2026 - 12:14:27 AM Line coverage: 100% (10/10) Branch coverage: 100% (4/4) Total lines: 646/1/2026 - 12:16:05 AM Line coverage: 97.3% (73/75) Branch coverage: 97.8% (45/46) Total lines: 236

Coverage delta

Coverage delta 3 -3

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ReadAllLines(...)100%11100%
ReadAllLines(...)100%11100%
ReadAllLines()100%22100%
ReadAllLinesAsync()100%22100%
IsFileIdenticalAsync()100%2294.44%
IsStreamIdenticalAsync()97.5%404097.87%

File(s)

/srv/git/jellyfin/src/Jellyfin.Extensions/StreamExtensions.cs

#LineLine coverage
 1using System;
 2using System.Buffers;
 3using System.Collections.Generic;
 4using System.IO;
 5using System.Linq;
 6using System.Runtime.CompilerServices;
 7using System.Text;
 8using System.Threading;
 9using System.Threading.Tasks;
 10
 11namespace Jellyfin.Extensions
 12{
 13    /// <summary>
 14    /// Extension methods for the <see cref="Stream"/> class.
 15    /// </summary>
 16    public static class StreamExtensions
 17    {
 18        private const int StreamComparisonBufferSize = 81920;
 19
 20        /// <summary>
 21        /// Reads all lines in the <see cref="Stream" />.
 22        /// </summary>
 23        /// <param name="stream">The <see cref="Stream" /> to read from.</param>
 24        /// <returns>All lines in the stream.</returns>
 25        public static string[] ReadAllLines(this Stream stream)
 526            => ReadAllLines(stream, Encoding.UTF8);
 27
 28        /// <summary>
 29        /// Reads all lines in the <see cref="Stream" />.
 30        /// </summary>
 31        /// <param name="stream">The <see cref="Stream" /> to read from.</param>
 32        /// <param name="encoding">The character encoding to use.</param>
 33        /// <returns>All lines in the stream.</returns>
 34        public static string[] ReadAllLines(this Stream stream, Encoding encoding)
 35        {
 536            using StreamReader reader = new StreamReader(stream, encoding);
 537            return ReadAllLines(reader).ToArray();
 538        }
 39
 40        /// <summary>
 41        /// Reads all lines in the <see cref="TextReader" />.
 42        /// </summary>
 43        /// <param name="reader">The <see cref="TextReader" /> to read from.</param>
 44        /// <returns>All lines in the stream.</returns>
 45        public static IEnumerable<string> ReadAllLines(this TextReader reader)
 46        {
 47            string? line;
 7248            while ((line = reader.ReadLine()) is not null)
 49            {
 6750                yield return line;
 51            }
 552        }
 53
 54        /// <summary>
 55        /// Reads all lines in the <see cref="TextReader" />.
 56        /// </summary>
 57        /// <param name="reader">The <see cref="TextReader" /> to read from.</param>
 58        /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
 59        /// <returns>All lines in the stream.</returns>
 60        public static async IAsyncEnumerable<string> ReadAllLinesAsync(this TextReader reader, [EnumeratorCancellation] 
 61        {
 62            string? line;
 3131163            while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) is not null)
 64            {
 3124865                yield return line;
 66            }
 6367        }
 68
 69        /// <summary>
 70        /// Determines whether a stream is identical to a file on disk.
 71        /// </summary>
 72        /// <param name="stream">The stream to compare.</param>
 73        /// <param name="path">The file path to compare against.</param>
 74        /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
 75        /// <returns>True if the stream and file are identical; otherwise false.</returns>
 76        /// <exception cref="ArgumentException"><paramref name="stream"/> does not support seeking.</exception>
 77        /// <remarks>
 78        /// The entire stream is compared against the file from the beginning (the position is reset to 0 on entry)
 79        /// and restored to its original value after the call.
 80        /// </remarks>
 81        public static async Task<bool> IsFileIdenticalAsync(this Stream stream, string path, CancellationToken cancellat
 82        {
 583            ArgumentNullException.ThrowIfNull(stream);
 584            ArgumentException.ThrowIfNullOrEmpty(path);
 85
 586            if (!stream.CanSeek)
 87            {
 188                throw new ArgumentException("Stream must support seeking.", nameof(stream));
 89            }
 90
 491            var originalPosition = stream.Position;
 92            try
 93            {
 494                stream.Position = 0;
 95
 496                var existingFileStream = new FileStream(
 497                    path,
 498                    FileMode.Open,
 499                    FileAccess.Read,
 4100                    FileShare.Read,
 4101                    bufferSize: StreamComparisonBufferSize,
 4102                    FileOptions.Asynchronous | FileOptions.SequentialScan);
 4103                await using (existingFileStream.ConfigureAwait(false))
 104                {
 4105                    return await stream.IsStreamIdenticalAsync(existingFileStream, cancellationToken).ConfigureAwait(fal
 106                }
 0107            }
 108            finally
 109            {
 4110                stream.Position = originalPosition;
 111            }
 4112        }
 113
 114        /// <summary>
 115        /// Determines whether two streams are identical.
 116        /// </summary>
 117        /// <param name="a">The first stream to compare.</param>
 118        /// <param name="b">The second stream to compare.</param>
 119        /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
 120        /// <returns>True if the streams are identical; otherwise false.</returns>
 121        /// <remarks>
 122        /// Seekable streams are compared from the beginning (their position is reset to 0 on entry).
 123        /// Non-seekable streams are compared from their current read position. Stream positions are not
 124        /// restored after the call.
 125        /// </remarks>
 126        public static async Task<bool> IsStreamIdenticalAsync(this Stream a, Stream b, CancellationToken cancellationTok
 127        {
 16128            ArgumentNullException.ThrowIfNull(a);
 16129            ArgumentNullException.ThrowIfNull(b);
 130
 16131            if (ReferenceEquals(a, b))
 132            {
 0133                return true;
 134            }
 135
 16136            if (a.CanSeek is var aCanSeek && aCanSeek)
 137            {
 12138                a.Position = 0;
 139            }
 140
 16141            if (b.CanSeek is var bCanSeek && bCanSeek)
 142            {
 12143                b.Position = 0;
 144            }
 145
 16146            if (aCanSeek && bCanSeek && b.Length != a.Length)
 147            {
 1148                return false;
 149            }
 150
 151            // MemoryStreams only unlock a fast path if their underlying buffer is exposed via TryGetBuffer.
 15152            var segmentA = a is MemoryStream streamA && streamA.TryGetBuffer(out var bufA) ? bufA : default;
 15153            var segmentB = b is MemoryStream streamB && streamB.TryGetBuffer(out var bufB) ? bufB : default;
 154
 155            // Fast path A: both streams expose buffers, compare segments directly
 15156            if (segmentA.Array is not null && segmentB.Array is not null)
 157            {
 1158                return segmentA.AsSpan().SequenceEqual(segmentB.AsSpan());
 159            }
 160
 14161            if (segmentB.Array is not null) // && segmentA.Array is null guaranteed by previous check
 162            {
 163                // swap so that segmentA is the non-null one, compared to b we need only one fast path B
 1164                (segmentA, b) = (segmentB, a);
 165            }
 166
 14167            if (segmentA.Array is not null) // either a was non-null, or b was non-null and was swapped there
 168            {
 169                // Fast path B: only one stream exposed a buffer, compare against the other chunk-by-chunk
 4170                var bufferB = ArrayPool<byte>.Shared.Rent(StreamComparisonBufferSize);
 171                try
 172                {
 4173                    var memoryB = bufferB.AsMemory();
 4174                    int offset = 0;
 175                    int bytesRead;
 7176                    while ((bytesRead = await b.ReadAtLeastAsync(memoryB, memoryB.Length, throwOnEndOfStream: false, can
 177                    {
 4178                        if (offset + bytesRead > segmentA.Count || !segmentA.AsSpan(offset, bytesRead).SequenceEqual(mem
 179                        {
 1180                            return false;
 181                        }
 182
 3183                        offset += bytesRead;
 184                    }
 185
 3186                    return offset == segmentA.Count;
 187                }
 188                finally
 189                {
 4190                    ArrayPool<byte>.Shared.Return(bufferB);
 191                }
 192            }
 193            else
 194            {
 10195                var bufferA = ArrayPool<byte>.Shared.Rent(StreamComparisonBufferSize);
 10196                var bufferB = ArrayPool<byte>.Shared.Rent(StreamComparisonBufferSize);
 197                try
 198                {
 10199                    var memoryA = bufferA.AsMemory();
 10200                    var memoryB = bufferB.AsMemory();
 7201                    while (true)
 202                    {
 17203                        cancellationToken.ThrowIfCancellationRequested();
 204
 17205                        var taskA = a.ReadAtLeastAsync(memoryA, memoryA.Length, throwOnEndOfStream: false, cancellationT
 17206                        var taskB = b.ReadAtLeastAsync(memoryB, memoryB.Length, throwOnEndOfStream: false, cancellationT
 17207                        await Task.WhenAll(taskA, taskB).ConfigureAwait(false);
 208
 17209                        var bytesReadA = await taskA.ConfigureAwait(false);
 17210                        var bytesReadB = await taskB.ConfigureAwait(false);
 211
 17212                        if (bytesReadA != bytesReadB)
 213                        {
 1214                            return false;
 215                        }
 216
 16217                        if (bytesReadA == 0)
 218                        {
 7219                            return true;
 220                        }
 221
 9222                        if (!memoryA.Span[..bytesReadA].SequenceEqual(memoryB.Span[..bytesReadB]))
 223                        {
 2224                            return false;
 225                        }
 7226                    }
 227                }
 228                finally
 229                {
 10230                    ArrayPool<byte>.Shared.Return(bufferA);
 10231                    ArrayPool<byte>.Shared.Return(bufferB);
 232                }
 233            }
 16234        }
 235    }
 236}