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();