From aede26f5c18b7b30fee853ea80db7bde1616e7f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:33:31 +0000 Subject: [PATCH 01/15] Initial plan From 70714c49744087f0bf8863f6f6a6920c2c6a2a43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:46:56 +0000 Subject: [PATCH 02/15] Add ARJ and ARC decompression support with SharpCompress 0.44.3 Co-authored-by: gfs <98900+gfs@users.noreply.github.com> --- .../ExtractorTests/ExpectedNumFilesTests.cs | 4 + .../ExtractorTests/MiniMagicTests.cs | 2 + .../TestData/TestDataArchives/TestData.arc | Bin 0 -> 88 bytes .../TestData/TestDataArchives/TestData.arj | Bin 0 -> 182 bytes RecursiveExtractor/Extractor.cs | 2 + RecursiveExtractor/Extractors/ArcExtractor.cs | 184 ++++++++++++++++++ RecursiveExtractor/Extractors/ArjExtractor.cs | 167 ++++++++++++++++ RecursiveExtractor/MiniMagic.cs | 19 ++ RecursiveExtractor/RecursiveExtractor.csproj | 4 +- nuget.config | 2 +- 10 files changed, 381 insertions(+), 3 deletions(-) create mode 100644 RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.arc create mode 100644 RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.arj create mode 100644 RecursiveExtractor/Extractors/ArcExtractor.cs create mode 100644 RecursiveExtractor/Extractors/ArjExtractor.cs diff --git a/RecursiveExtractor.Tests/ExtractorTests/ExpectedNumFilesTests.cs b/RecursiveExtractor.Tests/ExtractorTests/ExpectedNumFilesTests.cs index ab9544cf..1f7f6f24 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/ExpectedNumFilesTests.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/ExpectedNumFilesTests.cs @@ -43,6 +43,8 @@ public static IEnumerable ArchiveData new object[] { "TestDataArchivesNested.Zip", 54 }, new object[] { "UdfTest.iso", 3 }, new object[] { "UdfTestWithMultiSystem.iso", 3 }, + new object[] { "TestData.arj", 1 }, + new object[] { "TestData.arc", 1 }, // new object[] { "HfsSampleUDCO.dmg", 2 } }; } @@ -75,6 +77,8 @@ public static IEnumerable NoRecursionData new object[] { "EmptyFile.txt", 1 }, new object[] { "TestDataArchivesNested.Zip", 14 }, new object[] { "UdfTestWithMultiSystem.iso", 3 }, + new object[] { "TestData.arj", 1 }, + new object[] { "TestData.arc", 1 }, // new object[] { "HfsSampleUDCO.dmg", 2 } }; } diff --git a/RecursiveExtractor.Tests/ExtractorTests/MiniMagicTests.cs b/RecursiveExtractor.Tests/ExtractorTests/MiniMagicTests.cs index 016ef133..5258d76e 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/MiniMagicTests.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/MiniMagicTests.cs @@ -24,6 +24,8 @@ public class MiniMagicTests : BaseExtractorTestClass [DataRow("TestData.wim", ArchiveFileType.WIM)] [DataRow("Empty.vmdk", ArchiveFileType.VMDK)] [DataRow("HfsSampleUDCO.dmg", ArchiveFileType.DMG)] + [DataRow("TestData.arj", ArchiveFileType.ARJ)] + [DataRow("TestData.arc", ArchiveFileType.ARC)] [DataRow("EmptyFile.txt", ArchiveFileType.UNKNOWN)] public void TestMiniMagic(string fileName, ArchiveFileType expectedArchiveFileType) { diff --git a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.arc b/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.arc new file mode 100644 index 0000000000000000000000000000000000000000..0c99f274bc53ce84353f10f33d534fa1eefdcc5f GIT binary patch literal 88 zcmb1Q^2sks&DATZC}Ch=aIgdtHZe9^r=@^cU@3*ng5uI#g_Qi9{332tGScceTZjIoALU?Pwo3adl?u2Ku customExtractors) : this() /// public void SetDefaultExtractors() { + SetExtractor(ArchiveFileType.ARC, new ArcExtractor(this)); + SetExtractor(ArchiveFileType.ARJ, new ArjExtractor(this)); SetExtractor(ArchiveFileType.BZIP2, new BZip2Extractor(this)); SetExtractor(ArchiveFileType.DEB, new DebExtractor(this)); SetExtractor(ArchiveFileType.AR, new GnuArExtractor(this)); diff --git a/RecursiveExtractor/Extractors/ArcExtractor.cs b/RecursiveExtractor/Extractors/ArcExtractor.cs new file mode 100644 index 00000000..26c91fa5 --- /dev/null +++ b/RecursiveExtractor/Extractors/ArcExtractor.cs @@ -0,0 +1,184 @@ +using SharpCompress.Readers.Arc; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.CST.RecursiveExtractor.Extractors +{ + /// + /// The ARC Archive extractor implementation + /// + public class ArcExtractor : AsyncExtractorInterface + { + /// + /// The constructor takes the Extractor context for recursion. + /// + /// The Extractor context. + public ArcExtractor(Extractor context) + { + Context = context; + } + private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); + + internal Extractor Context { get; } + + /// + /// Safely gets the size of an entry, returning 0 if not available. + /// + private long GetEntrySize(SharpCompress.Common.IEntry entry) + { + try + { + return entry.Size; + } + catch (NotImplementedException) + { + return 0; + } + } + + /// + /// Extracts an ARC archive + /// + /// + public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) + { + ArcReader? arcReader = null; + try + { + arcReader = ArcReader.Open(fileEntry.Content, new SharpCompress.Readers.ReaderOptions() + { + LeaveStreamOpen = true + }); + } + catch (Exception e) + { + Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.ARC, fileEntry.FullPath, string.Empty, e.GetType()); + } + + if (arcReader != null) + { + using (arcReader) + { + while (arcReader.MoveToNextEntry()) + { + var entry = arcReader.Entry; + if (entry.IsDirectory) + { + continue; + } + + var entrySize = GetEntrySize(entry); + governor.CheckResourceGovernor(entrySize); + var name = entry.Key?.Replace('/', Path.DirectorySeparatorChar); + if (string.IsNullOrEmpty(name)) + { + Logger.Debug(Extractor.ENTRY_MISSING_NAME_ERROR_MESSAGE_STRING, ArchiveFileType.ARC, fileEntry.FullPath); + continue; + } + + var newFileEntry = await FileEntry.FromStreamAsync(name, arcReader.OpenEntryStream(), fileEntry, entry.CreatedTime, entry.LastModifiedTime, entry.LastAccessedTime, memoryStreamCutoff: options.MemoryStreamCutoff).ConfigureAwait(false); + if (newFileEntry != null) + { + if (options.Recurse || topLevel) + { + await foreach (var innerEntry in Context.ExtractAsync(newFileEntry, options, governor, false)) + { + yield return innerEntry; + } + } + else + { + yield return newFileEntry; + } + } + } + } + } + else + { + if (options.ExtractSelfOnFail) + { + fileEntry.EntryStatus = FileEntryStatus.FailedArchive; + yield return fileEntry; + } + } + } + + /// + /// Extracts an ARC archive + /// + /// + public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) + { + ArcReader? arcReader = null; + try + { + arcReader = ArcReader.Open(fileEntry.Content, new SharpCompress.Readers.ReaderOptions() + { + LeaveStreamOpen = true + }); + } + catch (Exception e) + { + Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.ARC, fileEntry.FullPath, string.Empty, e.GetType()); + } + + if (arcReader != null) + { + using (arcReader) + { + while (arcReader.MoveToNextEntry()) + { + var entry = arcReader.Entry; + if (entry.IsDirectory) + { + continue; + } + + var entrySize = GetEntrySize(entry); + governor.CheckResourceGovernor(entrySize); + FileEntry? newFileEntry = null; + try + { + var stream = arcReader.OpenEntryStream(); + var name = entry.Key?.Replace('/', Path.DirectorySeparatorChar); + if (string.IsNullOrEmpty(name)) + { + Logger.Debug(Extractor.ENTRY_MISSING_NAME_ERROR_MESSAGE_STRING, ArchiveFileType.ARC, fileEntry.FullPath); + continue; + } + newFileEntry = new FileEntry(name, stream, fileEntry, false, entry.CreatedTime, entry.LastModifiedTime, entry.LastAccessedTime, memoryStreamCutoff: options.MemoryStreamCutoff); + } + catch (Exception e) + { + Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.ARC, fileEntry.FullPath, entry.Key, e.GetType()); + } + if (newFileEntry != null) + { + if (options.Recurse || topLevel) + { + foreach (var innerEntry in Context.Extract(newFileEntry, options, governor, false)) + { + yield return innerEntry; + } + } + else + { + yield return newFileEntry; + } + } + } + } + } + else + { + if (options.ExtractSelfOnFail) + { + fileEntry.EntryStatus = FileEntryStatus.FailedArchive; + yield return fileEntry; + } + } + } + } +} diff --git a/RecursiveExtractor/Extractors/ArjExtractor.cs b/RecursiveExtractor/Extractors/ArjExtractor.cs new file mode 100644 index 00000000..88c38cf2 --- /dev/null +++ b/RecursiveExtractor/Extractors/ArjExtractor.cs @@ -0,0 +1,167 @@ +using SharpCompress.Readers.Arj; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.CST.RecursiveExtractor.Extractors +{ + /// + /// The ARJ Archive extractor implementation + /// + public class ArjExtractor : AsyncExtractorInterface + { + /// + /// The constructor takes the Extractor context for recursion. + /// + /// The Extractor context. + public ArjExtractor(Extractor context) + { + Context = context; + } + private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); + + internal Extractor Context { get; } + + /// + /// Extracts an ARJ archive + /// + /// + public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) + { + ArjReader? arjReader = null; + try + { + arjReader = ArjReader.Open(fileEntry.Content, new SharpCompress.Readers.ReaderOptions() + { + LeaveStreamOpen = true + }); + } + catch (Exception e) + { + Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.ARJ, fileEntry.FullPath, string.Empty, e.GetType()); + } + + if (arjReader != null) + { + using (arjReader) + { + while (arjReader.MoveToNextEntry()) + { + var entry = arjReader.Entry; + if (entry.IsDirectory) + { + continue; + } + + governor.CheckResourceGovernor(entry.Size); + var name = entry.Key?.Replace('/', Path.DirectorySeparatorChar); + if (string.IsNullOrEmpty(name)) + { + Logger.Debug(Extractor.ENTRY_MISSING_NAME_ERROR_MESSAGE_STRING, ArchiveFileType.ARJ, fileEntry.FullPath); + continue; + } + + var newFileEntry = await FileEntry.FromStreamAsync(name, arjReader.OpenEntryStream(), fileEntry, entry.CreatedTime, entry.LastModifiedTime, entry.LastAccessedTime, memoryStreamCutoff: options.MemoryStreamCutoff).ConfigureAwait(false); + if (newFileEntry != null) + { + if (options.Recurse || topLevel) + { + await foreach (var innerEntry in Context.ExtractAsync(newFileEntry, options, governor, false)) + { + yield return innerEntry; + } + } + else + { + yield return newFileEntry; + } + } + } + } + } + else + { + if (options.ExtractSelfOnFail) + { + fileEntry.EntryStatus = FileEntryStatus.FailedArchive; + yield return fileEntry; + } + } + } + + /// + /// Extracts an ARJ archive + /// + /// + public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) + { + ArjReader? arjReader = null; + try + { + arjReader = ArjReader.Open(fileEntry.Content, new SharpCompress.Readers.ReaderOptions() + { + LeaveStreamOpen = true + }); + } + catch (Exception e) + { + Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.ARJ, fileEntry.FullPath, string.Empty, e.GetType()); + } + + if (arjReader != null) + { + using (arjReader) + { + while (arjReader.MoveToNextEntry()) + { + var entry = arjReader.Entry; + if (entry.IsDirectory) + { + continue; + } + + governor.CheckResourceGovernor(entry.Size); + FileEntry? newFileEntry = null; + try + { + var stream = arjReader.OpenEntryStream(); + var name = entry.Key?.Replace('/', Path.DirectorySeparatorChar); + if (string.IsNullOrEmpty(name)) + { + Logger.Debug(Extractor.ENTRY_MISSING_NAME_ERROR_MESSAGE_STRING, ArchiveFileType.ARJ, fileEntry.FullPath); + continue; + } + newFileEntry = new FileEntry(name, stream, fileEntry, false, entry.CreatedTime, entry.LastModifiedTime, entry.LastAccessedTime, memoryStreamCutoff: options.MemoryStreamCutoff); + } + catch (Exception e) + { + Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.ARJ, fileEntry.FullPath, entry.Key, e.GetType()); + } + if (newFileEntry != null) + { + if (options.Recurse || topLevel) + { + foreach (var innerEntry in Context.Extract(newFileEntry, options, governor, false)) + { + yield return innerEntry; + } + } + else + { + yield return newFileEntry; + } + } + } + } + } + else + { + if (options.ExtractSelfOnFail) + { + fileEntry.EntryStatus = FileEntryStatus.FailedArchive; + yield return fileEntry; + } + } + } + } +} diff --git a/RecursiveExtractor/MiniMagic.cs b/RecursiveExtractor/MiniMagic.cs index 932ac5db..e314813c 100644 --- a/RecursiveExtractor/MiniMagic.cs +++ b/RecursiveExtractor/MiniMagic.cs @@ -85,6 +85,14 @@ public enum ArchiveFileType /// DMG, /// + /// An ARJ formatted file. + /// + ARJ, + /// + /// An ARC formatted file. + /// + ARC, + /// /// Unused. /// INVALID @@ -173,6 +181,17 @@ public static ArchiveFileType DetectFileType(Stream fileStream) { return ArchiveFileType.P7ZIP; } + // ARJ archive format https://en.wikipedia.org/wiki/ARJ + if (buffer[0] == 0x60 && buffer[1] == 0xEA) + { + return ArchiveFileType.ARJ; + } + // ARC archive format https://en.wikipedia.org/wiki/ARC_(file_format) + // First byte is marker 0x1A, second byte is compression method (0x00-0x0B valid for standard ARC) + if (buffer[0] == 0x1A && buffer[1] >= 0x00 && buffer[1] <= 0x0B) + { + return ArchiveFileType.ARC; + } if (Encoding.ASCII.GetString(buffer[0..8]) == "MSWIM\0\0\0" || Encoding.ASCII.GetString(buffer[0..8]) == "WLPWM\0\0\0") { return ArchiveFileType.WIM; diff --git a/RecursiveExtractor/RecursiveExtractor.csproj b/RecursiveExtractor/RecursiveExtractor.csproj index a45aeb8c..9185ad66 100644 --- a/RecursiveExtractor/RecursiveExtractor.csproj +++ b/RecursiveExtractor/RecursiveExtractor.csproj @@ -13,7 +13,7 @@ Enable false true - RecursiveExtractor is able to process the following formats: ar, bzip2, deb, gzip, iso, tar, vhd, vhdx, vmdk, wim, xzip, and zip. RecursiveExtractor automatically detects the archive type and fails gracefully when attempting to process malformed content. + RecursiveExtractor is able to process the following formats: ar, arc, arj, bzip2, deb, gzip, iso, tar, vhd, vhdx, vmdk, wim, xzip, and zip. RecursiveExtractor automatically detects the archive type and fails gracefully when attempting to process malformed content. Microsoft.CST.RecursiveExtractor unzip extract extractor 0.0.0-placeholder @@ -48,7 +48,7 @@ - + diff --git a/nuget.config b/nuget.config index 227ad0ce..d8f90e26 100644 --- a/nuget.config +++ b/nuget.config @@ -2,6 +2,6 @@ - + \ No newline at end of file From 3ca4f2c0c6de1d43641e5332a9b84496b0a45ea1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:50:18 +0000 Subject: [PATCH 03/15] Simplify redundant ARC magic byte check Co-authored-by: gfs <98900+gfs@users.noreply.github.com> --- RecursiveExtractor/MiniMagic.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RecursiveExtractor/MiniMagic.cs b/RecursiveExtractor/MiniMagic.cs index e314813c..5aecceb9 100644 --- a/RecursiveExtractor/MiniMagic.cs +++ b/RecursiveExtractor/MiniMagic.cs @@ -188,7 +188,7 @@ public static ArchiveFileType DetectFileType(Stream fileStream) } // ARC archive format https://en.wikipedia.org/wiki/ARC_(file_format) // First byte is marker 0x1A, second byte is compression method (0x00-0x0B valid for standard ARC) - if (buffer[0] == 0x1A && buffer[1] >= 0x00 && buffer[1] <= 0x0B) + if (buffer[0] == 0x1A && buffer[1] <= 0x0B) { return ArchiveFileType.ARC; } From 56933558f1b79ab5beb3219588ecd98460751d08 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:21:01 +0000 Subject: [PATCH 04/15] Revert nuget.config to original Microsoft feed Co-authored-by: gfs <98900+gfs@users.noreply.github.com> --- nuget.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nuget.config b/nuget.config index d8f90e26..227ad0ce 100644 --- a/nuget.config +++ b/nuget.config @@ -2,6 +2,6 @@ - + \ No newline at end of file From d8b877da816114215c58f6eda0d8498309fe1936 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 21:56:52 +0000 Subject: [PATCH 05/15] Add CopyToOutputDirectory for TestData.arj and TestData.arc test files Co-authored-by: gfs <98900+gfs@users.noreply.github.com> --- RecursiveExtractor.Tests/RecursiveExtractor.Tests.csproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RecursiveExtractor.Tests/RecursiveExtractor.Tests.csproj b/RecursiveExtractor.Tests/RecursiveExtractor.Tests.csproj index fe84518b..ab4cbd61 100644 --- a/RecursiveExtractor.Tests/RecursiveExtractor.Tests.csproj +++ b/RecursiveExtractor.Tests/RecursiveExtractor.Tests.csproj @@ -153,6 +153,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest From 6b9d02d84db460bf5476014e5531944371fef3d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 22:10:12 +0000 Subject: [PATCH 06/15] Track extracted bytes after decompression in ArcExtractor for overflow protection Co-authored-by: gfs <98900+gfs@users.noreply.github.com> --- RecursiveExtractor/Extractors/ArcExtractor.cs | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/RecursiveExtractor/Extractors/ArcExtractor.cs b/RecursiveExtractor/Extractors/ArcExtractor.cs index 26c91fa5..a3d2e44c 100644 --- a/RecursiveExtractor/Extractors/ArcExtractor.cs +++ b/RecursiveExtractor/Extractors/ArcExtractor.cs @@ -22,21 +22,6 @@ public ArcExtractor(Extractor context) internal Extractor Context { get; } - /// - /// Safely gets the size of an entry, returning 0 if not available. - /// - private long GetEntrySize(SharpCompress.Common.IEntry entry) - { - try - { - return entry.Size; - } - catch (NotImplementedException) - { - return 0; - } - } - /// /// Extracts an ARC archive /// @@ -68,8 +53,6 @@ public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, Extra continue; } - var entrySize = GetEntrySize(entry); - governor.CheckResourceGovernor(entrySize); var name = entry.Key?.Replace('/', Path.DirectorySeparatorChar); if (string.IsNullOrEmpty(name)) { @@ -80,6 +63,10 @@ public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, Extra var newFileEntry = await FileEntry.FromStreamAsync(name, arcReader.OpenEntryStream(), fileEntry, entry.CreatedTime, entry.LastModifiedTime, entry.LastAccessedTime, memoryStreamCutoff: options.MemoryStreamCutoff).ConfigureAwait(false); if (newFileEntry != null) { + // SharpCompress ARC does not expose entry sizes, so we check the resource governor + // after extraction using the actual decompressed content length. + governor.CheckResourceGovernor(newFileEntry.Content.Length); + if (options.Recurse || topLevel) { await foreach (var innerEntry in Context.ExtractAsync(newFileEntry, options, governor, false)) @@ -136,8 +123,6 @@ public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions opti continue; } - var entrySize = GetEntrySize(entry); - governor.CheckResourceGovernor(entrySize); FileEntry? newFileEntry = null; try { @@ -156,6 +141,10 @@ public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions opti } if (newFileEntry != null) { + // SharpCompress ARC does not expose entry sizes, so we check the resource governor + // after extraction using the actual decompressed content length. + governor.CheckResourceGovernor(newFileEntry.Content.Length); + if (options.Recurse || topLevel) { foreach (var innerEntry in Context.Extract(newFileEntry, options, governor, false)) From b86ac56927028624ed4032309f1147cba83660c9 Mon Sep 17 00:00:00 2001 From: Giulia Stocco <98900+gfs@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:28:39 -0800 Subject: [PATCH 07/15] Improve ARC Detection fidelityl Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- RecursiveExtractor/MiniMagic.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RecursiveExtractor/MiniMagic.cs b/RecursiveExtractor/MiniMagic.cs index 5aecceb9..76c4b294 100644 --- a/RecursiveExtractor/MiniMagic.cs +++ b/RecursiveExtractor/MiniMagic.cs @@ -187,8 +187,8 @@ public static ArchiveFileType DetectFileType(Stream fileStream) return ArchiveFileType.ARJ; } // ARC archive format https://en.wikipedia.org/wiki/ARC_(file_format) - // First byte is marker 0x1A, second byte is compression method (0x00-0x0B valid for standard ARC) - if (buffer[0] == 0x1A && buffer[1] <= 0x0B) + // First byte is marker 0x1A, second byte is compression method (0x01-0x09 or 0x7F valid for standard ARC) + if (buffer[0] == 0x1A && ((buffer[1] >= 0x01 && buffer[1] <= 0x09) || buffer[1] == 0x7F)) { return ArchiveFileType.ARC; } From a46c61498bc9f1206733b7c74b1aaf32fc932688 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 21:53:50 +0000 Subject: [PATCH 08/15] Initial plan From 36e1272cec732902ce03cf766b861fe69ae75299 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 21:58:03 +0000 Subject: [PATCH 09/15] Initial analysis Co-authored-by: gfs <98900+gfs@users.noreply.github.com> --- nuget.config | 6 +++--- nuget.config.backup | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 nuget.config.backup diff --git a/nuget.config b/nuget.config index 227ad0ce..765346e5 100644 --- a/nuget.config +++ b/nuget.config @@ -1,7 +1,7 @@ - + - + - \ No newline at end of file + diff --git a/nuget.config.backup b/nuget.config.backup new file mode 100644 index 00000000..227ad0ce --- /dev/null +++ b/nuget.config.backup @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file From b5ca6dbb4c87d557df81feb821ae4abc582e3567 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 22:13:02 +0000 Subject: [PATCH 10/15] Changes before error encountered Co-authored-by: gfs <98900+gfs@users.noreply.github.com> --- .editorconfig | 1 + Pipelines/recursive-extractor-pr.yml | 4 +-- Pipelines/recursive-extractor-release.yml | 4 +-- RecursiveExtractor.Tests/AssemblyInfo.cs | 3 +++ .../ExtractorTests/BaseExtractorTestClass.cs | 2 ++ .../ExtractorTests/CustomExtractorTests.cs | 26 ++++++++++--------- .../ExtractorTests/DisposeBehaviorTests.cs | 16 +++++------- .../ExtractorTests/EncryptedArchiveTests.cs | 8 +++--- .../ExtractorTests/FilterTests.cs | 14 +++++----- .../ExtractorTests/MiniMagicTests.cs | 2 +- .../ExtractorTests/MiscTests.cs | 22 +++++++++------- .../ExtractorTests/TestQuinesAndSlip.cs | 14 ++++++---- .../ExtractorTests/TimeOutTests.cs | 10 +++---- RecursiveExtractor.Tests/SanitizePathTests.cs | 4 +-- RecursiveExtractor.Tests/testconfig.json | 8 ++++++ nuget.config | 6 ++--- nuget.config.backup | 7 ----- 17 files changed, 80 insertions(+), 71 deletions(-) create mode 100644 RecursiveExtractor.Tests/AssemblyInfo.cs create mode 100644 RecursiveExtractor.Tests/testconfig.json delete mode 100644 nuget.config.backup diff --git a/.editorconfig b/.editorconfig index f195c50b..d03ac215 100644 --- a/.editorconfig +++ b/.editorconfig @@ -191,6 +191,7 @@ dotnet_naming_symbols.non_field_members.required_modifiers = # Naming styles dotnet_naming_style.pascal_case.required_prefix = + dotnet_naming_style.pascal_case.required_suffix = dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case diff --git a/Pipelines/recursive-extractor-pr.yml b/Pipelines/recursive-extractor-pr.yml index 3476419e..0baf2e62 100644 --- a/Pipelines/recursive-extractor-pr.yml +++ b/Pipelines/recursive-extractor-pr.yml @@ -42,7 +42,7 @@ extends: poolName: MSSecurity-1ES-Build-Agents-Pool poolImage: MSSecurity-1ES-Windows-2022 poolOs: windows - dotnetTestArgs: '-- --coverage --report-trx' + dotnetTestArgs: '--logger trx --results-directory $(Build.SourcesDirectory)/TestResults -- --coverage' includeNuGetOrg: false nugetFeedsToUse: 'config' nugetConfigPath: 'nuget.config' @@ -57,7 +57,7 @@ extends: poolName: MSSecurity-1ES-Build-Agents-Pool poolImage: MSSecurity-1ES-Windows-2022 poolOs: windows - dotnetTestArgs: '-- --coverage --report-trx' + dotnetTestArgs: '--logger trx --results-directory $(Build.SourcesDirectory)/TestResults -- --coverage' includeNuGetOrg: false nugetFeedsToUse: 'config' nugetConfigPath: 'nuget.config' diff --git a/Pipelines/recursive-extractor-release.yml b/Pipelines/recursive-extractor-release.yml index b2a85fc8..9aca2257 100644 --- a/Pipelines/recursive-extractor-release.yml +++ b/Pipelines/recursive-extractor-release.yml @@ -43,7 +43,7 @@ extends: poolName: MSSecurity-1ES-Build-Agents-Pool poolImage: MSSecurity-1ES-Windows-2022 poolOs: windows - dotnetTestArgs: '-- --coverage --report-trx' + dotnetTestArgs: '--logger trx --results-directory $(Build.SourcesDirectory)/TestResults -- --coverage' includeNuGetOrg: false nugetFeedsToUse: 'config' nugetConfigPath: 'nuget.config' @@ -57,7 +57,7 @@ extends: poolName: MSSecurity-1ES-Build-Agents-Pool poolImage: MSSecurity-1ES-Windows-2022 poolOs: windows - dotnetTestArgs: '-- --coverage --report-trx' + dotnetTestArgs: '--logger trx --results-directory $(Build.SourcesDirectory)/TestResults -- --coverage' includeNuGetOrg: false nugetFeedsToUse: 'config' nugetConfigPath: 'nuget.config' diff --git a/RecursiveExtractor.Tests/AssemblyInfo.cs b/RecursiveExtractor.Tests/AssemblyInfo.cs new file mode 100644 index 00000000..38707aef --- /dev/null +++ b/RecursiveExtractor.Tests/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.MethodLevel)] diff --git a/RecursiveExtractor.Tests/ExtractorTests/BaseExtractorTestClass.cs b/RecursiveExtractor.Tests/ExtractorTests/BaseExtractorTestClass.cs index d223a1a1..7c301b58 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/BaseExtractorTestClass.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/BaseExtractorTestClass.cs @@ -8,6 +8,8 @@ namespace RecursiveExtractor.Tests.ExtractorTests; public class BaseExtractorTestClass { + public TestContext TestContext { get; set; } = null!; + [ClassCleanup] public static void ClassCleanup() { diff --git a/RecursiveExtractor.Tests/ExtractorTests/CustomExtractorTests.cs b/RecursiveExtractor.Tests/ExtractorTests/CustomExtractorTests.cs index 547ba87d..048cf04f 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/CustomExtractorTests.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/CustomExtractorTests.cs @@ -12,6 +12,8 @@ namespace RecursiveExtractor.Tests.ExtractorTests; [TestClass] public class CustomExtractorTests { + public TestContext TestContext { get; set; } = null!; + /// /// A simple test custom extractor that extracts files with a specific magic number /// For testing purposes, it recognizes files starting with "CUSTOM1" @@ -127,7 +129,7 @@ public void Constructor_WithCustomExtractors_RegistersExtractors() var customExtractor = new TestCustomExtractor(null!); var extractor = new Extractor(new[] { customExtractor }); - Assert.AreEqual(1, extractor.CustomExtractors.Count); + Assert.HasCount(extractor.CustomExtractors, 1); } [TestMethod] @@ -137,7 +139,7 @@ public void Constructor_WithMultipleCustomExtractors_RegistersAll() var customExtractor2 = new SecondTestCustomExtractor(null!); var extractor = new Extractor(new ICustomAsyncExtractor[] { customExtractor1, customExtractor2 }); - Assert.AreEqual(2, extractor.CustomExtractors.Count); + Assert.HasCount(extractor.CustomExtractors, 2); } [TestMethod] @@ -146,7 +148,7 @@ public void Constructor_WithNullInCollection_IgnoresNull() var customExtractor = new TestCustomExtractor(null!); var extractor = new Extractor(new ICustomAsyncExtractor[] { customExtractor, null! }); - Assert.AreEqual(1, extractor.CustomExtractors.Count); + Assert.HasCount(extractor.CustomExtractors, 1); } [TestMethod] @@ -154,7 +156,7 @@ public void Constructor_WithNullCollection_CreatesEmptyExtractor() { var extractor = new Extractor((IEnumerable)null!); - Assert.AreEqual(0, extractor.CustomExtractors.Count); + Assert.IsEmpty(extractor.CustomExtractors); } [TestMethod] @@ -167,7 +169,7 @@ public void Extract_WithMatchingCustomExtractor_UsesCustomExtractor() var testData = System.Text.Encoding.ASCII.GetBytes("CUSTOM1 This is test data"); var results = extractor.Extract("test.custom", testData).ToList(); - Assert.AreEqual(1, results.Count); + Assert.HasCount(results, 1); Assert.AreEqual("extracted_from_custom.txt", results[0].Name); // Read the content to verify it was processed by our custom extractor @@ -185,9 +187,9 @@ public async Task ExtractAsync_WithMatchingCustomExtractor_UsesCustomExtractor() // Create a test file with the custom magic bytes var testData = System.Text.Encoding.ASCII.GetBytes("CUSTOM1 This is test data"); - var results = await extractor.ExtractAsync("test.custom", testData).ToListAsync(); + var results = await extractor.ExtractAsync("test.custom", testData).ToListAsync(TestContext.CancellationTokenSource.Token); - Assert.AreEqual(1, results.Count); + Assert.HasCount(results, 1); Assert.AreEqual("extracted_from_custom.txt", results[0].Name); // Read the content to verify it was processed by our custom extractor @@ -208,7 +210,7 @@ public void Extract_WithoutMatchingCustomExtractor_ReturnsOriginalFile() var results = extractor.Extract("test.txt", testData).ToList(); // Should return the original file since no custom extractor matched - Assert.AreEqual(1, results.Count); + Assert.HasCount(results, 1); Assert.AreEqual("test.txt", results[0].Name); // Verify it's the original content @@ -230,13 +232,13 @@ public void Extract_MultipleCustomExtractors_UsesCorrectOne() // Test with first custom format var testData1 = System.Text.Encoding.ASCII.GetBytes("CUSTOM1 data"); var results1 = extractor.Extract("test1.custom", testData1).ToList(); - Assert.AreEqual(1, results1.Count); + Assert.HasCount(results1, 1); Assert.AreEqual("extracted_from_custom.txt", results1[0].Name); // Test with second custom format var testData2 = System.Text.Encoding.ASCII.GetBytes("CUSTOM2 data"); var results2 = extractor.Extract("test2.custom", testData2).ToList(); - Assert.AreEqual(1, results2.Count); + Assert.HasCount(results2, 1); Assert.AreEqual("extracted_from_second_custom.txt", results2[0].Name); } @@ -250,7 +252,7 @@ public void Extract_NoCustomExtractors_ReturnsOriginalFile() var results = extractor.Extract("test.custom", testData).ToList(); // Should return the original file since no custom extractor is registered - Assert.AreEqual(1, results.Count); + Assert.HasCount(results, 1); Assert.AreEqual("test.custom", results[0].Name); } @@ -267,7 +269,7 @@ public void Extract_CustomExtractorForKnownFormat_UsesBuiltInExtractor() var results = extractor.Extract(path).ToList(); // Should extract the ZIP normally, not use the custom extractor - Assert.IsTrue(results.Count > 0); + Assert.IsGreaterThan(results.Count, 0); Assert.IsTrue(results.Any(r => r.Name.Contains("EmptyFile"))); } } diff --git a/RecursiveExtractor.Tests/ExtractorTests/DisposeBehaviorTests.cs b/RecursiveExtractor.Tests/ExtractorTests/DisposeBehaviorTests.cs index bab30610..08b9b91c 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/DisposeBehaviorTests.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/DisposeBehaviorTests.cs @@ -11,7 +11,7 @@ namespace RecursiveExtractor.Tests.ExtractorTests; [TestClass] public class DisposeBehaviorTests : BaseExtractorTestClass { - [DataTestMethod] + [TestMethod] [DataRow("TestData.7z", 3, false)] [DataRow("TestData.tar", 6, false)] [DataRow("TestData.rar", 3, false)] @@ -43,8 +43,6 @@ public class DisposeBehaviorTests : BaseExtractorTestClass [DataRow("EmptyFile.txt", 1, true)] [DataRow("TestDataArchivesNested.Zip", 54, true)] [DataRow("TestDataArchivesNested.Zip", 54, false)] - [DataRow("TestDataArchivesNested.Zip", 54, true)] - [DataRow("TestDataArchivesNested.Zip", 54, false)] public void ExtractArchiveAndDisposeWhileEnumerating(string fileName, int expectedNumFiles = 3, bool parallel = false) { @@ -63,11 +61,11 @@ public void ExtractArchiveAndDisposeWhileEnumerating(string fileName, int expect Assert.AreEqual(expectedNumFiles, disposedResults.Count); foreach (var disposedResult in disposedResults) { - Assert.ThrowsException(() => disposedResult.Content.Position); + Assert.ThrowsExactly(() => disposedResult.Content.Position); } } - [DataTestMethod] + [TestMethod] [DataRow("TestData.7z", 3, false)] [DataRow("TestData.tar", 6, false)] [DataRow("TestData.rar", 3, false)] @@ -99,8 +97,6 @@ public void ExtractArchiveAndDisposeWhileEnumerating(string fileName, int expect [DataRow("EmptyFile.txt", 1, true)] [DataRow("TestDataArchivesNested.Zip", 54, true)] [DataRow("TestDataArchivesNested.Zip", 54, false)] - [DataRow("TestDataArchivesNested.Zip", 54, true)] - [DataRow("TestDataArchivesNested.Zip", 54, false)] public async Task ExtractArchiveAndDisposeWhileEnumeratingAsync(string fileName, int expectedNumFiles = 3, bool parallel = false) { @@ -119,11 +115,11 @@ public async Task ExtractArchiveAndDisposeWhileEnumeratingAsync(string fileName, Assert.AreEqual(expectedNumFiles, disposedResults.Count); foreach (var disposedResult in disposedResults) { - Assert.ThrowsException(() => disposedResult.Content.Position); + Assert.ThrowsExactly(() => disposedResult.Content.Position); } } - [DataTestMethod] + [TestMethod] [DataRow("TestData.zip")] public void EnsureDisposedWithExtractToDirectory(string fileName) { @@ -146,7 +142,7 @@ public void EnsureDisposedWithExtractToDirectory(string fileName) } } - [DataTestMethod] + [TestMethod] [DataRow("TestData.zip")] public async Task EnsureDisposedWithExtractToDirectoryAsync(string fileName) { diff --git a/RecursiveExtractor.Tests/ExtractorTests/EncryptedArchiveTests.cs b/RecursiveExtractor.Tests/ExtractorTests/EncryptedArchiveTests.cs index 9d034e73..05b7d9e2 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/EncryptedArchiveTests.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/EncryptedArchiveTests.cs @@ -11,7 +11,7 @@ namespace RecursiveExtractor.Tests.ExtractorTests; [TestClass] public class EncryptedArchiveTests : BaseExtractorTestClass { - [DataTestMethod] + [TestMethod] [DataRow("TestDataEncryptedZipCrypto.zip")] [DataRow("TestDataEncryptedAes.zip")] [DataRow("TestDataEncrypted.7z")] @@ -26,7 +26,7 @@ public void FileTypeSetCorrectlyForEncryptedArchives(string fileName, int expect Assert.AreEqual(FileEntryStatus.EncryptedArchive, results.First().EntryStatus); } - [DataTestMethod] + [TestMethod] [DataRow("TestDataEncryptedZipCrypto.zip")] [DataRow("TestDataEncryptedAes.zip")] [DataRow("TestDataEncrypted.7z")] @@ -46,7 +46,7 @@ public async Task FileTypeSetCorrectlyForEncryptedArchivesAsync(string fileName, Assert.AreEqual(FileEntryStatus.EncryptedArchive, results.First().EntryStatus); } - [DataTestMethod] + [TestMethod] [DataRow("TestDataEncryptedZipCrypto.zip")] [DataRow("TestDataEncryptedAes.zip")] [DataRow("TestDataEncrypted.7z")] @@ -64,7 +64,7 @@ public void ExtractEncryptedArchive(string fileName, int expectedNumFiles = 3) Assert.AreEqual(0, results.Count(x => x.EntryStatus == FileEntryStatus.EncryptedArchive || x.EntryStatus == FileEntryStatus.FailedArchive)); } - [DataTestMethod] + [TestMethod] [DataRow("TestDataEncryptedZipCrypto.zip")] [DataRow("TestDataEncryptedAes.zip")] [DataRow("TestDataEncrypted.7z")] diff --git a/RecursiveExtractor.Tests/ExtractorTests/FilterTests.cs b/RecursiveExtractor.Tests/ExtractorTests/FilterTests.cs index 4fcc28aa..c5828a0f 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/FilterTests.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/FilterTests.cs @@ -10,7 +10,7 @@ namespace RecursiveExtractor.Tests.ExtractorTests; [TestClass] public class FilterTests : BaseExtractorTestClass { - [DataTestMethod] + [TestMethod] [DataRow("TestData.zip")] [DataRow("TestData.7z")] [DataRow("TestData.tar")] @@ -42,7 +42,7 @@ public async Task ExtractArchiveAsyncAllowFiltered(string fileName, int expected Assert.AreEqual(expectedNumFiles, numResults); } - [DataTestMethod] + [TestMethod] [DataRow("TestData.zip")] [DataRow("TestData.7z")] [DataRow("TestData.tar")] @@ -68,7 +68,7 @@ public void ExtractArchiveAllowFiltered(string fileName, int expectedNumFiles = Assert.AreEqual(expectedNumFiles, results.Count()); } - [DataTestMethod] + [TestMethod] [DataRow("TestData.zip")] [DataRow("TestData.7z")] [DataRow("TestData.tar")] @@ -94,7 +94,7 @@ public void ExtractArchiveParallelAllowFiltered(string fileName, int expectedNum Assert.AreEqual(expectedNumFiles, results.Count()); } - [DataTestMethod] + [TestMethod] [DataRow("TestData.zip", 4)] [DataRow("TestData.7z")] [DataRow("TestData.tar", 5)] @@ -119,7 +119,7 @@ public void ExtractArchiveDenyFiltered(string fileName, int expectedNumFiles = 2 Assert.AreEqual(expectedNumFiles, results.Count()); } - [DataTestMethod] + [TestMethod] [DataRow("TestData.zip", 4)] [DataRow("TestData.7z")] [DataRow("TestData.tar", 5)] @@ -145,7 +145,7 @@ public void ExtractArchiveParallelDenyFiltered(string fileName, int expectedNumF Assert.AreEqual(expectedNumFiles, results.Count()); } - [DataTestMethod] + [TestMethod] [DataRow("TestData.zip", 4)] [DataRow("TestData.7z")] [DataRow("TestData.tar", 5)] @@ -177,7 +177,7 @@ public async Task ExtractArchiveAsyncDenyFiltered(string fileName, int expectedN Assert.AreEqual(expectedNumFiles, numResults); } - [DataTestMethod] + [TestMethod] [DataRow(ArchiveFileType.ZIP, new[] { ArchiveFileType.ZIP }, new ArchiveFileType[] { }, false)] [DataRow(ArchiveFileType.ZIP, new[] { ArchiveFileType.TAR }, new ArchiveFileType[] { }, true)] [DataRow(ArchiveFileType.ZIP, new ArchiveFileType[] { }, new[] { ArchiveFileType.ZIP }, true)] diff --git a/RecursiveExtractor.Tests/ExtractorTests/MiniMagicTests.cs b/RecursiveExtractor.Tests/ExtractorTests/MiniMagicTests.cs index 5258d76e..f0cf3fb8 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/MiniMagicTests.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/MiniMagicTests.cs @@ -7,7 +7,7 @@ namespace RecursiveExtractor.Tests.ExtractorTests; [TestClass] public class MiniMagicTests : BaseExtractorTestClass { - [DataTestMethod] + [TestMethod] [DataRow("TestData.zip", ArchiveFileType.ZIP)] [DataRow("TestData.7z", ArchiveFileType.P7ZIP)] [DataRow("TestData.Tar", ArchiveFileType.TAR)] diff --git a/RecursiveExtractor.Tests/ExtractorTests/MiscTests.cs b/RecursiveExtractor.Tests/ExtractorTests/MiscTests.cs index e375904a..c06a75c2 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/MiscTests.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/MiscTests.cs @@ -10,7 +10,9 @@ namespace RecursiveExtractor.Tests.ExtractorTests; [TestClass] public class MiscTests { - [DataTestMethod] + public TestContext TestContext { get; set; } = null!; + + [TestMethod] [DataRow("TestDataCorrupt.tar", false, 0, 1)] [DataRow("TestDataCorrupt.tar", true, 1, 1)] [DataRow("TestDataCorrupt.tar.zip", false, 0, 2)] @@ -20,7 +22,7 @@ public async Task ExtractCorruptArchiveAsync(string fileName, bool requireTopLev var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); var results = await extractor.ExtractAsync(path, - new ExtractorOptions() { RequireTopLevelToBeArchive = requireTopLevelToBeArchive }).ToListAsync(); + new ExtractorOptions() { RequireTopLevelToBeArchive = requireTopLevelToBeArchive }).ToListAsync(TestContext.CancellationTokenSource.Token); Assert.AreEqual(expectedNumFiles, results.Count); @@ -28,23 +30,23 @@ public async Task ExtractCorruptArchiveAsync(string fileName, bool requireTopLev Assert.AreEqual(expectedNumFailures, actualNumberOfFailedArchives); } - [DataTestMethod] + [TestMethod] [DataRow("Lorem.txt", true, 1)] [DataRow("Lorem.txt", false, 0)] public async Task ExtractFlatFileAsync(string fileName, bool requireTopLevelToBeArchive, int expectedNumFailures) { var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestData", fileName); - var results = await extractor.ExtractAsync(path, new ExtractorOptions(){ RequireTopLevelToBeArchive = requireTopLevelToBeArchive }).ToListAsync(); + var results = await extractor.ExtractAsync(path, new ExtractorOptions(){ RequireTopLevelToBeArchive = requireTopLevelToBeArchive }).ToListAsync(TestContext.CancellationTokenSource.Token); - Assert.AreEqual(1, results.Count); + Assert.HasCount(results, 1); var actualNumberOfFailedArchives = results.Count(x => x.EntryStatus == FileEntryStatus.FailedArchive); Assert.AreEqual(expectedNumFailures, actualNumberOfFailedArchives); } - [DataTestMethod] + [TestMethod] [DataRow("TestDataCorrupt.tar", false, 0, 1)] [DataRow("TestDataCorrupt.tar", true, 1, 1)] [DataRow("TestDataCorrupt.tar.zip", false, 0, 2)] @@ -61,7 +63,7 @@ public void ExtractCorruptArchive(string fileName, bool requireTopLevelToBeArchi Assert.AreEqual(expectedNumFailures, actualNumberOfFailedArchives); } - [DataTestMethod] + [TestMethod] [DataRow("Lorem.txt", true, 1)] [DataRow("Lorem.txt", false, 0)] public void ExtractFlatFile(string fileName, bool requireTopLevelToBeArchive, int expectedNumFailures) @@ -70,12 +72,12 @@ public void ExtractFlatFile(string fileName, bool requireTopLevelToBeArchive, in var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestData", fileName); var results = extractor.Extract(path, new ExtractorOptions(){ RequireTopLevelToBeArchive = requireTopLevelToBeArchive }).ToList(); - Assert.AreEqual(1, results.Count); + Assert.HasCount(results, 1); var actualNumberOfFailedArchives = results.Count(x => x.EntryStatus == FileEntryStatus.FailedArchive); Assert.AreEqual(expectedNumFailures, actualNumberOfFailedArchives); } - [DataTestMethod] + [TestMethod] [DataRow("EmptyFile.txt")] [DataRow("TestData.zip", ".zip")] public void ExtractAsRaw(string fileName, string? RawExtension = null) @@ -88,6 +90,6 @@ public void ExtractAsRaw(string fileName, string? RawExtension = null) var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); var results = extractor.Extract(path, options); - Assert.AreEqual(1, results.Count()); + Assert.HasCount(results, 1); } } \ No newline at end of file diff --git a/RecursiveExtractor.Tests/ExtractorTests/TestQuinesAndSlip.cs b/RecursiveExtractor.Tests/ExtractorTests/TestQuinesAndSlip.cs index e2da32d4..252820b4 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/TestQuinesAndSlip.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/TestQuinesAndSlip.cs @@ -42,7 +42,7 @@ public async Task TestZipSlipAsync(string fileName) { var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "Bombs", fileName); - var results = await extractor.ExtractAsync(path, new ExtractorOptions()).ToListAsync(); + var results = await extractor.ExtractAsync(path, new ExtractorOptions()).ToListAsync(TestContext.CancellationTokenSource.Token); Assert.IsTrue(results.All(x => !x.FullPath.Contains(".."))); } @@ -65,21 +65,25 @@ public static IEnumerable QuineBombNames [TestMethod] [DynamicData(nameof(QuineBombNames))] - [ExpectedException(typeof(OverflowException))] public void TestQuineBombs(string fileName) { var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "Bombs", fileName); - _ = extractor.Extract(path, new ExtractorOptions() { MemoryStreamCutoff = 1024 * 1024 * 1024 }).ToList(); + Assert.ThrowsExactly(() => + { + _ = extractor.Extract(path, new ExtractorOptions() { MemoryStreamCutoff = 1024 * 1024 * 1024 }).ToList(); + }); } [TestMethod] [DynamicData(nameof(QuineBombNames))] - [ExpectedException(typeof(OverflowException))] public async Task TestQuineBombsAsync(string fileName) { var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "Bombs", fileName); - _ = await extractor.ExtractAsync(path, new ExtractorOptions() { MemoryStreamCutoff = 1024 * 1024 * 1024 }).ToListAsync(); + await Assert.ThrowsExactlyAsync(async () => + { + _ = await extractor.ExtractAsync(path, new ExtractorOptions() { MemoryStreamCutoff = 1024 * 1024 * 1024 }).ToListAsync(TestContext.CancellationTokenSource.Token); + }); } } \ No newline at end of file diff --git a/RecursiveExtractor.Tests/ExtractorTests/TimeOutTests.cs b/RecursiveExtractor.Tests/ExtractorTests/TimeOutTests.cs index 2f2c8455..16ec9392 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/TimeOutTests.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/TimeOutTests.cs @@ -9,7 +9,7 @@ namespace RecursiveExtractor.Tests.ExtractorTests; [TestClass] public class TimeOutTests : BaseExtractorTestClass { - [DataTestMethod] + [TestMethod] [DataRow("TestData.7z", 3, false)] [DataRow("TestData.tar", 6, false)] [DataRow("TestData.rar", 3, false)] @@ -45,7 +45,7 @@ public void TimeoutTest(string fileName, int expectedNumFiles = 3, bool parallel { var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); - Assert.ThrowsException(() => + Assert.ThrowsExactly(() => { var results = extractor.Extract(path, new ExtractorOptions() @@ -63,7 +63,7 @@ public void TimeoutTest(string fileName, int expectedNumFiles = 3, bool parallel }); } - [DataTestMethod] + [TestMethod] [DataRow("TestData.7z", 3, false)] [DataRow("TestData.tar", 6, false)] [DataRow("TestData.rar", 3, false)] @@ -95,13 +95,11 @@ public void TimeoutTest(string fileName, int expectedNumFiles = 3, bool parallel [DataRow("EmptyFile.txt", 1, true)] [DataRow("TestDataArchivesNested.Zip", 54, true)] [DataRow("TestDataArchivesNested.Zip", 54, false)] - [DataRow("TestDataArchivesNested.Zip", 54, true)] - [DataRow("TestDataArchivesNested.Zip", 54, false)] public async Task TimeoutTestAsync(string fileName, int expectedNumFiles = 3, bool parallel = false) { var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); - await Assert.ThrowsExceptionAsync(async () => + await Assert.ThrowsExactlyAsync(async () => { var results = extractor.ExtractAsync(path, new ExtractorOptions() diff --git a/RecursiveExtractor.Tests/SanitizePathTests.cs b/RecursiveExtractor.Tests/SanitizePathTests.cs index f8d56a29..774cf115 100644 --- a/RecursiveExtractor.Tests/SanitizePathTests.cs +++ b/RecursiveExtractor.Tests/SanitizePathTests.cs @@ -10,7 +10,7 @@ namespace RecursiveExtractor.Tests [TestClass] public class SanitizePathTests { - [DataTestMethod] + [TestMethod] [DataRow("a\\file\\with:colon.name", "a\\file\\with_colon.name")] [DataRow("a\\folder:with\\colon.name", "a\\folder_with\\colon.name")] @@ -23,7 +23,7 @@ public void TestSanitizePathWindows(string windowsInputPath, string expectedWind } } - [DataTestMethod] + [TestMethod] [DataRow("a/file/with:colon.name", "a/file/with_colon.name")] [DataRow("a/folder:with/colon.name", "a/folder_with/colon.name")] diff --git a/RecursiveExtractor.Tests/testconfig.json b/RecursiveExtractor.Tests/testconfig.json new file mode 100644 index 00000000..fb8cb9c1 --- /dev/null +++ b/RecursiveExtractor.Tests/testconfig.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/testconfig.json", + "testingPlatform": { + "trxReport": { + "enabled": true + } + } +} diff --git a/nuget.config b/nuget.config index 765346e5..227ad0ce 100644 --- a/nuget.config +++ b/nuget.config @@ -1,7 +1,7 @@ - + - + - + \ No newline at end of file diff --git a/nuget.config.backup b/nuget.config.backup deleted file mode 100644 index 227ad0ce..00000000 --- a/nuget.config.backup +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file From b1e8d742520f9a285e68498c0406f067f57de16c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 22:37:59 +0000 Subject: [PATCH 11/15] Fix all MSTest warnings: replace DataTestMethod, ThrowsException, ExpectedException, duplicate DataRows, add parallelization, use Assert.HasCount/IsEmpty/IsGreaterThan, add CancellationToken support. Update pipeline to use standard trx logger with results directory. Co-authored-by: gfs <98900+gfs@users.noreply.github.com> --- .editorconfig | 1 - .../ExtractorTests/CustomExtractorTests.cs | 18 +++++++++--------- .../ExtractorTests/MiscTests.cs | 6 +++--- RecursiveExtractor.Tests/testconfig.json | 8 -------- 4 files changed, 12 insertions(+), 21 deletions(-) delete mode 100644 RecursiveExtractor.Tests/testconfig.json diff --git a/.editorconfig b/.editorconfig index d03ac215..f195c50b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -191,7 +191,6 @@ dotnet_naming_symbols.non_field_members.required_modifiers = # Naming styles dotnet_naming_style.pascal_case.required_prefix = - dotnet_naming_style.pascal_case.required_suffix = dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case diff --git a/RecursiveExtractor.Tests/ExtractorTests/CustomExtractorTests.cs b/RecursiveExtractor.Tests/ExtractorTests/CustomExtractorTests.cs index 048cf04f..281e1c95 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/CustomExtractorTests.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/CustomExtractorTests.cs @@ -129,7 +129,7 @@ public void Constructor_WithCustomExtractors_RegistersExtractors() var customExtractor = new TestCustomExtractor(null!); var extractor = new Extractor(new[] { customExtractor }); - Assert.HasCount(extractor.CustomExtractors, 1); + Assert.HasCount(1, extractor.CustomExtractors); } [TestMethod] @@ -139,7 +139,7 @@ public void Constructor_WithMultipleCustomExtractors_RegistersAll() var customExtractor2 = new SecondTestCustomExtractor(null!); var extractor = new Extractor(new ICustomAsyncExtractor[] { customExtractor1, customExtractor2 }); - Assert.HasCount(extractor.CustomExtractors, 2); + Assert.HasCount(2, extractor.CustomExtractors); } [TestMethod] @@ -148,7 +148,7 @@ public void Constructor_WithNullInCollection_IgnoresNull() var customExtractor = new TestCustomExtractor(null!); var extractor = new Extractor(new ICustomAsyncExtractor[] { customExtractor, null! }); - Assert.HasCount(extractor.CustomExtractors, 1); + Assert.HasCount(1, extractor.CustomExtractors); } [TestMethod] @@ -169,7 +169,7 @@ public void Extract_WithMatchingCustomExtractor_UsesCustomExtractor() var testData = System.Text.Encoding.ASCII.GetBytes("CUSTOM1 This is test data"); var results = extractor.Extract("test.custom", testData).ToList(); - Assert.HasCount(results, 1); + Assert.HasCount(1, results); Assert.AreEqual("extracted_from_custom.txt", results[0].Name); // Read the content to verify it was processed by our custom extractor @@ -189,7 +189,7 @@ public async Task ExtractAsync_WithMatchingCustomExtractor_UsesCustomExtractor() var testData = System.Text.Encoding.ASCII.GetBytes("CUSTOM1 This is test data"); var results = await extractor.ExtractAsync("test.custom", testData).ToListAsync(TestContext.CancellationTokenSource.Token); - Assert.HasCount(results, 1); + Assert.HasCount(1, results); Assert.AreEqual("extracted_from_custom.txt", results[0].Name); // Read the content to verify it was processed by our custom extractor @@ -210,7 +210,7 @@ public void Extract_WithoutMatchingCustomExtractor_ReturnsOriginalFile() var results = extractor.Extract("test.txt", testData).ToList(); // Should return the original file since no custom extractor matched - Assert.HasCount(results, 1); + Assert.HasCount(1, results); Assert.AreEqual("test.txt", results[0].Name); // Verify it's the original content @@ -232,13 +232,13 @@ public void Extract_MultipleCustomExtractors_UsesCorrectOne() // Test with first custom format var testData1 = System.Text.Encoding.ASCII.GetBytes("CUSTOM1 data"); var results1 = extractor.Extract("test1.custom", testData1).ToList(); - Assert.HasCount(results1, 1); + Assert.HasCount(1, results1); Assert.AreEqual("extracted_from_custom.txt", results1[0].Name); // Test with second custom format var testData2 = System.Text.Encoding.ASCII.GetBytes("CUSTOM2 data"); var results2 = extractor.Extract("test2.custom", testData2).ToList(); - Assert.HasCount(results2, 1); + Assert.HasCount(1, results2); Assert.AreEqual("extracted_from_second_custom.txt", results2[0].Name); } @@ -252,7 +252,7 @@ public void Extract_NoCustomExtractors_ReturnsOriginalFile() var results = extractor.Extract("test.custom", testData).ToList(); // Should return the original file since no custom extractor is registered - Assert.HasCount(results, 1); + Assert.HasCount(1, results); Assert.AreEqual("test.custom", results[0].Name); } diff --git a/RecursiveExtractor.Tests/ExtractorTests/MiscTests.cs b/RecursiveExtractor.Tests/ExtractorTests/MiscTests.cs index c06a75c2..5b9aa104 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/MiscTests.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/MiscTests.cs @@ -39,7 +39,7 @@ public async Task ExtractFlatFileAsync(string fileName, bool requireTopLevelToBe var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestData", fileName); var results = await extractor.ExtractAsync(path, new ExtractorOptions(){ RequireTopLevelToBeArchive = requireTopLevelToBeArchive }).ToListAsync(TestContext.CancellationTokenSource.Token); - Assert.HasCount(results, 1); + Assert.HasCount(1, results); var actualNumberOfFailedArchives = results.Count(x => x.EntryStatus == FileEntryStatus.FailedArchive); Assert.AreEqual(expectedNumFailures, actualNumberOfFailedArchives); } @@ -72,7 +72,7 @@ public void ExtractFlatFile(string fileName, bool requireTopLevelToBeArchive, in var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestData", fileName); var results = extractor.Extract(path, new ExtractorOptions(){ RequireTopLevelToBeArchive = requireTopLevelToBeArchive }).ToList(); - Assert.HasCount(results, 1); + Assert.HasCount(1, results); var actualNumberOfFailedArchives = results.Count(x => x.EntryStatus == FileEntryStatus.FailedArchive); Assert.AreEqual(expectedNumFailures, actualNumberOfFailedArchives); } @@ -90,6 +90,6 @@ public void ExtractAsRaw(string fileName, string? RawExtension = null) var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); var results = extractor.Extract(path, options); - Assert.HasCount(results, 1); + Assert.HasCount(1, results); } } \ No newline at end of file diff --git a/RecursiveExtractor.Tests/testconfig.json b/RecursiveExtractor.Tests/testconfig.json deleted file mode 100644 index fb8cb9c1..00000000 --- a/RecursiveExtractor.Tests/testconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/testconfig.json", - "testingPlatform": { - "trxReport": { - "enabled": true - } - } -} From 162ebd8c8964b4f37b36e518345e581c94e297f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 00:09:37 +0000 Subject: [PATCH 12/15] Revert pipeline dotnetTestArgs to original format to fix argument parsing error Co-authored-by: gfs <98900+gfs@users.noreply.github.com> --- Pipelines/recursive-extractor-pr.yml | 4 ++-- Pipelines/recursive-extractor-release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Pipelines/recursive-extractor-pr.yml b/Pipelines/recursive-extractor-pr.yml index 0baf2e62..3476419e 100644 --- a/Pipelines/recursive-extractor-pr.yml +++ b/Pipelines/recursive-extractor-pr.yml @@ -42,7 +42,7 @@ extends: poolName: MSSecurity-1ES-Build-Agents-Pool poolImage: MSSecurity-1ES-Windows-2022 poolOs: windows - dotnetTestArgs: '--logger trx --results-directory $(Build.SourcesDirectory)/TestResults -- --coverage' + dotnetTestArgs: '-- --coverage --report-trx' includeNuGetOrg: false nugetFeedsToUse: 'config' nugetConfigPath: 'nuget.config' @@ -57,7 +57,7 @@ extends: poolName: MSSecurity-1ES-Build-Agents-Pool poolImage: MSSecurity-1ES-Windows-2022 poolOs: windows - dotnetTestArgs: '--logger trx --results-directory $(Build.SourcesDirectory)/TestResults -- --coverage' + dotnetTestArgs: '-- --coverage --report-trx' includeNuGetOrg: false nugetFeedsToUse: 'config' nugetConfigPath: 'nuget.config' diff --git a/Pipelines/recursive-extractor-release.yml b/Pipelines/recursive-extractor-release.yml index 9aca2257..b2a85fc8 100644 --- a/Pipelines/recursive-extractor-release.yml +++ b/Pipelines/recursive-extractor-release.yml @@ -43,7 +43,7 @@ extends: poolName: MSSecurity-1ES-Build-Agents-Pool poolImage: MSSecurity-1ES-Windows-2022 poolOs: windows - dotnetTestArgs: '--logger trx --results-directory $(Build.SourcesDirectory)/TestResults -- --coverage' + dotnetTestArgs: '-- --coverage --report-trx' includeNuGetOrg: false nugetFeedsToUse: 'config' nugetConfigPath: 'nuget.config' @@ -57,7 +57,7 @@ extends: poolName: MSSecurity-1ES-Build-Agents-Pool poolImage: MSSecurity-1ES-Windows-2022 poolOs: windows - dotnetTestArgs: '--logger trx --results-directory $(Build.SourcesDirectory)/TestResults -- --coverage' + dotnetTestArgs: '-- --coverage --report-trx' includeNuGetOrg: false nugetFeedsToUse: 'config' nugetConfigPath: 'nuget.config' From 2483e165d5215527a236ca8c8563ec29417ab8cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 23:13:40 +0000 Subject: [PATCH 13/15] Fix test result reporting: use --logger trx with relative results directory Co-authored-by: gfs <98900+gfs@users.noreply.github.com> --- Pipelines/recursive-extractor-pr.yml | 4 ++-- Pipelines/recursive-extractor-release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Pipelines/recursive-extractor-pr.yml b/Pipelines/recursive-extractor-pr.yml index 3476419e..be9ac8a4 100644 --- a/Pipelines/recursive-extractor-pr.yml +++ b/Pipelines/recursive-extractor-pr.yml @@ -42,7 +42,7 @@ extends: poolName: MSSecurity-1ES-Build-Agents-Pool poolImage: MSSecurity-1ES-Windows-2022 poolOs: windows - dotnetTestArgs: '-- --coverage --report-trx' + dotnetTestArgs: '--logger trx --results-directory TestResults -- --coverage' includeNuGetOrg: false nugetFeedsToUse: 'config' nugetConfigPath: 'nuget.config' @@ -57,7 +57,7 @@ extends: poolName: MSSecurity-1ES-Build-Agents-Pool poolImage: MSSecurity-1ES-Windows-2022 poolOs: windows - dotnetTestArgs: '-- --coverage --report-trx' + dotnetTestArgs: '--logger trx --results-directory TestResults -- --coverage' includeNuGetOrg: false nugetFeedsToUse: 'config' nugetConfigPath: 'nuget.config' diff --git a/Pipelines/recursive-extractor-release.yml b/Pipelines/recursive-extractor-release.yml index b2a85fc8..ab1dd80d 100644 --- a/Pipelines/recursive-extractor-release.yml +++ b/Pipelines/recursive-extractor-release.yml @@ -43,7 +43,7 @@ extends: poolName: MSSecurity-1ES-Build-Agents-Pool poolImage: MSSecurity-1ES-Windows-2022 poolOs: windows - dotnetTestArgs: '-- --coverage --report-trx' + dotnetTestArgs: '--logger trx --results-directory TestResults -- --coverage' includeNuGetOrg: false nugetFeedsToUse: 'config' nugetConfigPath: 'nuget.config' @@ -57,7 +57,7 @@ extends: poolName: MSSecurity-1ES-Build-Agents-Pool poolImage: MSSecurity-1ES-Windows-2022 poolOs: windows - dotnetTestArgs: '-- --coverage --report-trx' + dotnetTestArgs: '--logger trx --results-directory TestResults -- --coverage' includeNuGetOrg: false nugetFeedsToUse: 'config' nugetConfigPath: 'nuget.config' From a748a97ace48fbd7c35c442eca1cbaccae4c5943 Mon Sep 17 00:00:00 2001 From: Giulia Stocco Date: Thu, 5 Feb 2026 17:18:02 -0800 Subject: [PATCH 14/15] Update FileStream usage in tests to specify access/share Explicitly set FileAccess.Read and FileShare.Read when opening FileStream in test methods. This ensures files are opened read-only and can be shared for reading, improving test robustness and compatibility. No changes to test logic. --- .../ExtractorTests/ExpectedNumFilesTests.cs | 8 ++++---- RecursiveExtractor.Tests/ExtractorTests/MiniMagicTests.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/RecursiveExtractor.Tests/ExtractorTests/ExpectedNumFilesTests.cs b/RecursiveExtractor.Tests/ExtractorTests/ExpectedNumFilesTests.cs index 1f7f6f24..c8542d7f 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/ExpectedNumFilesTests.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/ExpectedNumFilesTests.cs @@ -227,9 +227,9 @@ public async Task ExtractArchiveAsync(string fileName, int expectedNumFiles) [DynamicData(nameof(ArchiveData))] public async Task ExtractArchiveFromStreamAsync(string fileName, int expectedNumFiles) { - var extractor = new Extractor(); + var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); - using var stream = new FileStream(path, FileMode.Open); + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); var results = extractor.ExtractAsync(path, stream, new ExtractorOptions()); var numFiles = 0; await foreach (var result in results) @@ -244,9 +244,9 @@ public async Task ExtractArchiveFromStreamAsync(string fileName, int expectedNum [DynamicData(nameof(ArchiveData))] public void ExtractArchiveFromStream(string fileName, int expectedNumFiles) { - var extractor = new Extractor(); + var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); - using var stream = new FileStream(path, FileMode.Open); + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); var results = extractor.Extract(path, stream, GetExtractorOptions()); Assert.AreEqual(expectedNumFiles, results.Count()); stream.Close(); diff --git a/RecursiveExtractor.Tests/ExtractorTests/MiniMagicTests.cs b/RecursiveExtractor.Tests/ExtractorTests/MiniMagicTests.cs index f0cf3fb8..ee4de521 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/MiniMagicTests.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/MiniMagicTests.cs @@ -30,7 +30,7 @@ public class MiniMagicTests : BaseExtractorTestClass public void TestMiniMagic(string fileName, ArchiveFileType expectedArchiveFileType) { var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); - using var fs = new FileStream(path, FileMode.Open); + using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); // Test just based on the content var fileEntry = new FileEntry("NoName", fs); From e37d5af22d6250652d3d04fe09a6abaa02603679 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:33:31 +0000 Subject: [PATCH 15/15] Initial plan