Skip to content

Commit 5d90944

Browse files
Copilotgfs
andcommitted
Add ARJ, ARC, ACE support and update agent instructions for NBGV deep clone requirement
Co-authored-by: gfs <98900+gfs@users.noreply.github.com>
1 parent b3482f9 commit 5d90944

14 files changed

Lines changed: 574 additions & 5 deletions

File tree

.github/copilot-instructions.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ RecursiveExtractor is a cross-platform .NET library and CLI tool for parsing arc
1313

1414
## Building and Testing
1515

16+
### Git Clone Depth
17+
18+
⚠️ **Important**: This repository uses [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) (NBGV) to calculate version numbers from git history. Shallow clones will cause the build to fail with a `GitException: Shallow clone lacks the objects required to calculate version height` error. If you encounter this, deepen the clone:
19+
```bash
20+
git fetch --unshallow
21+
# or if that fails:
22+
git fetch --depth=100
23+
```
24+
1625
### Build Commands
1726
```bash
1827
# Build the entire solution

RecursiveExtractor.Tests/ExtractorTests/ExpectedNumFilesTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ public static TheoryData<string, int> ArchiveData
4343
{ "TestDataArchivesNested.Zip", 54 },
4444
{ "UdfTest.iso", 3 },
4545
{ "UdfTestWithMultiSystem.iso", 3 },
46+
{ "TestData.arj", 1 },
47+
{ "TestData.arc", 1 },
48+
{ "TestData.ace", 1 },
4649
// { "HfsSampleUDCO.dmg", 2 }
4750
};
4851
}
@@ -75,6 +78,9 @@ public static TheoryData<string, int> NoRecursionData
7578
{ "EmptyFile.txt", 1 },
7679
{ "TestDataArchivesNested.Zip", 14 },
7780
{ "UdfTestWithMultiSystem.iso", 3 },
81+
{ "TestData.arj", 1 },
82+
{ "TestData.arc", 1 },
83+
{ "TestData.ace", 1 },
7884
// { "HfsSampleUDCO.dmg", 2 }
7985
};
8086
}

RecursiveExtractor.Tests/ExtractorTests/MiniMagicTests.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ public class MiniMagicTests
2424
[InlineData("Empty.vmdk", ArchiveFileType.VMDK)]
2525
[InlineData("HfsSampleUDCO.dmg", ArchiveFileType.DMG)]
2626
[InlineData("EmptyFile.txt", ArchiveFileType.UNKNOWN)]
27+
[InlineData("TestData.arj", ArchiveFileType.ARJ)]
28+
[InlineData("TestData.arc", ArchiveFileType.ARC)]
29+
[InlineData("TestData.ace", ArchiveFileType.ACE)]
2730
public void TestMiniMagic(string fileName, ArchiveFileType expectedArchiveFileType)
2831
{
2932
var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName);

RecursiveExtractor.Tests/RecursiveExtractor.Tests.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,15 @@
301301
<None Update="TestData\TestDataArchives\UdfTestWithMultiSystem.iso">
302302
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
303303
</None>
304+
<None Update="TestData\TestDataArchives\TestData.arj">
305+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
306+
</None>
307+
<None Update="TestData\TestDataArchives\TestData.arc">
308+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
309+
</None>
310+
<None Update="TestData\TestDataArchives\TestData.ace">
311+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
312+
</None>
304313
<None Update="TestData\Bombs\zoneinfo-2010g.tar">
305314
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
306315
</None>
117 Bytes
Binary file not shown.
75 Bytes
Binary file not shown.
176 Bytes
Binary file not shown.

RecursiveExtractor/Extractor.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ public void SetDefaultExtractors()
8585
SetExtractor(ArchiveFileType.VMDK, new VmdkExtractor(this));
8686
SetExtractor(ArchiveFileType.XZ, new XzExtractor(this));
8787
SetExtractor(ArchiveFileType.ZIP, new ZipExtractor(this));
88+
SetExtractor(ArchiveFileType.ARJ, new ArjExtractor(this));
89+
SetExtractor(ArchiveFileType.ARC, new ArcExtractor(this));
90+
SetExtractor(ArchiveFileType.ACE, new AceExtractor(this));
8891
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
8992
{
9093
SetExtractor(ArchiveFileType.WIM, new WimExtractor(this));
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
using SharpCompress.Readers;
2+
using SharpCompress.Readers.Ace;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.IO;
6+
7+
namespace Microsoft.CST.RecursiveExtractor.Extractors
8+
{
9+
/// <summary>
10+
/// The ACE Archive extractor implementation
11+
/// </summary>
12+
public class AceExtractor : AsyncExtractorInterface
13+
{
14+
/// <summary>
15+
/// The constructor takes the Extractor context for recursion.
16+
/// </summary>
17+
/// <param name="context">The Extractor context.</param>
18+
public AceExtractor(Extractor context)
19+
{
20+
Context = context;
21+
}
22+
private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger();
23+
24+
internal Extractor Context { get; }
25+
26+
/// <summary>
27+
/// Extracts an ACE archive
28+
/// </summary>
29+
///<inheritdoc />
30+
public async IAsyncEnumerable<FileEntry> ExtractAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true)
31+
{
32+
AceReader? aceReader = null;
33+
try
34+
{
35+
aceReader = AceReader.Open(fileEntry.Content, new ReaderOptions()
36+
{
37+
LeaveStreamOpen = true
38+
});
39+
}
40+
catch (Exception e)
41+
{
42+
Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.ACE, fileEntry.FullPath, string.Empty, e.GetType());
43+
}
44+
45+
if (aceReader != null)
46+
{
47+
using (aceReader)
48+
{
49+
while (aceReader.MoveToNextEntry())
50+
{
51+
var entry = aceReader.Entry;
52+
if (entry.IsDirectory)
53+
{
54+
continue;
55+
}
56+
57+
var name = entry.Key?.Replace('/', Path.DirectorySeparatorChar);
58+
if (string.IsNullOrEmpty(name))
59+
{
60+
Logger.Debug(Extractor.ENTRY_MISSING_NAME_ERROR_MESSAGE_STRING, ArchiveFileType.ACE, fileEntry.FullPath);
61+
continue;
62+
}
63+
64+
var newFileEntry = await FileEntry.FromStreamAsync(name, aceReader.OpenEntryStream(), fileEntry, entry.CreatedTime, entry.LastModifiedTime, entry.LastAccessedTime, memoryStreamCutoff: options.MemoryStreamCutoff).ConfigureAwait(false);
65+
if (newFileEntry != null)
66+
{
67+
governor.CheckResourceGovernor(newFileEntry.Content.Length);
68+
69+
if (options.Recurse || topLevel)
70+
{
71+
await foreach (var innerEntry in Context.ExtractAsync(newFileEntry, options, governor, false))
72+
{
73+
yield return innerEntry;
74+
}
75+
}
76+
else
77+
{
78+
yield return newFileEntry;
79+
}
80+
}
81+
}
82+
}
83+
}
84+
else
85+
{
86+
if (options.ExtractSelfOnFail)
87+
{
88+
fileEntry.EntryStatus = FileEntryStatus.FailedArchive;
89+
yield return fileEntry;
90+
}
91+
}
92+
}
93+
94+
/// <summary>
95+
/// Extracts an ACE archive
96+
/// </summary>
97+
///<inheritdoc />
98+
public IEnumerable<FileEntry> Extract(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true)
99+
{
100+
AceReader? aceReader = null;
101+
try
102+
{
103+
aceReader = AceReader.Open(fileEntry.Content, new ReaderOptions()
104+
{
105+
LeaveStreamOpen = true
106+
});
107+
}
108+
catch (Exception e)
109+
{
110+
Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.ACE, fileEntry.FullPath, string.Empty, e.GetType());
111+
}
112+
113+
if (aceReader != null)
114+
{
115+
using (aceReader)
116+
{
117+
while (aceReader.MoveToNextEntry())
118+
{
119+
var entry = aceReader.Entry;
120+
if (entry.IsDirectory)
121+
{
122+
continue;
123+
}
124+
125+
FileEntry? newFileEntry = null;
126+
try
127+
{
128+
var stream = aceReader.OpenEntryStream();
129+
var name = entry.Key?.Replace('/', Path.DirectorySeparatorChar);
130+
if (string.IsNullOrEmpty(name))
131+
{
132+
Logger.Debug(Extractor.ENTRY_MISSING_NAME_ERROR_MESSAGE_STRING, ArchiveFileType.ACE, fileEntry.FullPath);
133+
continue;
134+
}
135+
newFileEntry = new FileEntry(name, stream, fileEntry, false, entry.CreatedTime, entry.LastModifiedTime, entry.LastAccessedTime, memoryStreamCutoff: options.MemoryStreamCutoff);
136+
}
137+
catch (Exception e)
138+
{
139+
Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.ACE, fileEntry.FullPath, entry.Key, e.GetType());
140+
}
141+
if (newFileEntry != null)
142+
{
143+
governor.CheckResourceGovernor(newFileEntry.Content.Length);
144+
145+
if (options.Recurse || topLevel)
146+
{
147+
foreach (var innerEntry in Context.Extract(newFileEntry, options, governor, false))
148+
{
149+
yield return innerEntry;
150+
}
151+
}
152+
else
153+
{
154+
yield return newFileEntry;
155+
}
156+
}
157+
}
158+
}
159+
}
160+
else
161+
{
162+
if (options.ExtractSelfOnFail)
163+
{
164+
fileEntry.EntryStatus = FileEntryStatus.FailedArchive;
165+
yield return fileEntry;
166+
}
167+
}
168+
}
169+
}
170+
}

0 commit comments

Comments
 (0)