Skip to content

Commit 28254a9

Browse files
Feature: Added support for selecting encoding when extracting ZIP files (files-community#17022)
1 parent 87730e3 commit 28254a9

File tree

10 files changed

+275
-3
lines changed

10 files changed

+275
-3
lines changed

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
<PackageVersion Include="OwlCore.Storage" Version="0.12.2" />
3333
<PackageVersion Include="Sentry" Version="5.1.1" />
3434
<PackageVersion Include="SevenZipSharp" Version="1.0.2" />
35+
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
3536
<PackageVersion Include="SQLitePCLRaw.bundle_green" Version="2.1.10" />
3637
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250310001" />
3738
<PackageVersion Include="Microsoft.Graphics.Win2D" Version="1.3.2" />

src/Files.App/Actions/Content/Archives/Decompress/DecompressArchive.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,15 @@ public override async Task ExecuteAsync(object? parameter = null)
4242
return;
4343

4444
var isArchiveEncrypted = await FilesystemTasks.Wrap(() => StorageArchiveService.IsEncryptedAsync(archive.Path));
45+
var isArchiveEncodingUndetermined = await FilesystemTasks.Wrap(() => StorageArchiveService.IsEncodingUndeterminedAsync(archive.Path));
4546
var password = string.Empty;
47+
Encoding? encoding = null;
4648

4749
DecompressArchiveDialog decompressArchiveDialog = new();
4850
DecompressArchiveDialogViewModel decompressArchiveViewModel = new(archive)
4951
{
5052
IsArchiveEncrypted = isArchiveEncrypted,
53+
IsArchiveEncodingUndetermined = isArchiveEncodingUndetermined,
5154
ShowPathSelection = true
5255
};
5356
decompressArchiveDialog.ViewModel = decompressArchiveViewModel;
@@ -62,6 +65,8 @@ public override async Task ExecuteAsync(object? parameter = null)
6265
if (isArchiveEncrypted && decompressArchiveViewModel.Password is not null)
6366
password = Encoding.UTF8.GetString(decompressArchiveViewModel.Password);
6467

68+
encoding = decompressArchiveViewModel.SelectedEncoding.Encoding;
69+
6570
// Check if archive still exists
6671
if (!StorageHelpers.Exists(archive.Path))
6772
return;
@@ -77,7 +82,7 @@ public override async Task ExecuteAsync(object? parameter = null)
7782

7883
// Operate decompress
7984
var result = await FilesystemTasks.Wrap(() =>
80-
StorageArchiveService.DecompressAsync(archive?.Path ?? string.Empty, destinationFolder?.Path ?? string.Empty, password));
85+
StorageArchiveService.DecompressAsync(archive?.Path ?? string.Empty, destinationFolder?.Path ?? string.Empty, password, encoding));
8186

8287
if (decompressArchiveViewModel.OpenDestinationFolderOnCompletion)
8388
await NavigationHelpers.OpenPath(destinationFolderPath, context.ShellPage, FilesystemItemType.Directory);

src/Files.App/Data/Contracts/IStorageArchiveService.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Files Community
22
// Licensed under the MIT License.
33

4+
using System.Text;
45
using SevenZip;
56

67
namespace Files.App.Data.Contracts
@@ -37,8 +38,9 @@ public interface IStorageArchiveService
3738
/// <param name="archiveFilePath">The archive file path to decompress.</param>
3839
/// <param name="destinationFolderPath">The destination folder path which the archive file will be decompressed to.</param>
3940
/// <param name="password">The password to decrypt the archive file if applicable.</param>
41+
/// <param name="encoding">The file name encoding to decrypt the archive file. If set to null, system default encoding will be used.</param>
4042
/// <returns>True if the decompression has done successfully; otherwise, false.</returns>
41-
Task<bool> DecompressAsync(string archiveFilePath, string destinationFolderPath, string password = "");
43+
Task<bool> DecompressAsync(string archiveFilePath, string destinationFolderPath, string password = "", Encoding? encoding = null);
4244

4345
/// <summary>
4446
/// Generates the archive file name from item names.
@@ -54,6 +56,13 @@ public interface IStorageArchiveService
5456
/// <returns>True if the archive file is encrypted; otherwise, false.</returns>
5557
Task<bool> IsEncryptedAsync(string archiveFilePath);
5658

59+
/// <summary>
60+
/// Gets the value that indicates whether the archive file's encoding is undetermined.
61+
/// </summary>
62+
/// <param name="archiveFilePath">The archive file path to check if the item is encrypted.</param>
63+
/// <returns>True if the archive file's encoding is undetermined; otherwise, false.</returns>
64+
Task<bool> IsEncodingUndeterminedAsync(string archiveFilePath);
65+
5766
/// <summary>
5867
/// Gets the <see cref="SevenZipExtractor"/> instance from the archive file path.
5968
/// </summary>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) 2024 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
using System.Text;
5+
6+
namespace Files.App.Data.Items
7+
{
8+
/// <summary>
9+
/// Represents a text encoding in the application.
10+
/// </summary>
11+
public sealed class EncodingItem
12+
{
13+
14+
public Encoding? Encoding { get; set; }
15+
16+
/// <summary>
17+
/// Gets the encoding name. e.g. English (United States)
18+
/// </summary>
19+
public string Name { get; set; }
20+
21+
/// <summary>
22+
/// Initializes a new instance of the <see cref="EncodingItem"/> class.
23+
/// </summary>
24+
/// <param name="code">The code of the language.</param>
25+
public EncodingItem(string code)
26+
{
27+
if (string.IsNullOrEmpty(code))
28+
{
29+
Encoding = null;
30+
Name = Strings.Default.GetLocalizedResource();
31+
}
32+
else
33+
{
34+
Encoding = Encoding.GetEncoding(code);
35+
Name = Encoding.EncodingName;
36+
}
37+
}
38+
39+
public override string ToString() => Name;
40+
}
41+
}

src/Files.App/Dialogs/DecompressArchiveDialog.xaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
66
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
77
xmlns:helpers="using:Files.App.Helpers"
8+
xmlns:items="using:Files.App.Data.Items"
89
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
10+
xmlns:uc="using:Files.App.UserControls"
911
Title="{helpers:ResourceString Name=ExtractArchive}"
1012
CornerRadius="{StaticResource OverlayCornerRadius}"
1113
DefaultButton="Primary"
@@ -80,6 +82,34 @@
8082

8183
</StackPanel>
8284

85+
<!-- Encoding -->
86+
<StackPanel
87+
x:Name="EncodingStackPanel"
88+
x:Load="{x:Bind ViewModel.IsArchiveEncodingUndetermined, Mode=OneWay}"
89+
Orientation="Vertical"
90+
Spacing="8">
91+
92+
<TextBlock
93+
x:Name="EncodingHeader"
94+
HorizontalAlignment="Stretch"
95+
VerticalAlignment="Stretch"
96+
Text="{helpers:ResourceString Name=Encoding}" />
97+
98+
<ComboBox
99+
x:Name="EncodingBox"
100+
HorizontalAlignment="Stretch"
101+
VerticalAlignment="Stretch"
102+
ItemsSource="{x:Bind ViewModel.EncodingOptions, Mode=OneWay}"
103+
SelectedItem="{x:Bind ViewModel.SelectedEncoding, Mode=TwoWay}">
104+
<ComboBox.ItemTemplate>
105+
<DataTemplate x:DataType="items:EncodingItem">
106+
<TextBlock Text="{x:Bind Name}" />
107+
</DataTemplate>
108+
</ComboBox.ItemTemplate>
109+
</ComboBox>
110+
111+
</StackPanel>
112+
83113
<!-- Open when complete -->
84114
<CheckBox
85115
x:Name="OpenDestination"

src/Files.App/Files.App.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" />
9090
<PackageReference Include="Sentry" />
9191
<PackageReference Include="SevenZipSharp" />
92+
<PackageReference Include="SharpZipLib" />
9293
<PackageReference Include="SQLitePCLRaw.bundle_green" />
9394
<PackageReference Include="Microsoft.WindowsAppSDK" />
9495
<PackageReference Include="Microsoft.Graphics.Win2D" />

src/Files.App/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Microsoft.UI.Xaml;
77
using Microsoft.Windows.AppLifecycle;
88
using System.IO;
9+
using System.Text;
910
using Windows.ApplicationModel.Activation;
1011
using Windows.Storage;
1112
using static Files.App.Helpers.Win32PInvoke;
@@ -55,6 +56,7 @@ static Program()
5556
[STAThread]
5657
private static void Main()
5758
{
59+
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
5860
WinRT.ComWrappersSupport.InitializeComWrappers();
5961

6062
// We are about to do the first WinRT server call, in case the WinRT server is hanging

src/Files.App/Services/Storage/StorageArchiveService.cs

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
// Licensed under the MIT License.
33

44
using Files.Shared.Helpers;
5+
using ICSharpCode.SharpZipLib.Core;
6+
using ICSharpCode.SharpZipLib.Zip;
57
using SevenZip;
68
using System.IO;
9+
using System.Linq;
10+
using System.Text;
711
using Windows.Storage;
812
using Windows.Win32;
913

@@ -84,7 +88,17 @@ public async Task<bool> CompressAsync(ICompressArchiveModel compressionModel)
8488
}
8589

8690
/// <inheritdoc/>
87-
public async Task<bool> DecompressAsync(string archiveFilePath, string destinationFolderPath, string password = "")
91+
public Task<bool> DecompressAsync(string archiveFilePath, string destinationFolderPath, string password = "", Encoding? encoding = null)
92+
{
93+
if(encoding == null){
94+
return DecompressAsyncWithSevenZip(archiveFilePath, destinationFolderPath, password);
95+
}
96+
else
97+
{
98+
return DecompressAsyncWithSharpZipLib(archiveFilePath, destinationFolderPath, password, encoding);
99+
}
100+
}
101+
async Task<bool> DecompressAsyncWithSevenZip(string archiveFilePath, string destinationFolderPath, string password = "")
88102
{
89103
if (string.IsNullOrEmpty(archiveFilePath) ||
90104
string.IsNullOrEmpty(destinationFolderPath))
@@ -180,10 +194,134 @@ public async Task<bool> DecompressAsync(string archiveFilePath, string destinati
180194
fsProgress.Report();
181195
}
182196
};
197+
return isSuccess;
198+
}
199+
200+
async Task<bool> DecompressAsyncWithSharpZipLib(string archiveFilePath, string destinationFolderPath, string password, Encoding encoding)
201+
{
202+
if (string.IsNullOrEmpty(archiveFilePath) ||
203+
string.IsNullOrEmpty(destinationFolderPath))
204+
return false;
205+
using var zipFile = new ZipFile(archiveFilePath, StringCodec.FromEncoding(encoding));
206+
if(zipFile is null)
207+
return false;
208+
209+
if(!string.IsNullOrEmpty(password))
210+
zipFile.Password = password;
211+
212+
// Initialize a new in-progress status card
213+
var statusCard = StatusCenterHelper.AddCard_Decompress(
214+
archiveFilePath.CreateEnumerable(),
215+
destinationFolderPath.CreateEnumerable(),
216+
ReturnResult.InProgress);
217+
218+
// Check if the decompress operation canceled
219+
if (statusCard.CancellationToken.IsCancellationRequested)
220+
return false;
221+
222+
StatusCenterItemProgressModel fsProgress = new(
223+
statusCard.ProgressEventSource,
224+
enumerationCompleted: true,
225+
FileSystemStatusCode.InProgress,
226+
zipFile.Cast<ZipEntry>().Count<ZipEntry>(x => !x.IsDirectory));
227+
fsProgress.TotalSize = zipFile.Cast<ZipEntry>().Select(x => (long)x.Size).Sum();
228+
fsProgress.Report();
229+
230+
bool isSuccess = false;
231+
232+
try
233+
{
234+
long processedBytes = 0;
235+
int processedFiles = 0;
236+
237+
foreach (ZipEntry zipEntry in zipFile)
238+
{
239+
if (statusCard.CancellationToken.IsCancellationRequested)
240+
{
241+
isSuccess = false;
242+
break;
243+
}
244+
245+
if (!zipEntry.IsFile)
246+
{
247+
continue; // Ignore directories
248+
}
249+
250+
string entryFileName = zipEntry.Name;
251+
string fullZipToPath = Path.Combine(destinationFolderPath, entryFileName);
252+
string directoryName = Path.GetDirectoryName(fullZipToPath);
253+
254+
if (!Directory.Exists(directoryName))
255+
{
256+
Directory.CreateDirectory(directoryName);
257+
}
258+
259+
byte[] buffer = new byte[4096]; // 4K is a good default
260+
using (Stream zipStream = zipFile.GetInputStream(zipEntry))
261+
using (FileStream streamWriter = File.Create(fullZipToPath))
262+
{
263+
await ThreadingService.ExecuteOnUiThreadAsync(() =>
264+
{
265+
fsProgress.FileName = entryFileName;
266+
fsProgress.Report();
267+
});
268+
269+
StreamUtils.Copy(zipStream, streamWriter, buffer);
270+
}
271+
processedBytes += zipEntry.Size;
272+
if (fsProgress.TotalSize > 0)
273+
{
274+
fsProgress.Report(processedBytes / (double)fsProgress.TotalSize * 100);
275+
}
276+
processedFiles++;
277+
fsProgress.AddProcessedItemsCount(1);
278+
fsProgress.Report();
279+
}
280+
281+
if (!statusCard.CancellationToken.IsCancellationRequested)
282+
{
283+
isSuccess = true;
284+
}
285+
}
286+
catch (Exception ex)
287+
{
288+
isSuccess = false;
289+
Console.WriteLine($"Error during decompression: {ex.Message}");
290+
}
291+
finally
292+
{
293+
// Remove the in-progress status card
294+
StatusCenterViewModel.RemoveItem(statusCard);
295+
296+
if (isSuccess)
297+
{
298+
// Successful
299+
StatusCenterHelper.AddCard_Decompress(
300+
archiveFilePath.CreateEnumerable(),
301+
destinationFolderPath.CreateEnumerable(),
302+
ReturnResult.Success);
303+
}
304+
else
305+
{
306+
// Error
307+
StatusCenterHelper.AddCard_Decompress(
308+
archiveFilePath.CreateEnumerable(),
309+
destinationFolderPath.CreateEnumerable(),
310+
statusCard.CancellationToken.IsCancellationRequested
311+
? ReturnResult.Cancelled
312+
: ReturnResult.Failed);
313+
}
183314

315+
if (zipFile != null)
316+
{
317+
zipFile.IsStreamOwner = true; // Makes close also close the underlying stream
318+
zipFile.Close();
319+
}
320+
}
184321
return isSuccess;
185322
}
186323

324+
187325
/// <inheritdoc/>
188326
public string GenerateArchiveNameFromItems(IReadOnlyList<ListedItem> items)
189327
{
@@ -208,6 +346,25 @@ public async Task<bool> IsEncryptedAsync(string archiveFilePath)
208346
return zipFile.ArchiveFileData.Any(file => file.Encrypted || file.Method.Contains("Crypto") || file.Method.Contains("AES"));
209347
}
210348

349+
/// <inheritdoc/>
350+
public async Task<bool> IsEncodingUndeterminedAsync(string archiveFilePath)
351+
{
352+
if (archiveFilePath is null) return false;
353+
if (Path.GetExtension(archiveFilePath) != ".zip") return false;
354+
try
355+
{
356+
using (ZipFile zipFile = new ZipFile(archiveFilePath))
357+
{
358+
return !zipFile.Cast<ZipEntry>().All(entry=>entry.IsUnicodeText);
359+
}
360+
}
361+
catch (Exception ex)
362+
{
363+
Console.WriteLine($"SharpZipLib error: {ex.Message}");
364+
return true;
365+
}
366+
}
367+
211368
/// <inheritdoc/>
212369
public async Task<SevenZipExtractor?> GetSevenZipExtractorAsync(string archiveFilePath, string password = "")
213370
{

src/Files.App/Strings/en-US/Resources.resw

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2096,6 +2096,9 @@
20962096
<data name="ArchivePassword" xml:space="preserve">
20972097
<value>Archive password</value>
20982098
</data>
2099+
<data name="Encoding" xml:space="preserve">
2100+
<value>Encoding</value>
2101+
</data>
20992102
<data name="ExtractToPath" xml:space="preserve">
21002103
<value>Path</value>
21012104
</data>

0 commit comments

Comments
 (0)