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'
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..281e1c95 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(1, extractor.CustomExtractors);
}
[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(2, extractor.CustomExtractors);
}
[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(1, extractor.CustomExtractors);
}
[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(1, results);
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(1, results);
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(1, results);
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(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.AreEqual(1, results2.Count);
+ Assert.HasCount(1, results2);
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(1, results);
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/ExpectedNumFilesTests.cs b/RecursiveExtractor.Tests/ExtractorTests/ExpectedNumFilesTests.cs
index ab9544cf..c8542d7f 100644
--- a/RecursiveExtractor.Tests/ExtractorTests/ExpectedNumFilesTests.cs
+++ b/RecursiveExtractor.Tests/ExtractorTests/ExpectedNumFilesTests.cs
@@ -43,6 +43,8 @@ public static IEnumerable
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..a3d2e44c
--- /dev/null
+++ b/RecursiveExtractor/Extractors/ArcExtractor.cs
@@ -0,0 +1,173 @@
+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; }
+
+ ///
+ /// 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 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)
+ {
+ // 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))
+ {
+ 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;
+ }
+
+ 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)
+ {
+ // 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))
+ {
+ 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..76c4b294 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 (0x01-0x09 or 0x7F valid for standard ARC)
+ if (buffer[0] == 0x1A && ((buffer[1] >= 0x01 && buffer[1] <= 0x09) || buffer[1] == 0x7F))
+ {
+ 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 @@
-
+