Browse Source

Fix non-seekable stream reading.

pull/1574/head
James Jackson-South 6 years ago
parent
commit
5f58bbcba5
  1. 544
      src/ImageSharp/IO/ChunkedMemoryStream.cs
  2. 5
      src/ImageSharp/Image.FromStream.cs
  3. 183
      tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs
  4. 15
      tests/ImageSharp.Tests/Image/ImageTests.Load_FromStream_PassLocalConfiguration.cs
  5. 43
      tests/ImageSharp.Tests/Image/NonSeekableStream.cs
  6. 53
      tests/ImageSharp.Tests/Image/NoneSeekableStream.cs

544
src/ImageSharp/IO/ChunkedMemoryStream.cs

@ -0,0 +1,544 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.IO
{
/// <summary>
/// Provides an in-memory stream composed of non-contiguous chunks that doesn't need to be resized.
/// Chunks are allocated by the <see cref="MemoryAllocator"/> assigned via the constructor
/// and is designed to take advantage of buffer pooling when available.
/// </summary>
internal sealed class ChunkedMemoryStream : Stream
{
/// <summary>
/// The default length in bytes of each buffer chunk.
/// </summary>
public const int DefaultBufferLength = 4096;
// The memory allocator.
private readonly MemoryAllocator allocator;
// Data
private MemoryChunk memoryChunk;
// The length of each buffer chunk
private readonly int chunkLength;
// Has the stream been disposed.
private bool isDisposed;
// Current chunk to write to
private MemoryChunk writeChunk;
// Offset into chunk to write to
private int writeOffset;
// Current chunk to read from
private MemoryChunk readChunk;
// Offset into chunk to read from
private int readOffset;
/// <summary>
/// Initializes a new instance of the <see cref="ChunkedMemoryStream"/> class.
/// </summary>
public ChunkedMemoryStream(MemoryAllocator allocator)
: this(DefaultBufferLength, allocator)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ChunkedMemoryStream"/> class.
/// </summary>
/// <param name="bufferLength">The length, in bytes of each buffer chunk.</param>
/// <param name="allocator">The memory allocator.</param>
public ChunkedMemoryStream(int bufferLength, MemoryAllocator allocator)
{
Guard.MustBeGreaterThan(bufferLength, 0, nameof(bufferLength));
Guard.NotNull(allocator, nameof(allocator));
this.chunkLength = bufferLength;
this.allocator = allocator;
}
/// <inheritdoc/>
public override bool CanRead => !this.isDisposed;
/// <inheritdoc/>
public override bool CanSeek => !this.isDisposed;
/// <inheritdoc/>
public override bool CanWrite => !this.isDisposed;
/// <inheritdoc/>
public override long Length
{
get
{
this.EnsureNotDisposed();
int length = 0;
MemoryChunk chunk = this.memoryChunk;
while (chunk != null)
{
MemoryChunk next = chunk.Next;
if (next != null)
{
length += chunk.Length;
}
else
{
length += this.writeOffset;
}
chunk = next;
}
return length;
}
}
/// <inheritdoc/>
public override long Position
{
get
{
this.EnsureNotDisposed();
if (this.readChunk is null)
{
return 0;
}
int pos = 0;
MemoryChunk chunk = this.memoryChunk;
while (chunk != this.readChunk)
{
pos += chunk.Length;
chunk = chunk.Next;
}
pos += this.readOffset;
return pos;
}
set
{
this.EnsureNotDisposed();
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
// Back up current position in case new position is out of range
MemoryChunk backupReadChunk = this.readChunk;
int backupReadOffset = this.readOffset;
this.readChunk = null;
this.readOffset = 0;
int leftUntilAtPos = (int)value;
MemoryChunk chunk = this.memoryChunk;
while (chunk != null)
{
if ((leftUntilAtPos < chunk.Length)
|| ((leftUntilAtPos == chunk.Length)
&& (chunk.Next is null)))
{
// The desired position is in this chunk
this.readChunk = chunk;
this.readOffset = leftUntilAtPos;
break;
}
leftUntilAtPos -= chunk.Length;
chunk = chunk.Next;
}
if (this.readChunk is null)
{
// Position is out of range
this.readChunk = backupReadChunk;
this.readOffset = backupReadOffset;
throw new ArgumentOutOfRangeException(nameof(value));
}
}
}
/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin)
{
this.EnsureNotDisposed();
switch (origin)
{
case SeekOrigin.Begin:
this.Position = offset;
break;
case SeekOrigin.Current:
this.Position += offset;
break;
case SeekOrigin.End:
this.Position = this.Length + offset;
break;
}
return this.Position;
}
/// <inheritdoc/>
public override void SetLength(long value)
=> throw new NotSupportedException();
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (this.isDisposed)
{
return;
}
try
{
this.isDisposed = true;
if (disposing)
{
this.ReleaseMemoryChunks(this.memoryChunk);
}
this.memoryChunk = null;
this.writeChunk = null;
this.readChunk = null;
}
finally
{
base.Dispose(disposing);
}
}
/// <inheritdoc/>
public override void Flush()
{
}
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
Guard.NotNull(buffer, nameof(buffer));
Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset));
Guard.MustBeGreaterThanOrEqualTo(count, 0, nameof(count));
if (buffer.Length - offset < count)
{
throw new ArgumentException($"{offset} subtracted from the buffer length is less than {count}");
}
this.EnsureNotDisposed();
if (this.readChunk is null)
{
if (this.memoryChunk is null)
{
return 0;
}
this.readChunk = this.memoryChunk;
this.readOffset = 0;
}
byte[] chunkBuffer = this.writeChunk.Buffer.Array;
int chunkSize = this.readChunk.Length;
if (this.readChunk.Next is null)
{
chunkSize = this.writeOffset;
}
int bytesRead = 0;
while (count > 0)
{
if (this.readOffset == chunkSize)
{
// Exit if no more chunks are currently available
if (this.readChunk.Next is null)
{
break;
}
this.readChunk = this.readChunk.Next;
this.readOffset = 0;
chunkBuffer = this.writeChunk.Buffer.Array;
chunkSize = this.readChunk.Length;
if (this.readChunk.Next is null)
{
chunkSize = this.writeOffset;
}
}
int readCount = Math.Min(count, chunkSize - this.readOffset);
Buffer.BlockCopy(chunkBuffer, this.readOffset, buffer, offset, readCount);
offset += readCount;
count -= readCount;
this.readOffset += readCount;
bytesRead += readCount;
}
return bytesRead;
}
/// <inheritdoc/>
public override int ReadByte()
{
this.EnsureNotDisposed();
if (this.readChunk is null)
{
if (this.memoryChunk is null)
{
return 0;
}
this.readChunk = this.memoryChunk;
this.readOffset = 0;
}
byte[] chunkBuffer = this.writeChunk.Buffer.Array;
int chunkSize = this.readChunk.Length;
if (this.readChunk.Next is null)
{
chunkSize = this.writeOffset;
}
if (this.readOffset == chunkSize)
{
// Exit if no more chunks are currently available
if (this.readChunk.Next is null)
{
return -1;
}
this.readChunk = this.readChunk.Next;
this.readOffset = 0;
chunkBuffer = this.writeChunk.Buffer.Array;
}
return chunkBuffer[this.readOffset++];
}
/// <inheritdoc/>
public override void Write(byte[] buffer, int offset, int count)
{
this.EnsureNotDisposed();
if (this.memoryChunk is null)
{
this.memoryChunk = this.AllocateMemoryChunk();
this.writeChunk = this.memoryChunk;
this.writeOffset = 0;
}
byte[] chunkBuffer = this.writeChunk.Buffer.Array;
int chunkSize = this.writeChunk.Length;
while (count > 0)
{
if (this.writeOffset == chunkSize)
{
// Allocate a new chunk if the current one is full
this.writeChunk.Next = this.AllocateMemoryChunk();
this.writeChunk = this.writeChunk.Next;
this.writeOffset = 0;
chunkBuffer = this.writeChunk.Buffer.Array;
chunkSize = this.writeChunk.Length;
}
int copyCount = Math.Min(count, chunkSize - this.writeOffset);
Buffer.BlockCopy(buffer, offset, chunkBuffer, this.writeOffset, copyCount);
offset += copyCount;
count -= copyCount;
this.writeOffset += copyCount;
}
}
/// <inheritdoc/>
public override void WriteByte(byte value)
{
this.EnsureNotDisposed();
if (this.memoryChunk is null)
{
this.memoryChunk = this.AllocateMemoryChunk();
this.writeChunk = this.memoryChunk;
this.writeOffset = 0;
}
byte[] chunkBuffer = this.writeChunk.Buffer.Array;
int chunkSize = this.writeChunk.Length;
if (this.writeOffset == chunkSize)
{
// Allocate a new chunk if the current one is full
this.writeChunk.Next = this.AllocateMemoryChunk();
this.writeChunk = this.writeChunk.Next;
this.writeOffset = 0;
chunkBuffer = this.writeChunk.Buffer.Array;
}
chunkBuffer[this.writeOffset++] = value;
}
/// <summary>
/// Copy entire buffer into an array.
/// </summary>
/// <returns>The <see cref="T:byte[]"/>.</returns>
public byte[] ToArray()
{
int length = (int)this.Length; // This will throw if stream is closed
byte[] copy = new byte[this.Length];
MemoryChunk backupReadChunk = this.readChunk;
int backupReadOffset = this.readOffset;
this.readChunk = this.memoryChunk;
this.readOffset = 0;
this.Read(copy, 0, length);
this.readChunk = backupReadChunk;
this.readOffset = backupReadOffset;
return copy;
}
/// <summary>
/// Write remainder of this stream to another stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
public void WriteTo(Stream stream)
{
this.EnsureNotDisposed();
Guard.NotNull(stream, nameof(stream));
if (this.readChunk is null)
{
if (this.memoryChunk is null)
{
return;
}
this.readChunk = this.memoryChunk;
this.readOffset = 0;
}
byte[] chunkBuffer = this.readChunk.Buffer.Array;
int chunkSize = this.readChunk.Length;
if (this.readChunk.Next is null)
{
chunkSize = this.writeOffset;
}
// Following code mirrors Read() logic (readChunk/readOffset should
// point just past last byte of last chunk when done)
// loop until end of chunks is found
while (true)
{
if (this.readOffset == chunkSize)
{
// Exit if no more chunks are currently available
if (this.readChunk.Next is null)
{
break;
}
this.readChunk = this.readChunk.Next;
this.readOffset = 0;
chunkBuffer = this.readChunk.Buffer.Array;
chunkSize = this.readChunk.Length;
if (this.readChunk.Next is null)
{
chunkSize = this.writeOffset;
}
}
int writeCount = chunkSize - this.readOffset;
stream.Write(chunkBuffer, this.readOffset, writeCount);
this.readOffset = chunkSize;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void EnsureNotDisposed()
{
if (this.isDisposed)
{
ThrowDisposed();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowDisposed()
{
throw new ObjectDisposedException(null, "The stream is closed.");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private MemoryChunk AllocateMemoryChunk()
{
IManagedByteBuffer buffer = this.allocator.AllocateManagedByteBuffer(this.chunkLength);
return new MemoryChunk
{
Buffer = buffer,
Next = null,
Length = this.chunkLength
};
}
private void ReleaseMemoryChunks(MemoryChunk chunk)
{
while (chunk != null)
{
chunk.Dispose();
chunk = chunk.Next;
}
}
private sealed class MemoryChunk : IDisposable
{
private bool isDisposed;
public IManagedByteBuffer Buffer { get; set; }
public MemoryChunk Next { get; set; }
public int Length { get; set; }
private void Dispose(bool disposing)
{
if (!this.isDisposed)
{
if (disposing)
{
this.Buffer.Dispose();
}
this.Buffer = null;
this.isDisposed = true;
}
}
public void Dispose()
{
this.Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}
}

5
src/ImageSharp/Image.FromStream.cs

@ -8,6 +8,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -731,7 +732,7 @@ namespace SixLabors.ImageSharp
}
// We want to be able to load images from things like HttpContext.Request.Body
using MemoryStream memoryStream = configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length);
using var memoryStream = new ChunkedMemoryStream(configuration.MemoryAllocator);
stream.CopyTo(memoryStream, configuration.StreamProcessingBufferSize);
memoryStream.Position = 0;
@ -775,7 +776,7 @@ namespace SixLabors.ImageSharp
return await action(stream, cancellationToken).ConfigureAwait(false);
}
using MemoryStream memoryStream = configuration.MemoryAllocator.AllocateFixedCapacityMemoryStream(stream.Length);
using var memoryStream = new ChunkedMemoryStream(configuration.MemoryAllocator);
await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false);
memoryStream.Position = 0;

183
tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs

@ -0,0 +1,183 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.IO;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using Xunit;
namespace SixLabors.ImageSharp.Tests.IO
{
/// <summary>
/// Tests for the <see cref="ChunkedMemoryStream"/> class.
/// </summary>
public class ChunkedMemoryStreamTests
{
private readonly MemoryAllocator allocator;
public ChunkedMemoryStreamTests()
{
this.allocator = Configuration.Default.MemoryAllocator;
}
[Fact]
public void MemoryStream_Ctor_InvalidCapacities()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new ChunkedMemoryStream(int.MinValue, this.allocator));
Assert.Throws<ArgumentOutOfRangeException>(() => new ChunkedMemoryStream(0, this.allocator));
}
[Fact]
public void ChunkedPooledMemoryStream_GetPositionTest_Negative()
{
using var ms = new ChunkedMemoryStream(this.allocator);
long iCurrentPos = ms.Position;
for (int i = -1; i > -6; i--)
{
Assert.Throws<ArgumentOutOfRangeException>(() => ms.Position = i);
Assert.Equal(ms.Position, iCurrentPos);
}
}
[Fact]
public void MemoryStream_ReadTest_Negative()
{
var ms2 = new ChunkedMemoryStream(this.allocator);
Assert.Throws<ArgumentNullException>(() => ms2.Read(null, 0, 0));
Assert.Throws<ArgumentOutOfRangeException>(() => ms2.Read(new byte[] { 1 }, -1, 0));
Assert.Throws<ArgumentOutOfRangeException>(() => ms2.Read(new byte[] { 1 }, 0, -1));
Assert.Throws<ArgumentException>(null, () => ms2.Read(new byte[] { 1 }, 2, 0));
Assert.Throws<ArgumentException>(null, () => ms2.Read(new byte[] { 1 }, 0, 2));
ms2.Dispose();
Assert.Throws<ObjectDisposedException>(() => ms2.Read(new byte[] { 1 }, 0, 1));
}
[Fact]
public void MemoryStream_WriteToTests()
{
using (var ms2 = new ChunkedMemoryStream(this.allocator))
{
byte[] bytArrRet;
byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 };
// [] Write to FileStream, check the filestream
ms2.Write(bytArr, 0, bytArr.Length);
using var readonlyStream = new ChunkedMemoryStream(this.allocator);
ms2.WriteTo(readonlyStream);
readonlyStream.Flush();
readonlyStream.Position = 0;
bytArrRet = new byte[(int)readonlyStream.Length];
readonlyStream.Read(bytArrRet, 0, (int)readonlyStream.Length);
for (int i = 0; i < bytArr.Length; i++)
{
Assert.Equal(bytArr[i], bytArrRet[i]);
}
}
// [] Write to memoryStream, check the memoryStream
using (var ms2 = new ChunkedMemoryStream(this.allocator))
using (var ms3 = new ChunkedMemoryStream(this.allocator))
{
byte[] bytArrRet;
byte[] bytArr = new byte[] { byte.MinValue, byte.MaxValue, 1, 2, 3, 4, 5, 6, 128, 250 };
ms2.Write(bytArr, 0, bytArr.Length);
ms2.WriteTo(ms3);
ms3.Position = 0;
bytArrRet = new byte[(int)ms3.Length];
ms3.Read(bytArrRet, 0, (int)ms3.Length);
for (int i = 0; i < bytArr.Length; i++)
{
Assert.Equal(bytArr[i], bytArrRet[i]);
}
}
}
[Fact]
public void MemoryStream_WriteToTests_Negative()
{
using var ms2 = new ChunkedMemoryStream(this.allocator);
Assert.Throws<ArgumentNullException>(() => ms2.WriteTo(null));
ms2.Write(new byte[] { 1 }, 0, 1);
var readonlyStream = new MemoryStream(new byte[1028], false);
Assert.Throws<NotSupportedException>(() => ms2.WriteTo(readonlyStream));
readonlyStream.Dispose();
// [] Pass in a closed stream
Assert.Throws<ObjectDisposedException>(() => ms2.WriteTo(readonlyStream));
}
[Fact]
public void MemoryStream_CopyTo_Invalid()
{
ChunkedMemoryStream memoryStream;
const string BufferSize = "bufferSize";
using (memoryStream = new ChunkedMemoryStream(this.allocator))
{
const string Destination = "destination";
Assert.Throws<ArgumentNullException>(Destination, () => memoryStream.CopyTo(destination: null));
// Validate the destination parameter first.
Assert.Throws<ArgumentNullException>(Destination, () => memoryStream.CopyTo(destination: null, bufferSize: 0));
Assert.Throws<ArgumentNullException>(Destination, () => memoryStream.CopyTo(destination: null, bufferSize: -1));
// Then bufferSize.
Assert.Throws<ArgumentOutOfRangeException>(BufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // 0-length buffer doesn't make sense.
Assert.Throws<ArgumentOutOfRangeException>(BufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1));
}
// After the Stream is disposed, we should fail on all CopyTos.
Assert.Throws<ArgumentOutOfRangeException>(BufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // Not before bufferSize is validated.
Assert.Throws<ArgumentOutOfRangeException>(BufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1));
ChunkedMemoryStream disposedStream = memoryStream;
// We should throw first for the source being disposed...
Assert.Throws<ObjectDisposedException>(() => memoryStream.CopyTo(disposedStream, 1));
// Then for the destination being disposed.
memoryStream = new ChunkedMemoryStream(this.allocator);
Assert.Throws<ObjectDisposedException>(() => memoryStream.CopyTo(disposedStream, 1));
memoryStream.Dispose();
}
[Theory]
[MemberData(nameof(CopyToData))]
public void CopyTo(Stream source, byte[] expected)
{
using var destination = new ChunkedMemoryStream(this.allocator);
source.CopyTo(destination);
Assert.InRange(source.Position, source.Length, int.MaxValue); // Copying the data should have read to the end of the stream or stayed past the end.
Assert.Equal(expected, destination.ToArray());
}
public static IEnumerable<object[]> CopyToData()
{
// Stream is positioned @ beginning of data
byte[] data1 = new byte[] { 1, 2, 3 };
var stream1 = new MemoryStream(data1);
yield return new object[] { stream1, data1 };
// Stream is positioned in the middle of data
byte[] data2 = new byte[] { 0xff, 0xf3, 0xf0 };
var stream2 = new MemoryStream(data2) { Position = 1 };
yield return new object[] { stream2, new byte[] { 0xf3, 0xf0 } };
// Stream is positioned after end of data
byte[] data3 = data2;
var stream3 = new MemoryStream(data3) { Position = data3.Length + 1 };
yield return new object[] { stream3, Array.Empty<byte>() };
}
}
}

15
tests/ImageSharp.Tests/Image/ImageTests.Load_FromStream_PassLocalConfiguration.cs

@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System.IO;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.PixelFormats;
@ -39,7 +39,7 @@ namespace SixLabors.ImageSharp.Tests
[Fact]
public void NonSeekableStream()
{
var stream = new NoneSeekableStream(this.DataStream);
var stream = new NonSeekableStream(this.DataStream);
var img = Image.Load<Rgba32>(this.TopLevelConfiguration, stream);
Assert.NotNull(img);
@ -47,6 +47,17 @@ namespace SixLabors.ImageSharp.Tests
this.TestFormat.VerifySpecificDecodeCall<Rgba32>(this.Marker, this.TopLevelConfiguration);
}
[Fact]
public async Task NonSeekableStreamAsync()
{
var stream = new NonSeekableStream(this.DataStream);
Image<Rgba32> img = await Image.LoadAsync<Rgba32>(this.TopLevelConfiguration, stream);
Assert.NotNull(img);
this.TestFormat.VerifySpecificDecodeCall<Rgba32>(this.Marker, this.TopLevelConfiguration);
}
[Fact]
public void Configuration_Stream_Decoder_Specific()
{

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

@ -0,0 +1,43 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
namespace SixLabors.ImageSharp.Tests
{
internal class NonSeekableStream : Stream
{
private readonly Stream dataStream;
public NonSeekableStream(Stream dataStream)
=> this.dataStream = dataStream;
public override bool CanRead => this.dataStream.CanRead;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
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 long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotImplementedException();
}
}

53
tests/ImageSharp.Tests/Image/NoneSeekableStream.cs

@ -1,53 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
namespace SixLabors.ImageSharp.Tests
{
internal class NoneSeekableStream : Stream
{
private Stream dataStream;
public NoneSeekableStream(Stream dataStream)
{
this.dataStream = dataStream;
}
public override bool CanRead => this.dataStream.CanRead;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => this.dataStream.Length;
public override long Position { get => this.dataStream.Position; set => throw new NotImplementedException(); }
public override void Flush()
{
this.dataStream.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
return this.dataStream.Read(buffer, offset, count);
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotImplementedException();
}
}
}
Loading…
Cancel
Save