From 8c98bf6b9240ac92c38fee5694acd9108f0fcf9b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 26 Oct 2025 03:15:51 +0000
Subject: [PATCH 1/5] Initial plan
From 59798caefcaf64bb0889085bb1d3d06b60abba6c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 26 Oct 2025 03:20:42 +0000
Subject: [PATCH 2/5] Make ResourcePackageReader thread-safe with lock-based
synchronization
Co-authored-by: Soar360 <15421284+Soar360@users.noreply.github.com>
---
.../ResourcePackageReaderThreadSafetyTests.cs | 241 ++++++++++++++++++
LuYao.ResourcePacker/ResourcePackageReader.cs | 50 +++-
2 files changed, 277 insertions(+), 14 deletions(-)
create mode 100644 LuYao.ResourcePacker.Tests/ResourcePackageReaderThreadSafetyTests.cs
diff --git a/LuYao.ResourcePacker.Tests/ResourcePackageReaderThreadSafetyTests.cs b/LuYao.ResourcePacker.Tests/ResourcePackageReaderThreadSafetyTests.cs
new file mode 100644
index 0000000..d5b62d5
--- /dev/null
+++ b/LuYao.ResourcePacker.Tests/ResourcePackageReaderThreadSafetyTests.cs
@@ -0,0 +1,241 @@
+using Xunit;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace LuYao.ResourcePacker.Tests
+{
+ public class ResourcePackageReaderThreadSafetyTests : IDisposable
+ {
+ private readonly string _tempDirectory;
+ private readonly string _outputPath;
+
+ public ResourcePackageReaderThreadSafetyTests()
+ {
+ // 创建临时目录用于测试
+ _tempDirectory = Path.Combine(Path.GetTempPath(), $"ResourcePackerThreadSafetyTests_{Guid.NewGuid()}");
+ Directory.CreateDirectory(_tempDirectory);
+ _outputPath = Path.Combine(_tempDirectory, "test.dat");
+
+ // 创建测试资源包
+ var sourceDir = Path.Combine(Directory.GetCurrentDirectory(), "TestResources");
+ var packer = new ResourcePacker(sourceDir, "*.res.*");
+ packer.PackResources(_outputPath);
+ }
+
+ [Fact]
+ public async Task ConcurrentReadResourceAsync_ShouldNotCorruptData()
+ {
+ // Arrange
+ using var reader = new ResourcePackageReader(_outputPath);
+ const int threadCount = 10;
+ const int iterationsPerThread = 50;
+
+ // Act - Read the same resources concurrently from multiple threads
+ var tasks = Enumerable.Range(0, threadCount).Select(async _ =>
+ {
+ for (int i = 0; i < iterationsPerThread; i++)
+ {
+ var testContent = await reader.ReadResourceAsStringAsync("test");
+ var greetingContent = await reader.ReadResourceAsStringAsync("greeting");
+
+ // Assert - Verify data integrity
+ Assert.Contains("Hello, World!", testContent);
+ Assert.Contains("Hello from resource file!", greetingContent);
+ }
+ }).ToArray();
+
+ await Task.WhenAll(tasks);
+ }
+
+ [Fact]
+ public void ConcurrentReadResource_ShouldNotCorruptData()
+ {
+ // Arrange
+ using var reader = new ResourcePackageReader(_outputPath);
+ const int threadCount = 10;
+ const int iterationsPerThread = 50;
+
+ // Act - Read the same resources concurrently from multiple threads
+ var tasks = Enumerable.Range(0, threadCount).Select(i => Task.Run(() =>
+ {
+ for (int j = 0; j < iterationsPerThread; j++)
+ {
+ var testContent = reader.ReadResourceAsString("test");
+ var greetingContent = reader.ReadResourceAsString("greeting");
+
+ // Assert - Verify data integrity
+ Assert.Contains("Hello, World!", testContent);
+ Assert.Contains("Hello from resource file!", greetingContent);
+ }
+ })).ToArray();
+
+ Task.WaitAll(tasks);
+ }
+
+ [Fact]
+ public void ConcurrentReadResourceBytes_ShouldReturnCorrectData()
+ {
+ // Arrange
+ using var reader = new ResourcePackageReader(_outputPath);
+ const int threadCount = 10;
+ const int iterationsPerThread = 50;
+
+ // Act - Read the same resources concurrently from multiple threads
+ var tasks = Enumerable.Range(0, threadCount).Select(i => Task.Run(() =>
+ {
+ for (int j = 0; j < iterationsPerThread; j++)
+ {
+ var testBytes = reader.ReadResource("test");
+ var greetingBytes = reader.ReadResource("greeting");
+
+ // Assert - Verify data integrity by converting to string
+ var testContent = Encoding.UTF8.GetString(testBytes);
+ var greetingContent = Encoding.UTF8.GetString(greetingBytes);
+
+ Assert.Contains("Hello, World!", testContent);
+ Assert.Contains("Hello from resource file!", greetingContent);
+ }
+ })).ToArray();
+
+ Task.WaitAll(tasks);
+ }
+
+ [Fact]
+ public void ConcurrentGetStream_ShouldNotCorruptData()
+ {
+ // Arrange
+ using var reader = new ResourcePackageReader(_outputPath);
+ const int threadCount = 10;
+ const int iterationsPerThread = 20;
+
+ // Act - Get streams concurrently from multiple threads
+ var tasks = Enumerable.Range(0, threadCount).Select(i => Task.Run(() =>
+ {
+ for (int j = 0; j < iterationsPerThread; j++)
+ {
+ using var testStream = reader.GetStream("test");
+ using var greetingStream = reader.GetStream("greeting");
+
+ using var testReader = new StreamReader(testStream);
+ using var greetingReader = new StreamReader(greetingStream);
+
+ var testContent = testReader.ReadToEnd();
+ var greetingContent = greetingReader.ReadToEnd();
+
+ // Assert - Verify data integrity
+ Assert.Contains("Hello, World!", testContent);
+ Assert.Contains("Hello from resource file!", greetingContent);
+ }
+ })).ToArray();
+
+ Task.WaitAll(tasks);
+ }
+
+ [Fact]
+ public void MixedConcurrentOperations_ShouldNotCorruptData()
+ {
+ // Arrange
+ using var reader = new ResourcePackageReader(_outputPath);
+ const int threadCount = 15;
+ const int iterationsPerThread = 30;
+
+ // Act - Mix different read operations concurrently
+ var tasks = Enumerable.Range(0, threadCount).Select(i => Task.Run(() =>
+ {
+ for (int j = 0; j < iterationsPerThread; j++)
+ {
+ // Alternate between different read methods
+ switch (j % 3)
+ {
+ case 0:
+ var bytes = reader.ReadResource("test");
+ Assert.True(bytes.Length > 0);
+ break;
+ case 1:
+ var content = reader.ReadResourceAsString("greeting");
+ Assert.Contains("Hello from resource file!", content);
+ break;
+ case 2:
+ using (var stream = reader.GetStream("test"))
+ {
+ using var sr = new StreamReader(stream);
+ var streamContent = sr.ReadToEnd();
+ Assert.Contains("Hello, World!", streamContent);
+ }
+ break;
+ }
+ }
+ })).ToArray();
+
+ Task.WaitAll(tasks);
+ }
+
+ [Fact]
+ public async Task ConcurrentReadWithDifferentEncodings_ShouldNotCorruptData()
+ {
+ // Arrange
+ using var reader = new ResourcePackageReader(_outputPath);
+ const int threadCount = 8;
+ const int iterationsPerThread = 40;
+
+ // Act - Read with different encodings concurrently
+ var tasks = Enumerable.Range(0, threadCount).Select(async i =>
+ {
+ for (int j = 0; j < iterationsPerThread; j++)
+ {
+ var encoding = (j % 2 == 0) ? Encoding.UTF8 : Encoding.ASCII;
+ var content = await reader.ReadResourceAsStringAsync("test", encoding);
+
+ // Assert - Verify data integrity
+ Assert.Contains("Hello, World!", content);
+ }
+ }).ToArray();
+
+ await Task.WhenAll(tasks);
+ }
+
+ [Fact]
+ public void ConcurrentStreamReads_ShouldNotCorruptData()
+ {
+ // Arrange
+ using var reader = new ResourcePackageReader(_outputPath);
+ const int threadCount = 10;
+
+ // Act - Multiple threads reading from streams simultaneously
+ var tasks = Enumerable.Range(0, threadCount).Select(i => Task.Run(() =>
+ {
+ using var stream = reader.GetStream("test");
+ var buffer = new byte[1024];
+ var totalRead = 0;
+
+ while (true)
+ {
+ var bytesRead = stream.Read(buffer, totalRead, buffer.Length - totalRead);
+ if (bytesRead == 0)
+ break;
+ totalRead += bytesRead;
+ }
+
+ var content = Encoding.UTF8.GetString(buffer, 0, totalRead);
+
+ // Assert - Verify data integrity
+ Assert.Contains("Hello, World!", content);
+ })).ToArray();
+
+ Task.WaitAll(tasks);
+ }
+
+ public void Dispose()
+ {
+ // 清理临时目录
+ if (Directory.Exists(_tempDirectory))
+ {
+ Directory.Delete(_tempDirectory, true);
+ }
+ }
+ }
+}
diff --git a/LuYao.ResourcePacker/ResourcePackageReader.cs b/LuYao.ResourcePacker/ResourcePackageReader.cs
index e4a63dc..a04b89f 100644
--- a/LuYao.ResourcePacker/ResourcePackageReader.cs
+++ b/LuYao.ResourcePacker/ResourcePackageReader.cs
@@ -8,11 +8,13 @@ namespace LuYao.ResourcePacker
{
///
/// Provides functionality to read resources from a packaged resource file.
+ /// This class is thread-safe for concurrent read operations.
///
public class ResourcePackageReader : IDisposable
{
private readonly FileStream _fileStream;
private readonly Dictionary _resourceIndex;
+ private readonly object _lock = new object();
private bool _disposed;
///
@@ -91,9 +93,15 @@ public async Task ReadResourceAsync(string resourceKey)
throw new KeyNotFoundException($"Resource with key '{resourceKey}' not found.");
var buffer = new byte[entry.Length];
- _fileStream.Seek(entry.Offset, SeekOrigin.Begin);
- await _fileStream.ReadAsync(buffer, 0, buffer.Length);
- return buffer;
+
+ // Lock to ensure thread-safe access to FileStream
+ lock (_lock)
+ {
+ _fileStream.Seek(entry.Offset, SeekOrigin.Begin);
+ _fileStream.Read(buffer, 0, buffer.Length);
+ }
+
+ return await Task.FromResult(buffer);
}
///
@@ -133,15 +141,20 @@ public byte[] ReadResource(string resourceKey)
throw new KeyNotFoundException($"Resource with key '{resourceKey}' not found.");
var buffer = new byte[entry.Length];
- _fileStream.Seek(entry.Offset, SeekOrigin.Begin);
- int totalRead = 0;
- while (totalRead < entry.Length)
+ // Lock to ensure thread-safe access to FileStream
+ lock (_lock)
{
- int bytesRead = _fileStream.Read(buffer, totalRead, entry.Length - totalRead);
- if (bytesRead == 0)
- throw new EndOfStreamException($"Unexpected end of stream while reading resource '{resourceKey}'.");
- totalRead += bytesRead;
+ _fileStream.Seek(entry.Offset, SeekOrigin.Begin);
+
+ int totalRead = 0;
+ while (totalRead < entry.Length)
+ {
+ int bytesRead = _fileStream.Read(buffer, totalRead, entry.Length - totalRead);
+ if (bytesRead == 0)
+ throw new EndOfStreamException($"Unexpected end of stream while reading resource '{resourceKey}'.");
+ totalRead += bytesRead;
+ }
}
return buffer;
@@ -184,7 +197,7 @@ public Stream GetStream(string resourceKey)
throw new KeyNotFoundException($"Resource with key '{resourceKey}' not found.");
// Create a SubStream wrapper that provides a read-only view of a portion of the file
- return new SubStream(_fileStream, entry.Offset, entry.Length);
+ return new SubStream(_fileStream, entry.Offset, entry.Length, _lock);
}
///
@@ -208,17 +221,20 @@ internal class ResourceEntry
///
/// A stream wrapper that provides a read-only view of a portion of another stream.
+ /// This class is thread-safe when used with a lock object.
///
internal class SubStream : Stream
{
private readonly Stream _baseStream;
private readonly long _offset;
private readonly long _length;
+ private readonly object _lock;
private long _position;
- public SubStream(Stream baseStream, long offset, long length)
+ public SubStream(Stream baseStream, long offset, long length, object lockObject)
{
_baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream));
+ _lock = lockObject ?? throw new ArgumentNullException(nameof(lockObject));
_offset = offset;
_length = length;
_position = 0;
@@ -256,9 +272,15 @@ public override int Read(byte[] buffer, int offset, int count)
return 0;
int toRead = (int)Math.Min(count, remaining);
+ int bytesRead;
+
+ // Lock to ensure thread-safe access to the base stream
+ lock (_lock)
+ {
+ _baseStream.Seek(_offset + _position, SeekOrigin.Begin);
+ bytesRead = _baseStream.Read(buffer, offset, toRead);
+ }
- _baseStream.Seek(_offset + _position, SeekOrigin.Begin);
- int bytesRead = _baseStream.Read(buffer, offset, toRead);
_position += bytesRead;
return bytesRead;
From bdd6da583476b128cbf7e6fe55ce0f32475e61a5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 26 Oct 2025 03:22:36 +0000
Subject: [PATCH 3/5] Address code review feedback: improve ReadResourceAsync
and fix comments
Co-authored-by: Soar360 <15421284+Soar360@users.noreply.github.com>
---
.../ResourcePackageReaderThreadSafetyTests.cs | 6 +++---
LuYao.ResourcePacker/ResourcePackageReader.cs | 14 +++++++++++---
2 files changed, 14 insertions(+), 6 deletions(-)
diff --git a/LuYao.ResourcePacker.Tests/ResourcePackageReaderThreadSafetyTests.cs b/LuYao.ResourcePacker.Tests/ResourcePackageReaderThreadSafetyTests.cs
index d5b62d5..a35a3ab 100644
--- a/LuYao.ResourcePacker.Tests/ResourcePackageReaderThreadSafetyTests.cs
+++ b/LuYao.ResourcePacker.Tests/ResourcePackageReaderThreadSafetyTests.cs
@@ -15,12 +15,12 @@ public class ResourcePackageReaderThreadSafetyTests : IDisposable
public ResourcePackageReaderThreadSafetyTests()
{
- // 创建临时目录用于测试
+ // Create temporary directory for tests
_tempDirectory = Path.Combine(Path.GetTempPath(), $"ResourcePackerThreadSafetyTests_{Guid.NewGuid()}");
Directory.CreateDirectory(_tempDirectory);
_outputPath = Path.Combine(_tempDirectory, "test.dat");
- // 创建测试资源包
+ // Create test resource package
var sourceDir = Path.Combine(Directory.GetCurrentDirectory(), "TestResources");
var packer = new ResourcePacker(sourceDir, "*.res.*");
packer.PackResources(_outputPath);
@@ -231,7 +231,7 @@ public void ConcurrentStreamReads_ShouldNotCorruptData()
public void Dispose()
{
- // 清理临时目录
+ // Clean up temporary directory
if (Directory.Exists(_tempDirectory))
{
Directory.Delete(_tempDirectory, true);
diff --git a/LuYao.ResourcePacker/ResourcePackageReader.cs b/LuYao.ResourcePacker/ResourcePackageReader.cs
index a04b89f..17d1c41 100644
--- a/LuYao.ResourcePacker/ResourcePackageReader.cs
+++ b/LuYao.ResourcePacker/ResourcePackageReader.cs
@@ -84,7 +84,7 @@ public bool ContainsKey(string resourceKey)
///
/// The key of the resource to read.
/// A task that represents the asynchronous read operation.
- public async Task ReadResourceAsync(string resourceKey)
+ public Task ReadResourceAsync(string resourceKey)
{
if (_disposed)
throw new ObjectDisposedException(nameof(ResourcePackageReader));
@@ -98,10 +98,18 @@ public async Task ReadResourceAsync(string resourceKey)
lock (_lock)
{
_fileStream.Seek(entry.Offset, SeekOrigin.Begin);
- _fileStream.Read(buffer, 0, buffer.Length);
+
+ int totalRead = 0;
+ while (totalRead < entry.Length)
+ {
+ int bytesRead = _fileStream.Read(buffer, totalRead, entry.Length - totalRead);
+ if (bytesRead == 0)
+ throw new EndOfStreamException($"Unexpected end of stream while reading resource '{resourceKey}'.");
+ totalRead += bytesRead;
+ }
}
- return await Task.FromResult(buffer);
+ return Task.FromResult(buffer);
}
///
From 62be3a1afce7335c140fe27fddab2f7152ff3024 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 26 Oct 2025 03:37:22 +0000
Subject: [PATCH 4/5] Optimize thread-safety: use FileShare.Read instead of
locks
Co-authored-by: Soar360 <15421284+Soar360@users.noreply.github.com>
---
LuYao.ResourcePacker/ResourcePackageReader.cs | 139 +++---------------
1 file changed, 18 insertions(+), 121 deletions(-)
diff --git a/LuYao.ResourcePacker/ResourcePackageReader.cs b/LuYao.ResourcePacker/ResourcePackageReader.cs
index 17d1c41..8969b32 100644
--- a/LuYao.ResourcePacker/ResourcePackageReader.cs
+++ b/LuYao.ResourcePacker/ResourcePackageReader.cs
@@ -8,13 +8,12 @@ namespace LuYao.ResourcePacker
{
///
/// Provides functionality to read resources from a packaged resource file.
- /// This class is thread-safe for concurrent read operations.
+ /// This class is thread-safe for concurrent read operations by creating independent FileStream instances per operation.
///
public class ResourcePackageReader : IDisposable
{
- private readonly FileStream _fileStream;
+ private readonly string _filePath;
private readonly Dictionary _resourceIndex;
- private readonly object _lock = new object();
private bool _disposed;
///
@@ -23,14 +22,15 @@ public class ResourcePackageReader : IDisposable
/// The path to the resource package file.
public ResourcePackageReader(string filePath)
{
- _fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
+ _filePath = filePath ?? throw new ArgumentNullException(nameof(filePath));
_resourceIndex = new Dictionary();
LoadIndex();
}
private void LoadIndex()
{
- using var reader = new BinaryReader(_fileStream, System.Text.Encoding.UTF8, leaveOpen: true);
+ using var fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
+ using var reader = new BinaryReader(fileStream, System.Text.Encoding.UTF8, leaveOpen: false);
// Read version number
var version = reader.ReadByte();
@@ -52,7 +52,7 @@ private void LoadIndex()
}
// Calculate offsets based on the current position
- long currentOffset = _fileStream.Position;
+ long currentOffset = fileStream.Position;
foreach (var (key, length) in indexEntries)
{
_resourceIndex[key] = new ResourceEntry
@@ -94,15 +94,15 @@ public Task ReadResourceAsync(string resourceKey)
var buffer = new byte[entry.Length];
- // Lock to ensure thread-safe access to FileStream
- lock (_lock)
+ // Create a new FileStream for this read operation (thread-safe via FileShare.Read)
+ using (var fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
- _fileStream.Seek(entry.Offset, SeekOrigin.Begin);
+ fileStream.Seek(entry.Offset, SeekOrigin.Begin);
int totalRead = 0;
while (totalRead < entry.Length)
{
- int bytesRead = _fileStream.Read(buffer, totalRead, entry.Length - totalRead);
+ int bytesRead = fileStream.Read(buffer, totalRead, entry.Length - totalRead);
if (bytesRead == 0)
throw new EndOfStreamException($"Unexpected end of stream while reading resource '{resourceKey}'.");
totalRead += bytesRead;
@@ -150,15 +150,15 @@ public byte[] ReadResource(string resourceKey)
var buffer = new byte[entry.Length];
- // Lock to ensure thread-safe access to FileStream
- lock (_lock)
+ // Create a new FileStream for this read operation (thread-safe via FileShare.Read)
+ using (var fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
- _fileStream.Seek(entry.Offset, SeekOrigin.Begin);
+ fileStream.Seek(entry.Offset, SeekOrigin.Begin);
int totalRead = 0;
while (totalRead < entry.Length)
{
- int bytesRead = _fileStream.Read(buffer, totalRead, entry.Length - totalRead);
+ int bytesRead = fileStream.Read(buffer, totalRead, entry.Length - totalRead);
if (bytesRead == 0)
throw new EndOfStreamException($"Unexpected end of stream while reading resource '{resourceKey}'.");
totalRead += bytesRead;
@@ -204,8 +204,9 @@ public Stream GetStream(string resourceKey)
if (!_resourceIndex.TryGetValue(resourceKey, out var entry))
throw new KeyNotFoundException($"Resource with key '{resourceKey}' not found.");
- // Create a SubStream wrapper that provides a read-only view of a portion of the file
- return new SubStream(_fileStream, entry.Offset, entry.Length, _lock);
+ // Read the resource data into memory and return a MemoryStream
+ var buffer = ReadResource(resourceKey);
+ return new MemoryStream(buffer, writable: false);
}
///
@@ -213,11 +214,7 @@ public Stream GetStream(string resourceKey)
///
public void Dispose()
{
- if (!_disposed)
- {
- _fileStream?.Dispose();
- _disposed = true;
- }
+ _disposed = true;
}
}
@@ -226,104 +223,4 @@ internal class ResourceEntry
public long Offset { get; set; }
public int Length { get; set; }
}
-
- ///
- /// A stream wrapper that provides a read-only view of a portion of another stream.
- /// This class is thread-safe when used with a lock object.
- ///
- internal class SubStream : Stream
- {
- private readonly Stream _baseStream;
- private readonly long _offset;
- private readonly long _length;
- private readonly object _lock;
- private long _position;
-
- public SubStream(Stream baseStream, long offset, long length, object lockObject)
- {
- _baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream));
- _lock = lockObject ?? throw new ArgumentNullException(nameof(lockObject));
- _offset = offset;
- _length = length;
- _position = 0;
- }
-
- public override bool CanRead => true;
- public override bool CanSeek => true;
- public override bool CanWrite => false;
- public override long Length => _length;
-
- public override long Position
- {
- get => _position;
- set
- {
- if (value < 0 || value > _length)
- throw new ArgumentOutOfRangeException(nameof(value));
- _position = value;
- }
- }
-
- public override int Read(byte[] buffer, int offset, int count)
- {
- if (buffer == null)
- throw new ArgumentNullException(nameof(buffer));
- if (offset < 0)
- throw new ArgumentOutOfRangeException(nameof(offset));
- if (count < 0)
- throw new ArgumentOutOfRangeException(nameof(count));
- if (buffer.Length - offset < count)
- throw new ArgumentException("Invalid offset/count combination");
-
- long remaining = _length - _position;
- if (remaining <= 0)
- return 0;
-
- int toRead = (int)Math.Min(count, remaining);
- int bytesRead;
-
- // Lock to ensure thread-safe access to the base stream
- lock (_lock)
- {
- _baseStream.Seek(_offset + _position, SeekOrigin.Begin);
- bytesRead = _baseStream.Read(buffer, offset, toRead);
- }
-
- _position += bytesRead;
-
- return bytesRead;
- }
-
- public override long Seek(long offset, SeekOrigin origin)
- {
- long newPosition = origin switch
- {
- SeekOrigin.Begin => offset,
- SeekOrigin.Current => _position + offset,
- SeekOrigin.End => _length + offset,
- _ => throw new ArgumentException("Invalid seek origin", nameof(origin))
- };
-
- if (newPosition < 0 || newPosition > _length)
- throw new ArgumentOutOfRangeException(nameof(offset));
-
- _position = newPosition;
- return _position;
- }
-
- public override void Flush()
- {
- // Read-only stream, nothing to flush
- }
-
- public override void SetLength(long value)
- {
- throw new NotSupportedException("Cannot set length on a read-only stream.");
- }
-
- public override void Write(byte[] buffer, int offset, int count)
- {
- throw new NotSupportedException("Cannot write to a read-only stream.");
- }
- }
}
\ No newline at end of file
From f7d1483b4c34063708b06c167f6bb611abb0c0c9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 26 Oct 2025 03:50:12 +0000
Subject: [PATCH 5/5] Remove IDisposable and implement streaming GetStream
Co-authored-by: Soar360 <15421284+Soar360@users.noreply.github.com>
---
.../ResourcePackageReaderThreadSafetyTests.cs | 14 +-
.../ResourcePackerTests.cs | 94 ++---------
LuYao.ResourcePacker/ResourcePackageReader.cs | 151 +++++++++++++++---
examples/ExampleProject/Program.cs | 2 +-
4 files changed, 150 insertions(+), 111 deletions(-)
diff --git a/LuYao.ResourcePacker.Tests/ResourcePackageReaderThreadSafetyTests.cs b/LuYao.ResourcePacker.Tests/ResourcePackageReaderThreadSafetyTests.cs
index a35a3ab..92b4123 100644
--- a/LuYao.ResourcePacker.Tests/ResourcePackageReaderThreadSafetyTests.cs
+++ b/LuYao.ResourcePacker.Tests/ResourcePackageReaderThreadSafetyTests.cs
@@ -30,7 +30,7 @@ public ResourcePackageReaderThreadSafetyTests()
public async Task ConcurrentReadResourceAsync_ShouldNotCorruptData()
{
// Arrange
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
const int threadCount = 10;
const int iterationsPerThread = 50;
@@ -55,7 +55,7 @@ public async Task ConcurrentReadResourceAsync_ShouldNotCorruptData()
public void ConcurrentReadResource_ShouldNotCorruptData()
{
// Arrange
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
const int threadCount = 10;
const int iterationsPerThread = 50;
@@ -80,7 +80,7 @@ public void ConcurrentReadResource_ShouldNotCorruptData()
public void ConcurrentReadResourceBytes_ShouldReturnCorrectData()
{
// Arrange
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
const int threadCount = 10;
const int iterationsPerThread = 50;
@@ -108,7 +108,7 @@ public void ConcurrentReadResourceBytes_ShouldReturnCorrectData()
public void ConcurrentGetStream_ShouldNotCorruptData()
{
// Arrange
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
const int threadCount = 10;
const int iterationsPerThread = 20;
@@ -139,7 +139,7 @@ public void ConcurrentGetStream_ShouldNotCorruptData()
public void MixedConcurrentOperations_ShouldNotCorruptData()
{
// Arrange
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
const int threadCount = 15;
const int iterationsPerThread = 30;
@@ -178,7 +178,7 @@ public void MixedConcurrentOperations_ShouldNotCorruptData()
public async Task ConcurrentReadWithDifferentEncodings_ShouldNotCorruptData()
{
// Arrange
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
const int threadCount = 8;
const int iterationsPerThread = 40;
@@ -202,7 +202,7 @@ public async Task ConcurrentReadWithDifferentEncodings_ShouldNotCorruptData()
public void ConcurrentStreamReads_ShouldNotCorruptData()
{
// Arrange
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
const int threadCount = 10;
// Act - Multiple threads reading from streams simultaneously
diff --git a/LuYao.ResourcePacker.Tests/ResourcePackerTests.cs b/LuYao.ResourcePacker.Tests/ResourcePackerTests.cs
index 8f890d7..c8eea07 100644
--- a/LuYao.ResourcePacker.Tests/ResourcePackerTests.cs
+++ b/LuYao.ResourcePacker.Tests/ResourcePackerTests.cs
@@ -34,7 +34,7 @@ public async Task PackAndReadResources_ShouldWorkCorrectly()
// Assert
Assert.True(File.Exists(_outputPath));
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
var jsonContent = await reader.ReadResourceAsStringAsync("test");
var txtContent = await reader.ReadResourceAsStringAsync("greeting");
@@ -53,7 +53,7 @@ public async Task ResourceKeys_ShouldMatchSourceFiles()
packer.PackResources(_outputPath);
// Assert
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
var keys = reader.ResourceKeys.ToList();
Assert.Contains("test", keys);
@@ -70,28 +70,11 @@ public async Task ReadResource_WithInvalidKey_ShouldThrowException()
packer.PackResources(_outputPath);
// Act & Assert
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
await Assert.ThrowsAsync(() =>
reader.ReadResourceAsync("non_existent_key"));
}
- [Fact]
- public async Task ReadResource_AfterDispose_ShouldThrowException()
- {
- // Arrange
- var sourceDir = Path.Combine(Directory.GetCurrentDirectory(), "TestResources");
- var packer = new ResourcePacker(sourceDir, "*.res.*");
- packer.PackResources(_outputPath);
-
- // Act
- var reader = new ResourcePackageReader(_outputPath);
- reader.Dispose();
-
- // Assert
- await Assert.ThrowsAsync(() =>
- reader.ReadResourceAsync("test"));
- }
-
[Fact]
public void PackResources_WithEmptyDirectory_ShouldCreateEmptyPackage()
{
@@ -104,7 +87,7 @@ public void PackResources_WithEmptyDirectory_ShouldCreateEmptyPackage()
packer.PackResources(_outputPath);
// Assert
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
Assert.Empty(reader.ResourceKeys);
}
@@ -140,7 +123,7 @@ public async Task ResourceKeys_ShouldBeSorted()
packer.PackResources(_outputPath);
// Assert
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
var keys = reader.ResourceKeys.ToList();
var sortedKeys = keys.OrderBy(k => k).ToList();
@@ -207,7 +190,7 @@ public async Task ReadResourceAsStringAsync_WithEncoding_ShouldUseProvidedEncodi
packer.PackResources(_outputPath);
// Act
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
var content = await reader.ReadResourceAsStringAsync("greeting", Encoding.UTF8);
// Assert
@@ -223,7 +206,7 @@ public void ReadResource_ShouldReturnByteArraySynchronously()
packer.PackResources(_outputPath);
// Act
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
var bytes = reader.ReadResource("greeting");
// Assert
@@ -242,7 +225,7 @@ public void ReadResourceAsString_ShouldReturnStringSynchronously()
packer.PackResources(_outputPath);
// Act
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
var content = reader.ReadResourceAsString("greeting");
// Assert
@@ -258,7 +241,7 @@ public void ReadResourceAsString_WithEncoding_ShouldUseProvidedEncoding()
packer.PackResources(_outputPath);
// Act
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
var content = reader.ReadResourceAsString("test", Encoding.UTF8);
// Assert
@@ -274,7 +257,7 @@ public void GetStream_ShouldReturnReadOnlyStream()
packer.PackResources(_outputPath);
// Act
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
using var stream = reader.GetStream("greeting");
// Assert
@@ -296,62 +279,11 @@ public void GetStream_WithInvalidKey_ShouldThrowException()
packer.PackResources(_outputPath);
// Act & Assert
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
Assert.Throws(() =>
reader.GetStream("non_existent_key"));
}
- [Fact]
- public void ReadResourceSync_AfterDispose_ShouldThrowException()
- {
- // Arrange
- var sourceDir = Path.Combine(Directory.GetCurrentDirectory(), "TestResources");
- var packer = new ResourcePacker(sourceDir, "*.res.*");
- packer.PackResources(_outputPath);
-
- // Act
- var reader = new ResourcePackageReader(_outputPath);
- reader.Dispose();
-
- // Assert
- Assert.Throws(() =>
- reader.ReadResource("test"));
- }
-
- [Fact]
- public void ReadResourceAsStringSync_AfterDispose_ShouldThrowException()
- {
- // Arrange
- var sourceDir = Path.Combine(Directory.GetCurrentDirectory(), "TestResources");
- var packer = new ResourcePacker(sourceDir, "*.res.*");
- packer.PackResources(_outputPath);
-
- // Act
- var reader = new ResourcePackageReader(_outputPath);
- reader.Dispose();
-
- // Assert
- Assert.Throws(() =>
- reader.ReadResourceAsString("test"));
- }
-
- [Fact]
- public void GetStream_AfterDispose_ShouldThrowException()
- {
- // Arrange
- var sourceDir = Path.Combine(Directory.GetCurrentDirectory(), "TestResources");
- var packer = new ResourcePacker(sourceDir, "*.res.*");
- packer.PackResources(_outputPath);
-
- // Act
- var reader = new ResourcePackageReader(_outputPath);
- reader.Dispose();
-
- // Assert
- Assert.Throws(() =>
- reader.GetStream("test"));
- }
-
[Fact]
public void ContainsKey_WithExistingKey_ShouldReturnTrue()
{
@@ -361,7 +293,7 @@ public void ContainsKey_WithExistingKey_ShouldReturnTrue()
packer.PackResources(_outputPath);
// Act
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
var exists = reader.ContainsKey("test");
// Assert
@@ -377,7 +309,7 @@ public void ContainsKey_WithNonExistentKey_ShouldReturnFalse()
packer.PackResources(_outputPath);
// Act
- using var reader = new ResourcePackageReader(_outputPath);
+ var reader = new ResourcePackageReader(_outputPath);
var exists = reader.ContainsKey("non_existent_key");
// Assert
diff --git a/LuYao.ResourcePacker/ResourcePackageReader.cs b/LuYao.ResourcePacker/ResourcePackageReader.cs
index 8969b32..792aabe 100644
--- a/LuYao.ResourcePacker/ResourcePackageReader.cs
+++ b/LuYao.ResourcePacker/ResourcePackageReader.cs
@@ -10,11 +10,10 @@ namespace LuYao.ResourcePacker
/// Provides functionality to read resources from a packaged resource file.
/// This class is thread-safe for concurrent read operations by creating independent FileStream instances per operation.
///
- public class ResourcePackageReader : IDisposable
+ public class ResourcePackageReader
{
private readonly string _filePath;
private readonly Dictionary _resourceIndex;
- private bool _disposed;
///
/// Initializes a new instance of the class.
@@ -86,9 +85,6 @@ public bool ContainsKey(string resourceKey)
/// A task that represents the asynchronous read operation.
public Task ReadResourceAsync(string resourceKey)
{
- if (_disposed)
- throw new ObjectDisposedException(nameof(ResourcePackageReader));
-
if (!_resourceIndex.TryGetValue(resourceKey, out var entry))
throw new KeyNotFoundException($"Resource with key '{resourceKey}' not found.");
@@ -142,9 +138,6 @@ public async Task ReadResourceAsStringAsync(string resourceKey, Encoding
/// The resource data as a byte array.
public byte[] ReadResource(string resourceKey)
{
- if (_disposed)
- throw new ObjectDisposedException(nameof(ResourcePackageReader));
-
if (!_resourceIndex.TryGetValue(resourceKey, out var entry))
throw new KeyNotFoundException($"Resource with key '{resourceKey}' not found.");
@@ -193,28 +186,17 @@ public string ReadResourceAsString(string resourceKey, Encoding encoding)
///
/// Gets a read-only stream for the specified resource.
+ /// This allows streaming large resources without loading all data into memory.
///
/// The key of the resource to read.
/// A read-only stream containing the resource data.
public Stream GetStream(string resourceKey)
{
- if (_disposed)
- throw new ObjectDisposedException(nameof(ResourcePackageReader));
-
if (!_resourceIndex.TryGetValue(resourceKey, out var entry))
throw new KeyNotFoundException($"Resource with key '{resourceKey}' not found.");
- // Read the resource data into memory and return a MemoryStream
- var buffer = ReadResource(resourceKey);
- return new MemoryStream(buffer, writable: false);
- }
-
- ///
- /// Releases the resources used by the .
- ///
- public void Dispose()
- {
- _disposed = true;
+ // Create a SubStream for streaming access without loading entire resource into memory
+ return new ResourceSubStream(_filePath, entry.Offset, entry.Length);
}
}
@@ -223,4 +205,129 @@ internal class ResourceEntry
public long Offset { get; set; }
public int Length { get; set; }
}
+
+ ///
+ /// A read-only stream that provides access to a portion of a resource package file.
+ /// This allows streaming large resources without loading all data into memory.
+ /// Each instance creates its own FileStream for thread-safe operation.
+ ///
+ internal class ResourceSubStream : Stream
+ {
+ private readonly string _filePath;
+ private readonly long _resourceOffset;
+ private readonly long _resourceLength;
+ private long _position;
+ private FileStream? _fileStream;
+ private bool _disposed;
+
+ public ResourceSubStream(string filePath, long offset, long length)
+ {
+ _filePath = filePath ?? throw new ArgumentNullException(nameof(filePath));
+ _resourceOffset = offset;
+ _resourceLength = length;
+ _position = 0;
+ }
+
+ private FileStream EnsureFileStream()
+ {
+ if (_fileStream == null)
+ {
+ _fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
+ }
+ return _fileStream;
+ }
+
+ public override bool CanRead => !_disposed;
+ public override bool CanSeek => !_disposed;
+ public override bool CanWrite => false;
+ public override long Length => _resourceLength;
+
+ public override long Position
+ {
+ get => _position;
+ set
+ {
+ if (_disposed)
+ throw new ObjectDisposedException(nameof(ResourceSubStream));
+ if (value < 0 || value > _resourceLength)
+ throw new ArgumentOutOfRangeException(nameof(value));
+ _position = value;
+ }
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ if (_disposed)
+ throw new ObjectDisposedException(nameof(ResourceSubStream));
+ if (buffer == null)
+ throw new ArgumentNullException(nameof(buffer));
+ if (offset < 0)
+ throw new ArgumentOutOfRangeException(nameof(offset));
+ if (count < 0)
+ throw new ArgumentOutOfRangeException(nameof(count));
+ if (buffer.Length - offset < count)
+ throw new ArgumentException("Invalid offset/count combination");
+
+ long remaining = _resourceLength - _position;
+ if (remaining <= 0)
+ return 0;
+
+ int toRead = (int)Math.Min(count, remaining);
+
+ var fs = EnsureFileStream();
+ fs.Seek(_resourceOffset + _position, SeekOrigin.Begin);
+ int bytesRead = fs.Read(buffer, offset, toRead);
+ _position += bytesRead;
+
+ return bytesRead;
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ if (_disposed)
+ throw new ObjectDisposedException(nameof(ResourceSubStream));
+
+ long newPosition = origin switch
+ {
+ SeekOrigin.Begin => offset,
+ SeekOrigin.Current => _position + offset,
+ SeekOrigin.End => _resourceLength + offset,
+ _ => throw new ArgumentException("Invalid seek origin", nameof(origin))
+ };
+
+ if (newPosition < 0 || newPosition > _resourceLength)
+ throw new ArgumentOutOfRangeException(nameof(offset));
+
+ _position = newPosition;
+ return _position;
+ }
+
+ public override void Flush()
+ {
+ // Read-only stream, nothing to flush
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException("Cannot set length on a read-only stream.");
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException("Cannot write to a read-only stream.");
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ _fileStream?.Dispose();
+ }
+ _disposed = true;
+ }
+ base.Dispose(disposing);
+ }
+ }
}
\ No newline at end of file
diff --git a/examples/ExampleProject/Program.cs b/examples/ExampleProject/Program.cs
index 5bc8c8c..b556379 100644
--- a/examples/ExampleProject/Program.cs
+++ b/examples/ExampleProject/Program.cs
@@ -22,7 +22,7 @@ static async Task Main(string[] args)
return;
}
- using var reader = new ResourcePackageReader(datFilePath);
+ var reader = new ResourcePackageReader(datFilePath);
Console.WriteLine($"Resource package loaded: {datFilePath}");
Console.WriteLine();