Skip to content

Commit d76fe6f

Browse files
PBM decoder robustness improvements and BufferedReadStream observability (#2551)
* handle premature EOF in the PBM decoder * BufferedReadStreamExtensions: remove the 'Try' prefix * count EOF hits in BufferedReadStream * use EofHitCounter in pbm tests * Naming convention tweaks --------- Co-authored-by: James Jackson-South <[email protected]>
1 parent b8e8b86 commit d76fe6f

File tree

11 files changed

+253
-49
lines changed

11 files changed

+253
-49
lines changed

src/ImageSharp/Formats/ImageDecoderUtilities.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ internal static Image<TPixel> Decode<TPixel>(
5050
CancellationToken cancellationToken)
5151
where TPixel : unmanaged, IPixel<TPixel>
5252
{
53-
using BufferedReadStream bufferedReadStream = new(configuration, stream, cancellationToken);
53+
// Test may pass a BufferedReadStream in order to monitor EOF hits, if so, use the existing instance.
54+
BufferedReadStream bufferedReadStream = stream as BufferedReadStream ?? new BufferedReadStream(configuration, stream, cancellationToken);
5455

5556
try
5657
{
@@ -64,6 +65,13 @@ internal static Image<TPixel> Decode<TPixel>(
6465
{
6566
throw;
6667
}
68+
finally
69+
{
70+
if (bufferedReadStream != stream)
71+
{
72+
bufferedReadStream.Dispose();
73+
}
74+
}
6775
}
6876

6977
private static InvalidImageContentException DefaultLargeImageExceptionFactory(

src/ImageSharp/Formats/Pbm/BinaryDecoder.cs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@ private static void ProcessGrayscale<TPixel>(Configuration configuration, Buffer
7171

7272
for (int y = 0; y < height; y++)
7373
{
74-
stream.Read(rowSpan);
74+
if (stream.Read(rowSpan) == 0)
75+
{
76+
return;
77+
}
78+
7579
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
7680
PixelOperations<TPixel>.Instance.FromL8Bytes(
7781
configuration,
@@ -93,7 +97,11 @@ private static void ProcessWideGrayscale<TPixel>(Configuration configuration, Bu
9397

9498
for (int y = 0; y < height; y++)
9599
{
96-
stream.Read(rowSpan);
100+
if (stream.Read(rowSpan) == 0)
101+
{
102+
return;
103+
}
104+
97105
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
98106
PixelOperations<TPixel>.Instance.FromL16Bytes(
99107
configuration,
@@ -115,7 +123,11 @@ private static void ProcessRgb<TPixel>(Configuration configuration, Buffer2D<TPi
115123

116124
for (int y = 0; y < height; y++)
117125
{
118-
stream.Read(rowSpan);
126+
if (stream.Read(rowSpan) == 0)
127+
{
128+
return;
129+
}
130+
119131
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
120132
PixelOperations<TPixel>.Instance.FromRgb24Bytes(
121133
configuration,
@@ -137,7 +149,11 @@ private static void ProcessWideRgb<TPixel>(Configuration configuration, Buffer2D
137149

138150
for (int y = 0; y < height; y++)
139151
{
140-
stream.Read(rowSpan);
152+
if (stream.Read(rowSpan) == 0)
153+
{
154+
return;
155+
}
156+
141157
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
142158
PixelOperations<TPixel>.Instance.FromRgb48Bytes(
143159
configuration,
@@ -161,6 +177,11 @@ private static void ProcessBlackAndWhite<TPixel>(Configuration configuration, Bu
161177
for (int x = 0; x < width;)
162178
{
163179
int raw = stream.ReadByte();
180+
if (raw < 0)
181+
{
182+
return;
183+
}
184+
164185
int stopBit = Math.Min(8, width - x);
165186
for (int bit = 0; bit < stopBit; bit++)
166187
{

src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,20 @@ namespace SixLabors.ImageSharp.Formats.Pbm;
1111
internal static class BufferedReadStreamExtensions
1212
{
1313
/// <summary>
14-
/// Skip over any whitespace or any comments.
14+
/// Skip over any whitespace or any comments and signal if EOF has been reached.
1515
/// </summary>
16-
public static void SkipWhitespaceAndComments(this BufferedReadStream stream)
16+
/// <param name="stream">The buffered read stream.</param>
17+
/// <returns><see langword="false"/> if EOF has been reached while reading the stream; see langword="true"/> otherwise.</returns>
18+
public static bool TrySkipWhitespaceAndComments(this BufferedReadStream stream)
1719
{
1820
bool isWhitespace;
1921
do
2022
{
2123
int val = stream.ReadByte();
24+
if (val < 0)
25+
{
26+
return false;
27+
}
2228

2329
// Comments start with '#' and end at the next new-line.
2430
if (val == 0x23)
@@ -27,8 +33,12 @@ public static void SkipWhitespaceAndComments(this BufferedReadStream stream)
2733
do
2834
{
2935
innerValue = stream.ReadByte();
36+
if (innerValue < 0)
37+
{
38+
return false;
39+
}
3040
}
31-
while (innerValue is not 0x0a and not -0x1);
41+
while (innerValue is not 0x0a);
3242

3343
// Continue searching for whitespace.
3444
val = innerValue;
@@ -38,18 +48,31 @@ public static void SkipWhitespaceAndComments(this BufferedReadStream stream)
3848
}
3949
while (isWhitespace);
4050
stream.Seek(-1, SeekOrigin.Current);
51+
return true;
4152
}
4253

4354
/// <summary>
44-
/// Read a decimal text value.
55+
/// Read a decimal text value and signal if EOF has been reached.
4556
/// </summary>
46-
/// <returns>The integer value of the decimal.</returns>
47-
public static int ReadDecimal(this BufferedReadStream stream)
57+
/// <param name="stream">The buffered read stream.</param>
58+
/// <param name="value">The read value.</param>
59+
/// <returns><see langword="false"/> if EOF has been reached while reading the stream; <see langword="true"/> otherwise.</returns>
60+
/// <remarks>
61+
/// A 'false' return value doesn't mean that the parsing has been failed, since it's possible to reach EOF while reading the last decimal in the file.
62+
/// It's up to the call site to handle such a situation.
63+
/// </remarks>
64+
public static bool TryReadDecimal(this BufferedReadStream stream, out int value)
4865
{
49-
int value = 0;
66+
value = 0;
5067
while (true)
5168
{
52-
int current = stream.ReadByte() - 0x30;
69+
int current = stream.ReadByte();
70+
if (current < 0)
71+
{
72+
return false;
73+
}
74+
75+
current -= 0x30;
5376
if ((uint)current > 9)
5477
{
5578
break;
@@ -58,6 +81,6 @@ public static int ReadDecimal(this BufferedReadStream stream)
5881
value = (value * 10) + current;
5982
}
6083

61-
return value;
84+
return true;
6285
}
6386
}

src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4+
using System.Diagnostics.CodeAnalysis;
45
using SixLabors.ImageSharp.IO;
56
using SixLabors.ImageSharp.Memory;
67
using SixLabors.ImageSharp.Metadata;
@@ -95,6 +96,7 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
9596
/// Processes the ppm header.
9697
/// </summary>
9798
/// <param name="stream">The input stream.</param>
99+
/// <exception cref="InvalidImageContentException">An EOF marker has been read before the image has been decoded.</exception>
98100
private void ProcessHeader(BufferedReadStream stream)
99101
{
100102
Span<byte> buffer = stackalloc byte[2];
@@ -144,14 +146,22 @@ private void ProcessHeader(BufferedReadStream stream)
144146
throw new InvalidImageContentException("Unknown of not implemented image type encountered.");
145147
}
146148

147-
stream.SkipWhitespaceAndComments();
148-
int width = stream.ReadDecimal();
149-
stream.SkipWhitespaceAndComments();
150-
int height = stream.ReadDecimal();
151-
stream.SkipWhitespaceAndComments();
149+
if (!stream.TrySkipWhitespaceAndComments() ||
150+
!stream.TryReadDecimal(out int width) ||
151+
!stream.TrySkipWhitespaceAndComments() ||
152+
!stream.TryReadDecimal(out int height) ||
153+
!stream.TrySkipWhitespaceAndComments())
154+
{
155+
ThrowPrematureEof();
156+
}
157+
152158
if (this.colorType != PbmColorType.BlackAndWhite)
153159
{
154-
this.maxPixelValue = stream.ReadDecimal();
160+
if (!stream.TryReadDecimal(out this.maxPixelValue))
161+
{
162+
ThrowPrematureEof();
163+
}
164+
155165
if (this.maxPixelValue > 255)
156166
{
157167
this.componentType = PbmComponentType.Short;
@@ -161,7 +171,7 @@ private void ProcessHeader(BufferedReadStream stream)
161171
this.componentType = PbmComponentType.Byte;
162172
}
163173

164-
stream.SkipWhitespaceAndComments();
174+
stream.TrySkipWhitespaceAndComments();
165175
}
166176
else
167177
{
@@ -174,6 +184,9 @@ private void ProcessHeader(BufferedReadStream stream)
174184
meta.Encoding = this.encoding;
175185
meta.ColorType = this.colorType;
176186
meta.ComponentType = this.componentType;
187+
188+
[DoesNotReturn]
189+
static void ThrowPrematureEof() => throw new InvalidImageContentException("Reached EOF while reading the header.");
177190
}
178191

179192
private void ProcessPixels<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels)

0 commit comments

Comments
 (0)