Skip to content

Commit b9fc680

Browse files
authored
Merge pull request #1160 from adamhathcock/adam/check-if-seek
add check to see if we need to seek before hand
2 parents 4ca1a77 + 7dcc13c commit b9fc680

File tree

3 files changed

+74
-12
lines changed

3 files changed

+74
-12
lines changed

src/SharpCompress/IO/BufferedSubStream.cs

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Buffers;
23
using System.IO;
34
using System.Threading;
45
using System.Threading.Tasks;
@@ -28,14 +29,25 @@ protected override void Dispose(bool disposing)
2829
#if DEBUG_STREAMS
2930
this.DebugDispose(typeof(BufferedSubStream));
3031
#endif
31-
if (disposing) { }
32+
if (_isDisposed)
33+
{
34+
return;
35+
}
36+
_isDisposed = true;
37+
38+
if (disposing && _cache is not null)
39+
{
40+
ArrayPool<byte>.Shared.Return(_cache);
41+
_cache = null;
42+
}
3243
base.Dispose(disposing);
3344
}
3445

3546
private int _cacheOffset;
3647
private int _cacheLength;
37-
private readonly byte[] _cache = new byte[32 << 10];
48+
private byte[]? _cache = ArrayPool<byte>.Shared.Rent(81920);
3849
private long origin;
50+
private bool _isDisposed;
3951

4052
private long BytesLeftToRead { get; set; }
4153

@@ -57,29 +69,51 @@ public override long Position
5769

5870
private void RefillCache()
5971
{
60-
var count = (int)Math.Min(BytesLeftToRead, _cache.Length);
72+
if (_isDisposed)
73+
{
74+
throw new ObjectDisposedException(nameof(BufferedSubStream));
75+
}
76+
77+
var count = (int)Math.Min(BytesLeftToRead, _cache!.Length);
6178
_cacheOffset = 0;
6279
if (count == 0)
6380
{
6481
_cacheLength = 0;
6582
return;
6683
}
67-
Stream.Position = origin;
84+
85+
// Only seek if we're not already at the correct position
86+
// This avoids expensive seek operations when reading sequentially
87+
if (Stream.CanSeek && Stream.Position != origin)
88+
{
89+
Stream.Position = origin;
90+
}
91+
6892
_cacheLength = Stream.Read(_cache, 0, count);
6993
origin += _cacheLength;
7094
BytesLeftToRead -= _cacheLength;
7195
}
7296

7397
private async ValueTask RefillCacheAsync(CancellationToken cancellationToken)
7498
{
75-
var count = (int)Math.Min(BytesLeftToRead, _cache.Length);
99+
if (_isDisposed)
100+
{
101+
throw new ObjectDisposedException(nameof(BufferedSubStream));
102+
}
103+
104+
var count = (int)Math.Min(BytesLeftToRead, _cache!.Length);
76105
_cacheOffset = 0;
77106
if (count == 0)
78107
{
79108
_cacheLength = 0;
80109
return;
81110
}
82-
Stream.Position = origin;
111+
// Only seek if we're not already at the correct position
112+
// This avoids expensive seek operations when reading sequentially
113+
if (Stream.CanSeek && Stream.Position != origin)
114+
{
115+
Stream.Position = origin;
116+
}
83117
_cacheLength = await Stream
84118
.ReadAsync(_cache, 0, count, cancellationToken)
85119
.ConfigureAwait(false);
@@ -102,7 +136,7 @@ public override int Read(byte[] buffer, int offset, int count)
102136
}
103137

104138
count = Math.Min(count, _cacheLength - _cacheOffset);
105-
Buffer.BlockCopy(_cache, _cacheOffset, buffer, offset, count);
139+
Buffer.BlockCopy(_cache!, _cacheOffset, buffer, offset, count);
106140
_cacheOffset += count;
107141
}
108142

@@ -120,7 +154,7 @@ public override int ReadByte()
120154
}
121155
}
122156

123-
return _cache[_cacheOffset++];
157+
return _cache![_cacheOffset++];
124158
}
125159

126160
public override async Task<int> ReadAsync(
@@ -143,7 +177,7 @@ CancellationToken cancellationToken
143177
}
144178

145179
count = Math.Min(count, _cacheLength - _cacheOffset);
146-
Buffer.BlockCopy(_cache, _cacheOffset, buffer, offset, count);
180+
Buffer.BlockCopy(_cache!, _cacheOffset, buffer, offset, count);
147181
_cacheOffset += count;
148182
}
149183

@@ -170,7 +204,7 @@ public override async ValueTask<int> ReadAsync(
170204
}
171205

172206
count = Math.Min(count, _cacheLength - _cacheOffset);
173-
_cache.AsSpan(_cacheOffset, count).CopyTo(buffer.Span);
207+
_cache!.AsSpan(_cacheOffset, count).CopyTo(buffer.Span);
174208
_cacheOffset += count;
175209
}
176210

src/SharpCompress/IO/SharpCompressStream.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,6 @@ public override long Seek(long offset, SeekOrigin origin)
257257
ValidateBufferState();
258258
}
259259

260-
long orig = _internalPosition;
261260
long targetPos;
262261
// Calculate the absolute target position based on origin
263262
switch (origin)

tests/SharpCompress.Test/Streams/SharpCompressStreamTest.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Text;
66
using SharpCompress.Compressors.LZMA;
77
using SharpCompress.IO;
8+
using SharpCompress.Test.Mocks;
89
using Xunit;
910

1011
namespace SharpCompress.Test.Streams;
@@ -64,7 +65,14 @@ public void BufferReadAndSeekTest()
6465
{
6566
createData(ms);
6667

67-
using (SharpCompressStream scs = new SharpCompressStream(ms, true, false, 0x10000))
68+
using (
69+
SharpCompressStream scs = new SharpCompressStream(
70+
new ForwardOnlyStream(ms),
71+
true,
72+
false,
73+
0x10000
74+
)
75+
)
6876
{
6977
IStreamStack stack = (IStreamStack)scs;
7078

@@ -89,4 +97,25 @@ public void BufferReadAndSeekTest()
8997
}
9098
}
9199
}
100+
101+
[Fact]
102+
public void BufferedSubStream_DoubleDispose_DoesNotCorruptArrayPool()
103+
{
104+
// This test verifies that calling Dispose multiple times on BufferedSubStream
105+
// doesn't return the same array to the pool twice, which would cause pool corruption
106+
byte[] data = new byte[0x10000];
107+
using (MemoryStream ms = new MemoryStream(data))
108+
{
109+
var stream = new BufferedSubStream(ms, 0, data.Length);
110+
111+
// First disposal
112+
stream.Dispose();
113+
114+
// Second disposal should not throw or corrupt the pool
115+
stream.Dispose();
116+
}
117+
118+
// If we got here without an exception, the test passed
119+
Assert.True(true);
120+
}
92121
}

0 commit comments

Comments
 (0)