Files
archived-discord-bot/CompatBot/Utils/BufferCopyStream.cs
2025-03-08 07:01:22 +05:00

153 lines
4.8 KiB
C#

using System.Buffers;
using System.IO;
namespace CompatBot.Utils;
internal class BufferCopyStream : Stream, IDisposable
{
private readonly Stream baseStream;
private bool usedForReads;
private bool usedForWrites;
private long position;
private readonly int bufSize;
private readonly byte[] writeBuf;
private readonly byte[] readBuf;
private int bufStart, bufLength;
private readonly Lock sync = new();
private bool disposed;
public BufferCopyStream(Stream? baseStream, int bufferSize = 4096)
{
if (baseStream == null)
throw new ArgumentNullException(nameof(baseStream));
if (bufferSize < 1)
throw new ArgumentException("Buffer size cannot be non-positive", nameof(bufferSize));
this.baseStream = baseStream;
bufSize = bufferSize;
writeBuf = ArrayPool<byte>.Shared.Rent(bufSize);
readBuf = ArrayPool<byte>.Shared.Rent(16);
}
public override void Flush() => baseStream.Flush();
public override int Read(byte[] buffer, int offset, int count)
{
usedForReads = true;
if (usedForWrites)
throw new InvalidOperationException("Stream was used for writes before");
var useTempBuf = count < 16;
const int mask = ~0b1111;
try
{
int result;
if (useTempBuf)
{
result = baseStream.Read(readBuf, 0, count);
Buffer.BlockCopy(readBuf, 0, buffer, offset, result);
}
else
result = baseStream.Read(buffer, offset, count & mask); // make count divisible by 16 to workaround mega client issues
position += result;
CopyToBuf(buffer, offset, result);
return result;
}
catch (Exception e)
{
Config.Log.Error(e, $"Failed to read from the base stream: {nameof(buffer)}.Length={buffer.Length}, {nameof(offset)}={offset}, {nameof(count)}={count}");
throw;
}
}
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)
{
usedForWrites = true;
if (usedForReads)
throw new InvalidOperationException("Stream was used for reads before");
baseStream.Write(buffer, offset, count);
position += count;
CopyToBuf(buffer, offset, count);
}
protected override void Dispose(bool disposing)
{
if (!disposed)
lock (sync)
{
if (!disposed)
{
ArrayPool<byte>.Shared.Return(writeBuf);
ArrayPool<byte>.Shared.Return(readBuf);
disposed = true;
}
}
baseStream.Dispose();
base.Dispose(disposing);
}
void IDisposable.Dispose()
{
if (!disposed)
lock (sync)
{
if (!disposed)
{
ArrayPool<byte>.Shared.Return(writeBuf);
ArrayPool<byte>.Shared.Return(readBuf);
disposed = true;
}
}
baseStream.Dispose();
base.Dispose();
}
private void CopyToBuf(byte[] buffer, int offset, int count)
{
if (count >= bufSize)
{
bufStart = 0;
bufLength = bufSize;
Buffer.BlockCopy(buffer, offset + count - bufSize, writeBuf, bufStart, bufLength);
}
else
{
// copy as much data as we can to the end of the buffer
bufStart = (bufStart + bufLength) % bufSize;
bufLength = Math.Min(bufSize - bufStart, count);
Buffer.BlockCopy(buffer, offset, writeBuf, bufStart, bufLength);
// if there's still more data, loop it around to the beginning
if (bufLength < count)
{
Buffer.BlockCopy(buffer, offset + bufLength, writeBuf, 0, count-bufLength);
bufLength = count;
}
}
}
public byte[] GetBufferedBytes()
{
var result = new byte[bufLength];
var partLength = Math.Min(bufSize - bufStart, bufLength);
Buffer.BlockCopy(writeBuf, bufStart, result, 0, partLength);
if (partLength < bufLength)
Buffer.BlockCopy(writeBuf, 0, result, partLength, bufLength - partLength);
return result;
}
public override bool CanRead => baseStream.CanRead;
public override bool CanSeek => false;
public override bool CanWrite => baseStream.CanWrite;
public override long Length => baseStream.Length;
public override long Position
{
get => position;
set => throw new InvalidOperationException();
}
}