Browse Source

Better tests for stream synchronization

pull/2301/head
James Jackson-South 4 years ago
parent
commit
28243df8a4
  1. 17
      src/ImageSharp/Formats/ImageDecoder.cs
  2. 27
      tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs
  3. 64
      tests/ImageSharp.Tests/Image/NonSeekableStream.cs

17
src/ImageSharp/Formats/ImageDecoder.cs

@ -153,12 +153,13 @@ public abstract class ImageDecoder : IImageDecoder
throw new NotSupportedException("Cannot read from the stream."); throw new NotSupportedException("Cannot read from the stream.");
} }
T PeformActionAndResetPosittion(Stream s, long position) T PeformActionAndResetPosition(Stream s, long position)
{ {
T result = action(s); T result = action(s);
// Issue #2259. Our buffered reads may have left the stream in an incorrect non-zero position. // Issue #2259. Our buffered reads may have left the stream in an incorrect non-zero position.
// Reset the position of the seekable stream if we did not read to the end to allow additional reads. // Reset the position of the seekable stream if we did not read to the end to allow additional reads.
// The stream is always seekable in this scenario.
if (stream.Position != s.Position && s.Position != s.Length) if (stream.Position != s.Position && s.Position != s.Length)
{ {
stream.Position = position + s.Position; stream.Position = position + s.Position;
@ -169,7 +170,7 @@ public abstract class ImageDecoder : IImageDecoder
if (stream.CanSeek) if (stream.CanSeek)
{ {
return PeformActionAndResetPosittion(stream, stream.Position); return PeformActionAndResetPosition(stream, stream.Position);
} }
Configuration configuration = options.Configuration; Configuration configuration = options.Configuration;
@ -177,7 +178,7 @@ public abstract class ImageDecoder : IImageDecoder
stream.CopyTo(memoryStream, configuration.StreamProcessingBufferSize); stream.CopyTo(memoryStream, configuration.StreamProcessingBufferSize);
memoryStream.Position = 0; memoryStream.Position = 0;
return PeformActionAndResetPosittion(memoryStream, 0); return action(memoryStream);
} }
internal static async Task<T> WithSeekableStreamAsync<T>( internal static async Task<T> WithSeekableStreamAsync<T>(
@ -200,14 +201,15 @@ public abstract class ImageDecoder : IImageDecoder
// Issue #2259. Our buffered reads may have left the stream in an incorrect non-zero position. // Issue #2259. Our buffered reads may have left the stream in an incorrect non-zero position.
// Reset the position of the seekable stream if we did not read to the end to allow additional reads. // Reset the position of the seekable stream if we did not read to the end to allow additional reads.
if (stream.Position != s.Position && s.Position != s.Length) // We check here that the input stream is seekable because it is not guaranteed to be so since
// we always copy input streams of unknown type.
if (stream.CanSeek && stream.Position != s.Position && s.Position != s.Length)
{ {
stream.Position = position + s.Position; stream.Position = position + s.Position;
} }
// TODO: This is a hack. Our decoders do not check for cancellation requests. // TODO: This is a hack. Our decoders do not check for cancellation requests.
// We need to fix this properly by implemented each decoder or allowing passing a token to BufferedReadStream // We need to fix this properly by implemented each decoder.
// which can do the check.
if (ct.IsCancellationRequested) if (ct.IsCancellationRequested)
{ {
throw new TaskCanceledException(); throw new TaskCanceledException();
@ -229,10 +231,11 @@ public abstract class ImageDecoder : IImageDecoder
return PeformActionAndResetPosition(cms, cms.Position, cancellationToken); return PeformActionAndResetPosition(cms, cms.Position, cancellationToken);
} }
long position = stream.CanSeek ? stream.Position : 0;
Configuration configuration = options.Configuration; Configuration configuration = options.Configuration;
using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator); using ChunkedMemoryStream memoryStream = new(configuration.MemoryAllocator);
await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false);
memoryStream.Position = 0; memoryStream.Position = 0;
return PeformActionAndResetPosition(memoryStream, 0, cancellationToken); return PeformActionAndResetPosition(memoryStream, position, cancellationToken);
} }
} }

27
tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs

@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
using SixLabors.ImageSharp.Tests.TestUtilities;
namespace SixLabors.ImageSharp.Tests.Formats; namespace SixLabors.ImageSharp.Tests.Formats;
@ -47,7 +48,7 @@ public class GeneralFormatTests
} }
[Fact] [Fact]
public void ReadOriginIsRespectedOnLoad() public void ChainedReadOriginIsRespectedForSeekableStreamsOnLoad()
{ {
using FileStream stream = File.OpenRead(TestFile.GetInputFileFullPath(TestImages.Png.Issue2259)); using FileStream stream = File.OpenRead(TestFile.GetInputFileFullPath(TestImages.Png.Issue2259));
using Image<Rgb24> i = Image.Load<Rgb24>(stream); using Image<Rgb24> i = Image.Load<Rgb24>(stream);
@ -62,7 +63,18 @@ public class GeneralFormatTests
} }
[Fact] [Fact]
public async Task ReadOriginIsRespectedOnLoadAsync() public void ChainedReadOnLoadNonSeekable_ThrowsUnknownImageFormatException()
{
using FileStream stream = File.OpenRead(TestFile.GetInputFileFullPath(TestImages.Png.Issue2259));
using NonSeekableStream wrapper = new(stream);
using Image<Rgb24> i = Image.Load<Rgb24>(wrapper);
Assert.Equal(stream.Length, stream.Position);
Assert.Throws<UnknownImageFormatException>(() => { using Image<Rgb24> j = Image.Load<Rgb24>(wrapper); });
}
[Fact]
public async Task ChainedReadOriginIsRespectedForSeekableStreamsOnLoadAsync()
{ {
using FileStream stream = File.OpenRead(TestFile.GetInputFileFullPath(TestImages.Png.Issue2259)); using FileStream stream = File.OpenRead(TestFile.GetInputFileFullPath(TestImages.Png.Issue2259));
using Image<Rgb24> i = await Image.LoadAsync<Rgb24>(stream); using Image<Rgb24> i = await Image.LoadAsync<Rgb24>(stream);
@ -76,6 +88,17 @@ public class GeneralFormatTests
Assert.NotEqual(i[5, 5], j[5, 5]); Assert.NotEqual(i[5, 5], j[5, 5]);
} }
[Fact]
public async Task ChainedReadOnLoadNonSeekable_ThrowsUnknownImageFormatException_Async()
{
using FileStream stream = File.OpenRead(TestFile.GetInputFileFullPath(TestImages.Png.Issue2259));
using NonSeekableStream wrapper = new(stream);
using Image<Rgb24> i = await Image.LoadAsync<Rgb24>(wrapper);
Assert.Equal(stream.Length, stream.Position);
await Assert.ThrowsAsync<UnknownImageFormatException>(async () => { using Image<Rgb24> j = await Image.LoadAsync<Rgb24>(wrapper); });
}
[Fact] [Fact]
public void ImageCanEncodeToString() public void ImageCanEncodeToString()
{ {

64
tests/ImageSharp.Tests/Image/NonSeekableStream.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Tests; namespace SixLabors.ImageSharp.Tests;
@ -16,17 +16,73 @@ internal class NonSeekableStream : Stream
public override bool CanWrite => false; public override bool CanWrite => false;
public override bool CanTimeout => this.dataStream.CanTimeout;
public override long Length => throw new NotSupportedException(); public override long Length => throw new NotSupportedException();
public override long Position public override long Position
{ {
get { throw new NotSupportedException(); } get => throw new NotSupportedException();
set { throw new NotSupportedException(); } set => throw new NotSupportedException();
}
public override int ReadTimeout
{
get => this.dataStream.ReadTimeout;
set => this.dataStream.ReadTimeout = value;
}
public override int WriteTimeout
{
get => this.dataStream.WriteTimeout;
set => this.dataStream.WriteTimeout = value;
} }
public override void Flush() => this.dataStream.Flush(); public override void Flush() => this.dataStream.Flush();
public override int Read(byte[] buffer, int offset, int count) => this.dataStream.Read(buffer, offset, count); public override int ReadByte() => this.dataStream.ReadByte();
public override int Read(byte[] buffer, int offset, int count)
=> this.dataStream.Read(buffer, offset, count);
public override int Read(Span<byte> buffer)
=> this.dataStream.Read(buffer);
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
=> this.dataStream.BeginRead(buffer, offset, count, callback, state);
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> this.dataStream.ReadAsync(buffer, offset, count, cancellationToken);
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
=> this.dataStream.ReadAsync(buffer, cancellationToken);
public override int EndRead(IAsyncResult asyncResult)
=> this.dataStream.EndRead(asyncResult);
public override void WriteByte(byte value) => this.dataStream.WriteByte(value);
public override void Write(ReadOnlySpan<byte> buffer) => this.dataStream.Write(buffer);
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
=> this.dataStream.BeginWrite(buffer, offset, count, callback, state);
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> this.dataStream.WriteAsync(buffer, offset, count, cancellationToken);
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
=> this.dataStream.WriteAsync(buffer, cancellationToken);
public override void EndWrite(IAsyncResult asyncResult) => this.dataStream.EndWrite(asyncResult);
public override void CopyTo(Stream destination, int bufferSize) => this.dataStream.CopyTo(destination, bufferSize);
public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
=> this.dataStream.CopyToAsync(destination, bufferSize, cancellationToken);
public override Task FlushAsync(CancellationToken cancellationToken) => this.dataStream.FlushAsync(cancellationToken);
public override void Close() => this.dataStream.Close();
public override long Seek(long offset, SeekOrigin origin) public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException(); => throw new NotSupportedException();

Loading…
Cancel
Save