Repository navigation
Expand file tree
/
Copy pathStreamExtensions.cs
More file actions
123 lines (103 loc) · 4.13 KB
/
Copy pathStreamExtensions.cs
File metadata and controls
123 lines (103 loc) · 4.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
namespace Sentry.Internal.Extensions;
internal static class StreamExtensions
{
/// <summary>
/// Reads a single line from the stream.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="maxLength">
/// When supplied, the maximum number of bytes the line may occupy. Callers that know what a
/// reasonable length looks like should pass it, so that a corrupt stream containing no newline
/// fails fast instead of being buffered into memory in its entirety.
/// </param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="InvalidDataException">
/// No newline was found within <paramref name="maxLength"/> bytes.
/// </exception>
public static async Task<byte[]> ReadLineAsync(
this Stream stream,
int? maxLength = null,
CancellationToken cancellationToken = default)
{
// This approach avoids reading one byte at a time.
const int size = 128;
using var buffer = new PooledBuffer<byte>(size);
using var result = new MemoryStream(capacity: size);
var overreach = 0;
var found = false;
while (!found)
{
var bytesRead = await stream.ReadAsync(buffer.Array, 0, size, cancellationToken).ConfigureAwait(false);
if (bytesRead <= 0)
{
break;
}
for (var i = 0; i < bytesRead; i++)
{
if (buffer.Array[i] != '\n')
{
continue;
}
found = true;
overreach = bytesRead - i - 1;
bytesRead = i;
break;
}
result.Write(buffer.Array, 0, bytesRead);
if (maxLength is { } limit && result.Length > limit)
{
throw new InvalidDataException(
$"Expected a line of at most {limit} bytes but found no newline within that many.");
}
}
stream.Position -= overreach;
return result.ToArray();
}
public static async Task SkipNewlinesAsync(this Stream stream, CancellationToken cancellationToken = default)
{
// We probably have very few newline characters to skip, so reading one byte at a time is fine here.
using var buffer = new PooledBuffer<byte>(1);
while (await stream.ReadAsync(buffer.Array, 0, 1, cancellationToken).ConfigureAwait(false) > 0)
{
if (buffer.Array[0] != '\n')
{
stream.Position--;
return;
}
}
}
public static async Task<byte[]> ReadByteChunkAsync(
this Stream stream,
int expectedLength,
CancellationToken cancellationToken = default)
{
using var buffer = new PooledBuffer<byte>(expectedLength);
var bytesRead = await stream.ReadAsync(buffer.Array, 0, expectedLength, cancellationToken)
.ConfigureAwait(false);
// The buffer is rented so we can't return it, plus it may be larger than needed.
// So we copy everything to a new buffer.
var result = new byte[bytesRead];
Array.Copy(buffer.Array, result, bytesRead);
return result;
}
// pre-creating this buffer leads to an optimized path when writing
private static readonly byte[] NewlineBuffer = { (byte)'\n' };
public static Task WriteNewlineAsync(this Stream stream, CancellationToken cancellationToken = default) =>
#pragma warning disable CA1835 // the byte-array implementation of WriteAsync is more direct than using ReadOnlyMemory<byte>
stream.WriteAsync(NewlineBuffer, 0, 1, cancellationToken);
#pragma warning restore CA1835
public static void WriteNewline(this Stream stream) => stream.Write(NewlineBuffer, 0, 1);
public static long? TryGetLength(this Stream stream)
{
try
{
return stream.Length;
}
catch
{
return null;
}
}
public static bool IsFileStream(this Stream? stream) =>
stream is FileStream || stream?.GetType().Name == "MockFileStream";
}