Skip to content

Commit e74a55f

Browse files
[release/2.1] PBM decoder robustness improvements and BufferedReadStream observability (#2555)
* PBM decoder robustness improvements and BufferedReadStream observability Backport of #2551 & #2552 * Remove DoesNotReturn attribute --------- Co-authored-by: James Jackson-South <[email protected]>
1 parent 749b1c0 commit e74a55f

11 files changed

+256
-62
lines changed

src/ImageSharp/Formats/ImageDecoderUtilities.cs

+9-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ public static Image<TPixel> Decode<TPixel>(
4646
CancellationToken cancellationToken)
4747
where TPixel : unmanaged, IPixel<TPixel>
4848
{
49-
using var bufferedReadStream = new BufferedReadStream(configuration, stream);
49+
// Test may pass a BufferedReadStream in order to monitor EOF hits, if so, use the existing instance.
50+
BufferedReadStream bufferedReadStream = stream as BufferedReadStream ?? new BufferedReadStream(configuration, stream);
5051

5152
try
5253
{
@@ -56,6 +57,13 @@ public static Image<TPixel> Decode<TPixel>(
5657
{
5758
throw largeImageExceptionFactory(ex, decoder.Dimensions);
5859
}
60+
finally
61+
{
62+
if (bufferedReadStream != stream)
63+
{
64+
bufferedReadStream.Dispose();
65+
}
66+
}
5967
}
6068

6169
private static InvalidImageContentException DefaultLargeImageExceptionFactory(

src/ImageSharp/Formats/Pbm/BinaryDecoder.cs

+27-18
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ private static void ProcessGrayscale<TPixel>(Configuration configuration, Buffer
7272

7373
for (int y = 0; y < height; y++)
7474
{
75-
stream.Read(rowSpan);
75+
if (stream.Read(rowSpan) < rowSpan.Length)
76+
{
77+
return;
78+
}
79+
7680
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
7781
PixelOperations<TPixel>.Instance.FromL8Bytes(
7882
configuration,
@@ -94,7 +98,11 @@ private static void ProcessWideGrayscale<TPixel>(Configuration configuration, Bu
9498

9599
for (int y = 0; y < height; y++)
96100
{
97-
stream.Read(rowSpan);
101+
if (stream.Read(rowSpan) < rowSpan.Length)
102+
{
103+
return;
104+
}
105+
98106
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
99107
PixelOperations<TPixel>.Instance.FromL16Bytes(
100108
configuration,
@@ -116,7 +124,11 @@ private static void ProcessRgb<TPixel>(Configuration configuration, Buffer2D<TPi
116124

117125
for (int y = 0; y < height; y++)
118126
{
119-
stream.Read(rowSpan);
127+
if (stream.Read(rowSpan) < rowSpan.Length)
128+
{
129+
return;
130+
}
131+
120132
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
121133
PixelOperations<TPixel>.Instance.FromRgb24Bytes(
122134
configuration,
@@ -138,7 +150,11 @@ private static void ProcessWideRgb<TPixel>(Configuration configuration, Buffer2D
138150

139151
for (int y = 0; y < height; y++)
140152
{
141-
stream.Read(rowSpan);
153+
if (stream.Read(rowSpan) < rowSpan.Length)
154+
{
155+
return;
156+
}
157+
142158
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
143159
PixelOperations<TPixel>.Instance.FromRgb48Bytes(
144160
configuration,
@@ -153,7 +169,6 @@ private static void ProcessBlackAndWhite<TPixel>(Configuration configuration, Bu
153169
{
154170
int width = pixels.Width;
155171
int height = pixels.Height;
156-
int startBit = 0;
157172
MemoryAllocator allocator = configuration.MemoryAllocator;
158173
using IMemoryOwner<L8> row = allocator.Allocate<L8>(width);
159174
Span<L8> rowSpan = row.GetSpan();
@@ -163,23 +178,17 @@ private static void ProcessBlackAndWhite<TPixel>(Configuration configuration, Bu
163178
for (int x = 0; x < width;)
164179
{
165180
int raw = stream.ReadByte();
166-
int bit = startBit;
167-
startBit = 0;
168-
for (; bit < 8; bit++)
181+
if (raw < 0)
182+
{
183+
return;
184+
}
185+
186+
int stopBit = Math.Min(8, width - x);
187+
for (int bit = 0; bit < stopBit; bit++)
169188
{
170189
bool bitValue = (raw & (0x80 >> bit)) != 0;
171190
rowSpan[x] = bitValue ? black : white;
172191
x++;
173-
if (x == width)
174-
{
175-
startBit = (bit + 1) & 7; // Round off to below 8.
176-
if (startBit != 0)
177-
{
178-
stream.Seek(-1, System.IO.SeekOrigin.Current);
179-
}
180-
181-
break;
182-
}
183192
}
184193
}
185194

src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs

+32-9
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,20 @@ namespace SixLabors.ImageSharp.Formats.Pbm
1212
internal static class BufferedReadStreamExtensions
1313
{
1414
/// <summary>
15-
/// Skip over any whitespace or any comments.
15+
/// Skip over any whitespace or any comments and signal if EOF has been reached.
1616
/// </summary>
17-
public static void SkipWhitespaceAndComments(this BufferedReadStream stream)
17+
/// <param name="stream">The buffered read stream.</param>
18+
/// <returns><see langword="false"/> if EOF has been reached while reading the stream; see langword="true"/> otherwise.</returns>
19+
public static bool SkipWhitespaceAndComments(this BufferedReadStream stream)
1820
{
1921
bool isWhitespace;
2022
do
2123
{
2224
int val = stream.ReadByte();
25+
if (val < 0)
26+
{
27+
return false;
28+
}
2329

2430
// Comments start with '#' and end at the next new-line.
2531
if (val == 0x23)
@@ -28,8 +34,12 @@ public static void SkipWhitespaceAndComments(this BufferedReadStream stream)
2834
do
2935
{
3036
innerValue = stream.ReadByte();
37+
if (innerValue < 0)
38+
{
39+
return false;
40+
}
3141
}
32-
while (innerValue is not 0x0a and not -0x1);
42+
while (innerValue is not 0x0a);
3343

3444
// Continue searching for whitespace.
3545
val = innerValue;
@@ -39,18 +49,31 @@ public static void SkipWhitespaceAndComments(this BufferedReadStream stream)
3949
}
4050
while (isWhitespace);
4151
stream.Seek(-1, SeekOrigin.Current);
52+
return true;
4253
}
4354

4455
/// <summary>
45-
/// Read a decimal text value.
56+
/// Read a decimal text value and signal if EOF has been reached.
4657
/// </summary>
47-
/// <returns>The integer value of the decimal.</returns>
48-
public static int ReadDecimal(this BufferedReadStream stream)
58+
/// <param name="stream">The buffered read stream.</param>
59+
/// <param name="value">The read value.</param>
60+
/// <returns><see langword="false"/> if EOF has been reached while reading the stream; <see langword="true"/> otherwise.</returns>
61+
/// <remarks>
62+
/// 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.
63+
/// It's up to the call site to handle such a situation.
64+
/// </remarks>
65+
public static bool ReadDecimal(this BufferedReadStream stream, out int value)
4966
{
50-
int value = 0;
67+
value = 0;
5168
while (true)
5269
{
53-
int current = stream.ReadByte() - 0x30;
70+
int current = stream.ReadByte();
71+
if (current < 0)
72+
{
73+
return false;
74+
}
75+
76+
current -= 0x30;
5477
if ((uint)current > 9)
5578
{
5679
break;
@@ -59,7 +82,7 @@ public static int ReadDecimal(this BufferedReadStream stream)
5982
value = (value * 10) + current;
6083
}
6184

62-
return value;
85+
return true;
6386
}
6487
}
6588
}

src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs

+17-6
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella
9090
/// Processes the ppm header.
9191
/// </summary>
9292
/// <param name="stream">The input stream.</param>
93+
/// <exception cref="InvalidImageContentException">An EOF marker has been read before the image has been decoded.</exception>
9394
private void ProcessHeader(BufferedReadStream stream)
9495
{
9596
Span<byte> buffer = stackalloc byte[2];
@@ -139,14 +140,22 @@ private void ProcessHeader(BufferedReadStream stream)
139140
throw new InvalidImageContentException("Unknown of not implemented image type encountered.");
140141
}
141142

142-
stream.SkipWhitespaceAndComments();
143-
int width = stream.ReadDecimal();
144-
stream.SkipWhitespaceAndComments();
145-
int height = stream.ReadDecimal();
146-
stream.SkipWhitespaceAndComments();
143+
if (!stream.SkipWhitespaceAndComments() ||
144+
!stream.ReadDecimal(out int width) ||
145+
!stream.SkipWhitespaceAndComments() ||
146+
!stream.ReadDecimal(out int height) ||
147+
!stream.SkipWhitespaceAndComments())
148+
{
149+
ThrowPrematureEof();
150+
}
151+
147152
if (this.ColorType != PbmColorType.BlackAndWhite)
148153
{
149-
this.maxPixelValue = stream.ReadDecimal();
154+
if (!stream.ReadDecimal(out this.maxPixelValue))
155+
{
156+
ThrowPrematureEof();
157+
}
158+
150159
if (this.maxPixelValue > 255)
151160
{
152161
this.ComponentType = PbmComponentType.Short;
@@ -169,6 +178,8 @@ private void ProcessHeader(BufferedReadStream stream)
169178
meta.Encoding = this.Encoding;
170179
meta.ColorType = this.ColorType;
171180
meta.ComponentType = this.ComponentType;
181+
182+
static void ThrowPrematureEof() => throw new InvalidImageContentException("Reached EOF while reading the header.");
172183
}
173184

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

0 commit comments

Comments
 (0)